From bf58ce3c07c305a1412bc0d2e8d485b97c29672c Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 22 Apr 2026 15:44:42 +0900 Subject: [PATCH] feat: Implement multi-select functionality for work instruction items - Added new fields to the SelectedItem interface for managing item schedules, equipment, work teams, and workers. - Created a reusable MultiSelectPopover component to facilitate multi-selection of equipment, work teams, and workers. - Enhanced the applyRegistration function to include start and end dates, as well as equipment and team assignments for work instruction items. - Updated item handling logic to support production planning with optional scheduling details, improving the overall functionality of the work instruction page. --- .../production/work-instruction/page.tsx | 295 +++++++++++++++--- .../production/work-instruction/page.tsx | 295 +++++++++++++++--- .../production/work-instruction/page.tsx | 295 +++++++++++++++--- .../production/work-instruction/page.tsx | 295 +++++++++++++++--- .../production/work-instruction/page.tsx | 295 +++++++++++++++--- .../production/work-instruction/page.tsx | 295 +++++++++++++++--- 6 files changed, 1488 insertions(+), 282 deletions(-) diff --git a/frontend/app/(main)/COMPANY_10/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_10/production/work-instruction/page.tsx index 9a6fc954..110b2b4d 100644 --- a/frontend/app/(main)/COMPANY_10/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/work-instruction/page.tsx @@ -59,6 +59,90 @@ interface SelectedItem { itemCode: string; itemName: string; spec: string; qty: number; remark: string; sourceType: SourceType; sourceTable: string; sourceId: string | number; routing?: string; routingOptions?: RoutingVersionData[]; + // 품목별 일정/설비/작업조/작업자 (옵션 A — 다중선택 지원) + startDate?: string; + endDate?: string; + equipmentIds?: string[]; + workTeams?: string[]; + workers?: string[]; +} + +// 공용 다중선택 Popover 컴포넌트 (설비/작업조/작업자에 재사용) +interface MultiSelectOption { value: string; label: string; sub?: string; } +interface MultiSelectPopoverProps { + options: MultiSelectOption[]; + value: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + searchable?: boolean; + triggerClassName?: string; + emptyMessage?: string; +} + +function MultiSelectPopover({ options, value, onChange, placeholder = "선택", searchable = false, triggerClassName, emptyMessage = "항목이 없어요" }: MultiSelectPopoverProps) { + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + + const selectedSet = useMemo(() => new Set(value), [value]); + const toggle = (val: string) => { + if (selectedSet.has(val)) onChange(value.filter(v => v !== val)); + else onChange([...value, val]); + }; + + const filtered = useMemo(() => { + if (!searchable || !keyword.trim()) return options; + const k = keyword.trim().toLowerCase(); + return options.filter(o => o.label.toLowerCase().includes(k) || (o.sub || "").toLowerCase().includes(k)); + }, [options, keyword, searchable]); + + const display = useMemo(() => { + if (value.length === 0) return placeholder; + if (value.length === 1) return options.find(o => o.value === value[0])?.label || value[0]; + if (value.length === 2) { + const labels = value.map(v => options.find(o => o.value === v)?.label || v); + return labels.join(", "); + } + return `${value.length}개 선택`; + }, [value, options, placeholder]); + + return ( + + + + + + {searchable && ( +
+ setKeyword(e.target.value)} className="h-7 text-xs" /> +
+ )} +
+ {filtered.length === 0 ? ( +
{emptyMessage}
+ ) : filtered.map(opt => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.length}개 선택됨 + +
+ )} +
+
+ ); } export default function WorkInstructionPage() { @@ -197,17 +281,22 @@ export default function WorkInstructionPage() { const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } + const today = new Date().toISOString().split("T")[0]; const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; - if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); - else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); + const baseExtra = { startDate: today, endDate: "", equipmentIds: [], workTeams: [], workers: [] } as Pick; + if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code, ...baseExtra }); + else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id, ...baseExtra }); else { // 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능) const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null ? Number(item.remain_qty) : Number(item.plan_qty || 1); - items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); + // 생산계획: 일정이 있으면 기본값으로 전달 + const planStart = item.start_date ? String(item.start_date).split("T")[0] : today; + const planEnd = item.end_date ? String(item.end_date).split("T")[0] : ""; + items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id, startDate: planStart, endDate: planEnd, equipmentIds: [], workTeams: [], workers: [] }); } } @@ -256,6 +345,9 @@ export default function WorkInstructionPage() { itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "", qty: Number(confirmAddQty), remark: "", sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "", + startDate: firstItem?.startDate || new Date().toISOString().split("T")[0], + endDate: firstItem?.endDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setConfirmAddQty(""); }; @@ -265,11 +357,29 @@ export default function WorkInstructionPage() { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값으로 (하위 호환 유지 — 조회 화면이 헤더값으로 표시되는 레거시 대비) + const first = confirmItems[0]; + const headerStart = first?.startDate || ""; + const headerEnd = first?.endDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || ""; + const headerWorkTeam = first?.workTeams?.[0] || ""; + const headerWorker = first?.workers?.[0] || ""; const payload = { - status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, - equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, + 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, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: confirmItems.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 — 다중값 쉼표 구분) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } @@ -292,6 +402,12 @@ export default function WorkInstructionPage() { sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", routing: d.detail_routing_version_id || order.routing_version_id || "", routingOptions: [], + // 품목별 일정/설비/작업조/작업자 (detail 값 우선, 없으면 헤더값 폴백) + startDate: d.detail_start_date || d.start_date || "", + endDate: d.detail_end_date || d.end_date || "", + equipmentIds: (d.detail_equipment_ids || "").split(",").filter(Boolean), + workTeams: (d.detail_work_teams || "").split(",").filter(Boolean), + workers: (d.detail_workers || "").split(",").filter(Boolean), })); setEditItems(items); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); @@ -322,9 +438,13 @@ export default function WorkInstructionPage() { const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } + const firstItem = editItems[0]; setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", + startDate: firstItem?.startDate || editStartDate || "", + endDate: firstItem?.endDate || editEndDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setAddQty(""); }; @@ -333,11 +453,30 @@ export default function WorkInstructionPage() { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값 사용 (하위 호환 — 등록 모달과 동일 패턴) + const first = editItems[0]; + const headerStart = first?.startDate || editStartDate || ""; + const headerEnd = first?.endDate || editEndDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || editEquipmentId || ""; + const headerWorkTeam = first?.workTeams?.[0] || editWorkTeam || ""; + const headerWorker = first?.workers?.[0] || editWorker || ""; const payload = { - id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, - equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, + id: editOrder.wi_id, status: editStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, + remark: editRemark, routing: editRouting || null, - items: editItems.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 })), + items: editItems.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, + // 품목별 일정/설비/작업조/작업자 (다중값 쉼표 구분 — 등록 모달과 동일) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } @@ -625,7 +764,7 @@ export default function WorkInstructionPage() { {/* ── 2단계: 확인 모달 ── */} - + 작업지시 적용 확인 기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요. @@ -634,38 +773,33 @@ export default function WorkInstructionPage() {

작업지시 기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setConfirmStartDate(e.target.value)} className="h-9" />
-
setConfirmEndDate(e.target.value)} className="h-9" />
-
- -
-
- -
-
- -

