careermate 0.1.1 โ†’ 0.1.2

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.
Files changed (71) hide show
  1. package/README.md +12 -10
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/apps/mcp/src/index.ts +4 -5
  4. package/apps/web/demo/demo-shim.js +536 -0
  5. package/apps/web/demo/demo.css +75 -0
  6. package/apps/web/demo/seed.js +269 -0
  7. package/apps/web/public/app.js +34 -2
  8. package/apps/web/public/index.html +10 -2
  9. package/apps/web/public/lib.js +134 -22
  10. package/apps/web/public/pages/applications.js +2 -9
  11. package/apps/web/public/pages/documents.js +66 -50
  12. package/apps/web/public/pages/home.js +12 -11
  13. package/apps/web/public/pages/interview.js +9 -12
  14. package/apps/web/public/pages/jobs.js +39 -36
  15. package/apps/web/public/pages/profile.js +66 -86
  16. package/apps/web/public/pages/settings.js +15 -10
  17. package/apps/web/public/styles.css +56 -2
  18. package/apps/web/src/http.ts +31 -4
  19. package/apps/web/src/info.ts +3 -3
  20. package/apps/web/src/routes.ts +2 -1
  21. package/apps/web/src/server.ts +1 -4
  22. package/dist/doctor.mjs +72607 -611
  23. package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +12 -0
  24. package/dist/install-page/.omc/state/idle-notif-cooldown.json +3 -0
  25. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +147 -0
  26. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +22 -0
  27. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +44 -0
  28. package/dist/install-page/index.html +31 -70
  29. package/dist/install-page/landing.js +54 -0
  30. package/dist/install-page/start.html +13 -36
  31. package/dist/install-page/style.css +34 -1
  32. package/dist/mcp.mjs +73936 -1976
  33. package/dist/migrate.mjs +17 -10
  34. package/dist/pdf.worker.min.mjs +28 -0
  35. package/dist/public/app.js +34 -2
  36. package/dist/public/index.html +10 -2
  37. package/dist/public/lib.js +134 -22
  38. package/dist/public/pages/applications.js +2 -9
  39. package/dist/public/pages/documents.js +66 -50
  40. package/dist/public/pages/home.js +12 -11
  41. package/dist/public/pages/interview.js +9 -12
  42. package/dist/public/pages/jobs.js +39 -36
  43. package/dist/public/pages/profile.js +66 -86
  44. package/dist/public/pages/settings.js +15 -10
  45. package/dist/public/styles.css +56 -2
  46. package/dist/web.mjs +39631 -244
  47. package/docs/ARCHITECTURE.md +5 -5
  48. package/docs/DECISIONS.md +1 -1
  49. package/docs/DEMO.md +3 -3
  50. package/docs/MCP_TOOLS.md +49 -9
  51. package/docs/ROADMAP.md +8 -7
  52. package/docs/START_WORKFLOW.md +13 -7
  53. package/docs/TODO.md +1 -1
  54. package/docs/WORKFLOWS.md +7 -5
  55. package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +64 -0
  56. package/package.json +6 -2
  57. package/packages/db/src/paths.ts +11 -0
  58. package/packages/mcp-tools/src/bridge.ts +55 -32
  59. package/packages/mcp-tools/src/index.ts +9 -0
  60. package/packages/mcp-tools/src/tools.ts +193 -70
  61. package/packages/mcp-tools/src/update.ts +330 -0
  62. package/packages/parsers/src/documents.ts +521 -0
  63. package/packages/parsers/src/index.ts +8 -5
  64. package/packages/prompts/src/onboarding.ts +2 -2
  65. package/packages/shared/src/build-info.ts +22 -0
  66. package/packages/shared/src/index.ts +1 -0
  67. package/packages/shared/src/schemas.ts +24 -3
  68. package/packages/workflows/src/definitions.ts +4 -2
  69. package/scripts/build-demo.mjs +130 -0
  70. package/scripts/build-dist.mjs +20 -1
  71. package/scripts/doctor.ts +16 -3
