gov-scraper: 마스터 사업계획서 + 공고 매칭/추출 스크립트 추가

- docs/business-plans.md: Tasteby/Lyricsy/Parents Story 3개 앱 PSST 사업계획서 초안
- scripts/match.js: 앱별 주제 키워드 매칭 조회
- scripts/eligible.js: 예비창업자 자격 + 현재 열린 공고 목록
- scripts/export_eligible_csv.js: 신청 추적용 CSV(exports/) 생성
- exports/ gitignore

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 07:24:39 +00:00
parent 82504e2261
commit cdce7b86bb
5 changed files with 268 additions and 0 deletions

View File

@@ -2,3 +2,5 @@ node_modules/
*.log
# DB 접속 net 설정(지갑 경로/접속 디스크립터) — 환경별 재생성
oracle-net/
# 생성 데이터(공고 추출 CSV 등) — 매일 갱신
exports/

View File

@@ -0,0 +1,91 @@
# 마스터 사업계획서 (3개 앱)
한국 정부 창업지원(예비창업패키지·창업사업화)의 표준 **PSST 구조**(문제인식–실현가능성–성장전략–팀구성)로 작성한 마스터 초안. 공고별로 가볍게 변형해 재사용한다.
> 표기 규칙: `[ ]`는 본인 정보·검증 수치를 채울 자리. 통계는 임의로 채우지 않았으니 제출 전 출처 확인 후 삽입할 것.
> 공통 정체성(상단에 쓰면 강함): **"LLM으로 비정형 데이터(영상·가사·구술)를 구조화하는 역량"** 을 핵심으로, 각 앱은 그 역량의 도메인별 응용.
---
## 1. Tasteby — 인플루언서 영상 기반 맛집 정보 검색 플랫폼
**한 줄 소개:** "유튜버·인플루언서가 다녀간 그 맛집, 어디였지?" — LLM이 영상 자막에서 식당 정보를 추출·구조화해 검색 가능하게 만드는 서비스.
**정부지원 트랙:** 관광테크 / 로컬데이터 / 데이터·AI바우처 / 소상공인 상생
### ① 문제인식 (Problem)
- 소비자는 "성시경 맛집", "쯔양 다녀온 곳"을 영상으로 보지만, **영상 속 정보는 검색·재방문이 불가능**하다. 영상은 휘발되고 위치·메뉴·영업정보는 흩어져 있다.
- 기존 맛집 앱(네이버·캐치테이블·다이닝코드)은 **광고·리뷰 기반**이라 신뢰도 논란이 있는 반면, **신뢰하는 인플루언서의 실제 방문**이라는 강력한 추천 신호는 어디에도 구조화돼 있지 않다.
- **목적:** 영상이라는 비정형 데이터를 신뢰 가능한 맛집 DB로 전환해, "내가 좋아하는 채널이 간 곳"을 검색·지도·예약으로 연결.
### ② 실현가능성 (Solution)
- **핵심 기술:** ⓐ 영상 자막(STT/transcript) 수집 → ⓑ **LLM으로 식당명·위치·메뉴·맥락 추출 및 정규화** → ⓒ 지도·검색 인덱싱.
- **차별성:** 점포 광고가 아닌 **인플루언서 방문 사실** 기반 / 채널·인물별 필터 / 영상 타임스탬프 연결.
- **개발 현황(강점):** **MVP + 백오피스 보유** — 신규 영상 자동 백필 파이프라인이 이미 동작. (심사에서 "작동하는 제품"으로 어필)
- **리스크 대응:** 영상 저작권·플랫폼 약관 이슈는 ▲원문 인용 최소화·**출처(채널) 명시 및 트래픽 환원** ▲사실정보(상호·주소) 위주 추출 ▲창작자 제휴(어필리에이트) 모델로 상생 구조 설계.
### ③ 성장전략 (Scale-up)
- **시장:** 국내 외식 O2O·맛집검색 [시장규모 수치 확인] / 1차 타깃 = 맛집 영상 소비층(2030).
- **수익모델:** ⓐ 식당 예약·웨이팅 제휴 수수료 ⓑ 인플루언서·채널 어필리에이트 ⓒ 지역/관광 데이터 B2B 판매 ⓓ 프리미엄(저장·알림).
- **추진일정:** 영상 커버리지 확대 → 지도/예약 연동 → 지역(관광) 특화 → B2B 데이터.
- **정부지원 연계:** 관광플러스테크·로컬 데이터·데이터바우처로 데이터 구축비 조달.
### ④ 팀구성 (Team)
- **대표:** [본인] — LLM 파이프라인·풀스택 개발 역량(자체 서비스 다수 구축 이력). **이미 MVP를 혼자 구현**한 실행력.
- **필요 역량/계획:** 외식 도메인 자문, 제휴영업. [공동창업자/멘토 계획].
---
## 2. Lyricsy — K-pop·팝 차트 기반 음악 언어학습 앱
**한 줄 소개:** 빌보드·멜론 차트를 좋아하는 노래로 **영어·한국어를 배우는** 앱 — 뮤직비디오 + 가사(한/영·발음·뜻) + 표현 플래시카드.
**정부지원 트랙:** 콘텐츠진흥원(콘진원)·문체부 한류 / 에듀테크 / 글로벌 진출
### ① 문제인식 (Problem)
- 전 세계 K-pop 팬은 **"가사 뜻을 알고 싶다 → 한국어를 배우고 싶다"**는 강한 동기를 갖지만, Duolingo류는 음악·문화 맥락이 없고, 가사 사이트는 학습 기능이 없다.
- 반대로 한국 사용자에게는 **팝송으로 영어 학습** 수요가 크다. **양방향(영↔한)**으로 음악을 학습 콘텐츠로 만든 서비스는 드물다.
- **목적:** "좋아하는 노래"라는 최강의 학습 동기를 발음·뜻·표현 학습으로 전환.
### ② 실현가능성 (Solution)
- **핵심 기능:** 차트 연동(개인화: 최애 아티스트/곡) → MV + **가사(원문·발음표기·뜻 병기)** → 표현을 **플래시카드(SRS)**로 반복 학습.
- **차별성:** 음악×양방향 언어학습×개인화. K-pop 글로벌 팬덤이라는 **거대 유입 동기** 보유.
- **개발 현황:** 차트·개인화·가사·플래시카드 **MVP 구현**.
- **리스크 대응(사업 핵심):** 가사 저작권은 신뢰도를 가르는 지점. ▲가사는 **LyricFind 등 합법 라이선스/한국음악저작권협회 신탁 경로**로 확보 ▲MV는 **공식 YouTube 임베드**(권리자 수익 보장) ▲차트 데이터는 약관 준수. → "법적 리스크를 인지하고 라이선스 기반으로 설계"를 명시해 감점이 아닌 **전문성**으로 전환.
### ③ 성장전략 (Scale-up)
- **시장:** 글로벌 언어학습 [시장규모 수치 확인] × K-콘텐츠 팬덤 → **해외 TAM이 본질**. SOM = 영어권·동남아 K-pop 학습자.
- **수익모델:** 구독(에듀테크 표준) + 아티스트별 콘텐츠팩 + 제휴(엔터/교육).
- **추진일정:** 가사 라이선스 1차 확보 → 학습 루프 고도화 → 글로벌(영어권) 출시 → 아티스트 IP 제휴.
- **정부지원 연계:** 콘진원 음악·콘텐츠 스타트업 육성, 문체부 K-콘텐츠 글로벌(엑스포·해외진출).
### ④ 팀구성 (Team)
- **대표:** [본인] — 풀스택·LLM·콘텐츠 파이프라인. 다수 앱 자체 구축 이력 → **빠른 실증 능력**.
- **필요 역량/계획:** 음악 라이선스/저작권 자문(법무), 언어교육 콘텐츠 자문.
---
## 3. Parents Story — 온디바이스 AI 자서전(부모님 이야기) 앱
**한 줄 소개:** 부모님께 인생의 시기별 질문을 던져 **말·사진으로 기억을 모으고**, 온디바이스 AI(Gemma 4)가 **한 권의 책처럼** 엮어주는 앱 — 클라우드 없이, 완전 프라이버시.
**정부지원 트랙:** 고령친화산업 / 온디바이스 AI / 사회문제 해결·사회적가치 / AI헬스케어
### ① 문제인식 (Problem)
- **초고령사회 한국** — 부모 세대의 인생 이야기는 기록되지 않은 채 사라지고, 자녀·후손은 "그때 더 여쭤볼걸"이라는 후회를 남긴다.
- 기존 자서전 서비스는 **비싸고(대필), 사생활을 외부에 맡겨야** 한다. 어르신은 **타이핑이 어렵고**, 민감한 가족사를 **클라우드에 올리길 꺼린다**.
- **목적:** 누구나 **말로** 기억을 남기고, **데이터가 폰을 떠나지 않게** 하면서, AI가 그것을 읽기 좋은 책으로.
### ② 실현가능성 (Solution)
- **핵심 기술:** 시기별 질문 → **음성 입력(어르신이 말로)** + 사진 → **온디바이스 Gemma 4**가 챕터/아웃라인으로 구조화·서술 → 책 형태 생성.
- **차별성(시의성):** 2026.4 출시된 **Gemma 4 E2B/E4B**는 ▲**네이티브 음성 입력**(어르신 접근성) ▲**비전·OCR**(옛 사진·편지 편입) ▲**256K 컨텍스트**(생애 전체를 일관된 책으로) ▲**~1GB·저전력**으로 보급형 폰 동작. → **"클라우드 없이 폰 안에서, 음성으로 자서전"**이 비로소 가능해진 것이 핵심 차별점.
- **프라이버시:** 전 과정 온디바이스 = 민감한 가족 이야기 유출 우려 원천 차단(곧 셀링포인트).
- **개발 현황:** 기획 단계 → **예비창업 단계에 적합**(아이디어 단계 허용 사업 타깃). 챕터 분할 생성으로 장문 일관성 확보.
### ③ 성장전략 (Scale-up)
- **시장:** 시니어테크 + 가족/추모 시장 [수치 확인]. 1차 = 부모님 선물을 원하는 4050 자녀.
- **수익모델:** 앱 인앱구매 + **실물 인쇄책 제작 연계(객단가↑)** + 가족 공유/추모 프리미엄.
- **추진일정:** 음성·사진 수집 UX → 온디바이스 책 생성 품질화 → 인쇄 연계 → 다국어.
- **정부지원 연계:** 고령친화산업 육성(복지부·보건산업진흥원), 사회문제해결형 창업, AI 온디바이스.
### ④ 팀구성 (Team)
- **대표:** [본인] — 온디바이스 LLM·앱 개발 역량, 다수 서비스 실증 이력.
- **필요 역량/계획:** 시니어 UX 자문, (인쇄 연계) 출판 파트너.

