video_restaurants.relevance(strong/weak/incidental/unknown) 컬럼 + VideoRelevanceService. findVideoLinks에 includeWeak 파라미터. 어드민 4개 API. #322 식당 LLM 검증과 동일 패턴. 설계서: docs/design/356-video-relevance-llm/README.md (Approved) Refs: #356 (Architect)
설계서: 영상-식당 관련도 LLM 평가로 약한 매칭 자동 숨김 (#356)
상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #356 · 유사 패턴: #322(식당 LLM 검증, 09-Done) · 부모 영역: #270(영상→식당 추출 파이프라인 현행화, 09-Done) · 구현 파일:
backend-java/src/main/java/com/tasteby/service/VideoRelevanceService.java(신규),backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml,backend-java/src/main/java/com/tasteby/service/RestaurantService.java,backend-java/src/main/java/com/tasteby/controller/AdminVideoRelevanceController.java(신규), DB 마이그레이션 SQL · 테스트: 본 범위 밖 (테스트 인프라 #343 도입됨, 후속에서 점진 확장)
1. 목적 (Why)
식당 상세에 연결된 영상 중 식당과 본격적으로 관련 없는 약한 언급(비교 대상, 일반 토픽 중 잠깐 언급, 식당 입점 전 영상 등)이 노이즈로 표시. 실제 케이스 — 파이브가이즈 강남의 영상 7개 중 3건이 약한 매칭(쉐이크쉑 비교 / 미국 비만율 일반 토픽 / 한국 입점 전 미국 여행). LLM 평가로 약한 매칭 자동 숨김.
2. 범위 (Scope)
- 포함
video_restaurants테이블에relevance,relevance_reason,relevance_evaluated_at컬럼 추가.VideoRelevanceService신규 — LLM 판정 + DB 반영 (#322패턴 모방).PipelineService.processExtract완료 후verifyAsync(linkId)호출 — 신규 등록 자동 평가.GET /api/restaurants/{id}/videos: 기본relevance = 'strong'만 응답.?include_weak=true시 모두 포함.- 어드민 API: 단건 재평가 / 일괄 백필 / 수동 토글.
- 제외 (별도 후속)
- 어드민 UI(검증 칼럼 / 토글) —
#322의 RestaurantsPanel UI와 같은 패턴으로 별도 후속. - 프론트 사용자 옵션 UI("약한 매칭도 보기" 토글) — 별도 후속.
- LLM 비용 모니터링/메트릭 — 별도.
- 어드민 UI(검증 칼럼 / 토글) —
3. 인수조건
video_restaurants테이블에relevance VARCHAR2(16) DEFAULT 'unknown',relevance_reason VARCHAR2(120),relevance_evaluated_at TIMESTAMP컬럼 +idx_vr_relevance인덱스.- 가능한 값:
strong | weak | incidental | unknown(unknown = 미평가). - 신규 등록 시 60초 안에
relevance_evaluated_at설정. GET /api/restaurants/{id}/videos기본 응답:relevance IN ('strong','unknown')(안전한 기본값 = 평가 실패 시 표시).?include_weak=true: 모두 포함 +relevance,relevance_reason필드 동봉.- 어드민 API:
GET /api/admin/video-relevance/pending→ 미평가(unknown) 카운트POST /api/admin/video-relevance/all?batchSize=10→ 백필POST /api/admin/video-relevance/{linkId}/evaluate→ 단건 재평가PATCH /api/admin/video-relevance/{linkId}→ 수동 강제 토글{relevance, reason}
- LLM 호출 실패 시
unknown유지 + 로그 (#322와 같은 안전 기본값). - 빌드/배포 회귀 없음.
4. 컨텍스트 & 제약
- 기존
OciGenAiService.chat(prompt, maxTokens)재사용. - LLM 비용: 영상-식당 페어당 1회 단발. 현재 1,244건 → 백필 시 약 1,244 호출.
video_restaurants는 한 영상에 여러 식당, 한 식당에 여러 영상이 m:n 관계.- 같은 페어는
relevance_evaluated_at이 NULL 아니면 재평가 안 함 (캐시).
5. 아키텍처 개요
PipelineService.processExtract (기존)
│
▼
RestaurantService.linkVideoRestaurant (video_restaurants INSERT)
│
▼
VideoRelevanceService.verifyAsync(linkId) ← #356 신규
│ (비동기)
▼
OciGenAiService.chat(prompt, 120)
│
▼
parseRelevance → { relevance: strong|weak|incidental, reason: string }
│
▼
RestaurantMapper.updateRelevance(linkId, relevance, reason)
│
▼ (조회 시)
RestaurantMapper.findVideoLinks(restaurantId, includeWeak)
├ includeWeak=false (기본): WHERE relevance IN ('strong','unknown')
└ includeWeak=true: 모두 + relevance/reason 필드 노출
6. 데이터 모델
DB 마이그레이션
ALTER TABLE video_restaurants ADD (
relevance VARCHAR2(16) DEFAULT 'unknown' NOT NULL,
relevance_reason VARCHAR2(120),
relevance_evaluated_at TIMESTAMP
);
CREATE INDEX idx_vr_relevance ON video_restaurants(relevance);
도메인 (VideoRestaurantLink 확장은 본 범위 밖 — findVideoLinks는 resultType="map")
응답 Map에 키 추가:
relevance:"strong" | "weak" | "incidental" | "unknown"relevance_reason:string | null
LLM 응답 스키마
{
"relevance": "strong" | "weak" | "incidental",
"reason": "20자 이내"
}
7. 함수 명세
| 함수 | 책임 | 비고 |
|---|---|---|
VideoRelevanceService.verifyAsync(linkId) |
비동기 트리거 | #322의 RestaurantVerifyService.verifyAsync 유사 |
VideoRelevanceService.verify(linkId) |
단건 검증 + DB 반영 | LLM 실패 시 unknown 유지 |
VideoRelevanceService.verifyAll(batchSize) |
백필 (식당당 200ms sleep) | |
VideoRelevanceService.buildPrompt(...) |
프롬프트 생성 | 식당명·주소·음식·영상 제목·평가 |
VideoRelevanceService.parseRelevance(raw) |
LLM 응답 → DTO | 파싱 실패 시 unknown 안전 기본값 |
RestaurantMapper.updateRelevance(linkId, rel, reason) |
DB 갱신 | |
RestaurantMapper.findVideoLinks(restaurantId, includeWeak) |
기존 SQL에 WHERE 조건 추가 | |
AdminVideoRelevanceController 신규 |
4개 admin endpoint | requireAdmin |
8. 흐름
신규 등록 자동 평가
PipelineService.processExtract→linkVideoRestaurant→ linkId 획득.VideoRelevanceService.verifyAsync(linkId)호출(@Async).- 별도 스레드: 영상/식당/평가 데이터 조회 → buildPrompt → LLM → parse → DB.
백필
- 어드민
POST /api/admin/video-relevance/all호출. WHERE relevance_evaluated_at IS NULL인 link 10개씩 조회 → 순차 검증.- 식당당 200ms sleep (LLM rate 보호).
프롬프트 예시
다음 YouTube 영상이 이 식당을 어떻게 다루는지 판정하라.
식당명: {restaurantName}
주소: {address}
음식: {foodsMentioned}
영상 제목: {videoTitle}
영상 채널: {channelName}
영상에 등장한 평가 내용: {evaluation}
응답 형식(JSON만, 다른 텍스트 없이):
{"relevance": "strong"|"weak"|"incidental", "reason": "20자 이내 한국어"}
가이드:
- strong: 영상 본편이 이 식당을 본격 다룸. 방문 리뷰, 메뉴 평가 등.
- weak: 영상에서 잠깐 언급, 비교 대상으로만 등장, 다른 식당의 일부로.
- incidental: 식당 입점 전 영상에서 단순 언급, 일반 토픽(미국 비만, 환율 등)에서 잠깐.
- 판단 모호 시 strong (보수적 — 사용자에게 표시 유지).
9. 엣지케이스
- LLM 응답 비-JSON: parseRelevance → unknown 기본값.
- LLM 호출 실패: unknown 유지 → 다음 백필 재시도.
- 영상 데이터 누락(transcript 없음, evaluation 비어있음): 프롬프트에 "(미상)" 표기. LLM이 판정 어려우면 strong 보수적.
- 동시성: 같은 linkId verifyAsync 두 번 호출 → idempotent.
- 삭제된 영상: linkId 조회 결과 없으면 no-op.
10. 테스트 (수동)
- 파이브가이즈 강남 케이스 백필 → 7건 중 3건이 weak/incidental로 마킹되는지 확인.
- 공개 API
/api/restaurants/{id}/videos→ 약한 매칭 제외 확인. ?include_weak=true→ 모두 포함 확인.
11. 리스크 & 대안
- 선택:
#322동일 패턴 + DB 마이그레이션. - 대안 A: 사용자가 직접 "약한 매칭도 보기" 토글 → 사용자 결정 부담.
- 대안 B: 추출 단계에서 한 번에 판정 → 비용 ↓이지만 ExtractorService 비대.
- 트레이드오프: 단발 LLM 평가는 비용 합리적. false positive는 어드민 수동 토글 +
unknown안전 기본값으로 보완.
12. 미해결 질문
- 임계값(weak/incidental 둘 다 숨김 vs incidental만 숨김) — 현재는 둘 다 숨김.
- 영상 자막 전체를 LLM에 보낼지 vs 평가 텍스트만 → 비용/정확도 트레이드오프. 현재는 evaluation만(짧음).
- 사용자에게 "약한 매칭도 보기" UI → 별도 후속.
- 어드민 UI — 별도 후속 (#322 패턴 모방).