careermate 0.0.3 → 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/dist/mcp.mjs CHANGED
@@ -75028,7 +75028,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
75028
75028
  const ij = i2 * width + j;
75029
75029
  const center = buf[ij];
75030
75030
  let sum = 0;
75031
- let norm = 0;
75031
+ let norm2 = 0;
75032
75032
  for (let k = 0; k < kernelSize; k++) {
75033
75033
  const y = i2 + k - halfSize;
75034
75034
  if (y < 0 || y >= height) {
@@ -75042,10 +75042,10 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
75042
75042
  const neighbour = buf[y * width + x2];
75043
75043
  const w = kernel[k * kernelSize + l2] * rangeValues[Math.abs(neighbour - center)];
75044
75044
  sum += neighbour * w;
75045
- norm += w;
75045
+ norm2 += w;
75046
75046
  }
75047
75047
  }
75048
- const pix = out[ij] = Math.round(sum / norm);
75048
+ const pix = out[ij] = Math.round(sum / norm2);
75049
75049
  histogram[pix]++;
75050
75050
  }
75051
75051
  }
@@ -84952,13 +84952,13 @@ function _array(Class2, element, params) {
84952
84952
  });
84953
84953
  }
84954
84954
  function _custom(Class2, fn, _params) {
84955
- const norm = normalizeParams(_params);
84956
- norm.abort ?? (norm.abort = true);
84955
+ const norm2 = normalizeParams(_params);
84956
+ norm2.abort ?? (norm2.abort = true);
84957
84957
  const schema = new Class2({
84958
84958
  type: "custom",
84959
84959
  check: "custom",
84960
84960
  fn,
84961
- ...norm
84961
+ ...norm2
84962
84962
  });
84963
84963
  return schema;
84964
84964
  }
@@ -92516,6 +92516,24 @@ function setVerifyStrict(on) {
92516
92516
  return on;
92517
92517
  }
92518
92518
 
92519
+ // packages/shared/src/errors.ts
92520
+ var CareerMateError = class extends Error {
92521
+ constructor(message, httpStatus = 400, code) {
92522
+ super(message);
92523
+ this.httpStatus = httpStatus;
92524
+ this.code = code;
92525
+ this.name = "CareerMateError";
92526
+ }
92527
+ httpStatus;
92528
+ code;
92529
+ };
92530
+ function notFound(message) {
92531
+ return new CareerMateError(message, 404, "not_found");
92532
+ }
92533
+ function conflict(message) {
92534
+ return new CareerMateError(message, 409, "conflict");
92535
+ }
92536
+
92519
92537
  // packages/shared/src/enums.ts
