// 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(), }, { id: 'builtin-mindmap', name: '마인드맵', description: '전사 내용의 핵심 구조를 시각적 마인드맵으로 정리', templateType: 'mindmap' as MeetingDocTemplateType, systemPrompt: `전사록을 분석하여 마인드맵을 마크다운 계층 구조로 작성하세요. # 중심 주제 (회의의 핵심 주제) ## 주요 주제 1 - 세부 항목 A - 세부 항목 B - 하위 항목 ## 주요 주제 2 - 세부 항목 C ## 결론 및 다음 단계 - 액션 아이템 규칙: - 계층은 최대 4단계까지 - 각 항목은 간결하게 (한 줄) - 전사록에 없는 내용 추가 금지 - 한국어로 작성`, isBuiltin: true, createdAt: Date.now(), updatedAt: Date.now(), }, ] interface MeetingDocTemplateStoreSchema { templates: MeetingDocTemplate[] } class MeetingDocTemplateService extends EventEmitter { private _store: Store constructor() { super() this._store = new Store({ 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 }