diff --git a/.cursor/rules/private-always.mdc b/.cursor/rules/private-always.mdc new file mode 100644 index 00000000..081681eb --- /dev/null +++ b/.cursor/rules/private-always.mdc @@ -0,0 +1,117 @@ +--- +description: Personal always-on rules + project context (local only) +alwaysApply: true +--- + +# WhereYouAd Frontend — Cursor 규칙 + +**로컬 전용.** 팀 규칙·레포와 **동기화하지 않음** (본인 Cursor에만 적용). + +## 강제 규칙 (에이전트 최우선) + +1. **색**: `tokens.css`의 `@theme` 토큰만. 토큰 **이름 변경·임의 색 추가 금지**. +2. **컴포넌트/페이지**: `var(--*)`·인라인 hex **금지** → `bg-*` `text-*` `border-*` 유틸만. +3. **SVG**: `fill`/`stroke="currentColor"`, 색은 `text-*`. +4. **“디자인 변경 금지”** 요청 시: **색상 클래스만** (spacing·타이포·DOM 변경 금지). +5. **상태**: UI → Zustand · 서버 → react-query(스토어에 서버 데이터 금지) · 폼 → RHF(전역에 폼값 금지). +6. **커밋**: 제목 **1줄만** `type: subject`. 커밋 전 `eslint --fix`. +7. **Git**: 요청 없으면 `push`·PR 생성·`commit --amend`·`push --force` 금지. +8. **머지·리베이스**: 원격과 충돌 시 **디자인(UI·레이아웃·간격·타이포·`rounded` 등)은 로컬(현재 작업 브랜치) 우선**. API·타입·버그픽스 등은 합리적으로 합친다. 색·토큰은 규칙 1–2 유지. + +--- + +## `#project-overview` — 프로젝트 개요 + +- **프로젝트명**: WhereYouAd +- **한 줄**: 광고 성과 및 워크스페이스 관리를 지원하는 웹 서비스 **프론트엔드**. +- **성격**: 2026 Capstone · Graduation Project. +- **목표**: 대시보드 UI, 안정적인 상태 관리, 확장 가능한 구조. +- **팀 (Frontend)**: 서제경, 박재선, 임예림. +- **기술 방향**: 빠른 렌더링·효율적 데이터 패칭, **UI State / Server State 분리**, 린트·컨벤션으로 품질 유지. + +### 기술 스택 (요약) + +| 구분 | 내용 | +| ------------ | --------------------------------------------------- | +| Core | React 19, TypeScript, Vite | +| Routing | react-router-dom v7 | +| Client state | zustand | +| Server state | @tanstack/react-query | +| Styling | Tailwind CSS v4, clsx, vite-plugin-svgr | +| Form | react-hook-form, zod, @hookform/resolvers, sonner | +| 품질 | ESLint v9, Prettier, husky, lint-staged, commitlint | + +### 개발 환경 + +- **Node**: v20.x LTS 권장. **패키지 매니저**: pnpm. +- **스크립트**: `pnpm run dev` / `build` / `preview` / `lint` / `prepare`(Husky). + +### 브랜치 전략 + +- `main`: 배포 가능. `develop`: 다음 버전 개발. +- `feature/#[이슈번호]`: 기능 브랜치 (예: `feature/#1`). + +--- + +## `#feature-requirements` — 기능 요구사항 + +- 광고 **성과 지표 대시보드**. +- **워크스페이스** 단위 광고·캠페인 관리. +- 성과 데이터 **시각화·요약 리포트**. +- **인증** 및 **권한** 기반 접근 제어. + +--- + +## `#relevant-codes` — 경로·역할 + +``` +src/ +├── api/ API 통신 +├── assets/ 정적 리소스 +├── components/ UI 컴포넌트 +├── constants/ 상수 +├── layout/ 레이아웃 +├── pages/ 라우트 단위 페이지 +├── routes/ 라우터 +├── store/ Zustand +├── types/ 공통 타입 +└── utils/ 유틸 +``` + +- 스타일 토큰: `src/styles/tokens.css`, `utilities.css`, `base.css` · 엔트리: `src/index.css`. +- 외부 차트 등 라이브러리 옵션 색은 `@theme`과 맞출 것(임의 hex 추가 지양). + +--- + +## `#Current-file-instruction` — 현재 파일·작업 시 + +- 연 파일·커서 위치를 기준으로 수정 범위를 좁힌다. +- 색·디자인 제약은 **상단 강제 규칙 1–4**를 따른다. + +--- + +## `#rules` — 보강·참고 + +### 색상·스타일 + +- **상단 강제 규칙 1–4**와 동일. 토큰 경로는 `#relevant-codes`. + +### 타입 + +- 공통 타입(`Workspace`, `Campaign` 등): `src/types/`. +- 컴포넌트 로컬 타입(props 등): 해당 파일 상단. +- API 응답 타입: `src/types/api.ts` 또는 도메인별 파일. + +### 상태 관리 + +- **상단 강제 규칙 5**와 동일. + +### 커밋 + +- **상단 강제 규칙 6**과 동일. +- 허용 `type`: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `setting`. + +### Git·PR + +- **상단 강제 규칙 7–8**과 동일. +- PR 제목 예: `[Type/#이슈번호] 작업 요약`. 리뷰 라벨: P1 필수 · P2 권장 · P3 제안 · P4 Nit. diff --git a/.gitignore b/.gitignore index 497b745a..9536afa7 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ storybook-static scripts/strip-cursor-attribution.mjs scripts/strip-cursor-attribution 2.mjs scripts/optimize-landing-images.mjs + diff --git a/eslint/import.mjs b/eslint/import.mjs index 923b24c4..de831552 100644 --- a/eslint/import.mjs +++ b/eslint/import.mjs @@ -9,14 +9,8 @@ export default { "import-name": eslintName, }, rules: { - // 패키지명을 camelCase화해서 사용하도록 강제하여 코드의 일관성을 유지합니다. - // 예외적으로 react와 clsx는 별도의 명명 규칙을 따릅니다. - "import-name/all-imports-name": [ - "error", - { - clsx: "cx", - }, - ], + // 패키지명을 camelCase화해서 사용하도록 강제합니다. (옵션 객체는 플러그인 스키마상 최소 1키 필요 — 별도 예외 없음) + "import-name/all-imports-name": "off", // 유사한 항목을 그룹으로 묶어서 import를 정렬합니다. "simple-import-sort/imports": [ "error", diff --git a/package-lock.json b/package-lock.json index b2cec77c..b1082918 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,13 +14,11 @@ "@tanstack/react-query": "^5.90.17", "apexcharts": "^5.7.0", "axios": "^1.13.4", - "clsx": "^2.1.1", "framer-motion": "^12.38.0", "react": "^19.2.0", "react-apexcharts": "^2.0.1", "react-dom": "^19.2.0", "react-hook-form": "^7.71.1", - "react-intersection-observer": "^10.0.3", "react-markdown": "^10.1.0", "react-remove-scroll": "^2.7.2", "react-router-dom": "^7.12.0", @@ -4577,15 +4575,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9503,21 +9492,6 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, - "node_modules/react-intersection-observer": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.3.tgz", - "integrity": "sha512-luICLMbs0zxTO/70Zy7K5jOXkABPEVSAF8T3FdZUlctsrIaPLmx8TZe2SSA+CY2HGWfz2INyNTnp82pxNNsShA==", - "license": "MIT", - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 0d4d6ebb..62b28228 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,11 @@ "@tanstack/react-query": "^5.90.17", "apexcharts": "^5.7.0", "axios": "^1.13.4", - "clsx": "^2.1.1", "framer-motion": "^12.38.0", "react": "^19.2.0", "react-apexcharts": "^2.0.1", "react-dom": "^19.2.0", "react-hook-form": "^7.71.1", - "react-intersection-observer": "^10.0.3", "react-markdown": "^10.1.0", "react-remove-scroll": "^2.7.2", "react-router-dom": "^7.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8342fc3e..a58595e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: axios: specifier: ^1.13.4 version: 1.13.5 - clsx: - specifier: ^2.1.1 - version: 2.1.1 framer-motion: specifier: ^12.38.0 version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -44,9 +41,6 @@ importers: react-hook-form: specifier: ^7.71.1 version: 7.71.1(react@19.2.4) - react-intersection-observer: - specifier: ^10.0.3 - version: 10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.4) @@ -1515,10 +1509,6 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2911,15 +2901,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-intersection-observer@10.0.3: - resolution: {integrity: sha512-luICLMbs0zxTO/70Zy7K5jOXkABPEVSAF8T3FdZUlctsrIaPLmx8TZe2SSA+CY2HGWfz2INyNTnp82pxNNsShA==} - peerDependencies: - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - react-dom: - optional: true - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4932,8 +4913,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clsx@2.1.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -6531,12 +6510,6 @@ snapshots: dependencies: react: 19.2.4 - react-intersection-observer@10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - react: 19.2.4 - optionalDependencies: - react-dom: 19.2.4(react@19.2.4) - react-is@16.13.1: {} react-is@17.0.2: {} diff --git "a/src/assets/logo/service-logo/ai-\354\232\224\354\225\275\353\262\204\355\212\274.svg" "b/src/assets/icon/ai/ai-\354\232\224\354\225\275\353\262\204\355\212\274.svg" similarity index 100% rename from "src/assets/logo/service-logo/ai-\354\232\224\354\225\275\353\262\204\355\212\274.svg" rename to "src/assets/icon/ai/ai-\354\232\224\354\225\275\353\262\204\355\212\274.svg" diff --git a/src/assets/icon/common/camera.svg b/src/assets/icon/common/camera.svg index c86e1e5d..9e332074 100644 --- a/src/assets/icon/common/camera.svg +++ b/src/assets/icon/common/camera.svg @@ -1,4 +1,4 @@ - + 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/icon/sidebar/logout.svg b/src/assets/icon/sidebar/logout.svg deleted file mode 100644 index 5fd33dd8..00000000 --- a/src/assets/icon/sidebar/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - 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/assets/logo/service-logo/symbol-white.svg b/src/assets/logo/service-logo/symbol-white.svg deleted file mode 100644 index 837101bf..00000000 --- a/src/assets/logo/service-logo/symbol-white.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/logo/social-logo/circle/kakao-circle.svg b/src/assets/logo/social-logo/circle/kakao-circle.svg index 063d6330..f1072ee2 100644 --- a/src/assets/logo/social-logo/circle/kakao-circle.svg +++ b/src/assets/logo/social-logo/circle/kakao-circle.svg @@ -1,8 +1,11 @@ - - - - - - - + + + + + diff --git a/src/assets/logo/social-logo/plain/google.svg b/src/assets/logo/social-logo/plain/google.svg index 4f4c16b8..df97a207 100644 --- a/src/assets/logo/social-logo/plain/google.svg +++ b/src/assets/logo/social-logo/plain/google.svg @@ -1,7 +1,6 @@ - - - - - - + + + + + diff --git a/src/assets/logo/social-logo/plain/kakao.svg b/src/assets/logo/social-logo/plain/kakao.svg index 70cbda90..9eace7bf 100644 --- a/src/assets/logo/social-logo/plain/kakao.svg +++ b/src/assets/logo/social-logo/plain/kakao.svg @@ -1,3 +1,3 @@ - + diff --git a/src/components/ads/AdDetailContent.tsx b/src/components/ads/AdDetailContent.tsx index 63452183..ec83c4a8 100644 --- a/src/components/ads/AdDetailContent.tsx +++ b/src/components/ads/AdDetailContent.tsx @@ -6,11 +6,11 @@ import type { IAd } from "@/types/ads/campaign"; import { useControlModal } from "@/hooks/ads/useControlModal"; import Badge from "../common/badge/Badge"; -import ControlBox from "../common/controlbox/ControlBox"; +import Button from "../common/button/Button"; import Modal from "../common/modal/Modal"; import ModalContent from "../common/modal/ModalContent"; -import { createTrackingUrl, updateAdStatus } from "@/api/ads/ads"; +import { createTrackingUrl } from "@/api/ads/ads"; import LinkIcon from "@/assets/icon/common/link.svg?react"; import WarnCircleIcon from "@/assets/icon/common/warn-circle.svg?react"; @@ -21,23 +21,11 @@ export default function AdDetailContent({ ad: IAd; refetchAds: () => void; }) { - const { orgId, projectId } = useParams<{ + const { orgId } = useParams<{ orgId: string; projectId: string; }>(); - const stopControl = useControlModal({ - successMessage: "광고 소재 노출이 중단되었습니다.", - errorMessage: "중단 처리에 실패했습니다.", - onSuccess: refetchAds, - }); - - const resumeControl = useControlModal({ - successMessage: "광고 소재 운영이 재개되었습니다.", - errorMessage: "재개 처리에 실패했습니다.", - onSuccess: refetchAds, - }); - const trackControl = useControlModal({ successMessage: "트래킹 링크가 발급되었습니다.", errorMessage: "트래킹 링크 발급에 실패했습니다.", @@ -69,124 +57,103 @@ export default function AdDetailContent({ }; return ( -
-
- {/* description */} -
- 광고 소재 -

