feat: [stocks] 벤딩 LOT 폼 추가 + 재고 상세/액션 보강
This commit is contained in:
@@ -4,23 +4,19 @@
|
||||
* 재고생산관리 페이지
|
||||
*
|
||||
* - 기본: 목록 (StockProductionList)
|
||||
* - ?mode=new: 등록 (StockProductionForm)
|
||||
* - ?mode=new: 등록 (BendingLotForm — 절곡품 LOT 방식)
|
||||
*/
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { StockProductionList } from '@/components/stocks/StockProductionList';
|
||||
import { StockProductionForm } from '@/components/stocks/StockProductionForm';
|
||||
|
||||
function CreateStockContent() {
|
||||
return <StockProductionForm />;
|
||||
}
|
||||
import { BendingLotForm } from '@/components/stocks/BendingLotForm';
|
||||
|
||||
export default function StocksPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <CreateStockContent />;
|
||||
return <BendingLotForm />;
|
||||
}
|
||||
|
||||
return <StockProductionList />;
|
||||
|
||||
519
src/components/stocks/BendingLotForm.tsx
Normal file
519
src/components/stocks/BendingLotForm.tsx
Normal file
@@ -0,0 +1,519 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Package, ClipboardList, MessageSquare, Tag, Layers } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { FormSection } from '@/components/organisms/FormSection';
|
||||
import {
|
||||
getBendingCodeMap,
|
||||
resolveBendingItem,
|
||||
generateBendingLot,
|
||||
createBendingStockOrder,
|
||||
type BendingCodeMap,
|
||||
type BendingResolvedItem,
|
||||
} from './actions';
|
||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
|
||||
// ============================================================================
|
||||
// Config
|
||||
// ============================================================================
|
||||
|
||||
const bendingCreateConfig: DetailConfig = {
|
||||
title: '절곡품 재고생산',
|
||||
description: '절곡품 재고생산을 등록합니다',
|
||||
icon: Package,
|
||||
basePath: '/sales/stocks',
|
||||
fields: [],
|
||||
actions: {
|
||||
showBack: true,
|
||||
showSave: true,
|
||||
submitLabel: '저장',
|
||||
backLabel: '취소',
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// LOT 프리뷰 날짜코드 생성
|
||||
// ============================================================================
|
||||
|
||||
function generateDateCode(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
const year = date.getFullYear() % 10;
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const monthCode =
|
||||
month >= 10 ? String.fromCharCode(55 + month) : String(month);
|
||||
return `${year}${monthCode}${String(day).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 폼 상태
|
||||
// ============================================================================
|
||||
|
||||
interface BendingFormState {
|
||||
regDate: string;
|
||||
prodCode: string;
|
||||
specCode: string;
|
||||
lengthCode: string;
|
||||
quantity: number;
|
||||
rawLotNo: string;
|
||||
fabricLotNo: string;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
function getInitialForm(): BendingFormState {
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(today.getDate()).padStart(2, '0');
|
||||
return {
|
||||
regDate: `${yyyy}-${mm}-${dd}`,
|
||||
prodCode: '',
|
||||
specCode: '',
|
||||
lengthCode: '',
|
||||
quantity: 1,
|
||||
rawLotNo: '',
|
||||
fabricLotNo: '',
|
||||
memo: '',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
|
||||
export function BendingLotForm() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const locale = (params.locale as string) || 'ko';
|
||||
const basePath = `/${locale}/sales/stocks`;
|
||||
|
||||
const [form, setForm] = useState<BendingFormState>(getInitialForm);
|
||||
const [codeMap, setCodeMap] = useState<BendingCodeMap | null>(null);
|
||||
const [resolvedItem, setResolvedItem] = useState<BendingResolvedItem | null>(null);
|
||||
const [resolveError, setResolveError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 코드맵 로드
|
||||
useEffect(() => {
|
||||
async function loadCodeMap() {
|
||||
const result = await getBendingCodeMap();
|
||||
if (result.__authError) {
|
||||
toast.error('인증이 만료되었습니다.');
|
||||
return;
|
||||
}
|
||||
if (result.success && result.data) {
|
||||
setCodeMap(result.data);
|
||||
} else {
|
||||
toast.error(result.error || '코드맵 로딩에 실패했습니다.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
loadCodeMap();
|
||||
}, []);
|
||||
|
||||
// 필터링된 종류 목록
|
||||
const filteredSpecs = useMemo(() => {
|
||||
if (!codeMap || !form.prodCode) return [];
|
||||
return codeMap.specs.filter((s) => s.products.includes(form.prodCode));
|
||||
}, [codeMap, form.prodCode]);
|
||||
|
||||
// 필터링된 길이 목록
|
||||
const filteredLengths = useMemo(() => {
|
||||
if (!codeMap || !form.prodCode) return [];
|
||||
return form.prodCode === 'G'
|
||||
? codeMap.lengths.smoke_barrier
|
||||
: codeMap.lengths.general;
|
||||
}, [codeMap, form.prodCode]);
|
||||
|
||||
// 재질
|
||||
const material = useMemo(() => {
|
||||
if (!codeMap || !form.prodCode || !form.specCode) return '';
|
||||
return codeMap.material_map[`${form.prodCode}:${form.specCode}`] || '';
|
||||
}, [codeMap, form.prodCode, form.specCode]);
|
||||
|
||||
// LOT 프리뷰
|
||||
const lotPreview = useMemo(() => {
|
||||
if (!form.prodCode || !form.specCode || !form.regDate) return '';
|
||||
const dateCode = generateDateCode(form.regDate);
|
||||
if (!dateCode) return '';
|
||||
const base = `${form.prodCode}${form.specCode}${dateCode}`;
|
||||
return form.lengthCode ? `${base}-${form.lengthCode}` : base;
|
||||
}, [form.prodCode, form.specCode, form.lengthCode, form.regDate]);
|
||||
|
||||
// 연기차단재 여부
|
||||
const isSmokeBarrier = form.prodCode === 'G';
|
||||
|
||||
// 품목명 변경
|
||||
const handleProdChange = useCallback((value: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
prodCode: value,
|
||||
specCode: '',
|
||||
lengthCode: '',
|
||||
}));
|
||||
setResolvedItem(null);
|
||||
setResolveError('');
|
||||
}, []);
|
||||
|
||||
// 종류 변경
|
||||
const handleSpecChange = useCallback((value: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
specCode: value,
|
||||
lengthCode: '',
|
||||
}));
|
||||
setResolvedItem(null);
|
||||
setResolveError('');
|
||||
}, []);
|
||||
|
||||
// 모양&길이 변경 → 품목 매핑 조회
|
||||
const handleLengthChange = useCallback(
|
||||
async (value: string) => {
|
||||
setForm((prev) => ({ ...prev, lengthCode: value }));
|
||||
setResolvedItem(null);
|
||||
setResolveError('');
|
||||
|
||||
if (form.prodCode && form.specCode && value) {
|
||||
const result = await resolveBendingItem(form.prodCode, form.specCode, value);
|
||||
if (result.__authError) {
|
||||
toast.error('인증이 만료되었습니다.');
|
||||
return;
|
||||
}
|
||||
if (result.success && result.data) {
|
||||
setResolvedItem(result.data);
|
||||
} else {
|
||||
setResolveError('해당 조합에 매핑된 품목이 없습니다.');
|
||||
}
|
||||
}
|
||||
},
|
||||
[form.prodCode, form.specCode]
|
||||
);
|
||||
|
||||
// 저장
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!form.prodCode) {
|
||||
toast.error('품목명을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
if (!form.specCode) {
|
||||
toast.error('종류를 선택하세요.');
|
||||
return;
|
||||
}
|
||||
if (!form.lengthCode) {
|
||||
toast.error('모양&길이를 선택하세요.');
|
||||
return;
|
||||
}
|
||||
if (form.quantity < 1) {
|
||||
toast.error('수량을 입력하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// 1. LOT 번호 생성
|
||||
const lotResult = await generateBendingLot(
|
||||
form.prodCode,
|
||||
form.specCode,
|
||||
form.lengthCode,
|
||||
form.regDate
|
||||
);
|
||||
if (lotResult.__authError) {
|
||||
toast.error('인증이 만료되었습니다.');
|
||||
return;
|
||||
}
|
||||
if (!lotResult.success || !lotResult.data) {
|
||||
toast.error(lotResult.error || 'LOT 번호 생성에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const lotData = lotResult.data;
|
||||
|
||||
// 2. 재고생산 저장
|
||||
const itemName =
|
||||
resolvedItem?.item_name ||
|
||||
`${codeMap?.products.find((p) => p.code === form.prodCode)?.name || form.prodCode} ${codeMap?.specs.find((s) => s.code === form.specCode)?.name || form.specCode}`;
|
||||
|
||||
const saveResult = await createBendingStockOrder({
|
||||
memo: form.memo,
|
||||
targetStockQty: form.quantity,
|
||||
bendingLot: {
|
||||
lot_number: lotData.lot_number,
|
||||
prod_code: form.prodCode,
|
||||
spec_code: form.specCode,
|
||||
length_code: form.lengthCode,
|
||||
raw_lot_no: form.rawLotNo || undefined,
|
||||
fabric_lot_no: isSmokeBarrier ? form.fabricLotNo || undefined : undefined,
|
||||
material: lotData.material || material,
|
||||
},
|
||||
item: {
|
||||
itemId: resolvedItem?.item_id,
|
||||
itemCode: resolvedItem?.item_code,
|
||||
itemName,
|
||||
specification: resolvedItem?.specification || material,
|
||||
quantity: form.quantity,
|
||||
unit: resolvedItem?.unit || 'EA',
|
||||
},
|
||||
});
|
||||
|
||||
if (saveResult.__authError) {
|
||||
toast.error('인증이 만료되었습니다.');
|
||||
return;
|
||||
}
|
||||
if (!saveResult.success) {
|
||||
toast.error(saveResult.error || '저장에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('절곡품 재고생산이 등록되었습니다.');
|
||||
if (saveResult.data?.id) {
|
||||
router.push(`${basePath}/${saveResult.data.id}`);
|
||||
} else {
|
||||
router.push(basePath);
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [form, resolvedItem, codeMap, material, isSmokeBarrier, router, basePath]);
|
||||
|
||||
// 취소
|
||||
const handleCancel = useCallback(() => {
|
||||
router.push(basePath);
|
||||
}, [router, basePath]);
|
||||
|
||||
// renderForm
|
||||
const renderFormContent = useMemo(
|
||||
() => () => (
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<FormSection title="기본 정보" icon={ClipboardList}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>등록일</Label>
|
||||
<DatePicker
|
||||
value={form.regDate}
|
||||
onChange={(date) => setForm((prev) => ({ ...prev, regDate: date }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수량</Label>
|
||||
<NumberInput
|
||||
value={form.quantity}
|
||||
onChange={(value) =>
|
||||
setForm((prev) => ({ ...prev, quantity: value ?? 1 }))
|
||||
}
|
||||
min={1}
|
||||
placeholder="수량"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
{/* 품목 선택 (캐스케이딩) */}
|
||||
<FormSection title="품목 선택" icon={Layers}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* 품목명 */}
|
||||
<div className="space-y-2">
|
||||
<Label>품목명 <span className="text-red-500">*</span></Label>
|
||||
<Select
|
||||
key={`prod-${form.prodCode}`}
|
||||
value={form.prodCode}
|
||||
onValueChange={handleProdChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="품목 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codeMap?.products.map((p) => (
|
||||
<SelectItem key={p.code} value={p.code}>
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 종류 */}
|
||||
<div className="space-y-2">
|
||||
<Label>종류 <span className="text-red-500">*</span></Label>
|
||||
<Select
|
||||
key={`spec-${form.prodCode}-${form.specCode}`}
|
||||
value={form.specCode}
|
||||
onValueChange={handleSpecChange}
|
||||
disabled={!form.prodCode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="종류 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filteredSpecs.map((s) => (
|
||||
<SelectItem key={s.code} value={s.code}>
|
||||
{s.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 모양&길이 */}
|
||||
<div className="space-y-2">
|
||||
<Label>모양&길이 <span className="text-red-500">*</span></Label>
|
||||
<Select
|
||||
key={`length-${form.prodCode}-${form.specCode}-${form.lengthCode}`}
|
||||
value={form.lengthCode}
|
||||
onValueChange={handleLengthChange}
|
||||
disabled={!form.specCode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="모양&길이 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filteredLengths.map((l) => (
|
||||
<SelectItem key={l.code} value={l.code}>
|
||||
{l.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 매핑 결과 */}
|
||||
{resolvedItem && (
|
||||
<div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-md">
|
||||
<p className="text-sm font-medium text-green-800">매핑된 품목</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">품목코드: </span>
|
||||
<code className="bg-green-100 px-1 rounded">{resolvedItem.item_code}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">품목명: </span>
|
||||
{resolvedItem.item_name}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">규격: </span>
|
||||
{resolvedItem.specification}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">단위: </span>
|
||||
{resolvedItem.unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{resolveError && (
|
||||
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<p className="text-sm text-yellow-800">{resolveError}</p>
|
||||
<p className="text-xs text-yellow-600 mt-1">
|
||||
매핑 없이도 저장 가능합니다. 추후 관리자가 매핑을 등록하면 연결됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</FormSection>
|
||||
|
||||
{/* LOT 정보 */}
|
||||
<FormSection title="LOT 정보" icon={Tag}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>생산품 LOT (프리뷰)</Label>
|
||||
<Input
|
||||
value={lotPreview ? `${lotPreview}-___` : ''}
|
||||
disabled
|
||||
placeholder="품목/종류/등록일 선택 시 자동 생성"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
일련번호는 저장 시 자동 확정됩니다
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>원자재 재질</Label>
|
||||
<Input value={material} disabled placeholder="품목/종류 선택 시 자동 결정" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
{/* 메모 */}
|
||||
<FormSection title="메모" icon={MessageSquare}>
|
||||
<div className="space-y-2">
|
||||
<Label>메모</Label>
|
||||
<Textarea
|
||||
placeholder="메모를 입력하세요 (선택)"
|
||||
value={form.memo}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, memo: e.target.value }))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
),
|
||||
[
|
||||
form,
|
||||
codeMap,
|
||||
filteredSpecs,
|
||||
filteredLengths,
|
||||
material,
|
||||
lotPreview,
|
||||
resolvedItem,
|
||||
resolveError,
|
||||
isSmokeBarrier,
|
||||
handleProdChange,
|
||||
handleSpecChange,
|
||||
handleLengthChange,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={bendingCreateConfig}
|
||||
mode="create"
|
||||
isLoading={isLoading}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSave}
|
||||
renderForm={renderFormContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Factory,
|
||||
ClipboardList,
|
||||
MessageSquare,
|
||||
Tag,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
@@ -238,7 +239,7 @@ export function StockProductionDetail({ orderId }: StockProductionDetailProps) {
|
||||
icon: Factory,
|
||||
label: '생산지시 생성',
|
||||
onClick: handleCreateProductionOrder,
|
||||
className: 'bg-green-600 hover:bg-green-700 text-white',
|
||||
className: 'bg-green-600 hover:bg-green-500 text-white',
|
||||
disabled: isProcessing,
|
||||
});
|
||||
items.push({
|
||||
@@ -310,6 +311,28 @@ export function StockProductionDetail({ orderId }: StockProductionDetailProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* LOT 정보 (절곡품) */}
|
||||
{data.bendingLot && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Tag className="h-5 w-5 text-primary" />
|
||||
LOT 정보
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<InfoItem label="생산품 LOT" value={data.bendingLot.lotNumber} />
|
||||
<InfoItem label="원자재 재질" value={data.bendingLot.material || '-'} />
|
||||
<InfoItem label="원자재 LOT" value={data.bendingLot.rawLotNo || '-'} />
|
||||
{data.bendingLot.prodCode === 'G' && (
|
||||
<InfoItem label="원단 LOT" value={data.bendingLot.fabricLotNo || '-'} />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 비고 */}
|
||||
{(data.memo || data.remarks) && (
|
||||
<Card>
|
||||
|
||||
@@ -26,6 +26,15 @@ interface ApiStockOrder {
|
||||
production_reason?: string;
|
||||
target_stock_qty?: number;
|
||||
manager_name?: string;
|
||||
bending_lot?: {
|
||||
lot_number?: string;
|
||||
prod_code?: string;
|
||||
spec_code?: string;
|
||||
length_code?: string;
|
||||
raw_lot_no?: string;
|
||||
fabric_lot_no?: string;
|
||||
material?: string;
|
||||
};
|
||||
} | null;
|
||||
created_by: number | null;
|
||||
updated_by: number | null;
|
||||
@@ -90,6 +99,15 @@ export interface StockOrder {
|
||||
itemSummary: string;
|
||||
createdAt: string;
|
||||
items: StockOrderItem[];
|
||||
bendingLot?: {
|
||||
lotNumber: string;
|
||||
prodCode: string;
|
||||
specCode: string;
|
||||
lengthCode: string;
|
||||
rawLotNo?: string;
|
||||
fabricLotNo?: string;
|
||||
material?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StockOrderItem {
|
||||
@@ -168,6 +186,8 @@ function transformApiToFrontend(apiData: ApiStockOrder): StockOrder {
|
||||
const firstItemName = items[0]?.itemName || '';
|
||||
const extraCount = items.length > 1 ? ` 외 ${items.length - 1}건` : '';
|
||||
|
||||
const bendingLotData = apiData.options?.bending_lot;
|
||||
|
||||
return {
|
||||
id: String(apiData.id),
|
||||
orderNo: apiData.order_no,
|
||||
@@ -184,6 +204,15 @@ function transformApiToFrontend(apiData: ApiStockOrder): StockOrder {
|
||||
itemSummary: firstItemName ? `${firstItemName}${extraCount}` : '-',
|
||||
createdAt: formatDate(apiData.created_at),
|
||||
items,
|
||||
bendingLot: bendingLotData ? {
|
||||
lotNumber: bendingLotData.lot_number || '',
|
||||
prodCode: bendingLotData.prod_code || '',
|
||||
specCode: bendingLotData.spec_code || '',
|
||||
lengthCode: bendingLotData.length_code || '',
|
||||
rawLotNo: bendingLotData.raw_lot_no,
|
||||
fabricLotNo: bendingLotData.fabric_lot_no,
|
||||
material: bendingLotData.material,
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -478,3 +507,204 @@ export async function createStockProductionOrder(
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return { success: true, data: transformApiToFrontend(result.data.order) };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 절곡품 LOT API 타입 정의
|
||||
// ============================================================================
|
||||
|
||||
export interface BendingProduct {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BendingSpec {
|
||||
code: string;
|
||||
name: string;
|
||||
products: string[];
|
||||
}
|
||||
|
||||
export interface BendingLength {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BendingCodeMap {
|
||||
products: BendingProduct[];
|
||||
specs: BendingSpec[];
|
||||
lengths: {
|
||||
smoke_barrier: BendingLength[];
|
||||
general: BendingLength[];
|
||||
};
|
||||
material_map: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface BendingResolvedItem {
|
||||
item_id: number;
|
||||
item_code: string;
|
||||
item_name: string;
|
||||
specification: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface BendingLotResult {
|
||||
lot_base: string;
|
||||
lot_number: string;
|
||||
date_code: string;
|
||||
material: string;
|
||||
}
|
||||
|
||||
export interface BendingLotFormData {
|
||||
lot_number: string;
|
||||
prod_code: string;
|
||||
spec_code: string;
|
||||
length_code: string;
|
||||
raw_lot_no?: string;
|
||||
fabric_lot_no?: string;
|
||||
material?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 절곡품 LOT API 함수
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 절곡품 코드맵 조회 (캐스케이딩 드롭다운 옵션)
|
||||
*/
|
||||
export async function getBendingCodeMap(): Promise<{
|
||||
success: boolean;
|
||||
data?: BendingCodeMap;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<BendingCodeMap>({
|
||||
url: buildApiUrl('/api/v1/bending/code-map'),
|
||||
errorMessage: '절곡품 코드맵 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 절곡품 품목 매핑 조회 (드롭다운 3개 선택 후)
|
||||
*/
|
||||
export async function resolveBendingItem(
|
||||
prod: string,
|
||||
spec: string,
|
||||
length: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: BendingResolvedItem;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<BendingResolvedItem>({
|
||||
url: buildApiUrl('/api/v1/bending/resolve-item', { prod, spec, length }),
|
||||
errorMessage: '품목 매핑 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 절곡품 LOT 번호 생성
|
||||
*/
|
||||
export async function generateBendingLot(
|
||||
prodCode: string,
|
||||
specCode: string,
|
||||
lengthCode: string,
|
||||
regDate?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: BendingLotResult;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<BendingLotResult>({
|
||||
url: buildApiUrl('/api/v1/bending/generate-lot'),
|
||||
method: 'POST',
|
||||
body: {
|
||||
prod_code: prodCode,
|
||||
spec_code: specCode,
|
||||
length_code: lengthCode,
|
||||
reg_date: regDate,
|
||||
},
|
||||
errorMessage: 'LOT 번호 생성에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 절곡품 재고생산 저장 (기존 orders API + bending_lot 확장)
|
||||
*/
|
||||
export async function createBendingStockOrder(params: {
|
||||
memo?: string;
|
||||
targetStockQty: number;
|
||||
bendingLot: BendingLotFormData;
|
||||
item: {
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName: string;
|
||||
specification?: string;
|
||||
quantity: number;
|
||||
unit?: string;
|
||||
};
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: StockOrder;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const apiData = {
|
||||
order_type_code: 'STOCK',
|
||||
memo: params.memo || null,
|
||||
remarks: null,
|
||||
options: {
|
||||
production_reason: '절곡품 재고생산',
|
||||
target_stock_qty: params.targetStockQty || null,
|
||||
bending_lot: {
|
||||
lot_number: params.bendingLot.lot_number,
|
||||
prod_code: params.bendingLot.prod_code,
|
||||
spec_code: params.bendingLot.spec_code,
|
||||
length_code: params.bendingLot.length_code,
|
||||
raw_lot_no: params.bendingLot.raw_lot_no || null,
|
||||
fabric_lot_no: params.bendingLot.fabric_lot_no || null,
|
||||
material: params.bendingLot.material || null,
|
||||
},
|
||||
},
|
||||
client_id: null,
|
||||
client_name: null,
|
||||
site_name: null,
|
||||
delivery_date: null,
|
||||
delivery_method_code: null,
|
||||
discount_rate: 0,
|
||||
discount_amount: 0,
|
||||
supply_amount: 0,
|
||||
tax_amount: 0,
|
||||
total_amount: 0,
|
||||
items: [
|
||||
{
|
||||
item_id: params.item.itemId || null,
|
||||
item_code: params.item.itemCode || null,
|
||||
item_name: params.item.itemName,
|
||||
specification: params.item.specification || null,
|
||||
quantity: params.item.quantity,
|
||||
unit: params.item.unit || 'EA',
|
||||
unit_price: 0,
|
||||
supply_amount: 0,
|
||||
tax_amount: 0,
|
||||
total_amount: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: buildApiUrl('/api/v1/orders'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: ApiStockOrder) => transformApiToFrontend(d),
|
||||
errorMessage: '절곡품 재고생산 등록에 실패했습니다.',
|
||||
});
|
||||
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