@@ -0,0 +1,75 @@
1
+ /* Demo-only overlay: a slim TOP bar that frames the page as a no-save preview
2
+ and links back to the marketing site, plus a theme toggle. Uses the dashboard's
3
+ own theme tokens so it adapts to light/dark automatically.
4
+
5
+ The dashboard layout is a full-height grid with a sticky sidebar + sticky
6
+ topbar; we reserve --demo-bar-h at the top and push those sticky anchors down
7
+ so nothing hides behind the bar. */
8
+
9
+ :root { --demo-bar-h: 46px; }
10
+
11
+ #demo-bar {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ right: 0;
16
+ height: var(--demo-bar-h);
17
+ z-index: 55; /* above sidebar drawer (50), below modals (60) */
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 12px;
21
+ padding: 0 16px;
22
+ background: color-mix(in srgb, var(--bg) 90%, transparent);
23
+ backdrop-filter: blur(8px);
24
+ border-bottom: 1px solid var(--border);
25
+ font-size: 13px;
26
+ }
27
+
28
+ .demo-bar__brand { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--text); flex: none; }
29
+ .demo-bar__logo {
30
+ width: 24px; height: 24px; border-radius: 6px; flex: none;
31
+ background: var(--accent); color: var(--text-on-accent);
32
+ display: grid; place-items: center; font-weight: 800; font-size: 13px;
33
+ }
34
+ .demo-bar__name { font-weight: 700; font-size: 14px; letter-spacing: -.01em; }
35
+ .demo-bar__badge {
36
+ font-weight: 700; font-size: 11px; color: var(--accent);
37
+ background: var(--accent-soft); border: 1px solid var(--accent-border);
38
+ padding: 2px 7px; border-radius: 999px;
39
+ }
40
+ .demo-bar__text { color: var(--text-secondary); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
41
+ .demo-bar__spacer { flex: 1 1 auto; }
42
+
43
+ .demo-bar__icon {
44
+ flex: none; width: 32px; height: 32px; display: grid; place-items: center;
45
+ border: 1px solid var(--border); background: var(--surface); border-radius: 8px;
46
+ color: var(--text-secondary); cursor: pointer;
47
+ }
48
+ .demo-bar__icon:hover { background: var(--surface-hover); color: var(--text); }
49
+
50
+ .demo-bar__link {
51
+ flex: none; font-weight: 600; color: var(--text-secondary); text-decoration: none;
52
+ padding: 6px 11px; border-radius: 8px; white-space: nowrap;
53
+ }
54
+ .demo-bar__link:hover { background: var(--surface-hover); color: var(--text); }
55
+ .demo-bar__link--primary {
56
+ color: var(--accent); border: 1px solid var(--accent-border);
57
+ }
58
+ .demo-bar__link--primary:hover { background: var(--accent-soft); color: var(--accent); }
59
+
60
+ /* Reserve space for the bar by pushing the app + its sticky anchors down. */
61
+ body.has-demo-bar .app { margin-top: var(--demo-bar-h); }
62
+ body.has-demo-bar .sidebar { top: var(--demo-bar-h); height: calc(100vh - var(--demo-bar-h)); }
63
+ body.has-demo-bar .topbar { top: var(--demo-bar-h); }
64
+
65
+ @media (max-width: 900px) {
66
+ /* Sidebar is an off-canvas drawer here โ€” start it below the bar too. */
67
+ body.has-demo-bar .sidebar { top: var(--demo-bar-h); height: calc(100% - var(--demo-bar-h)); }
68
+ }
69
+
70
+ @media (max-width: 640px) {
71
+ /* Tight screens: drop the explanatory sentence, keep brand + actions. */
72
+ .demo-bar__text { display: none; }
73
+ .demo-bar__link[href="/#install"] { display: none; }
74
+ #demo-bar { gap: 8px; padding: 0 10px; }
75
+ }
@@ -0,0 +1,269 @@
1
+ // =============================================================================
2
+ // Demo seed data โ€” realistic sample career workspace for careermate.life/demo.
3
+ // Loaded fresh into the in-memory demo DB on every page load (and on reset).
4
+ // Shapes mirror @careermate/shared *Record types (see packages/shared/src/schemas.ts).
5
+ // All timestamps are fixed strings so the demo is deterministic (no Date.now()).
6
+ // =============================================================================
7
+
8
+ // Anchor "now" for relative-time rendering. Recent activity is dated near this.
9
+ const T = (iso) => iso; // readability helper
10
+
11
+ export function buildSeed() {
12
+ return {
13
+ profile: {
14
+ id: 'p1',
15
+ created_at: '2026-05-02T09:00:00.000Z',
16
+ updated_at: '2026-06-14T11:20:00.000Z',
17
+ name: '๊น€ํ•˜๋Š˜',
18
+ email: 'haneul.kim@example.com',
19
+ phone: '010-1234-5678',
20
+ location: '์„œ์šธ ์„ฑ๋™๊ตฌ',
21
+ headline: '5๋…„์ฐจ ๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด ยท ๊ฒฐ์ œ/์ •์‚ฐ ๋„๋ฉ”์ธ',
22
+ summary:
23
+ '๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ์˜ ๊ฒฐ์ œยท์ •์‚ฐ ์‹œ์Šคํ…œ์„ ์„ค๊ณ„ยท์šด์˜ํ•ด ์˜จ ๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด์ž…๋‹ˆ๋‹ค. ' +
24
+ '์žฅ์•  ๋Œ€์‘๊ณผ ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฒฝํ—˜์ด ๋งŽ๊ณ , ํŒ€์˜ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ์ •๋น„ํ•ด ' +
25
+ '๋ฐฐํฌ ๋นˆ๋„๋ฅผ ์ฃผ 1ํšŒ์—์„œ ์ผ 5ํšŒ๋กœ ๋Œ์–ด์˜ฌ๋ ธ์Šต๋‹ˆ๋‹ค.',
26
+ desired_roles: ['๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด', 'ํ”Œ๋žซํผ ์—”์ง€๋‹ˆ์–ด'],
27
+ desired_conditions: '์—ฐ๋ด‰ 7,000๋งŒ์›+ ยท ์„œ์šธ/ํŒ๊ต ยท ์ฃผ 2ํšŒ ์ด์ƒ ์žฌํƒ',
28
+ preferred_tone: '๋‹ด๋ฐฑํ•˜๊ณ  ๊ตฌ์ฒด์ ์ธ ยท ์„ฑ๊ณผ๋Š” ์ˆซ์ž๋กœ',
29
+ emphasis_points: ['๊ฒฐ์ œ/์ •์‚ฐ ๋„๋ฉ”์ธ ์ „๋ฌธ์„ฑ', '๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ์•ˆ์ •ํ™”', 'ํŒ€ ์ƒ์‚ฐ์„ฑ ๊ฐœ์„ '],
30
+ links: [
31
+ { label: 'GitHub', url: 'https://github.com/example' },
32
+ { label: '๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ', url: 'https://blog.example.com' },
33
+ ],
34
+ },
35
+
36
+ experiences: [
37
+ {
38
+ id: 'e1', created_at: '2026-05-02T09:05:00.000Z', updated_at: '2026-05-02T09:05:00.000Z',
39
+ company: 'ํŽ˜์ด์ฝ”์–ด', role: '๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด (์‹œ๋‹ˆ์–ด)', employment_type: '์ •๊ทœ์ง',
40
+ start_date: '2022-03', end_date: null, is_current: true,
41
+ description: '๊ฒฐ์ œ ๊ฒŒ์ดํŠธ์›จ์ด์™€ ์ •์‚ฐ ์‹œ์Šคํ…œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœยท์šด์˜.',
42
+ achievements: [
43
+ '์ •์‚ฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์„ 4์‹œ๊ฐ„ โ†’ 38๋ถ„์œผ๋กœ ๋‹จ์ถ• (์ฟผ๋ฆฌยท์ธ๋ฑ์Šคยท๋ณ‘๋ ฌํ™”)',
44
+ '๊ฒฐ์ œ ์Šน์ธ API p99 ์ง€์—ฐ 320ms โ†’ 110ms ๊ฐœ์„ ',
45
+ '๋ฌด์ค‘๋‹จ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์œผ๋กœ ์›” 1ํšŒ ์ •๊ธฐ ์ ๊ฒ€ ๋‹ค์šดํƒ€์ž„ ์ œ๊ฑฐ',
46
+ ],
47
+ tech: ['Java', 'Spring', 'Kafka', 'MySQL', 'Redis', 'Kubernetes'],
48
+ order_index: 0,
49
+ },
50
+ {
51
+ id: 'e2', created_at: '2026-05-02T09:06:00.000Z', updated_at: '2026-05-02T09:06:00.000Z',
52
+ company: '์ปค๋จธ์Šค๋žฉ', role: '๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด', employment_type: '์ •๊ทœ์ง',
53
+ start_date: '2019-01', end_date: '2022-02', is_current: false,
54
+ description: '์ด์ปค๋จธ์Šค ์ฃผ๋ฌธ/์žฌ๊ณ  ๋„๋ฉ”์ธ API ๊ฐœ๋ฐœ.',
55
+ achievements: [
56
+ '์ฃผ๋ฌธ ํญ์ฃผ ์‹œ ์žฌ๊ณ  ๋™์‹œ์„ฑ ๋ฒ„๊ทธ ํ•ด๊ฒฐ (๋ถ„์‚ฐ ๋ฝ ๋„์ž…, ์˜ค๋ฒ„์…€ 0๊ฑด)',
57
+ '์‹ ๊ทœ ์ •๊ธฐ๋ฐฐ์†ก ๊ธฐ๋Šฅ ๋ฐฑ์—”๋“œ ๋‹จ๋… ์„ค๊ณ„ยท์ถœ์‹œ',
58
+ ],
59
+ tech: ['Node.js', 'TypeScript', 'PostgreSQL', 'AWS'],
60
+ order_index: 1,
61
+ },
62
+ ],
63
+
64
+ projects: [
65
+ {
66
+ id: 'pr1', created_at: '2026-05-02T09:10:00.000Z', updated_at: '2026-05-02T09:10:00.000Z',
67
+ name: '์ •์‚ฐ ์‹œ์Šคํ…œ ์ฐจ์„ธ๋Œ€ ๋ฆฌ์•„ํ‚คํ…์ฒ˜', role: '๋ฆฌ๋“œ',
68
+ description: '๋ ˆ๊ฑฐ์‹œ ๋ฐฐ์น˜ ์ •์‚ฐ์„ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์ŠคํŠธ๋ฆฌ๋ฐ ์ •์‚ฐ์œผ๋กœ ์ „ํ™˜.',
69
+ highlights: ['Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์†Œ์‹ฑ ๋„์ž…', '์ •์‚ฐ ์ •ํ™•๋„ ๊ฒ€์ฆ ํŒŒ์ดํ”„๋ผ์ธ ์ž๋™ํ™”'],
70
+ tech: ['Kafka', 'Java', 'Spring Batch'],
71
+ url: 'https://github.com/example/settlement', start_date: '2023-06', end_date: '2024-01',
72
+ order_index: 0,
73
+ },
74
+ {
75
+ id: 'pr2', created_at: '2026-05-02T09:11:00.000Z', updated_at: '2026-05-02T09:11:00.000Z',
76
+ name: '์‚ฌ๋‚ด ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ์ •๋น„', role: '๊ฐœ์ธ ์ฃผ๋„',
77
+ description: 'GitHub Actions + ArgoCD๋กœ ์นด๋‚˜๋ฆฌ ๋ฐฐํฌ ์ž๋™ํ™”.',
78
+ highlights: ['๋ฐฐํฌ ๋นˆ๋„ ์ฃผ1ํšŒ โ†’ ์ผ5ํšŒ', '๋กค๋ฐฑ ํ‰๊ท  12๋ถ„ โ†’ 90์ดˆ'],
79
+ tech: ['GitHub Actions', 'ArgoCD', 'Kubernetes'],
80
+ url: null, start_date: '2024-02', end_date: '2024-05',
81
+ order_index: 1,
82
+ },
83
+ ],
84
+
85
+ skills: [
86
+ { id: 's1', created_at: '2026-05-02T09:12:00.000Z', updated_at: '2026-05-02T09:12:00.000Z', name: 'Java / Spring', category: '์–ธ์–ด/ํ”„๋ ˆ์ž„์›Œํฌ', level: '์ƒ', years: 5, order_index: 0 },
87
+ { id: 's2', created_at: '2026-05-02T09:12:10.000Z', updated_at: '2026-05-02T09:12:10.000Z', name: 'Kafka', category: '์ธํ”„๋ผ', level: '์ƒ', years: 3, order_index: 1 },
88
+ { id: 's3', created_at: '2026-05-02T09:12:20.000Z', updated_at: '2026-05-02T09:12:20.000Z', name: 'MySQL / PostgreSQL', category: '๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค', level: '์ƒ', years: 5, order_index: 2 },
89
+ { id: 's4', created_at: '2026-05-02T09:12:30.000Z', updated_at: '2026-05-02T09:12:30.000Z', name: 'Kubernetes', category: '์ธํ”„๋ผ', level: '์ค‘', years: 3, order_index: 3 },
90
+ { id: 's5', created_at: '2026-05-02T09:12:40.000Z', updated_at: '2026-05-02T09:12:40.000Z', name: 'TypeScript / Node.js', category: '์–ธ์–ด/ํ”„๋ ˆ์ž„์›Œํฌ', level: '์ค‘', years: 3, order_index: 4 },
91
+ ],
92
+
93
+ documents: [
94
+ {
95
+ id: 'd1', created_at: '2026-05-03T10:00:00.000Z', updated_at: '2026-06-10T08:30:00.000Z',
96
+ kind: 'resume', title: '๊น€ํ•˜๋Š˜ ์ด๋ ฅ์„œ (2026)', source: 'upload', is_primary: true,
97
+ tags: ['๋ฐฑ์—”๋“œ', '๊ฒฐ์ œ'],
98
+ content:
99
+ '# ๊น€ํ•˜๋Š˜ โ€” ๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด\n\n## ๊ฒฝ๋ ฅ ์š”์•ฝ\n๊ฒฐ์ œยท์ •์‚ฐ ๋„๋ฉ”์ธ 5๋…„. ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ์•ˆ์ •ํ™”์™€ ํŒ€ ์ƒ์‚ฐ์„ฑ ๊ฐœ์„ ์— ๊ฐ•์ .\n\n' +
100
+ '## ํ•ต์‹ฌ ์„ฑ๊ณผ\n- ์ •์‚ฐ ๋ฐฐ์น˜ 4h โ†’ 38m\n- ๊ฒฐ์ œ API p99 320ms โ†’ 110ms\n- ๋ฌด์ค‘๋‹จ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์œผ๋กœ ์ •๊ธฐ ๋‹ค์šดํƒ€์ž„ ์ œ๊ฑฐ\n',
101
+ },
102
+ {
103
+ id: 'd2', created_at: '2026-05-20T13:00:00.000Z', updated_at: '2026-05-20T13:00:00.000Z',
104
+ kind: 'career_description', title: '๊ฒฝ๋ ฅ๊ธฐ์ˆ ์„œ โ€” ํŽ˜์ด์ฝ”์–ด', source: 'manual', is_primary: false,
105
+ tags: ['๊ฒฐ์ œ'],
106
+ content: '## ํŽ˜์ด์ฝ”์–ด (2022.03~)\n๊ฒฐ์ œ ๊ฒŒ์ดํŠธ์›จ์ด/์ •์‚ฐ ๋ฐฑ์—”๋“œ. ์ƒ์„ธ ํ”„๋กœ์ ํŠธ์™€ ์„ฑ๊ณผ๋Š” ๋ณธ๋ฌธ ์ฐธ์กฐ.\n',
107
+ },
108
+ ],
109
+
110
+ jobs: [
111
+ {
112
+ id: 'j1', created_at: '2026-06-01T09:00:00.000Z', updated_at: '2026-06-12T09:00:00.000Z',
113
+ company: 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ ', position: '๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด (๊ฒฐ์ œ)', url: 'https://example.com/jobs/j1',
114
+ location: '์„œ์šธ ๊ฐ•๋‚จ๊ตฌ', employment_type: '์ •๊ทœ์ง',
115
+ description: '๊ฒฐ์ œ ์ฝ”์–ด ์‹œ์Šคํ…œ์„ ํ•จ๊ป˜ ๋งŒ๋“ค ๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๊ฒฝํ—˜ ์šฐ๋Œ€.',
116
+ requirements: ['JVM ๊ธฐ๋ฐ˜ ๋ฐฑ์—”๋“œ 5๋…„+', '๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ๊ฒฝํ—˜', '๊ฒฐ์ œ/๊ธˆ์œต ๋„๋ฉ”์ธ ์šฐ๋Œ€'],
117
+ keywords: ['Java', 'Spring', 'Kafka', 'MSA', '๊ฒฐ์ œ'],
118
+ deadline: '2026-06-30', source: '์›ํ‹ฐ๋“œ',
119
+ },
120
+ {
121
+ id: 'j2', created_at: '2026-06-03T09:00:00.000Z', updated_at: '2026-06-11T09:00:00.000Z',
122
+ company: '์ฟ ํŒก', position: 'Software Engineer, Backend', url: 'https://example.com/jobs/j2',
123
+ location: '์„œ์šธ ์†กํŒŒ๊ตฌ', employment_type: '์ •๊ทœ์ง',
124
+ description: '์ปค๋จธ์Šค ์ฃผ๋ฌธ/์ •์‚ฐ ํ”Œ๋žซํผ ๋ฐฑ์—”๋“œ. ๊ธ€๋กœ๋ฒŒ ์Šค์ผ€์ผ์˜ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ.',
125
+ requirements: ['๋ถ„์‚ฐ ์‹œ์Šคํ…œ ์„ค๊ณ„ ๊ฒฝํ—˜', 'Java ๋˜๋Š” Kotlin', '์˜์–ด ํ˜‘์—… ๊ฐ€๋Šฅ'],
126
+ keywords: ['Java', 'Kotlin', 'AWS', '๋ถ„์‚ฐ์‹œ์Šคํ…œ'],
127
+ deadline: '2026-07-15', source: '์‚ฌ๋žŒ์ธ',
128
+ },
129
+ {
130
+ id: 'j3', created_at: '2026-06-05T09:00:00.000Z', updated_at: '2026-06-05T09:00:00.000Z',
131
+ company: '๋‹น๊ทผ', position: 'ํ”Œ๋žซํผ ์—”์ง€๋‹ˆ์–ด', url: 'https://example.com/jobs/j3',
132
+ location: '์„œ์šธ ์„œ์ดˆ๊ตฌ', employment_type: '์ •๊ทœ์ง',
133
+ description: '์‚ฌ๋‚ด ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ๋Œ์–ด์˜ฌ๋ฆด ํ”Œ๋žซํผ ์—”์ง€๋‹ˆ์–ด.',
134
+ requirements: ['Kubernetes ์šด์˜ ๊ฒฝํ—˜', 'CI/CD ํŒŒ์ดํ”„๋ผ์ธ ์„ค๊ณ„', 'Go ๋˜๋Š” Java'],
135
+ keywords: ['Kubernetes', 'CI/CD', 'Platform', 'Go'],
136
+ deadline: null, source: '์ง์ ‘ ์ž…๋ ฅ',
137
+ },
138
+ {
139
+ id: 'j4', created_at: '2026-05-25T09:00:00.000Z', updated_at: '2026-06-08T09:00:00.000Z',
140
+ company: '๋„ค์ด๋ฒ„ํŽ˜์ด', position: '๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž (์ •์‚ฐ)', url: 'https://example.com/jobs/j4',
141
+ location: '๊ฒฝ๊ธฐ ์„ฑ๋‚จ์‹œ', employment_type: '์ •๊ทœ์ง',
142
+ description: '์ •์‚ฐ ์‹œ์Šคํ…œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ. ๋ฐฐ์น˜/์ŠคํŠธ๋ฆฌ๋ฐ ์ •์‚ฐ ๊ฒฝํ—˜ ์šฐ๋Œ€.',
143
+ requirements: ['Spring Batch ๊ฒฝํ—˜', '์ •์‚ฐ ๋„๋ฉ”์ธ ์ดํ•ด', 'SQL ์ตœ์ ํ™”'],
144
+ keywords: ['Java', 'Spring Batch', '์ •์‚ฐ', 'MySQL'],
145
+ deadline: '2026-06-22', source: '์›ํ‹ฐ๋“œ',
146
+ },
147
+ {
148
+ id: 'j5', created_at: '2026-05-15T09:00:00.000Z', updated_at: '2026-06-02T09:00:00.000Z',
149
+ company: '๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ', position: '์„œ๋ฒ„ ๊ฐœ๋ฐœ์ž', url: 'https://example.com/jobs/j5',
150
+ location: '์„œ์šธ ์†กํŒŒ๊ตฌ', employment_type: '์ •๊ทœ์ง',
151
+ description: '์ฃผ๋ฌธ ์ค‘๊ณ„ ํ”Œ๋žซํผ ์„œ๋ฒ„ ๊ฐœ๋ฐœ.',
152
+ requirements: ['Java/Kotlin', '๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ', 'MSA ๊ฒฝํ—˜'],
153
+ keywords: ['Kotlin', 'Spring', 'MSA'],
154
+ deadline: '2026-06-18', source: '์‚ฌ๋žŒ์ธ',
155
+ },
156
+ {
157
+ id: 'j6', created_at: '2026-06-10T09:00:00.000Z', updated_at: '2026-06-10T09:00:00.000Z',
158
+ company: '์นด์นด์˜ค๋ฑ…ํฌ', position: '๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด', url: 'https://example.com/jobs/j6',
159
+ location: '๊ฒฝ๊ธฐ ์„ฑ๋‚จ์‹œ', employment_type: '์ •๊ทœ์ง',
160
+ description: '๋ฑ…ํ‚น ์ฝ”์–ด ๋ฐฑ์—”๋“œ. ์•ˆ์ •์„ฑ๊ณผ ์ •ํ•ฉ์„ฑ์ด ์ตœ์šฐ์„ .',
161
+ requirements: ['๊ธˆ์œต ๋„๋ฉ”์ธ ๊ฒฝํ—˜', 'Java', '๋†’์€ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฌธํ™”'],
162
+ keywords: ['Java', '๊ธˆ์œต', 'Spring'],
163
+ deadline: '2026-07-05', source: '์ง์ ‘ ์ž…๋ ฅ',
164
+ },
165
+ ],
166
+
167
+ // job_id โ†’ application. status drives the pipeline + board.
168
+ applications: [
169
+ { id: 'a1', created_at: '2026-06-01T10:00:00.000Z', updated_at: '2026-06-13T10:00:00.000Z', job_id: 'j1', status: 'interview', resume_id: 'd1', cover_letter_id: 'c1', applied_at: '2026-06-06', notes: '1์ฐจ ๊ธฐ์ˆ ๋ฉด์ ‘ ํ†ต๊ณผ, 2์ฐจ ์ž„์›๋ฉด์ ‘ ์˜ˆ์ •' },
170
+ { id: 'a2', created_at: '2026-06-03T10:00:00.000Z', updated_at: '2026-06-09T10:00:00.000Z', job_id: 'j2', status: 'applied', resume_id: 'd1', cover_letter_id: null, applied_at: '2026-06-09', notes: null },
171
+ { id: 'a3', created_at: '2026-06-05T10:00:00.000Z', updated_at: '2026-06-05T10:00:00.000Z', job_id: 'j3', status: 'planned', resume_id: null, cover_letter_id: null, applied_at: null, notes: '์ง€์›์„œ ์ž‘์„ฑ ์ค‘' },
172
+ { id: 'a4', created_at: '2026-05-25T10:00:00.000Z', updated_at: '2026-06-12T10:00:00.000Z', job_id: 'j4', status: 'document_passed', resume_id: 'd1', cover_letter_id: 'c2', applied_at: '2026-05-28', notes: '์„œ๋ฅ˜ ํ•ฉ๊ฒฉ โ€” ๋ฉด์ ‘ ์ผ์ • ์กฐ์œจ ์ค‘' },
173
+ { id: 'a5', created_at: '2026-05-15T10:00:00.000Z', updated_at: '2026-06-02T10:00:00.000Z', job_id: 'j5', status: 'rejected', resume_id: 'd1', cover_letter_id: null, applied_at: '2026-05-18', notes: '์„œ๋ฅ˜ ํƒˆ๋ฝ' },
174
+ { id: 'a6', created_at: '2026-06-10T10:00:00.000Z', updated_at: '2026-06-10T10:00:00.000Z', job_id: 'j6', status: 'draft', resume_id: null, cover_letter_id: null, applied_at: null, notes: null },
175
+ ],
176
+
177
+ fits: [
178
+ {
179
+ id: 'f1', created_at: '2026-06-02T09:30:00.000Z', updated_at: '2026-06-02T09:30:00.000Z', job_id: 'j1',
180
+ score: 88, summary: '๊ฒฐ์ œ ๋„๋ฉ”์ธ ์ „๋ฌธ์„ฑ๊ณผ ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ๊ฒฝํ—˜์ด ๊ณต๊ณ  ์š”๊ตฌ์™€ ๊ฐ•ํ•˜๊ฒŒ ๋งž์Šต๋‹ˆ๋‹ค.',
181
+ strengths: ['๊ฒฐ์ œ/์ •์‚ฐ 5๋…„ ์‹ค๋ฌด', 'KafkaยทSpring ์ˆ™๋ จ', 'p99 ์ง€์—ฐ ๊ฐœ์„  ์ •๋Ÿ‰ ์„ฑ๊ณผ'],
182
+ gaps: ['MSA ์ „ํ™˜ ๋ฆฌ๋”ฉ ๊ฒฝํ—˜์€ ๋ณด๊ฐ•ํ•˜๋ฉด ์ข‹์Œ'],
183
+ matched_keywords: ['Java', 'Spring', 'Kafka', '๊ฒฐ์ œ'], missing_keywords: ['MSA'],
184
+ recommendations: ['์ง€์›๋™๊ธฐ์— ๊ฒฐ์ œ ์ฝ”์–ด ์•ˆ์ •ํ™” ๊ฒฝํ—˜์„ ์•ž์„ธ์šฐ๊ธฐ', 'MSA ๊ด€๋ จ ํ•™์Šต/์‚ฌ์ด๋“œ ์–ธ๊ธ‰'],
185
+ },
186
+ {
187
+ id: 'f2', created_at: '2026-05-26T09:30:00.000Z', updated_at: '2026-05-26T09:30:00.000Z', job_id: 'j4',
188
+ score: 91, summary: '์ •์‚ฐ ๋ฐฐ์น˜ ์ตœ์ ํ™” ๊ฒฝํ—˜์ด ๊ณต๊ณ  ํ•ต์‹ฌ ์š”๊ตฌ์™€ ๊ฑฐ์˜ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.',
189
+ strengths: ['Spring Batch ์ •์‚ฐ ์‹ค๋ฌด', '๋ฐฐ์น˜ 4hโ†’38m ์„ฑ๊ณผ', 'SQL ์ตœ์ ํ™”'],
190
+ gaps: ['์ŠคํŠธ๋ฆฌ๋ฐ ์ •์‚ฐ์€ ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜ 1๊ฑด'],
191
+ matched_keywords: ['Java', 'Spring Batch', '์ •์‚ฐ', 'MySQL'], missing_keywords: [],
192
+ recommendations: ['์ •์‚ฐ ์ •ํ™•๋„ ๊ฒ€์ฆ ์ž๋™ํ™” ์‚ฌ๋ก€๋ฅผ ๊ตฌ์ฒด ์ˆ˜์น˜๋กœ ์ œ์‹œ'],
193
+ },
194
+ {
195
+ id: 'f3', created_at: '2026-06-04T09:30:00.000Z', updated_at: '2026-06-04T09:30:00.000Z', job_id: 'j2',
196
+ score: 72, summary: '๋ฐฑ์—”๋“œ ์—ญ๋Ÿ‰์€ ์ถฉ๋ถ„ํ•˜๋‚˜ ๊ธ€๋กœ๋ฒŒ ์Šค์ผ€์ผ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ๊ฒฝํ—˜์€ ๋ถ€๋ถ„์ ์ž…๋‹ˆ๋‹ค.',
197
+ strengths: ['๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ์•ˆ์ •ํ™”', '๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ'],
198
+ gaps: ['๊ธ€๋กœ๋ฒŒ ๋ฉ€ํ‹ฐ๋ฆฌ์ „ ์šด์˜ ๊ฒฝํ—˜', '์˜์–ด ํ˜‘์—… ๋นˆ๋„'],
199
+ matched_keywords: ['Java', 'AWS'], missing_keywords: ['Kotlin', '๋ถ„์‚ฐ์‹œ์Šคํ…œ'],
200
+ recommendations: ['๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ์‚ฌ๋ก€ ๊ฐ•์กฐ', 'Kotlin ํ•™์Šต ๊ณ„ํš ์–ธ๊ธ‰'],
201
+ },
202
+ ],
203
+
204
+ coverLetters: [
205
+ {
206
+ id: 'c1', created_at: '2026-06-04T11:00:00.000Z', updated_at: '2026-06-08T11:00:00.000Z',
207
+ title: 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ๋ฐฑ์—”๋“œ ์ง€์›๋™๊ธฐ', job_id: 'j1', is_primary: true,
208
+ current_version_id: 'c1v2', version_count: 2,
209
+ current_content:
210
+ '๊ฒฐ์ œ๋Š” ํ•œ ๋ฒˆ์˜ ์˜ค๋ฅ˜๊ฐ€ ๊ณง ์‹ ๋ขฐ์˜ ์†์‹ค์ธ ๋„๋ฉ”์ธ์ž…๋‹ˆ๋‹ค. ํŽ˜์ด์ฝ”์–ด์—์„œ ๊ฒฐ์ œ ์Šน์ธ API์˜ p99 ์ง€์—ฐ์„ ' +
211
+ '320ms์—์„œ 110ms๋กœ ์ค„์ด๋ฉฐ, ๋น ๋ฅด๋ฉด์„œ๋„ ํ‹€๋ฆฌ์ง€ ์•Š๋Š” ์‹œ์Šคํ…œ์ด ๋ฌด์—‡์ธ์ง€ ์ฒด๋“ํ–ˆ์Šต๋‹ˆ๋‹ค. ' +
212
+ 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ ์˜ ๊ฒฐ์ œ ์ฝ”์–ด์—์„œ ์ด ๊ฒฝํ—˜์„ ์ด์–ด๊ฐ€๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.',
213
+ versions: [
214
+ { id: 'c1v1', cover_letter_id: 'c1', version_no: 1, content: '(์ดˆ์•ˆ) ๊ฒฐ์ œ ๋„๋ฉ”์ธ ๊ฒฝํ—˜ ์ค‘์‹ฌ ์ง€์›๋™๊ธฐ ์ดˆ์•ˆ.', note: '์ดˆ์•ˆ', source: 'ai', created_at: '2026-06-04T11:00:00.000Z' },
215
+ { id: 'c1v2', cover_letter_id: 'c1', version_no: 2, content: '๊ฒฐ์ œ๋Š” ํ•œ ๋ฒˆ์˜ ์˜ค๋ฅ˜๊ฐ€ ๊ณง ์‹ ๋ขฐ์˜ ์†์‹ค์ธ ๋„๋ฉ”์ธ์ž…๋‹ˆ๋‹ค. ํŽ˜์ด์ฝ”์–ด์—์„œ ๊ฒฐ์ œ ์Šน์ธ API์˜ p99 ์ง€์—ฐ์„ 320ms์—์„œ 110ms๋กœ ์ค„์ด๋ฉฐ, ๋น ๋ฅด๋ฉด์„œ๋„ ํ‹€๋ฆฌ์ง€ ์•Š๋Š” ์‹œ์Šคํ…œ์ด ๋ฌด์—‡์ธ์ง€ ์ฒด๋“ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ† ์ŠคํŽ˜์ด๋จผ์ธ ์˜ ๊ฒฐ์ œ ์ฝ”์–ด์—์„œ ์ด ๊ฒฝํ—˜์„ ์ด์–ด๊ฐ€๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.', note: '์ˆ˜์น˜ ๋ณด๊ฐ• + ๋งˆ๋ฌด๋ฆฌ ๋‹ค๋“ฌ๊ธฐ', source: 'edit', created_at: '2026-06-08T11:00:00.000Z' },
216
+ ],
217
+ },
218
+ {
219
+ id: 'c2', created_at: '2026-05-27T11:00:00.000Z', updated_at: '2026-05-27T11:00:00.000Z',
220
+ title: '๋„ค์ด๋ฒ„ํŽ˜์ด ์ •์‚ฐ ์ง€์›๋™๊ธฐ', job_id: 'j4', is_primary: false,
221
+ current_version_id: 'c2v1', version_count: 1,
222
+ current_content:
223
+ '์ •์‚ฐ์€ ์ •ํ™•ํ•จ์ด ์ „๋ถ€์ž…๋‹ˆ๋‹ค. Spring Batch ๊ธฐ๋ฐ˜ ์ •์‚ฐ ๋ฐฐ์น˜๋ฅผ 4์‹œ๊ฐ„์—์„œ 38๋ถ„์œผ๋กœ ์ค„์ด๋ฉด์„œ๋„ ' +
224
+ '๊ฒ€์ฆ ํŒŒ์ดํ”„๋ผ์ธ์„ ์ž๋™ํ™”ํ•ด ์ •ํ•ฉ์„ฑ์„ ์ง€์ผฐ์Šต๋‹ˆ๋‹ค. ๋„ค์ด๋ฒ„ํŽ˜์ด ์ •์‚ฐ ์‹œ์Šคํ…œ์— ๊ธฐ์—ฌํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.',
225
+ versions: [
226
+ { id: 'c2v1', cover_letter_id: 'c2', version_no: 1, content: '์ •์‚ฐ์€ ์ •ํ™•ํ•จ์ด ์ „๋ถ€์ž…๋‹ˆ๋‹ค. Spring Batch ๊ธฐ๋ฐ˜ ์ •์‚ฐ ๋ฐฐ์น˜๋ฅผ 4์‹œ๊ฐ„์—์„œ 38๋ถ„์œผ๋กœ ์ค„์ด๋ฉด์„œ๋„ ๊ฒ€์ฆ ํŒŒ์ดํ”„๋ผ์ธ์„ ์ž๋™ํ™”ํ•ด ์ •ํ•ฉ์„ฑ์„ ์ง€์ผฐ์Šต๋‹ˆ๋‹ค. ๋„ค์ด๋ฒ„ํŽ˜์ด ์ •์‚ฐ ์‹œ์Šคํ…œ์— ๊ธฐ์—ฌํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.', note: 'AI ์ƒ์„ฑ', source: 'ai', created_at: '2026-05-27T11:00:00.000Z' },
227
+ ],
228
+ },
229
+ ],
230
+
231
+ interviews: [
232
+ {
233
+ id: 'iv1', created_at: '2026-06-12T14:00:00.000Z', updated_at: '2026-06-13T09:30:00.000Z', job_id: 'j1',
234
+ self_introduction:
235
+ '์•ˆ๋…•ํ•˜์„ธ์š”, ๊ฒฐ์ œยท์ •์‚ฐ ๋„๋ฉ”์ธ 5๋…„์ฐจ ๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด ๊น€ํ•˜๋Š˜์ž…๋‹ˆ๋‹ค. ' +
236
+ 'ํŽ˜์ด์ฝ”์–ด์—์„œ ํ•˜๋ฃจ ์ˆ˜๋ฐฑ๋งŒ ๊ฑด์˜ ๊ฒฐ์ œ ํŠธ๋žœ์žญ์…˜์„ ๋‹ค๋ฃจ๋ฉฐ, "๋น ๋ฅด๋ฉด์„œ๋„ ์ ˆ๋Œ€ ํ‹€๋ฆฌ์ง€ ์•Š๋Š”" ์‹œ์Šคํ…œ์„ ' +
237
+ '๋งŒ๋“œ๋Š” ๋ฐ ์ง‘์ค‘ํ•ด ์™”์Šต๋‹ˆ๋‹ค. ๊ฒฐ์ œ ์Šน์ธ API์˜ p99 ์ง€์—ฐ์„ 320ms์—์„œ 110ms๋กœ ์ค„์˜€๊ณ , ' +
238
+ '์ •์‚ฐ ๋ฐฐ์น˜๋ฅผ 4์‹œ๊ฐ„์—์„œ 38๋ถ„์œผ๋กœ ๋‹จ์ถ•ํ•˜๋ฉด์„œ๋„ ๊ฒ€์ฆ ์ž๋™ํ™”๋กœ ์ •ํ•ฉ์„ฑ์„ ์ง€์ผฐ์Šต๋‹ˆ๋‹ค. ' +
239
+ 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ ์˜ ๊ฒฐ์ œ ์ฝ”์–ด์—์„œ ์ด ๊ฒฝํ—˜์„ ํ•œ ๋‹จ๊ณ„ ๋” ํ‚ค์šฐ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.',
240
+ notes:
241
+ '2์ฐจ ์ž„์›๋ฉด์ ‘ โ€” ์ปฌ์ฒ˜ํ• + ์‹œ์Šคํ…œ ์„ค๊ณ„ ์œ„์ฃผ ์˜ˆ์ƒ.\n' +
242
+ 'ยท ๊ฒฐ์ œ ๋„๋ฉ”์ธ์—์„œ์˜ "์ •ํ™•์„ฑ vs ์†๋„" ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ ๊ด€์  ๊ฐ•์กฐํ•˜๊ธฐ\n' +
243
+ 'ยท ํŽ˜์ด์ฝ”์–ด โ†’ ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ์ง€์› ๋™๊ธฐ(๊ทœ๋ชจ/์ž„ํŒฉํŠธ) ์†”์งํ•˜๊ฒŒ\n' +
244
+ 'ยท ์—ญ์งˆ๋ฌธ: ๊ฒฐ์ œ ์ฝ”์–ด ํŒ€์˜ ์˜จ์ฝœ/์žฅ์•  ๋Œ€์‘ ๋ฌธํ™”, ์ฐจ์„ธ๋Œ€ ์ •์‚ฐ ๋กœ๋“œ๋งต',
245
+ questions: [
246
+ { question: '๊ฒฐ์ œ ์Šน์ธ API์˜ p99 ์ง€์—ฐ์„ ์–ด๋–ป๊ฒŒ 320ms์—์„œ 110ms๋กœ ์ค„์˜€๋‚˜์š”?', intent: '์„ฑ๋Šฅ ์ตœ์ ํ™”์˜ ์‚ฌ๊ณ  ๊ณผ์ •๊ณผ ์ธก์ • ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ํ™•์ธ', followups: ['๋ณ‘๋ชฉ์„ ์–ด๋–ป๊ฒŒ ์ธก์ •ํ–ˆ๋‚˜์š”?', '๊ฐœ์„ ์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋Š” ๋ฌด์—‡์ด์—ˆ๋‚˜์š”?', '์บ์‹œ ์ •ํ•ฉ์„ฑ์€ ์–ด๋–ป๊ฒŒ ๋ณด์žฅํ–ˆ๋‚˜์š”?'], answer_outline: 'APM์œผ๋กœ ๊ตฌ๊ฐ„๋ณ„ ์ธก์ • โ†’ ๊ฐ€์„ค(N+1 ์ฟผ๋ฆฌยท์ง๋ ฌํ™”ยท์ปค๋„ฅ์…˜ ๋Œ€๊ธฐ) โ†’ ์ธ๋ฑ์Šค ์žฌ์„ค๊ณ„ยท์ปค๋„ฅ์…˜ํ’€ ํŠœ๋‹ยท์ฝ๊ธฐ ์บ์‹œ ์ˆœ์ฐจ ์ ์šฉ โ†’ ๊ฐ ๋‹จ๊ณ„ A/B๋กœ ํšจ๊ณผ ๊ฒ€์ฆ. ์บ์‹œ๋Š” TTL+์ด๋ฒคํŠธ ๋ฌดํšจํ™”๋กœ ์ •ํ•ฉ์„ฑ ์œ ์ง€.' },
247
+ { question: '๋ฌด์ค‘๋‹จ์œผ๋กœ ๊ฒฐ์ œ DB๋ฅผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•œ ๊ฒฝํ—˜์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”.', intent: '์šด์˜ ์•ˆ์ •์„ฑ๊ณผ ๋‹จ๊ณ„์  ์ „ํ™˜ ์„ค๊ณ„ ์—ญ๋Ÿ‰', followups: ['๋กค๋ฐฑ ์ „๋žต์€?', '๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ์€?'], answer_outline: '๋“€์–ผ ๋ผ์ดํŠธ โ†’ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ๋ฐฑํ•„ โ†’ ์‹ ๊ตฌ ๋น„๊ต ๊ฒ€์ฆ โ†’ ์ ์ง„ ํŠธ๋ž˜ํ”ฝ ์ปท์˜ค๋ฒ„. ๊ฐ ๋‹จ๊ณ„๊ฐ€ ๊ฐ€์—ญ์ ์ด๋ผ ์–ธ์ œ๋“  ๋กค๋ฐฑ ๊ฐ€๋Šฅํ•˜๊ฒŒ ์„ค๊ณ„.' },
248
+ { question: '๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ์—์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ ์‚ฌ๋ก€๊ฐ€ ์žˆ๋‚˜์š”?', intent: '๋ถ„์‚ฐ ํ™˜๊ฒฝ ๋™์‹œ์„ฑ ์ดํ•ด๋„', followups: ['๋ถ„์‚ฐ ๋ฝ์˜ ํ•œ๊ณ„๋Š”?', '๋ฉฑ๋“ฑ์„ฑ์€ ์–ด๋–ป๊ฒŒ ๋ณด์žฅํ–ˆ๋‚˜์š”?'], answer_outline: '์ฃผ๋ฌธ ํญ์ฃผ ์‹œ ์žฌ๊ณ  ์˜ค๋ฒ„์…€ โ†’ ๋ถ„์‚ฐ ๋ฝ(Redis) + ๋ฉฑ๋“ฑ ํ‚ค๋กœ ์ค‘๋ณต ์ฒ˜๋ฆฌ ์ฐจ๋‹จ. ๋ฝ ๊ฒฝํ•ฉ์ด ํฐ ๊ตฌ๊ฐ„์€ ํ ๊ธฐ๋ฐ˜ ์ง๋ ฌํ™”๋กœ ์ „ํ™˜ํ•ด ์˜ค๋ฒ„์…€ 0๊ฑด ๋‹ฌ์„ฑ.' },
249
+ { question: '๊ฒฐ์ œ์ฒ˜๋Ÿผ ์ •ํ™•์„ฑ์ด ์ค‘์š”ํ•œ ๋„๋ฉ”์ธ์—์„œ ์žฅ์• ๊ฐ€ ๋‚ฌ์„ ๋•Œ ์–ด๋–ป๊ฒŒ ๋Œ€์‘ํ•˜๋‚˜์š”?', intent: '์žฅ์•  ๋Œ€์‘ ์›์น™๊ณผ ์šฐ์„ ์ˆœ์œ„ ํŒ๋‹จ', followups: ['์‚ฌํ›„ ๋ถ„์„์€ ์–ด๋–ป๊ฒŒ ํ•˜๋‚˜์š”?'], answer_outline: '1) ์˜ํ–ฅ ๋ฒ”์œ„ ๊ฒฉ๋ฆฌยท์ฐจ๋‹จ(์„œํ‚ท๋ธŒ๋ ˆ์ด์ปค) 2) ์ •ํ•ฉ์„ฑ ์šฐ์„  โ€” ์˜์‹ฌ ๊ฑฐ๋ž˜๋Š” ๋ณด๋ฅ˜ ์ฒ˜๋ฆฌ 3) ๋ณต๊ตฌ ํ›„ ์ •์‚ฐ ์žฌ๋Œ€์‚ฌ 4) ๋ธ”๋ ˆ์ž„๋ฆฌ์Šค ํฌ์ŠคํŠธ๋ชจํ…œ์œผ๋กœ ์žฌ๋ฐœ ๋ฐฉ์ง€ ์•ก์…˜ํ™”.' },
250
+ { question: 'ํ•จ๊ป˜ ์ผํ•˜๊ธฐ ์ข‹์€ ๋™๋ฃŒ/ํŒ€ ๋ฌธํ™”๋Š” ๋ฌด์—‡์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋‚˜์š”?', intent: '์ปฌ์ฒ˜ํ•', followups: ['์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฌธํ™”์— ๋Œ€ํ•œ ์ƒ๊ฐ์€?'], answer_outline: '๋ฌธ์ œ๋ฅผ ์ˆจ๊ธฐ์ง€ ์•Š๊ณ  ์ผ์ฐ ๊ณต์œ ํ•˜๋Š” ๋ฌธํ™”, ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ์ •์˜ "์™œ"๋ฅผ ๋ฌธ์„œ๋กœ ๋‚จ๊ธฐ๋Š” ํŒ€. ๋ฆฌ๋ทฐ๋Š” ์‚ฌ๋žŒ์ด ์•„๋‹ˆ๋ผ ์ฝ”๋“œ/์„ค๊ณ„๋ฅผ ํ–ฅํ•˜๊ฒŒ.' },
251
+ ],
252
+ star_guides: [
253
+ { question: '๊ฐ€์žฅ ์–ด๋ ค์› ๋˜ ์žฅ์•  ๋Œ€์‘ ๊ฒฝํ—˜์€?', situation: '์›”๋ง ์ •์‚ฐ ๋ฐฐ์น˜๊ฐ€ ์ง€์—ฐ๋˜๋ฉฐ ๋งˆ๊ฐ ์‹œํ•œ์„ ๋„˜๊ธธ ์œ„ํ—˜์— ์ฒ˜ํ•จ', task: '4์‹œ๊ฐ„ ๋‚ด ์ •์‚ฐ์„ ์ •ํ™•ํžˆ ์™„๋ฃŒํ•˜๊ณ  ์žฌ๋ฐœ์„ ๋ง‰์•„์•ผ ํ–ˆ์Œ', action: '๋ณ‘๋ชฉ ๋ฐฐ์น˜๋ฅผ ๋ณ‘๋ ฌํ™”ํ•˜๊ณ  ์ธ๋ฑ์Šค๋ฅผ ์žฌ์„ค๊ณ„, ํ•ซํ”ฝ์Šค๋ฅผ ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ ํ›„ ๊ฒ€์ฆ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ์ •ํ•ฉ์„ฑ ํ™•์ธ', result: '์ฒ˜๋ฆฌ ์‹œ๊ฐ„์„ 38๋ถ„์œผ๋กœ ๋‹จ์ถ•ํ•˜๊ณ  ์ดํ›„ ๋™์ผ ์žฅ์•  ์žฌ๋ฐœ 0๊ฑด, ์ •์‚ฐ SLA๋ฅผ ์•ˆ์ •ํ™”' },
254
+ { question: '์ฃผ๋„์ ์œผ๋กœ ํŒ€์˜ ๋ฌธ์ œ๋ฅผ ๊ฐœ์„ ํ•œ ๊ฒฝํ—˜์€?', situation: '๋ฐฐํฌ๊ฐ€ ์ฃผ 1ํšŒ๋กœ ๋ฌถ์—ฌ ๊ธฐ๋Šฅ ์ถœ์‹œ๊ฐ€ ๋А๋ฆฌ๊ณ  ๋กค๋ฐฑ์ด ์–ด๋ ค์› ์Œ', task: '๋ฐฐํฌ ๋นˆ๋„๋ฅผ ๋†’์ด๋ฉด์„œ๋„ ์•ˆ์ •์„ฑ์„ ์žƒ์ง€ ์•Š์•„์•ผ ํ–ˆ์Œ', action: 'GitHub Actions + ArgoCD๋กœ ์นด๋‚˜๋ฆฌ ๋ฐฐํฌ ์ž๋™ํ™”, ํ—ฌ์Šค์ฒดํฌ ๊ธฐ๋ฐ˜ ์ž๋™ ๋กค๋ฐฑ ๊ตฌ์„ฑ', result: '๋ฐฐํฌ ๋นˆ๋„ ์ฃผ1ํšŒ โ†’ ์ผ5ํšŒ, ๋กค๋ฐฑ ํ‰๊ท  12๋ถ„ โ†’ 90์ดˆ๋กœ ๋‹จ์ถ•' },
255
+ ],
256
+ },
257
+ ],
258
+
259
+ activities: [
260
+ { id: 'ac1', type: 'application_status_changed', entity_type: 'application', entity_id: 'a1', summary: 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ  โ€” ๋ฉด์ ‘ ์ง„ํ–‰์œผ๋กœ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.', created_at: '2026-06-13T10:00:00.000Z' },
261
+ { id: 'ac2', type: 'interview_prep_saved', entity_type: 'interview_prep', entity_id: 'iv1', summary: 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ๋ฉด์ ‘ ์ค€๋น„ ์ž๋ฃŒ๋ฅผ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.', created_at: '2026-06-12T14:00:00.000Z' },
262
+ { id: 'ac3', type: 'application_status_changed', entity_type: 'application', entity_id: 'a4', summary: '๋„ค์ด๋ฒ„ํŽ˜์ด โ€” ์„œ๋ฅ˜ ํ•ฉ๊ฒฉ์œผ๋กœ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.', created_at: '2026-06-12T10:00:00.000Z' },
263
+ { id: 'ac4', type: 'cover_letter_version_saved', entity_type: 'cover_letter', entity_id: 'c1', summary: 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ์ง€์›๋™๊ธฐ v2๋ฅผ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.', created_at: '2026-06-08T11:00:00.000Z' },
264
+ { id: 'ac5', type: 'fit_analysis_saved', entity_type: 'fit_analysis', entity_id: 'f1', summary: 'ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ์ ํ•ฉ๋„ ๋ถ„์„(88์ )์„ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.', created_at: '2026-06-02T09:30:00.000Z' },
265
+ { id: 'ac6', type: 'job_saved', entity_type: 'job', entity_id: 'j6', summary: '์นด์นด์˜ค๋ฑ…ํฌ ๊ณต๊ณ ๋ฅผ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.', created_at: '2026-06-10T09:00:00.000Z' },
266
+ { id: 'ac7', type: 'resume_added', entity_type: 'document', entity_id: 'd1', summary: '๊น€ํ•˜๋Š˜ ์ด๋ ฅ์„œ(2026)๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.', created_at: '2026-05-03T10:00:00.000Z' },
267
+ ],
268
+ };
269
+ }
@@ -19,6 +19,7 @@ const view = document.getElementById('view');
19
19
  const navEl = document.getElementById('nav');
20
20
  const titleEl = document.getElementById('topbar-title');
21
21
  const actionsEl = document.getElementById('topbar-actions');
22
+ const routeStatusEl = document.getElementById('route-status');
22
23
  const moduleCache = new Map();
23
24
 
24
25
  function parseHash() {
@@ -87,23 +88,54 @@ async function route() {
87
88
  if (token !== renderToken) return; // a newer navigation superseded this one
88
89
  clear(view);
89
90
  await mod.render(ctx);
91
+ // Announce just the new page name to screen readers (titleEl reflects any
92
+ // detail-page ctx.setTitle override).
93
+ if (routeStatusEl) routeStatusEl.textContent = titleEl.textContent || def.label;
90
94
  } catch (err) {
91
95
  if (token !== renderToken) return;
92
96
  clear(view);
93
97
  view.append(el('div', { class: 'card' }, el('div', { class: 'card__body' },
94
98
  el('h3', { style: { marginBottom: '8px' } }, 'ํŽ˜์ด์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค'),
95
- el('p', { class: 'text-secondary' }, err instanceof Error ? err.message : String(err)))));
99
+ el('p', { class: 'text-secondary', style: { marginBottom: '14px' } }, err instanceof Error ? err.message : String(err)),
100
+ el('button', { class: 'btn btn--primary', type: 'button', onClick: () => route() }, '๋‹ค์‹œ ์‹œ๋„'))));
96
101
  toastError(err);
97
102
  }
98
103
  }
99
104
 
105
+ // Mobile/narrow-window nav: the sidebar is the only navigation, so below 900px it
106
+ // becomes an off-canvas drawer toggled from the topbar. Without this a phone/narrow
107
+ // window is stranded on whatever page loaded first.
108
+ function setupMobileNav() {
109
+ const toggle = document.getElementById('nav-toggle');
110
+ const scrim = document.getElementById('nav-scrim');
111
+ const app = document.querySelector('.app');
112
+ if (!toggle || !app) return;
113
+ const setOpen = (open) => {
114
+ app.classList.toggle('nav-open', open);
115
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
116
+ toggle.setAttribute('aria-label', open ? '๋ฉ”๋‰ด ๋‹ซ๊ธฐ' : '๋ฉ”๋‰ด ์—ด๊ธฐ');
117
+ };
118
+ toggle.addEventListener('click', () => setOpen(!app.classList.contains('nav-open')));
119
+ scrim?.addEventListener('click', () => setOpen(false));
120
+ navEl.addEventListener('click', (e) => { if (e.target.closest('.nav__item')) setOpen(false); });
121
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && app.classList.contains('nav-open')) setOpen(false); });
122
+ }
123
+
100
124
  function initTheme() {
101
- const saved = localStorage.getItem('cf-theme');
125
+ // Read new key, falling back to the legacy 'cf-theme' so existing users keep their choice.
126
+ const saved = localStorage.getItem('careermate-theme') || localStorage.getItem('cf-theme');
102
127
  if (saved === 'light' || saved === 'dark') document.documentElement.dataset.theme = saved;
103
128
  }
