careermate 0.0.7 → 0.0.9

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 (71) hide show
  1. package/AGENTS.md +2 -2
  2. package/LICENSE +1 -1
  3. package/README.md +108 -91
  4. package/THIRD_PARTY_NOTICES.md +2 -1
  5. package/bin/careermate.mjs +11 -2
  6. package/dist/career-os/eop/cover-letter.md +13 -6
  7. package/dist/career-os/eop/interview-prep.md +1 -0
  8. package/dist/career-os/knowledge/cover-letter.md +7 -3
  9. package/dist/career-os/knowledge/fit-matching.md +2 -0
  10. package/dist/career-os/knowledge/verifiers/ai-detection-risk.md +57 -0
  11. package/dist/career-os/knowledge/verifiers/human-voice.md +7 -2
  12. package/dist/career-os/knowledge/verifiers/truthfulness.md +1 -0
  13. package/dist/doctor.mjs +19760 -1018
  14. package/dist/mcp.mjs +19783 -1035
  15. package/dist/migrate.mjs +289 -20
  16. package/dist/public/i18n/en/applications.js +19 -0
  17. package/dist/public/i18n/en/common.js +10 -0
  18. package/dist/public/i18n/en/documents.js +0 -10
  19. package/dist/public/i18n/en/home.js +5 -0
  20. package/dist/public/i18n/en/jobs.js +18 -1
  21. package/dist/public/i18n/en/profile.js +0 -5
  22. package/dist/public/i18n/en/settings.js +5 -2
  23. package/dist/public/i18n/ko/applications.js +19 -0
  24. package/dist/public/i18n/ko/common.js +9 -0
  25. package/dist/public/i18n/ko/documents.js +0 -10
  26. package/dist/public/i18n/ko/home.js +5 -0
  27. package/dist/public/i18n/ko/jobs.js +18 -1
  28. package/dist/public/i18n/ko/profile.js +0 -5
  29. package/dist/public/i18n/ko/settings.js +5 -2
  30. package/dist/public/lib.js +101 -0
  31. package/dist/public/pages/applications.js +95 -15
  32. package/dist/public/pages/documents.js +20 -14
  33. package/dist/public/pages/home.js +38 -6
  34. package/dist/public/pages/interview.js +11 -4
  35. package/dist/public/pages/jobs.js +71 -1
  36. package/dist/public/pages/profile.js +2 -3
  37. package/dist/public/pages/settings.js +2 -2
  38. package/dist/public/styles.css +52 -4
  39. package/dist/web.mjs +19001 -758
  40. package/docs/FAQ.md +13 -3
  41. package/docs/MCP_TOOLS.md +32 -12
  42. package/docs/career-os/eop/cover-letter.md +13 -6
  43. package/docs/career-os/eop/interview-prep.md +1 -0
  44. package/docs/career-os/knowledge/cover-letter.md +7 -3
  45. package/docs/career-os/knowledge/fit-matching.md +2 -0
  46. package/docs/career-os/knowledge/verifiers/ai-detection-risk.md +57 -0
  47. package/docs/career-os/knowledge/verifiers/human-voice.md +7 -2
  48. package/docs/career-os/knowledge/verifiers/truthfulness.md +1 -0
  49. package/package.json +5 -2
  50. package/packages/core/src/index.ts +2 -0
  51. package/packages/core/src/jobsearch.ts +190 -0
  52. package/packages/core/src/offers.ts +41 -0
  53. package/packages/core/src/services.ts +29 -3
  54. package/packages/core/src/summary.ts +431 -9
  55. package/packages/core/src/verify/charcount.ts +87 -0
  56. package/packages/core/src/verify/index.ts +1 -0
  57. package/packages/core/src/verify/lint.ts +5 -0
  58. package/packages/db/src/connection.ts +70 -2
  59. package/packages/db/src/index.ts +1 -0
  60. package/packages/db/src/repositories.ts +179 -13
  61. package/packages/db/src/schema.ts +169 -8
  62. package/packages/exporters/src/docx.ts +165 -0
  63. package/packages/exporters/src/index.ts +142 -9
  64. package/packages/exporters/src/markdown.ts +16 -11
  65. package/packages/knowledge/src/index.ts +8 -3
  66. package/packages/mcp-tools/src/result.ts +9 -0
  67. package/packages/mcp-tools/src/tools.ts +303 -60
  68. package/packages/parsers/src/documents.ts +7 -1
  69. package/packages/prompts/src/humanize.ts +4 -0
  70. package/packages/shared/src/schemas.ts +198 -0
  71. package/scripts/init.ts +7 -0
