660 lines
23 KiB
TypeScript
660 lines
23 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
import { ReportMaster } from "@/types/report";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Copy, Trash2, Edit, Eye, FileText, Calendar, User, Loader2, Pencil, AlignLeft } 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";
|
|
import { getTypeBgClass, getTypeLabel, getTypeIcon } from "@/lib/reportTypeColors";
|
|
|
|
interface ReportListTableProps {
|
|
reports: ReportMaster[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
isLoading: boolean;
|
|
viewMode: "grid" | "list";
|
|
onPageChange: (page: number) => void;
|
|
onRefresh: () => void;
|
|
onViewClick: (report: ReportMaster) => void;
|
|
onCopyClick: (report: ReportMaster) => void;
|
|
}
|
|
|
|
export function ReportListTable({
|
|
reports,
|
|
total,
|
|
page,
|
|
limit,
|
|
isLoading,
|
|
viewMode,
|
|
onPageChange,
|
|
onRefresh,
|
|
onViewClick,
|
|
onCopyClick,
|
|
}: ReportListTableProps) {
|
|
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const { toast } = useToast();
|
|
const router = useRouter();
|
|
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
const handleEdit = (reportId: string) => {
|
|
router.push(`/admin/screenMng/reportList/designer/${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;
|
|
}
|
|
};
|
|
|
|
const formatUpdatedDate = (updatedAt: string | null, createdAt: string | null) => {
|
|
if (!updatedAt) return "-";
|
|
try {
|
|
const updatedStr = format(new Date(updatedAt), "yyyy-MM-dd HH:mm:ss");
|
|
const createdStr = createdAt ? format(new Date(createdAt), "yyyy-MM-dd HH:mm:ss") : null;
|
|
if (createdStr && updatedStr === createdStr) return "-";
|
|
return format(new Date(updatedAt), "yyyy-MM-dd");
|
|
} catch {
|
|
return updatedAt || "-";
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-72 items-center justify-center">
|
|
<Loader2 className="text-muted-foreground h-10 w-10 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (reports.length === 0) {
|
|
return (
|
|
<div className="text-muted-foreground flex h-72 flex-col items-center justify-center gap-3">
|
|
<FileText className="h-12 w-12 opacity-30" />
|
|
<p className="text-base">등록된 리포트가 없습니다.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const handleRename = async (reportId: string, newName: string) => {
|
|
try {
|
|
const response = await reportApi.updateReport(reportId, { reportNameKor: newName });
|
|
if (response.success) {
|
|
toast({ title: "성공", description: "리포트명이 변경되었습니다." });
|
|
onRefresh();
|
|
}
|
|
} catch (error: any) {
|
|
toast({
|
|
title: "오류",
|
|
description: error.message || "리포트명 변경에 실패했습니다.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDescriptionChange = async (reportId: string, newDesc: string) => {
|
|
try {
|
|
const response = await reportApi.updateReport(reportId, { description: newDesc });
|
|
if (response.success) {
|
|
toast({ title: "성공", description: "설명이 변경되었습니다." });
|
|
onRefresh();
|
|
}
|
|
} catch (error: any) {
|
|
toast({
|
|
title: "오류",
|
|
description: error.message || "설명 변경에 실패했습니다.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{viewMode === "grid" ? (
|
|
<GridView
|
|
reports={reports}
|
|
page={page}
|
|
limit={limit}
|
|
onEdit={handleEdit}
|
|
onView={onViewClick}
|
|
onCopyClick={onCopyClick}
|
|
onDeleteClick={setDeleteTarget}
|
|
onRename={handleRename}
|
|
onDescriptionChange={handleDescriptionChange}
|
|
formatDate={formatDate}
|
|
formatUpdatedDate={formatUpdatedDate}
|
|
/>
|
|
) : (
|
|
<ListView
|
|
reports={reports}
|
|
page={page}
|
|
limit={limit}
|
|
onEdit={handleEdit}
|
|
onView={onViewClick}
|
|
onCopyClick={onCopyClick}
|
|
onDeleteClick={setDeleteTarget}
|
|
onRename={handleRename}
|
|
onDescriptionChange={handleDescriptionChange}
|
|
formatDate={formatDate}
|
|
formatUpdatedDate={formatUpdatedDate}
|
|
/>
|
|
)}
|
|
|
|
{/* 페이지네이션 */}
|
|
<div className="flex items-center justify-between border-t border-gray-200 bg-gray-50 px-7 py-4">
|
|
<span className="text-base text-gray-500">
|
|
총 <span className="font-semibold text-gray-900">{total}건</span>의 리포트
|
|
</span>
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center gap-1.5">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(page - 1)}
|
|
disabled={page === 1}
|
|
className="h-9 px-4 text-sm"
|
|
>
|
|
이전
|
|
</Button>
|
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
|
|
<Button
|
|
key={p}
|
|
variant={p === page ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => onPageChange(p)}
|
|
className={`h-9 w-9 p-0 text-sm ${
|
|
p === page ? "bg-blue-600 text-white hover:bg-blue-700" : ""
|
|
}`}
|
|
>
|
|
{p}
|
|
</Button>
|
|
))}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(page + 1)}
|
|
disabled={page === totalPages}
|
|
className="h-9 px-4 text-sm"
|
|
>
|
|
다음
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 삭제 확인 다이얼로그 */}
|
|
<AlertDialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle className="text-lg">리포트 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-base">
|
|
이 리포트를 삭제하시겠습니까?
|
|
<br />
|
|
삭제된 리포트는 복구할 수 없습니다.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={isDeleting} className="text-base">취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleDeleteConfirm}
|
|
disabled={isDeleting}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 text-base"
|
|
>
|
|
{isDeleting ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
삭제 중...
|
|
</>
|
|
) : (
|
|
"삭제"
|
|
)}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface ViewProps {
|
|
reports: ReportMaster[];
|
|
page: number;
|
|
limit: number;
|
|
onEdit: (id: string) => void;
|
|
onView: (report: ReportMaster) => void;
|
|
onCopyClick: (report: ReportMaster) => void;
|
|
onDeleteClick: (id: string) => void;
|
|
onRename: (reportId: string, newName: string) => Promise<void>;
|
|
onDescriptionChange: (reportId: string, newDesc: string) => Promise<void>;
|
|
formatDate: (d: string | null) => string;
|
|
formatUpdatedDate: (updatedAt: string | null, createdAt: string | null) => string;
|
|
}
|
|
|
|
function InlineReportName({
|
|
reportId,
|
|
name,
|
|
onNavigate,
|
|
onRename,
|
|
}: {
|
|
reportId: string;
|
|
name: string;
|
|
onNavigate: () => void;
|
|
onRename: (reportId: string, newName: string) => Promise<void>;
|
|
}) {
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [editValue, setEditValue] = useState(name);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (isEditing) {
|
|
inputRef.current?.focus();
|
|
inputRef.current?.select();
|
|
}
|
|
}, [isEditing]);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
const trimmed = editValue.trim();
|
|
if (!trimmed || trimmed === name) {
|
|
setIsEditing(false);
|
|
setEditValue(name);
|
|
return;
|
|
}
|
|
setIsSaving(true);
|
|
try {
|
|
await onRename(reportId, trimmed);
|
|
} finally {
|
|
setIsSaving(false);
|
|
setIsEditing(false);
|
|
}
|
|
}, [editValue, name, reportId, onRename]);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
setIsEditing(false);
|
|
setEditValue(name);
|
|
}, [name]);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
handleSave();
|
|
} else if (e.key === "Escape") {
|
|
handleCancel();
|
|
}
|
|
},
|
|
[handleSave, handleCancel],
|
|
);
|
|
|
|
if (isEditing) {
|
|
return (
|
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
<Input
|
|
ref={inputRef}
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
onBlur={handleSave}
|
|
disabled={isSaving}
|
|
className="h-7 text-sm font-medium"
|
|
/>
|
|
{isSaving && <Loader2 className="h-4 w-4 shrink-0 animate-spin text-gray-400" />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="group/name flex min-w-0 items-center gap-1.5">
|
|
<button
|
|
onClick={onNavigate}
|
|
className="cursor-pointer truncate text-left text-base font-medium text-gray-900 hover:text-blue-600 hover:underline"
|
|
>
|
|
{name}
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsEditing(true);
|
|
}}
|
|
className="shrink-0 rounded p-0.5 opacity-0 transition-opacity hover:bg-gray-100 group-hover/name:opacity-100"
|
|
title="리포트명 수정"
|
|
>
|
|
<Pencil className="h-3.5 w-3.5 text-gray-400" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InlineDescription({
|
|
reportId,
|
|
description,
|
|
onSave,
|
|
}: {
|
|
reportId: string;
|
|
description: string | null;
|
|
onSave: (reportId: string, newDesc: string) => Promise<void>;
|
|
}) {
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [editValue, setEditValue] = useState(description || "");
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (isEditing) {
|
|
inputRef.current?.focus();
|
|
}
|
|
}, [isEditing]);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
const trimmed = editValue.trim();
|
|
if (trimmed === (description || "")) {
|
|
setIsEditing(false);
|
|
setEditValue(description || "");
|
|
return;
|
|
}
|
|
setIsSaving(true);
|
|
try {
|
|
await onSave(reportId, trimmed);
|
|
} finally {
|
|
setIsSaving(false);
|
|
setIsEditing(false);
|
|
}
|
|
}, [editValue, description, reportId, onSave]);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
setIsEditing(false);
|
|
setEditValue(description || "");
|
|
}, [description]);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
handleSave();
|
|
} else if (e.key === "Escape") {
|
|
handleCancel();
|
|
}
|
|
},
|
|
[handleSave, handleCancel],
|
|
);
|
|
|
|
if (isEditing) {
|
|
return (
|
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
<Input
|
|
ref={inputRef}
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
onBlur={handleSave}
|
|
disabled={isSaving}
|
|
placeholder="설명 입력"
|
|
className="h-6 text-xs"
|
|
/>
|
|
{isSaving && <Loader2 className="h-4 w-4 shrink-0 animate-spin text-gray-400" />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="group/desc flex min-w-0 items-center gap-1.5">
|
|
<span
|
|
className="cursor-pointer truncate text-gray-500 hover:text-gray-700"
|
|
onClick={() => setIsEditing(true)}
|
|
title={description || "클릭하여 설명 입력"}
|
|
>
|
|
{description || <span className="italic text-gray-300">Inline Description</span>}
|
|
</span>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsEditing(true);
|
|
}}
|
|
className="shrink-0 rounded p-0.5 opacity-0 transition-opacity hover:bg-gray-100 group-hover/desc:opacity-100"
|
|
title="설명 수정"
|
|
>
|
|
<Pencil className="h-3 w-3 text-gray-400" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ListView({ reports, page, limit, onEdit, onView, onCopyClick, onDeleteClick, onRename, onDescriptionChange, formatDate, formatUpdatedDate }: ViewProps) {
|
|
return (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full table-fixed">
|
|
<colgroup>
|
|
<col className="w-[16px]" />
|
|
<col className="w-[46px]" />
|
|
<col style={{ width: "26%" }} />
|
|
<col style={{ width: "18%" }} />
|
|
<col className="w-[120px]" />
|
|
<col className="w-[110px]" />
|
|
<col className="w-[110px]" />
|
|
<col className="w-[110px]" />
|
|
<col className="w-[340px]" />
|
|
<col className="w-[16px]" />
|
|
</colgroup>
|
|
<thead>
|
|
<tr className="border-b border-gray-200 bg-gray-50">
|
|
<th />
|
|
<th className="px-2 py-3.5 text-center text-sm font-semibold text-gray-500">NO</th>
|
|
<th className="py-3.5 text-center text-sm font-semibold text-gray-500">리포트명</th>
|
|
<th className="py-3.5 pl-20 pr-3 text-left text-sm font-semibold text-gray-500">설명</th>
|
|
<th className="px2 py-3.5 text-left text-sm font-semibold text-gray-500">카테고리</th>
|
|
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-500">작성자</th>
|
|
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-500">생성일</th>
|
|
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-500">수정일</th>
|
|
<th className="px-4 py-3.5 text-center text-sm font-semibold text-gray-500">관리</th>
|
|
<th />
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{reports.map((report, index) => {
|
|
const rowNumber = (page - 1) * limit + index + 1;
|
|
return (
|
|
<tr key={report.report_id} className="transition-colors hover:bg-blue-50/70">
|
|
<td />
|
|
<td className="px-2 py-4 text-center text-sm font-medium text-gray-400">
|
|
{rowNumber}
|
|
</td>
|
|
<td className="pl-1 py-4 pl-10 pr-2">
|
|
<div className="flex min-w-0 items-center gap-2.5">
|
|
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-blue-600">
|
|
<FileText className="h-3 w-3 text-white" />
|
|
</div>
|
|
<InlineReportName
|
|
reportId={report.report_id}
|
|
name={report.report_name_kor}
|
|
onNavigate={() => onEdit(report.report_id)}
|
|
onRename={onRename}
|
|
/>
|
|
</div>
|
|
</td>
|
|
<td className="min-w-0 overflow-hidden px-3 py-4">
|
|
<InlineDescription
|
|
reportId={report.report_id}
|
|
description={report.description}
|
|
onSave={onDescriptionChange}
|
|
/>
|
|
</td>
|
|
<td className="px-3 py-4">
|
|
{report.report_type && (() => {
|
|
const TypeIcon = getTypeIcon(report.report_type);
|
|
return (
|
|
<Badge className={`gap-1.5 whitespace-nowrap text-sm leading-tight hover:bg-transparent ${getTypeBgClass(report.report_type)}`}>
|
|
<TypeIcon className="h-3.5 w-3.5" strokeWidth={2.2} />
|
|
{getTypeLabel(report.report_type)}
|
|
</Badge>
|
|
);
|
|
})()}
|
|
</td>
|
|
<td className="px-3 py-4 text-sm text-gray-600">
|
|
{report.created_by || "-"}
|
|
</td>
|
|
<td className="px-3 py-4 text-sm text-gray-500">
|
|
{formatDate(report.created_at)}
|
|
</td>
|
|
<td className="px-3 py-4 text-sm text-gray-500">
|
|
{formatUpdatedDate(report.updated_at, report.created_at)}
|
|
</td>
|
|
<td className="px-4 py-4">
|
|
<div className="flex items-center justify-center gap-2">
|
|
<Button variant="outline" size="sm" onClick={() => onView(report)} className="h-9 gap-1.5 px-3 text-sm font-medium">
|
|
<Eye className="h-4 w-4" strokeWidth={2.2} />
|
|
미리보기
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onEdit(report.report_id)} className="h-9 gap-1.5 px-3 text-sm font-medium">
|
|
<Edit className="h-4 w-4" strokeWidth={2.2} />
|
|
수정
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onCopyClick(report)} className="h-9 gap-1.5 px-3 text-sm font-medium">
|
|
<Copy className="h-4 w-4" strokeWidth={2.2} />
|
|
복사
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onDeleteClick(report.report_id)} className="h-9 gap-1.5 px-3 text-sm font-medium text-red-600 hover:border-red-200 hover:bg-red-50 hover:text-red-700">
|
|
<Trash2 className="h-4 w-4" strokeWidth={2.2} />
|
|
삭제
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
<td />
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GridView({ reports, page, limit, onEdit, onView, onCopyClick, onDeleteClick, onRename, onDescriptionChange, formatDate, formatUpdatedDate }: ViewProps) {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-3 px-4 py-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{reports.map((report, index) => {
|
|
const rowNumber = (page - 1) * limit + index + 1;
|
|
|
|
return (
|
|
<div
|
|
key={report.report_id}
|
|
className="group rounded-lg border border-gray-200 bg-white px-4 py-3 transition-all hover:border-blue-300 hover:bg-blue-50/50 hover:shadow-md"
|
|
>
|
|
<div className="mb-2 flex items-center justify-between gap-2">
|
|
<div className="ml-1 flex min-w-0 items-center gap-2">
|
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-gradient-to-br from-blue-500 to-blue-600">
|
|
<FileText className="h-4 w-4 text-white" />
|
|
</div>
|
|
<InlineReportName
|
|
reportId={report.report_id}
|
|
name={report.report_name_kor}
|
|
onNavigate={() => onEdit(report.report_id)}
|
|
onRename={onRename}
|
|
/>
|
|
{report.report_type && (() => {
|
|
const TypeIcon = getTypeIcon(report.report_type);
|
|
return (
|
|
<Badge className={`-ml-1.5 shrink-0 gap-1 text-[11px] leading-tight hover:bg-transparent ${getTypeBgClass(report.report_type)}`}>
|
|
<TypeIcon className="h-2.5 w-2.5" strokeWidth={2.5} />
|
|
{getTypeLabel(report.report_type)}
|
|
</Badge>
|
|
);
|
|
})()}
|
|
</div>
|
|
<span className="shrink-0 text-[11px] font-medium text-gray-400">#{rowNumber}</span>
|
|
</div>
|
|
|
|
<div className="ml-3 space-y-1 text-xs">
|
|
<div className="flex items-center text-gray-600">
|
|
<User className="mr-1.5 h-3.5 w-3.5 shrink-0" />
|
|
<span className="w-[38px] shrink-0 font-medium text-gray-700">작성자</span>
|
|
<span>{report.created_by || "-"}</span>
|
|
</div>
|
|
<div className="flex items-center text-gray-600">
|
|
<Calendar className="mr-1.5 h-3.5 w-3.5 shrink-0" />
|
|
<span className="w-[38px] shrink-0 font-medium text-gray-700">생성일</span>
|
|
<span>{formatDate(report.created_at)}</span>
|
|
</div>
|
|
<div className="flex items-center text-gray-600">
|
|
<Calendar className="mr-1.5 h-3.5 w-3.5 shrink-0" />
|
|
<span className="w-[38px] shrink-0 font-medium text-gray-700">수정일</span>
|
|
<span>{formatUpdatedDate(report.updated_at, report.created_at)}</span>
|
|
</div>
|
|
<div className="flex items-center text-gray-600">
|
|
<AlignLeft className="mr-1.5 h-3.5 w-3.5 shrink-0" />
|
|
<span className="w-[38px] shrink-0 font-medium text-gray-700">설명</span>
|
|
<div className="min-w-0 flex-1">
|
|
<InlineDescription
|
|
reportId={report.report_id}
|
|
description={report.description}
|
|
onSave={onDescriptionChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-2 flex gap-1.5 border-t border-gray-100 pt-2">
|
|
<Button variant="outline" size="sm" onClick={() => onView(report)} className="h-7 flex-1 gap-1 px-0 text-[11px]">
|
|
<Eye className="h-3 w-3" />
|
|
미리보기
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onEdit(report.report_id)} className="h-7 flex-1 gap-1 px-0 text-[11px]">
|
|
<Edit className="h-3 w-3" />
|
|
수정
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onCopyClick(report)} className="h-7 flex-1 gap-1 px-0 text-[11px]">
|
|
<Copy className="h-3 w-3" />
|
|
복사
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => onDeleteClick(report.report_id)} className="h-7 flex-1 gap-1 px-0 text-[11px] text-red-600 hover:border-red-200 hover:bg-red-50 hover:text-red-700">
|
|
<Trash2 className="h-3 w-3" />
|
|
삭제
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|