Commit Graph
7 Commits
Author SHA1 Message Date
csbaeandClaude Opus 5 b115663ecf feat: 서버 STT 재전사 — 오디오 원본에서 확정 전사문을 뽑는다
#18이 오디오를 남기게 했다면, 이 PR은 그 오디오를 실제로 쓴다.
Web Speech 포착률 7~10%를 끌어올리는 유일한 방법은 인식 엔진 교체다.

## 두 경로가 하나로 만난다

    실시간 녹음 ─┐
                 ├→ recordingId → /api/transcribe → 확정 전사문
    파일 업로드 ─┘

`/api/upload`는 파일을 디스크에 떨궈만 두고 "실시간 녹음 탭에서 하세요"라고
안내하던 막다른 길이었다. 이제 녹음 저장소에 넣고 recordingId를 돌려주므로
브라우저 녹음과 완전히 같은 파이프라인을 탄다.

## 실제 타임스탬프

Whisper `verbose_json`이 구간별 실제 오디오 시각을 준다. Web Speech 경로가 쓰던
`결과 이벤트 시각 − 2초` 추정값과 달리 구간 재생·화자 분리에 그대로 쓸 수 있다.
이 값이 있어야 PR #17의 diarization 화자 배정이 비로소 의미를 갖는다.

verbose_json을 지원하지 않는 모델(gpt-4o-transcribe 등)은 400을 주므로 `json`
으로 한 번 더 시도해 텍스트만이라도 받는다.

## 프로바이더별 지원

| 프로바이더 | webm 녹음 | 업로드 파일 | 기본 모델 |
|---|:---:|:---:|---|
| OpenAI · OrcaRouter · 로컬 whisper | O | O | whisper-1 |
| Gemini | X | O | gemini-3.6-flash |

Gemini가 문서로 밝힌 오디오 형식에 webm이 없다. 조용히 실패시키지 않고
"OpenAI 호환 프로바이더를 쓰라"고 명시적으로 안내하며, 설정 화면에도 경고를 띄운다.

## 비트레이트 128k → 32k

이 오디오는 감상용이 아니라 STT 입력이다. 128kbps면 46분 회의가 44MB가 되어
Whisper 상한(25MB)도 Gemini inline 상한도 넘긴다. 32kbps mono Opus는 음성
인식 정확도에 영향이 없으면서 46분을 11MB로 줄인다.

## 참석자·용어 힌트

홈 화면에 입력란을 두고 전사 요청의 어휘 힌트로 보낸다. 실측 transcript에서
"계명대동산병원 → 저희 키스해 주셔서", "양식대로 → 양복점" 같은 고유명사
붕괴가 심했던 부분이다.

## 검증

가짜 Whisper 서버를 세워 종단간 확인:
- 업로드 → recordingId → 전사 → `[00:00]/[00:03]/[00:12]` 전사문
- multipart 필드 검증: model · response_format=verbose_json · language=ko ·
  prompt(어휘 힌트) · Bearer 인증 · 파일 바이트 정확히 일치
- webm 녹음도 Whisper로 정상 전사 (5000 bytes 그대로 전달)
- Gemini+webm 거부 / 미설정 400 / 경로조작 400 / 없는 녹음 404

테스트 206 → 229 통과, 신규 타입 에러 0, 린트 baseline과 동일, 빌드 성공.

### 검증하지 못한 것

실제 Whisper·Gemini API를 호출하지 못했다(키 없음). 모든 프로바이더 코드는
목 fetch와 가짜 서버로만 검증했다. 요청 형식은 문서를 따랐으나 실제 응답에
대한 확인이 필요하다.

포착률이 실제로 얼마나 오르는지도 아직 모른다. 다음 회의에서 같은 오디오로
Web Speech와 STT를 나란히 놓고 재야 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzAH7GPWSYTe2AoBV6CZDP
2026-09-09 15:19:49 +09:00
csbaeandClaude Opus 5 cf3ce3f40c feat: 회의 오디오 원본 보관 — 전사가 놓친 발화를 되살릴 수 있게
46분 대면 회의 실측에서 Web Speech 포착률이 7~10%에 그쳤다(489어절 /
분당 10.6어절, 30초 이상 공백 26곳 26분 29초). 빈 텍스트 청크 19개는
Chrome이 `isFinal: true`에 `transcript: ""`를 준 것으로, 소리는 감지했으나
인식에 실패했다는 신호다.

