diff --git a/src/app/desktop/item-list/_components/ItemTable/index.tsx b/src/app/desktop/item-list/_components/ItemTable/index.tsx index bda825e..7176f4d 100644 --- a/src/app/desktop/item-list/_components/ItemTable/index.tsx +++ b/src/app/desktop/item-list/_components/ItemTable/index.tsx @@ -10,28 +10,10 @@ import { } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; - -interface Item { - logo: string; - name: string; - isConsumable: boolean; - totalQuantity: number; - rentedQuantity: number; - id: string; - isAdmin?: boolean; -} - -interface ItemTableProps { - data: Item[]; - showCheckboxes?: boolean; - headers?: string[]; - selected: string[]; - setSelected: (selectedIds: (prev: string[]) => string[]) => void; - handleDelete?: (selectedIds: string[]) => void; -} +import { Item, ItemTableProps } from '@/types/items'; export default function ItemTable({ - data, + items = [], showCheckboxes = true, headers = ['로고', '물품명', '소모품', '총 수량', '대여 중'], selected, @@ -41,26 +23,25 @@ export default function ItemTable({ const [currentPage, setCurrentPage] = useState(1); const rowsPerPage = 10; - const handleSelect = (id: string) => { - setSelected((prev: string[]) => - prev.includes(id) - ? prev.filter((itemId) => itemId !== id) - : [...prev, id], - ); - }; + const totalPages = Math.ceil((items?.length || 0) / rowsPerPage); - const paginatedData = data.slice( - (currentPage - 1) * rowsPerPage, - currentPage * rowsPerPage, - ); + // 선택된 항목을 다루는 함수 + const handleSelect = (id: number) => { + setSelected(id); // 단일 선택으로 변경 + }; const handleSelectAll = () => { - const visibleIds = paginatedData.map((item) => item.id); - setSelected((prev: string[]) => - prev.length === visibleIds.length ? [] : visibleIds, - ); + if (selected === paginatedData[0]?.itemId) { + setSelected(0); // 전체 선택 해제 + } else { + setSelected(paginatedData[0]?.itemId); // 첫 번째 항목을 선택(전체 선택) + } }; + const paginatedData = items + ? items.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage) + : []; + return (
@@ -69,7 +50,7 @@ export default function ItemTable({ {showCheckboxes && ( @@ -82,55 +63,68 @@ export default function ItemTable({ - {paginatedData.map((item) => ( - - {showCheckboxes && ( - - handleSelect(item.id)} + {paginatedData.length > 0 ? ( + paginatedData.map((item: Item) => ( + + {showCheckboxes && ( + + handleSelect(item.itemId)} + /> + + )} + + item - )} - {item.logo} - {item.name} - - {item.isConsumable ? '소모품' : '대여 물품'} - - - {item.totalQuantity} - - - {item.rentedQuantity} - - {headers.includes('관리자 여부') && ( - {item.isAdmin !== undefined && (item.isAdmin ? 'o' : 'x')} + {item.itemName} + + + {item.itemType ? 'RENTAL' : 'CONSUMPTION'} + + {item.count} + + {item.renterCount} - )} + + )) + ) : ( + + + 데이터가 없습니다. + - ))} + )}
-
- - - {currentPage} / {Math.ceil(data.length / rowsPerPage)} - - +
+
+ + + {totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0'} + + +
); diff --git a/src/app/desktop/item-list/page.tsx b/src/app/desktop/item-list/page.tsx index 69f6e74..0d557fb 100644 --- a/src/app/desktop/item-list/page.tsx +++ b/src/app/desktop/item-list/page.tsx @@ -2,109 +2,113 @@ import Sidebar from 'src/components/desktop/Sidebar'; import Search from '@/components/desktop/Search'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { getItems, addItems, deleteItems } from '@/services/items'; import TableComponent from './_components/ItemTable'; -const dummyData = [ - { - id: 1, - name: '물품1', - isConsumable: false, - totalQuantity: 100, - rentedQuantity: 30, - logo: 'logo1.png', - }, - { - id: 2, - name: '물품2', - isConsumable: false, - totalQuantity: 200, - rentedQuantity: 50, - logo: 'logo2.png', - }, - { - id: 3, - name: '물품3', - isConsumable: false, - totalQuantity: 300, - rentedQuantity: 70, - logo: 'logo3.png', - }, -]; - -const dummyData2 = [ - { - id: 4, - name: '물품4', - isConsumable: false, - totalQuantity: 400, - rentedQuantity: 90, - logo: 'logo4.png', - }, - { - id: 5, - name: '물품5', - isConsumable: false, - totalQuantity: 500, - rentedQuantity: 120, - logo: 'logo5.png', - }, -]; - -export default function PayerInquiryPage() { - const [data, setData] = useState(dummyData); - const [, setAddedData] = useState(dummyData2); - const [isDeleteModeOriginal, setIsDeleteModeOriginal] = useState(false); - const [selectedOriginal, setSelectedOriginal] = useState([]); - - const [selectedImage, setSelectedImage] = useState(null); - const [itemName, setItemName] = useState(''); - const [isConsumable, setIsConsumable] = useState(false); - const [quantity, setQuantity] = useState(''); - - const handleDeleteOriginal = () => { - const hasRentedItems = data.some( - (item) => - selectedOriginal.includes(String(item.id)) && item.rentedQuantity > 0, - ); - - if (hasRentedItems) { - alert('대여 중인 물품은 삭제할 수 없습니다.'); - return; - } +export default function ItemListPage() { + const queryClient = useQueryClient(); - setData(data.filter((item) => !selectedOriginal.includes(String(item.id)))); - setIsDeleteModeOriginal(false); - setSelectedOriginal([]); - }; + const [formData, setFormData] = useState({ + selectedImage: null as File | null, + itemName: '', + isConsumable: false, + quantity: '' as number | '', + }); + + const [isDeleteMode, setIsDeleteMode] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + + const mutation = useMutation({ + mutationFn: addItems, + onSuccess: () => { + alert('물품 등록이 완료되었습니다.'); + queryClient.invalidateQueries({ queryKey: ['items'] }); + }, + onError: () => { + alert('물품 등록에 실패했습니다.'); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: deleteItems, + onSuccess: () => { + alert('선택된 물품이 삭제되었습니다.'); + }, + onError: () => { + alert('물품 삭제에 실패했습니다.'); + }, + }); + + const { + data: originalData = [], + isError: originalDataError, + isLoading, + } = useQuery({ + queryKey: ['items'], + queryFn: getItems, + }); + + // 물품 추가 핸들러 + const handleAddItem = useCallback(() => { + const { itemName, quantity, selectedImage, isConsumable } = formData; - const handleAddItem = () => { if (!itemName || quantity === '' || quantity <= 0) { alert('모든 정보를 입력하세요.'); return; } - const newItem = { - id: Date.now(), + const newFormData = new FormData(); + if (selectedImage) newFormData.append('image', selectedImage); + + const itemData = { name: itemName, - isConsumable, - totalQuantity: Number(quantity), - rentedQuantity: 0, - logo: selectedImage || 'default.png', + type: isConsumable ? 'CONSUMPTION' : 'RENTAL', + count: Number(quantity), }; - setAddedData((prev) => [...prev, newItem]); + newFormData.append( + 'itemRequest', + new Blob([JSON.stringify(itemData)], { type: 'application/json' }), + ); - setItemName(''); - setIsConsumable(false); - setQuantity(''); - setSelectedImage(null); + mutation.mutate(newFormData); + + setFormData({ + selectedImage: null, + itemName: '', + isConsumable: false, + quantity: '', + }); + }, [formData, mutation]); + + // 이미지 파일 선택 핸들러 + const handleImageChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + setFormData((prev) => ({ ...prev, selectedImage: file })); + } + }; - alert('물품 등록을 완료하였습니다.'); + // 삭제 모드 토글 + const toggleDeleteMode = () => { + setIsDeleteMode((prev) => !prev); + setSelectedItem(null); // 삭제 모드에서 선택된 항목 초기화 }; - const isAddButtonDisabled = !itemName || quantity === '' || quantity <= 0; + // 물품 삭제 핸들러 + const handleDeleteItem = () => { + if (selectedItem === null) { + alert('삭제할 물품을 선택해 주세요.'); + return; + } + + // @ts-ignore + deleteMutation.mutate([selectedItem]); // selectedItem을 배열로 전달 + setSelectedItem(null); // 삭제 후 선택 초기화 + }; return (
@@ -118,90 +122,70 @@ export default function PayerInquiryPage() { title="복지 물품 추가하기" description="설명" > -
-

복지물품 이모티콘

- { - if (e.target.files && e.target.files[0]) { - setSelectedImage(URL.createObjectURL(e.target.files[0])); - } - }} - className="hidden" - id="imageUpload" - /> - -
-
setItemName(e.target.value)} + value={formData.itemName} + onChange={(e) => + setFormData({ ...formData, itemName: e.target.value }) + } placeholder="등록할 복지물품의 이름을 입력해 주세요." className="rounded-md border px-4 py-2" /> -
- - setQuantity(e.target.value ? Number(e.target.value) : '') + setFormData({ + ...formData, + quantity: e.target.value ? Number(e.target.value) : '', + }) } placeholder="등록할 복지물품의 수량을 입력해 주세요." className="rounded-md border px-4 py-2" /> + + + {formData.selectedImage && ( + 미리보기 + )}
-
@@ -211,24 +195,26 @@ export default function PayerInquiryPage() {
- {isDeleteModeOriginal && ( + {isDeleteMode && ( diff --git a/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx b/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx index 315c100..01f7745 100644 --- a/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx +++ b/src/app/desktop/payer-inquiry/_components/TableComponent/index.tsx @@ -10,7 +10,7 @@ import { import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; -import { Payer, TableComponentProps } from '@/types/payerInquiry'; +import { Payer, TableComponentProps } from '@/types/payers'; export default function TableComponent({ payers, @@ -18,16 +18,15 @@ export default function TableComponent({ headers = ['이름', '학번', '회원 여부'], // 기본값을 설정 selected, setSelected, - handleDelete = () => {}, // 기본값으로 빈 함수 설정 }: TableComponentProps) { const [currentPage, setCurrentPage] = useState(1); const rowsPerPage = 10; - const handleSelect = (student_id: string) => { - setSelected((prev: string[]) => - prev.includes(student_id) - ? prev.filter((id) => id !== student_id) - : [...prev, student_id], + const handleSelect = (payerId: number) => { + setSelected((prev: number[]) => + prev.includes(payerId) + ? prev.filter((id) => id !== payerId) + : [...prev, payerId], ); }; @@ -38,10 +37,10 @@ export default function TableComponent({ const handleSelectAll = () => { const visibleIds = paginatedData.map( - (item: { studentId: string }) => item.studentId, + (item: { payerId: number }) => item.payerId, ); - setSelected((prev: string[]) => - prev.length === visibleIds.length ? [] : visibleIds, + setSelected( + (prev: number[]) => (prev.length === visibleIds.length ? [] : visibleIds), // 배열 길이를 비교하는 대신 선택된 아이디와 비교 ); }; @@ -67,12 +66,12 @@ export default function TableComponent({ {paginatedData.map((item: Payer) => ( - + {showCheckboxes && ( handleSelect(item.studentId)} + checked={selected.includes(item.payerId)} + onCheckedChange={() => handleSelect(item.payerId)} /> )} diff --git a/src/app/desktop/payer-inquiry/page.tsx b/src/app/desktop/payer-inquiry/page.tsx index ae9ca1c..ef6cead 100644 --- a/src/app/desktop/payer-inquiry/page.tsx +++ b/src/app/desktop/payer-inquiry/page.tsx @@ -6,7 +6,7 @@ import { useState, useEffect } from 'react'; import AddStudentId from '@/components/desktop/AddStudentId'; import { Button } from '@/components/ui/button'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { getPayer, addPayer } from '@/services/payer-inquiry'; +import { getPayer, addPayer, deletePayer } from '@/services/payers'; import TableComponent from './_components/TableComponent'; import AddInput from '../../../components/desktop/AddInput'; @@ -15,8 +15,8 @@ export default function PayerInquiryPage() { const [addedData, setAddedData] = useState([]); const [isDeleteModeOriginal, setIsDeleteModeOriginal] = useState(false); const [isDeleteModeAdded, setIsDeleteModeAdded] = useState(false); - const [selectedOriginal, setSelectedOriginal] = useState([]); - const [selectedAdded, setSelectedAdded] = useState([]); + const [selectedOriginal, setSelectedOriginal] = useState([]); // 수정: payerId 배열로 + const [selectedAdded, setSelectedAdded] = useState([]); // 수정: payerId 배열로 const [newStudentId, setNewStudentId] = useState(''); const [newStudentName, setNewStudentName] = useState(''); @@ -32,6 +32,17 @@ export default function PayerInquiryPage() { }, }); + const deletemutation = useMutation({ + mutationFn: deletePayer, + onSuccess: () => { + alert('선택된 납부자 정보가 성공적으로 삭제되었습니다.'); + setAddedData([]); + }, + onError: () => { + alert('납부자 정보 삭제에 실패했습니다.'); + }, + }); + const { data: originalData = [], isError: originalDataError, @@ -81,11 +92,14 @@ export default function PayerInquiryPage() { const handleDeleteData = (mode: 'original' | 'added') => { if (mode === 'original') { + // payerId 배열을 직접 전달 + deletemutation.mutate(selectedOriginal); // payerId 배열을 전달 setSelectedOriginal([]); setIsDeleteModeOriginal(false); } else { + // addedData에서 삭제할 때도 payerId로 삭제 const updatedData = addedData.filter( - (item) => !selectedAdded.includes(item.studentId), + (item) => !selectedAdded.includes(item.studentId), // studentId로 비교 후 삭제 ); setAddedData(updatedData); setSelectedAdded([]); diff --git a/src/services/items.ts b/src/services/items.ts new file mode 100644 index 0000000..88ee8b3 --- /dev/null +++ b/src/services/items.ts @@ -0,0 +1,16 @@ +import PrivateAxiosInstance from './privateAxiosInstance'; + +export const getItems = async () => { + const response = await PrivateAxiosInstance.get('/admin/items'); + return response.data; +}; + +export const addItems = async (data: FormData) => { + const response = await PrivateAxiosInstance.post('/admin/items', data); + return response.data; +}; + +export const deleteItems = async (id: number) => { + const response = await PrivateAxiosInstance.delete(`/admin/items/${id}`); + return response.data; +}; diff --git a/src/services/payer-inquiry.ts b/src/services/payers.ts similarity index 66% rename from src/services/payer-inquiry.ts rename to src/services/payers.ts index f4943de..6ebfe4e 100644 --- a/src/services/payer-inquiry.ts +++ b/src/services/payers.ts @@ -15,3 +15,10 @@ export const addPayer = async (data: { ); return response.data; }; + +export const deletePayer = async (payerIds: number[]) => { + const response = await PrivateAxiosInstance.delete('/admin/members/payers', { + data: { payerIds }, // DELETE 요청의 본문에 payerIds를 포함 + }); + return response.data; +}; diff --git a/src/services/privateAxiosInstance.ts b/src/services/privateAxiosInstance.ts index f027b58..9fdbe73 100644 --- a/src/services/privateAxiosInstance.ts +++ b/src/services/privateAxiosInstance.ts @@ -3,7 +3,6 @@ import axios, { InternalAxiosRequestConfig } from 'axios'; const PrivateAxiosInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE_URI, headers: { - 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.NEXT_PUBLIC_AUTH_TOKEN}`, }, }); diff --git a/src/types/items.ts b/src/types/items.ts new file mode 100644 index 0000000..be05be6 --- /dev/null +++ b/src/types/items.ts @@ -0,0 +1,17 @@ +export interface Item { + itemId: number; + itemName: string; + itemType: string; + count: number; + renterCount: number; + imageUrl: string; +} + +export interface ItemTableProps { + items: Item[]; + showCheckboxes?: boolean; + headers?: string[]; + selected: number; + setSelected: (selectedIds: number) => void; + handleDelete?: (selectedIds: string) => void; +} diff --git a/src/types/payerInquiry.ts b/src/types/payers.ts similarity index 75% rename from src/types/payerInquiry.ts rename to src/types/payers.ts index e2fd88e..4cf5b26 100644 --- a/src/types/payerInquiry.ts +++ b/src/types/payers.ts @@ -9,7 +9,7 @@ export interface TableComponentProps { payers: Payer[]; showCheckboxes?: boolean; headers?: string[]; - selected: string[]; - setSelected: (selectedIds: (prev: string[]) => string[]) => void; + selected: number[]; + setSelected: (selectedIds: (prev: number[]) => number[]) => void; handleDelete?: (selectedIds: string[]) => void; }