# 설계서: 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 ```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).