diff --git a/.gitignore b/.gitignore index ade2343..38331ee 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ k8s/secrets.yaml # OS / misc .DS_Store backend/cookies.txt +backend-java/cookies.txt +**/cookies.txt diff --git a/backend-java/src/main/java/com/tasteby/service/ExtractorService.java b/backend-java/src/main/java/com/tasteby/service/ExtractorService.java index c45475a..908cbe0 100644 --- a/backend-java/src/main/java/com/tasteby/service/ExtractorService.java +++ b/backend-java/src/main/java/com/tasteby/service/ExtractorService.java @@ -38,7 +38,7 @@ public class ExtractorService { %s - price_range: 가격대 (예: 1만원대, 2-3만원) (string | null) - foods_mentioned: 언급된 대표 메뉴 (string[], 최대 10개, 우선순위 높은 순, 반드시 한글로 작성) - - evaluation: 평가 내용 (string | null) + - evaluation: 평가 내용을 100자 이내로 요약 (string | null) - guests: 함께한 게스트 (string[]) 영상 제목: {title} @@ -62,6 +62,10 @@ public class ExtractorService { */ @SuppressWarnings("unchecked") public ExtractionResult extractRestaurants(String title, String transcript, String customPrompt) { + // #292 — transcript null/blank 가드 (NPE 방지) + if (transcript == null || transcript.isBlank()) { + return new ExtractionResult(List.of(), ""); + } // Truncate very long transcripts if (transcript.length() > 8000) { transcript = transcript.substring(0, 7000) + "\n...(중략)...\n" + transcript.substring(transcript.length() - 1000); diff --git a/backend-java/src/main/java/com/tasteby/service/GeocodingService.java b/backend-java/src/main/java/com/tasteby/service/GeocodingService.java index a591453..68db74e 100644 --- a/backend-java/src/main/java/com/tasteby/service/GeocodingService.java +++ b/backend-java/src/main/java/com/tasteby/service/GeocodingService.java @@ -156,7 +156,15 @@ public class GeocodingService { if (country.isEmpty() && !city.isEmpty()) country = "한국"; if (country.isEmpty()) return null; - return country + "|" + city + "|" + district; + // #292 — 빈 토큰은 region 문자열에 포함시키지 않는다(`한국||구` 형식 방지). + StringBuilder sb = new StringBuilder(country); + if (!city.isEmpty()) { + sb.append('|').append(city); + if (!district.isEmpty()) sb.append('|').append(district); + } else if (!district.isEmpty()) { + // city 없이 district만 있는 경우는 정확도 낮으므로 무시 + } + return sb.toString(); } private Map geocode(String query) { diff --git a/backend-java/src/main/java/com/tasteby/service/PipelineService.java b/backend-java/src/main/java/com/tasteby/service/PipelineService.java index c7c4350..28f8359 100644 --- a/backend-java/src/main/java/com/tasteby/service/PipelineService.java +++ b/backend-java/src/main/java/com/tasteby/service/PipelineService.java @@ -87,6 +87,9 @@ public class PipelineService { String videoDbId = (String) video.get("id"); String title = (String) video.get("title"); + // #292 — 외부 가시성을 위해 진입 시 processing 전이 (이미 processing이면 no-op) + updateVideoStatus(videoDbId, "processing", null, null); + var result = extractorService.extractRestaurants(title, transcript, customPrompt); if (result.restaurants().isEmpty()) { updateVideoStatus(videoDbId, "done", null, result.rawResponse()); @@ -105,18 +108,26 @@ public class PipelineService { // Build upsert data var data = new HashMap(); data.put("name", name); - data.put("address", geo != null ? geo.get("formatted_address") : restData.get("address")); data.put("region", restData.get("region")); - data.put("latitude", geo != null ? geo.get("latitude") : null); - data.put("longitude", geo != null ? geo.get("longitude") : null); data.put("cuisine_type", restData.get("cuisine_type")); data.put("price_range", restData.get("price_range")); - data.put("google_place_id", geo != null ? geo.get("google_place_id") : null); - data.put("phone", geo != null ? geo.get("phone") : null); - data.put("website", geo != null ? geo.get("website") : null); - data.put("business_status", geo != null ? geo.get("business_status") : null); - data.put("rating", geo != null ? geo.get("rating") : null); - data.put("rating_count", geo != null ? geo.get("rating_count") : null); + // #292 — geocode 실패(geo==null) 시 좌표/주소/place_id 등 기존 값 보존하기 위해 + // null을 명시적으로 put하지 않는다. upsert 측에서 누락 컬럼은 그대로 유지. + if (geo != null) { + data.put("address", geo.get("formatted_address")); + data.put("latitude", geo.get("latitude")); + data.put("longitude", geo.get("longitude")); + data.put("google_place_id", geo.get("google_place_id")); + data.put("phone", geo.get("phone")); + data.put("website", geo.get("website")); + data.put("business_status", geo.get("business_status")); + data.put("rating", geo.get("rating")); + data.put("rating_count", geo.get("rating_count")); + } else { + // geocode 실패한 첫 등록 케이스에서 최소한의 주소(LLM이 추출한 원시값)는 저장 + Object rawAddr = restData.get("address"); + if (rawAddr != null) data.put("address", rawAddr); + } String restId = restaurantService.upsert(data); diff --git a/backend-java/src/main/java/com/tasteby/service/VideoService.java b/backend-java/src/main/java/com/tasteby/service/VideoService.java index 81547d2..b7016be 100644 --- a/backend-java/src/main/java/com/tasteby/service/VideoService.java +++ b/backend-java/src/main/java/com/tasteby/service/VideoService.java @@ -28,6 +28,9 @@ public class VideoService { VideoDetail detail = mapper.findDetail(id); if (detail == null) return null; List restaurants = mapper.findVideoRestaurants(id); + if (restaurants != null) { + restaurants.forEach(r -> r.setEvaluation(JsonUtil.normalizeEvaluation(r.getEvaluation()))); + } detail.setRestaurants(restaurants != null ? restaurants : List.of()); return detail; } @@ -59,6 +62,7 @@ public class VideoService { mapper.cleanupOrphanRestaurant(restaurantId); } + @Transactional public int saveVideosBatch(String channelId, List> videos) { Set existing = new HashSet<>(mapper.getExistingVideoIds(channelId)); int saved = 0;