Skip to content

Feat : 팀스페이스 생성 API구현#29

Merged
jaeunda merged 10 commits intomainfrom
feat/4-projectgroup
Apr 19, 2026
Merged

Feat : 팀스페이스 생성 API구현#29
jaeunda merged 10 commits intomainfrom
feat/4-projectgroup

Conversation

@bellue
Copy link
Copy Markdown
Collaborator

@bellue bellue commented Apr 7, 2026

📌 Related Issue

Closes #4

🚀 Description

  • 4명 모이면 정보 받아서 팀스페이스 생성하는 기능 구현했습니다.

📢 Review Point

  • 중복 userId, 그룹 내 인원수, 중복된 groupId 등 예외처리부분 위주로 봐주시면 될 것 같아요

📚 Etc (선택)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8f7308921

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +169 to +171
List<Long> requestMemberIds = request.members().stream()
.map(CreateProjectGroupMemberRequest::userId)
.sorted()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 중복 요청 검증에 멤버 역할 비교를 포함하라

동일 matchId 재요청 검증에서 구성원을 userId만으로 비교하고 있어, 같은 사용자 목록이지만 MemberRole이 다른 요청도 동일 요청으로 간주되어 성공 응답으로 반환됩니다. 커밋 설명의 의도(페이로드 불일치 시 예외)와 실제 동작이 달라져 클라이언트는 역할 변경이 반영된 것으로 오해할 수 있고, 재시도/멱등 처리의 정확성이 깨집니다. userId만이 아니라 (userId, role) 기준으로 양쪽 멤버 구성을 비교하도록 검증을 보강하는 것이 안전합니다.

Useful? React with 👍 / 👎.

@sh0723
Copy link
Copy Markdown
Collaborator

sh0723 commented Apr 9, 2026

기본 구조는 4명매칭 -> host가 팀스페이스 생성버튼 클릭(projectgroup api 호출) -> 이후 중복멤버 처리 정도로 보면 될것같아요.

팀스페이스 생성시 중복멤버가 있는지, 생성자가 host가 맞는지, 멤버가 다른 팀에 있는지등을 검사하는 로직때문에 서비스코드가 길어져서 고봉밥이 됐는데 중간에 궁금한거 물어보시면 답장하고 세미나할때도 추가로 설명하겠습니다.

이슈랑 PR 템플릿 있는거 사용해주세요!

@sh0723
Copy link
Copy Markdown
Collaborator

sh0723 commented Apr 9, 2026