품목 목록

- +
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 비고 + 수량 + 라우팅 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 @@ -674,7 +808,7 @@ export default function WorkInstructionPage() { {idx + 1} {item.itemCode} - {item.itemName || item.itemCode} + {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -696,6 +830,40 @@ export default function WorkInstructionPage() { + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> @@ -716,7 +884,7 @@ export default function WorkInstructionPage() { {/* ── 수정 모달 ── */} { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}> - + {`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`} 품목을 추가/삭제하고 정보를 수정해주세요. @@ -725,48 +893,47 @@ export default function WorkInstructionPage() {

기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setEditStartDate(e.target.value)} className="h-9" />
-
setEditEndDate(e.target.value)} className="h-9" />
-
-
-
- -
-
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
+
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
- {/* 품목 테이블 — 라우팅/공정작업기준을 품목별로 표시 */} + {/* 품목 테이블 — 품목별 일정/설비/작업조/작업자 + 라우팅/공정작업기준 */}
작업지시 항목 {editItems.length}건
-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + 수량 + 라우팅 + 공정작업기준 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 {editItems.length === 0 ? ( - 등록된 품목이 없어요 + 등록된 품목이 없어요 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} - {item.itemName || "-"} + {item.itemName || "-"} {item.spec || "-"} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -809,6 +976,40 @@ export default function WorkInstructionPage() { 수정 + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> diff --git a/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx index 114b92a1..064a7dae 100644 --- a/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx @@ -59,6 +59,90 @@ interface SelectedItem { itemCode: string; itemName: string; spec: string; qty: number; remark: string; sourceType: SourceType; sourceTable: string; sourceId: string | number; routing?: string; routingOptions?: RoutingVersionData[]; + // 품목별 일정/설비/작업조/작업자 (옵션 A — 다중선택 지원) + startDate?: string; + endDate?: string; + equipmentIds?: string[]; + workTeams?: string[]; + workers?: string[]; +} + +// 공용 다중선택 Popover 컴포넌트 (설비/작업조/작업자에 재사용) +interface MultiSelectOption { value: string; label: string; sub?: string; } +interface MultiSelectPopoverProps { + options: MultiSelectOption[]; + value: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + searchable?: boolean; + triggerClassName?: string; + emptyMessage?: string; +} + +function MultiSelectPopover({ options, value, onChange, placeholder = "선택", searchable = false, triggerClassName, emptyMessage = "항목이 없어요" }: MultiSelectPopoverProps) { + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + + const selectedSet = useMemo(() => new Set(value), [value]); + const toggle = (val: string) => { + if (selectedSet.has(val)) onChange(value.filter(v => v !== val)); + else onChange([...value, val]); + }; + + const filtered = useMemo(() => { + if (!searchable || !keyword.trim()) return options; + const k = keyword.trim().toLowerCase(); + return options.filter(o => o.label.toLowerCase().includes(k) || (o.sub || "").toLowerCase().includes(k)); + }, [options, keyword, searchable]); + + const display = useMemo(() => { + if (value.length === 0) return placeholder; + if (value.length === 1) return options.find(o => o.value === value[0])?.label || value[0]; + if (value.length === 2) { + const labels = value.map(v => options.find(o => o.value === v)?.label || v); + return labels.join(", "); + } + return `${value.length}개 선택`; + }, [value, options, placeholder]); + + return ( + + + + + + {searchable && ( +
+ setKeyword(e.target.value)} className="h-7 text-xs" /> +
+ )} +
+ {filtered.length === 0 ? ( +
{emptyMessage}
+ ) : filtered.map(opt => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.length}개 선택됨 + +
+ )} +
+
+ ); } export default function WorkInstructionPage() { @@ -201,17 +285,22 @@ export default function WorkInstructionPage() { const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } + const today = new Date().toISOString().split("T")[0]; const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; - if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); - else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); + const baseExtra = { startDate: today, endDate: "", equipmentIds: [], workTeams: [], workers: [] } as Pick; + if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code, ...baseExtra }); + else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id, ...baseExtra }); else { // 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능) const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null ? Number(item.remain_qty) : Number(item.plan_qty || 1); - items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); + // 생산계획: 일정이 있으면 기본값으로 전달 + const planStart = item.start_date ? String(item.start_date).split("T")[0] : today; + const planEnd = item.end_date ? String(item.end_date).split("T")[0] : ""; + items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id, startDate: planStart, endDate: planEnd, equipmentIds: [], workTeams: [], workers: [] }); } } @@ -260,6 +349,9 @@ export default function WorkInstructionPage() { itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "", qty: Number(confirmAddQty), remark: "", sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "", + startDate: firstItem?.startDate || new Date().toISOString().split("T")[0], + endDate: firstItem?.endDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setConfirmAddQty(""); }; @@ -269,11 +361,29 @@ export default function WorkInstructionPage() { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값으로 (하위 호환 유지 — 조회 화면이 헤더값으로 표시되는 레거시 대비) + const first = confirmItems[0]; + const headerStart = first?.startDate || ""; + const headerEnd = first?.endDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || ""; + const headerWorkTeam = first?.workTeams?.[0] || ""; + const headerWorker = first?.workers?.[0] || ""; const payload = { - status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, - equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, + 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, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: confirmItems.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 — 다중값 쉼표 구분) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } @@ -296,6 +406,12 @@ export default function WorkInstructionPage() { sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", routing: d.detail_routing_version_id || order.routing_version_id || "", routingOptions: [], + // 품목별 일정/설비/작업조/작업자 (detail 값 우선, 없으면 헤더값 폴백) + startDate: d.detail_start_date || d.start_date || "", + endDate: d.detail_end_date || d.end_date || "", + equipmentIds: (d.detail_equipment_ids || "").split(",").filter(Boolean), + workTeams: (d.detail_work_teams || "").split(",").filter(Boolean), + workers: (d.detail_workers || "").split(",").filter(Boolean), })); setEditItems(items); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); @@ -326,9 +442,13 @@ export default function WorkInstructionPage() { const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } + const firstItem = editItems[0]; setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", + startDate: firstItem?.startDate || editStartDate || "", + endDate: firstItem?.endDate || editEndDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setAddQty(""); }; @@ -337,11 +457,30 @@ export default function WorkInstructionPage() { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값 사용 (하위 호환 — 등록 모달과 동일 패턴) + const first = editItems[0]; + const headerStart = first?.startDate || editStartDate || ""; + const headerEnd = first?.endDate || editEndDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || editEquipmentId || ""; + const headerWorkTeam = first?.workTeams?.[0] || editWorkTeam || ""; + const headerWorker = first?.workers?.[0] || editWorker || ""; const payload = { - id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, - equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, + id: editOrder.wi_id, status: editStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, + remark: editRemark, routing: editRouting || null, - items: editItems.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 })), + items: editItems.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, + // 품목별 일정/설비/작업조/작업자 (다중값 쉼표 구분 — 등록 모달과 동일) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } @@ -629,7 +768,7 @@ export default function WorkInstructionPage() { {/* ── 2단계: 확인 모달 ── */} - + 작업지시 적용 확인 기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요. @@ -638,38 +777,33 @@ export default function WorkInstructionPage() {

작업지시 기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setConfirmStartDate(e.target.value)} className="h-9" />
-
setConfirmEndDate(e.target.value)} className="h-9" />
-
- -
-
- -
-
- -

