o2o-site-ontology/scripts/build-dataset.mjs
hbyang 1addd7a270 스테이머뭄 시드를 실제 업체 정보로 교체
초기 시드의 스테이머뭄 프로필은 데모용으로 지어낸 값이었고
실제와 반대였다 (고군산군도 오션뷰 6객실 → 실제는 군산 원도심
신흥동 말랭이마을 인근 독채 2동, 기준 2인·최대 4인).

- 공개 정보로 확인된 항목만 반영, 미확인 항목은 profile.unverified 로 표시
- 데이터셋 어휘에 원도심 권역 보강 (말랭이마을·신흥동·영화동·근대문화역사거리·
  해망굴·뜬다리부두, 독채스테이/감성숙소/스테이 유형) — 말랭이마을이 누락돼 있었다
- 재적재 976건

매칭 결과가 오션뷰/고군산군도 계열에서
군산 원도심 독채펜션 / 군산 독채스테이 / 말랭이마을 근처 숙소 로 교정됨.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 15:32:07 +09:00

180 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* "군산 펜션" SEO/AEO 키워드·태그 데이터셋 1,000건 생성.
* node scripts/build-dataset.mjs → data/gunsan-pension-keywords.json
*
* 어휘는 실제 군산 지명·관광지·숙소 시설 용어로 구성했고,
* 패턴은 한국 로컬 숙박 검색에서 실제로 쓰이는 조합만 전개한다.
* 가치가 높은 순으로 방출하므로 1,000건에서 잘라도 상위 의도가 남는다.
*/
import { writeFileSync, mkdirSync } from 'node:fs';
// ──────────────────────────────────────────────── 어휘 (실제 군산 기반)
const REGION = '군산';
// 고군산군도·해안 권역
const ISLANDS = ['선유도', '무녀도', '장자도', '대장도', '신시도', '야미도', '고군산군도'];
// 시내·주요 권역
const AREAS = ['새만금', '비응항', '오식도', '은파호수공원', '월명동', '나운동', '수송동', '미룡동', '옥도면',
'군산 원도심', '신흥동', '영화동'];
// 관광지 (근처 숙소 검색의 앵커)
const SPOTS = [
'선유도해수욕장', '새만금방조제', '경암동 철길마을', '근대역사박물관', '초원사진관',
'이성당', '동국사', '진포해양테마공원', '신흥동 일본식가옥', '은파호수공원',
'월명공원', '금강하구둑', '철새조망대', '채만식문학관', '째보선창', '시간여행마을',
'말랭이마을', '군산 근대문화역사거리', '해망굴', '군산항 뜬다리부두',
];
// 숙소 유형
const STAY_CORE = ['펜션', '숙소', '풀빌라', '독채펜션', '스파펜션', '애견펜션', '감성펜션'];
const STAY_ALT = ['글램핑', '카라반', '캠핑장', '게스트하우스', '민박', '리조트', '한옥펜션', '촌집', '별장',
'독채스테이', '감성숙소', '스테이', '일본식가옥 숙소'];
// 검색 의도어
const INTENT_CORE = ['추천', '예약', '가격', '후기', '순위'];
const INTENT_MORE = ['저렴한곳', '가성비', '최저가', '실시간예약', '당일예약', '특가', '할인',
'위치', '주차', '전화번호', '체크인시간', '조식포함', '청소상태'];
// 동반자
const WITH_CORE = ['커플', '가족', '친구', '애견동반', '단체'];
const WITH_MORE = ['신혼', '아이동반', '유아동반', '부모님', '효도여행', '대학생', 'MT', '워크샵',
'회사', '태교여행', '혼자', '여자끼리', '4인', '6인', '10인', '20인'];
// 시설·특징
const FEAT_CORE = ['오션뷰', '바다뷰', '독채', '프라이빗', '스파', '자쿠지', '바베큐', '수영장'];
const FEAT_MORE = ['노을뷰', '일출뷰', '월풀', '온수풀', '야외수영장', '인피니티풀', '불멍', '화로대',
'넷플릭스', '빔프로젝터', '노래방', '파티룸', '복층', '테라스', '마당', '벽난로',
'애견운동장', '키즈룸', '트램폴린', '무료주차', '조식', '세미나실'];
// 시즌·행사
const SEASON = ['여름휴가', '물놀이', '해수욕', '겨울', '연말', '크리스마스', '신정', '설날', '추석',
'봄', '벚꽃', '가을', '단풍', '일출', '낙조', '불꽃놀이', '성수기', '비수기', '주말', '평일'];
const OCCASION = ['생일', '기념일', '결혼기념일', '프러포즈', '100일', '가족여행', '워크샵', '단합대회'];
// ──────────────────────────────────────────────── 방출
const rows = [];
const seen = new Set();
const INTENT_MAP = {
예약: 'transactional', 실시간예약: 'transactional', 당일예약: 'transactional',
가격: 'transactional', 최저가: 'transactional', 특가: 'transactional', 할인: 'transactional',
후기: 'informational', 순위: 'informational', 청소상태: 'informational', 체크인시간: 'informational',
};
const intentOf = (m) => INTENT_MAP[m] ?? 'local';
function add(keyword, { intent = 'local', kind = 'keyword', category, relevance }) {
const k = keyword.replace(/\s+/g, ' ').trim();
if (!k || seen.has(k)) return false;
seen.add(k);
rows.push({ keyword: k, intent, kind, category, relevance: Math.round(relevance * 100) / 100 });
return true;
}
// T1 — 코어: 지역 × 숙소유형 × 의도
for (const s of STAY_CORE) add(`${REGION} ${s}`, { category: '코어', relevance: 0.97 });
for (const s of STAY_CORE) for (const m of INTENT_CORE)
add(`${REGION} ${s} ${m}`, { intent: intentOf(m), category: '코어', relevance: 0.93 });
for (const s of STAY_ALT) add(`${REGION} ${s}`, { category: '코어', relevance: 0.86 });
for (const m of INTENT_MORE) add(`${REGION} 펜션 ${m}`, { intent: intentOf(m), category: '의도', relevance: 0.88 });
// T2 — 섬·권역 × 숙소유형
for (const g of [ISLANDS, AREAS]) for (const p of g) for (const s of STAY_CORE.slice(0, 4))
add(`${p} ${s}`, { category: '권역', relevance: g === ISLANDS ? 0.9 : 0.85 });
for (const p of ISLANDS) for (const m of INTENT_CORE)
add(`${p} 펜션 ${m}`, { intent: intentOf(m), category: '권역', relevance: 0.82 });
// T3 — 동반자
for (const w of WITH_CORE) {
add(`${REGION} ${w} 펜션`, { category: '동반자', relevance: 0.91 });
for (const m of INTENT_CORE) add(`${REGION} ${w} 펜션 ${m}`, { intent: intentOf(m), category: '동반자', relevance: 0.8 });
for (const s of STAY_CORE.slice(2, 6)) add(`${REGION} ${w} ${s}`, { category: '동반자', relevance: 0.78 });
}
for (const w of WITH_MORE) {
add(`${REGION} ${w} 펜션`, { category: '동반자', relevance: 0.82 });
add(`${REGION} ${w} 펜션 추천`, { category: '동반자', relevance: 0.75 });
add(`${REGION} ${w} 숙소`, { category: '동반자', relevance: 0.73 });
}
// T4 — 시설·특징
for (const f of FEAT_CORE) {
add(`${REGION} ${f} 펜션`, { category: '시설', relevance: 0.89 });
add(`${REGION} 펜션 ${f}`, { category: '시설', relevance: 0.76 });
for (const m of INTENT_CORE.slice(0, 3)) add(`${REGION} ${f} 펜션 ${m}`, { intent: intentOf(m), category: '시설', relevance: 0.72 });
}
for (const f of FEAT_MORE) {
add(`${REGION} ${f} 펜션`, { category: '시설', relevance: 0.79 });
add(`${REGION} 펜션 ${f}`, { category: '시설', relevance: 0.7 });
}
for (const p of ISLANDS.slice(0, 4)) for (const f of FEAT_CORE)
add(`${p} ${f} 펜션`, { category: '시설', relevance: 0.74 });
// T5 — 시즌·행사
for (const s of SEASON) {
add(`${REGION} ${s} 펜션`, { category: '시즌', relevance: 0.8 });
add(`${REGION} ${s} 펜션 예약`, { intent: 'transactional', category: '시즌', relevance: 0.71 });
add(`${s} ${REGION} 숙소`, { category: '시즌', relevance: 0.68 });
}
for (const o of OCCASION) {
add(`${REGION} ${o} 펜션`, { category: '시즌', relevance: 0.75 });
add(`${REGION} ${o} 펜션 추천`, { category: '시즌', relevance: 0.69 });
}
// T6 — 관광지 앵커
for (const sp of SPOTS) {
add(`${sp} 근처 펜션`, { category: '관광지', relevance: 0.83 });
add(`${sp} 근처 숙소`, { category: '관광지', relevance: 0.8 });
add(`${sp} 펜션 추천`, { category: '관광지', relevance: 0.72 });
add(`${sp} 가까운 숙소`, { category: '관광지', relevance: 0.66 });
}
// T7 — 태그 (칩 UI 용 짧은 패싯)
const TAGS = [...FEAT_CORE, ...FEAT_MORE, ...WITH_CORE, ...STAY_CORE, ...STAY_ALT,
'오션뷰객실', '반려동물동반', '금연객실', '엘리베이터', '와이파이', '취사가능',
'단체가능', '조용한', '신축', '리모델링', '뷰맛집', '인생샷'];
for (const t of TAGS) add(t, { kind: 'tag', category: '태그', relevance: 0.6 });
// T8 — 질문형 (AEO)
const Q = [];
for (const w of [...WITH_CORE, '아이', '부모님']) Q.push([`${REGION} ${w} 펜션 어디가 좋아요`, 'informational', 0.7]);
for (const f of FEAT_CORE) Q.push([`${REGION}${f} 펜션 있나요`, 'informational', 0.67]);
for (const p of ISLANDS.slice(0, 5)) {
Q.push([`${p} 펜션 어떻게 가나요`, 'informational', 0.64]);
Q.push([`${p} 숙소 예약 언제 해야 하나요`, 'informational', 0.6]);
}
Q.push([`${REGION} 펜션 1박 얼마인가요`, 'transactional', 0.72]);
Q.push([`${REGION} 펜션 바베큐 가능한가요`, 'informational', 0.7]);
Q.push([`${REGION} 펜션 체크인 몇시인가요`, 'informational', 0.66]);
Q.push([`${REGION} 펜션 주차 되나요`, 'informational', 0.66]);
Q.push([`${REGION} 애견동반 펜션 추가요금 있나요`, 'informational', 0.63]);
Q.push([`${REGION} 펜션 성수기 언제인가요`, 'informational', 0.61]);
Q.push([`선유도 들어가는 배 시간표`, 'informational', 0.55]);
Q.push([`${REGION} 여행 몇박이 좋을까요`, 'informational', 0.54]);
for (const [k, i, r] of Q) add(k, { intent: i, category: '질문형', relevance: r });
// T9 — 롱테일: 동반자 × 시설 / 권역 × 동반자 / 시즌 × 동반자
const LONGTAIL = [];
for (const w of WITH_CORE) for (const f of FEAT_CORE) LONGTAIL.push([`${REGION} ${w} ${f} 펜션`, 0.5]);
for (const p of ISLANDS) for (const w of WITH_CORE) LONGTAIL.push([`${p} ${w} 펜션`, 0.48]);
for (const s of SEASON) for (const w of WITH_CORE) LONGTAIL.push([`${REGION} ${s} ${w} 펜션`, 0.44]);
for (const f of FEAT_CORE) for (const f2 of FEAT_MORE) LONGTAIL.push([`${REGION} ${f} ${f2} 펜션`, 0.4]);
for (const a of AREAS) for (const f of FEAT_CORE) LONGTAIL.push([`${a} ${f} 펜션`, 0.42]);
for (const [k, r] of LONGTAIL) {
if (rows.length >= 1000) break;
add(k, { category: '롱테일', relevance: r });
}
const dataset = rows.slice(0, 1000);
mkdirSync('data', { recursive: true });
writeFileSync('data/gunsan-pension-keywords.json', JSON.stringify({
topic: '군산 펜션',
locale: 'ko-KR',
generatedBy: 'hand-authored vocabulary × search-pattern expansion',
count: dataset.length,
items: dataset,
}, null, 2) + '\n');
const by = (f) => dataset.reduce((a, r) => (a[r[f]] = (a[r[f]] ?? 0) + 1, a), {});
console.log(`✅ data/gunsan-pension-keywords.json ${dataset.length}`);
console.log(' 카테고리:', by('category'));
console.log(' 의도 :', by('intent'));
console.log(' 종류 :', by('kind'));