Skip to content

✨ Feat: 마이도도 로그아웃 및 회원 탈퇴 기능 구현 - #64

Merged
sooloin merged 13 commits into
DoDo-Project:developfrom
sooloin:feature/51
Jun 23, 2026
Merged

✨ Feat: 마이도도 로그아웃 및 회원 탈퇴 기능 구현#64
sooloin merged 13 commits into
DoDo-Project:developfrom
sooloin:feature/51

Conversation

@sooloin

@sooloin sooloin commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

📄 작업 내용 (Description)

이번 PR에서 변경되거나 추가된 주요 작업을 간단히 설명해주세요.

로그아웃

  • 사이드바 로그아웃 메뉴를 링크가 아닌 확인 모달 트리거로 전환 (메뉴에 link/action 타입 구분 추가)
  • "로그아웃 하시겠습니까?" 모달 → 확인 시 POST /auth/logout 호출 후 토큰/세션 정리(clearTokens) → 홈 이동
  • 인증 페이지에서 먼저 이탈한 뒤 토큰을 정리해, 잔여 인증 쿼리의 401 → 세션 만료 리다이렉트 문제 방지

회원 탈퇴 (2단계 플로우)

  • 회원정보 수정 화면 하단에 danger zone 진입점 배치 → /my/withdrawal 페이지로 이동
  • 1단계: POST /users/me/withdrawal/email로 인증 메일 발송, 발송 결과를 토스트로 안내, 60초 재발송 쿨다운
  • 2단계: 6자리 OTP 박스 입력(자동 이동/백스페이스/붙여넣기 지원) → DELETE /users/me로 최종 탈퇴
  • 탈퇴 성공 시 세션 정리(clearTokens) + 완료 모달 노출 (회원가입 완료 화면과 톤 대칭)
  • 예외 처리: 메일 발송 401/404/429(쿨다운 안내)/500, 최종 탈퇴 시 인증번호 불일치(400/401)는 "인증번호가 틀렸습니다"로 안내

🖥️ 플로우

로그아웃: 사이드바 로그아웃 → 확인 모달 → 홈
회원 탈퇴: 회원정보 수정 하단 → 탈퇴 페이지 → 인증 메일 발송 → 6자리 입력 → 탈퇴 완료 모달 → 홈


🔗 관련 이슈 (Related Issues)

작업한 이슈 번호를 아래 형식으로 PULL REQUEST BODY에 작성해주세요.
(PR 머지 시 해당 이슈가 자동으로 종료됩니다.)


✅ 체크리스트 (Checklist)

PR을 보내기 전 아래 항목들을 모두 확인해주세요.

  • PR 제목은 커밋 컨벤션을 따랐습니다.
  • 관련 이슈를 연결했습니다.
  • 스스로 코드를 검토하고 불필요한 코드를 제거했습니다.
  • 코드 스타일이 프로젝트 규칙과 일치합니다. (Style)
  • 새로운 기능에 대한 테스트 코드를 추가했거나, 기존 테스트가 모두 통과했습니다. (Test)

📸 스크린샷 (Screenshots)

작업 내용과 관련된 스크린샷이 있다면 첨부해주세요. (UI 변경이 있는 경우)


💬 기타 사항 (Etc)

리뷰어에게 전달하고 싶은 추가 정보가 있다면 자유롭게 작성해주세요.

sooloin added 6 commits June 23, 2026 13:33
- 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
sooloin requested review from WhiteBin-bin and limhb708 June 23, 2026 07:21
@sooloin sooloin self-assigned this Jun 23, 2026
@sooloin sooloin added ✨ Feature 새 기능 혹은 요구 사항 😸 조수빈 조수빈 파트 labels Jun 23, 2026
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

@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.

