음성봇을 스텁부터 만든 이유 — 테스트로 설계를 먼저 굳히다

시리즈 · 음성봇 직접 만들기 — 실시간 음성봇의 뼈대를 바닥부터 만든 8편 기록입니다. 1. 끼어들기(barge-in) — 재생 중인 TTS를 실시간으로 끊는 법 2. "말이 끝났다"는 판단 — VAD 엔드포인터 3. 엔진을 코드 수정 없이 바꾸는 인터페이스 설계 4. 로컬 LLM 대화 두뇌와 도구 호출 루프 5. 매 턴 지연시간을 재는 이유 6. 브라우저 마이크에서 서버까지 — 오디오 프로토콜 7. 로컬 Whisper STT 연결하기 8. 스텁부터 만든 이유 — 테스트 우선 설계 (완결) — 지금 보는 글 돌아보며 시작하는 마지막 편 이 시리즈를 시작할 때부터 정해 둔 규칙이 하나 있었습니다. 진짜 음성 인식 모델과 진짜 합성 모델이 준비되기 전까지는, 그 둘을 흉내만 내는 가짜 엔진으로 전체 구조를 먼저 돌려본다는 규칙입니다. 여덟 편을 거치며 끼어들기 처리, 상태 흐름, 프로토콜, 엔진 인터페이스를 하나씩 다뤘는데, 그 모든 것이 사실 이 가짜 엔진 위에서 검증됐습니다. 지금 코드를 다시 열어 보면 그때는 별생각 없이 세운 규칙이 결과적으로 시리즈 전체의 진행 속도를 결정했다는 게 눈에 들어옵니다. 이번 편에서는 왜 굳이 가짜부터 만들었는지, 그리고 그 선택이 시리즈 전체에 어떤 영향을 남겼는지 정리하면서 이 시리즈를 마무리하려 합니다. 실제 모델을 기다리지 않기로 한 이유 음성봇의 핵심 로직은 사실 음성 인식이나 음성 합성 자체가 아닙니다. 사용자가 말을 시작했는지 끝냈는지 판단하는 상태머신, 합성 중에 끼어들기가 들어왔을 때 스트림을 멈추는 동시성 처리, 이벤트가 순서대로 오가는 흐름 제어 같은 것들이 훨씬 까다롭습니다. 반면 실제 인식·합성 모델은 무겁고 설치가 까다롭고 실행 속도도 느립니다. 이 두 가지를 한꺼번에 붙잡고 있으면 어려운 동시성 버그를 실제 모델의 로딩 시간과 뒤섞어 디버깅하게 됩니다. 모델이 느려서 반응이 늦은 건지, 상태머신이 잘못 짜여서 늦은 건지 구분이 안 되는 상황을 미...

유튜브 노션 자동 저장 봇 만들기 — Claude CLI MCP + Discord 233일 운영기


Claude CLI로 만든 1인 콘텐츠 아카이브 봇 — Discord URL 한 줄을 Notion에 자동 저장하는 흐름

Claude CLI로 만든 1인 콘텐츠 아카이브 봇 — Discord URL 한 줄을 Notion에 자동 저장 (사용자 본인 실제 화면)

discord 유튜브 봇 노션 자동 저장 파이프라인을 만든 진짜 이유는 영어 유튜브 영상의 디테일을 놓치는 페인 포인트 때문이었습니다. 좋은 영어 영상을 발견해도 그 자리에서는 흘려듣게 되고, "나중에 자막 켜고 디테일하게 다시 봐야지" 해도 알고리즘 휘발성 때문에 다시 찾기가 거의 불가능합니다. 어렵게 다시 찾아도 매번 자동 번역 자막을 켜고 영상 길이만큼 다시 보는 마찰이 큽니다. 결국 영어로 흘려들은 좋은 내용은 대부분 그대로 증발합니다. 이 봇은 그 마찰을 0으로 줄입니다. Discord 채널에 YouTube URL 한 줄만 올리면 자막 원문을 자동 추출하고, Claude가 한국어로 번역·정제해서 Notion DB에 영구 박제합니다. 30분 후에는 영상을 다시 재생하지 않아도 디테일이 본인 vault에 텍스트로 남아 있습니다.

