diff --git a/package.json b/package.json index 5830ca3..08aec8d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "eslint-config-next": "13.4.19", "gsap": "^3.12.4", "next": "^14.0.3", + "next-auth": "^4.24.5", "pg": "^8.11.3", "punycode": "^2.3.1", "react": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2615b96..5d72e87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: next: specifier: ^14.0.3 version: 14.0.3(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0) + next-auth: + specifier: ^4.24.5 + version: 4.24.5(next@14.0.3)(react-dom@18.2.0)(react@18.2.0) pg: specifier: ^8.11.3 version: 8.11.3 @@ -923,6 +926,10 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@panva/hkdf@1.1.1: + resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==} + dev: false + /@playwright/test@1.40.1: resolution: {integrity: sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==} engines: {node: '>=16'} @@ -2261,6 +2268,11 @@ packages: /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + /create-jest@29.7.0(@types/node@20.5.7)(ts-node@10.9.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4254,6 +4266,10 @@ packages: - ts-node dev: true + /jose@4.15.4: + resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4605,6 +4621,31 @@ packages: /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + /next-auth@4.24.5(next@14.0.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==} + peerDependencies: + next: ^12.2.5 || ^13 || ^14 + nodemailer: ^6.6.5 + react: ^17.0.2 || ^18 + react-dom: ^17.0.2 || ^18 + peerDependenciesMeta: + nodemailer: + optional: true + dependencies: + '@babel/runtime': 7.23.5 + '@panva/hkdf': 1.1.1 + cookie: 0.5.0 + jose: 4.15.4 + next: 14.0.3(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0) + oauth: 0.9.15 + openid-client: 5.6.4 + preact: 10.19.3 + preact-render-to-string: 5.2.6(preact@10.19.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + uuid: 8.3.2 + dev: false + /next@14.0.3(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==} engines: {node: '>=18.17.0'} @@ -4734,11 +4775,20 @@ packages: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: true + /oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} dev: false + /object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + dev: false + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -4810,6 +4860,11 @@ packages: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} dev: false + /oidc-token-hash@5.0.3: + resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} + engines: {node: ^10.13.0 || >=12.0.0} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -4822,6 +4877,15 @@ packages: mimic-fn: 2.1.0 dev: true + /openid-client@5.6.4: + resolution: {integrity: sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==} + dependencies: + jose: 4.15.4 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.0.3 + dev: false + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -5132,6 +5196,19 @@ packages: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} dev: false + /preact-render-to-string@5.2.6(preact@10.19.3): + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + peerDependencies: + preact: '>=10' + dependencies: + preact: 10.19.3 + pretty-format: 3.8.0 + dev: false + + /preact@10.19.3: + resolution: {integrity: sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==} + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5154,6 +5231,10 @@ packages: ansi-styles: 5.2.0 react-is: 18.2.0 + /pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + dev: false + /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -6246,6 +6327,11 @@ packages: engines: {node: '>= 4'} dev: false + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + /uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true diff --git a/src/components/LoginForm/Form/Form.tsx b/src/components/LoginForm/Form/Form.tsx index 5f5b621..6c5367b 100644 --- a/src/components/LoginForm/Form/Form.tsx +++ b/src/components/LoginForm/Form/Form.tsx @@ -8,7 +8,7 @@ import LoginButton from "./LoginButton"; function Form() { const [id, setId] = React.useState(""); const [password, setPassword] = React.useState(""); - const [isIdValid, setIsIdValid] = React.useState({ + const [isValid, setIsValid] = React.useState({ id: true, password: true, }); @@ -16,13 +16,13 @@ function Form() { const InputProps = { setId, setPassword, - isIdValid, + isValid, }; const ButtonProps = { id, password, - setIsIdValid, + setIsValid, }; return ( diff --git a/src/components/LoginForm/Form/LoginButton/LoginButton.tsx b/src/components/LoginForm/Form/LoginButton/LoginButton.tsx index 8c376b4..d127f93 100644 --- a/src/components/LoginForm/Form/LoginButton/LoginButton.tsx +++ b/src/components/LoginForm/Form/LoginButton/LoginButton.tsx @@ -3,7 +3,7 @@ import * as Styled from "../Form.styled"; interface LoginButtonProps { id: string; password: string; - setIsIdValid: React.Dispatch< + setIsValid: React.Dispatch< React.SetStateAction<{ id: boolean; password: boolean; @@ -12,23 +12,23 @@ interface LoginButtonProps { } function LoginButton({ props }: { props: LoginButtonProps }) { - const { id, password, setIsIdValid } = props; + const { id, password, setIsValid } = props; return ( { if (id.length >= 6 && id.length <= 20) { - setIsIdValid((prev) => ({ ...prev, id: true })); + setIsValid((prev) => ({ ...prev, id: true })); } if (password.length >= 8) { - setIsIdValid((prev) => ({ ...prev, password: true })); + setIsValid((prev) => ({ ...prev, password: true })); } if (id.length < 6 || id.length > 20) { - setIsIdValid((prev) => ({ ...prev, id: false })); + setIsValid((prev) => ({ ...prev, id: false })); } else if (password.length < 8) { - setIsIdValid((prev) => ({ ...prev, password: false })); + setIsValid((prev) => ({ ...prev, password: false })); } }} > diff --git a/src/components/LoginForm/Form/LoginInput/LoginInput.tsx b/src/components/LoginForm/Form/LoginInput/LoginInput.tsx index 8dc143d..e0d060a 100644 --- a/src/components/LoginForm/Form/LoginInput/LoginInput.tsx +++ b/src/components/LoginForm/Form/LoginInput/LoginInput.tsx @@ -1,20 +1,24 @@ +"use client"; + +import { createPortal } from "react-dom"; import InvalidMessage from "@/components/InvalidMessage"; import Helper from "../Helper"; import Id from "./Id"; import Password from "./Password"; import * as Styled from "../Form.styled"; +import React from "react"; interface LoginInputProps { setId: React.Dispatch>; setPassword: React.Dispatch>; - isIdValid: { + isValid: { id: boolean; password: boolean; }; } function LoginInput({ props }: { props: LoginInputProps }) { - const { setId, setPassword, isIdValid } = props; + const { setId, setPassword } = props; return ( <> @@ -23,14 +27,15 @@ function LoginInput({ props }: { props: LoginInputProps }) { - {isIdValid.id ? null : ( - - )} - {isIdValid.password ? null : ( - - )} ); } export default LoginInput; + +// {isIdValid.id ? null : ( +// +// )} +// {isIdValid.password ? null : ( +// +// )} diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx index 285ccd8..1802063 100644 --- a/src/components/LoginForm/LoginForm.tsx +++ b/src/components/LoginForm/LoginForm.tsx @@ -7,21 +7,114 @@ import { createUserInfo, authenticate } from "@/lib/action"; import LoginInput from "./Form/LoginInput"; import LoginButton from "./Form/LoginButton"; import Form from "./Form/Form"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; +import { createPortal } from "react-dom"; +import InvalidMessage from "../InvalidMessage"; + +type LoginAction = { + type: "wrongId" | "wrongPassword" | "wrongLengthID" | "wrongLengthPassword"; +}; + +type ErrorMessage = { + message: string; +}; + +function reducer(errorMessage: ErrorMessage, action: LoginAction) { + switch (action.type) { + case "wrongId": + return { + message: "아이디가 존재하지 않습니다.", + }; + case "wrongPassword": + return { + message: "비밀번호 존재하지 않습니다.", + }; + case "wrongLengthID": + return { + message: "아이디는 4자 이상 20자 이하로 입력해주세요.", + }; + case "wrongLengthPassword": + return { + message: "비밀번호는 8자 이상 20자 이하로 입력해주세요.", + }; + default: + return { + message: "", + }; + } +} function LoginForm() { + const [id, setId] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [isValid, setIsValid] = React.useState({ + id: true, + password: true, + }); + + const [errorMessage, dispatch] = React.useReducer(reducer, { message: "" }); + const portalRef = React.useRef(null); + + const InputProps = { + setId, + setPassword, + isValid, + }; + + const ButtonProps = { + id, + password, + setIsValid, + }; + + React.useEffect(() => { + if (!isValid.id) { + dispatch({ + type: "wrongLengthID", + }); + } else if (!isValid.password) { + dispatch({ + type: "wrongLengthPassword", + }); + } + }, [isValid]); + return ( - { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - await authenticate(formData); - }} - > - -
- + loading...}> + { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const loginResult = await authenticate(formData); + + if (loginResult && !loginResult.success) { + dispatch({ type: loginResult.type as LoginAction["type"] }); + } + }} + > + + + +
+ {errorMessage.message.length > 0 + ? createPortal( + , + portalRef.current as HTMLDivElement + ) + : null} + + + + + + ); } diff --git a/tests/auth.ts b/tests/auth.ts new file mode 100644 index 0000000..e69de29