Skip to content

Commit 7c99bbd

Browse files
committed
feat: 체인 관리 기능 추가 및 히스토리 페이지 업데이트
- 체인 템플릿 저장, 업데이트, 삭제 기능을 IPC 핸들러로 추가 - 체인 실행 기능을 IPC 핸들러로 구현하여 사용자 요청 처리 - 기본 체인 템플릿을 스토어에 추가하여 초기화 - 히스토리 페이지에서 체인 작업 유형을 지원하도록 렌더링 로직 수정 - 사이드바에 체인 빌더 페이지 링크 추가 및 관련 아이콘 추가
1 parent 7ed5f7d commit 7c99bbd

File tree

11 files changed

+1461
-28
lines changed

11 files changed

+1461
-28
lines changed

src/main/chainExecutor.ts

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import NodeRSA from 'node-rsa';
2+
import { ChainStep, ChainStepResult, ChainExecutionResult, ChainStepType, SavedKey } from '../shared/types';
3+
import Store from 'electron-store';
4+
5+
interface StoreData {
6+
keys: SavedKey[];
7+
}
8+
9+
const store = new Store<StoreData>();
10+
11+
export class ChainExecutor {
12+
private keys: SavedKey[] = [];
13+
14+
constructor() {
15+
this.loadKeys();
16+
}
17+
18+
private loadKeys(): void {
19+
this.keys = (store as any).get('keys', []);
20+
}
21+
22+
private getKey(keyId: string): SavedKey | null {
23+
return this.keys.find(key => key.id === keyId) || null;
24+
}
25+
26+
private async executeStep(step: ChainStep, input: string): Promise<ChainStepResult> {
27+
const startTime = Date.now();
28+
const result: ChainStepResult = {
29+
stepId: step.id,
30+
stepType: step.type,
31+
input,
32+
output: '',
33+
success: false,
34+
duration: 0,
35+
};
36+
37+
try {
38+
let output: string;
39+
40+
switch (step.type) {
41+
case 'url-encode':
42+
output = encodeURIComponent(input);
43+
break;
44+
45+
case 'url-decode':
46+
output = decodeURIComponent(input);
47+
break;
48+
49+
case 'base64-encode':
50+
output = Buffer.from(input, 'utf8').toString('base64');
51+
break;
52+
53+
case 'base64-decode':
54+
output = Buffer.from(input, 'base64').toString('utf8');
55+
break;
56+
57+
case 'rsa-encrypt':
58+
output = await this.rsaEncrypt(input, step);
59+
break;
60+
61+
case 'rsa-decrypt':
62+
output = await this.rsaDecrypt(input, step);
63+
break;
64+
65+
default:
66+
throw new Error(`Unsupported step type: ${step.type}`);
67+
}
68+
69+
result.output = output;
70+
result.success = true;
71+
} catch (error) {
72+
result.error = error instanceof Error ? error.message : 'Unknown error occurred';
73+
result.success = false;
74+
result.output = input; // Pass through input on error
75+
} finally {
76+
result.duration = Date.now() - startTime;
77+
}
78+
79+
return result;
80+
}
81+
82+
private async rsaEncrypt(input: string, step: ChainStep): Promise<string> {
83+
const keyId = step.params?.keyId;
84+
if (!keyId) {
85+
throw new Error('RSA encryption requires a key ID');
86+
}
87+
88+
const key = this.getKey(keyId);
89+
if (!key) {
90+
throw new Error(`Key with ID ${keyId} not found`);
91+
}
92+
93+
const algorithm = step.params?.algorithm || 'RSA-OAEP';
94+
const rsaKey = new NodeRSA();
95+
96+
try {
97+
rsaKey.importKey(key.publicKey, 'pkcs8-public-pem');
98+
99+
if (algorithm === 'RSA-OAEP') {
100+
rsaKey.setOptions({
101+
encryptionScheme: 'pkcs1_oaep',
102+
} as any);
103+
} else if (algorithm === 'RSA-PKCS1') {
104+
rsaKey.setOptions({
105+
encryptionScheme: 'pkcs1',
106+
});
107+
}
108+
109+
const encrypted = rsaKey.encrypt(input, 'base64');
110+
return encrypted;
111+
} catch (error) {
112+
throw new Error(`RSA encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
113+
}
114+
}
115+
116+
private async rsaDecrypt(input: string, step: ChainStep): Promise<string> {
117+
const keyId = step.params?.keyId;
118+
if (!keyId) {
119+
throw new Error('RSA decryption requires a key ID');
120+
}
121+
122+
const key = this.getKey(keyId);
123+
if (!key) {
124+
throw new Error(`Key with ID ${keyId} not found`);
125+
}
126+
127+
const algorithm = step.params?.algorithm || 'RSA-OAEP';
128+
const rsaKey = new NodeRSA();
129+
130+
try {
131+
rsaKey.importKey(key.privateKey, 'pkcs8-private-pem');
132+
133+
if (algorithm === 'RSA-OAEP') {
134+
rsaKey.setOptions({
135+
encryptionScheme: 'pkcs1_oaep',
136+
} as any);
137+
} else if (algorithm === 'RSA-PKCS1') {
138+
rsaKey.setOptions({
139+
encryptionScheme: 'pkcs1',
140+
});
141+
}
142+
143+
const decrypted = rsaKey.decrypt(input, 'utf8');
144+
return decrypted;
145+
} catch (error) {
146+
throw new Error(`RSA decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
147+
}
148+
}
149+
150+
public async executeChain(
151+
steps: ChainStep[],
152+
inputText: string,
153+
templateId?: string,
154+
templateName?: string
155+
): Promise<ChainExecutionResult> {
156+
const startTime = Date.now();
157+
const executionId = crypto.randomUUID();
158+
const stepResults: ChainStepResult[] = [];
159+
let currentInput = inputText;
160+
let overallSuccess = true;
161+
162+
// Refresh keys before execution
163+
this.loadKeys();
164+
165+
// Filter enabled steps and execute them sequentially
166+
const enabledSteps = steps.filter(step => step.enabled);
167+
168+
for (const step of enabledSteps) {
169+
const stepResult = await this.executeStep(step, currentInput);
170+
stepResults.push(stepResult);
171+
172+
if (stepResult.success) {
173+
currentInput = stepResult.output;
174+
} else {
175+
overallSuccess = false;
176+
// Stop execution on first failure
177+
break;
178+
}
179+
}
180+
181+
const totalDuration = Date.now() - startTime;
182+
183+
const result: ChainExecutionResult = {
184+
id: executionId,
185+
templateId,
186+
templateName,
187+
success: overallSuccess,
188+
steps: stepResults,
189+
finalOutput: overallSuccess ? currentInput : inputText,
190+
totalDuration,
191+
timestamp: new Date(),
192+
inputText,
193+
};
194+
195+
return result;
196+
}
197+
198+
public validateChain(steps: ChainStep[]): { valid: boolean; errors: string[] } {
199+
const errors: string[] = [];
200+
const enabledSteps = steps.filter(step => step.enabled);
201+
202+
if (enabledSteps.length === 0) {
203+
errors.push('At least one step must be enabled');
204+
return { valid: false, errors };
205+
}
206+
207+
for (const step of enabledSteps) {
208+
// Validate RSA steps have required keys
209+
if ((step.type === 'rsa-encrypt' || step.type === 'rsa-decrypt') && !step.params?.keyId) {
210+
errors.push(`Step "${step.name || step.type}" requires a key to be selected`);
211+
}
212+
213+
// Validate key exists
214+
if (step.params?.keyId) {
215+
const key = this.getKey(step.params.keyId);
216+
if (!key) {
217+
errors.push(`Step "${step.name || step.type}" references a key that no longer exists`);
218+
}
219+
}
220+
}
221+
222+
return { valid: errors.length === 0, errors };
223+
}
224+
225+
public getAvailableModules(): Record<ChainStepType, { name: string; description: string; category: string; requiredParams?: string[] }> {
226+
return {
227+
'url-encode': {
228+
name: 'URL 인코딩',
229+
description: 'URL 안전 문자로 인코딩합니다',
230+
category: 'encoding',
231+
},
232+
'url-decode': {
233+
name: 'URL 디코딩',
234+
description: 'URL 인코딩된 문자를 디코딩합니다',
235+
category: 'encoding',
236+
},
237+
'base64-encode': {
238+
name: 'Base64 인코딩',
239+
description: 'Base64로 인코딩합니다',
240+
category: 'encoding',
241+
},
242+
'base64-decode': {
243+
name: 'Base64 디코딩',
244+
description: 'Base64를 디코딩합니다',
245+
category: 'encoding',
246+
},
247+
'rsa-encrypt': {
248+
name: 'RSA 암호화',
249+
description: 'RSA 공개키로 암호화합니다',
250+
category: 'crypto',
251+
requiredParams: ['keyId'],
252+
},
253+
'rsa-decrypt': {
254+
name: 'RSA 복호화',
255+
description: 'RSA 개인키로 복호화합니다',
256+
category: 'crypto',
257+
requiredParams: ['keyId'],
258+
},
259+
};
260+
}
261+
}
262+
263+
// Export singleton instance
264+
export const chainExecutor = new ChainExecutor();

src/main/main.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@ import NodeRSA from 'node-rsa';
55
import * as forge from 'node-forge';
66
import * as fs from 'fs';
77
import { autoUpdater } from 'electron-updater';
8-
import { SavedKey, RSAKeyPair, EncryptionResult, HistoryItem, HistoryFilter } from '../shared/types';
9-
import { IPC_CHANNELS } from '../shared/constants';
8+
import { SavedKey, RSAKeyPair, EncryptionResult, HistoryItem, HistoryFilter, ChainStep, ChainExecutionResult, ChainTemplate } from '../shared/types';
9+
import { IPC_CHANNELS, CHAIN_MODULES, DEFAULT_CHAIN_TEMPLATES } from '../shared/constants';
10+
import { chainExecutor } from './chainExecutor';
1011

1112
// Type-safe store interface
1213
interface StoreData {
1314
keys: SavedKey[];
1415
history: HistoryItem[];
16+
chainTemplates: ChainTemplate[];
1517
}
1618

1719
const store = new Store({
1820
defaults: {
1921
keys: [] as SavedKey[],
20-
history: [] as HistoryItem[]
22+
history: [] as HistoryItem[],
23+
chainTemplates: DEFAULT_CHAIN_TEMPLATES.map(template => ({
24+
...template,
25+
steps: template.steps.map(step => ({ ...step })), // Make steps mutable
26+
tags: [...template.tags], // Make tags mutable
27+
created: new Date(),
28+
})) as ChainTemplate[]
2129
}
2230
});
2331

@@ -534,4 +542,59 @@ ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, async (): Promise<void> => {
534542
console.error('Failed to clear history:', error);
535543
throw error;
536544
}
545+
});
546+
547+
// Chain management
548+
ipcMain.handle(IPC_CHANNELS.EXECUTE_CHAIN, async (_, steps: ChainStep[], inputText: string, templateId?: string, templateName?: string): Promise<ChainExecutionResult> => {
549+
try {
550+
const result = await chainExecutor.executeChain(steps, inputText, templateId, templateName);
551+
console.log(`Chain execution completed - Success: ${result.success}, Duration: ${result.totalDuration}ms`);
552+
return result;
553+
} catch (error) {
554+
console.error('Failed to execute chain:', error);
555+
throw error;
556+
}
557+
});
558+
559+
ipcMain.handle(IPC_CHANNELS.GET_CHAIN_TEMPLATES, (): ChainTemplate[] => {
560+
return storeGet('chainTemplates', []);
561+
});
562+
563+
ipcMain.handle(IPC_CHANNELS.SAVE_CHAIN_TEMPLATE, (_, template: ChainTemplate): void => {
564+
const templates = storeGet('chainTemplates', []);
565+
566+
// Check if template already exists and update it
567+
const existingIndex = templates.findIndex(t => t.id === template.id);
568+
if (existingIndex >= 0) {
569+
templates[existingIndex] = template;
570+
} else {
571+
templates.push(template);
572+
}
573+
574+
storeSet('chainTemplates', templates);
575+
console.log(`Chain template saved: ${template.name}`);
576+
});
577+
578+
ipcMain.handle(IPC_CHANNELS.UPDATE_CHAIN_TEMPLATE, (_, template: ChainTemplate): void => {
579+
const templates = storeGet('chainTemplates', []);
580+
const index = templates.findIndex(t => t.id === template.id);
581+
582+
if (index >= 0) {
583+
templates[index] = { ...template, lastUsed: new Date() };
584+
storeSet('chainTemplates', templates);
585+
console.log(`Chain template updated: ${template.name}`);
586+
} else {
587+
throw new Error(`Template with ID ${template.id} not found`);
588+
}
589+
});
590+
591+
ipcMain.handle(IPC_CHANNELS.DELETE_CHAIN_TEMPLATE, (_, templateId: string): void => {
592+
const templates = storeGet('chainTemplates', []);
593+
const filteredTemplates = templates.filter(t => t.id !== templateId);
594+
storeSet('chainTemplates', filteredTemplates);
595+
console.log(`Chain template deleted: ${templateId}`);
596+
});
597+
598+
ipcMain.handle(IPC_CHANNELS.GET_CHAIN_MODULES, () => {
599+
return chainExecutor.getAvailableModules();
537600
});

