Skip to content

Commit 6472d50

Browse files
authored
Merge pull request #1 from 0-ROK/add-web-demo
feat: 일렉트론 화면을 데모용 웹으로 배포
2 parents a77db75 + 1ca0e86 commit 6472d50

15 files changed

Lines changed: 1002 additions & 111 deletions

DEPLOYMENT.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,37 @@ pnpm run package:all
9090
- 다운로드 페이지 제작
9191
- 버전 관리 수동 처리
9292

93+
#### 3. Vercel 웹 데모 배포 (병행 운영)
94+
RiSA의 주요 기능을 브라우저에서 체험할 수 있도록 정적 웹 데모를 함께 제공할 수 있습니다.
95+
96+
1. **웹 번들 생성**
97+
```bash
98+
WEB_TARGET=web pnpm build:renderer
99+
```
100+
빌드 결과는 `dist/web` 폴더에 생성됩니다.
101+
102+
2. **Vercel 설정**
103+
- 빌드 명령은 `WEB_TARGET=web pnpm build:renderer`를 사용합니다.
104+
- 출력 디렉터리는 `dist/web`입니다.
105+
- 히스토리 라우팅을 위해 `vercel.json`에 아래 설정을 추가합니다.
106+
107+
```json
108+
{
109+
"builds": [
110+
{ "src": "package.json", "use": "@vercel/static-build", "config": { "distDir": "dist/web" } }
111+
],
112+
"scripts": { "build": "WEB_TARGET=web pnpm build:renderer" },
113+
"rewrites": [
114+
{ "source": "/(.*)", "destination": "/index.html" }
115+
]
116+
}
117+
```
118+
119+
3. **배포 시 주의사항**
120+
- RSA 암·복호화, 키 생성 등 일부 기능은 웹 데모에서 비활성화되어 안내 메시지가 노출됩니다.
121+
- 체인/HTTP 도구 등 클라이언트에서 실행 가능한 기능으로 데모를 구성합니다.
122+
- 정적 자산(이미지, 아이콘 등)이 `dist/web`에 포함되어 있는지 확인합니다.
123+
93124
### 💰 유료 앱스토어 배포
94125

95126
#### 1. Mac App Store
@@ -174,4 +205,4 @@ autoUpdater.setFeedURL({
174205
배포 완료 후 사용자들이 다운로드할 수 있는 링크가 생성됩니다!
175206

176207
---
177-
**💡 Tip:** 초기에는 베타 버전으로 출시하여 사용자 피드백을 받은 후 정식 버전을 릴리즈하는 것을 권장합니다.
208+
**💡 Tip:** 초기에는 베타 버전으로 출시하여 사용자 피드백을 받은 후 정식 버전을 릴리즈하는 것을 권장합니다.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"dev": "concurrently \"npm run dev:renderer\" \"npm run dev:main\"",
77
"dev:renderer": "webpack serve --config webpack.config.js --env NODE_ENV=development",
8+
"dev:web": "cross-env WEB_TARGET=web NODE_ENV=development webpack serve --config webpack.config.js",
89
"dev:main": "webpack --config webpack.config.js --env NODE_ENV=development --watch",
910
"build": "npm run build:renderer && npm run build:main && npm run build:preload",
1011
"build:main": "cross-env TARGET=main webpack --config webpack.config.js --mode production",

src/renderer/App.tsx

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import React from 'react';
2-
import { Layout } from 'antd';
1+
import React, { Suspense, lazy } from 'react';
2+
import { Layout, Spin } from 'antd';
33
import { Routes, Route } from 'react-router-dom';
44
import Sidebar from './components/Sidebar';
5-
import MainPage from './pages/MainPage';
6-
import KeyManagerPage from './pages/KeyManagerPage';
7-
import HistoryPage from './pages/HistoryPage';
8-
import EncodingToolsPage from './pages/EncodingToolsPage';
9-
import ChainBuilderPage from './pages/ChainBuilderPage';
10-
import HttpParserPage from './pages/HttpParserPage';
11-
import UpdateNotification from './components/UpdateNotification';
5+
6+
// Route-level code splitting
7+
const MainPage = lazy(() => import('./pages/MainPage'));
8+
const KeyManagerPage = lazy(() => import('./pages/KeyManagerPage'));
9+
const HistoryPage = lazy(() => import('./pages/HistoryPage'));
10+
const EncodingToolsPage = lazy(() => import('./pages/EncodingToolsPage'));
11+
const ChainBuilderPage = lazy(() => import('./pages/ChainBuilderPage'));
12+
const HttpParserPage = lazy(() => import('./pages/HttpParserPage'));
13+
const UpdateNotification = lazy(() => import('./components/UpdateNotification'));
1214

1315
const { Content } = Layout;
1416

@@ -22,19 +24,29 @@ const App: React.FC = () => {
2224
overflow: 'auto',
2325
backgroundColor: '#f0f2f5'
2426
}}>
25-
<Routes>
26-
<Route path="/" element={<MainPage />} />
27-
<Route path="/keys" element={<KeyManagerPage />} />
28-
<Route path="/encoding-tools" element={<EncodingToolsPage />} />
29-
<Route path="/http-parser" element={<HttpParserPage />} />
30-
<Route path="/chain-builder" element={<ChainBuilderPage />} />
31-
<Route path="/history" element={<HistoryPage />} />
32-
</Routes>
27+
<Suspense
28+
fallback={
29+
<div style={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
30+
<Spin size="large" />
31+
</div>
32+
}
33+
>
34+
<Routes>
35+
<Route path="/" element={<MainPage />} />
36+
<Route path="/keys" element={<KeyManagerPage />} />
37+
<Route path="/encoding-tools" element={<EncodingToolsPage />} />
38+
<Route path="/http-parser" element={<HttpParserPage />} />
39+
<Route path="/chain-builder" element={<ChainBuilderPage />} />
40+
<Route path="/history" element={<HistoryPage />} />
41+
</Routes>
42+
</Suspense>
3343
</Content>
3444
</Layout>
35-
<UpdateNotification />
45+
<Suspense fallback={null}>
46+
<UpdateNotification />
47+
</Suspense>
3648
</Layout>
3749
);
3850
};
3951

