From 52217279920e3e81f0820b3f40775ec1e797f1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Sun, 7 Sep 2025 19:16:39 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20AI=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EB=AA=A8=EB=8B=AC=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiFailModal/AiFailModal.stories.tsx | 4 +++ .../component/AiFailModal/AiFailModal.tsx | 25 ++++++++++++++----- .../AiRecommendModal/AiRecommendModal.tsx | 7 ++---- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/common/component/AiFailModal/AiFailModal.stories.tsx b/src/common/component/AiFailModal/AiFailModal.stories.tsx index c9c0d414..b48e1f88 100644 --- a/src/common/component/AiFailModal/AiFailModal.stories.tsx +++ b/src/common/component/AiFailModal/AiFailModal.stories.tsx @@ -9,6 +9,9 @@ const meta = { layout: 'centered', }, tags: ['autodocs'], + argTypes: { + onClose: { action: 'closed' }, + }, } satisfies Meta; export default meta; @@ -17,5 +20,6 @@ type Story = StoryObj; export const Default: Story = { args: { onClose: () => {}, + message: '다시 한 번 시도해주세요.', }, }; diff --git a/src/common/component/AiFailModal/AiFailModal.tsx b/src/common/component/AiFailModal/AiFailModal.tsx index fbdbfee2..0cba2da3 100644 --- a/src/common/component/AiFailModal/AiFailModal.tsx +++ b/src/common/component/AiFailModal/AiFailModal.tsx @@ -1,5 +1,7 @@ +import * as styles from './AiFailModal.css'; + import Button from '@/common/component/Button/Button'; -import Modal from '@/common/component/Modal/Modal'; +import { IcModalDelete } from '@/assets/svg'; interface AiFailModalProps { onClose: () => void; @@ -7,11 +9,22 @@ interface AiFailModalProps { } const AiFailModal = ({ onClose, message = '다시 한 번 시도해주세요.' }: AiFailModalProps) => ( - -

AI 추천 실패

-

{message}

- + ); + }; + return ; + }, + args: { + onClose: () => {}, + message: '다시 한 번 시도해주세요.', + }, +}; diff --git a/src/common/component/AiRecommendModal/AiRecommendModal.stories.tsx b/src/common/component/AiRecommendModal/AiRecommendModal.stories.tsx index 9c851010..fd89959d 100644 --- a/src/common/component/AiRecommendModal/AiRecommendModal.stories.tsx +++ b/src/common/component/AiRecommendModal/AiRecommendModal.stories.tsx @@ -3,6 +3,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import AiRecommendModal from './AiRecommendModal'; +import { useOverlayModal } from '@/common/hook/useOverlayModal'; + const queryClient = new QueryClient(); const meta = { @@ -29,8 +31,33 @@ export const Default: Story = { args: { onClose: () => {}, onSubmit: (selected) => console.log('Selected options:', selected), - values: [], - options: ['옵션1', '옵션2'], - mandalartId: 1, + values: ['', '', '', '', '', '', '', ''], + options: ['옵션1', '옵션2', '옵션3', '옵션4'], + mandalartId: 0, + }, +}; + +export const InOverlay: Story = { + render: (args) => { + const Demo = () => { + const { openModal } = useOverlayModal(); + return ( + + ); + }; + return ; + }, + args: { + onSubmit: (selected) => console.log('Selected:', selected), + values: ['', '', '', '', '', '', '', ''], + options: ['추천1', '추천2', '추천3', '추천4'], + mandalartId: 0, + onClose: () => {}, }, }; diff --git a/src/common/hook/useModal.tsx b/src/common/hook/useModal.tsx deleted file mode 100644 index aeb6bf22..00000000 --- a/src/common/hook/useModal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useCallback, useState } from 'react'; - -import Modal from '../component/Modal/Modal'; - -export const useModal = () => { - const [isOpen, setIsOpen] = useState(false); - const [content, setContent] = useState(null); - - const openModal = useCallback((modalContent: React.ReactNode) => { - setContent(modalContent); - setIsOpen(true); - }, []); - - const closeModal = useCallback(() => { - setIsOpen(false); - setContent(null); - }, []); - - const ModalWrapper = isOpen ? {content} : null; - - return { openModal, closeModal, ModalWrapper }; -}; diff --git a/src/common/hook/useOverlayModal.tsx b/src/common/hook/useOverlayModal.tsx new file mode 100644 index 00000000..bf5ee94c --- /dev/null +++ b/src/common/hook/useOverlayModal.tsx @@ -0,0 +1,29 @@ +import React, { useRef } from 'react'; +import type { ReactNode } from 'react'; +import { overlay } from 'overlay-kit'; + +import Modal from '@/common/component/Modal/Modal'; + +export const useOverlayModal = () => { + const lastIdRef = useRef(null); + + const openModal = (node: ReactNode) => { + const id = overlay.open(({ unmount }) => { + const handleClose = () => unmount(); + const content = React.isValidElement(node) + ? React.cloneElement(node as any, { onClose: handleClose }) + : node; + return {content}; + }); + lastIdRef.current = id; + }; + + const closeModal = () => { + if (lastIdRef.current) { + overlay.unmount(lastIdRef.current); + lastIdRef.current = null; + } + }; + + return { openModal, closeModal }; +}; diff --git a/src/main.tsx b/src/main.tsx index da800e2c..f6512184 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,16 +2,18 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { OverlayProvider } from 'overlay-kit'; import App from './App.tsx'; import { queryClient } from './common/util/queryClient.ts'; - import './style/global.css.ts'; createRoot(document.getElementById('root')!).render( - + + + {import.meta.env.DEV && } , diff --git a/src/page/todo/lowerTodo/LowerTodo.tsx b/src/page/todo/lowerTodo/LowerTodo.tsx index f44e5826..e0e6724a 100644 --- a/src/page/todo/lowerTodo/LowerTodo.tsx +++ b/src/page/todo/lowerTodo/LowerTodo.tsx @@ -10,7 +10,7 @@ import { PATH } from '@/route'; import { IcSmallNext } from '@/assets/svg'; import GradientBackground from '@/common/component/Background/GradientBackground'; import Tooltip from '@/common/component/Tooltip/Tooltip'; -import { useModal } from '@/common/hook/useModal'; +import { useOverlayModal } from '@/common/hook/useOverlayModal'; import AiRecommendModal from '@/common/component/AiRecommendModal/AiRecommendModal'; import AiFailModal from '@/common/component/AiFailModal/AiFailModal'; import Mandalart from '@/common/component/Mandalart/Mandalart'; @@ -36,7 +36,7 @@ interface TodoItem { const LowerTodo = ({ userName = '김도트' }: LowerTodoProps) => { const navigate = useNavigate(); - const { openModal, ModalWrapper, closeModal } = useModal(); + const { openModal, closeModal } = useOverlayModal(); const [selectedGoalIndex, setSelectedGoalIndex] = useState(0); const [allTodos, setAllTodos] = useState( Array(8) @@ -538,7 +538,6 @@ const LowerTodo = ({ userName = '김도트' }: LowerTodoProps) => { } /> - {ModalWrapper} ); diff --git a/src/page/todo/upperTodo/UpperTodo.tsx b/src/page/todo/upperTodo/UpperTodo.tsx index e4935014..734e68bb 100644 --- a/src/page/todo/upperTodo/UpperTodo.tsx +++ b/src/page/todo/upperTodo/UpperTodo.tsx @@ -10,7 +10,7 @@ import { useUpperTodoState } from './hook/useUpperTodoState'; import AiRecommendModal from '@/common/component/AiRecommendModal/AiRecommendModal'; import GradientBackground from '@/common/component/Background/GradientBackground'; -import { useModal } from '@/common/hook/useModal'; +import { useOverlayModal } from '@/common/hook/useOverlayModal'; import { PATH } from '@/route'; import { usePostAiRecommendCoreGoal } from '@/api/domain/upperTodo/hook'; @@ -35,7 +35,7 @@ const extractTitles = (goals: { title: string }[]) => goals.map((item) => item.t const UpperTodo = ({ userName = '김도트' }: UpperTodoProps) => { const mandalartId = 1; - const { openModal, ModalWrapper, closeModal } = useModal(); + const { openModal, closeModal } = useOverlayModal(); const navigate = useNavigate(); const { @@ -162,7 +162,6 @@ const UpperTodo = ({ userName = '김도트' }: UpperTodoProps) => { hasFilledSubGoals={hasFilledSubGoals} handleNavigateLower={handleNavigateLower} /> - {ModalWrapper} ); From 487f62762a155ccdee041a11ca8463fbf7d16007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Mon, 8 Sep 2025 15:20:43 +0900 Subject: [PATCH 04/17] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/hook/useOverlayModal.tsx | 34 ++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/common/hook/useOverlayModal.tsx b/src/common/hook/useOverlayModal.tsx index bf5ee94c..9fb50bab 100644 --- a/src/common/hook/useOverlayModal.tsx +++ b/src/common/hook/useOverlayModal.tsx @@ -1,29 +1,43 @@ import React, { useRef } from 'react'; -import type { ReactNode } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import { overlay } from 'overlay-kit'; import Modal from '@/common/component/Modal/Modal'; +type Closeable = { onClose: () => void }; +type OpenOptions = { withWrapper?: boolean }; + export const useOverlayModal = () => { - const lastIdRef = useRef(null); + const idsRef = useRef([]); + + const openModal = (node: ReactNode, options: OpenOptions = {}) => { + const { withWrapper = true } = options; - const openModal = (node: ReactNode) => { const id = overlay.open(({ unmount }) => { const handleClose = () => unmount(); const content = React.isValidElement(node) - ? React.cloneElement(node as any, { onClose: handleClose }) + ? React.cloneElement(node as ReactElement>, { onClose: handleClose }) : node; - return {content}; + return withWrapper ? {content} : <>{content}; }); - lastIdRef.current = id; + + idsRef.current.push(id); + return { id, close: () => overlay.unmount(id) }; }; const closeModal = () => { - if (lastIdRef.current) { - overlay.unmount(lastIdRef.current); - lastIdRef.current = null; + const id = idsRef.current.pop(); + if (id) { + overlay.unmount(id); + } + }; + + const closeAll = () => { + while (idsRef.current.length) { + const id = idsRef.current.pop()!; + overlay.unmount(id); } }; - return { openModal, closeModal }; + return { openModal, closeModal, closeAll }; }; From 1d37734abf67c279cf130040bd23bbc2c232623c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Mon, 8 Sep 2025 17:41:45 +0900 Subject: [PATCH 05/17] =?UTF-8?q?refactor:=20=EC=98=B5=EC=85=98=20undefine?= =?UTF-8?q?d=20=EC=8B=9C=20=EB=AA=A9=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiRecommendModal/AiRecommendModal.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/common/component/AiRecommendModal/AiRecommendModal.tsx b/src/common/component/AiRecommendModal/AiRecommendModal.tsx index cc239406..a0a4b223 100644 --- a/src/common/component/AiRecommendModal/AiRecommendModal.tsx +++ b/src/common/component/AiRecommendModal/AiRecommendModal.tsx @@ -28,21 +28,7 @@ const AiRecommendModal = ({ const emptyCount = values.filter((v) => v.trim() === '').length; const remainingSelections = emptyCount - selectedOptions.length; - const displayOptions = - options && options.length > 0 - ? options - : options === undefined - ? [ - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜1', - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜2', - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜3', - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜4', - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜5', - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜6', - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜7', - '와 이거 진짜같은데 와이거 진짜같은데 와 이거 진짜8', - ] - : []; + const displayOptions = Array.isArray(options) && options.length > 0 ? options : []; const toggleOption = (option: string) => { setSelectedOptions((prev) => From b8c02280c72bc3c56bf78258cf92c69fdb4f5eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Tue, 9 Sep 2025 13:30:15 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EB=B2=84=ED=8A=BC=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=20=EA=B3=B5=ED=86=B5=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/AiFailModal/AiFailModal.css.ts | 8 -------- src/common/component/AiFailModal/AiFailModal.tsx | 14 +++++++------- .../component/AiModalBase/AiModalBase.css.ts | 7 +++++++ src/common/component/AiModalBase/AiModalBase.tsx | 3 +++ .../AiRecommendModal/AiRecommendModal.css.ts | 1 - .../AiRecommendModal/AiRecommendModal.tsx | 4 +--- 6 files changed, 18 insertions(+), 19 deletions(-) delete mode 100644 src/common/component/AiFailModal/AiFailModal.css.ts diff --git a/src/common/component/AiFailModal/AiFailModal.css.ts b/src/common/component/AiFailModal/AiFailModal.css.ts deleted file mode 100644 index a39f5e6a..00000000 --- a/src/common/component/AiFailModal/AiFailModal.css.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -export const buttonWrapper = style({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - marginTop: '5rem', -}); diff --git a/src/common/component/AiFailModal/AiFailModal.tsx b/src/common/component/AiFailModal/AiFailModal.tsx index 1386ba9c..2890646b 100644 --- a/src/common/component/AiFailModal/AiFailModal.tsx +++ b/src/common/component/AiFailModal/AiFailModal.tsx @@ -1,5 +1,3 @@ -import * as styles from './AiFailModal.css'; - import AiModalBase from '@/common/component/AiModalBase/AiModalBase'; import Button from '@/common/component/Button/Button'; @@ -9,11 +7,13 @@ interface AiFailModalProps { } const AiFailModal = ({ onClose, message = '다시 한 번 시도해주세요.' }: AiFailModalProps) => ( - -
-
-
+ } + /> ); export default AiFailModal; diff --git a/src/common/component/AiModalBase/AiModalBase.css.ts b/src/common/component/AiModalBase/AiModalBase.css.ts index beb78f79..87d546b0 100644 --- a/src/common/component/AiModalBase/AiModalBase.css.ts +++ b/src/common/component/AiModalBase/AiModalBase.css.ts @@ -50,3 +50,10 @@ export const description = style({ ...fonts.subtitle06, marginTop: '0.9rem', }); + +export const footerWrapper = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginTop: '5rem', +}); diff --git a/src/common/component/AiModalBase/AiModalBase.tsx b/src/common/component/AiModalBase/AiModalBase.tsx index d163d817..5a79f61d 100644 --- a/src/common/component/AiModalBase/AiModalBase.tsx +++ b/src/common/component/AiModalBase/AiModalBase.tsx @@ -13,6 +13,7 @@ interface AiModalBaseProps { titleClassName?: string; descriptionClassName?: string; titleId?: string; + footer?: ReactNode; } const AiModalBase = ({ @@ -23,6 +24,7 @@ const AiModalBase = ({ titleClassName, descriptionClassName, titleId = 'ai-modal-title', + footer, }: AiModalBaseProps) => { return (
@@ -39,6 +41,7 @@ const AiModalBase = ({ ) : null}
{children} + {footer ?
{footer}
: null} ); diff --git a/src/common/component/AiRecommendModal/AiRecommendModal.css.ts b/src/common/component/AiRecommendModal/AiRecommendModal.css.ts index 02c75549..25d45720 100644 --- a/src/common/component/AiRecommendModal/AiRecommendModal.css.ts +++ b/src/common/component/AiRecommendModal/AiRecommendModal.css.ts @@ -12,7 +12,6 @@ export const listWrapper = style({ alignItems: 'flex-start', gap: '2rem', marginTop: '3.9rem', - marginBottom: '5rem', }); export const listItem = style({ diff --git a/src/common/component/AiRecommendModal/AiRecommendModal.tsx b/src/common/component/AiRecommendModal/AiRecommendModal.tsx index a0a4b223..9d63edef 100644 --- a/src/common/component/AiRecommendModal/AiRecommendModal.tsx +++ b/src/common/component/AiRecommendModal/AiRecommendModal.tsx @@ -72,6 +72,7 @@ const AiRecommendModal = ({ } titleId="modal-title" + footer={; -}; - -export default MandalButton; diff --git a/src/page/todo/lowerTodo/LowerTodo.tsx b/src/page/todo/lowerTodo/LowerTodo.tsx index e0e6724a..141adfd7 100644 --- a/src/page/todo/lowerTodo/LowerTodo.tsx +++ b/src/page/todo/lowerTodo/LowerTodo.tsx @@ -351,12 +351,10 @@ const LowerTodo = ({ userName = '김도트' }: LowerTodoProps) => { console.log('subGoals[newIndex]:', subGoals[newIndex]); }; - const handleApplyAiRecommendedGoals = async ( - selected: { id: number; position: number; title: string; cycle?: string }[], - ) => { + const handleApplyAiRecommendedGoals = async (selected: { title: string }[]) => { const goals = selected.map((item) => ({ title: item.title, - cycle: item.cycle || 'DAILY', + cycle: 'DAILY', })); try { if (!selectedCoreGoalId) { diff --git a/src/page/todo/upperTodo/UpperTodo.tsx b/src/page/todo/upperTodo/UpperTodo.tsx index 734e68bb..21ec44a4 100644 --- a/src/page/todo/upperTodo/UpperTodo.tsx +++ b/src/page/todo/upperTodo/UpperTodo.tsx @@ -12,7 +12,10 @@ import AiRecommendModal from '@/common/component/AiRecommendModal/AiRecommendMod import GradientBackground from '@/common/component/Background/GradientBackground'; import { useOverlayModal } from '@/common/hook/useOverlayModal'; import { PATH } from '@/route'; -import { usePostAiRecommendCoreGoal } from '@/api/domain/upperTodo/hook'; +import { + usePostAiRecommendCoreGoal, + usePostAiRecommendToCoreGoals, +} from '@/api/domain/upperTodo/hook'; interface UpperTodoProps { userName?: string; @@ -53,6 +56,7 @@ const UpperTodo = ({ userName = '김도트' }: UpperTodoProps) => { } = useUpperTodoState(mandalartId); const postAiRecommend = usePostAiRecommendCoreGoal(); + const postRecommendToCore = usePostAiRecommendToCoreGoals(); const mainGoal = data?.title || '사용자가 작성한 대목표'; @@ -60,12 +64,28 @@ const UpperTodo = ({ userName = '김도트' }: UpperTodoProps) => { navigate(PATH.TODO_LOWER); }; - const handleAiSubmit = (responseData: { id: number; position: number; title: string }[]) => { - setAiResponseData(responseData); - const updatedSubGoals = updateSubGoalsWithAiResponse(subGoals, responseData); - setSubGoals(updatedSubGoals); - refetchCoreGoalIds(); - refetch(); + const handleAiSubmit = (goals: { title: string }[]) => { + // Save selected goals to core goals, then update UI with server response + postRecommendToCore.mutate( + { mandalartId, goals: goals.map((g) => g.title) }, + { + onSuccess: (response) => { + const responseData = response.coreGoals as { + id: number; + position: number; + title: string; + }[]; + setAiResponseData(responseData); + const updatedSubGoals = updateSubGoalsWithAiResponse(subGoals, responseData); + setSubGoals(updatedSubGoals); + refetchCoreGoalIds(); + refetch(); + }, + onError: (error) => { + console.error('AI 추천 목표 저장 실패:', error); + }, + }, + ); }; const handleOpenAiModal = async () => { @@ -98,7 +118,6 @@ const UpperTodo = ({ userName = '김도트' }: UpperTodoProps) => { onSubmit={handleAiSubmit} values={subGoals} options={titles} - mandalartId={mandalartId} /> ); From e9ad4590ba5bbe447e7c5583b518f7ffab844976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Tue, 9 Sep 2025 15:20:10 +0900 Subject: [PATCH 08/17] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 1 - .../AiRecommendModal/AiRecommendModal.css.ts | 21 +------- .../AiRecommendModal/AiRecommendModal.tsx | 25 +++------ .../SelectableOption/SelectableOption.css.ts | 23 +++++++++ .../SelectableOption/SelectableOption.tsx | 51 +++++++++++++++++++ 5 files changed, 83 insertions(+), 38 deletions(-) create mode 100644 src/common/component/SelectableOption/SelectableOption.css.ts create mode 100644 src/common/component/SelectableOption/SelectableOption.tsx diff --git a/.storybook/main.ts b/.storybook/main.ts index eff981a9..b9a537d3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -23,7 +23,6 @@ const config: StorybookConfig = { ...(config.optimizeDeps || {}), include: ['@vanilla-extract/css'], }; - // Align aliases with app's Vite config config.resolve = { ...(config.resolve || {}), alias: { diff --git a/src/common/component/AiRecommendModal/AiRecommendModal.css.ts b/src/common/component/AiRecommendModal/AiRecommendModal.css.ts index 25d45720..7cd41c01 100644 --- a/src/common/component/AiRecommendModal/AiRecommendModal.css.ts +++ b/src/common/component/AiRecommendModal/AiRecommendModal.css.ts @@ -1,6 +1,6 @@ import { style } from '@vanilla-extract/css'; -import { colors, fonts } from '@/style/token'; +import { colors } from '@/style/token'; export const highlight = style({ color: colors.white01, @@ -14,25 +14,6 @@ export const listWrapper = style({ marginTop: '3.9rem', }); -export const listItem = style({ - display: 'flex', - alignItems: 'center', - gap: '0.6rem', - color: colors.white01, - ...fonts.body02, - cursor: 'pointer', - whiteSpace: 'nowrap', -}); - -export const listItemDisabled = style({ - cursor: 'not-allowed', -}); - -export const checkboxIcon = style({ - width: '2.4rem', - height: '2.4rem', -}); - export const buttonWrapper = style({ display: 'flex', justifyContent: 'center', diff --git a/src/common/component/AiRecommendModal/AiRecommendModal.tsx b/src/common/component/AiRecommendModal/AiRecommendModal.tsx index fc128a37..19f6320e 100644 --- a/src/common/component/AiRecommendModal/AiRecommendModal.tsx +++ b/src/common/component/AiRecommendModal/AiRecommendModal.tsx @@ -2,9 +2,9 @@ import { useState } from 'react'; import * as styles from './AiRecommendModal.css'; import Button from '../Button/Button'; +import SelectableOption from '../SelectableOption/SelectableOption'; import AiModalBase from '@/common/component/AiModalBase/AiModalBase'; -import { IcCheckboxDefault, IcCheckboxChecked } from '@/assets/svg'; interface AiRecommendModalProps { onClose: () => void; @@ -55,27 +55,18 @@ const AiRecommendModal = ({ onClose, onSubmit, values, options }: AiRecommendMod titleId="modal-title" footer={ )} - {ModalWrapper} ); }; From 6bffd1a15137ac4ffba03ab032863eda994819fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Sun, 14 Sep 2025 20:10:21 +0900 Subject: [PATCH 12/17] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 4 ++++ .../component/AiModalBase/AiModalBase.css.ts | 12 ++++++++++++ .../component/AiModalBase/AiModalBase.tsx | 10 +++++++++- src/common/hook/useOverlayModal.tsx | 18 +++++++++--------- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index b9a537d3..e9da0e06 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -2,6 +2,10 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import svgr from 'vite-plugin-svgr'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const config: StorybookConfig = { stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], diff --git a/src/common/component/AiModalBase/AiModalBase.css.ts b/src/common/component/AiModalBase/AiModalBase.css.ts index 9b8b1861..aa37e1fc 100644 --- a/src/common/component/AiModalBase/AiModalBase.css.ts +++ b/src/common/component/AiModalBase/AiModalBase.css.ts @@ -25,6 +25,18 @@ export const closeIcon = style({ cursor: 'pointer', }); +export const closeButton = style({ + width: '3.2rem', + height: '3.2rem', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + background: 'transparent', + border: 'none', + padding: 0, + cursor: 'pointer', +}); + export const contentWrapper = style({ width: '55.6rem', display: 'flex', diff --git a/src/common/component/AiModalBase/AiModalBase.tsx b/src/common/component/AiModalBase/AiModalBase.tsx index 5a79f61d..385b0b76 100644 --- a/src/common/component/AiModalBase/AiModalBase.tsx +++ b/src/common/component/AiModalBase/AiModalBase.tsx @@ -29,7 +29,15 @@ const AiModalBase = ({ return (
- +
diff --git a/src/common/hook/useOverlayModal.tsx b/src/common/hook/useOverlayModal.tsx index 064144eb..49ce43b1 100644 --- a/src/common/hook/useOverlayModal.tsx +++ b/src/common/hook/useOverlayModal.tsx @@ -24,15 +24,19 @@ const isCloseable = (element: ReactNode): element is ReactElement => export const useOverlayModal = () => { const lastIdRef = useRef(null); + const removeLastId = (id: string) => { + if (lastIdRef.current === id) { + lastIdRef.current = null; + } + }; + const openModal = (node: ReactNode, options: OpenOptions = {}) => { const { withWrapper = true } = options; const id = overlay.open(({ unmount }) => { const handleClose = () => { unmount(); - if (lastIdRef.current === id) { - lastIdRef.current = null; - } + removeLastId(id); }; const content = isCloseable(node) ? React.cloneElement(node, { onClose: handleClose }) : node; return withWrapper ? {content} : <>{content}; @@ -43,9 +47,7 @@ export const useOverlayModal = () => { id, close: () => { overlay.unmount(id); - if (lastIdRef.current === id) { - lastIdRef.current = null; - } + removeLastId(id); }, }; }; @@ -56,9 +58,7 @@ export const useOverlayModal = () => { return; } overlay.unmount(id); - if (lastIdRef.current === id) { - lastIdRef.current = null; - } + removeLastId(id); }; return { openModal, closeModal }; From a68156b2714446d59298816b58d15522129c0a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Sun, 14 Sep 2025 20:39:24 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=ED=86=A0=ED=81=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/AiModalBase/AiModalBase.css.ts | 90 +++++++++---------- .../AiRecommendModal/AiRecommendModal.css.ts | 23 +++-- src/common/component/Modal/Modal.css.ts | 28 +++--- src/style/token/layout.css.ts | 10 +++ 4 files changed, 78 insertions(+), 73 deletions(-) diff --git a/src/common/component/AiModalBase/AiModalBase.css.ts b/src/common/component/AiModalBase/AiModalBase.css.ts index aa37e1fc..5c76e15d 100644 --- a/src/common/component/AiModalBase/AiModalBase.css.ts +++ b/src/common/component/AiModalBase/AiModalBase.css.ts @@ -1,6 +1,6 @@ import { style } from '@vanilla-extract/css'; -import { colors, fonts, zIndex } from '@/style/token'; +import { colors, fonts, zIndex, layout } from '@/style/token'; export const container = style({ display: 'inline-flex', @@ -12,12 +12,13 @@ export const container = style({ zIndex: zIndex.modal, }); -export const iconWrapper = style({ - display: 'flex', - justifyContent: 'flex-end', - alignItems: 'center', - alignSelf: 'flex-end', -}); +export const iconWrapper = style([ + layout.rowCenter, + { + justifyContent: 'flex-end', + alignSelf: 'flex-end', + }, +]); export const closeIcon = style({ width: '3.2rem', @@ -25,51 +26,48 @@ export const closeIcon = style({ cursor: 'pointer', }); -export const closeButton = style({ - width: '3.2rem', - height: '3.2rem', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - background: 'transparent', - border: 'none', - padding: 0, - cursor: 'pointer', -}); +export const closeButton = style([ + layout.flexCenter, + { + width: '3.2rem', + height: '3.2rem', + display: 'inline-flex', + background: 'transparent', + border: 'none', + padding: 0, + cursor: 'pointer', + }, +]); -export const contentWrapper = style({ - width: '55.6rem', - display: 'flex', - flexDirection: 'column', - padding: '0 9.5rem', -}); +export const contentWrapper = style([ + layout.flexColumn, + { + width: '55.6rem', + padding: '0 9.5rem', + }, +]); -export const textWrapper = style({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', -}); +export const textWrapper = style([layout.flexColumn, { alignItems: 'center' }]); -export const title = style({ - color: colors.grey11, - textAlign: 'center', - ...fonts.display02, -}); +export const title = style([ + fonts.display02, + { + color: colors.grey11, + textAlign: 'center', + }, +]); -export const description = style({ - color: colors.grey7, - textAlign: 'center', - ...fonts.subtitle06, - marginTop: '0.9rem', -}); +export const description = style([ + fonts.subtitle06, + { + color: colors.grey7, + textAlign: 'center', + marginTop: '0.9rem', + }, +]); export const failDescription = style({ marginTop: '1.6rem', }); -export const footerWrapper = style({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - marginTop: '5rem', -}); +export const footerWrapper = style([layout.flexCenter, { marginTop: '5rem' }]); diff --git a/src/common/component/AiRecommendModal/AiRecommendModal.css.ts b/src/common/component/AiRecommendModal/AiRecommendModal.css.ts index 7cd41c01..90a57860 100644 --- a/src/common/component/AiRecommendModal/AiRecommendModal.css.ts +++ b/src/common/component/AiRecommendModal/AiRecommendModal.css.ts @@ -1,21 +1,18 @@ import { style } from '@vanilla-extract/css'; -import { colors } from '@/style/token'; +import { colors, layout } from '@/style/token'; export const highlight = style({ color: colors.white01, }); -export const listWrapper = style({ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: '2rem', - marginTop: '3.9rem', -}); +export const listWrapper = style([ + layout.flexColumn, + { + alignItems: 'flex-start', + gap: '2rem', + marginTop: '3.9rem', + }, +]); -export const buttonWrapper = style({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}); +export const buttonWrapper = style([layout.flexCenter]); diff --git a/src/common/component/Modal/Modal.css.ts b/src/common/component/Modal/Modal.css.ts index ea982226..8ec4a9d1 100644 --- a/src/common/component/Modal/Modal.css.ts +++ b/src/common/component/Modal/Modal.css.ts @@ -1,17 +1,17 @@ import { style } from '@vanilla-extract/css'; -import { zIndex } from '@/style/token'; +import { zIndex, layout } from '@/style/token'; -export const dimmed = style({ - position: 'fixed', - top: 0, - left: 0, - width: '100vw', - height: '100vh', - background: 'rgba(18, 18, 18, 0.70)', - backdropFilter: 'blur(2px)', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - zIndex: zIndex.modal, -}); +export const dimmed = style([ + layout.flexCenter, + { + position: 'fixed', + top: 0, + left: 0, + width: '100vw', + height: '100vh', + background: 'rgba(18, 18, 18, 0.70)', + backdropFilter: 'blur(2px)', + zIndex: zIndex.modal, + }, +]); diff --git a/src/style/token/layout.css.ts b/src/style/token/layout.css.ts index 803146da..a6da3efc 100644 --- a/src/style/token/layout.css.ts +++ b/src/style/token/layout.css.ts @@ -1,4 +1,14 @@ export const layout = { + flexColumn: { + display: 'flex', + flexDirection: 'column', + }, + rowBetween: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, flexCenter: { display: 'flex', alignItems: 'center', From 04e8de8647a6abcf1dde2755c33f80d85b215ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Sun, 14 Sep 2025 20:48:34 +0900 Subject: [PATCH 14/17] =?UTF-8?q?fix:=20=EB=B2=84=EC=A0=84=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8e936ce3..cacdbfbe 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,15 @@ "@eslint/js": "^9.29.0", "@storybook/addon-a11y": "^9.0.15", "@storybook/addon-docs": "^9.0.15", - "@storybook/addon-essentials": "^8.6.14", + "@storybook/addon-essentials": "^9.0.15", "@storybook/addon-onboarding": "^9.0.16", "@storybook/addon-vitest": "^9.0.15", - "@storybook/blocks": "^8.6.14", - "@storybook/preview-api": "^8.6.14", + "@storybook/blocks": "^9.0.15", + "@storybook/preview-api": "^9.0.15", "@storybook/react": "^9.0.15", "@storybook/react-vite": "^9.0.15", - "@storybook/test": "8.6.14", - "@storybook/types": "^8.6.14", + "@storybook/test": "^9.0.15", + "@storybook/types": "^9.0.15", "@tanstack/react-query-devtools": "^5.81.5", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", @@ -77,4 +77,4 @@ "vitest": "^3.2.4" }, "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184" -} \ No newline at end of file +} From fc6b8df975d7aa78e624c5590aefcd288d8fe1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Sun, 14 Sep 2025 20:54:48 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index cacdbfbe..e75a1701 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,15 @@ "@eslint/js": "^9.29.0", "@storybook/addon-a11y": "^9.0.15", "@storybook/addon-docs": "^9.0.15", - "@storybook/addon-essentials": "^9.0.15", + "@storybook/addon-essentials": "^8.6.14", "@storybook/addon-onboarding": "^9.0.16", "@storybook/addon-vitest": "^9.0.15", - "@storybook/blocks": "^9.0.15", - "@storybook/preview-api": "^9.0.15", + "@storybook/blocks": "^8.6.14", + "@storybook/preview-api": "^8.6.14", "@storybook/react": "^9.0.15", "@storybook/react-vite": "^9.0.15", - "@storybook/test": "^9.0.15", - "@storybook/types": "^9.0.15", + "@storybook/test": "8.6.14", + "@storybook/types": "^8.6.14", "@tanstack/react-query-devtools": "^5.81.5", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", From ab906f56529446d290e241f19d75ccf46613dc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Sun, 14 Sep 2025 21:16:15 +0900 Subject: [PATCH 16/17] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index e9da0e06..21980bb3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,11 +1,11 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import svgr from 'vite-plugin-svgr'; -import path from 'node:path'; +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __dirname = dirname(__filename); const config: StorybookConfig = { stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], @@ -31,15 +31,15 @@ const config: StorybookConfig = { ...(config.resolve || {}), alias: { ...(config.resolve?.alias || {}), - '@': path.resolve(__dirname, '../src'), - '@api': path.resolve(__dirname, '../src/api'), - '@assets': path.resolve(__dirname, '../src/assets'), - '@common': path.resolve(__dirname, '../src/common'), - '@page': path.resolve(__dirname, '../src/page'), - '@route': path.resolve(__dirname, '../src/route'), - '@shared': path.resolve(__dirname, '../src/shared'), - '@style': path.resolve(__dirname, '../src/style'), - '@type': path.resolve(__dirname, '../src/type'), + '@': resolve(__dirname, '../src'), + '@api': resolve(__dirname, '../src/api'), + '@assets': resolve(__dirname, '../src/assets'), + '@common': resolve(__dirname, '../src/common'), + '@page': resolve(__dirname, '../src/page'), + '@route': resolve(__dirname, '../src/route'), + '@shared': resolve(__dirname, '../src/shared'), + '@style': resolve(__dirname, '../src/style'), + '@type': resolve(__dirname, '../src/type'), }, }; return config; From 81bd90d5d78678fa12cb01c194c2c07b4afac568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=A7=80=EC=88=98?= Date: Sun, 14 Sep 2025 21:25:46 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 21980bb3..b9a537d3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,11 +1,7 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import svgr from 'vite-plugin-svgr'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +import path from 'node:path'; const config: StorybookConfig = { stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], @@ -31,15 +27,15 @@ const config: StorybookConfig = { ...(config.resolve || {}), alias: { ...(config.resolve?.alias || {}), - '@': resolve(__dirname, '../src'), - '@api': resolve(__dirname, '../src/api'), - '@assets': resolve(__dirname, '../src/assets'), - '@common': resolve(__dirname, '../src/common'), - '@page': resolve(__dirname, '../src/page'), - '@route': resolve(__dirname, '../src/route'), - '@shared': resolve(__dirname, '../src/shared'), - '@style': resolve(__dirname, '../src/style'), - '@type': resolve(__dirname, '../src/type'), + '@': path.resolve(__dirname, '../src'), + '@api': path.resolve(__dirname, '../src/api'), + '@assets': path.resolve(__dirname, '../src/assets'), + '@common': path.resolve(__dirname, '../src/common'), + '@page': path.resolve(__dirname, '../src/page'), + '@route': path.resolve(__dirname, '../src/route'), + '@shared': path.resolve(__dirname, '../src/shared'), + '@style': path.resolve(__dirname, '../src/style'), + '@type': path.resolve(__dirname, '../src/type'), }, }; return config;