diff --git a/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx b/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx index 8e52d0605..eef9343ab 100644 --- a/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx +++ b/frontend/src/components/dashboard/dashboard-edit/EditCardContent.tsx @@ -1,3 +1,9 @@ +import { + IngredientUsageRankingCardContent, + MenuSalesRankingCardContent, + PopularMenuCombinationCardContent, + TimeSlotMenuOrderCountCardContent, +} from '@/components/menu'; import { AveragePriceContent, OrderCountContent, @@ -10,6 +16,12 @@ import { SalesTypeContent, } from '@/components/sales'; import type { MetricCardCode } from '@/constants/dashboard'; +import { + INGREDIENT_USAGE_RANKING, + MENU_SALES_RANKING, + ORDER_COUNT as MENU_ORDER_COUNT, + POPULAR_MENU_COMBINATION, +} from '@/constants/menu'; import { AVERAGE_PRICE, ORDER_COUNT, @@ -66,6 +78,12 @@ const { EXAMPLE_IS_SIGNIFICANT: SALES_BY_DAY_EXAMPLE_IS_SIGNIFICANT, } = SALES_BY_DAY; const { EXAMPLE_DATA: SALES_TREND_EXAMPLE_DATA } = SALES_TREND; +const { EXAMPLE_HAS_INGREDIENT, EXAMPLE_INGREDIENT_USAGE_RANKING_ITEMS } = + INGREDIENT_USAGE_RANKING; +const { EXAMPLE_MENU_SALES_RANKING_ITEMS } = MENU_SALES_RANKING; +const { EXAMPLE_TIME_SLOT_2H, EXAMPLE_MENU_NAME } = MENU_ORDER_COUNT; +const { EXAMPLE_FIRST_MENU_NAME, EXAMPLE_SECOND_MENU_NAME } = + POPULAR_MENU_COMBINATION; export const EditCardContent = ({ cardCode }: EditCardContentProps) => { switch (cardCode) { @@ -163,6 +181,33 @@ export const EditCardContent = ({ cardCode }: EditCardContentProps) => { isSignificant={SALES_BY_DAY_EXAMPLE_IS_SIGNIFICANT} /> ); + 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/menu/dashboard-menu-combination/PopularMenuCombinationCardContent.tsx b/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationCardContent.tsx new file mode 100644 index 000000000..2e36f77cb --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationCardContent.tsx @@ -0,0 +1,24 @@ +import { POPULAR_MENU_COMBINATION } from '@/constants/menu'; +import type { GetDashboardPopularMenuCombinationResponseDto } from '@/types/menu'; + +import { PopularMenuCombinationContent } from './PopularMenuCombinationContent'; + +const { EXAMPLE_FIRST_MENU_NAME, EXAMPLE_SECOND_MENU_NAME } = + POPULAR_MENU_COMBINATION; +interface PopularMenuCombinationCardContentProps extends GetDashboardPopularMenuCombinationResponseDto { + className?: string; +} + +export const PopularMenuCombinationCardContent = ({ + firstMenuName = EXAMPLE_FIRST_MENU_NAME, + secondMenuName = EXAMPLE_SECOND_MENU_NAME, + className, +}: PopularMenuCombinationCardContentProps) => { + return ( + + ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationContent.tsx b/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationContent.tsx new file mode 100644 index 000000000..9b27e2e0f --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-combination/PopularMenuCombinationContent.tsx @@ -0,0 +1,33 @@ +import { cn } from '@/utils/shared'; + +interface PopularMenuCombinationContentProps { + className?: string; + baseMenuName?: string; + pairedMenu?: string; +} + +export const PopularMenuCombinationContent = ({ + className, + baseMenuName, + pairedMenu, +}: PopularMenuCombinationContentProps) => { + return ( +

+ 최고 인기 조합은 + + {baseMenuName} + + + + &{pairedMenu} + + 입니다 + +

+ ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-combination/index.ts b/frontend/src/components/menu/dashboard-menu-combination/index.ts new file mode 100644 index 000000000..1f9a4e98b --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-combination/index.ts @@ -0,0 +1 @@ +export { PopularMenuCombinationCardContent } from './PopularMenuCombinationCardContent'; diff --git a/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountCardContent.tsx b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountCardContent.tsx new file mode 100644 index 000000000..db2e4a7a6 --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountCardContent.tsx @@ -0,0 +1,24 @@ +import { ORDER_COUNT } from '@/constants/menu'; +import type { GetDashboardTimeSlotMenuOrderCountResponseDto } from '@/types/menu'; + +import { TimeSlotMenuOrderCountContent } from './TimeSlotMenuOrderCountContent'; + +const { EXAMPLE_TIME_SLOT_2H, EXAMPLE_MENU_NAME } = ORDER_COUNT; +// 현재 주문건수가 가장 많은 메뉴 카드 +interface TimeSlotMenuOrderCountCardContentProps extends GetDashboardTimeSlotMenuOrderCountResponseDto { + className?: string; +} + +export const TimeSlotMenuOrderCountCardContent = ({ + timeSlot2H = EXAMPLE_TIME_SLOT_2H, + menuName = EXAMPLE_MENU_NAME, + className, +}: TimeSlotMenuOrderCountCardContentProps) => { + return ( + + ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountContent.tsx b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountContent.tsx new file mode 100644 index 000000000..feeb5963d --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-order/TimeSlotMenuOrderCountContent.tsx @@ -0,0 +1,28 @@ +import { cn, getNextHour } from '@/utils/shared'; + +interface TimeSlotMenuOrderCountContentProps { + className?: string; + timeSlot2H: number; + menuName: string; +} +// 현재 주문건수가 가장 많은 메뉴 카드 +export const TimeSlotMenuOrderCountContent = ({ + className, + timeSlot2H, + menuName, +}: TimeSlotMenuOrderCountContentProps) => { + return ( +

+ + {menuName} + {`는 ${timeSlot2H}~${getNextHour(timeSlot2H)}시`} + + 주문이 가장 많아요 +

+ ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-order/index.ts b/frontend/src/components/menu/dashboard-menu-order/index.ts new file mode 100644 index 000000000..a52e7baaa --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-order/index.ts @@ -0,0 +1 @@ +export { TimeSlotMenuOrderCountCardContent } from './TimeSlotMenuOrderCountCardContent'; diff --git a/frontend/src/components/menu/dashboard-menu-ranking/DashboardRankingContent.tsx b/frontend/src/components/menu/dashboard-menu-ranking/DashboardRankingContent.tsx new file mode 100644 index 000000000..38f43b7ae --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-ranking/DashboardRankingContent.tsx @@ -0,0 +1,61 @@ +import type { ReactNode } from 'react'; + +import type { DashboardRankItem } from '@/types/menu'; +import { cn } from '@/utils/shared'; + +import { RankItem } from './RankItem'; + +interface DashboardRankingContentProps { + className?: string; + children?: ReactNode; + tHeadLabels?: string[]; +} + +// 메뉴분석에서 '메뉴 매출 랭킹', '식자재 소진량 랭킹' 카드에서 공통으로 사용하는 순위 컴포넌트 +export const DashboardRankingContent = ({ + className, + children, + tHeadLabels = ['순위', '메뉴명', '매출액'], // 테이블 각 열의 이름, sr-only 클래스로 화면에서는 보이지 않지만 스크린 리더로 읽을 수 있도록 함 +}: DashboardRankingContentProps) => { + return ( + + + + + + + + {/* 테이블 각 열의 이름 지정*/} + + {tHeadLabels.map((label) => ( + + ))} + + + {children} +
{label}
+ ); +}; + +interface DashboardRankingContentTableBodyProps { + rankItems: DashboardRankItem[]; +} +const DashboardRankingContentTableBody = ({ + rankItems, +}: DashboardRankingContentTableBodyProps) => { + return ( + + {rankItems.map(({ rank, itemName, totalAmount, unit }) => ( + + ))} + + ); +}; + +DashboardRankingContent.TableBody = DashboardRankingContentTableBody; diff --git a/frontend/src/components/menu/dashboard-menu-ranking/IngredientUnregisteredContent.tsx b/frontend/src/components/menu/dashboard-menu-ranking/IngredientUnregisteredContent.tsx new file mode 100644 index 000000000..77a75d681 --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-ranking/IngredientUnregisteredContent.tsx @@ -0,0 +1,28 @@ +import { CDN_BASE_URL } from '@/constants/shared'; +import { cn } from '@/utils/shared'; + +interface IngredientUnregisteredContentProps { + className?: string; +} + +// 등록된 식재료가 없는 경우 화면에 보여질 컴포넌트 +export const IngredientUnregisteredContent = ({ + className, +}: IngredientUnregisteredContentProps) => { + return ( +
+
+ 식재료 미등록 이미지 + 식재료 미등록 + + 자동으로 식재료 파악하고, +
간편하게 매장을 운영하세요. +
+
+
+ ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-ranking/IngredientUsageRankingCardContent.tsx b/frontend/src/components/menu/dashboard-menu-ranking/IngredientUsageRankingCardContent.tsx new file mode 100644 index 000000000..7a0950cd5 --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-ranking/IngredientUsageRankingCardContent.tsx @@ -0,0 +1,58 @@ +// 대시보드>메뉴분석에서 식재료별 소진량 랭킹 카드 +import { useMemo } from 'react'; + +import { DASHBOARD_RANKING } from '@/constants/menu'; +import type { DashboardRankItem } from '@/types/menu'; +import type { + GetIngredientUsageRankingResponseDto, + IngredientUsage, +} from '@/types/menu'; + +import { DashboardRankingContent } from './DashboardRankingContent'; +import { IngredientUnregisteredContent } from './IngredientUnregisteredContent'; + +// dto를 대시보드의 식재료 소진량 랭킹 카드 UI에서 사용하는 데이터 형태로 변환 +interface GetDashboardIngredientRankItemsParams { + items: IngredientUsage[]; +} +const getDashboardIngredientRankItems = ({ + items, +}: GetDashboardIngredientRankItemsParams): DashboardRankItem[] => { + return items + .map((item, index) => ({ + rank: index + 1, + itemName: item.ingredientName, + totalAmount: item.totalQuantity, + unit: item.baseUnit as DashboardRankItem['unit'], + })) + .slice(0, DASHBOARD_RANKING.MAX_DISPLAYED_RANK_ITEMS); // 최대 4등까지만 보여줌 +}; + +interface IngredientUsageRankingCardContentProps extends GetIngredientUsageRankingResponseDto { + className?: string; +} + +export const IngredientUsageRankingCardContent = ({ + hasIngredient, + items, + className, +}: IngredientUsageRankingCardContentProps) => { + // dto -> 대시보드의 메뉴>식재료 소진량 랭킹 카드 UI 데이터 형태로 변환 + const ingredientRankItems = useMemo( + () => getDashboardIngredientRankItems({ items }), + [items], + ); + // 등록된 식재료가 없는 경우 카드 내용 + if (!hasIngredient) { + return ; + } + // tHeadLabels를 통해 테이블 각 열의 이름을 지정 + return ( + + + + ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-ranking/MenuSalesRankingCardContent.tsx b/frontend/src/components/menu/dashboard-menu-ranking/MenuSalesRankingCardContent.tsx new file mode 100644 index 000000000..32e02f45d --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-ranking/MenuSalesRankingCardContent.tsx @@ -0,0 +1,50 @@ +// 대시보드>메뉴분석에서 메뉴별 매출 랭킹 카드 +import { useMemo } from 'react'; + +import { DASHBOARD_RANKING } from '@/constants/menu'; +import type { DashboardRankItem } from '@/types//menu'; +import type { GetMenuSalesRankingResponseDto, MenuSales } from '@/types/menu'; + +import { DashboardRankingContent } from './DashboardRankingContent'; + +// dto를 대시보드의 메뉴 매출 랭킹 카드 UI에서 사용하는 데이터 형태로 변환 +interface GetDashboardMenuRankItemsParams { + items: MenuSales[]; +} +const getDashboardMenuRankItems = ({ + items, +}: GetDashboardMenuRankItemsParams): DashboardRankItem[] => { + return items + .map((item, index) => ({ + rank: index + 1, + itemName: item.menuName, + totalAmount: item.totalSalesAmount, + unit: '원' as const, + })) + .slice(0, DASHBOARD_RANKING.MAX_DISPLAYED_RANK_ITEMS); + // 최대 4등까지만 보여줌 +}; + +interface MenuSalesRankingCardContentProps extends GetMenuSalesRankingResponseDto { + className?: string; +} + +export const MenuSalesRankingCardContent = ({ + items, + className, +}: MenuSalesRankingCardContentProps) => { + // dto -> 대시보드의 메뉴>매출 랭킹 카드 UI 데이터 형태로 변환 + const menuRankItems = useMemo( + () => getDashboardMenuRankItems({ items }), + [items], + ); + // tHeadLabels를 통해 테이블 각 열의 이름을 지정 + return ( + + + + ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-ranking/RankItem.tsx b/frontend/src/components/menu/dashboard-menu-ranking/RankItem.tsx new file mode 100644 index 000000000..c05d6ec31 --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-ranking/RankItem.tsx @@ -0,0 +1,44 @@ +import { RankBadge } from '@/components/shared'; +import type { IngredientUnit } from '@/constants/ingredient'; +import { DASHBOARD_RANKING } from '@/constants/menu'; +import { formatNumber } from '@/utils/shared'; + +// 대시보드의 매출 랭킹, 식자재 랭킹 테이블에서 사용하는 각 행 아이템 컴포넌트 +interface RankItemProps { + rank: number; + itemName: string; + totalAmount: number; + unit: '원' | IngredientUnit; +} + +export const RankItem = ({ + rank, + itemName, + totalAmount, + unit, +}: RankItemProps) => { + const isHighlight = rank === DASHBOARD_RANKING.HIGHLIGHT_RANKING; // 1등만 하이라이트 + + return ( + + + + + + + {itemName} + + + + + {formatNumber(totalAmount)} + {unit} + + + ); +}; diff --git a/frontend/src/components/menu/dashboard-menu-ranking/index.ts b/frontend/src/components/menu/dashboard-menu-ranking/index.ts new file mode 100644 index 000000000..b8a4a5120 --- /dev/null +++ b/frontend/src/components/menu/dashboard-menu-ranking/index.ts @@ -0,0 +1,2 @@ +export { IngredientUsageRankingCardContent } from './IngredientUsageRankingCardContent'; +export { MenuSalesRankingCardContent } from './MenuSalesRankingCardContent'; diff --git a/frontend/src/components/menu/index.ts b/frontend/src/components/menu/index.ts index 8753b4017..f01b60ef9 100644 --- a/frontend/src/components/menu/index.ts +++ b/frontend/src/components/menu/index.ts @@ -3,3 +3,9 @@ export { IngredientConsumptionOverview } from './ingredient-consumption'; export { MenuSalesPatternOverview } from './menu-sales-pattern'; export { MenuCombinationOverview } from './menu-combination'; export { CategoryRevenueChartLegend } from './CategoryRevenueChartLegend'; +export { + IngredientUsageRankingCardContent, + MenuSalesRankingCardContent, +} from './dashboard-menu-ranking'; +export { TimeSlotMenuOrderCountCardContent } from './dashboard-menu-order'; +export { PopularMenuCombinationCardContent } from './dashboard-menu-combination'; diff --git a/frontend/src/components/menu/ingredient-consumption/IngredientConsumptionEmptyView.tsx b/frontend/src/components/menu/ingredient-consumption/IngredientConsumptionEmptyView.tsx index 4a8a8f27c..4209934f9 100644 --- a/frontend/src/components/menu/ingredient-consumption/IngredientConsumptionEmptyView.tsx +++ b/frontend/src/components/menu/ingredient-consumption/IngredientConsumptionEmptyView.tsx @@ -12,7 +12,7 @@ export const IngredientConsumptionEmptyView = () => { >
식재료 미등록 이미지 diff --git a/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx b/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx index 2e86b1cb6..4d29124dd 100644 --- a/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx +++ b/frontend/src/components/shared/edit-card-wrapper/EditCardWrapper.tsx @@ -57,7 +57,7 @@ export const EditCardWrapper = ({ GRID_GAP * (sizeY - 1), // 최소 높이 147px, }} className={cn( - 'bg-special-card-bg rounded-400 border-grey-300 relative flex flex-col overflow-hidden border p-3', + 'bg-special-card-bg rounded-400 border-grey-300 relative flex flex-col overflow-hidden border p-2.5', className, )} > diff --git a/frontend/src/components/shared/images/images.stories.tsx b/frontend/src/components/shared/images/images.stories.tsx index 6fef70d28..14efdf0a1 100644 --- a/frontend/src/components/shared/images/images.stories.tsx +++ b/frontend/src/components/shared/images/images.stories.tsx @@ -20,7 +20,7 @@ const images = [ 'congratulation.svg', 'warning.svg', 'empty_dashboard.png', - 'empty_ingridient.svg', + 'empty_ingredient.svg', 'up.svg', 'down.svg', 'graph_down.svg', diff --git a/frontend/src/components/shared/rank-badge/RankBadge.stories.tsx b/frontend/src/components/shared/rank-badge/RankBadge.stories.tsx index 2d8bd246e..2b0bb8d81 100644 --- a/frontend/src/components/shared/rank-badge/RankBadge.stories.tsx +++ b/frontend/src/components/shared/rank-badge/RankBadge.stories.tsx @@ -12,7 +12,7 @@ const meta = { argTypes: { rank: { control: 'number' }, variant: { control: 'select', options: ['default', 'highlight'] }, - size: { control: 'select', options: ['sm', 'md'] }, + size: { control: 'select', options: ['sm', 'md', 'lg'] }, }, } satisfies Meta; diff --git a/frontend/src/components/shared/rank-badge/RankBadgeVariant.ts b/frontend/src/components/shared/rank-badge/RankBadgeVariant.ts index f780557e2..94b35a53d 100644 --- a/frontend/src/components/shared/rank-badge/RankBadgeVariant.ts +++ b/frontend/src/components/shared/rank-badge/RankBadgeVariant.ts @@ -10,7 +10,8 @@ export const rankBadgeVariants = cva( }, size: { sm: 'size-6 body-small-bold', - md: 'size-8 body-medium-bold', + md: 'size-7 body-small-bold', + lg: 'size-8 body-medium-bold', }, }, defaultVariants: { diff --git a/frontend/src/constants/menu/dashboard-menu-combination/index.ts b/frontend/src/constants/menu/dashboard-menu-combination/index.ts new file mode 100644 index 000000000..7f6da46c8 --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-combination/index.ts @@ -0,0 +1 @@ +export { POPULAR_MENU_COMBINATION } from './popularMenuCombination'; diff --git a/frontend/src/constants/menu/dashboard-menu-combination/popularMenuCombination.ts b/frontend/src/constants/menu/dashboard-menu-combination/popularMenuCombination.ts new file mode 100644 index 000000000..1541c4c1c --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-combination/popularMenuCombination.ts @@ -0,0 +1,5 @@ +export const POPULAR_MENU_COMBINATION = { + // 편집 패널에서 보여질 데이터 + EXAMPLE_FIRST_MENU_NAME: '아메리카노(ICE)', + EXAMPLE_SECOND_MENU_NAME: '휘낭시에', +} as const; diff --git a/frontend/src/constants/menu/dashboard-menu-order/index.ts b/frontend/src/constants/menu/dashboard-menu-order/index.ts new file mode 100644 index 000000000..d3ef8a489 --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-order/index.ts @@ -0,0 +1 @@ +export { ORDER_COUNT } from './orderCount'; diff --git a/frontend/src/constants/menu/dashboard-menu-order/orderCount.ts b/frontend/src/constants/menu/dashboard-menu-order/orderCount.ts new file mode 100644 index 000000000..054e79406 --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-order/orderCount.ts @@ -0,0 +1,5 @@ +export const ORDER_COUNT = { + // 편집 패널에서 보여질 데이터 + EXAMPLE_TIME_SLOT_2H: 9, + EXAMPLE_MENU_NAME: '아메리카노(ICE)', +} as const; diff --git a/frontend/src/constants/menu/dashboard-menu-ranking/dashboardRanking.ts b/frontend/src/constants/menu/dashboard-menu-ranking/dashboardRanking.ts new file mode 100644 index 000000000..60c9e984a --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-ranking/dashboardRanking.ts @@ -0,0 +1,4 @@ +export const DASHBOARD_RANKING = { + HIGHLIGHT_RANKING: 1, // 강조할 순위 + MAX_DISPLAYED_RANK_ITEMS: 4, //화면에 보여질 최대 순위 개수 +} as const; diff --git a/frontend/src/constants/menu/dashboard-menu-ranking/index.ts b/frontend/src/constants/menu/dashboard-menu-ranking/index.ts new file mode 100644 index 000000000..96f70b978 --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-ranking/index.ts @@ -0,0 +1,3 @@ +export { MENU_SALES_RANKING } from './menuSalesRanking'; +export { INGREDIENT_USAGE_RANKING } from './ingredientUsageRanking'; +export { DASHBOARD_RANKING } from './dashboardRanking'; diff --git a/frontend/src/constants/menu/dashboard-menu-ranking/ingredientUsageRanking.ts b/frontend/src/constants/menu/dashboard-menu-ranking/ingredientUsageRanking.ts new file mode 100644 index 000000000..b910cba42 --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-ranking/ingredientUsageRanking.ts @@ -0,0 +1,28 @@ +import type { GetIngredientUsageRankingResponseDto } from '@/types/menu'; + +// 편집 패널에서 보여질 데이터 -> 메뉴별 매출 랭킹 카드 +export const INGREDIENT_USAGE_RANKING = { + EXAMPLE_HAS_INGREDIENT: true, + EXAMPLE_INGREDIENT_USAGE_RANKING_ITEMS: [ + { + ingredientName: '우유', + totalQuantity: 3921, + baseUnit: 'ml', + }, + { + ingredientName: '케냐산 원두', + totalQuantity: 1200, + baseUnit: 'g', + }, + { + ingredientName: '생딸기', + totalQuantity: 840, + baseUnit: 'g', + }, + { + ingredientName: '딸기 시럽', + totalQuantity: 10, + baseUnit: 'ml', + }, + ] as const satisfies GetIngredientUsageRankingResponseDto['items'], +} as const; diff --git a/frontend/src/constants/menu/dashboard-menu-ranking/menuSalesRanking.ts b/frontend/src/constants/menu/dashboard-menu-ranking/menuSalesRanking.ts new file mode 100644 index 000000000..6df5fb632 --- /dev/null +++ b/frontend/src/constants/menu/dashboard-menu-ranking/menuSalesRanking.ts @@ -0,0 +1,32 @@ +import type { GetMenuSalesRankingResponseDto } from '@/types/menu'; + +// 편집 패널에서 보여질 데이터 -> 메뉴별 매출 랭킹 카드 +export const MENU_SALES_RANKING = { + EXAMPLE_MENU_SALES_RANKING_ITEMS: [ + { + menuName: '아메리카노(ICE)', + totalSalesAmount: 1500000, + orderCount: 120, + }, + { + menuName: '카페라떼(HOT)', + totalSalesAmount: 1200000, + orderCount: 100, + }, + { + menuName: '카페모카(ICE)', + totalSalesAmount: 900000, + orderCount: 80, + }, + { + menuName: '바닐라 라떼(HOT)', + totalSalesAmount: 600000, + orderCount: 60, + }, + { + menuName: '카라멜 마끼아또(ICE)', + totalSalesAmount: 400000, + orderCount: 90, + }, + ] as const satisfies GetMenuSalesRankingResponseDto['items'], +} as const; diff --git a/frontend/src/constants/menu/index.ts b/frontend/src/constants/menu/index.ts index a71e1a9e7..f8b4eb8b9 100644 --- a/frontend/src/constants/menu/index.ts +++ b/frontend/src/constants/menu/index.ts @@ -1,3 +1,10 @@ -export { MENU_SALES_RANK } from './menuSalesRank'; export { INGREDIENT_CONSUMPTION_RANK } from './ingredientConsumption'; export { MENU_COMBINATION } from './menuCombination'; +export { + MENU_SALES_RANKING, + INGREDIENT_USAGE_RANKING, + DASHBOARD_RANKING, +} from './dashboard-menu-ranking'; +export { ORDER_COUNT } from './dashboard-menu-order'; +export { POPULAR_MENU_COMBINATION } from './dashboard-menu-combination'; +export { MENU_SALES_RANK } from './menuSalesRank'; diff --git a/frontend/src/constants/shared/edit-card-wrapper/editCardWrapper.ts b/frontend/src/constants/shared/edit-card-wrapper/editCardWrapper.ts index c2c2c8459..b220eaa52 100644 --- a/frontend/src/constants/shared/edit-card-wrapper/editCardWrapper.ts +++ b/frontend/src/constants/shared/edit-card-wrapper/editCardWrapper.ts @@ -1,8 +1,8 @@ export const EDIT_CARD_WRAPPER = { CHANGE_SCALE: 0.65, // 65% 축소 - PADDING_SIZE: 12, // 패딩 박스 내 여백 사이즈(12px) + PADDING_SIZE: 10, // 패딩 박스 내 여백 사이즈(12px) HEADER_HEIGHT: 26, // 헤더 높이(위에 있는 버튼들 높이 : 26px) HEADER_MAIN_GAP: 16, // 헤더와 내용 사이 간격(16px) MIN_WIDTH: 220, // 최소 너비 220px - MIN_HEIGHT: 147, // 최소 높이 147px + MIN_HEIGHT: 150, // 최소 높이 150px }; diff --git a/frontend/src/pages/dashboard-page/DashboradPage.tsx b/frontend/src/pages/dashboard-page/DashboardPage.tsx similarity index 100% rename from frontend/src/pages/dashboard-page/DashboradPage.tsx rename to frontend/src/pages/dashboard-page/DashboardPage.tsx diff --git a/frontend/src/pages/dashboard-page/index.ts b/frontend/src/pages/dashboard-page/index.ts index ab90e9b1d..d1e7bf228 100644 --- a/frontend/src/pages/dashboard-page/index.ts +++ b/frontend/src/pages/dashboard-page/index.ts @@ -1 +1 @@ -export { DashboardPage } from './DashboradPage'; +export { DashboardPage } from './DashboardPage'; diff --git a/frontend/src/types/menu/dashboard-menu-ranking/dashboardRankItem.ts b/frontend/src/types/menu/dashboard-menu-ranking/dashboardRankItem.ts new file mode 100644 index 000000000..37fb8c4a1 --- /dev/null +++ b/frontend/src/types/menu/dashboard-menu-ranking/dashboardRankItem.ts @@ -0,0 +1,9 @@ +import type { IngredientUnit } from '@/constants/ingredient'; + +// 대시보드 메뉴 파트에서 메뉴/식자재 랭킹에서 사용되는 데이터 형태 +export interface DashboardRankItem { + rank: number; + itemName: string; + totalAmount: number; + unit: '원' | IngredientUnit; +} diff --git a/frontend/src/types/menu/dashboard-menu-ranking/index.ts b/frontend/src/types/menu/dashboard-menu-ranking/index.ts new file mode 100644 index 000000000..a29df2498 --- /dev/null +++ b/frontend/src/types/menu/dashboard-menu-ranking/index.ts @@ -0,0 +1 @@ +export type { DashboardRankItem } from './dashboardRankItem'; diff --git a/frontend/src/types/menu/dto/getDashboardPopularMenuCombinationDto.ts b/frontend/src/types/menu/dto/getDashboardPopularMenuCombinationDto.ts new file mode 100644 index 000000000..1b4d760df --- /dev/null +++ b/frontend/src/types/menu/dto/getDashboardPopularMenuCombinationDto.ts @@ -0,0 +1,13 @@ +// MNU_05 – 인기 메뉴 조합 카드 +// 대시보드 용 응답 DTO + +// GetDashboardPopularMenuCombinationResponseDto 예시 +// { +// "firstMenuName": "불고기 버거", +// "secondMenuName": "감자튀김" +// } + +export interface GetDashboardPopularMenuCombinationResponseDto { + firstMenuName: string; + secondMenuName: string; +} diff --git a/frontend/src/types/menu/dto/getDashboardTimeSlotMenuOrderCountDto.ts b/frontend/src/types/menu/dto/getDashboardTimeSlotMenuOrderCountDto.ts new file mode 100644 index 000000000..31c38d932 --- /dev/null +++ b/frontend/src/types/menu/dto/getDashboardTimeSlotMenuOrderCountDto.ts @@ -0,0 +1,9 @@ +// MNU_03 – 시간대별 메뉴 주문건수 카드 +// 대시보드 용 응답 DTO +// GetDashboardTimeSlotMenuOrderCountDto 예시 +// {"timeSlot2H":10,"menuName":"불고기 버거"} + +export interface GetDashboardTimeSlotMenuOrderCountResponseDto { + timeSlot2H: number; + menuName: string; +} diff --git a/frontend/src/types/menu/dto/getIngredientUsageRankingDto.ts b/frontend/src/types/menu/dto/getIngredientUsageRankingDto.ts new file mode 100644 index 000000000..b792c8875 --- /dev/null +++ b/frontend/src/types/menu/dto/getIngredientUsageRankingDto.ts @@ -0,0 +1,30 @@ +// IngredientUsage 예시 +// { "ingredientName": "소고기", "totalQuantity": 30000, "baseUnit": "g" }, + +export interface IngredientUsage { + ingredientName: string; + totalQuantity: number; + baseUnit: string; +} +// GetIngredientUsageRankingResponseDto 예시 +// { +// "hasIngredient": true, +// "items": [ +// { +// "ingredientName": "소고기", +// "totalQuantity": 30000, +// "baseUnit": "g" +// }, +// { +// "ingredientName": "치즈", +// "totalQuantity": 5000, +// "baseUnit": "g" +// } +// ] +// } + +// 백엔드에서 보내주는 식자재별 소진량 DTO -> 식자재별 소진량 랭킹에 사용 +export interface GetIngredientUsageRankingResponseDto { + hasIngredient: boolean; // 식자재 등록 여부 + items: IngredientUsage[]; +} diff --git a/frontend/src/types/menu/dto/getMenuSalesRankingDto.ts b/frontend/src/types/menu/dto/getMenuSalesRankingDto.ts new file mode 100644 index 000000000..4f1fb12be --- /dev/null +++ b/frontend/src/types/menu/dto/getMenuSalesRankingDto.ts @@ -0,0 +1,17 @@ +// MenuSales 예시 +// { +// "menuName": "불고기 버거", +// "totalSalesAmount": 1500000, +// "orderCount": 120 +// }, + +export interface MenuSales { + menuName: string; + totalSalesAmount: number; + orderCount: number; +} + +// 백엔드에서 보내주는 메뉴별 매출, 주문건 수 DTO -> 매출별 랭킹에 사용 +export interface GetMenuSalesRankingResponseDto { + items: MenuSales[]; +} diff --git a/frontend/src/types/menu/dto/getPopularMenuCombinationDto.ts b/frontend/src/types/menu/dto/getPopularMenuCombinationDto.ts new file mode 100644 index 000000000..fe305066b --- /dev/null +++ b/frontend/src/types/menu/dto/getPopularMenuCombinationDto.ts @@ -0,0 +1,20 @@ +export interface PairedMenu { + menuName: string; + count: number; +} +// PoplularMenuCombination 예시 +// { +// "baseMenuName": "불고기 버거", +// "pairedMenus": [ +// { "menuName": "감자튀김", "count": 80 }, +// { "menuName": "콜라", "count": 70 } +// ] +// } +export interface PoplularMenuCombination { + baseMenuName: string; + pairedMenus: PairedMenu[]; +} + +export interface GetPopularMenuCombinationResponseDto { + items: PoplularMenuCombination[]; +} diff --git a/frontend/src/types/menu/dto/index.ts b/frontend/src/types/menu/dto/index.ts new file mode 100644 index 000000000..b67587a98 --- /dev/null +++ b/frontend/src/types/menu/dto/index.ts @@ -0,0 +1,14 @@ +export type { + GetMenuSalesRankingResponseDto, + MenuSales, +} from './getMenuSalesRankingDto'; +export type { + GetIngredientUsageRankingResponseDto, + IngredientUsage, +} from './getIngredientUsageRankingDto'; +export type { + GetPopularMenuCombinationResponseDto, + PoplularMenuCombination, +} from './getPopularMenuCombinationDto'; +export type { GetDashboardTimeSlotMenuOrderCountResponseDto } from './getDashboardTimeSlotMenuOrderCountDto'; +export type { GetDashboardPopularMenuCombinationResponseDto } from './getDashboardPopularMenuCombinationDto'; diff --git a/frontend/src/types/menu/index.ts b/frontend/src/types/menu/index.ts index cb2102bcc..2caf694bb 100644 --- a/frontend/src/types/menu/index.ts +++ b/frontend/src/types/menu/index.ts @@ -2,3 +2,12 @@ export type { IngredientConsumptionRank } from './ingredientConsumptionRank'; export type { MenuCombinationRank } from './menuCombinationRank'; export type { MenuSalesRank } from './menuSalesRank'; export type { CategoriesRevenue } from './categoriesRevenue'; +export type { DashboardRankItem } from './dashboard-menu-ranking'; +export type { + GetDashboardPopularMenuCombinationResponseDto, + GetDashboardTimeSlotMenuOrderCountResponseDto, + GetMenuSalesRankingResponseDto, + GetIngredientUsageRankingResponseDto, + MenuSales, + IngredientUsage, +} from './dto'; diff --git a/frontend/src/utils/shared/getNextHour.ts b/frontend/src/utils/shared/getNextHour.ts new file mode 100644 index 000000000..e154f33f5 --- /dev/null +++ b/frontend/src/utils/shared/getNextHour.ts @@ -0,0 +1,5 @@ +// 시간이 들어오면 다음 시간대 (예: 9 -> 10, 23 -> 24, 24 -> 1)를 반환하는 함수 +// 24가 들어오면 1 반환해야함 +export const getNextHour = (hour: number): number => { + return hour === 24 ? 1 : hour + 1; +}; diff --git a/frontend/src/utils/shared/index.ts b/frontend/src/utils/shared/index.ts index 87edaff21..df454ca42 100644 --- a/frontend/src/utils/shared/index.ts +++ b/frontend/src/utils/shared/index.ts @@ -38,6 +38,7 @@ export { } from './doughnut-chart'; export { createPeriodTypeProvider } from './period-select'; +export { getNextHour } from './getNextHour'; export { assertNever } from './assertNever'; export { getCoordinate } from './getCoordinate'; export {