bizinfo.js•8.77 kB
import { URLSearchParams } from "node:url";
export const API_URL = "https://www.bizinfo.go.kr/uss/rss/bizinfoApi.do";
export const DEFAULT_API_KEY = "QP6yn2";
export const REGION_ALIASES = Object.freeze({
"서울특별시": "서울",
"서울": "서울",
"부산광역시": "부산",
"부산": "부산",
"대구광역시": "대구",
"대구": "대구",
"인천광역시": "인천",
"인천": "인천",
"광주광역시": "광주",
"광주": "광주",
"대전광역시": "대전",
"대전": "대전",
"울산광역시": "울산",
"울산": "울산",
"세종특별자치시": "세종",
"세종": "세종",
"경기도": "경기도",
"경기": "경기도",
"강원특별자치도": "강원도",
"강원도": "강원도",
"강원": "강원도",
"충청북도": "충청북도",
"충북": "충청북도",
"청주시": "청주",
"청주": "청주",
"충청남도": "충청남도",
"충남": "충청남도",
"전라북도": "전라북도",
"전북": "전라북도",
"전라남도": "전라남도",
"전남": "전라남도",
"경상북도": "경상북도",
"경북": "경상북도",
"경상남도": "경상남도",
"경남": "경상남도",
"제주특별자치도": "제주특별자치도",
"제주도": "제주특별자치도",
"제주": "제주특별자치도",
});
export const TARGET_ALIASES = Object.freeze({
"소상공인": "소상공인",
"중소기업": "중소기업",
"중견기업": "중견기업",
"벤처기업": "벤처기업",
"창업기업": "창업기업",
"예비창업자": "예비창업자",
"청년": "청년",
"여성": "여성",
"농업인": "농업인",
"어업인": "어업인",
"자영업자": "자영업자",
});
const DEFAULT_TARGET = undefined;
const BASE_URL = "https://www.bizinfo.go.kr";
export function getApiKey() {
return process.env.BIZINFO_API_KEY && process.env.BIZINFO_API_KEY.trim()
? process.env.BIZINFO_API_KEY.trim()
: DEFAULT_API_KEY;
}
function normalize(value) {
return (value ?? "").toString().trim();
}
function containsText(haystack, needle) {
if (!haystack || !needle) {
return false;
}
return haystack.includes(needle);
}
function normalizeKorean(text) {
return (text ?? "").replace(/\s+/g, "");
}
function findAlias(text, mapping) {
if (!text) return undefined;
const normalized = normalizeKorean(text);
for (const [alias, canonical] of Object.entries(mapping)) {
if (normalized.includes(normalizeKorean(alias))) {
return canonical;
}
}
return undefined;
}
function extractKeywords(text) {
if (!text) return undefined;
const match = text.match(/(?:키워드|검색어|분야)\s*(?:은|는|이|가|:|=)?\s*([가-힣0-9,\s]+)/i);
if (!match) return undefined;
const words = match[1]
.split(/[\s,]+/)
.map((w) => w.trim())
.filter(Boolean);
return words.length ? words.join(",") : undefined;
}
function extractEnglishKeywords(text) {
if (!text) return undefined;
const tokens = text.match(/[A-Za-z]+/g);
if (!tokens) return undefined;
const unique = [];
const seen = new Set();
for (const token of tokens) {
const upper = token.toUpperCase();
if (!seen.has(upper)) {
seen.add(upper);
unique.push(upper);
}
}
return unique.length ? unique.join(",") : undefined;
}
function extractNumbers(text) {
let page = 1;
let pageSize = 20;
if (!text) return { page, pageSize };
const pageMatch = text.match(/(\d+)\s*(?:페이지|page)/i);
if (pageMatch) {
page = Math.max(1, Number.parseInt(pageMatch[1], 10));
}
const sizeMatch = text.match(/(\d+)\s*(?:개|건)/);
if (sizeMatch) {
const parsed = Number.parseInt(sizeMatch[1], 10);
pageSize = Math.min(50, Math.max(1, parsed));
}
return { page, pageSize };
}
export function parseNaturalQuery(prompt) {
const region = findAlias(prompt, REGION_ALIASES);
let target = findAlias(prompt, TARGET_ALIASES);
if (!target) {
const match = prompt.match(/(?:대상|지원대상)\s*(?:은|는|이|가|:|=)?\s*([가-힣0-9,\s]+)/);
if (match) {
target = findAlias(match[1], TARGET_ALIASES);
}
}
let keywords = extractKeywords(prompt);
if (!keywords) {
keywords = extractEnglishKeywords(prompt);
}
const { page, pageSize } = extractNumbers(prompt);
let resolvedRegion = region;
if (!resolvedRegion) {
const regionMatch = prompt.match(/([가-힣]+(?:특별자치시|특별자치도|광역시|특별시|자치시|자치도|시|군|구|도))/);
if (regionMatch) {
const rawRegion = regionMatch[1];
resolvedRegion = REGION_ALIASES[rawRegion] ?? rawRegion;
}
}
return {
region: resolvedRegion,
target: target ?? DEFAULT_TARGET,
keywords,
page,
pageSize,
};
}
async function fetchBizinfo({ region, page, pageSize }) {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error("BIZINFO_API_KEY가 설정되어 있지 않습니다.");
}
const params = new URLSearchParams({
crtfcKey: apiKey,
dataType: "json",
pageIndex: String(page),
pageUnit: String(pageSize),
});
if (region) {
params.set("hashtags", region);
}
const response = await fetch(`${API_URL}?${params.toString()}`);
if (!response.ok) {
throw new Error(`기업마당 API 호출 실패 (${response.status})`);
}
const data = await response.json();
if (data && typeof data === "object" && data !== null && "jsonArray" in data) {
return data.jsonArray;
}
return data;
}
function toArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
function postFilter(items, { region, target, keywords }) {
const keywordList = (keywords ?? "")
.split(",")
.map((kw) => kw.trim())
.filter(Boolean);
return (items ?? []).reduce((acc, raw) => {
const item = raw ?? {};
const title = normalize(item.title) || normalize(item.pblancNm);
const desc = normalize(item.description) || normalize(item.bsnsSumryCn);
const tags = normalize(item.hashTags);
const targetName = normalize(item.trgetNm);
const regionHit = [tags, title, desc].some((field) => containsText(field, region));
if (!regionHit) {
return acc;
}
let targetHit = true;
if (target) {
targetHit = [targetName, title, desc, tags].some((field) => containsText(field, target));
}
let keywordHit = true;
if (keywordList.length) {
keywordHit = keywordList.every((kw) => [title, desc, tags].some((field) => containsText(field, kw)));
}
if (targetHit && keywordHit) {
const link = normalize(item.link) || normalize(item.pblancUrl);
acc.push({
title,
link: link.startsWith("http") ? link : link ? `${BASE_URL}${link}` : "",
pblancId: item.seq ?? item.pblancId ?? "",
agency: normalize(item.author) || normalize(item.jrsdInsttNm),
executor: normalize(item.excInsttNm),
category: normalize(item.lcategory) || normalize(item.pldirSportRealmLclasCodeNm),
pubDate: normalize(item.pubDate) || normalize(item.creatPnttm),
period: normalize(item.reqstDt) || normalize(item.reqstBeginEndDe),
target: targetName,
tags,
summary: desc,
});
}
return acc;
}, []);
}
export function formatBizinfoResults(items, { region, target, keywords }) {
if (!items.length) {
return `검색 결과가 없습니다. (region='${region}', target='${target ?? ""}', keywords='${keywords ?? ""}')`;
}
const lines = items.slice(0, 10).map((item, index) => (
`${index + 1}. ${item.title} \n` +
` - 기관: ${item.agency || ""} / 수행기관: ${item.executor || ""} / 분류: ${item.category || ""} \n` +
` - 모집기간: ${item.period || ""} / 등록일: ${item.pubDate || ""} \n` +
` - 대상: ${item.target || ""} \n` +
` - 링크: ${item.link || ""} \n`
));
return lines.join("\n");
}
export async function searchBizinfo({ region, target, keywords, page = 1, pageSize = 20 }) {
if (!region) {
throw new Error("region 파라미터가 필요합니다.");
}
const raw = await fetchBizinfo({ region, page, pageSize });
const items = Array.isArray(raw?.item) || Array.isArray(raw)
? toArray(raw.item ?? raw)
: toArray(raw?.item ?? raw);
return postFilter(items, { region, target, keywords });
}
export async function searchBizinfoText(params) {
const results = await searchBizinfo(params);
return formatBizinfoResults(results, params);
}
export async function searchBizinfoNaturalText(prompt) {
const params = parseNaturalQuery(prompt ?? "");
if (!params.region) {
return "지역을 인식하지 못했습니다. 예: '서울', '부산', '충청남도'";
}
return searchBizinfoText(params);
}