전국 지역별 펜션 키워드 데이터셋 + 엑셀 산출
기획 변경: 군산 단일 지역 → 전국 지역별. - data/regions.json — 전국 54개 펜션 수요 지역 마스터 시도/지역/지역키/성격(해변·산간·호수·강변·도심·섬·계곡)/대표 관광지 202곳 복합 지명은 별칭으로 분리 (보령·대천 → 대천 + 별칭 보령) - scripts/build-nationwide-dataset.mjs — 지역 성격에 맞는 시설·시즌만 전개 평창·무주 오션뷰 0건, 태안·거제 산뷰 0건으로 검증 - scripts/export-xlsx.py — 4개 시트 엑셀 (키워드/지역마스터/지역별요약/사용가이드) - npm run dataset:nationwide 7,138건 / 지역당 평균 110건. 군산 974건을 54개 지역에 곱하면 5만 건이 되지만, 단일 지역 검증에서 저장분의 89%가 미사용이었으므로 티어를 매겨 눌렀다. 검색량·경쟁도 열은 비워 두었다 — 키워드도구로 채운 뒤 월 10 미만을 걷어내야 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
25f2cc871d
commit
df8ce5f117
22
README.md
22
README.md
@ -64,7 +64,27 @@ OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
||||
"고군산군도 오션뷰" 설정으로는 상위 매칭이 전부 `오션뷰 / 고군산군도` 로 나왔고,
|
||||
실제 값(원도심 신흥동, 독채 2동)으로 고치자 `군산 원도심 독채펜션 / 군산 독채스테이` 로 바뀌었다.
|
||||
|
||||
## 데이터셋
|
||||
## 전국 지역별 데이터셋 (기획 변경분)
|
||||
|
||||
`data/전국_펜션_SEO_AEO_키워드.xlsx` — 전국 54개 펜션 수요 지역 × **7,138건**.
|
||||
`npm run dataset:nationwide` 로 재생성한다 (`data/regions.json` → JSON → 엑셀).
|
||||
|
||||
시트 4개: `키워드` / `지역마스터` / `지역별요약` / `사용가이드`
|
||||
|
||||
**조합 폭발을 하지 않았다.** 군산 단일 지역 974건을 54개에 곱하면 5만 건이 되는데,
|
||||
단일 지역 검증에서 저장분의 89%가 한 번도 쓰이지 않았다. 지역당 ~110건으로 눌렀다.
|
||||
|
||||
**지역 성격이 시설 키워드를 결정한다.** `regions.json` 의 `type`(해변·산간·호수·강변·도심·섬·계곡)에
|
||||
따라 유효한 시설만 전개한다 — 평창·무주에는 오션뷰 키워드가 0건, 태안·거제에는 산뷰가 0건이다.
|
||||
|
||||
**티어** — 주력 568 / 보조 3,816 / 롱테일 1,836 / 태그 918.
|
||||
주력은 페이지당 1개만 쓰는 대표 키워드 후보다.
|
||||
|
||||
⚠ **이 키워드는 검색 패턴 생성물이지 실제 검색 데이터가 아니다.**
|
||||
엑셀의 `월간검색수`·`경쟁도` 열은 비워 두었다. 네이버 검색광고 키워드도구로 채운 뒤
|
||||
월 10 미만을 걷어내야 실제로 쓸 수 있다.
|
||||
|
||||
## 데이터셋 (군산 단일 지역 · 매칭 엔진용)
|
||||
|
||||
`data/gunsan-pension-keywords.json` — "군산 펜션" 주제로 직접 작성한 1,000건.
|
||||
실제 군산 지명(선유도·고군산군도·새만금·은파호수공원·경암동 철길마을 …)과
|
||||
|
||||
85665
data/nationwide-pension-keywords.json
Normal file
85665
data/nationwide-pension-keywords.json
Normal file
File diff suppressed because it is too large
Load Diff
798
data/regions.json
Normal file
798
data/regions.json
Normal file
@ -0,0 +1,798 @@
|
||||
{
|
||||
"note": "전국 펜션 수요 지역 마스터. type 은 그 지역에서 유효한 시설 키워드를 결정한다 (해변→오션뷰, 산간→계곡뷰 등). spots 는 실제 대표 관광지. aliases 는 같은 지역을 가리키는 다른 검색 표기 (예: 대천/보령).",
|
||||
"regions": [
|
||||
{
|
||||
"sido": "경기",
|
||||
"name": "가평",
|
||||
"key": "kr.gyeonggi.gapyeong",
|
||||
"type": [
|
||||
"호수",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"남이섬",
|
||||
"쁘띠프랑스",
|
||||
"아침고요수목원",
|
||||
"자라섬",
|
||||
"청평호"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경기",
|
||||
"name": "양평",
|
||||
"key": "kr.gyeonggi.yangpyeong",
|
||||
"type": [
|
||||
"강변",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"두물머리",
|
||||
"세미원",
|
||||
"용문사"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경기",
|
||||
"name": "포천",
|
||||
"key": "kr.gyeonggi.pocheon",
|
||||
"type": [
|
||||
"호수",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"산정호수",
|
||||
"포천아트밸리",
|
||||
"허브아일랜드"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경기",
|
||||
"name": "파주",
|
||||
"key": "kr.gyeonggi.paju",
|
||||
"type": [
|
||||
"도심",
|
||||
"강변"
|
||||
],
|
||||
"spots": [
|
||||
"헤이리예술마을",
|
||||
"임진각",
|
||||
"프로방스마을"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "인천",
|
||||
"name": "강화",
|
||||
"key": "kr.incheon.ganghwa",
|
||||
"type": [
|
||||
"해변",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"마니산",
|
||||
"동막해변",
|
||||
"강화고인돌",
|
||||
"전등사"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "인천",
|
||||
"name": "을왕리",
|
||||
"key": "kr.incheon.yeongjong",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"을왕리해수욕장",
|
||||
"무의도",
|
||||
"하나개해수욕장"
|
||||
],
|
||||
"aliases": [
|
||||
"영종도"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "춘천",
|
||||
"key": "kr.gangwon.chuncheon",
|
||||
"type": [
|
||||
"호수",
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"소양강스카이워크",
|
||||
"김유정역",
|
||||
"의암호",
|
||||
"남이섬"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "홍천",
|
||||
"key": "kr.gangwon.hongcheon",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"비발디파크",
|
||||
"은행나무숲",
|
||||
"홍천강"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "인제",
|
||||
"key": "kr.gangwon.inje",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"자작나무숲",
|
||||
"내린천",
|
||||
"백담사"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "평창",
|
||||
"key": "kr.gangwon.pyeongchang",
|
||||
"type": [
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"대관령",
|
||||
"오대산",
|
||||
"월정사",
|
||||
"알펜시아",
|
||||
"양떼목장"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "정선",
|
||||
"key": "kr.gangwon.jeongseon",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"하이원리조트",
|
||||
"레일바이크",
|
||||
"병방치스카이워크"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "강릉",
|
||||
"key": "kr.gangwon.gangneung",
|
||||
"type": [
|
||||
"해변",
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"경포대",
|
||||
"안목해변",
|
||||
"정동진",
|
||||
"주문진",
|
||||
"오죽헌"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "속초",
|
||||
"key": "kr.gangwon.sokcho",
|
||||
"type": [
|
||||
"해변",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"설악산",
|
||||
"대포항",
|
||||
"영금정",
|
||||
"아바이마을",
|
||||
"속초해수욕장"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "양양",
|
||||
"key": "kr.gangwon.yangyang",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"낙산사",
|
||||
"죽도해변",
|
||||
"하조대",
|
||||
"서피비치",
|
||||
"인구해변"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "고성",
|
||||
"key": "kr.gangwon.goseong",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"송지호",
|
||||
"화진포",
|
||||
"통일전망대",
|
||||
"백섬해상전망대"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "동해",
|
||||
"key": "kr.gangwon.donghae",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"추암촛대바위",
|
||||
"망상해수욕장",
|
||||
"무릉계곡"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "삼척",
|
||||
"key": "kr.gangwon.samcheok",
|
||||
"type": [
|
||||
"해변",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"장호항",
|
||||
"환선굴",
|
||||
"맹방해변"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "강원",
|
||||
"name": "태백",
|
||||
"key": "kr.gangwon.taebaek",
|
||||
"type": [
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"태백산",
|
||||
"검룡소",
|
||||
"365세이프타운"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충북",
|
||||
"name": "단양",
|
||||
"key": "kr.chungbuk.danyang",
|
||||
"type": [
|
||||
"산간",
|
||||
"강변"
|
||||
],
|
||||
"spots": [
|
||||
"도담삼봉",
|
||||
"만천하스카이워크",
|
||||
"고수동굴",
|
||||
"단양강잔도"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충북",
|
||||
"name": "제천",
|
||||
"key": "kr.chungbuk.jecheon",
|
||||
"type": [
|
||||
"호수",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"청풍호",
|
||||
"의림지",
|
||||
"배론성지"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충북",
|
||||
"name": "충주",
|
||||
"key": "kr.chungbuk.chungju",
|
||||
"type": [
|
||||
"호수",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"탄금대",
|
||||
"수안보온천",
|
||||
"충주호"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충북",
|
||||
"name": "괴산",
|
||||
"key": "kr.chungbuk.goesan",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"산막이옛길",
|
||||
"화양구곡"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충남",
|
||||
"name": "태안",
|
||||
"key": "kr.chungnam.taean",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"만리포해수욕장",
|
||||
"꽃지해변",
|
||||
"안면도",
|
||||
"신두리해안사구",
|
||||
"청포대"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충남",
|
||||
"name": "대천",
|
||||
"key": "kr.chungnam.boryeong",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"대천해수욕장",
|
||||
"무창포",
|
||||
"죽도상화원",
|
||||
"성주산"
|
||||
],
|
||||
"aliases": [
|
||||
"보령"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충남",
|
||||
"name": "서산",
|
||||
"key": "kr.chungnam.seosan",
|
||||
"type": [
|
||||
"해변",
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"해미읍성",
|
||||
"간월암",
|
||||
"개심사"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충남",
|
||||
"name": "공주",
|
||||
"key": "kr.chungnam.gongju",
|
||||
"type": [
|
||||
"도심",
|
||||
"강변"
|
||||
],
|
||||
"spots": [
|
||||
"공산성",
|
||||
"무령왕릉",
|
||||
"마곡사"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "충남",
|
||||
"name": "부여",
|
||||
"key": "kr.chungnam.buyeo",
|
||||
"type": [
|
||||
"도심",
|
||||
"강변"
|
||||
],
|
||||
"spots": [
|
||||
"궁남지",
|
||||
"부소산성",
|
||||
"백제문화단지"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전북",
|
||||
"name": "군산",
|
||||
"key": "kr.jeonbuk.gunsan",
|
||||
"type": [
|
||||
"해변",
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"선유도",
|
||||
"고군산군도",
|
||||
"말랭이마을",
|
||||
"경암동 철길마을",
|
||||
"근대역사박물관",
|
||||
"은파호수공원",
|
||||
"새만금"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전북",
|
||||
"name": "변산",
|
||||
"key": "kr.jeonbuk.buan",
|
||||
"type": [
|
||||
"해변",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"채석강",
|
||||
"변산해수욕장",
|
||||
"내소사",
|
||||
"격포항"
|
||||
],
|
||||
"aliases": [
|
||||
"부안"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전북",
|
||||
"name": "전주",
|
||||
"key": "kr.jeonbuk.jeonju",
|
||||
"type": [
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"전주한옥마을",
|
||||
"경기전",
|
||||
"전동성당",
|
||||
"남부시장"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전북",
|
||||
"name": "무주",
|
||||
"key": "kr.jeonbuk.muju",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"덕유산",
|
||||
"무주리조트",
|
||||
"반디랜드",
|
||||
"구천동계곡"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전북",
|
||||
"name": "남원",
|
||||
"key": "kr.jeonbuk.namwon",
|
||||
"type": [
|
||||
"도심",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"광한루원",
|
||||
"지리산",
|
||||
"춘향테마파크"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전남",
|
||||
"name": "여수",
|
||||
"key": "kr.jeonnam.yeosu",
|
||||
"type": [
|
||||
"해변",
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"오동도",
|
||||
"돌산대교",
|
||||
"향일암",
|
||||
"여수해상케이블카",
|
||||
"낭만포차"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전남",
|
||||
"name": "순천",
|
||||
"key": "kr.jeonnam.suncheon",
|
||||
"type": [
|
||||
"도심",
|
||||
"습지"
|
||||
],
|
||||
"spots": [
|
||||
"순천만습지",
|
||||
"낙안읍성",
|
||||
"순천만국가정원"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전남",
|
||||
"name": "담양",
|
||||
"key": "kr.jeonnam.damyang",
|
||||
"type": [
|
||||
"도심",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"죽녹원",
|
||||
"메타세쿼이아길",
|
||||
"소쇄원"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전남",
|
||||
"name": "구례",
|
||||
"key": "kr.jeonnam.gurye",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"지리산",
|
||||
"화엄사",
|
||||
"섬진강",
|
||||
"사성암"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전남",
|
||||
"name": "해남",
|
||||
"key": "kr.jeonnam.haenam",
|
||||
"type": [
|
||||
"해변",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"땅끝마을",
|
||||
"두륜산",
|
||||
"대흥사"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전남",
|
||||
"name": "완도",
|
||||
"key": "kr.jeonnam.wando",
|
||||
"type": [
|
||||
"해변",
|
||||
"섬"
|
||||
],
|
||||
"spots": [
|
||||
"청산도",
|
||||
"명사십리해수욕장",
|
||||
"완도타워"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "전남",
|
||||
"name": "보성",
|
||||
"key": "kr.jeonnam.boseong",
|
||||
"type": [
|
||||
"도심",
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"보성녹차밭",
|
||||
"율포해수욕장"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경북",
|
||||
"name": "경주",
|
||||
"key": "kr.gyeongbuk.gyeongju",
|
||||
"type": [
|
||||
"도심",
|
||||
"호수"
|
||||
],
|
||||
"spots": [
|
||||
"불국사",
|
||||
"첨성대",
|
||||
"황리단길",
|
||||
"보문단지",
|
||||
"동궁과월지"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경북",
|
||||
"name": "포항",
|
||||
"key": "kr.gyeongbuk.pohang",
|
||||
"type": [
|
||||
"해변",
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"호미곶",
|
||||
"영일대해수욕장",
|
||||
"죽도시장",
|
||||
"스페이스워크"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경북",
|
||||
"name": "안동",
|
||||
"key": "kr.gyeongbuk.andong",
|
||||
"type": [
|
||||
"도심",
|
||||
"강변"
|
||||
],
|
||||
"spots": [
|
||||
"하회마을",
|
||||
"월영교",
|
||||
"도산서원"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경북",
|
||||
"name": "영덕",
|
||||
"key": "kr.gyeongbuk.yeongdeok",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"강구항",
|
||||
"블루로드",
|
||||
"고래불해수욕장"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경북",
|
||||
"name": "울진",
|
||||
"key": "kr.gyeongbuk.uljin",
|
||||
"type": [
|
||||
"해변",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"죽변항",
|
||||
"덕구온천",
|
||||
"성류굴",
|
||||
"후포항"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경북",
|
||||
"name": "문경",
|
||||
"key": "kr.gyeongbuk.mungyeong",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"문경새재",
|
||||
"단산모노레일",
|
||||
"에코랄라"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경남",
|
||||
"name": "거제",
|
||||
"key": "kr.gyeongnam.geoje",
|
||||
"type": [
|
||||
"해변",
|
||||
"섬"
|
||||
],
|
||||
"spots": [
|
||||
"외도보타니아",
|
||||
"바람의언덕",
|
||||
"학동몽돌해변",
|
||||
"매미성",
|
||||
"windy hill"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경남",
|
||||
"name": "통영",
|
||||
"key": "kr.gyeongnam.tongyeong",
|
||||
"type": [
|
||||
"해변",
|
||||
"섬"
|
||||
],
|
||||
"spots": [
|
||||
"동피랑벽화마을",
|
||||
"통영케이블카",
|
||||
"미륵산",
|
||||
"한산도",
|
||||
"장사도"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경남",
|
||||
"name": "남해",
|
||||
"key": "kr.gyeongnam.namhae",
|
||||
"type": [
|
||||
"해변",
|
||||
"섬"
|
||||
],
|
||||
"spots": [
|
||||
"다랭이마을",
|
||||
"독일마을",
|
||||
"상주은모래비치",
|
||||
"보리암"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경남",
|
||||
"name": "하동",
|
||||
"key": "kr.gyeongnam.hadong",
|
||||
"type": [
|
||||
"산간",
|
||||
"강변"
|
||||
],
|
||||
"spots": [
|
||||
"화개장터",
|
||||
"쌍계사",
|
||||
"섬진강",
|
||||
"최참판댁"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경남",
|
||||
"name": "사천",
|
||||
"key": "kr.gyeongnam.sacheon",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"삼천포대교",
|
||||
"사천케이블카",
|
||||
"실안노을길"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "경남",
|
||||
"name": "산청",
|
||||
"key": "kr.gyeongnam.sancheong",
|
||||
"type": [
|
||||
"산간",
|
||||
"계곡"
|
||||
],
|
||||
"spots": [
|
||||
"지리산",
|
||||
"동의보감촌",
|
||||
"대원사계곡"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "부산",
|
||||
"name": "기장",
|
||||
"key": "kr.busan.gijang",
|
||||
"type": [
|
||||
"해변"
|
||||
],
|
||||
"spots": [
|
||||
"해동용궁사",
|
||||
"일광해수욕장",
|
||||
"아난티코브",
|
||||
"죽성성당"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "제주",
|
||||
"name": "제주시",
|
||||
"key": "kr.jeju.jejusi",
|
||||
"type": [
|
||||
"해변",
|
||||
"도심"
|
||||
],
|
||||
"spots": [
|
||||
"애월",
|
||||
"함덕해수욕장",
|
||||
"협재해수욕장",
|
||||
"이호테우",
|
||||
"한림공원"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sido": "제주",
|
||||
"name": "서귀포",
|
||||
"key": "kr.jeju.seogwipo",
|
||||
"type": [
|
||||
"해변",
|
||||
"산간"
|
||||
],
|
||||
"spots": [
|
||||
"중문색달해변",
|
||||
"성산일출봉",
|
||||
"쇠소깍",
|
||||
"천지연폭포",
|
||||
"우도"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
data/전국_펜션_SEO_AEO_키워드.xlsx
Normal file
BIN
data/전국_펜션_SEO_AEO_키워드.xlsx
Normal file
Binary file not shown.
@ -17,7 +17,8 @@
|
||||
"dataset:build": "node scripts/build-dataset.mjs",
|
||||
"dataset:ingest": "tsx scripts/ingest-dataset.ts",
|
||||
"dataset:purge": "tsx scripts/purge-nondataset.ts",
|
||||
"dataset:import-related": "tsx scripts/import-related.ts"
|
||||
"dataset:import-related": "tsx scripts/import-related.ts",
|
||||
"dataset:nationwide": "node scripts/build-nationwide-dataset.mjs && python3 scripts/export-xlsx.py"
|
||||
},
|
||||
"dependencies": {
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
|
||||
153
scripts/build-nationwide-dataset.mjs
Normal file
153
scripts/build-nationwide-dataset.mjs
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 전국 지역별 펜션 SEO/AEO 키워드 데이터셋.
|
||||
* node scripts/build-nationwide-dataset.mjs → data/nationwide-pension-keywords.json
|
||||
*
|
||||
* 설계 원칙
|
||||
* · 조합 폭발을 하지 않는다. 군산 단일 지역 974건을 54개 지역에 곱하면 5만 건이 되고
|
||||
* 대부분 검색량 0이 된다 (실측: 저장분의 89% 미사용).
|
||||
* · 지역 성격(해변/산간/호수/도심/섬)에 맞는 시설 키워드만 전개한다.
|
||||
* 산간 지역에 '오션뷰 펜션'을 만들지 않는다.
|
||||
* · 티어를 매겨 주력/보조/롱테일을 구분한다. SEO 는 페이지당 주력 1개다.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const { regions } = JSON.parse(readFileSync('data/regions.json', 'utf8'));
|
||||
|
||||
// ── 공통 어휘
|
||||
const STAY = ['펜션', '숙소', '독채펜션', '풀빌라', '스파펜션', '애견펜션', '감성펜션', '글램핑', '독채'];
|
||||
const INTENT = { 추천:'local', 예약:'transactional', 가격:'transactional', 후기:'informational',
|
||||
순위:'informational', 저렴한곳:'local', 가성비:'local', 실시간예약:'transactional',
|
||||
당일예약:'transactional', 특가:'transactional' };
|
||||
const WITH = ['커플', '가족', '친구', '애견동반', '단체', '아이동반', '부모님', '4인', '6인', '2인'];
|
||||
const VIBE = ['감성', '조용한', '분위기 좋은', '사진찍기 좋은', '인생샷', '깔끔한', '신축'];
|
||||
const TRAVEL = ['1박2일', '2박3일', '주말여행', '뚜벅이 여행', '워케이션'];
|
||||
|
||||
// 지역 성격별 유효 시설 — 여기가 조합 폭발을 막는 장치다
|
||||
const FEATURES_BY_TYPE = {
|
||||
해변: ['오션뷰', '바다뷰', '노을뷰', '일출뷰', '해변 근처', '바다 보이는'],
|
||||
섬: ['오션뷰', '바다뷰', '배타고 가는', '섬'],
|
||||
산간: ['산뷰', '숲속', '불멍', '화로대', '벽난로', '단풍'],
|
||||
계곡: ['계곡', '물놀이', '계곡뷰', '불멍'],
|
||||
호수: ['호수뷰', '레이크뷰', '물놀이', '노을뷰'],
|
||||
강변: ['강뷰', '리버뷰', '노을뷰'],
|
||||
도심: ['역세권', '시내', '주차', '도보 여행'],
|
||||
습지: ['자연', '산책'],
|
||||
};
|
||||
const FEATURES_COMMON = ['바베큐', '스파', '자쿠지', '수영장', '독채', '프라이빗', '복층', '테라스', '애견운동장', '넷플릭스'];
|
||||
|
||||
const SEASON_BY_TYPE = {
|
||||
해변: ['여름휴가', '물놀이', '해수욕', '일출', '낙조'],
|
||||
섬: ['여름휴가', '일출'],
|
||||
산간: ['겨울', '단풍', '스키', '눈꽃'],
|
||||
계곡: ['여름휴가', '물놀이', '단풍'],
|
||||
호수: ['여름휴가', '단풍', '벚꽃'],
|
||||
강변: ['벚꽃', '단풍'],
|
||||
도심: ['벚꽃', '연말'],
|
||||
습지: ['가을', '갈대'],
|
||||
};
|
||||
const SEASON_COMMON = ['겨울', '연말', '크리스마스', '주말', '성수기'];
|
||||
|
||||
const rows = [];
|
||||
const seen = new Set();
|
||||
const norm = (s) => s.normalize('NFKC').toLowerCase().replace(/\s+/g, '');
|
||||
|
||||
function add(region, keyword, { intent = 'local', kind = 'keyword', category, tier, relevance }) {
|
||||
const k = keyword.replace(/\s+/g, ' ').trim();
|
||||
const id = `${region.key}|${norm(k)}`;
|
||||
if (!k || seen.has(id)) return;
|
||||
seen.add(id);
|
||||
rows.push({
|
||||
sido: region.sido, region: region.name, regionKey: region.key,
|
||||
regionType: region.type.join('·'),
|
||||
keyword: k, kind, intent, category, tier,
|
||||
relevance: Math.round(relevance * 100) / 100,
|
||||
});
|
||||
}
|
||||
|
||||
const uniq = (a) => [...new Set(a)];
|
||||
|
||||
for (const r of regions) {
|
||||
const R = r.name;
|
||||
const feats = uniq([...r.type.flatMap((t) => FEATURES_BY_TYPE[t] ?? []), ...FEATURES_COMMON]);
|
||||
const seasons = uniq([...r.type.flatMap((t) => SEASON_BY_TYPE[t] ?? []), ...SEASON_COMMON]);
|
||||
|
||||
// T1 코어 — 주력 후보.
|
||||
// 별칭(대천/보령 처럼 같은 지역의 다른 검색 표기)도 코어·의도 계층까지는 함께 전개한다.
|
||||
// 전 계층에 곱하면 두 배가 되므로 상위 티어에만 적용한다.
|
||||
const names = [R, ...(r.aliases ?? [])];
|
||||
for (const N of names) {
|
||||
add(r, `${N} 펜션`, { category: '코어', tier: '주력', relevance: N === R ? 0.98 : 0.94 });
|
||||
add(r, `${N} 숙소`, { category: '코어', tier: '주력', relevance: N === R ? 0.96 : 0.92 });
|
||||
for (const s of STAY.slice(2)) add(r, `${N} ${s}`, { category: '코어', tier: '주력', relevance: 0.9 });
|
||||
}
|
||||
|
||||
// T2 의도 — 보조
|
||||
for (const N of names)
|
||||
for (const [m, it] of Object.entries(INTENT))
|
||||
add(r, `${N} 펜션 ${m}`, { intent: it, category: '의도', tier: '보조', relevance: N === R ? 0.88 : 0.84 });
|
||||
for (const s of ['독채펜션', '풀빌라', '애견펜션', '감성펜션'])
|
||||
for (const m of ['추천', '예약', '가격', '후기'])
|
||||
add(r, `${R} ${s} ${m}`, { intent: INTENT[m], category: '의도', tier: '보조', relevance: 0.8 });
|
||||
|
||||
// T3 동반자
|
||||
for (const w of WITH) {
|
||||
add(r, `${R} ${w} 펜션`, { category: '동반자', tier: '보조', relevance: 0.85 });
|
||||
add(r, `${R} ${w} 펜션 추천`, { category: '동반자', tier: '롱테일', relevance: 0.7 });
|
||||
}
|
||||
|
||||
// T4 시설 — 지역 성격에 맞는 것만
|
||||
for (const f of feats) {
|
||||
add(r, `${R} ${f} 펜션`, { category: '시설', tier: '보조', relevance: 0.83 });
|
||||
}
|
||||
for (const f of feats.slice(0, 6))
|
||||
add(r, `${R} 커플 ${f} 펜션`, { category: '시설', tier: '롱테일', relevance: 0.55 });
|
||||
|
||||
// T5 시즌
|
||||
for (const s of seasons) add(r, `${R} ${s} 펜션`, { category: '시즌', tier: '보조', relevance: 0.76 });
|
||||
|
||||
// T6 관광지 앵커 — 지역 고유
|
||||
for (const sp of r.spots) {
|
||||
add(r, `${sp} 근처 펜션`, { category: '관광지', tier: '보조', relevance: 0.84 });
|
||||
add(r, `${sp} 근처 숙소`, { category: '관광지', tier: '보조', relevance: 0.81 });
|
||||
}
|
||||
|
||||
// T7 분위기·여행형태
|
||||
for (const v of VIBE) add(r, `${R} ${v} 숙소`, { category: '분위기', tier: '롱테일', relevance: 0.68 });
|
||||
for (const t of TRAVEL) add(r, `${R} ${t} 숙소`, { category: '여행형태', tier: '롱테일', relevance: 0.66 });
|
||||
|
||||
// T8 질문형 (AEO)
|
||||
const qs = [
|
||||
[`${R} 펜션 어디가 좋아요`, 'informational', 0.72],
|
||||
[`${R} 펜션 1박 얼마인가요`, 'transactional', 0.7],
|
||||
[`${R} 애견동반 펜션 있나요`, 'informational', 0.68],
|
||||
[`${R} 펜션 바베큐 가능한가요`, 'informational', 0.67],
|
||||
[`${R} 여행 몇박이 좋을까요`, 'informational', 0.6],
|
||||
[`${R} 펜션 성수기 언제인가요`, 'informational', 0.58],
|
||||
];
|
||||
for (const [q, it, rel] of qs) add(r, q, { intent: it, category: '질문형', tier: '롱테일', relevance: rel });
|
||||
|
||||
// T9 태그 (칩 UI)
|
||||
for (const t of uniq([...feats.slice(0, 8), ...WITH.slice(0, 5), ...VIBE.slice(0, 4)]))
|
||||
add(r, t, { kind: 'tag', category: '태그', tier: '태그', relevance: 0.5 });
|
||||
}
|
||||
|
||||
// 광역 단위 롤업
|
||||
const sidoList = uniq(regions.map((x) => x.sido));
|
||||
for (const sido of sidoList) {
|
||||
const pseudo = { sido, name: sido, key: `rollup.${sido}`, type: [] };
|
||||
for (const s of ['펜션', '숙소', '독채펜션', '풀빌라', '애견펜션'])
|
||||
add(pseudo, `${sido} ${s}`, { category: '광역', tier: '주력', relevance: 0.92 });
|
||||
for (const m of ['추천', '예약', '가격', '후기'])
|
||||
add(pseudo, `${sido} 펜션 ${m}`, { intent: INTENT[m], category: '광역', tier: '보조', relevance: 0.85 });
|
||||
}
|
||||
|
||||
writeFileSync('data/nationwide-pension-keywords.json',
|
||||
JSON.stringify({ topic: '전국 지역별 펜션', locale: 'ko-KR',
|
||||
generatedBy: 'region master × search-pattern expansion (region-type aware)',
|
||||
regionCount: regions.length, count: rows.length, items: rows }, null, 2) + '\n');
|
||||
|
||||
const by = (f) => rows.reduce((a, r) => (a[r[f]] = (a[r[f]] ?? 0) + 1, a), {});
|
||||
console.log(`✅ data/nationwide-pension-keywords.json ${rows.length}건 / ${regions.length}개 지역`);
|
||||
console.log(` 지역당 평균 ${Math.round(rows.length / (regions.length + sidoList.length))}건`);
|
||||
console.log(' 티어:', by('tier'));
|
||||
console.log(' 카테고리:', by('category'));
|
||||
155
scripts/export-xlsx.py
Normal file
155
scripts/export-xlsx.py
Normal file
@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""전국 펜션 키워드 데이터셋 → 엑셀.
|
||||
python3 scripts/export-xlsx.py
|
||||
검색량·경쟁도 열은 비워 둔다 — 네이버 검색광고 키워드도구에서 받아 채우는 자리."""
|
||||
import json, collections
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
SRC = 'data/nationwide-pension-keywords.json'
|
||||
OUT = 'data/전국_펜션_SEO_AEO_키워드.xlsx'
|
||||
|
||||
INK = '1F2A2B'
|
||||
ACC = '0D6A60'
|
||||
HEAD = PatternFill('solid', fgColor='0D6A60')
|
||||
BAND = PatternFill('solid', fgColor='F1F5F4')
|
||||
TIER = {'주력': 'DFF0EC', '보조': 'FFFFFF', '롱테일': 'F7F7F5', '태그': 'F6EAD2'}
|
||||
THIN = Side(style='thin', color='D5DCDB')
|
||||
BOX = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
|
||||
|
||||
SLOT = {
|
||||
'코어': '메인 페이지 (주력)', '광역': '광역 랜딩',
|
||||
'의도': '메인 / 예약 페이지', '동반자': '객실 페이지',
|
||||
'시설': '시설 페이지', '관광지': '주변 여행 페이지',
|
||||
'시즌': '블로그 · 프로모션', '분위기': '블로그 · 소개',
|
||||
'여행형태': '블로그 · 코스', '질문형': 'FAQ (AEO · FAQPage)',
|
||||
'태그': '필터 UI (SEO 아님)',
|
||||
}
|
||||
|
||||
data = json.load(open(SRC, encoding='utf-8'))
|
||||
items = data['items']
|
||||
regions = json.load(open('data/regions.json', encoding='utf-8'))['regions']
|
||||
|
||||
wb = Workbook()
|
||||
|
||||
def style_header(ws, ncols, height=22):
|
||||
for c in range(1, ncols + 1):
|
||||
cell = ws.cell(row=1, column=c)
|
||||
cell.fill = HEAD
|
||||
cell.font = Font(bold=True, color='FFFFFF', size=10)
|
||||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||||
cell.border = BOX
|
||||
ws.row_dimensions[1].height = height
|
||||
ws.freeze_panes = 'A2'
|
||||
ws.auto_filter.ref = f'A1:{get_column_letter(ncols)}{ws.max_row}'
|
||||
|
||||
def widths(ws, ws_widths):
|
||||
for i, w in enumerate(ws_widths, start=1):
|
||||
ws.column_dimensions[get_column_letter(i)].width = w
|
||||
|
||||
# ────────────────────────────────── 1. 키워드
|
||||
ws = wb.active
|
||||
ws.title = '키워드'
|
||||
cols = ['시도', '지역', '지역키', '지역성격', '키워드', '종류', '의도', '카테고리',
|
||||
'티어', '관련도', '월간검색수', '경쟁도', '추천 배치', '비고']
|
||||
ws.append(cols)
|
||||
for it in items:
|
||||
ws.append([
|
||||
it['sido'], it['region'], it['regionKey'], it['regionType'],
|
||||
it['keyword'], it['kind'], it['intent'], it['category'],
|
||||
it['tier'], it['relevance'], None, None,
|
||||
SLOT.get(it['category'], ''), None,
|
||||
])
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=len(cols)):
|
||||
fill = PatternFill('solid', fgColor=TIER.get(row[8].value, 'FFFFFF'))
|
||||
for c in row:
|
||||
c.font = Font(size=10, color=INK)
|
||||
c.border = BOX
|
||||
c.fill = fill
|
||||
row[9].number_format = '0.00'
|
||||
row[10].number_format = '#,##0'
|
||||
row[4].font = Font(size=10, bold=True, color=INK)
|
||||
style_header(ws, len(cols))
|
||||
widths(ws, [8, 12, 24, 14, 30, 8, 14, 10, 9, 9, 12, 10, 22, 16])
|
||||
|
||||
# ────────────────────────────────── 2. 지역 마스터
|
||||
ws = wb.create_sheet('지역마스터')
|
||||
ws.append(['시도', '지역', '지역키', '지역성격', '대표 관광지 (앵커)', '키워드 수'])
|
||||
cnt = collections.Counter(i['regionKey'] for i in items)
|
||||
for r in regions:
|
||||
ws.append([r['sido'], r['name'], r['key'], '·'.join(r['type']),
|
||||
', '.join(r['spots']), cnt.get(r['key'], 0)])
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=6):
|
||||
for c in row:
|
||||
c.font = Font(size=10, color=INK); c.border = BOX
|
||||
c.alignment = Alignment(vertical='center', wrap_text=(c.column == 5))
|
||||
style_header(ws, 6)
|
||||
widths(ws, [8, 14, 26, 14, 70, 10])
|
||||
|
||||
# ────────────────────────────────── 3. 지역별 요약
|
||||
ws = wb.create_sheet('지역별요약')
|
||||
tiers = ['주력', '보조', '롱테일', '태그']
|
||||
ws.append(['시도', '지역'] + tiers + ['합계'])
|
||||
per = collections.defaultdict(collections.Counter)
|
||||
meta = {}
|
||||
for i in items:
|
||||
per[i['regionKey']][i['tier']] += 1
|
||||
meta[i['regionKey']] = (i['sido'], i['region'])
|
||||
for key, c in sorted(per.items(), key=lambda kv: (-sum(kv[1].values()))):
|
||||
sido, name = meta[key]
|
||||
ws.append([sido, name] + [c[t] for t in tiers] + [sum(c.values())])
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=7):
|
||||
for c in row:
|
||||
c.font = Font(size=10, color=INK); c.border = BOX
|
||||
style_header(ws, 7)
|
||||
widths(ws, [8, 14, 9, 9, 10, 9, 9])
|
||||
|
||||
# ────────────────────────────────── 4. 사용 가이드
|
||||
ws = wb.create_sheet('사용가이드')
|
||||
guide = [
|
||||
('이 파일은 무엇인가', ''),
|
||||
('', f"전국 {data['regionCount']}개 펜션 수요 지역 × 검색 패턴으로 만든 SEO/AEO 키워드 후보 {len(items):,}건."),
|
||||
('', '지역 성격(해변·산간·호수·도심·섬)에 맞는 시설 키워드만 전개했다. 산간 지역에 오션뷰 키워드는 없다.'),
|
||||
('', ''),
|
||||
('⚠ 반드시 먼저 읽을 것', ''),
|
||||
('', '이 키워드는 검색 패턴으로 생성한 것이지 실제 검색 데이터가 아니다.'),
|
||||
('', '네이버 검색광고 > 도구 > 키워드도구 에서 월간검색수를 받아 [월간검색수] 열을 채운 뒤'),
|
||||
('', '월 10 미만은 걷어내야 한다. 앞선 단일 지역 검증에서 저장분의 89%가 한 번도 쓰이지 않았다.'),
|
||||
('', ''),
|
||||
('티어의 뜻', ''),
|
||||
('주력', '페이지의 대표 키워드 후보. 한 페이지에 1개만 쓴다.'),
|
||||
('보조', 'h2/h3 와 본문에 배치. 페이지당 3~5개.'),
|
||||
('롱테일', '블로그·상세 페이지용. 검색량 확인 후 취사선택.'),
|
||||
('태그', '사이트 필터 UI 용. SEO 키워드가 아니다.'),
|
||||
('', ''),
|
||||
('한 페이지에 몇 개를 넣나', ''),
|
||||
('', 'title 1개 · h1 1개 · meta description 2~3개 · h2/h3 3~5개 · 본문 5~10개'),
|
||||
('', 'meta keywords 태그는 쓰지 않는다 (구글은 2009년부터 랭킹에 반영하지 않는다).'),
|
||||
('', '한 페이지에 주력을 여러 개 넣으면 주제가 희석돼 어느 것으로도 안 잡힌다.'),
|
||||
('', ''),
|
||||
('AEO (답변엔진)', ''),
|
||||
('', '[카테고리=질문형] 행이 AEO 용이다. FAQPage 구조화 데이터로 8~15쌍 넣는다.'),
|
||||
('', '답변은 2~3문장, 업체 정보에 근거한 사실만 쓴다.'),
|
||||
('', ''),
|
||||
('다음 단계', ''),
|
||||
('', '1. 키워드도구로 [월간검색수]·[경쟁도] 채우기'),
|
||||
('', '2. 월 10 미만 행 제거'),
|
||||
('', '3. 관련도 높음 + 볼륨 중간 + 경쟁 낮음 조합을 우선 채택'),
|
||||
('', '4. [추천 배치] 열대로 페이지에 배분'),
|
||||
]
|
||||
ws.append(['항목', '내용'])
|
||||
for a, b in guide:
|
||||
ws.append([a, b])
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=2):
|
||||
bold = bool(row[0].value) and not row[1].value
|
||||
row[0].font = Font(size=10, bold=True, color=ACC if bold else INK)
|
||||
row[1].font = Font(size=10, color=INK)
|
||||
row[1].alignment = Alignment(wrap_text=True, vertical='center')
|
||||
style_header(ws, 2)
|
||||
widths(ws, [22, 100])
|
||||
|
||||
wb.save(OUT)
|
||||
print(f'✅ {OUT}')
|
||||
print(f' 시트: ' + ', '.join(s.title for s in wb.worksheets))
|
||||
print(f' 키워드 {len(items):,}행 / 지역 {len(regions)}개')
|
||||
Loading…
Reference in New Issue
Block a user