Skip to content

feat: 프로젝트 완료 시 참여자 포트폴리오 자동 생성 - #161

Merged
sangwon02 merged 6 commits into
mainfrom
feature/160-project-completion-portfolio
Aug 11, 2026
Merged

feat: 프로젝트 완료 시 참여자 포트폴리오 자동 생성#161
sangwon02 merged 6 commits into
mainfrom
feature/160-project-completion-portfolio

Conversation

@sangwon02

@sangwon02 sangwon02 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🔗 관련 이슈 (Related Issue)

Closes #160

📝 작업 내용

프로젝트 진행 단계를 COMPLETED로 바꾸면 참여 중인 멤버 전원의 포트폴리오에 해당 프로젝트가 자동으로 생성됩니다.

프로젝트명·유형·개인외주 구분·클라이언트명·설명·기간이 그대로 옮겨가고, 각자 맡은 역할이 함께 채워집니다. 생성된 뒤에는 본인이 프로필에서 수정·삭제할 수 있는 일반 포트폴리오와 동일하게 동작합니다.

포트폴리오에 참여 기간이 없어 컬럼을 추가했고, 직접 등록하는 포트폴리오에서도 선택 입력으로 받습니다.

주요 변경 사항

  • user_portfolio에 참여 기간(start_date, end_date) 컬럼 추가
  • 포트폴리오 생성·수정 요청과 상세·목록 응답에 참여 기간 반영
  • 프로젝트 완료 전환 시 참여자 포트폴리오 일괄 생성
  • 완료 전환을 조건부 UPDATE로 원자화
  • COMPLETED를 최종 상태로 고정하고, 완료에 필요한 값을 검증

상세 내용

1. 참여 기간은 선택 입력입니다

기간을 모르는 예전 작업도 이력에 남길 수 있어야 해서 NULL을 허용합니다. 기존에 등록된 포트폴리오는 값이 없는 채로 유지되고, 프론트에는 null로 내려갑니다.

한쪽만 입력하는 것도 허용합니다. 시작일만 아는 진행 중 작업이나 종료일만 기억나는 예전 작업이 있기 때문입니다. 종료일이 시작일보다 빠른 경우만 PORTFOLIO_PERIOD400으로 막습니다.

2. 완료는 최종 상태입니다

완료로 전환한 뒤에는 다른 단계로 되돌릴 수 없습니다. 되돌린 뒤 다시 완료하면 같은 이력이 두 번 생기기 때문입니다.

이 제약 덕분에 재완료 경로가 없어져 중복 생성 방지용 컬럼이 필요하지 않았습니다. 다만 관리자 두 명이 동시에 완료를 누르는 경우가 남아, 상태 변경을 조건부 UPDATE로 선점하고 갱신된 행이 1건일 때만 포트폴리오를 만듭니다.

벌크 UPDATE는 영속성 컨텍스트를 거치지 않으므로, 호출 직후 엔티티 상태를 맞춰 커밋 시점의 더티 체킹이 예전 status로 덮어쓰지 않도록 했습니다.

3. 완료 전에 값을 검증합니다

Project.kind는 nullable인데 UserPortfolio.kind는 NOT NULL이라, 비어 있으면 포트폴리오 생성이 실패하고 단계 변경까지 롤백됩니다. 완료 전환 시점에 PROJECT_COMPLETION400으로 먼저 막습니다.

title도 함께 검증하지만 실제로는 발동하지 않습니다. ProjectCreateRequestProjectUpdateRequest 모두 @NotBlank라 API를 통해서는 비어질 수 없습니다. 엔티티 컬럼이 nullable이라 방어용으로 남겨뒀습니다.

4. 제외 대상

  • 프로젝트를 나간 멤버(leftAt 기록)
  • 탈퇴한 유저(deletedAt 기록)

역할이 지정되지 않은 멤버는 제외하지 않고 역할 없이 생성합니다.

5. 순환 참조 회피

PortfolioServiceProject를 참조하면 ProjectService와 순환 참조가 생깁니다. ProjectPortfolioCreateCommand에 필요한 값만 옮겨 담아 전달합니다.

6. 자동 생성 포트폴리오의 빈 값

프로젝트에는 대표 영상 개념이 없어 영상 링크와 썸네일이 비어 있습니다. 유형이 ETC여도 기타 유형명이 없어 비워두며, 필요하면 본인이 수정합니다.

🧪 테스트