- {ad.description} -

-
- - {/* tags */} -
- 타겟 -
- {tags.length > 0 ? ( - tags.map((tag, idx) => ( - +
+
+
+

+ 광고 소재 +

+

+ {ad.description} +

+
+ +
+

타겟

+
+ {tags.length > 0 ? ( + tags.map((tag, idx) => ( + + #{tag} + + )) + ) : ( + + 설정된 타겟 정보가 없습니다. + + )} +
+
+ +
+

+ 랜딩 URL +

+
+
+ + {ad.landingUrl} + +
+ + +
+
+
- {/* link */} -
- 랜딩 URL -
-
- - {ad.landingUrl} - - +
+
+
+

트래킹

+

+ {isTrackingActive + ? "트래킹 링크가 발급되었습니다" + : "트래킹 링크를 발급받아 성과를 수집해보세요"} +

+

+ {isTrackingActive + ? "발급된 트래킹 링크를 해당 광고 플랫폼에 등록해 주세요." + : "링크를 광고 플랫폼에 등록하면 클릭 성과가 리포트에 반영됩니다."} +

+

+ 중단·재개는 목록 상단의 버튼에서 선택 후 진행할 수 있어요. +

+
-
-
- handleCopy(ad.trackingUrl!) - : trackControl.openModal - } - buttonDisabled={false} - containerClassName="bg-status-blue/7 border-status-blue px-6 py-4" - titleClassName="text-status-blue font-heading4 -mb-1" - descriptionClassName="font-caption text-text-sub" - buttonSize="small" - buttonClassName="bg-status-blue font-body2 tablet:w-full" - /> - - -
-
- {/* 트래킹 링크 발급 */} } + icon={} title="트래킹 링크를 발급하시겠습니까?" description={ <> @@ -219,53 +186,6 @@ export default function AdDetailContent({ variant="primary" /> - - {/* 광고 소재 중단 */} - - } - title="광고 소재를 중단하시겠습니까?" - description="해당 광고의 노출이 즉시 중단됩니다." - buttonText="중단하기" - onConfirm={() => - stopControl.handleConfirm(() => - updateAdStatus(Number(orgId), Number(projectId), ad.id, "PAUSED"), - ) - } - isLoading={stopControl.isLoading} - variant="danger" - /> - - - {/* 광고 소재 재개 */} - - } - title="광고 운영을 재개하시겠습니까?" - description="중단되었던 광고 소재가 다시 플랫폼에 노출되기 시작합니다." - buttonText="시작하기" - onConfirm={() => - resumeControl.handleConfirm(() => - updateAdStatus( - Number(orgId), - Number(projectId), - ad.id, - "ON_GOING", - ), - ) - } - isLoading={resumeControl.isLoading} - variant="primary" - /> -
); } diff --git a/src/components/ads/AdListTable.tsx b/src/components/ads/AdListTable.tsx index 1c89a946..1d8bcb87 100644 --- a/src/components/ads/AdListTable.tsx +++ b/src/components/ads/AdListTable.tsx @@ -1,64 +1,185 @@ -import { useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { twMerge } from "tailwind-merge"; import type { IAd, TPlatform } from "@/types/ads/campaign"; import AdDetailContent from "./AdDetailContent"; -import AdRow from "./AdRow"; +import AdRow, { adListTableHeaderGridClass } from "./AdRow"; interface IAdsListTableProps { ads: IAd[]; refetchAds: () => void; + embedded?: boolean; + selectedAdIds: ReadonlySet; + onToggleAd: (adId: number) => void; + onToggleSelectAllVisible: () => void; } -export default function AdListTable({ ads, refetchAds }: IAdsListTableProps) { +function normalizeProvider(raw: string | undefined): TPlatform | null { + if (!raw) return null; + const p = String(raw).trim().toLowerCase(); + if (p === "kakao" || p === "google" || p === "naver") return p; + return null; +} + +/** `provider`가 문자열이면 전체를, 배열이면 첫 요소를 사용 (문자열에 `[0]` 쓰면 첫 글자만 잡힘) */ +function firstProviderRaw(ad: IAd): string | undefined { + if (ad.providerType) return String(ad.providerType); + const p = ad.provider; + if (p == null) return undefined; + if (Array.isArray(p)) return p[0] != null ? String(p[0]) : undefined; + return String(p); +} + +function providerToPlatform(ad: IAd): TPlatform { + const fromPlatform = ad.platform ? normalizeProvider(ad.platform) : null; + if (fromPlatform) return fromPlatform; + const fromSource = normalizeProvider(firstProviderRaw(ad)); + if (fromSource) return fromSource; + return "naver"; +} + +export default function AdListTable({ + ads, + refetchAds, + embedded = false, + selectedAdIds, + onToggleAd, + onToggleSelectAllVisible, +}: IAdsListTableProps) { const [openAdId, setOpenAdId] = useState(null); + const operableAds = useMemo( + () => ads.filter((a) => a.status !== "OVER"), + [ads], + ); + + const operableIds = useMemo( + () => operableAds.map((a) => a.id), + [operableAds], + ); + + const allSelected = + operableIds.length > 0 && operableIds.every((id) => selectedAdIds.has(id)); + const someSelected = operableIds.some((id) => selectedAdIds.has(id)); + + const selectAllRef = useRef(null); + useEffect(() => { + const el = selectAllRef.current; + if (!el) return; + el.indeterminate = someSelected && !allSelected; + }, [allSelected, someSelected]); + const handleToggle = (id: number) => { setOpenAdId((prev) => (prev === id ? null : id)); }; - return ( -
-

광고 모아보기

-
- {ads && ads.length > 0 ? ( - ads.map((ad) => { - const platform = String(ad.provider).toLowerCase() as TPlatform; - - const isRunning = ad.status === "ON_GOING"; - const isPaused = ad.status === "PAUSED"; - - const runStatus = isRunning ? "running" : "stopped"; - const runStatusText = isRunning - ? "운영 중" - : isPaused - ? "중단" - : "종료"; - - return ( -
- handleToggle(ad.id)} - /> - - {openAdId === ad.id && ( -
- -
- )} -
- ); - }) - ) : ( -
- 연결된 광고 소재가 없습니다. -
- )} + + const titleBlock = ( +
+

+ 이 캠페인의 광고 +

+

광고 모아보기

+
+ ); + + const listInner = ( +
+
+
e.stopPropagation()} + > + +
+
+ 광고 명 +
+
+ 상태 +
+
+ 플랫폼 +
+
+   +
+ + {ads.length > 0 ? ( + ads.map((ad) => { + const platform = providerToPlatform(ad); + + const isRunning = ad.status === "ON_GOING"; + const isPaused = ad.status === "PAUSED"; + + const runStatus = isRunning ? "running" : "stopped"; + const runStatusText = isRunning + ? "운영 중" + : isPaused + ? "중단" + : "종료"; + + const isOpen = openAdId === ad.id; + const selectable = ad.status !== "OVER"; + + return ( +
+ onToggleAd(ad.id)} + onToggle={() => handleToggle(ad.id)} + /> + + {isOpen ? ( +
+ +
+ ) : null} +
+ ); + }) + ) : ( +
+ 연결된 광고 소재가 없습니다. +
+ )} +
+ ); + + if (embedded) { + return ( +
{listInner}
+ ); + } + + return ( +
+ {titleBlock} +
{listInner}
); } diff --git a/src/components/ads/AdRow.tsx b/src/components/ads/AdRow.tsx index b5d647a0..3eb7c4e8 100644 --- a/src/components/ads/AdRow.tsx +++ b/src/components/ads/AdRow.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; +import { twMerge } from "tailwind-merge"; -import type { TPlatform } from "@/types/ads/campaign"; +import type { TPlatform, TStatus } from "@/types/ads/campaign"; import Badge from "../common/badge/Badge"; @@ -9,68 +10,119 @@ import GoogleLogo from "@/assets/logo/social-logo/circle/google-circle.svg?react import KakaoLogo from "@/assets/logo/social-logo/circle/kakao-circle.svg?react"; import NaverLogo from "@/assets/logo/social-logo/circle/naver-circle.svg?react"; +/** 체크박스 | 광고 명 | 상태 | 플랫폼 | 펼침 — AdListTable과 동일 그리드 (CampaignTable 패딩과 통일) */ +const adListTableGridCols = + "grid w-full min-w-0 grid-cols-[2.75rem_minmax(0,1fr)_auto_auto_2.5rem] items-center gap-x-3"; + +export const adListTableHeaderGridClass = `${adListTableGridCols} px-6 py-4 tablet:px-5 tablet:py-3.5`; + +export const adListTableRowGridClass = `${adListTableGridCols} px-6 py-5 tablet:px-5 tablet:py-4`; + +/** @deprecated 행 패딩과 동일. 명시적으로 `adListTableRowGridClass` 사용 권장 */ +export const adListTableGridClass = adListTableRowGridClass; + interface IAdRowProps { name: string; + adStatus: TStatus; runStatus: "running" | "stopped"; runStatusText: string; platform: TPlatform; isOpen: boolean; + isSelected: boolean; + selectable: boolean; + onToggleSelect: () => void; onToggle: () => void; } const LogoMap: Record = { - kakao: , - google: , - naver: , + kakao: , + google: , + naver: , }; export default function AdRow({ name, + adStatus, runStatus, runStatusText, platform, isOpen, + isSelected, + selectable, + onToggleSelect, onToggle, }: IAdRowProps) { + const isPaused = adStatus === "PAUSED"; + return ( - + +
+ + {runStatusText} +
-
- {/* title */} -
- {name} -
+
+ + {LogoMap[platform] ?? ( + ? + )} + +
- {/* chevron */} -
- +
+ +
- +
); } diff --git a/src/components/ads/CampaignInfoCard.tsx b/src/components/ads/CampaignInfoCard.tsx index 466c002a..3e2bd716 100644 --- a/src/components/ads/CampaignInfoCard.tsx +++ b/src/components/ads/CampaignInfoCard.tsx @@ -13,28 +13,26 @@ export default function CampaignInfoCard({ date, className, }: ICampaignInfoCardProps) { - const badgeStyle = "border-none bg-bg-disabled text-text-sub"; + const badgeStyle = "border-none bg-surface-300 text-text-muted"; return (
{/* 예산 현황 */}
- + 예산 현황 - + {budget}
{/* 등록 날짜 */} -
- +
+ 등록 날짜 - - {date} - + {date}
diff --git a/src/components/ads/CampaignPlatformDropdown.tsx b/src/components/ads/CampaignPlatformDropdown.tsx new file mode 100644 index 00000000..43052850 --- /dev/null +++ b/src/components/ads/CampaignPlatformDropdown.tsx @@ -0,0 +1,63 @@ +import { twMerge } from "tailwind-merge"; + +import type { IPlatformCampaign } from "@/types/ads/campaign"; + +import { + DropdownMenu, + type TMenuItem, +} from "@/components/common/dropdownmenu/DropdownMenu"; + +import ChevronIcon from "@/assets/icon/chevron/chevron-up.svg?react"; + +type TProps = { + options: IPlatformCampaign[]; + selected: IPlatformCampaign | null; + onSelect: (option: IPlatformCampaign) => void; + placeholder?: string; +}; + +export default function CampaignPlatformDropdown({ + options, + selected, + onSelect, + placeholder = "캠페인 선택", +}: TProps) { + const items: TMenuItem[] = options.map((opt) => ({ + label: opt.name, + onClick: () => { + onSelect(opt); + }, + active: selected !== null && selected.adCampaignId === opt.adCampaignId, + })); + + const label = selected === null ? placeholder : selected.name; + + return ( + ( +
+ {label} + +
+ )} + /> + ); +} diff --git a/src/components/ads/CampaignRow.tsx b/src/components/ads/CampaignRow.tsx index 61b81616..930273dc 100644 --- a/src/components/ads/CampaignRow.tsx +++ b/src/components/ads/CampaignRow.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import { twMerge } from "tailwind-merge"; import type { TPlatform, TStatus } from "@/types/ads/campaign"; @@ -14,13 +15,17 @@ interface ICampaignRowProps { providers: TPlatform[]; status: TStatus; budgetUsageRate: number; + isSelected: boolean; + onToggleSelect: () => void; onClick?: () => void; } const LogoMap: Record = { - kakao: , - google: , - naver: , + kakao: ( + + ), + google: , + naver: , }; export default function CampaignRow({ @@ -28,15 +33,21 @@ export default function CampaignRow({ providers, status, budgetUsageRate, + isSelected, + onToggleSelect, onClick, }: ICampaignRowProps) { - const isPaused = status == "PAUSED"; + const isPaused = status === "PAUSED"; + return (
  • - {/* 플랫폼 */} -
    - {providers && providers.length > 0 ? ( - providers.map((p, idx) => ( -
    - {LogoMap[p.toLowerCase() as TPlatform]} -
    - )) - ) : ( -
    미연결
    - )} +
    e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > +
    - {/* 캠페인 명 */} -
    +
    {name}
    - {/* 동기화 상태 */} - {/*
    - - {statusText} - -
    */} +
    + {providers && providers.length > 0 ? ( +
    + {providers.map((p, idx) => ( + + {LogoMap[p.toLowerCase() as TPlatform] ?? ( + ? + )} + + ))} +
    + ) : ( +
    미연결
    + )} +
    - {/* 예산 소진 현황 */}
    diff --git a/src/components/ads/CampaignTable.tsx b/src/components/ads/CampaignTable.tsx index bd0b2bac..c60c8a94 100644 --- a/src/components/ads/CampaignTable.tsx +++ b/src/components/ads/CampaignTable.tsx @@ -1,50 +1,98 @@ +import { useEffect, useMemo, useRef } from "react"; +import { twMerge } from "tailwind-merge"; + import type { ICampaign } from "@/types/ads/campaign"; import CampaignRow from "./CampaignRow"; -//import { MOCK_CAMPAIGNS } from "@/pages/ads/list/campaign.mock"; - interface ICampaignTableProps { campaigns: ICampaign[]; onRowClick?: (id: number) => void; + selectedProjectIds: ReadonlySet; + onToggleProject: (projectId: number) => void; + onToggleSelectAllVisible: () => void; + embedded?: boolean; } export default function CampaignTable({ campaigns, onRowClick, + selectedProjectIds, + onToggleProject, + onToggleSelectAllVisible, + embedded = false, }: ICampaignTableProps) { - const visibleCampaigns = campaigns.filter( - (project) => project.status !== "OVER", + const visibleCampaigns = useMemo( + () => campaigns.filter((project) => project.status !== "OVER"), + [campaigns], ); + const visibleIds = useMemo( + () => visibleCampaigns.map((c) => c.projectId), + [visibleCampaigns], + ); + + const allSelected = + visibleIds.length > 0 && + visibleIds.every((id) => selectedProjectIds.has(id)); + const someSelected = visibleIds.some((id) => selectedProjectIds.has(id)); + + const selectAllRef = useRef(null); + useEffect(() => { + const el = selectAllRef.current; + if (!el) return; + el.indeterminate = someSelected && !allSelected; + }, [allSelected, someSelected]); + return ( -
    - {/* Table Header */} -
    -
    - 플랫폼 +
    +
    +
    e.stopPropagation()} + > +
    -
    +
    캠페인 명
    -
    +
    + 플랫폼 +
    +
    예산 소진 현황 예산
    - {/* Row */} -
      +
        {visibleCampaigns.length > 0 ? ( visibleCampaigns.map((project) => ( onToggleProject(project.projectId)} onClick={() => onRowClick?.(project.projectId)} /> )) ) : ( -
      • +
      • 현재 표시할 캠페인이 없습니다.
      • )} diff --git a/src/components/ads/PlatformCard.tsx b/src/components/ads/PlatformCard.tsx index 90fcc8ef..16b472b9 100644 --- a/src/components/ads/PlatformCard.tsx +++ b/src/components/ads/PlatformCard.tsx @@ -14,7 +14,7 @@ interface IPlatformCardProps { } const LogoMap: Record = { - kakao: , + kakao: , google: , naver: , }; 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 c0bf42df..21471023 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/find-email/ShowEmailResultStep.tsx b/src/components/auth/flows/find-email/ShowEmailResultStep.tsx index 03182de9..7abcba9c 100644 --- a/src/components/auth/flows/find-email/ShowEmailResultStep.tsx +++ b/src/components/auth/flows/find-email/ShowEmailResultStep.tsx @@ -13,23 +13,23 @@ export default function ShowEmailResultStep() { return ( -

        +

        입력하신 정보로 WYA에 가입된 계정을 찾았어요

        -
        +
        이메일 ID - + {maskEmail(email)}
        diff --git a/src/components/auth/flows/signup/EnterEmailStep.tsx b/src/components/auth/flows/signup/EnterEmailStep.tsx index ee533db4..fb6c6752 100644 --- a/src/components/auth/flows/signup/EnterEmailStep.tsx +++ b/src/components/auth/flows/signup/EnterEmailStep.tsx @@ -29,7 +29,7 @@ export default function EnterEmailStep({ onNext }: IEnterEmailStepProps) { return ( -

        +

        회원가입을 위해 이메일 인증을 진행할게요

        @@ -48,7 +48,7 @@ export default function EnterEmailStep({ onNext }: IEnterEmailStepProps) {
        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/auth/skeleton/AuthFormSkeleton.tsx b/src/components/auth/skeleton/AuthFormSkeleton.tsx index db668fd8..78621a80 100644 --- a/src/components/auth/skeleton/AuthFormSkeleton.tsx +++ b/src/components/auth/skeleton/AuthFormSkeleton.tsx @@ -11,14 +11,14 @@ export default function AuthFormSkeleton() {
    - - + +
    - +
    - +
    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..33a08a94 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}

    -
    -
    + +

    {footer}

    +
    +
    ); }); diff --git a/src/components/common/alert/Alert.tsx b/src/components/common/alert/Alert.tsx index dce12f01..76798281 100644 --- a/src/components/common/alert/Alert.tsx +++ b/src/components/common/alert/Alert.tsx @@ -17,13 +17,13 @@ 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", - success: "text-status-blue", - warning: "text-status-yellow", - danger: "text-status-red", + info: "text-primary-500", + success: "text-info-blue", + warning: "text-info-yellow", + danger: "text-info-red", }; return ( @@ -35,7 +35,7 @@ export default function Alert({ {icon &&
    {icon}
    }
    {title &&
    {title}
    } -
    {children}
    +
    {children}
    diff --git a/src/components/common/badge/Badge.stories.tsx b/src/components/common/badge/Badge.stories.tsx index 7e106424..300ae7af 100644 --- a/src/components/common/badge/Badge.stories.tsx +++ b/src/components/common/badge/Badge.stories.tsx @@ -5,36 +5,36 @@ import Badge from "./Badge"; const meta: Meta = { title: "Common/Badge", component: Badge, - parameters: { layout: "centered" }, + parameters: { + layout: "centered", + docs: { + description: { + component: + "variant는 **토큰 색 이름**과 맞춤: `infoYellow` · `infoBlue`(기본) · `infoRed` · `surface`. 크기는 **sm 고정** (`h-7` · `font-caption`).", + }, + }, + }, }; export default meta; type TBadgeStory = StoryObj; -export const Syncing: TBadgeStory = { - args: { variant: "syncing", children: "동기화중" }, +export const InfoYellow: TBadgeStory = { + name: "infoYellow", + args: { variant: "infoYellow", children: "예산 주의" }, }; -export const Success: TBadgeStory = { - args: { variant: "success", children: "완료" }, +export const InfoBlue: TBadgeStory = { + name: "infoBlue (기본)", + args: { variant: "infoBlue", children: "운영 중" }, }; -export const Inactive: TBadgeStory = { - args: { variant: "inactive", children: "미동기화" }, +export const InfoRed: TBadgeStory = { + name: "infoRed", + args: { variant: "infoRed", children: "예산 위험" }, }; -export const Running: TBadgeStory = { - args: { variant: "running", children: "운영중" }, -}; - -export const Stopped: TBadgeStory = { - args: { variant: "stopped", children: "중단" }, -}; - -export const SizeSm: TBadgeStory = { - args: { size: "sm", variant: "running", children: "운영중작은" }, -}; - -export const SizeMd: TBadgeStory = { - args: { size: "md", variant: "running", children: "운영중큰" }, +export const Surface: TBadgeStory = { + name: "surface", + args: { variant: "surface", children: "중단" }, }; diff --git a/src/components/common/badge/Badge.tsx b/src/components/common/badge/Badge.tsx index b2d9f526..31b36fce 100644 --- a/src/components/common/badge/Badge.tsx +++ b/src/components/common/badge/Badge.tsx @@ -1,41 +1,30 @@ import type { HTMLAttributes, ReactNode } from "react"; import { twMerge } from "tailwind-merge"; -export type TBadgeVariant = - | "syncing" - | "success" - | "inactive" - | "running" - | "stopped"; - -type TBadgeSize = "sm" | "md"; +/** 토큰 색 기준 (`info-*`, `surface`) */ +export type TBadgeVariant = "infoYellow" | "infoBlue" | "infoRed" | "surface"; interface IBadgeProps extends HTMLAttributes { variant?: TBadgeVariant; - size?: TBadgeSize; leftIcon?: ReactNode; } +const badgeSizeClass = "h-7 px-3 py-1 font-caption"; + export default function Badge({ - variant = "success", - size = "md", + variant = "infoBlue", leftIcon, className, children, ...rest }: IBadgeProps) { - const sizeClasses: Record = { - sm: "h-7 px-3 py-1 font-caption", - md: "h-9 px-4 py-1 font-body2", - }; - const variantClasses: Record = { - syncing: - "bg-status-yellow/15 text-status-yellow border border-status-yellow", - success: "bg-status-green/15 text-status-green border border-status-green", - inactive: "bg-status-red/15 text-status-red border border-status-red/40", - running: "bg-status-blue/15 text-status-blue border border-status-blue/40", - stopped: "bg-brand-300 text-text-sub border border-text-placeholder/40", + infoYellow: + "bg-info-yellow/20 text-info-yellow border border-info-yellow/50", + infoBlue: "bg-info-blue/15 text-info-blue border border-info-blue/40", + infoRed: "bg-info-red/15 text-info-red border border-info-red/40", + /* 중립 배지 */ + surface: "bg-surface-200 text-text-muted border border-text-placeholder/40", }; return ( @@ -43,7 +32,7 @@ export default function Badge({ role="status" className={twMerge( "inline-flex items-center gap-2 rounded-full whitespace-nowrap", - sizeClasses[size], + badgeSizeClass, variantClasses[variant], className, )} diff --git a/src/components/common/button/Button.tsx b/src/components/common/button/Button.tsx index 5b96b782..e51b4a24 100644 --- a/src/components/common/button/Button.tsx +++ b/src/components/common/button/Button.tsx @@ -29,26 +29,25 @@ 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", - small: - "h-button-small px-4 rounded-component-sm font-body1 transition-colors duration-normal ease-out", + big: "h-14 px-6 rounded-2xl font-heading4 transition-button", + small: "h-10 px-4 rounded-lg font-body1 transition-button", }; const variantClasses = { primary: - "bg-chart-3 text-white hover:bg-[#0e6add] disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled", + "bg-primary-400 text-surface-100 hover:bg-primary-500 border border-transparent disabled:bg-surface-200 disabled:text-text-muted disabled:border-surface-400 disabled:hover:bg-surface-200", secondary: - "bg-gray-100 text-brand-900 hover:bg-gray-200 disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled", + "bg-surface-200 text-surface-500 border border-transparent hover:bg-surface-300 disabled:bg-surface-100 disabled:text-text-muted disabled:border-surface-400 disabled:hover:bg-surface-100", outline: - "bg-transparent border border-chart-3 text-chart-3 hover:bg-chart-3/5 disabled:border-gray-300 disabled:text-text-disabled disabled:hover:bg-transparent", + "bg-transparent border border-primary-400 text-primary-400 hover:bg-primary-400/5 disabled:bg-surface-100 disabled:border-surface-400 disabled:text-text-muted disabled:hover:bg-surface-100", danger: - "bg-status-red text-white hover:bg-[#d91632] disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled", + "bg-info-red text-surface-100 hover:opacity-90 border border-transparent disabled:bg-surface-200 disabled:text-text-muted disabled:border-surface-400 disabled:hover:bg-surface-200", dangerSoft: - "bg-status-red/10 text-status-red border border-status-red hover:bg-status-red/20 disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-gray-300 disabled:hover:bg-bg-disabled", + "bg-info-red/10 text-info-red border border-info-red hover:bg-info-red/20 disabled:bg-surface-100 disabled:text-text-muted disabled:border-surface-400 disabled:hover:bg-surface-100", 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", + "bg-linear-to-r from-primary-400 to-primary-500 text-surface-100 border border-transparent hover:opacity-90 shadow-primary-500/30 disabled:bg-surface-200 disabled:text-text-muted disabled:border-surface-400 disabled:shadow-none disabled:hover:opacity-100", 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-surface-400 text-text-auth-sub px-5 rounded-3xl bg-surface-100 font-body2 hover:bg-surface-200 transition-colors duration-200 ease-in-out disabled:text-text-muted disabled:bg-surface-200 disabled:hover:bg-surface-200", custom: "", }; @@ -56,11 +55,11 @@ export default function Button({ , children: ( -

    카드 내용이 들어갑니다.

    +

    카드 내용이 들어갑니다.

    ), }, }; @@ -56,7 +56,7 @@ export const WithDescriptionAndRightElement: TCardStory = { description: "데이터 기준 | 2026.01.06 09:13", RightElement: , children: ( -

    카드 내용이 들어갑니다.

    +

    카드 내용이 들어갑니다.

    ), }, }; diff --git a/src/components/common/card/Card.tsx b/src/components/common/card/Card.tsx index 5e915123..afc6b51e 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 && ( -

    - {title} -

    +

    {title}

    )} {description && ( -
    {description}
    +
    {description}
    )}
    {RightElement} diff --git a/src/components/common/card/InfoCard.stories.tsx b/src/components/common/card/InfoCard.stories.tsx index ace57fe4..c409d93f 100644 --- a/src/components/common/card/InfoCard.stories.tsx +++ b/src/components/common/card/InfoCard.stories.tsx @@ -28,7 +28,7 @@ export const Default: TStory = { args: { title: "제목", children: ( -
    내용
    +
    내용
    ), }, }; diff --git a/src/components/common/card/InfoCard.tsx b/src/components/common/card/InfoCard.tsx index 242a186a..1ca4c9c0 100644 --- a/src/components/common/card/InfoCard.tsx +++ b/src/components/common/card/InfoCard.tsx @@ -15,12 +15,12 @@ export default function InfoCard({ return (
    -

    {title}

    +

    {title}

    {children}
    diff --git a/src/components/common/card/StatCard.stories.tsx b/src/components/common/card/StatCard.stories.tsx index 451a87ee..68cce017 100644 --- a/src/components/common/card/StatCard.stories.tsx +++ b/src/components/common/card/StatCard.stories.tsx @@ -34,7 +34,7 @@ export const WithTrendDown: TStatCardStory = { export const KpiGroup: TStatCardStory = { render: () => ( -
    +
    {[ { title: "클릭수(CTR)", @@ -58,7 +58,7 @@ export const KpiGroup: TStatCardStory = { }, ].map((kpi, index) => ( - {index > 0 &&
    } + {index > 0 &&
    } ))} diff --git a/src/components/common/card/StatCard.tsx b/src/components/common/card/StatCard.tsx index fe274b43..b82cb947 100644 --- a/src/components/common/card/StatCard.tsx +++ b/src/components/common/card/StatCard.tsx @@ -19,8 +19,8 @@ export interface IStatCardProps extends HTMLAttributes { } const trendClasses: Record = { - up: "bg-status-red/[0.08] text-status-red font-bold", - down: "bg-status-blue/[0.08] text-status-blue font-bold", + up: "bg-info-red/[0.08] text-info-red", + down: "bg-info-blue/[0.08] text-info-blue", }; export const TrendBadge = memo(function TrendBadge({ @@ -32,9 +32,9 @@ export const TrendBadge = memo(function TrendBadge({ -

    {title}

    +

    {title}

    diff --git a/src/components/common/chart/ChartLegend.tsx b/src/components/common/chart/ChartLegend.tsx index 53fe2158..7b01d931 100644 --- a/src/components/common/chart/ChartLegend.tsx +++ b/src/components/common/chart/ChartLegend.tsx @@ -24,9 +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} - + {item.label}

    ))}
    diff --git a/src/components/common/controlbox/ControlBox.stories.tsx b/src/components/common/controlbox/ControlBox.stories.tsx deleted file mode 100644 index 0275d23f..00000000 --- a/src/components/common/controlbox/ControlBox.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import ControlBox from "./ControlBox"; - -const meta: Meta = { - title: "Common/ControlBox", - component: ControlBox, - parameters: { layout: "padded" }, -}; - -export default meta; -type TControlBoxStory = StoryObj; - -export const Default: TControlBoxStory = { - args: { - title: "제목", - description: "설명", - buttonText: "버튼", - onButtonClick: () => console.log("click"), - className: "w-full", - }, -}; diff --git a/src/components/common/controlbox/ControlBox.tsx b/src/components/common/controlbox/ControlBox.tsx deleted file mode 100644 index 33a40651..00000000 --- a/src/components/common/controlbox/ControlBox.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import type { HTMLAttributes, ReactNode } from "react"; -import type React from "react"; -import { twMerge } from "tailwind-merge"; - -import Button from "../button/Button"; - -interface IControlBoxProps extends HTMLAttributes { - title: string; - description: string; - buttonText: string; - onButtonClick: () => void; - containerClassName?: string; - titleClassName?: string; - descriptionClassName?: string; - buttonVariant?: React.ComponentProps["variant"]; - buttonSize?: React.ComponentProps["size"]; - buttonClassName?: string; - buttonDisabled?: boolean; - buttonSlot?: ReactNode; - leadingSlot?: ReactNode; - contentClassName?: string; -} - -export default function ControlBox({ - title, - description, - buttonText, - onButtonClick, - className, - containerClassName, - titleClassName, - descriptionClassName, - buttonVariant = "primary", - buttonSize = "small", - buttonClassName, - buttonDisabled, - buttonSlot, - leadingSlot, - contentClassName, - ...rest -}: IControlBoxProps) { - return ( -
    -
    - {leadingSlot ?
    {leadingSlot}
    : null} -
    -

    - {title} -

    -

    - {description} -

    -
    -
    - - {buttonSlot ?? ( - - )} -
    - ); -} diff --git a/src/components/common/drawer/Drawer.tsx b/src/components/common/drawer/Drawer.tsx index c90c68a7..a7bc7a90 100644 --- a/src/components/common/drawer/Drawer.tsx +++ b/src/components/common/drawer/Drawer.tsx @@ -103,7 +103,7 @@ export default function Drawer({ >