feat(WEB): 생산지시 공정별 작업지시 분리 및 기타 품목 섹션 추가
- 수주 품목을 공정별로 그룹핑하여 작업지시 카드 분리 표시 - 공정 미매칭 품목을 "기타 품목" 섹션으로 별도 분리 - 작업지시 카운트에서 기타 품목 제외 - 수량 표시 시 소수점 0 제거 처리
This commit is contained in:
@@ -259,40 +259,68 @@ function matchItemToProcess(
|
||||
return null;
|
||||
}
|
||||
|
||||
// 수주 품목들에서 매칭되는 공정의 workSteps 추출
|
||||
function getWorkStepsForOrder(
|
||||
items: Array<{ itemName: string; itemCode?: string }>,
|
||||
// 공정별 작업지시 그룹 타입
|
||||
interface ProcessWorkOrderGroup {
|
||||
process: Process;
|
||||
items: Array<{ itemName: string; itemCode?: string; quantity: number }>;
|
||||
}
|
||||
|
||||
// 수주 품목들을 공정별로 그룹핑
|
||||
function groupItemsByProcess(
|
||||
items: Array<{ itemName: string; itemCode?: string; quantity: number }>,
|
||||
processes: Process[]
|
||||
): string[] {
|
||||
// 첫 번째 품목으로 공정 매칭 시도
|
||||
if (items.length > 0 && processes.length > 0) {
|
||||
const firstItem = items[0];
|
||||
): ProcessWorkOrderGroup[] {
|
||||
if (items.length === 0 || processes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 공정별 품목 그룹 맵
|
||||
const processGroupMap = new Map<string, ProcessWorkOrderGroup>();
|
||||
// 매칭되지 않은 품목들
|
||||
const unmatchedItems: Array<{ itemName: string; itemCode?: string; quantity: number }> = [];
|
||||
|
||||
for (const item of items) {
|
||||
const matchedProcess = matchItemToProcess(
|
||||
firstItem.itemName,
|
||||
firstItem.itemCode,
|
||||
item.itemName,
|
||||
item.itemCode,
|
||||
processes
|
||||
);
|
||||
|
||||
if (matchedProcess && matchedProcess.workSteps.length > 0) {
|
||||
// workSteps에 번호 추가하여 반환
|
||||
return matchedProcess.workSteps.map(
|
||||
(step, idx) => `${idx + 1}. ${step}`
|
||||
);
|
||||
if (matchedProcess) {
|
||||
if (!processGroupMap.has(matchedProcess.id)) {
|
||||
processGroupMap.set(matchedProcess.id, {
|
||||
process: matchedProcess,
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
processGroupMap.get(matchedProcess.id)!.items.push(item);
|
||||
} else {
|
||||
unmatchedItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭된 공정이 없거나 workSteps가 없으면 첫 번째 공정의 workSteps 사용
|
||||
if (processes.length > 0) {
|
||||
const firstProcess = processes[0];
|
||||
if (firstProcess.workSteps.length > 0) {
|
||||
return firstProcess.workSteps.map(
|
||||
(step, idx) => `${idx + 1}. ${step}`
|
||||
);
|
||||
}
|
||||
// 매칭되지 않은 품목이 있으면 "기타" 그룹 생성
|
||||
const groups = Array.from(processGroupMap.values());
|
||||
|
||||
if (unmatchedItems.length > 0) {
|
||||
// 기타 품목용 가상 공정 생성
|
||||
groups.push({
|
||||
process: {
|
||||
id: "unmatched",
|
||||
processName: "기타",
|
||||
processCode: "ETC",
|
||||
status: "사용중",
|
||||
description: "",
|
||||
classificationRules: [],
|
||||
workSteps: [],
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
} as Process,
|
||||
items: unmatchedItems,
|
||||
});
|
||||
}
|
||||
|
||||
// 공정이 없으면 빈 배열 반환
|
||||
return [];
|
||||
return groups;
|
||||
}
|
||||
|
||||
export default function ProductionOrderCreatePage() {
|
||||
@@ -432,7 +460,23 @@ export default function ProductionOrderCreatePage() {
|
||||
}
|
||||
|
||||
const selectedConfig = getSelectedPriorityConfig();
|
||||
const workOrderCount = 1; // 현재는 수주당 하나의 작업지시 생성
|
||||
|
||||
// 공정별 품목 그룹핑
|
||||
const allGroups = groupItemsByProcess(
|
||||
(order.items || []).map((item) => ({
|
||||
itemName: item.itemName,
|
||||
itemCode: item.itemCode,
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
processes
|
||||
);
|
||||
|
||||
// 공정 매칭된 그룹과 기타(미분류) 그룹 분리
|
||||
const processGroups = allGroups.filter((g) => g.process.id !== "unmatched");
|
||||
const unmatchedGroup = allGroups.find((g) => g.process.id === "unmatched");
|
||||
|
||||
// 작업지시 수 = 공정 매칭된 그룹 수만 (기타 제외)
|
||||
const workOrderCount = processGroups.length;
|
||||
|
||||
// order.items에서 스크린 품목 상세 데이터 변환
|
||||
const screenItems: ScreenItemDetail[] = (order.items || []).map((item, index) => ({
|
||||
@@ -635,54 +679,91 @@ export default function ProductionOrderCreatePage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 수주 데이터 기반 작업지시 카드 */}
|
||||
<div className="border rounded-lg p-4 bg-blue-50/50 border-blue-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<BadgeSm className="bg-blue-100 text-blue-700 border-blue-200">
|
||||
스크린
|
||||
</BadgeSm>
|
||||
<span className="font-mono text-sm font-medium">{order.lotNumber}</span>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-muted-foreground">품목 수</p>
|
||||
<p className="font-medium">{screenItems.length}건</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-2">공정 순서</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(() => {
|
||||
const workSteps = getWorkStepsForOrder(
|
||||
(order.items || []).map(item => ({
|
||||
itemName: item.itemName,
|
||||
itemCode: item.itemCode,
|
||||
})),
|
||||
processes
|
||||
);
|
||||
|
||||
if (workSteps.length === 0) {
|
||||
return (
|
||||
{processGroups.length > 0 ? (
|
||||
processGroups.map((group, groupIdx) => (
|
||||
<div
|
||||
key={group.process.id}
|
||||
className="border rounded-lg p-4 bg-blue-50/50 border-blue-200"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<BadgeSm className="bg-blue-100 text-blue-700 border-blue-200">
|
||||
{group.process.processName}
|
||||
</BadgeSm>
|
||||
<span className="font-mono text-sm font-medium">
|
||||
{order.lotNumber}-{String(groupIdx + 1).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-muted-foreground">품목 수</p>
|
||||
<p className="font-medium">{group.items.length}건</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-2">공정 순서</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.process.workSteps.length > 0 ? (
|
||||
group.process.workSteps.map((step, idx) => (
|
||||
<BadgeSm
|
||||
key={idx}
|
||||
className="bg-gray-50 text-gray-600 border-gray-200"
|
||||
>
|
||||
{idx + 1}. {step}
|
||||
</BadgeSm>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
공정관리에서 세부 작업단계를 등록해 주세요.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return workSteps.map((step, idx) => (
|
||||
<BadgeSm
|
||||
key={idx}
|
||||
className="bg-gray-50 text-gray-600 border-gray-200"
|
||||
>
|
||||
{step}
|
||||
</BadgeSm>
|
||||
));
|
||||
})()}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="border rounded-lg p-4 bg-gray-50/50 border-gray-200">
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
매칭되는 공정이 없습니다. 공정관리에서 분류규칙을 등록해 주세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 기타 품목 (공정 미매칭) */}
|
||||
{unmatchedGroup && unmatchedGroup.items.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
기타 품목
|
||||
<BadgeSm className="bg-gray-100 text-gray-600 border-gray-200">
|
||||
{unmatchedGroup.items.length}건
|
||||
</BadgeSm>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
공정에 매칭되지 않은 품목입니다. 출하 시 별도로 준비해 주세요.
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{unmatchedGroup.items.map((item, idx) => {
|
||||
const qty = Number(item.quantity);
|
||||
return (
|
||||
<li key={idx} className="text-sm flex items-center gap-2">
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span>{item.itemName}</span>
|
||||
<span className="text-muted-foreground">
|
||||
({Number.isInteger(qty) ? qty : qty.toFixed(2).replace(/\.?0+$/, "")}개)
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 자재 소요량 및 재고 현황 - 추후 BOM API 연동 예정 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
|
||||
Reference in New Issue
Block a user