@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dodo-frontend Ready Ready Preview, Comment Jun 23, 2026 7:56am

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +24 to +38
const handleConfirm = async () => {
setErrorMessage('');

try {
await mutateAsync();
// 인증 페이지(마이도도)에서 먼저 빠져나간 뒤 토큰을 정리해야
// 잔여 인증 쿼리의 재요청 → 401 → 세션 만료 리다이렉트를 피할 수 있다.
navigate('/', { replace: true });
clearTokens();
} catch (error) {
setErrorMessage(
getApiErrorMessage(error, '로그아웃에 실패했어요. 잠시 후 다시 시도해주세요.', LOGOUT_STATUS_MESSAGES),
);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

서버 로그아웃 API 요청(mutateAsync())이 실패하더라도, 클라이언트에서는 항상 토큰을 정리하고 홈 화면으로 이동할 수 있어야 합니다. 그렇지 않으면 서버 장애나 네트워크 불안정 시 사용자가 로그아웃을 할 수 없는 상태에 빠지게 됩니다.

실패 여부와 관계없이 로컬 세션을 정리하도록 finally 블록을 사용하거나 예외를 적절히 처리하는 것이 안전합니다.

  const handleConfirm = async () => {
    setErrorMessage('');

    try {
      await mutateAsync();
    } catch (error) {
      console.error('Server logout failed:', error);
    } finally {
      navigate('/', { replace: true });
      clearTokens();
    }
  };

Comment on lines +11 to +14
const handleCompleted = () => {
setCompleted(true);
clearTokens();
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

WithdrawalPageRequireAuth 가드로 보호되어 있습니다. 따라서 handleCompleted에서 즉시 clearTokens()를 호출하면 인증 상태가 해제되어, WithdrawalCompleteModal이 표시되기도 전에 로그인 페이지(/auth)로 강제 리다이렉트되는 UX 버그가 발생할 수 있습니다.

토큰 삭제는 탈퇴 완료 모달을 닫고 홈으로 이동하는 시점(WithdrawalCompleteModal 내부)으로 미루는 것이 안전합니다.

  const handleCompleted = () => {
    setCompleted(true);
  };

Comment on lines +1 to +13
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 });
  };

Comment thread src/shared/lib/useCooldown.ts Outdated
Comment on lines +14 to +22
useEffect(() => {
if (seconds <= 0) return;

const timer = setInterval(() => {
setSeconds((prev) => (prev <= 1 ? 0 : prev - 1));
}, 1000);

return () => clearInterval(timer);
}, [seconds]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useEffect의 의존성 배열에 seconds가 포함되어 있어, 매 초마다 타이머가 해제되고 새로 생성되는 비효율이 발생합니다.

의존성을 seconds > 0으로 변경하고, setSeconds 내부에서 이전 상태값을 기반으로 안전하게 감소시키면 타이머가 한 번만 생성되어 훨씬 효율적이고 정확하게 동작합니다.

Suggested change
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]);

Comment on lines +68 to +83
<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"
/>
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

사용자가 아직 입력하지 않은 빈 슬롯(예: 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"
        />

Comment thread src/shared/ui/Toast.tsx
Comment on lines +37 to +47
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,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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,
  );

sooloin added 6 commits June 23, 2026 16:27
- 서버 로그아웃 실패 여부와 무관하게 finally에서 토큰 정리·홈 이동 처리
- 서버 실패는 콘솔 로깅으로 대체하고 로컬 로그아웃은 항상 진행
- 화면 이탈로 불필요해진 에러 메시지 상태/표시 제거

`DoDo-Project#51`
- 탈퇴 성공 직후 clearTokens 호출을 제거하고 토큰 정리를 모달 이탈 시점으로 지연
- WithdrawalCompleteModal에서 홈 이동 시 clearTokens 수행
- RequireAuth 가드 아래에서 모달 노출 전 /auth로 리다이렉트되던 문제 해결

`DoDo-Project#51`
- useCooldown: 의존성을 isActive(seconds>0)로 변경해 타이머 매초 재생성 방지
- AuthCodeInput: 빈 슬롯보다 뒤 칸 포커스 시 첫 빈 슬롯으로 이동하는 onFocus 가드 추가

`DoDo-Project#51`
@sooloin
sooloin merged commit 871758a into DoDo-Project:develop Jun 23, 2026
1 of 3 checks passed
sooloin added a commit to sooloin/dodo-frontend that referenced this pull request Jun 23, 2026
@sooloin sooloin changed the title [Feat] 마이도도 로그아웃 및 회원 탈퇴 기능 구현 ✨ Feat: 마이도도 로그아웃 및 회원 탈퇴 기능 구현 Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새 기능 혹은 요구 사항 😸 조수빈 조수빈 파트

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 마이도도 회원정보 수정 및 로그아웃/탈퇴 UI 연동 [Feat] 마이도도 로그아웃 및 회원 탈퇴 기능 구현

2 participants