Commit Graph

157 Commits

Author SHA1 Message Date
2574174d22 fix: 시드 자동 발굴이 엉뚱한 채널을 잡던 문제 — 채널검색→영상검색 집계, 이중 인코딩 제거
실제 내 채널(clipOut-log) 데이터로 돌려보니 구독자 1~2명짜리 채널만
잡혀서 원인을 둘 찾았다.

1) type=channel 검색은 채널 '이름'을 매칭한다. 프로그램 클립은 "유퀴즈"라는
   이름의 채널이 아니라 tvN D ENT·뜬뜬 같은 제작사 채널에 올라오므로
   이름으로는 절대 찾을 수 없다. type=video 로 25건을 뽑아 채널별로 집계하고
   3건 이상 등장한 채널만 채택하도록 바꿨다.

2) UriComponentsBuilder.encode().toUriString() 을 RestTemplate 에 String 으로
   넘기면 URI 템플릿으로 보고 한 번 더 인코딩한다. 한글 키워드가
   %25EC.. 로 깨져 검색어가 무의미해졌다. URI 오버로드로 교체.
   ChannelService.saveChannelFromUrl 도 한글 @핸들에서 같은 문제가 생기므로
   함께 고쳤다. (YoutubeSearchService 등 기존 코드는 이미 올바르게 쓰고 있었다)

수정 후 8개 키워드 전부 정확한 공식채널을 찾는다:
유퀴즈→tvN D ENT/디글 클래식, 워크맨→워크맨-Workman, 살롱드립→TEO 테오,
짠한형 신동엽·미미미누·핫이슈지·입만열면→각 공식채널.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 09:04:16 +09:00
592ed12c2b feat: 소재 발굴 피드 — 소스 롱폼/경쟁 쇼츠 최신순 2탭
쇼츠 클립 채널의 소재를 최신순으로 발굴하는 /feed 화면과 수집 파이프라인.

- 소재 원본 탭: 웹예능 공식채널의 신규 롱폼(durationSec > 65)만 수집.
  아직 아무도 안 자른 구간을 선점하는 용도라 공식계정 쇼츠는 제외한다.
- 경쟁 쇼츠 탭: 예능짤 채널의 신규 쇼츠. 제목·썸네일 벤치마킹용이라
  재가공 대신 원본 열기 액션을 준다.
- Channel.role(MY/SOURCE/RIVAL) 컬럼 하나로 역할을 구분하고, 기존
  uploads 플레이리스트 동기화를 그대로 재사용한다. 채널당 2 units라
  search.list(100 units) 대비 쿼터가 거의 들지 않는다.
- 3시간 주기 수집(FeedCollectionService). 소재 선점은 업로드 직후가
  승부라 기존 일 1회 채널 수집으로는 늦다.
- 수집함/발굴/떡상 후보 쿼리는 source 미지정 시 CHANNEL·SEARCH만 보도록
  좁혀, 피드 영상이 기존 화면을 덮지 않게 격리했다.
- 시드는 자동 후보 → 수동 승인. 내 채널 해시태그를 역분석해(HashtagExtractor)
  공식채널 후보를 추천 목록에 쌓고, 승인 시 SOURCE/RIVAL로 등록한다.
- 연속 3회 수집 실패한 시드는 자동 스킵해 쿼터 낭비를 막는다.
- role이 null인 기존 채널은 부팅 시 MY로 1회 백필(ddl-auto:update 특성).

UX: 기존 Editorial 디자인 시스템 유지. 골든타임(24h)·공식클립/풀에피·
떡상중·작업함 배지, 프로그램/길이/기간 필터, URL 상태 보존, 스켈레톤·
빈 상태·에러 복구 액션, 탭 키보드 이동, 이모지 대신 Lucide 아이콘.

테스트: HashtagExtractor·FeedBadges·ChannelRole 순수 로직 16건 추가(총 76건 통과).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:42:26 +09:00
8e6926a14b feat: 댓글 카드 제외 단어도 쉼표 구분 + 검색보다 우선 명시
제외 단어 구분자를 검색어와 동일하게 쉼표만으로 통일했다. 공백이
구분자가 아니게 되어 구절 단위 제외가 가능하다. 제외 단어에 걸린
댓글은 검색·필터보다 먼저 걸러져 카드와 분석 모두에서 무조건 빠진다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:56:27 +09:00
b6bfa5046d feat: 댓글 카드 검색어 쉼표 OR 검색 지원
검색 입력을 쉼표로 구분하면 각 단어 중 하나라도 포함된 댓글을 표시한다.
공백은 구분자가 아니므로 여러 단어를 이어 쓴 구절 검색은 그대로 동작한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 14:24:47 +09:00
aecbdb4753 feat: 발굴 포맷 옵션(롱폼/쇼츠) 추가 — 롱폼 기본
발굴 대상이 SHORTS 하드코딩이었으나, 롱폼 콘텐츠 대응을 위해 포맷을 선택 가능하게 함.

- ChannelDiscoveryService: runDiscovery(String format) 오버로드. format(SHORTS|LONG_FORM),
  null/미인식이면 설정 기본값(hlab.discovery.format, 기본 LONG_FORM) 사용. 검색 조건의
  하드코딩 SHORTS를 fmt로 교체(키워드 없는 검색·키워드 검색 양쪽).
- application.yml: hlab.discovery.format 추가(기본 LONG_FORM, DISCOVERY_FORMAT env로 오버라이드).
- RecommendedChannelController /run: format 쿼리파라미터 수용.
- recommend.html: '지금 발굴' 옆 롱폼/쇼츠 선택 드롭다운(롱폼 기본), 소제목 KR·롱폼 반영.

동일 파일에 있던 진행 중 기능(추천채널 대표영상 '영상 재가공' 버튼 + /{id}/rework 엔드포인트)도 함께 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:32:28 +09:00
ab49e30a76 chore: 발굴 지역 인라인 기본값도 KR로 통일
application.yml(KR)이 이미 우선 적용되나, @Value 인라인 fallback이
KR,JP,US로 남아 혼동을 줘 KR로 맞춤.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:26:13 +09:00
bff39bb08b chore: 발굴 지역을 KR만으로 제한 (JP/US 제외)
예능(한국) 소재 중심이라 발굴 지역 기본값을 KR 단일로.
DISCOVERY_REGIONS env로 오버라이드 가능.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:16:55 +09:00
9dc0f0248a feat: 발굴 예능 특화(키워드 시드) + 텔레그램 아침 추천 알림
예능 전환 2단계 — 소재 발굴을 예능 쪽으로 강화하고, 매일 아침 추천을 자동으로 받는다.

발굴 예능 특화
- ChannelDiscoveryService: 지역 인기 Shorts 발굴에 더해, 설정된 예능 키워드
  (hlab.discovery.keywords, 기본 "예능,방송,하이라이트")로 지역×키워드 Shorts를
  조회수순 검색해 예능 클립 소재 채널을 우선 발굴.
