Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
620ad18
Add nodemon and ssr:watch script for development
ckdwns9121 Sep 2, 2025
ecc2e81
feat: 프로젝트 초기 설정 및 watch 모드 설정
ckdwns9121 Sep 3, 2025
07d048e
feat: Add core directive and implementation phases for software engin…
ckdwns9121 Sep 3, 2025
c4e7c87
feat: 쇼핑몰 SSR/SSG 구현
ckdwns9121 Sep 3, 2025
b57edf1
feat: API 라우트 개선 및 상품 상세 페이지 렌더링 추가
ckdwns9121 Sep 3, 2025
00a2ebe
feat: 개선된 빌드 및 미리보기 스크립트와 라우터 디버깅 로그 추가
ckdwns9121 Sep 3, 2025
2dcef3a
feat: 라우팅 시스템 자동화 개선
ckdwns9121 Sep 3, 2025
71b20c7
feat: 서버 사이드 렌더링에서 정렬 기준 통일
ckdwns9121 Sep 3, 2025
c383b57
feat: 정적 사이트 생성 프로세스 개선 및 MSW 서버 통합
ckdwns9121 Sep 3, 2025
8183592
feat: MSW 서버 통합 및 SSR 데이터 처리 개선
ckdwns9121 Sep 3, 2025
ed79461
feat: SSR 데이터 처리 및 페이지 로딩 최적화
ckdwns9121 Sep 3, 2025
d3bd8b3
refactor: HTML 템플릿 및 SSR 데이터 처리 개선
ckdwns9121 Sep 3, 2025
62a1b03
refactor: 개선된 쿼리 처리 및 SSR 데이터 통합
ckdwns9121 Sep 3, 2025
59479a4
refactor: 클라이언트 데이터 로드 및 상태 관리 개선
ckdwns9121 Sep 3, 2025
dcbc1b9
fix: localStorage 접근 시 SSR 환경 처리 및 에러 핸들링 개선
ckdwns9121 Sep 3, 2025
799b0e9
refactor: 서버 및 클라이언트 데이터 처리 개선
ckdwns9121 Sep 3, 2025
bbcdb09
refactor: 빌드 및 서버 설정 개선
ckdwns9121 Sep 3, 2025
401f6c8
refactor: SSR 테스트를 SSE로 변경 및 서버 라우팅 개선
ckdwns9121 Sep 3, 2025
a0b88f9
refactor: 서버 및 라우터 URL 처리 개선
ckdwns9121 Sep 4, 2025
f0109ee
refactor: Router.js에서 서버 환경 의존성 제거 및 URL 처리 개선
ckdwns9121 Sep 4, 2025
29f9cc9
refactor: 서버 및 클라이언트 라우팅 개선 및 상태 격리
ckdwns9121 Sep 4, 2025
473b77c
fix: msw 비활성화 제거
ckdwns9121 Sep 4, 2025
a3cfe4c
chore: 불필요한 파일 삭제
ckdwns9121 Sep 4, 2025
2b2c7f2
feat: React SSR 구현 완료
ckdwns9121 Sep 4, 2025
61264ea
chore: 불필요한 파일 삭제
ckdwns9121 Sep 4, 2025
6aad58f
feat: React SSR 구현 완료 및 하이드레이션 문제 해결
ckdwns9121 Sep 4, 2025
d145d99
chore: alert 디버깅 삭제
ckdwns9121 Sep 4, 2025
3920fe8
feat: SSR 쿼리 지원 및 검색바 개선
ckdwns9121 Sep 4, 2025
8bde363
fix: build 스크립트에서 npm을 pnpm으로 변경 및 정적 사이트 생성 로직 개선
ckdwns9121 Sep 4, 2025
cac41af
refactor: 요청별 상태 관리 및 SSR 데이터 처리 개선
ckdwns9121 Sep 4, 2025
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
114 changes: 114 additions & 0 deletions .cursor/rules/rule.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
alwaysApply: true
---

## Core Directive

You are a senior software engineer AI assistant. For EVERY task request, you MUST follow the three-phase process below in exact order. Each phase must be completed with expert-level precision and detail.

## Guiding Principles

- **Minimalistic Approach**: Implement high-quality, clean solutions while avoiding unnecessary complexity
- **Expert-Level Standards**: Every output must meet professional software engineering standards
- **Concrete Results**: Provide specific, actionable details at each step

---

## Phase 1: Codebase Exploration & Analysis

**REQUIRED ACTIONS:**

1. **Systematic File Discovery**
- List ALL potentially relevant files, directories, and modules
- Search for related keywords, functions, classes, and patterns
- Examine each identified file thoroughly

2. **Convention & Style Analysis**
- Document coding conventions (naming, formatting, architecture patterns)
- Identify existing code style guidelines
- Note framework/library usage patterns
- Catalog error handling approaches

