feat(V2-4차): Database 제네릭 복원 + /chat + /knowledge + Resend + Push + 문서 생성

묶음 K — Database 제네릭 완전 주입:
- @supabase/ssr 0.5→0.10, @supabase/supabase-js 2.45→2.103 bump
- 버전 정합으로 @supabase/ssr의 Database 제약이 정상 동작
- packages/api-client/src/types.ts:
  - TypedTable<Row, Insert, Update>로 Row & Record<string, unknown> 교차 유지
  - 각 테이블 Insert를 Partial<Row> & Pick<필수필드>로 교체
    (optional 컬럼이 required로 추론되던 문제 해결)
  - push_tokens / team_invites 테이블 타입도 inline 추가
- index.ts: types + client + auth + meetings + history + usage 전체 re-export 복원
- apps/web supabase-browser/server에 <Database> 제네릭 주입

묶음 L — /chat 페이지 (Voice Conversation 이식):
- apps/web/src/components/chat/chat-panel.tsx (client)
  - 메시지 state, user/assistant 말풍선, MetalCard 래핑
  - llm-proxy POST (non-streaming JSON), Anthropic 응답 형식 파싱
  - Enter 전송, Shift+Enter 줄바꿈, 초기화 버튼
- apps/web/src/app/(app)/chat/page.tsx
- Sidebar에 Chat 메뉴 추가 (ChatIcon)
- ko.json nav.chat 키

묶음 M — /knowledge 페이지 (RAG 이식):
- migrations/20260410000002_knowledge_documents.sql
  - knowledge_documents + knowledge_chunks 테이블
  - tsvector 자동 생성 컬럼 + GIN 인덱스 (전문 검색)
  - RLS: 개인/팀 분기 (team_id NULL 가능)
  - pgvector는 주석 처리 (V2-M+1에서 활성화)
- apps/web/src/app/(app)/knowledge/page.tsx — 카드 리스트 + 상태 배지
- components/knowledge/add-knowledge-form.tsx — 제목/타입/본문 입력,
  800자 단위 청킹 후 documents+chunks INSERT
- api-client types.ts에 KnowledgeDocument/Chunk 추가 + Database 등록
- Sidebar Knowledge 메뉴 (LibraryBooksIcon) + ko.json 키

묶음 N — team-invite Resend 이메일:
- functions/team-invite/index.ts에 Resend API 호출 로직 추가
- RESEND_API_KEY 설정 시 HTML 이메일 발송 (팀 이름/초대자/버튼/만료)
- 응답에 email_sent / email_error 포함
- API 키 없으면 기존대로 URL만 반환

묶음 O — Expo Push notification:
- migrations/20260410000003_push_tokens.sql (user_id, token unique, platform)
- functions/send-push/index.ts: 호출자 인증 + 본인/팀 멤버 권한 체크,
  대상 사용자 push_tokens 조회, Expo Push API 배치 호출
- config.toml에 send-push 함수 등록
- apps/mobile/package.json에 expo-device + expo-notifications 추가
- apps/mobile/lib/push.ts: registerPushToken (권한 요청, projectId,
  Android 채널, Supabase upsert)
- auth-context.tsx에서 로그인 직후 자동 등록
- app.json plugins에 expo-notifications

묶음 P — meetings/[id] 문서 생성:
- apps/web/components/meetings/generate-document-button.tsx (client)
  - 4개 템플릿 메뉴 (minutes/report/idea-note/mindmap)
  - 각 템플릿별 systemPrompt 지정
  - llm-proxy 호출 → meeting_documents INSERT
  - latency 측정, prompt_used 기록
- meetings/[id]/page.tsx DOCUMENTS 섹션에 버튼 노출
  (transcript는 edited_transcript ?? raw_transcript 우선)

검증:
- desktop typecheck + build OK
- web typecheck + build OK (14 라우트: 기존 12 + chat + knowledge)
- api-client test 19 passed

통계:
- 총 Edge Functions 10개: stt/llm-proxy, stripe-checkout/portal/webhook,
  team-invite/accept, send-push
- 총 SQL 마이그레이션 7개
- 웹 라우트 14개, 모바일 화면 5개
- 테스트 19개 passed
This commit is contained in:
yunchan8804 2026-04-10 08:51:53 +09:00
parent c167737198
commit 1fa24ce3c9
24 changed files with 1284 additions and 79 deletions

View file

