Several desktop paths quietly substituted defaults or partial results: a config write could fall back to a throwaway in-memory store, speech provider errors were absorbed into empty transcriptions, and meeting exports built file names from raw titles. Writes now fail explicitly when the store is unavailable, provider and model failures reach the UI as errors, and export names pass through one sanitizer. Settings, license, ad, and support surfaces use the shared theme tokens, unused hotkey helpers are gone, and the package gains strict node/renderer typecheck configs plus red-team e2e scenarios for these flows.
274 lines
7.2 KiB
TypeScript
274 lines
7.2 KiB
TypeScript
// src/main/services/MeetingDocTemplateService.ts
|
|
// Phase 14.5: 회의 문서 템플릿 서비스 (electron-store 기반)
|
|
|
|
import { EventEmitter } from 'events'
|
|
import Store from 'electron-store'
|
|
import { getLogger } from './LoggerService'
|
|
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
|
import type {
|
|
MeetingDocTemplate,
|
|
CreateMeetingDocTemplateParams,
|
|
UpdateMeetingDocTemplateParams,
|
|
MeetingDocTemplateType,
|
|
} from '@d3ro/core/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(),
|
|
},
|
|
{
|
|
id: 'builtin-mindmap',
|
|
name: '마인드맵',
|
|
description: '전사 내용의 핵심 구조를 시각적 마인드맵으로 정리',
|
|
templateType: 'mindmap' as MeetingDocTemplateType,
|
|
systemPrompt: `전사록을 분석하여 마인드맵을 마크다운 계층 구조로 작성하세요.
|
|
|
|
# 중심 주제 (회의의 핵심 주제)
|
|
## 주요 주제 1
|
|
- 세부 항목 A
|
|
- 세부 항목 B
|
|
- 하위 항목
|
|
## 주요 주제 2
|
|
- 세부 항목 C
|
|
## 결론 및 다음 단계
|
|
- 액션 아이템
|
|
|
|
규칙:
|
|
- 계층은 최대 4단계까지
|
|
- 각 항목은 간결하게 (한 줄)
|
|
- 전사록에 없는 내용 추가 금지
|
|
- 한국어로 작성`,
|
|
isBuiltin: true,
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
},
|
|
]
|
|
|
|
type 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: crypto.randomUUID(),
|
|
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
|
|
}
|
|
|
|
export function resetMeetingDocTemplateServiceForTests(): void {
|
|
instance = null
|
|
}
|