회의 음성 → STT → 화자 분리 → LLM 요약·결정·액션 추출 → (발행 시) Notion 연동까지 이어지는 풀스택 모노레포입니다.
| 영역 | 경로 | 스택 |
|---|---|---|
| 백엔드 / 파이프라인 | 레포 루트 (src/, scripts/) |
Python 3.11 (Modal 이미지 고정), uv, Modal 서버리스, Supabase (service_role) |
| 웹 앱 | actnote-web/ |
Next.js 14+ (App Router), TypeScript, Tailwind, Supabase JS, shadcn/ui |
프론트 ↔ 백엔드 통합은 docs/frontend-handoff.md 한 장을 기준으로 합니다.
- 핵심 차별점
- 아키텍처 한눈에
- 사전 준비
- 셋업 — 백엔드
- 셋업 — 프론트 (actnote-web)
- 실행
- 문서 인덱스
- 폴더 구조
- 비용 가드레일
- 메인 1단계 완료 기능 요약
- Next.js 서버 라우트
- A.U.D.N 사이클 — 새 액션을 기존과 비교해 ADD / UPDATE / DELETE / NOOP 자동 분류
- Bi-temporal —
decisions,action_items의valid_until/superseded_by로 변경 이력 추적 - CRAG (Corrective RAG) — 이전 회의 컨텍스트 자동 주입
- Draft → Ready → Published 거버넌스 (PUB-001) + Notion DB 연동 (INTEG-001/003/005)
- 회의유형별 결과 분기 (0.5v) — standup / project_review / one_on_one / other 4종, 유형별 필수·선택 섹션 (DRAFT-008-002)
- Notion 연동 = Internal Integration Token 방식 — 사용자가
ntn_토큰을 붙여넣고 회의록·액션 DB URL을 등록 (온보딩 5단계 / 설정에서 변경)
[Next.js actnote-web]
│ 업로드 → Storage → meetings INSERT → /api/trigger-pipeline (supabase.auth 인증 경계)
│ 워크스페이스 초대: create_invite (RPC) → /api/workspace/send-invite → SMTP/Resend 직접
▼ fetch(Modal 웹 엔드포인트, X-Actnote-Secret)
[Modal actnote-pipeline] ── 시크릿 검증 → run_pipeline_fn.spawn() → 즉시 202
▼
[Modal CPU 함수 (src/jobs.py)]
├─ STT (Whisper)
├─ Diarization → cross-app [Modal GPU actnote-diarization] (signed URL)
├─ Alignment
├─ CRAG context 검색
├─ LLM Extraction (Claude, 회의 유형별 prompt)
├─ A.U.D.N (action_items)
├─ Embedding 인덱싱
├─ 담당자·화자 매칭 (DRAFT-005 / DRAFT-010)
└─ 인앱 알림 + 메일 (NOTI-001, Resend 직접)
│
▼
[Supabase] ── RLS · RPC ── [브라우저 클라이언트] (상태는 meetings.status 5초 폴링)
│
▼
[발행] publish_meeting RPC → /api/trigger-publish → Modal run_publish_fn → Notion · 임베딩
[Modal cron] cleanup_orphans_fn (6h) — workspace_id NULL 회의 정리
세부 사항: docs/events.md, docs/rpc.md.
| 서비스 | 용도 | 비고 |
|---|---|---|
| OpenAI | Whisper STT + 임베딩 | https://platform.openai.com/api-keys |
| Anthropic | Claude | https://console.anthropic.com/ |
| HuggingFace | pyannote | 토큰 + 모델 라이선스 |
| Supabase | DB / Auth / Storage | service_role 는 서버·Modal 만 |
| Modal | 서버리스 파이프라인 실행 (CPU) + GPU 화자분리 | modal deploy, Secret actnote-secrets |
| 변수 | 용도 |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase 프로젝트 URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
anon 키 (RLS). service_role 금지 |
NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET |
Storage 버킷명 (워커 SUPABASE_STORAGE_BUCKET 과 동일 권장) |
NEXT_PUBLIC_APP_URL |
OAuth 리다이렉트, 초대 링크, 메일 내 링크의 절대 URL origin |
NEXT_PUBLIC_SUPPORT_EMAIL |
분석 실패 등 사용자 안내용 (기획 확정 주소) |
NEXT_PUBLIC_NOTION_TEMPLATE_MEETING_URL |
INTEG-006-001 — "회의록 템플릿 복제" 버튼 대상 (Notion 공개 복제 URL) |
NEXT_PUBLIC_NOTION_TEMPLATE_TICKET_URL |
INTEG-006-002 — "액션 트래커 템플릿 복제" 버튼 대상 |
Modal 트리거 변수(
MODAL_PIPELINE_TRIGGER_URL·MODAL_PUBLISH_TRIGGER_URL·MODAL_TRIGGER_SECRET)는 브라우저 노출 금지 — 아래 서버 전용 표 참고.
워커와 별도로, 브라우저에서 호출하는 Next API 가 메일을 직접 보낼 때 Vercel 등에 아래가 필요합니다.
| 변수 | 용도 |
|---|---|
RESEND_API_KEY |
워크스페이스 초대 메일 등 (/api/workspace/send-invite) |
EMAIL_FROM |
Resend from 필드. ASCII만 (표시 이름에 한글·전각 문자 금지). 검증된 도메인 주소 권장 |
MODAL_PIPELINE_TRIGGER_URL |
/api/trigger-pipeline 가 호출할 Modal 엔드포인트 (modal deploy 출력) |
MODAL_PUBLISH_TRIGGER_URL |
/api/trigger-publish 가 호출할 Modal 엔드포인트 (modal deploy 출력) |
MODAL_TRIGGER_SECRET |
Modal 엔드포인트 인증 헤더(X-Actnote-Secret). Modal Secret 의 동일 키와 같은 값 |
Resend 운영 참고
- 도메인 미검증(테스트 계정) 상태에서는 수신 주소가 Resend 가입 메일 등으로 제한되는 경우가 많습니다. 이 경우에도 초대 레코드와 개인 초대 링크는 생성되며, 설정 화면에서 링크를 복사해 공유할 수 있습니다.
- 임의 수신자에게 메일까지 보내려면 Resend Domains 에서 발송 도메인을 검증하고,
EMAIL_FROM을 그 도메인 주소로 맞춘 뒤 재배포하세요.
- 값에는 스킴만 포함된 URL 한 덩어리만 두는 것을 권장합니다 (예:
https://app.example.com). - 배포 플랫폼에서 같은 줄에
# 주석을 붙이면, 값 전체가 깨져 초대 링크가 이상해질 수 있습니다. 주석은 반드시 다음 줄에 작성하세요. - 서버 코드에서는
actnote-web/lib/server/public-app-url.ts의sanitizePublicAppOrigin로 공백+#이후를 잘라 복구하지만, 환경 변수는 깨끗하게 유지하는 것이 안전합니다.
전체 카탈로그: .env.example · 웹 전용 요약: actnote-web/.env.example · 로컬은 actnote-web/.env.local 권장.
# 1) uv — https://docs.astral.sh/uv/getting-started/installation/
# 2) Python 의존성
uv sync
# 3) 환경변수 (레포 루트)
cp .env.example .env # PowerShell: Copy-Item .env.example .env
# 4) ACTNOTE_ENCRYPTION_KEY (Fernet)
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# 5) Supabase 마이그레이션
# SQL Editor 에서 migrations/*.sql 을 팀이 정한 순서로 실행합니다.
# 파일명에 동일 번호(예: 014_*) 가 두 개 있을 수 있으므로, 순서는 docs/frontend-handoff.md 및 운영 DB 기준을 따르세요.
# 현재 레포에는 001 … 059 등이 포함되어 있습니다 (목록은 migrations/ 디렉터리 참고).
# 0.5v: 050~055 (유형별 섹션·발행검증·정규화·backfill·last_error), 059 (join_request ambiguous 재수정).Storage: meetings 버킷(private) 생성.
cd actnote-web
npm install
cp .env.example .env.local # 값 채우기
npm run dev
# → http://localhost:3000로컬에서 파이프라인까지 보려면 Modal 을 배포(§6.1)하고 actnote-web/.env.local 에 MODAL_*_TRIGGER_URL + MODAL_TRIGGER_SECRET 을 채웁니다. 또는 Modal 없이 파이프라인만 단독 검증하려면 uv run python scripts/run_pipeline.py <audio>.
동작 요약 (웹)
- 로그인/회원가입 후
/workspace/select에서 소속 워크스페이스 수에 따라 홈으로 보내거나 선택 UI 표시 - 현재 워크스페이스는 브라우저
localStorage(actnote_current_workspace_id)에 저장 (비밀값 아님) - 대시보드(
(dashboard))는WorkspaceProvider로 활성 워크스페이스를 공유
워크스페이스 초대 (SEC-006, 요약)
- 관리자가
create_inviteRPC 로 초대 행 생성 (workspace_invites, 이메일·역할·토큰). - 클라이언트가
POST /api/workspace/send-invite로 메일 발송을 요청합니다. RESEND_API_KEY(또는 SMTP) 가 있으면 Next 서버가 직접 발송합니다. 둘 다 없으면 메일 없이 초대 링크만 반환합니다(Inngest 워커 폴백 제거됨 — 링크 수동 공유).- 수락 URL 형식은
/invite/<token>입니다. 초대 토큰은 DB에서 hex 문자열로 발급되며,/invite/[slug]페이지는 토큰으로workspace_invites조회를 먼저 시도한 뒤, 없으면 워크스페이스 slug 로 열린 초대를 처리합니다. - 메일 발송이 제한되어도 초대는 유효합니다. 설정 UI에서 개인 초대 링크를 복사해 전달할 수 있습니다.
배포 (Vercel 등)
NEXT_PUBLIC_*,MODAL_PIPELINE_TRIGGER_URL/MODAL_PUBLISH_TRIGGER_URL/MODAL_TRIGGER_SECRET, 초대 메일용RESEND_API_KEY/EMAIL_FROM을 프로젝트 환경 변수에 넣은 뒤 재배포해야 런타임에 반영됩니다.
# Modal 대시보드 Secret "actnote-secrets" 에 백엔드 env 전체 등록 후:
modal deploy src/modal_diarization.py # GPU 화자분리 (선행)
modal deploy src/modal_app.py # 파이프라인 + 웹 엔드포인트 + cron
# 출력된 trigger_pipeline / trigger_publish URL 2개 →
# actnote-web 의 MODAL_PIPELINE_TRIGGER_URL / MODAL_PUBLISH_TRIGGER_URLModal 함수: run_pipeline_fn, run_publish_fn, cleanup_orphans_fn(cron 6h),
웹 엔드포인트 trigger_pipeline/trigger_publish. 두 이미지 모두 Python 3.11 고정
(3.13 은 stdlib audioop 제거로 pydub import 실패).
Inngest·
serve_worker.py·로컬 워커는 제거됨. Modal 없이 파이프라인만 단독 검증:uv run python scripts/run_pipeline.py <audio_path>.
cd actnote-web && npm run devuv run python src/llm_extractor.py
uv run python src/assignee_matcher.py
uv run python src/speaker_matcher.py
uv run python src/email_notifier.py
uv run python -m src.notificationsuv run python scripts/benchmark_crag.pyNotion 발행 후 담당자·참석자·마감일이 비어 있으면, 어디서 끊겼는지 한 번에 확인합니다.
Modal 안에서 실행 — 로컬에 ACTNOTE_ENCRYPTION_KEY(운영과 동일 값)가 없어도 됩니다.
modal run scripts/diagnose_modal.py --meeting-id <MEETING_ID>출력 해석:
[2] 이메일 매칭 가능 멤버 N명— people 컬럼(Assignee/Participants) 매칭 가능 여부. 0명이면 Notion 통합의 "이메일 포함 사용자 정보 읽기" 권한 또는 멤버십 문제.[3a]/[3b] DB 컬럼—{}면 컬럼 조회 실패(아래 caveat 참고). 정상이면 컬럼명→타입과 resolver 매칭 결과 표시.
로컬 키가 있으면 uv run python scripts/diagnose_notion_publish.py <MEETING_ID> 로도 동일 진단(+ action_items 데이터 유무)을 볼 수 있습니다.
Notion-Version 고정 caveat: 최신
notion-client는 신규 Notion-Version(2025-09-03, "data source" 모델)을 기본값으로 써서databases.retrieve가properties대신data_sources만 반환합니다. 그러면 컬럼 매칭이 전부 실패해 Assignee/Participants/Due Date 가 누락됩니다. 이를 막기 위해src/notion_sync.py의_client()가 Notion-Version 을2022-06-28로 고정합니다(프론트verify-db와 동일). 추후 data source API 정식 마이그레이션은 CLAUDE.md 백로그 참고.src/notion_sync.py변경 후에는modal deploy src/modal_app.py재배포 필요.
| 문서 | 내용 |
|---|---|
| docs/frontend-handoff.md | 프론트 통합 1장 요약 |
| docs/events.md | Modal 트리거 계약 (구 Inngest) |
| docs/rpc.md | Supabase RPC |
| docs/notion-oauth.md | Notion OAuth |
| docs/features.md | 기능 ID 카탈로그 |
| docs/local-qa-guidebook.md | 로컬 QA 체크리스트 |
| CLAUDE.md | 프로젝트 컨텍스트 · 백로그 · 메인 2 진행 상황 |
.cursor/rules/*.mdc |
코딩 룰 |
./ # 백엔드 루트
├── src/ # 파이프라인 · 워커 · 알림 · Notion 등
├── src/modal_app.py · jobs.py # Modal 앱 · 프레임워크 비의존 작업
├── scripts/ # run_pipeline, benchmark, diagnose_modal/notion (발행 진단), CLI
├── prompts/templates/ # 회의 유형별 MD 템플릿
├── migrations/ # Supabase SQL (팀 정한 순서 실행)
├── docs/
├── pyproject.toml # uv 단일 의존성
└── .env.example
actnote-web/ # Next.js 앱
├── app/
│ ├── (auth)/ # login, signup
│ ├── (dashboard)/ # meetings, settings (WorkspaceProvider 하위)
│ ├── workspace/select/ # 다중 워크스페이스 선택
│ ├── onboarding/
│ ├── invite/[slug]/ # 토큰 초대 또는 slug 오픈 초대
│ └── api/ # §11 참고
├── components/
├── lib/
│ ├── supabase/ # browser / server 클라이언트
│ └── server/
│ ├── public-app-url.ts # NEXT_PUBLIC_APP_URL 정규화
│ ├── invite-email.ts # 초대 메일 본문 · Resend 헬퍼
│ └── …
└── package.json
| 변수 | 기본값 | 설명 |
|---|---|---|
MAX_COST_PER_MEETING_USD |
1.0 |
회의 1건 예상 비용 경고 |
MAX_TOTAL_COST_USD |
10.0 |
누적 초과 시 중단 |
COST_GUARDRAIL_AUTO_APPROVE |
false |
CI 등에서만 true 권장 |
단가·정책: .cursor/rules/api-cost-guard.mdc.
| 기능 ID | 산출물 (요약) |
|---|---|
| MTG-002 / MTG-004 | 회의 메타 · 유형별 prompt |
| DRAFT-005 / DRAFT-010 | 담당자·화자 매칭 |
| PUB-001 | 발행 RPC + 워크플로 |
| INTEG-001~005 | Notion 동기화 · OAuth |
| NOTI-001 | 인앱 알림 + 메일 (notifications.py, Resend 직접) |
| SEC-006 / WS-004 | 초대 RPC · 멤버 역할 · 강퇴 등 |
| 재분석 멱등성 | pipeline.py _cleanup_for_reanalysis() |
DB 확장 예시 (운영 적용 여부는 마이그레이션 실행 기준과 동기화)
- 사용자별 분석 완료/실패 이메일 수신 설정:
migrations/022_user_notification_email_prefs.sql
현재 단계: 백엔드 메인 1 완료 후 메인 2 (프론트 통합·운영 폴리싱) 진행 중이라면 상세 백로그는 CLAUDE.md 를 참고하세요.
| 경로 | 역할 |
|---|---|
POST /api/trigger-pipeline |
인증 후 Modal trigger_pipeline 엔드포인트 호출 (X-Actnote-Secret) |
POST /api/trigger-publish |
인증 후 Modal trigger_publish 엔드포인트 호출 |
POST /api/workspace/send-invite |
초대 메일 발송 (SMTP/Resend 직접, 폴백 없음) |
POST /api/integrations/notion/verify-token |
Internal Integration Token(ntn_) 유효성 검증 (Notion /users/me) |
POST /api/integrations/notion/verify-db |
Notion DB URL → DB ID 추출 + 컬럼 목록 조회 (자동매핑용) |
POST /api/integrations/notion/save |
토큰 Fernet 암호화 + 회의록·액션 DB ID 저장 (온보딩 최초 연동) |
POST /api/integrations/notion/update-db |
연동된 DB ID 변경 (설정 Change 버튼) |
GET /api/integrations/notion/start · /callback |
(구) Notion OAuth — 현 온보딩/설정 플로우는 Internal Token 사용. 유지만 됨 |
POST /api/onboarding/workspace |
온보딩 워크스페이스 생성 등 |
0.5v Notion 연동 화면 플로우: 온보딩
workspace 이름 → /onboarding/notion(안내) → /setup(토큰 가이드) → /apikey(검증) → /db(회의록 DB) → /actiondb(액션 DB) → 멤버초대 → /complete. 설정에서 변경 시/settings/integrations/meeting-db·/action-db(또는?from=settings로 동일 화면 재사용).
팀 내부 프로젝트 정책에 따릅니다. 마이그레이션 번호·실행 순서는 운영 DB에 적용된 상태와 반드시 맞추세요.