From 93e5331d6cbfe4af8032a4b57425b63b2fa9b588 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 1 Oct 2025 11:41:03 +0900 Subject: [PATCH] =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/report/page.tsx | 111 ++++++++ .../components/report/ReportCreateModal.tsx | 228 ++++++++++++++++ .../components/report/ReportListTable.tsx | 250 ++++++++++++++++++ frontend/hooks/useReportList.ts | 63 +++++ frontend/lib/api/reportApi.ts | 119 +++++++++ frontend/types/report.ts | 156 +++++++++++ 6 files changed, 927 insertions(+) create mode 100644 frontend/app/(main)/admin/report/page.tsx create mode 100644 frontend/components/report/ReportCreateModal.tsx create mode 100644 frontend/components/report/ReportListTable.tsx create mode 100644 frontend/hooks/useReportList.ts create mode 100644 frontend/lib/api/reportApi.ts create mode 100644 frontend/types/report.ts diff --git a/frontend/app/(main)/admin/report/page.tsx b/frontend/app/(main)/admin/report/page.tsx new file mode 100644 index 00000000..11c3e89d --- /dev/null +++ b/frontend/app/(main)/admin/report/page.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ReportListTable } from "@/components/report/ReportListTable"; +import { ReportCreateModal } from "@/components/report/ReportCreateModal"; +import { Plus, Search, RotateCcw } from "lucide-react"; +import { useReportList } from "@/hooks/useReportList"; + +export default function ReportManagementPage() { + const [searchText, setSearchText] = useState(""); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + + const { reports, total, page, limit, isLoading, refetch, setPage, handleSearch } = useReportList(); + + const handleSearchClick = () => { + handleSearch(searchText); + }; + + const handleReset = () => { + setSearchText(""); + handleSearch(""); + }; + + const handleCreateSuccess = () => { + setIsCreateModalOpen(false); + refetch(); + }; + + return ( +
+
+ {/* 페이지 제목 */} +
+
+

리포트 관리

+

리포트를 생성하고 관리합니다

+
+ +
+ + {/* 검색 영역 */} + + + + + 검색 + + + +
+ setSearchText(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSearchClick(); + } + }} + className="flex-1" + /> + + +
+
+
+ + {/* 리포트 목록 */} + + + + + 📋 리포트 목록 + (총 {total}건) + + + + + + + +
+ + {/* 리포트 생성 모달 */} + setIsCreateModalOpen(false)} + onSuccess={handleCreateSuccess} + /> +
+ ); +} diff --git a/frontend/components/report/ReportCreateModal.tsx b/frontend/components/report/ReportCreateModal.tsx new file mode 100644 index 00000000..56644632 --- /dev/null +++ b/frontend/components/report/ReportCreateModal.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Loader2 } from "lucide-react"; +import { reportApi } from "@/lib/api/reportApi"; +import { useToast } from "@/hooks/use-toast"; +import { CreateReportRequest, ReportTemplate } from "@/types/report"; + +interface ReportCreateModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +} + +export function ReportCreateModal({ isOpen, onClose, onSuccess }: ReportCreateModalProps) { + const [formData, setFormData] = useState({ + reportNameKor: "", + reportNameEng: "", + templateId: "", + reportType: "BASIC", + description: "", + }); + const [templates, setTemplates] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingTemplates, setIsLoadingTemplates] = useState(false); + const { toast } = useToast(); + + // 템플릿 목록 불러오기 + useEffect(() => { + if (isOpen) { + fetchTemplates(); + } + }, [isOpen]); + + const fetchTemplates = async () => { + setIsLoadingTemplates(true); + try { + const response = await reportApi.getTemplates(); + if (response.success && response.data) { + setTemplates([...response.data.system, ...response.data.custom]); + } + } catch (error: any) { + toast({ + title: "오류", + description: "템플릿 목록을 불러오는데 실패했습니다.", + variant: "destructive", + }); + } finally { + setIsLoadingTemplates(false); + } + }; + + const handleSubmit = async () => { + // 유효성 검증 + if (!formData.reportNameKor.trim()) { + toast({ + title: "입력 오류", + description: "리포트명(한글)을 입력해주세요.", + variant: "destructive", + }); + return; + } + + if (!formData.reportType) { + toast({ + title: "입력 오류", + description: "리포트 타입을 선택해주세요.", + variant: "destructive", + }); + return; + } + + setIsLoading(true); + try { + const response = await reportApi.createReport(formData); + if (response.success) { + toast({ + title: "성공", + description: "리포트가 생성되었습니다.", + }); + handleClose(); + onSuccess(); + } + } catch (error: any) { + toast({ + title: "오류", + description: error.message || "리포트 생성에 실패했습니다.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + const handleClose = () => { + setFormData({ + reportNameKor: "", + reportNameEng: "", + templateId: "", + reportType: "BASIC", + description: "", + }); + onClose(); + }; + + return ( + + + + 새 리포트 생성 + 새로운 리포트를 생성합니다. 필수 항목을 입력해주세요. + + +
+ {/* 리포트명 (한글) */} +
+ + setFormData({ ...formData, reportNameKor: e.target.value })} + /> +
+ + {/* 리포트명 (영문) */} +
+ + setFormData({ ...formData, reportNameEng: e.target.value })} + /> +
+ + {/* 템플릿 선택 */} +
+ + +
+ + {/* 리포트 타입 */} +
+ + +
+ + {/* 설명 */} +
+ +