230 lines
8.7 KiB
TypeScript
230 lines
8.7 KiB
TypeScript
import regionsData from '../data/regions.json'
|
|
import keywordsData from '../data/keywords.json'
|
|
import curriculumsData from '../data/curriculums.json'
|
|
|
|
export interface RegionInfo {
|
|
fullName: string
|
|
sido: string
|
|
sigungu: string
|
|
dong: string
|
|
}
|
|
|
|
export interface ParsedRouteParams {
|
|
region: RegionInfo
|
|
sido: string
|
|
shortSido: string
|
|
sigungu: string
|
|
dong: string
|
|
shortRegionName: string
|
|
grade: string
|
|
gradeShort: string
|
|
subjectKey: string
|
|
subjectName: string
|
|
targetTerm: string
|
|
fullKeyword: string
|
|
isRegionOnly?: boolean
|
|
}
|
|
|
|
// 5대 핵심 과목 정의
|
|
export const SUBJECTS_MAP: Record<string, { key: string, name: string, badge: string, aliases: string[] }> = {
|
|
korean: { key: 'korean', name: '국어', badge: '문해력·서술형', aliases: ['국어', '문학', '독서', '비문학', '화법'] },
|
|
english: { key: 'english', name: '영어', badge: '구문독해·내신', aliases: ['영어', '영문법', '영어독해', '공통영어'] },
|
|
math: { key: 'math', name: '수학', badge: '개념정립·킬러문항', aliases: ['수학', '수1', '수2', '미적분', '확통', '기하'] },
|
|
science: { key: 'science', name: '과학', badge: '통합과학·물화생지', aliases: ['과학', '통합과학', '물리', '화학', '생명과학', '지구과학', '과탐'] },
|
|
social: { key: 'social', name: '사회', badge: '통합사회·한국사', aliases: ['사회', '통합사회', '한국사', '사탐', '역사', '사회문화', '생활과윤리'] }
|
|
}
|
|
|
|
// 학년 정의
|
|
export const GRADES_LIST = [
|
|
{ key: 'middle', name: '중학생', shortName: '중등', aliases: ['중학생', '중1', '중2', '중3', '중등'] },
|
|
{ key: 'high', name: '고등학생', shortName: '고등', aliases: ['고등학생', '고1', '고2', '고3', '고등', '예비고1', 'N수', '재수'] },
|
|
{ key: 'elem', name: '초등학생', shortName: '초등', aliases: ['초등학생', '초등', '초1', '초2', '초3', '초4', '초5', '초6'] }
|
|
]
|
|
|
|
// 동 이름으로 지역 검색
|
|
export function getRegionByDong(dongQuery: string): RegionInfo | null {
|
|
if (!dongQuery) return null
|
|
const clean = dongQuery.trim().replace(/[동읍면리가]$/, '')
|
|
const map = regionsData.dongMap as Record<string, RegionInfo[]>
|
|
|
|
// 1. 정확 매칭
|
|
if (map[dongQuery] && map[dongQuery].length > 0) {
|
|
return map[dongQuery][0]
|
|
}
|
|
// 2. '동', '읍', '면' 붙여서 매칭
|
|
for (const suffix of ['동', '읍', '면']) {
|
|
if (map[clean + suffix] && map[clean + suffix].length > 0) {
|
|
return map[clean + suffix][0]
|
|
}
|
|
}
|
|
// 3. 리스트 전체 순회 매칭
|
|
const found = (regionsData.list as RegionInfo[]).find(r =>
|
|
r.dong === dongQuery ||
|
|
r.dong === (clean + '동') ||
|
|
r.dong === (clean + '읍') ||
|
|
r.dong === (clean + '면') ||
|
|
(clean.length >= 2 && r.dong.startsWith(clean))
|
|
)
|
|
return found || null
|
|
}
|
|
|
|
// 17개 시도 축약 매핑 및 헬퍼 함수 (예: 대구광역시 -> 대구, 서울특별시 -> 서울)
|
|
export function getShortSido(sido: string): string {
|
|
if (!sido) return ''
|
|
const trimmed = sido.trim()
|
|
if (trimmed.includes('서울')) return '서울'
|
|
if (trimmed.includes('대구')) return '대구'
|
|
if (trimmed.includes('부산')) return '부산'
|
|
if (trimmed.includes('인천')) return '인천'
|
|
if (trimmed.includes('광주')) return '광주'
|
|
if (trimmed.includes('대전')) return '대전'
|
|
if (trimmed.includes('울산')) return '울산'
|
|
if (trimmed.includes('세종')) return '세종'
|
|
if (trimmed.includes('경기')) return '경기'
|
|
if (trimmed.includes('강원')) return '강원'
|
|
if (trimmed.includes('충북') || trimmed.includes('충청북')) return '충북'
|
|
if (trimmed.includes('충남') || trimmed.includes('충청남')) return '충남'
|
|
if (trimmed.includes('전북') || trimmed.includes('전라북')) return '전북'
|
|
if (trimmed.includes('전남') || trimmed.includes('전라남')) return '전남'
|
|
if (trimmed.includes('경북') || trimmed.includes('경상북')) return '경북'
|
|
if (trimmed.includes('경남') || trimmed.includes('경상남')) return '경남'
|
|
if (trimmed.includes('제주')) return '제주'
|
|
return trimmed.slice(0, 2)
|
|
}
|
|
|
|
// 축약 지역명 생성 (예: '대구 황금동', '서울 대치동', '경기 정발산동')
|
|
export function getShortRegionName(sido: string, dong: string, sigungu?: string): string {
|
|
const meta = regionsData as any
|
|
const prefix = meta.keywordPrefix !== undefined ? meta.keywordPrefix : `${getShortSido(sido)} `
|
|
if (prefix === '') {
|
|
// 서울 (예: 강남구 개포동)
|
|
return `${sigungu ? sigungu + ' ' : ''}${dong || ''}`.trim()
|
|
}
|
|
// 경기, 인천, 광주, 부산, 대구, 대전 (예: 경기 수원 영통동, 대구 수성구 범어동)
|
|
return `${prefix}${sigungu ? sigungu + ' ' : ''}${dong || ''}`.trim()
|
|
}
|
|
|
|
// 지능형 URL 슬러그 파서
|
|
export function parseRouteSlug(slugInput: string | string[]): ParsedRouteParams {
|
|
let tokens: string[] = []
|
|
|
|
if (Array.isArray(slugInput)) {
|
|
tokens = slugInput.flatMap(s => decodeURIComponent(s).split(/[-_\s/]+/))
|
|
} else if (typeof slugInput === 'string' && slugInput.trim()) {
|
|
tokens = decodeURIComponent(slugInput).split(/[-_\s/]+/)
|
|
}
|
|
tokens = tokens.map(t => t.trim()).filter(t => Boolean(t) && !/^index\.html?$/i.test(t))
|
|
|
|
let matchedRegion: RegionInfo | null = null
|
|
let matchedTokenIdx = -1
|
|
let detectedGrade = '중학생'
|
|
let detectedGradeShort = '중등'
|
|
let detectedSubjectKey = 'math'
|
|
let detectedSubjectName = '수학'
|
|
let detectedTerm = '과외'
|
|
|
|
// 1. 토큰들 중 동 이름 매칭
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
const token = tokens[i]
|
|
const reg = getRegionByDong(token)
|
|
if (reg) {
|
|
matchedRegion = reg
|
|
matchedTokenIdx = i
|
|
break
|
|
}
|
|
}
|
|
|
|
// 2. 동 이름이 토큰에 없으면 전체 리스트에서 부분 일치 검색
|
|
if (!matchedRegion && tokens.length > 0) {
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
const token = tokens[i]
|
|
const found = (regionsData.list as RegionInfo[]).find(r => token.includes(r.dong) || (token.length >= 2 && r.dong.includes(token)))
|
|
if (found) {
|
|
matchedRegion = found
|
|
matchedTokenIdx = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// 기본값 (타겟 지역 중 첫 번째 기준)
|
|
if (!matchedRegion) {
|
|
matchedRegion = (regionsData.list as RegionInfo[])[0] || {
|
|
fullName: '부산광역시 해운대구 우동',
|
|
sido: '부산광역시',
|
|
sigungu: '해운대구',
|
|
dong: '우동'
|
|
}
|
|
}
|
|
|
|
// 3. 학년 추출 (동 이름 토큰을 제외한 나머지 토큰에서만 검색하여 '고등동', '문학동' 등 별칭 충돌 원천 차단)
|
|
let hasExplicitGrade = false
|
|
const remainingTokens = tokens.filter((_, idx) => idx !== matchedTokenIdx)
|
|
const fullText = remainingTokens.join(' ')
|
|
for (const g of GRADES_LIST) {
|
|
if (g.aliases.some(a => fullText.includes(a))) {
|
|
detectedGrade = g.name
|
|
detectedGradeShort = g.shortName
|
|
hasExplicitGrade = true
|
|
break
|
|
}
|
|
}
|
|
|
|
const gradeShort = detectedGradeShort || (detectedGrade === '초등학생' ? '초등' : detectedGrade === '고등학생' ? '고등' : '중등')
|
|
|
|
// 4. 과목 추출
|
|
let hasExplicitSubject = false
|
|
for (const [key, sub] of Object.entries(SUBJECTS_MAP)) {
|
|
if (sub.aliases.some(a => fullText.includes(a))) {
|
|
detectedSubjectKey = key
|
|
detectedSubjectName = sub.name
|
|
hasExplicitSubject = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// 5. 목적/행동 추출
|
|
if (fullText.includes('내신')) detectedTerm = '내신대비'
|
|
else if (fullText.includes('학원')) detectedTerm = '학원추천'
|
|
else if (fullText.includes('수능')) detectedTerm = '수능대비'
|
|
else if (fullText.includes('기출')) detectedTerm = '기출문제분석'
|
|
else detectedTerm = '1:1 맞춤과외'
|
|
|
|
const shortSido = getShortSido(matchedRegion.sido)
|
|
const shortRegionName = getShortRegionName(matchedRegion.sido, matchedRegion.dong, matchedRegion.sigungu)
|
|
const fullKeyword = `${shortRegionName} ${detectedGrade} ${detectedSubjectName} ${detectedTerm}`
|
|
|
|
return {
|
|
region: matchedRegion,
|
|
sido: matchedRegion.sido,
|
|
shortSido,
|
|
sigungu: matchedRegion.sigungu,
|
|
dong: matchedRegion.dong,
|
|
shortRegionName,
|
|
grade: detectedGrade,
|
|
gradeShort,
|
|
subjectKey: detectedSubjectKey,
|
|
subjectName: detectedSubjectName,
|
|
targetTerm: detectedTerm,
|
|
fullKeyword,
|
|
isRegionOnly: !hasExplicitGrade && !hasExplicitSubject
|
|
}
|
|
}
|
|
|
|
export function getAllSubjectsList() {
|
|
return Object.values(SUBJECTS_MAP)
|
|
}
|
|
|
|
export function getCurriculumBySubject(subjectKey: string) {
|
|
const data = curriculumsData as any
|
|
return data[subjectKey] || data['english']
|
|
}
|
|
|
|
export function getPopularRegions(count = 20) {
|
|
return (regionsData.list as RegionInfo[]).slice(0, count)
|
|
}
|
|
|
|
export function getTotalRegionsCount() {
|
|
return regionsData.total
|
|
}
|