From 91b3af9c0cb11b22d6e5497b69b2a86902fc6e05 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Wed, 12 Jun 2024 17:29:30 +0900 Subject: [PATCH 01/17] =?UTF-8?q?Fix:=20=EB=B0=B0=EC=97=B4=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=ED=82=A4=EA=B0=92=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/containers/boards-detail/Comment/Comment.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/containers/boards-detail/Comment/Comment.tsx b/src/containers/boards-detail/Comment/Comment.tsx index 9507e72e3..faa79c8e9 100644 --- a/src/containers/boards-detail/Comment/Comment.tsx +++ b/src/containers/boards-detail/Comment/Comment.tsx @@ -10,6 +10,7 @@ import kebabIcon from "@/public/svgs/kebab.svg"; import defaultProfileIcon from "@/public/svgs/default-profile.svg"; import commentEmptyIcon from "@/public/svgs/comment-empty.svg"; import backIcon from "@/public/svgs/back-page.svg"; +import { Fragment } from "react"; export default function Comment() { const router = useRouter(); @@ -51,7 +52,7 @@ export default function Comment() { return (
{comments?.list?.map((comment) => ( - <> +
{comment.content}
케밥 아이콘 @@ -73,7 +74,7 @@ export default function Comment() {

- +
))}
From 0ea15a11035e3142ba81efeaab4e165b423b4937 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Wed, 12 Jun 2024 17:31:06 +0900 Subject: [PATCH 02/17] =?UTF-8?q?Chore:=20classnames=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 16 ++++++++++++++++ package.json | 2 ++ 2 files changed, 18 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2d3982bcb..a41289128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "fe-weekly-mission", "version": "0.1.0", "dependencies": { + "@types/classnames": "^2.3.1", "axios": "^1.7.2", + "classnames": "^2.5.1", "next": "13.5.6", "react": "^18", "react-dom": "^18", @@ -331,6 +333,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", + "deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.", + "dependencies": { + "classnames": "*" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -905,6 +916,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", diff --git a/package.json b/package.json index 263d3d119..870113eb1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "lint": "next lint" }, "dependencies": { + "@types/classnames": "^2.3.1", "axios": "^1.7.2", + "classnames": "^2.5.1", "next": "13.5.6", "react": "^18", "react-dom": "^18", From a434f0a00cad8ab6af8cadc7ea501c73f03c9b10 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Wed, 12 Jun 2024 17:37:13 +0900 Subject: [PATCH 03/17] =?UTF-8?q?Fix:=20classnames=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20/=20=EC=A1=B0=EA=B1=B4=EB=B6=80?= =?UTF-8?q?=EB=A1=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boards-detail/AddComment/AddComment.module.scss | 11 ++++++++--- .../boards-detail/AddComment/AddComment.tsx | 6 ++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/containers/boards-detail/AddComment/AddComment.module.scss b/src/containers/boards-detail/AddComment/AddComment.module.scss index 8f8997e82..bc425f46a 100644 --- a/src/containers/boards-detail/AddComment/AddComment.module.scss +++ b/src/containers/boards-detail/AddComment/AddComment.module.scss @@ -39,7 +39,8 @@ } } -.submitButton { +.submitButton, +.disabledButton { @include font-style(16px, 600, $white); padding: 12px 24px; background-color: $blue; @@ -52,9 +53,13 @@ &:hover { background-color: $dark-blue; } +} + +.disabledButton { + background-color: $grey-400; + cursor: not-allowed; - &:disabled { + &:hover { background-color: $grey-400; - cursor: not-allowed; } } diff --git a/src/containers/boards-detail/AddComment/AddComment.tsx b/src/containers/boards-detail/AddComment/AddComment.tsx index 751156628..2cd4fd7e3 100644 --- a/src/containers/boards-detail/AddComment/AddComment.tsx +++ b/src/containers/boards-detail/AddComment/AddComment.tsx @@ -4,6 +4,7 @@ import axios from "@/src/lib/axios"; import Spinner from "@/src/components/Spinner/Spinner"; import styles from "./AddComment.module.scss"; +import classnames from "classnames"; export default function AddComment() { const router = useRouter(); @@ -47,9 +48,10 @@ export default function AddComment() { ) : ( From 619bc1df17a74b11dd0e0af265b38bc68723cdca Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Wed, 12 Jun 2024 22:24:56 +0900 Subject: [PATCH 04/17] =?UTF-8?q?Refactor:=20=EA=B2=80=EC=83=89=EC=B0=BD?= =?UTF-8?q?=20=EB=94=94=EB=B0=94=EC=9A=B4=EC=8B=B1=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20=EC=BB=A4=EC=8A=A4=ED=85=80=ED=9B=85=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SearchBar/SearchBar.tsx | 19 ++++--------------- src/hooks/useDebounce.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 src/hooks/useDebounce.ts diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 725d9c68f..78244bc57 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -1,28 +1,17 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import Image from "next/image"; import styles from "./SearchBar.module.scss"; import searchIcon from "@/public/svgs/search.svg"; +import useDebounce from "@/src/hooks/useDebounce"; interface SearchBarProps { keyword: (searchTerm: string) => void; } const SearchBar: React.FC = ({ keyword }) => { - const [searchTerm, setSearchTerm] = useState(""); - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); + const debouncedSearchTerm = useDebounce(searchTerm, 2000); - // 디바운싱을 위한 useEffect - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm); - }, 2000); // 2초 딜레이가 지난 후 조회 API 호출 - - return () => { - clearTimeout(timer); - }; - }, [searchTerm]); - - // 디바운싱된 검색어가 변경될 때 keyword 함수 호출 useEffect(() => { if (debouncedSearchTerm) { keyword(debouncedSearchTerm); diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 000000000..97eb0cc0e --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from "react"; + +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} + +export default useDebounce; From e52d5e42b2b63a0d27095461810d14731f275b76 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 01:02:54 +0900 Subject: [PATCH 05/17] =?UTF-8?q?Chore:=20react-hook-from=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 | 31 ++++++++++++++++++++----------- package.json | 3 ++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index a41289128..7c2d72439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,12 @@ "next": "13.5.6", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.5", "sass": "^1.77.2" }, "devDependencies": { "@types/node": "^20", - "@types/react": "^18", + "@types/react": "^18.3.3", "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "13.5.6", @@ -364,13 +365,12 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.38", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.38.tgz", - "integrity": "sha512-cBBXHzuPtQK6wNthuVMV6IjHAFkdl/FOPFIlkd81/Cd1+IqkHu/A+w4g43kaQQoYHik/ruaQBDL72HyCy1vuMw==", + "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" } }, @@ -383,12 +383,6 @@ "@types/react": "*" } }, - "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/@typescript-eslint/parser": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", @@ -3136,6 +3130,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz", + "integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 870113eb1..eb1e55a4e 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,12 @@ "next": "13.5.6", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.5", "sass": "^1.77.2" }, "devDependencies": { "@types/node": "^20", - "@types/react": "^18", + "@types/react": "^18.3.3", "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "13.5.6", From 5a4ff756167d84a0c3bf8983d56d7472a5f87cf5 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:42:19 +0900 Subject: [PATCH 06/17] =?UTF-8?q?Chore:=20react=20toastify=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 | 21 +++++++++++++++++++++ package.json | 1 + 2 files changed, 22 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7c2d72439..69d63a3b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.5", + "react-toastify": "^10.0.5", "sass": "^1.77.2" }, "devDependencies": { @@ -920,6 +921,14 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3151,6 +3160,18 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/package.json b/package.json index eb1e55a4e..6314ec554 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.5", + "react-toastify": "^10.0.5", "sass": "^1.77.2" }, "devDependencies": { From 1f7b3f4e15c19d70929ae5cdc7b2cac597d1d34f Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:43:25 +0900 Subject: [PATCH 07/17] =?UTF-8?q?Feat:=20width,=20height,=20borderRadius,?= =?UTF-8?q?=20fontSize,=20disabled=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Button/Button.module.scss | 22 ++++++++++++ src/components/Button/Button.tsx | 44 ++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/components/Button/Button.module.scss b/src/components/Button/Button.module.scss index b8a29d721..c09ffdd09 100644 --- a/src/components/Button/Button.module.scss +++ b/src/components/Button/Button.module.scss @@ -15,3 +15,25 @@ background-color: $dark-blue; } } + +.active { + background-color: $grey-400; + cursor: not-allowed; + + &:hover { + background-color: $grey-400; + } +} + +.fullWidth { + width: 100%; +} + +.disabled { + background-color: $grey-400; + cursor: not-allowed; + + &:hover { + background-color: $grey-400; + } +} diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index df7cbdda2..d8ad4c8d9 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,14 +1,54 @@ import React, { ReactNode } from "react"; +import classNames from "classnames"; import styles from "./Button.module.scss"; interface ButtonProps { onClick?: () => void; children: ReactNode; + fullWidth?: boolean; + disabled?: boolean; // 추가: disabled 상태 여부를 받아오는 prop + width?: number; + height?: number; + radius?: number; + fontSize?: number; } -const Button: React.FC = ({ onClick, children }) => { +const Button: React.FC = ({ + onClick, + children, + fullWidth, + disabled, // 추가: disabled prop을 받아옴 + width, + height, + radius, + fontSize, +}) => { + const buttonClassName = classNames(styles.button, { + [styles.fullWidth]: fullWidth, + [styles.disabled]: disabled, + }); + + const buttonStyle: React.CSSProperties = {}; + if (width) { + buttonStyle.width = width; + } + if (height) { + buttonStyle.height = height; + } + if (radius) { + buttonStyle.borderRadius = radius; + } + if (fontSize) { + buttonStyle.fontSize = fontSize; + } + return ( - ); From edbbb2573ac87f297fe2c257cd6c4308d5bc4bcd Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:44:01 +0900 Subject: [PATCH 08/17] =?UTF-8?q?Fix:=20auth/=20=EC=9D=B4=ED=95=98=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EC=97=90=EC=84=9C=EB=8A=94=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=EA=B0=80=20=EC=95=88=EB=B3=B4=EC=9D=B4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Layout/Layout.module.scss | 13 +++++++++--- src/components/Layout/Layout.tsx | 27 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/components/Layout/Layout.module.scss b/src/components/Layout/Layout.module.scss index 17f86f9d5..2bb25081e 100644 --- a/src/components/Layout/Layout.module.scss +++ b/src/components/Layout/Layout.module.scss @@ -1,6 +1,5 @@ .container { width: 100%; - padding: 0 360px; display: flex; flex-direction: column; align-items: center; @@ -9,18 +8,26 @@ .page.container { margin-top: 100px; + padding: 0 360px; +} + +.authPage.container { + margin-top: 60px; + padding: 0 640px; } /* mobile */ @media (max-width: 768px) { - .container { + .page.container, + .authPage.container { padding: 0 16px; } } /* tablet */ @media (max-width: 1280px) { - .container { + .page.container, + .authPage.container { padding: 0 24px; } } diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index d882fcda2..8577d5de0 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,8 +1,23 @@ +import { useRouter } from "next/router"; +import classNames from "classnames"; import styles from "./Layout.module.scss"; -export default function Layout({ className = "", page = true, ...props }) { - const classNames = `${styles.container} ${ - page ? styles.page : "" - } ${className}`; - return
; -} +const Layout = ({ + className = "", + ...props +}: { + className?: string; + children: React.ReactNode; +}) => { + const router = useRouter(); + const isAuthPage = router.pathname.startsWith("/auth"); + const pageClassName = classNames(styles.container, { + [styles.authPage]: isAuthPage, + [styles.page]: !isAuthPage, + [className]: className, + }); + + return
; +}; + +export default Layout; From 1e0127eac887ba3f0552e920449035ac8a432444 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:45:55 +0900 Subject: [PATCH 09/17] =?UTF-8?q?Fix:=20auth/=20=EC=9D=B4=ED=95=98=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EC=97=90=EC=84=A0=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=EA=B0=80=20=EC=95=88=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header/Header.module.scss | 29 +++++++++++++++++++++ src/components/Header/Header.tsx | 32 +++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss index 238b60fb1..5d6c3af9c 100644 --- a/src/components/Header/Header.module.scss +++ b/src/components/Header/Header.module.scss @@ -33,6 +33,35 @@ } } +.headerRight { + display: flex; + gap: 10px; + + .userControl { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 4px; + + .user { + display: inline; + cursor: default; + span { + @include font-style(16px, 800, $grey-800); + } + } + + .signOut { + @include font-style(14px, 400, $grey-500); + cursor: pointer; + + &:hover { + color: $grey-800; + } + } + } +} + /* mobile */ @media (max-width: 768px) { .headerLeft { diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index ccc0fd6e2..ef4e6ec17 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,16 +1,27 @@ import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; + +import { useAuth } from "@/src/contexts/AuthProvider"; import styles from "./Header.module.scss"; import Button from "@/src/components/Button/Button"; import { isMobileDevice } from "@/src/utils/isMobileDevice"; import logoIcon from "@/public/svgs/logo.svg"; import mobileLogoIcon from "@/public/svgs/logo-mobile.svg"; +import defaultProfile from "@/public/svgs/default-profile.svg"; const isMobile = typeof window !== "undefined" && isMobileDevice(); export default function Header() { + const { user, signOut } = useAuth(); const router = useRouter(); + + const isAuthPath = router.pathname.startsWith("/auth"); + + if (isAuthPath) { + return null; // /auth 이하 경로에서는 Header 안보이게 + } + const setCurrentPage = (path: string) => { return { color: @@ -41,7 +52,26 @@ export default function Header() {
- + {user ? ( +
+ 기본 프로필 이미지 +
+
+ {user.nickname}님 +
+
+ 로그아웃 +
+
+
+ ) : ( + + )} ); } From d85404afb7a8285f77ad999d91bd3f27c5e200fc Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:46:59 +0900 Subject: [PATCH 10/17] =?UTF-8?q?Feat:=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/_app.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 000eaf475..1286a5fae 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,9 @@ import "@/src/styles/global.scss"; import type { AppProps } from "next/app"; import Head from "next/head"; +import { AuthProvider } from "@/src/contexts/AuthProvider"; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; import Layout from "@/src/components/Layout/Layout"; import Header from "@/src/components/Header/Header"; @@ -14,10 +17,13 @@ export default function App({ Component, pageProps }: AppProps) { -
- - - + +
+ + + + + ); } From 1d0613635d377c8dce6c2ffd2203efffa35c2631 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:47:25 +0900 Subject: [PATCH 11/17] =?UTF-8?q?Feat:=20=ED=86=A0=ED=81=B0=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - accessToken을 로컬스토리지에 저장 - refreshToken을 통해 accessToken 재발급 요청 --- src/lib/axios.ts | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/lib/axios.ts b/src/lib/axios.ts index ff742c75d..f68ba9966 100644 --- a/src/lib/axios.ts +++ b/src/lib/axios.ts @@ -1,7 +1,48 @@ -import axios from "axios"; +import axios, { AxiosError } from "axios"; const instance = axios.create({ - baseURL: " https://panda-market-api.vercel.app", + baseURL: "https://panda-market-api.vercel.app", }); +let isRefreshing = false; +let refreshPromise: Promise | null = null; + +instance.interceptors.request.use((config) => { + const token = localStorage.getItem("accessToken"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +instance.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config; + if (error.response?.status === 401 && !isRefreshing) { + isRefreshing = true; + + if (!refreshPromise) { + refreshPromise = instance.post("/auth/refresh-token"); + + try { + const { data } = await refreshPromise; + localStorage.setItem("accessToken", data.accessToken); + originalRequest!.headers.Authorization = `Bearer ${data.accessToken}`; + isRefreshing = false; + refreshPromise = null; + return instance(originalRequest!); // 재요청 + } catch (refreshError) { + console.error("Refresh token request failed:", refreshError); + isRefreshing = false; + refreshPromise = null; + return Promise.reject(refreshError); + } + } + return refreshPromise.then(() => instance(originalRequest!)); + } + return Promise.reject(error); + } +); + export default instance; From 936f5c9a5c0a264c893d8acab910b2f633fba80a Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:49:31 +0900 Subject: [PATCH 12/17] =?UTF-8?q?Feat:=20User=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=A0=95=EC=9D=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interfaces/User.interface.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/interfaces/User.interface.ts diff --git a/src/interfaces/User.interface.ts b/src/interfaces/User.interface.ts new file mode 100644 index 000000000..969005677 --- /dev/null +++ b/src/interfaces/User.interface.ts @@ -0,0 +1,4 @@ +export default interface User { + id: number; + nickname: string; +} From 70a457bf608ffcf5f7d762d03a57f260ec57eea5 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:51:29 +0900 Subject: [PATCH 13/17] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/containers/auth/SignInForm/SignInForm.tsx | 87 +++++++++++++++++++ src/interfaces/SignInForm.interface.ts | 4 + src/pages/auth/signin.tsx | 9 ++ 3 files changed, 100 insertions(+) create mode 100644 src/containers/auth/SignInForm/SignInForm.tsx create mode 100644 src/interfaces/SignInForm.interface.ts create mode 100644 src/pages/auth/signin.tsx diff --git a/src/containers/auth/SignInForm/SignInForm.tsx b/src/containers/auth/SignInForm/SignInForm.tsx new file mode 100644 index 000000000..de0d38f71 --- /dev/null +++ b/src/containers/auth/SignInForm/SignInForm.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import { useForm, SubmitHandler } from "react-hook-form"; +import Image from "next/image"; +import Link from "next/link"; + +import { useAuth } from "@/src/contexts/AuthProvider"; +import styles from "../SignInUpForm.module.scss"; +import logoIcon from "@/public/svgs/logo.svg"; +import Button from "@/src/components/Button/Button"; + +interface SignInFormValues { + email: string; + password: string; +} + +const SignInForm: React.FC = () => { + const { signIn } = useAuth(); + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + mode: "onChange", // 입력 값이 변경될 때마다 유효성 검사를 수행 + }); + + const onSubmit: SubmitHandler = async (data) => { + try { + await signIn(data); + } catch (error) { + console.error(error); + } + }; + + return ( + <> + + 판다마켓 로고 + + +
+
+
+ + + {errors.email && ( + 이메일을 입력하세요. + )} +
+
+ + + {errors.password && ( + + 비밀번호를 입력하세요. + + )} +
+ +
+ +
+ 판다마켓이 처음이신가요?{" "} + + 회원가입 + +
+
+ + ); +}; + +export default SignInForm; diff --git a/src/interfaces/SignInForm.interface.ts b/src/interfaces/SignInForm.interface.ts new file mode 100644 index 000000000..0c1089b70 --- /dev/null +++ b/src/interfaces/SignInForm.interface.ts @@ -0,0 +1,4 @@ +export default interface SignInForm { + email: string; + password: string; +} diff --git a/src/pages/auth/signin.tsx b/src/pages/auth/signin.tsx new file mode 100644 index 000000000..ae8f963e8 --- /dev/null +++ b/src/pages/auth/signin.tsx @@ -0,0 +1,9 @@ +import SignInForm from "@/src/containers/auth/SignInForm/SignInForm"; + +export default function SignInPage() { + return ( + <> + + + ); +} From d8ca1adff691b22c3c04c32ca6cee727e085f322 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:52:19 +0900 Subject: [PATCH 14/17] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=8F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/containers/auth/SignUpForm/SignUpForm.tsx | 125 ++++++++++++++++++ src/interfaces/SignUpForm.interface.ts | 6 + src/pages/auth/signup.tsx | 9 ++ 3 files changed, 140 insertions(+) create mode 100644 src/containers/auth/SignUpForm/SignUpForm.tsx create mode 100644 src/interfaces/SignUpForm.interface.ts create mode 100644 src/pages/auth/signup.tsx diff --git a/src/containers/auth/SignUpForm/SignUpForm.tsx b/src/containers/auth/SignUpForm/SignUpForm.tsx new file mode 100644 index 000000000..6078834c2 --- /dev/null +++ b/src/containers/auth/SignUpForm/SignUpForm.tsx @@ -0,0 +1,125 @@ +import React from "react"; +import { useRouter } from "next/router"; +import Image from "next/image"; +import Link from "next/link"; +import { useForm, SubmitHandler } from "react-hook-form"; +import SignUpFormInterface from "@/src/interfaces/SignUpForm.interface"; +import axios from "@/src/lib/axios"; +import { toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +import styles from "../SignInUpForm.module.scss"; +import Button from "@/src/components/Button/Button"; +import logoIcon from "@/public/svgs/logo.svg"; + +const SignUpForm: React.FC = () => { + const router = useRouter(); + const { + register, + handleSubmit, + watch, + formState: { errors, isValid }, + } = useForm(); + + const onSubmit: SubmitHandler = async (data) => { + try { + await axios.post("/auth/signUp", data); + toast.success("회원가입이 완료되었습니다."); + router.push("/auth/signin"); // 회원가입 성공하면 자동으로 로그인 페이지로 이동 + } catch (error) { + console.error(error); + } + }; + + const password = watch("password", ""); + + return ( + <> + + 판다마켓 로고 + +
+
+
+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+
+ + + {errors.nickname && ( +

{errors.nickname.message}

+ )} +
+
+ + + {errors.password && ( +

{errors.password.message}

+ )} +
+
+ + + value === password || "비밀번호가 일치하지 않습니다.", + })} + placeholder="비밀번호를 다시 한 번 입력해주세요" + /> + {errors.passwordConfirmation && ( +

+ {errors.passwordConfirmation.message} +

+ )} +
+ +
+ +
+ 이미 회원이신가요?{" "} + + 로그인 + +
+
+ + ); +}; + +export default SignUpForm; diff --git a/src/interfaces/SignUpForm.interface.ts b/src/interfaces/SignUpForm.interface.ts new file mode 100644 index 000000000..d6dacd442 --- /dev/null +++ b/src/interfaces/SignUpForm.interface.ts @@ -0,0 +1,6 @@ +export default interface SignUpForm { + email: string; + nickname: string; + password: string; + passwordConfirmation: string; +} diff --git a/src/pages/auth/signup.tsx b/src/pages/auth/signup.tsx new file mode 100644 index 000000000..09cec50c0 --- /dev/null +++ b/src/pages/auth/signup.tsx @@ -0,0 +1,9 @@ +import SignUpForm from "@/src/containers/auth/SignUpForm/SignUpForm"; + +export default function SignUpPage() { + return ( + <> + + + ); +} From dbd5fa63b28953de2e42d27c15afa941e68d4c2c Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:52:39 +0900 Subject: [PATCH 15/17] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8F=BC=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/containers/auth/SignInUpForm.module.scss | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/containers/auth/SignInUpForm.module.scss diff --git a/src/containers/auth/SignInUpForm.module.scss b/src/containers/auth/SignInUpForm.module.scss new file mode 100644 index 000000000..7aa1715e1 --- /dev/null +++ b/src/containers/auth/SignInUpForm.module.scss @@ -0,0 +1,62 @@ +@import "@/src/styles/_mixin"; +@import "@/src/styles/_colors"; + +.container { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; + margin-top: 40px; + align-items: center; +} + +.form { + display: flex; + flex-direction: column; + width: 100%; + + gap: 24px; +} + +.field { + display: flex; + flex-direction: column; + gap: 16px; + + label { + @include font-style(18px, 700, $grey-800); + } + + input { + background-color: $grey-100; + height: 56px; + border-radius: 12px; + padding: 16px 24px; + + &:focus { + outline: none; + } + + &::placeholder { + color: $grey-400; + font-size: 16px; + } + } + + .errorMessage { + @include font-style(15px, 600, $error); + line-height: 18px; + padding-left: 10px; + cursor: default; + } +} + +.goSignUp { + @include font-style(15px, 500, $grey-800); + cursor: default; + + span { + text-decoration: underline; + color: $blue; + } +} From d0ffd2773cbf126f16ab1cb15f1137193ce47395 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 20:53:11 +0900 Subject: [PATCH 16/17] =?UTF-8?q?Feat:=20AuthProvider=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthContext - 로그인, 로그아웃 함수 - useAuth 커스텀 훅 --- src/contexts/AuthProvider.tsx | 98 +++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/contexts/AuthProvider.tsx diff --git a/src/contexts/AuthProvider.tsx b/src/contexts/AuthProvider.tsx new file mode 100644 index 000000000..c9b0950dc --- /dev/null +++ b/src/contexts/AuthProvider.tsx @@ -0,0 +1,98 @@ +import { + createContext, + useState, + ReactNode, + useContext, + useEffect, +} from "react"; +import "react-toastify/dist/ReactToastify.css"; +import { toast } from "react-toastify"; +import { useRouter } from "next/router"; + +import axios from "@/src/lib/axios"; +import User from "@/src/interfaces/User.interface"; + +// 인증 컨텍스트 타입 +interface AuthContextType { + user: User | null; + signIn: (data: { email: string; password: string }) => Promise; + signOut: () => void; +} + +// 초기 상태 +const initialAuthContext: AuthContextType = { + user: null, + signIn: async () => {}, + signOut: () => {}, +}; + +// AuthContext 생성 +const AuthContext = createContext(initialAuthContext); + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider = ({ children }: AuthProviderProps) => { + const router = useRouter(); + const [user, setUser] = useState(null); + + useEffect(() => { + const accessToken = localStorage.getItem("accessToken"); + + if (accessToken) { + const fetchUserData = async () => { + try { + const response = await axios.get("/users/me"); + const user = response.data; + setUser(user); + } catch (error) { + console.error("Error fetching user data:", error); + signOut(); // 사용자 정보를 가져오는데 실패하면 로그아웃 처리 + } + }; + + fetchUserData(); + } + }, []); + + // 로그인 함수 + const signIn = async (data: { email: string; password: string }) => { + try { + const response = await axios.post("/auth/signIn", data); + const { accessToken, refreshToken, user } = response.data; + + localStorage.setItem("accessToken", accessToken); + localStorage.setItem("refreshToken", refreshToken); + setUser(user); + + toast.success("로그인 되었습니다."); + router.push("/boards"); + } catch (error) { + toast.error("로그인에 실패했습니다."); + console.error("Sign-in error:", error); + throw error; // 상위 컴포넌트에서 에러 처리 가능하도록 throw + } + }; + + // 로그아웃 함수 + const signOut = () => { + if (window.confirm("로그아웃 하시겠습니까?")) { + toast.success("로그아웃 되었습니다."); + router.push("/"); + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + + // 사용자 정보 초기화 + setUser(null); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); From d65255e2124c00a0c6faa9ec838c5531687ccbd6 Mon Sep 17 00:00:00 2001 From: Jiyun Kim Date: Fri, 14 Jun 2024 21:14:01 +0900 Subject: [PATCH 17/17] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=AF=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=8F=BC=EC=9E=88=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20/auth=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EC=95=88=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/containers/auth/SignInForm/SignInForm.tsx | 15 ++++++++++++++- src/containers/auth/SignUpForm/SignUpForm.tsx | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/containers/auth/SignInForm/SignInForm.tsx b/src/containers/auth/SignInForm/SignInForm.tsx index de0d38f71..5586768ee 100644 --- a/src/containers/auth/SignInForm/SignInForm.tsx +++ b/src/containers/auth/SignInForm/SignInForm.tsx @@ -1,7 +1,10 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useForm, SubmitHandler } from "react-hook-form"; import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/router"; +import "react-toastify/dist/ReactToastify.css"; +import { toast } from "react-toastify"; import { useAuth } from "@/src/contexts/AuthProvider"; import styles from "../SignInUpForm.module.scss"; @@ -15,6 +18,7 @@ interface SignInFormValues { const SignInForm: React.FC = () => { const { signIn } = useAuth(); + const router = useRouter(); const { register, handleSubmit, @@ -23,6 +27,15 @@ const SignInForm: React.FC = () => { mode: "onChange", // 입력 값이 변경될 때마다 유효성 검사를 수행 }); + // 토큰이 있으면 홈으로 리디렉션 + useEffect(() => { + const token = localStorage.getItem("accessToken"); + if (token) { + toast.success("이미 로그인된 상태입니다."); + router.push("/"); + } + }, [router]); + const onSubmit: SubmitHandler = async (data) => { try { await signIn(data); diff --git a/src/containers/auth/SignUpForm/SignUpForm.tsx b/src/containers/auth/SignUpForm/SignUpForm.tsx index 6078834c2..fe3f59b63 100644 --- a/src/containers/auth/SignUpForm/SignUpForm.tsx +++ b/src/containers/auth/SignUpForm/SignUpForm.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useRouter } from "next/router"; import Image from "next/image"; import Link from "next/link"; @@ -19,7 +19,18 @@ const SignUpForm: React.FC = () => { handleSubmit, watch, formState: { errors, isValid }, - } = useForm(); + } = useForm({ + mode: "onChange", // 입력 값이 변경될 때마다 유효성 검사를 수행 + }); + + // 토큰이 있으면 홈으로 리디렉션 + useEffect(() => { + const token = localStorage.getItem("accessToken"); + if (token) { + toast.success("이미 로그인된 상태입니다."); + router.push("/"); + } + }, [router]); const onSubmit: SubmitHandler = async (data) => { try {