Skip to content

Commit 1536ecd

Browse files
authored
Merge pull request #84 from Duri-Salon/feat(salon)/statistics-page
[feat] 미용사 통계페이지 반려견 부분 구현
2 parents ad7ba1c + 4943cb7 commit 1536ecd

File tree

6 files changed

+229
-32
lines changed

6 files changed

+229
-32
lines changed

apps/salon/src/components/home/DailyScheduleItem.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ const DailyScheduleItem = ({
3434

3535
</WidthFitFlex>
3636

37-
<Flex justify="space-between">
37+
<NameWrapper justify="space-between">
3838
<Text typo="Body2">{petName}</Text>
3939
<Text typo="Body4" colorCode={theme.palette.Gray500}>{petInfoStr}</Text>
4040
<Text typo="Body4">{groomerName}</Text>
41-
</Flex>
41+
</NameWrapper>
4242
</ItemWrapper>
4343
)
4444
}
@@ -55,4 +55,8 @@ const ItemWrapper = styled(Flex)`
5555
flex-shrink: 0;
5656
`
5757

58+
const NameWrapper = styled(Flex)`
59+
flex-wrap: wrap;
60+
`
61+
5862
export default DailyScheduleItem;

apps/salon/src/components/income/PetStatistic.tsx

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import { useState } from 'react';
2+
3+
import { Flex, Text, theme, WidthFitFlex } from '@duri-fe/ui';
4+
import styled from '@emotion/styled';
5+
import { ResponsivePie } from '@nivo/pie';
6+
import { PET_STATISTIC_MENT } from '@salon/constants/statistic';
7+
8+
import { TabBarItem } from '../quotation/TabBarItem';
9+
110
interface PetStatisticProps {
211
agePetStatistic?: {
312
standard: string;
@@ -18,11 +27,144 @@ interface PetStatisticProps {
1827
}[];
1928
}
2029

30+
const colorScheme = [
31+
`${theme.palette.Normal200}`,
32+
`${theme.palette.Normal500}`,
33+
`${theme.palette.Normal600}`,
34+
`${theme.palette.Normal800}`,
35+
`${theme.palette.Normal900}`,
36+
`${theme.palette.Gray300}`,
37+
];
38+
2139
export const PetStatistic = ({
2240
agePetStatistic,
2341
characterPetStatistic,
2442
diseasePetStatistic,
2543
}: PetStatisticProps) => {
26-
console.log(agePetStatistic, diseasePetStatistic, characterPetStatistic);
27-
return <div>PetStatistic</div>;
44+
const [selectedTab, setSelectedTab] = useState<'나이' | '질환' | '성격'>(
45+
'나이',
46+
);
47+
// 각 탭에 해당하는 데이터 (Props로 전달된 데이터를 사용)
48+
const dataMap = {
49+
나이: agePetStatistic,
50+
질환: diseasePetStatistic,
51+
성격: characterPetStatistic,
52+
};
53+
54+
// 선택된 탭에 맞는 데이터가 없으면 빈 배열 반환
55+
const chartData = (dataMap[selectedTab] ?? []).map((item, index) => ({
56+
id: item.standard,
57+
label: item.standard,
58+
value: item.count,
59+
ratio: item.ratio,
60+
color: colorScheme[index % colorScheme.length], // 순환하여 색상 적용
61+
}));
62+
63+
const handleToggleTab = (currTab: '나이' | '질환' | '성격') => {
64+
setSelectedTab(currTab);
65+
};
66+
67+
return (
68+
<Flex direction="column">
69+
<Flex direction="column" align="flex-start" gap={8} padding="0 20px">
70+
<Text typo="Title2">반려견 통계</Text>
71+
<Text typo="Caption1">우리 매장에는 이런 아이들이 자주 왔어요.</Text>
72+
</Flex>
73+
<Flex
74+
height={37}
75+
justify="flex-start"
76+
backgroundColor={theme.palette.White}
77+
margin="24px 0 0"
78+
>
79+
<TabBarItem
80+
label="나이"
81+
selected={selectedTab === '나이'}
82+
typo={selectedTab === '나이' ? 'Title3' : 'Label3'}
83+
onClick={() => handleToggleTab('나이')}
84+
/>
85+
<TabBarItem
86+
label="질환"
87+
selected={selectedTab === '질환'}
88+
typo={selectedTab === '질환' ? 'Title3' : 'Label3'}
89+
onClick={() => handleToggleTab('질환')}
90+
/>
91+
<TabBarItem
92+
label="성격"
93+
selected={selectedTab === '성격'}
94+
typo={selectedTab === '성격' ? 'Title3' : 'Label3'}
95+
onClick={() => handleToggleTab('성격')}
96+
/>
97+
</Flex>
98+
99+
{/* 통계 상단 타이틀 */}
100+
<Flex justify="flex-start" padding="0 20px" margin="24px 0 0">
101+
{dataMap[selectedTab] && dataMap[selectedTab]?.length > 0 ? (
102+
<RelativeText typo="Title3">
103+
{PET_STATISTIC_MENT[selectedTab]?.[
104+
dataMap[selectedTab][0].standard
105+
] ?? '다양한 아이들이 자주 와요!'}
106+
<TextHighlight width={selectedTab === '성격' ? '50px' : '62px'} />
107+
</RelativeText>
108+
) : (
109+
<Text typo="Title3">아직 방문한 고객이 없어요😞</Text>
110+
)}
111+
</Flex>
112+
113+
<Flex padding="0 20px" margin="28px 0 0" gap={48}>
114+
<Flex width={150} height={150}>
115+
<ResponsivePie
116+
data={chartData}
117+
innerRadius={0.9} // 파이 차트 안쪽 반지름 크기
118+
padAngle={1} // 조각 간의 간격
119+
cornerRadius={1} // 조각 모서리의 둥글기
120+
colors={({ data }) => data.color}
121+
enableArcLinkLabels={false} // 링크 레이블 표시 여부
122+
enableArcLabels={true} // 차트 안에 레이블 표시 여부
123+
arcLinkLabelsSkipAngle={10} // 링크 레이블이 보이려면 이 각도 이상일 때만 표시
124+
arcLabelsRadiusOffset={0.5} // 레이블이 원에서 떨어지는 거리
125+
arcLinkLabelsThickness={2} // 링크 레이블 두께
126+
arcLinkLabelsColor={{ from: 'color', modifiers: [['darker', 0.6]] }} // 링크 레이블 색상
127+
arcLabelsTextColor="transparent" // 레이블 텍스트 색상
128+
tooltip={() => null} // 툴팁 비활성화
129+
/>
130+
</Flex>
131+
<WidthFitFlex direction="column" gap={6}>
132+
{chartData &&
133+
chartData.map((item) => (
134+
<BarColorFlex
135+
justify="space-between"
136+
color={item.color}
137+
key={item.label}
138+
width={130}
139+
height={22}
140+
padding="10px 10px 10px 16px"
141+
>
142+
<Text typo="Label3">{item.label}</Text>
143+
<Text typo="Label2" colorCode={theme.palette.Gray300}>
144+
{item.ratio}%
145+
</Text>
146+
</BarColorFlex>
147+
))}
148+
</WidthFitFlex>
149+
</Flex>
150+
</Flex>
151+
);
28152
};
153+
154+
const BarColorFlex = styled(Flex)<{ color: string }>`
155+
border-left: 3px solid ${({ color }) => color};
156+
`;
157+
158+
const RelativeText = styled(Text)`
159+
position: relative;
160+
`;
161+
162+
const TextHighlight = styled.div<{ width: string }>`
163+
position: absolute;
164+
width: ${({ width }) => (width ? width : '62px')};
165+
height: 9px;
166+
background-color: ${theme.palette.Normal500};
167+
opacity: 0.5;
168+
left: -3px;
169+
top: 8px;
170+
`;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export const DEFAULT_AGE_LABEL = ['~3세', '3세~7세', '7세~'];
2+
3+
type PetStatisticMentType = {
4+
[key: string]: Record<string, string>;
5+
};
6+
7+
export const PET_STATISTIC_MENT: PetStatisticMentType = {
8+
나이: {
9+
'~3세': '3세 미만인 아이들이 가장 자주 와요!',
10+
11+
'3세~7세': '3세~7세 아이들이 가장 자주 와요!',
12+
13+
'7세~': '7세 이상 아이들이 가장 자주 와요!',
14+
},
15+
질환: {
16+
귀질환: '귀 질환이 있는 아이들이 가장 자주 와요!',
17+
18+
관절질환: '관절질환이 있는 아이들이 가장 자주 와요!',
19+
20+
기저질환: '기저질환이 있는 아이들이 가장 자주 와요!',
21+
22+
피부질환: '피부질환이 있는 아이들이 가장 자주 와요!',
23+
24+
해당없음: '다양한 아이들이 자주 와요!',
25+
},
26+
성격: {
27+
입질: '입질이 있는 아이들이 가장 자주 와요!',
28+
29+
낯가리는: '낯가리는 아이들이 가장 자주 와요!',
30+
31+
예민한: '예민한 아이들이 가장 자주 와요!',
32+
33+
친화적: '친화적인 아이들이 가장 자주 와요!',
34+
35+
얌전한: '얌전한 아이들이 가장 자주 와요!',
36+
37+
겁많은: '겁많은 아이들이 가장 자주 와요!',
38+
},
39+
};

apps/salon/src/pages/Income/Income.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useNavigate } from 'react-router-dom';
22

