feat(backend): #356 영상-식당 관련도 LLM 평가

- DB V20260615b: video_restaurants.{relevance, relevance_reason, relevance_evaluated_at} + idx_vr_relevance
- VideoRelevanceService (#322 패턴): @Async verifyAsync + verify + verifyAll(batchSize)
- PipelineService.processExtract → linkVideoRestaurant 후 verifyAsync(linkId) 자동 트리거
- GET /api/restaurants/{id}/videos: 기본 strong/unknown만, ?include_weak=true 시 모두 + relevance/reason
- AdminVideoRelevanceController: GET pending / POST all / POST {id}/evaluate / PATCH {id}
- 캐시 키 strong|all 분리, LLM 실패 시 unknown 안전 기본값(표시 유지)

Refs: #356 (close)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-06-15 19:38:07 +09:00
parent 88bbf3ca25
commit 47020fd649
9 changed files with 322 additions and 9 deletions

View File

@@ -69,14 +69,20 @@
</select>
<select id="findVideoLinks" resultType="map">
SELECT v.video_id, v.title, v.url,
<!-- #356 — relevance 컬럼 SELECT + includeWeak 가드 -->
SELECT vr.id AS link_id,
v.video_id, v.title, v.url,
TO_CHAR(v.published_at, 'YYYY-MM-DD"T"HH24:MI:SS') AS published_at,
vr.foods_mentioned, vr.evaluation, vr.guests,
vr.relevance, vr.relevance_reason,
c.channel_name, c.channel_id
FROM video_restaurants vr
JOIN videos v ON v.id = vr.video_id
JOIN channels c ON c.id = v.channel_id
WHERE vr.restaurant_id = #{restaurantId}
<if test="includeWeak == null or !includeWeak">
AND vr.relevance IN ('strong', 'unknown')
</if>
ORDER BY v.published_at DESC
</select>
@@ -315,4 +321,36 @@
SELECT COUNT(*) FROM restaurants WHERE verified_at IS NULL
</select>
<!-- ===== #356 영상-식당 관련도 ===== -->
<update id="updateLinkRelevance">
UPDATE video_restaurants
SET relevance = #{relevance},
relevance_reason = #{reason,jdbcType=VARCHAR},
relevance_evaluated_at = CURRENT_TIMESTAMP
WHERE id = #{linkId}
</update>
<select id="findLinkContext" resultType="map">
<!-- LLM 평가에 필요한 정보 -->
SELECT vr.id AS link_id, vr.foods_mentioned, vr.evaluation,
r.id AS restaurant_id, r.name AS restaurant_name, r.address, r.cuisine_type,
v.title AS video_title, c.channel_name
FROM video_restaurants vr
JOIN restaurants r ON r.id = vr.restaurant_id
JOIN videos v ON v.id = vr.video_id
JOIN channels c ON c.id = v.channel_id
WHERE vr.id = #{linkId}
</select>
<select id="findUnevaluatedLinks" resultType="map">
SELECT id AS link_id FROM video_restaurants
WHERE relevance_evaluated_at IS NULL
FETCH FIRST #{limit} ROWS ONLY
</select>
<select id="countUnevaluatedLinks" resultType="int">
SELECT COUNT(*) FROM video_restaurants WHERE relevance_evaluated_at IS NULL
</select>
</mapper>