Skip to content

Commit 30db635

Browse files
authored
Merge pull request #1 from yein1ee/devel
feat: 옵저버 패턴 기반 상태 관리 store 구현 및 필터 기능 추가
2 parents f0434cf + 3f274ce commit 30db635

31 files changed

+2110
-1194
lines changed

eslint.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import globals from "globals";
22
import pluginJs from "@eslint/js";
33
import eslintConfigPrettier from "eslint-config-prettier";
44
import eslintPluginPrettier from "eslint-plugin-prettier/recommended";
5+
import tseslint from "typescript-eslint";
56

67
/** @type {import('eslint').Linter.Config[]} */
78
export default [
9+
{ ignores: ["dist", "build", "node_modules", "src/template.ts"] },
810
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
911
pluginJs.configs.recommended,
12+
...tseslint.configs.recommended,
13+
{
14+
files: ["**/*.{ts,tsx}"],
15+
},
1016
eslintPluginPrettier,
1117
eslintConfigPrettier,
1218
];

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@testing-library/dom": "^10.4.0",
3232
"@testing-library/jest-dom": "^6.6.3",
3333
"@testing-library/user-event": "^14.6.1",
34+
"@types/node": "^24.10.1",
3435
"@vitest/coverage-v8": "latest",
3536
"@vitest/ui": "^2.1.8",
3637
"eslint": "^9.16.0",
@@ -42,7 +43,10 @@
4243
"lint-staged": "^15.2.11",
4344
"msw": "^2.10.2",
4445
"prettier": "^3.4.2",
46+
"typescript": "^5.9.3",
47+
"typescript-eslint": "^8.46.4",
4548
"vite": "npm:rolldown-vite@latest",
49+
"vite-tsconfig-paths": "^5.1.4",
4650
"vitest": "latest"
4751
},
4852
"msw": {

pnpm-lock.yaml

Lines changed: 389 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/mockServiceWorker.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,7 @@ addEventListener("fetch", function (event) {
100100

101101
// Opening the DevTools triggers the "only-if-cached" request
102102
// that cannot be handled by the worker. Bypass such requests.
103-
if (
104-
event.request.cache === "only-if-cached" &&
105-
event.request.mode !== "same-origin"
106-
) {
103+
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
107104
return;
108105
}
109106

@@ -219,9 +216,7 @@ async function getResponse(event, client, requestId) {
219216
const acceptHeader = headers.get("accept");
220217
if (acceptHeader) {
221218
const values = acceptHeader.split(",").map((value) => value.trim());
222-
const filteredValues = values.filter(
223-
(value) => value !== "msw/passthrough",
224-
);
219+
const filteredValues = values.filter((value) => value !== "msw/passthrough");
225220

226221
if (filteredValues.length > 0) {
227222
headers.set("accept", filteredValues.join(", "));
@@ -291,10 +286,7 @@ function sendToClient(client, message, transferrables = []) {
291286
resolve(event.data);
292287
};
293288

294-
client.postMessage(message, [
295-
channel.port2,
296-
...transferrables.filter(Boolean),
297-
]);
289+
client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
298290
});
299291
}
300292

src/App.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { AppContents, ProductListPage } from "./pages";
2+
import createRouter from "./router";
3+
import { fetchProducts } from "./store/products-list/fetchProducts";
4+
import { ProductState, productStore } from "./store/products-list/productStore";
5+
import { setupFilterEventHandlers } from "./components/SearchForm/filterEventHandlers";
6+
7+
export default function App() {
8+
const $root = document.querySelector("#root");
9+
10+
const renderHome = (state: ProductState) => {
11+
$root.innerHTML = AppContents({
12+
children: ProductListPage(state),
13+
});
14+
// DOM 렌더링 후 이벤트 리스너 등록
15+
setupFilterEventHandlers();
16+
};
17+
18+
const pages = {
19+
home: () => {
20+
const currentState = productStore.getState();
21+
renderHome(currentState);
22+
},
23+
products: () => ($root.innerHTML = AppContents({ children: `products 입니다` })),
24+
};
25+
const router = createRouter();
26+
27+
router.addRoute("#/", pages.home).addRoute("#/products", pages.products).start();
28+
29+
productStore.subscribe((state) => {
30+
const hash = window.location.hash || "#/";
31+
32+
if (hash === "#/" || hash === "" || hash === "#") {
33+
renderHome(state);
34+
}
35+
});
36+
37+
fetchProducts(productStore.getState().params);
38+
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { GetProductsParams, GetProductsResponse } from "../types";
2+
13
// 상품 목록 조회
2-
export async function getProducts(params = {}) {
4+
export async function getProducts(params: GetProductsParams): Promise<GetProductsResponse> {
35
const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params;
46
const page = params.current ?? params.page ?? 1;
57

6-
const searchParams = new URLSearchParams({
8+
const searchParams: URLSearchParams = new URLSearchParams({
79
page: page.toString(),
810
limit: limit.toString(),
911
...(search && { search }),

src/components/Loading/Skeleton.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const Skeleton = /* html */ `
2+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden animate-pulse">
3+
<div class="aspect-square bg-gray-200"></div>
4+
<div class="p-3">
5+
<div class="h-4 bg-gray-200 rounded mb-2"></div>
6+
<div class="h-3 bg-gray-200 rounded w-2/3 mb-2"></div>
7+
<div class="h-5 bg-gray-200 rounded w-1/2 mb-3"></div>
8+
<div class="h-8 bg-gray-200 rounded"></div>
9+
</div>
10+
</div>
11+
`;

src/components/Loading/Spinner.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const Spinner = /* html */ `
2+
<div class="inline-flex items-center">
3+
<svg class="animate-spin h-5 w-5 text-blue-600 mr-2" fill="none" viewBox="0 0 24 24">
4+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
5+
<path class="opacity-75" fill="currentColor"
6+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
7+
</svg>
8+
<span class="text-sm text-gray-600">상품을 불러오는 중...</span>
9+
</div>
10+
`;
11+
12+
export { Spinner };

src/components/Loading/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Skeleton } from "./Skeleton";
2+
import { Spinner } from "./Spinner";
3+
4+
export const Loading = /* html */ `
5+
<div>
6+
<div class="grid grid-cols-2 gap-4 mb-6" id="products-grid">
7+
${Skeleton.repeat(8)}
8+
</div>
9+
<div class="text-center py-4">
10+
${Spinner}
11+
</div>
12+
</div>
13+
`;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ProductItem } from "@/types";
2+
3+
export const ProductCard = (product: ProductItem) => {
4+
return /* html */ `
5+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden product-card"
6+
data-product-id="${product.productId}">
7+
<!-- 상품 이미지 -->
8+
<div class="aspect-square bg-gray-100 overflow-hidden cursor-pointer product-image">
9+
<img src="${product.image}"
10+
alt="${product.title}"
11+
class="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
12+
loading="lazy">
13+
</div>
14+
<!-- 상품 정보 -->
15+
<div class="p-3">
16+
<div class="cursor-pointer product-info mb-3">
17+
<h3 class="text-sm font-medium text-gray-900 line-clamp-2 mb-1">
18+
${product.title}
19+
</h3>
20+
<p class="text-xs text-gray-500 mb-2">${product.brand}</p>
21+
<p class="text-lg font-bold text-gray-900">
22+
${parseInt(product.lprice).toLocaleString()}
23+
</p>
24+
</div>
25+
<!-- 장바구니 버튼 -->
26+
<button class="w-full bg-blue-600 text-white text-sm py-2 px-3 rounded-md
27+
hover:bg-blue-700 transition-colors add-to-cart-btn" data-product-id="85067212996">
28+
장바구니 담기
29+
</button>
30+
</div>
31+
</div>
32+
`;
33+
};

0 commit comments

Comments
 (0)