careermate 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +22 -17
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/apps/mcp/src/index.ts +4 -3
  4. package/apps/web/DESIGN_GUIDE.md +2 -2
  5. package/apps/web/demo/demo-shim.js +536 -0
  6. package/apps/web/demo/demo.css +75 -0
  7. package/apps/web/demo/seed.js +269 -0
  8. package/apps/web/public/app.js +34 -2
  9. package/apps/web/public/icon-180.png +0 -0
  10. package/apps/web/public/index.html +14 -3
  11. package/apps/web/public/lib.js +135 -23
  12. package/apps/web/public/pages/applications.js +18 -9
  13. package/apps/web/public/pages/documents.js +66 -50
  14. package/apps/web/public/pages/home.js +12 -11
  15. package/apps/web/public/pages/interview.js +9 -12
  16. package/apps/web/public/pages/jobs.js +39 -36
  17. package/apps/web/public/pages/profile.js +66 -86
  18. package/apps/web/public/pages/settings.js +21 -14
  19. package/apps/web/public/styles.css +105 -38
  20. package/apps/web/src/http.ts +31 -4
  21. package/apps/web/src/info.ts +9 -22
  22. package/apps/web/src/routes.ts +19 -14
  23. package/apps/web/src/server.ts +34 -29
  24. package/apps/web/src/settings.ts +35 -17
  25. package/bin/careermate.mjs +8 -5
  26. package/dist/doctor.mjs +78908 -0
  27. package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +12 -0
  28. package/dist/install-page/.omc/state/idle-notif-cooldown.json +3 -0
  29. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +147 -0
  30. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +22 -0
  31. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +44 -0
  32. package/dist/install-page/favicon.svg +1 -0
  33. package/dist/install-page/icon-180.png +0 -0
  34. package/dist/install-page/icon-512.png +0 -0
  35. package/dist/install-page/index.html +328 -225
  36. package/dist/install-page/landing.js +54 -0
  37. package/dist/install-page/llms-install.txt +202 -0
  38. package/dist/install-page/og.png +0 -0
  39. package/dist/install-page/shots/applications-dark.png +0 -0
  40. package/dist/install-page/shots/applications-light.png +0 -0
  41. package/dist/install-page/shots/documents-dark.png +0 -0
  42. package/dist/install-page/shots/documents-light.png +0 -0
  43. package/dist/install-page/shots/home-dark.png +0 -0
  44. package/dist/install-page/shots/home-light.png +0 -0
  45. package/dist/install-page/shots/interview-dark.png +0 -0
  46. package/dist/install-page/shots/interview-light.png +0 -0
  47. package/dist/install-page/shots/jobs-dark.png +0 -0
  48. package/dist/install-page/shots/jobs-light.png +0 -0
  49. package/dist/install-page/shots/settings-dark.png +0 -0
  50. package/dist/install-page/shots/settings-light.png +0 -0
  51. package/dist/install-page/start.html +323 -0
  52. package/dist/install-page/style.css +128 -4
  53. package/dist/mcp.mjs +74225 -2007
  54. package/dist/{mcpb-stage/dist/web.mjs → migrate.mjs} +142 -2660
  55. package/dist/pdf.worker.min.mjs +28 -0
  56. package/dist/public/app.js +34 -2
  57. package/dist/public/icon-180.png +0 -0
  58. package/dist/public/index.html +14 -3
  59. package/dist/public/lib.js +135 -23
  60. package/dist/public/pages/applications.js +18 -9
  61. package/dist/public/pages/documents.js +66 -50
  62. package/dist/public/pages/home.js +12 -11
  63. package/dist/public/pages/interview.js +9 -12
  64. package/dist/public/pages/jobs.js +39 -36
  65. package/dist/public/pages/profile.js +66 -86
  66. package/dist/public/pages/settings.js +21 -14
  67. package/dist/public/styles.css +105 -38
  68. package/dist/web.mjs +39950 -369
  69. package/docs/ARCHITECTURE.md +5 -5
  70. package/docs/DECISIONS.md +1 -1
  71. package/docs/DEMO.md +6 -6
  72. package/docs/MCP_TOOLS.md +68 -9
  73. package/docs/ROADMAP.md +11 -10
  74. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
  75. package/docs/START_WORKFLOW.md +19 -7
  76. package/docs/SUPPORTED_AI_APPS.md +85 -33
  77. package/docs/TODO.md +1 -1
  78. package/docs/WORKFLOWS.md +8 -6
  79. package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +64 -0
  80. package/package.json +8 -3
  81. package/packages/core/src/services.ts +42 -1
  82. package/packages/db/src/connection.ts +30 -0
  83. package/packages/db/src/index.ts +1 -0
  84. package/packages/db/src/paths.ts +11 -0
  85. package/packages/db/src/repositories.ts +109 -63
  86. package/packages/db/src/schema.ts +118 -0
  87. package/packages/exporters/src/html.ts +9 -12
  88. package/packages/mcp-tools/src/bridge.ts +55 -32
  89. package/packages/mcp-tools/src/index.ts +9 -0
  90. package/packages/mcp-tools/src/tools.ts +269 -65
  91. package/packages/mcp-tools/src/update.ts +330 -0
  92. package/packages/parsers/src/documents.ts +521 -0
  93. package/packages/parsers/src/index.ts +8 -5
  94. package/packages/prompts/src/onboarding.ts +2 -2
  95. package/packages/shared/src/build-info.ts +22 -0
  96. package/packages/shared/src/enums.ts +39 -0
  97. package/packages/shared/src/index.ts +1 -0
  98. package/packages/shared/src/schemas.ts +24 -3
  99. package/packages/workflows/src/definitions.ts +4 -2
  100. package/scripts/build-demo.mjs +130 -0
  101. package/scripts/build-dist.mjs +46 -8
  102. package/scripts/build-mcpb.mjs +60 -4
  103. package/scripts/doctor.ts +16 -3
  104. package/scripts/gen-og.mjs +86 -0
  105. package/scripts/init.ts +32 -3
  106. package/scripts/seed.ts +5 -5
  107. package/scripts/shots-seed.ts +192 -0
  108. package/scripts/shots.ts +89 -0
  109. package/tsconfig.json +3 -1
  110. package/dist/careermate.mcpb +0 -0
  111. package/dist/mcp-smoke.err +0 -3
  112. package/dist/mcpb-stage/README.md +0 -219
  113. package/dist/mcpb-stage/dist/install-page/index.html +0 -434
  114. package/dist/mcpb-stage/dist/install-page/style.css +0 -407
  115. package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
  116. package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
  117. package/dist/mcpb-stage/dist/public/app.js +0 -118
  118. package/dist/mcpb-stage/dist/public/index.html +0 -41
  119. package/dist/mcpb-stage/dist/public/lib.js +0 -282
  120. package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
  121. package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
  122. package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
  123. package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
  124. package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
  125. package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
  126. package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
  127. package/dist/mcpb-stage/dist/public/styles.css +0 -420
  128. package/dist/mcpb-stage/manifest.json +0 -40
  129. package/docs/INSTALL.md +0 -148
  130. package/docs/INSTALL_AND_USAGE.md +0 -99
  131. package/install-page/index.html +0 -474
  132. package/install-page/style.css +0 -391
  133. package/install-page/vercel.json +0 -20
  134. /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
