2025-11-18 06:19:26 +09:00
|
|
|
'use client';
|
|
|
|
|
|
2025-11-18 07:53:09 +09:00
|
|
|
import { useEffect, useState } from "react";
|
2025-11-24 23:31:05 +09:00
|
|
|
import { useRouter } from "next/navigation";
|
2025-11-18 06:19:26 +09:00
|
|
|
import ChangePasswordModal from "../ChangePasswordModal";
|
2025-11-18 07:53:09 +09:00
|
|
|
import PasswordChangeDoneModal from "../PasswordChangeDoneModal";
|
|
|
|
|
import AccountDeleteModal from "../AccountDeleteModal";
|
|
|
|
|
import MenuAccountOption from "@/app/menu/account/MenuAccountOption";
|
|
|
|
|
|
|
|
|
|
type VerificationState = 'initial' | 'sent' | 'verified' | 'failed' | 'changed';
|
2025-11-18 06:19:26 +09:00
|
|
|
|
2025-11-24 23:31:05 +09:00
|
|
|
type UserInfo = {
|
|
|
|
|
email?: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
phone?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-18 06:19:26 +09:00
|
|
|
export default function AccountPage() {
|
2025-11-24 23:31:05 +09:00
|
|
|
const router = useRouter();
|
2025-11-18 06:19:26 +09:00
|
|
|
const [open, setOpen] = useState(false);
|
2025-11-18 07:53:09 +09:00
|
|
|
const [verificationState, setVerificationState] = useState<VerificationState>('initial');
|
|
|
|
|
const [doneOpen, setDoneOpen] = useState(false);
|
|
|
|
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
2025-11-24 23:31:05 +09:00
|
|
|
const [userInfo, setUserInfo] = useState<UserInfo>({});
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
// 페이지 로드 시 사용자 정보 가져오기
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let isMounted = true;
|
|
|
|
|
|
|
|
|
|
async function fetchUserInfo() {
|
|
|
|
|
try {
|
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
|
if (!token) {
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
router.push('/login');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch('https://hrdi.coconutmeet.net/auth/me', {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
if (response.status === 401) {
|
|
|
|
|
// 토큰이 만료되었거나 유효하지 않은 경우
|
|
|
|
|
localStorage.removeItem('token');
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
router.push('/login');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let errorMessage = `사용자 정보 조회 실패 (${response.status})`;
|
|
|
|
|
try {
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
if (errorData.error) {
|
|
|
|
|
errorMessage = errorData.error;
|
|
|
|
|
} else if (errorData.message) {
|
|
|
|
|
errorMessage = errorData.message;
|
|
|
|
|
}
|
|
|
|
|
} catch (parseError) {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
console.error('사용자 정보 조회 실패:', errorMessage);
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
setUserInfo(data);
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.';
|
|
|
|
|
console.error('사용자 정보 조회 오류:', errorMessage);
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetchUserInfo();
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
2025-11-18 07:53:09 +09:00
|
|
|
|
|
|
|
|
// 개발 옵션에서 'changed'로 전환하면 완료 모달 표시
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setDoneOpen(verificationState === 'changed');
|
|
|
|
|
}, [verificationState]);
|
2025-11-18 06:19:26 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<main className="flex w-full flex-col">
|
|
|
|
|
<div className="flex h-[100px] items-center px-8">
|
2025-11-24 23:31:05 +09:00
|
|
|
<h1 className="text-[24px] font-bold leading-normal text-[#1b2027]">내 정보 수정</h1>
|
2025-11-18 06:19:26 +09:00
|
|
|
</div>
|
|
|
|
|
<div className="px-8 pb-20">
|
|
|
|
|
<div className="rounded-lg border border-[#dee1e6] bg-white p-8">
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<label className="w-[100px] text-[15px] font-semibold leading-[1.5] text-[#6c7682]">
|
|
|
|
|
아이디 (이메일)
|
|
|
|
|
</label>
|
|
|
|
|
<div className="h-10 rounded-lg border border-[#dee1e6] bg-neutral-50 px-3 py-2">
|
2025-11-24 23:31:05 +09:00
|
|
|
<span className="text-[16px] leading-normal text-[#333c47]">
|
|
|
|
|
{isLoading ? '로딩 중...' : (userInfo.email || '이메일 정보 없음')}
|
|
|
|
|
</span>
|
2025-11-18 06:19:26 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-6 flex flex-col gap-2">
|
|
|
|
|
<label className="w-[100px] text-[15px] font-semibold leading-[1.5] text-[#6c7682]">
|
|
|
|
|
비밀번호 변경
|
|
|
|
|
</label>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="h-10 flex-1 rounded-lg border border-[#dee1e6] bg-neutral-50 px-3 py-2">
|
2025-11-24 23:31:05 +09:00
|
|
|
<span className="text-[16px] leading-normal text-[#333c47]">●●●●●●●●●●</span>
|
2025-11-18 06:19:26 +09:00
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setOpen(true)}
|
|
|
|
|
className="h-10 rounded-lg bg-[#f1f3f5] px-4 text-[16px] font-semibold leading-[1.5] text-[#4c5561]"
|
|
|
|
|
>
|
|
|
|
|
비밀번호 변경
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-6">
|
2025-11-18 07:53:09 +09:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setDeleteOpen(true)}
|
|
|
|
|
className="text-[15px] font-medium leading-[1.5] text-[#f64c4c] underline cursor-pointer"
|
|
|
|
|
>
|
2025-11-18 06:19:26 +09:00
|
|
|
회원 탈퇴하기
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<ChangePasswordModal
|
|
|
|
|
open={open}
|
|
|
|
|
onClose={() => setOpen(false)}
|
|
|
|
|
onSubmit={() => {
|
|
|
|
|
// TODO: integrate API
|
|
|
|
|
}}
|
2025-11-18 10:42:48 +09:00
|
|
|
devVerificationState={
|
|
|
|
|
verificationState === 'changed' ? 'verified' : verificationState
|
|
|
|
|
}
|
2025-11-18 07:53:09 +09:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<MenuAccountOption
|
|
|
|
|
verificationState={verificationState}
|
|
|
|
|
setVerificationState={setVerificationState}
|
|
|
|
|
deleteOpen={deleteOpen}
|
|
|
|
|
setDeleteOpen={setDeleteOpen}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<PasswordChangeDoneModal
|
|
|
|
|
open={doneOpen}
|
|
|
|
|
onClose={() => setDoneOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<AccountDeleteModal
|
|
|
|
|
open={deleteOpen}
|
|
|
|
|
onClose={() => setDeleteOpen(false)}
|
2025-11-24 23:31:05 +09:00
|
|
|
onConfirm={async () => {
|
|
|
|
|
try {
|
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
|
const response = await fetch('https://hrdi.coconutmeet.net/auth/delete/me', {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
let errorMessage = `회원 탈퇴 실패 (${response.status})`;
|
|
|
|
|
try {
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
if (errorData.error) {
|
|
|
|
|
errorMessage = errorData.error;
|
|
|
|
|
} else if (errorData.message) {
|
|
|
|
|
errorMessage = errorData.message;
|
|
|
|
|
} else if (response.statusText) {
|
|
|
|
|
errorMessage = `${response.statusText} (${response.status})`;
|
|
|
|
|
}
|
|
|
|
|
} catch (parseError) {
|
|
|
|
|
if (response.statusText) {
|
|
|
|
|
errorMessage = `${response.statusText} (${response.status})`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.error('회원 탈퇴 실패:', errorMessage);
|
|
|
|
|
alert(errorMessage);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 성공 시 토큰 제거 및 로그인 페이지로 이동
|
|
|
|
|
localStorage.removeItem('token');
|
|
|
|
|
setDeleteOpen(false);
|
|
|
|
|
router.push('/login');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.';
|
|
|
|
|
console.error('회원 탈퇴 오류:', errorMessage);
|
|
|
|
|
alert(errorMessage);
|
|
|
|
|
}
|
2025-11-18 07:53:09 +09:00
|
|
|
}}
|
2025-11-18 06:19:26 +09:00
|
|
|
/>
|
|
|
|
|
</main>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|