Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,17 @@ dependencies {
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudAwsVersion}")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'

// 파일의 맀직 λ°”μ΄νŠΈ κ²€μ‚¬μš© Apache Tika 라이브러리
implementation 'org.apache.tika:tika-core:3.2.2'

// queryDsl
implementation "com.querydsl:querydsl-core:${queryDslVersion}"
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor (
"com.querydsl:querydsl-apt:${queryDslVersion}:jakarta",
'jakarta.annotation:jakarta.annotation-api',
// JPA 메타λͺ¨λΈ μƒμ„±μš©
"jakarta.persistence:jakarta.persistence-api:3.1.0"
'jakarta.persistence:jakarta.persistence-api'
)

// db
Expand Down Expand Up @@ -117,12 +121,18 @@ dependencies {
/** Q 클래슀 생성 경둜 μ§€μ • **/
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

// gradle clean μ‹œμ— QClass 디렉토리 μ‚­μ œ
clean {
delete file(querydslDir)
}

// πŸ”§ μ†ŒμŠ€μ…‹μ— generated 디렉토리 μΆ”κ°€
sourceSets {
main { java.srcDirs += querydslDir }
main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile).configureEach {
options.annotationProcessorGeneratedSourcesDirectory = querydslDir
options.generatedSourceOutputDirectory.set(querydslDir)
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.teamEWSN.gitdeun.Application.controller;

import com.teamEWSN.gitdeun.Application.dto.*;
import com.teamEWSN.gitdeun.Application.service.ApplicationService;
import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Tag(name = "Application", description = "λͺ¨μ§‘ 곡고 지원 κ΄€λ ¨ API")
public class ApplicationController {

private final ApplicationService applicationService;

/**
* λͺ¨μ§‘ 곡고에 μ§€μ›ν•˜κΈ°
*/
@PostMapping("/recruitments/{recruitmentId}/applications")
@Operation(summary = "λͺ¨μ§‘ 곡고 지원", description = "νŠΉμ • λͺ¨μ§‘ 곡고에 μ§€μ›ν•©λ‹ˆλ‹€.")
public ResponseEntity<ApplicationResponseDto> createApplication(
@PathVariable Long recruitmentId,
@Valid @RequestBody ApplicationCreateRequestDto requestDto,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
log.info("Creating application for recruitment: {} by user: {}", recruitmentId, userDetails.getId());
ApplicationResponseDto response = applicationService.createApplication(
recruitmentId, requestDto, userDetails.getId()
);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

/**
* λ‚΄ 지원 λͺ©λ‘ 쑰회
*/
@GetMapping("/users/me/applications")
@Operation(summary = "λ‚΄ 지원 λͺ©λ‘ 쑰회", description = "λ‘œκ·ΈμΈν•œ μ‚¬μš©μžμ˜ 지원 λͺ©λ‘μ„ μ‘°νšŒν•©λ‹ˆλ‹€.")
public ResponseEntity<Page<ApplicationListResponseDto>> getMyApplications(
@PageableDefault(size = 10, sort = "createdAt,desc") Pageable pageable,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
log.info("Getting applications for user: {}", userDetails.getId());
Page<ApplicationListResponseDto> applications = applicationService.getMyApplications(
userDetails.getId(), pageable
);
return ResponseEntity.ok(applications);
}

/**
* νŠΉμ • 곡고의 μ§€μ›μž λͺ©λ‘ 쑰회 (λͺ¨μ§‘μžλ§Œ κ°€λŠ₯)
*/
@GetMapping("/recruitments/{recruitmentId}/applications")
@Operation(summary = "곡고 μ§€μ›μž λͺ©λ‘ 쑰회", description = "λͺ¨μ§‘μžκ°€ μžμ‹ μ˜ 곡고에 μ§€μ›ν•œ μ§€μ›μž λͺ©λ‘μ„ μ‘°νšŒν•©λ‹ˆλ‹€.")
public ResponseEntity<Page<ApplicationListResponseDto>> getRecruitmentApplications(
@PathVariable Long recruitmentId,
@PageableDefault(size = 10, sort = "createdAt,desc") Pageable pageable,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
log.info("Getting applications for recruitment: {} by user: {}", recruitmentId, userDetails.getId());
Page<ApplicationListResponseDto> applications = applicationService.getRecruitmentApplications(
recruitmentId, userDetails.getId(), pageable
);
return ResponseEntity.ok(applications);
}

/**
* 지원 상세 쑰회
*/
@GetMapping("/applications/{applicationId}")
@Operation(summary = "지원 상세 쑰회", description = "νŠΉμ • μ§€μ›μ˜ 상세 정보λ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.")
public ResponseEntity<ApplicationResponseDto> getApplication(
@PathVariable Long applicationId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
log.info("Getting application: {} by user: {}", applicationId, userDetails.getId());
ApplicationResponseDto application = applicationService.getApplication(
applicationId, userDetails.getId()
);
return ResponseEntity.ok(application);
}

/**
* 지원 철회 (μ§€μ›μžλ§Œ κ°€λŠ₯)
*/
@PatchMapping("/applications/{applicationId}/withdraw")
@Operation(summary = "지원 철회", description = "본인의 지원을 μ² νšŒν•©λ‹ˆλ‹€.")
public ResponseEntity<Void> withdrawApplication(
@PathVariable Long applicationId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
log.info("Withdrawing application: {} by user: {}", applicationId, userDetails.getId());
applicationService.withdrawApplication(applicationId, userDetails.getId());
return ResponseEntity.noContent().build();
}

/**
* 지원 수락 (λͺ¨μ§‘μžλ§Œ κ°€λŠ₯)
*/
@PatchMapping("/applications/{applicationId}/accept")
@Operation(summary = "지원 수락", description = "λͺ¨μ§‘μžκ°€ 지원을 μˆ˜λ½ν•©λ‹ˆλ‹€.")
public ResponseEntity<ApplicationResponseDto> acceptApplication(
@PathVariable Long applicationId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
log.info("Accepting application: {} by recruiter: {}", applicationId, userDetails.getId());
ApplicationResponseDto response = applicationService.acceptApplication(
applicationId, userDetails.getId()
);
return ResponseEntity.ok(response);
}

/**
* 지원 거절 (λͺ¨μ§‘μžλ§Œ κ°€λŠ₯)
*/
@PatchMapping("/applications/{applicationId}/reject")
@Operation(summary = "지원 거절", description = "λͺ¨μ§‘μžκ°€ 지원을 κ±°μ ˆν•©λ‹ˆλ‹€.")
public ResponseEntity<ApplicationResponseDto> rejectApplication(
@PathVariable Long applicationId,
@Valid @RequestBody ApplicationStatusUpdateDto updateDto,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
log.info("Rejecting application: {} by recruiter: {}", applicationId, userDetails.getId());
ApplicationResponseDto response = applicationService.rejectApplication(
applicationId, userDetails.getId(), updateDto
);
return ResponseEntity.ok(response);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.teamEWSN.gitdeun.Application.dto;

import com.teamEWSN.gitdeun.Recruitment.entity.RecruitmentField;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApplicationCreateRequestDto {

@NotNull(message = "지원 λΆ„μ•Όλ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”.")
private RecruitmentField appliedField;

@Size(max = 1000, message = "지원 λ©”μ‹œμ§€λŠ” 1000자 μ΄λ‚΄λ‘œ μž‘μ„±ν•΄μ£Όμ„Έμš”.")
private String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.teamEWSN.gitdeun.Application.dto;

import com.teamEWSN.gitdeun.Application.entity.ApplicationStatus;
import com.teamEWSN.gitdeun.Recruitment.entity.RecruitmentField;
import lombok.*;

import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApplicationListResponseDto {

private Long applicationId;

// μ§€μ›μž κ°„λž΅ 정보
private String applicantName;
private String applicantNickname;
private String applicantProfileImage;

// 곡고 κ°„λž΅ 정보
private String recruitmentTitle;

// 지원 정보
private RecruitmentField appliedField;
private ApplicationStatus status;
private LocalDateTime createdAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.teamEWSN.gitdeun.Application.dto;

import com.teamEWSN.gitdeun.Application.entity.ApplicationStatus;
import com.teamEWSN.gitdeun.Recruitment.entity.RecruitmentField;
import lombok.*;

import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApplicationResponseDto {

private Long applicationId;

// μ§€μ›μž 정보
private Long applicantId;
private String applicantName;
private String applicantEmail;
private String applicantNickname;
private String applicantProfileImage;

// 곡고 정보
private Long recruitmentId;
private String recruitmentTitle;
private String recruiterName;

// 지원 정보
private RecruitmentField appliedField;
private String message;
private ApplicationStatus status;
private String rejectReason;
private boolean active;

// λ‚ μ§œ 정보
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.teamEWSN.gitdeun.Application.dto;

import jakarta.validation.constraints.Size;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApplicationStatusUpdateDto {

@Size(max = 500, message = "거절 μ‚¬μœ λŠ” 500자 μ΄λ‚΄λ‘œ μž‘μ„±ν•΄μ£Όμ„Έμš”.")
private String rejectReason;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.teamEWSN.gitdeun.Application.mapper;

import com.teamEWSN.gitdeun.Application.dto.ApplicationListResponseDto;
import com.teamEWSN.gitdeun.Application.dto.ApplicationResponseDto;
import com.teamEWSN.gitdeun.Application.entity.Application;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ApplicationMapper {

@Mapping(source = "id", target = "applicationId")
@Mapping(source = "applicant.id", target = "applicantId")
@Mapping(source = "applicant.name", target = "applicantName")
@Mapping(source = "applicant.email", target = "applicantEmail")
@Mapping(source = "applicant.nickname", target = "applicantNickname")
@Mapping(source = "applicant.profileImage", target = "applicantProfileImage")
@Mapping(source = "recruitment.id", target = "recruitmentId")
@Mapping(source = "recruitment.title", target = "recruitmentTitle")
@Mapping(source = "recruitment.recruiter.name", target = "recruiterName")
ApplicationResponseDto toResponseDto(Application application);

@Mapping(source = "id", target = "applicationId")
@Mapping(source = "applicant.name", target = "applicantName")
@Mapping(source = "applicant.nickname", target = "applicantNickname")
@Mapping(source = "applicant.profileImage", target = "applicantProfileImage")
@Mapping(source = "recruitment.title", target = "recruitmentTitle")
ApplicationListResponseDto toListResponseDto(Application application);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.teamEWSN.gitdeun.Application.repository;

import com.teamEWSN.gitdeun.Application.entity.Application;
import com.teamEWSN.gitdeun.Application.entity.ApplicationStatus;
import com.teamEWSN.gitdeun.Recruitment.entity.Recruitment;
import com.teamEWSN.gitdeun.user.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface ApplicationRepository extends JpaRepository<Application, Long> {

// νŠΉμ • μ‚¬μš©μžμ˜ λͺ¨λ“  지원 λ‚΄μ—­ 쑰회 (νŽ˜μ΄μ§•)
Page<Application> findByApplicantOrderByCreatedAtDesc(User applicant, Pageable pageable);

// νŠΉμ • μ‚¬μš©μžμ˜ ν™œμ„± 지원 λ‚΄μ—­λ§Œ 쑰회
Page<Application> findByApplicantAndActiveTrueOrderByCreatedAtDesc(User applicant, Pageable pageable);

// νŠΉμ • 곡고의 λͺ¨λ“  μ§€μ›μž λͺ©λ‘ 쑰회 (νŽ˜μ΄μ§•)
Page<Application> findByRecruitmentOrderByCreatedAtDesc(Recruitment recruitment, Pageable pageable);

// νŠΉμ • 곡고의 ν™œμ„± μ§€μ›μžλ§Œ 쑰회
Page<Application> findByRecruitmentAndActiveTrueOrderByCreatedAtDesc(Recruitment recruitment, Pageable pageable);

// νŠΉμ • 곡고의 μƒνƒœλ³„ μ§€μ›μž 쑰회
List<Application> findByRecruitmentAndStatusAndActiveTrue(Recruitment recruitment, ApplicationStatus status);

// μ‚¬μš©μžκ°€ νŠΉμ • 곡고에 이미 μ§€μ›ν–ˆλŠ”μ§€ 확인 (ν™œμ„± μ§€μ›λ§Œ)
boolean existsByRecruitmentAndApplicantAndActiveTrue(Recruitment recruitment, User applicant);

// νŠΉμ • 지원 쑰회 (μ§€μ›μž 본인 ν™•μΈμš©)
Optional<Application> findByIdAndApplicant(Long id, User applicant);

// νŠΉμ • 지원 쑰회 (곡고 μž‘μ„±μž ν™•μΈμš©)
@Query("SELECT a FROM Application a WHERE a.id = :id AND a.recruitment.recruiter = :recruiter")
Optional<Application> findByIdAndRecruiter(@Param("id") Long id, @Param("recruiter") User recruiter);

// νŠΉμ • μ‚¬μš©μžμ™€ 곡고의 ν™œμ„± 지원 쑰회
Optional<Application> findByRecruitmentAndApplicantAndActiveTrue(Recruitment recruitment, User applicant);

// 곡고별 μƒνƒœλ³„ μ§€μ›μž 수 톡계
@Query("SELECT a.status, COUNT(a) FROM Application a " +
"WHERE a.recruitment = :recruitment AND a.active = true " +
"GROUP BY a.status")
List<Object[]> countByRecruitmentGroupByStatus(@Param("recruitment") Recruitment recruitment);

// νŠΉμ • 곡고의 수락된 μ§€μ›μž 수
long countByRecruitmentAndStatusAndActiveTrue(Recruitment recruitment, ApplicationStatus status);

}
Loading