Implement BOM base quantity retrieval functionality

- Added a new endpoint `/work-instruction/bom-base-qty` to retrieve base quantities for items based on their codes.
- Introduced the `getBomBaseQtyMap` function in the `workInstructionController` to handle the logic for fetching base quantities.
- Updated the frontend to call the new API and integrate base quantity mapping into the work instruction registration process.
- Enhanced the work instruction page to calculate batch counts and split quantities based on the retrieved base quantities.

(TASK:ERP-020)
This commit is contained in:
kjs
2026-05-07 12:01:03 +09:00
parent 20c76d3478
commit 629bc25cd5
4 changed files with 180 additions and 57 deletions

View File

@@ -314,7 +314,7 @@ export async function save(req: AuthenticatedRequest, res: Response) {
if (!firstRouting && itemRouting) firstRouting = itemRouting;
totalQty += Number(item.qty || 0);
await client.query(
`INSERT INTO work_instruction_detail (id,company_code,work_instruction_no,work_instruction_id,item_number,qty,remark,source_table,source_id,part_code,routing_version_id,start_date,end_date,equipment_ids,work_teams,workers,created_date,writer) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,NOW(),$16)`,
`INSERT INTO work_instruction_detail (id,company_code,work_instruction_no,work_instruction_id,item_number,qty,remark,source_table,source_id,part_code,routing_version_id,start_date,end_date,equipment_ids,work_teams,workers,created_date,writer) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,clock_timestamp(),$16)`,
[
companyCode,
wiNo,
@@ -993,3 +993,40 @@ export async function resetWorkStandard(req: AuthenticatedRequest, res: Response
return res.status(500).json({ success: false, message: error.message });
}
}
// ─── BOM 기준수(0레벨 base_qty) 일괄 조회 ───
// itemCodes(item_info.item_number 기준) 배열을 받아 { [itemCode]: base_qty | null } 맵 반환
export async function getBomBaseQtyMap(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const itemCodes: string[] = Array.isArray(req.body?.itemCodes) ? req.body.itemCodes.filter(Boolean) : [];
if (itemCodes.length === 0) return res.json({ success: true, data: {} });
const pool = getPool();
// bom.item_code 우선 매칭, 없으면 item_info.id 경유 매칭
const result = await pool.query(
`SELECT i.item_number AS item_code, b.base_qty
FROM bom b
LEFT JOIN item_info i ON i.id = b.item_id AND i.company_code = b.company_code
WHERE b.company_code = $1
AND (b.item_code = ANY($2::text[]) OR i.item_number = ANY($2::text[]))`,
[companyCode, itemCodes]
);
const map: Record<string, number | null> = {};
for (const code of itemCodes) map[code] = null;
for (const row of result.rows) {
const code = row.item_code;
const base = parseFloat(row.base_qty || "");
if (!code) continue;
if (Number.isFinite(base) && base > 0) {
// 동일 품목 다건 BOM 시 첫 유효값 유지
if (map[code] == null) map[code] = base;
}
}
return res.json({ success: true, data: map });
} catch (error: any) {
logger.error("BOM 기준수 일괄 조회 실패", { error: error.message });
return res.status(500).json({ success: false, message: error.message });
}
}

View File

@@ -18,6 +18,9 @@ router.get("/employees", ctrl.getEmployeeList);
// 벌크 라우팅 조회 (품목별 공정 일괄 조회)
router.post("/routing-versions-bulk", ctrl.getRoutingVersionsBulk);
// BOM 기준수 일괄 조회 (작업지시 등록 모달 기준수/배치수 산출용)
router.post("/bom-base-qty", ctrl.getBomBaseQtyMap);
// 라우팅 & 공정작업기준
router.get("/:wiNo/routing-versions/:itemCode", ctrl.getRoutingVersions);
router.put("/:wiNo/routing", ctrl.updateRouting);

View File

@@ -15,7 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
// API: /work-instruction/*
import {
getWorkInstructionList, previewWorkInstructionNo, saveWorkInstruction, deleteWorkInstructions,
getWorkInstructionList, previewWorkInstructionNo, saveWorkInstruction, deleteWorkInstructions, getBomBaseQtyMap,
getWIItemSource, getWISalesOrderSource, getWIProductionPlanSource, getEquipmentList, getEmployeeList,
getRoutingVersions, RoutingVersionData,
} from "@/lib/api/workInstruction";
@@ -65,6 +65,31 @@ interface SelectedItem {
equipmentIds?: string[];
workTeams?: string[];
workers?: string[];
// 기준수(BOM 0레벨 base_qty) / 배치수(자동) / 배분(균등|순차)
baseQty?: number | null;
splitMode?: "even" | "sequential";
}
// 배치수 산출: baseQty>0 && qty>baseQty면 ceil(qty/baseQty), 아니면 1
function calcBatchCount(qty: number, baseQty: number | null | undefined): number {
const b = Number(baseQty || 0);
if (!Number.isFinite(b) || b <= 0) return 1;
if (!Number.isFinite(qty) || qty <= 0) return 1;
return qty > b ? Math.ceil(qty / b) : 1;
}
// 분할 산출: batchCount<=1이면 [qty], 아니면 mode 따라 분할
function splitQty(qty: number, baseQty: number, batchCount: number, mode: "even" | "sequential"): number[] {
if (batchCount <= 1) return [qty];
if (mode === "sequential") {
const head = Array(batchCount - 1).fill(baseQty);
const tail = qty - baseQty * (batchCount - 1);
return [...head, tail];
}
// even: 앞은 floor, 마지막이 잔여 흡수
const base = Math.floor(qty / batchCount);
const remainder = qty - base * (batchCount - 1);
return [...Array(batchCount - 1).fill(base), remainder];
}
// 공용 다중선택 Popover 컴포넌트 (설비/작업조/작업자에 재사용)
@@ -334,6 +359,19 @@ export default function WorkInstructionPage() {
}).catch(() => {});
}
// BOM 기준수 일괄 조회 (품목별 base_qty 매핑)
if (uniqueItemCodes.length > 0) {
getBomBaseQtyMap(uniqueItemCodes).then(r => {
if (r.success && r.data) {
setConfirmItems(prev => prev.map(it => ({
...it,
baseQty: r.data[it.itemCode] ?? null,
splitMode: it.splitMode || "even",
})));
}
}).catch(() => {});
}
setIsRegModalOpen(false); setIsConfirmModalOpen(true);
};
@@ -364,13 +402,26 @@ export default function WorkInstructionPage() {
const headerEquipment = first?.equipmentIds?.[0] || "";
const headerWorkTeam = first?.workTeams?.[0] || "";
const headerWorker = first?.workers?.[0] || "";
// 배치수≥2인 품목은 splitMode에 따라 N건으로 펼친다.
const expandedItems: Array<typeof confirmItems[number] & { _qty: number }> = [];
for (const i of confirmItems) {
const qty = Number(i.qty || 0);
const baseQty = Number(i.baseQty || 0);
const batchCount = calcBatchCount(qty, i.baseQty);
if (batchCount > 1 && baseQty > 0) {
const parts = splitQty(qty, baseQty, batchCount, i.splitMode || "even");
for (const p of parts) expandedItems.push({ ...i, _qty: p });
} else {
expandedItems.push({ ...i, _qty: qty });
}
}
const payload = {
status: confirmStatus,
startDate: headerStart, endDate: headerEnd,
equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker,
routing: confirmRouting || null,
items: confirmItems.map(i => ({
itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i.qty), remark: i.remark,
items: expandedItems.map(i => ({
itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i._qty), remark: i.remark,
sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode,
routing: i.routing || null,
// 품목별 일정/설비/작업조/작업자 (옵션 A — 다중값 쉼표 구분)
@@ -764,7 +815,7 @@ export default function WorkInstructionPage() {
{/* ── 2단계: 확인 모달 ── */}
<Dialog open={isConfirmModalOpen} onOpenChange={setIsConfirmModalOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[1500px] w-[95vw] max-h-[90vh] flex flex-col overflow-hidden">
<DialogContent className="max-w-[98vw] sm:max-w-[1900px] w-[98vw] max-h-[92vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> '최종 적용' .</DialogDescription>
@@ -785,32 +836,57 @@ export default function WorkInstructionPage() {
<div className="border rounded-lg p-5">
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground mb-3"> </h4>
<div className="overflow-auto">
<Table className="min-w-[1500px]">
<Table className="min-w-[2200px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-0 z-20 bg-muted w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[50px] z-20 bg-muted w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[180px] z-20 bg-muted w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[360px] z-20 bg-muted w-[100px] border-r text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[40px]" />
</TableRow>
</TableHeader>
<TableBody>
{confirmItems.map((item, idx) => (
<TableRow key={idx}>
<TableCell className="text-[13px] text-center">{idx + 1}</TableCell>
<TableCell className="text-[13px] font-medium">{item.itemCode}</TableCell>
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName || item.itemCode}>{item.itemName || item.itemCode}</TableCell>
<TableCell className="text-[13px]">{item.spec || "-"}</TableCell>
<TableCell><Input type="number" className="h-7 text-[13px] w-20" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
{confirmItems.map((item, idx) => {
const batchCount = calcBatchCount(Number(item.qty || 0), item.baseQty);
const splitDisabled = batchCount <= 1 || !item.baseQty;
return (
<TableRow key={idx} className="bg-background">
<TableCell className="sticky left-0 z-10 bg-background text-[13px] text-center">{idx + 1}</TableCell>
<TableCell className="sticky left-[50px] z-10 bg-background text-[13px] font-medium">{item.itemCode}</TableCell>
<TableCell className="sticky left-[180px] z-10 bg-background text-sm truncate max-w-[180px]" title={item.itemName || item.itemCode}>{item.itemName || item.itemCode}</TableCell>
<TableCell className="sticky left-[360px] z-10 bg-background border-r text-[13px]">{item.spec || "-"}</TableCell>
<TableCell><Input type="number" className="h-9 text-sm w-full min-w-[100px]" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell className="text-right text-sm text-muted-foreground">
{item.baseQty != null && item.baseQty > 0 ? Number(item.baseQty).toLocaleString() : "-"}
</TableCell>
<TableCell className={cn("text-center text-sm font-semibold", batchCount > 1 ? "text-primary" : "text-muted-foreground")}>
{item.baseQty != null && item.baseQty > 0 ? batchCount : "-"}
</TableCell>
<TableCell>
<Select
value={item.splitMode || "even"}
onValueChange={v => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, splitMode: v as "even" | "sequential" } : it))}
disabled={splitDisabled}
>
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="even"></SelectItem>
<SelectItem value="sequential"></SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Select
value={nv(item.routing || "")}
@@ -819,7 +895,7 @@ export default function WorkInstructionPage() {
setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, routing: val } : it));
}}
>
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
{(item.routingOptions || []).map((rv: RoutingVersionData) => (
@@ -831,10 +907,10 @@ export default function WorkInstructionPage() {
</Select>
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.startDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.startDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.endDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.endDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<MultiSelectPopover
@@ -864,10 +940,11 @@ export default function WorkInstructionPage() {
emptyMessage="사원을 찾을 수 없어요"
/>
</TableCell>
<TableCell><Input className="h-7 text-[13px]" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Input className="h-9 text-sm w-full min-w-[160px]" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setConfirmItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
</TableRow>
))}
);
})}
</TableBody>
</Table>
</div>
@@ -884,7 +961,7 @@ export default function WorkInstructionPage() {
{/* ── 수정 모달 ── */}
<Dialog open={isEditModalOpen} onOpenChange={(v) => { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}>
<DialogContent className="max-w-[95vw] sm:max-w-[1500px] w-[95vw] max-h-[90vh] flex flex-col overflow-hidden">
<DialogContent className="max-w-[98vw] sm:max-w-[1900px] w-[98vw] max-h-[92vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>{`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`}</DialogTitle>
<DialogDescription> / .</DialogDescription>
@@ -907,22 +984,22 @@ export default function WorkInstructionPage() {
<span className="text-[11px] font-semibold text-primary bg-primary/8 border border-primary/15 px-2 py-0.5 rounded-full font-mono">{editItems.length}</span>
</div>
<div className="overflow-auto">
<Table className="min-w-[1500px]">
<Table className="min-w-[2200px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-0 z-20 bg-muted w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[50px] z-20 bg-muted w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[180px] z-20 bg-muted w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[360px] z-20 bg-muted w-[100px] border-r text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[40px]" />
</TableRow>
</TableHeader>
@@ -930,12 +1007,12 @@ export default function WorkInstructionPage() {
{editItems.length === 0 ? (
<TableRow><TableCell colSpan={14} className="text-center py-8 text-sm text-muted-foreground"> </TableCell></TableRow>
) : editItems.map((item, idx) => (
<TableRow key={idx}>
<TableCell className="text-[13px] text-center">{idx + 1}</TableCell>
<TableCell className="text-[13px] font-medium">{item.itemCode}</TableCell>
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName}>{item.itemName || "-"}</TableCell>
<TableCell className="text-[13px] truncate" title={item.spec}>{item.spec || "-"}</TableCell>
<TableCell className="text-right"><Input type="number" className="h-7 text-[13px] w-20 ml-auto" value={item.qty} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableRow key={idx} className="bg-background">
<TableCell className="sticky left-0 z-10 bg-background text-[13px] text-center">{idx + 1}</TableCell>
<TableCell className="sticky left-[50px] z-10 bg-background text-[13px] font-medium">{item.itemCode}</TableCell>
<TableCell className="sticky left-[180px] z-10 bg-background text-sm truncate max-w-[180px]" title={item.itemName}>{item.itemName || "-"}</TableCell>
<TableCell className="sticky left-[360px] z-10 bg-background border-r text-[13px] truncate" title={item.spec}>{item.spec || "-"}</TableCell>
<TableCell className="text-right"><Input type="number" className="h-9 text-sm w-full min-w-[100px] ml-auto" value={item.qty} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell>
<Select
value={nv(item.routing || "")}
@@ -944,7 +1021,7 @@ export default function WorkInstructionPage() {
setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, routing: val } : it));
}}
>
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
{(item.routingOptions || []).map((rv: RoutingVersionData) => (
@@ -959,7 +1036,7 @@ export default function WorkInstructionPage() {
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
className="h-9 text-sm"
disabled={!item.routing}
onClick={() => {
if (!editOrder || !item.routing) return;
@@ -973,14 +1050,14 @@ export default function WorkInstructionPage() {
);
}}
>
<ClipboardCheck className="w-3 h-3 mr-1" />
<ClipboardCheck className="w-3.5 h-3.5 mr-1" />
</Button>
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.startDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.startDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.endDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.endDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<MultiSelectPopover
@@ -1010,7 +1087,7 @@ export default function WorkInstructionPage() {
emptyMessage="사원을 찾을 수 없어요"
/>
</TableCell>
<TableCell><Input className="h-7 text-[13px]" value={item.remark} placeholder="비고" onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Input className="h-9 text-sm w-full min-w-[160px]" value={item.remark} placeholder="비고" onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
</TableRow>
))}

View File

@@ -47,6 +47,12 @@ export async function getEmployeeList() {
return res.data as { success: boolean; data: { user_id: string; user_name: string; dept_name: string | null }[] };
}
// BOM 기준수(0레벨 base_qty) 일괄 조회 — 작업지시 등록 모달의 기준수/배치수 산출용
export async function getBomBaseQtyMap(itemCodes: string[]) {
const res = await apiClient.post("/work-instruction/bom-base-qty", { itemCodes });
return res.data as { success: boolean; data: Record<string, number | null> };
}
// ─── 라우팅 & 공정작업기준 API ───
export interface RoutingProcess {