본인은 이 봇을 233일간 운영하며 vault에 콘텐츠 559건을 박제했습니다(2025-10-12 시작, 평균 활동일 4.14건/일). 사람이 옮기는 단계는 Discord에 URL 한 줄 붙여넣는 것뿐입니다. Discord(사람 인박스) → Claude CLI MCP(자동 번역·정제) → Notion(영구 아카이브)의 3단 파이프라인을 본인이 처음 세울 때 밟았던 순서 그대로 아래에 풀어둡니다. macOS·Python·discord.py·Claude Code CLI·Notion API·yt-dlp·SQLite Queue 조합이고, 실제로 쓴 명령어와 구조도를 붙여 두었으니 본인 맥에서 같은 흐름을 그대로 재현할 수 있습니다.

233일 누적 Notion DB — 유튜브 영상이 카드 형태로 자동 적재된 영상 페이지 목록

233일 누적 Notion DB — 자동 적재된 영상 페이지 목록 (사용자 본인 실제 화면)


1. 왜 Discord + Claude CLI + Notion 조합인가

진짜 페인 포인트 — 영어 영상의 디테일을 다시 못 본다

좋은 영어 유튜브 영상은 1시간에 한 번씩 발견됩니다. Andrej Karpathy의 LLM 강의, Anthropic 엔지니어링 토크, Y Combinator 인터뷰, 인디 해커 콘퍼런스 발표 — 그 자리에서는 흐름만 잡고 흘려듣게 됩니다. "나중에 자막 켜고 디테일하게 다시 봐야지" 마음먹어도 며칠 뒤 알고리즘에 묻혀 다시 찾기가 거의 불가능하고, 어렵게 다시 찾아도 1시간짜리 영상을 자동 번역 자막으로 다시 보는 마찰이 큽니다. 영어 청해 속도와 한국어 사고 속도 사이의 갭이 디테일 손실의 진짜 원인입니다. 영어로 들으면서 동시에 깊이 사고하기가 쉽지 않고, 자막을 켜도 영상 진행 속도에 끌려갑니다.

이 봇은 그 갭을 텍스트로 옮겨 해결합니다. yt-dlp가 자막 원문을 추출하고, Claude가 본인 사고 속도에 맞는 한국어 텍스트로 정제합니다. 영상 1시간이 텍스트 5분 정독으로 압축됩니다. 디테일을 잃지 않고 vault에 영구 박제되어 검색·백링크·재방문이 자유롭습니다.

Discord는 사람 인박스로, Notion은 영구 아카이브로

RSS 리더나 Read-later 도구로 모든 영상을 처리하려는 시도는 결국 큐가 쌓이기만 하고 다시 보지 않는 결과로 끝납니다. Discord 채널을 인박스로 두면 모바일·데스크톱 어디서나 URL 한 줄 붙여넣기만으로 큐에 들어갑니다. 본인이 X·유튜브 알고리즘·뉴스레터에서 발견한 영상을 즉시 던지면 됩니다. Notion은 검색·백링크·태그·플러그인 생태계를 갖춘 영구 vault 역할을 합니다. 두 도구의 역할 분리가 명확해 장기 단위 운영이 가능합니다.

Claude CLI MCP가 영어 자막을 한국어로 번역·정제·1000줄 이내 압축

봇의 핵심 엔진은 Claude Code CLI의 headless 모드입니다. yt-dlp가 추출한 자막 원문을 Claude에게 던지고, 영어 영상은 한국어로 번역 + 한국어 영상은 정제·요약을 1000줄 이내(Notion 호환 한계) 결과로 받습니다. 시스템 프롬프트에 "도구 승인은 항상 YES, 묻지 말 것"·"한국어 오타 주의"·"rating 속성은 건드리지 않음"·"핵심 인사이트·실용 팁·따라할 수 있는 단계 위주로 정리" 같은 룰을 박아두면 매 영상마다 일관된 품질의 정제 결과를 받을 수 있습니다. 단순 자막 번역이 아니라 본인 관점에서 다시 보기 좋은 구조로 재배열하는 단계가 들어갑니다.

Queue 기반 분리 — Discord 봇·Claude CLI·Notion API 모두 비동기