3-
import { Header, MobileLayout, SalonNavbar } from '@duri-fe/ui';
3+
import { Flex, Header, MobileLayout, SalonNavbar } from '@duri-fe/ui';
44
import {
55
useGetAgeStatistic,
66
useGetCharacterStatistic,
@@ -19,7 +19,9 @@ const IncomePage = () => {
1919
};
2020

2121
const { data: monthIncomeData } = useGetThisMonthIncome({});
22-
const { data: selectedIncomeData } = useGetSelectedMonthIncome({ month: "2024-12" });
22+
const { data: selectedIncomeData } = useGetSelectedMonthIncome({
23+
month: '2024-12',
24+
});
2325
const { data: agePetStatistic } = useGetAgeStatistic({});
2426
const { data: diseasePetStatistic } = useGetDiseaseStatistic({});
2527
const { data: characterPetStatistic } = useGetCharacterStatistic({});
@@ -32,25 +34,27 @@ const IncomePage = () => {
3234
backIcon
3335
onClickBack={handleClickBack}
3436
/>
35-
{monthIncomeData && (
36-
<MonthIncomeStatistic
37-
beforeRatio={monthIncomeData.beforeRatio}
38-
incomeMonthList={monthIncomeData.incomeMonthList}
39-
/>
40-
)}
37+
<Flex direction="column">
38+
{monthIncomeData && (
39+
<MonthIncomeStatistic
40+
beforeRatio={monthIncomeData.beforeRatio}
41+
incomeMonthList={monthIncomeData.incomeMonthList}
42+
/>
43+
)}
4144

42-
{selectedIncomeData && (
43-
<RecentIncomeStatistic
44-
incomeMonthList={selectedIncomeData.incomeMonthList}
45-
beforeRatio={selectedIncomeData.beforeRatio}
46-
nowRatio={selectedIncomeData.nowRatio}
45+
{selectedIncomeData && (
46+
<RecentIncomeStatistic
47+
incomeMonthList={selectedIncomeData.incomeMonthList}
48+
beforeRatio={selectedIncomeData.beforeRatio}
49+
nowRatio={selectedIncomeData.nowRatio}
50+
/>
51+
)}
52+
<PetStatistic
53+
agePetStatistic={agePetStatistic?.ageList}
54+
diseasePetStatistic={diseasePetStatistic?.diseaseList}
55+
characterPetStatistic={characterPetStatistic?.characterList}
4756
/>
48-
)}
49-
{<PetStatistic
50-
agePetStatistic={agePetStatistic?.ageList}
51-
diseasePetStatistic={diseasePetStatistic?.diseaseList}
52-
characterPetStatistic={characterPetStatistic?.characterList}
53-
/>}
57+
</Flex>
5458
<SalonNavbar />
5559
</MobileLayout>
5660
);

packages/utils/src/apis/duri/hooks/my.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { useMutation, useQuery } from '@tanstack/react-query';
1+
import {
2+
useMutation,
3+
useQuery,
4+
useQueryClient,
5+
} from '@tanstack/react-query';
26

37
import {
48
deletePetInfo,
@@ -25,6 +29,7 @@ export const useGetPetListInfo = () => {
2529
return useQuery({
2630
queryKey: ['getPetListInfo'],
2731
queryFn: () => getPetListInfo(),
32+
refetchOnWindowFocus: true,
2833
staleTime: 1000 * 60 * 30,
2934
select: (data) =>
3035
data.petProfileList.map((pet) => ({
@@ -36,20 +41,23 @@ export const useGetPetListInfo = () => {
3641
});
3742
};
3843
export const useGetPetDetailInfo = (petId: number) => {
39-
const { data, isError } = useQuery({
44+
return useQuery({
4045
queryKey: ['getPetDetailInfo'],
4146
queryFn: () => getPetDetailInfo(petId),
4247
staleTime: 1000 * 60 * 30,
4348
});
44-
return { data, isError };
4549
};
4650

4751
export const usePutPetInfo = () => {
52+
const queryClient = useQueryClient();
53+
4854
return useMutation({
4955
mutationKey: ['putPetInfo'],
5056
mutationFn: ({ petId, formData }: { petId: number; formData: FormData }) =>
5157
putPetInfo(petId, formData),
5258
onSuccess: () => {
59+
// 데이터가 성공적으로 변경되었을 때 refetch
60+
queryClient.invalidateQueries({ queryKey: ['getPetListInfo'] });
5361
alert('펫 정보가 수정되었습니다.');
5462
},
5563
onError: () => {

packages/utils/src/apis/salon/income.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const getThisMonthIncome = async (): Promise<
1212
GetThisMonthIncomeResponse['response']
1313
> => {
1414
const { data } = await salonInstance.get('statistics/income/five-month');
15-
return data;
15+
return data.response;
1616
};
1717

1818
export const getSelectedMonthIncome = async (month: string): Promise<
@@ -21,33 +21,33 @@ export const getSelectedMonthIncome = async (month: string): Promise<
2121
const { data } = await salonInstance.get('statistics/income/month', {
2222
params: { month: month },
2323
});
24-
return data;
24+
return data.response;
2525
};
2626

2727
export const getRecentDaysIncome = async (): Promise<
2828
GetRecentDaysResponse['response']
2929
> => {
3030
const { data } = await salonInstance.get('statistics/age');
31-
return data;
31+
return data.response;
3232
};
3333

3434
export const getAgeStatistic = async (): Promise<
3535
GetAgeStatisticResponse['response']
3636
> => {
3737
const { data } = await salonInstance.get('statistics/age');
38-
return data;
38+
return data.response;
3939
};
4040

4141
export const getCharacterStatistic = async (): Promise<
4242
GetCharacterStatisticResponse['response']
4343
> => {
4444
const { data } = await salonInstance.get('statistics/character');
45-
return data;
45+
return data.response;
4646
};
4747

4848
export const getDiseaseStatistic = async (): Promise<
4949
GetDiseaseStatisticResponse['response']
5050
> => {
5151
const { data } = await salonInstance.get('statistics/disease');
52-
return data;
52+
return data.response;
5353
};

0 commit comments

Comments
 (0)