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

@ -27,7 +27,18 @@
"backgroundColor": "#19191b"
}
},
"plugins": ["expo-router", "expo-secure-store", "expo-av"],
"plugins": [
"expo-router",
"expo-secure-store",
"expo-av",
[
"expo-notifications",
{
"icon": "./assets/icon.png",
"color": "#f25b29"
}
]
],
"experiments": {
"typedRoutes": true
}

View file

@ -1,9 +1,10 @@
// apps/mobile/lib/auth-context.tsx
// Supabase 세션 Context
// Supabase 세션 Context + 로그인 시 Expo Push 토큰 자동 등록
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import type { Session, User } from '@supabase/supabase-js'
import { supabase } from './supabase'
import { registerPushToken } from './push'
interface AuthContextValue {
session: Session | null
@ -26,6 +27,9 @@ export function AuthProvider({ children }: { children: ReactNode }): React.React
.getSession()
.then(({ data }) => {
setSession(data.session)
if (data.session?.user.id) {
void registerPushToken(data.session.user.id)
}
})
.finally(() => setLoading(false))
@ -33,6 +37,9 @@ export function AuthProvider({ children }: { children: ReactNode }): React.React
data: { subscription }
} = supabase.auth.onAuthStateChange((_event, newSession) => {
setSession(newSession)
if (newSession?.user.id) {
void registerPushToken(newSession.user.id)
}
})
return () => {

77
apps/mobile/lib/push.ts Normal file
View file

@ -0,0 +1,77 @@
// apps/mobile/lib/push.ts
// Expo Push token 등록 — 앱 진입 시 호출하면 Supabase push_tokens 테이블에 upsert.
import * as Notifications from 'expo-notifications'
import * as Device from 'expo-device'
import { Platform } from 'react-native'
import Constants from 'expo-constants'
import { supabase } from './supabase'
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
shouldShowBanner: true,
shouldShowList: true
})
})
/**
* Expo push token을 Supabase push_tokens upsert.
* / token이 return.
*/
export async function registerPushToken(userId: string): Promise<void> {
if (!Device.isDevice) {
return
}
try {
// 권한 요청
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
}
if (finalStatus !== 'granted') {
return
}
// EAS projectId는 app.json/eas.json에서
const projectId =
(Constants.expoConfig?.extra?.eas?.projectId as string | undefined) ??
(Constants.easConfig?.projectId as string | undefined)
const tokenData = await Notifications.getExpoPushTokenAsync(projectId ? { projectId } : undefined)
const token = tokenData.data
if (!token) return
const platform: 'ios' | 'android' | 'web' =
Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'web'
// Android 채널 설정
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#f25b29'
})
}
// Supabase upsert
await supabase.from('push_tokens').upsert(
{
user_id: userId,
token,
platform,
device_name: Device.deviceName ?? null
},
{ onConflict: 'token' }
)
} catch {
// push 등록 실패는 앱 진행 차단하지 않음
}
}

View file

@ -17,7 +17,9 @@
"expo": "~51.0.0",
"expo-av": "~14.0.0",
"expo-constants": "~16.0.0",
"expo-device": "~6.0.0",
"expo-linking": "~6.3.0",
"expo-notifications": "~0.28.0",
"expo-router": "~3.5.0",
"expo-secure-store": "~13.0.0",
"expo-status-bar": "~1.12.0",