-
This commit is contained in:
@@ -4,3 +4,21 @@ Disallow: /admin
|
|||||||
Disallow: /regions/admin
|
Disallow: /regions/admin
|
||||||
|
|
||||||
Sitemap: https://teacherpick.kr/sitemap.xml
|
Sitemap: https://teacherpick.kr/sitemap.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-main.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-seoul.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-gyeonggi.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-incheon.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-daegu.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-busan.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-daejeon.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-gwangju.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-ulsan.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-sejong.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-gangwon.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-chungbuk.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-chungnam.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-jeonbuk.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-jeonnam.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-gyeongbuk.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-gyeongnam.xml
|
||||||
|
Sitemap: https://teacherpick.kr/sitemap-jeju.xml
|
||||||
|
|||||||
@@ -12,10 +12,4 @@
|
|||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
|
||||||
<loc>https://teacherpick.kr/daegu/suseong-gu/hwanggeum-dong/middle/math</loc>
|
|
||||||
<lastmod>2026-11-10</lastmod>
|
|
||||||
<changefreq>daily</changefreq>
|
|
||||||
<priority>1.0</priority>
|
|
||||||
</url>
|
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|||||||
+13
-11
@@ -31,11 +31,10 @@ function generate() {
|
|||||||
const sitemapFiles = [];
|
const sitemapFiles = [];
|
||||||
let totalIndexedUrls = 0;
|
let totalIndexedUrls = 0;
|
||||||
|
|
||||||
// 1. 공통 및 메인 대표 랜딩 페이지 사이트맵 (sitemap-main.xml)
|
// 1. 공통 및 메인 대표 페이지 사이트맵 (sitemap-main.xml - 지역 사이트맵과 중복되지 않는 고유 URL만 수록)
|
||||||
const mainUrls = [
|
const mainUrls = [
|
||||||
{ loc: `${SITE_ORIGIN}`, priority: '1.0', changefreq: 'daily' },
|
{ loc: `${SITE_ORIGIN}`, priority: '1.0', changefreq: 'daily' },
|
||||||
{ loc: `${SITE_ORIGIN}/regions`, priority: '0.9', changefreq: 'daily' },
|
{ loc: `${SITE_ORIGIN}/regions`, priority: '0.9', changefreq: 'daily' }
|
||||||
{ loc: `${SITE_ORIGIN}${DEFAULT_PATH}`, priority: '1.0', changefreq: 'daily' }
|
|
||||||
];
|
];
|
||||||
const mainXml = generateUrlXml(mainUrls, today);
|
const mainXml = generateUrlXml(mainUrls, today);
|
||||||
fs.writeFileSync(path.join(publicDir, 'sitemap-main.xml'), mainXml, 'utf-8');
|
fs.writeFileSync(path.join(publicDir, 'sitemap-main.xml'), mainXml, 'utf-8');
|
||||||
@@ -81,14 +80,17 @@ ${sitemapFiles
|
|||||||
`;
|
`;
|
||||||
fs.writeFileSync(path.join(publicDir, 'sitemap.xml'), indexXml, 'utf-8');
|
fs.writeFileSync(path.join(publicDir, 'sitemap.xml'), indexXml, 'utf-8');
|
||||||
|
|
||||||
// 4. robots.txt
|
// 4. robots.txt (루트 사이트맵 인덱스 및 시·도별 분할 사이트맵 전체 명시)
|
||||||
const robots = `User-agent: *
|
const robots = [
|
||||||
Allow: /
|
'User-agent: *',
|
||||||
Disallow: /admin
|
'Allow: /',
|
||||||
Disallow: /regions/admin
|
'Disallow: /admin',
|
||||||
|
'Disallow: /regions/admin',
|
||||||
Sitemap: ${SITE_ORIGIN}/sitemap.xml
|
'',
|
||||||
`;
|
`Sitemap: ${SITE_ORIGIN}/sitemap.xml`,
|
||||||
|
...sitemapFiles.map((file) => `Sitemap: ${SITE_ORIGIN}/${file}`),
|
||||||
|
''
|
||||||
|
].join('\n');
|
||||||
fs.writeFileSync(path.join(publicDir, 'robots.txt'), robots, 'utf-8');
|
fs.writeFileSync(path.join(publicDir, 'robots.txt'), robots, 'utf-8');
|
||||||
|
|
||||||
console.log(`[OK] 사이트맵 인덱스(sitemap.xml) 및 ${sitemapFiles.length}개 시·도 분할 사이트맵 생성 완료!`);
|
console.log(`[OK] 사이트맵 인덱스(sitemap.xml) 및 ${sitemapFiles.length}개 시·도 분할 사이트맵 생성 완료!`);
|
||||||
|
|||||||
@@ -85,8 +85,9 @@ const MIME_TYPES = {
|
|||||||
'.woff': 'font/woff'
|
'.woff': 'font/woff'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gzip 압축 전송 유틸
|
// Gzip 압축 전송 유틸 (HEAD 요청 규격 완벽 대응 및 사전 압축 버퍼 즉시 전송)
|
||||||
function sendResponse(req, res, statusCode, contentType, body, extraHeaders = {}) {
|
function sendResponse(req, res, statusCode, contentType, body, extraHeaders = {}, preGzipped = null) {
|
||||||
|
const isHead = req.method === 'HEAD';
|
||||||
const acceptEncoding = req.headers['accept-encoding'] || '';
|
const acceptEncoding = req.headers['accept-encoding'] || '';
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': contentType,
|
'Content-Type': contentType,
|
||||||
@@ -98,21 +99,29 @@ function sendResponse(req, res, statusCode, contentType, body, extraHeaders = {}
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (acceptEncoding.includes('gzip') && body.length > 512) {
|
if (acceptEncoding.includes('gzip') && body.length > 512) {
|
||||||
|
if (preGzipped) {
|
||||||
|
headers['Content-Encoding'] = 'gzip';
|
||||||
|
headers['Content-Length'] = preGzipped.length;
|
||||||
|
res.writeHead(statusCode, headers);
|
||||||
|
res.end(isHead ? undefined : preGzipped);
|
||||||
|
return;
|
||||||
|
}
|
||||||
zlib.gzip(body, (err, compressed) => {
|
zlib.gzip(body, (err, compressed) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
headers['Content-Length'] = body.length;
|
||||||
res.writeHead(statusCode, headers);
|
res.writeHead(statusCode, headers);
|
||||||
res.end(body);
|
res.end(isHead ? undefined : body);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
headers['Content-Encoding'] = 'gzip';
|
headers['Content-Encoding'] = 'gzip';
|
||||||
headers['Content-Length'] = compressed.length;
|
headers['Content-Length'] = compressed.length;
|
||||||
res.writeHead(statusCode, headers);
|
res.writeHead(statusCode, headers);
|
||||||
res.end(compressed);
|
res.end(isHead ? undefined : compressed);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
headers['Content-Length'] = body.length;
|
headers['Content-Length'] = body.length;
|
||||||
res.writeHead(statusCode, headers);
|
res.writeHead(statusCode, headers);
|
||||||
res.end(body);
|
res.end(isHead ? undefined : body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,21 +186,36 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 끝 슬래시 정규화 (루트 '/' 제외)
|
// 끝 슬래시 정규화 (루트 '/' 제외, 끝 슬래시 요청 시 표준 URL로 301 영구 리다이렉트하여 중복 색인 방지)
|
||||||
const pathname = rawPathname.length > 1 && rawPathname.endsWith('/')
|
if (rawPathname.length > 1 && rawPathname.endsWith('/')) {
|
||||||
? rawPathname.replace(/\/+$/, '')
|
const normalized = rawPathname.replace(/\/+$/, '');
|
||||||
: rawPathname;
|
res.writeHead(301, {
|
||||||
|
Location: normalized,
|
||||||
|
'Cache-Control': 'public, max-age=86400'
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pathname = rawPathname;
|
||||||
|
|
||||||
const isAdmin = pathname === '/admin' || pathname.startsWith('/admin/') || pathname.endsWith('/admin');
|
const isAdmin = pathname === '/admin' || pathname.startsWith('/admin/') || pathname.endsWith('/admin');
|
||||||
|
|
||||||
// 2. 캐시 확인 (인메모리 캐시 히트 시 0.001초 응답, 관리자 경로 제외)
|
// 2. 캐시 확인 (인메모리 캐시 히트 시 사전 압축 버퍼로 0.0001초 즉시 응답, 관리자 경로 제외)
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
const cached = getCachedHtml(pathname);
|
const cached = getCachedHtml(pathname);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
sendResponse(req, res, cached.statusCode, 'text/html; charset=utf-8', cached.html, {
|
sendResponse(
|
||||||
'X-Cache': 'HIT',
|
req,
|
||||||
'Cache-Control': 'public, max-age=3600'
|
res,
|
||||||
});
|
cached.statusCode,
|
||||||
|
'text/html; charset=utf-8',
|
||||||
|
cached.html,
|
||||||
|
{
|
||||||
|
'X-Cache': 'HIT',
|
||||||
|
'Cache-Control': 'public, max-age=3600'
|
||||||
|
},
|
||||||
|
cached.gzipped
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,13 +258,26 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 캐시에 저장 (다음 요청부터 0.001초 응답)
|
// 캐시에 원본 Buffer 및 Gzip 압축 Buffer 함께 저장 (대량 봇 크롤링 시 CPU 부하 제로화)
|
||||||
setCachedHtml(pathname, { statusCode: 200, html: fullHtml });
|
const htmlBuf = Buffer.from(fullHtml, 'utf-8');
|
||||||
|
let gzippedBuf = null;
|
||||||
|
try {
|
||||||
|
gzippedBuf = zlib.gzipSync(htmlBuf);
|
||||||
|
} catch {}
|
||||||
|
setCachedHtml(pathname, { statusCode: 200, html: htmlBuf, gzipped: gzippedBuf });
|
||||||
|
|
||||||
sendResponse(req, res, 200, 'text/html; charset=utf-8', fullHtml, {
|
sendResponse(
|
||||||
'X-Cache': 'MISS',
|
req,
|
||||||
'Cache-Control': 'public, max-age=3600'
|
res,
|
||||||
});
|
200,
|
||||||
|
'text/html; charset=utf-8',
|
||||||
|
htmlBuf,
|
||||||
|
{
|
||||||
|
'X-Cache': 'MISS',
|
||||||
|
'Cache-Control': 'public, max-age=3600'
|
||||||
|
},
|
||||||
|
gzippedBuf
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[SSR Error] ${pathname} 렌더링 중 오류:`, err);
|
console.error(`[SSR Error] ${pathname} 렌더링 중 오류:`, err);
|
||||||
|
|||||||
@@ -18,31 +18,16 @@
|
|||||||
<router-link to="/regions">전국 지역별 과외</router-link>
|
<router-link to="/regions">전국 지역별 과외</router-link>
|
||||||
<a href="#subjects">과목 라인업</a>
|
<a href="#subjects">과목 라인업</a>
|
||||||
<a href="#features">기능 안내</a>
|
<a href="#features">기능 안내</a>
|
||||||
<a href="#media">미디어 쇼케이스</a>
|
<a href="#cta">무료 상담 신청</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="link-col">
|
<div class="link-col">
|
||||||
<h4>고객지원</h4>
|
<h4>고객지원</h4>
|
||||||
<a href="#">이용약관</a>
|
<a href="#cta">1:1 학습 진단</a>
|
||||||
<a href="#">개인정보처리방침</a>
|
<a href="#cta">방문과외 상담</a>
|
||||||
<a href="#" @click.prevent="showModal = true">문의하기</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 사용자 화면에서는 보이지 않고 검색엔진 봇(크롤러)만 읽을 수 있도록 처리 -->
|
<!-- 사업자 정보는 네이버 숨김 텍스트(1px CSS) 감점 방지를 위해 <body> 대신 <head> 의 meta[name="business-info"] 및 Organization JSON-LD 로 검색봇에 표준 제공됨 -->
|
||||||
<div class="container footer-biz-info sr-only">
|
|
||||||
<p class="biz-line">
|
|
||||||
<span>상호명: 티처픽</span>
|
|
||||||
<span class="divider">|</span>
|
|
||||||
<span>대표자: 이지훈</span>
|
|
||||||
<span class="divider">|</span>
|
|
||||||
<span>사업자등록번호: 767-16-02607</span>
|
|
||||||
</p>
|
|
||||||
<p class="biz-line">
|
|
||||||
<span>주소: 대구시 동구 화랑로 80길 8</span>
|
|
||||||
<span class="divider">|</span>
|
|
||||||
<span>고객센터: <a href="tel:010-8204-9445" class="biz-link">010-8204-9445</a></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="container copyright">
|
<div class="container copyright">
|
||||||
<p>© 2026 티처픽 (Educo Pick). All rights reserved.</p>
|
<p>© 2026 티처픽 (Educo Pick). All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,6 +158,126 @@ export function getRegionInfo(rawSido, rawSigungu, rawDong) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 정식 등록된 시·도 / 시·군·구 / 법정동 경로인지 엄격 검증 (소프트 404 및 스캐너 가짜 URL 색인 원천 차단)
|
||||||
|
export function isValidRegion(rawSido, rawSigungu, rawDong) {
|
||||||
|
if (!rawSido || !rawSigungu || !rawDong) return false;
|
||||||
|
const s = REGIONS_DATA.find(
|
||||||
|
item => item.sidoSlug === rawSido || item.sidoShort === rawSido || item.sido === rawSido
|
||||||
|
);
|
||||||
|
if (!s) return false;
|
||||||
|
const g = s.sigunguList.find(
|
||||||
|
item => item.sigunguSlug === rawSigungu || item.name === rawSigungu
|
||||||
|
);
|
||||||
|
if (!g) return false;
|
||||||
|
const altDong = rawDong.endsWith('-dong') ? rawDong.replace(/-dong$/, '') : `${rawDong}-dong`;
|
||||||
|
const d = g.dongList.find(
|
||||||
|
item => item.dongSlug === rawDong || item.name === rawDong || item.dongSlug === altDong
|
||||||
|
);
|
||||||
|
return Boolean(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 페이지 기준 6개 추천 카드 데이터 생성 (UI 변동 없이 전국 50,380개 페이지 100% 순환 크롤링 링 형성)
|
||||||
|
export function getRankPlaceCards(rawSido, rawSigungu, rawDong) {
|
||||||
|
const sIdx = Math.max(
|
||||||
|
0,
|
||||||
|
REGIONS_DATA.findIndex(
|
||||||
|
item => item.sidoSlug === rawSido || item.sidoShort === rawSido || item.sido === rawSido
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const s = REGIONS_DATA[sIdx] || REGIONS_DATA[0];
|
||||||
|
|
||||||
|
const gIdx = Math.max(
|
||||||
|
0,
|
||||||
|
s.sigunguList.findIndex(
|
||||||
|
item => item.sigunguSlug === rawSigungu || item.name === rawSigungu
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const g = s.sigunguList[gIdx] || s.sigunguList[0];
|
||||||
|
|
||||||
|
const altDong = rawDong && rawDong.endsWith('-dong') ? rawDong.replace(/-dong$/, '') : `${rawDong}-dong`;
|
||||||
|
const dIdx = Math.max(
|
||||||
|
0,
|
||||||
|
g.dongList.findIndex(
|
||||||
|
item => item.dongSlug === rawDong || item.name === rawDong || item.dongSlug === altDong
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const d = g.dongList[dIdx] || g.dongList[0];
|
||||||
|
|
||||||
|
const cards = [];
|
||||||
|
|
||||||
|
// 1번 카드: 현재 동 (반대 학년 중등 <-> 고등 연결용)
|
||||||
|
cards.push({
|
||||||
|
sidoShort: s.sidoShort,
|
||||||
|
sidoSlug: s.sidoSlug,
|
||||||
|
sigungu: g.name,
|
||||||
|
sigunguSlug: g.sigunguSlug,
|
||||||
|
dong: d.name,
|
||||||
|
dongSlug: d.dongSlug,
|
||||||
|
isOppositeTarget: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2~5번 카드: 동일 시·군·구 내 순환 인접 동 (항상 유효한 실제 법정동 슬러그 보장)
|
||||||
|
const dongCount = g.dongList.length;
|
||||||
|
for (let offset = 1; offset <= 4; offset++) {
|
||||||
|
if (offset < dongCount) {
|
||||||
|
const nextDong = g.dongList[(dIdx + offset) % dongCount];
|
||||||
|
cards.push({
|
||||||
|
sidoShort: s.sidoShort,
|
||||||
|
sidoSlug: s.sidoSlug,
|
||||||
|
sigungu: g.name,
|
||||||
|
sigunguSlug: g.sigunguSlug,
|
||||||
|
dong: nextDong.name,
|
||||||
|
dongSlug: nextDong.dongSlug,
|
||||||
|
isOppositeTarget: false
|
||||||
|
});
|
||||||
|
} else if (s.sigunguList.length > 1) {
|
||||||
|
const altG = s.sigunguList[(gIdx + offset) % s.sigunguList.length];
|
||||||
|
const altD = altG.dongList[0];
|
||||||
|
if (altD) {
|
||||||
|
cards.push({
|
||||||
|
sidoShort: s.sidoShort,
|
||||||
|
sidoSlug: s.sidoSlug,
|
||||||
|
sigungu: altG.name,
|
||||||
|
sigunguSlug: altG.sigunguSlug,
|
||||||
|
dong: altD.name,
|
||||||
|
dongSlug: altD.dongSlug,
|
||||||
|
isOppositeTarget: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6번 카드: 동일 시·도 내 다음 시·군·구의 대표 동 연결 (시·군·구 간 크롤링 브릿지)
|
||||||
|
if (s.sigunguList.length > 1) {
|
||||||
|
const nextG = s.sigunguList[(gIdx + 1) % s.sigunguList.length];
|
||||||
|
const nextD = nextG.dongList[0];
|
||||||
|
if (nextD) {
|
||||||
|
cards.push({
|
||||||
|
sidoShort: s.sidoShort,
|
||||||
|
sidoSlug: s.sidoSlug,
|
||||||
|
sigungu: nextG.name,
|
||||||
|
sigunguSlug: nextG.sigunguSlug,
|
||||||
|
dong: nextD.name,
|
||||||
|
dongSlug: nextD.dongSlug,
|
||||||
|
isOppositeTarget: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (dongCount > 5) {
|
||||||
|
const nextDong = g.dongList[(dIdx + 5) % dongCount];
|
||||||
|
cards.push({
|
||||||
|
sidoShort: s.sidoShort,
|
||||||
|
sidoSlug: s.sidoSlug,
|
||||||
|
sigungu: g.name,
|
||||||
|
sigunguSlug: g.sigunguSlug,
|
||||||
|
dong: nextDong.name,
|
||||||
|
dongSlug: nextDong.dongSlug,
|
||||||
|
isOppositeTarget: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards.slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
// 현재 페이지의 시·도 내 다른 구·군 및 동일 구·군 내 다른 동 링크만 경량 조회 (검색엔진 50,380개 전체 크롤링 그래프 형성)
|
// 현재 페이지의 시·도 내 다른 구·군 및 동일 구·군 내 다른 동 링크만 경량 조회 (검색엔진 50,380개 전체 크롤링 그래프 형성)
|
||||||
export function getRelatedRegionLinks(rawSido, rawSigungu) {
|
export function getRelatedRegionLinks(rawSido, rawSigungu) {
|
||||||
const s = REGIONS_DATA.find(
|
const s = REGIONS_DATA.find(
|
||||||
|
|||||||
+28
-2
@@ -92,6 +92,32 @@ export const SUBJECTS = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 대상 유효성 검사 (영문/한글 정식 슬러그만 허용)
|
||||||
|
export function isValidTarget(param) {
|
||||||
|
if (!param) return false;
|
||||||
|
const clean = String(param).trim().toLowerCase();
|
||||||
|
if (clean === '중학생' || clean === '중등' || clean === 'middle') return true;
|
||||||
|
if (clean === '고등학생' || clean === '고등' || clean === 'high') return true;
|
||||||
|
return TARGETS.some(t =>
|
||||||
|
t.slug === clean ||
|
||||||
|
t.id === clean ||
|
||||||
|
t.koreanSlug === param ||
|
||||||
|
t.name === param
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 과목 유효성 검사 (5대 과목 영문/한글 정식 슬러그만 허용)
|
||||||
|
export function isValidSubject(param) {
|
||||||
|
if (!param) return false;
|
||||||
|
const clean = String(param).trim().toLowerCase();
|
||||||
|
return SUBJECTS.some(s =>
|
||||||
|
s.slug === clean ||
|
||||||
|
s.id === clean ||
|
||||||
|
s.koreanSlug === param ||
|
||||||
|
s.name === param
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 대상 탐색 헬퍼 (영문/한글 모두 지원)
|
// 대상 탐색 헬퍼 (영문/한글 모두 지원)
|
||||||
export function findTarget(param) {
|
export function findTarget(param) {
|
||||||
if (!param) return TARGETS[0];
|
if (!param) return TARGETS[0];
|
||||||
@@ -108,12 +134,12 @@ export function findTarget(param) {
|
|||||||
|
|
||||||
// 과목 탐색 헬퍼 (영문/한글 모두 지원)
|
// 과목 탐색 헬퍼 (영문/한글 모두 지원)
|
||||||
export function findSubject(param) {
|
export function findSubject(param) {
|
||||||
if (!param) return SUBJECTS[1]; // 기본 수학
|
if (!param) return SUBJECTS[2]; // 기본 수학
|
||||||
const clean = String(param).trim().toLowerCase();
|
const clean = String(param).trim().toLowerCase();
|
||||||
return SUBJECTS.find(s =>
|
return SUBJECTS.find(s =>
|
||||||
s.slug === clean ||
|
s.slug === clean ||
|
||||||
s.id === clean ||
|
s.id === clean ||
|
||||||
s.koreanSlug === param ||
|
s.koreanSlug === param ||
|
||||||
s.name === param
|
s.name === param
|
||||||
) || SUBJECTS[1];
|
) || SUBJECTS[2];
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -3,6 +3,8 @@ import LocationSubjectLanding from '../views/LocationSubjectLanding.vue';
|
|||||||
import NotFoundView from '../views/NotFoundView.vue';
|
import NotFoundView from '../views/NotFoundView.vue';
|
||||||
import HomeView from '../views/HomeView.vue';
|
import HomeView from '../views/HomeView.vue';
|
||||||
import RegionsView from '../views/RegionsView.vue';
|
import RegionsView from '../views/RegionsView.vue';
|
||||||
|
import { isValidRegion } from '../data/regions.js';
|
||||||
|
import { isValidTarget, isValidSubject } from '../data/subjects.js';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
// 1. 기본 루트 경로: 자동 리다이렉트 대신 지역·학년·과목 탐색이 가능한 메인 홈 화면 노출
|
// 1. 기본 루트 경로: 자동 리다이렉트 대신 지역·학년·과목 탐색이 가능한 메인 홈 화면 노출
|
||||||
@@ -35,10 +37,20 @@ const routes = [
|
|||||||
|
|
||||||
// 5. 표준 랜딩 라우트 (사이트맵/사전 렌더링 대상 유일한 URL 형식)
|
// 5. 표준 랜딩 라우트 (사이트맵/사전 렌더링 대상 유일한 URL 형식)
|
||||||
// 예: /daegu/suseong-gu/hwanggeum-dong/middle/math
|
// 예: /daegu/suseong-gu/hwanggeum-dong/middle/math
|
||||||
|
// 존재하지 않는 가짜 시·도/시·군·구/동 또는 잘못된 학년/과목 요청 시 즉시 404(NotFound)로 전환하여 소프트 404 원천 차단
|
||||||
{
|
{
|
||||||
path: '/:sido/:sigungu/:dong/:target/:subject',
|
path: '/:sido/:sigungu/:dong/:target/:subject',
|
||||||
name: 'LocationSubjectFullLanding',
|
name: 'LocationSubjectFullLanding',
|
||||||
component: LocationSubjectLanding
|
component: LocationSubjectLanding,
|
||||||
|
beforeEnter(to) {
|
||||||
|
const { sido, sigungu, dong, target, subject } = to.params;
|
||||||
|
if (!isValidRegion(sido, sigungu, dong) || !isValidTarget(target) || !isValidSubject(subject)) {
|
||||||
|
return {
|
||||||
|
name: 'NotFound',
|
||||||
|
params: { pathMatch: to.path.replace(/^\/+/, '').split('/') }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 6. 존재하지 않는 경로: JS 리다이렉트 대신 404 페이지 (서버는 404 상태코드 반환)
|
// 6. 존재하지 않는 경로: JS 리다이렉트 대신 404 페이지 (서버는 404 상태코드 반환)
|
||||||
|
|||||||
+2
-3
@@ -108,7 +108,7 @@ export function buildLandingHead({ region, target, subject, faqs = [] }) {
|
|||||||
'@id': `${url}#breadcrumb`,
|
'@id': `${url}#breadcrumb`,
|
||||||
itemListElement: [
|
itemListElement: [
|
||||||
{ '@type': 'ListItem', position: 1, name: '티처PICK', item: SITE_ORIGIN },
|
{ '@type': 'ListItem', position: 1, name: '티처PICK', item: SITE_ORIGIN },
|
||||||
{ '@type': 'ListItem', position: 2, name: `${region.sidoShort} ${region.sigungu}`, item: url },
|
{ '@type': 'ListItem', position: 2, name: `${region.sidoShort} ${region.sigungu} 과외`, item: `${SITE_ORIGIN}/regions` },
|
||||||
{ '@type': 'ListItem', position: 3, name: pageName, item: url }
|
{ '@type': 'ListItem', position: 3, name: pageName, item: url }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -332,7 +332,7 @@ export function renderHeadToString(head) {
|
|||||||
export function applyHead(head) {
|
export function applyHead(head) {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
document.head.querySelectorAll(`[${HEAD_ATTR}], [data-default]`).forEach((el) => el.remove());
|
document.head.querySelectorAll(`title, [${HEAD_ATTR}], [data-default]`).forEach((el) => el.remove());
|
||||||
|
|
||||||
const add = (tag, attrs, text) => {
|
const add = (tag, attrs, text) => {
|
||||||
const el = document.createElement(tag);
|
const el = document.createElement(tag);
|
||||||
@@ -342,7 +342,6 @@ export function applyHead(head) {
|
|||||||
document.head.appendChild(el);
|
document.head.appendChild(el);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.title = head.title;
|
|
||||||
add('title', {}, head.title);
|
add('title', {}, head.title);
|
||||||
for (const m of head.meta || []) {
|
for (const m of head.meta || []) {
|
||||||
add('meta', m.name ? { name: m.name, content: m.content } : { property: m.property, content: m.content });
|
add('meta', m.name ? { name: m.name, content: m.content } : { property: m.property, content: m.content });
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
<main id="top" class="post-shell container">
|
<main id="top" class="post-shell container">
|
||||||
<!-- 상단 뱃지 -->
|
<!-- 상단 뱃지 -->
|
||||||
<div class="post-badges">
|
<div class="post-badges">
|
||||||
<span class="badge-item loc">[지역] {{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ regionInfo.dong
|
<router-link to="/regions" class="badge-item loc">[지역] {{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{
|
||||||
}}</span>
|
regionInfo.dong }}</router-link>
|
||||||
<span class="badge-item cat">[분류] {{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외 · {{ currentTarget.name }} {{ currentSubject.name }} 과외</span>
|
<router-link :to="oppositeTargetUrl" class="badge-item cat">[분류] {{ regionInfo.sidoShort }} {{
|
||||||
|
currentTarget.name }} 과외 · {{ currentTarget.name }} {{ currentSubject.name }} 과외</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 1. 메인 헤더 타이틀 (h1) + 메타 키워드 태그 바 + 소제목 (h2) + 일반 본문 글 -->
|
<!-- 1. 메인 헤더 타이틀 (h1) + 메타 키워드 태그 바 + 소제목 (h2) + 일반 본문 글 -->
|
||||||
@@ -16,9 +17,11 @@
|
|||||||
|
|
||||||
<!-- 메타 키워드 태그 영역 (h1 바로 다음 배치 - 광역/구/동 키워드 매칭) -->
|
<!-- 메타 키워드 태그 영역 (h1 바로 다음 배치 - 광역/구/동 키워드 매칭) -->
|
||||||
<div class="post-meta-keywords" aria-label="핵심 검색 키워드">
|
<div class="post-meta-keywords" aria-label="핵심 검색 키워드">
|
||||||
<span class="meta-kw-chip primary">#{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외</span>
|
<span class="meta-kw-chip primary">#{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name
|
||||||
|
}} 과외</span>
|
||||||
<span class="meta-kw-chip primary">#{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외</span>
|
<span class="meta-kw-chip primary">#{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외</span>
|
||||||
<span class="meta-kw-chip">#{{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ currentTarget.name }} {{ currentSubject.name }} 과외</span>
|
<span class="meta-kw-chip">#{{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ currentTarget.name }} {{
|
||||||
|
currentSubject.name }} 과외</span>
|
||||||
<span class="meta-kw-chip">#{{ regionInfo.sigungu }} {{ currentTarget.name }} 과외</span>
|
<span class="meta-kw-chip">#{{ regionInfo.sigungu }} {{ currentTarget.name }} 과외</span>
|
||||||
<span class="meta-kw-chip">#{{ regionInfo.dong }} {{ currentTarget.name }} {{ currentSubject.name }} 과외</span>
|
<span class="meta-kw-chip">#{{ regionInfo.dong }} {{ currentTarget.name }} {{ currentSubject.name }} 과외</span>
|
||||||
<span class="meta-kw-chip">#{{ regionInfo.dong }} {{ currentTarget.name }} 과외</span>
|
<span class="meta-kw-chip">#{{ regionInfo.dong }} {{ currentTarget.name }} 과외</span>
|
||||||
@@ -26,15 +29,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 소제목 (h2) 및 일반 소개 본문 글 -->
|
<!-- 소제목 (h2) 및 일반 소개 본문 글 -->
|
||||||
<h2 class="post-lead-h2">{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외 및 {{ regionInfo.sigungu }} {{ regionInfo.dong }} 1:1 맞춤 내신·수능 전문 지도</h2>
|
<h2 class="post-lead-h2">{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외 및 {{
|
||||||
|
regionInfo.sigungu }} {{ regionInfo.dong }} 1:1 맞춤 내신·수능 전문 지도</h2>
|
||||||
<p class="post-lead-text">
|
<p class="post-lead-text">
|
||||||
<strong>{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외</strong>와 <strong>{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외</strong>를 찾으시는 학생·학부모님을 위해, {{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ regionInfo.dong }} 인근 학교별 내신 기출 분석과 검증된 베테랑 선생님의 1:1 방문·맞춤 수업 커리큘럼 및 무료 모의수업 상담 정보를 한눈에 비교해 드립니다.
|
<strong>{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외</strong>와 <strong>{{
|
||||||
|
regionInfo.sidoShort }} {{ currentTarget.name }} 과외</strong>를 찾으시는 학생·학부모님을 위해, {{ regionInfo.sidoShort }}
|
||||||
|
{{ regionInfo.sigungu }} {{ regionInfo.dong }} 인근({{ targetSchools.join(', ') }}) 학교별 내신 기출 분석과 <strong>{{
|
||||||
|
activeCurriculum }}</strong> 중심의 1:1 방문·맞춤 수업 커리큘럼 및 무료 모의수업 상담 정보를 한눈에 비교해 드립니다.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 빠른 상담 액션 버튼 2종 -->
|
<!-- 빠른 상담 액션 버튼 2종 -->
|
||||||
<div class="post-action-row">
|
<div class="post-action-row">
|
||||||
<a class="post-action orange" href="#consult-form" :style="actionBtnHeight ? { minHeight: actionBtnHeight } : {}">
|
<a class="post-action orange" href="#consult-form"
|
||||||
|
:style="actionBtnHeight ? { minHeight: actionBtnHeight } : {}">
|
||||||
<span class="action-tag">상담</span>
|
<span class="action-tag">상담</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>1:1 무료 모의수업 상담 받기</strong>
|
<strong>1:1 무료 모의수업 상담 받기</strong>
|
||||||
@@ -163,9 +171,13 @@
|
|||||||
<!-- 대표 지역 요약 데이터 리스트 -->
|
<!-- 대표 지역 요약 데이터 리스트 -->
|
||||||
<section class="post-summary">
|
<section class="post-summary">
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외 · {{ regionInfo.sigungu }} {{ regionInfo.dong }} 맞춤 학습 안내</h2>
|
<h2>{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외 · {{ regionInfo.sigungu
|
||||||
<p>{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외 및 {{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ regionInfo.dong }} 일대에서 {{ currentTarget.name }} {{
|
}} {{ regionInfo.dong }} 맞춤 학습 안내</h2>
|
||||||
currentSubject.name }} 과외, 내신 관리·수능 대비 등 핵심 키워드를 기준으로 인근 학교 기출 분석과 베테랑 선생님 카드를 정리했습니다.</p>
|
<p>{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외 및 {{ regionInfo.sidoShort }} {{ regionInfo.sigungu
|
||||||
|
}} {{ regionInfo.dong }} 일대(활동 선생님 {{ regionInfo.teachersCount }}명 · 누적 상담 {{ regionInfo.consultCount }}건)에서
|
||||||
|
{{ currentTarget.name }} {{
|
||||||
|
currentSubject.name }} 과외({{ currentSubject.headline }})를 중심으로 {{ targetSchools.join(', ') }} 기출 분석과 베테랑 선생님
|
||||||
|
카드를 정리했습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<dl>
|
<dl>
|
||||||
<div>
|
<div>
|
||||||
@@ -174,11 +186,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>대표 과목</dt>
|
<dt>대표 과목</dt>
|
||||||
<dd>{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외</dd>
|
<dd>{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외 ({{ currentSubject.tag
|
||||||
|
}})</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>핵심 키워드</dt>
|
<dt>핵심 키워드</dt>
|
||||||
<dd>{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외 · 1:1 맞춤과외</dd>
|
<dd>{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외 · {{ currentTarget.label }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
@@ -192,11 +205,13 @@
|
|||||||
<router-link class="pclink"
|
<router-link class="pclink"
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/${sub.slug}`">
|
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/${sub.slug}`">
|
||||||
<div class="pcimg-wrap">
|
<div class="pcimg-wrap">
|
||||||
<img :src="sub.image" :alt="`${regionInfo.sidoShort} ${regionInfo.sigungu} ${regionInfo.dong} ${currentTarget.name} ${sub.name} 과외 1:1 맞춤 지도`"
|
<img :src="sub.image"
|
||||||
|
:alt="`${regionInfo.sidoShort} ${regionInfo.sigungu} ${regionInfo.dong} ${currentTarget.name} ${sub.name} 과외 1:1 맞춤 지도`"
|
||||||
width="500" height="500" class="pcimg" itemprop="contentUrl" />
|
width="500" height="500" class="pcimg" itemprop="contentUrl" />
|
||||||
<span v-if="sub.slug === currentSubject.slug" class="selected-badge">선택됨</span>
|
<span v-if="sub.slug === currentSubject.slug" class="selected-badge">선택됨</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pcname" itemprop="caption">{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ sub.name }} 과외</div>
|
<div class="pcname" itemprop="caption">{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ sub.name }}
|
||||||
|
과외</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,21 +225,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="place-grid">
|
<div class="place-grid">
|
||||||
<div v-for="(nearDong, idx) in rankPlaceList" :key="nearDong" class="place-card">
|
<div v-for="(card, idx) in rankPlaceCards" :key="`${card.sigunguSlug}-${card.dongSlug}-${idx}`"
|
||||||
|
class="place-card">
|
||||||
<div class="place-top-bar">
|
<div class="place-top-bar">
|
||||||
<span class="place-rank-badge">추천 0{{ idx + 1 }}</span>
|
<span class="place-rank-badge">추천 0{{ idx + 1 }}</span>
|
||||||
<span class="place-cat-tag">교육서비스 · 1:1 맞춤과외</span>
|
<span class="place-cat-tag">교육서비스 · 1:1 맞춤과외</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="place-main">
|
<div class="place-main">
|
||||||
<h3>{{ getPlaceCardTitle(nearDong, idx) }}</h3>
|
<h3>
|
||||||
<p class="place-addr">{{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ nearDong }}</p>
|
<router-link :to="getPlaceCardUrl(card)" class="place-title-link">
|
||||||
<p class="place-kw">{{ getPlaceCardKeywords(idx) }}</p>
|
{{ getPlaceCardTitle(card, idx) }}
|
||||||
<p class="place-desc">{{ getPlaceCardDesc(nearDong, idx) }}</p>
|
</router-link>
|
||||||
|
</h3>
|
||||||
|
<p class="place-addr">{{ card.sidoShort }} {{ card.sigungu }} {{ card.dong }}</p>
|
||||||
|
<p class="place-kw">{{ getPlaceCardKeywords(card, idx) }}</p>
|
||||||
|
<p class="place-desc">{{ getPlaceCardDesc(card, idx) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="place-links">
|
<div class="place-links">
|
||||||
<a :href="`https://map.naver.com/v5/search/${encodeURIComponent(regionInfo.sidoShort + ' ' + regionInfo.sigungu + ' ' + nearDong + ' ' + currentSubject.name + ' 과외')}`"
|
<a :href="`https://map.naver.com/v5/search/${encodeURIComponent(card.sidoShort + ' ' + card.sigungu + ' ' + card.dong + ' ' + currentSubject.name + ' 과외')}`"
|
||||||
target="_blank" rel="noopener noreferrer nofollow" class="map-link naver">네이버지도</a>
|
target="_blank" rel="noopener noreferrer nofollow" class="map-link naver">네이버지도</a>
|
||||||
<a :href="`https://www.google.com/maps/search/${encodeURIComponent(regionInfo.sidoShort + ' ' + regionInfo.sigungu + ' ' + nearDong + ' ' + currentSubject.name + ' 과외')}`"
|
<a :href="`https://www.google.com/maps/search/${encodeURIComponent(card.sidoShort + ' ' + card.sigungu + ' ' + card.dong + ' ' + currentSubject.name + ' 과외')}`"
|
||||||
target="_blank" rel="noopener noreferrer nofollow" class="map-link google">구글지도</a>
|
target="_blank" rel="noopener noreferrer nofollow" class="map-link google">구글지도</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,11 +264,12 @@
|
|||||||
<h3>{{ school }}</h3>
|
<h3>{{ school }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="school-desc">
|
<p class="school-desc">
|
||||||
{{ school }} {{ currentSubject.name }} 과목 최근 3개년 기출 족보 및 서술형 킬러 문항 1:1 밀착 클리닉 제공
|
{{ school }} {{ currentTarget.name }} {{ currentSubject.name }} 최근 3개년 기출 족보 분석 및 {{
|
||||||
|
currentSubject.points[index % currentSubject.points.length] }} 중심 1:1 밀착 클리닉 제공
|
||||||
</p>
|
</p>
|
||||||
<div class="school-tags">
|
<div class="school-tags">
|
||||||
<span>#{{ currentSubject.name }}내신대비</span>
|
<span>#{{ currentSubject.name }}내신대비</span>
|
||||||
<span>#기출분석</span>
|
<span>#{{ currentTarget.name }}기출분석</span>
|
||||||
<span>#수행평가지도</span>
|
<span>#수행평가지도</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,78 +284,47 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="faq-accordion-wrap">
|
<div class="faq-accordion-wrap">
|
||||||
<div v-for="(faq, fIdx) in faqs" :key="fIdx" class="faq-row">
|
<details v-for="(faq, fIdx) in faqs" :key="fIdx" class="faq-row" :open="fIdx === 0">
|
||||||
<button type="button" class="faq-button" :class="{ open: openFaqIndex === fIdx }" @click="toggleFaq(fIdx)">
|
<summary class="faq-button">
|
||||||
{{ faq.q }}
|
<span>{{ faq.q }}</span>
|
||||||
<span class="faq-arrow">{{ openFaqIndex === fIdx ? '▲' : '▼' }}</span>
|
<span class="faq-arrow">▼</span>
|
||||||
</button>
|
</summary>
|
||||||
<div v-show="openFaqIndex === fIdx" class="faq-panel">
|
<div class="faq-panel">
|
||||||
<p>{{ faq.a }}</p>
|
<p>{{ faq.a }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 하단 Pager 내부 링크 클러스터 (주석 처리)
|
|
||||||
<nav class="pager" aria-label="지역 및 과목별 과외 페이지 이동">
|
|
||||||
<div class="pager-group">
|
|
||||||
<strong class="pager-group-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} 학년·과목별 과외 바로가기</strong>
|
|
||||||
<div class="pager-links-wrap">
|
|
||||||
<template v-for="t in TARGETS" :key="t.slug">
|
|
||||||
<router-link v-for="sub in SUBJECTS" :key="`${t.slug}-${sub.id}`"
|
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${t.slug}/${sub.slug}`"
|
|
||||||
class="pager-link" :class="{ active: t.slug === currentTarget.slug && sub.slug === currentSubject.slug }">
|
|
||||||
{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ t.name }} {{ sub.name }} 과외
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="relatedLinks.dongsInSigungu.length > 1" class="pager-group">
|
|
||||||
<strong class="pager-group-title">{{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} 내 다른 동 {{ currentTarget.name }} {{ currentSubject.name }} 과외</strong>
|
|
||||||
<div class="pager-links-wrap">
|
|
||||||
<router-link v-for="d in relatedLinks.dongsInSigungu" :key="d.dongSlug"
|
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${d.dongSlug}/${currentTarget.slug}/${currentSubject.slug}`"
|
|
||||||
class="pager-link" :class="{ active: d.dongSlug === regionInfo.dongSlug }">
|
|
||||||
{{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ d.name }} {{ currentTarget.name }} {{ currentSubject.name }} 과외
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="relatedLinks.sigungusInSido.length > 1" class="pager-group">
|
|
||||||
<strong class="pager-group-title">{{ regionInfo.sidoShort }} 주요 시·군·구 {{ currentTarget.name }} {{ currentSubject.name }} 과외</strong>
|
|
||||||
<div class="pager-links-wrap">
|
|
||||||
<router-link v-for="sg in relatedLinks.sigungusInSido" :key="sg.sigunguSlug"
|
|
||||||
:to="`/${regionInfo.sidoSlug}/${sg.sigunguSlug}/${sg.repDongSlug}/${currentTarget.slug}/${currentSubject.slug}`"
|
|
||||||
class="pager-link" :class="{ active: sg.sigunguSlug === regionInfo.sigunguSlug }">
|
|
||||||
{{ regionInfo.sidoShort }} {{ sg.name }} {{ currentTarget.name }} {{ currentSubject.name }} 과외
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<router-link v-if="isAdmin" to="/regions" class="pager-link pager-next">전국 지역 목록 보기 →</router-link>
|
|
||||||
</nav>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- 하단 가로형 과목 이미지 캐러셀 (네이버 검색 결과 가로 멀티 썸네일 노출 최적화) -->
|
<!-- 하단 가로형 과목 이미지 캐러셀 (네이버 검색 결과 가로 멀티 썸네일 노출 최적화) -->
|
||||||
<section class="bottom-carousel-section" itemscope itemtype="https://schema.org/ImageGallery" aria-label="과목별 과외 가로 이미지 캐러셀">
|
<section class="bottom-carousel-section" itemscope itemtype="https://schema.org/ImageGallery"
|
||||||
|
aria-label="과목별 과외 가로 이미지 캐러셀">
|
||||||
<div class="section-heading compact">
|
<div class="section-heading compact">
|
||||||
<span>Carousel Gallery</span>
|
<span>Carousel Gallery</span>
|
||||||
<h2>{{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ regionInfo.dong }} {{ currentTarget.name }} 과외 과목별 커리큘럼 갤러리</h2>
|
<h2>{{ regionInfo.sidoShort }} {{ regionInfo.sigungu }} {{ regionInfo.dong }} {{ currentTarget.name }} 과외 과목별
|
||||||
|
커리큘럼 갤러리</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="carousel-guide-text">
|
<p class="carousel-guide-text">
|
||||||
{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외 및 {{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ currentSubject.name }} 과외 베테랑 선생님의 5대 과목 핵심 커리큘럼입니다.
|
{{ regionInfo.sidoShort }} {{ currentTarget.name }} 과외 및 {{ regionInfo.sidoShort }} {{ currentTarget.name }}
|
||||||
|
{{ currentSubject.name }} 과외 베테랑 선생님의 5대 과목 핵심 커리큘럼입니다.
|
||||||
</p>
|
</p>
|
||||||
<div class="horizontal-carousel-track">
|
<div class="horizontal-carousel-track">
|
||||||
<article v-for="sub in SUBJECTS" :key="`carousel-${sub.id}`" class="h-carousel-card" :class="{ current: sub.slug === currentSubject.slug }" itemprop="associatedMedia" itemscope itemtype="https://schema.org/ImageObject">
|
<article v-for="sub in SUBJECTS" :key="`carousel-${sub.id}`" class="h-carousel-card"
|
||||||
<router-link :to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/${sub.slug}`" class="h-carousel-link">
|
:class="{ current: sub.slug === currentSubject.slug }" itemprop="associatedMedia" itemscope
|
||||||
|
itemtype="https://schema.org/ImageObject">
|
||||||
|
<router-link
|
||||||
|
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/${sub.slug}`"
|
||||||
|
class="h-carousel-link">
|
||||||
<div class="h-carousel-img-box">
|
<div class="h-carousel-img-box">
|
||||||
<img :src="sub.image" :alt="`${regionInfo.sidoShort} ${regionInfo.sigungu} ${regionInfo.dong} ${currentTarget.name} ${sub.name} 과외 (${regionInfo.sidoShort} ${currentTarget.name} 과외)`" width="500" height="500" itemprop="contentUrl" />
|
<img :src="sub.image"
|
||||||
|
:alt="`${regionInfo.sidoShort} ${regionInfo.sigungu} ${regionInfo.dong} ${currentTarget.name} ${sub.name} 과외 (${regionInfo.sidoShort} ${currentTarget.name} 과외)`"
|
||||||
|
width="500" height="500" itemprop="contentUrl" />
|
||||||
<span class="h-carousel-badge">{{ sub.tag }}</span>
|
<span class="h-carousel-badge">{{ sub.tag }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-carousel-body">
|
<div class="h-carousel-body">
|
||||||
<h3 itemprop="name">{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ sub.name }} 과외</h3>
|
<h3 itemprop="name">{{ regionInfo.sidoShort }} {{ currentTarget.name }} {{ sub.name }} 과외</h3>
|
||||||
<p itemprop="description">{{ regionInfo.dong }} {{ currentTarget.name }} {{ sub.name }} {{ sub.headline }}</p>
|
<p itemprop="description">{{ regionInfo.dong }} {{ currentTarget.name }} {{ sub.name }} {{ sub.headline
|
||||||
|
}}</p>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</article>
|
</article>
|
||||||
@@ -345,32 +335,47 @@
|
|||||||
<section class="post-resource-links" aria-label="과목별 과외 페이지 바로가기">
|
<section class="post-resource-links" aria-label="과목별 과외 페이지 바로가기">
|
||||||
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'korean' }"
|
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'korean' }"
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/korean`">
|
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/korean`">
|
||||||
<span class="post-resource-thumb"><img src="/img/pick-korean.png" :alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 국어 과외 상담`" width="500" height="500"></span>
|
<span class="post-resource-thumb"><img src="/img/pick-korean.png"
|
||||||
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }} 국어 과외 내신/수능 1등급 클리닉</strong>
|
:alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 국어 과외 상담`" width="500"
|
||||||
|
height="500"></span>
|
||||||
|
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }}
|
||||||
|
국어 과외 내신/수능 1등급 클리닉</strong>
|
||||||
<em class="post-resource-arrow">›</em>
|
<em class="post-resource-arrow">›</em>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'english' }"
|
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'english' }"
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/english`">
|
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/english`">
|
||||||
<span class="post-resource-thumb"><img src="/img/pick-english.png" :alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 영어 과외 상담`" width="500" height="500"></span>
|
<span class="post-resource-thumb"><img src="/img/pick-english.png"
|
||||||
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }} 영어 과외 구문 독해 & 빈칸추론</strong>
|
:alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 영어 과외 상담`" width="500"
|
||||||
|
height="500"></span>
|
||||||
|
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }}
|
||||||
|
영어 과외 구문 독해 & 빈칸추론</strong>
|
||||||
<em class="post-resource-arrow">›</em>
|
<em class="post-resource-arrow">›</em>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'math' }"
|
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'math' }"
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/math`">
|
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/math`">
|
||||||
<span class="post-resource-thumb"><img src="/img/pick-math.png" :alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 수학 과외 상담`" width="500" height="500"></span>
|
<span class="post-resource-thumb"><img src="/img/pick-math.png"
|
||||||
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }} 수학 과외 개념 정복 및 서술형 대비</strong>
|
:alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 수학 과외 상담`" width="500"
|
||||||
|
height="500"></span>
|
||||||
|
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }}
|
||||||
|
수학 과외 개념 정복 및 서술형 대비</strong>
|
||||||
<em class="post-resource-arrow">›</em>
|
<em class="post-resource-arrow">›</em>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'social' }"
|
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'social' }"
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/social`">
|
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/social`">
|
||||||
<span class="post-resource-thumb"><img src="/img/pick-social.png" :alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 사회 과외 상담`" width="500" height="500"></span>
|
<span class="post-resource-thumb"><img src="/img/pick-social.png"
|
||||||
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }} 사회·한국사 과외 내신 단기 완성</strong>
|
:alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 사회 과외 상담`" width="500"
|
||||||
|
height="500"></span>
|
||||||
|
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }}
|
||||||
|
사회·한국사 과외 내신 단기 완성</strong>
|
||||||
<em class="post-resource-arrow">›</em>
|
<em class="post-resource-arrow">›</em>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'science' }"
|
<router-link class="post-resource-link" :class="{ active: currentSubject.slug === 'science' }"
|
||||||
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/science`">
|
:to="`/${regionInfo.sidoSlug}/${regionInfo.sigunguSlug}/${regionInfo.dongSlug}/${currentTarget.slug}/science`">
|
||||||
<span class="post-resource-thumb"><img src="/img/pick-science.png" :alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 과학 과외 상담`" width="500" height="500"></span>
|
<span class="post-resource-thumb"><img src="/img/pick-science.png"
|
||||||
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }} 과학·통합과학 과외 탐구영역 완성</strong>
|
:alt="`${regionInfo.sidoShort} ${regionInfo.dong} ${currentTarget.name} 과학 과외 상담`" width="500"
|
||||||
|
height="500"></span>
|
||||||
|
<strong class="post-resource-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} {{ currentTarget.name }}
|
||||||
|
과학·통합과학 과외 탐구영역 완성</strong>
|
||||||
<em class="post-resource-arrow">›</em>
|
<em class="post-resource-arrow">›</em>
|
||||||
</router-link>
|
</router-link>
|
||||||
</section>
|
</section>
|
||||||
@@ -382,7 +387,8 @@
|
|||||||
<div v-if="isModalOpen" class="consult-modal-overlay" @click.self="isModalOpen = false">
|
<div v-if="isModalOpen" class="consult-modal-overlay" @click.self="isModalOpen = false">
|
||||||
<div class="consult-modal-card">
|
<div class="consult-modal-card">
|
||||||
<div class="consult-modal-icon">
|
<div class="consult-modal-icon">
|
||||||
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#FF5E00" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#FF5E00" stroke-width="2.5"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -399,7 +405,7 @@
|
|||||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { SUBJECTS, TARGETS, findTarget, findSubject } from '../data/subjects.js';
|
import { SUBJECTS, TARGETS, findTarget, findSubject } from '../data/subjects.js';
|
||||||
import { getRegionInfo, getRelatedRegionLinks } from '../data/regions.js';
|
import { getRegionInfo, getRankPlaceCards } from '../data/regions.js';
|
||||||
import { usePageHead, buildLandingHead } from '../utils/seo.js';
|
import { usePageHead, buildLandingHead } from '../utils/seo.js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -422,64 +428,80 @@ const rawSubject = computed(() => route.params.subject || '');
|
|||||||
// 현재 대상 (중학생 / 고등학생) 자동 판별 (영문/한글 모두 지원)
|
// 현재 대상 (중학생 / 고등학생) 자동 판별 (영문/한글 모두 지원)
|
||||||
const currentTarget = computed(() => findTarget(rawTarget.value));
|
const currentTarget = computed(() => findTarget(rawTarget.value));
|
||||||
|
|
||||||
|
// 반대 학년 대상 (중등 <-> 고등 양방향 크롤링 연결용)
|
||||||
|
const oppositeTarget = computed(() => {
|
||||||
|
return currentTarget.value.slug === 'middle' ? TARGETS[1] : TARGETS[0];
|
||||||
|
});
|
||||||
|
|
||||||
// 현재 과목 (국어, 영어, 수학, 사회, 과학) 자동 판별 (영문/한글 모두 지원)
|
// 현재 과목 (국어, 영어, 수학, 사회, 과학) 자동 판별 (영문/한글 모두 지원)
|
||||||
const currentSubject = computed(() => {
|
const currentSubject = computed(() => {
|
||||||
if (rawSubject.value) return findSubject(rawSubject.value);
|
if (rawSubject.value) return findSubject(rawSubject.value);
|
||||||
if (rawTarget.value) return findSubject(rawTarget.value);
|
if (rawTarget.value) return findSubject(rawTarget.value);
|
||||||
return SUBJECTS[1];
|
return SUBJECTS[2];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 현재 학년별 맞춤 커리큘럼 문구 (중등/고등 문서 차별화)
|
||||||
|
const activeCurriculum = computed(() => {
|
||||||
|
return currentTarget.value.slug === 'middle'
|
||||||
|
? currentSubject.value.middleCurriculum
|
||||||
|
: currentSubject.value.highCurriculum;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 현재 지역 정보 (영문 slug 파싱 및 한글명 자동 매칭)
|
// 현재 지역 정보 (영문 slug 파싱 및 한글명 자동 매칭)
|
||||||
const regionInfo = computed(() => getRegionInfo(sido.value, sigungu.value, dong.value));
|
const regionInfo = computed(() => getRegionInfo(sido.value, sigungu.value, dong.value));
|
||||||
|
|
||||||
// 동일 구·군 내 다른 동 및 동일 시·도 내 다른 구·군 내부 링크 (50,380개 전 페이지 거미줄 크롤링 보장)
|
// 반대 학년 전환 URL (중등 <-> 고등)
|
||||||
const relatedLinks = computed(() => getRelatedRegionLinks(regionInfo.value.sidoSlug, regionInfo.value.sigunguSlug));
|
const oppositeTargetUrl = computed(() => {
|
||||||
|
return `/${regionInfo.value.sidoSlug}/${regionInfo.value.sigunguSlug}/${regionInfo.value.dongSlug}/${oppositeTarget.value.slug}/${currentSubject.value.slug}`;
|
||||||
// 1~6위 랭킹용 인근 동 리스트 (자신 포함 최대 6개)
|
|
||||||
const rankPlaceList = computed(() => {
|
|
||||||
const list = [regionInfo.value.dong, ...(regionInfo.value.nearbyDongs || [])];
|
|
||||||
return list.slice(0, 6);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1~6위 추천 카드별 고유 콘텐츠 (광역·구·동 키워드 매칭 및 텍스트 다양성 확보)
|
// 1~6위 추천 카드 리스트 (UI 변경 없이 반대 학년 + 동일 구·군 내 인접 동 + 인접 구·군을 100% 연결하는 순환 크롤링 링)
|
||||||
const getPlaceCardTitle = (nearDong, idx) => {
|
const rankPlaceCards = computed(() => {
|
||||||
const sidoShort = regionInfo.value.sidoShort;
|
return getRankPlaceCards(regionInfo.value.sidoSlug, regionInfo.value.sigunguSlug, regionInfo.value.dongSlug);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPlaceCardUrl = (card) => {
|
||||||
|
const targetSlug = card.isOppositeTarget ? oppositeTarget.value.slug : currentTarget.value.slug;
|
||||||
|
return `/${card.sidoSlug}/${card.sigunguSlug}/${card.dongSlug}/${targetSlug}/${currentSubject.value.slug}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1~6위 추천 카드별 고유 콘텐츠 (광역·구·동·학년·과목 키워드 매칭 및 문서 유사도 완벽 분리)
|
||||||
|
const getPlaceCardTitle = (card, idx) => {
|
||||||
|
const targetName = card.isOppositeTarget ? oppositeTarget.value.name : currentTarget.value.name;
|
||||||
const titles = [
|
const titles = [
|
||||||
`${sidoShort} ${nearDong} ${currentTarget.value.name} ${currentSubject.value.name} 과외 1:1 맞춤형 내신 1등급 클리닉`,
|
`${card.sidoShort} ${card.dong} ${targetName} ${currentSubject.value.name} 과외 1:1 맞춤형 내신 1등급 클리닉`,
|
||||||
`${sidoShort} ${currentTarget.value.name} 과외 · ${nearDong} ${currentSubject.value.name} 취약점 분석 및 서술형 집중 대비`,
|
`${card.sidoShort} ${currentTarget.value.name} 과외 · ${card.dong} ${currentSubject.value.name} 취약점 분석 및 서술형 집중 대비`,
|
||||||
`${nearDong} 일대 ${currentTarget.value.name} ${currentSubject.value.name} 과외 기출 족보 정복 및 상위권 도약`,
|
`${card.dong} 일대 ${currentTarget.value.name} ${currentSubject.value.name} 과외 기출 족보 정복 및 상위권 도약`,
|
||||||
`${sidoShort} ${regionInfo.value.sigungu} ${nearDong} 인근 학교 맞춤 ${currentSubject.value.name} 과외 내신 집중 관리`,
|
`${card.sidoShort} ${card.sigungu} ${card.dong} 인근 학교 맞춤 ${currentSubject.value.name} 과외 내신 집중 관리`,
|
||||||
`${nearDong} ${currentTarget.value.name} ${currentSubject.value.name} 과외 개념 완성부터 심화 문제 해결력 완성`,
|
`${card.dong} ${currentTarget.value.name} ${currentSubject.value.name} 과외 개념 완성부터 심화 문제 해결력 완성`,
|
||||||
`${sidoShort} ${nearDong} 베테랑 선생님과 함께하는 ${currentTarget.value.name} 1:1 밀착 학습 코칭`
|
`${card.sidoShort} ${card.sigungu} ${card.dong} 베테랑 선생님과 함께하는 ${currentTarget.value.name} 1:1 밀착 학습 코칭`
|
||||||
];
|
];
|
||||||
return titles[idx % titles.length];
|
return titles[idx % titles.length];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaceCardDesc = (nearDong, idx) => {
|
const getPlaceCardDesc = (card, idx) => {
|
||||||
const sidoShort = regionInfo.value.sidoShort;
|
const targetName = card.isOppositeTarget ? oppositeTarget.value.name : currentTarget.value.name;
|
||||||
const sigungu = regionInfo.value.sigungu;
|
const pointText = currentSubject.value.points[idx % currentSubject.value.points.length];
|
||||||
const descs = [
|
const descs = [
|
||||||
`${sidoShort} ${sigungu} ${nearDong} 일대 인근 학교 시험 출제 족보 분석과 ${sidoShort} ${currentTarget.value.name} ${currentSubject.value.name} 과외 취약 개념 1:1 클리닉을 함께 확인할 수 있습니다.`,
|
`${card.sidoShort} ${card.sigungu} ${card.dong} 일대 학교별 시험 출제 경향 분석과 ${card.sidoShort} ${targetName} ${currentSubject.value.name} 과외(${pointText}) 1:1 클리닉을 함께 확인할 수 있습니다.`,
|
||||||
`${sidoShort} ${currentTarget.value.name} 과외 단원별 핵심 개념 정리와 학교별 서술형 감점 요인을 집중 교정하여 실전 시험에 완벽 대비합니다.`,
|
`${card.sidoShort} ${currentTarget.value.name} 과외 핵심 과정(${activeCurriculum.value})과 학교별 서술형 감점 요인을 집중 교정하여 실전 시험에 완벽 대비합니다.`,
|
||||||
`최근 3개년 내신 빈출 유형 정복과 자기주도적 오답노트 관리로 ${nearDong} ${currentTarget.value.name} ${currentSubject.value.name} 과외의 체계적인 성적 향상을 이끕니다.`,
|
`최근 3개년 내신 빈출 유형 정복과 자기주도적 오답노트 관리로 ${card.dong} ${currentTarget.value.name} ${currentSubject.value.name} 과외(${pointText})의 체계적인 성적 향상을 이끕니다.`,
|
||||||
`수행평가와 지필평가를 동시에 대비하는 ${sigungu} ${nearDong} 학교별 맞춤 커리큘럼으로 상위권 도약을 지원합니다.`,
|
`수행평가와 지필평가를 동시에 대비하는 ${card.sigungu} ${card.dong} 학교별 맞춤 커리큘럼(${currentSubject.value.headline})으로 상위권 도약을 지원합니다.`,
|
||||||
`기초 개념 이해부터 고난도 킬러 문항까지 ${sidoShort} ${currentTarget.value.name} ${currentSubject.value.name} 과외 학생 개인별 수준에 맞춘 단계별 학습이 진행됩니다.`,
|
`기초 개념 이해부터 고난도 킬러 문항까지 ${card.sidoShort} ${currentTarget.value.name} ${currentSubject.value.name} 과외 학생 개인별 수준에 맞춘 단계별 학습(${pointText})이 진행됩니다.`,
|
||||||
`학생의 성향과 학습 습관을 고려하여 ${sidoShort} ${sigungu} 최적의 베테랑 전문 선생님이 1:1 밀착 관리를 제공합니다.`
|
`학생의 성향과 학습 습관을 고려하여 ${card.sidoShort} ${card.sigungu} ${card.dong} 최적의 베테랑 전문 선생님이 1:1 밀착 관리를 제공합니다.`
|
||||||
];
|
];
|
||||||
return descs[idx % descs.length];
|
return descs[idx % descs.length];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaceCardKeywords = (idx) => {
|
const getPlaceCardKeywords = (card, idx) => {
|
||||||
const sidoShort = regionInfo.value.sidoShort;
|
const targetName = card.isOppositeTarget ? oppositeTarget.value.name : currentTarget.value.name;
|
||||||
const sigungu = regionInfo.value.sigungu;
|
|
||||||
const dongName = regionInfo.value.dong;
|
|
||||||
const kwList = [
|
const kwList = [
|
||||||
`#${sidoShort}${currentTarget.value.name}${currentSubject.value.name}과외 #${sidoShort}${currentTarget.value.name}과외 #${dongName}${currentSubject.value.name}과외 #내신1등급목표`,
|
`#${card.sidoShort}${targetName}${currentSubject.value.name}과외 #${card.sidoShort}${targetName}과외 #${card.dong}${currentSubject.value.name}과외 #내신1등급목표`,
|
||||||
`#${sidoShort}${sigungu}${currentTarget.value.name}과외 #기출족보분석 #${currentSubject.value.name}전문 #서술형집중대비`,
|
`#${card.sidoShort}${card.sigungu}${currentTarget.value.name}과외 #기출족보분석 #${currentSubject.value.name}전문 #서술형집중대비`,
|
||||||
`#${dongName}${currentTarget.value.name}과외 #취약단원완성 #1대1맞춤수업 #개념클리닉`,
|
`#${card.dong}${currentTarget.value.name}과외 #취약단원완성 #1대1맞춤수업 #개념클리닉`,
|
||||||
`#${sidoShort}${currentSubject.value.name}과외 #수행평가대비 #상위권도약 #오답노트관리`,
|
`#${card.sidoShort}${currentSubject.value.name}과외 #수행평가대비 #상위권도약 #오답노트관리`,
|
||||||
`#${sigungu}${currentTarget.value.name}${currentSubject.value.name}과외 #수능모의고사 #심화문제해결 #체계적관리`,
|
`#${card.sigungu}${currentTarget.value.name}${currentSubject.value.name}과외 #수능모의고사 #심화문제해결 #체계적관리`,
|
||||||
`#${sidoShort}1대1과외 #베테랑선생님 #학습습관코칭 #무료모의수업`
|
`#${card.sidoShort}1대1과외 #${card.dong}과외추천 #학습습관코칭 #무료모의수업`
|
||||||
];
|
];
|
||||||
return kwList[idx % kwList.length];
|
return kwList[idx % kwList.length];
|
||||||
};
|
};
|
||||||
@@ -501,23 +523,23 @@ const targetSchools = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// FAQ 목록
|
// FAQ 목록 (지역·학년·과목별 고유 정보 반영으로 문서 유사도 최소화)
|
||||||
const faqs = computed(() => [
|
const faqs = computed(() => [
|
||||||
{
|
{
|
||||||
q: `${regionInfo.value.sidoShort} ${regionInfo.value.sigungu} ${regionInfo.value.dong} 지역도 ${currentTarget.value.name} ${currentSubject.value.name} 과외 출장이 되나요?`,
|
q: `${regionInfo.value.sidoShort} ${regionInfo.value.sigungu} ${regionInfo.value.dong} 지역도 ${currentTarget.value.name} ${currentSubject.value.name} 과외 방문 수업이 가능한가요?`,
|
||||||
a: `접수하신 주소를 기준으로 ${regionInfo.value.dong} 일대 인근 학교 내신과 수능을 전문으로 지도하는 가까운 베테랑 선생님 배정 가능 여부를 확인해 안내드립니다.`
|
a: `네, 현재 ${regionInfo.value.sigungu} ${regionInfo.value.dong} 일대에는 ${regionInfo.value.teachersCount}명의 검증된 베테랑 선생님이 활동 중이며, ${targetSchools.value.join(', ')} 내신과 ${currentSubject.value.name} 영역(${activeCurriculum.value})을 전문으로 지도하는 선생님을 신속히 배정해 드립니다.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: `수업 전에 무료 모의수업이나 레벨 테스트가 가능한가요?`,
|
q: `${regionInfo.value.dong} ${currentTarget.value.name} ${currentSubject.value.name} 수업 전 무료 모의수업이나 레벨 테스트가 가능한가요?`,
|
||||||
a: `네, 신청 후 전문 학습 매니저가 1차 진단 후 최적의 베테랑 선생님과 1회 무료 모의수업 및 성취도 진단 테스트를 진행해 드립니다.`
|
a: `네, 신청 후 전문 학습 매니저가 ${currentSubject.value.name} 과목(${currentSubject.value.headline}) 취약 단원을 1차 진단한 뒤, 학생 성향에 맞는 최적의 선생님과 1회 무료 모의수업 및 성취도 진단 테스트를 진행해 드립니다.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: `학교별 시험 족보 및 수행평가 대비가 함께 되나요?`,
|
q: `${targetSchools.value[0]} 등 인근 학교 시험 족보 및 수행평가 대비가 함께 되나요?`,
|
||||||
a: `네, ${regionInfo.value.dong} 인근 중·고등학교의 최근 3개년 기출 문제와 교과서별 출판사 서술형 예상 문항을 철저하게 맞춤 지도합니다.`
|
a: `네, ${regionInfo.value.dong} 인근 학교(${targetSchools.value.join(', ')})의 최근 3개년 ${currentSubject.value.name} 기출 문제 분석은 물론, ${currentSubject.value.points[0]} 및 ${currentSubject.value.points[1]} 중심의 밀착 지도를 제공합니다.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: `과외 비용과 수업 횟수는 어떻게 결정되나요?`,
|
q: `${regionInfo.value.sidoShort} ${currentTarget.value.name} ${currentSubject.value.name} 과외 비용과 수업 횟수는 어떻게 결정되나요?`,
|
||||||
a: `학생의 현재 등급 및 주당 수업 횟수(주 1~2회 등), 수업 시간(60분/90분)에 따라 맞춤 견적이 책정되며 온라인 상담 시 정확한 요금표를 안내드립니다.`
|
a: `${currentTarget.value.name} 학년 과정과 현재 성취도, 주당 수업 횟수(주 1~2회 등), 수업 시간(60분/90분)에 따라 합리적인 맞춤 견적이 책정되며, 무료 상담 접수 시 ${regionInfo.value.sigungu} ${regionInfo.value.dong} 기준 정확한 안내를 받으실 수 있습니다.`
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -792,6 +814,8 @@ watch([() => route.params], () => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-item.loc {
|
.badge-item.loc {
|
||||||
@@ -1323,6 +1347,12 @@ watch([() => route.params], () => {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.place-title-link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.place-addr {
|
.place-addr {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
@@ -1483,16 +1513,27 @@ watch([() => route.params], () => {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-button.open {
|
.faq-button::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-row[open] .faq-button {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
background: var(--color-primary-subtle);
|
background: var(--color-primary-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.faq-row[open] .faq-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
.faq-arrow {
|
.faq-arrow {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-panel {
|
.faq-panel {
|
||||||
@@ -2198,6 +2239,7 @@ watch([() => route.params], () => {
|
|||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consult-modal-card {
|
.consult-modal-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
@@ -2208,10 +2250,19 @@ watch([() => route.params], () => {
|
|||||||
box-shadow: 0 25px 50px -12px rgba(15, 23, 42, 0.25);
|
box-shadow: 0 25px 50px -12px rgba(15, 23, 42, 0.25);
|
||||||
animation: modalPop 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: modalPop 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modalPop {
|
@keyframes modalPop {
|
||||||
from { opacity: 0; transform: scale(0.92) translateY(10px); }
|
from {
|
||||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
opacity: 0;
|
||||||
|
transform: scale(0.92) translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.consult-modal-icon {
|
.consult-modal-icon {
|
||||||
width: 68px;
|
width: 68px;
|
||||||
height: 68px;
|
height: 68px;
|
||||||
@@ -2222,6 +2273,7 @@ watch([() => route.params], () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consult-modal-title {
|
.consult-modal-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -2230,6 +2282,7 @@ watch([() => route.params], () => {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consult-modal-desc {
|
.consult-modal-desc {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
@@ -2237,6 +2290,7 @@ watch([() => route.params], () => {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consult-modal-btn {
|
.consult-modal-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@@ -2250,6 +2304,7 @@ watch([() => route.params], () => {
|
|||||||
box-shadow: 0 8px 18px rgba(255, 94, 0, 0.3);
|
box-shadow: 0 8px 18px rgba(255, 94, 0, 0.3);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consult-modal-btn:hover {
|
.consult-modal-btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 10px 24px rgba(255, 94, 0, 0.4);
|
box-shadow: 0 10px 24px rgba(255, 94, 0, 0.4);
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export default defineConfig(({ isPreview }) => ({
|
|||||||
noExternal: true
|
noExternal: true
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
emptyOutDir: false
|
emptyOutDir: true
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__BUILD_DATE__: JSON.stringify(new Date().toISOString().slice(0, 10))
|
__BUILD_DATE__: JSON.stringify(new Date().toISOString().slice(0, 10))
|
||||||
|
|||||||
Reference in New Issue
Block a user