- 입찰관리: 목록/상세/수정 페이지 및 목업 데이터 - 계약관리: 목록/상세/수정 페이지 구현 - 주문관리: 수주/발주 목록 및 상세 페이지 구현 - 견적 상세 폼: 섹션별 분리 및 hooks/utils 리팩토링 - 품목관리, 카테고리관리, 단가관리 기능 추가 - 현장설명회/협력업체 폼 개선 - 프린트 유틸리티 공통화 (print-utils.ts) - 문서 모달 공통 컴포넌트 정리 - IntegratedListTemplateV2, StatCards 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
712 lines
24 KiB
TypeScript
712 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useRef } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { FileText, Upload, X, Eye, Download } 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/components/ui/alert-dialog';
|
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
|
import { toast } from 'sonner';
|
|
import type { ContractDetail, ContractFormData, ContractAttachment, ContractStatus } from './types';
|
|
import {
|
|
CONTRACT_STATUS_LABELS,
|
|
VAT_TYPE_OPTIONS,
|
|
getEmptyContractFormData,
|
|
contractDetailToFormData,
|
|
} from './types';
|
|
import { updateContract, deleteContract } from './actions';
|
|
import { downloadFileById } from '@/lib/utils/fileDownload';
|
|
import { ContractDocumentModal } from './modals/ContractDocumentModal';
|
|
import {
|
|
ElectronicApprovalModal,
|
|
type ElectronicApproval,
|
|
getEmptyElectronicApproval,
|
|
} from '../common';
|
|
|
|
// 금액 포맷팅
|
|
function formatAmount(amount: number): string {
|
|
return new Intl.NumberFormat('ko-KR').format(amount);
|
|
}
|
|
|
|
// 파일 사이즈 포맷팅
|
|
function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|
|
|
|
interface ContractDetailFormProps {
|
|
mode: 'view' | 'edit';
|
|
contractId: string;
|
|
initialData?: ContractDetail;
|
|
}
|
|
|
|
export default function ContractDetailForm({
|
|
mode,
|
|
contractId,
|
|
initialData,
|
|
}: ContractDetailFormProps) {
|
|
const router = useRouter();
|
|
const isViewMode = mode === 'view';
|
|
const isEditMode = mode === 'edit';
|
|
|
|
// 폼 데이터
|
|
const [formData, setFormData] = useState<ContractFormData>(
|
|
initialData ? contractDetailToFormData(initialData) : getEmptyContractFormData()
|
|
);
|
|
|
|
// 기존 첨부파일 (서버에서 가져온 파일)
|
|
const [existingAttachments, setExistingAttachments] = useState<ContractAttachment[]>(
|
|
initialData?.attachments || []
|
|
);
|
|
|
|
// 새로 추가된 파일
|
|
const [newAttachments, setNewAttachments] = useState<File[]>([]);
|
|
|
|
// 기존 계약서 파일 삭제 여부
|
|
const [isContractFileDeleted, setIsContractFileDeleted] = useState(false);
|
|
|
|
// 로딩 상태
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// 다이얼로그 상태
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
|
|
|
// 모달 상태
|
|
const [showDocumentModal, setShowDocumentModal] = useState(false);
|
|
const [showApprovalModal, setShowApprovalModal] = useState(false);
|
|
|
|
// 전자결재 데이터
|
|
const [approvalData, setApprovalData] = useState<ElectronicApproval>(
|
|
getEmptyElectronicApproval()
|
|
);
|
|
|
|
// 파일 업로드 ref
|
|
const contractFileInputRef = useRef<HTMLInputElement>(null);
|
|
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// 드래그 상태
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
// 네비게이션 핸들러
|
|
const handleBack = useCallback(() => {
|
|
router.push('/ko/juil/project/contract');
|
|
}, [router]);
|
|
|
|
const handleEdit = useCallback(() => {
|
|
router.push(`/ko/juil/project/contract/${contractId}/edit`);
|
|
}, [router, contractId]);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
router.push(`/ko/juil/project/contract/${contractId}`);
|
|
}, [router, contractId]);
|
|
|
|
// 폼 필드 변경
|
|
const handleFieldChange = useCallback(
|
|
(field: keyof ContractFormData, value: string | number) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
},
|
|
[]
|
|
);
|
|
|
|
// 저장 핸들러
|
|
const handleSave = useCallback(() => {
|
|
setShowSaveDialog(true);
|
|
}, []);
|
|
|
|
const handleConfirmSave = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const result = await updateContract(contractId, formData);
|
|
if (result.success) {
|
|
toast.success('수정이 완료되었습니다.');
|
|
setShowSaveDialog(false);
|
|
router.push(`/ko/juil/project/contract/${contractId}`);
|
|
router.refresh();
|
|
} else {
|
|
toast.error(result.error || '저장에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : '저장에 실패했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [router, contractId, formData]);
|
|
|
|
// 삭제 핸들러
|
|
const handleDelete = useCallback(() => {
|
|
setShowDeleteDialog(true);
|
|
}, []);
|
|
|
|
const handleConfirmDelete = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const result = await deleteContract(contractId);
|
|
if (result.success) {
|
|
toast.success('계약이 삭제되었습니다.');
|
|
setShowDeleteDialog(false);
|
|
router.push('/ko/juil/project/contract');
|
|
router.refresh();
|
|
} else {
|
|
toast.error(result.error || '삭제에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : '삭제에 실패했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [router, contractId]);
|
|
|
|
// 계약서 파일 선택
|
|
const handleContractFileSelect = useCallback(() => {
|
|
contractFileInputRef.current?.click();
|
|
}, []);
|
|
|
|
const handleContractFileChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
if (file.type !== 'application/pdf') {
|
|
toast.error('PDF 파일만 업로드 가능합니다.');
|
|
return;
|
|
}
|
|
setFormData((prev) => ({ ...prev, contractFile: file }));
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
// 첨부 파일 드래그 앤 드롭
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(true);
|
|
}, []);
|
|
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(false);
|
|
}, []);
|
|
|
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(false);
|
|
|
|
const files = Array.from(e.dataTransfer.files);
|
|
setNewAttachments((prev) => [...prev, ...files]);
|
|
}, []);
|
|
|
|
const handleAttachmentSelect = useCallback(() => {
|
|
attachmentInputRef.current?.click();
|
|
}, []);
|
|
|
|
const handleAttachmentChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = Array.from(e.target.files || []);
|
|
setNewAttachments((prev) => [...prev, ...files]);
|
|
},
|
|
[]
|
|
);
|
|
|
|
// 기존 첨부파일 삭제
|
|
const handleRemoveExistingAttachment = useCallback((id: string) => {
|
|
setExistingAttachments((prev) => prev.filter((att) => att.id !== id));
|
|
}, []);
|
|
|
|
// 새 첨부파일 삭제
|
|
const handleRemoveNewAttachment = useCallback((index: number) => {
|
|
setNewAttachments((prev) => prev.filter((_, i) => i !== index));
|
|
}, []);
|
|
|
|
// 기존 계약서 파일 삭제
|
|
const handleRemoveContractFile = useCallback(() => {
|
|
setIsContractFileDeleted(true);
|
|
setFormData((prev) => ({ ...prev, contractFile: null }));
|
|
}, []);
|
|
|
|
// 계약서 보기 핸들러
|
|
const handleViewDocument = useCallback(() => {
|
|
setShowDocumentModal(true);
|
|
}, []);
|
|
|
|
// 파일 다운로드 핸들러
|
|
const handleFileDownload = useCallback(async (fileId: string, fileName?: string) => {
|
|
try {
|
|
await downloadFileById(parseInt(fileId), fileName);
|
|
} catch (error) {
|
|
console.error('[ContractDetailForm] 다운로드 실패:', error);
|
|
toast.error('파일 다운로드에 실패했습니다.');
|
|
}
|
|
}, []);
|
|
|
|
// 전자결재 핸들러
|
|
const handleApproval = useCallback(() => {
|
|
setShowApprovalModal(true);
|
|
}, []);
|
|
|
|
// 전자결재 저장
|
|
const handleApprovalSave = useCallback((approval: ElectronicApproval) => {
|
|
setApprovalData(approval);
|
|
setShowApprovalModal(false);
|
|
toast.success('전자결재 정보가 저장되었습니다.');
|
|
}, []);
|
|
|
|
// 헤더 액션 버튼
|
|
const headerActions = isViewMode ? (
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" onClick={handleViewDocument}>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
계약서 보기
|
|
</Button>
|
|
<Button variant="outline" onClick={handleApproval}>
|
|
전자결재
|
|
</Button>
|
|
<Button onClick={handleEdit}>수정</Button>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" onClick={handleCancel}>
|
|
취소
|
|
</Button>
|
|
<Button variant="destructive" onClick={handleDelete}>
|
|
삭제
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={isLoading}>
|
|
저장
|
|
</Button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<PageLayout>
|
|
<PageHeader
|
|
title="계약 상세"
|
|
description="계약 정보를 관리합니다"
|
|
icon={FileText}
|
|
onBack={handleBack}
|
|
actions={headerActions}
|
|
/>
|
|
|
|
<div className="space-y-6">
|
|
{/* 계약 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">계약 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{/* 계약번호 */}
|
|
<div className="space-y-2">
|
|
<Label>계약번호</Label>
|
|
<Input
|
|
value={formData.contractCode}
|
|
onChange={(e) => handleFieldChange('contractCode', e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 계약담당자 */}
|
|
<div className="space-y-2">
|
|
<Label>계약담당자</Label>
|
|
<Input
|
|
value={formData.contractManagerName}
|
|
onChange={(e) => handleFieldChange('contractManagerName', e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 거래처명 */}
|
|
<div className="space-y-2">
|
|
<Label>거래처명</Label>
|
|
<Input
|
|
value={formData.partnerName}
|
|
onChange={(e) => handleFieldChange('partnerName', e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 현장명 */}
|
|
<div className="space-y-2">
|
|
<Label>현장명</Label>
|
|
<Input
|
|
value={formData.projectName}
|
|
onChange={(e) => handleFieldChange('projectName', e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 계약일자 */}
|
|
<div className="space-y-2">
|
|
<Label>계약일자</Label>
|
|
<Input
|
|
type="date"
|
|
value={formData.contractDate}
|
|
onChange={(e) => handleFieldChange('contractDate', e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 개소 */}
|
|
<div className="space-y-2">
|
|
<Label>개소</Label>
|
|
<Input
|
|
type="number"
|
|
value={formData.totalLocations}
|
|
onChange={(e) => handleFieldChange('totalLocations', parseInt(e.target.value) || 0)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 계약기간 */}
|
|
<div className="space-y-2">
|
|
<Label>계약기간</Label>
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
type="date"
|
|
value={formData.contractStartDate}
|
|
onChange={(e) => handleFieldChange('contractStartDate', e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
<span>~</span>
|
|
<Input
|
|
type="date"
|
|
value={formData.contractEndDate}
|
|
onChange={(e) => handleFieldChange('contractEndDate', e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 부가세 */}
|
|
<div className="space-y-2">
|
|
<Label>부가세</Label>
|
|
<Select
|
|
value={formData.vatType}
|
|
onValueChange={(value) => handleFieldChange('vatType', value)}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{VAT_TYPE_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 계약금액 */}
|
|
<div className="space-y-2">
|
|
<Label>계약금액</Label>
|
|
<Input
|
|
type="text"
|
|
value={formatAmount(formData.contractAmount)}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/[^0-9]/g, '');
|
|
handleFieldChange('contractAmount', parseInt(value) || 0);
|
|
}}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 상태 */}
|
|
<div className="space-y-2">
|
|
<Label>상태</Label>
|
|
<RadioGroup
|
|
value={formData.status}
|
|
onValueChange={(value) => handleFieldChange('status', value as ContractStatus)}
|
|
disabled={isViewMode}
|
|
className="flex gap-4"
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="pending" id="pending" />
|
|
<Label htmlFor="pending" className="font-normal cursor-pointer">
|
|
{CONTRACT_STATUS_LABELS.pending}
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="completed" id="completed" />
|
|
<Label htmlFor="completed" className="font-normal cursor-pointer">
|
|
{CONTRACT_STATUS_LABELS.completed}
|
|
</Label>
|
|
</div>
|
|
</RadioGroup>
|
|
</div>
|
|
|
|
{/* 비고 */}
|
|
<div className="space-y-2 md:col-span-2">
|
|
<Label>비고</Label>
|
|
<Textarea
|
|
value={formData.remarks}
|
|
onChange={(e) => handleFieldChange('remarks', e.target.value)}
|
|
disabled={isViewMode}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 계약서 관리 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">계약서 관리</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{/* 파일 선택 버튼 (수정 모드에서만) */}
|
|
{isEditMode && (
|
|
<Button variant="outline" onClick={handleContractFileSelect}>
|
|
찾기
|
|
</Button>
|
|
)}
|
|
|
|
{/* 새로 선택한 파일 */}
|
|
{formData.contractFile && (
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
|
<span className="text-sm font-medium">{formData.contractFile.name}</span>
|
|
<span className="text-xs text-blue-600">(새 파일)</span>
|
|
</div>
|
|
{isEditMode && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setFormData((prev) => ({ ...prev, contractFile: null }))}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 기존 계약서 파일 */}
|
|
{!isContractFileDeleted && initialData?.contractFile && !formData.contractFile && (
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
|
<span className="text-sm font-medium">{initialData.contractFile.fileName}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleFileDownload(initialData.contractFile!.id, initialData.contractFile!.fileName)}
|
|
>
|
|
<Download className="h-4 w-4 mr-1" />
|
|
다운로드
|
|
</Button>
|
|
{isEditMode && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleRemoveContractFile}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 파일 없음 안내 */}
|
|
{!formData.contractFile && (isContractFileDeleted || !initialData?.contractFile) && (
|
|
<span className="text-sm text-muted-foreground">PDF 파일만 업로드 가능합니다</span>
|
|
)}
|
|
|
|
<input
|
|
ref={contractFileInputRef}
|
|
type="file"
|
|
accept=".pdf"
|
|
className="hidden"
|
|
onChange={handleContractFileChange}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 계약 첨부 문서 관리 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">계약 첨부 문서 관리</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* 드래그 앤 드롭 영역 */}
|
|
{isEditMode && (
|
|
<div
|
|
className={`border-2 border-dashed rounded-lg p-8 text-center mb-4 transition-colors cursor-pointer ${
|
|
isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'
|
|
}`}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={handleAttachmentSelect}
|
|
>
|
|
<Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
|
<p className="text-muted-foreground">
|
|
클릭하여 파일을 찾거나, 마우스로 파일을 끌어오세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 파일 목록 */}
|
|
<div className="space-y-2">
|
|
{/* 기존 첨부파일 */}
|
|
{existingAttachments.map((att) => (
|
|
<div
|
|
key={att.id}
|
|
className="flex items-center justify-between p-3 bg-muted rounded-lg"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
|
<div>
|
|
<p className="text-sm font-medium">{att.fileName}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{formatFileSize(att.fileSize)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleFileDownload(att.id, att.fileName)}
|
|
>
|
|
<Download className="h-4 w-4 mr-1" />
|
|
다운로드
|
|
</Button>
|
|
{isEditMode && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleRemoveExistingAttachment(att.id)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* 새로 추가된 파일 */}
|
|
{newAttachments.map((file, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between p-3 bg-muted rounded-lg"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
|
<div>
|
|
<p className="text-sm font-medium">{file.name}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{formatFileSize(file.size)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleRemoveNewAttachment(index)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<input
|
|
ref={attachmentInputRef}
|
|
type="file"
|
|
multiple
|
|
className="hidden"
|
|
onChange={handleAttachmentChange}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 저장 확인 다이얼로그 */}
|
|
<AlertDialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>저장 확인</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
변경사항을 저장하시겠습니까?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleConfirmSave} disabled={isLoading}>
|
|
저장
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* 삭제 확인 다이얼로그 */}
|
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>계약 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
이 계약을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleConfirmDelete}
|
|
disabled={isLoading}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* 계약서 보기 모달 */}
|
|
{initialData && (
|
|
<ContractDocumentModal
|
|
open={showDocumentModal}
|
|
onOpenChange={setShowDocumentModal}
|
|
contract={initialData}
|
|
/>
|
|
)}
|
|
|
|
{/* 전자결재 모달 */}
|
|
<ElectronicApprovalModal
|
|
isOpen={showApprovalModal}
|
|
onClose={() => setShowApprovalModal(false)}
|
|
approval={approvalData}
|
|
onSave={handleApprovalSave}
|
|
/>
|
|
</PageLayout>
|
|
);
|
|
} |