Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,91 @@
'use client';

import ResetPasswordModal from '@/components/common/Modal/content/ResetPasswordModal';
import { postResetPasswordToEmail } from '@/lib/apis/user';
import { ResetPasswordToEmailBody } from '@/lib/apis/user/type';
import { useModalStore } from '@/store/useModalStore';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';

export default function OpenPasswordResetModal({ ...props }) {
const { openModal } = useModalStore();
const router = useRouter();

useEffect(() => {
const unsubscribe = useModalStore.subscribe(
(state) => state.requestBody, // selector
// listener
(newRequestBody) => {
if (!newRequestBody) return;
const { email } = newRequestBody as { email: string };

if (!email) return;

const requestBody = {
email,
redirectUrl: `${window.location.origin}/`,
};
handleSendResetPasswordLink(requestBody);
}
);
return () => {
unsubscribe(); // 언마운트, 구독 해제
};
}, []);

// 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');
Comment on lines +54 to +55
Copy link
Contributor

Choose a reason for hiding this comment

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

사용자가 가입되지 않은 이메일을 입력한 경우 회원가입 페이지로 리다이렉트 하는 것도 좋다고 생각하지만, 만약 회원가입한 유저가 이메일을 잘못 입력한 경우 매번 회원가입 페이지로 이동하게 된다면 조금 피로도가 생길 것 같기도 합니다..!
회원가입 페이지로 리다이렉트 없이 존재하지 않는 이메일이라는 에러 토스트만 띄워줘도 괜찮지 않을까 생각합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

의견 감사합니다! 다음 PR에서 반영하겠습니다 ~!

} 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 호출 방식과의 일관성을 위해 빈 함수 전달
onRequest: () => {},
Copy link
Contributor

@hyeonjiroh hyeonjiroh May 21, 2025

Choose a reason for hiding this comment

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

이번에 모달 컴포넌트를 확장하기 전에 onRequest를 필수로 해놨는데, 해명님이 사용하신 form 태그에서 onSubmit으로 제출하는 경우에는 onRequest가 따로 필요 없군요...! 나중에 해당 onRequest 옵션은 옵셔널로 바꿀까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

흠 저도 이 부분을 고민하긴 했는데, 다른 모달 컴포넌트 일관성을 고려했을 때는 남겨둬도 좋을 것 같아요. onRequest 가 기존 모달 컴포넌트들에서는 필수 콜백함수이기도 하고요 ! 현지님 생각은 어떠신가요?

Copy link
Contributor

Choose a reason for hiding this comment

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

해명님 말씀처럼 기존 모달들, 특히 폼이 없는 모달들의 경우에는 필수로 있어야 되겠네요..! 그럼 우선은 필수로 해놓는 걸로 합시다~! 의견 감사합니다!

},
},

(() => <ResetPasswordModal />)()
);
}}
{...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,5 @@
'use client';

import ForgotPasswordButton from '@/app/(auth)/login/_components/LoginForm/ForgotPasswordButton';
import InputWithLabel from '@/components/auth/InputWithLabel';
import Button from '@/components/common/Button';
import { signIn } from '@/lib/apis/auth';
Expand All @@ -17,6 +16,7 @@ import Cookies from 'js-cookie';
import { z } from 'zod';
import { useRouter } from 'next/navigation';
import { InputType } from '@/components/auth/type';
import OpenPasswordResetModal from '@/app/(auth)/login/_components/LoginForm/OpenPasswordResetModal';

// schema
const inputEmptySchema = z.object({
Expand Down Expand Up @@ -205,7 +205,7 @@ export default function LoginForm() {
/>
</div>

<ForgotPasswordButton />
<OpenPasswordResetModal />

<Button
size="lg"
Expand Down

This file was deleted.

36 changes: 36 additions & 0 deletions src/components/common/Modal/content/ResetPasswordModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';

import InputBase from '@/components/common/Input/InputBase';
import { useModalStore } from '@/store/useModalStore';
import { validateEmail } from '@/utils/inputValidation';
import { useRef } from 'react';

export default function ResetPasswordModal() {
const inputRef = useRef<HTMLInputElement>(null);
const { setRequestBody, closeModal } = useModalStore.getState();

return (
<form
id="reset-password-form"
onSubmit={(e) => {
e.preventDefault();

// 유효성 검사
const email = inputRef.current?.value.trim();
if (!email || !validateEmail(email)) return;

const requestBody = { email };
setRequestBody(requestBody); // 상태 변경, useEffect 에서 api 호출 위해 필요

// options.button?.onRequest?.(requestBody);
closeModal();
}}
>
<InputBase
ref={inputRef}
placeholder="이메일을 입력하세요."
defaultValue="" // 초기값 설정
/>
</form>
);
}
39 changes: 34 additions & 5 deletions src/components/common/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,43 @@ 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;

// button 클릭 시 실행되는 함수 (form 존재 여부에 따른 분기 처리 )
const handleRequest = () => {
button?.onRequest?.(requestBody);
closeModal();
isSubmittedRef.current = true;

// 기존 모달 흐름
if (!formId) {
if (isSubmittedRef.current) {
button?.onRequest?.(requestBody);
closeModal();
isSubmittedRef.current = false;
console.log('handleRequest 실행');
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분은 디버깅을 위한 console.log일까요?? 그렇다면 나중에 지워주시면 좋을 것 같습니다!

return;
}
}

// formId 존재 (브라우저 submit 트리거)
const form = document.getElementById(
formId as string
) as HTMLFormElement | null;
if (form) {
form.requestSubmit(); // 폼 제출만 담당
}
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 +106,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 +127,7 @@ export default function Modal() {
</Button>
)}
<Button
{...(formId ? { form: formId } : {})} // formId 존재 여부에 따라 form 속성 추가
variant="primary"
styleType={variant === 'danger' ? 'danger' : 'filled'}
className="flex-1"
Expand Down
51 changes: 33 additions & 18 deletions src/store/useModalStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { ReactNode } from 'react';
import { subscribeWithSelector } from 'zustand/middleware';

interface ModalOptions {
variant?: 'default' | 'danger' | 'taskForm';
Expand All @@ -9,33 +10,47 @@ 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;
}

export const useModalStore = create<ModalState>((set) => ({
options: {},
content: null,
requestBody: null,
setRequestBody: (body) => set({ requestBody: body }),
isButtonDisabled: false,
setIsButtonDisabled: (isButtonDisabled) => set({ isButtonDisabled }),
openModal: (options, content) => set({ options, content }),
closeModal: () =>
set({
options: {},
content: null,
requestBody: null,
isButtonDisabled: false,
}),
}));
export const useModalStore = create<
ModalState,
[['zustand/subscribeWithSelector', never]]
>(
subscribeWithSelector((set) => ({
options: {},
content: null,
requestBody: null,
setRequestBody: (body) => set({ requestBody: body }),
isButtonDisabled: false,
setIsButtonDisabled: (isButtonDisabled) => set({ isButtonDisabled }),
openModal: (options, content) => {
const resolvedContent =
typeof content === 'function' ? content() : content; // 함수형 content 처리 (IIFE)
set({ options, content: resolvedContent });
},

closeModal: () =>
set({
options: {},
content: null,
requestBody: null,
isButtonDisabled: false,
}),
}))
);