Files
tasteby/backend-java/src/main/resources/mybatis/mapper/VideoMapper.xml
joungmin 0f985d52a9 벌크 자막/추출 개선, 검색 필터 무시, geocoding 필드 수정, 네이버맵 링크
- 벌크 자막: 브라우저 우선 + API fallback, 광고 즉시 skip, 대기 시간 단축
- 벌크 자막/추출: 선택한 영상만 처리 가능 (체크박스 선택 후 실행)
- 자막 실패 시 no_transcript 상태 마킹하여 재시도 방지
- 검색 시 필터 조건 무시 (채널/장르/가격/지역/영역 초기화)
- 리셋 버튼 클릭 시 검색어 입력란 초기화
- RestaurantMapper updateFields에 google_place_id, rating 등 geocoding 필드 추가
- SearchMapper에 tabling_url, catchtable_url, phone, website 필드 추가
- 식당 상세에 네이버 지도 링크 추가
- YouTubeService.getTranscriptApi public 전환

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:00:40 +09:00

257 lines
10 KiB
XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tasteby.mapper.VideoMapper">
<!-- ===== Result Maps ===== -->
<resultMap id="videoSummaryMap" type="VideoSummary">
<id property="id" column="id"/>
<result property="videoId" column="video_id"/>
<result property="title" column="title"/>
<result property="url" column="url"/>
<result property="status" column="status"/>
<result property="publishedAt" column="published_at"/>
<result property="channelName" column="channel_name"/>
<result property="hasTranscript" column="has_transcript"/>
<result property="hasLlm" column="has_llm"/>
<result property="restaurantCount" column="restaurant_count"/>
<result property="matchedCount" column="matched_count"/>
</resultMap>
<resultMap id="videoDetailMap" type="VideoDetail">
<id property="id" column="id"/>
<result property="videoId" column="video_id"/>
<result property="title" column="title"/>
<result property="url" column="url"/>
<result property="status" column="status"/>
<result property="publishedAt" column="published_at"/>
<result property="channelName" column="channel_name"/>
<result property="transcriptText" column="transcript_text"
typeHandler="com.tasteby.config.ClobTypeHandler"/>
</resultMap>
<resultMap id="videoRestaurantLinkMap" type="VideoRestaurantLink">
<result property="restaurantId" column="id"/>
<result property="name" column="name"/>
<result property="address" column="address"/>
<result property="cuisineType" column="cuisine_type"/>
<result property="priceRange" column="price_range"/>
<result property="region" column="region"/>
<result property="foodsMentioned" column="foods_mentioned"
typeHandler="com.tasteby.config.ClobTypeHandler"/>
<result property="evaluation" column="evaluation"
typeHandler="com.tasteby.config.ClobTypeHandler"/>
<result property="guests" column="guests"
typeHandler="com.tasteby.config.ClobTypeHandler"/>
<result property="googlePlaceId" column="google_place_id"/>
<result property="latitude" column="latitude"/>
<result property="longitude" column="longitude"/>
</resultMap>
<!-- ===== Queries ===== -->
<select id="findAll" resultMap="videoSummaryMap">
SELECT v.id, v.video_id, v.title, v.url, v.status,
v.published_at, c.channel_name,
CASE WHEN v.transcript_text IS NOT NULL AND dbms_lob.getlength(v.transcript_text) &gt; 0 THEN 1 ELSE 0 END AS has_transcript,
CASE WHEN v.llm_raw_response IS NOT NULL AND dbms_lob.getlength(v.llm_raw_response) &gt; 0 THEN 1 ELSE 0 END AS has_llm,
(SELECT COUNT(*) FROM video_restaurants vr WHERE vr.video_id = v.id) AS restaurant_count,
(SELECT COUNT(*) FROM video_restaurants vr JOIN restaurants r ON r.id = vr.restaurant_id
WHERE vr.video_id = v.id AND r.google_place_id IS NOT NULL) AS matched_count
FROM videos v
JOIN channels c ON c.id = v.channel_id
<if test="status != null and status != ''">
WHERE v.status = #{status}
</if>
ORDER BY v.published_at DESC NULLS LAST
</select>
<select id="findDetail" resultMap="videoDetailMap">
SELECT v.id, v.video_id, v.title, v.url, v.status,
v.published_at, v.transcript_text, c.channel_name
FROM videos v
JOIN channels c ON c.id = v.channel_id
WHERE v.id = #{id}
</select>
<select id="findVideoRestaurants" resultMap="videoRestaurantLinkMap">
SELECT r.id, r.name, r.address, r.cuisine_type, r.price_range, r.region,
vr.foods_mentioned, vr.evaluation, vr.guests,
r.google_place_id, r.latitude, r.longitude
FROM video_restaurants vr
JOIN restaurants r ON r.id = vr.restaurant_id
WHERE vr.video_id = #{videoId}
</select>
<!-- ===== Updates ===== -->
<update id="updateStatus">
UPDATE videos SET status = #{status} WHERE id = #{id}
</update>
<update id="updateTitle">
UPDATE videos SET title = #{title} WHERE id = #{id}
</update>
<update id="updateTranscript">
UPDATE videos SET transcript_text = #{transcript} WHERE id = #{id}
</update>
<update id="updateVideoFields">
UPDATE videos SET
status = #{status},
processed_at = SYSTIMESTAMP
<if test="transcript != null">
, transcript_text = #{transcript}
</if>
<if test="llmResponse != null">
, llm_raw_response = #{llmResponse}
</if>
WHERE id = #{id}
</update>
<!-- ===== Cascade deletes for video deletion ===== -->
<delete id="deleteVectorsByVideoOnly">
DELETE FROM restaurant_vectors WHERE restaurant_id IN (
SELECT vr.restaurant_id FROM video_restaurants vr
WHERE vr.video_id = #{videoId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
)
</delete>
<delete id="deleteReviewsByVideoOnly">
DELETE FROM user_reviews WHERE restaurant_id IN (
SELECT vr.restaurant_id FROM video_restaurants vr
WHERE vr.video_id = #{videoId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
)
</delete>
<delete id="deleteFavoritesByVideoOnly">
DELETE FROM user_favorites WHERE restaurant_id IN (
SELECT vr.restaurant_id FROM video_restaurants vr
WHERE vr.video_id = #{videoId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
)
</delete>
<delete id="deleteRestaurantsByVideoOnly">
DELETE FROM restaurants WHERE id IN (
SELECT vr.restaurant_id FROM video_restaurants vr
WHERE vr.video_id = #{videoId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants vr2
WHERE vr2.restaurant_id = vr.restaurant_id AND vr2.video_id != #{videoId})
)
</delete>
<delete id="deleteVideoRestaurants">
DELETE FROM video_restaurants WHERE video_id = #{videoId}
</delete>
<delete id="deleteVideo">
DELETE FROM videos WHERE id = #{videoId}
</delete>
<!-- ===== Single video-restaurant unlink + orphan cleanup ===== -->
<delete id="deleteOneVideoRestaurant">
DELETE FROM video_restaurants WHERE video_id = #{videoId} AND restaurant_id = #{restaurantId}
</delete>
<delete id="cleanupOrphanVectors">
DELETE FROM restaurant_vectors WHERE restaurant_id = #{restaurantId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
</delete>
<delete id="cleanupOrphanReviews">
DELETE FROM user_reviews WHERE restaurant_id = #{restaurantId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
</delete>
<delete id="cleanupOrphanFavorites">
DELETE FROM user_favorites WHERE restaurant_id = #{restaurantId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
</delete>
<delete id="cleanupOrphanRestaurant">
DELETE FROM restaurants WHERE id = #{restaurantId}
AND NOT EXISTS (SELECT 1 FROM video_restaurants WHERE restaurant_id = #{restaurantId})
</delete>
<!-- ===== Insert / Lookup ===== -->
<insert id="insertVideo">
INSERT INTO videos (id, channel_id, video_id, title, url, published_at)
VALUES (#{id}, #{channelId}, #{videoId}, #{title}, #{url},
TO_TIMESTAMP(#{publishedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"'))
</insert>
<select id="getExistingVideoIds" resultType="string">
SELECT video_id FROM videos WHERE channel_id = #{channelId}
</select>
<select id="getLatestVideoDate" resultType="string">
SELECT TO_CHAR(MAX(published_at), 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS latest_date
FROM videos WHERE channel_id = #{channelId}
</select>
<!-- ===== Pipeline queries ===== -->
<select id="findPendingVideos" resultType="map">
SELECT id, video_id, title, url FROM videos
WHERE status = 'pending' ORDER BY created_at
FETCH FIRST #{limit} ROWS ONLY
</select>
<select id="findVideosForBulkExtract" resultType="map">
SELECT v.id, v.video_id, v.title, v.url, v.transcript_text
FROM videos v
WHERE v.transcript_text IS NOT NULL
AND dbms_lob.getlength(v.transcript_text) &gt; 0
AND (v.llm_raw_response IS NULL OR dbms_lob.getlength(v.llm_raw_response) = 0)
AND v.status != 'skip'
ORDER BY v.published_at DESC
</select>
<select id="findVideosWithoutTranscript" resultType="map">
SELECT id, video_id, title, url
FROM videos
WHERE (transcript_text IS NULL OR dbms_lob.getlength(transcript_text) = 0)
AND status NOT IN ('skip', 'no_transcript')
ORDER BY created_at
</select>
<select id="findVideosByIds" resultType="map">
SELECT id, video_id, title, url
FROM videos
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
ORDER BY created_at
</select>
<select id="findVideosForExtractByIds" resultType="map">
SELECT v.id, v.video_id, v.title, v.url, v.transcript_text
FROM videos v
WHERE v.id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
ORDER BY v.published_at DESC
</select>
<update id="updateVideoRestaurantFields">
UPDATE video_restaurants
SET foods_mentioned = #{foodsJson,jdbcType=CLOB},
evaluation = #{evaluation,jdbcType=CLOB},
guests = #{guestsJson,jdbcType=CLOB}
WHERE video_id = #{videoId} AND restaurant_id = #{restaurantId}
</update>
</mapper>