Files
sam-react-prod/src/components/business/construction/structure-review/StructureReviewDetailForm.tsx
유병철 a1f4c82cec fix: 프로젝트 전체 TypeScript 타입에러 408개 수정 (tsc --noEmit 0 errors)
- 공통 템플릿 타입 수정 (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>
2026-01-30 10:07:58 +09:00

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()}
/>
);
}