[TMT-223] feat: 리뷰 공유 집합 실구현 — GET·PUT 전체 교체·파생 집계 반영 - #82
Conversation
PUT은 집합 교체다 — NOT IN 삭제 + ON CONFLICT DO NOTHING 삽입이라 양쪽 다 멱등. 교체와 GroupStatsPort.refreshShareStats(group_place 재구축·지표 재계산)가 한 트랜잭션이다(TX-4). 내 리뷰 검증은 unnest 배열로 한 번에, 미가입은 GROUP_MEMBERSHIP_REQUIRED(403). GroupReviewSharePort의 share/unshareAllByUser는 TMT-224 기구현이라 replaceUserShares만 추가. mock의 review-shares 핸들러 2개 제거. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| groupReviewSharePort.replaceUserShares(groupId, userId, distinct) | ||
| groupStatsPort.refreshShareStats(groupId) | ||
|
|
||
| val shared = groupShareQueryPort.findSharedReviewIds(groupId, userId) |
There was a problem hiding this comment.
[must] replace가 돌려주는 sharedCount/sharedReviewIds는 findSharedReviewIds(...) 결과인데, 이 조회에는 review.deleted_at IS NULL 필터가 없습니다.
GET 경로의 countSharedByUser(GroupShareQueryRepository.kt:49)는 필터하고 있어서, 소프트 삭제된 리뷰가 공유에 남아 있으면 PUT 응답과 직후 GET의 값이 어긋납니다. 화면이 PUT 응답으로 카운트를 갱신하면 새로고침에서 숫자가 바뀌는 형태로 보일 것 같습니다.
There was a problem hiding this comment.
반영했습니다 (d2a34b6) — findSharedReviewIds에 JOIN review ... deleted_at IS NULL을 넣어 countSharedByUser(GET)와 같은 기준으로 맞췄습니다. PUT 응답과 직후 GET이 항상 같은 값을 냅니다.
| private val groupReviewQueryPort: GroupReviewQueryPort, | ||
| private val groupReviewSharePort: GroupReviewSharePort, | ||
| private val groupStatsPort: GroupStatsPort, | ||
| @param:Value("\${tmt.media.base-url:}") private val mediaBaseUrl: String, |
There was a problem hiding this comment.
[must] 빈 기본값 때문에 프로퍼티가 없으면 thumbnailUrl이 /{s3Key} 상대경로로 조용히 나갑니다.
TMT-221의 GroupDetailComposer와 같은 형태라 두 곳을 같은 방식으로 정하면 좋겠습니다 — 기동 시 실패 쪽을 제안드립니다.
There was a problem hiding this comment.
반영했습니다 (d2a34b6) — #80에서 신설한 MediaUrlResolver(빈 값이면 기동 실패)로 교체해 두 곳이 같은 방식입니다.
- findSharedReviewIds에 review.deleted_at IS NULL JOIN — countSharedByUser(GET)와 같은 기준으로 PUT/GET 정합 - GroupShareService의 base-url 직접 조립을 MediaUrlResolver(기동 검증)로 교체 — TMT-221과 동일 방식 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MediaAttachmentService도 MediaUrlResolver 주입 — tmt.media.base-url을 읽는 곳이 하나가 된다 - 실구현 그룹 컨트롤러 5곳의 Swagger 태그에서 (mock) 제거 - parseGroupId를 PublicIds로 승격 — 접두 표기 규칙이 한 파일에 모인다 - GroupRegionTagRepository의 clearAutomatically 제거 (flush만 필요), @EmbeddedId merge 선행 SELECT는 주석으로 감수 명시 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TMT-223 — 리뷰 공유 집합 실구현 (그룹 레인 마지막)
GET·PUT /v1/groups/{groupId}/review-shares를 실 DB로 옮깁니다. 응답 형태는 mock과 같고, 리뷰 ID 표기만 실구현 표준(rv_)입니다 — mock은review_접두였는데 근처·상세 실구현이 이미rv_라 카드와 일관되게 맞췄습니다 (계약 문서 §4의 ID 표기 결정 그대로).설계
NOT IN삭제 +ON CONFLICT DO NOTHING삽입. 양쪽 다 멱등이라 재시도가 안전하고,Idempotency-Key가 필요 없는 이유가 코드에 그대로 드러납니다GroupStatsPort.refreshShareStats가group_place를 재구축하고review_count·place_count를 다시 셉니다. 같은 매장에 공유가 겹쳐도(D3) 행 수와 지표가 어긋나지 않습니다unnest(bigint[])한 방 — 남의 리뷰·없는 리뷰가 섞이면 그 id로REVIEW_NOT_FOUNDGroupReviewSharePort의share(가입 자동 공유)·unshareAllByUser(탈퇴 일괄 해제)는 TMT-224에서 이미 구현돼 있어replaceUserShares만 추가했습니다검증 — 로컬 E2E (승인 기준 전부)
isShared·첫 사진 썸네일·sharedCount[rv_1,rv_3]→ 지표(2,1)·group_place{p1:2}/ PUT[rv_2]→ rv_1·rv_3 해제, 지표(1,1)/ PUT[]→ 전부 해제, 지표(0,0)GROUP_MEMBERSHIP_REQUIRED./gradlew build통과 (신규 컨트롤러 테스트 5개). mock review-shares 핸들러 2개·테스트 3개 제거.과도기 주의 (스택 4장 공통)
groups가 0행이라 머지 후 목록·상세가 빕니다 — TMT-231(시드 SQL)이 후속 필수GroupMembershipMockController)는 mock 인메모리 멤버십을 쓰므로, mock 가입으로는 실구현 PUT의 멤버십 검증(403)을 못 통과합니다. 가입·탈퇴 실구현 티켓이 다음 순서여야 합니다🤖 Generated with Claude Code