careermate 0.0.2 → 0.0.4

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/INSTALL.md CHANGED
@@ -27,7 +27,7 @@
27
27
  | 내가 누구인가 | MCP 등록 방식 |
28
28
  |---|---|
29
29
  | 사용자가 **Claude Desktop**을 쓰는 경우 | `careermate.zip` 압축 해제 후 폴더 추가 — **터미널 불필요** (`.mcpb` 직접 추가는 known issue) |
30
- | **Claude Code** | CareerMate 폴더의 프로젝트 `.mcp.json`, 또는 `claude mcp add` (첫 실행 시 1회 승인) |
30
+ | **Claude Code** | 사용자가 여는 작업 폴더의 프로젝트 `.mcp.json`, 또는 `claude mcp add` (첫 실행 시 1회 승인) |
31
31
  | **Codex** (OpenAI Codex CLI) | `~/.codex/config.toml`의 `[mcp_servers.careermate]` 테이블, 또는 `codex mcp add` |
32
32
 
33
33
  대부분 `npm run init`(5단계)이 위 등록을 자동으로 처리합니다. 수동 등록 명령은 5단계 끝에 정리해 두었습니다.
@@ -105,7 +105,7 @@ npm run init
105
105
  `init`이 하는 일:
106
106
  - `~/.careermate` 폴더를 보장합니다.
107
107
  - **감지된** AI 클라이언트를 자동 등록합니다(설정 파일 또는 그 상위 폴더가 있으면 감지된 것으로 간주).
108
- - **Claude Code 프로젝트 `.mcp.json`은 항상** CareerMate 폴더에 기록합니다.
108
+ - **Claude Code 프로젝트 `.mcp.json`은 항상** 이 명령을 실행한 작업 폴더에 기록합니다.
109
109
  - 기존 설정은 변경 전에 타임스탬프 백업을 만들고, 사용자 env와 무관한 TOML 테이블은 보존합니다.
110
110
 
111
111
  ### 클라이언트별 결과
@@ -113,7 +113,7 @@ npm run init
113
113
  | 클라이언트 | 등록 결과 |
114
114
  |---|---|
115
115
  | **Claude Desktop** | `careermate.zip` 압축 해제 후 폴더 추가(아래) — `init`과 별개. `.mcpb` 직접 추가는 known issue |
116
- | **Claude Code** | CareerMate 폴더에 프로젝트 `.mcp.json` 기록 → 첫 실행 시 **1회 승인** 필요 |
116
+ | **Claude Code** | 현재 작업 폴더에 프로젝트 `.mcp.json` 기록 → 첫 실행 시 **1회 승인** 필요 |
117
117
  | **Codex** | `~/.codex/config.toml`의 `[mcp_servers.careermate]` 기록 → Codex 안에서 `/mcp`로 확인 |
118
118
 
119
119
  `.mcp.json`은 최상위 `"mcpServers"` 아래에 서버별 `{"type":"stdio","command":...,"args":[...]}` 형태로 들어갑니다.
package/README.md CHANGED
@@ -30,7 +30,7 @@
30
30
 
31
31
  ## 주요 기능
32
32
 
33
- - **AI와 대화로 모든 작업** — MCP 도구 33개로 온보딩·프로필·이력서·자소서·공고·핏 분석·지원 상태·면접 준비·전문가 플레이북·AI 티 안 나는 글쓰기까지 처리.
33
+ - **AI와 대화로 모든 작업** — MCP 도구 35개로 온보딩·프로필·이력서·자소서·공고·핏 분석·지원 상태·면접 준비·전문가 플레이북·AI 티 안 나는 글쓰기까지 처리.
34
34
  - **지원 상태 8단계 관리** — `draft`(작성 중) · `planned`(지원 예정) · `applied`(지원 완료) · `document_passed`(서류 합격) · `interview`(면접 진행) · `final_passed`(최종 합격) · `rejected`(불합격) · `on_hold`(보류). `document_passed` 이상에서 면접 준비가 해금됩니다.
35
35
  - **자기소개서 버전 관리** — 공고별로 자소서 버전을 쌓고 타임라인으로 비교, 파일로 내보내기.
36
36
  - **채용공고 파싱** — 붙여넣은 공고 텍스트를 구조화해 저장.
@@ -46,7 +46,7 @@
46
46
  | 프로세스 | 실행 | 역할 |
47
47
  | --- | --- | --- |
48
48
  | **대시보드 웹 서버** | `npm start` | `http://127.0.0.1:4319` — 내 데이터를 눈으로 확인·관리. `127.0.0.1`에만 바인딩. |
49
- | **MCP 서버** | `npm run mcp` | stdio 기반. 보통 AI 클라이언트가 자동 실행. 도구 33개 제공. |
49
+ | **MCP 서버** | `npm run mcp` | stdio 기반. 보통 AI 클라이언트가 자동 실행. 도구 35개 제공. |
50
50
 
51
51
  ---
52
52
 
@@ -141,7 +141,7 @@ npx -y careermate init
141
141
 
142
142
  ---
143
143
 
144
- ## MCP 도구 한눈에 (33개)
144
+ ## MCP 도구 한눈에 (35개)
145
145
 
146
146
  | 분류 | 도구 |
147
147
  | --- | --- |
@@ -157,6 +157,7 @@ npx -y careermate init
157
157
  | 면접 준비 | `save_interview_prep` |
158
158
  | 글쓰기 | `get_writing_style_guide` (AI 티 안 나는 한국어 글쓰기 규칙) |
159
159
  | 전문가 지식 (Career-OS) | `get_playbook` (도메인 플레이북 16종) · `get_verifier` (저장 전 검증 루브릭 6종) |
160
+ | 저장 전 점검 | `validate_cover_letter` (자소서 저장 전 미리보기) · `set_verify_mode` (점검 엄격도 기본/엄격) |
160
161
  | 대시보드/활동 | `open_dashboard` · `open_application` · `list_recent_activity` · `get_workflow_guide` |
161
162
  | 업데이트 | `check_for_update` · `update_careermate` |
162
163
 
@@ -175,7 +176,7 @@ CareerMate/
175
176
  │ ├─ shared/ # 공용 타입·zod 스키마·유틸
