Skip to content

[TMT-220] feat: 그룹 탐색 실구현 — 검색·필터·추천순 정렬·커서 - #79

Merged
wnsvy607 merged 1 commit into
mainfrom
feat/TMT-220
Sep 1, 2026
Merged

[TMT-220] feat: 그룹 탐색 실구현 — 검색·필터·추천순 정렬·커서#79
wnsvy607 merged 1 commit into
mainfrom
feat/TMT-220

Conversation

@wnsvy607

Copy link
Copy Markdown
Contributor

TMT-220 — 그룹 탐색 실구현

GET /v1/groups · GET /v1/group-tags · GET /v1/groups/name-availability를 실 DB로 옮깁니다. 응답 형태·ID 표기(group_)는 mock과 같습니다.

설계 — 정렬 3종을 키셋 하나로

정렬 키를 (k1, k2, groupId) 한 형태로 통일했습니다. 쿼리가 CASE :sort로 키를 고르고, 키셋 비교와 커서 스펙이 하나로 끝납니다.

sort k1 k2
RECOMMENDED (G17) 일치 저장 수 member_count
MEMBER_COUNT member_count 0
REVIEW_COUNT review_count 0
  • RECOMMENDED는 1차 키가 조회자마다 달라, 조회자가 커서 조건에 들어갑니다 — 조회자가 바뀌면 INVALID_CURSOR
  • CASE 정렬이라 groups_recommend_ix를 못 타지만 그룹은 수백 규모라 감수 (주석에 남김)
  • 일치 저장 수(G12)는 group_place(파생 집계, D3) ∩ 내 저장. 커버(G16)는 공유 리뷰 최신 사진 1장 LATERAL

검색 (G18)

검색 대상이 그룹명·한줄 소개·태그 라벨인데 라벨은 DB에 없습니다. 라벨 매칭은 앱이 태그 id 목록으로 풀어서 쿼리에 넘깁니다 — 예: "강서" → region_gangseo, "고기" → cat_meat.

태그 정본 이동

음식 14·지역 26을 GroupTagCatalog(application)로 올렸습니다. mock GroupTags는 위임으로 전환 — 남은 mock(생성·상세)이 지워질 때 함께 사라집니다. mock의 listGroups·nameAvailability 핸들러와 GroupTagMockController는 제거했습니다 (경로가 실구현과 겹치므로 같은 PR).

검증 — 합성 시드 로컬 E2E (승인 기준 전부)

  • 추천순: matched 2 그룹이 member 5 동률을 제치고 1위, 커버 URL 조립 확인
  • 비로그인: matched 전부 0 → (member DESC, id DESC) 동률 tie-break 확인
  • limit=1 키셋 순회: member 5·5 동률이 경계에 걸려도 중복·누락 없음
  • 검색 "강서"(이름+한줄+지역라벨) / "고기"(음식라벨) 정확
  • 필터 food·region, 미정의 태그 → GROUP_TAG_NOT_FOUND(400)
  • sort 3종 순서 정확, 미지원 값 → VALIDATION_FAILED
  • sort 변경·조회자 변경 커서 → INVALID_CURSOR
  • name-availability: 중복 false·가용 true·비로그인 401
  • group-tags: 음식 14·지역 26

./gradlew build 통과 (신규 테스트 11개 — 서비스 6·컨트롤러 5).

참고

  • DB groups가 현재 0행이라 머지해도 목록이 빕니다 — TMT-231(시드 SQL)이 후속입니다
  • 다음: TMT-221(생성·편집) → 222(상세) → 223(공유 집합)

🤖 Generated with Claude Code

정렬 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>
WHERE t.group_id = g.id AND t.region_tag_id = ANY(string_to_array(:regionCsv, ','))
))
AND (CAST(:query AS text) IS NULL
OR g.name ILIKE '%' || :query || '%'

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] 검색어의 LIKE 메타문자가 그대로 나갑니다.

%가 든 검색어를 넣으면 '%' || '%' || '%'가 되어 전체 매칭이 됩니다. _도 한 글자 와일드카드로 동작하고요. 사용자가 의도적으로 넣는 경우는 드물지만 검색창에 %를 치면 전 그룹이 나오는 게 좀 이상합니다.

replace(replace(:query, '\\', '\\\\'), '%', '\\%') 식으로 이스케이프하거나, 앱에서 정제해 넘기는 방법이 있습니다.

