diff --git a/package-lock.json b/package-lock.json index 2110c546..b28f3615 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "framer-motion": "^10.16.16", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", "react-router-dom": "^6.21.1", "recoil": "^0.7.7" @@ -5903,6 +5904,17 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.622", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.622.tgz", @@ -9956,6 +9968,22 @@ } } }, + "node_modules/react-hook-form": { + "version": "7.49.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", + "integrity": "sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-icons": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", diff --git a/package.json b/package.json index 62353a45..30d2604d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "framer-motion": "^10.16.16", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", "react-router-dom": "^6.21.1", "recoil": "^0.7.7" @@ -54,7 +55,7 @@ "vite": "^5.0.8", "vite-plugin-svgr": "^4.2.0" }, - "msw": { + "msw": { "workerDirectory": "public" } } diff --git a/src/assets/icons/removeBtn.svg b/src/assets/icons/removeBtn.svg new file mode 100644 index 00000000..7ec6ea61 --- /dev/null +++ b/src/assets/icons/removeBtn.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/kakao/kakao_login_large_narrow.png b/src/assets/kakao/kakao_login_large_narrow.png new file mode 100644 index 00000000..894c2232 Binary files /dev/null and b/src/assets/kakao/kakao_login_large_narrow.png differ diff --git a/src/assets/kakao/kakao_login_large_wide.png b/src/assets/kakao/kakao_login_large_wide.png new file mode 100644 index 00000000..c0c18561 Binary files /dev/null and b/src/assets/kakao/kakao_login_large_wide.png differ diff --git a/src/assets/kakao/kakao_login_medium_narrow.png b/src/assets/kakao/kakao_login_medium_narrow.png new file mode 100644 index 00000000..09bb3588 Binary files /dev/null and b/src/assets/kakao/kakao_login_medium_narrow.png differ diff --git a/src/assets/kakao/kakao_login_medium_wide.png b/src/assets/kakao/kakao_login_medium_wide.png new file mode 100644 index 00000000..c882acc7 Binary files /dev/null and b/src/assets/kakao/kakao_login_medium_wide.png differ diff --git a/src/assets/kakao/kakao_path.svg b/src/assets/kakao/kakao_path.svg new file mode 100644 index 00000000..cc5eb65a --- /dev/null +++ b/src/assets/kakao/kakao_path.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 00000000..16f794a7 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,14 @@ + + + diff --git a/src/components/Auth/Input/Input.module.scss b/src/components/Auth/Input/Input.module.scss new file mode 100644 index 00000000..019b6fd9 --- /dev/null +++ b/src/components/Auth/Input/Input.module.scss @@ -0,0 +1,47 @@ +@use "@/sass" as *; +.container { + position: relative; + + label { + display: block; + + @include typography(tabLabel); + color: $neutral900; + } + + &:focus-within { + input { + outline: none; + border-width: 1px; + border-color: $primary300; + } + + .removeBtn { + display: block; + } + } + + .input { + width: 100%; + margin-top: 6px; + margin-bottom: 24px; + padding: 16px; + border: 1px solid $neutral200; + border-radius: 8px; + + @include typography(bodyLarge); + color: $neutral900; + + &::placeholder { + color: $neutral400; + } + } + + .removeBtn { + display: none; + position: absolute; + top: 47px; + right: 14px; + cursor: default; + } +} diff --git a/src/components/Auth/Input/InputEmail.tsx b/src/components/Auth/Input/InputEmail.tsx new file mode 100644 index 00000000..b6219e32 --- /dev/null +++ b/src/components/Auth/Input/InputEmail.tsx @@ -0,0 +1,44 @@ +import styles from "./Input.module.scss"; + +import RemoveBtn from "@/assets/icons/removeBtn.svg?react"; + +import { LoginInput } from "@/types/auth"; + +function InputEmail({ label, register, dirtyFields, resetField }: LoginInput) { + const resetEmail = () => { + resetField("email"); + }; + + return ( +
+ + + + + {dirtyFields.email && ( + + )} +
+ ); +} + +export default InputEmail; diff --git a/src/components/Auth/Input/InputPassword.tsx b/src/components/Auth/Input/InputPassword.tsx new file mode 100644 index 00000000..b99ef1b2 --- /dev/null +++ b/src/components/Auth/Input/InputPassword.tsx @@ -0,0 +1,48 @@ +import styles from "./Input.module.scss"; + +import RemoveBtn from "@/assets/icons/removeBtn.svg?react"; + +import { LoginInput } from "@/types/auth"; + +function InputPassword({ + label, + register, + dirtyFields, + resetField, +}: LoginInput) { + const resetPassword = () => { + resetField("password"); + }; + + return ( +
+ + + + + {dirtyFields.password && ( + + )} +
+ ); +} + +export default InputPassword; diff --git a/src/components/Auth/Login/LoginForm.module.scss b/src/components/Auth/Login/LoginForm.module.scss new file mode 100644 index 00000000..d0679794 --- /dev/null +++ b/src/components/Auth/Login/LoginForm.module.scss @@ -0,0 +1,31 @@ +@use "@/sass" as *; + +.container { + position: relative; + + small { + width: 100%; + position: absolute; + bottom: 65px; + left: 0; + text-align: center; + + @include typography(captionSmall); + color: $danger300; + } + + .submitBtn { + width: 100%; + padding: 13px 0; + background: $primary300; + margin-top: 32px; + border-radius: 16px; + + @include typography(button); + color: $neutral0; + + &:hover { + background: $primary400; + } + } +} diff --git a/src/components/Auth/Login/LoginForm.tsx b/src/components/Auth/Login/LoginForm.tsx new file mode 100644 index 00000000..7e290703 --- /dev/null +++ b/src/components/Auth/Login/LoginForm.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; + +import styles from "./LoginForm.module.scss"; + +import InputEmail from "../Input/InputEmail"; +import InputPassword from "../Input/InputPassword"; + +import { LoginForm, SubmitResult } from "@/types/auth"; + +function LoginForm() { + const { + register, + resetField, + formState: { errors, dirtyFields }, + } = useForm({ + mode: "onChange", + defaultValues: { + email: "", + password: "", + }, + }); + + const [submitResult, setSubmitResult] = useState({ + try: false, + isPassed: false, + }); + + const clickLogin = () => { + setSubmitResult({ ...submitResult, try: true }); + + const isError = Object.keys(errors).length > 0; + if (isError) { + setSubmitResult({ try: true, isPassed: false }); + return; + } + + setSubmitResult({ try: true, isPassed: true }); + + // 이메일 비밀번호 validation 성공 + // 로그인 api 요청 + /* + * + * + * + */ + }; + + return ( +
+ + + + + {submitResult.try && !submitResult.isPassed ? ( + 이메일 또는 비밀번호를 확인해주세요. + ) : null} + + + + ); +} + +export default LoginForm; diff --git a/src/pages/Login/Login.module.scss b/src/pages/Login/Login.module.scss new file mode 100644 index 00000000..2c2a4d3b --- /dev/null +++ b/src/pages/Login/Login.module.scss @@ -0,0 +1,88 @@ +@use "@/sass" as *; + +.container { + padding: 0 24px; + + h1 { + margin: 48px 0 64px 0; + width: 100%; + display: flex; + justify-content: center; + } + + .aboutAuth { + display: flex; + justify-content: center; + gap: 16px; + width: 100%; + margin-top: 24px; + + @include typography(captionSmall); + color: $neutral400; + + a:hover { + font-weight: bold; + } + } + + .snsLogin { + margin: 128px 0 24px 0; + + &__head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + + @include typography(captionSmall); + color: $neutral400; + } + + &__btn { + display: flex; + justify-content: space-between; + align-items: center; + padding: 11.5px 28px; + margin-top: 16px; + background: $etc1; + width: 100%; + border-radius: 6px; + + @include typography(button); + + .kakao_text { + flex-grow: 1; + } + + .kakao_space { + width: 18px; + } + } + } + + .toHome { + text-align: center; + margin-bottom: 55px; + + &__link { + @include typography(tabLabel); + color: $neutral800; + text-decoration-line: underline; + text-underline-position: under; + + &:hover { + font-weight: bold; + } + } + } + + .col_bar { + border-left: 1px solid $neutral300; + margin: 4px 0; + } + .row_bar { + flex-grow: 1; + background: $neutral200; + height: 1px; + } +} diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx new file mode 100644 index 00000000..1e7663c2 --- /dev/null +++ b/src/pages/Login/Login.tsx @@ -0,0 +1,48 @@ +import { Link } from "react-router-dom"; + +import styles from "./Login.module.scss"; + +import LoginForm from "@/components/Auth/Login/LoginForm"; + +import KakaoIcon from "@/assets/kakao/kakao_path.svg?react"; +import Logo from "@/assets/logo.svg?react"; + +function Login() { + return ( +
+

+ +

+ + + +
+ 비밀번호를 잊으셨나요? +
+ 회원가입 +
+ +
+

+
+

SNS 계정으로 로그인

+
+

+ + +
+ +
+ + 로그인 전 둘러보기 + +
+
+ ); +} + +export default Login; diff --git a/src/routes/MainRouter/MainRouter.tsx b/src/routes/MainRouter/MainRouter.tsx index 8cf9c10e..b1d9f440 100644 --- a/src/routes/MainRouter/MainRouter.tsx +++ b/src/routes/MainRouter/MainRouter.tsx @@ -1,5 +1,7 @@ import { Route, Routes } from "react-router-dom"; + +import Login from "@/pages/Login/Login"; import Alarm from "@/pages/Alarm/Alarm"; import Detail from "@/pages/Detail/Detail"; import Home from "@/pages/Home/Home"; @@ -18,6 +20,7 @@ function MainRouter() { } /> } /> + } /> } /> } /> } /> diff --git a/src/sass/abstracts/_variables.scss b/src/sass/abstracts/_variables.scss index d656cacf..93404580 100644 --- a/src/sass/abstracts/_variables.scss +++ b/src/sass/abstracts/_variables.scss @@ -31,6 +31,7 @@ $neutral900: #1d2433; $neutral1000: #0a0d14; $etc0: #fed600; +$etc1: #fee500; //shadow $shadow100: 0px 0px 8px 0px rgba(20, 20, 20, 0.08), diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 00000000..2937d280 --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,25 @@ +import { UseFormRegister, UseFormResetField } from "react-hook-form"; + +interface SubmitResult { + try: boolean; + isPassed: boolean; +} + +interface LoginForm { + email: string; + password: string; +} + +interface LoginDirtyFields { + email?: boolean; + password?: boolean; +} + +interface LoginInput { + label: string; + dirtyFields: LoginDirtyFields; + register: UseFormRegister; + resetField: UseFormResetField; +} + +export type { LoginDirtyFields, LoginForm, LoginInput, SubmitResult };