fix(pipeline): #291+#292 운영 영향 큰 결함 6건 일괄 수정
#292: - ExtractorService.extractRestaurants: transcript null/blank 가드 (NPE 방지) - PipelineService.processExtract: 진입 시 status='processing' 명시 전이 - PipelineService.processExtract: geocode 실패(geo==null) 시 좌표/place_id/주소 관련 컬럼을 data에 put하지 않아 upsert COALESCE 보존 의도 명확화 - GeocodingService.parseRegionFromAddress: 빈 토큰을 region 문자열에 포함하지 않도록 정규화 (예: '한국||구' 같은 깨진 토큰 방지) #291: - VideoService.saveVideosBatch: @Transactional 추가 → batch insert 원자성 - .gitignore: backend-java/cookies.txt 및 **/cookies.txt 명시 (보안 노출 차단) 후속 분리: #325 (#291 잔여 MINOR), #326 (#292 parseJson 최적화 + MINOR) Refs: #291 #292
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,3 +18,5 @@ k8s/secrets.yaml
|
||||
# OS / misc
|
||||
.DS_Store
|
||||
backend/cookies.txt
|
||||
backend-java/cookies.txt
|
||||
**/cookies.txt
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, Object> geocode(String query) {
|
||||
|
||||
@@ -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<String, Object>();
|
||||
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);
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ public class VideoService {
|
||||
VideoDetail detail = mapper.findDetail(id);
|
||||
if (detail == null) return null;
|
||||
List<VideoRestaurantLink> 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<Map<String, Object>> videos) {
|
||||
Set<String> existing = new HashSet<>(mapper.getExistingVideoIds(channelId));
|
||||
int saved = 0;
|
||||
|
||||
Reference in New Issue
Block a user