d3ro-voice/apps/desktop/src/main/services/MeetingDocTemplateService.ts
yunchan8804 45a580878a feat(V2-1a): Monorepo 구조 전환 — apps/desktop으로 V1 이동
- npm workspaces 루트 (apps/*, packages/*) 세팅
- V1 전체를 apps/desktop/으로 git mv (src, resources, tests, sidecar,
  scripts, electron.vite.config.ts, electron-builder.yml, vitest.config.ts,
  tsconfig.node.json, tsconfig.web.json)
- apps/desktop/package.json 신규 (name=@d3ro/desktop)
- productName: 'd3ro-voice' 명시 — app.getName()을 고정하여 userData 경로
  %APPDATA%\d3ro-voice\ 그대로 유지 (기존 DB/설정 연속성 보장)
- 루트 package.json을 workspace 루트로 재구성, 공통 devDep만 유지
  (typescript, eslint, prettier)
- turbo.json, tsconfig.base.json 추가 (Turborepo 자체 설치는 별도 sub-phase)
- memory/project_status.md 생성 (규칙 13)

검증:
- npm run typecheck 통과
- npm run build 통과 (electron-vite main+preload+renderer)
- npm run dev 실제 실행 → DB/핫키/Ollama 자동 실행 모두 정상
2026-04-08 14:04:41 +09:00

271 lines
7.1 KiB
TypeScript

// 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<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
}