Material Symbols 아이콘 전환 + 로고 이미지 적용 + 테이블링 이름 유사도 체크
- 전체 인라인 SVG를 Google Material Symbols Rounded로 교체 - Icon 컴포넌트 추가, cuisine-icons 매핑 리팩토링 - Tasteby 핀 로고 이미지 적용 (라이트/다크 버전) - 테이블링/캐치테이블 이름 유사도 체크 및 리셋 API 추가 - 어드민 페이지 리셋 버튼 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<String, Object> resetTabling() {
|
||||
AuthUtil.requireAdmin();
|
||||
restaurantService.resetTablingUrls();
|
||||
cache.flush();
|
||||
return Map.of("ok", true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/reset-catchtable")
|
||||
public Map<String, Object> resetCatchtable() {
|
||||
AuthUtil.requireAdmin();
|
||||
restaurantService.resetCatchtableUrls();
|
||||
cache.flush();
|
||||
return Map.of("ok", true);
|
||||
}
|
||||
|
||||
/** 단건 캐치테이블 URL 검색 */
|
||||
@GetMapping("/{id}/catchtable-search")
|
||||
public List<Map<String, Object>> 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<String, Object> data) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(data)));
|
||||
|
||||
@@ -59,6 +59,10 @@ public interface RestaurantMapper {
|
||||
|
||||
List<Restaurant> findWithoutCatchtable();
|
||||
|
||||
void resetTablingUrls();
|
||||
|
||||
void resetCatchtableUrls();
|
||||
|
||||
List<Map<String, Object>> findForRemapCuisine();
|
||||
|
||||
List<Map<String, Object>> findForRemapFoods();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -239,6 +239,14 @@
|
||||
ORDER BY r.name
|
||||
</select>
|
||||
|
||||
<update id="resetTablingUrls">
|
||||
UPDATE restaurants SET tabling_url = NULL WHERE tabling_url IS NOT NULL
|
||||
</update>
|
||||
|
||||
<update id="resetCatchtableUrls">
|
||||
UPDATE restaurants SET catchtable_url = NULL WHERE catchtable_url IS NOT NULL
|
||||
</update>
|
||||
|
||||
<!-- ===== Remap operations ===== -->
|
||||
|
||||
<update id="updateCuisineType">
|
||||
|
||||
Reference in New Issue
Block a user