careermate 0.0.3 → 0.0.5

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/web.mjs CHANGED
@@ -6746,15 +6746,15 @@ var require_generators = __commonJS({
6746
6746
  var stack = new Error().stack;
6747
6747
  return function() {
6748
6748
  var generator = generatorFunction.apply(this, arguments);
6749
- var spawn2 = new PromiseSpawn$(
6749
+ var spawn3 = new PromiseSpawn$(
6750
6750
  void 0,
6751
6751
  void 0,
6752
6752
  yieldHandler,
6753
6753
  stack
6754
6754
  );
6755
- var ret2 = spawn2.promise();
6756
- spawn2._generator = generator;
6757
- spawn2._promiseFulfilled(void 0);
6755
+ var ret2 = spawn3.promise();
6756
+ spawn3._generator = generator;
6757
+ spawn3._promiseFulfilled(void 0);
6758
6758
  return ret2;
6759
6759
  };
6760
6760
  };
@@ -6769,9 +6769,9 @@ var require_generators = __commonJS({
6769
6769
  if (typeof generatorFunction !== "function") {
6770
6770
  return apiRejection("generatorFunction must be a function\n\n See http://goo.gl/MqrFmX\n");
6771
6771
  }
6772
- var spawn2 = new PromiseSpawn(generatorFunction, this);
6773
- var ret2 = spawn2.promise();
6774
- spawn2._run(Promise2.spawn);
6772
+ var spawn3 = new PromiseSpawn(generatorFunction, this);
6773
+ var ret2 = spawn3.promise();
6774
+ spawn3._run(Promise2.spawn);
6775
6775
  return ret2;
6776
6776
  };
6777
6777
  };
@@ -28299,7 +28299,7 @@ var require_files = __commonJS({
28299
28299
  "use strict";
28300
28300
  var fs5 = __require("fs");
28301
28301
  var url = __require("url");
28302
- var os2 = __require("os");
28302
+ var os3 = __require("os");
28303
28303
  var dirname = __require("path").dirname;
28304
28304
  var resolvePath = __require("path").resolve;
28305
28305
  var isAbsolutePath = require_path_is_absolute();
@@ -28341,7 +28341,7 @@ var require_files = __commonJS({
28341
28341
  var readFile = promises.promisify(fs5.readFile.bind(fs5));
28342
28342
  function uriToPath(uriString, platform) {
28343
28343
  if (!platform) {
28344
- platform = os2.platform();
28344
+ platform = os3.platform();
28345
28345
  }
28346
28346
  var uri = url.parse(uriString);
28347
28347
  if (isLocalFileUri(uri) || isRelativeUri(uri)) {
@@ -39354,12 +39354,12 @@ var require_cjs = __commonJS({
39354
39354
  });
39355
39355
 
39356
39356
  // apps/web/src/index.ts
39357
- import { spawn } from "node:child_process";
39357
+ import { spawn as spawn2 } from "node:child_process";
39358
39358
 
39359
39359
  // apps/web/src/server.ts
39360
39360
  import http from "node:http";
39361
39361
  import path5 from "node:path";
39362
- import { fileURLToPath } from "node:url";
39362
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
39363
39363
 
39364
39364
  // packages/db/src/paths.ts
39365
39365
  import os from "node:os";
@@ -39766,6 +39766,24 @@ function setVerifyStrict(on) {
39766
39766
  return on;
39767
39767
  }
39768
39768
 
39769
+ // packages/shared/src/errors.ts
39770
+ var CareerMateError = class extends Error {
39771
+ constructor(message, httpStatus = 400, code) {
39772
+ super(message);
39773
+ this.httpStatus = httpStatus;
39774
+ this.code = code;
39775
+ this.name = "CareerMateError";
39776
+ }
39777
+ httpStatus;
39778
+ code;
39779
+ };
39780
+ function notFound(message) {
39781
+ return new CareerMateError(message, 404, "not_found");
39782
+ }
39783
+ function conflict(message) {
39784
+ return new CareerMateError(message, 409, "conflict");
39785
+ }
39786
+
39769
39787
  // packages/shared/src/enums.ts
39770
39788
  var APPLICATION_STATUSES = [
39771
39789
  "draft",
@@ -39826,8 +39844,9 @@ function canTransitionStatus(from, to) {
39826
39844
  }
39827
39845
  function assertStatusTransition(from, to) {
39828
39846
  if (!canTransitionStatus(from, to)) {
39829
- throw new Error(
39830
- `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
39847
+ 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.)` : "";
39848
+ throw conflict(
39849
+ `'${APPLICATION_STATUS_LABELS[from]}'\uC5D0\uC11C '${APPLICATION_STATUS_LABELS[to]}'(\uC73C)\uB85C\uB294 \uC0C1\uD0DC\uB97C \uBC14\uAFC0 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.${suffix}`
39831
39850
  );
39832
39851
  }
39833
39852
  }
@@ -43926,7 +43945,20 @@ var optLine = external_exports.string().max(MAX_LINE).optional();
43926
43945
  var optNote = external_exports.string().max(MAX_NOTE).optional();
43927
43946
  var optBody = external_exports.string().max(MAX_BODY).optional();
43928
43947
  var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
43929
- var strList = external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS);
43948
+ function toStrList(v) {
43949
+ if (typeof v !== "string") return v;
43950
+ const s = v.trim();
43951
+ if (!s) return [];
43952
+ if (s.startsWith("[")) {
43953
+ try {
43954
+ const parsed = JSON.parse(s);
43955
+ if (Array.isArray(parsed)) return parsed;
43956
+ } catch {
43957
+ }
43958
+ }
43959
+ return s.split(/[\n,]/).map((x) => x.trim()).filter(Boolean);
43960
+ }
43961
+ var strList = external_exports.preprocess(toStrList, external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS));
43930
43962
  var baseRecord = {
43931
43963
  id,
43932
43964
  created_at: isoDateTime,
@@ -43988,6 +44020,9 @@ var ExperienceRecordSchema = external_exports.object({
43988
44020
  tech: external_exports.array(external_exports.string()),
43989
44021
  order_index: external_exports.number()
43990
44022
  });
44023
+ var ExperienceBatchInputSchema = external_exports.object({
44024
+ 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.")
44025
+ });
43991
44026
  var ProjectInputSchema = external_exports.object({
43992
44027
  name: reqLine,
43993
44028
  role: optLine,
@@ -44011,6 +44046,9 @@ var ProjectRecordSchema = external_exports.object({
44011
44046
  end_date: external_exports.string().nullable(),
44012
44047
  order_index: external_exports.number()
44013
44048
  });
44049
+ var ProjectBatchInputSchema = external_exports.object({
44050
+ 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.")
44051
+ });
44014
44052
  var SkillInputSchema = external_exports.object({
44015
44053
  name: reqLine,
44016
44054
  category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
@@ -44026,6 +44064,9 @@ var SkillRecordSchema = external_exports.object({
44026
44064
  years: external_exports.number().nullable(),
44027
44065
  order_index: external_exports.number()
44028
44066
  });
44067
+ var SkillBatchInputSchema = external_exports.object({
44068
+ 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.")
44069
+ });
44029
44070
  var DocumentInputSchema = external_exports.object({
44030
44071
  kind: external_exports.enum(DOCUMENT_KINDS),
44031
44072
  title: reqLine,
@@ -44222,7 +44263,7 @@ var ApplicationContextSchema = external_exports.object({
44222
44263
  });
44223
44264
 
44224
44265
  // packages/shared/src/build-info.ts
44225
- var APP_VERSION = true ? "0.0.3" : "0.0.0-dev";
44266
+ var APP_VERSION = true ? "0.0.5" : "0.0.0-dev";
44226
44267
  var BUNDLED = true;
44227
44268
 
44228
44269
  // packages/shared/src/index.ts
@@ -44237,6 +44278,7 @@ function now() {
44237
44278
 
44238
44279
  // packages/db/src/repositories.ts
44239
44280
  var PROFILE_ID = "profile-singleton";
44281
+ var norm = (s) => (s ?? "").trim().toLowerCase();
44240
44282
  function mapProfile(r) {
44241
44283
  return {
44242
44284
  id: r.id,
@@ -44314,6 +44356,9 @@ function mapExperience(r) {
44314
44356
  updated_at: r.updated_at
44315
44357
  };
44316
44358
  }
44359
+ function expKey(company, role, start) {
44360
+ return [norm(company), norm(role), norm(start)].join("\0");
44361
+ }
44317
44362
  var experienceRepo = {
44318
44363
  list() {
44319
44364
  return getDb().prepare(`SELECT * FROM experiences ORDER BY order_index ASC, start_date DESC`).all().map(mapExperience);
@@ -44322,6 +44367,32 @@ var experienceRepo = {
44322
44367
  const r = getDb().prepare(`SELECT * FROM experiences WHERE id = ?`).get(id3);
44323
44368
  return r ? mapExperience(r) : null;
44324
44369
  },
44370
+ /** Existing experience with the same company+role+start_date, or null (for idempotent batch upserts). */
44371
+ findMatch(input) {
44372
+ const key = expKey(input.company, input.role ?? null, input.start_date ?? null);
44373
+ return this.list().find((e) => expKey(e.company, e.role, e.start_date) === key) ?? null;
44374
+ },
44375
+ /** Insert, or merge into the matching experience (omitted fields keep their stored value). */
44376
+ upsert(input) {
44377
+ const existing = this.findMatch(input);
44378
+ if (existing) return { record: this.update(existing.id, input), created: false };
44379
+ return { record: this.add(input), created: true };
44380
+ },
44381
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
44382
+ addMany(inputs) {
44383
+ return tx(() => {
44384
+ const records = [];
44385
+ let created = 0;
44386
+ let updated = 0;
44387
+ for (const input of inputs) {
44388
+ const { record, created: isNew } = this.upsert(input);
44389
+ records.push(record);
44390
+ if (isNew) created += 1;
44391
+ else updated += 1;
44392
+ }
44393
+ return { records, created, updated };
44394
+ });
44395
+ },
44325
44396
  add(input) {
44326
44397
  const ts = now();
44327
44398
  const id3 = newId("exp_");
@@ -44395,6 +44466,32 @@ var projectRepo = {
44395
44466
  const r = getDb().prepare(`SELECT * FROM projects WHERE id = ?`).get(id3);
44396
44467
  return r ? mapProject(r) : null;
44397
44468
  },
44469
+ /** Existing project with the same name (case-insensitive), or null (for idempotent batch upserts). */
44470
+ findByName(name) {
44471
+ const key = norm(name);
44472
+ return this.list().find((p) => norm(p.name) === key) ?? null;
44473
+ },
44474
+ /** Insert, or merge into the same-named project (omitted fields keep their stored value). */
44475
+ upsert(input) {
44476
+ const existing = this.findByName(input.name);
44477
+ if (existing) return { record: this.update(existing.id, input), created: false };
44478
+ return { record: this.add(input), created: true };
44479
+ },
44480
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
44481
+ addMany(inputs) {
44482
+ return tx(() => {
44483
+ const records = [];
44484
+ let created = 0;
44485
+ let updated = 0;
44486
+ for (const input of inputs) {
44487
+ const { record, created: isNew } = this.upsert(input);
44488
+ records.push(record);
44489
+ if (isNew) created += 1;
44490
+ else updated += 1;
44491
+ }
44492
+ return { records, created, updated };
44493
+ });
44494
+ },
44398
44495
  add(input) {
44399
44496
  const ts = now();
44400
44497
  const id3 = newId("prj_");
@@ -44462,6 +44559,32 @@ var skillRepo = {
44462
44559
  const r = getDb().prepare(`SELECT * FROM skills WHERE id = ?`).get(id3);
44463
44560
  return r ? mapSkill(r) : null;
44464
44561
  },
44562
+ /** Existing skill with the same name (case-insensitive), or null (for idempotent batch upserts). */
44563
+ findByName(name) {
44564
+ const key = norm(name);
44565
+ return this.list().find((s) => norm(s.name) === key) ?? null;
44566
+ },
44567
+ /** Insert, or merge into the same-named skill (omitted fields keep their stored value). */
44568
+ upsert(input) {
44569
+ const existing = this.findByName(input.name);
44570
+ if (existing) return { record: this.update(existing.id, input), created: false };
44571
+ return { record: this.add(input), created: true };
44572
+ },
44573
+ /** Batch insert/merge in one transaction; within-batch duplicates collapse onto one row. */
44574
+ addMany(inputs) {
44575
+ return tx(() => {
44576
+ const records = [];
44577
+ let created = 0;
44578
+ let updated = 0;
44579
+ for (const input of inputs) {
44580
+ const { record, created: isNew } = this.upsert(input);
44581
+ records.push(record);
44582
+ if (isNew) created += 1;
44583
+ else updated += 1;
44584
+ }
44585
+ return { records, created, updated };
44586
+ });
44587
+ },
44465
44588
  add(input) {
44466
44589
  const ts = now();
44467
44590
  const id3 = newId("skl_");
@@ -44652,6 +44775,7 @@ var coverLetterRepo = {
44652
44775
  remove(id3) {
44653
44776
  const db = getDb();
44654
44777
  return tx(() => {
44778
+ db.prepare(`UPDATE applications SET cover_letter_id=NULL, updated_at=? WHERE cover_letter_id=?`).run(now(), id3);
44655
44779
  db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id3);
44656
44780
  return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id3).changes > 0;
44657
44781
  });
@@ -44740,6 +44864,7 @@ var jobRepo = {
44740
44864
  remove(id3) {
44741
44865
  const db = getDb();
44742
44866
  return tx(() => {
44867
+ db.prepare(`UPDATE cover_letters SET job_id=NULL, updated_at=? WHERE job_id=?`).run(now(), id3);
44743
44868
  db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id3);
44744
44869
  db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id3);
44745
44870
  db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id3);
@@ -45028,7 +45153,7 @@ function injectToken(html) {
45028
45153
  // apps/web/src/http.ts
45029
45154
  import fs3 from "node:fs";
45030
45155
  import path3 from "node:path";
45031
- var MAX_BODY2 = 8 * 1024 * 1024;
45156
+ var MAX_BODY2 = 32 * 1024 * 1024;
45032
45157
  var Router = class {
45033
45158
  routes = [];
45034
45159
  add(method, path6, handler) {
@@ -45536,7 +45661,7 @@ function styleSignals(text) {
45536
45661
  }
45537
45662
  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.";
45538
45663
  function lintArtifact(kind, text, corpus, opts = {}) {
45539
- const norm = normalizeText(text);
45664
+ const norm2 = normalizeText(text);
45540
45665
  const corpusAvailable = (corpus.documents + corpus.structured).trim().length > 0;
45541
45666
  const provenance = analyzeProvenance(text, corpus);
45542
45667
  const signals = styleSignals(text);
@@ -45560,7 +45685,7 @@ function lintArtifact(kind, text, corpus, opts = {}) {
45560
45685
  }
45561
45686
  return {
45562
45687
  kind,
45563
- charCount: [...norm].length,
45688
+ charCount: [...norm2].length,
45564
45689
  signals,
45565
45690
  provenance,
45566
45691
  blocking,
@@ -45623,7 +45748,7 @@ function saveJobPosting(input, id3) {
45623
45748
  }
45624
45749
  function saveFitAnalysis(input) {
45625
45750
  const job = jobRepo.get(input.job_id);
45626
- 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.`);
45751
+ if (!job) throw notFound("\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uACF5\uACE0\uB97C \uC800\uC7A5\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.");
45627
45752
  applicationRepo.ensure(job.id, "draft");
45628
45753
  const fit = fitRepo.save(input);
45629
45754
  const scoreText = fit.score != null ? ` (\uC801\uD569\uB3C4 ${fit.score}\uC810)` : "";
@@ -45638,7 +45763,7 @@ function saveCoverLetterVersion(input) {
45638
45763
  const detail = verification.blocking.map((b) => `${b.label}: ${b.detail}`).join("; ");
45639
45764
  const strictHit = verification.blocking.some((b) => b.id === "unverified_numbers_strict");
45640
45765
  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).`;
45641
- throw new Error(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`);
45766
+ throw new CareerMateError(`\uC800\uC7A5 \uC804 \uC790\uB3D9 \uC810\uAC80\uC5D0\uC11C \uB9C9\uD614\uC2B5\uB2C8\uB2E4 \u2014 ${detail}. ${tail}`, 400, "save_gate");
45642
45767
  }
45643
45768
  const { coverLetter, version } = coverLetterRepo.addVersion({
45644
45769
  cover_letter_id: input.cover_letter_id,
@@ -45665,7 +45790,7 @@ function saveCoverLetterVersion(input) {
45665
45790
  }
45666
45791
  function updateApplicationStatus(jobId, status, note) {
45667
45792
  const job = jobRepo.get(jobId);
45668
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${jobId}`);
45793
+ if (!job) throw notFound("\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
45669
45794
  const application = applicationRepo.setStatus(jobId, status, note);
45670
45795
  activityRepo.log(
45671
45796
  "application_status_changed",
@@ -45675,17 +45800,17 @@ function updateApplicationStatus(jobId, status, note) {
45675
45800
  );
45676
45801
  const interview_unlocked = INTERVIEW_UNLOCK_STATUSES.includes(status);
45677
45802
  const hasPrep = !!interviewRepo.getByJob(jobId);
45678
- const hint = interview_unlocked && !hasPrep ? "\uC11C\uB958 \uB2E8\uACC4\uB97C \uD1B5\uACFC\uD588\uC5B4\uC694. \uC774 \uACF5\uACE0 \uAE30\uC900\uC73C\uB85C \uC608\uC0C1 \uBA74\uC811 \uC9C8\uBB38\uACFC 1\uBD84 \uC790\uAE30\uC18C\uAC1C\uB97C \uC900\uBE44\uD560\uAE4C\uC694? (save_interview_prep)" : null;
45803
+ const hint = interview_unlocked && !hasPrep ? "\uC11C\uB958 \uB2E8\uACC4\uB97C \uD1B5\uACFC\uD588\uC5B4\uC694. \uC774 \uACF5\uACE0 \uAE30\uC900\uC73C\uB85C \uC608\uC0C1 \uBA74\uC811 \uC9C8\uBB38\uACFC 1\uBD84 \uC790\uAE30\uC18C\uAC1C\uB97C \uC900\uBE44\uD560\uAE4C\uC694?" : null;
45679
45804
  return { application, job, interview_unlocked, hint };
45680
45805
  }
45681
45806
  function saveInterviewPrep(input) {
45682
45807
  const job = jobRepo.get(input.job_id);
45683
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
45808
+ if (!job) throw notFound("\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
45684
45809
  const app = applicationRepo.getByJob(job.id);
45685
45810
  if (!app || !INTERVIEW_UNLOCK_STATUSES.includes(app.status)) {
45686
45811
  const current = app ? APPLICATION_STATUS_LABELS[app.status] : "\uBBF8\uC9C0\uC6D0";
45687
- throw new Error(
45688
- `\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.`
45812
+ throw conflict(
45813
+ `\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 \uC9C0\uC6D0 \uC0C1\uD0DC\uB97C '\uC11C\uB958 \uD569\uACA9' \uC774\uC0C1\uC73C\uB85C \uBC14\uAFBC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
45689
45814
  );
45690
45815
  }
45691
45816
  const prep = interviewRepo.save(input);
@@ -45773,17 +45898,25 @@ CareerMate\uB294 \uC0AC\uC6A9\uC790\uC758 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uB3D9\
45773
45898
 
45774
45899
  6. \uC0AC\uC6A9\uC790\uC5D0\uAC8C\uB294 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB9BD\uB2C8\uB2E4.
45775
45900
  - "MCP", "\uD234 \uD638\uCD9C", "job_id" \uAC19\uC740 \uAE30\uC220 \uC6A9\uC5B4\uB294 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC9C1\uC811 \uB178\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694. "\uACF5\uACE0\uB97C \uC800\uC7A5\uD588\uC5B4\uC694", "\uC801\uD569\uB3C4\uB97C \uBD84\uC11D\uD588\uC5B4\uC694"\uCC98\uB7FC \uC77C\uC0C1\uC801\uC778 \uD45C\uD604\uC73C\uB85C \uC9C4\uD589 \uC0C1\uD669\uC744 \uC54C\uB9AC\uC138\uC694.
45901
+ - \uB3C4\uAD6C \uC751\uB2F5\uC5D0 \`user_message\`\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBCF4\uACE0\uD560 \uB54C \uADF8 \uBB38\uC7A5\uC744 \uC6B0\uC120 \uD65C\uC6A9\uD558\uC138\uC694. \`id\`, \`job_id\`, \`cover_letter_id\`, \`workflow_id\`, JSON \uC6D0\uBB38, \uB3C4\uAD6C \uC774\uB984\uC740 \uB0B4\uBD80 \uC791\uC5C5\uC6A9\uC73C\uB85C\uB9CC \uC4F0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uADF8\uB300\uB85C \uBCF5\uC0AC\uD558\uC9C0 \uB9C8\uC138\uC694.
45902
+ - \uB3C4\uAD6C \uC751\uB2F5\uC5D0 \`suggested_next_action\`\uC774 \uC788\uC73C\uBA74 \uB2E4\uC74C \uD589\uB3D9\uC744 \uAC15\uC694\uD558\uC9C0 \uB9D0\uACE0 \uC120\uD0DD\uC9C0\uB85C \uC81C\uC548\uD558\uC138\uC694.
45776
45903
  - \uBB34\uC5C7\uC744 \uD588\uACE0, \uBB34\uC5C7\uC744 \uC54C\uAC8C \uB418\uC5C8\uACE0, \uB2E4\uC74C\uC5D0 \uBB34\uC5C7\uC744 \uD560 \uC218 \uC788\uB294\uC9C0 \uC9E7\uACE0 \uBA85\uD655\uD558\uAC8C \uC804\uB2EC\uD558\uC138\uC694.
45777
45904
 
45778
45905
  7. \uB2E4\uC74C \uB2E8\uACC4\uB97C \uBA3C\uC800 \uC81C\uC548\uD569\uB2C8\uB2E4.
45779
45906
  - \uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC790\uC5F0\uC2A4\uB7EC\uC6B4 \uB2E4\uC74C \uD589\uB3D9\uC744 \uC81C\uC548\uD558\uC138\uC694. \uC608: \uACF5\uACE0 \uBD84\uC11D \uD6C4 \u2192 "\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694?", \uC790\uAE30\uC18C\uAC1C\uC11C \uC800\uC7A5 \uD6C4 \u2192 "\uC9C0\uC6D0 \uC0C1\uD0DC\uB97C '\uC9C0\uC6D0 \uC644\uB8CC'\uB85C \uBC14\uAFC0\uAE4C\uC694?", \uC0C1\uD0DC\uAC00 '\uC11C\uB958 \uD569\uACA9'\uC774 \uB418\uBA74 \u2192 "\uBA74\uC811 \uC900\uBE44\uB97C \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?".
45907
+ - \uACF5\uACE0 \uBD84\uC11D \uD6C4\uC5D0\uB294 \uACE7\uC7A5 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC4F0\uC9C0 \uB9D0\uACE0, \uBC18\uB4DC\uC2DC \uC120\uD0DD\uC9C0\uB97C \uBCF4\uC5EC\uC8FC\uC138\uC694: "1. \uB124, \uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uC918" / "2. \uC544\uB2C8\uC694, \uBD84\uC11D\uB9CC \uC800\uC7A5\uD574\uC918". \uC0AC\uC6A9\uC790\uAC00 1\uBC88\uC744 \uACE0\uB978 \uACBD\uC6B0\uC5D0\uB9CC \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131 \uD750\uB984\uC73C\uB85C \uC774\uC5B4\uAC11\uB2C8\uB2E4.
45780
45908
  - \`get_onboarding_status\`\uC758 next_steps\uB098 \`open_dashboard\`/\`open_application\`\uC744 \uD65C\uC6A9\uD574 \uC0AC\uC6A9\uC790\uB97C \uB2E4\uC74C \uD589\uB3D9\uC73C\uB85C \uC548\uB0B4\uD558\uC138\uC694.
45781
45909
 
45782
- 8. \uAE00\uC740 AI \uD2F0 \uC548 \uB098\uAC8C \uC501\uB2C8\uB2E4.
45910
+ 8. \uC5C5\uB370\uC774\uD2B8 \uC694\uCCAD\uC740 \uBA85\uD655\uD558\uAC8C \uCC98\uB9AC\uD569\uB2C8\uB2E4.
45911
+ - \uC0AC\uC6A9\uC790\uAC00 "\uCD5C\uC2E0\uC774\uC57C?", "\uC5C5\uB370\uC774\uD2B8 \uC788\uC5B4?"\uCC98\uB7FC \uD655\uC778\uB9CC \uC694\uCCAD\uD558\uBA74 \`check_for_update\`\uB97C \uD638\uCD9C\uD558\uACE0 \uACB0\uACFC\uB97C \uC26C\uC6B4 \uB9D0\uB85C \uC54C\uB824\uC8FC\uC138\uC694.
45912
+ - \uC0AC\uC6A9\uC790\uAC00 "\uC5C5\uB370\uC774\uD2B8\uD574\uC918", "CareerMate \uC5C5\uB370\uC774\uD2B8\uD574\uC918", "\uCD5C\uC2E0\uC73C\uB85C \uC62C\uB824\uC918"\uCC98\uB7FC \uC801\uC6A9\uC744 \uBA85\uC2DC\uD558\uBA74 \`update_careermate\`\uB97C \uBC14\uB85C \uD638\uCD9C\uD558\uC138\uC694. \uC774 \uB3C4\uAD6C\uAC00 \uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778\uB3C4 \uD568\uAED8 \uD569\uB2C8\uB2E4.
45913
+ - \uC5C5\uB370\uC774\uD2B8 \uC131\uACF5 \uD6C4\uC5D0\uB294 \uC801\uC6A9\uD558\uB824\uBA74 AI \uC571 \uB610\uB294 MCP \uC5F0\uACB0\uC744 \uC7AC\uC2DC\uC791\uD574\uC57C \uD55C\uB2E4\uACE0 \uC548\uB0B4\uD558\uC138\uC694. \uC0AC\uC6A9\uC790 \uCEE4\uB9AC\uC5B4 \uB370\uC774\uD130\uB294 \uB85C\uCEEC\uC5D0 \uBCF4\uC874\uB418\uACE0, npm\uC5D0\uB294 \uACF5\uAC1C \uBC84\uC804 \uC815\uBCF4 \uD655\uC778/\uC124\uCE58 \uC694\uCCAD\uB9CC \uB098\uAC11\uB2C8\uB2E4.
45914
+
45915
+ 9. \uAE00\uC740 AI \uD2F0 \uC548 \uB098\uAC8C \uC501\uB2C8\uB2E4.
45783
45916
  - \uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC790\uAE30 PR\xB7\uC9C0\uC6D0 \uBA54\uC77C \uB4F1 \uC0AC\uB78C\uC774 \uC4F4 \uB4EF\uD55C \uAE00\uC744 \uC791\uC131\uD558\uAE30 \uC9C1\uC804\uC5D0\uB294 \`get_writing_style_guide\`\uB97C \uD638\uCD9C\uD574 \uAE00\uC4F0\uAE30 \uADDC\uCE59\uC744 \uAC00\uC838\uC624\uACE0 \uADF8\uB300\uB85C \uC801\uC6A9\uD558\uC138\uC694.
45784
45917
  - \uBC88\uC5ED\uD22C, "\uC5F4\uC815\uC744 \uAC00\uC9C0\uACE0/\uADC0\uC0AC\uC758 \uBC1C\uC804\uC5D0 \uAE30\uC5EC" \uAC19\uC740 \uD074\uB9AC\uC170, \uAE30\uACC4\uC801 \uBCD1\uB82C(\uCCAB\uC9F8\xB7\uB458\uC9F8\xB7\uC14B\uC9F8), \uC0C1\uD22C\uC801 \uC5F0\uACB0\uC5B4, \uADE0\uC77C\uD55C \uBB38\uC7A5 \uB9AC\uB4EC\uC744 \uD53C\uD558\uACE0 \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801\uC73C\uB85C \uC501\uB2C8\uB2E4. \uB2E8, \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uB294 \uC808\uB300 \uBC14\uAFB8\uAC70\uB098 \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
45785
45918
 
45786
- 9. \uC804\uBB38\uAC00 \uC808\uCC28\uB85C \uB9CC\uB4E4\uACE0, \uC800\uC7A5 \uC804 \uB2F9\uC2E0\uC774 \uC790\uAC00\uAC80\uC99D\uD569\uB2C8\uB2E4.
45919
+ 10. \uC804\uBB38\uAC00 \uC808\uCC28\uB85C \uB9CC\uB4E4\uACE0, \uC800\uC7A5 \uC804 \uB2F9\uC2E0\uC774 \uC790\uAC00\uAC80\uC99D\uD569\uB2C8\uB2E4.
45787
45920
  - \uACF5\uACE0 \uBD84\uC11D\xB7\uC790\uC18C\uC11C\xB7\uBA74\uC811 \uC790\uB8CC\uCC98\uB7FC \uC0AC\uC6A9\uC790 \uB300\uBA74 \uACB0\uACFC\uB97C \uB9CC\uB4E4\uAE30 \uC804\uC5D0\uB294, \uC989\uB2F5\uD558\uC9C0 \uB9D0\uACE0 \`get_workflow_guide\`(workflow_id \uC9C0\uC815)\uB97C \uBA3C\uC800 \uD638\uCD9C\uD574 \uADF8 \uC791\uC5C5\uC758 \uC804\uBB38\uAC00 \uC2E4\uD589 \uC808\uCC28(EOP)\uC640 \uC801\uC6A9\uD560 \uD50C\uB808\uC774\uBD81\xB7\uAC80\uC99D\uAE30 \uC21C\uC11C\uB97C \uBC1B\uC73C\uC138\uC694. \uC774\uC5B4 \`get_playbook\`\uC73C\uB85C \uB3C4\uBA54\uC778 \uC9C0\uC2DD\uC744 \uBC1B\uC544 \uC791\uC131\uD558\uACE0, save\uB85C \uC800\uC7A5\uD558\uAE30 \uC9C1\uC804 \`get_verifier\`\uB85C \uAC80\uC99D \uB8E8\uBE0C\uB9AD\uC744 \uBC1B\uC544 **\uB2F9\uC2E0\uC774 \uC9C1\uC811** \uC810\uAC80\xB7\uC218\uC815\uD55C \uB4A4 \uC800\uC7A5\uD569\uB2C8\uB2E4.
45788
45921
  - CareerMate\uB294 \uC2AC\uB86D \uBC00\uB3C4\xB7\uD0A4\uC6CC\uB4DC \uB9E4\uCE6D\uCC98\uB7FC \uC140 \uC218 \uC788\uB294 \uD56D\uBAA9\uB9CC \uBCF4\uC870\uB85C \uB3D5\uACE0, \uC758\uBBF8 \uD310\uB2E8(\uC774 \uAE00\uC774 \uC9C8\uBB38\uC5D0 \uB2F5\uD558\uB294\uAC00\xB7\uD1A4\uC774 \uB9DE\uB294\uAC00)\uC740 \uB2F9\uC2E0\uC774 \uD569\uB2C8\uB2E4. \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uB294 \uADF8\uB300\uB85C \uBCF4\uC874\uD569\uB2C8\uB2E4.
45789
45922
 
@@ -45838,7 +45971,7 @@ var ONBOARDING_PROMPT = `\uC774\uC81C CareerMate\uC5D0 \uC5F0\uACB0\uB418\uC5C8\
45838
45971
  - \uC774\uB984, \uC5F0\uB77D\uCC98, \uD55C \uC904 \uC18C\uAC1C(headline), \uC790\uAE30\uC18C\uAC1C \uC694\uC57D(summary), \uD76C\uB9DD \uC9C1\uBB34(desired_roles), \uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74(desired_conditions)\uC744 \uC815\uB9AC\uD569\uB2C8\uB2E4.
45839
45972
  - \uAE00\uC4F0\uAE30 \uC120\uD638\uB3C4 \uD568\uAED8 \uBB3C\uC5B4\uBCF4\uC138\uC694: \uC120\uD638 \uBB38\uCCB4(preferred_tone, \uC608: "\uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801")\uC640 \uAC15\uC870\uD558\uACE0 \uC2F6\uC740 \uD575\uC2EC \uD3EC\uC778\uD2B8(emphasis_points). \uC774 \uC815\uBCF4\uB294 \uC774\uD6C4 \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131\uC5D0 \uADF8\uB300\uB85C \uD65C\uC6A9\uB429\uB2C8\uB2E4.
45840
45973
  - \uC815\uB9AC\uD55C \uB0B4\uC6A9\uC744 \`save_profile\`\uB85C \uC800\uC7A5\uD558\uC138\uC694. \uD30C\uC77C\uC5D0\uC11C \uC77D\uC5B4\uC628 \uC815\uBCF4\uB294 source\uB97C 'upload', \uC0AC\uC6A9\uC790\uAC00 \uB9D0\uB85C \uC54C\uB824\uC900 \uC815\uBCF4\uB294 'manual'\uB85C \uD45C\uC2DC\uD558\uC138\uC694.
45841
- - \uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC \uC815\uBCF4\uAC00 \uC788\uC73C\uBA74 \`add_experience\`\xB7\`add_project\`\xB7\`add_skill\`\uB85C \uAC01\uAC01 \uC800\uC7A5\uD558\uC138\uC694(\uACBD\uB825 achievements\uB294 \uAC00\uB2A5\uD558\uBA74 \uC815\uB7C9 \uC9C0\uD45C \uD3EC\uD568). \uC774 \uD56D\uBAA9\uB4E4\uC740 \uC628\uBCF4\uB529 \uC644\uC131\uB3C4(has_experience/has_skills)\uC5D0 \uBC18\uC601\uB429\uB2C8\uB2E4.
45974
+ - \uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC \uC815\uBCF4\uAC00 \uC788\uC73C\uBA74 \`add_experience\`\xB7\`add_project\`\xB7\`add_skill\`\uB85C \uC800\uC7A5\uD558\uC138\uC694. \uAC01 \uB3C4\uAD6C\uB294 \uBC30\uC5F4\uC744 \uBC1B\uC73C\uB2C8 \uD30C\uC545\uD55C \uD56D\uBAA9\uC744 \uD55C \uBC88\uC5D0 \uBAA8\uC544 \uB118\uAE30\uC138\uC694(\uD56D\uBAA9\uB9C8\uB2E4 \uBC18\uBCF5 \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694). \uAC19\uC740 \uD56D\uBAA9\uC744 \uB2E4\uC2DC \uB123\uC5B4\uB3C4 \uC911\uBCF5 \uC5C6\uC774 \uAC31\uC2E0\uB429\uB2C8\uB2E4(\uACBD\uB825 achievements\uB294 \uAC00\uB2A5\uD558\uBA74 \uC815\uB7C9 \uC9C0\uD45C \uD3EC\uD568). \uC774 \uD56D\uBAA9\uB4E4\uC740 \uC628\uBCF4\uB529 \uC644\uC131\uB3C4(has_experience/has_skills)\uC5D0 \uBC18\uC601\uB429\uB2C8\uB2E4.
45842
45975
  - \uD655\uC2E4\uD558\uC9C0 \uC54A\uC740 \uC815\uBCF4\uB294 \uC9C0\uC5B4\uB0B4\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD558\uC138\uC694.
45843
45976
 
45844
45977
  # 3\uB2E8\uACC4 \u2014 \uC774\uB825\uC11C \uB4F1\uB85D
@@ -45956,7 +46089,7 @@ var WORKFLOWS = [
45956
46089
  '\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.)',
45957
46090
  "\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.",
45958
46091
  "\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).",
45959
- "\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).",
46092
+ "\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).",
45960
46093
  "\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).",
45961
46094
  "`open_dashboard`\uB97C \uD638\uCD9C\uD574 \uC0AC\uC6A9\uC790\uAC00 \uC790\uAE30 \uB370\uC774\uD130\uB97C \uD655\uC778\uD558\uAC8C \uD55C\uB2E4.",
45962
46095
  "\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."
@@ -45974,8 +46107,8 @@ var WORKFLOWS = [
45974
46107
  "\uC544\uC9C1 \uC800\uC7A5\uB418\uC9C0 \uC54A\uC740 \uACF5\uACE0\uB77C\uBA74 `save_job_posting`\uC73C\uB85C \uACF5\uACE0\uB97C \uC800\uC7A5\uD558\uACE0 job_id\uB97C \uD655\uBCF4\uD55C\uB2E4.",
45975
46108
  "\uB3C4\uCD9C\uD55C \uBD84\uC11D \uACB0\uACFC\uB97C `save_fit_analysis`\uB85C \uC800\uC7A5\uD55C\uB2E4(job_id, score, summary, strengths, gaps, matched_keywords, missing_keywords, recommendations).",
45976
46109
  "\uBD84\uC11D \uACB0\uACFC\uB97C \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC124\uBA85\uD55C\uB2E4(\uC798 \uB9DE\uB294 \uC810, \uBCF4\uC644\uD560 \uC810, \uCD94\uCC9C \uC804\uB7B5). \uAE30\uC220 \uC6A9\uC5B4\uB294 \uBE7C\uACE0 \uC804\uB2EC\uD55C\uB2E4.",
45977
- '"\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694?"\uB77C\uACE0 \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131\uC744 \uC81C\uC548\uD55C\uB2E4.',
45978
- '\uC0AC\uC6A9\uC790\uAC00 \uB3D9\uC758\uD558\uBA74 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC791\uC131\uD558\uAE30 \uC9C1\uC804\uC5D0 `get_writing_style_guide`\uB97C \uD638\uCD9C\uD574 "AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uADDC\uCE59"\uC744 \uAC00\uC838\uC628\uB2E4.',
46110
+ '\uBD84\uC11D \uB4A4 \uACE7\uC7A5 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC4F0\uC9C0 \uB9D0\uACE0, "\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694?"\uB77C\uACE0 \uBB3B\uACE0 \uC120\uD0DD\uC9C0\uB97C \uC81C\uC2DC\uD55C\uB2E4: \u2460 "\uB124, \uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uC918" \u2461 "\uC544\uB2C8\uC694, \uBD84\uC11D\uB9CC \uC800\uC7A5\uD574\uC918".',
46111
+ '\uC0AC\uC6A9\uC790\uAC00 \u2460\uC744 \uC120\uD0DD\uD558\uBA74 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC791\uC131\uD558\uAE30 \uC9C1\uC804\uC5D0 `get_writing_style_guide`\uB97C \uD638\uCD9C\uD574 "AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uADDC\uCE59"\uC744 \uAC00\uC838\uC628\uB2E4. \u2461\uB97C \uC120\uD0DD\uD558\uBA74 \uC790\uC18C\uC11C \uC791\uC131 \uC5C6\uC774 \uBA48\uCD94\uACE0 \uC800\uC7A5 \uACB0\uACFC\uC640 \uB300\uC2DC\uBCF4\uB4DC \uD655\uC778\uB9CC \uC548\uB0B4\uD55C\uB2E4.',
45979
46112
  "\uC801\uD569\uB3C4 \uBD84\uC11D\xB7\uAE00\uC4F0\uAE30 \uC120\uD638\uC640 \uAE00\uC4F0\uAE30 \uADDC\uCE59\uC744 \uD568\uAED8 \uBC18\uC601\uD574 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC791\uC131\uD558\uACE0(\uBC88\uC5ED\uD22C\xB7\uD074\uB9AC\uC170\xB7\uAE30\uACC4\uC801 \uBCD1\uB82C \uC81C\uAC70, \uC0AC\uC2E4\xB7\uC218\uCE58\uB294 \uADF8\uB300\uB85C), `save_cover_letter_version`\uC73C\uB85C \uC800\uC7A5\uD55C\uB2E4(job_id \uC5F0\uACB0, note\uC5D0 \uC791\uC131 \uC758\uB3C4 \uC694\uC57D).",
45980
46113
  "`open_application`\uC73C\uB85C \uD574\uB2F9 \uC9C0\uC6D0 \uAC74\uC744 \uC5F4\uC5B4 \uC0AC\uC6A9\uC790\uAC00 \uACB0\uACFC\uB97C \uD655\uC778\uD558\uAC8C \uD558\uACE0, \uC9C0\uC6D0 \uC0C1\uD0DC \uBCC0\uACBD(\uC608: \uC9C0\uC6D0 \uC608\uC815/\uC9C0\uC6D0 \uC644\uB8CC)\uC744 \uC81C\uC548\uD55C\uB2E4."
45981
46114
  ]
@@ -46751,20 +46884,286 @@ function getServerInfo() {
46751
46884
 
46752
46885
  // apps/web/src/settings.ts
46753
46886
  import fs4 from "node:fs";
46887
+ import os2 from "node:os";
46754
46888
  import path4 from "node:path";
46889
+ import { spawn } from "node:child_process";
46890
+ import { fileURLToPath } from "node:url";
46891
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
46755
46892
  function listTables(db) {
46756
46893
  return db.prepare(
46757
46894
  `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != '_meta' ORDER BY name`
46758
46895
  ).all().map((r) => r.name);
46759
46896
  }
46897
+ var RESTORE_ORDER = [
46898
+ "profile",
46899
+ "experiences",
46900
+ "projects",
46901
+ "skills",
46902
+ "documents",
46903
+ "jobs",
46904
+ "cover_letters",
46905
+ "cover_letter_versions",
46906
+ "fit_analyses",
46907
+ "applications",
46908
+ "interview_preps",
46909
+ "activities"
46910
+ ];
46911
+ function quoteIdent(name) {
46912
+ return `"${name.replace(/"/g, '""')}"`;
46913
+ }
46914
+ function shQuote(value) {
46915
+ return `'${value.replace(/'/g, `'\\''`)}'`;
46916
+ }
46917
+ function cmdQuote(value) {
46918
+ return value.replace(/"/g, '""');
46919
+ }
46920
+ function schemaVersion(db) {
46921
+ const ver = db.prepare(`SELECT value FROM _meta WHERE key='schema_version'`).get();
46922
+ return Number(ver?.value ?? 0);
46923
+ }
46924
+ function tableColumns(db, table) {
46925
+ return db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all().map((r) => r.name);
46926
+ }
46927
+ function isRecord(v) {
46928
+ return typeof v === "object" && v !== null && !Array.isArray(v);
46929
+ }
46930
+ function appRoot() {
46931
+ return BUNDLED ? __dirname : path4.resolve(__dirname, "..", "..", "..");
46932
+ }
46933
+ function shortcutFolder() {
46934
+ const override = process.env.CAREERMATE_SHORTCUT_DIR?.trim();
46935
+ if (override) return override;
46936
+ const desktop = path4.join(os2.homedir(), "Desktop");
46937
+ const parent = fs4.existsSync(desktop) ? desktop : getExportsDir();
46938
+ return path4.join(parent, "CareerMate \uB300\uC2DC\uBCF4\uB4DC");
46939
+ }
46940
+ function openFolder(dir) {
46941
+ try {
46942
+ if (process.platform === "win32") spawn("explorer", [dir], { stdio: "ignore", detached: true }).unref();
46943
+ else if (process.platform === "darwin") spawn("open", [dir], { stdio: "ignore", detached: true }).unref();
46944
+ else spawn("xdg-open", [dir], { stdio: "ignore", detached: true }).unref();
46945
+ } catch {
46946
+ }
46947
+ }
46948
+ function parseBackup(raw) {
46949
+ if (!isRecord(raw) || !isRecord(raw.tables)) {
46950
+ throw new Error("CareerMate \uBC31\uC5C5 JSON \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4.");
46951
+ }
46952
+ const rawTables = raw.tables;
46953
+ const db = getDb();
46954
+ const currentVersion = schemaVersion(db);
46955
+ const version = Number(raw.version);
46956
+ if (!Number.isInteger(version) || version < 0) {
46957
+ throw new Error("\uBC31\uC5C5 \uD30C\uC77C\uC758 \uC2A4\uD0A4\uB9C8 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
46958
+ }
46959
+ if (version > currentVersion) {
46960
+ throw new Error("\uC774 \uBC31\uC5C5\uC740 \uB354 \uCD5C\uC2E0 CareerMate\uC5D0\uC11C \uB9CC\uB4E4\uC5B4\uC84C\uC2B5\uB2C8\uB2E4. \uC571\uC744 \uC5C5\uB370\uC774\uD2B8\uD55C \uB4A4 \uB2E4\uC2DC \uAC00\uC838\uC640 \uC8FC\uC138\uC694.");
46961
+ }
46962
+ const allowedTables = new Set(listTables(db));
46963
+ const unknownTables = Object.keys(rawTables).filter((name) => !allowedTables.has(name));
46964
+ if (unknownTables.length > 0) {
46965
+ throw new Error(`\uD604\uC7AC \uC571\uC5D0\uC11C \uC54C \uC218 \uC5C6\uB294 \uB370\uC774\uD130\uAC00 \uD3EC\uD568\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: ${unknownTables.join(", ")}`);
46966
+ }
46967
+ const columnsByTable = /* @__PURE__ */ new Map();
46968
+ for (const table of allowedTables) columnsByTable.set(table, new Set(tableColumns(db, table)));
46969
+ const tables = {};
46970
+ const counts = {};
46971
+ let totalRows = 0;
46972
+ for (const table of allowedTables) {
46973
+ const rows = rawTables[table] ?? [];
46974
+ if (!Array.isArray(rows)) throw new Error(`${table} \uB370\uC774\uD130\uAC00 \uBC30\uC5F4 \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4.`);
46975
+ const columns = columnsByTable.get(table);
46976
+ tables[table] = rows.map((row, rowIndex) => {
46977
+ if (!isRecord(row)) throw new Error(`${table} ${rowIndex + 1}\uBC88\uC9F8 \uD589\uC774 \uC62C\uBC14\uB978 \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4.`);
46978
+ const clean = {};
46979
+ for (const [key, value] of Object.entries(row)) {
46980
+ if (!columns.has(key)) throw new Error(`${table} \uB370\uC774\uD130\uC5D0 \uD604\uC7AC \uC571\uC5D0\uC11C \uC4F0\uC9C0 \uC54A\uB294 \uD56D\uBAA9(${key})\uC774 \uC788\uC2B5\uB2C8\uB2E4.`);
46981
+ if (value !== null && !["string", "number", "boolean"].includes(typeof value)) {
46982
+ throw new Error(`${table} \uB370\uC774\uD130\uC758 ${key} \uAC12 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`);
46983
+ }
46984
+ clean[key] = value;
46985
+ }
46986
+ if (Object.keys(clean).length === 0) throw new Error(`${table} ${rowIndex + 1}\uBC88\uC9F8 \uD589\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`);
46987
+ return clean;
46988
+ });
46989
+ counts[table] = tables[table].length;
46990
+ totalRows += tables[table].length;
46991
+ }
46992
+ const warnings = [...allowedTables].filter((table) => !(table in rawTables)).map((table) => `${table} \uB370\uC774\uD130\uAC00 \uBC31\uC5C5 \uD30C\uC77C\uC5D0 \uC5C6\uC5B4 \uBE48 \uAC12\uC73C\uB85C \uAC00\uC838\uC635\uB2C8\uB2E4.`);
46993
+ return {
46994
+ exported_at: typeof raw.exported_at === "string" ? raw.exported_at : null,
46995
+ version,
46996
+ current_version: currentVersion,
46997
+ tables,
46998
+ counts,
46999
+ total_rows: totalRows,
47000
+ warnings
47001
+ };
47002
+ }
47003
+ function macLauncherScript() {
47004
+ const root2 = appRoot();
47005
+ const dataDir = getDataDir();
47006
+ const nodePath = process.execPath;
47007
+ const binPath = path4.join(root2, "bin", "careermate.mjs");
47008
+ const bundledWeb = BUNDLED ? path4.join(root2, "web.mjs") : path4.join(root2, "dist", "web.mjs");
47009
+ return `#!/bin/zsh
47010
+ set -e
47011
+
47012
+ export CAREERMATE_DATA_DIR=${shQuote(dataDir)}
47013
+ NODE=${shQuote(nodePath)}
47014
+ ROOT=${shQuote(root2)}
47015
+ BIN=${shQuote(binPath)}
47016
+ BUNDLED_WEB=${shQuote(bundledWeb)}
47017
+ INFO="$CAREERMATE_DATA_DIR/server.json"
47018
+
47019
+ if [ ! -x "$NODE" ]; then
47020
+ NODE="$(command -v node || true)"
47021
+ fi
47022
+ if [ -z "$NODE" ]; then
47023
+ echo "Node.js\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. CareerMate\uB97C \uC124\uCE58\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694."
47024
+ exit 1
47025
+ fi
47026
+
47027
+ URL=$("$NODE" -e 'const fs=require("fs"); try { const info=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(info.url || ""); } catch {}' "$INFO")
47028
+ if [ -n "$URL" ] && command -v curl >/dev/null 2>&1 && curl -fsS --max-time 1 "$URL/api/health" >/dev/null 2>&1; then
47029
+ open "$URL"
47030
+ exit 0
47031
+ fi
47032
+
47033
+ echo "CareerMate \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4."
47034
+ if [ -f "$BIN" ]; then
47035
+ cd "$ROOT"
47036
+ exec "$NODE" "$BIN" start
47037
+ fi
47038
+
47039
+ if [ -f "$BUNDLED_WEB" ]; then
47040
+ cd "$(dirname "$BUNDLED_WEB")"
47041
+ exec "$NODE" --no-warnings --experimental-sqlite "$BUNDLED_WEB"
47042
+ fi
47043
+
47044
+ if command -v careermate >/dev/null 2>&1; then
47045
+ exec careermate start
47046
+ fi
47047
+
47048
+ exec npx -y careermate start
47049
+ `;
47050
+ }
47051
+ function windowsLauncherScript() {
47052
+ const root2 = appRoot();
47053
+ const dataDir = getDataDir();
47054
+ const nodePath = process.execPath;
47055
+ const binPath = path4.join(root2, "bin", "careermate.mjs");
47056
+ const bundledWeb = BUNDLED ? path4.join(root2, "web.mjs") : path4.join(root2, "dist", "web.mjs");
47057
+ return `@echo off
47058
+ setlocal
47059
+ set "CAREERMATE_DATA_DIR=${cmdQuote(dataDir)}"
47060
+ set "NODE=${cmdQuote(nodePath)}"
47061
+ set "ROOT=${cmdQuote(root2)}"
47062
+ set "BIN=${cmdQuote(binPath)}"
47063
+ set "BUNDLED_WEB=${cmdQuote(bundledWeb)}"
47064
+ set "INFO=%CAREERMATE_DATA_DIR%\\server.json"
47065
+ set "URL="
47066
+
47067
+ if not exist "%NODE%" (
47068
+ for /f "usebackq delims=" %%N in (\`where node 2^>nul\`) do (
47069
+ set "NODE=%%N"
47070
+ goto :node_found
47071
+ )
47072
+ )
47073
+ :node_found
47074
+ if not exist "%NODE%" (
47075
+ echo Node.js\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. CareerMate\uB97C \uC124\uCE58\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.
47076
+ pause
47077
+ exit /b 1
47078
+ )
47079
+
47080
+ for /f "usebackq delims=" %%U in (\`"%NODE%" -e "const fs=require('fs'); try { const info=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.stdout.write(info.url || ''); } catch {}" "%INFO%"\`) do set "URL=%%U"
47081
+ if defined URL (
47082
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 1 '%URL%/api/health' | Out-Null; Start-Process '%URL%'; exit 0 } catch { exit 1 }"
47083
+ if not errorlevel 1 exit /b 0
47084
+ )
47085
+
47086
+ echo CareerMate \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4.
47087
+ if exist "%BIN%" (
47088
+ cd /d "%ROOT%"
47089
+ "%NODE%" "%BIN%" start
47090
+ exit /b %ERRORLEVEL%
47091
+ )
47092
+
47093
+ if exist "%BUNDLED_WEB%" (
47094
+ cd /d "%ROOT%"
47095
+ "%NODE%" --no-warnings --experimental-sqlite "%BUNDLED_WEB%"
47096
+ exit /b %ERRORLEVEL%
47097
+ )
47098
+
47099
+ where careermate >nul 2>nul
47100
+ if not errorlevel 1 (
47101
+ careermate start
47102
+ exit /b %ERRORLEVEL%
47103
+ )
47104
+
47105
+ npx -y careermate start
47106
+ `;
47107
+ }
47108
+ function linuxLauncherScript() {
47109
+ return macLauncherScript().replace("#!/bin/zsh", "#!/usr/bin/env sh").replace('\n open "$URL"', '\n xdg-open "$URL"');
47110
+ }
46760
47111
  function exportAll() {
46761
47112
  const db = getDb();
46762
47113
  const tables = {};
46763
47114
  for (const t of listTables(db)) {
46764
- tables[t] = db.prepare(`SELECT * FROM ${t}`).all();
47115
+ tables[t] = db.prepare(`SELECT * FROM ${quoteIdent(t)}`).all();
46765
47116
  }
46766
- const ver = db.prepare(`SELECT value FROM _meta WHERE key='schema_version'`).get();
46767
- return { exported_at: (/* @__PURE__ */ new Date()).toISOString(), version: Number(ver?.value ?? 0), tables };
47117
+ return { exported_at: (/* @__PURE__ */ new Date()).toISOString(), version: schemaVersion(db), tables };
47118
+ }
47119
+ function previewBackupImport(raw) {
47120
+ const parsed = parseBackup(raw);
47121
+ return {
47122
+ exported_at: parsed.exported_at,
47123
+ version: parsed.version,
47124
+ current_version: parsed.current_version,
47125
+ counts: parsed.counts,
47126
+ total_rows: parsed.total_rows,
47127
+ warnings: parsed.warnings
47128
+ };
47129
+ }
47130
+ function restoreBackup(raw) {
47131
+ const parsed = parseBackup(raw);
47132
+ const safety = createBackup();
47133
+ const db = getDb();
47134
+ const tables = listTables(db);
47135
+ const restoreOrder = [
47136
+ ...RESTORE_ORDER.filter((table) => tables.includes(table)),
47137
+ ...tables.filter((table) => !RESTORE_ORDER.includes(table))
47138
+ ];
47139
+ tx(() => {
47140
+ for (const table of [...restoreOrder].reverse()) {
47141
+ db.prepare(`DELETE FROM ${quoteIdent(table)}`).run();
47142
+ }
47143
+ for (const table of restoreOrder) {
47144
+ for (const row of parsed.tables[table] ?? []) {
47145
+ const columns = Object.keys(row);
47146
+ const placeholders = columns.map(() => "?").join(",");
47147
+ const values2 = columns.map((column) => {
47148
+ const value = row[column];
47149
+ return typeof value === "boolean" ? Number(value) : value;
47150
+ });
47151
+ db.prepare(
47152
+ `INSERT INTO ${quoteIdent(table)} (${columns.map(quoteIdent).join(",")}) VALUES (${placeholders})`
47153
+ ).run(...values2);
47154
+ }
47155
+ }
47156
+ const fkProblems = db.prepare(`PRAGMA foreign_key_check`).all();
47157
+ if (fkProblems.length > 0) {
47158
+ throw new Error("\uBC31\uC5C5 \uD30C\uC77C\uC758 \uB370\uC774\uD130 \uAD00\uACC4\uAC00 \uB9DE\uC9C0 \uC54A\uC544 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
47159
+ }
47160
+ });
47161
+ return {
47162
+ ok: true,
47163
+ safety_backup_path: safety.backup_path,
47164
+ safety_json_path: safety.json_path,
47165
+ restored: previewBackupImport(raw)
47166
+ };
46768
47167
  }
46769
47168
  function createBackup() {
46770
47169
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -46785,6 +47184,38 @@ function createBackup() {
46785
47184
  fs4.writeFileSync(jsonBackup, JSON.stringify(exportAll(), null, 2), "utf8");
46786
47185
  return { backup_path: dbBackup, json_path: jsonBackup };
46787
47186
  }
47187
+ function createDashboardShortcut(open = false) {
47188
+ const dir = shortcutFolder();
47189
+ fs4.mkdirSync(dir, { recursive: true });
47190
+ const readme = `CareerMate \uB300\uC2DC\uBCF4\uB4DC \uBC14\uB85C\uAC00\uAE30
47191
+
47192
+ - \uB300\uC2DC\uBCF4\uB4DC\uAC00 \uC774\uBBF8 \uC2E4\uD589 \uC911\uC774\uBA74 \uBE0C\uB77C\uC6B0\uC800\uB9CC \uC5FD\uB2C8\uB2E4.
47193
+ - \uB300\uC2DC\uBCF4\uB4DC\uAC00 \uAEBC\uC838 \uC788\uC73C\uBA74 \uC11C\uBC84\uB97C \uC2DC\uC791\uD558\uACE0 \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5FD\uB2C8\uB2E4.
47194
+ - \uC774 \uD3F4\uB354\uB294 \uC774 \uCEF4\uD4E8\uD130\uC758 CareerMate \uC124\uCE58 \uACBD\uB85C\uC640 \uB370\uC774\uD130 \uD3F4\uB354\uB97C \uAE30\uC900\uC73C\uB85C \uB9CC\uB4E4\uC5B4\uC84C\uC2B5\uB2C8\uB2E4.
47195
+ `;
47196
+ const readmePath = path4.join(dir, "README.txt");
47197
+ fs4.writeFileSync(readmePath, readme, "utf8");
47198
+ let launcherPath;
47199
+ if (process.platform === "win32") {
47200
+ launcherPath = path4.join(dir, "CareerMate \uB300\uC2DC\uBCF4\uB4DC.cmd");
47201
+ fs4.writeFileSync(launcherPath, windowsLauncherScript(), "utf8");
47202
+ } else if (process.platform === "darwin") {
47203
+ launcherPath = path4.join(dir, "CareerMate \uB300\uC2DC\uBCF4\uB4DC.command");
47204
+ fs4.writeFileSync(launcherPath, macLauncherScript(), { encoding: "utf8", mode: 493 });
47205
+ fs4.chmodSync(launcherPath, 493);
47206
+ } else {
47207
+ launcherPath = path4.join(dir, "careermate-dashboard.sh");
47208
+ fs4.writeFileSync(launcherPath, linuxLauncherScript(), { encoding: "utf8", mode: 493 });
47209
+ fs4.chmodSync(launcherPath, 493);
47210
+ }
47211
+ if (open) openFolder(dir);
47212
+ return {
47213
+ shortcut_dir: dir,
47214
+ launcher_path: launcherPath,
47215
+ platform: process.platform,
47216
+ files: [launcherPath, readmePath]
47217
+ };
47218
+ }
46788
47219
  function resetAll(confirm) {
46789
47220
  if (confirm !== "DELETE") {
46790
47221
  return { ok: false };
@@ -46793,7 +47224,7 @@ function resetAll(confirm) {
46793
47224
  const db = getDb();
46794
47225
  db.exec("BEGIN");
46795
47226
  try {
46796
- for (const t of listTables(db)) db.exec(`DELETE FROM ${t}`);
47227
+ for (const t of listTables(db)) db.exec(`DELETE FROM ${quoteIdent(t)}`);
46797
47228
  db.exec("COMMIT");
46798
47229
  } catch (e) {
46799
47230
  db.exec("ROLLBACK");
@@ -46821,6 +47252,9 @@ function fmt(ctx) {
46821
47252
  const f = (ctx.query.get("format") ?? "md").toLowerCase();
46822
47253
  return ["md", "html", "txt"].includes(f) ? f : "md";
46823
47254
  }
47255
+ function backupError(e) {
47256
+ return new HttpError(400, e instanceof Error ? e.message : "\uBC31\uC5C5 \uD30C\uC77C\uC744 \uCC98\uB9AC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", "backup_import");
47257
+ }
46824
47258
  function jobDetail(jobId) {
46825
47259
  const job = jobRepo.get(jobId);
46826
47260
  if (!job) throw new HttpError(404, "\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
@@ -47037,6 +47471,32 @@ function registerApiRoutes(router2) {
47037
47471
  return { verify_strict: on };
47038
47472
  });
47039
47473
  router2.post("/api/settings/backup", () => createBackup());
47474
+ router2.post("/api/settings/dashboard-shortcut", async (ctx) => {
47475
+ const { open } = await readJsonBody(ctx.req, external_exports.object({ open: external_exports.boolean().optional() }));
47476
+ return { shortcut: createDashboardShortcut(open === true) };
47477
+ });
47478
+ router2.post("/api/settings/import-preview", async (ctx) => {
47479
+ const { backup } = await readJsonBody(ctx.req, external_exports.object({ backup: external_exports.unknown() }));
47480
+ try {
47481
+ return { preview: previewBackupImport(backup) };
47482
+ } catch (e) {
47483
+ throw backupError(e);
47484
+ }
47485
+ });
47486
+ router2.post("/api/settings/restore", async (ctx) => {
47487
+ const { backup, confirm } = await readJsonBody(
47488
+ ctx.req,
47489
+ external_exports.object({ backup: external_exports.unknown(), confirm: external_exports.string() })
47490
+ );
47491
+ if (confirm !== "RESTORE") {
47492
+ throw new HttpError(400, "\uBCF5\uC6D0\uC744 \uC9C4\uD589\uD558\uB824\uBA74 \uD655\uC778 \uAC12\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", "restore_confirm_required");
47493
+ }
47494
+ try {
47495
+ return restoreBackup(backup);
47496
+ } catch (e) {
47497
+ throw backupError(e);
47498
+ }
47499
+ });
47040
47500
  router2.post("/api/settings/reset", async (ctx) => {
47041
47501
  const { confirm } = await readJsonBody(ctx.req, external_exports.object({ confirm: external_exports.string() }));
47042
47502
  const result2 = resetAll(confirm);
@@ -47064,9 +47524,9 @@ function isDownload(v) {
47064
47524
  }
47065
47525
 
47066
47526
  // apps/web/src/server.ts
47067
- var __dirname = path5.dirname(fileURLToPath(import.meta.url));
47068
- var WEB_ROOT = BUNDLED ? path5.resolve(__dirname, "public") : path5.resolve(__dirname, "..", "public");
47069
- var INSTALL_ROOT = BUNDLED ? path5.resolve(__dirname, "site") : path5.resolve(__dirname, "..", "..", "..", "site");
47527
+ var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
47528
+ var WEB_ROOT = BUNDLED ? path5.resolve(__dirname2, "public") : path5.resolve(__dirname2, "..", "public");
47529
+ var INSTALL_ROOT = BUNDLED ? path5.resolve(__dirname2, "site") : path5.resolve(__dirname2, "..", "..", "..", "site");
47070
47530
  var router = new Router();
47071
47531
  registerApiRoutes(router);
47072
47532
  function createServer() {
@@ -47118,6 +47578,8 @@ function createServer() {
47118
47578
  } catch (err) {
47119
47579
  if (err instanceof HttpError) {
47120
47580
  sendJson(res, err.status, { error: err.message, code: err.code });
47581
+ } else if (err instanceof CareerMateError) {
47582
+ sendJson(res, err.httpStatus, { error: err.message, code: err.code });
47121
47583
  } else {
47122
47584
  const message = err instanceof Error ? err.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
47123
47585
  console.error("[careermate] \uC694\uCCAD \uCC98\uB9AC \uC911 \uC624\uB958:", message);
@@ -47166,11 +47628,11 @@ function openBrowser(url) {
47166
47628
  const platform = process.platform;
47167
47629
  try {
47168
47630
  if (platform === "win32") {
47169
- spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
47631
+ spawn2("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
47170
47632
  } else if (platform === "darwin") {
47171
- spawn("open", [url], { stdio: "ignore", detached: true }).unref();
47633
+ spawn2("open", [url], { stdio: "ignore", detached: true }).unref();
47172
47634
  } else {
47173
- spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
47635
+ spawn2("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
47174
47636
  }
47175
47637
  } catch {
47176
47638
  }