같은 패턴이 매장 검색(#72)에도 있어서 레포 공통 이슈입니다. 이 PR에서 꼭 고쳐야 한다기보다, 어디서 한 번 정리할지 정하면 좋겠습니다 — 검색어 정제 유틸을 하나 두는 쪽이 나을 수도 있고요.

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.

동의합니다 — 매장 검색(#72, main의 E9 술어)과 공통이라 유틸 하나로 한 번에 정리하는 쪽이 맞고, TMT-296으로 만들었습니다. 이 PR에서는 기존 정책 그대로 두고(main과 동일 동작), 유틸이 생기면 그룹·매장·근처 탐색을 같이 갈아탑니다.

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.

아이고 제가 모르고 299도 만들었네요

@Param("limitPlusOne") limitPlusOne: Int,
): List<GroupCardRowView>

fun existsByName(name: String): Boolean

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.

[q] 그룹명 중복 판정이 대소문자·공백을 구분합니다.

existsByName이 정확히 일치할 때만 true라 TMTtmt가 둘 다 생성됩니다. DB의 name VARCHAR(50) NOT NULL UNIQUE와 동일한 기준이라 구현은 일관됩니다 — 검사에서 통과한 이름이 INSERT에서 막히는 일은 없습니다.

다만 G6(그룹명 유일)의 의도가 "표기가 달라도 같은 이름은 하나"인지 확인이 필요해 보입니다. 그렇다면 lower(trim(name)) 기준 유니크 인덱스가 필요하고 마이그레이션이 따라옵니다.

TMT-221(생성)이 이 판정을 그대로 쓸 테니 거기서 결론이 나면 좋겠습니다. 지금 형태로 간다면 명세 D_02에 "대소문자·공백을 구분한다"를 명시해두는 게 안전합니다.

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.

도메인 설계 v2의 G6 정본을 확인했는데 "그룹명은 중복 불가" 한 줄뿐이라 대소문자·공백 기준이 명세에 없습니다 — CLAUDE.md의 "명세에 없어서 내가 정했다" 기록 대상이네요. 현재 구현(DB UNIQUE와 동일한 정확 일치)을 유지하고, 변경 이력 문서의 "명세 반영이 필요한 것"에 "그룹명 중복 판정은 대소문자·공백을 구분한다(DB UNIQUE 기준)"로 적어두겠습니다. lower(trim(name)) 기준으로 가려면 유니크 인덱스 마이그레이션 + 기존 시드 충돌 검사가 따라와서, UX 요구가 확인되면 그때 별도 티켓으로 가는 게 맞다고 봅니다. #80(TMT-221 생성)도 같은 판정을 쓰니 결론은 거기 반영됩니다.

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.

좋습니다. 우선 대소문자와 공백을 구분하는 걸로 갑시다. 다만 trim은 나중에라도 조금 필요할 것 같네요 굿!

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

정렬 3종을 (k1, k2, groupId) 한 형태로 통일한 게 이 PR의 핵심이고, 잘 풀렸다고 봅니다. 키셋 비교와 커서 스펙이 하나로 끝나서 읽기도 쉽고, 같은 정렬 값이 경계에 걸려도 groupId가 받아줍니다.

특히 좋았던 것 셋입니다.

  • RECOMMENDED의 1차 키가 조회자마다 다르다는 걸 알아채고 조회자를 커서 조건에 넣은 것. 이게 없으면 로그인 상태가 바뀐 뒤 이전 커서로 이어갈 때 조용히 누락이 납니다
  • 태그 정본을 GroupTagCatalog(application)로 올리고 mock GroupTags를 위임으로 바꾼 것 — mock이 지워질 때 자연스럽게 같이 사라지는 구조입니다
  • 커버를 LATERAL 한 번으로 뽑은 것. 카드마다 조회했으면 N+1이었습니다

로컬 E2E를 승인 기준별로 다 돌리고 결과를 적어주신 것도 도움이 됐습니다. 특히 limit=1 키셋 순회로 동률 경계를 확인한 것 — CI가 못 보여주는 부분이라 이런 기록이 실제로 값이 있습니다.

CASE :sort 때문에 groups_recommend_ix를 못 탄다는 것도 본문과 주석에 남겨주셔서 판단이 쉬웠습니다. 그룹이 수백 규모면 동의합니다. 다만 세 정렬 모두 인덱스를 못 타게 된 셈이라(MEMBER_COUNT도 CASE 안에 들어가 있어서), 그 인덱스를 계속 둘 이유가 있는지는 한 번 생각해볼 만합니다.

라인 코멘트 3개 남겼습니다. [must]는 없고 전부 질문·제안입니다.

후속 PR 두 개는 CI가 안 돕니다

#80(base=feat/TMT-220) · #81(base=feat/TMT-221)에 자동 검증이 붙어 있지 않습니다. ci-pull-request.ymlbranches: [main]에만 걸려 있어서 base가 main이 아닌 PR은 트리거되지 않습니다.

이 PR이 머지되면 #80의 base가 main으로 자동 재타겟되면서 그때 처음 돕니다. 순서대로 머지하시되 각 단계에서 CI 결과를 확인해주세요. #76(준형 인증)도 같은 상태라, 스택으로 쌓을 때 공통으로 걸리는 부분입니다.

DB가 비어 있는 것

본문에 적어주신 대로 groups가 0행이라 머지해도 목록이 빕니다. TMT-231(시드 SQL)이 후속인 것 맞고, 지금은 main 자동 배포가 꺼져 있어(TMT-290) 빈 목록이 사용자에게 보일 일은 없습니다. 릴리즈 태그 전까지 채우면 됩니다.

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

좋습니다~

@wnsvy607
wnsvy607 merged commit 4e4066a into main Sep 1, 2026
3 checks passed
@wnsvy607
wnsvy607 deleted the feat/TMT-220 branch September 1, 2026 16:54
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