Phase 6 구현: 커스텀 명령어 + i18n (ko/en)
- CustomInstructionService: 프리셋 5개 (번역/요약/전문리라이트/코드설명/자유프롬프트) - 커스텀 명령어 CRUD + 프리셋 보호 (삭제 불가) - CommandsPage: 명령어 목록 + 추가/편집 다이얼로그 - i18n: ko.json/en.json 리소스 파일, t() 함수, React 컨텍스트 - IPC: instruction 핸들러 6개 - AppLayout: Commands 네비게이션 추가
This commit is contained in:
parent
5ccbf85a65
commit
d940c5020e
11 changed files with 701 additions and 5 deletions
242
src/main/services/CustomInstructionService.ts
Normal file
242
src/main/services/CustomInstructionService.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
// src/main/services/CustomInstructionService.ts
|
||||
// 사용자 정의 LLM 명령어 관리. 설계서 01/Phase 6 참조.
|
||||
// electron-store에 저장, 프리셋 5개 기본 제공.
|
||||
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getLogger } from './LoggerService'
|
||||
import { configGet } from './ConfigService'
|
||||
|
||||
const logger = getLogger('CustomInstructionService')
|
||||
|
||||
// ============================================================
|
||||
// 타입
|
||||
// ============================================================
|
||||
|
||||
export interface CustomInstruction {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
prompt: string
|
||||
icon: string
|
||||
isBuiltin: boolean
|
||||
order: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
type CreateInput = Omit<CustomInstruction, 'id' | 'isBuiltin' | 'order' | 'createdAt' | 'updatedAt'>
|
||||
|
||||
// ============================================================
|
||||
// 프리셋 명령어 (Phase 6 설계)
|
||||
// ============================================================
|
||||
|
||||
const BUILTIN_INSTRUCTIONS: ReadonlyArray<Omit<CustomInstruction, 'createdAt' | 'updatedAt'>> = [
|
||||
{
|
||||
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 never) 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 {
|
||||
const { configSet } = require('./ConfigService')
|
||||
configSet('customInstructions' as never, instructions as never)
|
||||
} 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 {
|
||||
const now = Date.now()
|
||||
const instruction: CustomInstruction = {
|
||||
id: nanoid(),
|
||||
name: input.name,
|
||||
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<Omit<CustomInstruction, 'id' | 'isBuiltin' | 'createdAt'>>): 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue