// 앱 후보별로 매칭되는 정부지원사업을 gov_opportunity 에서 조회한다. // 실행: LD_LIBRARY_PATH=$ORACLE_IC_LIB_DIR node scripts/match.js import { withConnection, closePool, oracledb } from '../src/db.js'; // 앱별 주제 키워드(제목/분야/대상/주관기관 대상으로 LIKE) const APPS = { Tasteby: ['외식', '맛집', '음식', '푸드', '소상공인', '관광', '로컬', '지역특화', '상권', '데이터바우처', 'AI바우처', '빅데이터'], Lyricsy: ['콘텐츠', '한류', '케이팝', 'K-팝', '음악', '뮤직', '어학', '한국어', '에듀테크', '웹툰', '문화산업', '글로벌진출'], ParentsStory: ['시니어', '고령', '노인', '실버', '돌봄', '복지', '사회적가치', '사회문제', '헬스케어', '웰니스', '온디바이스', '기록'], }; async function queryApp(conn, keywords) { // 키워드 OR 조건 (title/category/target/agency) const binds = {}; const ors = keywords.map((kw, i) => { binds[`k${i}`] = `%${kw}%`; return `(title LIKE :k${i} OR category LIKE :k${i} OR target LIKE :k${i} OR agency LIKE :k${i})`; }); const sql = ` SELECT * FROM ( SELECT title, category, agency, source_code, detail_url, apply_end, CASE WHEN apply_end IS NULL OR apply_end >= TRUNC(SYSDATE) THEN 1 ELSE 0 END AS is_open, 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 ${ors.join(' OR ')} ) WHERE rn = 1 ORDER BY is_open DESC, dleft ASC FETCH FIRST 14 ROWS ONLY`; const r = await conn.execute(sql, binds, { outFormat: oracledb.OUT_FORMAT_OBJECT }); return r.rows; } async function countOpen(conn, keywords) { const binds = {}; const ors = keywords.map((kw, i) => { binds[`k${i}`] = `%${kw}%`; return `(title LIKE :k${i} OR category LIKE :k${i} OR target LIKE :k${i} OR agency LIKE :k${i})`; }); const r = await conn.execute( `SELECT COUNT(DISTINCT title) total, COUNT(DISTINCT CASE WHEN apply_end IS NULL OR apply_end >= TRUNC(SYSDATE) THEN title END) open_now FROM gov_opportunity WHERE ${ors.join(' OR ')}`, binds, { outFormat: oracledb.OUT_FORMAT_OBJECT } ); return r.rows[0]; } function fmtDate(d) { if (!d) return '상시/별도'; return new Date(d).toISOString().slice(0, 10); } await withConnection(async (conn) => { for (const [app, keywords] of Object.entries(APPS)) { const cnt = await countOpen(conn, keywords); console.log(`\n##### ${app} — 주제 매칭 총 ${cnt.TOTAL}건 (현재 신청가능 ${cnt.OPEN_NOW}건) #####`); const rows = await queryApp(conn, keywords); for (const r of rows) { const tag = r.IS_OPEN === 1 ? (r.DLEFT === 9999 ? '[상시]' : `[D-${r.DLEFT}]`) : '[마감]'; console.log( `${tag}\t${r.SOURCE_CODE}\t${r.CATEGORY || '-'}\t${(r.AGENCY || '-').slice(0, 16)}\t${r.TITLE.slice(0, 50)}\t${r.DETAIL_URL || '-'}` ); } } }); await closePool();