careermate 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -0
- package/INSTALL.md +2 -1
- package/README.md +8 -6
- package/dist/doctor.mjs +345 -65
- package/dist/mcp.mjs +344 -64
- package/dist/public/lib.js +1 -0
- package/dist/public/pages/settings.js +160 -14
- package/dist/public/styles.css +10 -2
- package/dist/site/index.html +88 -188
- package/dist/site/landing.js +109 -0
- package/dist/site/start.html +97 -94
- package/dist/site/style.css +169 -63
- package/dist/web.mjs +369 -32
- package/docs/FAQ.md +3 -2
- package/docs/MCP_TOOLS.md +10 -3
- package/docs/WORKFLOWS.md +5 -5
- package/package.json +2 -2
- package/packages/core/src/services.ts +5 -5
- package/packages/db/src/repositories.ts +0 -0
- package/packages/mcp-tools/src/bridge.ts +194 -13
- package/packages/mcp-tools/src/tools.ts +145 -17
- package/packages/mcp-tools/src/update.ts +1 -1
- package/packages/prompts/src/system.ts +10 -2
- package/packages/workflows/src/definitions.ts +2 -2
package/dist/public/lib.js
CHANGED
|
@@ -83,6 +83,7 @@ const ICONS = {
|
|
|
83
83
|
trash: '<path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M6 6v14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6"/>',
|
|
84
84
|
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>',
|
|
85
85
|
download: '<path d="M12 3v12"/><path d="m7 11 5 4 5-4"/><path d="M5 21h14"/>',
|
|
86
|
+
upload: '<path d="M12 21V9"/><path d="m7 13 5-4 5 4"/><path d="M5 3h14"/>',
|
|
86
87
|
external: '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/>',
|
|
87
88
|
check: '<path d="m20 6-11 11-5-5"/>',
|
|
88
89
|
sparkle: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5 18 18M18 6l-2.5 2.5M8.5 15.5 6 18"/>',
|
|
@@ -10,6 +10,12 @@ const COUNT_LABELS = {
|
|
|
10
10
|
documents: '문서', cover_letters: '자기소개서', jobs: '채용공고',
|
|
11
11
|
applications: '지원', interview_preps: '면접 준비',
|
|
12
12
|
};
|
|
13
|
+
const BACKUP_COUNT_LABELS = {
|
|
14
|
+
...COUNT_LABELS,
|
|
15
|
+
cover_letter_versions: '자소서 버전',
|
|
16
|
+
fit_analyses: '적합도 분석',
|
|
17
|
+
activities: '활동',
|
|
18
|
+
};
|
|
13
19
|
|
|
14
20
|
export async function render(ctx) {
|
|
15
21
|
ctx.setActions([]);
|
|
@@ -21,7 +27,7 @@ export async function render(ctx) {
|
|
|
21
27
|
Theme(),
|
|
22
28
|
VerifyMode(info),
|
|
23
29
|
MyData(info.counts),
|
|
24
|
-
Backup(backups, () => render(ctx)),
|
|
30
|
+
Backup(backups, async () => { await ctx.refreshNav(); await render(ctx); }),
|
|
25
31
|
DangerZone(ctx),
|
|
26
32
|
);
|
|
27
33
|
|
|
@@ -64,18 +70,42 @@ function DataLocation(info) {
|
|
|
64
70
|
|
|
65
71
|
/* ---------------------------------------------------------- 연결 상태 */
|
|
66
72
|
function Connection(info) {
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
async function createShortcut() {
|
|
74
|
+
try {
|
|
75
|
+
const { shortcut } = await post('/api/settings/dashboard-shortcut', { open: true });
|
|
76
|
+
toastOk('대시보드 바로가기를 만들었어요.');
|
|
77
|
+
openModal({
|
|
78
|
+
title: '바로가기 생성',
|
|
79
|
+
body: el('div', { class: 'stack-3' },
|
|
80
|
+
el('div', { class: 'callout' },
|
|
81
|
+
icon('info'),
|
|
82
|
+
el('div', {},
|
|
83
|
+
el('div', { class: 'callout__title' }, '폴더를 만들었습니다'),
|
|
84
|
+
el('div', { class: 'callout__body' },
|
|
85
|
+
'대시보드가 실행 중이면 브라우저만 열고, 꺼져 있으면 서버를 시작한 뒤 브라우저를 엽니다.'))),
|
|
86
|
+
el('dl', { class: 'kv' },
|
|
87
|
+
...kvRow('폴더', shortcut.shortcut_dir, true),
|
|
88
|
+
...kvRow('바로가기', shortcut.launcher_path, true))),
|
|
89
|
+
footer: (close) => [
|
|
90
|
+
Btn('경로 복사', { icon: 'copy', onClick: () => copyText(shortcut.launcher_path) }),
|
|
91
|
+
Btn('확인', { variant: 'primary', onClick: close }),
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
} catch (e) {
|
|
95
|
+
toastError(e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const dashRow = el('div', { class: 'settings-action-row flex between gap-3', style: { padding: '4px 0' } },
|
|
100
|
+
el('div', { class: 'settings-action-row__main flex gap-3', style: { alignItems: 'center' } },
|
|
69
101
|
el('span', { class: 'badge badge--final_passed' }, el('span', { class: 'dot' }), '실행 중'),
|
|
70
102
|
el('div', {},
|
|
71
103
|
el('div', { class: 'strong' }, '대시보드'),
|
|
72
104
|
el('div', { class: 'muted text-sm tnum' }, info.url || '—'))),
|
|
73
|
-
|
|
74
|
-
? Btn('새 탭에서 열기', { icon: 'external', sm: true, variant: 'ghost', onClick: () => window.open(info.url, '_blank') })
|
|
75
|
-
: null);
|
|
105
|
+
Btn('바로가기 만들기', { icon: 'copy', sm: true, variant: 'ghost', onClick: createShortcut }));
|
|
76
106
|
|
|
77
|
-
const mcpRow = el('div', { class: 'flex between gap-3', style: { padding: '4px 0', alignItems: 'flex-start' } },
|
|
78
|
-
el('div', { class: 'flex gap-3', style: { alignItems: 'flex-start' } },
|
|
107
|
+
const mcpRow = el('div', { class: 'settings-action-row flex between gap-3', style: { padding: '4px 0', alignItems: 'flex-start' } },
|
|
108
|
+
el('div', { class: 'settings-action-row__main flex gap-3', style: { alignItems: 'flex-start' } },
|
|
79
109
|
el('div', { class: 'empty__icon', style: { width: '30px', height: '30px', margin: 0, borderRadius: '8px' } }, icon('link')),
|
|
80
110
|
el('div', {},
|
|
81
111
|
el('div', { class: 'strong' }, 'MCP 서버'),
|
|
@@ -193,8 +223,26 @@ function MyData(counts = {}) {
|
|
|
193
223
|
});
|
|
194
224
|
}
|
|
195
225
|
|
|
196
|
-
/* -------------------------------------------------
|
|
226
|
+
/* ------------------------------------------------- 내보내기 / 가져오기 */
|
|
197
227
|
function Backup(backups = [], rerender) {
|
|
228
|
+
const fileInput = el('input', {
|
|
229
|
+
type: 'file',
|
|
230
|
+
accept: '.json,application/json',
|
|
231
|
+
style: { display: 'none' },
|
|
232
|
+
onChange: async (e) => {
|
|
233
|
+
const file = e.target.files?.[0];
|
|
234
|
+
e.target.value = '';
|
|
235
|
+
if (!file) return;
|
|
236
|
+
try {
|
|
237
|
+
const backup = await readBackupFile(file);
|
|
238
|
+
const { preview } = await post('/api/settings/import-preview', { backup });
|
|
239
|
+
openImportModal({ fileName: file.name, backup, preview, rerender });
|
|
240
|
+
} catch (err) {
|
|
241
|
+
toastError(err);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
198
246
|
async function createBackup() {
|
|
199
247
|
try {
|
|
200
248
|
const res = await post('/api/settings/backup');
|
|
@@ -213,15 +261,113 @@ function Backup(backups = [], rerender) {
|
|
|
213
261
|
el('td', {}, el('span', { class: 'truncate', title: b.path || b.filename }, b.filename)),
|
|
214
262
|
el('td', { class: 'muted text-sm' }, fmtRelative(b.created_at)),
|
|
215
263
|
el('td', { class: 'muted text-sm tnum', style: { textAlign: 'right' } }, `${Math.round((b.size || 0) / 1024)} KB`)))))
|
|
216
|
-
: el('p', { class: 'muted', style: { margin: 0 } }, '아직 백업이 없습니다. "백업
|
|
264
|
+
: el('p', { class: 'muted', style: { margin: 0 } }, '아직 로컬 백업이 없습니다. "현재 상태 백업"으로 지금 데이터를 보관하세요.');
|
|
217
265
|
|
|
218
266
|
return Card({
|
|
219
|
-
title: '
|
|
267
|
+
title: '내보내기 / 가져오기',
|
|
220
268
|
actions: [
|
|
221
|
-
Btn('
|
|
222
|
-
Btn('
|
|
269
|
+
Btn('내보내기(JSON)', { icon: 'download', sm: true, variant: 'primary', onClick: () => downloadUrl('/api/settings/export-all') }),
|
|
270
|
+
Btn('가져오기(JSON)', { icon: 'upload', sm: true, variant: 'ghost', onClick: () => fileInput.click() }),
|
|
271
|
+
Btn('현재 상태 백업', { icon: 'copy', sm: true, variant: 'ghost', onClick: createBackup }),
|
|
223
272
|
],
|
|
224
|
-
body: list,
|
|
273
|
+
body: el('div', { class: 'stack-3' }, fileInput, list),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readBackupFile(file) {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const reader = new FileReader();
|
|
280
|
+
reader.onerror = () => reject(new Error('백업 파일을 읽지 못했어요.'));
|
|
281
|
+
reader.onload = () => {
|
|
282
|
+
try {
|
|
283
|
+
resolve(JSON.parse(String(reader.result || '')));
|
|
284
|
+
} catch {
|
|
285
|
+
reject(new Error('JSON 백업 파일만 복원할 수 있어요.'));
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
reader.readAsText(file);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function fmtBackupDate(iso) {
|
|
293
|
+
if (!iso) return '알 수 없음';
|
|
294
|
+
const d = new Date(iso);
|
|
295
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
296
|
+
return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function previewRows(counts = {}) {
|
|
300
|
+
const entries = Object.entries(counts)
|
|
301
|
+
.filter(([, count]) => Number(count) > 0)
|
|
302
|
+
.sort(([a], [b]) => (BACKUP_COUNT_LABELS[a] || a).localeCompare(BACKUP_COUNT_LABELS[b] || b, 'ko'));
|
|
303
|
+
|
|
304
|
+
if (!entries.length) {
|
|
305
|
+
return el('p', { class: 'muted', style: { margin: 0 } }, '복원할 항목이 없습니다.');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return el('table', { class: 'table' },
|
|
309
|
+
el('thead', {}, el('tr', {}, el('th', {}, '데이터'), el('th', { style: { textAlign: 'right' } }, '항목'))),
|
|
310
|
+
el('tbody', {}, ...entries.map(([key, count]) => el('tr', {},
|
|
311
|
+
el('td', {}, BACKUP_COUNT_LABELS[key] || key),
|
|
312
|
+
el('td', { class: 'tnum', style: { textAlign: 'right' } }, String(count))))));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function openImportModal({ fileName, backup, preview, rerender }) {
|
|
316
|
+
let confirmBtn;
|
|
317
|
+
const input = Input({
|
|
318
|
+
placeholder: '가져오기',
|
|
319
|
+
onInput: (e) => { if (confirmBtn) confirmBtn.disabled = e.target.value.trim() !== '가져오기'; },
|
|
320
|
+
onKeydown: (e) => {
|
|
321
|
+
if (e.key === 'Enter' && e.target.value.trim() === '가져오기' && confirmBtn && !confirmBtn.disabled) confirmBtn.click();
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
openModal({
|
|
326
|
+
title: '데이터 가져오기',
|
|
327
|
+
size: 'lg',
|
|
328
|
+
body: el('div', { class: 'stack-3' },
|
|
329
|
+
el('div', { class: 'callout' },
|
|
330
|
+
icon('info'),
|
|
331
|
+
el('div', {},
|
|
332
|
+
el('div', { class: 'callout__title' }, '현재 데이터는 먼저 백업됩니다'),
|
|
333
|
+
el('div', { class: 'callout__body' },
|
|
334
|
+
'가져오면 이 컴퓨터의 CareerMate 데이터가 선택한 JSON 내용으로 전체 교체됩니다.'))),
|
|
335
|
+
el('dl', { class: 'kv' },
|
|
336
|
+
...kvRow('파일', fileName, false),
|
|
337
|
+
...kvRow('백업 시각', fmtBackupDate(preview.exported_at), false),
|
|
338
|
+
...kvRow('스키마', `${preview.version} / 현재 ${preview.current_version}`, false),
|
|
339
|
+
...kvRow('전체 항목', String(preview.total_rows || 0), false)),
|
|
340
|
+
previewRows(preview.counts),
|
|
341
|
+
preview.warnings?.length
|
|
342
|
+
? el('div', { class: 'callout' },
|
|
343
|
+
icon('info'),
|
|
344
|
+
el('div', {},
|
|
345
|
+
el('div', { class: 'callout__title' }, '확인 필요'),
|
|
346
|
+
el('div', { class: 'callout__body' }, preview.warnings.join(' '))))
|
|
347
|
+
: null,
|
|
348
|
+
el('p', { class: 'text-secondary', style: { margin: 0, lineHeight: '1.6' } },
|
|
349
|
+
'계속하려면 아래 입력란에 ', el('span', { class: 'strong' }, '가져오기'), ' 를 입력하세요.'),
|
|
350
|
+
input),
|
|
351
|
+
footer: (close) => {
|
|
352
|
+
confirmBtn = Btn('가져오기', {
|
|
353
|
+
icon: 'upload',
|
|
354
|
+
variant: 'danger',
|
|
355
|
+
disabled: true,
|
|
356
|
+
onClick: async () => {
|
|
357
|
+
confirmBtn.disabled = true;
|
|
358
|
+
try {
|
|
359
|
+
await post('/api/settings/restore', { backup, confirm: 'RESTORE' });
|
|
360
|
+
close();
|
|
361
|
+
toastOk('데이터를 가져왔어요.');
|
|
362
|
+
await rerender();
|
|
363
|
+
} catch (e) {
|
|
364
|
+
toastError(e);
|
|
365
|
+
confirmBtn.disabled = input.value.trim() !== '가져오기';
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
return [Btn('취소', { onClick: close }), confirmBtn];
|
|
370
|
+
},
|
|
225
371
|
});
|
|
226
372
|
}
|
|
227
373
|
|
package/dist/public/styles.css
CHANGED
|
@@ -190,10 +190,10 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
190
190
|
|
|
191
191
|
/* ----------------------------------------------------------------- card */
|
|
192
192
|
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
|
|
193
|
-
.card__head { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
|
|
193
|
+
.card__head { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
194
194
|
.card__head h3 { font-size: 15px; }
|
|
195
195
|
.card__head .sub { color: var(--text-tertiary); font-size: 13px; }
|
|
196
|
-
.card__head .right { margin-left: auto; display: flex; gap: 6px; }
|
|
196
|
+
.card__head .right { margin-left: auto; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; min-width: 0; max-width: 100%; }
|
|
197
197
|
.card__body { padding: 18px; }
|
|
198
198
|
.card__body--tight { padding: 0; }
|
|
199
199
|
/* a light bordered block for grouping inside a card/modal — never nest a full .card */
|
|
@@ -417,6 +417,10 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
417
417
|
/* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
|
|
418
418
|
can shrink below its content — otherwise a long path/value overflows the grid. */
|
|
419
419
|
.kv dd { margin: 0; min-width: 0; }
|
|
420
|
+
.settings-action-row { align-items: flex-start; min-width: 0; }
|
|
421
|
+
.settings-action-row__main { flex: 1 1 auto; min-width: 0; }
|
|
422
|
+
.settings-action-row__main > div:last-child { min-width: 0; }
|
|
423
|
+
.settings-action-row > .btn { flex: 0 0 auto; }
|
|
420
424
|
|
|
421
425
|
/* ----------------------------------------------------------------- modal */
|
|
422
426
|
.modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
|
|
@@ -505,5 +509,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
505
509
|
.kv{ grid-template-columns: 1fr; gap: 0; }
|
|
506
510
|
.kv dt{ margin-top: 10px; }
|
|
507
511
|
.kv dd{ margin: 0 0 2px; }
|
|
512
|
+
.settings-action-row{ align-items: stretch; flex-direction: column; }
|
|
513
|
+
.settings-action-row > .btn{ width: 100%; justify-content: center; }
|
|
514
|
+
.card__head{ align-items: flex-start; flex-wrap: wrap; }
|
|
515
|
+
.card__head .right{ margin-left: 0; width: 100%; justify-content: flex-start; }
|
|
508
516
|
.page-head{ flex-wrap: wrap; }
|
|
509
517
|
}
|