careermate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/apps/mcp/src/index.ts +66 -0
- package/apps/web/DESIGN_GUIDE.md +105 -0
- package/apps/web/UI_CONTRACT.md +44 -0
- package/apps/web/public/app.js +118 -0
- package/apps/web/public/fonts/PretendardVariable.woff2 +0 -0
- package/apps/web/public/index.html +41 -0
- package/apps/web/public/lib.js +282 -0
- package/apps/web/public/pages/applications.js +98 -0
- package/apps/web/public/pages/documents.js +446 -0
- package/apps/web/public/pages/home.js +263 -0
- package/apps/web/public/pages/interview.js +230 -0
- package/apps/web/public/pages/jobs.js +494 -0
- package/apps/web/public/pages/profile.js +576 -0
- package/apps/web/public/pages/settings.js +233 -0
- package/apps/web/public/styles.css +426 -0
- package/apps/web/src/exports.ts +68 -0
- package/apps/web/src/http.ts +180 -0
- package/apps/web/src/index.ts +49 -0
- package/apps/web/src/info.ts +50 -0
- package/apps/web/src/routes.ts +350 -0
- package/apps/web/src/security.ts +102 -0
- package/apps/web/src/server.ts +141 -0
- package/apps/web/src/settings.ts +88 -0
- package/bin/careermate.mjs +74 -0
- package/dist/careermate.mcpb +0 -0
- package/dist/install-page/index.html +474 -0
- package/dist/install-page/style.css +391 -0
- package/dist/install-page/vercel.json +20 -0
- package/dist/mcp-smoke.err +3 -0
- package/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/README.md +219 -0
- package/dist/mcpb-stage/dist/install-page/index.html +434 -0
- package/dist/mcpb-stage/dist/install-page/style.css +407 -0
- package/dist/mcpb-stage/dist/install-page/vercel.json +20 -0
- package/dist/mcpb-stage/dist/mcp.mjs +23704 -0
- package/dist/mcpb-stage/dist/public/app.js +118 -0
- package/dist/mcpb-stage/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/mcpb-stage/dist/public/index.html +41 -0
- package/dist/mcpb-stage/dist/public/lib.js +282 -0
- package/dist/mcpb-stage/dist/public/pages/applications.js +98 -0
- package/dist/mcpb-stage/dist/public/pages/documents.js +446 -0
- package/dist/mcpb-stage/dist/public/pages/home.js +263 -0
- package/dist/mcpb-stage/dist/public/pages/interview.js +230 -0
- package/dist/mcpb-stage/dist/public/pages/jobs.js +494 -0
- package/dist/mcpb-stage/dist/public/pages/profile.js +576 -0
- package/dist/mcpb-stage/dist/public/pages/settings.js +233 -0
- package/dist/mcpb-stage/dist/public/styles.css +420 -0
- package/dist/mcpb-stage/dist/web.mjs +7240 -0
- package/dist/mcpb-stage/manifest.json +40 -0
- package/dist/public/app.js +118 -0
- package/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/public/index.html +41 -0
- package/dist/public/lib.js +282 -0
- package/dist/public/pages/applications.js +98 -0
- package/dist/public/pages/documents.js +446 -0
- package/dist/public/pages/home.js +263 -0
- package/dist/public/pages/interview.js +230 -0
- package/dist/public/pages/jobs.js +494 -0
- package/dist/public/pages/profile.js +576 -0
- package/dist/public/pages/settings.js +233 -0
- package/dist/public/styles.css +426 -0
- package/dist/web.mjs +7240 -0
- package/docs/ARCHITECTURE.md +208 -0
- package/docs/CHANGES_V1.md +103 -0
- package/docs/DATA_MODEL.md +460 -0
- package/docs/DECISIONS.md +277 -0
- package/docs/DEMO.md +242 -0
- package/docs/INSTALL.md +148 -0
- package/docs/INSTALL_AND_USAGE.md +99 -0
- package/docs/MCP_TOOLS.md +233 -0
- package/docs/ROADMAP.md +134 -0
- package/docs/START_WORKFLOW.md +125 -0
- package/docs/SUPPORTED_AI_APPS.md +60 -0
- package/docs/TODO.md +57 -0
- package/docs/UX_NOTES.md +247 -0
- package/docs/WORKFLOWS.md +200 -0
- package/install-page/index.html +474 -0
- package/install-page/style.css +391 -0
- package/install-page/vercel.json +20 -0
- package/package.json +68 -0
- package/packages/core/src/context.ts +74 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/onboarding.ts +81 -0
- package/packages/core/src/services.ts +146 -0
- package/packages/core/src/summary.ts +104 -0
- package/packages/db/src/connection.ts +46 -0
- package/packages/db/src/index.ts +22 -0
- package/packages/db/src/paths.ts +41 -0
- package/packages/db/src/repositories.ts +828 -0
- package/packages/db/src/runtime.ts +58 -0
- package/packages/db/src/schema.ts +189 -0
- package/packages/exporters/src/html.ts +113 -0
- package/packages/exporters/src/index.ts +364 -0
- package/packages/exporters/src/markdown.ts +178 -0
- package/packages/mcp-tools/src/bridge.ts +83 -0
- package/packages/mcp-tools/src/index.ts +8 -0
- package/packages/mcp-tools/src/result.ts +49 -0
- package/packages/mcp-tools/src/tools.ts +455 -0
- package/packages/parsers/src/html.ts +86 -0
- package/packages/parsers/src/index.ts +228 -0
- package/packages/parsers/src/keywords.ts +151 -0
- package/packages/prompts/src/humanize.ts +59 -0
- package/packages/prompts/src/index.ts +82 -0
- package/packages/prompts/src/install.ts +43 -0
- package/packages/prompts/src/onboarding.ts +35 -0
- package/packages/prompts/src/system.ts +53 -0
- package/packages/shared/src/enums.ts +103 -0
- package/packages/shared/src/index.ts +18 -0
- package/packages/shared/src/schemas.ts +398 -0
- package/packages/workflows/src/definitions.ts +107 -0
- package/packages/workflows/src/index.ts +39 -0
- package/scripts/build-dist.mjs +62 -0
- package/scripts/build-mcpb.mjs +70 -0
- package/scripts/doctor.ts +81 -0
- package/scripts/init.ts +342 -0
- package/scripts/mcp-probe.ts +55 -0
- package/scripts/migrate.ts +6 -0
- package/scripts/run.mjs +33 -0
- package/scripts/seed.ts +129 -0
- package/scripts/test.ts +117 -0
- package/scripts/ui-smoke.ts +73 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* careermate — 공개 배포용 실행 진입점 (npm bin).
|
|
4
|
+
*
|
|
5
|
+
* 이 파일은 plain JS라서 Node가 바로 실행할 수 있고, 내부에서 tsx 로더를 얹어
|
|
6
|
+
* 무빌드(TS 직접 실행) 방식 그대로 각 진입점을 띄운다. `npm run mcp` 등과 동일한
|
|
7
|
+
* `node --no-warnings --import tsx <entry>` 호출을 재현하되, cwd를 패키지 루트로
|
|
8
|
+
* 고정해 tsx/tsconfig(paths 별칭) 해석이 어디서 실행되든 깨지지 않게 한다.
|
|
9
|
+
*
|
|
10
|
+
* careermate init AI 클라이언트(MCP 설정)에 CareerMate를 자동 등록
|
|
11
|
+
* careermate mcp MCP stdio 서버 실행 (AI 클라이언트가 호출)
|
|
12
|
+
* careermate start 로컬 대시보드 실행
|
|
13
|
+
* careermate doctor 설치/환경 점검
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
|
|
22
|
+
const ENTRIES = {
|
|
23
|
+
init: 'scripts/init.ts',
|
|
24
|
+
mcp: 'apps/mcp/src/index.ts',
|
|
25
|
+
start: 'apps/web/src/index.ts',
|
|
26
|
+
dashboard: 'apps/web/src/index.ts',
|
|
27
|
+
doctor: 'scripts/doctor.ts',
|
|
28
|
+
migrate: 'scripts/migrate.ts',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 번들 빌드가 있으면 tsx 없이 플레인 node로 실행(배포 경로). 없으면 소스를 tsx로 실행(개발).
|
|
32
|
+
const BUNDLES = {
|
|
33
|
+
mcp: 'dist/mcp.mjs',
|
|
34
|
+
start: 'dist/web.mjs',
|
|
35
|
+
dashboard: 'dist/web.mjs',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const [cmd = 'help', ...rest] = process.argv.slice(2);
|
|
39
|
+
|
|
40
|
+
if (cmd === 'help' || cmd === '--help' || cmd === '-h' || !ENTRIES[cmd]) {
|
|
41
|
+
const lines = [
|
|
42
|
+
'CareerMate — 내 AI를 커리어 비서로 (로컬 MCP 도구)',
|
|
43
|
+
'',
|
|
44
|
+
'사용법: careermate <명령>',
|
|
45
|
+
'',
|
|
46
|
+
' init AI 클라이언트에 CareerMate를 자동 연결(MCP 설정 등록)',
|
|
47
|
+
' mcp MCP 서버 실행 (보통 AI 클라이언트가 자동 호출)',
|
|
48
|
+
' start 로컬 대시보드 실행 (http://127.0.0.1:4319)',
|
|
49
|
+
' doctor 설치/환경 점검',
|
|
50
|
+
'',
|
|
51
|
+
'처음이라면: careermate init → AI 클라이언트 재시작',
|
|
52
|
+
];
|
|
53
|
+
// 알 수 없는 명령이면 안내를 stderr로 보내 stdout(MCP 전송 채널)을 오염시키지 않는다.
|
|
54
|
+
const out = ENTRIES[cmd] ? console.log : console.error;
|
|
55
|
+
out(lines.join('\n'));
|
|
56
|
+
process.exit(cmd === 'help' || cmd === '--help' || cmd === '-h' ? 0 : 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const bundle = BUNDLES[cmd] ? path.join(ROOT, BUNDLES[cmd]) : null;
|
|
60
|
+
const runArgs =
|
|
61
|
+
bundle && fs.existsSync(bundle)
|
|
62
|
+
? ['--no-warnings', '--experimental-sqlite', bundle, ...rest]
|
|
63
|
+
: ['--no-warnings', '--experimental-sqlite', '--import', 'tsx', path.join(ROOT, ENTRIES[cmd]), ...rest];
|
|
64
|
+
|
|
65
|
+
const child = spawn(process.execPath, runArgs, { cwd: ROOT, stdio: 'inherit' });
|
|
66
|
+
|
|
67
|
+
child.on('exit', (code, signal) => {
|
|
68
|
+
if (signal) process.kill(process.pid, signal);
|
|
69
|
+
else process.exit(code ?? 0);
|
|
70
|
+
});
|
|
71
|
+
child.on('error', (err) => {
|
|
72
|
+
console.error('[careermate] 실행 실패:', err instanceof Error ? err.message : err);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko" data-theme="">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>CareerMate — 내 AI를 커리어 비서로</title>
|
|
7
|
+
<meta name="description" content="Claude Desktop·Claude Code·Codex 같은 로컬 AI 앱에 연결하세요. CareerMate가 이력서·자기소개서·지원 현황을 이 컴퓨터에만 저장하고 MCP로 당신의 AI에 연결합니다." />
|
|
8
|
+
<link rel="stylesheet" href="style.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<!-- ============================================================ nav -->
|
|
12
|
+
<nav class="nav">
|
|
13
|
+
<a class="nav__brand" href="#top">
|
|
14
|
+
<span class="nav__logo" aria-hidden="true">C</span>
|
|
15
|
+
<span class="nav__name">CareerMate</span>
|
|
16
|
+
</a>
|
|
17
|
+
<div class="nav__links">
|
|
18
|
+
<a href="#how">동작 방식</a>
|
|
19
|
+
<a href="#features">기능</a>
|
|
20
|
+
<a href="#install">설치</a>
|
|
21
|
+
<a href="#privacy">개인정보</a>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="nav__actions">
|
|
24
|
+
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="다크 모드 전환" title="다크 모드 전환">
|
|
25
|
+
<svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
|
26
|
+
<svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
|
|
27
|
+
</button>
|
|
28
|
+
<a class="btn btn--ghost" href="#install">설치하기</a>
|
|
29
|
+
</div>
|
|
30
|
+
</nav>
|
|
31
|
+
|
|
32
|
+
<main id="top">
|
|
33
|
+
|
|
34
|
+
<!-- ========================================================== hero -->
|
|
35
|
+
<header class="hero">
|
|
36
|
+
<span class="pill-badge">
|
|
37
|
+
<span class="pill-badge__dot" aria-hidden="true"></span>
|
|
38
|
+
로컬 전용 · 오픈소스 · Claude Desktop 원클릭
|
|
39
|
+
</span>
|
|
40
|
+
<h1 class="hero__title">
|
|
41
|
+
내가 쓰던 <span class="accent">AI</span>를<br />
|
|
42
|
+
나만의 <span class="accent">커리어 비서</span>로
|
|
43
|
+
</h1>
|
|
44
|
+
<p class="hero__lede">
|
|
45
|
+
이력서·자기소개서·지원 현황을 <strong>이 컴퓨터 안에서</strong> 관리하세요.<br />
|
|
46
|
+
분석과 글쓰기는 평소 쓰던 AI가, 데이터 보관은 CareerMate가 맡습니다.
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<div class="hero__cta">
|
|
50
|
+
<a class="btn btn--primary" href="#install">
|
|
51
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
|
52
|
+
설치 방법 보기
|
|
53
|
+
</a>
|
|
54
|
+
<a class="btn btn--ghost" href="#install-other">기타 MCP 클라이언트</a>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<p class="hero__note">
|
|
58
|
+
CareerMate는 <strong>내 컴퓨터에서 도는 로컬 도구</strong>라, 내 컴퓨터의 프로그램을 실행할 수 있는 AI에서 동작합니다 — <strong>Claude Desktop · Claude Code · Codex</strong>.
|
|
59
|
+
설치를 마쳤다면 대시보드 <code>http://127.0.0.1:4319</code>에서 내 데이터를 확인하세요.
|
|
60
|
+
</p>
|
|
61
|
+
</header>
|
|
62
|
+
|
|
63
|
+
<!-- ========================================================== how -->
|
|
64
|
+
<section id="how" class="section">
|
|
65
|
+
<div class="section__head">
|
|
66
|
+
<span class="eyebrow">동작 방식</span>
|
|
67
|
+
<h2>당신의 AI가 <span class="accent">두뇌</span>,<br />CareerMate는 <span class="accent">커리어 서랍장</span></h2>
|
|
68
|
+
<p>CareerMate 안에는 AI가 들어 있지 않습니다.<br />생각하는 일은 당신의 AI가, 데이터 보관은 CareerMate가 맡습니다.</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="cards-3">
|
|
71
|
+
<article class="feature">
|
|
72
|
+
<div class="feature__icon">
|
|
73
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5a3 3 0 1 0-5.99.13 3 3 0 0 0-1.5 5.5A3 3 0 0 0 6 16.5a3 3 0 0 0 6 .5V5z"/><path d="M12 5a3 3 0 1 1 5.99.13 3 3 0 0 1 1.5 5.5A3 3 0 0 1 18 16.5a3 3 0 0 1-6 .5V5z"/></svg>
|
|
74
|
+
</div>
|
|
75
|
+
<h3>1. 당신의 AI와 대화</h3>
|
|
76
|
+
<p>평소 쓰던 Claude에게 말로 시키면 됩니다.<br />분석·글쓰기 같은 “생각”은 전부 당신의 AI가 합니다.</p>
|
|
77
|
+
</article>
|
|
78
|
+
<article class="feature">
|
|
79
|
+
<div class="feature__icon">
|
|
80
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 12h6"/><path d="M9 12a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3"/><path d="M15 12a3 3 0 0 0 3 3v2a3 3 0 0 1-3 3"/></svg>
|
|
81
|
+
</div>
|
|
82
|
+
<h3>2. MCP로 연결</h3>
|
|
83
|
+
<p>표준 프로토콜 MCP가 AI와 CareerMate를 잇는 “USB-C” 역할을 합니다.<br />AI가 24개 도구로 당신의 커리어 DB를 읽고 씁니다.</p>
|
|
84
|
+
</article>
|
|
85
|
+
<article class="feature">
|
|
86
|
+
<div class="feature__icon">
|
|
87
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/><path d="M7 7h.01M7 17h.01"/></svg>
|
|
88
|
+
</div>
|
|
89
|
+
<h3>3. 이 컴퓨터에 저장</h3>
|
|
90
|
+
<p>모든 데이터는 로컬 SQLite(<code>~/.careermate</code>)에만 저장됩니다.<br />외부 서버로 전송하지 않습니다. 대시보드로 눈으로 확인하세요.</p>
|
|
91
|
+
</article>
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
<!-- ====================================================== features -->
|
|
96
|
+
<section id="features" class="section">
|
|
97
|
+
<div class="section__head">
|
|
98
|
+
<span class="eyebrow">할 수 있는 일</span>
|
|
99
|
+
<h2>지원 한 건을 <span class="accent">끝까지</span> 함께 처리</h2>
|
|
100
|
+
<p>프로필 정리부터 면접 준비까지, AI와 대화만으로 이어집니다.</p>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="checks">
|
|
103
|
+
<div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><div><strong>프로필·이력서 구조화</strong><span>업로드한 파일을 읽어 경력·프로젝트·스킬로 정리·보관.</span></div></div>
|
|
104
|
+
<div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><div><strong>채용공고 파싱·저장</strong><span>붙여넣은 공고 텍스트에서 회사·직무·자격요건을 추출.</span></div></div>
|
|
105
|
+
<div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><div><strong>적합도(핏) 분석</strong><span>내 경력과 공고를 비교해 강점·보완점·매칭 키워드 도출.</span></div></div>
|
|
106
|
+
<div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><div><strong>맞춤 자소서 + 버전 관리</strong><span>공고별 자기소개서를 버전으로 쌓고 비교, 파일로 내보내기.</span></div></div>
|
|
107
|
+
<div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><div><strong>지원 상태 8단계 관리</strong><span>작성 중 → 지원 → 서류 합격 → 면접 → 최종까지 칸반으로.</span></div></div>
|
|
108
|
+
<div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><div><strong>면접 준비</strong><span>서류 합격 시 예상 질문·STAR 답변·1분 자기소개 초안 생성.</span></div></div>
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
|
|
112
|
+
<!-- ======================================================= install -->
|
|
113
|
+
<section id="install" class="section">
|
|
114
|
+
<div class="section__head">
|
|
115
|
+
<span class="eyebrow">설치</span>
|
|
116
|
+
<h2>세 가지 AI 앱에 연결</h2>
|
|
117
|
+
<p>지원하는 로컬 AI 앱은 <strong>Claude Desktop · Claude Code · Codex</strong> 셋입니다. 어느 쪽이든 같은 코어(로컬 DB + 도구 24개)를 씁니다.</p>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="note">
|
|
121
|
+
<strong>어떤 AI에서 되나요?</strong> CareerMate는 데이터를 내 컴퓨터에만 두는 <strong>로컬 MCP 도구</strong>입니다.
|
|
122
|
+
그래서 <strong>내 컴퓨터에서 실행되며 로컬 프로그램을 띄울 수 있는 AI</strong>에서 동작합니다 — Claude Desktop · Claude Code · Codex(그 외 Cursor·Cline·Windsurf 등도 가능).
|
|
123
|
+
ChatGPT·Gemini·Claude의 <strong>웹/모바일 앱</strong>은 클라우드에서 돌아 내 컴퓨터의 로컬 서버에 직접 연결할 수 없습니다(원격 URL 기반 MCP만 지원).
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- track 1 · Claude Desktop -->
|
|
127
|
+
<div class="track">
|
|
128
|
+
<div class="track__head">
|
|
129
|
+
<span class="track__tag">1 · Claude Desktop</span>
|
|
130
|
+
<h3>원클릭 <code>.mcpb</code> 설치</h3>
|
|
131
|
+
<p>터미널도, Node 설치도 필요 없습니다. 파일 하나를 더블클릭하면 끝납니다.</p>
|
|
132
|
+
</div>
|
|
133
|
+
<ol class="steps">
|
|
134
|
+
<li class="step">
|
|
135
|
+
<div class="step__no">1</div>
|
|
136
|
+
<div class="step__body">
|
|
137
|
+
<h3>설치 파일 받기</h3>
|
|
138
|
+
<p><code>careermate.mcpb</code> 파일이 필요합니다. <strong>(Release <code>v0.1.0</code>에 있으나 저장소가 비공개라 공개 다운로드 링크는 아직 없습니다.)</strong></p>
|
|
139
|
+
<!-- TODO: 저장소를 public으로 전환하면 https://github.com/osntak/CareerMate/releases/latest 의 .mcpb로 이 버튼을 활성화. 현재는 private repo라 공개 다운로드 불가. -->
|
|
140
|
+
<button class="btn btn--primary" type="button" disabled aria-disabled="true">
|
|
141
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
|
142
|
+
careermate.mcpb 다운로드 (비공개 저장소)
|
|
143
|
+
</button>
|
|
144
|
+
<p class="step__tip">접근 권한이 있으면 Release <code>v0.1.0</code>에서 받고, 없으면 터미널에서 <code>npx -y careermate init</code>로 설치하거나 소스에서 <code>npm run build:mcpb</code>로 빌드하세요.</p>
|
|
145
|
+
</div>
|
|
146
|
+
</li>
|
|
147
|
+
<li class="step">
|
|
148
|
+
<div class="step__no">2</div>
|
|
149
|
+
<div class="step__body">
|
|
150
|
+
<h3>더블클릭으로 설치</h3>
|
|
151
|
+
<p>내려받은 <code>careermate.mcpb</code>를 더블클릭하면 Claude Desktop이 설치 창을 엽니다. <strong>설치</strong>를 누르세요.
|
|
152
|
+
(또는 Claude Desktop → <strong>Settings → Extensions</strong>에 파일을 끌어다 놓아도 됩니다.)</p>
|
|
153
|
+
</div>
|
|
154
|
+
</li>
|
|
155
|
+
<li class="step">
|
|
156
|
+
<div class="step__no">3</div>
|
|
157
|
+
<div class="step__body">
|
|
158
|
+
<h3>재시작하고 확인</h3>
|
|
159
|
+
<p>Claude Desktop을 완전히 종료했다 다시 켜세요. 그다음 Claude에게
|
|
160
|
+
<strong>“<code>get_onboarding_status</code> 호출해서 연결됐는지 확인해줘”</strong>라고 말해 결과가 돌아오면 정상입니다.</p>
|
|
161
|
+
</div>
|
|
162
|
+
</li>
|
|
163
|
+
</ol>
|
|
164
|
+
<p class="step__tip">Claude Desktop이 Node를 내장하므로 따로 설치할 게 없습니다. 드물게 내장 Node가 22.5 미만이라 설치가 막히면 아래 <a href="#install-other">터미널 방식</a>을 쓰세요.</p>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- track 2 · Claude Code -->
|
|
168
|
+
<div class="track">
|
|
169
|
+
<div class="track__head">
|
|
170
|
+
<span class="track__tag">2 · Claude Code</span>
|
|
171
|
+
<h3>CareerMate 폴더에서 한 문장으로</h3>
|
|
172
|
+
<p>루트 <code>CLAUDE.md</code>가 설치 안내 역할을 합니다. 폴더를 열고 아래 문장만 말하세요.</p>
|
|
173
|
+
</div>
|
|
174
|
+
<ol class="steps">
|
|
175
|
+
<li class="step">
|
|
176
|
+
<div class="step__no">1</div>
|
|
177
|
+
<div class="step__body">
|
|
178
|
+
<h3>CareerMate 폴더에서 Claude Code 열기</h3>
|
|
179
|
+
<p>CareerMate 폴더에서 <code>claude</code>를 실행한 뒤, 아래 문장을 그대로 입력합니다. Claude가 <code>INSTALL.md</code>를 따라 등록까지 진행합니다.</p>
|
|
180
|
+
<div class="code">
|
|
181
|
+
<button class="copy-btn" type="button" data-copy-target="code-prompt-cc">복사</button>
|
|
182
|
+
<pre id="code-prompt-cc">CareerMate를 설치하고 설정해줘. INSTALL.md를 따라 진행해줘.</pre>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</li>
|
|
186
|
+
<li class="step">
|
|
187
|
+
<div class="step__no">2</div>
|
|
188
|
+
<div class="step__body">
|
|
189
|
+
<h3>한 번만 승인하고 확인</h3>
|
|
190
|
+
<p>Claude Code가 프로젝트 MCP 서버 등록을 <strong>한 번 승인</strong> 요청하면 허용하세요. 그다음 <strong>“<code>get_onboarding_status</code> 호출해서 연결됐는지 확인해줘”</strong>로 점검합니다.</p>
|
|
191
|
+
</div>
|
|
192
|
+
</li>
|
|
193
|
+
</ol>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- track 3 · Codex -->
|
|
197
|
+
<div class="track">
|
|
198
|
+
<div class="track__head">
|
|
199
|
+
<span class="track__tag">3 · Codex</span>
|
|
200
|
+
<h3>CareerMate 폴더에서 한 문장으로</h3>
|
|
201
|
+
<p>루트 <code>AGENTS.md</code>가 설치 안내 역할을 합니다. 폴더를 열고 같은 문장을 말하세요.</p>
|
|
202
|
+
</div>
|
|
203
|
+
<ol class="steps">
|
|
204
|
+
<li class="step">
|
|
205
|
+
<div class="step__no">1</div>
|
|
206
|
+
<div class="step__body">
|
|
207
|
+
<h3>CareerMate 폴더에서 Codex 열기</h3>
|
|
208
|
+
<p>CareerMate 폴더에서 <code>codex</code>를 실행한 뒤, 아래 문장을 그대로 입력합니다. Codex가 <code>INSTALL.md</code>를 따라 등록까지 진행합니다.</p>
|
|
209
|
+
<div class="code">
|
|
210
|
+
<button class="copy-btn" type="button" data-copy-target="code-prompt-codex">복사</button>
|
|
211
|
+
<pre id="code-prompt-codex">CareerMate를 설치하고 설정해줘. INSTALL.md를 따라 진행해줘.</pre>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</li>
|
|
215
|
+
<li class="step">
|
|
216
|
+
<div class="step__no">2</div>
|
|
217
|
+
<div class="step__body">
|
|
218
|
+
<h3><code>/mcp</code>로 확인</h3>
|
|
219
|
+
<p>Codex 안에서 <code>/mcp</code> 명령으로 <code>careermate</code>가 등록됐는지 확인하고, <strong>“<code>get_onboarding_status</code> 호출해서 연결됐는지 확인해줘”</strong>로 점검합니다.</p>
|
|
220
|
+
</div>
|
|
221
|
+
</li>
|
|
222
|
+
</ol>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- track · 기타 MCP 클라이언트 (터미널) -->
|
|
226
|
+
<div class="track" id="install-other">
|
|
227
|
+
<div class="track__head">
|
|
228
|
+
<span class="track__tag">기타 MCP 클라이언트 · 터미널</span>
|
|
229
|
+
<h3>Cursor 등 — 직접 등록</h3>
|
|
230
|
+
<p>Cursor · Cline · Windsurf 등 다른 로컬 stdio MCP 클라이언트는 터미널에서 직접 설치·등록합니다.</p>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="note">
|
|
234
|
+
<strong>준비물은 Node.js 22.5 이상</strong> 하나뿐입니다. 내장 SQLite를 쓰므로 별도 컴파일러·빌드 도구가 필요 없습니다.
|
|
235
|
+
터미널(Windows는 PowerShell)에서 <code>node --version</code>으로 확인하고, 낮으면
|
|
236
|
+
<a href="https://nodejs.org/" rel="noopener" target="_blank">nodejs.org</a>에서 LTS를 설치하세요.
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<ol class="steps">
|
|
240
|
+
<li class="step">
|
|
241
|
+
<div class="step__no">1</div>
|
|
242
|
+
<div class="step__body">
|
|
243
|
+
<h3>코드 받기</h3>
|
|
244
|
+
<p>CareerMate 저장소를 내려받습니다. (Git이 없으면 <em>Code → Download ZIP</em>으로 받아 압축을 풀어도 됩니다.)</p>
|
|
245
|
+
<!-- 저장소(osntak/CareerMate)는 현재 비공개라 clone에 접근 권한이 필요합니다. 권한이 없으면 위 npx 방식을 쓰세요. -->
|
|
246
|
+
<div class="code">
|
|
247
|
+
<button class="copy-btn" type="button" data-copy-target="code-clone">복사</button>
|
|
248
|
+
<pre id="code-clone">git clone https://github.com/osntak/CareerMate.git
|
|
249
|
+
cd CareerMate</pre>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</li>
|
|
253
|
+
<li class="step">
|
|
254
|
+
<div class="step__no">2</div>
|
|
255
|
+
<div class="step__body">
|
|
256
|
+
<h3>설치</h3>
|
|
257
|
+
<p>의존성을 한 번만 설치합니다. (빌드 단계 없음) — 또는 클론 없이 <strong><code>npx -y careermate init</code></strong> 한 줄로도 등록됩니다(<code>careermate</code> npm 게시 완료).</p>
|
|
258
|
+
<div class="code">
|
|
259
|
+
<span class="code__label">프로젝트 폴더에서</span>
|
|
260
|
+
<button class="copy-btn" type="button" data-copy-target="code-install">복사</button>
|
|
261
|
+
<pre id="code-install">npm install</pre>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</li>
|
|
265
|
+
<li class="step">
|
|
266
|
+
<div class="step__no">3</div>
|
|
267
|
+
<div class="step__body">
|
|
268
|
+
<h3>AI 클라이언트에 자동 연결</h3>
|
|
269
|
+
<p>감지된 클라이언트의 MCP 설정에 CareerMate를 자동 등록하고, Claude Code 프로젝트 설정(<code>.mcp.json</code>)은 항상 함께 작성합니다. 끝나면 해당 AI 클라이언트를 <strong>완전히 종료했다 다시 켜세요.</strong></p>
|
|
270
|
+
<div class="code">
|
|
271
|
+
<span class="code__label">자동 등록</span>
|
|
272
|
+
<button class="copy-btn" type="button" data-copy-target="code-init">복사</button>
|
|
273
|
+
<pre id="code-init">npm run init</pre>
|
|
274
|
+
</div>
|
|
275
|
+
<p class="step__tip">직접 붙여넣고 싶다면 <code>npm run init -- --print</code>로 설정 JSON·명령만 출력해 클라이언트에 추가하세요. 특정 클라이언트만 등록하려면 <code>npm run init -- --client cursor</code>처럼 지정합니다. 클라이언트별 설정 파일 위치는 아래 탭을 참고하세요.</p>
|
|
276
|
+
|
|
277
|
+
<div class="tabs" role="tablist" aria-label="설정 파일 위치">
|
|
278
|
+
<button class="tab is-active" id="tab-claude" role="tab" aria-controls="panel-claude" aria-selected="true">Claude Desktop</button>
|
|
279
|
+
<button class="tab" id="tab-cursor" role="tab" aria-controls="panel-cursor" aria-selected="false" tabindex="-1">Cursor / 기타</button>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="panel" id="panel-claude" role="tabpanel" aria-labelledby="tab-claude" data-active="true">
|
|
282
|
+
<p class="path-note">Windows: <code>%APPDATA%\Claude\claude_desktop_config.json</code> · macOS: <code>~/Library/Application Support/Claude/claude_desktop_config.json</code><br />(Settings → Developer → Edit Config 로도 열 수 있습니다. <code>npm run init</code>이 이 파일을 자동으로 수정합니다.)</p>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="panel" id="panel-cursor" role="tabpanel" aria-labelledby="tab-cursor" data-active="false" hidden>
|
|
285
|
+
<p class="path-note">프로젝트별: <code>.cursor/mcp.json</code> · 전역: <code>~/.cursor/mcp.json</code> — Cursor·Cline·Windsurf 등 대부분 같은 형식입니다. 추가 후 클라이언트를 다시 시작하세요.</p>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</li>
|
|
289
|
+
<li class="step">
|
|
290
|
+
<div class="step__no">4</div>
|
|
291
|
+
<div class="step__body">
|
|
292
|
+
<h3>대시보드 실행 (선택)</h3>
|
|
293
|
+
<p>내 데이터를 눈으로 확인하려면 대시보드를 켭니다. 기본 주소는 <code>http://127.0.0.1:4319</code>, 포트가 사용 중이면 다음 빈 포트로 넘어갑니다. 종료는 <kbd>Ctrl</kbd>+<kbd>C</kbd>.</p>
|
|
294
|
+
<div class="code">
|
|
295
|
+
<span class="code__label">대시보드 시작</span>
|
|
296
|
+
<button class="copy-btn" type="button" data-copy-target="code-start">복사</button>
|
|
297
|
+
<pre id="code-start">npm start</pre>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</li>
|
|
301
|
+
<li class="step">
|
|
302
|
+
<div class="step__no">5</div>
|
|
303
|
+
<div class="step__body">
|
|
304
|
+
<h3>연결 확인</h3>
|
|
305
|
+
<p>AI에게 <strong>“<code>get_onboarding_status</code>를 호출해서 연결됐는지 확인해줘”</strong>라고 말해 보세요. 결과가 돌아오면 정상입니다.</p>
|
|
306
|
+
</div>
|
|
307
|
+
</li>
|
|
308
|
+
</ol>
|
|
309
|
+
|
|
310
|
+
<details class="acc">
|
|
311
|
+
<summary>문제가 생겼을 때 <span class="acc__hint">포트 충돌 · MCP 연결 실패 · 데이터 위치 · 초기화</span></summary>
|
|
312
|
+
<div class="acc__body">
|
|
313
|
+
<div class="trouble">
|
|
314
|
+
<div class="trouble__item">
|
|
315
|
+
<h4>포트가 이미 사용 중</h4>
|
|
316
|
+
<p>4319가 막히면 자동으로 다음 빈 포트로 옮겨갑니다. 터미널에 출력된 실제 주소를 사용하거나, <code>CAREERMATE_PORT</code> 환경변수로 고정하세요.</p>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="trouble__item">
|
|
319
|
+
<h4>AI에 MCP가 연결되지 않음</h4>
|
|
320
|
+
<p>① <code>npm run init</code>이 정상 종료됐는지 ② <code>node --version</code>이 22.5 이상인지 ③ 클라이언트를 <strong>완전히 종료 후 재시작</strong>했는지 ④ <code>npm install</code>이 끝났는지 확인하세요. 폴더를 옮기거나 이름을 바꿨다면 등록된 경로가 깨지므로 <code>npm run init</code>을 다시 실행하세요.</p>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="trouble__item">
|
|
323
|
+
<h4>상태 점검</h4>
|
|
324
|
+
<p>프로젝트 폴더에서 진단을 실행하면 Node 버전·데이터 폴더·DB 상태를 점검합니다.</p>
|
|
325
|
+
<div class="code">
|
|
326
|
+
<button class="copy-btn" type="button" data-copy-target="code-doctor">복사</button>
|
|
327
|
+
<pre id="code-doctor">npm run doctor</pre>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="trouble__item">
|
|
331
|
+
<h4>데이터 위치 / 초기화</h4>
|
|
332
|
+
<p>기본 위치는 <code>~/.careermate/careermate.sqlite</code>(Windows: <code>%USERPROFILE%\.careermate</code>). <code>CAREERMATE_DATA_DIR</code>로 변경 가능. <code>npm run migrate</code>는 DB를 준비/업그레이드(반복 실행 안전)하고, 완전 초기화는 <code>careermate.sqlite</code> 삭제 후 재실행(백업 권장)입니다.</p>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</details>
|
|
337
|
+
</div>
|
|
338
|
+
</section>
|
|
339
|
+
|
|
340
|
+
<!-- ======================================================= privacy -->
|
|
341
|
+
<section id="privacy" class="section">
|
|
342
|
+
<div class="privacy-card">
|
|
343
|
+
<div class="privacy-card__icon" aria-hidden="true">
|
|
344
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="privacy-card__text">
|
|
347
|
+
<h2>당신의 데이터는 당신 컴퓨터를 떠나지 않습니다</h2>
|
|
348
|
+
<p>대시보드 서버는 <code>127.0.0.1</code>(이 컴퓨터)에만 바인딩되어 외부에서 접근할 수 없고,<br />MCP 서버는 네트워크 호출을 하지 않습니다. 분석·글쓰기는 당신이 직접 고른 AI가 수행합니다.</p>
|
|
349
|
+
<ul class="privacy-card__list">
|
|
350
|
+
<li><strong>저장 위치</strong> <code>~/.careermate/careermate.sqlite</code></li>
|
|
351
|
+
<li><strong>내보내기</strong> 자기소개서 등을 <code>~/.careermate/exports</code>로</li>
|
|
352
|
+
<li><strong>삭제·관리</strong> 대시보드 <strong>설정(Settings)</strong>에서 직접</li>
|
|
353
|
+
</ul>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</section>
|
|
357
|
+
|
|
358
|
+
</main>
|
|
359
|
+
|
|
360
|
+
<footer class="foot">
|
|
361
|
+
<div class="foot__brand"><span class="nav__logo" aria-hidden="true">C</span> CareerMate</div>
|
|
362
|
+
<p>로컬에서 동작하는 MCP 기반 커리어 관리 도구. 모든 데이터는 이 컴퓨터에만 저장됩니다.</p>
|
|
363
|
+
<div class="foot__links">
|
|
364
|
+
<a href="#top">맨 위로</a>
|
|
365
|
+
<a href="#features">기능</a>
|
|
366
|
+
<a href="#install">설치</a>
|
|
367
|
+
</div>
|
|
368
|
+
</footer>
|
|
369
|
+
|
|
370
|
+
<div class="sr-only" role="status" aria-live="polite" id="copy-status"></div>
|
|
371
|
+
|
|
372
|
+
<script>
|
|
373
|
+
(function () {
|
|
374
|
+
'use strict';
|
|
375
|
+
var status = document.getElementById('copy-status');
|
|
376
|
+
|
|
377
|
+
// --- generic clipboard helper ------------------------------------
|
|
378
|
+
function legacyCopy(text) {
|
|
379
|
+
var ta = document.createElement('textarea');
|
|
380
|
+
ta.value = text; ta.setAttribute('readonly', '');
|
|
381
|
+
ta.style.position = 'absolute'; ta.style.left = '-9999px';
|
|
382
|
+
document.body.appendChild(ta); ta.select();
|
|
383
|
+
var ok = false;
|
|
384
|
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
|
385
|
+
document.body.removeChild(ta);
|
|
386
|
+
return ok;
|
|
387
|
+
}
|
|
388
|
+
function copyText(text, onok) {
|
|
389
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
390
|
+
navigator.clipboard.writeText(text).then(onok, function () { if (legacyCopy(text)) onok(); });
|
|
391
|
+
} else if (legacyCopy(text)) { onok(); }
|
|
392
|
+
}
|
|
393
|
+
function announce(msg) { if (status) status.textContent = msg; }
|
|
394
|
+
|
|
395
|
+
// --- copy buttons (code blocks) ----------------------------------
|
|
396
|
+
function flashSmall(btn) {
|
|
397
|
+
var original = btn.textContent;
|
|
398
|
+
btn.textContent = '복사됨'; btn.classList.add('copied');
|
|
399
|
+
announce('클립보드에 복사되었습니다.');
|
|
400
|
+
window.setTimeout(function () { btn.textContent = original; btn.classList.remove('copied'); }, 1600);
|
|
401
|
+
}
|
|
402
|
+
document.querySelectorAll('.copy-btn').forEach(function (btn) {
|
|
403
|
+
btn.addEventListener('click', function () {
|
|
404
|
+
var src = document.getElementById(btn.getAttribute('data-copy-target'));
|
|
405
|
+
if (src) copyText(src.textContent || '', function () { flashSmall(btn); });
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// --- config tabs -------------------------------------------------
|
|
410
|
+
var tablist = document.querySelector('.tabs[role="tablist"]');
|
|
411
|
+
if (tablist) {
|
|
412
|
+
var tabs = Array.prototype.slice.call(tablist.querySelectorAll('[role="tab"]'));
|
|
413
|
+
function selectTab(tab) {
|
|
414
|
+
tabs.forEach(function (t) {
|
|
415
|
+
var sel = t === tab;
|
|
416
|
+
t.setAttribute('aria-selected', sel ? 'true' : 'false');
|
|
417
|
+
t.classList.toggle('is-active', sel);
|
|
418
|
+
t.tabIndex = sel ? 0 : -1;
|
|
419
|
+
var panel = document.getElementById(t.getAttribute('aria-controls'));
|
|
420
|
+
if (panel) {
|
|
421
|
+
panel.setAttribute('data-active', sel ? 'true' : 'false');
|
|
422
|
+
if (sel) panel.removeAttribute('hidden'); else panel.setAttribute('hidden', '');
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
tabs.forEach(function (tab, i) {
|
|
427
|
+
tab.addEventListener('click', function () { selectTab(tab); });
|
|
428
|
+
tab.addEventListener('keydown', function (e) {
|
|
429
|
+
var idx = null;
|
|
430
|
+
if (e.key === 'ArrowRight') idx = (i + 1) % tabs.length;
|
|
431
|
+
else if (e.key === 'ArrowLeft') idx = (i - 1 + tabs.length) % tabs.length;
|
|
432
|
+
else if (e.key === 'Home') idx = 0;
|
|
433
|
+
else if (e.key === 'End') idx = tabs.length - 1;
|
|
434
|
+
if (idx !== null) { e.preventDefault(); tabs[idx].focus(); selectTab(tabs[idx]); }
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// --- scroll reveal (fade-up on enter) ----------------------------
|
|
440
|
+
var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
441
|
+
if (!reduceMotion && 'IntersectionObserver' in window) {
|
|
442
|
+
var revealEls = document.querySelectorAll('.section__head, .feature, .check, .track__head, .step, .acc, .privacy-card');
|
|
443
|
+
revealEls.forEach(function (el) { el.classList.add('reveal'); });
|
|
444
|
+
var io = new IntersectionObserver(function (entries) {
|
|
445
|
+
entries.forEach(function (e) {
|
|
446
|
+
if (e.isIntersecting) { e.target.classList.add('is-visible'); io.unobserve(e.target); }
|
|
447
|
+
});
|
|
448
|
+
}, { threshold: 0.1, rootMargin: '0px 0px -48px 0px' });
|
|
449
|
+
revealEls.forEach(function (el) { io.observe(el); });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// --- theme toggle (persist) --------------------------------------
|
|
453
|
+
var root = document.documentElement;
|
|
454
|
+
var toggle = document.getElementById('theme-toggle');
|
|
455
|
+
var saved = null;
|
|
456
|
+
try { saved = window.localStorage.getItem('cf-theme'); } catch (e) {}
|
|
457
|
+
if (saved === 'dark' || saved === 'light') root.setAttribute('data-theme', saved);
|
|
458
|
+
function isDark() {
|
|
459
|
+
var t = root.getAttribute('data-theme');
|
|
460
|
+
if (t === 'dark') return true;
|
|
461
|
+
if (t === 'light') return false;
|
|
462
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
463
|
+
}
|
|
464
|
+
if (toggle) {
|
|
465
|
+
toggle.addEventListener('click', function () {
|
|
466
|
+
var next = isDark() ? 'light' : 'dark';
|
|
467
|
+
root.setAttribute('data-theme', next);
|
|
468
|
+
try { window.localStorage.setItem('cf-theme', next); } catch (e) {}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
})();
|
|
472
|
+
</script>
|
|
473
|
+
</body>
|
|
474
|
+
</html>
|