176
177
  │ ├─ db/ # node:sqlite DB 접근·스키마·마이그레이션
177
178
  │ ├─ core/ # 도메인 유스케이스
178
- │ ├─ mcp-tools/ # MCP 도구 33개 정의
179
+ │ ├─ mcp-tools/ # MCP 도구 35개 정의
179
180
  │ ├─ knowledge/ # Career-OS 전문가 플레이북·검증 루브릭 serve
180
181
  │ ├─ exporters/ # 내보내기(자소서 등)
181
182
  │ ├─ parsers/ # 채용공고 파싱
@@ -301,7 +302,7 @@ You can open the dashboard at any time to see your stored data. For a step-by-st
301
302
 
302
303
  ## Key features
303
304
 
304
- - **Everything through conversation with AI** — 33 MCP tools cover onboarding, profile, resumes, cover letters, job postings, fit analysis, application status, interview prep, expert playbooks, and human-sounding writing.
305
+ - **Everything through conversation with AI** — 35 MCP tools cover onboarding, profile, resumes, cover letters, job postings, fit analysis, application status, interview prep, expert playbooks, and human-sounding writing.
305
306
  - **8-stage application status** — `draft` · `planned` · `applied` · `document_passed` · `interview` · `final_passed` · `rejected` · `on_hold`. Interview prep unlocks at `document_passed` or later.
306
307
  - **Cover-letter versioning** — Stack versions per posting, compare on a timeline, export to a file.
307
308
  - **Job-posting parsing** — Turn pasted posting text into structured records.
@@ -317,7 +318,7 @@ Two local processes share the same database.
317
318
  | Process | Run | Role |
318
319
  | --- | --- | --- |
319
320
  | **Dashboard web server** | `npm start` | `http://127.0.0.1:4319` — view and manage your data. Binds to `127.0.0.1` only. |
320
- | **MCP server** | `npm run mcp` | stdio-based. Usually launched automatically by the AI client. Provides 33 tools. |
321
+ | **MCP server** | `npm run mcp` | stdio-based. Usually launched automatically by the AI client. Provides 35 tools. |
321
322
 
322
323
  ---
323
324
 
@@ -412,7 +413,7 @@ Dark mode is supported.
412
413
 
413
414
  ---
414
415
 
415
- ## MCP tools at a glance (33)
416
+ ## MCP tools at a glance (35)
416
417
 
417
418
  | Category | Tools |
418
419
  | --- | --- |
@@ -428,6 +429,7 @@ Dark mode is supported.
428
429
  | Interview prep | `save_interview_prep` |
429
430
  | Writing | `get_writing_style_guide` (rules for human-sounding Korean writing) |
430
431
  | Expert knowledge (Career-OS) | `get_playbook` (16 domain playbooks) · `get_verifier` (6 pre-save rubrics) |
432
+ | Pre-save check | `validate_cover_letter` (dry-run preview before saving) · `set_verify_mode` (check strictness: default/strict) |
431
433
  | Dashboard / activity | `open_dashboard` · `open_application` · `list_recent_activity` · `get_workflow_guide` |
432
434
  | Update | `check_for_update` · `update_careermate` |
433
435
 
@@ -446,7 +448,7 @@ CareerMate/
446
448
  │ ├─ shared/ # Shared types, zod schemas, utils
447
449
  │ ├─ db/ # node:sqlite DB access, schema, migrations
448
450
  │ ├─ core/ # Domain use cases
449
- │ ├─ mcp-tools/ # The 33 MCP tool definitions
451
+ │ ├─ mcp-tools/ # The 35 MCP tool definitions
450
452
  │ ├─ knowledge/ # Career-OS expert playbooks & verifier rubrics (serve)
451
453
  │ ├─ exporters/ # Exporters (cover letters, etc.)
452
454
  │ ├─ parsers/ # Job-posting parsing
@@ -46,12 +46,12 @@ if (isHelp || !ENTRIES[cmd]) {
46
46
  '',
47
47
  '사용법: careermate <명령>',
48
48
  '',
49
- ' init AI 클라이언트에 CareerMate를 자동 연결(MCP 설정 등록)',
49
+ ' init AI 클라이언트에 CareerMate를 자동 연결(MCP 설정 등록, Claude Code는 현재 폴더 기준)',
50
50
  ' mcp MCP 서버 실행 (보통 AI 클라이언트가 자동 호출)',
51
51
  ' start 로컬 대시보드 실행 (http://127.0.0.1:4319)',
52
52
  ' doctor 설치/환경 점검',
53
53
  '',
54
- '처음이라면: careermate init → AI 클라이언트 재시작',
54
+ '처음이라면: 사용할 작업 폴더에서 careermate init → AI 클라이언트 재시작',
55
55
  ];
56
56
  // 명시적 도움말은 stdout+exit0, 알 수 없는 명령은 stderr+exit1
57
57
  // (stdout은 MCP 전송 채널이므로 오류 안내로 오염시키지 않는다).
package/dist/doctor.mjs CHANGED
@@ -68172,7 +68172,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
68172
68172
  const ij = i2 * width + j;
68173
68173
  const center = buf[ij];
68174
68174
  let sum = 0;
68175
- let norm = 0;
68175
+ let norm2 = 0;
68176
68176
  for (let k = 0; k < kernelSize; k++) {
68177
68177
  const y = i2 + k - halfSize;
68178
68178
  if (y < 0 || y >= height) {
@@ -68186,10 +68186,10 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
68186
68186
  const neighbour = buf[y * width + x2];
68187
68187
  const w = kernel[k * kernelSize + l2] * rangeValues[Math.abs(neighbour - center)];
68188
68188
  sum += neighbour * w;
68189
- norm += w;
68189
+ norm2 += w;
68190
68190
  }
68191
68191
  }
68192
- const pix = out[ij] = Math.round(sum / norm);
68192
+ const pix = out[ij] = Math.round(sum / norm2);
68193
68193
  histogram[pix]++;
68194
68194
  }
68195
68195
  }
@@ -71451,6 +71451,24 @@ function setVerifyStrict(on) {
71451
71451
  return on;
71452
71452
  }
71453
71453
 
