// 예비창업자 자격 + 현재 열린 공고를 마감일 그룹별 체크리스트(Markdown)로 생성. // git 추적 대상인 docs/apply-checklist.md 에 저장 → 신청 완료 시 [x] 체크하며 진행. // 실행: LD_LIBRARY_PATH=$ORACLE_IC_LIB_DIR node scripts/generate_checklist.js import { writeFile } from 'node:fs/promises'; import { withConnection, closePool, oracledb } from '../src/db.js'; const SQL = ` SELECT * FROM ( SELECT title, category, agency, source_code, detail_url, apply_end, body_text, CASE WHEN apply_end IS NULL THEN 9999 ELSE (apply_end - TRUNC(SYSDATE)) END AS dleft, ROW_NUMBER() OVER (PARTITION BY title ORDER BY CASE source_code WHEN 'kstartup' THEN 1 WHEN 'bizinfo' THEN 2 ELSE 3 END) rn FROM gov_opportunity WHERE (apply_end IS NULL OR apply_end >= TRUNC(SYSDATE)) AND ( target LIKE '%' || :k1 || '%' OR target LIKE '%' || :k2 || '%' OR DBMS_LOB.INSTR(body_text, :k1) > 0 OR DBMS_LOB.INSTR(body_text, :k2) > 0 ) ) WHERE rn = 1 ORDER BY dleft ASC, source_code`; function region(title) { const m = /^\[([^\]]{1,8})\]/.exec(title); return m ? `\`${m[1]}\` ` : ''; } // 거주지(서울) 기준 지원 가능 판정. // 타 지역(광역시/도/주요 시) 키워드가 제목 접두 또는 주관기관에 있으면 제거. // '서울'이 명시돼 있으면 유지. 둘 다 없으면 전국 사업으로 보고 유지. // 제목 접두 + 주관기관 에서 검사하는 전체 타지역 키워드(축약어 '대전'·'광주' 등 오탐 위험 토큰 포함 가능) const NON_SEOUL = [ '부산', '대구', '인천', '광주', '대전', '울산', '세종', '경기', '강원', '충북', '충남', '전북', '전남', '경북', '경남', '제주', '충청', '전라', '경상', '호남', '영남', '수원', '성남', '용인', '화성', '부천', '안양', '안산', '시흥', '군포', '고양', '김포', '평택', '파주', '의정부', '남양주', '청주', '충주', '제천', '천안', '아산', '전주', '군산', '익산', '여수', '순천', '광양', '목포', '나주', '포항', '경주', '구미', '칠곡', '문경', '안동', '창원', '김해', '진주', '양산', '밀양', '거제', '춘천', '원주', '강릉', '홍천', '속초', '강화', '서귀포', '달구벌', ]; // 제목 본문까지 검사해도 안전한(흔한 단어에 잘 안 섞이는) 도/권역 키워드. // 제외: 대전(대전환), 광주(관광주간), 경기(경기침체), 경상(경상비), 세종(세종대왕) 등 오탐 위험. const NON_SEOUL_BODY = [ '부산', '대구', '인천', '울산', '강원', '충북', '충남', '전북', '전남', '경북', '경남', '제주', '호남', '영남', '홍천', '청주', '천안', '전주', '여수', '순천', '포항', '창원', '김해', '춘천', '원주', '강릉', ]; // 신청자 나이(MY_AGE) 기준 지원 가능 판정. // 본문에서 연령 상한을 추출해 최대 허용연령이 MY_AGE 미만이면 제외. // '만 40세 이상/중장년/시니어/연령무관' 신호가 있으면(중장년 트랙 포함) 유지. const MY_AGE = 46; function ageAllows(x) { const text = `${x.TITLE}\n${x.BODY_TEXT || ''}`; // 46세 이상 허용 신호 → 유지 (만 40~59세 이상, 중장년/장년/시니어, 연령 무관/제한없음, 전 연령, 누구나) if (/만\s*[45]\d\s*세\s*이상|중장년|장년층|시니어|연령\s*무관|연령\s*제한\s*없|나이\s*제한\s*없|제한\s*없음|전\s*연령|누구나/.test(text)) { return true; } // 제목에 '청년/대학생' = 청년 한정 신호 → 제외 (46세 초과) if (/청년|대학생/.test(x.TITLE)) return false; // 연령 상한 추출 const uppers = []; let m; const reUpper = /(\d{2})\s*세\s*(?:이하|미만|까지)/g; while ((m = reUpper.exec(text))) uppers.push(Number(m[1])); const reRange = /(\d{2})\s*세\s*[~∼\-–]\s*만?\s*(\d{2})\s*세/g; while ((m = reRange.exec(text))) uppers.push(Number(m[2])); if (uppers.length === 0) return true; // 연령 상한 언급 없음 → 유지 return Math.max(...uppers) >= MY_AGE; // 최대 허용연령이 MY_AGE 이상이어야 지원 가능 } // 성별/특수대상 기준 지원 가능 판정. const MY_GENDER = '남성'; // 남성 → 여성 전용 제외 const MY_TARGETS = []; // 본인이 해당하는 특수대상(있으면 그 전용 사업 유지). 예: ['장애인'] const TARGET_PATTERNS = { 장애인: /장애인/, 보훈: /보훈|국가유공자|제대군인/, 다문화: /다문화|결혼이민/, 북한이탈: /북한이탈|탈북|새터민/, }; function targetGroupOk(x) { const sig = `${x.TITLE} ${x.AGENCY || ''}`; // 제목 + 주관기관(가점성 본문 언급은 검사 안 함) if (MY_GENDER === '남성' && /여성/.test(sig)) return false; // 여성 전용 제외 if (MY_GENDER === '여성' && /남성\s*(전용|기업)/.test(sig)) return false; for (const [name, re] of Object.entries(TARGET_PATTERNS)) { if (re.test(sig) && !MY_TARGETS.includes(name)) return false; // 특수대상 전용 제외 } return true; } function applicableInSeoul(x) { const prefix = (/^\[([^\]]{1,8})\]/.exec(x.TITLE) || [])[1] || ''; const strong = `${prefix} ${x.AGENCY || ''}`; // 접두 + 주관기관 const full = `${x.TITLE} ${x.AGENCY || ''}`; if (full.includes('서울')) return true; // 서울 명시(서울·경기 등 포함) → 유지 if (NON_SEOUL.some((t) => strong.includes(t))) return false; // 접두/주관기관에 타지역 → 제거 if (NON_SEOUL_BODY.some((t) => x.TITLE.includes(t))) return false; // 제목 본문 도/권역 → 제거 return true; // 지역 신호 없음 → 전국 사업으로 유지 } // 본문 지원자격에 '비서울 지역 + 거주/소재/재학/관내' 가 있으면 지역 제한으로 제외. // 단 '수도권/서울/전국' 거주·소재면 서울 거주자도 가능 → 유지. const RES_KW = '거주|소재|관내|재학|주소|소재지|사업장|위치'; const SEOUL_OK_RE = new RegExp( `(수도권|서울|전국)[가-힣A-Za-z0-9\\s(),·~/]{0,18}(${RES_KW})|(${RES_KW})[가-힣A-Za-z0-9\\s(),·~/]{0,18}(수도권|서울|전국)` ); function regionRestrictedInBody(x) { const body = x.BODY_TEXT || ''; if (!body) return false; // 서울 기관/사업이면(제목·주관기관에 서울/Seoul) 본문에 타지역 언급돼도 유지 if (/서울|seoul/i.test(`${x.TITLE} ${x.AGENCY || ''}`)) return false; if (SEOUL_OK_RE.test(body)) return false; // 수도권/서울/전국 포함 → 제한 아님 // 정방향만 검사('지역 + 거주/소재/관내'). 역방향('주소 + 지역')은 기관 연락처 푸터 오탐이라 제외. for (const t of NON_SEOUL) { if (new RegExp(`${t}[가-힣A-Za-z0-9\\s(),·~/]{0,8}(${RES_KW})`).test(body)) return true; } return false; } function ymd(d) { return d ? new Date(d).toISOString().slice(0, 10) : ''; } function line(x) { const dtag = x.DLEFT === 9999 ? '상시' : `D-${x.DLEFT}`; const deadline = x.DLEFT === 9999 ? '상시/예산소진' : ymd(x.APPLY_END); const cat = x.CATEGORY ? `[${x.CATEGORY}] ` : ''; const title = x.TITLE.replace(/\s+/g, ' ').trim(); return `- [ ] **${dtag}** (${deadline}) ${region(x.TITLE)}${cat}[${title}](${x.DETAIL_URL}) — ${x.AGENCY || ''}`; } const allRows = await withConnection(async (conn) => { const r = await conn.execute( SQL, { k1: '예비창업', k2: '예비 창업' }, { outFormat: oracledb.OUT_FORMAT_OBJECT } ); return r.rows; }); await closePool(); // 서울 거주 기준: 타 지역 한정 공고 제외 (제목/주관기관 + 본문 지원자격) const afterTitleRegion = allRows.filter(applicableInSeoul); const seoulRows = afterTitleRegion.filter((x) => !regionRestrictedInBody(x)); const removedRegion = allRows.length - seoulRows.length; const removedByBody = afterTitleRegion.length - seoulRows.length; console.log(` (그중 본문 지원자격 지역제한: ${removedByBody}건)`); // 연령(46세) 기준: 청년 한정 등 연령 초과 공고 제외 const ageRows = seoulRows.filter(ageAllows); const removedAge = seoulRows.length - ageRows.length; // 성별/특수대상 기준: 여성 전용·장애인/보훈 등 전용 공고 제외 const rows = ageRows.filter(targetGroupOk); const removedTarget = ageRows.length - rows.length; console.log( `필터: 전체 ${allRows.length}건 → 지역(서울) -${removedRegion} → 연령(${MY_AGE}세) -${removedAge} → 성별/대상 -${removedTarget} → 최종 ${rows.length}건` ); const buckets = { '🔴 이번 주 마감 (D-0 ~ D-7)': (d) => d <= 7, '🟡 2주 내 (D-8 ~ D-14)': (d) => d >= 8 && d <= 14, '🟢 여유 (D-15 이상)': (d) => d >= 15 && d !== 9999, '⏳ 상시 접수 (마감 압박 없음)': (d) => d === 9999, }; const today = new Date().toISOString().slice(0, 10); const out = []; out.push('# 정부지원사업 신청 체크리스트'); out.push(''); out.push(`> 생성일: ${today} · 대상: **예비창업자 + 현재 신청 가능 + 서울 거주 + 만 ${MY_AGE}세 + 남성**(타 지역·청년·여성/장애인/보훈 등 전용 제외) 공고 (총 ${rows.length}건)`); out.push('> 신청을 마치면 `[ ]` → `[x]` 로 체크하세요. 갱신: `LD_LIBRARY_PATH=$ORACLE_IC_LIB_DIR node scripts/generate_checklist.js`'); out.push('> ⚠️ 마감 "시각"과 정확한 자격요건은 각 공고 원문에서 반드시 확인하세요.'); out.push(''); for (const [title, pred] of Object.entries(buckets)) { const group = rows.filter((x) => pred(x.DLEFT)); if (group.length === 0) continue; out.push(`## ${title} — ${group.length}건`); out.push(''); for (const x of group) out.push(line(x)); out.push(''); } const path = new URL('../docs/apply-checklist.md', import.meta.url); await writeFile(path, out.join('\n'), 'utf8'); console.log(`생성: ${rows.length}건 → government/docs/apply-checklist.md`);