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.
Files changed (93) hide show
  1. package/README.md +10 -7
  2. package/apps/mcp/src/index.ts +3 -1
  3. package/apps/web/DESIGN_GUIDE.md +2 -2
  4. package/apps/web/public/icon-180.png +0 -0
  5. package/apps/web/public/index.html +4 -1
  6. package/apps/web/public/lib.js +1 -1
  7. package/apps/web/public/pages/applications.js +17 -1
  8. package/apps/web/public/pages/settings.js +6 -4
  9. package/apps/web/public/styles.css +49 -36
  10. package/apps/web/src/info.ts +9 -22
  11. package/apps/web/src/routes.ts +17 -13
  12. package/apps/web/src/server.ts +33 -25
  13. package/apps/web/src/settings.ts +35 -17
  14. package/bin/careermate.mjs +8 -5
  15. package/dist/{mcpb-stage/dist/web.mjs → doctor.mjs} +1122 -1450
  16. package/dist/install-page/favicon.svg +1 -0
  17. package/dist/install-page/icon-180.png +0 -0
  18. package/dist/install-page/icon-512.png +0 -0
  19. package/dist/install-page/index.html +309 -167
  20. package/dist/install-page/llms-install.txt +202 -0
  21. package/dist/install-page/og.png +0 -0
  22. package/dist/install-page/shots/applications-dark.png +0 -0
  23. package/dist/install-page/shots/applications-light.png +0 -0
  24. package/dist/install-page/shots/documents-dark.png +0 -0
  25. package/dist/install-page/shots/documents-light.png +0 -0
  26. package/dist/install-page/shots/home-dark.png +0 -0
  27. package/dist/install-page/shots/home-light.png +0 -0
  28. package/dist/install-page/shots/interview-dark.png +0 -0
  29. package/dist/install-page/shots/interview-light.png +0 -0
  30. package/dist/install-page/shots/jobs-dark.png +0 -0
  31. package/dist/install-page/shots/jobs-light.png +0 -0
  32. package/dist/install-page/shots/settings-dark.png +0 -0
  33. package/dist/install-page/shots/settings-light.png +0 -0
  34. package/dist/install-page/start.html +346 -0
  35. package/dist/install-page/style.css +94 -3
  36. package/dist/mcp.mjs +326 -68
  37. package/dist/migrate.mjs +4715 -0
  38. package/dist/public/icon-180.png +0 -0
  39. package/dist/public/index.html +4 -1
  40. package/dist/public/lib.js +1 -1
  41. package/dist/public/pages/applications.js +17 -1
  42. package/dist/public/pages/settings.js +6 -4
  43. package/dist/public/styles.css +49 -36
  44. package/dist/web.mjs +323 -129
  45. package/docs/DEMO.md +3 -3
  46. package/docs/MCP_TOOLS.md +21 -2
  47. package/docs/ROADMAP.md +3 -3
  48. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
  49. package/docs/START_WORKFLOW.md +6 -0
  50. package/docs/SUPPORTED_AI_APPS.md +85 -33
  51. package/docs/WORKFLOWS.md +1 -1
  52. package/package.json +3 -2
  53. package/packages/core/src/services.ts +42 -1
  54. package/packages/db/src/connection.ts +30 -0
  55. package/packages/db/src/index.ts +1 -0
  56. package/packages/db/src/repositories.ts +109 -63
  57. package/packages/db/src/schema.ts +118 -0
  58. package/packages/exporters/src/html.ts +9 -12
  59. package/packages/mcp-tools/src/tools.ts +81 -0
  60. package/packages/shared/src/enums.ts +39 -0
  61. package/scripts/build-dist.mjs +27 -8
  62. package/scripts/build-mcpb.mjs +60 -4
  63. package/scripts/gen-og.mjs +86 -0
  64. package/scripts/init.ts +32 -3
  65. package/scripts/seed.ts +5 -5
  66. package/scripts/shots-seed.ts +192 -0
  67. package/scripts/shots.ts +89 -0
  68. package/tsconfig.json +3 -1
  69. package/dist/careermate.mcpb +0 -0
  70. package/dist/mcp-smoke.err +0 -3
  71. package/dist/mcpb-stage/README.md +0 -219
  72. package/dist/mcpb-stage/dist/install-page/index.html +0 -434
  73. package/dist/mcpb-stage/dist/install-page/style.css +0 -407
  74. package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
  75. package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
  76. package/dist/mcpb-stage/dist/public/app.js +0 -118
  77. package/dist/mcpb-stage/dist/public/index.html +0 -41
  78. package/dist/mcpb-stage/dist/public/lib.js +0 -282
  79. package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
  80. package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
  81. package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
  82. package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
  83. package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
  84. package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
  85. package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
  86. package/dist/mcpb-stage/dist/public/styles.css +0 -420
  87. package/dist/mcpb-stage/manifest.json +0 -40
  88. package/docs/INSTALL.md +0 -148
  89. package/docs/INSTALL_AND_USAGE.md +0 -99
  90. package/install-page/index.html +0 -474
  91. package/install-page/style.css +0 -391
  92. package/install-page/vercel.json +0 -20
  93. /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
package/docs/MCP_TOOLS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CareerMate는 **MCP-우선** 로컬 커리어 관리 도구입니다. 사용자는 자신의 ChatGPT/Claude/Gemini와 대화하고, 그 AI가 여기 정의된 MCP 도구를 호출해 로컬 커리어 DB(`~/.careermate/careermate.sqlite`)를 읽고 씁니다. **CareerMate 내부에는 LLM이 없습니다.** 적합도 분석·자기소개서 작성 같은 추론은 모두 사용자의 AI가 수행하고, CareerMate는 데이터를 저장하고 다시 돌려주는 역할만 합니다.
4
4
 
5
- 이 문서는 `packages/mcp-tools/src/tools.ts`에 정의된 **24개 도구**의 레퍼런스입니다. 도구의 입력 파라미터는 `packages/shared/src/schemas.ts`의 zod 스키마와 일치합니다.
5
+ 이 문서는 `packages/mcp-tools/src/tools.ts`에 정의된 **30개 도구**의 레퍼런스입니다. 도구의 입력 파라미터는 `packages/shared/src/schemas.ts`의 zod 스키마와 일치합니다.
6
6
 