품목 목록

-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 비고 + 수량 + 라우팅 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 @@ -678,7 +812,7 @@ export default function WorkInstructionPage() { {idx + 1} {item.itemCode} - {item.itemName || item.itemCode} + {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -700,6 +834,40 @@ export default function WorkInstructionPage() { + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> @@ -720,7 +888,7 @@ export default function WorkInstructionPage() { {/* ── 수정 모달 ── */} { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}> - + {`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`} 품목을 추가/삭제하고 정보를 수정해주세요. @@ -729,48 +897,47 @@ export default function WorkInstructionPage() {

기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setEditStartDate(e.target.value)} className="h-9" />
-
setEditEndDate(e.target.value)} className="h-9" />
-
-
-
- -
-
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
+
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
- {/* 품목 테이블 — 라우팅/공정작업기준을 품목별로 표시 */} + {/* 품목 테이블 — 품목별 일정/설비/작업조/작업자 + 라우팅/공정작업기준 */}
작업지시 항목 {editItems.length}건
-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + 수량 + 라우팅 + 공정작업기준 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 {editItems.length === 0 ? ( - 등록된 품목이 없어요 + 등록된 품목이 없어요 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} - {item.itemName || "-"} + {item.itemName || "-"} {item.spec || "-"} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -813,6 +980,40 @@ export default function WorkInstructionPage() { 수정 + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> diff --git a/frontend/app/(main)/COMPANY_29/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_29/production/work-instruction/page.tsx index 9a6fc954..110b2b4d 100644 --- a/frontend/app/(main)/COMPANY_29/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_29/production/work-instruction/page.tsx @@ -59,6 +59,90 @@ interface SelectedItem { itemCode: string; itemName: string; spec: string; qty: number; remark: string; sourceType: SourceType; sourceTable: string; sourceId: string | number; routing?: string; routingOptions?: RoutingVersionData[]; + // 품목별 일정/설비/작업조/작업자 (옵션 A — 다중선택 지원) + startDate?: string; + endDate?: string; + equipmentIds?: string[]; + workTeams?: string[]; + workers?: string[]; +} + +// 공용 다중선택 Popover 컴포넌트 (설비/작업조/작업자에 재사용) +interface MultiSelectOption { value: string; label: string; sub?: string; } +interface MultiSelectPopoverProps { + options: MultiSelectOption[]; + value: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + searchable?: boolean; + triggerClassName?: string; + emptyMessage?: string; +} + +function MultiSelectPopover({ options, value, onChange, placeholder = "선택", searchable = false, triggerClassName, emptyMessage = "항목이 없어요" }: MultiSelectPopoverProps) { + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + + const selectedSet = useMemo(() => new Set(value), [value]); + const toggle = (val: string) => { + if (selectedSet.has(val)) onChange(value.filter(v => v !== val)); + else onChange([...value, val]); + }; + + const filtered = useMemo(() => { + if (!searchable || !keyword.trim()) return options; + const k = keyword.trim().toLowerCase(); + return options.filter(o => o.label.toLowerCase().includes(k) || (o.sub || "").toLowerCase().includes(k)); + }, [options, keyword, searchable]); + + const display = useMemo(() => { + if (value.length === 0) return placeholder; + if (value.length === 1) return options.find(o => o.value === value[0])?.label || value[0]; + if (value.length === 2) { + const labels = value.map(v => options.find(o => o.value === v)?.label || v); + return labels.join(", "); + } + return `${value.length}개 선택`; + }, [value, options, placeholder]); + + return ( + + + + + + {searchable && ( +
+ setKeyword(e.target.value)} className="h-7 text-xs" /> +
+ )} +
+ {filtered.length === 0 ? ( +
{emptyMessage}
+ ) : filtered.map(opt => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.length}개 선택됨 + +
+ )} +
+
+ ); } export default function WorkInstructionPage() { @@ -197,17 +281,22 @@ export default function WorkInstructionPage() { const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } + const today = new Date().toISOString().split("T")[0]; const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; - if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); - else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); + const baseExtra = { startDate: today, endDate: "", equipmentIds: [], workTeams: [], workers: [] } as Pick; + if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code, ...baseExtra }); + else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id, ...baseExtra }); else { // 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능) const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null ? Number(item.remain_qty) : Number(item.plan_qty || 1); - items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); + // 생산계획: 일정이 있으면 기본값으로 전달 + const planStart = item.start_date ? String(item.start_date).split("T")[0] : today; + const planEnd = item.end_date ? String(item.end_date).split("T")[0] : ""; + items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id, startDate: planStart, endDate: planEnd, equipmentIds: [], workTeams: [], workers: [] }); } } @@ -256,6 +345,9 @@ export default function WorkInstructionPage() { itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "", qty: Number(confirmAddQty), remark: "", sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "", + startDate: firstItem?.startDate || new Date().toISOString().split("T")[0], + endDate: firstItem?.endDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setConfirmAddQty(""); }; @@ -265,11 +357,29 @@ export default function WorkInstructionPage() { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값으로 (하위 호환 유지 — 조회 화면이 헤더값으로 표시되는 레거시 대비) + const first = confirmItems[0]; + const headerStart = first?.startDate || ""; + const headerEnd = first?.endDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || ""; + const headerWorkTeam = first?.workTeams?.[0] || ""; + const headerWorker = first?.workers?.[0] || ""; const payload = { - status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, - equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, + 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, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: confirmItems.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 — 다중값 쉼표 구분) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } @@ -292,6 +402,12 @@ export default function WorkInstructionPage() { sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", routing: d.detail_routing_version_id || order.routing_version_id || "", routingOptions: [], + // 품목별 일정/설비/작업조/작업자 (detail 값 우선, 없으면 헤더값 폴백) + startDate: d.detail_start_date || d.start_date || "", + endDate: d.detail_end_date || d.end_date || "", + equipmentIds: (d.detail_equipment_ids || "").split(",").filter(Boolean), + workTeams: (d.detail_work_teams || "").split(",").filter(Boolean), + workers: (d.detail_workers || "").split(",").filter(Boolean), })); setEditItems(items); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); @@ -322,9 +438,13 @@ export default function WorkInstructionPage() { const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } + const firstItem = editItems[0]; setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", + startDate: firstItem?.startDate || editStartDate || "", + endDate: firstItem?.endDate || editEndDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setAddQty(""); }; @@ -333,11 +453,30 @@ export default function WorkInstructionPage() { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값 사용 (하위 호환 — 등록 모달과 동일 패턴) + const first = editItems[0]; + const headerStart = first?.startDate || editStartDate || ""; + const headerEnd = first?.endDate || editEndDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || editEquipmentId || ""; + const headerWorkTeam = first?.workTeams?.[0] || editWorkTeam || ""; + const headerWorker = first?.workers?.[0] || editWorker || ""; const payload = { - id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, - equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, + id: editOrder.wi_id, status: editStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, + remark: editRemark, routing: editRouting || null, - items: editItems.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 })), + items: editItems.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, + // 품목별 일정/설비/작업조/작업자 (다중값 쉼표 구분 — 등록 모달과 동일) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } @@ -625,7 +764,7 @@ export default function WorkInstructionPage() { {/* ── 2단계: 확인 모달 ── */} - + 작업지시 적용 확인 기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요. @@ -634,38 +773,33 @@ export default function WorkInstructionPage() {

작업지시 기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setConfirmStartDate(e.target.value)} className="h-9" />
-
setConfirmEndDate(e.target.value)} className="h-9" />
-
- -
-
- -
-
- -