Discord 봇 daemon은 URL을 받아 SQLite 큐에 행을 추가하는 역할만 합니다. 실제 처리는 별도 Queue Processor가 30분 주기로 큐를 비웁니다. 이 분리 덕분에 Discord 메시지가 폭주해도 봇이 멈추지 않고, Claude CLI 호출이 5~10분 걸려도 다음 메시지 수신이 막히지 않습니다. Notion API rate limit(초당 3건)에도 큐가 자연 throttling 역할을 합니다.


2. 사전 준비물 체크리스트

  • macOS Sequoia 15.x 이상 (Linux·Windows도 동일 흐름)
  • Python 3.11+ + 가상환경 (venv 또는 uv)
  • Discord 계정 + 본인 개인 서버 (회사 서버 X — 본업 분리)
  • Notion 계정 + Internal Integration 권한
  • Claude Code CLI 설치 (Anthropic API 키 또는 Pro 구독)
  • yt-dlp + ffmpeg (자막 추출용)
  • 디스크 여유 5GB 이상 (yt-dlp 캐시 + SQLite Queue)
  • 봇 24/7 실행 환경 (개인 맥 + sleepwatcher 또는 small VPS)

3. 단계별 구현

3.1 Discord Developer Portal에서 봇 생성

  1. 접속 → New Application
  2. 이름 입력 (예: YouTube Notion Archive Bot) → Create
  3. 좌측 Bot 탭 → Add Bot → Reset Token → 토큰 복사 → .env에 박제
  4. Privileged Gateway Intents → Message Content Intent 활성화 (필수)
  5. 좌측 OAuth2 → URL Generator → Scopes: bot 체크 → Permissions: Read Messages/View Channels, Send Messages, Read Message History 체크
  6. 생성된 초대 URL → 본인 Discord 서버에 봇 초대

봇 토큰은 절대 공개하지 말고 .env 파일에만 저장합니다. 노출되면 같은 화면의 Reset Token 버튼으로 즉시 무효화하고 재발급할 수 있습니다.

3.2 Notion Internal Integration + DB 스키마

  1. → + New integration → Internal · 개인 워크스페이스
  2. Capabilities: Read content · Insert content · Update content 체크
  3. Submit → Internal Integration Secret 복사 → .env에 박제
  4. Notion에서 새 DB 생성 (페이지 안 inline DB 권장). 처음엔 핵심 6~7개만 있으면 봇이 동작합니다 — 제목(Title) · URL · Channel · Date(게시일) · Tags(Multi-select) · Status. Ratings·Review·Files & media 같은 나머지는 운영하면서 필요할 때 추가하면 됩니다. 본인 환경은 233일간 운영하며 자연스럽게 13속성으로 늘었습니다. 전체 컬럼 구성은 아래 스크린샷을 그대로 따라 만들면 됩니다.
Notion DB 13속성 스키마 + Integration 연결 — 본인 233일 운영 환경 그대로

Notion DB 13속성 스키마 + Integration 연결 — 본인 233일 운영 환경 그대로

3.3 Python 봇 daemon + SQLite Queue


python3 -m venv ~/.youtube-notion-bot
source ~/.youtube-notion-bot/bin/activate
pip install discord.py yt-dlp notion-client python-dotenv

봇 코드의 핵심 골격(bot.py):


import discord
import sqlite3
import re
import os
from dotenv import load_dotenv

load_dotenv()
DB_PATH = os.path.expanduser("~/.youtube-notion-bot/queue.db")
YT_PATTERN = re.compile(r"https?://(www\.)?(youtube\.com|youtu\.be)/\S+")

intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)

