발행이 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>
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
/**
|
|
* 로컬 엔드투엔드 점검 스크립트.
|
|
* npm run db:reset && npm start (다른 터미널)
|
|
* npm run smoke
|
|
*/
|
|
const BASE = process.env.BASE_URL ?? 'http://localhost:3100';
|
|
|
|
const j = async (method: string, path: string, body?: unknown) => {
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
method,
|
|
headers: body ? { 'content-type': 'application/json' } : undefined,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const text = await res.text();
|
|
if (!res.ok) throw new Error(`${method} ${path} → ${res.status} ${text}`);
|
|
return text ? JSON.parse(text) : null;
|
|
};
|
|
|
|
const h = (t: string) => console.log(`\n\x1b[1m${t}\x1b[0m`);
|
|
|
|
async function main() {
|
|
h('0. health');
|
|
console.log(' ', await j('GET', '/health'));
|
|
|
|
h('1. site-1001 생성 (첫 업체 — 전부 신규)');
|
|
const a = await j('POST', '/v1/merchants/site-1001/generate?sync=true');
|
|
printStats(a);
|
|
|
|
h('2. site-1002 생성 (같은 강남/미용실 — 중복제거 발동)');
|
|
const b = await j('POST', '/v1/merchants/site-1002/generate?sync=true');
|
|
printStats(b);
|
|
printDetails(b);
|
|
|
|
h('3. publish 웹훅 + 표기 변형 (trigram 단계)');
|
|
const c = await j('POST', '/v1/merchants/publish', {
|
|
externalId: 'site-1003',
|
|
name: '강남 뷰티랩',
|
|
industryId: 'beauty.hair',
|
|
regionId: 'kr.seoul.gangnam',
|
|
description: '강남 미용실. 염색 전문.',
|
|
profile: { services: ['뿌리염색약', '여성펌'], features: ['주차가능'] },
|
|
sync: true,
|
|
});
|
|
printStats(c.generation);
|
|
printDetails(c.generation);
|
|
|
|
h('4. SEO payload');
|
|
const seo = await j('GET', '/v1/sites/site-1001/seo?limit=8');
|
|
console.log(' title :', seo.title);
|
|
console.log(' description:', seo.description);
|
|
console.log(' keywords :', seo.keywords.join(', '));
|
|
|
|
h('5. AEO payload');
|
|
const aeo = await j('GET', '/v1/sites/site-1001/aeo?limit=3');
|
|
for (const f of aeo.faqs) console.log(` Q. ${f.question}\n A. ${f.answer}`);
|
|
|
|
h('6. 의미 기반 키워드 검색');
|
|
const found = await j('POST', '/v1/keywords/search', { query: '강남 미용실 예약하고 싶어요', limit: 5 });
|
|
for (const r of found) console.log(` ${r.score.toFixed(3)} ${r.canonical} (${r.intent}, ${r.usage_count}개 업체)`);
|
|
|
|
h('7. 성과 피드백 → 저성과 강등');
|
|
console.log(
|
|
' ',
|
|
await j('POST', '/v1/sites/site-1001/performance', {
|
|
items: [
|
|
{ keyword: '강남 미용실 후기', impressions: 500, clicks: 0 },
|
|
{ keyword: '강남 미용실', impressions: 300, clicks: 40 },
|
|
],
|
|
}),
|
|
);
|
|
|
|
h('8. 비동기 큐 (BullMQ)');
|
|
console.log(' ', await j('POST', '/v1/merchants/site-2001/generate'));
|
|
for (let i = 0; i < 30; i++) {
|
|
const s = await j('GET', '/v1/sites/site-2001/seo?limit=5');
|
|
if (s.keywords.length) {
|
|
console.log(' 워커 처리 완료 →', s.keywords.join(', '));
|
|
break;
|
|
}
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
|
|
console.log('\n✅ smoke 완료');
|
|
}
|
|
|
|
function printStats(s: any) {
|
|
console.log(
|
|
` 후보 ${s.candidates} → 신규 ${s.created} / 중복(정확 ${s.matchedExact}, 표기 ${s.matchedTrigram}, 의미 ${s.matchedVector})` +
|
|
` / 차단 ${s.rejected} / 연결 ${s.linked} / QA ${s.qaCreated} (${s.durationMs}ms)`,
|
|
);
|
|
}
|
|
|
|
function printDetails(s: any) {
|
|
for (const d of s.details ?? []) {
|
|
const sim = d.similarity != null ? ` (sim=${d.similarity.toFixed(3)} → '${d.matchedTo}')` : '';
|
|
console.log(` ${d.action.padEnd(16)} ${d.candidate}${sim}`);
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error('\n❌', e.message);
|
|
process.exit(1);
|
|
});
|