728 lines
28 KiB
TypeScript
728 lines
28 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 세금계산서 작성/수정 폼
|
|
* 파일 첨부 기능 포함
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { format } from "date-fns";
|
|
import {
|
|
Plus,
|
|
Trash2,
|
|
Upload,
|
|
X,
|
|
FileText,
|
|
Image,
|
|
File,
|
|
Paperclip,
|
|
} from "lucide-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 {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { toast } from "sonner";
|
|
|
|
import {
|
|
createTaxInvoice,
|
|
updateTaxInvoice,
|
|
getTaxInvoiceById,
|
|
TaxInvoice,
|
|
TaxInvoiceAttachment,
|
|
CreateTaxInvoiceDto,
|
|
CreateTaxInvoiceItemDto,
|
|
CostType,
|
|
costTypeLabels,
|
|
} from "@/lib/api/taxInvoice";
|
|
import { uploadFiles } from "@/lib/api/file";
|
|
|
|
interface TaxInvoiceFormProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSave: () => void;
|
|
invoice?: TaxInvoice | null;
|
|
}
|
|
|
|
// 품목 초기값
|
|
const emptyItem: CreateTaxInvoiceItemDto = {
|
|
item_date: format(new Date(), "yyyy-MM-dd"),
|
|
item_name: "",
|
|
item_spec: "",
|
|
quantity: 1,
|
|
unit_price: 0,
|
|
supply_amount: 0,
|
|
tax_amount: 0,
|
|
remarks: "",
|
|
};
|
|
|
|
export function TaxInvoiceForm({ open, onClose, onSave, invoice }: TaxInvoiceFormProps) {
|
|
// 폼 상태
|
|
const [formData, setFormData] = useState<CreateTaxInvoiceDto>({
|
|
invoice_type: "sales",
|
|
invoice_date: format(new Date(), "yyyy-MM-dd"),
|
|
supply_amount: 0,
|
|
tax_amount: 0,
|
|
total_amount: 0,
|
|
items: [{ ...emptyItem }],
|
|
});
|
|
|
|
// 첨부파일 상태
|
|
const [attachments, setAttachments] = useState<TaxInvoiceAttachment[]>([]);
|
|
const [uploading, setUploading] = useState(false);
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
const [activeTab, setActiveTab] = useState("basic");
|
|
|
|
// 수정 모드일 때 데이터 로드
|
|
useEffect(() => {
|
|
if (invoice) {
|
|
loadInvoiceData(invoice.id);
|
|
} else {
|
|
// 새 세금계산서
|
|
setFormData({
|
|
invoice_type: "sales",
|
|
invoice_date: format(new Date(), "yyyy-MM-dd"),
|
|
supply_amount: 0,
|
|
tax_amount: 0,
|
|
total_amount: 0,
|
|
items: [{ ...emptyItem }],
|
|
});
|
|
setAttachments([]);
|
|
}
|
|
}, [invoice]);
|
|
|
|
// 세금계산서 데이터 로드
|
|
const loadInvoiceData = async (id: string) => {
|
|
try {
|
|
const response = await getTaxInvoiceById(id);
|
|
if (response.success) {
|
|
const { invoice: inv, items } = response.data;
|
|
setFormData({
|
|
invoice_type: inv.invoice_type,
|
|
invoice_date: inv.invoice_date?.split("T")[0] || "",
|
|
supplier_business_no: inv.supplier_business_no,
|
|
supplier_name: inv.supplier_name,
|
|
supplier_ceo_name: inv.supplier_ceo_name,
|
|
supplier_address: inv.supplier_address,
|
|
supplier_business_type: inv.supplier_business_type,
|
|
supplier_business_item: inv.supplier_business_item,
|
|
buyer_business_no: inv.buyer_business_no,
|
|
buyer_name: inv.buyer_name,
|
|
buyer_ceo_name: inv.buyer_ceo_name,
|
|
buyer_address: inv.buyer_address,
|
|
buyer_email: inv.buyer_email,
|
|
supply_amount: inv.supply_amount,
|
|
tax_amount: inv.tax_amount,
|
|
total_amount: inv.total_amount,
|
|
remarks: inv.remarks,
|
|
cost_type: inv.cost_type || undefined,
|
|
items:
|
|
items.length > 0
|
|
? items.map((item) => ({
|
|
item_date: item.item_date?.split("T")[0] || "",
|
|
item_name: item.item_name,
|
|
item_spec: item.item_spec,
|
|
quantity: item.quantity,
|
|
unit_price: item.unit_price,
|
|
supply_amount: item.supply_amount,
|
|
tax_amount: item.tax_amount,
|
|
remarks: item.remarks,
|
|
}))
|
|
: [{ ...emptyItem }],
|
|
});
|
|
setAttachments(inv.attachments || []);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error("데이터 로드 실패", { description: error.message });
|
|
}
|
|
};
|
|
|
|
// 필드 변경
|
|
const handleChange = (field: keyof CreateTaxInvoiceDto, value: any) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
// 품목 변경
|
|
const handleItemChange = (index: number, field: keyof CreateTaxInvoiceItemDto, value: any) => {
|
|
setFormData((prev) => {
|
|
const items = [...(prev.items || [])];
|
|
items[index] = { ...items[index], [field]: value };
|
|
|
|
// 공급가액 자동 계산
|
|
if (field === "quantity" || field === "unit_price") {
|
|
const qty = field === "quantity" ? value : items[index].quantity;
|
|
const price = field === "unit_price" ? value : items[index].unit_price;
|
|
items[index].supply_amount = qty * price;
|
|
items[index].tax_amount = Math.round(items[index].supply_amount * 0.1);
|
|
}
|
|
|
|
// 총액 재계산
|
|
const totalSupply = items.reduce((sum, item) => sum + (item.supply_amount || 0), 0);
|
|
const totalTax = items.reduce((sum, item) => sum + (item.tax_amount || 0), 0);
|
|
|
|
return {
|
|
...prev,
|
|
items,
|
|
supply_amount: totalSupply,
|
|
tax_amount: totalTax,
|
|
total_amount: totalSupply + totalTax,
|
|
};
|
|
});
|
|
};
|
|
|
|
// 품목 추가
|
|
const handleAddItem = () => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
items: [...(prev.items || []), { ...emptyItem }],
|
|
}));
|
|
};
|
|
|
|
// 품목 삭제
|
|
const handleRemoveItem = (index: number) => {
|
|
setFormData((prev) => {
|
|
const items = (prev.items || []).filter((_, i) => i !== index);
|
|
const totalSupply = items.reduce((sum, item) => sum + (item.supply_amount || 0), 0);
|
|
const totalTax = items.reduce((sum, item) => sum + (item.tax_amount || 0), 0);
|
|
|
|
return {
|
|
...prev,
|
|
items: items.length > 0 ? items : [{ ...emptyItem }],
|
|
supply_amount: totalSupply,
|
|
tax_amount: totalTax,
|
|
total_amount: totalSupply + totalTax,
|
|
};
|
|
});
|
|
};
|
|
|
|
// 파일 업로드 (화면 관리 파일 업로드 컴포넌트와 동일한 방식 사용)
|
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = e.target.files;
|
|
if (!files || files.length === 0) return;
|
|
|
|
setUploading(true);
|
|
try {
|
|
// 화면 관리 파일 업로드 컴포넌트와 동일한 uploadFiles 함수 사용
|
|
const response = await uploadFiles({
|
|
files: files,
|
|
tableName: "tax_invoice",
|
|
fieldName: "attachments",
|
|
recordId: invoice?.id,
|
|
docType: "tax-invoice",
|
|
docTypeName: "세금계산서",
|
|
});
|
|
|
|
if (response.success && response.files?.length > 0) {
|
|
const newAttachments: TaxInvoiceAttachment[] = response.files.map((uploadedFile) => ({
|
|
id: uploadedFile.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
file_name: uploadedFile.name,
|
|
file_path: uploadedFile.serverPath || "",
|
|
file_size: uploadedFile.size,
|
|
file_type: uploadedFile.type,
|
|
uploaded_at: uploadedFile.uploadedAt || new Date().toISOString(),
|
|
uploaded_by: "",
|
|
}));
|
|
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
toast.success(`${response.files.length}개 파일 업로드 완료`);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error("파일 업로드 실패", { description: error.message });
|
|
} finally {
|
|
setUploading(false);
|
|
// input 초기화
|
|
e.target.value = "";
|
|
}
|
|
};
|
|
|
|
// 첨부파일 삭제
|
|
const handleRemoveAttachment = (id: string) => {
|
|
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
};
|
|
|
|
// 파일 아이콘
|
|
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 formatFileSize = (bytes: number) => {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
};
|
|
|
|
// 저장
|
|
const handleSave = async () => {
|
|
// 유효성 검사
|
|
if (!formData.invoice_date) {
|
|
toast.error("작성일자를 입력해주세요.");
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
const dataToSave = {
|
|
...formData,
|
|
attachments,
|
|
};
|
|
|
|
let response;
|
|
if (invoice) {
|
|
response = await updateTaxInvoice(invoice.id, dataToSave);
|
|
} else {
|
|
response = await createTaxInvoice(dataToSave);
|
|
}
|
|
|
|
if (response.success) {
|
|
toast.success(response.message || "저장되었습니다.");
|
|
onSave();
|
|
}
|
|
} catch (error: any) {
|
|
toast.error("저장 실패", { description: error.message });
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// 금액 포맷
|
|
const formatAmount = (amount: number) => {
|
|
return new Intl.NumberFormat("ko-KR").format(amount);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
<DialogContent className="max-h-[90vh] max-w-[900px] overflow-hidden p-0">
|
|
<DialogHeader className="border-b px-6 py-4">
|
|
<DialogTitle>{invoice ? "세금계산서 수정" : "세금계산서 작성"}</DialogTitle>
|
|
<DialogDescription>세금계산서 정보를 입력해주세요.</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<ScrollArea className="max-h-[calc(90vh-180px)]">
|
|
<div className="space-y-4 p-6">
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList className="grid w-full grid-cols-4">
|
|
<TabsTrigger value="basic">기본정보</TabsTrigger>
|
|
<TabsTrigger value="supplier">공급자</TabsTrigger>
|
|
<TabsTrigger value="buyer">공급받는자</TabsTrigger>
|
|
<TabsTrigger value="attachments">
|
|
첨부파일
|
|
{attachments.length > 0 && (
|
|
<Badge variant="secondary" className="ml-2">
|
|
{attachments.length}
|
|
</Badge>
|
|
)}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 기본정보 탭 */}
|
|
<TabsContent value="basic" className="space-y-4">
|
|
<div className="grid grid-cols-4 gap-4">
|
|
<div>
|
|
<Label className="text-xs">유형 *</Label>
|
|
<Select
|
|
value={formData.invoice_type}
|
|
onValueChange={(v) => handleChange("invoice_type", v)}
|
|
>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sales">매출</SelectItem>
|
|
<SelectItem value="purchase">매입</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">비용 유형</Label>
|
|
<Select
|
|
value={formData.cost_type || "none"}
|
|
onValueChange={(v) => handleChange("cost_type", v === "none" ? undefined : v)}
|
|
>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue placeholder="선택 안함" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">선택 안함</SelectItem>
|
|
{Object.entries(costTypeLabels).map(([value, label]) => (
|
|
<SelectItem key={value} value={value}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">작성일자 *</Label>
|
|
<Input
|
|
type="date"
|
|
value={formData.invoice_date}
|
|
onChange={(e) => handleChange("invoice_date", e.target.value)}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">비고</Label>
|
|
<Input
|
|
value={formData.remarks || ""}
|
|
onChange={(e) => handleChange("remarks", e.target.value)}
|
|
className="h-9"
|
|
placeholder="비고 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 품목 테이블 */}
|
|
<Card>
|
|
<CardHeader className="py-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-sm">품목 내역</CardTitle>
|
|
<Button variant="outline" size="sm" onClick={handleAddItem}>
|
|
<Plus className="mr-1 h-4 w-4" />
|
|
품목 추가
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[100px]">일자</TableHead>
|
|
<TableHead>품목명</TableHead>
|
|
<TableHead className="w-[80px]">규격</TableHead>
|
|
<TableHead className="w-[70px] text-right">수량</TableHead>
|
|
<TableHead className="w-[100px] text-right">단가</TableHead>
|
|
<TableHead className="w-[100px] text-right">공급가액</TableHead>
|
|
<TableHead className="w-[80px] text-right">세액</TableHead>
|
|
<TableHead className="w-[50px]"></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{(formData.items || []).map((item, index) => (
|
|
<TableRow key={index}>
|
|
<TableCell className="p-1">
|
|
<Input
|
|
type="date"
|
|
value={item.item_date || ""}
|
|
onChange={(e) =>
|
|
handleItemChange(index, "item_date", e.target.value)
|
|
}
|
|
className="h-8 text-xs"
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="p-1">
|
|
<Input
|
|
value={item.item_name || ""}
|
|
onChange={(e) =>
|
|
handleItemChange(index, "item_name", e.target.value)
|
|
}
|
|
className="h-8 text-xs"
|
|
placeholder="품목명"
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="p-1">
|
|
<Input
|
|
value={item.item_spec || ""}
|
|
onChange={(e) =>
|
|
handleItemChange(index, "item_spec", e.target.value)
|
|
}
|
|
className="h-8 text-xs"
|
|
placeholder="규격"
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="p-1">
|
|
<Input
|
|
type="number"
|
|
value={item.quantity || 0}
|
|
onChange={(e) =>
|
|
handleItemChange(index, "quantity", parseFloat(e.target.value) || 0)
|
|
}
|
|
className="h-8 text-right text-xs"
|
|
min={0}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="p-1">
|
|
<Input
|
|
type="number"
|
|
value={item.unit_price || 0}
|
|
onChange={(e) =>
|
|
handleItemChange(
|
|
index,
|
|
"unit_price",
|
|
parseFloat(e.target.value) || 0
|
|
)
|
|
}
|
|
className="h-8 text-right text-xs"
|
|
min={0}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="p-1 text-right font-mono text-xs">
|
|
{formatAmount(item.supply_amount || 0)}
|
|
</TableCell>
|
|
<TableCell className="p-1 text-right font-mono text-xs">
|
|
{formatAmount(item.tax_amount || 0)}
|
|
</TableCell>
|
|
<TableCell className="p-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7"
|
|
onClick={() => handleRemoveItem(index)}
|
|
disabled={(formData.items?.length || 0) <= 1}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 합계 */}
|
|
<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(formData.supply_amount || 0)}원</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">세액</span>
|
|
<span className="font-mono">{formatAmount(formData.tax_amount || 0)}원</span>
|
|
</div>
|
|
<div className="flex justify-between border-t pt-2 text-lg font-bold">
|
|
<span>합계</span>
|
|
<span className="font-mono text-primary">
|
|
{formatAmount(formData.total_amount || 0)}원
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 공급자 탭 */}
|
|
<TabsContent value="supplier" className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">사업자등록번호</Label>
|
|
<Input
|
|
value={formData.supplier_business_no || ""}
|
|
onChange={(e) => handleChange("supplier_business_no", e.target.value)}
|
|
className="h-9"
|
|
placeholder="000-00-00000"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">상호</Label>
|
|
<Input
|
|
value={formData.supplier_name || ""}
|
|
onChange={(e) => handleChange("supplier_name", e.target.value)}
|
|
className="h-9"
|
|
placeholder="상호명"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">대표자명</Label>
|
|
<Input
|
|
value={formData.supplier_ceo_name || ""}
|
|
onChange={(e) => handleChange("supplier_ceo_name", e.target.value)}
|
|
className="h-9"
|
|
placeholder="대표자명"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">업태</Label>
|
|
<Input
|
|
value={formData.supplier_business_type || ""}
|
|
onChange={(e) => handleChange("supplier_business_type", e.target.value)}
|
|
className="h-9"
|
|
placeholder="업태"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">종목</Label>
|
|
<Input
|
|
value={formData.supplier_business_item || ""}
|
|
onChange={(e) => handleChange("supplier_business_item", e.target.value)}
|
|
className="h-9"
|
|
placeholder="종목"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<Label className="text-xs">주소</Label>
|
|
<Input
|
|
value={formData.supplier_address || ""}
|
|
onChange={(e) => handleChange("supplier_address", e.target.value)}
|
|
className="h-9"
|
|
placeholder="주소"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 공급받는자 탭 */}
|
|
<TabsContent value="buyer" className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">사업자등록번호</Label>
|
|
<Input
|
|
value={formData.buyer_business_no || ""}
|
|
onChange={(e) => handleChange("buyer_business_no", e.target.value)}
|
|
className="h-9"
|
|
placeholder="000-00-00000"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">상호</Label>
|
|
<Input
|
|
value={formData.buyer_name || ""}
|
|
onChange={(e) => handleChange("buyer_name", e.target.value)}
|
|
className="h-9"
|
|
placeholder="상호명"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">대표자명</Label>
|
|
<Input
|
|
value={formData.buyer_ceo_name || ""}
|
|
onChange={(e) => handleChange("buyer_ceo_name", e.target.value)}
|
|
className="h-9"
|
|
placeholder="대표자명"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">이메일</Label>
|
|
<Input
|
|
type="email"
|
|
value={formData.buyer_email || ""}
|
|
onChange={(e) => handleChange("buyer_email", e.target.value)}
|
|
className="h-9"
|
|
placeholder="email@example.com"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<Label className="text-xs">주소</Label>
|
|
<Input
|
|
value={formData.buyer_address || ""}
|
|
onChange={(e) => handleChange("buyer_address", e.target.value)}
|
|
className="h-9"
|
|
placeholder="주소"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 첨부파일 탭 */}
|
|
<TabsContent value="attachments" className="space-y-4">
|
|
{/* 업로드 영역 */}
|
|
<div className="rounded-lg border-2 border-dashed p-6 text-center">
|
|
<input
|
|
type="file"
|
|
id="file-upload"
|
|
multiple
|
|
onChange={handleFileUpload}
|
|
className="hidden"
|
|
accept=".pdf,.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx"
|
|
/>
|
|
<label
|
|
htmlFor="file-upload"
|
|
className="flex cursor-pointer flex-col items-center gap-2"
|
|
>
|
|
<Upload className="text-muted-foreground h-8 w-8" />
|
|
<span className="text-muted-foreground text-sm">
|
|
{uploading ? "업로드 중..." : "파일을 선택하거나 드래그하세요"}
|
|
</span>
|
|
<span className="text-muted-foreground text-xs">
|
|
PDF, 이미지, 문서 파일 (최대 10MB)
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* 첨부파일 목록 */}
|
|
{attachments.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">첨부된 파일 ({attachments.length}개)</Label>
|
|
<div className="space-y-2">
|
|
{attachments.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)}
|
|
<div>
|
|
<p className="text-sm font-medium">{file.file_name}</p>
|
|
<p className="text-muted-foreground text-xs">
|
|
{formatFileSize(file.file_size)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => handleRemoveAttachment(file.id)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{attachments.length === 0 && (
|
|
<div className="text-muted-foreground py-8 text-center text-sm">
|
|
<Paperclip className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
|
첨부된 파일이 없습니다.
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
<DialogFooter className="border-t px-6 py-4">
|
|
<Button variant="outline" onClick={onClose} disabled={saving}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={saving}>
|
|
{saving ? "저장 중..." : "저장"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|