✨ Feat: 마이도도 로그아웃 및 회원 탈퇴 기능 구현 - #64
Conversation
- POST /auth/logout 호출 함수(logout) 추가 - LogoutRequest/LogoutResponse 타입 정의 - 로그아웃 상태 코드별 안내 메시지 맵 추가 `DoDo-Project#51`
- 사이드바 로그아웃 버튼 클릭 시 확인 모달 노출 - 확인 시 POST /auth/logout 호출 후 홈 이동 및 토큰 정리(clearTokens) - 인증 페이지 이탈 후 토큰을 정리해 잔여 인증 쿼리의 401 리다이렉트 방지 - 사이드바 메뉴에 link/action 타입 구분 추가 `DoDo-Project#51`
- 탈퇴 인증 메일 발송(POST /users/me/withdrawal/email) 함수 추가 - 최종 회원 탈퇴(DELETE /users/me) 함수 추가 - 관련 요청/응답 타입 및 상태 코드별 안내 메시지 맵 추가 - 메일 재요청 제한(429) 안내 문구 포함 `DoDo-Project#51`
- /my/withdrawal 라우트 추가 (인증 보호) - 회원탈퇴 페이지 스캐폴드 및 안내/뒤로가기 동선 구성 - 회원정보 수정 화면 하단에 탈퇴 진입 영역(danger zone) 배치 `DoDo-Project#51`
- 탈퇴 인증 메일 발송 후 6자리 인증번호(OTP 박스) 입력 UI 노출 - 인증번호 입력 후 DELETE /users/me로 최종 탈퇴 처리 - 탈퇴 성공 시 토큰/세션 정리(clearTokens) - 메일 발송 결과를 자동 사라짐 토스트로 안내 - 인증번호 불일치(400/401) 시 '인증번호가 틀렸습니다' 안내 - 인증 메일 재발송 60초 쿨다운 및 카운트다운 처리 - 공용 Toast/useCooldown/AuthCodeInput 추가 `DoDo-Project#51`
- 최종 탈퇴 성공 후 탈퇴 완료 안내 화면 노출 - 회원가입 완료 화면과 대칭되는 레이아웃 적용 - 완료 화면에서 홈으로 이동(replace) 동선 제공 `DoDo-Project#51`
|
@sooloin is attempting to deploy a commit to the sooloin's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request implements the logout and user withdrawal features, including API integrations, state hooks, and UI components such as confirmation dialogs, verification code inputs, and status modals. The review feedback highlights several critical improvements: ensuring that local tokens are cleared even if the logout API fails, delaying token clearance during withdrawal to prevent premature redirection before the completion modal is shown, optimizing the countdown timer's effect dependencies, adding focus guards to the auth code input to prevent UX inconsistencies, and correcting the non-standard Tailwind class z-60 to z-[60].
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const handleConfirm = async () => { | ||
| setErrorMessage(''); | ||
|
|
||
| try { | ||
| await mutateAsync(); | ||
| // 인증 페이지(마이도도)에서 먼저 빠져나간 뒤 토큰을 정리해야 | ||
| // 잔여 인증 쿼리의 재요청 → 401 → 세션 만료 리다이렉트를 피할 수 있다. | ||
| navigate('/', { replace: true }); | ||
| clearTokens(); | ||
| } catch (error) { | ||
| setErrorMessage( | ||
| getApiErrorMessage(error, '로그아웃에 실패했어요. 잠시 후 다시 시도해주세요.', LOGOUT_STATUS_MESSAGES), | ||
| ); | ||
| } | ||
| }; |
There was a problem hiding this comment.
서버 로그아웃 API 요청(mutateAsync())이 실패하더라도, 클라이언트에서는 항상 토큰을 정리하고 홈 화면으로 이동할 수 있어야 합니다. 그렇지 않으면 서버 장애나 네트워크 불안정 시 사용자가 로그아웃을 할 수 없는 상태에 빠지게 됩니다.
실패 여부와 관계없이 로컬 세션을 정리하도록 finally 블록을 사용하거나 예외를 적절히 처리하는 것이 안전합니다.
const handleConfirm = async () => {
setErrorMessage('');
try {
await mutateAsync();
} catch (error) {
console.error('Server logout failed:', error);
} finally {
navigate('/', { replace: true });
clearTokens();
}
};
| const handleCompleted = () => { | ||
| setCompleted(true); | ||
| clearTokens(); | ||
| }; |
There was a problem hiding this comment.
WithdrawalPage는 RequireAuth 가드로 보호되어 있습니다. 따라서 handleCompleted에서 즉시 clearTokens()를 호출하면 인증 상태가 해제되어, WithdrawalCompleteModal이 표시되기도 전에 로그인 페이지(/auth)로 강제 리다이렉트되는 UX 버그가 발생할 수 있습니다.
토큰 삭제는 탈퇴 완료 모달을 닫고 홈으로 이동하는 시점(WithdrawalCompleteModal 내부)으로 미루는 것이 안전합니다.
const handleCompleted = () => {
setCompleted(true);
};
| import { useNavigate } from 'react-router-dom'; | ||
|
|
||
| import DoDoLogo from '@/shared/assets/images/Logo_light.svg?react'; | ||
| import { Modal } from '@/shared/ui'; | ||
|
|
||
| interface WithdrawalCompleteModalProps { | ||
| open: boolean; | ||
| } | ||
|
|
||
| export function WithdrawalCompleteModal({ open }: WithdrawalCompleteModalProps) { | ||
| const navigate = useNavigate(); | ||
| const goHome = () => navigate('/', { replace: true }); | ||
|
|
There was a problem hiding this comment.
WithdrawalPage에서 토큰 삭제 시점을 미룸에 따라, 탈퇴 완료 모달에서 '홈으로' 버튼을 누르거나 모달을 닫을 때 clearTokens()가 호출되도록 변경합니다. 이를 통해 사용자가 탈퇴 완료 메시지를 정상적으로 확인한 후 안전하게 세션이 정리되도록 보장할 수 있습니다.
import { useNavigate } from 'react-router-dom';
import DoDoLogo from '@/shared/assets/images/Logo_light.svg?react';
import { clearTokens } from '@/shared/lib/auth/token';
import { Modal } from '@/shared/ui';
interface WithdrawalCompleteModalProps {
open: boolean;
}
export function WithdrawalCompleteModal({ open }: WithdrawalCompleteModalProps) {
const navigate = useNavigate();
const goHome = () => {
clearTokens();
navigate('/', { replace: true });
};
| useEffect(() => { | ||
| if (seconds <= 0) return; | ||
|
|
||
| const timer = setInterval(() => { | ||
| setSeconds((prev) => (prev <= 1 ? 0 : prev - 1)); | ||
| }, 1000); | ||
|
|
||
| return () => clearInterval(timer); | ||
| }, [seconds]); |
There was a problem hiding this comment.
useEffect의 의존성 배열에 seconds가 포함되어 있어, 매 초마다 타이머가 해제되고 새로 생성되는 비효율이 발생합니다.
의존성을 seconds > 0으로 변경하고, setSeconds 내부에서 이전 상태값을 기반으로 안전하게 감소시키면 타이머가 한 번만 생성되어 훨씬 효율적이고 정확하게 동작합니다.
| useEffect(() => { | |
| if (seconds <= 0) return; | |
| const timer = setInterval(() => { | |
| setSeconds((prev) => (prev <= 1 ? 0 : prev - 1)); | |
| }, 1000); | |
| return () => clearInterval(timer); | |
| }, [seconds]); | |
| useEffect(() => { | |
| if (seconds <= 0) return; | |
| const timer = setInterval(() => { | |
| setSeconds((prev) => (prev <= 1 ? 0 : prev - 1)); | |
| }, 1000); | |
| return () => clearInterval(timer); | |
| }, [seconds > 0]); |
| <input | ||
| key={index} | ||
| ref={(el) => { | ||
| inputsRef.current[index] = el; | ||
| }} | ||
| type="text" | ||
| inputMode="numeric" | ||
| autoComplete={index === 0 ? 'one-time-code' : 'off'} | ||
| maxLength={1} | ||
| value={value[index] ?? ''} | ||
| disabled={disabled} | ||
| onChange={(event) => handleChange(index, event.target.value)} | ||
| onKeyDown={(event) => handleKeyDown(index, event)} | ||
| className="h-20 w-full rounded-xl border border-neutral-200 bg-white text-center text-2xl font-semibold text-neutral-900 outline-none transition-colors focus:border-brand disabled:bg-neutral-50 disabled:opacity-60" | ||
| /> | ||
| ))} |
There was a problem hiding this comment.
사용자가 아직 입력하지 않은 빈 슬롯(예: 3번째 칸이 비어있는데 5번째 칸)을 클릭하여 입력할 경우, 내부 문자열 처리(replace(/ /g, ''))로 인해 입력한 값이 앞쪽 빈 슬롯으로 당겨져 채워지지만 포커스는 뒤쪽에 남아있는 UX 불일치 현상이 발생합니다.
이를 방지하기 위해, 사용자가 현재 입력 가능한 가장 첫 번째 빈 슬롯보다 뒤쪽의 슬롯을 포커스하려고 할 때 자동으로 첫 번째 빈 슬롯으로 포커스를 이동시켜주는 onFocus 가드를 추가하는 것을 권장합니다.
<input
key={index}
ref={(el) => {
inputsRef.current[index] = el;
}}
type="text"
inputMode="numeric"
autoComplete={index === 0 ? 'one-time-code' : 'off'}
maxLength={1}
value={value[index] ?? ''}
disabled={disabled}
onChange={(event) => handleChange(index, event.target.value)}
onKeyDown={(event) => handleKeyDown(index, event)}
onFocus={() => {
const firstEmptyIndex = value.length;
if (index > firstEmptyIndex) {
focusInput(firstEmptyIndex);
}
}}
className="h-20 w-full rounded-xl border border-neutral-200 bg-white text-center text-2xl font-semibold text-neutral-900 outline-none transition-colors focus:border-brand disabled:bg-neutral-50 disabled:opacity-60"
/>
| return createPortal( | ||
| <div className="fixed left-1/2 top-6 z-60 -translate-x-1/2 px-4"> | ||
| <div | ||
| role="status" | ||
| className={`rounded-2xl px-5 py-3 text-sm font-medium shadow-[0_18px_42px_rgba(15,23,42,0.18)] ${TONE_CLASS[tone]}`} | ||
| > | ||
| {message} | ||
| </div> | ||
| </div>, | ||
| document.body, | ||
| ); |
There was a problem hiding this comment.
z-60은 Tailwind CSS의 기본 z-index 클래스(z-0 ~ z-50)에 포함되지 않습니다. 프로젝트의 Tailwind 설정에 z-60이 별도로 정의되어 있지 않다면 z-index가 적용되지 않아 토스트 메시지가 모달 등의 요소 뒤에 가려질 수 있습니다.
안전하게 임의 값 문법인 z-[60]을 사용하는 것을 권장합니다.
return createPortal(
<div className="fixed left-1/2 top-6 z-[60] -translate-x-1/2 px-4">
<div
role="status"
className={
`rounded-2xl px-5 py-3 text-sm font-medium shadow-[0_18px_42px_rgba(15,23,42,0.18)] ${TONE_CLASS[tone]}`
}
>
{message}
</div>
</div>,
document.body,
);
- 서버 로그아웃 실패 여부와 무관하게 finally에서 토큰 정리·홈 이동 처리 - 서버 실패는 콘솔 로깅으로 대체하고 로컬 로그아웃은 항상 진행 - 화면 이탈로 불필요해진 에러 메시지 상태/표시 제거 `DoDo-Project#51`
- 탈퇴 성공 직후 clearTokens 호출을 제거하고 토큰 정리를 모달 이탈 시점으로 지연 - WithdrawalCompleteModal에서 홈 이동 시 clearTokens 수행 - RequireAuth 가드 아래에서 모달 노출 전 /auth로 리다이렉트되던 문제 해결 `DoDo-Project#51`
- useCooldown: 의존성을 isActive(seconds>0)로 변경해 타이머 매초 재생성 방지 - AuthCodeInput: 빈 슬롯보다 뒤 칸 포커스 시 첫 빈 슬롯으로 이동하는 onFocus 가드 추가 `DoDo-Project#51`
📄 작업 내용 (Description)
로그아웃
로그아웃메뉴를 링크가 아닌 확인 모달 트리거로 전환 (메뉴에link/action타입 구분 추가)POST /auth/logout호출 후 토큰/세션 정리(clearTokens) → 홈 이동회원 탈퇴 (2단계 플로우)
/my/withdrawal페이지로 이동POST /users/me/withdrawal/email로 인증 메일 발송, 발송 결과를 토스트로 안내, 60초 재발송 쿨다운DELETE /users/me로 최종 탈퇴clearTokens) + 완료 모달 노출 (회원가입 완료 화면과 톤 대칭)🖥️ 플로우
로그아웃: 사이드바 로그아웃 → 확인 모달 → 홈
회원 탈퇴: 회원정보 수정 하단 → 탈퇴 페이지 → 인증 메일 발송 → 6자리 입력 → 탈퇴 완료 모달 → 홈
🔗 관련 이슈 (Related Issues)
✅ 체크리스트 (Checklist)
Style)Test)📸 스크린샷 (Screenshots)
💬 기타 사항 (Etc)