diff --git a/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx b/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx
index eef9343ab..5685476fd 100644
--- a/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx
+++ b/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx
@@ -6,8 +6,8 @@ import {
} from '@/components/menu';
import {
AveragePriceContent,
+ OrderChannelContent,
OrderCountContent,
- OrderMethodContent,
PaymentMethodContent,
PeakTimeContent,
RealSalesContent,
@@ -24,8 +24,8 @@ import {
} from '@/constants/menu';
import {
AVERAGE_PRICE,
+ ORDER_CHANNEL,
ORDER_COUNT,
- ORDER_METHOD,
PAYMENT_METHOD,
PEAK_TIME,
REAL_SALES,
@@ -33,7 +33,6 @@ import {
SALES_TREND,
SALES_TYPE,
} from '@/constants/sales';
-
interface EditCardContentProps {
cardCode: MetricCardCode;
}
@@ -60,11 +59,11 @@ const {
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;
+ EXAMPLE_TOP_TYPE: ORDER_CHANNEL_EXAMPLE_TOP_TYPE,
+ EXAMPLE_TOP_SHARE: ORDER_CHANNEL_EXAMPLE_TOP_SHARE,
+ EXAMPLE_DELTA_SHARE: ORDER_CHANNEL_EXAMPLE_DELTA_SHARE,
+ EXAMPLE_ORDER_CHANNEL_DATA: ORDER_CHANNEL_EXAMPLE_ORDER_CHANNEL_DATA,
+} = ORDER_CHANNEL;
const {
EXAMPLE_TOP_TYPE: PAYMENT_METHOD_EXAMPLE_TOP_TYPE,
EXAMPLE_TOP_SHARE: PAYMENT_METHOD_EXAMPLE_TOP_SHARE,
@@ -138,14 +137,14 @@ export const EditCardContent = ({ cardCode }: EditCardContentProps) => {
case 'SLS_07_02':
case 'SLS_07_03':
return (
-
);
case 'SLS_08_01':
diff --git a/frontend/src/components/dashboard/dashboard-layout/DashboardLayout.tsx b/frontend/src/components/dashboard/dashboard-layout/DashboardLayout.tsx
index 101676aea..8355e41d2 100644
--- a/frontend/src/components/dashboard/dashboard-layout/DashboardLayout.tsx
+++ b/frontend/src/components/dashboard/dashboard-layout/DashboardLayout.tsx
@@ -1,7 +1,10 @@
import { Suspense } from 'react';
import { Tabs } from '@/components/shared/shadcn-ui';
-import { useDashboardTabsContext } from '@/hooks/dashboard';
+import {
+ useDashboardSseConnection,
+ useDashboardTabsContext,
+} from '@/hooks/dashboard';
import { DashboardHeader } from '../dashboard-header';
import { DashboardMain } from '../dashboard-main';
@@ -10,6 +13,7 @@ import { DashboardMainSuspense } from '../dashboard-main';
export const DashboardLayout = () => {
const { currentDashboardId, setCurrentDashboardId } =
useDashboardTabsContext();
+ useDashboardSseConnection();
return (
{
+ 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_09_04':
+ case 'SLS_10_07':
+ case 'SLS_11_07':
+ return ;
+ case 'SLS_13_01':
+ return ;
+ case 'SLS_14_06':
+ return ;
+ case 'MNU_01_01':
+ case 'MNU_01_04':
+ case 'MNU_01_05':
+ return ;
+ case 'MNU_03_01':
+ return ;
+ case 'MNU_04_01':
+ return ;
+ case 'MNU_05_04':
+ return ;
+ default:
+ return null;
+ }
+};
diff --git a/frontend/src/components/dashboard/dashboard-main/DashboardMain.tsx b/frontend/src/components/dashboard/dashboard-main/DashboardMain.tsx
index ebacc5d7a..efc9de88a 100644
--- a/frontend/src/components/dashboard/dashboard-main/DashboardMain.tsx
+++ b/frontend/src/components/dashboard/dashboard-main/DashboardMain.tsx
@@ -1,25 +1,19 @@
-import { useSuspenseQuery } from '@tanstack/react-query';
-
-import { useDashboardTabsContext } from '@/hooks/dashboard';
-import { dashboardOptions } from '@/services/dashboard';
+import {
+ useDashboardCardList,
+ useDashboardCardSubscription,
+} from '@/hooks/dashboard';
import { DashboardEmptyContent } from './DashboardEmptyContent';
import { DashboardMainContent } from './DashboardMainContent';
export const DashboardMain = () => {
- const { currentDashboardId } = useDashboardTabsContext();
+ const { cardList } = useDashboardCardList();
+
+ useDashboardCardSubscription({ cardList });
- const { data: cardList } = useSuspenseQuery(
- dashboardOptions.cardList(currentDashboardId),
- );
+ if (!cardList || cardList.length === 0) {
+ return ;
+ }
- return (
- <>
- {cardList?.length > 0 ? (
-
- ) : (
-
- )}
- >
- );
+ return ;
};
diff --git a/frontend/src/components/dashboard/dashboard-main/DashboardMainContent.tsx b/frontend/src/components/dashboard/dashboard-main/DashboardMainContent.tsx
index 61af4efa5..05bac8076 100644
--- a/frontend/src/components/dashboard/dashboard-main/DashboardMainContent.tsx
+++ b/frontend/src/components/dashboard/dashboard-main/DashboardMainContent.tsx
@@ -1,26 +1,59 @@
-import { DASHBOARD_METRIC_CARDS } from '@/constants/dashboard';
+import { useNavigate } from 'react-router-dom';
+
+import { DefaultCardWrapper } from '@/components/shared';
+import { DefaultCardFetchBoundary } from '@/components/shared/default-card-fetch-boundary';
+import {
+ DASHBOARD_METRIC_CARDS,
+ isMenuMetricCardCode,
+ isSalesMetricCardCode,
+ type MetricCardCode,
+} from '@/constants/dashboard';
+import { ROUTE_PATHS } from '@/constants/shared';
import type { DashboardCard } from '@/types/dashboard';
+import { DashboardCard as DashboardCardComponent } from './DashboardCard';
+
interface DashboardMainContentProps {
cards: DashboardCard[];
}
export const DashboardMainContent = ({ cards }: DashboardMainContentProps) => {
+ const navigate = useNavigate();
+
+ const handleClickChevronRightIcon = (cardCode: MetricCardCode) => {
+ const analysisPath = ROUTE_PATHS.ANALYSIS.BASE;
+ if (isSalesMetricCardCode(cardCode)) {
+ navigate(`${analysisPath}/${ROUTE_PATHS.ANALYSIS.SALES}`);
+ return;
+ }
+ if (isMenuMetricCardCode(cardCode)) {
+ navigate(`${analysisPath}/${ROUTE_PATHS.ANALYSIS.MENU}`);
+ return;
+ }
+ };
+
return (
{cards.map((item) => {
const card = DASHBOARD_METRIC_CARDS[item.cardCode];
+
return (
-
- 카드 {item.cardCode}
-
+
+
+ handleClickChevronRightIcon(item.cardCode)
+ }
+ >
+
+
+
);
})}
diff --git a/frontend/src/components/dashboard/dashboard-menu/DashboardIngredientUsageRankingCard.tsx b/frontend/src/components/dashboard/dashboard-menu/DashboardIngredientUsageRankingCard.tsx
new file mode 100644
index 000000000..9d9f0e7cf
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-menu/DashboardIngredientUsageRankingCard.tsx
@@ -0,0 +1,41 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import {
+ IngredientUsageRankingCardContent,
+ IngredientUsageRankingCardContentEmptyView,
+} from '@/components/menu';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetIngredientUsageRankingResponseDto } from '@/types/menu';
+
+type DashboardIngredientUsageRankingCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.MENU.sections.INGREDIENT_CONSUMPTION_RANK.items.INGREDIENT_CONSUMPTION_RANK
+>;
+interface DashboardIngredientUsageRankingCardProps {
+ cardCode: DashboardIngredientUsageRankingCardCodes;
+}
+
+export const DashboardIngredientUsageRankingCard = ({
+ cardCode,
+}: DashboardIngredientUsageRankingCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ if (data.hasIngredient && data.items.length === 0) {
+ return ;
+ }
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-menu/DashboardMenuSalesRankingCard.tsx b/frontend/src/components/dashboard/dashboard-menu/DashboardMenuSalesRankingCard.tsx
new file mode 100644
index 000000000..2e3e832e7
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-menu/DashboardMenuSalesRankingCard.tsx
@@ -0,0 +1,37 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import {
+ MenuSalesRankingCardContent,
+ MenuSalesRankingCardContentEmptyView,
+} from '@/components/menu';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetMenuSalesRankingResponseDto } from '@/types/menu';
+
+type DashboardMenuSalesRankingCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.MENU.sections.POPULAR_MENU.items.MENU_SALES_RANKING
+>;
+
+interface DashboardMenuSalesRankingCardProps {
+ cardCode: DashboardMenuSalesRankingCardCodes;
+}
+
+export const DashboardMenuSalesRankingCard = ({
+ cardCode,
+}: DashboardMenuSalesRankingCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ if (data.items.length === 0) {
+ return ;
+ }
+
+ return ;
+};
diff --git a/frontend/src/components/dashboard/dashboard-menu/DashboardPopularMenuCombinationCard.tsx b/frontend/src/components/dashboard/dashboard-menu/DashboardPopularMenuCombinationCard.tsx
new file mode 100644
index 000000000..57bf9f1fa
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-menu/DashboardPopularMenuCombinationCard.tsx
@@ -0,0 +1,38 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { PopularMenuCombinationCardContent } from '@/components/menu';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetPopularMenuCombinationResponseDto } from '@/types/menu';
+
+type DashboardPopularMenuCombinationCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.MENU.sections.POPULAR_MENU_COMBINATION.items.POPULAR_MENU_COMBINATION
+>;
+
+interface DashboardPopularMenuCombinationCardProps {
+ cardCode: DashboardPopularMenuCombinationCardCodes;
+}
+
+export const DashboardPopularMenuCombinationCard = ({
+ cardCode,
+}: DashboardPopularMenuCombinationCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ const firstMenuName = data.items[0]?.baseMenuName;
+ const secondMenuName = data.items[0]?.pairedMenus?.[0]?.menuName;
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-menu/DashboardTimeSlotMenuOrderCountCard.tsx b/frontend/src/components/dashboard/dashboard-menu/DashboardTimeSlotMenuOrderCountCard.tsx
new file mode 100644
index 000000000..9905cc55c
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-menu/DashboardTimeSlotMenuOrderCountCard.tsx
@@ -0,0 +1,38 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { TimeSlotMenuOrderCountCardContent } from '@/components/menu';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetDetailTimeSlotMenuOrderCountResponseDto } from '@/types/menu';
+
+type DashboardTimeSlotMenuOrderCountCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.MENU.sections.MENU_SALES_PATTERN.items.TIME_BASED_MENU_ORDER_COUNT
+>;
+
+interface DashboardTimeSlotMenuOrderCountCardProps {
+ cardCode: DashboardTimeSlotMenuOrderCountCardCodes;
+}
+
+export const DashboardTimeSlotMenuOrderCountCard = ({
+ cardCode,
+}: DashboardTimeSlotMenuOrderCountCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ const timeSlot2H = data.items[0]?.timeSlot2H;
+ const menuName = data.items[0]?.menus[0]?.menuName;
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-menu/index.ts b/frontend/src/components/dashboard/dashboard-menu/index.ts
new file mode 100644
index 000000000..af03ed881
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-menu/index.ts
@@ -0,0 +1,4 @@
+export { DashboardMenuSalesRankingCard } from './DashboardMenuSalesRankingCard';
+export { DashboardTimeSlotMenuOrderCountCard } from './DashboardTimeSlotMenuOrderCountCard';
+export { DashboardIngredientUsageRankingCard } from './DashboardIngredientUsageRankingCard';
+export { DashboardPopularMenuCombinationCard } from './DashboardPopularMenuCombinationCard';
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardAveragePriceCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardAveragePriceCard.tsx
new file mode 100644
index 000000000..ae3dc4a01
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardAveragePriceCard.tsx
@@ -0,0 +1,36 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { AveragePriceContent } from '@/components/sales';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetAveragePriceResponseDto } from '@/types/sales';
+
+type DashboardAveragePriceCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.CURRENT_SALES.items.AVERAGE_PRICE
+>;
+
+interface DashboardAveragePriceCardProps {
+ cardCode: DashboardAveragePriceCardCodes;
+}
+export const DashboardAveragePriceCard = ({
+ cardCode,
+}: DashboardAveragePriceCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructureOrderChannelCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructureOrderChannelCard.tsx
new file mode 100644
index 000000000..b10a83075
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructureOrderChannelCard.tsx
@@ -0,0 +1,45 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import {
+ OrderChannelContent,
+ OrderChannelContentEmptyView,
+} from '@/components/sales';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetIncomeStructureByOrderChannelResponseDto } from '@/types/sales';
+
+type DashboardIncomeStructureOrderChannelCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.ORDER_CHANNEL
+>;
+
+interface DashboardIncomeStructureOrderChannelCardProps {
+ cardCode: DashboardIncomeStructureOrderChannelCardCodes;
+}
+
+export const DashboardIncomeStructureOrderChannelCard = ({
+ cardCode,
+}: DashboardIncomeStructureOrderChannelCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(
+ cardCode,
+ );
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ if (data.items.length === 0) {
+ return ;
+ }
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructurePaymentMethodCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructurePaymentMethodCard.tsx
new file mode 100644
index 000000000..2197f5ef8
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructurePaymentMethodCard.tsx
@@ -0,0 +1,44 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import {
+ PaymentMethodContent,
+ PaymentMethodContentEmptyView,
+} from '@/components/sales';
+import type {
+ DASHBOARD_METRICS,
+ ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetIncomeStructureByPaymentMethodResponseDto } from '@/types/sales';
+
+type DashboardIncomeStructurePaymentMethodCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.PAYMENT_METHOD
+>;
+interface DashboardIncomeStructurePaymentMethodCardProps {
+ cardCode: DashboardIncomeStructurePaymentMethodCardCodes;
+}
+
+export const DashboardIncomeStructurePaymentMethodCard = ({
+ cardCode,
+}: DashboardIncomeStructurePaymentMethodCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(
+ cardCode,
+ );
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ if (data.items.length === 0) {
+ return ;
+ }
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructureSalesTypeCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructureSalesTypeCard.tsx
new file mode 100644
index 000000000..8b70f0922
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardIncomeStructureSalesTypeCard.tsx
@@ -0,0 +1,43 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import {
+ SalesTypeContent,
+ SalesTypeContentEmptyView,
+} from '@/components/sales';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetIncomeStructureBySalesTypeResponseDto } from '@/types/sales';
+
+type DashboardIncomeStructureSalesTypeCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.SALES_TYPE
+>;
+
+interface DashboardIncomeStructureSalesTypeCardProps {
+ cardCode: DashboardIncomeStructureSalesTypeCardCodes;
+}
+
+export const DashboardIncomeStructureSalesTypeCard = ({
+ cardCode,
+}: DashboardIncomeStructureSalesTypeCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ if (data.items.length === 0) {
+ return ;
+ }
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardOrderCountCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardOrderCountCard.tsx
new file mode 100644
index 000000000..1e4190562
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardOrderCountCard.tsx
@@ -0,0 +1,36 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { OrderCountContent } from '@/components/sales';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetOrderCountResponseDto } from '@/types/sales';
+
+type DashboardOrderCountCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.CURRENT_SALES.items.ORDER_COUNT
+>;
+
+interface DashboardOrderCountCardProps {
+ cardCode: DashboardOrderCountCardCodes;
+}
+
+export const DashboardOrderCountCard = ({
+ cardCode,
+}: DashboardOrderCountCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption = createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardPeakTimeCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardPeakTimeCard.tsx
new file mode 100644
index 000000000..600eb67bd
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardPeakTimeCard.tsx
@@ -0,0 +1,30 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { PeakTimeContent } from '@/components/sales';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetDetailPeakTimeResponseDto } from '@/types/sales';
+
+type DashboardPeakTimeCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.SALES_PATTERN.items.PEAK_TIME
+>;
+
+interface DashboardPeakTimeCardProps {
+ cardCode: DashboardPeakTimeCardCodes;
+}
+
+export const DashboardPeakTimeCard = ({
+ cardCode,
+}: DashboardPeakTimeCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ return ;
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardRealSalesCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardRealSalesCard.tsx
new file mode 100644
index 000000000..e44601514
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardRealSalesCard.tsx
@@ -0,0 +1,35 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { RealSalesContent } from '@/components/sales';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetRealSalesResponseDto } from '@/types/sales';
+
+type DashboardRealSalesCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.CURRENT_SALES.items.REAL_SALES
+>;
+
+interface DashboardRealSalesCardProps {
+ cardCode: DashboardRealSalesCardCodes;
+}
+export const DashboardRealSalesCard = ({
+ cardCode,
+}: DashboardRealSalesCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption = createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardSalesByDayCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardSalesByDayCard.tsx
new file mode 100644
index 000000000..54733084d
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardSalesByDayCard.tsx
@@ -0,0 +1,36 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { SalesByDayContent } from '@/components/sales';
+import {
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetDetailSalesByDayResponseDto } from '@/types/sales';
+
+type DashboardSalesByDayCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.SALES_PATTERN.items.WEEKDAY_SALES_PATTERN
+>;
+
+interface DashboardSalesByDayCardProps {
+ cardCode: DashboardSalesByDayCardCodes;
+}
+
+export const DashboardSalesByDayCard = ({
+ cardCode,
+}: DashboardSalesByDayCardProps) => {
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption =
+ createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/DashboardSalesTrendCard.tsx b/frontend/src/components/dashboard/dashboard-sales/DashboardSalesTrendCard.tsx
new file mode 100644
index 000000000..88b6e7c56
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/DashboardSalesTrendCard.tsx
@@ -0,0 +1,59 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { SalesTrendContent } from '@/components/sales';
+import {
+ DASHBOARD_METRIC_CARDS,
+ DASHBOARD_METRICS,
+ type ExtractCardCodesFromSection,
+} from '@/constants/dashboard';
+import { PERIOD_PRESETS } from '@/constants/shared';
+import { useDashboardCardDetailQueryOption } from '@/hooks/dashboard';
+import type { GetSalesTrendResponseDto } from '@/types/sales';
+
+type DashboardSalesTrendCardCodes = ExtractCardCodesFromSection<
+ typeof DASHBOARD_METRICS.SALES.sections.SALES_TREND
+>;
+
+const TREND_CHART_WIDTH_FOR_RECENT_30_DAYS = 660;
+const TREND_CHART_WIDTH = 1020;
+const TREND_CHART_HEIGHT = 120;
+
+interface DashboardSalesTrendCardProps {
+ cardCode: DashboardSalesTrendCardCodes;
+}
+
+export const DashboardSalesTrendCard = ({
+ cardCode,
+}: DashboardSalesTrendCardProps) => {
+ const { period } = DASHBOARD_METRIC_CARDS[cardCode];
+ const { createCardDetailQuery } = useDashboardCardDetailQueryOption();
+
+ const queryOption = createCardDetailQuery(cardCode);
+
+ const { data } = useSuspenseQuery(queryOption);
+
+ const salesTrendData = {
+ data: {
+ mainX: data.items.map((item) => ({ amount: item.label, unit: '' })),
+ mainY: data.items.map((item) => ({ amount: item.netAmount, unit: '원' })),
+ subX: data.items.map((item) => ({ amount: item.label, unit: '' })),
+ subY: data.items.map((item) => ({ amount: item.orderCount, unit: '건' })),
+ },
+ color: 'var(--color-grey-400)',
+ };
+
+ const trendChartWidth =
+ period === PERIOD_PRESETS.recentDays7_14_30.recent30Days
+ ? TREND_CHART_WIDTH_FOR_RECENT_30_DAYS
+ : TREND_CHART_WIDTH;
+ const trendChartHeight = TREND_CHART_HEIGHT;
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/dashboard/dashboard-sales/index.ts b/frontend/src/components/dashboard/dashboard-sales/index.ts
new file mode 100644
index 000000000..f3f55a144
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-sales/index.ts
@@ -0,0 +1,9 @@
+export { DashboardRealSalesCard } from './DashboardRealSalesCard';
+export { DashboardOrderCountCard } from './DashboardOrderCountCard';
+export { DashboardAveragePriceCard } from './DashboardAveragePriceCard';
+export { DashboardIncomeStructureSalesTypeCard } from './DashboardIncomeStructureSalesTypeCard';
+export { DashboardIncomeStructureOrderChannelCard } from './DashboardIncomeStructureOrderChannelCard';
+export { DashboardIncomeStructurePaymentMethodCard } from './DashboardIncomeStructurePaymentMethodCard';
+export { DashboardSalesTrendCard } from './DashboardSalesTrendCard';
+export { DashboardPeakTimeCard } from './DashboardPeakTimeCard';
+export { DashboardSalesByDayCard } from './DashboardSalesByDayCard';
diff --git a/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationCardContent.tsx b/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationCardContent.tsx
index 2e36f77cb..e999096f6 100644
--- a/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationCardContent.tsx
+++ b/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationCardContent.tsx
@@ -1,19 +1,20 @@
-import { POPULAR_MENU_COMBINATION } from '@/constants/menu';
-import type { GetDashboardPopularMenuCombinationResponseDto } from '@/types/menu';
-
import { PopularMenuCombinationContent } from './PopularMenuCombinationContent';
+import { PopularMenuCombinationContentEmptyView } from './PopularMenuCombinationContentEmptyView';
-const { EXAMPLE_FIRST_MENU_NAME, EXAMPLE_SECOND_MENU_NAME } =
- POPULAR_MENU_COMBINATION;
-interface PopularMenuCombinationCardContentProps extends GetDashboardPopularMenuCombinationResponseDto {
+interface PopularMenuCombinationCardContentProps {
className?: string;
+ firstMenuName?: string | null;
+ secondMenuName?: string | null;
}
export const PopularMenuCombinationCardContent = ({
- firstMenuName = EXAMPLE_FIRST_MENU_NAME,
- secondMenuName = EXAMPLE_SECOND_MENU_NAME,
+ firstMenuName,
+ secondMenuName,
className,
}: PopularMenuCombinationCardContentProps) => {
+ if (!firstMenuName || !secondMenuName) {
+ return ;
+ }
return (
{
+ return (
+
+
+
+
+
+ 아직 추천해 드릴{'\n'}
+ 인기 조합이 없어요.
+
+
+ 조금만 기다려 주시면{'\n'}맛있는 조합을 찾아올게요!
+
+
+ );
+};
diff --git a/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountCardContent.tsx b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountCardContent.tsx
index db2e4a7a6..a9334bbb9 100644
--- a/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountCardContent.tsx
+++ b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountCardContent.tsx
@@ -1,19 +1,21 @@
-import { ORDER_COUNT } from '@/constants/menu';
import type { GetDashboardTimeSlotMenuOrderCountResponseDto } from '@/types/menu';
import { TimeSlotMenuOrderCountContent } from './TimeSlotMenuOrderCountContent';
+import { TimeSlotMenuOrderCountContentEmptyView } from './TimeSlotMenuOrderCountContentEmptyView';
-const { EXAMPLE_TIME_SLOT_2H, EXAMPLE_MENU_NAME } = ORDER_COUNT;
// 현재 주문건수가 가장 많은 메뉴 카드
-interface TimeSlotMenuOrderCountCardContentProps extends GetDashboardTimeSlotMenuOrderCountResponseDto {
+interface TimeSlotMenuOrderCountCardContentProps extends Partial {
className?: string;
}
export const TimeSlotMenuOrderCountCardContent = ({
- timeSlot2H = EXAMPLE_TIME_SLOT_2H,
- menuName = EXAMPLE_MENU_NAME,
+ timeSlot2H,
+ menuName,
className,
}: TimeSlotMenuOrderCountCardContentProps) => {
+ if (!timeSlot2H || !menuName) {
+ return ;
+ }
return (
{menuName}
- {`는 ${timeSlot2H}~${getNextHour(timeSlot2H)}시`}
+ {`은(는) ${timeSlot2H}~${getNextHour(timeSlot2H)}시`}
주문이 가장 많아요
diff --git a/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountContentEmptyView.tsx b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountContentEmptyView.tsx
new file mode 100644
index 000000000..8f23eb14b
--- /dev/null
+++ b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountContentEmptyView.tsx
@@ -0,0 +1,19 @@
+import { Clock } from 'lucide-react';
+
+export const TimeSlotMenuOrderCountContentEmptyView = () => {
+ return (
+
+
+
+
+
+ 오늘 주문 데이터가{'\n'}
+ 아직 없어요
+
+
+ 첫 주문이 들어오면{'\n'}
+ 시간대별 분석을 시작할게요!
+
+
+ );
+};
diff --git a/frontend/src/components/menu/dashboard-menu-ranking/IngredientUsageRankingCardContentEmptyView.tsx b/frontend/src/components/menu/dashboard-menu-ranking/IngredientUsageRankingCardContentEmptyView.tsx
new file mode 100644
index 000000000..8005ecd2f
--- /dev/null
+++ b/frontend/src/components/menu/dashboard-menu-ranking/IngredientUsageRankingCardContentEmptyView.tsx
@@ -0,0 +1,18 @@
+import { ShoppingBasket } from 'lucide-react';
+
+export const IngredientUsageRankingCardContentEmptyView = () => {
+ return (
+
+
+
+
+
+ 오늘 소진된 식재료가{'\n'}아직
+ 없어요
+
+
+ 첫 주문이 들어오면{'\n'}식재료 소진량을 분석해 드릴게요!
+
+
+ );
+};
diff --git a/frontend/src/components/menu/dashboard-menu-ranking/MenuSalesRankingCardContentEmptView.tsx b/frontend/src/components/menu/dashboard-menu-ranking/MenuSalesRankingCardContentEmptView.tsx
new file mode 100644
index 000000000..5a9201330
--- /dev/null
+++ b/frontend/src/components/menu/dashboard-menu-ranking/MenuSalesRankingCardContentEmptView.tsx
@@ -0,0 +1,83 @@
+import { ListOrdered } from 'lucide-react';
+
+import {
+ DASHBOARD_METRIC_CARDS,
+ DASHBOARD_METRICS,
+ type ExtractCardCodes,
+} from '@/constants/dashboard';
+import {
+ PERIOD_PRESET_KEYS,
+ PERIOD_PRESETS,
+ type PeriodType,
+} from '@/constants/shared';
+import { createMessageToken } from '@/utils/sales/dashboard';
+import { cn } from '@/utils/shared';
+
+type MenuSalesRankingCardContentEmptyViewCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.MENU.sections.POPULAR_MENU.items.MENU_SALES_RANKING
+>;
+
+interface MenuSalesRankingCardContentEmptyViewProps {
+ cardCode: MenuSalesRankingCardContentEmptyViewCodes;
+}
+
+const getEmptyViewMessage = (
+ period: PeriodType,
+) => {
+ const dateMessage = period;
+
+ if (period === PERIOD_PRESETS.today7_30.today) {
+ return {
+ body: [
+ createMessageToken(dateMessage),
+ createMessageToken(' 판매된 메뉴', true, 'primary'),
+ createMessageToken('가\n 아직 없어요.'),
+ ],
+ caption: [
+ createMessageToken('첫 주문이 들어오면\n 인기 메뉴를 알려드릴게요!'),
+ ],
+ };
+ }
+
+ return {
+ body: [
+ createMessageToken(dateMessage),
+ createMessageToken(' 주문 데이터', true, 'primary'),
+ createMessageToken('가\n 아직 부족해요.'),
+ ],
+ caption: [
+ createMessageToken('데이터가 쌓이면\n 주간 인기 메뉴를 분석해 드릴게요!'),
+ ],
+ };
+};
+
+export const MenuSalesRankingCardContentEmptyView = ({
+ cardCode,
+}: MenuSalesRankingCardContentEmptyViewProps) => {
+ const { period } = DASHBOARD_METRIC_CARDS[cardCode];
+ const { body, caption } = getEmptyViewMessage(period);
+
+ return (
+
+
+
+
+
+ {body.map(({ text, isHighlight, highlightColor }, index) => (
+
+ {text}
+
+ ))}
+
+
+ {caption.map((token) => token.text).join('')}
+
+
+ );
+};
diff --git a/frontend/src/components/menu/dashboard-menu-ranking/index.ts b/frontend/src/components/menu/dashboard-menu-ranking/index.ts
index b8a4a5120..4610b63fa 100644
--- a/frontend/src/components/menu/dashboard-menu-ranking/index.ts
+++ b/frontend/src/components/menu/dashboard-menu-ranking/index.ts
@@ -1,2 +1,4 @@
export { IngredientUsageRankingCardContent } from './IngredientUsageRankingCardContent';
export { MenuSalesRankingCardContent } from './MenuSalesRankingCardContent';
+export { MenuSalesRankingCardContentEmptyView } from './MenuSalesRankingCardContentEmptView';
+export { IngredientUsageRankingCardContentEmptyView } from './IngredientUsageRankingCardContentEmptyView';
diff --git a/frontend/src/components/menu/index.ts b/frontend/src/components/menu/index.ts
index f01b60ef9..144bc537c 100644
--- a/frontend/src/components/menu/index.ts
+++ b/frontend/src/components/menu/index.ts
@@ -6,6 +6,8 @@ export { CategoryRevenueChartLegend } from './CategoryRevenueChartLegend';
export {
IngredientUsageRankingCardContent,
MenuSalesRankingCardContent,
+ MenuSalesRankingCardContentEmptyView,
+ IngredientUsageRankingCardContentEmptyView,
} from './dashboard-menu-ranking';
export { TimeSlotMenuOrderCountCardContent } from './dashboard-menu-order';
export { PopularMenuCombinationCardContent } from './dashboard-menu-combination';
diff --git a/frontend/src/components/sales/dashboard-current-sales/RealSalesContent.tsx b/frontend/src/components/sales/dashboard-current-sales/RealSalesContent.tsx
index 337b07628..8691b5756 100644
--- a/frontend/src/components/sales/dashboard-current-sales/RealSalesContent.tsx
+++ b/frontend/src/components/sales/dashboard-current-sales/RealSalesContent.tsx
@@ -4,7 +4,7 @@ import {
type ExtractCardCodes,
} from '@/constants/dashboard';
import { REAL_SALES, SALES_UNIT } from '@/constants/sales';
-import type { GetRealTimeSalesResponseDto } from '@/types/sales';
+import type { GetRealSalesResponseDto } from '@/types/sales';
import { getMetricTrend } from '@/utils/dashboard';
import { getSalesCurrentComparisonMessage } from '@/utils/sales';
@@ -24,7 +24,7 @@ type RealSalesCardCodes = ExtractCardCodes<
>;
interface RealSalesContentProps extends Omit<
- GetRealTimeSalesResponseDto,
+ GetRealSalesResponseDto,
'differenceAmount'
> {
cardCode: RealSalesCardCodes;
diff --git a/frontend/src/components/sales/dashboard-sales-income/DashboardSalesIncomeContent.tsx b/frontend/src/components/sales/dashboard-sales-income/DashboardSalesIncomeContent.tsx
index b72bbf7c5..c101803d3 100644
--- a/frontend/src/components/sales/dashboard-sales-income/DashboardSalesIncomeContent.tsx
+++ b/frontend/src/components/sales/dashboard-sales-income/DashboardSalesIncomeContent.tsx
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react';
import { DoughnutChart } from '@/components/shared';
+import type { SALES_SOURCE, SalesSourceType } from '@/constants/sales';
import { PERIOD_PRESETS } from '@/constants/shared';
import type { SalesIncomeStructureInsight, SalesSource } from '@/types/sales';
import type { DoughnutChartItem } from '@/types/shared';
@@ -32,20 +33,22 @@ export const DashboardSalesIncomeContent = ({
interface DashboardSalesIncomeContentComparisonMessageProps {
periodType: ValueOf;
- topType: SalesIncomeStructureInsight['topType'];
- topShare: SalesIncomeStructureInsight['topShare'];
- deltaShare: SalesIncomeStructureInsight['deltaShare'];
+ topTypeLabel: SalesSourceType;
+ topShare: SalesIncomeStructureInsight['topShare'];
+ deltaShare: SalesIncomeStructureInsight<
+ keyof typeof SALES_SOURCE
+ >['deltaShare'];
}
export const DashboardSalesIncomeContentComparisonMessage = ({
periodType,
- topType,
+ topTypeLabel,
topShare,
deltaShare,
}: DashboardSalesIncomeContentComparisonMessageProps) => {
const comparisonMessageTokens = getSalesIncomeStructureComparisonMessage({
periodType,
- topType,
+ topTypeLabel,
topShare,
deltaShare,
});
diff --git a/frontend/src/components/sales/dashboard-sales-income/OrderMethodContent.tsx b/frontend/src/components/sales/dashboard-sales-income/OrderChannelContent.tsx
similarity index 53%
rename from frontend/src/components/sales/dashboard-sales-income/OrderMethodContent.tsx
rename to frontend/src/components/sales/dashboard-sales-income/OrderChannelContent.tsx
index 5ca7f662d..91eee40aa 100644
--- a/frontend/src/components/sales/dashboard-sales-income/OrderMethodContent.tsx
+++ b/frontend/src/components/sales/dashboard-sales-income/OrderChannelContent.tsx
@@ -3,53 +3,62 @@ import {
DASHBOARD_METRICS,
type ExtractCardCodes,
} from '@/constants/dashboard';
-import { ORDER_METHOD, SALES_SOURCE_COLORS } from '@/constants/sales';
-import type { GetIncomeStructureByOrderMethodResponseDto } from '@/types/sales';
+import {
+ ORDER_CHANNEL,
+ SALES_SOURCE,
+ SALES_SOURCE_COLORS,
+} from '@/constants/sales';
+import type { GetIncomeStructureByOrderChannelResponseDto } from '@/types/sales';
import { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent';
-const { DOUGHNUT_CHART_TITLE } = ORDER_METHOD;
+const { DOUGHNUT_CHART_TITLE } = ORDER_CHANNEL;
-type OrderMethodCardCodes = ExtractCardCodes<
- typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.ORDER_METHOD
+type OrderChannelCardCodes = ExtractCardCodes<
+ typeof DASHBOARD_METRICS.SALES.sections.INCOME_STRUCTURE.items.ORDER_CHANNEL
>;
-interface OrderMethodContentProps extends GetIncomeStructureByOrderMethodResponseDto {
- cardCode: OrderMethodCardCodes;
+interface OrderChannelContentProps extends GetIncomeStructureByOrderChannelResponseDto {
+ cardCode: OrderChannelCardCodes;
}
-export const OrderMethodContent = ({
+export const OrderChannelContent = ({
cardCode,
insight,
items,
-}: OrderMethodContentProps) => {
+}: OrderChannelContentProps) => {
const periodType = DASHBOARD_METRIC_CARDS[cardCode].period;
- const orderMethodData = items.map((item) => ({
- salesSourceType: item.orderChannel,
+ const orderChannelData = items.map((item) => ({
+ salesSourceType: SALES_SOURCE.ORDER_METHOD[item.orderChannel],
revenue: item.salesAmount,
count: item.orderCount,
changeRate: item.deltaShare,
}));
- const chartData = orderMethodData.map((data) => ({
+ const chartData = orderChannelData.map((data) => ({
label: data.salesSourceType,
value: data.revenue,
color: SALES_SOURCE_COLORS[data.salesSourceType],
}));
+ const topTypeLabel =
+ SALES_SOURCE.ORDER_METHOD[
+ insight.topType as keyof typeof SALES_SOURCE.ORDER_METHOD
+ ];
+
return (
diff --git a/frontend/src/components/sales/dashboard-sales-income/OrderChannelContentEmptyView.tsx b/frontend/src/components/sales/dashboard-sales-income/OrderChannelContentEmptyView.tsx
new file mode 100644
index 000000000..91159b134
--- /dev/null
+++ b/frontend/src/components/sales/dashboard-sales-income/OrderChannelContentEmptyView.tsx
@@ -0,0 +1,17 @@
+import { Inbox } from 'lucide-react';
+
+export const OrderChannelContentEmptyView = () => {
+ return (
+
+
+
+
+
+ 주문 내역이 없어요
+
+
+ POS, 키오스크, 배달앱 등을 통해{'\n'}주문된 내역이 없습니다.
+
+
+ );
+};
diff --git a/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContent.tsx b/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContent.tsx
index 26e49de50..582c1025c 100644
--- a/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContent.tsx
+++ b/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContent.tsx
@@ -3,7 +3,11 @@ import {
DASHBOARD_METRICS,
type ExtractCardCodes,
} from '@/constants/dashboard';
-import { PAYMENT_METHOD, SALES_SOURCE_COLORS } from '@/constants/sales';
+import {
+ PAYMENT_METHOD,
+ SALES_SOURCE,
+ SALES_SOURCE_COLORS,
+} from '@/constants/sales';
import type { GetIncomeStructureByPaymentMethodResponseDto } from '@/types/sales';
import { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent';
@@ -26,7 +30,7 @@ export const PaymentMethodContent = ({
const periodType = DASHBOARD_METRIC_CARDS[cardCode].period;
const paymentMethodData = items.map((item) => ({
- salesSourceType: item.payMethod,
+ salesSourceType: SALES_SOURCE.PAYMENT_METHOD[item.payMethod],
revenue: item.salesAmount,
count: item.orderCount,
changeRate: item.deltaShare,
@@ -38,11 +42,13 @@ export const PaymentMethodContent = ({
color: SALES_SOURCE_COLORS[data.salesSourceType],
}));
+ const topTypeLabel = SALES_SOURCE.PAYMENT_METHOD[insight.topType];
+
return (
diff --git a/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContentEmptyView.tsx b/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContentEmptyView.tsx
new file mode 100644
index 000000000..11ddd7e61
--- /dev/null
+++ b/frontend/src/components/sales/dashboard-sales-income/PaymentMethodContentEmptyView.tsx
@@ -0,0 +1,17 @@
+import { CreditCard } from 'lucide-react';
+
+export const PaymentMethodContentEmptyView = () => {
+ return (
+
+
+
+
+
+ 결제 내역이 없어요
+
+
+ 카드, 현금, 간편결제 등으로{'\n'}결제된 내역이 없습니다.
+
+
+ );
+};
diff --git a/frontend/src/components/sales/dashboard-sales-income/SalesTypeContent.tsx b/frontend/src/components/sales/dashboard-sales-income/SalesTypeContent.tsx
index 310cb20f4..474d1fd75 100644
--- a/frontend/src/components/sales/dashboard-sales-income/SalesTypeContent.tsx
+++ b/frontend/src/components/sales/dashboard-sales-income/SalesTypeContent.tsx
@@ -3,7 +3,11 @@ import {
DASHBOARD_METRICS,
type ExtractCardCodes,
} from '@/constants/dashboard';
-import { SALES_SOURCE_COLORS, SALES_TYPE } from '@/constants/sales';
+import {
+ SALES_SOURCE,
+ SALES_SOURCE_COLORS,
+ SALES_TYPE,
+} from '@/constants/sales';
import type { GetIncomeStructureBySalesTypeResponseDto } from '@/types/sales';
import { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent';
@@ -26,7 +30,7 @@ export const SalesTypeContent = ({
const periodType = DASHBOARD_METRIC_CARDS[cardCode].period;
const salesTypeData = items.map((item) => ({
- salesSourceType: item.salesType,
+ salesSourceType: SALES_SOURCE.SALE_TYPE[item.salesType],
revenue: item.salesAmount,
count: item.orderCount,
changeRate: item.deltaShare,
@@ -38,11 +42,13 @@ export const SalesTypeContent = ({
color: SALES_SOURCE_COLORS[data.salesSourceType],
}));
+ const topTypeLabel = SALES_SOURCE.SALE_TYPE[insight.topType];
+
return (
diff --git a/frontend/src/components/sales/dashboard-sales-income/SalesTypeContentEmptyView.tsx b/frontend/src/components/sales/dashboard-sales-income/SalesTypeContentEmptyView.tsx
new file mode 100644
index 000000000..f69e42303
--- /dev/null
+++ b/frontend/src/components/sales/dashboard-sales-income/SalesTypeContentEmptyView.tsx
@@ -0,0 +1,17 @@
+import { PieChart } from 'lucide-react';
+
+export const SalesTypeContentEmptyView = () => {
+ return (
+
+
+
+ 판매 내역이 없어요
+
+
+ 선택하신 기간 동안 발생한 홀, 배달, 포장{'\n'}매출 데이터가 없습니다.
+
+
+ );
+};
diff --git a/frontend/src/components/sales/dashboard-sales-income/index.ts b/frontend/src/components/sales/dashboard-sales-income/index.ts
index 108d2f0e2..37c7bdabc 100644
--- a/frontend/src/components/sales/dashboard-sales-income/index.ts
+++ b/frontend/src/components/sales/dashboard-sales-income/index.ts
@@ -1,4 +1,7 @@
export { DashboardSalesIncomeContent } from './DashboardSalesIncomeContent';
export { SalesTypeContent } from './SalesTypeContent';
-export { OrderMethodContent } from './OrderMethodContent';
+export { OrderChannelContent } from './OrderChannelContent';
export { PaymentMethodContent } from './PaymentMethodContent';
+export { SalesTypeContentEmptyView } from './SalesTypeContentEmptyView';
+export { PaymentMethodContentEmptyView } from './PaymentMethodContentEmptyView';
+export { OrderChannelContentEmptyView } from './OrderChannelContentEmptyView';
diff --git a/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeContent.tsx b/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeContent.tsx
index 27b4508b6..01545837b 100644
--- a/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeContent.tsx
+++ b/frontend/src/components/sales/dashboard-sales-pattern/PeakTimeContent.tsx
@@ -20,7 +20,7 @@ export const PeakTimeContent = ({
peakTimeData,
className,
}: PeakTimeContentProps) => {
- const weekday = DAY_OF_WEEK_LIST[new Date().getDay()];
+ const weekday = DAY_OF_WEEK_LIST[(new Date().getDay() + 6) % 7];
const {
todayItems,
@@ -37,14 +37,39 @@ export const PeakTimeContent = ({
});
const primarySeries = useMemo(() => {
+ const lastItemIndexNotNull = [...todayItems]
+ .reverse()
+ .findIndex((item) => item.orderCount !== null);
+ const lastItemIndex =
+ lastItemIndexNotNull === -1
+ ? -1
+ : todayItems.length - lastItemIndexNotNull - 1;
+
return {
- ...createPeakTimeSeries(todayItems, 'var(--color-brand-main)'),
+ ...createPeakTimeSeries(
+ todayItems.map((item, index) => {
+ if (index < lastItemIndex) {
+ return {
+ ...item,
+ orderCount: item.orderCount ?? 0,
+ };
+ }
+ return item;
+ }),
+ 'var(--color-brand-main)',
+ ),
};
}, [todayItems]);
const secondarySeries = useMemo(() => {
return {
- ...createPeakTimeSeries(week4Items, 'var(--color-grey-400)'),
+ ...createPeakTimeSeries(
+ week4Items.map((item) => ({
+ ...item,
+ orderCount: item.orderCount ?? 0,
+ })),
+ 'var(--color-grey-400)',
+ ),
};
}, [week4Items]);
@@ -62,9 +87,9 @@ export const PeakTimeContent = ({
color="default"
/>
-
+
-
+
{peakTimeBriefingMessage.map(
({ text, isHighlight, highlightColor }, index) => {
return (
diff --git a/frontend/src/components/sales/dashboard-sales-pattern/SalesByDayContent.tsx b/frontend/src/components/sales/dashboard-sales-pattern/SalesByDayContent.tsx
index 1bf8f27d2..38e2e746e 100644
--- a/frontend/src/components/sales/dashboard-sales-pattern/SalesByDayContent.tsx
+++ b/frontend/src/components/sales/dashboard-sales-pattern/SalesByDayContent.tsx
@@ -29,7 +29,7 @@ export const SalesByDayContent = ({
unit: CHART_X_UNIT,
})),
mainY: salesByDayItems.map((item) => ({
- amount: item.avgNetAmount,
+ amount: item.avgNetAmount ?? 0,
unit: CHART_Y_UNIT,
})),
},
@@ -53,13 +53,13 @@ export const SalesByDayContent = ({
barChartSeries={salesByDaySeries}
hasXAxis
hasBarGradient
- showYGuideLine
yGuideLineCount={4}
hasBarLabel={false}
+ barColorChangeOnHover={false}
activeDataIndex={activeDataIndex === -1 ? undefined : activeDataIndex}
xAxisType="default"
/>
-
+
{salesByDayBriefingMessage.map(
({ text, isHighlight, highlightColor }, index) => {
return (
diff --git a/frontend/src/components/sales/dashboard-sales-trend/SalesTrendContent.tsx b/frontend/src/components/sales/dashboard-sales-trend/SalesTrendContent.tsx
index 50b455d45..50e4fbb20 100644
--- a/frontend/src/components/sales/dashboard-sales-trend/SalesTrendContent.tsx
+++ b/frontend/src/components/sales/dashboard-sales-trend/SalesTrendContent.tsx
@@ -28,7 +28,7 @@ export const SalesTrendContent = ({
trendChartHeight,
className,
}: SalesTrendContentProps) => {
- const { period, label } = DASHBOARD_METRIC_CARDS[cardCode];
+ const { period } = DASHBOARD_METRIC_CARDS[cardCode];
const {
DEFAULT_TREND_CHART_WIDTH,
DEFAULT_TREND_CHART_WIDTH_FOR_RECENT_30_DAYS,
@@ -53,29 +53,30 @@ export const SalesTrendContent = ({
className,
)}
>
-
-
{label}
-
-
-
+
+
+
-
);
};
diff --git a/frontend/src/components/sales/index.ts b/frontend/src/components/sales/index.ts
index 70173ff0b..f0aefd52f 100644
--- a/frontend/src/components/sales/index.ts
+++ b/frontend/src/components/sales/index.ts
@@ -7,11 +7,14 @@ export {
OrderCountContent,
RealSalesContent,
} from './dashboard-current-sales';
-export { DashboardSalesIncomeContent } from './dashboard-sales-income';
export {
+ DashboardSalesIncomeContent,
SalesTypeContent,
- OrderMethodContent,
+ OrderChannelContent,
PaymentMethodContent,
+ SalesTypeContentEmptyView,
+ PaymentMethodContentEmptyView,
+ OrderChannelContentEmptyView,
} from './dashboard-sales-income';
export { PeakTimeContent, SalesByDayContent } from './dashboard-sales-pattern';
export { SalesTrendContent } from './dashboard-sales-trend';
diff --git a/frontend/src/components/shared/bar-line-chart/BarLineSeriesWithTooltip.tsx b/frontend/src/components/shared/bar-line-chart/BarLineSeriesWithTooltip.tsx
index ebf27ba53..32949ad3e 100644
--- a/frontend/src/components/shared/bar-line-chart/BarLineSeriesWithTooltip.tsx
+++ b/frontend/src/components/shared/bar-line-chart/BarLineSeriesWithTooltip.tsx
@@ -35,6 +35,8 @@ export const BarLineSeriesWithTooltip = ({
+ {/* g태그는 Dot과 Bar 사이의 빈공간에 대한 interaction을 하지 않음
+ 따라서, Dot과 Bar를 감싸는 path 태그를 추가해서 interaction을 가능하게 함 */}
{
+ const titleComponent = ({
+ title,
+ hasChevronRightIcon,
+ onClickChevronRightIcon,
+ }: {
+ title?: string;
+ hasChevronRightIcon?: boolean;
+ onClickChevronRightIcon?: () => void;
+ }) => {
+ if (hasChevronRightIcon && onClickChevronRightIcon) {
+ return (
+
+ );
+ }
+
+ if (title) {
+ return {title}
;
+ }
+
+ return null;
+ };
+
return (
{(title || hasChevronRightIcon) && (
- {title &&
{title}
}
- {hasChevronRightIcon && (
-
- )}
+ {titleComponent({
+ title,
+ hasChevronRightIcon,
+ onClickChevronRightIcon,
+ })}
)}
{children}
diff --git a/frontend/src/components/shared/line-chart/LineChart.tsx b/frontend/src/components/shared/line-chart/LineChart.tsx
index 86ebcea21..7ca37aaa2 100644
--- a/frontend/src/components/shared/line-chart/LineChart.tsx
+++ b/frontend/src/components/shared/line-chart/LineChart.tsx
@@ -155,15 +155,6 @@ export const LineChart = ({
/>
>
)}
-
{secondarySeries && (
)}
+
);
};
diff --git a/frontend/src/constants/dashboard/dashboardMetric.ts b/frontend/src/constants/dashboard/dashboardMetric.ts
index 3cfb12848..f1d7d6d09 100644
--- a/frontend/src/constants/dashboard/dashboardMetric.ts
+++ b/frontend/src/constants/dashboard/dashboardMetric.ts
@@ -1,4 +1,4 @@
-import type { MetricCardCode } from './dashboardMetricCards';
+import { isMetricCardCode, type MetricCardCode } from './dashboardMetricCards';
/**
* 카드 및 섹션 구조 인터페이스
@@ -46,7 +46,7 @@ const SALES_METRICS = {
label: '판매유형별 매출',
cardCodes: ['SLS_06_01', 'SLS_06_02', 'SLS_06_03'] as const,
},
- ORDER_METHOD: {
+ ORDER_CHANNEL: {
label: '주문수단별 매출',
cardCodes: ['SLS_07_01', 'SLS_07_02', 'SLS_07_03'] as const,
},
@@ -205,3 +205,106 @@ export type ExtractCardCodes = T extends {
export type ExtractCardCodesFromSection = T extends MetricSection
? ExtractCardCodes
: never;
+
+type ExtractCardCodesFromMetricTabs = T extends MetricTabs
+ ? ExtractCardCodesFromSection
+ : never;
+
+const createMetricCardCodeByCategoryGuard = (
+ metrics: M,
+) => {
+ const codesInMetrics = new Set(
+ Object.values(metrics.sections).flatMap((section) =>
+ Object.values(section.items).flatMap((item) => item.cardCodes),
+ ),
+ );
+
+ return (code: string): code is ExtractCardCodesFromMetricTabs => {
+ return codesInMetrics.has(code as MetricCardCode);
+ };
+};
+
+export const isSalesMetricCardCode =
+ createMetricCardCodeByCategoryGuard(SALES_METRICS);
+export const isMenuMetricCardCode =
+ createMetricCardCodeByCategoryGuard(MENU_METRICS);
+
+const createMetricCardCodeBySectionItemGuard = (
+ metrics: M,
+) => {
+ const codesInMetrics = new Set(metrics.cardCodes);
+
+ return (code: string): code is ExtractCardCodes => {
+ if (!isMetricCardCode(code)) {
+ return false;
+ }
+ return codesInMetrics.has(code);
+ };
+};
+
+/**
+ * 매출현황 관련 카드 코드 타입 가드
+ */
+export const isRealSalesMetricCardCode = createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.CURRENT_SALES.items.REAL_SALES,
+);
+export const isOrderCountMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.CURRENT_SALES.items.ORDER_COUNT,
+ );
+export const isAveragePriceMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.CURRENT_SALES.items.AVERAGE_PRICE,
+ );
+export const isSalesTypeMetricCardCode = createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.INCOME_STRUCTURE.items.SALES_TYPE,
+);
+export const isOrderChannelMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.INCOME_STRUCTURE.items.ORDER_CHANNEL,
+ );
+export const isPaymentMethodMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.INCOME_STRUCTURE.items.PAYMENT_METHOD,
+ );
+export const isDailySalesTrendMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.SALES_TREND.items.DAILY_SALES_TREND,
+ );
+export const isWeeklySalesTrendMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.SALES_TREND.items.WEEKLY_SALES_TREND,
+ );
+export const isMonthlySalesTrendMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.SALES_TREND.items.MONTHLY_SALES_TREND,
+ );
+export const isPeakTimeMetricCardCode = createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.SALES_PATTERN.items.PEAK_TIME,
+);
+export const isWeekdaySalesPatternMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ SALES_METRICS.sections.SALES_PATTERN.items.WEEKDAY_SALES_PATTERN,
+ );
+
+/**
+ * 메뉴분석 관련 카드 코드 타입 가드
+ */
+export const isMenuSalesRankingMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ MENU_METRICS.sections.POPULAR_MENU.items.MENU_SALES_RANKING,
+ );
+export const isTimeBasedMenuOrderCountMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ MENU_METRICS.sections.MENU_SALES_PATTERN.items.TIME_BASED_MENU_ORDER_COUNT,
+ );
+export const isIngredientConsumptionRankMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ MENU_METRICS.sections.INGREDIENT_CONSUMPTION_RANK.items
+ .INGREDIENT_CONSUMPTION_RANK,
+ );
+export const isPopularMenuCombinationMetricCardCode =
+ createMetricCardCodeBySectionItemGuard(
+ MENU_METRICS.sections.POPULAR_MENU_COMBINATION.items
+ .POPULAR_MENU_COMBINATION,
+ );
diff --git a/frontend/src/constants/dashboard/index.ts b/frontend/src/constants/dashboard/index.ts
index a91b39b82..f1eac2bc0 100644
--- a/frontend/src/constants/dashboard/index.ts
+++ b/frontend/src/constants/dashboard/index.ts
@@ -11,6 +11,23 @@ export {
type ExtractCardCodesFromSection,
type MetricTabs,
type ExtractCardCodes,
+ isSalesMetricCardCode,
+ isMenuMetricCardCode,
+ isRealSalesMetricCardCode,
+ isOrderCountMetricCardCode,
+ isAveragePriceMetricCardCode,
+ isSalesTypeMetricCardCode,
+ isOrderChannelMetricCardCode,
+ isPaymentMethodMetricCardCode,
+ isDailySalesTrendMetricCardCode,
+ isWeeklySalesTrendMetricCardCode,
+ isMonthlySalesTrendMetricCardCode,
+ isPeakTimeMetricCardCode,
+ isWeekdaySalesPatternMetricCardCode,
+ isMenuSalesRankingMetricCardCode,
+ isIngredientConsumptionRankMetricCardCode,
+ isTimeBasedMenuOrderCountMetricCardCode,
+ isPopularMenuCombinationMetricCardCode,
} from './dashboardMetric';
export {
DASHBOARD_METRIC_CARDS,
diff --git a/frontend/src/constants/sales/dashboard-sales-income/index.ts b/frontend/src/constants/sales/dashboard-sales-income/index.ts
index 29a6b99f6..04aa7b2e2 100644
--- a/frontend/src/constants/sales/dashboard-sales-income/index.ts
+++ b/frontend/src/constants/sales/dashboard-sales-income/index.ts
@@ -1,3 +1,3 @@
-export { ORDER_METHOD } from './orderMethod';
+export { ORDER_CHANNEL } from './orderChannel';
export { PAYMENT_METHOD } from './paymentMethod';
export { SALES_TYPE } from './salesType';
diff --git a/frontend/src/constants/sales/dashboard-sales-income/orderChannel.ts b/frontend/src/constants/sales/dashboard-sales-income/orderChannel.ts
new file mode 100644
index 000000000..22ae2fa96
--- /dev/null
+++ b/frontend/src/constants/sales/dashboard-sales-income/orderChannel.ts
@@ -0,0 +1,37 @@
+import type { GetIncomeStructureByOrderChannelResponseDto } from '@/types/sales';
+
+import { SALES_SOURCE } from '../salesSource';
+
+const ORDER_METHOD_KEYS = Object.keys(
+ SALES_SOURCE.ORDER_METHOD,
+) as (keyof typeof SALES_SOURCE.ORDER_METHOD)[];
+
+export const ORDER_CHANNEL = {
+ EXAMPLE_TOP_TYPE: 'KIOSK',
+ EXAMPLE_TOP_SHARE: 50,
+ EXAMPLE_DELTA_SHARE: 4,
+ EXAMPLE_ORDER_CHANNEL_DATA: [
+ {
+ orderChannel: ORDER_METHOD_KEYS[0],
+ salesAmount: 2371000,
+ orderCount: 26,
+ share: 25,
+ deltaShare: 2.4,
+ },
+ {
+ orderChannel: ORDER_METHOD_KEYS[1],
+ salesAmount: 5329000,
+ orderCount: 53,
+ share: 25,
+ deltaShare: 4,
+ },
+ {
+ orderChannel: ORDER_METHOD_KEYS[2],
+ salesAmount: 1986000,
+ orderCount: 19,
+ share: 25,
+ deltaShare: -5.2,
+ },
+ ] as const satisfies GetIncomeStructureByOrderChannelResponseDto['items'],
+ DOUGHNUT_CHART_TITLE: '주문수단별 매출 관련 도넛 차트',
+} as const;
diff --git a/frontend/src/constants/sales/dashboard-sales-income/orderMethod.ts b/frontend/src/constants/sales/dashboard-sales-income/orderMethod.ts
deleted file mode 100644
index 4d2818f9d..000000000
--- a/frontend/src/constants/sales/dashboard-sales-income/orderMethod.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-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
index 8f8ad018e..aa8a00a13 100644
--- a/frontend/src/constants/sales/dashboard-sales-income/paymentMethod.ts
+++ b/frontend/src/constants/sales/dashboard-sales-income/paymentMethod.ts
@@ -1,33 +1,39 @@
import type { GetIncomeStructureByPaymentMethodResponseDto } from '@/types/sales';
+import { SALES_SOURCE } from '../salesSource';
+
+const PAYMENT_METHOD_KEYS = Object.keys(
+ SALES_SOURCE.PAYMENT_METHOD,
+) as (keyof typeof SALES_SOURCE.PAYMENT_METHOD)[];
+
export const PAYMENT_METHOD = {
- EXAMPLE_TOP_TYPE: '현금',
+ EXAMPLE_TOP_TYPE: 'CASH' as const,
EXAMPLE_TOP_SHARE: 46,
EXAMPLE_DELTA_SHARE: 6.7,
EXAMPLE_PAYMENT_METHOD_DATA: [
{
- payMethod: '카드',
+ payMethod: PAYMENT_METHOD_KEYS[0],
salesAmount: 2371000,
orderCount: 26,
share: 25,
deltaShare: 4.4,
},
{
- payMethod: '현금',
+ payMethod: PAYMENT_METHOD_KEYS[1],
salesAmount: 7531000,
orderCount: 25,
share: 25,
deltaShare: 6.7,
},
{
- payMethod: '간편결제',
+ payMethod: PAYMENT_METHOD_KEYS[2],
salesAmount: 2567000,
orderCount: 75,
share: 25,
deltaShare: -5.2,
},
{
- payMethod: '기타',
+ payMethod: PAYMENT_METHOD_KEYS[3],
salesAmount: 3894000,
orderCount: 39,
share: 25,
diff --git a/frontend/src/constants/sales/dashboard-sales-income/salesType.ts b/frontend/src/constants/sales/dashboard-sales-income/salesType.ts
index dcff66872..07b871de8 100644
--- a/frontend/src/constants/sales/dashboard-sales-income/salesType.ts
+++ b/frontend/src/constants/sales/dashboard-sales-income/salesType.ts
@@ -2,32 +2,36 @@ import type { GetIncomeStructureBySalesTypeResponseDto } from '@/types/sales';
import { SALES_SOURCE } from '../salesSource';
+const SALE_TYPE_KEYS = Object.keys(
+ SALES_SOURCE.SALE_TYPE,
+) as (keyof typeof SALES_SOURCE.SALE_TYPE)[];
+
export const SALES_TYPE = {
- EXAMPLE_TOP_TYPE: '배달' as const,
+ EXAMPLE_TOP_TYPE: 'DELIVERY' as const,
EXAMPLE_TOP_SHARE: 43,
EXAMPLE_DELTA_SHARE: 6.8,
EXAMPLE_SALES_SOURCE_DATA: [
{
- salesType: SALES_SOURCE.SALE_TYPE.DINE_IN,
+ salesType: SALE_TYPE_KEYS[0],
salesAmount: 2371000,
orderCount: 26,
share: 25,
deltaShare: 4.4,
},
{
- salesType: SALES_SOURCE.SALE_TYPE.TAKEOUT,
+ salesType: SALE_TYPE_KEYS[2],
salesAmount: 3255000,
orderCount: 45,
share: 45,
deltaShare: -5.2,
},
{
- salesType: SALES_SOURCE.SALE_TYPE.DELIVERY,
+ salesType: SALE_TYPE_KEYS[1],
salesAmount: 4255000,
orderCount: 28,
share: 30,
deltaShare: 6.8,
},
- ] as GetIncomeStructureBySalesTypeResponseDto['items'],
+ ] as const satisfies GetIncomeStructureBySalesTypeResponseDto['items'],
DOUGHNUT_CHART_TITLE: '판매 유형 관련 도넛 차트',
} as const;
diff --git a/frontend/src/constants/sales/index.ts b/frontend/src/constants/sales/index.ts
index d09fd3704..62941eec6 100644
--- a/frontend/src/constants/sales/index.ts
+++ b/frontend/src/constants/sales/index.ts
@@ -15,7 +15,7 @@ export {
REAL_SALES,
} from './dashboard-current-sales';
export {
- ORDER_METHOD,
+ ORDER_CHANNEL,
PAYMENT_METHOD,
SALES_TYPE,
} from './dashboard-sales-income';
diff --git a/frontend/src/constants/sales/salesSource.ts b/frontend/src/constants/sales/salesSource.ts
index 6980fbe3c..fbf0a671b 100644
--- a/frontend/src/constants/sales/salesSource.ts
+++ b/frontend/src/constants/sales/salesSource.ts
@@ -5,7 +5,7 @@ export const SALES_SOURCE = {
// 홀, 배달, 포장
DINE_IN: '홀',
DELIVERY: '배달',
- TAKEOUT: '포장',
+ TAKE_OUT: '포장',
},
ORDER_METHOD: {
POS: 'POS',
@@ -15,7 +15,7 @@ export const SALES_SOURCE = {
PAYMENT_METHOD: {
CARD: '카드',
CASH: '현금',
- MOBILE: '간편결제',
+ EASY_PAY: '간편결제',
ETC: '기타',
},
} as const;
@@ -25,13 +25,13 @@ export type SalesSourceType = DeepValueOf;
export const SALES_SOURCE_COLORS = {
[SALES_SOURCE.SALE_TYPE.DINE_IN]: 'var(--color-brand-500)',
[SALES_SOURCE.SALE_TYPE.DELIVERY]: 'var(--color-grey-500)',
- [SALES_SOURCE.SALE_TYPE.TAKEOUT]: 'var(--color-brand-50)',
+ [SALES_SOURCE.SALE_TYPE.TAKE_OUT]: '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-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.EASY_PAY]: 'var(--color-brand-200)',
[SALES_SOURCE.PAYMENT_METHOD.ETC]: 'var(--color-brand-50)',
};
diff --git a/frontend/src/hooks/dashboard/index.ts b/frontend/src/hooks/dashboard/index.ts
index f7775c732..773449cff 100644
--- a/frontend/src/hooks/dashboard/index.ts
+++ b/frontend/src/hooks/dashboard/index.ts
@@ -1,6 +1,10 @@
export { useDashboardTabsContext } from './useDashboardTabsContext';
export { useDashboardTabsDialog } from './useDashboardTabsDialog';
export { useEditCard } from './useEditCard';
+export { useDashboardCardDetailQueryOption } from './useDashboardCardDetailQueryOption';
+export { useDashboardCardList } from './useDashboardCardList';
+export { useDashboardCardSubscription } from './useDashboardCardSubscription';
+export { useDashboardSseConnection } from './useDashboardSseConnection';
export { useEditCardContext } from './useEditCardContext';
export { useGridCellSize } from './useGridCellSize';
export { useDragAndDropCard } from './useDragAndDropCard';
diff --git a/frontend/src/hooks/dashboard/useDashboardCardDetailQueryOption.ts b/frontend/src/hooks/dashboard/useDashboardCardDetailQueryOption.ts
new file mode 100644
index 000000000..b85a236a9
--- /dev/null
+++ b/frontend/src/hooks/dashboard/useDashboardCardDetailQueryOption.ts
@@ -0,0 +1,15 @@
+import { type MetricCardCode } from '@/constants/dashboard/dashboardMetricCards';
+import { useDashboardTabsContext } from '@/hooks/dashboard';
+import { dashboardOptions } from '@/services/dashboard';
+
+export const useDashboardCardDetailQueryOption = () => {
+ const { currentDashboardId } = useDashboardTabsContext();
+
+ const createCardDetailQuery = (cardCode: MetricCardCode) =>
+ dashboardOptions.cardDetail(currentDashboardId, {
+ analysisCardCode: cardCode,
+ customPeriod: false,
+ });
+
+ return { createCardDetailQuery };
+};
diff --git a/frontend/src/hooks/dashboard/useDashboardCardList.ts b/frontend/src/hooks/dashboard/useDashboardCardList.ts
new file mode 100644
index 000000000..0ecf863d5
--- /dev/null
+++ b/frontend/src/hooks/dashboard/useDashboardCardList.ts
@@ -0,0 +1,15 @@
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import { dashboardOptions } from '@/services/dashboard';
+
+import { useDashboardTabsContext } from './useDashboardTabsContext';
+
+export const useDashboardCardList = () => {
+ const { currentDashboardId } = useDashboardTabsContext();
+
+ const { data: cardList } = useSuspenseQuery(
+ dashboardOptions.cardList(currentDashboardId),
+ );
+
+ return { cardList };
+};
diff --git a/frontend/src/hooks/dashboard/useDashboardCardSubscription.ts b/frontend/src/hooks/dashboard/useDashboardCardSubscription.ts
new file mode 100644
index 000000000..a905456b7
--- /dev/null
+++ b/frontend/src/hooks/dashboard/useDashboardCardSubscription.ts
@@ -0,0 +1,45 @@
+import { useEffect } from 'react';
+
+import { useMutation } from '@tanstack/react-query';
+
+import {
+ deleteDashboardSseSubscription,
+ postDashboardSseSubscription,
+} from '@/services/dashboard';
+import type { GetDashboardCardListResponseDto } from '@/types/dashboard';
+
+interface UseDashboardCardSubscriptionProps {
+ cardList: GetDashboardCardListResponseDto;
+}
+
+export const useDashboardCardSubscription = ({
+ cardList,
+}: UseDashboardCardSubscriptionProps) => {
+ const { mutate: subscribeDashboardCardList } = useMutation({
+ mutationFn: postDashboardSseSubscription,
+ });
+
+ const { mutate: unsubscribeDashboardCardList } = useMutation({
+ mutationFn: deleteDashboardSseSubscription,
+ });
+
+ useEffect(() => {
+ if (!cardList || cardList.length === 0) {
+ return;
+ }
+
+ const topics = cardList.map((card) => card.cardCode);
+
+ subscribeDashboardCardList({
+ topics,
+ });
+
+ return () => {
+ if (cardList && cardList.length > 0) {
+ unsubscribeDashboardCardList({
+ topics,
+ });
+ }
+ };
+ }, [cardList, subscribeDashboardCardList, unsubscribeDashboardCardList]);
+};
diff --git a/frontend/src/hooks/dashboard/useDashboardSseConnection.ts b/frontend/src/hooks/dashboard/useDashboardSseConnection.ts
new file mode 100644
index 000000000..27469350a
--- /dev/null
+++ b/frontend/src/hooks/dashboard/useDashboardSseConnection.ts
@@ -0,0 +1,341 @@
+import { useCallback, useEffect, useRef } from 'react';
+import { useErrorBoundary } from 'react-error-boundary';
+
+import { useQueryClient } from '@tanstack/react-query';
+
+import {
+ isAveragePriceMetricCardCode,
+ isDailySalesTrendMetricCardCode,
+ isIngredientConsumptionRankMetricCardCode,
+ isMenuSalesRankingMetricCardCode,
+ isMetricCardCode,
+ isMonthlySalesTrendMetricCardCode,
+ isOrderChannelMetricCardCode,
+ isOrderCountMetricCardCode,
+ isPaymentMethodMetricCardCode,
+ isPeakTimeMetricCardCode,
+ isPopularMenuCombinationMetricCardCode,
+ isRealSalesMetricCardCode,
+ isSalesTypeMetricCardCode,
+ isTimeBasedMenuOrderCountMetricCardCode,
+ isWeekdaySalesPatternMetricCardCode,
+ isWeeklySalesTrendMetricCardCode,
+ type MetricCardCode,
+} from '@/constants/dashboard';
+import { dashboardKeys } from '@/services/dashboard/keys';
+import { sseClient } from '@/services/shared';
+import type {
+ GetDashboardPopularMenuCombinationResponseDto,
+ GetDashboardTimeSlotMenuOrderCountResponseDto,
+ GetDetailTimeSlotMenuOrderCountResponseDto,
+ GetPopularMenuCombinationResponseDto,
+} from '@/types/menu';
+import type {
+ GetDashboardPeakTimeResponseDto,
+ GetDashboardSalesByDayResponseDto,
+ GetDetailPeakTimeResponseDto,
+ GetDetailSalesByDayResponseDto,
+ GetSalesTrendResponseDto,
+ SalesTrendItem,
+} from '@/types/sales';
+import type { EventSourceMessage } from '@/types/shared';
+
+import { useDashboardTabsContext } from './useDashboardTabsContext';
+
+export const useDashboardSseConnection = () => {
+ const { showBoundary } = useErrorBoundary();
+ const { currentDashboardId } = useDashboardTabsContext();
+
+ const queryClient = useQueryClient();
+
+ const retryCountRef = useRef(0);
+ const currentDashboardIdRef = useRef(currentDashboardId);
+
+ const getCardDetailQueryKey = useCallback((cardCode: MetricCardCode) => {
+ return dashboardKeys.cardDetail(currentDashboardIdRef.current, {
+ analysisCardCode: cardCode,
+ customPeriod: false,
+ });
+ }, []);
+
+ /**
+ * 매출 추이 쿼리 데이터 업데이트 함수
+ */
+ const updateSalesTrendData = useCallback(
+ (response: SalesTrendItem) => (oldData?: GetSalesTrendResponseDto) => {
+ if (!oldData) {
+ return oldData;
+ }
+
+ return {
+ items: [...oldData.items.slice(0, -1), response],
+ };
+ },
+ [],
+ );
+
+ /**
+ * 피크타임 쿼리 데이터 업데이트 함수
+ */
+ const updatePeakTimeData = useCallback(
+ (response: GetDashboardPeakTimeResponseDto) =>
+ (oldData?: GetDetailPeakTimeResponseDto) => {
+ if (!oldData) {
+ return oldData;
+ }
+
+ const {
+ timeSlot2H,
+ orderCount,
+ netAmount,
+ todayPeak,
+ comparisonPeak,
+ diff,
+ shiftDirection,
+ beforeComparisonPeak,
+ } = response;
+
+ return {
+ todayPeak,
+ comparisonPeak,
+ diff,
+ shiftDirection,
+ beforeComparisonPeak,
+ todayItems: [
+ ...oldData.todayItems.map((item) => {
+ if (item.timeSlot2H === timeSlot2H) {
+ return {
+ ...item,
+ orderCount,
+ netAmount,
+ };
+ }
+ return item;
+ }),
+ ],
+ week4Items: [...oldData.week4Items],
+ };
+ },
+ [],
+ );
+
+ /**
+ * 요일별 매출 패턴 쿼리 데이터 업데이트 함수
+ */
+ const updateWeekdaySalesPatternData = useCallback(
+ (response: GetDashboardSalesByDayResponseDto) =>
+ (oldData?: GetDetailSalesByDayResponseDto) => {
+ if (!oldData) {
+ return oldData;
+ }
+
+ const { topDay, isSignificant, day, avgNetAmount, orderCount } =
+ response;
+ return {
+ topDay,
+ isSignificant,
+ items: oldData.items.map((item) => {
+ if (item.day === day) {
+ return {
+ ...item,
+ avgNetAmount,
+ orderCount,
+ };
+ }
+ return item;
+ }),
+ };
+ },
+ [],
+ );
+
+ /**
+ * 시간대별 메뉴 주문건수 쿼리 데이터 업데이트 함수
+ */
+ const updateTimeBasedMenuOrderCountData = useCallback(
+ (response: GetDashboardTimeSlotMenuOrderCountResponseDto) =>
+ (oldData?: GetDetailTimeSlotMenuOrderCountResponseDto) => {
+ const { timeSlot2H, menuName } = response;
+
+ if (!oldData) {
+ return oldData;
+ }
+
+ // 깊은 복사 후 해당 객체 수정
+ const newItems = structuredClone(oldData.items);
+
+ const firstItem = newItems[0];
+ if (!firstItem) {
+ return {
+ items: newItems,
+ };
+ }
+
+ firstItem.timeSlot2H = timeSlot2H;
+
+ const firstMenu = firstItem.menus?.[0];
+ if (firstMenu) {
+ firstMenu.menuName = menuName;
+ }
+
+ return {
+ items: newItems,
+ };
+ },
+ [],
+ );
+
+ /**
+ * 인기 메뉴 조합 쿼리 데이터 업데이트 함수
+ */
+ const updatePopularMenuCombinationData = useCallback(
+ (response: GetDashboardPopularMenuCombinationResponseDto) =>
+ (oldData?: GetPopularMenuCombinationResponseDto) => {
+ const { firstMenuName, secondMenuName } = response;
+
+ if (!oldData) {
+ return oldData;
+ }
+
+ // 깊은 복사 후 해당 객체 수정
+ const newItems = structuredClone(oldData.items);
+
+ const firstItem = newItems[0];
+ if (!firstItem) {
+ return {
+ items: newItems,
+ };
+ }
+
+ firstItem.baseMenuName = firstMenuName;
+
+ if (firstItem.pairedMenus) {
+ const newPairedMenus = [...firstItem.pairedMenus];
+ const firstPairedMenu = newPairedMenus[0];
+
+ if (firstPairedMenu) {
+ firstPairedMenu.menuName = secondMenuName;
+ }
+
+ firstItem.pairedMenus = newPairedMenus;
+ }
+
+ return {
+ items: newItems,
+ };
+ },
+ [],
+ );
+
+ const handleSseMessage = useCallback(
+ (message: EventSourceMessage) => {
+ if (message.event === 'connect') {
+ retryCountRef.current = 0;
+ }
+
+ if (isMetricCardCode(message.event)) {
+ try {
+ const response = JSON.parse(message.data);
+ // 매출 추이, 매출 패턴 제외 최신 데이터로 덮어쓰기
+ if (
+ isRealSalesMetricCardCode(message.event) ||
+ isOrderCountMetricCardCode(message.event) ||
+ isAveragePriceMetricCardCode(message.event) ||
+ isMenuSalesRankingMetricCardCode(message.event) ||
+ isIngredientConsumptionRankMetricCardCode(message.event) ||
+ isSalesTypeMetricCardCode(message.event) ||
+ isOrderChannelMetricCardCode(message.event) ||
+ isPaymentMethodMetricCardCode(message.event)
+ ) {
+ queryClient.setQueryData(
+ getCardDetailQueryKey(message.event),
+ response,
+ );
+ }
+
+ // 매출 추이 제일 최근 데이터만 업데이트
+ if (
+ isDailySalesTrendMetricCardCode(message.event) ||
+ isWeeklySalesTrendMetricCardCode(message.event) ||
+ isMonthlySalesTrendMetricCardCode(message.event)
+ ) {
+ queryClient.setQueryData(
+ getCardDetailQueryKey(message.event),
+ updateSalesTrendData(response),
+ );
+ }
+
+ // 피크타임 현재 시간 데이터만 업데이트
+ if (isPeakTimeMetricCardCode(message.event)) {
+ queryClient.setQueryData(
+ getCardDetailQueryKey(message.event),
+ updatePeakTimeData(response),
+ );
+ }
+
+ // 요일별 매출 패턴 제일 최근 데이터만 업데이트 및 최다 매출 요일 갱신
+ if (isWeekdaySalesPatternMetricCardCode(message.event)) {
+ queryClient.setQueryData(
+ getCardDetailQueryKey(message.event),
+ updateWeekdaySalesPatternData(response),
+ );
+ }
+
+ if (isTimeBasedMenuOrderCountMetricCardCode(message.event)) {
+ queryClient.setQueryData(
+ getCardDetailQueryKey(message.event),
+ updateTimeBasedMenuOrderCountData(response),
+ );
+ }
+
+ if (isPopularMenuCombinationMetricCardCode(message.event)) {
+ queryClient.setQueryData(
+ getCardDetailQueryKey(message.event),
+ updatePopularMenuCombinationData(response),
+ );
+ }
+ } catch (error) {
+ showBoundary(error);
+ }
+ }
+ },
+ [
+ queryClient,
+ getCardDetailQueryKey,
+ updatePeakTimeData,
+ updateTimeBasedMenuOrderCountData,
+ updateWeekdaySalesPatternData,
+ updateSalesTrendData,
+ updatePopularMenuCombinationData,
+ showBoundary,
+ ],
+ );
+
+ const handleSseError = useCallback(() => {
+ retryCountRef.current++;
+ // 지수 백오프
+ const backoff = Math.min(
+ 1000 * Math.pow(2, retryCountRef.current - 1),
+ 30000,
+ );
+ return backoff;
+ }, []);
+
+ useEffect(() => {
+ currentDashboardIdRef.current = currentDashboardId;
+ }, [currentDashboardId]);
+
+ useEffect(() => {
+ const abortController = new AbortController();
+
+ sseClient('/api/sse/connection', {
+ signal: abortController.signal,
+ onmessage: handleSseMessage,
+ onerror: handleSseError,
+ });
+
+ return () => {
+ abortController.abort();
+ };
+ }, [handleSseMessage, handleSseError]);
+};
diff --git a/frontend/src/mocks/analysis/analysisHandler.ts b/frontend/src/mocks/analysis/analysisHandler.ts
new file mode 100644
index 000000000..ae688362d
--- /dev/null
+++ b/frontend/src/mocks/analysis/analysisHandler.ts
@@ -0,0 +1,102 @@
+import { HttpResponse, passthrough } from 'msw';
+
+import {
+ isOrderChannelMetricCardCode,
+ isPaymentMethodMetricCardCode,
+ isSalesTypeMetricCardCode,
+ isWeekdaySalesPatternMetricCardCode,
+} from '@/constants/dashboard';
+import {
+ ORDER_CHANNEL,
+ PAYMENT_METHOD,
+ SALES_BY_DAY,
+ SALES_TYPE,
+} from '@/constants/sales';
+import type { SuccessResponse } from '@/services/shared';
+import type {
+ GetDetailSalesByDayResponseDto,
+ GetIncomeStructureByOrderChannelResponseDto,
+ GetIncomeStructureByPaymentMethodResponseDto,
+ GetIncomeStructureBySalesTypeResponseDto,
+} from '@/types/sales';
+
+import { mswHttp } from '../shared';
+
+const getHandler = [
+ mswHttp.get('/api/analysis/detail', ({ request }) => {
+ const url = new URL(request.url);
+ const cardCode = url.searchParams.get('analysisCardCode');
+
+ if (!cardCode) {
+ return passthrough();
+ }
+
+ if (isSalesTypeMetricCardCode(cardCode)) {
+ return HttpResponse.json<
+ SuccessResponse
+ >({
+ success: true,
+ message: 'Success',
+ data: {
+ insight: {
+ topType: 'DINE_IN',
+ topShare: 43,
+ deltaShare: 6.8,
+ },
+ items: SALES_TYPE.EXAMPLE_SALES_SOURCE_DATA,
+ },
+ });
+ }
+
+ if (isOrderChannelMetricCardCode(cardCode)) {
+ return HttpResponse.json<
+ SuccessResponse
+ >({
+ success: true,
+ message: 'Success',
+ data: {
+ insight: {
+ topType: 'KIOSK',
+ topShare: 50,
+ deltaShare: 4,
+ },
+ items: ORDER_CHANNEL.EXAMPLE_ORDER_CHANNEL_DATA,
+ },
+ });
+ }
+
+ if (isPaymentMethodMetricCardCode(cardCode)) {
+ return HttpResponse.json<
+ SuccessResponse
+ >({
+ success: true,
+ message: 'Success',
+ data: {
+ insight: {
+ topType: 'CASH',
+ topShare: 46,
+ deltaShare: 6.7,
+ },
+ items: PAYMENT_METHOD.EXAMPLE_PAYMENT_METHOD_DATA,
+ },
+ });
+ }
+
+ if (isWeekdaySalesPatternMetricCardCode(cardCode)) {
+ return HttpResponse.json>(
+ {
+ success: true,
+ message: 'Success',
+ data: {
+ topDay: '금',
+ isSignificant: false,
+ items: SALES_BY_DAY.EXAMPLE_DATA,
+ },
+ },
+ );
+ }
+ return passthrough();
+ }),
+];
+
+export const analysisHandler = [...getHandler];
diff --git a/frontend/src/mocks/analysis/index.ts b/frontend/src/mocks/analysis/index.ts
new file mode 100644
index 000000000..d38d158f0
--- /dev/null
+++ b/frontend/src/mocks/analysis/index.ts
@@ -0,0 +1 @@
+export { analysisHandler } from './analysisHandler';
diff --git a/frontend/src/mocks/auth/authHandler.ts b/frontend/src/mocks/auth/authHandler.ts
index 02ce27534..b5842bcf4 100644
--- a/frontend/src/mocks/auth/authHandler.ts
+++ b/frontend/src/mocks/auth/authHandler.ts
@@ -11,6 +11,7 @@ import { mswHttp } from '../shared';
const getHandler = [
mswHttp.get('/auth/status', ({ request }) => {
+ return passthrough();
// access token 갱신 테스트를 위해 첫 요청은 401 응답
if (request.headers.get('Authorization') === null) {
return HttpResponse.json(
diff --git a/frontend/src/mocks/data/dashboard/index.ts b/frontend/src/mocks/data/dashboard/index.ts
index ef182d1b2..21c924257 100644
--- a/frontend/src/mocks/data/dashboard/index.ts
+++ b/frontend/src/mocks/data/dashboard/index.ts
@@ -1 +1,5 @@
export { gridItems } from './gridItem';
+export {
+ dashboardMenuSalesRankItems,
+ dashboardMenuIngredientRankItems,
+} from './menu';
diff --git a/frontend/src/mocks/data/dashboard/menu/dashboardMenuIngredientRankItems.ts b/frontend/src/mocks/data/dashboard/menu/dashboardMenuIngredientRankItems.ts
new file mode 100644
index 000000000..6c030b6f3
--- /dev/null
+++ b/frontend/src/mocks/data/dashboard/menu/dashboardMenuIngredientRankItems.ts
@@ -0,0 +1,26 @@
+export const dashboardMenuIngredientRankItems = [
+ {
+ rank: 1,
+ itemName: '우유',
+ amount: 3920,
+ unit: 'ml',
+ },
+ {
+ rank: 2,
+ itemName: '케냐산 원두',
+ amount: 541200,
+ unit: 'g',
+ },
+ {
+ rank: 3,
+ itemName: '생딸기',
+ amount: 820,
+ unit: 'g',
+ },
+ {
+ rank: 4,
+ itemName: '바닐라 시럽',
+ amount: 400,
+ unit: 'ml',
+ },
+] as const;
diff --git a/frontend/src/mocks/data/dashboard/menu/dashboardMenuSalesRank.ts b/frontend/src/mocks/data/dashboard/menu/dashboardMenuSalesRank.ts
new file mode 100644
index 000000000..0d0395a2c
--- /dev/null
+++ b/frontend/src/mocks/data/dashboard/menu/dashboardMenuSalesRank.ts
@@ -0,0 +1,26 @@
+export const dashboardMenuSalesRankItems = [
+ {
+ rank: 1,
+ itemName: '제주 유기농 말차로 만든 부드러운 라떼 (ICE)',
+ amount: 999999999,
+ unit: '원',
+ },
+ {
+ rank: 2,
+ itemName: '아메리카노(ICE)',
+ amount: 541200,
+ unit: '원',
+ },
+ {
+ rank: 3,
+ itemName: '진한 초콜릿 칩 자바 칩 프라푸치노 휘핑 많이',
+ amount: 482000,
+ unit: '원',
+ },
+ {
+ rank: 4,
+ itemName: '아메리카노(HOT)',
+ amount: 41200,
+ unit: '원',
+ },
+] as const;
diff --git a/frontend/src/mocks/data/dashboard/menu/index.ts b/frontend/src/mocks/data/dashboard/menu/index.ts
new file mode 100644
index 000000000..e174c7a52
--- /dev/null
+++ b/frontend/src/mocks/data/dashboard/menu/index.ts
@@ -0,0 +1,2 @@
+export { dashboardMenuIngredientRankItems } from './dashboardMenuIngredientRankItems';
+export { dashboardMenuSalesRankItems } from './dashboardMenuSalesRank';
diff --git a/frontend/src/mocks/data/sales/paymentMethod.ts b/frontend/src/mocks/data/sales/paymentMethod.ts
index 1c30629a7..d2cb213a2 100644
--- a/frontend/src/mocks/data/sales/paymentMethod.ts
+++ b/frontend/src/mocks/data/sales/paymentMethod.ts
@@ -15,7 +15,7 @@ export const PAYMENT_METHOD_DATA: SalesSource[] = [
changeRate: 6.7,
},
{
- salesSourceType: SALES_SOURCE.PAYMENT_METHOD.MOBILE,
+ salesSourceType: SALES_SOURCE.PAYMENT_METHOD.EASY_PAY,
revenue: 7531000,
count: 75,
changeRate: -5.2,
diff --git a/frontend/src/mocks/data/sales/saleType.ts b/frontend/src/mocks/data/sales/saleType.ts
index 66c12c4fe..4ed65f512 100644
--- a/frontend/src/mocks/data/sales/saleType.ts
+++ b/frontend/src/mocks/data/sales/saleType.ts
@@ -9,7 +9,7 @@ export const SALE_TYPE_DATA: SalesSource[] = [
changeRate: -4.4,
},
{
- salesSourceType: SALES_SOURCE.SALE_TYPE.TAKEOUT,
+ salesSourceType: SALES_SOURCE.SALE_TYPE.TAKE_OUT,
revenue: 0,
count: 0,
changeRate: -6.7,
diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts
index 28925648d..0c9205b31 100644
--- a/frontend/src/mocks/handlers.ts
+++ b/frontend/src/mocks/handlers.ts
@@ -1,4 +1,5 @@
import { storeRegisterHandler } from './onboarding/store-register';
+import { analysisHandler } from './analysis';
import { authHandler } from './auth';
import { aiIngredientRecommendHandler, ingredientHandler } from './ingredient';
import { settingHandler } from './setting';
@@ -9,4 +10,5 @@ export const handlers = [
...authHandler,
...settingHandler,
...ingredientHandler,
+ ...analysisHandler,
];
diff --git a/frontend/src/mocks/onboarding/store-register/store-register-handler.ts b/frontend/src/mocks/onboarding/store-register/store-register-handler.ts
index f536f4021..da5d0c6c1 100644
--- a/frontend/src/mocks/onboarding/store-register/store-register-handler.ts
+++ b/frontend/src/mocks/onboarding/store-register/store-register-handler.ts
@@ -24,6 +24,7 @@ const postHandler = [
);
}),
mswHttp.post('/api/stores', () => {
+ return passthrough();
mockDb.hasStore = true;
return HttpResponse.json(
{
diff --git a/frontend/src/services/analysis/get.ts b/frontend/src/services/analysis/get.ts
new file mode 100644
index 000000000..a4283b442
--- /dev/null
+++ b/frontend/src/services/analysis/get.ts
@@ -0,0 +1,21 @@
+import type { GetAnalysisDetailQuery } from '@/types/analysis';
+
+import { authorizedApi } from '../shared';
+
+export const getAnalysisDetail = async (query: GetAnalysisDetailQuery) => {
+ const queryParams = new URLSearchParams();
+ Object.entries(query).forEach(([key, value]) => {
+ if (value === undefined) {
+ return;
+ }
+ queryParams.set(key, String(value));
+ });
+
+ const queryString = queryParams.toString();
+
+ const path = `/api/analysis/detail?${queryString}`;
+
+ const { data } = await authorizedApi.get(path);
+
+ return data;
+};
diff --git a/frontend/src/services/analysis/index.ts b/frontend/src/services/analysis/index.ts
new file mode 100644
index 000000000..d390bb856
--- /dev/null
+++ b/frontend/src/services/analysis/index.ts
@@ -0,0 +1 @@
+export { getAnalysisDetail } from './get';
diff --git a/frontend/src/services/dashboard/delete.ts b/frontend/src/services/dashboard/delete.ts
index c7845ce46..11f144379 100644
--- a/frontend/src/services/dashboard/delete.ts
+++ b/frontend/src/services/dashboard/delete.ts
@@ -1,4 +1,7 @@
-import type { DeleteDashboardParam } from '@/types/dashboard';
+import type {
+ DeleteDashboardParam,
+ DeleteDashboardSseSubscriptionRequestDto,
+} from '@/types/dashboard';
import { authorizedApi } from '../shared';
@@ -14,3 +17,11 @@ export const deleteDashboard = async (param: DeleteDashboardParam) => {
return data;
};
+
+export const deleteDashboardSseSubscription = async (
+ body: DeleteDashboardSseSubscriptionRequestDto,
+) => {
+ await authorizedApi.delete('/api/sse/subscriptions', {
+ body: JSON.stringify(body),
+ });
+};
diff --git a/frontend/src/services/dashboard/index.ts b/frontend/src/services/dashboard/index.ts
index 04cee248e..90e0e9916 100644
--- a/frontend/src/services/dashboard/index.ts
+++ b/frontend/src/services/dashboard/index.ts
@@ -1,6 +1,6 @@
export { getDashboardList, getDashboardCardList } from './get';
export { patchDashboardName } from './patch';
-export { postDashboard } from './post';
-export { deleteDashboard } from './delete';
+export { postDashboard, postDashboardSseSubscription } from './post';
+export { deleteDashboard, deleteDashboardSseSubscription } from './delete';
export { putDashboardCardList } from './put';
export { dashboardOptions } from './options';
diff --git a/frontend/src/services/dashboard/keys.ts b/frontend/src/services/dashboard/keys.ts
index fef6d4248..e2e87e23e 100644
--- a/frontend/src/services/dashboard/keys.ts
+++ b/frontend/src/services/dashboard/keys.ts
@@ -1,3 +1,5 @@
+import type { GetAnalysisDetailQuery } from '@/types/analysis';
+
export const dashboardKeys = {
all: ['dashboard'] as const,
list: () => [...dashboardKeys.all, 'list'] as const,
@@ -5,4 +7,14 @@ export const dashboardKeys = {
[...dashboardKeys.all, 'detail', dashboardId] as const,
cards: (dashboardId: number | string) =>
[...dashboardKeys.name(dashboardId), 'cards'] as const,
+ cardDetail: (dashboardId: number | string, query: GetAnalysisDetailQuery) =>
+ [
+ ...dashboardKeys.cards(dashboardId),
+ query.analysisCardCode,
+ {
+ customPeriod: query.customPeriod,
+ from: query.from ?? null,
+ to: query.to ?? null,
+ },
+ ] as const,
};
diff --git a/frontend/src/services/dashboard/options.ts b/frontend/src/services/dashboard/options.ts
index 25ed84db8..0c7718c25 100644
--- a/frontend/src/services/dashboard/options.ts
+++ b/frontend/src/services/dashboard/options.ts
@@ -1,5 +1,9 @@
import { queryOptions } from '@tanstack/react-query';
+import type { GetAnalysisDetailQuery } from '@/types/analysis';
+
+import { getAnalysisDetail } from '../analysis';
+
import { getDashboardCardList, getDashboardList } from './get';
import { dashboardKeys } from './keys';
@@ -15,4 +19,9 @@ export const dashboardOptions = {
queryFn: () => getDashboardCardList({ dashboardId }),
staleTime: 24 * 60 * 60 * 1000, // 24시간
}),
+ cardDetail: (dashboardId: number, query: GetAnalysisDetailQuery) =>
+ queryOptions({
+ queryKey: dashboardKeys.cardDetail(dashboardId, query),
+ queryFn: () => getAnalysisDetail(query),
+ }),
};
diff --git a/frontend/src/services/dashboard/post.ts b/frontend/src/services/dashboard/post.ts
index a0381c83b..a8203fe78 100644
--- a/frontend/src/services/dashboard/post.ts
+++ b/frontend/src/services/dashboard/post.ts
@@ -1,6 +1,7 @@
import type {
PostDashboardRequestDto,
PostDashboardResponseDto,
+ PostDashboardSseSubscriptionRequestDto,
} from '@/types/dashboard';
import { authorizedApi } from '../shared';
@@ -17,3 +18,11 @@ export const postDashboard = async (body: PostDashboardRequestDto) => {
);
return data;
};
+
+export const postDashboardSseSubscription = async (
+ body: PostDashboardSseSubscriptionRequestDto,
+) => {
+ await authorizedApi.post('/api/sse/subscriptions', {
+ body: JSON.stringify(body),
+ });
+};
diff --git a/frontend/src/services/shared/sseClient.ts b/frontend/src/services/shared/sseClient.ts
index 05ada47e9..886b8e08b 100644
--- a/frontend/src/services/shared/sseClient.ts
+++ b/frontend/src/services/shared/sseClient.ts
@@ -149,7 +149,9 @@ export const sseClient = (
const dispose = () => {
document.removeEventListener('visibilitychange', onVisibilitychange);
window.clearTimeout(retryTimer);
- currentRequestAbortController.abort();
+ if (!currentRequestAbortController.signal.aborted) {
+ currentRequestAbortController.abort();
+ }
};
customSignal?.addEventListener('abort', () => {
@@ -170,7 +172,8 @@ export const sseClient = (
* SSE 연결 생성
*/
async function create() {
- currentRequestAbortController = new AbortController();
+ const currentController = new AbortController();
+ currentRequestAbortController = currentController;
try {
const apiPath = `${API_BASE_URL}${url}`;
diff --git a/frontend/src/services/sse/delete.ts b/frontend/src/services/sse/delete.ts
new file mode 100644
index 000000000..ddcc8e8d5
--- /dev/null
+++ b/frontend/src/services/sse/delete.ts
@@ -0,0 +1,5 @@
+import { authorizedApi } from '../shared';
+
+export const deleteSseConnection = async () => {
+ await authorizedApi.delete('/api/sse/connection');
+};
diff --git a/frontend/src/services/sse/index.ts b/frontend/src/services/sse/index.ts
new file mode 100644
index 000000000..ff29d905b
--- /dev/null
+++ b/frontend/src/services/sse/index.ts
@@ -0,0 +1 @@
+export { deleteSseConnection } from './delete';
diff --git a/frontend/src/types/analysis/index.ts b/frontend/src/types/analysis/index.ts
new file mode 100644
index 000000000..d4cdbd878
--- /dev/null
+++ b/frontend/src/types/analysis/index.ts
@@ -0,0 +1 @@
+export type { GetAnalysisDetailQuery } from './query';
diff --git a/frontend/src/types/analysis/query/getAnalysisDetail.ts b/frontend/src/types/analysis/query/getAnalysisDetail.ts
new file mode 100644
index 000000000..b2975ca2b
--- /dev/null
+++ b/frontend/src/types/analysis/query/getAnalysisDetail.ts
@@ -0,0 +1,8 @@
+import type { MetricCardCode } from '@/constants/dashboard';
+
+export interface GetAnalysisDetailQuery {
+ analysisCardCode: MetricCardCode;
+ customPeriod: boolean;
+ from?: string;
+ to?: string;
+}
diff --git a/frontend/src/types/analysis/query/index.ts b/frontend/src/types/analysis/query/index.ts
new file mode 100644
index 000000000..b8a11e8b5
--- /dev/null
+++ b/frontend/src/types/analysis/query/index.ts
@@ -0,0 +1 @@
+export type { GetAnalysisDetailQuery } from './getAnalysisDetail';
diff --git a/frontend/src/types/dashboard/dto/deleteDashboardSseSubscriptionDto.ts b/frontend/src/types/dashboard/dto/deleteDashboardSseSubscriptionDto.ts
new file mode 100644
index 000000000..91d7630a1
--- /dev/null
+++ b/frontend/src/types/dashboard/dto/deleteDashboardSseSubscriptionDto.ts
@@ -0,0 +1,5 @@
+import type { MetricCardCode } from '@/constants/dashboard';
+
+export interface DeleteDashboardSseSubscriptionRequestDto {
+ topics: MetricCardCode[];
+}
diff --git a/frontend/src/types/dashboard/dto/index.ts b/frontend/src/types/dashboard/dto/index.ts
index 07bd9cffa..e4d832b88 100644
--- a/frontend/src/types/dashboard/dto/index.ts
+++ b/frontend/src/types/dashboard/dto/index.ts
@@ -9,3 +9,5 @@ export type {
PutDashboardCardListRequestDto,
PutDashboardCardListResponseDto,
} from './putDashboardCardListDto';
+export type { PostDashboardSseSubscriptionRequestDto } from './postDashboardSseSubscriptionDto';
+export type { DeleteDashboardSseSubscriptionRequestDto } from './deleteDashboardSseSubscriptionDto';
diff --git a/frontend/src/types/dashboard/dto/postDashboardSseSubscriptionDto.ts b/frontend/src/types/dashboard/dto/postDashboardSseSubscriptionDto.ts
new file mode 100644
index 000000000..637e5572a
--- /dev/null
+++ b/frontend/src/types/dashboard/dto/postDashboardSseSubscriptionDto.ts
@@ -0,0 +1,5 @@
+import type { MetricCardCode } from '@/constants/dashboard';
+
+export interface PostDashboardSseSubscriptionRequestDto {
+ topics: MetricCardCode[];
+}
diff --git a/frontend/src/types/dashboard/index.ts b/frontend/src/types/dashboard/index.ts
index 92b2767c9..8b3b27b52 100644
--- a/frontend/src/types/dashboard/index.ts
+++ b/frontend/src/types/dashboard/index.ts
@@ -8,6 +8,8 @@ export type {
GetDashboardCardListResponseDto,
PutDashboardCardListRequestDto,
PutDashboardCardListResponseDto,
+ PostDashboardSseSubscriptionRequestDto,
+ DeleteDashboardSseSubscriptionRequestDto,
} from './dto';
export type {
PatchDashboardNameParam,
diff --git a/frontend/src/types/menu/dto/getDetailTimeSlotMenuOrderCountDto.ts b/frontend/src/types/menu/dto/getDetailTimeSlotMenuOrderCountDto.ts
new file mode 100644
index 000000000..c5c46a33d
--- /dev/null
+++ b/frontend/src/types/menu/dto/getDetailTimeSlotMenuOrderCountDto.ts
@@ -0,0 +1,14 @@
+interface TimeSlotMenuOrderCountItem {
+ menuName: string;
+ orderCount: number;
+}
+
+interface TimeSlotMenuGroupItem {
+ timeSlot2H: number;
+ totalOrderCount: number;
+ menus: TimeSlotMenuOrderCountItem[];
+}
+
+export interface GetDetailTimeSlotMenuOrderCountResponseDto {
+ items: TimeSlotMenuGroupItem[];
+}
diff --git a/frontend/src/types/menu/dto/getPopularMenuCombinationDto.ts b/frontend/src/types/menu/dto/getPopularMenuCombinationDto.ts
index fe305066b..0cb3db29d 100644
--- a/frontend/src/types/menu/dto/getPopularMenuCombinationDto.ts
+++ b/frontend/src/types/menu/dto/getPopularMenuCombinationDto.ts
@@ -2,7 +2,7 @@ export interface PairedMenu {
menuName: string;
count: number;
}
-// PoplularMenuCombination 예시
+// PopularMenuCombination 예시
// {
// "baseMenuName": "불고기 버거",
// "pairedMenus": [
@@ -10,11 +10,11 @@ export interface PairedMenu {
// { "menuName": "콜라", "count": 70 }
// ]
// }
-export interface PoplularMenuCombination {
- baseMenuName: string;
- pairedMenus: PairedMenu[];
+export interface PopularMenuCombination {
+ baseMenuName: string | null;
+ pairedMenus: PairedMenu[] | null;
}
export interface GetPopularMenuCombinationResponseDto {
- items: PoplularMenuCombination[];
+ items: PopularMenuCombination[];
}
diff --git a/frontend/src/types/menu/dto/index.ts b/frontend/src/types/menu/dto/index.ts
index b67587a98..db682ccd0 100644
--- a/frontend/src/types/menu/dto/index.ts
+++ b/frontend/src/types/menu/dto/index.ts
@@ -8,7 +8,8 @@ export type {
} from './getIngredientUsageRankingDto';
export type {
GetPopularMenuCombinationResponseDto,
- PoplularMenuCombination,
+ PopularMenuCombination,
} from './getPopularMenuCombinationDto';
export type { GetDashboardTimeSlotMenuOrderCountResponseDto } from './getDashboardTimeSlotMenuOrderCountDto';
export type { GetDashboardPopularMenuCombinationResponseDto } from './getDashboardPopularMenuCombinationDto';
+export type { GetDetailTimeSlotMenuOrderCountResponseDto } from './getDetailTimeSlotMenuOrderCountDto';
diff --git a/frontend/src/types/menu/index.ts b/frontend/src/types/menu/index.ts
index 2caf694bb..8358d79d4 100644
--- a/frontend/src/types/menu/index.ts
+++ b/frontend/src/types/menu/index.ts
@@ -4,10 +4,13 @@ export type { MenuSalesRank } from './menuSalesRank';
export type { CategoriesRevenue } from './categoriesRevenue';
export type { DashboardRankItem } from './dashboard-menu-ranking';
export type {
- GetDashboardPopularMenuCombinationResponseDto,
- GetDashboardTimeSlotMenuOrderCountResponseDto,
GetMenuSalesRankingResponseDto,
- GetIngredientUsageRankingResponseDto,
MenuSales,
+ GetIngredientUsageRankingResponseDto,
IngredientUsage,
+ GetPopularMenuCombinationResponseDto,
+ PopularMenuCombination,
+ GetDashboardTimeSlotMenuOrderCountResponseDto,
+ GetDashboardPopularMenuCombinationResponseDto,
+ GetDetailTimeSlotMenuOrderCountResponseDto,
} from './dto';
diff --git a/frontend/src/types/sales/dashboard-sales-income/salesIncomeStructureInsight.ts b/frontend/src/types/sales/dashboard-sales-income/salesIncomeStructureInsight.ts
index 3612df04c..7d2c434db 100644
--- a/frontend/src/types/sales/dashboard-sales-income/salesIncomeStructureInsight.ts
+++ b/frontend/src/types/sales/dashboard-sales-income/salesIncomeStructureInsight.ts
@@ -1,17 +1,12 @@
-export type SalesIncomeStructureTopType =
- | '홀'
- | '포장'
- | '배달'
- | 'POS'
- | '키오스크'
- | '배달앱'
- | '카드'
- | '현금'
- | '간편결제'
- | '기타';
+import type { SALES_SOURCE } from '@/constants/sales';
-export interface SalesIncomeStructureInsight {
- topType: SalesIncomeStructureTopType;
+export type SalesIncomeStructureTopType =
+ keyof (typeof SALES_SOURCE)[T];
+
+export interface SalesIncomeStructureInsight<
+ T extends keyof typeof SALES_SOURCE,
+> {
+ topType: SalesIncomeStructureTopType;
topShare: number;
deltaShare: number;
showDeltaText?: boolean;
diff --git a/frontend/src/types/sales/dto/getIncomeStructureByOrderChannelDto.ts b/frontend/src/types/sales/dto/getIncomeStructureByOrderChannelDto.ts
new file mode 100644
index 000000000..020e04666
--- /dev/null
+++ b/frontend/src/types/sales/dto/getIncomeStructureByOrderChannelDto.ts
@@ -0,0 +1,18 @@
+import type { SALES_SOURCE } from '@/constants/sales';
+
+import type { SalesIncomeStructureInsight } from '../dashboard-sales-income';
+
+interface OrderChannelItem {
+ orderChannel: keyof typeof SALES_SOURCE.ORDER_METHOD;
+ salesAmount: number;
+ orderCount: number;
+ share: number;
+ deltaShare: number;
+}
+
+export interface GetIncomeStructureByOrderChannelResponseDto {
+ insight: SalesIncomeStructureInsight<
+ Extract
+ >;
+ items: OrderChannelItem[];
+}
diff --git a/frontend/src/types/sales/dto/getIncomeStructureByOrderMethodDto.ts b/frontend/src/types/sales/dto/getIncomeStructureByOrderMethodDto.ts
deleted file mode 100644
index 2266ac0c1..000000000
--- a/frontend/src/types/sales/dto/getIncomeStructureByOrderMethodDto.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-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
index 31de5c183..219e8662f 100644
--- a/frontend/src/types/sales/dto/getIncomeStructureByPaymentMethodDto.ts
+++ b/frontend/src/types/sales/dto/getIncomeStructureByPaymentMethodDto.ts
@@ -1,13 +1,9 @@
-import type {
- SalesIncomeStructureInsight,
- SalesIncomeStructureTopType,
-} from '../dashboard-sales-income';
+import { SALES_SOURCE } from '@/constants/sales';
+
+import type { SalesIncomeStructureInsight } from '../dashboard-sales-income';
interface PaymentMethodItem {
- payMethod: Extract<
- SalesIncomeStructureTopType,
- '카드' | '현금' | '간편결제' | '기타'
- >;
+ payMethod: keyof typeof SALES_SOURCE.PAYMENT_METHOD;
salesAmount: number;
orderCount: number;
share: number;
@@ -15,6 +11,8 @@ interface PaymentMethodItem {
}
export interface GetIncomeStructureByPaymentMethodResponseDto {
- insight: SalesIncomeStructureInsight;
+ insight: SalesIncomeStructureInsight<
+ Extract
+ >;
items: PaymentMethodItem[];
}
diff --git a/frontend/src/types/sales/dto/getIncomeStructureBySalesTypeDto.ts b/frontend/src/types/sales/dto/getIncomeStructureBySalesTypeDto.ts
index 6aa77de18..98ed13bd3 100644
--- a/frontend/src/types/sales/dto/getIncomeStructureBySalesTypeDto.ts
+++ b/frontend/src/types/sales/dto/getIncomeStructureBySalesTypeDto.ts
@@ -1,10 +1,9 @@
-import type {
- SalesIncomeStructureInsight,
- SalesIncomeStructureTopType,
-} from '../dashboard-sales-income';
+import type { SALES_SOURCE } from '@/constants/sales';
+
+import type { SalesIncomeStructureInsight } from '../dashboard-sales-income';
interface SalesTypeItem {
- salesType: Extract;
+ salesType: keyof typeof SALES_SOURCE.SALE_TYPE;
salesAmount: number;
orderCount: number;
share: number;
@@ -12,6 +11,8 @@ interface SalesTypeItem {
}
export interface GetIncomeStructureBySalesTypeResponseDto {
- insight: SalesIncomeStructureInsight;
+ insight: SalesIncomeStructureInsight<
+ Extract
+ >;
items: SalesTypeItem[];
}
diff --git a/frontend/src/types/sales/dto/getRealTimeSalesDto.ts b/frontend/src/types/sales/dto/getRealSalesDto.ts
similarity index 68%
rename from frontend/src/types/sales/dto/getRealTimeSalesDto.ts
rename to frontend/src/types/sales/dto/getRealSalesDto.ts
index 411d5a155..ad63a0683 100644
--- a/frontend/src/types/sales/dto/getRealTimeSalesDto.ts
+++ b/frontend/src/types/sales/dto/getRealSalesDto.ts
@@ -1,4 +1,4 @@
-export interface GetRealTimeSalesResponseDto {
+export interface GetRealSalesResponseDto {
netAmount: number;
differenceAmount: number;
changeRate: number;
diff --git a/frontend/src/types/sales/dto/index.ts b/frontend/src/types/sales/dto/index.ts
index 2e7cec780..b200bd9b8 100644
--- a/frontend/src/types/sales/dto/index.ts
+++ b/frontend/src/types/sales/dto/index.ts
@@ -1,8 +1,8 @@
-export type { GetRealTimeSalesResponseDto } from './getRealTimeSalesDto';
+export type { GetRealSalesResponseDto } from './getRealSalesDto';
export type { GetOrderCountResponseDto } from './getOrderCountDto';
export type { GetAveragePriceResponseDto } from './getAveragePriceDto';
export type { GetIncomeStructureBySalesTypeResponseDto } from './getIncomeStructureBySalesTypeDto';
-export type { GetIncomeStructureByOrderMethodResponseDto } from './getIncomeStructureByOrderMethodDto';
+export type { GetIncomeStructureByOrderChannelResponseDto } from './getIncomeStructureByOrderChannelDto';
export type { GetIncomeStructureByPaymentMethodResponseDto } from './getIncomeStructureByPaymentMethodDto';
export type {
GetDetailPeakTimeResponseDto,
diff --git a/frontend/src/types/sales/index.ts b/frontend/src/types/sales/index.ts
index de423677a..b690a821d 100644
--- a/frontend/src/types/sales/index.ts
+++ b/frontend/src/types/sales/index.ts
@@ -1,10 +1,10 @@
export type { SalesSource } from './salesSource';
export type {
- GetRealTimeSalesResponseDto,
+ GetRealSalesResponseDto,
GetOrderCountResponseDto,
GetAveragePriceResponseDto,
GetIncomeStructureBySalesTypeResponseDto,
- GetIncomeStructureByOrderMethodResponseDto,
+ GetIncomeStructureByOrderChannelResponseDto,
GetIncomeStructureByPaymentMethodResponseDto,
GetDetailPeakTimeResponseDto,
GetDashboardPeakTimeResponseDto,
@@ -18,3 +18,4 @@ export type {
SalesByDaySummary,
SalesByDayItem,
} from './dashboard-sales-pattern';
+export type { SalesTrendItem } from './dashboard-sales-trend';
diff --git a/frontend/src/utils/sales/dashboard-current-sales/getSalesCurrentComparisonMessage.ts b/frontend/src/utils/sales/dashboard-current-sales/getSalesCurrentComparisonMessage.ts
index 5579c97e6..f8212c6f8 100644
--- a/frontend/src/utils/sales/dashboard-current-sales/getSalesCurrentComparisonMessage.ts
+++ b/frontend/src/utils/sales/dashboard-current-sales/getSalesCurrentComparisonMessage.ts
@@ -21,7 +21,7 @@ export const getSalesCurrentComparisonMessage = ({
comparisonAmount,
unit,
}: GetSalesCurrentComparisonMessageArgs): MessageToken[] => {
- const weekday = DAY_OF_WEEK_LIST[new Date().getDay()];
+ const weekday = DAY_OF_WEEK_LIST[(new Date().getDay() + 6) % 7];
const PERIOD_TEXT = {
[PERIOD_PRESETS.dayWeekMonth.today]: `지난주 ${weekday}요일`,
diff --git a/frontend/src/utils/sales/dashboard-sales-income/getSalesIncomeStructureComparisonMessage.ts b/frontend/src/utils/sales/dashboard-sales-income/getSalesIncomeStructureComparisonMessage.ts
index d8406a238..52d2436da 100644
--- a/frontend/src/utils/sales/dashboard-sales-income/getSalesIncomeStructureComparisonMessage.ts
+++ b/frontend/src/utils/sales/dashboard-sales-income/getSalesIncomeStructureComparisonMessage.ts
@@ -1,3 +1,4 @@
+import type { SALES_SOURCE, SalesSourceType } from '@/constants/sales';
import { PERIOD_PRESETS } from '@/constants/shared';
import type { SalesIncomeStructureInsight } from '@/types/sales/dashboard-sales-income/salesIncomeStructureInsight';
import { formatNumber, type ValueOf } from '@/utils/shared';
@@ -7,15 +8,16 @@ import { createMessageToken, type MessageToken } from '../dashboard';
const DELTA_SHARE_THRESHOLD = 3;
interface GetSalesIncomeStructureComparisonMessageArgs extends Omit<
- SalesIncomeStructureInsight,
- 'showDeltaText' | 'showFocusText'
+ SalesIncomeStructureInsight,
+ 'showDeltaText' | 'showFocusText' | 'topType'
> {
periodType: ValueOf;
+ topTypeLabel: SalesSourceType;
}
export const getSalesIncomeStructureComparisonMessage = ({
periodType,
- topType,
+ topTypeLabel,
topShare,
deltaShare,
}: GetSalesIncomeStructureComparisonMessageArgs): MessageToken[] => {
@@ -26,7 +28,7 @@ export const getSalesIncomeStructureComparisonMessage = ({
return [
createMessageToken('최근 7일 대비 '),
createMessageToken(
- `${topType} 비중이 ${deltaShare >= 0 ? '+' : ''}${formatNumber(deltaShare)}%p `,
+ `${topTypeLabel} 비중이 ${deltaShare >= 0 ? '+' : ''}${formatNumber(deltaShare)}%p `,
true,
deltaShare >= 0 ? 'primary' : 'negative',
),
@@ -37,13 +39,13 @@ export const getSalesIncomeStructureComparisonMessage = ({
if (topShare >= 60) {
return [
createMessageToken('매출이 '),
- createMessageToken(`${topType}(${formatNumber(topShare)}%)`, true),
+ createMessageToken(`${topTypeLabel}(${formatNumber(topShare)}%)`, true),
createMessageToken('에 집중돼 있어요.'),
];
}
return [
- createMessageToken(`${topType}(${formatNumber(topShare)}%) `, true),
+ createMessageToken(`${topTypeLabel}(${formatNumber(topShare)}%) `, true),
createMessageToken('매출이 가장 많아요.'),
];
};
diff --git a/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternByDayMessage.ts b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternByDayMessage.ts
index 7c0f65e85..aad09a939 100644
--- a/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternByDayMessage.ts
+++ b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternByDayMessage.ts
@@ -14,6 +14,14 @@ export const getSalesPatternByDayMessage = ({
topDay,
isSignificant,
}: GetSalesPatternByDayMessageArgs): MessageToken[] => {
+ if (!topDay) {
+ return [
+ createMessageToken('데이터가 더 쌓이면 '),
+ createMessageToken('가장 매출이 높은 요일', true, 'primary'),
+ createMessageToken('을 알려드릴게요.'),
+ ];
+ }
+
if (isSignificant) {
return [
createMessageToken(`${topDay}요일`, true, 'primary'),
diff --git a/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternPeakTimeMessage.ts b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternPeakTimeMessage.ts
index 0b5f9ea0b..f573730e0 100644
--- a/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternPeakTimeMessage.ts
+++ b/frontend/src/utils/sales/dashboard-sales-pattern/getSalesPatternPeakTimeMessage.ts
@@ -13,6 +13,14 @@ export const getSalesPatternPeakTimeMessage = ({
comparisonPeak,
beforeComparisonPeak,
}: GetSalesPatternPeakTimeMessageArgs): MessageToken[] => {
+ if (!todayPeak || !comparisonPeak) {
+ return [
+ createMessageToken('데이터가 더 쌓이면 '),
+ createMessageToken('가장 바쁜 시간', true, 'primary'),
+ createMessageToken('을 알려드릴게요.'),
+ ];
+ }
+
if (beforeComparisonPeak) {
return [
createMessageToken('오늘은 '),
diff --git a/frontend/src/utils/sales/sales-overview/getPeriodComparisonMessage.ts b/frontend/src/utils/sales/sales-overview/getPeriodComparisonMessage.ts
index 6cd4add20..271f3a9da 100644
--- a/frontend/src/utils/sales/sales-overview/getPeriodComparisonMessage.ts
+++ b/frontend/src/utils/sales/sales-overview/getPeriodComparisonMessage.ts
@@ -4,7 +4,7 @@ import type { ValueOf } from '@/utils/shared';
export const getPeriodComparisonMessage = (
type: ValueOf,
) => {
- const weekday = DAY_OF_WEEK_LIST[new Date().getDay()];
+ const weekday = DAY_OF_WEEK_LIST[(new Date().getDay() + 6) % 7];
switch (type) {
case PERIOD_PRESETS.dayWeekMonth.today:
diff --git a/frontend/src/utils/shared/bar-chart/getBarHeight.ts b/frontend/src/utils/shared/bar-chart/getBarHeight.ts
index 3c21e45e5..4fdc1d839 100644
--- a/frontend/src/utils/shared/bar-chart/getBarHeight.ts
+++ b/frontend/src/utils/shared/bar-chart/getBarHeight.ts
@@ -1,6 +1,7 @@
import { BAR_CHART } from '@/constants/shared';
-// 바 전체 높이 계산 (바의 상단 y좌표 부터 x축 또는 svg 하단까지의 거리)
+// 막대 높이 계산:
+// `y`(막대 상단)부터 x축 또는 SVG 하단까지의 세로 거리
export const getBarHeight = ({
y,
hasXAxis,
@@ -12,8 +13,10 @@ export const getBarHeight = ({
}) => {
const { XAXIS_Y_OFFSET, XAXIS_STROKE_WIDTH } = BAR_CHART;
if (hasXAxis) {
- // x축이 있을 때는 x축의 y위치 만큼을 빼고 축 높이의 0.5배 만큼 더 빼줘야 함
- return viewBoxHeight - XAXIS_Y_OFFSET - y - XAXIS_STROKE_WIDTH / 2; // x축이 있을 때 바 높이는 y좌표 ~ x 축까지 거리
+ // x축이 있으면 바닥 기준은 SVG 하단이 아니라 x축 위치.
+ // x축 선 두께가 중심 기준으로 그려지므로, 실제 높이 보정을 위해 stroke의 절반을 추가로 차감.
+ return viewBoxHeight - XAXIS_Y_OFFSET - y - XAXIS_STROKE_WIDTH / 2;
}
- return viewBoxHeight - y; // x축이 없을 떄 바 높이는 y좌표 ~ svg 최하단 까지 거리
+ // x축이 없으면 SVG 하단(viewBoxHeight)을 바닥 기준으로 사용.
+ return viewBoxHeight - y;
};
diff --git a/frontend/src/utils/shared/index.ts b/frontend/src/utils/shared/index.ts
index 61da95c5c..d8ff7d10b 100644
--- a/frontend/src/utils/shared/index.ts
+++ b/frontend/src/utils/shared/index.ts
@@ -39,8 +39,9 @@ export {
} from './doughnut-chart';
export { createPeriodTypeProvider } from './period-select';
-export { getNextHour } from './getNextHour';
export { assertNever } from './assertNever';
+export type { Nullable } from './nullable';
+export { getNextHour } from './getNextHour';
export { getCoordinate } from './getCoordinate';
export {
getBarSegmentInfoList,
diff --git a/frontend/src/utils/shared/nullable.ts b/frontend/src/utils/shared/nullable.ts
new file mode 100644
index 000000000..4b1276e32
--- /dev/null
+++ b/frontend/src/utils/shared/nullable.ts
@@ -0,0 +1,3 @@
+export type Nullable = {
+ [P in keyof T]?: T[P];
+};