**OUTPUT FORMAT:**

```
### Codebase Analysis Results
**Relevant Files Found:**
- [file_path]: [brief description of relevance]

**Code Conventions Identified:**
- Naming: [convention details]
- Architecture: [pattern details]
- Styling: [format details]

**Key Dependencies & Patterns:**
- [library/framework]: [usage pattern]
```

---

## Phase 2: Implementation Planning

**REQUIRED ACTIONS:**
Based on Phase 1 findings, create a detailed implementation roadmap.

**OUTPUT FORMAT:**

```markdown
## Implementation Plan

### Module: [Module Name]

**Summary:** [1-2 sentence description of what needs to be implemented]

**Tasks:**

- [ ] [Specific implementation task]
- [ ] [Specific implementation task]

**Acceptance Criteria:**

- [ ] [Measurable success criterion]
- [ ] [Measurable success criterion]
- [ ] [Performance/quality requirement]

### Module: [Next Module Name]

[Repeat structure above]
```

---

## Phase 3: Implementation Execution

**REQUIRED ACTIONS:**

1. Implement each module following the plan from Phase 2
2. Verify ALL acceptance criteria are met before proceeding
3. Ensure code adheres to conventions identified in Phase 1

**QUALITY GATES:**

- [ ] All acceptance criteria validated
- [ ] Code follows established conventions
- [ ] Minimalistic approach maintained
- [ ] Expert-level implementation standards met

---

## Success Validation

Before completing any task, confirm:

- ✅ All three phases completed sequentially
- ✅ Each phase output meets specified format requirements
- ✅ Implementation satisfies all acceptance criteria
- ✅ Code quality meets professional standards

## Response Structure

Always structure your response as:

1. **Phase 1 Results**: [Codebase analysis findings]
2. **Phase 2 Plan**: [Implementation roadmap]
3. **Phase 3 Implementation**: [Actual code with validation]
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ dist-ssr
/test-results/
/playwright-report/
/coverage/

/docs
12 changes: 6 additions & 6 deletions e2e/createTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,8 +756,8 @@ export const createSSRTest = (baseUrl: string) => {
});
});