40-
export default App;
52+
export default App;

src/renderer/components/UpdateNotification.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useState } from 'react';
22
import { Modal, Button, Progress, Typography, Space } from 'antd';
33
import { DownloadOutlined, ReloadOutlined } from '@ant-design/icons';
4+
import { getPlatformServices } from '../services';
45

56
const { Text, Title } = Typography;
67

@@ -17,46 +18,52 @@ interface DownloadProgress {
1718
total: number;
1819
}
1920

21+
const services = getPlatformServices();
22+
2023
const UpdateNotification: React.FC = () => {
24+
if (services.environment !== 'electron') {
25+
return null;
26+
}
27+
2128
const [updateAvailable, setUpdateAvailable] = useState<UpdateInfo | null>(null);
2229
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress | null>(null);
2330
const [updateDownloaded, setUpdateDownloaded] = useState<UpdateInfo | null>(null);
2431
const [updateError, setUpdateError] = useState<string | null>(null);
2532

2633
useEffect(() => {
27-
if (!window.electronAPI) return;
34+
const updateService = services.update;
35+
if (!updateService) return;
2836

29-
// 업데이트 이벤트 리스너 등록
30-
window.electronAPI.onUpdateAvailable((info: UpdateInfo) => {
37+
updateService.onAvailable?.((info: UpdateInfo) => {
3138
setUpdateAvailable(info);
3239
});
3340

34-
window.electronAPI.onDownloadProgress((progress: DownloadProgress) => {
41+
updateService.onDownloadProgress?.((progress: DownloadProgress) => {
3542
setDownloadProgress(progress);
3643
});
3744

38-
window.electronAPI.onUpdateDownloaded((info: UpdateInfo) => {
45+
updateService.onDownloaded?.((info: UpdateInfo) => {
3946
setUpdateDownloaded(info);
4047
setDownloadProgress(null);
4148
setUpdateError(null);
4249
});
4350

4451
// 업데이트 에러 핸들링
45-
window.electronAPI.onUpdateError?.((error: string) => {
52+
updateService.onError?.((error: string) => {
4653
setUpdateError(error);
4754
setDownloadProgress(null);
4855
});
4956

5057
// 컴포넌트 언마운트 시 이벤트 리스너 제거
5158
return () => {
52-
window.electronAPI.removeUpdateListeners();
59+
updateService.removeAll?.();
5360
};
5461
}, []);
5562

5663
const handleDownloadUpdate = () => {
5764
// === 자동 다운로드 ===
5865
// electron-updater가 GitHub Releases에서 직접 다운로드
59-
window.electronAPI.startDownload?.();
66+
services.update?.startDownload?.();
6067
setUpdateAvailable(null);
6168
// 다운로드 진행 상황은 onDownloadProgress로 추적됨
6269
};
@@ -71,7 +78,7 @@ const UpdateNotification: React.FC = () => {
7178
};
7279

7380
const handleRestartAndInstall = () => {
74-
window.electronAPI.restartAndInstall();
81+
services.update?.restartAndInstall?.();
7582
};
7683

7784
const formatBytes = (bytes: number): string => {
@@ -363,4 +370,4 @@ const UpdateNotification: React.FC = () => {
363370
);
364371
};
365372

366-
export default UpdateNotification;
373+
export default UpdateNotification;

src/renderer/pages/KeyManagerPage.tsx

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ import { SavedKey } from '../../shared/types';
3232
import { RSA_KEY_SIZES } from '../../shared/constants';
3333
import AlgorithmSelector from '../components/AlgorithmSelector';
3434
import PageHeader from '../components/PageHeader';
35+
import { getPlatformServices } from '../services';
3536

3637
const { Text } = Typography;
3738
const { TextArea } = Input;
3839

40+
const services = getPlatformServices();
41+
const isWebEnvironment = services.environment === 'web';
42+
3943
const KeyManagerPage: React.FC = () => {
4044
const { keys, loading, saveKey, deleteKey } = useKeys();
4145
const [generateLoading, setGenerateLoading] = useState(false);
@@ -54,9 +58,18 @@ const KeyManagerPage: React.FC = () => {
5458
const [selectedEditAlgorithm, setSelectedEditAlgorithm] = useState<'RSA-OAEP' | 'RSA-PKCS1'>('RSA-OAEP');
5559

5660
const handleGenerateKey = async (values: { name: string; keySize: number; preferredAlgorithm: 'RSA-OAEP' | 'RSA-PKCS1' }) => {
61+
if (isWebEnvironment) {
62+
notification.info({
63+
message: '웹 데모 제한',
64+
description: '웹 데모에서는 RSA 키 생성을 지원하지 않습니다. 데스크톱 버전에서 키를 생성하거나 직접 등록 기능을 사용해주세요.',
65+
placement: 'topRight',
66+
});
67+
return;
68+
}
69+
5770
setGenerateLoading(true);
5871
try {
59-
const keyPair = await window.electronAPI.generateRSAKeys(values.keySize);
72+
const keyPair = await services.crypto.generateKeyPair(values.keySize);
6073

6174
const savedKey: SavedKey = {
6275
id: crypto.randomUUID(),
@@ -391,14 +404,17 @@ const KeyManagerPage: React.FC = () => {
391404
icon={<KeyOutlined />}
392405
extra={
393406
<Space>
394-
<Button
395-
type="primary"
396-
icon={<PlusOutlined />}
397-
onClick={() => setGenerateModalVisible(true)}
398-
size="large"
399-
>
400-
새 키 생성
401-
</Button>
407+
<Tooltip title={isWebEnvironment ? '웹 데모에서는 키 생성을 지원하지 않습니다.' : undefined}>
408+
<Button
409+
type="primary"
410+
icon={<PlusOutlined />}
411+
onClick={() => setGenerateModalVisible(true)}
412+
size="large"
413+
disabled={isWebEnvironment}
414+
>
415+
새 키 생성
416+
</Button>
417+
</Tooltip>
402418
<Button
403419
type="default"
404420
icon={<ImportOutlined />}
@@ -418,6 +434,16 @@ const KeyManagerPage: React.FC = () => {
418434
flex: 1
419435
}}>
420436

437+
{isWebEnvironment && (
438+
<Alert
439+
type="info"
440+
showIcon
441+
message="웹 데모에서는 키 생성이 제한됩니다"
442+
description="데스크톱 버전에서 생성한 키를 가져오거나 직접 키를 등록하여 기능을 체험할 수 있습니다."
443+
style={{ marginBottom: 16 }}
444+
/>
445+
)}
446+
421447
<Card>
422448
<Table
423449
columns={columns}
@@ -852,4 +878,4 @@ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC..."
852878
);
853879
};
854880

855-
export default KeyManagerPage;
881+
export default KeyManagerPage;

src/renderer/pages/MainPage.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ import { DEFAULT_ENCRYPTION_OPTIONS } from '../../shared/constants';
3232
import { HistoryItem } from '../../shared/types';
3333
import AlgorithmSelector from '../components/AlgorithmSelector';
3434
import PageHeader from '../components/PageHeader';
35+
import { getPlatformServices } from '../services';
3536

3637
const { TextArea } = Input;
3738
const { Text } = Typography;
3839
const { TabPane } = Tabs;
3940

41+
const services = getPlatformServices();
42+
const isWebEnvironment = services.environment === 'web';
43+
4044
const MainPage: React.FC = () => {
4145
const { keys, selectedKey, selectKey } = useKeys();
4246
const { saveHistoryItem } = useHistory();
@@ -55,6 +59,14 @@ const MainPage: React.FC = () => {
5559
const [decryptionStatus, setDecryptionStatus] = useState<'idle' | 'success' | 'error'>('idle');
5660
const [lastError, setLastError] = useState<string>('');
5761

62+
const showWebRestrictionNotice = () => {
63+
notification.info({
64+
message: '웹 데모 제한',
65+
description: '웹 데모에서는 RSA 암·복호화 기능을 사용할 수 없습니다. 데스크톱 버전에서 전체 기능을 이용해주세요.',
66+
placement: 'topRight',
67+
});
68+
};
69+
5870
// 선택된 키가 변경될 때마다 selectedKey 업데이트 및 알고리즘 자동 설정
5971
useEffect(() => {
6072
const key = keys.find(k => k.id === selectedKeyId);
@@ -67,6 +79,11 @@ const MainPage: React.FC = () => {
6779
}, [selectedKeyId, keys, selectKey]);
6880

6981
const handleEncrypt = async () => {
82+
if (isWebEnvironment) {
83+
showWebRestrictionNotice();
84+
return;
85+
}
86+
7087
if (!encryptText.trim()) {
7188
message.error('암호화할 텍스트를 입력해주세요.');
7289
return;
@@ -84,7 +101,7 @@ const MainPage: React.FC = () => {
84101
setLoading(true);
85102
setEncryptionStatus('idle');
86103
try {
87-
const result = await window.electronAPI.encryptText(
104+
const result = await services.crypto.encrypt(
88105
encryptText,
89106
selectedKey.publicKey,
90107
algorithm
@@ -140,6 +157,11 @@ const MainPage: React.FC = () => {
140157
};
141158

142159
const handleDecrypt = async () => {
160+
if (isWebEnvironment) {
161+
showWebRestrictionNotice();
162+
return;
163+
}
164+
143165
if (!decryptText.trim()) {
144166
message.error('복호화할 텍스트를 입력해주세요.');
145167
return;
@@ -188,7 +210,7 @@ const MainPage: React.FC = () => {
188210
setLoading(true);
189211
setDecryptionStatus('idle');
190212
try {
191-
const result = await window.electronAPI.decryptText(
213+
const result = await services.crypto.decrypt(
192214
decryptText,
193215
selectedKey.privateKey,
194216
algorithm
@@ -489,8 +511,8 @@ const MainPage: React.FC = () => {
489511
// 탭 오른쪽에 표시할 버튼들
490512
const renderTabBarExtraContent = () => {
491513
const isEncryptTab = activeTab === 'encrypt';
492-
const hasInputText = isEncryptTab ? encryptText.trim() : decryptText.trim();
493-
const canExecute = selectedKey && hasInputText && !loading;
514+
const hasInputText = (isEncryptTab ? encryptText : decryptText).trim();
515+
const canExecute = !isWebEnvironment && !!selectedKey && !!hasInputText && !loading;
494516

495517
return (
496518
<Space>
@@ -534,6 +556,16 @@ const MainPage: React.FC = () => {
534556
flex: 1
535557
}}>
536558

559+
{isWebEnvironment && (
560+
<Alert
561+
type="info"
562+
showIcon
563+
message="웹 데모 제한 안내"
564+
description="웹 데모에서는 RSA 암·복호화 기능이 비활성화되어 있습니다. 데스크톱 앱에서 전체 기능을 사용하거나 체인 빌더, HTTP 도구 등을 체험해보세요."
565+
style={{ marginBottom: 16 }}
566+
/>
567+
)}
568+
537569
{/* 키 선택 및 알고리즘 선택 섹션 */}
538570
<Card style={{ marginBottom: 16, flexShrink: 0 }}>
539571
<Row gutter={[16, 16]} align="top">
@@ -938,4 +970,4 @@ const MainPage: React.FC = () => {
938970
);
939971
};
940972

941-
export default MainPage;
973+
export default MainPage;

0 commit comments

Comments
 (0)