- 매 검색마다 쿼터 가드, 소진 시 전체 발굴 중단(라벨 break). 키워드 비면 기존 동작 유지.

텔레그램 아침 추천
- TelegramNotifier 신규(global/notify): bot-token/chat-id 미설정 시 no-op,
  발송 예외는 삼켜 호출측을 깨지 않음. @Qualifier("restTemplate") 주입.
- 매일 04:30 발굴 직후 NEW 추천채널 상위 N개를 한국어 HTML 다이제스트로 발송.
- 활성화: env TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID 설정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:06:41 +09:00
d0faada002 perf: SQL 로깅 상시화 해제 + channel_videos 인덱스 추가
느려짐의 유력 원인 두 가지를 정리한다.
1) p6spy가 모든 SQL을 파라미터값 포함해 매번 로깅하던 것을 기본 OFF로.
   디버깅 시 P6SPY_LOGGING=true 로 켠다.
2) channel_videos에 인덱스가 하나도 없어 데이터가 늘면 풀스캔이었다.
   리포지토리의 실제 필터/정렬 컬럼에 인덱스 추가(ddl-auto가 자동 생성):
   video_id, channel_id, interest_status, source, category_id,
   published_at, views_per_sub_ratio

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:13:55 +09:00
f22f7028c4 refactor: 발행/유통(발행 큐) 기능 제거 — 예능 전환
유통(내가 올려서 발행)에서 예능 소재 파이프라인으로 방향을 틀면서
발행 큐 기능을 제거한다. domain/publish 전체와 발행 준비 UI(재가공 화면),
사이드바 발행 큐 링크, 대시보드 발행 현황 위젯을 걷어냈다.

