careermate 0.1.0
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/README.md +256 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/apps/mcp/src/index.ts +66 -0
- package/apps/web/DESIGN_GUIDE.md +105 -0
- package/apps/web/UI_CONTRACT.md +44 -0
- package/apps/web/public/app.js +118 -0
- package/apps/web/public/fonts/PretendardVariable.woff2 +0 -0
- package/apps/web/public/index.html +41 -0
- package/apps/web/public/lib.js +282 -0
- package/apps/web/public/pages/applications.js +98 -0
- package/apps/web/public/pages/documents.js +446 -0
- package/apps/web/public/pages/home.js +263 -0
- package/apps/web/public/pages/interview.js +230 -0
- package/apps/web/public/pages/jobs.js +494 -0
- package/apps/web/public/pages/profile.js +576 -0
- package/apps/web/public/pages/settings.js +233 -0
- package/apps/web/public/styles.css +426 -0
- package/apps/web/src/exports.ts +68 -0
- package/apps/web/src/http.ts +180 -0
- package/apps/web/src/index.ts +49 -0
- package/apps/web/src/info.ts +50 -0
- package/apps/web/src/routes.ts +350 -0
- package/apps/web/src/security.ts +102 -0
- package/apps/web/src/server.ts +141 -0
- package/apps/web/src/settings.ts +88 -0
- package/bin/careermate.mjs +74 -0
- package/dist/careermate.mcpb +0 -0
- package/dist/install-page/index.html +474 -0
- package/dist/install-page/style.css +391 -0
- package/dist/install-page/vercel.json +20 -0
- package/dist/mcp-smoke.err +3 -0
- package/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/README.md +219 -0
- package/dist/mcpb-stage/dist/install-page/index.html +434 -0
- package/dist/mcpb-stage/dist/install-page/style.css +407 -0
- package/dist/mcpb-stage/dist/install-page/vercel.json +20 -0
- package/dist/mcpb-stage/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/dist/public/app.js +118 -0
- package/dist/mcpb-stage/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/mcpb-stage/dist/public/index.html +41 -0
- package/dist/mcpb-stage/dist/public/lib.js +282 -0
- package/dist/mcpb-stage/dist/public/pages/applications.js +98 -0
- package/dist/mcpb-stage/dist/public/pages/documents.js +446 -0
- package/dist/mcpb-stage/dist/public/pages/home.js +263 -0
- package/dist/mcpb-stage/dist/public/pages/interview.js +230 -0
- package/dist/mcpb-stage/dist/public/pages/jobs.js +494 -0
- package/dist/mcpb-stage/dist/public/pages/profile.js +576 -0
- package/dist/mcpb-stage/dist/public/pages/settings.js +233 -0
- package/dist/mcpb-stage/dist/public/styles.css +420 -0
- package/dist/mcpb-stage/dist/web.mjs +7240 -0
- package/dist/mcpb-stage/manifest.json +40 -0
- package/dist/public/app.js +118 -0
- package/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/public/index.html +41 -0
- package/dist/public/lib.js +282 -0
- package/dist/public/pages/applications.js +98 -0
- package/dist/public/pages/documents.js +446 -0
- package/dist/public/pages/home.js +263 -0
- package/dist/public/pages/interview.js +230 -0
- package/dist/public/pages/jobs.js +494 -0
- package/dist/public/pages/profile.js +576 -0
- package/dist/public/pages/settings.js +233 -0
- package/dist/public/styles.css +426 -0
- package/dist/web.mjs +7240 -0
- package/docs/ARCHITECTURE.md +208 -0
- package/docs/CHANGES_V1.md +103 -0
- package/docs/DATA_MODEL.md +460 -0
- package/docs/DECISIONS.md +277 -0
- package/docs/DEMO.md +242 -0
- package/docs/INSTALL.md +148 -0
- package/docs/INSTALL_AND_USAGE.md +99 -0
- package/docs/MCP_TOOLS.md +233 -0
- package/docs/ROADMAP.md +134 -0
- package/docs/START_WORKFLOW.md +125 -0
- package/docs/SUPPORTED_AI_APPS.md +60 -0
- package/docs/TODO.md +57 -0
- package/docs/UX_NOTES.md +247 -0
- package/docs/WORKFLOWS.md +200 -0
- package/install-page/index.html +474 -0
- package/install-page/style.css +391 -0
- package/install-page/vercel.json +20 -0
- package/package.json +68 -0
- package/packages/core/src/context.ts +74 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/onboarding.ts +81 -0
- package/packages/core/src/services.ts +146 -0
- package/packages/core/src/summary.ts +104 -0
- package/packages/db/src/connection.ts +46 -0
- package/packages/db/src/index.ts +22 -0
- package/packages/db/src/paths.ts +41 -0
- package/packages/db/src/repositories.ts +828 -0
- package/packages/db/src/runtime.ts +58 -0
- package/packages/db/src/schema.ts +189 -0
- package/packages/exporters/src/html.ts +113 -0
- package/packages/exporters/src/index.ts +364 -0
- package/packages/exporters/src/markdown.ts +178 -0
- package/packages/mcp-tools/src/bridge.ts +83 -0
- package/packages/mcp-tools/src/index.ts +8 -0
- package/packages/mcp-tools/src/result.ts +49 -0
- package/packages/mcp-tools/src/tools.ts +455 -0
- package/packages/parsers/src/html.ts +86 -0
- package/packages/parsers/src/index.ts +228 -0
- package/packages/parsers/src/keywords.ts +151 -0
- package/packages/prompts/src/humanize.ts +59 -0
- package/packages/prompts/src/index.ts +82 -0
- package/packages/prompts/src/install.ts +43 -0
- package/packages/prompts/src/onboarding.ts +35 -0
- package/packages/prompts/src/system.ts +53 -0
- package/packages/shared/src/enums.ts +103 -0
- package/packages/shared/src/index.ts +18 -0
- package/packages/shared/src/schemas.ts +398 -0
- package/packages/workflows/src/definitions.ts +107 -0
- package/packages/workflows/src/index.ts +39 -0
- package/scripts/build-dist.mjs +62 -0
- package/scripts/build-mcpb.mjs +70 -0
- package/scripts/doctor.ts +81 -0
- package/scripts/init.ts +342 -0
- package/scripts/mcp-probe.ts +55 -0
- package/scripts/migrate.ts +6 -0
- package/scripts/run.mjs +33 -0
- package/scripts/seed.ts +129 -0
- package/scripts/test.ts +117 -0
- package/scripts/ui-smoke.ts +73 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
CareerMate — public landing / install page.
|
|
3
|
+
Self-contained: no external fonts, no CDN, no network (works fully offline).
|
|
4
|
+
|
|
5
|
+
Design language synthesized from established install/registry sites
|
|
6
|
+
(Homebrew, Bun, npm, MCP docs), not copied from any one:
|
|
7
|
+
· restraint over decoration — one flat accent, warm-neutral paper, no
|
|
8
|
+
gradients / glows / gradient-text
|
|
9
|
+
· the two install paths (.mcpb one-click / terminal) are the focal point
|
|
10
|
+
· system sans for prose, monospace for commands → reads as a real tool
|
|
11
|
+
· trust through whitespace, hairlines and consistency
|
|
12
|
+
Light + dark (system preference or manual toggle).
|
|
13
|
+
========================================================================== */
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* warm paper neutrals (light) */
|
|
17
|
+
--bg: #faf8f4;
|
|
18
|
+
--bg-elev: #ffffff;
|
|
19
|
+
--bg-subtle: #f2efe8;
|
|
20
|
+
--bg-sunken: #ece8df;
|
|
21
|
+
--surface: #ffffff;
|
|
22
|
+
--surface-hover: #f5f2ec;
|
|
23
|
+
--border: #e6e1d6;
|
|
24
|
+
--border-strong: #d6d0c2;
|
|
25
|
+
--text: #1c1a16;
|
|
26
|
+
--text-secondary: #595449;
|
|
27
|
+
--text-tertiary: #8b867a;
|
|
28
|
+
|
|
29
|
+
/* single flat accent — deep emerald (growth / "proceed"), no gradient */
|
|
30
|
+
--accent: #0f7256;
|
|
31
|
+
--accent-hover: #0b5b44;
|
|
32
|
+
--accent-soft: #e7f1ec;
|
|
33
|
+
--accent-border: #bcdccd;
|
|
34
|
+
--text-on-accent: #ffffff;
|
|
35
|
+
|
|
36
|
+
--code-bg: #f4f1ea;
|
|
37
|
+
--code-text: #2a2722;
|
|
38
|
+
--warn-bg: #fbf3e3;
|
|
39
|
+
--warn-border: #ecd9ac;
|
|
40
|
+
--warn-text: #876214;
|
|
41
|
+
|
|
42
|
+
--shadow-sm: 0 1px 2px rgba(28,26,22,.05);
|
|
43
|
+
--shadow-md: 0 4px 14px rgba(28,26,22,.07);
|
|
44
|
+
|
|
45
|
+
--radius-sm: 6px;
|
|
46
|
+
--radius: 9px;
|
|
47
|
+
--radius-lg: 12px;
|
|
48
|
+
--maxw: 1000px;
|
|
49
|
+
|
|
50
|
+
--font: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Malgun Gothic',
|
|
51
|
+
'Apple SD Gothic Neo', 'Noto Sans KR', Roboto, sans-serif;
|
|
52
|
+
--mono: 'Cascadia Code', 'SFMono-Regular', 'JetBrains Mono', Consolas, 'D2Coding', ui-monospace, monospace;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
:root:not([data-theme='light']) { color-scheme: dark; }
|
|
57
|
+
}
|
|
58
|
+
:root[data-theme='dark'] { color-scheme: dark; }
|
|
59
|
+
|
|
60
|
+
@media (prefers-color-scheme: dark) {
|
|
61
|
+
:root:not([data-theme='light']) {
|
|
62
|
+
--bg: #14130f; --bg-elev: #1b1915; --bg-subtle: #201d17; --bg-sunken: #100f0b;
|
|
63
|
+
--surface: #1b1915; --surface-hover: #262219;
|
|
64
|
+
--border: #2e2a22; --border-strong: #3e3930;
|
|
65
|
+
--text: #f2eee5; --text-secondary: #aaa599; --text-tertiary: #79746a;
|
|
66
|
+
--accent: #34d399; --accent-hover: #6ee7b7; --accent-soft: #11281d; --accent-border: #1f4f3c; --text-on-accent: #05241a;
|
|
67
|
+
--code-bg: #100f0b; --code-text: #d6d1c6;
|
|
68
|
+
--warn-bg: #292008; --warn-border: #574510; --warn-text: #e6c578;
|
|
69
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,.4); --shadow-md: 0 4px 14px rgba(0,0,0,.45);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
:root[data-theme='dark'] {
|
|
73
|
+
--bg: #14130f; --bg-elev: #1b1915; --bg-subtle: #201d17; --bg-sunken: #100f0b;
|
|
74
|
+
--surface: #1b1915; --surface-hover: #262219;
|
|
75
|
+
--border: #2e2a22; --border-strong: #3e3930;
|
|
76
|
+
--text: #f2eee5; --text-secondary: #aaa599; --text-tertiary: #79746a;
|
|
77
|
+
--accent: #34d399; --accent-hover: #6ee7b7; --accent-soft: #11281d; --accent-border: #1f4f3c;
|
|
78
|
+
--code-bg: #100f0b; --code-text: #d6d1c6;
|
|
79
|
+
--warn-bg: #292008; --warn-border: #574510; --warn-text: #e6c578;
|
|
80
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,.4); --shadow-md: 0 4px 14px rgba(0,0,0,.45);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
* { box-sizing: border-box; }
|
|
84
|
+
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
|
|
85
|
+
body {
|
|
86
|
+
margin: 0; background: var(--bg); color: var(--text);
|
|
87
|
+
font-family: var(--font); font-size: 16px; line-height: 1.65;
|
|
88
|
+
word-break: keep-all; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
|
89
|
+
}
|
|
90
|
+
h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; line-height: 1.25; }
|
|
91
|
+
p { margin: 0; }
|
|
92
|
+
a { color: var(--accent); text-decoration: none; }
|
|
93
|
+
a:hover { text-decoration: underline; }
|
|
94
|
+
code { font-family: var(--mono); }
|
|
95
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
|
96
|
+
::selection { background: color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
97
|
+
|
|
98
|
+
.accent { color: var(--accent); }
|
|
99
|
+
|
|
100
|
+
/* ----------------------------------------------------------------- nav */
|
|
101
|
+
.nav {
|
|
102
|
+
position: sticky; top: 0; z-index: 50;
|
|
103
|
+
display: flex; align-items: center; gap: 18px;
|
|
104
|
+
max-width: var(--maxw); margin: 0 auto; padding: 15px 24px;
|
|
105
|
+
background: color-mix(in srgb, var(--bg) 85%, transparent);
|
|
106
|
+
backdrop-filter: saturate(120%) blur(8px);
|
|
107
|
+
border-bottom: 1px solid var(--border);
|
|
108
|
+
}
|
|
109
|
+
.nav__brand { display: inline-flex; align-items: center; gap: 9px; color: var(--text); font-weight: 700; }
|
|
110
|
+
.nav__brand:hover { text-decoration: none; }
|
|
111
|
+
.nav__logo {
|
|
112
|
+
width: 24px; height: 24px; border-radius: 6px; flex: none;
|
|
113
|
+
background: var(--accent); color: var(--text-on-accent); font-weight: 800; font-size: 13px;
|
|
114
|
+
display: grid; place-items: center;
|
|
115
|
+
}
|
|
116
|
+
.nav__name { font-size: 15.5px; font-family: var(--mono); letter-spacing: -0.02em; }
|
|
117
|
+
.nav__links { display: flex; gap: 2px; margin-left: 12px; }
|
|
118
|
+
.nav__links a {
|
|
119
|
+
color: var(--text-secondary); font-size: 14px; font-weight: 500;
|
|
120
|
+
padding: 6px 10px; border-radius: var(--radius-sm);
|
|
121
|
+
}
|
|
122
|
+
.nav__links a:hover { color: var(--text); background: var(--surface-hover); text-decoration: none; }
|
|
123
|
+
.nav__actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
|
124
|
+
|
|
125
|
+
.theme-toggle {
|
|
126
|
+
width: 34px; height: 34px; display: grid; place-items: center;
|
|
127
|
+
border: 1px solid var(--border-strong); background: var(--surface); border-radius: var(--radius-sm);
|
|
128
|
+
color: var(--text-secondary); cursor: pointer; transition: background .15s, color .15s;
|
|
129
|
+
}
|
|
130
|
+
.theme-toggle:hover { background: var(--surface-hover); color: var(--text); }
|
|
131
|
+
.theme-toggle svg { width: 16px; height: 16px; }
|
|
132
|
+
.theme-toggle .i-moon { display: none; }
|
|
133
|
+
.theme-toggle .i-sun { display: block; }
|
|
134
|
+
:root[data-theme='dark'] .theme-toggle .i-moon { display: block; }
|
|
135
|
+
:root[data-theme='dark'] .theme-toggle .i-sun { display: none; }
|
|
136
|
+
@media (prefers-color-scheme: dark) {
|
|
137
|
+
:root:not([data-theme='light']) .theme-toggle .i-moon { display: block; }
|
|
138
|
+
:root:not([data-theme='light']) .theme-toggle .i-sun { display: none; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ----------------------------------------------------------------- buttons */
|
|
142
|
+
.btn {
|
|
143
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
144
|
+
height: 38px; padding: 0 15px; border-radius: var(--radius-sm);
|
|
145
|
+
border: 1px solid var(--border-strong); background: var(--surface); color: var(--text);
|
|
146
|
+
font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap;
|
|
147
|
+
transition: background .15s, border-color .15s;
|
|
148
|
+
}
|
|
149
|
+
.btn:hover { background: var(--surface-hover); text-decoration: none; }
|
|
150
|
+
.btn svg { width: 16px; height: 16px; flex: none; }
|
|
151
|
+
.btn--ghost { background: transparent; border-color: var(--border); color: var(--text-secondary); }
|
|
152
|
+
.btn--ghost:hover { color: var(--text); background: var(--surface-hover); }
|
|
153
|
+
.btn--primary { background: var(--accent); border-color: var(--accent); color: var(--text-on-accent); }
|
|
154
|
+
.btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
155
|
+
|
|
156
|
+
/* ----------------------------------------------------------------- hero */
|
|
157
|
+
/* single centered column — headline on top, the two install CTAs right below. */
|
|
158
|
+
.hero {
|
|
159
|
+
max-width: 720px; margin: 0 auto; padding: 66px 24px 8px; text-align: center;
|
|
160
|
+
}
|
|
161
|
+
/* gentle staggered entrance on load */
|
|
162
|
+
@keyframes riseIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
|
163
|
+
.hero > * { animation: riseIn .55s cubic-bezier(.22,.61,.36,1) both; }
|
|
164
|
+
.hero__title { animation-delay: .04s; }
|
|
165
|
+
.hero__lede { animation-delay: .10s; }
|
|
166
|
+
.hero__cta { animation-delay: .18s; }
|
|
167
|
+
.hero__note { animation-delay: .24s; }
|
|
168
|
+
.pill-badge {
|
|
169
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
170
|
+
padding: 5px 13px; border-radius: 999px; font-size: 12.5px; font-weight: 600;
|
|
171
|
+
color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border);
|
|
172
|
+
}
|
|
173
|
+
.pill-badge__dot { width: 6px; height: 6px; border-radius: 999px; background: var(--accent); }
|
|
174
|
+
.hero__title { font-size: clamp(2.05rem, 5vw, 3rem); margin: 20px 0 0; letter-spacing: -0.028em; }
|
|
175
|
+
.hero__lede {
|
|
176
|
+
font-size: clamp(1rem, 2vw, 1.12rem); color: var(--text-secondary);
|
|
177
|
+
margin: 18px auto 0; max-width: 37rem;
|
|
178
|
+
}
|
|
179
|
+
.hero__lede strong { color: var(--text); font-weight: 650; }
|
|
180
|
+
|
|
181
|
+
/* hero call-to-action — two install paths */
|
|
182
|
+
.hero__cta { margin: 28px auto 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
|
183
|
+
.hero__cta .btn { height: 42px; padding: 0 18px; font-size: 14.5px; }
|
|
184
|
+
|
|
185
|
+
.hero__note {
|
|
186
|
+
margin: 18px auto 0; max-width: 38rem; font-size: 12.5px; line-height: 1.6;
|
|
187
|
+
color: var(--text-tertiary);
|
|
188
|
+
}
|
|
189
|
+
.hero__note strong { color: var(--text-secondary); font-weight: 600; }
|
|
190
|
+
.hero__note code { font-family: var(--mono); font-size: .92em; background: var(--bg-sunken); padding: 1px 5px; border-radius: 4px; color: var(--text-secondary); }
|
|
191
|
+
|
|
192
|
+
/* ----------------------------------------------------------------- sections */
|
|
193
|
+
.section { max-width: var(--maxw); margin: 0 auto; padding: 112px 24px 0; scroll-margin-top: 80px; }
|
|
194
|
+
.section__head { max-width: 46rem; margin: 0 auto 34px; text-align: center; }
|
|
195
|
+
.eyebrow {
|
|
196
|
+
display: inline-block; font-size: 12px; font-weight: 700; letter-spacing: .09em;
|
|
197
|
+
text-transform: uppercase; color: var(--accent); margin-bottom: 9px;
|
|
198
|
+
}
|
|
199
|
+
.section__head h2 { font-size: clamp(1.4rem, 3vw, 1.9rem); }
|
|
200
|
+
.section__head p { margin-top: 11px; color: var(--text-secondary); font-size: 1rem; }
|
|
201
|
+
|
|
202
|
+
/* how-it-works */
|
|
203
|
+
.cards-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
|
204
|
+
.feature {
|
|
205
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
206
|
+
padding: 22px 20px; transition: border-color .15s;
|
|
207
|
+
}
|
|
208
|
+
.feature:hover { border-color: var(--border-strong); }
|
|
209
|
+
.feature__icon {
|
|
210
|
+
width: 40px; height: 40px; border-radius: 9px; display: grid; place-items: center; margin-bottom: 14px;
|
|
211
|
+
color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border);
|
|
212
|
+
}
|
|
213
|
+
.feature__icon svg { width: 20px; height: 20px; }
|
|
214
|
+
.feature h3 { font-size: 16px; margin-bottom: 6px; }
|
|
215
|
+
.feature p { color: var(--text-secondary); font-size: 14px; }
|
|
216
|
+
.feature code { font-family: var(--mono); font-size: .85em; background: var(--bg-sunken); padding: 1px 5px; border-radius: 4px; }
|
|
217
|
+
|
|
218
|
+
/* checklist */
|
|
219
|
+
.checks { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; max-width: 840px; margin: 0 auto; }
|
|
220
|
+
.check {
|
|
221
|
+
display: flex; gap: 12px; align-items: flex-start;
|
|
222
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
|
223
|
+
padding: 15px 17px;
|
|
224
|
+
}
|
|
225
|
+
.check > svg { width: 18px; height: 18px; flex: none; color: var(--accent); margin-top: 1px; }
|
|
226
|
+
.check strong { display: block; font-size: 14.5px; font-weight: 650; }
|
|
227
|
+
.check span { display: block; color: var(--text-secondary); font-size: 13px; margin-top: 2px; }
|
|
228
|
+
|
|
229
|
+
/* ----------------------------------------------------------------- install */
|
|
230
|
+
/* install track (A: .mcpb / B: terminal) */
|
|
231
|
+
.track {
|
|
232
|
+
max-width: 720px; margin: 0 auto;
|
|
233
|
+
padding: 26px 0 0;
|
|
234
|
+
}
|
|
235
|
+
.track + .track { margin-top: 14px; padding-top: 30px; border-top: 1px solid var(--border); }
|
|
236
|
+
.track__head { max-width: 720px; margin: 0 auto 18px; }
|
|
237
|
+
.track__tag {
|
|
238
|
+
display: inline-block; font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
|
|
239
|
+
color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border);
|
|
240
|
+
padding: 3px 10px; border-radius: 999px; margin-bottom: 10px;
|
|
241
|
+
}
|
|
242
|
+
.track__head h3 { font-size: 1.15rem; }
|
|
243
|
+
.track__head h3 code { font-family: var(--mono); font-size: .82em; background: var(--bg-sunken); padding: 1px 6px; border-radius: 4px; font-weight: 600; }
|
|
244
|
+
.track__head > p { margin-top: 7px; color: var(--text-secondary); font-size: 14px; }
|
|
245
|
+
.track .steps, .track .note, .track .acc { margin-left: 0; margin-right: 0; }
|
|
246
|
+
.track .step__body .btn { margin-top: 4px; }
|
|
247
|
+
|
|
248
|
+
.note {
|
|
249
|
+
border: 1px solid var(--border); border-left: 3px solid var(--accent);
|
|
250
|
+
background: var(--surface); border-radius: var(--radius);
|
|
251
|
+
padding: 13px 17px; margin: 0 auto 24px; max-width: 720px;
|
|
252
|
+
font-size: 14px; color: var(--text-secondary); line-height: 1.6;
|
|
253
|
+
}
|
|
254
|
+
.note strong { color: var(--text); }
|
|
255
|
+
.note code { font-family: var(--mono); background: var(--bg-sunken); padding: 1px 5px; border-radius: 4px; font-size: .88em; color: var(--text); }
|
|
256
|
+
|
|
257
|
+
.steps { list-style: none; margin: 0 auto; padding: 0; max-width: 720px; }
|
|
258
|
+
.step { display: flex; gap: 15px; padding: 2px 0 24px; position: relative; }
|
|
259
|
+
.step__no {
|
|
260
|
+
width: 30px; height: 30px; flex: none; border-radius: 999px;
|
|
261
|
+
display: grid; place-items: center; font-weight: 700; font-size: 14px;
|
|
262
|
+
color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border); z-index: 1;
|
|
263
|
+
}
|
|
264
|
+
.step:not(:last-of-type)::before {
|
|
265
|
+
content: ''; position: absolute; left: 15px; top: 34px; bottom: 6px; width: 1px;
|
|
266
|
+
background: var(--border); z-index: 0;
|
|
267
|
+
}
|
|
268
|
+
.step__body { flex: 1; min-width: 0; padding-top: 3px; }
|
|
269
|
+
.step__body h3 { font-size: 16px; margin-bottom: 5px; }
|
|
270
|
+
.step__body > p { color: var(--text-secondary); font-size: 14px; margin-bottom: 11px; }
|
|
271
|
+
.step__body code { font-family: var(--mono); background: var(--bg-sunken); padding: 1px 5px; border-radius: 4px; font-size: .86em; }
|
|
272
|
+
.step__tip { margin-top: 11px; font-size: 13px; color: var(--text-secondary); }
|
|
273
|
+
.step__tip code { font-family: var(--mono); background: var(--bg-sunken); padding: 1px 5px; border-radius: 4px; }
|
|
274
|
+
|
|
275
|
+
kbd {
|
|
276
|
+
font-family: inherit; font-size: .82em; background: var(--bg-sunken);
|
|
277
|
+
border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 4px; padding: 1px 6px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* code blocks */
|
|
281
|
+
.code {
|
|
282
|
+
position: relative; margin: 11px 0; background: var(--code-bg);
|
|
283
|
+
border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
|
|
284
|
+
}
|
|
285
|
+
.code__label {
|
|
286
|
+
display: block; font-family: var(--mono); font-size: 11px; letter-spacing: .03em;
|
|
287
|
+
color: var(--text-tertiary); padding: 9px 13px 0;
|
|
288
|
+
}
|
|
289
|
+
.code pre {
|
|
290
|
+
margin: 0; padding: 11px 13px 13px; overflow-x: auto;
|
|
291
|
+
font-family: var(--mono); font-size: 12.5px; line-height: 1.6; color: var(--code-text); white-space: pre;
|
|
292
|
+
}
|
|
293
|
+
.copy-btn {
|
|
294
|
+
position: absolute; top: 7px; right: 7px; font-family: inherit; font-size: 12px; font-weight: 600;
|
|
295
|
+
color: var(--text-secondary); background: var(--surface); border: 1px solid var(--border-strong);
|
|
296
|
+
border-radius: 6px; padding: 3px 10px; cursor: pointer; transition: background .12s, color .12s, border-color .12s;
|
|
297
|
+
}
|
|
298
|
+
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
299
|
+
.copy-btn.copied { color: var(--text-on-accent); background: var(--accent); border-color: var(--accent); }
|
|
300
|
+
|
|
301
|
+
/* tabs */
|
|
302
|
+
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin: 4px 0 0; flex-wrap: wrap; }
|
|
303
|
+
.tab {
|
|
304
|
+
font-family: inherit; font-size: 13.5px; font-weight: 600; color: var(--text-secondary);
|
|
305
|
+
background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px;
|
|
306
|
+
padding: 8px 12px; cursor: pointer; transition: color .12s;
|
|
307
|
+
}
|
|
308
|
+
.tab:hover { color: var(--text); }
|
|
309
|
+
.tab.is-active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
310
|
+
.panel { display: none; padding-top: 11px; }
|
|
311
|
+
.panel[data-active='true'] { display: block; }
|
|
312
|
+
.path-note { font-size: 12.5px; color: var(--text-tertiary); margin-bottom: 6px; line-height: 1.6; }
|
|
313
|
+
.path-note code { font-family: var(--mono); font-size: .9em; background: var(--bg-sunken); padding: 1px 5px; border-radius: 4px; }
|
|
314
|
+
|
|
315
|
+
/* troubleshooting accordion */
|
|
316
|
+
.acc {
|
|
317
|
+
max-width: 720px; margin: 4px auto 0; border: 1px solid var(--border);
|
|
318
|
+
border-radius: var(--radius); background: var(--surface); overflow: hidden;
|
|
319
|
+
}
|
|
320
|
+
.acc > summary {
|
|
321
|
+
cursor: pointer; list-style: none; padding: 15px 17px; font-weight: 650; font-size: 14.5px;
|
|
322
|
+
display: flex; align-items: center; gap: 10px;
|
|
323
|
+
}
|
|
324
|
+
.acc > summary::-webkit-details-marker { display: none; }
|
|
325
|
+
.acc > summary::after { content: '+'; margin-left: auto; color: var(--text-tertiary); font-weight: 400; }
|
|
326
|
+
.acc[open] > summary::after { content: '-'; }
|
|
327
|
+
.acc > summary:hover { background: var(--surface-hover); }
|
|
328
|
+
.acc__hint { font-weight: 500; font-size: 12.5px; color: var(--text-tertiary); }
|
|
329
|
+
.acc__body { padding: 0 17px 16px; }
|
|
330
|
+
.trouble { display: grid; grid-template-columns: repeat(2, 1fr); gap: 13px; }
|
|
331
|
+
.trouble__item h4 { font-size: 14px; margin-bottom: 4px; }
|
|
332
|
+
.trouble__item p { color: var(--text-secondary); font-size: 13px; }
|
|
333
|
+
.trouble__item code { font-family: var(--mono); background: var(--bg-sunken); padding: 1px 5px; border-radius: 4px; font-size: .86em; }
|
|
334
|
+
|
|
335
|
+
/* privacy */
|
|
336
|
+
.privacy-card {
|
|
337
|
+
max-width: 800px; margin: 0 auto; display: flex; gap: 20px; align-items: flex-start;
|
|
338
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
339
|
+
padding: 28px 30px;
|
|
340
|
+
}
|
|
341
|
+
.privacy-card__icon {
|
|
342
|
+
width: 50px; height: 50px; flex: none; border-radius: 12px; display: grid; place-items: center;
|
|
343
|
+
color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border);
|
|
344
|
+
}
|
|
345
|
+
.privacy-card__icon svg { width: 26px; height: 26px; }
|
|
346
|
+
.privacy-card__text h2 { font-size: 1.3rem; }
|
|
347
|
+
.privacy-card__text > p { color: var(--text-secondary); margin-top: 9px; font-size: 14.5px; }
|
|
348
|
+
.privacy-card__list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 7px; }
|
|
349
|
+
.privacy-card__list li { font-size: 13.5px; color: var(--text-secondary); }
|
|
350
|
+
.privacy-card__list strong { color: var(--text); margin-right: 8px; }
|
|
351
|
+
.privacy-card__list code { font-family: var(--mono); font-size: .86em; background: var(--bg-sunken); padding: 1px 6px; border-radius: 4px; color: var(--text); }
|
|
352
|
+
|
|
353
|
+
/* footer */
|
|
354
|
+
.foot {
|
|
355
|
+
max-width: var(--maxw); margin: 104px auto 0; padding: 32px 24px 56px;
|
|
356
|
+
border-top: 1px solid var(--border); text-align: center; color: var(--text-tertiary);
|
|
357
|
+
}
|
|
358
|
+
.foot__brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 700; color: var(--text); font-size: 15px; font-family: var(--mono); }
|
|
359
|
+
.foot p { margin: 11px 0 0; font-size: 13px; }
|
|
360
|
+
.foot__links { display: flex; justify-content: center; gap: 16px; margin-top: 14px; font-size: 13px; }
|
|
361
|
+
.foot__links a { color: var(--text-secondary); }
|
|
362
|
+
|
|
363
|
+
/* a11y */
|
|
364
|
+
.sr-only {
|
|
365
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
366
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* scroll reveal — JS adds .reveal only when motion is allowed, so no-JS /
|
|
370
|
+
reduced-motion shows everything immediately (no hidden content risk) */
|
|
371
|
+
.reveal { opacity: 0; transform: translateY(14px); transition: opacity .55s ease, transform .55s ease; }
|
|
372
|
+
.reveal.is-visible { opacity: 1; transform: none; }
|
|
373
|
+
.cards-3 .feature:nth-child(2).reveal { transition-delay: .07s; }
|
|
374
|
+
.cards-3 .feature:nth-child(3).reveal { transition-delay: .14s; }
|
|
375
|
+
.checks .check:nth-child(even).reveal { transition-delay: .05s; }
|
|
376
|
+
|
|
377
|
+
/* responsive */
|
|
378
|
+
@media (max-width: 820px) {
|
|
379
|
+
.cards-3 { grid-template-columns: 1fr; }
|
|
380
|
+
.checks { grid-template-columns: 1fr; }
|
|
381
|
+
.trouble { grid-template-columns: 1fr; }
|
|
382
|
+
.nav__links { display: none; }
|
|
383
|
+
}
|
|
384
|
+
@media (max-width: 540px) {
|
|
385
|
+
.hero { padding-top: 46px; }
|
|
386
|
+
.privacy-card { flex-direction: column; gap: 15px; padding: 22px; }
|
|
387
|
+
}
|
|
388
|
+
@media (prefers-reduced-motion: reduce) {
|
|
389
|
+
html { scroll-behavior: auto; }
|
|
390
|
+
* { animation: none !important; transition: none !important; }
|
|
391
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
3
|
+
"cleanUrls": true,
|
|
4
|
+
"trailingSlash": false,
|
|
5
|
+
"headers": [
|
|
6
|
+
{
|
|
7
|
+
"source": "/style.css",
|
|
8
|
+
"headers": [
|
|
9
|
+
{ "key": "Cache-Control", "value": "public, max-age=3600, must-revalidate" }
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"source": "/(.*)",
|
|
14
|
+
"headers": [
|
|
15
|
+
{ "key": "X-Content-Type-Options", "value": "nosniff" },
|
|
16
|
+
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "careermate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"license": "MIT",
|
|
7
|
+
"author": "CareerMate",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"career",
|
|
10
|
+
"resume",
|
|
11
|
+
"cover-letter",
|
|
12
|
+
"job-application",
|
|
13
|
+
"korean",
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"local-first",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"codex"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"careermate": "bin/careermate.mjs"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"dist",
|
|
29
|
+
"apps",
|
|
30
|
+
"packages",
|
|
31
|
+
"scripts",
|
|
32
|
+
"install-page",
|
|
33
|
+
"docs",
|
|
34
|
+
"tsconfig.json",
|
|
35
|
+
"README.md",
|
|
36
|
+
"THIRD_PARTY_NOTICES.md"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.5.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "node scripts/build-dist.mjs",
|
|
43
|
+
"build:mcpb": "node scripts/build-mcpb.mjs",
|
|
44
|
+
"prepack": "node scripts/build-dist.mjs",
|
|
45
|
+
"init": "node --no-warnings --import tsx scripts/init.ts",
|
|
46
|
+
"start": "node --no-warnings --experimental-sqlite --import tsx apps/web/src/index.ts",
|
|
47
|
+
"dev": "node --no-warnings --experimental-sqlite --watch --import tsx apps/web/src/index.ts",
|
|
48
|
+
"mcp": "node --no-warnings --experimental-sqlite --import tsx apps/mcp/src/index.ts",
|
|
49
|
+
"migrate": "node --no-warnings --experimental-sqlite --import tsx scripts/migrate.ts",
|
|
50
|
+
"seed": "node --no-warnings --experimental-sqlite --import tsx scripts/seed.ts",
|
|
51
|
+
"doctor": "node --no-warnings --experimental-sqlite --import tsx scripts/doctor.ts",
|
|
52
|
+
"test": "node --no-warnings scripts/run.mjs scripts/test.ts \"TEST_VERDICT PASS\"",
|
|
53
|
+
"test:ui": "node --no-warnings scripts/run.mjs scripts/ui-smoke.ts \"UI_VERDICT PASS\"",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
58
|
+
"tsx": "^4.19.2",
|
|
59
|
+
"zod": "^3.24.1"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@playwright/test": "^1.60.0",
|
|
63
|
+
"@types/node": "^22.10.0",
|
|
64
|
+
"esbuild": "^0.24.0",
|
|
65
|
+
"playwright": "^1.60.0",
|
|
66
|
+
"typescript": "^5.7.2"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get_application_context — the single most important read path.
|
|
3
|
+
*
|
|
4
|
+
* In ONE call the AI gets everything it needs to analyze a posting or write a
|
|
5
|
+
* cover letter: the user's profile, primary resume, experiences, projects,
|
|
6
|
+
* skills, existing cover letters, recent applications, and — when a job_id is
|
|
7
|
+
* given — the target job, its prior fit analysis, related history for the same
|
|
8
|
+
* company/role, and the user's writing preferences.
|
|
9
|
+
*
|
|
10
|
+
* Designed so the AI never has to make five round-trips before doing real work.
|
|
11
|
+
*/
|
|
12
|
+
import { type ApplicationContext } from '@careermate/shared';
|
|
13
|
+
import {
|
|
14
|
+
profileRepo,
|
|
15
|
+
documentRepo,
|
|
16
|
+
experienceRepo,
|
|
17
|
+
projectRepo,
|
|
18
|
+
skillRepo,
|
|
19
|
+
coverLetterRepo,
|
|
20
|
+
applicationRepo,
|
|
21
|
+
jobRepo,
|
|
22
|
+
fitRepo,
|
|
23
|
+
} from '@careermate/db';
|
|
24
|
+
|
|
25
|
+
export interface ApplicationContextOptions {
|
|
26
|
+
/** Target job to focus the bundle around. */
|
|
27
|
+
job_id?: string;
|
|
28
|
+
/** Cap on recent applications returned (default 10). */
|
|
29
|
+
recent_limit?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getApplicationContext(opts: ApplicationContextOptions = {}): ApplicationContext {
|
|
33
|
+
const profile = profileRepo.get();
|
|
34
|
+
const resumes = documentRepo.list('resume');
|
|
35
|
+
const primary_resume = documentRepo.primary('resume');
|
|
36
|
+
const experiences = experienceRepo.list();
|
|
37
|
+
const projects = projectRepo.list();
|
|
38
|
+
const skills = skillRepo.list();
|
|
39
|
+
const cover_letters = coverLetterRepo.list();
|
|
40
|
+
const recent_applications = applicationRepo.recent(opts.recent_limit ?? 10);
|
|
41
|
+
|
|
42
|
+
const job = opts.job_id ? jobRepo.get(opts.job_id) : null;
|
|
43
|
+
const fit_analysis = opts.job_id ? fitRepo.getByJob(opts.job_id) : null;
|
|
44
|
+
|
|
45
|
+
// Related history: prior postings at the same company or with a similar role,
|
|
46
|
+
// with their application + fit so the AI can reuse angles and avoid repeats.
|
|
47
|
+
let related_history: ApplicationContext['related_history'] = [];
|
|
48
|
+
if (job) {
|
|
49
|
+
const related = jobRepo.relatedTo(job.company, job.position, job.id);
|
|
50
|
+
related_history = related.map((rj) => ({
|
|
51
|
+
job: rj,
|
|
52
|
+
application: applicationRepo.getByJob(rj.id),
|
|
53
|
+
fit_analysis: fitRepo.getByJob(rj.id),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
profile,
|
|
59
|
+
primary_resume,
|
|
60
|
+
resumes,
|
|
61
|
+
experiences,
|
|
62
|
+
projects,
|
|
63
|
+
skills,
|
|
64
|
+
cover_letters,
|
|
65
|
+
recent_applications,
|
|
66
|
+
job,
|
|
67
|
+
fit_analysis,
|
|
68
|
+
related_history,
|
|
69
|
+
writing_preferences: {
|
|
70
|
+
preferred_tone: profile?.preferred_tone ?? null,
|
|
71
|
+
emphasis_points: profile?.emphasis_points ?? [],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/core — transport-agnostic business use-cases. The HTTP API and the
|
|
3
|
+
* MCP server both call into these so the dashboard and the AI always agree.
|
|
4
|
+
*/
|
|
5
|
+
export * from './onboarding.ts';
|
|
6
|
+
export * from './context.ts';
|
|
7
|
+
export * from './services.ts';
|
|
8
|
+
export * from './summary.ts';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding status + profile completeness.
|
|
3
|
+
*
|
|
4
|
+
* "Completed" is intentionally generous: the moment the user has a profile and
|
|
5
|
+
* at least one document, the AI should stop nagging and move to real work.
|
|
6
|
+
* `next_steps` is what powers the "지금 해야 할 일" guidance everywhere.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
type OnboardingStatus,
|
|
10
|
+
type ProfileRecord,
|
|
11
|
+
} from '@careermate/shared';
|
|
12
|
+
import {
|
|
13
|
+
profileRepo,
|
|
14
|
+
documentRepo,
|
|
15
|
+
coverLetterRepo,
|
|
16
|
+
experienceRepo,
|
|
17
|
+
skillRepo,
|
|
18
|
+
jobRepo,
|
|
19
|
+
} from '@careermate/db';
|
|
20
|
+
|
|
21
|
+
/** 0–100 score over the fields that make profiles useful for the AI. */
|
|
22
|
+
export function profileCompleteness(p: ProfileRecord | null): number {
|
|
23
|
+
if (!p) return 0;
|
|
24
|
+
const checks: boolean[] = [
|
|
25
|
+
!!p.name,
|
|
26
|
+
!!(p.headline || p.summary),
|
|
27
|
+
!!p.location,
|
|
28
|
+
p.desired_roles.length > 0,
|
|
29
|
+
!!p.desired_conditions,
|
|
30
|
+
!!p.preferred_tone,
|
|
31
|
+
p.emphasis_points.length > 0,
|
|
32
|
+
p.links.length > 0,
|
|
33
|
+
experienceRepo.list().length > 0,
|
|
34
|
+
skillRepo.list().length > 0,
|
|
35
|
+
];
|
|
36
|
+
const score = Math.round((checks.filter(Boolean).length / checks.length) * 100);
|
|
37
|
+
return score;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getOnboardingStatus(): OnboardingStatus {
|
|
41
|
+
const profile = profileRepo.get();
|
|
42
|
+
const resumes = documentRepo.list('resume');
|
|
43
|
+
const coverLetters = coverLetterRepo.list();
|
|
44
|
+
const experiences = experienceRepo.list();
|
|
45
|
+
const skills = skillRepo.list();
|
|
46
|
+
const jobs = jobRepo.list();
|
|
47
|
+
|
|
48
|
+
const has_profile = !!(profile && profile.name);
|
|
49
|
+
const has_resume = resumes.length > 0;
|
|
50
|
+
const has_cover_letter = coverLetters.length > 0;
|
|
51
|
+
const has_experience = experiences.length > 0;
|
|
52
|
+
const has_skills = skills.length > 0;
|
|
53
|
+
const has_job = jobs.length > 0;
|
|
54
|
+
|
|
55
|
+
const completeness = profileCompleteness(profile);
|
|
56
|
+
const completed = has_profile && (has_resume || has_experience);
|
|
57
|
+
|
|
58
|
+
const next_steps: string[] = [];
|
|
59
|
+
if (!has_profile) next_steps.push('기본 프로필을 입력하세요 (이름, 한 줄 소개, 희망 직무).');
|
|
60
|
+
if (!has_resume) next_steps.push('이력서나 경력기술서를 추가하세요. 파일 내용을 붙여넣어도 됩니다.');
|
|
61
|
+
if (!has_experience) next_steps.push('주요 경력을 1개 이상 추가하면 적합도 분석 품질이 올라갑니다.');
|
|
62
|
+
if (!has_skills) next_steps.push('보유 기술스택을 정리해 두면 공고 매칭이 정확해집니다.');
|
|
63
|
+
if (has_profile && !has_cover_letter)
|
|
64
|
+
next_steps.push('기존 자기소개서가 있다면 추가해 두세요. AI가 문체를 학습합니다.');
|
|
65
|
+
if (completed && !has_job)
|
|
66
|
+
next_steps.push('관심 있는 채용공고 URL이나 내용을 AI에게 전달해 적합도 분석을 받아보세요.');
|
|
67
|
+
if (next_steps.length === 0)
|
|
68
|
+
next_steps.push('준비가 잘 되어 있어요. 새 공고를 분석하거나 자기소개서를 작성해 보세요.');
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
completed,
|
|
72
|
+
has_profile,
|
|
73
|
+
has_resume,
|
|
74
|
+
has_cover_letter,
|
|
75
|
+
has_experience,
|
|
76
|
+
has_skills,
|
|
77
|
+
has_job,
|
|
78
|
+
profile_completeness: completeness,
|
|
79
|
+
next_steps,
|
|
80
|
+
};
|
|
81
|
+
}
|