Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d9c55b0
feat: ResetPasswordLinkModal IIFE 로 호출되도록 변경 / options.button 에 formI…
jjanie00 May 17, 2025
32ef02b
feat: form 태그에 id 추가 / 폼 제출 시 requestBody 설정
jjanie00 May 17, 2025
ac18ce5
feat: IIFE 호출방식에 따른 타이밍 제어 / Modal 버튼에 formId 기반 form 연결 로직 추가
jjanie00 May 17, 2025
9db5472
feat: 모달 IIFE 호출 지원을 위한 타입 확장 / formId 처리 로직 추가
jjanie00 May 17, 2025
c47aaf1
chore(useModalStore): 주석 변경
jjanie00 May 17, 2025
9bb4e6d
chore: handleEmailValidation 으로 이메일 유효성 검사 함수명 수정
jjanie00 May 18, 2025
af0f46e
feat: requestBody 설정 및 비밀번호 재설정 이메일 api 호출
jjanie00 May 19, 2025
dbcdcc4
feat: 비밀번호 재설정 요청 성공 시 토스트 메시지 및 모달 종료 처리
jjanie00 May 19, 2025
48f626a
feat: 204 error 한글 토스트 메시지로 변환 및 signup 으로 리다이렉트
jjanie00 May 19, 2025
7479f1e
chore: 불필요한 closeModal import 삭제
jjanie00 May 19, 2025
41fa164
refactor: PasswordResetRequestModal 로 컴포넌트명 수정
jjanie00 May 19, 2025
3c585b2
refactor: handleEmailValidation 로직 onRequest 내부로 이동
jjanie00 May 21, 2025
372717b
refactor: 유효성 검사 함수 이동에 따라 onSubmit 내부에서 해당 함수 호출 삭제
jjanie00 May 21, 2025
d6b21e3
fix(Modal.tsx): useEffect 내부 로직 handleRequest 내부로 이동시켜 taskList에서 api…
jjanie00 May 21, 2025
734fa9e
Merge remote-tracking branch 'origin/dev' into feat/#177/reset-passwo…
jjanie00 May 21, 2025
f758015
feat: subscribe 사용해 requestBody 상태 구독 추가
jjanie00 May 21, 2025
395f1a8
refactor: OpenPasswordResetModal 로 컴포넌트명 변경에 따른 import 문 수정
jjanie00 May 21, 2025
205486a
refactor: form 존재 여부에 따른 handleRequest 함수 분기 처리
jjanie00 May 21, 2025
0be972f
refactor(useModalStore): subscribeWithSelector 미들웨어 도입으로 모달 상태 구독 방식 개선
jjanie00 May 21, 2025
6c58ff8
feat(ResetPasswordModal): form 내부에서 유효성 검사 및 상태 설정 로직 통합
jjanie00 May 21, 2025
90b1fc4
feat(OpenPasswordResetModal): useEffect 내 requestBody 구독을 통한 api 호출 트…
jjanie00 May 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use client';

import ResetPasswordLinkModal from '@/components/common/Modal/content/ResetPasswordLinkModal';
import { postResetPasswordToEmail } from '@/lib/apis/user';
import { ResetPasswordToEmailBody } from '@/lib/apis/user/type';
import { useModalStore } from '@/store/useModalStore';
import { validateEmail } from '@/utils/inputValidation';
import { useRouter } from 'next/navigation';
import { useRef } from 'react';
import { toast } from 'react-toastify';

