diff --git a/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java b/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java index 0c57f04..e480052 100644 --- a/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java +++ b/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java @@ -198,10 +198,18 @@ public class RestaurantController { if (!results.isEmpty()) { String url = String.valueOf(results.get(0).get("url")); String title = String.valueOf(results.get(0).get("title")); - restaurantService.update(r.getId(), Map.of("tabling_url", url)); - linked++; - emit(emitter, Map.of("type", "done", "current", i + 1, - "name", r.getName(), "url", url, "title", title)); + if (isNameSimilar(r.getName(), title)) { + restaurantService.update(r.getId(), Map.of("tabling_url", url)); + linked++; + emit(emitter, Map.of("type", "done", "current", i + 1, + "name", r.getName(), "url", url, "title", title)); + } else { + restaurantService.update(r.getId(), Map.of("tabling_url", "NONE")); + notFound++; + log.info("[TABLING] Name mismatch: '{}' vs '{}', skipping", r.getName(), title); + emit(emitter, Map.of("type", "notfound", "current", i + 1, + "name", r.getName(), "reason", "이름 불일치: " + title)); + } } else { restaurantService.update(r.getId(), Map.of("tabling_url", "NONE")); notFound++; @@ -246,6 +254,23 @@ public class RestaurantController { return Map.of("ok", true); } + /** 테이블링/캐치테이블 매핑 초기화 */ + @DeleteMapping("/reset-tabling") + public Map resetTabling() { + AuthUtil.requireAdmin(); + restaurantService.resetTablingUrls(); + cache.flush(); + return Map.of("ok", true); + } + + @DeleteMapping("/reset-catchtable") + public Map resetCatchtable() { + AuthUtil.requireAdmin(); + restaurantService.resetCatchtableUrls(); + cache.flush(); + return Map.of("ok", true); + } + /** 단건 캐치테이블 URL 검색 */ @GetMapping("/{id}/catchtable-search") public List> catchtableSearch(@PathVariable String id) { @@ -311,10 +336,18 @@ public class RestaurantController { if (!results.isEmpty()) { String url = String.valueOf(results.get(0).get("url")); String title = String.valueOf(results.get(0).get("title")); - restaurantService.update(r.getId(), Map.of("catchtable_url", url)); - linked++; - emit(emitter, Map.of("type", "done", "current", i + 1, - "name", r.getName(), "url", url, "title", title)); + if (isNameSimilar(r.getName(), title)) { + restaurantService.update(r.getId(), Map.of("catchtable_url", url)); + linked++; + emit(emitter, Map.of("type", "done", "current", i + 1, + "name", r.getName(), "url", url, "title", title)); + } else { + restaurantService.update(r.getId(), Map.of("catchtable_url", "NONE")); + notFound++; + log.info("[CATCHTABLE] Name mismatch: '{}' vs '{}', skipping", r.getName(), title); + emit(emitter, Map.of("type", "notfound", "current", i + 1, + "name", r.getName(), "reason", "이름 불일치: " + title)); + } } else { restaurantService.update(r.getId(), Map.of("catchtable_url", "NONE")); notFound++; @@ -489,6 +522,31 @@ public class RestaurantController { return results; } + /** + * 식당 이름과 검색 결과 제목의 유사도 검사. + * 한쪽 이름이 다른쪽에 포함되거나, 공통 글자 비율이 40% 이상이면 유사하다고 판단. + */ + private boolean isNameSimilar(String restaurantName, String resultTitle) { + String a = normalize(restaurantName); + String b = normalize(resultTitle); + if (a.isEmpty() || b.isEmpty()) return false; + + // 포함 관계 체크 + if (a.contains(b) || b.contains(a)) return true; + + // 공통 문자 비율 (Jaccard-like) + var setA = a.chars().boxed().collect(java.util.stream.Collectors.toSet()); + var setB = b.chars().boxed().collect(java.util.stream.Collectors.toSet()); + long common = setA.stream().filter(setB::contains).count(); + double ratio = (double) common / Math.max(setA.size(), setB.size()); + return ratio >= 0.4; + } + + private String normalize(String s) { + if (s == null) return ""; + return s.replaceAll("[\\s·\\-_()()\\[\\]【】]", "").toLowerCase(); + } + private void emit(SseEmitter emitter, Map data) { try { emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(data))); 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 d38bc9d..04c5675 100644 --- a/backend-java/src/main/java/com/tasteby/mapper/RestaurantMapper.java +++ b/backend-java/src/main/java/com/tasteby/mapper/RestaurantMapper.java @@ -59,6 +59,10 @@ public interface RestaurantMapper { List findWithoutCatchtable(); + void resetTablingUrls(); + + void resetCatchtableUrls(); + List> findForRemapCuisine(); List> findForRemapFoods(); 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 3a5603a..ab7eb74 100644 --- a/backend-java/src/main/java/com/tasteby/service/RestaurantService.java +++ b/backend-java/src/main/java/com/tasteby/service/RestaurantService.java @@ -34,6 +34,16 @@ public class RestaurantService { return mapper.findWithoutCatchtable(); } + @Transactional + public void resetTablingUrls() { + mapper.resetTablingUrls(); + } + + @Transactional + public void resetCatchtableUrls() { + mapper.resetCatchtableUrls(); + } + public Restaurant findById(String id) { Restaurant restaurant = mapper.findById(id); if (restaurant == null) return null; diff --git a/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml b/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml index d6c08a2..413d228 100644 --- a/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml +++ b/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml @@ -239,6 +239,14 @@ ORDER BY r.name + + UPDATE restaurants SET tabling_url = NULL WHERE tabling_url IS NOT NULL + + + + UPDATE restaurants SET catchtable_url = NULL WHERE catchtable_url IS NOT NULL + + diff --git a/frontend/dev-restart.sh b/frontend/dev-restart.sh new file mode 100755 index 0000000..48e3172 --- /dev/null +++ b/frontend/dev-restart.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Build + restart dev server (standalone mode) +set -euo pipefail +cd "$(dirname "$0")" + +echo "▶ Building..." +npm run build + +echo "▶ Copying static files to standalone..." +cp -r .next/static .next/standalone/.next/static +cp -r public .next/standalone/public 2>/dev/null || true + +echo "▶ Restarting PM2..." +pm2 restart tasteby-web + +echo "✅ Done — http://localhost:3001" diff --git a/frontend/public/logo-120h.png b/frontend/public/logo-120h.png new file mode 100644 index 0000000..6ec00f4 Binary files /dev/null and b/frontend/public/logo-120h.png differ diff --git a/frontend/public/logo-200h.png b/frontend/public/logo-200h.png new file mode 100644 index 0000000..029ffb8 Binary files /dev/null and b/frontend/public/logo-200h.png differ diff --git a/frontend/public/logo-80h.png b/frontend/public/logo-80h.png new file mode 100644 index 0000000..776b511 Binary files /dev/null and b/frontend/public/logo-80h.png differ diff --git a/frontend/public/logo-dark-120h.png b/frontend/public/logo-dark-120h.png new file mode 100644 index 0000000..18ff48b Binary files /dev/null and b/frontend/public/logo-dark-120h.png differ diff --git a/frontend/public/logo-dark-80h.png b/frontend/public/logo-dark-80h.png new file mode 100644 index 0000000..ffc30c5 Binary files /dev/null and b/frontend/public/logo-dark-80h.png differ diff --git a/frontend/public/logo-dark.png b/frontend/public/logo-dark.png new file mode 100644 index 0000000..34605c8 Binary files /dev/null and b/frontend/public/logo-dark.png differ diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000..73fd597 Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index 949c328..a56b9a3 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -41,33 +41,37 @@ export default function AdminPage() { const isAdmin = user?.is_admin === true; if (isLoading) { - return
로딩 중...
; + return
로딩 중...
; } if (!user) { return ( -
+

로그인이 필요합니다

- 메인으로 돌아가기 + 메인으로 돌아가기
); } return ( -
-
+
+
-

Tasteby Admin

+ + + Tasteby + + Admin {!isAdmin && ( 읽기 전용 )}
{isAdmin && } - + ← 메인으로
@@ -79,8 +83,8 @@ export default function AdminPage() { onClick={() => setTab(t)} className={`px-4 py-2 text-sm rounded-t font-medium ${ tab === t - ? "bg-blue-600 text-white" - : "bg-gray-200 text-gray-700 hover:bg-gray-300" + ? "bg-brand-600 text-white" + : "bg-brand-50 text-brand-700 hover:bg-brand-100" }`} > {t === "channels" ? "채널 관리" : t === "videos" ? "영상 관리" : t === "restaurants" ? "식당 관리" : t === "users" ? "유저 관리" : "데몬 설정"} @@ -171,40 +175,40 @@ function ChannelsPanel({ isAdmin }: { isAdmin: boolean }) { return (
- {isAdmin &&
+ {isAdmin &&

채널 추가

setNewId(e.target.value)} - className="border rounded px-3 py-2 flex-1 text-sm bg-white text-gray-900" + className="border rounded px-3 py-2 flex-1 text-sm bg-surface text-gray-900" /> setNewName(e.target.value)} - className="border rounded px-3 py-2 flex-1 text-sm bg-white text-gray-900" + className="border rounded px-3 py-2 flex-1 text-sm bg-surface text-gray-900" /> setNewFilter(e.target.value)} - className="border rounded px-3 py-2 w-40 text-sm bg-white text-gray-900" + className="border rounded px-3 py-2 w-40 text-sm bg-surface text-gray-900" />
} -
+
- + @@ -219,14 +223,14 @@ function ChannelsPanel({ isAdmin }: { isAdmin: boolean }) { {channels.map((ch) => ( - +
채널 이름 Channel ID
{ch.channel_name} {ch.channel_id} {ch.title_filter ? ( - + {ch.title_filter} ) : ( @@ -236,7 +240,7 @@ function ChannelsPanel({ isAdmin }: { isAdmin: boolean }) { {editingChannel === ch.id ? ( setEditDesc(e.target.value)} - className="border rounded px-2 py-1 text-xs w-32 bg-white text-gray-900" placeholder="설명" /> + className="border rounded px-2 py-1 text-xs w-32 bg-surface text-gray-900" placeholder="설명" /> ) : ( { if (!isAdmin) return; @@ -248,8 +252,8 @@ function ChannelsPanel({ isAdmin }: { isAdmin: boolean }) { {editingChannel === ch.id ? (
setEditTags(e.target.value)} - className="border rounded px-2 py-1 text-xs w-40 bg-white text-gray-900" placeholder="태그 (쉼표 구분)" /> - + className="border rounded px-2 py-1 text-xs w-40 bg-surface text-gray-900" placeholder="태그 (쉼표 구분)" /> +
) : ( @@ -262,7 +266,7 @@ function ChannelsPanel({ isAdmin }: { isAdmin: boolean }) {
{editingChannel === ch.id ? ( setEditOrder(Number(e.target.value))} - className="border rounded px-2 py-1 text-xs w-14 text-center bg-white text-gray-900" min={1} /> + className="border rounded px-2 py-1 text-xs w-14 text-center bg-surface text-gray-900" min={1} /> ) : ( {ch.sort_order ?? 99} )} @@ -277,7 +281,7 @@ function ChannelsPanel({ isAdmin }: { isAdmin: boolean }) { {isAdmin && @@ -738,7 +742,7 @@ function VideosPanel({ isAdmin }: { isAdmin: boolean }) { const statusColor: Record = { pending: "bg-yellow-100 text-yellow-800", - processing: "bg-blue-100 text-blue-800", + processing: "bg-brand-100 text-brand-800", done: "bg-green-100 text-green-800", error: "bg-red-100 text-red-800", skip: "bg-gray-100 text-gray-600", @@ -750,7 +754,7 @@ function VideosPanel({ isAdmin }: { isAdmin: boolean }) { setStatusFilter(e.target.value)} - className="border rounded px-3 py-2 text-sm bg-white text-gray-900" + className="border rounded px-3 py-2 text-sm bg-surface text-gray-900" > @@ -776,7 +780,7 @@ function VideosPanel({ isAdmin }: { isAdmin: boolean }) { value={titleSearch} onChange={(e) => { setTitleSearch(e.target.value); setPage(0); }} onKeyDown={(e) => e.key === "Escape" && setTitleSearch("")} - className="border border-r-0 rounded-l px-3 py-2 text-sm w-48 bg-white text-gray-900" + className="border border-r-0 rounded-l px-3 py-2 text-sm w-48 bg-surface text-gray-900" /> {titleSearch ? (