- 공통 템플릿 타입 수정 (IntegratedDetailTemplate, UniversalListPage) - 페이지(app/[locale]) 타입 호환성 수정 (80개) - 재고/자재 모듈 타입 수정 (StockStatus, ReceivingManagement) - 생산 모듈 타입 수정 (WorkOrders, WorkerScreen, WorkResults) - 주문/출고 모듈 타입 수정 (ShipmentManagement, Orders) - 견적/단가 모듈 타입 수정 (Quotes, Pricing) - 건설 모듈 타입 수정 (49개, 17개 하위 모듈) - HR 모듈 타입 수정 (CardManagement, VacationManagement 등) - 설정 모듈 타입 수정 (PermissionManagement, AccountManagement 등) - 게시판 모듈 타입 수정 (BoardManagement, BoardList 등) - 회계 모듈 타입 수정 (VendorManagement, BadDebtCollection 등) - 기타 모듈 타입 수정 (CEODashboard, clients, vehicle 등) - 유틸/훅/API 타입 수정 (hooks, contexts, lib) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 구조검토 상세 페이지
|
|
* IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20)
|
|
*/
|
|
|
|
import { useState, useCallback, useMemo } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { FileDropzone } from '@/components/ui/file-dropzone';
|
|
import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
|
import { structureReviewConfig } from './structureReviewConfig';
|
|
import { toast } from 'sonner';
|
|
import type { StructureReview } from './types';
|
|
import { STRUCTURE_REVIEW_STATUS_OPTIONS } from './types';
|
|
import { deleteStructureReview } from './actions';
|
|
|
|
// 목업 거래처 목록
|
|
const MOCK_PARTNERS = [
|
|
{ value: '1', label: '거래처명A' },
|
|
{ value: '2', label: '거래처명B' },
|
|
{ value: '3', label: '거래처명C' },
|
|
];
|
|
|
|
// 목업 현장 목록
|
|
const MOCK_SITES = [
|
|
{ value: '1', label: '현장A' },
|
|
{ value: '2', label: '현장B' },
|
|
{ value: '3', label: '현장C' },
|
|
];
|
|
|
|
// 파일 타입 정의
|
|
interface ReviewFile {
|
|
id: string;
|
|
fileName: string;
|
|
fileUrl: string;
|
|
fileSize: number;
|
|
uploadedAt: string;
|
|
}
|
|
|
|
interface StructureReviewDetailFormProps {
|
|
review?: StructureReview;
|
|
mode?: 'view' | 'edit' | 'new';
|
|
}
|
|
|
|
export default function StructureReviewDetailForm({
|
|
review,
|
|
mode = 'view',
|
|
}: StructureReviewDetailFormProps) {
|
|
const router = useRouter();
|
|
const isViewMode = mode === 'view';
|
|
const isNewMode = mode === 'new';
|
|
|
|
// 새 파일 상태
|
|
const [newFiles, setNewFiles] = useState<File[]>([]);
|
|
|
|
// 목데이터: 기존 구조검토 파일
|
|
const MOCK_REVIEW_FILES: ReviewFile[] = review ? [
|
|
{
|
|
id: '1',
|
|
fileName: '구조검토_보고서_최종.pdf',
|
|
fileUrl: '#',
|
|
fileSize: 3145728, // 3MB
|
|
uploadedAt: '2024-12-10T11:00:00',
|
|
},
|
|
{
|
|
id: '2',
|
|
fileName: '구조계산서_v3.xlsx',
|
|
fileUrl: '#',
|
|
fileSize: 1572864, // 1.5MB
|
|
uploadedAt: '2024-12-12T16:45:00',
|
|
},
|
|
] : [];
|
|
|
|
// 파일 목록
|
|
const [reviewFiles, setReviewFiles] = useState<ReviewFile[]>(MOCK_REVIEW_FILES);
|
|
|
|
// 폼 상태
|
|
const [formData, setFormData] = useState({
|
|
reviewNumber: review?.reviewNumber || '',
|
|
partnerId: review?.partnerId || '',
|
|
siteId: review?.siteId || '',
|
|
status: review?.status || 'pending',
|
|
reviewCompany: review?.reviewCompany || '',
|
|
reviewerName: review?.reviewerName || '',
|
|
requestDate: review?.requestDate || '',
|
|
completionDate: review?.completionDate || '',
|
|
});
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
// 입력 핸들러
|
|
const handleInputChange = useCallback(
|
|
(field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setFormData((prev) => ({ ...prev, [field]: e.target.value }));
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleSelectChange = useCallback((field: string) => (value: string) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
}, []);
|
|
|
|
// 파일 추가 핸들러
|
|
const handleFilesSelect = useCallback((files: File[]) => {
|
|
setNewFiles((prev) => [...prev, ...files]);
|
|
}, []);
|
|
|
|
// 새 파일 삭제 핸들러
|
|
const handleNewFileRemove = useCallback((index: number) => {
|
|
setNewFiles((prev) => prev.filter((_, i) => i !== index));
|
|
}, []);
|
|
|
|
// 기존 파일 삭제 핸들러
|
|
const handleExistingFileRemove = useCallback((docId: string | number) => {
|
|
setReviewFiles((prev) => prev.filter((d) => d.id !== String(docId)));
|
|
}, []);
|
|
|
|
// 동적 config (mode에 따라 title 변경)
|
|
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'수정'/'상세' 자동 추가
|
|
const dynamicConfig = useMemo(() => {
|
|
return {
|
|
...structureReviewConfig,
|
|
title: '구조검토',
|
|
};
|
|
}, [mode]);
|
|
|
|
// onSubmit 핸들러 (Promise 반환)
|
|
const handleFormSubmit = useCallback(async () => {
|
|
if (!formData.partnerId) {
|
|
toast.error('거래처를 선택해주세요.');
|
|
return { success: false, error: '거래처를 선택해주세요.' };
|
|
}
|
|
if (!formData.siteId) {
|
|
toast.error('현장을 선택해주세요.');
|
|
return { success: false, error: '현장을 선택해주세요.' };
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
// TODO: API 연동
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
toast.success('저장되었습니다.');
|
|
router.push('/ko/construction/order/structure-review');
|
|
return { success: true };
|
|
} catch {
|
|
toast.error('저장에 실패했습니다.');
|
|
return { success: false, error: '저장에 실패했습니다.' };
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [formData, router]);
|
|
|
|
// onDelete 핸들러 (Promise 반환)
|
|
const handleFormDelete = useCallback(async () => {
|
|
if (!review?.id) return { success: false, error: '삭제할 데이터가 없습니다.' };
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
const result = await deleteStructureReview(review.id);
|
|
if (result.success) {
|
|
toast.success('삭제되었습니다.');
|
|
router.push('/ko/construction/order/structure-review');
|
|
return { success: true };
|
|
} else {
|
|
toast.error(result.error || '삭제에 실패했습니다.');
|
|
return { success: false, error: result.error || '삭제에 실패했습니다.' };
|
|
}
|
|
} catch {
|
|
toast.error('삭제 중 오류가 발생했습니다.');
|
|
return { success: false, error: '삭제 중 오류가 발생했습니다.' };
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [review?.id, router]);
|
|
|
|
// 폼 콘텐츠 렌더링
|
|
const renderFormContent = useCallback(() => (
|
|
<div className="space-y-6">
|
|
{/* 기본 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">기본 정보 *</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{/* 검토번호 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reviewNumber">검토번호</Label>
|
|
<Input
|
|
id="reviewNumber"
|
|
value={formData.reviewNumber}
|
|
onChange={handleInputChange('reviewNumber')}
|
|
placeholder="검토번호"
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 거래처 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="partnerId">거래처</Label>
|
|
<Select
|
|
value={formData.partnerId}
|
|
onValueChange={handleSelectChange('partnerId')}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="거래처 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{MOCK_PARTNERS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 현장명 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="siteId">현장명</Label>
|
|
<Select
|
|
value={formData.siteId}
|
|
onValueChange={handleSelectChange('siteId')}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="현장 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{MOCK_SITES.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 상태 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="status">상태</Label>
|
|
<Select
|
|
value={formData.status}
|
|
onValueChange={handleSelectChange('status')}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="상태 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{STRUCTURE_REVIEW_STATUS_OPTIONS.filter((opt) => opt.value !== 'all').map(
|
|
(option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
)
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 구조검토 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">구조검토 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{/* 구조검토 회사 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reviewCompany">구조검토 회사</Label>
|
|
<Input
|
|
id="reviewCompany"
|
|
value={formData.reviewCompany}
|
|
onChange={handleInputChange('reviewCompany')}
|
|
placeholder="회사명"
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 구조검토자 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reviewerName">구조검토자</Label>
|
|
<Input
|
|
id="reviewerName"
|
|
value={formData.reviewerName}
|
|
onChange={handleInputChange('reviewerName')}
|
|
placeholder="담당자명"
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 구조검토 의뢰일 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="requestDate">구조검토 의뢰일</Label>
|
|
<Input
|
|
id="requestDate"
|
|
type="date"
|
|
value={formData.requestDate}
|
|
onChange={handleInputChange('requestDate')}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 구조검토 완료일 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="completionDate">구조검토 완료일</Label>
|
|
<Input
|
|
id="completionDate"
|
|
type="date"
|
|
value={formData.completionDate || ''}
|
|
onChange={handleInputChange('completionDate')}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 구조검토 파일 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">구조검토 파일</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{!isViewMode && (
|
|
<FileDropzone
|
|
onFilesSelect={handleFilesSelect}
|
|
multiple
|
|
maxSize={10}
|
|
title="클릭하여 파일을 찾거나, 마우스로 파일을 끌어오세요."
|
|
/>
|
|
)}
|
|
<FileList
|
|
files={newFiles.map((file): NewFile => ({ file }))}
|
|
existingFiles={reviewFiles.map((doc): ExistingFile => ({
|
|
id: doc.id,
|
|
name: doc.fileName,
|
|
size: doc.fileSize,
|
|
url: doc.fileUrl,
|
|
}))}
|
|
onRemove={handleNewFileRemove}
|
|
onRemoveExisting={handleExistingFileRemove}
|
|
showRemove={!isViewMode}
|
|
emptyMessage="업로드된 파일이 없습니다"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
), [
|
|
formData,
|
|
isViewMode,
|
|
reviewFiles,
|
|
newFiles,
|
|
handleInputChange,
|
|
handleSelectChange,
|
|
handleFilesSelect,
|
|
handleNewFileRemove,
|
|
handleExistingFileRemove,
|
|
]);
|
|
|
|
return (
|
|
<IntegratedDetailTemplate
|
|
config={dynamicConfig}
|
|
mode={isNewMode ? 'create' : (isViewMode ? 'view' : 'edit')}
|
|
initialData={formData}
|
|
itemId={review?.id}
|
|
isLoading={isSubmitting}
|
|
onSubmit={handleFormSubmit}
|
|
onDelete={handleFormDelete}
|
|
renderView={() => renderFormContent()}
|
|
renderForm={() => renderFormContent()}
|
|
/>
|
|
);
|
|
}
|