careermate 0.1.0 → 0.1.1
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 +10 -7
- package/apps/mcp/src/index.ts +3 -1
- package/apps/web/DESIGN_GUIDE.md +2 -2
- package/apps/web/public/icon-180.png +0 -0
- package/apps/web/public/index.html +4 -1
- package/apps/web/public/lib.js +1 -1
- package/apps/web/public/pages/applications.js +17 -1
- package/apps/web/public/pages/settings.js +6 -4
- package/apps/web/public/styles.css +49 -36
- package/apps/web/src/info.ts +9 -22
- package/apps/web/src/routes.ts +17 -13
- package/apps/web/src/server.ts +33 -25
- package/apps/web/src/settings.ts +35 -17
- package/bin/careermate.mjs +8 -5
- package/dist/{mcpb-stage/dist/web.mjs → doctor.mjs} +1122 -1450
- package/dist/install-page/favicon.svg +1 -0
- package/dist/install-page/icon-180.png +0 -0
- package/dist/install-page/icon-512.png +0 -0
- package/dist/install-page/index.html +309 -167
- package/dist/install-page/llms-install.txt +202 -0
- package/dist/install-page/og.png +0 -0
- package/dist/install-page/shots/applications-dark.png +0 -0
- package/dist/install-page/shots/applications-light.png +0 -0
- package/dist/install-page/shots/documents-dark.png +0 -0
- package/dist/install-page/shots/documents-light.png +0 -0
- package/dist/install-page/shots/home-dark.png +0 -0
- package/dist/install-page/shots/home-light.png +0 -0
- package/dist/install-page/shots/interview-dark.png +0 -0
- package/dist/install-page/shots/interview-light.png +0 -0
- package/dist/install-page/shots/jobs-dark.png +0 -0
- package/dist/install-page/shots/jobs-light.png +0 -0
- package/dist/install-page/shots/settings-dark.png +0 -0
- package/dist/install-page/shots/settings-light.png +0 -0
- package/dist/install-page/start.html +346 -0
- package/dist/install-page/style.css +94 -3
- package/dist/mcp.mjs +326 -68
- package/dist/migrate.mjs +4715 -0
- package/dist/public/icon-180.png +0 -0
- package/dist/public/index.html +4 -1
- package/dist/public/lib.js +1 -1
- package/dist/public/pages/applications.js +17 -1
- package/dist/public/pages/settings.js +6 -4
- package/dist/public/styles.css +49 -36
- package/dist/web.mjs +323 -129
- package/docs/DEMO.md +3 -3
- package/docs/MCP_TOOLS.md +21 -2
- package/docs/ROADMAP.md +3 -3
- package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
- package/docs/START_WORKFLOW.md +6 -0
- package/docs/SUPPORTED_AI_APPS.md +85 -33
- package/docs/WORKFLOWS.md +1 -1
- package/package.json +3 -2
- package/packages/core/src/services.ts +42 -1
- package/packages/db/src/connection.ts +30 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/repositories.ts +109 -63
- package/packages/db/src/schema.ts +118 -0
- package/packages/exporters/src/html.ts +9 -12
- package/packages/mcp-tools/src/tools.ts +81 -0
- package/packages/shared/src/enums.ts +39 -0
- package/scripts/build-dist.mjs +27 -8
- package/scripts/build-mcpb.mjs +60 -4
- package/scripts/gen-og.mjs +86 -0
- package/scripts/init.ts +32 -3
- package/scripts/seed.ts +5 -5
- package/scripts/shots-seed.ts +192 -0
- package/scripts/shots.ts +89 -0
- package/tsconfig.json +3 -1
- package/dist/careermate.mcpb +0 -0
- package/dist/mcp-smoke.err +0 -3
- package/dist/mcpb-stage/README.md +0 -219
- package/dist/mcpb-stage/dist/install-page/index.html +0 -434
- package/dist/mcpb-stage/dist/install-page/style.css +0 -407
- package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
- package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
- package/dist/mcpb-stage/dist/public/app.js +0 -118
- package/dist/mcpb-stage/dist/public/index.html +0 -41
- package/dist/mcpb-stage/dist/public/lib.js +0 -282
- package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
- package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
- package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
- package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
- package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
- package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
- package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
- package/dist/mcpb-stage/dist/public/styles.css +0 -420
- package/dist/mcpb-stage/manifest.json +0 -40
- package/docs/INSTALL.md +0 -148
- package/docs/INSTALL_AND_USAGE.md +0 -99
- package/install-page/index.html +0 -474
- package/install-page/style.css +0 -391
- package/install-page/vercel.json +0 -20
- /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
package/scripts/build-mcpb.mjs
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
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'
|
|
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
|
-
|
|
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();
|
|
@@ -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: '
|
|
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: '
|
|
80
|
-
content: `#
|
|
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년간 "정확함"을 지켜온 백엔드 엔지니어
|
|
118
|
+
self_introduction: '안녕하세요. 결제·정산 도메인에서 5년간 "정확함"을 지켜온 백엔드 엔지니어 홍길동입니다...(데모 1분 자기소개)',
|
|
119
119
|
notes: '결제 도메인 질문에 집중 예상. 정량 수치 암기.',
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shots-seed` — richer demo data for marketing screenshots ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `scripts/seed.ts` (one job, for a first-run feel), this fills the whole
|
|
5
|
+
* pipeline: jobs across every status, a few cover letters, fit analyses, and an
|
|
6
|
+
* interview prep — so the home dashboard, the applications board and the jobs
|
|
7
|
+
* list all look populated in the landing-page screenshots.
|
|
8
|
+
*
|
|
9
|
+
* NEVER run this against real data. Always isolate into a throwaway dir:
|
|
10
|
+
* CAREERMATE_DATA_DIR=./.shots-data npm run shots:seed
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
profileRepo, experienceRepo, projectRepo, skillRepo, getDataDir,
|
|
14
|
+
} from '@careermate/db';
|
|
15
|
+
import {
|
|
16
|
+
saveProfile, saveJobPosting, saveFitAnalysis, saveCoverLetterVersion,
|
|
17
|
+
updateApplicationStatus, saveInterviewPrep, addResume,
|
|
18
|
+
} from '@careermate/core';
|
|
19
|
+
|
|
20
|
+
if (profileRepo.get()?.name) {
|
|
21
|
+
console.error('⚠️ 데이터가 이미 있습니다. 마케팅 시드는 빈 throwaway 폴더에서만 실행하세요.');
|
|
22
|
+
console.error(' 예: CAREERMATE_DATA_DIR=./.shots-data npm run shots:seed');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
saveProfile({
|
|
27
|
+
name: '홍길동',
|
|
28
|
+
email: 'gildong.hong@example.com',
|
|
29
|
+
phone: '010-1234-5678',
|
|
30
|
+
location: '서울',
|
|
31
|
+
headline: '5년 차 백엔드 엔지니어 · 결제/정산 도메인',
|
|
32
|
+
summary: '대규모 트래픽 환경에서 결제·정산 시스템을 설계·운영했습니다. 안정성과 데이터 정합성을 중시하며, 문제를 정량적으로 정의하고 개선하는 것을 좋아합니다.',
|
|
33
|
+
desired_roles: ['백엔드 엔지니어', '서버 개발자', '플랫폼 엔지니어'],
|
|
34
|
+
desired_conditions: '서울/판교, 정규직, 원격 일부 가능, 결제/금융 도메인 선호',
|
|
35
|
+
preferred_tone: '담백하고 구체적이며, 성과를 숫자로 보여주는 문체',
|
|
36
|
+
emphasis_points: ['대규모 트래픽 처리 경험', '데이터 정합성', '장애 대응', '동료와의 협업'],
|
|
37
|
+
links: [
|
|
38
|
+
{ label: 'GitHub', url: 'https://github.com/example' },
|
|
39
|
+
{ label: '기술 블로그', url: 'https://blog.example.com' },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
experienceRepo.add({
|
|
44
|
+
company: '페이테크', role: '백엔드 엔지니어', employment_type: '정규직',
|
|
45
|
+
start_date: '2021-03', is_current: true,
|
|
46
|
+
description: '결제 게이트웨이와 정산 시스템 개발·운영',
|
|
47
|
+
achievements: ['결제 승인 API p99 응답시간 320ms→110ms 개선', '정산 배치 처리 시간 6시간→40분 단축', '장애 0건으로 연간 거래액 1.2조 처리'],
|
|
48
|
+
tech: ['Java', 'Spring Boot', 'Kafka', 'MySQL', 'Redis', 'Kubernetes'],
|
|
49
|
+
});
|
|
50
|
+
experienceRepo.add({
|
|
51
|
+
company: '커머스랩', role: '주니어 백엔드 개발자', employment_type: '정규직',
|
|
52
|
+
start_date: '2019-01', end_date: '2021-02',
|
|
53
|
+
description: '커머스 주문/재고 서비스 개발',
|
|
54
|
+
achievements: ['주문 동시성 이슈 해결로 재고 오차 99% 감소', '검색 응답속도 2배 개선'],
|
|
55
|
+
tech: ['Python', 'Django', 'PostgreSQL', 'Elasticsearch'],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
projectRepo.add({
|
|
59
|
+
name: '실시간 정산 파이프라인', role: '설계 및 리드',
|
|
60
|
+
description: 'Kafka 기반 이벤트 스트리밍으로 준실시간 정산 파이프라인 구축',
|
|
61
|
+
highlights: ['정산 지연 6시간→실시간', '재처리 가능한 멱등 설계'],
|
|
62
|
+
tech: ['Kafka', 'Spring', 'MySQL'], url: 'https://github.com/example/settlement',
|
|
63
|
+
});
|
|
64
|
+
projectRepo.add({
|
|
65
|
+
name: '결제 장애 자동 복구 시스템', role: '개발',
|
|
66
|
+
description: 'PG사 장애 시 자동 우회 라우팅 및 알림',
|
|
67
|
+
highlights: ['장애 평균 복구시간 15분→2분'], tech: ['Java', 'Spring', 'Redis'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
for (const s of [
|
|
71
|
+
{ name: 'Java', category: '언어', level: '상', years: 5 },
|
|
72
|
+
{ name: 'Spring Boot', category: '프레임워크', level: '상', years: 5 },
|
|
73
|
+
{ name: 'Kafka', category: '인프라', level: '중', years: 3 },
|
|
74
|
+
{ name: 'MySQL', category: '데이터베이스', level: '상', years: 5 },
|
|
75
|
+
{ name: 'Kubernetes', category: '인프라', level: '중', years: 2 },
|
|
76
|
+
{ name: 'Python', category: '언어', level: '중', years: 3 },
|
|
77
|
+
]) skillRepo.add(s);
|
|
78
|
+
|
|
79
|
+
addResume({
|
|
80
|
+
kind: 'resume', title: '홍길동 이력서 (백엔드)', is_primary: true, source: 'manual',
|
|
81
|
+
content: `# 홍길동 — 백엔드 엔지니어\n\n## 경력 요약\n결제·정산 도메인 5년. 대규모 트래픽 안정화와 데이터 정합성에 강점.\n\n## 핵심 성과\n- 결제 승인 API p99 320ms→110ms\n- 정산 배치 6시간→40분\n- 연간 거래액 1.2조 무중단 처리`,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** One demo job: posting → optional fit analysis → optional cover letter → status. */
|
|
85
|
+
function job(spec: {
|
|
86
|
+
company: string; position: string; location: string; deadline: string;
|
|
87
|
+
description: string; requirements: string[]; keywords: string[];
|
|
88
|
+
status: string; score?: number; fit?: { summary: string; strengths: string[]; gaps: string[]; matched: string[]; missing: string[] };
|
|
89
|
+
cover?: { title: string; content: string };
|
|
90
|
+
}) {
|
|
91
|
+
const { job: j } = saveJobPosting({
|
|
92
|
+
company: spec.company, position: spec.position, url: `https://example.com/${encodeURIComponent(spec.company)}`,
|
|
93
|
+
location: spec.location, employment_type: '정규직', description: spec.description,
|
|
94
|
+
requirements: spec.requirements, keywords: spec.keywords, deadline: spec.deadline, source: '데모',
|
|
95
|
+
});
|
|
96
|
+
if (spec.fit && spec.score != null) {
|
|
97
|
+
saveFitAnalysis({
|
|
98
|
+
job_id: j.id, score: spec.score, summary: spec.fit.summary,
|
|
99
|
+
strengths: spec.fit.strengths, gaps: spec.fit.gaps,
|
|
100
|
+
matched_keywords: spec.fit.matched, missing_keywords: spec.fit.missing,
|
|
101
|
+
recommendations: ['지원동기에 결제 도메인 경험을 구체 수치로', '부족 키워드는 학습 의지로 보완'],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
let coverLetterId: number | undefined;
|
|
105
|
+
if (spec.cover) {
|
|
106
|
+
const { coverLetter } = saveCoverLetterVersion({
|
|
107
|
+
job_id: j.id, title: spec.cover.title, content: spec.cover.content, note: '초안', source: 'ai',
|
|
108
|
+
});
|
|
109
|
+
coverLetterId = coverLetter.id;
|
|
110
|
+
}
|
|
111
|
+
if (spec.status !== 'draft') updateApplicationStatus(j.id, spec.status, `데모: ${spec.status}`);
|
|
112
|
+
return { jobId: j.id, coverLetterId };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 최종 합격
|
|
116
|
+
job({
|
|
117
|
+
company: '쿠팡', position: '백엔드 엔지니어 (정산플랫폼)', location: '서울 송파', deadline: '2026-05-30',
|
|
118
|
+
description: '대규모 정산 플랫폼의 안정성과 확장성을 책임질 백엔드 엔지니어를 찾습니다.',
|
|
119
|
+
requirements: ['Java/Kotlin 백엔드', '대규모 트래픽', '정산 도메인'], keywords: ['Java', 'Spring', 'Kafka', '정산', 'MSA'],
|
|
120
|
+
status: 'final_passed', score: 91,
|
|
121
|
+
fit: { summary: '정산 도메인 경험이 공고와 거의 완벽히 일치합니다.', strengths: ['정산 파이프라인 설계 경험', '대규모 트래픽 안정화'], gaps: ['Kotlin 실무 경험'], matched: ['Java', 'Spring', 'Kafka', '정산'], missing: ['Kotlin'] },
|
|
122
|
+
cover: { title: '쿠팡 정산플랫폼 자기소개서', content: '저는 "정산은 단 1원도 틀려선 안 된다"는 원칙으로 일해 왔습니다...(데모)' },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 면접
|
|
126
|
+
const naver = job({
|
|
127
|
+
company: '네이버', position: '서버 개발자 (페이)', location: '경기 성남', deadline: '2026-07-10',
|
|
128
|
+
description: '네이버페이 결제/정산 백엔드를 함께 만들 서버 개발자를 찾습니다.',
|
|
129
|
+
requirements: ['Java 백엔드', '결제 시스템 경험', '대용량 처리'], keywords: ['Java', 'Spring', '결제', '대용량'],
|
|
130
|
+
status: 'interview', score: 84,
|
|
131
|
+
fit: { summary: '결제 백엔드 경험과 강하게 일치하며 대용량 처리 성과가 돋보입니다.', strengths: ['결제 API 성능 개선(p99)', '대용량 트래픽 경험'], gaps: ['검색 도메인 경험은 제한적'], matched: ['Java', 'Spring', '결제'], missing: ['대용량 검색'] },
|
|
132
|
+
cover: { title: '네이버페이 자기소개서', content: '결제 시스템의 신뢰성을 숫자로 증명해 온 백엔드 엔지니어입니다...(데모)' },
|
|
133
|
+
});
|
|
134
|
+
saveInterviewPrep({
|
|
135
|
+
job_id: naver.jobId,
|
|
136
|
+
questions: [
|
|
137
|
+
{ question: '결제 승인 API 응답시간을 어떻게 개선했나요?', intent: '성능 개선 접근법', followups: ['병목은 어떻게 찾았나요?'], answer_outline: '프로파일링으로 커넥션 풀 병목 발견 → 캐시 도입 → p99 320→110ms.' },
|
|
138
|
+
{ question: '데이터 정합성이 깨질 뻔한 경험은?', intent: '장애 대응', followups: ['멱등성 보장은?'], answer_outline: '정산 재처리 중복 지급 위험 → 멱등키+분산락으로 해결.' },
|
|
139
|
+
],
|
|
140
|
+
star_guides: [
|
|
141
|
+
{ question: '가장 큰 기술적 성과', situation: '정산 배치 6시간 소요', task: '처리 시간 단축', action: 'Kafka 이벤트 스트리밍 재설계', result: '6시간→40분, 지연 0건' },
|
|
142
|
+
],
|
|
143
|
+
self_introduction: '안녕하세요. 결제·정산 도메인에서 5년간 "정확함"을 지켜온 백엔드 엔지니어 홍길동입니다...(데모 1분 자기소개)',
|
|
144
|
+
notes: '결제 도메인 질문 집중 예상. 정량 수치 암기.',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 서류 합격
|
|
148
|
+
const toss = job({
|
|
149
|
+
company: '토스', position: '백엔드 엔지니어 (결제)', location: '서울 강남', deadline: '2026-07-15',
|
|
150
|
+
description: '토스 결제 시스템의 안정성과 확장성을 책임질 백엔드 엔지니어를 찾습니다.',
|
|
151
|
+
requirements: ['Java/Kotlin 백엔드', '대규모 트래픽', '결제/정산 도메인 이해'], keywords: ['Java', 'Kotlin', 'Spring', 'Kafka', '결제', 'MSA'],
|
|
152
|
+
status: 'document_passed', score: 88,
|
|
153
|
+
fit: { summary: '결제·정산 경험과 대규모 트래픽 이력이 요구사항과 강하게 일치합니다.', strengths: ['결제/정산 5년', 'p99 개선 성과', 'Kafka 이벤트 처리'], gaps: ['Kotlin 실무 경험 부족', 'MSA 리딩 제한적'], matched: ['Java', 'Spring', 'Kafka', '결제'], missing: ['Kotlin', 'MSA'] },
|
|
154
|
+
cover: { title: '토스 백엔드 자기소개서', content: '저는 결제 시스템의 "한 푼도 틀리면 안 된다"는 긴장감 속에서 성장했습니다...(데모)' },
|
|
155
|
+
});
|
|
156
|
+
saveCoverLetterVersion({
|
|
157
|
+
cover_letter_id: toss.coverLetterId,
|
|
158
|
+
content: '저는 결제 시스템의 "한 푼도 틀리면 안 된다"는 긴장감 속에서 성장했습니다. 페이테크에서 결제 승인 API의 p99 응답시간을 320ms에서 110ms로 줄이며...(데모 v2)',
|
|
159
|
+
note: '지원동기와 정량 성과 보강', source: 'edit',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 지원 완료
|
|
163
|
+
job({
|
|
164
|
+
company: '카카오페이', position: '백엔드 엔지니어 (정산)', location: '경기 판교', deadline: '2026-07-20',
|
|
165
|
+
description: '카카오페이 정산 시스템 백엔드 개발자를 찾습니다.',
|
|
166
|
+
requirements: ['Java/Kotlin', 'MSA', '정산/회계 이해'], keywords: ['Java', 'Kotlin', 'MSA', '정산'],
|
|
167
|
+
status: 'applied', score: 79,
|
|
168
|
+
fit: { summary: '정산 경험은 강하나 MSA 전환 리딩 경험 보강이 필요합니다.', strengths: ['정산 도메인 경험', '데이터 정합성'], gaps: ['MSA 리딩', 'Kotlin'], matched: ['Java', '정산'], missing: ['Kotlin', 'MSA'] },
|
|
169
|
+
cover: { title: '카카오페이 자기소개서', content: '정산의 정확성과 속도를 동시에 끌어올린 경험을 가지고 있습니다...(데모)' },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// 지원 예정
|
|
173
|
+
job({
|
|
174
|
+
company: '당근', position: '서버 개발자 (커머스)', location: '서울 강남', deadline: '2026-08-01',
|
|
175
|
+
description: '당근 중고거래/커머스 백엔드 서버 개발자를 찾습니다.',
|
|
176
|
+
requirements: ['Java/Kotlin 또는 Go', '대용량 트래픽', '커머스 도메인'], keywords: ['Java', 'Go', '대용량', '커머스'],
|
|
177
|
+
status: 'planned', score: 73,
|
|
178
|
+
fit: { summary: '대용량 처리 경험은 적합하나 커머스 도메인 노출은 초기 경력에 한정됩니다.', strengths: ['대용량 트래픽', '주문/재고 경험'], gaps: ['최근 커머스 경험', 'Go'], matched: ['Java', '대용량'], missing: ['Go', '커머스'] },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 보류
|
|
182
|
+
job({
|
|
183
|
+
company: '라인', position: '백엔드 엔지니어 (Fintech)', location: '서울 영등포', deadline: '2026-08-10',
|
|
184
|
+
description: 'LINE Fintech 백엔드 엔지니어를 찾습니다.',
|
|
185
|
+
requirements: ['Java/Kotlin', '글로벌 서비스 경험', '금융 도메인'], keywords: ['Java', 'Kotlin', '금융', '글로벌'],
|
|
186
|
+
status: 'on_hold', score: 70,
|
|
187
|
+
fit: { summary: '금융 도메인은 적합하나 글로벌 서비스 경험이 부족합니다.', strengths: ['금융/결제 도메인'], gaps: ['글로벌 서비스', 'Kotlin'], matched: ['Java', '금융'], missing: ['글로벌', 'Kotlin'] },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
console.log('✅ 마케팅용 데모 데이터를 생성했습니다.');
|
|
191
|
+
console.log(` 위치: ${getDataDir()}`);
|
|
192
|
+
console.log(' 공고 6건(최종합격·면접·서류합격·지원·예정·보류) · 적합도 6 · 자기소개서 4 · 면접 준비 1');
|
package/scripts/shots.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shots` — capture real dashboard screenshots for the landing page.
|
|
3
|
+
*
|
|
4
|
+
* Starts the local web server against whatever data dir is configured, then uses
|
|
5
|
+
* Playwright to capture home / applications / jobs in BOTH light and dark themes
|
|
6
|
+
* into install-page/shots/{screen}-{theme}.png. The landing page embeds these and
|
|
7
|
+
* swaps light↔dark to match its own theme toggle.
|
|
8
|
+
*
|
|
9
|
+
* Run against the throwaway marketing data (see shots-seed.ts):
|
|
10
|
+
* CAREERMATE_DATA_DIR=./.shots-data CAREERMATE_NO_OPEN=1 npm run shots
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { chromium } from 'playwright';
|
|
16
|
+
import { startServer } from '../apps/web/src/server.ts';
|
|
17
|
+
import { getDataDir } from '@careermate/db';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const OUT_DIR = path.resolve(__dirname, '../install-page/shots');
|
|
21
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
22
|
+
|
|
23
|
+
const SCREENS = [
|
|
24
|
+
{ name: 'home', hash: '#/home' },
|
|
25
|
+
{ name: 'applications', hash: '#/applications' },
|
|
26
|
+
{ name: 'jobs', hash: '#/jobs' },
|
|
27
|
+
{ name: 'documents', hash: '#/documents' },
|
|
28
|
+
{ name: 'interview', hash: '#/interview' },
|
|
29
|
+
{ name: 'settings', hash: '#/settings' },
|
|
30
|
+
];
|
|
31
|
+
const THEMES = ['light', 'dark'] as const;
|
|
32
|
+
|
|
33
|
+
// A desktop viewport that fits the home/board content without scrolling; @2x for
|
|
34
|
+
// crisp text on retina/landing-card sizes.
|
|
35
|
+
const VIEWPORT = { width: 1360, height: 940 };
|
|
36
|
+
|
|
37
|
+
async function main(): Promise<void> {
|
|
38
|
+
process.env.CAREERMATE_NO_OPEN = '1';
|
|
39
|
+
const { url } = await startServer(Number(process.env.CAREERMATE_PORT ?? 4337));
|
|
40
|
+
const expectedDir = getDataDir();
|
|
41
|
+
console.log(`서버 시작: ${url} (data: ${expectedDir})`);
|
|
42
|
+
|
|
43
|
+
// Safety: never screenshot real career data. Confirm the server we'll capture is
|
|
44
|
+
// actually serving our throwaway demo dir — not another careermate instance that
|
|
45
|
+
// grabbed the URL (e.g. a real dashboard on the default port).
|
|
46
|
+
const health = await fetch(`${url}/api/health`).then((r) => r.json()).catch(() => null) as { data_dir?: string } | null;
|
|
47
|
+
if (!health || path.resolve(health.data_dir ?? '') !== path.resolve(expectedDir)) {
|
|
48
|
+
throw new Error(`연결된 서버의 데이터 폴더(${health?.data_dir ?? '?'})가 예상(${expectedDir})과 다릅니다. `
|
|
49
|
+
+ '실제 데이터를 캡처할 위험이 있어 중단합니다. CAREERMATE_DATA_DIR을 빈 throwaway 폴더로 지정하세요.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const browser = await chromium.launch();
|
|
53
|
+
try {
|
|
54
|
+
for (const theme of THEMES) {
|
|
55
|
+
const ctx = await browser.newContext({ viewport: VIEWPORT, deviceScaleFactor: 2 });
|
|
56
|
+
// The dashboard reads localStorage 'cf-theme' on boot (app.js initTheme).
|
|
57
|
+
await ctx.addInitScript((t) => {
|
|
58
|
+
try { window.localStorage.setItem('cf-theme', t as string); } catch { /* ignore */ }
|
|
59
|
+
}, theme);
|
|
60
|
+
const page = await ctx.newPage();
|
|
61
|
+
|
|
62
|
+
for (const s of SCREENS) {
|
|
63
|
+
await page.goto(`${url}/${s.hash}`, { waitUntil: 'networkidle' });
|
|
64
|
+
// wait for the route to render (spinner gone) and webfont to load
|
|
65
|
+
await page.waitForFunction(() => {
|
|
66
|
+
const v = document.getElementById('view');
|
|
67
|
+
const hasSpinner = !!document.querySelector('.spinner');
|
|
68
|
+
const fontsReady = !document.fonts || document.fonts.status === 'loaded';
|
|
69
|
+
return !!v && v.childElementCount > 0 && !hasSpinner && fontsReady;
|
|
70
|
+
}, { timeout: 10_000 }).catch(() => { /* best effort */ });
|
|
71
|
+
await page.waitForTimeout(450);
|
|
72
|
+
|
|
73
|
+
const file = path.join(OUT_DIR, `${s.name}-${theme}.png`);
|
|
74
|
+
await page.screenshot({ path: file });
|
|
75
|
+
console.log(` ✓ ${path.relative(process.cwd(), file)}`);
|
|
76
|
+
}
|
|
77
|
+
await ctx.close();
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
await browser.close();
|
|
81
|
+
}
|
|
82
|
+
console.log('완료. install-page/shots/*.png 생성됨.');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
console.error('스크린샷 캡처 실패:', err instanceof Error ? err.stack : err);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -25,5 +25,7 @@
|
|
|
25
25
|
"@careermate/workflows": ["packages/workflows/src/index.ts"]
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
-
"include": ["packages/**/*.ts", "apps/**/*.ts", "scripts/**/*.ts"]
|
|
28
|
+
"include": ["packages/**/*.ts", "apps/**/*.ts", "scripts/**/*.ts"],
|
|
29
|
+
"//exclude": "scripts/shots*.ts는 Playwright 브라우저 컨텍스트(window/document)·임시 시드용 마케팅 도구라 타입검사 제외(배포물 아님).",
|
|
30
|
+
"exclude": ["node_modules", "scripts/shots.ts", "scripts/shots-seed.ts"]
|
|
29
31
|
}
|
package/dist/careermate.mcpb
DELETED
|
Binary file
|
package/dist/mcp-smoke.err
DELETED