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
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# CareerMate
|
|
2
|
+
|
|
3
|
+
**MCP 우선(MCP-first) 로컬 커리어 관리 도구.** 내 컴퓨터에서 동작하는 AI 에이전트(Claude Desktop·Claude Code·Codex 등)를 인터페이스로 쓰고, CareerMate는 당신의 커리어 데이터를 **이 컴퓨터에만** 저장한 뒤 MCP로 노출합니다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 핵심 철학
|
|
8
|
+
|
|
9
|
+
- **MCP 우선** — 새 앱 사용법을 익힐 필요가 없습니다. 내 컴퓨터에서 동작하는 AI 에이전트(Claude Desktop·Claude Code·Codex 등 로컬 MCP 클라이언트)와 대화하면, 그 AI가 CareerMate의 로컬 MCP 도구를 호출해 당신의 커리어 DB를 읽고 씁니다.
|
|
10
|
+
- **로컬 우선** — 모든 데이터는 당신의 컴퓨터에만 저장됩니다(`~/.careermate`). 외부 서버로 전송하지 않습니다. 대시보드 웹 서버는 `127.0.0.1`(이 컴퓨터)에만 연결됩니다.
|
|
11
|
+
- **LLM 비내장** — CareerMate 안에는 AI가 들어 있지 않습니다. 분석·글쓰기 같은 “생각하는 일”은 **당신의 AI**가 하고, CareerMate는 데이터를 **안전하게 보관·조회**하는 역할만 합니다.
|
|
12
|
+
|
|
13
|
+
> 쉽게 말해: 당신의 AI가 “두뇌”, CareerMate는 그 두뇌가 꺼내 쓰는 “커리어 서랍장”입니다.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 무엇을 하나요?
|
|
18
|
+
|
|
19
|
+
프로필·이력서·자기소개서·채용공고·지원 현황·면접 준비를 로컬 SQLite에 구조화해 저장하고, AI 어시스턴트가 MCP 도구로 그 데이터를 읽고 씁니다. 전형적인 흐름은 이렇습니다.
|
|
20
|
+
|
|
21
|
+
1. 프로필·이력서 등록 → 2. 채용공고 저장·파싱 → 3. 적합도(핏) 분석 → 4. 맞춤 자기소개서 작성·버전 관리 → 5. 지원 상태 관리 → 6. 면접 준비
|
|
22
|
+
|
|
23
|
+
언제든 대시보드를 열어 저장된 데이터를 눈으로 확인할 수 있습니다. 자세한 단계별 런북은 [`docs/START_WORKFLOW.md`](docs/START_WORKFLOW.md)를 참고하세요.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 주요 기능
|
|
28
|
+
|
|
29
|
+
- **AI와 대화로 모든 작업** — MCP 도구 24개로 온보딩·프로필·이력서·자소서·공고·핏 분석·지원 상태·면접 준비·AI 티 안 나는 글쓰기까지 처리.
|
|
30
|
+
- **지원 상태 8단계 관리** — `draft`(작성 중) · `planned`(지원 예정) · `applied`(지원 완료) · `document_passed`(서류 합격) · `interview`(면접 진행) · `final_passed`(최종 합격) · `rejected`(불합격) · `on_hold`(보류). `document_passed` 이상에서 면접 준비가 해금됩니다.
|
|
31
|
+
- **자기소개서 버전 관리** — 공고별로 자소서 버전을 쌓고 타임라인으로 비교, 파일로 내보내기.
|
|
32
|
+
- **채용공고 파싱** — 붙여넣은 공고 텍스트를 구조화해 저장.
|
|
33
|
+
- **로컬 대시보드** — 프레임워크·CDN 없는 바닐라 JS로 만든 7페이지 웹 화면(다크모드 지원).
|
|
34
|
+
- **데이터 내보내기/삭제** — Settings 페이지에서 직접.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 구성
|
|
39
|
+
|
|
40
|
+
두 개의 로컬 프로세스가 같은 데이터베이스를 공유합니다.
|
|
41
|
+
|
|
42
|
+
| 프로세스 | 실행 | 역할 |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| **대시보드 웹 서버** | `npm start` | `http://127.0.0.1:4319` — 내 데이터를 눈으로 확인·관리. `127.0.0.1`에만 바인딩. |
|
|
45
|
+
| **MCP 서버** | `npm run mcp` | stdio 기반. 보통 AI 클라이언트가 자동 실행. 도구 24개 제공. |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 빠른 시작
|
|
50
|
+
|
|
51
|
+
### 요구사항
|
|
52
|
+
|
|
53
|
+
- **Node.js >= 22.5.0** — 내장 `node:sqlite`를 사용하므로 컴파일러나 네이티브 빌드가 **필요 없습니다**.
|
|
54
|
+
- 버전 확인:
|
|
55
|
+
```bash
|
|
56
|
+
node --version
|
|
57
|
+
```
|
|
58
|
+
`v22.5.0` 미만이거나 명령을 찾지 못하면 https://nodejs.org 에서 최신 LTS를 설치하세요.
|
|
59
|
+
|
|
60
|
+
### 1) 설치
|
|
61
|
+
|
|
62
|
+
빌드 단계가 없습니다. TypeScript는 `tsx`로 바로 실행됩니다.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2) 대시보드 실행
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm start
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- 기본 주소는 `http://127.0.0.1:4319` 이며 브라우저가 자동으로 열립니다.
|
|
75
|
+
- 포트가 사용 중이면 자동으로 다음 빈 포트로 폴백하니, 터미널에 출력된 실제 주소를 확인하세요.
|
|
76
|
+
- 종료는 터미널에서 `Ctrl+C`.
|
|
77
|
+
|
|
78
|
+
### 3) 데모 데이터로 둘러보기(선택)
|
|
79
|
+
|
|
80
|
+
빈 화면이 낯설다면 예시 데이터를 넣어 기능을 미리 체험할 수 있습니다.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm run seed
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4) AI(MCP)에 연결
|
|
87
|
+
|
|
88
|
+
CareerMate는 **내 컴퓨터에서 로컬 stdio MCP 서버를 띄울 수 있는 AI**에서 동작합니다. (ChatGPT·Gemini의 웹/앱은 클라우드에서 동작해 로컬 stdio MCP 서버에 직접 연결할 수 없습니다.)
|
|
89
|
+
|
|
90
|
+
v1에서 권장하는 연결 방식은 셋입니다. 아래 순서대로 골라 쓰세요.
|
|
91
|
+
|
|
92
|
+
#### ① Claude Desktop 연결 (.mcpb 원클릭)
|
|
93
|
+
|
|
94
|
+
터미널·Node 없이 가장 간단하게 붙이는 **하나의 연결 방법**입니다. `dist/careermate.mcpb`(`npm run build:mcpb`로 생성)를 더블클릭하거나 Claude Desktop의 **Settings → Extensions**로 끌어다 놓고, Claude Desktop을 재시작합니다.
|
|
95
|
+
|
|
96
|
+
#### ② Claude Code
|
|
97
|
+
|
|
98
|
+
CareerMate 폴더에서 Claude Code를 실행한 뒤, 이렇게 시켜 주세요.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
CareerMate를 설치하고 설정해줘. INSTALL.md를 따라 진행해줘.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Claude Code가 [`INSTALL.md`](INSTALL.md) 런북을 따라 프로젝트 범위 `.mcp.json`을 쓰고 MCP 서버를 등록합니다. 첫 실행 시 프로젝트 서버에 대한 **일회성 승인**만 눌러 주면 됩니다.
|
|
105
|
+
|
|
106
|
+
#### ③ Codex (OpenAI Codex CLI)
|
|
107
|
+
|
|
108
|
+
CareerMate 폴더에서 Codex를 실행한 뒤, 같은 프롬프트로 시작합니다.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
CareerMate를 설치하고 설정해줘. INSTALL.md를 따라 진행해줘.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Codex가 `~/.codex/config.toml`에 MCP 서버를 등록합니다. 연결은 Codex 안에서 `/mcp` 명령으로 확인하세요.
|
|
115
|
+
|
|
116
|
+
> **기타 MCP 클라이언트(Cursor 등)** — Cursor·Cline·Windsurf 같은 로컬 stdio MCP 클라이언트는 `npm run init`(또는 `npm run init -- --print`)으로 등록할 수 있습니다.
|
|
117
|
+
|
|
118
|
+
연결 후 AI 클라이언트를 **완전히 재시작**하고, `get_onboarding_status`를 호출해 달라고 시켜 연결을 검증하세요. 자세한 절차는 AI용 런북 **[`INSTALL.md`](INSTALL.md)**와 사람용 설치 가이드 **[`docs/INSTALL_AND_USAGE.md`](docs/INSTALL_AND_USAGE.md)**를 참고하세요.
|
|
119
|
+
|
|
120
|
+
> **가장 간단한 길 — npx:** `careermate`가 npm에 게시되어 클론 없이 `npx -y careermate init` 한 줄로 등록됩니다. 소스 폴더에서 작업 중이면 `npm install` 후 `npm run init`을 써도 됩니다.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 대시보드 7페이지
|
|
125
|
+
|
|
126
|
+
| 페이지 | 설명 |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| **Home** | 현재 상태·다음 할 일 한눈에. |
|
|
129
|
+
| **Profile** | 프로필, 경력(experiences), 프로젝트, 스킬 관리. |
|
|
130
|
+
| **Jobs** | 저장한 채용공고 목록과 상세(공고 내용·핏 분석). |
|
|
131
|
+
| **Applications** | 지원 현황을 8단계 상태로 보는 칸반 보드. |
|
|
132
|
+
| **Documents** | 자기소개서 버전 타임라인과 이력서. |
|
|
133
|
+
| **Interview** | 면접 준비 자료(`document_passed` 이상에서 해금). |
|
|
134
|
+
| **Settings** | 데이터 내보내기·삭제, 환경 정보. |
|
|
135
|
+
|
|
136
|
+
다크모드를 지원합니다.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## MCP 도구 한눈에 (24개)
|
|
141
|
+
|
|
142
|
+
| 분류 | 도구 |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| 온보딩 | `get_onboarding_status` · `start_onboarding` |
|
|
145
|
+
| 프로필 | `save_profile` · `get_profile` · `update_profile` |
|
|
146
|
+
| 이력서 | `add_resume` · `get_resumes` |
|
|
147
|
+
| 자기소개서 | `add_cover_letter` · `get_cover_letters` · `save_cover_letter_version` · `export_cover_letter` |
|
|
148
|
+
| 채용공고 | `save_job_posting` · `get_job_posting` · `list_jobs` · `parse_job_posting` |
|
|
149
|
+
| 핵심 컨텍스트 | **`get_application_context`** (지원에 필요한 맥락을 한 번에 모아줌) |
|
|
150
|
+
| 핏 분석 | `save_fit_analysis` |
|
|
151
|
+
| 지원 상태 | `update_application_status` |
|
|
152
|
+
| 면접 준비 | `save_interview_prep` |
|
|
153
|
+
| 글쓰기 | `get_writing_style_guide` (AI 티 안 나는 한국어 글쓰기 규칙) |
|
|
154
|
+
| 대시보드/활동 | `open_dashboard` · `open_application` · `list_recent_activity` · `get_workflow_guide` |
|
|
155
|
+
|
|
156
|
+
이 도구들 위에 5종의 워크플로우(`onboarding`, `analyze_job`, `write_cover_letter`, `manage_application_status`, `prepare_interview`)가 정의되어 있어, AI가 단계별로 자연스럽게 안내합니다.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 프로젝트 구조
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
CareerMate/
|
|
164
|
+
├─ apps/
|
|
165
|
+
│ ├─ web/ # 대시보드 + 로컬 API 서버 (npm start)
|
|
166
|
+
│ └─ mcp/ # MCP stdio 서버 (npm run mcp)
|
|
167
|
+
├─ packages/
|
|
168
|
+
│ ├─ shared/ # 공용 타입·zod 스키마·유틸
|
|
169
|
+
│ ├─ db/ # node:sqlite DB 접근·스키마·마이그레이션
|
|
170
|
+
│ ├─ core/ # 도메인 유스케이스
|
|
171
|
+
│ ├─ mcp-tools/ # MCP 도구 24개 정의
|
|
172
|
+
│ ├─ exporters/ # 내보내기(자소서 등)
|
|
173
|
+
│ ├─ parsers/ # 채용공고 파싱
|
|
174
|
+
│ ├─ prompts/ # 프롬프트·안내 문구
|
|
175
|
+
│ └─ workflows/ # 워크플로우 5종
|
|
176
|
+
├─ install-page/ # 설치 안내 페이지
|
|
177
|
+
├─ docs/ # 문서
|
|
178
|
+
├─ scripts/ # migrate / seed / doctor / test 등
|
|
179
|
+
└─ package.json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- **스택**: TypeScript(ESM), 무빌드 실행(`tsx`), 내장 `node:sqlite`(네이티브 컴파일 없음), `zod`, `@modelcontextprotocol/sdk`. 대시보드는 프레임워크·CDN 없는 바닐라 JS + 자체 CSS 디자인 시스템.
|
|
183
|
+
- **데이터 저장소**: 12개 테이블(profile, experiences, projects, skills, documents, cover_letters, cover_letter_versions, jobs, fit_analyses, applications, interview_preps, activities). 두 프로세스(대시보드·MCP)가 **같은 SQLite DB**를 공유합니다.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## npm 스크립트
|
|
188
|
+
|
|
189
|
+
| 스크립트 | 설명 |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `npm install` | 의존성 설치 (빌드 없음) |
|
|
192
|
+
| `npm start` | 대시보드 웹 서버 실행 |
|
|
193
|
+
| `npm run dev` | 대시보드 실행 (파일 변경 시 자동 재시작) |
|
|
194
|
+
| `npm run mcp` | MCP 서버 실행 (stdio) |
|
|
195
|
+
| `npm run migrate` | DB 생성/업그레이드 |
|
|
196
|
+
| `npm run doctor` | 설치/환경 점검 |
|
|
197
|
+
| `npm run seed` | 예시 데이터 삽입 |
|
|
198
|
+
| `npm test` | E2E 테스트 실행 |
|
|
199
|
+
| `npm run test:ui` | Playwright UI 스모크 테스트 |
|
|
200
|
+
| `npm run typecheck` | 타입 검사 (`tsc --noEmit`) |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 데이터 위치 & 환경변수
|
|
205
|
+
|
|
206
|
+
- 기본 폴더: `~/.careermate` (Windows: `%USERPROFILE%\.careermate`).
|
|
207
|
+
- `careermate.sqlite` — 데이터베이스 파일
|
|
208
|
+
- `exports/` — 내보낸 파일
|
|
209
|
+
- `backups/` — 백업
|
|
210
|
+
- `uploads/` — 업로드 파일
|
|
211
|
+
- `server.json` — 실행 중 핸드셰이크 정보
|
|
212
|
+
- 환경변수로 동작을 바꿀 수 있습니다.
|
|
213
|
+
- `CAREERMATE_DATA_DIR` — 데이터 폴더 위치 변경
|
|
214
|
+
- `CAREERMATE_PORT` — 대시보드 포트 고정
|
|
215
|
+
- `CAREERMATE_NO_OPEN` — 시작 시 브라우저 자동 열기 끄기
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 보안 / 프라이버시
|
|
220
|
+
|
|
221
|
+
- **로컬 전용 바인딩** — 대시보드 서버는 `127.0.0.1`(loopback)에만 바인딩되어 외부에서 접근할 수 없습니다.
|
|
222
|
+
- **DNS 리바인딩 차단** — Host 허용목록으로 검증.
|
|
223
|
+
- **변경 요청 보호** — CSRF 세션 토큰(HTML `meta`로 주입), 외부 Origin 차단.
|
|
224
|
+
- **정적 파일 보호** — 경로 traversal(상위 폴더 탈출) 차단.
|
|
225
|
+
- **본문 크기 제한** — 요청 본문 8MB 제한.
|
|
226
|
+
- **민감 정보 비노출** — 이력서·자기소개서 본문은 로그나 에러 응답에 노출되지 않습니다.
|
|
227
|
+
- **외부 전송 없음** — MCP 서버는 네트워크 호출을 하지 않으며, 모든 데이터는 당신의 컴퓨터에만 남습니다. 내보내기·삭제는 대시보드 **Settings**에서 직접 할 수 있습니다.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 문서
|
|
232
|
+
|
|
233
|
+
- [`INSTALL.md`](INSTALL.md) — AI 어시스턴트가 따라가는 설치·연결 런북(소스 직접 실행 기준).
|
|
234
|
+
- [`AGENTS.md`](AGENTS.md) — Codex용 영속 지침. / [`CLAUDE.md`](CLAUDE.md) — Claude Code용 영속 지침.
|
|
235
|
+
- [`docs/SUPPORTED_AI_APPS.md`](docs/SUPPORTED_AI_APPS.md) — 지원하는 AI 앱과 연결 방식 정리.
|
|
236
|
+
- [`docs/INSTALL_AND_USAGE.md`](docs/INSTALL_AND_USAGE.md) — 사람용 두 트랙 설치(.mcpb 원클릭 / 터미널)와 설치 후 사용법.
|
|
237
|
+
- [`docs/START_WORKFLOW.md`](docs/START_WORKFLOW.md) — 등록부터 면접 준비까지 단계별 작업 런북.
|
|
238
|
+
- [`docs/TODO.md`](docs/TODO.md) — 남은 작업과 v1 범위 밖 항목.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 테스트
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
npm test # E2E 테스트 (보안·업무흐름·MCP stdio·대시보드↔MCP 동일 DB 양방향)
|
|
246
|
+
npm run test:ui # 대시보드 페이지 렌더 스모크 (Playwright)
|
|
247
|
+
npm run typecheck
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
기타 유용한 스크립트: `npm run migrate`(DB 생성/업그레이드), `npm run seed`(예시 데이터), `npm run doctor`(설치·환경 점검).
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 라이선스
|
|
255
|
+
|
|
256
|
+
MIT.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
CareerMate에 포함된 일부 콘텐츠는 제3자 오픈소스에서 파생되었습니다. 해당 라이선스 고지를 아래에 보존합니다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## im-not-ai (한국어 "AI 티 안 나는 글쓰기" 분류)
|
|
8
|
+
|
|
9
|
+
- 출처: `epoko77-ai/im-not-ai` ("AI가 쓴 글이 아닌 것처럼 윤문해주는 스킬")
|
|
10
|
+
- 라이선스: MIT
|
|
11
|
+
- 사용 범위: `packages/prompts/src/humanize.ts`의 `HUMANIZE_WRITING_GUIDE`는 위 프로젝트의
|
|
12
|
+
한국어 AI-tell 분류(번역투·기계적 병렬·클리셰·상투적 연결어·균일한 문장 리듬 등)를
|
|
13
|
+
자기소개서·커리어 글쓰기용으로 압축·각색한 파생 저작물입니다. 원문을 그대로 복제하지 않았습니다.
|
|
14
|
+
|
|
15
|
+
원본 라이선스(MIT) 전문:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
MIT License
|
|
19
|
+
|
|
20
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
21
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
22
|
+
in the Software without restriction, including without limitation the rights
|
|
23
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
24
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
25
|
+
furnished to do so, subject to the following conditions:
|
|
26
|
+
|
|
27
|
+
The above copyright notice and this permission notice shall be included in all
|
|
28
|
+
copies or substantial portions of the Software.
|
|
29
|
+
|
|
30
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
31
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
32
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
33
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
34
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
35
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
36
|
+
SOFTWARE.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> 참고: 원 저장소의 정확한 저작권자 표기(`Copyright (c) <year> <holder>`)를 확인해
|
|
40
|
+
> 위 전문 상단에 그대로 채워 넣는 것을 권장합니다.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CareerMate MCP server (stdio).
|
|
3
|
+
*
|
|
4
|
+
* Launched by the AI client (Claude Desktop, ChatGPT, Cursor, …). It registers
|
|
5
|
+
* every CareerMate tool and talks to the SAME local SQLite database the
|
|
6
|
+
* dashboard uses, so anything the AI writes appears in the dashboard instantly,
|
|
7
|
+
* and anything the user edits in the dashboard is visible to the AI.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here calls out to the network. All reasoning stays in the user's AI.
|
|
10
|
+
*/
|
|
11
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import { getDb, getDataDir } from '@careermate/db';
|
|
14
|
+
import { TOOLS, toCallToolResult } from '@careermate/mcp-tools';
|
|
15
|
+
|
|
16
|
+
const APP_VERSION = '0.1.0';
|
|
17
|
+
|
|
18
|
+
async function main(): Promise<void> {
|
|
19
|
+
// Ensure the DB is created/migrated before serving any tool call.
|
|
20
|
+
getDb();
|
|
21
|
+
|
|
22
|
+
const server = new McpServer(
|
|
23
|
+
{ name: 'careermate', version: APP_VERSION },
|
|
24
|
+
{
|
|
25
|
+
instructions:
|
|
26
|
+
'CareerMate는 사용자의 로컬 커리어 데이터베이스입니다. 공고 분석이나 자기소개서 작성 전에는 항상 get_application_context를 먼저 호출해 사용자 정보를 가져오고, 결과는 save_fit_analysis / save_cover_letter_version 등으로 다시 저장하세요. 분석과 작성은 당신(AI)이 수행하고, CareerMate는 데이터를 제공·보관합니다. 모든 데이터는 사용자 로컬에만 저장됩니다.',
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
for (const tool of TOOLS) {
|
|
31
|
+
server.registerTool(
|
|
32
|
+
tool.name,
|
|
33
|
+
{
|
|
34
|
+
title: tool.title,
|
|
35
|
+
description: tool.description,
|
|
36
|
+
inputSchema: tool.inputSchema,
|
|
37
|
+
annotations: {
|
|
38
|
+
title: tool.title,
|
|
39
|
+
readOnlyHint: tool.readOnly ?? false,
|
|
40
|
+
openWorldHint: false,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async (args: unknown) => {
|
|
44
|
+
try {
|
|
45
|
+
const result = await tool.handler(args ?? {});
|
|
46
|
+
return toCallToolResult(result);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Never leak document bodies; return a safe message.
|
|
49
|
+
const message = err instanceof Error ? err.message : '도구 실행 중 오류가 발생했습니다.';
|
|
50
|
+
return toCallToolResult({ text: message, isError: true });
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const transport = new StdioServerTransport();
|
|
57
|
+
await server.connect(transport);
|
|
58
|
+
|
|
59
|
+
// IMPORTANT: never write to stdout — it is the MCP transport. Logs go to stderr.
|
|
60
|
+
console.error(`[careermate-mcp] 연결됨 · 도구 ${TOOLS.length}개 · 데이터: ${getDataDir()}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main().catch((err) => {
|
|
64
|
+
console.error('[careermate-mcp] 시작 실패:', err instanceof Error ? err.message : err);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# CareerMate 대시보드 — 디자인 가이드 (안티-AI-슬롭 규칙집)
|
|
2
|
+
|
|
3
|
+
이 문서는 CareerMate 웹 대시보드의 **디자인 원칙과 컨벤션**입니다. `UI_CONTRACT.md`가 "페이지를 *어떻게* 짜는가(컴포넌트·API 계약)"라면, 이 문서는 "결과물이 *어떻게 보여야 하는가*"를 규정합니다. **새 화면을 만들거나 기존 화면을 고치기 전에 반드시 읽고**, 마지막의 감사 체크리스트로 자가 점검하세요.
|
|
4
|
+
|
|
5
|
+
근거: Anthropic `frontend-design`/`interface-design` 스킬, Linear·Vercel(Geist)·Notion·Stripe·shadcn 디자인 가이드, Refactoring UI, Polaris, NN/g. (출처는 맨 아래.)
|
|
6
|
+
|
|
7
|
+
> 북극성: **"누가 봐도 AI가 만든 것 같다"의 반대.** 차분하고 밀도 있고, 정보가 먼저 오고, 장식이 아니라 위계로 말한다. Linear/Notion/Raycast의 절제.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0. AI 슬롭 5대 증상 (이 프로젝트에서 실제로 발견된 것)
|
|
12
|
+
|
|
13
|
+
1. **제목 중복** — 같은 단어가 사이드바 active + 상단바 + 페이지 `<h1>`로 3번.
|
|
14
|
+
2. **버튼 중복** — 동일한 1차 액션("공고 추가")이 상단바 + 페이지헤더 + 빈 상태에 동시에.
|
|
15
|
+
3. **필러 설명문** — 제목을 그대로 풀어 쓴 "…를 한눈에 보고 …확인/관리하세요".
|
|
16
|
+
4. **반복 보일러플레이트** — "ChatGPT/Claude에게 …하면 자동으로 저장됩니다"가 화면마다.
|
|
17
|
+
5. **과한 카드화** — 빈 상태 하나도 카드로 감싸고, 카드 안에 카드.
|
|
18
|
+
|
|
19
|
+
이 5가지가 보이면 슬롭입니다. 아래 규칙으로 제거합니다.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. 핵심 규칙 (Rulebook)
|
|
24
|
+
|
|
25
|
+
1. **제목은 한 곳에만 — 상단바.** 현재 화면 제목은 **앱 셸 상단바**가 한 번만 표시한다(사이드바 active는 위치 표시일 뿐, 콘텐츠에서 제목을 반복하지 않는다). 인덱스 페이지(채용공고/지원현황/문서/면접/프로필)에는 in-content `PageHead` `<h1>`·설명을 **두지 않고** 곧장 콘텐츠로 시작. 예외: 상세(`jobs/:id`)는 회사명을 in-content `<h1>`으로 가진다(nav가 못 보여주는 콘텐츠).
|
|
26
|
+
2. **1차 액션은 한 개, 한 곳(상단바 `ctx.setActions`).** `PageHead({actions})`·빈 상태의 중복 버튼 금지. 리스트가 비었을 때만 빈 상태에 생성 CTA를 두고, 그 핸들러는 상단바와 동일.
|
|
27
|
+
3. **필러 설명 금지.** 설명 줄은 *수치·제약·상태·자명하지 않은 이유*를 말할 때만 허용. 제목을 바꿔 말하면 삭제. **"한눈에" 금지.**
|
|
28
|
+
4. **설정/MCP 안내는 단 한 곳(홈 '시작하기' 카드)에만.** 빈 상태는 화면별 **고유 한 문장**. "ChatGPT/Claude에게 …저장됩니다" 반복·`MCP_HINT` 공용 상수 금지.
|
|
29
|
+
5. **위계는 타이포·여백으로, 박스로 X.** 단일 빈 상태/단일 블록을 `.card`로 감싸지 않는다. 카드는 **반복·선택 가능한 객체**(공고 행, 자소서, 보드 카드)에만. **카드 in 카드 금지.**
|
|
30
|
+
6. **악센트(인디고)는 절제.** 1차 버튼·링크·포커스 링·active nav에만. **장식용 보라 그라데이션 금지** (`.sidebar__logo`, `.progress__bar`의 `135deg accent→#7c3aed`는 평면 `var(--accent)`로). 코드에 raw hex 금지 — 색은 CSS 변수로만.
|
|
31
|
+
7. **버튼 라벨은 동사+명사, 흐름 내내 일관.** 모달 저장 버튼은 `저장`이 아니라 **`공고 저장`/`경력 저장`**. 생성 라벨과 성공 토스트의 명사가 일치(`공고 저장`→`공고를 저장했습니다`). 맨동사는 `취소`·`닫기`·`복사`만.
|
|
32
|
+
8. **인덱스 페이지는 툴바+행으로 직행.** 설명 헤더 블록은 settings/profile/온보딩/상세에만. 상세(`jobs/:id`)의 회사명 `<h1>`은 nav가 못 보여주는 *콘텐츠*이므로 유일한 정당한 예외.
|
|
33
|
+
9. **숫자 열은 우정렬 + `.tnum`.** 적합도 점수·파일 크기 등은 `text-align:right`로 자릿수를 정렬.
|
|
34
|
+
10. **여백은 스케일에 스냅.** 4/8/12/16/24/32. 페이지 모듈의 일회성 인라인 px(`'9px'`, `'8px 0'`, `'440px'`…) 금지 — `.stack-*`/`.gap-*`/`.mt-*` 유틸 또는 토큰 사용.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. 감사 체크리스트 (커밋 전 grep)
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
한눈에 → 0건이어야 함 (필러 설명 시그니처)
|
|
42
|
+
확인하세요|관리하세요|관리하고 → 제목 재진술 설명이면 삭제
|
|
43
|
+
ChatGPT/Claude|자동으로 저장 → 최대 1건(홈 시작하기 카드)
|
|
44
|
+
MCP_HINT → 삭제 (화면별 고유 문구로)
|
|
45
|
+
공고 추가 (단일 1차 라벨) → 한 화면에 1번만
|
|
46
|
+
Btn('저장' → 동사+명사로 (공고 저장 등)
|
|
47
|
+
linear-gradient (styles.css) → 장식 보라 그라데이션 2건 제거
|
|
48
|
+
#7c3aed / pages 내 raw hex → CSS 변수로
|
|
49
|
+
Card( 가 EmptyState( 를 감쌈 → 카드 제거
|
|
50
|
+
class:'card' in card body → 카드 in 카드 제거
|
|
51
|
+
PageHead( (인덱스 페이지) → 제거 (상단바 제목으로 충분; 홈 인사·jobs 상세만 허용)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
추가 육안 테스트(페이지마다):
|
|
55
|
+
- **눈 가늘게 뜨기 테스트**: 채워진 인디고 버튼은 정확히 1개. 2개가 경쟁하면 하나를 ghost로 강등.
|
|
56
|
+
- **h1 개수**: 인덱스 0개(사이드바가 제목) / 상세·설정·프로필 1개. h1→h3 건너뜀 없음.
|
|
57
|
+
- **빈 상태 형태**: 제목 + 고유 한 문장 + (있다면) 버튼 1개. 검색결과 없음은 '필터 초기화'지 생성 CTA가 아님.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. 페이지 셸 컨벤션 (제목·액션의 단일 출처)
|
|
62
|
+
|
|
63
|
+
- **제목** = 상단바(`#topbar-title`). `app.js`가 nav 라벨로 한 번 설정하며, 콘텐츠 안에서 이를 반복하지 않는다(인덱스 페이지 in-content `PageHead` 제거). 상세 페이지는 `ctx.setTitle()`로 맥락(회사명 등)을 덮어쓴다.
|
|
64
|
+
- **1차 액션** = 상단바 `#topbar-actions`(`ctx.setActions([...])`), 우측 정렬. 화면당 최대 1개의 `btn--primary`.
|
|
65
|
+
- **PageHead 사용처**: 홈(인사 배너), 프로필·설정(h1 + 진짜 상태 줄), 공고 상세(회사명 in-content 헤더). 그 외 인덱스 페이지에서는 사용하지 않음.
|
|
66
|
+
- 모든 라우트에서 이전 페이지 액션이 새지 않도록 `clear(actionsEl)` 유지(이미 됨).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4. 마이크로카피 규칙 (예시 포함)
|
|
71
|
+
|
|
72
|
+
- **길이 예산**: 페이지 제목 ≤ 약 12자(명사구) · 버튼 2~4단어 · 빈 상태 본문 한 문장 ≤ 약 40자 · 에러는 "무엇이 왜 + 어떻게"를 한 문장.
|
|
73
|
+
- **빈 상태(화면별 고유, MCP 설명 없이)**
|
|
74
|
+
- 채용공고: `저장된 공고가 없어요` / `관심 있는 채용공고를 추가해 적합도와 진행 상태를 관리해 보세요.` · CTA `공고 추가`
|
|
75
|
+
- 적합도(상세): `적합도 분석 전이에요` / `이 공고와 내 프로필을 비교한 분석 결과가 여기에 표시됩니다.`
|
|
76
|
+
- 자기소개서: `아직 자기소개서가 없어요` / `작성하거나 붙여넣어 버전과 함께 보관하세요.` · CTA `새 자기소개서`
|
|
77
|
+
- 면접: `면접 준비 자료가 아직 없어요` / `예상 질문과 1분 자기소개를 정리해 두면 한곳에서 연습할 수 있어요.`
|
|
78
|
+
- 문서: `저장된 문서가 없어요` / `이력서·경력기술서·포트폴리오 텍스트를 보관하세요.` · CTA `문서 추가`
|
|
79
|
+
- 검색결과 없음: `조건에 맞는 항목이 없어요` + `필터 초기화` (생성 CTA 아님)
|
|
80
|
+
- **버튼·토스트 일치**: `공고 저장`→`공고를 저장했습니다`. 에러는 제품 보이스로(`회사와 직무를 입력해야 저장할 수 있어요.`), 막연한 `오류` 금지.
|
|
81
|
+
- **자격 미달 상태**(면접 단계 전, 분석 전)는 가짜 CTA 대신 차분한 설명 한 줄, 버튼 없음.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 5. 패턴별 지침
|
|
86
|
+
|
|
87
|
+
- **페이지 헤더**: 인덱스는 헤더 없이 툴바+리스트로 직행. 설명 줄은 수치/상태만(`면접 준비 대상 ${n}곳`처럼).
|
|
88
|
+
- **버튼 위계**: 화면당 `btn--primary` 1개. 보조는 `btn--ghost`/텍스트. 상세 페이지의 `수정`은 상단바 1개만, `삭제`는 위험 액션이므로 보조/케밥으로.
|
|
89
|
+
- **리스트/테이블**: 1px 하단 헤어라인, 균일 패딩, 숫자 열 우정렬+`.tnum`, 클릭 행 ≥44px. 가능하면 `ListRow` 프리미티브 재사용.
|
|
90
|
+
- **빈 상태**: `EmptyState` 컴포넌트만(카드로 감싸지 않음). 제목+한 문장+버튼≤1.
|
|
91
|
+
- **카드/과카드화**: `.card`는 반복·선택 객체에만. 단일 빈 상태·프로필 하위 항목·모달 내부 행은 카드로 감싸지 말고 `.stack-*`+`.divider`로 구분. 카드 in 카드 금지.
|
|
92
|
+
- **여백·타이포·색**: 인라인 px → 유틸/토큰(4/8/12/16/24/32). 3단계 텍스트 램프(`--text`/`--text-secondary`/`--text-tertiary`) 유지. heading weight ≤ 650 유지(이미 됨). 보라 그라데이션 제거, 악센트는 평면.
|
|
93
|
+
- **폰트**: Pretendard를 **self-host**(`/fonts/PretendardVariable.woff2` + `@font-face`)해 시스템 폴백이 아니라 의도한 서체가 모든 기기에서 항상 렌더되게 한다. *선언만 하고 로드 안 하면 결국 Segoe UI·맑은 고딕 같은 시스템 기본(=제네릭)으로 떨어진다 — "디폴트는 인프라에 숨는다".* Inter/Roboto/Arial 같은 "AI 디폴트 폰트"는 1차 서체·폴백 어디에도 쓰지 않는다.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 출처
|
|
98
|
+
- Anthropic `frontend-design` 스킬 — <https://raw.githubusercontent.com/anthropics/skills/main/skills/frontend-design/SKILL.md>
|
|
99
|
+
- Anthropic `claude-code` frontend-design 플러그인 — <https://raw.githubusercontent.com/anthropics/claude-code/main/plugins/frontend-design/skills/frontend-design/SKILL.md>
|
|
100
|
+
- `interface-design` principles — <https://raw.githubusercontent.com/Dammyjay93/interface-design/main/.claude/skills/interface-design/references/principles.md>
|
|
101
|
+
- awesome-design-md: Linear / Vercel(Geist) / Notion / Stripe DESIGN.md — <https://github.com/VoltAgent/awesome-design-md>
|
|
102
|
+
- Geist Button — <https://vercel.com/geist/button> · Polaris Page actions — <https://polaris-react.shopify.com/components/page-actions>
|
|
103
|
+
- Reverse engineering Linear (Header) — <https://pustelto.com/blog/reverse-engineer-linear-1-header/>
|
|
104
|
+
- NN/g Headlines & Page Titles — <https://www.nngroup.com/articles/microcontent-how-to-write-headlines-page-titles-and-subject-lines/>
|
|
105
|
+
- Refactoring UI 요약 · Impeccable slop catalogue — <https://impeccable.style/slop/>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# CareerMate dashboard — page module contract
|
|
2
|
+
|
|
3
|
+
> **먼저 `DESIGN_GUIDE.md`(디자인 원칙·안티-AI-슬롭 규칙)를 읽으세요.** 이 문서는 *어떻게 짜는가*(컴포넌트·API), `DESIGN_GUIDE.md`는 *어떻게 보여야 하는가*(위계·카피·셸 컨벤션)를 규정합니다.
|
|
4
|
+
|
|
5
|
+
Read this + `DESIGN_GUIDE.md` + `apps/web/public/pages/home.js` (the reference implementation) before writing any page. Every page must look and behave like home.js. No frameworks, no build, no CDN. Korean-first. **XSS-safe: never use innerHTML with user/DB content — `el()` puts strings into textContent. Render document/cover-letter bodies as text inside a `.doc-preview` element (CSS already does `white-space: pre-wrap`).**
|
|
6
|
+
|
|
7
|
+
## File shape
|
|
8
|
+
Create `apps/web/public/pages/<name>.js`:
|
|
9
|
+
```js
|
|
10
|
+
import { el, get, post, put, del, icon, navigate, Card, Badge, Btn, IconBtn, Stat,
|
|
11
|
+
EmptyState, Chips, Field, Input, Textarea, Select, PageHead, openModal, closeModal,
|
|
12
|
+
confirmDialog, toast, toastOk, toastError, copyText, downloadUrl, fmtDate, fmtRelative,
|
|
13
|
+
scoreClass, mount, meta } from '/lib.js';
|
|
14
|
+
|
|
15
|
+
export async function render(ctx) {
|
|
16
|
+
// fetch data, build a root node, then: mount(ctx.view, root);
|
|
17
|
+
// set page actions in the topbar: ctx.setActions([Btn(...)]);
|
|
18
|
+
// after any mutation that changes counts: await ctx.refreshNav();
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
`ctx = { view, params, setTitle(str), setActions(node|nodes), navigate, refreshNav }`. `params` are the path segments after the page id (e.g. `#/jobs/abc` → params=['abc']).
|
|
22
|
+
|
|
23
|
+
## lib.js component API (use these — do not hand-roll styles)
|
|
24
|
+
- `el(tag, props, ...children)` — props: class, text, value, placeholder, type, href, disabled, dataset:{}, style:{}, attrs:{}, onClick/onInput/onChange/onSubmit/onKeydown. children: strings (safe text) or nodes; null/false ignored; arrays flattened.
|
|
25
|
+
- `icon(name, cls?)` → svg. Names: home,user,briefcase,layers,file,mic,settings,plus,edit,trash,copy,download,external,check,sparkle,clock,link,lock,search,inbox,target,info,chevronRight.
|
|
26
|
+
- API: `get(path)`, `post(path,body)`, `put(path,body)`, `del(path)` → parsed JSON; throw Error(message) on failure (wrap mutations in try/catch + toastError).
|
|
27
|
+
- Components: `Card({title,sub,actions,body,clickable,onClick})`, `Stat({label,value,hint,iconName})`, `Badge(statusCode,label)`, `Btn(label,{icon,variant,sm,onClick,type,disabled,title})` (variant: primary|ghost|danger), `IconBtn(name,{onClick,title,variant})`, `EmptyState({iconName,title,body,action})`, `Chips(items,{accent})`, `ListRow({leading,title,sub,trailing,onClick})` (dense list row — leading node + title/sub + right-aligned trailing node(s); becomes clickable when onClick is set), `CheckRow({done,label,onClick})` (onboarding checklist row — hollow dot + accent link when todo, green check + strikethrough when done), `Field(label,control,hint)`, `Input(props)`, `Textarea(props)`, `Select(options,props)` where options=[{value,label,selected}], `PageHead({title,desc,actions})`.
|
|
28
|
+
- Overlays: `openModal({title, body, footer, size})` — body is a node or `(close)=>node`; footer is `(close)=>node|nodes`; size 'lg' optional. `closeModal()`. `confirmDialog({title,message,confirmLabel,danger})` → Promise<boolean>.
|
|
29
|
+
- Feedback: `toast(msg,{title,type})`, `toastOk(msg)`, `toastError(err)`.
|
|
30
|
+
- Utils: `copyText(str)`, `downloadUrl(url)` (GET file download), `fmtDate(iso)`, `fmtRelative(iso)`, `scoreClass(score)` → 'score-strong|score-mid|score-weak|muted', `mount(view, ...nodes)`, `meta()` → {statuses:[{value,label}], document_kinds:[{value,label}]}.
|
|
31
|
+
|
|
32
|
+
## CSS classes available (styles.css) — reuse, don't invent
|
|
33
|
+
Layout: `.stack-2/.stack-3/.stack-4` (vertical gap), `.grid .grid--2/3/4`, `.flex .between .center .wrap .gap-2/3/4 .ml-auto`, `.divider`, `.mt-2..4 .mb-2..4`.
|
|
34
|
+
Surfaces: `.card .card__head .card__body`, `.stat`, `.callout .callout--privacy`, `.doc-preview`, `.kv`(dt/dd), `.timeline .tl-item .tl-item__rail .tl-item__dot .tl-item__line .tl-item__body` (+`.is-current`), `.board .board__col .board__col-head .board__cards .board-card` (kanban).
|
|
35
|
+
Lists/feed/pipeline (prefer the `ListRow`/`CheckRow` helpers over these classes): `.list-row .list-row__lead(.--chip) .list-row__dot .list-row__main .list-row__title .list-row__sub .list-row__trail`, `.pipebar .pipebar__seg` + `.pipefunnel .pipefunnel__tile .pipefunnel__label .pipefunnel__dot .pipefunnel__value .pipefunnel__conv` (application funnel), `.check-row(.is-done) .check-row__dot .check-row__icon .check-row__label(.is-link)`, `.feed-group .feed-item .feed-item__icon .feed-item__body .feed-item__text .feed-item__time`, `.firstrun-hero` (day-one single-card canvas).
|
|
36
|
+
Bits: `.badge .badge--<status>`, `.chip .chip--accent .chips`, `.btn ...`, `.input .textarea .select .field`, `.progress .progress__bar`, `.tabs .tab .is-active`, `.table`, `.empty`, `.muted .text-sm .text-secondary .strong .truncate .tnum`, score colors `.score-strong/.score-mid/.score-weak`.
|
|
37
|
+
Status codes (for Badge + classes): draft(작성 중), planned(지원 예정), applied(지원 완료), document_passed(서류 합격), interview(면접 진행), final_passed(최종 합격), rejected(불합격), on_hold(보류).
|
|
38
|
+
|
|
39
|
+
## Conventions
|
|
40
|
+
- Build forms with Field+Input/Textarea/Select inside `openModal`; submit → post/put → toastOk → closeModal → re-render the page (re-run render or refetch+remount) → `ctx.refreshNav()` if counts changed.
|
|
41
|
+
- Always provide a friendly empty state (see home.js) with an action.
|
|
42
|
+
- Tables/rows that navigate use `class:'is-clickable'` + onClick navigate.
|
|
43
|
+
- Dense, calm, real-product feel. Avoid emoji-heavy or flashy "AI" styling.
|
|
44
|
+
- Tabs: render a `.tabs` bar; clicking re-renders the panel; keep selected tab in a local variable (or in `location.hash` query if you prefer, but local is fine).
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// CareerMate dashboard shell — hash router, sidebar, and page orchestration.
|
|
3
|
+
// Page modules live in /pages/*.js and export `render(ctx)`. They receive a ctx
|
|
4
|
+
// with { view, params, setTitle, setActions, refreshNav } and fill the view.
|
|
5
|
+
// =============================================================================
|
|
6
|
+
import { el, icon, get, navigate, toastError, Spinner, clear } from '/lib.js';
|
|
7
|
+
|
|
8
|
+
const ROUTES = [
|
|
9
|
+
{ id: 'home', label: '홈', icon: 'home', module: '/pages/home.js' },
|
|
10
|
+
{ id: 'profile', label: '프로필', icon: 'user', module: '/pages/profile.js' },
|
|
11
|
+
{ id: 'jobs', label: '채용공고', icon: 'briefcase', module: '/pages/jobs.js', countKey: 'jobs' },
|
|
12
|
+
{ id: 'applications', label: '지원 현황', icon: 'layers', module: '/pages/applications.js', countKey: 'active_applications' },
|
|
13
|
+
{ id: 'documents', label: '문서', icon: 'file', module: '/pages/documents.js', countKey: 'cover_letters' },
|
|
14
|
+
{ id: 'interview', label: '면접 준비', icon: 'mic', module: '/pages/interview.js', countKey: 'interview_pending' },
|
|
15
|
+
{ id: 'settings', label: '설정', icon: 'settings', module: '/pages/settings.js' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const view = document.getElementById('view');
|
|
19
|
+
const navEl = document.getElementById('nav');
|
|
20
|
+
const titleEl = document.getElementById('topbar-title');
|
|
21
|
+
const actionsEl = document.getElementById('topbar-actions');
|
|
22
|
+
const moduleCache = new Map();
|
|
23
|
+
|
|
24
|
+
function parseHash() {
|
|
25
|
+
const raw = location.hash.replace(/^#\/?/, '');
|
|
26
|
+
const segments = raw.split('/').filter(Boolean);
|
|
27
|
+
return { page: segments[0] || 'home', params: segments.slice(1) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderNav(counts = {}) {
|
|
31
|
+
clear(navEl);
|
|
32
|
+
navEl.append(el('div', { class: 'nav__label' }, '워크스페이스'));
|
|
33
|
+
const { page } = parseHash();
|
|
34
|
+
for (const r of ROUTES) {
|
|
35
|
+
if (r.id === 'settings') navEl.append(el('div', { class: 'nav__label' }, '시스템'));
|
|
36
|
+
const count = r.countKey ? counts[r.countKey] : null;
|
|
37
|
+
navEl.append(el('a', {
|
|
38
|
+
class: `nav__item${page === r.id ? ' is-active' : ''}`,
|
|
39
|
+
href: `#/${r.id}`,
|
|
40
|
+
}, icon(r.icon), el('span', {}, r.label),
|
|
41
|
+
count ? el('span', { class: 'nav__badge tnum' }, String(count)) : null));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function refreshNav() {
|
|
46
|
+
try {
|
|
47
|
+
const summary = await get('/api/summary');
|
|
48
|
+
renderNav(summary.counts);
|
|
49
|
+
} catch {
|
|
50
|
+
renderNav();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function loadModule(path) {
|
|
55
|
+
if (moduleCache.has(path)) return moduleCache.get(path);
|
|
56
|
+
const mod = await import(path);
|
|
57
|
+
moduleCache.set(path, mod);
|
|
58
|
+
return mod;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let renderToken = 0;
|
|
62
|
+
async function route() {
|
|
63
|
+
const token = ++renderToken;
|
|
64
|
+
const { page, params } = parseHash();
|
|
65
|
+
const def = ROUTES.find((r) => r.id === page) || ROUTES[0];
|
|
66
|
+
|
|
67
|
+
// active nav state
|
|
68
|
+
navEl.querySelectorAll('.nav__item').forEach((n) => n.classList.toggle('is-active', n.getAttribute('href') === `#/${def.id}`));
|
|
69
|
+
titleEl.textContent = def.label;
|
|
70
|
+
clear(actionsEl);
|
|
71
|
+
view.scrollTop = 0;
|
|
72
|
+
window.scrollTo(0, 0);
|
|
73
|
+
|
|
74
|
+
const ctx = {
|
|
75
|
+
view,
|
|
76
|
+
params,
|
|
77
|
+
setTitle: (t) => { titleEl.textContent = t; },
|
|
78
|
+
setActions: (nodes) => { clear(actionsEl); [].concat(nodes).filter(Boolean).forEach((n) => actionsEl.append(n)); },
|
|
79
|
+
navigate,
|
|
80
|
+
refreshNav,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
clear(view);
|
|
84
|
+
view.append(Spinner());
|
|
85
|
+
try {
|
|
86
|
+
const mod = await loadModule(def.module);
|
|
87
|
+
if (token !== renderToken) return; // a newer navigation superseded this one
|
|
88
|
+
clear(view);
|
|
89
|
+
await mod.render(ctx);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (token !== renderToken) return;
|
|
92
|
+
clear(view);
|
|
93
|
+
view.append(el('div', { class: 'card' }, el('div', { class: 'card__body' },
|
|
94
|
+
el('h3', { style: { marginBottom: '8px' } }, '페이지를 불러오지 못했습니다'),
|
|
95
|
+
el('p', { class: 'text-secondary' }, err instanceof Error ? err.message : String(err)))));
|
|
96
|
+
toastError(err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function initTheme() {
|
|
101
|
+
const saved = localStorage.getItem('cf-theme');
|
|
102
|
+
if (saved === 'light' || saved === 'dark') document.documentElement.dataset.theme = saved;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function boot() {
|
|
106
|
+
initTheme();
|
|
107
|
+
// Show data location in the footer (privacy: user can always see where data lives).
|
|
108
|
+
try {
|
|
109
|
+
const h = await get('/api/health');
|
|
110
|
+
document.getElementById('foot-path').textContent = h.data_dir;
|
|
111
|
+
document.getElementById('foot-path').title = h.data_dir;
|
|
112
|
+
} catch { /* ignore */ }
|
|
113
|
+
await refreshNav();
|
|
114
|
+
window.addEventListener('hashchange', route);
|
|
115
|
+
await route();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
boot();
|
|
Binary file
|