diff --git a/package.json b/package.json index fe6b338..0e065c9 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,16 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@vanilla-extract/css": "^1.15.3", "@vanilla-extract/sprinkles": "^1.6.3", "classnames": "^2.5.1", + "immer": "^10.1.1", "next": "14.2.5", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.53.1", + "zod": "^3.23.8", "zustand": "^4.5.5" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e46b41..5e763e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.53.1(react@18.3.1)) '@vanilla-extract/css': specifier: ^1.15.3 version: 1.15.5 @@ -17,6 +20,9 @@ importers: classnames: specifier: ^2.5.1 version: 2.5.1 + immer: + specifier: ^10.1.1 + version: 10.1.1 next: specifier: 14.2.5 version: 14.2.5(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -26,9 +32,15 @@ importers: react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.53.1 + version: 7.53.1(react@18.3.1) + zod: + specifier: ^3.23.8 + version: 3.23.8 zustand: specifier: ^4.5.5 - version: 4.5.5(@types/react@18.3.5)(react@18.3.1) + version: 4.5.5(@types/react@18.3.5)(immer@10.1.1)(react@18.3.1) devDependencies: '@types/node': specifier: ^20 @@ -446,6 +458,11 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@hookform/resolvers@3.9.0': + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -1423,6 +1440,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1862,6 +1882,12 @@ packages: peerDependencies: react: ^18.3.1 + react-hook-form@7.53.1: + resolution: {integrity: sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2266,6 +2292,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zustand@4.5.5: resolution: {integrity: sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==} engines: {node: '>=12.7.0'} @@ -2577,6 +2606,10 @@ snapshots: '@eslint/js@8.57.0': {} + '@hookform/resolvers@3.9.0(react-hook-form@7.53.1(react@18.3.1))': + dependencies: + react-hook-form: 7.53.1(react@18.3.1) + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -3447,7 +3480,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -3478,7 +3511,7 @@ snapshots: is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -3496,7 +3529,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -3814,6 +3847,8 @@ snapshots: ignore@5.3.2: {} + immer@10.1.1: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -4245,6 +4280,10 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-hook-form@7.53.1(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react@18.3.1: @@ -4723,9 +4762,12 @@ snapshots: yocto-queue@0.1.0: {} - zustand@4.5.5(@types/react@18.3.5)(react@18.3.1): + zod@3.23.8: {} + + zustand@4.5.5(@types/react@18.3.5)(immer@10.1.1)(react@18.3.1): dependencies: use-sync-external-store: 1.2.2(react@18.3.1) optionalDependencies: '@types/react': 18.3.5 + immer: 10.1.1 react: 18.3.1 diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx new file mode 100644 index 0000000..3b4b813 --- /dev/null +++ b/src/app/auth/signup/page.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { flexSprinklesFc } from "@/app/components/common/utils/flex"; +import { signupContainer, signupWrapper } from "./signup.css"; +import { inputStyle } from "@/app/styles/common/input.css"; +import { caption, heading2, regular, semiBold } from "@/app/styles/font.css"; +import { paddingSprinkles } from "@/app/styles/padding.css"; +import { colors, pink80 } from "@/app/styles/colors.css"; +import Image from "next/image"; +import { pointer } from "@/app/styles/global.css"; +import { useRouter } from "next/navigation"; +import Button from "@/app/components/common/Button"; +import { useForm } from "react-hook-form"; +import { signupSchema } from "@/app/types/signupSchema"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import Popup from "@/app/components/common/Popup"; +import { LocalStorage } from "@/app/types/localStorageSchema"; + +const Page = () => { + const router = useRouter(); + const [submit, setSubmit] = useState(false); + + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm>({ + resolver: zodResolver(signupSchema), + mode: "onChange", + defaultValues: { + id: "", + password: "", + confirmPassword: "", + nickname: "", + }, + }); + + const onSubmit = (data: z.infer) => { + if (data) { + const signupStorage = new LocalStorage("signup"); + + setSubmit(true); + signupStorage.set(data); + } + }; + + return ( + <> + {submit && ( + + ); }; diff --git a/src/app/components/common/Popup.tsx b/src/app/components/common/Popup.tsx index 9fed3b6..3ea7e20 100644 --- a/src/app/components/common/Popup.tsx +++ b/src/app/components/common/Popup.tsx @@ -1,8 +1,11 @@ import { popupContents, popupWrapper } from "@/app/styles/common/popup.css"; import { paragraph2, semiBold } from "@/app/styles/font.css"; import { flexSprinklesFc } from "./utils/flex"; +import { z } from "zod"; -const Popup = ({ text, children }: { text: string; children: React.ReactNode }) => { +const textSchema = z.string().nullable(); + +const Popup = ({ text, children }: { text: z.infer; children: React.ReactNode }) => { return (
diff --git a/src/app/components/record/DateCircle.tsx b/src/app/components/record/DateCircle.tsx index 7a1895d..3c29bbc 100644 --- a/src/app/components/record/DateCircle.tsx +++ b/src/app/components/record/DateCircle.tsx @@ -1,4 +1,4 @@ -import { circle } from "@/app/styles/record/record.css"; +import { circle } from "@/app/record/record.css"; const DateCircle = ({ date }: { date: string }) => { return
{date}
; diff --git a/src/app/styles/info/common.css.ts b/src/app/info/common.css.ts similarity index 100% rename from src/app/styles/info/common.css.ts rename to src/app/info/common.css.ts diff --git a/src/app/info/detail/page.tsx b/src/app/info/detail/page.tsx index 3ab9426..b77d93c 100644 --- a/src/app/info/detail/page.tsx +++ b/src/app/info/detail/page.tsx @@ -5,10 +5,10 @@ import Memo from "@/app/components/common/Memo"; import { flexSprinklesFc } from "@/app/components/common/utils/flex"; import { gray300, colors } from "@/app/styles/colors.css"; import { semiBold, heading2, caption } from "@/app/styles/font.css"; -import { infoContainer } from "@/app/styles/info/common.css"; import { useRouter } from "next/navigation"; +import { infoContainer } from "../common.css"; -const page = () => { +const Page = () => { const router = useRouter(); return (
@@ -39,4 +39,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/app/info/layout.tsx b/src/app/info/layout.tsx index 29c2e92..3f03ace 100644 --- a/src/app/info/layout.tsx +++ b/src/app/info/layout.tsx @@ -1,10 +1,10 @@ "use client"; import { useRouter } from "next/navigation"; -import { infoWrapper } from "../styles/info/common.css"; import Image from "next/image"; import { pointer } from "../styles/global.css"; import { paddingSprinkles } from "../styles/padding.css"; +import { infoWrapper } from "./common.css"; export default function Layout({ children }: { children: React.ReactNode }) { const router = useRouter(); diff --git a/src/app/info/shape/page.tsx b/src/app/info/shape/page.tsx index 2fa9349..97478dd 100644 --- a/src/app/info/shape/page.tsx +++ b/src/app/info/shape/page.tsx @@ -4,11 +4,10 @@ import Button from "@/app/components/common/Button"; import { flexSprinklesFc } from "@/app/components/common/utils/flex"; import { colors, gray300, primary, white } from "@/app/styles/colors.css"; import { heading2, semiBold } from "@/app/styles/font.css"; -import { infoBox } from "@/app/styles/info/common.css"; -import { shapeContentBox, shapeContentBoxWrapper, shapeContentBoxText } from "@/app/styles/info/shape.css"; import { paddingSprinkles } from "@/app/styles/padding.css"; import Image, { ImageProps } from "next/image"; import { useRouter } from "next/navigation"; +import { shapeContentBoxWrapper, shapeContentBox, shapeContentBoxText } from "./shape.css"; type ContentBoxProps = { src: string; @@ -29,11 +28,11 @@ const ContentBox = ({ src, text, width, height, active }: ContentBoxProps) => { ); }; -const page = () => { +const Page = () => { const router = useRouter(); return ( -
+

묽기 및 모양을 @@ -110,4 +109,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/app/styles/info/shape.css.ts b/src/app/info/shape/shape.css.ts similarity index 84% rename from src/app/styles/info/shape.css.ts rename to src/app/info/shape/shape.css.ts index 5d5ee95..c23059a 100644 --- a/src/app/styles/info/shape.css.ts +++ b/src/app/info/shape/shape.css.ts @@ -1,6 +1,6 @@ +import { colors } from "@/app/styles/colors.css"; +import { regular, caption } from "@/app/styles/font.css"; import { style } from "@vanilla-extract/css"; -import { colors } from "../colors.css"; -import { caption, regular } from "../font.css"; export const shapeContent = style({ display: "flex", diff --git a/src/app/info/time/page.tsx b/src/app/info/time/page.tsx index 366d3c7..0e35e15 100644 --- a/src/app/info/time/page.tsx +++ b/src/app/info/time/page.tsx @@ -4,10 +4,10 @@ import Button from "@/app/components/common/Button"; import { flexSprinklesFc } from "@/app/components/common/utils/flex"; import { colors, gray300 } from "@/app/styles/colors.css"; import { caption, heading2, semiBold } from "@/app/styles/font.css"; -import { infoContainer } from "@/app/styles/info/common.css"; import { useRouter } from "next/navigation"; +import { infoContainer } from "../common.css"; -const page = () => { +const Page = () => { const router = useRouter(); return ( @@ -47,4 +47,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/app/styles/info/time.css.ts b/src/app/info/time/time.css.ts similarity index 100% rename from src/app/styles/info/time.css.ts rename to src/app/info/time/time.css.ts diff --git a/src/app/login/components/loginForm.tsx b/src/app/login/components/loginForm.tsx new file mode 100644 index 0000000..0474e7b --- /dev/null +++ b/src/app/login/components/loginForm.tsx @@ -0,0 +1,80 @@ +import { flexSprinklesFc } from "@/app/components/common/utils/flex"; +import { gray300, pink80 } from "@/app/styles/colors.css"; +import { inputStyle } from "@/app/styles/common/input.css"; +import { caption, caption2 } from "@/app/styles/font.css"; +import { paddingSprinkles } from "@/app/styles/padding.css"; +import { formBox } from "../styles/login.css"; +import { useRouter } from "next/navigation"; + +import { signinSchema } from "@/app/types/signinSchema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { pointer, buttonOutLine } from "@/app/styles/global.css"; + +import { useAuth } from "../hook"; + +const LoginForm = () => { + const router = useRouter(); + const { onLoginSubmit } = useAuth(); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm>({ + resolver: zodResolver(signinSchema), + mode: "onChange", + defaultValues: { + id: "", + password: "", + }, + }); + + return ( + <> +
+
+
+ + +

+ {errors.id?.message} +

+
+ +
+ +

+ {errors.password?.message} +

+
+
+
+ + {/* */} +

+ {" "} + |{" "} + +

+ + ); +}; + +export default LoginForm; diff --git a/src/app/login/components/loginPopup.tsx b/src/app/login/components/loginPopup.tsx new file mode 100644 index 0000000..41db7fa --- /dev/null +++ b/src/app/login/components/loginPopup.tsx @@ -0,0 +1,29 @@ +import Button from "@/app/components/common/Button"; +import Popup from "@/app/components/common/Popup"; +import { useLoginUiStore } from "@/app/store/login/loginStore"; +import { colors } from "@/app/styles/colors.css"; + +const LoginPopup = () => { + const { setLoginMessage, setIsLoginPopup } = useLoginUiStore(); + const loginMessageState = useLoginUiStore((state) => state.loginMessage); + const isLoginPopupState = useLoginUiStore((state) => state.isLoginPopup); + return ( + <> + {isLoginPopupState && ( + +

- 로그아웃 | {""} - 회원탈퇴 + {" "} + | {""} +

); }; -export default page; +export default Page; diff --git a/src/app/page.tsx b/src/app/page.tsx index 6411a6f..4760acd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,99 +1,7 @@ -"use client"; +import LoginPage from "./login/loginPage"; -import Image from "next/image"; -import Logo from "@svgs/logo.svg"; +const page = () => { + return ; +}; -import { loginTextBox, loginHeading, loginCaption, loginWrapper, loginLogo } from "@styles/login/login.css"; -import { useRouter } from "next/navigation"; -import { flexSprinklesFc } from "./components/common/utils/flex"; -import { inputStyle } from "./styles/common/input.css"; -import { gray300 } from "./styles/colors.css"; -import { caption2 } from "./styles/font.css"; -import { pointer } from "./styles/global.css"; - -// type LoginBoxProps = { -// image: string; -// text: string; -// bg: string; -// textColor: string; -// border?: boolean; -// onClick: MouseEventHandler; -// }; - -// const LoginBox = ({ image, text, bg, textColor, border, onClick }: LoginBoxProps) => { -// return ( -//
-//
-// {text} -//

{text}

-//
-//
-// ); -// }; - -export default function Login() { - const router = useRouter(); - - const logoClassName = flexSprinklesFc({ - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - gap: "24px", - }); - return ( -
-
- logo -
-

Eungeori

-

건강한 습관을 위한 스마트 앱

-
- -
- - - -
- -

- 로그인 | 회원가입 -

-
- - {/* router.push("/record")} - /> - router.push("/record")} - /> - router.push("/record")} - border - /> */} -
- ); -} +export default page; diff --git a/src/app/record/layout.tsx b/src/app/record/layout.tsx index 2db5ccf..77f2bbe 100644 --- a/src/app/record/layout.tsx +++ b/src/app/record/layout.tsx @@ -1,5 +1,5 @@ import Navigation from "../components/common/Navigation"; -import { recordWrapper, recordContainer } from "../styles/record/record.css"; +import { recordWrapper, recordContainer } from "./record.css"; const layout = ({ children }: { children: React.ReactNode }) => { return ( diff --git a/src/app/record/page.tsx b/src/app/record/page.tsx index 18c8292..10f42e4 100644 --- a/src/app/record/page.tsx +++ b/src/app/record/page.tsx @@ -3,9 +3,7 @@ import Image from "next/image"; import { caption2, paragraph, paragraph3, semiBold } from "../styles/font.css"; import { colors, gray150, gray300, gray400 } from "../styles/colors.css"; -import { plusIcon, plusIconBox, recordDate, recordDateSection } from "../styles/record/record.css"; import { paddingSprinkles } from "../styles/padding.css"; -import DateCircle from "../components/record/DateCircle"; import Memo from "../components/common/Memo"; import Popup from "../components/common/Popup"; import { useRouter } from "next/navigation"; @@ -13,8 +11,10 @@ import { flexSprinklesFc } from "../components/common/utils/flex"; import Button from "../components/common/Button"; import { useState } from "react"; import { pointer } from "../styles/global.css"; +import { recordDateSection, recordDate, plusIconBox, plusIcon } from "./record.css"; +import DateCircle from "../components/record/DateCircle"; -const page = () => { +const Page = () => { const router = useRouter(); const [click, setClick] = useState(false); return ( @@ -398,4 +398,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/app/styles/record/record.css.ts b/src/app/record/record.css.ts similarity index 87% rename from src/app/styles/record/record.css.ts rename to src/app/record/record.css.ts index aa18d7d..164d810 100644 --- a/src/app/styles/record/record.css.ts +++ b/src/app/record/record.css.ts @@ -1,7 +1,7 @@ import { style } from "@vanilla-extract/css"; -import { paddingSprinkles } from "../padding.css"; -import { colors } from "../colors.css"; -import { paragraph3, semiBold } from "../font.css"; +import { colors } from "../styles/colors.css"; +import { semiBold, paragraph3 } from "../styles/font.css"; +import { paddingSprinkles } from "../styles/padding.css"; export const circle = style([ semiBold, diff --git a/src/app/store/login/loginStore.ts b/src/app/store/login/loginStore.ts new file mode 100644 index 0000000..3575f54 --- /dev/null +++ b/src/app/store/login/loginStore.ts @@ -0,0 +1,27 @@ +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +type LoginUiStore = { + isLoginPopup: boolean; + setIsLoginPopup: (state: boolean) => void; + + loginMessage: null | string; + setLoginMessage: (state: string) => void; +}; + +export const useLoginUiStore = create()( + immer((set) => ({ + isLoginPopup: false, + setIsLoginPopup: (state) => { + set((draft) => { + draft.isLoginPopup = state; + }); + }, + loginMessage: null, + setLoginMessage: (state) => { + set((draft) => { + draft.loginMessage = state; + }); + }, + })) +); diff --git a/src/app/styles/colors.css.ts b/src/app/styles/colors.css.ts index 02911cd..730c4fc 100644 --- a/src/app/styles/colors.css.ts +++ b/src/app/styles/colors.css.ts @@ -11,6 +11,7 @@ export const colors = { gray300: "#AEB0B2", gray400: "#B8B8B8", gray500: "#9B9B9B", + pink80: "#ee6a7b", background: "#F5F5F5", }; @@ -54,6 +55,10 @@ export const gray500 = style({ color: colors.gray500, }); +export const pink80 = style({ + color: colors.pink80, +}); + export const background = style({ color: colors.background, }); diff --git a/src/app/styles/common/button.css.ts b/src/app/styles/common/button.css.ts index f5c08a1..a741ce9 100644 --- a/src/app/styles/common/button.css.ts +++ b/src/app/styles/common/button.css.ts @@ -5,11 +5,11 @@ import { style } from "@vanilla-extract/css"; const buttonProperties = defineProperties({ properties: { - width: ["115px", "226px", "343px"], + width: ["115px", "226px", "343px", "100%"], height: ["38px", "59px"], fontSize: ["12px", "16px", "18px"], color: [colors.primary, colors.white], - background: [colors.primary, colors.white30], + background: [colors.primary, colors.white30, colors.point, colors.gray200], borderRadius: ["5px", "10px"], }, shorthands: { diff --git a/src/app/styles/common/input.css.ts b/src/app/styles/common/input.css.ts index 320940b..de53543 100644 --- a/src/app/styles/common/input.css.ts +++ b/src/app/styles/common/input.css.ts @@ -1,11 +1,10 @@ import { style } from "@vanilla-extract/css"; import { colors } from "../colors.css"; -import { caption } from "../font.css"; export const inputStyle = style({ width: "100%", padding: "4px 8px", - borderRadius: "4px", + borderRadius: "8px", border: `1px solid ${colors.gray200}`, selectors: { "&::placeholder": { diff --git a/src/app/styles/global.css.ts b/src/app/styles/global.css.ts index 25514fe..d59b70b 100644 --- a/src/app/styles/global.css.ts +++ b/src/app/styles/global.css.ts @@ -21,3 +21,10 @@ export const contentContainer = style({ export const pointer = style({ cursor: "pointer", }); + +export const buttonOutLine = style({ + ":focus": { + outline: "2px solid #005fcc", + outlineOffset: "2px", + }, +}); diff --git a/src/app/types/localStorageSchema.ts b/src/app/types/localStorageSchema.ts new file mode 100644 index 0000000..81878f3 --- /dev/null +++ b/src/app/types/localStorageSchema.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; +import { signinSchema } from "./signinSchema"; +import { signupSchema } from "./signupSchema"; + +export type LocalStorageSchema = { + signup: z.infer; + signin: z.infer; + goal: string; +}; + +type LocalStorageMapper = { + fromJson: (json: string | null) => T; + toJson: (data: T) => string; +}; + +export class LocalStorage { + private key: T; + private mapper: LocalStorageMapper; + + constructor(key: T, mapper?: LocalStorageMapper) { + this.key = key; + this.mapper = mapper || LocalStorage.defaultMapper(); + } + + static defaultMapper(): LocalStorageMapper { + return { + fromJson: (json) => (json ? JSON.parse(json) : null), + toJson: (data) => JSON.stringify(data), + }; + } + + get(): LocalStorageSchema[T] | null { + const item = localStorage.getItem(this.key); + return item ? this.mapper.fromJson(item) : null; + } + + set(target: LocalStorageSchema[T]): void { + const value = this.mapper.toJson(target); + localStorage.setItem(this.key, value); + } + + remove(): void { + localStorage.removeItem(this.key); + } +} diff --git a/src/app/types/signinSchema.ts b/src/app/types/signinSchema.ts new file mode 100644 index 0000000..ac60e53 --- /dev/null +++ b/src/app/types/signinSchema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; +import { passwordSchema } from "./signupSchema"; + +export const signinSchema = z.object({ + id: z + .string() + .min(6, { message: "최소 6자 이상입니다." }) + .max(10, { message: "최대 10자 이하로 입력해주세요." }) + .regex(/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9]{6,10}$/, { + message: "6자 이상 10자 이하의 영문 혹은 영문과 숫자를 조합해주세요.", + }), + password: passwordSchema, +}); diff --git a/src/app/types/signupSchema.ts b/src/app/types/signupSchema.ts new file mode 100644 index 0000000..eb77957 --- /dev/null +++ b/src/app/types/signupSchema.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const passwordSchema = z + .string() + .min(8, { message: "최소 8자 이상 입력해주세요." }) + .max(12, { message: "최대 12자 이하로 입력해주세요." }) + .regex(/[A-Z]/, { message: "비밀번호에는 최소 하나의 대문자가 포함되어야 합니다." }) + .regex(/[a-z]/, { message: "비밀번호에는 최소 하나의 소문자가 포함되어야 합니다." }) + .regex(/[0-9]/, { message: "비밀번호에는 최소 하나의 숫자가 포함되어야 합니다." }) + .regex(/[@$!%*?&]/, { message: "비밀번호에는 최소 하나의 특수 문자가 포함되어야 합니다." }); + +export const signupSchema = z + .object({ + id: z + .string() + .min(6, { message: "최소 6자 이상입니다." }) + .max(10, { message: "최대 10자 이하로 입력해주세요." }) + .regex(/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9]{6,10}$/, { + message: "6자 이상 10자 이하의 영문 혹은 영문과 숫자를 조합해주세요.", + }), + password: passwordSchema, + confirmPassword: z.string(), + nickname: z.string().min(3, { message: "닉네임은 최소 3자 이상입니다." }), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "비밀번호가 일치하지 않습니다.", + path: ["confirmPassword"], + });