careermate 0.1.0 → 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 (134) hide show
  1. package/README.md +22 -17
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/apps/mcp/src/index.ts +4 -3
  4. package/apps/web/DESIGN_GUIDE.md +2 -2
  5. package/apps/web/demo/demo-shim.js +536 -0
  6. package/apps/web/demo/demo.css +75 -0
  7. package/apps/web/demo/seed.js +269 -0
  8. package/apps/web/public/app.js +34 -2
  9. package/apps/web/public/icon-180.png +0 -0
  10. package/apps/web/public/index.html +14 -3
  11. package/apps/web/public/lib.js +135 -23
  12. package/apps/web/public/pages/applications.js +18 -9
  13. package/apps/web/public/pages/documents.js +66 -50
  14. package/apps/web/public/pages/home.js +12 -11
  15. package/apps/web/public/pages/interview.js +9 -12
  16. package/apps/web/public/pages/jobs.js +39 -36
  17. package/apps/web/public/pages/profile.js +66 -86
  18. package/apps/web/public/pages/settings.js +21 -14
  19. package/apps/web/public/styles.css +105 -38
  20. package/apps/web/src/http.ts +31 -4
  21. package/apps/web/src/info.ts +9 -22
  22. package/apps/web/src/routes.ts +19 -14
  23. package/apps/web/src/server.ts +34 -29
  24. package/apps/web/src/settings.ts +35 -17
  25. package/bin/careermate.mjs +8 -5
  26. package/dist/doctor.mjs +78908 -0
  27. package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +12 -0
  28. package/dist/install-page/.omc/state/idle-notif-cooldown.json +3 -0
  29. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +147 -0
  30. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +22 -0
  31. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +44 -0
  32. package/dist/install-page/favicon.svg +1 -0
  33. package/dist/install-page/icon-180.png +0 -0
  34. package/dist/install-page/icon-512.png +0 -0
  35. package/dist/install-page/index.html +328 -225
  36. package/dist/install-page/landing.js +54 -0
  37. package/dist/install-page/llms-install.txt +202 -0
  38. package/dist/install-page/og.png +0 -0
  39. package/dist/install-page/shots/applications-dark.png +0 -0
  40. package/dist/install-page/shots/applications-light.png +0 -0
  41. package/dist/install-page/shots/documents-dark.png +0 -0
  42. package/dist/install-page/shots/documents-light.png +0 -0
  43. package/dist/install-page/shots/home-dark.png +0 -0
  44. package/dist/install-page/shots/home-light.png +0 -0
  45. package/dist/install-page/shots/interview-dark.png +0 -0
  46. package/dist/install-page/shots/interview-light.png +0 -0
  47. package/dist/install-page/shots/jobs-dark.png +0 -0
  48. package/dist/install-page/shots/jobs-light.png +0 -0
  49. package/dist/install-page/shots/settings-dark.png +0 -0
  50. package/dist/install-page/shots/settings-light.png +0 -0
  51. package/dist/install-page/start.html +323 -0
  52. package/dist/install-page/style.css +128 -4
  53. package/dist/mcp.mjs +74225 -2007
  54. package/dist/{mcpb-stage/dist/web.mjs → migrate.mjs} +142 -2660
  55. package/dist/pdf.worker.min.mjs +28 -0
  56. package/dist/public/app.js +34 -2
  57. package/dist/public/icon-180.png +0 -0
  58. package/dist/public/index.html +14 -3
  59. package/dist/public/lib.js +135 -23
  60. package/dist/public/pages/applications.js +18 -9
  61. package/dist/public/pages/documents.js +66 -50
  62. package/dist/public/pages/home.js +12 -11
  63. package/dist/public/pages/interview.js +9 -12
  64. package/dist/public/pages/jobs.js +39 -36
  65. package/dist/public/pages/profile.js +66 -86
  66. package/dist/public/pages/settings.js +21 -14
  67. package/dist/public/styles.css +105 -38
  68. package/dist/web.mjs +39950 -369
  69. package/docs/ARCHITECTURE.md +5 -5
  70. package/docs/DECISIONS.md +1 -1
  71. package/docs/DEMO.md +6 -6
  72. package/docs/MCP_TOOLS.md +68 -9
  73. package/docs/ROADMAP.md +11 -10
  74. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
  75. package/docs/START_WORKFLOW.md +19 -7
  76. package/docs/SUPPORTED_AI_APPS.md +85 -33
  77. package/docs/TODO.md +1 -1
  78. package/docs/WORKFLOWS.md +8 -6
  79. package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +64 -0
  80. package/package.json +8 -3
  81. package/packages/core/src/services.ts +42 -1
  82. package/packages/db/src/connection.ts +30 -0
  83. package/packages/db/src/index.ts +1 -0
  84. package/packages/db/src/paths.ts +11 -0
  85. package/packages/db/src/repositories.ts +109 -63
  86. package/packages/db/src/schema.ts +118 -0
  87. package/packages/exporters/src/html.ts +9 -12
  88. package/packages/mcp-tools/src/bridge.ts +55 -32
  89. package/packages/mcp-tools/src/index.ts +9 -0
  90. package/packages/mcp-tools/src/tools.ts +269 -65
  91. package/packages/mcp-tools/src/update.ts +330 -0
  92. package/packages/parsers/src/documents.ts +521 -0
  93. package/packages/parsers/src/index.ts +8 -5
  94. package/packages/prompts/src/onboarding.ts +2 -2
  95. package/packages/shared/src/build-info.ts +22 -0
  96. package/packages/shared/src/enums.ts +39 -0
  97. package/packages/shared/src/index.ts +1 -0
  98. package/packages/shared/src/schemas.ts +24 -3
  99. package/packages/workflows/src/definitions.ts +4 -2
  100. package/scripts/build-demo.mjs +130 -0
  101. package/scripts/build-dist.mjs +46 -8
  102. package/scripts/build-mcpb.mjs +60 -4
  103. package/scripts/doctor.ts +16 -3
  104. package/scripts/gen-og.mjs +86 -0
  105. package/scripts/init.ts +32 -3
  106. package/scripts/seed.ts +5 -5
  107. package/scripts/shots-seed.ts +192 -0
  108. package/scripts/shots.ts +89 -0
  109. package/tsconfig.json +3 -1
  110. package/dist/careermate.mcpb +0 -0
  111. package/dist/mcp-smoke.err +0 -3
  112. package/dist/mcpb-stage/README.md +0 -219
  113. package/dist/mcpb-stage/dist/install-page/index.html +0 -434
  114. package/dist/mcpb-stage/dist/install-page/style.css +0 -407
  115. package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
  116. package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
  117. package/dist/mcpb-stage/dist/public/app.js +0 -118
  118. package/dist/mcpb-stage/dist/public/index.html +0 -41
  119. package/dist/mcpb-stage/dist/public/lib.js +0 -282
  120. package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
  121. package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
  122. package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
  123. package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
  124. package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
  125. package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
  126. package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
  127. package/dist/mcpb-stage/dist/public/styles.css +0 -420
  128. package/dist/mcpb-stage/manifest.json +0 -40
  129. package/docs/INSTALL.md +0 -148
  130. package/docs/INSTALL_AND_USAGE.md +0 -99
  131. package/install-page/index.html +0 -474
  132. package/install-page/style.css +0 -391
  133. package/install-page/vercel.json +0 -20
  134. /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