ProjectCompletionPortfolioTest 5건을 추가했습니다.

  • 완료로 바꾸면 참여 멤버 전원의 포트폴리오가 프로젝트 정보와 역할로 생성된다
  • 나간 멤버와 탈퇴한 유저는 포트폴리오를 받지 않는다
  • 역할이 지정되지 않은 멤버도 역할 없이 포트폴리오를 받는다
  • 완료된 프로젝트는 다른 진행 단계로 되돌릴 수 없다
  • 개인/외주 구분이 없으면 완료로 바꿀 수 없다

UserPortfolio.create 시그니처가 바뀌어 기존 테스트 4곳의 호출부를 함께 수정했습니다. 전체 테스트 스위트 통과를 확인했습니다.

배포 전 확인 사항

이미 COMPLETED 상태인 프로젝트는 진행 단계가 영구히 잠깁니다. 포트폴리오는 전환 시점에만 생성되므로 기존 완료 건은 이력을 받지 못합니다.

배포 전 건수 확인이 필요합니다.

SELECT COUNT(*) FROM project WHERE status = 'COMPLETED' AND deleted_at IS NULL;

프론트엔드 대응이 함께 나가야 합니다. 완료 확인 팝업에 되돌릴 수 없다는 안내가 없으면 사용자가 경고 없이 완료를 누르고 복구할 수 없게 됩니다.

📡 프론트엔드 영향

API 변경
GET /api/v1/users/{userId}/portfolios 응답에 items[].startDate, items[].endDate 추가
GET /api/v1/users/me/portfolios/{portfolioId} 응답에 startDate, endDate 추가
POST /api/v1/users/me/portfolios 요청에 startDate, endDate 추가 (선택)
PATCH /api/v1/users/me/portfolios/{portfolioId} 요청에 startDate, endDate 추가 (선택)
PATCH /api/v1/projects/{projectId} 완료 전환 시 포트폴리오 자동 생성, 완료 후 단계 변경 차단

기존 필드는 그대로라 붙여둔 화면이 깨지지 않습니다.

새 에러 코드

코드 상황 HTTP
PROJECT_COMPLETED409 완료된 프로젝트의 단계 변경 시도 409
PROJECT_COMPLETION400 개인/외주 구분 없이 완료 시도 400
PORTFOLIO_PERIOD400 포트폴리오 종료일이 시작일보다 빠름 400

✅ PR 체크리스트

  • PR 제목은 커밋 컨벤션을 따랐습니다.
  • 관련 이슈를 연결했습니다.
  • 변경 사항에 대한 테스트를 진행했습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 프로젝트를 완료하면 활성 참여자의 포트폴리오가 프로젝트 정보와 역할을 반영해 자동 생성됩니다.
    • 포트폴리오에 참여 시작일과 종료일을 등록하고 수정할 수 있습니다.
    • 포트폴리오 상세 및 목록에서 참여 기간을 확인할 수 있습니다.
  • 버그 수정

    • 유효하지 않은 참여 기간을 저장할 수 없도록 검증을 추가했습니다.
    • 필수 정보가 없는 프로젝트의 완료 처리를 제한합니다.
    • 완료된 프로젝트를 다른 상태로 되돌릴 수 없도록 개선했습니다.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c3327e4-e720-4703-aabf-b1a5b8041eac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

프로젝트를 COMPLETED로 변경할 때 필수 정보를 검증하고 활성 참여자의 포트폴리오를 생성합니다. 완료 후 상태 변경을 차단합니다. 포트폴리오에 선택적 참여 기간을 추가하고 저장·수정·조회합니다.

Changes

프로젝트 완료 및 포트폴리오

