Enhance Cutting Plan and Work Instruction Functionality

- Integrated `SmartSelect` component for improved material selection in the cutting plan page, providing a better user experience when no materials are available.
- Updated the work instruction modal to include routing options directly selectable by users, enhancing the flexibility of work instruction management.
- Adjusted table layouts across various components to accommodate new data fields and improve overall UI consistency.

(TASK: ERP-XXX)
This commit is contained in:
kjs
2026-05-22 13:33:16 +09:00
parent 9f9be20e34
commit e5e7a5c261
6 changed files with 175 additions and 97 deletions

View File

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

View File

@@ -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({
<div className="border rounded-lg p-5">
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground mb-3"> </h4>
<div className="overflow-auto">
<Table className="min-w-[1700px]">
<Table className="min-w-[1900px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
@@ -304,6 +323,7 @@ export default function WorkInstructionApplyModal({
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
@@ -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))} />
</TableCell>
<TableCell>
{/* 작업지시 등록창과 동일하게 품목별 라우팅 버전 직접 선택. 옵션 0건이면 비활성. */}
{(item.routingOptions || []).length > 0 ? (
<SmartSelect
value={item.routing || ""}
onValueChange={(v) =>
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]"
/>
) : (
<span className="text-[12px] text-muted-foreground"> </span>
)}
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]"
value={item.startDate || ""}
@@ -381,7 +419,7 @@ export default function WorkInstructionApplyModal({
))}
{items.length === 0 && (
<TableRow>
<TableCell colSpan={13} className="text-center text-muted-foreground text-[12px] py-6">
<TableCell colSpan={14} className="text-center text-muted-foreground text-[12px] py-6">
</TableCell>
</TableRow>

View File

@@ -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() {
{/* 원자재 선택 */}
<div className="flex flex-wrap items-center gap-2 border-b px-3 py-2">
<span className="inline-flex items-center justify-center h-6 w-6 rounded bg-primary/10 text-primary font-bold text-xs"></span>
<Select value={mat1Id} onValueChange={setMat1Id}>
<SelectTrigger className="h-7 w-[220px] text-xs flex-none">
<SelectValue placeholder="-- 원자재 선택 --" />
</SelectTrigger>
<SelectContent>
{materials.map((m) => (
<SelectItem key={String(m.id)} value={String(m.id)}>{m.name}</SelectItem>
))}
{materials.length === 0 && <div className="px-2 py-4 text-center text-xs text-muted-foreground"> </div>}
</SelectContent>
</Select>
{materials.length === 0 ? (
<div className="flex h-7 w-[220px] flex-none items-center rounded border bg-muted/30 px-2 text-xs text-muted-foreground">
</div>
) : (
<SmartSelect
value={mat1Id}
onValueChange={setMat1Id}
options={materials.map((m) => ({ code: String(m.id), label: m.name }))}
placeholder="-- 원자재 선택 --"
className="h-7 w-[220px] text-xs flex-none"
/>
)}
{mat1 && (
<div className="flex items-center gap-1.5 rounded bg-primary/5 border border-primary/20 px-1.5 py-0.5 text-[11px]">
<span className="font-semibold text-foreground">
@@ -1480,16 +1483,19 @@ export default function CuttingPlanPage() {
{showMat2 ? (
<>
<span className="inline-flex items-center justify-center h-6 w-6 rounded bg-violet-100 text-violet-600 font-bold text-xs"></span>
<Select value={mat2Id} onValueChange={setMat2Id}>
<SelectTrigger className="h-7 w-[180px] text-[11px] flex-none border-violet-300">
<SelectValue placeholder="-- 선택 --" />
</SelectTrigger>
<SelectContent>
{materials.map((m) => (
<SelectItem key={String(m.id)} value={String(m.id)}>{m.name}</SelectItem>
))}
</SelectContent>
</Select>
{materials.length === 0 ? (
<div className="flex h-7 w-[180px] flex-none items-center rounded border bg-muted/30 px-2 text-[11px] text-muted-foreground">
</div>
) : (
<SmartSelect
value={mat2Id}
onValueChange={setMat2Id}
options={materials.map((m) => ({ code: String(m.id), label: m.name }))}
placeholder="-- 선택 --"
className="h-7 w-[180px] text-[11px] flex-none border-violet-300"
/>
)}
{mat2 && (
<div className="flex items-center gap-1 rounded bg-violet-50 border border-violet-200 px-1.5 py-0.5 text-[11px]">
<span className="font-semibold text-foreground">

View File

@@ -800,21 +800,21 @@ export default function WorkInstructionPage() {
<div className="border rounded-lg p-5">
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground mb-3"> </h4>
<div className="overflow-auto">
<Table className="min-w-[1500px]">
<Table className="min-w-[2050px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[40px]" />
</TableRow>
</TableHeader>
@@ -823,9 +823,9 @@ export default function WorkInstructionPage() {
<TableRow key={idx}>
<TableCell className="text-[13px] text-center">{idx + 1}</TableCell>
<TableCell className="text-[13px] font-medium">{item.itemCode}</TableCell>
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName || item.itemCode}>{item.itemName || item.itemCode}</TableCell>
<TableCell className="text-sm truncate max-w-[180px]" title={item.itemName || item.itemCode}>{item.itemName || item.itemCode}</TableCell>
<TableCell className="text-[13px]">{item.spec || "-"}</TableCell>
<TableCell><Input type="number" className="h-7 text-[13px] w-20" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell><Input type="number" className="h-9 text-sm w-full min-w-[100px]" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell>
<Select
value={nv(item.routing || "")}
@@ -834,7 +834,7 @@ export default function WorkInstructionPage() {
setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, routing: val } : it));
}}
>
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
{(item.routingOptions || []).map((rv: RoutingVersionData) => (
@@ -846,10 +846,10 @@ export default function WorkInstructionPage() {
</Select>
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.startDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.startDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.endDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.endDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<MultiSelectPopover
@@ -879,7 +879,7 @@ export default function WorkInstructionPage() {
emptyMessage="사원을 찾을 수 없어요"
/>
</TableCell>
<TableCell><Input className="h-7 text-[13px]" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Input className="h-9 text-sm w-full min-w-[160px]" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setConfirmItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
</TableRow>
))}
@@ -924,25 +924,25 @@ export default function WorkInstructionPage() {
<span className="text-[11px] font-semibold text-primary bg-primary/8 border border-primary/15 px-2 py-0.5 rounded-full font-mono">{editItems.length}</span>
</div>
<div className="overflow-auto">
<Table className="min-w-[1500px]">
<Table className="min-w-[2200px]">
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
{editOrder?.batch_no ? (
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
) : null}
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[40px]" />
</TableRow>
</TableHeader>
@@ -956,9 +956,9 @@ export default function WorkInstructionPage() {
<TableCell className="text-[13px] font-mono text-primary">{editOrder.batch_no}</TableCell>
) : null}
<TableCell className="text-[13px] font-medium">{item.itemCode}</TableCell>
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName}>{item.itemName || "-"}</TableCell>
<TableCell className="text-sm truncate max-w-[180px]" title={item.itemName}>{item.itemName || "-"}</TableCell>
<TableCell className="text-[13px] truncate" title={item.spec}>{item.spec || "-"}</TableCell>
<TableCell className="text-right"><Input type="number" className="h-7 text-[13px] w-20 ml-auto" value={item.qty} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell className="text-right"><Input type="number" className="h-9 text-sm w-full min-w-[100px] ml-auto" value={item.qty} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell>
<Select
value={nv(item.routing || "")}
@@ -967,7 +967,7 @@ export default function WorkInstructionPage() {
setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, routing: val } : it));
}}
>
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="라우팅" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
{(item.routingOptions || []).map((rv: RoutingVersionData) => (
@@ -982,7 +982,7 @@ export default function WorkInstructionPage() {
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
className="h-9 text-sm"
disabled={!item.routing}
onClick={() => {
if (!editOrder || !item.routing) return;
@@ -996,14 +996,14 @@ export default function WorkInstructionPage() {
);
}}
>
<ClipboardCheck className="w-3 h-3 mr-1" />
<ClipboardCheck className="w-3.5 h-3.5 mr-1" />
</Button>
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.startDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.startDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<Input type="date" className="h-7 text-[13px]" value={item.endDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.endDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
</TableCell>
<TableCell>
<MultiSelectPopover
@@ -1033,7 +1033,7 @@ export default function WorkInstructionPage() {
emptyMessage="사원을 찾을 수 없어요"
/>
</TableCell>
<TableCell><Input className="h-7 text-[13px]" value={item.remark} placeholder="비고" onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Input className="h-9 text-sm w-full min-w-[160px]" value={item.remark} placeholder="비고" onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
</TableRow>
))}

View File

@@ -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() {
<TableRow>
<TableHead className="w-[30px]"></TableHead>
<TableHead className="w-[36px]"></TableHead>
<TableHead className="w-[40px]">No</TableHead>
<TableHead className="min-w-[90px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[80px]">()</TableHead>
<TableHead className="min-w-[60px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[90px]"></TableHead>
<TableHead className="min-w-[90px]"></TableHead>
<TableHead className="min-w-[90px]"></TableHead>
<TableHead className="min-w-[140px]"></TableHead>
<TableHead className="min-w-[160px]"></TableHead>
<TableHead className="w-[44px]">No</TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[240px]"></TableHead>
<TableHead className="min-w-[180px]"></TableHead>
<TableHead className="min-w-[110px]"></TableHead>
<TableHead className="min-w-[110px]"></TableHead>
<TableHead className="min-w-[110px]"></TableHead>
<TableHead className="min-w-[100px]">()</TableHead>
<TableHead className="min-w-[90px]"></TableHead>
<TableHead className="min-w-[170px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[130px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[160px]"></TableHead>
<TableHead className="min-w-[220px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1293,7 +1295,7 @@ export default function JeilGlassOrderPage() {
<span className="text-sm px-2">{row.part_name || "-"}</span>
) : (
<Input value={row.part_name || ""} onChange={(e) => updateDetailRow(idx, "part_name", e.target.value)}
className="h-8 text-sm" placeholder="품명" />
className="h-9 text-sm w-full" placeholder="품명" />
)}
</TableCell>
{/* 규격: 품목검색 → 읽기전용, 행추가 → 입력 */}
@@ -1302,20 +1304,20 @@ export default function JeilGlassOrderPage() {
<span className="text-sm px-2">{row.spec || "-"}</span>
) : (
<Input value={row.spec || ""} onChange={(e) => updateDetailRow(idx, "spec", e.target.value)}
className="h-8 text-sm" placeholder="규격" />
className="h-9 text-sm w-full" placeholder="규격" />
)}
</TableCell>
<TableCell>
<Input value={formatNumber(row.width || "")} onChange={(e) => 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" />
</TableCell>
<TableCell>
<Input value={formatNumber(row.height || "")} onChange={(e) => 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" />
</TableCell>
<TableCell>
<Input value={row.thickness || ""} onChange={(e) => 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" />
</TableCell>
<TableCell className="text-sm text-right text-muted-foreground">
{row.area || "-"}
@@ -1326,14 +1328,14 @@ export default function JeilGlassOrderPage() {
<span className="text-sm px-2">{row.unit || "-"}</span>
) : (
<Input value={row.unit || ""} onChange={(e) => updateDetailRow(idx, "unit", e.target.value)}
className="h-8 text-sm" placeholder="㎡" />
className="h-9 text-sm w-full" placeholder="㎡" />
)}
</TableCell>
{/* 포장재: 등록된 옵션이 있으면 셀렉트, 없으면 안내 */}
<TableCell>
{(row.pkg_options && row.pkg_options.length > 0) ? (
<Select value={row.pkg_code || ""} onValueChange={(v) => updateDetailRow(idx, "pkg_code", v)}>
<SelectTrigger className="h-8 text-sm w-full"><SelectValue placeholder="포장재" /></SelectTrigger>
<SelectTrigger className="h-9 text-sm w-full"><SelectValue placeholder="포장재" /></SelectTrigger>
<SelectContent>
{row.pkg_options.map((o: any) => (
<SelectItem key={o.pkg_code} value={o.pkg_code}>
@@ -1348,15 +1350,15 @@ export default function JeilGlassOrderPage() {
</TableCell>
<TableCell>
<Input value={formatNumber(row.qty || "")} onChange={(e) => updateDetailRow(idx, "qty", parseNumber(e.target.value))}
className="h-8 text-sm text-right" />
className="h-9 text-sm text-right w-full" placeholder="수량" />
</TableCell>
<TableCell>
<Input type="number" min="0" value={row.pack_count || "0"} onChange={(e) => 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} />
</TableCell>
<TableCell>
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => 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="단가" />
</TableCell>
<TableCell className="text-sm text-right font-medium">
{row.amount ? Number(row.amount).toLocaleString() : ""}
@@ -1366,7 +1368,7 @@ export default function JeilGlassOrderPage() {
</TableCell>
<TableCell>
<Input value={row.delivery_location || ""} onChange={(e) => updateDetailRow(idx, "delivery_location", e.target.value)}
className="h-8 text-sm" placeholder="납품장소" />
className="h-9 text-sm w-full" placeholder="납품장소" />
</TableCell>
</TableRow>
))}

View File

@@ -147,6 +147,8 @@ export function DetailFormModal({
const [equipInspChecked, setEquipInspChecked] = useState<Set<string>>(new Set());
// 공정에 지정된 설비 건수 — 0건(설비 미지정) vs 점검항목 0건 메시지 구분용 — TASK:ERP-102
const [equipProcEquipCount, setEquipProcEquipCount] = useState<number>(0);
// 설비 점검항목의 점검방법 카테고리 옵션 (코드→라벨 변환용) — TASK:ERP-102 후속
const [equipMethodOptions, setEquipMethodOptions] = useState<CatOption[]>([]);
// 공정 설비 목록 (자재투입 자재별 설비 연결용) — TASK:ERP-022
const [processEquipments, setProcessEquipments] = useState<any[]>([]);
@@ -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({
</td>
<td className="py-1">{item.equipment_name || item.equipment_code || "-"}</td>
<td className="py-1">{item.inspection_item || "-"}</td>
<td className="py-1">{item.inspection_method || "-"}</td>
{/* 점검방법은 카테고리 코드(CAT_...)로 저장 → 라벨 변환, 매핑 없으면 코드 fallback — TASK:ERP-102 후속 */}
<td className="py-1">{resolveCat(equipMethodOptions, item.inspection_method) || "-"}</td>
<td className="py-1 font-mono">{item.lower_limit || item.upper_limit ? `${item.lower_limit || ""} ~ ${item.upper_limit || ""}${item.unit ? ` ${item.unit}` : ""}` : "-"}</td>
</tr>
);