② 사전 보강 — 실측으로 찾은 어휘 공백을 메움
- 인원: 2인·3인 추가 (4/6/10/20인만 있어 기준 2인 업체가 노릴 키워드가 없었다)
- 단독 명사형: 군산 독채 / 스테이 / 풀빌라 (조합형만 있어 단독 검색을 놓쳤다)
- 분위기 11종(감성·조용한·사진찍기 좋은·인생샷 …), 여행형태 9종(뚜벅이·1박2일 …)
- scripts/import-related.ts — 검색광고 키워드도구 내려받기 병합 (파일 임포터)
③ 사전 오염 정리
- source='llm' 27건 삭제 (강남 미용실·해운대 한식당이 군산 펜션 사전에 있었다)
- 매칭 후보를 업체 업종으로 한정 + source IN ('dataset','manual') 만 조회
- 적재 시 데이터셋에서 빠진 행 삭제 — upsert 만 하면 재빌드마다 누적된다 (974 → 1072)
④ 고객 언어 레인 (w=0.9)
- hashtags + reviewSignals(원문 아닌 빈도 집계) 를 별도 레인으로
- 사업자 표현보다 검색어에 가까우므로 유형 다음으로 높게 잡음
- 스테이머뭄 데이터는 아직 없음 — 인스타는 로그인 월이라 스크래퍼가 채워야 함
⑤ 레인 토큰 중복 제거
- '신흥동' 과 '신흥동 일본식가옥' 이 별개 원소라 같은 낱말이 두 번 실렸다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
205 lines
12 KiB
JavaScript
205 lines
12 KiB
JavaScript
/**
|
||
* "군산 펜션" 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', '워크샵',
|
||
'회사', '태교여행', '혼자', '여자끼리', '2인', '3인', '4인', '6인', '10인', '20인'];
|
||
|
||
// 분위기·취향 — 감성 독채 스테이 계열에서 실제로 많이 쓰이는 수식어
|
||
const VIBE = ['감성', '조용한', '분위기 좋은', '예쁜', '사진찍기 좋은', '인생샷', '뷰맛집',
|
||
'깔끔한', '신축', '프라이빗한', '혼자 있기 좋은'];
|
||
|
||
// 여행 형태 — 숙소 검색은 '며칠/어떻게 다니는가'로도 갈린다
|
||
const TRAVEL = ['1박2일', '2박3일', '당일치기', '주말여행', '뚜벅이 여행', '혼행',
|
||
'워케이션', '무박', '한달살기'];
|
||
|
||
// 시설·특징
|
||
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 });
|
||
// 단독 명사형 — '군산 독채펜션' 만 있으면 '군산 독채' 검색을 놓친다
|
||
for (const n of ['독채', '스테이', '풀빌라', '민박', '한옥', '글램핑', '숙박'])
|
||
add(`${REGION} ${n}`, { category: '코어', relevance: 0.87 });
|
||
|
||
// 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 });
|
||
}
|
||
|
||
// T5.5 — 분위기·여행형태
|
||
for (const v of VIBE) {
|
||
add(`${REGION} ${v} 펜션`, { category: '분위기', relevance: 0.81 });
|
||
add(`${REGION} ${v} 숙소`, { category: '분위기', relevance: 0.78 });
|
||
add(`${REGION} ${v} 독채`, { category: '분위기', relevance: 0.7 });
|
||
}
|
||
for (const t of TRAVEL) {
|
||
add(`${REGION} ${t} 숙소`, { category: '여행형태', relevance: 0.76 });
|
||
add(`${REGION} ${t} 펜션 추천`, { category: '여행형태', relevance: 0.7 });
|
||
}
|
||
for (const v of VIBE.slice(0, 6)) for (const w of WITH_CORE.slice(0, 3))
|
||
add(`${REGION} ${w} ${v} 숙소`, { category: '분위기', relevance: 0.58 });
|
||
|
||
// 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,
|
||
'오션뷰객실', '반려동물동반', '금연객실', '엘리베이터', '와이파이', '취사가능',
|
||
'단체가능', '조용한', '신축', '리모델링', '뷰맛집', '인생샷',
|
||
...VIBE, ...TRAVEL, '2인전용', '소인원', '뚜벅이', '원도심'];
|
||
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'));
|