Skip to content

Commit fd33ce8

Browse files
authored
Merge pull request #104 from billilge/#96-item-api-fix
[Feat/#96] 물품 수정 api 연결
2 parents beb45a8 + 3b619a2 commit fd33ce8

File tree

4 files changed

+212
-11
lines changed

4 files changed

+212
-11
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
Sheet,
3+
SheetContent,
4+
SheetHeader,
5+
SheetTitle,
6+
// SheetDescription,
7+
SheetTrigger,
8+
} from '@/components/ui/sheet';
9+
import { ReactNode } from 'react';
10+
11+
interface SidebarProps {
12+
children: ReactNode;
13+
title?: string;
14+
// description?: string;
15+
triggerText?: string;
16+
}
17+
18+
export default function Sidebar({
19+
children,
20+
title = 'Sidebar Title',
21+
// description = '',
22+
triggerText = 'Open',
23+
}: SidebarProps) {
24+
return (
25+
<Sheet>
26+
<SheetTrigger className="whitespace-nowrap bg-transparent text-sm text-black-primary">
27+
{triggerText}
28+
</SheetTrigger>
29+
<SheetContent className="max-w-[40rem]! w-full">
30+
<SheetHeader>
31+
<SheetTitle className="flex justify-center text-2xl font-medium">
32+
{title}
33+
</SheetTitle>
34+
{/* {description && <SheetDescription>{description}</SheetDescription>} */}
35+
</SheetHeader>
36+
<div className="mt-4">{children}</div>
37+
</SheetContent>
38+
</Sheet>
39+
);
40+
}

src/app/desktop/(nav-bar)/item-list/_components/ItemTable/index.tsx

