diff --git a/src/components/layout/header/nav.tsx b/src/components/layout/header/nav.tsx index 03d08c3..d81297f 100644 --- a/src/components/layout/header/nav.tsx +++ b/src/components/layout/header/nav.tsx @@ -31,7 +31,13 @@ const Nav = () => { {isLogin && ( <> - + + +

+ 아직 계정이 없으신가요?{' '} + + 회원가입 + +

+ + + + {/* 로그인 실패 모달 */} + setFailOpen(false)} + title='이메일 또는 비밀번호가 올바르지 않습니다' + description={

다시 한 번 확인해 주세요.

} + variant='warning' + primaryText='확인' + onPrimary={() => setFailOpen(false)} + /> + + ); +} diff --git a/src/pages/my-profile/index.tsx b/src/pages/my-profile/index.tsx new file mode 100644 index 0000000..c122c1e --- /dev/null +++ b/src/pages/my-profile/index.tsx @@ -0,0 +1,156 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useMemo, useState } from 'react'; + +import Frame from '@/components/layout/frame/frame'; +import Button from '@/components/ui/button/button'; +import Table from '@/components/ui/table/Table'; +import type { TableRowProps } from '@/components/ui/table/TableRowProps'; +import { ADDRESS_CODE, type AddressCode } from '@/constants/dropdown'; +import { ICONS, ICON_SIZES } from '@/constants/icon'; +import useAuth from '@/hooks/useAuth'; +import type { UserType } from '@/types/user'; // 'employee' | 'employer' + +/** 임시 프로필 타입 — API 붙이면 서버 타입으로 교체 */ +type Profile = { + name: string; + phone: string; + region: AddressCode | ''; + bio?: string; +}; + +/** ADDRESS_CODE는 문자열 배열 → 포함되면 그대로 표기 */ +function renderAddress(code?: AddressCode | ''): string { + return code && (ADDRESS_CODE as readonly AddressCode[]).includes(code) ? code : '—'; +} + +export default function MyProfileDetailPage() { + const { isLogin, user } = useAuth(); + + // 임시 저장 기준 — 추후 API로 교체 가능 + const [profile, setProfile] = useState({ name: '', phone: '', region: '' }); + const [applications, setApplications] = useState([]); + const [isLoadingApps, setIsLoadingApps] = useState(true); + + const profileKey = useMemo(() => `thejulge_profile_${user?.id ?? 'guest'}`, [user?.id]); + const appsKey = useMemo(() => `thejulge_apps_${user?.id ?? 'guest'}`, [user?.id]); + + useEffect(() => { + if (!isLogin) return; + try { + const txt = localStorage.getItem(profileKey); + if (txt) setProfile(JSON.parse(txt) as Profile); + } catch { + /* ignore */ + } + }, [isLogin, profileKey]); + + useEffect(() => { + if (!isLogin) return; + setIsLoadingApps(true); + try { + const txt = localStorage.getItem(appsKey); + const parsed = txt ? (JSON.parse(txt) as TableRowProps[]) : []; + setApplications(parsed); + } catch { + setApplications([]); + } finally { + setIsLoadingApps(false); + } + }, [isLogin, appsKey]); + + const headers: string[] = ['가게명', '근무일시', '시급', '상태']; + const userType: UserType = 'employee'; + + return ( +
+ {/* ✅ 데스크톱에서 고정폭 컨테이너(957px) 안에 제목 + 카드 모두 배치해서 안정화 */} +
+

내 프로필

+ + {/* 프로필 카드 — 배경: --red-100, 보더: --red-300, 라운드 24px */} +
+
+ {/* 왼쪽 정보 */} +
+

이름

+

+ {profile.name || '—'} +

