From 38107bef31aa8b287b3991ee6f1f362aaecb8654 Mon Sep 17 00:00:00 2001 From: Hyeongmin Jo Date: Thu, 13 Mar 2025 09:22:19 +0900 Subject: [PATCH 1/5] feat: add tsconfig.json --- tsconfig.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e69de29 From 2286690afdf77dd6cd500be733433d444a0dc01d Mon Sep 17 00:00:00 2001 From: Hyeongmin Jo Date: Thu, 13 Mar 2025 09:38:22 +0900 Subject: [PATCH 2/5] =?UTF-8?q?BREADKING:=20Typescript=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 29 +++++++++++++++++++++++++++++ package.json | 2 ++ tsconfig.json | 24 ++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/package-lock.json b/package-lock.json index c27c27b..4e61886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@eslint/eslintrc": "^3", "@hookform/devtools": "^4.3.3", "@tanstack/react-query-devtools": "^5.64.2", + "@types/node": "22.13.10", + "@types/react": "19.0.10", "autoprefixer": "^10.4.20", "eslint": "^9", "eslint-config-next": "15.1.4", @@ -1327,6 +1329,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1334,6 +1346,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.19.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", @@ -6507,6 +6529,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", diff --git a/package.json b/package.json index 8ade942..e24d8c7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "@eslint/eslintrc": "^3", "@hookform/devtools": "^4.3.3", "@tanstack/react-query-devtools": "^5.64.2", + "@types/node": "22.13.10", + "@types/react": "19.0.10", "autoprefixer": "^10.4.20", "eslint": "^9", "eslint-config-next": "15.1.4", diff --git a/tsconfig.json b/tsconfig.json index e69de29..1d4f624 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} From 552c3d16bf9914b44105febf2f5db5a1b12357f3 Mon Sep 17 00:00:00 2001 From: Hyeongmin Jo Date: Fri, 14 Mar 2025 09:58:31 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20strict:false=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20build=20=EB=B0=8F=20=EC=8B=A4=ED=96=89(npm?= =?UTF-8?q?=20run=20dev)=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + api/{index.js => index.ts} | 84 ++++-- .../[articleId]/{loading.jsx => loading.tsx} | 0 .../(root)/articles/[articleId]/page.jsx | 17 -- .../(root)/articles/[articleId]/page.tsx | 22 ++ .../articles/{loading.jsx => loading.tsx} | 0 .../articles/loading/{page.jsx => page.tsx} | 0 .../(root)/articles/{page.jsx => page.tsx} | 0 .../post/[articleId]/{page.jsx => page.tsx} | 28 +- .../articles/post/{page.jsx => page.tsx} | 22 +- .../(root)/auth/log-in/{page.jsx => page.tsx} | 32 ++- .../auth/sign-up/{page.jsx => page.tsx} | 32 ++- .../(root)/{error.jsx => error.tsx} | 0 .../(root)/faq/{page.jsx => page.tsx} | 0 .../(root)/{layout.jsx => layout.tsx} | 0 app/(providers)/(root)/{page.jsx => page.tsx} | 0 .../(root)/privacy/{page.jsx => page.tsx} | 0 .../[productId]/{loading.jsx => loading.tsx} | 0 .../[productId]/{page.jsx => page.tsx} | 4 +- .../products/{loading.jsx => loading.tsx} | 0 .../(root)/products/{page.jsx => page.tsx} | 0 .../post/[productId]/{page.jsx => page.tsx} | 84 +++--- .../products/post/{page.jsx => page.tsx} | 56 ++-- app/(providers)/{layout.jsx => layout.tsx} | 0 app/{layout.jsx => layout.tsx} | 0 .../{ArticleCard.jsx => ArticleCard.tsx} | 0 .../{ArticleDetail.jsx => ArticleDetail.tsx} | 29 ++- .../{ArticleList.jsx => ArticleList.tsx} | 17 +- ...tSuspense.jsx => ArticlesListSuspense.tsx} | 0 ...estArticleCard.jsx => BestArticleCard.tsx} | 0 ...estArticleList.jsx => BestArticleList.tsx} | 0 .../articles/{Comment.jsx => CommentItem.tsx} | 16 +- .../{Comments.jsx => CommentList.tsx} | 41 +-- .../auth/{AuthFooter.jsx => AuthFooter.tsx} | 0 .../common/{AlertModal.jsx => AlertModal.tsx} | 9 +- ...stSkeleton.jsx => ArticleListSkeleton.tsx} | 0 .../common/{AuthButton.jsx => AuthButton.tsx} | 0 components/common/{Button.jsx => Button.tsx} | 8 +- .../{ConfirmModal.jsx => ConfirmModal.tsx} | 0 components/common/DropDownMenu.tsx | 104 ++++++++ .../common/{Dropdown.jsx => Dropdown.tsx} | 0 components/common/{Footer.jsx => Footer.tsx} | 0 components/common/{Header.jsx => Header.tsx} | 0 .../common/{HeaderNav.jsx => HeaderNav.tsx} | 0 components/common/{Input.jsx => Input.tsx} | 2 +- components/common/{Loader.jsx => Loader.tsx} | 0 components/common/{Modal.jsx => Modal.tsx} | 0 components/common/{Page.jsx => Page.tsx} | 4 +- .../common/{Pagination.jsx => Pagination.tsx} | 16 +- ...inationButton.jsx => PaginationButton.tsx} | 14 +- components/common/PopMenuButton.jsx | 176 ------------- components/common/PopMenuButton.tsx | 83 ++++++ ...stSkeleton.jsx => ProductListSkeleton.tsx} | 0 ...roductSkeleton.jsx => ProductSkeleton.tsx} | 0 .../common/{TagChip.jsx => TagChip.tsx} | 8 +- .../home/{BannerImage.jsx => BannerImage.tsx} | 2 +- .../home/{HomeMain.jsx => HomeMain.tsx} | 0 ...estProductList.jsx => BestProductList.tsx} | 0 .../{ProductDetail.jsx => ProductDetail.tsx} | 20 +- .../{ProductItem.jsx => ProductItem.tsx} | 13 +- .../{ProductList.jsx => ProductList.tsx} | 32 +-- components/product/ProductListSuspense.jsx | 15 -- contexts/{AuthContext.jsx => AuthContext.tsx} | 28 +- contexts/ModalContext.jsx | 23 -- contexts/ModalContext.tsx | 27 ++ eslint.config.mjs | 13 +- ...eckInputValid.js => useCheckInputValid.ts} | 25 +- hooks/{useDeviceSize.js => useDeviceSize.ts} | 0 package-lock.json | 243 +++++++++++++++--- tsconfig.json | 12 +- types/dtos/article.dto.ts | 3 + types/dtos/comment.dto.ts | 3 + types/dtos/product.dto.ts | 17 ++ types/dtos/user.dto.ts | 13 + types/entities/article.entity.ts | 13 + types/entities/comment.entity.ts | 7 + types/entities/product.entity.ts | 16 ++ types/entities/user.entity.ts | 9 + utils/actions.js | 24 -- utils/{delay.js => delay.ts} | 2 +- utils/{formattedDate.js => formattedDate.ts} | 2 +- utils/{lineBreakText.js => lineBreakText.tsx} | 2 +- 82 files changed, 947 insertions(+), 528 deletions(-) create mode 100644 .vscode/settings.json rename api/{index.js => index.ts} (84%) rename app/(providers)/(root)/articles/[articleId]/{loading.jsx => loading.tsx} (100%) delete mode 100644 app/(providers)/(root)/articles/[articleId]/page.jsx create mode 100644 app/(providers)/(root)/articles/[articleId]/page.tsx rename app/(providers)/(root)/articles/{loading.jsx => loading.tsx} (100%) rename app/(providers)/(root)/articles/loading/{page.jsx => page.tsx} (100%) rename app/(providers)/(root)/articles/{page.jsx => page.tsx} (100%) rename app/(providers)/(root)/articles/post/[articleId]/{page.jsx => page.tsx} (84%) rename app/(providers)/(root)/articles/post/{page.jsx => page.tsx} (85%) rename app/(providers)/(root)/auth/log-in/{page.jsx => page.tsx} (87%) rename app/(providers)/(root)/auth/sign-up/{page.jsx => page.tsx} (90%) rename app/(providers)/(root)/{error.jsx => error.tsx} (100%) rename app/(providers)/(root)/faq/{page.jsx => page.tsx} (100%) rename app/(providers)/(root)/{layout.jsx => layout.tsx} (100%) rename app/(providers)/(root)/{page.jsx => page.tsx} (100%) rename app/(providers)/(root)/privacy/{page.jsx => page.tsx} (100%) rename app/(providers)/(root)/products/[productId]/{loading.jsx => loading.tsx} (100%) rename app/(providers)/(root)/products/[productId]/{page.jsx => page.tsx} (87%) rename app/(providers)/(root)/products/{loading.jsx => loading.tsx} (100%) rename app/(providers)/(root)/products/{page.jsx => page.tsx} (100%) rename app/(providers)/(root)/products/post/[productId]/{page.jsx => page.tsx} (84%) rename app/(providers)/(root)/products/post/{page.jsx => page.tsx} (89%) rename app/(providers)/{layout.jsx => layout.tsx} (100%) rename app/{layout.jsx => layout.tsx} (100%) rename components/articles/{ArticleCard.jsx => ArticleCard.tsx} (100%) rename components/articles/{ArticleDetail.jsx => ArticleDetail.tsx} (77%) rename components/articles/{ArticleList.jsx => ArticleList.tsx} (85%) rename components/articles/{ArticlesListSuspense.jsx => ArticlesListSuspense.tsx} (100%) rename components/articles/{BestArticleCard.jsx => BestArticleCard.tsx} (100%) rename components/articles/{BestArticleList.jsx => BestArticleList.tsx} (100%) rename components/articles/{Comment.jsx => CommentItem.tsx} (83%) rename components/articles/{Comments.jsx => CommentList.tsx} (82%) rename components/auth/{AuthFooter.jsx => AuthFooter.tsx} (100%) rename components/common/{AlertModal.jsx => AlertModal.tsx} (77%) rename components/common/{ArticleListSkeleton.jsx => ArticleListSkeleton.tsx} (100%) rename components/common/{AuthButton.jsx => AuthButton.tsx} (100%) rename components/common/{Button.jsx => Button.tsx} (91%) rename components/common/{ConfirmModal.jsx => ConfirmModal.tsx} (100%) create mode 100644 components/common/DropDownMenu.tsx rename components/common/{Dropdown.jsx => Dropdown.tsx} (100%) rename components/common/{Footer.jsx => Footer.tsx} (100%) rename components/common/{Header.jsx => Header.tsx} (100%) rename components/common/{HeaderNav.jsx => HeaderNav.tsx} (100%) rename components/common/{Input.jsx => Input.tsx} (97%) rename components/common/{Loader.jsx => Loader.tsx} (100%) rename components/common/{Modal.jsx => Modal.tsx} (100%) rename components/common/{Page.jsx => Page.tsx} (56%) rename components/common/{Pagination.jsx => Pagination.tsx} (86%) rename components/common/{PaginationButton.jsx => PaginationButton.tsx} (84%) delete mode 100644 components/common/PopMenuButton.jsx create mode 100644 components/common/PopMenuButton.tsx rename components/common/{ProductListSkeleton.jsx => ProductListSkeleton.tsx} (100%) rename components/common/{ProductSkeleton.jsx => ProductSkeleton.tsx} (100%) rename components/common/{TagChip.jsx => TagChip.tsx} (81%) rename components/home/{BannerImage.jsx => BannerImage.tsx} (96%) rename components/home/{HomeMain.jsx => HomeMain.tsx} (100%) rename components/product/{BestProductList.jsx => BestProductList.tsx} (100%) rename components/product/{ProductDetail.jsx => ProductDetail.tsx} (90%) rename components/product/{ProductItem.jsx => ProductItem.tsx} (78%) rename components/product/{ProductList.jsx => ProductList.tsx} (82%) delete mode 100644 components/product/ProductListSuspense.jsx rename contexts/{AuthContext.jsx => AuthContext.tsx} (77%) delete mode 100644 contexts/ModalContext.jsx create mode 100644 contexts/ModalContext.tsx rename hooks/{useCheckInputValid.js => useCheckInputValid.ts} (58%) rename hooks/{useDeviceSize.js => useDeviceSize.ts} (100%) create mode 100644 types/dtos/article.dto.ts create mode 100644 types/dtos/comment.dto.ts create mode 100644 types/dtos/product.dto.ts create mode 100644 types/dtos/user.dto.ts create mode 100644 types/entities/article.entity.ts create mode 100644 types/entities/comment.entity.ts create mode 100644 types/entities/product.entity.ts create mode 100644 types/entities/user.entity.ts delete mode 100644 utils/actions.js rename utils/{delay.js => delay.ts} (69%) rename utils/{formattedDate.js => formattedDate.ts} (74%) rename utils/{lineBreakText.js => lineBreakText.tsx} (88%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/api/index.js b/api/index.ts similarity index 84% rename from api/index.js rename to api/index.ts index 71434a8..34816ed 100644 --- a/api/index.js +++ b/api/index.ts @@ -1,3 +1,5 @@ +import { ProductPostDto } from '@/types/dtos/product.dto'; +import { UserLoginDto, UserSignUpDto } from '@/types/dtos/user.dto'; import axios from 'axios'; // const baseURL = 'https://four-sprint-mission-be.onrender.com/'; @@ -9,12 +11,19 @@ export const client = axios.create({ baseURL, }); -function errorHandler(error) { +function errorHandler(error: { + response: { status: any; data: any }; + message: string; +}) { console.log('AxiosError', error); if (error.response) { throw new Error(`${error.response.status}: ${error.response.data}`); } else { - throw new Error(error, '요청에 실패하였습니다.'); + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error('요청에 실패하였습니다.'); + } } } @@ -30,7 +39,7 @@ client.interceptors.request.use( ) return config; console.log('do interceptor'); - let accessToken; + let accessToken: string; if (typeof window !== 'undefined') { accessToken = localStorage.getItem('accessToken'); } @@ -57,7 +66,7 @@ client.interceptors.response.use( if ((statusCode === 401 || statusCode === 419) && !originalRequest._retry) { console.log('토큰 만료'); originalRequest._retry = true; - let prevRefreshToken; + let prevRefreshToken: string; if (typeof window !== 'undefined') { prevRefreshToken = localStorage.getItem('refreshToken'); } @@ -79,7 +88,11 @@ client.interceptors.response.use( * 게시글(article) 관련 API */ // 게시글 등록 -const postArticle = async (articleData) => { +const postArticle = async (articleData: { + writer: string; + title: string; + content: string; +}) => { try { const url = '/articles'; const response = await client.post(url, articleData); @@ -90,7 +103,10 @@ const postArticle = async (articleData) => { }; // 게시글 수정 -const editArticle = async (articleId, articleData) => { +const editArticle = async ( + articleId: string, + articleData: { title: string; content: string } +) => { try { const url = `/articles/${articleId}`; const response = await client.patch(url, articleData); @@ -129,7 +145,7 @@ const getArticles = async ({ }; // 특정 id 게시글 조회 -const getArticle = async (articleId) => { +const getArticle = async (articleId: string) => { try { const url = `/articles/${articleId}`; const response = await client.get(url); @@ -140,7 +156,7 @@ const getArticle = async (articleId) => { }; // 게시글에 좋아요 하기 -const likeArticle = async (articleId) => { +const likeArticle = async (articleId: string) => { try { const url = `/articles/${articleId}/like`; const response = await client.post(url); @@ -151,7 +167,7 @@ const likeArticle = async (articleId) => { }; // 게시글에 좋아요 취소하기 -const unLikeArticle = async (articleId) => { +const unLikeArticle = async (articleId: string) => { try { const url = `/articles/${articleId}/unlike`; const response = await client.delete(url); @@ -167,7 +183,10 @@ const unLikeArticle = async (articleId) => { // panda 마켓 - cursor가 숫자, 그 외에는 '' // 댓글 목록 조회 - 게시글 -const getCommentsOfArticle = async (articleId, { limit = 3, cursor = '' }) => { +const getCommentsOfArticle = async ( + articleId: string, + { limit = 3, cursor = '' } +) => { try { const query = `limit=${limit}&cursor=${cursor}`; const url = `/articles/${articleId}/comments?${query}`; @@ -179,7 +198,10 @@ const getCommentsOfArticle = async (articleId, { limit = 3, cursor = '' }) => { }; // 댓글 등록 - 게시글 -const postArticleComment = async (articleId, commentData) => { +const postArticleComment = async ( + articleId: string, + commentData: { writer: string; content: string } +) => { try { const url = `/articles/${articleId}/comments`; const response = await client.post(url, commentData); @@ -190,7 +212,7 @@ const postArticleComment = async (articleId, commentData) => { }; // 댓글 삭제 -const deleteComment = async (commentId) => { +const deleteComment = async (commentId: string) => { try { const url = `/comments/${commentId}`; const response = await client.delete(url); @@ -201,10 +223,10 @@ const deleteComment = async (commentId) => { }; // 댓글 수정 -const editComment = async (commentId, content) => { +const editComment = async (commentId: string, content: string) => { try { const url = `/comments/${commentId}`; - const response = await client.patch(url, content); + const response = await client.patch(url, { content }); return response.data; } catch (error) { errorHandler(error); @@ -213,7 +235,10 @@ const editComment = async (commentId, content) => { // panda 마켓 - cursor가 숫자, 그 외에는 '' // 댓글 목록 조회 - 상품 -const getCommentsOfProduct = async (productId, { limit = 3, cursor = '' }) => { +const getCommentsOfProduct = async ( + productId: string, + { limit = 3, cursor = '' } +) => { try { const query = `limit=${limit}&cursor=${cursor}`; const url = `/products/${productId}/comments?${query}`; @@ -225,7 +250,10 @@ const getCommentsOfProduct = async (productId, { limit = 3, cursor = '' }) => { }; // 댓글 등록 - 상품 -const postProductComment = async (productId, commentData) => { +const postProductComment = async ( + productId: string, + commentData: { writer: string; content: string } +) => { try { const url = `/products/${productId}/comments`; const response = await client.post(url, commentData); @@ -273,15 +301,15 @@ const getProduct = async (productId) => { }; // 상품 등록 -const postProduct = async (productData) => { +const postProduct = async (productData: ProductPostDto) => { try { const { name, description, price, tags, writer, images } = productData; // file을 전달하므로 반드시 formData형식으로 전달 const formData = new FormData(); formData.append('name', name); formData.append('description', description); - formData.append('price', price); - formData.append('tags', tags); + formData.append('price', price.toString()); + tags.forEach((tag) => formData.append('tags', tag)); formData.append('writer', writer); images.forEach((image) => formData.append('imgUrls', image)); @@ -294,7 +322,7 @@ const postProduct = async (productData) => { }; // 상품 삭제 -const deleteProduct = async (productId) => { +const deleteProduct = async (productId: string) => { try { const url = `/products/${productId}`; const response = await client.delete(url); @@ -305,15 +333,15 @@ const deleteProduct = async (productId) => { }; // 상품 수정 -const editProduct = async (productId, productData) => { +const editProduct = async (productId: string, productData: ProductPostDto) => { try { const { name, description, price, tags, writer, images } = productData; // file을 전달하므로 반드시 formData형식으로 전달 const formData = new FormData(); formData.append('name', name); formData.append('description', description); - formData.append('price', price); - formData.append('tags', tags); + formData.append('price', price.toString()); + tags.forEach((tag) => formData.append('tags', tag)); formData.append('writer', writer); images.forEach((image) => formData.append('imgUrls', image)); @@ -326,7 +354,7 @@ const editProduct = async (productId, productData) => { }; // 상품에 좋아요 하기 -const likeProduct = async (productId) => { +const likeProduct = async (productId: string) => { try { const url = `/products/${productId}/like`; const response = await client.post(url); @@ -337,7 +365,7 @@ const likeProduct = async (productId) => { }; // 상품에 좋아요 취소하기 -const unLikeProduct = async (productId) => { +const unLikeProduct = async (productId: string) => { try { const url = `/products/${productId}/unlike`; const response = await client.delete(url); @@ -351,7 +379,7 @@ const unLikeProduct = async (productId) => { * 회원(user) 관련 API */ // 회원 가입 -const signUp = async (dto) => { +const signUp = async (dto: UserSignUpDto) => { const url = '/users/sign-up'; const response = await client.post(url, dto); const data = response.data; @@ -379,7 +407,7 @@ const signUp = async (dto) => { }; // 로그인 -const logIn = async (dto) => { +const logIn = async (dto: UserLoginDto) => { const url = '/users/log-in'; const response = await client.post(url, dto); const data = response.data; @@ -412,7 +440,7 @@ const logIn = async (dto) => { }; // refreshToken -const refreshToken = async (prevRefreshToken) => { +const refreshToken = async (prevRefreshToken: string) => { try { const url = '/users/refresh-token'; const response = await client.post(url, { prevRefreshToken }); diff --git a/app/(providers)/(root)/articles/[articleId]/loading.jsx b/app/(providers)/(root)/articles/[articleId]/loading.tsx similarity index 100% rename from app/(providers)/(root)/articles/[articleId]/loading.jsx rename to app/(providers)/(root)/articles/[articleId]/loading.tsx diff --git a/app/(providers)/(root)/articles/[articleId]/page.jsx b/app/(providers)/(root)/articles/[articleId]/page.jsx deleted file mode 100644 index 2f4a0d7..0000000 --- a/app/(providers)/(root)/articles/[articleId]/page.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import api from '@/api'; -import ArticleDetail from '@/components/articles/ArticleDetail'; -import Comments from '@/components/articles/Comments'; -import PageContainer from '@/components/common/Page'; - -async function ArticleDetailPage({ params }) { - const { articleId } = await params; - const article = await api.getArticle(articleId); - return ( - - - - - ); -} - -export default ArticleDetailPage; diff --git a/app/(providers)/(root)/articles/[articleId]/page.tsx b/app/(providers)/(root)/articles/[articleId]/page.tsx new file mode 100644 index 0000000..336b5df --- /dev/null +++ b/app/(providers)/(root)/articles/[articleId]/page.tsx @@ -0,0 +1,22 @@ +import api from '@/api'; +import ArticleDetail from '@/components/articles/ArticleDetail'; +import CommentList from '@/components/articles/CommentList'; +import PageContainer from '@/components/common/Page'; +import { ArticleDetailDto } from '@/types/dtos/article.dto'; + +type Params = Promise<{ + articleId: string; +}>; + +async function ArticleDetailPage({ params }: { params: Params }) { + const { articleId } = await params; + const article: ArticleDetailDto = await api.getArticle(articleId); + return ( + + + + + ); +} + +export default ArticleDetailPage; diff --git a/app/(providers)/(root)/articles/loading.jsx b/app/(providers)/(root)/articles/loading.tsx similarity index 100% rename from app/(providers)/(root)/articles/loading.jsx rename to app/(providers)/(root)/articles/loading.tsx diff --git a/app/(providers)/(root)/articles/loading/page.jsx b/app/(providers)/(root)/articles/loading/page.tsx similarity index 100% rename from app/(providers)/(root)/articles/loading/page.jsx rename to app/(providers)/(root)/articles/loading/page.tsx diff --git a/app/(providers)/(root)/articles/page.jsx b/app/(providers)/(root)/articles/page.tsx similarity index 100% rename from app/(providers)/(root)/articles/page.jsx rename to app/(providers)/(root)/articles/page.tsx diff --git a/app/(providers)/(root)/articles/post/[articleId]/page.jsx b/app/(providers)/(root)/articles/post/[articleId]/page.tsx similarity index 84% rename from app/(providers)/(root)/articles/post/[articleId]/page.jsx rename to app/(providers)/(root)/articles/post/[articleId]/page.tsx index ebc0075..495e752 100644 --- a/app/(providers)/(root)/articles/post/[articleId]/page.jsx +++ b/app/(providers)/(root)/articles/post/[articleId]/page.tsx @@ -1,21 +1,23 @@ 'use client'; -import api from '@/api'; -import AlertModal from '@/components/common/AlertModal'; -import Button from '@/components/common/Button'; -import Loader from '@/components/common/Loader'; -import PageContainer from '@/components/common/Page'; -import { useModal } from '@/contexts/ModalContext'; -import useCheckInputValid from '@/hooks/useCheckInputValid'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import api from '../../../../../../api'; +import AlertModal from '../../../../../../components/common/AlertModal'; +import Button from '../../../../../../components/common/Button'; +import Loader from '../../../../../../components/common/Loader'; +import PageContainer from '../../../../../../components/common/Page'; +import { useModal } from '../../../../../../contexts/ModalContext'; +import useCheckInputValid from '../../../../../../hooks/useCheckInputValid'; function ArticleEditPage() { const router = useRouter(); - const params = useParams(); + const params: { + articleId: string; + } = useParams(); const articleId = params.articleId; - const [isBtnActive, setIsBtnActive] = useState(true); + const [isBtnActive, setIsBtnActive] = useState(true); const modal = useModal(); const queryClient = useQueryClient(); @@ -23,7 +25,7 @@ function ArticleEditPage() { const { inputValue: inputTitle, isValid: isValidTitle, - isBeforeTouch: isBeforeTouchTitle, + // isBeforeTouch: isBeforeTouchTitle, setInputValue: setInputTitle, handleBlur: handleTitleBlur, handleChange: handleTitleChange, @@ -31,7 +33,7 @@ function ArticleEditPage() { const { inputValue: inputContent, isValid: isValidContent, - isBeforeTouch: isBeforeTouchContent, + // isBeforeTouch: isBeforeTouchContent, setInputValue: setInputContent, handleBlur: handleContentBlur, handleChange: handleContentChange, @@ -63,7 +65,7 @@ function ArticleEditPage() { }, }); - const handleRegistClick = async () => { + const handleRegistClick = () => { if (!isBtnActive) return; setIsBtnActive(false); editArticle(); @@ -73,7 +75,7 @@ function ArticleEditPage() { if (!article) return; setInputContent(article.content); setInputTitle(article.title); - }, []); + }, [article, setInputContent, setInputTitle]); useEffect(() => { /** diff --git a/app/(providers)/(root)/articles/post/page.jsx b/app/(providers)/(root)/articles/post/page.tsx similarity index 85% rename from app/(providers)/(root)/articles/post/page.jsx rename to app/(providers)/(root)/articles/post/page.tsx index 8d8b54d..52f81cc 100644 --- a/app/(providers)/(root)/articles/post/page.jsx +++ b/app/(providers)/(root)/articles/post/page.tsx @@ -1,20 +1,20 @@ 'use client'; -import api from '@/api'; -import AlertModal from '@/components/common/AlertModal'; -import Button from '@/components/common/Button'; -import Loader from '@/components/common/Loader'; -import PageContainer from '@/components/common/Page'; -import { useAuth } from '@/contexts/AuthContext'; -import { useModal } from '@/contexts/ModalContext'; -import useCheckInputValid from '@/hooks/useCheckInputValid'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import api from '../../../../../api'; +import AlertModal from '../../../../../components/common/AlertModal'; +import Button from '../../../../../components/common/Button'; +import Loader from '../../../../../components/common/Loader'; +import PageContainer from '../../../../../components/common/Page'; +import { useAuth } from '../../../../../contexts/AuthContext'; +import { useModal } from '../../../../../contexts/ModalContext'; +import useCheckInputValid from '../../../../../hooks/useCheckInputValid'; function ArticlePostPage() { - const [isBtnActive, setIsBtnActive] = useState(false); - const { isLoggedIn, isAuthInitialized, logOut, userInfo } = useAuth(); + const [isBtnActive, setIsBtnActive] = useState(false); + const { userInfo } = useAuth(); const router = useRouter(); const queryClient = useQueryClient(); @@ -57,7 +57,7 @@ function ArticlePostPage() { }, }); - const handleRegistClick = async () => { + const handleRegistClick = () => { if (!isBtnActive) return; setIsBtnActive(false); createArticle(); diff --git a/app/(providers)/(root)/auth/log-in/page.jsx b/app/(providers)/(root)/auth/log-in/page.tsx similarity index 87% rename from app/(providers)/(root)/auth/log-in/page.jsx rename to app/(providers)/(root)/auth/log-in/page.tsx index df3fbb1..e8f8c8f 100644 --- a/app/(providers)/(root)/auth/log-in/page.jsx +++ b/app/(providers)/(root)/auth/log-in/page.tsx @@ -10,7 +10,9 @@ import Loader from '@/components/common/Loader'; import PageContainer from '@/components/common/Page'; import { useAuth } from '@/contexts/AuthContext'; import { useModal } from '@/contexts/ModalContext'; +import { UserLoginDto } from '@/types/dtos/user.dto'; import { useMutation } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; import dynamic from 'next/dynamic'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; @@ -24,7 +26,7 @@ const DevT = dynamic( { ssr: false } ); function LogInPage() { - const [isShowPassword, setIsShowPassword] = useState(false); + const [isShowPassword, setIsShowPassword] = useState(false); const { logIn: authLogin } = useAuth(); const router = useRouter(); const modal = useModal(); @@ -44,21 +46,25 @@ function LogInPage() { }); // mode: onBlur, onChange, onSubmit(default) const { mutate: logIn, isPending } = useMutation({ - mutationFn: (userData) => api.logIn(userData), + mutationFn: (userData: UserLoginDto) => api.logIn(userData), onSuccess: () => { router.push('/products'); authLogin(); }, onError: (error) => { - const errorMessage = error.response.data; - if (errorMessage === 'No user founded') { - modal.open(); - setError('email', { message: '이메일을 확인해 주세요' }); - } else if (errorMessage === 'Wrong password') { - modal.open( - - ); - setError('password', { message: '비밀번호를 확인해 주세요' }); + if (error instanceof AxiosError) { + const errorMessage = error.response.data; + if (errorMessage === 'No user founded') { + modal.open( + + ); + setError('email', { message: '이메일을 확인해 주세요' }); + } else if (errorMessage === 'Wrong password') { + modal.open( + + ); + setError('password', { message: '비밀번호를 확인해 주세요' }); + } } else { modal.open( @@ -67,7 +73,7 @@ function LogInPage() { }, }); - const onSubmit = (data) => { + const onSubmit = (data: UserLoginDto) => { logIn(data); }; @@ -164,7 +170,7 @@ function LogInPage() { - + ); diff --git a/app/(providers)/(root)/auth/sign-up/page.jsx b/app/(providers)/(root)/auth/sign-up/page.tsx similarity index 90% rename from app/(providers)/(root)/auth/sign-up/page.jsx rename to app/(providers)/(root)/auth/sign-up/page.tsx index 575767a..0737f6a 100644 --- a/app/(providers)/(root)/auth/sign-up/page.jsx +++ b/app/(providers)/(root)/auth/sign-up/page.tsx @@ -10,7 +10,9 @@ import Loader from '@/components/common/Loader'; import PageContainer from '@/components/common/Page'; import { useAuth } from '@/contexts/AuthContext'; import { useModal } from '@/contexts/ModalContext'; +import { UserSignUpDto } from '@/types/dtos/user.dto'; import { useMutation } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; import dynamic from 'next/dynamic'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; @@ -25,9 +27,9 @@ const DevT = dynamic( ); function SignUpPage() { const { logIn } = useAuth(); - const [isShowPassword, setIsShowPassword] = useState(false); + const [isShowPassword, setIsShowPassword] = useState(false); const [isShowPasswordConfirmation, setIsShowPasswordConfirmation] = - useState(false); + useState(false); const router = useRouter(); const modal = useModal(); @@ -55,7 +57,7 @@ function SignUpPage() { }; const { mutate: signUp, isPending } = useMutation({ - mutationFn: (userData) => api.signUp(userData), + mutationFn: (userData: UserSignUpDto) => api.signUp(userData), onSuccess: () => { modal.open( { - const errorMessage = error.response.data; - if (errorMessage === 'Already used email') { - modal.open(); - setError('email', { message: '이메일을 확인해 주세요' }); - } else if (errorMessage === '이미 사용중인 닉네임입니다.') { - modal.open(); - setError('nickname', { message: '닉네임을 확인해 주세요' }); + if (error instanceof AxiosError) { + const errorMessage = error.response.data; + if (errorMessage === 'Already used email') { + modal.open( + + ); + setError('email', { message: '이메일을 확인해 주세요' }); + } else if (errorMessage === '이미 사용중인 닉네임입니다.') { + modal.open( + + ); + setError('nickname', { message: '닉네임을 확인해 주세요' }); + } } else { modal.open( @@ -80,7 +88,7 @@ function SignUpPage() { }, }); - const onSubmit = (inputData) => { + const onSubmit = (inputData: UserSignUpDto) => { signUp(inputData); }; @@ -236,7 +244,7 @@ function SignUpPage() { - + ); diff --git a/app/(providers)/(root)/error.jsx b/app/(providers)/(root)/error.tsx similarity index 100% rename from app/(providers)/(root)/error.jsx rename to app/(providers)/(root)/error.tsx diff --git a/app/(providers)/(root)/faq/page.jsx b/app/(providers)/(root)/faq/page.tsx similarity index 100% rename from app/(providers)/(root)/faq/page.jsx rename to app/(providers)/(root)/faq/page.tsx diff --git a/app/(providers)/(root)/layout.jsx b/app/(providers)/(root)/layout.tsx similarity index 100% rename from app/(providers)/(root)/layout.jsx rename to app/(providers)/(root)/layout.tsx diff --git a/app/(providers)/(root)/page.jsx b/app/(providers)/(root)/page.tsx similarity index 100% rename from app/(providers)/(root)/page.jsx rename to app/(providers)/(root)/page.tsx diff --git a/app/(providers)/(root)/privacy/page.jsx b/app/(providers)/(root)/privacy/page.tsx similarity index 100% rename from app/(providers)/(root)/privacy/page.jsx rename to app/(providers)/(root)/privacy/page.tsx diff --git a/app/(providers)/(root)/products/[productId]/loading.jsx b/app/(providers)/(root)/products/[productId]/loading.tsx similarity index 100% rename from app/(providers)/(root)/products/[productId]/loading.jsx rename to app/(providers)/(root)/products/[productId]/loading.tsx diff --git a/app/(providers)/(root)/products/[productId]/page.jsx b/app/(providers)/(root)/products/[productId]/page.tsx similarity index 87% rename from app/(providers)/(root)/products/[productId]/page.jsx rename to app/(providers)/(root)/products/[productId]/page.tsx index 7269715..668f8f2 100644 --- a/app/(providers)/(root)/products/[productId]/page.jsx +++ b/app/(providers)/(root)/products/[productId]/page.tsx @@ -1,5 +1,5 @@ import api from '@/api'; -import Comments from '@/components/articles/Comments'; +import CommentList from '@/components/articles/CommentList'; import PageContainer from '@/components/common/Page'; import ProductDetail from '@/components/product/ProductDetail'; import { @@ -20,7 +20,7 @@ async function ProductDetailPage({ params }) { - + ); diff --git a/app/(providers)/(root)/products/loading.jsx b/app/(providers)/(root)/products/loading.tsx similarity index 100% rename from app/(providers)/(root)/products/loading.jsx rename to app/(providers)/(root)/products/loading.tsx diff --git a/app/(providers)/(root)/products/page.jsx b/app/(providers)/(root)/products/page.tsx similarity index 100% rename from app/(providers)/(root)/products/page.jsx rename to app/(providers)/(root)/products/page.tsx diff --git a/app/(providers)/(root)/products/post/[productId]/page.jsx b/app/(providers)/(root)/products/post/[productId]/page.tsx similarity index 84% rename from app/(providers)/(root)/products/post/[productId]/page.jsx rename to app/(providers)/(root)/products/post/[productId]/page.tsx index 0569312..1f47dbd 100644 --- a/app/(providers)/(root)/products/post/[productId]/page.jsx +++ b/app/(providers)/(root)/products/post/[productId]/page.tsx @@ -1,23 +1,35 @@ 'use client'; -import api from '@/api'; import icX from '@/assets/images/ic-x.png'; import icPlus from '@/assets/images/ic_plus.png'; -import AlertModal from '@/components/common/AlertModal'; -import Button from '@/components/common/Button'; -import Loader from '@/components/common/Loader'; -import PageContainer from '@/components/common/Page'; -import TagChip from '@/components/common/TagChip'; -import { useAuth } from '@/contexts/AuthContext'; -import { useModal } from '@/contexts/ModalContext'; +import { ProductPostDto } from '@/types/dtos/product.dto'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import Image from 'next/image'; import { useParams, useRouter } from 'next/navigation'; -import { useRef, useState } from 'react'; +import { + ChangeEventHandler, + KeyboardEventHandler, + useRef, + useState, +} from 'react'; import { useForm } from 'react-hook-form'; +import api from '../../../../../../api'; +import AlertModal from '../../../../../../components/common/AlertModal'; +import Button from '../../../../../../components/common/Button'; +import Loader from '../../../../../../components/common/Loader'; +import PageContainer from '../../../../../../components/common/Page'; +import TagChip from '../../../../../../components/common/TagChip'; +import { useAuth } from '../../../../../../contexts/AuthContext'; +import { useModal } from '../../../../../../contexts/ModalContext'; + +export interface InputData { + name: string; + description: string; + price: number; +} function ProductEditPage() { - const params = useParams(); + const params: { productId: string } = useParams(); const productId = params.productId; const queryClient = useQueryClient(); @@ -40,22 +52,24 @@ function ProductEditPage() { description: product.description, price: product.price, tag: '', + images: [], }, }); const modal = useModal(); const { isLoggedIn, userInfo } = useAuth(); - const [tags, setTags] = useState(product.tags); + const [tags, setTags] = useState(product.tags); const router = useRouter(); - const fileInputRef = useRef(); - const [pickedImages, setPickedImages] = useState([]); - const [imgUrls, setImgUrls] = useState(product.imgUrls); + const fileInputRef = useRef(null); + const [pickedImages, setPickedImages] = useState([]); + const [imgUrls, setImgUrls] = useState(product.imgUrls); const isTagsNotEmpty = tags.length !== 0; const pickedImagesNotEmpty = pickedImages.length !== 0; const isPossibleRegist = isValid && isTagsNotEmpty && pickedImagesNotEmpty; const { mutate: editProduct, isPending } = useMutation({ - mutationFn: (dto) => api.editProduct(productId, dto), + mutationFn: (formData: ProductPostDto) => + api.editProduct(productId, formData), onSuccess: () => { function handleClickSuccess() { router.replace(`/products`); @@ -94,10 +108,10 @@ function ProductEditPage() { checkIsLoggedIn(); }; - const onSubmit = (dto) => { + const onSubmit = (data: InputData) => { if (!isLoggedIn || !isPossibleRegist) return; - const { name, description, price } = dto; - const reqData = { + const { name, description, price } = data; + const formData: ProductPostDto = { name, description, tags, @@ -105,19 +119,21 @@ function ProductEditPage() { price: Number(price), images: pickedImages, }; - editProduct(reqData); + editProduct(formData); }; - const handleClickDeleteTag = (index) => { + const handleClickDeleteTag = (index: number) => { setTags((prevTags) => [ ...prevTags.slice(0, index), ...prevTags.slice(index + 1), ]); }; - const handleChangeImages = (e) => { - if (!e.target.files) rerturn; - const fileList = e.target.files; + const handleChangeImages: ChangeEventHandler = (e) => { + const { files } = e.target as HTMLInputElement; + let { value } = e.target as HTMLInputElement; + if (!files) return; + const fileList = files; const fileArray = Array.from(fileList); // iterable을 array로 변경(map method를 쓰기 위해) if (fileArray.length > 3) @@ -127,14 +143,14 @@ function ProductEditPage() { setPickedImages(fileArray); const pickedImgUrls = fileArray.map((file) => URL.createObjectURL(file)); setImgUrls(pickedImgUrls); - e.target.value = ''; + value = ''; }; const handleClickAddImageButton = () => { fileInputRef.current.click(); }; - const handleClickDeleteImageButton = (idx) => { + const handleClickDeleteImageButton = (idx: number) => { if (imgUrls.length > 1) { setImgUrls(imgUrls.filter((_, index) => index !== idx)); } else { @@ -142,18 +158,19 @@ function ProductEditPage() { } }; - const handleTagEnter = (e) => { + const handleTagEnter: KeyboardEventHandler = (e) => { + const { value } = e.target as HTMLInputElement; if ( - e.target.value && // 빈값이 아니고 + value && // 빈값이 아니고 e.key === 'Enter' && e.nativeEvent.isComposing === false // 한글 입력 시의 문제를 해결하기 위해 추가 ) { e.preventDefault(); - const isTagValid = e.target.value.length <= 5; + const isTagValid = value.length <= 5; if (!isTagValid) { return setError('tag', { message: '5글자 이내로 입력해주세요' }); } - setTags((prevTags) => [...prevTags, e.target.value]); // 태그에 추가 + setTags((prevTags) => [...prevTags, value]); // 태그에 추가 /** * 입력된 tag 초기화 @@ -167,7 +184,7 @@ function ProductEditPage() { // // /products/post로 접근 시 로그인 여부 체크 // checkIsLoggedIn(); // }, [isLoggedIn]); - console.log(imgUrls > 0); + console.log(imgUrls.length > 0); return ( @@ -249,7 +266,7 @@ function ProductEditPage() { /> {!isValid && ( - {errors.name?.message} + {errors.name?.message.toString()} )} @@ -260,7 +277,6 @@ function ProductEditPage() {