diff --git a/src/main/java/com/example/nexus/app/global/code/status/ErrorStatus.java b/src/main/java/com/example/nexus/app/global/code/status/ErrorStatus.java index 5688a36..1c76ca5 100644 --- a/src/main/java/com/example/nexus/app/global/code/status/ErrorStatus.java +++ b/src/main/java/com/example/nexus/app/global/code/status/ErrorStatus.java @@ -39,6 +39,8 @@ public enum ErrorStatus implements BaseErrorCode { PARTICIPATION_NOT_TEST_COMPLETED(HttpStatus.BAD_REQUEST, "FEEDBACK40025", "테스트 완료 상태에서만 피드백을 제출할 수 있습니다."), FEEDBACK_NOT_SUBMITTED(HttpStatus.BAD_REQUEST, "PARTICIPANT40026", "피드백이 제출되지 않았습니다."), PARTICIPATION_ALREADY_TEST_COMPLETED(HttpStatus.BAD_REQUEST, "PARTICIPANT40027", "이미 테스트 완료 처리된 참여입니다."), + FEEDBACK_NOT_COMPLETED(HttpStatus.BAD_REQUEST, "PARTICIPANT40028", "피드백 완료 상태가 아닙니다."), + PARTICIPATION_ALREADY_FEEDBACK_COMPLETED(HttpStatus.BAD_REQUEST, "PARTICIPANT40029", "이미 피드백 완료 처리된 참여입니다."), // 401 Unauthorized UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH401", "인증이 필요합니다."), diff --git a/src/main/java/com/example/nexus/app/participation/controller/doc/ParticipationControllerDoc.java b/src/main/java/com/example/nexus/app/participation/controller/doc/ParticipationControllerDoc.java index 82e96ca..060ee6c 100644 --- a/src/main/java/com/example/nexus/app/participation/controller/doc/ParticipationControllerDoc.java +++ b/src/main/java/com/example/nexus/app/participation/controller/doc/ParticipationControllerDoc.java @@ -40,17 +40,17 @@ ResponseEntity> applyForParticipation( @Operation( summary = "내 신청 내역 조회", description = """ - 내 참가 신청 내역을 조회합니다. status 파라미터로 필터링 가능 - - **ParticipationStatus (참가 신청 상태):** - - `PENDING`: 승인 대기 - - `APPROVED`: 진행중 - - `COMPLETED`: 지급 대기 - - `REJECTED`: 거절됨 - - `PAID`: 완료 (지급완료) - - status를 입력하지 않으면 전체 조회 - """ + 내 참가 신청 내역을 조회합니다. status 파라미터로 필터링 가능 + + **ParticipationStatus (참가 신청 상태):** + - `PENDING`: 승인 대기 + - `APPROVED`: 진행중 + - `FEEDBACK_COMPLETED`: 피드백 완료 + - `TEST_COMPLETED`: 테스트 완료 + - `REJECTED`: 거절됨 + + status를 입력하지 않으면 전체 조회 + """ ) ResponseEntity>> getMyApplications( @Parameter(description = "참가 상태 (선택사항)", required = false) @@ -110,8 +110,13 @@ ResponseEntity>> getParticipantsPri ); @Operation( - summary = "참여자 완료 처리", - description = "특정 참여자의 테스트를 완료 처리합니다 (게시글 작성자만 가능)" + summary = "참여자 테스트 완료 처리", + description = """ + 모집자가 참여자의 피드백을 확인 후 최종 완료 처리합니다. + - FEEDBACK_COMPLETED(피드백 완료) 상태에서만 호출 가능 + - TEST_COMPLETED(테스트 완료) 상태로 변경 + - 리워드 지급 준비 완료 + """ ) ResponseEntity> completeParticipant( @Parameter(description = "참여 ID", required = true) @@ -122,15 +127,15 @@ ResponseEntity> completeParticipant( @Operation( summary = "게시글 참가 신청 통계", description = """ - 게시글의 상태별 참가 신청자 인원 통계를 조회합니다. (작성자만 가능) - - - pendingCount: 승인 대기 인원 - - approvedCount: 진행중 인원 - - completedCount: 완료 (지급 대기) 인원 - - paidCount: 지급 완료 인원 - - rejectedCount: 거절됨 인원 - - totalCount: 전체 신청 인원 - """ + 게시글의 상태별 참가 신청자 인원 통계를 조회합니다. (작성자만 가능) + + - pendingCount: 승인 대기 인원 + - approvedCount: 진행중 인원 + - feedbackCompletedCount: 피드백 완료 인원 + - testCompletedCount: 테스트 완료 인원 + - rejectedCount: 거절됨 인원 + - totalCount: 전체 신청 인원 + """ ) ResponseEntity> getPostApplicationStatistics( @Parameter(description = "게시글 ID", required = true) @@ -141,22 +146,22 @@ ResponseEntity> getPostApplicationS @Operation( summary = "게시글 참여자 목록 조회", description = """ - 특정 게시글의 참여자 목록 조회 (페이징, 필터링, 정렬, 검색 지원) - - **필터링 가능 항목:** - - status (참여자 상태) - - `PENDING`: 승인 대기 - - `APPROVED`: 진행중 - - `COMPLETED`: 테스트 완료 - - `PAID`: 지급 완료 - - `REJECTED`: 거절됨 - - null: 전체 - - searchKeyword: 닉네임 또는 이메일 검색 - - **정렬:** - - sortDirection: `ASC` (오래된순), `DESC` (최신순, 기본값) - - 항상 신청일시(appliedAt) 기준으로 정렬 - """ + 특정 게시글의 참여자 목록 조회 (페이징, 필터링, 정렬, 검색 지원) + + **필터링 가능 항목:** + - status (참여자 상태) + - `PENDING`: 승인 대기 + - `APPROVED`: 진행중 + - `FEEDBACK_COMPLETED`: 피드백 완료 + - `TEST_COMPLETED`: 테스트 완료 + - `REJECTED`: 거절됨 + - null: 전체 + - searchKeyword: 닉네임 또는 이메일 검색 + + **정렬:** + - sortDirection: `ASC` (오래된순), `DESC` (최신순, 기본값) + - 항상 신청일시(appliedAt) 기준으로 정렬 + """ ) ResponseEntity>> getParticipants( @Parameter(description = "게시글 ID", required = true) @@ -177,13 +182,13 @@ ResponseEntity> getParticipantDetail( ); @Operation( - summary = "참여자 테스트 완료 처리", + summary = "참여자 피드백 완료 처리", description = """ - 참여자 본인이 테스트를 완료 처리합니다. - - APPROVED(진행중) 상태에서만 호출 가능 - - TEST_COMPLETED(테스트 완료) 상태로 변경 - - 이후 피드백을 제출해야 함 - """ + 참여자 본인이 피드백 제출을 완료 처리합니다. + - APPROVED(진행중) 상태에서만 호출 가능 + - FEEDBACK_COMPLETED(피드백 완료) 상태로 변경 + - 피드백 제출 후 호출해야 함 + """ ) ResponseEntity> completeTestByParticipant( @Parameter(description = "참가 신청 ID", required = true) diff --git a/src/main/java/com/example/nexus/app/participation/controller/dto/response/ParticipationStatisticsResponse.java b/src/main/java/com/example/nexus/app/participation/controller/dto/response/ParticipationStatisticsResponse.java index 5df4daa..8392dd2 100644 --- a/src/main/java/com/example/nexus/app/participation/controller/dto/response/ParticipationStatisticsResponse.java +++ b/src/main/java/com/example/nexus/app/participation/controller/dto/response/ParticipationStatisticsResponse.java @@ -4,33 +4,33 @@ @Schema(description = "참가 신청 통계") public record ParticipationStatisticsResponse( - @Schema(description = "승인 대기 인원") + @Schema(description = "승인 대기 인원", example = "5") Long pendingCount, @Schema(description = "진행중 인원", example = "10") Long approvedCount, - @Schema(description = "완료 (지급 대기) 인원", example = "8") - Long completedCount, + @Schema(description = "피드백 완료 인원", example = "7") + Long feedbackCompletedCount, - @Schema(description = "지급 완료 인원", example = "15") - Long paidCount, + @Schema(description = "테스트 완료 인원", example = "8") + Long testCompletedCount, @Schema(description = "거절됨 인원", example = "3") Long rejectedCount, - @Schema(description = "전체 신청 인원", example = "41") + @Schema(description = "전체 신청 인원", example = "33") Long totalCount ) { - public static ParticipationStatisticsResponse of (Long pendingCount, Long approvedCount, - Long completedCount, Long paidCount, - Long rejectedCount) { - Long total = pendingCount + approvedCount + completedCount + paidCount + rejectedCount; + public static ParticipationStatisticsResponse of(Long pendingCount, Long approvedCount, + Long feedbackCompletedCount, Long testCompletedCount, + Long rejectedCount) { + Long total = pendingCount + approvedCount + feedbackCompletedCount + testCompletedCount + rejectedCount; return new ParticipationStatisticsResponse( pendingCount, approvedCount, - completedCount, - paidCount, + feedbackCompletedCount, + testCompletedCount, rejectedCount, total ); diff --git a/src/main/java/com/example/nexus/app/participation/domain/Participation.java b/src/main/java/com/example/nexus/app/participation/domain/Participation.java index 6427db9..a94307a 100644 --- a/src/main/java/com/example/nexus/app/participation/domain/Participation.java +++ b/src/main/java/com/example/nexus/app/participation/domain/Participation.java @@ -131,14 +131,23 @@ public void reject() { this.approvedAt = LocalDateTime.now(); } - // 모집자가 테스터 피드백까지 받은 후 최종 완료 처리 상태 + public void completeTest() { + if (!isApproved()) { + throw new GeneralException(ErrorStatus.PARTICIPATION_NOT_APPROVED); + } + this.status = ParticipationStatus.FEEDBACK_COMPLETED; + } + public void complete() { - this.status = ParticipationStatus.COMPLETED; + if (!isFeedbackCompleted()) { + throw new GeneralException(ErrorStatus.FEEDBACK_NOT_COMPLETED); + } + this.status = ParticipationStatus.TEST_COMPLETED; this.completedAt = LocalDateTime.now(); } public void updatePaidStatus(LocalDateTime paidAt) { - if (!isCompleted()) { + if (!isTestCompleted()) { throw new GeneralException(ErrorStatus.NOT_COMPLETED_YET); } if (isPaid()) { @@ -160,23 +169,15 @@ public boolean isRejected() { return this.status == ParticipationStatus.REJECTED; } - public boolean isCompleted() { - return this.status == ParticipationStatus.COMPLETED; - } - - public boolean isPaid() { - return this.isPaid; - } - - // 참여자의 테스트 참여 완료 처리 (피드백 제출) - public void completeTest() { - if (!isApproved()) { - throw new GeneralException(ErrorStatus.PARTICIPATION_NOT_APPROVED); - } - this.status = ParticipationStatus.TEST_COMPLETED; + public boolean isFeedbackCompleted() { + return this.status == ParticipationStatus.FEEDBACK_COMPLETED; } public boolean isTestCompleted() { return this.status == ParticipationStatus.TEST_COMPLETED; } + + public boolean isPaid() { + return this.isPaid; + } } diff --git a/src/main/java/com/example/nexus/app/participation/domain/ParticipationStatus.java b/src/main/java/com/example/nexus/app/participation/domain/ParticipationStatus.java index 98d5b51..828cf30 100644 --- a/src/main/java/com/example/nexus/app/participation/domain/ParticipationStatus.java +++ b/src/main/java/com/example/nexus/app/participation/domain/ParticipationStatus.java @@ -14,14 +14,12 @@ public enum ParticipationStatus { @Schema(description = "진행중") APPROVED("진행중"), - // 참여자의 테스트 완료 (피드백 제출) + @Schema(description = "피드백 완료") + FEEDBACK_COMPLETED("피드백 완료"), + @Schema(description = "테스트 완료") TEST_COMPLETED("테스트 완료"), - // 모집자가 확인 후 최종 테스트 완료 및 지급 대기 상태 - @Schema(description = "지급 대기") - COMPLETED("지급 대기"), - @Schema(description = "거절됨") REJECTED("거절됨"); diff --git a/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepository.java b/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepository.java index 59d68c8..377abe8 100644 --- a/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepository.java +++ b/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepository.java @@ -163,9 +163,8 @@ Long countByPostIdAndStatusAndIsPaid( "SELECT " + "COALESCE(COUNT(CASE WHEN status = 'PENDING' THEN 1 END), 0) as pendingCount, " + "COALESCE(COUNT(CASE WHEN status = 'APPROVED' THEN 1 END), 0) as approvedCount, " + - "COALESCE(COUNT(CASE WHEN status = 'COMPLETED' AND is_paid = false THEN 1 END), 0) as completedCount, " - + - "COALESCE(COUNT(CASE WHEN status = 'COMPLETED' AND is_paid = true THEN 1 END), 0) as paidCount, " + + "COALESCE(COUNT(CASE WHEN status = 'FEEDBACK_COMPLETED' THEN 1 END), 0) as feedbackCompletedCount, " + + "COALESCE(COUNT(CASE WHEN status = 'TEST_COMPLETED' THEN 1 END), 0) as testCompletedCount, " + "COALESCE(COUNT(CASE WHEN status = 'REJECTED' THEN 1 END), 0) as rejectedCount " + "FROM participations " + "WHERE post_id = :postId", diff --git a/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepositoryImpl.java b/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepositoryImpl.java index c641ce1..9b936d9 100644 --- a/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepositoryImpl.java +++ b/src/main/java/com/example/nexus/app/participation/repository/ParticipationRepositoryImpl.java @@ -90,7 +90,7 @@ private BooleanExpression statusCondition(String status) { } if (PAID_STATUS.equalsIgnoreCase(status)) { - return participation.status.eq(ParticipationStatus.COMPLETED) + return participation.status.eq(ParticipationStatus.TEST_COMPLETED) .and(participation.isPaid.isTrue()); } diff --git a/src/main/java/com/example/nexus/app/participation/service/ParticipationService.java b/src/main/java/com/example/nexus/app/participation/service/ParticipationService.java index 1c97ec3..ced47a5 100644 --- a/src/main/java/com/example/nexus/app/participation/service/ParticipationService.java +++ b/src/main/java/com/example/nexus/app/participation/service/ParticipationService.java @@ -97,12 +97,8 @@ public Page getMyApplications(Long userId, String Page participations; - // status가 없으면 전체 조회 if (statusParam == null || statusParam.isEmpty()) { participations = participationRepository.findByUserIdWithPost(userId, pageable); - } else if ("PAID".equalsIgnoreCase(statusParam)) { - participations = participationRepository.findByUserIdAndStatusAndIsPaidWithPost( - userId, ParticipationStatus.COMPLETED, true, pageable); } else { ParticipationStatus status = ParticipationStatus.valueOf(statusParam.toUpperCase()); participations = participationRepository.findByUserIdAndStatusWithPost(userId, status, pageable); @@ -196,7 +192,7 @@ public void completeByParticipant(Long participationId, Long userId) { Participation participation = getParticipation(participationId); validateParticipantOwnership(participation, userId); - validateNotAlreadyTestCompleted(participation); + validateNotAlreadyFeedbackCompleted(participation); participation.completeTest(); } @@ -251,8 +247,8 @@ public ParticipationStatisticsResponse getPostApplicationStatistics(Long postId, return ParticipationStatisticsResponse.of( stats.pendingCount(), stats.approvedCount(), - stats.completedCount(), - stats.paidCount(), + stats.feedbackCompletedCount(), + stats.testCompletedCount(), stats.rejectedCount() ); } @@ -390,15 +386,21 @@ private ParticipationStatsDto extractParticipationStats(Long postId) { Object[] result = resultList.get(0); return new ParticipationStatsDto( - extractLongValue(result[0]), - extractLongValue(result[1]), - extractLongValue(result[2]), - extractLongValue(result[3]), - extractLongValue(result[4]) + extractLongValue(result[0]), // pendingCount + extractLongValue(result[1]), // approvedCount + extractLongValue(result[2]), // feedbackCompletedCount + extractLongValue(result[3]), // testCompletedCount + extractLongValue(result[4]) // rejectedCount ); } private Long extractLongValue(Object value) { return value != null ? ((Number) value).longValue() : 0L; } + + private void validateNotAlreadyFeedbackCompleted(Participation participation) { + if (participation.isFeedbackCompleted() || participation.isTestCompleted()) { + throw new GeneralException(ErrorStatus.PARTICIPATION_ALREADY_FEEDBACK_COMPLETED); + } + } } diff --git a/src/main/java/com/example/nexus/app/participation/service/dto/ParticipationStatsDto.java b/src/main/java/com/example/nexus/app/participation/service/dto/ParticipationStatsDto.java index 1458d60..e0b4dbb 100644 --- a/src/main/java/com/example/nexus/app/participation/service/dto/ParticipationStatsDto.java +++ b/src/main/java/com/example/nexus/app/participation/service/dto/ParticipationStatsDto.java @@ -3,8 +3,8 @@ public record ParticipationStatsDto( Long pendingCount, Long approvedCount, - Long completedCount, - Long paidCount, + Long feedbackCompletedCount, + Long testCompletedCount, Long rejectedCount ) { } diff --git a/src/main/java/com/example/nexus/app/reward/domain/ParticipantReward.java b/src/main/java/com/example/nexus/app/reward/domain/ParticipantReward.java index 2e7b702..2becca4 100644 --- a/src/main/java/com/example/nexus/app/reward/domain/ParticipantReward.java +++ b/src/main/java/com/example/nexus/app/reward/domain/ParticipantReward.java @@ -69,7 +69,7 @@ public static ParticipantReward create(Participation participation, PostReward p } public void markAsCompleted() { - this.completionStatus = ParticipationStatus.COMPLETED; + this.completionStatus = ParticipationStatus.TEST_COMPLETED; this.completedAt = LocalDateTime.now(); this.rewardStatus = RewardStatus.PENDING; } @@ -80,7 +80,7 @@ public void markAsPaid() { } public boolean isCompleted() { - return this.completionStatus == ParticipationStatus.COMPLETED; + return this.completionStatus == ParticipationStatus.TEST_COMPLETED; } public boolean isRewardPaid() {