flowchart TD
    A["클라이언트 API 호출"] --> B["POST /api/project-groups"]

    %% 인증 사용자 해석
    B --> C["@LoginUser resolver"]
    C --> C1{"인증 정보 존재 여부"}
    C1 -->|"No"| X1["401 NO_AUTHENTICATED_USER"]
    C1 -->|"Yes"| C2{"principal이 UserPrincipal인가"}
    C2 -->|"No"| X2["500 INVALID_SECURITY_CONTEXT"]
    C2 -->|"Yes"| C3["findByIdAndDeletedAtIsNull(userId)"]
    C3 --> C4{"활성 사용자 존재 여부"}
    C4 -->|"No"| X3["401 UNEXISTED_USER"]
    C4 -->|"Yes"| D["요청 본문 검증"]

    %% 컨트롤러 입력 검증
    D --> D1{"Errors.hasErrors()"}
    D1 -->|"Yes"| X4["400 INVALID_INPUT_FIELD"]
    D1 -->|"No"| E["createProjectGroup(requester.id, request)"]

    %% 요청자 권한 검증
    E --> E1{"requesterUserId == hostUserId"}
    E1 -->|"No"| X5["403 PROJECT_GROUP_PERMISSION_DENIED"]
    E1 -->|"Yes"| E2["findByMatchId(matchId)"]

    %% 동일 matchId 중복 요청 분기
    E2 --> E3{"기존 팀스페이스 존재 여부"}
    E3 -->|"Yes"| F["handleDuplicateMatchRequest"]
    E3 -->|"No"| G["members -> userIds / uniqueUserIds 추출"]

    %% 멤버 목록 검증
    G --> G1{"팀 인원 4명인가"}
    G1 -->|"No"| X6["400 INVALID_PROJECT_GROUP_REQUEST"]
    G1 -->|"Yes"| G2{"중복 userId 없는가"}
    G2 -->|"No"| X7["400 INVALID_PROJECT_GROUP_REQUEST"]
    G2 -->|"Yes"| G3{"hostUserId가 멤버 목록에 포함되는가"}
    G3 -->|"No"| X8["400 INVALID_PROJECT_GROUP_REQUEST"]
    G3 -->|"Yes"| H["findAllByIdInAndDeletedAtIsNull(uniqueUserIds)"]

    %% 사용자 존재 / 이미 소속 여부 검증
    H --> H1{"요청한 모든 사용자가 존재하는가"}
    H1 -->|"No"| X9["404 PROJECT_GROUP_MEMBER_NOT_FOUND"]
    H1 -->|"Yes"| I{"이미 팀에 속한 사용자 존재 여부"}
    I -->|"Yes"| X10["400 INVALID_PROJECT_GROUP_REQUEST\n이미 팀에 속한 사용자가 포함"]
    I -->|"No"| J["ProjectGroup 저장\nstatus = request.status ?? ACTIVE"]

    %% 멤버 저장
    J --> K["ProjectGroupMember 생성\nhost -> HOST\nothers -> MEMBER"]
    K --> L["saveAllAndFlush(members)"]
    L -->|"성공"| R1["201 CREATED + CreateProjectGroupResponse"]
    L -->|"DataIntegrityViolationException"| M["중복/경합 재확인"]

    %% 동시성 충돌 처리
    M --> M1{"matchId로 이미 생성된 그룹 존재 여부"}
    M1 -->|"Yes"| F
    M1 -->|"No"| M2{"이미 팀에 속한 사용자 생겼는가"}
    M2 -->|"Yes"| X11["400 INVALID_PROJECT_GROUP_REQUEST\n이미 팀에 속한 사용자가 포함"]
    M2 -->|"No"| X12["400 INVALID_PROJECT_GROUP_REQUEST\n팀 스페이스 저장 중 데이터 충돌"]

    %% 기존 그룹 반환 로직
    F --> F1["기존 그룹 멤버 조회"]
    F1 --> F2{"HOST 멤버 존재 여부"}
    F2 -->|"No"| X13["400 INVALID_PROJECT_GROUP_REQUEST"]
    F2 -->|"Yes"| F3{"기존 hostUserId == 요청 hostUserId"}
    F3 -->|"No"| X14["403 PROJECT_GROUP_PERMISSION_DENIED"]
    F3 -->|"Yes"| F4["idempotent payload 검증"]

    F4 --> F5{"프로젝트 정보 동일 여부"}
    F5 -->|"No"| X15["400 INVALID_PROJECT_GROUP_REQUEST\n같은 matchId에 다른 프로젝트 정보 불가"]
    F5 -->|"Yes"| F6{"멤버 ID 목록 동일 여부"}
    F6 -->|"No"| X16["400 INVALID_PROJECT_GROUP_REQUEST\n같은 matchId에 다른 구성원 목록 불가"]
    F6 -->|"Yes"| R2["201 CREATED + 기존 팀스페이스 반환"]
Loading

중복 요청/동시성 처리 요약

  flowchart LR
      A["같은 matchId로 재요청"] --> B{"기존 팀스페이스 존재?"}
      B -->|"Yes"| C["기존 그룹 + payload 동일성 검증"]
      C -->|"동일"| D["기존 팀스페이스 반환"]
      C -->|"다름"| E["400 INVALID_PROJECT_GROUP_REQUEST"]

      B -->|"No"| F["새 팀스페이스 생성 시도"]
      F -->|"저장 성공"| G["새 팀스페이스 반환"]
      F -->|"저장 중 경합 발생"| H["DataIntegrityViolationException"]
      H --> I["matchId / 사용자 소속 상태 재확인"]
      I -->|"기존 그룹 생김"| D
      I -->|"사용자 중복 소속"| E
      I -->|"기타 충돌"| E
Loading

참고

  • status가 없으면 기본값은 ACTIVE입니다.
  • 입력 검증은 컨트롤러와 서비스 양쪽에서 모두 수행됩니다.
  • 동일 matchId 재요청은 멱등 요청으로 취급해 기존 팀스페이스를 반환합니다.
  • 현재 구현상 기존 요청과의 동일성 비교는 멤버 userId 목록 기준이며, MemberRole까지는 비교하지 않습니다.
  • 중복 요청으로 기존 그룹을 반환하는 경우에도 컨트롤러가 고정으로 201 Created를 내려줍니다.

