Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c23463b
chore: 임시커밋
mskwon02 Feb 13, 2026
8b25224
feat: dashboard metric card 상수 변경 및 관련 타입 제너릭 정의
lee0jae330 Feb 13, 2026
be20d12
chore: cherry-pick 오류 해결(타입가드)
mskwon02 Feb 13, 2026
62ec726
feat: 식자재 소진량 매출 랭킹 카드
mskwon02 Feb 15, 2026
c448d48
feat: 메뉴 주문 건수 카드
mskwon02 Feb 15, 2026
1c7d312
feat: 주문건수 가장 많은 메뉴 카드
mskwon02 Feb 15, 2026
a376748
feat: 메뉴별 매출 랭킹 카드
mskwon02 Feb 15, 2026
20ee6d2
chore: 랭킹 배지에 size 7인 smmd이름의 사이즈 속성 추가
mskwon02 Feb 15, 2026
df7f858
feat: nullable 타입 추가
mskwon02 Feb 15, 2026
ef370cb
feat: 대시보드의 메뉴파트에서 메뉴랭킹, 식자재 랭킹 UI에서 공통으로 사용되는 랭킹 아이템 타입 정의
mskwon02 Feb 15, 2026
98a4895
feat: 메뉴별 매출 랭킹 DTO 타입 정의
mskwon02 Feb 15, 2026
4809bbf
feat: 대시보드의 메뉴파트에서 메뉴랭킹, 식자재 랭킹 UI에서 공통으로 사용되는 랭킹 아이템 컴포넌트 구현
mskwon02 Feb 15, 2026
0bd7d6a
feat: 대시보드 메뉴의 메뉴별 매출 랭킹 카드 구현
mskwon02 Feb 15, 2026
9d8b24b
feat: 백엔드에서 보내주는 식자재별 소진량 dto 정의
mskwon02 Feb 15, 2026
9d591e5
chore: 대시보드 랭킹 테이블에서 사용하는 행 아이템 컴포넌트 주석 수정
mskwon02 Feb 15, 2026
33ab632
chore: 메뉴 매출 랭킹에서 사용하던 랭킹 테이블 컴포넌트를 식재료 소진량 랭킹에서도 사용할 수 있도록 수정
mskwon02 Feb 15, 2026
94d8af2
feat: 대시보드 메뉴분석에서 식재료 사용량 랭킹 카드 구현
mskwon02 Feb 15, 2026
065551a
chore: nullable한 특징에 맞게 유틸 함수의 파라미터 타입도 변형
mskwon02 Feb 15, 2026
5cec2f1
chore: 식자재별 소진량 랭킹 카드 DTO 주석 수정
mskwon02 Feb 15, 2026
9650a63
feat: 인기 메뉴 조합 랭킹에 사용되는 dto 정의
mskwon02 Feb 15, 2026
c952c16
feat: 인기 메뉴 조합 카드 콘텐츠 컴포넌트 구현
mskwon02 Feb 15, 2026
4145ea3
feat: 시간대별 메뉴 주문 건수, 인기 메뉴 조합 카드 새로 바뀐 dto 정의
mskwon02 Feb 15, 2026
f28e5d0
feat: 다음 시간 구하는 유틸 함수 정의
mskwon02 Feb 15, 2026
bc7f262
feat: 대시보드>메뉴분석>현재 주문건수 가장 많은 메뉴 카드
mskwon02 Feb 15, 2026
e321250
chore: 대시보드 랭킹 컴포넌트 css 일부 수정
mskwon02 Feb 15, 2026
88118f3
chore: 인기메뉴 조합 랭킹 카드를 새로 바뀐 대시보드용 dto(SSE용)를 사용하는 방식으로 변경ㅇ
mskwon02 Feb 15, 2026
5d34697
chore: 식재료 소진량 DTO 구조 변경에 맞추어 카드 수정
mskwon02 Feb 15, 2026
3e322ff
chore: 대시보드 카드 랭킹 테이블에서 양 옆 여백 생기는 문제 해결
mskwon02 Feb 15, 2026
709410b
chore: 폴더 구조 및 파일 위치 변경. 상수 분리
mskwon02 Feb 15, 2026
2e94468
chore: editCardWrapper 수정
mskwon02 Feb 15, 2026
2b9a00d
chore: 대시보드 메뉴분석 상수들 as const로 export
mskwon02 Feb 15, 2026
7627ae7
Merge remote-tracking branch 'origin/develop' into feature/#262-fe-da…
mskwon02 Feb 15, 2026
6335aef
chore: 편집모드 패널에 대시보드 메뉴분석 컨텐츠 사용
mskwon02 Feb 15, 2026
a699e1e
chore: null값이 props로 들어올 수 없기 때문에 불필요한 null 체크 제거
mskwon02 Feb 15, 2026
6bf2446
chore: PR리뷰반영-오타 수정
mskwon02 Feb 15, 2026
275aaf7
chore: empty_ingredient.svg 오타 수정
mskwon02 Feb 18, 2026
de6a045
chore: className props 전달 누락 해결
mskwon02 Feb 18, 2026
395fa5d
chore: 불필요한 파일 삭제
mskwon02 Feb 18, 2026
c4204d9
chore: dto import 경로 단축
mskwon02 Feb 18, 2026
132ff34
chore: 랭킹 데이터 정렬은 백엔드에서만. 프론트에서는 그냥 보여주기만
mskwon02 Feb 18, 2026
1b9cc54
chore: 사용하지 않는 유틸함수 제거
mskwon02 Feb 18, 2026
cd5a092
chore: 랭킹 배지 사이즈명 md, lg 수정
mskwon02 Feb 18, 2026
57f3dae
chore: 메뉴분석에서 랭킹 지표들 높이 길게 나오는 오류 해결
mskwon02 Feb 18, 2026
3f98866
chore: 편집 카드에서 보여지는 데이터 일부 수정(감자 -> 딸기시럽)
mskwon02 Feb 18, 2026
238a705
fix: resolve merge conflict
lee0jae330 Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from '@/constants/dashboard';
import { useEditCard } from '@/hooks/dashboard';

