diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ecf49..85dd286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ ## 2026-06-15 +### ๐Ÿ” #359 1๋‹จ๊ณ„ โ€” google_place_id ์ค‘๋ณต ์กฐํšŒ API (v0.1.46) +- GET /api/admin/restaurants/duplicates/place-id (์–ด๋“œ๋ฏผ ์ „์šฉ) +- ์‘๋‹ต: ๊ทธ๋ฃน๋ณ„ ์‹๋‹น + video/review/memo ์นด์šดํŠธ (๋ณ‘ํ•ฉ ์˜์‚ฌ๊ฒฐ์ • ์ž๋ฃŒ) +- ์ •๋ฆฌ/๋ณ‘ํ•ฉ + UNIQUE ์ œ์•ฝ์€ ๋ณ„๋„ PR (๋ฐ์ดํ„ฐ ์œ„ํ—˜ ๋ถ„๋ฆฌ) +- ์„ค๊ณ„์„œ: docs/design/359a-duplicate-place-id-view/README.md +- Refs: #359 (์กฐํšŒ ๋‹จ๊ณ„ ์™„๋ฃŒ, ํ›„์† ๋ถ„๋ฆฌ ์œ ์ง€) + ### ๐Ÿ“‹ #358 RestaurantUpdateDTO + @Valid ํ‘œ์ค€ํ™” (v0.1.45) - dto/RestaurantUpdateDTO record ์‹ ๊ทœ (15 ํ•„๋“œ, ๋ชจ๋‘ nullable) - Bean Validation: @Size/@Pattern(URL or NONE)/@DecimalMinยทMax/@MinยทMax diff --git a/backend-java/src/main/java/com/tasteby/controller/AdminRestaurantController.java b/backend-java/src/main/java/com/tasteby/controller/AdminRestaurantController.java index 062f6f7..d4428a0 100644 --- a/backend-java/src/main/java/com/tasteby/controller/AdminRestaurantController.java +++ b/backend-java/src/main/java/com/tasteby/controller/AdminRestaurantController.java @@ -62,6 +62,15 @@ public class AdminRestaurantController { return Map.of("success", true, "id", id); } + // #359 1๋‹จ๊ณ„ โ€” google_place_id ์ค‘๋ณต ์กฐํšŒ (์ •๋ฆฌ/UNIQUE๋Š” ํ›„์†). + @GetMapping("/duplicates/place-id") + public Map duplicatePlaceIds() { + var admin = AuthUtil.requireAdmin(); + var groups = restaurantService.findDuplicatePlaceIdGroups(); + log.info("[ADMIN] {} duplicate place_id groups: {}", admin.getSubject(), groups.size()); + return Map.of("groups", groups, "group_count", groups.size()); + } + /** * ์–ด๋“œ๋ฏผ์šฉ hidden ํ† ๊ธ€. */ diff --git a/backend-java/src/main/java/com/tasteby/mapper/RestaurantMapper.java b/backend-java/src/main/java/com/tasteby/mapper/RestaurantMapper.java index be6eae4..8f270b7 100644 --- a/backend-java/src/main/java/com/tasteby/mapper/RestaurantMapper.java +++ b/backend-java/src/main/java/com/tasteby/mapper/RestaurantMapper.java @@ -39,6 +39,9 @@ public interface RestaurantMapper { int countUnevaluatedLinks(); + // #359 1๋‹จ๊ณ„ โ€” google_place_id ์ค‘๋ณต ์กฐํšŒ + List> findDuplicatePlaceIdRows(); + Restaurant findById(@Param("id") String id); List> findVideoLinks(@Param("restaurantId") String restaurantId, diff --git a/backend-java/src/main/java/com/tasteby/service/RestaurantService.java b/backend-java/src/main/java/com/tasteby/service/RestaurantService.java index 8448e5d..e1397e6 100644 --- a/backend-java/src/main/java/com/tasteby/service/RestaurantService.java +++ b/backend-java/src/main/java/com/tasteby/service/RestaurantService.java @@ -111,6 +111,23 @@ public class RestaurantService { return mapper.countUnevaluatedLinks(); } + // #359 1๋‹จ๊ณ„ โ€” google_place_id ์ค‘๋ณต ๊ทธ๋ฃน (์ฐธ์กฐ ์นด์šดํŠธ ๋™๋ด‰) + public List> findDuplicatePlaceIdGroups() { + var rows = mapper.findDuplicatePlaceIdRows().stream() + .map(JsonUtil::lowerKeys) + .toList(); + Map>> grouped = new LinkedHashMap<>(); + for (var r : rows) { + String key = (String) r.get("google_place_id"); + grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(r); + } + List> out = new ArrayList<>(grouped.size()); + for (var e : grouped.entrySet()) { + out.add(Map.of("google_place_id", e.getKey(), "items", e.getValue())); + } + return out; + } + public void update(String id, Map fields) { mapper.updateFields(id, fields); } diff --git a/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml b/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml index afef3bb..df6fd14 100644 --- a/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml +++ b/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml @@ -353,4 +353,21 @@ SELECT COUNT(*) FROM video_restaurants WHERE relevance_evaluated_at IS NULL + + + diff --git a/docs/design/359a-duplicate-place-id-view/README.md b/docs/design/359a-duplicate-place-id-view/README.md new file mode 100644 index 0000000..25bbf1f --- /dev/null +++ b/docs/design/359a-duplicate-place-id-view/README.md @@ -0,0 +1,47 @@ +# ์„ค๊ณ„์„œ: 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).