diff --git a/src/components/common/dropdownmenu/DropdownMenu.tsx b/src/components/common/dropdownmenu/DropdownMenu.tsx
index 20a815fd..8e196e53 100644
--- a/src/components/common/dropdownmenu/DropdownMenu.tsx
+++ b/src/components/common/dropdownmenu/DropdownMenu.tsx
@@ -77,6 +77,7 @@ export function DropdownMenu({
menuClassName,
fullWidth = false,
placement = "bottom",
+ inFlow = false,
"aria-label": ariaLabel,
onOpenChange,
}: {
@@ -88,6 +89,7 @@ export function DropdownMenu({
fullWidth?: boolean;
/** bottom: 아래 / top: 위 / auto: 공간에 따라 자동 */
placement?: TDropdownPlacement;
+ inFlow?: boolean;
"aria-label"?: string;
onOpenChange?: (open: boolean) => void;
}) {
@@ -114,7 +116,10 @@ export function DropdownMenu({
}, []);
useLayoutEffect(() => {
- if (!open) return;
+ if (!open || inFlow) {
+ setResolvedPlacement("bottom");
+ return;
+ }
if (placement === "bottom") {
setResolvedPlacement("bottom");
@@ -130,13 +135,13 @@ export function DropdownMenu({
if (!el) return;
setResolvedPlacement(resolveAutoPlacement(el, items.length));
- }, [open, placement, items.length]);
+ }, [open, placement, items.length, inFlow]);
useEffect(() => {
onOpenChange?.(open);
}, [open, onOpenChange]);
- const isTopPlacement = resolvedPlacement === "top";
+ const isTopPlacement = !inFlow && resolvedPlacement === "top";
return (
{items.map((it, idx) => (
diff --git a/src/components/timeline/TimelineCreateModal.tsx b/src/components/timeline/TimelineCreateModal.tsx
index 2ce74790..089e9dad 100644
--- a/src/components/timeline/TimelineCreateModal.tsx
+++ b/src/components/timeline/TimelineCreateModal.tsx
@@ -229,7 +229,7 @@ export default function TimelineCreateModal({
aria-pressed={isSelected}
onClick={() => toggleMetric(option.value)}
className={twMerge(
- "inline-flex h-8 items-center rounded-full border px-4 font-body2 transition-colors",
+ "inline-flex h-8 items-center rounded-full border px-4 font-body2 transition-all duration-300 ease-out active:scale-95",
isSelected
? "border-info-blue/40 bg-info-blue/15 text-info-blue"
: "border-text-placeholder/40 bg-surface-200 text-text-muted hover:bg-surface-300",
@@ -260,6 +260,7 @@ export default function TimelineCreateModal({
render={() => (
(
diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx
index be74a429..bc9147d3 100644
--- a/src/components/timeline/TimelinePerformancePanel.tsx
+++ b/src/components/timeline/TimelinePerformancePanel.tsx
@@ -1,5 +1,5 @@
import type { FC, SVGProps } from "react";
-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { twMerge } from "tailwind-merge";
import type { TProviderType } from "@/types/dashboard/provider";
@@ -8,6 +8,7 @@ import type { TTimelineViewUnit } from "@/types/timeline/ui";
import { resolveTimelinePerformanceStatusStyle } from "@/constants/timeline/statusStyle";
import { getTimelineMetricLabel } from "@/utils/timeline/buildTimelineChartSeries";
+import { getPeriodIndexContainingDate } from "@/utils/timeline/period";
import {
canGoToPrevChartPeriod,
sliceDailyTrendByPeriod,
@@ -150,6 +151,12 @@ export default function TimelinePerformancePanel({
const chartMetric = data.metrics[0]?.metric ?? "CLICK";
const chartMetricLabel = getTimelineMetricLabel(chartMetric);
+ useEffect(() => {
+ if (!isOpen) return;
+ setViewUnit("WEEK");
+ setChartPeriodIndex(getPeriodIndexContainingDate("WEEK", data.startDate));
+ }, [isOpen, data.startDate]);
+
const {
periodLabel: chartPeriodLabel,
slicedTrend,
@@ -265,7 +272,7 @@ export default function TimelinePerformancePanel({
성과 지표
{data.metrics.map((metric) => (
-
+
{metric.label}
))}
@@ -275,56 +282,88 @@ export default function TimelinePerformancePanel({
{/* AI 요약 */}
-
+
{aiState === "idle" && (
-
{/* KPI — 선택한 지표 수만큼 가로 균등 분할 */}
-
+
{data.metrics.map((metric) => (
{metric.label}
-
+
{formatMetricValue(metric.value, metric.unit)}
{metric.changeRate !== undefined && (
diff --git a/src/constants/timeline/layout.ts b/src/constants/timeline/layout.ts
index 711b91bc..1a21f267 100644
--- a/src/constants/timeline/layout.ts
+++ b/src/constants/timeline/layout.ts
@@ -2,7 +2,7 @@
// 열 최소 너비. 컨테이너가 더 넓으면 균등 분배로 커짐
export const TIMELINE_COL_WIDTH = 92;
-export const TIMELINE_ROW_HEIGHT = 104;
+export const TIMELINE_ROW_HEIGHT = 92;
export const TIMELINE_ROW_OFFSET = 24;
export const TIMELINE_AXIS_HEIGHT = 56;
export const TIMELINE_BAR_HEIGHT = 80;
diff --git a/src/utils/timeline/period.ts b/src/utils/timeline/period.ts
index 5577903b..e0e00293 100644
--- a/src/utils/timeline/period.ts
+++ b/src/utils/timeline/period.ts
@@ -113,3 +113,35 @@ export function resolveVisiblePeriod(
periodIndex === 0 ? formatMonthLabel(start) : formatMonthLabel(start),
};
}
+
+export function getPeriodIndexContainingDate(
+ viewUnit: TTimelineViewUnit,
+ targetIso: string,
+ today = new Date(),
+): number {
+ const target = startOfDay(parseIsoDate(targetIso));
+ const normalizedToday = startOfDay(today);
+ const dayMs = 24 * 60 * 60 * 1000;
+
+ if (viewUnit === "DAY") {
+ return Math.max(
+ 0,
+ Math.round((normalizedToday.getTime() - target.getTime()) / dayMs),
+ );
+ }
+ if (viewUnit === "WEEK") {
+ const todayWeekStart = getWeekStart(normalizedToday);
+ const targetWeekStart = getWeekStart(target);
+ return Math.max(
+ 0,
+ Math.round(
+ (todayWeekStart.getTime() - targetWeekStart.getTime()) / (7 * dayMs),
+ ),
+ );
+ }
+
+ const monthDiff =
+ (normalizedToday.getFullYear() - target.getFullYear()) * 12 +
+ (normalizedToday.getMonth() - target.getMonth());
+ return Math.max(0, monthDiff);
+}