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 (
+
+ );
+}
+
+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 (
+
+ );
+}
+
+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 (
+
+ );
+}
+
+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 };