careermate 0.0.13 → 0.0.15

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.
@@ -50,7 +50,7 @@ export interface JobSearchResponse {
50
50
  query: string;
51
51
  results: JobSearchResult[];
52
52
  providers: ProviderOutcome[];
53
- /** Results dropped by `postedSince`. */
53
+ /** Results dropped by `postedSince` and `years`. */
54
54
  filtered_out?: number;
55
55
  }
56
56
 
@@ -64,6 +64,8 @@ export interface JobSearchOptions {
64
64
  saraminCategories?: string[];
65
65
  /** Keep only postings dated on/after this day (YYYY-MM-DD): posted_at, else first_seen_at. */
66
66
  postedSince?: string;
67
+ /** User's years of experience: keep postings whose range covers it (unknown ranges stay). */
68
+ years?: number;
67
69
  /** Record first-seen dates in the local DB (default true; tests turn it off). */
68
70
  trackSeen?: boolean;
69
71
  /** "Today" for first-seen/postedSince, injectable for tests. */
@@ -450,8 +452,13 @@ export async function searchJobs(query: string, opts: JobSearchOptions = {}): Pr
450
452
  }
451
453
  results = dedupeAcrossBoards(results);
452
454
  let filtered_out: number | undefined;
453
- if (opts.postedSince) {
454
- const kept = results.filter((r) => (r.posted_at ?? r.first_seen_at ?? today) >= opts.postedSince!);
455
+ const years = opts.years;
456
+ if (opts.postedSince || years != null) {
457
+ const kept = results.filter((r) => {
458
+ if (opts.postedSince && (r.posted_at ?? r.first_seen_at ?? today) < opts.postedSince) return false;
459
+ const range = years != null ? experienceRange(r.experience) : null;
460
+ return !range || (range[0] <= years! && years! <= range[1]);
461
+ });
455
462
  filtered_out = results.length - kept.length;
456
463
  results = kept;
457
464
  }
@@ -464,6 +471,22 @@ export async function searchJobs(query: string, opts: JobSearchOptions = {}): Pr
464
471
  };
465
472
  }
466
473
 
474
+ /**
475
+ * Years a posting accepts, from each board's wording: "3~10년", "경력 3~10년", "5년 이상",
476
+ * "경력5년↑", "~3년", "신입", "신입·경력", "경력무관". Unreadable text gives null.
477
+ */
478
+ export function experienceRange(text: string | undefined): [number, number] | null {
479
+ const t = (text ?? '').replace(/\s/g, '');
480
+ if (!t) return null;
481
+ if (/무관/.test(t)) return [0, Infinity];
482
+ const m = t.match(/(\d+)?~(\d+)년/);
483
+ if (m) return [m[1] ? +m[1] : 0, +m[2]];
484
+ const min = t.match(/(\d+)년(이상|↑)/);
485
+ if (min) return [/신입/.test(t) ? 0 : +min[1], Infinity];
486
+ if (/신입/.test(t)) return /경력/.test(t) ? [0, Infinity] : [0, 0];
487
+ return null;
488
+ }
489
+
467
490
  // Any "(...)" goes: "(주)" and brand notes like "힐링페이퍼(강남언니)" vs "(주)힐링페이퍼".
468
491
  const normCompany = (s: string) =>
469
492
  s.replace(/\([^)]*\)|㈜|주식회사/g, '').toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
@@ -1110,6 +1110,13 @@ export const TOOLS: ToolDef[] = [
1110
1110
  .refine((s) => s === 'today' || isYmd(s), '"today" 또는 실제 날짜(YYYY-MM-DD)')
1111
1111
  .optional()
1112
1112
  .describe('"today" 또는 YYYY-MM-DD. 그날 이후 공고만'),
1113
+ years: z
1114
+ .number()
1115
+ .int()
1116
+ .min(0)
1117
+ .max(40)
1118
+ .optional()
1119
+ .describe('사용자 경력 연차(신입 0). 공고의 경력 범위에 이 연차가 들어가는 것만 남김. "5년차"면 5. 사용자가 말하지 않았으면 프로필 경력으로 계산해 넣되 확인. 경력 표기를 읽을 수 없는 공고는 남김'),
1113
1120
  },
1114
1121
  // 공고를 가져오기만 한다. 처음 본 날짜 기록은 사용자 데이터가 아닌 로컬 캐시라 readOnly로 둔다.
1115
1122
  readOnly: true,
@@ -1135,18 +1142,19 @@ export const TOOLS: ToolDef[] = [
1135
1142
  rememberCategories,
1136
1143
  saraminCategories,
1137
1144
  postedSince: args.posted_since === 'today' ? today : args.posted_since,
1145
+ years: args.years,
1138
1146
  today,
1139
1147
  });
1140
1148
  const failed = res.providers.filter((p) => !p.ok);
1141
1149
  const okCount = res.results.length;
1142
1150
  const parts = [`'${res.query || '직무 조건'}' 검색 결과 ${okCount}건${usedSaved ? '(저장된 검색 조건 사용)' : ''}`];
1143
- if (res.filtered_out) parts.push(`(게시일 기준 ${res.filtered_out}건 제외)`);
1151
+ if (res.filtered_out) parts.push(`(게시일·경력 조건으로 ${res.filtered_out}건 제외)`);
1144
1152
  if (failed.length) parts.push(`(일부 사이트 응답 실패: ${failed.map((f) => `${f.source}: ${f.error ?? f.status}`).join('; ')})`);
1145
1153
  if (okCount === 0) parts.push('- 키워드나 직무 조건을 바꾸거나, 공고 URL/본문을 주시면 직접 저장해 드릴게요.');
1146
1154
  return ok(parts.join(' '), {
1147
1155
  ...res,
1148
1156
  used_saved_preferences: usedSaved,
1149
- conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories },
1157
+ conditions: { keyword, remember_categories: rememberCategories, saramin_categories: saraminCategories, years: args.years },
1150
1158
  });
1151
1159
  },
1152
1160
  },