- domain/publish/* (Controller/Service/Repository/Entity) 삭제
- publish.html · PublishServiceTest 삭제
- rework.html 발행 준비 섹션·JS 제거
- dashboard.html · DashboardService 발행 집계 제거
- WebController /publish 라우트, sidebar 링크 제거
- ChannelVideo.publishedAt(원본 게시일)은 발행 기능과 무관하여 그대로 유지

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:13:46 +09:00
fcfa4084e4 fix(comment-cards): 영상에서 확대 시 글자 깨지던 문제(저장 화질 상향)
캡처 배율이 2배 고정이라 카드(~320px)가 ~640px PNG로 저장돼 영상에서 확대하면
뭉개졌음. 화질 선택(2x/4x/6x/8x)을 추가하고 기본을 4배로 올림.
검정 배경 카드는 그레이스케일 안티에일리어싱을 적용해 색 번짐 제거.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:09:45 +09:00
387cdc9400 feat(comment-cards): 기본값을 검정 배경 + 둥근 모서리로 변경
카드 첫 렌더 시 배경 검정(글자 흰색) + 둥근 모서리가 적용되도록 초기값과
툴바 버튼 상태(활성 표시·레이블) 조정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:18:43 +09:00
1f2c4e8e95 feat(comment-cards): 내용 글자 크기 조절 + 배경색(흰/검) 옵션 추가
카드 레이아웃은 유지한 채 댓글 내용 텍스트만 확대/축소하는 A−/A+ 스테퍼와,
배경을 기본→흰색→검정 순환하는 토글 추가. 검정 배경일 때 글자는 흰색으로 대비 확보.
배율은 카드 인라인 변수(--cc-text-scale)로 지정해 재렌더·PNG 캡처에도 그대로 반영됨.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:50:46 +09:00
114aeb15d5 fix(comment-cards): 저장 표시 회색 톤이 다크 테마에서 안 보이던 문제
- surface-2는 다크에서 배경과 거의 같아 color-mix로 텍스트색을 섞어 확실한 회색으로
- 왼쪽 accent 바 + 흐림(opacity)로 저장 여부를 한눈에 구분
- 렌더 루프에서 카드마다 lucide.createIcons()를 호출하던 것 제거(전체 스캔 반복 방지)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:55:12 +09:00
73aebf82d3 feat(comment-cards): 저장한 카드 표시 + 저장 폴더 토글 해제
- 저장 완료 카드에 회색 톤(.saved)과 '저장됨 ✓' 버튼 상태 부여
- 저장 표시는 화면 전용: 캡처 직전 .saved를 벗겨 PNG엔 남지 않음
- 댓글에 고유 id를 붙여 필터·정렬 재렌더 후에도 저장 표시 유지
- '저장 폴더' 버튼 재클릭 시 지정 해제 → 브라우저 다운로드 폴더로 저장
  (해제 이후 '전체 저장'은 폴더를 다시 묻지 않음)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:40:54 +09:00
5ad597c97a feat(comment-cards): 저장 폴더 선택 + 표시된 카드 전체 저장
- '저장 폴더' 버튼(File System Access API)으로 폴더를 한 번 고르면
  ⬇ 저장·전체 저장이 그 폴더로 바로 저장(이 사이트 세션에만 적용)
- '전체 저장' 버튼: 현재 표시된 카드를 순서대로 일괄 저장(진행 표시)
- 미지원 브라우저(파이어폭스/사파리)는 폴더 버튼 숨김·기존 다운로드 폴백

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 12:05:12 +09:00
f109d08539 fix(comment-cards): UI/UX 리뷰 반영(접근성·대비·성능·상태표시)
- 토글(모자이크/모서리/단어가리기) ON 상태 accent로 시각화 + aria-pressed
- 검색·좋아요 필터 200ms 디바운스(대량 카드 렉 완화), 아바타 loading=lazy
- 수집 중 스피너, prefers-reduced-motion 대응
- 작은 회색 글자 대비 상향(text-3→text-2), 숫자 tabular-nums
- 아이콘 버튼/셀렉트 터치 타깃 확대, 입력 16px(iOS 확대 방지)
- 월별 댓글량 값 상시 노출(hover 의존 제거)
- 카드 키보드 접근(role/tabindex/Enter·Space 복사), focus-visible 링
- 섹션 아이콘 이모지→Lucide 통일(카드 내 👍💬는 export용 유지)
- 입력 aria-label, 결과 URL ?v= 딥링크(공유·새로고침 유지)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:35:42 +09:00
af53219dcc feat(comment-cards): 단어 클릭 가리기(부분 모자이크) 추가
기본은 댓글 단어 클릭 시 해당 단어만 블러(다시 클릭 해제).
'🖍 단어 가리기' 토글을 끄면 카드 클릭=이미지 복사로 전환.
블러는 캡처 PNG에도 그대로 반영됨.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:57:37 +09:00
528b48280c feat(comment-cards): 제외 단어 필터 추가
입력한 단어가 포함된 댓글을 카드·분석 양쪽에서 제외.
쉼표/공백 다중 지정, 대량 댓글 대비 입력 디바운스(250ms).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:07:40 +09:00
28b072df42 feat(comment-cards): 타임라인 분석에 시간순 정렬 옵션 추가
많이 언급된 타임라인 섹션에 '언급순/시간순' 선택 추가.
시간순은 선별된 상위 구간을 재생 순서대로 재정렬해 보여줌.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:52:39 +09:00
92726b1c08 feat(comment-cards): 전체 댓글 수집 + 인사이트 분석 패널 추가
- 수집 상한 1,000→20,000개(안전 상한)로 상향해 전체 댓글 기준 분석
- 분석 패널: 많이 언급된 타임라인(클릭 시 해당 시점 유튜브 이동), 자주 나온 단어(조사 제거+불용어, 좋아요 가중 토글), 요약 통계(총/평균 좋아요·참여자·최다 좋아요 댓글·월별 댓글량)
- 모자이크 기본 ON, '모서리 둥글게' 토글 추가(기본 각진 모서리)
- 카드 렌더는 상위 2,000개로 제한(브라우저 보호), 분석은 전체 기준

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:37:15 +09:00
b1b23a45f2 feat(comment-cards): 댓글 내용 실시간 검색 필터 추가
필터바에 검색 입력창 추가 → 입력 즉시 댓글 내용에 그 단어가 포함된 카드만 노출
(작성자 제외, 내용만 매칭). 클라이언트 필터라 추가 API 호출 없음. JS 캐시버스터 갱신.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:41:20 +09:00
3a3eeef87a feat(comment-cards): 댓글 수집 상한 500→1,000개로 상향
MAX_PAGES 5→10 (페이지당 100개). commentThreads는 호출당 1유닛이라
1,000개=10유닛으로 쿼터 부담 미미.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:46:35 +09:00
ba3776eb7e feat(comment-cards): 카드 PNG 파일 저장(다운로드) 버튼 추가
캡컷은 클립보드 이미지 붙여넣기를 못 받아 파일 불러오기가 필요 → 카드별 '⬇ 저장' 버튼으로
PNG 다운로드 제공. 저장 버튼은 cc-noexport+modern-screenshot filter로 캡처 이미지에서 제외.
카드 클릭=클립보드 복사는 유지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:45:46 +09:00
7de8f954ad fix(comment-cards): 복사 버튼 제거, 카드 클릭으로 복사
복사 버튼이 캡처 이미지에 함께 찍히던 문제 해결. 버튼을 없애고 카드 자체 클릭으로
복사하도록 변경, 완료 피드백은 카드 밖 토스트로 표시(캡처에 미포함). JS 캐시버스터 갱신.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 18:31:25 +09:00
ee5a13163b fix(comment-cards): 카드 메이슨리 레이아웃으로 짧은 글 흰 여백 제거
grid가 행 단위로 최대 높이에 맞춰 늘어나던 것을 CSS 다단(column)으로 변경.
각 카드가 내용 높이에 맞고 빈틈없이 쌓이도록 break-inside:avoid + margin-bottom 적용.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 18:24:13 +09:00
6136eb83e9 fix(comment-cards): 일시적 YouTube 403/5xx 자동 재시도
commentThreads가 order=relevance에서 간헐적으로 403 forbidden을 반환하는 문제 대응.
generic 403/rateLimit/5xx는 백오프 후 최대 2회 재시도, commentsDisabled/quotaExceeded는
재시도 없이 즉시 안내. 재시도 성공/비재시도 단위 테스트 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 18:13:19 +09:00
6f59554b6d fix(comment-cards): YouTube API 오류를 reason별로 구분 처리
403을 무조건 '댓글 비활성화'로 표시하던 문제 수정. error.errors[0].reason을 파싱해
commentsDisabled/quotaExceeded/기타(keyInvalid 등)를 각각 안내하고, 비-403 오류가
opaque 500으로 새던 것도 의미있는 메시지로 노출. 단위 테스트 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:54:50 +09:00
0ee343ceca feat(comment-cards): 카드 PNG 클립보드 복사 추가
modern-screenshot로 카드 캡처→클립보드(투명배경, 모자이크 반영), 미지원 시 다운로드 폴백

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:07:13 +09:00
418242f47d feat(comment-cards): 댓글 카드 페이지·사이드바·렌더/정렬/필터 추가
/comment-cards 라우트, 유튜브 스타일 카드 렌더, 정렬/임계값 필터, 전체 모자이크 토글

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:06:53 +09:00
0e84f5de08 feat(comment-cards): 댓글 fetch + 프로필 프록시 API 추가
POST /api/comment-cards/fetch, GET /api/comment-cards/avatar(구글 도메인 화이트리스트)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:05:27 +09:00
15a655d7f9 feat(comment-cards): commentThreads 댓글 수집 서비스 추가
CommentCardDto + YoutubeCommentService(최대 5페이지, 쿼터가드, 댓글비활성 처리)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:05:27 +09:00
39e2b42995 feat(comment-cards): 유튜브 videoId 파서 추가
watch/youtu.be/shorts/embed/순수ID 추출, 단위 테스트 포함

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:04:15 +09:00
e2caf9a0ba docs(board): 안내 문구에 모바일 상태 드롭다운 사용법 추가
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:04:45 +09:00
e7418e7c3c feat(board): 모바일에서 카드 상태 변경 드롭다운 추가(터치로 칸반 이동)
HTML5 드래그가 터치에서 동작 안 하는 문제 — 카드마다 상태 드롭다운을 두어
터치로도 컬럼 이동 가능. 데스크톱은 드래그 유지(드롭다운은 모바일/터치에서만 노출).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:03:10 +09:00
db8452d1ea feat(channels): 전체 선택을 현재 활성 국가 탭으로 한정
전체 선택/카운트/선택 영상 보기를 보이는 카드 기준으로 동작, 탭 전환 시
숨겨진 카드는 자동 선택 해제.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:45:09 +09:00
24f964364a fix(channels): 국가 탭 라벨 한글화(국기 이모지 미렌더 대응)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:25:59 +09:00
84155a69f8 feat(channels): 국가별 탭(KR/JP/US/기타) + Channel.country 수집·백필
Channel 에 country 추가(YouTube snippet.country 저장), 추천 채널 region 으로 기존 채널
백필(POST /api/channels/backfill-country), /channels 에 국가 탭+카운트 필터.
선택 영상 보기 버튼 한글화.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:24:40 +09:00
fec4c77cb9 feat(channels): 채널 영상 모아보기 카드형 재디자인 + 한국어 + 정렬 드롭다운
테이블→카드 그리드(썸네일·배율 배지·조회수·구독자·날짜), 한국어 라벨, 정렬 드롭다운
(배율/조회수/최신/구독자), 미리보기 모달(9:16), 선택/전체 담기·다운로드 기능 보존.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:02:53 +09:00
f3c6d20442 fix(channels): 전체 선택 시 체크 시각화 깨지던 문제(lucide가 i를 svg로 치환)
querySelector('i')가 lucide 치환 후 null→TypeError로 시각 토글이 첫 카드에서 멈춰
전체 선택이 안 되는 것처럼 보였음. querySelector('svg, i') + null 가드로 수정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:44:53 +09:00
fd18a8981a feat(discover): 제외 목록 보기 + 복원 기능
- 목록 API status 파라미터(NEW|EXCLUDED|REGISTERED), POST /{id}/restore(→NEW)
- recommend.html: 헤더 '제외 목록' 토글로 EXCLUDED 채널 보기, 카드에 '복원' 버튼
  (제외 보기에선 등록/제외 대신 복원+채널이동만), 지역 탭은 그대로 적용

검증: 제외→EXCLUDED 목록 노출→복원→NEW 복귀 end-to-end 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:48:34 +09:00
1d7d50f50b feat(discover): 추천 채널 카드에 '채널 이동'(유튜브 채널 새 탭) 버튼 추가
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:39:07 +09:00
08d50230c4 feat(discover): 추천 채널 지역(KR/JP/US) 탭 + 발견 지역 태깅
- region 을 채널 국가가 아닌 '검색 발견 지역(regionCode)'으로 태깅(UNKNOWN 해소)
- upsert: 같은 배율이면 갱신(지역 등 메타 최신화)
- recommend.html: 전체/KR/JP/US 탭으로 필터, 등록/제외 시 목록 갱신

검증: 재발굴 후 KR14·JP7·US1 로 분포, 탭 필터 동작.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:34:43 +09:00
3bfb7a7495 feat(discover): 추천 채널 썸네일 클릭 시 미리보기 모달(다른 페이지와 동일)
YouTube 새 탭 대신 인라인 iframe 모달로 영상 재생.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:26:11 +09:00
e9b192eabb feat(discover): 추천 채널 전용 페이지 /recommend + 사이드바 + 발굴 조회수순 보강
- recommend.html(카드: 썸네일·구독자·배율·대표영상, 등록/제외) + WebController 라우트 + 사이드바 메뉴
- 발굴 검색 order=viewCount 강제(키워드 없을 때 최신순→조회수순) — 떡상 채널이 잡히게
  (YoutubeSearchCondition.order 추가, YoutubeSearchService 반영)

검증: /run → KR,JP,US 26개 떡상 채널 발굴(구독 76~7천, 배율 1500~9만x).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:49:42 +09:00
35944a8fd6 feat(discover): 발굴 일별 스케줄 연결 + 추천 채널 REST(목록/등록/제외/수동)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:44:27 +09:00
02fa5be25b feat(discover): ChannelDiscoveryService(지역 Shorts 검색→떡상 채널 upsert)+설정
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:43:24 +09:00
507ae7c760 feat(discover): RecommendedChannel 엔티티+Repo, DiscoveryRanker 랭킹로직(TDD)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:41:52 +09:00
279cb4c9d0 chore(rework): 제거된 버튼의 잔여 안내문구·죽은 JS 정리
URL자막 버튼 제거에 따라 안내문/placeholder의 'URL자막' 언급 삭제,
미사용 함수 extractScript/translateToEditor 제거(copyToEditor는 유지).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:49:00 +09:00
0decc4439c chore(rework): 상단 버튼 정리 — URL자막·번역→재작성·[00:00]번역 제거
AI 자막(음성/화면) 기능이 번역을 대체해 중복되는 버튼 3개 삭제.
원본 다운로드·자막 추출(음성)·받은 원본 삭제·영상 업로드·에디터로 복사는 유지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:46:44 +09:00
d16f23a4c0 feat(rework): AI 자막 패널에 원문+번역 비교 표시
화면(Gemini)·음성 패널에서 원문과 한국어 번역을 함께 보여줘 비교 가능하게 함.
- ScriptResponseDto.Segment: original 필드 추가(NON_NULL)
- Gemini 프롬프트/스키마: original(화면 원문) + text(한국어 번역) 둘 다 요청,
  text 는 반드시 한국어(영어 방지) 명시
- rework.html: 각 줄에 한국어(위)+원문(아래 회색) 표시, 음성 패널은 Whisper 원문 사용

검증: 470 화면자막 → 원문 일본어 + 한국어 번역 + 진행형 타임스탬프 정상.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:12:31 +09:00
b151204267 fix(gemini): 화면 자막 타임스탬프 0/빈결과 문제 해결
원인: Gemini 2.5 Flash의 thinking(타임스탬프 추론)+JSON 출력이 자막 많은 영상에서
출력 토큰을 초과해 JSON이 잘림 → 0개 또는 타임스탬프 전부 0.

- maxOutputTokens 65536(최대)로 상향 — thinking+JSON 잘림 방지(핵심)
- temperature 0 — 일관성
- 프롬프트: 시작/끝 시각 정확히, 전부 0 금지, 누락 없이 추출하도록 보강
- 빈 결과면 최대 3회 재시도(영상이해 비결정성 보완)

검증: 470·560(자막 많은 영상) 모두 진행형 타임스탬프로 정상 추출.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:43:47 +09:00
99dbe80fdb fix(rework): 자막 하이라이트가 페이지 전체를 스크롤해 영상이 흔들리던 문제
재생 중 활성 세그먼트로 이동할 때 scrollIntoView가 페이지를 스크롤해 sticky 영상이
위아래로 튀던 현상 수정. 세그먼트 박스(#segmentList) 내부에서만 스크롤하도록 변경.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:25:41 +09:00
31b7720af9 feat(rework): '전사' → '자막 추출(음성)' 용어 변경 + 왼쪽 영상 패널 sticky
- 이해하기 쉽게 모든 '전사' 문구를 '자막 추출'로 변경(메인 버튼은 '자막 추출(음성)')
- 좌측 영상 카드 position:sticky(top:1rem) → 스크롤해도 상단에 고정되어 항상 보임

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:12:26 +09:00
7b94515180 fix(rework): AI 자막 - 음성 없음은 오류 아닌 안내(회색)로 표시
말소리 없는 영상(음악·화면자막만)의 음성 패널을 빨강 오류가 아니라 회색 안내
('음성 없음 — 말소리가 없거나 전사 미실행')로 표시. renderAiPanel에 note 인자 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:31:02 +09:00
6380a28a6f fix(rework): Gemini 실패 사유를 사용자에게 명확히 + 음성 없음 메시지 정정
- GeminiSubtitleService: 403(프로젝트 차단)/429(한도)/400(키 무효)을 한국어 메시지로
  IllegalArgumentException throw → 'Internal Server Error' 대신 실제 사유 노출
- rework.html: 음성 패널 빈 경우 '말소리 없음(음악/화면자막만) 또는 전사 미실행'으로 정정

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:16:29 +09:00
447645c19a feat(rework): AI 자막 2종(음성 Whisper / 화면 Gemini)→한국어, 좌우 분할 + SRT
재가공 화면에 'AI 자막(음성/화면)' 카드 추가. '생성' 한 번에:
- 왼쪽(음성): 저장된 Whisper 세그먼트(정밀 타임스탬프) → LibreTranslate 한국어
- 오른쪽(화면): 유튜브 URL → Gemini가 화면 박힌 자막 추출+한국어 번역
각 패널을 [00:00] 한국어 리스트로 표시, 각각 SRT 다운로드(audio_ko/screen_ko).

- GeminiSubtitleService 신규: youtube URL을 generativelanguage API에 전송,
  responseMimeType=json 구조화 응답 파싱. buildRequest/extractSegments/sanitizeApiKey 순수+단위테스트
- sanitizeApiKey: 잘못 붙은 선행 '='·공백 정리(env var 오타 방어)
- geminiRestTemplate 빈(5분), POST /{id}/gemini-subtitles, CurationService.geminiScreenSubtitles
- 설정 gemini.api-key/model(gemini-2.5-flash), rework.html 분할 카드+JS
- 음성 한국어는 기존 translate(format=segments) 재사용

검증: 470 화면자막 → 한국어 27세그먼트 30초 추출(스타일 일본어 자막도 정확).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:04:24 +09:00
7ebee1caa3 revert(rework): 화면 자막 OCR 기능 제거
Tesseract 기반 OCR이 실제 쇼츠의 스타일 자막을 거의 못 읽어 실효성이 없고,
URL→Gemini 방향으로 전환하기로 해 h-lab의 OCR 연동을 제거한다.

- 컨트롤러 POST /{id}/ocr 제거
- CurationService.ocrScreenSubtitles 제거
- ChannelService.ocrFromCached/joinSegmentText 제거(persistScript는 전사가 계속 사용)
- rework.html '화면 자막 OCR' 버튼·crop/fps 입력·ocrScreen() 제거
- docs/python-service/ocr_video_endpoint.py(videocr 예시) 삭제

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:19:32 +09:00
fbce1ec8ea feat(rework): 화면 자막 OCR 속도(sample_fps) 컨트롤 추가, 기본 1
저사양 서버(N150)에서 sample_fps=3은 프레임 과다로 10분 타임아웃 발생.
OCR 영역(crop %) 옆에 'fps' 입력을 추가하고 기본을 1로 낮춰 빠르게 끝나게 함
(자막이 빨리 바뀌면 2~3으로 올림). ocrScreen()이 sampleFps 를 함께 전달.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:42:57 +09:00
32b2eae62c feat(rework): 받아진 상태면 '원본 다운로드' 숨기고 '전사' 버튼 노출
캐시(받은 원본) 보유 시 '원본 다운로드'를 숨기고, 그 자리에 '전사' 버튼을 띄운다
(전사만 실패했을 때 재다운로드 없이 재시도). 캐시 없으면 다시 '원본 다운로드' 표시.

- updateCacheUI: downloadBtn 숨김/표시 대칭 토글 + transcribeCachedBtn 노출
- runTranscribeCached 헬퍼 추출(다운로드 2단계 전사와 '전사' 버튼이 공유)
- '전사' 버튼 onclick=transcribeCached()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:25:35 +09:00
bba81760a2 fix(rework): 원본 다운로드와 전사 분리 — 전사 지연/실패에 안 갇히게
기존엔 '원본 다운로드'가 다운로드+Whisper전사를 한 동기호출로 처리해, 전사 서버
(h-python)가 느리거나 타임아웃나면 화면이 '다운로드 중'에서 안 풀렸다.
다운로드(빠름)와 전사(분리)를 나눠, 다운로드는 즉시 끝나고 영상이 바로 뜨며
전사가 실패해도 다운로드 결과는 유지(화면자막 OCR/재시도 가능).

- POST /{id}/download: 다운로드만(캐시), 응답 {downloaded, sizeBytes}
- POST /{id}/transcribe-cached: 받은 원본 전사(분리)
- downloadOriginal(): 1)다운로드 즉시완료·영상표시 2)전사 별도(실패해도 다운로드 유지)

검증: /download 2.7초 완료. 전사는 분리 단계로 진행/실패 격리.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:19:18 +09:00
6a0a7294bb feat(rework): 화면 자막 OCR 에 자막영역(crop) 전달 추가
OCR 정확도·속도 향상을 위해 '자막영역 하단 N%' 입력을 추가, 플레이어 영상의 실제
해상도로 crop 픽셀(crop_x/y/width/height)을 환산해 /ocr_video 로 전달한다.
100%면 use_fullframe=true, 해상도 못 읽으면 생략(서버 기본 하단30%).

- ocrFromCached(file, formParams Map)로 일반화 — 서버 필드명 그대로 전달
- CurationService/Controller에 useFullframe·cropX/Y/Width/Height 파라미터 추가
- rework.html: 자막영역 % 입력 + ocrScreen()이 영상 해상도로 crop 계산

검증: sample_fps=3 + 하단30% crop 으로 2m28s 완료(프록시 600s 적용 후 504 해소),
crop 전달 정상 동작 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:53:42 +09:00
f7ac96f1dd feat(rework): 화면 자막 OCR(영상 박힌 자막) 연동
받은 원본 영상을 Python /ocr_video(ffmpeg+Tesseract)로 보내 화면에 박힌 자막을
시간 싱크 세그먼트로 추출·저장한다. 음성 전사와 같은 자리(ChannelVideoScript)에
저장되어 스크립트 리스트·SRT·번역·한국어 SRT에 그대로 흐른다.

- ChannelService.ocrFromCached + persistScript 공통 추출(전사/OCR 공유), joinSegmentText
- CurationService.ocrScreenSubtitles(받은 원본 캐시 사용), POST /{id}/ocr
- rework.html '화면 자막 OCR' 버튼 + ocrScreen()
- sampleFps/confThreshold null이면 미전송 → 서버 기본값 사용

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:31:56 +09:00
a2934af1a3 chore(translate): 번역 서비스 URL을 h-translate.tolag.shop 으로 변경
h-etc2.tolag.shop → https://h-translate.tolag.shop (application.yml 기본값 + TranslateService @Value).
새 URL 동작 확인(/translate, /languages 정상).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:05:26 +09:00
e9ad800134 feat(rework): 한국어 SRT 내보내기 추가
세그먼트를 한국어로 일괄번역해 기존 타임라인(타임코드·배속·말없는구간 제거)에
그대로 끼워 SRT로 내보낸다. 원본 SRT와 별개로 '한국어 SRT' 버튼 추가.

- translateScript format=segments: 세그먼트와 1:1 정렬된 번역 배열 반환
- rework.html exportKoSrt(): texts 배열을 workingSegments에 매핑해 buildSrt로 생성
  (텍스트만 한국어로 교체, 타임코드/배속은 기존 로직 재사용)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:45:20 +09:00
198334c6f8 feat(rework): 타임라인 [00:00] 형식 번역 옵션 추가
'[00:00] 번역' 버튼 추가 — 세그먼트별로 번역해 '[mm:ss] 한국어' 줄로 재작성 칸에 내린다.
LibreTranslate의 q 배열 일괄번역(1회 호출, 입력순서 1:1 정렬)을 사용.
기존 '번역→재작성'(평문 흐름)은 그대로 유지(format=plain|timeline).

- TranslateService.translateBatch + buildBatchRequestBody/extractTranslatedTexts(+단위테스트)
- translateScript(id,target,format): timeline 분기, 세그먼트 시각 [mm:ss] 머리표시
- 컨트롤러 format 파라미터, rework.html '[00:00] 번역' 버튼

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:10:05 +09:00
77e77894c9 feat(rework): 원본 스크립트 번역 → 재작성(내 버전) 버튼 추가
자가호스팅 LibreTranslate(h-etc2.tolag.shop) 연동. 재가공 화면 '번역→재작성'
버튼으로 원본 전사 스크립트를 한국어로 번역해 재작성 칸 초안으로 채운다.
소스 언어는 전사 언어로 자동(없으면 auto), 타깃 기본 ko.

- TranslateService 신규: POST /translate {q,source,target} → translatedText
  (요청 조립/응답 파싱 순수 메서드 + 단위테스트)
- POST /{id}/translate 엔드포인트, CurationService.translateScript
- rework.html: '번역→재작성' 버튼 + translateToEditor()
- 설정 translate.base-url(기본값 有)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:41:23 +09:00
ff9609c9d1 feat(rework): 받은 원본을 왼쪽 플레이어에 싣고 타임라인 클릭으로 seek
원본 다운로드/캐시 보유 시 왼쪽 플레이어를 YouTube 임베드 대신 받은 원본 mp4로
전환한다. 세그먼트(타임라인) 클릭 시 기존 seekTo/하이라이트가 그대로 동작해
해당 지점으로 이동·재생된다. (업로드본이 있으면 업로드본 우선)

- GET /{id}/download/file: 받은 원본 스트리밍(Resource 반환 → HTTP Range 206 자동 지원)
- CurationService.cachedDownloadFile(id)
- rework.html: showServerVideoInPlayer/showYoutubeInPlayer, updateCacheUI가 플레이어 전환,
  다운로드 직후/진입 시 캐시 있으면 자동 적용, 삭제 시 YouTube로 복귀

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:58:23 +09:00
986dcffd55 feat(rework): 받은 원본 삭제 + 영상 삭제 시 캐시 자동정리
1) 재가공 화면에 '받은 원본 삭제' 버튼 추가(캐시 보유 시에만 노출).
   GET/DELETE /{id}/download 로 캐시 상태 조회·삭제(자막/세그먼트는 유지).
   진입 시 캐시 상태를 조회해 삭제버튼 노출 + 렌더 캐시 사용 여부를 갱신.
2) 수집함 영상 삭제 시 downloads/{id}.mp4 도 함께 삭제(고아 파일 방지).

- VideoDownloadService.deleteCache(id) + 단위테스트(@TempDir)
- CurationService: downloadStatus/deleteDownloadCache, delete() 캐시 정리

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:10:02 +09:00
f4ac1be171 docs(rework): 사용법 모달에 '원본 다운로드' 이미지 가이드 추가
재가공 사용법 모달 상단에 실제 화면 캡쳐 3장으로 단계별 안내 추가:
1) 원본 다운로드 버튼  2) 자동 전사된 시간싱크 자막  3) 내보내기(업로드 불필요).
?help=1 딥링크로 사용법을 바로 펼칠 수 있게 함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:09:30 +09:00
26b8be1ff1 feat(rework): yt-dlp 원본 다운로드 → 전사·렌더 파이프라인 자동연결
재가공 화면 '원본 다운로드' 버튼으로 저장된 videoId를 yt-dlp(로컬 ProcessBuilder)로
받아 서버에 캐시하고, 그 파일을 기존 Python /transcribe·/render 에 그대로 투입한다.
수동 다운로드+업로드 단계를 전사·렌더 양쪽에서 제거.

- VideoDownloadService 신규: 인자 리스트 ProcessBuilder(셸 미사용), videoId 검증, 캐시 조회
- ChannelService: 전사·렌더를 Resource 기반 공통 메서드로 추출(업로드/캐시 공유)
- 컨트롤러: POST /{id}/download(다운로드+전사), /render 의 file 을 선택값으로(없으면 캐시)
- rework.html: '원본 다운로드' 버튼 + downloadOriginal(), 렌더가 서버 캐시 재사용
- 설정 ytdlp.*/download.dir(기본값 有), downloads/ gitignore
- buildCommand/videoId 검증 단위테스트(src/test 신규)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:58:50 +09:00
8ec6268a1e fix(ui): 모바일에서 사이드바 펼치면 텍스트 라벨도 표시
모바일(≤768px)에서 사이드바를 펼쳐도 아이콘만 보이던 문제 수정.
태블릿용(≤1024px) 규칙이 .nav-text/.brand-text/.nav-section/.user-info를
display:none 처리하는데, 모바일 오버레이로 펼친 상태(.sidebar:not(.collapsed))
에서 그게 안 풀려 아이콘만 남았다.
→ 펼친 상태에서 라벨 다시 표시(display:revert) + nav 좌측 정렬. 모바일 사이드바를
전체폭 대신 280px 드로어 오버레이(그림자)로 정리.
CSS 변경에 맞춰 캐시버스팅 버전 20260617b 로 bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 07:26:42 +09:00
921c24f143 fix(ui): 모바일 깨짐 수정 — 표 카드뷰 opt-in, 대시보드 반응형, CSS 캐시버스팅
폰에서 화면이 깨지던 문제 수정.
- 전역 반응형 표 '카드뷰'가 모든 table에 적용돼, data-label 없는 목록 표
  (수집함·발굴·발행 등)가 거대한 행으로 변해 페이지가 32만px로 늘어남.
  → 카드뷰를 .rcards opt-in 으로 스코프. 그 클래스 가진 레거시 표
  (multi_channel_videos·videos)만 카드뷰, 나머지는 모바일에서 가로 스크롤.
