|
1 | | -import fs from "fs"; |
| 1 | +import fs from "fs/promises"; |
| 2 | +import path from "path"; |
| 3 | +import { fileURLToPath } from "url"; |
| 4 | +import { mockGetProducts } from "./src/api/mockApi.js"; |
2 | 5 |
|
3 | | -const render = () => { |
4 | | - return `<div>안녕하세요</div>`; |
5 | | -}; |
| 6 | +const __filename = fileURLToPath(import.meta.url); |
| 7 | +const __dirname = path.dirname(__filename); |
6 | 8 |
|
| 9 | +// 디렉토리 경로 설정 |
| 10 | +const PROJECT_ROOT = path.join(__dirname, "../.."); |
| 11 | +const DIST_DIR = path.join(PROJECT_ROOT, "dist", "vanilla-ssg"); |
| 12 | +const CLIENT_TEMPLATE = path.join(PROJECT_ROOT, "dist", "vanilla", "index.html"); |
| 13 | +const SERVER_MODULE = path.join(__dirname, "dist", "vanilla-ssr", "main-server.js"); |
| 14 | + |
| 15 | +/** |
| 16 | + * SSG에서 생성할 페이지 목록을 반환 |
| 17 | + * @returns {Promise<Array>} 페이지 목록 |
| 18 | + */ |
| 19 | +async function getPages() { |
| 20 | + const pages = []; |
| 21 | + |
| 22 | + // 1. 홈페이지 |
| 23 | + pages.push({ |
| 24 | + url: "/", |
| 25 | + filePath: path.join(DIST_DIR, "index.html"), |
| 26 | + description: "홈페이지", |
| 27 | + }); |
| 28 | + |
| 29 | + // 2. 404 페이지 |
| 30 | + pages.push({ |
| 31 | + url: "/404", |
| 32 | + filePath: path.join(DIST_DIR, "404.html"), |
| 33 | + description: "404 페이지", |
| 34 | + }); |
| 35 | + |
| 36 | + try { |
| 37 | + // 3. 상품 상세 페이지들 (처음 20개 상품) |
| 38 | + console.log("📦 상품 목록 로딩 중..."); |
| 39 | + const productsData = await mockGetProducts({ limit: 20, page: 1 }); |
| 40 | + |
| 41 | + for (const product of productsData.products) { |
| 42 | + pages.push({ |
| 43 | + url: `/product/${product.productId}/`, |
| 44 | + filePath: path.join(DIST_DIR, "product", product.productId, "index.html"), |
| 45 | + description: `상품: ${product.title}`, |
| 46 | + }); |
| 47 | + } |
| 48 | + |
| 49 | + console.log(`✅ ${productsData.products.length}개 상품 페이지 추가됨`); |
| 50 | + } catch (error) { |
| 51 | + console.warn("⚠️ 상품 데이터 로딩 실패, 기본 페이지만 생성:", error.message); |
| 52 | + } |
| 53 | + |
| 54 | + return pages; |
| 55 | +} |
| 56 | + |
| 57 | +/** |
| 58 | + * 디렉토리가 없으면 생성 |
| 59 | + * @param {string} dirPath |
| 60 | + */ |
| 61 | +async function ensureDir(dirPath) { |
| 62 | + try { |
| 63 | + await fs.mkdir(dirPath, { recursive: true }); |
| 64 | + } catch (error) { |
| 65 | + // 이미 존재하면 무시 |
| 66 | + if (error.code !== "EEXIST") { |
| 67 | + throw error; |
| 68 | + } |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +/** |
| 73 | + * HTML 파일을 지정된 경로에 저장 |
| 74 | + * @param {string} filePath |
| 75 | + * @param {string} html |
| 76 | + */ |
| 77 | +async function saveHtmlFile(filePath, html) { |
| 78 | + const dir = path.dirname(filePath); |
| 79 | + await ensureDir(dir); |
| 80 | + await fs.writeFile(filePath, html, "utf-8"); |
| 81 | +} |
| 82 | + |
| 83 | +/** |
| 84 | + * 정적 사이트 생성 메인 함수 |
| 85 | + */ |
7 | 86 | async function generateStaticSite() { |
8 | | - // HTML 템플릿 읽기 |
9 | | - const template = fs.readFileSync("../../dist/vanilla/index.html", "utf-8"); |
| 87 | + try { |
| 88 | + console.log("🚀 Static Site Generation 시작..."); |
| 89 | + |
| 90 | + // 1. 페이지 목록 생성 |
| 91 | + console.log("📋 페이지 목록 생성 중..."); |
| 92 | + const pages = await getPages(); |
| 93 | + console.log(`📄 총 ${pages.length}개 페이지 생성 예정`); |
| 94 | + |
| 95 | + // 2. 템플릿 및 렌더링 함수 로드 |
| 96 | + console.log("📄 템플릿 및 렌더링 함수 로드 중..."); |
| 97 | + const templatePath = CLIENT_TEMPLATE; |
10 | 98 |
|
11 | | - // 어플리케이션 렌더링하기 |
12 | | - const appHtml = render(); |
| 99 | + let template; |
| 100 | + try { |
| 101 | + template = await fs.readFile(templatePath, "utf-8"); |
| 102 | + } catch (error) { |
| 103 | + console.error(`❌ 템플릿 파일을 찾을 수 없음: ${templatePath}`, error); |
| 104 | + console.log("💡 먼저 'pnpm run build:ssg' 명령을 실행하세요"); |
| 105 | + process.exit(1); |
| 106 | + } |
13 | 107 |
|
14 | | - // 결과 HTML 생성하기 |
15 | | - const result = template.replace("<!--app-html-->", appHtml); |
16 | | - fs.writeFileSync("../../dist/vanilla/index.html", result); |
| 108 | + let render; |
| 109 | + try { |
| 110 | + const serverModule = await import(SERVER_MODULE); |
| 111 | + render = serverModule.render; |
| 112 | + } catch (error) { |
| 113 | + console.error(`❌ 서버 렌더링 모듈을 불러올 수 없음: ${SERVER_MODULE}`, error); |
| 114 | + console.log("💡 먼저 'pnpm run build:server' 명령을 실행하세요"); |
| 115 | + process.exit(1); |
| 116 | + } |
| 117 | + |
| 118 | + // 3. 각 페이지 렌더링 및 저장 |
| 119 | + console.log("🎨 페이지 렌더링 시작..."); |
| 120 | + let successCount = 0; |
| 121 | + let errorCount = 0; |
| 122 | + |
| 123 | + for (const page of pages) { |
| 124 | + try { |
| 125 | + console.log(` 🔄 ${page.description} (${page.url})`); |
| 126 | + |
| 127 | + // 서버 렌더링 |
| 128 | + const rendered = await render(page.url); |
| 129 | + |
| 130 | + // HTML 템플릿에 삽입 |
| 131 | + const html = template |
| 132 | + .replace("<!--app-head-->", rendered.head ?? "") |
| 133 | + .replace("<!--app-html-->", rendered.html ?? "") |
| 134 | + .replace( |
| 135 | + "</head>", |
| 136 | + `<script>window.__INITIAL_DATA__ = ${JSON.stringify(rendered.initialData ?? {})}</script></head>`, |
| 137 | + ); |
| 138 | + |
| 139 | + // 파일 저장 |
| 140 | + await saveHtmlFile(page.filePath, html); |
| 141 | + |
| 142 | + console.log(` ✅ ${page.filePath}`); |
| 143 | + successCount++; |
| 144 | + } catch (error) { |
| 145 | + console.error(` ❌ ${page.description} 생성 실패:`, error.message); |
| 146 | + errorCount++; |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + // 4. 결과 요약 |
| 151 | + console.log("\n📊 SSG 완료!"); |
| 152 | + console.log(`✅ 성공: ${successCount}개 페이지`); |
| 153 | + if (errorCount > 0) { |
| 154 | + console.log(`❌ 실패: ${errorCount}개 페이지`); |
| 155 | + } |
| 156 | + console.log(`📁 출력 디렉토리: ${DIST_DIR}`); |
| 157 | + |
| 158 | + // 5. 생성된 파일 목록 표시 |
| 159 | + console.log("\n📋 생성된 파일들:"); |
| 160 | + const allFiles = await getAllFiles(DIST_DIR); |
| 161 | + allFiles.forEach((file) => { |
| 162 | + const relPath = path.relative(DIST_DIR, file); |
| 163 | + console.log(` • ${relPath}`); |
| 164 | + }); |
| 165 | + |
| 166 | + console.log("\n🎉 Static Site Generation 완료!"); |
| 167 | + } catch (error) { |
| 168 | + console.error("💥 SSG 실행 중 오류 발생:", error.message); |
| 169 | + process.exit(1); |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +/** |
| 174 | + * 디렉토리 내 모든 HTML 파일 목록 반환 |
| 175 | + * @param {string} dir |
| 176 | + * @returns {Promise<string[]>} |
| 177 | + */ |
| 178 | +async function getAllFiles(dir) { |
| 179 | + const files = []; |
| 180 | + |
| 181 | + try { |
| 182 | + const entries = await fs.readdir(dir, { withFileTypes: true }); |
| 183 | + |
| 184 | + for (const entry of entries) { |
| 185 | + const fullPath = path.join(dir, entry.name); |
| 186 | + if (entry.isDirectory()) { |
| 187 | + const subFiles = await getAllFiles(fullPath); |
| 188 | + files.push(...subFiles); |
| 189 | + } else if (entry.name.endsWith(".html")) { |
| 190 | + files.push(fullPath); |
| 191 | + } |
| 192 | + } |
| 193 | + } catch (error) { |
| 194 | + console.error(error.message); |
| 195 | + } |
| 196 | + |
| 197 | + return files; |
17 | 198 | } |
18 | 199 |
|
19 | 200 | // 실행 |
20 | | -generateStaticSite(); |
| 201 | +if (import.meta.url === `file://${process.argv[1]}`) { |
| 202 | + generateStaticSite(); |
| 203 | +} |
0 commit comments