diff --git a/.eslintrc.json b/.eslintrc.json index fbc08616..23e864d8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,7 +18,6 @@ "sourceType": "module" }, "extends": [ - "tui", "prettier", "plugin:import/recommended", "plugin:import/errors", @@ -51,7 +50,14 @@ "import/order": [ "error", { - "groups": ["external", "internal", "builtin", "parent", "sibling", "index"], + "groups": [ + "external", + "internal", + "builtin", + "parent", + "sibling", + "index" + ], "pathGroupsExcludedImportTypes": ["react"], "newlines-between": "always", "pathGroups": [ @@ -73,7 +79,10 @@ } ], "react/react-in-jsx-scope": "off", - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], + "react/jsx-filename-extension": [ + 1, + { "extensions": [".js", ".jsx", ".ts", ".tsx"] } + ], "@typescript-eslint/no-unused-vars": ["warn"], "@typescript-eslint/no-shadow": ["warn"], "@typescript-eslint/explicit-module-boundary-types": "off", diff --git a/package.json b/package.json index 75e4d842..1cd86401 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,17 @@ ] }, "dependencies": { + "@hookform/resolvers": "^3.6.0", "@reduxjs/toolkit": "^2.2.5", "axios": "^1.7.2", "next": "14.2.4", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.52.0", "react-query": "^3.39.3", "react-redux": "^9.1.2", - "redux-persist": "^6.0.0" + "redux-persist": "^6.0.0", + "yup": "^1.4.0" }, "devDependencies": { "@commitlint/cli": "^19.3.0", diff --git a/public/icons/invisibility.svg b/public/icons/invisibility.svg new file mode 100644 index 00000000..febddd10 --- /dev/null +++ b/public/icons/invisibility.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/visibility.svg b/public/icons/visibility.svg new file mode 100644 index 00000000..a4c5284b --- /dev/null +++ b/public/icons/visibility.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/logo_sign.svg b/public/images/logo_sign.svg new file mode 100644 index 00000000..ff73125d --- /dev/null +++ b/public/images/logo_sign.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/containers/signin&signup/PwdInputWithLabel.tsx b/src/containers/signin&signup/PwdInputWithLabel.tsx new file mode 100644 index 00000000..bdf56cb1 --- /dev/null +++ b/src/containers/signin&signup/PwdInputWithLabel.tsx @@ -0,0 +1,55 @@ +import Image from 'next/image'; +import { useState } from 'react'; +import { UseFormRegister, FieldValues, Path } from 'react-hook-form'; + +interface Props { + id: Path; // Path로 타입 지정 + label: string; + placeholder: string; + error?: string; + register: UseFormRegister; +} + +export default function PwdInputWithLabel({ + id, + label, + placeholder, + error, + register, +}: Props) { + const [visible, setVisible] = useState(false); + const type = visible ? 'text' : 'password'; + + return ( +
+ +
+ + +
+ {error &&

{error}

} +
+ ); +} diff --git a/src/containers/signin&signup/SignInForm.tsx b/src/containers/signin&signup/SignInForm.tsx new file mode 100644 index 00000000..31476bcb --- /dev/null +++ b/src/containers/signin&signup/SignInForm.tsx @@ -0,0 +1,88 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useSelector } from 'react-redux'; +import * as yup from 'yup'; + +import Button from '@/components/Button'; +import PwdInputWithLabel from '@/containers/signin&signup/PwdInputWithLabel'; +import TextInputWithLabel from '@/containers/signin&signup/TextInputWithLabel'; +import { useSignIn } from '@/hooks/useSignIn'; +import { RootState } from '@/store/store'; + +export type TSignInInputs = { + email: string; + password: string; +}; + +const schema = yup.object().shape({ + email: yup.string().email('유효한 이메일 주소를 입력해주세요.').required('이메일을 입력해주세요.'), + password: yup.string().required('비밀번호를 입력해주세요.').min(8, '비밀번호는 최소 8자 이상이어야 합니다.'), +}); + +export default function SignInForm() { + const [checkTerms, setCheckTerms] = useState(false); + + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: yupResolver(schema), + mode: 'onChange', + }); + const mutation = useSignIn(); + + // useSelector를 사용하여 Redux store의 상태를 조회 + const { user, error } = useSelector((state: RootState) => state.user); + + const onSubmit = (data: TSignInInputs) => { + mutation.mutate(data); + }; + + return ( +
+ + +
+ { + setCheckTerms(!checkTerms); + }} + />{' '} + +
+
+ +
+ {mutation.isError && ( +

Error: {mutation.error instanceof Error ? mutation.error.message : '알 수 없는 오류가 발생했습니다.'}

+ )} + {user && ( +

+ 로그인 성공: {user.nickname} ({user.id}) +

+ )} + {error &&

오류: {error}

} + + ); +} diff --git a/src/containers/signin&signup/SignUpForm.tsx b/src/containers/signin&signup/SignUpForm.tsx new file mode 100644 index 00000000..53b788a0 --- /dev/null +++ b/src/containers/signin&signup/SignUpForm.tsx @@ -0,0 +1,104 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import * as yup from 'yup'; + +import Button from '@/components/Button'; +import PwdInputWithLabel from '@/containers/signin&signup/PwdInputWithLabel'; +import TextInputWithLabel from '@/containers/signin&signup/TextInputWithLabel'; + +export type TSignUpInputs = { + email: string; + nickname: string; + password: string; + passwordConfirmation: string; +}; + +const schema = yup.object().shape({ + email: yup.string().email('유효한 이메일 주소를 입력해주세요.').required('이메일을 입력해주세요.'), + nickname: yup.string().required('닉네임을 입력해주세요.'), + password: yup.string().required('비밀번호를 입력해주세요.').min(8, '비밀번호는 최소 8자 이상이어야 합니다.'), + passwordConfirmation: yup + .string() + .required('비밀번호 확인을 입력해주세요.') + .oneOf([yup.ref('password'), ''], '비밀번호가 일치하지 않습니다.'), +}); + +export default function SignUpForm() { + const [checkTerms, setCheckTerms] = useState(false); + + const { + register, + handleSubmit, + watch, + trigger, + formState: { errors, isValid }, + } = useForm({ + resolver: yupResolver(schema), + mode: 'onChange', + }); + + const password = watch('password'); + const passwordConfirmation = watch('passwordConfirmation'); + + useEffect(() => { + if (password?.length > 0) { + trigger('passwordConfirmation'); + } + }, [password, passwordConfirmation, trigger]); + + const onSubmit = (data: TSignUpInputs) => { + console.log(data); + }; + + return ( +
+ + + + +
+ { + setCheckTerms(!checkTerms); + }} + />{' '} + +
+
+ +
+ + ); +} diff --git a/src/containers/signin&signup/TextInputWithLabel.tsx b/src/containers/signin&signup/TextInputWithLabel.tsx new file mode 100644 index 00000000..9ed085b5 --- /dev/null +++ b/src/containers/signin&signup/TextInputWithLabel.tsx @@ -0,0 +1,46 @@ +import { UseFormRegister, FieldValues, Path } from 'react-hook-form'; + +interface Props { + id: Path; + label: string; + placeholder: string; + error?: string; + register: UseFormRegister; +} + +export default function TextInputWithLabel({ + id, + label, + placeholder, + error, + register, +}: Props) { + let type = 'text'; + let autoComplete = 'off'; + + if (id === 'email') { + type = 'email'; + autoComplete = 'email'; + } + + return ( +
+ +
+ +
+ {error &&

{error}

} +
+ ); +} diff --git a/src/containers/signin&signup/TopLogoSection.tsx b/src/containers/signin&signup/TopLogoSection.tsx new file mode 100644 index 00000000..99a7d0de --- /dev/null +++ b/src/containers/signin&signup/TopLogoSection.tsx @@ -0,0 +1,17 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import LOGO_SIGN from '@/../public/images/logo_sign.svg'; + +export default function TopLogoSection({ text }: { text: string }) { + return ( +
+ +
+ 로고 이미지 +

{text}

+
+ +
+ ); +} diff --git a/src/containers/signin/SignInForm.tsx b/src/containers/signin/SignInForm.tsx deleted file mode 100644 index 45946960..00000000 --- a/src/containers/signin/SignInForm.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useState } from 'react'; -import { useSelector } from 'react-redux'; - -import { useSignIn } from '@/hooks/useSignIn'; -import { RootState } from '@/store/store'; - -export default function SignInForm() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const mutation = useSignIn(); - - // useSelector를 사용하여 Redux store의 상태를 조회 - const { user, error } = useSelector((state: RootState) => state.user); - - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - mutation.mutate({ email, password }); - }; - - return ( -
- setEmail(e.target.value)} - placeholder='이메일을 입력하세요.' - required - /> - setPassword(e.target.value)} - placeholder='비밀번호를 입력하세요.' - required - /> - - {mutation.isError && ( -

Error: {mutation.error instanceof Error ? mutation.error.message : '알 수 없는 오류가 발생했습니다.'}

- )} - - {user && ( -

- 로그인 성공: {user.nickname} ({user.id}) -

- )} - {error &&

오류: {error}

} -
- ); -} diff --git a/src/pages/signin/index.tsx b/src/pages/signin/index.tsx index bfe914f4..c1a408b2 100644 --- a/src/pages/signin/index.tsx +++ b/src/pages/signin/index.tsx @@ -1,9 +1,21 @@ -import SignInForm from '@/containers/signin/SignInForm'; +import Link from 'next/link'; -const SignInPage: React.FC = () => ( -
- -
-); +import SignInForm from '@/containers/signin&signup/SignInForm'; +import TopLogoSection from '@/containers/signin&signup/TopLogoSection'; -export default SignInPage; +export default function SignInPage() { + return ( +
+
+ + +

+ 회원이 아니신가요?{' '} + + 회원가입하기 + +

+
+
+ ); +} diff --git a/src/pages/signup/index.tsx b/src/pages/signup/index.tsx index 0d092fa2..1a14da3b 100644 --- a/src/pages/signup/index.tsx +++ b/src/pages/signup/index.tsx @@ -1,5 +1,21 @@ -const SignUpPage: React.FC = () => { - return
SignUp
; -}; +import Link from 'next/link'; -export default SignUpPage; +import SignUpForm from '@/containers/signin&signup/SignUpForm'; +import TopLogoSection from '@/containers/signin&signup/TopLogoSection'; + +export default function SignUp() { + return ( +
+
+ + +

+ 이미 가입하셨나요?{' '} + + 로그인하기 + +

+
+
+ ); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 6fec5d9d..481b309a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -5,6 +5,7 @@ const config: Config = { './src/pages/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/app/**/*.{js,ts,jsx,tsx,mdx}', + './src/containers/**/*.{js,ts,jsx,tsx,mdx}', './src/layouts/**/*.{js,ts,jsx,tsx,mdx}', ], theme: {