104
129
 
105
130
  async function boot() {
106
131
  initTheme();
132
+ setupMobileNav();
133
+ // Skip link: jump keyboard focus past the sidebar to the page content. A hash
134
+ // anchor would collide with the router, so move focus directly instead.
135
+ document.getElementById('skip-link')?.addEventListener('click', () => {
136
+ view.setAttribute('tabindex', '-1');
137
+ view.focus();
138
+ });
107
139
  // Show data location in the footer (privacy: user can always see where data lives).
108
140
  try {
109
141
  const h = await get('/api/health');
@@ -13,8 +13,10 @@
13
13
  <link rel="stylesheet" href="/styles.css" />
14
14
  </head>
15
15
  <body>
16
+ <button class="skip-link" id="skip-link" type="button">๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ</button>
16
17
  <div class="app">
17
- <aside class="sidebar">
18
+ <div class="nav-scrim" id="nav-scrim"></div>
19
+ <aside class="sidebar" id="sidebar">
18
20
  <div class="sidebar__brand">
19
21
  <div class="sidebar__logo">C</div>
20
22
  <div>
@@ -31,12 +33,18 @@
31
33
 
32
34
  <div class="main">
33
35
  <header class="topbar">
36
+ <button class="nav-toggle" id="nav-toggle" type="button" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ" aria-expanded="false" aria-controls="sidebar">
37
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
38
+ </button>
34
39
  <div class="topbar__title" id="topbar-title">CareerMate</div>
35
40
  <div class="topbar__spacer"></div>
36
41
  <div id="topbar-actions" class="flex gap-2"></div>
37
42
  </header>
38
- <main id="view" class="view" aria-live="polite"></main>
43
+ <main id="view" class="view"></main>
39
44
  </div>
45
+ <!-- Concise route announcer: speaks only the new page name, not the whole view
46
+ (the old aria-live on #view re-read the entire page on every navigation). -->
47
+ <div id="route-status" class="sr-only" role="status" aria-live="polite"></div>
40
48
  </div>
41
49
 
42
50
  <script type="module" src="/app.js"></script>