71454
+ // packages/shared/src/errors.ts
71455
+ var CareerMateError = class extends Error {
71456
+ constructor(message, httpStatus = 400, code) {
71457
+ super(message);
71458
+ this.httpStatus = httpStatus;
71459
+ this.code = code;
71460
+ this.name = "CareerMateError";
71461
+ }
71462
+ httpStatus;
71463
+ code;
71464
+ };
71465
+ function notFound(message) {
71466
+ return new CareerMateError(message, 404, "not_found");
71467
+ }
71468
+ function conflict(message) {
71469
+ return new CareerMateError(message, 409, "conflict");
71470
+ }
71471
+
71454
71472
  // packages/shared/src/enums.ts
71455
71473
  var APPLICATION_STATUSES = [
71456
71474
  "draft",
@@ -71501,8 +71519,9 @@ function canTransitionStatus(from, to) {
71501
71519
  }
71502
71520
  function assertStatusTransition(from, to) {
71503
71521
  if (!canTransitionStatus(from, to)) {
71504
- throw new Error(
71505
- `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
71522
+ const suffix = to === "draft" ? ` ('${APPLICATION_STATUS_LABELS[from]}' \uB2E8\uACC4\uC758 \uC9C0\uC6D0\uC740 \uC791\uC131 \uC911(\uBBF8\uCD94\uC801)\uC73C\uB85C \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD08\uAE30 \uB2E8\uACC4\uB85C \uC62E\uAE30\uB824\uBA74 '\uC9C0\uC6D0 \uC608\uC815'\uC744 \uC120\uD0DD\uD558\uC138\uC694.)` : "";
71523
+ throw conflict(
71524
+ `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.${suffix}`
71506
71525
  );
71507
71526
  }
71508
71527
  }
@@ -75595,7 +75614,20 @@ var optLine = external_exports.string().max(MAX_LINE).optional();
75595
75614
  var optNote = external_exports.string().max(MAX_NOTE).optional();
75596
75615
  var optBody = external_exports.string().max(MAX_BODY).optional();
75597
75616
  var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
75598
- var strList = external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS);
75617
+ function toStrList(v) {
75618
+ if (typeof v !== "string") return v;
75619
+ const s = v.trim();
75620
+ if (!s) return [];
75621
+ if (s.startsWith("[")) {
75622
+ try {
75623
+ const parsed = JSON.parse(s);
75624
+ if (Array.isArray(parsed)) return parsed;
75625
+ } catch {
75626
+ }
75627
+ }
75628
+ return s.split(/[\n,]/).map((x2) => x2.trim()).filter(Boolean);
75629
+ }
75630
+ var strList = external_exports.preprocess(toStrList, external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS));
75599
75631
  var baseRecord = {
75600
75632
  id,
75601
75633
  created_at: isoDateTime,
@@ -75657,6 +75689,9 @@ var ExperienceRecordSchema = external_exports.object({
75657
75689
  tech: external_exports.array(external_exports.string()),
75658
75690
  order_index: external_exports.number()
75659
75691
  });
75692
+ var ExperienceBatchInputSchema = external_exports.object({
75693
+ experiences: external_exports.array(ExperienceInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uACBD\uB825 \uBAA9\uB85D(\uD55C \uAC74\uC774\uC5B4\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uD68C\uC0AC\xB7\uC9C1\uBB34\xB7\uC785\uC0AC\uC77C\uC740 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
75694
+ });
75660
75695
  var ProjectInputSchema = external_exports.object({
75661
75696
  name: reqLine,
75662
75697
  role: optLine,
@@ -75680,6 +75715,9 @@ var ProjectRecordSchema = external_exports.object({
75680
75715
  end_date: external_exports.string().nullable(),
75681
75716
  order_index: external_exports.number()
75682
75717
  });
75718
+ var ProjectBatchInputSchema = external_exports.object({
75719
+ projects: external_exports.array(ProjectInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uC774\uB984\uC758 \uD504\uB85C\uC81D\uD2B8\uB294 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
75720
+ });
75683
75721
  var SkillInputSchema = external_exports.object({
75684
75722
  name: reqLine,
75685
75723
  category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
@@ -75695,6 +75733,9 @@ var SkillRecordSchema = external_exports.object({
75695
75733
  years: external_exports.number().nullable(),
75696
75734
  order_index: external_exports.number()
75697
75735
  });
75736
+ var SkillBatchInputSchema = external_exports.object({
75737
+ skills: external_exports.array(SkillInputSchema).min(1).max(MAX_ITEMS).describe("\uCD94\uAC00\uD560 \uAE30\uC220\uC2A4\uD0DD \uBAA9\uB85D(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C). \uAC19\uC740 \uC774\uB984\uC758 \uAE30\uC220\uC740 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")
75738
+ });
75698
75739
  var DocumentInputSchema = external_exports.object({
75699
75740
  kind: external_exports.enum(DOCUMENT_KINDS),
75700
75741
  title: reqLine,
@@ -75891,7 +75932,7 @@ var ApplicationContextSchema = external_exports.object({
75891
75932
  });
75892
75933
 
75893
75934
  // packages/shared/src/build-info.ts
75894
- var APP_VERSION = true ? "0.0.2" : "0.0.0-dev";
75935
+ var APP_VERSION = true ? "0.0.4" : "0.0.0-dev";
75895
75936
  var BUNDLED = true;
75896
75937
 
75897
75938
  // packages/shared/src/index.ts
@@ -75906,6 +75947,7 @@ function now() {
75906
75947
 
75907
75948
  // packages/db/src/repositories.ts
75908
75949
  var PROFILE_ID = "profile-singleton";
75950
+ var norm = (s) => (s ?? "").trim().toLowerCase();
75909
75951
  function mapProfile(r) {
75910
75952
  return {
75911
75953
  id: r.id,
@@ -75983,6 +76025,9 @@ function mapExperience(r) {
75983
76025
  updated_at: r.updated_at
75984
76026
  };
75985
76027
  }
76028
+ function expKey(company, role, start) {
76029
+ return [norm(company), norm(role), norm(start)].join("\0");
76030
+ }
75986
76031
  var experienceRepo = {
75987
76032
  list() {
75988
76033
  return getDb().prepare(`SELECT * FROM experiences ORDER BY order_index ASC, start_date DESC`).all().map(mapExperience);
@@ -75991,6 +76036,32 @@ var experienceRepo = {
75991
76036
  const r = getDb().prepare(`SELECT * FROM experiences WHERE id = ?`).get(id2);
75992
76037
  return r ? mapExperience(r) : null;
75993
76038
  },
76039
+ /** Existing experience with the same company+role+start_date, or null (for idempotent batch upserts). */
76040
+ findMatch(input) {
76041
+ const key = expKey(input.company, input.role ?? null, input.start_date ?? null);
76042
+ return this.list().find((e) => expKey(e.company, e.role, e.start_date) === key) ?? null;
76043
+ },
76044
+ /** Insert, or merge into the matching experience (omitted fields keep their stored value). */
76045
+ upsert(input) {
76046
+ const existing = this.findMatch(input);
76047
+ if (existing) return { record: this.update(existing.id, input), created: false };
76048
+ return { record: this.add(input), created: true };
76049
+ },
76050
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
76051
+ addMany(inputs) {
76052
+ return tx(() => {
76053
+ const records = [];
76054
+ let created = 0;
76055
+ let updated = 0;
76056
+ for (const input of inputs) {
76057
+ const { record, created: isNew } = this.upsert(input);
76058
+ records.push(record);
76059
+ if (isNew) created += 1;
76060
+ else updated += 1;
76061
+ }
76062
+ return { records, created, updated };
76063
+ });
76064
+ },
75994
76065
  add(input) {
75995
76066
  const ts = now();
75996
76067
  const id2 = newId("exp_");
@@ -76064,6 +76135,32 @@ var projectRepo = {
76064
76135
  const r = getDb().prepare(`SELECT * FROM projects WHERE id = ?`).get(id2);
76065
76136
  return r ? mapProject(r) : null;
76066
76137
  },
76138
+ /** Existing project with the same name (case-insensitive), or null (for idempotent batch upserts). */
76139
+ findByName(name) {
76140
+ const key = norm(name);
76141
+ return this.list().find((p) => norm(p.name) === key) ?? null;
76142
+ },
76143
+ /** Insert, or merge into the same-named project (omitted fields keep their stored value). */
76144
+ upsert(input) {
76145
+ const existing = this.findByName(input.name);
76146
+ if (existing) return { record: this.update(existing.id, input), created: false };
76147
+ return { record: this.add(input), created: true };
76148
+ },
76149
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
76150
+ addMany(inputs) {
76151
+ return tx(() => {
76152
+ const records = [];
76153
+ let created = 0;
76154
+ let updated = 0;
76155
+ for (const input of inputs) {
76156
+ const { record, created: isNew } = this.upsert(input);
76157
+ records.push(record);
76158
+ if (isNew) created += 1;
76159
+ else updated += 1;
76160
+ }
76161
+ return { records, created, updated };
76162
+ });
76163
+ },
76067
76164
  add(input) {
76068
76165
  const ts = now();
76069
76166
  const id2 = newId("prj_");
@@ -76131,6 +76228,32 @@ var skillRepo = {
76131
76228
  const r = getDb().prepare(`SELECT * FROM skills WHERE id = ?`).get(id2);
76132
76229
  return r ? mapSkill(r) : null;
76133
76230
  },
76231
+ /** Existing skill with the same name (case-insensitive), or null (for idempotent batch upserts). */
76232
+ findByName(name) {
76233
+ const key = norm(name);
76234
+ return this.list().find((s) => norm(s.name) === key) ?? null;
76235
+ },
76236
+ /** Insert, or merge into the same-named skill (omitted fields keep their stored value). */
76237
+ upsert(input) {
76238
+ const existing = this.findByName(input.name);
76239
+ if (existing) return { record: this.update(existing.id, input), created: false };
76240
+ return { record: this.add(input), created: true };
76241
+ },
76242
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
76243
+ addMany(inputs) {
76244
+ return tx(() => {
76245
+ const records = [];
76246
+ let created = 0;
76247
+ let updated = 0;
76248
+ for (const input of inputs) {
76249
+ const { record, created: isNew } = this.upsert(input);
76250
+ records.push(record);
76251
+ if (isNew) created += 1;
76252
+ else updated += 1;
76253
+ }
76254
+ return { records, created, updated };
76255
+ });
76256
+ },
76134
76257
  add(input) {
76135
76258
  const ts = now();
76136
76259
  const id2 = newId("skl_");
@@ -76960,7 +77083,7 @@ function styleSignals(text) {
76960
77083
  }
76961
77084
  var DISCLAIMER = "\uC774 \uC810\uAC80\uC740 LLM \uC5C6\uC774 \uC140 \uC218 \uC788\uB294 \uAC83\uB9CC \uBD05\uB2C8\uB2E4(\uC22B\uC790 \uCD9C\uCC98\xB7\uBB38\uCCB4 \uC2E0\uD638). \uD1B5\uACFC=\uD569\uACA9\uC774 \uC544\uB2C8\uBA70, \uC0AC\uC2E4\xB7\uB3D9\uBB38\uC11C\uB2F5\xB7\uC758\uBBF8 \uD310\uB2E8\uC740 \uB2F9\uC2E0(AI)\uC774 \uC9C1\uC811 \uD574\uC57C \uD569\uB2C8\uB2E4.";
76962
77085
  function lintArtifact(kind, text, corpus, opts = {}) {
76963
- const norm = normalizeText(text);
77086
+ const norm2 = normalizeText(text);
76964
77087
  const corpusAvailable = (corpus.documents + corpus.structured).trim().length > 0;
76965
77088
  const provenance = analyzeProvenance(text, corpus);
76966
77089
  const signals = styleSignals(text);
@@ -76984,7 +77107,7 @@ function lintArtifact(kind, text, corpus, opts = {}) {
76984
77107
  }
76985
77108
  return {
76986
77109
  kind,
76987
- charCount: [...norm].length,
77110
+ charCount: [...norm2].length,
76988
77111
  signals,
76989
77112
  provenance,
76990
77113
  blocking,
@@ -77044,20 +77167,40 @@ function addResume(input) {
77044
77167
  activityRepo.log("resume_added", `${doc.title} \uBB38\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "document", doc.id);
77045
77168
  return doc;
77046
77169
  }
77047
- function addExperience(input) {
77048
- const exp = experienceRepo.add(input);
77049
- activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
77050
- return exp;
77170
+ function batchNames(names) {
77171
+ const shown = names.filter(Boolean).slice(0, 8).join(", ");
77172
+ if (!shown) return "";
77173
+ return `: ${shown}${names.length > 8 ? " \uC678" : ""}`;
77051
77174
  }
77052
- function addProject(input) {
77053
- const prj = projectRepo.add(input);
77054
- activityRepo.log("profile_updated", `\uD504\uB85C\uC81D\uD2B8(${prj.name})\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "project", prj.id);
77055
- return prj;
77175
+ function addExperiences(inputs) {
77176
+ const res = experienceRepo.addMany(inputs);
77177
+ activityRepo.log(
77178
+ "profile_updated",
77179
+ `\uACBD\uB825 ${res.records.length}\uAC74\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4(\uC2E0\uADDC ${res.created} \xB7 \uAC31\uC2E0 ${res.updated})${batchNames(res.records.map((e) => e.company))}.`,
77180
+ "experience",
77181
+ res.records[0]?.id
77182
+ );
77183
+ return res;
77184
+ }
77185
+ function addProjects(inputs) {
77186
+ const res = projectRepo.addMany(inputs);
77187
+ activityRepo.log(
77188
+ "profile_updated",
77189
+ `\uD504\uB85C\uC81D\uD2B8 ${res.records.length}\uAC74\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4(\uC2E0\uADDC ${res.created} \xB7 \uAC31\uC2E0 ${res.updated})${batchNames(res.records.map((p) => p.name))}.`,
77190
+ "project",
77191
+ res.records[0]?.id
77192
+ );
77193
+ return res;
77056
77194
  }
77057
- function addSkill(input) {
77058
- const skill = skillRepo.add(input);
77059
- activityRepo.log("profile_updated", `\uAE30\uC220(${skill.name})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "skill", skill.id);
77060
- return skill;
77195
+ function addSkills(inputs) {
77196
+ const res = skillRepo.addMany(inputs);
77197
+ activityRepo.log(
77198
+ "profile_updated",
77199
+ `\uAE30\uC220 ${res.records.length}\uAC1C\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4(\uC2E0\uADDC ${res.created} \xB7 \uAC31\uC2E0 ${res.updated})${batchNames(res.records.map((s) => s.name))}.`,
77200
+ "skill",
77201
+ res.records[0]?.id
77202
+ );
77203
+ return res;
77061
77204
  }
77062
77205
  function saveJobPosting(input, id2) {
77063
77206
  const job = jobRepo.upsert(input, id2);
@@ -77067,7 +77210,7 @@ function saveJobPosting(input, id2) {
77067
77210
  }
77068
77211
  function saveFitAnalysis(input) {
77069
77212
  const job = jobRepo.get(input.job_id);
77070
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}. \uBA3C\uC800 save_job_posting\uC73C\uB85C \uACF5\uACE0\uB97C \uC800\uC7A5\uD558\uC138\uC694.`);
77213
+ if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}. \uBA3C\uC800 save_job_posting\uC73C\uB85C \uACF5\uACE0\uB97C \uC800\uC7A5\uD558\uC138\uC694.`);
77071
77214
  applicationRepo.ensure(job.id, "draft");
77072
77215
  const fit = fitRepo.save(input);
77073
77216
  const scoreText = fit.score != null ? ` (\uC801\uD569\uB3C4 ${fit.score}\uC810)` : "";
@@ -77082,7 +77225,7 @@ function saveCoverLetterVersion(input) {
77082
77225
  const detail = verification.blocking.map((b) => `${b.label}: ${b.detail}`).join("; ");
77083
77226
  const strictHit = verification.blocking.some((b) => b.id === "unverified_numbers_strict");
77084
77227
  const tail = strictHit ? `\uC5C4\uACA9 \uBAA8\uB4DC\uAC00 \uCF1C\uC838 \uC788\uC5B4, \uC62C\uB9B0 \uC774\uB825\uC11C \uBCF8\uBB38\uC5D0 \uC5C6\uB294 \uC218\uCE58\uB294 \uB9C9\uD799\uB2C8\uB2E4. \uC774\uB825\uC11C\uB97C \uC62C\uB9AC\uAC70\uB098 \uC5C4\uACA9 \uBAA8\uB4DC\uB97C \uB044\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force: true\uB85C \uC800\uC7A5\uD558\uC138\uC694.` : `\uC774 \uC218\uCE58\uB294 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4. \uC6D0\uBCF8 \uB370\uC774\uD130\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774 \uB9DE\uB2E4\uBA74 \uBA3C\uC800 \uACBD\uB825/\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADF8 \uC218\uCE58\uB97C \uCD94\uAC00\uD55C \uB4A4 \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694(\uADF8\uB300\uB85C \uC800\uC7A5\uD558\uB824\uBA74 force: true).`;
77085
- throw new Error(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`);
77228
+ throw new CareerMateError(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`, 400, "save_gate");
77086
77229
  }
77087
77230
  const { coverLetter, version: version2 } = coverLetterRepo.addVersion({
77088
77231
  cover_letter_id: input.cover_letter_id,
@@ -77109,7 +77252,7 @@ function saveCoverLetterVersion(input) {
77109
77252
  }
77110
77253
  function updateApplicationStatus(jobId, status, note) {
77111
77254
  const job = jobRepo.get(jobId);
77112
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${jobId}`);
77255
+ if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${jobId}`);
77113
77256
  const application = applicationRepo.setStatus(jobId, status, note);
77114
77257
  activityRepo.log(
77115
77258
  "application_status_changed",
@@ -77124,11 +77267,11 @@ function updateApplicationStatus(jobId, status, note) {
77124
77267
  }
77125
77268
  function saveInterviewPrep(input) {
77126
77269
  const job = jobRepo.get(input.job_id);
77127
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
77270
+ if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
77128
77271
  const app = applicationRepo.getByJob(job.id);
77129
77272
  if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
77130
77273
  const current = app ? APPLICATION_STATUS_LABELS[app.status] : "\uBBF8\uC9C0\uC6D0";
77131
- throw new Error(
77274
+ throw conflict(
77132
77275
  `\uBA74\uC811 \uC900\uBE44\uB294 '\uC11C\uB958 \uD569\uACA9' \uC774\uC0C1 \uC0C1\uD0DC\uC5D0\uC11C\uB9CC \uC800\uC7A5\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uD604\uC7AC \uC0C1\uD0DC: '${current}'. \uBA3C\uC800 update_application_status\uB85C \uC0C1\uD0DC\uB97C \uC62C\uB9B0 \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
77133
77276
  );
77134
77277
  }
@@ -77209,7 +77352,7 @@ var WORKFLOWS = [
77209
77352
  '\uD30C\uC77C \uC785\uB825 \uBD84\uAE30: \u2460 \uC0AC\uC6A9\uC790\uAC00 "\uD30C\uC77C\uC774 \uC788\uB2E4"\uACE0 \uD558\uBA74 `open_inbox`\uB85C \uC778\uC785 \uD3F4\uB354\uB97C \uC5F4\uC5B4 \uD30C\uC77C\uC744 \uB123\uAC8C \uD558\uACE0, "\uB2E4 \uB123\uC5C8\uB2E4"\uACE0 \uD558\uBA74 `read_inbox`\uB85C \uBCF8\uBB38\uC744 \uC77D\uB294\uB2E4(pdf\xB7\uC774\uBBF8\uC9C0\uB294 read_inbox\uAC00 \uACBD\uB85C\uB9CC \uC8FC\uBBC0\uB85C \uD074\uB77C\uC774\uC5B8\uD2B8\uC758 \uD30C\uC77C \uC77D\uAE30\uB85C \uC9C1\uC811 \uC77D\uB294\uB2E4). \u2461 \uC0AC\uC6A9\uC790\uAC00 \uD30C\uC77C \uACBD\uB85C\uB97C \uC9C1\uC811 \uC54C\uB824\uC8FC\uBA74 `read_document`\uB85C \uC77D\uB294\uB2E4. \u2462 "\uC5C6\uB2E4/\uC9C1\uC811 \uC785\uB825/\uD14D\uC2A4\uD2B8\uB85C \uBD99\uC5EC\uB123\uAE30"\uBA74 \uD3F4\uB354\uB97C \uC5F4\uC9C0 \uB9D0\uACE0 \uBC1B\uC740 \uD14D\uC2A4\uD2B8\uB97C \uADF8\uB300\uB85C \uC4F4\uB2E4. (\uBB3B\uC9C0\uB3C4 \uC54A\uACE0 open_inbox\uB85C \uD3F4\uB354\uB97C \uC790\uB3D9\uC73C\uB85C \uC5F4\uC9C0 \uC54A\uB294\uB2E4.)',
77210
77353
  "\uC218\uC9D1\uD55C \uC815\uBCF4\uB97C \uAD6C\uC870\uD654\uD574 `save_profile`\uB85C \uC800\uC7A5\uD55C\uB2E4(\uC774\uB984\xB7\uC5F0\uB77D\uCC98\xB7headline\xB7summary\xB7desired_roles\xB7desired_conditions, \uADF8\uB9AC\uACE0 \uAE00\uC4F0\uAE30 \uC120\uD638\uC778 preferred_tone\uACFC emphasis_points \uD3EC\uD568). \uD30C\uC77C \uCD9C\uCC98\uB294 source=upload, \uC9C1\uC811 \uC785\uB825\uC740 manual.",
77211
77354
  "\uC5C5\uB85C\uB4DC\uD55C \uC774\uB825\uC11C/\uACBD\uB825\uAE30\uC220\uC11C/\uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uBCF8\uBB38\uC744 `add_resume`\uB85C \uC800\uC7A5\uD55C\uB2E4(kind\xB7title \uC9C0\uC815, \uB300\uD45C \uBB38\uC11C\uB294 is_primary=true).",
77212
- "\uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC\uC774 \uD30C\uC545\uB418\uBA74 `add_experience`\xB7`add_project`\xB7`add_skill`\uB85C \uAC01\uAC01 \uC800\uC7A5\uD55C\uB2E4(\uC644\uC131\uB3C4\uC758 has_experience/has_skills\uC5D0 \uBC18\uC601).",
77355
+ "\uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC\uC774 \uD30C\uC545\uB418\uBA74 `add_experience`\xB7`add_project`\xB7`add_skill`\uB85C \uC800\uC7A5\uD55C\uB2E4 \u2014 \uAC01 \uB3C4\uAD6C\uB294 \uBC30\uC5F4\uC744 \uBC1B\uC73C\uBBC0\uB85C \uD30C\uC545\uD55C \uD56D\uBAA9\uC744 \uD55C \uBC88\uC5D0 \uBAA8\uC544 \uB118\uAE34\uB2E4(\uD56D\uBAA9\uB9C8\uB2E4 \uBC18\uBCF5 \uD638\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4). \uAC19\uC740 \uD56D\uBAA9\uC744 \uB2E4\uC2DC \uB123\uC5B4\uB3C4 \uC911\uBCF5 \uC5C6\uC774 \uAC31\uC2E0\uB41C\uB2E4(\uC644\uC131\uB3C4\uC758 has_experience/has_skills\uC5D0 \uBC18\uC601).",
77213
77356
  "\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uAC00 \uC788\uC73C\uBA74 `save_cover_letter_version`\uC73C\uB85C \uC800\uC7A5\uD55C\uB2E4(title+content\uBA74 v1\uB85C \uC800\uC7A5, \uC5C6\uC73C\uBA74 \uB098\uC911\uC5D0 \uC791\uC131 \uAC00\uB2A5\uD568\uC744 \uC548\uB0B4).",
77214
77357
  "`open_dashboard`\uB97C \uD638\uCD9C\uD574 \uC0AC\uC6A9\uC790\uAC00 \uC790\uAE30 \uB370\uC774\uD130\uB97C \uD655\uC778\uD558\uAC8C \uD55C\uB2E4.",
77215
77358
  "\uB2E4\uC2DC `get_onboarding_status`\uB85C \uC644\uC131\uB3C4\uB97C \uD655\uC778\uD558\uACE0, \uB2E4\uC74C \uB2E8\uACC4(\uACF5\uACE0 \uBD84\uC11D \uB4F1)\uB97C \uC81C\uC548\uD55C\uB2E4."
@@ -79018,11 +79161,12 @@ ${lines}`;
79018
79161
  {
79019
79162
  name: "add_experience",
79020
79163
  title: "\uACBD\uB825 \uCD94\uAC00",
79021
- description: "\uAD6C\uC870\uD654\uB41C \uACBD\uB825(\uC9C1\uC7A5 \uB2E8\uC704)\uC744 \uBCC4\uB3C4 \uD56D\uBAA9\uC73C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. \uC774\uB825\uC11C \uBCF8\uBB38(add_resume)\uACFC \uB2EC\uB9AC, \uD68C\uC0AC\xB7\uC9C1\uBB34\xB7\uAE30\uAC04\xB7\uC131\uACFC\uB97C \uD544\uB4DC\uB85C \uB098\uB220 \uC800\uC7A5\uD574 \uC801\uD569\uB3C4 \uBD84\uC11D/\uC790\uC18C\uC11C \uC791\uC131 \uC2DC \uC815\uBC00\uD558\uAC8C \uD65C\uC6A9\uB429\uB2C8\uB2E4. company\uB294 \uD544\uC218\uC774\uBA70, achievements\uC5D0\uB294 \uC815\uB7C9 \uC9C0\uD45C\uAC00 \uB2F4\uAE34 \uC131\uACFC\uB97C, tech\uC5D0\uB294 \uC0AC\uC6A9 \uAE30\uC220\uC744 \uBC30\uC5F4\uB85C \uB123\uC73C\uC138\uC694. \uC628\uBCF4\uB529 \uC644\uC131\uB3C4(get_onboarding_status)\uC758 has_experience\uB97C \uCC44\uC6B0\uB824\uBA74 \uC774 \uB3C4\uAD6C\uB85C 1\uAC74 \uC774\uC0C1 \uCD94\uAC00\uD574\uC57C \uD569\uB2C8\uB2E4.",
79022
- inputSchema: ExperienceInputSchema.shape,
79164
+ description: "\uAD6C\uC870\uD654\uB41C \uACBD\uB825(\uC9C1\uC7A5 \uB2E8\uC704)\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. \uC774\uB825\uC11C\uC5D0\uC11C \uD30C\uC545\uD55C \uACBD\uB825\uC744 experiences \uBC30\uC5F4\uC5D0 \uD55C \uBC88\uC5D0 \uB2F4\uC544 \uD638\uCD9C\uD558\uC138\uC694(\uD55C \uAC74\uC774\uC5B4\uB3C4 \uBC30\uC5F4\uB85C \u2014 \uD56D\uBAA9\uB9C8\uB2E4 \uB3C4\uAD6C\uB97C \uBC18\uBCF5 \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694). \uC774\uB825\uC11C \uBCF8\uBB38(add_resume)\uACFC \uB2EC\uB9AC \uD68C\uC0AC\xB7\uC9C1\uBB34\xB7\uAE30\uAC04\xB7\uC131\uACFC\uB97C \uD544\uB4DC\uB85C \uB098\uB220 \uC800\uC7A5\uD574 \uC801\uD569\uB3C4 \uBD84\uC11D/\uC790\uC18C\uC11C \uC791\uC131 \uC2DC \uC815\uBC00\uD558\uAC8C \uD65C\uC6A9\uB429\uB2C8\uB2E4. \uAC01 \uD56D\uBAA9\uC740 company\uAC00 \uD544\uC218\uC774\uBA70, achievements\uC5D0\uB294 \uC815\uB7C9 \uC9C0\uD45C\uAC00 \uB2F4\uAE34 \uC131\uACFC\uB97C, tech\uC5D0\uB294 \uC0AC\uC6A9 \uAE30\uC220\uC744 \uBC30\uC5F4\uB85C \uB123\uC73C\uC138\uC694. \uAC19\uC740 \uD68C\uC0AC\xB7\uC9C1\uBB34\xB7\uC785\uC0AC\uC77C(start_date)\uC774 \uC77C\uCE58\uD558\uBA74 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC73C\uB2C8 \uC5EC\uB7EC \uBC88 \uC800\uC7A5\uD574\uB3C4 \uC548\uC804\uD569\uB2C8\uB2E4. \uC628\uBCF4\uB529 \uC644\uC131\uB3C4(get_onboarding_status)\uC758 has_experience\uB97C \uCC44\uC6B0\uB824\uBA74 1\uAC74 \uC774\uC0C1 \uCD94\uAC00\uD558\uC138\uC694.",
79165
+ inputSchema: ExperienceBatchInputSchema.shape,
79023
79166
  handler: (args) => {
79024
- const exp = addExperience(args);
79025
- return ok(`\uACBD\uB825 '${exp.company}${exp.role ? ` \xB7 ${exp.role}` : ""}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, exp);
79167
+ const { records, created, updated } = addExperiences(args.experiences);
79168
+ const names = records.map((e) => `${e.company}${e.role ? ` \xB7 ${e.role}` : ""}`).join(", ");
79169
+ return ok(`\uACBD\uB825 ${records.length}\uAC74\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (\uC2E0\uADDC ${created} \xB7 \uAC31\uC2E0 ${updated}): ${names}`, records);
79026
79170
  }
79027
79171
  },
79028
79172
  {
@@ -79040,11 +79184,12 @@ ${lines}`;
79040
79184
  {
79041
79185
  name: "add_project",
79042
79186
  title: "\uD504\uB85C\uC81D\uD2B8 \uCD94\uAC00",
79043
- description: "\uB300\uD45C \uD504\uB85C\uC81D\uD2B8\uB97C \uBCC4\uB3C4 \uD56D\uBAA9\uC73C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. name\uC740 \uD544\uC218\uC774\uBA70, highlights(\uD575\uC2EC \uC131\uACFC)\uC640 tech(\uC0AC\uC6A9 \uAE30\uC220)\uB97C \uBC30\uC5F4\uB85C \uB123\uC73C\uBA74 \uC790\uC18C\uC11C\xB7\uBA74\uC811 \uC900\uBE44\uC5D0\uC11C \uADFC\uAC70\uB85C \uD65C\uC6A9\uB429\uB2C8\uB2E4. url\uC5D0 \uACB0\uACFC\uBB3C \uB9C1\uD06C\uB97C \uB123\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
79044
- inputSchema: ProjectInputSchema.shape,
79187
+ description: "\uB300\uD45C \uD504\uB85C\uC81D\uD2B8\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. projects \uBC30\uC5F4\uC5D0 \uC5EC\uB7EC \uAC1C\uB97C \uD55C \uBC88\uC5D0 \uB2F4\uC544 \uD638\uCD9C\uD558\uC138\uC694(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C \u2014 \uD56D\uBAA9\uB9C8\uB2E4 \uBC18\uBCF5 \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694). \uAC01 \uD56D\uBAA9\uC740 name\uC774 \uD544\uC218\uC774\uBA70, highlights(\uD575\uC2EC \uC131\uACFC)\uC640 tech(\uC0AC\uC6A9 \uAE30\uC220)\uB97C \uBC30\uC5F4\uB85C \uB123\uC73C\uBA74 \uC790\uC18C\uC11C\xB7\uBA74\uC811 \uC900\uBE44\uC5D0\uC11C \uADFC\uAC70\uB85C \uD65C\uC6A9\uB429\uB2C8\uB2E4. url\uC5D0 \uACB0\uACFC\uBB3C \uB9C1\uD06C\uB97C \uB123\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uAC19\uC740 \uC774\uB984\uC758 \uD504\uB85C\uC81D\uD2B8\uB294 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC73C\uB2C8 \uC5EC\uB7EC \uBC88 \uC800\uC7A5\uD574\uB3C4 \uC548\uC804\uD569\uB2C8\uB2E4.",
79188
+ inputSchema: ProjectBatchInputSchema.shape,
79045
79189
  handler: (args) => {
79046
- const prj = addProject(args);
79047
- return ok(`\uD504\uB85C\uC81D\uD2B8 '${prj.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, prj);
79190
+ const { records, created, updated } = addProjects(args.projects);
79191
+ const names = records.map((p) => p.name).join(", ");
79192
+ return ok(`\uD504\uB85C\uC81D\uD2B8 ${records.length}\uAC74\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (\uC2E0\uADDC ${created} \xB7 \uAC31\uC2E0 ${updated}): ${names}`, records);
79048
79193
  }
79049
79194
  },
79050
79195
  {
@@ -79062,11 +79207,12 @@ ${lines}`;
79062
79207
  {
79063
79208
  name: "add_skill",
79064
79209
  title: "\uAE30\uC220\uC2A4\uD0DD \uCD94\uAC00",
79065
- description: "\uBCF4\uC720 \uAE30\uC220\uC2A4\uD0DD\uC744 \uBCC4\uB3C4 \uD56D\uBAA9\uC73C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. name\uC740 \uD544\uC218\uC774\uBA70, category(\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1)\xB7level\xB7years\uB85C \uC219\uB828\uB3C4\uB97C \uD568\uAED8 \uAE30\uB85D\uD558\uBA74 \uACF5\uACE0 \uD0A4\uC6CC\uB4DC \uB9E4\uCE6D\uC774 \uC815\uD655\uD574\uC9D1\uB2C8\uB2E4. \uC628\uBCF4\uB529 \uC644\uC131\uB3C4\uC758 has_skills\uB97C \uCC44\uC6B0\uB824\uBA74 \uC774 \uB3C4\uAD6C\uB85C 1\uAC74 \uC774\uC0C1 \uCD94\uAC00\uD574\uC57C \uD569\uB2C8\uB2E4. \uC5EC\uB7EC \uAC1C\uBA74 \uB3C4\uAD6C\uB97C \uBC18\uBCF5 \uD638\uCD9C\uD558\uC138\uC694.",
79066
- inputSchema: SkillInputSchema.shape,
79210
+ description: "\uBCF4\uC720 \uAE30\uC220\uC2A4\uD0DD\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. \uC5EC\uB7EC \uAC1C\uB97C skills \uBC30\uC5F4\uC5D0 \uD55C \uBC88\uC5D0 \uB2F4\uC544 \uD638\uCD9C\uD558\uC138\uC694(\uD55C \uAC1C\uC5EC\uB3C4 \uBC30\uC5F4\uB85C \u2014 \uAE30\uC220\uB9C8\uB2E4 \uB3C4\uAD6C\uB97C \uBC18\uBCF5 \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694). \uAC01 \uD56D\uBAA9\uC740 name\uC774 \uD544\uC218\uC774\uBA70, category(\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1)\xB7level\xB7years\uB85C \uC219\uB828\uB3C4\uB97C \uD568\uAED8 \uAE30\uB85D\uD558\uBA74 \uACF5\uACE0 \uD0A4\uC6CC\uB4DC \uB9E4\uCE6D\uC774 \uC815\uD655\uD574\uC9D1\uB2C8\uB2E4. \uAC19\uC740 \uC774\uB984\uC758 \uAE30\uC220\uC740 \uAC31\uC2E0\uB418\uACE0 \uC911\uBCF5 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC73C\uB2C8 \uC5EC\uB7EC \uBC88 \uC800\uC7A5\uD574\uB3C4 \uC548\uC804\uD569\uB2C8\uB2E4. \uC628\uBCF4\uB529 \uC644\uC131\uB3C4\uC758 has_skills\uB97C \uCC44\uC6B0\uB824\uBA74 1\uAC1C \uC774\uC0C1 \uCD94\uAC00\uD558\uC138\uC694.",
79211
+ inputSchema: SkillBatchInputSchema.shape,
79067
79212
  handler: (args) => {
79068
- const skill = addSkill(args);
79069
- return ok(`\uAE30\uC220 '${skill.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, skill);
79213
+ const { records, created, updated } = addSkills(args.skills);
79214
+ const names = records.map((s) => s.name).join(", ");
79215
+ return ok(`\uAE30\uC220 ${records.length}\uAC1C\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (\uC2E0\uADDC ${created} \xB7 \uAC31\uC2E0 ${updated}): ${names}`, records);
79070
79216
  }
79071
79217
  },
79072
79218
  {