- 대시보드: 모바일(≤680px) 미디어쿼리 추가 — 마스트헤드 세로 정렬, 긴
  브레드크럼 숨김, 떡상 리더보드 액션 버튼을 아랫줄 우측 정렬, 숫자 축소.
- base.html: CSS/JS 링크에 v= 캐시버스팅 — 배포 후 브라우저가 옛 CSS를
  캐시해 스타일이 깨지는 것 방지(CSS 변경 시 버전 올리기).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 06:56:50 +09:00
e0d92792b1 feat(ui): 전체 화면을 편집(editorial) 디자인으로 통일
대시보드의 editorial 정체성을 공용 디자인 토큰으로 끌어올려 전 페이지에 전파.
- variables.css: 종이 캔버스 + 잉크 텍스트 + 버밀리언(#ff4d23) 단색 액센트,
  Bricolage Grotesque(헤딩, 라틴) + Hanken Grotesk(본문) + JetBrains Mono,
  샤프한 모서리(radius 축소). 라이트 기본 + 다크. 기존 변수 별칭 유지로
  각 페이지가 자동 적응.
- style.css: 헤딩 디스플레이 페이스, primary 버튼 단색 버밀리언, 배지 각짐 등
  편집풍 마무리 터치.
한글은 디스플레이/모노 폰트에 없으므로 시스템 폰트로 자연 폴백.
수집함·칸반 라이트/다크 Playwright 검증.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:15:59 +09:00
c0ade287c2 feat(ui): 썸네일 클릭 미리보기를 칸반·채널상세·프로덕션상세에 추가
수집함·발굴·대시보드처럼, 썸네일 있는 나머지 화면에서도 썸네일을 클릭하면
YouTube 9:16 미리보기 팝업이 뜨도록 통일.
- 칸반(board): 카드 썸네일 클릭(드래그와 분리 위해 stopPropagation)
- 채널상세(channel_detail): 영상 목록 썸네일(제목 링크는 YouTube 유지)
- 프로덕션상세(production_detail): videoUrl에서 11자리 id 추출해 임베드,
  id 없으면 새 탭 폴백
각 페이지에 동일 모달(배경/ESC 닫힘, 닫을 때 iframe 비워 재생 중지) 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:18:09 +09:00
c7ce8de90e feat(ui): 대시보드 떡상 리더보드 썸네일 클릭 미리보기
수집함처럼, 떡상 후보 리더보드의 썸네일을 클릭하면 YouTube 임베드(9:16)
미리보기 팝업이 바로 뜨도록 추가. 편집 정체성에 맞춘 모달(.fd-vmodal),
배경 클릭/ESC로 닫힘, 닫을 때 iframe 비워 재생 중지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:52:24 +09:00
b97e4644ea fix(ui): 대시보드 빈 화면 수정 — Thymeleaf [[ 충돌 제거
대시보드 인라인 스크립트의 `const stages = [['미검토',...],[...]]` 에서
앞쪽 `[[` 가 Thymeleaf 인라인 표현식 문법(`[[${...}]]`)과 충돌해,
렌더링이 그 지점에서 예외로 잘려 응답이 truncate → JS 미완성 → 빈 화면이었다.
(로그의 "response committed already" 예외가 이 증상)

배열 리터럴을 줄바꿈/공백으로 풀어 `[[`·`]]` 인접을 제거. 렌더 결과가
온전해짐(잘림 21217B → 정상 24519B, </html>·loadDashboard() 복구).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:38:28 +09:00
22dd2f6d87 feat(ui): action-first editorial dashboard (frontend-design)
Rebuild the dashboard with a distinctive editorial identity (Bricolage Grotesque
display + JetBrains Mono tickers + paper canvas + vermilion signal accent, ruled
layout, grain, staggered load) scoped to .fd-dash so other pages keep the SaaS
theme. UX is action-first:
- action bar: 미검토/떡상 후보/재가공 대기 with primary CTAs (real counts)
- 떡상 후보 leaderboard wired to real /api/dashboard/summary outperformers, with
  working per-row 재가공(/rework/{id}) and 제외(status EXCLUDED) actions
- pipeline funnel with bottleneck callout + 칸반 CTA
- source/format + publish with empty-state guidance
Light/dark aware. Editorial fonts loaded in base.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:07:01 +09:00
dd42499f43 feat(ui): localize production page + page-header
Production -> 프로덕션, Crawl History -> 크롤 이력, table headers to Korean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 06:33:45 +09:00
652d25a483 feat(ui): discover table fits viewport (merged 영상 column)
Merge thumbnail+title+channel into one "영상" column like 수집함 so the table
fits without horizontal scroll. Promote .coll-table/.row-sel to global style.css
for reuse across list pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 06:32:27 +09:00
f3be6bd803 feat(ui): localize channels page to Korean
채널/추적 안내/전체 선택/채널 추가/구독자/영상 수/총 조회수/상세/방문/삭제.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 06:24:49 +09:00
1ebe2dda44 feat(ui): discover filter chips + help modals, localize, tokenize long pages
- discover: filter converted to toggle chips + inline selects (matches 수집함);
  page-header + 사용법 modal (배율 지표/필터/고른 뒤 안내)
- rework: 사용법 modal (전사·세그먼트·무음제거·내보내기·발행 guide) + page-header
- dashboard: h1 "Dashboard" -> "대시보드"
- channel_detail: sort-active header uses accent color (was invisible text-white)
- multi_channel_videos/videos/production_detail: tokenize dark-assuming colors
  and title-link text-white for light-theme readability

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 06:22:55 +09:00
52c6b51e61 feat(ui): roll out light/dark theme to board, publish, rework, discover, etc.
- board: tokenized kanban columns/cards; page-header + 사용법 modal (drag guide)
- publish: tokenized table/tabs; accent tab-active; page-header + 사용법 modal
- rework: tokenized the many dark-assuming inline styles so the editor renders
  correctly in the light theme
- discover/channels/production/channel_detail: tokenized dark-assuming colors
  (white text, translucent-white surfaces, #1e1e2d) for light-theme readability

Verified board/publish/rework/discover render cleanly in light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 06:07:51 +09:00
616003479b feat(ui): collection — compact category, help modal, fit-to-width table
- Category management collapsed into a slim single-row strip
- "사용법" button opens a reusable help modal (.modal-overlay/.modal-card +
  .help-item) explaining categories, filters, list view, and row actions
- Merge thumbnail+title+channel into one "영상" column and tighten cells so the
  whole table fits the viewport — no horizontal scroll on page or table card
- #mainContent min-width:0 to avoid flex overflow

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:12:03 +09:00
fc58a32a20 feat(ui): collection filter toolbar — toggle chips + tidy inline selects
Replace raw checkboxes + stacked-label dropdowns with pill toggle chips
(.chip, accent when checked via :has) and a single aligned toolbar row;
result count as a badge. Adds reusable .toolbar/.chip/.field components.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:45:05 +09:00
7497cb08ce fix(ui): collection page readable in light theme
Tokenized dark-assuming inline colors (white title/dropdown text, translucent
white surfaces, #1e1e2d modal) so the collection table renders correctly in the
light theme; header uses shared page-header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:39:45 +09:00
adb51943a3 feat(ui): light/dark SaaS design system + redesigned sidebar & dashboard
Phase A+dashboard of the UI redesign:
- variables.css: light-default tokens + [data-theme=dark] overrides; old var
  names aliased to theme tokens so existing markup adapts to both themes
- style.css: refined components (card/btn/nav/badge/bar/table/forms), tokenized
  the dark-assuming rgba colors
- theme toggle: pre-paint init in base.html <head> + toggleTheme() in common.js,
  persisted to localStorage; toggle button in sidebar
- sidebar.html: labeled nav with sections (분석/파이프라인/제작), active state,
  account; Korean labels
- dashboard.html: tokenized inline colors; verified light & dark with real data

Spec: docs/superpowers/specs/2026-06-12-ui-redesign-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:26:01 +09:00
fa6342f97e fix(rework): long-op timeout, dedup query, clearer error, logging
Review follow-ups (safe fixes):
- pythonRestTemplate bean (10min read timeout) for /transcribe and /render so
  ffmpeg encoding / Whisper don't hit the shared 120s timeout; resolved by bean
  name so existing restTemplate injections are unaffected
- getScriptData: fetch the script row once instead of 3 separate queries
- renderTrimmed "no segments" now IllegalArgumentException (400 + visible message)
  instead of IllegalStateException swallowed as generic 500
- ProductionService: System.out.println -> log.info (3x)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:06:00 +09:00
8178d45209 feat(rework): speech-gap trimming + render, language override
Phase 3: remove "no-talk" gaps (Whisper-segment based, not audio silencedetect
which finds nothing under background music) and render a trimmed (+speed) video
via ffmpeg, with subtitles remapped to match.

- KeepIntervalPlanner + TimelineRemapper (pure, unit-tested): keep/remove plan
  from segments (pad/minGap) and timestamp remap f(t)=t-removedBefore(t)
- GET /{id}/trim-plan (preview: keep/remove/remapped segments/kept duration)
- POST /{id}/render (multipart: file,pad,minGap,speed) -> proxy Python /render
  (ffmpeg trim/atrim+concat+atempo) -> mp4 download; ffmpeg graph validated locally
- rework.html: export panel (speed + speech-gap trim preview + SRT/video export),
  client-side SRT from working segments, language selector (auto/ko/en/zh/ja)
- transcribeFromFile forwards optional language (Whisper auto-detect misfired -> zh)

Spec updated with the audio-silence -> speech-gap design correction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:53:32 +09:00
b9aa04d4a3 feat(rework): synced subtitle extraction via file upload + Whisper, SRT export
Phase 1 of subtitle timeline studio. Upload a video in the rework editor →
proxy to Python /transcribe (faster-whisper) → store segments → render a
playback-synced segment list and export CapCut-importable SRT.

- ScriptSegment + SrtFormatter (pure, unit-tested) with speed rescaling
- ChannelVideoScript.segmentsJson column (ddl-auto adds it)
- ChannelService.transcribeFromFile + getSegments/parseSegments
- POST /{id}/transcribe (multipart), GET /{id}/script.srt?speed=, /script now returns segments
- rework.html: upload button, local <video>, segment list, SRT export + speed
- multipart 200MB limit; python.base-url config (PYTHON_BASE_URL)
- repo: findFirstByVideoIdOrderByIdDesc/findAllByVideoId (dedup-safe lookups)

Spec: docs/superpowers/specs/2026-06-12-subtitle-timeline-studio-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:23:55 +09:00
hehih
9edd4c5d97 fix(publish): dashboard 'recent' = latest-updated 5, stable status order
dashboardSummary surfaced the earliest-scheduled packages as "recent"
(it reused the queue's scheduledAt-asc sort); query updatedAt-desc instead.
Build byStatus from an explicit ordered list since Set.of iteration order
is undefined, so the dashboard renders DRAFT/READY/PUBLISHED consistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:40:58 +09:00
hehih
fd8403438e chore(security): externalize secrets to git-ignored application-local.yml
Remove hardcoded DB credentials and YouTube API key fallbacks from
application.yml; resolve them from env vars or an optional, git-ignored
application-local.yml (spring.config.import). Add a tracked
application-local.yml.example template and ignore the real local file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:08:26 +09:00
2cdae03998 test: add unit test infrastructure and core-logic tests
First tests in the project (src/test was absent). Add testRuntimeOnly
junit-platform-launcher (required by Gradle 9 to load the JUnit Platform),
then cover:
- VideoMetrics: duration parse, isShorts boundary, viewsPerHour clamp/
  division, viewsPerSubRatio rounding & zero-guards (9 cases, pure)
- DashboardService.summary(): composes each domain service under the
  right key (Mockito)
- PublishService.dashboardSummary(): status counts + recent capped at 5

12 tests, all green. No Spring context / DB needed — fast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:37:52 +09:00
9828918b97 style: dedupe to shared components (publish badge, filter selects, gap token)
- publish.html: status column uses shared .badge (badge-muted/warning/
  success) instead of its own .st-badge inline-color span
- collection.html / discover.html: drop .filter-sel rules now that the
  shared `select` styling covers them (row-sel kept for compact inline)
- style.css: .gap-2 uses var(--space-2) for token consistency

No behavior change; follow-up cleanup from the design-uplift review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:45:15 +09:00
152dc0bae4 fix(discover): cast nullable params so Postgres can infer type
The discover @Query used the `(:param is null or col >= :param)` idiom.
For the timestamp (publishedAfter) and numeric (minRatio) params, Postgres
threw "could not determine data type of parameter $1" because the bare
`$1 is null` placeholder is untyped — returning HTTP 500 and breaking the
whole /discover page. (The search() query survives because its nullable
params are only bigint/varchar, which Postgres can null-type.) Wrap the
two problematic params in cast(... as timestamp/big_decimal) in the
is-null check so the type is explicit. ORDER BY ... nulls last / fetch
first were never the problem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:56:28 +09:00
632a2d4f8a fix: scope form focus rule, robust .hidden, drop unused shadow tokens, funnel amber stage 2026-05-31 01:38:08 +09:00
04891a7838 feat(collection): apply initial filters from URL params (dashboard deep-link target) 2026-05-31 01:01:18 +09:00
8fe90e431f fix(dashboard): null-ratio badge color, escape categoryId, robust refresh selector, animate once 2026-05-31 00:59:20 +09:00
2d6c962567 feat(dashboard): count-up, 3-tone bars, rank/ratio badges, click-through deep links
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:53:25 +09:00
5ec04eaa60 style(css): refine shared components (buttons, badge, bar, forms, table, skeleton) 2026-05-31 00:48:14 +09:00