careermate 0.0.3 → 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 +526 -100
- package/dist/mcp.mjs +528 -102
- package/dist/migrate.mjs +23 -1
- package/dist/public/lib.js +6 -1
- package/dist/public/pages/applications.js +7 -0
- package/dist/public/pages/jobs.js +22 -15
- package/dist/public/pages/settings.js +160 -14
- package/dist/public/styles.css +19 -2
- package/dist/site/index.html +88 -228
- package/dist/site/landing.js +109 -0
- package/dist/site/start.html +154 -131
- package/dist/site/style.css +169 -63
- package/dist/web.mjs +503 -41
- package/docs/FAQ.md +3 -2
- package/docs/MCP_TOOLS.md +16 -8
- package/docs/START_WORKFLOW.md +2 -1
- package/docs/WORKFLOWS.md +6 -6
- package/package.json +2 -2
- package/packages/core/src/services.ts +69 -7
- package/packages/db/src/repositories.ts +0 -0
- package/packages/mcp-tools/src/bridge.ts +194 -13
- package/packages/mcp-tools/src/tools.ts +166 -35
- package/packages/mcp-tools/src/update.ts +1 -1
- package/packages/prompts/src/onboarding.ts +1 -1
- package/packages/prompts/src/system.ts +10 -2
- package/packages/shared/src/enums.ts +7 -2
- package/packages/shared/src/errors.ts +33 -0
- package/packages/shared/src/index.ts +1 -0
- package/packages/shared/src/schemas.ts +56 -2
- package/packages/workflows/src/definitions.ts +3 -3
package/dist/migrate.mjs
CHANGED
|
@@ -4436,7 +4436,20 @@ var optLine = external_exports.string().max(MAX_LINE).optional();
|
|
|
4436
4436
|
var optNote = external_exports.string().max(MAX_NOTE).optional();
|
|
4437
4437
|
var optBody = external_exports.string().max(MAX_BODY).optional();
|
|
4438
4438
|
var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
|
|
4439
|
-
|
|
4439
|
+
function toStrList(v) {
|
|
4440
|
+
if (typeof v !== "string") return v;
|
|
4441
|
+
const s = v.trim();
|
|
4442
|
+
if (!s) return [];
|
|
4443
|
+
if (s.startsWith("[")) {
|
|
4444
|
+
try {
|
|
4445
|
+
const parsed = JSON.parse(s);
|
|
4446
|
+
if (Array.isArray(parsed)) return parsed;
|
|
4447
|
+
} catch {
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
return s.split(/[\n,]/).map((x) => x.trim()).filter(Boolean);
|
|
4451
|
+
}
|
|
4452
|
+
var strList = external_exports.preprocess(toStrList, external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS));
|
|
4440
4453
|
var baseRecord = {
|
|
4441
4454
|
id,
|
|
4442
4455
|
created_at: isoDateTime,
|
|
@@ -4498,6 +4511,9 @@ var ExperienceRecordSchema = external_exports.object({
|
|
|
4498
4511
|
tech: external_exports.array(external_exports.string()),
|
|
4499
4512
|
order_index: external_exports.number()
|
|
4500
4513
|
});
|
|
4514
|
+
var ExperienceBatchInputSchema = external_exports.object({
|
|
4515
|
+
experiences: external_exports.array(ExperienceInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uACBD\uB825 \uBAA9\uB85D(\uD55C \uAC74\uC774\uC5B4\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uD68C\uC0AC\xB7\uC9C1\uBB34\xB7\uC785\uC0AC\uC77C\uC740 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
|
|
4516
|
+
});
|
|
4501
4517
|
var ProjectInputSchema = external_exports.object({
|
|
4502
4518
|
name: reqLine,
|
|
4503
4519
|
role: optLine,
|
|
@@ -4521,6 +4537,9 @@ var ProjectRecordSchema = external_exports.object({
|
|
|
4521
4537
|
end_date: external_exports.string().nullable(),
|
|
4522
4538
|
order_index: external_exports.number()
|
|
4523
4539
|
});
|
|
4540
|
+
var ProjectBatchInputSchema = external_exports.object({
|
|
4541
|
+
projects: external_exports.array(ProjectInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uC774\uB984\uC758 \uD504\uB85C\uC81D\uD2B8\uB294 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
|
|
4542
|
+
});
|
|
4524
4543
|
var SkillInputSchema = external_exports.object({
|
|
4525
4544
|
name: reqLine,
|
|
4526
4545
|
category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
|
|
@@ -4536,6 +4555,9 @@ var SkillRecordSchema = external_exports.object({
|
|
|
4536
4555
|
years: external_exports.number().nullable(),
|
|
4537
4556
|
order_index: external_exports.number()
|
|
4538
4557
|
});
|
|
4558
|
+
var SkillBatchInputSchema = external_exports.object({
|
|
4559
|
+
skills: external_exports.array(SkillInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uAE30\uC220\uC2A4\uD0DD \uBAA9\uB85D(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uC774\uB984\uC758 \uAE30\uC220\uC740 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
|
|
4560
|
+
});
|
|
4539
4561
|
var DocumentInputSchema = external_exports.object({
|
|
4540
4562
|
kind: external_exports.enum(DOCUMENT_KINDS),
|
|
4541
4563
|
title: reqLine,
|
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"/>',
|
|
@@ -135,7 +136,11 @@ export async function api(method, path, body) {
|
|
|
135
136
|
const text = await res.text();
|
|
136
137
|
let data = {};
|
|
137
138
|
if (text) { try { data = JSON.parse(text); } catch { /* non-JSON error page */ } }
|
|
138
|
-
if (!res.ok)
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const e = new Error(data.error || friendlyStatus(res.status));
|
|
141
|
+
e.status = res.status; // lets callers self-heal (e.g. drop a stale row on 404)
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
139
144
|
return data;
|
|
140
145
|
}
|
|
141
146
|
// Friendly fallback when the server didn't supply a message — never show a raw status code.
|
|
@@ -100,6 +100,13 @@ export async function render(ctx) {
|
|
|
100
100
|
await render(ctx); // refetch + re-render so the item moves to its new group
|
|
101
101
|
} catch (err) {
|
|
102
102
|
toastError(err);
|
|
103
|
+
if (err && err.status === 404) {
|
|
104
|
+
// The job/application no longer exists (e.g. a stale tab after a reset).
|
|
105
|
+
// Refresh so the phantom row disappears instead of erroring on every retry.
|
|
106
|
+
await ctx.refreshNav();
|
|
107
|
+
await render(ctx);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
103
110
|
selectEl.value = app.status; // revert on failure
|
|
104
111
|
selectEl.disabled = false;
|
|
105
112
|
}
|
|
@@ -262,7 +262,13 @@ async function renderDetail(ctx, jobId) {
|
|
|
262
262
|
if (res && res.hint) toastOk(res.hint);
|
|
263
263
|
await ctx.refreshNav();
|
|
264
264
|
await remount();
|
|
265
|
-
} catch (err) {
|
|
265
|
+
} catch (err) {
|
|
266
|
+
toastError(err);
|
|
267
|
+
// Stale tab: the job was removed since this page loaded. Re-render so the
|
|
268
|
+
// detail reflects reality (shows "not found") instead of looping the error.
|
|
269
|
+
if (err && err.status === 404) { await ctx.refreshNav(); await remount(); return; }
|
|
270
|
+
e.target.value = job.status;
|
|
271
|
+
}
|
|
266
272
|
},
|
|
267
273
|
},
|
|
268
274
|
);
|
|
@@ -309,21 +315,19 @@ async function renderDetail(ctx, jobId) {
|
|
|
309
315
|
el('div', { class: 'page-head__actions' }, headActions),
|
|
310
316
|
);
|
|
311
317
|
|
|
312
|
-
// --- Layout
|
|
313
|
-
|
|
318
|
+
// --- Layout ---------------------------------------------------------------
|
|
319
|
+
// 자기소개서·면접을 맨 위 2열로(둘 다 짧음), 그 아래 공고 정보(원문은 접힘)와
|
|
320
|
+
// 적합도를 전체폭으로 쌓는다. 한쪽 칼럼만 길어 빈 공간이 도드라지던 50/50
|
|
321
|
+
// 그리드를 대체한다. 관련 기록은 있을 때만 맨 아래.
|
|
322
|
+
const wrap = el('div', { class: 'stack-4' },
|
|
323
|
+
head,
|
|
324
|
+
el('div', { class: 'grid grid--2' },
|
|
325
|
+
CoverLettersCard(job.cover_letters),
|
|
326
|
+
InterviewCard(job)),
|
|
314
327
|
PostingCard(job),
|
|
315
328
|
FitCard(job.fit),
|
|
316
|
-
);
|
|
317
|
-
const right = el('div', { class: 'stack-4' },
|
|
318
|
-
CoverLettersCard(job.cover_letters),
|
|
319
|
-
InterviewCard(job),
|
|
320
329
|
RelatedCard(job.related),
|
|
321
330
|
);
|
|
322
|
-
|
|
323
|
-
const wrap = el('div', { class: 'stack-4' },
|
|
324
|
-
head,
|
|
325
|
-
el('div', { class: 'grid grid--2' }, left, right),
|
|
326
|
-
);
|
|
327
331
|
mount(ctx.view, wrap);
|
|
328
332
|
}
|
|
329
333
|
|
|
@@ -353,9 +357,12 @@ function PostingCard(job) {
|
|
|
353
357
|
}
|
|
354
358
|
|
|
355
359
|
if (job.description) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
el('
|
|
360
|
+
// 원문은 가장 길어 칼럼을 늘리는 주범 — 기본 접힘(아코디언)으로 둔다.
|
|
361
|
+
body.push(el('details', { class: 'disclosure mt-3' },
|
|
362
|
+
el('summary', { class: 'disclosure__summary' },
|
|
363
|
+
icon('chevronRight', 'disclosure__chevron'),
|
|
364
|
+
el('span', {}, '공고 원문')),
|
|
365
|
+
el('div', { class: 'doc-preview mt-2' }, job.description)));
|
|
359
366
|
}
|
|
360
367
|
|
|
361
368
|
return Card({ title: '공고 정보', body });
|
|
@@ -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 */
|
|
@@ -403,11 +403,24 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
403
403
|
/* ----------------------------------------------------------------- doc preview */
|
|
404
404
|
.doc-preview { white-space: pre-wrap; word-break: break-word; font-size: 15px; line-height: 1.75; color: var(--text);
|
|
405
405
|
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
|
|
406
|
+
|
|
407
|
+
/* disclosure — native <details> for collapsing long content (예: 공고 원문) */
|
|
408
|
+
.disclosure__summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
|
|
409
|
+
list-style: none; color: var(--text-secondary); font-size: 13px; font-weight: 600; user-select: none; }
|
|
410
|
+
.disclosure__summary::-webkit-details-marker { display: none; }
|
|
411
|
+
.disclosure__summary:hover { color: var(--text); }
|
|
412
|
+
.disclosure__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
|
|
413
|
+
.disclosure__chevron { width: 16px; height: 16px; flex: none; transition: transform .15s ease; }
|
|
414
|
+
.disclosure[open] .disclosure__chevron { transform: rotate(90deg); }
|
|
406
415
|
.kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
|
|
407
416
|
.kv dt { color: var(--text-tertiary); font-size: 13px; }
|
|
408
417
|
/* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
|
|
409
418
|
can shrink below its content — otherwise a long path/value overflows the grid. */
|
|
410
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; }
|
|
411
424
|
|
|
412
425
|
/* ----------------------------------------------------------------- modal */
|
|
413
426
|
.modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
|
|
@@ -496,5 +509,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
496
509
|
.kv{ grid-template-columns: 1fr; gap: 0; }
|
|
497
510
|
.kv dt{ margin-top: 10px; }
|
|
498
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; }
|
|
499
516
|
.page-head{ flex-wrap: wrap; }
|
|
500
517
|
}
|