품목 목록

-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 비고 + 수량 + 라우팅 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 @@ -674,7 +808,7 @@ export default function WorkInstructionPage() { {idx + 1} {item.itemCode} - {item.itemName || item.itemCode} + {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -696,6 +830,40 @@ export default function WorkInstructionPage() { + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> @@ -716,7 +884,7 @@ export default function WorkInstructionPage() { {/* ── 수정 모달 ── */} { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}> - + {`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`} 품목을 추가/삭제하고 정보를 수정해주세요. @@ -725,48 +893,47 @@ export default function WorkInstructionPage() {

기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setEditStartDate(e.target.value)} className="h-9" />
-
setEditEndDate(e.target.value)} className="h-9" />
-
-
-
- -
-
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
+
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
- {/* 품목 테이블 — 라우팅/공정작업기준을 품목별로 표시 */} + {/* 품목 테이블 — 품목별 일정/설비/작업조/작업자 + 라우팅/공정작업기준 */}
작업지시 항목 {editItems.length}건
-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + 수량 + 라우팅 + 공정작업기준 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 {editItems.length === 0 ? ( - 등록된 품목이 없어요 + 등록된 품목이 없어요 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} - {item.itemName || "-"} + {item.itemName || "-"} {item.spec || "-"} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -809,6 +976,40 @@ export default function WorkInstructionPage() { 수정 + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> diff --git a/frontend/app/(main)/COMPANY_30/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_30/production/work-instruction/page.tsx index c01796fe..4e66d483 100644 --- a/frontend/app/(main)/COMPANY_30/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_30/production/work-instruction/page.tsx @@ -59,6 +59,90 @@ interface SelectedItem { itemCode: string; itemName: string; spec: string; qty: number; remark: string; sourceType: SourceType; sourceTable: string; sourceId: string | number; routing?: string; routingOptions?: RoutingVersionData[]; + // 품목별 일정/설비/작업조/작업자 (옵션 A — 다중선택 지원) + startDate?: string; + endDate?: string; + equipmentIds?: string[]; + workTeams?: string[]; + workers?: string[]; +} + +// 공용 다중선택 Popover 컴포넌트 (설비/작업조/작업자에 재사용) +interface MultiSelectOption { value: string; label: string; sub?: string; } +interface MultiSelectPopoverProps { + options: MultiSelectOption[]; + value: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + searchable?: boolean; + triggerClassName?: string; + emptyMessage?: string; +} + +function MultiSelectPopover({ options, value, onChange, placeholder = "선택", searchable = false, triggerClassName, emptyMessage = "항목이 없어요" }: MultiSelectPopoverProps) { + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + + const selectedSet = useMemo(() => new Set(value), [value]); + const toggle = (val: string) => { + if (selectedSet.has(val)) onChange(value.filter(v => v !== val)); + else onChange([...value, val]); + }; + + const filtered = useMemo(() => { + if (!searchable || !keyword.trim()) return options; + const k = keyword.trim().toLowerCase(); + return options.filter(o => o.label.toLowerCase().includes(k) || (o.sub || "").toLowerCase().includes(k)); + }, [options, keyword, searchable]); + + const display = useMemo(() => { + if (value.length === 0) return placeholder; + if (value.length === 1) return options.find(o => o.value === value[0])?.label || value[0]; + if (value.length === 2) { + const labels = value.map(v => options.find(o => o.value === v)?.label || v); + return labels.join(", "); + } + return `${value.length}개 선택`; + }, [value, options, placeholder]); + + return ( + + + + + + {searchable && ( +
+ setKeyword(e.target.value)} className="h-7 text-xs" /> +
+ )} +
+ {filtered.length === 0 ? ( +
{emptyMessage}
+ ) : filtered.map(opt => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.length}개 선택됨 + +
+ )} +
+
+ ); } export default function WorkInstructionPage() { @@ -207,17 +291,22 @@ export default function WorkInstructionPage() { const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } + const today = new Date().toISOString().split("T")[0]; const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; - if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); - else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); + const baseExtra = { startDate: today, endDate: "", equipmentIds: [], workTeams: [], workers: [] } as Pick; + if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code, ...baseExtra }); + else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id, ...baseExtra }); else { // 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능) const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null ? Number(item.remain_qty) : Number(item.plan_qty || 1); - items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); + // 생산계획: 일정이 있으면 기본값으로 전달 + const planStart = item.start_date ? String(item.start_date).split("T")[0] : today; + const planEnd = item.end_date ? String(item.end_date).split("T")[0] : ""; + items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id, startDate: planStart, endDate: planEnd, equipmentIds: [], workTeams: [], workers: [] }); } } @@ -266,6 +355,9 @@ export default function WorkInstructionPage() { itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "", qty: Number(confirmAddQty), remark: "", sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "", + startDate: firstItem?.startDate || new Date().toISOString().split("T")[0], + endDate: firstItem?.endDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setConfirmAddQty(""); }; @@ -275,11 +367,29 @@ export default function WorkInstructionPage() { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값으로 (하위 호환 유지 — 조회 화면이 헤더값으로 표시되는 레거시 대비) + const first = confirmItems[0]; + const headerStart = first?.startDate || ""; + const headerEnd = first?.endDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || ""; + const headerWorkTeam = first?.workTeams?.[0] || ""; + const headerWorker = first?.workers?.[0] || ""; const payload = { - status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, - equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, + 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, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: confirmItems.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 — 다중값 쉼표 구분) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } @@ -302,6 +412,12 @@ export default function WorkInstructionPage() { sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", routing: d.detail_routing_version_id || order.routing_version_id || "", routingOptions: [], + // 품목별 일정/설비/작업조/작업자 (detail 값 우선, 없으면 헤더값 폴백) + startDate: d.detail_start_date || d.start_date || "", + endDate: d.detail_end_date || d.end_date || "", + equipmentIds: (d.detail_equipment_ids || "").split(",").filter(Boolean), + workTeams: (d.detail_work_teams || "").split(",").filter(Boolean), + workers: (d.detail_workers || "").split(",").filter(Boolean), })); setEditItems(items); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); @@ -332,9 +448,13 @@ export default function WorkInstructionPage() { const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } + const firstItem = editItems[0]; setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", + startDate: firstItem?.startDate || editStartDate || "", + endDate: firstItem?.endDate || editEndDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setAddQty(""); }; @@ -343,11 +463,30 @@ export default function WorkInstructionPage() { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값 사용 (하위 호환 — 등록 모달과 동일 패턴) + const first = editItems[0]; + const headerStart = first?.startDate || editStartDate || ""; + const headerEnd = first?.endDate || editEndDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || editEquipmentId || ""; + const headerWorkTeam = first?.workTeams?.[0] || editWorkTeam || ""; + const headerWorker = first?.workers?.[0] || editWorker || ""; const payload = { - id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, - equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, + id: editOrder.wi_id, status: editStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, + remark: editRemark, routing: editRouting || null, - items: editItems.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 })), + items: editItems.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, + // 품목별 일정/설비/작업조/작업자 (다중값 쉼표 구분 — 등록 모달과 동일) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } @@ -641,7 +780,7 @@ export default function WorkInstructionPage() { {/* ── 2단계: 확인 모달 ── */} - + 작업지시 적용 확인 기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요. @@ -650,38 +789,33 @@ export default function WorkInstructionPage() {

작업지시 기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setConfirmStartDate(e.target.value)} className="h-9" />
-
setConfirmEndDate(e.target.value)} className="h-9" />
-
- -
-
- -
-
- -

