diff --git a/src/main/kotlin/picklab/backend/archive/application/ArchiveUseCase.kt b/src/main/kotlin/picklab/backend/archive/application/ArchiveUseCase.kt index 32e611be..b5e42ac8 100644 --- a/src/main/kotlin/picklab/backend/archive/application/ArchiveUseCase.kt +++ b/src/main/kotlin/picklab/backend/archive/application/ArchiveUseCase.kt @@ -3,7 +3,6 @@ package picklab.backend.archive.application import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import picklab.backend.activity.domain.enums.ActivityType -import picklab.backend.activity.domain.service.ActivityService import picklab.backend.archive.domain.entity.ArchiveReferenceUrl import picklab.backend.archive.domain.entity.ArchiveUploadFileUrl import picklab.backend.archive.domain.enums.ArchiveSortType @@ -12,17 +11,19 @@ import picklab.backend.archive.domain.service.ArchiveService import picklab.backend.archive.domain.service.ArchiveUploadFileUrlService import picklab.backend.archive.entrypoint.request.ArchiveCreateRequest import picklab.backend.archive.entrypoint.request.ArchiveRecordUpdateRequest -import picklab.backend.archive.entrypoint.request.ArchiveStatusUpdateRequest import picklab.backend.archive.entrypoint.response.ArchiveActivityResponse +import picklab.backend.common.model.BusinessException +import picklab.backend.common.model.ErrorCode import picklab.backend.common.model.MemberPrincipal import picklab.backend.file.application.FileManagementService import picklab.backend.member.domain.MemberService +import picklab.backend.participation.domain.service.ActivityParticipationService @Component class ArchiveUseCase( private val memberService: MemberService, private val archiveService: ArchiveService, - private val activityService: ActivityService, + private val participationService: ActivityParticipationService, private val archiveReferenceUrlService: ArchiveReferenceUrlService, private val archiveUploadFileUrlService: ArchiveUploadFileUrlService, private val fileManagementService: FileManagementService, @@ -33,17 +34,23 @@ class ArchiveUseCase( memberPrincipal: MemberPrincipal, ) { val member = memberService.findActiveMember(memberPrincipal.memberId) - val activity = activityService.mustFindById(request.activityId) + val participation = participationService.mustFindByIdAndMemberId(request.participationId, member.id) + if (!participation.canArchive()) { + throw BusinessException(ErrorCode.CANNOT_CREATE_ARCHIVE) + } + if (archiveService.existsActiveByParticipationId(participation.id)) { + throw BusinessException(ErrorCode.ALREADY_EXISTS_ARCHIVE) + } val permanentFileUrls = fileManagementService.verifyAndMoveTempFilesToPermanent( fileUrls = request.fileUrls, memberId = member.id, - activityId = request.activityId, + activityId = participation.activity.id, category = "archive", ) - val entity = request.toCreateEntity(member, activity) + val entity = request.toCreateEntity(participation) val archive = archiveService.save(entity) val referenceUrls = request.referenceUrls.map { url -> ArchiveReferenceUrl(archive, url) } @@ -53,23 +60,6 @@ class ArchiveUseCase( archiveUploadFileUrlService.saveAll(uploadedFileUrls) } - @Transactional - fun updateArchiveStatus( - archiveId: Long, - request: ArchiveStatusUpdateRequest, - memberPrincipal: MemberPrincipal, - ) { - val member = memberService.findActiveMember(memberPrincipal.memberId) - val archive = archiveService.mustFindByIdAndMember(archiveId, member) - - archive.update( - activityProgressStatus = request.activityProgressStatus, - passOrFailStatus = request.passOrFailStatus, - ) - - archiveService.save(archive) - } - @Transactional fun updateArchiveRecord( archiveId: Long, @@ -83,7 +73,7 @@ class ArchiveUseCase( fileManagementService.processUpdatedFileUrls( fileUrls = request.fileUrls, memberId = member.id, - activityId = archive.activity.id, + activityId = archive.participation.activity.id, category = "archive", ) @@ -111,8 +101,22 @@ class ArchiveUseCase( memberPrincipal: MemberPrincipal, ): List { val member = memberService.findActiveMember(memberPrincipal.memberId) - return archiveService - .findCompletedArchives(member, activityType, sort) - .map { ArchiveActivityResponse.from(it) } + val participations = + participationService.findCompletedForArchive( + memberId = member.id, + activityType = activityType, + sort = sort.toSort(), + ) + val archivesByParticipationId = + archiveService + .findAllByParticipationIds(participations.map { it.id }) + .associateBy { it.participation.id } + + return participations.map { participation -> + ArchiveActivityResponse.from( + participation = participation, + archive = archivesByParticipationId[participation.id], + ) + } } } diff --git a/src/main/kotlin/picklab/backend/archive/domain/entity/Archive.kt b/src/main/kotlin/picklab/backend/archive/domain/entity/Archive.kt index 4d9ae10c..90e10e7b 100644 --- a/src/main/kotlin/picklab/backend/archive/domain/entity/Archive.kt +++ b/src/main/kotlin/picklab/backend/archive/domain/entity/Archive.kt @@ -12,15 +12,11 @@ import jakarta.persistence.Table import org.hibernate.annotations.Comment import org.hibernate.annotations.SQLDelete import org.hibernate.annotations.SQLRestriction -import picklab.backend.activity.domain.entity.Activity -import picklab.backend.activity.domain.enums.ActivityType import picklab.backend.archive.domain.enums.DetailRoleType -import picklab.backend.archive.domain.enums.PassOrFailStatus -import picklab.backend.archive.domain.enums.ProgressStatus import picklab.backend.archive.domain.enums.RoleType import picklab.backend.archive.domain.enums.WriteStatus import picklab.backend.common.model.SoftDeleteEntity -import picklab.backend.member.domain.entity.Member +import picklab.backend.participation.domain.entity.ActivityParticipation import java.time.LocalDate @Entity @@ -35,23 +31,17 @@ class Archive( @Comment("활동 종료일") var userEndDate: LocalDate, @Column(name = "role", length = 50, nullable = false) + @Enumerated(EnumType.STRING) @Comment("활동 역할") var role: RoleType, @Column(name = "detail_role", length = 50, nullable = false) + @Enumerated(EnumType.STRING) @Comment("상세 역할") var detailRole: DetailRoleType, @Lob @Column(name = "activity_record", nullable = false, columnDefinition = "TEXT") @Comment("활동 기록") var activityRecord: String, - @Column(name = "activity_type", length = 50, nullable = false) - @Enumerated(EnumType.STRING) - @Comment("활동 구분") - var activityType: ActivityType, - @Column(name = "activity_progress_status", length = 50, nullable = false) - @Enumerated(EnumType.STRING) - @Comment("활동 진행 상태") - var activityProgressStatus: ProgressStatus, @Column(name = "write_status", length = 50, nullable = false) @Enumerated(EnumType.STRING) @Comment("작성 상태") @@ -60,24 +50,9 @@ class Archive( @Comment("상세 역할에서 기타를 선택하여 직접 입력한 역할") var customRole: String? = null, @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - val member: Member, - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "activity_id", nullable = false) - val activity: Activity, - @Comment("합불 여부") - @Enumerated(EnumType.STRING) - @Column(name = "pass_or_fail_status", nullable = false) - var passOrFailStatus: PassOrFailStatus = PassOrFailStatus.FAIL, + @JoinColumn(name = "participation_id", nullable = false) + val participation: ActivityParticipation, ) : SoftDeleteEntity() { - fun update( - activityProgressStatus: ProgressStatus = this.activityProgressStatus, - passOrFailStatus: PassOrFailStatus = this.passOrFailStatus, - ) { - this.activityProgressStatus = activityProgressStatus - this.passOrFailStatus = passOrFailStatus - } - fun updateRecord( activityRecord: String, role: RoleType, diff --git a/src/main/kotlin/picklab/backend/archive/domain/enums/PassOrFailStatus.kt b/src/main/kotlin/picklab/backend/archive/domain/enums/PassOrFailStatus.kt deleted file mode 100644 index 36d75222..00000000 --- a/src/main/kotlin/picklab/backend/archive/domain/enums/PassOrFailStatus.kt +++ /dev/null @@ -1,6 +0,0 @@ -package picklab.backend.archive.domain.enums - -enum class PassOrFailStatus { - PASS, - FAIL, -} diff --git a/src/main/kotlin/picklab/backend/archive/domain/enums/ProgressStatus.kt b/src/main/kotlin/picklab/backend/archive/domain/enums/ProgressStatus.kt deleted file mode 100644 index 31290e46..00000000 --- a/src/main/kotlin/picklab/backend/archive/domain/enums/ProgressStatus.kt +++ /dev/null @@ -1,9 +0,0 @@ -package picklab.backend.archive.domain.enums - -enum class ProgressStatus( - val label: String, -) { - IN_PROGRESSING("진행중"), - COMPLETED("수료 완료"), - DROPPED("중도 포기"), -} diff --git a/src/main/kotlin/picklab/backend/archive/domain/repository/ArchiveRepository.kt b/src/main/kotlin/picklab/backend/archive/domain/repository/ArchiveRepository.kt index 5f04a1dc..98a8c386 100644 --- a/src/main/kotlin/picklab/backend/archive/domain/repository/ArchiveRepository.kt +++ b/src/main/kotlin/picklab/backend/archive/domain/repository/ArchiveRepository.kt @@ -1,41 +1,25 @@ package picklab.backend.archive.domain.repository -import org.springframework.data.domain.Sort 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 picklab.backend.activity.domain.enums.ActivityType import picklab.backend.archive.domain.entity.Archive -import picklab.backend.archive.domain.enums.ProgressStatus import picklab.backend.member.domain.entity.Member @Repository interface ArchiveRepository : JpaRepository { - fun findByIdAndMember( + fun findByIdAndParticipationMember( id: Long, member: Member, ): Archive? - fun existsByActivityIdAndMemberIdAndDeletedAtIsNull( + fun findByParticipationId(participationId: Long): Archive? + + fun existsByParticipationActivityIdAndParticipationMemberIdAndDeletedAtIsNull( activityId: Long, memberId: Long, ): Boolean - @Query("SELECT a FROM Archive a JOIN FETCH a.activity WHERE a.member = :member AND a.activityProgressStatus = :status") - fun findByMemberAndProgressStatus( - @Param("member") member: Member, - @Param("status") status: ProgressStatus, - sort: Sort, - ): List + fun existsByParticipationIdAndDeletedAtIsNull(participationId: Long): Boolean - @Query( - "SELECT a FROM Archive a JOIN FETCH a.activity WHERE a.member = :member AND a.activityProgressStatus = :status AND a.activityType = :activityType", - ) - fun findByMemberAndProgressStatusAndActivityType( - @Param("member") member: Member, - @Param("status") status: ProgressStatus, - @Param("activityType") activityType: ActivityType, - sort: Sort, - ): List + fun findAllByParticipationIdIn(participationIds: Collection): List } diff --git a/src/main/kotlin/picklab/backend/archive/domain/service/ArchiveService.kt b/src/main/kotlin/picklab/backend/archive/domain/service/ArchiveService.kt index ebff94aa..f35671dd 100644 --- a/src/main/kotlin/picklab/backend/archive/domain/service/ArchiveService.kt +++ b/src/main/kotlin/picklab/backend/archive/domain/service/ArchiveService.kt @@ -2,10 +2,7 @@ package picklab.backend.archive.domain.service import jakarta.transaction.Transactional import org.springframework.stereotype.Service -import picklab.backend.activity.domain.enums.ActivityType import picklab.backend.archive.domain.entity.Archive -import picklab.backend.archive.domain.enums.ArchiveSortType -import picklab.backend.archive.domain.enums.ProgressStatus import picklab.backend.archive.domain.repository.ArchiveRepository import picklab.backend.common.model.BusinessException import picklab.backend.common.model.ErrorCode @@ -23,28 +20,28 @@ class ArchiveService( member: Member, ): Archive = archiveRepository - .findByIdAndMember(archiveId, member) ?: throw BusinessException(ErrorCode.NOT_FOUND_ARCHIVE) + .findByIdAndParticipationMember(archiveId, member) ?: throw BusinessException(ErrorCode.NOT_FOUND_ARCHIVE) + + fun findByParticipationId(participationId: Long): Archive? = archiveRepository.findByParticipationId(participationId) fun existsActiveByActivityIdAndMemberId( activityId: Long, memberId: Long, - ): Boolean = archiveRepository.existsByActivityIdAndMemberIdAndDeletedAtIsNull(activityId, memberId) + ): Boolean = + archiveRepository.existsByParticipationActivityIdAndParticipationMemberIdAndDeletedAtIsNull( + activityId, + memberId, + ) - fun findCompletedArchives( - member: Member, - activityType: ActivityType?, - sort: ArchiveSortType, - ): List { - val domainSort = sort.toSort() - return if (activityType != null) { - archiveRepository.findByMemberAndProgressStatusAndActivityType( - member, - ProgressStatus.COMPLETED, - activityType, - domainSort, - ) + fun existsActiveByParticipationId(participationId: Long): Boolean = + archiveRepository.existsByParticipationIdAndDeletedAtIsNull( + participationId, + ) + + fun findAllByParticipationIds(participationIds: Collection): List = + if (participationIds.isEmpty()) { + emptyList() } else { - archiveRepository.findByMemberAndProgressStatus(member, ProgressStatus.COMPLETED, domainSort) + archiveRepository.findAllByParticipationIdIn(participationIds) } - } } diff --git a/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveApi.kt b/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveApi.kt index ce23726e..2563b427 100644 --- a/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveApi.kt +++ b/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveApi.kt @@ -14,7 +14,6 @@ import picklab.backend.activity.domain.enums.ActivityType import picklab.backend.archive.domain.enums.ArchiveSortType import picklab.backend.archive.entrypoint.request.ArchiveCreateRequest import picklab.backend.archive.entrypoint.request.ArchiveRecordUpdateRequest -import picklab.backend.archive.entrypoint.request.ArchiveStatusUpdateRequest import picklab.backend.archive.entrypoint.response.ArchiveActivityResponse import picklab.backend.common.model.MemberPrincipal import picklab.backend.common.model.ResponseWrapper @@ -35,22 +34,6 @@ interface ArchiveApi { request: ArchiveCreateRequest, ): ResponseEntity> - @Operation( - summary = "아카이브 상태 수정", - description = "아카이브의 활동 진행 상태 및 합불 여부를 수정합니다", - ) - @ApiResponses( - value = [ - ApiResponse(responseCode = "200", description = "아카이브 상태 수정에 성공했습니다."), - ApiResponse(responseCode = "404", description = "아카이브 정보를 찾을 수 없습니다."), - ], - ) - fun updateStatus( - @AuthenticationPrincipal member: MemberPrincipal, - @PathVariable archiveId: Long, - @RequestBody request: ArchiveStatusUpdateRequest, - ): ResponseEntity> - @Operation( summary = "아카이브 기록 내용 수정", description = "아카이브의 활동 기록 내용(역할, 파일, 연관 URL 등)을 작성하거나 수정합니다", diff --git a/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveController.kt b/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveController.kt index e023ba52..3aee5e9f 100644 --- a/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveController.kt +++ b/src/main/kotlin/picklab/backend/archive/entrypoint/ArchiveController.kt @@ -14,7 +14,6 @@ import picklab.backend.archive.application.ArchiveUseCase import picklab.backend.archive.domain.enums.ArchiveSortType import picklab.backend.archive.entrypoint.request.ArchiveCreateRequest import picklab.backend.archive.entrypoint.request.ArchiveRecordUpdateRequest -import picklab.backend.archive.entrypoint.request.ArchiveStatusUpdateRequest import picklab.backend.archive.entrypoint.response.ArchiveActivityResponse import picklab.backend.common.model.MemberPrincipal import picklab.backend.common.model.ResponseWrapper @@ -33,16 +32,6 @@ class ArchiveController( return ResponseEntity.ok(ResponseWrapper.success(SuccessCode.CREATE_ARCHIVE_SUCCESS)) } - @PatchMapping("/v1/archive/{archiveId}/status") - override fun updateStatus( - @AuthenticationPrincipal member: MemberPrincipal, - @PathVariable archiveId: Long, - @RequestBody request: ArchiveStatusUpdateRequest, - ): ResponseEntity> { - archiveUseCase.updateArchiveStatus(archiveId, request, member) - return ResponseEntity.ok(ResponseWrapper.success(SuccessCode.UPDATE_ARCHIVE_SUCCESS)) - } - @PatchMapping("/v1/archive/{archiveId}/record") override fun updateRecord( @AuthenticationPrincipal member: MemberPrincipal, diff --git a/src/main/kotlin/picklab/backend/archive/entrypoint/request/ArchiveCreateRequest.kt b/src/main/kotlin/picklab/backend/archive/entrypoint/request/ArchiveCreateRequest.kt index 379b9272..bf5c9706 100644 --- a/src/main/kotlin/picklab/backend/archive/entrypoint/request/ArchiveCreateRequest.kt +++ b/src/main/kotlin/picklab/backend/archive/entrypoint/request/ArchiveCreateRequest.kt @@ -1,25 +1,20 @@ package picklab.backend.archive.entrypoint.request import io.swagger.v3.oas.annotations.media.Schema -import picklab.backend.activity.domain.entity.Activity -import picklab.backend.activity.domain.enums.ActivityType import picklab.backend.archive.domain.entity.Archive import picklab.backend.archive.domain.enums.DetailRoleType -import picklab.backend.archive.domain.enums.ProgressStatus import picklab.backend.archive.domain.enums.RoleType import picklab.backend.archive.domain.enums.WriteStatus -import picklab.backend.member.domain.entity.Member +import picklab.backend.participation.domain.entity.ActivityParticipation import java.time.LocalDate class ArchiveCreateRequest( - @field:Schema(description = "활동 ID") - val activityId: Long, + @field:Schema(description = "활동 참여 ID") + val participationId: Long, @field:Schema(description = "상세 역할") val detailRole: DetailRoleType, @field:Schema(description = "활동 기록") val activityRecord: String, - @field:Schema(description = "활동 구분") - val activityType: ActivityType, @field:Schema(description = "활동 파일 URLs") val fileUrls: List, @field:Schema(description = "활동 연관 URLs") @@ -33,20 +28,14 @@ class ArchiveCreateRequest( @field:Schema(description = "상세 역할에서 기타를 선택하여 직접 입력한 역할") val customRole: String?, ) { - fun toCreateEntity( - member: Member, - activity: Activity, - ): Archive = + fun toCreateEntity(participation: ActivityParticipation): Archive = Archive( - member = member, - activity = activity, + participation = participation, detailRole = detailRole, - activityType = activityType, activityRecord = activityRecord, userStartDate = startDate, userEndDate = endDate, role = role, - activityProgressStatus = ProgressStatus.IN_PROGRESSING, writeStatus = WriteStatus.COMPLETED, customRole = customRole, ) diff --git a/src/main/kotlin/picklab/backend/archive/entrypoint/request/ArchiveStatusUpdateRequest.kt b/src/main/kotlin/picklab/backend/archive/entrypoint/request/ArchiveStatusUpdateRequest.kt deleted file mode 100644 index 39b098f4..00000000 --- a/src/main/kotlin/picklab/backend/archive/entrypoint/request/ArchiveStatusUpdateRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package picklab.backend.archive.entrypoint.request - -import io.swagger.v3.oas.annotations.media.Schema -import picklab.backend.archive.domain.enums.PassOrFailStatus -import picklab.backend.archive.domain.enums.ProgressStatus - -class ArchiveStatusUpdateRequest( - @field:Schema(description = "활동 진행 상태") - val activityProgressStatus: ProgressStatus, - @field:Schema(description = "합격/불합격 상태") - val passOrFailStatus: PassOrFailStatus, -) diff --git a/src/main/kotlin/picklab/backend/archive/entrypoint/response/ArchiveActivityResponse.kt b/src/main/kotlin/picklab/backend/archive/entrypoint/response/ArchiveActivityResponse.kt index 4b49d291..f0623e29 100644 --- a/src/main/kotlin/picklab/backend/archive/entrypoint/response/ArchiveActivityResponse.kt +++ b/src/main/kotlin/picklab/backend/archive/entrypoint/response/ArchiveActivityResponse.kt @@ -3,12 +3,15 @@ package picklab.backend.archive.entrypoint.response import io.swagger.v3.oas.annotations.media.Schema import picklab.backend.archive.domain.entity.Archive import picklab.backend.archive.domain.enums.WriteStatus +import picklab.backend.participation.domain.entity.ActivityParticipation import java.time.LocalDate data class ArchiveActivityResponse( @field:Schema(description = "아카이브 ID") - val id: Long, - @field:Schema(description = "원본 활동 ID") + val archiveId: Long?, + @field:Schema(description = "활동 참여 ID") + val activityParticipationId: Long, + @field:Schema(description = "공고보기용 원본 활동 ID") val activityId: Long, @field:Schema(description = "활동 썸네일 이미지 URL") val activityThumbnailUrl: String?, @@ -19,23 +22,43 @@ data class ArchiveActivityResponse( @field:Schema(description = "주최기관/단체명") val organizer: String?, @field:Schema(description = "활동 시작일") - val userStartDate: LocalDate, + val startDate: LocalDate, @field:Schema(description = "활동 종료일") - val userEndDate: LocalDate, + val endDate: LocalDate?, @field:Schema(description = "작성 여부") val writeStatus: WriteStatus, ) { companion object { + fun from( + participation: ActivityParticipation, + archive: Archive?, + ): ArchiveActivityResponse { + val activity = participation.activity + return ArchiveActivityResponse( + archiveId = archive?.id, + activityParticipationId = participation.id, + activityId = activity.id, + activityThumbnailUrl = activity.activityThumbnailUrl, + activityType = activity.activityType ?: "", + title = activity.title, + organizer = activity.organizer, + startDate = archive?.userStartDate ?: activity.startDate, + endDate = archive?.userEndDate ?: activity.endDate, + writeStatus = archive?.writeStatus ?: WriteStatus.NOT_WRITTEN, + ) + } + fun from(archive: Archive): ArchiveActivityResponse = ArchiveActivityResponse( - id = archive.id, - activityId = archive.activity.id, - activityThumbnailUrl = archive.activity.activityThumbnailUrl, - activityType = archive.activityType.name, - title = archive.activity.title, - organizer = archive.activity.organizer, - userStartDate = archive.userStartDate, - userEndDate = archive.userEndDate, + archiveId = archive.id, + activityParticipationId = archive.participation.id, + activityId = archive.participation.activity.id, + activityThumbnailUrl = archive.participation.activity.activityThumbnailUrl, + activityType = archive.participation.activity.activityType ?: "", + title = archive.participation.activity.title, + organizer = archive.participation.activity.organizer, + startDate = archive.userStartDate, + endDate = archive.userEndDate, writeStatus = archive.writeStatus, ) } diff --git a/src/main/kotlin/picklab/backend/common/model/ErrorCode.kt b/src/main/kotlin/picklab/backend/common/model/ErrorCode.kt index b5061804..0e62ee69 100644 --- a/src/main/kotlin/picklab/backend/common/model/ErrorCode.kt +++ b/src/main/kotlin/picklab/backend/common/model/ErrorCode.kt @@ -58,6 +58,8 @@ enum class ErrorCode( * 아카이브 도메인 관련 */ NOT_FOUND_ARCHIVE(HttpStatus.NOT_FOUND, "아카이브 정보를 찾을 수 없습니다."), + ALREADY_EXISTS_ARCHIVE(HttpStatus.BAD_REQUEST, "이미 해당 활동 참여에 대한 아카이브가 존재합니다."), + CANNOT_CREATE_ARCHIVE(HttpStatus.BAD_REQUEST, "수료 완료한 활동 참여만 아카이브를 작성할 수 있습니다."), /** * 북마크 도메인 관련 diff --git a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt index f13d3fd5..acade395 100644 --- a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt +++ b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt @@ -45,7 +45,6 @@ enum class SuccessCode( // Archive 관련 CREATE_ARCHIVE_SUCCESS(HttpStatus.OK, "아카이브 생성에 성공했습니다."), - UPDATE_ARCHIVE_SUCCESS(HttpStatus.OK, "아카이브 상태 수정에 성공했습니다."), UPDATE_ARCHIVE_RECORD_SUCCESS(HttpStatus.OK, "아카이브 기록 내용 수정에 성공했습니다."), GET_ARCHIVE_LIST(HttpStatus.OK, "아카이브 목록 조회에 성공했습니다."), diff --git a/src/main/kotlin/picklab/backend/participation/domain/repository/ActivityParticipationRepository.kt b/src/main/kotlin/picklab/backend/participation/domain/repository/ActivityParticipationRepository.kt index 935110b9..2c1f6aca 100644 --- a/src/main/kotlin/picklab/backend/participation/domain/repository/ActivityParticipationRepository.kt +++ b/src/main/kotlin/picklab/backend/participation/domain/repository/ActivityParticipationRepository.kt @@ -2,6 +2,7 @@ package picklab.backend.participation.domain.repository import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import org.springframework.data.jpa.repository.JpaRepository import picklab.backend.participation.domain.entity.ActivityParticipation import picklab.backend.participation.domain.enums.ApplicationStatus @@ -35,4 +36,17 @@ interface ActivityParticipationRepository : JpaRepository + + fun findAllByMemberIdAndProgressStatusAndActivityActivityType( + memberId: Long, + progressStatus: ProgressStatus, + activityType: String, + sort: Sort, + ): List } diff --git a/src/main/kotlin/picklab/backend/participation/domain/service/ActivityParticipationService.kt b/src/main/kotlin/picklab/backend/participation/domain/service/ActivityParticipationService.kt index 38aea058..b18f5992 100644 --- a/src/main/kotlin/picklab/backend/participation/domain/service/ActivityParticipationService.kt +++ b/src/main/kotlin/picklab/backend/participation/domain/service/ActivityParticipationService.kt @@ -2,8 +2,10 @@ package picklab.backend.participation.domain.service import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import org.springframework.stereotype.Service import picklab.backend.activity.domain.entity.Activity +import picklab.backend.activity.domain.enums.ActivityType import picklab.backend.common.model.BusinessException import picklab.backend.common.model.ErrorCode import picklab.backend.member.domain.entity.Member @@ -73,6 +75,22 @@ class ActivityParticipationService( progressStatus: ProgressStatus, ): Long = participationRepository.countByMemberIdAndProgressStatus(memberId, progressStatus) + fun findCompletedForArchive( + memberId: Long, + activityType: ActivityType?, + sort: Sort, + ): List = + if (activityType == null) { + participationRepository.findAllByMemberIdAndProgressStatus(memberId, ProgressStatus.COMPLETED, sort) + } else { + participationRepository.findAllByMemberIdAndProgressStatusAndActivityActivityType( + memberId, + ProgressStatus.COMPLETED, + activityType.discriminator, + sort, + ) + } + fun updateApplicationStatus( participation: ActivityParticipation, applicationStatus: ApplicationStatus, diff --git a/src/main/resources/db/migration/V1.10__refactor_archive_to_participation.sql b/src/main/resources/db/migration/V1.10__refactor_archive_to_participation.sql new file mode 100644 index 00000000..16e07019 --- /dev/null +++ b/src/main/resources/db/migration/V1.10__refactor_archive_to_participation.sql @@ -0,0 +1,32 @@ +ALTER TABLE archive + ADD COLUMN participation_id BIGINT NULL COMMENT '활동 참여 ID' AFTER id; + +UPDATE archive a + JOIN activity_participation ap + ON ap.member_id = a.member_id + AND ap.activity_id = a.activity_id + AND ap.deleted_at IS NULL +SET a.participation_id = ap.id; + +DELETE aru +FROM archive_reference_url aru + JOIN archive a ON aru.archive_id = a.id +WHERE a.participation_id IS NULL; + +DELETE aufu +FROM archive_upload_file_url aufu + JOIN archive a ON aufu.archive_id = a.id +WHERE a.participation_id IS NULL; + +DELETE FROM archive +WHERE participation_id IS NULL; + +ALTER TABLE archive + MODIFY participation_id BIGINT NOT NULL COMMENT '활동 참여 ID', + DROP COLUMN member_id, + DROP COLUMN activity_id, + DROP COLUMN activity_type, + DROP COLUMN activity_progress_status; + +CREATE INDEX idx_archive_participation + ON archive (participation_id); diff --git a/src/test/kotlin/picklab/backend/archive/ArchiveIntegrationTest.kt b/src/test/kotlin/picklab/backend/archive/ArchiveIntegrationTest.kt new file mode 100644 index 00000000..e5b0300d --- /dev/null +++ b/src/test/kotlin/picklab/backend/archive/ArchiveIntegrationTest.kt @@ -0,0 +1,186 @@ +package picklab.backend.archive + +import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import picklab.backend.activity.domain.entity.Activity +import picklab.backend.activity.domain.entity.ExternalActivity +import picklab.backend.activity.domain.enums.ActivityFieldType +import picklab.backend.activity.domain.enums.LocationType +import picklab.backend.activity.domain.enums.OrganizerType +import picklab.backend.activity.domain.enums.ParticipantType +import picklab.backend.activity.domain.enums.RecruitmentStatus +import picklab.backend.activity.domain.repository.ActivityRepository +import picklab.backend.activitygroup.domain.entity.ActivityGroup +import picklab.backend.activitygroup.domain.repository.ActivityGroupRepository +import picklab.backend.archive.domain.enums.DetailRoleType +import picklab.backend.archive.domain.enums.RoleType +import picklab.backend.archive.domain.repository.ArchiveRepository +import picklab.backend.archive.entrypoint.request.ArchiveCreateRequest +import picklab.backend.common.model.ErrorCode +import picklab.backend.common.model.SuccessCode +import picklab.backend.helper.WithMockUser +import picklab.backend.member.domain.entity.Member +import picklab.backend.member.domain.repository.MemberRepository +import picklab.backend.participation.domain.entity.ActivityParticipation +import picklab.backend.participation.domain.enums.ApplicationStatus +import picklab.backend.participation.domain.enums.ProgressStatus +import picklab.backend.participation.domain.repository.ActivityParticipationRepository +import picklab.backend.template.IntegrationTest +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +class ArchiveIntegrationTest : IntegrationTest() { + @Autowired + lateinit var memberRepository: MemberRepository + + @Autowired + lateinit var activityRepository: ActivityRepository + + @Autowired + lateinit var activityGroupRepository: ActivityGroupRepository + + @Autowired + lateinit var participationRepository: ActivityParticipationRepository + + @Autowired + lateinit var archiveRepository: ArchiveRepository + + lateinit var member: Member + lateinit var activity: Activity + + @BeforeEach + fun setUp() { + cleanUp.all() + + member = + memberRepository.save( + Member( + name = "테스트 유저", + email = "test@example.com", + ), + ) + + val activityGroup = + activityGroupRepository.save( + ActivityGroup( + name = "테스트 그룹", + description = "테스트 그룹 설명", + ), + ) + + activity = + activityRepository.save( + ExternalActivity( + title = "테스트 대외활동", + organizer = "테스트 주최", + organizerType = OrganizerType.PUBLIC_ORGANIZATION, + targetAudience = ParticipantType.WORKER, + location = LocationType.SEOUL_INCHEON, + recruitmentStartDate = LocalDate.now().minusDays(1), + recruitmentEndDate = LocalDate.now().plusMonths(1), + startDate = LocalDate.now().plusMonths(2), + endDate = LocalDate.now().plusMonths(3), + status = RecruitmentStatus.OPEN, + viewCount = 0L, + duration = + ChronoUnit.DAYS + .between(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 2, 1)) + .toInt(), + activityHomepageUrl = null, + activityApplicationUrl = null, + activityThumbnailUrl = null, + description = "테스트 설명", + benefit = "테스트 혜택", + activityGroup = activityGroup, + activityField = ActivityFieldType.MENTORING, + ), + ) + } + + @Nested + @WithMockUser + @DisplayName("아카이브 API") + inner class ArchiveApiTests { + @Test + @DisplayName("[성공] 수료 완료한 참여 이력은 아카이브가 없어도 미작성으로 조회된다") + fun getCompletedParticipationWithoutArchive() { + val participation = saveParticipation(ProgressStatus.COMPLETED) + + mockMvc + .get("/v1/archive") + .andExpect { status { isOk() } } + .andExpect { jsonPath("$.code") { value(SuccessCode.GET_ARCHIVE_LIST.status.value()) } } + .andExpect { jsonPath("$.data[0].archive_id") { value(nullValue()) } } + .andExpect { jsonPath("$.data[0].activity_participation_id") { value(participation.id) } } + .andExpect { jsonPath("$.data[0].write_status") { value("NOT_WRITTEN") } } + } + + @Test + @DisplayName("[성공] 수료 완료한 참여 이력에 아카이브를 생성한다") + fun createArchiveForCompletedParticipation() { + val participation = saveParticipation(ProgressStatus.COMPLETED) + + mockMvc + .post("/v1/archive") { + contentType = MediaType.APPLICATION_JSON + content = mapper.writeValueAsString(createRequest(participation.id)) + }.andExpect { status { isOk() } } + .andExpect { jsonPath("$.code") { value(SuccessCode.CREATE_ARCHIVE_SUCCESS.status.value()) } } + + val archive = archiveRepository.findByParticipationId(participation.id) + assertThat(archive).isNotNull + assertThat(archive!!.participation.id).isEqualTo(participation.id) + + mockMvc + .get("/v1/archive") + .andExpect { status { isOk() } } + .andExpect { jsonPath("$.data[0].archive_id") { value(archive.id) } } + .andExpect { jsonPath("$.data[0].write_status") { value("COMPLETED") } } + } + + @Test + @DisplayName("[실패] 수료 완료하지 않은 참여 이력은 아카이브를 생성할 수 없다") + fun cannotCreateArchiveForNotCompletedParticipation() { + val participation = saveParticipation(ProgressStatus.NOT_SELECTED) + + mockMvc + .post("/v1/archive") { + contentType = MediaType.APPLICATION_JSON + content = mapper.writeValueAsString(createRequest(participation.id)) + }.andExpect { status { isBadRequest() } } + .andExpect { jsonPath("$.code") { value(ErrorCode.CANNOT_CREATE_ARCHIVE.status.value()) } } + .andExpect { jsonPath("$.message") { value(ErrorCode.CANNOT_CREATE_ARCHIVE.message) } } + } + } + + private fun saveParticipation(progressStatus: ProgressStatus): ActivityParticipation = + participationRepository.save( + ActivityParticipation( + applicationStatus = ApplicationStatus.ACCEPTED, + progressStatus = progressStatus, + member = member, + activity = activity, + ), + ) + + private fun createRequest(participationId: Long): ArchiveCreateRequest = + ArchiveCreateRequest( + participationId = participationId, + detailRole = DetailRoleType.BACKEND, + activityRecord = "백엔드 API를 구현했습니다.", + fileUrls = emptyList(), + referenceUrls = emptyList(), + startDate = LocalDate.now().minusMonths(1), + endDate = LocalDate.now(), + role = RoleType.DEVELOPMENT, + customRole = null, + ) +}