Files
sam-react-prod/src/components/material/ReceivingManagement/InspectionCreate.tsx
유병철 0db6302652 refactor(WEB): 코드 품질 개선 및 불필요 코드 제거
- 미사용 import/변수/console.log 대량 정리 (100+개 파일)
- ItemMasterContext 간소화 (미사용 로직 제거)
- IntegratedListTemplateV2 / UniversalListPage 개선
- 결재 컴포넌트(ApprovalBox, DraftBox, ReferenceBox) 정리
- HR 컴포넌트(급여/휴가/부서) 코드 간소화
- globals.css 스타일 정리 및 개선
- AuthenticatedLayout 개선
- middleware CSP 정리
- proxy route 불필요 로깅 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:55:11 +09:00

364 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
/**
* 수입검사 등록 (IQC) 페이지
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
*
* - 검사 대상 선택
* - 검사 정보 입력 (검사일, 검사자*, LOT번호)
* - 검사 항목 테이블 (겉모양, 두께, 폭, 길이)
* - 종합 의견
*/
import { useState, useCallback, useMemo, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { getTodayString } from '@/utils/date';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { materialInspectionCreateConfig } from './inspectionConfig';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { DatePicker } from '@/components/ui/date-picker';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { getReceivings } from './actions';
import type { InspectionCheckItem, ReceivingItem } from './types';
import { SuccessDialog } from './SuccessDialog';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
// LOT 번호 생성 함수 (YYMMDD-NN 형식)
function generateLotNo(): string {
const now = new Date();
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const random = String(Math.floor(Math.random() * 100)).padStart(2, '0');
return `${yy}${mm}${dd}-${random}`;
}
// 기본 검사 항목
const defaultInspectionItems: InspectionCheckItem[] = [
{ id: '1', name: '겉모양', specification: '외관 이상 없음', method: '육안', judgment: '', remark: '' },
{ id: '2', name: '두께', specification: 't 1.0', method: '계측', judgment: '', remark: '' },
{ id: '3', name: '폭', specification: 'W 1,000mm', method: '계측', judgment: '', remark: '' },
{ id: '4', name: '길이', specification: 'L 2,000mm', method: '계측', judgment: '', remark: '' },
];
interface Props {
id?: string; // 특정 발주건으로 바로 진입하는 경우
}
export function InspectionCreate({ id }: Props) {
const router = useRouter();
// 검사 대상 목록 (API에서 조회)
const [inspectionTargets, setInspectionTargets] = useState<ReceivingItem[]>([]);
const [isLoadingTargets, setIsLoadingTargets] = useState(true);
// 선택된 검사 대상
const [selectedTargetId, setSelectedTargetId] = useState<string>(id || '');
// 검사 정보
const [inspectionDate, setInspectionDate] = useState(() => getTodayString());
const [inspector, setInspector] = useState('');
const [lotNo, setLotNo] = useState(() => generateLotNo());
// 검사 항목
const [inspectionItems, setInspectionItems] = useState<InspectionCheckItem[]>(
defaultInspectionItems.map((item) => ({ ...item }))
);
// 종합 의견
const [opinion, setOpinion] = useState('');
// 유효성 검사 에러
const [validationErrors, setValidationErrors] = useState<string[]>([]);
// 성공 다이얼로그
const [showSuccess, setShowSuccess] = useState(false);
// 검사 대상 목록 로드 (inspection_pending 상태인 입고 건)
useEffect(() => {
const loadTargets = async () => {
setIsLoadingTargets(true);
try {
const result = await getReceivings({ status: 'inspection_pending', perPage: 100 });
if (result.success) {
setInspectionTargets(result.data);
// 초기 선택: id가 있으면 해당 건, 없으면 첫 번째 항목
if (!selectedTargetId && result.data.length > 0) {
setSelectedTargetId(result.data[0].id);
}
}
} catch (err) {
if (isNextRedirectError(err)) throw err;
console.error('[InspectionCreate] loadTargets error:', err);
} finally {
setIsLoadingTargets(false);
}
};
loadTargets();
}, []);
// 선택된 대상 정보 (나중에 검사 저장 시 사용)
const _selectedTarget = useMemo(() => {
return inspectionTargets.find((t) => t.id === selectedTargetId);
}, [inspectionTargets, selectedTargetId]);
// 대상 선택 핸들러
const handleTargetSelect = useCallback((targetId: string) => {
setSelectedTargetId(targetId);
setValidationErrors([]);
}, []);
// 판정 변경 핸들러
const handleJudgmentChange = useCallback((itemId: string, judgment: '적' | '부적') => {
setInspectionItems((prev) =>
prev.map((item) => (item.id === itemId ? { ...item, judgment } : item))
);
setValidationErrors([]);
}, []);
// 비고 변경 핸들러
const handleRemarkChange = useCallback((itemId: string, remark: string) => {
setInspectionItems((prev) =>
prev.map((item) => (item.id === itemId ? { ...item, remark } : item))
);
}, []);
// 유효성 검사
const validateForm = useCallback((): boolean => {
const errors: string[] = [];
// 필수 필드: 검사자
if (!inspector.trim()) {
errors.push('검사자는 필수 입력 항목입니다.');
}
// 검사 항목 판정 확인
inspectionItems.forEach((item, index) => {
if (!item.judgment) {
errors.push(`${index + 1}. ${item.name}: 판정을 선택해주세요.`);
}
});
setValidationErrors(errors);
return errors.length === 0;
}, [inspector, inspectionItems]);
// 검사 저장
const handleSubmit = useCallback(async () => {
if (!validateForm()) {
return;
}
// TODO: API 호출
setShowSuccess(true);
}, [validateForm, selectedTargetId, inspectionDate, inspector, lotNo, inspectionItems, opinion]);
// 취소 - 목록으로
const handleCancel = useCallback(() => {
router.push('/ko/material/receiving-management');
}, [router]);
// 성공 다이얼로그 닫기
const handleSuccessClose = useCallback(() => {
setShowSuccess(false);
router.push('/ko/material/receiving-management');
}, [router]);
// ===== 폼 콘텐츠 렌더링 =====
const renderFormContent = useCallback(() => (
<>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* 좌측: 검사 대상 선택 */}
<div className="lg:col-span-1 space-y-2">
<Label className="text-sm font-medium"> </Label>
<div className="space-y-2 border rounded-lg p-2 bg-white min-h-[200px]">
{isLoadingTargets ? (
<ContentSkeleton type="list" rows={3} />
) : inspectionTargets.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm">
.
</div>
) : (
inspectionTargets.map((target) => (
<div
key={target.id}
onClick={() => handleTargetSelect(target.id)}
className={`p-3 rounded-lg cursor-pointer border transition-colors ${
selectedTargetId === target.id
? 'bg-blue-50 border-blue-300'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}
>
<p className="font-medium text-sm">{target.orderNo}</p>
<p className="text-xs text-muted-foreground">
{target.supplier} · {target.orderQty ?? target.receivingQty ?? '-'} {target.unit}
</p>
</div>
))
)}
</div>
</div>
{/* 우측: 검사 정보 및 항목 */}
<div className="lg:col-span-3 space-y-6">
{/* Validation 에러 표시 */}
{validationErrors.length > 0 && (
<Alert className="bg-red-50 border-red-200">
<AlertDescription className="text-red-900">
<div className="flex items-start gap-2">
<span className="text-lg"></span>
<div className="flex-1">
<strong className="block mb-2">
({validationErrors.length} )
</strong>
<ul className="space-y-1 text-sm">
{validationErrors.map((error, index) => (
<li key={index} className="flex items-start gap-1">
<span></span>
<span>{error}</span>
</li>
))}
</ul>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* 검사 정보 */}
<div className="space-y-4 bg-white p-4 rounded-lg border">
<h3 className="font-medium"> </h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="text-sm text-muted-foreground"></Label>
<DatePicker
value={inspectionDate}
onChange={(date) => setInspectionDate(date)}
/>
</div>
<div className="space-y-2">
<Label className="text-sm text-muted-foreground">
<span className="text-red-500">*</span>
</Label>
<Input
value={inspector}
onChange={(e) => {
setInspector(e.target.value);
setValidationErrors([]);
}}
placeholder="검사자명 입력"
/>
</div>
<div className="space-y-2">
<Label className="text-sm text-muted-foreground">LOT번호 (YYMMDD-##)</Label>
<Input value={lotNo} onChange={(e) => setLotNo(e.target.value)} />
</div>
</div>
</div>
{/* 검사 항목 */}
<div className="space-y-4 bg-white p-4 rounded-lg border">
<h3 className="font-medium"> </h3>
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-center font-medium w-[100px]"></th>
<th className="px-3 py-2 text-left font-medium w-[120px]"></th>
</tr>
</thead>
<tbody>
{inspectionItems.map((item) => (
<tr key={item.id} className="border-t">
<td className="px-3 py-2">{item.name}</td>
<td className="px-3 py-2 text-muted-foreground text-xs">
{item.specification}
</td>
<td className="px-3 py-2">{item.method}</td>
<td className="px-3 py-2">
<Select
value={item.judgment || ''}
onValueChange={(value) =>
handleJudgmentChange(item.id, value as '적' | '부적')
}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="적"></SelectItem>
<SelectItem value="부적"></SelectItem>
</SelectContent>
</Select>
</td>
<td className="px-3 py-2">
<Input
value={item.remark || ''}
onChange={(e) => handleRemarkChange(item.id, e.target.value)}
placeholder="비고"
className="h-8"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 종합 의견 */}
<div className="space-y-2 bg-white p-4 rounded-lg border">
<Label className="text-sm font-medium"> </Label>
<Textarea
value={opinion}
onChange={(e) => setOpinion(e.target.value)}
placeholder="검사 관련 특이사항 입력"
rows={3}
/>
</div>
</div>
</div>
{/* 성공 다이얼로그 */}
<SuccessDialog
open={showSuccess}
type="inspection"
lotNo={lotNo}
onClose={handleSuccessClose}
/>
</>
), [
isLoadingTargets, inspectionTargets, selectedTargetId, inspectionDate,
inspector, lotNo, inspectionItems, opinion, validationErrors, showSuccess,
handleTargetSelect, handleJudgmentChange, handleRemarkChange, handleSuccessClose,
]);
return (
<IntegratedDetailTemplate
config={materialInspectionCreateConfig}
mode="create"
isLoading={isLoadingTargets}
isSubmitting={false}
onBack={handleCancel}
onCancel={handleCancel}
onSubmit={handleSubmit}
renderForm={renderFormContent}
/>
);
}