@@ -6,7 +6,7 @@ import {
6
6
  el, get, post, put, del, icon, navigate, Card, Badge, Btn, SubmitBtn,
7
7
  EmptyState, Chips, Field, Input, Textarea, Select,
8
8
  openModal, closeModal, confirmDialog, toastOk, toastError,
9
- fmtDate, scoreClass, mount, meta, linesToArray, csvToArray,
9
+ fmtDate, scoreClass, mount, meta, linesToArray, csvToArray, daysUntil, scheduleDdayBadge,
10
10
  } from '/lib.js';
11
11
  import { t } from '/i18n.js';
12
12
 
@@ -340,6 +340,7 @@ async function renderDetail(ctx, jobId) {
340
340
  CoverLettersCard(job.cover_letters),
341
341
  InterviewCard(job)),
342
342
  PostingCard(job),
343
+ CompanyResearchCard(job),
343
344
  FitCard(job.fit),
344
345
  TimelineCard(job.timeline),
345
346
  RelatedCard(job.related),
@@ -372,6 +373,41 @@ function PostingCard(job) {
372
373
  ...job.requirements.map((r) => el('li', {}, r)))));
373
374
  }
374
375
 
376
+ if (job.questions && job.questions.length) {
377
+ // 한국 문항형 자소서: 회사 지정 문항·글자수 한도. AI가 문항별 답변을 쓰고 한도를
378
+ // 점검할 원본이자, 사용자가 대시보드에서 요구 문항을 그대로 볼 수 있게 한다.
379
+ body.push(el('div', { class: 'mt-3' },
380
+ el('div', { class: 'muted text-sm mb-2' }, t('jobs.questions.title')),
381
+ el('ol', { class: 'stack-2', style: { margin: 0, paddingLeft: '18px' } },
382
+ ...job.questions.map((q) => {
383
+ const label = questionLimitLabel(q);
384
+ return el('li', {},
385
+ el('span', {}, q.question),
386
+ label ? el('span', { class: 'muted text-sm', style: { marginLeft: '8px' } }, label) : null);
387
+ }))));
388
+ }
389
+
390
+ if (job.schedule && job.schedule.length) {
391
+ // 전형 일정: 면접·인적성·발표 등 단계별 날짜. 날짜순으로 D-day 라벨과 함께 보여주고,
392
+ // 이미 지난 일정은 흐리게 처리한다(v9 문항 블록과 같은 위치·스타일 패턴).
393
+ const sorted = job.schedule.slice().sort((a, b) => {
394
+ if (a.date !== b.date) return a.date < b.date ? -1 : 1;
395
+ return (a.time || '99:99').localeCompare(b.time || '99:99');
396
+ });
397
+ body.push(el('div', { class: 'mt-3' },
398
+ el('div', { class: 'muted text-sm mb-2' }, t('jobs.schedule.title')),
399
+ el('ul', { class: 'stack-2', style: { margin: 0, padding: 0, listStyle: 'none' } },
400
+ ...sorted.map((s) => {
401
+ const dDay = daysUntil(s.date);
402
+ const when = s.time ? `${fmtDate(s.date)} ${s.time}` : fmtDate(s.date);
403
+ return el('li', { class: `flex gap-2 center${dDay != null && dDay < 0 ? ' muted' : ''}` },
404
+ scheduleDdayBadge(dDay),
405
+ el('span', { class: 'strong' }, s.label),
406
+ el('span', { class: 'text-sm' }, when),
407
+ s.note ? el('span', { class: 'muted text-sm' }, s.note) : null);
408
+ }))));
409
+ }
410
+
375
411
  if (job.description) {
376
412
  // 원문은 가장 길어 칼럼을 늘리는 주범 — 기본 접힘(아코디언)으로 둔다.
377
413
  body.push(el('details', { class: 'disclosure mt-3' },
@@ -384,6 +420,40 @@ function PostingCard(job) {
384
420
  return Card({ title: t('jobs.posting.title'), body });
385
421
  }
386
422
 
423
+ // 문항 글자수 한도 라벨(예: "최대 700자(공백포함)"). 한도가 없으면 null.
424
+ // count_mode: with_space=공백포함 · no_space=공백제외 · byte=UTF-8 바이트(공백 구분 없음).
425
+ function questionLimitLabel(q) {
426
+ const hasMin = typeof q.min_chars === 'number';
427
+ const hasMax = typeof q.max_chars === 'number';
428
+ if (!hasMin && !hasMax) return null;
429
+ const unit = q.count_mode === 'byte' ? t('jobs.questions.unitByte') : t('jobs.questions.unitChar');
430
+ const suffix = q.count_mode === 'no_space' ? t('jobs.questions.modeNoSpace')
431
+ : q.count_mode === 'with_space' ? t('jobs.questions.modeWithSpace')
432
+ : '';
433
+ const parts = [];
434
+ if (hasMin) parts.push(t('jobs.questions.min', { n: q.min_chars, unit }));
435
+ if (hasMax) parts.push(t('jobs.questions.max', { n: q.max_chars, unit }));
436
+ return `${parts.join(' · ')}${suffix}`;
437
+ }
438
+
439
+ // Company research the AI saved (overview·인재상·핵심가치·평판) — rendered so the
440
+ // user can actually SEE it in the dashboard, not just have the AI use it.
441
+ function CompanyResearchCard(job) {
442
+ const hasValues = job.core_values && job.core_values.length;
443
+ if (!job.company_overview && !job.talent_profile && !hasValues && !job.reputation) return null;
444
+ const field = (labelKey, value) => el('div', { class: 'mt-3' },
445
+ el('div', { class: 'muted text-sm mb-2' }, t(labelKey)),
446
+ el('p', { style: { margin: 0, whiteSpace: 'pre-wrap' } }, value));
447
+ const body = [];
448
+ if (job.company_overview) body.push(field('jobs.company.overview', job.company_overview));
449
+ if (job.talent_profile) body.push(field('jobs.company.talent', job.talent_profile));
450
+ if (hasValues) body.push(el('div', { class: 'mt-3' },
451
+ el('div', { class: 'muted text-sm mb-2' }, t('jobs.company.values')),
452
+ Chips(job.core_values, { accent: true })));
453
+ if (job.reputation) body.push(field('jobs.company.reputation', job.reputation));
454
+ return Card({ title: t('jobs.company.title'), body });
455
+ }
456
+
387
457
  function FitCard(fit) {
388
458
  if (!fit) {
389
459
  return Card({
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  el, get, post, put, del, icon, Card, Badge, Btn, IconBtn, SubmitBtn, EmptyState, Chips,
4
4
  Field, Input, Textarea, openModal, closeModal, confirmDialog,
5
- toastOk, toastError, fmtDate, mount, linesToArray, csvToArray, downloadUrl,
5
+ toastOk, toastError, fmtDate, mount, linesToArray, csvToArray, ExportMenu,
6
6
  } from '/lib.js';
7
7
  import { t } from '/i18n.js';
8
8
 
@@ -67,8 +67,7 @@ function CompletenessCard(o) {
67
67
  title: t('profile.completeness.title'),
68
68
  sub: t('profile.completeness.percent', { pct }),
69
69
  actions: [
70
- Btn(t('profile.export.md'), { sm: true, variant: 'ghost', icon: 'download', title: t('profile.export.mdTitle'), onClick: () => downloadUrl('/api/export/profile?format=md') }),
71
- Btn(t('profile.export.html'), { sm: true, variant: 'ghost', icon: 'download', title: t('profile.export.htmlTitle'), onClick: () => downloadUrl('/api/export/profile?format=html') }),
70
+ ExportMenu('/api/export/profile'),
72
71
  ],
73
72
  body: [
74
73
  el('div', { class: 'progress', style: { marginBottom: steps.length ? '12px' : '0' } },
@@ -168,7 +168,6 @@ function Theme() {
168
168
  /* ------------------------------------------------------------- 언어 / Language */
169
169
  function LanguageSwitcher() {
170
170
  // 테마 컴포넌트와 동일 패턴: localStorage 영속 + paint() + 변경 시 재페인트.
171
- // 카탈로그가 채워지는 Phase 2부터 페이지 텍스트가 실제로 전환된다.
172
171
  const options = [
173
172
  { value: 'ko', label: '한국어' },
174
173
  { value: 'en', label: 'English' },
@@ -188,8 +187,9 @@ function LanguageSwitcher() {
188
187
  onLangChange(paint);
189
188
 
190
189
  return Card({
190
+ // 제목은 의도적으로 양언어 병기 — 현재 언어를 못 읽는 사용자도 이 카드를 찾아야 한다.
191
191
  title: '언어 / Language',
192
- sub: '대시보드 표시 언어',
192
+ sub: t('settings.lang.sub'),
193
193
  body: row,
194
194
  });
195
195
  }
@@ -201,14 +201,16 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
201
201
  .card.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s, transform .12s; }
202
202
  .card.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
203
203
 
204
- /* stat tiles */
205
- .stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; }
204
+ /* stat tiles — slim, dense KPI row (home only). A contextual hint line, when
205
+ present, sits at the bottom so tiles align evenly whether or not they carry one. */
206
+ .stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
207
+ padding: 12px 15px; min-height: 84px; display: flex; flex-direction: column; }
206
208
  .stat.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s; }
207
209
  .stat.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
208
210
  .stat__label { font-size: 13px; color: var(--text-secondary); display:flex; align-items:center; gap:7px; }
209
211
  .stat__label svg { width: 15px; height: 15px; color: var(--text-tertiary); }
210
- .stat__value { font-size: 26px; font-weight: 700; letter-spacing: -.02em; margin-top: 6px; }
211
- .stat__hint { font-size: 12.5px; color: var(--text-tertiary); margin-top: 2px; }
212
+ .stat__value { font-size: 23px; font-weight: 700; letter-spacing: -.02em; margin-top: 3px; line-height: 1.15; }
213
+ .stat__hint { font-size: 12.5px; color: var(--text-tertiary); margin-top: auto; padding-top: 6px; }
212
214
  /* borderless count strip (settings 내 데이터) — flat counts, never a card-in-card */
213
215
  .countstrip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px 24px; }
214
216
  .countstrip__item { display: flex; flex-direction: column; gap: 2px; }
@@ -379,6 +381,24 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
379
381
  .app-group__title { font-weight: 650; font-size: 15px; letter-spacing: -.01em; }
380
382
  .app-group__count { margin-left: auto; color: var(--text-tertiary); font-size: 13px; }
381
383
 
384
+ /* applications row trailing — keep the right edge informative (never empty):
385
+ right-aligned score column, an optional D-day chip, then the status. The status
386
+ shows a calm badge at rest and swaps to the inline change-select on hover/focus
387
+ (display swap, not opacity, so the hidden control reserves no width). */
388
+ .app-score { min-width: 42px; text-align: right; font-size: 13.5px; }
389
+ .app-dday { color: var(--text-tertiary); }
390
+ .app-dday--urgent { color: var(--amber); }
391
+ .app-status { display: inline-flex; align-items: center; }
392
+ .app-status .select { display: none; }
393
+ .list-row:hover .app-status .badge,
394
+ .list-row:focus-within .app-status .badge { display: none; }
395
+ .list-row:hover .app-status .select,
396
+ .list-row:focus-within .app-status .select { display: inline-flex; }
397
+ @media (hover: none) {
398
+ .app-status .badge { display: none; }
399
+ .app-status .select { display: inline-flex; }
400
+ }
401
+
382
402
  /* row-level action (e.g. status select) — hidden until the row is hovered or
383
403
  focused, so grouped lists stay calm. Always shown where hover isn't available. */
384
404
  .list-row .row-action { opacity: 0; pointer-events: none; transition: opacity .12s; }
@@ -467,6 +487,21 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
467
487
  @keyframes pop { from { transform: translate(-50%,-46%); opacity: 0; } to { transform: translate(-50%,-50%); opacity: 1; } }
468
488
  @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
469
489
 
490
+ /* Dropdown menu (Menu/ExportMenu) — fixed popover anchored to its trigger so it
491
+ never clips inside a scrolling modal body. Sits above the modal layer (60). */
492
+ .menu { position: relative; display: inline-flex; }
493
+ .menu__trigger .menu__caret { width: 14px; height: 14px; margin-left: -2px; opacity: .55; transform: rotate(90deg); }
494
+ .menu__list { position: fixed; z-index: 70; min-width: 176px; padding: 5px; display: flex; flex-direction: column; gap: 2px;
495
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-lg); animation: fade .12s; }
496
+ .menu__list[hidden] { display: none; }
497
+ .menu__item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 10px; border: 0; border-radius: var(--radius-sm);
498
+ background: transparent; color: var(--text); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
499
+ .menu__item:hover, .menu__item:focus-visible { background: var(--surface-hover); outline: none; }
500
+ .menu__item .icon { width: 15px; height: 15px; color: var(--text-tertiary); flex: none; }
501
+ .menu__item-label { flex: 1; }
502
+ .menu__item-hint { color: var(--text-tertiary); font-size: 11.5px; font-variant-numeric: tabular-nums; }
503
+ .menu__item--danger { color: var(--red); }
504
+
470
505
  /* ----------------------------------------------------------------- toast */
471
506
  .toasts { position: fixed; bottom: 20px; right: 20px; z-index: 80; display: flex; flex-direction: column; gap: 10px; }
472
507
  .toast { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius);
@@ -542,3 +577,16 @@ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
542
577
  .card__head .right{ margin-left: 0; width: 100%; justify-content: flex-start; }
543
578
  .page-head{ flex-wrap: wrap; }
544
579
  }
580
+
581
+ /* ---------- reduced motion ----------
582
+ 전정기관 민감 사용자를 위해 OS 설정을 존중한다: 모달·토스트·오프캔버스의
583
+ 등장 애니메이션과 hover 트랜지션을 사실상 즉시(0.01ms)로 줄인다.
584
+ (스켈레톤 shimmer 포함 — 반복 애니메이션이 가장 부담이 크다.) */
585
+ @media (prefers-reduced-motion: reduce) {
586
+ *, *::before, *::after {
587
+ animation-duration: 0.01ms !important;
588
+ animation-iteration-count: 1 !important;
589
+ transition-duration: 0.01ms !important;
590
+ scroll-behavior: auto !important;
591
+ }
592
+ }