feat(WEB): 생산지시 공정별 작업지시 분리 및 기타 품목 섹션 추가

- 수주 품목을 공정별로 그룹핑하여 작업지시 카드 분리 표시
- 공정 미매칭 품목을 "기타 품목" 섹션으로 별도 분리
- 작업지시 카운트에서 기타 품목 제외
- 수량 표시 시 소수점 0 제거 처리
This commit is contained in:
2026-01-12 18:06:20 +09:00
parent b9f0e24950
commit 495e46fc31

View File

@@ -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">