개발관리>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>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -109,6 +109,8 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||
"/COMPANY_16/project/wbs-template": dynamic(() => import("@/app/(main)/COMPANY_16/project/wbs-template/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/development/part-regist": dynamic(() => import("@/app/(main)/COMPANY_16/development/part-regist/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/development/part-search": dynamic(() => import("@/app/(main)/COMPANY_16/development/part-search/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/development/ebom-regist": dynamic(() => import("@/app/(main)/COMPANY_16/development/ebom-regist/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/development/ebom-search": dynamic(() => import("@/app/(main)/COMPANY_16/development/ebom-search/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/process-info": dynamic(() => import("@/app/(main)/COMPANY_16/production/process-info/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/result": dynamic(() => import("@/app/(main)/COMPANY_16/production/result/page"), { ssr: false, loading: LoadingFallback }),
|
||||
"/COMPANY_16/production/work-instruction": dynamic(() => import("@/app/(main)/COMPANY_16/production/work-instruction/page"), { ssr: false, loading: LoadingFallback }),
|
||||
|
||||
Reference in New Issue
Block a user