Skip to content

Commit 3fceafd

Browse files
authored
Merge pull request #101 from autoever-wms-project/feat/WMS-211
[WMS-211] 창고 현황 상세 구현
2 parents 5e6f6da + d0c354c commit 3fceafd

File tree

8 files changed

+199
-3
lines changed

8 files changed

+199
-3
lines changed

src/features/warehouse/api/queries.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,9 @@ export const queries = createQueryKeyStore({
1212
queryKey: ['warehouse', 'stockStatus', warehouseId],
1313
queryFn: () => warehouseApi.getStockStatus(warehouseId),
1414
}),
15+
detail: (rackName) => ({
16+
queryKey: [rackName],
17+
queryFn: () => warehouseApi.getDetail(rackName).then((res) => res.data),
18+
}),
1519
},
1620
});
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { client } from 'shared/api/axiosInstance';
2-
import { WarehouseSaturationRate, WarehouseStockInformation } from '../model/types';
2+
import { WarehouseDetail, WarehouseSaturationRate, WarehouseStockInformation } from '../model/types';
33

44
export const warehouseApi = {
55
getSaturationRate: async (warehouseId: number) =>
66
client.get<WarehouseSaturationRate[]>(`/inventory/storageRate/${warehouseId}`), // 창고 포화도 조회
77
getStockStatus: async (warehouseId: number) =>
88
client.get<WarehouseStockInformation[]>(`/inventory/stockStatus/${warehouseId}`), // 창고 재고 현황 조회
9+
getDetail: async (rackName: string) => client.get<WarehouseDetail[]>(`/stockIn/locationDetail?rackName=${rackName}`), // 랙 상세 조회
910
};

src/features/warehouse/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { WarehouseHeatmap } from './ui/WarehouseHeatmap';
22
export { Warehouse3DModel } from './ui/warehouse3d/Warehouse3DModel';
3+
export { RackDetail } from './ui/detail/RackDetail';

src/features/warehouse/lib/getChartOptions.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import { ApexOptions } from 'apexcharts';
22
import { LayerStorageRate } from '../model/types';
33

