diff --git a/backend-node/src/controllers/outboundController.ts b/backend-node/src/controllers/outboundController.ts
index 93940ebb..9d4fa035 100644
--- a/backend-node/src/controllers/outboundController.ts
+++ b/backend-node/src/controllers/outboundController.ts
@@ -85,11 +85,12 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
NULLIF(si.delivery_address, ''),
''
) AS delivery_destination_name,
- -- 거래처명 (TASK:ERP-098 항목20): 출고 행 customer_name 우선,
- -- 없으면 customer_mng JOIN, 매핑 깨지면 코드 fallback
+ -- 거래처명 (TASK:ERP-098 항목20): customer_mng JOIN 라벨을 최우선.
+ -- 출고 생성 시 om.customer_name에 거래처 코드가 저장되는 경우가 있어
+ -- JOIN 라벨 → 저장값 → 코드 순으로 fallback
COALESCE(
- NULLIF(om.customer_name, ''),
NULLIF(c.customer_name, ''),
+ NULLIF(om.customer_name, ''),
NULLIF(om.customer_code, ''),
''
) AS customer_name,
diff --git a/frontend/app/(main)/COMPANY_9/production/cutting-plan/WorkInstructionApplyModal.tsx b/frontend/app/(main)/COMPANY_9/production/cutting-plan/WorkInstructionApplyModal.tsx
index b578e341..901148ad 100644
--- a/frontend/app/(main)/COMPANY_9/production/cutting-plan/WorkInstructionApplyModal.tsx
+++ b/frontend/app/(main)/COMPANY_9/production/cutting-plan/WorkInstructionApplyModal.tsx
@@ -28,9 +28,12 @@ import {
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
+import { SmartSelect } from "@/components/common/SmartSelect";
+
import {
previewWorkInstructionNo, saveWorkInstruction,
getEquipmentList, getEmployeeList, getRoutingVersions,
+ RoutingVersionData,
} from "@/lib/api/workInstruction";
// ─── 공용 다중선택 Popover (설비/작업조/작업자) ────────────────────
@@ -128,6 +131,9 @@ export interface WorkInstructionApplyItem {
equipmentIds?: string[];
workTeams?: string[];
workers?: string[];
+ // 품목별 라우팅 (작업지시 등록창과 동일하게 직접 선택)
+ routing?: string;
+ routingOptions?: RoutingVersionData[];
}
export interface WorkInstructionApplyModalProps {
@@ -173,14 +179,26 @@ export default function WorkInstructionApplyModal({
getEquipmentList().then((r) => { if (r.success) setEquipmentOptions(r.data || []); }).catch(() => {});
getEmployeeList().then((r) => { if (r.success) setWorkerOptions(r.data || []); }).catch(() => {});
- // 품목별 라우팅 등록 여부 사전 조회 (엣지 1 — 라우팅 미등록 안내).
- // 라우팅 세팅 자체는 백엔드 save 가 자동 처리하므로 여기서는 안내 표시용으로만 사용.
+ // 품목별 라우팅 버전 사전 조회 — 작업지시 등록창과 동일 패턴(getRoutingVersions("__new__", itemCode)).
+ // 조회 결과를 각 행의 routingOptions 에 채우고 is_default 버전을 초기 선택값으로 세팅.
+ // routingStatus 는 라우팅 미등록 안내 배너용(엣지 1).
const uniqueCodes = [...new Set(initialItems.map((x) => x.itemCode).filter(Boolean))];
for (const code of uniqueCodes) {
getRoutingVersions("__new__", code)
.then((r) => {
const has = !!(r.success && r.data && r.data.length > 0);
setRoutingStatus((prev) => ({ ...prev, [code]: has }));
+ if (r.success && r.data) {
+ // 작업지시 등록창과 동일: is_default 버전을 기본 선택, 없으면 빈값
+ const defaultRv = r.data.find((rv) => rv.is_default);
+ setItems((prev) =>
+ prev.map((it) =>
+ it.itemCode === code
+ ? { ...it, routingOptions: r.data, routing: defaultRv?.id || "" }
+ : it,
+ ),
+ );
+ }
})
.catch(() => {
// 조회 실패 시 안내를 띄우지 않음(미등록으로 단정하지 않음)
@@ -218,7 +236,8 @@ export default function WorkInstructionApplyModal({
qty: String(i.qty), remark: i.remark || "",
sourceTable: i.sourceTable || "cutting_plan",
sourceId: i.sourceId || (cuttingPlanId != null ? String(cuttingPlanId) : ""),
- routing: null,
+ // 사용자가 선택한 라우팅 버전. 빈값이면 백엔드 save 가 기본 버전을 자동 resolve(104).
+ routing: i.routing || null,
startDate: i.startDate || "",
endDate: i.endDate || "",
equipmentIds: (i.equipmentIds || []).join(","),
@@ -295,7 +314,7 @@ export default function WorkInstructionApplyModal({
품목 목록
-
+
순번
@@ -304,6 +323,7 @@ export default function WorkInstructionApplyModal({
품목명
규격
수량
+ 라우팅
시작일
완료예정일
설비
@@ -328,6 +348,24 @@ export default function WorkInstructionApplyModal({
value={item.qty}
onChange={(e) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} />
+
+ {/* 작업지시 등록창과 동일하게 품목별 라우팅 버전 직접 선택. 옵션 0건이면 비활성. */}
+ {(item.routingOptions || []).length > 0 ? (
+
+ setItems((prev) => prev.map((it, i) => i === idx ? { ...it, routing: v } : it))}
+ options={(item.routingOptions || []).map((rv) => ({
+ code: rv.id,
+ label: `${rv.version_name || "라우팅"}${rv.is_default ? " (기본)" : ""} - ${rv.processes.length}공정`,
+ }))}
+ placeholder="라우팅 선택"
+ className="h-7 text-[13px]"
+ />
+ ) : (
+ 라우팅 미등록
+ )}
+
-
+
품목이 없습니다
diff --git a/frontend/app/(main)/COMPANY_9/production/cutting-plan/page.tsx b/frontend/app/(main)/COMPANY_9/production/cutting-plan/page.tsx
index 5d84fefc..620398c1 100644
--- a/frontend/app/(main)/COMPANY_9/production/cutting-plan/page.tsx
+++ b/frontend/app/(main)/COMPANY_9/production/cutting-plan/page.tsx
@@ -24,6 +24,7 @@ import {
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
+import { SmartSelect } from "@/components/common/SmartSelect";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
@@ -1454,17 +1455,19 @@ export default function CuttingPlanPage() {
{/* 원자재 선택 */}
①
-
+ {materials.length === 0 ? (
+
+ 등록된 원자재가 없습니다
+
+ ) : (
+
({ code: String(m.id), label: m.name }))}
+ placeholder="-- 원자재 선택 --"
+ className="h-7 w-[220px] text-xs flex-none"
+ />
+ )}
{mat1 && (
@@ -1480,16 +1483,19 @@ export default function CuttingPlanPage() {
{showMat2 ? (
<>
②
-
+ {materials.length === 0 ? (
+
+ 등록된 원자재가 없습니다
+
+ ) : (
+ ({ code: String(m.id), label: m.name }))}
+ placeholder="-- 선택 --"
+ className="h-7 w-[180px] text-[11px] flex-none border-violet-300"
+ />
+ )}
{mat2 && (
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 7cb6ab2e..b6c8c667 100644
--- a/frontend/app/(main)/COMPANY_9/production/work-instruction/page.tsx
+++ b/frontend/app/(main)/COMPANY_9/production/work-instruction/page.tsx
@@ -800,21 +800,21 @@ export default function WorkInstructionPage() {
품목 목록
-
+
순번
- 품목코드
- 품목명
- 규격
- 수량
- 라우팅
- 시작일
- 완료예정일
- 설비
- 작업조
- 작업자
- 비고
+ 품목코드
+ 품목명
+ 규격
+ 수량
+ 라우팅
+ 시작일
+ 완료예정일
+ 설비
+ 작업조
+ 작업자
+ 비고
@@ -823,9 +823,9 @@ 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))} />
+ setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} />
- setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
+ 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))} />
+ setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
- setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} />
+ setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} />
))}
@@ -924,25 +924,25 @@ export default function WorkInstructionPage() {
{editItems.length}건
-
+
순번
{editOrder?.batch_no ? (
배치번호
) : null}
- 품목코드
- 품목명
- 규격
- 수량
- 라우팅
- 공정작업기준
- 시작일
- 완료예정일
- 설비
- 작업조
- 작업자
- 비고
+ 품목코드
+ 품목명
+ 규격
+ 수량
+ 라우팅
+ 공정작업기준
+ 시작일
+ 완료예정일
+ 설비
+ 작업조
+ 작업자
+ 비고
@@ -956,9 +956,9 @@ export default function WorkInstructionPage() {
{editOrder.batch_no}
) : null}
{item.itemCode}
- {item.itemName || "-"}
+ {item.itemName || "-"}
{item.spec || "-"}
- setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} />
+ setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} />
- setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
+ 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))} />
+ setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
- setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} />
+ setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} />
))}
diff --git a/frontend/app/(main)/COMPANY_9/sales/order/page.tsx b/frontend/app/(main)/COMPANY_9/sales/order/page.tsx
index 7513b377..152a82b4 100644
--- a/frontend/app/(main)/COMPANY_9/sales/order/page.tsx
+++ b/frontend/app/(main)/COMPANY_9/sales/order/page.tsx
@@ -429,7 +429,9 @@ export default function JeilGlassOrderPage() {
if (!previewOrderNo) {
previewOrderNo = `ORD-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${String(Date.now()).slice(-4)}`;
}
- setMasterForm({ order_no: previewOrderNo, manager_id: user?.userId || "" });
+ // status는 화면 Select 기본 표시값("수주")과 실제 저장값을 일치시킨다.
+ // 비우면 백엔드가 'WAITING'으로 보정 → 출하계획 일괄 등록 가드에 차단됨.
+ setMasterForm({ order_no: previewOrderNo, manager_id: user?.userId || "", status: "수주" });
setModalDetailRows([]);
setSelectedDestId("");
setDestinations([]);
@@ -1231,22 +1233,22 @@ export default function JeilGlassOrderPage() {
- No
- 구분
- 품명
- 규격
- 가로
- 세로
- 두께
- 면적(㎡)
- 단위
- 포장재
- 수량
- 포장수량
- 단가
- 금액
- 납기일
- 납품장소
+ No
+ 구분
+ 품명
+ 규격
+ 가로
+ 세로
+ 두께
+ 면적(㎡)
+ 단위
+ 포장재
+ 수량
+ 포장수량
+ 단가
+ 금액
+ 납기일
+ 납품장소
@@ -1293,7 +1295,7 @@ export default function JeilGlassOrderPage() {
{row.part_name || "-"}
) : (
updateDetailRow(idx, "part_name", e.target.value)}
- className="h-8 text-sm" placeholder="품명" />
+ className="h-9 text-sm w-full" placeholder="품명" />
)}
{/* 규격: 품목검색 → 읽기전용, 행추가 → 입력 */}
@@ -1302,20 +1304,20 @@ export default function JeilGlassOrderPage() {
{row.spec || "-"}
) : (
updateDetailRow(idx, "spec", e.target.value)}
- className="h-8 text-sm" placeholder="규격" />
+ className="h-9 text-sm w-full" placeholder="규격" />
)}
updateDetailRow(idx, "width", parseNumber(e.target.value))}
- className="h-8 text-sm text-right" placeholder="mm" />
+ className="h-9 text-sm text-right w-full" placeholder="mm" />
updateDetailRow(idx, "height", parseNumber(e.target.value))}
- className="h-8 text-sm text-right" placeholder="mm" />
+ className="h-9 text-sm text-right w-full" placeholder="mm" />
updateDetailRow(idx, "thickness", e.target.value)}
- className="h-8 text-sm text-right" placeholder="mm" />
+ className="h-9 text-sm text-right w-full" placeholder="mm" />
{row.area || "-"}
@@ -1326,14 +1328,14 @@ export default function JeilGlassOrderPage() {
{row.unit || "-"}
) : (
updateDetailRow(idx, "unit", e.target.value)}
- className="h-8 text-sm" placeholder="㎡" />
+ className="h-9 text-sm w-full" placeholder="㎡" />
)}
{/* 포장재: 등록된 옵션이 있으면 셀렉트, 없으면 안내 */}
{(row.pkg_options && row.pkg_options.length > 0) ? (
updateDetailRow(idx, "qty", parseNumber(e.target.value))}
- className="h-8 text-sm text-right" />
+ className="h-9 text-sm text-right w-full" placeholder="수량" />
updateDetailRow(idx, "pack_count", e.target.value)}
- className="h-8 text-sm text-right font-mono" disabled={!row.pkg_code} />
+ className="h-9 text-sm text-right font-mono w-full" disabled={!row.pkg_code} />
updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
- className="h-8 text-sm text-right" />
+ className="h-9 text-sm text-right w-full" placeholder="단가" />
{row.amount ? Number(row.amount).toLocaleString() : ""}
@@ -1366,7 +1368,7 @@ export default function JeilGlassOrderPage() {
updateDetailRow(idx, "delivery_location", e.target.value)}
- className="h-8 text-sm" placeholder="납품장소" />
+ className="h-9 text-sm w-full" placeholder="납품장소" />
))}
diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx
index 525697d8..ace1a075 100644
--- a/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx
+++ b/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx
@@ -147,6 +147,8 @@ export function DetailFormModal({
const [equipInspChecked, setEquipInspChecked] = useState>(new Set());
// 공정에 지정된 설비 건수 — 0건(설비 미지정) vs 점검항목 0건 메시지 구분용 — TASK:ERP-102
const [equipProcEquipCount, setEquipProcEquipCount] = useState(0);
+ // 설비 점검항목의 점검방법 카테고리 옵션 (코드→라벨 변환용) — TASK:ERP-102 후속
+ const [equipMethodOptions, setEquipMethodOptions] = useState([]);
// 공정 설비 목록 (자재투입 자재별 설비 연결용) — TASK:ERP-022
const [processEquipments, setProcessEquipments] = useState([]);
@@ -435,6 +437,34 @@ export function DetailFormModal({
})();
}, [open, formData.detail_type, formData.equip_inspection_apply, selectedProcessCode]);
+ // 설비 점검항목의 점검방법 카테고리 옵션 로드 — TASK:ERP-102 후속
+ // equipment_inspection_item.inspection_method 가 카테고리 코드(CAT_...)로 저장됨 → 라벨 변환용
+ useEffect(() => {
+ if (!open || formData.detail_type !== "equip_inspection") return;
+ if (equipMethodOptions.length) return;
+ const flatten = (arr: any[]): CatOption[] => {
+ const out: CatOption[] = [];
+ const walk = (list: any[]) => {
+ for (const v of list || []) {
+ out.push({ code: v.valueCode, label: v.valueLabel });
+ if (v.children?.length) walk(v.children);
+ }
+ };
+ walk(arr || []);
+ return out;
+ };
+ (async () => {
+ try {
+ const res = await apiClient.get(
+ "/table-categories/equipment_inspection_item/inspection_method/values"
+ );
+ if (res.data?.data?.length) setEquipMethodOptions(flatten(res.data.data));
+ } catch {
+ /* 카테고리 미정의 시 빈 옵션 — 코드 fallback 으로 표시 */
+ }
+ })();
+ }, [open, formData.detail_type, equipMethodOptions.length]);
+
// 검사항목 미적용(수동입력) 경로 셀렉트용 카테고리 옵션 로드 — TASK:ERP-061
// 품목검사정보 화면(quality/item-inspection)과 동일 출처(inspection_standard) 사용
useEffect(() => {
@@ -1262,7 +1292,8 @@ export function DetailFormModal({
| {item.equipment_name || item.equipment_code || "-"} |
{item.inspection_item || "-"} |
- {item.inspection_method || "-"} |
+ {/* 점검방법은 카테고리 코드(CAT_...)로 저장 → 라벨 변환, 매핑 없으면 코드 fallback — TASK:ERP-102 후속 */}
+ {resolveCat(equipMethodOptions, item.inspection_method) || "-"} |
{item.lower_limit || item.upper_limit ? `${item.lower_limit || ""} ~ ${item.upper_limit || ""}${item.unit ? ` ${item.unit}` : ""}` : "-"} |
);