/** * 추가정보 입력 / 프로필 수정 (PRD §3.5.3, §3.6.3) * mode="signup": 회원가입 직후 (/signup/profile) — 상단 타이틀 + 저장 버튼 * mode="edit": 마이페이지에서 수정 (/edit-profile) — Layout 내부, 타이틀 변경 + 저장 버튼 * Phase D-3: MD3 디자인 토큰 + 공용 컴포넌트(PillButton, InfoBanner, StatusBadge, MatIcon) 적용 */ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { http } from '../lib/api'; import { useAuthStore } from '../store/auth'; import PillButton from '../components/PillButton'; import InfoBanner from '../components/InfoBanner'; import MatIcon from '../components/MatIcon'; const Container = styled.div` padding-top: 16px; `; const PageTitle = styled.h1` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 24px; font-weight: 800; color: var(--md3-on-surface); margin: 0 0 24px; `; const Card = styled.div` background: var(--md3-surface); border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06); border: 1px solid var(--md3-outline-variant); `; const SectionTitle = styled.h3` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 16px; font-weight: 700; color: var(--md3-on-surface); margin: 0 0 16px; `; const Field = styled.input` width: 100%; padding: 14px 16px; border: 1px solid var(--md3-outline); border-radius: 12px; font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 15px; color: var(--md3-on-surface); background: var(--md3-surface); box-sizing: border-box; transition: border-color 0.15s, box-shadow 0.15s; &::placeholder { color: var(--md3-on-surface-variant); opacity: 0.6; } &:focus { outline: none; border-color: var(--md3-primary); box-shadow: 0 0 0 3px var(--md3-primary-container); } &:disabled { background: var(--md3-surface-container-highest); color: var(--md3-on-surface-variant); } `; const Select = styled.select` width: 100%; padding: 14px 16px; border: 1px solid var(--md3-outline); border-radius: 12px; font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 15px; color: var(--md3-on-surface); background: var(--md3-surface); box-sizing: border-box; transition: border-color 0.15s, box-shadow 0.15s; appearance: none; &:focus { outline: none; border-color: var(--md3-primary); box-shadow: 0 0 0 3px var(--md3-primary-container); } `; const Row = styled.div` display: flex; gap: 8px; ${Field} { flex: 1; } `; const CheckBtn = styled.button` display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 0 14px; height: 48px; border: none; border-radius: 9999px; background: var(--md3-primary); color: var(--md3-on-primary); font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 12px; font-weight: 600; letter-spacing: 0.02em; white-space: nowrap; flex-shrink: 0; cursor: pointer; transition: background-color 0.15s, transform 0.1s; box-shadow: 0 4px 12px rgba(0, 104, 95, 0.25); &:hover:not(:disabled) { background: var(--md3-primary-container); color: var(--md3-primary); } &:active:not(:disabled) { transform: scale(0.98); } &:disabled { opacity: 0.45; cursor: not-allowed; background: var(--md3-outline-variant); color: var(--md3-on-surface-variant); box-shadow: none; } `; const MsgText = styled.p<{ $type: 'success' | 'error' }>` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 12px; margin-top: 6px; color: ${(p) => (p.$type === 'success' ? 'var(--md3-success, #2ecc71)' : 'var(--md3-error, #e74c3c)')}; `; const GenderRow = styled.div` display: flex; gap: 8px; button { flex: 1; padding: 14px; border: 1px solid var(--md3-outline); border-radius: 12px; background: var(--md3-surface); font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 15px; font-weight: 600; color: var(--md3-on-surface); transition: all 0.15s; &.on { border-color: var(--md3-primary); color: var(--md3-primary); background: var(--md3-primary-container); } } `; const ErrorBanner = styled(InfoBanner)` && { margin-top: 16px; } `; const SubmitButton = styled(PillButton)` && { width: 100%; margin-top: 24px; } `; const COUNTRIES = [ 'south korea', 'united states', 'japan', 'china', 'vietnam', 'united kingdom', 'canada', 'australia', 'germany', 'france', 'etc', ]; interface Props { mode?: 'signup' | 'edit'; } export default function SignUpProfile({ mode = 'signup' }: Props) { const nav = useNavigate(); const setUser = useAuthStore((s) => s.setUser); const user = useAuthStore((s) => s.user); const [nickname, setNickname] = useState(user?.nickname ?? ''); const [nickChecked, setNickChecked] = useState(!!user?.nickname); const [nickMsg, setNickMsg] = useState<{ ok: boolean; text: string } | null>( user?.nickname ? { ok: true, text: '현재 닉네임' } : null, ); const [birth, setBirth] = useState(user?.dateOfBirth ?? ''); const [gender, setGender] = useState(user?.gender ?? ''); const [country, setCountry] = useState(user?.country ?? 'south korea'); const [error, setError] = useState(''); useEffect(() => { if (mode === 'edit' && user) { setNickname(user.nickname ?? ''); setBirth(user.dateOfBirth ?? ''); setGender(user.gender ?? ''); setCountry(user.country ?? 'south korea'); } }, [mode, user]); const checkNick = async () => { if (!nickname.trim()) { setNickMsg({ ok: false, text: '닉네임을 입력해주세요.' }); return; } try { const res = await http.post<{ available: boolean }>('/api/profile/check-nickname', { nickname }); setNickChecked(true); setNickMsg(res.available ? { ok: true, text: '사용 가능한 닉네임입니다.' } : { ok: false, text: '이미 사용 중인 닉네임입니다.' }); } catch (err: any) { setNickMsg({ ok: false, text: err?.message ?? '확인에 실패했습니다.' }); } }; const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); if (!nickChecked) { setError('닉네임 중복확인을 해주세요.'); return; } try { const res = await http.post<{ user: any }>('/api/profile', { nickname, dateOfBirth: birth, gender, country, }); setUser(res.user); nav(mode === 'edit' ? '/mypage' : '/home'); } catch (err: any) { setError(err?.message ?? '저장에 실패했습니다.'); } }; const pageTitle = mode === 'edit' ? '프로필 수정' : '추가 정보 입력'; const submitText = mode === 'edit' ? '수정사항 저장하기' : '저장하고 시작하기'; return ( {pageTitle} 닉네임 { setNickname(e.target.value); setNickChecked(false); setNickMsg(null); }} /> 중복확인 {nickMsg && {nickMsg.text}} 생년월일 setBirth(e.target.value)} /> 성별 국가 {error && {error}} {submitText} ); }