From adb27aa723cb70dd6ae10e0c292b0592c2ed08ff Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Fri, 16 Jan 2026 01:39:29 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=20chore:=20chore:=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EC=98=88=EC=A0=9C=20=ED=94=8C=EB=A0=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EC=9A=B4=EB=93=9C=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/playground.tsx | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/pages/playground.tsx b/src/pages/playground.tsx index 6fe2fdfc..4bce8596 100644 --- a/src/pages/playground.tsx +++ b/src/pages/playground.tsx @@ -2,20 +2,25 @@ import { Button } from '@shared/ui/Button/Button'; import { Card } from '@shared/ui/Card/Card'; import { Checkbox } from '@shared/ui/Checkbox/Checkbox'; import { Header } from '@shared/ui/Header/Header'; +import { Modal } from '@shared/ui/Modal/Modal'; import { Profile } from '@shared/ui/Profile/Profile'; import { RadioButton } from '@shared/ui/RadioButton/RadioButton'; +import { useState } from 'react'; export default function Playground() { + const [isModalOpen, setIsModalOpen] = useState(false); + return (

UI Playground

- {/* Button */} + {/* Header */}

Header

+ {/* Button */}

Button

@@ -96,7 +101,6 @@ export default function Playground() { {/* Card */}

Card

-
+ + {/* Modal */} +
+

Modal

+ + + + {isModalOpen && ( + setIsModalOpen(false)} + onConfirm={() => { + setIsModalOpen(false); + console.log('삭제 확정'); + }} + /> + )} +
); } From c9b987063e8ec7f1a8ed4e695b679ef07894bccf Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Fri, 16 Jan 2026 01:40:06 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=20Modal=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=A0=EA=B7=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Modal/Modal.tsx | 38 +++++++++++++++++++++++++++ src/shared/ui/Modal/Modal.variants.ts | 16 +++++++++++ src/shared/ui/Modal/index.ts | 2 ++ 3 files changed, 56 insertions(+) create mode 100644 src/shared/ui/Modal/Modal.tsx create mode 100644 src/shared/ui/Modal/Modal.variants.ts create mode 100644 src/shared/ui/Modal/index.ts diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx new file mode 100644 index 00000000..807cfee1 --- /dev/null +++ b/src/shared/ui/Modal/Modal.tsx @@ -0,0 +1,38 @@ +import { Button } from '@shared/ui/Button/Button'; +import { modalVariants } from './Modal.variants'; + +export interface ModalProps { + title: string; + subtitle?: string; + onCancel: () => void; + onConfirm: () => void; +} + +export const Modal = ({ title, subtitle, onCancel, onConfirm }: ModalProps) => { + const { overlay, container, textGroup, buttonGroup, buttonWrapper } = modalVariants(); + + return ( +
+
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ +
+
+ +
+ +
+ +
+
+
+
+ ); +}; diff --git a/src/shared/ui/Modal/Modal.variants.ts b/src/shared/ui/Modal/Modal.variants.ts new file mode 100644 index 00000000..b78a7a6d --- /dev/null +++ b/src/shared/ui/Modal/Modal.variants.ts @@ -0,0 +1,16 @@ +import { tv } from 'tailwind-variants'; + +export const modalVariants = tv({ + slots: { + overlay: 'fixed inset-0 z-50 flex items-center justify-center bg-black/40', + + container: + 'w-[347px] h-[167px] rounded-[var(--radius-l)] bg-[var(--color-green-500)] px-[32px] py-[24px] flex flex-col justify-between', + + textGroup: 'flex flex-col gap-[var(--spacing-xxs)]', + + buttonGroup: 'flex gap-3', + + buttonWrapper: 'flex-1', + }, +}); diff --git a/src/shared/ui/Modal/index.ts b/src/shared/ui/Modal/index.ts new file mode 100644 index 00000000..05844a90 --- /dev/null +++ b/src/shared/ui/Modal/index.ts @@ -0,0 +1,2 @@ +export { Modal } from './Modal'; +export type { ModalProps } from './Modal'; From 6b07d0de5226fccadb1652164c51fe8a7cab0bf8 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Fri, 16 Jan 2026 01:40:55 +0900 Subject: [PATCH 03/14] =?UTF-8?q?chore:=20Modal=20test=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/shared/ui/Modal/Modal.test.tsx | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/unit/shared/ui/Modal/Modal.test.tsx diff --git a/tests/unit/shared/ui/Modal/Modal.test.tsx b/tests/unit/shared/ui/Modal/Modal.test.tsx new file mode 100644 index 00000000..38486b11 --- /dev/null +++ b/tests/unit/shared/ui/Modal/Modal.test.tsx @@ -0,0 +1,82 @@ +import { Modal } from '@shared/ui/Modal/Modal'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +describe('Modal', () => { + const setup = (props?: Partial>) => { + const onCancel = vi.fn(); + const onConfirm = vi.fn(); + + render( + + ); + + return { onCancel, onConfirm }; + }; + + describe('Rendering', () => { + it('title 렌더링', () => { + setup(); + expect(screen.getByText('해당 게시물을 삭제하시겠어요?')).toBeInTheDocument(); + }); + + it('subtitle 렌더링', () => { + setup(); + expect(screen.getByText('subtitle 1줄')).toBeInTheDocument(); + }); + + it('subtitle이 없을 경우 렌더링되지 않음', () => { + render(); + + expect(screen.queryByText('삭제 후에는 복구할 수 없습니다.')).not.toBeInTheDocument(); + }); + + it('취소 / 삭제 버튼 렌더링', () => { + setup(); + expect(screen.getByRole('button', { name: '취소' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '삭제' })).toBeInTheDocument(); + }); + }); + + describe('Interaction', () => { + it('취소 버튼 클릭 시 onCancel 호출', async () => { + const user = userEvent.setup(); + const { onCancel } = setup(); + + await user.click(screen.getByRole('button', { name: '취소' })); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('삭제 버튼 클릭 시 onConfirm 호출', async () => { + const user = userEvent.setup(); + const { onConfirm } = setup(); + + await user.click(screen.getByRole('button', { name: '삭제' })); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + }); + + describe('Accessibility', () => { + it('버튼은 접근 가능한 role을 가잠', () => { + setup(); + expect(screen.getAllByRole('button')).toHaveLength(2); + }); + + it('Tab 키로 버튼 포커스 이동 가능', async () => { + const user = userEvent.setup(); + setup(); + + await user.tab(); + expect(screen.getByRole('button', { name: '취소' })).toHaveFocus(); + + await user.tab(); + expect(screen.getByRole('button', { name: '삭제' })).toHaveFocus(); + }); + }); +}); From be6f6c15d8167a59f8dbdd2681ab01e2e335e6f0 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 15:04:15 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20cancelText,=20confirmText=20props=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=ED=95=B4=20=EB=AA=A8=EB=8B=AC=20=EC=9E=AC?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=84=B1=EC=9D=84=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Modal/Modal.tsx | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 807cfee1..6ce1d8cc 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -4,11 +4,22 @@ import { modalVariants } from './Modal.variants'; export interface ModalProps { title: string; subtitle?: string; + + cancelText?: string; + confirmText?: string; + onCancel: () => void; onConfirm: () => void; } -export const Modal = ({ title, subtitle, onCancel, onConfirm }: ModalProps) => { +export const Modal = ({ + title, + subtitle, + cancelText = '취소', + confirmText = '확인', + onCancel, + onConfirm, +}: ModalProps) => { const { overlay, container, textGroup, buttonGroup, buttonWrapper } = modalVariants(); return ( @@ -21,14 +32,24 @@ export const Modal = ({ title, subtitle, onCancel, onConfirm }: ModalProps) => {
-
-
From a03765b18fedf0512d90144600c30cb9ebc40a57 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 15:12:56 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor:overlay=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Modal/Modal.tsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 6ce1d8cc..e65834f8 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -20,11 +20,11 @@ export const Modal = ({ onCancel, onConfirm, }: ModalProps) => { - const { overlay, container, textGroup, buttonGroup, buttonWrapper } = modalVariants(); + const { container, content, textGroup, buttonGroup, buttonWrapper } = modalVariants(); return ( -
-
+
+

{title}

{subtitle &&

{subtitle}

} @@ -32,23 +32,13 @@ export const Modal = ({
-
-
From 2acec6f2a1f746ec344a8a526d78a79408279540 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 15:17:12 +0900 Subject: [PATCH 06/14] =?UTF-8?q?refactor:=20padding=20=EA=B0=92=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=92=20=EC=82=AC=EC=9A=A9=20=EB=B0=8F?= =?UTF-8?q?=20=EC=83=88=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/styles/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/styles/index.css b/src/app/styles/index.css index 37dbba4c..940a658a 100644 --- a/src/app/styles/index.css +++ b/src/app/styles/index.css @@ -73,6 +73,7 @@ --margin-s: 16px; /* Padding (Primitive/Scale) */ + --padding-xxl: 32px; --padding-xl: 24px; --padding-l: 16px; --padding-m: 12px; From 023891cb4fbebf18501cbc2bdf632ef419291c89 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 15:38:07 +0900 Subject: [PATCH 07/14] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=A9=98=ED=8A=B8?= =?UTF-8?q?=20=EC=B5=9C=EC=A2=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/src/app/root.tsx | 9 +++ src/app/styles/index.css | 33 ++++++++++- src/pages/playground.tsx | 1 - src/shared/ui/BannerCard/BannerCard.styles.ts | 55 ++++++++++++++++++ src/shared/ui/Modal/Modal.styles.ts | 21 +++++++ src/shared/ui/Modal/Modal.tsx | 56 +++++++++++++++---- src/shared/ui/Modal/Modal.variants.ts | 16 ------ 7 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 src/app/src/app/root.tsx create mode 100644 src/shared/ui/BannerCard/BannerCard.styles.ts create mode 100644 src/shared/ui/Modal/Modal.styles.ts delete mode 100644 src/shared/ui/Modal/Modal.variants.ts diff --git a/src/app/src/app/root.tsx b/src/app/src/app/root.tsx new file mode 100644 index 00000000..6d8f980e --- /dev/null +++ b/src/app/src/app/root.tsx @@ -0,0 +1,9 @@ +import { Outlet } from 'react-router'; + +export default function Root() { + return ( +
+ +
+ ); +} diff --git a/src/app/styles/index.css b/src/app/styles/index.css index 940a658a..68b5ec07 100644 --- a/src/app/styles/index.css +++ b/src/app/styles/index.css @@ -73,7 +73,7 @@ --margin-s: 16px; /* Padding (Primitive/Scale) */ - --padding-xxl: 32px; + --padding-xxl: 32px; --padding-xl: 24px; --padding-l: 16px; --padding-m: 12px; @@ -149,4 +149,35 @@ line-height: var(--line-height-xs); letter-spacing: var(--letter-spacing); } + @layer utilities { + .animate-fade-in { + animation: fade-in 0.2s ease-out forwards; + } + + .animate-fade-out { + animation: fade-out 0.15s ease-in forwards; + } + + @keyframes fade-in { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes fade-out { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(4px); + } + } + } } diff --git a/src/pages/playground.tsx b/src/pages/playground.tsx index 4bce8596..9d993694 100644 --- a/src/pages/playground.tsx +++ b/src/pages/playground.tsx @@ -122,7 +122,6 @@ export default function Playground() { onCancel={() => setIsModalOpen(false)} onConfirm={() => { setIsModalOpen(false); - console.log('삭제 확정'); }} /> )} diff --git a/src/shared/ui/BannerCard/BannerCard.styles.ts b/src/shared/ui/BannerCard/BannerCard.styles.ts new file mode 100644 index 00000000..c9dcedd8 --- /dev/null +++ b/src/shared/ui/BannerCard/BannerCard.styles.ts @@ -0,0 +1,55 @@ +export const bannerCardStyles = { + base: [ + 'group', + 'relative', + 'flex', + 'flex-col', + 'justify-end', + 'items-start', + 'w-[427px]', + 'h-[384px]', + 'px-[var(--padding-xl)]', + 'py-[27px]', + 'gap-[var(--spacing-xxs)]', + 'rounded-[var(--radius-xl)]', + 'bg-white', + 'bg-[linear-gradient(0deg,rgba(4,217,181,0)_0%,rgba(0,179,155,0.2)_71.11%)]', + 'cursor-pointer', + 'outline-none', + ].join(' '), + + frame: [ + 'flex', + 'flex-col', + 'items-start', + 'gap-[var(--spacing-xs)]', + 'self-stretch', + 'h-[330px]', + ].join(' '), + + textWrapper: [ + 'relative', + 'z-10', + 'flex', + 'flex-col', + 'items-start', + 'gap-[var(--spacing-xs)]', + 'self-stretch', + ].join(' '), + + title: 'typo-title-3 text-black', + + description: 'typo-body-1 text-black overflow-hidden text-ellipsis line-clamp-2', + + imageWrapper: 'absolute right-0 bottom-0 z-0 pointer-events-none', + + image: [ + 'w-[213px]', + 'h-[213px]', + 'object-cover', + 'transition-all', + 'duration-300', + 'group-hover:w-[307px]', + 'group-hover:h-[307px]', + ].join(' '), +}; diff --git a/src/shared/ui/Modal/Modal.styles.ts b/src/shared/ui/Modal/Modal.styles.ts new file mode 100644 index 00000000..f5d481f5 --- /dev/null +++ b/src/shared/ui/Modal/Modal.styles.ts @@ -0,0 +1,21 @@ +export const modalStyles = { + container: ['fixed', 'inset-0', 'z-50', 'flex', 'items-center', 'justify-center'].join(' '), + + content: [ + 'w-[347px]', + 'h-[167px]', + 'flex', + 'flex-col', + 'justify-between', + 'rounded-[var(--radius-l)]', + 'bg-[var(--color-green-500)]', + 'px-[var(--padding-xxl)]', + 'py-[var(--padding-xl)]', + ].join(' '), + + textGroup: ['flex', 'flex-col', 'gap-[var(--spacing-xxs)]'].join(' '), + + buttonGroup: ['flex', 'gap-3'].join(' '), + + buttonWrapper: ['flex-1'].join(' '), +}; diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index e65834f8..91c65fdd 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -1,13 +1,12 @@ import { Button } from '@shared/ui/Button/Button'; -import { modalVariants } from './Modal.variants'; +import { useEffect, useState } from 'react'; +import { modalStyles } from './Modal.styles'; export interface ModalProps { title: string; subtitle?: string; - cancelText?: string; confirmText?: string; - onCancel: () => void; onConfirm: () => void; } @@ -20,25 +19,58 @@ export const Modal = ({ onCancel, onConfirm, }: ModalProps) => { - const { container, content, textGroup, buttonGroup, buttonWrapper } = modalVariants(); + const [closing, setClosing] = useState(false); + + const handleClose = (callback: () => void) => { + setClosing(true); + setTimeout(callback, 150); + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + handleClose(onCancel); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [onCancel]); return ( -
-
-
+
handleClose(onCancel)}> +
e.stopPropagation()} + > +

{title}

{subtitle &&

{subtitle}

}
-
-
-
-
-
diff --git a/src/shared/ui/Modal/Modal.variants.ts b/src/shared/ui/Modal/Modal.variants.ts deleted file mode 100644 index b78a7a6d..00000000 --- a/src/shared/ui/Modal/Modal.variants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { tv } from 'tailwind-variants'; - -export const modalVariants = tv({ - slots: { - overlay: 'fixed inset-0 z-50 flex items-center justify-center bg-black/40', - - container: - 'w-[347px] h-[167px] rounded-[var(--radius-l)] bg-[var(--color-green-500)] px-[32px] py-[24px] flex flex-col justify-between', - - textGroup: 'flex flex-col gap-[var(--spacing-xxs)]', - - buttonGroup: 'flex gap-3', - - buttonWrapper: 'flex-1', - }, -}); From a0e9bddba9edb1f48208160f9524d7894a9a5a43 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 17:47:42 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=20fix:=20eslint=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Modal/Modal.tsx | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 91c65fdd..8d073c5f 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -1,5 +1,5 @@ import { Button } from '@shared/ui/Button/Button'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { modalStyles } from './Modal.styles'; export interface ModalProps { @@ -21,11 +21,24 @@ export const Modal = ({ }: ModalProps) => { const [closing, setClosing] = useState(false); - const handleClose = (callback: () => void) => { - setClosing(true); - setTimeout(callback, 150); - }; + /** + * 닫힘 로직 (모든 경로에서 공통 사용) + */ + const handleClose = useCallback( + (callback: () => void) => { + if (closing) { + return; + } + + setClosing(true); + setTimeout(callback, 150); // fade-out duration + }, + [closing] + ); + /** + * ESC 키로 닫기 + */ useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -37,21 +50,23 @@ export const Modal = ({ return () => { window.removeEventListener('keydown', handleKeyDown); }; - }, [onCancel]); + }, [handleClose, onCancel]); return ( + /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
handleClose(onCancel)}> + {/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
e.stopPropagation()} > + {/* Text */}

{title}

{subtitle &&

{subtitle}

}
+ {/* Buttons */}
+ /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ ); }; From 83a143657f5432d0c04717b8f15cc0dfca1459d7 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 18:02:35 +0900 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/shared/ui/Modal/Modal.test.tsx | 31 ++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/unit/shared/ui/Modal/Modal.test.tsx b/tests/unit/shared/ui/Modal/Modal.test.tsx index 38486b11..69282a80 100644 --- a/tests/unit/shared/ui/Modal/Modal.test.tsx +++ b/tests/unit/shared/ui/Modal/Modal.test.tsx @@ -1,5 +1,5 @@ import { Modal } from '@shared/ui/Modal/Modal'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; describe('Modal', () => { @@ -23,7 +23,9 @@ describe('Modal', () => { describe('Rendering', () => { it('title 렌더링', () => { setup(); - expect(screen.getByText('해당 게시물을 삭제하시겠어요?')).toBeInTheDocument(); + expect( + screen.getByText('해당 게시물을 삭제하시겠어요?') + ).toBeInTheDocument(); }); it('subtitle 렌더링', () => { @@ -33,14 +35,13 @@ describe('Modal', () => { it('subtitle이 없을 경우 렌더링되지 않음', () => { render(); - - expect(screen.queryByText('삭제 후에는 복구할 수 없습니다.')).not.toBeInTheDocument(); + expect(screen.queryByText('subtitle 1줄')).not.toBeInTheDocument(); }); - it('취소 / 삭제 버튼 렌더링', () => { + it('취소 / 확인 버튼 렌더링', () => { setup(); expect(screen.getByRole('button', { name: '취소' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '삭제' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '확인' })).toBeInTheDocument(); }); }); @@ -50,20 +51,26 @@ describe('Modal', () => { const { onCancel } = setup(); await user.click(screen.getByRole('button', { name: '취소' })); - expect(onCancel).toHaveBeenCalledTimes(1); + + await waitFor(() => { + expect(onCancel).toHaveBeenCalledTimes(1); + }); }); - it('삭제 버튼 클릭 시 onConfirm 호출', async () => { + it('확인 버튼 클릭 시 onConfirm 호출', async () => { const user = userEvent.setup(); const { onConfirm } = setup(); - await user.click(screen.getByRole('button', { name: '삭제' })); - expect(onConfirm).toHaveBeenCalledTimes(1); + await user.click(screen.getByRole('button', { name: '확인' })); + + await waitFor(() => { + expect(onConfirm).toHaveBeenCalledTimes(1); + }); }); }); describe('Accessibility', () => { - it('버튼은 접근 가능한 role을 가잠', () => { + it('버튼은 접근 가능한 role을 가짐', () => { setup(); expect(screen.getAllByRole('button')).toHaveLength(2); }); @@ -76,7 +83,7 @@ describe('Modal', () => { expect(screen.getByRole('button', { name: '취소' })).toHaveFocus(); await user.tab(); - expect(screen.getByRole('button', { name: '삭제' })).toHaveFocus(); + expect(screen.getByRole('button', { name: '확인' })).toHaveFocus(); }); }); }); From f836e1a7451b7f360108e9972a2b645bd499a64d Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 18:07:04 +0900 Subject: [PATCH 11/14] =?UTF-8?q?chore:prettier=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/shared/ui/Modal/Modal.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/shared/ui/Modal/Modal.test.tsx b/tests/unit/shared/ui/Modal/Modal.test.tsx index 69282a80..de08128b 100644 --- a/tests/unit/shared/ui/Modal/Modal.test.tsx +++ b/tests/unit/shared/ui/Modal/Modal.test.tsx @@ -23,9 +23,7 @@ describe('Modal', () => { describe('Rendering', () => { it('title 렌더링', () => { setup(); - expect( - screen.getByText('해당 게시물을 삭제하시겠어요?') - ).toBeInTheDocument(); + expect(screen.getByText('해당 게시물을 삭제하시겠어요?')).toBeInTheDocument(); }); it('subtitle 렌더링', () => { From aec3192a312e5c6184f5c0f58946a359ffd47eb8 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sat, 17 Jan 2026 23:41:38 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=EC=A3=BC=EC=84=9D=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Modal/Modal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 83719843..6facd440 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -53,7 +53,6 @@ export const Modal = ({ className={`${modalStyles.content} ${closing ? 'animate-fade-out' : 'animate-fade-in'}`} onClick={(e) => e.stopPropagation()} > - {/* Text */}

{title}

{subtitle &&

{subtitle}

} From f2f70e7058ded13ca14b619b36a8bab031c7d75e Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sun, 18 Jan 2026 02:01:56 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=20chore:=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=ED=8C=8C=EC=9D=BC=20push=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/root.tsx | 33 ----------- src/shared/ui/BannerCard/BannerCard.styles.ts | 55 ------------------- 2 files changed, 88 deletions(-) delete mode 100644 src/app/root.tsx delete mode 100644 src/shared/ui/BannerCard/BannerCard.styles.ts diff --git a/src/app/root.tsx b/src/app/root.tsx deleted file mode 100644 index 1994e571..00000000 --- a/src/app/root.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { AppProviders } from '@app/providers'; -import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; -import '@app/styles/index.css'; - -export const links = () => [ - // Favicon - { rel: 'icon', type: 'image/png', sizes: '96x96', href: '/favicon-96x96.png' }, - { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }, - { rel: 'shortcut icon', href: '/favicon.ico' }, - { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }, - { rel: 'manifest', href: '/site.webmanifest' }, -]; - -export default function Root() { - return ( - - - - - - - - - - - - - - - - - ); -} diff --git a/src/shared/ui/BannerCard/BannerCard.styles.ts b/src/shared/ui/BannerCard/BannerCard.styles.ts deleted file mode 100644 index c9dcedd8..00000000 --- a/src/shared/ui/BannerCard/BannerCard.styles.ts +++ /dev/null @@ -1,55 +0,0 @@ -export const bannerCardStyles = { - base: [ - 'group', - 'relative', - 'flex', - 'flex-col', - 'justify-end', - 'items-start', - 'w-[427px]', - 'h-[384px]', - 'px-[var(--padding-xl)]', - 'py-[27px]', - 'gap-[var(--spacing-xxs)]', - 'rounded-[var(--radius-xl)]', - 'bg-white', - 'bg-[linear-gradient(0deg,rgba(4,217,181,0)_0%,rgba(0,179,155,0.2)_71.11%)]', - 'cursor-pointer', - 'outline-none', - ].join(' '), - - frame: [ - 'flex', - 'flex-col', - 'items-start', - 'gap-[var(--spacing-xs)]', - 'self-stretch', - 'h-[330px]', - ].join(' '), - - textWrapper: [ - 'relative', - 'z-10', - 'flex', - 'flex-col', - 'items-start', - 'gap-[var(--spacing-xs)]', - 'self-stretch', - ].join(' '), - - title: 'typo-title-3 text-black', - - description: 'typo-body-1 text-black overflow-hidden text-ellipsis line-clamp-2', - - imageWrapper: 'absolute right-0 bottom-0 z-0 pointer-events-none', - - image: [ - 'w-[213px]', - 'h-[213px]', - 'object-cover', - 'transition-all', - 'duration-300', - 'group-hover:w-[307px]', - 'group-hover:h-[307px]', - ].join(' '), -}; From 2389d0f2bfa72a2b8390e3aacd9a6fff172a4fc1 Mon Sep 17 00:00:00 2001 From: Wonjun Jung Date: Sun, 18 Jan 2026 02:07:37 +0900 Subject: [PATCH 14/14] =?UTF-8?q?chore:=20=EC=A4=91=EB=B3=B5=20root=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/root.tsx | 33 +++++++++++++++++++++++++++++++++ src/app/src/app/root.tsx | 9 --------- 2 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 src/app/root.tsx delete mode 100644 src/app/src/app/root.tsx diff --git a/src/app/root.tsx b/src/app/root.tsx new file mode 100644 index 00000000..1994e571 --- /dev/null +++ b/src/app/root.tsx @@ -0,0 +1,33 @@ +import { AppProviders } from '@app/providers'; +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; +import '@app/styles/index.css'; + +export const links = () => [ + // Favicon + { rel: 'icon', type: 'image/png', sizes: '96x96', href: '/favicon-96x96.png' }, + { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }, + { rel: 'shortcut icon', href: '/favicon.ico' }, + { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }, + { rel: 'manifest', href: '/site.webmanifest' }, +]; + +export default function Root() { + return ( + + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/src/app/root.tsx b/src/app/src/app/root.tsx deleted file mode 100644 index 6d8f980e..00000000 --- a/src/app/src/app/root.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Outlet } from 'react-router'; - -export default function Root() { - return ( -
- -
- ); -}