Layer / File(s) Summary
포트폴리오 참여 기간 계약 및 저장
src/main/java/com/slatto/domain/user/dto/*, src/main/java/com/slatto/domain/user/entity/UserPortfolio.java, src/main/java/com/slatto/domain/user/service/PortfolioService.java, src/main/resources/db/migration/V016__portfolio_period.sql
포트폴리오 생성·수정·응답에 startDate, endDate를 추가했습니다. 엔티티는 유효한 기간을 검증하고 nullable 날짜 컬럼에 저장합니다.
프로젝트 완료 상태와 포트폴리오 생성 흐름
src/main/java/com/slatto/domain/project/controller/ProjectController.java, src/main/java/com/slatto/domain/project/exception/ProjectErrorCode.java, src/main/java/com/slatto/domain/project/repository/ProjectRepository.java, src/main/java/com/slatto/domain/project/service/ProjectService.java, src/main/java/com/slatto/domain/user/dto/ProjectPortfolioCreateCommand.java
조건부 UPDATE로 프로젝트를 완료 처리합니다. titlekind를 검증한 뒤 활성 참여자와 역할을 포트폴리오 생성 명령으로 변환합니다. 완료 후 상태 변경은 거부합니다.
완료 처리 통합 검증
src/test/java/com/slatto/domain/project/service/ProjectCompletionPortfolioTest.java, src/test/java/com/slatto/domain/{recruitment,user}/...
참여자별 포트폴리오 생성, 역할 복사, 탈퇴·이탈 멤버 제외, 역할 없는 멤버 처리, 완료 후 변경 차단, kind 누락 처리를 검증합니다. 기존 테스트의 생성자 호출도 갱신했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: feature

Suggested reviewers: chazy-d, kohseoyoung, young0206

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 핵심 구현은 #160의 요구사항을 충족하지만 동시 완료와 포트폴리오 생성 실패 롤백을 검증하는 테스트는 확인되지 않습니다. 동시 완료 요청에서 한 건만 생성되는 경우와 포트폴리오 생성 실패 시 프로젝트 상태가 롤백되는 경우를 검증하는 테스트를 추가하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 프로젝트 완료 시 참여자 포트폴리오 자동 생성이라는 주요 변경 사항을 정확하고 간결하게 설명합니다.
Description check ✅ Passed 관련 이슈, 작업 내용, 테스트 체크리스트를 포함하며 변경 사항과 배포 및 프론트엔드 영향을 구체적으로 설명합니다.
Out of Scope Changes check ✅ Passed 참여 기간 지원, 프로젝트 완료 시 포트폴리오 생성, 상태 검증, 관련 테스트 변경은 모두 #160의 범위에 포함됩니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 705745b and 6d33455.

📒 Files selected for processing (18)
  • src/main/java/com/slatto/domain/project/controller/ProjectController.java
  • src/main/java/com/slatto/domain/project/exception/ProjectErrorCode.java
  • src/main/java/com/slatto/domain/project/repository/ProjectRepository.java
  • src/main/java/com/slatto/domain/project/service/ProjectService.java
  • src/main/java/com/slatto/domain/user/dto/PortfolioCreateRequest.java
  • src/main/java/com/slatto/domain/user/dto/PortfolioDetailResponse.java
  • src/main/java/com/slatto/domain/user/dto/PortfolioSummaryResponse.java
  • src/main/java/com/slatto/domain/user/dto/PortfolioUpdateRequest.java
  • src/main/java/com/slatto/domain/user/dto/ProjectPortfolioCreateCommand.java
  • src/main/java/com/slatto/domain/user/entity/UserPortfolio.java
  • src/main/java/com/slatto/domain/user/exception/UserErrorCode.java
  • src/main/java/com/slatto/domain/user/service/PortfolioService.java
  • src/main/resources/db/migration/V016__portfolio_period.sql
  • src/test/java/com/slatto/domain/project/service/ProjectCompletionPortfolioTest.java
  • src/test/java/com/slatto/domain/recruitment/service/RecruitmentApplicationDetailIntegrationTest.java
  • src/test/java/com/slatto/domain/user/repository/UserPortfolioStatsIntegrationTest.java
  • src/test/java/com/slatto/domain/user/service/PortfolioUpdatedAtTest.java
  • src/test/java/com/slatto/domain/user/service/UserWithdrawTest.java

Comment on lines +86 to +87
`kind` 가 비어 있으면 포트폴리오를 만들 수 없어 완료로 바꿀 수 없다.
이때는 `PROJECT_COMPLETION400` 이 나간다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

완료 조건에 title 검증도 문서화하십시오.

ProjectService.completeProjectkind가 없을 때뿐 아니라 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.

Suggested change
`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.

Comment on lines +109 to +199
@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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

@chazy-d chazy-d left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

깔끔한 것 같습니다! 수고하셨습니다!

@sangwon02
sangwon02 merged commit 7caa67d into main Aug 11, 2026
2 checks passed
@guingguing
guingguing deleted the feature/160-project-completion-portfolio branch August 12, 2026 15:36
@guingguing guingguing added the feature 새로운 기능 추가 label Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT: 프로젝트 완료 시 참여자 포트폴리오 자동 생성

3 participants