Files
wace_rps/frontend/components/development/BomReportStatusDialog.tsx
T
hjjeong 0872199b30 개발관리>E-BOM 등록·조회 메뉴 신설 (PR-B) — wace partMng 1:1 이식
backend (M3+M4):
- devBomService: list/getByObjid/updateStatus/removeMany/ascending/descending
- M3 그리드 SQL (getBOMStandardStructureGridList 1:1)
  - customer_mng 매핑 (wace SUPPLY_MNG → vexplor customer_mng.customer_code)
  - PRODUCT_NAME LEFT JOIN comm_code (CODE_NAME 함수 대체)
- M3 다중 삭제 트랜잭션 (bom_part_qty + part_bom_report CASCADE)
- M4 정/역전개 재귀 CTE (bom_part_qty 트리 + part_mng JOIN)
- vexplor 적응: M4 product_mgmt_spec/upg/vc 분기 제거 (스키마 단순화)
- PG 재귀 CTE 타입 일치: ARRAY[BP.objid::varchar] 명시 cast

frontend (M3+M4):
- ebom-regist (M3): 9셀 그리드 (제품구분·품번·품명·E-BOM·등록자·등록일·확정일·Version·상태)
- ebom-search (M4): 동적 LEVEL 컬럼 + 정/역전개 토글
- BomReportStatusDialog: 상태 변경 (create/changeDesign/deploy + version)
- AdminPageRenderer dynamic 임포트 2건 + menu_info URL spec 정렬

본 PR 제외 (별 PR): E-BOM Excel Import, 정/역전개 Excel Download, BOM_PART_QTY 수량 편집

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:23:10 +09:00

131 lines
4.7 KiB
TypeScript

"use client";
// 개발관리 > E-BOM 상태 변경 다이얼로그.
// wace structureStatusChangePopup 1:1 — STATUS select(create/changeDesign/deploy) + 부속 4필드.
import React, { useEffect, useState } from "react";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Loader2, Save } from "lucide-react";
import { toast } from "sonner";
import { devBomApi, BomReportRow } from "@/lib/api/devBom";
const STATUS_OPTIONS = [
{ code: "create", label: "등록중" },
{ code: "changeDesign", label: "설계변경미배포" },
{ code: "deploy", label: "배포완료" },
];
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
objid: string | null;
onSaved: () => void;
}
export function BomReportStatusDialog({ open, onOpenChange, objid, onSaved }: Props) {
const [row, setRow] = useState<BomReportRow | null>(null);
const [status, setStatus] = useState<string>("");
const [version, setVersion] = useState<string>("");
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open || !objid) return;
let alive = true;
setLoading(true);
devBomApi.detail(objid)
.then((data) => {
if (!alive) return;
if (!data) {
toast.error("E-BOM 보고서를 찾을 수 없습니다.");
onOpenChange(false);
return;
}
setRow(data);
setStatus(data.status ?? "");
setVersion(data.revision ?? "");
})
.catch((e: any) => {
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
onOpenChange(false);
})
.finally(() => { if (alive) setLoading(false); });
return () => { alive = false; };
}, [open, objid, onOpenChange]);
const handleSave = async () => {
if (!objid) return;
if (!status) return toast.error("상태를 선택하세요.");
setSaving(true);
try {
await devBomApi.updateStatus(objid, {
status,
version: version || undefined,
});
toast.success("상태가 변경되었습니다.");
onSaved();
onOpenChange(false);
} catch (e: any) {
toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패");
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>E-BOM </DialogTitle>
</DialogHeader>
{loading || !row ? (
<div className="flex h-40 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="space-y-3 py-2">
<div className="rounded-md border bg-muted/30 p-3 text-sm space-y-1">
<div><span className="text-muted-foreground">:</span> {row.product_name ?? row.product_cd ?? "—"}</div>
<div><span className="text-muted-foreground">:</span> {row.part_no ?? "—"}</div>
<div><span className="text-muted-foreground">:</span> {row.part_name ?? "—"}</div>
<div><span className="text-muted-foreground">:</span> {row.status_title ?? row.status ?? "—"}</div>
</div>
<div>
<Label className="mb-1 block text-xs text-muted-foreground"> *</Label>
<select
className="h-9 w-full rounded-md border bg-background px-2 text-sm"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value=""></option>
{STATUS_OPTIONS.map((o) =>
<option key={o.code} value={o.code}>{o.label}</option>)}
</select>
</div>
<div>
<Label className="mb-1 block text-xs text-muted-foreground">Version</Label>
<Input value={version} onChange={(e) => setVersion(e.target.value)} placeholder="예: RE, A, B..." />
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
</Button>
<Button onClick={handleSave} disabled={saving || loading}>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}