Skip to content
Open
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
478 changes: 451 additions & 27 deletions .github/pull_request_template.md

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions e2e/createTests.ts

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions packages/vanilla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@testing-library/user-event": "^14.6.1",
"@vitest/coverage-v8": "latest",
"@vitest/ui": "^2.1.8",
"concurrently": "latest",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
Expand All @@ -49,15 +50,15 @@
"msw": "^2.10.2",
"prettier": "^3.4.2",
"vite": "npm:rolldown-vite@latest",
"vitest": "latest",
"concurrently": "latest"
"vitest": "latest"
},
"msw": {
"workerDirectory": [
"public"
]
},
"dependencies": {
"@mswjs/http-middleware": "^0.10.3",
"compression": "^1.8.1",
"express": "^5.1.0",
"sirv": "^3.0.1"
Expand Down
172 changes: 155 additions & 17 deletions packages/vanilla/server.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,172 @@
import express from "express";
import fs from "fs/promises";
import routes from "./src/routes.js";
import { createMiddleware } from "@mswjs/http-middleware";
import { handlers } from "./src/mocks/handlers.js";
import { createServer as createViteServer } from "vite";
import { render } from "./src/main-server.js";
import { runWithContext } from "./src/lib/asyncContext.js";

const app = express();

const prod = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5173;
const port = Number(process.env.PORT) || 5173;
const base = process.env.BASE || (prod ? "/front_7th_chapter4-1/vanilla/" : "/");

const app = express();
let vite;
let htmlTemplate = "";

if (!prod) {
// 개발 환경: Vite 미들웨어 사용
vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
base,
});
app.use(vite.middlewares);
} else {
// 프로덕션 환경: 빌드된 정적 파일 서빙
const distPath = "./dist/vanilla";
app.use(base, express.static(distPath));

// 빌드된 index.html을 템플릿으로 로드
htmlTemplate = await fs.readFile(`${distPath}/index.html`, "utf-8");
}

const render = () => {
return `<div>안녕하세요</div>`;
};
app.use(createMiddleware(...handlers));
app.use(express.static("public"));

app.get("*all", (req, res) => {
res.send(
`
const styles = fs.readFile("./src/styles.css", "utf-8");

// HTML 생성 헬퍼 함수
async function generateHtml({ html, title, metaTags = "", initialData }) {
if (prod) {
// 프로덕션: 빌드된 템플릿 사용
let result = htmlTemplate
.replace("<title>Document</title>", `<title>${title}</title>`)
.replace("<!--app-head-->", metaTags)
.replace("<!--app-html-->", html);

// </body> 직전에 initialData 주입
result = result.replace(
"</body>",
` <script>
window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
</script>
</body>`,
);

return result;
} else {
// 개발: 간단한 템플릿 (Vite가 /src/main.js 처리)
return `
<!DOCTYPE html>
<html lang="en">
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla Javascript SSR</title>
<title>${title}</title>
${metaTags}
<script src="https://cdn.tailwindcss.com"></script>
<style>
${await styles}
</style>
</head>
<body>
<div id="app">${render()}</div>
<body class="bg-gray-50">
<div id="root">${html}</div>
<script type="module" src="/src/main.js"></script>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
</script>
</body>
</html>
`.trim(),
);
</html>`.trim();
}
}

routes.forEach((route) => {
if (route.path === ".*") {
return app.get(async (req, res) => {
const origin = `${req.protocol}://${req.get("host")}`;

// 요청별로 격리된 컨텍스트 생성
const context = {
origin,
pathname: req.url,
params: req.params,
search: req.query,
initialData: {},
};

await runWithContext(context, async () => {
// globalThis에도 설정 (하위 호환성)
globalThis.origin = context.origin;
globalThis.pathname = context.pathname;
globalThis.params = context.params;
globalThis.search = context.search;
globalThis.initialData = context.initialData;

const html = await render(route.component);

res.send(
await generateHtml({
html,
title: "404 - Page Not Found",
metaTags: '<meta name="description" content="페이지를 찾을 수 없습니다" />',
initialData: context.initialData,
}),
);
});
});
}

app.get(route.path, async (req, res) => {
const origin = `${req.protocol}://${req.get("host")}`;

// 요청별로 격리된 컨텍스트 생성
const context = {
origin,
pathname: req.url,
params: req.params,
search: req.query,
initialData: {},
};

await runWithContext(context, async () => {
// globalThis에도 설정 (하위 호환성)
globalThis.origin = context.origin;
globalThis.pathname = context.pathname;
globalThis.params = context.params;
globalThis.search = context.search;
globalThis.initialData = context.initialData;

const html = await render(route.component);

// 메타태그 생성
let metaTags = `<meta property="og:title" content="${route.title}" />`;
let title = route.title;

if (context.initialData.meta) {
const meta = context.initialData.meta;
title = meta.title;
metaTags = `
<meta name="description" content="${meta.description}" />
<meta property="og:title" content="${title}" />
<meta property="og:description" content="${meta.description}" />
<meta property="og:image" content="${meta.image}" />`;
}

res.send(
await generateHtml({
html,
title,
metaTags,
initialData: context.initialData,
}),
);
});
});
});

// Start http server
app.listen(port, () => {
console.log(`React Server started at http://localhost:${port}`);
console.log(`Example app listening on port ${port}`);
});
12 changes: 9 additions & 3 deletions packages/vanilla/src/api/productApi.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getContext } from "../lib/asyncContext.js";

