careermate 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/AGENTS.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -1
  4. package/apps/web/public/pages/documents.js +7 -2
  5. package/apps/web/public/pages/home.js +4 -7
  6. package/apps/web/public/pages/jobs.js +1 -5
  7. package/apps/web/public/pages/profile.js +3 -3
  8. package/apps/web/public/styles.css +7 -2
  9. package/dist/doctor.mjs +112 -90
  10. package/dist/install-page/analytics.js +97 -0
  11. package/dist/install-page/index.html +77 -56
  12. package/dist/install-page/og.png +0 -0
  13. package/dist/install-page/robots.txt +4 -0
  14. package/dist/install-page/sitemap.xml +21 -0
  15. package/dist/install-page/start.html +13 -10
  16. package/dist/install-page/style.css +75 -40
  17. package/dist/mcp.mjs +112 -90
  18. package/dist/migrate.mjs +95 -82
  19. package/dist/public/pages/documents.js +7 -2
  20. package/dist/public/pages/home.js +4 -7
  21. package/dist/public/pages/jobs.js +1 -5
  22. package/dist/public/pages/profile.js +3 -3
  23. package/dist/public/styles.css +7 -2
  24. package/dist/web.mjs +113 -86
  25. package/docs/FAQ.md +101 -0
  26. package/docs/career-os/README.md +76 -0
  27. package/docs/career-os/eop/cover-letter.md +46 -0
  28. package/docs/career-os/eop/fit-analysis.md +48 -0
  29. package/docs/career-os/eop/interview-prep.md +39 -0
  30. package/docs/career-os/eop/job-analysis.md +42 -0
  31. package/docs/career-os/eop/profile-extraction.md +37 -0
  32. package/docs/career-os/knowledge/ats.md +131 -0
  33. package/docs/career-os/knowledge/company-research.md +137 -0
  34. package/docs/career-os/knowledge/cover-letter.md +135 -0
  35. package/docs/career-os/knowledge/fit-matching.md +121 -0
  36. package/docs/career-os/knowledge/human-writing.md +118 -0
  37. package/docs/career-os/knowledge/interview-behavioral.md +132 -0
  38. package/docs/career-os/knowledge/interview-technical.md +126 -0
  39. package/docs/career-os/knowledge/linkedin-profile.md +152 -0
  40. package/docs/career-os/knowledge/networking-referrals.md +166 -0
  41. package/docs/career-os/knowledge/offer-evaluation-decision.md +121 -0
  42. package/docs/career-os/knowledge/onboarding-first-90-days.md +126 -0
  43. package/docs/career-os/knowledge/portfolio.md +121 -0
  44. package/docs/career-os/knowledge/recruiter-screen.md +105 -0
  45. package/docs/career-os/knowledge/rejection-triage-iteration.md +115 -0
  46. package/docs/career-os/knowledge/resume.md +123 -0
  47. package/docs/career-os/knowledge/salary-negotiation.md +123 -0
  48. package/docs/career-os/knowledge/verifiers/ats-compat.md +64 -0
  49. package/docs/career-os/knowledge/verifiers/consistency.md +61 -0
  50. package/docs/career-os/knowledge/verifiers/human-voice.md +82 -0
  51. package/docs/career-os/knowledge/verifiers/recency-staleness.md +72 -0
  52. package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
  53. package/docs/career-os/knowledge/verifiers/truthfulness.md +67 -0
  54. package/package.json +11 -3
  55. package/packages/db/src/repositories.ts +18 -1
  56. package/packages/mcp-tools/src/tools.ts +5 -5
  57. package/packages/parsers/src/documents.ts +13 -1
  58. package/packages/shared/src/schemas.ts +115 -81
  59. package/scripts/build-dist.mjs +7 -1
  60. package/scripts/gen-og.mjs +1 -1
  61. package/scripts/test.ts +25 -0
  62. package/scripts/ui-smoke.ts +32 -4
  63. package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +0 -12
  64. package/dist/install-page/.omc/state/idle-notif-cooldown.json +0 -3
  65. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +0 -147
  66. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +0 -22
  67. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +0 -44
  68. package/docs/ARCHITECTURE.md +0 -208
  69. package/docs/CHANGES_V1.md +0 -103
  70. package/docs/DATA_MODEL.md +0 -460
  71. package/docs/DECISIONS.md +0 -277
  72. package/docs/DEMO.md +0 -242
  73. package/docs/ROADMAP.md +0 -135
  74. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +0 -135
  75. package/docs/TODO.md +0 -57
  76. package/docs/UX_NOTES.md +0 -247
  77. package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +0 -64
@@ -23,6 +23,7 @@ export async function render(ctx) {
23
23
  root.append(el('div', { class: 'tabs' }, tabCover, tabDocs), panel);
24
24
 
25
25
  let query = '';
26
+ let searchInput = null;
26
27
  function selectTab(next) {
27
28
  tab = next;
28
29
  query = '';
@@ -43,9 +44,12 @@ export async function render(ctx) {
43
44
  if (!hide) shown += 1;
44
45
  }
45
46
  const existing = panel.querySelector('.search-empty');
47
+ if (existing) existing.remove();
46
48
  if (q && cards.length && shown === 0) {
47
- if (!existing) panel.append(el('p', { class: 'search-empty muted', style: { textAlign: 'center', margin: '8px 0' } }, '검색 결과가 없어요.'));
48
- } else if (existing) existing.remove();
49
+ panel.append(el('div', { class: 'search-empty empty' },
50
+ el('p', { class: 'muted', style: { margin: '0 0 12px' } }, `‘${query.trim()}’에 맞는 항목이 없어요.`),
51
+ Btn('필터 초기화', { sm: true, variant: 'ghost', onClick: () => { query = ''; if (searchInput) searchInput.value = ''; applyFilter(); } })));
52
+ }
49
53
  }
50
54
 