품목 목록

-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 비고 + 수량 + 라우팅 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 @@ -690,7 +824,7 @@ export default function WorkInstructionPage() { {idx + 1} {item.itemCode} - {item.itemName || item.itemCode} + {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -712,6 +846,40 @@ export default function WorkInstructionPage() { + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> @@ -732,7 +900,7 @@ export default function WorkInstructionPage() { {/* ── 수정 모달 ── */} { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}> - + {`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`} 품목을 추가/삭제하고 정보를 수정해주세요. @@ -741,48 +909,47 @@ export default function WorkInstructionPage() {

기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setEditStartDate(e.target.value)} className="h-9" />
-
setEditEndDate(e.target.value)} className="h-9" />
-
-
-
- -
-
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
+
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
- {/* 품목 테이블 — 라우팅/공정작업기준을 품목별로 표시 */} + {/* 품목 테이블 — 품목별 일정/설비/작업조/작업자 + 라우팅/공정작업기준 */}
작업지시 항목 {editItems.length}건
-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + 수량 + 라우팅 + 공정작업기준 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 {editItems.length === 0 ? ( - 등록된 품목이 없어요 + 등록된 품목이 없어요 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} - {item.itemName || "-"} + {item.itemName || "-"} {item.spec || "-"} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -825,6 +992,40 @@ export default function WorkInstructionPage() { 수정 + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> diff --git a/frontend/app/(main)/COMPANY_8/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_8/production/work-instruction/page.tsx index 9a6fc954..110b2b4d 100644 --- a/frontend/app/(main)/COMPANY_8/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_8/production/work-instruction/page.tsx @@ -59,6 +59,90 @@ interface SelectedItem { itemCode: string; itemName: string; spec: string; qty: number; remark: string; sourceType: SourceType; sourceTable: string; sourceId: string | number; routing?: string; routingOptions?: RoutingVersionData[]; + // 품목별 일정/설비/작업조/작업자 (옵션 A — 다중선택 지원) + startDate?: string; + endDate?: string; + equipmentIds?: string[]; + workTeams?: string[]; + workers?: string[]; +} + +// 공용 다중선택 Popover 컴포넌트 (설비/작업조/작업자에 재사용) +interface MultiSelectOption { value: string; label: string; sub?: string; } +interface MultiSelectPopoverProps { + options: MultiSelectOption[]; + value: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + searchable?: boolean; + triggerClassName?: string; + emptyMessage?: string; +} + +function MultiSelectPopover({ options, value, onChange, placeholder = "선택", searchable = false, triggerClassName, emptyMessage = "항목이 없어요" }: MultiSelectPopoverProps) { + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + + const selectedSet = useMemo(() => new Set(value), [value]); + const toggle = (val: string) => { + if (selectedSet.has(val)) onChange(value.filter(v => v !== val)); + else onChange([...value, val]); + }; + + const filtered = useMemo(() => { + if (!searchable || !keyword.trim()) return options; + const k = keyword.trim().toLowerCase(); + return options.filter(o => o.label.toLowerCase().includes(k) || (o.sub || "").toLowerCase().includes(k)); + }, [options, keyword, searchable]); + + const display = useMemo(() => { + if (value.length === 0) return placeholder; + if (value.length === 1) return options.find(o => o.value === value[0])?.label || value[0]; + if (value.length === 2) { + const labels = value.map(v => options.find(o => o.value === v)?.label || v); + return labels.join(", "); + } + return `${value.length}개 선택`; + }, [value, options, placeholder]); + + return ( + + + + + + {searchable && ( +
+ setKeyword(e.target.value)} className="h-7 text-xs" /> +
+ )} +
+ {filtered.length === 0 ? ( +
{emptyMessage}
+ ) : filtered.map(opt => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.length}개 선택됨 + +
+ )} +
+
+ ); } export default function WorkInstructionPage() { @@ -197,17 +281,22 @@ export default function WorkInstructionPage() { const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } + const today = new Date().toISOString().split("T")[0]; const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; - if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); - else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); + const baseExtra = { startDate: today, endDate: "", equipmentIds: [], workTeams: [], workers: [] } as Pick; + if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code, ...baseExtra }); + else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id, ...baseExtra }); else { // 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능) const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null ? Number(item.remain_qty) : Number(item.plan_qty || 1); - items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); + // 생산계획: 일정이 있으면 기본값으로 전달 + const planStart = item.start_date ? String(item.start_date).split("T")[0] : today; + const planEnd = item.end_date ? String(item.end_date).split("T")[0] : ""; + items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id, startDate: planStart, endDate: planEnd, equipmentIds: [], workTeams: [], workers: [] }); } } @@ -256,6 +345,9 @@ export default function WorkInstructionPage() { itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "", qty: Number(confirmAddQty), remark: "", sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "", + startDate: firstItem?.startDate || new Date().toISOString().split("T")[0], + endDate: firstItem?.endDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setConfirmAddQty(""); }; @@ -265,11 +357,29 @@ export default function WorkInstructionPage() { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값으로 (하위 호환 유지 — 조회 화면이 헤더값으로 표시되는 레거시 대비) + const first = confirmItems[0]; + const headerStart = first?.startDate || ""; + const headerEnd = first?.endDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || ""; + const headerWorkTeam = first?.workTeams?.[0] || ""; + const headerWorker = first?.workers?.[0] || ""; const payload = { - status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, - equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, + 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, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: confirmItems.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 — 다중값 쉼표 구분) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } @@ -292,6 +402,12 @@ export default function WorkInstructionPage() { sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", routing: d.detail_routing_version_id || order.routing_version_id || "", routingOptions: [], + // 품목별 일정/설비/작업조/작업자 (detail 값 우선, 없으면 헤더값 폴백) + startDate: d.detail_start_date || d.start_date || "", + endDate: d.detail_end_date || d.end_date || "", + equipmentIds: (d.detail_equipment_ids || "").split(",").filter(Boolean), + workTeams: (d.detail_work_teams || "").split(",").filter(Boolean), + workers: (d.detail_workers || "").split(",").filter(Boolean), })); setEditItems(items); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); @@ -322,9 +438,13 @@ export default function WorkInstructionPage() { const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } + const firstItem = editItems[0]; setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", + startDate: firstItem?.startDate || editStartDate || "", + endDate: firstItem?.endDate || editEndDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setAddQty(""); }; @@ -333,11 +453,30 @@ export default function WorkInstructionPage() { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값 사용 (하위 호환 — 등록 모달과 동일 패턴) + const first = editItems[0]; + const headerStart = first?.startDate || editStartDate || ""; + const headerEnd = first?.endDate || editEndDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || editEquipmentId || ""; + const headerWorkTeam = first?.workTeams?.[0] || editWorkTeam || ""; + const headerWorker = first?.workers?.[0] || editWorker || ""; const payload = { - id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, - equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, + id: editOrder.wi_id, status: editStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, + remark: editRemark, routing: editRouting || null, - items: editItems.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 })), + items: editItems.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, + // 품목별 일정/설비/작업조/작업자 (다중값 쉼표 구분 — 등록 모달과 동일) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } @@ -625,7 +764,7 @@ export default function WorkInstructionPage() { {/* ── 2단계: 확인 모달 ── */} - + 작업지시 적용 확인 기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요. @@ -634,38 +773,33 @@ export default function WorkInstructionPage() {

작업지시 기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setConfirmStartDate(e.target.value)} className="h-9" />
-
setConfirmEndDate(e.target.value)} className="h-9" />
-
- -
-
- -
-
- -