92520
92538
  var APPLICATION_STATUSES = [
92521
92539
  "draft",
@@ -92566,8 +92584,9 @@ function canTransitionStatus(from, to) {
92566
92584
  }
92567
92585
  function assertStatusTransition(from, to) {
92568
92586
  if (!canTransitionStatus(from, to)) {
92569
- throw new Error(
92570
- `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
92587
+ 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.)` : "";
92588
+ throw conflict(
92589
+ `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.${suffix}`
92571
92590
  );
92572
92591
  }
92573
92592
  }
@@ -92619,7 +92638,20 @@ var optLine = external_exports.string().max(MAX_LINE).optional();
92619
92638
  var optNote = external_exports.string().max(MAX_NOTE).optional();
92620
92639
  var optBody = external_exports.string().max(MAX_BODY).optional();
92621
92640
  var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
92622
- var strList = external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS);
92641
+ function toStrList(v) {
92642
+ if (typeof v !== "string") return v;
92643
+ const s = v.trim();
92644
+ if (!s) return [];
92645
+ if (s.startsWith("[")) {
92646
+ try {
92647
+ const parsed = JSON.parse(s);
92648
+ if (Array.isArray(parsed)) return parsed;
92649
+ } catch {
92650
+ }
92651
+ }
92652
+ return s.split(/[\n,]/).map((x2) => x2.trim()).filter(Boolean);
92653
+ }
92654
+ var strList = external_exports.preprocess(toStrList, external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS));
92623
92655
  var baseRecord = {
92624
92656
  id,
92625
92657
  created_at: isoDateTime,
@@ -92681,6 +92713,9 @@ var ExperienceRecordSchema = external_exports.object({
92681
92713
  tech: external_exports.array(external_exports.string()),
92682
92714
  order_index: external_exports.number()
92683
92715
  });
92716
+ var ExperienceBatchInputSchema = external_exports.object({
92717
+ 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.")
92718
+ });
92684
92719
  var ProjectInputSchema = external_exports.object({
92685
92720
  name: reqLine,
92686
92721
  role: optLine,
@@ -92704,6 +92739,9 @@ var ProjectRecordSchema = external_exports.object({
92704
92739
  end_date: external_exports.string().nullable(),
92705
92740
  order_index: external_exports.number()
92706
92741
  });
92742
+ var ProjectBatchInputSchema = external_exports.object({
92743
+ 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.")
92744
+ });
92707
92745
  var SkillInputSchema = external_exports.object({
92708
92746
  name: reqLine,
92709
92747
  category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
@@ -92719,6 +92757,9 @@ var SkillRecordSchema = external_exports.object({
92719
92757
  years: external_exports.number().nullable(),
92720
92758
  order_index: external_exports.number()
92721
92759
  });
92760
+ var SkillBatchInputSchema = external_exports.object({
92761
+ 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.")
92762
+ });
92722
92763
  var DocumentInputSchema = external_exports.object({
92723
92764
  kind: external_exports.enum(DOCUMENT_KINDS),
92724
92765
  title: reqLine,
@@ -92915,7 +92956,7 @@ var ApplicationContextSchema = external_exports.object({
92915
92956
  });
92916
92957
 
92917
92958
  // packages/shared/src/build-info.ts
92918
- var APP_VERSION = true ? "0.0.3" : "0.0.0-dev";
92959
+ var APP_VERSION = true ? "0.0.4" : "0.0.0-dev";
92919
92960
  var BUNDLED = true;
92920
92961
 
92921
92962
  // packages/shared/src/index.ts
@@ -92930,6 +92971,7 @@ function now() {
92930
92971
 
92931
92972
  // packages/db/src/repositories.ts
92932
92973
  var PROFILE_ID = "profile-singleton";
92974
+ var norm = (s) => (s ?? "").trim().toLowerCase();
92933
92975
  function mapProfile(r) {
92934
92976
  return {
92935
92977
  id: r.id,
@@ -93007,6 +93049,9 @@ function mapExperience(r) {
93007
93049
  updated_at: r.updated_at
93008
93050
  };
93009
93051
  }
93052
+ function expKey(company, role, start) {
93053
+ return [norm(company), norm(role), norm(start)].join("\0");
93054
+ }
93010
93055
  var experienceRepo = {
93011
93056
  list() {
93012
93057
  return getDb().prepare(`SELECT * FROM experiences ORDER BY order_index ASC, start_date DESC`).all().map(mapExperience);
@@ -93015,6 +93060,32 @@ var experienceRepo = {
93015
93060
  const r = getDb().prepare(`SELECT * FROM experiences WHERE id = ?`).get(id2);
93016
93061
  return r ? mapExperience(r) : null;
93017
93062
  },
93063
+ /** Existing experience with the same company+role+start_date, or null (for idempotent batch upserts). */
93064
+ findMatch(input) {
93065
+ const key = expKey(input.company, input.role ?? null, input.start_date ?? null);
93066
+ return this.list().find((e) => expKey(e.company, e.role, e.start_date) === key) ?? null;
93067
+ },
93068
+ /** Insert, or merge into the matching experience (omitted fields keep their stored value). */
93069
+ upsert(input) {
93070
+ const existing = this.findMatch(input);
93071
+ if (existing) return { record: this.update(existing.id, input), created: false };
93072
+ return { record: this.add(input), created: true };
93073
+ },
93074
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
93075
+ addMany(inputs) {
93076
+ return tx(() => {
93077
+ const records = [];
93078
+ let created = 0;
93079
+ let updated = 0;
93080
+ for (const input of inputs) {
93081
+ const { record: record2, created: isNew } = this.upsert(input);
93082
+ records.push(record2);
93083
+ if (isNew) created += 1;
93084
+ else updated += 1;
93085
+ }
93086
+ return { records, created, updated };
93087
+ });
93088
+ },
93018
93089
  add(input) {
93019
93090
  const ts = now();
93020
93091
  const id2 = newId("exp_");
@@ -93088,6 +93159,32 @@ var projectRepo = {
93088
93159
  const r = getDb().prepare(`SELECT * FROM projects WHERE id = ?`).get(id2);
93089
93160
  return r ? mapProject(r) : null;
93090
93161
  },
93162
+ /** Existing project with the same name (case-insensitive), or null (for idempotent batch upserts). */
93163
+ findByName(name) {
93164
+ const key = norm(name);
93165
+ return this.list().find((p) => norm(p.name) === key) ?? null;
93166
+ },
93167
+ /** Insert, or merge into the same-named project (omitted fields keep their stored value). */
93168
+ upsert(input) {
93169
+ const existing = this.findByName(input.name);
93170
+ if (existing) return { record: this.update(existing.id, input), created: false };
93171
+ return { record: this.add(input), created: true };
93172
+ },
93173
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
93174
+ addMany(inputs) {
93175
+ return tx(() => {
93176
+ const records = [];
93177
+ let created = 0;
93178
+ let updated = 0;
93179
+ for (const input of inputs) {
93180
+ const { record: record2, created: isNew } = this.upsert(input);
93181
+ records.push(record2);
93182
+ if (isNew) created += 1;
93183
+ else updated += 1;
93184
+ }
93185
+ return { records, created, updated };
93186
+ });
93187
+ },
93091
93188
  add(input) {
93092
93189
  const ts = now();
93093
93190
  const id2 = newId("prj_");
@@ -93155,6 +93252,32 @@ var skillRepo = {
93155
93252
  const r = getDb().prepare(`SELECT * FROM skills WHERE id = ?`).get(id2);
93156
93253
  return r ? mapSkill(r) : null;
93157
93254
  },
93255
+ /** Existing skill with the same name (case-insensitive), or null (for idempotent batch upserts). */
93256
+ findByName(name) {
93257
+ const key = norm(name);
93258
+ return this.list().find((s) => norm(s.name) === key) ?? null;
93259
+ },
93260
+ /** Insert, or merge into the same-named skill (omitted fields keep their stored value). */
93261
+ upsert(input) {
93262
+ const existing = this.findByName(input.name);
93263
+ if (existing) return { record: this.update(existing.id, input), created: false };
93264
+ return { record: this.add(input), created: true };
93265
+ },
93266
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
93267
+ addMany(inputs) {
93268
+ return tx(() => {
93269
+ const records = [];
93270
+ let created = 0;
93271
+ let updated = 0;
93272
+ for (const input of inputs) {
93273
+ const { record: record2, created: isNew } = this.upsert(input);
93274
+ records.push(record2);
93275
+ if (isNew) created += 1;
93276
+ else updated += 1;
93277
+ }
93278
+ return { records, created, updated };
93279
+ });
93280
+ },
93158
93281
  add(input) {
93159
93282
  const ts = now();
93160
93283
  const id2 = newId("skl_");
@@ -93984,7 +94107,7 @@ function styleSignals(text) {
93984
94107
  }
93985
94108
  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.";
93986
94109
  function lintArtifact(kind, text, corpus, opts = {}) {
93987
- const norm = normalizeText(text);
94110
+ const norm2 = normalizeText(text);
93988
94111
  const corpusAvailable = (corpus.documents + corpus.structured).trim().length > 0;
93989
94112
  const provenance = analyzeProvenance(text, corpus);
93990
94113
  const signals = styleSignals(text);
@@ -94008,7 +94131,7 @@ function lintArtifact(kind, text, corpus, opts = {}) {
94008
94131
  }
94009
94132
  return {
94010
94133
  kind,
94011
- charCount: [...norm].length,
94134
+ charCount: [...norm2].length,
94012
94135
  signals,
94013
94136
  provenance,
94014
94137
  blocking,
@@ -94068,20 +94191,40 @@ function addResume(input) {
94068
94191
  activityRepo.log("resume_added", `${doc.title} \uBB38\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "document", doc.id);
94069
94192
  return doc;
94070
94193
  }
94071
- function addExperience(input) {
94072
- const exp = experienceRepo.add(input);
94073
- activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
94074
- return exp;
94194
+ function batchNames(names) {
94195
+ const shown = names.filter(Boolean).slice(0, 8).join(", ");
94196
+ if (!shown) return "";
94197
+ return `: ${shown}${names.length > 8 ? " \uC678" : ""}`;
94075
94198
  }
94076
- function addProject(input) {
94077
- const prj = projectRepo.add(input);
94078
- activityRepo.log("profile_updated", `\uD504\uB85C\uC81D\uD2B8(${prj.name})\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "project", prj.id);
94079
- return prj;
94199
+ function addExperiences(inputs) {
94200
+ const res = experienceRepo.addMany(inputs);
94201
+ activityRepo.log(
94202
+ "profile_updated",
94203
+ `\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))}.`,
94204
+ "experience",
94205
+ res.records[0]?.id
94206
+ );
94207
+ return res;
94208
+ }
94209
+ function addProjects(inputs) {
94210
+ const res = projectRepo.addMany(inputs);
94211
+ activityRepo.log(
94212
+ "profile_updated",
94213
+ `\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))}.`,
94214
+ "project",
94215
+ res.records[0]?.id
94216
+ );
94217
+ return res;
94080
94218
  }
94081
- function addSkill(input) {
94082
- const skill = skillRepo.add(input);
94083
- activityRepo.log("profile_updated", `\uAE30\uC220(${skill.name})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "skill", skill.id);
94084
- return skill;
94219
+ function addSkills(inputs) {
94220
+ const res = skillRepo.addMany(inputs);
94221
+ activityRepo.log(
94222
+ "profile_updated",
94223
+ `\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))}.`,
94224
+ "skill",
94225
+ res.records[0]?.id
94226
+ );
94227
+ return res;
94085
94228
  }
94086
94229
  function saveJobPosting(input, id2) {
94087
94230
  const job = jobRepo.upsert(input, id2);
@@ -94091,7 +94234,7 @@ function saveJobPosting(input, id2) {
94091
94234
  }
94092
94235
  function saveFitAnalysis(input) {
94093
94236
  const job = jobRepo.get(input.job_id);
94094
- 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.`);
94237
+ 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.`);
94095
94238
  applicationRepo.ensure(job.id, "draft");
94096
94239
  const fit = fitRepo.save(input);
94097
94240
  const scoreText = fit.score != null ? ` (\uC801\uD569\uB3C4 ${fit.score}\uC810)` : "";
@@ -94106,7 +94249,7 @@ function saveCoverLetterVersion(input) {
94106
94249
  const detail = verification.blocking.map((b) => `${b.label}: ${b.detail}`).join("; ");
94107
94250
  const strictHit = verification.blocking.some((b) => b.id === "unverified_numbers_strict");
94108
94251
  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).`;
94109
- throw new Error(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`);
94252
+ throw new CareerMateError(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`, 400, "save_gate");
94110
94253
  }
94111
94254
  const { coverLetter, version: version3 } = coverLetterRepo.addVersion({
94112
94255
  cover_letter_id: input.cover_letter_id,
@@ -94133,7 +94276,7 @@ function saveCoverLetterVersion(input) {
94133
94276
  }
94134
94277
  function updateApplicationStatus(jobId, status, note) {
94135
94278
  const job = jobRepo.get(jobId);
94136
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${jobId}`);
94279
+ if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${jobId}`);
94137
94280
  const application = applicationRepo.setStatus(jobId, status, note);
94138
94281
  activityRepo.log(
94139
94282
  "application_status_changed",
@@ -94148,11 +94291,11 @@ function updateApplicationStatus(jobId, status, note) {
94148
94291
  }
94149
94292
  function saveInterviewPrep(input) {
94150
94293
  const job = jobRepo.get(input.job_id);
94151
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
94294
+ if (!job) throw notFound(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
94152
94295
  const app = applicationRepo.getByJob(job.id);
94153
94296
  if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
94154
94297
  const current = app ? APPLICATION_STATUS_LABELS[app.status] : "\uBBF8\uC9C0\uC6D0";
94155
- throw new Error(
94298
+ throw conflict(
94156
94299
  `\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.`
94157
94300
  );
94158
94301
  }
@@ -94233,7 +94376,7 @@ var WORKFLOWS = [
94233
94376
  '\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.)',
94234
94377
  "\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.",
94235
94378
  "\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).",
94236
- "\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).",
94379
+ "\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).",
94237
94380
  "\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).",
94238
94381
  "`open_dashboard`\uB97C \uD638\uCD9C\uD574 \uC0AC\uC6A9\uC790\uAC00 \uC790\uAE30 \uB370\uC774\uD130\uB97C \uD655\uC778\uD558\uAC8C \uD55C\uB2E4.",
94239
94382
  "\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."
@@ -96054,11 +96197,12 @@ ${lines}`;
96054
96197
  {
96055
96198
  name: "add_experience",
96056
96199
  title: "\uACBD\uB825 \uCD94\uAC00",
96057
- 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.",
96058
- inputSchema: ExperienceInputSchema.shape,
96200
+ 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.",
96201
+ inputSchema: ExperienceBatchInputSchema.shape,
96059
96202
  handler: (args) => {
96060
- const exp = addExperience(args);
96061
- return ok(`\uACBD\uB825 '${exp.company}${exp.role ? ` \xB7 ${exp.role}` : ""}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, exp);
96203
+ const { records, created, updated } = addExperiences(args.experiences);
96204
+ const names = records.map((e) => `${e.company}${e.role ? ` \xB7 ${e.role}` : ""}`).join(", ");
96205
+ return ok(`\uACBD\uB825 ${records.length}\uAC74\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (\uC2E0\uADDC ${created} \xB7 \uAC31\uC2E0 ${updated}): ${names}`, records);
96062
96206
  }
96063
96207
  },
96064
96208
  {
@@ -96076,11 +96220,12 @@ ${lines}`;
96076
96220
  {
96077
96221
  name: "add_project",
96078
96222
  title: "\uD504\uB85C\uC81D\uD2B8 \uCD94\uAC00",
96079
- 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.",
96080
- inputSchema: ProjectInputSchema.shape,
96223
+ 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.",
96224
+ inputSchema: ProjectBatchInputSchema.shape,
96081
96225
  handler: (args) => {
96082
- const prj = addProject(args);
96083
- return ok(`\uD504\uB85C\uC81D\uD2B8 '${prj.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, prj);
96226
+ const { records, created, updated } = addProjects(args.projects);
96227
+ const names = records.map((p) => p.name).join(", ");
96228
+ 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);
96084
96229
  }
96085
96230
  },
96086
96231
  {
@@ -96098,11 +96243,12 @@ ${lines}`;
96098
96243
  {
96099
96244
  name: "add_skill",
96100
96245
  title: "\uAE30\uC220\uC2A4\uD0DD \uCD94\uAC00",
96101
- 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.",
96102
- inputSchema: SkillInputSchema.shape,
96246
+ 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.",
96247
+ inputSchema: SkillBatchInputSchema.shape,
96103
96248
  handler: (args) => {
96104
- const skill = addSkill(args);
96105
- return ok(`\uAE30\uC220 '${skill.name}'\uC744(\uB97C) \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, skill);
96249
+ const { records, created, updated } = addSkills(args.skills);
96250
+ const names = records.map((s) => s.name).join(", ");
96251
+ return ok(`\uAE30\uC220 ${records.length}\uAC1C\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (\uC2E0\uADDC ${created} \xB7 \uAC31\uC2E0 ${updated}): ${names}`, records);
96106
96252
  }
96107
96253
  },
96108
96254
  {
package/dist/migrate.mjs CHANGED
@@ -4436,7 +4436,20 @@ var optLine = external_exports.string().max(MAX_LINE).optional();
4436
4436
  var optNote = external_exports.string().max(MAX_NOTE).optional();
4437
4437
  var optBody = external_exports.string().max(MAX_BODY).optional();
4438
4438
  var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
4439
- var strList = external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS);
4439
+ function toStrList(v) {
4440
+ if (typeof v !== "string") return v;
4441
+ const s = v.trim();
4442
+ if (!s) return [];
4443
+ if (s.startsWith("[")) {
4444
+ try {
4445
+ const parsed = JSON.parse(s);
4446
+ if (Array.isArray(parsed)) return parsed;
4447
+ } catch {
4448
+ }
4449
+ }
4450
+ return s.split(/[\n,]/).map((x) => x.trim()).filter(Boolean);
4451
+ }
4452
+ var strList = external_exports.preprocess(toStrList, external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS));
4440
4453
  var baseRecord = {
4441
4454
  id,
4442
4455
  created_at: isoDateTime,
@@ -4498,6 +4511,9 @@ var ExperienceRecordSchema = external_exports.object({
4498
4511
  tech: external_exports.array(external_exports.string()),
4499
4512
  order_index: external_exports.number()
4500
4513
  });
4514
+ var ExperienceBatchInputSchema = external_exports.object({
4515
+ 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.")
4516
+ });
4501
4517
  var ProjectInputSchema = external_exports.object({
4502
4518
  name: reqLine,
4503
4519
  role: optLine,
@@ -4521,6 +4537,9 @@ var ProjectRecordSchema = external_exports.object({
4521
4537
  end_date: external_exports.string().nullable(),
4522
4538
  order_index: external_exports.number()
4523
4539
  });
4540
+ var ProjectBatchInputSchema = external_exports.object({
4541
+ 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.")
4542
+ });
4524
4543
  var SkillInputSchema = external_exports.object({
4525
4544
  name: reqLine,
4526
4545
  category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
@@ -4536,6 +4555,9 @@ var SkillRecordSchema = external_exports.object({
4536
4555
  years: external_exports.number().nullable(),
4537
4556
  order_index: external_exports.number()
4538
4557
  });
4558
+ var SkillBatchInputSchema = external_exports.object({
4559
+ 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.")
4560
+ });
4539
4561
  var DocumentInputSchema = external_exports.object({
4540
4562
  kind: external_exports.enum(DOCUMENT_KINDS),
4541
4563
  title: reqLine,
@@ -135,7 +135,11 @@ export async function api(method, path, body) {
135
135
  const text = await res.text();
136
136
  let data = {};
137
137
  if (text) { try { data = JSON.parse(text); } catch { /* non-JSON error page */ } }
138
- if (!res.ok) throw new Error(data.error || friendlyStatus(res.status));
138
+ if (!res.ok) {
139
+ const e = new Error(data.error || friendlyStatus(res.status));
140
+ e.status = res.status; // lets callers self-heal (e.g. drop a stale row on 404)
141
+ throw e;
142
+ }
139
143
  return data;
140
144
  }
141
145
  // Friendly fallback when the server didn't supply a message — never show a raw status code.
@@ -100,6 +100,13 @@ export async function render(ctx) {
100
100
  await render(ctx); // refetch + re-render so the item moves to its new group
101
101
  } catch (err) {
102
102
  toastError(err);
103
+ if (err && err.status === 404) {
104
+ // The job/application no longer exists (e.g. a stale tab after a reset).
105
+ // Refresh so the phantom row disappears instead of erroring on every retry.
106
+ await ctx.refreshNav();
107
+ await render(ctx);
108
+ return;
109
+ }
103
110
  selectEl.value = app.status; // revert on failure
104
111
  selectEl.disabled = false;
105
112
  }
@@ -262,7 +262,13 @@ async function renderDetail(ctx, jobId) {
262
262
  if (res && res.hint) toastOk(res.hint);
263
263
  await ctx.refreshNav();
264
264
  await remount();
265
- } catch (err) { toastError(err); e.target.value = job.status; }
265
+ } catch (err) {
266
+ toastError(err);
267
+ // Stale tab: the job was removed since this page loaded. Re-render so the
268
+ // detail reflects reality (shows "not found") instead of looping the error.
269
+ if (err && err.status === 404) { await ctx.refreshNav(); await remount(); return; }
270
+ e.target.value = job.status;
271
+ }
266
272
  },
267
273
  },
268
274
  );
@@ -309,21 +315,19 @@ async function renderDetail(ctx, jobId) {
309
315
  el('div', { class: 'page-head__actions' }, headActions),
310
316
  );
311
317
 
312
- // --- Layout: two columns (info+fit left, side cards right) ----------------
313
- const left = el('div', { class: 'stack-4' },
318
+ // --- Layout ---------------------------------------------------------------
319
+ // 자기소개서·면접을 맨 위 2열로(둘 다 짧음), 그 아래 공고 정보(원문은 접힘)와
320
+ // 적합도를 전체폭으로 쌓는다. 한쪽 칼럼만 길어 빈 공간이 도드라지던 50/50
321
+ // 그리드를 대체한다. 관련 기록은 있을 때만 맨 아래.
322
+ const wrap = el('div', { class: 'stack-4' },
323
+ head,
324
+ el('div', { class: 'grid grid--2' },
325
+ CoverLettersCard(job.cover_letters),
326
+ InterviewCard(job)),
314
327
  PostingCard(job),
315
328
  FitCard(job.fit),
316
- );
317
- const right = el('div', { class: 'stack-4' },
318
- CoverLettersCard(job.cover_letters),
319
- InterviewCard(job),
320
329
  RelatedCard(job.related),
321
330
  );
322
-
323
- const wrap = el('div', { class: 'stack-4' },
324
- head,
325
- el('div', { class: 'grid grid--2' }, left, right),
326
- );
327
331
  mount(ctx.view, wrap);
328
332
  }
329
333
 
@@ -353,9 +357,12 @@ function PostingCard(job) {
353
357
  }
354
358
 
355
359
  if (job.description) {
356
- body.push(el('div', { class: 'mt-3' },
357
- el('div', { class: 'muted text-sm mb-2' }, '공고 원문'),
358
- el('div', { class: 'doc-preview' }, job.description)));
360
+ // 원문은 가장 길어 칼럼을 늘리는 주범 — 기본 접힘(아코디언)으로 둔다.
361
+ body.push(el('details', { class: 'disclosure mt-3' },
362
+ el('summary', { class: 'disclosure__summary' },
363
+ icon('chevronRight', 'disclosure__chevron'),
364
+ el('span', {}, '공고 원문')),
365
+ el('div', { class: 'doc-preview mt-2' }, job.description)));
359
366
  }
360
367
 
361
368
  return Card({ title: '공고 정보', body });
@@ -403,6 +403,15 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
403
403
  /* ----------------------------------------------------------------- doc preview */
404
404
  .doc-preview { white-space: pre-wrap; word-break: break-word; font-size: 15px; line-height: 1.75; color: var(--text);
405
405
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
406
+
407
+ /* disclosure — native <details> for collapsing long content (예: 공고 원문) */
408
+ .disclosure__summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
409
+ list-style: none; color: var(--text-secondary); font-size: 13px; font-weight: 600; user-select: none; }
410
+ .disclosure__summary::-webkit-details-marker { display: none; }
411
+ .disclosure__summary:hover { color: var(--text); }
412
+ .disclosure__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
413
+ .disclosure__chevron { width: 16px; height: 16px; flex: none; transition: transform .15s ease; }
414
+ .disclosure[open] .disclosure__chevron { transform: rotate(90deg); }
406
415
  .kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
407
416
  .kv dt { color: var(--text-tertiary); font-size: 13px; }
408
417
  /* min-width:0 overrides a grid item's default min-width:auto so the 1fr column