diff --git a/src/app/(user-access)/components/modal/Modal.module.css b/src/app/(user-access)/components/modal/Modal.module.css new file mode 100644 index 0000000..44aa445 --- /dev/null +++ b/src/app/(user-access)/components/modal/Modal.module.css @@ -0,0 +1,33 @@ +.modalContainer { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.modal { + width: 308px; + height: 152px; + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.message { + margin-top: 20px; + font-size: 18px; + font-weight: 500; + text-align: center; +} + +.button { + margin-top: 30px; + height: 40px; +} diff --git a/src/app/(user-access)/components/modal/Modal.tsx b/src/app/(user-access)/components/modal/Modal.tsx new file mode 100644 index 0000000..a528693 --- /dev/null +++ b/src/app/(user-access)/components/modal/Modal.tsx @@ -0,0 +1,35 @@ +import useModalStore from '../../modalStore/modalStore'; +import Button from '@/components/Button'; +import styles from './Modal.module.css'; +import { useRouter } from 'next/navigation'; + +interface AlertModalProps { + message: string; +} + +const AlertModal = ({ message }: AlertModalProps) => { + const { closeModal, messageType } = useModalStore(); + const router = useRouter(); + + const handleConfirm = () => { + closeModal(); + if (messageType === 'success') { + router.push('/login'); + } + }; + + return ( +
+
+

{message}

+
+
+ +
+
+ ); +}; + +export default AlertModal; diff --git a/src/app/(user-access)/components/modal/ModalContainer.tsx b/src/app/(user-access)/components/modal/ModalContainer.tsx new file mode 100644 index 0000000..7f5f534 --- /dev/null +++ b/src/app/(user-access)/components/modal/ModalContainer.tsx @@ -0,0 +1,17 @@ +import useModalStore from '../../modalStore/modalStore'; +import styles from './Modal.module.css'; +import AlertModal from './Modal'; + +const ModalContainer = () => { + const { isOpen, message } = useModalStore(); + + if (!isOpen) return null; + + return ( +
+ +
+ ); +}; + +export default ModalContainer; diff --git a/src/app/(user-access)/layout.module.css b/src/app/(user-access)/layout.module.css index 61e1788..c93b01d 100644 --- a/src/app/(user-access)/layout.module.css +++ b/src/app/(user-access)/layout.module.css @@ -5,20 +5,18 @@ align-items: center; min-height: 100vh; width: 100%; - background-color: var(--white); + background-color: var(--gray-100); } - .authContent { width: 100%; max-width: 400px; box-sizing: border-box; display: flex; justify-content: center; - background-color: var(--white); + background-color: var(--gray-100); border-radius: 8px; padding: 20px; } - .logoContainer { display: flex; flex-direction: column; @@ -26,12 +24,10 @@ align-items: center; margin: 16px 0; } - .logo { width: 100%; height: auto; } - @media screen and (min-width: 768px) { .authContent { max-width: 351px; diff --git a/src/api/auth/login.ts b/src/app/(user-access)/lib/loginService.ts similarity index 100% rename from src/api/auth/login.ts rename to src/app/(user-access)/lib/loginService.ts diff --git a/src/api/auth/signup.ts b/src/app/(user-access)/lib/signupService.ts similarity index 100% rename from src/api/auth/signup.ts rename to src/app/(user-access)/lib/signupService.ts diff --git a/src/app/(user-access)/login/loginPage.module.css b/src/app/(user-access)/login/loginPage.module.css index 6ae59b8..b0cba24 100644 --- a/src/app/(user-access)/login/loginPage.module.css +++ b/src/app/(user-access)/login/loginPage.module.css @@ -33,6 +33,7 @@ border-color: var(--violet); outline: none; } + .errorMessage { color: var(--red); font-size: 12px; @@ -49,3 +50,9 @@ font-size: 20px; text-align: center; } + +.signupLink { + color: var(--violet); + text-decoration: underline; + cursor: pointer; +} diff --git a/src/app/(user-access)/login/page.tsx b/src/app/(user-access)/login/page.tsx index e92f0c2..68ddc56 100644 --- a/src/app/(user-access)/login/page.tsx +++ b/src/app/(user-access)/login/page.tsx @@ -1,7 +1,6 @@ 'use client'; import React from 'react'; -import axios from 'axios'; import { useForm, FieldValues, UseFormReturn } from 'react-hook-form'; import { useRouter } from 'next/navigation'; import useAuthStore from '@/store/authStore'; @@ -9,6 +8,10 @@ import Button from '@/components/Button'; import { ERROR_MESSAGES } from '@/constants/message'; import type { User } from '@/types/user'; import styles from './loginPage.module.css'; +import ModalContainer from '../components/modal/ModalContainer'; +import useModalStore from '../modalStore/modalStore'; +import axios from 'axios'; +import { EMAIL_REGEX } from '@/constants/regex'; type LoginFormInputs = { email: string; @@ -33,6 +36,7 @@ export default function LoginPage() { const router = useRouter(); const { setUser } = useAuthStore(); + const { openModal } = useModalStore(); const onSubmit = async (data: LoginFormInputs) => { try { @@ -43,78 +47,76 @@ export default function LoginPage() { router.replace('/mydashboard'); } catch (error) { - console.error('로그인 실패:', error); - alert('비밀번호가 일치하지 않습니다.'); + openModal('비밀번호가 일치하지 않습니다.', 'error'); } }; return ( -
-
-

오늘도 만나서 반가워요!

- - - {errors.email && ( - {errors.email.message} - )} -
+
+ + +
+

오늘도 만나서 반가워요!

+ + + {errors.email && ( + {errors.email.message} + )} +
-
- - - {errors.password && ( -

{errors.password.message}

- )} -
- -
-

- 회원이 아니신가요?{' '} - router.push('/signup')} - > - 회원가입하기 - {' '} -

-
- +
+ + + {errors.password && ( +

{errors.password.message}

+ )} +
+ +
+

+ 회원이 아니신가요?{' '} + router.push('/signup')} + > + 회원가입하기 + +

+
+ +
); } diff --git a/src/app/(user-access)/modalStore/modalStore.ts b/src/app/(user-access)/modalStore/modalStore.ts new file mode 100644 index 0000000..b333325 --- /dev/null +++ b/src/app/(user-access)/modalStore/modalStore.ts @@ -0,0 +1,20 @@ +import { create } from 'zustand'; + +interface ModalState { + isOpen: boolean; + message: string; + messageType: string; + openModal: (message: string, messageType: string) => void; + closeModal: () => void; +} + +const useModalStore = create((set) => ({ + isOpen: false, + message: '', + messageType: '', + openModal: (message, messageType) => + set({ isOpen: true, message, messageType }), + closeModal: () => set({ isOpen: false, message: '', messageType: '' }), +})); + +export default useModalStore; diff --git a/src/app/(user-access)/signup/page.tsx b/src/app/(user-access)/signup/page.tsx index 0b98f2d..5d767df 100644 --- a/src/app/(user-access)/signup/page.tsx +++ b/src/app/(user-access)/signup/page.tsx @@ -1,3 +1,168 @@ -export default function Page() { - return
; +'use client'; + +import { useForm } from 'react-hook-form'; +import { useRouter } from 'next/navigation'; +import Button from '@/components/Button'; +import styles from './signPage.module.css'; +import axiosInstance from '@/lib/axiosInstance'; +import { ERROR_MESSAGES } from '@/constants/message'; +import ModalContainer from '../components/modal/ModalContainer'; +import useModalStore from '../modalStore/modalStore'; +import { EMAIL_REGEX } from '@/constants/regex'; + +type SignupFormInputs = { + email: string; + nickname: string; + password: string; + confirmPassword: string; + termsAccepted: boolean; +}; + +export default function SignupPage() { + const { + register, + handleSubmit, + formState: { errors, isValid }, + watch, + } = useForm({ mode: 'onChange' }); + const router = useRouter(); + + const watchPassword = watch('password'); + const { openModal } = useModalStore(); + + const onSubmit = async (data: SignupFormInputs) => { + try { + await axiosInstance.post('/users', { + email: data.email, + nickname: data.nickname, + password: data.password, + }); + openModal('가입이 완료되었습니다!', 'success'); + } catch (error) { + openModal('이미 사용 중인 이메일입니다.', 'error'); + } + }; + + return ( +
+ +
+
+

첫 방문을 환영합니다!

+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+ +
+ + + {errors.nickname && ( +

{errors.nickname.message}

+ )} +
+
+ + + {errors.password && ( +

{errors.password.message}

+ )} +
+
+ + + value === watchPassword || ERROR_MESSAGES.PASSWORDS_MATCH, + })} + /> + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+
+ + + {errors.termsAccepted && ( +

+ {errors.termsAccepted.message} +

+ )} +
+ +
+

+ 이미 회원이신가요?{' '} + router.push('/login')} + > + 로그인하기 + +

+
+
+
+ ); } diff --git a/src/app/(user-access)/signup/signPage.module.css b/src/app/(user-access)/signup/signPage.module.css index e69de29..7d4383a 100644 --- a/src/app/(user-access)/signup/signPage.module.css +++ b/src/app/(user-access)/signup/signPage.module.css @@ -0,0 +1,108 @@ +.signupContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 50vh; + width: 100%; + background-color: var(--gray-100); +} + +.signupForm { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + max-width: 400px; +} + +.submitButton { + height: 40px; +} + +.inputWrapper { + display: flex; + flex-direction: column; +} + +.label { + color: var(--black-100); + font-weight: bold; + margin-top: 24px; + margin-bottom: 14px; +} + +.input { + width: 100%; + padding: 8px; + border: 1px solid var(--gray-300); + border-radius: 8px; +} + +.inputError { + border-color: var(--red); +} + +.input:focus { + border-color: var(--violet); + outline: none; +} + +.errorMessage { + color: var(--red); + font-size: 12px; + margin-top: 10px; +} + +.disabled { + background-color: var(--gray-400); + cursor: not-allowed; + opacity: 0.5; +} + +.greeting { + font-size: 20px; + text-align: center; + margin-top: 30px; +} + +.termsAccepted { + display: flex; + align-items: center; + gap: 8px; +} + +.linkContainer { + display: flex; + justify-content: center; +} + +.linkText { + color: var(--violet); + text-decoration: underline; + cursor: pointer; +} + +.input[type='checkbox'] { + width: 16px; + height: 16px; + margin: 0; + border: 2px solid var(--gray-300); + border-radius: 2px; + background-color: white; + cursor: pointer; + position: relative; +} + +.input[type='checkbox']:checked { + background-color: var(--white); +} + +.input[type='checkbox']:checked::before { + content: '✔'; + position: absolute; + top: 0; + left: 3px; + font-size: 12px; + color: var(--violet); +} diff --git a/src/constants/message.ts b/src/constants/message.ts index c432e77..4082663 100644 --- a/src/constants/message.ts +++ b/src/constants/message.ts @@ -11,4 +11,8 @@ export const ERROR_MESSAGES = { DESCRIPTION_REQUIRE: '설명은 필수입니다.', REQUIRED_EMAIL: '이메일을 입력해 주세요.', INVALID_EMAIL: '이메일 형식으로 작성해 주세요.', + EMAIL_DUPLICATE: '이미 사용중인 이메일입니다.', + NICKNAME_REQUIRED: '닉네임을 입력해주세요.', + NICKNAME_TOO_LONG: '닉네임은 10자 이하로 입력해주세요.', + PASSWORD_CONFIRM_MISMATCH: '비밀번호가 일치하지 않습니다', }; diff --git a/src/constants/regex.ts b/src/constants/regex.ts new file mode 100644 index 0000000..ac89e7e --- /dev/null +++ b/src/constants/regex.ts @@ -0,0 +1 @@ +export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;