품목 목록

-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 비고 + 수량 + 라우팅 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 @@ -674,7 +808,7 @@ export default function WorkInstructionPage() { {idx + 1} {item.itemCode} - {item.itemName || item.itemCode} + {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -696,6 +830,40 @@ export default function WorkInstructionPage() { + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> @@ -716,7 +884,7 @@ export default function WorkInstructionPage() { {/* ── 수정 모달 ── */} { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}> - + {`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`} 품목을 추가/삭제하고 정보를 수정해주세요. @@ -725,48 +893,47 @@ export default function WorkInstructionPage() {

기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setEditStartDate(e.target.value)} className="h-9" />
-
setEditEndDate(e.target.value)} className="h-9" />
-
-
-
- -
-
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
+
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
- {/* 품목 테이블 — 라우팅/공정작업기준을 품목별로 표시 */} + {/* 품목 테이블 — 품목별 일정/설비/작업조/작업자 + 라우팅/공정작업기준 */}
작업지시 항목 {editItems.length}건
-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + 수량 + 라우팅 + 공정작업기준 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 {editItems.length === 0 ? ( - 등록된 품목이 없어요 + 등록된 품목이 없어요 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} - {item.itemName || "-"} + {item.itemName || "-"} {item.spec || "-"} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -809,6 +976,40 @@ export default function WorkInstructionPage() { 수정 + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> diff --git a/frontend/app/(main)/COMPANY_9/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_9/production/work-instruction/page.tsx index 9a6fc954..110b2b4d 100644 --- a/frontend/app/(main)/COMPANY_9/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_9/production/work-instruction/page.tsx @@ -59,6 +59,90 @@ interface SelectedItem { itemCode: string; itemName: string; spec: string; qty: number; remark: string; sourceType: SourceType; sourceTable: string; sourceId: string | number; routing?: string; routingOptions?: RoutingVersionData[]; + // 품목별 일정/설비/작업조/작업자 (옵션 A — 다중선택 지원) + startDate?: string; + endDate?: string; + equipmentIds?: string[]; + workTeams?: string[]; + workers?: string[]; +} + +// 공용 다중선택 Popover 컴포넌트 (설비/작업조/작업자에 재사용) +interface MultiSelectOption { value: string; label: string; sub?: string; } +interface MultiSelectPopoverProps { + options: MultiSelectOption[]; + value: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + searchable?: boolean; + triggerClassName?: string; + emptyMessage?: string; +} + +function MultiSelectPopover({ options, value, onChange, placeholder = "선택", searchable = false, triggerClassName, emptyMessage = "항목이 없어요" }: MultiSelectPopoverProps) { + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + + const selectedSet = useMemo(() => new Set(value), [value]); + const toggle = (val: string) => { + if (selectedSet.has(val)) onChange(value.filter(v => v !== val)); + else onChange([...value, val]); + }; + + const filtered = useMemo(() => { + if (!searchable || !keyword.trim()) return options; + const k = keyword.trim().toLowerCase(); + return options.filter(o => o.label.toLowerCase().includes(k) || (o.sub || "").toLowerCase().includes(k)); + }, [options, keyword, searchable]); + + const display = useMemo(() => { + if (value.length === 0) return placeholder; + if (value.length === 1) return options.find(o => o.value === value[0])?.label || value[0]; + if (value.length === 2) { + const labels = value.map(v => options.find(o => o.value === v)?.label || v); + return labels.join(", "); + } + return `${value.length}개 선택`; + }, [value, options, placeholder]); + + return ( + + + + + + {searchable && ( +
+ setKeyword(e.target.value)} className="h-7 text-xs" /> +
+ )} +
+ {filtered.length === 0 ? ( +
{emptyMessage}
+ ) : filtered.map(opt => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.length}개 선택됨 + +
+ )} +
+
+ ); } export default function WorkInstructionPage() { @@ -197,17 +281,22 @@ export default function WorkInstructionPage() { const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } + const today = new Date().toISOString().split("T")[0]; const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; - if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); - else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); + const baseExtra = { startDate: today, endDate: "", equipmentIds: [], workTeams: [], workers: [] } as Pick; + if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code, ...baseExtra }); + else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id, ...baseExtra }); else { // 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능) const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null ? Number(item.remain_qty) : Number(item.plan_qty || 1); - items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); + // 생산계획: 일정이 있으면 기본값으로 전달 + const planStart = item.start_date ? String(item.start_date).split("T")[0] : today; + const planEnd = item.end_date ? String(item.end_date).split("T")[0] : ""; + items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id, startDate: planStart, endDate: planEnd, equipmentIds: [], workTeams: [], workers: [] }); } } @@ -256,6 +345,9 @@ export default function WorkInstructionPage() { itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "", qty: Number(confirmAddQty), remark: "", sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "", + startDate: firstItem?.startDate || new Date().toISOString().split("T")[0], + endDate: firstItem?.endDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setConfirmAddQty(""); }; @@ -265,11 +357,29 @@ export default function WorkInstructionPage() { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값으로 (하위 호환 유지 — 조회 화면이 헤더값으로 표시되는 레거시 대비) + const first = confirmItems[0]; + const headerStart = first?.startDate || ""; + const headerEnd = first?.endDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || ""; + const headerWorkTeam = first?.workTeams?.[0] || ""; + const headerWorker = first?.workers?.[0] || ""; const payload = { - status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, - equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, + 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, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: confirmItems.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 — 다중값 쉼표 구분) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } @@ -292,6 +402,12 @@ export default function WorkInstructionPage() { sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", routing: d.detail_routing_version_id || order.routing_version_id || "", routingOptions: [], + // 품목별 일정/설비/작업조/작업자 (detail 값 우선, 없으면 헤더값 폴백) + startDate: d.detail_start_date || d.start_date || "", + endDate: d.detail_end_date || d.end_date || "", + equipmentIds: (d.detail_equipment_ids || "").split(",").filter(Boolean), + workTeams: (d.detail_work_teams || "").split(",").filter(Boolean), + workers: (d.detail_workers || "").split(",").filter(Boolean), })); setEditItems(items); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); @@ -322,9 +438,13 @@ export default function WorkInstructionPage() { const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } + const firstItem = editItems[0]; setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", + startDate: firstItem?.startDate || editStartDate || "", + endDate: firstItem?.endDate || editEndDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setAddQty(""); }; @@ -333,11 +453,30 @@ export default function WorkInstructionPage() { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값 사용 (하위 호환 — 등록 모달과 동일 패턴) + const first = editItems[0]; + const headerStart = first?.startDate || editStartDate || ""; + const headerEnd = first?.endDate || editEndDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || editEquipmentId || ""; + const headerWorkTeam = first?.workTeams?.[0] || editWorkTeam || ""; + const headerWorker = first?.workers?.[0] || editWorker || ""; const payload = { - id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, - equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, + id: editOrder.wi_id, status: editStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, + remark: editRemark, routing: editRouting || null, - items: editItems.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 })), + items: editItems.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, + // 품목별 일정/설비/작업조/작업자 (다중값 쉼표 구분 — 등록 모달과 동일) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } @@ -625,7 +764,7 @@ export default function WorkInstructionPage() { {/* ── 2단계: 확인 모달 ── */} - + 작업지시 적용 확인 기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요. @@ -634,38 +773,33 @@ export default function WorkInstructionPage() {

작업지시 기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setConfirmStartDate(e.target.value)} className="h-9" />
-
setConfirmEndDate(e.target.value)} className="h-9" />
-
- -
-
- -
-
- -