export async function getProducts(params = {}) {
const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params;
const page = params.current ?? params.page ?? 1;
Expand All @@ -11,17 +13,21 @@ export async function getProducts(params = {}) {
sort,
});

const response = await fetch(`/api/products?${searchParams}`);
const context = getContext();

const response = await fetch(`${context.origin ?? ""}/api/products?${searchParams}`);

return await response.json();
}

export async function getProduct(productId) {
const response = await fetch(`/api/products/${productId}`);
const context = getContext();
const response = await fetch(`${context.origin ?? ""}/api/products/${productId}`);
return await response.json();
}

export async function getCategories() {
const response = await fetch("/api/categories");
const context = getContext();
const response = await fetch(`${context.origin ?? ""}/api/categories`);
return await response.json();
}
2 changes: 1 addition & 1 deletion packages/vanilla/src/components/CartModal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CartItem } from "./CartItem";
import { CartItem } from "./CartItem.js";

export function CartModal({ items = [], selectedAll = false, isOpen = false }) {
if (!isOpen) {
Expand Down
46 changes: 46 additions & 0 deletions packages/vanilla/src/components/PageWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { cartStore, uiStore } from "../stores/index.js";
import { CartModal, Footer, Toast } from ".";

export const PageWrapper = ({ headerLeft, children }) => {
const cart = cartStore.getState();
const { cartModal, toast } = uiStore.getState();
const cartSize = cart.items.length;

const cartCount = `
<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>
`;

return `
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow-sm sticky top-0 z-40">
<div class="max-w-md mx-auto px-4 py-4">
<div class="flex items-center justify-between">
${headerLeft}
<div class="flex items-center space-x-2">
<!-- 장바구니 아이콘 -->
<button id="cart-icon-btn" class="relative p-2 text-gray-700 hover:text-gray-900 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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"/>
</svg>
${cartSize > 0 ? cartCount : ""}
</button>
</div>
</div>
</div>
</header>

<main class="max-w-md mx-auto px-4 py-4">
${children}
</main>

${CartModal({ ...cart, isOpen: cartModal.isOpen })}

${Toast(toast)}

${Footer()}
</div>
`;
};
2 changes: 1 addition & 1 deletion packages/vanilla/src/components/ProductList.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProductCard, ProductCardSkeleton } from "./ProductCard";
import { ProductCard, ProductCardSkeleton } from "./ProductCard.js";

const loadingSkeleton = Array(6).fill(0).map(ProductCardSkeleton).join("");

Expand Down
16 changes: 8 additions & 8 deletions packages/vanilla/src/components/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export * from "./ProductCard";
export * from "./SearchBar";
export * from "./ProductList";
export * from "./CartItem";
export * from "./CartModal";
export * from "./Toast";
export * from "./Logo";
export * from "./Footer";
export * from "./ProductCard.js";
export * from "./SearchBar.js";
export * from "./ProductList.js";
export * from "./CartItem.js";
export * from "./CartModal.js";
export * from "./Toast.js";
export * from "./Logo.js";
export * from "./Footer.js";
2 changes: 1 addition & 1 deletion packages/vanilla/src/constants.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const BASE_URL = import.meta.env.PROD ? "/front_7th_chapter4-1/vanilla/" : "/";
export const BASE_URL = import.meta.env?.PROD ? "/front_7th_chapter4-1/vanilla/" : "/";
8 changes: 4 additions & 4 deletions packages/vanilla/src/events.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { addEvent, isNearBottom } from "./utils";
import { router } from "./router";
import { addEvent, isNearBottom } from "./utils/index.js";
import { router } from "./router/index.js";
import {
addToCart,
clearCart,
Expand All @@ -15,8 +15,8 @@ import {
setSort,
toggleCartSelect,
updateCartQuantity,
} from "./services";
import { productStore, uiStore, UI_ACTIONS } from "./stores";
} from "./services/index.js";
import { productStore, uiStore, UI_ACTIONS } from "./stores/index.js";

/**
* 상품 관련 이벤트 등록
Expand Down
Loading
Loading