51
55
  async function reload() {
@@ -70,6 +74,7 @@ export async function render(ctx) {
70
74
  function setActions() {
71
75
  const search = Input({ type: 'search', placeholder: tab === 'cover' ? '자기소개서 검색' : '문서 검색', value: query, attrs: { 'aria-label': '검색' } });
72
76
  search.classList.add('input--inline');
77
+ searchInput = search;
73
78
  search.addEventListener('input', () => { query = search.value; applyFilter(); });
74
79
  const createBtn = tab === 'cover'
75
80
  ? Btn('새 자기소개서', { icon: 'plus', variant: 'primary', onClick: () => openCoverCreate(reload) })
@@ -4,7 +4,7 @@
4
4
  // the 시작하기 card. Every empty state is ONE screen-specific sentence; never repeat
5
5
  // the same "AI에게 보내면 저장됩니다" instruction across surfaces.
6
6
  import {
7
- el, get, icon, navigate, Card, Badge, Btn, EmptyState, ListRow, CheckRow,
7
+ el, get, icon, navigate, Card, Badge, Btn, ListRow, CheckRow,
8
8
  Stat, PageHead, fmtDate, fmtRelative, scoreClass, STATUS_COLOR, statusColor, mount,
9
9
  } from '/lib.js';
10
10
 
@@ -59,7 +59,7 @@ function Greeting(name, s, firstRun) {
59
59
  let focus;
60
60
  if (firstRun) focus = '시작해 볼까요 — 먼저 기본 정보를 채워 주세요.';
61
61
  else if (s.interview_todo.length) focus = `면접 준비가 필요한 지원 ${s.interview_todo.length}건이 있어요.`;
62
- else if (s.in_progress.length) focus = `진행 중인 지원 ${s.in_progress.length}건을 확인하세요.`;
62
+ else if (s.in_progress.length) focus = `진행 중인 지원 ${s.in_progress.length}건이 있어요.`;
63
63
  else if (s.counts.jobs) focus = `저장된 공고 ${s.counts.jobs}건 · 새 적합도 분석을 받아보세요.`;
64
64
  else focus = '오늘도 한 걸음 나아가 볼까요.';
65
65
  return PageHead({ title: name ? `${name}님, 안녕하세요` : '환영합니다', desc: focus });
@@ -87,7 +87,7 @@ function GettingStarted(o) {
87
87
  el('div', { class: 'callout__body' },
88
88
  '공고 분석·자기소개서·면접 준비는 평소 쓰던 AI에게 말로 시키면, 결과가 이 대시보드에 쌓여요.'))),
89
89
  el('div', { class: 'progress' }, el('div', { class: 'progress__bar', style: { width: `${o.profile_completeness}%` } })),
90
- el('p', { class: 'text-secondary text-sm', style: { margin: '12px 0 8px' } },
90
+ el('p', { class: 'text-secondary text-sm mt-3 mb-2' },
91
91
  '완성할수록 AI의 적합도 분석과 자기소개서 품질이 좋아져요.'),
92
92
  el('div', {}, ...steps.map((it) => CheckRow({ done: it.done, label: it.label, onClick: () => navigate(it.to) }))),
93
93
  ],
@@ -201,10 +201,7 @@ function RecentJobs(jobs) {
201
201
  if (!jobs.length) {
202
202
  return Card({
203
203
  title: '최근 공고',
204
- body: EmptyState({
205
- iconName: 'briefcase', title: '저장된 공고가 아직 없어요',
206
- action: Btn('공고 추가', { icon: 'plus', variant: 'ghost', onClick: () => navigate('/jobs') }),
207
- }),
204
+ body: el('p', { class: 'muted', style: { margin: 0 } }, '저장된 공고가 아직 없어요. 새 공고를 추가해 보세요.'),
208
205
  });
209
206
  }
210
207
  const rows = jobs.map((j) => ListRow({
@@ -365,11 +365,7 @@ function FitCard(fit) {
365
365
  if (!fit) {
366
366
  return Card({
367
367
  title: '적합도 분석',
368
- body: EmptyState({
369
- iconName: 'target',
370
- title: '적합도 분석 전이에요',
371
- body: '이 공고와 내 프로필을 비교한 분석 결과가 여기에 표시됩니다.',
372
- }),
368
+ body: el('p', { class: 'muted', style: { margin: 0 } }, '아직 분석 전이에요. 이 공고와 내 프로필을 비교한 결과가 여기에 표시됩니다.'),
373
369
  });
374
370
  }
375
371
 
@@ -264,7 +264,7 @@ function editWriting(p, reload) {
264
264
  /* ---------------------------------------------------- 4. 경력 (Experiences) */
265
265
 
266
266
  function ExperiencesCard(items, reload) {
267
- const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editExperience(null, reload) });
267
+ const add = Btn('경력 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editExperience(null, reload) });
268
268
 
269
269
  if (!items.length) {
270
270
  return Card({
@@ -368,7 +368,7 @@ function editExperience(x, reload) {
368
368
  /* ----------------------------------------------------- 5. 프로젝트 (Projects) */
369
369
 
370
370
  function ProjectsCard(items, reload) {
371
- const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editProject(null, reload) });
371
+ const add = Btn('프로젝트 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editProject(null, reload) });
372
372
 
373
373
  if (!items.length) {
374
374
  return Card({
@@ -469,7 +469,7 @@ function editProject(x, reload) {
469
469
  /* -------------------------------------------------------- 6. 기술 (Skills) */
470
470
 
471
471
  function SkillsCard(items, reload) {
472
- const add = Btn('추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editSkill(null, reload) });
472
+ const add = Btn('기술 추가', { icon: 'plus', sm: true, variant: 'ghost', onClick: () => editSkill(null, reload) });
473
473
 
474
474
  if (!items.length) {
475
475
  return Card({
@@ -403,7 +403,9 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
403
403
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
404
404
  .kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
405
405
  .kv dt { color: var(--text-tertiary); font-size: 13px; }
406
- .kv dd { margin: 0; }
406
+ /* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
407
+ can shrink below its content — otherwise a long path/value overflows the grid. */
408
+ .kv dd { margin: 0; min-width: 0; }
407
409
 
408
410
  /* ----------------------------------------------------------------- modal */
409
411
  .modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
@@ -450,7 +452,10 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
450
452
  .between { justify-content: space-between; } .center { align-items: center; } .wrap { flex-wrap: wrap; }
451
453
  .ml-auto { margin-left: auto; } .mt-2{margin-top:8px;} .mt-3{margin-top:12px;} .mt-4{margin-top:16px;} .mb-2{margin-bottom:8px;} .mb-3{margin-bottom:12px;} .mb-4{margin-bottom:16px;}
452
454
  .tnum { font-variant-numeric: tabular-nums; }
453
- .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
455
+ /* min-width:0 lets a truncating element actually shrink inside a flex row;
456
+ without it a long unbreakable value (e.g. a data-folder path) forces the
457
+ flex item to its content width and overflows the viewport on narrow screens. */
458
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; min-width: 0; }
454
459
  .divider { height: 1px; background: var(--border); margin: 16px 0; }
455
460
  .skeleton { background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--surface-hover) 37%, var(--bg-sunken) 63%); background-size: 400% 100%; animation: shimmer 1.4s infinite; border-radius: 6px; }
456
461
  @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
package/dist/web.mjs CHANGED
@@ -43892,29 +43892,42 @@ function isSafeUrl(u) {
43892
43892
  return !/^[a-z][a-z0-9+.-]*:/i.test(s) || /^(https?:|mailto:)/i.test(s);
43893
43893
  }
43894
43894
  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.";
43895
- var SafeUrl = external_exports.string().min(1).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
43896
- var SafeUrlOptional = external_exports.string().optional().refine((u) => u == null || u === "" || isSafeUrl(u), { message: UNSAFE_URL_MSG });
43895
+ var MAX_URL = 2e3;
43896
+ var SafeUrl = external_exports.string().min(1).max(MAX_URL).refine(isSafeUrl, { message: UNSAFE_URL_MSG });
43897
+ var SafeUrlOptional = external_exports.string().max(MAX_URL).optional().refine((u) => u == null || u === "" || isSafeUrl(u), { message: UNSAFE_URL_MSG });
43898
+ var MAX_LINE = 500;
43899
+ var MAX_NOTE = 1e4;
43900
+ var MAX_BODY = 2e5;
43901
+ var MAX_ITEM = 2e3;
43902
+ var MAX_ITEMS = 500;
43903
+ var EMPTY_MSG = "\uB0B4\uC6A9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4(\uACF5\uBC31\uB9CC\uC73C\uB85C\uB294 \uC800\uC7A5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4).";
43904
+ var reqLine = external_exports.string().trim().min(1).max(MAX_LINE);
43905
+ var optLine = external_exports.string().max(MAX_LINE).optional();
43906
+ var optNote = external_exports.string().max(MAX_NOTE).optional();
43907
+ var optBody = external_exports.string().max(MAX_BODY).optional();
43908
+ var reqBody = external_exports.string().max(MAX_BODY).refine((s) => s.trim().length > 0, { message: EMPTY_MSG });
43909
+ var strList = external_exports.array(external_exports.string().max(MAX_ITEM)).max(MAX_ITEMS);
43897
43910
  var baseRecord = {
43898
43911
  id,
43899
43912
  created_at: isoDateTime,
43900
43913
  updated_at: isoDateTime
43901
43914
  };
43902
43915
  var LinkSchema = external_exports.object({
43903
- label: external_exports.string().min(1),
43916
+ label: reqLine,
43904
43917
  url: SafeUrl
43905
43918
  });
43906
43919
  var ProfileInputSchema = external_exports.object({
43907
- name: external_exports.string().min(1).optional(),
43908
- email: external_exports.string().optional(),
43909
- phone: external_exports.string().optional(),
43910
- location: external_exports.string().optional(),
43911
- headline: external_exports.string().optional().describe("\uD55C \uC904 \uC18C\uAC1C / \uC9C1\uBB34 \uD0C0\uC774\uD2C0"),
43912
- summary: external_exports.string().optional().describe("\uC790\uAE30\uC18C\uAC1C \uC694\uC57D"),
43913
- desired_roles: external_exports.array(external_exports.string()).optional().describe("\uD76C\uB9DD \uC9C1\uBB34"),
43914
- desired_conditions: external_exports.string().optional().describe("\uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74 (\uC5F0\uBD09, \uC9C0\uC5ED, \uADFC\uBB34\uD615\uD0DC \uB4F1)"),
43915
- preferred_tone: external_exports.string().optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C \uC120\uD638 \uBB38\uCCB4 (\uC608: \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801)"),
43916
- emphasis_points: external_exports.array(external_exports.string()).optional().describe("\uAC15\uC870\uD558\uACE0 \uC2F6\uC740 \uD575\uC2EC \uD3EC\uC778\uD2B8"),
43917
- links: external_exports.array(LinkSchema).optional().describe("\uD3EC\uD2B8\uD3F4\uB9AC\uC624/\uAE43\uD5C8\uBE0C/\uB9C1\uD06C\uB4DC\uC778 \uB4F1")
43920
+ name: reqLine.optional(),
43921
+ email: optLine,
43922
+ phone: optLine,
43923
+ location: optLine,
43924
+ headline: external_exports.string().max(MAX_LINE).optional().describe("\uD55C \uC904 \uC18C\uAC1C / \uC9C1\uBB34 \uD0C0\uC774\uD2C0"),
43925
+ summary: external_exports.string().max(MAX_NOTE).optional().describe("\uC790\uAE30\uC18C\uAC1C \uC694\uC57D"),
43926
+ desired_roles: strList.optional().describe("\uD76C\uB9DD \uC9C1\uBB34"),
43927
+ desired_conditions: external_exports.string().max(MAX_NOTE).optional().describe("\uD76C\uB9DD \uADFC\uBB34 \uC870\uAC74 (\uC5F0\uBD09, \uC9C0\uC5ED, \uADFC\uBB34\uD615\uD0DC \uB4F1)"),
43928
+ preferred_tone: external_exports.string().max(MAX_LINE).optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C \uC120\uD638 \uBB38\uCCB4 (\uC608: \uB2F4\uBC31\uD558\uACE0 \uAD6C\uCCB4\uC801)"),
43929
+ emphasis_points: strList.optional().describe("\uAC15\uC870\uD558\uACE0 \uC2F6\uC740 \uD575\uC2EC \uD3EC\uC778\uD2B8"),
43930
+ links: external_exports.array(LinkSchema).max(MAX_ITEMS).optional().describe("\uD3EC\uD2B8\uD3F4\uB9AC\uC624/\uAE43\uD5C8\uBE0C/\uB9C1\uD06C\uB4DC\uC778 \uB4F1")
43918
43931
  });
43919
43932
  var ProfileRecordSchema = external_exports.object({
43920
43933
  ...baseRecord,
@@ -43931,15 +43944,15 @@ var ProfileRecordSchema = external_exports.object({
43931
43944
  links: external_exports.array(LinkSchema)
43932
43945
  });
43933
43946
  var ExperienceInputSchema = external_exports.object({
43934
- company: external_exports.string().min(1),
43935
- role: external_exports.string().optional(),
43936
- employment_type: external_exports.string().optional().describe("\uC815\uADDC\uC9C1/\uACC4\uC57D\uC9C1/\uC778\uD134/\uD504\uB9AC\uB79C\uC11C \uB4F1"),
43937
- start_date: external_exports.string().optional().describe("YYYY-MM \uB610\uB294 YYYY-MM-DD"),
43938
- end_date: external_exports.string().optional().describe("\uC7AC\uC9C1 \uC911\uC774\uBA74 \uBE44\uC6CC\uB450\uAE30"),
43947
+ company: reqLine,
43948
+ role: optLine,
43949
+ employment_type: external_exports.string().max(MAX_LINE).optional().describe("\uC815\uADDC\uC9C1/\uACC4\uC57D\uC9C1/\uC778\uD134/\uD504\uB9AC\uB79C\uC11C \uB4F1"),
43950
+ start_date: external_exports.string().max(MAX_LINE).optional().describe("YYYY-MM \uB610\uB294 YYYY-MM-DD"),
43951
+ end_date: external_exports.string().max(MAX_LINE).optional().describe("\uC7AC\uC9C1 \uC911\uC774\uBA74 \uBE44\uC6CC\uB450\uAE30"),
43939
43952
  is_current: external_exports.boolean().optional(),
43940
- description: external_exports.string().optional(),
43941
- achievements: external_exports.array(external_exports.string()).optional().describe("\uC131\uACFC/\uC5C5\uC801 (\uC815\uB7C9 \uC9C0\uD45C \uAD8C\uC7A5)"),
43942
- tech: external_exports.array(external_exports.string()).optional().describe("\uC0AC\uC6A9 \uAE30\uC220/\uB3C4\uAD6C"),
43953
+ description: optBody,
43954
+ achievements: strList.optional().describe("\uC131\uACFC/\uC5C5\uC801 (\uC815\uB7C9 \uC9C0\uD45C \uAD8C\uC7A5)"),
43955
+ tech: strList.optional().describe("\uC0AC\uC6A9 \uAE30\uC220/\uB3C4\uAD6C"),
43943
43956
  order_index: external_exports.number().optional()
43944
43957
  });
43945
43958
  var ExperienceRecordSchema = external_exports.object({
@@ -43956,14 +43969,14 @@ var ExperienceRecordSchema = external_exports.object({
43956
43969
  order_index: external_exports.number()
43957
43970
  });
43958
43971
  var ProjectInputSchema = external_exports.object({
43959
- name: external_exports.string().min(1),
43960
- role: external_exports.string().optional(),
43961
- description: external_exports.string().optional(),
43962
- highlights: external_exports.array(external_exports.string()).optional(),
43963
- tech: external_exports.array(external_exports.string()).optional(),
43972
+ name: reqLine,
43973
+ role: optLine,
43974
+ description: optBody,
43975
+ highlights: strList.optional(),
43976
+ tech: strList.optional(),
43964
43977
  url: SafeUrlOptional,
43965
- start_date: external_exports.string().optional(),
43966
- end_date: external_exports.string().optional(),
43978
+ start_date: optLine,
43979
+ end_date: optLine,
43967
43980
  order_index: external_exports.number().optional()
43968
43981
  });
43969
43982
  var ProjectRecordSchema = external_exports.object({
@@ -43979,9 +43992,9 @@ var ProjectRecordSchema = external_exports.object({
43979
43992
  order_index: external_exports.number()
43980
43993
  });
43981
43994
  var SkillInputSchema = external_exports.object({
43982
- name: external_exports.string().min(1),
43983
- category: external_exports.string().optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
43984
- level: external_exports.string().optional().describe("\uC0C1/\uC911/\uD558 \uB610\uB294 \uC790\uC720 \uC11C\uC220"),
43995
+ name: reqLine,
43996
+ category: external_exports.string().max(MAX_LINE).optional().describe("\uC5B8\uC5B4/\uD504\uB808\uC784\uC6CC\uD06C/\uD234/\uC18C\uD504\uD2B8\uC2A4\uD0AC \uB4F1"),
43997
+ level: external_exports.string().max(MAX_LINE).optional().describe("\uC0C1/\uC911/\uD558 \uB610\uB294 \uC790\uC720 \uC11C\uC220"),
43985
43998
  years: external_exports.number().optional(),
43986
43999
  order_index: external_exports.number().optional()
43987
44000
  });
@@ -43995,11 +44008,11 @@ var SkillRecordSchema = external_exports.object({
43995
44008
  });
43996
44009
  var DocumentInputSchema = external_exports.object({
43997
44010
  kind: external_exports.enum(DOCUMENT_KINDS),
43998
- title: external_exports.string().min(1),
43999
- content: external_exports.string().describe("Markdown \uB610\uB294 \uC77C\uBC18 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38"),
44011
+ title: reqLine,
44012
+ content: reqBody.describe("Markdown \uB610\uB294 \uC77C\uBC18 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38"),
44000
44013
  source: external_exports.enum(CONTENT_SOURCES).optional(),
44001
44014
  is_primary: external_exports.boolean().optional().describe("\uB300\uD45C \uBB38\uC11C \uC5EC\uBD80"),
44002
- tags: external_exports.array(external_exports.string()).optional()
44015
+ tags: strList.optional()
44003
44016
  });
44004
44017
  var DocumentRecordSchema = external_exports.object({
44005
44018
  ...baseRecord,
@@ -44011,19 +44024,19 @@ var DocumentRecordSchema = external_exports.object({
44011
44024
  tags: external_exports.array(external_exports.string())
44012
44025
  });
44013
44026
  var CoverLetterInputSchema = external_exports.object({
44014
- title: external_exports.string().min(1),
44015
- job_id: external_exports.string().optional().describe("\uD2B9\uC815 \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD560 \uACBD\uC6B0"),
44027
+ title: reqLine,
44028
+ job_id: optLine.describe("\uD2B9\uC815 \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD560 \uACBD\uC6B0"),
44016
44029
  is_primary: external_exports.boolean().optional(),
44017
- content: external_exports.string().optional().describe("\uCD08\uAE30 \uBC84\uC804 \uBCF8\uBB38 (\uC788\uC73C\uBA74 v1\uB85C \uC800\uC7A5)"),
44030
+ content: optBody.describe("\uCD08\uAE30 \uBC84\uC804 \uBCF8\uBB38 (\uC788\uC73C\uBA74 v1\uB85C \uC800\uC7A5)"),
44018
44031
  source: external_exports.enum(CONTENT_SOURCES).optional(),
44019
- note: external_exports.string().optional().describe("\uCD08\uAE30 \uBC84\uC804 \uBA54\uBAA8")
44032
+ note: optNote.describe("\uCD08\uAE30 \uBC84\uC804 \uBA54\uBAA8")
44020
44033
  });
44021
44034
  var CoverLetterVersionInputSchema = external_exports.object({
44022
- cover_letter_id: external_exports.string().optional().describe("\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uC5D0 \uC0C8 \uBC84\uC804 \uCD94\uAC00 \uC2DC"),
44023
- title: external_exports.string().optional().describe("\uC0C8 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB9CC\uB4E4 \uB54C \uC81C\uBAA9"),
44024
- job_id: external_exports.string().optional(),
44025
- content: external_exports.string().min(1),
44026
- note: external_exports.string().optional().describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
44035
+ cover_letter_id: optLine.describe("\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uC5D0 \uC0C8 \uBC84\uC804 \uCD94\uAC00 \uC2DC"),
44036
+ title: external_exports.string().max(MAX_LINE).optional().describe("\uC0C8 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uB9CC\uB4E4 \uB54C \uC81C\uBAA9"),
44037
+ job_id: optLine,
44038
+ content: reqBody,
44039
+ note: optNote.describe("\uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D (\uC608: \uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15)"),
44027
44040
  source: external_exports.enum(CONTENT_SOURCES).optional(),
44028
44041
  set_current: external_exports.boolean().optional().describe("\uC774 \uBC84\uC804\uC744 \uD604\uC7AC \uBC84\uC804\uC73C\uB85C \uC9C0\uC815 (\uAE30\uBCF8 true)")
44029
44042
  });
@@ -44047,16 +44060,16 @@ var CoverLetterRecordSchema = external_exports.object({
44047
44060
  versions: external_exports.array(CoverLetterVersionRecordSchema).optional()
44048
44061
  });
44049
44062
  var JobInputSchema = external_exports.object({
44050
- company: external_exports.string().min(1),
44051
- position: external_exports.string().min(1).describe("\uC9C1\uBB34/\uD3EC\uC9C0\uC158\uBA85"),
44063
+ company: reqLine,
44064
+ position: reqLine.describe("\uC9C1\uBB34/\uD3EC\uC9C0\uC158\uBA85"),
44052
44065
  url: SafeUrlOptional,
44053
- location: external_exports.string().optional(),
44054
- employment_type: external_exports.string().optional(),
44055
- description: external_exports.string().optional().describe("\uACF5\uACE0 \uC6D0\uBB38 \uB610\uB294 \uC815\uB9AC\uB41C \uD14D\uC2A4\uD2B8"),
44056
- requirements: external_exports.array(external_exports.string()).optional().describe("\uC790\uACA9\uC694\uAC74/\uC6B0\uB300\uC0AC\uD56D \uD575\uC2EC"),
44057
- keywords: external_exports.array(external_exports.string()).optional().describe("\uD575\uC2EC \uD0A4\uC6CC\uB4DC"),
44058
- deadline: external_exports.string().optional().describe("\uB9C8\uAC10\uC77C YYYY-MM-DD"),
44059
- source: external_exports.string().optional().describe("\uCD9C\uCC98 (\uC0AC\uB78C\uC778/\uC6D0\uD2F0\uB4DC/\uC9C1\uC811 \uC785\uB825 \uB4F1)")
44066
+ location: optLine,
44067
+ employment_type: optLine,
44068
+ description: optBody.describe("\uACF5\uACE0 \uC6D0\uBB38 \uB610\uB294 \uC815\uB9AC\uB41C \uD14D\uC2A4\uD2B8"),
44069
+ requirements: strList.optional().describe("\uC790\uACA9\uC694\uAC74/\uC6B0\uB300\uC0AC\uD56D \uD575\uC2EC"),
44070
+ keywords: strList.optional().describe("\uD575\uC2EC \uD0A4\uC6CC\uB4DC"),
44071
+ deadline: external_exports.string().max(MAX_LINE).optional().describe("\uB9C8\uAC10\uC77C YYYY-MM-DD"),
44072
+ source: external_exports.string().max(MAX_LINE).optional().describe("\uCD9C\uCC98 (\uC0AC\uB78C\uC778/\uC6D0\uD2F0\uB4DC/\uC9C1\uC811 \uC785\uB825 \uB4F1)")
44060
44073
  });
44061
44074
  var JobRecordSchema = external_exports.object({
44062
44075
  ...baseRecord,
@@ -44072,14 +44085,14 @@ var JobRecordSchema = external_exports.object({
44072
44085
  source: external_exports.string().nullable()
44073
44086
  });
44074
44087
  var FitAnalysisInputSchema = external_exports.object({
44075
- job_id: external_exports.string().describe("\uBD84\uC11D \uB300\uC0C1 \uACF5\uACE0 ID"),
44088
+ job_id: reqLine.describe("\uBD84\uC11D \uB300\uC0C1 \uACF5\uACE0 ID"),
44076
44089
  score: external_exports.number().min(0).max(100).optional().describe("\uC885\uD569 \uC801\uD569\uB3C4 0~100"),
44077
- summary: external_exports.string().optional().describe("\uD55C\uB450 \uBB38\uB2E8 \uC694\uC57D"),
44078
- strengths: external_exports.array(external_exports.string()).optional().describe("\uAC15\uC810 / \uC798 \uB9DE\uB294 \uBD80\uBD84"),
44079
- gaps: external_exports.array(external_exports.string()).optional().describe("\uBD80\uC871\uD55C \uBD80\uBD84 / \uBCF4\uC644 \uD544\uC694"),
44080
- matched_keywords: external_exports.array(external_exports.string()).optional(),
44081
- missing_keywords: external_exports.array(external_exports.string()).optional(),
44082
- recommendations: external_exports.array(external_exports.string()).optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C/\uC9C0\uC6D0 \uC804\uB7B5 \uC81C\uC548")
44090
+ summary: external_exports.string().max(MAX_NOTE).optional().describe("\uD55C\uB450 \uBB38\uB2E8 \uC694\uC57D"),
44091
+ strengths: strList.optional().describe("\uAC15\uC810 / \uC798 \uB9DE\uB294 \uBD80\uBD84"),
44092
+ gaps: strList.optional().describe("\uBD80\uC871\uD55C \uBD80\uBD84 / \uBCF4\uC644 \uD544\uC694"),
44093
+ matched_keywords: strList.optional(),
44094
+ missing_keywords: strList.optional(),
44095
+ recommendations: strList.optional().describe("\uC790\uAE30\uC18C\uAC1C\uC11C/\uC9C0\uC6D0 \uC804\uB7B5 \uC81C\uC548")
44083
44096
  });
44084
44097
  var FitAnalysisRecordSchema = external_exports.object({
44085
44098
  ...baseRecord,
@@ -44093,18 +44106,18 @@ var FitAnalysisRecordSchema = external_exports.object({
44093
44106
  recommendations: external_exports.array(external_exports.string())
44094
44107
  });
44095
44108
  var ApplicationInputSchema = external_exports.object({
44096
- job_id: external_exports.string(),
44109
+ job_id: reqLine,
44097
44110
  status: external_exports.enum(APPLICATION_STATUSES).optional(),
44098
- resume_id: external_exports.string().optional(),
44099
- cover_letter_id: external_exports.string().optional(),
44100
- applied_at: external_exports.string().optional(),
44101
- notes: external_exports.string().optional()
44111
+ resume_id: optLine,
44112
+ cover_letter_id: optLine,
44113
+ applied_at: optLine,
44114
+ notes: optNote
44102
44115
  });
44103
44116
  var ApplicationStatusUpdateSchema = external_exports.object({
44104
- job_id: external_exports.string().optional(),
44105
- application_id: external_exports.string().optional(),
44117
+ job_id: optLine,
44118
+ application_id: optLine,
44106
44119
  status: external_exports.enum(APPLICATION_STATUSES),
44107
- note: external_exports.string().optional().describe("\uC0C1\uD0DC \uBCC0\uACBD \uC0AC\uC720/\uBA54\uBAA8")
44120
+ note: optNote.describe("\uC0C1\uD0DC \uBCC0\uACBD \uC0AC\uC720/\uBA54\uBAA8")
44108
44121
  });
44109
44122
  var ApplicationRecordSchema = external_exports.object({
44110
44123
  ...baseRecord,
@@ -44116,24 +44129,24 @@ var ApplicationRecordSchema = external_exports.object({
44116
44129
  notes: external_exports.string().nullable()
44117
44130
  });
44118
44131
  var StarGuideSchema = external_exports.object({
44119
- question: external_exports.string(),
44120
- situation: external_exports.string().optional(),
44121
- task: external_exports.string().optional(),
44122
- action: external_exports.string().optional(),
44123
- result: external_exports.string().optional()
44132
+ question: external_exports.string().max(MAX_NOTE),
44133
+ situation: optNote,
44134
+ task: optNote,
44135
+ action: optNote,
44136
+ result: optNote
44124
44137
  });
44125
44138
  var InterviewQuestionSchema = external_exports.object({
44126
- question: external_exports.string(),
44127
- intent: external_exports.string().optional().describe("\uC9C8\uBB38 \uC758\uB3C4"),
44128
- followups: external_exports.array(external_exports.string()).optional().describe("\uC608\uC0C1 \uAF2C\uB9AC \uC9C8\uBB38"),
44129
- answer_outline: external_exports.string().optional().describe("\uB2F5\uBCC0 \uAC00\uC774\uB4DC/\uD575\uC2EC \uD3EC\uC778\uD2B8")
44139
+ question: external_exports.string().max(MAX_NOTE),
44140
+ intent: external_exports.string().max(MAX_NOTE).optional().describe("\uC9C8\uBB38 \uC758\uB3C4"),
44141
+ followups: strList.optional().describe("\uC608\uC0C1 \uAF2C\uB9AC \uC9C8\uBB38"),
44142
+ answer_outline: optBody.describe("\uB2F5\uBCC0 \uAC00\uC774\uB4DC/\uD575\uC2EC \uD3EC\uC778\uD2B8")
44130
44143
  });
44131
44144
  var InterviewPrepInputSchema = external_exports.object({
44132
- job_id: external_exports.string(),
44133
- questions: external_exports.array(InterviewQuestionSchema).optional(),
44134
- star_guides: external_exports.array(StarGuideSchema).optional(),
44135
- self_introduction: external_exports.string().optional().describe("1\uBD84 \uC790\uAE30\uC18C\uAC1C \uCD08\uC548"),
44136
- notes: external_exports.string().optional().describe("\uBA74\uC811 \uD6C4\uAE30/\uBA54\uBAA8")
44145
+ job_id: reqLine,
44146
+ questions: external_exports.array(InterviewQuestionSchema).max(MAX_ITEMS).optional(),
44147
+ star_guides: external_exports.array(StarGuideSchema).max(MAX_ITEMS).optional(),
44148
+ self_introduction: optBody.describe("1\uBD84 \uC790\uAE30\uC18C\uAC1C \uCD08\uC548"),
44149
+ notes: optNote.describe("\uBA74\uC811 \uD6C4\uAE30/\uBA54\uBAA8")
44137
44150
  });
44138
44151
  var InterviewPrepRecordSchema = external_exports.object({
44139
44152
  ...baseRecord,
@@ -44187,7 +44200,7 @@ var ApplicationContextSchema = external_exports.object({
44187
44200
  });
44188
44201
 
44189
44202
  // packages/shared/src/build-info.ts
44190
- var APP_VERSION = true ? "0.1.2" : "0.0.0-dev";
44203
+ var APP_VERSION = true ? "0.1.4" : "0.0.0-dev";
44191
44204
  var BUNDLED = true;
44192
44205
 
44193
44206
  // packages/shared/src/index.ts
@@ -44903,7 +44916,21 @@ var activityRepo = {
44903
44916
  };
44904
44917
  function getEntityCounts() {
44905
44918
  const db = getDb();
44906
- const c = (t) => db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
44919
+ const ALLOWED_TABLES = /* @__PURE__ */ new Set([
44920
+ "profile",
44921
+ "experiences",
44922
+ "projects",
44923
+ "skills",
44924
+ "documents",
44925
+ "cover_letters",
44926
+ "jobs",
44927
+ "applications",
44928
+ "interview_preps"
44929
+ ]);
44930
+ const c = (t) => {
44931
+ if (!ALLOWED_TABLES.has(t)) throw new Error(`getEntityCounts: \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC740 \uD14C\uC774\uBE14 \uC774\uB984: ${t}`);
44932
+ return db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
44933
+ };
44907
44934
  return {
44908
44935
  profile: c("profile"),
44909
44936
  experiences: c("experiences"),
@@ -44979,7 +45006,7 @@ function injectToken(html) {
44979
45006
  // apps/web/src/http.ts
44980
45007
  import fs3 from "node:fs";
44981
45008
  import path3 from "node:path";
44982
- var MAX_BODY = 8 * 1024 * 1024;
45009
+ var MAX_BODY2 = 8 * 1024 * 1024;
44983
45010
  var Router = class {
44984
45011
  routes = [];
44985
45012
  add(method, path6, handler) {
@@ -45074,7 +45101,7 @@ function readBody(req) {
45074
45101
  const chunks = [];
45075
45102
  req.on("data", (c) => {
45076
45103
  size2 += c.length;
45077
- if (size2 > MAX_BODY) {
45104
+ if (size2 > MAX_BODY2) {
45078
45105
  reject2(new HttpError(413, "\uC694\uCCAD \uBCF8\uBB38\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4.", "too_large"));
45079
45106
  req.destroy();
45080
45107
  return;
package/docs/FAQ.md ADDED
@@ -0,0 +1,101 @@
1
+ # 자주 묻는 질문 / 문제 해결 (FAQ & Troubleshooting)
2
+
3
+ 설치·연결·사용 중 자주 막히는 지점을 모았습니다. 설치 자체가 처음이라면
4
+ [`../INSTALL.md`](../INSTALL.md)를, 연결 직후 무엇을 할지는
5
+ [`START_WORKFLOW.md`](START_WORKFLOW.md)를 먼저 보세요.
6
+
7
+ ---
8
+
9
+ ## 설치 / 연결
10
+
11
+ ### "MCP 도구가 안 보여요" / AI가 CareerMate를 모릅니다
12
+ 1. AI 클라이언트를 **완전히 종료한 뒤 다시 시작**했는지 확인하세요(설정만 바꾸고 재시작을
13
+ 안 하면 적용되지 않습니다).
14
+ 2. Claude Code는 첫 실행 시 프로젝트 서버에 대한 **1회 승인**을 묻습니다 — 승인하세요.
15
+ 3. 클라이언트의 `/mcp` 명령으로 `careermate`가 연결됐는지 확인하거나, AI에게
16
+ "온보딩 상태 확인해줘"라고 해 응답이 오는지 보세요.
17
+ 4. 그래도 안 되면 클라이언트별 등록 방법을 [`../INSTALL.md`](../INSTALL.md)와
18
+ [`SUPPORTED_AI_APPS.md`](SUPPORTED_AI_APPS.md)에서 확인하세요.
19
+
20
+ ### ChatGPT 웹/모바일, Gemini 웹에서 안 돼요
21
+ 정상입니다. 이들은 클라우드에서 실행되어 **원격 URL MCP만** 쓸 수 있고, CareerMate는
22
+ 사용자 컴퓨터에서 도는 **로컬 stdio 서버**라 닿지 못합니다. 로컬에서 실행되는
23
+ **Claude Desktop · Claude Code · Codex CLI** 를 사용하세요(자세한 이유는
24
+ [`SUPPORTED_AI_APPS.md`](SUPPORTED_AI_APPS.md)).
25
+
26
+ ### Node 버전 오류가 나요
27
+ CareerMate는 **Node.js 22.5.0 이상**이 필요합니다(`node:sqlite` 사용).
28
+ `node -v`로 확인하고 필요하면 업데이트하세요.
29
+
30
+ ---
31
+
32
+ ## 대시보드
33
+
34
+ ### "대시보드가 안 열려요" (`http://127.0.0.1:4319`)
35
+ - 그 주소는 **CareerMate가 실행 중일 때만** 열립니다. AI에게 무언가를 저장해 달라고 해
36
+ 서버가 떠 있는 상태인지 확인하세요.
37
+ - **포트가 이미 사용 중**이면 자동으로 다른 포트로 폴백합니다. 환경변수
38
+ `CAREERMATE_PORT`로 고정할 수 있습니다.
39
+ - 대시보드는 보안상 **`127.0.0.1`(이 컴퓨터)** 에만 열립니다. 다른 기기에서는 접속되지
40
+ 않습니다(의도된 동작).
41
+
42
+ ### 브라우저를 자동으로 안 열어요
43
+ 첫 온보딩 때 한 번만 자동으로 엽니다. 이후에는 링크만 안내합니다 — 직접 주소를 열거나,
44
+ AI에게 "대시보드 열어줘"라고 하세요. 자동 열기를 끄려면 `CAREERMATE_NO_OPEN=1`.
45
+
46
+ ---
47
+
48
+ ## 문서(이력서·공고) 읽기
49
+
50
+ ### "PDF가 안 읽혀요"
51
+ - **스캔본/이미지 PDF**(텍스트 레이어 없음)는 추출되지 않습니다. 이미지를 직접 볼 수 있는
52
+ AI 클라이언트라면 PDF를 그대로 보여주고, 아니면 내용을 **텍스트로 붙여넣어** 주세요.
53
+ - **암호로 보호된 PDF**는 읽을 수 없습니다. 뷰어에서 암호를 해제해 다시 저장하거나 텍스트로
54
+ 붙여넣으세요.
55
+ - 손상된 PDF, 25MB가 넘는 파일도 거부됩니다 — 안내 메시지에 이유가 나옵니다.
56
+
57
+ ### 지원하는 파일 형식은?
58
+ PDF, Word(`.docx`), 한컴(`.hwp`/`.hwpx`), 텍스트(`.txt`/`.md`). 가능하면 **절대 경로**로
59
+ 파일 위치를 알려 주세요(예: `C:\Users\me\Desktop\이력서.pdf`).
60
+
61
+ ### 한글이 깨져요
62
+ CareerMate는 UTF-8과 한국어 인코딩(CP949/EUC-KR)을 자동 감지합니다. 그래도 깨지면 파일을
63
+ UTF-8로 다시 저장하거나 내용을 붙여넣어 주세요.
64
+
65
+ ---
66
+
67
+ ## 공고 분석 / 자소서
68
+
69
+ ### "공고 URL은 어떻게 넣어요?"
70
+ URL을 그대로 붙여넣어도 되고, 공고 **본문 텍스트**를 붙여넣어도 됩니다. CareerMate는 LLM이
71
+ 없어 URL을 직접 크롤링하지 않으므로, **공고 본문 텍스트가 있으면 분석 품질이 가장 좋습니다.**
72
+ AI에게 "이 공고 저장하고 분석해줘"라고 하면서 URL이나 본문을 함께 주세요.
73
+
74
+ ### "분석이 안 돼요" / 결과가 비어요
75
+ - 분석은 **AI가** 수행하고 CareerMate는 입력(프로필·공고)과 결과를 저장합니다. 먼저
76
+ **프로필과 공고가 저장돼 있어야** 합니다.
77
+ - AI에게 "분석 전에 application context부터 불러와줘"라고 하면 프로필·이력서·공고를 한 번에
78
+ 모아 분석 맥락을 갖춥니다.
79
+
80
+ ### 면접 준비가 잠겨 있어요
81
+ 면접 준비는 지원 상태가 **'서류 합격(document_passed)' 이상**일 때만 열립니다. 먼저 해당
82
+ 공고의 상태를 서류 합격으로 바꿔 주세요(`draft → planned → applied → document_passed → …`).
83
+
84
+ ---
85
+
86
+ ## 데이터 / 프라이버시
87
+
88
+ ### 내 데이터는 어디에 저장되나요?
89
+ 사용자 컴퓨터의 `~/.careermate`(Windows: `%USERPROFILE%\.careermate`)에만 저장됩니다.
90
+ 대시보드 **설정 > 데이터 저장 위치**에서 정확한 경로를 볼 수 있습니다. 외부로 전송되지
91
+ 않습니다.
92
+
93
+ ### 백업하거나 전부 삭제하려면?
94
+ 대시보드 **설정 > 백업 / 내보내기**에서 백업 생성·전체 JSON 내보내기를, **위험 구역**에서
95
+ 전체 초기화를 할 수 있습니다(초기화 직전 자동 백업).
96
+
97
+ ---
98
+
99
+ 여기서 해결되지 않으면 [GitHub 이슈](https://github.com/osntak/CareerMate/issues)에 남겨
100
+ 주세요. **보안 문제**는 이슈가 아니라 [`../SECURITY.md`](../SECURITY.md)의 비공개 절차를
101
+ 이용해 주세요.