From 2ca908b95e2a554ec2f77678108c8ec9221638e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EC=A0=9C=EA=B2=BD?= Date: Sat, 8 Aug 2026 20:44:20 +0900 Subject: [PATCH 01/46] =?UTF-8?q?style:=20=ED=94=8C=EB=9E=AB=ED=8F=BC?= =?UTF-8?q?=EB=B3=84=20=EB=B9=84=EA=B5=90=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/platform/PlatformRoasTable.tsx | 237 ++++++++++++------ 1 file changed, 156 insertions(+), 81 deletions(-) diff --git a/src/components/dashboard/platform/PlatformRoasTable.tsx b/src/components/dashboard/platform/PlatformRoasTable.tsx index 78a64f2a..05e8820c 100644 --- a/src/components/dashboard/platform/PlatformRoasTable.tsx +++ b/src/components/dashboard/platform/PlatformRoasTable.tsx @@ -1,4 +1,4 @@ -import { memo } from "react"; +import { memo, useState } from "react"; import type { IPlatformRankingItem, @@ -11,6 +11,8 @@ import { METRIC_REGISTRY as M } from "@/utils/dashboard/metricRegistry"; import { TrendBadge } from "@/components/common/card/StatCard"; +import ChevronDownIcon from "@/assets/icon/chevron/chevron-down.svg?react"; + function toProviderType(provider: string): TProviderType | null { const key = provider.toUpperCase(); if (key in PLATFORM_MAP) return key as TProviderType; @@ -50,20 +52,166 @@ const Delta = memo(function Delta({ value }: { value: number }) { }); export const PLATFORM_ROAS_TABLE_COL = - "grid-cols-[36px_minmax(0,1.45fr)_minmax(5.5rem,7.25rem)_minmax(0,1.25fr)] @2xl:grid-cols-[36px_minmax(0,1.15fr)_minmax(5.5rem,7.25rem)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.28fr)]"; + "mobile:grid-cols-[28px_minmax(0,1fr)_minmax(4.5rem,5.5rem)_20px] grid-cols-[36px_minmax(0,1.45fr)_minmax(5.5rem,7.25rem)_minmax(0,1.28fr)] @2xl:grid-cols-[36px_minmax(0,1.15fr)_minmax(5.5rem,7.25rem)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.28fr)]"; interface IPlatformRoasTableProps { rankings: IPlatformRankingItem[]; } +const PlatformRoasRow = memo(function PlatformRoasRow({ + item, +}: { + item: IPlatformRankingItem; +}) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( + <> +
+ {/* 순위 */} +
+ + {item.rank} + +
+ + {/* 플랫폼 이름 */} +
+
+ {getPlatformLogo(item.provider)} +
+ + {getDisplayName(item.provider)} + +
+ + {/* ROAS */} +
+ + {M.roas.format(item.roas)} + +
+ + {/* 클릭수 */} +
+ {item.clicks !== undefined ? ( + <> + + {M.clicks.format(item.clicks)} + + {item.clickDelta !== undefined && ( +
+ +
+ )} + + ) : ( + - + )} +
+ + {/* CVR */} +
+ {item.conversionRate !== undefined ? ( + <> + + {M.conversion.format(item.conversionRate)} + + {item.conversionDelta !== undefined && ( +
+ +
+ )} + + ) : ( + - + )} +
+ + {/* 매출/광고비 - 모바일에서 숨김 */} +
+ + {M.revenue.format(item.revenue)} + +
+ {M.adSpend.label} + + {M.adSpend.format(item.adSpend)} + +
+
+ + {/* 펼치기/접기 버튼 - 모바일 전용 */} + +
+ + {/* 펼쳐진 상세 정보 - 모바일 전용 */} + {isExpanded && ( +
+
+
+ + {M.revenue.label} + + + {M.revenue.format(item.revenue)} + +
+
+ + {M.adSpend.label} + + + {M.adSpend.format(item.adSpend)} + +
+ {item.clicks !== undefined && ( +
+ + {M.clicks.label} + + + {M.clicks.format(item.clicks)} + +
+ )} + {item.conversionRate !== undefined && ( +
+ + {M.conversion.label} + + + {M.conversion.format(item.conversionRate)} + +
+ )} +
+
+ )} + + ); +}); + const PlatformRoasTable = memo(function PlatformRoasTable({ rankings, }: IPlatformRoasTableProps) { return (
+ {/* 헤더 */}
순위 @@ -78,9 +226,12 @@ const PlatformRoasTable = memo(function PlatformRoasTable({ {M.conversion.label} - + {/* 매출/광고비 헤더 - 모바일에서 숨김 */} + {M.revenue.label} / {M.adSpend.label} + {/* 펼치기 헤더 placeholder - 모바일 전용 */} +
{rankings.length === 0 && ( @@ -90,83 +241,7 @@ const PlatformRoasTable = memo(function PlatformRoasTable({ )}
{rankings.map((item) => ( -
- {/* 순위 */} -
- - {item.rank} - -
- - {/* 플랫폼 이름 */} -
-
- {getPlatformLogo(item.provider)} -
- - {getDisplayName(item.provider)} - -
- - {/* ROAS */} -
- - {M.roas.format(item.roas)} - -
- - {/* 클릭수 */} -
- {item.clicks !== undefined ? ( - <> - - {M.clicks.format(item.clicks)} - - {item.clickDelta !== undefined && ( -
- -
- )} - - ) : ( - - - )} -
- - {/* CVR */} -
- {item.conversionRate !== undefined ? ( - <> - - {M.conversion.format(item.conversionRate)} - - {item.conversionDelta !== undefined && ( -
- -
- )} - - ) : ( - - - )} -
- - {/* 매출/광고비 */} -
- - {M.revenue.format(item.revenue)} - -
- {M.adSpend.label} - - {M.adSpend.format(item.adSpend)} - -
-
-
+ ))}
From 325cbae080750748d75c8322cef1292d765cbb9e Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Sat, 8 Aug 2026 20:58:37 +0900 Subject: [PATCH 02/46] =?UTF-8?q?fix:=20=EB=9E=9C=EB=94=A9=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=82=A0=EC=A7=9C=20=EA=B7=A0=EB=93=B1?= =?UTF-8?q?=EB=B6=84=EB=B0=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=B9=88=EA=B3=B5=EA=B0=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/landing/GuideTimeline.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/landing/GuideTimeline.tsx b/src/components/landing/GuideTimeline.tsx index 2cd0b2af..370a91a5 100644 --- a/src/components/landing/GuideTimeline.tsx +++ b/src/components/landing/GuideTimeline.tsx @@ -1,9 +1,13 @@ +import { useRef } from "react"; + import { LANDING_TIMELINE_BARS, LANDING_TIMELINE_COLUMNS, } from "@/constants/landing/timeline"; import { TIMELINE_COL_WIDTH } from "@/constants/timeline/layout"; +import { useContainerWidth } from "@/hooks/timeline/useContainerWidth"; + import TimelineAxis from "@/components/timeline/TimelineAxis"; import TimelineBar from "@/components/timeline/TimelineBar"; import TimelineGrid from "@/components/timeline/TimelineGrid"; @@ -13,7 +17,14 @@ import SortIcon from "@/assets/icon/timeline/sort.svg?react"; export default function GuideTimeline() { //랜딩 가이드 column 폭이 좁아서 실제 타임라인보다는 작게 - const colWidth = Math.min(TIMELINE_COL_WIDTH, 72); + const scrollRef = useRef(null); + const containerWidth = useContainerWidth(scrollRef); + + const columnCount = LANDING_TIMELINE_COLUMNS.length; + const colWidth = + columnCount > 0 && containerWidth > 0 + ? Math.max(TIMELINE_COL_WIDTH, containerWidth / columnCount) + : TIMELINE_COL_WIDTH; const totalWidth = LANDING_TIMELINE_COLUMNS.length * colWidth; const rowCount = Math.max(...LANDING_TIMELINE_BARS.map((bar) => bar.row), 1); @@ -64,7 +75,10 @@ export default function GuideTimeline() {
-
+
Date: Sat, 8 Aug 2026 21:10:04 +0900 Subject: [PATCH 03/46] =?UTF-8?q?feat:=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=98=ED=94=BD=20=EC=B0=A8=ED=8A=B8=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=9A=A9=20id=C2=B7=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20items=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/platformTrafficChartDownload.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/components/dashboard/platform/platformTrafficChartDownload.ts diff --git a/src/components/dashboard/platform/platformTrafficChartDownload.ts b/src/components/dashboard/platform/platformTrafficChartDownload.ts new file mode 100644 index 00000000..17867534 --- /dev/null +++ b/src/components/dashboard/platform/platformTrafficChartDownload.ts @@ -0,0 +1,47 @@ +import { + downloadChartCsv, + downloadChartPng, + downloadChartSvg, +} from "@/utils/dashboard/downloadChart"; + +/** 개별 플랫폼 실시간 트래픽 */ +export const PLATFORM_TRAFFIC_CHART_ID = "platform-traffic-chart"; +export const PLATFORM_TRAFFIC_CONTAINER_ID = `${PLATFORM_TRAFFIC_CHART_ID}-container`; + +/** 전체보기 실시간 클릭수 비교 */ +export const PLATFORM_ALL_TRAFFIC_CHART_ID = "platform-all-traffic-chart"; +export const PLATFORM_ALL_TRAFFIC_CONTAINER_ID = `${PLATFORM_ALL_TRAFFIC_CHART_ID}-container`; + +function todayStamp() { + return new Date().toISOString().slice(0, 10); +} + +/** DropdownMenu items — 통합 DOWNLOAD_ITEMS와 동일 역할 */ +export function getTrafficChartDownloadItems( + chartId: string, + containerId: string, + filename: string, +) { + return [ + { + label: "PNG 저장", + onClick: () => downloadChartPng(chartId, filename), + }, + { + label: "SVG 저장", + onClick: () => downloadChartSvg(containerId, filename), + }, + { + label: "CSV 다운로드", + onClick: () => downloadChartCsv(chartId), + }, + ]; +} + +export function getPlatformTrafficFilename(platform: string) { + return `platform-traffic-${platform}-${todayStamp()}`; +} + +export function getPlatformAllTrafficFilename() { + return `platform-all-traffic-${todayStamp()}`; +} From 7923cec20360225c5592913a7f19996da4dfdb8a Mon Sep 17 00:00:00 2001 From: YermIm Date: Sat, 8 Aug 2026 21:16:14 +0900 Subject: [PATCH 04/46] =?UTF-8?q?feat:=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=98=ED=94=BD=20=EC=B0=A8=ED=8A=B8=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=9A=A9=20id=C2=B7=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20items=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ficChartDownload.ts => platformTrafficChartDownload.config.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/dashboard/platform/{platformTrafficChartDownload.ts => platformTrafficChartDownload.config.ts} (100%) diff --git a/src/components/dashboard/platform/platformTrafficChartDownload.ts b/src/components/dashboard/platform/platformTrafficChartDownload.config.ts similarity index 100% rename from src/components/dashboard/platform/platformTrafficChartDownload.ts rename to src/components/dashboard/platform/platformTrafficChartDownload.config.ts From 4a55ce7a91f89bd2c1f507d8a3beaa7bcf1c2b87 Mon Sep 17 00:00:00 2001 From: YermIm Date: Sat, 8 Aug 2026 21:19:18 +0900 Subject: [PATCH 05/46] =?UTF-8?q?feat:=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=98=ED=94=BD=20=EC=B0=A8=ED=8A=B8=20=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EB=A9=94=EB=89=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/PlatformTrafficChartDownload.tsx | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/components/dashboard/platform/PlatformTrafficChartDownload.tsx diff --git a/src/components/dashboard/platform/PlatformTrafficChartDownload.tsx b/src/components/dashboard/platform/PlatformTrafficChartDownload.tsx new file mode 100644 index 00000000..bce655d3 --- /dev/null +++ b/src/components/dashboard/platform/PlatformTrafficChartDownload.tsx @@ -0,0 +1,52 @@ +import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu"; +import { + getPlatformAllTrafficFilename, + getPlatformTrafficFilename, + getTrafficChartDownloadItems, + PLATFORM_ALL_TRAFFIC_CHART_ID, + PLATFORM_ALL_TRAFFIC_CONTAINER_ID, + PLATFORM_TRAFFIC_CHART_ID, + PLATFORM_TRAFFIC_CONTAINER_ID, +} from "@/components/dashboard/platform/platformTrafficChartDownload.config"; + +import MoreIcon from "@/assets/icon/common/more.svg?react"; + +type TPlatformTrafficChartDownloadProps = { + /** 개별 보기: 플랫폼 코드 (GOOGLE 등). 없으면 전체보기 */ + platform?: string; +}; + +export default function PlatformTrafficChartDownload({ + platform, +}: TPlatformTrafficChartDownloadProps) { + const chartId = platform + ? PLATFORM_TRAFFIC_CHART_ID + : PLATFORM_ALL_TRAFFIC_CHART_ID; + const containerId = platform + ? PLATFORM_TRAFFIC_CONTAINER_ID + : PLATFORM_ALL_TRAFFIC_CONTAINER_ID; + const filename = platform + ? getPlatformTrafficFilename(platform) + : getPlatformAllTrafficFilename(); + + return ( + +