Skip to content

Commit 4786969

Browse files
committed
feat: ssr 테스트 케이스 1차 대응
1 parent fc247f6 commit 4786969

File tree

5 files changed

+488
-16
lines changed

5 files changed

+488
-16
lines changed

packages/vanilla/src/main-server.js

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { getProducts, getCategories, getProduct } from "./api/routes.js";
22
import { ServerRouter } from "./router/serverRouter.js";
3+
import { ServerHomePage } from "./pages/server/ServerHomePage.js";
4+
import { ServerProductDetailPage } from "./pages/server/ServerProductDetailPage.js";
35

46
const serverRouter = new ServerRouter();
57

@@ -55,7 +57,15 @@ export const render = async (url) => {
5557
const result = await notFoundRoute.handler({}, {});
5658

5759
return {
58-
html: '<div id="app"><h1>404 - Page Not Found</h1></div>',
60+
html: `<div id="app">
61+
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
62+
<div class="text-center">
63+
<h1 class="text-2xl font-bold text-gray-900 mb-2">404 - Page Not Found</h1>
64+
<p class="text-gray-600 mb-4">${result.data.message}</p>
65+
<a href="/" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">홈으로</a>
66+
</div>
67+
</div>
68+
</div>`,
5969
head: "<title>404 - Page Not Found</title>",
6070
initialData: result.data,
6171
};
@@ -64,46 +74,73 @@ export const render = async (url) => {
6474
// 2. 데이터 프리페칭
6575
const result = await route.handler(route.params, route.query);
6676

67-
// 3. HTML 및 메타데이터 생성 (현재는 간단한 형태)
77+
// 3. 실제 컴포넌트 렌더링
6878
let html, title;
6979

80+
let initialData;
81+
7082
switch (result.type) {
7183
case "homepage":
72-
html = `<div id="app">
73-
<h1>Shopping Mall</h1>
74-
<p>Products loaded: ${result.data.products.length}</p>
75-
<p>Total products: ${result.data.pagination.total}</p>
76-
</div>`;
84+
html = `<div id="app">${ServerHomePage({
85+
products: result.data.products,
86+
categories: result.data.categories,
87+
query: result.data.filters,
88+
totalCount: result.data.pagination.total,
89+
})}</div>`;
7790
title = "Shopping Mall - Home";
91+
// 테스트에서 기대하는 형태로 initialData 구성
92+
initialData = {
93+
products: result.data.products,
94+
categories: result.data.categories,
95+
totalCount: result.data.pagination.total,
96+
};
7897
break;
7998

8099
case "product-detail":
81-
html = `<div id="app">
82-
<h1>${result.data.currentProduct.title}</h1>
83-
<p>Price: ${result.data.currentProduct.lprice}원</p>
84-
<p>Brand: ${result.data.currentProduct.brand}</p>
85-
</div>`;
100+
html = `<div id="app">${ServerProductDetailPage({
101+
product: result.data.currentProduct,
102+
relatedProducts: result.data.relatedProducts || [],
103+
})}</div>`;
86104
title = `${result.data.currentProduct.title} - Shopping Mall`;
105+
// 상품 상세 페이지용 initialData
106+
initialData = {
107+
currentProduct: result.data.currentProduct,
108+
};
87109
break;
88110

89111
default:
90112
html = `<div id="app">
91-
<h1>404 - Page Not Found</h1>
92-
<p>${result.data.message}</p>
113+
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
114+
<div class="text-center">
115+
<h1 class="text-2xl font-bold text-gray-900 mb-2">404 - Page Not Found</h1>
116+
<p class="text-gray-600 mb-4">${result.data.message}</p>
117+
<a href="/" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">홈으로</a>
118+
</div>
119+
</div>
93120
</div>`;
94121
title = "404 - Page Not Found";
122+
initialData = { error: result.data.message };
95123
}
96124

97125
return {
98126
html,
99127
head: `<title>${title}</title>`,
100-
initialData: result.data,
128+
initialData,
101129
};
102130
} catch (error) {
103131
console.error("Server rendering error:", error);
104132

105133
return {
106-
html: '<div id="app"><h1>Server Error</h1><p>Something went wrong.</p></div>',
134+
html: `<div id="app">
135+
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
136+
<div class="text-center">
137+
<h1 class="text-2xl font-bold text-gray-900 mb-2">Server Error</h1>
138+
<p class="text-gray-600 mb-4">Something went wrong.</p>
139+
<p class="text-sm text-red-600">${error.message}</p>
140+
<a href="/" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 mt-4 inline-block">홈으로</a>
141+
</div>
142+
</div>
143+
</div>`,
107144
head: "<title>Server Error</title>",
108145
initialData: { error: "Server rendering failed" },
109146
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ProductList, SearchBar } from "../../components/index.js";
2+
import { ServerPageWrapper } from "./ServerPageWrapper.js";
3+
4+
/**
5+
* 서버 사이드 렌더링을 위한 HomePage 컴포넌트
6+
* withLifecycle 없이 순수한 렌더링 함수로 구현
7+
*
8+
* @param {Object} props - 렌더링에 필요한 데이터
9+
* @param {Array} props.products - 상품 목록
10+
* @param {Object} props.categories - 카테고리 목록
11+
* @param {Object} props.query - 현재 쿼리 파라미터
12+
* @param {number} props.totalCount - 총 상품 수
13+
*/
14+
export function ServerHomePage({ products = [], categories = {}, query = {}, totalCount = 0 }) {
15+
const { search = "", limit = 20, sort = "price_asc", category1 = "", category2 = "" } = query;
16+
17+
const category = { category1, category2 };
18+
const hasMore = products.length < totalCount;
19+
20+
return ServerPageWrapper({
21+
headerLeft: `
22+
<h1 class="text-xl font-bold text-gray-900">
23+
<a href="/" data-link>쇼핑몰</a>
24+
</h1>
25+
`.trim(),
26+
children: `
27+
<!-- 검색 및 필터 -->
28+
${SearchBar({
29+
searchQuery: search,
30+
limit,
31+
sort,
32+
category,
33+
categories,
34+
})}
35+
36+
<!-- 상품 목록 -->
37+
<div class="mb-6">
38+
${ProductList({
39+
products,
40+
loading: false, // 서버에서는 로딩 상태 없음
41+
error: null, // 서버에서는 에러 상태 없음 (에러 시 다른 처리)
42+
totalCount,
43+
hasMore,
44+
})}
45+
</div>
46+
`.trim(),
47+
});
48+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { CartModal, Footer, Toast } from "../../components/index.js";
2+
3+
/**
4+
* 서버 사이드 렌더링을 위한 PageWrapper
5+
* 스토어 의존성 없이 순수한 렌더링 함수로 구현
6+
*/
7+
export const ServerPageWrapper = ({ headerLeft, children }) => {
8+
// 서버에서는 빈 장바구니로 시작
9+
const cartSize = 0;
10+
const cartModal = { isOpen: false };
11+
const toast = { isVisible: false, message: "", type: "info" };
12+
13+
return `
14+
<div class="min-h-screen bg-gray-50">
15+
<header class="bg-white shadow-sm sticky top-0 z-40">
16+
<div class="max-w-md mx-auto px-4 py-4">
17+
<div class="flex items-center justify-between">
18+
${headerLeft}
19+
<div class="flex items-center space-x-2">
20+
<!-- 장바구니 아이콘 -->
21+
<button id="cart-icon-btn" class="relative p-2 text-gray-700 hover:text-gray-900 transition-colors">
22+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
23+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
24+
d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6"/>
25+
</svg>
26+
${cartSize > 0 ? `<span class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">${cartSize > 99 ? "99+" : cartSize}</span>` : ""}
27+
</button>
28+
</div>
29+
</div>
30+
</div>
31+
</header>
32+
33+
<main class="max-w-md mx-auto px-4 py-4">
34+
${children}
35+
</main>
36+
37+
${CartModal({ items: [], isOpen: cartModal.isOpen })}
38+
39+
${Toast(toast)}
40+
41+
${Footer()}
42+
</div>
43+
`;
44+
};

0 commit comments

Comments
 (0)