test.describe("3. SSR 상품 상세 페이지", () => {
test("상품 상세 페이지가 SSR로 올바르게 렌더링된다", async ({ browser }) => {
test.describe("3. SSE 상품 상세 페이지", () => {
test("상품 상세 페이지가 SSE로 올바르게 렌더링된다", async ({ browser }) => {
// JavaScript가 완전히 비활성화된 컨텍스트 생성
const context = await browser.newContext({
javaScriptEnabled: false,
Expand All @@ -767,21 +767,21 @@ export const createSSRTest = (baseUrl: string) => {
// 직접 상품 상세 URL로 접근
await page.goto(`${baseUrl}product/85067212996/`);

// SSR로 렌더링된 상품 정보가 즉시 표시되는지 확인
// SSE로 렌더링된 상품 정보가 즉시 표시되는지 확인
const bodyContent = await page.locator("body").textContent();
expect(bodyContent).toContain("PVC 투명 젤리 쇼핑백");
expect(bodyContent).toContain("220원");
expect(bodyContent).toContain("상품 상세");

// 관련 상품 섹션도 SSR로 렌더링되었는지 확인
// 관련 상품 섹션도 SSE로 렌더링되었는지 확인
expect(bodyContent).toContain("관련 상품");

await context.close();
});
});

test.describe("4. SSR SEO 및 메타데이터", () => {
test("SSR 페이지에 적절한 메타태그가 포함된다", async ({ browser }) => {
test.describe("4. SSE SEO 및 메타데이터", () => {
test("SSE 페이지에 적절한 메타태그가 포함된다", async ({ browser }) => {
// JavaScript가 완전히 비활성화된 컨텍스트 생성
const context = await browser.newContext({
javaScriptEnabled: false,
Expand Down
19 changes: 13 additions & 6 deletions packages/lib/src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,19 @@ export class Router<Handler extends (...args: any[]) => any> {
addRoute(path: string, handler: Handler) {
// 경로 패턴을 정규식으로 변환
const paramNames: string[] = [];
const regexPath = path
.replace(/:\w+/g, (match) => {
paramNames.push(match.slice(1)); // ':id' -> 'id'
return "([^/]+)";
})
.replace(/\//g, "\\/");

let regexPath;
if (path === "*") {
// 와일드카드는 모든 경로에 매치
regexPath = ".*";
} else {
regexPath = path
.replace(/:\w+/g, (match) => {
paramNames.push(match.slice(1)); // ':id' -> 'id'
return "([^/]+)";
})
.replace(/\//g, "\\/");
}

const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`);

Expand Down
23 changes: 19 additions & 4 deletions packages/lib/src/createStorage.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { createObserver } from "./createObserver.ts";

export const createStorage = <T>(key: string, storage = window.localStorage) => {
let data: T | null = JSON.parse(storage.getItem(key) ?? "null");
export const createStorage = <T>(key: string, storage = typeof window !== "undefined" ? window.localStorage : null) => {
let data: T | null = null;

// SSR 환경에서는 초기화하지 않음
if (storage) {
try {
data = JSON.parse(storage.getItem(key) ?? "null");
} catch (error) {
console.error(`Error parsing storage item for key "${key}":`, error);
data = null;
}
}

const { subscribe, notify } = createObserver();

const get = () => data;

const set = (value: T) => {
try {
data = value;
storage.setItem(key, JSON.stringify(data));
if (storage) {
storage.setItem(key, JSON.stringify(data));
}
notify();
} catch (error) {
console.error(`Error setting storage item for key "${key}":`, error);
Expand All @@ -19,7 +32,9 @@ export const createStorage = <T>(key: string, storage = window.localStorage) =>
const reset = () => {
try {
data = null;
storage.removeItem(key);
if (storage) {
storage.removeItem(key);
}
notify();
} catch (error) {
console.error(`Error removing storage item for key "${key}":`, error);
Expand Down
7 changes: 6 additions & 1 deletion packages/lib/src/hooks/useRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ const defaultSelector = <T, S = T>(state: T) => state as unknown as S;

export const useRouter = <T extends RouterInstance<AnyFunction>, S>(router: T, selector = defaultSelector<T, S>) => {
const shallowSelector = useShallowSelector(selector);
return useSyncExternalStore(router.subscribe, () => shallowSelector(router));

// 서버 환경에서도 동작하도록 getServerSnapshot 제공
const getSnapshot = () => shallowSelector(router);
const getServerSnapshot = () => shallowSelector(router);

return useSyncExternalStore(router.subscribe, getSnapshot, getServerSnapshot);
};
7 changes: 6 additions & 1 deletion packages/lib/src/hooks/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@ const defaultSelector = <T, S = T>(state: T) => state as unknown as S;

export const useStore = <T, S = T>(store: Store<T>, selector: (state: T) => S = defaultSelector<T, S>) => {
const shallowSelector = useShallowSelector(selector);
return useSyncExternalStore(store.subscribe, () => shallowSelector(store.getState()));

// 서버 환경에서도 동작하도록 getServerSnapshot 제공
const getSnapshot = () => shallowSelector(store.getState());
const getServerSnapshot = () => shallowSelector(store.getState());

return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
};
47 changes: 24 additions & 23 deletions packages/react/index.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--app-head-->
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280"
}
}
}
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"><!--app-html--></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--app-head-->
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css" />
<!-- app-data -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280",
},
},
},
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"><!--app-html--></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
22 changes: 11 additions & 11 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"build:client": "rm -rf ./dist/react && vite build --outDir ./dist/react && cp ./dist/react/index.html ./dist/react/404.html",
"build:client-for-ssg": "rm -rf ../../dist/react && vite build --outDir ../../dist/react",
"build:server": "vite build --outDir ./dist/react-ssr --ssr src/main-server.tsx",
"build:without-ssg": "pnpm run build:client && npm run build:server",
"build:without-ssg": "pnpm run build:client && pnpm run build:server",
"build:ssg": "pnpm run build:client-for-ssg && node static-site-generate.js",
"build": "npm run build:client && npm run build:server && npm run build:ssg",
"build": "pnpm run build:client && pnpm run build:server && pnpm run build:ssg",
"preview:csr": "vite preview --outDir ./dist/react --port 4175",
"preview:csr-with-build": "pnpm run build:client && pnpm run preview:csr",
"preview:ssr": "PORT=4176 NODE_ENV=production node server.js",
Expand All @@ -27,30 +27,30 @@
"prettier:write": "prettier --write ./src"
},
"dependencies": {
"@hanghae-plus/lib": "workspace:*",
"react": "latest",
"react-dom": "latest",
"@hanghae-plus/lib": "workspace:*"
"react-dom": "latest"
},
"devDependencies": {
"@babel/core": "latest",
"@babel/plugin-transform-react-jsx": "latest",
"@eslint/js": "^9.16.0",
"@types/node": "^24.0.13",
"@types/react": "latest",
"@types/react-dom": "latest",
"@types/use-sync-external-store": "latest",
"@vitejs/plugin-react": "latest",
"@types/node": "^24.0.13",
"compression": "^1.7.5",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"express": "^4.21.2",
"msw": "^2.10.2",
"prettier": "^3.4.2",
"sirv": "^3.0.0",
"typescript": "^5.8.3",
"vite": "npm:rolldown-vite@latest",
"express": "^5.1.0",
"compression": "^1.7.5",
"sirv": "^3.0.0"
"vite": "npm:rolldown-vite@latest"
}
}
Loading