diff --git a/frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx b/frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx new file mode 100644 index 00000000..29f30006 --- /dev/null +++ b/frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx @@ -0,0 +1,299 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { Plus, Trash2, Pencil, Download, Loader2, FlaskConical, Save } from "lucide-react"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { toast } from "sonner"; +import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { FullscreenDialog } from "@/components/common/FullscreenDialog"; +import { useConfirmDialog } from "@/components/common/ConfirmDialog"; + +// --- 타입 --- + +interface SampleItem { + id: string; + name: string; + description: string; + created_at: string; + updated_at: string; +} + +// --- 컬럼 정의 --- + +const COLUMNS: DataGridColumn[] = [ + { key: "name", label: "이름", width: "w-[200px]", sortable: true, filterable: true }, + { key: "description", label: "설명", minWidth: "min-w-[300px]", sortable: true, filterable: true }, + { key: "created_at", label: "등록일시", width: "w-[180px]", sortable: true }, + { key: "updated_at", label: "수정일시", width: "w-[180px]", sortable: true }, +]; + +// --- 페이지 --- + +export default function SamplePage() { + const [items, setItems] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); + const [loading, setLoading] = useState(false); + + // 체크박스 다중 선택 + const [checkedIds, setCheckedIds] = useState([]); + + // DynamicSearchFilter 필터 값 + const [activeFilters, setActiveFilters] = useState([]); + + // 등록/수정 모달 + const [modalOpen, setModalOpen] = useState(false); + const [isEdit, setIsEdit] = useState(false); + const [editId, setEditId] = useState(null); + const [saving, setSaving] = useState(false); + const [formName, setFormName] = useState(""); + const [formDesc, setFormDesc] = useState(""); + + // 삭제 확인 다이얼로그 + const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + + // 목록 조회 + const fetchItems = useCallback(async () => { + setLoading(true); + try { + const res = await apiClient.get("/sample/list?page=1&limit=100"); + const data: SampleItem[] = res.data?.data || []; + setItems(data); + } catch (err) { + console.error("샘플 목록 조회 실패:", err); + toast.error("목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchItems(); + }, [fetchItems]); + + // 클라이언트 필터 적용 + useEffect(() => { + if (activeFilters.length === 0) { + setFilteredItems(items); + return; + } + const filtered = items.filter((item) => { + return activeFilters.every((f) => { + const val = String((item as any)[f.columnName] || "").toLowerCase(); + if (f.operator === "contains") return val.includes(f.value.toLowerCase()); + if (f.operator === "equals") return val === f.value.toLowerCase(); + return true; + }); + }); + setFilteredItems(filtered); + }, [items, activeFilters]); + + // 등록 모달 열기 + const openRegister = () => { + setIsEdit(false); + setEditId(null); + setFormName(""); + setFormDesc(""); + setModalOpen(true); + }; + + // 수정 모달 열기 + const openEdit = (item: SampleItem) => { + setIsEdit(true); + setEditId(item.id); + setFormName(item.name); + setFormDesc(item.description); + setModalOpen(true); + }; + + // 저장 (등록 or 수정) + const handleSave = async () => { + if (!formName.trim()) { + toast.error("이름은 필수 입력입니다."); + return; + } + if (!formDesc.trim()) { + toast.error("설명은 필수 입력입니다."); + return; + } + + setSaving(true); + try { + if (isEdit && editId) { + await apiClient.put(`/sample/${editId}`, { name: formName.trim(), description: formDesc.trim() }); + toast.success("수정되었습니다."); + } else { + await apiClient.post("/sample", { name: formName.trim(), description: formDesc.trim() }); + toast.success("등록되었습니다."); + } + setModalOpen(false); + setCheckedIds([]); + await fetchItems(); + } catch (err: any) { + toast.error(err.response?.data?.message || "저장에 실패했습니다."); + } finally { + setSaving(false); + } + }; + + // 삭제 + const handleDelete = async () => { + if (checkedIds.length === 0) { + toast.error("삭제할 항목을 선택해주세요."); + return; + } + const ok = await confirm(`선택한 ${checkedIds.length}건을 삭제하시겠습니까?`, { + description: "삭제된 데이터는 복구할 수 없습니다.", + variant: "destructive", + confirmText: "삭제", + }); + if (!ok) return; + + try { + await Promise.all(checkedIds.map((id) => apiClient.delete(`/sample/${id}`))); + toast.success("삭제되었습니다."); + setCheckedIds([]); + await fetchItems(); + } catch (err) { + toast.error("삭제에 실패했습니다."); + } + }; + + // 수정 버튼: 체크된 항목 1건일 때 활성 + const handleEditButton = () => { + if (checkedIds.length !== 1) return; + const item = filteredItems.find((i) => i.id === checkedIds[0]); + if (item) openEdit(item); + }; + + // 엑셀 다운로드 + const handleExcelDownload = async () => { + if (filteredItems.length === 0) { + toast.error("다운로드할 데이터가 없습니다."); + return; + } + const exportData = filteredItems.map((item) => ({ + 이름: item.name, + 설명: item.description, + 등록일시: item.created_at, + 수정일시: item.updated_at, + })); + await exportToExcel(exportData, "샘플목록.xlsx", "샘플"); + toast.success("엑셀 다운로드 완료"); + }; + + return ( +
+ {/* 검색 필터 */} + + + {/* 메인 테이블 */} +
+ {/* 헤더 버튼 */} +
+
+ + 샘플 목록 + {filteredItems.length}건 +
+
+ + + + +
+
+ + {/* DataGrid */} +
+ openEdit(row as SampleItem)} + emptyMessage="등록된 샘플이 없습니다" + /> +
+
+ + {/* 등록/수정 모달 */} + + + + + } + > +
+
+ + setFormName(e.target.value)} + placeholder="이름을 입력하세요" + className="h-9" + /> +
+
+ +