@ -1,10 +1,8 @@
// packages/api-client — barrel export
// 루트는 타입만. 함수는 subpath로 접근 — 현재 Supabase SSR과 supabase-js 버전
// 정합 이슈로 Database 제네릭이 @supabase/ssr에서 전파 실패하기 때문에,
// 내부에서 SupabaseClient<Database>를 쓰는 meetings/history/usage 함수들을
// 루트로 끌어올리면 consumer의 tsc가 이 함수들까지 scan하여 never 전파가 일어남.
// Database 타입 + 모든 도메인 함수 재수출.
// @supabase/ssr 0.10 + supabase-js 2.103 정합으로 제네릭이 consumer에 전파됨.
//
// Sub-path로만 접근:
// Sub-path도 지원:
// '@d3ro/api-client/client' — Supabase 클라이언트 팩토리
// '@d3ro/api-client/auth' — 로그인/세션
// '@d3ro/api-client/meetings' — 회의 CRUD
@ -12,3 +10,8 @@
// '@d3ro/api-client/usage' — 쿼터/구독
export * from './types'
export * from './client'
export * from './auth'
export * from './meetings'
export * from './history'
export * from './usage'

View file

@ -158,6 +158,32 @@ export type Subscription = {
updated_at: string
}
export type KnowledgeFileType = 'txt' | 'md' | 'pdf' | 'docx' | 'html' | 'url'
export type KnowledgeDocument = {
id: string
user_id: string
team_id: string | null
title: string
file_name: string | null
file_type: KnowledgeFileType | null
source_url: string | null
storage_key: string | null
chunk_count: number
indexed: boolean
indexed_at: string | null
created_at: string
updated_at: string
}
export type KnowledgeChunk = {
id: string
document_id: string
chunk_index: number
content: string
created_at: string
}
/**
* 유틸: Supabase GenericTable (Row: Record<string, unknown>)
* Row/Insert/Update에 index signature를 .
@ -182,12 +208,12 @@ export type Database = {
profiles: TypedTable<Profile, Partial<Profile> & Pick<Profile, 'id'>, Partial<Profile>>
teams: TypedTable<
Team,
Omit<Team, 'id' | 'created_at' | 'updated_at'> & Partial<Pick<Team, 'id'>>,
Partial<Team> & Pick<Team, 'name' | 'owner_id'>,
Partial<Team>
>
team_members: TypedTable<
TeamMember,
Omit<TeamMember, 'joined_at'> & Partial<Pick<TeamMember, 'joined_at'>>,
Partial<TeamMember> & Pick<TeamMember, 'team_id' | 'user_id'>,
Partial<TeamMember>
>
meetings: TypedTable<
@ -197,19 +223,19 @@ export type Database = {
>
meeting_memos: TypedTable<
MeetingMemo,
Omit<MeetingMemo, 'id' | 'created_at'> & Partial<Pick<MeetingMemo, 'id' | 'created_at'>>,
Partial<MeetingMemo> &
Pick<MeetingMemo, 'meeting_id' | 'user_id' | 'content' | 'timestamp_ms'>,
Partial<MeetingMemo>
>
meeting_documents: TypedTable<
MeetingDocument,
Omit<MeetingDocument, 'id' | 'created_at' | 'updated_at'> &
Partial<Pick<MeetingDocument, 'id' | 'created_at' | 'updated_at'>>,
Partial<MeetingDocument> &
Pick<MeetingDocument, 'meeting_id' | 'user_id' | 'template_type' | 'title' | 'content'>,
Partial<MeetingDocument>
>
transcripts: TypedTable<
Transcript,
Omit<Transcript, 'id' | 'created_at' | 'updated_at'> &
Partial<Pick<Transcript, 'id' | 'created_at' | 'updated_at'>>,
Partial<Transcript> & Pick<Transcript, 'meeting_id' | 'segment_index' | 'timestamp_ms' | 'text'>,
Partial<Transcript>
>
history: TypedTable<
@ -228,6 +254,57 @@ export type Database = {
Partial<Subscription> & Pick<Subscription, 'user_id'>,
Partial<Subscription>
>
knowledge_documents: TypedTable<
KnowledgeDocument,
Partial<KnowledgeDocument> & Pick<KnowledgeDocument, 'user_id' | 'title'>,
Partial<KnowledgeDocument>
>
knowledge_chunks: TypedTable<
KnowledgeChunk,
Partial<KnowledgeChunk> &
Pick<KnowledgeChunk, 'document_id' | 'chunk_index' | 'content'>,
Partial<KnowledgeChunk>
>
push_tokens: TypedTable<
{
id: string
user_id: string
token: string
platform: 'ios' | 'android' | 'web'
device_name: string | null
created_at: string
updated_at: string
},
{
user_id: string
token: string
platform: 'ios' | 'android' | 'web'
device_name?: string | null
},
Partial<{ device_name: string | null }>
>
team_invites: TypedTable<
{
id: string
team_id: string
invited_by: string
email: string
role: 'admin' | 'member'
token: string
accepted_at: string | null
accepted_by: string | null
expires_at: string
created_at: string
},
{
team_id: string
invited_by: string
email: string
role?: 'admin' | 'member'
token: string
},
Partial<{ accepted_at: string | null; accepted_by: string | null }>
>
}
Views: Record<string, never>
Functions: Record<string, never>