진행 중 · 2026년 5월 23일
TubeShadow
YouTube 영상에서 클립 따고 AI 분석 받고 SM-2 간격 반복으로 복습하는 영어 쉐도잉 도구. 개발자와 지식노동자용.
- 역할
- Solo
- 스택
- Java 21 · Spring Boot 3.3 · PostgreSQL · Next.js 16 · TypeScript · Tailwind 4 · Claude Haiku
YouTube 클립 마이너 + 쉐도잉 트레이너. YouTube URL 붙여넣으면 자막/메타데이터 자동 추출, 자막 더블클릭 또는 수동으로 시작/끝 마커 찍어서 자유롭게 클립 자르고, 태그/덱/SM-2 간격 반복이 있는 개인 라이브러리에 모음.
무엇을 하는가
- 임포트: YouTube URL →
yt-dlp로 자막+메타데이터 추출. Idempotent self-healing — 다시 임포트하면 일시적 실패 자동 복구. - 클립: 자막 더블클릭 또는 수동 시작/끝. 0.5~1.5x 속도로 무한반복.
- 라이브러리: 이름/자막 검색, 태그 필터, 정렬, JSON 내보내기.
- AI 분석: 클립당 Claude Haiku 한 번 호출 (맥락/문법/표현/어휘) → Postgres JSONB에 캐싱. 클립당 평생 LLM 호출 1회.
- 녹음 + A/B: MediaRecorder → 업로드 → 원본과 본인 녹음 교차 재생, 시간순 누적.
- 복습: SM-2 알고리즘 (Anki식 Again/Hard/Good/Easy), 연속 일수 위젯, 덱 필터.
- 큐레이션 콘텐츠: 신규 사용자가 즉시 시작할 수 있는 시작용 영상 컬렉션 (
curated-videos.yml) 포함. - 5개 언어: next-intl path-based 라우팅 (en 기본 + ko / ja / zh / es).
스택
백엔드: Java 21 · Spring Boot 3.3 · Gradle Kotlin DSL · PostgreSQL · Flyway · Spring Security + JWT. 프론트엔드: Next.js 16 (App Router) · TypeScript strict · Tailwind 4 · shadcn/ui · Zustand · TanStack Query. AI: Anthropic Claude Haiku 4.5 + prompt caching (Gemini 2.5 Flash도 free-tier용으로 pluggable).
아직 없는 것
- 배포 URL 없음 — 로컬
docker-compose up으로만 실행. - BYOAI (사용자가 본인 API 키로 분석): Claude/Gemini/Perplexity용 프롬프트를 사용자가 본인 채팅 도구에 붙여넣는 형태로 일부 구현.
- 모바일 앱 없음. 브라우저 only, 모바일 반응형까지.
프로젝트 로그
이 프로젝트를 만들면서 남긴 트러블슈팅 · 회고 · 업데이트의 시간순 기록.
AuthRateLimitFilter: Filter auto-registration trap → HandlerInterceptor
기술 회고2026년 5월 23일 · 1 분
A `Filter` bean was auto-registered by Spring Boot independent of the `SecurityFilterChain` wiring and tried to instantiate it with a default constructor it didn't have. Switched to `HandlerInterceptor`; same behavior, no auto-registration trap.
YouTube transcript fetch replaced with yt-dlp + self-healing import
트러블슈팅2026년 5월 23일 · 1 분
The `timedtext` URL embedded in YouTube watch HTML carries a short-lived token; subtitle fetch returned 200 with empty body within minutes. Migrated entirely to `yt-dlp` subprocess and added an idempotent recovery hook so re-importing the same URL retries the transcript and the dimension probe.
BYOAI — send analysis prompts to the user's own ChatGPT/Claude/Gemini
수익화2026년 5월 24일 · 1 분
Built a 'Send to my own AI' button that constructs an analysis prompt locally and opens ChatGPT/Claude/Gemini/Perplexity with `?q=...`, falling back to clipboard if auto-fill fails. Operating cost: zero; user keeps the conversation in their preferred model. Sets up a clean alternative to a paid tier.
StaleObjectStateException on clip delete cascading to recordings
트러블슈팅2026년 5월 24일 · 1 분
Spring Data's derived `deleteByClipId` hydrates the entity into the persistence context before deleting it, racing with the DB-level `ON DELETE CASCADE`. Switched to `@Modifying @Query` so the delete bypasses the session cache.
i18n: next-intl with path-based routing across 5 locales
기술 회고2026년 5월 25일 · 2 분
Migrated the entire frontend to `next-intl` with path-based routing (`/en`, `/ko`, `/ja`, `/zh`, `/es`), default English. All 17 user-facing files were keyed; `<Link>` and `useRouter` imports across the app moved to `@/i18n/navigation` wrappers. ja/zh/es fall back to English copy for now (translation is a non-engineering task).
Recording upload 415: MIME `Content-Type` whitelist must strip codec parameter
트러블슈팅2026년 5월 26일 · 1 분
Chrome's MediaRecorder tags audio with `audio/webm;codecs=opus`. The recording upload service compared the full string against a whitelist of base types, so Chrome uploads were rejected with 415. Stripped MIME parameters before the check.
AI provider abstraction — Claude credit ran out, swapped to Gemini in one env var
기술 회고2026년 5월 27일 · 1 분
Anthropic credit hit zero mid-session. Introduced `AiAnalysisClient` interface; `ClaudeClient` and a new `GeminiClient` both implement it, each gated by `@ConditionalOnProperty(name = "tubeshadow.ai.provider")`. Switching providers is now one env var. Gemini's free tier (1500 req/day) covers personal use indefinitely; operating cost dropped to $0/mo.
Decks (Anki-style clip grouping) + Review next-due toast
업데이트2026년 5월 27일 · 2 분
Two user-perception fixes shipped together. (1) Anki-style decks: a new `decks` table, nullable `deck_id` on `clips` (`ON DELETE SET NULL` so clips fall back to Inbox), library sidebar, per-card move dropdown, deck filter on review. (2) After Easy/Good/Hard/Again, a toast surfaces the actual next-due date so the clip doesn't feel like it 'vanished' from the queue.
Gemini 2.5 Flash: thinking tokens silently truncated JSON output
트러블슈팅2026년 5월 27일 · 1 분
After switching to Gemini, every analysis came back as `GEMINI_PARSE_FAILED: Unexpected end-of-input` at the same column. Cause: Gemini 2.5 Flash burns 'thinking' tokens before emitting visible output, eating most of `maxOutputTokens: 800` and truncating the JSON mid-string. Fix: `thinkingConfig: { thinkingBudget: 0 }` and bumped output cap to 4096.
Prompt-engineering 직독직해: forcing English word order in Korean output
사용성 회고2026년 5월 27일 · 3 분
Asking Gemini for 'Korean chunks paired with English chunks' produced grammatical Korean in natural Korean order — useless for shadowing practice. Three prompt revisions: (1) hard rules + bad examples to force source order, (2) tune chunk size from 1-word to 2–5-word sense groups, (3) explicit BAD examples that show the failure mode.