622 lines
26 KiB
TypeScript
622 lines
26 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 세금계산서 상세 보기 컴포넌트
|
|
* PDF 출력 및 첨부파일 다운로드 기능 포함
|
|
*/
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { format } from "date-fns";
|
|
import { ko } from "date-fns/locale";
|
|
import {
|
|
Printer,
|
|
Download,
|
|
FileText,
|
|
Image,
|
|
File,
|
|
Loader2,
|
|
} from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { toast } from "sonner";
|
|
|
|
import {
|
|
getTaxInvoiceById,
|
|
TaxInvoice,
|
|
TaxInvoiceItem,
|
|
TaxInvoiceAttachment,
|
|
} from "@/lib/api/taxInvoice";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface TaxInvoiceDetailProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
invoiceId: string;
|
|
}
|
|
|
|
// 상태 라벨
|
|
const statusLabels: Record<string, string> = {
|
|
draft: "임시저장",
|
|
issued: "발행완료",
|
|
sent: "전송완료",
|
|
cancelled: "취소됨",
|
|
};
|
|
|
|
// 상태 색상
|
|
const statusColors: Record<string, string> = {
|
|
draft: "bg-gray-100 text-gray-800",
|
|
issued: "bg-green-100 text-green-800",
|
|
sent: "bg-blue-100 text-blue-800",
|
|
cancelled: "bg-red-100 text-red-800",
|
|
};
|
|
|
|
export function TaxInvoiceDetail({ open, onClose, invoiceId }: TaxInvoiceDetailProps) {
|
|
const [invoice, setInvoice] = useState<TaxInvoice | null>(null);
|
|
const [items, setItems] = useState<TaxInvoiceItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [pdfLoading, setPdfLoading] = useState(false);
|
|
const printRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 데이터 로드
|
|
useEffect(() => {
|
|
if (open && invoiceId) {
|
|
loadData();
|
|
}
|
|
}, [open, invoiceId]);
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await getTaxInvoiceById(invoiceId);
|
|
if (response.success) {
|
|
setInvoice(response.data.invoice);
|
|
setItems(response.data.items);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error("데이터 로드 실패", { description: error.message });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// 금액 포맷
|
|
const formatAmount = (amount: number) => {
|
|
return new Intl.NumberFormat("ko-KR").format(amount);
|
|
};
|
|
|
|
// 날짜 포맷
|
|
const formatDate = (dateString: string | null) => {
|
|
if (!dateString) return "-";
|
|
try {
|
|
return format(new Date(dateString), "yyyy년 MM월 dd일", { locale: ko });
|
|
} catch {
|
|
return dateString;
|
|
}
|
|
};
|
|
|
|
// 파일 미리보기 URL 생성 (objid 기반) - 이미지용
|
|
const getFilePreviewUrl = (attachment: TaxInvoiceAttachment) => {
|
|
// objid가 숫자형이면 API를 통해 미리보기
|
|
if (attachment.id && !attachment.id.includes("-")) {
|
|
// apiClient의 baseURL 사용
|
|
const baseURL = apiClient.defaults.baseURL || "";
|
|
return `${baseURL}/files/preview/${attachment.id}`;
|
|
}
|
|
return attachment.file_path;
|
|
};
|
|
|
|
// 공통 인쇄용 HTML 생성 함수
|
|
const generatePrintHtml = (autoPrint: boolean = false) => {
|
|
if (!invoice) return "";
|
|
|
|
const invoiceTypeText = invoice.invoice_type === "sales" ? "매출" : "매입";
|
|
const itemsHtml = items.map((item, index) => `
|
|
<tr>
|
|
<td style="text-align:center">${index + 1}</td>
|
|
<td style="text-align:center">${item.item_date?.split("T")[0] || "-"}</td>
|
|
<td>${item.item_name}</td>
|
|
<td>${item.item_spec || "-"}</td>
|
|
<td style="text-align:right">${item.quantity}</td>
|
|
<td style="text-align:right">${formatAmount(item.unit_price)}</td>
|
|
<td style="text-align:right">${formatAmount(item.supply_amount)}</td>
|
|
<td style="text-align:right">${formatAmount(item.tax_amount)}</td>
|
|
</tr>
|
|
`).join("");
|
|
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>세금계산서_${invoice.invoice_number}</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif; padding: 30px; background: #fff; color: #333; }
|
|
.container { max-width: 800px; margin: 0 auto; }
|
|
.header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 3px solid #333; }
|
|
.header h1 { font-size: 28px; margin-bottom: 10px; }
|
|
.header .invoice-number { font-size: 14px; color: #666; }
|
|
.header .status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 12px; margin-top: 10px; }
|
|
.status-draft { background: #f3f4f6; color: #374151; }
|
|
.status-issued { background: #d1fae5; color: #065f46; }
|
|
.status-sent { background: #dbeafe; color: #1e40af; }
|
|
.status-cancelled { background: #fee2e2; color: #991b1b; }
|
|
.parties { display: flex; gap: 20px; margin-bottom: 30px; }
|
|
.party { flex: 1; border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
|
|
.party h3 { font-size: 14px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #eee; }
|
|
.party-row { display: flex; margin-bottom: 6px; font-size: 13px; }
|
|
.party-label { width: 80px; color: #666; }
|
|
.party-value { flex: 1; }
|
|
.items-section { margin-bottom: 30px; }
|
|
.items-section h3 { font-size: 14px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #333; }
|
|
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
th, td { border: 1px solid #ddd; padding: 8px; }
|
|
th { background: #f9fafb; font-weight: 600; }
|
|
.total-section { display: flex; justify-content: flex-end; }
|
|
.total-box { width: 280px; border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
|
|
.total-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 13px; }
|
|
.total-row.grand { font-size: 16px; font-weight: bold; padding-top: 8px; border-top: 1px solid #ddd; margin-top: 8px; }
|
|
.total-row.grand .value { color: #1d4ed8; }
|
|
.remarks { margin-top: 20px; padding: 12px; background: #f9fafb; border-radius: 8px; font-size: 13px; }
|
|
.footer { margin-top: 20px; font-size: 11px; color: #666; display: flex; justify-content: space-between; }
|
|
.attachments { margin-top: 20px; padding: 12px; border: 1px solid #ddd; border-radius: 8px; }
|
|
.attachments h3 { font-size: 14px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #eee; }
|
|
.attachments ul { list-style: none; font-size: 12px; }
|
|
.attachments li { padding: 4px 0; }
|
|
@media print {
|
|
body { padding: 15px; }
|
|
.no-print { display: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>세금계산서 (${invoiceTypeText})</h1>
|
|
<div class="invoice-number">계산서번호: ${invoice.invoice_number}</div>
|
|
<span class="status status-${invoice.invoice_status}">${statusLabels[invoice.invoice_status]}</span>
|
|
</div>
|
|
|
|
<div class="parties">
|
|
<div class="party">
|
|
<h3>공급자</h3>
|
|
<div class="party-row"><span class="party-label">사업자번호</span><span class="party-value">${invoice.supplier_business_no || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">상호</span><span class="party-value">${invoice.supplier_name || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">대표자</span><span class="party-value">${invoice.supplier_ceo_name || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">업태/종목</span><span class="party-value">${invoice.supplier_business_type || "-"} / ${invoice.supplier_business_item || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">주소</span><span class="party-value">${invoice.supplier_address || "-"}</span></div>
|
|
</div>
|
|
<div class="party">
|
|
<h3>공급받는자</h3>
|
|
<div class="party-row"><span class="party-label">사업자번호</span><span class="party-value">${invoice.buyer_business_no || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">상호</span><span class="party-value">${invoice.buyer_name || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">대표자</span><span class="party-value">${invoice.buyer_ceo_name || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">이메일</span><span class="party-value">${invoice.buyer_email || "-"}</span></div>
|
|
<div class="party-row"><span class="party-label">주소</span><span class="party-value">${invoice.buyer_address || "-"}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="items-section">
|
|
<h3>품목 내역</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:40px">No</th>
|
|
<th style="width:80px">일자</th>
|
|
<th>품목명</th>
|
|
<th style="width:70px">규격</th>
|
|
<th style="width:50px">수량</th>
|
|
<th style="width:80px">단가</th>
|
|
<th style="width:90px">공급가액</th>
|
|
<th style="width:70px">세액</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${itemsHtml || '<tr><td colspan="8" style="text-align:center;color:#999">품목 내역이 없습니다.</td></tr>'}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="total-section">
|
|
<div class="total-box">
|
|
<div class="total-row"><span>공급가액</span><span>${formatAmount(invoice.supply_amount)}원</span></div>
|
|
<div class="total-row"><span>세액</span><span>${formatAmount(invoice.tax_amount)}원</span></div>
|
|
<div class="total-row grand"><span>합계금액</span><span class="value">${formatAmount(invoice.total_amount)}원</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
${invoice.remarks ? `<div class="remarks"><strong>비고:</strong> ${invoice.remarks}</div>` : ""}
|
|
|
|
${invoice.attachments && invoice.attachments.length > 0 ? `
|
|
<div class="attachments">
|
|
<h3>첨부파일 (${invoice.attachments.length}개)</h3>
|
|
<ul>
|
|
${invoice.attachments.map(file => `<li>📄 ${file.file_name}</li>`).join("")}
|
|
</ul>
|
|
</div>
|
|
` : ""}
|
|
|
|
<div class="footer">
|
|
<span>작성일: ${formatDate(invoice.invoice_date)}</span>
|
|
${invoice.issue_date ? `<span>발행일: ${formatDate(invoice.issue_date)}</span>` : ""}
|
|
</div>
|
|
</div>
|
|
|
|
${autoPrint ? `<script>window.onload = function() { window.print(); };</script>` : ""}
|
|
</body>
|
|
</html>
|
|
`;
|
|
};
|
|
|
|
// 인쇄
|
|
const handlePrint = () => {
|
|
if (!invoice) return;
|
|
|
|
const printWindow = window.open("", "_blank");
|
|
if (!printWindow) {
|
|
toast.error("팝업이 차단되었습니다. 팝업 차단을 해제해주세요.");
|
|
return;
|
|
}
|
|
|
|
printWindow.document.write(generatePrintHtml(true));
|
|
printWindow.document.close();
|
|
};
|
|
|
|
// PDF 다운로드 (인쇄 다이얼로그 사용)
|
|
const handleDownloadPdf = async () => {
|
|
if (!invoice) return;
|
|
|
|
setPdfLoading(true);
|
|
try {
|
|
const printWindow = window.open("", "_blank");
|
|
if (!printWindow) {
|
|
toast.error("팝업이 차단되었습니다. 팝업 차단을 해제해주세요.");
|
|
return;
|
|
}
|
|
|
|
printWindow.document.write(generatePrintHtml(true));
|
|
printWindow.document.close();
|
|
toast.success("PDF 인쇄 창이 열렸습니다. 'PDF로 저장'을 선택하세요.");
|
|
} catch (error: any) {
|
|
console.error("PDF 생성 오류:", error);
|
|
toast.error("PDF 생성 실패", { description: error.message });
|
|
} finally {
|
|
setPdfLoading(false);
|
|
}
|
|
};
|
|
|
|
// 파일 아이콘
|
|
const getFileIcon = (fileType: string) => {
|
|
if (fileType.startsWith("image/")) return <Image className="h-4 w-4" />;
|
|
if (fileType.includes("pdf")) return <FileText className="h-4 w-4" />;
|
|
return <File className="h-4 w-4" />;
|
|
};
|
|
|
|
// 파일 다운로드 (인증 토큰 포함)
|
|
const handleDownload = async (attachment: TaxInvoiceAttachment) => {
|
|
try {
|
|
// objid가 숫자형이면 API를 통해 다운로드
|
|
if (attachment.id && !attachment.id.includes("-")) {
|
|
const response = await apiClient.get(`/files/download/${attachment.id}`, {
|
|
responseType: "blob",
|
|
});
|
|
|
|
// Blob으로 다운로드
|
|
const blob = new Blob([response.data]);
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = attachment.file_name;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
} else {
|
|
// 직접 경로로 다운로드
|
|
window.open(attachment.file_path, "_blank");
|
|
}
|
|
} catch (error: any) {
|
|
toast.error("파일 다운로드 실패", { description: error.message });
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
<DialogContent className="max-w-[800px]" aria-describedby={undefined}>
|
|
<DialogHeader>
|
|
<DialogTitle>세금계산서 상세</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="flex h-[400px] items-center justify-center">
|
|
<span className="text-muted-foreground">로딩 중...</span>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
if (!invoice) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
<DialogContent className="max-h-[90vh] max-w-[800px] overflow-hidden p-0" aria-describedby={undefined}>
|
|
<DialogHeader className="flex flex-row items-center justify-between border-b px-6 py-4">
|
|
<DialogTitle>세금계산서 상세</DialogTitle>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={handlePrint}>
|
|
<Printer className="mr-1 h-4 w-4" />
|
|
인쇄
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleDownloadPdf}
|
|
disabled={pdfLoading}
|
|
>
|
|
{pdfLoading ? (
|
|
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Download className="mr-1 h-4 w-4" />
|
|
)}
|
|
PDF 다운로드
|
|
</Button>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<ScrollArea className="max-h-[calc(90vh-120px)]">
|
|
<div className="p-6" ref={printRef}>
|
|
<div className="invoice-container">
|
|
{/* 헤더 */}
|
|
<div className="mb-6 text-center">
|
|
<h1 className="mb-2 text-2xl font-bold">
|
|
{invoice.invoice_type === "sales" ? "세금계산서 (매출)" : "세금계산서 (매입)"}
|
|
</h1>
|
|
<p className="text-muted-foreground text-sm">
|
|
계산서번호: {invoice.invoice_number}
|
|
</p>
|
|
<Badge className={statusColors[invoice.invoice_status]}>
|
|
{statusLabels[invoice.invoice_status]}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* 공급자 / 공급받는자 정보 */}
|
|
<div className="mb-6 grid grid-cols-2 gap-6">
|
|
{/* 공급자 */}
|
|
<div className="rounded-lg border p-4">
|
|
<h3 className="mb-3 border-b pb-2 font-semibold">공급자</h3>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">사업자번호</span>
|
|
<span>{invoice.supplier_business_no || "-"}</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">상호</span>
|
|
<span>{invoice.supplier_name || "-"}</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">대표자</span>
|
|
<span>{invoice.supplier_ceo_name || "-"}</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">업태/종목</span>
|
|
<span>
|
|
{invoice.supplier_business_type || "-"} / {invoice.supplier_business_item || "-"}
|
|
</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">주소</span>
|
|
<span className="flex-1">{invoice.supplier_address || "-"}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 공급받는자 */}
|
|
<div className="rounded-lg border p-4">
|
|
<h3 className="mb-3 border-b pb-2 font-semibold">공급받는자</h3>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">사업자번호</span>
|
|
<span>{invoice.buyer_business_no || "-"}</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">상호</span>
|
|
<span>{invoice.buyer_name || "-"}</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">대표자</span>
|
|
<span>{invoice.buyer_ceo_name || "-"}</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">이메일</span>
|
|
<span>{invoice.buyer_email || "-"}</span>
|
|
</div>
|
|
<div className="flex">
|
|
<span className="text-muted-foreground w-24">주소</span>
|
|
<span className="flex-1">{invoice.buyer_address || "-"}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 품목 내역 */}
|
|
<div className="mb-6">
|
|
<h3 className="mb-3 border-b-2 border-gray-800 pb-2 font-semibold">품목 내역</h3>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[50px]">No</TableHead>
|
|
<TableHead className="w-[100px]">일자</TableHead>
|
|
<TableHead>품목명</TableHead>
|
|
<TableHead className="w-[80px]">규격</TableHead>
|
|
<TableHead className="w-[60px] text-right">수량</TableHead>
|
|
<TableHead className="w-[100px] text-right">단가</TableHead>
|
|
<TableHead className="w-[100px] text-right">공급가액</TableHead>
|
|
<TableHead className="w-[80px] text-right">세액</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{items.length > 0 ? (
|
|
items.map((item, index) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell>{index + 1}</TableCell>
|
|
<TableCell>{item.item_date?.split("T")[0] || "-"}</TableCell>
|
|
<TableCell>{item.item_name}</TableCell>
|
|
<TableCell>{item.item_spec || "-"}</TableCell>
|
|
<TableCell className="text-right">{item.quantity}</TableCell>
|
|
<TableCell className="text-right font-mono">
|
|
{formatAmount(item.unit_price)}
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono">
|
|
{formatAmount(item.supply_amount)}
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono">
|
|
{formatAmount(item.tax_amount)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={8} className="text-muted-foreground py-4 text-center">
|
|
품목 내역이 없습니다.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* 합계 */}
|
|
<div className="flex justify-end">
|
|
<div className="w-[300px] space-y-2 rounded-lg border p-4">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">공급가액</span>
|
|
<span className="font-mono">{formatAmount(invoice.supply_amount)}원</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">세액</span>
|
|
<span className="font-mono">{formatAmount(invoice.tax_amount)}원</span>
|
|
</div>
|
|
<Separator />
|
|
<div className="flex justify-between text-lg font-bold">
|
|
<span>합계금액</span>
|
|
<span className="font-mono text-primary">
|
|
{formatAmount(invoice.total_amount)}원
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 비고 */}
|
|
{invoice.remarks && (
|
|
<div className="mt-6">
|
|
<h3 className="mb-2 font-semibold">비고</h3>
|
|
<p className="text-muted-foreground rounded-lg border p-3 text-sm">
|
|
{invoice.remarks}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 날짜 정보 */}
|
|
<div className="text-muted-foreground mt-6 flex justify-between text-xs">
|
|
<span>작성일: {formatDate(invoice.invoice_date)}</span>
|
|
{invoice.issue_date && <span>발행일: {formatDate(invoice.issue_date)}</span>}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 첨부파일 */}
|
|
{invoice.attachments && invoice.attachments.length > 0 && (
|
|
<div className="mt-6">
|
|
<Separator className="mb-4" />
|
|
<h3 className="mb-3 font-semibold">첨부파일 ({invoice.attachments.length}개)</h3>
|
|
|
|
{/* 이미지 미리보기 */}
|
|
{invoice.attachments.some((f) => f.file_type?.startsWith("image/")) && (
|
|
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
|
{invoice.attachments
|
|
.filter((f) => f.file_type?.startsWith("image/"))
|
|
.map((file) => (
|
|
<div
|
|
key={file.id}
|
|
className="group relative aspect-square overflow-hidden rounded-lg border bg-gray-50"
|
|
>
|
|
<img
|
|
src={getFilePreviewUrl(file)}
|
|
alt={file.file_name}
|
|
className="h-full w-full object-cover transition-transform group-hover:scale-105"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = "none";
|
|
}}
|
|
/>
|
|
<div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/60 to-transparent opacity-0 transition-opacity group-hover:opacity-100">
|
|
<div className="w-full p-2">
|
|
<p className="truncate text-xs text-white">{file.file_name}</p>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
className="mt-1 h-7 w-full text-xs"
|
|
onClick={() => handleDownload(file)}
|
|
>
|
|
<Download className="mr-1 h-3 w-3" />
|
|
다운로드
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 기타 파일 목록 */}
|
|
{invoice.attachments.some((f) => !f.file_type?.startsWith("image/")) && (
|
|
<div className="space-y-2">
|
|
{invoice.attachments
|
|
.filter((f) => !f.file_type?.startsWith("image/"))
|
|
.map((file) => (
|
|
<div
|
|
key={file.id}
|
|
className="flex items-center justify-between rounded-lg border p-3"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{getFileIcon(file.file_type)}
|
|
<span className="text-sm">{file.file_name}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDownload(file)}
|
|
>
|
|
<Download className="mr-1 h-4 w-4" />
|
|
다운로드
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|