Commit Graph

26 Commits

Author SHA1 Message Date
joungmin
a0e8878d9a feat: P5-2 작은 후속 (#338+#320+#340+#333)
#338: /api/version 신규
- HealthController에 @Value 빌드 정보 + GET /api/version 추가
- SecurityConfig.permitAll에 /api/version 추가
- application.yml app.build.version/commit (env APP_VERSION/APP_COMMIT)
- 부수: SecurityConfig에서 /api/daemon/config permitAll 제거 (이미 admin-only)

#320: findRegionFromCoords 거리 보정
- 유클리드 거리 → cos(lat) 가중치(equirectangular approx)로 위경도 실거리 보정
- 위도가 큰 지역(부산↔서울)에서 city 추정 정확도 향상

#340: MapView 마커/범례 ARIA
- 클러스터 마커: role=button + aria-label
- 개별 식당 마커: role=button + aria-label (name + 폐업 여부)
- 채널 범례: role=region + aria-label, 색상 점은 aria-hidden

#333: ChannelController 캐시 세분화
- cache.flush() 전체 무효화 → cache.del(makeKey("channels"))로 채널 키만 evict
- 다른 모듈(restaurants/search) 캐시 hit율 보존

후속: deploy.sh에 APP_VERSION/APP_COMMIT env 주입은 별도 (현재 dev/unknown 응답)

Refs: #338 #320 #340 #333
2026-06-15 14:48:32 +09:00
joungmin
437e709a8d feat: P5-1 작은 후속 묶음 (#319+#325+#344)
#325 (#291 후속):
- VideoSseController.bulkExtract: Math.random() → ThreadLocalRandom 통일
  (bulkTranscript와 일관)
- VideoSseController.rebuildVectors: 즉시 complete(total=0) 대신 명시적
  'not_implemented' SSE 이벤트로 운영자 가시성 확보 + timeout 600s → 60s
- YouTubeService.getTranscript JavaDoc: mode 인자가 youtube-transcript-api
  폴백에서만 사용된다는 점, 브라우저 추출은 mode 무관 명시

#319 (#301 후속):
- RestaurantDetail: buildSearchQuery 헬퍼 추출 (외부 지도 검색 URL 조합)
  '한국' 단독 region 더미 케이스 가드 포함
- BottomSheet SNAP_POINTS/VELOCITY_THRESHOLD 정책 fn-doc 신규
  (docs/design/279-frontend-restaurant-detail/fn-bottomsheet-snap.md)

#344 (#283 후속):
- globals.css에 --z-bottom-sheet=50, --z-filter-sheet=60, --z-modal=70 토큰
- LoginMenu: zIndex 99999 매직 넘버 → var(--z-modal)

Refs: #319 #325 #344
2026-06-15 14:40:45 +09:00
joungmin
ed076411ed fix: P4-3 인증 메시지 + 지도 cleanup/터치/접근성 (#266+#278)
#266 (인증):
- AuthService.loginGoogle: catch-all에서 e.getMessage() 노출 → "Invalid Google token"
  고정 메시지 + 상세는 log.warn (Google verifier 내부 오류 정보 누출 차단)

#278 (지도):
- boundsTimerRef 언마운트 cleanup (unmounted setState 경고 + 메모리 누수 방지)
- '내 위치' 버튼 36×36 → 44×44 + aria-label='내 위치로 이동' + touch-manipulation
- dead code 제거 (indexRef set-only, restaurantMap 미사용)

#277 (health) — 결함 모두 후속 분리 (deep health, version, 테스트, rate limit)

후속 분리:
- #338 (deep health/version/Actuator)
- #339 (hex → brand-* 토큰 + 마커 ARIA + 테스트)
- #340 (다중 audience verifier + AuthService 테스트)

Refs: #266 #277 #278
2026-06-15 14:25:53 +09:00
joungmin
c6428e5d5f fix(infra): P4-2 데몬/캐시/통계 결함 (#275+#276+#274)
#275 (데몬):
- DaemonConfigService.updateConfig: 정수 필드 가드 (비숫자/0/음수 → 400)
- DaemonScheduler: 외부 호출(scan/process) try-finally로 updateLastX 보장
  (예외 시에도 다음 cron까지 backoff)
- DaemonController.getConfig: AuthUtil.requireAdmin() 추가 (운영 설정 노출 차단)

#276 (캐시):
- CacheService 생성자: ping을 try-with-resources로 자원 누수 차단,
  ConnectionFactory null 가드
- makeKey: null/빈 parts 가드 (잘못된 키 생성 방지)

#274 (통계):
- SiteVisitStats: int → long (21억 누적 시 오버플로 방지)
- StatsMapper: getTodayVisits/getTotalVisits long 반환
- StatsService.recordVisit: 자정 경계 동시성 DataIntegrityViolationException
  1회 재시도, 2회 실패 시 1건 손실 수용 (운영 영향 미미)

후속 분리:
- #336 (#275 분산 락 + DTO + 테스트)
- #337 (#276 SCAN + 자동복구 + 메트릭)
- #338 (#274 봇/레이트리밋 + Redis INCR + 테스트)

Refs: #275 #276 #274
2026-06-15 14:20:14 +09:00
joungmin
4b02293046 fix(crud): P4-1 백엔드 CRUD 결함 일괄 수정 (#290+#294+#295)
#294 (리뷰/메모):
- MemoService.upsert: 동시성 INSERT 시 DuplicateKeyException 폴백 → UPDATE
- ReviewService.toggleFavorite: 동시성 INSERT 시 DuplicateKeyException ignored (토글 ON)
- ReviewController: rating(0~5) Bean validation 헬퍼, body.rating null/비숫자 → 400
- ReviewMapper.xml getAvgRating: NVL로 0건 시에도 0.0 보장

#295 (채널):
- ChannelController.create: typed DataIntegrityViolationException으로 유니크 충돌 감지 (제약명 문자열 매칭 폐기)
- ChannelController.create: channel_id/channel_name null/빈값 → 400
- ChannelService.deactivate: "UC..." 형식 검증으로 명시적 분기 (이전 폴백 방식의 의도 모호함 해결)
- ChannelMapper.xml findByChannelId: description/tags/sort_order까지 SELECT

#290 (식당 CRUD):
- RestaurantController: @PreDestroy로 virtual thread executor shutdown
- RestaurantController: 캐시 역직렬화 실패를 silent ignore → log.warn + cache.del 자동 evict
- RestaurantController: setTablingUrl/setCatchtableUrl URL 스킴 화이트리스트 검증
- CacheService: 단일 키 del() 메서드 추가

후속 분리:
- #333 (#290 DTO 화이트리스트 + DDG 대체)
- #334 (#295 cache.flush 세분화 + scan 비동기)
- #335 (#294 테스트)

Refs: #290 #294 #295
2026-06-15 14:14:41 +09:00
joungmin
9c2dc9f43a fix(search): #293 검색/벡터 결함 7건 일괄 수정
- SearchController: q 빈값 가드 (HTTP 400) — '%%' LIKE 응답 폭발 차단
- SearchService:
  - keywordSearch: LIKE 와일드카드 escape (%, _, \\)
  - hybrid 모드: semantic 결과에도 attachChannels 호출 (이전: keyword만)
  - ObjectMapper/TypeReference static 재사용 (캐시 hit 경로 GC 압박 완화)
  - 알 수 없는 mode → warn 로그 + keyword fallback (이전: silent)
  - maxDistance를 @Value("${app.search.max-distance:0.57}")로 외부화
- SearchMapper.xml: LIKE 절에 ESCAPE '\\' 추가
- VectorService.searchSimilar: embeddings/first list null/empty 가드 (NPE 방지)
- application.yml: app.search.max-distance (env SEARCH_MAX_DISTANCE) 추가

후속 분리: batch insert + 테스트 (별도 후속 이슈)

Refs: #293
2026-06-15 14:01:59 +09:00
joungmin
4407f2d67d fix(pipeline): #291+#292 운영 영향 큰 결함 6건 일괄 수정
#292:
- ExtractorService.extractRestaurants: transcript null/blank 가드 (NPE 방지)
- PipelineService.processExtract: 진입 시 status='processing' 명시 전이
- PipelineService.processExtract: geocode 실패(geo==null) 시 좌표/place_id/주소
  관련 컬럼을 data에 put하지 않아 upsert COALESCE 보존 의도 명확화
- GeocodingService.parseRegionFromAddress: 빈 토큰을 region 문자열에 포함하지
  않도록 정규화 (예: '한국||구' 같은 깨진 토큰 방지)

#291:
- VideoService.saveVideosBatch: @Transactional 추가 → batch insert 원자성
- .gitignore: backend-java/cookies.txt 및 **/cookies.txt 명시 (보안 노출 차단)

후속 분리: #325 (#291 잔여 MINOR), #326 (#292 parseJson 최적화 + MINOR)

Refs: #291 #292
2026-06-15 13:21:25 +09:00
joungmin
d2e78b0363 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
2026-06-15 13:04:23 +09:00
joungmin
d3cd1b5d5f feat(daemon): instance-level enable flag (dev/prod 중복 폴링 방지)
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
2026-06-15 12:50:41 +09:00
joungmin
51dcacc728 fix(scan): #291 YouTubeService.fetchChannelVideos publishedAfter 조기 종료 버그
업로드 재생목록(uploads playlist) 스캔에서 publishedAfter 이전 영상을 만나
break해도, do-while 조건이 응답의 nextPageToken을 보고 paging을 지속하던 결함.

수정:
- stopPaging boolean 플래그 추가
- inner-loop 조기 break에서 stopPaging = true
- outer paging 갱신 시 stopPaging 검사 우선

영향:
- 백필 효율 향상 (불필요한 API quota 소모 방지)
- 봇 감지 회피 (과한 페이징 요청 안 함)
- daemon 자동 모드의 안정적 동작 기반

Refs: #291 #321
2026-06-15 12:41:35 +09:00
joungmin
4638f605aa fix(security): [Developer] #267 AdminUserController GET 4종에 requireAdmin() 추가
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
2026-06-15 11:17:48 +09:00
joungmin
4b1f7c13b7 Playwright 제거 → DuckDuckGo HTML 검색 전환 + UI 미세 조정
- 테이블링/캐치테이블 검색: Google+Playwright → DuckDuckGo HTML 파싱 (브라우저 불필요)
- 검색 딜레이 5~15초 → 2~5초로 단축
- 프론트엔드: 정보 텍스트 계층 개선 (폰트 크기/색상 조정)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 19:28:49 +09:00
joungmin
3134994817 비공개 메모 기능 추가 + 아이콘 개선
- 식당별 1:1 비공개 메모 CRUD (user_memos 테이블)
- 내 기록에 리뷰/메모 탭 분리
- 백오피스 유저 관리에 메모 수/상세 표시
- 리뷰/메모 작성 시 현재 날짜 기본값
- 지도우선/목록우선 버튼 Material Symbols 아이콘 적용

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:10:06 +09:00
joungmin
824c171158 Material Symbols 아이콘 전환 + 로고 이미지 적용 + 테이블링 이름 유사도 체크
- 전체 인라인 SVG를 Google Material Symbols Rounded로 교체
- Icon 컴포넌트 추가, cuisine-icons 매핑 리팩토링
- Tasteby 핀 로고 이미지 적용 (라이트/다크 버전)
- 테이블링/캐치테이블 이름 유사도 체크 및 리셋 API 추가
- 어드민 페이지 리셋 버튼 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:55:04 +09:00
joungmin
e85e135c8b 검색/필터 UI 개선, 채널 정렬, 드래그 스크롤, 지도링크 수정
- 검색바: 아이콘 내장, 모드 select 제거 (hybrid 고정), 엔터 검색
- 필터 그룹화: [음식 장르·가격] [지역 나라·시·구] + X 해제 버튼
- 채널 필터: 드롭다운 → 유튜브 아이콘 토글 카드, 드래그 스크롤
- 채널 정렬: sort_order 컬럼 추가, 백오피스 순서 편집
- 채널+필터 동시 적용: API 호출 대신 클라이언트 필터링
- 내위치 ON 시 다른 필터 초기화, 역방향도 동일
- 전체보기 버튼: 모든 필터 일괄 해제
- 네이버맵: 한국 식당만, 식당명만 검색
- 구글맵: 식당명+주소/지역 검색
- 로그인 영역 데스크톱 Row 1 우측 배치
- scrollbar-hide CSS 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:42:25 +09:00
joungmin
2a0ee1d2cc 채널 카드 필터 UI, 캐시 초기화, 나라 필터 수정
- 채널 설명/태그 DB 컬럼 추가 및 백오피스 편집 기능
- 채널 드롭다운을 유튜브 아이콘 토글 카드로 변경 (데스크톱 최대 4개 표시, 스크롤)
- 모바일 홈탭 채널 카드 가로 스크롤
- region "나라" 값 필터 옵션에서 제외
- 관리자 캐시 초기화 버튼 및 API 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:10:21 +09:00
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
joungmin
cdee37e341 UI/UX 개선: 모바일 네비게이션, 로그인 모달, 지도 기능, 캐치테이블 연동
- 모바일 하단 네비게이션(홈/식당목록/내주변/찜/내정보) 추가
- 로그인 버튼을 모달 방식으로 변경 (소셜 로그인 확장 가능)
- 내위치 기반 정렬 및 영역 필터, 지도 내위치 버튼 추가
- 채널 필터 시 해당 채널만 마커/범례 표시
- 캐치테이블 검색/연동 (단건/벌크), NONE 저장 패턴
- 벌크 트랜스크립트 SSE (Playwright 브라우저 재사용)
- 테이블링/캐치테이블 버튼 UI 차별화
- Google Maps 링크 모바일 호환, 초기화 버튼, 검색 라벨 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:49:16 +09:00
joungmin
0ad09e5b67 Add dockerignore, fix Redis image, add troubleshooting docs
- 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>
2026-03-10 00:23:42 +09:00
joungmin
ff4e8d742d Add cuisine subcategory filter, fix remap logic, and add OKE deployment manifests
- Add 파인다이닝/코스 cuisine type to 한식/일식/중식/양식 categories
- Change cuisine filter from flat list to grouped optgroup with subcategories
- Fix remap-foods/remap-cuisine: add jdbcType=CLOB, fix CLOB LISTAGG,
  improve retry logic (3 attempts, batch size 5), add error logging
- Add OKE deployment: Dockerfiles, K8s manifests, deploy.sh, deployment guide
- Add Next.js standalone output for Docker builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:58:09 +09:00
joungmin
69e1882c2b Add video management endpoints and fix LLM extraction pipeline
- Add missing endpoints: fetch-transcript, extract, bulk-extract/pending,
  bulk-transcript/pending, manual restaurant add, restaurant update
- Add OCI HTTP client dependency (jersey3) for GenAI SDK compatibility
- Fix Oracle null parameter ORA-17004 with jdbcType=CLOB in MyBatis
- Fix evaluation IS JSON constraint by storing as valid JSON
- Add @JsonProperty("transcript") for frontend compatibility
- Add Korean-only rule to LLM extraction prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:08:40 +09:00
joungmin
16bd83c570 Fix login 401, admin permission, video links serialization, and admin UI styling
- 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>
2026-03-09 21:41:57 +09:00
joungmin
c16add08c3 Migrate to MyBatis with proper Controller→Service→Mapper layering
- Add MyBatis Spring Boot Starter with XML mappers and domain classes
- Create 9 mapper interfaces + XML: Restaurant, Video, Channel, Review,
  User, Stats, DaemonConfig, Search, Vector
- Create 10 domain classes with Lombok: Restaurant, VideoSummary,
  VideoDetail, VideoRestaurantLink, Channel, Review, UserInfo,
  DaemonConfig, SiteVisitStats, VectorSearchResult
- Create 7 new service classes: RestaurantService, VideoService,
  ChannelService, ReviewService, UserService, StatsService,
  DaemonConfigService
- Refactor all controllers to be thin (HTTP + auth only), delegating
  business logic to services
- Refactor SearchService, PipelineService, DaemonScheduler, AuthService,
  YouTubeService to use mappers/services instead of JDBC/repositories
- Add Jackson SNAKE_CASE property naming for consistent API responses
- Add ClobTypeHandler for Oracle CLOB→String in MyBatis
- Add IdGenerator utility for centralized UUID generation
- Delete old repository/ package (6 files), JdbcConfig, LowerCaseKeyAdvice
- VectorService retains JDBC for Oracle VECTOR type support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:13:44 +09:00
joungmin
91d0ad4598 Fix Oracle uppercase column keys: return lowercase in all API responses
- 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>
2026-03-09 20:38:43 +09:00
joungmin
a844fd44cc Fix CORS: allow tasteby.net origin and integrate with Spring Security
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:30:54 +09:00
joungmin
6d05be2331 Migrate backend from Python to Java Spring Boot
- 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>
2026-03-09 20:26:32 +09:00