Lines changed: 155 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useCallback, useState } from 'react';
12
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
23
import {
34
Table,
@@ -12,6 +13,10 @@ import { Checkbox } from '@/components/ui/checkbox';
1213
import { Item, ItemTableProps, ItemTypeText } from '@/types/items';
1314
import Image from 'next/image';
1415
import { PageChangeAction } from '@/types/paginationType';
16+
import { useMutation } from '@tanstack/react-query';
17+
import { updateItems } from '@/services/items';
18+
import toast from 'react-hot-toast';
19+
import Sidebar from '../ItemSidebar/index';
1520

1621
export default function ItemTable({
1722
items = [],
@@ -25,17 +30,31 @@ export default function ItemTable({
2530
console.log(pageAction);
2631
},
2732
}: ItemTableProps) {
28-
// 선택된 항목을 다루는 함수
33+
const [formData, setFormData] = useState({
34+
itemId: selected,
35+
selectedImage: null as File | null,
36+
itemName: '',
37+
isConsumable: false,
38+
quantity: '' as number | '',
39+
});
40+
41+
const mutation = useMutation({
42+
mutationFn: (data: { itemId: number; formData: FormData }) =>
43+
updateItems(data.formData, data.itemId),
44+
onSuccess: () => {
45+
toast.success('변경사항이 저장되었습니다.');
46+
},
47+
onError: () => {
48+
toast.error('변경사항 저장에 실패했습니다.');
49+
},
50+
});
51+
2952
const handleSelect = (id: number) => {
30-
setSelected(id); // 단일 선택으로 변경
53+
setSelected(id);
3154
};
3255

3356
const handleSelectAll = () => {
34-
if (selected === items[0]?.itemId) {
35-
setSelected(0); // 전체 선택 해제
36-
} else {
37-
setSelected(items[0]?.itemId); // 첫 번째 항목을 선택(전체 선택)
38-
}
57+
setSelected(selected === items[0]?.itemId ? 0 : items[0]?.itemId); // Toggle selection of first item
3958
};
4059

4160
const handlePageChangeBtnClick = (
@@ -46,6 +65,49 @@ export default function ItemTable({
4665
onPageChange(pageChangeAction);
4766
};
4867

68+
const handleUpdateItem = useCallback(
69+
(itemId: number) => {
70+
const { itemName, quantity, selectedImage, isConsumable } = formData;
71+
72+
if (!itemName || quantity === '' || quantity <= 0) {
73+
toast.error('모든 정보를 입력하세요.');
74+
return;
75+
}
76+
77+
const newFormData = new FormData();
78+
if (selectedImage) newFormData.append('image', selectedImage);
79+
80+
const editData = {
81+
name: itemName,
82+
type: isConsumable ? 'CONSUMPTION' : 'RENTAL',
83+
count: Number(quantity),
84+
};
85+
86+
newFormData.append(
87+
'itemRequest',
88+
new Blob([JSON.stringify(editData)], { type: 'application/json' }),
89+
);
90+
91+
mutation.mutate({ itemId, formData: newFormData });
92+
93+
setFormData({
94+
itemId: 0,
95+
selectedImage: null,
96+
itemName: '',
97+
isConsumable: false,
98+
quantity: '',
99+
});
100+
},
101+
[formData, mutation],
102+
);
103+
104+
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
105+
const file = event.target.files?.[0];
106+
if (file) {
107+
setFormData((prev) => ({ ...prev, selectedImage: file }));
108+
}
109+
};
110+
49111
return (
50112
<div className="flex w-full flex-col p-10">
51113
<Table>
@@ -54,7 +116,7 @@ export default function ItemTable({
54116
{showCheckboxes && (
55117
<TableHead className="w-10 text-center">
56118
<Checkbox
57-
checked={selected === items[0]?.itemId} // 선택된 첫 번째 항목이 전체 항목과 일치하는지 확인
119+
checked={selected === items[0]?.itemId}
58120
onCheckedChange={handleSelectAll}
59121
/>
60122
</TableHead>
@@ -73,7 +135,7 @@ export default function ItemTable({
73135
{showCheckboxes && (
74136
<TableCell className="w-10 text-center">
75137
<Checkbox
76-
checked={selected === item.itemId} // selected 상태가 현재 itemId와 일치하면 체크
138+
checked={selected === item.itemId}
77139
onCheckedChange={() => handleSelect(item.itemId)}
78140
/>
79141
</TableCell>
@@ -97,6 +159,90 @@ export default function ItemTable({
97159
<TableCell className="w-30 text-center">
98160
{item.renterCount}
99161
</TableCell>
162+
<TableCell className="w-30 text-center">
163+
<Sidebar triggerText="수정하기" title="물품 수정하기">
164+
<div className="mt-4 flex flex-col gap-2">
165+
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
166+
<label className="text-sm font-semibold">
167+
복지물품명
168+
</label>
169+
<input
170+
type="text"
171+
value={formData.itemName}
172+
onChange={(e) =>
173+
setFormData({ ...formData, itemName: e.target.value })
174+
}
175+
className="rounded-md border px-4 py-2"
176+
/>
177+
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
178+
<label className="text-sm font-semibold">
179+
소모품 여부
180+
</label>
181+
<div className="flex gap-2">
182+
<button
183+
type="button"
184+
className={`rounded-md border px-4 py-2 ${!formData.isConsumable ? 'bg-blue-500 text-white' : 'text-blue-500'}`}
185+
onClick={() =>
186+
setFormData({ ...formData, isConsumable: false })
187+
}
188+
>
189+
대여물품
190+
</button>
191+
<button
192+
type="button"
193+
className={`rounded-md border px-4 py-2 ${formData.isConsumable ? 'bg-blue-500 text-white' : 'text-blue-500'}`}
194+
onClick={() =>
195+
setFormData({ ...formData, isConsumable: true })
196+
}
197+
>
198+
소모물품
199+
</button>
200+
</div>
201+
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
202+
<label className="text-sm font-semibold">수량</label>
203+
<input
204+
type="number"
205+
value={formData.quantity}
206+
onChange={(e) =>
207+
setFormData({
208+
...formData,
209+
quantity: Number(e.target.value),
210+
})
211+
}
212+
className="rounded-md border px-4 py-2"
213+
/>
214+
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
215+
<label className="text-sm font-semibold">
216+
이미지 업로드
217+
</label>
218+
<p>이미지 변경이 없을 경우 업로드하지 않고 저장합니다.</p>
219+
<input
220+
type="file"
221+
accept="image/*"
222+
onChange={handleImageChange}
223+
/>
224+
{formData.selectedImage && (
225+
<Image
226+
src={URL.createObjectURL(formData.selectedImage)}
227+
width={24}
228+
height={24}
229+
alt="미리보기"
230+
className="mt-2 h-32 w-32 rounded-md object-cover"
231+
/>
232+
)}
233+
</div>
234+
<div className="flex justify-center">
235+
<Button
236+
size="lg"
237+
variant="primary"
238+
onClick={() => handleUpdateItem(item.itemId)}
239+
className="mt-4 w-full"
240+
>
241+
저장
242+
</Button>
243+
</div>
244+
</Sidebar>
245+
</TableCell>
100246
</TableRow>
101247
))
102248
) : (

src/services/items.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,17 @@ export const deleteItems = async (id: number) => {
2323
const response = await PrivateAxiosInstance.delete(`/admin/items/${id}`);
2424
return response.data;
2525
};
26+
27+
export const getItem = async (id: number) => {
28+
const response = await PrivateAxiosInstance.get(`/admin/items/${id}`);
29+
return response.data;
30+
};
31+
32+
export const updateItems = async (data: FormData, id: number) => {
33+
const response = await PrivateAxiosInstance.put(`/admin/items/${id}`, data, {
34+
headers: {
35+
'Content-Type': 'multipart/form-data',
36+
},
37+
});
38+
return response.data;
39+
};

src/types/items.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export interface Item {
44
itemId: number;
55
itemName: string;
66
itemType: string;
7-
count: number;
8-
renterCount: number;
7+
count?: number;
8+
renterCount?: number;
99
imageUrl: string;
1010
}
1111

@@ -16,6 +16,7 @@ export interface ItemTableProps extends PaginationProps {
1616
selected: number;
1717
setSelected: (selectedIds: number) => void;
1818
handleDelete?: (selectedIds: string) => void;
19+
onEdit?: (selectedId: number) => void;
1920
}
2021

2122
export const ItemTypeText: Record<string, string> = {

0 commit comments

Comments
 (0)