feat: 프로젝트 완료 시 참여자 포트폴리오 자동 생성 - #161
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthrough프로젝트를 Changes프로젝트 완료 및 포트폴리오
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/com/slatto/domain/project/controller/ProjectController.java`:
- Around line 86-87: Update the completion-condition documentation near
ProjectController to state that an empty title, as well as an empty kind,
prevents portfolio creation and causes ProjectService.completeProject to return
PROJECT_COMPLETION400.
In
`@src/test/java/com/slatto/domain/project/service/ProjectCompletionPortfolioTest.java`:
- Around line 109-199: Extend the completion tests around
ProjectService.updateProject and markCompleted to cover concurrent completion
requests, asserting that only one request succeeds and exactly one portfolio set
is created. Add a failure-injection scenario for portfolio persistence in
completeProject, asserting the transaction rolls back both the project status
and all portfolios created during the failed completion.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 07fa92f8-a39c-47df-a28d-b536d679c3f8
📒 Files selected for processing (18)
src/main/java/com/slatto/domain/project/controller/ProjectController.javasrc/main/java/com/slatto/domain/project/exception/ProjectErrorCode.javasrc/main/java/com/slatto/domain/project/repository/ProjectRepository.javasrc/main/java/com/slatto/domain/project/service/ProjectService.javasrc/main/java/com/slatto/domain/user/dto/PortfolioCreateRequest.javasrc/main/java/com/slatto/domain/user/dto/PortfolioDetailResponse.javasrc/main/java/com/slatto/domain/user/dto/PortfolioSummaryResponse.javasrc/main/java/com/slatto/domain/user/dto/PortfolioUpdateRequest.javasrc/main/java/com/slatto/domain/user/dto/ProjectPortfolioCreateCommand.javasrc/main/java/com/slatto/domain/user/entity/UserPortfolio.javasrc/main/java/com/slatto/domain/user/exception/UserErrorCode.javasrc/main/java/com/slatto/domain/user/service/PortfolioService.javasrc/main/resources/db/migration/V016__portfolio_period.sqlsrc/test/java/com/slatto/domain/project/service/ProjectCompletionPortfolioTest.javasrc/test/java/com/slatto/domain/recruitment/service/RecruitmentApplicationDetailIntegrationTest.javasrc/test/java/com/slatto/domain/user/repository/UserPortfolioStatsIntegrationTest.javasrc/test/java/com/slatto/domain/user/service/PortfolioUpdatedAtTest.javasrc/test/java/com/slatto/domain/user/service/UserWithdrawTest.java
| `kind` 가 비어 있으면 포트폴리오를 만들 수 없어 완료로 바꿀 수 없다. | ||
| 이때는 `PROJECT_COMPLETION400` 이 나간다. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
완료 조건에 title 검증도 문서화하십시오.
ProjectService.completeProject는 kind가 없을 때뿐 아니라 title이 비어 있을 때도 PROJECT_COMPLETION400을 반환합니다. 현재 설명은 kind 조건만 안내하므로 API 클라이언트가 완료 요청 실패 원인을 알 수 없습니다.
수정 예시
- `kind` 가 비어 있으면 포트폴리오를 만들 수 없어 완료로 바꿀 수 없다.
+ `title` 또는 `kind` 가 비어 있으면 포트폴리오를 만들 수 없어 완료로 바꿀 수 없다.
이때는 `PROJECT_COMPLETION400` 이 나간다.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| `kind` 가 비어 있으면 포트폴리오를 만들 수 없어 완료로 바꿀 수 없다. | |
| 이때는 `PROJECT_COMPLETION400` 이 나간다. | |
| `title` 또는 `kind` 가 비어 있으면 포트폴리오를 만들 수 없어 완료로 바꿀 수 없다. | |
| 이때는 `PROJECT_COMPLETION400` 이 나간다. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/domain/project/controller/ProjectController.java`
around lines 86 - 87, Update the completion-condition documentation near
ProjectController to state that an empty title, as well as an empty kind,
prevents portfolio creation and causes ProjectService.completeProject to return
PROJECT_COMPLETION400.
| @Test | ||
| @DisplayName("완료로 바꾸면 참여 멤버 전원의 포트폴리오가 프로젝트 정보와 역할로 생성된다") | ||
| void completeProject_createsPortfolioForEveryMember() { | ||
| Users editor = saveUser("editor@slatto.com", "에디터", "social-editor"); | ||
| ProjectMember editorMember = projectMemberRepository.save(ProjectMember.createMember(project, editor)); | ||
| projectUserRoleRepository.save(ProjectUserRole.create(editorMember, RoleName.EDITOR)); | ||
| entityManager.flush(); | ||
|
|
||
| projectService.updateProject(project.getId(), owner.getId(), completeRequest()); | ||
|
|
||
| List<UserPortfolio> portfolios = userPortfolioRepository.findAll(); | ||
| assertThat(portfolios).hasSize(2); | ||
| assertThat(portfolios).allSatisfy(portfolio -> { | ||
| assertThat(portfolio.getTitle()).isEqualTo("연애혁명"); | ||
| assertThat(portfolio.getType()).isEqualTo(CategoryName.FILM_DRAMA); | ||
| assertThat(portfolio.getKind()).isEqualTo(Kind.EXTERNAL); | ||
| assertThat(portfolio.getClientName()).isEqualTo("스튜디오 X"); | ||
| assertThat(portfolio.getStartDate()).isEqualTo(project.getStartDate()); | ||
| assertThat(portfolio.getEndDate()).isEqualTo(project.getEndDate()); | ||
| }); | ||
| assertThat(roleNamesOf(editor)).containsExactly(RoleName.EDITOR); | ||
| assertThat(roleNamesOf(owner)).containsExactly(RoleName.DIRECTOR); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("나간 멤버와 탈퇴한 유저는 포트폴리오를 받지 않는다") | ||
| void completeProject_excludesLeftMemberAndWithdrawnUser() { | ||
| Users leftUser = saveUser("left@slatto.com", "나간사람", "social-left"); | ||
| ProjectMember leftMember = projectMemberRepository.save(ProjectMember.createMember(project, leftUser)); | ||
| leftMember.leave(); | ||
|
|
||
| Users withdrawnUser = saveUser("withdrawn@slatto.com", "탈퇴자", "social-withdrawn"); | ||
| projectMemberRepository.save(ProjectMember.createMember(project, withdrawnUser)); | ||
| withdrawnUser.withdraw(java.time.LocalDateTime.now()); | ||
| entityManager.flush(); | ||
|
|
||
| projectService.updateProject(project.getId(), owner.getId(), completeRequest()); | ||
|
|
||
| assertThat(userPortfolioRepository.findAll()) | ||
| .extracting(portfolio -> portfolio.getUser().getId()) | ||
| .containsExactly(owner.getId()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("역할이 지정되지 않은 멤버도 역할 없이 포트폴리오를 받는다") | ||
| void completeProject_createsPortfolioWithoutRoles() { | ||
| Users helper = saveUser("helper@slatto.com", "도우미", "social-helper"); | ||
| projectMemberRepository.save(ProjectMember.createMember(project, helper)); | ||
| entityManager.flush(); | ||
|
|
||
| projectService.updateProject(project.getId(), owner.getId(), completeRequest()); | ||
|
|
||
| assertThat(userPortfolioRepository.findAll()).hasSize(2); | ||
| assertThat(roleNamesOf(helper)).isEmpty(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("완료된 프로젝트는 다른 진행 단계로 되돌릴 수 없다") | ||
| void updateProject_afterCompletion_cannotChangeStatus() { | ||
| projectService.updateProject(project.getId(), owner.getId(), completeRequest()); | ||
| entityManager.flush(); | ||
|
|
||
| assertThatThrownBy(() -> | ||
| projectService.updateProject(project.getId(), owner.getId(), statusRequest("EDITING")) | ||
| ).isInstanceOf(BaseException.class); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("개인/외주 구분이 없으면 완료로 바꿀 수 없다") | ||
| void completeProject_withoutKind_throws() { | ||
| Project noKindProject = projectRepository.save(Project.create( | ||
| owner, | ||
| "종류 없는 프로젝트", | ||
| CategoryName.FILM_DRAMA, | ||
| LengthType.SHORT_FORM, | ||
| "설명", | ||
| LocalDate.now().plusDays(10), | ||
| null, | ||
| null | ||
| )); | ||
| projectMemberRepository.save(ProjectMember.createAdmin(noKindProject, owner)); | ||
| entityManager.flush(); | ||
|
|
||
| assertThatThrownBy(() -> | ||
| projectService.updateProject(noKindProject.getId(), owner.getId(), completeRequestWithoutKind()) | ||
| ).isInstanceOf(BaseException.class); | ||
|
|
||
| assertThat(userPortfolioRepository.findAll()).isEmpty(); | ||
| assertThat(projectRepository.findById(noKindProject.getId()).orElseThrow().getStatus()) | ||
| .isNotEqualTo(ProjectStatus.COMPLETED); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
동시 완료와 롤백 시나리오를 테스트하십시오.
현재 테스트는 동시 요청과 포트폴리오 생성 실패 후 롤백을 검증하지 않습니다. markCompleted의 조건부 UPDATE와 completeProject의 단일 트랜잭션은 이 PR의 핵심 계약입니다.
두 완료 요청을 병렬 실행해서 한 요청만 성공하고 포트폴리오가 한 벌만 생성되는지 검증하십시오. 포트폴리오 저장 실패를 유도해서 프로젝트 상태와 생성된 포트폴리오가 모두 롤백되는지도 검증하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/test/java/com/slatto/domain/project/service/ProjectCompletionPortfolioTest.java`
around lines 109 - 199, Extend the completion tests around
ProjectService.updateProject and markCompleted to cover concurrent completion
requests, asserting that only one request succeeds and exactly one portfolio set
is created. Add a failure-injection scenario for portfolio persistence in
completeProject, asserting the transaction rolls back both the project status
and all portfolios created during the failed completion.
🔗 관련 이슈 (Related Issue)
Closes #160
📝 작업 내용
프로젝트 진행 단계를
COMPLETED로 바꾸면 참여 중인 멤버 전원의 포트폴리오에 해당 프로젝트가 자동으로 생성됩니다.프로젝트명·유형·개인외주 구분·클라이언트명·설명·기간이 그대로 옮겨가고, 각자 맡은 역할이 함께 채워집니다. 생성된 뒤에는 본인이 프로필에서 수정·삭제할 수 있는 일반 포트폴리오와 동일하게 동작합니다.
포트폴리오에 참여 기간이 없어 컬럼을 추가했고, 직접 등록하는 포트폴리오에서도 선택 입력으로 받습니다.
주요 변경 사항
user_portfolio에 참여 기간(start_date,end_date) 컬럼 추가COMPLETED를 최종 상태로 고정하고, 완료에 필요한 값을 검증상세 내용
1. 참여 기간은 선택 입력입니다
기간을 모르는 예전 작업도 이력에 남길 수 있어야 해서
NULL을 허용합니다. 기존에 등록된 포트폴리오는 값이 없는 채로 유지되고, 프론트에는null로 내려갑니다.한쪽만 입력하는 것도 허용합니다. 시작일만 아는 진행 중 작업이나 종료일만 기억나는 예전 작업이 있기 때문입니다. 종료일이 시작일보다 빠른 경우만
PORTFOLIO_PERIOD400으로 막습니다.2. 완료는 최종 상태입니다
완료로 전환한 뒤에는 다른 단계로 되돌릴 수 없습니다. 되돌린 뒤 다시 완료하면 같은 이력이 두 번 생기기 때문입니다.
이 제약 덕분에 재완료 경로가 없어져 중복 생성 방지용 컬럼이 필요하지 않았습니다. 다만 관리자 두 명이 동시에 완료를 누르는 경우가 남아, 상태 변경을 조건부 UPDATE로 선점하고 갱신된 행이 1건일 때만 포트폴리오를 만듭니다.
벌크 UPDATE는 영속성 컨텍스트를 거치지 않으므로, 호출 직후 엔티티 상태를 맞춰 커밋 시점의 더티 체킹이 예전 status로 덮어쓰지 않도록 했습니다.
3. 완료 전에 값을 검증합니다
Project.kind는 nullable인데UserPortfolio.kind는 NOT NULL이라, 비어 있으면 포트폴리오 생성이 실패하고 단계 변경까지 롤백됩니다. 완료 전환 시점에PROJECT_COMPLETION400으로 먼저 막습니다.title도 함께 검증하지만 실제로는 발동하지 않습니다.ProjectCreateRequest와ProjectUpdateRequest모두@NotBlank라 API를 통해서는 비어질 수 없습니다. 엔티티 컬럼이 nullable이라 방어용으로 남겨뒀습니다.4. 제외 대상
leftAt기록)deletedAt기록)역할이 지정되지 않은 멤버는 제외하지 않고 역할 없이 생성합니다.
5. 순환 참조 회피
PortfolioService가Project를 참조하면ProjectService와 순환 참조가 생깁니다.ProjectPortfolioCreateCommand에 필요한 값만 옮겨 담아 전달합니다.6. 자동 생성 포트폴리오의 빈 값
프로젝트에는 대표 영상 개념이 없어 영상 링크와 썸네일이 비어 있습니다. 유형이
ETC여도 기타 유형명이 없어 비워두며, 필요하면 본인이 수정합니다.🧪 테스트
ProjectCompletionPortfolioTest5건을 추가했습니다.UserPortfolio.create시그니처가 바뀌어 기존 테스트 4곳의 호출부를 함께 수정했습니다. 전체 테스트 스위트 통과를 확인했습니다.배포 전 확인 사항
이미
COMPLETED상태인 프로젝트는 진행 단계가 영구히 잠깁니다. 포트폴리오는 전환 시점에만 생성되므로 기존 완료 건은 이력을 받지 못합니다.배포 전 건수 확인이 필요합니다.
프론트엔드 대응이 함께 나가야 합니다. 완료 확인 팝업에 되돌릴 수 없다는 안내가 없으면 사용자가 경고 없이 완료를 누르고 복구할 수 없게 됩니다.
📡 프론트엔드 영향
GET /api/v1/users/{userId}/portfoliositems[].startDate,items[].endDate추가GET /api/v1/users/me/portfolios/{portfolioId}startDate,endDate추가POST /api/v1/users/me/portfoliosstartDate,endDate추가 (선택)PATCH /api/v1/users/me/portfolios/{portfolioId}startDate,endDate추가 (선택)PATCH /api/v1/projects/{projectId}기존 필드는 그대로라 붙여둔 화면이 깨지지 않습니다.
새 에러 코드
PROJECT_COMPLETED409PROJECT_COMPLETION400PORTFOLIO_PERIOD400✅ PR 체크리스트
Summary by CodeRabbit
새로운 기능
버그 수정