import { EditCardContent } from './EditCardContent';

interface CardEditViewCardProps {
cardCode: MetricCardCode;
}
Expand All @@ -29,7 +31,7 @@ export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => {
return null; // 카드 정보가 없는 경우 렌더링하지 않음
}

const { code, label, type, period, sizeX, sizeY } = card;
const { period, sizeX } = card;

return (
<li style={{ gridColumn: `span ${sizeX}` }}>
Expand All @@ -38,17 +40,10 @@ export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => {
period={period}
className="min-w-full"
sizeX={sizeX}
sizeY={sizeY}
onClickAddButton={handleAddCard}
onClickDeleteButton={handleDeleteCard}
>
{label}
<br />
{code}
<br />
{type}
<br />
{sizeX} x {sizeY}
<EditCardContent cardCode={cardCode} />
</EditCardWrapper>
</li>
);
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2: 오우 제 형식이랑 맞추어주셨군요 감사합니다 !

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';

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 (
<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,23 @@
import { ORDER_COUNT } from '@/constants/menu';
import type { GetDashboardTimeSlotMenuOrderCountResponseDto } from '@/types/menu/dto';
import type { Nullable } from '@/utils/shared';

import { TimeSlotMenuOrderCountContent } from './TimeSlotMenuOrderCountContent';

const { EXAMPLE_TIME_SLOT_2H, EXAMPLE_MENU_NAME } = ORDER_COUNT;
// 현재 주문건수가 가장 많은 메뉴 카드
interface TimeSlotMenuOrderCountCardContentProps extends Nullable<GetDashboardTimeSlotMenuOrderCountResponseDto> {
className?: string;
}

export const TimeSlotMenuOrderCountCardContent = ({
timeSlot2H = EXAMPLE_TIME_SLOT_2H,
menuName = EXAMPLE_MENU_NAME,
}: TimeSlotMenuOrderCountCardContentProps) => {
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>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/menu/dashboard-menu-order/index.ts
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 MenuSalesRankingCardContentProps {
className?: string;
children?: ReactNode;
tHeadLabels?: string[];
}

// 메뉴분석에서 '메뉴 매출 랭킹', '식자재 소진량 랭킹' 카드에서 공통으로 사용하는 순위 컴포넌트
export const DashboardRankingContent = ({
className,
children,
tHeadLabels = ['순위', '메뉴명', '매출액'], // 테이블 각 열의 이름, sr-only 클래스로 화면에서는 보이지 않지만 스크린 리더로 읽을 수 있도록 함
}: MenuSalesRankingCardContentProps) => {
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`}
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,70 @@
// 대시보드>메뉴분석에서 식재료별 소진량 랭킹 카드
import { useMemo } from 'react';

import { DASHBOARD_RANKING } from '@/constants/menu';
import type { DashboardRankItem } from '@/types/menu';
import type {
GetIngredientUsageRankingResponseDto,
IngredientUsage,
} from '@/types/menu/dto';

import { DashboardRankingContent } from './DashboardRankingContent';
import { IngredientUnregisteredContent } from './IngredientUnregisteredContent';

// dto를 대시보드의 식재료 소진량 랭킹 카드 UI에서 사용하는 데이터 형태로 변환
interface GetDashboardIngredientRankItemsParams {
items: IngredientUsage[];
}
const getDashboardIngredientRankItems = ({
items,
}: GetDashboardIngredientRankItemsParams): DashboardRankItem[] => {
if (!items) {
return [];
}
// 사용량 기준으로 내림차순 정렬
// 단 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;
});
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3: 여기도 dto 누락되었습니다 !

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dto 가 누락되었다는게 className이 누락되었다는 말씀이신가요??

Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
);
};
Loading
Loading