private final UserRepository userRepository;

@Transactional
public CreateProjectGroupResponse createProjectGroup(Long requesterUserId, CreateProjectGroupRequest request) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

제가 여기서 예외 처리 책임 축소 얘기를 했었는데요,
caller에게 데이터 정합성을 모두 위임하는게 다시 생각해보니 썩 좋은 구조는 아닌거같아요. 추후 또다른 caller가 생겼는데, 거기서 데이터 정합성 검증이 약하면 문제가 될 것 같아요

그래서 작업해주신 검증들은 유지하되 requesterUserId == hostUserId부분만 제거하면 되겠다 싶네요

Copy link
Copy Markdown
Member

@jaeunda jaeunda left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~

package team.po.feature.projectgroup.domain;

public enum ProjectGroupStatus {
ACTIVE
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FINISHED 추가하면 좋을 것 같습니다

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 반영했습니다!

@@ -0,0 +1,7 @@
package team.po.feature.projectgroup.domain;

public enum MemberRole {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

match 쪽은 DESIGN, BE, FE로 했는데
나중에 제가 여기에 맞출게요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵!

this.matchId = matchId;
this.projectDescription = projectDescription;
this.projectMvp = projectMvp;
this.status = status;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

초기 status는 ACTIVE로 고정해도 될 것 같습니다

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵!

String projectTitle,
String projectDescription,
String projectMvp,
ProjectGroupStatus status
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

status는 내부에서 초기값으로 설정하는 게 좋지 않을까요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

반영했습니다 !

Copy link
Copy Markdown
Member

@jaeunda jaeunda left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~

Copy link
Copy Markdown
Member

@hwangjokim hwangjokim left a comment

Choose a reason for hiding this comment

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

생각보다 규모가 커서 고민도 많이했을텐데 고생하셨습니다. 코멘트 확인해주세요~

created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
CONSTRAINT fk_project_group_member_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_project_group_member_group FOREIGN KEY (project_group_id) REFERENCES project_group(id),
CONSTRAINT uq_project_group_member_user UNIQUE (user_id),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[blocker] userId 왜 유니크 걸려있어요??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

해당 유저가 두개의 팀에 들어가는 경우를 db에서도 검증하고싶어서 unique처리를 했습니다

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

finished라는 상태를 둬서 플젝 끝나도 레코드가 남아있을텐데, 유니크를 걸면 안될것같아요

Copy link
Copy Markdown
Collaborator Author

@bellue bellue Apr 13, 2026

Choose a reason for hiding this comment

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

그렇겠네요,, CONSTRAINT uq_project_group_member UNIQUE (project_group_id, user_id)에만 unique제약 걸어놓고 말씀하신 부분은 제거하겠습니다

@Column(name = "created_at", nullable = false, insertable = false, updatable = false)
private Instant createdAt;

public ProjectGroupMember(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

우리 빌더패턴 써용

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵!

Comment on lines +135 to +143
@Transactional
public void grantAdminPermission(Long projectGroupId, Long requesterUserId, Long targetUserId) {
this.changeAdminPermission(projectGroupId, requesterUserId, targetUserId, true);
}

@Transactional
public void revokeAdminPermission(Long projectGroupId, Long requesterUserId, Long targetUserId) {
this.changeAdminPermission(projectGroupId, requesterUserId, targetUserId, false);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이거 쓰이는곳이 없는것같아요. 의도인가요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

너 정말 핵심을 짚었어!
감사합니다

Comment on lines +33 to +34
@Column(name = "group_id", nullable = false, unique = true)
private Long groupId;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이 groupId는 의도가 뭔지 궁금해요. PK있는데 별도로 Unique한 값을 둬야하는 이유랑, 어떤 의도로 쓰이고 있는건지 잘 모르겠어요..

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

groupId는 중복검사 로직에 사용하고있습니다.
매칭이후 groupId를 제쪽으로 넘겨주면 중복검사(재요청 등에 대비)를 하려고 생각중입니다.
pk를 사용할 경우 insert시점에서 발급받기때문에 재요청에 대응할 수 없다고 생각해 groupId를 별도로 두었습니다

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

저는 groupId를 중복검사용으로 따로 넘기는 것보다, 생성하려는 사용자들이 이미 ACTIVE 상태의 그룹 멤버인지 확인하는 방식이 더 맞다고 생각해요.
실제로 막고 싶은 건 같은 groupId 중복이 아니라, 같은 유저들이 이미 활성 그룹에 들어갔는데 또 다른 그룹이 생기는 상황이니까요.
그리고 groupId를 중복검사용으로 쓰더라도, 같은 구성의 멤버에 대해 서로 다른 groupId가 들어올 수 있지 않을까요?
예를 들어 A, B, C, D가 묶이는 상황에서 각 유저의 polling 요청이 비슷한 시점에 createProjectGroup을 실행하면, 요청마다 다른 groupId가 생성될 가능성이 있어 보여요.
그래서 중복 기준은 groupId보다 userId와 ACTIVE GroupMember 여부로 잡는 게 더 의도에 맞고, 매칭 확정부터 그룹 생성까지는 하나의 트랜잭션으로 묶는 게 좋을 것 같습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

좋은 생각인 것 같습니다 ! 해당방식으로 한 멤버가 두개의 팀에 들어가는 상황에 예외처리를 하면 될 것 같습니다. 그럼 groupId를 제거하고 projectgroup의 pk 를 써도 될 것 같은데 이렇게 했을 때 재시도(두번클릭)등의 상황에서 팀이 두번 생기는 걸 방지할 수 있는지도 같이 고민해보고 수정해서 올리도록 하겠습니다.

Comment on lines +86 to +88
if (memberRequest.groupRole() == GroupRole.MEMBER && Boolean.TRUE.equals(memberRequest.admin())) {
member.grantAdmin();
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

여기를 보니까 헷갈리던데

  1. grouprole == HOST면 방장
  2. grouprole == MEMBER면 팀원
  3. admin은 별개의 관리자 권한

이라서, 동작 자체는 이해가 잘 가요. 다만 가독성 부분에서 이해가 바로 되진 않았어요.

  1. HOST 는 항상 관리자라고 규칙을 고정하고, 요청 DTO에서는 admin 을 멤버에게만 허용하거나
  2. 아니면 아예 admin 을 없애고 HOST, ADMIN, MEMBER 같은 권한으로 통합하면

어떨까요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

동의합니다. 1번에 맞춰서 host는 항상 admin을 true로 하고 요청dto에서는 admin을 제거했습니다!

projectGroup.getId(), groupId, members.size());

return new CreateProjectGroupResponse(
projectGroup.getId(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

groupId 코멘트의 연장선인데

Request의 groupId는 DB에 별도로 있는 칼럼의 groupId고
Response의 groupId는 PK네용

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

response에서도 groupId로 통일하겠습니다 !

}
}

private void changeAdminPermission(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

!= FINISHED 상태검사도 필요해보여요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 해당부분 추가했습니다!

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record CreateProjectGroupRequest(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

여기 달린 Sprng validation용 어노테이션은 컨트롤러가 없으니 동작하지 않습니다

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

조회용 controller를 만드는게 나을 것 같아서 반영해서 수정하겠습니다 !

Comment on lines +59 to +60
lenient().when(projectGroupRepository.findByGroupId(anyLong())).thenReturn(Optional.empty());
lenient().when(projectGroupMemberRepository.existsByUser_IdIn(anyList())).thenReturn(false);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI한테 시켜서 lenient 쓰지 말라고 해주세용 테스트코드 검증력 약해집니다

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

감사합니다 ㅎㅎ

Comment on lines +39 to +40
@Transactional
public CreateProjectGroupResponse createProjectGroup(CreateProjectGroupRequest request) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

다은이가 작업하기 전까진 쓰이는 곳 없으니 주석 달아둡시다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

고민인게 다은이가 작업하기 전에 안쓰이는 부분이 너무 많아서 해당 부분 완료 전까지 merge를 안하는 방향으로 가는게 어떨까...생각...중입니다..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

격리 잘 되어있으니까 머지해도 ㄱㅊ아보여요

Copy link
Copy Markdown
Collaborator

@sh0723 sh0723 left a comment

Choose a reason for hiding this comment

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

고생하셨어요~~ 코멘트 확인해주세요!

@jaeunda jaeunda requested review from hwangjokim and sh0723 April 19, 2026 04:28
@jaeunda jaeunda merged commit 8a622ec into main Apr 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat : 팀 생성 및 기본 관리 API 구현

4 participants