- evaluation 컬럼이 IS JSON 제약이라 평문은 {"text":"..."}로 정규화
- parseMap이 잘못된 JSON 받았을 때 빈 Map 대신 {"text":원문}으로 보존
- PipelineService/RestaurantService에서 이미 호출 중인 유틸 — 미커밋 상태였음
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- StatsMapper interface는 long 반환인데 XML resultType이 int
- Integer를 primitive long으로 cast 못 함 → ClassCastException → 500
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 실제 캐치테이블은 app.catchtable.co.kr/ct/shop/... 형식
- 옛 /shop/, /dining/ 패턴은 contains 매칭 실패 → 첫 회차 1044건 전부 미발견
- 패턴 교정 후 NONE 해제 + 재실행 필요
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 특정 검색에서 무한 hang → backend virtual thread 점유로 후속 벌크 작업 중단
- Naver/DDG 둘 다 timeout 적용
- 타임아웃 시 HttpTimeoutException → 호출자(bulk)에서 notfound 안전 처리
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 대량 백필(700+건 ≈ 100분) 시 10분 SSE timeout으로 중간 끊김
- emit() 실패 시 마지막 cache.flush + complete 누락 → 3시간으로 확장
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Naver/DDG 결과가 www.tabling.co.kr 형태인데 PUT validation에서 거부됨
- bulk-tabling SSE는 validation 없이 통과 — 불일치 해소
- catchtable은 이미 app/www 둘 다 허용 (기존)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- GET /api/admin/restaurants/duplicates/place-id (어드민 전용)
- 그룹별 식당 목록 + video/review/memo 카운트 동봉
- Mapper: findDuplicatePlaceIdRows + Service 그룹핑
- 정리/병합 + UNIQUE 제약은 데이터 위험 분리 위해 후속 PR로
Refs: #359 (조회 단계 완료)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- dto/RestaurantUpdateDTO record 신규 (15 필드, 모두 nullable)
- @Size/@Pattern(URL or NONE)/@DecimalMin·Max/@Min·Max
- RestaurantController.update 시그니처 Map → @Valid DTO 교체
- toFieldMap()으로 null 제외 후 기존 Service.update 호출 (회귀 0)
- #332 ALLOWED_UPDATE_FIELDS Set 제거 (DTO 필드 자체가 화이트리스트)
Refs: #358 (close)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- WebSearchService 신규 (Naver webkr.json 우선, 키 미설정/실패 시 DDG)
- RestaurantController.searchTabling/searchCatchtable 내부 호출 교체
- 인라인 DDG 80줄 제거, 미사용 import 정리
- app.naver.client-id/secret 추가 (env: NAVER_CLIENT_ID/SECRET)
- k8s secrets template에 NAVER 키 항목
Refs: #357 (close)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- N+1 단건 jdbc.update → 단일 jdbc.batchUpdate(SqlParameterSource[])
- UUID 인라인 변환 → IdGenerator.newId() 공통 유틸
- 현재 N=1로 영향 작으나, chunk 분할 도입 시를 위한 사전 정비
- 회귀 없음: 동일 INSERT SQL, 동일 파라미터 매핑
설계서: docs/design/331-vector-batch-insert/README.md
Refs: #331 (Developer 단계)
OciGenAiService.parseJson:
- 잘린 배열 복구 시 각 idx에서 end를 1씩 늘려 readValue 시도하던 O(N²) 로직 제거
- findObjectEnd: brace depth counter (문자열/escape 처리) 단일 패스 O(N)
- 8192 token 응답 처리 시간 수백 ms → 10ms 이하 예상
- 매 try마다 Jackson 예외 객체/스택트레이스 생성하던 부담 제거
설계서: docs/design/326-parsejson-optimization/README.md (Approved)
Refs: #326
ALLOWED_UPDATE_FIELDS set으로 PUT /api/restaurants/{id} body를 SQL updateFields
컬럼 가드와 1:1로 매핑. 허용 외 키는 silent drop + DEBUG 로그.
기존 SQL <if containsKey>로 이미 임의 컬럼 갱신이 차단되어 있으나, Controller에
명시 화이트리스트가 없어 의도 모호. 본 변경으로 두 레이어 모두 화이트리스트 확보.
sanitized가 비면 200 no-op로 응답 (사용자 경험 우선).
DDG/isNameSimilar/DTO는 별도 후속 (예: #346) 분리.
설계서: docs/design/332-restaurant-update-whitelist/README.md
Refs: #332
- BotDetector 유틸 (Pattern.CASE_INSENSITIVE: bot|crawler|spider|slurp|scrap|fetch|monitor|preview|lighthouse)
- RateLimitService: Redis SET NX EX(60s) 패턴으로 같은 IP 윈도우 차단
- Bucket4j 대신 spring-data-redis 기존 의존성 재사용 (간결)
- Redis 다운 시 fail-open (사용자 경험 우선)
- StatsController.recordVisit: HttpServletRequest 받아 UA + X-Forwarded-For 우선 IP
- 봇/리밋 초과 → 200 + counted:false (사용자 페이지 로드 지장 X)
- 통과 → 200 + counted:true → statsService.recordVisit()
- application.yml: app.rate-limit.visit-window-seconds (env VISIT_WINDOW_SECONDS) 기본 60
- dev 검증: 봇 UA → counted=false, Mozilla → true, 즉시 재호출 → false
설계서: docs/design/337-stats-bot-ratelimit/README.md
Refs: #337 (Developer 단계)
dev와 prod가 같은 Oracle ATP 인스턴스(_low vs _medium tier만 다름)를 공유하는
환경에서 dev/prod 양쪽 DaemonScheduler가 같은 daemon_config row를 폴링하면
같은 시점에 동일 채널 스캔이 발생 → YouTube 봇 감지 위험 증가.
수정:
- application.yml: app.daemon.enabled (env DAEMON_ENABLED, 기본 true)
- DaemonScheduler.run() 첫 줄에서 인스턴스 플래그 검사 후 차단
- dev/backend/.env에 DAEMON_ENABLED=false 설정 (이 커밋엔 미포함, 로컬만)
운영(OKE)은 env 미설정 → 기본 true로 정상 동작.
dev(PM2)는 .env로 false → 스케줄러 자체가 동작 안 함.
Refs: #275#321
업로드 재생목록(uploads playlist) 스캔에서 publishedAfter 이전 영상을 만나
break해도, do-while 조건이 응답의 nextPageToken을 보고 paging을 지속하던 결함.
수정:
- stopPaging boolean 플래그 추가
- inner-loop 조기 break에서 stopPaging = true
- outer paging 갱신 시 stopPaging 검사 우선
영향:
- 백필 효율 향상 (불필요한 API quota 소모 방지)
- 봇 감지 회피 (과한 페이징 요청 안 함)
- daemon 자동 모드의 안정적 동작 기반
Refs: #291#321
CRITICAL: listUsers, userFavorites, userReviews, userMemos 4개 GET이 인증만 요구하고 admin 검사가 없어, 일반 사용자 토큰으로 전체 사용자 목록 및 타인 활동 조회 가능했음. 각 메서드 첫 줄에 AuthUtil.requireAdmin() 호출 추가 → non-admin은 403.
함께 커밋(이전 미커밋 작업):
- Logger 등록 (감사 로그용)
- AuthUtil/Logger/HttpStatus/ResponseStatusException import 정리
- updateAdmin: 자기 자신 admin 변경 차단 + 감사 로그
(이미 동작 중이던 변경이나 git 미커밋 상태였음)
문서:
- 설계서 §3 인수조건에 권한 강제 항목 추가, 상태 Draft → Approved
- CHANGELOG.md 2026-06-15 핫픽스 항목 추가
검증:
- Anonymous GET /api/admin/users → 403 ✓
- Bad-token GET /api/admin/users → 403 ✓
- 백엔드 빌드 성공, tasteby-api 재시작 완료
Refs: #267
- 테이블링/캐치테이블 검색: Google+Playwright → DuckDuckGo HTML 파싱 (브라우저 불필요)
- 검색 딜레이 5~15초 → 2~5초로 단축
- 프론트엔드: 정보 텍스트 계층 개선 (폰트 크기/색상 조정)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 식당별 1:1 비공개 메모 CRUD (user_memos 테이블)
- 내 기록에 리뷰/메모 탭 분리
- 백오피스 유저 관리에 메모 수/상세 표시
- 리뷰/메모 작성 시 현재 날짜 기본값
- 지도우선/목록우선 버튼 Material Symbols 아이콘 적용
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 전체 인라인 SVG를 Google Material Symbols Rounded로 교체
- Icon 컴포넌트 추가, cuisine-icons 매핑 리팩토링
- Tasteby 핀 로고 이미지 적용 (라이트/다크 버전)
- 테이블링/캐치테이블 이름 유사도 체크 및 리셋 API 추가
- 어드민 페이지 리셋 버튼 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 검색바: 아이콘 내장, 모드 select 제거 (hybrid 고정), 엔터 검색
- 필터 그룹화: [음식 장르·가격] [지역 나라·시·구] + X 해제 버튼
- 채널 필터: 드롭다운 → 유튜브 아이콘 토글 카드, 드래그 스크롤
- 채널 정렬: sort_order 컬럼 추가, 백오피스 순서 편집
- 채널+필터 동시 적용: API 호출 대신 클라이언트 필터링
- 내위치 ON 시 다른 필터 초기화, 역방향도 동일
- 전체보기 버튼: 모든 필터 일괄 해제
- 네이버맵: 한국 식당만, 식당명만 검색
- 구글맵: 식당명+주소/지역 검색
- 로그인 영역 데스크톱 Row 1 우측 배치
- scrollbar-hide CSS 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 채널 설명/태그 DB 컬럼 추가 및 백오피스 편집 기능
- 채널 드롭다운을 유튜브 아이콘 토글 카드로 변경 (데스크톱 최대 4개 표시, 스크롤)
- 모바일 홈탭 채널 카드 가로 스크롤
- region "나라" 값 필터 옵션에서 제외
- 관리자 캐시 초기화 버튼 및 API 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 벌크 자막: 브라우저 우선 + 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>
- 모바일 하단 네비게이션(홈/식당목록/내주변/찜/내정보) 추가
- 로그인 버튼을 모달 방식으로 변경 (소셜 로그인 확장 가능)
- 내위치 기반 정렬 및 영역 필터, 지도 내위치 버튼 추가
- 채널 필터 시 해당 채널만 마커/범례 표시
- 캐치테이블 검색/연동 (단건/벌크), NONE 저장 패턴
- 벌크 트랜스크립트 SSE (Playwright 브라우저 재사용)
- 테이블링/캐치테이블 버튼 UI 차별화
- Google Maps 링크 모바일 호환, 초기화 버튼, 검색 라벨 개선
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add .dockerignore for backend-java and frontend (276MB → 336KB)
- Fix Redis image to use full registry path (CRI-O compatibility)
- Update ingress TLS to www only (root domain DNS pending)
- Add comprehensive troubleshooting documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix UserInfo boolean field naming (isAdmin → admin) for proper Jackson/MyBatis mapping
- Configure Google OAuth audience with actual client ID to fix token verification
- Parse CLOB fields and convert Oracle TIMESTAMP in restaurant video links API
- Add explicit bg-white/text-gray-900 to admin page inputs, selects, and table headers
- Add keyPrefix to RestaurantList to avoid duplicate React keys across desktop/mobile
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add LowerCaseKeyAdvice (ResponseBodyAdvice) to auto-convert Map keys
- Add LowerCaseJdbcTemplate with overridden getColumnMapRowMapper
- Update all repository/service code to use lowercase key access
- Add lowerKeys utility to JsonUtil
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Full Java 21 + Spring Boot 3.3 backend with Virtual Threads
- HikariCP connection pool for Oracle ADB
- JWT auth, Redis caching, OCI GenAI integration
- YouTube transcript extraction via API + Playwright browser fallback
- SSE streaming for bulk operations
- Scheduled daemon for channel scanning/video processing
- Mobile UI: collapse restaurant list to single row on selection
- Switch PM2 ecosystem config to Java backend
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>