Phase 14.5 구현: 회의 모드 고도화 — 다중 문서 생성/편집/내보내기

PLAUD 수준의 기능 확장:
- 전사 편집: 인라인 편집 + 원본 보존 + 수정됨 표시(점선 밑줄)
- 다중 문서 생성: 회의록/보고서/아이디어노트/커스텀 템플릿
- MD 에디터: 렌더링/편집 토글 (react-markdown + remark-gfm)
- 상세 페이지 리디자인: 풀스크린 탭 전환 구조
- 내보내기: MD, PDF, TXT, DOCX 4종 (docx 패키지)
- 커스텀 프롬프트 템플릿 저장/재사용 (electron-store)
- SSOT: 마크다운 파싱 유틸 추출, 후처리에서 자동 문서 생성 제거

DB: meeting_documents 테이블 + edited_transcript 컬럼
IPC: 8+4 채널, 서비스 2개(MeetingModeService 확장 + MeetingDocTemplateService 신규)
UI: 8개 신규 컴포넌트 (meeting/ 디렉토리), 12개 locale i18n
This commit is contained in:
Yun Chan 2026-04-08 09:53:54 +09:00
parent 6197ceb132
commit d4928ffa60
38 changed files with 4791 additions and 438 deletions

View file

@ -0,0 +1,514 @@
# Phase 14.5: 회의 모드 고도화 — 문서 생성/편집/내보내기
> PLAUD/Otter.ai 수준의 다중 문서 생성 + 전사 편집 + MD 에디터 + 다운로드 확장
> 기존 Phase 14 인프라(녹음+전사+메모+LLM) 위에 확장
---
## 1. 개요
### 1.1 사용자 플로우
```
[회의 종료 후 상세 페이지]
├── [Transcript 탭] ← 전사 원문 편집 + 원본/수정본 토글
│ ├── 세그먼트 클릭 → 인라인 편집 (점선 밑줄 + hover "수정됨")
│ ├── 원본 보기 토글 (rawTranscript ↔ editedTranscript)
│ └── [TXT 다운로드] 버튼
├── [+ 버튼] → 문서 생성 다이얼로그
│ ├── 템플릿 선택 (회의록 / 보고서 / 아이디어 노트 / 커스텀)
│ ├── 커스텀: 이름 + 프롬프트 입력 (저장/재사용 가능)
│ └── [생성] → LLM 호출 → 새 탭 추가
└── [문서 탭 (동적)] ← 회의록, 보고서 등
├── 미리보기 모드: react-markdown 렌더링
├── 편집 모드: 마크다운 원본 textarea
├── 토글 버튼 (미리보기 ↔ 편집)
├── debounce 자동 저장 (500ms)
└── [내보내기 ▼] 드롭다운 (MD / PDF / TXT / DOCX)
```
### 1.2 핵심 차별점
- **다중 문서**: 하나의 전사에서 여러 양식의 문서 생성 (PLAUD Multidimensional Summary)
- **전사 편집**: 녹음 중 + 종료 후 인라인 편집, 원본 보존, 수정 표시
- **커스텀 프롬프트**: 사용자 정의 AI 프롬프트 저장/재사용
- **4종 내보내기**: MD, PDF, TXT, DOCX
- **MD 에디터**: 렌더링/편집 토글
---
## 2. DB 스키마
### 2.1 `meeting_sessions` 테이블 확장
```sql
-- 마이그레이션: 전사 편집 원본 보존용 컬럼 추가
ALTER TABLE meeting_sessions ADD COLUMN edited_transcript TEXT;
```
### 2.2 `meeting_documents` 테이블 (신규)
```sql
CREATE TABLE IF NOT EXISTS meeting_documents (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
template_type TEXT NOT NULL, -- 'minutes' | 'report' | 'idea-note' | 'custom'
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
prompt_used TEXT, -- 커스텀 프롬프트 기록용
llm_model TEXT,
llm_latency_ms INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_meeting_documents_session_id ON meeting_documents(session_id);
```
### 2.3 drizzle-orm 스키마 (`src/main/db/schema.ts`)
```typescript
// meetingSessions에 추가
editedTranscript: text('edited_transcript'),
// 신규 테이블
export const meetingDocuments = sqliteTable(
'meeting_documents',
{
id: text('id').primaryKey(),
sessionId: text('session_id').notNull(),
templateType: text('template_type', {
enum: ['minutes', 'report', 'idea-note', 'custom'],
}).notNull(),
title: text('title').notNull(),
content: text('content').notNull().default(''),
promptUsed: text('prompt_used'),
llmModel: text('llm_model'),
llmLatencyMs: integer('llm_latency_ms'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull(),
},
(table) => [
index('idx_meeting_documents_session_id').on(table.sessionId),
]
)
```
---
## 3. 공유 타입 (`src/shared/types.ts`)
```typescript
// ============================================================
// Phase 14.5: Meeting Document Types
// ============================================================
export type MeetingDocTemplateType = 'minutes' | 'report' | 'idea-note' | 'custom'
export interface MeetingDocTemplate {
id: string
name: string
description: string
templateType: MeetingDocTemplateType
systemPrompt: string
isBuiltin: boolean
createdAt: number
updatedAt: number
}
export interface MeetingDocument {
id: string
sessionId: string
templateType: MeetingDocTemplateType
title: string
content: string
promptUsed: string | null
llmModel: string | null
llmLatencyMs: number | null
createdAt: number
updatedAt: number
}
export interface MeetingGenerateDocParams {
sessionId: string
templateId: string
customPrompt?: string
customTitle?: string
}
export interface MeetingGenerateDocResult {
document: MeetingDocument
}
export interface MeetingUpdateDocParams {
documentId: string
content: string
}
export interface MeetingDeleteDocParams {
documentId: string
}
export interface MeetingGetDocsParams {
sessionId: string
}
export interface MeetingUpdateTranscriptParams {
sessionId: string
editedTranscript: string
}
export type MeetingExportFormat = 'md' | 'pdf' | 'txt' | 'docx'
export interface MeetingExportDocParams {
documentId: string
format: MeetingExportFormat
}
export interface MeetingExportTranscriptParams {
sessionId: string
}
export interface MeetingDocGeneratingProgress {
sessionId: string
templateId: string
percent: number
}
export interface CreateMeetingDocTemplateParams {
name: string
description: string
systemPrompt: string
}
export interface UpdateMeetingDocTemplateParams {
id: string
name?: string
description?: string
systemPrompt?: string
}
export interface DeleteMeetingDocTemplateParams {
id: string
}
```
**MeetingSessionDetail 확장:**
```typescript
export interface MeetingSessionDetail extends MeetingSessionSummary {
rawTranscript: string | null
editedTranscript: string | null // 추가
minutesMarkdown: string | null
minutes: MeetingMinutes | null
memos: MeetingMemo[]
documents: MeetingDocument[] // 추가
sttModel: string | null
llmModel: string | null
errorMessage: string | null
}
```
---
## 4. 에러 코드 (`src/shared/errors.ts`)
```typescript
// === Phase 14.5: Meeting Enhancement (886-892) ===
MeetingDocumentNotFound = 886,
MeetingDocGenerationFailed = 887,
MeetingDocExportFailed = 888,
MeetingTranscriptUpdateFailed = 889,
MeetingDocTemplateNotFound = 890,
MeetingDocTemplateBuiltinDelete = 891,
MeetingDocxExportFailed = 892,
```
---
## 5. IPC 채널 (`src/shared/ipc-channels.ts`)
```typescript
// MEETING_MODE 블록에 추가
UPDATE_TRANSCRIPT: 'meetingMode:updateTranscript',
GENERATE_DOCUMENT: 'meetingMode:generateDocument',
GET_DOCUMENTS: 'meetingMode:getDocuments',
UPDATE_DOCUMENT: 'meetingMode:updateDocument',
DELETE_DOCUMENT: 'meetingMode:deleteDocument',
EXPORT_DOCUMENT: 'meetingMode:exportDocument',
EXPORT_TRANSCRIPT: 'meetingMode:exportTranscript',
DOC_GENERATING_PROGRESS: 'meetingMode:docGeneratingProgress',
// 신규 그룹
MEETING_DOC_TEMPLATE: {
GET_ALL: 'meetingDocTemplate:getAll',
CREATE: 'meetingDocTemplate:create',
UPDATE: 'meetingDocTemplate:update',
DELETE: 'meetingDocTemplate:delete',
},
```
---
## 6. 서비스 명세
### 6.1 MeetingDocTemplateService (신규)
**파일**: `src/main/services/MeetingDocTemplateService.ts`
**패턴**: electron-store 전용 인스턴스 (DictationTemplateService 패턴)
빌트인 3종:
```typescript
const BUILTIN_TEMPLATES: MeetingDocTemplate[] = [
{
id: 'builtin-minutes',
name: '회의록',
templateType: 'minutes',
systemPrompt: `전사록을 분석하여 구조화된 회의록을 작성하세요.
## 요약 (3-5줄)
## 핵심 결정사항 (- 항목)
## 할 일 목록 (- [ ] 담당자: 할일)
## 타임라인 (| 시간 | 내용 |)`,
isBuiltin: true,
},
{
id: 'builtin-report',
name: '보고서',
templateType: 'report',
systemPrompt: `전사록을 분석하여 제출용 보고서를 작성하세요.
## 개요 (배경, 목적)
## 핵심 내용 (주요 논의 사항)
## 결론 및 제안
## 첨부 참고사항`,
isBuiltin: true,
},
{
id: 'builtin-idea-note',
name: '아이디어 노트',
templateType: 'idea-note',
systemPrompt: `전사록에서 아이디어와 인사이트를 추출하세요.
## 핵심 아이디어 (번호 매기기)
## 각 아이디어의 장단점
## 우선순위 제안
## 다음 단계`,
isBuiltin: true,
},
]
```
메서드: `getAll()`, `getById()`, `create()`, `update()`, `delete()`
### 6.2 MeetingModeService 확장
**새 메서드:**
| 메서드 | 설명 |
|--------|------|
| `updateTranscript(sessionId, editedTranscript)` | 수정 전사 저장 |
| `generateDocument(params)` | 전사+템플릿→LLM→meeting_documents 저장 |
| `getDocuments(sessionId)` | 세션의 문서 목록 |
| `updateDocument(documentId, content)` | MD 편집 저장 |
| `deleteDocument(documentId)` | 문서 삭제 |
| `exportDocument(documentId, format)` | MD/PDF/TXT/DOCX 내보내기 |
| `exportTranscript(sessionId)` | 전사 TXT 다운로드 |
**후처리 변경:** `_runPostProcessing()`에서 자동 회의록 생성 제거. 전사 합산 + DB 저장(rawTranscript, status=completed)만 수행. 사용자가 [+]로 직접 문서 생성.
**_loadSessionDetail 확장:** editedTranscript + documents[] 포함
### 6.3 유틸 추출 (SSOT)
**`src/main/utils/meeting-markdown.ts`**:
- `parseMinutes(md)` — MeetingModeService._parseMinutes() 추출
- `buildExportMarkdown(session)` — _buildExportMarkdown() 추출
- `markdownToSimpleHtml(md)` — _markdownToSimpleHtml() 추출
**`src/main/utils/markdown-to-docx.ts`**:
- `markdownToDocx(md, title): Promise<Buffer>` — docx 패키지
---
## 7. Preload API (`src/preload/index.ts`)
```typescript
// meetingMode 확장
updateTranscript: (params) => invoke(ch.UPDATE_TRANSCRIPT, params),
generateDocument: (params) => invoke(ch.GENERATE_DOCUMENT, params),
getDocuments: (params) => invoke(ch.GET_DOCUMENTS, params),
updateDocument: (params) => invoke(ch.UPDATE_DOCUMENT, params),
deleteDocument: (params) => invoke(ch.DELETE_DOCUMENT, params),
exportDocument: (params) => invoke(ch.EXPORT_DOCUMENT, params),
exportTranscript: (params) => invoke(ch.EXPORT_TRANSCRIPT, params),
onDocGeneratingProgress: (cb) => on(ch.DOC_GENERATING_PROGRESS, cb),
// 신규 그룹
meetingDocTemplate: {
getAll: () => invoke(ch.GET_ALL),
create: (params) => invoke(ch.CREATE, params),
update: (params) => invoke(ch.UPDATE, params),
delete: (params) => invoke(ch.DELETE, params),
},
```
---
## 8. UI 컴포넌트
### 8.1 컴포넌트 디렉토리: `src/renderer/components/meeting/`
| 컴포넌트 | 설명 |
|----------|------|
| `EditableSegment.tsx` | 전사 세그먼트 인라인 편집. 클릭→TextField, 점선 밑줄, hover "수정됨" 툴팁 |
| `MarkdownRenderer.tsx` | react-markdown + remark-gfm 래퍼. d3roPalette 스타일 |
| `MarkdownEditor.tsx` | 미리보기/편집 토글. 편집 모드=textarea, debounce 자동저장(500ms) |
| `ExportMenu.tsx` | MD/PDF/TXT/DOCX 드롭다운 |
| `TranscriptTab.tsx` | 전사 편집 탭. 원본/수정본 토글, EditableSegment 목록, TXT 다운로드 |
| `DocumentTab.tsx` | 문서 탭. MarkdownEditor + ExportMenu + 삭제 |
| `AddDocumentDialog.tsx` | [+] 버튼 다이얼로그. 템플릿 선택(빌트인3+커스텀), 프롬프트 입력, 생성 |
| `MeetingDetailTabs.tsx` | 탭 바 + 콘텐츠 라우팅 컨테이너 |
### 8.2 상세 뷰 레이아웃
```
┌──────────────────────────────────────────────────┐
│ [←] 프로젝트 킥오프 ✏️ [Export ▼] [🗑] │
│ 2026-04-06 14:30 ~ 15:12 · 42분 │
├──────────────────────────────────────────────────┤
│ [📝 Transcript] [회의록] [보고서] [+ 추가] │
├──────────────────────────────────────────────────┤
│ │
│ (선택된 탭의 풀스크린 콘텐츠) │
│ │
│ Transcript탭: EditableSegment 목록 │
│ 문서탭: MarkdownEditor (렌더링↔편집 토글) │
│ │
└──────────────────────────────────────────────────┘
```
### 8.3 녹음 뷰 세그먼트 편집
```
┌─ 실시간 전사 ScreenPanel ────────────────┐
│ [00:15] 안녕하세요 오늘 회의를... │ ← 클릭 시 편집 가능
│ ✏̣[̣0̣1̣:̣3̣0̣]̣ ̣예̣산̣ ̣관̣련̣인̣데̣요̣.̣.̣.̣ │ ← 수정됨: 점선 밑줄
│ [02:00] 다음 안건은... │
└──────────────────────────────────────────┘
```
---
## 9. i18n 키
### ko.json 추가
```json
"meeting.transcriptTab": "전사",
"meeting.addDocument": "문서 추가",
"meeting.selectTemplate": "템플릿 선택",
"meeting.generate": "생성",
"meeting.generating": "문서 생성 중...",
"meeting.editMode": "편집",
"meeting.previewMode": "미리보기",
"meeting.modified": "수정됨",
"meeting.originalText": "원본 보기",
"meeting.editedText": "수정본 보기",
"meeting.exportDocx": "DOCX 내보내기",
"meeting.exportTxt": "TXT 내보내기",
"meeting.exportFormat": "내보내기 형식",
"meeting.customPrompt": "커스텀 프롬프트",
"meeting.templateName": "템플릿 이름",
"meeting.templatePrompt": "AI 프롬프트",
"meeting.templateSave": "템플릿 저장",
"meeting.documentDeleted": "문서가 삭제되었습니다",
"meeting.autoSaved": "자동 저장됨",
"meeting.minutes": "회의록",
"meeting.report": "보고서",
"meeting.ideaNote": "아이디어 노트",
"meeting.custom": "커스텀"
```
---
## 10. 구현 순서
### Step A: 기반 인프라 (타입 + DB + IPC 채널 + 에러코드)
1. `src/shared/types.ts` — 새 타입 추가 + MeetingSessionDetail 확장
2. `src/shared/ipc-channels.ts` — MEETING_MODE 확장 + MEETING_DOC_TEMPLATE 추가
3. `src/shared/errors.ts` — 886-892 추가
4. `src/main/db/schema.ts` — meetingDocuments + editedTranscript
5. `src/main/db/index.ts` — 마이그레이션 SQL
### Step B: npm 패키지 설치
6. `npm install react-markdown remark-gfm docx`
### Step C: 유틸 추출 (SSOT)
7. `src/main/utils/meeting-markdown.ts` — 파싱/내보내기 로직 추출
8. `src/main/utils/markdown-to-docx.ts` — DOCX 변환
### Step D: 서비스 계층
9. `src/main/services/MeetingDocTemplateService.ts` — 신규
10. `src/main/services/MeetingModeService.ts` — 확장 (7 메서드 + 후처리 변경 + _loadSessionDetail)
### Step E: IPC + Preload
11. `src/main/ipc/meeting-doc-template-handlers.ts` — 신규
12. `src/main/ipc/meeting-mode-handlers.ts` — 확장
13. `src/main/ipc/index.ts` — import 추가
14. `src/preload/index.ts` — API 확장
### Step F: UI 컴포넌트
15-22. `src/renderer/components/meeting/` — 8개 컴포넌트
### Step G: 페이지 통합
23. `src/renderer/pages/MeetingModePage.tsx` — 상세 뷰 리디자인 + 녹음 뷰 편집
### Step H: i18n
24. 12개 locale 키 추가
### Step I: 마무리
25. typecheck + build + test
26. bootstrap 초기화 단계 추가 (MeetingDocTemplateService)
---
## 11. TODO 항목 (향후 구현)
- [ ] AI 채팅: 회의록 레퍼런스 기반 Q&A (여러 레퍼런스 선택 + 대화)
- [ ] 웹 공유: 만료 링크 공유 (웹 서비스 연동 대비 설계)
- [ ] 마인드맵: AI 시각적 마인드맵 생성 (JPEG/SVG 내보내기)
- [ ] Auto Polish: AI 전사 다듬기 (필러 제거, 가독성 향상)
- [ ] 화자 구분: 스피커 라벨링 + 이름 지정
---
## 파일 목록
### 신규 (12개)
| 파일 | 설명 |
|------|------|
| `src/main/services/MeetingDocTemplateService.ts` | 문서 템플릿 CRUD |
| `src/main/ipc/meeting-doc-template-handlers.ts` | 템플릿 IPC |
| `src/main/utils/meeting-markdown.ts` | MD 파싱 유틸 |
| `src/main/utils/markdown-to-docx.ts` | DOCX 변환 |
| `src/renderer/components/meeting/MeetingDetailTabs.tsx` | 탭 컨테이너 |
| `src/renderer/components/meeting/TranscriptTab.tsx` | 전사 편집 탭 |
| `src/renderer/components/meeting/DocumentTab.tsx` | 문서 뷰/편집 탭 |
| `src/renderer/components/meeting/AddDocumentDialog.tsx` | 문서 생성 다이얼로그 |
| `src/renderer/components/meeting/MarkdownRenderer.tsx` | MD 렌더러 래퍼 |
| `src/renderer/components/meeting/MarkdownEditor.tsx` | 렌더링/편집 토글 |
| `src/renderer/components/meeting/EditableSegment.tsx` | 인라인 편집 세그먼트 |
| `src/renderer/components/meeting/ExportMenu.tsx` | 내보내기 드롭다운 |
### 수정 (10+12 locale)
| 파일 | 변경 |
|------|------|
| `src/shared/types.ts` | 새 타입 + MeetingSessionDetail 확장 |
| `src/shared/ipc-channels.ts` | 8+4 채널 |
| `src/shared/errors.ts` | 7개 에러 코드 |
| `src/main/db/schema.ts` | meetingDocuments + editedTranscript |
| `src/main/db/index.ts` | 마이그레이션 |
| `src/main/services/MeetingModeService.ts` | 7 메서드 + 후처리 변경 |
| `src/main/ipc/meeting-mode-handlers.ts` | 7 핸들러 |
| `src/main/ipc/index.ts` | import 추가 |
| `src/preload/index.ts` | API 확장 |
| `src/renderer/pages/MeetingModePage.tsx` | 상세 뷰 리디자인 |
| `src/renderer/i18n/*.json` | 12개 locale |