From 15705e3a0cc40dc335a08c17659daf0e2d4a182a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Sat, 9 May 2026 21:01:01 +0900 Subject: [PATCH 01/33] refactor(styles): split global CSS and tidy base layer - Split index.css into tokens, base, utilities, and print modules. - Move @font-face to the top of base.css. - Co-locate GuideTimeline scrollbar rules with the component. --- src/components/landing/GuideTimeline.tsx | 11 + src/index.css | 557 +---------------------- src/styles/base.css | 23 + src/styles/print.css | 27 ++ src/styles/tokens.css | 76 ++++ src/styles/utilities.css | 413 +++++++++++++++++ 6 files changed, 554 insertions(+), 553 deletions(-) create mode 100644 src/styles/base.css create mode 100644 src/styles/print.css create mode 100644 src/styles/tokens.css create mode 100644 src/styles/utilities.css diff --git a/src/components/landing/GuideTimeline.tsx b/src/components/landing/GuideTimeline.tsx index 574977c0..b1bde50a 100644 --- a/src/components/landing/GuideTimeline.tsx +++ b/src/components/landing/GuideTimeline.tsx @@ -17,6 +17,17 @@ export default function GuideTimeline() { return (
+ {/* Top Navigation */}
* { - animation: slide-fade-up 350ms var(--ease-out) both; - } - .coming-soon-stagger > :nth-child(1) { - animation-delay: 0ms; - } - .coming-soon-stagger > :nth-child(2) { - animation-delay: 50ms; - } - .coming-soon-stagger > :nth-child(3) { - animation-delay: 100ms; - } - .coming-soon-stagger > :nth-child(4) { - animation-delay: 150ms; - } - - /* 그래프 상승 애니메이션 */ - @keyframes graph-up { - from { - transform: scaleY(0); - } - to { - transform: scaleY(1); - } - } - - .animate-graph-up { - animation: graph-up 0.7s ease-out forwards; - transform-origin: bottom; - } - - /* AI 버블 진입 전환 */ - .bubble-transition { - transition-property: transform, opacity; - transition-duration: 300ms; - transition-timing-function: cubic-bezier(0.2, 1.05, 0.4, 1); - } - - /* 로딩 애니메이션 */ - @keyframes shimmer { - to { - transform: translateX(200%); - } - } - - .animate-shimmer { - position: relative; - overflow: hidden; - } - - .animate-shimmer::after { - content: ""; - position: absolute; - inset: 0; - transform: translateX(-100%); - background: linear-gradient( - 90deg, - transparent 0%, - rgba(255, 255, 255, 0.5) 50%, - transparent 100% - ); - animation: shimmer 1.5s infinite linear; - } - - /* AI 리포트 인쇄 — 드로어 패널 외 모든 요소 숨김 */ - @media print { - body.ai-report-printing > *:not([data-drawer-portal]) { - display: none !important; - } - - body.ai-report-printing [data-drawer-portal] { - position: static !important; - display: block !important; - } - - body.ai-report-printing [data-print-hide] { - display: none !important; - } - - body.ai-report-printing [data-drawer-portal] > div:last-child { - box-shadow: none !important; - border-radius: 0 !important; - max-width: 100% !important; - height: auto !important; - transform: none !important; - } - - @page { - margin: 16mm; - } - } - - /* AI 요약 버튼 스파클 pulse 애니메이션 */ - @keyframes sparkle-pulse { - 0%, - 100% { - transform: scale(1); - opacity: 0.7; - } - 50% { - transform: scale(1.2); - opacity: 1; - } - } - - .animate-sparkle-pulse { - animation: sparkle-pulse 2.8s ease-in-out infinite; - } - - @media (prefers-reduced-motion: reduce) { - /* 스파클 */ - .animate-sparkle-pulse { - animation: none; - opacity: 1; - } - - /* 인트로 텍스트 진입 — 즉시 표시 */ - .animate-slide-fade-up { - animation: none; - opacity: 1; - transform: none; - } - - .coming-soon-stagger > * { - animation: none; - opacity: 1; - transform: none; - } - - /* 인트로 캐러셀 — 위치 이동 중단 */ - .animate-scroll { - animation: none; - } - - /* 바 차트 — 즉시 완성 상태로 표시 */ - .animate-graph-up { - animation: none; - transform: scaleY(1); - } - - /* AI 버블 — transform/opacity 전환 제거 */ - .bubble-transition { - transition: none; - } - - /* 모달 — scale 제거, opacity 전환만 유지 */ - .animate-modal-content { - animation: modal-fade-in var(--duration-normal) var(--ease-out); - } - .animate-modal-content-out { - animation: modal-fade-out var(--duration-fast) var(--ease-in) forwards; - } - - /* 토스트 — translateY 제거, opacity 전환만 유지 */ - @keyframes fade-in-up-reduced { - from { - opacity: 0; - transform: translate(-50%, 0); - } - to { - opacity: 1; - transform: translate(-50%, 0); - } - } - .animate-fade-in-up { - animation: fade-in-up-reduced 200ms var(--ease-out) forwards; - } - } - - /* 토스트 애니메이션 */ - @keyframes fade-in-up { - from { - opacity: 0; - transform: translate(-50%, 12px); - } - to { - opacity: 1; - transform: translate(-50%, 0); - } - } - - .animate-fade-in-up { - animation: fade-in-up 0.2s var(--ease-out) both; - } -} - -/* (removed) react-calendar-timeline overrides — now using custom mock timeline */ diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 00000000..a775d51b --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,23 @@ +@font-face { + font-family: "Pretendard"; + src: url("/fonts/PretendardVariable.woff2") format("woff2"); + font-display: swap; + font-weight: 45 920; +} + +* { + font-family: "Pretendard"; +} + +html, +body, +#root { + min-height: 100dvh; + overflow-x: hidden; + margin: 0; + padding: 0; +} + +button { + cursor: pointer; +} diff --git a/src/styles/print.css b/src/styles/print.css new file mode 100644 index 00000000..9f777d63 --- /dev/null +++ b/src/styles/print.css @@ -0,0 +1,27 @@ +/* AI 리포트 인쇄 — 드로어 패널 외 모든 요소 숨김 */ +@media print { + body.ai-report-printing > *:not([data-drawer-portal]) { + display: none !important; + } + + body.ai-report-printing [data-drawer-portal] { + position: static !important; + display: block !important; + } + + body.ai-report-printing [data-print-hide] { + display: none !important; + } + + body.ai-report-printing [data-drawer-portal] > div:last-child { + box-shadow: none !important; + border-radius: 0 !important; + max-width: 100% !important; + height: auto !important; + transform: none !important; + } + + @page { + margin: 16mm; + } +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 00000000..4941ac7c --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,76 @@ +@theme { + --color-logo-1: #2eb4ff; + --color-logo-2: #6088fe; + --color-logo-2-dark: #4d67e5; + + --color-brand-900: #000000; + --color-brand-800: #001f5b; + --color-brand-700: #213e68; + --color-brand-600: #466a9c; + --color-brand-500: #88c1ff; + --color-brand-400: #dce7f2; + --color-brand-300: #f9fafb; + --color-brand-200: #ffffff; + + --color-chart-1: #0a3d91; + --color-chart-2: #0056b3; + --color-chart-3: #1485ff; + --color-chart-4: #00aeef; + --color-chart-5: #4fc3f7; + --color-chart-inactive: #f2f4f6; + + --color-status-red: #ff2a4b; + --color-status-blue: #0084fe; + --color-status-green: #22c55e; + --color-status-yellow: #facc15; + + --color-text-main: #212121; + --color-text-auth-sub: #546171; + --color-text-sub: #8b8b8f; + --color-text-placeholder: #c3c3c3; + --color-text-disabled: #b0b8c1; + --color-bg-disabled: #e5e8eb; + --color-bg-surface: #f6f6f6; + + /* Landing (legacy 디자인 컬러를 토큰화) */ + --color-landing-surface: #f5f5f7; + --color-landing-section: #f8f9fc; + --color-landing-pricing: #fafafa; + --color-landing-dark: #1e1e1e; + --color-footer-bg: #121212; + --color-footer-border: #2a2a2a; + --color-landing-muted: #6e6e73; + + --color-social-kakao: #fee500; + --color-social-naver: #03c75a; + --color-social-google: #ffffff; + --color-social-text-kakao: #3a1d1d; + + --height-input: 56px; + --height-button-big: 55px; + --height-button-small: 40px; + + --radius-component-sm: 0.5rem; + --radius-component-md: 1rem; + --radius-component-lg: 1.5rem; + + --padding-none: 0; + --padding-sm: 1rem; + --padding-md: 1.5rem; + --padding-lg: 2rem; + + --max-width-sm: 384px; + --max-width-md: 512px; + --max-width-lg: 672px; + --max-width-xl: 896px; + + /* 애니메이션 이징 */ + --ease-smooth: cubic-bezier(0.25, 0.1, 0.25, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + + /* 애니메이션 속도 */ + --duration-fast: 150ms; + --duration-normal: 250ms; +} diff --git a/src/styles/utilities.css b/src/styles/utilities.css new file mode 100644 index 00000000..3bc88efe --- /dev/null +++ b/src/styles/utilities.css @@ -0,0 +1,413 @@ +@layer utilities { + .h-input { + height: var(--height-input); + } + .h-button-big { + height: var(--height-button-big); + } + .h-button-small { + height: var(--height-button-small); + } + + .rounded-component-sm { + border-radius: var(--radius-component-sm); + } + .rounded-component-md { + border-radius: var(--radius-component-md); + } + .rounded-component-lg { + border-radius: var(--radius-component-lg); + } + + .max-w-modal-sm { + max-width: var(--max-width-sm); + } + .max-w-modal-md { + max-width: var(--max-width-md); + } + .max-w-modal-lg { + max-width: var(--max-width-lg); + } + .max-w-modal-xl { + max-width: var(--max-width-xl); + } + + .p-modal-none { + padding: var(--padding-none); + } + .p-modal-sm { + padding: var(--padding-sm); + } + .p-modal-md { + padding: var(--padding-md); + } + .p-modal-lg { + padding: var(--padding-lg); + } + + /* 기본 트랜지션 */ + .transition-smooth { + transition-property: + transform, opacity, background-color, border-color, color, box-shadow; + transition-duration: var(--duration-normal); + transition-timing-function: var(--ease-smooth); + } + + .transition-fast { + transition-property: + transform, opacity, background-color, border-color, color, box-shadow; + transition-duration: var(--duration-fast); + transition-timing-function: var(--ease-out); + } + + /* 클릭 */ + .active-scale { + transition: transform var(--duration-fast) var(--ease-in-out); + } + .active-scale:active { + transform: scale(0.98); + } + + .font-hero { + font-size: 54px; + font-weight: 700; + line-height: 140%; + } + + .font-heading1 { + font-size: 30px; + font-weight: 600; + line-height: 130%; + } + + .font-heading2 { + font-size: 28px; + font-weight: 600; + line-height: 130%; + } + + .font-heading3 { + font-size: 24px; + font-weight: 500; + line-height: 130%; + } + + .font-heading4 { + font-size: 18px; + font-weight: 400; + line-height: 130%; + } + + .font-body1 { + font-size: 16px; + font-weight: 500; + line-height: 140%; + } + + .font-body2 { + font-size: 14px; + font-weight: 500; + line-height: 140%; + } + + .font-caption { + font-size: 12px; + font-weight: 500; + line-height: 130%; + } + + .font-label { + font-size: 14px; + font-weight: 600; + line-height: 140%; + } + + .shadow-Soft { + box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.1); + } + + .shadow-Medium { + box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.2); + } + + .shadow-card { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03); + } + + .shadow-card-hover { + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08); + } + + .checkbox { + appearance: none; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: #e5e8eb; + cursor: pointer; + position: relative; + transition: + background-color 0.2s ease-in-out, + border-color 0.2s ease-in-out; + flex-shrink: 0; + } + + .checkbox:checked { + background-color: #0084fe; + background-image: url("../assets/icon/common/check-white.svg"); + background-size: 60% 60%; + background-position: center; + background-repeat: no-repeat; + } + + .checkbox:focus-visible { + outline: 2px solid #0084fe; + outline-offset: 2px; + } + + /* 모달 애니메이션 */ + @keyframes modal-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes modal-fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + @keyframes modal-scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } + } + + @keyframes modal-scale-out { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } + } + + .animate-modal-overlay { + animation: modal-fade-in var(--duration-normal) var(--ease-out); + } + + .animate-modal-overlay-out { + animation: modal-fade-out var(--duration-fast) var(--ease-in) forwards; + } + + .animate-modal-content { + animation: modal-scale-in var(--duration-normal) var(--ease-out); + } + + .animate-modal-content-out { + animation: modal-scale-out var(--duration-fast) var(--ease-in) forwards; + } + + /* 스크롤 애니메이션 */ + @keyframes scroll { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } + } + + .animate-scroll { + animation: scroll 30s linear infinite; + } + + /* 슬라이드 텍스트 진입 애니메이션 */ + @keyframes slide-fade-up { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .animate-slide-fade-up { + animation: slide-fade-up 350ms var(--ease-out) both; + animation-delay: 100ms; + } + + /* 준비 중 플레이스홀더 — 자식 순차 진입 (인라인 delay 제거) */ + .coming-soon-stagger > * { + animation: slide-fade-up 350ms var(--ease-out) both; + } + .coming-soon-stagger > :nth-child(1) { + animation-delay: 0ms; + } + .coming-soon-stagger > :nth-child(2) { + animation-delay: 50ms; + } + .coming-soon-stagger > :nth-child(3) { + animation-delay: 100ms; + } + .coming-soon-stagger > :nth-child(4) { + animation-delay: 150ms; + } + + /* 그래프 상승 애니메이션 */ + @keyframes graph-up { + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); + } + } + + .animate-graph-up { + animation: graph-up 0.7s ease-out forwards; + transform-origin: bottom; + } + + /* AI 버블 진입 전환 */ + .bubble-transition { + transition-property: transform, opacity; + transition-duration: 300ms; + transition-timing-function: cubic-bezier(0.2, 1.05, 0.4, 1); + } + + /* 로딩 애니메이션 */ + @keyframes shimmer { + to { + transform: translateX(200%); + } + } + + .animate-shimmer { + position: relative; + overflow: hidden; + } + + .animate-shimmer::after { + content: ""; + position: absolute; + inset: 0; + transform: translateX(-100%); + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.5) 50%, + transparent 100% + ); + animation: shimmer 1.5s infinite linear; + } + + /* AI 요약 버튼 스파클 pulse 애니메이션 */ + @keyframes sparkle-pulse { + 0%, + 100% { + transform: scale(1); + opacity: 0.7; + } + 50% { + transform: scale(1.2); + opacity: 1; + } + } + + .animate-sparkle-pulse { + animation: sparkle-pulse 2.8s ease-in-out infinite; + } + + @media (prefers-reduced-motion: reduce) { + /* 스파클 */ + .animate-sparkle-pulse { + animation: none; + opacity: 1; + } + + /* 인트로 텍스트 진입 — 즉시 표시 */ + .animate-slide-fade-up { + animation: none; + opacity: 1; + transform: none; + } + + .coming-soon-stagger > * { + animation: none; + opacity: 1; + transform: none; + } + + /* 인트로 캐러셀 — 위치 이동 중단 */ + .animate-scroll { + animation: none; + } + + /* 바 차트 — 즉시 완성 상태로 표시 */ + .animate-graph-up { + animation: none; + transform: scaleY(1); + } + + /* AI 버블 — transform/opacity 전환 제거 */ + .bubble-transition { + transition: none; + } + + /* 모달 — scale 제거, opacity 전환만 유지 */ + .animate-modal-content { + animation: modal-fade-in var(--duration-normal) var(--ease-out); + } + .animate-modal-content-out { + animation: modal-fade-out var(--duration-fast) var(--ease-in) forwards; + } + + /* 토스트 — translateY 제거, opacity 전환만 유지 */ + @keyframes fade-in-up-reduced { + from { + opacity: 0; + transform: translate(-50%, 0); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } + } + .animate-fade-in-up { + animation: fade-in-up-reduced 200ms var(--ease-out) forwards; + } + } + + /* 토스트 애니메이션 */ + @keyframes fade-in-up { + from { + opacity: 0; + transform: translate(-50%, 12px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } + } + + .animate-fade-in-up { + animation: fade-in-up 0.2s var(--ease-out) both; + } +} From 29f925f8f4ec84d70f81826c9d4aa6de31a07988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Mon, 11 May 2026 14:56:19 +0900 Subject: [PATCH 02/33] =?UTF-8?q?refactor(styles):=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=C2=B7=EC=9C=A0=ED=8B=B8=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AA=A8=EC=85=98=20Framer=20Motion=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tokens: 텍스트/서페이스/프라이머리 팔레트 정리, 타이포 스케일 조정 - 전역 컴포넌트 반경·높이·모달 크기를 Tailwind 기본 스케일로 통일 - utilities: 미사용 CSS 애니메이션 제거, 쉬머만 유지(reduced-motion 대응) - Modal·DropdownMenu·ComingSoon: AnimatePresence/motion으로 전환 - Storybook Colors·Typography·BorderRadius를 토큰과 맞춤 --- src/components/ads/AdDetailContent.tsx | 2 +- src/components/auth/common/InputActions.tsx | 2 +- .../auth/flows/find-email/EnterPhoneStep.tsx | 2 +- .../flows/find-email/ShowEmailResultStep.tsx | 4 +- .../reset-password/EmailVerificationStep.tsx | 2 +- .../auth/flows/signup/EnterEmailStep.tsx | 2 +- .../auth/skeleton/AuthFormSkeleton.tsx | 8 +- .../auth/skeleton/LoginPageSkeleton.tsx | 6 +- .../auth/skeleton/SignupEmailStepSkeleton.tsx | 8 +- .../auth/skeleton/SignupPageSkeleton.tsx | 8 +- .../common/ComingSoonPlaceholder.tsx | 69 +++- src/components/common/alert/Alert.tsx | 2 +- src/components/common/button/Button.tsx | 6 +- src/components/common/card/Card.tsx | 2 +- src/components/common/card/InfoCard.tsx | 2 +- .../common/card/StatCard.stories.tsx | 2 +- src/components/common/card/StatCard.tsx | 2 +- .../common/controlbox/ControlBox.tsx | 4 +- src/components/common/drawer/Drawer.tsx | 8 +- .../common/dropdownmenu/DropdownMenu.tsx | 120 ++++--- src/components/common/input/Input.tsx | 2 +- src/components/common/modal/Modal.stories.tsx | 6 +- src/components/common/modal/Modal.tsx | 163 ++++----- .../common/select/DropdownSelect.tsx | 4 +- src/components/common/select/SearchSelect.tsx | 4 +- .../common/textarea/TextareaField.tsx | 2 +- .../dashboard/charts/AdStatusChart.tsx | 2 +- .../dashboard/charts/TrafficChart.tsx | 2 +- .../dashboard/platform/PlatformDetailCard.tsx | 2 +- .../dashboard/platform/PlatformRoasTable.tsx | 2 +- .../platform/skeleton/PlatformSkeleton.tsx | 14 +- src/components/landing/GuideOverviewChart.tsx | 4 +- src/components/landing/GuidePlatform.tsx | 2 +- src/components/landing/LandingFAQ.tsx | 2 +- src/components/landing/LandingFeatures.tsx | 8 +- src/components/landing/LandingGuide.tsx | 4 +- src/components/landing/LandingMultiDevice.tsx | 6 +- src/components/landing/LandingPricing.tsx | 4 +- .../modal/privacyModal/AgreementItem.tsx | 2 +- src/components/setting/PasswordSection.tsx | 2 +- src/components/setting/ProfileSection.tsx | 6 +- src/components/sidebar/Sidebar.tsx | 6 +- src/components/sidebar/SubMenu.tsx | 4 +- src/components/sidebar/WorkspaceSwitcher.tsx | 16 +- .../workspace/InviteMemberModal.tsx | 6 +- src/components/workspace/MemberItem.tsx | 4 +- src/components/workspace/MemberList.tsx | 6 +- .../workspace/MemberManagementLoading.tsx | 12 +- src/components/workspace/MemberRoleSelect.tsx | 2 +- src/components/workspace/PermissionTable.tsx | 8 +- src/components/workspace/WorkspaceCard.tsx | 6 +- .../workspace/WorkspaceEmptyState.tsx | 2 +- .../workspace/WorkspaceListError.tsx | 2 +- .../workspace/WorkspaceListLoading.tsx | 8 +- .../workspace/WorkspaceSettingLoading.tsx | 18 +- src/pages/ads/list/CampaignDetail.tsx | 2 +- .../dashboard/overview/OverviewKpiSection.tsx | 2 +- .../dashboard/platform/PlatformDashboard.tsx | 4 +- src/pages/workspace/Workspace.tsx | 2 +- src/pages/workspace/WorkspaceSetting.tsx | 16 +- src/stories/BorderRadius.stories.tsx | 29 +- src/stories/Colors.stories.tsx | 204 +++++++---- src/stories/Typography.stories.tsx | 310 ++++++++++++++--- src/styles/print.css | 5 + src/styles/tokens.css | 131 ++++---- src/styles/utilities.css | 318 ++---------------- 66 files changed, 862 insertions(+), 765 deletions(-) diff --git a/src/components/ads/AdDetailContent.tsx b/src/components/ads/AdDetailContent.tsx index 63452183..82e2649b 100644 --- a/src/components/ads/AdDetailContent.tsx +++ b/src/components/ads/AdDetailContent.tsx @@ -106,7 +106,7 @@ export default function AdDetailContent({
랜딩 URL
-
+
{ad.landingUrl} diff --git a/src/components/auth/common/InputActions.tsx b/src/components/auth/common/InputActions.tsx index c0bf42df..577526cc 100644 --- a/src/components/auth/common/InputActions.tsx +++ b/src/components/auth/common/InputActions.tsx @@ -45,7 +45,7 @@ export default function InputActions({ {validationState && ( diff --git a/src/components/auth/flows/find-email/EnterPhoneStep.tsx b/src/components/auth/flows/find-email/EnterPhoneStep.tsx index 9c9806d7..e5319729 100644 --- a/src/components/auth/flows/find-email/EnterPhoneStep.tsx +++ b/src/components/auth/flows/find-email/EnterPhoneStep.tsx @@ -149,7 +149,7 @@ export default function EnterPhoneStep({ onNext }: IEnterPhoneStepProps) { value={watchedPhone || ""} readOnly aria-label="입력된 전화번호" - className="w-full h-13.5 px-5 border rounded-component-md text-body1 text-text-main bg-white border-brand-400 focus:outline-none focus:border-brand-400" + className="w-full h-13.5 px-5 border rounded-2xl text-body1 text-text-main bg-white border-brand-400 focus:outline-none focus:border-brand-400" />
diff --git a/src/components/auth/skeleton/LoginPageSkeleton.tsx b/src/components/auth/skeleton/LoginPageSkeleton.tsx index 554ec7d9..5158f133 100644 --- a/src/components/auth/skeleton/LoginPageSkeleton.tsx +++ b/src/components/auth/skeleton/LoginPageSkeleton.tsx @@ -14,11 +14,11 @@ export default function LoginPageSkeleton() {
- +
- +
@@ -26,7 +26,7 @@ export default function LoginPageSkeleton() {
- +
diff --git a/src/components/auth/skeleton/SignupEmailStepSkeleton.tsx b/src/components/auth/skeleton/SignupEmailStepSkeleton.tsx index e3cbdc80..5dc3da9f 100644 --- a/src/components/auth/skeleton/SignupEmailStepSkeleton.tsx +++ b/src/components/auth/skeleton/SignupEmailStepSkeleton.tsx @@ -12,17 +12,17 @@ export default function SignupEmailStepSkeleton() {
- +
- +
- +
- +
diff --git a/src/components/auth/skeleton/SignupPageSkeleton.tsx b/src/components/auth/skeleton/SignupPageSkeleton.tsx index f8319cf0..502768e7 100644 --- a/src/components/auth/skeleton/SignupPageSkeleton.tsx +++ b/src/components/auth/skeleton/SignupPageSkeleton.tsx @@ -5,10 +5,10 @@ export default function SignupPageSkeleton() { return (
- - - - + + + +
diff --git a/src/components/common/ComingSoonPlaceholder.tsx b/src/components/common/ComingSoonPlaceholder.tsx index 37d28778..ef9b5336 100644 --- a/src/components/common/ComingSoonPlaceholder.tsx +++ b/src/components/common/ComingSoonPlaceholder.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; import { memo } from "react"; +import { motion, useReducedMotion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import WarningIcon from "@/assets/icon/ai/warning.svg?react"; @@ -11,12 +12,38 @@ export interface IComingSoonPlaceholderProps { className?: string; } +const easeOut = [0, 0, 0.2, 1] as const; + +const containerVariants = { + hidden: {}, + visible: (reduce: boolean) => ({ + transition: { + staggerChildren: reduce ? 0 : 0.05, + delayChildren: 0, + }, + }), +}; + +const itemVariants = { + hidden: (reduce: boolean) => ({ + opacity: reduce ? 1 : 0, + y: reduce ? 0 : 10, + }), + visible: (reduce: boolean) => ({ + opacity: 1, + y: 0, + transition: { duration: reduce ? 0 : 0.35, ease: easeOut }, + }), +}; + const ComingSoonPlaceholder = memo(function ComingSoonPlaceholder({ title, description, footer, className, }: IComingSoonPlaceholderProps) { + const reduceMotion = useReducedMotion(); + return (
-
-
+ -
+ -

+ {title} -

+ -

+ {description} -

+ -
+

{footer}

-
-
+ +
); }); diff --git a/src/components/common/alert/Alert.tsx b/src/components/common/alert/Alert.tsx index dce12f01..85da9ca6 100644 --- a/src/components/common/alert/Alert.tsx +++ b/src/components/common/alert/Alert.tsx @@ -17,7 +17,7 @@ export default function Alert({ children, ...rest }: IAlertProps) { - const base = "w-full rounded-component-lg px-5 py-4"; + const base = "w-full rounded-3xl px-5 py-4"; const variantClasses: Record = { info: "text-brand-700", diff --git a/src/components/common/button/Button.tsx b/src/components/common/button/Button.tsx index 5b96b782..48ab6a4c 100644 --- a/src/components/common/button/Button.tsx +++ b/src/components/common/button/Button.tsx @@ -29,9 +29,9 @@ export default function Button({ ...rest }: IButtonProps) { const sizeClasses = { - big: "h-button-big px-6 rounded-component-md font-heading4 transition-colors duration-normal ease-out", + big: "h-14 px-6 rounded-2xl font-heading4 transition-colors duration-normal ease-out", small: - "h-button-small px-4 rounded-component-sm font-body1 transition-colors duration-normal ease-out", + "h-10 px-4 rounded-lg font-body1 transition-colors duration-normal ease-out", }; const variantClasses = { @@ -48,7 +48,7 @@ export default function Button({ gradient: "bg-linear-to-r from-logo-1 to-logo-2 text-white hover:opacity-90 shadow-brand-500/30 disabled:bg-bg-disabled disabled:text-text-disabled disabled:shadow-none disabled:hover:opacity-50", tertiary: - "!h-7 border border-gray-200 text-text-auth-sub px-5 rounded-component-lg bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out", + "!h-7 border border-gray-200 text-text-auth-sub px-5 rounded-3xl bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out", custom: "", }; diff --git a/src/components/common/card/Card.tsx b/src/components/common/card/Card.tsx index 5e915123..df29d6b0 100644 --- a/src/components/common/card/Card.tsx +++ b/src/components/common/card/Card.tsx @@ -21,7 +21,7 @@ const Card = memo(function Card({ return (
( -
+
{[ { title: "클릭수(CTR)", diff --git a/src/components/common/card/StatCard.tsx b/src/components/common/card/StatCard.tsx index fe274b43..c4c6ac83 100644 --- a/src/components/common/card/StatCard.tsx +++ b/src/components/common/card/StatCard.tsx @@ -66,7 +66,7 @@ const StatCard = memo(function StatCard({ return (
diff --git a/src/components/common/drawer/Drawer.tsx b/src/components/common/drawer/Drawer.tsx index c90c68a7..f3f86f77 100644 --- a/src/components/common/drawer/Drawer.tsx +++ b/src/components/common/drawer/Drawer.tsx @@ -152,13 +152,13 @@ export default function Drawer({ } aria-label="더보기" - className="h-10 w-10 cursor-pointer rounded-component-sm hover:bg-gray-100 transition-colors flex items-center justify-center" + className="h-10 w-10 cursor-pointer rounded-lg hover:bg-gray-100 transition-colors flex items-center justify-center" items={dropdownItems} /> )}
diff --git a/src/components/common/dropdownmenu/DropdownMenu.tsx b/src/components/common/dropdownmenu/DropdownMenu.tsx index 25696afd..2f4f1d31 100644 --- a/src/components/common/dropdownmenu/DropdownMenu.tsx +++ b/src/components/common/dropdownmenu/DropdownMenu.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useId, useRef, useState } from "react"; +import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { twMerge } from "tailwind-merge"; export type TMenuItem = { @@ -8,6 +9,9 @@ export type TMenuItem = { active?: boolean; }; +const easeOut = [0, 0, 0.2, 1] as const; +const easeIn = [0.4, 0, 1, 1] as const; + export function DropdownMenu({ trigger, items, @@ -21,10 +25,14 @@ export function DropdownMenu({ menuClassName?: string; "aria-label"?: string; }) { + const reduceMotion = useReducedMotion(); const [open, setOpen] = useState(false); const ref = useRef(null); const menuId = useId(); + const openMs = reduceMotion ? 0 : 0.2; + const closeMs = reduceMotion ? 0 : 0.15; + useEffect(() => { const onDocClick = (e: MouseEvent) => { if (!ref.current) return; @@ -55,61 +63,73 @@ export function DropdownMenu({ > {typeof trigger === "function" ? trigger(open) : trigger}
- {open && ( - - )} +
+ +
+ ))} +
+ + ) : null} +
); } diff --git a/src/components/common/input/Input.tsx b/src/components/common/input/Input.tsx index 4d7f8bb5..9fdb02de 100644 --- a/src/components/common/input/Input.tsx +++ b/src/components/common/input/Input.tsx @@ -52,7 +52,7 @@ const Input = forwardRef( )}
setOpen(true)} - className="h-button-small px-4 rounding-15 bg-status-blue text-white font-body2 hover:bg-status-blue/80 transition" + className="h-10 px-4 rounding-15 bg-status-blue text-white font-body2 hover:bg-status-blue/80 transition" aria-label="모달 열기 버튼" > 모달 열기 @@ -57,7 +57,7 @@ export const Default: TModalStory = { - )} - {children} -
-
+ {title ? ( +

+ {title} +

+ ) : null} + {!hideCloseButton ? ( + + ) : null} + {children} + + + ) : null} + , modalRoot, ); diff --git a/src/components/common/select/DropdownSelect.tsx b/src/components/common/select/DropdownSelect.tsx index 634ca8ad..6f6db1e2 100644 --- a/src/components/common/select/DropdownSelect.tsx +++ b/src/components/common/select/DropdownSelect.tsx @@ -51,7 +51,7 @@ export default function DropdownSelect({ type="button" onClick={() => setIsOpen(!isOpen)} className={twMerge( - "flex items-center justify-between w-full h-14 px-5 bg-white border border-bg-disabled rounded-component-md text-body1 transition-all outline-none", + "flex items-center justify-between w-full h-14 px-5 bg-white border border-bg-disabled rounded-2xl text-body1 transition-all outline-none", isOpen ? "border-status-blue ring-1 ring-status-blue" : "hover:border-text-disabled focus:border-status-blue focus:ring-1 focus:ring-status-blue", @@ -70,7 +70,7 @@ export default function DropdownSelect({ {isOpen && ( -
+
    {options.map((option) => { const key = getOptionKey(option); diff --git a/src/components/common/select/SearchSelect.tsx b/src/components/common/select/SearchSelect.tsx index 1a661572..f58edb55 100644 --- a/src/components/common/select/SearchSelect.tsx +++ b/src/components/common/select/SearchSelect.tsx @@ -107,13 +107,13 @@ export default function SearchSelect({ aria-controls={listboxId} aria-haspopup="listbox" aria-autocomplete="list" - className="h-13 w-full rounded-component-md border border-status-blue px-4 pl-5 font-body1 outline-none transition-colors placeholder:text-text-placeholder focus:border-status-blue" + className="h-13 w-full rounded-2xl border border-status-blue px-4 pl-5 font-body1 outline-none transition-colors placeholder:text-text-placeholder focus:border-status-blue" />
{isOpen && ( -
+
{filteredOptions.length > 0 ? (
    {
{/* 세그먼트 바 */} -
+
{data.map((item) => (
-
+

diff --git a/src/components/dashboard/platform/PlatformDetailCard.tsx b/src/components/dashboard/platform/PlatformDetailCard.tsx index ccfd3ddd..668812b6 100644 --- a/src/components/dashboard/platform/PlatformDetailCard.tsx +++ b/src/components/dashboard/platform/PlatformDetailCard.tsx @@ -31,7 +31,7 @@ export const PlatformDetailCard = memo( } = data; const innerCardClass = - "shadow-none! hover:shadow-none! rounded-component-md! p-2! gap-2!"; + "shadow-none! hover:shadow-none! !rounded-2xl p-2! gap-2!"; return ( diff --git a/src/components/dashboard/platform/PlatformRoasTable.tsx b/src/components/dashboard/platform/PlatformRoasTable.tsx index f477d57a..b528c041 100644 --- a/src/components/dashboard/platform/PlatformRoasTable.tsx +++ b/src/components/dashboard/platform/PlatformRoasTable.tsx @@ -91,7 +91,7 @@ const PlatformRoasTable = memo(function PlatformRoasTable({ {rankings.map((item) => (

{/* 순위 */}
diff --git a/src/components/dashboard/platform/skeleton/PlatformSkeleton.tsx b/src/components/dashboard/platform/skeleton/PlatformSkeleton.tsx index 79670d56..3444ac45 100644 --- a/src/components/dashboard/platform/skeleton/PlatformSkeleton.tsx +++ b/src/components/dashboard/platform/skeleton/PlatformSkeleton.tsx @@ -24,7 +24,7 @@ export function TopPerformanceListSkeleton() { // 개별 플랫폼 상세 카드 export function PlatformDetailCardSkeleton() { return ( -
+
@@ -33,7 +33,7 @@ export function PlatformDetailCardSkeleton() { {[1, 2, 3, 4].map((i) => (
@@ -48,7 +48,7 @@ export function PlatformDetailCardSkeleton() { export function AdStatusChartSkeleton() { return (
- +
); } @@ -57,18 +57,16 @@ export function AdStatusChartSkeleton() { export function PerformanceEfficiencyChartSkeleton() { return (
- +
); } // 실시간 트래픽 변화 export function TrafficChartSkeleton() { - return ; + return ; } export function BadgeSkeleton({ className }: { className?: string }) { - return ( - - ); + return ; } diff --git a/src/components/landing/GuideOverviewChart.tsx b/src/components/landing/GuideOverviewChart.tsx index 0b26c25d..6ea4d3af 100644 --- a/src/components/landing/GuideOverviewChart.tsx +++ b/src/components/landing/GuideOverviewChart.tsx @@ -31,11 +31,11 @@ export default function GuideOverviewChart() {

+
} >
-
+

광고 클릭수 추이

diff --git a/src/components/landing/GuidePlatform.tsx b/src/components/landing/GuidePlatform.tsx index ec056fd3..d17d5ada 100644 --- a/src/components/landing/GuidePlatform.tsx +++ b/src/components/landing/GuidePlatform.tsx @@ -90,7 +90,7 @@ export default function GuidePlatform() { {isMenuOpen && ( -
+
{PLATFORMS.map((platform) => { const isSelected = selectedIds.includes(platform.id); diff --git a/src/components/landing/LandingFAQ.tsx b/src/components/landing/LandingFAQ.tsx index a1cf3055..f85f74c9 100644 --- a/src/components/landing/LandingFAQ.tsx +++ b/src/components/landing/LandingFAQ.tsx @@ -20,7 +20,7 @@ export default function LandingFAQ() { />
-
+
{LANDING_FAQ_ITEMS.map(({ q, a }) => (
diff --git a/src/components/landing/LandingFeatures.tsx b/src/components/landing/LandingFeatures.tsx index dc7cb41d..592f8e3f 100644 --- a/src/components/landing/LandingFeatures.tsx +++ b/src/components/landing/LandingFeatures.tsx @@ -28,7 +28,7 @@ function IntegrationGraphic() { viewport={{ once: true, amount: 0.45 }} transition={{ duration: 0.5, ease: "easeOut" }} > - +
@@ -40,7 +40,7 @@ function IntegrationGraphic() {
- +
@@ -56,7 +56,7 @@ function IntegrationGraphic() {
- +
@@ -158,7 +158,7 @@ function WorkspaceGraphic() { return (
diff --git a/src/components/landing/LandingGuide.tsx b/src/components/landing/LandingGuide.tsx index dd2dd0e1..8caaafaf 100644 --- a/src/components/landing/LandingGuide.tsx +++ b/src/components/landing/LandingGuide.tsx @@ -47,7 +47,7 @@ export default function LandingGuide() { transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }} > (
diff --git a/src/components/landing/LandingMultiDevice.tsx b/src/components/landing/LandingMultiDevice.tsx index 6dc9c206..272b2bbe 100644 --- a/src/components/landing/LandingMultiDevice.tsx +++ b/src/components/landing/LandingMultiDevice.tsx @@ -20,7 +20,7 @@ export default function LandingMultiDevice() {
-
+
@@ -38,7 +38,7 @@ export default function LandingMultiDevice() {
-
+
@@ -56,7 +56,7 @@ export default function LandingMultiDevice() {
-
+
diff --git a/src/components/landing/LandingPricing.tsx b/src/components/landing/LandingPricing.tsx index 6c82ffcd..282d3907 100644 --- a/src/components/landing/LandingPricing.tsx +++ b/src/components/landing/LandingPricing.tsx @@ -110,7 +110,7 @@ export default function LandingPricing() { {plan.name === "프로" ? ( {expanded && ( -
+
{content} diff --git a/src/components/setting/PasswordSection.tsx b/src/components/setting/PasswordSection.tsx index 9e55a403..824035f5 100644 --- a/src/components/setting/PasswordSection.tsx +++ b/src/components/setting/PasswordSection.tsx @@ -34,7 +34,7 @@ export default function PasswordSection({ const [showConfirm, setShowConfirm] = useState(false); return ( -
+
diff --git a/src/components/setting/ProfileSection.tsx b/src/components/setting/ProfileSection.tsx index 99118312..f9496ce1 100644 --- a/src/components/setting/ProfileSection.tsx +++ b/src/components/setting/ProfileSection.tsx @@ -32,7 +32,7 @@ export default function ProfileSection({ resetImage, }: TProfileSectionProps) { return ( -
+
@@ -69,7 +69,7 @@ export default function ProfileSection({ variant="custom" type="button" onClick={openFilePicker} - className="h-7! border border-gray-200 text-text-auth-sub px-4 rounded-component-lg bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out" + className="h-7! border border-gray-200 text-text-auth-sub px-4 rounded-3xl bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out" aria-label="프로필 이미지 변경 버튼" > 변경 @@ -78,7 +78,7 @@ export default function ProfileSection({ variant="custom" type="button" onClick={resetImage} - className="h-7! border border-gray-200 text-text-auth-sub px-4 rounded-component-lg bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out disabled:cursor-not-allowed disabled:opacity-40" + className="h-7! border border-gray-200 text-text-auth-sub px-4 rounded-3xl bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out disabled:cursor-not-allowed disabled:opacity-40" aria-label="프로필 이미지 초기화 버튼" disabled={!preview} > diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index b8a6dfd6..e74bd53b 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -20,7 +20,7 @@ import ChevronIcon from "@/assets/icon/chevron/chevron-up.svg?react"; function getMainItemClass(isActive: boolean, isCollapsed: boolean) { return twMerge( - "flex items-center rounded-component-md px-3 text-sm cursor-pointer transition-colors duration-200", + "flex items-center rounded-2xl px-3 text-sm cursor-pointer transition-colors duration-200", isCollapsed ? "h-[55px] w-[55px] mx-auto flex justify-center" : "h-[55px] gap-4 px-3", @@ -32,7 +32,7 @@ function getMainItemClass(isActive: boolean, isCollapsed: boolean) { function getFooterItemClass(isActive: boolean, isCollapsed: boolean) { return twMerge( - "flex w-full h-[55px] items-center rounded-component-md px-3 text-sm cursor-pointer transition-all duration-200", + "flex w-full h-[55px] items-center rounded-2xl px-3 text-sm cursor-pointer transition-all duration-200", isCollapsed ? "justify-center px-0" : "gap-4 px-3", isActive ? "text-chart-3" : "text-text-auth-sub hover:text-chart-3", ); @@ -196,7 +196,7 @@ export default function Sidebar() { aria-label={isCollapsed ? "사이드바 펼치기" : "사이드바 접기"} onClick={toggleSidebar} className={twMerge( - "w-full h-button-big rounded-component-md text-sm transition-all duration-200 inline-flex items-center", + "w-full h-14 rounded-2xl text-sm transition-all duration-200 inline-flex items-center", isCollapsed ? "justify-center px-0" : "gap-4 px-3", "text-text-auth-sub hover:text-chart-3 hover:bg-bg-surface", )} diff --git a/src/components/sidebar/SubMenu.tsx b/src/components/sidebar/SubMenu.tsx index dc507943..fad5e243 100644 --- a/src/components/sidebar/SubMenu.tsx +++ b/src/components/sidebar/SubMenu.tsx @@ -13,7 +13,7 @@ interface ISubMenuProps { export function SubMenu({ items, isCollapsed, parentLabel }: ISubMenuProps) { const getSubItemClass = (isActive: boolean) => twMerge( - "flex items-center rounded-component-md px-3 text-sm transition-all duration-200 whitespace-nowrap", + "flex items-center rounded-2xl px-3 text-sm transition-all duration-200 whitespace-nowrap", isCollapsed ? "h-auto py-2.5" : "h-10 pl-4", isActive ? "bg-chart-3 text-white" @@ -21,7 +21,7 @@ export function SubMenu({ items, isCollapsed, parentLabel }: ISubMenuProps) { ); const menuContainerClass = isCollapsed - ? "absolute left-full top-0 pl-2 w-52 flex flex-col gap-1 rounded-component-md bg-white p-2 shadow-Soft z-50 whitespace-nowrap" + ? "absolute left-full top-0 pl-2 w-52 flex flex-col gap-1 rounded-2xl bg-white p-2 shadow-Soft z-50 whitespace-nowrap" : "ml-11 mt-1 flex flex-col gap-1 overflow-hidden"; const expandedMaxHeight = Math.min(640, Math.max(160, items.length * 44 + 8)); diff --git a/src/components/sidebar/WorkspaceSwitcher.tsx b/src/components/sidebar/WorkspaceSwitcher.tsx index c0fbe1a3..63d5b4ed 100644 --- a/src/components/sidebar/WorkspaceSwitcher.tsx +++ b/src/components/sidebar/WorkspaceSwitcher.tsx @@ -111,7 +111,7 @@ export function WorkspaceSwitcher({ const renderImage = useCallback( (workspace: TWorkspace) => ( -
+
{workspace?.logoUrl ? ( -
+
{!isCollapsed && (
@@ -157,7 +157,7 @@ export function WorkspaceSwitcher({ return (
-
+
{!isCollapsed && ( @@ -214,7 +214,7 @@ export function WorkspaceSwitcher({ { saveWorkspace(org.orgId); }} - className="group flex w-full items-center gap-3 rounded-component-md px-2 py-1.5 text-sm text-text-main hover:bg-bg-surface transition-colors" + className="group flex w-full items-center gap-3 rounded-2xl px-2 py-1.5 text-sm text-text-main hover:bg-bg-surface transition-colors" > {renderImage(org)}
@@ -270,9 +270,9 @@ export function WorkspaceSwitcher({ setIsOpen(false); navigate("/workspace"); }} - className="flex w-full items-center gap-3 rounded-component-md px-2 py-1.5 text-sm text-text-sub hover:bg-bg-surface transition-colors" + className="flex w-full items-center gap-3 rounded-2xl px-2 py-1.5 text-sm text-text-sub hover:bg-bg-surface transition-colors" > -
+
+
diff --git a/src/components/workspace/InviteMemberModal.tsx b/src/components/workspace/InviteMemberModal.tsx index 46fac710..1624bdc5 100644 --- a/src/components/workspace/InviteMemberModal.tsx +++ b/src/components/workspace/InviteMemberModal.tsx @@ -113,7 +113,7 @@ export default function InviteMemberModal({ type="button" aria-label="팀원 초대 링크 복사 버튼" onClick={handleCopyLink} - className="flex items-center gap-2 rounded-component-sm px-2 py-1 text-chart-3 transition-opacity hover:opacity-80" + className="flex items-center gap-2 rounded-lg px-2 py-1 text-chart-3 transition-opacity hover:opacity-80" > 링크 복사 @@ -152,7 +152,7 @@ export default function InviteMemberModal({ className="flex items-center justify-between gap-4 py-4" >
-
+
@@ -179,7 +179,7 @@ export default function InviteMemberModal({ className="flex items-center justify-between gap-4 py-4" >
-
+
{item.profileImageUrl ? (
-
+
{member.profileImageUrl ? ( {member.isMe ? ( 0; return ( -
+

@@ -81,7 +81,7 @@ export default function MemberList({ size="small" aria-label="팀원 초대 버튼" onClick={openInviteMember} - className="p-5 py-6 rounded-component-md" + className="p-5 py-6 rounded-2xl" > 팀원 초대 @@ -89,7 +89,7 @@ export default function MemberList({

{!hasVisibleItems ? ( -
+
아직 등록된 팀원이 없습니다
) : ( diff --git a/src/components/workspace/MemberManagementLoading.tsx b/src/components/workspace/MemberManagementLoading.tsx index a50338a7..2afcac95 100644 --- a/src/components/workspace/MemberManagementLoading.tsx +++ b/src/components/workspace/MemberManagementLoading.tsx @@ -2,7 +2,7 @@ import { Skeleton, SkeletonCircle } from "../common/skeleton/Skeleton"; function MemberRowSkeleton() { return ( -
+
@@ -11,8 +11,8 @@ function MemberRowSkeleton() {
- - + +
); @@ -38,14 +38,14 @@ export default function MemberManagementLoading() { className="w-full min-w-0 flex flex-col gap-8" >
-
+
- +
{Array.from({ length: 5 }).map((_, i) => ( @@ -54,7 +54,7 @@ export default function MemberManagementLoading() {
-
+
diff --git a/src/components/workspace/MemberRoleSelect.tsx b/src/components/workspace/MemberRoleSelect.tsx index ce6300dd..c48cf01f 100644 --- a/src/components/workspace/MemberRoleSelect.tsx +++ b/src/components/workspace/MemberRoleSelect.tsx @@ -45,7 +45,7 @@ export default function MemberRoleSelect({ className={disabled ? "pointer-events-none opacity-50" : undefined} trigger={(open) => (
{ROLE_LABEL_MAP[role]} +
가능
@@ -109,7 +109,7 @@ export default function PermissionTable() { }; return ( -
+

권한 설정 @@ -172,7 +172,7 @@ export default function PermissionTable() { size="big" onClick={handleResetChange} disabled={!hasChanges || isSaving} - className="rounded-component-md" + className="rounded-2xl" > 변경 취소 @@ -182,7 +182,7 @@ export default function PermissionTable() { size="big" onClick={handleSaveChanges} disabled={!hasChanges || isSaving} - className="rounded-component-md" + className="rounded-2xl" > {isSaving ? "저장 중..." : "변경사항 저장하기"} diff --git a/src/components/workspace/WorkspaceCard.tsx b/src/components/workspace/WorkspaceCard.tsx index 6269608e..2f06ad51 100644 --- a/src/components/workspace/WorkspaceCard.tsx +++ b/src/components/workspace/WorkspaceCard.tsx @@ -28,7 +28,7 @@ function WorkspaceCard({ workspace: w, isSelected = false, onClick }: TProps) {

- {radiusTokens.map(({ className, value, usage }) => ( + {radiusTokens.map(({ className, label, value, usage }) => (
- .{className} + + .{className} + + {label} + +
diff --git a/src/stories/Colors.stories.tsx b/src/stories/Colors.stories.tsx index d5832856..39b95211 100644 --- a/src/stories/Colors.stories.tsx +++ b/src/stories/Colors.stories.tsx @@ -1,99 +1,149 @@ +import type { CSSProperties } from "react"; +import { useLayoutEffect, useRef, useState } from "react"; import type { Meta, StoryObj } from "@storybook/react"; -const colorGroups = [ - { - label: "Brand", - colors: [ - { name: "brand-900", value: "#000000" }, - { name: "brand-800", value: "#001f5b" }, - { name: "brand-700", value: "#213e68" }, - { name: "brand-600", value: "#466a9c" }, - { name: "brand-500", value: "#88c1ff" }, - { name: "brand-400", value: "#dce7f2" }, - { name: "brand-300", value: "#f9fafb" }, - { name: "brand-200", value: "#ffffff" }, - ], - }, - { - label: "Logo", - colors: [ - { name: "logo-1", value: "#2eb4ff" }, - { name: "logo-2", value: "#6088fe" }, - ], - }, +type TSwatchItem = { + varName: string; + note?: string; +}; + +const colorSections: { + title: string; + description?: string; + items: TSwatchItem[]; +}[] = [ { - label: "Status", - colors: [ - { name: "status-red", value: "#ff2a4b" }, - { name: "status-blue", value: "#0084fe" }, - { name: "status-green", value: "#22c55e" }, - { name: "status-yellow", value: "#facc15" }, + title: "Primary", + description: + "원시 팔레트 (--palette-primary-*). 100이 가장 밝고 500이 가장 진합니다.", + items: [ + { varName: "--palette-primary-100", note: "Blue tint" }, + { varName: "--palette-primary-200", note: "Light blue" }, + { varName: "--palette-primary-300", note: "Mid blue" }, + { varName: "--palette-primary-400", note: "CTA blue" }, + { varName: "--palette-primary-500", note: "Brand blue" }, ], }, { - label: "Text", - colors: [ - { name: "text-main", value: "#212121" }, - { name: "text-auth-sub", value: "#546171" }, - { name: "text-sub", value: "#8b8b8f" }, - { name: "text-placeholder", value: "#c3c3c3" }, - { name: "text-disabled", value: "#b0b8c1" }, - { name: "bg-disabled", value: "#e5e8eb" }, - { name: "bg-surface", value: "#f6f6f6" }, + title: "Surface", + description: + "밝기 순 (--color-surface-*). 100이 가장 밝고 500이 가장 진합니다.", + items: [ + { varName: "--color-surface-100" }, + { varName: "--color-surface-200" }, + { varName: "--color-surface-300" }, + { varName: "--color-surface-400" }, + { varName: "--color-surface-500" }, ], }, { - label: "Social", - colors: [ - { name: "social-kakao", value: "#fee500" }, - { name: "social-naver", value: "#03c75a" }, - { name: "social-google", value: "#ffffff" }, + title: "Text (밝기)", + description: "쿨 그레이 스케일. 100이 가장 밝고 400이 가장 진합니다.", + items: [ + { varName: "--color-text-100", note: "가장 밝음" }, + { varName: "--color-text-200" }, + { varName: "--color-text-300" }, + { varName: "--color-text-400", note: "가장 진함" }, ], }, { - label: "Chart", - colors: [ - { name: "chart-1", value: "#0a3d91" }, - { name: "chart-2", value: "#0056b3" }, - { name: "chart-3", value: "#1485ff" }, - { name: "chart-4", value: "#00aeef" }, - { name: "chart-5", value: "#4fc3f7" }, - { name: "chart-inactive", value: "#f2f4f6" }, + title: "Text (역할 별칭)", + description: + "위 밝기 토큰을 가리킵니다. 계산 색은 동일 스케일로 이어집니다.", + items: [ + { varName: "--color-text-title", note: "→ 400" }, + { varName: "--color-text-body", note: "→ 300" }, + { varName: "--color-text-muted", note: "→ 200" }, + { varName: "--color-text-placeholder", note: "→ 200" }, + { varName: "--color-text-disabled", note: "→ 100" }, ], }, ]; +const checkerboardStyle: CSSProperties = { + backgroundImage: + "linear-gradient(45deg, #d1d5db 25%, transparent 25%), linear-gradient(-45deg, #d1d5db 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #d1d5db 75%), linear-gradient(-45deg, transparent 75%, #d1d5db 75%)", + backgroundSize: "8px 8px", + backgroundPosition: "0 0, 0 4px, 4px -4px, -4px 0px", +}; + function ColorDoc() { + const swatchRefs = useRef>({}); + const [resolved, setResolved] = useState>({}); + + useLayoutEffect(() => { + const next: Record = {}; + for (const section of colorSections) { + for (const item of section.items) { + const el = swatchRefs.current[item.varName]; + if (!el) continue; + next[item.varName] = getComputedStyle(el).backgroundColor; + } + } + setResolved(next); + }, []); + return ( -
- {colorGroups.map(({ label, colors }) => ( -
-

- {label} -

-
- {colors.map(({ name, value }) => ( -
+
+
+

Colors

+

+ 스와치는 tokens.css{" "} + @theme 변수로만 칠한 뒤, + 아래 hex/rgb는 브라우저에서 계산된 값입니다. +

+
+ + {colorSections.map((section) => ( +
+
+

{section.title}

+ {section.description ? ( +

+ {section.description} +

+ ) : null} +
+
+ 토큰 + 스와치 + 계산 색 +
+
    + {section.items.map((item) => ( +
  • +
    + + {item.varName} + + {item.note ? ( +

    + {item.note} +

    + ) : null} +
    { + swatchRefs.current[item.varName] = el; + }} + className="h-full w-full" + style={{ backgroundColor: `var(${item.varName})` }} />
    - {name} - {value} -
    + + {resolved[item.varName] ?? "—"} + +
  • ))} -
-
+ + ))}
); @@ -102,7 +152,15 @@ function ColorDoc() { const meta: Meta = { title: "Design Tokens/Colors", component: ColorDoc, - parameters: { layout: "fullscreen" }, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: + "`tokens.css`에 정의된 색 토큰만 표시합니다. Primary·Surface·Text는 여기가 단일 출처이며, 스토리북 값은 런타임 계산 결과입니다.", + }, + }, + }, }; export default meta; diff --git a/src/stories/Typography.stories.tsx b/src/stories/Typography.stories.tsx index ba32b9b2..26fd4b6f 100644 --- a/src/stories/Typography.stories.tsx +++ b/src/stories/Typography.stories.tsx @@ -1,93 +1,295 @@ +import { useLayoutEffect, useRef, useState } from "react"; import type { Meta, StoryObj } from "@storybook/react"; +/** `tokens.css` 타이포 블록과 동일 순서·이름 — 값은 런타임에 `getComputedStyle`으로 표시 */ const fontTokens = [ { className: "font-hero", label: "Hero", - size: "54px", - weight: "700", - lineHeight: "140%", + cssVars: ["--type-hero-size", "--type-hero-weight", "--type-hero-leading"], }, { className: "font-heading1", label: "Heading 1", - size: "30px", - weight: "600", - lineHeight: "130%", + cssVars: [ + "--type-heading1-size", + "--type-heading1-weight", + "--type-heading1-leading", + ], }, { className: "font-heading2", label: "Heading 2", - size: "28px", - weight: "600", - lineHeight: "130%", + cssVars: [ + "--type-heading2-size", + "--type-heading2-weight", + "--type-heading2-leading", + ], }, { className: "font-heading3", label: "Heading 3", - size: "24px", - weight: "500", - lineHeight: "130%", + cssVars: [ + "--type-heading3-size", + "--type-heading3-weight", + "--type-heading3-leading", + ], }, { className: "font-heading4", label: "Heading 4", - size: "18px", - weight: "400", - lineHeight: "130%", + cssVars: [ + "--type-heading4-size", + "--type-heading4-weight", + "--type-heading4-leading", + ], }, { className: "font-body1", label: "Body 1", - size: "16px", - weight: "500", - lineHeight: "140%", + cssVars: [ + "--type-body1-size", + "--type-body1-weight", + "--type-body1-leading", + ], }, { className: "font-body2", label: "Body 2", - size: "14px", - weight: "500", - lineHeight: "140%", + cssVars: [ + "--type-body2-size", + "--type-body2-weight", + "--type-body2-leading", + ], + }, + { + className: "font-caption", + label: "Caption", + cssVars: [ + "--type-caption-size", + "--type-caption-weight", + "--type-caption-leading", + ], }, { className: "font-label", label: "Label", - size: "14px", - weight: "600", - lineHeight: "140%", + cssVars: [ + "--type-label-size", + "--type-label-weight", + "--type-label-leading", + ], }, +] as const; + +type TFontMetrics = { + fontSize: string; + fontWeight: string; + lineHeight: string; +}; + +const textColorTokens = [ { - className: "font-caption", - label: "Caption", - size: "12px", - weight: "500", - lineHeight: "130%", + varName: "--color-text-100", + label: "100", + note: "가장 밝음 · disabled", + }, + { + varName: "--color-text-200", + label: "200", + note: "muted · placeholder", + }, + { + varName: "--color-text-300", + label: "300", + note: "body", }, -]; + { + varName: "--color-text-400", + label: "400", + note: "가장 진함 · title", + }, +] as const; + +const textRoleAliases = [ + { varName: "--color-text-title", mapsTo: "--color-text-400" }, + { varName: "--color-text-body", mapsTo: "--color-text-300" }, + { varName: "--color-text-muted", mapsTo: "--color-text-200" }, + { varName: "--color-text-placeholder", mapsTo: "--color-text-200" }, + { varName: "--color-text-disabled", mapsTo: "--color-text-100" }, +] as const; function TypographyDoc() { + const sampleRefs = useRef>({}); + const colorRefs = useRef>({}); + const [metrics, setMetrics] = useState>({}); + const [colors, setColors] = useState>({}); + + useLayoutEffect(() => { + const nextMetrics: Record = {}; + for (const row of fontTokens) { + const el = sampleRefs.current[row.className]; + if (!el) continue; + const cs = getComputedStyle(el); + nextMetrics[row.className] = { + fontSize: cs.fontSize, + fontWeight: cs.fontWeight, + lineHeight: cs.lineHeight, + }; + } + setMetrics(nextMetrics); + + const nextColors: Record = {}; + for (const row of textColorTokens) { + const el = colorRefs.current[row.varName]; + if (!el) continue; + nextColors[row.varName] = getComputedStyle(el).color; + } + for (const row of textRoleAliases) { + const el = colorRefs.current[row.varName]; + if (!el) continue; + nextColors[row.varName] = getComputedStyle(el).color; + } + setColors(nextColors); + }, []); + return ( -
-
- Token - Sample - Size - Weight - Leading -
- {fontTokens.map(({ className, label, size, weight, lineHeight }) => ( -
- .{className} - {label} — 가나다라마바사 - {size} - {weight} - {lineHeight} +
+
+

Typography

+

+ 유틸 클래스는 utilities.css + , 값은 tokens.css{" "} + @theme의{" "} + --type-*만 참조합니다. +

+
+ +
+

타이포 스케일

+
+ 스타일 + CSS 변수 + 샘플 + Size + Weight + Leading +
+ {fontTokens.map((row) => { + const m = metrics[row.className]; + return ( +
+
+ {row.label} + + .{row.className} + +
+
    + {row.cssVars.map((v) => ( +
  • {v}
  • + ))} +
+ { + sampleRefs.current[row.className] = el; + }} + className={row.className} + > + {row.label} — 가나다라마바사 + + + {m?.fontSize ?? "—"} + + + {m?.fontWeight ?? "—"} + + + {m?.lineHeight ?? "—"} + +
+ ); + })} +
+ +
+

텍스트 색 (밝기 순)

+

+ --color-text-100 ···{" "} + --color-text-400 +

+
+ 토큰 + 샘플 + 계산 색
- ))} + {textColorTokens.map((row) => ( +
+
+ + {row.varName} + +

{row.note}

+
+ { + colorRefs.current[row.varName] = el; + }} + className="font-body1" + style={{ color: `var(${row.varName})` }} + > + 본문 샘플 · {row.label} + + + {colors[row.varName] ?? "—"} + +
+ ))} +
+ +
+

텍스트 역할 별칭

+

+ 아래 토큰은 위 밝기 스케일을 가리킵니다. +

+
+ 별칭 + + 샘플 + 계산 색 +
+ {textRoleAliases.map((row) => ( +
+ + {row.varName} + + + {row.mapsTo} + + { + colorRefs.current[row.varName] = el; + }} + className="font-body2" + style={{ color: `var(${row.varName})` }} + > + 역할 샘플 텍스트 + + + {colors[row.varName] ?? "—"} + +
+ ))} +
); } @@ -95,7 +297,15 @@ function TypographyDoc() { const meta: Meta = { title: "Design Tokens/Typography", component: TypographyDoc, - parameters: { layout: "fullscreen" }, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: + "타이포는 `tokens.css`의 `--type-*`와 `utilities.css`의 `.font-*`로 정리되어 있습니다. 표의 Size·Weight·Leading·색은 브라우저에서 계산된 값입니다.", + }, + }, + }, }; export default meta; diff --git a/src/styles/print.css b/src/styles/print.css index 9f777d63..4e6cb9db 100644 --- a/src/styles/print.css +++ b/src/styles/print.css @@ -1,5 +1,10 @@ /* AI 리포트 인쇄 — 드로어 패널 외 모든 요소 숨김 */ @media print { + /* + * !important: 화면용 Tailwind·컴포넌트 스타일이 인쇄 시에도 적용되어 레이아웃이 어긋나는 것을 + * 막기 위해 선언 우선순위를 올림. 인쇄 전용 규칙만 해당하며, 남용 시 수정이 어려워지므로 + * 이 블록 안에서만 사용합니다. + */ body.ai-report-printing > *:not([data-drawer-portal]) { display: none !important; } diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 4941ac7c..dfcf30b9 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -1,68 +1,71 @@ @theme { - --color-logo-1: #2eb4ff; - --color-logo-2: #6088fe; - --color-logo-2-dark: #4d67e5; - - --color-brand-900: #000000; - --color-brand-800: #001f5b; - --color-brand-700: #213e68; - --color-brand-600: #466a9c; - --color-brand-500: #88c1ff; - --color-brand-400: #dce7f2; - --color-brand-300: #f9fafb; - --color-brand-200: #ffffff; - - --color-chart-1: #0a3d91; - --color-chart-2: #0056b3; - --color-chart-3: #1485ff; - --color-chart-4: #00aeef; - --color-chart-5: #4fc3f7; - --color-chart-inactive: #f2f4f6; - - --color-status-red: #ff2a4b; - --color-status-blue: #0084fe; - --color-status-green: #22c55e; - --color-status-yellow: #facc15; - - --color-text-main: #212121; - --color-text-auth-sub: #546171; - --color-text-sub: #8b8b8f; - --color-text-placeholder: #c3c3c3; - --color-text-disabled: #b0b8c1; - --color-bg-disabled: #e5e8eb; - --color-bg-surface: #f6f6f6; - - /* Landing (legacy 디자인 컬러를 토큰화) */ - --color-landing-surface: #f5f5f7; - --color-landing-section: #f8f9fc; - --color-landing-pricing: #fafafa; - --color-landing-dark: #1e1e1e; - --color-footer-bg: #121212; - --color-footer-border: #2a2a2a; - --color-landing-muted: #6e6e73; - - --color-social-kakao: #fee500; - --color-social-naver: #03c75a; - --color-social-google: #ffffff; - --color-social-text-kakao: #3a1d1d; - - --height-input: 56px; - --height-button-big: 55px; - --height-button-small: 40px; - - --radius-component-sm: 0.5rem; - --radius-component-md: 1rem; - --radius-component-lg: 1.5rem; - - --padding-none: 0; - --padding-sm: 1rem; - --padding-md: 1.5rem; - --padding-lg: 2rem; - - --max-width-sm: 384px; - --max-width-md: 512px; - --max-width-lg: 672px; - --max-width-xl: 896px; + + /* PRIMARY — 원시 팔레트 (100 밝음 → 500 진함) */ + --palette-primary-100: #e8effe; /* Blue Tint */ + --palette-primary-200: #b8ccfc; /* Light Blue */ + --palette-primary-300: #7a9bf8; /* Mid Blue */ + --palette-primary-400: #4b74f5; /* CTA Blue */ + --palette-primary-500: #2f5bea; /* Brand Blue */ + + + /* SURFACE */ + --color-surface-100: #ffffff; + --color-surface-200: #f4f6fb; + --color-surface-300: #edf0f8; + --color-surface-400: #dde3f0; + --color-surface-500: #1b2b6b; + + + /* TEXT — 밝기 순 (100 가장 밝음 → 400 가장 진함), 쿨 그레이 */ + --color-text-100: #e4e7ec; + --color-text-200: #9ca3af; + --color-text-300: #374151; + --color-text-400: #111827; + + /* text 역할 기준 고정 값 */ + --color-text-title: var(--color-text-400); + --color-text-body: var(--color-text-300); + --color-text-muted: var(--color-text-200); + --color-text-placeholder: var(--color-text-200); + --color-text-disabled: var(--color-text-100); + + + /* 타이포 스케일 — 유틸(.font-*)은 여기 값만 참조 */ + --type-hero-size: 54px; + --type-hero-weight: 700; + --type-hero-leading: 140%; + + --type-heading1-size: 32px; + --type-heading1-weight: 600; + --type-heading1-leading: 130%; + + --type-heading2-size: 28px; + --type-heading2-weight: 600; + --type-heading2-leading: 130%; + + --type-heading3-size: 24px; + --type-heading3-weight: 500; + --type-heading3-leading: 130%; + + --type-heading4-size: 18px; + --type-heading4-weight: 500; + --type-heading4-leading: 130%; + + --type-body1-size: 16px; + --type-body1-weight: 500; + --type-body1-leading: 140%; + + --type-body2-size: 14px; + --type-body2-weight: 500; + --type-body2-leading: 140%; + + --type-caption-size: 12px; + --type-caption-weight: 500; + --type-caption-leading: 130%; + + --type-label-size: 14px; + --type-label-weight: 600; + --type-label-leading: 140%; /* 애니메이션 이징 */ --ease-smooth: cubic-bezier(0.25, 0.1, 0.25, 1); diff --git a/src/styles/utilities.css b/src/styles/utilities.css index 3bc88efe..372f1fb5 100644 --- a/src/styles/utilities.css +++ b/src/styles/utilities.css @@ -1,49 +1,4 @@ @layer utilities { - .h-input { - height: var(--height-input); - } - .h-button-big { - height: var(--height-button-big); - } - .h-button-small { - height: var(--height-button-small); - } - - .rounded-component-sm { - border-radius: var(--radius-component-sm); - } - .rounded-component-md { - border-radius: var(--radius-component-md); - } - .rounded-component-lg { - border-radius: var(--radius-component-lg); - } - - .max-w-modal-sm { - max-width: var(--max-width-sm); - } - .max-w-modal-md { - max-width: var(--max-width-md); - } - .max-w-modal-lg { - max-width: var(--max-width-lg); - } - .max-w-modal-xl { - max-width: var(--max-width-xl); - } - - .p-modal-none { - padding: var(--padding-none); - } - .p-modal-sm { - padding: var(--padding-sm); - } - .p-modal-md { - padding: var(--padding-md); - } - .p-modal-lg { - padding: var(--padding-lg); - } /* 기본 트랜지션 */ .transition-smooth { @@ -68,60 +23,62 @@ transform: scale(0.98); } + /* 타이포 스케일 */ .font-hero { - font-size: 54px; - font-weight: 700; - line-height: 140%; + font-size: var(--type-hero-size); + font-weight: var(--type-hero-weight); + line-height: var(--type-hero-leading); } .font-heading1 { - font-size: 30px; - font-weight: 600; - line-height: 130%; + font-size: var(--type-heading1-size); + font-weight: var(--type-heading1-weight); + line-height: var(--type-heading1-leading); } .font-heading2 { - font-size: 28px; - font-weight: 600; - line-height: 130%; + font-size: var(--type-heading2-size); + font-weight: var(--type-heading2-weight); + line-height: var(--type-heading2-leading); } .font-heading3 { - font-size: 24px; - font-weight: 500; - line-height: 130%; + font-size: var(--type-heading3-size); + font-weight: var(--type-heading3-weight); + line-height: var(--type-heading3-leading); } .font-heading4 { - font-size: 18px; - font-weight: 400; - line-height: 130%; + font-size: var(--type-heading4-size); + font-weight: var(--type-heading4-weight); + line-height: var(--type-heading4-leading); } .font-body1 { - font-size: 16px; - font-weight: 500; - line-height: 140%; + font-size: var(--type-body1-size); + font-weight: var(--type-body1-weight); + line-height: var(--type-body1-leading); } .font-body2 { - font-size: 14px; - font-weight: 500; - line-height: 140%; + font-size: var(--type-body2-size); + font-weight: var(--type-body2-weight); + line-height: var(--type-body2-leading); } .font-caption { - font-size: 12px; - font-weight: 500; - line-height: 130%; + font-size: var(--type-caption-size); + font-weight: var(--type-caption-weight); + line-height: var(--type-caption-leading); } .font-label { - font-size: 14px; - font-weight: 600; - line-height: 140%; + font-size: var(--type-label-size); + font-weight: var(--type-label-weight); + line-height: var(--type-label-leading); } + /* 그림자 */ .shadow-Soft { box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.1); } @@ -138,6 +95,7 @@ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08); } + /* 체크박스 */ .checkbox { appearance: none; width: 24px; @@ -165,133 +123,6 @@ outline-offset: 2px; } - /* 모달 애니메이션 */ - @keyframes modal-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - @keyframes modal-fade-out { - from { - opacity: 1; - } - to { - opacity: 0; - } - } - - @keyframes modal-scale-in { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } - } - - @keyframes modal-scale-out { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.95); - } - } - - .animate-modal-overlay { - animation: modal-fade-in var(--duration-normal) var(--ease-out); - } - - .animate-modal-overlay-out { - animation: modal-fade-out var(--duration-fast) var(--ease-in) forwards; - } - - .animate-modal-content { - animation: modal-scale-in var(--duration-normal) var(--ease-out); - } - - .animate-modal-content-out { - animation: modal-scale-out var(--duration-fast) var(--ease-in) forwards; - } - - /* 스크롤 애니메이션 */ - @keyframes scroll { - from { - transform: translateX(0); - } - to { - transform: translateX(-50%); - } - } - - .animate-scroll { - animation: scroll 30s linear infinite; - } - - /* 슬라이드 텍스트 진입 애니메이션 */ - @keyframes slide-fade-up { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - .animate-slide-fade-up { - animation: slide-fade-up 350ms var(--ease-out) both; - animation-delay: 100ms; - } - - /* 준비 중 플레이스홀더 — 자식 순차 진입 (인라인 delay 제거) */ - .coming-soon-stagger > * { - animation: slide-fade-up 350ms var(--ease-out) both; - } - .coming-soon-stagger > :nth-child(1) { - animation-delay: 0ms; - } - .coming-soon-stagger > :nth-child(2) { - animation-delay: 50ms; - } - .coming-soon-stagger > :nth-child(3) { - animation-delay: 100ms; - } - .coming-soon-stagger > :nth-child(4) { - animation-delay: 150ms; - } - - /* 그래프 상승 애니메이션 */ - @keyframes graph-up { - from { - transform: scaleY(0); - } - to { - transform: scaleY(1); - } - } - - .animate-graph-up { - animation: graph-up 0.7s ease-out forwards; - transform-origin: bottom; - } - - /* AI 버블 진입 전환 */ - .bubble-transition { - transition-property: transform, opacity; - transition-duration: 300ms; - transition-timing-function: cubic-bezier(0.2, 1.05, 0.4, 1); - } - /* 로딩 애니메이션 */ @keyframes shimmer { to { @@ -318,96 +149,9 @@ animation: shimmer 1.5s infinite linear; } - /* AI 요약 버튼 스파클 pulse 애니메이션 */ - @keyframes sparkle-pulse { - 0%, - 100% { - transform: scale(1); - opacity: 0.7; - } - 50% { - transform: scale(1.2); - opacity: 1; - } - } - - .animate-sparkle-pulse { - animation: sparkle-pulse 2.8s ease-in-out infinite; - } - @media (prefers-reduced-motion: reduce) { - /* 스파클 */ - .animate-sparkle-pulse { + .animate-shimmer::after { animation: none; - opacity: 1; } - - /* 인트로 텍스트 진입 — 즉시 표시 */ - .animate-slide-fade-up { - animation: none; - opacity: 1; - transform: none; - } - - .coming-soon-stagger > * { - animation: none; - opacity: 1; - transform: none; - } - - /* 인트로 캐러셀 — 위치 이동 중단 */ - .animate-scroll { - animation: none; - } - - /* 바 차트 — 즉시 완성 상태로 표시 */ - .animate-graph-up { - animation: none; - transform: scaleY(1); - } - - /* AI 버블 — transform/opacity 전환 제거 */ - .bubble-transition { - transition: none; - } - - /* 모달 — scale 제거, opacity 전환만 유지 */ - .animate-modal-content { - animation: modal-fade-in var(--duration-normal) var(--ease-out); - } - .animate-modal-content-out { - animation: modal-fade-out var(--duration-fast) var(--ease-in) forwards; - } - - /* 토스트 — translateY 제거, opacity 전환만 유지 */ - @keyframes fade-in-up-reduced { - from { - opacity: 0; - transform: translate(-50%, 0); - } - to { - opacity: 1; - transform: translate(-50%, 0); - } - } - .animate-fade-in-up { - animation: fade-in-up-reduced 200ms var(--ease-out) forwards; - } - } - - /* 토스트 애니메이션 */ - @keyframes fade-in-up { - from { - opacity: 0; - transform: translate(-50%, 12px); - } - to { - opacity: 1; - transform: translate(-50%, 0); - } - } - - .animate-fade-in-up { - animation: fade-in-up 0.2s var(--ease-out) both; } } From 4f65eda2f96505e61287fdf15590665e04d737a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Wed, 13 May 2026 15:41:56 +0900 Subject: [PATCH 03/33] =?UTF-8?q?fix:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D/=EB=9E=9C=EB=94=A9=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/icon/common/eye-off.svg | 6 +- src/assets/icon/common/eye.svg | 4 +- src/assets/logo/service-logo/logo.svg | 8 +- src/components/ads/AdDetailContent.tsx | 4 +- .../auth/common/CommonAuthInput.tsx | 4 +- src/components/auth/common/InputActions.tsx | 4 +- src/components/auth/common/PasswordForm.tsx | 4 +- .../auth/flows/signup/EnterEmailStep.tsx | 8 +- .../auth/flows/signup/ProfileSetupStep.tsx | 8 +- .../common/ComingSoonPlaceholder.tsx | 2 +- src/components/common/button/Button.tsx | 20 +-- src/components/common/card/Card.tsx | 4 +- src/components/common/card/StatCard.tsx | 4 +- src/components/common/chart/ChartLegend.tsx | 2 +- .../common/dropdownmenu/DropdownMenu.tsx | 2 +- src/components/common/input/Input.tsx | 16 +- src/components/common/modal/Modal.stories.tsx | 4 +- .../dashboard/charts/BudgetGaugeChart.tsx | 16 +- .../dashboard/charts/TrafficChart.tsx | 12 +- .../dashboard/platform/PlatformRoasTable.tsx | 16 +- src/components/landing/GuideOverviewChart.tsx | 10 +- src/components/landing/GuidePlatform.tsx | 14 +- src/components/landing/GuideTimeline.tsx | 34 ++-- src/components/landing/LandingFAQ.tsx | 10 +- src/components/landing/LandingFeatures.tsx | 74 ++++---- src/components/landing/LandingFooter.tsx | 16 +- src/components/landing/LandingGuide.tsx | 20 +-- src/components/landing/LandingHeader.tsx | 16 +- src/components/landing/LandingHero.tsx | 16 +- src/components/landing/LandingMultiDevice.tsx | 35 ++-- src/components/landing/LandingPricing.tsx | 48 +++-- .../landing/LandingSectionHeader.tsx | 2 +- src/components/setting/PasswordSection.tsx | 18 +- src/constants/landing/overviewChart.ts | 12 +- src/constants/landing/timeline.ts | 6 +- src/pages/auth/Login.tsx | 24 +-- src/pages/auth/Signup.tsx | 18 +- .../dashboard/overview/OverviewAiDrawer.tsx | 4 +- .../overview/OverviewAiReportPanel.tsx | 10 +- .../overview/OverviewBudgetSection.tsx | 2 +- .../overview/OverviewCampaignSnapshotCard.tsx | 6 +- .../overview/OverviewPlatformSection.tsx | 2 +- src/pages/landing/LandingPage.tsx | 6 +- src/stories/Colors.stories.tsx | 12 +- src/styles/tokens.css | 40 +++-- src/styles/utilities.css | 164 ++++++++++++++---- 46 files changed, 453 insertions(+), 314 deletions(-) diff --git a/src/assets/icon/common/eye-off.svg b/src/assets/icon/common/eye-off.svg index 3c6d4458..5a3b7237 100644 --- a/src/assets/icon/common/eye-off.svg +++ b/src/assets/icon/common/eye-off.svg @@ -1,14 +1,14 @@ - + - + - + diff --git a/src/assets/icon/common/eye.svg b/src/assets/icon/common/eye.svg index 5c479ab8..0d3b02ae 100644 --- a/src/assets/icon/common/eye.svg +++ b/src/assets/icon/common/eye.svg @@ -1,9 +1,9 @@ - + - + diff --git a/src/assets/logo/service-logo/logo.svg b/src/assets/logo/service-logo/logo.svg index f2b6747d..7f10139b 100644 --- a/src/assets/logo/service-logo/logo.svg +++ b/src/assets/logo/service-logo/logo.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/src/components/ads/AdDetailContent.tsx b/src/components/ads/AdDetailContent.tsx index 82e2649b..e0f3cd74 100644 --- a/src/components/ads/AdDetailContent.tsx +++ b/src/components/ads/AdDetailContent.tsx @@ -106,7 +106,7 @@ export default function AdDetailContent({
랜딩 URL
-
+
{ad.landingUrl} @@ -115,7 +115,7 @@ export default function AdDetailContent({ e.stopPropagation(); ad.landingUrl && handleCopy(ad.landingUrl); }} - className="shrink-0 text-text-placeholder hover:text-primary-main transition-colors p-1" + className="shrink-0 text-text-placeholder hover:text-primary-500 transition-colors p-1" title="링크 복사" > diff --git a/src/components/auth/common/CommonAuthInput.tsx b/src/components/auth/common/CommonAuthInput.tsx index b5c72326..c94eb0a2 100644 --- a/src/components/auth/common/CommonAuthInput.tsx +++ b/src/components/auth/common/CommonAuthInput.tsx @@ -73,9 +73,9 @@ const CommonAuthInput = React.forwardRef< aria-label={showPassword ? "비밀번호 숨기기" : "비밀번호 보기"} > {showPassword ? ( - + ) : ( - + )} ); diff --git a/src/components/auth/common/InputActions.tsx b/src/components/auth/common/InputActions.tsx index 577526cc..5a416065 100644 --- a/src/components/auth/common/InputActions.tsx +++ b/src/components/auth/common/InputActions.tsx @@ -29,7 +29,7 @@ export default function InputActions({ return (
- {timer && {timer}} + {timer && {timer}} {button && (
diff --git a/src/components/auth/flows/signup/ProfileSetupStep.tsx b/src/components/auth/flows/signup/ProfileSetupStep.tsx index d2a7aef5..7146e309 100644 --- a/src/components/auth/flows/signup/ProfileSetupStep.tsx +++ b/src/components/auth/flows/signup/ProfileSetupStep.tsx @@ -71,7 +71,7 @@ export default function ProfileSetupStep({ password }: IProfileSetupStepProps) { return ( -

+

사용자의 기본 정보를 입력해 주세요

@@ -119,14 +119,14 @@ export default function ProfileSetupStep({ password }: IProfileSetupStepProps) { checked={watch("terms")} readOnly /> - - (필수) + + (필수) 개인정보 수집 및 이용 동의
diff --git a/src/components/common/ComingSoonPlaceholder.tsx b/src/components/common/ComingSoonPlaceholder.tsx index ef9b5336..8425086f 100644 --- a/src/components/common/ComingSoonPlaceholder.tsx +++ b/src/components/common/ComingSoonPlaceholder.tsx @@ -52,7 +52,7 @@ const ComingSoonPlaceholder = memo(function ComingSoonPlaceholder({ )} >
{title && ( -

+

{title}

)} {description && ( -
{description}
+
{description}
)}
{RightElement} diff --git a/src/components/common/card/StatCard.tsx b/src/components/common/card/StatCard.tsx index c4c6ac83..8259575e 100644 --- a/src/components/common/card/StatCard.tsx +++ b/src/components/common/card/StatCard.tsx @@ -71,10 +71,10 @@ const StatCard = memo(function StatCard({ )} {...rest} > -

{title}

+

{title}

diff --git a/src/components/common/chart/ChartLegend.tsx b/src/components/common/chart/ChartLegend.tsx index 53fe2158..3c20f844 100644 --- a/src/components/common/chart/ChartLegend.tsx +++ b/src/components/common/chart/ChartLegend.tsx @@ -24,7 +24,7 @@ const ChartLegend = memo(function ChartLegend({ className={twMerge("w-1.5 h-1.5 rounded-full", item.colorClass)} style={item.color ? { backgroundColor: item.color } : undefined} /> - + {item.label}

diff --git a/src/components/common/dropdownmenu/DropdownMenu.tsx b/src/components/common/dropdownmenu/DropdownMenu.tsx index 2f4f1d31..dd6aa085 100644 --- a/src/components/common/dropdownmenu/DropdownMenu.tsx +++ b/src/components/common/dropdownmenu/DropdownMenu.tsx @@ -94,7 +94,7 @@ export function DropdownMenu({ setOpen(false); }} className={twMerge( - "group flex w-full items-center justify-between rounded-2xl px-5 py-4 text-left font-body2 transition-fast", + "group flex w-full items-center justify-between rounded-2xl px-5 py-4 text-left font-body2 transition-ui-fast", it.active ? "bg-status-blue/10 text-status-blue" : "text-text-main hover:bg-status-blue/5 hover:text-status-blue", diff --git a/src/components/common/input/Input.tsx b/src/components/common/input/Input.tsx index 9fdb02de..bd5b6bdc 100644 --- a/src/components/common/input/Input.tsx +++ b/src/components/common/input/Input.tsx @@ -45,28 +45,28 @@ const Input = forwardRef( {label && ( )}
( aria-live="polite" className={twMerge( "font-caption pl-1 mt-1.5", - error ? "text-status-red" : "text-text-sub", + error ? "text-red-600" : "text-text-body", )} > {helperText} diff --git a/src/components/common/modal/Modal.stories.tsx b/src/components/common/modal/Modal.stories.tsx index b9c06bd0..f5ca10e8 100644 --- a/src/components/common/modal/Modal.stories.tsx +++ b/src/components/common/modal/Modal.stories.tsx @@ -57,7 +57,7 @@ export const Default: TModalStory = {
@@ -267,7 +269,7 @@ const TrafficChart = memo(function TrafficChart() { if (isError && !data) { return ( -
+

실시간 데이터를 불러오지 못했습니다.

잠시 후 다시 시도해 주세요.

diff --git a/src/components/dashboard/platform/PlatformRoasTable.tsx b/src/components/dashboard/platform/PlatformRoasTable.tsx index b528c041..bcb79edc 100644 --- a/src/components/dashboard/platform/PlatformRoasTable.tsx +++ b/src/components/dashboard/platform/PlatformRoasTable.tsx @@ -62,7 +62,7 @@ const PlatformRoasTable = memo(function PlatformRoasTable({
순위 @@ -95,7 +95,7 @@ const PlatformRoasTable = memo(function PlatformRoasTable({ > {/* 순위 */}
- + {item.rank}
@@ -105,14 +105,14 @@ const PlatformRoasTable = memo(function PlatformRoasTable({
{getPlatformLogo(item.provider)}
- + {getDisplayName(item.provider)}
{/* ROAS */}
- + {item.roas.toLocaleString()}%
@@ -121,7 +121,7 @@ const PlatformRoasTable = memo(function PlatformRoasTable({
{item.clickRate !== undefined ? ( <> - + {item.clickRate.toFixed(1)}% {item.ctrDelta !== undefined && ( @@ -139,7 +139,7 @@ const PlatformRoasTable = memo(function PlatformRoasTable({
{item.conversionRate !== undefined ? ( <> - + {item.conversionRate.toFixed(1)}% {item.conversionDelta !== undefined && ( @@ -155,10 +155,10 @@ const PlatformRoasTable = memo(function PlatformRoasTable({ {/* 매출/광고비 */}
- + ₩{item.revenue.toLocaleString()} -
+
광고비 ₩{item.adSpend.toLocaleString()} diff --git a/src/components/landing/GuideOverviewChart.tsx b/src/components/landing/GuideOverviewChart.tsx index 6ea4d3af..d46ad502 100644 --- a/src/components/landing/GuideOverviewChart.tsx +++ b/src/components/landing/GuideOverviewChart.tsx @@ -19,8 +19,8 @@ export default function GuideOverviewChart() { description={ } @@ -31,12 +31,12 @@ export default function GuideOverviewChart() {

+
} >
-
-

+

+

광고 클릭수 추이

diff --git a/src/components/landing/GuidePlatform.tsx b/src/components/landing/GuidePlatform.tsx index d17d5ada..40f0340b 100644 --- a/src/components/landing/GuidePlatform.tsx +++ b/src/components/landing/GuidePlatform.tsx @@ -73,24 +73,24 @@ export default function GuidePlatform() { return (

-
+
{isMenuOpen && ( -
+
{PLATFORMS.map((platform) => { const isSelected = selectedIds.includes(platform.id); @@ -99,10 +99,8 @@ export default function GuidePlatform() { key={platform.id} type="button" onClick={() => togglePlatform(platform.id)} - className={`w-full h-14 px-4 flex items-center justify-start border-b last:border-b-0 transition-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-logo-2/25 focus-visible:ring-inset ${ - isSelected - ? "bg-brand-300/22 border-chart-inactive/40" - : "bg-white hover:bg-brand-300/25 border-chart-inactive/40" + className={`w-full h-14 px-4 flex items-center justify-start border-b last:border-b-0 transition-ui-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/25 focus-visible:ring-inset bg-surface-100 border-surface-400/40 ${ + isSelected ? "" : "hover:bg-surface-100" }`} >
+
{/* Top Navigation */} -
+
- + Day - + Week - + Month
-
+
@@ -58,7 +58,7 @@ export default function GuideTimeline() { @@ -83,20 +83,20 @@ export default function GuideTimeline() {
{/* Chart Area */} -
+
{/* Header (Dates) */} -
+
{LANDING_TIMELINE_COLUMNS.map((c, i) => (
- {c.day} {c.date} + {c.day} {c.date}
))} @@ -108,7 +108,7 @@ export default function GuideTimeline() { {LANDING_TIMELINE_COLUMNS.map((_, i) => (
))} @@ -124,7 +124,7 @@ export default function GuideTimeline() { return (
{/* Left Indicator */} @@ -134,10 +134,10 @@ export default function GuideTimeline() { {/* Text */}
- + {card.title} - + {card.subtitle}
@@ -147,7 +147,7 @@ export default function GuideTimeline() { diff --git a/src/components/landing/LandingFAQ.tsx b/src/components/landing/LandingFAQ.tsx index f85f74c9..83ec9453 100644 --- a/src/components/landing/LandingFAQ.tsx +++ b/src/components/landing/LandingFAQ.tsx @@ -10,7 +10,7 @@ export default function LandingFAQ() {
@@ -20,12 +20,12 @@ export default function LandingFAQ() { />
-
+
{LANDING_FAQ_ITEMS.map(({ q, a }) => (
- - {q} - + + {q} + diff --git a/src/components/landing/LandingFeatures.tsx b/src/components/landing/LandingFeatures.tsx index 592f8e3f..c70ca57e 100644 --- a/src/components/landing/LandingFeatures.tsx +++ b/src/components/landing/LandingFeatures.tsx @@ -28,43 +28,43 @@ function IntegrationGraphic() { viewport={{ once: true, amount: 0.45 }} transition={{ duration: 0.5, ease: "easeOut" }} > - +
- +
- Naver + Naver
-
- +
+
- +
-
+
- + Google Ads
-
- +
+
- +
- +
- Kakao + Kakao
-
- +
+
@@ -73,13 +73,13 @@ function IntegrationGraphic() { function WorkflowGraphic() { return ( -
+
- + AI로 요약하기 - +
다운로드
@@ -158,16 +158,16 @@ function WorkspaceGraphic() { return (
-
+
- You - + You + Team JSON
@@ -177,25 +177,23 @@ function WorkspaceGraphic() {
-
+
{typedText}
{showCursor && ( - | + + | + )}
- +
@@ -228,7 +226,7 @@ export default function LandingFeatures() {
-
+
-

+

{title}

diff --git a/src/components/landing/LandingFooter.tsx b/src/components/landing/LandingFooter.tsx index 8de23428..16f51e67 100644 --- a/src/components/landing/LandingFooter.tsx +++ b/src/components/landing/LandingFooter.tsx @@ -1,19 +1,19 @@ export default function LandingFooter() { return ( -