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.
- package/README.md +22 -17
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/apps/mcp/src/index.ts +4 -3
- package/apps/web/DESIGN_GUIDE.md +2 -2
- package/apps/web/demo/demo-shim.js +536 -0
- package/apps/web/demo/demo.css +75 -0
- package/apps/web/demo/seed.js +269 -0
- package/apps/web/public/app.js +34 -2
- package/apps/web/public/icon-180.png +0 -0
- package/apps/web/public/index.html +14 -3
- package/apps/web/public/lib.js +135 -23
- package/apps/web/public/pages/applications.js +18 -9
- package/apps/web/public/pages/documents.js +66 -50
- package/apps/web/public/pages/home.js +12 -11
- package/apps/web/public/pages/interview.js +9 -12
- package/apps/web/public/pages/jobs.js +39 -36
- package/apps/web/public/pages/profile.js +66 -86
- package/apps/web/public/pages/settings.js +21 -14
- package/apps/web/public/styles.css +105 -38
- package/apps/web/src/http.ts +31 -4
- package/apps/web/src/info.ts +9 -22
- package/apps/web/src/routes.ts +19 -14
- package/apps/web/src/server.ts +34 -29
- package/apps/web/src/settings.ts +35 -17
- package/bin/careermate.mjs +8 -5
- package/dist/doctor.mjs +78908 -0
- package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +12 -0
- package/dist/install-page/.omc/state/idle-notif-cooldown.json +3 -0
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +147 -0
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +22 -0
- package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +44 -0
- package/dist/install-page/favicon.svg +1 -0
- package/dist/install-page/icon-180.png +0 -0
- package/dist/install-page/icon-512.png +0 -0
- package/dist/install-page/index.html +328 -225
- package/dist/install-page/landing.js +54 -0
- package/dist/install-page/llms-install.txt +202 -0
- package/dist/install-page/og.png +0 -0
- package/dist/install-page/shots/applications-dark.png +0 -0
- package/dist/install-page/shots/applications-light.png +0 -0
- package/dist/install-page/shots/documents-dark.png +0 -0
- package/dist/install-page/shots/documents-light.png +0 -0
- package/dist/install-page/shots/home-dark.png +0 -0
- package/dist/install-page/shots/home-light.png +0 -0
- package/dist/install-page/shots/interview-dark.png +0 -0
- package/dist/install-page/shots/interview-light.png +0 -0
- package/dist/install-page/shots/jobs-dark.png +0 -0
- package/dist/install-page/shots/jobs-light.png +0 -0
- package/dist/install-page/shots/settings-dark.png +0 -0
- package/dist/install-page/shots/settings-light.png +0 -0
- package/dist/install-page/start.html +323 -0
- package/dist/install-page/style.css +128 -4
- package/dist/mcp.mjs +74225 -2007
- package/dist/{mcpb-stage/dist/web.mjs → migrate.mjs} +142 -2660
- package/dist/pdf.worker.min.mjs +28 -0
- package/dist/public/app.js +34 -2
- package/dist/public/icon-180.png +0 -0
- package/dist/public/index.html +14 -3
- package/dist/public/lib.js +135 -23
- package/dist/public/pages/applications.js +18 -9
- package/dist/public/pages/documents.js +66 -50
- package/dist/public/pages/home.js +12 -11
- package/dist/public/pages/interview.js +9 -12
- package/dist/public/pages/jobs.js +39 -36
- package/dist/public/pages/profile.js +66 -86
- package/dist/public/pages/settings.js +21 -14
- package/dist/public/styles.css +105 -38
- package/dist/web.mjs +39950 -369
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/DECISIONS.md +1 -1
- package/docs/DEMO.md +6 -6
- package/docs/MCP_TOOLS.md +68 -9
- package/docs/ROADMAP.md +11 -10
- package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
- package/docs/START_WORKFLOW.md +19 -7
- package/docs/SUPPORTED_AI_APPS.md +85 -33
- package/docs/TODO.md +1 -1
- package/docs/WORKFLOWS.md +8 -6
- package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +64 -0
- package/package.json +8 -3
- package/packages/core/src/services.ts +42 -1
- package/packages/db/src/connection.ts +30 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/paths.ts +11 -0
- package/packages/db/src/repositories.ts +109 -63
- package/packages/db/src/schema.ts +118 -0
- package/packages/exporters/src/html.ts +9 -12
- package/packages/mcp-tools/src/bridge.ts +55 -32
- package/packages/mcp-tools/src/index.ts +9 -0
- package/packages/mcp-tools/src/tools.ts +269 -65
- package/packages/mcp-tools/src/update.ts +330 -0
- package/packages/parsers/src/documents.ts +521 -0
- package/packages/parsers/src/index.ts +8 -5
- package/packages/prompts/src/onboarding.ts +2 -2
- package/packages/shared/src/build-info.ts +22 -0
- package/packages/shared/src/enums.ts +39 -0
- package/packages/shared/src/index.ts +1 -0
- package/packages/shared/src/schemas.ts +24 -3
- package/packages/workflows/src/definitions.ts +4 -2
- package/scripts/build-demo.mjs +130 -0
- package/scripts/build-dist.mjs +46 -8
- package/scripts/build-mcpb.mjs +60 -4
- package/scripts/doctor.ts +16 -3
- package/scripts/gen-og.mjs +86 -0
- package/scripts/init.ts +32 -3
- package/scripts/seed.ts +5 -5
- package/scripts/shots-seed.ts +192 -0
- package/scripts/shots.ts +89 -0
- package/tsconfig.json +3 -1
- package/dist/careermate.mcpb +0 -0
- package/dist/mcp-smoke.err +0 -3
- package/dist/mcpb-stage/README.md +0 -219
- package/dist/mcpb-stage/dist/install-page/index.html +0 -434
- package/dist/mcpb-stage/dist/install-page/style.css +0 -407
- package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
- package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
- package/dist/mcpb-stage/dist/public/app.js +0 -118
- package/dist/mcpb-stage/dist/public/index.html +0 -41
- package/dist/mcpb-stage/dist/public/lib.js +0 -282
- package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
- package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
- package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
- package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
- package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
- package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
- package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
- package/dist/mcpb-stage/dist/public/styles.css +0 -420
- package/dist/mcpb-stage/manifest.json +0 -40
- package/docs/INSTALL.md +0 -148
- package/docs/INSTALL_AND_USAGE.md +0 -99
- package/install-page/index.html +0 -474
- package/install-page/style.css +0 -391
- package/install-page/vercel.json +0 -20
- /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:
|
|
819
|
-
const fullPath = [...
|
|
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,
|
|
971
|
+
constructor(parent, value, path2, key) {
|
|
936
972
|
this._cachedPath = [];
|
|
937
973
|
this.parent = parent;
|
|
938
974
|
this.data = value;
|
|
939
|
-
this._path =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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()}`);
|