diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c5a43bb3..678e7580 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,65 +2,59 @@ ### 기본과제 -#### 1) 라우팅 구현: -- [ ] History API를 사용하여 SPA 라우터 구현 - - [ ] '/' (홈 페이지) - - [ ] '/profile' (프로필 페이지) - - [ ] '/404' (Not Found 페이지) -- [ ] 각 라우트에 해당하는 컴포넌트 렌더링 함수 작성 -- [ ] 네비게이션 이벤트 처리 (링크 클릭 시 페이지 전환) - -#### 2) 사용자 관리 기능: -- [ ] LocalStorage를 사용한 간단한 사용자 데이터 관리 - - [ ] 사용자 정보 저장 (이름, 간단한 소개) - - [ ] 로그인 상태 관리 (로그인/로그아웃 토글) -- [ ] 로그인 폼 구현 - - [ ] 사용자 이름 입력 및 검증 - - [ ] 로그인 버튼 클릭 시 LocalStorage에 사용자 정보 저장 -- [ ] 로그아웃 기능 구현 - - [ ] 로그아웃 버튼 클릭 시 LocalStorage에서 사용자 정보 제거 - -#### 3) 프로필 페이지 구현: -- [ ] 현재 로그인한 사용자의 정보 표시 - - [ ] 사용자 이름 - - [ ] 간단한 소개 -- [ ] 프로필 수정 기능 - - [ ] 사용자 소개 텍스트 수정 가능 - - [ ] 수정된 정보 LocalStorage에 저장 - -#### 4) 컴포넌트 기반 구조 설계: -- [ ] 재사용 가능한 컴포넌트 작성 - - [ ] Header 컴포넌트 - - [ ] Footer 컴포넌트 -- [ ] 페이지별 컴포넌트 작성 - - [ ] HomePage 컴포넌트 - - [ ] ProfilePage 컴포넌트 - - [ ] NotFoundPage 컴포넌트 - -#### 5) 상태 관리 초기 구현: -- [ ] 간단한 상태 관리 시스템 설계 - - [ ] 전역 상태 객체 생성 (예: 현재 로그인한 사용자 정보) -- [ ] 상태 변경 함수 구현 - - [ ] 상태 업데이트 시 관련 컴포넌트 리렌더링 - -#### 6) 이벤트 처리 및 DOM 조작: -- [ ] 사용자 입력 처리 (로그인 폼, 프로필 수정 등) -- [ ] 동적 컨텐츠 렌더링 (사용자 정보 표시, 페이지 전환 등) - -#### 7) 기본적인 에러 처리: -- [ ] 잘못된 라우트 접근 시 404 페이지로 리다이렉션 -- [ ] 로그인 실패 시 에러 메시지 표시 - -### 심화과제 - -#### 1) 고급 라우팅 -- [ ] 라우트 가드 구현 - - [ ] 로그인 상태에 따른 접근 제어 - - [ ] 비로그인 사용자의 특정 페이지 접근 시 로그인 페이지로 리다이렉션 - -#### 2) 고급 이벤트 처리 - -- [ ] 이벤트 위임 활용 +#### 1) createVNode + +- [ ] 올바른 구조의 vNode를 생성해야 한다 +- [ ] 여러 자식을 처리해야 한다 +- [ ] 자식 배열을 평탄화해야 한다 +- [ ] 중첩 구조를 올바르게 표현해야 한다 +- [ ] JSX로 표현한 결과가 createVNode 함수 호출과 동일해야 한다 + +#### 2) createElement + +- [ ] 문자열 입력에 대해 텍스트 노드를 생성해야 한다 +- [ ] 숫자 입력에 대해 텍스트 노드를 생성해야 한다 +- [ ] null 입력에 대해 빈 텍스트 노드를 생성해야 한다 +- [ ] false 입력에 대해 빈 텍스트 노드를 생성해야 한다 +- [ ] 배열 입력에 대해 DocumentFragment를 생성해야 한다 +- [ ] 함수 컴포넌트를 처리해야 한다 +- [ ] 올바른 속성으로 요소를 생성해야 한다 +- [ ] 이벤트 리스너를 연결해야 한다 +- [ ] 중첩된 자식 요소를 올바르게 처리해야 한다 +- [ ] 깊게 중첩된 구조를 처리해야 한다 +- [ ] 혼합 콘텐츠(텍스트와 요소)를 처리해야 한다 +- [ ] 빈 자식 배열을 처리해야 한다 +- [ ] undefined 자식을 무시해야 한다 +- [ ] 불리언 속성을 처리해야 한다 +- [ ] 데이터 속성을 처리해야 한다 + +#### 3) 컴포넌트를 jsx로 정의하여 사용했는지 확인하기 +- [ ] Post +- [ ] PostForm +- [ ] Header +- [ ] Navigation +- [ ] Footer +- [ ] HomePage +- [ ] LoginPage +- [ ] NotFoundPage +- [ ] ProfilePage +- [ ] HomePage를 렌더링 했을 때 html 문자열로 잘 변환되는지 확인 + +### 심화 과제 + +#### 1) Diff 알고리즘 구현 + +- [ ] 초기 렌더링이 올바르게 수행되어야 한다 +- [ ] diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다 +- [ ] 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다 +- [ ] 요소의 속성만 변경되었을 때 요소를 재사용해야 한다 +- [ ] 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다 + +#### 2) 이벤트 위임 구현 + +- [ ] 이벤트가 위임 방식으로 등록되어야 한다 +- [ ] 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다 +- [ ] 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다 ## 리뷰 받고 싶은 내용 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..f840004f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +__tests__ +*.md +.babelrc +vite.config.js + +# Ignore GitHub workflow files +.github/workflows/**/*.yml +.github/workflows/**/*.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..d16bc4c2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 2, + "semi": true +} diff --git a/package-lock.json b/package-lock.json index b1e3134e..cad6c045 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@vitejs/plugin-react": "^4.3.1", "@vitest/ui": "^2.1.1", "jsdom": "^25.0.0", + "prettier": "3.3.3", "vite": "^5.1.0", "vitest": "^2.1.1" } @@ -864,9 +865,9 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.2.tgz", - "integrity": "sha512-8Ao+EDmTPjZ1ZBABc1ohN7Ylx7UIYcjReZinigedTOnGFhIctyGPxY2II+hJ6gD2/vkDKZTyQ0e7++kwv6wDrw==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", + "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", "cpu": [ "arm" ], @@ -877,9 +878,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.2.tgz", - "integrity": "sha512-I+B1v0a4iqdS9DvYt1RJZ3W+Oh9EVWjbY6gp79aAYipIbxSLEoQtFQlZEnUuwhDXCqMxJ3hluxKAdPD+GiluFQ==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", + "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", "cpu": [ "arm64" ], @@ -890,9 +891,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.2.tgz", - "integrity": "sha512-BTHO7rR+LC67OP7I8N8GvdvnQqzFujJYWo7qCQ8fGdQcb8Gn6EQY+K1P+daQLnDCuWKbZ+gHAQZuKiQkXkqIYg==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", + "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", "cpu": [ "arm64" ], @@ -903,9 +904,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.2.tgz", - "integrity": "sha512-1esGwDNFe2lov4I6GsEeYaAMHwkqk0IbuGH7gXGdBmd/EP9QddJJvTtTF/jv+7R8ZTYPqwcdLpMTxK8ytP6k6Q==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", + "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", "cpu": [ "x64" ], @@ -916,9 +917,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.2.tgz", - "integrity": "sha512-GBHuY07x96OTEM3OQLNaUSUwrOhdMea/LDmlFHi/HMonrgF6jcFrrFFwJhhe84XtA1oK/Qh4yFS+VMREf6dobg==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", + "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", "cpu": [ "arm" ], @@ -929,9 +930,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.2.tgz", - "integrity": "sha512-Dbfa9Sc1G1lWxop0gNguXOfGhaXQWAGhZUcqA0Vs6CnJq8JW/YOw/KvyGtQFmz4yDr0H4v9X248SM7bizYj4yQ==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", + "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", "cpu": [ "arm" ], @@ -942,9 +943,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.2.tgz", - "integrity": "sha512-Z1YpgBvFYhZIyBW5BoopwSg+t7yqEhs5HCei4JbsaXnhz/eZehT18DaXl957aaE9QK7TRGFryCAtStZywcQe1A==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", + "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", "cpu": [ "arm64" ], @@ -955,9 +956,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.2.tgz", - "integrity": "sha512-66Zszr7i/JaQ0u/lefcfaAw16wh3oT72vSqubIMQqWzOg85bGCPhoeykG/cC5uvMzH80DQa2L539IqKht6twVA==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", + "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", "cpu": [ "arm64" ], @@ -968,9 +969,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.2.tgz", - "integrity": "sha512-HpJCMnlMTfEhwo19bajvdraQMcAq3FX08QDx3OfQgb+414xZhKNf3jNvLFYKbbDSGBBrQh5yNwWZrdK0g0pokg==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", + "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", "cpu": [ "ppc64" ], @@ -981,9 +982,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.2.tgz", - "integrity": "sha512-/egzQzbOSRef2vYCINKITGrlwkzP7uXRnL+xU2j75kDVp3iPdcF0TIlfwTRF8woBZllhk3QaxNOEj2Ogh3t9hg==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", + "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", "cpu": [ "riscv64" ], @@ -994,9 +995,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.2.tgz", - "integrity": "sha512-qgYbOEbrPfEkH/OnUJd1/q4s89FvNJQIUldx8X2F/UM5sEbtkqZpf2s0yly2jSCKr1zUUOY1hnTP2J1WOzMAdA==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", + "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", "cpu": [ "s390x" ], @@ -1007,9 +1008,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.2.tgz", - "integrity": "sha512-a0lkvNhFLhf+w7A95XeBqGQaG0KfS3hPFJnz1uraSdUe/XImkp/Psq0Ca0/UdD5IEAGoENVmnYrzSC9Y2a2uKQ==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", + "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", "cpu": [ "x64" ], @@ -1020,9 +1021,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.2.tgz", - "integrity": "sha512-sSWBVZgzwtsuG9Dxi9kjYOUu/wKW+jrbzj4Cclabqnfkot8Z3VEHcIgyenA3lLn/Fu11uDviWjhctulkhEO60g==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", + "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", "cpu": [ "x64" ], @@ -1033,9 +1034,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.2.tgz", - "integrity": "sha512-t/YgCbZ638R/r7IKb9yCM6nAek1RUvyNdfU0SHMDLOf6GFe/VG1wdiUAsxTWHKqjyzkRGg897ZfCpdo1bsCSsA==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", + "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", "cpu": [ "arm64" ], @@ -1046,9 +1047,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.2.tgz", - "integrity": "sha512-kTmX5uGs3WYOA+gYDgI6ITkZng9SP71FEMoHNkn+cnmb9Zuyyay8pf0oO5twtTwSjNGy1jlaWooTIr+Dw4tIbw==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", + "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", "cpu": [ "ia32" ], @@ -1059,9 +1060,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.2.tgz", - "integrity": "sha512-Yy8So+SoRz8I3NS4Bjh91BICPOSVgdompTIPYTByUqU66AXSIOgmW3Lv1ke3NORPqxdF+RdrZET+8vYai6f4aA==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", + "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", "cpu": [ "x64" ], @@ -1207,9 +1208,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@vitejs/plugin-react": { @@ -2142,6 +2143,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -2234,12 +2250,12 @@ "dev": true }, "node_modules/rollup": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.2.tgz", - "integrity": "sha512-JWWpTrZmqQGQWt16xvNn6KVIUz16VtZwl984TKw0dfqqRpFwtLJYYk1/4BTgplndMQKWUk/yB4uOShYmMzA2Vg==", + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", + "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -2249,22 +2265,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.2", - "@rollup/rollup-android-arm64": "4.22.2", - "@rollup/rollup-darwin-arm64": "4.22.2", - "@rollup/rollup-darwin-x64": "4.22.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.2", - "@rollup/rollup-linux-arm-musleabihf": "4.22.2", - "@rollup/rollup-linux-arm64-gnu": "4.22.2", - "@rollup/rollup-linux-arm64-musl": "4.22.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.2", - "@rollup/rollup-linux-riscv64-gnu": "4.22.2", - "@rollup/rollup-linux-s390x-gnu": "4.22.2", - "@rollup/rollup-linux-x64-gnu": "4.22.2", - "@rollup/rollup-linux-x64-musl": "4.22.2", - "@rollup/rollup-win32-arm64-msvc": "4.22.2", - "@rollup/rollup-win32-ia32-msvc": "4.22.2", - "@rollup/rollup-win32-x64-msvc": "4.22.2", + "@rollup/rollup-android-arm-eabi": "4.22.5", + "@rollup/rollup-android-arm64": "4.22.5", + "@rollup/rollup-darwin-arm64": "4.22.5", + "@rollup/rollup-darwin-x64": "4.22.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", + "@rollup/rollup-linux-arm-musleabihf": "4.22.5", + "@rollup/rollup-linux-arm64-gnu": "4.22.5", + "@rollup/rollup-linux-arm64-musl": "4.22.5", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", + "@rollup/rollup-linux-riscv64-gnu": "4.22.5", + "@rollup/rollup-linux-s390x-gnu": "4.22.5", + "@rollup/rollup-linux-x64-gnu": "4.22.5", + "@rollup/rollup-linux-x64-musl": "4.22.5", + "@rollup/rollup-win32-arm64-msvc": "4.22.5", + "@rollup/rollup-win32-ia32-msvc": "4.22.5", + "@rollup/rollup-win32-x64-msvc": "4.22.5", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 59b8baca..0f45a6da 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@vitejs/plugin-react": "^4.3.1", "@vitest/ui": "^2.1.1", "jsdom": "^25.0.0", + "prettier": "3.3.3", "vite": "^5.1.0", "vitest": "^2.1.1" } diff --git a/src/App.jsx b/src/App.jsx index 3764f6dc..7dfd85de 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,38 @@ /** @jsx createVNode */ -import{ createVNode } from "./lib"; +import { createVNode } from './lib'; +import { globalStore } from './stores/index.js'; +import { NotFoundPage } from './pages/index.js'; -export const App = () => ({}); +export const App = ({ targetPage }) => { + const PageComponent = targetPage ?? NotFoundPage; + const error = globalStore.getState().error; + + const closeError = (e) => { + e.preventDefault(); + globalStore.setState({ error: null }); + }; + + return ( +
+ + {error ? ( + + ) : ( + '' + )} +
+ ); +}; diff --git a/src/__tests__/chapter1-2/advanced.test.jsx b/src/__tests__/chapter1-2/advanced.test.jsx index 01384637..94611aed 100644 --- a/src/__tests__/chapter1-2/advanced.test.jsx +++ b/src/__tests__/chapter1-2/advanced.test.jsx @@ -99,6 +99,53 @@ describe('Chapter1-2 > 심화과제 > Virtual DOM과 이벤트 관리', () => { expect(container.innerHTML).toBe('Hello'); expect(container.firstChild).not.toBe(originalElement); // 다른 요소 참조 확인 }); + + it('함수형 컴포넌트가 업데이트될 때 필요한 부분만 렌더링해야 한다', () => { + const FuncComponent = ({ title, content }) => ( +
+

{title}

+

{content}

+
+ ); + + const initialVNode = ; + renderElement(initialVNode, container); + + const originalH1 = container.querySelector('h1'); + const originalP = container.querySelector('p'); + + const updatedVNode = ; + renderElement(updatedVNode, container); + + expect(container.querySelector('h1')).toBe(originalH1); + expect(container.querySelector('p')).toBe(originalP); + expect(container.querySelector('h1').textContent).toBe('Updated Title'); + expect(container.querySelector('p').textContent).toBe('Initial Content'); + }); + + it('중첩된 함수형 컴포넌트에서 깊은 레벨의 변경사항만 업데이트해야 한다', () => { + const ChildComponent = ({ text }) =>

{text}

; + const ParentComponent = ({ title, childText }) => ( +
+

{title}

+ +
+ ); + + const initialVNode = ; + renderElement(initialVNode, container); + + const originalH1 = container.querySelector('h1'); + const originalP = container.querySelector('p'); + + const updatedVNode = ; + renderElement(updatedVNode, container); + + expect(container.querySelector('h1')).toBe(originalH1); + expect(container.querySelector('p')).toBe(originalP); + expect(container.querySelector('h1').textContent).toBe('Parent Title'); + expect(container.querySelector('p').textContent).toBe('Updated Child Text'); + }); }); describe('이벤트 관리 > ', () => { diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index 2fc9f16c..ce9bb6b5 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -1,4 +1,24 @@ /** @jsx createVNode */ -import{ createVNode } from "../../lib"; +import { createVNode } from '../../lib'; -export const Post = () => ({}); +export const Post = ({ id, author, time, content }) => { + return ( +
+
+ 프로필 +
+
{author}
+
{time}
+
+
+

{content}

+
+ + 좋아요 + + 댓글 + 공유 +
+
+ ); +}; diff --git a/src/components/posts/PostForm.jsx b/src/components/posts/PostForm.jsx index 7788cd67..e901b26e 100644 --- a/src/components/posts/PostForm.jsx +++ b/src/components/posts/PostForm.jsx @@ -1,4 +1,17 @@ /** @jsx createVNode */ -import{ createVNode } from "../../lib"; +import { createVNode } from '../../lib'; -export const PostForm = () => ({}); +export const PostForm = () => { + return ( +
+ + +
+ ); +}; diff --git a/src/components/templates/Footer.jsx b/src/components/templates/Footer.jsx index c2d3a109..acee97fb 100644 --- a/src/components/templates/Footer.jsx +++ b/src/components/templates/Footer.jsx @@ -1,4 +1,10 @@ /** @jsx createVNode */ -import{ createVNode } from "../../lib"; +import { createVNode } from '../../lib'; -export const Footer = () => ({}); +export const Footer = () => { + return ( +
+

© 2024 항해플러스. All rights reserved.

+
+ ); +}; diff --git a/src/components/templates/Header.jsx b/src/components/templates/Header.jsx index 2d799a6a..f53e8b31 100644 --- a/src/components/templates/Header.jsx +++ b/src/components/templates/Header.jsx @@ -1,4 +1,10 @@ /** @jsx createVNode */ -import{ createVNode } from "../../lib"; +import { createVNode } from '../../lib'; -export const Header = () => ({}); +export const Header = () => { + return ( +
+

항해플러스

+
+ ); +}; diff --git a/src/components/templates/Navigation.jsx b/src/components/templates/Navigation.jsx index 63a020ea..c911852b 100644 --- a/src/components/templates/Navigation.jsx +++ b/src/components/templates/Navigation.jsx @@ -1,4 +1,48 @@ /** @jsx createVNode */ -import{ createVNode } from "../../lib"; +import { createVNode } from '../../lib'; -export const Navigation = () => ({}); +const getNavItemClass = (path) => { + const currentPath = window.location.pathname; + return currentPath === path ? 'text-blue-600 font-bold' : 'text-gray-600'; +}; + +export const Navigation = ({ loggedIn }) => { + return ( + + ); +}; diff --git a/src/lib/createElement.js b/src/lib/createElement.js index e6092bf3..45a6ea5d 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,14 +1,48 @@ -// TODO: createElement 함수 구현 -// 1. vNode가 falsy면 빈 텍스트 노드를 반환합니다. -// 2. vNode가 문자열이나 숫자면 텍스트 노드를 생성하여 반환합니다. -// 3. vNode가 배열이면 DocumentFragment를 생성하고 각 자식에 대해 createElement를 재귀 호출하여 추가합니다. -// 4. vNode.type이 함수면 해당 함수를 호출하고 그 결과로 createElement를 재귀 호출합니다. -// 5. 위 경우가 아니면 실제 DOM 요소를 생성합니다: -// - vNode.type에 해당하는 요소를 생성 -// - vNode.props의 속성들을 적용 (이벤트 리스너, className, 일반 속성 등 처리) -// - vNode.children의 각 자식에 대해 createElement를 재귀 호출하여 추가 - -export function createElement(vNode) { - // 여기에 구현하세요 - return {} -} +export const createElement = (vNode) => { + if (vNode === null || vNode === undefined || typeof vNode === 'boolean') { + return document.createTextNode(''); + } + + if (typeof vNode === 'string' || typeof vNode === 'number') { + return document.createTextNode(vNode); + } + + if (Array.isArray(vNode)) { + const fragment = document.createDocumentFragment(); + vNode.forEach((child) => fragment.appendChild(createElement(child))); + return fragment; + } + + if (typeof vNode.type === 'function') { + return createElement(vNode.type(vNode.props)); + } + + const element = document.createElement(vNode.type); + + if (vNode.props) { + Object.keys(vNode.props).forEach((key) => { + const value = vNode.props[key]; + + if (key.startsWith('on') && typeof value === 'function') { + const eventType = key.slice(2).toLowerCase(); + element.addEventListener(eventType, value); + } else if (key === 'className') { + element.className = value; + } else if (key === 'style' && typeof value === 'object') { + Object.keys(value).forEach((styleName) => { + element.style[styleName] = value[styleName]; + }); + } else { + element.setAttribute(key, value); + } + }); + } + + if (vNode.children) { + vNode.children.forEach((child) => { + element.appendChild(createElement(child)); + }); + } + + return element; +}; diff --git a/src/lib/createElement__v2.js b/src/lib/createElement__v2.js index fea8dd12..54fcf19d 100644 --- a/src/lib/createElement__v2.js +++ b/src/lib/createElement__v2.js @@ -1,11 +1,50 @@ -export function createElement__v2(vNode) { - // 이 함수는 createElement의 개선된 버전입니다. -// 1. falsy vNode 처리 -// 2. 문자열 또는 숫자 vNode 처리 -// 3. 배열 vNode 처리 (DocumentFragment 사용) -// 4. 일반 요소 vNode 처리: -// - 요소 생성 -// - 속성 설정 (이벤트 함수를 이벤트 위임 방식으로 등록할 수 있도록 개선) -// - 자식 요소 추가 - return {} -} +import { addEvent } from './eventManager.js'; + +export const createElement__v2 = (vNode) => { + if (vNode === null || vNode === undefined || typeof vNode === 'boolean') { + return document.createTextNode(''); + } + + if (typeof vNode === 'string' || typeof vNode === 'number') { + return document.createTextNode(vNode); + } + + if (Array.isArray(vNode)) { + const fragment = document.createDocumentFragment(); + vNode.forEach((child) => fragment.appendChild(createElement__v2(child))); + return fragment; + } + + if (typeof vNode.type === 'function') { + return createElement__v2(vNode.type(vNode.props)); + } + + const element = document.createElement(vNode.type); + + if (vNode.props) { + Object.keys(vNode.props).forEach((key) => { + const value = vNode.props[key]; + + if (key.startsWith('on') && typeof value === 'function') { + const eventType = key.slice(2).toLowerCase(); + addEvent(element, eventType, value); + } else if (key === 'className') { + element.className = value; + } else if (key === 'style' && typeof value === 'object') { + Object.keys(value).forEach((styleName) => { + element.style[styleName] = value[styleName]; + }); + } else { + element.setAttribute(key, value); + } + }); + } + + if (vNode.children) { + vNode.children.forEach((child) => { + element.appendChild(createElement__v2(child)); + }); + } + + return element; +}; diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index bd0ee273..d8bf9c84 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,10 +1,7 @@ -// TODO: createVNode 함수 구현 -// 1. type, props, ...children을 매개변수로 받는 함수를 작성하세요. -// 2. 반환값은 { type, props, children } 형태의 객체여야 합니다. -// 3. children은 평탄화(flat)되어야 하며, falsy 값은 필터링되어야 합니다. -// 4. Infinity를 사용하여 모든 깊이의 배열을 평탄화하세요. - -export function createVNode(type, props, ...children) { - // 여기에 구현하세요 - return {} -} +export const createVNode = (type, props, ...children) => { + return { + type, + props, + children: children.flat(Infinity).filter((child) => !!child), + }; +}; diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 3bf1eb31..54592524 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -1,40 +1,58 @@ -// eventManager.js - -// 이벤트 위임을 위한 전역 이벤트 맵 -// 이 맵은 이벤트 타입별로 요소와 해당 요소의 이벤트 핸들러를 저장합니다. const eventMap = new Map(); - -// 이벤트 위임이 설정될 루트 요소 let rootElement = null; -// TODO: setupEventListeners 함수 구현 -// 이 함수는 루트 요소에 이벤트 위임을 설정합니다. -export function setupEventListeners(root) { - // 1. rootElement 설정 - // 2. 기존에 설정된 이벤트 리스너 제거 (있다면) - // 3. eventMap에 등록된 모든 이벤트 타입에 대해 루트 요소에 이벤트 리스너 추가 - // 주의: 이벤트 캡처링을 사용하여 이벤트를 상위에서 하위로 전파 -} - -// TODO: handleEvent 함수 구현 -// 이 함수는 실제 이벤트가 발생했을 때 호출되는 핸들러입니다. -function handleEvent(event) { - // 1. 이벤트 타겟에서 시작하여 루트 요소까지 버블링 - // 2. 각 요소에 대해 해당 이벤트 타입의 핸들러가 있는지 확인 - // 3. 핸들러가 있다면 실행 - // 이를 통해 하위 요소에서 발생한 이벤트를 상위에서 효율적으로 처리할 수 있습니다. -} - -// TODO: addEvent 함수 구현 -export function addEvent(element, eventType, handler) { - // 1. eventMap에 이벤트 타입과 요소, 핸들러 정보 저장 - // 2. 필요한 경우 루트 요소에 새 이벤트 리스너 추가 - // 이 함수를 통해 개별 요소에 직접 이벤트를 붙이지 않고도 이벤트 처리 가능 -} - -// TODO: removeEvent 함수 구현 -export function removeEvent(element, eventType, handler) { - // 1. eventMap에서 해당 요소와 이벤트 타입에 대한 핸들러 제거 - // 2. 해당 이벤트 타입의 모든 핸들러가 제거되면 루트 요소의 리스너도 제거 - // 이를 통해 더 이상 필요 없는 이벤트 핸들러를 정리하고 메모리 누수 방지 -} +export const setupEventListeners = (root) => { + rootElement = root; + + if (rootElement) { + eventMap.forEach((_, eventType) => { + rootElement.removeEventListener(eventType, handleEvent, true); + rootElement.addEventListener(eventType, handleEvent, true); + }); + } +}; + +const handleEvent = (event) => { + let target = event.target; + + while (target && target !== rootElement) { + const handlers = eventMap.get(event.type); + if (handlers) { + const handler = handlers.get(target); + if (handler) { + handler(event); + if (event.cancelBubble) { + break; + } + } + } + target = target.parentNode; + } +}; + +export const addEvent = (element, eventType, handler) => { + if (!eventMap.has(eventType)) { + eventMap.set(eventType, new Map()); + } + + const handlers = eventMap.get(eventType); + handlers.set(element, handler); + + if (rootElement && !eventMap.get(eventType).size) { + rootElement.addEventListener(eventType, handleEvent, true); + } +}; + +export const removeEvent = (element, eventType, handler) => { + const handlers = eventMap.get(eventType); + if (handlers && handlers.get(element) === handler) { + handlers.delete(element); + + if (handlers.size === 0) { + eventMap.delete(eventType); + if (rootElement) { + rootElement.removeEventListener(eventType, handleEvent, true); + } + } + } +}; diff --git a/src/lib/index.js b/src/lib/index.js index 8bdf3883..20705105 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1,6 +1,9 @@ -export * from "./createStore"; -export * from "./createStorage"; -export * from "./createRouter"; -export * from "./createVNode"; -export * from "./createElement"; -export * from "./renderElement"; +export * from './createStore'; +export * from './createStorage'; +export * from './createRouter'; +export * from './createVNode'; +export * from './createElement'; +export * from './renderElement'; +export * from './eventManager'; +export * from './createElement__v2.js'; +export * from './createObserver.js'; diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 9b96f62c..fafdd2ae 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -1,62 +1,126 @@ -// renderElement.js import { addEvent, removeEvent, setupEventListeners } from './eventManager'; -import { createElement__v2 } from "./createElement__v2.js"; - -// TODO: processVNode 함수 구현 -function processVNode() { - // vNode를 처리하여 렌더링 가능한 형태로 변환합니다. - // - null, undefined, boolean 값 처리 - // - 문자열과 숫자를 문자열로 변환 - // - 함수형 컴포넌트 처리 <---- 이게 제일 중요합니다. - // - 자식 요소들에 대해 재귀적으로 processVNode 호출 -} - -// TODO: updateAttributes 함수 구현 -function updateAttributes() { - // DOM 요소의 속성을 업데이트합니다. - // - 이전 props에서 제거된 속성 처리 - // - 새로운 props의 속성 추가 또는 업데이트 - // - 이벤트 리스너, className, style 등 특별한 경우 처리 - // <이벤트 리스너 처리> - // - TODO: 'on'으로 시작하는 속성을 이벤트 리스너로 처리 - // - 주의: 직접 addEventListener를 사용하지 않고, eventManager의 addEvent와 removeEvent 함수를 사용하세요. - // - 이는 이벤트 위임을 통해 효율적으로 이벤트를 관리하기 위함입니다. -} - -// TODO: updateElement 함수 구현 -function updateElement() { - // 1. 노드 제거 (newNode가 없고 oldNode가 있는 경우) - // TODO: oldNode만 존재하는 경우, 해당 노드를 DOM에서 제거 - - // 2. 새 노드 추가 (newNode가 있고 oldNode가 없는 경우) - // TODO: newNode만 존재하는 경우, 새 노드를 생성하여 DOM에 추가 - - // 3. 텍스트 노드 업데이트 - // TODO: newNode와 oldNode가 둘 다 문자열 또는 숫자인 경우 - // TODO: 내용이 다르면 텍스트 노드 업데이트 - - // 4. 노드 교체 (newNode와 oldNode의 타입이 다른 경우) - // TODO: 타입이 다른 경우, 이전 노드를 제거하고 새 노드로 교체 - - // 5. 같은 타입의 노드 업데이트 - // 5-1. 속성 업데이트 - // TODO: updateAttributes 함수를 호출하여 속성 업데이트 - - // 5-2. 자식 노드 재귀적 업데이트 - // TODO: newNode와 oldNode의 자식 노드들을 비교하며 재귀적으로 updateElement 호출 - // HINT: 최대 자식 수를 기준으로 루프를 돌며 업데이트 - - // 5-3. 불필요한 자식 노드 제거 - // TODO: oldNode의 자식 수가 더 많은 경우, 남은 자식 노드들을 제거 -} - -// TODO: renderElement 함수 구현 -export function renderElement(vNode, container) { - // 최상위 수준의 렌더링 함수입니다. - // - 이전 vNode와 새로운 vNode를 비교하여 업데이트 - // - 최초 렌더링과 업데이트 렌더링 처리 - - // 이벤트 위임 설정 - // TODO: 렌더링이 완료된 후 setupEventListeners 함수를 호출하세요. - // 이는 루트 컨테이너에 이벤트 위임을 설정하여 모든 하위 요소의 이벤트를 효율적으로 관리합니다. -} +import { createElement__v2 } from './createElement__v2.js'; + +const processVNode = (vNode) => { + if (vNode === null || vNode === undefined || typeof vNode === 'boolean') { + return ''; + } + + if (typeof vNode === 'string' || typeof vNode === 'number') { + return String(vNode); + } + + if (typeof vNode.type === 'function') { + return processVNode(vNode.type(vNode.props)); + } + + if (typeof vNode === 'object') { + const children = vNode.children.map(processVNode); + return { ...vNode, children }; + } + + return vNode; +}; + +const updateAttributes = (element, oldProps, newProps) => { + Object.keys(oldProps).forEach((key) => { + if (!(key in newProps)) { + if (key.startsWith('on') && typeof oldProps[key] === 'function') { + const eventType = key.slice(2).toLowerCase(); + removeEvent(element, eventType, oldProps[key]); + } else { + element.removeAttribute(key); + } + } + }); + + Object.keys(newProps).forEach((key) => { + const newValue = newProps[key]; + const oldValue = oldProps[key]; + + if (key.startsWith('on') && typeof newValue === 'function') { + const eventType = key.slice(2).toLowerCase(); + if (oldValue !== newValue) { + if (oldValue) { + removeEvent(element, eventType, oldValue); + } + addEvent(element, eventType, newValue); + } + } else if (key === 'className') { + if (newValue !== oldValue) { + element.className = newValue; + } + } else if (key === 'style' && typeof newValue === 'object') { + Object.keys(newValue).forEach((styleName) => { + if (newValue[styleName] !== oldValue?.[styleName]) { + element.style[styleName] = newValue[styleName]; + } + }); + + Object.keys(oldValue || {}).forEach((styleName) => { + if (!(styleName in newValue)) { + element.style[styleName] = ''; + } + }); + } else if (newValue !== oldValue) { + element.setAttribute(key, newValue); + } + }); +}; + +const updateElement = (container, oldVNode, newVNode, index = 0) => { + const oldElement = container.childNodes[index]; + const newElement = createElement__v2(newVNode); + + if (!newVNode && oldVNode) { + container.removeChild(oldElement); + return; + } + + if (newVNode && !oldVNode) { + container.appendChild(newElement); + return; + } + + if (typeof newVNode === 'string' || typeof newVNode === 'number') { + if (newVNode !== oldVNode) { + container.replaceChild(newElement, oldElement); + } + return; + } + + if (newVNode.type !== oldVNode.type) { + container.replaceChild(newElement, oldElement); + return; + } + + updateAttributes(oldElement, oldVNode.props || {}, newVNode.props || {}); + + const oldChildren = oldVNode.children; + const newChildren = newVNode.children; + const maxLen = Math.max(oldChildren.length, newChildren.length); + + for (let i = 0; i < maxLen; i++) { + if (i < newChildren.length) { + updateElement(oldElement, oldChildren[i], newChildren[i], i); + } else if (i < oldChildren.length) { + oldElement.removeChild(oldElement.childNodes[i]); + } + } +}; + +export const renderElement = (vNode, container) => { + const oldVNode = container._vNode; + const newVNode = processVNode(vNode); + + if (!oldVNode) { + const newElement = createElement__v2(newVNode); + container.appendChild(newElement); + } else { + updateElement(container, oldVNode, newVNode); + } + + container._vNode = newVNode; + + setupEventListeners(container); +}; diff --git a/src/main.jsx b/src/main.jsx index 826615fa..7fe1971e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,58 +1,53 @@ /** @jsx createVNode */ -import { createElement, createRouter, createVNode, renderElement } from "./lib"; -import { HomePage, LoginPage, ProfilePage } from "./pages"; -import { globalStore } from "./stores"; -import { ForbiddenError, UnauthorizedError } from "./errors"; -import { userStorage } from "./storages"; -import { addEvent, registerGlobalEvents } from "./utils"; -import { App } from "./App"; +import { createRouter, createVNode, renderElement } from './lib'; +import { HomePage, LoginPage, ProfilePage } from './pages'; +import { globalStore } from './stores'; +import { ForbiddenError, UnauthorizedError } from './errors'; +import { userStorage } from './storages'; +import { addEvent, registerGlobalEvents } from './utils'; +import { App } from './App'; -const router = createRouter({ - "/": HomePage, - "/login": () => { +export const router = createRouter({ + '/': HomePage, + '/login': () => { const { loggedIn } = globalStore.getState(); if (loggedIn) { throw new ForbiddenError(); } - return ; + return ; }, - "/profile": () => { + '/profile': () => { const { loggedIn } = globalStore.getState(); if (!loggedIn) { throw new UnauthorizedError(); } - return ; + return ; }, }); -function logout() { +const logout = () => { globalStore.setState({ currentUser: null, loggedIn: false }); router.push('/login'); userStorage.reset(); -} +}; function handleError(error) { globalStore.setState({ error }); } // 초기화 함수 -function render() { +const render = () => { const $root = document.querySelector('#root'); try { - const $app = createElement(); - if ($root.hasChildNodes()) { - $root.firstChild.replaceWith($app) - } else{ - $root.appendChild($app); - } + renderElement(, $root); } catch (error) { if (error instanceof ForbiddenError) { - router.push("/"); + router.push('/'); return; } if (error instanceof UnauthorizedError) { - router.push("/login"); + router.push('/login'); return; } @@ -61,9 +56,9 @@ function render() { // globalStore.setState({ error }); } registerGlobalEvents(); -} +}; -function main() { +const main = () => { router.subscribe(render); globalStore.subscribe(render); window.addEventListener('error', handleError); @@ -85,6 +80,6 @@ function main() { }); render(); -} +}; main(); diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index 6e61db75..83d2852d 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -1,4 +1,29 @@ /** @jsx createVNode */ -import{ createVNode } from "../lib"; +import { createVNode } from '../lib'; +import { globalStore } from '../stores/index.js'; +import { Footer, Header, Navigation, Post, PostForm } from '../components/index.js'; -export const HomePage = () => ({}); +export const HomePage = () => { + const { loggedIn, posts } = globalStore.getState(); + + return ( +
+
+
+ + + +
+ {loggedIn && } +
+ {posts.map(({ id, author, time, content }) => { + return ; + })} +
+
+ +
+
+
+ ); +}; diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 36ab4ccb..426720e4 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,4 +1,50 @@ /** @jsx createVNode */ -import{ createVNode } from "../lib"; +import { createVNode } from '../lib'; +import { globalStore } from '../stores/index.js'; +import { router } from '../main.jsx'; +import { userStorage } from '../storages/index.js'; -export const LoginPage = () => ({}); +export const LoginPage = () => { + const login = (e) => { + e.preventDefault(); + + const username = document.getElementById('username').value; + const currentUser = { username, email: '', bio: '' }; + globalStore.setState({ + currentUser, + loggedIn: true, + }); + router.push('/'); + userStorage.set(currentUser); + }; + + return ( +
+
+

항해플러스

+
+ + + +
+ +
+
+ +
+
+
+ ); +}; diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx index 09f48ef8..6160239b 100644 --- a/src/pages/NotFoundPage.jsx +++ b/src/pages/NotFoundPage.jsx @@ -1,4 +1,18 @@ /** @jsx createVNode */ -import{ createVNode } from "../lib"; +import { createVNode } from '../lib'; -export const NotFoundPage = () => ({}); +export const NotFoundPage = () => { + return ( +
+
+

항해플러스

+

404

+

페이지를 찾을 수 없습니다

+

요청하신 페이지가 존재하지 않거나 이동되었을 수 있습니다.

+ + 홈으로 돌아가기 + +
+
+ ); +}; diff --git a/src/pages/ProfilePage.jsx b/src/pages/ProfilePage.jsx index 70fb9a9e..a36d873a 100644 --- a/src/pages/ProfilePage.jsx +++ b/src/pages/ProfilePage.jsx @@ -1,4 +1,77 @@ /** @jsx createVNode */ -import{ createVNode } from "../lib"; +import { createVNode } from '../lib'; +import { Footer, Header, Navigation } from '../components/index.js'; +import { globalStore } from '../stores/index.js'; +import { userStorage } from '../storages/index.js'; -export const ProfilePage = () => ({}); +const updateProfile = (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + const updatedProfile = Object.fromEntries(formData); + const currentUser = { ...globalStore.getState().currentUser, ...updatedProfile }; + globalStore.setState({ currentUser }); + userStorage.set(currentUser); + alert('프로필이 업데이트되었습니다.'); +}; + +export const ProfilePage = () => { + const { loggedIn, currentUser } = globalStore.getState(); + const { username = '', email = '', bio = '' } = currentUser ?? {}; + + return ( +
+
+
+ + + +
+
+

내 프로필

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+
+ ); +}; diff --git a/src/template.md b/src/template.md index 99cab607..65beb921 100644 --- a/src/template.md +++ b/src/template.md @@ -74,9 +74,9 @@ const HomePage = () => { ${Navigation({ loggedIn })}
- ${loggedIn ? NotFoundPage() : ''} + ${loggedIn ? PostForm() : ''}
- ${posts.map(ProfilePage).join('')} + ${posts.map(Post).join('')}