4-
export const getBaseOptions = (): ApexOptions => ({
4+
export const getBaseOptions = (
5+
handleDetailModalOpen: (layerStorageRate: number[], rackName: string) => void
6+
): ApexOptions => ({
57
chart: {
68
type: 'heatmap',
79
fontFamily: 'Pretendard',
810
toolbar: { show: false },
911
background: 'white',
12+
events: {
13+
dataPointSelection: (_, chartContext, config) => {
14+
const { seriesIndex, dataPointIndex } = config;
15+
const yLabel = chartContext.w.config.series[seriesIndex].name;
16+
const xLabel = chartContext.w.config.xaxis.categories[dataPointIndex];
17+
const dataPoint = chartContext.w.config.series[seriesIndex].data[dataPointIndex];
18+
const rackName = `${yLabel}-${xLabel}`;
19+
console.log(rackName);
20+
21+
const layerStorageRate = dataPoint.layerStorageRate.map((item: LayerStorageRate) => item.rate);
22+
handleDetailModalOpen(layerStorageRate, rackName);
23+
},
24+
},
1025
},
1126
legend: {
1227
position: 'bottom',

src/features/warehouse/model/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,18 @@ export interface ModelProps {
6262
scale?: [number, number, number];
6363
rotation?: [number, number, number];
6464
}
65+
66+
export interface WarehouseDetailItem {
67+
itemName: string;
68+
itemCode: string;
69+
categoryName: string;
70+
count: number;
71+
itemPrice: number;
72+
}
73+
74+
export interface WarehouseDetail {
75+
locationName: string;
76+
rackName: string;
77+
sectionName: string;
78+
locationItemList: WarehouseDetailItem[];
79+
}

src/features/warehouse/ui/WarehouseHeatmap.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import styled from '@emotion/styled';
33
import { useQuery } from '@tanstack/react-query';
44
import { ApexOptions } from 'apexcharts';
55
import ReactApexChart from 'react-apexcharts';
6+
import { useModalStore } from 'shared/model/modalStore';
67
import InnerLoadingSpinner from 'shared/ui/InnerLoadingSpinner';
78
import { queries } from '../api/queries';
89
import { getBaseOptions } from '../lib/getChartOptions';
910
import { transformDataToHeatmapSeries } from '../lib/transformDataToHeatmapSeries';
11+
import { RackDetail } from './detail/RackDetail';
1012

1113
const StyledReactApexChart = styled(ReactApexChart)`
1214
.apexcharts-xaxis-label {
@@ -37,15 +39,20 @@ export function WarehouseHeatmap() {
3739
const { data, isLoading } = useQuery(queries.warehouse.saturationRate(1));
3840
const [series, setSeries] = useState<ApexAxisChartSeries>([]);
3941
const [options, setOptions] = useState<ApexOptions>({});
42+
const { openModal } = useModalStore();
4043

4144
useEffect(() => {
4245
if (data?.data) {
4346
const transformedSeries = transformDataToHeatmapSeries(data.data);
4447
setSeries(transformedSeries);
45-
setOptions(getBaseOptions());
48+
setOptions(getBaseOptions(handleDetailModalOpen));
4649
}
4750
}, [data]);
4851

52+
const handleDetailModalOpen = (layerStorageRate: number[], rackName: string) => {
53+
openModal(<RackDetail layerStorageRate={layerStorageRate} rackName={rackName} />, { title: '구역 상세 정보' });
54+
};
55+
4956
if (isLoading) {
5057
return <InnerLoadingSpinner $minHeight="500px" />;
5158
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import styled from 'styled-components';
2+
3+
export const RackDetailWrapper = styled.div`
4+
display: flex;
5+
flex-direction: column;
6+
padding: 16px;
7+
gap: 52px;
8+
`;
9+
10+
export const TitleWrapper = styled.div`
11+
width: 600px;
12+
display: flex;
13+
justify-content: center;
14+
align-items: center;
15+
`;
16+
17+
export const Title = styled.span`
18+
font-size: 24px;
19+
font-weight: 600;
20+
letter-spacing: 2px;
21+
`;
22+
23+
export const LayerWrapper = styled.div`
24+
display: flex;
25+
align-items: center;
26+
justify-content: space-between;
27+
position: relative;
28+
padding-bottom: 16px;
29+
`;
30+
31+
export const DotLine = styled.div`
32+
position: absolute;
33+
left: 45%;
34+
top: 200%;
35+
width: 0; /* 배경 대신 border 사용 */
36+
height: 60px;
37+
border-left: 2px dotted rgba(0, 0, 0, 0.5); /* 점선 효과 */
38+
`;
39+
40+
export const LayerLevel = styled.span`
41+
font-size: 22px;
42+
font-weight: 400;
43+
`;
44+
45+
export const LayerSquareWrapper = styled.div`
46+
position: relative;
47+
`;
48+
49+
export const LayerRate = styled.span`
50+
position: absolute;
51+
top: 50%;
52+
left: 50%;
53+
transform: translate(-50%, -50%);
54+
font-size: 18px;
55+
font-weight: 500;
56+
`;
57+
58+
export const LayerSquare = styled.div<{ $color?: 'red' | 'blue' | 'green' | 'yellow' }>`
59+
width: 100px;
60+
height: 40px;
61+
background: ${({ $color }) =>
62+
$color === 'blue'
63+
? 'linear-gradient(135deg, rgb(91, 143, 249, 0.9), rgba(91, 143, 249, 0.7))'
64+
: $color === 'green'
65+
? 'linear-gradient(135deg, rgba(88, 240, 0, 0.9), rgba(88, 240, 0, 0.7))'
66+
: $color === 'yellow'
67+
? 'linear-gradient(135deg, rgba(255, 221, 0, 0.9), rgba(255, 255, 100, 0.7))'
68+
: 'linear-gradient(135deg, rgba(255, 0, 0, 0.9), rgba(255, 100, 100, 0.7))'};
69+
transform: skew(-40deg) rotateX(10deg) scale(1.05);
70+
box-shadow: 8px 12px 18px rgba(0, 0, 0, 0.4);
71+
border: 1px solid rgba(0, 0, 0, 0.2);
72+
`;
73+
74+
export const LayerInfoWrapper = styled.div`
75+
width: 300px;
76+
height: 100px;
77+
display: flex;
78+
flex-direction: column;
79+
gap: 8px;
80+
border: 1px solid rgba(0, 0, 0, 0.2);
81+
border-radius: 5px;
82+
padding: 16px;
83+
`;
84+
85+
export const Span = styled.span`
86+
font-size: 16px;
87+
`;
88+
89+
export const EmptyWrapper = styled.div`
90+
width: 100%;
91+
height: 100%;
92+
display: flex;
93+
justify-content: center;
94+
align-items: center;
95+
`;
96+
97+
export const Empty = styled.div`
98+
font-size: 16px;
99+
color: ${({ theme }) => theme.colors.grayLight};
100+
`;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import CircularProgress from '@mui/material/CircularProgress';
2+
import { useQuery } from '@tanstack/react-query';
3+
import { queries } from 'features/warehouse/api/queries';
4+
import * as S from './RackDetail.style';
5+
6+
export const RackDetail = ({ layerStorageRate, rackName }: { layerStorageRate: number[]; rackName: string }) => {
7+
const { data, isLoading } = useQuery({ ...queries.warehouse.detail(rackName) });
8+
9+
if (isLoading) return <CircularProgress />;
10+
if (!data) return null;
11+
12+
// 값에 따라 색상을 결정하는 함수
13+
const getColor = (rate: number) => {
14+
if (rate <= 25) return 'green';
15+
if (rate <= 50) return 'blue';
16+
if (rate <= 75) return 'yellow';
17+
return 'red';
18+
};
19+
const rateList = layerStorageRate.slice().reverse();
20+
const title = data[0].locationName.replace(/\s\d$/, '');
21+
22+
return (
23+
<S.RackDetailWrapper>
24+
<S.TitleWrapper>
25+
<S.Title>{title}</S.Title>
26+
</S.TitleWrapper>
27+
28+
{rateList.map((rate, index) => (
29+
<S.LayerWrapper key={index}>
30+
<S.LayerLevel>{3 - index}</S.LayerLevel>
31+
<S.LayerSquareWrapper>
32+
<S.LayerSquare $color={getColor(rate)}></S.LayerSquare>
33+
{index < rateList.length - 1 && <S.DotLine />}
34+
<S.LayerRate>{rate.toFixed()}%</S.LayerRate>
35+
</S.LayerSquareWrapper>
36+
<S.LayerInfoWrapper>
37+
{data[2 - index].locationItemList.length ? (
38+
<>
39+
<S.Span>카테고리: {data[2 - index].locationItemList[0].categoryName}</S.Span>
40+
<S.Span>품목명: {data[2 - index].locationItemList[0].itemName}</S.Span>
41+
<S.Span>수량: {data[2 - index].locationItemList[0].count}</S.Span>
42+
</>
43+
) : (
44+
<S.EmptyWrapper>
45+
<S.Empty>비어있음</S.Empty>
46+
</S.EmptyWrapper>
47+
)}
48+
</S.LayerInfoWrapper>
49+
</S.LayerWrapper>
50+
))}
51+
</S.RackDetailWrapper>
52+
);
53+
};

0 commit comments

Comments
 (0)