diff --git a/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx b/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx index d1e16eb01..325aef1a8 100644 --- a/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx +++ b/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx @@ -7,9 +7,12 @@ import { } from '@/constants/dashboard'; import { useEditCard } from '@/hooks/dashboard'; +import { EditCardContent } from './EditCardContent'; + interface CardEditViewCardProps { cardCode: MetricCardCode; } + export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => { const { addCard, removeCard, isAdded } = useEditCard(); @@ -29,7 +32,7 @@ export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => { return null; // 카드 정보가 없는 경우 렌더링하지 않음 } - const { code, label, type, period, sizeX, sizeY } = card; + const { period, sizeX } = card; return (
  • @@ -38,17 +41,10 @@ export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => { period={period} className="min-w-full" sizeX={sizeX} - sizeY={sizeY} onClickAddButton={handleAddCard} onClickDeleteButton={handleDeleteCard} > - {label} -
    - {code} -
    - {type} -
    - {sizeX} x {sizeY} +
  • ); diff --git a/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx b/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx new file mode 100644 index 000000000..7668a7eb6 --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx @@ -0,0 +1,157 @@ +import { + AveragePriceContent, + OrderCountContent, + OrderMethodContent, + PaymentMethodContent, + PeakTimeContent, + RealSalesContent, + SalesByDayContent, + SalesTypeContent, +} from '@/components/sales'; +import type { MetricCardCode } from '@/constants/dashboard'; +import { + AVERAGE_PRICE, + ORDER_COUNT, + ORDER_METHOD, + PAYMENT_METHOD, + PEAK_TIME, + REAL_SALES, + SALES_BY_DAY, + SALES_TYPE, +} from '@/constants/sales'; + +interface EditCardContentProps { + cardCode: MetricCardCode; +} + +const { + EXAMPLE_AMOUNT: REAL_SALES_EXAMPLE_AMOUNT, + EXAMPLE_CHANGE_RATE: REAL_SALES_EXAMPLE_CHANGE_RATE, + EXAMPLE_HAS_PREVIOUS_DATA: REAL_SALES_EXAMPLE_HAS_PREVIOUS_DATA, +} = REAL_SALES; +const { + EXAMPLE_AMOUNT: ORDER_COUNT_EXAMPLE_AMOUNT, + EXAMPLE_CHANGE_RATE: ORDER_COUNT_EXAMPLE_CHANGE_RATE, + EXAMPLE_HAS_PREVIOUS_DATA: ORDER_COUNT_EXAMPLE_HAS_PREVIOUS_DATA, +} = ORDER_COUNT; +const { + EXAMPLE_AMOUNT: AVERAGE_PRICE_EXAMPLE_AMOUNT, + EXAMPLE_COMPARISON_AMOUNT: AVERAGE_PRICE_EXAMPLE_COMPARISON_AMOUNT, + EXAMPLE_HAS_PREVIOUS_DATA: AVERAGE_PRICE_EXAMPLE_HAS_PREVIOUS_DATA, +} = AVERAGE_PRICE; +const { + EXAMPLE_TOP_TYPE: SALES_TYPE_EXAMPLE_TOP_TYPE, + EXAMPLE_TOP_SHARE: SALES_TYPE_EXAMPLE_TOP_SHARE, + EXAMPLE_DELTA_SHARE: SALES_TYPE_EXAMPLE_DELTA_SHARE, + EXAMPLE_SALES_SOURCE_DATA: SALES_TYPE_EXAMPLE_SALES_SOURCE_DATA, +} = SALES_TYPE; +const { + EXAMPLE_TOP_TYPE: ORDER_METHOD_EXAMPLE_TOP_TYPE, + EXAMPLE_TOP_SHARE: ORDER_METHOD_EXAMPLE_TOP_SHARE, + EXAMPLE_DELTA_SHARE: ORDER_METHOD_EXAMPLE_DELTA_SHARE, + EXAMPLE_ORDER_METHOD_DATA: ORDER_METHOD_EXAMPLE_ORDER_METHOD_DATA, +} = ORDER_METHOD; +const { + EXAMPLE_TOP_TYPE: PAYMENT_METHOD_EXAMPLE_TOP_TYPE, + EXAMPLE_TOP_SHARE: PAYMENT_METHOD_EXAMPLE_TOP_SHARE, + EXAMPLE_DELTA_SHARE: PAYMENT_METHOD_EXAMPLE_DELTA_SHARE, + EXAMPLE_PAYMENT_METHOD_DATA: PAYMENT_METHOD_EXAMPLE_PAYMENT_METHOD_DATA, +} = PAYMENT_METHOD; +const { EXAMPLE_DATA: PEAK_TIME_EXAMPLE_DATA } = PEAK_TIME; +const { + EXAMPLE_DATA: SALES_BY_DAY_EXAMPLE_DATA, + EXAMPLE_TOP_DAY: SALES_BY_DAY_EXAMPLE_TOP_DAY, + EXAMPLE_IS_SIGNIFICANT: SALES_BY_DAY_EXAMPLE_IS_SIGNIFICANT, +} = SALES_BY_DAY; + +export const EditCardContent = ({ cardCode }: EditCardContentProps) => { + switch (cardCode) { + case 'SLS_01_01': + case 'SLS_01_02': + case 'SLS_01_03': + return ( + + ); + case 'SLS_02_01': + case 'SLS_02_02': + case 'SLS_02_03': + return ( + + ); + case 'SLS_03_01': + case 'SLS_03_02': + case 'SLS_03_03': + return ( + + ); + case 'SLS_06_01': + case 'SLS_06_02': + case 'SLS_06_03': + return ( + + ); + case 'SLS_07_01': + case 'SLS_07_02': + case 'SLS_07_03': + return ( + + ); + case 'SLS_08_01': + case 'SLS_08_02': + case 'SLS_08_03': + return ( + + ); + case 'SLS_13_01': + return ; + case 'SLS_14_06': + return ( + + ); + default: + return null; + } +}; diff --git a/frontend/src/components/sales/dashboard-current-sales/AveragePriceContent.tsx b/frontend/src/components/sales/dashboard-current-sales/AveragePriceContent.tsx new file mode 100644 index 000000000..8bf6c18ef --- /dev/null +++ b/frontend/src/components/sales/dashboard-current-sales/AveragePriceContent.tsx @@ -0,0 +1,64 @@ +import { + DASHBOARD_METRIC_CARDS, + DASHBOARD_METRICS, + type ExtractCardCodes, +} from '@/constants/dashboard'; +import { AVERAGE_PRICE, SALES_UNIT } from '@/constants/sales'; +import type { GetAveragePriceResponseDto } from '@/types/sales'; +import { getMetricTrend } from '@/utils/dashboard'; +import { getSalesCurrentComparisonMessage } from '@/utils/sales'; + +import { CurrentSalesContent } from './CurrentSalesContent'; + +const { + EXAMPLE_AMOUNT, + EXAMPLE_COMPARISON_AMOUNT, + EXAMPLE_HAS_PREVIOUS_DATA, + MIN_CHANGE_RATE, + MAX_CHANGE_RATE, + METRIC_LABEL, +} = AVERAGE_PRICE; + +type AveragePriceCardCodes = ExtractCardCodes< + typeof DASHBOARD_METRICS.SALES.sections.CURRENT_SALES.items.AVERAGE_PRICE +>; + +interface AveragePriceContentProps extends GetAveragePriceResponseDto { + cardCode: AveragePriceCardCodes; + className?: string; +} + +export const AveragePriceContent = ({ + cardCode, + averageOrderAmount = EXAMPLE_AMOUNT, + differenceAmount = EXAMPLE_COMPARISON_AMOUNT, + hasPreviousData = EXAMPLE_HAS_PREVIOUS_DATA, + className, +}: AveragePriceContentProps) => { + const periodType = DASHBOARD_METRIC_CARDS[cardCode].period; + const metricTrend = getMetricTrend({ + comparisonAmount: differenceAmount, + minValue: MIN_CHANGE_RATE, + maxValue: MAX_CHANGE_RATE, + }); + const comparisonMessageTokens = getSalesCurrentComparisonMessage({ + periodType, + hasPreviousData, + metricTrend, + metricLabel: METRIC_LABEL, + comparisonAmount: differenceAmount, + unit: SALES_UNIT.WON, + }); + return ( + + + + + + ); +}; diff --git a/frontend/src/components/sales/dashboard-current-sales/CurrentSalesContent.tsx b/frontend/src/components/sales/dashboard-current-sales/CurrentSalesContent.tsx new file mode 100644 index 000000000..bc7f9f7db --- /dev/null +++ b/frontend/src/components/sales/dashboard-current-sales/CurrentSalesContent.tsx @@ -0,0 +1,107 @@ +import { type ReactNode, useMemo } from 'react'; + +import { METRIC_TREND, type MetricTrend } from '@/constants/dashboard'; +import { CDN_BASE_URL } from '@/constants/shared'; +import type { MessageToken } from '@/utils/sales/dashboard/createMessageToken'; +import { cn, formatNumber } from '@/utils/shared'; + +interface CurrentSalesContentProps { + className?: string; + children?: ReactNode; +} + +export const CurrentSalesContent = ({ + children, + className, +}: CurrentSalesContentProps) => { + return ( +
    + {children} +
    + ); +}; + +interface CurrentSalesTrendBadgeProps { + trend: MetricTrend; +} + +const CurrentSalesTrendBadge = ({ trend }: CurrentSalesTrendBadgeProps) => { + const iconSrc = useMemo(() => { + switch (trend) { + case METRIC_TREND.UP: + return '/assets/images/graph_up.svg'; + case METRIC_TREND.DOWN: + return '/assets/images/graph_down.svg'; + case METRIC_TREND.SAME: + return '/assets/images/graph_same.svg'; + default: + return null; + } + }, [trend]); + + if (!iconSrc) { + return
    ; + } + + return ; +}; + +interface CurrentSalesContentAmountProps { + amount: number; + unit: string; + className?: string; +} + +const CurrentSalesContentAmount = ({ + amount, + unit, + className, +}: CurrentSalesContentAmountProps) => { + return ( + + + {formatNumber(amount)} + + {unit} + + ); +}; + +interface CurrentSalesContentComparisonMessageProps { + comparisonMessageTokens: MessageToken[]; + className?: string; +} + +const CurrentSalesContentComparisonMessage = ({ + comparisonMessageTokens, + className, +}: CurrentSalesContentComparisonMessageProps) => { + return ( +

    + {comparisonMessageTokens.map( + ({ text, isHighlight, highlightColor }, index) => { + return ( + + {text} + + ); + }, + )} +

    + ); +}; + +CurrentSalesContent.TrendBadge = CurrentSalesTrendBadge; +CurrentSalesContent.Amount = CurrentSalesContentAmount; +CurrentSalesContent.ComparisonMessage = CurrentSalesContentComparisonMessage; diff --git a/frontend/src/components/sales/dashboard-current-sales/OrderCountContent.tsx b/frontend/src/components/sales/dashboard-current-sales/OrderCountContent.tsx new file mode 100644 index 000000000..82656ecc2 --- /dev/null +++ b/frontend/src/components/sales/dashboard-current-sales/OrderCountContent.tsx @@ -0,0 +1,66 @@ +import { + DASHBOARD_METRIC_CARDS, + DASHBOARD_METRICS, + type ExtractCardCodes, +} from '@/constants/dashboard'; +import { ORDER_COUNT, SALES_UNIT } from '@/constants/sales'; +import type { GetOrderCountResponseDto } from '@/types/sales'; +import { getMetricTrend } from '@/utils/dashboard'; +import { getSalesCurrentComparisonMessage } from '@/utils/sales'; + +import { CurrentSalesContent } from './CurrentSalesContent'; + +const { + METRIC_LABEL, + MIN_CHANGE_RATE, + MAX_CHANGE_RATE, + EXAMPLE_AMOUNT, + EXAMPLE_CHANGE_RATE, + EXAMPLE_HAS_PREVIOUS_DATA, +} = ORDER_COUNT; + +type OrderCountCardCodes = ExtractCardCodes< + typeof DASHBOARD_METRICS.SALES.sections.CURRENT_SALES.items.ORDER_COUNT +>; + +interface OrderCountContentProps extends Omit< + GetOrderCountResponseDto, + 'differenceOrderCount' +> { + cardCode: OrderCountCardCodes; + className?: string; +} + +export const OrderCountContent = ({ + cardCode, + orderCount = EXAMPLE_AMOUNT, + changeRate = EXAMPLE_CHANGE_RATE, + hasPreviousData = EXAMPLE_HAS_PREVIOUS_DATA, + className, +}: OrderCountContentProps) => { + const periodType = DASHBOARD_METRIC_CARDS[cardCode].period; + + const metricTrend = getMetricTrend({ + comparisonAmount: changeRate, + minValue: MIN_CHANGE_RATE, + maxValue: MAX_CHANGE_RATE, + }); + + const comparisonMessageTokens = getSalesCurrentComparisonMessage({ + periodType, + hasPreviousData, + metricTrend, + metricLabel: METRIC_LABEL, + comparisonAmount: changeRate, + unit: SALES_UNIT.PERCENT, + }); + return ( + + + + + + ); +}; diff --git a/frontend/src/components/sales/dashboard-current-sales/RealSalesContent.tsx b/frontend/src/components/sales/dashboard-current-sales/RealSalesContent.tsx new file mode 100644 index 000000000..337b07628 --- /dev/null +++ b/frontend/src/components/sales/dashboard-current-sales/RealSalesContent.tsx @@ -0,0 +1,67 @@ +import { + DASHBOARD_METRIC_CARDS, + DASHBOARD_METRICS, + type ExtractCardCodes, +} from '@/constants/dashboard'; +import { REAL_SALES, SALES_UNIT } from '@/constants/sales'; +import type { GetRealTimeSalesResponseDto } from '@/types/sales'; +import { getMetricTrend } from '@/utils/dashboard'; +import { getSalesCurrentComparisonMessage } from '@/utils/sales'; + +import { CurrentSalesContent } from './CurrentSalesContent'; + +const { + METRIC_LABEL, + MIN_CHANGE_RATE, + MAX_CHANGE_RATE, + EXAMPLE_AMOUNT, + EXAMPLE_CHANGE_RATE, + EXAMPLE_HAS_PREVIOUS_DATA, +} = REAL_SALES; + +type RealSalesCardCodes = ExtractCardCodes< + typeof DASHBOARD_METRICS.SALES.sections.CURRENT_SALES.items.REAL_SALES +>; + +interface RealSalesContentProps extends Omit< + GetRealTimeSalesResponseDto, + 'differenceAmount' +> { + cardCode: RealSalesCardCodes; + className?: string; +} + +export const RealSalesContent = ({ + cardCode, + netAmount = EXAMPLE_AMOUNT, + changeRate = EXAMPLE_CHANGE_RATE, + hasPreviousData = EXAMPLE_HAS_PREVIOUS_DATA, + className, +}: RealSalesContentProps) => { + const periodType = DASHBOARD_METRIC_CARDS[cardCode].period; + + const metricTrend = getMetricTrend({ + comparisonAmount: changeRate, + minValue: MIN_CHANGE_RATE, + maxValue: MAX_CHANGE_RATE, + }); + + const comparisonMessageTokens = getSalesCurrentComparisonMessage({ + periodType, + hasPreviousData, + metricTrend, + metricLabel: METRIC_LABEL, + comparisonAmount: changeRate, + unit: SALES_UNIT.PERCENT, + }); + + return ( + + + + + + ); +}; diff --git a/frontend/src/components/sales/dashboard-current-sales/index.ts b/frontend/src/components/sales/dashboard-current-sales/index.ts new file mode 100644 index 000000000..b684b5622 --- /dev/null +++ b/frontend/src/components/sales/dashboard-current-sales/index.ts @@ -0,0 +1,3 @@ +export { RealSalesContent } from './RealSalesContent'; +export { OrderCountContent } from './OrderCountContent'; +export { AveragePriceContent } from './AveragePriceContent'; diff --git a/frontend/src/components/sales/dashboard-sales-income/DashboardSalesIncomeContent.tsx b/frontend/src/components/sales/dashboard-sales-income/DashboardSalesIncomeContent.tsx new file mode 100644 index 000000000..b72bbf7c5 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-income/DashboardSalesIncomeContent.tsx @@ -0,0 +1,112 @@ +import type { ReactNode } from 'react'; + +import { DoughnutChart } from '@/components/shared'; +import { PERIOD_PRESETS } from '@/constants/shared'; +import type { SalesIncomeStructureInsight, SalesSource } from '@/types/sales'; +import type { DoughnutChartItem } from '@/types/shared'; +import { getSalesIncomeStructureComparisonMessage } from '@/utils/sales'; +import { cn, type ValueOf } from '@/utils/shared'; + +import { SalesSourceChartLegend } from '../sales-source'; + +interface DashboardSalesIncomeContentProps { + className?: string; + children?: ReactNode; +} + +export const DashboardSalesIncomeContent = ({ + className, + children, +}: DashboardSalesIncomeContentProps) => { + return ( +
    + {children} +
    + ); +}; + +interface DashboardSalesIncomeContentComparisonMessageProps { + periodType: ValueOf; + topType: SalesIncomeStructureInsight['topType']; + topShare: SalesIncomeStructureInsight['topShare']; + deltaShare: SalesIncomeStructureInsight['deltaShare']; +} + +export const DashboardSalesIncomeContentComparisonMessage = ({ + periodType, + topType, + topShare, + deltaShare, +}: DashboardSalesIncomeContentComparisonMessageProps) => { + const comparisonMessageTokens = getSalesIncomeStructureComparisonMessage({ + periodType, + topType, + topShare, + deltaShare, + }); + + return ( +

    + {comparisonMessageTokens.map( + ({ text, isHighlight, highlightColor }, index) => { + return ( + + {text} + + ); + }, + )} +

    + ); +}; + +interface DashboardSalesIncomeContentDoughnutChartProps { + periodType: ValueOf; + chartData: DoughnutChartItem[]; + salesSourceData: SalesSource[]; + title: string; +} + +export const DashboardSalesIncomeContentDoughnutChart = ({ + periodType, + chartData, + salesSourceData, + title, +}: DashboardSalesIncomeContentDoughnutChartProps) => { + return ( +
    +
    + +
    + +
    + ); +}; + +DashboardSalesIncomeContent.ComparisonMessage = + DashboardSalesIncomeContentComparisonMessage; + +DashboardSalesIncomeContent.DoughnutChart = + DashboardSalesIncomeContentDoughnutChart; diff --git a/frontend/src/components/sales/dashboard-sales-income/OrderMethodContent.tsx b/frontend/src/components/sales/dashboard-sales-income/OrderMethodContent.tsx new file mode 100644 index 000000000..5ca7f662d --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-income/OrderMethodContent.tsx @@ -0,0 +1,57 @@ +import { + DASHBOARD_METRIC_CARDS, + DASHBOARD_METRICS, + type ExtractCardCodes, +} from '@/constants/dashboard'; +import { ORDER_METHOD, SALES_SOURCE_COLORS } from '@/constants/sales'; +import type { GetIncomeStructureByOrderMethodResponseDto } from '@/types/sales'; + +import { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent'; + +const { DOUGHNUT_CHART_TITLE } = ORDER_METHOD; + +type OrderMethodCardCodes = ExtractCardCodes< + typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.ORDER_METHOD +>; + +interface OrderMethodContentProps extends GetIncomeStructureByOrderMethodResponseDto { + cardCode: OrderMethodCardCodes; +} + +export const OrderMethodContent = ({ + cardCode, + insight, + items, +}: OrderMethodContentProps) => { + const periodType = DASHBOARD_METRIC_CARDS[cardCode].period; + + const orderMethodData = items.map((item) => ({ + salesSourceType: item.orderChannel, + revenue: item.salesAmount, + count: item.orderCount, + changeRate: item.deltaShare, + })); + + const chartData = orderMethodData.map((data) => ({ + label: data.salesSourceType, + value: data.revenue, + color: SALES_SOURCE_COLORS[data.salesSourceType], + })); + + return ( + + + + + ); +}; diff --git a/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContent.tsx b/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContent.tsx new file mode 100644 index 000000000..26e49de50 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContent.tsx @@ -0,0 +1,57 @@ +import { + DASHBOARD_METRIC_CARDS, + DASHBOARD_METRICS, + type ExtractCardCodes, +} from '@/constants/dashboard'; +import { PAYMENT_METHOD, SALES_SOURCE_COLORS } from '@/constants/sales'; +import type { GetIncomeStructureByPaymentMethodResponseDto } from '@/types/sales'; + +import { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent'; + +const { DOUGHNUT_CHART_TITLE } = PAYMENT_METHOD; + +type PaymentMethodCardCodes = ExtractCardCodes< + typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.PAYMENT_METHOD +>; + +interface PaymentMethodContentProps extends GetIncomeStructureByPaymentMethodResponseDto { + cardCode: PaymentMethodCardCodes; +} + +export const PaymentMethodContent = ({ + cardCode, + insight, + items, +}: PaymentMethodContentProps) => { + const periodType = DASHBOARD_METRIC_CARDS[cardCode].period; + + const paymentMethodData = items.map((item) => ({ + salesSourceType: item.payMethod, + revenue: item.salesAmount, + count: item.orderCount, + changeRate: item.deltaShare, + })); + + const chartData = paymentMethodData.map((data) => ({ + label: data.salesSourceType, + value: data.revenue, + color: SALES_SOURCE_COLORS[data.salesSourceType], + })); + + return ( + + + + + ); +}; diff --git a/frontend/src/components/sales/dashboard-sales-income/SalesTypeContent.tsx b/frontend/src/components/sales/dashboard-sales-income/SalesTypeContent.tsx new file mode 100644 index 000000000..310cb20f4 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-income/SalesTypeContent.tsx @@ -0,0 +1,57 @@ +import { + DASHBOARD_METRIC_CARDS, + DASHBOARD_METRICS, + type ExtractCardCodes, +} from '@/constants/dashboard'; +import { SALES_SOURCE_COLORS, SALES_TYPE } from '@/constants/sales'; +import type { GetIncomeStructureBySalesTypeResponseDto } from '@/types/sales'; + +import { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent'; + +const { DOUGHNUT_CHART_TITLE } = SALES_TYPE; + +type DashboardSalesIncomeCardCodes = ExtractCardCodes< + typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.SALES_TYPE +>; + +interface SalesTypeContentProps extends GetIncomeStructureBySalesTypeResponseDto { + cardCode: DashboardSalesIncomeCardCodes; +} + +export const SalesTypeContent = ({ + cardCode, + insight, + items, +}: SalesTypeContentProps) => { + const periodType = DASHBOARD_METRIC_CARDS[cardCode].period; + + const salesTypeData = items.map((item) => ({ + salesSourceType: item.salesType, + revenue: item.salesAmount, + count: item.orderCount, + changeRate: item.deltaShare, + })); + + const chartData = salesTypeData.map((data) => ({ + label: data.salesSourceType, + value: data.revenue, + color: SALES_SOURCE_COLORS[data.salesSourceType], + })); + + return ( + + + + + ); +}; diff --git a/frontend/src/components/sales/dashboard-sales-income/index.ts b/frontend/src/components/sales/dashboard-sales-income/index.ts new file mode 100644 index 000000000..108d2f0e2 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-income/index.ts @@ -0,0 +1,4 @@ +export { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent'; +export { SalesTypeContent } from './SalesTypeContent'; +export { OrderMethodContent } from './OrderMethodContent'; +export { PaymentMethodContent } from './PaymentMethodContent'; diff --git a/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeChartCaption.tsx b/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeChartCaption.tsx new file mode 100644 index 000000000..07cdf9f84 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeChartCaption.tsx @@ -0,0 +1,32 @@ +import { cn } from '@/utils/shared'; + +interface PeakTimeChartCaptionProps { + label: string; + color: 'primary' | 'default'; +} + +export const PeakTimeChartCaption = ({ + label, + color, +}: PeakTimeChartCaptionProps) => { + return ( +
    +
    + + {label} + +
    + ); +}; diff --git a/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeContent.tsx b/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeContent.tsx new file mode 100644 index 000000000..27b4508b6 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeContent.tsx @@ -0,0 +1,98 @@ +import { useMemo } from 'react'; + +import { LineChart } from '@/components/shared'; +import { DAY_OF_WEEK_LIST } from '@/constants/shared'; +import type { GetDetailPeakTimeResponseDto } from '@/types/sales'; +import { + createPeakTimeSeries, + getSalesPatternPeakTimeMessage, +} from '@/utils/sales'; +import { cn } from '@/utils/shared'; + +import { PeakTimeChartCaption } from './PeakTimeChartCaption'; + +interface PeakTimeContentProps { + peakTimeData: GetDetailPeakTimeResponseDto; + className?: string; +} + +export const PeakTimeContent = ({ + peakTimeData, + className, +}: PeakTimeContentProps) => { + const weekday = DAY_OF_WEEK_LIST[new Date().getDay()]; + + const { + todayItems, + week4Items, + todayPeak, + comparisonPeak, + beforeComparisonPeak, + } = peakTimeData; + + const peakTimeBriefingMessage = getSalesPatternPeakTimeMessage({ + todayPeak, + comparisonPeak, + beforeComparisonPeak, + }); + + const primarySeries = useMemo(() => { + return { + ...createPeakTimeSeries(todayItems, 'var(--color-brand-main)'), + }; + }, [todayItems]); + + const secondarySeries = useMemo(() => { + return { + ...createPeakTimeSeries(week4Items, 'var(--color-grey-400)'), + }; + }, [week4Items]); + + return ( +
    +
    + + +
    +
    + +
    +

    + {peakTimeBriefingMessage.map( + ({ text, isHighlight, highlightColor }, index) => { + return ( + + {text} + + ); + }, + )} +

    +
    + ); +}; diff --git a/frontend/src/components/sales/dashboard-sales-pattern/SalesByDayContent.tsx b/frontend/src/components/sales/dashboard-sales-pattern/SalesByDayContent.tsx new file mode 100644 index 000000000..1bf8f27d2 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-pattern/SalesByDayContent.tsx @@ -0,0 +1,81 @@ +import { BarChart } from '@/components/shared/bar-chart'; +import { SALES_BY_DAY } from '@/constants/sales'; +import type { SalesByDayItem, SalesByDaySummary } from '@/types/sales'; +import { getSalesPatternByDayMessage } from '@/utils/sales'; +import { cn } from '@/utils/shared'; + +const { CHART_X_UNIT, CHART_Y_UNIT, CHART_COLOR } = SALES_BY_DAY; + +interface SalesByDayContentProps extends SalesByDaySummary { + salesByDayItems: SalesByDayItem[]; + className?: string; +} + +export const SalesByDayContent = ({ + salesByDayItems, + topDay, + isSignificant, + className, +}: SalesByDayContentProps) => { + const salesByDayBriefingMessage = getSalesPatternByDayMessage({ + topDay, + isSignificant, + }); + + const salesByDaySeries = { + data: { + mainX: salesByDayItems.map((item) => ({ + amount: item.day, + unit: CHART_X_UNIT, + })), + mainY: salesByDayItems.map((item) => ({ + amount: item.avgNetAmount, + unit: CHART_Y_UNIT, + })), + }, + color: CHART_COLOR, + }; + + const activeDataIndex = salesByDayItems.findIndex( + (item) => item.day === topDay, + ); + + return ( +
    + +

    + {salesByDayBriefingMessage.map( + ({ text, isHighlight, highlightColor }, index) => { + return ( + + {text} + + ); + }, + )} +

    +
    + ); +}; diff --git a/frontend/src/components/sales/dashboard-sales-pattern/index.ts b/frontend/src/components/sales/dashboard-sales-pattern/index.ts new file mode 100644 index 000000000..025eb1f50 --- /dev/null +++ b/frontend/src/components/sales/dashboard-sales-pattern/index.ts @@ -0,0 +1,2 @@ +export { PeakTimeContent } from './PeakTimeContent'; +export { SalesByDayContent } from './SalesByDayContent'; diff --git a/frontend/src/components/sales/index.ts b/frontend/src/components/sales/index.ts index 4370df7ea..28b23e4fd 100644 --- a/frontend/src/components/sales/index.ts +++ b/frontend/src/components/sales/index.ts @@ -1,4 +1,16 @@ export { SalesOverview } from './sales-overview'; export { SalesPatterns } from './sales-patterns'; -export { SalesSource } from './sales-source'; +export { SalesSource, SalesSourceChartLegend } from './sales-source'; export { SalesTrends } from './sales-trends'; +export { + AveragePriceContent, + OrderCountContent, + RealSalesContent, +} from './dashboard-current-sales'; +export { DashboardSalesIncomeContent } from './dashboard-sales-income'; +export { + SalesTypeContent, + OrderMethodContent, + PaymentMethodContent, +} from './dashboard-sales-income'; +export { PeakTimeContent, SalesByDayContent } from './dashboard-sales-pattern'; diff --git a/frontend/src/components/sales/sales-source/index.ts b/frontend/src/components/sales/sales-source/index.ts index 52d6affa6..efd7dce8a 100644 --- a/frontend/src/components/sales/sales-source/index.ts +++ b/frontend/src/components/sales/sales-source/index.ts @@ -1 +1,2 @@ export { SalesSource } from './SalesSource'; +export { SalesSourceChart, SalesSourceChartLegend } from './sales-source-chart'; diff --git a/frontend/src/components/sales/sales-source/sales-source-chart/index.ts b/frontend/src/components/sales/sales-source/sales-source-chart/index.ts index 7c2a81b91..79eb06950 100644 --- a/frontend/src/components/sales/sales-source/sales-source-chart/index.ts +++ b/frontend/src/components/sales/sales-source/sales-source-chart/index.ts @@ -1 +1,2 @@ export { SalesSourceChart } from './SalesSourceChart'; +export { SalesSourceChartLegend } from './SalesSourceChartLegend'; diff --git a/frontend/src/components/shared/bar-chart/BarChart.stories.tsx b/frontend/src/components/shared/bar-chart/BarChart.stories.tsx index 70f794137..4c7ef0327 100644 --- a/frontend/src/components/shared/bar-chart/BarChart.stories.tsx +++ b/frontend/src/components/shared/bar-chart/BarChart.stories.tsx @@ -77,7 +77,7 @@ export const Default: Story = { chartTitle: '일별 매출 꺾은선 차트', chartDescription: '일별 매출 꺾은선 차트 설명', xAxisType: 'right-arrow', - activeLastData: true, + activeDataIndex: 3, barColorChangeOnHover: true, }, render: (args) => ( @@ -108,7 +108,7 @@ export const StackBar: Story = { chartTitle: '일별 매출 꺾은선 차트', chartDescription: '일별 매출 꺾은선 차트 설명', xAxisType: 'right-arrow', - activeLastData: true, + activeDataIndex: 3, barColorChangeOnHover: true, }, render: (args) => ( @@ -130,6 +130,10 @@ const RealtimeBarChart = (args: Story['args']) => { args.barChartSeries as BarChartSeries, ); + const [activeDataIndex, setActiveDataIndex] = useState( + args.barChartSeries.data.mainX.length - 1, + ); + const handleUpdateCurrentBarChartSeries = () => { let currentIndex = barChartSeries.data.mainY.filter((datum) => datum.amount !== null) @@ -139,6 +143,8 @@ const RealtimeBarChart = (args: Story['args']) => { currentIndex = 0; } + setActiveDataIndex(currentIndex); + setBarChartSeries((prev) => { const newMainY = [...prev.data.mainY]; //const newSubY = [...prev.data.subY]; @@ -177,6 +183,8 @@ const RealtimeBarChart = (args: Story['args']) => { // return; // } + setActiveDataIndex(nextIndex); + setBarChartSeries((prev) => ({ ...prev, data: { @@ -216,6 +224,7 @@ const RealtimeBarChart = (args: Story['args']) => { diff --git a/frontend/src/components/shared/bar-chart/BarChart.tsx b/frontend/src/components/shared/bar-chart/BarChart.tsx index ab9f2e6aa..f11d7655d 100644 --- a/frontend/src/components/shared/bar-chart/BarChart.tsx +++ b/frontend/src/components/shared/bar-chart/BarChart.tsx @@ -72,9 +72,9 @@ interface BarChartProps { */ hasBarLabel?: boolean; /** - * 가장 우측 막대 바 색상 강조할 것인지 여부 + * 현재 포커스된 데이터의 인덱스 */ - activeLastData?: boolean; + activeDataIndex?: number; /** * 바 호버 시 색상 변경할 건지 */ @@ -96,13 +96,13 @@ export const BarChart = ({ chartDescription, hasBarLabel = true, xAxisType, - activeLastData = true, + activeDataIndex, barColorChangeOnHover = true, }: BarChartProps) => { const { titleId, descId } = useBarChartId(); const { - svgRect, + svgWidth, adjustedHeight, xLabelList, xCoordinate, @@ -110,6 +110,8 @@ export const BarChart = ({ svgRef, xAxisRef, } = useBarChart({ + viewBoxWidth, + viewBoxHeight, barChartSeries, hasXAxis, }); @@ -132,17 +134,13 @@ export const BarChart = ({ {showYGuideLine && ( )} {showXGuideLine && ( - + )} {hasXAxis && ( <> @@ -171,7 +169,7 @@ export const BarChart = ({ xCoordinate={xCoordinate} hasXAxis={hasXAxis} hasBarLabel={hasBarLabel} - activeLastData={activeLastData} + activeDataIndex={activeDataIndex} barColorChangeOnHover={barColorChangeOnHover} /> diff --git a/frontend/src/components/shared/bar-chart/BarSeries.tsx b/frontend/src/components/shared/bar-chart/BarSeries.tsx index ecc342de8..c8b3a4213 100644 --- a/frontend/src/components/shared/bar-chart/BarSeries.tsx +++ b/frontend/src/components/shared/bar-chart/BarSeries.tsx @@ -25,7 +25,7 @@ interface BarSeriesProps { hasXAxis?: boolean; //현재 barchart에서 x축 사용하고 있는지 tooltipContent?: (...args: string[]) => string; xCoordinate: Coordinate[]; - activeLastData?: boolean; // 가장 우측 막대 바 색상 강조할 것인지 여부. 스택 바에는 적용 안됨. + activeDataIndex?: number; // 현재 포커스된 데이터의 인덱스 barColorChangeOnHover?: boolean; // 바 호버 시 색상 변경할 건지 } @@ -41,7 +41,7 @@ export const BarSeries = ({ xCoordinate, tooltipContent, activeTooltip, - activeLastData = true, + activeDataIndex, barColorChangeOnHover, }: BarSeriesProps) => { const { XAXIS_Y_OFFSET, XAXIS_STROKE_WIDTH, BAR_RADIUS } = BAR_CHART; // X축이 있을 때 X축의 Y좌표 오프셋 값 @@ -153,7 +153,7 @@ export const BarSeries = ({ activeTooltip={activeTooltip} tooltipContentText={tooltipContentText} // activeLastData가 true이라면 마지막 막대를 강조 표시 - isActive={activeLastData && index === coordinate.length - 1} + isActive={activeDataIndex === index} barColorChangeOnHover={barColorChangeOnHover} /> )} diff --git a/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.stories.tsx b/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.stories.tsx index 2f5495ecd..c22599b2b 100644 --- a/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.stories.tsx +++ b/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.stories.tsx @@ -43,10 +43,9 @@ export const Default: Story = { export const WithTitleIcon: Story = { args: { - width: 340, - height: 228, title: '오늘 날씨 예보', hasChevronRightIcon: true, children: , + className: 'w-85 h-57', }, }; diff --git a/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.tsx b/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.tsx index 2efffa228..bba029184 100644 --- a/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.tsx +++ b/frontend/src/components/shared/default-card-wrapper/DefaultCardWrapper.tsx @@ -12,8 +12,6 @@ interface DefaultCardWrapperProps extends ComponentProps<'article'> { hasChevronRightIcon?: boolean; onClickChevronRightIcon?: () => void; className?: string; - width?: number; - height?: number; } export const DefaultCardWrapper = ({ diff --git a/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx b/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx index 4c96b5972..86ca1b95a 100644 --- a/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx +++ b/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx @@ -16,9 +16,9 @@ interface EditCardWrapperProps { isAdded: boolean; children: ReactNode; className?: string; // 전체 wrapper 크기나 보더 등 스타일 + innerClassName?: string; // 자식 컴포넌트 클래스명 period: string; // 오늘, 이번주, 이번달 등 문구 sizeX?: number; // 가로 크기 - sizeY?: number; // 세로 크기 onClickDeleteButton?: () => void; // 대시보드에서 삭제하는 버튼 클릭 헨들러 onClickAddButton?: () => void; // 대시보드에 추가하는 버튼 클릭 핸들러 } @@ -29,8 +29,8 @@ export const EditCardWrapper = ({ isAdded, children, className, + innerClassName, sizeX = 1, - sizeY = 1, period = '기간없음', onClickDeleteButton, onClickAddButton, @@ -50,8 +50,7 @@ export const EditCardWrapper = ({ Math.max(EDIT_CARD_WRAPPER.MIN_WIDTH, computedCardWidth) * sizeX + GRID_GAP * (sizeX - 1), // 최소 너비 220px, gap 20px height: - Math.max(EDIT_CARD_WRAPPER.MIN_HEIGHT, computedCardHeight) * sizeY + - GRID_GAP * (sizeY - 1), // 최소 높이 147px, gap 20px + Math.max(EDIT_CARD_WRAPPER.MIN_HEIGHT, computedCardHeight) + GRID_GAP, // 최소 높이 147px, }} className={cn( 'bg-special-card-bg rounded-400 border-grey-300 relative flex flex-col overflow-hidden border p-3', @@ -68,7 +67,10 @@ export const EditCardWrapper = ({
    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) => {