o2o-site-AEO/ontology/src/db/seed.ts
Mina Choi 01098835e9 [chore] docker-compose,ontology: 온톨로지를 이 레포로 들여 compose 한 벌로 띄운다 — 앱 Dockerfile 신설
발행이 SiteOntology 를 부르는데 서버는 따로 띄워야 했다. 실측(2026-09-14): 서버가 없으면
`[seo] SiteOntology 실패 — 키워드 없이 발행: ConnectError` 로 빌드는 성공하고 메타만 빈다 —
화면으로는 안 보이는 종류다. 한 벌로 묶어 "코드는 올라갔는데 서버가 없는" 상태를 없앤다.

- ontology/: gitea.o2o.kr/Web4ai/o2o-site-ontology 를 이 레포로 편입(그 원격은 그대로 남는다)
- ontology/Dockerfile(신규): 베이스는 node:22-slim. alpine 은 임베딩 런타임(onnxruntime)이
  musl 바이너리를 안 줘서 적재가 ERR_DLOPEN_FAILED 로 죽는다 — 빌드는 성공하고 실행에서만 터진다
- docker-compose.yml: ontology · ontology-postgres(pgvector) · ontology-redis 추가.
  자체 DB 를 쓰는 이유는 pgvector 확장 때문이다 — web4ai_db 를 남의 서비스 확장에 묶지 않는다
- 임베딩 모델(120MB)은 이미지에 굽지 않고 볼륨(ontology-model)에 남긴다
- 컨테이너끼리는 `http://ontology:3100` 으로 만난다. `.env` 의 127.0.0.1 은 컨테이너 자기 자신이라 안 닿는다

검증: 3개 기동 · 백엔드 컨테이너에서 ontology:3100/demo HTTP 200 · 마이그레이션·시드 완료

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 17:41:03 +09:00

120 lines
4.6 KiB
TypeScript

import { asJson, createSql } from './db';
const industries = [
['beauty', 'beauty', '뷰티'],
['beauty.hair', 'beauty.hair', '미용실'],
['beauty.nail', 'beauty.nail', '네일샵'],
['food', 'food', '음식점'],
['food.korean', 'food.korean', '한식당'],
['health', 'health', '의료'],
['health.dental', 'health.dental', '치과'],
['stay', 'stay', '숙박'],
['stay.pension', 'stay.pension', '펜션'],
];
const regions = [
['kr', 'kr', '대한민국'],
['kr.seoul', 'kr.seoul', '서울'],
['kr.seoul.gangnam', 'kr.seoul.gangnam', '강남'],
['kr.seoul.mapo', 'kr.seoul.mapo', '마포'],
['kr.busan', 'kr.busan', '부산'],
['kr.busan.haeundae', 'kr.busan.haeundae', '해운대'],
['kr.jeonbuk', 'kr.jeonbuk', '전북'],
['kr.jeonbuk.gunsan', 'kr.jeonbuk.gunsan', '군산'],
];
const merchants = [
{
externalId: 'site-1001',
name: '레브살롱',
industryId: 'beauty.hair',
regionId: 'kr.seoul.gangnam',
description: '강남역 3번 출구 앞 프라이빗 헤어살롱. 1:1 디자이너 전담 시스템.',
siteUrl: 'https://rev-salon.example.com',
profile: {
services: ['남자 커트', '여성 펌', '뿌리염색', '두피 클리닉'],
features: ['주차 가능', '심야 영업', '예약제'],
priceRange: '30,000~120,000원',
},
},
{
externalId: 'site-1002',
name: '헤어랩 강남점',
industryId: 'beauty.hair',
regionId: 'kr.seoul.gangnam',
description: '강남 대형 헤어샵. 염색과 클리닉 전문.',
siteUrl: 'https://hairlab.example.com',
profile: {
services: ['뿌리 염색', '여성 펌', '두피클리닉'],
features: ['주차가능', '단체 예약'],
priceRange: '25,000~150,000원',
},
},
{
externalId: 'site-2001',
name: '해운대 소담한상',
industryId: 'food.korean',
regionId: 'kr.busan.haeundae',
description: '해운대 해변 인근 한정식집. 제철 해산물 코스 제공.',
siteUrl: 'https://sodam.example.com',
profile: {
services: ['한정식 코스', '점심 특선', '단체 예약'],
features: ['오션뷰', '룸 완비', '발렛파킹'],
priceRange: '25,000~80,000원',
},
},
{
// 실제 업체. 공개 정보로 확인된 항목만 넣는다.
// 확인됨 : 상호, 군산 원도심(신흥동 말랭이마을 인근), 독채 2개 동, 기준 2인·최대 4인
// 미확인 : 가격, 바베큐/스파/주차/애견동반 여부 ← 사업자 확인 후 채울 것
externalId: 'site-3001',
name: '스테이머뭄',
industryId: 'stay.pension',
regionId: 'kr.jeonbuk.gunsan',
description:
'군산 원도심 말랭이마을 옆에 자리한 독채 스테이. A동·B동 두 채를 통째로 쓰며 기준 2인, 최대 4인.',
siteUrl: 'https://www.instagram.com/staymeomoom/',
profile: {
services: ['독채 대여', 'A동', 'B동'],
features: ['독채', '프라이빗', '2인 기준', '최대 4인', '원도심', '감성숙소'],
audiences: ['커플', '친구', '가족', '혼자'],
nearby: ['말랭이마을', '신흥동 일본식가옥', '동국사', '초원사진관', '이성당',
'경암동 철길마을', '근대역사박물관', '월명공원', '시간여행마을'],
address: '전북특별자치도 군산시 절골길 18 (신흥동)',
capacity: { standard: 2, max: 4 },
buildings: 2,
unverified: ['가격', '바베큐', '스파', '주차', '애견동반'],
},
},
];
async function main() {
const sql = createSql();
for (const [id, path, name] of industries) {
await sql`INSERT INTO industry (id, path, name) VALUES (${id}, ${path}::ltree, ${name})
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name`;
}
for (const [id, path, name] of regions) {
await sql`INSERT INTO region (id, path, name) VALUES (${id}, ${path}::ltree, ${name})
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name`;
}
for (const m of merchants) {
await sql`
INSERT INTO merchant (external_id, name, industry_id, region_id, description, profile, site_url)
VALUES (${m.externalId}, ${m.name}, ${m.industryId}, ${m.regionId},
${m.description}, ${sql.json(asJson(m.profile))}, ${m.siteUrl})
ON CONFLICT (external_id) DO UPDATE SET
name = EXCLUDED.name, description = EXCLUDED.description,
profile = EXCLUDED.profile, updated_at = now()`;
}
await sql.end();
console.log(`✅ seed: industry=${industries.length} region=${regions.length} merchant=${merchants.length}`);
}
main().catch((e) => {
console.error('❌ seed failed:', e);
process.exit(1);
});