-
Notifications
You must be signed in to change notification settings - Fork 0
[FE] 대시보드 편집창 - 메뉴 분석 지표카드 추가 #277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 35 commits
c23463b
8b25224
be20d12
62ec726
c448d48
1c7d312
a376748
20ee6d2
df7f858
ef370cb
98a4895
4809bbf
0bd7d6a
9d8b24b
9d591e5
33ab632
94d8af2
065551a
5cec2f1
9650a63
c952c16
4145ea3
f28e5d0
bc7f262
e321250
88118f3
5d34697
3e322ff
709410b
2e94468
2b9a00d
7627ae7
6335aef
a699e1e
6bf2446
275aaf7
de6a045
395fa5d
c4204d9
132ff34
1b9cc54
cd5a092
57f3dae
3f98866
238a705
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { | ||
| IngredientUsageRankingCardContent, | ||
| MenuSalesRankingCardContent, | ||
| PopularMenuCombinationCardContent, | ||
| TimeSlotMenuOrderCountCardContent, | ||
| } from '@/components/menu'; | ||
| import type { MetricCardCode } from '@/constants/dashboard'; | ||
| import { | ||
| INGREDIENT_USAGE_RANKING, | ||
| MENU_SALES_RANKING, | ||
| ORDER_COUNT, | ||
| POPULAR_MENU_COMBINATION, | ||
| } from '@/constants/menu'; | ||
|
|
||
| interface EditCardContentProps { | ||
| cardCode: MetricCardCode; | ||
| } | ||
|
|
||
| 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 } = ORDER_COUNT; | ||
| const { EXAMPLE_FIRST_MENU_NAME, EXAMPLE_SECOND_MENU_NAME } = | ||
| POPULAR_MENU_COMBINATION; | ||
| export const EditCardContent = ({ cardCode }: EditCardContentProps) => { | ||
| switch (cardCode) { | ||
| case 'MNU_01_01': | ||
| case 'MNU_01_04': | ||
| case 'MNU_01_05': | ||
| return ( | ||
| <MenuSalesRankingCardContent items={EXAMPLE_MENU_SALES_RANKING_ITEMS} /> | ||
| ); | ||
| case 'MNU_03_01': | ||
| return ( | ||
| <TimeSlotMenuOrderCountCardContent | ||
| timeSlot2H={EXAMPLE_TIME_SLOT_2H} | ||
| menuName={EXAMPLE_MENU_NAME} | ||
| /> | ||
| ); | ||
| case 'MNU_04_01': | ||
| return ( | ||
| <IngredientUsageRankingCardContent | ||
| hasIngredient={EXAMPLE_HAS_INGREDIENT} | ||
| items={EXAMPLE_INGREDIENT_USAGE_RANKING_ITEMS} | ||
| /> | ||
| ); | ||
| case 'MNU_05_04': | ||
| return ( | ||
| <PopularMenuCombinationCardContent | ||
| firstMenuName={EXAMPLE_FIRST_MENU_NAME} | ||
| secondMenuName={EXAMPLE_SECOND_MENU_NAME} | ||
| /> | ||
| ); | ||
| default: | ||
| return null; | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { POPULAR_MENU_COMBINATION } from '@/constants/menu'; | ||
| import type { GetDashboardPopularMenuCombinationResponseDto } from '@/types/menu/dto'; | ||
mskwon02 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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, | ||
| }: PopularMenuCombinationCardContentProps) => { | ||
| return ( | ||
mskwon02 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <PopularMenuCombinationContent | ||
| baseMenuName={firstMenuName} | ||
| pairedMenu={secondMenuName} | ||
| /> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <p | ||
| className={cn( | ||
| 'title-large-semibold text-grey-900 flex w-75 flex-col', | ||
| className, | ||
| )} | ||
| > | ||
| <span>최고 인기 조합은</span> | ||
| <span className="title-large-bold text-brand-main min-w-0 truncate"> | ||
| {baseMenuName} | ||
| </span> | ||
| <span className="flex w-full items-center gap-1"> | ||
| <span className="title-large-bold text-brand-main min-w-0 truncate"> | ||
| &{pairedMenu} | ||
| </span> | ||
| <span className="whitespace-nowrap">입니다</span> | ||
| </span> | ||
| </p> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { PopularMenuCombinationCardContent } from './PopularMenuCombinationCardContent'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { ORDER_COUNT } from '@/constants/menu'; | ||
| import type { GetDashboardTimeSlotMenuOrderCountResponseDto } from '@/types/menu/dto'; | ||
mskwon02 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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, | ||
| }: TimeSlotMenuOrderCountCardContentProps) => { | ||
mskwon02 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return ( | ||
| <TimeSlotMenuOrderCountContent | ||
| timeSlot2H={timeSlot2H} | ||
| menuName={menuName} | ||
| /> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <p | ||
| className={cn( | ||
| 'title-large-semibold text-grey-900 flex w-75 flex-col', | ||
| className, | ||
| )} | ||
| > | ||
| <span className="title-large-bold text-brand-main flex w-70"> | ||
| <span className="min-w-0 truncate">{menuName}</span> | ||
| <span className="shrink-0">{`는 ${timeSlot2H}~${getNextHour(timeSlot2H)}시`}</span> | ||
| </span> | ||
| <span>주문이 가장 많아요</span> | ||
| </p> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { TimeSlotMenuOrderCountCardContent } from './TimeSlotMenuOrderCountCardContent'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| 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 ( | ||
| <table | ||
| className={cn( | ||
| 'w-75 table-fixed border-separate border-spacing-y-2', | ||
| className, | ||
| )} | ||
| > | ||
| <colgroup> | ||
| <col className="w-10" /> | ||
| <col className="w-32" /> | ||
| <col className="w-auto" /> | ||
| </colgroup> | ||
| <thead> | ||
| {/* 테이블 각 열의 이름 지정*/} | ||
| <tr className="sr-only"> | ||
| {tHeadLabels.map((label) => ( | ||
| <th key={label}>{label}</th> | ||
| ))} | ||
| </tr> | ||
| </thead> | ||
| {children} | ||
| </table> | ||
| ); | ||
| }; | ||
|
|
||
| interface DashboardRankingContentTableBodyProps { | ||
| rankItems: DashboardRankItem[]; | ||
| } | ||
| const DashboardRankingContentTableBody = ({ | ||
| rankItems, | ||
| }: DashboardRankingContentTableBodyProps) => { | ||
| return ( | ||
| <tbody> | ||
| {rankItems.map(({ rank, itemName, totalAmount, unit }) => ( | ||
| <RankItem | ||
| key={rank} | ||
| rank={rank} | ||
| itemName={itemName} | ||
| totalAmount={totalAmount} | ||
| unit={unit} | ||
| /> | ||
| ))} | ||
| </tbody> | ||
| ); | ||
| }; | ||
|
|
||
| DashboardRankingContent.TableBody = DashboardRankingContentTableBody; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className={cn('flex w-75 flex-col items-center', className)}> | ||
| <div className="flex flex-col items-center gap-1"> | ||
| <img | ||
| src={`${CDN_BASE_URL}/assets/images/empty_ingridient.svg`} | ||
mskwon02 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| alt="식재료 미등록 이미지" | ||
| className="size-18" | ||
| /> | ||
| <span className="body-large-bold">식재료 미등록</span> | ||
| <span className="body-small-medium text-grey-700 text-center"> | ||
| 자동으로 식재료 파악하고, | ||
| <br /> 간편하게 매장을 운영하세요. | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| // 대시보드>메뉴분석에서 식재료별 소진량 랭킹 카드 | ||
| import { useMemo } from 'react'; | ||
|
|
||
| import { DASHBOARD_RANKING } from '@/constants/menu'; | ||
| import type { DashboardRankItem } from '@/types/menu'; | ||
| import type { | ||
| GetIngredientUsageRankingResponseDto, | ||
| IngredientUsage, | ||
| } from '@/types/menu/dto'; | ||
mskwon02 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| import { DashboardRankingContent } from './DashboardRankingContent'; | ||
| import { IngredientUnregisteredContent } from './IngredientUnregisteredContent'; | ||
|
|
||
| // dto를 대시보드의 식재료 소진량 랭킹 카드 UI에서 사용하는 데이터 형태로 변환 | ||
| interface GetDashboardIngredientRankItemsParams { | ||
| items: IngredientUsage[]; | ||
| } | ||
| const getDashboardIngredientRankItems = ({ | ||
| items, | ||
| }: GetDashboardIngredientRankItemsParams): DashboardRankItem[] => { | ||
| // 사용량 기준으로 내림차순 정렬 | ||
| // 단 kg , L 단위는 g, ml로 변환하여 비교해야 함 | ||
| const sortedItems = [...items].sort((a, b) => { | ||
| const aQuantity = | ||
| a.baseUnit === 'kg' || a.baseUnit === 'L' | ||
| ? a.totalQuantity * 1000 | ||
| : a.totalQuantity; | ||
| const bQuantity = | ||
| b.baseUnit === 'kg' || b.baseUnit === 'L' | ||
| ? b.totalQuantity * 1000 | ||
| : b.totalQuantity; | ||
| return bQuantity - aQuantity; | ||
| }); | ||
mskwon02 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return sortedItems | ||
| .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, | ||
| }: IngredientUsageRankingCardContentProps) => { | ||
|
Comment on lines
31
to
39
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3: 여기도 dto 누락되었습니다 !
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dto 가 누락되었다는게 className이 누락되었다는 말씀이신가요??
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 className입니다 ㅠㅠ |
||
| // dto -> 대시보드의 메뉴>식재료 소진량 랭킹 카드 UI 데이터 형태로 변환 | ||
| const ingredientRankItems = useMemo( | ||
| () => getDashboardIngredientRankItems({ items }), | ||
| [items], | ||
| ); | ||
| // 등록된 식재료가 없는 경우 카드 내용 | ||
| if (!hasIngredient) { | ||
| return <IngredientUnregisteredContent />; | ||
| } | ||
| // tHeadLabels를 통해 테이블 각 열의 이름을 지정 | ||
| return ( | ||
| <DashboardRankingContent tHeadLabels={['순위', '식재료명', '소진량']}> | ||
| <DashboardRankingContent.TableBody rankItems={ingredientRankItems} /> | ||
| </DashboardRankingContent> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p2: 오우 제 형식이랑 맞추어주셨군요 감사합니다 !