mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 12:16:24 +00:00
## 주요 변경사항 ### 1. FSD 아키텍처 기반 API 레이어 구축 - entities/user: User 엔티티 (타입, API) - features/auth: 인증 기능 (useAuth, AuthProvider) - shared/api: 공통 API 클라이언트 (Axios, 인터셉터) ### 2. 전체 User API 화면 연동 완료 - ✅ POST /api/v1/users/login → login/page.tsx - ✅ POST /api/v1/users/register → register/page.tsx - ✅ POST /api/v1/users/logout → profile/page.tsx - ✅ GET /api/v1/users/profile → profile/page.tsx - ✅ PUT /api/v1/users/profile → profile/page.tsx - ✅ PUT /api/v1/users/password → profile/page.tsx ### 3. 로그인 페이지 API 연동 - useAuthStore → useAuthContext 변경 - 실제 로그인 API 호출 - 비밀번호 검증 완화 (API 스펙에 맞춤) - 상세 로깅 추가 ### 4. 프로필 페이지 API 연동 - 프로필 자동 로드 (GET /profile) - 프로필 수정 (PUT /profile) - 비밀번호 변경 (PUT /password) - 로그아웃 (POST /logout) - 전화번호 형식 변환 (01012345678 ↔ 010-1234-5678) ### 5. 로그아웃 에러 처리 개선 - 백엔드 500 에러 발생해도 로컬 상태 정리 후 로그아웃 진행 - 사용자 경험 우선: 정상 로그아웃으로 처리 - 개발자용 상세 에러 로그 출력 ### 6. 문서화 - docs/api-integration-complete.md: 전체 연동 완료 보고서 - docs/api-server-issue.md: 백엔드 이슈 상세 보고 (회원가입 타임아웃, 로그아웃 500 에러) - docs/user-api-integration.md: User API 통합 가이드 - docs/register-api-guide.md: 회원가입 API 가이드 ### 7. 에러 처리 강화 - 서버 응답 에러 / 네트워크 에러 / 요청 설정 에러 구분 - 사용자 친화적 에러 메시지 - 전체 프로세스 상세 로깅 ## 기술 스택 - FSD Architecture - React Context API (AuthProvider) - Axios (인터셉터, 90초 타임아웃) - Zod (폼 검증) - TypeScript (엄격한 타입) ## 테스트 - ✅ 빌드 성공 - ⏳ 백엔드 안정화 후 전체 플로우 테스트 필요 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
720 lines
24 KiB
TypeScript
720 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import {
|
|
Box,
|
|
Container,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
Card,
|
|
CardContent,
|
|
Avatar,
|
|
Select,
|
|
MenuItem,
|
|
FormControl,
|
|
InputLabel,
|
|
InputAdornment,
|
|
IconButton,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
} from '@mui/material';
|
|
import { Person, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-material';
|
|
import { useAuthContext } from '@/features/auth';
|
|
import { useUIStore } from '@/stores/uiStore';
|
|
import { userApi } from '@/entities/user';
|
|
import Header from '@/shared/ui/Header';
|
|
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
|
|
|
// 기본 정보 스키마
|
|
const basicInfoSchema = z.object({
|
|
name: z.string().min(2, '이름은 2자 이상이어야 합니다'),
|
|
phone: z
|
|
.string()
|
|
.min(1, '휴대폰 번호를 입력해주세요')
|
|
.regex(/^010-\d{4}-\d{4}$/, '올바른 휴대폰 번호 형식이 아닙니다'),
|
|
email: z.string().email('올바른 이메일 형식이 아닙니다'),
|
|
});
|
|
|
|
// 사업장 정보 스키마
|
|
const businessInfoSchema = z.object({
|
|
businessName: z.string().min(2, '매장명은 2자 이상이어야 합니다'),
|
|
businessType: z.string().min(1, '업종을 선택해주세요'),
|
|
businessLocation: z.string().optional(),
|
|
businessHours: z.string().optional(),
|
|
});
|
|
|
|
// 비밀번호 변경 스키마
|
|
const passwordSchema = z
|
|
.object({
|
|
currentPassword: z.string().min(1, '현재 비밀번호를 입력해주세요'),
|
|
newPassword: z
|
|
.string()
|
|
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
|
.max(100, '비밀번호는 100자 이하여야 합니다'),
|
|
confirmPassword: z.string(),
|
|
})
|
|
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
message: '새 비밀번호가 일치하지 않습니다',
|
|
path: ['confirmPassword'],
|
|
});
|
|
|
|
type BasicInfoData = z.infer<typeof basicInfoSchema>;
|
|
type BusinessInfoData = z.infer<typeof businessInfoSchema>;
|
|
type PasswordData = z.infer<typeof passwordSchema>;
|
|
|
|
export default function ProfilePage() {
|
|
const router = useRouter();
|
|
const { user, logout, refreshProfile } = useAuthContext();
|
|
const { showToast, setLoading } = useUIStore();
|
|
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
|
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
|
const [profileLoaded, setProfileLoaded] = useState(false);
|
|
|
|
// 기본 정보 폼
|
|
const {
|
|
control: basicControl,
|
|
handleSubmit: handleBasicSubmit,
|
|
formState: { errors: basicErrors },
|
|
reset: resetBasic,
|
|
} = useForm<BasicInfoData>({
|
|
resolver: zodResolver(basicInfoSchema),
|
|
defaultValues: {
|
|
name: '',
|
|
phone: '',
|
|
email: '',
|
|
},
|
|
});
|
|
|
|
// 사업장 정보 폼
|
|
const {
|
|
control: businessControl,
|
|
handleSubmit: handleBusinessSubmit,
|
|
formState: { errors: businessErrors },
|
|
reset: resetBusiness,
|
|
} = useForm<BusinessInfoData>({
|
|
resolver: zodResolver(businessInfoSchema),
|
|
defaultValues: {
|
|
businessName: '',
|
|
businessType: '',
|
|
businessLocation: '',
|
|
businessHours: '',
|
|
},
|
|
});
|
|
|
|
// 비밀번호 변경 폼
|
|
const {
|
|
control: passwordControl,
|
|
handleSubmit: handlePasswordSubmit,
|
|
formState: { errors: passwordErrors },
|
|
reset: resetPassword,
|
|
} = useForm<PasswordData>({
|
|
resolver: zodResolver(passwordSchema),
|
|
defaultValues: {
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
},
|
|
});
|
|
|
|
// 프로필 데이터 로드
|
|
useEffect(() => {
|
|
const loadProfile = async () => {
|
|
console.log('📋 프로필 페이지: 프로필 데이터 로드 시작');
|
|
|
|
if (!user) {
|
|
console.log('❌ 사용자 정보 없음, 로그인 페이지로 이동');
|
|
router.push('/login');
|
|
return;
|
|
}
|
|
|
|
if (profileLoaded) {
|
|
console.log('✅ 프로필 이미 로드됨');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
console.log('📡 프로필 조회 API 호출');
|
|
|
|
const profile = await userApi.getProfile();
|
|
console.log('📥 프로필 조회 성공:', profile);
|
|
|
|
// 전화번호 형식 변환: 01012345678 → 010-1234-5678
|
|
const formattedPhone = profile.phoneNumber
|
|
? `${profile.phoneNumber.slice(0, 3)}-${profile.phoneNumber.slice(3, 7)}-${profile.phoneNumber.slice(7, 11)}`
|
|
: '';
|
|
|
|
// 기본 정보 폼 초기화
|
|
resetBasic({
|
|
name: profile.userName || '',
|
|
phone: formattedPhone,
|
|
email: profile.email || '',
|
|
});
|
|
|
|
// 사업장 정보 폼 초기화
|
|
resetBusiness({
|
|
businessName: profile.storeName || '',
|
|
businessType: profile.industry || '',
|
|
businessLocation: profile.address || '',
|
|
businessHours: profile.businessHours || '',
|
|
});
|
|
|
|
setProfileLoaded(true);
|
|
console.log('✅ 프로필 폼 초기화 완료');
|
|
} catch (error: any) {
|
|
console.error('❌ 프로필 로드 실패:', error);
|
|
|
|
if (error.response?.status === 401) {
|
|
showToast('로그인이 필요합니다', 'error');
|
|
router.push('/login');
|
|
} else {
|
|
showToast('프로필 정보를 불러오는데 실패했습니다', 'error');
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadProfile();
|
|
}, [user, profileLoaded, router, resetBasic, resetBusiness, setLoading, showToast]);
|
|
|
|
const formatPhoneNumber = (value: string) => {
|
|
const numbers = value.replace(/[^\d]/g, '');
|
|
if (numbers.length <= 3) return numbers;
|
|
if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
|
|
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7, 11)}`;
|
|
};
|
|
|
|
const onSaveProfile = async (data: BasicInfoData & BusinessInfoData) => {
|
|
console.log('💾 프로필 저장 시작');
|
|
console.log('📦 저장 데이터:', { ...data, phone: data.phone });
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
// 전화번호 형식 변환: 010-1234-5678 → 01012345678
|
|
const phoneNumber = data.phone.replace(/-/g, '');
|
|
console.log('📞 전화번호 변환:', data.phone, '->', phoneNumber);
|
|
|
|
const updateData = {
|
|
userName: data.name,
|
|
phoneNumber: phoneNumber,
|
|
storeName: data.businessName,
|
|
industry: data.businessType,
|
|
address: data.businessLocation || '',
|
|
businessHours: data.businessHours || '',
|
|
};
|
|
|
|
console.log('📡 프로필 업데이트 API 호출:', updateData);
|
|
await userApi.updateProfile(updateData);
|
|
console.log('✅ 프로필 업데이트 성공');
|
|
|
|
// 최신 프로필 정보 다시 가져오기
|
|
console.log('🔄 프로필 새로고침');
|
|
await refreshProfile();
|
|
console.log('✅ 프로필 새로고침 완료');
|
|
|
|
setSuccessDialogOpen(true);
|
|
showToast('프로필이 저장되었습니다', 'success');
|
|
} catch (error: any) {
|
|
console.error('❌ 프로필 저장 실패:', error);
|
|
|
|
let errorMessage = '프로필 저장에 실패했습니다';
|
|
if (error.response) {
|
|
errorMessage = error.response.data?.message ||
|
|
error.response.data?.error ||
|
|
`서버 오류 (${error.response.status})`;
|
|
} else if (error.request) {
|
|
errorMessage = '서버로부터 응답이 없습니다';
|
|
}
|
|
|
|
showToast(errorMessage, 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const onChangePassword = async (data: PasswordData) => {
|
|
console.log('🔐 비밀번호 변경 시작');
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
const passwordData = {
|
|
currentPassword: data.currentPassword,
|
|
newPassword: data.newPassword,
|
|
};
|
|
|
|
console.log('📡 비밀번호 변경 API 호출');
|
|
await userApi.changePassword(passwordData);
|
|
console.log('✅ 비밀번호 변경 성공');
|
|
|
|
showToast('비밀번호가 변경되었습니다', 'success');
|
|
resetPassword();
|
|
} catch (error: any) {
|
|
console.error('❌ 비밀번호 변경 실패:', error);
|
|
|
|
let errorMessage = '비밀번호 변경에 실패했습니다';
|
|
if (error.response) {
|
|
errorMessage = error.response.data?.message ||
|
|
error.response.data?.error ||
|
|
`서버 오류 (${error.response.status})`;
|
|
} else if (error.request) {
|
|
errorMessage = '서버로부터 응답이 없습니다';
|
|
}
|
|
|
|
showToast(errorMessage, 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = () => {
|
|
handleBasicSubmit((basicData) => {
|
|
handleBusinessSubmit((businessData) => {
|
|
onSaveProfile({ ...basicData, ...businessData });
|
|
})();
|
|
})();
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
console.log('🚪 로그아웃 시작');
|
|
setLoading(true);
|
|
|
|
try {
|
|
await logout();
|
|
showToast('로그아웃되었습니다', 'success');
|
|
} catch (error) {
|
|
console.error('❌ 로그아웃 중 예상치 못한 에러:', error);
|
|
showToast('로그아웃되었습니다', 'success');
|
|
} finally {
|
|
setLoading(false);
|
|
// 로그아웃은 항상 로그인 페이지로 이동
|
|
router.push('/login');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Header title="프로필" showBack={true} showMenu={false} showProfile={false} />
|
|
<Box
|
|
sx={{
|
|
pt: { xs: 7, sm: 8 },
|
|
pb: 10,
|
|
bgcolor: colors.gray[50],
|
|
minHeight: '100vh',
|
|
}}
|
|
>
|
|
<Container maxWidth="lg" sx={{ pt: 8, pb: 6, px: { xs: 6, sm: 8, md: 10 } }}>
|
|
{/* 사용자 정보 섹션 */}
|
|
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10, textAlign: 'center' }}>
|
|
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
|
<Avatar
|
|
sx={{
|
|
width: 100,
|
|
height: 100,
|
|
mx: 'auto',
|
|
mb: 3,
|
|
bgcolor: colors.purple,
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<Person sx={{ fontSize: 56 }} />
|
|
</Avatar>
|
|
<Typography variant="h5" sx={{ ...responsiveText.h3, mb: 1 }}>
|
|
{user?.userName}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ ...responsiveText.body1 }}>
|
|
{user?.email}
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 기본 정보 */}
|
|
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
|
|
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
|
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
|
|
기본 정보
|
|
</Typography>
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
<Controller
|
|
name="name"
|
|
control={basicControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label="이름"
|
|
error={!!basicErrors.name}
|
|
helperText={basicErrors.name?.message}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="phone"
|
|
control={basicControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label="전화번호"
|
|
onChange={(e) => {
|
|
const formatted = formatPhoneNumber(e.target.value);
|
|
field.onChange(formatted);
|
|
}}
|
|
error={!!basicErrors.phone}
|
|
helperText={basicErrors.phone?.message}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="email"
|
|
control={basicControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label="이메일"
|
|
type="email"
|
|
error={!!basicErrors.email}
|
|
helperText={basicErrors.email?.message}
|
|
/>
|
|
)}
|
|
/>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 매장 정보 */}
|
|
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
|
|
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
|
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
|
|
매장 정보
|
|
</Typography>
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
<Controller
|
|
name="businessName"
|
|
control={businessControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label="매장명"
|
|
error={!!businessErrors.businessName}
|
|
helperText={businessErrors.businessName?.message}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="businessType"
|
|
control={businessControl}
|
|
render={({ field }) => (
|
|
<FormControl fullWidth error={!!businessErrors.businessType}>
|
|
<InputLabel>업종</InputLabel>
|
|
<Select {...field} label="업종">
|
|
<MenuItem value="restaurant">음식점</MenuItem>
|
|
<MenuItem value="cafe">카페/베이커리</MenuItem>
|
|
<MenuItem value="retail">소매/편의점</MenuItem>
|
|
<MenuItem value="beauty">미용/뷰티</MenuItem>
|
|
<MenuItem value="fitness">헬스/피트니스</MenuItem>
|
|
<MenuItem value="education">학원/교육</MenuItem>
|
|
<MenuItem value="service">서비스업</MenuItem>
|
|
<MenuItem value="other">기타</MenuItem>
|
|
</Select>
|
|
{businessErrors.businessType && (
|
|
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 2 }}>
|
|
{businessErrors.businessType.message}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="businessLocation"
|
|
control={businessControl}
|
|
render={({ field }) => (
|
|
<TextField {...field} fullWidth label="주소" />
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="businessHours"
|
|
control={businessControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
label="영업시간"
|
|
placeholder="예: 10:00 ~ 22:00"
|
|
/>
|
|
)}
|
|
/>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 비밀번호 변경 */}
|
|
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
|
|
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
|
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
|
|
비밀번호 변경
|
|
</Typography>
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
<Controller
|
|
name="currentPassword"
|
|
control={passwordControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
type={showCurrentPassword ? 'text' : 'password'}
|
|
label="현재 비밀번호"
|
|
placeholder="현재 비밀번호를 입력하세요"
|
|
error={!!passwordErrors.currentPassword}
|
|
helperText={passwordErrors.currentPassword?.message}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
|
edge="end"
|
|
>
|
|
{showCurrentPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="newPassword"
|
|
control={passwordControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
type={showNewPassword ? 'text' : 'password'}
|
|
label="새 비밀번호"
|
|
placeholder="새 비밀번호를 입력하세요"
|
|
error={!!passwordErrors.newPassword}
|
|
helperText={passwordErrors.newPassword?.message || '8자 이상 입력해주세요'}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowNewPassword(!showNewPassword)}
|
|
edge="end"
|
|
>
|
|
{showNewPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
name="confirmPassword"
|
|
control={passwordControl}
|
|
render={({ field }) => (
|
|
<TextField
|
|
{...field}
|
|
fullWidth
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
label="비밀번호 확인"
|
|
placeholder="비밀번호를 다시 입력하세요"
|
|
error={!!passwordErrors.confirmPassword}
|
|
helperText={passwordErrors.confirmPassword?.message}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
edge="end"
|
|
>
|
|
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Button
|
|
fullWidth
|
|
variant="outlined"
|
|
size="large"
|
|
onClick={handlePasswordSubmit(onChangePassword)}
|
|
sx={{
|
|
mt: 1,
|
|
py: 3,
|
|
borderRadius: 3,
|
|
fontSize: '1rem',
|
|
fontWeight: 600,
|
|
borderWidth: 2,
|
|
'&:hover': {
|
|
borderWidth: 2,
|
|
},
|
|
}}
|
|
>
|
|
비밀번호 변경
|
|
</Button>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 액션 버튼 */}
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
size="large"
|
|
onClick={handleSave}
|
|
sx={{
|
|
py: 3,
|
|
borderRadius: 3,
|
|
fontSize: '1rem',
|
|
fontWeight: 700,
|
|
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
|
'&:hover': {
|
|
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
|
opacity: 0.9,
|
|
},
|
|
}}
|
|
>
|
|
저장하기
|
|
</Button>
|
|
<Button
|
|
fullWidth
|
|
variant="text"
|
|
size="large"
|
|
color="error"
|
|
onClick={() => setLogoutDialogOpen(true)}
|
|
sx={{
|
|
py: 3,
|
|
fontSize: '1rem',
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
로그아웃
|
|
</Button>
|
|
</Box>
|
|
</Container>
|
|
</Box>
|
|
|
|
{/* 저장 완료 다이얼로그 */}
|
|
<Dialog
|
|
open={successDialogOpen}
|
|
onClose={() => setSuccessDialogOpen(false)}
|
|
PaperProps={{
|
|
sx: {
|
|
borderRadius: 4,
|
|
minWidth: 300,
|
|
},
|
|
}}
|
|
>
|
|
<DialogContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
|
|
<CheckCircle sx={{ fontSize: 64, color: 'success.main', mb: 2 }} />
|
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1, fontSize: '1.25rem' }}>
|
|
저장 완료
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1rem' }}>
|
|
프로필 정보가 업데이트되었습니다.
|
|
</Typography>
|
|
</DialogContent>
|
|
<DialogActions sx={{ justifyContent: 'center', pb: 3 }}>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => {
|
|
setSuccessDialogOpen(false);
|
|
window.location.reload();
|
|
}}
|
|
sx={{
|
|
minWidth: 120,
|
|
py: 2,
|
|
borderRadius: 2,
|
|
fontSize: '1rem',
|
|
fontWeight: 700,
|
|
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
|
'&:hover': {
|
|
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.pink} 100%)`,
|
|
opacity: 0.9,
|
|
},
|
|
}}
|
|
>
|
|
확인
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* 로그아웃 확인 다이얼로그 */}
|
|
<Dialog
|
|
open={logoutDialogOpen}
|
|
onClose={() => setLogoutDialogOpen(false)}
|
|
PaperProps={{
|
|
sx: {
|
|
borderRadius: 4,
|
|
minWidth: 300,
|
|
},
|
|
}}
|
|
>
|
|
<DialogTitle sx={{ fontSize: '1.25rem', fontWeight: 700 }}>로그아웃</DialogTitle>
|
|
<DialogContent>
|
|
<Typography variant="body1" sx={{ textAlign: 'center', py: 2, fontSize: '1rem' }}>
|
|
로그아웃 하시겠습니까?
|
|
</Typography>
|
|
</DialogContent>
|
|
<DialogActions sx={{ justifyContent: 'center', gap: 2, pb: 3 }}>
|
|
<Button
|
|
onClick={() => setLogoutDialogOpen(false)}
|
|
sx={{
|
|
fontWeight: 600,
|
|
fontSize: '1rem',
|
|
py: 2,
|
|
px: 4,
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleLogout}
|
|
color="error"
|
|
sx={{
|
|
fontWeight: 700,
|
|
fontSize: '1rem',
|
|
py: 2,
|
|
px: 4,
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
확인
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|