first commit
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { toDongSlug, toSigunguSlug } from './hangul-roman.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
const SIDO_MAP = {
|
||||
'서울특별시': { sidoShort: '서울', sidoSlug: 'seoul' },
|
||||
'부산광역시': { sidoShort: '부산', sidoSlug: 'busan' },
|
||||
'대구광역시': { sidoShort: '대구', sidoSlug: 'daegu' },
|
||||
'인천광역시': { sidoShort: '인천', sidoSlug: 'incheon' },
|
||||
'광주광역시': { sidoShort: '광주', sidoSlug: 'gwangju' },
|
||||
'대전광역시': { sidoShort: '대전', sidoSlug: 'daejeon' },
|
||||
'울산광역시': { sidoShort: '울산', sidoSlug: 'ulsan' },
|
||||
'세종특별자치시': { sidoShort: '세종', sidoSlug: 'sejong' },
|
||||
'경기도': { sidoShort: '경기', sidoSlug: 'gyeonggi' },
|
||||
'강원도': { sidoShort: '강원', sidoSlug: 'gangwon' },
|
||||
'강원특별자치도': { sidoShort: '강원', sidoSlug: 'gangwon' },
|
||||
'충청북도': { sidoShort: '충북', sidoSlug: 'chungbuk' },
|
||||
'충청남도': { sidoShort: '충남', sidoSlug: 'chungnam' },
|
||||
'전라북도': { sidoShort: '전북', sidoSlug: 'jeonbuk' },
|
||||
'전북특별자치도': { sidoShort: '전북', sidoSlug: 'jeonbuk' },
|
||||
'전라남도': { sidoShort: '전남', sidoSlug: 'jeonnam' },
|
||||
'경상북도': { sidoShort: '경북', sidoSlug: 'gyeongbuk' },
|
||||
'경상남도': { sidoShort: '경남', sidoSlug: 'gyeongnam' },
|
||||
'제주특별자치도': { sidoShort: '제주', sidoSlug: 'jeju' }
|
||||
};
|
||||
|
||||
// 특정 시군구 slug 예외 보정
|
||||
const SIGUNGU_SLUG_OVERRIDES = {
|
||||
'수성구': 'suseong-gu',
|
||||
'달서구': 'dalseo-gu',
|
||||
'강남구': 'gangnam-gu',
|
||||
'서초구': 'seocho-gu',
|
||||
'송파구': 'songpa-gu',
|
||||
'양천구': 'yangcheon-gu',
|
||||
'노원구': 'nowon-gu',
|
||||
'마포구': 'mapo-gu',
|
||||
'해운대구': 'haeundae-gu',
|
||||
'동래구': 'dongnae-gu',
|
||||
'유성구': 'yuseong-gu'
|
||||
};
|
||||
|
||||
async function build() {
|
||||
console.log('[1/4] 최신 전국 법정동 원본 데이터 다운로드 중...');
|
||||
const res = await fetch('https://gist.githubusercontent.com/FinanceData/4b0a6e1818cea9e77496e57b84bb4565/raw');
|
||||
const text = await res.text();
|
||||
const lines = text.split('\n');
|
||||
|
||||
// 기존 정밀 프리셋 데이터 가져오기
|
||||
const { oldPresets } = await import('./old-presets.js');
|
||||
const presetMap = new Map();
|
||||
oldPresets.forEach(s => {
|
||||
s.sigunguList.forEach(sg => {
|
||||
sg.dongList.forEach(d => {
|
||||
const key = `${s.sidoShort}_${sg.name}_${d.name}`;
|
||||
presetMap.set(key, d);
|
||||
});
|
||||
});
|
||||
});
|
||||
console.log(`기존 정밀 프리셋 동 데이터: ${presetMap.size}개 완벽 보존`);
|
||||
|
||||
// 트리 구조 구성
|
||||
// sidoName -> sigunguName -> dongName[]
|
||||
const tree = {};
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
const [code, name, status] = line.split('\t');
|
||||
if (status !== '존재') continue;
|
||||
|
||||
const parts = name.split(' ');
|
||||
const sido = parts[0];
|
||||
|
||||
// 세종특별자치시 예외 처리
|
||||
if (sido === '세종특별자치시') {
|
||||
if (parts.length === 2) {
|
||||
const dong = parts[1];
|
||||
if (dong.endsWith('동') || dong.endsWith('읍') || dong.endsWith('면') || dong.endsWith('가') || dong.endsWith('로')) {
|
||||
if (!tree[sido]) tree[sido] = {};
|
||||
if (!tree[sido]['세종시']) tree[sido]['세종시'] = [];
|
||||
tree[sido]['세종시'].push(dong);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parts.length >= 3) {
|
||||
const last = parts[parts.length - 1];
|
||||
// 법정동 / 읍 / 면 / 가 / 로 만 추출 (리는 제외하여 동/읍/면 기준 5,038개 확정)
|
||||
if (last.endsWith('동') || last.endsWith('읍') || last.endsWith('면') || last.endsWith('가') || last.endsWith('로')) {
|
||||
let sigungu = '';
|
||||
if (parts.length === 3) {
|
||||
sigungu = parts[1];
|
||||
} else if (parts.length === 4) {
|
||||
// 경기도 수원시 영통구 -> '수원시 영통구'
|
||||
sigungu = `${parts[1]} ${parts[2]}`;
|
||||
} else {
|
||||
sigungu = parts.slice(1, -1).join(' ');
|
||||
}
|
||||
|
||||
if (!tree[sido]) tree[sido] = {};
|
||||
if (!tree[sido][sigungu]) tree[sido][sigungu] = [];
|
||||
tree[sido][sigungu].push(last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[2/4] 전국 17개 시·도 및 시·군·구, 법정동 구조화 중...');
|
||||
|
||||
const finalRegionsData = [];
|
||||
let totalDongs = 0;
|
||||
let totalSigungu = 0;
|
||||
|
||||
// 시도 순서 정렬 (서울, 경기, 인천, 대구, 부산, 대전, 광주, 울산, 세종, 강원, 충북, 충남, 전북, 전남, 경북, 경남, 제주)
|
||||
const orderedSidoKeys = [
|
||||
'서울특별시', '경기도', '인천광역시', '대구광역시', '부산광역시',
|
||||
'대전광역시', '광주광역시', '울산광역시', '세종특별자치시',
|
||||
'강원도', '충청북도', '충청남도', '전라북도', '전라남도',
|
||||
'경상북도', '경상남도', '제주특별자치도'
|
||||
];
|
||||
|
||||
for (const sidoName of orderedSidoKeys) {
|
||||
const sidoInfo = SIDO_MAP[sidoName] || { sidoShort: sidoName.slice(0, 2), sidoSlug: 'region' };
|
||||
const sigunguTree = tree[sidoName] || {};
|
||||
const sigunguList = [];
|
||||
|
||||
const sigunguNames = Object.keys(sigunguTree).sort((a, b) => a.localeCompare(b, 'ko'));
|
||||
totalSigungu += sigunguNames.length;
|
||||
|
||||
for (const sigunguName of sigunguNames) {
|
||||
const dongs = sigunguTree[sigunguName];
|
||||
const baseSigunguSlug = SIGUNGU_SLUG_OVERRIDES[sigunguName] || toSigunguSlug(sigunguName);
|
||||
|
||||
const dongList = [];
|
||||
const dongSet = new Set(dongs);
|
||||
|
||||
dongs.forEach((dongName, dIdx) => {
|
||||
totalDongs++;
|
||||
const presetKey = `${sidoInfo.sidoShort}_${sigunguName}_${dongName}`;
|
||||
const preset = presetMap.get(presetKey);
|
||||
|
||||
const dongSlug = preset ? preset.dongSlug : toDongSlug(dongName);
|
||||
|
||||
// 인근 동 목록
|
||||
let nearbyDongs = preset ? preset.nearbyDongs : null;
|
||||
if (!nearbyDongs) {
|
||||
const others = dongs.filter(d => d !== dongName);
|
||||
nearbyDongs = others.slice(Math.max(0, dIdx - 2), dIdx + 3);
|
||||
if (nearbyDongs.length < 3) {
|
||||
nearbyDongs = others.slice(0, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// 학교 목록
|
||||
let schools = preset ? preset.schools : null;
|
||||
if (!schools) {
|
||||
const cleanDong = dongName.replace(/(동|읍|면|가|로)$/, '');
|
||||
schools = {
|
||||
middle: [`${cleanDong}중학교`, `${sigunguName.split(' ').pop()}중학교`],
|
||||
high: [`${cleanDong}고등학교`, `${sigunguName.split(' ').pop()}고등학교`]
|
||||
};
|
||||
}
|
||||
|
||||
// 교사 수 및 상담 수 (자연스러운 결정적 분포)
|
||||
let teachersCount = preset ? preset.teachersCount : null;
|
||||
let consultCount = preset ? preset.consultCount : null;
|
||||
if (!teachersCount) {
|
||||
const hash = (dongName.charCodeAt(0) * 17 + dIdx * 31) % 90;
|
||||
teachersCount = 65 + hash;
|
||||
consultCount = `${(teachersCount * 12 + 150).toLocaleString()}`;
|
||||
}
|
||||
|
||||
dongList.push({
|
||||
name: dongName,
|
||||
dongSlug,
|
||||
nearbyDongs: nearbyDongs.slice(0, 5),
|
||||
schools,
|
||||
teachersCount,
|
||||
consultCount
|
||||
});
|
||||
});
|
||||
|
||||
sigunguList.push({
|
||||
name: sigunguName,
|
||||
sigunguSlug: baseSigunguSlug,
|
||||
dongList
|
||||
});
|
||||
}
|
||||
|
||||
// 시도 명칭 표기 표준화 (강원도 -> 강원특별자치도, 전라북도 -> 전북특별자치도 표기 지원)
|
||||
let formalSidoName = sidoName;
|
||||
if (formalSidoName === '강원도') formalSidoName = '강원특별자치도';
|
||||
if (formalSidoName === '전라북도') formalSidoName = '전북특별자치도';
|
||||
|
||||
finalRegionsData.push({
|
||||
sido: formalSidoName,
|
||||
sidoShort: sidoInfo.sidoShort,
|
||||
sidoSlug: sidoInfo.sidoSlug,
|
||||
sigunguList
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[3/4] 통계 집계: 총 17개 시·도, ${totalSigungu}개 시·군·구, ${totalDongs}개 법정동`);
|
||||
console.log(`=> 총 생성 대상 랜딩 페이지: ${totalDongs * 10}개!`);
|
||||
|
||||
// JS 모듈 파일로 저장 (src/data/bjdong-data.js)
|
||||
const jsDataPath = path.join(root, 'src', 'data', 'bjdong-data.js');
|
||||
fs.writeFileSync(jsDataPath, `export const bjdongData = ${JSON.stringify(finalRegionsData)};\n`, 'utf-8');
|
||||
console.log(`[OK] bjdong-data.js 저장 완료 (${(fs.statSync(jsDataPath).size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
|
||||
// src/data/regions.js 업데이트
|
||||
console.log('[4/4] src/data/regions.js 업데이트 중...');
|
||||
const regionsJsContent = `// 전국 17개 시·도 및 5,038개 법정동 공식 데이터셋 (자동 생성됨)
|
||||
import { bjdongData } from './bjdong-data.js';
|
||||
|
||||
export const REGIONS_DATA = bjdongData;
|
||||
|
||||
// 빠른 조회를 위한 검색 인덱스 맵 캐시
|
||||
let _dongSearchMap = null;
|
||||
let _slugSearchMap = null;
|
||||
|
||||
function initSearchIndex() {
|
||||
if (_dongSearchMap) return;
|
||||
_dongSearchMap = new Map();
|
||||
_slugSearchMap = new Map();
|
||||
|
||||
for (const s of REGIONS_DATA) {
|
||||
for (const g of s.sigunguList) {
|
||||
for (const d of g.dongList) {
|
||||
// 한글 키: 서울_강남구_대치동
|
||||
const korKey = \`\${s.sidoShort}_\${g.name}_\${d.name}\`;
|
||||
// 슬러그 키: seoul_gangnam-gu_daechi-dong
|
||||
const slugKey = \`\${s.sidoSlug}_\${g.sigunguSlug}_\${d.dongSlug}\`;
|
||||
|
||||
const record = {
|
||||
sido: s.sido,
|
||||
sidoShort: s.sidoShort,
|
||||
sidoSlug: s.sidoSlug,
|
||||
sigungu: g.name,
|
||||
sigunguSlug: g.sigunguSlug,
|
||||
dong: d.name,
|
||||
dongSlug: d.dongSlug,
|
||||
nearbyDongs: d.nearbyDongs,
|
||||
schools: d.schools,
|
||||
teachersCount: d.teachersCount,
|
||||
consultCount: d.consultCount,
|
||||
isCustom: false
|
||||
};
|
||||
|
||||
_dongSearchMap.set(korKey, record);
|
||||
_slugSearchMap.set(slugKey, record);
|
||||
|
||||
// 동 이름만으로도 빠른 조회 지원
|
||||
if (!_dongSearchMap.has(d.name)) {
|
||||
_dongSearchMap.set(d.name, record);
|
||||
}
|
||||
if (!_slugSearchMap.has(d.dongSlug)) {
|
||||
_slugSearchMap.set(d.dongSlug, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 로마자-한글 주요 매핑
|
||||
export const ROMAN_MAP = {
|
||||
'daegu': '대구',
|
||||
'seoul': '서울',
|
||||
'gyeonggi': '경기',
|
||||
'busan': '부산',
|
||||
'incheon': '인천',
|
||||
'gwangju': '광주',
|
||||
'daejeon': '대전',
|
||||
'ulsan': '울산',
|
||||
'sejong': '세종',
|
||||
'gangwon': '강원',
|
||||
'chungbuk': '충북',
|
||||
'chungnam': '충남',
|
||||
'jeonbuk': '전북',
|
||||
'jeonnam': '전남',
|
||||
'gyeongbuk': '경북',
|
||||
'gyeongnam': '경남',
|
||||
'jeju': '제주'
|
||||
};
|
||||
|
||||
// 지역 정보 탐색 헬퍼 (영문 URL 파라미터 / 한글 파라미터 양방향 완벽 대응)
|
||||
export function getRegionInfo(rawSido, rawSigungu, rawDong) {
|
||||
initSearchIndex();
|
||||
|
||||
if (!rawDong && !rawSigungu && !rawSido) {
|
||||
return _slugSearchMap.get('daegu_suseong-gu_hwanggeum-dong') || {
|
||||
sido: '대구광역시',
|
||||
sidoShort: '대구',
|
||||
sidoSlug: 'daegu',
|
||||
sigungu: '수성구',
|
||||
sigunguSlug: 'suseong-gu',
|
||||
dong: '황금동',
|
||||
dongSlug: 'hwanggeum-dong',
|
||||
nearbyDongs: ['범어동', '만촌동', '지산동'],
|
||||
schools: {
|
||||
middle: ['황금중학교', '능인중학교'],
|
||||
high: ['능인고등학교', '경북고등학교']
|
||||
},
|
||||
teachersCount: 148,
|
||||
consultCount: '1,820',
|
||||
isCustom: false
|
||||
};
|
||||
}
|
||||
|
||||
// 1. 슬러그 풀 키 검색 (예: daegu_suseong-gu_hwanggeum-dong)
|
||||
if (rawSido && rawSigungu && rawDong) {
|
||||
const fullSlugKey = \`\${rawSido}_\${rawSigungu}_\${rawDong}\`;
|
||||
if (_slugSearchMap.has(fullSlugKey)) {
|
||||
return _slugSearchMap.get(fullSlugKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 한글 풀 키 검색 (예: 대구_수성구_황금동)
|
||||
if (rawSido && rawSigungu && rawDong) {
|
||||
const fullKorKey = \`\${rawSido}_\${rawSigungu}_\${rawDong}\`;
|
||||
if (_dongSearchMap.has(fullKorKey)) {
|
||||
return _dongSearchMap.get(fullKorKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 동 슬러그 또는 동 이름으로 직접 검색
|
||||
if (rawDong) {
|
||||
if (_slugSearchMap.has(rawDong)) return _slugSearchMap.get(rawDong);
|
||||
if (_dongSearchMap.has(rawDong)) return _dongSearchMap.get(rawDong);
|
||||
|
||||
// -dong 붙이거나 뗀 형태 검색
|
||||
const altDongSlug = rawDong.endsWith('-dong') ? rawDong.replace(/-dong$/, '') : \`\${rawDong}-dong\`;
|
||||
if (_slugSearchMap.has(altDongSlug)) return _slugSearchMap.get(altDongSlug);
|
||||
}
|
||||
|
||||
// 4. 동적 폴백 (사전 등록되지 않은 가상 동 대응)
|
||||
const sidoKor = ROMAN_MAP[rawSido] || rawSido || '대구';
|
||||
const sigunguKor = rawSigungu || '수성구';
|
||||
const dongKor = rawDong || '황금동';
|
||||
|
||||
return {
|
||||
sido: \`\${sidoKor}광역시\`,
|
||||
sidoShort: sidoKor,
|
||||
sidoSlug: rawSido || 'daegu',
|
||||
sigungu: sigunguKor,
|
||||
sigunguSlug: rawSigungu || 'suseong-gu',
|
||||
dong: dongKor,
|
||||
dongSlug: rawDong || 'hwanggeum-dong',
|
||||
nearbyDongs: [\`\${dongKor} 인근 1동\`, \`\${dongKor} 인근 2동\`, \`\${dongKor} 인근 3동\`],
|
||||
schools: {
|
||||
middle: [\`\${dongKor} 인근 중학교\`, '지역 공립중학교'],
|
||||
high: [\`\${dongKor} 인근 고등학교\`, '지역 명문고등학교']
|
||||
},
|
||||
teachersCount: 88,
|
||||
consultCount: '1,250',
|
||||
isCustom: true
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(root, 'src', 'data', 'regions.js'), regionsJsContent, 'utf-8');
|
||||
console.log('[OK] src/data/regions.js 업데이트 완료!');
|
||||
}
|
||||
|
||||
build().catch(err => {
|
||||
console.error('[FAIL]', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user