fix: [stocks] 벤딩 LOT 폼 개선 + package.json 정리
This commit is contained in:
@@ -51,7 +51,6 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"immer": "^11.0.1",
|
||||
"jspdf": "^4.0.0",
|
||||
"lucide-react": "^0.552.0",
|
||||
"next": "^15.5.9",
|
||||
"next-intl": "^4.4.0",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
@@ -14,7 +15,21 @@ import {
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Package, ClipboardList, MessageSquare, Tag, Layers } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Package, ClipboardList, MessageSquare, Tag, Layers, Search, X, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { FormSection } from '@/components/organisms/FormSection';
|
||||
@@ -23,8 +38,10 @@ import {
|
||||
resolveBendingItem,
|
||||
generateBendingLot,
|
||||
createBendingStockOrder,
|
||||
getMaterialLots,
|
||||
type BendingCodeMap,
|
||||
type BendingResolvedItem,
|
||||
type MaterialLot,
|
||||
} from './actions';
|
||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
|
||||
@@ -93,6 +110,105 @@ function getInitialForm(): BendingFormState {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 원자재 LOT 선택 모달
|
||||
// ============================================================================
|
||||
|
||||
function MaterialLotModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
material,
|
||||
title,
|
||||
onSelect,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
material: string;
|
||||
title: string;
|
||||
onSelect: (lotNo: string) => void;
|
||||
}) {
|
||||
const [lots, setLots] = useState<MaterialLot[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !material) return;
|
||||
setIsLoading(true);
|
||||
getMaterialLots(material)
|
||||
.then((result) => {
|
||||
if (result.__authError) {
|
||||
toast.error('인증이 만료되었습니다.');
|
||||
return;
|
||||
}
|
||||
if (result.success && result.data) {
|
||||
setLots(result.data);
|
||||
} else {
|
||||
toast.error(result.error || 'LOT 목록 조회에 실패했습니다.');
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [open, material]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
재질: <span className="font-medium">{material}</span>
|
||||
</p>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : lots.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
해당 재질의 입고 LOT가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>LOT번호</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
<TableHead>규격</TableHead>
|
||||
<TableHead className="text-right">입고수량</TableHead>
|
||||
<TableHead>입고일</TableHead>
|
||||
<TableHead>공급업체</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{lots.map((lot) => (
|
||||
<TableRow
|
||||
key={lot.id}
|
||||
className="cursor-pointer hover:bg-accent"
|
||||
onClick={() => {
|
||||
onSelect(lot.lot_no);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-gray-100 px-1.5 py-0.5 rounded">
|
||||
{lot.lot_no}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>{lot.item_name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{lot.specification}</TableCell>
|
||||
<TableCell className="text-right">{lot.receiving_qty}</TableCell>
|
||||
<TableCell>{lot.receiving_date}</TableCell>
|
||||
<TableCell>{lot.supplier}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
@@ -109,6 +225,8 @@ export function BendingLotForm() {
|
||||
const [resolveError, setResolveError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [rawLotModalOpen, setRawLotModalOpen] = useState(false);
|
||||
const [fabricLotModalOpen, setFabricLotModalOpen] = useState(false);
|
||||
|
||||
// 코드맵 로드
|
||||
useEffect(() => {
|
||||
@@ -451,24 +569,67 @@ export function BendingLotForm() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>원자재(철판) LOT</Label>
|
||||
<Input
|
||||
placeholder="원자재 LOT 번호 입력 (선택)"
|
||||
value={form.rawLotNo}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, rawLotNo: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={form.rawLotNo}
|
||||
readOnly
|
||||
placeholder="LOT 선택 (선택)"
|
||||
className="flex-1"
|
||||
/>
|
||||
{form.rawLotNo && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => setForm((prev) => ({ ...prev, rawLotNo: '' }))}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="shrink-0"
|
||||
disabled={!material}
|
||||
onClick={() => setRawLotModalOpen(true)}
|
||||
>
|
||||
<Search className="h-4 w-4 mr-1" />
|
||||
선택
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isSmokeBarrier && (
|
||||
<div className="space-y-2">
|
||||
<Label>원단 LOT</Label>
|
||||
<Input
|
||||
placeholder="원단 LOT 번호 입력 (선택)"
|
||||
value={form.fabricLotNo}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, fabricLotNo: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={form.fabricLotNo}
|
||||
readOnly
|
||||
placeholder="LOT 선택 (선택)"
|
||||
className="flex-1"
|
||||
/>
|
||||
{form.fabricLotNo && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => setForm((prev) => ({ ...prev, fabricLotNo: '' }))}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="shrink-0"
|
||||
onClick={() => setFabricLotModalOpen(true)}
|
||||
>
|
||||
<Search className="h-4 w-4 mr-1" />
|
||||
선택
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -507,13 +668,35 @@ export function BendingLotForm() {
|
||||
);
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={bendingCreateConfig}
|
||||
mode="create"
|
||||
isLoading={isLoading}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSave}
|
||||
renderForm={renderFormContent}
|
||||
/>
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={bendingCreateConfig}
|
||||
mode="create"
|
||||
isLoading={isLoading}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSave}
|
||||
renderForm={renderFormContent}
|
||||
/>
|
||||
|
||||
{/* 원자재 LOT 선택 모달 */}
|
||||
<MaterialLotModal
|
||||
open={rawLotModalOpen}
|
||||
onOpenChange={setRawLotModalOpen}
|
||||
material={material}
|
||||
title="원자재(철판) LOT 선택"
|
||||
onSelect={(lotNo) => setForm((prev) => ({ ...prev, rawLotNo: lotNo }))}
|
||||
/>
|
||||
|
||||
{/* 원단 LOT 선택 모달 (연기차단재 전용) */}
|
||||
{isSmokeBarrier && (
|
||||
<MaterialLotModal
|
||||
open={fabricLotModalOpen}
|
||||
onOpenChange={setFabricLotModalOpen}
|
||||
material="화이바원단"
|
||||
title="원단 LOT 선택"
|
||||
onSelect={(lotNo) => setForm((prev) => ({ ...prev, fabricLotNo: lotNo }))}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ClipboardList,
|
||||
MessageSquare,
|
||||
Tag,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
@@ -263,15 +264,14 @@ export function StockProductionDetail({ orderId }: StockProductionDetailProps) {
|
||||
});
|
||||
}
|
||||
|
||||
// cancelled → 수정 불가 안내
|
||||
// cancelled → 복원 버튼
|
||||
if (order.status === 'cancelled') {
|
||||
items.push({
|
||||
icon: Pencil,
|
||||
label: '수정',
|
||||
onClick: () => toast.warning('취소 상태에서는 수정이 불가합니다.'),
|
||||
variant: 'outline',
|
||||
disabled: false,
|
||||
className: 'opacity-50',
|
||||
icon: RotateCcw,
|
||||
label: '복원',
|
||||
onClick: () => handleStatusChange('draft'),
|
||||
className: 'bg-blue-600 hover:bg-blue-500 text-white',
|
||||
disabled: isProcessing,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -563,6 +563,21 @@ export interface BendingLotFormData {
|
||||
material?: string;
|
||||
}
|
||||
|
||||
export interface MaterialLot {
|
||||
id: number;
|
||||
lot_no: string;
|
||||
supplier_lot: string;
|
||||
item_name: string;
|
||||
specification: string;
|
||||
receiving_qty: string;
|
||||
receiving_date: string;
|
||||
supplier: string;
|
||||
options?: {
|
||||
inspection_status?: string;
|
||||
inspection_result?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 절곡품 LOT API 함수
|
||||
// ============================================================================
|
||||
@@ -708,3 +723,20 @@ export async function createBendingStockOrder(params: {
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 원자재 LOT 목록 조회 (수입검사 완료 입고 건)
|
||||
*/
|
||||
export async function getMaterialLots(material: string): Promise<{
|
||||
success: boolean;
|
||||
data?: MaterialLot[];
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<MaterialLot[]>({
|
||||
url: buildApiUrl('/api/v1/bending/material-lots', { material }),
|
||||
errorMessage: '원자재 LOT 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user