-
Notifications
You must be signed in to change notification settings - Fork 4
[Refactor] Observer pattern을 통한 전역 모달 관리 #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 5 commits
7c54bb8
95e1f35
dbb16da
9a63791
5988b12
fac343c
da52f43
509a915
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { useEffect, useState, type ReactNode } from "react"; | ||
| import { useLocation } from "react-router-dom"; | ||
|
|
||
| import { modalStore } from "@/shared/model/store"; | ||
|
|
||
| import { Modal } from "../../shared/ui/modal/modal"; | ||
|
|
||
| interface ModalItem { | ||
| id: string; | ||
| content: ReactNode; | ||
| autoPlay?: number; | ||
| } | ||
|
Comment on lines
+8
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial store의 여기 타입이 🤖 Prompt for AI Agents |
||
|
|
||
| export const ModalProvider = () => { | ||
| const { pathname } = useLocation(); | ||
| const [modals, setModals] = useState<ModalItem[]>([]); | ||
|
|
||
| useEffect(() => { | ||
| modalStore.subscribe(setModals); | ||
| return () => modalStore.unsubscribe(); | ||
| }, []); | ||
|
Comment on lines
+16
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 외부 store 구독은 지금 패턴은 🤖 Prompt for AI Agents |
||
|
|
||
| useEffect(() => { | ||
| modalStore.reset(); | ||
| }, [pathname]); | ||
|
|
||
| return ( | ||
| <> | ||
| {modals.map((modal) => { | ||
| return ( | ||
| <Modal | ||
| key={modal.id} | ||
| isOpen={true} | ||
| autoPlay={modal.autoPlay} | ||
| onClose={() => modalStore.close(modal.id)} | ||
| > | ||
| {modal.content} | ||
| </Modal> | ||
| ); | ||
| })} | ||
| </> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| import { EXPERIENCE_TYPE } from "@/shared/config/experience"; | ||
| import { parseYMD } from "@/shared/lib/format-date"; | ||
| import { modalStore } from "@/shared/model/store"; | ||
| import { ModalBasic, Tooltip } from "@/shared/ui"; | ||
| import { Button } from "@/shared/ui/button/button"; | ||
| import { useModal } from "@/shared/ui/modal/use-modal"; | ||
| import { Tag } from "@/shared/ui/tag/tag"; | ||
| import { Textfield } from "@/shared/ui/textfield/textfield"; | ||
| import { HELP_TOOLTIP_CONTENT } from "@/shared/ui/tooltip/tooltip.content"; | ||
|
|
@@ -24,9 +24,6 @@ const ExperienceViewer = () => { | |
| const { showEditDelete, onClickEdit, onClickDelete, onToggleDefault } = | ||
| useExperienceHeaderActions(); | ||
|
|
||
| const { isOpen: isDeleteModalOpen, handleModal: toggleDeleteModal } = | ||
| useModal(); | ||
|
|
||
| const startDate = current?.startAt ? parseYMD(current.startAt) : null; | ||
| const endDate = current?.endAt ? parseYMD(current.endAt) : null; | ||
|
|
||
|
|
@@ -42,6 +39,22 @@ const ExperienceViewer = () => { | |
|
|
||
| const typeLabel = current.type ? EXPERIENCE_TYPE[current.type] : "미지정"; | ||
|
|
||
| const handleOpenDeleteModal = () => { | ||
| modalStore.open( | ||
| <ModalBasic | ||
| title="이 경험을 삭제하시겠습니까?" | ||
| subTitle="작성한 내용은 즉시 제거되며, 복구할 수 없습니다." | ||
| closeText="취소" | ||
| confirmText="삭제" | ||
| onClose={() => modalStore.reset()} // 취소 시 닫기 | ||
| onConfirm={() => { | ||
| onClickDelete(); // 실제 삭제 동작 | ||
| modalStore.reset(); // 모달 닫기 | ||
| }} | ||
| /> | ||
| ); | ||
|
Comment on lines
+42
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 확인창에서
🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| return ( | ||
| <main className={s.page}> | ||
| <StickyHeader | ||
|
|
@@ -53,7 +66,7 @@ const ExperienceViewer = () => { | |
| <Button | ||
| variant="secondary" | ||
| size="small" | ||
| onClick={toggleDeleteModal} | ||
| onClick={handleOpenDeleteModal} | ||
| > | ||
| 삭제하기 | ||
| </Button> | ||
|
|
@@ -130,19 +143,6 @@ const ExperienceViewer = () => { | |
| </div> | ||
| </div> | ||
| </section> | ||
|
|
||
| <ModalBasic | ||
| title="이 경험을 삭제하시겠습니까?" | ||
| subTitle="작성한 내용은 즉시 제거되며, 복구할 수 없습니다." | ||
| closeText="취소" | ||
| confirmText="삭제" | ||
| isOpen={isDeleteModalOpen} | ||
| onClose={toggleDeleteModal} | ||
| onConfirm={() => { | ||
| toggleDeleteModal(); | ||
| onClickDelete(); | ||
| }} | ||
| /> | ||
| </main> | ||
| ); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { useNavigate } from "react-router-dom"; | ||
|
|
||
| import { ROUTES } from "@/app/routes/paths"; | ||
| import { Button, Modal, useModal } from "@/shared/ui"; | ||
| import { modalStore } from "@/shared/model/store"; | ||
| import { Button, Modal } from "@/shared/ui"; | ||
| import { | ||
| useGetExperience, | ||
| useGetCompanyList, | ||
|
|
@@ -19,53 +20,61 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => { | |
| const navigate = useNavigate(); | ||
| const { data } = useGetExperience(); // 경험 조회 API | ||
|
|
||
| /** Report 전역 상태 */ | ||
| // AI-Report 입력 단계 저장을 위한 전역 상태 | ||
| const setCompany = useReportStore((state) => state.setCompany); | ||
| const company = useReportStore((state) => state.company); | ||
|
|
||
| /** 모달 상태 관리 */ | ||
| const { autoPlay, isOpen, handleModal } = useModal(3000); // 몇 초 뒤 닫히게 할 건지 정의 | ||
| const alertModal = useModal(); // 경험 등록 여부 확인 모달 | ||
|
|
||
| /** 입력 데이터 상태 관리 */ | ||
| // 기업 입력 데이터 상태 관리 | ||
| const [inputValue, setInputValue] = useState(""); // 실시간 입력 상태 | ||
| const [searchKeyword, setSearchKeyword] = useState(""); // 디바운스된 키워드 상태 | ||
| const [selectedCompany, setSelectedCompany] = useState<Company | null>( | ||
| company | ||
| ); | ||
| const { data: searchResults = [] } = useGetCompanyList(searchKeyword); // 기업 검색 API | ||
|
|
||
| // 경험 등록 여부 확인 모달 오픈 | ||
| // 경험 등록 여부 확인 모달 | ||
| useEffect(() => { | ||
| if (data?.totalElements === 0) { | ||
| if (!alertModal.isOpen) { | ||
| alertModal.openModal(); | ||
| } | ||
| modalStore.open( | ||
| <> | ||
| <Modal.Content> | ||
| <Modal.Title>아직 등록된 경험이 없습니다</Modal.Title> | ||
| <Modal.SubTitle>지금 바로 경험을 등록하러 가볼까요?</Modal.SubTitle> | ||
| </Modal.Content> | ||
| <Modal.Buttons> | ||
| <Button variant="secondary" onClick={() => navigate(ROUTES.HOME)}> | ||
| 나가기 | ||
| </Button> | ||
| <Button | ||
| variant="primary" | ||
| onClick={() => navigate(ROUTES.EXPERIENCE_CREATE)} | ||
| > | ||
| 이동하기 | ||
| </Button> | ||
| </Modal.Buttons> | ||
| </> | ||
| ); | ||
| } | ||
| }, [data, alertModal]); | ||
| }, [data, navigate]); | ||
|
|
||
| // useEffect(() => { | ||
| // if (company?.id) { | ||
| // const temp={ | ||
| // id:company.id, | ||
| // name:company.name | ||
| // } | ||
| // setSelectedCompany(temp); | ||
| // } | ||
| // }, [company]); | ||
|
|
||
| const { data: searchResults = [] } = useGetCompanyList(searchKeyword); // 기업 검색 API | ||
|
|
||
| // 모달 닫힘 여부 확인 후 페이지 이동 | ||
| const prevIsOpen = useRef(isOpen); | ||
| useEffect(() => { | ||
| if (prevIsOpen.current === true && isOpen === false) { | ||
| if (selectedCompany) { | ||
| setCompany(selectedCompany); // 선택된 기업 데이터 저장 | ||
| const handleSearch = () => { | ||
| if (!selectedCompany) return; | ||
| // 기업 선택 후, 대기하는 모달 | ||
| modalStore.open( | ||
| <> | ||
| <Modal.Content type="auto"> | ||
| <Modal.Title>{selectedCompany.name}을 선택하셨습니다</Modal.Title> | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <Modal.SubTitle>기업분석 내용을 불러오는 중입니다.</Modal.SubTitle> | ||
| </Modal.Content> | ||
| <Modal.Image /> | ||
| </>, | ||
| 3000, | ||
| () => { | ||
| setCompany(selectedCompany); | ||
| onClick(); | ||
| } | ||
| } | ||
| prevIsOpen.current = isOpen; | ||
| }, [isOpen, selectedCompany, onClick, setCompany]); | ||
| ); | ||
| }; | ||
|
Comment on lines
+60
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 플로우는 재진입 방지가 필요합니다.
🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <div className={styles.layout}> | ||
|
|
@@ -77,39 +86,8 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => { | |
| onDebounceChange={setSearchKeyword} | ||
| selectedItem={selectedCompany} | ||
| onSelect={setSelectedCompany} | ||
| onSearch={handleModal} | ||
| onSearch={handleSearch} | ||
| /> | ||
| {/** 경험 등록 여부 확인 모달 */} | ||
| <Modal isOpen={alertModal.isOpen} onClose={alertModal.closeModal}> | ||
| <Modal.Content> | ||
| <Modal.Title>아직 등록된 경험이 없습니다</Modal.Title> | ||
| <Modal.SubTitle>지금 바로 경험을 등록하러 가볼까요?</Modal.SubTitle> | ||
| </Modal.Content> | ||
| <Modal.Buttons> | ||
| <Button | ||
| variant="secondary" | ||
| size="large" | ||
| onClick={() => navigate(ROUTES.HOME)} | ||
| > | ||
| 나가기 | ||
| </Button> | ||
| <Button | ||
| variant="primary" | ||
| size="large" | ||
| onClick={() => navigate(ROUTES.EXPERIENCE_CREATE)} | ||
| > | ||
| 이동하기 | ||
| </Button> | ||
| </Modal.Buttons> | ||
| </Modal> | ||
| {/** 기업 선택 후, 대기 모달 */} | ||
| <Modal autoPlay={autoPlay} isOpen={isOpen} onClose={handleModal}> | ||
| <Modal.Content type="auto"> | ||
| <Modal.Title>{selectedCompany?.name}을 선택하셨습니다</Modal.Title> | ||
| <Modal.SubTitle>기업분석 내용을 불러오는 중입니다.</Modal.SubTitle> | ||
| </Modal.Content> | ||
| <Modal.Image /> | ||
| </Modal> | ||
| </div> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
깊은 상대 경로 import는 alias로 통일해주세요.
이 파일은 Line 4에서 이미 alias import를 쓰고 있는데,
../../shared/ui/modal/modal만 상대 경로라 이동/리팩터링에 더 취약합니다.@/shared/ui/modal/modal로 맞추는 편이 일관성과 유지보수성에 좋습니다. As per coding guidelines, "@shared,@features,@widgets등 alias 기반 절대 경로 import 사용 권장" and "깊은 상대 경로(../../..) 사용은 가독성과 유지보수 측면에서 지양".🤖 Prompt for AI Agents