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,245 @@
// src/main/services/MeetingDocTemplateService.ts
// Phase 14.5: 회의 문서 템플릿 서비스 (electron-store 기반)
import { EventEmitter } from 'events'
import { nanoid } from 'nanoid'
import Store from 'electron-store'
import { getLogger } from './LoggerService'
import { D3ROError, ErrorCode } from '@shared/errors'
import type {
MeetingDocTemplate,
CreateMeetingDocTemplateParams,
UpdateMeetingDocTemplateParams,
MeetingDocTemplateType,
} from '@shared/types'
const logger = getLogger('MeetingDocTemplateService')
// ── 빌트인 템플릿 ──
const BUILTIN_TEMPLATES: MeetingDocTemplate[] = [
{
id: 'builtin-minutes',
name: '회의록',
description: '결정사항, 할 일, 타임라인 구조의 표준 회의록',
templateType: 'minutes' as MeetingDocTemplateType,
systemPrompt: `당신은 전문 회의록 작성 비서입니다.
.
:
-
-
- ( )
:
##
(3-5 )
##
- ( 1)
- ( 2)
##
- [ ] ( ) ( )
##
| | |
|------|------|
| MM:SS | / |`,
isBuiltin: true,
createdAt: Date.now(),
updatedAt: Date.now(),
},
{
id: 'builtin-report',
name: '보고서',
description: '개요, 핵심 내용, 결론, 제안 구조의 보고서',
templateType: 'report' as MeetingDocTemplateType,
systemPrompt: `당신은 전문 비즈니스 보고서 작성 비서입니다.
.
:
-
-
-
:
##
( 2-3)
##
- ( 1)
- ( 2)
- ( 3)
##
( 2-3)
##
- ( 1)
- ( 2)`,
isBuiltin: true,
createdAt: Date.now(),
updatedAt: Date.now(),
},
{
id: 'builtin-idea-note',
name: '아이디어 노트',
description: '핵심 아이디어, 장단점, 우선순위, 다음 단계 구조',
templateType: 'idea-note' as MeetingDocTemplateType,
systemPrompt: `당신은 창의적 아이디어 정리 전문가입니다.
.
:
-
-
-
:
##
- ( 1)
- ( 2)
##
- ( 1)
- ( 2)
## /
- ( 1)
- ( 2)
##
1. ( )
2. ( )
##
- [ ] ( 1)
- [ ] ( 2)`,
isBuiltin: true,
createdAt: Date.now(),
updatedAt: Date.now(),
},
]
interface MeetingDocTemplateStoreSchema {
templates: MeetingDocTemplate[]
}
class MeetingDocTemplateService extends EventEmitter {
private _store: Store<MeetingDocTemplateStoreSchema>
constructor() {
super()
this._store = new Store<MeetingDocTemplateStoreSchema>({
name: 'meeting-doc-templates',
defaults: {
templates: [...BUILTIN_TEMPLATES],
},
})
this._ensureBuiltins()
}
// ── CRUD ──
getAll(): MeetingDocTemplate[] {
return this._store.get('templates', [])
}
getById(id: string): MeetingDocTemplate | null {
return this.getAll().find((t) => t.id === id) ?? null
}
create(params: CreateMeetingDocTemplateParams): MeetingDocTemplate {
const template: MeetingDocTemplate = {
id: nanoid(),
name: params.name,
description: params.description,
templateType: 'custom' as MeetingDocTemplateType,
systemPrompt: params.systemPrompt,
isBuiltin: false,
createdAt: Date.now(),
updatedAt: Date.now(),
}
const templates = this.getAll()
templates.push(template)
this._store.set('templates', templates)
logger.info(`회의 문서 템플릿 생성: ${template.id} (${template.name})`)
return template
}
update(params: UpdateMeetingDocTemplateParams): MeetingDocTemplate {
const templates = this.getAll()
const idx = templates.findIndex((t) => t.id === params.id)
if (idx === -1) {
throw new D3ROError(
ErrorCode.MeetingDocTemplateNotFound,
`템플릿을 찾을 수 없습니다: ${params.id}`,
)
}
const existing = templates[idx]
const updated: MeetingDocTemplate = {
...existing,
...(params.name !== undefined && { name: params.name }),
...(params.description !== undefined && { description: params.description }),
...(params.systemPrompt !== undefined && { systemPrompt: params.systemPrompt }),
updatedAt: Date.now(),
}
templates[idx] = updated
this._store.set('templates', templates)
logger.info(`회의 문서 템플릿 수정: ${params.id}`)
return updated
}
delete(id: string): void {
const templates = this.getAll()
const template = templates.find((t) => t.id === id)
if (!template) {
throw new D3ROError(
ErrorCode.MeetingDocTemplateNotFound,
`템플릿을 찾을 수 없습니다: ${id}`,
)
}
if (template.isBuiltin) {
throw new D3ROError(
ErrorCode.MeetingDocTemplateBuiltinDelete,
'빌트인 템플릿은 삭제할 수 없습니다',
)
}
this._store.set(
'templates',
templates.filter((t) => t.id !== id),
)
logger.info(`회의 문서 템플릿 삭제: ${id}`)
}
private _ensureBuiltins(): void {
const templates = this.getAll()
let changed = false
for (const builtin of BUILTIN_TEMPLATES) {
if (!templates.find((t) => t.id === builtin.id)) {
templates.push(builtin)
changed = true
}
}
if (changed) {
this._store.set('templates', templates)
}
}
dispose(): void {
this.removeAllListeners()
}
}
// ── 싱글톤 ──
let instance: MeetingDocTemplateService | null = null
export function getMeetingDocTemplateService(): MeetingDocTemplateService {
if (!instance) {
instance = new MeetingDocTemplateService()
}
return instance
}