From f4ad5120565d13773983fada26a75f781d8cb1ad Mon Sep 17 00:00:00 2001 From: "N@D4" Date: Tue, 28 Apr 2026 18:59:39 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=EA=B3=B5=EA=B0=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=20=EC=A0=84=ED=99=98=20=EB=B3=B4=EC=95=88=20?= =?UTF-8?q?=EA=B0=95=ED=99=94=20+=20README=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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에서 직원/팀원 공유 시 가이드로 링크. --- .env.example | 37 ++- LICENSE | 21 ++ README.md | 529 ++++++++++++++++++++++++--------- SECURITY.md | 194 ++++++++++++ docker-compose.yml | 21 +- docs/EMPLOYEE_QUICKSTART.md | 114 +++++++ docs/ROADMAP.md | 25 ++ next.config.ts | 18 ++ src/app/api/summarize/route.ts | 11 + src/app/settings/page.tsx | 6 + src/lib/db.ts | 21 +- src/lib/markdown.ts | 2 + 12 files changed, 837 insertions(+), 162 deletions(-) create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 docs/EMPLOYEE_QUICKSTART.md diff --git a/.env.example b/.env.example index 9ff4c80..1568aac 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,37 @@ -# PostgreSQL -DATABASE_URL="postgresql://meetinguser:meetingpass@localhost:5432/meetingminutes" +# ============================================================================= +# Meeting Minutes — 환경 변수 +# +# 사용법: +# 1. 이 파일을 .env로 복사: cp .env.example .env +# 2. 아래 값들을 직접 채워주세요. 빈 값으로 두면 docker compose가 실패합니다. +# +# 강력한 비밀번호 생성 (macOS / Linux): +# openssl rand -base64 24 +# ============================================================================= + +# ----------------------------------------------------------------------------- +# 필수 — PostgreSQL +# ----------------------------------------------------------------------------- POSTGRES_USER=meetinguser -POSTGRES_PASSWORD=meetingpass POSTGRES_DB=meetingminutes -# Gemini API (선택사항 - AI 요약 기능에 필요) +# ⚠️ 강력한 임의 값으로 채우세요 (예: openssl rand -base64 24) +# 이 비밀번호는 docker compose가 첫 기동 시 사용하며, 이후 변경하려면 +# `docker compose down -v`로 볼륨을 삭제하거나 ALTER USER로 갱신해야 합니다. +POSTGRES_PASSWORD= + +# 위 USER/PASSWORD/DB와 일치해야 합니다. +# 로컬 개발(npm run dev) 시에는 host=localhost, Docker에서는 host=db. +DATABASE_URL= + +# ----------------------------------------------------------------------------- +# 선택 — Gemini API 키 +# ----------------------------------------------------------------------------- +# Gemini AI 요약 기능을 쓰려면 키가 필요합니다. +# 두 가지 방법 중 선택: +# (A) 웹 UI 방식 — 앱 기동 후 /settings에서 입력 (브라우저 LocalStorage 저장, 추천) +# (B) 환경변수 방식 — 아래 값 채우면 모든 사용자가 공통으로 사용 +# +# 무료 발급: https://aistudio.google.com/apikey +# 키 없이도 "단순 변환" 모드는 정상 동작합니다. GEMINI_API_KEY= diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..342f018 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 nad4-su + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2ac8e1e..3b43ffb 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,71 @@ -# Meeting Minutes — 실시간 AI 회의록 +# 🎙️ Meeting Minutes -음성 녹음을 실시간으로 텍스트·요약으로 변환하고, 구조화된 회의록을 자동 생성하는 웹 서비스. +**음성을 텍스트로, 텍스트를 회의록으로** — 브라우저 기반 실시간 AI 회의록 자동 작성 도구. -향후 로드맵 — 노션 AI Meeting Notes 수준의 워크스페이스: [`docs/ROADMAP.md`](docs/ROADMAP.md) +녹음 시작 한 번이면 끝. Gemini AI가 30초마다 회의록을 갱신하고, 종료 시 자동으로 최종 문서를 생성합니다. 회의록 외에도 강의 노트, 1:1 미팅, 브레인스토밍 등 6가지 템플릿을 제공하며, Google Docs에 서식 그대로 붙여넣기까지 한 번에. + +```bash +git clone https://github.com/nad4-su/meeting-minutes.git +cd meeting-minutes && cp .env.example .env +# ⚠️ .env 파일을 열어 POSTGRES_PASSWORD를 강력한 값으로 채워주세요 +# 예: echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)" >> .env +docker compose --profile tools run --rm migrate +docker compose up -d +# → http://127.0.0.1:3000 (Chrome 권장, localhost 전용) +``` + +> ⚠️ **보안 주의**: 회의 내용에 민감 정보가 있다면 [SECURITY.md](SECURITY.md)의 위협 모델 섹션을 먼저 읽어주세요. 실시간 녹음은 Chrome Web Speech API를 사용하여 음성을 Google 서버로 전송합니다. +> +> 👥 **직원/팀원에게 공유할 때**: [docs/EMPLOYEE_QUICKSTART.md](docs/EMPLOYEE_QUICKSTART.md) — 5분 시작 가이드 (사내 정책 안내 포함) --- -## 현재 기능 (Phase 0 + 0.5 + 1 + 1.5 + 2) +## 📑 목차 -### 녹음 & 전사 -- 브라우저 실시간 음성 인식 (Web Speech API · Chrome `ko-KR`) -- 네트워크 끊김 시 **자동 재연결** (최대 8회) — 누적 transcript 보존 -- 파일 업로드: MP3, WAV, WebM, M4A, OGG, FLAC (최대 500MB) -- 인식된 텍스트는 textarea에서 직접 편집 가능 +- [✨ 주요 기능](#-주요-기능) + - [🎤 실시간 녹음 & 전사](#-실시간-녹음--전사) + - [🤖 AI 회의록 (6 템플릿 × 3 강도)](#-ai-회의록-6-템플릿--3-강도) + - [⚡ 실시간 롤링 요약](#-실시간-롤링-요약) + - [💾 회의록 저장/조회/검색](#-회의록-저장조회검색) + - [✅ 액션 아이템 자동 추출](#-액션-아이템-자동-추출) + - [🔑 웹에서 API 키 설정](#-웹에서-api-키-설정) + - [📋 Google Docs 호환 복사](#-google-docs-호환-복사) +- [🚀 시작하기](#-시작하기) + - [Docker Compose (권장)](#docker-compose-권장) + - [로컬 개발 환경](#로컬-개발-환경) +- [🛠️ 기술 스택](#️-기술-스택) +- [📁 프로젝트 구조](#-프로젝트-구조) +- [🗺️ 로드맵](#️-로드맵) +- [🔒 보안 모델](#-보안-모델) +- [⚠️ 알려진 제약](#️-알려진-제약) +- [🤝 기여하기](#-기여하기) +- [📄 라이선스](#-라이선스) -### AI 요약 (Gemini) -- **실시간 회의록**: 녹음 중 30초 간격으로 Gemini가 중간 요약 갱신 (25단어 이상, 40단어 증분 게이트) -- **최종 회의록**: 녹음 중지 시 자동 생성 -- 모델: `gemini-2.5-flash-lite` (개인 무료 등급에서 여유있게 동작) -- 429 쿼터 초과 시 **60초 쿨다운** + UI 안내, 그 외 실패는 지수 백오프 -- Gemini 실패 시 **단순 변환 마크다운으로 자동 폴백** — 결과물은 항상 보장 -- 내보내기: Markdown(.md), HTML(.html), 클립보드 복사 +--- -### 템플릿 & 강도 조절 (Phase 0.5) -용도에 맞게 출력 구조와 요약 깊이를 조절할 수 있음. +## ✨ 주요 기능 + +### 🎤 실시간 녹음 & 전사 + +브라우저 내장 Web Speech API(Chrome `ko-KR`)로 마이크 입력을 실시간 텍스트로 변환합니다. + +**특징** +- 즉시 시작 — 별도 STT 서버나 키 없이 작동 +- **자동 재연결** — 네트워크 끊김 시 최대 8회 재시도, 누적 transcript 보존 +- 확정 전 텍스트는 회색 이탤릭 + 깜빡이는 커서로 시각화 +- 녹음 중 `🔴 N단어 · M개 구간` 실시간 카운터 + +**사용** +1. 홈에서 **🎤 녹음 시작** 클릭 +2. 마이크 권한 허용 +3. 발화 → 좌측 패널에 텍스트가 쌓임 +4. **녹음 중지** → 텍스트가 textarea로 이동, 직접 편집 가능 + +--- + +### 🤖 AI 회의록 (6 템플릿 × 3 강도) + +회의 외에도 다양한 용도에 맞춰 출력 구조를 선택할 수 있습니다. | 템플릿 | 생성되는 구조 | 기본 강도 | 라이브 요약 | |---|---|---|---| @@ -35,221 +77,418 @@ | 📝 원문 정리 | 요약 없이 문단화·오탈자 정리만 | 상세 고정 | ❌ | | ⚙️ 커스텀 | 자유 프롬프트 입력 | - | ✅ | -강도 3단계: +**강도 3단계** - **간결** — 각 섹션 3줄 이내 - **표준** — 맥락이 이해될 정도 - **상세** — 세부사항·수치 누락 없이 보존 -### 액션 아이템 (Phase 2) -- 회의록 저장 시 마크다운의 `- [ ]` 항목을 자동으로 추출 → DB에 구조화 저장 -- 상세 페이지에 **체크리스트 섹션** — 체크박스 토글로 완료 표시 -- 개별 항목 삭제, 마크다운 편집 후 **🔄 재추출** 버튼으로 동기화 -- `/meetings` 상단에 "내 미완료 액션 (최근 5)" 위젯 -- 회의 카드에 미완료 카운트 배지 (예: `✅ 3 미완료`) -- **Google Docs 호환 복사** — `📋 Docs용` 버튼으로 서식 유지 (heading/list/bold/code) 채로 클립보드에 복사 → Docs/Word/Notion에 그대로 붙여넣기 - -### Gemini 키 웹 설정 (Phase 1.5) -- **`/settings` 페이지** — 헤더 ⚙️ 링크로 진입 -- 브라우저 LocalStorage에 저장 (서버 DB 미사용) -- 마스킹된 현재 키 표시 (`AIza••••XYZ12`), 보이기/숨기기 토글 -- **🧪 테스트 호출** — 가벼운 Gemini 응답으로 키 유효성 검증 -- 우선순위: 브라우저 키 → 환경변수 → 단순 변환 폴백 -- 키 미설정 시에도 단순 변환 모드로 회의록은 항상 생성됨 - -### 저장·조회 워크스페이스 (Phase 1) -- **`📌 저장` 버튼** — 생성된 회의록을 DB에 영속 저장 -- **`/meetings` 목록 페이지** — 카드 그리드, 페이지네이션, 빈 상태 UI -- **`/meetings/[id]` 상세 페이지** — Markdown 렌더 + 인라인 편집 + 삭제 - - 편집 모드는 좌(textarea) · 우(live preview) 분할 - - 참석자/태그도 상세 페이지에서 편집 -- **검색 바** — 제목·transcript·본문 부분 검색 (300ms debounce, URL 동기화) -- **Markdown 라이브러리** — `marked` + `isomorphic-dompurify`로 안전한 HTML 렌더 (표/리스트/인용 등 모두 정상) - -### 화면 -- 녹음 시 **좌(실시간 텍스트) · 우(실시간 회의록)** 분할 뷰 -- 확정 전 interim 텍스트는 회색 이탤릭 + 깜빡이는 커서 -- 새 내용 도착 시 자동 스크롤 -- 진행률 바 (첫 요약까지 단어 수), 쿨다운 카운터 -- 템플릿/강도 변경 시 라이브 요약도 즉시 반영 -- 메인 화면 상단 우측 `📚 저장된 회의록` 링크 → 목록으로 이동 +**사용** +1. 변환 모드 = `Gemini AI 요약` 선택 +2. 템플릿 선택 (예: `🎓 강의·세미나 노트`) +3. 강도 선택 +4. 녹음 또는 텍스트 입력 → **회의록 생성** --- -## 기술 스택 +### ⚡ 실시간 롤링 요약 -| 구분 | 기술 | -|------|------| -| 프레임워크 | Next.js 16 (App Router) + TypeScript | -| STT (실시간) | Web Speech API — Chrome 내장, 무료 | -| AI 요약 | Gemini 2.5 Flash Lite — 무료 등급 15 RPM / 1000 RPD | -| DB | PostgreSQL 16 + Prisma 7 (driver adapter `@prisma/adapter-pg`) | -| Markdown | `marked` + `isomorphic-dompurify` | -| 테스트 | Vitest (102 tests, jsdom) | -| 배포 | Docker Compose (app + db + test profile) | +녹음 중 **30초마다** Gemini가 그동안의 발화를 분석해 중간 회의록을 자동 갱신합니다. + +**동작** +- 첫 갱신 조건: 25단어 이상 +- 이후 갱신 조건: 직전 요약 후 40단어 증가 +- 화면 우측에 진행률 바 + 갱신 시각 표시 +- 429 쿼터 초과 시 60초 자동 쿨다운, UI에 카운트다운 노출 +- 일반 실패 시 지수 백오프 (15s → 30s → 최대 120s) + +**비용 가드 (Gemini 2.5 Flash Lite 무료 등급 기준)** +- 15 RPM / 1000 RPD / 250K TPM +- 30초 폴링 + 증분 게이트 → 1시간 회의 ≤ 60회 호출, 발화량 적으면 훨씬 적음 +- 개인 사용 시 일일 한도 도달 거의 불가 --- -## 빠른 시작 (Docker Compose) +### 💾 회의록 저장/조회/검색 -### 1. 준비 +생성한 회의록을 PostgreSQL에 영속 저장하고 나중에 다시 조회/편집/검색할 수 있습니다. + +**기능** +- **`📌 저장`** 버튼 → DB에 저장 → 상세 페이지 링크 제공 +- **`/meetings`** 카드 그리드 — 제목 / 날짜 / 템플릿 배지 / 미완료 액션 카운트 +- **검색** — 300ms debounce, 제목·transcript·본문 부분 매칭, URL 동기화 +- **`/meetings/[id]`** 상세 — Markdown 렌더링 + 인라인 편집 (좌 textarea / 우 미리보기) +- **참석자 / 태그** 편집 (목록 필터링은 Phase 3 예정) +- 삭제 (확인 다이얼로그) + +**API** +| 메서드 | 경로 | 용도 | +|---|---|---| +| `GET` | `/api/meetings` | 목록 + 검색 (`?q=`) + 페이지네이션 | +| `POST` | `/api/meetings` | 새 회의록 저장 (액션 아이템 자동 추출 동반) | +| `GET` | `/api/meetings/[id]` | 상세 (actionItems 포함) | +| `PUT` | `/api/meetings/[id]` | 본문/메타 편집 | +| `DELETE` | `/api/meetings/[id]` | 삭제 | + +--- + +### ✅ 액션 아이템 자동 추출 + +회의록 저장 시 마크다운의 `- [ ]` / `- [x]` 항목을 **자동으로 구조화** 추출하여 별도 체크리스트로 관리합니다. + +**기능** +- 저장 시 자동 추출 (Gemini 추가 호출 0 — 휴리스틱 파싱) +- 상세 페이지에 **✅ 액션 아이템** 섹션 +- 체크박스 토글 (낙관적 업데이트, 즉시 반영) +- 진행률 배지 (`완료/전체`) +- 개별 삭제 (hover ✕) +- **🔄 재추출** — 마크다운 편집 후 동기화 (확인 후 토글 상태 초기화) +- `/meetings` 상단 위젯 "내 미완료 액션 (최근 5건)" +- 회의 카드에 미완료 카운트 배지 + +**API** +| 메서드 | 경로 | 용도 | +|---|---|---| +| `PATCH` | `/api/action-items/[id]` | 토글 / task 수정 | +| `DELETE` | `/api/action-items/[id]` | 개별 삭제 | +| `POST` | `/api/meetings/[id]/reparse-action-items` | 마크다운 재파싱 | + +--- + +### 🔑 웹에서 API 키 설정 + +`.env`를 만지지 않고 `/settings` 페이지에서 Gemini 키를 입력·저장·검증할 수 있습니다. + +**저장 위치** +- 브라우저 LocalStorage (서버 DB에 저장되지 않음) +- 요청 body로 함께 전송되어 일회성으로 사용 + +**우선순위** +``` +1. 요청 body의 apiKey (브라우저 LocalStorage) + ↓ 없으면 +2. process.env.GEMINI_API_KEY (서버 환경변수 fallback) + ↓ 없으면 +3. summarize → 단순 변환 폴백 (warning 표시) + summarize-live → 503 + 안내 +``` + +**기능** +- 마스킹된 현재 키 표시 (`AIza••••XYZ12`) +- 보이기/숨기기 토글 +- **🧪 테스트 호출** — 가벼운 Gemini 응답으로 즉시 키 검증 +- 현재 키 소스 배지 (`브라우저` / `환경변수` / `없음`) + +**보안** +- HTTPS 권장 (LocalStorage는 동일 출처 정책 의존) +- GET 응답에 절대 풀 키 노출 안 함 + +--- + +### 📋 Google Docs 호환 복사 + +서식(헤딩/리스트/굵게/코드/표)을 유지한 채 클립보드에 복사 → Google Docs / Word / Notion에 그대로 붙여넣기. + +**원리** +- `navigator.clipboard.write([new ClipboardItem({ 'text/html', 'text/plain' })])` +- 미지원 브라우저는 `contenteditable` + `execCommand('copy')` 폴백 + +**버튼 라벨링** +- `📋 .md` — 마크다운 원문 복사 +- `📋 Docs용` — 서식 유지 복사 (파랑 강조) +- `⬇ .md` / `⬇ .html` — 파일 다운로드 + +--- + +## 🚀 시작하기 + +### Docker Compose (권장) + +#### 사전 요구사항 +- [Docker Desktop](https://docs.docker.com/get-docker/) 또는 Docker Engine + Compose v2 +- Chrome 브라우저 (Web Speech API) + +#### 1. 저장소 클론 ```bash -git clone git@github.com:nad4-su/meeting-minutes.git +git clone https://github.com/nad4-su/meeting-minutes.git cd meeting-minutes +``` + +#### 2. 환경 변수 (필수) + +```bash cp .env.example .env ``` -**Gemini API 키 설정 — 두 가지 방법 중 선택**: -- (A) **웹 UI** — 앱 기동 후 `/settings` 페이지에서 입력 (브라우저 LocalStorage 저장, 추천) -- (B) **`.env` 환경변수** — `GEMINI_API_KEY=AIza...` 작성 ([Google AI Studio](https://aistudio.google.com/apikey)에서 무료 발급) +`.env` 파일을 열어 다음 값을 채워주세요: -> 키가 없어도 "단순 변환" 모드는 정상 동작. Gemini 모드를 쓰려면 둘 중 하나는 필요. +```bash +POSTGRES_USER=meetinguser # 임의 사용자명 +POSTGRES_DB=meetingminutes +POSTGRES_PASSWORD=<강력한_임의_값> # openssl rand -base64 24 +DATABASE_URL=postgresql://meetinguser:<위와_같은_비번>@localhost:5432/meetingminutes +``` -### 2. DB 마이그레이션 (최초 1회 + 스키마 변경 시) +> ⚠️ `POSTGRES_PASSWORD`를 비워두면 docker compose가 명시적 에러로 실패합니다. 보안을 위한 의도된 동작. + +> 💡 **Gemini API 키는 두 가지 방법 중 선택** +> +> - **(A) 웹 UI** — 앱 기동 후 `/settings`에서 입력 (브라우저 LocalStorage, 추천) +> - **(B) `.env` 환경변수** — `GEMINI_API_KEY=AIza...` 작성 +> +> [Google AI Studio](https://aistudio.google.com/apikey)에서 무료로 발급. 키 없이도 "단순 변환" 모드는 동작. + +#### 3. DB 마이그레이션 (최초 1회) ```bash docker compose --profile tools run --rm migrate ``` -이 명령은 `db` 컨테이너를 자동 기동하고 `prisma migrate deploy`로 테이블을 생성합니다. +`db` 컨테이너를 자동 기동 후 `prisma migrate deploy`로 테이블 생성. -### 3. 기동 +#### 4. 앱 기동 ```bash -docker compose up -d --build +docker compose up -d ``` -브라우저에서 [http://localhost:3000](http://localhost:3000) 접속 → **Chrome 권장** (Web Speech API). +브라우저에서 [http://localhost:3000](http://localhost:3000) 접속. -### 4. 확인 방법 - -1. 회의 제목 입력 (선택) -2. **Gemini AI 요약** 모드 + 원하는 **템플릿** / **강도** 선택 -3. **녹음 시작** → 마이크 권한 허용 -4. 말하기 시작 → 좌측 실시간 텍스트, 25단어 이상 쌓이면 우측에 중간 회의록 생성 -5. **녹음 중지** → 최종 회의록이 페이지 하단에 자동 생성됨 -6. **`📌 저장`** 버튼 → `/meetings/[id]`로 영속화 -7. 상단 **`📚 저장된 회의록`** 링크 → 목록·검색·편집 - -### 서비스 관리 +#### 서비스 관리 ```bash -docker compose logs -f app # 로그 -docker compose restart app # 재시작 -docker compose down # 중지 (데이터 유지) -docker compose down -v # 중지 + 볼륨 삭제 -docker compose up -d --build # 코드 변경 후 재빌드 +docker compose logs -f app # 로그 +docker compose restart app # 재시작 (데이터 유지) +docker compose down # 중지 +docker compose down -v # 중지 + 볼륨 삭제 (데이터 초기화) +docker compose up -d --build # 코드 변경 후 재빌드 ``` -### 테스트 (Docker) +#### 데이터 백업 + +```bash +docker compose exec db pg_dump -U meetinguser meetingminutes > backup-$(date +%Y%m%d).sql +``` + +#### 테스트 ```bash docker compose --profile tools run --rm test ``` -별도 Node 설치 불필요. `tools` 프로필 컨테이너에서 `vitest` 실행. +별도 Node 설치 불필요. `tools` 프로필이 vitest를 컨테이너에서 실행 (102 tests). --- -## 로컬 개발 (Docker 없이) +### 로컬 개발 환경 ```bash npm install + +# 별도 PostgreSQL 필요 +docker compose up -d db +npx prisma migrate dev + +# 개발 서버 npm run dev # http://localhost:3000 npm test # 전체 테스트 +npm run test:watch # 워치 모드 npm run test:coverage # 커버리지 리포트 ``` --- -## 프로젝트 구조 +## 🛠️ 기술 스택 + +| 구분 | 기술 | +|------|------| +| 프레임워크 | Next.js 16 (App Router) + React 19 + TypeScript | +| STT (실시간) | Web Speech API — Chrome 내장, 무료 | +| AI 요약 | Gemini 2.5 Flash Lite (무료 등급 15 RPM / 1000 RPD) | +| DB | PostgreSQL 16 + Prisma 7 (driver adapter `@prisma/adapter-pg`) | +| Markdown | `marked` + `isomorphic-dompurify` | +| 스타일 | Tailwind CSS v4 | +| 테스트 | Vitest 4 (jsdom, 102 tests) | +| 배포 | Docker Compose + standalone Next.js 빌드 | + +--- + +## 📁 프로젝트 구조 ``` src/ ├── app/ │ ├── api/ -│ │ ├── upload/route.ts # 파일 업로드 -│ │ ├── summarize/route.ts # 최종 회의록 (Gemini + simple fallback) -│ │ ├── summarize-live/route.ts # 실시간 중간 요약 +│ │ ├── upload/route.ts # 파일 업로드 +│ │ ├── summarize/route.ts # 최종 회의록 (Gemini + simple fallback) +│ │ ├── summarize-live/route.ts # 실시간 중간 요약 │ │ ├── meetings/ -│ │ │ ├── route.ts # GET 목록 (q 검색) / POST 저장 +│ │ │ ├── route.ts # GET 목록 / POST 저장 │ │ │ └── [id]/ -│ │ │ ├── route.ts # GET / PUT / DELETE 상세 -│ │ │ └── reparse-action-items/route.ts # POST 마크다운 재파싱 +│ │ │ ├── route.ts # GET / PUT / DELETE +│ │ │ └── reparse-action-items/route.ts # 마크다운 재파싱 │ │ ├── action-items/ -│ │ │ └── [id]/route.ts # PATCH 토글 / DELETE +│ │ │ └── [id]/route.ts # PATCH / DELETE │ │ └── settings/ -│ │ ├── status/route.ts # 환경변수 키 설정 여부 -│ │ └── test/route.ts # 키 유효성 검증 (가벼운 Gemini 호출) +│ │ ├── status/route.ts # 환경변수 키 존재 여부 +│ │ └── test/route.ts # 키 유효성 검증 │ ├── meetings/ -│ │ ├── page.tsx # 저장된 회의록 목록 + 검색 -│ │ └── [id]/page.tsx # 상세 + 편집 + 삭제 -│ ├── settings/page.tsx # 키 설정 페이지 (LocalStorage) -│ ├── page.tsx # 메인 UI (녹음/요약) +│ │ ├── page.tsx # 목록 + 검색 + 미완료 위젯 +│ │ └── [id]/page.tsx # 상세 + 편집 +│ ├── settings/page.tsx # 키 설정 +│ ├── page.tsx # 메인 (녹음/요약) │ └── layout.tsx ├── lib/ -│ ├── db.ts # Prisma 싱글톤 (pg adapter) -│ ├── markdown.ts # marked + DOMPurify 렌더 + Docs용 서식 복사 -│ ├── action-items.ts # 마크다운 - [ ] 휴리스틱 파서 -│ ├── api-keys.ts # 서버: 요청 키 → env fallback 우선순위 -│ ├── api-key-storage.ts # 클라이언트: LocalStorage 헬퍼 + 마스킹 +│ ├── db.ts # Prisma 싱글톤 (pg adapter) +│ ├── markdown.ts # marked + DOMPurify + Docs용 복사 +│ ├── action-items.ts # - [ ] 휴리스틱 파서 +│ ├── api-keys.ts # 서버: 요청 키 → env fallback +│ ├── api-key-storage.ts # 클라: LocalStorage + 마스킹 +│ ├── templates.ts # 템플릿 레지스트리 + 프롬프트 빌더 +│ ├── minutes-generator.ts # 최종 요약 생성 +│ ├── live-summary.ts # 롤링 요약 생성 +│ ├── transcript-formatter.ts │ ├── audio-validation.ts │ ├── upload-handler.ts -│ ├── transcript-formatter.ts -│ ├── templates.ts # 템플릿 레지스트리 + 프롬프트 빌더 -│ ├── minutes-generator.ts # 최종 요약 생성 (템플릿 적용) -│ ├── live-summary.ts # 롤링 요약 생성 (템플릿 적용) │ └── export-minutes.ts ├── hooks/ -│ ├── useSpeechRecognition.ts # Web Speech API + 네트워크 재시도 -│ └── useLiveSummary.ts # 30s 폴링 + 429 쿨다운 + 증분 게이트 +│ ├── useSpeechRecognition.ts # Web Speech + 네트워크 재시도 +│ └── useLiveSummary.ts # 30s 폴링 + 429 쿨다운 ├── components/ │ ├── upload/AudioUploader.tsx -│ ├── recorder/LiveRecorder.tsx # 좌우 분할 뷰 -│ ├── minutes/MinutesViewer.tsx # 미리보기/원문 토글 + 저장 +│ ├── recorder/LiveRecorder.tsx # 좌우 분할 뷰 +│ ├── minutes/MinutesViewer.tsx # 미리보기 / 원문 / 저장 │ └── meeting/ -│ ├── MeetingCard.tsx # 미완료 액션 카운트 배지 포함 -│ ├── MeetingSearchBar.tsx # debounce + URL 동기화 -│ ├── MeetingDetail.tsx # 상세/편집 UI + Docs용 복사 -│ └── ActionItemList.tsx # 체크박스 토글 + 재추출 + 삭제 -└── __tests__/ # 102 tests +│ ├── MeetingCard.tsx +│ ├── MeetingSearchBar.tsx +│ ├── MeetingDetail.tsx +│ └── ActionItemList.tsx +└── __tests__/ # 102 tests / 13 files docs/ -├── ROADMAP.md # 개발 로드맵 (Phase 0~4) -└── CREATE_ISSUES.sh # gh CLI용 이슈 자동 생성 스크립트 +├── ROADMAP.md # 단계별 계획 +├── CREATE_ISSUES.sh # gh CLI용 epic 이슈 생성 +└── PR_DRAFT_*.md # PR 본문 히스토리 prisma/ -├── schema.prisma # Meeting 모델 -└── migrations/ # 마이그레이션 히스토리 +├── schema.prisma # Meeting + ActionItem +└── migrations/ # 2개 (init, add_action_items) ``` --- -## 로드맵 요약 +## 🗺️ 로드맵 -상세: [`docs/ROADMAP.md`](docs/ROADMAP.md) +상세는 [`docs/ROADMAP.md`](docs/ROADMAP.md) 또는 [GitHub Milestones](https://github.com/nad4-su/meeting-minutes/milestones). | Phase | 내용 | 상태 | |---|---|---| -| **0** | 실시간 전사 + 롤링 요약 + 자동 최종 생성 | ✅ 완료 | -| **0.5** | 템플릿 (6종) + 강도 조절 (3단계) + 커스텀 프롬프트 | ✅ 완료 | +| **0** | 실시간 전사 + Gemini 롤링 요약 + 자동 최종 | ✅ 완료 | +| **0.5** | 6 템플릿 + 3 강도 + 커스텀 프롬프트 | ✅ 완료 | | **1** | 회의 저장/조회/검색 워크스페이스 | ✅ 완료 | -| **1.5** | 웹에서 Gemini 키 설정 (LocalStorage) | ✅ 완료 | +| **1.5** | 웹에서 Gemini API 키 설정 (LocalStorage) | ✅ 완료 | | **2** | 액션 아이템 추출/체크리스트 + Google Docs 복사 | ✅ 완료 | -| **3** | 참석자 + 태그 시스템 | 📋 기획됨 | -| **4** | 차별화 기능 (캘린더, AI Q&A, 블록 에디터) | 💡 선택 | +| **3** | 참석자 + 태그 시스템 (목록 필터) | 📋 [`#7`](https://github.com/nad4-su/meeting-minutes/issues/7) | +| **v0.1.0** | MVP + Phase 3 + 폴리시 | 🚧 [`#6`](https://github.com/nad4-su/meeting-minutes/issues/6) | +| **4** | 캘린더 / AI Q&A / 후속 메일 / 블록 에디터 | 💡 [`#8`](https://github.com/nad4-su/meeting-minutes/issues/8) | -### 진행 관리 -1. `gh auth login` 후 `bash docs/CREATE_ISSUES.sh` — 4개 epic issue 자동 생성 -2. 이슈당 1 브랜치 1 PR 원칙 -3. PR 머지 전 `docker compose --profile tools run --rm test` 필수 -4. 완료 시 `docs/ROADMAP.md` 체크박스 업데이트 +### 의도된 비목표 (Non-goals) +- **화자 구분** — 유료 API 비용 / 복잡도 대비 개인용 범위에 과함 +- **공동 편집 (CRDT)** — 단일 사용자 가정 +- **권한 관리 / 다중 사용자** — 1인 1인스턴스 모델 +- **모바일 네이티브 앱** — 웹 PWA로 충분 --- -## 알려진 제약 +## 🔒 보안 모델 -- **Chrome 전용**: Web Speech API 비표준 — Safari/Firefox는 제한적 -- **화자 구분 불가**: 의도적 비지원 (개인용 범위를 넘음) -- **Gemini 무료 등급**: 15 RPM / 1000 RPD — 개인 사용에 충분하나 팀 단위는 유료 전환 권장 -- **검색**: 현재 PostgreSQL `ILIKE`(contains) 방식. 수천 건 이상 저장 시 `tsvector` GIN 인덱스로 후속 업그레이드 예정 +> 이 프로젝트는 **개인용 단일 인스턴스 사용을 가정**합니다. 회사 직원이 사용할 경우 각자 본인 머신에서 별도 인스턴스를 실행하는 방식을 권장합니다. + +### 신뢰 모델 +- 인증 시스템 없음 — `localhost:3000`에 접근하는 사용자는 모든 데이터에 접근 가능 +- 같은 머신을 다른 사람과 공유하지 않는 것을 가정 +- 회의 transcript / 회의록은 평문으로 PostgreSQL에 저장됨 (디스크 암호화는 호스트 OS에 위임) + +### Gemini API 키 +- LocalStorage 저장 (브라우저 동일 출처 정책으로 보호) +- 서버 DB에 저장되지 않음 +- 요청 시점에만 body로 전송, 일회성 사용 +- HTTPS 환경 권장 (HTTP는 localhost 한정) + +### 외부 데이터 송출 +- **Web Speech API** — Chrome이 마이크 오디오를 Google 서버로 전송하여 전사 (Chrome 자체 동작, 우리 서버 경유 X) +- **Gemini API** — 사용자가 명시적으로 활성화한 경우에만 transcript를 Google 서버로 전송 +- **그 외** — 외부 호출 없음 (텔레메트리 / 분석 도구 미설치) + +> ⚠️ **회사 회의 등 민감 정보가 외부 클라우드(Google)로 송출되는 점에 유의.** 사내 컴플라이언스 정책 확인 후 사용 권장. + +### 권장 배포 방식 + +**1인 1인스턴스 (권장)** +- 직원 각자 본인 머신에 `docker compose up` +- 데이터는 각 머신에 격리됨 +- 키 / 회의록 / 액션 아이템 모두 본인만 접근 + +**중앙 서버 배포는 권장하지 않음** — 인증 / 권한 / 격리 미구현 + +### 보안 체크리스트 (배포 전) +- [ ] `.env`에 강력한 `POSTGRES_PASSWORD` 설정 +- [ ] `docker-compose.yml`의 Postgres 포트 노출 (`5432:5432`) 사용 환경에 맞게 검토 (외부 노출 불필요 시 제거) +- [ ] HTTPS reverse proxy 추가 (사외 접속 시) +- [ ] 방화벽 / VPN 뒤에서만 접근 가능하도록 구성 +- [ ] 정기 백업 (`pg_dump`) + +자세한 보안 검토 결과는 [`SECURITY.md`](SECURITY.md) 참고. + +--- + +## ⚠️ 알려진 제약 + +| 제약 | 설명 | 대응 | +|---|---|---| +| **Chrome 전용** | Web Speech API는 비표준 — Safari / Firefox는 제한적 | Phase 4에서 서버 사이드 전사 검토 | +| **화자 구분 없음** | 의도적 비지원 (Non-goal) | — | +| **Gemini 무료 등급 한도** | 15 RPM / 1000 RPD | 한도 초과 시 자동 쿨다운, 단순 변환 폴백 | +| **단일 사용자** | 인증 없음, 데이터 격리 없음 | 1인 1인스턴스로 운용 | +| **마크다운 검색** | PostgreSQL `ILIKE` (수천 건 이상에서 느려질 수 있음) | 필요 시 `tsvector` 인덱스 추가 | +| **모바일 UX** | 데스크톱 우선 | 기본 동작은 가능, 폴리시 미흡 | + +--- + +## 🤝 기여하기 + +이슈 / PR 환영합니다. 기여 전 다음을 확인해주세요: + +1. [`docs/ROADMAP.md`](docs/ROADMAP.md)와 [Issues](https://github.com/nad4-su/meeting-minutes/issues)에서 중복 / 진행 중 항목 확인 +2. **테스트 통과 필수** — `docker compose --profile tools run --rm test` +3. **빌드 통과 필수** — `docker compose build app` +4. 기능 추가 시 단위 테스트 동반 권장 + +### 개발 가이드 + +```bash +# 변경 후 검증 +docker compose --profile tools run --rm test +docker compose build app + +# DB 스키마 변경 시 +# 1. prisma/schema.prisma 편집 +# 2. 마이그레이션 생성 +docker compose --profile tools run --rm \ + --entrypoint "" migrate \ + sh -c "npm ci && npx prisma migrate dev --name <변경_이름>" +``` + +### 커밋 컨벤션 +- `feat:` 새 기능 +- `fix:` 버그 수정 +- `refactor:` 리팩터링 +- `docs:` 문서 +- `test:` 테스트 +- `chore:` 빌드/설정 + +--- + +## 📄 라이선스 + +[MIT License](LICENSE) © 2026 nad4-su diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2f69e29 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,194 @@ +# 🔒 Security + +이 문서는 Meeting Minutes의 **위협 모델, 알려진 한계, 안전한 사용 방법**을 정리합니다. 보안 취약점을 발견했다면 [신고 방법](#-취약점-신고)을 참고해주세요. + +--- + +## 📑 목차 + +- [위협 모델](#-위협-모델) +- [신뢰 모델](#-신뢰-모델) +- [데이터 흐름과 외부 송출](#-데이터-흐름과-외부-송출) +- [구현된 보안 조치](#-구현된-보안-조치) +- [알려진 한계](#-알려진-한계) +- [배포 권장 사항](#-배포-권장-사항) +- [취약점 신고](#-취약점-신고) + +--- + +## 🎯 위협 모델 + +이 프로젝트는 **개인용 단일 인스턴스**를 가정합니다. 위협 모델은 다음과 같습니다: + +### In-scope (방어 대상) +- 같은 LAN/Wifi에 있는 다른 사용자가 사용자의 노트북에 접근하는 시나리오 +- 공유 PC에서 잠시 자리를 비웠을 때 다른 사람이 데이터에 접근하는 시나리오 +- 마크다운에 삽입된 악성 HTML/스크립트로 인한 XSS +- 클라이언트가 보낸 비정상 입력 (큰 텍스트, 잘못된 MIME 등) + +### Out-of-scope (방어하지 않음) +- **다중 사용자 / 인증** — 단일 사용자 가정으로 인증 시스템 미구현 +- **호스트 OS / 디스크 암호화** — 운영체제와 디스크 암호화 설정에 위임 +- **클라우드 환경 배포** — localhost 전용으로 설계됨 +- **물리적 접근 통제** — 사용자 PC에 대한 물리적 보안 + +--- + +## 🔐 신뢰 모델 + +| 신뢰 수준 | 항목 | +|---|---| +| **신뢰** | 호스트 OS, Docker, 사용자가 발급한 Gemini API 키, npm 패키지 (`marked`, `DOMPurify`, Prisma 등) | +| **반신뢰** | 사용자 입력 transcript / markdown (DOMPurify로 sanitize) | +| **신뢰 안 함** | 같은 네트워크의 다른 장치, 외부 HTTP 요청, Web Speech API 결과 | + +--- + +## 📡 데이터 흐름과 외부 송출 + +| 데이터 | 저장 위치 | 외부 송출 | +|---|---|---| +| 회의 transcript / 회의록 | PostgreSQL (`meetings.markdownMinutes`, `rawTranscript`) | Gemini AI 요약 활성화 시 transcript가 Google로 전송 | +| 액션 아이템 | PostgreSQL (`action_items`) | 외부 송출 없음 | +| Gemini API 키 | 브라우저 LocalStorage 또는 `.env` | 요청 시 Google에만 전송 | +| 음성 데이터 | 메모리 (실시간), `/app/uploads` (파일 업로드) | **Chrome Web Speech API → Google 서버** ⚠️ | + +### ⚠️ 주의: Web Speech API의 음성 외부 전송 + +Chrome의 `SpeechRecognition` API는 **음성 데이터를 Google 서버로 전송하여 처리**합니다. 이는 Chrome 자체의 동작이며 우리 서버를 경유하지 않습니다. 다음의 경우 사용을 재고하세요: + +- 회의가 회사 기밀, 인사 정보, 환자 정보 등을 포함 +- 사내 컴플라이언스 정책이 외부 클라우드 음성 전송을 금지 +- GDPR / HIPAA 등 규제 환경 + +이 경우 **파일 업로드 탭**도 같은 한계가 있으므로(현재 업로드 후 전사는 미구현, Phase 4에서 자체 STT 검토 예정), 이 도구의 사용을 보류하는 것을 권장합니다. + +--- + +## ✅ 구현된 보안 조치 + +### 인프라 +- **Postgres 포트 미노출** — `db` 컨테이너는 `app`만 내부 네트워크로 접근, 호스트 포트 안 열림 +- **App 포트 localhost 바인딩** — `127.0.0.1:3000` (LAN의 다른 장치는 접근 불가) +- **DB 비밀번호 fallback 제거** — `.env`에서 명시적으로 설정해야 docker-compose 시작됨 +- **Prisma adapter 사용** — Prisma 7 `@prisma/adapter-pg`로 안전한 PostgreSQL 연결 + +### 응답 헤더 +`next.config.ts`에서 다음 헤더 적용: +- `X-Content-Type-Options: nosniff` — MIME sniffing 차단 +- `X-Frame-Options: DENY` — 클릭재킹 방지 +- `Referrer-Policy: strict-origin-when-cross-origin` +- `Permissions-Policy: camera=(), microphone=(self), geolocation=(), interest-cohort=()` + +### 입력 검증 +- **마크다운**: `marked` → `isomorphic-dompurify` 통과 + - `USE_PROFILES: { html: true }` + - `FORBID_TAGS: ['style']`, `FORBID_ATTR: ['style']` + - `