src/main/preload.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { contextBridge, ipcRenderer } from 'electron';
2-
import { SavedKey, RSAKeyPair, EncryptionResult, HistoryItem, HistoryFilter } from '../shared/types';
2+
import { SavedKey, RSAKeyPair, EncryptionResult, HistoryItem, HistoryFilter, ChainStep, ChainExecutionResult, ChainTemplate, ChainStepType } from '../shared/types';
33
import { IPC_CHANNELS } from '../shared/constants';
44

55
const electronAPI = {
@@ -73,6 +73,25 @@ const electronAPI = {
7373
clearHistory: (): Promise<void> =>
7474
ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY),
7575

76+
// Chain operations
77+
executeChain: (steps: ChainStep[], inputText: string, templateId?: string, templateName?: string): Promise<ChainExecutionResult> =>
78+
ipcRenderer.invoke(IPC_CHANNELS.EXECUTE_CHAIN, steps, inputText, templateId, templateName),
79+
80+
getChainTemplates: (): Promise<ChainTemplate[]> =>
81+
ipcRenderer.invoke(IPC_CHANNELS.GET_CHAIN_TEMPLATES),
82+
83+
saveChainTemplate: (template: ChainTemplate): Promise<void> =>
84+
ipcRenderer.invoke(IPC_CHANNELS.SAVE_CHAIN_TEMPLATE, template),
85+
86+
updateChainTemplate: (template: ChainTemplate): Promise<void> =>
87+
ipcRenderer.invoke(IPC_CHANNELS.UPDATE_CHAIN_TEMPLATE, template),
88+
89+
deleteChainTemplate: (templateId: string): Promise<void> =>
90+
ipcRenderer.invoke(IPC_CHANNELS.DELETE_CHAIN_TEMPLATE, templateId),
91+
92+
getChainModules: (): Promise<Record<ChainStepType, { name: string; description: string; category: string; requiredParams?: string[] }>> =>
93+
ipcRenderer.invoke(IPC_CHANNELS.GET_CHAIN_MODULES),
94+
7695
// 이벤트 리스너 제거
7796
removeUpdateListeners: () => {
7897
ipcRenderer.removeAllListeners('update-available');

0 commit comments

Comments
 (0)