품목 목록

-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 비고 + 수량 + 라우팅 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 @@ -674,7 +808,7 @@ export default function WorkInstructionPage() { {idx + 1} {item.itemCode} - {item.itemName || item.itemCode} + {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -696,6 +830,40 @@ export default function WorkInstructionPage() { + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> @@ -716,7 +884,7 @@ export default function WorkInstructionPage() { {/* ── 수정 모달 ── */} { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}> - + {`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`} 품목을 추가/삭제하고 정보를 수정해주세요. @@ -725,48 +893,47 @@ export default function WorkInstructionPage() {

기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setEditStartDate(e.target.value)} className="h-9" />
-
setEditEndDate(e.target.value)} className="h-9" />
-
-
-
- -
-
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
+
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
- {/* 품목 테이블 — 라우팅/공정작업기준을 품목별로 표시 */} + {/* 품목 테이블 — 품목별 일정/설비/작업조/작업자 + 라우팅/공정작업기준 */}
작업지시 항목 {editItems.length}건
-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + 수량 + 라우팅 + 공정작업기준 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 {editItems.length === 0 ? ( - 등록된 품목이 없어요 + 등록된 품목이 없어요 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} - {item.itemName || "-"} + {item.itemName || "-"} {item.spec || "-"} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -809,6 +976,40 @@ export default function WorkInstructionPage() { 수정 + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} />