careermate 0.0.1
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/AGENTS.md +33 -0
- package/INSTALL.md +230 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/THIRD_PARTY_NOTICES.md +57 -0
- package/bin/careermate.mjs +77 -0
- package/dist/career-os/README.md +78 -0
- package/dist/career-os/eop/cover-letter.md +46 -0
- package/dist/career-os/eop/fit-analysis.md +48 -0
- package/dist/career-os/eop/interview-prep.md +39 -0
- package/dist/career-os/eop/job-analysis.md +42 -0
- package/dist/career-os/eop/profile-extraction.md +37 -0
- package/dist/career-os/knowledge/ats.md +131 -0
- package/dist/career-os/knowledge/company-research.md +137 -0
- package/dist/career-os/knowledge/cover-letter.md +135 -0
- package/dist/career-os/knowledge/fit-matching.md +121 -0
- package/dist/career-os/knowledge/human-writing.md +118 -0
- package/dist/career-os/knowledge/interview-behavioral.md +132 -0
- package/dist/career-os/knowledge/interview-technical.md +126 -0
- package/dist/career-os/knowledge/linkedin-profile.md +152 -0
- package/dist/career-os/knowledge/networking-referrals.md +166 -0
- package/dist/career-os/knowledge/offer-evaluation-decision.md +121 -0
- package/dist/career-os/knowledge/onboarding-first-90-days.md +126 -0
- package/dist/career-os/knowledge/portfolio.md +121 -0
- package/dist/career-os/knowledge/recruiter-screen.md +105 -0
- package/dist/career-os/knowledge/rejection-triage-iteration.md +115 -0
- package/dist/career-os/knowledge/resume.md +123 -0
- package/dist/career-os/knowledge/salary-negotiation.md +123 -0
- package/dist/career-os/knowledge/verifiers/ats-compat.md +64 -0
- package/dist/career-os/knowledge/verifiers/consistency.md +61 -0
- package/dist/career-os/knowledge/verifiers/human-voice.md +82 -0
- package/dist/career-os/knowledge/verifiers/recency-staleness.md +72 -0
- package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
- package/dist/career-os/knowledge/verifiers/truthfulness.md +67 -0
- package/dist/doctor.mjs +79094 -0
- package/dist/mcp.mjs +96108 -0
- package/dist/migrate.mjs +4735 -0
- package/dist/pdf.worker.min.mjs +28 -0
- package/dist/public/app.js +150 -0
- package/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/public/icon-180.png +0 -0
- package/dist/public/index.html +52 -0
- package/dist/public/lib.js +394 -0
- package/dist/public/pages/applications.js +107 -0
- package/dist/public/pages/documents.js +467 -0
- package/dist/public/pages/home.js +261 -0
- package/dist/public/pages/interview.js +227 -0
- package/dist/public/pages/jobs.js +493 -0
- package/dist/public/pages/profile.js +556 -0
- package/dist/public/pages/settings.js +240 -0
- package/dist/public/styles.css +498 -0
- package/dist/site/analytics.js +97 -0
- package/dist/site/favicon.svg +1 -0
- package/dist/site/fonts/PretendardVariable.woff2 +0 -0
- package/dist/site/icon-180.png +0 -0
- package/dist/site/icon-512.png +0 -0
- package/dist/site/index.html +598 -0
- package/dist/site/landing.js +54 -0
- package/dist/site/llms-install.txt +217 -0
- package/dist/site/robots.txt +4 -0
- package/dist/site/sitemap.xml +21 -0
- package/dist/site/start.html +326 -0
- package/dist/site/style.css +550 -0
- package/dist/site/vercel.json +20 -0
- package/dist/web.mjs +46852 -0
- package/docs/FAQ.md +101 -0
- package/docs/MCP_TOOLS.md +311 -0
- package/docs/README.md +28 -0
- package/docs/START_WORKFLOW.md +137 -0
- package/docs/SUPPORTED_AI_APPS.md +112 -0
- package/docs/WORKFLOWS.md +202 -0
- package/docs/career-os/README.md +78 -0
- package/docs/career-os/eop/cover-letter.md +46 -0
- package/docs/career-os/eop/fit-analysis.md +48 -0
- package/docs/career-os/eop/interview-prep.md +39 -0
- package/docs/career-os/eop/job-analysis.md +42 -0
- package/docs/career-os/eop/profile-extraction.md +37 -0
- package/docs/career-os/knowledge/ats.md +131 -0
- package/docs/career-os/knowledge/company-research.md +137 -0
- package/docs/career-os/knowledge/cover-letter.md +135 -0
- package/docs/career-os/knowledge/fit-matching.md +121 -0
- package/docs/career-os/knowledge/human-writing.md +118 -0
- package/docs/career-os/knowledge/interview-behavioral.md +132 -0
- package/docs/career-os/knowledge/interview-technical.md +126 -0
- package/docs/career-os/knowledge/linkedin-profile.md +152 -0
- package/docs/career-os/knowledge/networking-referrals.md +166 -0
- package/docs/career-os/knowledge/offer-evaluation-decision.md +121 -0
- package/docs/career-os/knowledge/onboarding-first-90-days.md +126 -0
- package/docs/career-os/knowledge/portfolio.md +121 -0
- package/docs/career-os/knowledge/recruiter-screen.md +105 -0
- package/docs/career-os/knowledge/rejection-triage-iteration.md +115 -0
- package/docs/career-os/knowledge/resume.md +123 -0
- package/docs/career-os/knowledge/salary-negotiation.md +123 -0
- package/docs/career-os/knowledge/verifiers/ats-compat.md +64 -0
- package/docs/career-os/knowledge/verifiers/consistency.md +61 -0
- package/docs/career-os/knowledge/verifiers/human-voice.md +82 -0
- package/docs/career-os/knowledge/verifiers/recency-staleness.md +72 -0
- package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
- package/docs/career-os/knowledge/verifiers/truthfulness.md +67 -0
- package/package.json +89 -0
- package/packages/core/src/context.ts +74 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/onboarding.ts +81 -0
- package/packages/core/src/services.ts +187 -0
- package/packages/core/src/summary.ts +104 -0
- package/packages/db/src/connection.ts +76 -0
- package/packages/db/src/index.ts +23 -0
- package/packages/db/src/paths.ts +52 -0
- package/packages/db/src/repositories.ts +891 -0
- package/packages/db/src/runtime.ts +58 -0
- package/packages/db/src/schema.ts +307 -0
- package/packages/exporters/src/html.ts +110 -0
- package/packages/exporters/src/index.ts +364 -0
- package/packages/exporters/src/markdown.ts +178 -0
- package/packages/knowledge/src/index.ts +168 -0
- package/packages/mcp-tools/src/bridge.ts +106 -0
- package/packages/mcp-tools/src/index.ts +17 -0
- package/packages/mcp-tools/src/result.ts +49 -0
- package/packages/mcp-tools/src/tools.ts +747 -0
- package/packages/mcp-tools/src/update.ts +330 -0
- package/packages/parsers/src/documents.ts +533 -0
- package/packages/parsers/src/html.ts +86 -0
- package/packages/parsers/src/index.ts +231 -0
- package/packages/parsers/src/keywords.ts +151 -0
- package/packages/prompts/src/humanize.ts +59 -0
- package/packages/prompts/src/index.ts +82 -0
- package/packages/prompts/src/install.ts +43 -0
- package/packages/prompts/src/onboarding.ts +35 -0
- package/packages/prompts/src/system.ts +57 -0
- package/packages/shared/src/build-info.ts +22 -0
- package/packages/shared/src/enums.ts +142 -0
- package/packages/shared/src/index.ts +19 -0
- package/packages/shared/src/schemas.ts +453 -0
- package/packages/workflows/src/definitions.ts +109 -0
- package/packages/workflows/src/index.ts +39 -0
- package/scripts/init.ts +371 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time constants injected by esbuild's `define` in the dist bundle
|
|
3
|
+
* (scripts/build-dist.mjs): `__APP_VERSION__` = package.json version, and
|
|
4
|
+
* `__BUNDLED__` = true inside the bundle. Under tsx/source runs they're
|
|
5
|
+
* undefined, so the `typeof` guards fall back to dev defaults.
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth so this idiom isn't copy-pasted across every
|
|
8
|
+
* entrypoint (mcp/web/doctor) and package.
|
|
9
|
+
*
|
|
10
|
+
* MUST stay dependency-free and side-effect-free: @careermate/shared re-exports
|
|
11
|
+
* it, so every shared consumer evaluates this module at import time. The
|
|
12
|
+
* injected tokens are only ever read inside `typeof` guards (never bare), so
|
|
13
|
+
* evaluation can't throw even where the define isn't applied.
|
|
14
|
+
*/
|
|
15
|
+
declare const __APP_VERSION__: string | undefined;
|
|
16
|
+
declare const __BUNDLED__: boolean | undefined;
|
|
17
|
+
|
|
18
|
+
/** App version: package.json version in the bundle, `0.0.0-dev` under source/tsx. */
|
|
19
|
+
export const APP_VERSION: string = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0-dev';
|
|
20
|
+
|
|
21
|
+
/** True only when running from the esbuild dist bundle (vs tsx source). */
|
|
22
|
+
export const BUNDLED: boolean = typeof __BUNDLED__ !== 'undefined' && __BUNDLED__;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CareerMate — shared enums and human-facing labels.
|
|
3
|
+
*
|
|
4
|
+
* These value sets are the single source of truth for both the MCP server
|
|
5
|
+
* (used by ChatGPT/Claude/Gemini) and the local web dashboard. Labels are in
|
|
6
|
+
* Korean because the primary audience is Korean job seekers; codes are stable
|
|
7
|
+
* machine values that must never change once persisted.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Application lifecycle. Drives the Applications board in the dashboard. */
|
|
11
|
+
export const APPLICATION_STATUSES = [
|
|
12
|
+
'draft', // 작성 중
|
|
13
|
+
'planned', // 지원 예정
|
|
14
|
+
'applied', // 지원 완료
|
|
15
|
+
'document_passed', // 서류 합격
|
|
16
|
+
'interview', // 면접 진행
|
|
17
|
+
'final_passed', // 최종 합격
|
|
18
|
+
'rejected', // 불합격
|
|
19
|
+
'on_hold', // 보류
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type ApplicationStatus = (typeof APPLICATION_STATUSES)[number];
|
|
23
|
+
|
|
24
|
+
export const APPLICATION_STATUS_LABELS: Record<ApplicationStatus, string> = {
|
|
25
|
+
draft: '작성 중',
|
|
26
|
+
planned: '지원 예정',
|
|
27
|
+
applied: '지원 완료',
|
|
28
|
+
document_passed: '서류 합격',
|
|
29
|
+
interview: '면접 진행',
|
|
30
|
+
final_passed: '최종 합격',
|
|
31
|
+
rejected: '불합격',
|
|
32
|
+
on_hold: '보류',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Ordered columns for the kanban-style Applications board. */
|
|
36
|
+
export const APPLICATION_BOARD_ORDER: ApplicationStatus[] = [
|
|
37
|
+
'draft',
|
|
38
|
+
'planned',
|
|
39
|
+
'applied',
|
|
40
|
+
'document_passed',
|
|
41
|
+
'interview',
|
|
42
|
+
'final_passed',
|
|
43
|
+
'on_hold',
|
|
44
|
+
'rejected',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Reaching this status unlocks interview-prep prompting in the workflow. */
|
|
48
|
+
export const INTERVIEW_UNLOCK_STATUSES: ApplicationStatus[] = [
|
|
49
|
+
'document_passed',
|
|
50
|
+
'interview',
|
|
51
|
+
'final_passed',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Allowed application-status transitions (server-enforced lifecycle).
|
|
56
|
+
*
|
|
57
|
+
* Design note: CareerMate lets the user's AI record where an application *really*
|
|
58
|
+
* is, so `draft` (the initial, untracked state) may jump to ANY real-world stage.
|
|
59
|
+
* Once an application is being tracked, it may move forward, be corrected
|
|
60
|
+
* backward among active stages, be parked (`on_hold`) or closed (`rejected`) from
|
|
61
|
+
* anywhere, and be reopened/resumed from those terminal states. The one move we
|
|
62
|
+
* forbid is regressing an *active* application back to `draft` ("untracked"),
|
|
63
|
+
* which would silently drop its lifecycle history. The hard spec invariant
|
|
64
|
+
* (interview prep requires `document_passed`+) is enforced separately at the
|
|
65
|
+
* service layer via {@link INTERVIEW_UNLOCK_STATUSES}.
|
|
66
|
+
*/
|
|
67
|
+
export const ALLOWED_STATUS_TRANSITIONS: Record<ApplicationStatus, ApplicationStatus[]> = {
|
|
68
|
+
draft: ['planned', 'applied', 'document_passed', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
69
|
+
planned: ['applied', 'document_passed', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
70
|
+
applied: ['planned', 'document_passed', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
71
|
+
document_passed: ['planned', 'applied', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
72
|
+
interview: ['planned', 'applied', 'document_passed', 'final_passed', 'rejected', 'on_hold'],
|
|
73
|
+
final_passed: ['document_passed', 'interview', 'rejected', 'on_hold'],
|
|
74
|
+
rejected: ['draft', 'planned', 'applied', 'document_passed', 'interview', 'final_passed', 'on_hold'],
|
|
75
|
+
on_hold: ['draft', 'planned', 'applied', 'document_passed', 'interview', 'final_passed', 'rejected'],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Whether moving from `from` to `to` is a permitted status transition (same→same is always allowed). */
|
|
79
|
+
export function canTransitionStatus(from: ApplicationStatus, to: ApplicationStatus): boolean {
|
|
80
|
+
if (from === to) return true;
|
|
81
|
+
return ALLOWED_STATUS_TRANSITIONS[from]?.includes(to) ?? false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Throw a clear, user-facing (Korean) error when a status transition is not allowed. */
|
|
85
|
+
export function assertStatusTransition(from: ApplicationStatus, to: ApplicationStatus): void {
|
|
86
|
+
if (!canTransitionStatus(from, to)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`'${APPLICATION_STATUS_LABELS[from]}'에서 '${APPLICATION_STATUS_LABELS[to]}'(으)로는 상태를 바꿀 수 없습니다.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Stored document kinds (resumes and career descriptions). Cover letters live in their own tables. */
|
|
94
|
+
export const DOCUMENT_KINDS = ['resume', 'career_description', 'portfolio', 'other'] as const;
|
|
95
|
+
export type DocumentKind = (typeof DOCUMENT_KINDS)[number];
|
|
96
|
+
|
|
97
|
+
export const DOCUMENT_KIND_LABELS: Record<DocumentKind, string> = {
|
|
98
|
+
resume: '이력서',
|
|
99
|
+
career_description: '경력기술서',
|
|
100
|
+
portfolio: '포트폴리오',
|
|
101
|
+
other: '기타 문서',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Where a piece of content originated — used to show provenance and protect privacy. */
|
|
105
|
+
export const CONTENT_SOURCES = ['manual', 'upload', 'ai', 'edit'] as const;
|
|
106
|
+
export type ContentSource = (typeof CONTENT_SOURCES)[number];
|
|
107
|
+
|
|
108
|
+
export const CONTENT_SOURCE_LABELS: Record<ContentSource, string> = {
|
|
109
|
+
manual: '직접 입력',
|
|
110
|
+
upload: '파일 업로드',
|
|
111
|
+
ai: 'AI 생성',
|
|
112
|
+
edit: '직접 수정',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Activity feed entry types (list_recent_activity). */
|
|
116
|
+
export const ACTIVITY_TYPES = [
|
|
117
|
+
'profile_updated',
|
|
118
|
+
'resume_added',
|
|
119
|
+
'cover_letter_added',
|
|
120
|
+
'cover_letter_version_saved',
|
|
121
|
+
'job_saved',
|
|
122
|
+
'fit_analysis_saved',
|
|
123
|
+
'application_status_changed',
|
|
124
|
+
'interview_prep_saved',
|
|
125
|
+
'document_exported',
|
|
126
|
+
] as const;
|
|
127
|
+
export type ActivityType = (typeof ACTIVITY_TYPES)[number];
|
|
128
|
+
|
|
129
|
+
/** Entity kinds referenced by activities and deep links. */
|
|
130
|
+
export const ENTITY_TYPES = [
|
|
131
|
+
'profile',
|
|
132
|
+
'experience',
|
|
133
|
+
'project',
|
|
134
|
+
'skill',
|
|
135
|
+
'document',
|
|
136
|
+
'cover_letter',
|
|
137
|
+
'job',
|
|
138
|
+
'application',
|
|
139
|
+
'fit_analysis',
|
|
140
|
+
'interview_prep',
|
|
141
|
+
] as const;
|
|
142
|
+
export type EntityType = (typeof ENTITY_TYPES)[number];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @careermate/shared — single source of truth for types, enums, and zod schemas
|
|
3
|
+
* shared by the DB layer, core use-cases, HTTP API, and MCP server.
|
|
4
|
+
*/
|
|
5
|
+
export * from './enums.ts';
|
|
6
|
+
export * from './schemas.ts';
|
|
7
|
+
export { APP_VERSION, BUNDLED } from './build-info.ts';
|
|
8
|
+
|
|
9
|
+
/** Short, URL-safe, sortable-ish id. Avoids a uuid dependency. */
|
|
10
|
+
export function newId(prefix = ''): string {
|
|
11
|
+
const rnd = Math.random().toString(36).slice(2, 10);
|
|
12
|
+
const time = Date.now().toString(36);
|
|
13
|
+
return `${prefix}${time}${rnd}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Current timestamp as ISO-8601 string (stored verbatim in SQLite). */
|
|
17
|
+
export function now(): string {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CareerMate — zod schemas + inferred TypeScript types.
|
|
3
|
+
*
|
|
4
|
+
* Two layers per entity:
|
|
5
|
+
* - `*Input` : what the MCP tools / HTTP API accept from the AI or dashboard.
|
|
6
|
+
* - `*Record` : the persisted shape returned from the DB (adds id + timestamps).
|
|
7
|
+
*
|
|
8
|
+
* JSON-ish nested fields (achievements, tech, strengths…) are stored as JSON
|
|
9
|
+
* text columns in SQLite and parsed back into arrays/objects by the repositories.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import {
|
|
13
|
+
APPLICATION_STATUSES,
|
|
14
|
+
ACTIVITY_TYPES,
|
|
15
|
+
CONTENT_SOURCES,
|
|
16
|
+
DOCUMENT_KINDS,
|
|
17
|
+
ENTITY_TYPES,
|
|
18
|
+
} from './enums.ts';
|
|
19
|
+
|
|
20
|
+
const isoDateTime = z.string();
|
|
21
|
+
const id = z.string();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reject script-capable URL schemes (javascript:, data:, vbscript:, file:, …)
|
|
25
|
+
* while allowing http(s), mailto, and scheme-less/relative links. Links are
|
|
26
|
+
* rendered as clickable anchors in the dashboard, so a `javascript:` URL stored
|
|
27
|
+
* here would otherwise execute when clicked (DOM-XSS). The dashboard also
|
|
28
|
+
* sanitizes href/src at render time (lib.js `safeUrl`); this is the write-time
|
|
29
|
+
* half of that defense-in-depth.
|
|
30
|
+
*/
|
|
31
|
+
function isSafeUrl(u: string): boolean {
|
|
32
|
+
const s = u.trim();
|
|
33
|
+
// An explicit scheme (text before the first ':') must be http(s) or mailto.
|
|
34
|
+
// Scheme-less values (relative paths, bare domains, #anchors) cannot execute.
|
|
35
|
+
return !/^[a-z][a-z0-9+.-]*:/i.test(s) || /^(https?:|mailto:)/i.test(s);
|
|
36
|
+
}
|
|
37
|
+
const UNSAFE_URL_MSG = 'javascript: 같은 위험한 링크는 사용할 수 없습니다. http(s) 링크를 사용하세요.';
|
|
38
|
+
const MAX_URL = 2_000;
|
|
39
|
+
const SafeUrl = z.string().min(1).max(MAX_URL).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
|
|
40
|
+
const SafeUrlOptional = z
|
|
41
|
+
.string()
|
|
42
|
+
.max(MAX_URL)
|
|
43
|
+
.optional()
|
|
44
|
+
.refine((u) => u == null || u === '' || isSafeUrl(u), { message: UNSAFE_URL_MSG });
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Length-bounded string helpers. MCP/HTTP inputs are otherwise unbounded, so a
|
|
48
|
+
* malicious or buggy client could send multi-MB strings to exhaust memory/disk.
|
|
49
|
+
* These caps are generous for real résumés/cover letters but reject abuse.
|
|
50
|
+
* - LINE : single-line fields (names, titles, locations)
|
|
51
|
+
* - NOTE : short multi-line fields (summaries, notes)
|
|
52
|
+
* - BODY : full document bodies (resume / cover letter / job description)
|
|
53
|
+
* - ITEM : one array element; ITEMS : array length
|
|
54
|
+
*/
|
|
55
|
+
const MAX_LINE = 500;
|
|
56
|
+
const MAX_NOTE = 10_000;
|
|
57
|
+
const MAX_BODY = 200_000;
|
|
58
|
+
const MAX_ITEM = 2_000;
|
|
59
|
+
const MAX_ITEMS = 500;
|
|
60
|
+
const EMPTY_MSG = '내용이 비어 있습니다(공백만으로는 저장할 수 없습니다).';
|
|
61
|
+
|
|
62
|
+
/** Required single-line value: rejects whitespace-only, trims, bounds length. */
|
|
63
|
+
const reqLine = z.string().trim().min(1).max(MAX_LINE);
|
|
64
|
+
/** Optional single-line value (bounded). */
|
|
65
|
+
const optLine = z.string().max(MAX_LINE).optional();
|
|
66
|
+
/** Optional short multi-line note (bounded). */
|
|
67
|
+
const optNote = z.string().max(MAX_NOTE).optional();
|
|
68
|
+
/** Optional full document body (bounded). */
|
|
69
|
+
const optBody = z.string().max(MAX_BODY).optional();
|
|
70
|
+
/** Required full document body: bounded, rejects whitespace-only (no trim — preserve formatting). */
|
|
71
|
+
const reqBody = z
|
|
72
|
+
.string()
|
|
73
|
+
.max(MAX_BODY)
|
|
74
|
+
.refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
|
|
75
|
+
/** Bounded array of bounded strings. */
|
|
76
|
+
const strList = z.array(z.string().max(MAX_ITEM)).max(MAX_ITEMS);
|
|
77
|
+
|
|
78
|
+
const baseRecord = {
|
|
79
|
+
id,
|
|
80
|
+
created_at: isoDateTime,
|
|
81
|
+
updated_at: isoDateTime,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/* ------------------------------------------------------------------ Profile */
|
|
85
|
+
|
|
86
|
+
export const LinkSchema = z.object({
|
|
87
|
+
label: reqLine,
|
|
88
|
+
url: SafeUrl,
|
|
89
|
+
});
|
|
90
|
+
export type Link = z.infer<typeof LinkSchema>;
|
|
91
|
+
|
|
92
|
+
export const ProfileInputSchema = z.object({
|
|
93
|
+
name: reqLine.optional(),
|
|
94
|
+
email: optLine,
|
|
95
|
+
phone: optLine,
|
|
96
|
+
location: optLine,
|
|
97
|
+
headline: z.string().max(MAX_LINE).optional().describe('한 줄 소개 / 직무 타이틀'),
|
|
98
|
+
summary: z.string().max(MAX_NOTE).optional().describe('자기소개 요약'),
|
|
99
|
+
desired_roles: strList.optional().describe('희망 직무'),
|
|
100
|
+
desired_conditions: z.string().max(MAX_NOTE).optional().describe('희망 근무 조건 (연봉, 지역, 근무형태 등)'),
|
|
101
|
+
preferred_tone: z.string().max(MAX_LINE).optional().describe('자기소개서 선호 문체 (예: 담백하고 구체적)'),
|
|
102
|
+
emphasis_points: strList.optional().describe('강조하고 싶은 핵심 포인트'),
|
|
103
|
+
links: z.array(LinkSchema).max(MAX_ITEMS).optional().describe('포트폴리오/깃허브/링크드인 등'),
|
|
104
|
+
});
|
|
105
|
+
export type ProfileInput = z.infer<typeof ProfileInputSchema>;
|
|
106
|
+
|
|
107
|
+
export const ProfileRecordSchema = z.object({
|
|
108
|
+
...baseRecord,
|
|
109
|
+
name: z.string().nullable(),
|
|
110
|
+
email: z.string().nullable(),
|
|
111
|
+
phone: z.string().nullable(),
|
|
112
|
+
location: z.string().nullable(),
|
|
113
|
+
headline: z.string().nullable(),
|
|
114
|
+
summary: z.string().nullable(),
|
|
115
|
+
desired_roles: z.array(z.string()),
|
|
116
|
+
desired_conditions: z.string().nullable(),
|
|
117
|
+
preferred_tone: z.string().nullable(),
|
|
118
|
+
emphasis_points: z.array(z.string()),
|
|
119
|
+
links: z.array(LinkSchema),
|
|
120
|
+
});
|
|
121
|
+
export type ProfileRecord = z.infer<typeof ProfileRecordSchema>;
|
|
122
|
+
|
|
123
|
+
/* --------------------------------------------------------------- Experience */
|
|
124
|
+
|
|
125
|
+
export const ExperienceInputSchema = z.object({
|
|
126
|
+
company: reqLine,
|
|
127
|
+
role: optLine,
|
|
128
|
+
employment_type: z.string().max(MAX_LINE).optional().describe('정규직/계약직/인턴/프리랜서 등'),
|
|
129
|
+
start_date: z.string().max(MAX_LINE).optional().describe('YYYY-MM 또는 YYYY-MM-DD'),
|
|
130
|
+
end_date: z.string().max(MAX_LINE).optional().describe('재직 중이면 비워두기'),
|
|
131
|
+
is_current: z.boolean().optional(),
|
|
132
|
+
description: optBody,
|
|
133
|
+
achievements: strList.optional().describe('성과/업적 (정량 지표 권장)'),
|
|
134
|
+
tech: strList.optional().describe('사용 기술/도구'),
|
|
135
|
+
order_index: z.number().optional(),
|
|
136
|
+
});
|
|
137
|
+
export type ExperienceInput = z.infer<typeof ExperienceInputSchema>;
|
|
138
|
+
|
|
139
|
+
export const ExperienceRecordSchema = z.object({
|
|
140
|
+
...baseRecord,
|
|
141
|
+
company: z.string(),
|
|
142
|
+
role: z.string().nullable(),
|
|
143
|
+
employment_type: z.string().nullable(),
|
|
144
|
+
start_date: z.string().nullable(),
|
|
145
|
+
end_date: z.string().nullable(),
|
|
146
|
+
is_current: z.boolean(),
|
|
147
|
+
description: z.string().nullable(),
|
|
148
|
+
achievements: z.array(z.string()),
|
|
149
|
+
tech: z.array(z.string()),
|
|
150
|
+
order_index: z.number(),
|
|
151
|
+
});
|
|
152
|
+
export type ExperienceRecord = z.infer<typeof ExperienceRecordSchema>;
|
|
153
|
+
|
|
154
|
+
/* ------------------------------------------------------------------ Project */
|
|
155
|
+
|
|
156
|
+
export const ProjectInputSchema = z.object({
|
|
157
|
+
name: reqLine,
|
|
158
|
+
role: optLine,
|
|
159
|
+
description: optBody,
|
|
160
|
+
highlights: strList.optional(),
|
|
161
|
+
tech: strList.optional(),
|
|
162
|
+
url: SafeUrlOptional,
|
|
163
|
+
start_date: optLine,
|
|
164
|
+
end_date: optLine,
|
|
165
|
+
order_index: z.number().optional(),
|
|
166
|
+
});
|
|
167
|
+
export type ProjectInput = z.infer<typeof ProjectInputSchema>;
|
|
168
|
+
|
|
169
|
+
export const ProjectRecordSchema = z.object({
|
|
170
|
+
...baseRecord,
|
|
171
|
+
name: z.string(),
|
|
172
|
+
role: z.string().nullable(),
|
|
173
|
+
description: z.string().nullable(),
|
|
174
|
+
highlights: z.array(z.string()),
|
|
175
|
+
tech: z.array(z.string()),
|
|
176
|
+
url: z.string().nullable(),
|
|
177
|
+
start_date: z.string().nullable(),
|
|
178
|
+
end_date: z.string().nullable(),
|
|
179
|
+
order_index: z.number(),
|
|
180
|
+
});
|
|
181
|
+
export type ProjectRecord = z.infer<typeof ProjectRecordSchema>;
|
|
182
|
+
|
|
183
|
+
/* -------------------------------------------------------------------- Skill */
|
|
184
|
+
|
|
185
|
+
export const SkillInputSchema = z.object({
|
|
186
|
+
name: reqLine,
|
|
187
|
+
category: z.string().max(MAX_LINE).optional().describe('언어/프레임워크/툴/소프트스킬 등'),
|
|
188
|
+
level: z.string().max(MAX_LINE).optional().describe('상/중/하 또는 자유 서술'),
|
|
189
|
+
years: z.number().optional(),
|
|
190
|
+
order_index: z.number().optional(),
|
|
191
|
+
});
|
|
192
|
+
export type SkillInput = z.infer<typeof SkillInputSchema>;
|
|
193
|
+
|
|
194
|
+
export const SkillRecordSchema = z.object({
|
|
195
|
+
...baseRecord,
|
|
196
|
+
name: z.string(),
|
|
197
|
+
category: z.string().nullable(),
|
|
198
|
+
level: z.string().nullable(),
|
|
199
|
+
years: z.number().nullable(),
|
|
200
|
+
order_index: z.number(),
|
|
201
|
+
});
|
|
202
|
+
export type SkillRecord = z.infer<typeof SkillRecordSchema>;
|
|
203
|
+
|
|
204
|
+
/* ----------------------------------------------------------------- Document */
|
|
205
|
+
|
|
206
|
+
export const DocumentInputSchema = z.object({
|
|
207
|
+
kind: z.enum(DOCUMENT_KINDS),
|
|
208
|
+
title: reqLine,
|
|
209
|
+
content: reqBody.describe('Markdown 또는 일반 텍스트 본문'),
|
|
210
|
+
source: z.enum(CONTENT_SOURCES).optional(),
|
|
211
|
+
is_primary: z.boolean().optional().describe('대표 문서 여부'),
|
|
212
|
+
tags: strList.optional(),
|
|
213
|
+
});
|
|
214
|
+
export type DocumentInput = z.infer<typeof DocumentInputSchema>;
|
|
215
|
+
|
|
216
|
+
export const DocumentRecordSchema = z.object({
|
|
217
|
+
...baseRecord,
|
|
218
|
+
kind: z.enum(DOCUMENT_KINDS),
|
|
219
|
+
title: z.string(),
|
|
220
|
+
content: z.string(),
|
|
221
|
+
source: z.enum(CONTENT_SOURCES),
|
|
222
|
+
is_primary: z.boolean(),
|
|
223
|
+
tags: z.array(z.string()),
|
|
224
|
+
});
|
|
225
|
+
export type DocumentRecord = z.infer<typeof DocumentRecordSchema>;
|
|
226
|
+
|
|
227
|
+
/* -------------------------------------------------------------- CoverLetter */
|
|
228
|
+
|
|
229
|
+
export const CoverLetterInputSchema = z.object({
|
|
230
|
+
title: reqLine,
|
|
231
|
+
job_id: optLine.describe('특정 공고에 연결할 경우'),
|
|
232
|
+
is_primary: z.boolean().optional(),
|
|
233
|
+
content: optBody.describe('초기 버전 본문 (있으면 v1로 저장)'),
|
|
234
|
+
source: z.enum(CONTENT_SOURCES).optional(),
|
|
235
|
+
note: optNote.describe('초기 버전 메모'),
|
|
236
|
+
});
|
|
237
|
+
export type CoverLetterInput = z.infer<typeof CoverLetterInputSchema>;
|
|
238
|
+
|
|
239
|
+
export const CoverLetterVersionInputSchema = z.object({
|
|
240
|
+
cover_letter_id: optLine.describe('기존 자기소개서에 새 버전 추가 시'),
|
|
241
|
+
title: z.string().max(MAX_LINE).optional().describe('새 자기소개서를 만들 때 제목'),
|
|
242
|
+
job_id: optLine,
|
|
243
|
+
content: reqBody,
|
|
244
|
+
note: optNote.describe('이 버전의 변경 요약 (예: 지원동기 보강)'),
|
|
245
|
+
source: z.enum(CONTENT_SOURCES).optional(),
|
|
246
|
+
set_current: z.boolean().optional().describe('이 버전을 현재 버전으로 지정 (기본 true)'),
|
|
247
|
+
});
|
|
248
|
+
export type CoverLetterVersionInput = z.infer<typeof CoverLetterVersionInputSchema>;
|
|
249
|
+
|
|
250
|
+
export const CoverLetterVersionRecordSchema = z.object({
|
|
251
|
+
id,
|
|
252
|
+
cover_letter_id: z.string(),
|
|
253
|
+
version_no: z.number(),
|
|
254
|
+
content: z.string(),
|
|
255
|
+
note: z.string().nullable(),
|
|
256
|
+
source: z.enum(CONTENT_SOURCES),
|
|
257
|
+
created_at: isoDateTime,
|
|
258
|
+
});
|
|
259
|
+
export type CoverLetterVersionRecord = z.infer<typeof CoverLetterVersionRecordSchema>;
|
|
260
|
+
|
|
261
|
+
export const CoverLetterRecordSchema = z.object({
|
|
262
|
+
...baseRecord,
|
|
263
|
+
title: z.string(),
|
|
264
|
+
job_id: z.string().nullable(),
|
|
265
|
+
is_primary: z.boolean(),
|
|
266
|
+
current_version_id: z.string().nullable(),
|
|
267
|
+
version_count: z.number(),
|
|
268
|
+
current_content: z.string().nullable(),
|
|
269
|
+
versions: z.array(CoverLetterVersionRecordSchema).optional(),
|
|
270
|
+
});
|
|
271
|
+
export type CoverLetterRecord = z.infer<typeof CoverLetterRecordSchema>;
|
|
272
|
+
|
|
273
|
+
/* --------------------------------------------------------------------- Job */
|
|
274
|
+
|
|
275
|
+
export const JobInputSchema = z.object({
|
|
276
|
+
company: reqLine,
|
|
277
|
+
position: reqLine.describe('직무/포지션명'),
|
|
278
|
+
url: SafeUrlOptional,
|
|
279
|
+
location: optLine,
|
|
280
|
+
employment_type: optLine,
|
|
281
|
+
description: optBody.describe('공고 원문 또는 정리된 텍스트'),
|
|
282
|
+
requirements: strList.optional().describe('자격요건/우대사항 핵심'),
|
|
283
|
+
keywords: strList.optional().describe('핵심 키워드'),
|
|
284
|
+
deadline: z.string().max(MAX_LINE).optional().describe('마감일 YYYY-MM-DD'),
|
|
285
|
+
source: z.string().max(MAX_LINE).optional().describe('출처 (사람인/원티드/직접 입력 등)'),
|
|
286
|
+
});
|
|
287
|
+
export type JobInput = z.infer<typeof JobInputSchema>;
|
|
288
|
+
|
|
289
|
+
export const JobRecordSchema = z.object({
|
|
290
|
+
...baseRecord,
|
|
291
|
+
company: z.string(),
|
|
292
|
+
position: z.string(),
|
|
293
|
+
url: z.string().nullable(),
|
|
294
|
+
location: z.string().nullable(),
|
|
295
|
+
employment_type: z.string().nullable(),
|
|
296
|
+
description: z.string().nullable(),
|
|
297
|
+
requirements: z.array(z.string()),
|
|
298
|
+
keywords: z.array(z.string()),
|
|
299
|
+
deadline: z.string().nullable(),
|
|
300
|
+
source: z.string().nullable(),
|
|
301
|
+
});
|
|
302
|
+
export type JobRecord = z.infer<typeof JobRecordSchema>;
|
|
303
|
+
|
|
304
|
+
/* ------------------------------------------------------------- FitAnalysis */
|
|
305
|
+
|
|
306
|
+
export const FitAnalysisInputSchema = z.object({
|
|
307
|
+
job_id: reqLine.describe('분석 대상 공고 ID'),
|
|
308
|
+
score: z.number().min(0).max(100).optional().describe('종합 적합도 0~100'),
|
|
309
|
+
summary: z.string().max(MAX_NOTE).optional().describe('한두 문단 요약'),
|
|
310
|
+
strengths: strList.optional().describe('강점 / 잘 맞는 부분'),
|
|
311
|
+
gaps: strList.optional().describe('부족한 부분 / 보완 필요'),
|
|
312
|
+
matched_keywords: strList.optional(),
|
|
313
|
+
missing_keywords: strList.optional(),
|
|
314
|
+
recommendations: strList.optional().describe('자기소개서/지원 전략 제안'),
|
|
315
|
+
});
|
|
316
|
+
export type FitAnalysisInput = z.infer<typeof FitAnalysisInputSchema>;
|
|
317
|
+
|
|
318
|
+
export const FitAnalysisRecordSchema = z.object({
|
|
319
|
+
...baseRecord,
|
|
320
|
+
job_id: z.string(),
|
|
321
|
+
score: z.number().nullable(),
|
|
322
|
+
summary: z.string().nullable(),
|
|
323
|
+
strengths: z.array(z.string()),
|
|
324
|
+
gaps: z.array(z.string()),
|
|
325
|
+
matched_keywords: z.array(z.string()),
|
|
326
|
+
missing_keywords: z.array(z.string()),
|
|
327
|
+
recommendations: z.array(z.string()),
|
|
328
|
+
});
|
|
329
|
+
export type FitAnalysisRecord = z.infer<typeof FitAnalysisRecordSchema>;
|
|
330
|
+
|
|
331
|
+
/* ------------------------------------------------------------- Application */
|
|
332
|
+
|
|
333
|
+
export const ApplicationInputSchema = z.object({
|
|
334
|
+
job_id: reqLine,
|
|
335
|
+
status: z.enum(APPLICATION_STATUSES).optional(),
|
|
336
|
+
resume_id: optLine,
|
|
337
|
+
cover_letter_id: optLine,
|
|
338
|
+
applied_at: optLine,
|
|
339
|
+
notes: optNote,
|
|
340
|
+
});
|
|
341
|
+
export type ApplicationInput = z.infer<typeof ApplicationInputSchema>;
|
|
342
|
+
|
|
343
|
+
export const ApplicationStatusUpdateSchema = z.object({
|
|
344
|
+
job_id: optLine,
|
|
345
|
+
application_id: optLine,
|
|
346
|
+
status: z.enum(APPLICATION_STATUSES),
|
|
347
|
+
note: optNote.describe('상태 변경 사유/메모'),
|
|
348
|
+
});
|
|
349
|
+
export type ApplicationStatusUpdate = z.infer<typeof ApplicationStatusUpdateSchema>;
|
|
350
|
+
|
|
351
|
+
export const ApplicationRecordSchema = z.object({
|
|
352
|
+
...baseRecord,
|
|
353
|
+
job_id: z.string(),
|
|
354
|
+
status: z.enum(APPLICATION_STATUSES),
|
|
355
|
+
resume_id: z.string().nullable(),
|
|
356
|
+
cover_letter_id: z.string().nullable(),
|
|
357
|
+
applied_at: z.string().nullable(),
|
|
358
|
+
notes: z.string().nullable(),
|
|
359
|
+
});
|
|
360
|
+
export type ApplicationRecord = z.infer<typeof ApplicationRecordSchema>;
|
|
361
|
+
|
|
362
|
+
/* ----------------------------------------------------------- InterviewPrep */
|
|
363
|
+
|
|
364
|
+
export const StarGuideSchema = z.object({
|
|
365
|
+
question: z.string().max(MAX_NOTE),
|
|
366
|
+
situation: optNote,
|
|
367
|
+
task: optNote,
|
|
368
|
+
action: optNote,
|
|
369
|
+
result: optNote,
|
|
370
|
+
});
|
|
371
|
+
export type StarGuide = z.infer<typeof StarGuideSchema>;
|
|
372
|
+
|
|
373
|
+
export const InterviewQuestionSchema = z.object({
|
|
374
|
+
question: z.string().max(MAX_NOTE),
|
|
375
|
+
intent: z.string().max(MAX_NOTE).optional().describe('질문 의도'),
|
|
376
|
+
followups: strList.optional().describe('예상 꼬리 질문'),
|
|
377
|
+
answer_outline: optBody.describe('답변 가이드/핵심 포인트'),
|
|
378
|
+
});
|
|
379
|
+
export type InterviewQuestion = z.infer<typeof InterviewQuestionSchema>;
|
|
380
|
+
|
|
381
|
+
export const InterviewPrepInputSchema = z.object({
|
|
382
|
+
job_id: reqLine,
|
|
383
|
+
questions: z.array(InterviewQuestionSchema).max(MAX_ITEMS).optional(),
|
|
384
|
+
star_guides: z.array(StarGuideSchema).max(MAX_ITEMS).optional(),
|
|
385
|
+
self_introduction: optBody.describe('1분 자기소개 초안'),
|
|
386
|
+
notes: optNote.describe('면접 후기/메모'),
|
|
387
|
+
});
|
|
388
|
+
export type InterviewPrepInput = z.infer<typeof InterviewPrepInputSchema>;
|
|
389
|
+
|
|
390
|
+
export const InterviewPrepRecordSchema = z.object({
|
|
391
|
+
...baseRecord,
|
|
392
|
+
job_id: z.string(),
|
|
393
|
+
questions: z.array(InterviewQuestionSchema),
|
|
394
|
+
star_guides: z.array(StarGuideSchema),
|
|
395
|
+
self_introduction: z.string().nullable(),
|
|
396
|
+
notes: z.string().nullable(),
|
|
397
|
+
});
|
|
398
|
+
export type InterviewPrepRecord = z.infer<typeof InterviewPrepRecordSchema>;
|
|
399
|
+
|
|
400
|
+
/* ----------------------------------------------------------------- Activity */
|
|
401
|
+
|
|
402
|
+
export const ActivityRecordSchema = z.object({
|
|
403
|
+
id,
|
|
404
|
+
type: z.enum(ACTIVITY_TYPES),
|
|
405
|
+
entity_type: z.enum(ENTITY_TYPES).nullable(),
|
|
406
|
+
entity_id: z.string().nullable(),
|
|
407
|
+
summary: z.string(),
|
|
408
|
+
created_at: isoDateTime,
|
|
409
|
+
});
|
|
410
|
+
export type ActivityRecord = z.infer<typeof ActivityRecordSchema>;
|
|
411
|
+
|
|
412
|
+
/* ----------------------------------------------------- Aggregates / context */
|
|
413
|
+
|
|
414
|
+
export const OnboardingStatusSchema = z.object({
|
|
415
|
+
completed: z.boolean(),
|
|
416
|
+
has_profile: z.boolean(),
|
|
417
|
+
has_resume: z.boolean(),
|
|
418
|
+
has_cover_letter: z.boolean(),
|
|
419
|
+
has_experience: z.boolean(),
|
|
420
|
+
has_skills: z.boolean(),
|
|
421
|
+
has_job: z.boolean(),
|
|
422
|
+
profile_completeness: z.number().describe('0~100 프로필 완성도'),
|
|
423
|
+
next_steps: z.array(z.string()).describe('지금 해야 할 다음 단계 안내'),
|
|
424
|
+
});
|
|
425
|
+
export type OnboardingStatus = z.infer<typeof OnboardingStatusSchema>;
|
|
426
|
+
|
|
427
|
+
/** The one-shot context bundle the AI pulls before analyzing a job or writing. */
|
|
428
|
+
export const ApplicationContextSchema = z.object({
|
|
429
|
+
profile: ProfileRecordSchema.nullable(),
|
|
430
|
+
primary_resume: DocumentRecordSchema.nullable(),
|
|
431
|
+
resumes: z.array(DocumentRecordSchema),
|
|
432
|
+
experiences: z.array(ExperienceRecordSchema),
|
|
433
|
+
projects: z.array(ProjectRecordSchema),
|
|
434
|
+
skills: z.array(SkillRecordSchema),
|
|
435
|
+
cover_letters: z.array(CoverLetterRecordSchema),
|
|
436
|
+
recent_applications: z.array(ApplicationRecordSchema),
|
|
437
|
+
job: JobRecordSchema.nullable(),
|
|
438
|
+
fit_analysis: FitAnalysisRecordSchema.nullable(),
|
|
439
|
+
related_history: z
|
|
440
|
+
.array(
|
|
441
|
+
z.object({
|
|
442
|
+
job: JobRecordSchema,
|
|
443
|
+
application: ApplicationRecordSchema.nullable(),
|
|
444
|
+
fit_analysis: FitAnalysisRecordSchema.nullable(),
|
|
445
|
+
}),
|
|
446
|
+
)
|
|
447
|
+
.describe('같은 회사/직무 관련 이전 기록'),
|
|
448
|
+
writing_preferences: z.object({
|
|
449
|
+
preferred_tone: z.string().nullable(),
|
|
450
|
+
emphasis_points: z.array(z.string()),
|
|
451
|
+
}),
|
|
452
|
+
});
|
|
453
|
+
export type ApplicationContext = z.infer<typeof ApplicationContextSchema>;
|