feat(verify): #322 LLM 검증으로 잘못된/프랜차이즈 식당 자동 숨김
DB 마이그레이션 (운영 ATP에 사전 실행 완료):
- restaurants.hidden NUMBER(1) DEFAULT 0 NOT NULL
- restaurants.hidden_reason VARCHAR2(120)
- restaurants.verified_at TIMESTAMP
- idx_restaurants_hidden 인덱스
코드:
- Restaurant 도메인에 hidden/hiddenReason/verifiedAt 필드 추가
- RestaurantMapper.xml resultMap 갱신 + findAll에 hidden=0 조건 (includeHidden=true 시 제외)
- RestaurantMapper에 updateVerification/clearHidden/findUnverified/countUnverified 추가
- RestaurantService.findAll() includeHidden 오버로드 + 검증 헬퍼 메서드
- RestaurantVerifyService 신규 (verify, verifyAsync, verifyAll, buildPrompt, parseVerifyResponse)
- LLM 응답이 JSON 아닐 때 안전 기본값(valid=true) → hidden 유지
- 백필은 식당당 200ms sleep으로 LLM rate 보호
- PipelineService.processExtract 끝에 verifyAsync(restId) 호출 (신규 등록 자동 검증)
- AdminRestaurantController 신규 — requireAdmin 필수:
- GET /api/admin/restaurants/verify/pending
- POST /api/admin/restaurants/verify/all?batchSize=10
- POST /api/admin/restaurants/{id}/verify
- PATCH /api/admin/restaurants/{id}/hidden {hidden, reason}
프롬프트:
- 식당명, 주소, 지역, cuisine, foods를 OCI GenAI로 보내 valid/is_franchise/reason 판정
- 보수적 가이드 (모호하면 valid=true)
설계서: docs/design/322-restaurant-llm-verify/README.md (Approved 대기)
Refs: #322
This commit is contained in:
@@ -22,6 +22,9 @@
|
||||
<result property="rating" column="rating"/>
|
||||
<result property="ratingCount" column="rating_count"/>
|
||||
<result property="updatedAt" column="updated_at"/>
|
||||
<result property="hidden" column="hidden" javaType="java.lang.Boolean"/>
|
||||
<result property="hiddenReason" column="hidden_reason"/>
|
||||
<result property="verifiedAt" column="verified_at"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- ===== Queries ===== -->
|
||||
@@ -29,7 +32,8 @@
|
||||
<select id="findAll" resultMap="restaurantMap">
|
||||
SELECT DISTINCT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
|
||||
r.cuisine_type, r.price_range, r.google_place_id, r.tabling_url, r.catchtable_url,
|
||||
r.business_status, r.rating, r.rating_count, r.updated_at
|
||||
r.business_status, r.rating, r.rating_count, r.updated_at,
|
||||
r.hidden, r.hidden_reason, r.verified_at
|
||||
FROM restaurants r
|
||||
<if test="channel != null and channel != ''">
|
||||
JOIN video_restaurants vr_f ON vr_f.restaurant_id = r.id
|
||||
@@ -39,6 +43,9 @@
|
||||
<where>
|
||||
r.latitude IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM video_restaurants vr0 WHERE vr0.restaurant_id = r.id)
|
||||
<if test="includeHidden == null or !includeHidden">
|
||||
AND r.hidden = 0
|
||||
</if>
|
||||
<if test="cuisine != null and cuisine != ''">
|
||||
AND r.cuisine_type = #{cuisine}
|
||||
</if>
|
||||
@@ -277,4 +284,35 @@
|
||||
ORDER BY r.name
|
||||
</select>
|
||||
|
||||
<!-- ===== #322 LLM 검증 ===== -->
|
||||
|
||||
<update id="updateVerification">
|
||||
UPDATE restaurants
|
||||
SET hidden = #{hidden},
|
||||
hidden_reason = #{hiddenReason,jdbcType=VARCHAR},
|
||||
verified_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="clearHidden">
|
||||
UPDATE restaurants
|
||||
SET hidden = 0,
|
||||
hidden_reason = NULL,
|
||||
verified_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<select id="findUnverified" resultMap="restaurantMap">
|
||||
SELECT r.id, r.name, r.address, r.region, r.cuisine_type, r.price_range,
|
||||
r.hidden, r.hidden_reason, r.verified_at
|
||||
FROM restaurants r
|
||||
WHERE r.verified_at IS NULL
|
||||
ORDER BY r.updated_at DESC
|
||||
FETCH FIRST #{limit} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<select id="countUnverified" resultType="int">
|
||||
SELECT COUNT(*) FROM restaurants WHERE verified_at IS NULL
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user