[TMT-220] feat: 그룹 탐색 실구현 — 검색·필터·추천순 정렬·커서 - #79
Conversation
정렬 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 || '%' |
There was a problem hiding this comment.
[want] 검색어의 LIKE 메타문자가 그대로 나갑니다.
%가 든 검색어를 넣으면 '%' || '%' || '%'가 되어 전체 매칭이 됩니다. _도 한 글자 와일드카드로 동작하고요. 사용자가 의도적으로 넣는 경우는 드물지만 검색창에 %를 치면 전 그룹이 나오는 게 좀 이상합니다.
replace(replace(:query, '\\', '\\\\'), '%', '\\%') 식으로 이스케이프하거나, 앱에서 정제해 넘기는 방법이 있습니다.
같은 패턴이 매장 검색(#72)에도 있어서 레포 공통 이슈입니다. 이 PR에서 꼭 고쳐야 한다기보다, 어디서 한 번 정리할지 정하면 좋겠습니다 — 검색어 정제 유틸을 하나 두는 쪽이 나을 수도 있고요.
There was a problem hiding this comment.
동의합니다 — 매장 검색(#72, main의 E9 술어)과 공통이라 유틸 하나로 한 번에 정리하는 쪽이 맞고, TMT-296으로 만들었습니다. 이 PR에서는 기존 정책 그대로 두고(main과 동일 동작), 유틸이 생기면 그룹·매장·근처 탐색을 같이 갈아탑니다.
| @Param("limitPlusOne") limitPlusOne: Int, | ||
| ): List<GroupCardRowView> | ||
|
|
||
| fun existsByName(name: String): Boolean |
There was a problem hiding this comment.
[q] 그룹명 중복 판정이 대소문자·공백을 구분합니다.
existsByName이 정확히 일치할 때만 true라 TMT와 tmt가 둘 다 생성됩니다. DB의 name VARCHAR(50) NOT NULL UNIQUE와 동일한 기준이라 구현은 일관됩니다 — 검사에서 통과한 이름이 INSERT에서 막히는 일은 없습니다.
다만 G6(그룹명 유일)의 의도가 "표기가 달라도 같은 이름은 하나"인지 확인이 필요해 보입니다. 그렇다면 lower(trim(name)) 기준 유니크 인덱스가 필요하고 마이그레이션이 따라옵니다.
TMT-221(생성)이 이 판정을 그대로 쓸 테니 거기서 결론이 나면 좋겠습니다. 지금 형태로 간다면 명세 D_02에 "대소문자·공백을 구분한다"를 명시해두는 게 안전합니다.
There was a problem hiding this comment.
도메인 설계 v2의 G6 정본을 확인했는데 "그룹명은 중복 불가" 한 줄뿐이라 대소문자·공백 기준이 명세에 없습니다 — CLAUDE.md의 "명세에 없어서 내가 정했다" 기록 대상이네요. 현재 구현(DB UNIQUE와 동일한 정확 일치)을 유지하고, 변경 이력 문서의 "명세 반영이 필요한 것"에 "그룹명 중복 판정은 대소문자·공백을 구분한다(DB UNIQUE 기준)"로 적어두겠습니다. lower(trim(name)) 기준으로 가려면 유니크 인덱스 마이그레이션 + 기존 시드 충돌 검사가 따라와서, UX 요구가 확인되면 그때 별도 티켓으로 가는 게 맞다고 봅니다. #80(TMT-221 생성)도 같은 판정을 쓰니 결론은 거기 반영됩니다.
There was a problem hiding this comment.
좋습니다. 우선 대소문자와 공백을 구분하는 걸로 갑시다. 다만 trim은 나중에라도 조금 필요할 것 같네요 굿!
mingdodev
left a comment
There was a problem hiding this comment.
정렬 3종을 (k1, k2, groupId) 한 형태로 통일한 게 이 PR의 핵심이고, 잘 풀렸다고 봅니다. 키셋 비교와 커서 스펙이 하나로 끝나서 읽기도 쉽고, 같은 정렬 값이 경계에 걸려도 groupId가 받아줍니다.
특히 좋았던 것 셋입니다.
- RECOMMENDED의 1차 키가 조회자마다 다르다는 걸 알아채고 조회자를 커서 조건에 넣은 것. 이게 없으면 로그인 상태가 바뀐 뒤 이전 커서로 이어갈 때 조용히 누락이 납니다
- 태그 정본을
GroupTagCatalog(application)로 올리고 mockGroupTags를 위임으로 바꾼 것 — 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.yml이 branches: [main]에만 걸려 있어서 base가 main이 아닌 PR은 트리거되지 않습니다.
이 PR이 머지되면 #80의 base가 main으로 자동 재타겟되면서 그때 처음 돕니다. 순서대로 머지하시되 각 단계에서 CI 결과를 확인해주세요. #76(준형 인증)도 같은 상태라, 스택으로 쌓을 때 공통으로 걸리는 부분입니다.
DB가 비어 있는 것
본문에 적어주신 대로 groups가 0행이라 머지해도 목록이 빕니다. TMT-231(시드 SQL)이 후속인 것 맞고, 지금은 main 자동 배포가 꺼져 있어(TMT-290) 빈 목록이 사용자에게 보일 일은 없습니다. 릴리즈 태그 전까지 채우면 됩니다.
TMT-220 — 그룹 탐색 실구현
GET /v1/groups·GET /v1/group-tags·GET /v1/groups/name-availability를 실 DB로 옮깁니다. 응답 형태·ID 표기(group_)는 mock과 같습니다.설계 — 정렬 3종을 키셋 하나로
정렬 키를
(k1, k2, groupId)한 형태로 통일했습니다. 쿼리가CASE :sort로 키를 고르고, 키셋 비교와 커서 스펙이 하나로 끝납니다.groups_recommend_ix를 못 타지만 그룹은 수백 규모라 감수 (주석에 남김)group_place(파생 집계, D3) ∩ 내 저장. 커버(G16)는 공유 리뷰 최신 사진 1장 LATERAL검색 (G18)
검색 대상이 그룹명·한줄 소개·태그 라벨인데 라벨은 DB에 없습니다. 라벨 매칭은 앱이 태그 id 목록으로 풀어서 쿼리에 넘깁니다 — 예: "강서" →
region_gangseo, "고기" →cat_meat.태그 정본 이동
음식 14·지역 26을
GroupTagCatalog(application)로 올렸습니다. mockGroupTags는 위임으로 전환 — 남은 mock(생성·상세)이 지워질 때 함께 사라집니다. mock의listGroups·nameAvailability핸들러와GroupTagMockController는 제거했습니다 (경로가 실구현과 겹치므로 같은 PR).검증 — 합성 시드 로컬 E2E (승인 기준 전부)
GROUP_TAG_NOT_FOUND(400)VALIDATION_FAILEDINVALID_CURSOR./gradlew build통과 (신규 테스트 11개 — 서비스 6·컨트롤러 5).참고
groups가 현재 0행이라 머지해도 목록이 빕니다 — TMT-231(시드 SQL)이 후속입니다🤖 Generated with Claude Code