export default function ForgotPasswordButton({ ...props }) {
const { openModal } = useModalStore();
const router = useRouter();
const inputRef = useRef<HTMLInputElement>(null);
const { setRequestBody } = useModalStore();

// send reset password link
const handleSendResetPasswordLink = async (
requestBody: ResetPasswordToEmailBody
) => {
try {
const response = await postResetPasswordToEmail({ body: requestBody });

if (!response) {
throw new Error('204 : No Content');
}
toast.success('비밀번호 재설정 링크가 전송되었습니다.');
} catch (error) {
if (error instanceof Error) {
const errorMessage = error.message;

// 존재하지 않는 유저 : User not found
if (errorMessage.includes('User not found')) {
toast.error('존재하지 않는 이메일입니다. 회원가입을 먼저 해주세요.');
router.push('/signup');
} else {
toast.error(`Error : ${error}`);
}
}
}
};

return (
<div className="mt-3 mb-10 flex justify-end">
<button
className="leading-normal font-medium text-emerald-500 underline"
type="button"
onClick={() => {
openModal(
{
title: '비밀번호 재설정',
description: '비밀번호 재설정 링크를 보내드립니다.',
button: {
formId: 'reset-password-form', // formId 연결
number: 2,
text: '링크 보내기',
onRequest: () => {
// 이메일 유효성 검사
const email = inputRef.current?.value.trim();
if (!email) return;
if (validateEmail(email)) {
setRequestBody({ email });
}

// const { email } = newBody as { email: string };

const requestBodyApi: ResetPasswordToEmailBody = {
email,
redirectUrl: `${window.location.origin}/`,
};

console.log('requestBody:', requestBodyApi); // x
handleSendResetPasswordLink(requestBodyApi);
},
},
},

(() => <ResetPasswordLinkModal />)()
);
}}
{...props}
>
비밀번호를 잊으셨나요?
</button>
</div>
);
}
4 changes: 2 additions & 2 deletions src/app/(auth)/login/_components/LoginForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import ForgotPasswordButton from '@/app/(auth)/login/_components/LoginForm/ForgotPasswordButton';
import PasswordResetRequestModal from '@/app/(auth)/login/_components/LoginForm/PasswordResetRequestModal';
import InputWithLabel from '@/components/auth/InputWithLabel';
import Button from '@/components/common/Button';
import { signIn } from '@/lib/apis/auth';
Expand Down Expand Up @@ -205,7 +205,7 @@ export default function LoginForm() {
/>
</div>

<ForgotPasswordButton />
<PasswordResetRequestModal />

<Button
size="lg"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
'use client';

import InputBase from '@/components/common/Input/InputBase';
import { useRef } from 'react';

export default function ResetPasswordLinkModal() {
const inputRef = useRef<HTMLInputElement>(null);

return (
<div>
<InputBase placeholder="이메일을 입력하세요." />
</div>
<form
id="reset-password-form"
onSubmit={(e) => {
e.preventDefault();
}}
>
<InputBase
ref={inputRef}
placeholder="이메일을 입력하세요."
defaultValue="" // 초기값 설정
/>
</form>
);
}
26 changes: 21 additions & 5 deletions src/components/common/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,30 @@ export default function Modal() {
closeModal,
} = useModalStore();

const formId = button?.formId; // formId 추출
const modalRef = useRef<HTMLDivElement>(null);
const isModalOpen = Boolean(title || content);

// 폼 제출 시도 여부를 추적하는 플래그
const isSubmittedRef = useRef(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

수정 이후 <form> 태그에서도 해당 ref 는 사용하고 있지 않은 상황이라, 추후 삭제해도 괜찮을 것 같습니다 ~!


useClosePopup(modalRef, closeModal);
useLockBackgroundScroll(isModalOpen);

if (!isModalOpen) return null;

// ResetPasswordModal IIFE 호출에 따라 onRequest 실행 시점 조절을 위한 타이밍 처리
const handleRequest = () => {
button?.onRequest?.(requestBody);
closeModal();
isSubmittedRef.current = true;
console.log('handleRequest');
if (isSubmittedRef.current) {
button?.onRequest?.(requestBody);
console.log('handleRequest 실행');
closeModal();
isSubmittedRef.current = false;
}
};

if (!isModalOpen) return null;

return (
<div className="tablet:items-center fixed inset-0 z-80 flex h-full w-full items-end justify-center bg-black/50">
<div
Expand Down Expand Up @@ -82,7 +93,11 @@ export default function Modal() {
)}
</div>
)}
{content && <div className="overflow-y-auto">{content}</div>}
{content && (
<div className="overflow-y-auto">
{typeof content === 'function' ? content() : content}
</div>
)}
</div>
</div>
<div className="flex gap-2">
Expand All @@ -99,6 +114,7 @@ export default function Modal() {
</Button>
)}
<Button
{...(formId ? { form: formId } : {})} // formId 존재 여부에 따라 form 속성 추가
variant="primary"
styleType={variant === 'danger' ? 'danger' : 'filled'}
className="flex-1"
Expand Down
14 changes: 11 additions & 3 deletions src/store/useModalStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ interface ModalOptions {
number: 1 | 2;
text: string;
onRequest: (body?: unknown) => void;
formId?: string; // 외부 form 과 연결하기 위한 HTML form 속성
};
}

interface ModalState {
options: ModalOptions;
content: ReactNode | null;
content: ReactNode | (() => ReactNode) | null;
requestBody: unknown;
setRequestBody: (body: unknown) => void;
isButtonDisabled: boolean;
setIsButtonDisabled: (isValid: boolean) => void;
openModal: (options: ModalOptions, content?: ReactNode) => void;
openModal: (
options: ModalOptions,
content?: ReactNode | (() => ReactNode) // 함수형 컴포넌트 호출(IIFE) 지원 위한 타입 확장
) => void;
closeModal: () => void;
}

Expand All @@ -30,7 +34,11 @@ export const useModalStore = create<ModalState>((set) => ({
setRequestBody: (body) => set({ requestBody: body }),
isButtonDisabled: false,
setIsButtonDisabled: (isButtonDisabled) => set({ isButtonDisabled }),
openModal: (options, content) => set({ options, content }),
openModal: (options, content) => {
const resolvedContent = typeof content === 'function' ? content() : content; // 함수형 content 처리 (IIFE)
set({ options, content: resolvedContent });
},

closeModal: () =>
set({
options: {},
Expand Down