Files
sam-react-prod/src/components/material/ReceivingManagement/InspectionCreate.tsx
유병철 a2c3e4c41e refactor(WEB): 프론트엔드 대규모 코드 정리 및 리팩토링
- 미사용 코드 삭제: ThemeContext, itemStore, utils/date.ts, utils/formatAmount.ts
- 유틸리티 이동: date, formatAmount → src/lib/utils/ (중앙 집중화)
- 다수 page.tsx 클라이언트 컴포넌트 패턴 통일
- DateRangeSelector 리팩토링 및 date-range-picker UI 컴포넌트 추가
- ThemeSelect/themeStore Zustand 직접 연동으로 전환
- 건설/회계/영업/품목/출하 등 전반적 컴포넌트 개선
- UniversalListPage, IntegratedListTemplateV2 타입 확장
- 프론트엔드 종합 리뷰 문서 및 개선 체크리스트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:30:07 +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 '@/lib/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}
/>
);
}