void;
@@ -7,11 +9,11 @@ interface PlusIconButtonProps {
export const PlusIconButton = ({ onClickAddButton }: PlusIconButtonProps) => {
return (
-
+
);
};
diff --git a/frontend/src/components/shared/edit-card-wrapper/TrashCanIconButton.tsx b/frontend/src/components/shared/edit-card-wrapper/TrashCanIconButton.tsx
index d327d9f50..ed835da0f 100644
--- a/frontend/src/components/shared/edit-card-wrapper/TrashCanIconButton.tsx
+++ b/frontend/src/components/shared/edit-card-wrapper/TrashCanIconButton.tsx
@@ -1,5 +1,7 @@
import { Trash2 } from 'lucide-react';
+import { Button } from '@/components/shared/shadcn-ui';
+
// 대시보드 편집 용 패널의 우측 위 쓰래기통 버튼
interface TrashCanIconButtonProps {
onClickDeleteButton?: () => void;
@@ -9,11 +11,11 @@ export const TrashCanIconButton = ({
onClickDeleteButton,
}: TrashCanIconButtonProps) => {
return (
-
+
);
};
diff --git a/frontend/src/components/shared/images/images.stories.tsx b/frontend/src/components/shared/images/images.stories.tsx
index d28305211..6fef70d28 100644
--- a/frontend/src/components/shared/images/images.stories.tsx
+++ b/frontend/src/components/shared/images/images.stories.tsx
@@ -24,6 +24,7 @@ const images = [
'up.svg',
'down.svg',
'graph_down.svg',
+ 'graph_same.svg',
'graph_up.svg',
'line_graph.svg',
'bar_graph.svg',
diff --git a/frontend/src/components/shared/index.ts b/frontend/src/components/shared/index.ts
index 474540a90..2ae1715e5 100644
--- a/frontend/src/components/shared/index.ts
+++ b/frontend/src/components/shared/index.ts
@@ -17,3 +17,4 @@ export { MainLayout } from './main-layout';
export { PeriodTag, EditCardWrapper } from './edit-card-wrapper';
export { ButtonGroup } from './button-group';
export { PaginationBar } from './pagenation';
+export { LineChart } from './line-chart';
diff --git a/frontend/src/components/shared/line-chart/LineChart.tsx b/frontend/src/components/shared/line-chart/LineChart.tsx
index c28f24916..41d2a3d51 100644
--- a/frontend/src/components/shared/line-chart/LineChart.tsx
+++ b/frontend/src/components/shared/line-chart/LineChart.tsx
@@ -90,7 +90,7 @@ export const LineChart = ({
const { lineGradientId, backgroundGradientId, titleId, descId } =
useLineChartId();
const {
- svgRect,
+ svgWidth,
adjustedHeight,
xLabelList,
xCoordinate,
@@ -100,6 +100,8 @@ export const LineChart = ({
svgRef,
xAxisRef,
} = useLineChart({
+ viewBoxWidth,
+ viewBoxHeight,
primarySeries,
secondarySeries,
hasXAxis,
@@ -132,17 +134,13 @@ export const LineChart = ({
)}
{showYGuideLine && (
)}
{showXGuideLine && (
-
+
)}
{hasXAxis && (
<>
diff --git a/frontend/src/components/shared/line-chart/XGuideLine.tsx b/frontend/src/components/shared/line-chart/XGuideLine.tsx
index f1066fa3e..2137796ec 100644
--- a/frontend/src/components/shared/line-chart/XGuideLine.tsx
+++ b/frontend/src/components/shared/line-chart/XGuideLine.tsx
@@ -5,16 +5,11 @@ import type { Coordinate } from '@/types/shared';
interface XGuideLineProps {
xCoordinate: Coordinate[];
- svgRect: DOMRect | null;
adjustedHeight: number;
}
export const XGuideLine = memo(
- ({ xCoordinate, svgRect, adjustedHeight }: XGuideLineProps) => {
- if (svgRect === null) {
- return null;
- }
-
+ ({ xCoordinate, adjustedHeight }: XGuideLineProps) => {
const { TICK_HEIGHT, GUIDE_LINE_STROKE_WIDTH, GUIDE_LINE_DASH_ARRAY } =
LINE_CHART;
diff --git a/frontend/src/components/shared/line-chart/YGuideLine.tsx b/frontend/src/components/shared/line-chart/YGuideLine.tsx
index 0952de61e..3c79e1a70 100644
--- a/frontend/src/components/shared/line-chart/YGuideLine.tsx
+++ b/frontend/src/components/shared/line-chart/YGuideLine.tsx
@@ -3,17 +3,13 @@ import { memo } from 'react';
import { LINE_CHART } from '@/constants/shared';
interface YGuideLineProps {
- svgRect: DOMRect | null;
+ svgWidth: number;
adjustedHeight: number;
yGuideLineCount: number;
}
export const YGuideLine = memo(
- ({ svgRect, adjustedHeight, yGuideLineCount }: YGuideLineProps) => {
- if (svgRect === null) {
- return null;
- }
-
+ ({ svgWidth, adjustedHeight, yGuideLineCount }: YGuideLineProps) => {
const { GUIDE_LINE_STROKE_WIDTH, GUIDE_LINE_DASH_ARRAY } = LINE_CHART;
const intervalY = adjustedHeight / yGuideLineCount;
@@ -29,7 +25,7 @@ export const YGuideLine = memo(
);
const pathD = cordinateYGuideLine
- .map(([x, y]) => `M ${x} ${y} h ${svgRect.width}`)
+ .map(([x, y]) => `M ${x} ${y} h ${svgWidth}`)
.join(' ');
return (
diff --git a/frontend/src/constants/dashboard/dashboardMetric.ts b/frontend/src/constants/dashboard/dashboardMetric.ts
index 3ed6d0da0..3cfb12848 100644
--- a/frontend/src/constants/dashboard/dashboardMetric.ts
+++ b/frontend/src/constants/dashboard/dashboardMetric.ts
@@ -56,7 +56,7 @@ const SALES_METRICS = {
},
},
},
- SAELS_TREND: {
+ SALES_TREND: {
title: '매출 추이',
items: {
DAILY_SALES_TREND: {
@@ -191,3 +191,17 @@ export type ExtractCardCodes
= T extends {
}
? U
: never;
+
+/**
+ * 주어진 T가 MetricSection 타입인 경우
+ *
+ * MetricSection의 item들의 cardCodes 타입을 추출하는 유틸리티 타입
+ *
+ * @example
+ * type RealSalesCardCode = ExtractCardCodesFromSection;
+ * 결과: 'SLS_01_01' | 'SLS_01_02' | 'SLS_01_03' | 'SLS_02_01' | 'SLS_02_02' | 'SLS_02_03' | 'SLS_03_01' | 'SLS_03_02' | 'SLS_03_03';
+ *
+ */
+export type ExtractCardCodesFromSection = T extends MetricSection
+ ? ExtractCardCodes
+ : never;
diff --git a/frontend/src/constants/dashboard/index.ts b/frontend/src/constants/dashboard/index.ts
index df7531ff3..a63210d86 100644
--- a/frontend/src/constants/dashboard/index.ts
+++ b/frontend/src/constants/dashboard/index.ts
@@ -8,12 +8,14 @@ export {
DASHBOARD_METRICS,
type MetricSection,
type MetricItem,
+ type ExtractCardCodesFromSection,
type MetricTabs,
type ExtractCardCodes,
} from './dashboardMetric';
-export { GRID_ROW_SIZE, GRID_COL_SIZE } from './dashboardGridSize';
export {
DASHBOARD_METRIC_CARDS,
isMetricCardCode,
type MetricCardCode,
} from './dashboardMetricCards';
+export { METRIC_TREND, type MetricTrend } from './metricTrend';
+export { GRID_ROW_SIZE, GRID_COL_SIZE } from './dashboardGridSize';
diff --git a/frontend/src/constants/dashboard/metricTrend.ts b/frontend/src/constants/dashboard/metricTrend.ts
new file mode 100644
index 000000000..bb876fd8f
--- /dev/null
+++ b/frontend/src/constants/dashboard/metricTrend.ts
@@ -0,0 +1,9 @@
+import type { ValueOf } from '@/utils/shared';
+
+export const METRIC_TREND = {
+ UP: 'up',
+ DOWN: 'down',
+ SAME: 'same',
+} as const;
+
+export type MetricTrend = ValueOf;
diff --git a/frontend/src/constants/sales/dashboard-current-sales/averagePrice.ts b/frontend/src/constants/sales/dashboard-current-sales/averagePrice.ts
new file mode 100644
index 000000000..a3a5eabef
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-current-sales/averagePrice.ts
@@ -0,0 +1,8 @@
+export const AVERAGE_PRICE = {
+ EXAMPLE_AMOUNT: 123000,
+ EXAMPLE_COMPARISON_AMOUNT: 16000,
+ EXAMPLE_HAS_PREVIOUS_DATA: true,
+ METRIC_LABEL: '주문',
+ MIN_CHANGE_RATE: 0,
+ MAX_CHANGE_RATE: 0,
+} as const;
diff --git a/frontend/src/constants/sales/dashboard-current-sales/index.ts b/frontend/src/constants/sales/dashboard-current-sales/index.ts
new file mode 100644
index 000000000..529d36bff
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-current-sales/index.ts
@@ -0,0 +1,3 @@
+export { AVERAGE_PRICE } from './averagePrice';
+export { ORDER_COUNT } from './orderCount';
+export { REAL_SALES } from './realSales';
diff --git a/frontend/src/constants/sales/dashboard-current-sales/orderCount.ts b/frontend/src/constants/sales/dashboard-current-sales/orderCount.ts
new file mode 100644
index 000000000..7fe6f1332
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-current-sales/orderCount.ts
@@ -0,0 +1,8 @@
+export const ORDER_COUNT = {
+ EXAMPLE_AMOUNT: 42,
+ EXAMPLE_CHANGE_RATE: 5,
+ EXAMPLE_HAS_PREVIOUS_DATA: true,
+ METRIC_LABEL: '주문',
+ MIN_CHANGE_RATE: -3,
+ MAX_CHANGE_RATE: 3,
+} as const;
diff --git a/frontend/src/constants/sales/dashboard-current-sales/realSales.ts b/frontend/src/constants/sales/dashboard-current-sales/realSales.ts
new file mode 100644
index 000000000..a5254d168
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-current-sales/realSales.ts
@@ -0,0 +1,8 @@
+export const REAL_SALES = {
+ EXAMPLE_AMOUNT: 256000,
+ EXAMPLE_CHANGE_RATE: 5,
+ EXAMPLE_HAS_PREVIOUS_DATA: true,
+ METRIC_LABEL: '매출',
+ MIN_CHANGE_RATE: -3,
+ MAX_CHANGE_RATE: 3,
+} as const;
diff --git a/frontend/src/constants/sales/dashboard-sales-income/index.ts b/frontend/src/constants/sales/dashboard-sales-income/index.ts
new file mode 100644
index 000000000..29a6b99f6
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-income/index.ts
@@ -0,0 +1,3 @@
+export { ORDER_METHOD } from './orderMethod';
+export { PAYMENT_METHOD } from './paymentMethod';
+export { SALES_TYPE } from './salesType';
diff --git a/frontend/src/constants/sales/dashboard-sales-income/orderMethod.ts b/frontend/src/constants/sales/dashboard-sales-income/orderMethod.ts
new file mode 100644
index 000000000..4d2818f9d
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-income/orderMethod.ts
@@ -0,0 +1,38 @@
+import type { GetIncomeStructureByOrderMethodResponseDto } from '@/types/sales';
+
+export const ORDER_METHOD = {
+ EXAMPLE_TOP_TYPE: '키오스크',
+ EXAMPLE_TOP_SHARE: 50,
+ EXAMPLE_DELTA_SHARE: 4,
+ EXAMPLE_ORDER_METHOD_DATA: [
+ {
+ orderChannel: 'POS',
+ salesAmount: 2371000,
+ orderCount: 26,
+ share: 25,
+ deltaShare: 2.4,
+ },
+ {
+ orderChannel: '키오스크',
+ salesAmount: 5329000,
+ orderCount: 53,
+ share: 25,
+ deltaShare: 4,
+ },
+ {
+ orderChannel: '배달앱',
+ salesAmount: 1986000,
+ orderCount: 19,
+ share: 25,
+ deltaShare: -5.2,
+ },
+ {
+ orderChannel: '기타',
+ salesAmount: 954000,
+ orderCount: 24,
+ share: 25,
+ deltaShare: -1.8,
+ },
+ ] as const satisfies GetIncomeStructureByOrderMethodResponseDto['items'],
+ DOUGHNUT_CHART_TITLE: '주문수단별 매출 관련 도넛 차트',
+} as const;
diff --git a/frontend/src/constants/sales/dashboard-sales-income/paymentMethod.ts b/frontend/src/constants/sales/dashboard-sales-income/paymentMethod.ts
new file mode 100644
index 000000000..8f8ad018e
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-income/paymentMethod.ts
@@ -0,0 +1,38 @@
+import type { GetIncomeStructureByPaymentMethodResponseDto } from '@/types/sales';
+
+export const PAYMENT_METHOD = {
+ EXAMPLE_TOP_TYPE: '현금',
+ EXAMPLE_TOP_SHARE: 46,
+ EXAMPLE_DELTA_SHARE: 6.7,
+ EXAMPLE_PAYMENT_METHOD_DATA: [
+ {
+ payMethod: '카드',
+ salesAmount: 2371000,
+ orderCount: 26,
+ share: 25,
+ deltaShare: 4.4,
+ },
+ {
+ payMethod: '현금',
+ salesAmount: 7531000,
+ orderCount: 25,
+ share: 25,
+ deltaShare: 6.7,
+ },
+ {
+ payMethod: '간편결제',
+ salesAmount: 2567000,
+ orderCount: 75,
+ share: 25,
+ deltaShare: -5.2,
+ },
+ {
+ payMethod: '기타',
+ salesAmount: 3894000,
+ orderCount: 39,
+ share: 25,
+ deltaShare: 2.4,
+ },
+ ] as const satisfies GetIncomeStructureByPaymentMethodResponseDto['items'],
+ DOUGHNUT_CHART_TITLE: '결제수단별 매출 관련 도넛 차트',
+} as const;
diff --git a/frontend/src/constants/sales/dashboard-sales-income/salesType.ts b/frontend/src/constants/sales/dashboard-sales-income/salesType.ts
new file mode 100644
index 000000000..dcff66872
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-income/salesType.ts
@@ -0,0 +1,33 @@
+import type { GetIncomeStructureBySalesTypeResponseDto } from '@/types/sales';
+
+import { SALES_SOURCE } from '../salesSource';
+
+export const SALES_TYPE = {
+ EXAMPLE_TOP_TYPE: '배달' as const,
+ EXAMPLE_TOP_SHARE: 43,
+ EXAMPLE_DELTA_SHARE: 6.8,
+ EXAMPLE_SALES_SOURCE_DATA: [
+ {
+ salesType: SALES_SOURCE.SALE_TYPE.DINE_IN,
+ salesAmount: 2371000,
+ orderCount: 26,
+ share: 25,
+ deltaShare: 4.4,
+ },
+ {
+ salesType: SALES_SOURCE.SALE_TYPE.TAKEOUT,
+ salesAmount: 3255000,
+ orderCount: 45,
+ share: 45,
+ deltaShare: -5.2,
+ },
+ {
+ salesType: SALES_SOURCE.SALE_TYPE.DELIVERY,
+ salesAmount: 4255000,
+ orderCount: 28,
+ share: 30,
+ deltaShare: 6.8,
+ },
+ ] as GetIncomeStructureBySalesTypeResponseDto['items'],
+ DOUGHNUT_CHART_TITLE: '판매 유형 관련 도넛 차트',
+} as const;
diff --git a/frontend/src/constants/sales/dashboard-sales-pattern/index.ts b/frontend/src/constants/sales/dashboard-sales-pattern/index.ts
new file mode 100644
index 000000000..a032dbb7f
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-pattern/index.ts
@@ -0,0 +1,2 @@
+export { PEAK_TIME } from './peakTime';
+export { SALES_BY_DAY } from './salesByDay';
diff --git a/frontend/src/constants/sales/dashboard-sales-pattern/peakTime.ts b/frontend/src/constants/sales/dashboard-sales-pattern/peakTime.ts
new file mode 100644
index 000000000..5de0f06b2
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-pattern/peakTime.ts
@@ -0,0 +1,39 @@
+import type { GetDetailPeakTimeResponseDto } from '@/types/sales';
+
+export const PEAK_TIME = {
+ EXAMPLE_DATA: {
+ todayPeak: 8,
+ comparisonPeak: 18,
+ diff: 10,
+ shiftDirection: 'EARLY',
+ beforeComparisonPeak: true,
+ todayItems: [
+ { timeSlot2H: 0, orderCount: 12, netAmount: 300000 },
+ { timeSlot2H: 2, orderCount: 8, netAmount: 180000 },
+ { timeSlot2H: 4, orderCount: 5, netAmount: 120000 },
+ { timeSlot2H: 6, orderCount: 18, netAmount: 420000 },
+ { timeSlot2H: 8, orderCount: 42, netAmount: 980000 },
+ { timeSlot2H: 10, orderCount: null, netAmount: null },
+ { timeSlot2H: 12, orderCount: null, netAmount: null },
+ { timeSlot2H: 14, orderCount: null, netAmount: null },
+ { timeSlot2H: 16, orderCount: null, netAmount: null },
+ { timeSlot2H: 18, orderCount: null, netAmount: null },
+ { timeSlot2H: 20, orderCount: null, netAmount: null },
+ { timeSlot2H: 22, orderCount: null, netAmount: null },
+ ],
+ week4Items: [
+ { timeSlot2H: 0, orderCount: 10, netAmount: 250000 },
+ { timeSlot2H: 2, orderCount: 7, netAmount: 170000 },
+ { timeSlot2H: 4, orderCount: 6, netAmount: 140000 },
+ { timeSlot2H: 6, orderCount: 15, netAmount: 360000 },
+ { timeSlot2H: 8, orderCount: 35, netAmount: 840000 },
+ { timeSlot2H: 10, orderCount: 48, netAmount: 1160000 },
+ { timeSlot2H: 12, orderCount: 62, netAmount: 1520000 },
+ { timeSlot2H: 14, orderCount: 58, netAmount: 1430000 },
+ { timeSlot2H: 16, orderCount: 54, netAmount: 1310000 },
+ { timeSlot2H: 18, orderCount: 66, netAmount: 1650000 },
+ { timeSlot2H: 20, orderCount: 40, netAmount: 990000 },
+ { timeSlot2H: 22, orderCount: 22, netAmount: 540000 },
+ ],
+ } satisfies GetDetailPeakTimeResponseDto,
+};
diff --git a/frontend/src/constants/sales/dashboard-sales-pattern/salesByDay.ts b/frontend/src/constants/sales/dashboard-sales-pattern/salesByDay.ts
new file mode 100644
index 000000000..2e6c0ae47
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-pattern/salesByDay.ts
@@ -0,0 +1,46 @@
+import type { SalesByDayItem } from '@/types/sales';
+
+export const SALES_BY_DAY = {
+ CHART_X_UNIT: '요일',
+ CHART_Y_UNIT: '원',
+ CHART_COLOR: 'black',
+ EXAMPLE_TOP_DAY: '금' as const,
+ EXAMPLE_IS_SIGNIFICANT: false,
+ EXAMPLE_DATA: [
+ {
+ day: '월',
+ avgNetAmount: 820000,
+ orderCount: 86,
+ },
+ {
+ day: '화',
+ avgNetAmount: 790000,
+ orderCount: 82,
+ },
+ {
+ day: '수',
+ avgNetAmount: 880000,
+ orderCount: 91,
+ },
+ {
+ day: '목',
+ avgNetAmount: 940000,
+ orderCount: 97,
+ },
+ {
+ day: '금',
+ avgNetAmount: 1320000,
+ orderCount: 141,
+ },
+ {
+ day: '토',
+ avgNetAmount: 1110000,
+ orderCount: 118,
+ },
+ {
+ day: '일',
+ avgNetAmount: 960000,
+ orderCount: 102,
+ },
+ ] as const satisfies SalesByDayItem[],
+};
diff --git a/frontend/src/constants/sales/dashboard/briefingMessageHighlightColor.ts b/frontend/src/constants/sales/dashboard/briefingMessageHighlightColor.ts
new file mode 100644
index 000000000..17f986a3f
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard/briefingMessageHighlightColor.ts
@@ -0,0 +1,8 @@
+export const BRIEFING_MESSAGE_HIGHLIGHT_COLOR = {
+ primary: 'text-brand-main font-bold',
+ negative: 'text-others-negative font-bold',
+ default: 'text-grey-500 font-bold',
+} as const;
+
+export type BriefingMessageHighlightColor =
+ keyof typeof BRIEFING_MESSAGE_HIGHLIGHT_COLOR;
diff --git a/frontend/src/constants/sales/dashboard/index.ts b/frontend/src/constants/sales/dashboard/index.ts
new file mode 100644
index 000000000..6d4f7f2ca
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard/index.ts
@@ -0,0 +1,4 @@
+export {
+ BRIEFING_MESSAGE_HIGHLIGHT_COLOR,
+ type BriefingMessageHighlightColor,
+} from './briefingMessageHighlightColor';
diff --git a/frontend/src/constants/sales/index.ts b/frontend/src/constants/sales/index.ts
index b5c33cb39..9122a5ec5 100644
--- a/frontend/src/constants/sales/index.ts
+++ b/frontend/src/constants/sales/index.ts
@@ -1,2 +1,22 @@
-export { SALES_SOURCE_COLORS } from './salesSource';
+export {
+ SALES_SOURCE_COLORS,
+ isSalesSourceType,
+ SALES_SOURCE,
+} from './salesSource';
export type { SalesSourceType } from './salesSource';
+export { SALES_UNIT } from './salesUnit';
+export {
+ BRIEFING_MESSAGE_HIGHLIGHT_COLOR,
+ type BriefingMessageHighlightColor,
+} from './dashboard';
+export {
+ AVERAGE_PRICE,
+ ORDER_COUNT,
+ REAL_SALES,
+} from './dashboard-current-sales';
+export {
+ ORDER_METHOD,
+ PAYMENT_METHOD,
+ SALES_TYPE,
+} from './dashboard-sales-income';
+export { PEAK_TIME, SALES_BY_DAY } from './dashboard-sales-pattern';
diff --git a/frontend/src/constants/sales/salesSource.ts b/frontend/src/constants/sales/salesSource.ts
index 55959e712..6980fbe3c 100644
--- a/frontend/src/constants/sales/salesSource.ts
+++ b/frontend/src/constants/sales/salesSource.ts
@@ -28,9 +28,19 @@ export const SALES_SOURCE_COLORS = {
[SALES_SOURCE.SALE_TYPE.TAKEOUT]: 'var(--color-brand-50)',
[SALES_SOURCE.ORDER_METHOD.POS]: 'var(--color-brand-500)',
[SALES_SOURCE.ORDER_METHOD.KIOSK]: 'var(--color-grey-500)',
- [SALES_SOURCE.ORDER_METHOD.DELIVERY_APP]: 'var(--color-brand-50)',
+ [SALES_SOURCE.ORDER_METHOD.DELIVERY_APP]: 'var(--color-brand-200)',
[SALES_SOURCE.PAYMENT_METHOD.CARD]: 'var(--color-brand-500)',
[SALES_SOURCE.PAYMENT_METHOD.CASH]: 'var(--color-grey-500)',
[SALES_SOURCE.PAYMENT_METHOD.MOBILE]: 'var(--color-brand-200)',
[SALES_SOURCE.PAYMENT_METHOD.ETC]: 'var(--color-brand-50)',
};
+
+const SALES_SOURCE_TYPES: readonly string[] = [
+ ...Object.values(SALES_SOURCE.SALE_TYPE),
+ ...Object.values(SALES_SOURCE.ORDER_METHOD),
+ ...Object.values(SALES_SOURCE.PAYMENT_METHOD),
+];
+
+export const isSalesSourceType = (value: string): value is SalesSourceType => {
+ return SALES_SOURCE_TYPES.includes(value);
+};
diff --git a/frontend/src/constants/sales/salesUnit.ts b/frontend/src/constants/sales/salesUnit.ts
new file mode 100644
index 000000000..66f54d7c4
--- /dev/null
+++ b/frontend/src/constants/sales/salesUnit.ts
@@ -0,0 +1,6 @@
+export const SALES_UNIT = {
+ WON: '원',
+ PERCENT: '%',
+ PERCENT_POINT: '%p',
+ ORDER: '건',
+} as const;
diff --git a/frontend/src/hooks/shared/bar-chart/useBarChart.ts b/frontend/src/hooks/shared/bar-chart/useBarChart.ts
index a45935000..83b27662b 100644
--- a/frontend/src/hooks/shared/bar-chart/useBarChart.ts
+++ b/frontend/src/hooks/shared/bar-chart/useBarChart.ts
@@ -10,16 +10,19 @@ import { getCoordinate, getXCoordinate } from '@/utils/shared';
import { checkIsStackBarChart } from '@/utils/shared/bar-chart';
interface UseBarChartProps {
+ viewBoxWidth: number;
+ viewBoxHeight: number;
barChartSeries: AllBarChartSeries;
hasXAxis?: boolean;
}
export const useBarChart = ({
+ viewBoxWidth,
+ viewBoxHeight,
barChartSeries,
hasXAxis = false,
}: UseBarChartProps) => {
- const [svgRect, setSvgRect] = useState(null);
- const [adjustedHeight, setAdjustedHeight] = useState(0);
+ const [adjustedHeight, setAdjustedHeight] = useState(viewBoxHeight);
const svgRef = useRef(null);
const xAxisRef = useRef(null);
@@ -31,14 +34,14 @@ export const useBarChart = ({
}, [barChartSeries.data.mainX]);
const xCoordinate = useMemo(() => {
- if (svgRect === null) {
+ if (barChartSeries.data.mainX.length === 0) {
return [];
}
return getXCoordinate({
- svgRect,
+ svgWidth: viewBoxWidth,
xDataLength: barChartSeries.data.mainX.length,
});
- }, [svgRect, barChartSeries.data.mainX.length]);
+ }, [viewBoxWidth, barChartSeries.data.mainX.length]);
//
const maximumY = useMemo(() => {
@@ -61,7 +64,7 @@ export const useBarChart = ({
return adjustedMaximumAmount;
}, [barChartSeries.data.mainY, isStackBarChart]);
const primaryCoordinate = useMemo(() => {
- if (svgRect === null) {
+ if (barChartSeries.data.mainX.length === 0) {
return [];
}
// 만약 stackBarChart면 스택별로 데이터를 하나의 바로 합친 후 좌표를 계산해야 함
@@ -85,30 +88,27 @@ export const useBarChart = ({
: barChartSeries;
return getCoordinate({
- svgRect,
+ svgWidth: viewBoxWidth,
adjustedHeight,
series: barSeriesForCoordinate as BarChartSeries,
maximumY,
});
- }, [svgRect, adjustedHeight, barChartSeries, maximumY, isStackBarChart]);
+ }, [viewBoxWidth, adjustedHeight, barChartSeries, maximumY, isStackBarChart]);
useLayoutEffect(() => {
- if (!svgRef.current) {
- return;
- }
- setSvgRect(svgRef.current.getBoundingClientRect());
- setAdjustedHeight(svgRef.current.getBoundingClientRect().height);
- if (hasXAxis && xAxisRef.current) {
- setAdjustedHeight(
- xAxisRef.current.getBoundingClientRect().y -
- svgRef.current.getBoundingClientRect().y +
- xAxisRef.current.getBoundingClientRect().height / 2,
- );
- }
- }, [svgRef, xAxisRef, hasXAxis]);
+ const updateAdjustedHeight = () => {
+ if (!hasXAxis || !xAxisRef.current) {
+ setAdjustedHeight(viewBoxHeight);
+ return;
+ }
+ const xAxisBBox = xAxisRef.current.getBBox();
+ setAdjustedHeight(xAxisBBox.y + xAxisBBox.height / 2);
+ };
+ updateAdjustedHeight();
+ }, [hasXAxis, viewBoxHeight]);
return {
- svgRect,
+ svgWidth: viewBoxWidth,
adjustedHeight,
xLabelList,
xCoordinate,
diff --git a/frontend/src/hooks/shared/line-chart/useLineChart.ts b/frontend/src/hooks/shared/line-chart/useLineChart.ts
index fd0a68d51..6a3f1cd90 100644
--- a/frontend/src/hooks/shared/line-chart/useLineChart.ts
+++ b/frontend/src/hooks/shared/line-chart/useLineChart.ts
@@ -8,18 +8,21 @@ import {
} from '@/utils/shared';
interface UseLineChartProps {
+ viewBoxWidth: number;
+ viewBoxHeight: number;
primarySeries: LineChartSeries;
secondarySeries?: LineChartSeries;
hasXAxis?: boolean;
}
export const useLineChart = ({
+ viewBoxWidth,
+ viewBoxHeight,
primarySeries,
secondarySeries,
hasXAxis = false,
}: UseLineChartProps) => {
- const [svgRect, setSvgRect] = useState(null);
- const [adjustedHeight, setAdjustedHeight] = useState(0);
+ const [adjustedHeight, setAdjustedHeight] = useState(viewBoxHeight);
const svgRef = useRef(null);
const xAxisRef = useRef(null);
@@ -29,14 +32,14 @@ export const useLineChart = ({
}, [primarySeries.data.mainX]);
const xCoordinate = useMemo(() => {
- if (svgRect === null) {
+ if (primarySeries.data.mainX.length === 0) {
return [];
}
return getXCoordinate({
- svgRect,
+ svgWidth: viewBoxWidth,
xDataLength: primarySeries.data.mainX.length,
});
- }, [svgRect, primarySeries.data.mainX.length]);
+ }, [viewBoxWidth, primarySeries.data.mainX.length]);
const maximumY = useMemo(() => {
const totalData = [
@@ -56,16 +59,16 @@ export const useLineChart = ({
}, [primarySeries.data.mainY, secondarySeries?.data.mainY]);
const primaryCoordinate = useMemo(() => {
- if (svgRect === null) {
+ if (primarySeries.data.mainX.length === 0) {
return [];
}
return getCoordinate({
- svgRect,
+ svgWidth: viewBoxWidth,
adjustedHeight,
series: primarySeries,
maximumY,
});
- }, [svgRect, adjustedHeight, primarySeries, maximumY]);
+ }, [viewBoxWidth, adjustedHeight, primarySeries, maximumY]);
const lastXCoordinate = useMemo(() => {
const filteredCoordinate = filterCoordinate(primaryCoordinate);
@@ -78,34 +81,34 @@ export const useLineChart = ({
}, [primaryCoordinate]);
const secondaryCoordinate = useMemo(() => {
- if (svgRect === null || secondarySeries === undefined) {
+ if (
+ secondarySeries === undefined ||
+ secondarySeries.data.mainX.length === 0
+ ) {
return [];
}
return getCoordinate({
- svgRect,
+ svgWidth: viewBoxWidth,
adjustedHeight,
series: secondarySeries,
maximumY,
});
- }, [svgRect, adjustedHeight, secondarySeries, maximumY]);
+ }, [viewBoxWidth, adjustedHeight, secondarySeries, maximumY]);
useLayoutEffect(() => {
- if (!svgRef.current) {
- return;
- }
- setSvgRect(svgRef.current.getBoundingClientRect());
- setAdjustedHeight(svgRef.current.getBoundingClientRect().height);
- if (hasXAxis && xAxisRef.current) {
- setAdjustedHeight(
- xAxisRef.current.getBoundingClientRect().y -
- svgRef.current.getBoundingClientRect().y +
- xAxisRef.current.getBoundingClientRect().height / 2,
- );
- }
- }, [svgRef, xAxisRef, hasXAxis]);
+ const updateAdjustedHeight = () => {
+ if (!hasXAxis || !xAxisRef.current) {
+ setAdjustedHeight(viewBoxHeight);
+ return;
+ }
+ const xAxisBBox = xAxisRef.current.getBBox();
+ setAdjustedHeight(xAxisBBox.y + xAxisBBox.height / 2);
+ };
+ updateAdjustedHeight();
+ }, [hasXAxis, viewBoxHeight]);
return {
- svgRect,
+ svgWidth: viewBoxWidth,
adjustedHeight,
xLabelList,
xCoordinate,
diff --git a/frontend/src/mocks/auth/authHandler.ts b/frontend/src/mocks/auth/authHandler.ts
index 42e9ff05c..02ce27534 100644
--- a/frontend/src/mocks/auth/authHandler.ts
+++ b/frontend/src/mocks/auth/authHandler.ts
@@ -41,7 +41,7 @@ const getHandler = [
const postHandler = [
mswHttp.post('/auth/refresh', () => {
- return HttpResponse.json>(
+ HttpResponse.json>(
{
success: true,
message: 'Success',
diff --git a/frontend/src/types/sales/dashboard-sales-income/index.ts b/frontend/src/types/sales/dashboard-sales-income/index.ts
new file mode 100644
index 000000000..db536c49c
--- /dev/null
+++ b/frontend/src/types/sales/dashboard-sales-income/index.ts
@@ -0,0 +1,4 @@
+export type {
+ SalesIncomeStructureInsight,
+ SalesIncomeStructureTopType,
+} from './salesIncomeStructureInsight';
diff --git a/frontend/src/types/sales/dashboard-sales-income/salesIncomeStructureInsight.ts b/frontend/src/types/sales/dashboard-sales-income/salesIncomeStructureInsight.ts
new file mode 100644
index 000000000..3612df04c
--- /dev/null
+++ b/frontend/src/types/sales/dashboard-sales-income/salesIncomeStructureInsight.ts
@@ -0,0 +1,19 @@
+export type SalesIncomeStructureTopType =
+ | '홀'
+ | '포장'
+ | '배달'
+ | 'POS'
+ | '키오스크'
+ | '배달앱'
+ | '카드'
+ | '현금'
+ | '간편결제'
+ | '기타';
+
+export interface SalesIncomeStructureInsight {
+ topType: SalesIncomeStructureTopType;
+ topShare: number;
+ deltaShare: number;
+ showDeltaText?: boolean;
+ showFocusText?: boolean;
+}
diff --git a/frontend/src/types/sales/dashboard-sales-pattern/index.ts b/frontend/src/types/sales/dashboard-sales-pattern/index.ts
new file mode 100644
index 000000000..890ad2b99
--- /dev/null
+++ b/frontend/src/types/sales/dashboard-sales-pattern/index.ts
@@ -0,0 +1,2 @@
+export type { PeakTimeItem, PeakTimeSummary } from './peakTime';
+export type { SalesByDaySummary, SalesByDayItem } from './salesByDay';
diff --git a/frontend/src/types/sales/dashboard-sales-pattern/peakTime.ts b/frontend/src/types/sales/dashboard-sales-pattern/peakTime.ts
new file mode 100644
index 000000000..3ed5a8fb2
--- /dev/null
+++ b/frontend/src/types/sales/dashboard-sales-pattern/peakTime.ts
@@ -0,0 +1,15 @@
+export interface PeakTimeItem {
+ timeSlot2H: number;
+ orderCount: number | null;
+ netAmount: number | null;
+}
+
+type ShiftDirection = 'EARLY' | 'LATE' | 'SAME' | 'UNKNOWN';
+
+export interface PeakTimeSummary {
+ todayPeak: number; // 오늘 피크 시간
+ comparisonPeak: number; // 비교 시간 피크 시간
+ diff: number; // 비교 시간 피크 시간 차이
+ shiftDirection: ShiftDirection; // 피크 시간 방향성
+ beforeComparisonPeak: boolean; // 비교 시간 피크 시간 이전
+}
diff --git a/frontend/src/types/sales/dashboard-sales-pattern/salesByDay.ts b/frontend/src/types/sales/dashboard-sales-pattern/salesByDay.ts
new file mode 100644
index 000000000..7ec8fcb4f
--- /dev/null
+++ b/frontend/src/types/sales/dashboard-sales-pattern/salesByDay.ts
@@ -0,0 +1,12 @@
+type Day = '월' | '화' | '수' | '목' | '금' | '토' | '일';
+
+export interface SalesByDaySummary {
+ topDay: Day;
+ isSignificant: boolean;
+}
+
+export interface SalesByDayItem {
+ day: Day;
+ avgNetAmount: number;
+ orderCount: number;
+}
diff --git a/frontend/src/types/sales/dto/getAveragePriceDto.ts b/frontend/src/types/sales/dto/getAveragePriceDto.ts
new file mode 100644
index 000000000..9bb4c5e66
--- /dev/null
+++ b/frontend/src/types/sales/dto/getAveragePriceDto.ts
@@ -0,0 +1,5 @@
+export interface GetAveragePriceResponseDto {
+ averageOrderAmount: number;
+ differenceAmount: number;
+ hasPreviousData: boolean;
+}
diff --git a/frontend/src/types/sales/dto/getIncomeStructureByOrderMethodDto.ts b/frontend/src/types/sales/dto/getIncomeStructureByOrderMethodDto.ts
new file mode 100644
index 000000000..2266ac0c1
--- /dev/null
+++ b/frontend/src/types/sales/dto/getIncomeStructureByOrderMethodDto.ts
@@ -0,0 +1,20 @@
+import type {
+ SalesIncomeStructureInsight,
+ SalesIncomeStructureTopType,
+} from '../dashboard-sales-income';
+
+interface OrderMethodItem {
+ orderChannel: Extract<
+ SalesIncomeStructureTopType,
+ 'POS' | '키오스크' | '배달앱' | '기타'
+ >;
+ salesAmount: number;
+ orderCount: number;
+ share: number;
+ deltaShare: number;
+}
+
+export interface GetIncomeStructureByOrderMethodResponseDto {
+ insight: SalesIncomeStructureInsight;
+ items: OrderMethodItem[];
+}
diff --git a/frontend/src/types/sales/dto/getIncomeStructureByPaymentMethodDto.ts b/frontend/src/types/sales/dto/getIncomeStructureByPaymentMethodDto.ts
new file mode 100644
index 000000000..31de5c183
--- /dev/null
+++ b/frontend/src/types/sales/dto/getIncomeStructureByPaymentMethodDto.ts
@@ -0,0 +1,20 @@
+import type {
+ SalesIncomeStructureInsight,
+ SalesIncomeStructureTopType,
+} from '../dashboard-sales-income';
+
+interface PaymentMethodItem {
+ payMethod: Extract<
+ SalesIncomeStructureTopType,
+ '카드' | '현금' | '간편결제' | '기타'
+ >;
+ salesAmount: number;
+ orderCount: number;
+ share: number;
+ deltaShare: number;
+}
+
+export interface GetIncomeStructureByPaymentMethodResponseDto {
+ insight: SalesIncomeStructureInsight;
+ items: PaymentMethodItem[];
+}
diff --git a/frontend/src/types/sales/dto/getIncomeStructureBySalesTypeDto.ts b/frontend/src/types/sales/dto/getIncomeStructureBySalesTypeDto.ts
new file mode 100644
index 000000000..6aa77de18
--- /dev/null
+++ b/frontend/src/types/sales/dto/getIncomeStructureBySalesTypeDto.ts
@@ -0,0 +1,17 @@
+import type {
+ SalesIncomeStructureInsight,
+ SalesIncomeStructureTopType,
+} from '../dashboard-sales-income';
+
+interface SalesTypeItem {
+ salesType: Extract;
+ salesAmount: number;
+ orderCount: number;
+ share: number;
+ deltaShare: number;
+}
+
+export interface GetIncomeStructureBySalesTypeResponseDto {
+ insight: SalesIncomeStructureInsight;
+ items: SalesTypeItem[];
+}
diff --git a/frontend/src/types/sales/dto/getOrderCountDto.ts b/frontend/src/types/sales/dto/getOrderCountDto.ts
new file mode 100644
index 000000000..0cbb5841b
--- /dev/null
+++ b/frontend/src/types/sales/dto/getOrderCountDto.ts
@@ -0,0 +1,6 @@
+export interface GetOrderCountResponseDto {
+ orderCount: number;
+ differenceOrderCount: number;
+ changeRate: number;
+ hasPreviousData: boolean;
+}
diff --git a/frontend/src/types/sales/dto/getPeakTimeDto.ts b/frontend/src/types/sales/dto/getPeakTimeDto.ts
new file mode 100644
index 000000000..5f19e2b86
--- /dev/null
+++ b/frontend/src/types/sales/dto/getPeakTimeDto.ts
@@ -0,0 +1,15 @@
+import type { PeakTimeItem, PeakTimeSummary } from '../dashboard-sales-pattern';
+
+export interface GetDetailPeakTimeResponseDto extends PeakTimeSummary {
+ todayItems: PeakTimeItem[];
+ week4Items: PeakTimeItem[];
+}
+
+/**
+ * 대시보드 피크 시간 DTO (SSE)
+ */
+export interface GetDashboardPeakTimeResponseDto extends PeakTimeSummary {
+ timeSlot2H: number; // 현재 시간
+ orderCount: number; // 현재 시간 주문건수
+ netAmount: number; // 현재 시간 실매출액
+}
diff --git a/frontend/src/types/sales/dto/getRealTimeSalesDto.ts b/frontend/src/types/sales/dto/getRealTimeSalesDto.ts
new file mode 100644
index 000000000..411d5a155
--- /dev/null
+++ b/frontend/src/types/sales/dto/getRealTimeSalesDto.ts
@@ -0,0 +1,6 @@
+export interface GetRealTimeSalesResponseDto {
+ netAmount: number;
+ differenceAmount: number;
+ changeRate: number;
+ hasPreviousData: boolean;
+}
diff --git a/frontend/src/types/sales/dto/getSalesByDayDto.ts b/frontend/src/types/sales/dto/getSalesByDayDto.ts
new file mode 100644
index 000000000..5e8cba6f2
--- /dev/null
+++ b/frontend/src/types/sales/dto/getSalesByDayDto.ts
@@ -0,0 +1,11 @@
+import type {
+ SalesByDayItem,
+ SalesByDaySummary,
+} from '../dashboard-sales-pattern';
+
+export interface GetDetailSalesByDayResponseDto extends SalesByDaySummary {
+ items: SalesByDayItem[];
+}
+
+export interface GetDashboardSalesByDayResponseDto
+ extends SalesByDaySummary, SalesByDayItem {}
diff --git a/frontend/src/types/sales/dto/index.ts b/frontend/src/types/sales/dto/index.ts
new file mode 100644
index 000000000..2c9747f31
--- /dev/null
+++ b/frontend/src/types/sales/dto/index.ts
@@ -0,0 +1,14 @@
+export type { GetRealTimeSalesResponseDto } from './getRealTimeSalesDto';
+export type { GetOrderCountResponseDto } from './getOrderCountDto';
+export type { GetAveragePriceResponseDto } from './getAveragePriceDto';
+export type { GetIncomeStructureBySalesTypeResponseDto } from './getIncomeStructureBySalesTypeDto';
+export type { GetIncomeStructureByOrderMethodResponseDto } from './getIncomeStructureByOrderMethodDto';
+export type { GetIncomeStructureByPaymentMethodResponseDto } from './getIncomeStructureByPaymentMethodDto';
+export type {
+ GetDetailPeakTimeResponseDto,
+ GetDashboardPeakTimeResponseDto,
+} from './getPeakTimeDto';
+export type {
+ GetDetailSalesByDayResponseDto,
+ GetDashboardSalesByDayResponseDto,
+} from './getSalesByDayDto';
diff --git a/frontend/src/types/sales/index.ts b/frontend/src/types/sales/index.ts
index c27a2918d..60ba30d7f 100644
--- a/frontend/src/types/sales/index.ts
+++ b/frontend/src/types/sales/index.ts
@@ -1 +1,19 @@
export type { SalesSource } from './salesSource';
+export type {
+ GetRealTimeSalesResponseDto,
+ GetOrderCountResponseDto,
+ GetAveragePriceResponseDto,
+ GetIncomeStructureBySalesTypeResponseDto,
+ GetIncomeStructureByOrderMethodResponseDto,
+ GetIncomeStructureByPaymentMethodResponseDto,
+ GetDetailPeakTimeResponseDto,
+ GetDashboardPeakTimeResponseDto,
+ GetDetailSalesByDayResponseDto,
+ GetDashboardSalesByDayResponseDto,
+} from './dto';
+export type { SalesIncomeStructureInsight } from './dashboard-sales-income';
+export type { PeakTimeItem, PeakTimeSummary } from './dashboard-sales-pattern';
+export type {
+ SalesByDaySummary,
+ SalesByDayItem,
+} from './dashboard-sales-pattern';
diff --git a/frontend/src/types/shared/bar-chart/barChartDataType.ts b/frontend/src/types/shared/bar-chart/barChartDataType.ts
index f141f5205..7d2ab5f9c 100644
--- a/frontend/src/types/shared/bar-chart/barChartDataType.ts
+++ b/frontend/src/types/shared/bar-chart/barChartDataType.ts
@@ -1,12 +1,6 @@
-import type { LineChartDatum } from '../line-chart';
+import type { ChartDatum } from '../chart';
-// LineChartDatum과 동일한 구조를 가짐
-// export interface LineChartDatum {
-// amount: number | string | null;
-// unit: string;
-// }
-
-export type BarChartDatum = LineChartDatum;
+export type BarChartDatum = ChartDatum;
export interface BarChartData {
mainX: BarChartDatum[]; // 시간 목록
diff --git a/frontend/src/types/shared/chartDataType.ts b/frontend/src/types/shared/chart/chartDataType.ts
similarity index 100%
rename from frontend/src/types/shared/chartDataType.ts
rename to frontend/src/types/shared/chart/chartDataType.ts
diff --git a/frontend/src/types/shared/chart/index.ts b/frontend/src/types/shared/chart/index.ts
new file mode 100644
index 000000000..24fd610bc
--- /dev/null
+++ b/frontend/src/types/shared/chart/index.ts
@@ -0,0 +1 @@
+export type { ChartData, ChartSeries, ChartDatum } from './chartDataType';
diff --git a/frontend/src/types/shared/index.ts b/frontend/src/types/shared/index.ts
index 0d0239185..1fb4323aa 100644
--- a/frontend/src/types/shared/index.ts
+++ b/frontend/src/types/shared/index.ts
@@ -8,7 +8,6 @@ export type {
export type { RouteHandle } from './routeHandle';
export type { StoreInfo } from './storeInfo';
export type {
- LineChartDatum,
LineChartData,
LineChartSeries,
Coordinate,
@@ -25,5 +24,5 @@ export type {
StackBarChartSeries,
AllBarChartSeries,
} from './bar-chart';
-export type { ChartData, ChartSeries } from './chartDataType';
+export type { ChartData, ChartSeries, ChartDatum } from './chart';
export type { EventSourceMessage } from './eventSourceMessage';
diff --git a/frontend/src/types/shared/line-chart/index.ts b/frontend/src/types/shared/line-chart/index.ts
index 0b28324f8..e7d069ab6 100644
--- a/frontend/src/types/shared/line-chart/index.ts
+++ b/frontend/src/types/shared/line-chart/index.ts
@@ -1,7 +1,3 @@
-export type {
- LineChartDatum,
- LineChartData,
- LineChartSeries,
-} from './lineChartDataType';
+export type { LineChartData, LineChartSeries } from './lineChartDataType';
export type { Coordinate } from './Coordinate';
export type { XAxisType } from './xAxis';
diff --git a/frontend/src/types/shared/line-chart/lineChartDataType.ts b/frontend/src/types/shared/line-chart/lineChartDataType.ts
index d678d89bd..8aac47d22 100644
--- a/frontend/src/types/shared/line-chart/lineChartDataType.ts
+++ b/frontend/src/types/shared/line-chart/lineChartDataType.ts
@@ -1,13 +1,10 @@
-export interface LineChartDatum {
- amount: number | string | null;
- unit: string;
-}
+import type { ChartDatum } from '../chart';
export interface LineChartData {
- mainX: LineChartDatum[];
- subX: LineChartDatum[];
- mainY: LineChartDatum[];
- subY: LineChartDatum[];
+ mainX: ChartDatum[];
+ subX: ChartDatum[];
+ mainY: ChartDatum[];
+ subY: ChartDatum[];
}
export interface LineChartSeries {
diff --git a/frontend/src/utils/dashboard/getMetricTrend.ts b/frontend/src/utils/dashboard/getMetricTrend.ts
new file mode 100644
index 000000000..1cd9f429c
--- /dev/null
+++ b/frontend/src/utils/dashboard/getMetricTrend.ts
@@ -0,0 +1,31 @@
+import { METRIC_TREND } from '@/constants/dashboard';
+
+interface GetMetricTrendArgs {
+ comparisonAmount: number;
+ minValue: number;
+ maxValue: number;
+}
+
+export const getMetricTrend = ({
+ comparisonAmount,
+ minValue,
+ maxValue,
+}: GetMetricTrendArgs) => {
+ if (!maxValue && !minValue) {
+ if (comparisonAmount > 0) {
+ return METRIC_TREND.UP;
+ } else if (comparisonAmount < 0) {
+ return METRIC_TREND.DOWN;
+ } else {
+ return METRIC_TREND.SAME;
+ }
+ }
+
+ if (comparisonAmount >= maxValue) {
+ return METRIC_TREND.UP;
+ } else if (comparisonAmount <= minValue) {
+ return METRIC_TREND.DOWN;
+ } else {
+ return METRIC_TREND.SAME;
+ }
+};
diff --git a/frontend/src/utils/dashboard/index.ts b/frontend/src/utils/dashboard/index.ts
index e29d11f92..e62c29881 100644
--- a/frontend/src/utils/dashboard/index.ts
+++ b/frontend/src/utils/dashboard/index.ts
@@ -1,3 +1,4 @@
+export { getMetricTrend } from './getMetricTrend';
export {
getAvailablePositionOnGrid,
addCardOnGrid,
diff --git a/frontend/src/utils/sales/dashboard-current-sales/getSalesCurrentComparisonMessage.ts b/frontend/src/utils/sales/dashboard-current-sales/getSalesCurrentComparisonMessage.ts
new file mode 100644
index 000000000..5579c97e6
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-current-sales/getSalesCurrentComparisonMessage.ts
@@ -0,0 +1,91 @@
+import { METRIC_TREND, type MetricTrend } from '@/constants/dashboard';
+import { DAY_OF_WEEK_LIST, PERIOD_PRESETS } from '@/constants/shared';
+import { assertNever, formatNumber, type ValueOf } from '@/utils/shared';
+
+import { createMessageToken, type MessageToken } from '../dashboard';
+
+interface GetSalesCurrentComparisonMessageArgs {
+ periodType: ValueOf;
+ hasPreviousData: boolean;
+ metricTrend: MetricTrend;
+ metricLabel: string;
+ comparisonAmount: number;
+ unit: string;
+}
+
+export const getSalesCurrentComparisonMessage = ({
+ periodType,
+ hasPreviousData,
+ metricTrend,
+ metricLabel,
+ comparisonAmount,
+ unit,
+}: GetSalesCurrentComparisonMessageArgs): MessageToken[] => {
+ const weekday = DAY_OF_WEEK_LIST[new Date().getDay()];
+
+ const PERIOD_TEXT = {
+ [PERIOD_PRESETS.dayWeekMonth.today]: `지난주 ${weekday}요일`,
+ [PERIOD_PRESETS.dayWeekMonth.thisWeek]: `지난주 이맘때`,
+ [PERIOD_PRESETS.dayWeekMonth.thisMonth]: `지난달 이맘때`,
+ };
+
+ if (!hasPreviousData) {
+ return [
+ createMessageToken(
+ `${PERIOD_TEXT[periodType]}에는 ${metricLabel}이 거의 없었어요.`,
+ ),
+ ];
+ }
+
+ const METRIC_TREND_TEXT = {
+ [METRIC_TREND.UP]: '늘었어요.',
+ [METRIC_TREND.DOWN]: '줄었어요.',
+ [METRIC_TREND.SAME]: '비슷해요.',
+ };
+
+ const formattedComparisonAmount = formatNumber(comparisonAmount);
+
+ switch (periodType) {
+ case PERIOD_PRESETS.dayWeekMonth.today:
+ if (metricTrend === METRIC_TREND.SAME) {
+ return [
+ createMessageToken(`${PERIOD_TEXT[periodType]} 이 시간과 `),
+ createMessageToken(
+ `${METRIC_TREND_TEXT[metricTrend]}`,
+ true,
+ 'default',
+ ),
+ ];
+ }
+ return [
+ createMessageToken(`${PERIOD_TEXT[periodType]} 이 시간보다 `),
+ createMessageToken(
+ `${formattedComparisonAmount}${unit} ${METRIC_TREND_TEXT[metricTrend]}`,
+ true,
+ metricTrend === METRIC_TREND.UP ? 'primary' : 'negative',
+ ),
+ ];
+ case PERIOD_PRESETS.dayWeekMonth.thisWeek:
+ case PERIOD_PRESETS.dayWeekMonth.thisMonth:
+ if (metricTrend === METRIC_TREND.SAME) {
+ return [
+ createMessageToken(`${PERIOD_TEXT[periodType]}와 `),
+ createMessageToken(
+ `${METRIC_TREND_TEXT[metricTrend]}`,
+ true,
+ 'default',
+ ),
+ ];
+ }
+ return [
+ createMessageToken(`${PERIOD_TEXT[periodType]}보다 `),
+ createMessageToken(
+ `${formattedComparisonAmount}${unit} ${METRIC_TREND_TEXT[metricTrend]}`,
+ true,
+ metricTrend === METRIC_TREND.UP ? 'primary' : 'negative',
+ ),
+ ];
+ default:
+ return assertNever(periodType);
+ }
+};
diff --git a/frontend/src/utils/sales/dashboard-current-sales/index.ts b/frontend/src/utils/sales/dashboard-current-sales/index.ts
new file mode 100644
index 000000000..d8d3a8ab8
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-current-sales/index.ts
@@ -0,0 +1 @@
+export { getSalesCurrentComparisonMessage } from './getSalesCurrentComparisonMessage';
diff --git a/frontend/src/utils/sales/dashboard-sales-income/getSalesIncomeStructureComparisonMessage.ts b/frontend/src/utils/sales/dashboard-sales-income/getSalesIncomeStructureComparisonMessage.ts
new file mode 100644
index 000000000..d8406a238
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-sales-income/getSalesIncomeStructureComparisonMessage.ts
@@ -0,0 +1,49 @@
+import { PERIOD_PRESETS } from '@/constants/shared';
+import type { SalesIncomeStructureInsight } from '@/types/sales/dashboard-sales-income/salesIncomeStructureInsight';
+import { formatNumber, type ValueOf } from '@/utils/shared';
+
+import { createMessageToken, type MessageToken } from '../dashboard';
+
+const DELTA_SHARE_THRESHOLD = 3;
+
+interface GetSalesIncomeStructureComparisonMessageArgs extends Omit<
+ SalesIncomeStructureInsight,
+ 'showDeltaText' | 'showFocusText'
+> {
+ periodType: ValueOf;
+}
+
+export const getSalesIncomeStructureComparisonMessage = ({
+ periodType,
+ topType,
+ topShare,
+ deltaShare,
+}: GetSalesIncomeStructureComparisonMessageArgs): MessageToken[] => {
+ if (
+ periodType === PERIOD_PRESETS.dayWeekMonth.today &&
+ Math.abs(deltaShare) >= DELTA_SHARE_THRESHOLD
+ ) {
+ return [
+ createMessageToken('최근 7일 대비 '),
+ createMessageToken(
+ `${topType} 비중이 ${deltaShare >= 0 ? '+' : ''}${formatNumber(deltaShare)}%p `,
+ true,
+ deltaShare >= 0 ? 'primary' : 'negative',
+ ),
+ createMessageToken('변했어요.'),
+ ];
+ }
+
+ if (topShare >= 60) {
+ return [
+ createMessageToken('매출이 '),
+ createMessageToken(`${topType}(${formatNumber(topShare)}%)`, true),
+ createMessageToken('에 집중돼 있어요.'),
+ ];
+ }
+
+ return [
+ createMessageToken(`${topType}(${formatNumber(topShare)}%) `, true),
+ createMessageToken('매출이 가장 많아요.'),
+ ];
+};
diff --git a/frontend/src/utils/sales/dashboard-sales-income/index.ts b/frontend/src/utils/sales/dashboard-sales-income/index.ts
new file mode 100644
index 000000000..aaf0c202a
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-sales-income/index.ts
@@ -0,0 +1 @@
+export { getSalesIncomeStructureComparisonMessage } from './getSalesIncomeStructureComparisonMessage';
diff --git a/frontend/src/utils/sales/dashboard-sales-pattern/createPeakTimeSeries.ts b/frontend/src/utils/sales/dashboard-sales-pattern/createPeakTimeSeries.ts
new file mode 100644
index 000000000..ba6dba5d5
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-sales-pattern/createPeakTimeSeries.ts
@@ -0,0 +1,19 @@
+import type { PeakTimeItem } from '@/types/sales';
+
+export const createPeakTimeSeries = (
+ items: PeakTimeItem[],
+ color: string,
+ mainXUnit: string = '',
+ mainYUnit: string = '',
+) => {
+ const data = {
+ mainX: items.map((item) => ({ amount: item.timeSlot2H, unit: mainXUnit })),
+ mainY: items.map((item) => ({ amount: item.orderCount, unit: mainYUnit })),
+ subX: [],
+ subY: [],
+ };
+ return {
+ data,
+ color,
+ };
+};
diff --git a/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternByDayMessage.ts b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternByDayMessage.ts
new file mode 100644
index 000000000..7c0f65e85
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternByDayMessage.ts
@@ -0,0 +1,29 @@
+import type { SalesByDaySummary } from '@/types/sales';
+
+import {
+ createMessageToken,
+ type MessageToken,
+} from '../dashboard/createMessageToken';
+
+interface GetSalesPatternByDayMessageArgs {
+ topDay: SalesByDaySummary['topDay'];
+ isSignificant: boolean;
+}
+
+export const getSalesPatternByDayMessage = ({
+ topDay,
+ isSignificant,
+}: GetSalesPatternByDayMessageArgs): MessageToken[] => {
+ if (isSignificant) {
+ return [
+ createMessageToken(`${topDay}요일`, true, 'primary'),
+ createMessageToken('이 다른 요일보다 확실히 매출이 높아요.'),
+ ];
+ }
+
+ return [
+ createMessageToken('최근 4주 기준 '),
+ createMessageToken(`${topDay}요일 매출`, true, 'primary'),
+ createMessageToken('이 가장 좋아요.'),
+ ];
+};
diff --git a/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternPeakTimeMessage.ts b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternPeakTimeMessage.ts
new file mode 100644
index 000000000..0b5f9ea0b
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternPeakTimeMessage.ts
@@ -0,0 +1,29 @@
+import type { GetDashboardPeakTimeResponseDto } from '@/types/sales';
+
+import { createMessageToken, type MessageToken } from '../dashboard';
+
+interface GetSalesPatternPeakTimeMessageArgs {
+ todayPeak: GetDashboardPeakTimeResponseDto['todayPeak'];
+ comparisonPeak: GetDashboardPeakTimeResponseDto['comparisonPeak'];
+ beforeComparisonPeak: GetDashboardPeakTimeResponseDto['beforeComparisonPeak'];
+}
+
+export const getSalesPatternPeakTimeMessage = ({
+ todayPeak,
+ comparisonPeak,
+ beforeComparisonPeak,
+}: GetSalesPatternPeakTimeMessageArgs): MessageToken[] => {
+ if (beforeComparisonPeak) {
+ return [
+ createMessageToken('오늘은 '),
+ createMessageToken(`${comparisonPeak}시대가 피크타임`, true, 'primary'),
+ createMessageToken('으로 예상돼요.'),
+ ];
+ }
+
+ return [
+ createMessageToken('지금까지 주문이 가장 몰린 시간은 '),
+ createMessageToken(`${todayPeak}시대`, true, 'primary'),
+ createMessageToken('예요.'),
+ ];
+};
diff --git a/frontend/src/utils/sales/dashboard-sales-pattern/index.ts b/frontend/src/utils/sales/dashboard-sales-pattern/index.ts
new file mode 100644
index 000000000..aec3990e5
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard-sales-pattern/index.ts
@@ -0,0 +1,3 @@
+export { getSalesPatternPeakTimeMessage } from './getSalesPatternPeakTimeMessage';
+export { createPeakTimeSeries } from './createPeakTimeSeries';
+export { getSalesPatternByDayMessage } from './getSalesPatternByDayMessage';
diff --git a/frontend/src/utils/sales/dashboard/createMessageToken.ts b/frontend/src/utils/sales/dashboard/createMessageToken.ts
new file mode 100644
index 000000000..e3b262dc3
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard/createMessageToken.ts
@@ -0,0 +1,20 @@
+import {
+ BRIEFING_MESSAGE_HIGHLIGHT_COLOR,
+ type BriefingMessageHighlightColor,
+} from '@/constants/sales';
+
+export const createMessageToken = (
+ text: string,
+ isHighlight?: boolean,
+ highlight?: BriefingMessageHighlightColor,
+) => {
+ return {
+ text,
+ isHighlight,
+ highlightColor: highlight
+ ? BRIEFING_MESSAGE_HIGHLIGHT_COLOR[highlight]
+ : BRIEFING_MESSAGE_HIGHLIGHT_COLOR.primary,
+ };
+};
+
+export type MessageToken = ReturnType;
diff --git a/frontend/src/utils/sales/dashboard/index.ts b/frontend/src/utils/sales/dashboard/index.ts
new file mode 100644
index 000000000..b61c87692
--- /dev/null
+++ b/frontend/src/utils/sales/dashboard/index.ts
@@ -0,0 +1 @@
+export { createMessageToken, type MessageToken } from './createMessageToken';
diff --git a/frontend/src/utils/sales/index.ts b/frontend/src/utils/sales/index.ts
index 2b69505a3..3c90c909b 100644
--- a/frontend/src/utils/sales/index.ts
+++ b/frontend/src/utils/sales/index.ts
@@ -1 +1,8 @@
export { getPeriodComparisonMessage } from './sales-overview';
+export {
+ getSalesPatternPeakTimeMessage,
+ createPeakTimeSeries,
+ getSalesPatternByDayMessage,
+} from './dashboard-sales-pattern';
+export { getSalesCurrentComparisonMessage } from './dashboard-current-sales';
+export { getSalesIncomeStructureComparisonMessage } from './dashboard-sales-income';
diff --git a/frontend/src/utils/shared/assertNever.ts b/frontend/src/utils/shared/assertNever.ts
new file mode 100644
index 000000000..d2f8ed00a
--- /dev/null
+++ b/frontend/src/utils/shared/assertNever.ts
@@ -0,0 +1,3 @@
+export const assertNever = (value: never, message?: string): never => {
+ throw new Error(message ?? `Unhandled case: ${String(value)}`);
+};
diff --git a/frontend/src/utils/shared/getCoordinate.ts b/frontend/src/utils/shared/getCoordinate.ts
index f03b78d56..1bc8800f1 100644
--- a/frontend/src/utils/shared/getCoordinate.ts
+++ b/frontend/src/utils/shared/getCoordinate.ts
@@ -2,7 +2,7 @@ import type { ChartSeries } from '@/types/shared';
// 바, 라인 그래프에서 사용되는 데이터별 좌표 점 계산 유틸
interface GetCoordinateArgs {
- svgRect: DOMRect;
+ svgWidth: number;
adjustedHeight: number;
series: T;
maximumY: number;
@@ -14,12 +14,11 @@ interface Coordinate {
}
export const getCoordinate = ({
- svgRect,
+ svgWidth,
adjustedHeight,
series,
maximumY,
}: GetCoordinateArgs): Coordinate[] => {
- const { width: svgWidth } = svgRect;
const xDataLength = series.data.mainX.length;
const intervalX = svgWidth / xDataLength;
diff --git a/frontend/src/utils/shared/index.ts b/frontend/src/utils/shared/index.ts
index 8a5da5604..256dcb208 100644
--- a/frontend/src/utils/shared/index.ts
+++ b/frontend/src/utils/shared/index.ts
@@ -37,6 +37,7 @@ export {
} from './doughnut-chart';
export { createPeriodTypeProvider } from './period-select';
+export { assertNever } from './assertNever';
export { getCoordinate } from './getCoordinate';
export {
getBarSegmentInfoList,
diff --git a/frontend/src/utils/shared/line-chart/getXCoordinate.ts b/frontend/src/utils/shared/line-chart/getXCoordinate.ts
index e601ac49e..cb738e669 100644
--- a/frontend/src/utils/shared/line-chart/getXCoordinate.ts
+++ b/frontend/src/utils/shared/line-chart/getXCoordinate.ts
@@ -1,14 +1,12 @@
import type { Coordinate } from '@/types/shared';
export const getXCoordinate = ({
- svgRect,
+ svgWidth,
xDataLength,
}: {
- svgRect: DOMRect;
+ svgWidth: number;
xDataLength: number;
}): Coordinate[] => {
- const { width: svgWidth } = svgRect;
-
const intervalX = svgWidth / xDataLength;
const lastX = intervalX * (xDataLength - 1);
const offsetX = (svgWidth - lastX) / 2;
diff --git a/frontend/src/utils/shared/period-select/createPeriodTypeProvider.tsx b/frontend/src/utils/shared/period-select/createPeriodTypeProvider.tsx
index ce6549a4a..1faa663ba 100644
--- a/frontend/src/utils/shared/period-select/createPeriodTypeProvider.tsx
+++ b/frontend/src/utils/shared/period-select/createPeriodTypeProvider.tsx
@@ -32,15 +32,8 @@ export const createPeriodTypeProvider = ({
periodPreset,
}: createPeriodTypeProviderOptions) => {
const periodTypeContext = createContext<
- PeriodTypeContextState & PeriodTypeContextAction
- >({
- periodType: undefined,
- startDate: undefined,
- endDate: undefined,
- setPeriodType: () => {},
- setStartDate: () => {},
- setEndDate: () => {},
- });
+ (PeriodTypeContextState & PeriodTypeContextAction) | undefined
+ >(undefined);
const usePeriodTypeContext = () => {
const context = useContext(periodTypeContext);
@@ -49,7 +42,7 @@ export const createPeriodTypeProvider = ({
throw new Error('periodTypeContext not found');
}
- return context as PeriodTypeContextState & PeriodTypeContextAction;
+ return context;
};
const PeriodTypeProvider = ({ children }: PropsWithChildren) => {