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 (
+
+ );
+}
diff --git a/frontend/components/report/ReportListTable.tsx b/frontend/components/report/ReportListTable.tsx
new file mode 100644
index 00000000..7c09537f
--- /dev/null
+++ b/frontend/components/report/ReportListTable.tsx
@@ -0,0 +1,250 @@
+"use client";
+
+import { useState } from "react";
+import { ReportMaster } from "@/types/report";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import { Button } from "@/components/ui/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { Pencil, Copy, Trash2, Loader2 } from "lucide-react";
+import { reportApi } from "@/lib/api/reportApi";
+import { useToast } from "@/hooks/use-toast";
+import { useRouter } from "next/navigation";
+import { format } from "date-fns";
+
+interface ReportListTableProps {
+ reports: ReportMaster[];
+ total: number;
+ page: number;
+ limit: number;
+ isLoading: boolean;
+ onPageChange: (page: number) => void;
+ onRefresh: () => void;
+}
+
+export function ReportListTable({
+ reports,
+ total,
+ page,
+ limit,
+ isLoading,
+ onPageChange,
+ onRefresh,
+}: ReportListTableProps) {
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [isCopying, setIsCopying] = useState(false);
+ const { toast } = useToast();
+ const router = useRouter();
+
+ const totalPages = Math.ceil(total / limit);
+
+ // 수정
+ const handleEdit = (reportId: string) => {
+ router.push(`/admin/report/designer/${reportId}`);
+ };
+
+ // 복사
+ const handleCopy = async (reportId: string) => {
+ setIsCopying(true);
+ try {
+ const response = await reportApi.copyReport(reportId);
+ if (response.success) {
+ toast({
+ title: "성공",
+ description: "리포트가 복사되었습니다.",
+ });
+ onRefresh();
+ }
+ } catch (error: any) {
+ toast({
+ title: "오류",
+ description: error.message || "리포트 복사에 실패했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsCopying(false);
+ }
+ };
+
+ // 삭제 확인
+ const handleDeleteClick = (reportId: string) => {
+ setDeleteTarget(reportId);
+ };
+
+ // 삭제 실행
+ const handleDeleteConfirm = async () => {
+ if (!deleteTarget) return;
+
+ setIsDeleting(true);
+ try {
+ const response = await reportApi.deleteReport(deleteTarget);
+ if (response.success) {
+ toast({
+ title: "성공",
+ description: "리포트가 삭제되었습니다.",
+ });
+ setDeleteTarget(null);
+ onRefresh();
+ }
+ } catch (error: any) {
+ toast({
+ title: "오류",
+ description: error.message || "리포트 삭제에 실패했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ // 날짜 포맷
+ const formatDate = (dateString: string | null) => {
+ if (!dateString) return "-";
+ try {
+ return format(new Date(dateString), "yyyy-MM-dd");
+ } catch {
+ return dateString;
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (reports.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+ No
+ 리포트명
+ 작성자
+ 수정일
+ 액션
+
+
+
+ {reports.map((report, index) => {
+ const rowNumber = (page - 1) * limit + index + 1;
+ return (
+
+ {rowNumber}
+
+
+
{report.report_name_kor}
+ {report.report_name_eng && (
+
{report.report_name_eng}
+ )}
+
+
+ {report.created_by || "-"}
+ {formatDate(report.updated_at || report.created_at)}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ {/* 페이지네이션 */}
+ {totalPages > 1 && (
+
+
+
+ {page} / {totalPages}
+
+
+
+ )}
+
+ {/* 삭제 확인 다이얼로그 */}
+ !open && setDeleteTarget(null)}>
+
+
+ 리포트 삭제
+
+ 이 리포트를 삭제하시겠습니까?
+
+ 삭제된 리포트는 복구할 수 없습니다.
+
+
+
+ 취소
+
+ {isDeleting ? (
+ <>
+
+ 삭제 중...
+ >
+ ) : (
+ "삭제"
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/hooks/useReportList.ts b/frontend/hooks/useReportList.ts
new file mode 100644
index 00000000..e266f17d
--- /dev/null
+++ b/frontend/hooks/useReportList.ts
@@ -0,0 +1,63 @@
+import { useState, useEffect, useCallback } from "react";
+import { ReportMaster, GetReportsParams } from "@/types/report";
+import { reportApi } from "@/lib/api/reportApi";
+import { useToast } from "@/hooks/use-toast";
+
+export function useReportList() {
+ const [reports, setReports] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [page, setPage] = useState(1);
+ const [limit] = useState(20);
+ const [isLoading, setIsLoading] = useState(false);
+ const [searchText, setSearchText] = useState("");
+ const { toast } = useToast();
+
+ const fetchReports = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const params: GetReportsParams = {
+ page,
+ limit,
+ searchText,
+ useYn: "Y",
+ sortBy: "created_at",
+ sortOrder: "DESC",
+ };
+
+ const response = await reportApi.getReports(params);
+
+ if (response.success && response.data) {
+ setReports(response.data.items);
+ setTotal(response.data.total);
+ }
+ } catch (error: any) {
+ toast({
+ title: "오류",
+ description: error.message || "리포트 목록을 불러오는데 실패했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }, [page, limit, searchText, toast]);
+
+ useEffect(() => {
+ fetchReports();
+ }, [fetchReports]);
+
+ const handleSearch = useCallback((text: string) => {
+ setSearchText(text);
+ setPage(1);
+ }, []);
+
+ return {
+ reports,
+ total,
+ page,
+ limit,
+ isLoading,
+ refetch: fetchReports,
+ setPage,
+ handleSearch,
+ };
+}
diff --git a/frontend/lib/api/reportApi.ts b/frontend/lib/api/reportApi.ts
new file mode 100644
index 00000000..08d278f8
--- /dev/null
+++ b/frontend/lib/api/reportApi.ts
@@ -0,0 +1,119 @@
+import { apiClient } from "./client";
+import {
+ ReportMaster,
+ ReportDetail,
+ GetReportsParams,
+ GetReportsResponse,
+ CreateReportRequest,
+ UpdateReportRequest,
+ SaveLayoutRequest,
+ GetTemplatesResponse,
+ CreateTemplateRequest,
+ ReportLayout,
+} from "@/types/report";
+
+const BASE_URL = "/admin/reports";
+
+export const reportApi = {
+ // 리포트 목록 조회
+ getReports: async (params: GetReportsParams) => {
+ const response = await apiClient.get<{
+ success: boolean;
+ data: GetReportsResponse;
+ }>(BASE_URL, { params });
+ return response.data;
+ },
+
+ // 리포트 상세 조회
+ getReportById: async (reportId: string) => {
+ const response = await apiClient.get<{
+ success: boolean;
+ data: ReportDetail;
+ }>(`${BASE_URL}/${reportId}`);
+ return response.data;
+ },
+
+ // 리포트 생성
+ createReport: async (data: CreateReportRequest) => {
+ const response = await apiClient.post<{
+ success: boolean;
+ data: { reportId: string };
+ message: string;
+ }>(BASE_URL, data);
+ return response.data;
+ },
+
+ // 리포트 수정
+ updateReport: async (reportId: string, data: UpdateReportRequest) => {
+ const response = await apiClient.put<{
+ success: boolean;
+ message: string;
+ }>(`${BASE_URL}/${reportId}`, data);
+ return response.data;
+ },
+
+ // 리포트 삭제
+ deleteReport: async (reportId: string) => {
+ const response = await apiClient.delete<{
+ success: boolean;
+ message: string;
+ }>(`${BASE_URL}/${reportId}`);
+ return response.data;
+ },
+
+ // 리포트 복사
+ copyReport: async (reportId: string) => {
+ const response = await apiClient.post<{
+ success: boolean;
+ data: { reportId: string };
+ message: string;
+ }>(`${BASE_URL}/${reportId}/copy`);
+ return response.data;
+ },
+
+ // 레이아웃 조회
+ getLayout: async (reportId: string) => {
+ const response = await apiClient.get<{
+ success: boolean;
+ data: ReportLayout;
+ }>(`${BASE_URL}/${reportId}/layout`);
+ return response.data;
+ },
+
+ // 레이아웃 저장
+ saveLayout: async (reportId: string, data: SaveLayoutRequest) => {
+ const response = await apiClient.put<{
+ success: boolean;
+ message: string;
+ }>(`${BASE_URL}/${reportId}/layout`, data);
+ return response.data;
+ },
+
+ // 템플릿 목록 조회
+ getTemplates: async () => {
+ const response = await apiClient.get<{
+ success: boolean;
+ data: GetTemplatesResponse;
+ }>(`${BASE_URL}/templates`);
+ return response.data;
+ },
+
+ // 템플릿 생성
+ createTemplate: async (data: CreateTemplateRequest) => {
+ const response = await apiClient.post<{
+ success: boolean;
+ data: { templateId: string };
+ message: string;
+ }>(`${BASE_URL}/templates`, data);
+ return response.data;
+ },
+
+ // 템플릿 삭제
+ deleteTemplate: async (templateId: string) => {
+ const response = await apiClient.delete<{
+ success: boolean;
+ message: string;
+ }>(`${BASE_URL}/templates/${templateId}`);
+ return response.data;
+ },
+};
diff --git a/frontend/types/report.ts b/frontend/types/report.ts
new file mode 100644
index 00000000..e270b595
--- /dev/null
+++ b/frontend/types/report.ts
@@ -0,0 +1,156 @@
+/**
+ * 리포트 관리 시스템 타입 정의
+ */
+
+// 리포트 템플릿
+export interface ReportTemplate {
+ template_id: string;
+ template_name_kor: string;
+ template_name_eng: string | null;
+ template_type: string;
+ is_system: string;
+ thumbnail_url: string | null;
+ description: string | null;
+ layout_config: string | null;
+ default_queries: string | null;
+ use_yn: string;
+ sort_order: number;
+ created_at: string;
+ created_by: string | null;
+ updated_at: string | null;
+ updated_by: string | null;
+}
+
+// 리포트 마스터
+export interface ReportMaster {
+ report_id: string;
+ report_name_kor: string;
+ report_name_eng: string | null;
+ template_id: string | null;
+ report_type: string;
+ company_code: string | null;
+ description: string | null;
+ use_yn: string;
+ created_at: string;
+ created_by: string | null;
+ updated_at: string | null;
+ updated_by: string | null;
+}
+
+// 리포트 레이아웃
+export interface ReportLayout {
+ layout_id: string;
+ report_id: string;
+ canvas_width: number;
+ canvas_height: number;
+ page_orientation: string;
+ margin_top: number;
+ margin_bottom: number;
+ margin_left: number;
+ margin_right: number;
+ components: ComponentConfig[];
+ created_at: string;
+ created_by: string | null;
+ updated_at: string | null;
+ updated_by: string | null;
+}
+
+// 컴포넌트 설정
+export interface ComponentConfig {
+ id: string;
+ type: string;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ zIndex: number;
+ fontSize?: number;
+ fontFamily?: string;
+ fontWeight?: string;
+ fontColor?: string;
+ backgroundColor?: string;
+ borderWidth?: number;
+ borderColor?: string;
+ borderRadius?: number;
+ textAlign?: string;
+ padding?: number;
+ queryId?: string;
+ fieldName?: string;
+ defaultValue?: string;
+ format?: string;
+ visible?: boolean;
+ printable?: boolean;
+ conditional?: string;
+}
+
+// 리포트 상세
+export interface ReportDetail {
+ report: ReportMaster;
+ layout: ReportLayout | null;
+}
+
+// 리포트 목록 응답
+export interface GetReportsResponse {
+ items: ReportMaster[];
+ total: number;
+ page: number;
+ limit: number;
+}
+
+// 리포트 목록 조회 파라미터
+export interface GetReportsParams {
+ page?: number;
+ limit?: number;
+ searchText?: string;
+ reportType?: string;
+ useYn?: string;
+ sortBy?: string;
+ sortOrder?: "ASC" | "DESC";
+}
+
+// 리포트 생성 요청
+export interface CreateReportRequest {
+ reportNameKor: string;
+ reportNameEng?: string;
+ templateId?: string;
+ reportType: string;
+ description?: string;
+ companyCode?: string;
+}
+
+// 리포트 수정 요청
+export interface UpdateReportRequest {
+ reportNameKor?: string;
+ reportNameEng?: string;
+ reportType?: string;
+ description?: string;
+ useYn?: string;
+}
+
+// 레이아웃 저장 요청
+export interface SaveLayoutRequest {
+ canvasWidth: number;
+ canvasHeight: number;
+ pageOrientation: string;
+ marginTop: number;
+ marginBottom: number;
+ marginLeft: number;
+ marginRight: number;
+ components: ComponentConfig[];
+}
+
+// 템플릿 목록 응답
+export interface GetTemplatesResponse {
+ system: ReportTemplate[];
+ custom: ReportTemplate[];
+}
+
+// 템플릿 생성 요청
+export interface CreateTemplateRequest {
+ templateNameKor: string;
+ templateNameEng?: string;
+ templateType: string;
+ description?: string;
+ layoutConfig?: any;
+ defaultQueries?: any;
+}