지금까지는 오디오를 어디에도 남기지 않아 놓친 발화를 복구할 수도, 얼마나
놓쳤는지 잴 수도 없었다. 전사와 독립적으로 원음을 파일로 남긴다.

## 유실 방지 설계

메모리에 모았다가 종료 시 한 번에 쓰지 않는다. 그러면 탭이나 서버가 죽는
순간 회의 전체가 사라진다. 15초 조각마다 디스크에 append 하므로 어느
시점에 중단되든 그때까지의 오디오는 남는다.

- 서버 업로드가 실패해도 녹음을 멈추지 않고 브라우저 사본으로 회수한다
- 조각이 3회 재시도 후에도 실패하면 뒤를 이어 붙이지 않고 멈춘다.
  중간이 빈 파일은 짧은 파일보다 나쁘다 — 재생도 전사도 안 된다
- 한 조각도 못 받았으면 "보관됨"이라고 하지 않고 오류를 띄운다
- 서버 사본이 로컬보다 짧으면 경고한다

## 캡처 제약 변경

`noiseSuppression: false, echoCancellation: false, autoGainControl: true`.
브라우저 기본값은 전화 통화용 튜닝이라 멀리 앉은 화자를 노이즈로 지운다.
AGC는 조용한 화자를 끌어올려 주므로 남긴다. 실측으로 검증할 가설이며
회귀 방지 테스트를 걸어 뒀다.

## 검증

- 서버 파이프라인: 184조각 755KB 업로드 → 다운로드 SHA256 바이트 일치
- 클라이언트 전 경로: 합성 스트림으로 48초 녹음 → 로컬·서버 크기 일치,
  서버 파일이 decodeAudioData로 48.12초 실제 오디오로 디코딩됨
- 경로 조작 400 / 없는 세션 404 / 빈 조각 400 / 미허용 mimeType 400
- 테스트 162 → 206 통과, 신규 타입 에러 0, 린트 baseline과 동일

