diff --git a/components/Icons/BackIcon.tsx b/components/Icons/BackIcon.tsx new file mode 100644 index 000000000..ddbb5512b --- /dev/null +++ b/components/Icons/BackIcon.tsx @@ -0,0 +1,24 @@ +interface IconProps { + width?: string; + height?: string; +} + +function BackIcon({ width, height }: IconProps) { + return ( + + + + + ); +} + +export default BackIcon; diff --git a/components/Icons/EmptyIcon.tsx b/components/Icons/EmptyIcon.tsx new file mode 100644 index 000000000..27786f822 --- /dev/null +++ b/components/Icons/EmptyIcon.tsx @@ -0,0 +1,21 @@ +interface IconProps { + width?: string; + height?: string; +} + +function EmptyIcon({ width, height }: IconProps) { + return ( + + + + + + ); +} + +export default EmptyIcon; diff --git a/components/Icons/HeartIcon.tsx b/components/Icons/HeartIcon.tsx new file mode 100644 index 000000000..1e93f477e --- /dev/null +++ b/components/Icons/HeartIcon.tsx @@ -0,0 +1,26 @@ +interface IconProps { + width?: string; + height?: string; + isFavorite?: boolean; +} + +function HeartIcon({ width, height, isFavorite = false }: IconProps) { + return ( + + + + ); +} + +export default HeartIcon; diff --git a/components/Icons/InquiryEmptyIcon.tsx b/components/Icons/InquiryEmptyIcon.tsx new file mode 100644 index 000000000..b3762b4b0 --- /dev/null +++ b/components/Icons/InquiryEmptyIcon.tsx @@ -0,0 +1,115 @@ +interface IconProps { + width?: string; + height?: string; + isComment?: boolean; +} + +function InquiryEmptyIcon({ width, height, isComment = false }: IconProps) { + return ( + <> + {isComment ? ( + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + )} + + ); +} + +export default InquiryEmptyIcon; diff --git a/components/Icons/MedalIcon.tsx b/components/Icons/MedalIcon.tsx new file mode 100644 index 000000000..3c7efdae0 --- /dev/null +++ b/components/Icons/MedalIcon.tsx @@ -0,0 +1,26 @@ +interface IconProps { + width?: string; + height?: string; +} + +function MedalIcon({ width, height }: IconProps) { + return ( + + + + + ); +} + +export default MedalIcon; diff --git a/components/Icons/PlusIcon.tsx b/components/Icons/PlusIcon.tsx new file mode 100644 index 000000000..643268f1a --- /dev/null +++ b/components/Icons/PlusIcon.tsx @@ -0,0 +1,31 @@ +interface IconProps { + width?: string; + height?: string; +} + +function PlusIcon({ width, height }: IconProps) { + return ( + + + + + ); +} + +export default PlusIcon; diff --git a/components/Icons/ProfileIcon.tsx b/components/Icons/ProfileIcon.tsx new file mode 100644 index 000000000..3d8ad1f5b --- /dev/null +++ b/components/Icons/ProfileIcon.tsx @@ -0,0 +1,64 @@ +interface IconProps { + width?: string; + height?: string; +} + +function ProfileIcon({ width, height }: IconProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default ProfileIcon; diff --git a/components/Icons/SearchIcon.tsx b/components/Icons/SearchIcon.tsx new file mode 100644 index 000000000..1708b2f0e --- /dev/null +++ b/components/Icons/SearchIcon.tsx @@ -0,0 +1,23 @@ +interface IconProps { + width?: string; + height?: string; +} + +function SearchIcon({ width, height }: IconProps) { + return ( + + + + ); +} + +export default SearchIcon; diff --git a/components/Icons/ToggleIcon.tsx b/components/Icons/ToggleIcon.tsx new file mode 100644 index 000000000..2ae9f5390 --- /dev/null +++ b/components/Icons/ToggleIcon.tsx @@ -0,0 +1,22 @@ +interface IconProps { + width?: string; + height?: string; +} + +function ToggleIcon({ width, height }: IconProps) { + return ( + + + + + + ); +} + +export default ToggleIcon; diff --git a/components/addBoard/ImgFileInput.tsx b/components/addBoard/ImgFileInput.tsx index ecfc3ab51..cea6f862d 100644 --- a/components/addBoard/ImgFileInput.tsx +++ b/components/addBoard/ImgFileInput.tsx @@ -1,13 +1,19 @@ import { FormInputInterface } from "@/types/addBoard"; import Image from "next/image"; import { ReactNode, useEffect, useRef, useState } from "react"; -import { FieldValues, UseFormRegister, UseFormWatch } from "react-hook-form"; +import { + FieldValues, + UseFormRegister, + UseFormSetValue, + UseFormWatch, +} from "react-hook-form"; +import PlusIcon from "../Icons/PlusIcon"; interface ImgFileInputProps { name: keyof FormInputInterface; register: UseFormRegister; watch: UseFormWatch; - setValue: any; + setValue: UseFormSetValue; children: ReactNode; } @@ -61,7 +67,6 @@ function ImgFileInput({ register(name).ref(el); }} /> - {/* className='sr-only' */}
diff --git a/components/addItem/ImgFileInput.tsx b/components/addItem/ImgFileInput.tsx index 80b6f7be8..376bbd443 100644 --- a/components/addItem/ImgFileInput.tsx +++ b/components/addItem/ImgFileInput.tsx @@ -9,6 +9,7 @@ import { useRef, useState, } from "react"; +import PlusIcon from "../Icons/PlusIcon"; interface ImgFileInputProps { images: string[]; @@ -95,26 +96,7 @@ function ImgFileInput({ onClick={handleClick} > - - - - + 이미지 등록 diff --git a/components/addItem/PriceInput.tsx b/components/addItem/PriceInput.tsx index aa48b386c..17eeec6b7 100644 --- a/components/addItem/PriceInput.tsx +++ b/components/addItem/PriceInput.tsx @@ -22,7 +22,7 @@ function PriceInput({ const priceValue = formatToPrice(target.value); const regExp = /^\d*$/; // 숫자만 입력 가능하게 하기 위한 정규식 if (regExp.test(priceValue)) { - dispatch({ type: "SET_PRICE", payload: +priceValue }); + dispatch({ type: "SET_PRICE", payload: Number(priceValue) }); } }; return ( diff --git a/components/articles/Article.tsx b/components/articles/Article.tsx index 9a4051502..a1133e22d 100644 --- a/components/articles/Article.tsx +++ b/components/articles/Article.tsx @@ -3,6 +3,9 @@ import { formatDate } from "@/utils/formatDate"; import ImageArticle from "./ImageArticle"; import Link from "next/link"; import clsx from "clsx"; +import MedalIcon from "../Icons/MedalIcon"; +import HeartIcon from "../Icons/HeartIcon"; +import ProfileIcon from "../Icons/ProfileIcon"; interface ArticleProps { isBest?: boolean; @@ -17,21 +20,7 @@ function Article({ isBest = false, article }: ArticleProps) { {isBest && (
- - - - + Best
@@ -46,17 +35,7 @@ function Article({ isBest = false, article }: ArticleProps) {
{writer.nickname}
- - - + {likeCount}
@@ -67,80 +46,14 @@ function Article({ isBest = false, article }: ArticleProps) {
- - - - - - - - - - - - - - - - - - - - - +
{writer.nickname}
{formatDate(updatedAt)}
- - - + {likeCount}
diff --git a/components/articles/ArticleList.tsx b/components/articles/ArticleList.tsx index 48d91addc..ba11a9966 100644 --- a/components/articles/ArticleList.tsx +++ b/components/articles/ArticleList.tsx @@ -41,7 +41,7 @@ function ArticleList() { }).then(({ list, totalCount }) => { // 서버의 더 요청할 데이터가 없으면 스크롤 막기 위해 분기처리 const pageLimit = Math.ceil(totalCount / productsPerPage); - if (pageLimit <= +page) setHasMoreData(false); + if (pageLimit <= Number(page)) setHasMoreData(false); setArticles((prev) => (page === "1" ? list : [...prev, ...list])); setIsInitial(false); }); diff --git a/components/articles/ImageArticle.tsx b/components/articles/ImageArticle.tsx index 8ebedbb72..919c70a81 100644 --- a/components/articles/ImageArticle.tsx +++ b/components/articles/ImageArticle.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import { useState } from "react"; +import EmptyIcon from "../Icons/EmptyIcon"; interface ImageProps { image: string; @@ -13,11 +14,7 @@ function ImageArticle({ image, name }: ImageProps) { return (
{imageRenderError || !image ? ( - - - - - + ) : ( ; + register: UseFormRegisterReturn; + errorMessage?: string; } -function EmailInput({ state, handleValue }: EmailInputInterface) { - const { value, isValid, errorMessage, hasFocused } = state; - const { handleChange, handleBlur } = useInputHandler({ - field: "email", - handleValue, - }); +function EmailInput({ register, errorMessage }: EmailInputInterface) { return (
- {errorMessage} + {errorMessage && {errorMessage}}
); } diff --git a/components/auth/Form.tsx b/components/auth/Form.tsx index f5f2c1996..e3e87595c 100644 --- a/components/auth/Form.tsx +++ b/components/auth/Form.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import Link from "next/link"; -import { FormEvent, PropsWithChildren, ReactNode } from "react"; +import { PropsWithChildren, ReactNode } from "react"; type FormType = "login" | "signup"; @@ -8,16 +8,14 @@ interface FormInterface { formType: FormType; isValid: boolean; children: ReactNode; + handleSubmit: () => void; } -const handleSubmit = (e: FormEvent) => { - e.preventDefault(); -}; - function Form({ children, formType, isValid = false, + handleSubmit, }: PropsWithChildren) { return (
diff --git a/components/auth/NickNameInput.tsx b/components/auth/NickNameInput.tsx index af0eb2867..cb8c00018 100644 --- a/components/auth/NickNameInput.tsx +++ b/components/auth/NickNameInput.tsx @@ -1,35 +1,21 @@ -import validateField from "@/hooks/validateFied"; -import { AuthFormAction, AuthInputState } from "@/types/authForm"; -import { ChangeEvent, Dispatch, FocusEvent } from "react"; -import useInputHandler from "../../hooks/useInputHandler"; +import { UseFormRegisterReturn } from "react-hook-form"; interface PasswordInputInterface { - state: AuthInputState; - handleValue: Dispatch; + register: UseFormRegisterReturn; + errorMessage?: string; } -function NickNameInput({ state, handleValue }: PasswordInputInterface) { - const { value, isValid, errorMessage, hasFocused } = state; - const { handleChange, handleBlur } = useInputHandler({ - field: "nickname", - handleValue, - }); - +function NickNameInput({ register, errorMessage }: PasswordInputInterface) { return (
- {errorMessage} + {errorMessage && {errorMessage}}
); } diff --git a/components/auth/PassWordInput.tsx b/components/auth/PassWordInput.tsx index de5ea2471..4a873690b 100644 --- a/components/auth/PassWordInput.tsx +++ b/components/auth/PassWordInput.tsx @@ -1,21 +1,12 @@ import usePasswordVisibility from "@/hooks/usePassWordVisivility"; -import validateField from "@/hooks/validateFied"; -import { AuthFormAction, AuthInputState } from "@/types/authForm"; -import { ChangeEvent, Dispatch, FocusEvent } from "react"; -import useInputHandler from "../../hooks/useInputHandler"; -import clsx from "clsx"; +import { UseFormRegisterReturn } from "react-hook-form"; interface PasswordInputInterface { - state: AuthInputState; - handleValue: Dispatch; + register: UseFormRegisterReturn; + errorMessage?: string; } -function PassWordInput({ state, handleValue }: PasswordInputInterface) { - const { value, isValid, errorMessage, hasFocused } = state; - const { handleChange, handleBlur } = useInputHandler({ - field: "password", - handleValue, - }); +function PassWordInput({ register, errorMessage }: PasswordInputInterface) { const { inputType, toggleVisibility } = usePasswordVisibility(); return ( @@ -24,15 +15,11 @@ function PassWordInput({ state, handleValue }: PasswordInputInterface) { - {errorMessage} + {errorMessage && {errorMessage}}
- {isLogin ? ( + {hasToken ? (
- setIsOpen((prev) => !prev)} > - - - - - - - - - - - - - - - - - - - - - - + + {isOpen && ( + + )} +
) : ( diff --git a/components/detail/DetailArticle.tsx b/components/detail/DetailArticle.tsx index cb027f41f..c23944d63 100644 --- a/components/detail/DetailArticle.tsx +++ b/components/detail/DetailArticle.tsx @@ -1,5 +1,7 @@ import { ArticleInterface } from "@/types/article"; import { formatDate } from "@/utils/formatDate"; +import ProfileIcon from "../Icons/ProfileIcon"; +import HeartIcon from "../Icons/HeartIcon"; function DetailArticle({ id, @@ -17,80 +19,14 @@ function DetailArticle({
- - - - - - - - - - - - - - - - - - - - - +
{writer.nickname}
{formatDate(updatedAt)}
- - - +
{likeCount}
diff --git a/components/detail/DetailInquiry.tsx b/components/detail/DetailInquiry.tsx index 598a2b8c2..5140312d6 100644 --- a/components/detail/DetailInquiry.tsx +++ b/components/detail/DetailInquiry.tsx @@ -3,6 +3,7 @@ import DropDownInquiry from "./DropDownInquiry"; import { updateComment, updateCommentInterface } from "@/pages/api/productApi"; import { calculateGapTime } from "@/utils/formatDate"; import Image from "next/image"; +import ProfileIcon from "../Icons/ProfileIcon"; interface DetailInquiryProps { id: string; @@ -59,63 +60,7 @@ function DetailInquiry({ id, content, writer, updatedAt }: DetailInquiryProps) { {writer.image ? ( {writer.nickname} ) : ( - - - - - - - - - - - - - - - - - - - - - + )}
diff --git a/components/detail/DetailProduct.tsx b/components/detail/DetailProduct.tsx index 0930c3777..bdb6e010c 100644 --- a/components/detail/DetailProduct.tsx +++ b/components/detail/DetailProduct.tsx @@ -1,6 +1,8 @@ import { formatPriceToKRW } from "@/utils/formatPrice"; import { formatDate } from "@/utils/formatDate"; import ImageProduct from "../items/ImageProduct"; +import ProfileIcon from "../Icons/ProfileIcon"; +import HeartIcon from "../Icons/HeartIcon"; interface DetailProductProps { name: string; @@ -48,63 +50,7 @@ function DetailProduct({
- - - - - - - - - - - - - - - - - - - - - +
{ownerNickname}
@@ -113,32 +59,7 @@ function DetailProduct({
diff --git a/components/detail/DropDownInquiry.tsx b/components/detail/DropDownInquiry.tsx index 19fe5171e..013ad0425 100644 --- a/components/detail/DropDownInquiry.tsx +++ b/components/detail/DropDownInquiry.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import { useState } from "react"; +import ToggleIcon from "../Icons/ToggleIcon"; interface DropDownInquiryProps { setIsEditting: (value: boolean) => void; @@ -23,11 +24,7 @@ function DropDownInquiry({ setIsEditting }: DropDownInquiryProps) { className='btn-open-menu' onClick={() => setIsOpen((prev) => !prev)} > - - - - - + {isOpen && (
diff --git a/components/detail/InquiryEmpty.tsx b/components/detail/InquiryEmpty.tsx index 57a701f8c..9ec396357 100644 --- a/components/detail/InquiryEmpty.tsx +++ b/components/detail/InquiryEmpty.tsx @@ -1,3 +1,5 @@ +import InquiryEmptyIcon from "../Icons/InquiryEmptyIcon"; + interface InquiryEmptyProps { isArticle?: boolean; } @@ -8,30 +10,7 @@ function InquiryEmpty({ isArticle = false }: InquiryEmptyProps) { {isArticle ? ( <>
- - - - - - - - - - +
아직 댓글이 없어요, @@ -42,76 +21,7 @@ function InquiryEmpty({ isArticle = false }: InquiryEmptyProps) { ) : ( <>
- - - - - - - - - - - - - - - - - +
아직 문의가 없어요 diff --git a/components/items/ImageProduct.tsx b/components/items/ImageProduct.tsx index 6b669bcf6..e2670ba7d 100644 --- a/components/items/ImageProduct.tsx +++ b/components/items/ImageProduct.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import { useState } from "react"; +import EmptyIcon from "../Icons/EmptyIcon"; interface ImageProps { images: string[]; @@ -13,11 +14,7 @@ function ImageProduct({ images, name }: ImageProps) { return (
{imageRenderError || !image ? ( - - - - - + ) : ( {formattedPrice}
- - - +
{favoriteCount}
diff --git a/components/items/Search.tsx b/components/items/Search.tsx index e1937b96d..64fb354ca 100644 --- a/components/items/Search.tsx +++ b/components/items/Search.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import { ChangeEvent } from "react"; +import SearchIcon from "../Icons/SearchIcon"; interface SearchProps { setKeyword: (value: string) => void; @@ -11,12 +12,7 @@ function Search({ setKeyword }: SearchProps) { return (
- - - +
-
+
diff --git a/pages/addboard/index.tsx b/pages/addboard/index.tsx index c74530290..ea8b3ada3 100644 --- a/pages/addboard/index.tsx +++ b/pages/addboard/index.tsx @@ -29,7 +29,7 @@ function AddBoard() { }; return ( <> -
+
diff --git a/pages/api/articleApi.ts b/pages/api/articleApi.ts index a65aa92bc..e890eb900 100644 --- a/pages/api/articleApi.ts +++ b/pages/api/articleApi.ts @@ -55,39 +55,6 @@ const fetchInquiryById = async (id: string, cursor: string | null = null) => { } }; -const SetTokensToLocalStorage = async () => { - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/signIn`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: process.env.NEXT_PUBLIC_USER_EMAIL, - password: process.env.NEXT_PUBLIC_USER_PASSWORD, - }), - } - ); - - if (!response.ok) { - throw new Error("로그인 실패: 서버에서 인증 정보를 확인하세요."); - } - - const { accessToken, refreshToken } = await response.json(); - - // localStorage에 토큰 저장 - localStorage.setItem("accessToken", accessToken); - localStorage.setItem("refreshToken", refreshToken); - - console.log("토큰이 성공적으로 설정되었습니다."); - } catch (error) { - console.error("초기 로그인 요청 실패:", error); - throw error; - } -}; - const retryFetch = async ( url: string, options: RequestInit @@ -233,5 +200,4 @@ export { fetchInquiryById, postArticle, postArticleComment, - SetTokensToLocalStorage, }; diff --git a/pages/api/authApi.ts b/pages/api/authApi.ts new file mode 100644 index 000000000..63c80e6db --- /dev/null +++ b/pages/api/authApi.ts @@ -0,0 +1,65 @@ +import { LoginInterface, SignupInterface } from "@/types/auth"; + +const loginAndSetToken = async ({ email, password }: LoginInterface) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/signIn`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: email, + password: password, + }), + } + ); + + if (response.ok) { + const { accessToken, refreshToken } = await response.json(); + + // localStorage에 토큰 저장 + localStorage.setItem("accessToken", accessToken); + localStorage.setItem("refreshToken", refreshToken); + + console.log("토큰이 성공적으로 설정되었습니다."); + } else { + throw new Error("로그인 실패: 서버에서 인증 정보를 확인하세요."); + } + } catch (error) { + console.error("초기 로그인 요청 실패:", error); + throw error; + } +}; + +const signUp = async ({ email, nickname, password }: SignupInterface) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/signUp`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + nickname, + password, + passwordConfirmation: password, + }), + } + ); + + if (!response.ok) { + console.log(response.statusText); + } + + return await response.json(); + } catch (error) { + console.error("초기 로그인 요청 실패:", error); + throw error; + } +}; + +export { loginAndSetToken, signUp }; diff --git a/pages/boards/[id].tsx b/pages/boards/[id].tsx index 2861ddf7b..e67d8acab 100644 --- a/pages/boards/[id].tsx +++ b/pages/boards/[id].tsx @@ -12,6 +12,7 @@ import { ArticleCommentInterface, ArticleInterface } from "@/types/article"; import PrimaryButton from "@/components/common/PrimaryButton"; import DetailInquiry from "@/components/detail/DetailInquiry"; import InquiryEmpty from "@/components/detail/InquiryEmpty"; +import BackIcon from "@/components/Icons/BackIcon"; const INITIAL_DETAILS: ArticleInterface = { id: 0, @@ -69,7 +70,7 @@ function Detail() { throw new Error("Id is null"); } const response = await postArticleComment({ - id: +id, + id: Number(id), content: newComment, }); setComments((prev) => ({ @@ -119,7 +120,7 @@ function Detail() { }, [handleObserver]); return ( <> -
+
@@ -153,21 +154,7 @@ function Detail() { 목록으로 돌아가기 - {/* Sprint10에 추가 예정 */} - - - - +
diff --git a/pages/boards/index.tsx b/pages/boards/index.tsx index aa695470c..e8a0be55d 100644 --- a/pages/boards/index.tsx +++ b/pages/boards/index.tsx @@ -5,7 +5,7 @@ import Header from "@/components/common/Header"; function index() { return ( <> -
+
diff --git a/pages/index.tsx b/pages/index.tsx index a803c0dd0..c2097d5eb 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -2,13 +2,8 @@ import Header from "@/components/common/Header"; import Link from "next/link"; import Footer from "@/components/common/Footer"; import Image from "next/image"; -import { useEffect } from "react"; -import { SetTokensToLocalStorage } from "./api/articleApi"; export default function Home() { - useEffect(() => { - SetTokensToLocalStorage(); - }, []); return ( <>
diff --git a/pages/items/[id].tsx b/pages/items/[id].tsx index 6f2a0eb0b..e0434c3e9 100644 --- a/pages/items/[id].tsx +++ b/pages/items/[id].tsx @@ -11,6 +11,7 @@ import DetailInquiry from "@/components/detail/DetailInquiry"; import InquiryEmpty from "@/components/detail/InquiryEmpty"; import Link from "next/link"; import { useRouter } from "next/router"; +import BackIcon from "@/components/Icons/BackIcon"; interface InputState { name: string; @@ -144,7 +145,7 @@ function Detail() { }, [handleObserver]); return ( <> -
+
@@ -178,20 +179,7 @@ function Detail() { 목록으로 돌아가기 - - - - +
diff --git a/pages/items/index.tsx b/pages/items/index.tsx index 348680a49..825329bd2 100644 --- a/pages/items/index.tsx +++ b/pages/items/index.tsx @@ -5,7 +5,7 @@ import ProductList from "@/components/items/ProductList"; function Items() { return ( <> -
+
diff --git a/pages/login.tsx b/pages/login.tsx index 638e1362e..f51975dd3 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -1,31 +1,45 @@ import EmailInput from "@/components/auth/EmailInput"; import Form from "@/components/auth/Form"; import PassWordInput from "@/components/auth/PassWordInput"; -import useInputReducer from "@/reducers/useInputReducer"; -import { AuthFormState } from "@/types/authForm"; import Image from "next/image"; import Link from "next/link"; -import { useReducer } from "react"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { loginAndSetToken } from "./api/authApi"; +import { LoginInterface } from "@/types/auth"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; -const INITIAL_FORM_STATE: AuthFormState = { - email: { - value: "", - isValid: false, - errorMessage: "", - hasFocused: false, - }, - password: { - value: "", - isValid: false, - errorMessage: "", - hasFocused: false, - }, - isFormValid: false, +const INITIAL_FORM_STATE: LoginInterface = { + email: "", + password: "", }; function Login() { - const [state, dispatch] = useReducer(useInputReducer, INITIAL_FORM_STATE); - const { email, password, isFormValid } = state; + const router = useRouter(); + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + mode: "onBlur", + defaultValues: INITIAL_FORM_STATE, + }); + const [isLoading, setIsLoading] = useState(true); + + const onSubmit: SubmitHandler = async ({ + email, + password, + }) => { + await loginAndSetToken({ email, password }); + router.push("/"); + }; + + useEffect(() => { + if (localStorage.getItem("accessToken")) router.push("/"); + else setIsLoading(false); + }, [router]); + + if (isLoading) return null; return (
@@ -39,9 +53,27 @@ function Login() { /> - - - + + +
); diff --git a/pages/signup.tsx b/pages/signup.tsx index 32067b5fa..f17f8b2a1 100644 --- a/pages/signup.tsx +++ b/pages/signup.tsx @@ -3,43 +3,53 @@ import Form from "@/components/auth/Form"; import NickNameInput from "@/components/auth/NickNameInput"; import PassWordInput from "@/components/auth/PassWordInput"; import PassWordInputConfirm from "@/components/auth/PassWordInputConfirm"; -import useInputReducer from "@/reducers/useInputReducer"; -import { AuthFormState } from "@/types/authForm"; +import { SignupInterface } from "@/types/auth"; import Image from "next/image"; import Link from "next/link"; -import { useReducer } from "react"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { signUp } from "./api/authApi"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; -const INITIAL_FORM_STATE: AuthFormState = { - email: { - value: "", - isValid: false, - errorMessage: "", - hasFocused: false, - }, - nickname: { - value: "", - isValid: false, - errorMessage: "", - hasFocused: false, - }, - password: { - value: "", - isValid: false, - errorMessage: "", - hasFocused: false, - }, - passwordConfirm: { - value: "", - isValid: false, - errorMessage: "", - hasFocused: false, - }, - isFormValid: false, +const INITIAL_FORM_STATE: SignupInterface = { + email: "", + nickname: "", + password: "", + passwordConfirm: "", }; function Signup() { - const [state, dispatch] = useReducer(useInputReducer, INITIAL_FORM_STATE); - const { email, nickname, password, passwordConfirm, isFormValid } = state; + const router = useRouter(); + const { + register, + handleSubmit, + watch, + formState: { errors, isValid }, + } = useForm({ + mode: "onBlur", + defaultValues: INITIAL_FORM_STATE, + }); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const onSubmit: SubmitHandler = async ({ + email, + nickname, + password, + }) => { + const res = await signUp({ email, nickname, password }); + + if (res.message) setError(res.message); + else router.push("/login"); + }; + + useEffect(() => { + if (localStorage.getItem("accessToken")) router.push("/"); + else setIsLoading(false); + }, [router]); + + if (isLoading) return null; + return (

@@ -52,18 +62,51 @@ function Signup() { />

-
- - {nickname && } - - {passwordConfirm && ( - - )} + + + + + + value === watch("password") || "비밀번호가 일치하지 않습니다.", + })} + errorMessage={errors.passwordConfirm?.message} + /> + {error &&
{error}
}
); } diff --git a/styles/css/style.css b/styles/css/style.css index fe57815c2..6ff5df027 100644 --- a/styles/css/style.css +++ b/styles/css/style.css @@ -474,6 +474,7 @@ template { --gray600: #4b5563; --gray500: #6b7280; --gray400: #9ca3af; + --gray300: #d1d5db; --gray200: #e5e7eb; --gray100: #f3f4f6; --gray50: #f9fafb; @@ -577,10 +578,25 @@ header .login-area .content-link a + a { header .login-area .content-link a.active { color: var(--primary100); } -header .login-area .link-profile { +header .login-area .btn-profile { position: relative; width: 4rem; height: 4rem; + border: 0; + background-color: transparent; +} +header .login-area .btn-profile .btn-logout { + position: absolute; + bottom: -6rem; + right: 0; + width: 13.9rem; + padding: 1.6rem 0; + border: 0.1rem solid var(--gray300); + border-radius: 0.8rem; + background-color: var(--white); + font-size: 1.6rem; + line-height: 1.9rem; + color: var(--gray500); } header .link-login { display: flex; @@ -1954,7 +1970,6 @@ footer .footer-wrap .sns a img { height: 100%; } .container form .input-area .msg-error { - display: none; margin: 0.8rem 0 0 1.6rem; font-size: 1.4rem; font-weight: var(--semiBold); @@ -2013,6 +2028,12 @@ footer .footer-wrap .sns a img { color: var(--primary100); text-decoration: underline; } +.container .res-message { + margin-top: 1.5rem; + font-size: 1.8rem; + font-weight: var(--bold); + color: var(--error); +} /* BreakPoint Mobile */ @media (max-width: 767px) { diff --git a/styles/scss/components/_header.scss b/styles/scss/components/_header.scss index c2d577573..58b75519e 100644 --- a/styles/scss/components/_header.scss +++ b/styles/scss/components/_header.scss @@ -45,10 +45,25 @@ header { } } } - .link-profile { + .btn-profile { position: relative; width: 4rem; height: 4rem; + border: 0; + background-color: transparent; + .btn-logout { + position: absolute; + bottom: -6rem; + right: 0; + width: 13.9rem; + padding: 1.6rem 0; + border: 0.1rem solid var(--gray300); + border-radius: 0.8rem; + background-color: var(--white); + font-size: 1.6rem; + line-height: 1.9rem; + color: var(--gray500); + } } } .link-login { diff --git a/styles/scss/helpers/_common.scss b/styles/scss/helpers/_common.scss index 392b9177e..a0812a9eb 100644 --- a/styles/scss/helpers/_common.scss +++ b/styles/scss/helpers/_common.scss @@ -19,6 +19,7 @@ --gray600: #4b5563; --gray500: #6b7280; --gray400: #9ca3af; + --gray300: #d1d5db; --gray200: #e5e7eb; --gray100: #f3f4f6; --gray50: #f9fafb; diff --git a/styles/scss/pages/_auth.scss b/styles/scss/pages/_auth.scss index 87e640c11..d5ba83973 100644 --- a/styles/scss/pages/_auth.scss +++ b/styles/scss/pages/_auth.scss @@ -83,7 +83,6 @@ } } .msg-error { - display: none; margin: 0.8rem 0 0 1.6rem; font-size: 1.4rem; font-weight: var(--semiBold); @@ -144,6 +143,12 @@ } } } + .res-message { + margin-top: 1.5rem; + font-size: 1.8rem; + font-weight: var(--bold); + color: var(--error); + } } /* BreakPoint Mobile */ diff --git a/types/auth.ts b/types/auth.ts new file mode 100644 index 000000000..1622cd505 --- /dev/null +++ b/types/auth.ts @@ -0,0 +1,13 @@ +interface LoginInterface { + email: string; + password: string; +} + +interface SignupInterface { + email: string; + nickname: string; + password: string; + passwordConfirm?: string; +} + +export type { LoginInterface, SignupInterface }; diff --git a/utils/formatDate.ts b/utils/formatDate.ts index a8424dbb0..812b03ba0 100644 --- a/utils/formatDate.ts +++ b/utils/formatDate.ts @@ -11,7 +11,7 @@ const calculateGapTime = (date: string) => { const currentDate = new Date(); const targetDate = new Date(date); const differenceInMinutes = Math.floor( - (+currentDate - +targetDate) / (1000 * 60) + (Number(currentDate) - Number(targetDate)) / (1000 * 60) ); const differenceInHour = Math.floor(differenceInMinutes / 60); const differenceInDays = Math.floor(differenceInHour / 24);