View File

@@ -0,0 +1,37 @@
// 예비창업자가 지원 가능(자격에 '예비창업' 명시)하고 현재 열린 공고 전체를 마감일 순으로.
// 실행: LD_LIBRARY_PATH=$ORACLE_IC_LIB_DIR node scripts/eligible.js
import { withConnection, closePool, oracledb } from '../src/db.js';
const SQL = `
SELECT * FROM (
SELECT title, category, agency, source_code, detail_url, apply_end,
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
FETCH FIRST 80 ROWS ONLY`;
await withConnection(async (conn) => {
const r = await conn.execute(
SQL,
{ k1: '예비창업', k2: '예비 창업' },
{ outFormat: oracledb.OUT_FORMAT_OBJECT }
);
console.log(`현재 신청가능(예비창업자 자격) 공고: ${r.rows.length}\n`);
for (const x of r.rows) {
const tag = x.DLEFT === 9999 ? '[상시]' : `[D-${x.DLEFT}]`;
console.log(
`${tag}\t${x.SOURCE_CODE}\t${(x.CATEGORY || '-').slice(0, 8)}\t${x.TITLE.slice(0, 52)}\t${x.DETAIL_URL}`
);
}
});
await closePool();

View File

@@ -0,0 +1,68 @@
// 예비창업자 자격 + 현재 열린 공고 전체를 CSV 로 내보낸다(신청 추적용).
// 실행: LD_LIBRARY_PATH=$ORACLE_IC_LIB_DIR node scripts/export_eligible_csv.js
import { writeFile, mkdir } 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_start, apply_end,
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 csvCell(v) {
if (v == null) return '';
const s = String(v).replace(/"/g, '""').replace(/\r?\n/g, ' ');
return `"${s}"`;
}
function ymd(d) {
return d ? new Date(d).toISOString().slice(0, 10) : '';
}
function region(title) {
const m = /^\[([^\]]{1,8})\]/.exec(title);
return m ? m[1] : '';
}
const rows = await withConnection(async (conn) => {
const r = await conn.execute(
SQL,
{ k1: '예비창업', k2: '예비 창업' },
{ outFormat: oracledb.OUT_FORMAT_OBJECT }
);
return r.rows;
});
await closePool();
const header = ['신청완료', 'D-day', '마감일', '지역', '분야', '주관기관', '소스', '제목', '링크'];
const lines = [header.map(csvCell).join(',')];
for (const x of rows) {
lines.push(
[
'',
x.DLEFT === 9999 ? '상시' : `D-${x.DLEFT}`,
x.DLEFT === 9999 ? '상시/예산소진' : ymd(x.APPLY_END),
region(x.TITLE),
x.CATEGORY || '',
x.AGENCY || '',
x.SOURCE_CODE,
x.TITLE,
x.DETAIL_URL || '',
]
.map(csvCell)
.join(',')
);
}
await mkdir(new URL('../exports/', import.meta.url), { recursive: true });
const out = new URL('../exports/eligible_opportunities.csv', import.meta.url);
// 엑셀 한글 깨짐 방지 BOM
await writeFile(out, '' + lines.join('\n'), 'utf8');
console.log(`내보냄: ${rows.length}건 → government/exports/eligible_opportunities.csv`);

View File

@@ -0,0 +1,70 @@
// 앱 후보별로 매칭되는 정부지원사업을 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();