@@ -5,14 +5,6 @@ var __export = (target, all) => {
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
- // apps/web/src/index.ts
9
- import { spawn } from "node:child_process";
10
-
11
- // apps/web/src/server.ts
12
- import http from "node:http";
13
- import path5 from "node:path";
14
- import { fileURLToPath } from "node:url";
15
-
16
8
  // packages/db/src/paths.ts
17
9
  import os from "node:os";
18
10
  import path from "node:path";
@@ -26,35 +18,6 @@ function getDataDir() {
26
18
  function getDbPath() {
27
19
  return path.join(getDataDir(), "careermate.sqlite");
28
20
  }
29
- function getExportsDir() {
30
- const dir = path.join(getDataDir(), "exports");
31
- fs.mkdirSync(dir, { recursive: true });
32
- return dir;
33
- }
34
- function getBackupsDir() {
35
- const dir = path.join(getDataDir(), "backups");
36
- fs.mkdirSync(dir, { recursive: true });
37
- return dir;
38
- }
39
-
40
- // packages/db/src/runtime.ts
41
- import fs2 from "node:fs";
42
- import path2 from "node:path";
43
- function runtimeFile() {
44
- return path2.join(getDataDir(), "server.json");
45
- }
46
- function writeRuntimeInfo(info) {
47
- try {
48
- fs2.writeFileSync(runtimeFile(), JSON.stringify(info, null, 2), "utf8");
49
- } catch {
50
- }
51
- }
52
- function clearRuntimeInfo() {
53
- try {
54
- fs2.unlinkSync(runtimeFile());
55
- } catch {
56
- }
57
- }
58
21
 
59
22
  // packages/db/src/connection.ts
60
23
  import { DatabaseSync } from "node:sqlite";
@@ -216,6 +179,123 @@ var MIGRATIONS = [
216
179
  CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
217
180
  CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
218
181
  CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
182
+ `,
183
+ // v2 — add foreign keys so PRAGMA foreign_keys=ON is actually enforced.
184
+ //
185
+ // SQLite cannot ALTER a table to add constraints, so each table that needs an
186
+ // FK is recreated (create new → copy → drop old → rename). The copy also
187
+ // repairs pre-existing orphans: child rows whose parent is gone are dropped
188
+ // (CASCADE intent) and dangling optional refs are nulled (SET NULL intent), so
189
+ // a subsequent PRAGMA foreign_key_check stays clean. FKs are disabled for the
190
+ // rebuild and the whole thing runs in one transaction (auto-rolled-back if the
191
+ // process dies mid-migration, since the version bump happens only afterwards).
192
+ `
193
+ PRAGMA foreign_keys=OFF;
194
+ BEGIN;
195
+
196
+ -- cover_letters.job_id -> jobs(id) ON DELETE SET NULL
197
+ CREATE TABLE cover_letters_new (
198
+ id TEXT PRIMARY KEY,
199
+ title TEXT NOT NULL,
200
+ job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
201
+ is_primary INTEGER NOT NULL DEFAULT 0,
202
+ current_version_id TEXT,
203
+ created_at TEXT NOT NULL,
204
+ updated_at TEXT NOT NULL
205
+ );
206
+ INSERT INTO cover_letters_new (id,title,job_id,is_primary,current_version_id,created_at,updated_at)
207
+ SELECT id,title,
208
+ CASE WHEN job_id IN (SELECT id FROM jobs) THEN job_id ELSE NULL END,
209
+ is_primary,current_version_id,created_at,updated_at
210
+ FROM cover_letters;
211
+ DROP TABLE cover_letters;
212
+ ALTER TABLE cover_letters_new RENAME TO cover_letters;
213
+
214
+ -- cover_letter_versions.cover_letter_id -> cover_letters(id) ON DELETE CASCADE
215
+ CREATE TABLE cover_letter_versions_new (
216
+ id TEXT PRIMARY KEY,
217
+ cover_letter_id TEXT NOT NULL REFERENCES cover_letters(id) ON DELETE CASCADE,
218
+ version_no INTEGER NOT NULL,
219
+ content TEXT NOT NULL,
220
+ note TEXT,
221
+ source TEXT NOT NULL DEFAULT 'ai',
222
+ created_at TEXT NOT NULL
223
+ );
224
+ INSERT INTO cover_letter_versions_new (id,cover_letter_id,version_no,content,note,source,created_at)
225
+ SELECT id,cover_letter_id,version_no,content,note,source,created_at
226
+ FROM cover_letter_versions
227
+ WHERE cover_letter_id IN (SELECT id FROM cover_letters);
228
+ DROP TABLE cover_letter_versions;
229
+ ALTER TABLE cover_letter_versions_new RENAME TO cover_letter_versions;
230
+
231
+ -- fit_analyses.job_id -> jobs(id) ON DELETE CASCADE
232
+ CREATE TABLE fit_analyses_new (
233
+ id TEXT PRIMARY KEY,
234
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
235
+ score REAL,
236
+ summary TEXT,
237
+ strengths TEXT NOT NULL DEFAULT '[]',
238
+ gaps TEXT NOT NULL DEFAULT '[]',
239
+ matched_keywords TEXT NOT NULL DEFAULT '[]',
240
+ missing_keywords TEXT NOT NULL DEFAULT '[]',
241
+ recommendations TEXT NOT NULL DEFAULT '[]',
242
+ created_at TEXT NOT NULL,
243
+ updated_at TEXT NOT NULL
244
+ );
245
+ INSERT INTO fit_analyses_new (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
246
+ SELECT id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at
247
+ FROM fit_analyses
248
+ WHERE job_id IN (SELECT id FROM jobs);
249
+ DROP TABLE fit_analyses;
250
+ ALTER TABLE fit_analyses_new RENAME TO fit_analyses;
251
+
252
+ -- applications: job_id CASCADE, resume_id/cover_letter_id SET NULL
253
+ CREATE TABLE applications_new (
254
+ id TEXT PRIMARY KEY,
255
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
256
+ status TEXT NOT NULL DEFAULT 'draft',
257
+ resume_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
258
+ cover_letter_id TEXT REFERENCES cover_letters(id) ON DELETE SET NULL,
259
+ applied_at TEXT,
260
+ notes TEXT,
261
+ created_at TEXT NOT NULL,
262
+ updated_at TEXT NOT NULL
263
+ );
264
+ INSERT INTO applications_new (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at)
265
+ SELECT id,job_id,status,
266
+ CASE WHEN resume_id IN (SELECT id FROM documents) THEN resume_id ELSE NULL END,
267
+ CASE WHEN cover_letter_id IN (SELECT id FROM cover_letters) THEN cover_letter_id ELSE NULL END,
268
+ applied_at,notes,created_at,updated_at
269
+ FROM applications
270
+ WHERE job_id IN (SELECT id FROM jobs);
271
+ DROP TABLE applications;
272
+ ALTER TABLE applications_new RENAME TO applications;
273
+
274
+ -- interview_preps.job_id -> jobs(id) ON DELETE CASCADE
275
+ CREATE TABLE interview_preps_new (
276
+ id TEXT PRIMARY KEY,
277
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
278
+ questions TEXT NOT NULL DEFAULT '[]',
279
+ star_guides TEXT NOT NULL DEFAULT '[]',
280
+ self_introduction TEXT,
281
+ notes TEXT,
282
+ created_at TEXT NOT NULL,
283
+ updated_at TEXT NOT NULL
284
+ );
285
+ INSERT INTO interview_preps_new (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at)
286
+ SELECT id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at
287
+ FROM interview_preps
288
+ WHERE job_id IN (SELECT id FROM jobs);
289
+ DROP TABLE interview_preps;
290
+ ALTER TABLE interview_preps_new RENAME TO interview_preps;
291
+
292
+ -- Recreate indexes dropped along with their tables.
293
+ CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
294
+ CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
295
+ CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
296
+
297
+ COMMIT;
298
+ PRAGMA foreign_keys=ON;
219
299
  `
220
300
  ];
221
301
  function migrate(db) {
@@ -247,19 +327,6 @@ function getDb() {
247
327
  _db = db;
248
328
  return db;
249
329
  }
250
- function toJson(value) {
251
- return JSON.stringify(value ?? null);
252
- }
253
- function fromJson(text, fallback) {
254
- if (text == null || text === "") return fallback;
255
- try {
256
- return JSON.parse(text);
257
- } catch {
258
- return fallback;
259
- }
260
- }
261
- var toBit = (b) => b ? 1 : 0;
262
- var fromBit = (n) => Number(n) === 1;
263
330
 
264
331
  // packages/shared/src/enums.ts
265
332
  var APPLICATION_STATUSES = [
@@ -280,38 +347,7 @@ var APPLICATION_STATUSES = [
280
347
  "on_hold"
281
348
  // 보류
282
349
  ];
283
- var APPLICATION_STATUS_LABELS = {
284
- draft: "\uC791\uC131 \uC911",
285
- planned: "\uC9C0\uC6D0 \uC608\uC815",
286
- applied: "\uC9C0\uC6D0 \uC644\uB8CC",
287
- document_passed: "\uC11C\uB958 \uD569\uACA9",
288
- interview: "\uBA74\uC811 \uC9C4\uD589",
289
- final_passed: "\uCD5C\uC885 \uD569\uACA9",
290
- rejected: "\uBD88\uD569\uACA9",
291
- on_hold: "\uBCF4\uB958"
292
- };
293
- var APPLICATION_BOARD_ORDER = [
294
- "draft",
295
- "planned",
296
- "applied",
297
- "document_passed",
298
- "interview",
299
- "final_passed",
300
- "on_hold",
301
- "rejected"
302
- ];
303
- var INTERVIEW_UNLOCK_STATUSES = [
304
- "document_passed",
305
- "interview",
306
- "final_passed"
307
- ];
308
350
  var DOCUMENT_KINDS = ["resume", "career_description", "portfolio", "other"];
309
- var DOCUMENT_KIND_LABELS = {
310
- resume: "\uC774\uB825\uC11C",
311
- career_description: "\uACBD\uB825\uAE30\uC220\uC11C",
312
- portfolio: "\uD3EC\uD2B8\uD3F4\uB9AC\uC624",
313
- other: "\uAE30\uD0C0 \uBB38\uC11C"
314
- };
315
351
  var CONTENT_SOURCES = ["manual", "upload", "ai", "edit"];
316
352
  var ACTIVITY_TYPES = [
317
353
  "profile_updated",
@@ -815,8 +851,8 @@ function getErrorMap() {
815
851
 
816
852
  // node_modules/zod/v3/helpers/parseUtil.js
817
853
  var makeIssue = (params) => {
818
- const { data, path: path6, errorMaps, issueData } = params;
819
- const fullPath = [...path6, ...issueData.path || []];
854
+ const { data, path: path2, errorMaps, issueData } = params;
855
+ const fullPath = [...path2, ...issueData.path || []];
820
856
  const fullIssue = {
821
857
  ...issueData,
822
858
  path: fullPath
@@ -932,11 +968,11 @@ var errorUtil;
932
968
 
933
969
  // node_modules/zod/v3/types.js
934
970
  var ParseInputLazyPath = class {
935
- constructor(parent, value, path6, key) {
971
+ constructor(parent, value, path2, key) {
936
972
  this._cachedPath = [];
937
973
  this.parent = parent;
938
974
  this.data = value;
939
- this._path = path6;
975
+ this._path = path2;
940
976
  this._key = key;
941
977
  }
942
978
  get path() {
@@ -4165,7 +4201,7 @@ ZodNaN.create = (params) => {
4165
4201
  ...processCreateParams(params)
4166
4202
  });
4167
4203
  };
4168
- var BRAND = Symbol("zod_brand");
4204
+ var BRAND = /* @__PURE__ */ Symbol("zod_brand");
4169
4205
  var ZodBranded = class extends ZodType {
4170
4206
  _parse(input) {
4171
4207
  const { ctx } = this._processInputParams(input);
@@ -4367,20 +4403,27 @@ var ostring = () => stringType().optional();
4367
4403
  var onumber = () => numberType().optional();
4368
4404
  var oboolean = () => booleanType().optional();
4369
4405
  var coerce = {
4370
- string: (arg) => ZodString.create({ ...arg, coerce: true }),
4371
- number: (arg) => ZodNumber.create({ ...arg, coerce: true }),
4372
- boolean: (arg) => ZodBoolean.create({
4406
+ string: ((arg) => ZodString.create({ ...arg, coerce: true })),
4407
+ number: ((arg) => ZodNumber.create({ ...arg, coerce: true })),
4408
+ boolean: ((arg) => ZodBoolean.create({
4373
4409
  ...arg,
4374
4410
  coerce: true
4375
- }),
4376
- bigint: (arg) => ZodBigInt.create({ ...arg, coerce: true }),
4377
- date: (arg) => ZodDate.create({ ...arg, coerce: true })
4411
+ })),
4412
+ bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })),
4413
+ date: ((arg) => ZodDate.create({ ...arg, coerce: true }))
4378
4414
  };
4379
4415
  var NEVER = INVALID;
4380
4416
 
4381
4417
  // packages/shared/src/schemas.ts
4382
4418
  var isoDateTime = external_exports.string();
4383
4419
  var id = external_exports.string();
4420
+ function isSafeUrl(u) {
4421
+ const s = u.trim();
4422
+ return !/^[a-z][a-z0-9+.-]*:/i.test(s) || /^(https?:|mailto:)/i.test(s);
4423
+ }
4424
+ var UNSAFE_URL_MSG = "javascript: \uAC19\uC740 \uC704\uD5D8\uD55C \uB9C1\uD06C\uB294 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. http(s) \uB9C1\uD06C\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.";
4425
+ var SafeUrl = external_exports.string().min(1).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
4426
+ var SafeUrlOptional = external_exports.string().optional().refine((u) => u == null || u === "" || isSafeUrl(u), { message: UNSAFE_URL_MSG });
4384
4427
  var baseRecord = {
4385
4428
  id,
4386
4429
  created_at: isoDateTime,
@@ -4388,7 +4431,7 @@ var baseRecord = {
4388
4431
  };
4389
4432
  var LinkSchema = external_exports.object({
4390
4433
  label: external_exports.string().min(1),
4391
- url: external_exports.string().min(1)
4434
+ url: SafeUrl
4392
4435
  });
4393
4436
  var ProfileInputSchema = external_exports.object({
4394
4437
  name: external_exports.string().min(1).optional(),
@@ -4448,7 +4491,7 @@ var ProjectInputSchema = external_exports.object({
4448
4491
  description: external_exports.string().optional(),
4449
4492
  highlights: external_exports.array(external_exports.string()).optional(),
4450
4493
  tech: external_exports.array(external_exports.string()).optional(),
4451
- url: external_exports.string().optional(),
4494
+ url: SafeUrlOptional,
4452
4495
  start_date: external_exports.string().optional(),
4453
4496
  end_date: external_exports.string().optional(),
4454
4497
  order_index: external_exports.number().optional()
@@ -4536,7 +4579,7 @@ var CoverLetterRecordSchema = external_exports.object({
4536
4579
  var JobInputSchema = external_exports.object({
4537
4580
  company: external_exports.string().min(1),
4538
4581
  position: external_exports.string().min(1).describe("\uC9C1\uBB34/\uD3EC\uC9C0\uC158\uBA85"),
4539
- url: external_exports.string().optional(),
4582
+ url: SafeUrlOptional,
4540
4583
  location: external_exports.string().optional(),
4541
4584
  employment_type: external_exports.string().optional(),
4542
4585
  description: external_exports.string().optional().describe("\uACF5\uACE0 \uC6D0\uBB38 \uB610\uB294 \uC815\uB9AC\uB41C \uD14D\uC2A4\uD2B8"),
@@ -4673,2568 +4716,7 @@ var ApplicationContextSchema = external_exports.object({
4673
4716
  })
4674
4717
  });
4675
4718
 
4676
- // packages/shared/src/index.ts
4677
- function newId(prefix = "") {
4678
- const rnd = Math.random().toString(36).slice(2, 10);
4679
- const time = Date.now().toString(36);
4680
- return `${prefix}${time}${rnd}`;
4681
- }
4682
- function now() {
4683
- return (/* @__PURE__ */ new Date()).toISOString();
4684
- }
4685
-
4686
- // packages/db/src/repositories.ts
4687
- var PROFILE_ID = "profile-singleton";
4688
- function mapProfile(r) {
4689
- return {
4690
- id: r.id,
4691
- name: r.name,
4692
- email: r.email,
4693
- phone: r.phone,
4694
- location: r.location,
4695
- headline: r.headline,
4696
- summary: r.summary,
4697
- desired_roles: fromJson(r.desired_roles, []),
4698
- desired_conditions: r.desired_conditions,
4699
- preferred_tone: r.preferred_tone,
4700
- emphasis_points: fromJson(r.emphasis_points, []),
4701
- links: fromJson(r.links, []),
4702
- created_at: r.created_at,
4703
- updated_at: r.updated_at
4704
- };
4705
- }
4706
- var profileRepo = {
4707
- get() {
4708
- const r = getDb().prepare(`SELECT * FROM profile WHERE id = ?`).get(PROFILE_ID);
4709
- return r ? mapProfile(r) : null;
4710
- },
4711
- /** Upsert + merge: only provided fields overwrite; arrays replace when given. */
4712
- save(input) {
4713
- const db = getDb();
4714
- const existing = this.get();
4715
- const ts = now();
4716
- const merged = {
4717
- id: PROFILE_ID,
4718
- name: input.name ?? existing?.name ?? null,
4719
- email: input.email ?? existing?.email ?? null,
4720
- phone: input.phone ?? existing?.phone ?? null,
4721
- location: input.location ?? existing?.location ?? null,
4722
- headline: input.headline ?? existing?.headline ?? null,
4723
- summary: input.summary ?? existing?.summary ?? null,
4724
- desired_roles: input.desired_roles ?? existing?.desired_roles ?? [],
4725
- desired_conditions: input.desired_conditions ?? existing?.desired_conditions ?? null,
4726
- preferred_tone: input.preferred_tone ?? existing?.preferred_tone ?? null,
4727
- emphasis_points: input.emphasis_points ?? existing?.emphasis_points ?? [],
4728
- links: input.links ?? existing?.links ?? [],
4729
- created_at: existing?.created_at ?? ts,
4730
- updated_at: ts
4731
- };
4732
- db.prepare(
4733
- `INSERT INTO profile (id,name,email,phone,location,headline,summary,desired_roles,desired_conditions,preferred_tone,emphasis_points,links,created_at,updated_at)
4734
- VALUES (@id,@name,@email,@phone,@location,@headline,@summary,@desired_roles,@desired_conditions,@preferred_tone,@emphasis_points,@links,@created_at,@updated_at)
4735
- ON CONFLICT(id) DO UPDATE SET
4736
- name=@name,email=@email,phone=@phone,location=@location,headline=@headline,summary=@summary,
4737
- desired_roles=@desired_roles,desired_conditions=@desired_conditions,preferred_tone=@preferred_tone,
4738
- emphasis_points=@emphasis_points,links=@links,updated_at=@updated_at`
4739
- ).run({
4740
- ...merged,
4741
- desired_roles: toJson(merged.desired_roles),
4742
- emphasis_points: toJson(merged.emphasis_points),
4743
- links: toJson(merged.links)
4744
- });
4745
- return merged;
4746
- }
4747
- };
4748
- function mapExperience(r) {
4749
- return {
4750
- id: r.id,
4751
- company: r.company,
4752
- role: r.role,
4753
- employment_type: r.employment_type,
4754
- start_date: r.start_date,
4755
- end_date: r.end_date,
4756
- is_current: fromBit(r.is_current),
4757
- description: r.description,
4758
- achievements: fromJson(r.achievements, []),
4759
- tech: fromJson(r.tech, []),
4760
- order_index: r.order_index,
4761
- created_at: r.created_at,
4762
- updated_at: r.updated_at
4763
- };
4764
- }
4765
- var experienceRepo = {
4766
- list() {
4767
- return getDb().prepare(`SELECT * FROM experiences ORDER BY order_index ASC, start_date DESC`).all().map(mapExperience);
4768
- },
4769
- get(id3) {
4770
- const r = getDb().prepare(`SELECT * FROM experiences WHERE id = ?`).get(id3);
4771
- return r ? mapExperience(r) : null;
4772
- },
4773
- add(input) {
4774
- const ts = now();
4775
- const id3 = newId("exp_");
4776
- getDb().prepare(
4777
- `INSERT INTO experiences (id,company,role,employment_type,start_date,end_date,is_current,description,achievements,tech,order_index,created_at,updated_at)
4778
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`
4779
- ).run(
4780
- id3,
4781
- input.company,
4782
- input.role ?? null,
4783
- input.employment_type ?? null,
4784
- input.start_date ?? null,
4785
- input.end_date ?? null,
4786
- toBit(input.is_current),
4787
- input.description ?? null,
4788
- toJson(input.achievements ?? []),
4789
- toJson(input.tech ?? []),
4790
- input.order_index ?? 0,
4791
- ts,
4792
- ts
4793
- );
4794
- return this.get(id3);
4795
- },
4796
- update(id3, input) {
4797
- const cur = this.get(id3);
4798
- if (!cur) return null;
4799
- const m = { ...cur, ...input };
4800
- getDb().prepare(
4801
- `UPDATE experiences SET company=?,role=?,employment_type=?,start_date=?,end_date=?,is_current=?,description=?,achievements=?,tech=?,order_index=?,updated_at=? WHERE id=?`
4802
- ).run(
4803
- m.company,
4804
- m.role,
4805
- m.employment_type,
4806
- m.start_date,
4807
- m.end_date,
4808
- toBit(m.is_current),
4809
- m.description,
4810
- toJson(m.achievements),
4811
- toJson(m.tech),
4812
- m.order_index,
4813
- now(),
4814
- id3
4815
- );
4816
- return this.get(id3);
4817
- },
4818
- remove(id3) {
4819
- return getDb().prepare(`DELETE FROM experiences WHERE id=?`).run(id3).changes > 0;
4820
- }
4821
- };
4822
- function mapProject(r) {
4823
- return {
4824
- id: r.id,
4825
- name: r.name,
4826
- role: r.role,
4827
- description: r.description,
4828
- highlights: fromJson(r.highlights, []),
4829
- tech: fromJson(r.tech, []),
4830
- url: r.url,
4831
- start_date: r.start_date,
4832
- end_date: r.end_date,
4833
- order_index: r.order_index,
4834
- created_at: r.created_at,
4835
- updated_at: r.updated_at
4836
- };
4837
- }
4838
- var projectRepo = {
4839
- list() {
4840
- return getDb().prepare(`SELECT * FROM projects ORDER BY order_index ASC, start_date DESC`).all().map(mapProject);
4841
- },
4842
- get(id3) {
4843
- const r = getDb().prepare(`SELECT * FROM projects WHERE id = ?`).get(id3);
4844
- return r ? mapProject(r) : null;
4845
- },
4846
- add(input) {
4847
- const ts = now();
4848
- const id3 = newId("prj_");
4849
- getDb().prepare(
4850
- `INSERT INTO projects (id,name,role,description,highlights,tech,url,start_date,end_date,order_index,created_at,updated_at)
4851
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`
4852
- ).run(
4853
- id3,
4854
- input.name,
4855
- input.role ?? null,
4856
- input.description ?? null,
4857
- toJson(input.highlights ?? []),
4858
- toJson(input.tech ?? []),
4859
- input.url ?? null,
4860
- input.start_date ?? null,
4861
- input.end_date ?? null,
4862
- input.order_index ?? 0,
4863
- ts,
4864
- ts
4865
- );
4866
- return this.get(id3);
4867
- },
4868
- update(id3, input) {
4869
- const cur = this.get(id3);
4870
- if (!cur) return null;
4871
- const m = { ...cur, ...input };
4872
- getDb().prepare(
4873
- `UPDATE projects SET name=?,role=?,description=?,highlights=?,tech=?,url=?,start_date=?,end_date=?,order_index=?,updated_at=? WHERE id=?`
4874
- ).run(
4875
- m.name,
4876
- m.role,
4877
- m.description,
4878
- toJson(m.highlights),
4879
- toJson(m.tech),
4880
- m.url,
4881
- m.start_date,
4882
- m.end_date,
4883
- m.order_index,
4884
- now(),
4885
- id3
4886
- );
4887
- return this.get(id3);
4888
- },
4889
- remove(id3) {
4890
- return getDb().prepare(`DELETE FROM projects WHERE id=?`).run(id3).changes > 0;
4891
- }
4892
- };
4893
- function mapSkill(r) {
4894
- return {
4895
- id: r.id,
4896
- name: r.name,
4897
- category: r.category,
4898
- level: r.level,
4899
- years: r.years,
4900
- order_index: r.order_index,
4901
- created_at: r.created_at,
4902
- updated_at: r.updated_at
4903
- };
4904
- }
4905
- var skillRepo = {
4906
- list() {
4907
- return getDb().prepare(`SELECT * FROM skills ORDER BY order_index ASC, name ASC`).all().map(mapSkill);
4908
- },
4909
- get(id3) {
4910
- const r = getDb().prepare(`SELECT * FROM skills WHERE id = ?`).get(id3);
4911
- return r ? mapSkill(r) : null;
4912
- },
4913
- add(input) {
4914
- const ts = now();
4915
- const id3 = newId("skl_");
4916
- getDb().prepare(
4917
- `INSERT INTO skills (id,name,category,level,years,order_index,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`
4918
- ).run(
4919
- id3,
4920
- input.name,
4921
- input.category ?? null,
4922
- input.level ?? null,
4923
- input.years ?? null,
4924
- input.order_index ?? 0,
4925
- ts,
4926
- ts
4927
- );
4928
- return this.get(id3);
4929
- },
4930
- update(id3, input) {
4931
- const cur = this.get(id3);
4932
- if (!cur) return null;
4933
- const m = { ...cur, ...input };
4934
- getDb().prepare(`UPDATE skills SET name=?,category=?,level=?,years=?,order_index=?,updated_at=? WHERE id=?`).run(m.name, m.category, m.level, m.years, m.order_index, now(), id3);
4935
- return this.get(id3);
4936
- },
4937
- remove(id3) {
4938
- return getDb().prepare(`DELETE FROM skills WHERE id=?`).run(id3).changes > 0;
4939
- }
4940
- };
4941
- function mapDocument(r) {
4942
- return {
4943
- id: r.id,
4944
- kind: r.kind,
4945
- title: r.title,
4946
- content: r.content,
4947
- source: r.source,
4948
- is_primary: fromBit(r.is_primary),
4949
- tags: fromJson(r.tags, []),
4950
- created_at: r.created_at,
4951
- updated_at: r.updated_at
4952
- };
4953
- }
4954
- var documentRepo = {
4955
- list(kind) {
4956
- const db = getDb();
4957
- const rows = kind ? db.prepare(`SELECT * FROM documents WHERE kind=? ORDER BY is_primary DESC, updated_at DESC`).all(kind) : db.prepare(`SELECT * FROM documents ORDER BY is_primary DESC, updated_at DESC`).all();
4958
- return rows.map(mapDocument);
4959
- },
4960
- get(id3) {
4961
- const r = getDb().prepare(`SELECT * FROM documents WHERE id=?`).get(id3);
4962
- return r ? mapDocument(r) : null;
4963
- },
4964
- primary(kind) {
4965
- const r = getDb().prepare(`SELECT * FROM documents WHERE kind=? ORDER BY is_primary DESC, updated_at DESC LIMIT 1`).get(kind);
4966
- return r ? mapDocument(r) : null;
4967
- },
4968
- add(input) {
4969
- const ts = now();
4970
- const id3 = newId("doc_");
4971
- const db = getDb();
4972
- if (input.is_primary) {
4973
- db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(input.kind);
4974
- }
4975
- db.prepare(
4976
- `INSERT INTO documents (id,kind,title,content,source,is_primary,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
4977
- ).run(
4978
- id3,
4979
- input.kind,
4980
- input.title,
4981
- input.content ?? "",
4982
- input.source ?? "manual",
4983
- toBit(input.is_primary),
4984
- toJson(input.tags ?? []),
4985
- ts,
4986
- ts
4987
- );
4988
- return this.get(id3);
4989
- },
4990
- update(id3, input) {
4991
- const cur = this.get(id3);
4992
- if (!cur) return null;
4993
- const db = getDb();
4994
- const m = { ...cur, ...input };
4995
- if (input.is_primary) db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(m.kind);
4996
- db.prepare(
4997
- `UPDATE documents SET kind=?,title=?,content=?,source=?,is_primary=?,tags=?,updated_at=? WHERE id=?`
4998
- ).run(m.kind, m.title, m.content, m.source, toBit(m.is_primary), toJson(m.tags), now(), id3);
4999
- return this.get(id3);
5000
- },
5001
- remove(id3) {
5002
- return getDb().prepare(`DELETE FROM documents WHERE id=?`).run(id3).changes > 0;
5003
- }
5004
- };
5005
- function mapVersion(r) {
5006
- return {
5007
- id: r.id,
5008
- cover_letter_id: r.cover_letter_id,
5009
- version_no: r.version_no,
5010
- content: r.content,
5011
- note: r.note,
5012
- source: r.source,
5013
- created_at: r.created_at
5014
- };
5015
- }
5016
- function mapCoverLetter(r, withVersions = false) {
5017
- const db = getDb();
5018
- const versions = db.prepare(`SELECT * FROM cover_letter_versions WHERE cover_letter_id=? ORDER BY version_no DESC`).all(r.id).map(mapVersion);
5019
- const current = versions.find((v) => v.id === r.current_version_id) ?? versions[0] ?? null;
5020
- return {
5021
- id: r.id,
5022
- title: r.title,
5023
- job_id: r.job_id,
5024
- is_primary: fromBit(r.is_primary),
5025
- current_version_id: r.current_version_id,
5026
- version_count: versions.length,
5027
- current_content: current ? current.content : null,
5028
- versions: withVersions ? versions : void 0,
5029
- created_at: r.created_at,
5030
- updated_at: r.updated_at
5031
- };
5032
- }
5033
- var coverLetterRepo = {
5034
- list() {
5035
- return getDb().prepare(`SELECT * FROM cover_letters ORDER BY is_primary DESC, updated_at DESC`).all().map((r) => mapCoverLetter(r, false));
5036
- },
5037
- get(id3, withVersions = true) {
5038
- const r = getDb().prepare(`SELECT * FROM cover_letters WHERE id=?`).get(id3);
5039
- return r ? mapCoverLetter(r, withVersions) : null;
5040
- },
5041
- listByJob(jobId) {
5042
- return getDb().prepare(`SELECT * FROM cover_letters WHERE job_id=? ORDER BY updated_at DESC`).all(jobId).map((r) => mapCoverLetter(r, false));
5043
- },
5044
- create(opts) {
5045
- const ts = now();
5046
- const id3 = newId("cl_");
5047
- const db = getDb();
5048
- if (opts.is_primary) db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
5049
- db.prepare(
5050
- `INSERT INTO cover_letters (id,title,job_id,is_primary,current_version_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)`
5051
- ).run(id3, opts.title, opts.job_id ?? null, toBit(opts.is_primary), null, ts, ts);
5052
- return this.get(id3);
5053
- },
5054
- /** Append a new version. Creates the cover letter if cover_letter_id is omitted. */
5055
- addVersion(opts) {
5056
- const db = getDb();
5057
- let clId = opts.cover_letter_id;
5058
- if (!clId) {
5059
- const cl = this.create({ title: opts.title ?? "\uC790\uAE30\uC18C\uAC1C\uC11C", job_id: opts.job_id ?? null });
5060
- clId = cl.id;
5061
- }
5062
- const maxRow = db.prepare(`SELECT MAX(version_no) AS m FROM cover_letter_versions WHERE cover_letter_id=?`).get(clId);
5063
- const versionNo = (maxRow.m ?? 0) + 1;
5064
- const vid = newId("clv_");
5065
- const ts = now();
5066
- db.prepare(
5067
- `INSERT INTO cover_letter_versions (id,cover_letter_id,version_no,content,note,source,created_at) VALUES (?,?,?,?,?,?,?)`
5068
- ).run(vid, clId, versionNo, opts.content, opts.note ?? null, opts.source ?? "ai", ts);
5069
- const setCurrent = opts.set_current !== false;
5070
- if (setCurrent) {
5071
- db.prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(vid, ts, clId);
5072
- } else {
5073
- db.prepare(`UPDATE cover_letters SET updated_at=? WHERE id=?`).run(ts, clId);
5074
- }
5075
- if (opts.title) db.prepare(`UPDATE cover_letters SET title=? WHERE id=?`).run(opts.title, clId);
5076
- if (opts.job_id !== void 0)
5077
- db.prepare(`UPDATE cover_letters SET job_id=? WHERE id=?`).run(opts.job_id, clId);
5078
- return { coverLetter: this.get(clId), version: mapVersion(db.prepare(`SELECT * FROM cover_letter_versions WHERE id=?`).get(vid)) };
5079
- },
5080
- setCurrentVersion(coverLetterId, versionId) {
5081
- getDb().prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(versionId, now(), coverLetterId);
5082
- return this.get(coverLetterId);
5083
- },
5084
- setPrimary(id3) {
5085
- const db = getDb();
5086
- db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
5087
- db.prepare(`UPDATE cover_letters SET is_primary=1, updated_at=? WHERE id=?`).run(now(), id3);
5088
- return this.get(id3);
5089
- },
5090
- remove(id3) {
5091
- const db = getDb();
5092
- db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id3);
5093
- return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id3).changes > 0;
5094
- }
5095
- };
5096
- function mapJob(r) {
5097
- return {
5098
- id: r.id,
5099
- company: r.company,
5100
- position: r.position,
5101
- url: r.url,
5102
- location: r.location,
5103
- employment_type: r.employment_type,
5104
- description: r.description,
5105
- requirements: fromJson(r.requirements, []),
5106
- keywords: fromJson(r.keywords, []),
5107
- deadline: r.deadline,
5108
- source: r.source,
5109
- created_at: r.created_at,
5110
- updated_at: r.updated_at
5111
- };
5112
- }
5113
- var jobRepo = {
5114
- list() {
5115
- return getDb().prepare(`SELECT * FROM jobs ORDER BY updated_at DESC`).all().map(mapJob);
5116
- },
5117
- get(id3) {
5118
- const r = getDb().prepare(`SELECT * FROM jobs WHERE id=?`).get(id3);
5119
- return r ? mapJob(r) : null;
5120
- },
5121
- findByUrl(url) {
5122
- const r = getDb().prepare(`SELECT * FROM jobs WHERE url=? LIMIT 1`).get(url);
5123
- return r ? mapJob(r) : null;
5124
- },
5125
- relatedTo(company, position, excludeId) {
5126
- const rows = getDb().prepare(
5127
- `SELECT * FROM jobs WHERE (company=? OR position LIKE ?) AND id != ? ORDER BY updated_at DESC LIMIT 10`
5128
- ).all(company, `%${position}%`, excludeId ?? "");
5129
- return rows.map(mapJob);
5130
- },
5131
- upsert(input, id3) {
5132
- const db = getDb();
5133
- const existing = id3 ? this.get(id3) : input.url ? this.findByUrl(input.url) : null;
5134
- const ts = now();
5135
- if (existing) {
5136
- const m = { ...existing, ...input };
5137
- db.prepare(
5138
- `UPDATE jobs SET company=?,position=?,url=?,location=?,employment_type=?,description=?,requirements=?,keywords=?,deadline=?,source=?,updated_at=? WHERE id=?`
5139
- ).run(
5140
- m.company,
5141
- m.position,
5142
- m.url ?? null,
5143
- m.location ?? null,
5144
- m.employment_type ?? null,
5145
- m.description ?? null,
5146
- toJson(m.requirements ?? []),
5147
- toJson(m.keywords ?? []),
5148
- m.deadline ?? null,
5149
- m.source ?? null,
5150
- ts,
5151
- existing.id
5152
- );
5153
- return this.get(existing.id);
5154
- }
5155
- const newJobId = id3 ?? newId("job_");
5156
- db.prepare(
5157
- `INSERT INTO jobs (id,company,position,url,location,employment_type,description,requirements,keywords,deadline,source,created_at,updated_at)
5158
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`
5159
- ).run(
5160
- newJobId,
5161
- input.company,
5162
- input.position,
5163
- input.url ?? null,
5164
- input.location ?? null,
5165
- input.employment_type ?? null,
5166
- input.description ?? null,
5167
- toJson(input.requirements ?? []),
5168
- toJson(input.keywords ?? []),
5169
- input.deadline ?? null,
5170
- input.source ?? null,
5171
- ts,
5172
- ts
5173
- );
5174
- return this.get(newJobId);
5175
- },
5176
- remove(id3) {
5177
- const db = getDb();
5178
- db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id3);
5179
- db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id3);
5180
- db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id3);
5181
- return db.prepare(`DELETE FROM jobs WHERE id=?`).run(id3).changes > 0;
5182
- }
5183
- };
5184
- function mapFit(r) {
5185
- return {
5186
- id: r.id,
5187
- job_id: r.job_id,
5188
- score: r.score,
5189
- summary: r.summary,
5190
- strengths: fromJson(r.strengths, []),
5191
- gaps: fromJson(r.gaps, []),
5192
- matched_keywords: fromJson(r.matched_keywords, []),
5193
- missing_keywords: fromJson(r.missing_keywords, []),
5194
- recommendations: fromJson(r.recommendations, []),
5195
- created_at: r.created_at,
5196
- updated_at: r.updated_at
5197
- };
5198
- }
5199
- var fitRepo = {
5200
- getByJob(jobId) {
5201
- const r = getDb().prepare(`SELECT * FROM fit_analyses WHERE job_id=? ORDER BY updated_at DESC LIMIT 1`).get(jobId);
5202
- return r ? mapFit(r) : null;
5203
- },
5204
- save(input) {
5205
- const db = getDb();
5206
- const existing = this.getByJob(input.job_id);
5207
- const ts = now();
5208
- if (existing) {
5209
- const m = { ...existing, ...input };
5210
- db.prepare(
5211
- `UPDATE fit_analyses SET score=?,summary=?,strengths=?,gaps=?,matched_keywords=?,missing_keywords=?,recommendations=?,updated_at=? WHERE id=?`
5212
- ).run(
5213
- m.score ?? null,
5214
- m.summary ?? null,
5215
- toJson(m.strengths ?? []),
5216
- toJson(m.gaps ?? []),
5217
- toJson(m.matched_keywords ?? []),
5218
- toJson(m.missing_keywords ?? []),
5219
- toJson(m.recommendations ?? []),
5220
- ts,
5221
- existing.id
5222
- );
5223
- return this.getByJob(input.job_id);
5224
- }
5225
- const id3 = newId("fit_");
5226
- db.prepare(
5227
- `INSERT INTO fit_analyses (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
5228
- VALUES (?,?,?,?,?,?,?,?,?,?,?)`
5229
- ).run(
5230
- id3,
5231
- input.job_id,
5232
- input.score ?? null,
5233
- input.summary ?? null,
5234
- toJson(input.strengths ?? []),
5235
- toJson(input.gaps ?? []),
5236
- toJson(input.matched_keywords ?? []),
5237
- toJson(input.missing_keywords ?? []),
5238
- toJson(input.recommendations ?? []),
5239
- ts,
5240
- ts
5241
- );
5242
- return this.getByJob(input.job_id);
5243
- }
5244
- };
5245
- function mapApplication(r) {
5246
- return {
5247
- id: r.id,
5248
- job_id: r.job_id,
5249
- status: r.status,
5250
- resume_id: r.resume_id,
5251
- cover_letter_id: r.cover_letter_id,
5252
- applied_at: r.applied_at,
5253
- notes: r.notes,
5254
- created_at: r.created_at,
5255
- updated_at: r.updated_at
5256
- };
5257
- }
5258
- var applicationRepo = {
5259
- list() {
5260
- return getDb().prepare(`SELECT * FROM applications ORDER BY updated_at DESC`).all().map(
5261
- mapApplication
5262
- );
5263
- },
5264
- get(id3) {
5265
- const r = getDb().prepare(`SELECT * FROM applications WHERE id=?`).get(id3);
5266
- return r ? mapApplication(r) : null;
5267
- },
5268
- getByJob(jobId) {
5269
- const r = getDb().prepare(`SELECT * FROM applications WHERE job_id=?`).get(jobId);
5270
- return r ? mapApplication(r) : null;
5271
- },
5272
- recent(limit = 10) {
5273
- return getDb().prepare(`SELECT * FROM applications ORDER BY updated_at DESC LIMIT ?`).all(limit).map(mapApplication);
5274
- },
5275
- /** Ensure a (single) application row exists for a job. */
5276
- ensure(jobId, status = "draft") {
5277
- const existing = this.getByJob(jobId);
5278
- if (existing) return existing;
5279
- const ts = now();
5280
- const id3 = newId("app_");
5281
- getDb().prepare(
5282
- `INSERT INTO applications (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`
5283
- ).run(id3, jobId, status, null, null, null, null, ts, ts);
5284
- return this.get(id3);
5285
- },
5286
- upsert(input) {
5287
- const app = this.ensure(input.job_id, input.status ?? "draft");
5288
- const m = { ...app, ...input };
5289
- getDb().prepare(
5290
- `UPDATE applications SET status=?,resume_id=?,cover_letter_id=?,applied_at=?,notes=?,updated_at=? WHERE id=?`
5291
- ).run(m.status, m.resume_id, m.cover_letter_id, m.applied_at, m.notes, now(), app.id);
5292
- return this.get(app.id);
5293
- },
5294
- setStatus(jobId, status, note) {
5295
- const app = this.ensure(jobId);
5296
- const appliedAt = status === "applied" && !app.applied_at ? now() : app.applied_at;
5297
- const notes = note ? [app.notes, note].filter(Boolean).join("\n") : app.notes;
5298
- getDb().prepare(`UPDATE applications SET status=?, applied_at=?, notes=?, updated_at=? WHERE id=?`).run(status, appliedAt, notes, now(), app.id);
5299
- return this.get(app.id);
5300
- }
5301
- };
5302
- function mapInterviewPrep(r) {
5303
- return {
5304
- id: r.id,
5305
- job_id: r.job_id,
5306
- questions: fromJson(r.questions, []),
5307
- star_guides: fromJson(r.star_guides, []),
5308
- self_introduction: r.self_introduction,
5309
- notes: r.notes,
5310
- created_at: r.created_at,
5311
- updated_at: r.updated_at
5312
- };
5313
- }
5314
- var interviewRepo = {
5315
- getByJob(jobId) {
5316
- const r = getDb().prepare(`SELECT * FROM interview_preps WHERE job_id=?`).get(jobId);
5317
- return r ? mapInterviewPrep(r) : null;
5318
- },
5319
- list() {
5320
- return getDb().prepare(`SELECT * FROM interview_preps ORDER BY updated_at DESC`).all().map(
5321
- mapInterviewPrep
5322
- );
5323
- },
5324
- save(input) {
5325
- const db = getDb();
5326
- const existing = this.getByJob(input.job_id);
5327
- const ts = now();
5328
- if (existing) {
5329
- const m = { ...existing, ...input };
5330
- db.prepare(
5331
- `UPDATE interview_preps SET questions=?,star_guides=?,self_introduction=?,notes=?,updated_at=? WHERE id=?`
5332
- ).run(
5333
- toJson(m.questions ?? []),
5334
- toJson(m.star_guides ?? []),
5335
- m.self_introduction ?? null,
5336
- m.notes ?? null,
5337
- ts,
5338
- existing.id
5339
- );
5340
- return this.getByJob(input.job_id);
5341
- }
5342
- const id3 = newId("itv_");
5343
- db.prepare(
5344
- `INSERT INTO interview_preps (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`
5345
- ).run(
5346
- id3,
5347
- input.job_id,
5348
- toJson(input.questions ?? []),
5349
- toJson(input.star_guides ?? []),
5350
- input.self_introduction ?? null,
5351
- input.notes ?? null,
5352
- ts,
5353
- ts
5354
- );
5355
- return this.getByJob(input.job_id);
5356
- }
5357
- };
5358
- var activityRepo = {
5359
- log(type, summary, entity_type, entity_id) {
5360
- const ts = now();
5361
- const id3 = newId("act_");
5362
- getDb().prepare(
5363
- `INSERT INTO activities (id,type,entity_type,entity_id,summary,created_at) VALUES (?,?,?,?,?,?)`
5364
- ).run(id3, type, entity_type ?? null, entity_id ?? null, summary, ts);
5365
- return { id: id3, type, entity_type: entity_type ?? null, entity_id: entity_id ?? null, summary, created_at: ts };
5366
- },
5367
- recent(limit = 20) {
5368
- return getDb().prepare(`SELECT * FROM activities ORDER BY created_at DESC LIMIT ?`).all(limit);
5369
- }
5370
- };
5371
-
5372
- // apps/web/src/security.ts
5373
- import crypto from "node:crypto";
5374
- var SESSION_TOKEN = crypto.randomBytes(24).toString("hex");
5375
- var TOKEN_HEADER = "x-careermate-token";
5376
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
5377
- function hostOnly(value) {
5378
- if (!value) return null;
5379
- if (value.startsWith("[")) {
5380
- const end = value.indexOf("]");
5381
- return end === -1 ? value.toLowerCase() : value.slice(0, end + 1).toLowerCase();
5382
- }
5383
- return value.split(":")[0].toLowerCase();
5384
- }
5385
- function isLoopbackHost(hostHeader) {
5386
- const h = hostOnly(hostHeader);
5387
- return h != null && LOOPBACK_HOSTS.has(h);
5388
- }
5389
- function originHost(origin) {
5390
- if (!origin || origin === "null") return null;
5391
- try {
5392
- return new URL(origin).hostname.toLowerCase();
5393
- } catch {
5394
- return null;
5395
- }
5396
- }
5397
- var MUTATING = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
5398
- function checkRequest(req) {
5399
- if (!isLoopbackHost(req.headers.host)) {
5400
- return { ok: false, status: 403, reason: "invalid_host" };
5401
- }
5402
- const origin = req.headers.origin;
5403
- if (origin) {
5404
- const oh = originHost(origin);
5405
- if (oh == null || !LOOPBACK_HOSTS.has(oh)) {
5406
- return { ok: false, status: 403, reason: "cross_origin" };
5407
- }
5408
- }
5409
- if (MUTATING.has(req.method ?? "")) {
5410
- const token = req.headers[TOKEN_HEADER];
5411
- const provided = Array.isArray(token) ? token[0] : token;
5412
- if (!provided || !timingSafeEqual(provided, SESSION_TOKEN)) {
5413
- return { ok: false, status: 403, reason: "invalid_csrf_token" };
5414
- }
5415
- }
5416
- return { ok: true };
5417
- }
5418
- function timingSafeEqual(a, b) {
5419
- const ab = Buffer.from(a);
5420
- const bb = Buffer.from(b);
5421
- if (ab.length !== bb.length) return false;
5422
- return crypto.timingSafeEqual(ab, bb);
5423
- }
5424
- function injectToken(html) {
5425
- const tag = `<meta name="careermate-token" content="${SESSION_TOKEN}">`;
5426
- if (html.includes("</head>")) return html.replace("</head>", ` ${tag}
5427
- </head>`);
5428
- return tag + html;
5429
- }
5430
-
5431
- // apps/web/src/http.ts
5432
- import fs3 from "node:fs";
5433
- import path3 from "node:path";
5434
- var MAX_BODY = 8 * 1024 * 1024;
5435
- var Router = class {
5436
- routes = [];
5437
- add(method, path6, handler) {
5438
- const keys = [];
5439
- const pattern = new RegExp(
5440
- "^" + path6.replace(/:[^/]+/g, (m) => {
5441
- keys.push(m.slice(1));
5442
- return "([^/]+)";
5443
- }) + "/?$"
5444
- );
5445
- this.routes.push({ method: method.toUpperCase(), pattern, keys, handler });
5446
- return this;
5447
- }
5448
- get(p, h) {
5449
- return this.add("GET", p, h);
5450
- }
5451
- post(p, h) {
5452
- return this.add("POST", p, h);
5453
- }
5454
- put(p, h) {
5455
- return this.add("PUT", p, h);
5456
- }
5457
- patch(p, h) {
5458
- return this.add("PATCH", p, h);
5459
- }
5460
- delete(p, h) {
5461
- return this.add("DELETE", p, h);
5462
- }
5463
- match(method, pathname) {
5464
- for (const r of this.routes) {
5465
- if (r.method !== method) continue;
5466
- const m = r.pattern.exec(pathname);
5467
- if (!m) continue;
5468
- const params = {};
5469
- r.keys.forEach((k, i) => params[k] = decodeURIComponent(m[i + 1]));
5470
- return { handler: r.handler, params };
5471
- }
5472
- return null;
5473
- }
5474
- };
5475
- function sendJson(res, status, data) {
5476
- const body = JSON.stringify(data);
5477
- res.writeHead(status, {
5478
- "Content-Type": "application/json; charset=utf-8",
5479
- "Cache-Control": "no-store",
5480
- "X-Content-Type-Options": "nosniff"
5481
- });
5482
- res.end(body);
5483
- }
5484
- function sendDownload(res, filename, mimeType, content) {
5485
- const safe = filename.replace(/[^\w.\-가-힣 ]/g, "_");
5486
- res.writeHead(200, {
5487
- "Content-Type": mimeType,
5488
- "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(safe)}`,
5489
- "Cache-Control": "no-store",
5490
- "X-Content-Type-Options": "nosniff"
5491
- });
5492
- res.end(content);
5493
- }
5494
- var HttpError = class extends Error {
5495
- constructor(status, message, code) {
5496
- super(message);
5497
- this.status = status;
5498
- this.code = code;
5499
- }
5500
- };
5501
- async function readJsonBody(req, schema) {
5502
- const raw = await readBody(req);
5503
- if (!raw) return schema ? schema.parse({}) : {};
5504
- let parsed;
5505
- try {
5506
- parsed = JSON.parse(raw);
5507
- } catch {
5508
- throw new HttpError(400, "\uC798\uBABB\uB41C JSON \uD615\uC2DD\uC785\uB2C8\uB2E4.", "bad_json");
5509
- }
5510
- if (!schema) return parsed;
5511
- try {
5512
- return schema.parse(parsed);
5513
- } catch (e) {
5514
- if (e instanceof ZodError) {
5515
- const msg = e.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
5516
- throw new HttpError(400, `\uC785\uB825\uAC12 \uAC80\uC99D \uC2E4\uD328 \u2014 ${msg}`, "validation");
5517
- }
5518
- throw e;
5519
- }
5520
- }
5521
- function readBody(req) {
5522
- return new Promise((resolve, reject) => {
5523
- let size = 0;
5524
- const chunks = [];
5525
- req.on("data", (c) => {
5526
- size += c.length;
5527
- if (size > MAX_BODY) {
5528
- reject(new HttpError(413, "\uC694\uCCAD \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4.", "too_large"));
5529
- req.destroy();
5530
- return;
5531
- }
5532
- chunks.push(c);
5533
- });
5534
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
5535
- req.on("error", reject);
5536
- });
5537
- }
5538
- var MIME = {
5539
- ".html": "text/html; charset=utf-8",
5540
- ".js": "text/javascript; charset=utf-8",
5541
- ".mjs": "text/javascript; charset=utf-8",
5542
- ".css": "text/css; charset=utf-8",
5543
- ".json": "application/json; charset=utf-8",
5544
- ".svg": "image/svg+xml",
5545
- ".png": "image/png",
5546
- ".jpg": "image/jpeg",
5547
- ".ico": "image/x-icon",
5548
- ".woff2": "font/woff2",
5549
- ".map": "application/json"
5550
- };
5551
- function serveStatic(res, root, relPath, transform) {
5552
- const cleaned = decodeURIComponent(relPath).replace(/\0/g, "");
5553
- const resolved = path3.resolve(root, "." + path3.posix.normalize("/" + cleaned));
5554
- if (resolved !== root && !resolved.startsWith(root + path3.sep)) {
5555
- res.writeHead(403).end("Forbidden");
5556
- return true;
5557
- }
5558
- if (!fs3.existsSync(resolved) || !fs3.statSync(resolved).isFile()) return false;
5559
- const ext = path3.extname(resolved).toLowerCase();
5560
- const mime = MIME[ext] ?? "application/octet-stream";
5561
- let payload = fs3.readFileSync(resolved);
5562
- if (transform && ext === ".html") payload = transform(payload.toString("utf8"));
5563
- res.writeHead(200, {
5564
- "Content-Type": mime,
5565
- "X-Content-Type-Options": "nosniff",
5566
- "Cache-Control": ext === ".html" ? "no-store" : "no-cache"
5567
- });
5568
- res.end(payload);
5569
- return true;
5570
- }
5571
-
5572
- // packages/core/src/onboarding.ts
5573
- function profileCompleteness(p) {
5574
- if (!p) return 0;
5575
- const checks = [
5576
- !!p.name,
5577
- !!(p.headline || p.summary),
5578
- !!p.location,
5579
- p.desired_roles.length > 0,
5580
- !!p.desired_conditions,
5581
- !!p.preferred_tone,
5582
- p.emphasis_points.length > 0,
5583
- p.links.length > 0,
5584
- experienceRepo.list().length > 0,
5585
- skillRepo.list().length > 0
5586
- ];
5587
- const score = Math.round(checks.filter(Boolean).length / checks.length * 100);
5588
- return score;
5589
- }
5590
- function getOnboardingStatus() {
5591
- const profile = profileRepo.get();
5592
- const resumes = documentRepo.list("resume");
5593
- const coverLetters = coverLetterRepo.list();
5594
- const experiences = experienceRepo.list();
5595
- const skills = skillRepo.list();
5596
- const jobs = jobRepo.list();
5597
- const has_profile = !!(profile && profile.name);
5598
- const has_resume = resumes.length > 0;
5599
- const has_cover_letter = coverLetters.length > 0;
5600
- const has_experience = experiences.length > 0;
5601
- const has_skills = skills.length > 0;
5602
- const has_job = jobs.length > 0;
5603
- const completeness = profileCompleteness(profile);
5604
- const completed = has_profile && (has_resume || has_experience);
5605
- const next_steps = [];
5606
- if (!has_profile) next_steps.push("\uAE30\uBCF8 \uD504\uB85C\uD544\uC744 \uC785\uB825\uD558\uC138\uC694 (\uC774\uB984, \uD55C \uC904 \uC18C\uAC1C, \uD76C\uB9DD \uC9C1\uBB34).");
5607
- if (!has_resume) next_steps.push("\uC774\uB825\uC11C\uB098 \uACBD\uB825\uAE30\uC220\uC11C\uB97C \uCD94\uAC00\uD558\uC138\uC694. \uD30C\uC77C \uB0B4\uC6A9\uC744 \uBD99\uC5EC\uB123\uC5B4\uB3C4 \uB429\uB2C8\uB2E4.");
5608
- if (!has_experience) next_steps.push("\uC8FC\uC694 \uACBD\uB825\uC744 1\uAC1C \uC774\uC0C1 \uCD94\uAC00\uD558\uBA74 \uC801\uD569\uB3C4 \uBD84\uC11D \uD488\uC9C8\uC774 \uC62C\uB77C\uAC11\uB2C8\uB2E4.");
5609
- if (!has_skills) next_steps.push("\uBCF4\uC720 \uAE30\uC220\uC2A4\uD0DD\uC744 \uC815\uB9AC\uD574 \uB450\uBA74 \uACF5\uACE0 \uB9E4\uCE6D\uC774 \uC815\uD655\uD574\uC9D1\uB2C8\uB2E4.");
5610
- if (has_profile && !has_cover_letter)
5611
- next_steps.push("\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uAC00 \uC788\uB2E4\uBA74 \uCD94\uAC00\uD574 \uB450\uC138\uC694. AI\uAC00 \uBB38\uCCB4\uB97C \uD559\uC2B5\uD569\uB2C8\uB2E4.");
5612
- if (completed && !has_job)
5613
- next_steps.push("\uAD00\uC2EC \uC788\uB294 \uCC44\uC6A9\uACF5\uACE0 URL\uC774\uB098 \uB0B4\uC6A9\uC744 AI\uC5D0\uAC8C \uC804\uB2EC\uD574 \uC801\uD569\uB3C4 \uBD84\uC11D\uC744 \uBC1B\uC544\uBCF4\uC138\uC694.");
5614
- if (next_steps.length === 0)
5615
- next_steps.push("\uC900\uBE44\uAC00 \uC798 \uB418\uC5B4 \uC788\uC5B4\uC694. \uC0C8 \uACF5\uACE0\uB97C \uBD84\uC11D\uD558\uAC70\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC791\uC131\uD574 \uBCF4\uC138\uC694.");
5616
- return {
5617
- completed,
5618
- has_profile,
5619
- has_resume,
5620
- has_cover_letter,
5621
- has_experience,
5622
- has_skills,
5623
- has_job,
5624
- profile_completeness: completeness,
5625
- next_steps
5626
- };
5627
- }
5628
-
5629
- // packages/core/src/context.ts
5630
- function getApplicationContext(opts = {}) {
5631
- const profile = profileRepo.get();
5632
- const resumes = documentRepo.list("resume");
5633
- const primary_resume = documentRepo.primary("resume");
5634
- const experiences = experienceRepo.list();
5635
- const projects = projectRepo.list();
5636
- const skills = skillRepo.list();
5637
- const cover_letters = coverLetterRepo.list();
5638
- const recent_applications = applicationRepo.recent(opts.recent_limit ?? 10);
5639
- const job = opts.job_id ? jobRepo.get(opts.job_id) : null;
5640
- const fit_analysis = opts.job_id ? fitRepo.getByJob(opts.job_id) : null;
5641
- let related_history = [];
5642
- if (job) {
5643
- const related = jobRepo.relatedTo(job.company, job.position, job.id);
5644
- related_history = related.map((rj) => ({
5645
- job: rj,
5646
- application: applicationRepo.getByJob(rj.id),
5647
- fit_analysis: fitRepo.getByJob(rj.id)
5648
- }));
5649
- }
5650
- return {
5651
- profile,
5652
- primary_resume,
5653
- resumes,
5654
- experiences,
5655
- projects,
5656
- skills,
5657
- cover_letters,
5658
- recent_applications,
5659
- job,
5660
- fit_analysis,
5661
- related_history,
5662
- writing_preferences: {
5663
- preferred_tone: profile?.preferred_tone ?? null,
5664
- emphasis_points: profile?.emphasis_points ?? []
5665
- }
5666
- };
5667
- }
5668
-
5669
- // packages/core/src/services.ts
5670
- function saveProfile(input) {
5671
- const p = profileRepo.save(input);
5672
- activityRepo.log("profile_updated", `\uD504\uB85C\uD544\uC744 ${p.name ? `(${p.name}) ` : ""}\uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, "profile", p.id);
5673
- return p;
5674
- }
5675
- function addResume(input) {
5676
- const doc = documentRepo.add({ ...input, kind: input.kind ?? "resume" });
5677
- activityRepo.log("resume_added", `${doc.title} \uBB38\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "document", doc.id);
5678
- return doc;
5679
- }
5680
- function saveJobPosting(input, id3) {
5681
- const job = jobRepo.upsert(input, id3);
5682
- const application = applicationRepo.ensure(job.id, "draft");
5683
- activityRepo.log("job_saved", `${job.company} \xB7 ${job.position} \uACF5\uACE0\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, "job", job.id);
5684
- return { job, application };
5685
- }
5686
- function saveFitAnalysis(input) {
5687
- const job = jobRepo.get(input.job_id);
5688
- 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.`);
5689
- applicationRepo.ensure(job.id, "draft");
5690
- const fit = fitRepo.save(input);
5691
- const scoreText = fit.score != null ? ` (\uC801\uD569\uB3C4 ${fit.score}\uC810)` : "";
5692
- activityRepo.log("fit_analysis_saved", `${job.company} \xB7 ${job.position} \uC801\uD569\uB3C4 \uBD84\uC11D\uC744 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4${scoreText}.`, "fit_analysis", fit.id);
5693
- return fit;
5694
- }
5695
- function saveCoverLetterVersion(input) {
5696
- const { coverLetter, version } = coverLetterRepo.addVersion({
5697
- cover_letter_id: input.cover_letter_id,
5698
- title: input.title,
5699
- job_id: input.job_id ?? null,
5700
- content: input.content,
5701
- note: input.note,
5702
- source: input.source ?? "ai",
5703
- set_current: input.set_current
5704
- });
5705
- if (coverLetter.job_id) {
5706
- const app = applicationRepo.getByJob(coverLetter.job_id);
5707
- if (app && !app.cover_letter_id) {
5708
- applicationRepo.upsert({ job_id: coverLetter.job_id, cover_letter_id: coverLetter.id });
5709
- }
5710
- }
5711
- activityRepo.log(
5712
- "cover_letter_version_saved",
5713
- `${coverLetter.title} \uC790\uAE30\uC18C\uAC1C\uC11C v${version.version_no}\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`,
5714
- "cover_letter",
5715
- coverLetter.id
5716
- );
5717
- return { coverLetter, version };
5718
- }
5719
- function updateApplicationStatus(jobId, status, note) {
5720
- const job = jobRepo.get(jobId);
5721
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${jobId}`);
5722
- const application = applicationRepo.setStatus(jobId, status, note);
5723
- activityRepo.log(
5724
- "application_status_changed",
5725
- `${job.company} \xB7 ${job.position} \uC0C1\uD0DC\uB97C '${APPLICATION_STATUS_LABELS[status]}'(\uC73C)\uB85C \uBCC0\uACBD\uD588\uC2B5\uB2C8\uB2E4.`,
5726
- "application",
5727
- application.id
5728
- );
5729
- const interview_unlocked = INTERVIEW_UNLOCK_STATUSES.includes(status);
5730
- const hasPrep = !!interviewRepo.getByJob(jobId);
5731
- 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;
5732
- return { application, job, interview_unlocked, hint };
5733
- }
5734
- function saveInterviewPrep(input) {
5735
- const job = jobRepo.get(input.job_id);
5736
- if (!job) throw new Error(`\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.job_id}`);
5737
- applicationRepo.ensure(job.id);
5738
- const prep = interviewRepo.save(input);
5739
- activityRepo.log("interview_prep_saved", `${job.company} \xB7 ${job.position} \uBA74\uC811 \uC900\uBE44 \uC790\uB8CC\uB97C \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4.`, "interview_prep", prep.id);
5740
- return prep;
5741
- }
5742
-
5743
- // packages/core/src/summary.ts
5744
- var ACTIVE = ["draft", "planned", "applied", "document_passed", "interview"];
5745
- function jobWithMeta(job) {
5746
- const app = applicationRepo.getByJob(job.id);
5747
- const fit = fitRepo.getByJob(job.id);
5748
- const status = app?.status ?? "draft";
5749
- return { ...job, status, status_label: APPLICATION_STATUS_LABELS[status], fit_score: fit?.score ?? null };
5750
- }
5751
- function getHomeSummary() {
5752
- const onboarding = getOnboardingStatus();
5753
- const jobs = jobRepo.list();
5754
- const applications = applicationRepo.list();
5755
- const coverLetters = coverLetterRepo.list();
5756
- const status_breakdown = Object.keys(APPLICATION_STATUS_LABELS).map((status) => ({
5757
- status,
5758
- label: APPLICATION_STATUS_LABELS[status],
5759
- count: applications.filter((a) => a.status === status).length
5760
- }));
5761
- const active = applications.filter((a) => ACTIVE.includes(a.status));
5762
- const interview_todo = applications.filter((a) => INTERVIEW_UNLOCK_STATUSES.includes(a.status) && !interviewRepo.getByJob(a.job_id)).map((a) => {
5763
- const job = jobRepo.get(a.job_id);
5764
- return job ? { job, status_label: APPLICATION_STATUS_LABELS[a.status] } : null;
5765
- }).filter((x) => x !== null);
5766
- return {
5767
- onboarding,
5768
- counts: {
5769
- jobs: jobs.length,
5770
- active_applications: active.length,
5771
- cover_letters: coverLetters.length,
5772
- interview_pending: interview_todo.length
5773
- },
5774
- status_breakdown,
5775
- recent_jobs: jobs.slice(0, 6).map(jobWithMeta),
5776
- in_progress: active.slice(0, 6).map((application) => {
5777
- const job = jobRepo.get(application.job_id);
5778
- return {
5779
- application,
5780
- job,
5781
- status_label: APPLICATION_STATUS_LABELS[application.status]
5782
- };
5783
- }),
5784
- recent_cover_letters: coverLetters.slice(0, 5),
5785
- interview_todo,
5786
- recent_activity: activityRepo.recent(10)
5787
- };
5788
- }
5789
- function listRecentActivity(limit = 20) {
5790
- return activityRepo.recent(limit);
5791
- }
5792
-
5793
- // packages/prompts/src/system.ts
5794
- var CAREERMATE_SYSTEM_PROMPT = `\uB2F9\uC2E0\uC740 \uC0AC\uC6A9\uC790\uC758 'CareerMate' \uCEE4\uB9AC\uC5B4 \uBE44\uC11C\uC785\uB2C8\uB2E4.
5795
-
5796
- CareerMate\uB294 \uC0AC\uC6A9\uC790\uC758 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uB3D9\uC791\uD558\uB294 \uB85C\uCEEC \uCEE4\uB9AC\uC5B4 \uAD00\uB9AC \uB3C4\uAD6C\uC774\uBA70, MCP(\uD234 \uD638\uCD9C)\uB97C \uD1B5\uD574 \uC0AC\uC6A9\uC790\uC758 \uD504\uB85C\uD544\xB7\uC774\uB825\uC11C\xB7\uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC9C0\uC6D0 \uACF5\uACE0\xB7\uC9C0\uC6D0 \uD604\uD669 \uB370\uC774\uD130\uB97C \uC77D\uACE0 \uC501\uB2C8\uB2E4. CareerMate \uC548\uC5D0\uB294 \uBCC4\uB3C4\uC758 AI\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBAA8\uB4E0 \uBD84\uC11D\xB7\uC791\uC131\xB7\uC694\uC57D\uC740 '\uB2F9\uC2E0'(\uC0AC\uC6A9\uC790\uC758 AI)\uC774 MCP \uD234\uC744 \uD638\uCD9C\uD574 \uC218\uD589\uD569\uB2C8\uB2E4.
5797
-
5798
- # \uD575\uC2EC \uD589\uB3D9 \uC6D0\uCE59
5799
-
5800
- 1. \uD56D\uC0C1 \uB9E5\uB77D\uC744 \uBA3C\uC800 \uBD88\uB7EC\uC635\uB2C8\uB2E4.
5801
- - \uACF5\uACE0\uB97C \uBD84\uC11D\uD558\uAC70\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC4F0\uAE30 \uC804\uC5D0\uB294 \uBC18\uB4DC\uC2DC \`get_application_context\`\uB97C \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694. \uC774 \uD55C \uBC88\uC758 \uD638\uCD9C\uB85C \uD504\uB85C\uD544, \uB300\uD45C \uC774\uB825\uC11C, \uACBD\uB825(experiences), \uD504\uB85C\uC81D\uD2B8(projects), \uC2A4\uD0AC(skills), \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C, \uCD5C\uADFC \uC9C0\uC6D0 \uD604\uD669, \uB300\uC0C1 \uACF5\uACE0(job_id \uC804\uB2EC \uC2DC), \uC774\uC804 \uC801\uD569\uB3C4 \uBD84\uC11D, \uAC19\uC740 \uD68C\uC0AC/\uC9C1\uBB34 \uAD00\uB828 \uAE30\uB85D, \uADF8\uB9AC\uACE0 \uC0AC\uC6A9\uC790\uC758 \uAE00\uC4F0\uAE30 \uC120\uD638(\uC120\uD638 \uBB38\uCCB4 preferred_tone + \uAC15\uC870 \uD3EC\uC778\uD2B8 emphasis_points)\uB97C \uD55C\uAEBC\uBC88\uC5D0 \uBC1B\uC2B5\uB2C8\uB2E4.
5802
- - \uC77C\uBC18\uC801\uC778 \uC0C1\uD0DC \uD655\uC778\uC774 \uD544\uC694\uD558\uBA74 \`get_onboarding_status\`, \`get_profile\`, \`get_resumes\`, \`get_cover_letters\`, \`list_recent_activity\` \uB4F1 \uC77D\uAE30 \uD234\uC744 \uC801\uADF9 \uC0AC\uC6A9\uD558\uC138\uC694.
5803
- - \uAE30\uC5B5\uC5D0 \uC758\uC874\uD558\uC9C0 \uB9D0\uACE0, \uB9E4 \uC791\uC5C5 \uC2DC\uC791 \uC2DC \uC2E4\uC81C \uB370\uC774\uD130\uB97C \uB2E4\uC2DC \uBD88\uB7EC\uC624\uC138\uC694.
5804
-
5805
- 2. \uACB0\uACFC\uB294 \uBC18\uB4DC\uC2DC \uB2E4\uC2DC \uC800\uC7A5\uD569\uB2C8\uB2E4.
5806
- - \uBD84\uC11D\xB7\uC791\uC131\uD55C \uB0B4\uC6A9\uC740 \uBA38\uB9BF\uC18D\uC774\uB098 \uB300\uD654\uCC3D\uC5D0\uB9CC \uB0A8\uAE30\uC9C0 \uB9D0\uACE0 \uD574\uB2F9 \uC800\uC7A5 \uD234\uB85C CareerMate\uC5D0 \uAE30\uB85D\uD558\uC138\uC694: \uACF5\uACE0\uB294 \`save_job_posting\`, \uC801\uD569\uB3C4 \uBD84\uC11D\uC740 \`save_fit_analysis\`, \uC790\uAE30\uC18C\uAC1C\uC11C \uBC84\uC804\uC740 \`save_cover_letter_version\`, \uBA74\uC811 \uC900\uBE44\uB294 \`save_interview_prep\`, \uC0C1\uD0DC \uBCC0\uACBD\uC740 \`update_application_status\`.
5807
- - \uC800\uC7A5 \uD6C4\uC5D0\uB294 \uBB34\uC5C7\uC774 \uC800\uC7A5\uB418\uC5C8\uB294\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uAC04\uB2E8\uD788 \uC54C\uB824\uC8FC\uC138\uC694.
5808
-
5809
- 3. \uB370\uC774\uD130\uB97C \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
5810
- - \uD504\uB85C\uD544\xB7\uACBD\uB825\xB7\uC131\uACFC\xB7\uC218\uCE58\xB7\uD68C\uC0AC\uBA85 \uB4F1\uC740 \uC0AC\uC6A9\uC790\uAC00 \uC81C\uACF5\uD588\uAC70\uB098 CareerMate\uC5D0 \uC800\uC7A5\uB41C \uAC83\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694. \uBD88\uD655\uC2E4\uD558\uBA74 \uCD94\uCE21\uD558\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694.
5811
- - \uC790\uAE30\uC18C\uAC1C\uC11C\uC5D0 \uAC80\uC99D\uB418\uC9C0 \uC54A\uC740 \uACBD\uD5D8\uC774\uB098 \uACFC\uC7A5\uB41C \uC131\uACFC\uB97C \uB123\uC9C0 \uB9C8\uC138\uC694. \uBE48\uCE78\uC774 \uC788\uC73C\uBA74 "\uC774 \uBD80\uBD84\uC740 \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"\uB77C\uACE0 \uD45C\uC2DC\uD558\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uC744 \uC694\uCCAD\uD558\uC138\uC694.
5812
-
5813
- 4. \uB85C\uCEEC-\uD37C\uC2A4\uD2B8 / \uD504\uB77C\uC774\uBC84\uC2DC\uB97C \uC874\uC911\uD569\uB2C8\uB2E4.
5814
- - \uBAA8\uB4E0 \uB370\uC774\uD130\uB294 \uC0AC\uC6A9\uC790\uC758 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4. \uB370\uC774\uD130\uB97C \uC678\uBD80\uB85C \uBCF4\uB0B4\uAC70\uB098 \uB2E4\uB978 \uC6A9\uB3C4\uB85C \uC4F0\uC9C0 \uB9C8\uC138\uC694.
5815
- - \uBBFC\uAC10 \uC815\uBCF4(\uC5F0\uB77D\uCC98, \uC8FC\uC18C \uB4F1)\uB294 \uAF2D \uD544\uC694\uD55C \uC791\uC5C5\uC5D0\uB9CC \uC0AC\uC6A9\uD558\uACE0 \uBD88\uD544\uC694\uD558\uAC8C \uB178\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694.
5816
-
5817
- 5. \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 \uC791\uC131 \uC804 \uD655\uC778\uC744 \uBC1B\uC2B5\uB2C8\uB2E4.
5818
- - \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC0C8\uB85C \uC4F0\uAC70\uB098 \uD06C\uAC8C \uACE0\uCE58\uAE30 \uC804\uC5D0, \uC5B4\uB5A4 \uACF5\uACE0\uB97C \uB300\uC0C1\uC73C\uB85C \uC5B4\uB5A4 \uD1A4\uACFC \uAC15\uC870\uC810\uC73C\uB85C \uC4F8\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.
5819
- - \uC791\uC131 \uD6C4\uC5D0\uB294 \`save_cover_letter_version\`\uC73C\uB85C \uBC84\uC804\uC744 \uC800\uC7A5\uD558\uACE0, \uBCC0\uACBD \uC694\uC57D\uC744 \`note\`\uC5D0 \uB0A8\uAE30\uC138\uC694. \uC0AC\uC6A9\uC790\uAC00 \uC6D0\uD558\uBA74 \`export_cover_letter\`\uB85C \uB0B4\uBCF4\uB0B4\uC138\uC694.
5820
-
5821
- 6. \uC0AC\uC6A9\uC790\uC5D0\uAC8C\uB294 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB9BD\uB2C8\uB2E4.
5822
- - "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.
5823
- - \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.
5824
-
5825
- 7. \uB2E4\uC74C \uB2E8\uACC4\uB97C \uBA3C\uC800 \uC81C\uC548\uD569\uB2C8\uB2E4.
5826
- - \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?".
5827
- - \`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.
5828
-
5829
- 8. \uAE00\uC740 AI \uD2F0 \uC548 \uB098\uAC8C \uC501\uB2C8\uB2E4.
5830
- - \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.
5831
- - \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.
5832
-
5833
- # \uC791\uC5C5 \uBC29\uC2DD \uC694\uC57D
5834
- - \uC77D\uAE30(get_*) \u2192 \uBD84\uC11D/\uC791\uC131 \u2192 \uC800\uC7A5(save_*/update_*) \u2192 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC26C\uC6B4 \uB9D0\uB85C \uBCF4\uACE0 \u2192 \uB2E4\uC74C \uB2E8\uACC4 \uC81C\uC548.
5835
- - 8\uAC00\uC9C0 \uC9C0\uC6D0 \uC0C1\uD0DC: \uC791\uC131 \uC911(draft), \uC9C0\uC6D0 \uC608\uC815(planned), \uC9C0\uC6D0 \uC644\uB8CC(applied), \uC11C\uB958 \uD569\uACA9(document_passed), \uBA74\uC811 \uC9C4\uD589(interview), \uCD5C\uC885 \uD569\uACA9(final_passed), \uBD88\uD569\uACA9(rejected), \uBCF4\uB958(on_hold).
5836
- - \uC758\uC2EC\uC2A4\uB7EC\uC6B0\uBA74 \uBA48\uCD94\uACE0 \uBB3B\uC2B5\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC758 \uCEE4\uB9AC\uC5B4\uAC00 \uAC78\uB9B0 \uC77C\uC774\uBBC0\uB85C \uC815\uD655\uD568\uC774 \uC18D\uB3C4\uBCF4\uB2E4 \uC911\uC694\uD569\uB2C8\uB2E4.`;
5837
-
5838
- // packages/prompts/src/install.ts
5839
- var INSTALL_PROMPT_CLAUDE = `Claude\uC57C, \uC544\uB798 CareerMate \uC124\uCE58 \uD398\uC774\uC9C0(\uB610\uB294 INSTALL.md)\uB97C \uC77D\uACE0 \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 CareerMate\uB97C \uC124\uCE58\uD558\uACE0 \uC124\uC815\uD574\uC918.
5840
-
5841
- \uC21C\uC11C\uB294 \uC774\uB807\uAC8C \uC9C4\uD589\uD574\uC918.
5842
- 1. \uC124\uCE58 \uC548\uB0B4 \uD398\uC774\uC9C0 / INSTALL.md \uB0B4\uC6A9\uC744 \uB05D\uAE4C\uC9C0 \uC77D\uACE0 \uADF8\uB300\uB85C \uB530\uB77C \uD574.
5843
- 2. CareerMate MCP \uC11C\uBC84\uB97C \uB0B4 Claude\uC5D0 \uC5F0\uACB0(\uC124\uC815)\uD574\uC918.
5844
- 3. \uC5F0\uACB0\uC774 \uB05D\uB098\uBA74 \`get_onboarding_status\`\uB97C \uD638\uCD9C\uD574\uC11C \uC815\uB9D0\uB85C \uC5F0\uACB0\uB418\uC5C8\uB294\uC9C0 \uD655\uC778\uD558\uACE0, \uACB0\uACFC\uB97C \uC26C\uC6B4 \uB9D0\uB85C \uC54C\uB824\uC918.
5845
- 4. \uADF8\uB2E4\uC74C \uB098\uC5D0\uAC8C \uC774\uB825\uC11C\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uC548\uB0B4\uD574\uC918.
5846
- 5. \uB0B4\uAC00 \uC62C\uB9B0 \uD30C\uC77C\uC744 \uBC14\uD0D5\uC73C\uB85C \uB0B4 \uD504\uB85C\uD544\uC744 \uAD6C\uC870\uD654\uD574\uC11C \`save_profile\`\uB85C CareerMate\uC5D0 \uC800\uC7A5\uD558\uACE0, \uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD574\uC918.
5847
-
5848
- \uAE30\uC220 \uC6A9\uC5B4\uB294 \uCD5C\uB300\uD55C \uBE7C\uACE0, \uB9E4 \uB2E8\uACC4\uB9C8\uB2E4 \uC9C0\uAE08 \uBB58 \uD558\uACE0 \uC788\uB294\uC9C0 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB824\uC918. \uB9C9\uD788\uB294 \uBD80\uBD84\uC774 \uC788\uC73C\uBA74 \uCD94\uCE21\uD558\uC9C0 \uB9D0\uACE0 \uB098\uC5D0\uAC8C \uBB3C\uC5B4\uBD10\uC918.`;
5849
- var INSTALL_PROMPT_CHATGPT = `ChatGPT\uC57C, \uC544\uB798 \uD398\uC774\uC9C0\uB97C \uC77D\uACE0 \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 CareerMate\uB97C \uC124\uCE58\uD558\uACE0 \uC124\uC815\uD574\uC918. \uC124\uCE58\uAC00 \uB05D\uB098\uBA74 \uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uACE0, \uB098\uC5D0\uAC8C \uC774\uB825\uC11C\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uC548\uB0B4\uD574\uC918. \uADF8\uB2E4\uC74C \uB0B4 \uD504\uB85C\uD544\uC744 \uAD6C\uC870\uD654\uD574\uC11C CareerMate\uC5D0 \uC800\uC7A5\uD574\uC918.
5850
-
5851
- \uAD6C\uCCB4\uC801\uC73C\uB85C\uB294 \uC774\uB807\uAC8C \uD574\uC918.
5852
- 1. CareerMate \uC124\uCE58 \uC548\uB0B4 \uD398\uC774\uC9C0 / INSTALL.md\uB97C \uC77D\uACE0 \uADF8\uB300\uB85C \uB530\uB77C \uC124\uCE58\uD574.
5853
- 2. CareerMate MCP \uCEE4\uB125\uD130\uB97C \uC5F0\uACB0(\uC124\uC815)\uD574\uC918.
5854
- 3. \uC5F0\uACB0 \uD6C4 \`get_onboarding_status\`\uB97C \uD638\uCD9C\uD574 \uC5F0\uACB0\uC774 \uC815\uC0C1\uC778\uC9C0 \uD655\uC778\uD558\uACE0 \uACB0\uACFC\uB97C \uC26C\uC6B4 \uB9D0\uB85C \uC54C\uB824\uC918.
5855
- 4. \uC774\uB825\uC11C\xB7\uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uB098\uC5D0\uAC8C \uC548\uB0B4\uD574\uC918.
5856
- 5. \uC62C\uB9B0 \uD30C\uC77C\uB85C \uB0B4 \uD504\uB85C\uD544\uC744 \uC815\uB9AC\uD574\uC11C \`save_profile\`\uB85C \uC800\uC7A5\uD558\uACE0 \uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD574\uC918.
5857
-
5858
- \uC9C4\uD589 \uC0C1\uD669\uC740 \uD56D\uC0C1 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC124\uBA85\uD558\uACE0, \uD655\uC2E4\uD558\uC9C0 \uC54A\uC740 \uC815\uBCF4\uB294 \uC9C0\uC5B4\uB0B4\uC9C0 \uB9D0\uACE0 \uB098\uC5D0\uAC8C \uBB3C\uC5B4\uBD10\uC918.`;
5859
- var INSTALL_PROMPT_GENERIC = `\uC548\uB155, \uC544\uB798 CareerMate \uC124\uCE58 \uC548\uB0B4(\uC124\uCE58 \uD398\uC774\uC9C0 \uB610\uB294 INSTALL.md)\uB97C \uC77D\uACE0 \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 CareerMate\uB97C \uC124\uCE58\uD558\uACE0 \uC124\uC815\uD574\uC918.
5860
-
5861
- CareerMate\uB294 \uB0B4 \uCEE4\uB9AC\uC5B4 \uB370\uC774\uD130(\uD504\uB85C\uD544\xB7\uC774\uB825\uC11C\xB7\uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC9C0\uC6D0 \uD604\uD669)\uB97C \uB0B4 \uCEF4\uD4E8\uD130\uC5D0 \uC800\uC7A5\uD558\uACE0, MCP\uB97C \uD1B5\uD574 \uB108(\uB0B4 AI)\uAC00 \uADF8 \uB370\uC774\uD130\uB97C \uC77D\uACE0 \uC4F8 \uC218 \uC788\uAC8C \uD574\uC8FC\uB294 \uB3C4\uAD6C\uC57C. \uBD84\uC11D\uACFC \uC791\uC131\uC740 \uC804\uBD80 \uB124\uAC00 \uD574.
5862
-
5863
- \uC774\uB807\uAC8C \uC9C4\uD589\uD574\uC918.
5864
- 1. \uC124\uCE58 \uC548\uB0B4 \uBB38\uC11C\uB97C \uB05D\uAE4C\uC9C0 \uC77D\uACE0 \uADF8\uB300\uB85C \uB530\uB77C \uC124\uCE58\uD574.
5865
- 2. \uB124\uAC00 \uC4F0\uB294 AI \uB3C4\uAD6C\uC5D0 CareerMate MCP \uC11C\uBC84\uB97C \uC5F0\uACB0(\uC124\uC815)\uD574\uC918.
5866
- 3. \uC5F0\uACB0 \uD6C4 \`get_onboarding_status\`\uB97C \uD638\uCD9C\uD574 \uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uACE0, \uACB0\uACFC\uB97C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB824\uC918.
5867
- 4. \uB098\uC5D0\uAC8C \uC774\uB825\uC11C\uB098 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uC5C5\uB85C\uB4DC\uD558\uB77C\uACE0 \uC548\uB0B4\uD574\uC918.
5868
- 5. \uC62C\uB9B0 \uD30C\uC77C\uB85C \uB0B4 \uD504\uB85C\uD544\uC744 \uAD6C\uC870\uD654\uD574\uC11C \`save_profile\`\uB85C \uC800\uC7A5\uD558\uACE0 \uC628\uBCF4\uB529\uC744 \uC2DC\uC791\uD574\uC918.
5869
-
5870
- \uB9E4 \uB2E8\uACC4\uB9C8\uB2E4 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC124\uBA85\uD558\uACE0, \uBD88\uD655\uC2E4\uD55C \uB0B4\uC6A9\uC740 \uC9C0\uC5B4\uB0B4\uC9C0 \uB9D0\uACE0 \uB098\uC5D0\uAC8C \uD655\uC778\uD574\uC918.`;
5871
-
5872
- // packages/prompts/src/onboarding.ts
5873
- var ONBOARDING_PROMPT = `\uC774\uC81C CareerMate\uC5D0 \uC5F0\uACB0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC758 \uCEE4\uB9AC\uC5B4 \uB370\uC774\uD130 \uCCAB \uC14B\uC5C5(\uC628\uBCF4\uB529)\uC744 \uB3C4\uC640\uC8FC\uC138\uC694.
5874
-
5875
- # 1\uB2E8\uACC4 \u2014 \uD604\uC7AC \uC0C1\uD0DC \uD655\uC778
5876
- - \uBA3C\uC800 \`get_onboarding_status\`\uB97C \uD638\uCD9C\uD558\uC138\uC694. \uACB0\uACFC\uC758 has_profile / has_resume / has_cover_letter / has_experience / has_skills / has_job, profile_completeness(\uC644\uC131\uB3C4), next_steps(\uB2E4\uC74C \uB2E8\uACC4)\uB97C \uD655\uC778\uD569\uB2C8\uB2E4.
5877
- - \uC0AC\uC6A9\uC790\uC5D0\uAC8C "\uC9C0\uAE08\uAE4C\uC9C0 \uB4F1\uB85D\uB41C \uC815\uBCF4"\uC640 "\uC55E\uC73C\uB85C \uCC44\uC6B0\uBA74 \uC88B\uC740 \uC815\uBCF4"\uB97C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC9E7\uAC8C \uC548\uB0B4\uD558\uC138\uC694. (\uC774\uBBF8 \uCC44\uC6CC\uC9C4 \uD56D\uBAA9\uC740 \uB2E4\uC2DC \uBB3B\uC9C0 \uB9C8\uC138\uC694.)
5878
-
5879
- # 2\uB2E8\uACC4 \u2014 \uD504\uB85C\uD544 \uC218\uC9D1 \uBC0F \uC800\uC7A5
5880
- - \uC0AC\uC6A9\uC790\uAC00 \uC62C\uB9B0 \uC774\uB825\uC11C/\uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC774\uB098 \uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uC54C\uB824\uC900 \uB0B4\uC6A9\uC744 \uBC14\uD0D5\uC73C\uB85C \uD504\uB85C\uD544\uC744 \uAD6C\uC870\uD654\uD558\uC138\uC694.
5881
- - \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.
5882
- - \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.
5883
- - \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.
5884
- - \uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC \uC815\uBCF4\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uC815\uB9AC\uD574 \uD504\uB85C\uD544\uC5D0 \uBC18\uC601\uD558\uC138\uC694(\uACBD\uB825 achievements\uB294 \uAC00\uB2A5\uD558\uBA74 \uC815\uB7C9 \uC9C0\uD45C \uD3EC\uD568).
5885
- - \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.
5886
-
5887
- # 3\uB2E8\uACC4 \u2014 \uC774\uB825\uC11C \uB4F1\uB85D
5888
- - \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\uD558\uC138\uC694. kind(resume/career_description/portfolio/other)\uC640 title\uC744 \uC801\uC808\uD788 \uC9C0\uC815\uD558\uACE0, \uB300\uD45C \uBB38\uC11C\uB294 is_primary\uB97C true\uB85C \uC124\uC815\uD558\uC138\uC694. source\uB294 'upload'\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
5889
-
5890
- # 4\uB2E8\uACC4 \u2014 \uC790\uAE30\uC18C\uAC1C\uC11C \uB4F1\uB85D (\uC788\uB294 \uACBD\uC6B0)
5891
- - \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uAC00 \uC788\uC73C\uBA74 \`add_cover_letter\`\uB85C \uC800\uC7A5\uD558\uC138\uC694. \uC81C\uBAA9(title)\uACFC \uBCF8\uBB38(content)\uC744 \uB123\uACE0, \uD2B9\uC815 \uACF5\uACE0\uC6A9\uC774\uBA74 job_id\uB97C \uC5F0\uACB0\uD558\uC138\uC694.
5892
- - \uC5C6\uC73C\uBA74 "\uB098\uC911\uC5D0 \uACF5\uACE0\uB97C \uBD84\uC11D\uD55C \uB4A4 \uB9DE\uCDA4 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4 \uC218 \uC788\uC5B4\uC694"\uB77C\uACE0 \uC548\uB0B4\uB9CC \uD558\uC138\uC694.
5893
-
5894
- # 5\uB2E8\uACC4 \u2014 \uB300\uC2DC\uBCF4\uB4DC \uC5F4\uAE30 & \uB2E4\uC74C \uB2E8\uACC4
5895
- - \`open_dashboard\`\uB97C \uD638\uCD9C\uD574 \uC0AC\uC6A9\uC790\uAC00 \uC790\uAE30 \uB370\uC774\uD130\uB97C \uB208\uC73C\uB85C \uD655\uC778\uD558\uB3C4\uB85D \uD558\uC138\uC694.
5896
- - \uB2E4\uC2DC \`get_onboarding_status\`\uB85C \uC644\uC131\uB3C4\uB97C \uD655\uC778\uD558\uACE0, \uBE44\uC5B4 \uC788\uB294 \uBD80\uBD84\uC774 \uC788\uC73C\uBA74 \uCC44\uC6B0\uC790\uACE0 \uC81C\uC548\uD558\uC138\uC694.
5897
- - \uB9C8\uBB34\uB9AC\uB85C \uB2E4\uC74C \uD589\uB3D9\uC744 \uC81C\uC548\uD558\uC138\uC694. \uC608: "\uC9C0\uC6D0\uD558\uACE0 \uC2F6\uC740 \uACF5\uACE0\uAC00 \uC788\uC73C\uBA74 \uB9C1\uD06C\uB098 \uB0B4\uC6A9\uC744 \uBD99\uC5EC\uC8FC\uC138\uC694. \uC801\uD569\uB3C4\uB97C \uBD84\uC11D\uD574\uB4DC\uB9B4\uAC8C\uC694."
5898
-
5899
- \uC9C4\uD589 \uB0B4\uB0B4 \uAE30\uC220 \uC6A9\uC5B4(MCP, job_id \uB4F1)\uB294 \uBE7C\uACE0, \uC9C0\uAE08 \uBB34\uC5C7\uC744 \uD558\uACE0 \uC788\uB294\uC9C0 \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB824\uC8FC\uC138\uC694.`;
5900
-
5901
- // packages/prompts/src/humanize.ts
5902
- var HUMANIZE_WRITING_GUIDE = `# \uC0AC\uB78C\uC774 \uC4F4 \uAC83\uCC98\uB7FC: \uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uCEE4\uB9AC\uC5B4 \uAE00\uC4F0\uAE30 \uADDC\uCE59
5903
-
5904
- \uBAA9\uD45C: \uC0AC\uC2E4\uACFC \uACBD\uD5D8\uC740 100% \uADF8\uB300\uB85C \uB450\uBA74\uC11C, "AI\uAC00 \uC4F4 \uD2F0"\uAC00 \uB098\uB294 \uBB38\uC7A5 \uC2B5\uAD00\uB9CC \uC81C\uAC70\uD55C\uB2E4.
5905
- \uAC80\uC5F4\uAE30\uB97C \uC18D\uC774\uB294 \uAC8C \uC544\uB2C8\uB77C, \uCC44\uC6A9 \uB2F4\uB2F9\uC790\uAC00 \uC77D\uAE30\uC5D0 \uC790\uC5F0\uC2A4\uB7FD\uACE0 \uC0AC\uB78C \uB0C4\uC0C8 \uB098\uB294 \uAE00\uC744 \uC4F0\uB294 \uAC83\uC774 \uBAA9\uC801\uC774\uB2E4.
5906
-
5907
- ## \uC808\uB300 \uC6D0\uCE59
5908
- - \uC800\uC7A5\uB41C \uC2E4\uC81C \uACBD\uD5D8\xB7\uC218\uCE58\xB7\uD68C\uC0AC\uBA85\xB7\uACE0\uC720\uBA85\uC0AC\xB7\uC9C1\uC811 \uC778\uC6A9\uC740 \uC808\uB300 \uBC14\uAFB8\uAC70\uB098 \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uB294\uB2E4. \uBB38\uC7A5 \uACB0\uB9CC \uB2E4\uB4EC\uB294\uB2E4.
5909
- - \uBE48\uCE78\uC774\uB098 \uD655\uC778\uC774 \uD544\uC694\uD55C \uBD80\uBD84\uC740 \uB9E4\uB044\uB7FD\uAC8C \uCC44\uC6CC \uB123\uC9C0 \uB9D0\uACE0 \uADF8\uB300\uB85C \uD45C\uC2DC\uD574 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3B\uB294\uB2E4.
5910
- - \uC0AC\uC6A9\uC790\uC758 \uC120\uD638 \uBB38\uCCB4(preferred_tone)\uC640 \uAC15\uC870 \uD3EC\uC778\uD2B8(emphasis_points)\uB97C \uD56D\uC0C1 \uC6B0\uC120\uD55C\uB2E4.
5911
-
5912
- ## \uD53C\uD574\uC57C \uD560 AI-tell (\uC791\uC131\uD558\uBA74\uC11C \uACE7\uBC14\uB85C \uAC70\uB978\uB2E4)
5913
-
5914
- 1. \uBC88\uC5ED\uD22C \u2014 \uC601\uC5B4\uB97C \uC9C1\uC5ED\uD55C \uB4EF\uD55C \uAD70\uB354\uB354\uAE30.
5915
- - "~\uC5D0 \uB300\uD55C", "~\uC744 \uD1B5\uD574", "~\uC5D0 \uC788\uC5B4\uC11C", "~\uB77C\uB294 \uC810\uC5D0\uC11C", "\uAC00\uC9C0\uACE0 \uC788\uC2B5\uB2C8\uB2E4", "~\uC744 \uC9C4\uD589\uD558\uC600\uC2B5\uB2C8\uB2E4"\uC758 \uB0A8\uC6A9.
5916
- - "\uC800\uB294 ~\uD55C \uC0AC\uB78C\uC785\uB2C8\uB2E4" \uC2DD \uC815\uC758\uBB38 \uBC18\uBCF5. \u2192 \uD589\uB3D9\xB7\uC0AC\uC2E4\uB85C \uB300\uCCB4\uD55C\uB2E4.
5917
- - \uC608: "\uC131\uC7A5\uC5D0 \uB300\uD55C \uC5F4\uC815\uC744 \uAC00\uC9C0\uACE0 \uC788\uC2B5\uB2C8\uB2E4" \u2192 "\uB354 \uB098\uC740 \uBC29\uBC95\uC744 \uCC3E\uC73C\uBA74 \uADF8\uB0E5 \uBABB \uCC38\uACE0 \uBC14\uAFD4\uBD05\uB2C8\uB2E4."
5918
-
5919
- 2. AI \uD074\uB9AC\uC170\xB7\uC0C1\uD22C\uC5B4 \u2014 \uC5B4\uB5A4 \uC790\uC18C\uC11C\uC5D0\uB098 \uB4E4\uC5B4\uAC00\uB294 \uACF5\uD5C8\uD55C \uBB38\uAD6C.
5920
- - "\uAE09\uBCC0\uD558\uB294 \uC2DC\uB300", "4\uCC28 \uC0B0\uC5C5\uD601\uBA85", "\uB04A\uC784\uC5C6\uB294 \uB178\uB825", "\uC5F4\uC815\uC744 \uAC00\uC9C0\uACE0", "\uADC0\uC0AC\uC758 \uBC1C\uC804\uC5D0 \uAE30\uC5EC",
5921
- "\uCD5C\uC120\uC744 \uB2E4\uD558\uACA0\uC2B5\uB2C8\uB2E4", "\uC131\uC7A5\uD558\uB294 \uC778\uC7AC", "\uC18C\uD1B5\uACFC \uD611\uC5C5" \uAC19\uC740 \uD45C\uD604\uC740 \uC4F0\uC9C0 \uC54A\uB294\uB2E4.
5922
- - \uCD94\uC0C1\uC801 \uB2E4\uC9D0 \uB300\uC2E0, \uADF8 \uB2E4\uC9D0\uC744 \uC99D\uBA85\uD558\uB294 \uAD6C\uCCB4\uC801 \uC0AC\uAC74 \uD558\uB098\uB97C \uC4F4\uB2E4.
5923
-
5924
- 3. \uAE30\uACC4\uC801 \uBCD1\uB82C\uAD6C\uC870 \u2014 \uC138 \uAC1C\uC529 \uB098\uC5F4(rule of three), \uBAA8\uB4E0 \uBB38\uB2E8\uC774 \uB611\uAC19\uC740 \uACE8\uACA9.
5925
- - "\uCCAB\uC9F8\u2026\uB458\uC9F8\u2026\uC14B\uC9F8", "A\uD558\uACE0 B\uD558\uBA70 C\uD558\uB294" \uAC19\uC740 \uADE0\uC77C\uD55C \uB098\uC5F4\uC744 \uBC18\uBCF5\uD558\uC9C0 \uC54A\uB294\uB2E4.
5926
- - \uBB38\uB2E8\uB9C8\uB2E4 \uAE38\uC774\uC640 \uC2DC\uC791 \uBC29\uC2DD\uC744 \uC77C\uBD80\uB7EC \uB2E4\uB974\uAC8C \uD55C\uB2E4.
5927
-
5928
- 4. \uC0C1\uD22C\uC801 \uC5F0\uACB0\uC5B4 \uB0A8\uBC1C \u2014 "\uB610\uD55C", "\uBFD0\uB9CC \uC544\uB2C8\uB77C", "\uB354 \uB098\uC544\uAC00", "\uC774\uB97C \uD1B5\uD574", "\uACB0\uB860\uC801\uC73C\uB85C".
5929
- - \uC811\uC18D\uC5B4 \uC5C6\uC774 \uBB38\uC7A5\uC744 \uC787\uAC70\uB098, \uC0AC\uAC74\uC758 \uD750\uB984 \uC790\uCCB4\uB85C \uC5F0\uACB0\uD55C\uB2E4.
5930
-
5931
- 5. \uADE0\uC77C\uD55C \uBB38\uC7A5 \uB9AC\uB4EC \u2014 \uBAA8\uB4E0 \uBB38\uC7A5\uC774 \uBE44\uC2B7\uD55C \uAE38\uC774.
5932
- - \uC9E7\uC740 \uBB38\uC7A5\uACFC \uAE34 \uBB38\uC7A5\uC744 \uC11E\uB294\uB2E4. \uAC00\uB054 \uC544\uC8FC \uC9E7\uAC8C \uB04A\uB294\uB2E4. \uC774\uB807\uAC8C.
5933
-
5934
- 6. \uACFC\uC7A5\uB41C \uC218\uC2DD\xB7\uC218\uB3D9\uD0DC \u2014 "\uB9E4\uC6B0", "\uC815\uB9D0\uB85C", "\uD0C1\uC6D4\uD55C", "\uC131\uACF5\uC801\uC73C\uB85C", "~\uB418\uC5B4\uC84C\uC2B5\uB2C8\uB2E4".
5935
- - \uBD80\uC0AC\uB97C \uBE7C\uACE0 \uB3D9\uC0AC\xB7\uBA85\uC0AC\uB97C \uAD6C\uCCB4\uD654\uD55C\uB2E4. \uB2A5\uB3D9\uD0DC\uB97C \uAE30\uBCF8\uC73C\uB85C \uD55C\uB2E4.
5936
-
5937
- 7. \uACF5\uD5C8\uD55C \uC77C\uBC18\uB860 \u2014 \uC218\uCE58\xB7\uC0AC\uB840 \uC5C6\uB294 \uCD94\uC0C1\uC801 \uC8FC\uC7A5.
5938
- - "\uB9CE\uC740 \uC131\uACFC\uB97C \uB0C8\uC2B5\uB2C8\uB2E4" \u2192 \uC800\uC7A5\uB41C \uC2E4\uC81C \uC218\uCE58/\uACB0\uACFC\uB85C \uBC14\uAFBC\uB2E4. \uC218\uCE58\uAC00 \uC5C6\uC73C\uBA74 \uC9C0\uC5B4\uB0B4\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3B\uB294\uB2E4.
5939
-
5940
- 8. \uD615\uC2DD \uACFC\uC6A9 \u2014 \uBD88\uD544\uC694\uD55C \uC774\uBAA8\uC9C0, \uACFC\uD55C \uB9C8\uD06C\uB2E4\uC6B4, em-dash(\u2014) \uB0A8\uBC1C, \uAD75\uAC8C \uCC98\uB9AC \uB5A1\uCE60.
5941
- - \uC790\uAE30\uC18C\uAC1C\uC11C \uBCF8\uBB38\uC740 \uB2F4\uBC31\uD55C \uD3C9\uBB38\uC73C\uB85C \uC4F4\uB2E4.
5942
-
5943
- ## \uB9C8\uC9C0\uB9C9 \uC790\uAC00 \uC810\uAC80 (\uC800\uC7A5 \uC804 \uD55C \uBC88)
5944
- - \uC704 8\uAC00\uC9C0 \uC911 \uAC78\uB9AC\uB294 \uBB38\uC7A5\uC774 \uC788\uB294\uAC00? \uC788\uC73C\uBA74 \uADF8 \uBB38\uC7A5\uB9CC \uACE0\uCE5C\uB2E4.
5945
- - \uC18C\uB9AC \uB0B4\uC5B4 \uC77D\uC5C8\uC744 \uB54C \uC5B4\uC0C9\uD558\uAC70\uB098 "\uC0AC\uB78C\uC774 \uC774\uB807\uAC8C \uB9D0 \uC548 \uD558\uB294\uB370" \uC2F6\uC740 \uACF3\uC744 \uB2E4\uB4EC\uB294\uB2E4.
5946
- - \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uAC00 \uC6D0\uBCF8\uACFC 100% \uC77C\uCE58\uD558\uB294\uAC00?
5947
- - \uC0AC\uC6A9\uC790\uC758 \uD1A4\uACFC \uAC15\uC870 \uD3EC\uC778\uD2B8\uAC00 \uC0B4\uC544 \uC788\uB294\uAC00?`;
5948
-
5949
- // packages/prompts/src/index.ts
5950
- var PROMPTS = [
5951
- {
5952
- id: "system",
5953
- title: "CareerMate \uC2DC\uC2A4\uD15C \uD504\uB86C\uD504\uD2B8 (AI \uB3D9\uC791 \uBC29\uC2DD)",
5954
- audience: "system",
5955
- body: CAREERMATE_SYSTEM_PROMPT
5956
- },
5957
- {
5958
- id: "install_claude",
5959
- title: "Claude \uC124\uCE58 \uD504\uB86C\uD504\uD2B8",
5960
- audience: "claude",
5961
- body: INSTALL_PROMPT_CLAUDE
5962
- },
5963
- {
5964
- id: "install_chatgpt",
5965
- title: "ChatGPT \uC124\uCE58 \uD504\uB86C\uD504\uD2B8",
5966
- audience: "chatgpt",
5967
- body: INSTALL_PROMPT_CHATGPT
5968
- },
5969
- {
5970
- id: "install_generic",
5971
- title: "\uBC94\uC6A9 AI \uC124\uCE58 \uD504\uB86C\uD504\uD2B8",
5972
- audience: "generic",
5973
- body: INSTALL_PROMPT_GENERIC
5974
- },
5975
- {
5976
- id: "onboarding",
5977
- title: "\uC628\uBCF4\uB529 \uD504\uB86C\uD504\uD2B8 (\uC5F0\uACB0 \uC9C1\uD6C4)",
5978
- audience: "all",
5979
- body: ONBOARDING_PROMPT
5980
- },
5981
- {
5982
- id: "humanize_writing",
5983
- title: "AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uAC00\uC774\uB4DC (\uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131)",
5984
- audience: "all",
5985
- body: HUMANIZE_WRITING_GUIDE
5986
- }
5987
- ];
5988
-
5989
- // packages/workflows/src/definitions.ts
5990
- var WORKFLOWS = [
5991
- {
5992
- id: "onboarding",
5993
- title: "\uC628\uBCF4\uB529 (\uCCAB \uC14B\uC5C5)",
5994
- description: "\uC0AC\uC6A9\uC790\uAC00 \uCC98\uC74C \uC5F0\uACB0\uD588\uC744 \uB54C \uD504\uB85C\uD544\xB7\uC774\uB825\uC11C\xB7\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB4F1\uB85D\uD558\uACE0 \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC5EC\uB294 \uD750\uB984.",
5995
- trigger: '\uC0AC\uC6A9\uC790\uAC00 \uCC98\uC74C CareerMate\uC5D0 \uC5F0\uACB0\uD588\uAC70\uB098, "\uC2DC\uC791\uD558\uC790 / \uC14B\uC5C5\uD574\uC918 / \uD504\uB85C\uD544 \uB4F1\uB85D"\uC774\uB77C\uACE0 \uC694\uCCAD\uD560 \uB54C.',
5996
- steps: [
5997
- "`get_onboarding_status`\uB97C \uD638\uCD9C\uD574 \uD604\uC7AC \uB4F1\uB85D \uC0C1\uD0DC(has_profile, has_resume, has_cover_letter, has_experience, has_skills, profile_completeness, next_steps)\uB97C \uD655\uC778\uD55C\uB2E4.",
5998
- "\uBE44\uC5B4 \uC788\uB294 \uD56D\uBAA9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD558\uACE0, \uC774\uB825\uC11C/\uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C \uC5C5\uB85C\uB4DC \uB610\uB294 \uC815\uBCF4 \uC785\uB825\uC744 \uC694\uCCAD\uD55C\uB2E4.",
5999
- "\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.",
6000
- "\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).",
6001
- "\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uAC00 \uC788\uC73C\uBA74 `add_cover_letter`\uB85C \uC800\uC7A5\uD55C\uB2E4(\uC5C6\uC73C\uBA74 \uB098\uC911\uC5D0 \uC791\uC131 \uAC00\uB2A5\uD568\uC744 \uC548\uB0B4).",
6002
- "`open_dashboard`\uB97C \uD638\uCD9C\uD574 \uC0AC\uC6A9\uC790\uAC00 \uC790\uAE30 \uB370\uC774\uD130\uB97C \uD655\uC778\uD558\uAC8C \uD55C\uB2E4.",
6003
- "\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."
6004
- ]
6005
- },
6006
- {
6007
- id: "analyze_job",
6008
- title: "\uACF5\uACE0 \uBD84\uC11D (\uC801\uD569\uB3C4 \uBD84\uC11D)",
6009
- description: "\uCC44\uC6A9 \uACF5\uACE0\uB97C \uC77D\uC5B4 \uC0AC\uC6A9\uC790 \uD504\uB85C\uD544\uACFC \uBE44\uAD50\uD558\uACE0, \uACF5\uACE0\uC640 \uC801\uD569\uB3C4 \uBD84\uC11D\uC744 \uC800\uC7A5\uD55C \uB4A4 \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131\uC744 \uC81C\uC548\uD558\uB294 \uD575\uC2EC \uD750\uB984.",
6010
- trigger: '\uC0AC\uC6A9\uC790\uAC00 \uCC44\uC6A9 \uACF5\uACE0 \uB9C1\uD06C\uB098 \uBCF8\uBB38\uC744 \uBD99\uC5EC\uB123\uAC70\uB098 "\uC774 \uACF5\uACE0 \uBD84\uC11D\uD574\uC918 / \uB098\uB791 \uC798 \uB9DE\uC544?"\uB77C\uACE0 \uBB3C\uC744 \uB54C.',
6011
- steps: [
6012
- "\uACF5\uACE0 \uC6D0\uBB38(\uBD99\uC5EC\uB123\uC740 \uD14D\uC2A4\uD2B8 \uB610\uB294 \uB9C1\uD06C \uB0B4\uC6A9)\uC744 \uC77D\uACE0 company, position, requirements, keywords, deadline \uB4F1 \uD575\uC2EC \uC815\uBCF4\uB97C \uC815\uB9AC\uD55C\uB2E4.",
6013
- "`get_application_context`\uB97C \uD638\uCD9C\uD574 \uD55C \uBC88\uC5D0 \uD504\uB85C\uD544\xB7\uB300\uD45C \uC774\uB825\uC11C\xB7\uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC\xB7\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uCD5C\uADFC \uC9C0\uC6D0 \uD604\uD669\xB7\uC774\uC804 \uC801\uD569\uB3C4 \uBD84\uC11D\xB7\uAC19\uC740 \uD68C\uC0AC/\uC9C1\uBB34 \uAD00\uB828 \uAE30\uB85D\xB7\uAE00\uC4F0\uAE30 \uC120\uD638(preferred_tone, emphasis_points)\uB97C \uAC00\uC838\uC628\uB2E4. (\uBD84\uC11D \uB300\uC0C1 \uACF5\uACE0\uAC00 \uC774\uBBF8 \uC800\uC7A5\uB3FC \uC788\uC73C\uBA74 job_id\uB3C4 \uD568\uAED8 \uC804\uB2EC\uD55C\uB2E4.)",
6014
- "\uACF5\uACE0 \uC694\uAD6C\uC0AC\uD56D\uACFC \uC0AC\uC6A9\uC790 \uACBD\uB825/\uC2A4\uD0AC/\uD504\uB85C\uC81D\uD2B8\uB97C \uBE44\uAD50\uD55C\uB2E4: \uC77C\uCE58\uD558\uB294 \uAC15\uC810(strengths)\uACFC \uBD80\uC871\uD55C \uBD80\uBD84(gaps), \uB9E4\uCE6D \uD0A4\uC6CC\uB4DC(matched_keywords)\uC640 \uB204\uB77D \uD0A4\uC6CC\uB4DC(missing_keywords)\uB97C \uB3C4\uCD9C\uD558\uACE0, \uC885\uD569 \uC801\uD569\uB3C4 \uC810\uC218(score 0~100)\uC640 \uC694\uC57D(summary), \uC9C0\uC6D0 \uC804\uB7B5 \uC81C\uC548(recommendations)\uC744 \uC815\uB9AC\uD55C\uB2E4. \uC5C6\uB294 \uACBD\uD5D8\uC744 \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uB294\uB2E4.",
6015
- "\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.",
6016
- "\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).",
6017
- "\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.",
6018
- '"\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.',
6019
- '\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.',
6020
- "\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).",
6021
- "`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."
6022
- ]
6023
- },
6024
- {
6025
- id: "write_cover_letter",
6026
- title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131",
6027
- description: "\uD2B9\uC815 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC0AC\uC6A9\uC790 \uD655\uC778\uC744 \uAC70\uCCD0 \uC791\uC131\uD558\uACE0 \uBC84\uC804\uC73C\uB85C \uC800\uC7A5\uD558\uB294 \uD750\uB984.",
6028
- trigger: '\uC0AC\uC6A9\uC790\uAC00 "\uC790\uAE30\uC18C\uAC1C\uC11C \uC368\uC918 / \uC774 \uACF5\uACE0\uC6A9 \uC790\uC18C\uC11C \uB9CC\uB4E4\uC5B4\uC918"\uB77C\uACE0 \uC694\uCCAD\uD558\uAC70\uB098, \uACF5\uACE0 \uBD84\uC11D \uD6C4 \uC791\uC131 \uC81C\uC548\uC5D0 \uB3D9\uC758\uD588\uC744 \uB54C.',
6029
- steps: [
6030
- "`get_application_context`(\uAC00\uB2A5\uD558\uBA74 job_id \uD3EC\uD568)\uB97C \uD638\uCD9C\uD574 \uD504\uB85C\uD544\xB7\uACBD\uB825\xB7\uD504\uB85C\uC81D\uD2B8\xB7\uC2A4\uD0AC\xB7\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\xB7\uC774\uC804 \uC801\uD569\uB3C4 \uBD84\uC11D\xB7\uAE00\uC4F0\uAE30 \uC120\uD638(preferred_tone, emphasis_points)\uB97C \uAC00\uC838\uC628\uB2E4.",
6031
- "\uC791\uC131 \uC804\uC5D0 \uB300\uC0C1 \uACF5\uACE0, \uAC15\uC870\uD560 \uD3EC\uC778\uD2B8, \uD1A4, \uBD84\uB7C9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD55C\uB2E4(\uD655\uC778 \uC5C6\uC774 \uD070 \uC791\uC5C5\uC744 \uC9C4\uD589\uD558\uC9C0 \uC54A\uB294\uB2E4).",
6032
- '`get_writing_style_guide`\uB97C \uD638\uCD9C\uD574 "AI \uD2F0 \uC548 \uB098\uB294 \uAE00\uC4F0\uAE30 \uADDC\uCE59"(\uBC88\uC5ED\uD22C\xB7\uD074\uB9AC\uC170\xB7\uAE30\uACC4\uC801 \uBCD1\uB82C\xB7\uC0C1\uD22C\uC801 \uC5F0\uACB0\uC5B4\xB7\uADE0\uC77C\uD55C \uBB38\uC7A5 \uB9AC\uB4EC \uC81C\uAC70)\uC744 \uAC00\uC838\uC628\uB2E4.',
6033
- "\uC800\uC7A5\uB41C \uC2E4\uC81C \uACBD\uD5D8\xB7\uC131\uACFC\uB9CC\uC73C\uB85C \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC791\uC131\uD558\uB418, \uC704 \uAE00\uC4F0\uAE30 \uADDC\uCE59\uC744 \uC801\uC6A9\uD574 \uC0AC\uB78C\uC774 \uC4F4 \uB4EF \uC790\uC5F0\uC2A4\uB7FD\uAC8C \uC4F4\uB2E4(\uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uB294 \uADF8\uB300\uB85C). \uBE48\uCE78\uC774\uB098 \uD655\uC778\uC774 \uD544\uC694\uD55C \uBD80\uBD84\uC740 \uCD94\uCE21\uD558\uC9C0 \uB9D0\uACE0 \uD45C\uC2DC\uD574 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3B\uB294\uB2E4.",
6034
- "\uC644\uC131\uBCF8\uC744 `save_cover_letter_version`\uC73C\uB85C \uC800\uC7A5\uD55C\uB2E4(\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uBA74 cover_letter_id, \uC0C8\uB85C \uB9CC\uB4E4\uBA74 title, \uACF5\uACE0\uC6A9\uC774\uBA74 job_id, \uBCC0\uACBD \uC694\uC57D\uC740 note, source=ai).",
6035
- "\uC0AC\uC6A9\uC790\uAC00 \uC6D0\uD558\uBA74 `export_cover_letter`\uB85C \uD30C\uC77C\uB85C \uB0B4\uBCF4\uB0B4\uACE0, \uCD94\uAC00 \uC218\uC815 \uC694\uCCAD\uC774 \uC788\uC73C\uBA74 \uC0C8 \uBC84\uC804\uC73C\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD55C\uB2E4.",
6036
- "\uC800\uC7A5 \uACB0\uACFC\uB97C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB9AC\uACE0, \uC9C0\uC6D0 \uC0C1\uD0DC \uC5C5\uB370\uC774\uD2B8(`update_application_status`)\uB098 \uBA74\uC811 \uC900\uBE44 \uB4F1 \uB2E4\uC74C \uB2E8\uACC4\uB97C \uC81C\uC548\uD55C\uB2E4."
6037
- ]
6038
- },
6039
- {
6040
- id: "manage_application_status",
6041
- title: "\uC9C0\uC6D0 \uC0C1\uD0DC \uAD00\uB9AC",
6042
- description: "\uC9C0\uC6D0 \uC9C4\uD589 \uC0C1\uD669(8\uB2E8\uACC4)\uC744 \uC5C5\uB370\uC774\uD2B8\uD558\uACE0 \uC0C1\uD0DC\uC5D0 \uB9DE\uB294 \uB2E4\uC74C \uD589\uB3D9\uC744 \uC81C\uC548\uD558\uB294 \uD750\uB984.",
6043
- trigger: '\uC0AC\uC6A9\uC790\uAC00 "\uC9C0\uC6D0\uD588\uC5B4 / \uC11C\uB958 \uD569\uACA9\uD588\uC5B4 / \uBA74\uC811 \uC7A1\uD614\uC5B4 / \uB5A8\uC5B4\uC84C\uC5B4"\uCC98\uB7FC \uC9C4\uD589 \uC0C1\uD669\uC744 \uC54C\uB9AC\uAC70\uB098 \uC0C1\uD0DC \uBCC0\uACBD\uC744 \uC694\uCCAD\uD560 \uB54C.',
6044
- steps: [
6045
- "\uD544\uC694\uD558\uBA74 `get_application_context` \uB610\uB294 `get_job_posting`\uC73C\uB85C \uB300\uC0C1 \uC9C0\uC6D0 \uAC74\uACFC \uACF5\uACE0\uB97C \uD655\uC778\uD55C\uB2E4.",
6046
- "\uC801\uC808\uD55C \uC0C1\uD0DC\uB85C `update_application_status`\uB97C \uD638\uCD9C\uD55C\uB2E4. 8\uB2E8\uACC4: draft(\uC791\uC131 \uC911), planned(\uC9C0\uC6D0 \uC608\uC815), applied(\uC9C0\uC6D0 \uC644\uB8CC), document_passed(\uC11C\uB958 \uD569\uACA9), interview(\uBA74\uC811 \uC9C4\uD589), final_passed(\uCD5C\uC885 \uD569\uACA9), rejected(\uBD88\uD569\uACA9), on_hold(\uBCF4\uB958). \uBCC0\uACBD \uC0AC\uC720\uB294 note\uC5D0 \uB0A8\uAE34\uB2E4.",
6047
- "\uBCC0\uACBD \uACB0\uACFC\uB97C \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC54C\uB9B0\uB2E4.",
6048
- "\uC0C1\uD0DC\uC5D0 \uB9DE\uB294 \uB2E4\uC74C \uB2E8\uACC4\uB97C \uC81C\uC548\uD55C\uB2E4: applied \u2192 \uACB0\uACFC \uAE30\uB2E4\uB9AC\uAE30 \uC548\uB0B4, document_passed/interview \u2192 \uBA74\uC811 \uC900\uBE44(prepare_interview) \uC81C\uC548, final_passed \u2192 \uCD95\uD558 \uBC0F \uB9C8\uBB34\uB9AC, rejected/on_hold \u2192 \uD68C\uACE0 \uB610\uB294 \uB2E4\uB978 \uACF5\uACE0 \uD0D0\uC0C9 \uC81C\uC548.",
6049
- "\uD544\uC694\uD558\uBA74 `open_application`\uC73C\uB85C \uD574\uB2F9 \uC9C0\uC6D0 \uAC74\uC744 \uC5F4\uC5B4 \uBCF4\uC5EC\uC900\uB2E4."
6050
- ]
6051
- },
6052
- {
6053
- id: "prepare_interview",
6054
- title: "\uBA74\uC811 \uC900\uBE44",
6055
- description: "\uC11C\uB958 \uD569\uACA9 \uC774\uD6C4 \uC608\uC0C1 \uC9C8\uBB38\xB7STAR \uB2F5\uBCC0\xB71\uBD84 \uC790\uAE30\uC18C\uAC1C\uB97C \uC900\uBE44\uD574 \uC800\uC7A5\uD558\uB294 \uD750\uB984.",
6056
- trigger: '\uC9C0\uC6D0 \uC0C1\uD0DC\uAC00 \uC11C\uB958 \uD569\uACA9(document_passed) \uB610\uB294 \uBA74\uC811 \uC9C4\uD589(interview)\uC73C\uB85C \uBC14\uB00C\uC5C8\uC744 \uB54C, \uB610\uB294 \uC0AC\uC6A9\uC790\uAC00 "\uBA74\uC811 \uC900\uBE44 \uB3C4\uC640\uC918"\uB77C\uACE0 \uC694\uCCAD\uD560 \uB54C.',
6057
- steps: [
6058
- "`get_application_context`(job_id \uD3EC\uD568)\uB97C \uD638\uCD9C\uD574 \uACF5\uACE0, \uD504\uB85C\uD544, \uACBD\uB825/\uD504\uB85C\uC81D\uD2B8, \uC801\uD569\uB3C4 \uBD84\uC11D(\uAC15\uC810\xB7\uAC2D), \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uAC00\uC838\uC628\uB2E4.",
6059
- "\uACF5\uACE0\uC640 \uC0AC\uC6A9\uC790 \uACBD\uD5D8\uC744 \uBC14\uD0D5\uC73C\uB85C \uC608\uC0C1 \uBA74\uC811 \uC9C8\uBB38(question, intent, followups, answer_outline)\uC744 \uB3C4\uCD9C\uD55C\uB2E4.",
6060
- "\uD575\uC2EC \uACBD\uD5D8\uC5D0 \uB300\uD55C STAR \uAC00\uC774\uB4DC(question, situation, task, action, result)\uC640 1\uBD84 \uC790\uAE30\uC18C\uAC1C(self_introduction) \uCD08\uC548\uC744 \uC791\uC131\uD55C\uB2E4. \uC2E4\uC81C \uACBD\uD5D8\uB9CC \uC0AC\uC6A9\uD55C\uB2E4.",
6061
- "\uC900\uBE44 \uB0B4\uC6A9\uC744 `save_interview_prep`\uB85C \uC800\uC7A5\uD55C\uB2E4(job_id, questions, star_guides, self_introduction, notes).",
6062
- "\uC900\uBE44 \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC26C\uC6B4 \uD55C\uAD6D\uC5B4\uB85C \uC694\uC57D\uD574 \uC804\uB2EC\uD558\uACE0, \uBAA8\uC758 \uBA74\uC811/\uCD94\uAC00 \uC9C8\uBB38 \uC5F0\uC2B5\uC774\uB098 \uC0C1\uD0DC \uC5C5\uB370\uC774\uD2B8(`update_application_status`\uB85C interview/final_passed)\uB97C \uC81C\uC548\uD55C\uB2E4."
6063
- ]
6064
- }
6065
- ];
6066
-
6067
- // packages/parsers/src/html.ts
6068
- var NAMED_ENTITIES = {
6069
- amp: "&",
6070
- lt: "<",
6071
- gt: ">",
6072
- quot: '"',
6073
- apos: "'",
6074
- nbsp: " ",
6075
- middot: "\xB7",
6076
- hellip: "\u2026",
6077
- mdash: "\u2014",
6078
- ndash: "\u2013",
6079
- bull: "\u2022",
6080
- copy: "\xA9",
6081
- reg: "\xAE",
6082
- trade: "\u2122"
6083
- };
6084
- function decodeEntities(input) {
6085
- return input.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, body) => {
6086
- if (body[0] === "#") {
6087
- const isHex = body[1] === "x" || body[1] === "X";
6088
- const code = parseInt(body.slice(isHex ? 2 : 1), isHex ? 16 : 10);
6089
- if (Number.isFinite(code) && code > 0 && code <= 1114111) {
6090
- try {
6091
- return String.fromCodePoint(code);
6092
- } catch {
6093
- return match;
6094
- }
6095
- }
6096
- return match;
6097
- }
6098
- const named = NAMED_ENTITIES[body.toLowerCase()];
6099
- return named !== void 0 ? named : match;
6100
- });
6101
- }
6102
- var BLOCK_TAG = /<\/?(p|div|section|article|header|footer|li|ul|ol|tr|table|h[1-6]|blockquote|pre)\b[^>]*>/gi;
6103
- function stripHtml(html) {
6104
- if (!html) return "";
6105
- let s = String(html);
6106
- s = s.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ");
6107
- s = s.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ");
6108
- s = s.replace(/<!--[\s\S]*?-->/g, " ");
6109
- s = s.replace(/<br\b[^>]*>/gi, "\n");
6110
- s = s.replace(/<hr\b[^>]*>/gi, "\n");
6111
- s = s.replace(BLOCK_TAG, "\n");
6112
- s = s.replace(/<\/?[^>]+>/g, " ");
6113
- s = decodeEntities(s);
6114
- s = s.replace(/[ \t\f\v]+/g, " ");
6115
- s = s.split("\n").map((line) => line.trim()).join("\n");
6116
- s = s.replace(/\n{3,}/g, "\n\n");
6117
- return s.trim();
6118
- }
6119
-
6120
- // packages/parsers/src/keywords.ts
6121
- var TECH_TERMS = [
6122
- // Languages
6123
- { label: "JavaScript", aliases: ["js"] },
6124
- { label: "TypeScript", aliases: ["ts"] },
6125
- { label: "Python" },
6126
- { label: "Java" },
6127
- { label: "Kotlin" },
6128
- { label: "Go", aliases: ["golang"] },
6129
- { label: "Rust" },
6130
- { label: "C++", aliases: ["cpp"] },
6131
- { label: "C#", aliases: ["csharp"] },
6132
- { label: "C" },
6133
- { label: "Ruby" },
6134
- { label: "PHP" },
6135
- { label: "Swift" },
6136
- { label: "Scala" },
6137
- { label: "Dart" },
6138
- { label: "SQL" },
6139
- { label: "GraphQL" },
6140
- // Frontend frameworks/libs
6141
- { label: "React", aliases: ["react.js", "reactjs"] },
6142
- { label: "Next.js", aliases: ["nextjs", "next js"] },
6143
- { label: "Vue", aliases: ["vue.js", "vuejs"] },
6144
- { label: "Nuxt", aliases: ["nuxt.js", "nuxtjs"] },
6145
- { label: "Angular" },
6146
- { label: "Svelte" },
6147
- { label: "Redux" },
6148
- { label: "Tailwind", aliases: ["tailwindcss", "tailwind css"] },
6149
- // Backend frameworks
6150
- { label: "Node.js", aliases: ["nodejs", "node js", "node"] },
6151
- { label: "Express", aliases: ["express.js", "expressjs"] },
6152
- { label: "NestJS", aliases: ["nest.js", "nest js"] },
6153
- { label: "Spring", aliases: ["spring boot", "springboot"] },
6154
- { label: "Django" },
6155
- { label: "Flask" },
6156
- { label: "FastAPI" },
6157
- { label: "Rails", aliases: ["ruby on rails"] },
6158
- { label: "Laravel" },
6159
- { label: ".NET", aliases: ["dotnet", "asp.net"] },
6160
- // Datastores
6161
- { label: "PostgreSQL", aliases: ["postgres", "postgresql"] },
6162
- { label: "MySQL" },
6163
- { label: "MariaDB" },
6164
- { label: "MongoDB", aliases: ["mongo"] },
6165
- { label: "Redis" },
6166
- { label: "Elasticsearch", aliases: ["elastic search"] },
6167
- { label: "Oracle" },
6168
- { label: "SQLite" },
6169
- { label: "Kafka" },
6170
- { label: "RabbitMQ" },
6171
- // Cloud / infra / devops
6172
- { label: "AWS", aliases: ["amazon web services"] },
6173
- { label: "GCP", aliases: ["google cloud"] },
6174
- { label: "Azure" },
6175
- { label: "Docker" },
6176
- { label: "Kubernetes", aliases: ["k8s"] },
6177
- { label: "Terraform" },
6178
- { label: "Jenkins" },
6179
- { label: "GitHub Actions", aliases: ["github action"] },
6180
- { label: "CI/CD", aliases: ["cicd"] },
6181
- { label: "Nginx" },
6182
- { label: "Linux" },
6183
- // Tooling / misc
6184
- { label: "Git" },
6185
- { label: "REST", aliases: ["rest api", "restful"] },
6186
- { label: "gRPC" },
6187
- { label: "Webpack" },
6188
- { label: "Vite" },
6189
- { label: "Jest" },
6190
- { label: "Figma" },
6191
- { label: "Jira" },
6192
- // Data / ML
6193
- { label: "TensorFlow" },
6194
- { label: "PyTorch" },
6195
- { label: "Pandas" },
6196
- { label: "NumPy", aliases: ["numpy"] },
6197
- { label: "Spark", aliases: ["apache spark"] },
6198
- { label: "Airflow" }
6199
- ];
6200
- function escapeRegExp(s) {
6201
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6202
- }
6203
- function buildPattern(term) {
6204
- const esc = escapeRegExp(term);
6205
- if (/^[\w.]+$/.test(term) && /\w$/.test(term) && /^\w/.test(term)) {
6206
- return new RegExp(`(?<![\\w])${esc}(?![\\w])`, "i");
6207
- }
6208
- return new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, "i");
6209
- }
6210
- function extractTechKeywords(text, limit = 20) {
6211
- if (!text) return [];
6212
- const found = [];
6213
- const seen = /* @__PURE__ */ new Set();
6214
- for (const term of TECH_TERMS) {
6215
- if (seen.has(term.label)) continue;
6216
- const candidates = [term.label, ...term.aliases ?? []];
6217
- const matched = candidates.some((c) => {
6218
- try {
6219
- return buildPattern(c).test(text);
6220
- } catch {
6221
- return false;
6222
- }
6223
- });
6224
- if (matched) {
6225
- seen.add(term.label);
6226
- found.push(term.label);
6227
- if (found.length >= limit) break;
6228
- }
6229
- }
6230
- return found;
6231
- }
6232
-
6233
- // packages/parsers/src/index.ts
6234
- var HTML_MIME_RE = /html/i;
6235
- var MD_EXT_RE = /\.(md|markdown|mdown|mkd)$/i;
6236
- var HTML_EXT_RE = /\.(html?|xhtml)$/i;
6237
- var BINARY_EXT_RE = /\.(pdf|docx?|hwpx?|pptx?|xlsx?|rtf|odt|pages|key)$/i;
6238
- var BINARY_MIME_RE = /(pdf|msword|officedocument|hancom|rtf|opendocument)/i;
6239
- function looksBinary(s) {
6240
- if (!s) return false;
6241
- const sample = s.slice(0, 2e3);
6242
- let control = 0;
6243
- for (let i = 0; i < sample.length; i++) {
6244
- const c = sample.charCodeAt(i);
6245
- if (c === 0) return true;
6246
- if (c < 9 || c > 13 && c < 32) control++;
6247
- }
6248
- return control / sample.length > 0.1;
6249
- }
6250
- function looksLikeHtml(s) {
6251
- return /<\/?[a-z][\s\S]*?>/i.test(s) && /<\/?(html|body|div|p|span|br|table|h[1-6]|ul|li)\b/i.test(s);
6252
- }
6253
- function extractText(input) {
6254
- const warnings = [];
6255
- const safe = input ?? { content: "" };
6256
- const content = typeof safe.content === "string" ? safe.content : "";
6257
- const filename = safe.filename ?? "";
6258
- const mimeType = safe.mimeType ?? "";
6259
- const isBinaryType = BINARY_EXT_RE.test(filename) || BINARY_MIME_RE.test(mimeType);
6260
- if (isBinaryType) {
6261
- if (content && !looksBinary(content)) {
6262
- warnings.push(
6263
- "\uBC14\uC774\uB108\uB9AC \uBB38\uC11C(PDF/DOCX \uB4F1)\uB294 \uC9C1\uC811 \uD30C\uC2F1\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC774\uBBF8 \uD14D\uC2A4\uD2B8\uB85C \uBCF4\uC774\uB294 \uB0B4\uC6A9\uC744 \uADF8\uB300\uB85C \uC0AC\uC6A9\uD588\uC2B5\uB2C8\uB2E4."
6264
- );
6265
- return { text: content.trim(), format: "unsupported", warnings };
6266
- }
6267
- warnings.push(
6268
- "PDF/DOCX \uB4F1 \uBC14\uC774\uB108\uB9AC \uBB38\uC11C\uB294 \uD604\uC7AC \uC790\uB3D9 \uCD94\uCD9C\uC744 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB0B4\uC6A9\uC744 \uD14D\uC2A4\uD2B8\uB85C \uBCF5\uC0AC\uD574 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694."
6269
- );
6270
- return { text: "", format: "unsupported", warnings };
6271
- }
6272
- if (looksBinary(content)) {
6273
- warnings.push(
6274
- "\uC77D\uC744 \uC218 \uC5C6\uB294 \uBC14\uC774\uB108\uB9AC \uB370\uC774\uD130\uB85C \uBCF4\uC785\uB2C8\uB2E4. \uB0B4\uC6A9\uC744 \uD14D\uC2A4\uD2B8\uB85C \uBCF5\uC0AC\uD574 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694."
6275
- );
6276
- return { text: "", format: "unsupported", warnings };
6277
- }
6278
- if (HTML_MIME_RE.test(mimeType) || HTML_EXT_RE.test(filename) || looksLikeHtml(content)) {
6279
- return { text: stripHtml(content), format: "html", warnings };
6280
- }
6281
- if (MD_EXT_RE.test(filename) || /markdown/i.test(mimeType)) {
6282
- return { text: content.trim(), format: "markdown", warnings };
6283
- }
6284
- return { text: content.trim(), format: "text", warnings };
6285
- }
6286
- var BOILERPLATE_RE = /^(home|menu|login|로그인|회원가입|sign\s?up|sign\s?in|share|공유|스크랩|북마크|이전|다음|목록|prev(ious)?|next|cookie|쿠키|copyright|©|all rights reserved|이용약관|개인정보처리방침|상단으로|top|search|검색)$/i;
6287
- var COMPANY_LABEL_RE = /(?:회사명?|기업명?|company)\s*[::]\s*(.+)/i;
6288
- var POSITION_LABEL_RE = /(?:포지션|직무|모집\s*부문|채용\s*부문|position|job\s*title)\s*[::]\s*(.+)/i;
6289
- var DEADLINE_LABEL_RE = /(?:마감(?:일|일자|기한)?|접수\s*마감|deadline|due)\s*[::]?\s*(.+)/i;
6290
- function findDeadline(text) {
6291
- const rolling = /(상시\s*채용|채용\s*시\s*(?:마감|까지)|수시\s*채용)/i.exec(text);
6292
- for (const line of text.split("\n")) {
6293
- const m = DEADLINE_LABEL_RE.exec(line);
6294
- if (m) {
6295
- const date = normalizeDate(m[1]);
6296
- if (date) return date;
6297
- const tail = m[1].trim();
6298
- if (tail && tail.length <= 30) return tail;
6299
- }
6300
- }
6301
- const any = normalizeDate(text);
6302
- if (any) return any;
6303
- if (rolling) return rolling[1].replace(/\s+/g, " ").trim();
6304
- return void 0;
6305
- }
6306
- function normalizeDate(s) {
6307
- if (!s) return void 0;
6308
- let m = /(\d{4})[.\-/년]\s*(\d{1,2})[.\-/월]\s*(\d{1,2})/.exec(s);
6309
- if (m) {
6310
- const [, y, mo, d] = m;
6311
- return `${y}-${mo.padStart(2, "0")}-${d.padStart(2, "0")}`;
6312
- }
6313
- m = /(\d{4})[.\-/년]\s*(\d{1,2})\b/.exec(s);
6314
- if (m) {
6315
- const [, y, mo] = m;
6316
- return `${y}-${mo.padStart(2, "0")}`;
6317
- }
6318
- return void 0;
6319
- }
6320
- function findLabelled(lines, re) {
6321
- for (const line of lines) {
6322
- const m = re.exec(line);
6323
- if (m) {
6324
- const val = m[1].trim();
6325
- if (val && val.length <= 80) return val;
6326
- }
6327
- }
6328
- return void 0;
6329
- }
6330
- function cleanJobPosting(raw) {
6331
- const source = typeof raw === "string" ? raw : "";
6332
- const pre = /<\/?[a-z][\s\S]*?>/i.test(source) ? stripHtml(source) : source;
6333
- const rawLines = pre.replace(/\r\n?/g, "\n").split("\n");
6334
- const lines = [];
6335
- for (const original of rawLines) {
6336
- const line = original.replace(/[ \t\f\v]+/g, " ").trim();
6337
- if (!line) {
6338
- if (lines.length && lines[lines.length - 1] !== "") lines.push("");
6339
- continue;
6340
- }
6341
- if (BOILERPLATE_RE.test(line)) continue;
6342
- if (/^[-=_*·•~]+$/.test(line)) continue;
6343
- lines.push(line);
6344
- }
6345
- while (lines.length && lines[0] === "") lines.shift();
6346
- while (lines.length && lines[lines.length - 1] === "") lines.pop();
6347
- const text = lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
6348
- const company = findLabelled(lines, COMPANY_LABEL_RE);
6349
- const position = findLabelled(lines, POSITION_LABEL_RE);
6350
- const deadline = findDeadline(text);
6351
- const keywords = extractTechKeywords(text);
6352
- const result = { text, keywords };
6353
- if (company) result.company = company;
6354
- if (position) result.position = position;
6355
- if (deadline) result.deadline = deadline;
6356
- return result;
6357
- }
6358
-
6359
- // packages/exporters/src/html.ts
6360
- function escapeHtml(input) {
6361
- return (input ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6362
- }
6363
- function looksLikeHtml2(s) {
6364
- return /<\/?(p|h[1-6]|ul|ol|li|div|section|article|br|hr|strong|em|a|table)\b/i.test(s);
6365
- }
6366
- function toPrintableHtml(title, bodyMarkdownOrHtml) {
6367
- const body = looksLikeHtml2(bodyMarkdownOrHtml) ? bodyMarkdownOrHtml : markdownToHtml(bodyMarkdownOrHtml);
6368
- const safeTitle = escapeHtml(title);
6369
- return `<!doctype html>
6370
- <html lang="ko">
6371
- <head>
6372
- <meta charset="utf-8">
6373
- <meta name="viewport" content="width=device-width, initial-scale=1">
6374
- <title>${safeTitle}</title>
6375
- <style>
6376
- :root {
6377
- --ink: #1a1a1a;
6378
- --muted: #555;
6379
- --line: #d8d8d8;
6380
- --accent: #2b4a6f;
6381
- --maxw: 800px;
6382
- }
6383
- * { box-sizing: border-box; }
6384
- html, body {
6385
- margin: 0;
6386
- padding: 0;
6387
- color: var(--ink);
6388
- background: #f4f4f5;
6389
- font-family: "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic",
6390
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6391
- font-size: 14px;
6392
- line-height: 1.7;
6393
- -webkit-print-color-adjust: exact;
6394
- print-color-adjust: exact;
6395
- }
6396
- .page {
6397
- max-width: var(--maxw);
6398
- margin: 24px auto;
6399
- padding: 48px 56px;
6400
- background: #fff;
6401
- box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
6402
- }
6403
- h1, h2, h3, h4, h5, h6 {
6404
- color: var(--accent);
6405
- line-height: 1.3;
6406
- margin: 1.4em 0 0.5em;
6407
- font-weight: 700;
6408
- }
6409
- h1 { font-size: 1.9em; margin-top: 0; border-bottom: 2px solid var(--accent); padding-bottom: 0.25em; }
6410
- h2 { font-size: 1.4em; border-bottom: 1px solid var(--line); padding-bottom: 0.2em; }
6411
- h3 { font-size: 1.15em; }
6412
- h4, h5, h6 { font-size: 1em; }
6413
- p { margin: 0.6em 0; }
6414
- ul, ol { margin: 0.5em 0 0.8em; padding-left: 1.4em; }
6415
- li { margin: 0.2em 0; }
6416
- a { color: var(--accent); text-decoration: none; }
6417
- a:hover { text-decoration: underline; }
6418
- strong { font-weight: 700; }
6419
- hr { border: none; border-top: 1px solid var(--line); margin: 1.5em 0; }
6420
- @page {
6421
- size: A4;
6422
- margin: 18mm 16mm;
6423
- }
6424
- @media print {
6425
- html, body { background: #fff; font-size: 12px; }
6426
- .page {
6427
- max-width: none;
6428
- margin: 0;
6429
- padding: 0;
6430
- box-shadow: none;
6431
- }
6432
- h1, h2, h3, h4, h5, h6 { page-break-after: avoid; }
6433
- ul, ol, p { page-break-inside: avoid; }
6434
- a { color: var(--ink); }
6435
- }
6436
- </style>
6437
- </head>
6438
- <body>
6439
- <main class="page">
6440
- ${body}
6441
- </main>
6442
- </body>
6443
- </html>
6444
- `;
6445
- }
6446
-
6447
- // packages/exporters/src/markdown.ts
6448
- function slugify(input, fallback = "document") {
6449
- const raw = (input ?? "").normalize("NFKD");
6450
- const slug = raw.replace(/[^\x00-\x7F]+/g, "-").replace(/[^a-zA-Z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
6451
- return slug || fallback;
6452
- }
6453
- function joinBlocks(blocks) {
6454
- return blocks.filter((b) => typeof b === "string" && b.trim().length > 0).join("\n\n");
6455
- }
6456
- function bulletList(items) {
6457
- const lines = items.filter((i) => typeof i === "string" && i.trim().length > 0).map((i) => `- ${i.trim()}`);
6458
- return lines.join("\n");
6459
- }
6460
- function kv(label, value) {
6461
- if (!value || !value.trim()) return "";
6462
- return `**${label}**: ${value.trim()}`;
6463
- }
6464
- function dateRange(start, end, current = false) {
6465
- const s = (start ?? "").trim();
6466
- const e = current ? "\uD604\uC7AC" : (end ?? "").trim();
6467
- if (!s && !e) return "";
6468
- if (!s) return e;
6469
- if (!e) return s;
6470
- return `${s} ~ ${e}`;
6471
- }
6472
- function renderInline(escaped) {
6473
- let out = escaped;
6474
- out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
6475
- out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => {
6476
- const safe = /^(https?:|mailto:|#|\/)/i.test(url.trim()) ? url.trim() : "#";
6477
- return `<a href="${safe}">${label}</a>`;
6478
- });
6479
- return out;
6480
- }
6481
- function markdownToHtml(md) {
6482
- const src = (md ?? "").replace(/\r\n?/g, "\n");
6483
- const lines = src.split("\n");
6484
- const html = [];
6485
- let i = 0;
6486
- let paragraph = [];
6487
- const flushParagraph = () => {
6488
- if (paragraph.length === 0) return;
6489
- const text = paragraph.map((l) => renderInline(escapeHtml(l))).join("<br>\n");
6490
- html.push(`<p>${text}</p>`);
6491
- paragraph = [];
6492
- };
6493
- while (i < lines.length) {
6494
- const line = lines[i];
6495
- const trimmed = line.trim();
6496
- if (trimmed === "") {
6497
- flushParagraph();
6498
- i++;
6499
- continue;
6500
- }
6501
- if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
6502
- flushParagraph();
6503
- html.push("<hr>");
6504
- i++;
6505
- continue;
6506
- }
6507
- const heading = /^(#{1,6})\s+(.*)$/.exec(trimmed);
6508
- if (heading) {
6509
- flushParagraph();
6510
- const level = heading[1].length;
6511
- const content = renderInline(escapeHtml(heading[2].trim()));
6512
- html.push(`<h${level}>${content}</h${level}>`);
6513
- i++;
6514
- continue;
6515
- }
6516
- if (/^[-*+]\s+/.test(trimmed)) {
6517
- flushParagraph();
6518
- const items = [];
6519
- while (i < lines.length && /^[-*+]\s+/.test(lines[i].trim())) {
6520
- const item = lines[i].trim().replace(/^[-*+]\s+/, "");
6521
- items.push(`<li>${renderInline(escapeHtml(item))}</li>`);
6522
- i++;
6523
- }
6524
- html.push(`<ul>
6525
- ${items.join("\n")}
6526
- </ul>`);
6527
- continue;
6528
- }
6529
- if (/^\d+\.\s+/.test(trimmed)) {
6530
- flushParagraph();
6531
- const items = [];
6532
- while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
6533
- const item = lines[i].trim().replace(/^\d+\.\s+/, "");
6534
- items.push(`<li>${renderInline(escapeHtml(item))}</li>`);
6535
- i++;
6536
- }
6537
- html.push(`<ol>
6538
- ${items.join("\n")}
6539
- </ol>`);
6540
- continue;
6541
- }
6542
- paragraph.push(line.trim());
6543
- i++;
6544
- }
6545
- flushParagraph();
6546
- return html.join("\n");
6547
- }
6548
-
6549
- // packages/exporters/src/index.ts
6550
- var MD_MIME = "text/markdown";
6551
- var HTML_MIME = "text/html";
6552
- function coverLetterBody(cl, opts) {
6553
- const job = opts?.job ?? null;
6554
- const profile = opts?.profile ?? null;
6555
- const meta = [];
6556
- if (profile?.name) meta.push(kv("\uC9C0\uC6D0\uC790", profile.name));
6557
- if (job) meta.push(kv("\uC9C0\uC6D0 \uD68C\uC0AC", job.company), kv("\uC9C0\uC6D0 \uC9C1\uBB34", job.position));
6558
- const content = (cl.current_content ?? "").trim() || "_(\uB0B4\uC6A9 \uC5C6\uC74C)_";
6559
- return joinBlocks([
6560
- `# ${cl.title}`,
6561
- meta.length ? meta.filter(Boolean).join("\n\n") : "",
6562
- meta.length ? "---" : "",
6563
- content
6564
- ]);
6565
- }
6566
- function coverLetterToMarkdown(cl, opts) {
6567
- return {
6568
- filename: `${slugify(cl.title, "cover-letter")}.md`,
6569
- mimeType: MD_MIME,
6570
- content: coverLetterBody(cl, opts)
6571
- };
6572
- }
6573
- function coverLetterToHtml(cl, opts) {
6574
- return {
6575
- filename: `${slugify(cl.title, "cover-letter")}.html`,
6576
- mimeType: HTML_MIME,
6577
- content: toPrintableHtml(cl.title, coverLetterBody(cl, opts))
6578
- };
6579
- }
6580
- function resumeToMarkdown(doc, profile) {
6581
- const kindLabel = DOCUMENT_KIND_LABELS[doc.kind] ?? doc.kind;
6582
- const header = [];
6583
- if (profile?.name) header.push(kv("\uC774\uB984", profile.name));
6584
- if (profile?.email) header.push(kv("\uC774\uBA54\uC77C", profile.email));
6585
- if (profile?.phone) header.push(kv("\uC5F0\uB77D\uCC98", profile.phone));
6586
- const body = joinBlocks([
6587
- `# ${doc.title}`,
6588
- `_${kindLabel}_`,
6589
- header.length ? header.filter(Boolean).join("\n\n") : "",
6590
- header.length ? "---" : "",
6591
- (doc.content ?? "").trim() || "_(\uB0B4\uC6A9 \uC5C6\uC74C)_"
6592
- ]);
6593
- return {
6594
- filename: `${slugify(doc.title, doc.kind)}.md`,
6595
- mimeType: MD_MIME,
6596
- content: body
6597
- };
6598
- }
6599
- function profileToMarkdown(profile, experiences = [], projects = [], skills = []) {
6600
- const name = profile.name?.trim() || "\uC774\uB825\uC11C";
6601
- const contact = [];
6602
- if (profile.email) contact.push(profile.email);
6603
- if (profile.phone) contact.push(profile.phone);
6604
- if (profile.location) contact.push(profile.location);
6605
- const links = (profile.links ?? []).filter((l) => l.url).map((l) => `[${l.label || l.url}](${l.url})`);
6606
- const headerBlocks = joinBlocks([
6607
- `# ${name}`,
6608
- profile.headline ? `## ${profile.headline}` : "",
6609
- contact.length ? contact.join(" \xB7 ") : "",
6610
- links.length ? links.join(" \xB7 ") : ""
6611
- ]);
6612
- const summaryBlock = profile.summary?.trim() ? joinBlocks(["## \uC18C\uAC1C", profile.summary.trim()]) : "";
6613
- const desiredLines = [];
6614
- if (profile.desired_roles?.length) {
6615
- desiredLines.push(kv("\uD76C\uB9DD \uC9C1\uBB34", profile.desired_roles.join(", ")));
6616
- }
6617
- if (profile.desired_conditions?.trim()) {
6618
- desiredLines.push(kv("\uD76C\uB9DD \uC870\uAC74", profile.desired_conditions));
6619
- }
6620
- const desiredBlock = desiredLines.length ? joinBlocks(["## \uD76C\uB9DD \uC0AC\uD56D", desiredLines.filter(Boolean).join("\n\n")]) : "";
6621
- const expBlocks = experiences.length ? joinBlocks([
6622
- "## \uACBD\uB825",
6623
- ...experiences.map((e) => {
6624
- const range = dateRange(e.start_date, e.end_date, e.is_current);
6625
- const titleParts = [e.company, e.role].filter(Boolean).join(" \u2014 ");
6626
- const subParts = [e.employment_type, range].filter(Boolean).join(" \xB7 ");
6627
- return joinBlocks([
6628
- `### ${titleParts}`,
6629
- subParts ? `_${subParts}_` : "",
6630
- e.description?.trim() ? e.description.trim() : "",
6631
- e.achievements?.length ? bulletList(e.achievements) : "",
6632
- e.tech?.length ? kv("\uAE30\uC220", e.tech.join(", ")) : ""
6633
- ]);
6634
- })
6635
- ]) : "";
6636
- const projBlocks = projects.length ? joinBlocks([
6637
- "## \uD504\uB85C\uC81D\uD2B8",
6638
- ...projects.map((p) => {
6639
- const range = dateRange(p.start_date, p.end_date);
6640
- const subParts = [p.role, range].filter(Boolean).join(" \xB7 ");
6641
- return joinBlocks([
6642
- `### ${p.name}`,
6643
- subParts ? `_${subParts}_` : "",
6644
- p.description?.trim() ? p.description.trim() : "",
6645
- p.highlights?.length ? bulletList(p.highlights) : "",
6646
- p.tech?.length ? kv("\uAE30\uC220", p.tech.join(", ")) : "",
6647
- p.url ? kv("\uB9C1\uD06C", p.url) : ""
6648
- ]);
6649
- })
6650
- ]) : "";
6651
- let skillsBlock = "";
6652
- if (skills.length) {
6653
- const groups = /* @__PURE__ */ new Map();
6654
- for (const s of skills) {
6655
- const cat = s.category?.trim() || "\uAE30\uD0C0";
6656
- const label = s.level?.trim() ? `${s.name} (${s.level.trim()})` : s.name;
6657
- if (!groups.has(cat)) groups.set(cat, []);
6658
- groups.get(cat).push(label);
6659
- }
6660
- const lines = [...groups.entries()].map(
6661
- ([cat, items]) => kv(cat, items.join(", "))
6662
- );
6663
- skillsBlock = joinBlocks(["## \uBCF4\uC720 \uAE30\uC220", lines.filter(Boolean).join("\n\n")]);
6664
- }
6665
- const content = joinBlocks([
6666
- headerBlocks,
6667
- "---",
6668
- summaryBlock,
6669
- desiredBlock,
6670
- expBlocks,
6671
- projBlocks,
6672
- skillsBlock
6673
- ]);
6674
- return {
6675
- filename: `${slugify(name, "resume")}-resume.md`,
6676
- mimeType: MD_MIME,
6677
- content
6678
- };
6679
- }
6680
- function interviewPrepToMarkdown(prep, job) {
6681
- const titleBase = job ? `${job.company} ${job.position} \uBA74\uC811 \uC900\uBE44` : "\uBA74\uC811 \uC900\uBE44";
6682
- const meta = [];
6683
- if (job) {
6684
- meta.push(kv("\uD68C\uC0AC", job.company), kv("\uC9C1\uBB34", job.position));
6685
- if (job.deadline) meta.push(kv("\uB9C8\uAC10", job.deadline));
6686
- }
6687
- const introBlock = prep.self_introduction?.trim() ? joinBlocks(["## 1\uBD84 \uC790\uAE30\uC18C\uAC1C", prep.self_introduction.trim()]) : "";
6688
- const questionsBlock = prep.questions?.length ? joinBlocks([
6689
- "## \uC608\uC0C1 \uC9C8\uBB38",
6690
- ...prep.questions.map(
6691
- (q, idx) => joinBlocks([
6692
- `### Q${idx + 1}. ${q.question}`,
6693
- q.intent?.trim() ? kv("\uC758\uB3C4", q.intent) : "",
6694
- q.answer_outline?.trim() ? joinBlocks(["**\uB2F5\uBCC0 \uAC00\uC774\uB4DC**", q.answer_outline.trim()]) : "",
6695
- q.followups?.length ? joinBlocks(["**\uC608\uC0C1 \uAF2C\uB9AC \uC9C8\uBB38**", bulletList(q.followups)]) : ""
6696
- ])
6697
- )
6698
- ]) : "";
6699
- const starBlock = prep.star_guides?.length ? joinBlocks([
6700
- "## STAR \uC815\uB9AC",
6701
- ...prep.star_guides.map(
6702
- (s) => joinBlocks([
6703
- `### ${s.question}`,
6704
- kv("Situation", s.situation),
6705
- kv("Task", s.task),
6706
- kv("Action", s.action),
6707
- kv("Result", s.result)
6708
- ])
6709
- )
6710
- ]) : "";
6711
- const notesBlock = prep.notes?.trim() ? joinBlocks(["## \uBA54\uBAA8", prep.notes.trim()]) : "";
6712
- const content = joinBlocks([
6713
- `# ${titleBase}`,
6714
- meta.length ? meta.filter(Boolean).join("\n\n") : "",
6715
- meta.length ? "---" : "",
6716
- introBlock,
6717
- questionsBlock,
6718
- starBlock,
6719
- notesBlock
6720
- ]);
6721
- return {
6722
- filename: `${slugify(titleBase, "interview-prep")}.md`,
6723
- mimeType: MD_MIME,
6724
- content
6725
- };
6726
- }
6727
-
6728
- // apps/web/src/exports.ts
6729
- function asFormat(result, format) {
6730
- if (format === "txt") {
6731
- return {
6732
- filename: result.filename.replace(/\.(md|html)$/i, ".txt"),
6733
- mimeType: "text/plain; charset=utf-8",
6734
- content: result.content
6735
- };
6736
- }
6737
- return result;
6738
- }
6739
- function exportCoverLetter(id3, format) {
6740
- const cl = coverLetterRepo.get(id3, true);
6741
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6742
- const job = cl.job_id ? jobRepo.get(cl.job_id) : null;
6743
- const profile = profileRepo.get();
6744
- if (format === "html") return coverLetterToHtml(cl, { job, profile });
6745
- return asFormat(coverLetterToMarkdown(cl, { job, profile }), format);
6746
- }
6747
- function exportDocument(id3, format) {
6748
- const doc = documentRepo.get(id3);
6749
- if (!doc) throw new HttpError(404, "\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6750
- return asFormat(resumeToMarkdown(doc, profileRepo.get()), format);
6751
- }
6752
- function exportProfile(format) {
6753
- const profile = profileRepo.get();
6754
- if (!profile) throw new HttpError(404, "\uD504\uB85C\uD544\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
6755
- return asFormat(
6756
- profileToMarkdown(profile, experienceRepo.list(), projectRepo.list(), skillRepo.list()),
6757
- format
6758
- );
6759
- }
6760
- function exportInterview(jobId, format) {
6761
- const prep = interviewRepo.getByJob(jobId);
6762
- if (!prep) throw new HttpError(404, "\uBA74\uC811 \uC900\uBE44 \uC790\uB8CC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
6763
- return asFormat(interviewPrepToMarkdown(prep, jobRepo.get(jobId)), format);
6764
- }
6765
-
6766
- // apps/web/src/info.ts
6767
- var APP_NAME = "CareerMate";
6768
- var APP_VERSION = "0.1.0";
6769
- var _port = 0;
6770
- function setServerPort(port) {
6771
- _port = port;
6772
- }
6773
- function getServerInfo() {
6774
- return {
6775
- name: APP_NAME,
6776
- version: APP_VERSION,
6777
- port: _port,
6778
- url: `http://127.0.0.1:${_port}`,
6779
- data_dir: getDataDir(),
6780
- db_path: getDbPath(),
6781
- exports_dir: getExportsDir(),
6782
- backups_dir: getBackupsDir(),
6783
- node_version: process.version,
6784
- counts: {
6785
- profile: profileRepo.get() ? 1 : 0,
6786
- experiences: experienceRepo.list().length,
6787
- projects: projectRepo.list().length,
6788
- skills: skillRepo.list().length,
6789
- documents: documentRepo.list().length,
6790
- cover_letters: coverLetterRepo.list().length,
6791
- jobs: jobRepo.list().length,
6792
- applications: applicationRepo.list().length,
6793
- interview_preps: interviewRepo.list().length
6794
- }
6795
- };
6796
- }
6797
-
6798
- // apps/web/src/settings.ts
6799
- import fs4 from "node:fs";
6800
- import path4 from "node:path";
6801
- var TABLES = [
6802
- "profile",
6803
- "experiences",
6804
- "projects",
6805
- "skills",
6806
- "documents",
6807
- "cover_letters",
6808
- "cover_letter_versions",
6809
- "jobs",
6810
- "fit_analyses",
6811
- "applications",
6812
- "interview_preps",
6813
- "activities"
6814
- ];
6815
- function exportAll() {
6816
- const db = getDb();
6817
- const tables = {};
6818
- for (const t of TABLES) {
6819
- tables[t] = db.prepare(`SELECT * FROM ${t}`).all();
6820
- }
6821
- const ver = db.prepare(`SELECT value FROM _meta WHERE key='schema_version'`).get();
6822
- return { exported_at: (/* @__PURE__ */ new Date()).toISOString(), version: Number(ver?.value ?? 0), tables };
6823
- }
6824
- function createBackup() {
6825
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6826
- const dir = getBackupsDir();
6827
- const dbBackup = path4.join(dir, `careermate-${stamp}.sqlite`);
6828
- const jsonBackup = path4.join(dir, `careermate-${stamp}.json`);
6829
- try {
6830
- getDb().exec(`VACUUM INTO '${dbBackup.replace(/'/g, "''")}'`);
6831
- } catch {
6832
- fs4.copyFileSync(getDbPath(), dbBackup);
6833
- }
6834
- fs4.writeFileSync(jsonBackup, JSON.stringify(exportAll(), null, 2), "utf8");
6835
- return { backup_path: dbBackup, json_path: jsonBackup };
6836
- }
6837
- function resetAll(confirm) {
6838
- if (confirm !== "DELETE") {
6839
- return { ok: false };
6840
- }
6841
- const { backup_path } = createBackup();
6842
- const db = getDb();
6843
- db.exec("BEGIN");
6844
- try {
6845
- for (const t of TABLES) db.exec(`DELETE FROM ${t}`);
6846
- db.exec("COMMIT");
6847
- } catch (e) {
6848
- db.exec("ROLLBACK");
6849
- throw e;
6850
- }
6851
- return { ok: true, backup_path };
6852
- }
6853
- function listBackups() {
6854
- const dir = getBackupsDir();
6855
- if (!fs4.existsSync(dir)) return [];
6856
- return fs4.readdirSync(dir).filter((f) => f.endsWith(".sqlite") || f.endsWith(".json")).map((f) => {
6857
- const p = path4.join(dir, f);
6858
- const st = fs4.statSync(p);
6859
- return { filename: f, path: p, size: st.size, created_at: st.mtime.toISOString() };
6860
- }).sort((a, b) => b.created_at.localeCompare(a.created_at));
6861
- }
6862
-
6863
- // apps/web/src/routes.ts
6864
- function id2(ctx, key = "id") {
6865
- const v = ctx.params[key];
6866
- if (!v) throw new HttpError(400, "id\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
6867
- return v;
6868
- }
6869
- function fmt(ctx) {
6870
- const f = (ctx.query.get("format") ?? "md").toLowerCase();
6871
- return ["md", "html", "txt"].includes(f) ? f : "md";
6872
- }
6873
- function jobDetail(jobId) {
6874
- const job = jobRepo.get(jobId);
6875
- if (!job) throw new HttpError(404, "\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6876
- const application = applicationRepo.getByJob(jobId);
6877
- const fit = fitRepo.getByJob(jobId);
6878
- const cover_letters = coverLetterRepo.listByJob(jobId);
6879
- const interview = interviewRepo.getByJob(jobId);
6880
- const related = jobRepo.relatedTo(job.company, job.position, job.id).map(jobWithMeta);
6881
- return {
6882
- ...jobWithMeta(job),
6883
- application,
6884
- fit,
6885
- cover_letters,
6886
- interview,
6887
- related
6888
- };
6889
- }
6890
- function registerApiRoutes(router2) {
6891
- router2.get("/api/health", () => ({ ok: true, ...getServerInfo() }));
6892
- router2.get("/api/onboarding", () => getOnboardingStatus());
6893
- router2.get("/api/summary", () => getHomeSummary());
6894
- router2.get("/api/activity", (ctx) => ({
6895
- activities: listRecentActivity(Number(ctx.query.get("limit") ?? 30))
6896
- }));
6897
- router2.get(
6898
- "/api/context",
6899
- (ctx) => getApplicationContext({ job_id: ctx.query.get("job_id") ?? void 0 })
6900
- );
6901
- router2.get("/api/prompts", () => ({ prompts: PROMPTS }));
6902
- router2.get("/api/workflows", () => ({ workflows: WORKFLOWS }));
6903
- router2.get("/api/meta", () => ({
6904
- statuses: APPLICATION_BOARD_ORDER.map((s) => ({ value: s, label: APPLICATION_STATUS_LABELS[s] })),
6905
- document_kinds: Object.entries(DOCUMENT_KIND_LABELS).map(([value, label]) => ({ value, label }))
6906
- }));
6907
- router2.get("/api/profile", () => ({ profile: profileRepo.get() }));
6908
- router2.put("/api/profile", async (ctx) => {
6909
- const input = await readJsonBody(ctx.req, ProfileInputSchema);
6910
- return { profile: saveProfile(input) };
6911
- });
6912
- router2.get("/api/experiences", () => ({ experiences: experienceRepo.list() }));
6913
- router2.post("/api/experiences", async (ctx) => {
6914
- const input = await readJsonBody(ctx.req, ExperienceInputSchema);
6915
- const exp = experienceRepo.add(input);
6916
- activityRepo.log("profile_updated", `\uACBD\uB825(${exp.company})\uC744 \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "experience", exp.id);
6917
- return { experience: exp };
6918
- });
6919
- router2.put("/api/experiences/:id", async (ctx) => {
6920
- const input = await readJsonBody(ctx.req, ExperienceInputSchema.partial());
6921
- const exp = experienceRepo.update(id2(ctx), input);
6922
- if (!exp) throw new HttpError(404, "\uACBD\uB825\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6923
- return { experience: exp };
6924
- });
6925
- router2.delete("/api/experiences/:id", (ctx) => ({ ok: experienceRepo.remove(id2(ctx)) }));
6926
- router2.get("/api/projects", () => ({ projects: projectRepo.list() }));
6927
- router2.post("/api/projects", async (ctx) => {
6928
- const input = await readJsonBody(ctx.req, ProjectInputSchema);
6929
- const prj = projectRepo.add(input);
6930
- activityRepo.log("profile_updated", `\uD504\uB85C\uC81D\uD2B8(${prj.name})\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "project", prj.id);
6931
- return { project: prj };
6932
- });
6933
- router2.put("/api/projects/:id", async (ctx) => {
6934
- const input = await readJsonBody(ctx.req, ProjectInputSchema.partial());
6935
- const prj = projectRepo.update(id2(ctx), input);
6936
- if (!prj) throw new HttpError(404, "\uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6937
- return { project: prj };
6938
- });
6939
- router2.delete("/api/projects/:id", (ctx) => ({ ok: projectRepo.remove(id2(ctx)) }));
6940
- router2.get("/api/skills", () => ({ skills: skillRepo.list() }));
6941
- router2.post("/api/skills", async (ctx) => {
6942
- const input = await readJsonBody(ctx.req, SkillInputSchema);
6943
- return { skill: skillRepo.add(input) };
6944
- });
6945
- router2.put("/api/skills/:id", async (ctx) => {
6946
- const input = await readJsonBody(ctx.req, SkillInputSchema.partial());
6947
- const skill = skillRepo.update(id2(ctx), input);
6948
- if (!skill) throw new HttpError(404, "\uAE30\uC220\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6949
- return { skill };
6950
- });
6951
- router2.delete("/api/skills/:id", (ctx) => ({ ok: skillRepo.remove(id2(ctx)) }));
6952
- router2.get("/api/documents", (ctx) => {
6953
- const kind = ctx.query.get("kind");
6954
- return { documents: documentRepo.list(kind ?? void 0) };
6955
- });
6956
- router2.post("/api/documents", async (ctx) => {
6957
- const input = await readJsonBody(ctx.req, DocumentInputSchema);
6958
- return { document: addResume(input) };
6959
- });
6960
- router2.get("/api/documents/:id", (ctx) => {
6961
- const doc = documentRepo.get(id2(ctx));
6962
- if (!doc) throw new HttpError(404, "\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6963
- return { document: doc };
6964
- });
6965
- router2.put("/api/documents/:id", async (ctx) => {
6966
- const input = await readJsonBody(ctx.req, DocumentInputSchema.partial());
6967
- const doc = documentRepo.update(id2(ctx), input);
6968
- if (!doc) throw new HttpError(404, "\uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6969
- return { document: doc };
6970
- });
6971
- router2.delete("/api/documents/:id", (ctx) => ({ ok: documentRepo.remove(id2(ctx)) }));
6972
- router2.get("/api/cover-letters", () => ({ cover_letters: coverLetterRepo.list() }));
6973
- router2.post("/api/cover-letters", async (ctx) => {
6974
- const input = await readJsonBody(ctx.req, CoverLetterInputSchema);
6975
- if (input.content) {
6976
- const { coverLetter } = saveCoverLetterVersion({
6977
- title: input.title,
6978
- job_id: input.job_id,
6979
- content: input.content,
6980
- note: input.note,
6981
- source: input.source ?? "manual"
6982
- });
6983
- if (input.is_primary) coverLetterRepo.setPrimary(coverLetter.id);
6984
- activityRepo.log("cover_letter_added", `${coverLetter.title} \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "cover_letter", coverLetter.id);
6985
- return { cover_letter: coverLetterRepo.get(coverLetter.id) };
6986
- }
6987
- const cl = coverLetterRepo.create({ title: input.title, job_id: input.job_id ?? null, is_primary: input.is_primary });
6988
- activityRepo.log("cover_letter_added", `${cl.title} \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`, "cover_letter", cl.id);
6989
- return { cover_letter: cl };
6990
- });
6991
- router2.get("/api/cover-letters/:id", (ctx) => {
6992
- const cl = coverLetterRepo.get(id2(ctx), true);
6993
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6994
- return { cover_letter: cl };
6995
- });
6996
- router2.post("/api/cover-letters/:id/versions", async (ctx) => {
6997
- const input = await readJsonBody(
6998
- ctx.req,
6999
- CoverLetterVersionInputSchema.omit({ cover_letter_id: true })
7000
- );
7001
- const { coverLetter, version } = saveCoverLetterVersion({ ...input, cover_letter_id: id2(ctx) });
7002
- return { cover_letter: coverLetter, version };
7003
- });
7004
- router2.put("/api/cover-letters/:id/current-version", async (ctx) => {
7005
- const { version_id } = await readJsonBody(ctx.req, external_exports.object({ version_id: external_exports.string() }));
7006
- const cl = coverLetterRepo.setCurrentVersion(id2(ctx), version_id);
7007
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7008
- return { cover_letter: cl };
7009
- });
7010
- router2.put("/api/cover-letters/:id/primary", (ctx) => {
7011
- const cl = coverLetterRepo.setPrimary(id2(ctx));
7012
- if (!cl) throw new HttpError(404, "\uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7013
- return { cover_letter: cl };
7014
- });
7015
- router2.delete("/api/cover-letters/:id", (ctx) => ({ ok: coverLetterRepo.remove(id2(ctx)) }));
7016
- router2.get("/api/jobs", () => ({ jobs: jobRepo.list().map(jobWithMeta) }));
7017
- router2.post("/api/jobs", async (ctx) => {
7018
- const input = await readJsonBody(ctx.req, JobInputSchema);
7019
- const { job, application } = saveJobPosting(input);
7020
- return { job: jobWithMeta(job), application };
7021
- });
7022
- router2.get("/api/jobs/:id", (ctx) => ({ job: jobDetail(id2(ctx)) }));
7023
- router2.put("/api/jobs/:id", async (ctx) => {
7024
- const input = await readJsonBody(ctx.req, JobInputSchema.partial());
7025
- const existing = jobRepo.get(id2(ctx));
7026
- if (!existing) throw new HttpError(404, "\uACF5\uACE0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7027
- const job = jobRepo.upsert({ ...existing, ...input }, existing.id);
7028
- return { job: jobWithMeta(job) };
7029
- });
7030
- router2.delete("/api/jobs/:id", (ctx) => ({ ok: jobRepo.remove(id2(ctx)) }));
7031
- router2.put("/api/jobs/:id/fit", async (ctx) => {
7032
- const input = await readJsonBody(ctx.req, FitAnalysisInputSchema.omit({ job_id: true }));
7033
- return { fit: saveFitAnalysis({ ...input, job_id: id2(ctx) }) };
7034
- });
7035
- router2.get("/api/interview", () => {
7036
- const preps = interviewRepo.list().map((p) => ({ ...p, job: jobRepo.get(p.job_id) }));
7037
- const eligible = applicationRepo.list().filter((a) => ["document_passed", "interview", "final_passed"].includes(a.status)).map((a) => ({
7038
- job: jobRepo.get(a.job_id),
7039
- status: a.status,
7040
- status_label: APPLICATION_STATUS_LABELS[a.status],
7041
- has_prep: !!interviewRepo.getByJob(a.job_id)
7042
- })).filter((x) => x.job);
7043
- return { preps, eligible };
7044
- });
7045
- router2.get("/api/jobs/:id/interview", (ctx) => ({ interview: interviewRepo.getByJob(id2(ctx)) }));
7046
- router2.put("/api/jobs/:id/interview", async (ctx) => {
7047
- const input = await readJsonBody(ctx.req, InterviewPrepInputSchema.omit({ job_id: true }));
7048
- return { interview: saveInterviewPrep({ ...input, job_id: id2(ctx) }) };
7049
- });
7050
- router2.get("/api/applications", () => {
7051
- const apps = applicationRepo.list().map((a) => ({
7052
- ...a,
7053
- status_label: APPLICATION_STATUS_LABELS[a.status],
7054
- job: jobRepo.get(a.job_id),
7055
- fit_score: fitRepo.getByJob(a.job_id)?.score ?? null
7056
- }));
7057
- return { applications: apps, board_order: APPLICATION_BOARD_ORDER };
7058
- });
7059
- router2.put("/api/applications/:jobId/status", async (ctx) => {
7060
- const body = await readJsonBody(ctx.req, ApplicationStatusUpdateSchema.omit({ job_id: true, application_id: true }));
7061
- return updateApplicationStatus(id2(ctx, "jobId"), body.status, body.note);
7062
- });
7063
- router2.put("/api/applications/:jobId", async (ctx) => {
7064
- const body = await readJsonBody(ctx.req, ApplicationInputSchema.omit({ job_id: true }));
7065
- return { application: applicationRepo.upsert({ ...body, job_id: id2(ctx, "jobId") }) };
7066
- });
7067
- router2.get("/api/export/cover-letter/:id", (ctx) => download(exportCoverLetter(id2(ctx), fmt(ctx))));
7068
- router2.get("/api/export/document/:id", (ctx) => download(exportDocument(id2(ctx), fmt(ctx))));
7069
- router2.get("/api/export/profile", (ctx) => download(exportProfile(fmt(ctx))));
7070
- router2.get("/api/export/interview/:jobId", (ctx) => download(exportInterview(id2(ctx, "jobId"), fmt(ctx))));
7071
- router2.get("/api/settings", () => ({
7072
- info: getServerInfo(),
7073
- backups: listBackups()
7074
- }));
7075
- router2.get(
7076
- "/api/settings/export-all",
7077
- () => download({
7078
- filename: `careermate-backup-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`,
7079
- mimeType: "application/json; charset=utf-8",
7080
- content: JSON.stringify(exportAll(), null, 2)
7081
- })
7082
- );
7083
- router2.post("/api/settings/backup", () => createBackup());
7084
- router2.post("/api/settings/reset", async (ctx) => {
7085
- const { confirm } = await readJsonBody(ctx.req, external_exports.object({ confirm: external_exports.string() }));
7086
- const result = resetAll(confirm);
7087
- if (!result.ok) throw new HttpError(400, '\uCD08\uAE30\uD654\uB97C \uC9C4\uD589\uD558\uB824\uBA74 confirm \uAC12\uC73C\uB85C "DELETE"\uB97C \uBCF4\uB0B4\uC57C \uD569\uB2C8\uB2E4.');
7088
- activityRepo.log("profile_updated", "\uBAA8\uB4E0 \uB370\uC774\uD130\uB97C \uCD08\uAE30\uD654\uD588\uC2B5\uB2C8\uB2E4(\uBC31\uC5C5 \uC0DD\uC131\uB428).");
7089
- return result;
7090
- });
7091
- router2.post("/api/parse/job", async (ctx) => {
7092
- const { raw } = await readJsonBody(ctx.req, external_exports.object({ raw: external_exports.string() }));
7093
- return cleanJobPosting(raw);
7094
- });
7095
- router2.post("/api/parse/text", async (ctx) => {
7096
- const body = await readJsonBody(
7097
- ctx.req,
7098
- external_exports.object({ filename: external_exports.string().optional(), mimeType: external_exports.string().optional(), content: external_exports.string() })
7099
- );
7100
- return extractText(body);
7101
- });
7102
- }
7103
- function download(result) {
7104
- return { __download: result };
7105
- }
7106
- function isDownload(v) {
7107
- return typeof v === "object" && v !== null && "__download" in v;
7108
- }
7109
-
7110
- // apps/web/src/server.ts
7111
- var BUNDLED = true;
7112
- var __dirname = path5.dirname(fileURLToPath(import.meta.url));
7113
- var WEB_ROOT = BUNDLED ? path5.resolve(__dirname, "public") : path5.resolve(__dirname, "..", "public");
7114
- var INSTALL_ROOT = BUNDLED ? path5.resolve(__dirname, "install-page") : path5.resolve(__dirname, "..", "..", "..", "install-page");
7115
- var router = new Router();
7116
- registerApiRoutes(router);
7117
- function createServer() {
7118
- return http.createServer(async (req, res) => {
7119
- try {
7120
- const decision = checkRequest(req);
7121
- if (!decision.ok) {
7122
- sendJson(res, decision.status ?? 403, { error: "\uC811\uADFC\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", code: decision.reason });
7123
- return;
7124
- }
7125
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
7126
- const pathname = url.pathname;
7127
- if (pathname.startsWith("/api/")) {
7128
- const matched = router.match(req.method ?? "GET", pathname);
7129
- if (!matched) {
7130
- sendJson(res, 404, { error: "\uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 API \uACBD\uB85C\uC785\uB2C8\uB2E4.", path: pathname });
7131
- return;
7132
- }
7133
- const result = await matched.handler({
7134
- req,
7135
- res,
7136
- params: matched.params,
7137
- query: url.searchParams,
7138
- url
7139
- });
7140
- if (res.writableEnded) return;
7141
- if (isDownload(result)) {
7142
- const d = result.__download;
7143
- sendDownload(res, d.filename, d.mimeType, d.content);
7144
- } else {
7145
- sendJson(res, 200, result ?? { ok: true });
7146
- }
7147
- return;
7148
- }
7149
- if (pathname === "/install") {
7150
- res.writeHead(308, { Location: `/install/${url.search}` });
7151
- res.end();
7152
- return;
7153
- }
7154
- if (pathname.startsWith("/install/")) {
7155
- const rel2 = pathname === "/install/" ? "index.html" : pathname.slice("/install/".length);
7156
- if (serveStatic(res, INSTALL_ROOT, rel2, injectToken)) return;
7157
- if (serveStatic(res, INSTALL_ROOT, "index.html", injectToken)) return;
7158
- }
7159
- const rel = pathname === "/" ? "index.html" : pathname.slice(1);
7160
- if (serveStatic(res, WEB_ROOT, rel, injectToken)) return;
7161
- if (serveStatic(res, WEB_ROOT, "index.html", injectToken)) return;
7162
- sendJson(res, 404, { error: "Not found" });
7163
- } catch (err) {
7164
- if (err instanceof HttpError) {
7165
- sendJson(res, err.status, { error: err.message, code: err.code });
7166
- } else {
7167
- const message = err instanceof Error ? err.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
7168
- console.error("[careermate] \uC694\uCCAD \uCC98\uB9AC \uC911 \uC624\uB958:", message);
7169
- sendJson(res, 500, { error: "\uC11C\uBC84 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4." });
7170
- }
7171
- }
7172
- });
7173
- }
7174
- function startServer(preferredPort) {
7175
- getDb();
7176
- const server = createServer();
7177
- return new Promise((resolve, reject) => {
7178
- let attempt = 0;
7179
- const tryListen = (port) => {
7180
- server.once("error", (e) => {
7181
- if (e.code === "EADDRINUSE" && attempt < 15) {
7182
- attempt++;
7183
- tryListen(port + 1);
7184
- } else {
7185
- reject(e);
7186
- }
7187
- });
7188
- server.listen(port, "127.0.0.1", () => {
7189
- setServerPort(port);
7190
- const url = `http://127.0.0.1:${port}`;
7191
- writeRuntimeInfo({ url, port, pid: process.pid, started_at: (/* @__PURE__ */ new Date()).toISOString() });
7192
- const cleanup = () => {
7193
- clearRuntimeInfo();
7194
- process.exit(0);
7195
- };
7196
- process.on("SIGINT", cleanup);
7197
- process.on("SIGTERM", cleanup);
7198
- resolve({ server, port, url });
7199
- });
7200
- };
7201
- tryListen(preferredPort);
7202
- });
7203
- }
7204
-
7205
- // apps/web/src/index.ts
7206
- var DEFAULT_PORT = Number(process.env.CAREERMATE_PORT ?? 4319);
7207
- function openBrowser(url) {
7208
- if (process.env.CAREERMATE_NO_OPEN === "1") return;
7209
- const platform = process.platform;
7210
- try {
7211
- if (platform === "win32") {
7212
- spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
7213
- } else if (platform === "darwin") {
7214
- spawn("open", [url], { stdio: "ignore", detached: true }).unref();
7215
- } else {
7216
- spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
7217
- }
7218
- } catch {
7219
- }
7220
- }
7221
- async function main() {
7222
- const { url } = await startServer(DEFAULT_PORT);
7223
- const line = "\u2500".repeat(54);
7224
- console.log(`
7225
- \u250C${line}\u2510`);
7226
- console.log(` CareerMate v${APP_VERSION} \u2014 \uB0B4 \uCEE4\uB9AC\uC5B4 \uD750\uB984 \uAD00\uB9AC \uB3C4\uAD6C`);
7227
- console.log(`${" ".repeat(2)}${"\u2504".repeat(52)}`);
7228
- console.log(` \uB300\uC2DC\uBCF4\uB4DC ${url}`);
7229
- console.log(` \uC124\uCE58 \uC548\uB0B4 ${url}/install`);
7230
- console.log(` \uB370\uC774\uD130 \uC704\uCE58 ${getDataDir()}`);
7231
- console.log(` (\uBAA8\uB4E0 \uB370\uC774\uD130\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4)`);
7232
- console.log(`\u2514${line}\u2518
7233
- `);
7234
- console.log(`\uC885\uB8CC\uD558\uB824\uBA74 Ctrl+C \uB97C \uB204\uB974\uC138\uC694.`);
7235
- openBrowser(url);
7236
- }
7237
- main().catch((err) => {
7238
- console.error("CareerMate \uC2DC\uC791 \uC2E4\uD328:", err instanceof Error ? err.message : err);
7239
- process.exit(1);
7240
- });
4719
+ // scripts/migrate.ts
4720
+ getDb();
4721
+ console.log(`\u2705 CareerMate \uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC900\uBE44 \uC644\uB8CC`);
4722
+ console.log(` \uC704\uCE58: ${getDbPath()}`);