@@ -20,6 +20,27 @@ import {
20
20
  const isoDateTime = z.string();
21
21
  const id = z.string();
22
22
 
23
+ /**
24
+ * Reject script-capable URL schemes (javascript:, data:, vbscript:, file:, …)
25
+ * while allowing http(s), mailto, and scheme-less/relative links. Links are
26
+ * rendered as clickable anchors in the dashboard, so a `javascript:` URL stored
27
+ * here would otherwise execute when clicked (DOM-XSS). The dashboard also
28
+ * sanitizes href/src at render time (lib.js `safeUrl`); this is the write-time
29
+ * half of that defense-in-depth.
30
+ */
31
+ function isSafeUrl(u: string): boolean {
32
+ const s = u.trim();
33
+ // An explicit scheme (text before the first ':') must be http(s) or mailto.
34
+ // Scheme-less values (relative paths, bare domains, #anchors) cannot execute.
35
+ return !/^[a-z][a-z0-9+.-]*:/i.test(s) || /^(https?:|mailto:)/i.test(s);
36
+ }
37
+ const UNSAFE_URL_MSG = 'javascript: 같은 위험한 링크는 사용할 수 없습니다. http(s) 링크를 사용하세요.';
38
+ const SafeUrl = z.string().min(1).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
39
+ const SafeUrlOptional = z
40
+ .string()
41
+ .optional()
42
+ .refine((u) => u == null || u === '' || isSafeUrl(u), { message: UNSAFE_URL_MSG });
43
+
23
44
  const baseRecord = {
24
45
  id,
25
46
  created_at: isoDateTime,
@@ -30,7 +51,7 @@ const baseRecord = {
30
51
 
31
52
  export const LinkSchema = z.object({
32
53
  label: z.string().min(1),
33
- url: z.string().min(1),
54
+ url: SafeUrl,
34
55
  });
35
56
  export type Link = z.infer<typeof LinkSchema>;
36
57
 
@@ -104,7 +125,7 @@ export const ProjectInputSchema = z.object({
104
125
  description: z.string().optional(),
105
126
  highlights: z.array(z.string()).optional(),
106
127
  tech: z.array(z.string()).optional(),
107
- url: z.string().optional(),
128
+ url: SafeUrlOptional,
108
129
  start_date: z.string().optional(),
109
130
  end_date: z.string().optional(),
110
131
  order_index: z.number().optional(),
@@ -220,7 +241,7 @@ export type CoverLetterRecord = z.infer<typeof CoverLetterRecordSchema>;
220
241
  export const JobInputSchema = z.object({
221
242
  company: z.string().min(1),
222
243
  position: z.string().min(1).describe('직무/포지션명'),
223
- url: z.string().optional(),
244
+ url: SafeUrlOptional,
224
245
  location: z.string().optional(),
225
246
  employment_type: z.string().optional(),
226
247
  description: z.string().optional().describe('공고 원문 또는 정리된 텍스트'),
@@ -29,10 +29,12 @@ export const WORKFLOWS: WorkflowDefinition[] = [
29
29
  '사용자가 처음 CareerMate에 연결했거나, "시작하자 / 셋업해줘 / 프로필 등록"이라고 요청할 때.',
30
30
  steps: [
31
31
  '`get_onboarding_status`를 호출해 현재 등록 상태(has_profile, has_resume, has_cover_letter, has_experience, has_skills, profile_completeness, next_steps)를 확인한다.',
32
- '비어 있는 항목을 사용자에게 쉬운 한국어로 안내하고, 이력서/자기소개서 파일 업로드 또는 정보 입력을 요청한다.',
32
+ '비어 있는 항목을 사용자에게 쉬운 한국어로 안내하고, 먼저 "기존에 작성해 둔 이력서·경력기술서 파일이 있는지" 묻는다(hwp/docx/pdf 모두 가능).',
33
+ '파일 입력 분기: ① 사용자가 "파일이 있다"고 하면 `open_inbox`로 인입 폴더를 열어 파일을 넣게 하고, "다 넣었다"고 하면 `read_inbox`로 본문을 읽는다(pdf·이미지는 read_inbox가 경로만 주므로 클라이언트의 파일 읽기로 직접 읽는다). ② 사용자가 파일 경로를 직접 알려주면 `read_document`로 읽는다. ③ "없다/직접 입력/텍스트로 붙여넣기"면 폴더를 열지 말고 받은 텍스트를 그대로 쓴다. (묻지도 않고 open_inbox로 폴더를 자동으로 열지 않는다.)',
33
34
  '수집한 정보를 구조화해 `save_profile`로 저장한다(이름·연락처·headline·summary·desired_roles·desired_conditions, 그리고 글쓰기 선호인 preferred_tone과 emphasis_points 포함). 파일 출처는 source=upload, 직접 입력은 manual.',
34
35
  '업로드한 이력서/경력기술서/포트폴리오 본문을 `add_resume`로 저장한다(kind·title 지정, 대표 문서는 is_primary=true).',
35
- '기존 자기소개서가 있으면 `add_cover_letter`로 저장한다(없으면 나중에 작성 가능함을 안내).',
36
+ '경력·프로젝트·스킬이 파악되면 `add_experience`·`add_project`·`add_skill`로 각각 저장한다(완성도의 has_experience/has_skills에 반영).',
37
+ '기존 자기소개서가 있으면 `save_cover_letter_version`으로 저장한다(title+content면 v1로 저장, 없으면 나중에 작성 가능함을 안내).',
36
38
  '`open_dashboard`를 호출해 사용자가 자기 데이터를 확인하게 한다.',
37
39
  '다시 `get_onboarding_status`로 완성도를 확인하고, 다음 단계(공고 분석 등)를 제안한다.',
38
40
  ],
@@ -0,0 +1,130 @@
1
+ // =============================================================================
2
+ // build-demo.mjs — generate the no-server demo dashboard served at
3
+ // careermate.life/demo.
4
+ //
5
+ // What it does:
6
+ // 1. Copy apps/web/public/* -> install-page/demo/
7
+ // 2. Rewrite root-absolute asset paths (/lib.js, /pages/, /styles.css, ...)
8
+ // to /demo/... so they resolve under the subpath. (/api/* is left alone —
9
+ // the shim intercepts those at runtime.)
10
+ // 3. Copy the demo overlay (demo-shim.js, seed.js, demo.css) into install-page/demo/.
11
+ // 4. Inject the shim + demo.css into the copied index.html, and a hint that
12
+ // this is a demo build.
13
+ //
14
+ // The generated install-page/demo/ folder is committed so Vercel (which serves
15
+ // install-page/ statically) publishes it with no build step or config change.
16
+ //
17
+ // Run: node scripts/build-demo.mjs
18
+ //
19
+ // Note: avoids fs.rmSync(recursive) — it hard-crashes on Node 25 / Windows
20
+ // (see project memory). Uses a manual recursive remove instead.
21
+ // =============================================================================
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ const ROOT = path.resolve(__dirname, '..');
28
+ const SRC = path.join(ROOT, 'apps', 'web', 'public');
29
+ const OVERLAY = path.join(ROOT, 'apps', 'web', 'demo');
30
+ const OUT = path.join(ROOT, 'install-page', 'demo');
31
+
32
+ const TEXT_EXT = new Set(['.html', '.js', '.css', '.svg', '.json', '.txt', '.md']);
33
+
34
+ /** Manual recursive delete (Node-25-safe; fs.rmSync recursive crashes there). */
35
+ function rimraf(target) {
36
+ let st;
37
+ try { st = fs.lstatSync(target); } catch { return; }
38
+ if (st.isDirectory()) {
39
+ for (const entry of fs.readdirSync(target)) rimraf(path.join(target, entry));
40
+ fs.rmdirSync(target);
41
+ } else {
42
+ fs.unlinkSync(target);
43
+ }
44
+ }
45
+
46
+ /** Rewrite root-absolute references to assets so they work under /demo/. */
47
+ function rewritePaths(code) {
48
+ return code
49
+ // import/href/src strings: '/lib.js', "/app.js", '/styles.css', '/icon-180.png'
50
+ .replace(/(['"`])\/(lib\.js|app\.js|styles\.css|icon-180\.png)/g, '$1/demo/$2')
51
+ // directories referenced as '/pages/...' or '/fonts/...' (JS/HTML)
52
+ .replace(/(['"`])\/(pages|fonts)\//g, '$1/demo/$2/')
53
+ // CSS url(/fonts/...) — with or without quotes
54
+ .replace(/url\(\s*(['"]?)\/fonts\//g, 'url($1/demo/fonts/');
55
+ }
56
+
57
+ function copyTree(src, out) {
58
+ fs.mkdirSync(out, { recursive: true });
59
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
60
+ const from = path.join(src, entry.name);
61
+ const to = path.join(out, entry.name);
62
+ if (entry.isDirectory()) { copyTree(from, to); continue; }
63
+ const ext = path.extname(entry.name).toLowerCase();
64
+ if (TEXT_EXT.has(ext)) {
65
+ fs.writeFileSync(to, rewritePaths(fs.readFileSync(from, 'utf8')));
66
+ } else {
67
+ fs.copyFileSync(from, to);
68
+ }
69
+ }
70
+ }
71
+
72
+ function injectDemo(htmlPath) {
73
+ let html = fs.readFileSync(htmlPath, 'utf8');
74
+
75
+ // Pre-paint theme resolver — same key/default as the marketing site so the
76
+ // theme is shared and there's no flash (FOUC) before app.js runs.
77
+ const themeScript =
78
+ '\n <!-- Resolve the saved theme before first paint (shared with the marketing site; default dark). -->\n' +
79
+ ' <script>\n' +
80
+ ' (function () {\n' +
81
+ " try {\n" +
82
+ " var t = window.localStorage.getItem('careermate-theme') || window.localStorage.getItem('cf-theme');\n" +
83
+ " document.documentElement.setAttribute('data-theme', t === 'light' || t === 'dark' ? t : 'dark');\n" +
84
+ " } catch (e) { document.documentElement.setAttribute('data-theme', 'dark'); }\n" +
85
+ ' })();\n' +
86
+ ' </script>';
87
+ html = html.replace(
88
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
89
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />' + themeScript,
90
+ );
91
+
92
+ // demo.css after the dashboard stylesheet
93
+ html = html.replace(
94
+ '<link rel="stylesheet" href="/demo/styles.css" />',
95
+ '<link rel="stylesheet" href="/demo/styles.css" />\n <link rel="stylesheet" href="/demo/demo.css" />',
96
+ );
97
+
98
+ // Shim must run before app.js. Both are ES modules; module scripts execute in
99
+ // document order, so placing the shim immediately before app.js guarantees the
100
+ // fetch patch is installed before any page code calls it.
101
+ html = html.replace(
102
+ '<script type="module" src="/demo/app.js"></script>',
103
+ '<script type="module" src="/demo/demo-shim.js"></script>\n <script type="module" src="/demo/app.js"></script>',
104
+ );
105
+
106
+ // Title hint so the browser tab reads as a demo.
107
+ html = html.replace(
108
+ '<title>CareerMate — 내 커리어 흐름 관리</title>',
109
+ '<title>CareerMate 데모 — 대시보드 미리보기</title>',
110
+ );
111
+
112
+ fs.writeFileSync(htmlPath, html);
113
+ }
114
+
115
+ /* ------------------------------------------------------------------- run */
116
+ console.log('· clearing', path.relative(ROOT, OUT));
117
+ rimraf(OUT);
118
+
119
+ console.log('· copying dashboard', path.relative(ROOT, SRC), '->', path.relative(ROOT, OUT));
120
+ copyTree(SRC, OUT);
121
+
122
+ console.log('· copying demo overlay');
123
+ for (const f of ['demo-shim.js', 'seed.js', 'demo.css']) {
124
+ fs.copyFileSync(path.join(OVERLAY, f), path.join(OUT, f));
125
+ }
126
+
127
+ console.log('· injecting shim + styles into index.html');
128
+ injectDemo(path.join(OUT, 'index.html'));
129
+
130
+ console.log('✓ demo built at', path.relative(ROOT, OUT), '— serve install-page/ and open /demo');
@@ -3,10 +3,12 @@
3
3
  * scripts/build-dist.mjs — 배포용 플레인 JS 번들 빌드 (esbuild).
4
4
  *
5
5
  * 개발은 그대로 tsx 무빌드로 돌리고, 배포(.mcpb / npm publish / 추후 독립 .exe)에서는
6
- * tsx·tsconfig 경로별칭 런타임 의존성을 없애기 위해 두 진입점을 단일 ESM 파일로 번들한다.
6
+ * tsx·tsconfig 경로별칭 런타임 의존성을 없애기 위해 네 진입점을 각각 단일 ESM 파일로 번들한다.
7
7
  *
8
- * dist/mcp.mjs MCP stdio 서버 (Claude Desktop이 실행)
9
- * dist/web.mjs 로컬 대시보드 서버
8
+ * dist/mcp.mjs MCP stdio 서버 (Claude Desktop이 실행)
9
+ * dist/web.mjs 로컬 대시보드 서버
10
+ * dist/doctor.mjs 설치/환경 점검 CLI
11
+ * dist/migrate.mjs DB 마이그레이션 CLI
10
12
  * dist/public/ 대시보드 정적 자산 (apps/web/public 복사)
11
13
  * dist/install-page/ 설치 안내 페이지 복사
12
14
  *
@@ -23,14 +25,25 @@ const DIST = path.join(ROOT, 'dist');
23
25
 
24
26
  async function main() {
25
27
  console.log('CareerMate dist 번들 빌드(esbuild)…');
26
- fs.rmSync(path.join(DIST, 'mcp.mjs'), { force: true });
27
- fs.rmSync(path.join(DIST, 'web.mjs'), { force: true });
28
+ // package.json을 버전 단일 출처로 삼아 번들에 주입(앱별 하드코딩 버전 드리프트 방지).
29
+ const pkgVersion = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
30
+ // dist를 통째로 비우고 다시 만든다. build-dist가 소유하는 산출물(mcp/web/doctor/migrate/
31
+ // public/install-page)만 남기기 위함. 이렇게 해야 직전 build:mcpb가 남긴 mcpb-stage/·
32
+ // careermate.mcpb(수 MB)나 테스트 잔여물(mcp-smoke.err)이 npm tarball에 섞여 들어가지 않는다
33
+ // (npm publish의 prepack은 build-dist만 실행하므로 여기서 비우면 게시본이 깨끗하다).
34
+ // build:mcpb는 이 함수를 먼저 호출한 뒤 mcpb-stage/·careermate.mcpb를 다시 생성하므로 안전.
35
+ fs.rmSync(DIST, { recursive: true, force: true });
36
+ fs.mkdirSync(DIST, { recursive: true });
28
37
 
29
38
  await esbuild.build({
30
39
  absWorkingDir: ROOT,
31
40
  entryPoints: {
32
41
  mcp: 'apps/mcp/src/index.ts',
33
42
  web: 'apps/web/src/index.ts',
43
+ // doctor/migrate도 번들해 둔다. 이 둘은 @careermate/* 워크스페이스 패키지를 import하는데,
44
+ // 게시 tarball에는 그 패키지들이 resolvable하지 않아 tsx 폴백이 실패한다(번들이면 인라인돼 동작).
45
+ doctor: 'scripts/doctor.ts',
46
+ migrate: 'scripts/migrate.ts',
34
47
  },
35
48
  outdir: 'dist',
36
49
  outExtension: { '.js': '.mjs' },
@@ -39,7 +52,7 @@ async function main() {
39
52
  format: 'esm',
40
53
  target: 'node22',
41
54
  tsconfig: 'tsconfig.json',
42
- define: { __BUNDLED__: 'true' },
55
+ define: { __BUNDLED__: 'true', __APP_VERSION__: JSON.stringify(pkgVersion) },
43
56
  // ESM 출력에서 CJS 의존성이 require/__dirname을 써도 동작하도록 shim 주입.
44
57
  banner: {
45
58
  js: "import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);",
@@ -51,9 +64,34 @@ async function main() {
51
64
  fs.rmSync(path.join(DIST, 'public'), { recursive: true, force: true });
52
65
  fs.rmSync(path.join(DIST, 'install-page'), { recursive: true, force: true });
53
66
  fs.cpSync(path.join(ROOT, 'apps', 'web', 'public'), path.join(DIST, 'public'), { recursive: true });
54
- fs.cpSync(path.join(ROOT, 'install-page'), path.join(DIST, 'install-page'), { recursive: true });
67
+ // install-page/careermate.mcpb 는 공개 사이트(careermate.life)의 다운로드 산출물일 뿐,
68
+ // 번들 대시보드의 /install/ 라우트에는 필요 없다. 이걸 dist로 들이면 .mcpb 빌드가 직전
69
+ // 번들(자기 자신, ~6MB)을 다시 접어 넣어 매 빌드마다 용량이 불어난다(자기참조). 제외한다.
70
+ // install-page/demo 는 공개 사이트(careermate.life/demo) 전용 미리보기다. 자산 경로가
71
+ // /demo/ 로 재작성돼 있어 번들 대시보드의 /install/ 라우트에서는 동작하지 않고, 폰트까지
72
+ // 포함해 ~2.5MB를 헛되이 키운다. dist에서 제외한다.
73
+ const demoDir = path.join(ROOT, 'install-page', 'demo');
74
+ fs.cpSync(path.join(ROOT, 'install-page'), path.join(DIST, 'install-page'), {
75
+ recursive: true,
76
+ filter: (src) => path.basename(src) !== 'careermate.mcpb' && src !== demoDir,
77
+ });
78
+
79
+ // pdfjs 워커 사이드카: pdf.mjs는 번들에 인라인되지만, 워커는 런타임에 별도 파일로
80
+ // 동적 로드된다(메인스레드 fake worker). 번들 옆에 복사해 두고 documents.ts가
81
+ // workerSrc로 가리킨다. 이게 없으면 PDF 추출 시 "fake worker" 로드가 실패한다.
82
+ // (node_modules가 없는 .mcpb 배포에서도 PDF가 동작하도록 dist에 자기완결로 동봉.)
83
+ const pdfWorker = path.join(ROOT, 'node_modules', 'pdfjs-dist', 'legacy', 'build', 'pdf.worker.min.mjs');
84
+ if (!fs.existsSync(pdfWorker)) {
85
+ // 경고로 넘기면 워커 없는 번들이 조용히 게시되고 런타임에 모든 PDF 추출이 깨진다(빌드는 초록).
86
+ // 빌드를 실패시켜 누락을 게시 전에 드러낸다. pdfjs 업그레이드로 워커 경로가 바뀌면 여기서 잡힌다.
87
+ throw new Error(
88
+ `pdfjs 워커를 찾지 못했습니다: ${pdfWorker}\n` +
89
+ `pdfjs-dist가 설치됐는지, 워커 파일명/경로가 바뀌지 않았는지 확인하세요(PDF 추출의 필수 자산).`,
90
+ );
91
+ }
92
+ fs.copyFileSync(pdfWorker, path.join(DIST, 'pdf.worker.min.mjs'));
55
93
 
56
- console.log('✅ dist 빌드 완료: dist/mcp.mjs, dist/web.mjs, dist/public, dist/install-page');
94
+ console.log('✅ dist 빌드 완료: dist/mcp.mjs, dist/web.mjs, dist/doctor.mjs, dist/migrate.mjs, dist/public, dist/install-page');
57
95
  }
58
96
 
59
97
  main().catch((err) => {
@@ -28,6 +28,27 @@ function run(cmd, args, opts = {}) {
28
28
  return r.status === 0;
29
29
  }
30
30
 
31
+ /**
32
+ * The MCP tool registry (packages/mcp-tools) is the single source of truth for
33
+ * what tools exist. Extract {name, description} from it so manifest.json never
34
+ * drifts from the real 24-tool list. Returns null if extraction fails.
35
+ */
36
+ function extractTools() {
37
+ const script =
38
+ "import {TOOLS} from '@careermate/mcp-tools';" +
39
+ "process.stdout.write(JSON.stringify(TOOLS.map(t=>({name:t.name,description:t.title||t.description}))));";
40
+ const r = spawnSync(process.execPath, ['--no-warnings', '--import', 'tsx', '-e', script], {
41
+ cwd: ROOT,
42
+ encoding: 'utf8',
43
+ });
44
+ if (r.status !== 0 || !r.stdout) return null;
45
+ try {
46
+ return JSON.parse(r.stdout);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
31
52
  function main() {
32
53
  console.log('CareerMate .mcpb 빌드를 시작합니다…\n');
33
54
 
@@ -41,10 +62,32 @@ function main() {
41
62
  console.log('\n2) 번들 스테이징(manifest + dist)…');
42
63
  fs.rmSync(STAGE, { recursive: true, force: true });
43
64
  fs.mkdirSync(STAGE, { recursive: true });
44
- fs.copyFileSync(path.join(ROOT, 'manifest.json'), path.join(STAGE, 'manifest.json'));
45
- // dist/ 에서 산출물만 복사 (스테이지/번들 산출물 자기참조 제외).
65
+ // manifest 버전은 package.json을 단일 출처로 삼아 주입한다(.mcpb 버전 드리프트 방지).
66
+ const pkgVersion = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
67
+ const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'manifest.json'), 'utf8'));
68
+ if (manifest.version !== pkgVersion) {
69
+ console.log(` manifest 버전 ${manifest.version} → ${pkgVersion} (package.json 기준으로 맞춤)`);
70
+ manifest.version = pkgVersion;
71
+ }
72
+ // 도구 목록은 mcp-tools 레지스트리를 단일 출처로 주입(24개 ↔ manifest 드리프트 방지).
73
+ const tools = extractTools();
74
+ if (tools) {
75
+ console.log(` manifest 도구 목록 ${manifest.tools?.length ?? 0} → ${tools.length} (mcp-tools 기준으로 맞춤)`);
76
+ manifest.tools = tools;
77
+ } else {
78
+ console.warn(' ⚠ 도구 목록 추출 실패 — 기존 manifest.tools를 그대로 사용합니다.');
79
+ }
80
+ fs.writeFileSync(path.join(STAGE, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
81
+ // dist/ 에서 런타임에 필요한 산출물만 복사한다.
82
+ // - mcp.mjs : .mcpb의 entry_point(필수)
83
+ // - web.mjs/public : open_dashboard로 띄우는 로컬 대시보드(필수)
84
+ // - install-page : 제외. 공개 마케팅 랜딩의 사본이라 MCP 서버·대시보드 동작에 불필요하고,
85
+ // fonts(2MB)+shots(2MB) 때문에 .mcpb를 두 배로 불린다. 대시보드 /install/
86
+ // 라우트는 부재 시 serveStatic이 폴백하므로 깨지지 않는다(.mcpb 사용자는
87
+ // 애초에 careermate.life를 거쳐 들어온다). npm 패키지는 dist/install-page를
88
+ // 그대로 포함하므로 /install/가 유지된다.
46
89
  fs.mkdirSync(path.join(STAGE, 'dist'), { recursive: true });
47
- for (const name of ['mcp.mjs', 'web.mjs', 'public', 'install-page']) {
90
+ for (const name of ['mcp.mjs', 'web.mjs', 'public']) {
48
91
  const src = path.join(DIST, name);
49
92
  if (fs.existsSync(src)) fs.cpSync(src, path.join(STAGE, 'dist', name), { recursive: true });
50
93
  }
@@ -64,7 +107,20 @@ function main() {
64
107
 
65
108
  const size = (fs.statSync(OUT).size / (1024 * 1024)).toFixed(1);
66
109
  console.log(`\n✅ 완성: ${OUT} (${size} MB)`);
67
- console.log(' 배포: 이 파일을 GitHub Releases 등에 올리면 유저가 더블클릭만 하면 Claude Desktop에 설치됩니다.');
110
+
111
+ // 랜딩 페이지(install-page/)가 .mcpb를 직접 서빙한다(careermate.life/careermate.mcpb).
112
+ // private repo라 GitHub Release 다운로드가 막혀, 빌드 결과를 페이지로 복사해 다운로드
113
+ // 버튼을 항상 최신으로 유지한다. install-page/ 는 배포 소스라 사이트와 함께 배포된다.
114
+ console.log('\n4) 랜딩 페이지로 복사…');
115
+ const pageDir = path.join(ROOT, 'install-page');
116
+ if (fs.existsSync(pageDir)) {
117
+ const pageCopy = path.join(pageDir, 'careermate.mcpb');
118
+ fs.copyFileSync(OUT, pageCopy);
119
+ console.log(` 복사 완료: ${pageCopy}`);
120
+ console.log(' 배포: install-page/ 를 배포하면 유저가 페이지에서 바로 내려받아 Settings → Extensions → Install extension… 으로 설치합니다.');
121
+ } else {
122
+ console.log(` 건너뜀: ${pageDir} 없음 — 페이지 호스팅 복사 생략.`);
123
+ }
68
124
  }
69
125
 
70
126
  main();
package/scripts/doctor.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileRepo, experienceRepo, projectRepo, skillRepo, documentRepo,
11
11
  coverLetterRepo, jobRepo, applicationRepo, interviewRepo,
12
12
  } from '@careermate/db';
13
- import { TOOLS } from '@careermate/mcp-tools';
13
+ import { TOOLS, getUpdateStatusAsync } from '@careermate/mcp-tools';
14
14
 
15
15
  const ok = (s: string) => console.log(` ✅ ${s}`);
16
16
  const warn = (s: string) => console.log(` ⚠️ ${s}`);
@@ -24,7 +24,7 @@ function checkNode(): boolean {
24
24
  return good;
25
25
  }
26
26
 
27
- function main(): void {
27
+ async function main(): Promise<void> {
28
28
  console.log('\nCareerMate 상태 점검\n' + '─'.repeat(40));
29
29
 
30
30
  const nodeOk = checkNode();
@@ -68,6 +68,16 @@ function main(): void {
68
68
  // MCP tools
69
69
  ok(`MCP 도구 ${TOOLS.length}개 등록됨`);
70
70
 
71
+ // Update available?
72
+ try {
73
+ const u = await getUpdateStatusAsync();
74
+ if (u.latest === null) info(`버전 v${u.current} (업데이트 확인 생략 — 오프라인이거나 일시적 오류)`);
75
+ else if (u.update_available) warn(`새 버전 v${u.latest} 있음 (현재 v${u.current}). 업데이트: ${u.update_command}`);
76
+ else ok(`최신 버전 v${u.current}`);
77
+ } catch {
78
+ /* 업데이트 확인 실패는 점검을 막지 않는다. */
79
+ }
80
+
71
81
  // Dashboard running?
72
82
  const rt = readRuntimeInfo();
73
83
  if (rt && isProcessAlive(rt.pid)) ok(`대시보드 실행 중: ${rt.url}`);
@@ -78,4 +88,7 @@ function main(): void {
78
88
  process.exit(nodeOk && dbOk ? 0 : 1);
79
89
  }
80
90
 
81
- main();
91
+ main().catch((e) => {
92
+ console.error(e instanceof Error ? e.message : e);
93
+ process.exit(1);
94
+ });
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/gen-og.mjs — 소셜 공유용 OG 이미지와 앱 아이콘(PNG) 생성기.
4
+ *
5
+ * 브랜드 색(에메랄드 #0f7256 · 크림 페이퍼 #faf8f4)을 랜딩/대시보드와 통일해
6
+ * Playwright(chromium)로 다음을 렌더링한다. 한 번 만들어 두면 정적 자산으로 커밋된다.
7
+ *
8
+ * install-page/favicon.svg 벡터 파비콘 (모던 브라우저)
9
+ * install-page/og.png 1200×630 소셜 공유 카드 (og:image / twitter:image)
10
+ * install-page/icon-180.png apple-touch-icon
11
+ * install-page/icon-512.png PWA/대형 폴백
12
+ * apps/web/public/icon-180.png 대시보드 apple-touch-icon (동일 자산)
13
+ *
14
+ * 실행: node scripts/gen-og.mjs (사전: npx playwright install chromium)
15
+ */
16
+ import { chromium } from 'playwright';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const LANDING = path.join(ROOT, 'install-page');
23
+ const PUBLIC = path.join(ROOT, 'apps', 'web', 'public');
24
+
25
+ const EMERALD = '#0f7256';
26
+ const CREAM = '#faf8f4';
27
+ const KR_FONT = "'Malgun Gothic','Apple SD Gothic Neo','Noto Sans KR',sans-serif";
28
+
29
+ const favicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="${EMERALD}"/><text x="12" y="17" font-size="14" text-anchor="middle" fill="#fff" font-family="Segoe UI,sans-serif" font-weight="bold">C</text></svg>`;
30
+
31
+ const ogHtml = `<!doctype html><meta charset="utf-8"><style>
32
+ *{margin:0;box-sizing:border-box}
33
+ body{width:1200px;height:630px;background:${CREAM};font-family:${KR_FONT};color:#1c1a16}
34
+ .wrap{width:100%;height:100%;padding:78px 88px;display:flex;flex-direction:column;justify-content:space-between}
35
+ .brand{display:flex;align-items:center;gap:20px}
36
+ .logo{width:84px;height:84px;border-radius:20px;background:${EMERALD};color:#fff;font-weight:800;font-size:48px;display:grid;place-items:center}
37
+ .word{font-size:40px;font-weight:800;letter-spacing:-1.5px}
38
+ .h{font-size:78px;font-weight:800;line-height:1.08;letter-spacing:-2.5px}
39
+ .accent{color:${EMERALD}}
40
+ .sub{font-size:29px;color:#595449;line-height:1.5;margin-top:26px;max-width:900px}
41
+ .pill{display:inline-flex;align-items:center;gap:13px;font-size:23px;font-weight:600;color:#0b5b44;border:1px solid #bcdccd;background:#e7f1ec;padding:13px 24px;border-radius:999px;align-self:flex-start}
42
+ .dot{width:12px;height:12px;border-radius:50%;background:${EMERALD}}
43
+ </style><div class="wrap">
44
+ <div class="brand"><div class="logo">C</div><div class="word">CareerMate</div></div>
45
+ <div>
46
+ <div class="h">내 <span class="accent">AI</span>를<br>나만의 <span class="accent">커리어 비서</span>로</div>
47
+ <div class="sub">이력서·자기소개서·지원 현황을 이 컴퓨터 안에서 관리하세요.<br>분석·글쓰기는 평소 쓰던 AI가, 데이터 보관은 CareerMate가.</div>
48
+ </div>
49
+ <div class="pill"><span class="dot"></span>로컬 전용 · MCP · 오픈소스</div>
50
+ </div>`;
51
+
52
+ const iconHtml = (size) => `<!doctype html><meta charset="utf-8"><style>
53
+ *{margin:0}
54
+ body{width:${size}px;height:${size}px;background:${EMERALD};display:grid;place-items:center}
55
+ .c{color:#fff;font-family:${KR_FONT};font-weight:800;font-size:${Math.round(size * 0.56)}px;line-height:1}
56
+ </style><div class="c">C</div>`;
57
+
58
+ async function shoot(page, html, w, h, out) {
59
+ await page.setViewportSize({ width: w, height: h });
60
+ await page.setContent(html, { waitUntil: 'networkidle' });
61
+ await page.screenshot({ path: out, clip: { x: 0, y: 0, width: w, height: h } });
62
+ console.log(' ✔', path.relative(ROOT, out));
63
+ }
64
+
65
+ async function main() {
66
+ fs.writeFileSync(path.join(LANDING, 'favicon.svg'), favicon);
67
+ console.log(' ✔', path.relative(ROOT, path.join(LANDING, 'favicon.svg')));
68
+
69
+ const browser = await chromium.launch();
70
+ const page = await browser.newPage({ deviceScaleFactor: 1 });
71
+ try {
72
+ await shoot(page, ogHtml, 1200, 630, path.join(LANDING, 'og.png'));
73
+ await shoot(page, iconHtml(180), 180, 180, path.join(LANDING, 'icon-180.png'));
74
+ await shoot(page, iconHtml(512), 512, 512, path.join(LANDING, 'icon-512.png'));
75
+ fs.copyFileSync(path.join(LANDING, 'icon-180.png'), path.join(PUBLIC, 'icon-180.png'));
76
+ console.log(' ✔', path.relative(ROOT, path.join(PUBLIC, 'icon-180.png')));
77
+ } finally {
78
+ await browser.close();
79
+ }
80
+ console.log('OG/아이콘 생성 완료.');
81
+ }
82
+
83
+ main().catch((err) => {
84
+ console.error('생성 실패:', err);
85
+ process.exitCode = 1;
86
+ });
package/scripts/init.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * 사용:
15
15
  * careermate init 감지되는 클라이언트에 등록 (기본: 로컬 실행 모드)
16
- * careermate init --client claude-code 특정 클라이언트만 (claude | claude-code | codex | cursor)
16
+ * careermate init --client claude-code 특정 클라이언트만 (claude | claude-code | codex | cursor | gemini | antigravity)
17
17
  * careermate init --all-clients 감지 여부와 무관하게 지원하는 모든 클라이언트에 등록
18
18
  * careermate init --npx MCP 실행을 npx 방식으로 등록(공개 배포/개발자용)
19
19
  * careermate init --print 실제로 쓰지 않고 등록할 설정/명령만 출력
@@ -41,6 +41,8 @@ interface ClientTarget {
41
41
  jsonType?: boolean;
42
42
  /** project scope처럼 항상 적용 가능한 대상은 감지 없이 기본 포함. */
43
43
  alwaysPresent?: boolean;
44
+ /** 설정 경로가 유동적인 대상(Antigravity)은 '실제 파일이 있을 때만' 감지해 추측 파일 생성을 막는다. */
45
+ requireFileForPresent?: boolean;
44
46
  }
45
47
 
46
48
  interface ServerEntry {
@@ -90,12 +92,37 @@ function codexConfigPath(): string {
90
92
  return path.join(base, 'config.toml');
91
93
  }
92
94
 
95
+ /** Gemini CLI: `~/.gemini/settings.json` 의 mcpServers(Cursor 등과 동일 JSON 형식). */
96
+ function geminiConfigPath(): string {
97
+ return path.join(os.homedir(), '.gemini', 'settings.json');
98
+ }
99
+
100
+ /**
101
+ * Antigravity(Gemini CLI의 소비자용 후속, 2026-06-18 전환) MCP 설정.
102
+ * 출시 직후라 정확한 파일 경로가 커뮤니티 소스 간 갈려, 존재하는 후보를 우선 사용한다.
103
+ * requireFileForPresent로 '실제 파일이 있을 때만' 감지 → 기본 init은 추측 경로에 파일을 만들지 않는다
104
+ * (--all-clients / --client antigravity 일 때만 첫 후보에 생성). 스키마는 Gemini와 동일(mcpServers).
105
+ */
106
+ function antigravityCandidates(): string[] {
107
+ const g = path.join(os.homedir(), '.gemini');
108
+ return [
109
+ path.join(g, 'config', 'mcp_config.json'),
110
+ path.join(g, 'antigravity', 'mcp_config.json'),
111
+ path.join(g, 'antigravity-cli', 'mcp_config.json'),
112
+ ];
113
+ }
114
+ function antigravityConfigPath(): string {
115
+ return antigravityCandidates().find((p) => fs.existsSync(p)) ?? antigravityCandidates()[0];
116
+ }
117
+
93
118
  function allTargets(): ClientTarget[] {
94
119
  return [
95
120
  { id: 'claude', label: 'Claude Desktop', format: 'mcp-json', configPath: claudeDesktopConfigPath() },
96
121
  { id: 'claude-code', label: 'Claude Code', format: 'mcp-json', configPath: claudeCodeConfigPath(), jsonType: true, alwaysPresent: true },
97
122
  { id: 'codex', label: 'Codex CLI', format: 'codex-toml', configPath: codexConfigPath() },
98
123
  { id: 'cursor', label: 'Cursor (기타 MCP 클라이언트)', format: 'mcp-json', configPath: cursorConfigPath() },
124
+ { id: 'gemini', label: 'Gemini CLI', format: 'mcp-json', configPath: geminiConfigPath() },
125
+ { id: 'antigravity', label: 'Antigravity', format: 'mcp-json', configPath: antigravityConfigPath(), requireFileForPresent: true },
99
126
  ];
100
127
  }
101
128
 
@@ -105,6 +132,7 @@ function allTargets(): ClientTarget[] {
105
132
  */
106
133
  function isPresent(t: ClientTarget): boolean {
107
134
  if (t.alwaysPresent) return true;
135
+ if (t.requireFileForPresent) return fs.existsSync(t.configPath);
108
136
  return fs.existsSync(t.configPath) || fs.existsSync(path.dirname(t.configPath));
109
137
  }
110
138
 
@@ -270,7 +298,7 @@ function printConfigs(server: ServerEntry): void {
270
298
  const cmd = [server.command, ...server.args];
271
299
  console.log('CareerMate를 AI 클라이언트에 직접 연결하는 방법입니다.\n');
272
300
 
273
- console.log('① Claude Desktop / Claude Code / Cursor — MCP 설정(mcpServers)에 추가:');
301
+ console.log('① Claude Desktop / Claude Code / Cursor / Gemini CLI / Antigravity — MCP 설정(mcpServers)에 추가:');
274
302
  console.log(
275
303
  JSON.stringify(
276
304
  { mcpServers: { [PKG]: { type: 'stdio', command: server.command, args: server.args } } },
@@ -279,6 +307,7 @@ function printConfigs(server: ServerEntry): void {
279
307
  ),
280
308
  );
281
309
  console.log('\n · Claude Code는 위 내용을 프로젝트 루트의 .mcp.json 으로 저장하면 됩니다.');
310
+ console.log(' · Gemini CLI는 ~/.gemini/settings.json, Antigravity는 ~/.gemini/(config|antigravity|antigravity-cli)/mcp_config.json 의 mcpServers에 같은 블록을 넣으세요(type 키는 없어도 됨).');
282
311
  console.log(` · 또는 명령으로: ${shellJoin(['claude', 'mcp', 'add', '--scope', 'project', '--transport', 'stdio', PKG, '--', ...cmd])}`);
283
312
 
284
313
  console.log('\n② Codex CLI — ~/.codex/config.toml 에 추가:');
@@ -324,7 +353,7 @@ function main(): void {
324
353
 
325
354
  if (connected === 0) {
326
355
  console.log('\n연결할 AI 클라이언트를 찾지 못했습니다.');
327
- console.log('특정 클라이언트를 지정하거나(--client claude|claude-code|codex|cursor),');
356
+ console.log('특정 클라이언트를 지정하거나(--client claude|claude-code|codex|cursor|gemini|antigravity),');
328
357
  console.log('설정을 직접 복사해 추가할 수 있어요: careermate init --print');
329
358
  process.exitCode = 1;
330
359
  return;
package/scripts/seed.ts CHANGED
@@ -23,8 +23,8 @@ function main(): void {
23
23
  }
24
24
 
25
25
  saveProfile({
26
- name: '이서연',
27
- email: 'seoyeon.lee@example.com',
26
+ name: '홍길동',
27
+ email: 'gildong.hong@example.com',
28
28
  phone: '010-1234-5678',
29
29
  location: '서울',
30
30
  headline: '5년 차 백엔드 엔지니어 · 결제/정산 도메인',
@@ -76,8 +76,8 @@ function main(): void {
76
76
  ]) skillRepo.add(s);
77
77
 
78
78
  addResume({
79
- kind: 'resume', title: '이서연 이력서 (백엔드)', is_primary: true, source: 'manual',
80
- content: `# 이서연 — 백엔드 엔지니어\n\n## 경력 요약\n결제·정산 도메인 5년. 대규모 트래픽 안정화와 데이터 정합성에 강점.\n\n## 핵심 성과\n- 결제 승인 API p99 320ms→110ms\n- 정산 배치 6시간→40분\n- 연간 거래액 1.2조 무중단 처리`,
79
+ kind: 'resume', title: '홍길동 이력서 (백엔드)', is_primary: true, source: 'manual',
80
+ content: `# 홍길동 — 백엔드 엔지니어\n\n## 경력 요약\n결제·정산 도메인 5년. 대규모 트래픽 안정화와 데이터 정합성에 강점.\n\n## 핵심 성과\n- 결제 승인 API p99 320ms→110ms\n- 정산 배치 6시간→40분\n- 연간 거래액 1.2조 무중단 처리`,
81
81
  });
82
82
 
83
83
  // A job with full analysis + cover letter + status + interview prep
@@ -115,7 +115,7 @@ function main(): void {
115
115
  star_guides: [
116
116
  { question: '가장 큰 기술적 성과', situation: '정산 배치가 6시간 소요되어 익일 지연', task: '처리 시간 단축', action: 'Kafka 이벤트 스트리밍으로 준실시간 파이프라인 재설계', result: '6시간→40분, 정산 지연 0건' },
117
117
  ],
118
- self_introduction: '안녕하세요. 결제·정산 도메인에서 5년간 "정확함"을 지켜온 백엔드 엔지니어 이서연입니다...(데모 1분 자기소개)',
118
+ self_introduction: '안녕하세요. 결제·정산 도메인에서 5년간 "정확함"을 지켜온 백엔드 엔지니어 홍길동입니다...(데모 1분 자기소개)',
119
119
  notes: '결제 도메인 질문에 집중 예상. 정량 수치 암기.',
120
120
  });
121
121