7
7
  > 설계 원칙: 이 도구들은 단순 CRUD 래퍼가 아닙니다. 각 도구는 AI가 사용자를 대신해 수행하는 실제 작업에 대응하며, 설명에 *언제* 그리고 *어떻게* 호출해야 하는지가 담겨 있습니다. 가장 중요한 도구는 [`get_application_context`](#get_application_context-) 입니다.
8
8
 
@@ -16,7 +16,7 @@ CareerMate는 **MCP-우선** 로컬 커리어 관리 도구입니다. 사용자
16
16
 
17
17
  | 읽기전용(✅) 도구 | 쓰기/부수효과(✏️) 도구 |
18
18
  | --- | --- |
19
- | `get_onboarding_status`, `start_onboarding`, `get_profile`, `get_resumes`, `get_cover_letters`, `get_job_posting`, `list_jobs`, `parse_job_posting`, `get_application_context`, `list_recent_activity`, `get_workflow_guide`, `get_writing_style_guide` | `save_profile`, `update_profile`, `add_resume`, `add_cover_letter`, `save_cover_letter_version`, `save_job_posting`, `save_fit_analysis`, `update_application_status`, `save_interview_prep`, `export_cover_letter`, `open_dashboard`, `open_application` |
19
+ | `get_onboarding_status`, `start_onboarding`, `get_profile`, `get_resumes`, `get_experiences`, `get_projects`, `get_skills`, `get_cover_letters`, `get_job_posting`, `list_jobs`, `parse_job_posting`, `get_application_context`, `list_recent_activity`, `get_workflow_guide`, `get_writing_style_guide` | `save_profile`, `update_profile`, `add_resume`, `add_experience`, `add_project`, `add_skill`, `add_cover_letter`, `save_cover_letter_version`, `save_job_posting`, `save_fit_analysis`, `update_application_status`, `save_interview_prep`, `export_cover_letter`, `open_dashboard`, `open_application` |
20
20
 
21
21
  ---
22
22
 
@@ -64,6 +64,25 @@ CareerMate는 **MCP-우선** 로컬 커리어 관리 도구입니다. 사용자
64
64
 
65
65
  ---
66
66
 
67
+ ## 3-1. 경력·프로젝트·기술스택 (Experiences / Projects / Skills)
68
+
69
+ 이력서 본문(`add_resume`)과 별개로, **구조화된** 경력·프로젝트·기술을 각각의 테이블에 항목 단위로 저장합니다. `get_application_context`의 `experiences`/`projects`/`skills`로 노출되며, `get_onboarding_status`의 `has_experience`/`has_skills`와 프로필 완성도(0~100) 계산에 직접 반영됩니다. (대시보드의 Profile 페이지에서도 같은 데이터를 편집합니다.)
70
+
71
+ | 도구 | 목적 | 주요 입력 | 반환 | 언제 호출 | 읽기전용 |
72
+ | --- | --- | --- | --- | --- | --- |
73
+ | `add_experience` | 경력(직장 단위) 항목 저장 | `company`(필수), `role?`, `employment_type?`, `start_date?`, `end_date?`, `is_current?`, `description?`, `achievements?`, `tech?`, `order_index?` | 저장된 `ExperienceRecord` | 이력서에서 경력을 구조화해 보관할 때 | ✏️ |
74
+ | `get_experiences` | 저장된 경력을 시간 역순으로 조회 | 없음 | `ExperienceRecord[]` | 경력 목록만 빠르게 확인할 때 | ✅ |
75
+ | `add_project` | 대표 프로젝트 항목 저장 | `name`(필수), `role?`, `description?`, `highlights?`, `tech?`, `url?`, `start_date?`, `end_date?`, `order_index?` | 저장된 `ProjectRecord` | 프로젝트 이력을 구조화해 보관할 때 | ✏️ |
76
+ | `get_projects` | 저장된 프로젝트 조회 | 없음 | `ProjectRecord[]` | 프로젝트 목록만 확인할 때 | ✅ |
77
+ | `add_skill` | 기술스택 항목 저장(개당 1건) | `name`(필수), `category?`, `level?`, `years?`, `order_index?` | 저장된 `SkillRecord` | 보유 기술을 정리해 둘 때 | ✏️ |
78
+ | `get_skills` | 저장된 기술스택 조회 | 없음 | `SkillRecord[]` | 기술 목록만 확인할 때 | ✅ |
79
+
80
+ - `achievements`/`highlights`에는 정량 지표가 담긴 성과를, `tech`에는 사용 기술을 배열로 넣으면 적합도 분석·자소서·면접 준비에서 근거로 활용됩니다.
81
+ - 기술은 항목당 1건이므로 여러 개면 `add_skill`을 반복 호출하세요.
82
+ - 입력 스키마: `ExperienceInputSchema` / `ProjectInputSchema` / `SkillInputSchema`(`packages/shared/src/schemas.ts`).
83
+
84
+ ---
85
+
67
86
  ## 4. 자기소개서 (Cover Letters)
68
87
 
69
88
  | 도구 | 목적 | 주요 입력 | 반환 | 언제 호출 | 읽기전용 |
package/docs/ROADMAP.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  CareerMate는 MCP-우선 로컬 커리어 관리 도구다. 사용자는 자신의 ChatGPT/Claude/Gemini와 대화하고, 그 AI가 CareerMate의 로컬 MCP 도구를 호출해 로컬 커리어 DB를 읽고 쓴다. CareerMate 내부에는 LLM이 없으며(분석·작성은 사용자의 AI가 수행), 모든 데이터는 로컬에만 저장된다.
4
4
 
5
- 이 문서는 현재 버전(`0.1.0`)의 MVP 완료 범위와, 지금은 제외하되 구조는 열어둔 향후 과제를 정리한다.
5
+ 이 문서는 현재 버전(`0.1.1`)의 MVP 완료 범위와, 지금은 제외하되 구조는 열어둔 향후 과제를 정리한다.
6
6
 
7
- 관련 문서: [README](../README.md), [설치 안내](./INSTALL.md), [워크플로우](./WORKFLOWS.md), [시작하기 워크플로우](./START_WORKFLOW.md)
7
+ 관련 문서: [README](../README.md), [설치 런북](../INSTALL.md), [지원 앱·설치 레퍼런스](./SUPPORTED_AI_APPS.md), [워크플로우](./WORKFLOWS.md), [시작하기 워크플로우](./START_WORKFLOW.md)
8
8
 
9
9
  ---
10
10
 
11
11
  ## 현재 버전
12
12
 
13
- - 버전: `0.1.0` (`package.json`)
13
+ - 버전: `0.1.1` (`package.json`)
14
14
  - 런타임 요구: Node `>=22.5.0` (`node:sqlite` 내장 사용, 네이티브 컴파일 없음)
15
15
  - 실행 방식: 무빌드(tsx) — `node --no-warnings --import tsx ...`
16
16
 
@@ -0,0 +1,135 @@
1
+ # 런타임·배포 조사: 완전 원클릭(Codex .exe / Claude `.mcpb`) 실현 가능성
2
+
3
+ - **작성일**: 2026-06-15
4
+ - **상태**: 조사 완료 / 큰 작업은 **보류(문서만)**
5
+ - **관련**: [ADR #1 `node:sqlite` 채택](./DECISIONS.md), [ADR #6 대시보드·MCP의 SQLite(WAL) 공유 + `server.json` 핸드셰이크](./DECISIONS.md), [ROADMAP](./ROADMAP.md)
6
+
7
+ > 이 문서는 "사용자가 Node/npx/git을 미리 깔지 않고도 한 번에 설치되는 경험"을 두 경로
8
+ > (① OpenAI **Codex Windows 앱** + 단일 `.exe`, ② Claude Desktop **`.mcpb`**)에서 만들 수
9
+ > 있는지 조사한 기록이다. 결론과 근거, 그리고 다음에 누가 이어받든 헛수고하지 않도록
10
+ > "무엇이 작은 수정이고 무엇이 큰 작업인지"를 남긴다.
11
+
12
+ ---
13
+
14
+ ## TL;DR
15
+
16
+ - **공통 근본 제약**: CareerMate는 **`node:sqlite`(Node ≥22.5의 최신 내장 기능)**에 의존한다.
17
+ "무설치 번들 런타임" 전략의 성패는 전부 **"충분히 새로운 Node를 어떻게 확보하느냐"**로 수렴한다.
18
+ - **Codex Windows 앱 + 단일 `.exe`**: 패키징(Node SEA)은 **기술적으로 성립**하나, **자가등록 지속성**과
19
+ **샌드박스** 두 개의 (2026-05~06 기준) 외부 버그/제약 때문에 **진정한 무인 원클릭은 현재 불가**. → **보류.**
20
+ - **Claude `.mcpb`**: 산출물(`dist/mcp.mjs`)은 **Node ≥22.5에서 정상 동작**함을 실증함. 사용자가 겪은
21
+ "설치는 됐는데 도구가 안 뜸/빨간 에러"는 **Claude Desktop 내장 Node가 22.5 미만**일 때의 증상으로 추정되며,
22
+ 그 경우의 해결은 **저장 계층 rewrite(= 큰 작업)**다. → **로그로 근본 원인 확정 후 결정.**
23
+
24
+ ---
25
+
26
+ ## 1. 배경 / 질문
27
+
28
+ - CareerMate는 로컬 MCP stdio 서버 + 로컬 웹 대시보드. 저장은 `node:sqlite`.
29
+ - 목표: 비개발자가 **아무것도 미리 설치하지 않고** 한 번에 쓰기 시작하는 경험.
30
+ - 두 경로를 검토:
31
+ 1. **Codex Windows 데스크톱 앱**(2026-03 출시, MS Store, 로컬 샌드박스+PowerShell)에 **Node 런타임을
32
+ 동봉한 단일 `.exe`**(Node SEA)를 더블클릭 → 자가등록 → 동작.
33
+ 2. **Claude Desktop `.mcpb`**(런타임 동봉형 원클릭) — 이미 빌드 파이프라인 존재(`scripts/build-mcpb.mjs`).
34
+
35
+ ---
36
+
37
+ ## 2. Codex Windows 앱 + 단일 `.exe` (Node SEA)
38
+
39
+ ### 2.1 패키징 자체 — 성립 (조건부 가능)
40
+
41
+ | 항목 | 판정 | 근거 |
42
+ |---|---|---|
43
+ | Windows에서 SEA `.exe` 생성 | 가능 | Node SEA의 **첫 번째 CI 테스트 플랫폼이 Windows**. `.exe` 출력 지원. 안정성 지수는 1.1(Active development). |
44
+ | 빌드 워크플로 | 간소화됨 | Node **25.5.0**의 `--build-sea` 단일 플래그로 통합(종전 postject 3단계 불필요). |
45
+ | 정적 자산(대시보드 HTML/CSS/JS) 임베드 | 가능 | `sea.getAsset()`/`getRawAsset()` 계열로 번들·런타임 서빙(v20.12.0~). 단 현재 `apps/web`는 디스크에서 자산을 읽으므로 **임베드용 코드 수정 필요**. |
46
+ | `node:sqlite` 플래그 | 사실상 비이슈 | **Node ≥22.13 / ≥23.4부터 `--experimental-sqlite` 없이 동작**. 최신 Node를 임베드하면 해결. 필요시 SEA `execArgv`로 플래그 주입도 가능. |
47
+
48
+ 근거 출처(주요): nodejs.org `single-executable-applications`, `sqlite` 문서; Node v25.5.0 릴리스 블로그;
49
+ Joyee Cheung 블로그(Windows PE 주입 실테스트); commit `55239a48b6`("unflag sqlite module").
50
+
51
+ ### 2.2 자가등록·동작을 막는 두 개의 블로커 — 현재 불가
52
+
53
+ 1. **🚨 Codex 데스크톱이 시작 시 `~/.codex/config.toml`을 재작성하며 사용자 커스텀 `[mcp_servers]` 블록을 삭제**
54
+ - openai/codex **issue #24718** (2026-05-27 OPEN, v26.519.11010.0, Win11 26200): "Codex Desktop overwrites
55
+ `%USERPROFILE%\.codex\config.toml` during startup ... removes a manually configured custom MCP server",
56
+ "blocking data-loss issue." (보강: #26530, #24387, JetBrains LLM-24906)
57
+ - 의미: `.exe`가 `init`으로 스스로 등록해도 **앱이 지워버릴 수 있음** → 자가등록 전략의 핵심 전제가 깨짐.
58
+ 2. **샌드박스가 MCP stdio 서브프로세스에도 상속됨**
59
+ - 공식 문서: "The sandbox applies to spawned commands ... inherit the same sandbox boundaries", "Every file
60
+ write, terminal command, network request, and process spawn goes through the sandbox."
61
+ - 기본 `workspace-write`의 쓰기 가능 루트는 `cwd + $TMPDIR + /tmp`뿐 → **워크스페이스 바깥인
62
+ `~/.careermate` 쓰기와 127.0.0.1 포트 바인딩이 기본 거부**. `danger-full-access` 또는
63
+ `sandbox_workspace_write.writable_roots` 명시 필요. (실증: issue #18243, #24727 "windows sandbox failed: spawn setup")
64
+
65
+ > 두 블로커 모두 **CareerMate 코드 문제가 아니라 Codex 앱 측 미해결 버그/설계 제약**이라 우리가 코드로 못 고친다.
66
+ > #24718이 패치되고 샌드박스 정책이 정리되면 결론이 뒤집힐 수 있으므로 **추적 대상**.
67
+
68
+ ### 2.3 결론(Codex)
69
+
70
+ - 패키징은 가능, **무인 원클릭은 현재 불가** → **보류.** 당분간 Codex는 CLI/IDE 확장 경로 또는
71
+ "Node 1회 설치 → `npx -y careermate init`" 안내가 현실적.
72
+
73
+ ---
74
+
75
+ ## 3. Claude Desktop `.mcpb`
76
+
77
+ ### 3.1 실증 결과 (이 저장소, 2026-06-15, Node v25)
78
+
79
+ - `manifest.json`은 정상: `server.type:"node"`, `args:["--experimental-sqlite","${__dirname}/dist/mcp.mjs"]`.
80
+ - **`dist/mcp.mjs` 단독 부팅 성공** — `initialize` 응답 정상, **도구 24개** 로드, 데이터 `~/.careermate` 연결.
81
+ - 이 PC의 **Node v25**에서: `--experimental-sqlite`는 **거부되지 않고 경고만**, 플래그 없이도 `node:sqlite` 동작.
82
+ → "최신 Node가 플래그를 거부해 죽는다" 시나리오는 (최소 v25까지) **없음**.
83
+
84
+ ### 3.2 그렇다면 사용자가 본 "도구 안 뜸/빨간 에러"의 원인
85
+
86
+ - 산출물·플래그가 Node ≥22.5에서 멀쩡하므로, **sqlite 경로의 유일한 실패 원인 = Claude Desktop 내장 Node < 22.5**.
87
+ 그 경우 `node:sqlite` 모듈이 **아예 없고** `--experimental-sqlite`도 **모르는 옵션**이라 서버가 즉시 죽는다.
88
+ - (대안 가설) sqlite 무관 환경 문제 — 단 Claude Desktop 미설치 환경에서는 재현 불가.
89
+ - **확정 방법(저비용, 먼저 할 것)**: `.mcpb`를 깐 PC에서 로그 확인
90
+ - Windows: `%APPDATA%\Claude\logs\mcp-server-careermate.log` (또는 `mcp.log`)
91
+ - `bad option: --experimental-sqlite` / `No such built-in module: node:sqlite` → **내장 Node < 22.5 확정**
92
+ - 그 외(경로/ESM/`__dirname`) → 패키징 문제(별개 갈래)
93
+
94
+ ### 3.3 내장 Node < 22.5가 확정될 경우 — 저장 계층 대안
95
+
96
+ 코드 결합도는 낮다. `node:sqlite`를 만지는 곳은 **3개 파일**뿐:
97
+ `packages/db/src/{connection.ts, schema.ts, repositories.ts}`. 사용 API도 작은 부분집합
98
+ (`DatabaseSync`, `exec`, `prepare().get()/.all()/.run()`, `?`/`@name` 인자, `.changes`, `close`), **JOIN 없음**.
99
+ → `getDb()` 뒤 어댑터 교체 자체는 소규모.
100
+
101
+ **그러나 두 개의 지뢰:**
102
+
103
+ 1. **2-프로세스 동시성(ADR #6)** — 웹 대시보드와 MCP 서버가 **별도 프로세스로 같은 DB를 WAL 동시 접근**.
104
+ - `sql.js`(WASM): 낡은 Node까지 호환되지만 **인메모리+수동 저장**이라 두 프로세스 공유 시 last-writer-wins로 **깨짐**.
105
+ - JSON 스토어(lowdb 등): 동일하게 동시성 재설계 필요.
106
+ 2. **네이티브 ABI** — `better-sqlite3`는 API가 거의 동일하지만 **네이티브 모듈**이라 내장 Node ABI와
107
+ prebuilt 불일치(NODE_MODULE_VERSION) 위험. 버전 문제를 다른 형태로 되살림.
108
+
109
+ | 대안 | 낡은 내장 Node 호환 | 2-프로세스 동시성 | 비용 |
110
+ |---|---|---|---|
111
+ | sql.js (WASM) | ✅ | ❌ 재설계 필요 | 中~大 |
112
+ | better-sqlite3 | ❌ ABI 위험 | ✅ | 中(근본 미해결) |
113
+ | JSON 스토어 | ✅ | ❌ 재설계 필요 | 中~大 |
114
+
115
+ 가장 깔끔한 방향: **MCP 서버가 DB를 직접 열지 않고 웹 서버의 127.0.0.1 HTTP로 데이터를 요청 → 웹 서버가
116
+ 단일 DB 소유자**. 이러면 sql.js가 성립하지만 "대시보드가 안 떠 있어도 MCP 동작" UX를 잃는다(진짜 아키텍처 변경).
117
+
118
+ → 어느 쪽이든 **"작은 수정"이 아니라 아키텍처 변경**이므로, 근본 원인(로그) 확정 전에는 착수하지 않는다.
119
+
120
+ ---
121
+
122
+ ## 4. 권고 / 다음 단계
123
+
124
+ 1. **(먼저, 저비용)** `.mcpb` 깐 PC에서 로그 한 줄 확인 → 내장 Node < 22.5 여부 확정.
125
+ - 플래그류 에러면: (가설과 달리) 단순 수정 가능성 → 재검토.
126
+ - `node:sqlite` 부재면: 아래 3.3 rewrite를 정식 과제로.
127
+ 2. **단기 우회**(무설치 포기 허용 시): "Node ≥22.5 1회 설치 → `npx -y careermate init`" — Claude/Codex 공통으로 가장 견고.
128
+ 3. **추적**: Codex issue #24718(config 재작성) 상태. 닫히면 Codex SEA 원클릭을 재평가.
129
+
130
+ ## 5. 검증 메모 / 한계
131
+
132
+ - Codex 데스크톱 관련 결론은 **2026-05~06 기준 OPEN 버그**에 의존(시간 민감).
133
+ - #24718의 구체 재현은 HTTP MCP였다는 점에서 stdio 삭제는 광범위 재작성 정황으로의 소폭 일반화.
134
+ - 미서명 `.exe`의 SmartScreen/Defender 경고가 원클릭 UX를 얼마나 저해하는지는 **이번 라운드에서 확정 근거 미확보**(미검증).
135
+ - `.mcpb` 실패의 "내장 Node < 22.5" 결론은 **로그로 직접 확정 전까지 강한 추정**이다.
@@ -4,6 +4,8 @@
4
4
 
5
5
  > 핵심 원칙: **분석과 글쓰기는 당신(AI)이 수행하고, CareerMate는 데이터를 제공·보관**합니다. 공고 분석이나 자기소개서 작성 전에는 항상 `get_application_context`를 먼저 호출해 근거 데이터를 가져오세요.
6
6
 
7
+ > **대시보드 안내 정책**: 브라우저를 직접 여는 건 **첫 온보딩에서 한 번만**(`open_dashboard`)입니다. 이후 무언가를 저장·완료할 때는 **자동으로 새로 열지 말고** 주소(`http://127.0.0.1:4319`)만 안내하고, 사용자가 "열어줘"라고 할 때만 `open_application`(단건)·`open_dashboard`(전체)로 엽니다.
8
+
7
9
  ---
8
10
 
9
11
  ## A. 온보딩 (첫 연결 시 한 번)
@@ -25,6 +27,10 @@
25
27
 
26
28
  ### 3. 이력서 등록 — `add_resume`
27
29
 
30
+ - **사용자가 이력서를 주는 방법은 앱마다 다릅니다.** 상황에 맞게 안내하세요:
31
+ - 가장 쉬운 공통 방법은 **내용을 대화창에 그대로 붙여넣기**(파일 위치 무관).
32
+ - Claude 채팅 앱(.mcpb)이면 **파일 직접 첨부**, 폴더 기반(Claude Code 탭/Codex/Antigravity)이면 작업 폴더에 두고 **파일명·경로로 지시**.
33
+ - 스캔본(이미지) PDF·보안 걸린 파일은 못 읽을 수 있으니, 그때는 **텍스트로 붙여넣어** 달라고 요청합니다.
28
34
  - 업로드한 이력서/경력기술서/포트폴리오 본문을 `add_resume`로 저장합니다.
29
35
  - `kind`(resume / career_description / portfolio / other)와 `title`을 적절히 지정하고, 대표 문서는 `is_primary=true`로 설정합니다. 대표 문서는 이후 `get_application_context`의 `primary_resume`로 노출됩니다.
30
36
  - `source`는 `upload`를 사용합니다.
@@ -1,60 +1,112 @@
1
- # 지원 AI 앱 (Supported AI Apps)
1
+ # 지원 AI 앱 & 설치 레퍼런스 (Supported AI Apps)
2
2
 
3
- CareerMate는 LLM이 없는 **로컬 MCP 도구**입니다. 그래서 *내 컴퓨터에서 로컬 프로그램을 직접 실행할 수 있는* AI 앱에서만 동작합니다. 1차 지원(first-class) 앱은 다음 셋입니다.
3
+ CareerMate는 LLM이 없는 **로컬 MCP 도구**입니다. *내 컴퓨터에서 로컬 프로그램을 직접 실행할 수 있는* AI 앱에서만 동작합니다.
4
4
 
5
- | 앱 | 영구 지침 | MCP 등록 위치 |
6
- | --- | --- | --- |
7
- | **Claude Desktop** | — | `.mcpb` 번들 (원클릭) |
8
- | **Claude Code** | 루트 `CLAUDE.md` | 프로젝트 `.mcp.json` |
9
- | **Codex** (OpenAI Codex CLI) | 루트 `AGENTS.md` | `~/.codex/config.toml` |
5
+ > **가장 쉬운 사람용 설치 안내는 https://careermate.life (랜딩)** 입니다. 이 문서는 **지원 앱 매트릭스 + 클라이언트별 수동 MCP 설정 + 알려진 제약**을 모은 레퍼런스입니다. AI 에이전트가 따라가는 단계별 런북은 루트 [`/INSTALL.md`](../INSTALL.md), 설치 후 사용 흐름은 [docs/START_WORKFLOW.md](./START_WORKFLOW.md)를 보세요.
10
6
 
11
7
  ---
12
8
 
13
- ## Claude Desktop
9
+ ## 설치는 크게 3가지 — "누가 설치하느냐"
14
10
 
15
- Claude Desktop이 Node를 내장 제공하므로 미리 깔 것이 없습니다.
11
+ | | 무엇 | Node | 대상 |
12
+ |---|---|---|---|
13
+ | **1. 에이전트가 대신 설치** | 작업 폴더에서 **한 문장** → AI가 `careermate.life/llms-install.txt`를 읽고 npm 설치 + MCP 등록 | 필요(에이전트가 winget/brew 또는 nodejs.org로 도와줌) | Claude Code(데스크톱 **"Code" 탭** / CLI)·Codex CLI·Gemini CLI·Cursor 등 *명령 실행 가능한 에이전트* |
14
+ | **2. npm 명령 직접** | 터미널에 `npx -y careermate init` 한 줄(감지된 앱 자동 등록) | 필요 | Claude Desktop·Codex·Cursor 등(그 외는 `--print`로 수동) |
15
+ | **3. `.mcpb`** | 파일 하나를 설정에서 추가 | **불필요**(Claude Desktop 내장) | **Claude Desktop "채팅" 앱 전용** |
16
16
 
17
- 1. **Claude Desktop 설치**
18
- 2. **`careermate.mcpb` 받기** — Release `v0.1.0`에 첨부돼 있으나 저장소가 **비공개**라 접근 권한이 있어야 받을 수 있습니다. 그 외에는 소스에서 `npm run build:mcpb`로 직접 빌드하세요.
19
- 3. **더블클릭** (또는 Settings → Extensions 로 끌어다 놓기)
20
- 4. **Claude Desktop 재시작**
17
+ **1·2는 같은 메커니즘**(Node + npm/npx + MCP 등록) — 에이전트가 치느냐 / 내가 치느냐의 차이일 뿐입니다. 그래서 1·2의 "대상" 제품군이 서로 **겹칩니다**(같은 Claude Code·Codex·Cursor 등). 어느 앱이 어느 방식을 쓰는지는 아래 **앱 × 방식** 매트릭스로 보세요. **3(`.mcpb`)만 Claude Desktop "채팅" 앱 전용**이라 성격이 다릅니다(내장 Node 사용, 단 버전<22.5면 불안정).
21
18
 
22
- ---
23
-
24
- ## Claude Code
19
+ ### 앱 × 방식
25
20
 
26
- CareerMate 폴더 안에서 실행하는 것이 핵심입니다.
21
+ | 앱 | 1 에이전트 | 2 npm직접 | 3 .mcpb | 영구 지침 | MCP 등록 위치 |
22
+ |---|---|---|---|---|---|
23
+ | **Claude Desktop "채팅"** | — | ✅(config 등록) | ✅ | — | `.mcpb` 또는 `claude_desktop_config.json` |
24
+ | **Claude Code (Code 탭 / CLI)** | ✅ | ✅ | — | 루트 `CLAUDE.md` | 프로젝트 `.mcp.json` |
25
+ | **Codex CLI** (⚠️ 데스크톱 앱 ✗) | ✅ | ✅ | — | 루트 `AGENTS.md` | `~/.codex/config.toml` |
26
+ | **Gemini CLI / Antigravity** | ✅ | ✅ | — | — | Gemini CLI `~/.gemini/settings.json` · Antigravity `~/.gemini/.../mcp_config.json` |
27
+ | **Cursor · Cline · Windsurf** | ✅ | ✅ | — | — | `~/.cursor/mcp.json` 등 |
27
28
 
28
- 1. **Claude Code 실행** (CareerMate 폴더에서)
29
- 2. 프롬프트 입력:
30
- > "CareerMate를 설치하고 설정해줘. INSTALL.md를 따라 진행해줘."
29
+ > **Gemini CLI → Antigravity 전환(2026-06):** Google이 소비자(무료/Pro/Ultra)용 Gemini CLI를 **2026-06-18부터 Antigravity CLI로 전환**합니다(Gemini CLI repo는 오픈소스로 유지·엔터프라이즈 계속). MCP 스키마는 동일(`mcpServers` + `command`/`args`/`env`)합니다. `careermate init`은 **Gemini CLI(`~/.gemini/settings.json`)를 자동 등록**하고, **Antigravity는 설정 파일 경로가 유동적이라 후보 파일이 이미 있을 때만** 등록합니다(없으면 위 mcpServers 블록을 해당 파일에 수동 추가).
31
30
 
32
- 프로젝트 스코프 `.mcp.json`을 CareerMate 폴더에 등록하고, 첫 실행 때 **1회 승인**을 묻습니다. 승인하면 끝입니다.
31
+ ### 동작하지 않는 것
32
+ **ChatGPT · Gemini · Claude의 웹·모바일 앱은 안 됩니다.** 클라우드에서 실행되어 **원격 URL 기반 MCP만** 지원하므로, 내 컴퓨터의 로컬 stdio 서버에 직접 닿을 수 없습니다.
33
33
 
34
34
  ---
35
35
 
36
- ## Codex
36
+ ## 구독·준비물 (2026)
37
37
 
38
- OpenAI Codex CLI(로컬 코딩 에이전트)에서 동작합니다.
38
+ - **Claude Code**(Code 탭/CLI): **Pro 이상**(무료 플랜 불가) 또는 `ANTHROPIC_API_KEY` 종량제.
39
+ - **Codex**: **ChatGPT Plus 이상** 로그인으로 충분(API 키 불필요).
40
+ - **`.mcpb`**(Claude Desktop 채팅 확장): **무료 플랜에서도 설치** 가능(안정성이 변수).
41
+ - **터미널/npx**: 무료(Node만). 연결할 앱의 구독 정책은 별개.
39
42
 
40
- 1. **Codex 실행** (CareerMate 폴더에서)
41
- 2. 프롬프트 입력:
42
- > "CareerMate를 설치하고 설정해줘. INSTALL.md를 따라 진행해줘."
43
+ > **런타임.** CareerMate 서버는 Node 프로그램이라 **Node.js 22.5+ 가 필요**합니다 — 위 1·2 경로 공통(없으면 에이전트가 winget/brew, 안 되면 [nodejs.org](https://nodejs.org) LTS 설치 파일). **Node가 아예 필요 없는 길은 `.mcpb` 하나뿐**인데 그건 내장 Node 버전에 의존해 불안정할 수 있습니다 — **"완전 무설치 + 안정"을 동시에 주는 길은 아직 없습니다.**
44
+ >
45
+ > **Windows + Claude Code(Code 탭)**: [Git for Windows](https://git-scm.com/download/win) 사전 설치가 필요합니다(앱이 없으면 "Git is required" 안내). Claude Code가 **세션 격리(git worktree)와 내부 POSIX 셸 도구**에 Git을 쓰기 때문이며 CareerMate와는 무관합니다. macOS는 Git 기본 포함이라 불필요.
46
+
47
+ ---
43
48
 
44
- `~/.codex/config.toml`의 `[mcp_servers.careermate]` 항목을 등록합니다. Codex 안에서 `/mcp` 명령으로 연결을 확인하세요.
49
+ ## 작업 폴더 안에 설치 (권장 — GUI 앱·샌드박스 친화)
50
+
51
+ Claude "Code" 탭이나 Codex처럼 **작업 폴더(cwd)를 잡고** 쓰는 경우, 설치물과 데이터를 그 폴더 **안에** 두면 (1) 폴더만 백업·이동하면 데이터가 따라오고 (2) 샌드박스가 cwd 하위 쓰기를 허용해 마찰이 줄어듭니다. `<WS>`를 작업 폴더 절대경로라 할 때:
52
+
53
+ ```bash
54
+ npm install --prefix "<WS>/.cm/app" careermate
55
+ ```
56
+ 데이터는 `CAREERMATE_DATA_DIR` 환경변수로 작업 폴더 안에 고정합니다(기본값 `~/.careermate` 대신). 아래 MCP 등록의 `env`에 넣으면 됩니다:
57
+ ```
58
+ CAREERMATE_DATA_DIR = <WS>/.cm/data
59
+ ```
60
+ - `<BIN>` = `<WS>/.cm/app/node_modules/careermate/bin/careermate.mjs` (이 셸이 `--experimental-sqlite`를 알아서 붙임).
61
+ - 데이터 폴더 이름을 `.git`·`.codex`·`.agents`로 두지 마세요(앱이 읽기 전용으로 보호).
62
+ - **포터블 Node(고급):** 시스템에 Node 22.5+가 없으면, 작업 폴더 안에 포터블 Node를 두고 `command`를 그 `node` 절대경로로 가리켜도 동작합니다. 단 다운로드 용량·OS 보안 경고·폴더 이동 시 경로 갱신 부담이 있어, 지금은 "시스템 Node 1회 설치"가 더 단순합니다.
45
63
 
46
64
  ---
47
65
 
48
- ## 그 외 — 기타 MCP 클라이언트
66
+ ## 클라이언트별 수동 MCP 설정 (방식 2의 수동판)
67
+
68
+ 대부분 `npx -y careermate init`(또는 소스에서 `npm run init`)이 자동 등록하므로 **수동 설정은 보통 불필요**합니다. 직접 붙여넣어야 할 때만 참고하세요. `npm run init -- --print`로 아래 스니펫을 출력할 수도 있습니다.
49
69
 
50
- Cursor · Cline · Windsurf 등 로컬 stdio MCP를 지원하는 클라이언트도 동작합니다. CareerMate 폴더에서 `npm run init`(또는 설정만 출력하는 `npm run init -- --print`)으로 등록하세요.
70
+ **경로 규칙** — 아래 `<BIN>`은 **실제 절대경로**로 바꾸세요(`~`·`$HOME`은 설정 파일 안에서 확장 안 됨).
71
+ - **Windows**: JSON에서 역슬래시는 `\\`로 두 번(`C:\\...\\careermate.mjs`). TOML은 **작은따옴표 리터럴**(`'C:\...\careermate.mjs'`)을 쓰면 한 번만.
72
+ - **macOS/Linux**: `/` 그대로.
51
73
 
52
- ## 동작하지 않는 것
74
+ ### Claude Desktop (채팅앱 수동)
75
+ 설정 파일: Windows `%APPDATA%\Claude\claude_desktop_config.json` · macOS `~/Library/Application Support/Claude/claude_desktop_config.json` (Settings → Developer → Edit Config로도 열림)
76
+ ```json
77
+ { "mcpServers": { "careermate": { "command": "node", "args": ["<BIN>", "mcp"] } } }
78
+ ```
53
79
 
54
- **ChatGPT / Gemini 웹·모바일은 동작하지 않습니다.** 이들은 클라우드에서 실행되므로 내 컴퓨터의 **로컬 stdio MCP 서버에 닿을 수 없습니다.** 로컬에서 도는 Codex(CLI) · Claude Code · Claude Desktop만 CareerMate를 쓸 수 있습니다.
80
+ ### Claude Code (프로젝트 `.mcp.json`, 작업 폴더 최상단)
81
+ ```json
82
+ { "mcpServers": { "careermate": { "type": "stdio", "command": "node", "args": ["<BIN>", "mcp"], "env": { "CAREERMATE_DATA_DIR": "<WS>/.cm/data" } } } }
83
+ ```
84
+ 또는 CLI: `claude mcp add --scope project --transport stdio careermate -- node <BIN> mcp` · 첫 실행 시 **1회 승인**.
85
+
86
+ ### Codex CLI (`~/.codex/config.toml`)
87
+ ```toml
88
+ [mcp_servers.careermate]
89
+ command = "node"
90
+ args = ["<BIN>", "mcp"]
91
+ # 작업 폴더 안에 데이터: env = { CAREERMATE_DATA_DIR = "<WS>/.cm/data" }
92
+ ```
93
+ 또는 CLI: `codex mcp add careermate -- node <BIN> mcp` · 등록 후 `/mcp`로 확인.
94
+
95
+ ### Cursor / Cline / Windsurf
96
+ `~/.cursor/mcp.json`(전역) 또는 `.cursor/mcp.json`(프로젝트) 등에 위 Claude Code와 같은 `mcpServers` 블록. 추가 후 클라이언트 재시작.
97
+
98
+ > **소스를 클론한 개발자**는 `<BIN>` 대신 TS 소스를 직접 가리킬 수 있습니다(빌드 불필요):
99
+ > `node --no-warnings --experimental-sqlite --import tsx <careermate>/apps/mcp/src/index.ts mcp`
55
100
 
56
101
  ---
57
102
 
58
- 설치 절차 전체는 [/INSTALL.md](../INSTALL.md)(AI용 런북)를 따르세요. 클라이언트별 **수동 MCP 설정 스니펫**은 [docs/INSTALL.md](./INSTALL.md), 사람용 두 갈래 설치는 [docs/INSTALL_AND_USAGE.md](./INSTALL_AND_USAGE.md)를 참고하세요.
103
+ ## 알려진 제약 · 트러블슈팅
104
+
105
+ - **Codex 데스크톱 앱(⚠️ CLI 권장):** [#24718](https://github.com/openai/codex/issues/24718)(OPEN) 시작 시 전역 `config.toml`을 재작성해 등록이 사라질 수 있음 + [#13025](https://github.com/openai/codex/issues/13025)(OPEN) 프로젝트 `.codex/config.toml` 무시. 그래서 **데스크톱 앱은 비권장, CLI를 쓰세요.** 설치 단계 `npm install`은 기본 샌드박스에서 네트워크가 꺼져 있어 **1회 허용**(또는 `[sandbox_workspace_write] network_access = true`)이 필요. (단 **MCP 서버 자체는 샌드박스 밖에서 돌아** 연결 후 데이터 읽기/쓰기는 막히지 않습니다.)
106
+ - **Claude Desktop에서 등록이 사라짐:** 팀/클라우드 동기화가 로컬 MCP를 덮어쓴 이슈([#59368](https://github.com/anthropics/claude-code/issues/59368), 수정됨). 동기화를 끄거나 **CLI + 프로젝트 `.mcp.json`** 사용.
107
+ - **연결 검증:** `get_onboarding_status` 호출(Codex는 `/mcp`). 빠른 점검: `npx -y careermate doctor`(Node·데이터 폴더·DB 상태).
108
+ - **자세한 복구**(포트 충돌·DB 마이그레이션·완전 초기화): 루트 [`/INSTALL.md`](../INSTALL.md) 9단계.
109
+
110
+ ---
59
111
 
60
- 각 모드의 **영구 지침 파일**: Claude Code → [/CLAUDE.md](../CLAUDE.md), Codex → [/AGENTS.md](../AGENTS.md).
112
+ 각 모드의 **영구 지침 파일**: Claude Code → [/CLAUDE.md](../CLAUDE.md), Codex → [/AGENTS.md](../AGENTS.md). 사람용 설치 → https://careermate.life · 사용 흐름 → [docs/START_WORKFLOW.md](./START_WORKFLOW.md).
package/docs/WORKFLOWS.md CHANGED
@@ -7,7 +7,7 @@ CareerMate는 내부에 LLM을 두지 않는다. 분석과 작성은 사용자
7
7
  관련 문서:
8
8
 
9
9
  - 연결 직후 AI가 따라가는 시작 런북은 [시작 런북](./START_WORKFLOW.md)을 참고한다.
10
- - 설치·연결 절차는 [설치 가이드](./INSTALL.md)를 참고한다.
10
+ - 설치·연결 절차는 [설치 런북](../INSTALL.md)(또는 [지원 앱·설치 레퍼런스](./SUPPORTED_AI_APPS.md))을 참고한다.
11
11
  - MCP 도구 24종의 입출력 명세는 코드(`packages/mcp-tools/src/tools.ts`)에 정의되어 있으며, AI는 각 도구의 `inputSchema`로 직접 확인할 수 있다.
12
12
 
13
13
  ## 워크플로우 5종 개요
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careermate",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "CareerMate — an MCP-first, local-first career workspace. Connect it to a local AI agent (Claude Desktop, Claude Code, Codex); CareerMate stores your career data locally on your machine and exposes it over MCP.",
6
6
  "license": "MIT",
@@ -29,7 +29,6 @@
29
29
  "apps",
30
30
  "packages",
31
31
  "scripts",
32
- "install-page",
33
32
  "docs",
34
33
  "tsconfig.json",
35
34
  "README.md",
@@ -48,6 +47,8 @@
48
47
  "mcp": "node --no-warnings --experimental-sqlite --import tsx apps/mcp/src/index.ts",
49
48
  "migrate": "node --no-warnings --experimental-sqlite --import tsx scripts/migrate.ts",
50
49
  "seed": "node --no-warnings --experimental-sqlite --import tsx scripts/seed.ts",
50
+ "shots:seed": "node --no-warnings --experimental-sqlite --import tsx scripts/shots-seed.ts",
51
+ "shots": "node --no-warnings --experimental-sqlite --import tsx scripts/shots.ts",
51
52
  "doctor": "node --no-warnings --experimental-sqlite --import tsx scripts/doctor.ts",
52
53
  "test": "node --no-warnings scripts/run.mjs scripts/test.ts \"TEST_VERDICT PASS\"",
53
54
  "test:ui": "node --no-warnings scripts/run.mjs scripts/ui-smoke.ts \"UI_VERDICT PASS\"",
@@ -20,6 +20,12 @@ import {
20
20
  type DocumentRecord,
21
21
  type ProfileInput,
22
22
  type ProfileRecord,
23
+ type ExperienceInput,
24
+ type ExperienceRecord,
25
+ type ProjectInput,
26
+ type ProjectRecord,
27
+ type SkillInput,
28
+ type SkillRecord,
23
29
  APPLICATION_STATUS_LABELS,
24
30
  INTERVIEW_UNLOCK_STATUSES,
25
31
  } from '@careermate/shared';
@@ -31,6 +37,9 @@ import {
31
37
  interviewRepo,
32
38
  documentRepo,
33
39
  profileRepo,
40
+ experienceRepo,
41
+ projectRepo,
42
+ skillRepo,
34
43
  activityRepo,
35
44
  } from '@careermate/db';
36
45
 
@@ -50,6 +59,30 @@ export function addResume(input: Omit<DocumentInput, 'kind'> & { kind?: Document
50
59
  return doc;
51
60
  }
52
61
 
62
+ /* ----------------------------------------------- Experiences / Projects / Skills */
63
+
64
+ // These structured-profile entities have no dedicated activity type; the dashboard
65
+ // logs them under 'profile_updated' (see apps/web routes), so the MCP path matches.
66
+ // Summaries carry only company/project/skill names — never résumé body text.
67
+
68
+ export function addExperience(input: ExperienceInput): ExperienceRecord {
69
+ const exp = experienceRepo.add(input);
70
+ activityRepo.log('profile_updated', `경력(${exp.company})을 추가했습니다.`, 'experience', exp.id);
71
+ return exp;
72
+ }
73
+
74
+ export function addProject(input: ProjectInput): ProjectRecord {
75
+ const prj = projectRepo.add(input);
76
+ activityRepo.log('profile_updated', `프로젝트(${prj.name})를 추가했습니다.`, 'project', prj.id);
77
+ return prj;
78
+ }
79
+
80
+ export function addSkill(input: SkillInput): SkillRecord {
81
+ const skill = skillRepo.add(input);
82
+ activityRepo.log('profile_updated', `기술(${skill.name})을 추가했습니다.`, 'skill', skill.id);
83
+ return skill;
84
+ }
85
+
53
86
  /* -------------------------------------------------------------------- Jobs */
54
87
 
55
88
  /** Save (or update) a posting and make sure an application row tracks it. */
@@ -139,7 +172,15 @@ export function updateApplicationStatus(
139
172
  export function saveInterviewPrep(input: InterviewPrepInput): InterviewPrepRecord {
140
173
  const job = jobRepo.get(input.job_id);
141
174
  if (!job) throw new Error(`공고를 찾을 수 없습니다: ${input.job_id}`);
142
- applicationRepo.ensure(job.id);
175
+ // Spec gate: interview prep is only allowed once the application has reached
176
+ // 서류 합격(document_passed) or beyond. Enforce it server-side, not just in prompts.
177
+ const app = applicationRepo.getByJob(job.id);
178
+ if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
179
+ const current = app ? APPLICATION_STATUS_LABELS[app.status] : '미지원';
180
+ throw new Error(
181
+ `면접 준비는 '서류 합격' 이상 상태에서만 저장할 수 있습니다. 현재 상태: '${current}'. 먼저 update_application_status로 상태를 올린 뒤 다시 시도하세요.`,
182
+ );
183
+ }
143
184
  const prep = interviewRepo.save(input);
144
185
  activityRepo.log('interview_prep_saved', `${job.company} · ${job.position} 면접 준비 자료를 저장했습니다.`, 'interview_prep', prep.id);
145
186
  return prep;
@@ -28,6 +28,36 @@ export function closeDb(): void {
28
28
  }
29
29
  }
30
30
 
31
+ let _inTx = false;
32
+
33
+ /**
34
+ * Run `fn` inside a single transaction so multi-statement writes are atomic:
35
+ * any thrown error rolls back every statement instead of leaving a half-written
36
+ * row. node:sqlite has no transaction() helper, so we drive BEGIN/COMMIT/ROLLBACK
37
+ * directly. Re-entrant: a nested tx() call joins the outer transaction (SQLite
38
+ * has no nested transactions), so only the outermost call commits or rolls back.
39
+ */
40
+ export function tx<T>(fn: () => T): T {
41
+ if (_inTx) return fn();
42
+ const db = getDb();
43
+ db.exec('BEGIN');
44
+ _inTx = true;
45
+ try {
46
+ const result = fn();
47
+ db.exec('COMMIT');
48
+ return result;
49
+ } catch (err) {
50
+ try {
51
+ db.exec('ROLLBACK');
52
+ } catch {
53
+ /* ignore rollback failure; surface the original error */
54
+ }
55
+ throw err;
56
+ } finally {
57
+ _inTx = false;
58
+ }
59
+ }
60
+
31
61
  /** Helpers for the JSON-text columns. */
32
62
  export function toJson(value: unknown): string {
33
63
  return JSON.stringify(value ?? null);
@@ -19,4 +19,5 @@ export {
19
19
  applicationRepo,
20
20
  interviewRepo,
21
21
  activityRepo,
22
+ getEntityCounts,
22
23
  } from './repositories.ts';