Skip to content

[TMT-222] feat: 그룹 상세·리뷰 목록 실구현 — 게이트 마스킹·커서 - #81

Open
wnsvy607 wants to merge 8 commits into
mainfrom
feat/TMT-222
Open

[TMT-222] feat: 그룹 상세·리뷰 목록 실구현 — 게이트 마스킹·커서#81
wnsvy607 wants to merge 8 commits into
mainfrom
feat/TMT-222

Conversation

@wnsvy607

Copy link
Copy Markdown
Contributor

TMT-222 — 그룹 상세·리뷰 목록 실구현

스택 PR — base가 feat/TMT-221(#80)입니다. 머지 순서: #79#80 → 이 PR.

GET /v1/groups/{groupId} · GET /v1/groups/{groupId}/reviews를 실 DB로 옮깁니다. 미가입자에게 무엇을 가리느냐가 핵심이고, 게이트 정책은 TMT-216(전체 페이지네이션 + 서버 마스킹) 그대로입니다.

설계 — 새 코드가 얇습니다

  • 상세: TMT-221에서 분리해 둔 GroupDetailComposer를 그대로 사용 — 컨트롤러 한 장. 지표 3종은 groups 파생 집계(D3), 커버는 공유 리뷰 사진 최신순 5장(G16)
  • 리뷰 목록: ReviewCardComposer + NearbyReviewRowView projection 재사용 (근처·가게 상세와 같은 카드 파이프라인). 커서도 가게 상세와 같은 (created_at, review_id) 내림차순
  • 마스킹은 응답 매퍼 한 곳: toResponse(masked) — 미가입이면 content·aiSummary.cons를 지운다. contentLength는 원본 코드포인트 기준이라 마스킹과 무관 (FE가 블러 자리를 잡는 값, TMT-147)
  • 미가입도 회원과 같은 커서 경로hasNext·nextCursor가 똑같이 채워진다

검증 — 로컬 E2E (승인 기준 전부)

  • 지표 3종 (2/3/2), 커버 3장 — 5장 미만 그룹도 성립, 리뷰 최신순·photo_order 정렬 확인
  • 미가입 응답: content=null·cons=null, pros·rating·photos·place·author는 그대로
  • contentLength — 이모지 포함 본문이 8 코드포인트로 가입/미가입 동일
  • gate{gated, reason} 두 필드 (visibleCount 없음)
  • 같은 created_at 리뷰 2건이 경계에 걸린 limit=1 순회 — 중복·누락 없음
  • 비회원도 hasNext·nextCursor 채워짐, 다른 그룹 커서 → INVALID_CURSOR, 없는 그룹 → 404

./gradlew build 통과 (신규 컨트롤러 테스트 5개).

mock 정리

GroupMockController는 마지막 두 핸들러가 이번 대상이라 클래스째 삭제했습니다. mock GroupAssembler는 홈·마이페이지가 쓰는 카드 조립만 남기고 상세 조립(죽은 코드)을 걷어냈습니다.

남은 그룹 레인

TMT-223(공유 집합 GET·PUT)이 마지막입니다 — 이어서 진행합니다.

🤖 Generated with Claude Code

wnsvy607 and others added 3 commits August 31, 2026 19:08
정렬 3종을 (k1,k2,id) 한 형태로 통일해 키셋 하나로 처리. RECOMMENDED는 (일치 저장 수, 멤버 수)이고 조회자가 커서 조건에 들어간다. 검색은 그룹명·한줄 소개 ILIKE + 태그 라벨 매칭(앱에서 id로 풀어 전달, G18). 커버는 공유 리뷰 최신 사진 1장(G16), 집계는 groups 비정규화 컬럼(D3). 태그 정본을 GroupTagCatalog(application)로 올리고 mock GroupTags는 위임으로 전환. mock의 listGroups·nameAvailability 핸들러와 GroupTagMockController 제거.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이름은 groups.name UNIQUE가 정본 — saveAndFlush로 제약 위반을 즉시 터뜨려 경합 포함 GROUP_NAME_DUPLICATED로 변환. 생성자는 자동 멤버(G11·G13, member_count 기본 1이 생성자 몫). 대표 이미지는 attach/detach로 STAGED-ATTACHED 전이(M4·M7). 상세 조립을 GroupDetailComposer로 분리 — 상세 조회(TMT-222)가 재사용한다. GroupStatsPort 구현은 TMT-224에서 이미 들어와 있어 검증만 했다. mock의 createGroup·updateGroup 핸들러 제거.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
상세는 GroupDetailComposer(TMT-221) 재사용 — 지표 3종은 groups 파생 집계, 커버는 공유 리뷰 사진 최신순 5장(G16). 리뷰 목록은 미가입도 같은 커서 경로를 타고 카드의 content·aiSummary.cons만 서버가 지운다(G1, TMT-216) — toResponse(masked)로 분기, contentLength는 원본 코드포인트 기준. ReviewCardComposer·NearbyReviewRowView 재사용. GroupMockController 삭제(마지막 두 핸들러가 이번 대상), mock GroupAssembler는 카드 조립만 남김.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@mingdodev mingdodev left a comment

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.

새로 들어온 keyset 쿼리·커서 규격·게이트 경로와 contentLength를 원본 본문 기준으로 유지한 처리는 문제없어 보입니다. [want] 하나만 라인에 남겼습니다.

photos = photos.map { ReviewCardResponse.Photo(PublicIds.savePhoto(it.photoId), it.url, it.order) },
aiSummary = aiSummary?.let { ReviewCardResponse.AiSummary(it.pros, it.cons) },
content = content,
aiSummary = aiSummary?.let { ReviewCardResponse.AiSummary(it.pros, if (masked) null else it.cons) },

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.

[want] masked 파라미터가 이 PR에서 死코드가 된 것으로 보입니다.

masked = true를 넘기던 유일한 호출부가 삭제된 GroupMockController라, 남은 호출부는 전부 기본값 false이고 여기와 아래 content 분기는 도달하지 않습니다. 게이트 마스킹이 GroupReviewQueryService 쪽으로 옮겨간 결과라면 파라미터째 걷어내도 될 것 같습니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

절반만 맞는 지적이라 절반만 걷어냈습니다 (37a9732) — 실구현 toResponse(masked)는 죽지 않았습니다. GroupQueryController.kt:82toResponse(masked = result.gated)로 호출하는, 게이트 마스킹의 본체예요 (리터럴 masked = true가 없어 grep에 안 걸렸을 것 같습니다). 죽은 건 mock ReviewCardAssembler의 masked 파라미터라 그쪽만 제거했습니다.

wnsvy607 and others added 4 commits September 2, 2026 01:54
- DataIntegrityViolationException은 groups_name_key일 때만 GROUP_NAME_DUPLICATED (FK 위반이 409로 둔갑하지 않게)
- name 50자·oneLineDescription 100자 코드 포인트 검증 (없으면 51자가 "이미 있는 그룹명"으로 나갔다)
- 그룹 상세 라벨은 카탈로그에 없는 id면 id 폴백 — 조회가 500이 되지 않게
- MediaUrlResolver 신설: base-url 빈 값이면 기동 실패. 탐색·상세·리뷰카드 세 사용처 통일

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
실구현 toResponse(masked)는 GroupQueryController가 게이트 결과로 호출하는 살아있는 경로라 유지.
mock 쪽만 GroupMockController 삭제로 사용처가 사라져 걷어낸다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wnsvy607
wnsvy607 requested a review from mingdodev September 1, 2026 17:06
Base automatically changed from feat/TMT-221 to main September 2, 2026 01:22
@wnsvy607 wnsvy607 closed this Sep 2, 2026
@wnsvy607 wnsvy607 reopened this Sep 2, 2026
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.

2 participants