This commit is contained in:
2026-11-10 10:53:38 +09:00
parent ae325fde83
commit aedc8191f0
2 changed files with 151 additions and 116 deletions
+30 -3
View File
@@ -144,12 +144,14 @@ function tryServeStatic(req, res, pathname) {
} }
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
const reqUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
let rawPathname;
try { try {
let rawPathname = '/';
try {
const safeHost = (req.headers.host || 'localhost').replace(/[^a-zA-Z0-9.:-]/g, '');
const reqUrl = new URL(req.url || '/', `http://${safeHost || 'localhost'}`);
rawPathname = decodeURIComponent(reqUrl.pathname); rawPathname = decodeURIComponent(reqUrl.pathname);
} catch { } catch {
rawPathname = reqUrl.pathname; rawPathname = (req.url || '/').split('?')[0] || '/';
} }
// 헬스체크 엔드포인트 (Docker / Nginx Proxy Manager 상태 확인용) // 헬스체크 엔드포인트 (Docker / Nginx Proxy Manager 상태 확인용)
@@ -160,6 +162,16 @@ const server = http.createServer(async (req, res) => {
return; return;
} }
// 브라우저 기본 /favicon.ico 요청 시 정적 아이콘으로 즉시 응답 (불필요한 SSR/404 방지)
if (rawPathname === '/favicon.ico') {
if (tryServeStatic(req, res, '/img/pavicon.png') || tryServeStatic(req, res, '/favicon.svg')) {
return;
}
res.writeHead(204);
res.end();
return;
}
// 1. 정적 파일 우선 서빙 (/assets/*, /img/*, /sitemap*.xml, /robots.txt, /favicon.svg 등, 단 '/' 제외) // 1. 정적 파일 우선 서빙 (/assets/*, /img/*, /sitemap*.xml, /robots.txt, /favicon.svg 등, 단 '/' 제외)
if (rawPathname !== '/' && tryServeStatic(req, res, rawPathname)) { if (rawPathname !== '/' && tryServeStatic(req, res, rawPathname)) {
return; return;
@@ -242,6 +254,21 @@ const server = http.createServer(async (req, res) => {
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }); res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('서버 빌드 산출물이 존재하지 않습니다. npm run build:fast 를 먼저 실행해주세요.'); res.end('서버 빌드 산출물이 존재하지 않습니다. npm run build:fast 를 먼저 실행해주세요.');
} }
} catch (fatalErr) {
console.error('[HTTP Handler Error]:', fatalErr);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
}
res.end('Internal Server Error');
}
});
// 비정상 봇/스캐너 패킷으로 인한 프로세스 종료 원천 차단
process.on('uncaughtException', (err) => {
console.error('[Uncaught Exception]:', err);
});
process.on('unhandledRejection', (reason) => {
console.error('[Unhandled Rejection]:', reason);
}); });
await loadSsrEntry(); await loadSsrEntry();
+34 -26
View File
@@ -275,7 +275,7 @@
</div> </div>
</section> </section>
<!-- 하단 Pager 내부 링크 클러스터 (동일 동 10개 과목·학년 + 동일 구·군 내 다른 동 + 동일 시·도 내 다른 구·군 연결) --> <!-- 하단 Pager 내부 링크 클러스터 (주석 처리)
<nav class="pager" aria-label="지역 및 과목별 과외 페이지 이동"> <nav class="pager" aria-label="지역 및 과목별 과외 페이지 이동">
<div class="pager-group"> <div class="pager-group">
<strong class="pager-group-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} 학년·과목별 과외 바로가기</strong> <strong class="pager-group-title">{{ regionInfo.sidoShort }} {{ regionInfo.dong }} 학년·과목별 과외 바로가기</strong>
@@ -314,6 +314,7 @@
<router-link v-if="isAdmin" to="/regions" class="pager-link pager-next">전국 지역 목록 보기 →</router-link> <router-link v-if="isAdmin" to="/regions" class="pager-link pager-next">전국 지역 목록 보기 →</router-link>
</nav> </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="과목별 과외 가로 이미지 캐러셀">
@@ -1562,10 +1563,10 @@ watch([() => route.params], () => {
border-color: var(--border-primary); border-color: var(--border-primary);
} }
/* 하단 가로형 과목 이미지 캐러셀 (네이버 가로 멀티 썸네일 최적화) */ /* 하단 가로형 과목 이미지 캐러셀 (네이버 가로 멀티 썸네일 최적화 - 가로 스크롤 없이 비율 자동 맞춤) */
.bottom-carousel-section { .bottom-carousel-section {
margin-bottom: 36px; margin-bottom: 36px;
padding: 22px; padding: 20px;
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 16px; border-radius: 16px;
@@ -1579,27 +1580,14 @@ watch([() => route.params], () => {
} }
.horizontal-carousel-track { .horizontal-carousel-track {
display: flex; display: grid;
flex-direction: row; grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px; gap: 10px;
overflow-x: auto; width: 100%;
padding-bottom: 8px;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.horizontal-carousel-track::-webkit-scrollbar {
height: 6px;
}
.horizontal-carousel-track::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
} }
.h-carousel-card { .h-carousel-card {
flex: 0 0 165px; min-width: 0;
scroll-snap-align: start;
background: #ffffff; background: #ffffff;
border: 1.5px solid var(--border-color); border: 1.5px solid var(--border-color);
border-radius: 14px; border-radius: 14px;
@@ -1619,7 +1607,9 @@ watch([() => route.params], () => {
} }
.h-carousel-link { .h-carousel-link {
display: block; display: flex;
flex-direction: column;
height: 100%;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
@@ -1645,22 +1635,27 @@ watch([() => route.params], () => {
left: 6px; left: 6px;
background: rgba(15, 23, 42, 0.8); background: rgba(15, 23, 42, 0.8);
color: #ffffff; color: #ffffff;
font-size: 10.5px; font-size: 10px;
font-weight: 700; font-weight: 700;
padding: 2px 7px; padding: 2px 6px;
border-radius: 999px; border-radius: 999px;
max-width: calc(100% - 12px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.h-carousel-body { .h-carousel-body {
padding: 10px 12px; padding: 10px;
} }
.h-carousel-body h3 { .h-carousel-body h3 {
font-size: 13.5px; font-size: 13px;
font-weight: 800; font-weight: 800;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: 4px; margin-bottom: 4px;
line-height: 1.3; line-height: 1.3;
word-break: keep-all;
} }
.h-carousel-body p { .h-carousel-body p {
@@ -1674,6 +1669,19 @@ watch([() => route.params], () => {
overflow: hidden; overflow: hidden;
} }
@media (max-width: 768px) {
.horizontal-carousel-track {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 480px) {
.horizontal-carousel-track {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
}
/* 빠른 상담 리소스 링크 */ /* 빠른 상담 리소스 링크 */
.post-resource-links { .post-resource-links {
display: grid; display: grid;