Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "next build && node scripts/critical-css.js",
"start": "next start",
"lint": "next lint"
},
Expand All @@ -28,7 +28,7 @@
"axios": "^1.6.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"critters": "^0.0.23",
"beasties": "^0.2.0",
"firebase": "^10.7.2",
"firebase-admin": "^12.0.0",
"js-cookie": "^3.0.5",
Expand Down
68 changes: 68 additions & 0 deletions scripts/critical-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const Beasties = require("beasties");
const fs = require("fs");
const path = require("path");

const NEXT_BUILD_DIR = path.join(process.cwd(), ".next");
const SERVER_APP_DIR = path.join(NEXT_BUILD_DIR, "server", "app");

async function processCriticalCSS() {
console.log("Starting Critical CSS extraction with Beasties...");

const beasties = new Beasties({
path: NEXT_BUILD_DIR,
publicPath: "/_next/",
preload: "swap",
noscriptFallback: true,
inlineFonts: true,
pruneSource: false,
reduceInlineStyles: true,
mergeStylesheets: true,
additionalStylesheets: [],
fonts: true,
});

const htmlFiles = findHtmlFiles(SERVER_APP_DIR);
console.log(`Found ${htmlFiles.length} HTML files to process`);

for (const htmlFile of htmlFiles) {
try {
const html = fs.readFileSync(htmlFile, "utf-8");
const processedHtml = await beasties.process(html);
fs.writeFileSync(htmlFile, processedHtml);
console.log(`Processed: ${path.relative(process.cwd(), htmlFile)}`);
} catch (error) {
console.error(`Error processing ${htmlFile}:`, error.message);
}
}

console.log("Critical CSS extraction complete!");
}

function findHtmlFiles(dir) {
const files = [];

if (!fs.existsSync(dir)) {
console.warn(`Directory not found: ${dir}`);
return files;
}

const items = fs.readdirSync(dir);

for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) {
files.push(...findHtmlFiles(fullPath));
} else if (item.endsWith(".html")) {
files.push(fullPath);
}
}

return files;
}

processCriticalCSS().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
43 changes: 2 additions & 41 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @next/next/no-css-tags */
import type { Metadata, Viewport } from "next";
import dynamic from "next/dynamic";
import localFont from "next/font/local";
Expand All @@ -18,14 +17,12 @@ export const metadata: Metadata = {
description: "솔리드 커넥션. 교환학생의 첫 걸음",
};

// 🎯 폰트 최적화: 하나의 폰트만 사용
const pretendard = localFont({
src: "../../public/fonts/PretendardVariable.woff2",
display: "swap", // optional → swap으로 변경 (preload와 호환)
display: "swap",
weight: "45 920",
variable: "--font-pretendard",
preload: true,
// 폰트 로딩 실패 시 fallback 폰트 체인
fallback: [
"system-ui",
"-apple-system",
Expand All @@ -44,8 +41,7 @@ const AppleScriptLoader = dynamic(() => import("@/lib/ScriptLoader/AppleScriptLo

declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Kakao: any;
Kakao: unknown;
}
}

Expand All @@ -60,48 +56,13 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => (
<AlertProvider>
<html lang="ko" className={pretendard.variable}>
<head>
{/* 폰트 preload - CSS 블로킹 방지 */}
<link
rel="preload"
href="/fonts/PretendardVariable.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>

{/* 최소한의 Critical CSS - 폰트 최적화만 */}
<style
dangerouslySetInnerHTML={{
__html: `
/* 폰트 즉시 렌더링 - swap과 호환 */
html {
font-family: var(--font-pretendard), system-ui, -apple-system, sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

body {
margin: 0;
background: white;
font-family: system-ui, -apple-system, sans-serif; /* 폰트 로딩 전 즉시 렌더링 */
}

/* 폰트 로딩 시 깜빡임 최소화 */
@font-face {
font-family: 'Pretendard Variable';
font-display: swap;
}

/* LCP 이미지만 최적화 */
.w-\\[153px\\] { width: 153px; }
.h-\\[120px\\] { height: 120px; }
.rounded-lg { border-radius: 0.5rem; }
.object-cover { object-fit: cover; }
`,
}}
/>
</head>
<body className={pretendard.className}>
<AppleScriptLoader />
Expand Down
21 changes: 14 additions & 7 deletions src/app/university/application/ScoreSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import styles from "./score-search-bar.module.css";

import { IconSearchFilled } from "@/public/svgs";

type ScoreSearchBarProps = {
onClick: () => void;
textRef: React.RefObject<HTMLInputElement>;
searchHandler: (e: React.FormEvent) => void;
searchHandler: (_e: React.FormEvent) => void;
};

const ScoreSearchBar = ({ onClick, textRef, searchHandler }: ScoreSearchBarProps) => (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<form onClick={onClick} className={styles.searchBar} onSubmit={searchHandler}>
<input className={styles.searchInput} placeholder="해외 파견 학교를 검색하세요." ref={textRef} />
<button className={styles.searchButton} type="submit" aria-label="검색">
<form
onClick={onClick}
onKeyDown={(e) => e.key === "Enter" && onClick()}
className="flex h-[53px] flex-row items-center border-b border-[#d7d7d7]"
onSubmit={searchHandler}
role="search"
>
<input
className="w-full border-0 pl-6 text-base font-normal leading-6 text-[#606060] outline-none"
placeholder="해외 파견 학교를 검색하세요."
ref={textRef}
/>
<button className="cursor-pointer border-0 bg-white pr-[11px]" type="submit" aria-label="검색">
<IconSearchFilled />
</button>
</form>
Expand Down
15 changes: 9 additions & 6 deletions src/app/university/application/ScoreSearchField.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import styles from "./score-search-field.module.css";

type ScoreSearchFieldProps = {
keyWords: string[];
setKeyWord: (keyWord: string) => void;
setKeyWord: (_keyWord: string) => void;
};

const ScoreSearchField = ({ keyWords, setKeyWord }: ScoreSearchFieldProps) => (
<div>
<div className={styles.title}>인기 검색</div>
<div className={styles.container}>
<div className="ml-5 mt-[18px] text-base font-semibold text-black">인기 검색</div>
<div className="ml-5 mt-2.5 flex flex-wrap gap-2">
{keyWords.map((keyWord) => (
<button key={keyWord} className={styles.item} onClick={() => setKeyWord(keyWord)} type="button">
<button
key={keyWord}
className="flex items-center justify-center gap-2.5 rounded-full bg-[#fafafa] px-3 py-[5px] text-sm font-medium leading-[160%] text-black"
onClick={() => setKeyWord(keyWord)}
type="button"
>
{keyWord}
</button>
))}
Expand Down
30 changes: 0 additions & 30 deletions src/app/university/application/score-search-bar.module.css

This file was deleted.

39 changes: 0 additions & 39 deletions src/app/university/application/score-search-field.module.css

This file was deleted.

31 changes: 0 additions & 31 deletions src/app/university/score/score-search-bar.module.css

This file was deleted.

41 changes: 0 additions & 41 deletions src/app/university/score/score-search-field.module.css

This file was deleted.

Loading