diff --git a/.eslintrc.js b/.eslintrc.js
index bebdb7ce..1c9f431f 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -49,6 +49,8 @@ module.exports = {
},
],
'@typescript-eslint/no-use-before-define': ['off'],
+ 'react-hooks/exhaustive-deps': 'off',
+ '@typescript-eslint/no-shadow': 'off',
},
ignorePatterns: ['**/build/**/*', '.eslintrc.js', 'craco.config.js'],
settings: {
diff --git a/README.md b/README.md
index e69de29b..bbbcea78 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,80 @@
+## 3주차 질문
+
+### 1. CORS 에러는 무엇이고 언제 발생하는지 설명해주세요. 이를 해결할 수 있는 방법에 대해서도 설명해주세요.
+
+CORS(Cross Origin Resource Sharing)는 교차-출처 리소스 공유로 말할 수 있는데 서로 다른 도메인간에 자원을 공유하는 것을 의미하는데 기본적으로 브라우저는 차단되어있다.
+URL 구조에는 출처인 protocal, host, 포트번호가 있는데 이 출처를 비교하는 로직이 서버에서 구현되는 것이 아니라 브라우저단에서 이루어져서 cors 정책을 위반하는 리소스를 요청해도 일단 정상적으로 응답하고 브라우저가 이 응답을 분석해서 CORS 위반이라고 생각하면 응답을 버리게되면서 발생!
+
+- 클라이언트에서 해결
+ 가장 간단한 방법은 Proxy 패턴을 이용해서 클라이언트 웹 페이지에서 직접 하는 것이 아니라 페이지에서 클라이언트 서버로 보내고 여기서 다시 백엔드 서버로 요청을 보내도록 한다. 서버끼리 통신할때는 CORS 정책이 적용되지 않기 때문!
+
+- 서버에서 해결
+
+1. @CrossOrigin 어노테이션 사용하기
+2. CorsFilter 사용하기
+
+### 2. 비동기 처리 방법인 callback, promise, async await에 대해 각각 장단점과 함께 설명해주세요.
+
+- Callback
+ 다른 함수가 실행을 끝낸 뒤 실행(call back)되는 함수(나중에 호출되는 함수)를 말한다.
+ 장점 : 자바스크립트는 싱글스레드를 사용하는데 멈춤을 방지해준다.
+ 단점 : 에러/예외처리 어려움, 중첩으로 인한 복잡도 증가
+
+- Promise
+ 싱글스레드인 자바스크립트에서 비동기 처리를 위해 사용한 callback 함수의 단점을 해결하기 위해 프로미스 객체를 언어적 차원으로 지원
+ 장점 : 콜백함수에 비해 가독성이 좋고 비동기 처리를 동기적으로 보이게하며 순서파악에 용이
+ 단점 : 콜백지옥과 같은 맥락으로 then을 연쇄적으로 호출하면 코드가 복잡해지고 가독성이 떨어진다.
+
+ - Async & await
+ 기존의 비동기 처리방식인 콜백함수의 단점을 보완하기위한 프로미스의 단점을 해결하기위한 최신 문법
+ 장점 : 동기코드처럼 보이게 작성해 가독성을 높일 수 있고 사용 방법이 굉장히 간단하다.
+
+### 3. react query의 주요 특징에 대해 설명하고, queryKey는 어떤 역할을 하는지 설명해주세요.
+
+React Query는 데이터 Fetching, 동기화, 서버 데이터 업데이트 등을쉽게 도와주는 라이브러리
+특징은 동일한 데이터에 대한 중복 요청 제거, 오래된 데이터 상태파악 후 updating을 지원, 리액트 훅과 유사한 인터페이스 제공 등이 있다.
+queryKey는 useQuery마다 부여되는 고유한 키 값
+문자열로 사용되기도하고 배열의 형태로 사용될 수도 있으며 이것을 통해 다른 곳에서도 해당 쿼리의 결과를 꺼내올 수 있다.
+
+# 요구사항
+
+## Step1
+
+- 첨부된 oas.yaml 파일을 토대로 Request, Response Type을 정의
+- React Query를 사용하지 말고 axiou를 사용해서 구현
+- 첨부된 oas.yaml 파일과 목 API URL을 사용하여 API를 구현
+
+### 메인 페이지 - Themes 카테고리 섹션
+
+- /api/vi/thmes API를 사용하여 Section을 구현
+- API는 Axios 또는 React Query 등을 모두 활용해서 구현해도 가능
+
+### 메인 페이지 - 실시간 급상승 선물생킹 섹션
+
+- /api/v1/ranking/products API를 사용하여 Section을 구현
+- 필터 조건을 선택하면 해당 조건에 맞게 API를 요청하여 보여지게 하기
+
+### Theme 페이지 - header
+
+- url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현
+- themeKey 가 잘못 된 경우 메인 페이지로 연결
+
+### Theme 페이지 - 상품 목록 섹션
+
+- /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현
+- API 요청 시 한번에 20개의 상품 목록이 내려오도록
+
+---
+
+## Step2
+
+- 각API에서 Loading 상태에 대한 UI 대응
+- 데이터가 없는 경우에 대한 UI 대응
+- Http Status 에 따라 Error를 다르게 처리
+
+---
+
+## step3
+
+- 스크롤을 내리면 추가로 데이터를 요청하여 보여지게 하기
+- 1단계에서 구현한 API를 react-query를 사용해서 구현
diff --git a/package-lock.json b/package-lock.json
index 89581c64..26510f45 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,8 +10,11 @@
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
+ "axios": "^1.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-intersection-observer": "^9.13.0",
+ "react-query": "^3.39.3",
"react-router-dom": "^6.22.1"
},
"devDependencies": {
@@ -31,7 +34,7 @@
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.82",
- "@types/react": "^18.2.57",
+ "@types/react": "^18.3.3",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
@@ -10545,13 +10548,12 @@
"dev": true
},
"node_modules/@types/react": {
- "version": "18.2.58",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.58.tgz",
- "integrity": "sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==",
+ "version": "18.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
+ "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
- "@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
@@ -10576,12 +10578,6 @@
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"dev": true
},
- "node_modules/@types/scheduler": {
- "version": "0.16.8",
- "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
- "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
- "dev": true
- },
"node_modules/@types/semver": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
@@ -11988,8 +11984,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
@@ -12061,6 +12056,16 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -12666,8 +12671,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -12727,7 +12731,6 @@
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
- "dev": true,
"engines": {
"node": ">=0.6"
}
@@ -12870,6 +12873,21 @@
"node": ">=8"
}
},
+ "node_modules/broadcast-channel": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
+ "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
+ "dependencies": {
+ "@babel/runtime": "^7.7.2",
+ "detect-node": "^2.1.0",
+ "js-sha3": "0.8.0",
+ "microseconds": "0.2.0",
+ "nano-time": "1.0.0",
+ "oblivious-set": "1.0.0",
+ "rimraf": "3.0.2",
+ "unload": "2.2.0"
+ }
+ },
"node_modules/browser-assert": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz",
@@ -13498,7 +13516,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -13599,8 +13616,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/concat-stream": {
"version": "1.6.2",
@@ -15481,7 +15497,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -15535,8 +15550,7 @@
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
- "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
- "dev": true
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
},
"node_modules/detect-node-es": {
"version": "1.1.0",
@@ -18405,10 +18419,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
- "dev": true,
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -18614,7 +18627,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
- "dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -18714,8 +18726,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
@@ -19740,7 +19751,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -19749,8 +19759,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "1.3.8",
@@ -23850,6 +23859,11 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/js-sha3": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -24626,6 +24640,15 @@
"react": ">= 0.14.0"
}
},
+ "node_modules/match-sorter": {
+ "version": "6.3.4",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
+ "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.8",
+ "remove-accents": "0.5.0"
+ }
+ },
"node_modules/mdast-util-definitions": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz",
@@ -24728,6 +24751,11 @@
"node": ">=8.6"
}
},
+ "node_modules/microseconds": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
+ "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
+ },
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -24744,7 +24772,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -24753,7 +24780,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -24970,6 +24996,14 @@
"thenify-all": "^1.0.0"
}
},
+ "node_modules/nano-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
+ "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
+ "dependencies": {
+ "big-integer": "^1.6.16"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -25514,6 +25548,11 @@
"integrity": "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==",
"dev": true
},
+ "node_modules/oblivious-set": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
+ "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
+ },
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
@@ -25551,7 +25590,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -25835,7 +25873,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -27576,8 +27613,7 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "dev": true
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": {
"version": "1.9.0",
@@ -28266,11 +28302,50 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"dev": true
},
+ "node_modules/react-intersection-observer": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz",
+ "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==",
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-query": {
+ "version": "3.39.3",
+ "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
+ "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "broadcast-channel": "^3.4.1",
+ "match-sorter": "^6.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -28941,6 +29016,11 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remove-accents": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
+ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
+ },
"node_modules/renderkid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -29135,7 +29215,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -29150,7 +29229,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -29160,7 +29238,6 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -29180,7 +29257,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -31842,6 +31918,15 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/unload": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
+ "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
+ "dependencies": {
+ "@babel/runtime": "^7.6.2",
+ "detect-node": "^2.0.4"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -33313,8 +33398,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/write-file-atomic": {
"version": "4.0.2",
diff --git a/package.json b/package.json
index 0a6f0b8f..47f5299a 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,11 @@
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
+ "axios": "^1.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-intersection-observer": "^9.13.0",
+ "react-query": "^3.39.3",
"react-router-dom": "^6.22.1"
},
"devDependencies": {
@@ -45,7 +48,7 @@
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.82",
- "@types/react": "^18.2.57",
+ "@types/react": "^18.3.3",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
diff --git a/src/App.tsx b/src/App.tsx
index 26d8766c..e16a1545 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,11 +1,17 @@
+import { QueryClient, QueryClientProvider } from 'react-query';
+
import { AuthProvider } from './provider/Auth';
import { Routes } from './routes';
+const queryClient = new QueryClient();
+
const App = () => {
return (
-
-
-
+
+
+
+
+
);
};
diff --git a/src/api/api.ts b/src/api/api.ts
new file mode 100644
index 00000000..268173d1
--- /dev/null
+++ b/src/api/api.ts
@@ -0,0 +1,47 @@
+import axios from 'axios';
+
+import { RankingProductsResponse, ThemeProductsResponse, ThemesResponse } from '@/types';
+
+const API_URL = 'https://react-gift-mock-api-eunkyung.vercel.app';
+
+const apiClient = axios.create({
+ baseURL: API_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+export const fetchThemes = async (): Promise => {
+ const response = await apiClient.get('/api/v1/themes');
+ return response.data;
+};
+
+export const fetchRankingProducts = async (
+ targetType: string,
+ rankType: string,
+): Promise => {
+ const response = await apiClient.get('/api/v1/ranking/products', {
+ params: {
+ targetType,
+ rankType,
+ },
+ });
+ return response.data;
+};
+
+export const fetchThemeProducts = async (
+ themeKey: string,
+ page: number = 0,
+): Promise => {
+ const response = await apiClient.get(
+ `/api/v1/themes/${themeKey}/products`,
+ {
+ params: {
+ maxResults: 20,
+ page,
+ },
+ },
+ );
+ const nextCursor = response.data.products.length < 20 ? null : page + 1;
+ return { ...response.data, nextCursor };
+};
diff --git a/src/components/features/Home/GoodsRankingSection/List.tsx b/src/components/features/Home/GoodsRankingSection/List.tsx
index 27bd332c..08c45250 100644
--- a/src/components/features/Home/GoodsRankingSection/List.tsx
+++ b/src/components/features/Home/GoodsRankingSection/List.tsx
@@ -64,6 +64,5 @@ const ButtonWrapper = styled.div`
width: 100%;
display: flex;
justify-content: center;
-
padding-top: 30px;
`;
diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx
index 9464d67c..c9afce77 100644
--- a/src/components/features/Home/GoodsRankingSection/index.tsx
+++ b/src/components/features/Home/GoodsRankingSection/index.tsx
@@ -1,28 +1,54 @@
import styled from '@emotion/styled';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
+import { fetchRankingProducts } from '@/api/api';
import { Container } from '@/components/common/layouts/Container';
import { breakpoints } from '@/styles/variants';
-import type { RankingFilterOption } from '@/types';
-import { GoodsMockList } from '@/types/mock';
+import type { RankingFilterOption, RankingProductsResponse } from '@/types';
import { GoodsRankingFilter } from './Filter';
import { GoodsRankingList } from './List';
-export const GoodsRankingSection = () => {
+export const GoodsRankingSection: React.FC = () => {
const [filterOption, setFilterOption] = useState({
targetType: 'ALL',
rankType: 'MANY_WISH',
});
+ const [rankingProducts, setRankingProducts] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [errorMessage, setErrorMessage] = useState(null);
- // GoodsMockData를 21번 반복 생성
+ useEffect(() => {
+ const loadRankingProducts = async () => {
+ setIsLoading(true);
+ setErrorMessage(null);
+ try {
+ const response = await fetchRankingProducts(filterOption.targetType, filterOption.rankType);
+ setRankingProducts(response);
+ setIsLoading(false);
+ } catch (error) {
+ setErrorMessage('Failed to load ranking products');
+ setIsLoading(false);
+ }
+ };
+
+ loadRankingProducts();
+ }, [filterOption]);
+
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ if (errorMessage) {
+ return {errorMessage}
;
+ }
return (
실시간 급상승 선물랭킹
-
+ {rankingProducts && }
);
diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx
index d82e3afe..a44daf84 100644
--- a/src/components/features/Home/ThemeCategorySection/index.tsx
+++ b/src/components/features/Home/ThemeCategorySection/index.tsx
@@ -1,14 +1,44 @@
import styled from '@emotion/styled';
+import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
+import { fetchThemes } from '@/api/api';
import { Container } from '@/components/common/layouts/Container';
import { Grid } from '@/components/common/layouts/Grid';
import { getDynamicPath } from '@/routes/path';
import { breakpoints } from '@/styles/variants';
+import { ThemeData } from '@/types';
import { ThemeCategoryItem } from './ThemeCategoryItem';
-export const ThemeCategorySection = () => {
+export const ThemeCategorySection: React.FC = () => {
+ const [themes, setThemes] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ useEffect(() => {
+ const loadThemes = async () => {
+ try {
+ const response = await fetchThemes();
+ setThemes(response.themes);
+ setIsLoading(false);
+ } catch (error) {
+ setErrorMessage('Failed to load themes');
+ setIsLoading(false);
+ }
+ };
+
+ loadThemes();
+ }, []);
+
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ if (errorMessage) {
+ return {errorMessage}
;
+ }
+
return (
@@ -18,78 +48,11 @@ export const ThemeCategorySection = () => {
md: 6,
}}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {themes.map((theme) => (
+
+
+
+ ))}
diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx
index 8edbf70e..0c260315 100644
--- a/src/components/features/Theme/ThemeGoodsSection/index.tsx
+++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx
@@ -1,16 +1,52 @@
import styled from '@emotion/styled';
+import { useEffect, useRef } from 'react';
+import { useInfiniteQuery } from 'react-query';
+import { fetchThemeProducts } from '@/api/api';
import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default';
import { Container } from '@/components/common/layouts/Container';
import { Grid } from '@/components/common/layouts/Grid';
import { breakpoints } from '@/styles/variants';
-import { GoodsMockList } from '@/types/mock';
type Props = {
themeKey: string;
};
-export const ThemeGoodsSection = ({}: Props) => {
+export const ThemeGoodsSection = ({ themeKey }: Props) => {
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status, error } = useInfiniteQuery(
+ ['themeProducts', themeKey],
+ ({ pageParam = 0 }) => fetchThemeProducts(themeKey, pageParam),
+ {
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? false,
+ },
+ );
+
+ const observerRef = useRef(null);
+ const loadMoreRef = useRef(null);
+
+ useEffect(() => {
+ if (isFetchingNextPage) return;
+
+ if (observerRef.current) observerRef.current.disconnect();
+
+ observerRef.current = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && hasNextPage) {
+ fetchNextPage();
+ }
+ });
+
+ if (loadMoreRef.current) observerRef.current.observe(loadMoreRef.current);
+
+ return () => {
+ if (observerRef.current) observerRef.current.disconnect();
+ };
+ }, [isFetchingNextPage, fetchNextPage, hasNextPage]);
+
+ if (status === 'loading') return Loading...;
+ if (status === 'error') return {(error as Error).message};
+
+ const allGoods = data?.pages.flatMap((page) => page.products) || [];
+
return (
@@ -21,7 +57,7 @@ export const ThemeGoodsSection = ({}: Props) => {
}}
gap={16}
>
- {GoodsMockList.map(({ id, imageURL, name, price, brandInfo }) => (
+ {allGoods.map(({ id, imageURL, name, price, brandInfo }) => (
{
/>
))}
+
+ {isFetchingNextPage && Loading more...}
);
@@ -44,3 +82,12 @@ const Wrapper = styled.section`
padding: 40px 16px 360px;
}
`;
+
+const Message = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ font-size: 1.5em;
+ color: #999;
+`;
diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx
index 36cfc038..6bbe7f73 100644
--- a/src/components/features/Theme/ThemeHeroSection/index.tsx
+++ b/src/components/features/Theme/ThemeHeroSection/index.tsx
@@ -3,20 +3,13 @@ import styled from '@emotion/styled';
import { Container } from '@/components/common/layouts/Container';
import { breakpoints } from '@/styles/variants';
import type { ThemeData } from '@/types';
-import { ThemeMockList } from '@/types/mock';
type Props = {
- themeKey: string;
+ theme: ThemeData;
};
-export const ThemeHeroSection = ({ themeKey }: Props) => {
- const currentTheme = getCurrentTheme(themeKey, ThemeMockList);
-
- if (!currentTheme) {
- return null;
- }
-
- const { backgroundColor, label, title, description } = currentTheme;
+export const ThemeHeroSection: React.FC = ({ theme }) => {
+ const { backgroundColor, label, title, description } = theme;
return (
diff --git a/src/index.tsx b/src/index.tsx
index ab5f7ad6..45cab63c 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,12 +2,17 @@ import '@/styles';
import React from 'react';
import ReactDOM from 'react-dom/client';
+import { QueryClient, QueryClientProvider } from 'react-query';
import App from '@/App';
+const queryClient = new QueryClient();
+
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
-
+
+
+
,
);
diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx
index 4d02e6c1..e87fa280 100644
--- a/src/pages/Theme/index.tsx
+++ b/src/pages/Theme/index.tsx
@@ -1,22 +1,83 @@
-import { Navigate, useParams } from 'react-router-dom';
+import styled from '@emotion/styled';
+import axios from 'axios';
+import { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { fetchThemes } from '@/api/api';
import { ThemeGoodsSection } from '@/components/features/Theme/ThemeGoodsSection';
import { getCurrentTheme, ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection';
-import { RouterPath } from '@/routes/path';
-import { ThemeMockList } from '@/types/mock';
+import { ThemeData } from '@/types';
-export const ThemePage = () => {
+export const ThemePage: React.FC = () => {
const { themeKey = '' } = useParams<{ themeKey: string }>();
- const currentTheme = getCurrentTheme(themeKey, ThemeMockList);
+ const [currentTheme, setCurrentTheme] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [errorMessage, setErrorMessage] = useState(null);
- if (!currentTheme) {
- return ;
+ useEffect(() => {
+ const loadThemeData = async () => {
+ setIsLoading(true);
+ setErrorMessage(null);
+
+ try {
+ const themesResponse = await fetchThemes();
+ const theme = getCurrentTheme(themeKey, themesResponse.themes);
+ if (!theme) {
+ setErrorMessage('유효하지 않은 키입니다.');
+ setIsLoading(false);
+ return;
+ }
+ setCurrentTheme(theme);
+ setIsLoading(false);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response) {
+ switch (error.response.status) {
+ case 404:
+ setErrorMessage('상품을 찾을 수 없습니다.');
+ break;
+ case 500:
+ setErrorMessage('서버 오류가 발생했습니다.');
+ break;
+ default:
+ setErrorMessage('예기치 않은 오류가 발생했습니다.');
+ }
+ } else if (error.request) {
+ setErrorMessage('요청이 있지만 응답을 받지 못한 경우');
+ } else {
+ setErrorMessage('오류 설정문제발생');
+ }
+ } else {
+ setErrorMessage('에러가 발생했습니다.');
+ }
+ setIsLoading(false);
+ }
+ };
+
+ loadThemeData();
+ }, [themeKey]);
+
+ if (isLoading) {
+ return Loading...;
+ }
+
+ if (errorMessage) {
+ return {errorMessage};
}
return (
<>
-
+ {currentTheme && }
>
);
};
+
+const Message = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ font-size: 1.5em;
+ color: #999;
+`;
diff --git a/src/types/index.ts b/src/types/index.ts
index 9d76b97b..12f8d75f 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -5,8 +5,18 @@ export type ThemeData = {
title: string;
description?: string;
backgroundColor: string;
+ imageURL: string;
};
+export interface ThemesResponse {
+ themes: ThemeData[];
+}
+
+export interface ThemeProductsResponse {
+ products: GoodsData[];
+ nextCursor: number | null;
+}
+
export type RankingFilterOption = {
targetType: 'ALL' | 'FEMALE' | 'MALE' | 'TEEN';
rankType: 'MANY_WISH' | 'MANY_RECEIVE' | 'MANY_WISH_RECEIVE';
@@ -31,3 +41,8 @@ export type GoodsData = {
imageURL: string;
};
};
+
+export interface RankingProductsResponse {
+ products: GoodsData[];
+ nextCursor: number | null;
+}
diff --git a/src/types/mock.ts b/src/types/mock.ts
index cdd90cf7..f56a5e12 100644
--- a/src/types/mock.ts
+++ b/src/types/mock.ts
@@ -7,6 +7,8 @@ export const ThemeMockData: ThemeData = {
title: '예산은 가볍게, 감동은 무겁게❤️',
description: '당신의 센스를 뽐내줄 부담 없는 선물',
backgroundColor: '#4b4d50',
+ imageURL:
+ 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png',
};
export const ThemeMockList = [ThemeMockData];
@@ -33,4 +35,10 @@ export const GoodsMockData: GoodsData = {
},
};
-export const GoodsMockList: GoodsData[] = Array.from({ length: 21 }, () => GoodsMockData);
+export const GoodsMockList: GoodsData[] = Array.from({ length: 21 }, (_, index) => ({
+ ...GoodsMockData,
+ id: GoodsMockData.id + index,
+}));
+
+export const RankingProductsMockList = GoodsMockList;
+export const ThemeProductsMockList = GoodsMockList;