careermate 0.1.2 → 0.1.3
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/LICENSE +21 -0
- package/README.md +3 -1
- package/apps/web/public/pages/documents.js +7 -2
- package/apps/web/public/pages/home.js +4 -7
- package/apps/web/public/pages/jobs.js +1 -5
- package/apps/web/public/pages/profile.js +3 -3
- package/apps/web/public/styles.css +7 -2
- package/dist/doctor.mjs +112 -90
- package/dist/install-page/analytics.js +97 -0
- package/dist/install-page/index.html +77 -56
- package/dist/install-page/og.png +0 -0
- package/dist/install-page/robots.txt +4 -0
- package/dist/install-page/sitemap.xml +21 -0
- package/dist/install-page/start.html +13 -10
- package/dist/install-page/style.css +75 -40
- package/dist/mcp.mjs +112 -90
- package/dist/migrate.mjs +95 -82
- package/dist/public/pages/documents.js +7 -2
- package/dist/public/pages/home.js +4 -7
- package/dist/public/pages/jobs.js +1 -5
- package/dist/public/pages/profile.js +3 -3
- package/dist/public/styles.css +7 -2
- package/dist/web.mjs +113 -86
- package/package.json +1 -2
- package/packages/db/src/repositories.ts +18 -1
- package/packages/mcp-tools/src/tools.ts +5 -5
- package/packages/parsers/src/documents.ts +13 -1
- package/packages/shared/src/schemas.ts +115 -81
- package/scripts/build-dist.mjs +7 -1
- package/scripts/gen-og.mjs +1 -1
- package/scripts/test.ts +25 -0
- package/scripts/ui-smoke.ts +32 -4
- package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +0 -12
- package/dist/install-page/.omc/state/idle-notif-cooldown.json +0 -3
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +0 -147
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +0 -22
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +0 -44
- package/docs/ARCHITECTURE.md +0 -208
- package/docs/CHANGES_V1.md +0 -103
- package/docs/DATA_MODEL.md +0 -460
- package/docs/DECISIONS.md +0 -277
- package/docs/DEMO.md +0 -242
- package/docs/MCP_TOOLS.md +0 -292
- package/docs/ROADMAP.md +0 -135
- package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +0 -135
- package/docs/START_WORKFLOW.md +0 -137
- package/docs/SUPPORTED_AI_APPS.md +0 -112
- package/docs/TODO.md +0 -57
- package/docs/UX_NOTES.md +0 -247
- package/docs/WORKFLOWS.md +0 -202
- package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +0 -64
|
@@ -23,6 +23,7 @@ export async function render(ctx) {
|
|
|
23
23
|
root.append(el('div', { class: 'tabs' }, tabCover, tabDocs), panel);
|
|
24
24
|
|
|
25
25
|
let query = '';
|
|
26
|
+
let searchInput = null;
|
|
26
27
|
function selectTab(next) {
|
|
27
28
|
tab = next;
|
|
28
29
|
query = '';
|
|
@@ -43,9 +44,12 @@ export async function render(ctx) {
|
|
|
43
44
|
if (!hide) shown += 1;
|
|
44
45
|
}
|
|
45
46
|
const existing = panel.querySelector('.search-empty');
|
|
47
|
+
if (existing) existing.remove();
|
|
46
48
|
if (q && cards.length && shown === 0) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
panel.append(el('div', { class: 'search-empty empty' },
|
|
50
|
+
el('p', { class: 'muted', style: { margin: '0 0 12px' } }, `‘${query.trim()}’에 맞는 항목이 없어요.`),
|
|
51
|
+
Btn('필터 초기화', { sm: true, variant: 'ghost', onClick: () => { query = ''; if (searchInput) searchInput.value = ''; applyFilter(); } })));
|
|
52
|
+
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
async function reload() {
|
|
@@ -70,6 +74,7 @@ export async function render(ctx) {
|
|
|
70
74
|
function setActions() {
|
|
71
75
|
const search = Input({ type: 'search', placeholder: tab === 'cover' ? '자기소개서 검색' : '문서 검색', value: query, attrs: { 'aria-label': '검색' } });
|
|
72
76
|
search.classList.add('input--inline');
|
|
77
|
+
searchInput = search;
|
|
73
78
|
search.addEventListener('input', () => { query = search.value; applyFilter(); });
|
|
74
79
|
const createBtn = tab === 'cover'
|
|
75
80
|
? Btn('새 자기소개서', { icon: 'plus', variant: 'primary', onClick: () => openCoverCreate(reload) })
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the 시작하기 card. Every empty state is ONE screen-specific sentence; never repeat
|
|
5
5
|
// the same "AI에게 보내면 저장됩니다" instruction across surfaces.
|
|
6
6
|
import {
|
|
7
|
-
el, get, icon, navigate, Card, Badge, Btn,
|
|
7
|
+
el, get, icon, navigate, Card, Badge, Btn, ListRow, CheckRow,
|
|
8
8
|
Stat, PageHead, fmtDate, fmtRelative, scoreClass, STATUS_COLOR, statusColor, mount,
|
|
9
9
|
} from '/lib.js';
|
|
10
10
|
|
|
@@ -59,7 +59,7 @@ function Greeting(name, s, firstRun) {
|
|
|
59
59
|
let focus;
|
|
60
60
|
if (firstRun) focus = '시작해 볼까요 — 먼저 기본 정보를 채워 주세요.';
|
|
61
61
|
else if (s.interview_todo.length) focus = `면접 준비가 필요한 지원 ${s.interview_todo.length}건이 있어요.`;
|
|
62
|
-
else if (s.in_progress.length) focus = `진행 중인 지원 ${s.in_progress.length}
|
|
62
|
+
else if (s.in_progress.length) focus = `진행 중인 지원 ${s.in_progress.length}건이 있어요.`;
|
|
63
63
|
else if (s.counts.jobs) focus = `저장된 공고 ${s.counts.jobs}건 · 새 적합도 분석을 받아보세요.`;
|
|
64
64
|
else focus = '오늘도 한 걸음 나아가 볼까요.';
|
|
65
65
|
return PageHead({ title: name ? `${name}님, 안녕하세요` : '환영합니다', desc: focus });
|
|
@@ -87,7 +87,7 @@ function GettingStarted(o) {
|
|
|
87
87
|
el('div', { class: 'callout__body' },
|
|
88
88
|
'공고 분석·자기소개서·면접 준비는 평소 쓰던 AI에게 말로 시키면, 결과가 이 대시보드에 쌓여요.'))),
|
|
89
89
|
el('div', { class: 'progress' }, el('div', { class: 'progress__bar', style: { width: `${o.profile_completeness}%` } })),
|
|
90
|
-
el('p', { class: 'text-secondary text-sm
|
|
90
|
+
el('p', { class: 'text-secondary text-sm mt-3 mb-2' },
|
|
91
91
|
'완성할수록 AI의 적합도 분석과 자기소개서 품질이 좋아져요.'),
|
|
92
92
|
el('div', {}, ...steps.map((it) => CheckRow({ done: it.done, label: it.label, onClick: () => navigate(it.to) }))),
|
|
93
93
|
],
|
|
@@ -201,10 +201,7 @@ function RecentJobs(jobs) {
|
|
|
201
201
|
if (!jobs.length) {
|
|
202
202
|
return Card({
|
|
203
203
|
title: '최근 공고',
|
|
204
|
-
body:
|
|
205
|
-
iconName: 'briefcase', title: '저장된 공고가 아직 없어요',
|
|
206
|
-
action: Btn('공고 추가', { icon: 'plus', variant: 'ghost', onClick: () => navigate('/jobs') }),
|
|
207
|
-
}),
|
|
204
|
+
body: el('p', { class: 'muted', style: { margin: 0 } }, '저장된 공고가 아직 없어요. 새 공고를 추가해 보세요.'),
|
|
208
205
|
});
|
|
209
206
|
}
|
|
210
207
|
const rows = jobs.map((j) => ListRow({
|
|
@@ -365,11 +365,7 @@ function FitCard(fit) {
|
|
|
365
365
|
if (!fit) {
|
|
366
366
|
return Card({
|
|
367
367
|
title: '적합도 분석',
|
|
368
|
-
body:
|
|
369
|
-
iconName: 'target',
|
|
370
|
-
title: '적합도 분석 전이에요',
|
|
371
|
-
body: '이 공고와 내 프로필을 비교한 분석 결과가 여기에 표시됩니다.',
|
|
372
|
-
}),
|
|
368
|
+
body: el('p', { class: 'muted', style: { margin: 0 } }, '아직 분석 전이에요. 이 공고와 내 프로필을 비교한 결과가 여기에 표시됩니다.'),
|
|
373
369
|
});
|
|
374
370
|
}
|
|
375
371
|
|
|
@@ -264,7 +264,7 @@ function editWriting(p, reload) {
|
|
|
264
264
|
/* ---------------------------------------------------- 4. 경력 (Experiences) */
|
|
265
265
|
|
|
266
266
|
function ExperiencesCard(items, reload) {
|
|
267
|
-
const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editExperience(null, reload) });
|
|
267
|
+
const add = Btn('경력 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editExperience(null, reload) });
|
|
268
268
|
|
|
269
269
|
if (!items.length) {
|
|
270
270
|
return Card({
|
|
@@ -368,7 +368,7 @@ function editExperience(x, reload) {
|
|
|
368
368
|
/* ----------------------------------------------------- 5. 프로젝트 (Projects) */
|
|
369
369
|
|
|
370
370
|
function ProjectsCard(items, reload) {
|
|
371
|
-
const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editProject(null, reload) });
|
|
371
|
+
const add = Btn('프로젝트 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editProject(null, reload) });
|
|
372
372
|
|
|
373
373
|
if (!items.length) {
|
|
374
374
|
return Card({
|
|
@@ -469,7 +469,7 @@ function editProject(x, reload) {
|
|
|
469
469
|
/* -------------------------------------------------------- 6. 기술 (Skills) */
|
|
470
470
|
|
|
471
471
|
function SkillsCard(items, reload) {
|
|
472
|
-
const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editSkill(null, reload) });
|
|
472
|
+
const add = Btn('기술 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editSkill(null, reload) });
|
|
473
473
|
|
|
474
474
|
if (!items.length) {
|
|
475
475
|
return Card({
|
package/dist/public/styles.css
CHANGED
|
@@ -403,7 +403,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
403
403
|
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
|
|
404
404
|
.kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
|
|
405
405
|
.kv dt { color: var(--text-tertiary); font-size: 13px; }
|
|
406
|
-
|
|
406
|
+
/* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
|
|
407
|
+
can shrink below its content — otherwise a long path/value overflows the grid. */
|
|
408
|
+
.kv dd { margin: 0; min-width: 0; }
|
|
407
409
|
|
|
408
410
|
/* ----------------------------------------------------------------- modal */
|
|
409
411
|
.modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
|
|
@@ -450,7 +452,10 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
|
450
452
|
.between { justify-content: space-between; } .center { align-items: center; } .wrap { flex-wrap: wrap; }
|
|
451
453
|
.ml-auto { margin-left: auto; } .mt-2{margin-top:8px;} .mt-3{margin-top:12px;} .mt-4{margin-top:16px;} .mb-2{margin-bottom:8px;} .mb-3{margin-bottom:12px;} .mb-4{margin-bottom:16px;}
|
|
452
454
|
.tnum { font-variant-numeric: tabular-nums; }
|
|
453
|
-
|
|
455
|
+
/* min-width:0 lets a truncating element actually shrink inside a flex row;
|
|
456
|
+
without it a long unbreakable value (e.g. a data-folder path) forces the
|
|
457
|
+
flex item to its content width and overflows the viewport on narrow screens. */
|
|
458
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; min-width: 0; }
|
|
454
459
|
.divider { height: 1px; background: var(--border); margin: 16px 0; }
|
|
455
460
|
.skeleton { background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--surface-hover) 37%, var(--bg-sunken) 63%); background-size: 400% 100%; animation: shimmer 1.4s infinite; border-radius: 6px; }
|
|
456
461
|
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
|
package/dist/web.mjs
CHANGED
|
@@ -43892,29 +43892,42 @@ function isSafeUrl(u) {
|
|
|
43892
43892
|
return !/^[a-z][a-z0-9+.-]*:/i.test(s) || /^(https?:|mailto:)/i.test(s);
|
|
43893
43893
|
}
|
|
43894
43894
|
var UNSAFE_URL_MSG = "javascript: \uAC19\uC740 \uC704\uD5D8\uD55C \uB9C1\uD06C\uB294 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. http(s) \uB9C1\uD06C\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.";
|
|
43895
|
-
var
|
|
43896
|
-
var
|
|
43895
|
+
var MAX_URL = 2e3;
|
|
43896
|
+
var SafeUrl = external_exports.string().min(1).max(MAX_URL).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
|
|
43897
|
+
var SafeUrlOptional = external_exports.string().max(MAX_URL).optional().refine((u) => u == null || u === "" || isSafeUrl(u), { message: UNSAFE_URL_MSG });
|
|
43898
|
+
var MAX_LINE = 500;
|
|
43899
|
+
var MAX_NOTE = 1e4;
|
|
43900
|
+
var MAX_BODY = 2e5;
|
|
43901
|
+
var MAX_ITEM = 2e3;
|
|
43902
|
+
var MAX_ITEMS = 500;
|
|
43903
|
+
var EMPTY_MSG = "\uB0B4\uC6A9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4(\uACF5\uBC31\uB9CC\uC73C\uB85C\uB294 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4).";
|
|
43904
|
+
var reqLine = external_exports.string().trim().min(1).max(MAX_LINE);
|
|
43905
|
+
var optLine = external_exports.string().max(MAX_LINE).optional();
|
|
43906
|
+
var optNote = external_exports.string().max(MAX_NOTE).optional();
|
|
43907
|
+
var optBody = external_exports.string().max(MAX_BODY).optional();
|
|
43908
|
+
var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
|
|
43909
|
+
var strList = external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS);
|
|
43897
43910
|
var baseRecord = {
|
|
43898
43911
|
id,
|
|
43899
43912
|
created_at: isoDateTime,
|
|
43900
43913
|
updated_at: isoDateTime
|
|
43901
43914
|
};
|
|
43902
43915
|
var LinkSchema = external_exports.object({
|
|
43903
|
-
label:
|
|
43916
|
+
label: reqLine,
|
|
43904
43917
|
url: SafeUrl
|
|
43905
43918
|
});
|
|
43906
43919
|
var ProfileInputSchema = external_exports.object({
|
|
43907
|
-
name:
|
|
43908
|
-
email:
|
|
43909
|
-
phone:
|
|
43910
|
-
location:
|
|
43911
|
-
headline: external_exports.string().optional().describe("\uD55C \uC904 \uC18C\uAC1C / \uC9C1\uBB34 \uD0C0\uC774\uD2C0"),
|
|
43912
|
-
summary: external_exports.string().optional().describe("\uC790\uAE30\uC18C\uAC1C \uC694\uC57D"),
|
|
43913
|
-
desired_roles:
|
|
43914
|
-
desired_conditions: external_exports.string().optional().describe("\uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74 (\uC5F0\uBD09, \uC9C0\uC5ED, \uADFC\uBB34\uD615\uD0DC \uB4F1)"),
|
|
43915
|
-
preferred_tone: external_exports.string().optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C \uC120\uD638 \uBB38\uCCB4 (\uC608: \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801)"),
|
|
43916
|
-
emphasis_points:
|
|
43917
|
-
links: external_exports.array(LinkSchema).optional().describe("\uD3EC\uD2B8\uD3F4\uB9AC\uC624/\uAE43\uD5C8\uBE0C/\uB9C1\uD06C\uB4DC\uC778 \uB4F1")
|
|
43920
|
+
name: reqLine.optional(),
|
|
43921
|
+
email: optLine,
|
|
43922
|
+
phone: optLine,
|
|
43923
|
+
location: optLine,
|
|
43924
|
+
headline: external_exports.string().max(MAX_LINE).optional().describe("\uD55C \uC904 \uC18C\uAC1C / \uC9C1\uBB34 \uD0C0\uC774\uD2C0"),
|
|
43925
|
+
summary: external_exports.string().max(MAX_NOTE).optional().describe("\uC790\uAE30\uC18C\uAC1C \uC694\uC57D"),
|
|
43926
|
+
desired_roles: strList.optional().describe("\uD76C\uB9DD \uC9C1\uBB34"),
|
|
43927
|
+
desired_conditions: external_exports.string().max(MAX_NOTE).optional().describe("\uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74 (\uC5F0\uBD09, \uC9C0\uC5ED, \uADFC\uBB34\uD615\uD0DC \uB4F1)"),
|
|
43928
|
+
preferred_tone: external_exports.string().max(MAX_LINE).optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C \uC120\uD638 \uBB38\uCCB4 (\uC608: \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801)"),
|
|
43929
|
+
emphasis_points: strList.optional().describe("\uAC15\uC870\uD558\uACE0 \uC2F6\uC740 \uD575\uC2EC \uD3EC\uC778\uD2B8"),
|
|
43930
|
+
links: external_exports.array(LinkSchema).max(MAX_ITEMS).optional().describe("\uD3EC\uD2B8\uD3F4\uB9AC\uC624/\uAE43\uD5C8\uBE0C/\uB9C1\uD06C\uB4DC\uC778 \uB4F1")
|
|
43918
43931
|
});
|
|
43919
43932
|
var ProfileRecordSchema = external_exports.object({
|
|
43920
43933
|
...baseRecord,
|
|
@@ -43931,15 +43944,15 @@ var ProfileRecordSchema = external_exports.object({
|
|
|
43931
43944
|
links: external_exports.array(LinkSchema)
|
|
43932
43945
|
});
|
|
43933
43946
|
var ExperienceInputSchema = external_exports.object({
|
|
43934
|
-
company:
|
|
43935
|
-
role:
|
|
43936
|
-
employment_type: external_exports.string().optional().describe("\uC815\uADDC\uC9C1/\uACC4\uC57D\uC9C1/\uC778\uD134/\uD504\uB9AC\uB79C\uC11C \uB4F1"),
|
|
43937
|
-
start_date: external_exports.string().optional().describe("YYYY-MM \uB610\uB294 YYYY-MM-DD"),
|
|
43938
|
-
end_date: external_exports.string().optional().describe("\uC7AC\uC9C1 \uC911\uC774\uBA74 \uBE44\uC6CC\uB450\uAE30"),
|
|
43947
|
+
company: reqLine,
|
|
43948
|
+
role: optLine,
|
|
43949
|
+
employment_type: external_exports.string().max(MAX_LINE).optional().describe("\uC815\uADDC\uC9C1/\uACC4\uC57D\uC9C1/\uC778\uD134/\uD504\uB9AC\uB79C\uC11C \uB4F1"),
|
|
43950
|
+
start_date: external_exports.string().max(MAX_LINE).optional().describe("YYYY-MM \uB610\uB294 YYYY-MM-DD"),
|
|
43951
|
+
end_date: external_exports.string().max(MAX_LINE).optional().describe("\uC7AC\uC9C1 \uC911\uC774\uBA74 \uBE44\uC6CC\uB450\uAE30"),
|
|
43939
43952
|
is_current: external_exports.boolean().optional(),
|
|
43940
|
-
description:
|
|
43941
|
-
achievements:
|
|
43942
|
-
tech:
|
|
43953
|
+
description: optBody,
|
|
43954
|
+
achievements: strList.optional().describe("\uC131\uACFC/\uC5C5\uC801 (\uC815\uB7C9 \uC9C0\uD45C \uAD8C\uC7A5)"),
|
|
43955
|
+
tech: strList.optional().describe("\uC0AC\uC6A9 \uAE30\uC220/\uB3C4\uAD6C"),
|
|
43943
43956
|
order_index: external_exports.number().optional()
|
|
43944
43957
|
});
|
|
43945
43958
|
var ExperienceRecordSchema = external_exports.object({
|
|
@@ -43956,14 +43969,14 @@ var ExperienceRecordSchema = external_exports.object({
|
|
|
43956
43969
|
order_index: external_exports.number()
|
|
43957
43970
|
});
|
|
43958
43971
|
var ProjectInputSchema = external_exports.object({
|
|
43959
|
-
name:
|
|
43960
|
-
role:
|
|
43961
|
-
description:
|
|
43962
|
-
highlights:
|
|
43963
|
-
tech:
|
|
43972
|
+
name: reqLine,
|
|
43973
|
+
role: optLine,
|
|
43974
|
+
description: optBody,
|
|
43975
|
+
highlights: strList.optional(),
|
|
43976
|
+
tech: strList.optional(),
|
|
43964
43977
|
url: SafeUrlOptional,
|
|
43965
|
-
start_date:
|
|
43966
|
-
end_date:
|
|
43978
|
+
start_date: optLine,
|
|
43979
|
+
end_date: optLine,
|
|
43967
43980
|
order_index: external_exports.number().optional()
|
|
43968
43981
|
});
|
|
43969
43982
|
var ProjectRecordSchema = external_exports.object({
|
|
@@ -43979,9 +43992,9 @@ var ProjectRecordSchema = external_exports.object({
|
|
|
43979
43992
|
order_index: external_exports.number()
|
|
43980
43993
|
});
|
|
43981
43994
|
var SkillInputSchema = external_exports.object({
|
|
43982
|
-
name:
|
|
43983
|
-
category: external_exports.string().optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
|
|
43984
|
-
level: external_exports.string().optional().describe("\uC0C1/\uC911/\uD558 \uB610\uB294 \uC790\uC720 \uC11C\uC220"),
|
|
43995
|
+
name: reqLine,
|
|
43996
|
+
category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
|
|
43997
|
+
level: external_exports.string().max(MAX_LINE).optional().describe("\uC0C1/\uC911/\uD558 \uB610\uB294 \uC790\uC720 \uC11C\uC220"),
|
|
43985
43998
|
years: external_exports.number().optional(),
|
|
43986
43999
|
order_index: external_exports.number().optional()
|
|
43987
44000
|
});
|
|
@@ -43995,11 +44008,11 @@ var SkillRecordSchema = external_exports.object({
|
|
|
43995
44008
|
});
|
|
43996
44009
|
var DocumentInputSchema = external_exports.object({
|
|
43997
44010
|
kind: external_exports.enum(DOCUMENT_KINDS),
|
|
43998
|
-
title:
|
|
43999
|
-
content:
|
|
44011
|
+
title: reqLine,
|
|
44012
|
+
content: reqBody.describe("Markdown \uB610\uB294 \uC77C\uBC18 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38"),
|
|
44000
44013
|
source: external_exports.enum(CONTENT_SOURCES).optional(),
|
|
44001
44014
|
is_primary: external_exports.boolean().optional().describe("\uB300\uD45C \uBB38\uC11C \uC5EC\uBD80"),
|
|
44002
|
-
tags:
|
|
44015
|
+
tags: strList.optional()
|
|
44003
44016
|
});
|
|
44004
44017
|
var DocumentRecordSchema = external_exports.object({
|
|
44005
44018
|
...baseRecord,
|
|
@@ -44011,19 +44024,19 @@ var DocumentRecordSchema = external_exports.object({
|
|
|
44011
44024
|
tags: external_exports.array(external_exports.string())
|
|
44012
44025
|
});
|
|
44013
44026
|
var CoverLetterInputSchema = external_exports.object({
|
|
44014
|
-
title:
|
|
44015
|
-
job_id:
|
|
44027
|
+
title: reqLine,
|
|
44028
|
+
job_id: optLine.describe("\uD2B9\uC815 \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD560 \uACBD\uC6B0"),
|
|
44016
44029
|
is_primary: external_exports.boolean().optional(),
|
|
44017
|
-
content:
|
|
44030
|
+
content: optBody.describe("\uCD08\uAE30 \uBC84\uC804 \uBCF8\uBB38 (\uC788\uC73C\uBA74 v1\uB85C \uC800\uC7A5)"),
|
|
44018
44031
|
source: external_exports.enum(CONTENT_SOURCES).optional(),
|
|
44019
|
-
note:
|
|
44032
|
+
note: optNote.describe("\uCD08\uAE30 \uBC84\uC804 \uBA54\uBAA8")
|
|
44020
44033
|
});
|
|
44021
44034
|
var CoverLetterVersionInputSchema = external_exports.object({
|
|
44022
|
-
cover_letter_id:
|
|
44023
|
-
title: external_exports.string().optional().describe("\uC0C8 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB9CC\uB4E4 \uB54C \uC81C\uBAA9"),
|
|
44024
|
-
job_id:
|
|
44025
|
-
content:
|
|
44026
|
-
note:
|
|
44035
|
+
cover_letter_id: optLine.describe("\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uC5D0 \uC0C8 \uBC84\uC804 \uCD94\uAC00 \uC2DC"),
|
|
44036
|
+
title: external_exports.string().max(MAX_LINE).optional().describe("\uC0C8 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB9CC\uB4E4 \uB54C \uC81C\uBAA9"),
|
|
44037
|
+
job_id: optLine,
|
|
44038
|
+
content: reqBody,
|
|
44039
|
+
note: optNote.describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
|
|
44027
44040
|
source: external_exports.enum(CONTENT_SOURCES).optional(),
|
|
44028
44041
|
set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)")
|
|
44029
44042
|
});
|
|
@@ -44047,16 +44060,16 @@ var CoverLetterRecordSchema = external_exports.object({
|
|
|
44047
44060
|
versions: external_exports.array(CoverLetterVersionRecordSchema).optional()
|
|
44048
44061
|
});
|
|
44049
44062
|
var JobInputSchema = external_exports.object({
|
|
44050
|
-
company:
|
|
44051
|
-
position:
|
|
44063
|
+
company: reqLine,
|
|
44064
|
+
position: reqLine.describe("\uC9C1\uBB34/\uD3EC\uC9C0\uC158\uBA85"),
|
|
44052
44065
|
url: SafeUrlOptional,
|
|
44053
|
-
location:
|
|
44054
|
-
employment_type:
|
|
44055
|
-
description:
|
|
44056
|
-
requirements:
|
|
44057
|
-
keywords:
|
|
44058
|
-
deadline: external_exports.string().optional().describe("\uB9C8\uAC10\uC77C YYYY-MM-DD"),
|
|
44059
|
-
source: external_exports.string().optional().describe("\uCD9C\uCC98 (\uC0AC\uB78C\uC778/\uC6D0\uD2F0\uB4DC/\uC9C1\uC811 \uC785\uB825 \uB4F1)")
|
|
44066
|
+
location: optLine,
|
|
44067
|
+
employment_type: optLine,
|
|
44068
|
+
description: optBody.describe("\uACF5\uACE0 \uC6D0\uBB38 \uB610\uB294 \uC815\uB9AC\uB41C \uD14D\uC2A4\uD2B8"),
|
|
44069
|
+
requirements: strList.optional().describe("\uC790\uACA9\uC694\uAC74/\uC6B0\uB300\uC0AC\uD56D \uD575\uC2EC"),
|
|
44070
|
+
keywords: strList.optional().describe("\uD575\uC2EC \uD0A4\uC6CC\uB4DC"),
|
|
44071
|
+
deadline: external_exports.string().max(MAX_LINE).optional().describe("\uB9C8\uAC10\uC77C YYYY-MM-DD"),
|
|
44072
|
+
source: external_exports.string().max(MAX_LINE).optional().describe("\uCD9C\uCC98 (\uC0AC\uB78C\uC778/\uC6D0\uD2F0\uB4DC/\uC9C1\uC811 \uC785\uB825 \uB4F1)")
|
|
44060
44073
|
});
|
|
44061
44074
|
var JobRecordSchema = external_exports.object({
|
|
44062
44075
|
...baseRecord,
|
|
@@ -44072,14 +44085,14 @@ var JobRecordSchema = external_exports.object({
|
|
|
44072
44085
|
source: external_exports.string().nullable()
|
|
44073
44086
|
});
|
|
44074
44087
|
var FitAnalysisInputSchema = external_exports.object({
|
|
44075
|
-
job_id:
|
|
44088
|
+
job_id: reqLine.describe("\uBD84\uC11D \uB300\uC0C1 \uACF5\uACE0 ID"),
|
|
44076
44089
|
score: external_exports.number().min(0).max(100).optional().describe("\uC885\uD569 \uC801\uD569\uB3C4 0~100"),
|
|
44077
|
-
summary: external_exports.string().optional().describe("\uD55C\uB450 \uBB38\uB2E8 \uC694\uC57D"),
|
|
44078
|
-
strengths:
|
|
44079
|
-
gaps:
|
|
44080
|
-
matched_keywords:
|
|
44081
|
-
missing_keywords:
|
|
44082
|
-
recommendations:
|
|
44090
|
+
summary: external_exports.string().max(MAX_NOTE).optional().describe("\uD55C\uB450 \uBB38\uB2E8 \uC694\uC57D"),
|
|
44091
|
+
strengths: strList.optional().describe("\uAC15\uC810 / \uC798 \uB9DE\uB294 \uBD80\uBD84"),
|
|
44092
|
+
gaps: strList.optional().describe("\uBD80\uC871\uD55C \uBD80\uBD84 / \uBCF4\uC644 \uD544\uC694"),
|
|
44093
|
+
matched_keywords: strList.optional(),
|
|
44094
|
+
missing_keywords: strList.optional(),
|
|
44095
|
+
recommendations: strList.optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C/\uC9C0\uC6D0 \uC804\uB7B5 \uC81C\uC548")
|
|
44083
44096
|
});
|
|
44084
44097
|
var FitAnalysisRecordSchema = external_exports.object({
|
|
44085
44098
|
...baseRecord,
|
|
@@ -44093,18 +44106,18 @@ var FitAnalysisRecordSchema = external_exports.object({
|
|
|
44093
44106
|
recommendations: external_exports.array(external_exports.string())
|
|
44094
44107
|
});
|
|
44095
44108
|
var ApplicationInputSchema = external_exports.object({
|
|
44096
|
-
job_id:
|
|
44109
|
+
job_id: reqLine,
|
|
44097
44110
|
status: external_exports.enum(APPLICATION_STATUSES).optional(),
|
|
44098
|
-
resume_id:
|
|
44099
|
-
cover_letter_id:
|
|
44100
|
-
applied_at:
|
|
44101
|
-
notes:
|
|
44111
|
+
resume_id: optLine,
|
|
44112
|
+
cover_letter_id: optLine,
|
|
44113
|
+
applied_at: optLine,
|
|
44114
|
+
notes: optNote
|
|
44102
44115
|
});
|
|
44103
44116
|
var ApplicationStatusUpdateSchema = external_exports.object({
|
|
44104
|
-
job_id:
|
|
44105
|
-
application_id:
|
|
44117
|
+
job_id: optLine,
|
|
44118
|
+
application_id: optLine,
|
|
44106
44119
|
status: external_exports.enum(APPLICATION_STATUSES),
|
|
44107
|
-
note:
|
|
44120
|
+
note: optNote.describe("\uC0C1\uD0DC \uBCC0\uACBD \uC0AC\uC720/\uBA54\uBAA8")
|
|
44108
44121
|
});
|
|
44109
44122
|
var ApplicationRecordSchema = external_exports.object({
|
|
44110
44123
|
...baseRecord,
|
|
@@ -44116,24 +44129,24 @@ var ApplicationRecordSchema = external_exports.object({
|
|
|
44116
44129
|
notes: external_exports.string().nullable()
|
|
44117
44130
|
});
|
|
44118
44131
|
var StarGuideSchema = external_exports.object({
|
|
44119
|
-
question: external_exports.string(),
|
|
44120
|
-
situation:
|
|
44121
|
-
task:
|
|
44122
|
-
action:
|
|
44123
|
-
result:
|
|
44132
|
+
question: external_exports.string().max(MAX_NOTE),
|
|
44133
|
+
situation: optNote,
|
|
44134
|
+
task: optNote,
|
|
44135
|
+
action: optNote,
|
|
44136
|
+
result: optNote
|
|
44124
44137
|
});
|
|
44125
44138
|
var InterviewQuestionSchema = external_exports.object({
|
|
44126
|
-
question: external_exports.string(),
|
|
44127
|
-
intent: external_exports.string().optional().describe("\uC9C8\uBB38 \uC758\uB3C4"),
|
|
44128
|
-
followups:
|
|
44129
|
-
answer_outline:
|
|
44139
|
+
question: external_exports.string().max(MAX_NOTE),
|
|
44140
|
+
intent: external_exports.string().max(MAX_NOTE).optional().describe("\uC9C8\uBB38 \uC758\uB3C4"),
|
|
44141
|
+
followups: strList.optional().describe("\uC608\uC0C1 \uAF2C\uB9AC \uC9C8\uBB38"),
|
|
44142
|
+
answer_outline: optBody.describe("\uB2F5\uBCC0 \uAC00\uC774\uB4DC/\uD575\uC2EC \uD3EC\uC778\uD2B8")
|
|
44130
44143
|
});
|
|
44131
44144
|
var InterviewPrepInputSchema = external_exports.object({
|
|
44132
|
-
job_id:
|
|
44133
|
-
questions: external_exports.array(InterviewQuestionSchema).optional(),
|
|
44134
|
-
star_guides: external_exports.array(StarGuideSchema).optional(),
|
|
44135
|
-
self_introduction:
|
|
44136
|
-
notes:
|
|
44145
|
+
job_id: reqLine,
|
|
44146
|
+
questions: external_exports.array(InterviewQuestionSchema).max(MAX_ITEMS).optional(),
|
|
44147
|
+
star_guides: external_exports.array(StarGuideSchema).max(MAX_ITEMS).optional(),
|
|
44148
|
+
self_introduction: optBody.describe("1\uBD84 \uC790\uAE30\uC18C\uAC1C \uCD08\uC548"),
|
|
44149
|
+
notes: optNote.describe("\uBA74\uC811 \uD6C4\uAE30/\uBA54\uBAA8")
|
|
44137
44150
|
});
|
|
44138
44151
|
var InterviewPrepRecordSchema = external_exports.object({
|
|
44139
44152
|
...baseRecord,
|
|
@@ -44187,7 +44200,7 @@ var ApplicationContextSchema = external_exports.object({
|
|
|
44187
44200
|
});
|
|
44188
44201
|
|
|
44189
44202
|
// packages/shared/src/build-info.ts
|
|
44190
|
-
var APP_VERSION = true ? "0.1.
|
|
44203
|
+
var APP_VERSION = true ? "0.1.3" : "0.0.0-dev";
|
|
44191
44204
|
var BUNDLED = true;
|
|
44192
44205
|
|
|
44193
44206
|
// packages/shared/src/index.ts
|
|
@@ -44903,7 +44916,21 @@ var activityRepo = {
|
|
|
44903
44916
|
};
|
|
44904
44917
|
function getEntityCounts() {
|
|
44905
44918
|
const db = getDb();
|
|
44906
|
-
const
|
|
44919
|
+
const ALLOWED_TABLES = /* @__PURE__ */ new Set([
|
|
44920
|
+
"profile",
|
|
44921
|
+
"experiences",
|
|
44922
|
+
"projects",
|
|
44923
|
+
"skills",
|
|
44924
|
+
"documents",
|
|
44925
|
+
"cover_letters",
|
|
44926
|
+
"jobs",
|
|
44927
|
+
"applications",
|
|
44928
|
+
"interview_preps"
|
|
44929
|
+
]);
|
|
44930
|
+
const c = (t) => {
|
|
44931
|
+
if (!ALLOWED_TABLES.has(t)) throw new Error(`getEntityCounts: \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC740 \uD14C\uC774\uBE14 \uC774\uB984: ${t}`);
|
|
44932
|
+
return db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
|
|
44933
|
+
};
|
|
44907
44934
|
return {
|
|
44908
44935
|
profile: c("profile"),
|
|
44909
44936
|
experiences: c("experiences"),
|
|
@@ -44979,7 +45006,7 @@ function injectToken(html) {
|
|
|
44979
45006
|
// apps/web/src/http.ts
|
|
44980
45007
|
import fs3 from "node:fs";
|
|
44981
45008
|
import path3 from "node:path";
|
|
44982
|
-
var
|
|
45009
|
+
var MAX_BODY2 = 8 * 1024 * 1024;
|
|
44983
45010
|
var Router = class {
|
|
44984
45011
|
routes = [];
|
|
44985
45012
|
add(method, path6, handler) {
|
|
@@ -45074,7 +45101,7 @@ function readBody(req) {
|
|
|
45074
45101
|
const chunks = [];
|
|
45075
45102
|
req.on("data", (c) => {
|
|
45076
45103
|
size2 += c.length;
|
|
45077
|
-
if (size2 >
|
|
45104
|
+
if (size2 > MAX_BODY2) {
|
|
45078
45105
|
reject2(new HttpError(413, "\uC694\uCCAD \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4.", "too_large"));
|
|
45079
45106
|
req.destroy();
|
|
45080
45107
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "careermate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CareerMate — an MCP-first, local-first career workspace. Connect it to a local AI agent (Claude Desktop, Claude Code, Codex); CareerMate stores your career data locally on your machine and exposes it over MCP.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"apps",
|
|
30
30
|
"packages",
|
|
31
31
|
"scripts",
|
|
32
|
-
"docs",
|
|
33
32
|
"tsconfig.json",
|
|
34
33
|
"README.md",
|
|
35
34
|
"THIRD_PARTY_NOTICES.md"
|
|
@@ -859,7 +859,24 @@ export const activityRepo = {
|
|
|
859
859
|
*/
|
|
860
860
|
export function getEntityCounts(): Record<string, number> {
|
|
861
861
|
const db = getDb();
|
|
862
|
-
|
|
862
|
+
// Table names are hardcoded below, but the name is interpolated into SQL, so
|
|
863
|
+
// guard with an allowlist as defense-in-depth: a future caller can never turn
|
|
864
|
+
// this into an injection sink even if `t` were sourced from elsewhere.
|
|
865
|
+
const ALLOWED_TABLES = new Set([
|
|
866
|
+
'profile',
|
|
867
|
+
'experiences',
|
|
868
|
+
'projects',
|
|
869
|
+
'skills',
|
|
870
|
+
'documents',
|
|
871
|
+
'cover_letters',
|
|
872
|
+
'jobs',
|
|
873
|
+
'applications',
|
|
874
|
+
'interview_preps',
|
|
875
|
+
]);
|
|
876
|
+
const c = (t: string): number => {
|
|
877
|
+
if (!ALLOWED_TABLES.has(t)) throw new Error(`getEntityCounts: 허용되지 않은 테이블 이름: ${t}`);
|
|
878
|
+
return (db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get() as { n: number }).n;
|
|
879
|
+
};
|
|
863
880
|
return {
|
|
864
881
|
profile: c('profile'),
|
|
865
882
|
experiences: c('experiences'),
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
FitAnalysisInputSchema,
|
|
23
23
|
CoverLetterVersionInputSchema,
|
|
24
24
|
InterviewPrepInputSchema,
|
|
25
|
+
DocumentInputSchema,
|
|
25
26
|
APPLICATION_STATUSES,
|
|
26
27
|
APPLICATION_STATUS_LABELS,
|
|
27
28
|
DOCUMENT_KINDS,
|
|
@@ -272,13 +273,12 @@ export const TOOLS: ToolDef[] = [
|
|
|
272
273
|
title: '이력서/경력기술서 추가',
|
|
273
274
|
description:
|
|
274
275
|
'이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트(또는 정리한 Markdown)를 넣으세요. kind 기본값은 resume입니다. is_primary=true로 대표 문서를 지정하면 get_application_context의 primary_resume로 노출됩니다.',
|
|
276
|
+
// Reuse the shared (length-bounded, whitespace-rejecting) Document schema so
|
|
277
|
+
// the MCP ingestion path — the primary route for résumé text — inherits the
|
|
278
|
+
// same caps as the web API. kind stays optional here (defaults to resume).
|
|
275
279
|
inputSchema: {
|
|
276
|
-
|
|
277
|
-
content: z.string().describe('본문 (Markdown/일반 텍스트)'),
|
|
280
|
+
...DocumentInputSchema.omit({ kind: true }).shape,
|
|
278
281
|
kind: z.enum(DOCUMENT_KINDS).optional().describe('resume(이력서)/career_description(경력기술서)/portfolio/other'),
|
|
279
|
-
source: z.enum(CONTENT_SOURCES).optional(),
|
|
280
|
-
is_primary: z.boolean().optional().describe('대표 문서 여부'),
|
|
281
|
-
tags: z.array(z.string()).optional(),
|
|
282
282
|
},
|
|
283
283
|
handler: (args) => {
|
|
284
284
|
const doc = addResume(args);
|
|
@@ -427,9 +427,21 @@ async function finishPdf(buf: Buffer, base: BaseFn): Promise<ExtractDocumentResu
|
|
|
427
427
|
});
|
|
428
428
|
return base('pdf', { text, bytes: buf.length, warnings: pageWarnings });
|
|
429
429
|
} catch (e) {
|
|
430
|
+
// pdfjs throws a named PasswordException for password-protected PDFs — give
|
|
431
|
+
// a distinct, actionable hint instead of the generic "corrupted or encrypted".
|
|
432
|
+
const name = e && typeof e === 'object' && 'name' in e ? String((e as { name?: unknown }).name) : '';
|
|
433
|
+
if (name === 'PasswordException') {
|
|
434
|
+
return base('unsupported', {
|
|
435
|
+
bytes: buf.length,
|
|
436
|
+
warnings: [
|
|
437
|
+
'이 PDF는 암호(비밀번호)로 보호되어 있어 읽을 수 없습니다. PDF 뷰어에서 암호를 해제해 다시 저장하거나, 내용을 텍스트로 붙여넣어 주세요.',
|
|
438
|
+
],
|
|
439
|
+
unsupported: true,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
430
442
|
return base('unsupported', {
|
|
431
443
|
bytes: buf.length,
|
|
432
|
-
warnings: [`PDF를 읽지 못했습니다(${errMsg(e)}).
|
|
444
|
+
warnings: [`PDF를 읽지 못했습니다(${errMsg(e)}). 손상되었거나 지원하지 않는 PDF일 수 있습니다. 텍스트로 붙여넣어 주세요.`],
|
|
433
445
|
unsupported: true,
|
|
434
446
|
});
|
|
435
447
|
}
|