회원가입 api
This commit is contained in:
@@ -15,11 +15,55 @@ const NAV_ITEMS = [
|
|||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||||
|
const [userName, setUserName] = useState<string>('');
|
||||||
const userMenuRef = useRef<HTMLDivElement | null>(null);
|
const userMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const userButtonRef = useRef<HTMLButtonElement | null>(null);
|
const userButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const hideCenterNav = /^\/[^/]+\/review$/.test(pathname);
|
const hideCenterNav = /^\/[^/]+\/review$/.test(pathname);
|
||||||
const isAdminPage = pathname.startsWith('/admin');
|
const isAdminPage = pathname.startsWith('/admin');
|
||||||
|
|
||||||
|
// 사용자 정보 가져오기
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('https://hrdi.coconutmeet.net/auth/me', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
// 토큰이 만료되었거나 유효하지 않은 경우
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (isMounted && data.name) {
|
||||||
|
setUserName(data.name);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사용자 정보 조회 오류:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserInfo();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isUserMenuOpen) return;
|
if (!isUserMenuOpen) return;
|
||||||
const onDown = (e: MouseEvent) => {
|
const onDown = (e: MouseEvent) => {
|
||||||
@@ -91,7 +135,7 @@ export default function NavBar() {
|
|||||||
aria-expanded={isUserMenuOpen}
|
aria-expanded={isUserMenuOpen}
|
||||||
className="flex items-center gap-1 px-4 py-2 text-[16px] font-semibold text-white cursor-pointer"
|
className="flex items-center gap-1 px-4 py-2 text-[16px] font-semibold text-white cursor-pointer"
|
||||||
>
|
>
|
||||||
김이름
|
{userName || '사용자'}
|
||||||
<ChevronDownSvg
|
<ChevronDownSvg
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import MainLogo from "@/app/svgs/mainlogosvg"
|
import MainLogo from "@/app/svgs/mainlogosvg"
|
||||||
import LoginCheckboxActiveSvg from "@/app/svgs/logincheckboxactivesvg";
|
import LoginCheckboxActiveSvg from "@/app/svgs/logincheckboxactivesvg";
|
||||||
@@ -10,6 +11,7 @@ import LoginErrorModal from "./LoginErrorModal";
|
|||||||
import LoginOption from "@/app/login/LoginOption";
|
import LoginOption from "@/app/login/LoginOption";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [rememberId, setRememberId] = useState(false);
|
const [rememberId, setRememberId] = useState(false);
|
||||||
@@ -60,12 +62,18 @@ export default function LoginPage() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("로그인 성공:", data);
|
console.log("로그인 성공:", data);
|
||||||
|
|
||||||
// 로그인 성공 시 처리 (예: 토큰 저장, 리다이렉트 등)
|
// 로그인 성공 시 토큰 저장 (다양한 필드명 지원)
|
||||||
// TODO: 성공 시 처리 로직 추가 (예: localStorage에 토큰 저장, 메인 페이지로 이동 등)
|
const token = data.token || data.accessToken || data.access_token;
|
||||||
// if (data.token) {
|
if (token) {
|
||||||
// localStorage.setItem('token', data.token);
|
localStorage.setItem('token', token);
|
||||||
// window.location.href = '/menu';
|
console.log("토큰 저장 완료");
|
||||||
// }
|
} else {
|
||||||
|
console.warn("토큰이 응답에 없습니다. 응답 데이터:", data);
|
||||||
|
// 토큰이 없어도 로그인은 성공했으므로 진행
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메인 페이지로 이동
|
||||||
|
router.push('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "네트워크 오류가 발생했습니다.";
|
const errorMessage = error instanceof Error ? error.message : "네트워크 오류가 발생했습니다.";
|
||||||
console.error("로그인 오류:", errorMessage);
|
console.error("로그인 오류:", errorMessage);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import ModalCloseSvg from "../svgs/closexsvg";
|
import ModalCloseSvg from "../svgs/closexsvg";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -9,6 +11,78 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AccountDeleteModal({ open, onClose, onConfirm }: Props) {
|
export default function AccountDeleteModal({ open, onClose, onConfirm }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
alert('로그인이 필요합니다.');
|
||||||
|
setIsLoading(false);
|
||||||
|
onClose();
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('회원 탈퇴 요청 시작, 토큰 존재:', !!token);
|
||||||
|
console.log('토큰 길이:', token?.length);
|
||||||
|
console.log('토큰 시작 부분:', token?.substring(0, 20));
|
||||||
|
|
||||||
|
const response = await fetch('https://hrdi.coconutmeet.net/auth/delete/me', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('회원 탈퇴 응답 상태:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `회원 탈퇴 실패 (${response.status})`;
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('회원 탈퇴 API 오류 응답:', errorData);
|
||||||
|
if (errorData.error) {
|
||||||
|
errorMessage = errorData.error;
|
||||||
|
} else if (errorData.message) {
|
||||||
|
errorMessage = errorData.message;
|
||||||
|
} else if (errorData.errorMessage) {
|
||||||
|
errorMessage = errorData.errorMessage;
|
||||||
|
} else if (response.statusText) {
|
||||||
|
errorMessage = `${response.statusText} (${response.status})`;
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('응답 파싱 오류:', parseError);
|
||||||
|
if (response.statusText) {
|
||||||
|
errorMessage = `${response.statusText} (${response.status})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error('회원 탈퇴 실패:', errorMessage, '상태 코드:', response.status);
|
||||||
|
alert(errorMessage);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 성공 시 토큰 제거 및 로그인 페이지로 이동
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
onClose();
|
||||||
|
router.push('/login');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.';
|
||||||
|
console.error('회원 탈퇴 오류:', errorMessage);
|
||||||
|
alert(errorMessage);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,7 +94,7 @@ export default function AccountDeleteModal({ open, onClose, onConfirm }: Props)
|
|||||||
<div className="w-[528px] rounded-[12px] border border-[#dee1e6] bg-white shadow-[0_10px_30px_rgba(0,0,0,0.06)]">
|
<div className="w-[528px] rounded-[12px] border border-[#dee1e6] bg-white shadow-[0_10px_30px_rgba(0,0,0,0.06)]">
|
||||||
{/* header */}
|
{/* header */}
|
||||||
<div className="flex items-center justify-between p-6">
|
<div className="flex items-center justify-between p-6">
|
||||||
<h2 className="text-[20px] font-bold leading-[1.5] text-[#333c47]">회원 탈퇴</h2>
|
<h2 className="text-[20px] font-bold leading-normal text-[#333c47]">회원 탈퇴</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="닫기"
|
aria-label="닫기"
|
||||||
@@ -34,10 +108,10 @@ export default function AccountDeleteModal({ open, onClose, onConfirm }: Props)
|
|||||||
{/* body */}
|
{/* body */}
|
||||||
<div className="px-6">
|
<div className="px-6">
|
||||||
<div className="rounded-[16px] border border-[#dee1e6] bg-gray-50 p-6">
|
<div className="rounded-[16px] border border-[#dee1e6] bg-gray-50 p-6">
|
||||||
<p className="mb-3 text-[15px] font-bold leading-[1.5] text-[#4c5561]">
|
<p className="mb-3 text-[15px] font-bold leading-normal text-basic-text">
|
||||||
회원 탈퇴 시 유의사항을 확인해주세요.
|
회원 탈퇴 시 유의사항을 확인해주세요.
|
||||||
</p>
|
</p>
|
||||||
<div className="text-[15px] leading-[1.5] text-[#4c5561]">
|
<div className="text-[15px] leading-normal text-basic-text">
|
||||||
<p className="mb-0">- 탈퇴 후에도 재가입은 가능합니다.</p>
|
<p className="mb-0">- 탈퇴 후에도 재가입은 가능합니다.</p>
|
||||||
<p className="mb-0">- 수강 및 학습 이력이 모두 삭제되며, 복구가 불가능합니다.</p>
|
<p className="mb-0">- 수강 및 학습 이력이 모두 삭제되며, 복구가 불가능합니다.</p>
|
||||||
<p>- 수강 서비스 이용 권한이 즉시 종료됩니다.</p>
|
<p>- 수강 서비스 이용 권한이 즉시 종료됩니다.</p>
|
||||||
@@ -50,16 +124,17 @@ export default function AccountDeleteModal({ open, onClose, onConfirm }: Props)
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="h-12 w-[136px] rounded-[10px] bg-[#f1f3f5] px-4 text-[16px] font-semibold leading-[1.5] text-[#4c5561] cursor-pointer"
|
className="h-12 w-[136px] rounded-[10px] bg-[#f1f3f5] px-4 text-[16px] font-semibold leading-normal text-basic-text cursor-pointer"
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onConfirm}
|
onClick={handleConfirm}
|
||||||
className="h-12 w-[136px] rounded-[10px] bg-red-50 px-4 text-[16px] font-semibold leading-[1.5] text-[#f64c4c] cursor-pointer"
|
disabled={isLoading}
|
||||||
|
className="h-12 w-[136px] rounded-[10px] bg-red-50 px-4 text-[16px] font-semibold leading-normal text-error cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
회원 탈퇴
|
{isLoading ? '처리 중...' : '회원 탈퇴'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import ChangePasswordModal from "../ChangePasswordModal";
|
import ChangePasswordModal from "../ChangePasswordModal";
|
||||||
import PasswordChangeDoneModal from "../PasswordChangeDoneModal";
|
import PasswordChangeDoneModal from "../PasswordChangeDoneModal";
|
||||||
import AccountDeleteModal from "../AccountDeleteModal";
|
import AccountDeleteModal from "../AccountDeleteModal";
|
||||||
@@ -8,11 +9,90 @@ import MenuAccountOption from "@/app/menu/account/MenuAccountOption";
|
|||||||
|
|
||||||
type VerificationState = 'initial' | 'sent' | 'verified' | 'failed' | 'changed';
|
type VerificationState = 'initial' | 'sent' | 'verified' | 'failed' | 'changed';
|
||||||
|
|
||||||
|
type UserInfo = {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function AccountPage() {
|
export default function AccountPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [verificationState, setVerificationState] = useState<VerificationState>('initial');
|
const [verificationState, setVerificationState] = useState<VerificationState>('initial');
|
||||||
const [doneOpen, setDoneOpen] = useState(false);
|
const [doneOpen, setDoneOpen] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 개발 옵션에서 'changed'로 전환하면 완료 모달 표시
|
// 개발 옵션에서 'changed'로 전환하면 완료 모달 표시
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -22,7 +102,7 @@ export default function AccountPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="flex w-full flex-col">
|
<main className="flex w-full flex-col">
|
||||||
<div className="flex h-[100px] items-center px-8">
|
<div className="flex h-[100px] items-center px-8">
|
||||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">내 정보 수정</h1>
|
<h1 className="text-[24px] font-bold leading-normal text-[#1b2027]">내 정보 수정</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-8 pb-20">
|
<div className="px-8 pb-20">
|
||||||
<div className="rounded-lg border border-[#dee1e6] bg-white p-8">
|
<div className="rounded-lg border border-[#dee1e6] bg-white p-8">
|
||||||
@@ -31,7 +111,9 @@ export default function AccountPage() {
|
|||||||
아이디 (이메일)
|
아이디 (이메일)
|
||||||
</label>
|
</label>
|
||||||
<div className="h-10 rounded-lg border border-[#dee1e6] bg-neutral-50 px-3 py-2">
|
<div className="h-10 rounded-lg border border-[#dee1e6] bg-neutral-50 px-3 py-2">
|
||||||
<span className="text-[16px] leading-[1.5] text-[#333c47]">skyblue@edu.com</span>
|
<span className="text-[16px] leading-normal text-[#333c47]">
|
||||||
|
{isLoading ? '로딩 중...' : (userInfo.email || '이메일 정보 없음')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex flex-col gap-2">
|
<div className="mt-6 flex flex-col gap-2">
|
||||||
@@ -40,7 +122,7 @@ export default function AccountPage() {
|
|||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-3">
|
<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">
|
<div className="h-10 flex-1 rounded-lg border border-[#dee1e6] bg-neutral-50 px-3 py-2">
|
||||||
<span className="text-[16px] leading-[1.5] text-[#333c47]">●●●●●●●●●●</span>
|
<span className="text-[16px] leading-normal text-[#333c47]">●●●●●●●●●●</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -89,9 +171,47 @@ export default function AccountPage() {
|
|||||||
<AccountDeleteModal
|
<AccountDeleteModal
|
||||||
open={deleteOpen}
|
open={deleteOpen}
|
||||||
onClose={() => setDeleteOpen(false)}
|
onClose={() => setDeleteOpen(false)}
|
||||||
onConfirm={() => {
|
onConfirm={async () => {
|
||||||
// TODO: 탈퇴 API 연동
|
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);
|
setDeleteOpen(false);
|
||||||
|
router.push('/login');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.';
|
||||||
|
console.error('회원 탈퇴 오류:', errorMessage);
|
||||||
|
alert(errorMessage);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user