def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS queue (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            url TEXT NOT NULL,
            channel TEXT,
            user TEXT,
            posted_at TEXT DEFAULT (datetime('now')),
            status TEXT DEFAULT 'pending'
        )
    """)
    conn.commit()
    conn.close()

@client.event
async def on_message(message):
    if message.author.bot:
        return
    for url in YT_PATTERN.findall(message.content):
        # findall은 group만 반환 — 전체 매칭은 search·finditer 권장
        pass
    matches = YT_PATTERN.finditer(message.content)
    for m in matches:
        url = m.group(0)
        conn = sqlite3.connect(DB_PATH)
        conn.execute("INSERT INTO queue (url, channel, user) VALUES (?, ?, ?)",
                     (url, str(message.channel), str(message.author)))
        conn.commit()
        conn.close()
        await message.add_reaction("✅")

init_db()
client.run(os.environ["DISCORD_BOT_TOKEN"])

봇은 큐에 넣기만 합니다. 실제 처리는 별도 프로세서가 담당하므로 Discord 메시지 처리는 0.1초 안에 끝납니다.

3.4 Queue Processor — Claude CLI MCP + Notion 저장

처리 스크립트(processor.py)의 흐름:

  1. SQLite에서 status='pending' 행 1개씩 fetch
  2. yt-dlp로 자막 다운로드 (한국어 자막 없으면 영문 fallback)
  3. Claude CLI headless 호출:

claude --print --model claude-sonnet-4-5 \
  "다음 유튜브 자막을 한국어로 정제하여 일목요연하게 정리. 영문이면 한글 번역.
   1000줄 이내. 핵심 인사이트·실용 팁·따라할 수 있는 단계 위주.
   본문 끝에 5문장 이내 요약." < transcript.txt > cleaned.md
  1. Notion API로 페이지 생성:

from notion_client import Client
notion = Client(auth=os.environ["NOTION_API_KEY"])
notion.pages.create(
    parent={"database_id": os.environ["NOTION_DB_ID"]},
    properties={
        "제목":     {"title":     [{"text": {"content": video_title}}]},
        "URL":      {"url":       video_url},
        "Channel":  {"rich_text": [{"text": {"content": channel}}]},
        "Duration": {"rich_text": [{"text": {"content": duration}}]},
        "Date":     {"date":      {"start": video_published_at}},
        "Tags":     {"multi_select": [{"name": t} for t in tags]},
        "Status":   {"status":    {"name": "Done"}},
        "Status 2": {"status":    {"name": "요약완료"}},
    },
    children=cleaned_blocks,  # 한국어 정제 결과는 본문 블록으로 저장 (rich_text 2000자 한계 회피)
)
  1. SQLite status='done' 갱신

30분 주기 cron 등록:


*/30 * * * * cd ~/youtube-notion-bot && /usr/bin/python3 processor.py >> ~/logs/yn_$(date +\%Y\%m).log 2>&1

3.5 실전 검증 — Discord 채널에 URL 던지기

  1. Discord 서버의 봇이 들어있는 채널에 YouTube URL 1개 메시지로 전송
  2. 봇이 ✅ reaction 즉시 추가 (큐에 들어갔다는 신호)
  3. 30분 내 다음 Queue Processor 실행 시점에 처리 시작
  4. Notion DB에 새 페이지 자동 생성 — 한국어 정제 결과 + 자막 원문 + 메타데이터

4. 자주 만나는 에러 3가지 (233일 운영 실측)

운영하면서 가장 자주 부딪힌 케이스 3개와 실제 해결 방법입니다. 처음 봇을 만들 때는 골든 패스만 보고 끝났다고 생각했지만, 1주일·1개월 단위로 운영하다 보면 거의 정해진 순서대로 같은 문제가 반복됩니다.

4.1 YouTube 자막 추출 실패 — 한국어 자막 없음 또는 자막 자체 disable

증상: yt-dlp 호출 결과가 빈 문자열이거나 WARNING: There are no subtitles for the requested languages만 찍힙니다. 큐는 status='pending'인데 자막 텍스트가 없어 Claude 호출 단계에서 멈춥니다. 영어권 강연·인터뷰 영상에서 특히 자주 발생하고, 일부 영상은 채널 정책으로 자막이 아예 꺼져 있습니다.

진단: yt-dlp --list-subs 로 사용 가능한 자막 언어를 먼저 확인합니다. 한국어가 없으면 영문 자막, 영문도 없으면 자동 생성 자막(--write-auto-sub) 순으로 fallback이 필요합니다. 자막이 완전히 disable된 영상은 추출 자체가 불가능하므로 별도 분기로 빠져야 합니다.

해결: processor.py에 자막 추출 우선순위를 박아둡니다. (1) 한국어 자막 → (2) 영문 자막 → (3) 자동 생성 자막 → (4) 추출 실패 시 status='no_subtitle'로 마킹하고 Notion에는 URL·제목만 저장. Claude 프롬프트는 입력 언어가 영어면 "한국어로 번역하면서 정제", 한국어면 "정제만" 분기하도록 짧은 시스템 메시지를 앞에 둡니다. 자동 생성 자막은 구두점·문장 단위가 깨져 있으므로 Claude에게 "타임스탬프·줄바꿈 무시하고 문맥 단위로 재구성" 룰을 함께 줘야 결과가 깔끔하게 나옵니다.


yt-dlp --write-sub --write-auto-sub --sub-langs "ko,en" \
  --skip-download --sub-format vtt -o "%(id)s.%(ext)s" <URL>

재발 방지: 자막 없음 영상은 큐에 남겨두지 말고 failed 상태로 분리합니다. 매주 1회 failed 큐를 훑어 수동으로 처리할지 폐기할지 결정하면 큐 길이가 무한히 늘어나지 않습니다.

4.2 macOS sleep/wake 후 봇 daemon 좀비화

증상: 맥북 뚜껑을 닫고 한참 뒤 다시 열면 봇 프로세스는 ps 결과에 살아있는데 Discord 채널에 URL을 올려도 큐에 행이 추가되지 않습니다. Discord 클라이언트 쪽에서는 봇 상태가 "온라인"으로 보이기도 해서 발견이 늦어집니다. 233일 운영 중 가장 자주 부딪힌 문제이고, 초기에는 며칠치 URL이 통째로 누락되는 일도 있었습니다.

진단: macOS의 sleep은 네트워크 소켓을 강제로 끊는데, discord.py의 heartbeat 타임아웃 처리가 wake 직후 곧장 복구되지 않습니다. 프로세스는 살아있지만 게이트웨이 연결이 끊긴 채로 on_message 이벤트가 발화되지 않는 "좀비" 상태가 됩니다. 로그를 보면 WebSocket closed with 1000 다음에 자동 재연결 시도가 무한 대기에 빠진 흔적이 남아 있습니다.

해결: sleepwatcher를 brew로 설치하고 ~/.wakeup 훅에 봇 재시작 명령을 박아둡니다. sleep에서 깰 때마다 봇 프로세스를 한 번 죽이고 다시 띄우는 게 가장 단순하고 안정적입니다. discord.py 내부의 자동 재연결을 신뢰하는 것보다 외부 트리거로 강제 재시작하는 쪽이 장기 운영으로 보면 누락이 훨씬 적습니다.


brew install sleepwatcher
brew services start sleepwatcher
cat > ~/.wakeup <<'EOF'
#!/bin/bash
pkill -f "python.*bot.py"
sleep 2
cd ~/.youtube-notion-bot && nohup python3 bot.py >> ~/logs/bot.log 2>&1 &
echo "[$(date)] bot restarted on wake" >> /tmp/wake-restart.log
EOF
chmod +x ~/.wakeup

재발 방지: /tmp/wake-restart.log를 주 1회 확인해 wake 훅이 실제로 발화하고 있는지 본인이 직접 모니터링합니다. sleepwatcher 자체가 죽으면 모든 게 무너지므로 brew services list로 상태를 가끔 확인하는 습관이 필요합니다.

4.3 Claude 정제 결과의 한국어 품질 저하 — 오타·1000줄 초과·rating 컬럼 침범

증상: Sonnet 3.5 시절에는 거의 없었는데, 자막 길이가 긴 영상(1시간+)을 처리하면 한국어 오타가 눈에 띄게 늘어나고 가끔 결과가 1000줄을 넘어 Notion API가 body.children should be ≤ 100 blocks 에러로 거부합니다. 더 골치 아픈 경우는 Claude가 "기존 Notion 페이지의 rating 속성을 갱신해 드릴까요?" 같은 메타 응답을 내거나 본인이 정의하지 않은 컬럼을 건드리려 하는 케이스입니다.

진단: 입력이 길어질수록 출력 압축률·한국어 일관성이 흔들리고, headless 모드에서도 모델이 가끔 "에이전트적" 행동을 시도합니다. Sonnet 4·4.5로 올라오면서 품질은 좋아졌지만 자유도가 함께 늘어나 가드레일이 더 필요해졌습니다.

해결: 시스템 프롬프트에 명시적 가드레일을 박는 것이 가장 효과가 큽니다. 본인이 운영 중인 프롬프트의 핵심 룰은 다음과 같습니다.


- 출력은 1000줄 이내. 초과 시 본인이 자동 분할 저장.
- 한국어 맞춤법·오타에 특별히 주의. 어색한 직역체 금지.
- rating·평점·태그 컬럼은 절대 수정·제안하지 말 것 (운영자 수동 영역).
- 도구 호출·메타 응답·"~해 드릴까요?" 같은 대화체 금지.
- 출력은 본문 텍스트만. 인사말·요약 표지·서명 모두 제외.

후처리 단계로 len(cleaned.splitlines()) > 1000이면 자동 2분할해서 페이지 2개로 저장하고, 두 페이지를 Notion 관계 속성으로 연결합니다. 한국어 오타는 모델 버전이 올라가면 자연스럽게 줄지만, 그 사이 기간을 견디려면 후처리 검수가 필요합니다.

재발 방지: 한 달에 한 번 무작위로 처리된 페이지 10개를 본인이 직접 읽어 품질을 체크합니다. 오타·구조 깨짐이 일정 비율 이상이면 프롬프트를 업데이트하거나 모델을 갱신합니다. 모델 변경은 한 달 단위로 보수적으로 진행하고, 변경 직후 1주일은 출력을 평소보다 더 자주 들여다봅니다.


실측 운영 통계 — 233일 누적

누적 559건 · 운영 233일 · 활동일 평균 4.14건

지표 수치
누적 적재 건수 559건
운영 기간 233일
전체일 평균 2.40건/일
활동일 평균 4.14건/일
활동률 58% (135일 / 233일)
월간 피크 2026-02 · 168건
일간 피크 2026-02-22 · 78건
유료 비용 0원 (Claude Code Pro $20/월 한도 내)

핵심은 단순 합계보다 활동 패턴에서 드러납니다.

  • 활동률 58% — 233일 중 135일에 1건 이상 적재 (휴면일 98일)
  • 활동일 평균 4.14건/일 — 실제로 vault에 던지는 날은 4건씩 누적
  • 월간 피크 168건 — 한 달간 콘텐츠 백필이 폭주한 시기에도 봇은 멈추지 않음
  • 일간 피크 78건 — 일괄 백필이 들어와도 큐가 자연 throttling 역할

영상 발견·박제까지 마찰이 0에 가까운 상태가 유지되면 활동일 평균은 자연스럽게 4건/일에 수렴하고, 매일 던지지 못해도 운영이 끊기지 않는 구조가 됩니다. 휴면일 98일에도 봇은 살아있고 큐는 비어있는 상태로 대기합니다 — 사람의 의지가 아니라 인프라가 운영을 지속시킨다는 점이 본인이 233일 동안 멈추지 않은 진짜 이유입니다.


마치며

233일을 돌려보고 나서 정작 크게 남은 건 비용 절감이나 자동화 그 자체가 아니었습니다. 영어 영상에서 발견한 인사이트를 흘려보내지 않게 됐다는 것이 제일 컸습니다. 영상 1시간을 텍스트 5분으로 압축한 한국어 정제 결과가 vault에 박제되어 있어, "어디서 본 적 있는데" 하는 순간에 검색 한 번으로 디테일까지 복구됩니다. 매일 던지는 한 줄 URL이 6개월 뒤 검색 한 번에 답을 주는 자산이 됩니다.

운영자 233일 운영 노트 (실측 1차 정보): 누적 559건 적재 (상단 통계 참고). Claude CLI 모델이 Sonnet 3.5 → 4 → 4.5로 바뀌면서 한국어 정제 품질이 단계적으로 좋아졌습니다. 가장 자주 부딪힌 문제는 macOS sleep 후 daemon 좀비화 — sleepwatcher + wake 훅으로 해결한 후 누락이 거의 사라졌습니다.

이 글의 봇은 Discord 콘텐츠 아카이브 시리즈의 1편입니다. Instagram URL·뉴스 URL·일정 캡처 같은 변형 봇은 후속 편에서 다룰 예정입니다. 본문 중 명령어가 동작하지 않으면 Discord.py 공식 문서와 Notion API 문서, Claude Code CLI 문서를 함께 확인하기를 권합니다.

댓글

이 블로그의 인기 게시물

디스코드 구글캘린더 연동 봇 만들기: 카톡 캡처 OCR 자동 등록 시스템 구축

클로드 코드 비용 줄이는 법 — ollama 로컬 LLM 연동 (M3 맥북)