실제 마이크 경로는 테스트하지 못했다. Web Speech와 getUserMedia가 같은
마이크를 두고 경합하는지 실사용 확인이 필요하다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzAH7GPWSYTe2AoBV6CZDP
2026-09-09 15:04:04 +09:00
nad4 f4ad512056 chore: 공개 저장소 전환 보안 강화 + README 재구성 (#11)
* chore: 공개 저장소 전환 보안 강화 + README 재구성

보안 리뷰 결과를 반영하여 회사 직원 로컬 사용에 안전하도록 강화.
README는 사용자가 보기 쉽도록 목차·기능별 anchor·보안 모델로 재구성.

## 보안 변경 (CRITICAL/HIGH 위주)

### 인프라 (docker-compose.yml)
- Postgres 호스트 포트 5432 노출 제거 (컨테이너 내부 네트워크만 사용)
  → LAN의 다른 장치에서 직접 DB 접근 불가
- App 포트 127.0.0.1:3000으로 바인딩 (LAN 노출 차단)
- DB 자격증명 fallback `:- meetingpass` 제거 → `:?` 강제 환경변수
  → .env 누락 시 명시적 에러로 실패 (기본 비밀번호 사용 위험 제거)

### 응답 헤더 (next.config.ts)
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: camera=(), microphone=(self), ...

### 입력 검증
- /api/summarize: transcript 100,000자 상한 (이전 무제한)
- DOMPurify: FORBID_TAGS/ATTR ['style'] 추가 → 인라인 CSS 인젝션 차단

### 런타임 안전성
- src/lib/db.ts: DATABASE_URL 미설정 시 production에서 명시적 경고
  (build-time 빌드는 placeholder로 통과 유지)

### 사용자 안내 (UI)
- /settings 페이지: 공유 PC 경고 + Web Speech API 외부 송출 안내

## 문서

### 신규
- LICENSE (MIT)
- SECURITY.md (위협 모델 / 신뢰 모델 / 한계 / 신고 방법)

### README 재구성
- 4-line Quick Start 헤더
- 📑 목차 (anchor 점프)
- ✨ 주요 기능 7개 sub-section (각각 사용법/동작/제한)
- 🔒 보안 모델 섹션 추가 (공개 배포 권장 사항 명시)
- ⚠️ 알려진 제약 표 정리

### .env.example
- 강력한 비밀번호 생성 가이드 (openssl rand -base64 24)
- 필수/선택 명확히 구분

### docs/ROADMAP.md
- "보안 강화" 섹션 추가 (완료된 보안 조치 목록)

## 사용자 직접 조치 필요 (코드로 처리 불가)
- ⚠️ .env 파일에 발급된 Gemini API 키가 주석 처리되어 남아 있음
  Google AI Studio에서 폐기 후 .env에서 해당 줄 삭제 필요
- POSTGRES_PASSWORD를 강력한 임의 값으로 교체 (openssl rand -base64 24)
- 비밀번호 변경 시 docker compose down -v 후 재시작 (데이터 초기화)

## 검증
- 102 tests passing
- 보안 헤더 4종 적용 확인 (curl -sI)
- LAN IP에서 접근 시도 차단 확인 (192.168.x.x:3000 → 000)
- 127.0.0.1:3000 정상 동작 확인 (200)

## 보류된 항목 (문서화로 처리)
- M-1: Web Speech API의 음성 → Google 송출 (구조적 한계)
- M-2: npm audit 6건 (dev 의존성, 자동 수정 가능한 것 없음)
- H-3: 파일 업로드 매직 바이트 검증 (업로드 파일 미사용 상태)
- 인증 시스템: 단일 사용자 가정으로 비목표

* docs: 직원용 5분 시작 가이드 추가

EMPLOYEE_QUICKSTART.md에 직원 셋업 5분 가이드 작성:
- 한 번에 끝나는 setup 명령 (openssl로 강력한 비밀번호 자동 생성)
- 추천 사용 흐름 + 기능별 단축 액션
- 사내 정책 확인 안내 (음성 → Google 송출 명시)
- 단순 변환 모드(API 키 없음) 옵션 안내
- 자주 쓰는 docker 명령

README에서 직원/팀원 공유 시 가이드로 링크.
2026-04-28 18:59:39 +09:00
nad4 908cc860e8 Phase 2: 액션 아이템 + Google Docs 호환 복사 (#4)
* feat(phase-1): 저장/목록/상세 UI + 마크다운 라이브러리 + 검색

ROADMAP Phase 1 #3~#7 완료.

## Markdown 렌더링 (#6)
- marked + isomorphic-dompurify 도입
- src/lib/markdown.ts 공용 유틸 (renderMarkdownToSafeHtml + markdownToPlainText)
- 기존 커스텀 파서(export-minutes.ts) 교체 — 표/리스트/인용/코드블록 정상 렌더
- globals.css에 .prose-minutes 스타일 추가

## 저장 버튼 (#3)
- MinutesViewer에 📌 저장 버튼 + 상태별 피드백
- 미리보기/원문 토글
- 저장 성공 시 /meetings/[id] 상세 링크 배너
- URL.revokeObjectURL 타이밍을 setTimeout으로 지연 (Firefox/Safari 호환)

## 목록 페이지 (#4)
- /meetings 서버 컴포넌트, 페이지네이션 (prev/next)
- MeetingCard: 제목/날짜/템플릿 배지/참석자/태그/preview
- 빈 상태 UI (검색 결과 없음 / 첫 녹음 안내)
- preview는 markdown → plain text 변환 후 160자로 자르기

## 상세 페이지 + 편집 (#5)
- /meetings/[id] 서버 컴포넌트 + 클라이언트 편집 island
- 제목/본문/참석자/태그 인라인 편집
- 편집 모드에서 좌(textarea) · 우(live preview) 분할
- 삭제 버튼 (confirm)
- 원본 transcript는 <details>로 접어둠

## 검색 (#7)
- MeetingSearchBar 클라이언트 컴포넌트
- 300ms debounce + useTransition + URL query 동기화 (replace, scroll false)
- 서버에서 ILIKE contains로 title/transcript/markdown 검색
- tsvector GIN 인덱스는 후속 최적화로 분리 (개인용 규모에서 ILIKE 충분)

## Prisma 7 호환
- @prisma/adapter-pg + pg 추가 (driver adapter 필수)
- generator를 prisma-client-js로 전환 (안정적 레거시)
- src/lib/db.ts에 PrismaPg 어댑터 사용 + DATABASE_URL 누락 시 placeholder로 fallback
- DB 접근 페이지/라우트에 export const dynamic = 'force-dynamic' 추가
  (Next 16 pre-render 시 Prisma 초기화 회피)
- Dockerfile에서 src/generated 복사 제거, .gitignore도 정리

## UX
- 메인 페이지 우상단 "📚 저장된 회의록" 링크 추가

## 테스트
- src/__tests__/markdown.test.ts 9개 신규 (렌더/sanitize/plain text)
- 전체: 80 tests passing (이전 71 + 9)

* docs: PR#2 본문 드래프트 추가

* feat(phase-1.5): 웹에서 Gemini API 키 설정 (LocalStorage)

.env를 만지지 않고도 브라우저에서 키를 설정·관리할 수 있게 함.
개인용 단일 인스턴스 가정으로 LocalStorage만 사용 (DB 미관여).

## 구조
- src/lib/api-keys.ts (서버): resolveGeminiApiKey() 우선순위
  요청 body 키 → process.env.GEMINI_API_KEY → null
- src/lib/api-key-storage.ts (클라이언트): get/set/clear/maskApiKey
- /api/settings/status: 환경변수 키 존재 여부만 노출 (값 X)
- /api/settings/test: 키로 짧은 Gemini 호출 → 200/401/403/429 분류
- /settings 페이지: 입력/저장/삭제/마스킹 표시/테스트 호출

## 라우트 변경
- /api/summarize, /api/summarize-live가 body.apiKey 수용
- 키 없을 때 summarize는 단순 변환으로 폴백 (UX 보존)
- summarize-live는 503 + 안내 메시지 (라이브 요약은 키 필수)

## UI
- 메인 헤더에 ⚙️ 설정 링크 추가
- 설정 페이지에 현재 키 소스 배지 (브라우저 / 환경변수 / 없음)
- 보이기/숨기기 토글, 테스트 호출 버튼, 보안 안내

## 테스트
- api-keys.test.ts: 7개 (요청>env, 공백 처리, env 미설정 시 null)
- api-key-storage.test.ts: 7개 (LocalStorage CRUD + 마스킹)
- 전체: 94 tests (이전 80 + 14)

## 비목표 (개인용 가정)
- 다중 사용자 / 권한
- DB 영속화 / 키 암호화

* docs: PR#3 본문 드래프트 추가

* feat(phase-2): 액션 아이템 추출/체크리스트 + Google Docs 호환 복사

## 액션 아이템 (ROADMAP #8~#11)
ROADMAP의 Gemini JSON 출력 대신 마크다운 체크박스 휴리스틱 채택.
모든 템플릿이 이미 - [ ] 형식 출력하므로 Gemini 호출 비용 0.

### Schema
- ActionItem 테이블 추가 (Meeting 1:N, cascade delete)
- 필드: id, meetingId, task, isDone, position, createdAt, updatedAt
- 인덱스: (meetingId, position) / isDone
- 마이그레이션 20260428071709_add_action_items

### 추출 (src/lib/action-items.ts)
- /^\s*[-*]\s+\[(x|X|\s)\]\s+(.+)/ 매칭
- 빈/짧은(<2자)/점만(...) task 노이즈 제거
- 들여쓰기/대문자 X 모두 허용
- 단위 테스트 8개

### API
- POST /api/meetings: 마크다운에서 액션 아이템 자동 추출 후 함께 저장
- GET /api/meetings (목록): _count로 미완료 액션 카운트 포함
- GET /api/meetings/[id] (상세): actionItems include
- PATCH /api/action-items/[id]: isDone 토글, task 수정
- DELETE /api/action-items/[id]
- POST /api/meetings/[id]/reparse-action-items: 마크다운 재파싱으로 교체

### UI
- ActionItemList 컴포넌트: 체크박스 + 낙관적 업데이트 + 진행률(완료/전체)
  + 개별 삭제 (hover ✕) + 🔄 재추출 버튼
- MeetingDetail에 통합 (조회 모드에서만 노출, 편집 중에는 숨김)
- MeetingCard에 미완료 카운트 배지 (✅ N 미완료)
- /meetings 상단에 "내 미완료 액션 (최근 5)" 위젯

### 비목표 (이번 단계)
- assignee/dueDate 필드 — Gemini JSON 모드 도입 시 추가
- 홈 페이지 대시보드 승격 — 녹음 흐름 보존이 우선

## Google Docs 호환 복사
- src/lib/markdown.ts: copyMarkdownAsRichText()
  - ClipboardItem({text/html, text/plain}) 우선
  - 미지원 브라우저는 contenteditable + execCommand 폴백
- MinutesViewer / MeetingDetail에 📋 Docs용 버튼 추가
- 기존 .md 복사는 📋 .md로 분리 라벨링
- 다운로드 버튼은 ⬇ 접두로 구분

## 테스트
- action-items.test.ts: 8개 (다양한 마크다운 패턴)
- 전체: 102 tests passing (이전 94 + 8)

* docs: PR#4 본문 드래프트 추가
2026-04-28 16:41:24 +09:00
nad4 cf6613bb94 Phase 1 foundation: Prisma + /api/meetings CRUD (#1)
* feat(phase-1): Prisma 연동 + 회의록 CRUD API

Phase 1 foundation (ROADMAP #1~#3):

- src/lib/db.ts: Prisma 클라이언트 싱글톤 (dev hot-reload 대응)
- Meeting 스키마 확장: attendees/tags (String[]), template/depth/
  summaryMode/customPrompt (향후 참조 복원용), createdAt 인덱스
- 첫 마이그레이션 `20260421104750_init` 생성
- /api/meetings (GET 목록 + POST 생성)
  - q 파라미터로 제목/transcript/markdown 부분 검색
  - limit (max 100), offset 페이지네이션
  - attendees/tags 배열 타입 정규화
- /api/meetings/[id] (GET/PUT/DELETE)
  - PUT은 markdownMinutes/title/attendees/tags만 편집 허용
  - 존재하지 않는 id에 대해 404
- docker-compose.yml: tools 프로필에 migrate 서비스 추가
  (app 컨테이너 bloat 없이 마이그레이션 실행)
- Prisma mock 기반 API 테스트 14개 신규 (총 71 tests)

* docs: migrate 실행 단계 추가, ROADMAP의 #1/#2 완료 체크

* docs: PR 본문 드래프트 추가
2026-04-28 16:38:09 +09:00
nad4 78e0caac0b docs: Phase 0.5 템플릿 시스템 반영, 화자 구분은 비목표로 이전
- ROADMAP: Phase 0.5 (완료) 섹션 추가 — 6개 템플릿, 3단계 강도
- ROADMAP: 화자 구분(#15) 제거 → Non-goals 섹션으로 이동
  (유료 API 비용/복잡도가 개인용 범위에 과함)
- README: 템플릿 대조표 추가, 강도 설명, 테스트 수 57로 갱신
- CREATE_ISSUES.sh: Phase 4 에픽에서 #15 제거
2026-04-21 19:38:32 +09:00
nad4 6e104cbca1 docs: ROADMAP 추가, README에 Phase 0 현황 반영
노션 AI Meeting Notes를 참고한 4단계 로드맵을 docs/ROADMAP.md에 정리.
Phase 0(완료), Phase 1(워크스페이스), Phase 2(액션 아이템),
Phase 3(참석자/태그), Phase 4(선택 — 화자 구분/캘린더/AI Q&A).

README는 현재 구현 기능(실시간 요약, 자동 폴백, 네트워크 재연결 등)을
반영하고 테스트 38개, Docker tools 프로필 설명 추가.

docs/CREATE_ISSUES.sh는 gh CLI로 4개 epic 이슈를 자동 생성.
2026-04-21 17:24:34 +09:00