+ + {/* 연락처 */} +
+ 전화 + {profile.phone || '—'} +
+ + {/* 선호 지역 */} +
+ 지도 + 선호 지역: {renderAddress(profile.region)} +
+ + {/* 소개 — 선호지역과 24px 간격 */} + {profile.bio && ( +

+ {profile.bio} +

+ )} +
+ + {/* 우상단 편집 버튼 — 169×48 */} +
+ +
+
+
+
+ + {/* 신청 내역 — 0건이면 Frame, ≥1건이면 타이틀 + Table / 데스크톱 964px 중앙 */} +
+ {isLoadingApps ? ( +
불러오는 중…
+ ) : applications.length === 0 ? ( +
+ +
+ ) : ( +
+

신청 내역

+ + + )} + + + ); +} diff --git a/src/pages/my-profile/register.tsx b/src/pages/my-profile/register.tsx new file mode 100644 index 0000000..b101532 --- /dev/null +++ b/src/pages/my-profile/register.tsx @@ -0,0 +1,228 @@ +import { useRouter } from 'next/router'; +import { useEffect, useMemo, useState } from 'react'; + +import Button from '@/components/ui/button/button'; +import Dropdown from '@/components/ui/dropdown/dropdown'; +import Input from '@/components/ui/input/input'; +import Modal from '@/components/ui/modal/modal'; +import useAuth from '@/hooks/useAuth'; + +import { ADDRESS_CODE, type AddressCode } from '@/constants/dropdown'; + +/** 스토리지 헬퍼 */ +function makeProfileStorageKey(userId?: string | null) { + return `thejulge_profile_${userId ?? 'guest'}`; +} +function readJsonFromStorage(key: string): T | null { + try { + const text = localStorage.getItem(key); + if (!text) return null; + return JSON.parse(text) as T; + } catch { + return null; + } +} +function writeJsonToStorage(key: string, value: T) { + localStorage.setItem(key, JSON.stringify(value)); +} + +/** 폼 타입 */ +type ProfileForm = { + name: string; + phone: string; + region: AddressCode | ''; + bio: string; +}; + +export default function MyProfileRegisterPage() { + const router = useRouter(); + const { isLogin, user } = useAuth(); + + const profileStorageKey = useMemo(() => makeProfileStorageKey(user?.id), [user?.id]); + + const [formState, setFormState] = useState({ + name: '', + phone: '', + region: '', + bio: '', + }); + + const [nameErrorMessage, setNameErrorMessage] = useState(null); + const [phoneErrorMessage, setPhoneErrorMessage] = useState(null); + const [regionErrorMessage, setRegionErrorMessage] = useState(null); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDoneOpen, setIsDoneOpen] = useState(false); // ✅ 등록 완료 모달 + + // 로그인 가드 + useEffect(() => { + if (!isLogin) { + router.replace('/login'); + } + }, [isLogin, router]); + + // 기존 저장값 로드(수정 모드) + useEffect(() => { + if (!isLogin) return; + const saved = readJsonFromStorage(profileStorageKey); + if (saved) setFormState(saved); + }, [isLogin, profileStorageKey]); + + const updateFormField = (fieldName: K, value: ProfileForm[K]) => { + setFormState(prev => ({ ...prev, [fieldName]: value })); + }; + + const isFormSubmittable = + !!formState.name && + !!formState.phone && + !!formState.region && + !nameErrorMessage && + !phoneErrorMessage && + !regionErrorMessage && + !isSubmitting; + + const handleSubmit: React.FormEventHandler = e => { + e.preventDefault(); + + if (!formState.name.trim()) setNameErrorMessage('이름을 입력해 주세요.'); + if (!/^0\d{1,2}-\d{3,4}-\d{4}$/.test(formState.phone.trim())) + setPhoneErrorMessage('연락처 형식(010-1234-5678)으로 입력해 주세요.'); + if (!formState.region) setRegionErrorMessage('선호 지역을 선택해 주세요.'); + + if (!isFormSubmittable || !user) return; + + setIsSubmitting(true); + try { + writeJsonToStorage(profileStorageKey, { + name: formState.name.trim(), + phone: formState.phone.trim(), + region: formState.region, + bio: formState.bio, + }); + setIsDoneOpen(true); // ✅ 모달 오픈 + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+

내 프로필

+ +
+
+ {/* 이름 */} +
+ { + updateFormField('name', e.target.value); + if (nameErrorMessage) setNameErrorMessage(null); + }} + onBlur={() => + setNameErrorMessage(formState.name.trim() ? null : '이름을 입력해 주세요.') + } + required + error={nameErrorMessage ?? undefined} + /> +
+ + {/* 연락처 */} +
+ { + updateFormField('phone', e.target.value); + if (phoneErrorMessage) setPhoneErrorMessage(null); + }} + onBlur={() => + setPhoneErrorMessage( + /^0\d{1,2}-\d{3,4}-\d{4}$/.test(formState.phone.trim()) + ? null + : '연락처 형식(010-1234-5678)으로 입력해 주세요.' + ) + } + required + error={phoneErrorMessage ?? undefined} + /> +
+ + {/* 선호 지역 */} +
+ + + name='region' + ariaLabel='선호 지역 선택' + values={ADDRESS_CODE} + selected={formState.region || undefined} + onChange={value => { + updateFormField('region', value); + if (regionErrorMessage) setRegionErrorMessage(null); + }} + size='md' + placeholder='선택' + className='w-full' + /> + {regionErrorMessage && ( +

{regionErrorMessage}

+ )} +
+
+ + {/* 소개(선택) */} +
+ +