- GET /api/admin/restaurants/duplicates/place-id (어드민 전용) - 그룹별 식당 목록 + video/review/memo 카운트 동봉 - Mapper: findDuplicatePlaceIdRows + Service 그룹핑 - 정리/병합 + UNIQUE 제약은 데이터 위험 분리 위해 후속 PR로 Refs: #359 (조회 단계 완료) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
설계서: google_place_id 중복 조회 API (#359 1단계)
상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #359 · 1단계(조회 전용, 위험 0). 2단계(자동 병합) / 3단계(UNIQUE)는 별도 PR. · 구현 파일:
backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml,backend-java/src/main/java/com/tasteby/mapper/RestaurantMapper.java,backend-java/src/main/java/com/tasteby/service/RestaurantService.java,backend-java/src/main/java/com/tasteby/controller/AdminRestaurantController.java· 테스트: 본 범위 밖 (수동 — admin token으로 호출).
1. 목적 (Why)
같은 google_place_id에 다중 식당이 매핑된 경우 운영자가 어떤 것을 유지/병합할지 결정 필요. 본 단계는 조회만 — 그룹과 후보 식당을 메타데이터(연결된 영상/리뷰/메모 수)와 함께 보여줘 의사결정 자료 제공.
2. 범위
- 포함:
GET /api/admin/restaurants/duplicates/place-id— 운영자만, 그룹별 식당 + 카운트 동봉. - 제외 (별도 PR): 병합/삭제, UNIQUE constraint.
3. 인수조건
- requireAdmin 보호.
- 응답 구조:
[{ google_place_id, items: [{ id, name, address, created_at, video_count, review_count, memo_count, hidden }] }]. - 그룹은
COUNT(*) > 1만 반환.
4. SQL
SELECT r.id, r.google_place_id, r.name, r.address,
TO_CHAR(r.created_at, 'YYYY-MM-DD"T"HH24:MI:SS') AS created_at,
r.hidden,
(SELECT COUNT(*) FROM video_restaurants vr WHERE vr.restaurant_id = r.id) AS video_count,
(SELECT COUNT(*) FROM reviews rv WHERE rv.restaurant_id = r.id) AS review_count,
(SELECT COUNT(*) FROM memos mm WHERE mm.restaurant_id = r.id) AS memo_count
FROM restaurants r
WHERE r.google_place_id IN (
SELECT google_place_id FROM restaurants
WHERE google_place_id IS NOT NULL
GROUP BY google_place_id HAVING COUNT(*) > 1
)
ORDER BY r.google_place_id, r.created_at
Service 계층에서 google_place_id로 그룹핑하여 응답 구조 변환.
5. 엣지케이스
- 중복 0건 → 빈 배열.
- 누군가가 google_place_id를 동시에 변경 중 → 다음 호출에서 반영 (캐시 X).