// src/main/services/CustomInstructionService.ts // 사용자 정의 LLM 명령어 관리. 설계서 01/Phase 6 참조. // electron-store에 저장, 프리셋 5개 기본 제공. import { getLogger } from './LoggerService' import { configGet, configSet } from './ConfigService' import { D3ROError, ErrorCode } from '@d3ro/core/errors' import type { CustomInstruction } from '@d3ro/core/types' const logger = getLogger('CustomInstructionService') type CreateInput = Omit< CustomInstruction, 'id' | 'isBuiltin' | 'order' | 'createdAt' | 'updatedAt' | 'icon' > & { icon?: string } // ============================================================ // 프리셋 명령어 (Phase 6 설계) // ============================================================ const BUILTIN_INSTRUCTIONS: ReadonlyArray> = [ { id: 'builtin-translate', name: '번역', description: '텍스트를 다른 언어로 번역', prompt: '다음 텍스트를 {{targetLanguage}}로 번역해주세요.\n자연스럽고 정확한 번역만 출력하세요.', icon: 'Translate', isBuiltin: true, order: 0 }, { id: 'builtin-summarize', name: '요약', description: '핵심 내용을 3줄 이내로 요약', prompt: '다음 텍스트의 핵심 내용을 3줄 이내로 요약해주세요.\n요약문만 출력하세요.', icon: 'Summarize', isBuiltin: true, order: 1 }, { id: 'builtin-formal', name: '전문 리라이트', description: '격식 있는 비즈니스 문체로 변환', prompt: '다음 텍스트를 격식 있는 비즈니스 문체로 다시 작성해주세요.\n원래 의미를 유지하면서 전문적인 톤으로 변환하세요.\n다시 작성된 텍스트만 출력하세요.', icon: 'Business', isBuiltin: true, order: 2 }, { id: 'builtin-explain-code', name: '코드 설명', description: '코드를 한국어로 설명', prompt: '다음 코드를 한국어로 설명해주세요.\n각 부분이 무엇을 하는지 간결하게 설명하세요.', icon: 'Code', isBuiltin: true, order: 3 }, { id: 'builtin-free-prompt', name: '자유 프롬프트', description: '직접 프롬프트를 입력', prompt: '{{userPrompt}}', icon: 'Edit', isBuiltin: true, order: 4 } ] // ============================================================ // 저장소 (electron-store 사용) // ============================================================ // electron-store 대신 간단한 JSON 파일 사용 (ConfigService와 별도) // 실제로는 electron-store의 별도 인스턴스를 사용하지만, // Phase 6에서는 메모리 + configGet/configSet 패턴으로 단순화 let instructions: CustomInstruction[] = [] let initialized = false function loadInstructions(): CustomInstruction[] { // electron-store에서 로드 시도 try { const stored = configGet('customInstructions') as CustomInstruction[] | undefined if (Array.isArray(stored) && stored.length > 0) { return stored } } catch { // 첫 실행 시 키가 없을 수 있음 } // 프리셋으로 초기화 const now = Date.now() return BUILTIN_INSTRUCTIONS.map((b) => ({ ...b, createdAt: now, updatedAt: now })) } function saveInstructions(): void { try { configSet('customInstructions', instructions) } catch (error) { logger.warn(`Failed to save instructions: ${error instanceof Error ? error.message : String(error)}`) } } // ============================================================ // CustomInstructionService // ============================================================ class CustomInstructionService { initialize(): void { if (initialized) return instructions = loadInstructions() initialized = true logger.info(`CustomInstructionService initialized (${instructions.length} instructions)`) } getAll(): CustomInstruction[] { return [...instructions].sort((a, b) => a.order - b.order) } getById(id: string): CustomInstruction | null { return instructions.find((i) => i.id === id) ?? null } create(input: CreateInput): CustomInstruction { if (!input.name.trim()) { throw new D3ROError(ErrorCode.ConfigInvalidValue, 'Instruction name is empty') } if (!input.prompt.trim()) { throw new D3ROError(ErrorCode.ConfigInvalidValue, 'Instruction prompt is empty') } const now = Date.now() const instruction: CustomInstruction = { id: crypto.randomUUID(), name: input.name.trim(), description: input.description, prompt: input.prompt, icon: input.icon || 'Extension', isBuiltin: false, order: instructions.length, createdAt: now, updatedAt: now } instructions.push(instruction) saveInstructions() logger.info(`Custom instruction created: "${instruction.name}"`) return instruction } update(id: string, data: Partial>): CustomInstruction | null { const index = instructions.findIndex((i) => i.id === id) if (index === -1) return null const existing = instructions[index] // 프리셋은 프롬프트만 수정 가능 if (existing.isBuiltin) { if (data.prompt !== undefined) { instructions[index] = { ...existing, prompt: data.prompt, updatedAt: Date.now() } } } else { instructions[index] = { ...existing, ...data, updatedAt: Date.now() } } saveInstructions() return instructions[index] } delete(id: string): boolean { const index = instructions.findIndex((i) => i.id === id) if (index === -1) return false // 프리셋은 삭제 불가 if (instructions[index].isBuiltin) { logger.warn(`Cannot delete builtin instruction: ${id}`) return false } instructions.splice(index, 1) saveInstructions() logger.info(`Custom instruction deleted: ${id}`) return true } reorder(ids: string[]): void { const reordered: CustomInstruction[] = [] for (let i = 0; i < ids.length; i++) { const inst = instructions.find((item) => item.id === ids[i]) if (inst) { reordered.push({ ...inst, order: i }) } } // ids에 포함되지 않은 항목 추가 for (const inst of instructions) { if (!ids.includes(inst.id)) { reordered.push({ ...inst, order: reordered.length }) } } instructions = reordered saveInstructions() } resetBuiltins(): void { const now = Date.now() const userInstructions = instructions.filter((i) => !i.isBuiltin) const builtins = BUILTIN_INSTRUCTIONS.map((b) => ({ ...b, createdAt: now, updatedAt: now })) instructions = [...builtins, ...userInstructions] saveInstructions() logger.info('Builtin instructions reset') } dispose(): void { logger.info('CustomInstructionService disposed') } } let instance: CustomInstructionService | null = null export function getCustomInstructionService(): CustomInstructionService { if (!instance) { instance = new CustomInstructionService() } return instance } export function resetCustomInstructionServiceForTests(): void { instance = null initialized = false instructions = [] }