jskim-node #423
|
|
@ -420,3 +420,44 @@ export async function saveRoutingDetails(req: AuthenticatedRequest, res: Respons
|
|||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// BOM 구성 자재 조회 (품목코드 기반)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
export async function getBomMaterials(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { itemCode } = req.params;
|
||||
|
||||
if (!itemCode) {
|
||||
return res.status(400).json({ success: false, message: "itemCode는 필수입니다" });
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
bd.id,
|
||||
bd.child_item_id,
|
||||
bd.quantity,
|
||||
bd.unit as detail_unit,
|
||||
bd.process_type,
|
||||
i.item_name as child_item_name,
|
||||
i.item_number as child_item_code,
|
||||
i.type as child_item_type,
|
||||
i.unit as item_unit
|
||||
FROM bom b
|
||||
JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code
|
||||
LEFT JOIN item_info i ON bd.child_item_id = i.id AND bd.company_code = i.company_code
|
||||
WHERE b.item_code = $1 AND b.company_code = $2
|
||||
ORDER BY bd.seq_no ASC, bd.created_date ASC
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [itemCode, companyCode]);
|
||||
|
||||
logger.info("BOM 자재 조회 성공", { companyCode, itemCode, count: result.rowCount });
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("BOM 자재 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,4 +39,7 @@ router.delete("/routing-versions/:id", ctrl.deleteRoutingVersion);
|
|||
router.get("/routing-details/:versionId", ctrl.getRoutingDetails);
|
||||
router.put("/routing-details/:versionId", ctrl.saveRoutingDetails);
|
||||
|
||||
// BOM 구성 자재 조회
|
||||
router.get("/bom-materials/:itemCode", ctrl.getBomMaterials);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -274,3 +274,26 @@ export async function saveRoutingDetails(
|
|||
return { success: false, message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ BOM 구성 자재 조회 ═══
|
||||
|
||||
export interface BomMaterial {
|
||||
id: string;
|
||||
child_item_id: string;
|
||||
quantity: string;
|
||||
detail_unit: string | null;
|
||||
process_type: string | null;
|
||||
child_item_name: string | null;
|
||||
child_item_code: string | null;
|
||||
child_item_type: string | null;
|
||||
item_unit: string | null;
|
||||
}
|
||||
|
||||
export async function getBomMaterials(itemCode: string): Promise<ApiResponse<BomMaterial[]>> {
|
||||
try {
|
||||
const res = await apiClient.get(`${BASE}/bom-materials/${encodeURIComponent(itemCode)}`);
|
||||
return res.data;
|
||||
} catch (e: any) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ export function ProcessWorkStandardComponent({
|
|||
selectedWorkItemDetails={selectedDetailsByPhase[phase.key] || []}
|
||||
detailTypes={config.detailTypes}
|
||||
readonly={config.readonly}
|
||||
selectedItemCode={selection.itemCode || undefined}
|
||||
onSelectWorkItem={handleSelectWorkItem}
|
||||
onAddWorkItem={handleAddWorkItem}
|
||||
onEditWorkItem={handleEditWorkItem}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Search, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
} from "@/components/ui/dialog";
|
||||
import { WorkItemDetail, DetailTypeDefinition, InspectionStandard } from "../types";
|
||||
import { InspectionStandardLookup } from "./InspectionStandardLookup";
|
||||
import { getBomMaterials, BomMaterial } from "@/lib/api/processInfo";
|
||||
|
||||
interface DetailFormModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -30,24 +32,53 @@ interface DetailFormModalProps {
|
|||
detailTypes: DetailTypeDefinition[];
|
||||
editData?: WorkItemDetail | null;
|
||||
mode: "add" | "edit";
|
||||
selectedItemCode?: string;
|
||||
}
|
||||
|
||||
const LOOKUP_TARGETS = [
|
||||
{ value: "equipment", label: "설비정보" },
|
||||
{ value: "material", label: "자재정보" },
|
||||
{ value: "worker", label: "작업자정보" },
|
||||
{ value: "tool", label: "공구정보" },
|
||||
{ value: "document", label: "문서정보" },
|
||||
];
|
||||
|
||||
const INPUT_TYPES = [
|
||||
{ value: "text", label: "텍스트" },
|
||||
{ value: "number", label: "숫자" },
|
||||
{ value: "date", label: "날짜" },
|
||||
{ value: "textarea", label: "장문 텍스트" },
|
||||
{ value: "select", label: "선택형" },
|
||||
];
|
||||
|
||||
const UNIT_OPTIONS = [
|
||||
"mm", "cm", "m", "μm", "℃", "℉", "bar", "Pa", "MPa", "psi",
|
||||
"RPM", "kg", "N", "N·m", "m/s", "m/min", "A", "V", "kW", "%",
|
||||
"L/min", "Hz", "dB", "ea", "g", "mg", "ml", "L",
|
||||
];
|
||||
|
||||
const PLC_DATA_OPTIONS = [
|
||||
{ value: "PLC_TEMP_01", label: "온도 (PLC_TEMP_01)" },
|
||||
{ value: "PLC_PRES_01", label: "압력 (PLC_PRES_01)" },
|
||||
{ value: "PLC_RPM_01", label: "회전수 (PLC_RPM_01)" },
|
||||
{ value: "PLC_TORQ_01", label: "토크 (PLC_TORQ_01)" },
|
||||
{ value: "PLC_SPD_01", label: "속도 (PLC_SPD_01)" },
|
||||
{ value: "PLC_CUR_01", label: "전류 (PLC_CUR_01)" },
|
||||
{ value: "PLC_VOLT_01", label: "전압 (PLC_VOLT_01)" },
|
||||
{ value: "PLC_VIB_01", label: "진동 (PLC_VIB_01)" },
|
||||
{ value: "PLC_HUM_01", label: "습도 (PLC_HUM_01)" },
|
||||
{ value: "PLC_FLOW_01", label: "유량 (PLC_FLOW_01)" },
|
||||
];
|
||||
|
||||
const PLC_PRODUCTION_OPTIONS = {
|
||||
work_qty: [
|
||||
{ value: "PLC_CNT_01", label: "생산카운터 (PLC_CNT_01)" },
|
||||
{ value: "PLC_CNT_02", label: "완료카운터 (PLC_CNT_02)" },
|
||||
{ value: "PLC_QTY_01", label: "작업수량 (PLC_QTY_01)" },
|
||||
],
|
||||
defect_qty: [
|
||||
{ value: "PLC_NG_01", label: "불량카운터 (PLC_NG_01)" },
|
||||
{ value: "PLC_NG_02", label: "NG감지기 (PLC_NG_02)" },
|
||||
{ value: "PLC_REJ_01", label: "리젝트수 (PLC_REJ_01)" },
|
||||
],
|
||||
good_qty: [
|
||||
{ value: "PLC_OK_01", label: "양품카운터 (PLC_OK_01)" },
|
||||
{ value: "PLC_OK_02", label: "합격카운터 (PLC_OK_02)" },
|
||||
{ value: "PLC_GOOD_01", label: "양품수량 (PLC_GOOD_01)" },
|
||||
],
|
||||
};
|
||||
|
||||
export function DetailFormModal({
|
||||
open,
|
||||
onClose,
|
||||
|
|
@ -55,10 +86,35 @@ export function DetailFormModal({
|
|||
detailTypes,
|
||||
editData,
|
||||
mode,
|
||||
selectedItemCode,
|
||||
}: DetailFormModalProps) {
|
||||
const [formData, setFormData] = useState<Partial<WorkItemDetail>>({});
|
||||
const [inspectionLookupOpen, setInspectionLookupOpen] = useState(false);
|
||||
const [selectedInspection, setSelectedInspection] = useState<InspectionStandard | null>(null);
|
||||
const [bomMaterials, setBomMaterials] = useState<BomMaterial[]>([]);
|
||||
const [bomLoading, setBomLoading] = useState(false);
|
||||
const [bomChecked, setBomChecked] = useState<Set<string>>(new Set());
|
||||
|
||||
const loadBomMaterials = useCallback(async () => {
|
||||
if (!selectedItemCode) {
|
||||
setBomMaterials([]);
|
||||
return;
|
||||
}
|
||||
setBomLoading(true);
|
||||
try {
|
||||
const res = await getBomMaterials(selectedItemCode);
|
||||
if (res.success && res.data) {
|
||||
setBomMaterials(res.data);
|
||||
setBomChecked(new Set(res.data.map((m) => m.child_item_id)));
|
||||
} else {
|
||||
setBomMaterials([]);
|
||||
}
|
||||
} catch {
|
||||
setBomMaterials([]);
|
||||
} finally {
|
||||
setBomLoading(false);
|
||||
}
|
||||
}, [selectedItemCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
|
@ -86,6 +142,12 @@ export function DetailFormModal({
|
|||
}
|
||||
}, [open, mode, editData, detailTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && formData.detail_type === "material_input") {
|
||||
loadBomMaterials();
|
||||
}
|
||||
}, [open, formData.detail_type, loadBomMaterials]);
|
||||
|
||||
const updateField = (field: string, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
|
@ -108,17 +170,33 @@ export function DetailFormModal({
|
|||
|
||||
const type = formData.detail_type;
|
||||
|
||||
if (type === "check" && !formData.content?.trim()) return;
|
||||
if (type === "inspect" && !formData.content?.trim()) return;
|
||||
if (type === "checklist" && !formData.content?.trim()) return;
|
||||
if (type === "inspection") {
|
||||
if (!formData.process_inspection_apply) return;
|
||||
if (formData.process_inspection_apply === "none" && !formData.content?.trim()) return;
|
||||
}
|
||||
if (type === "procedure" && !formData.content?.trim()) return;
|
||||
if (type === "input" && !formData.content?.trim()) return;
|
||||
if (type === "info" && !formData.lookup_target) return;
|
||||
if (type === "equip_inspection" && !formData.equip_inspection_apply) return;
|
||||
if (type === "equip_condition" && !formData.content?.trim()) return;
|
||||
|
||||
const submitData = { ...formData };
|
||||
|
||||
if (type === "info" && !submitData.content?.trim()) {
|
||||
const targetLabel = LOOKUP_TARGETS.find(t => t.value === submitData.lookup_target)?.label || submitData.lookup_target;
|
||||
submitData.content = `${targetLabel} 조회`;
|
||||
// content 자동 설정 (UI에서 직접 입력이 없는 유형들)
|
||||
if (type === "inspection" && submitData.process_inspection_apply === "apply") {
|
||||
submitData.content = submitData.content || "품목별 검사정보 (자동 연동)";
|
||||
}
|
||||
if (type === "lookup") {
|
||||
submitData.content = submitData.content || "품목 등록 문서 (자동 연동)";
|
||||
}
|
||||
if (type === "equip_inspection" && submitData.equip_inspection_apply === "apply") {
|
||||
submitData.content = submitData.content || "설비 점검항목 (설비정보 연동)";
|
||||
}
|
||||
if (type === "production_result") {
|
||||
submitData.content = submitData.content || "작업수량 / 불량수량 / 양품수량";
|
||||
}
|
||||
if (type === "material_input") {
|
||||
submitData.content = submitData.content || "BOM 구성 자재 (자동 연동)";
|
||||
}
|
||||
|
||||
onSubmit(submitData);
|
||||
|
|
@ -130,7 +208,7 @@ export function DetailFormModal({
|
|||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">
|
||||
상세 항목 {mode === "add" ? "추가" : "수정"}
|
||||
|
|
@ -149,12 +227,11 @@ export function DetailFormModal({
|
|||
<Select
|
||||
value={currentType}
|
||||
onValueChange={(v) => {
|
||||
updateField("detail_type", v);
|
||||
setSelectedInspection(null);
|
||||
setFormData((prev) => ({
|
||||
setFormData({
|
||||
detail_type: v,
|
||||
is_required: prev.is_required || "Y",
|
||||
}));
|
||||
is_required: formData.is_required || "Y",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
||||
|
|
@ -170,9 +247,8 @@ export function DetailFormModal({
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 체크리스트 */}
|
||||
{currentType === "check" && (
|
||||
<>
|
||||
{/* ============ 체크리스트 ============ */}
|
||||
{currentType === "checklist" && (
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
체크 내용 <span className="text-destructive">*</span>
|
||||
|
|
@ -184,68 +260,55 @@ export function DetailFormModal({
|
|||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 검사항목 */}
|
||||
{currentType === "inspect" && (
|
||||
{/* ============ 검사항목 ============ */}
|
||||
{currentType === "inspection" && (
|
||||
<>
|
||||
{/* 공정검사 적용 여부 */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
검사기준 선택 <span className="text-destructive">*</span>
|
||||
공정검사 적용 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<Select value="_placeholder" disabled>
|
||||
<SelectTrigger className="h-8 flex-1 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue>
|
||||
{selectedInspection
|
||||
? `${selectedInspection.inspection_code} - ${selectedInspection.inspection_item}`
|
||||
: "검사기준을 선택하세요"}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_placeholder">선택</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-8 shrink-0 gap-1 text-xs sm:h-10 sm:text-sm"
|
||||
onClick={() => setInspectionLookupOpen(true)}
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
조회
|
||||
</Button>
|
||||
<div className="mt-2 flex gap-4">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs sm:text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="processInspection"
|
||||
checked={formData.process_inspection_apply === "apply"}
|
||||
onChange={() => updateField("process_inspection_apply", "apply")}
|
||||
className="h-4 w-4 accent-primary"
|
||||
/>
|
||||
적용
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs sm:text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="processInspection"
|
||||
checked={formData.process_inspection_apply === "none"}
|
||||
onChange={() => updateField("process_inspection_apply", "none")}
|
||||
className="h-4 w-4 accent-primary"
|
||||
/>
|
||||
미적용
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedInspection && (
|
||||
<div className="rounded border bg-muted/30 p-3">
|
||||
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
||||
선택된 검사기준 정보
|
||||
{/* 적용 시: 품목별 검사정보 자동 연동 안내 */}
|
||||
{formData.process_inspection_apply === "apply" && (
|
||||
<div className="rounded-lg border border-sky-200 bg-sky-50 p-3">
|
||||
<p className="text-xs font-semibold text-sky-800">
|
||||
품목별 검사정보 (자동 연동)
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
<p>
|
||||
<strong>검사코드:</strong> {selectedInspection.inspection_code}
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">
|
||||
품목에 등록된 검사기준이 자동으로 적용됩니다.
|
||||
</p>
|
||||
<p>
|
||||
<strong>검사항목:</strong> {selectedInspection.inspection_item}
|
||||
</p>
|
||||
<p>
|
||||
<strong>검사방법:</strong> {selectedInspection.inspection_method || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>단위:</strong> {selectedInspection.unit || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>하한값:</strong> {selectedInspection.lower_limit || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>상한값:</strong> {selectedInspection.upper_limit || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 미적용 시: 수동 입력 */}
|
||||
{formData.process_inspection_apply === "none" && (
|
||||
<>
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
검사 항목명 <span className="text-destructive">*</span>
|
||||
|
|
@ -257,7 +320,6 @@ export function DetailFormModal({
|
|||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">검사 방법</Label>
|
||||
|
|
@ -270,39 +332,82 @@ export function DetailFormModal({
|
|||
</div>
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">단위</Label>
|
||||
<Input
|
||||
<Select
|
||||
value={formData.unit || ""}
|
||||
onChange={(e) => updateField("unit", e.target.value)}
|
||||
placeholder="예: mm"
|
||||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||
onValueChange={(v) => updateField("unit", v)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder="단위 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{UNIT_OPTIONS.map((u) => (
|
||||
<SelectItem key={u} value={u}>{u}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기준값 ± 오차범위 */}
|
||||
<div className="rounded-lg border bg-muted/30 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
value={formData.base_value || ""}
|
||||
onChange={(e) => updateField("base_value", e.target.value)}
|
||||
placeholder="기준값"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-muted-foreground">±</span>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
value={formData.tolerance || ""}
|
||||
onChange={(e) => updateField("tolerance", e.target.value)}
|
||||
placeholder="오차범위"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">하한값</Label>
|
||||
<Input
|
||||
value={formData.lower_limit || ""}
|
||||
onChange={(e) => updateField("lower_limit", e.target.value)}
|
||||
placeholder="예: 7.95"
|
||||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">상한값</Label>
|
||||
<Input
|
||||
value={formData.upper_limit || ""}
|
||||
onChange={(e) => updateField("upper_limit", e.target.value)}
|
||||
placeholder="예: 8.05"
|
||||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||
{/* 자동수집 */}
|
||||
<div className="mt-3 flex items-center gap-3 border-t pt-3">
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-2 text-xs font-medium sm:text-sm">
|
||||
<Checkbox
|
||||
checked={formData.auto_collect === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
updateField("auto_collect", checked ? "Y" : "N");
|
||||
if (!checked) updateField("plc_data", "");
|
||||
}}
|
||||
/>
|
||||
자동수집
|
||||
</label>
|
||||
<Select
|
||||
value={formData.plc_data || ""}
|
||||
onValueChange={(v) => updateField("plc_data", v)}
|
||||
disabled={formData.auto_collect !== "Y"}
|
||||
>
|
||||
<SelectTrigger className={`h-8 flex-1 text-xs sm:h-10 sm:text-sm ${formData.auto_collect !== "Y" ? "opacity-50" : ""}`}>
|
||||
<SelectValue placeholder="수집데이터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLC_DATA_OPTIONS.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 작업절차 */}
|
||||
{/* ============ 작업절차 ============ */}
|
||||
{currentType === "procedure" && (
|
||||
<>
|
||||
<div>
|
||||
|
|
@ -322,10 +427,7 @@ export function DetailFormModal({
|
|||
type="number"
|
||||
value={formData.duration_minutes ?? ""}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
"duration_minutes",
|
||||
e.target.value ? Number(e.target.value) : undefined
|
||||
)
|
||||
updateField("duration_minutes", e.target.value ? Number(e.target.value) : undefined)
|
||||
}
|
||||
placeholder="예: 5"
|
||||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||
|
|
@ -334,7 +436,7 @@ export function DetailFormModal({
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* 직접입력 */}
|
||||
{/* ============ 직접입력 ============ */}
|
||||
{currentType === "input" && (
|
||||
<>
|
||||
<div>
|
||||
|
|
@ -359,9 +461,7 @@ export function DetailFormModal({
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INPUT_TYPES.map((t) => (
|
||||
<SelectItem key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</SelectItem>
|
||||
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -369,41 +469,367 @@ export function DetailFormModal({
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* 정보조회 */}
|
||||
{currentType === "info" && (
|
||||
{/* ============ 문서참조 ============ */}
|
||||
{currentType === "lookup" && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2.5">
|
||||
<span className="text-sm">📄</span>
|
||||
<span className="text-xs text-blue-800">
|
||||
해당 품목에 등록된 문서를 자동으로 불러옵니다.
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">참조 문서 목록</Label>
|
||||
<div className="mt-1 rounded-lg border p-3">
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
품목이 선택되지 않았습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ============ 설비점검 ============ */}
|
||||
{currentType === "equip_inspection" && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-3 py-2.5">
|
||||
<span className="text-sm">🏭</span>
|
||||
<span className="text-xs text-green-800">
|
||||
공정에 지정된 설비를 자동 참조합니다.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
조회 대상 <span className="text-destructive">*</span>
|
||||
설비점검 적용 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="mt-2 flex gap-4">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs sm:text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="equipInspApply"
|
||||
checked={formData.equip_inspection_apply === "apply"}
|
||||
onChange={() => updateField("equip_inspection_apply", "apply")}
|
||||
className="h-4 w-4 accent-primary"
|
||||
/>
|
||||
적용
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs sm:text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="equipInspApply"
|
||||
checked={formData.equip_inspection_apply === "none"}
|
||||
onChange={() => updateField("equip_inspection_apply", "none")}
|
||||
className="h-4 w-4 accent-primary"
|
||||
/>
|
||||
미적용
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 적용 시: 설비 점검항목 자동 연동 */}
|
||||
{formData.equip_inspection_apply === "apply" && (
|
||||
<div className="rounded-lg border border-sky-200 bg-sky-50 p-3">
|
||||
<p className="text-xs font-semibold text-sky-800">
|
||||
설비 점검항목 (설비정보 연동)
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">
|
||||
공정에 지정된 설비의 점검항목이 자동으로 적용됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ============ 설비조건 ============ */}
|
||||
{currentType === "equip_condition" && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-3 py-2.5">
|
||||
<span className="text-sm">🏭</span>
|
||||
<span className="text-xs text-green-800">
|
||||
공정에 지정된 설비를 자동 참조합니다.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
설비조건 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="mt-2 rounded-lg border bg-muted/30 p-3 space-y-3">
|
||||
{/* 조건명 + 단위 */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-[1.2]">
|
||||
<Input
|
||||
value={formData.content || ""}
|
||||
onChange={(e) => updateField("content", e.target.value)}
|
||||
placeholder="조건명 (예: 온도, 압력, RPM)"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[0.6]">
|
||||
<Select
|
||||
value={formData.lookup_target || ""}
|
||||
onValueChange={(v) => updateField("lookup_target", v)}
|
||||
value={formData.condition_unit || ""}
|
||||
onValueChange={(v) => updateField("condition_unit", v)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder="단위" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOOKUP_TARGETS.map((t) => (
|
||||
<SelectItem key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</SelectItem>
|
||||
{UNIT_OPTIONS.map((u) => (
|
||||
<SelectItem key={u} value={u}>{u}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">표시 항목</Label>
|
||||
</div>
|
||||
|
||||
{/* 기준값 ± 오차범위 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={formData.display_fields || ""}
|
||||
onChange={(e) => updateField("display_fields", e.target.value)}
|
||||
placeholder="예: 설비명, 설비코드"
|
||||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||
type="number"
|
||||
step="any"
|
||||
value={formData.condition_base_value || ""}
|
||||
onChange={(e) => updateField("condition_base_value", e.target.value)}
|
||||
placeholder="기준값"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-muted-foreground">±</span>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
value={formData.condition_tolerance || ""}
|
||||
onChange={(e) => updateField("condition_tolerance", e.target.value)}
|
||||
placeholder="오차범위"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 자동수집 */}
|
||||
<div className="flex items-center gap-3 border-t pt-3">
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-2 text-xs font-medium sm:text-sm">
|
||||
<Checkbox
|
||||
checked={formData.condition_auto_collect === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
updateField("condition_auto_collect", checked ? "Y" : "N");
|
||||
if (!checked) updateField("condition_plc_data", "");
|
||||
}}
|
||||
/>
|
||||
자동수집
|
||||
</label>
|
||||
<Select
|
||||
value={formData.condition_plc_data || ""}
|
||||
onValueChange={(v) => updateField("condition_plc_data", v)}
|
||||
disabled={formData.condition_auto_collect !== "Y"}
|
||||
>
|
||||
<SelectTrigger className={`h-8 flex-1 text-xs sm:h-10 sm:text-sm ${formData.condition_auto_collect !== "Y" ? "opacity-50" : ""}`}>
|
||||
<SelectValue placeholder="수집데이터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLC_DATA_OPTIONS.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ============ 실적등록 ============ */}
|
||||
{currentType === "production_result" && (
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">실적등록 항목 설정</Label>
|
||||
<div className="mt-2 rounded-lg border bg-muted/30 p-3 space-y-0 divide-y">
|
||||
{/* 작업수량 */}
|
||||
<div className="flex items-center justify-between py-3 first:pt-0">
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-semibold">📦 작업수량</span>
|
||||
<span className="ml-2 text-[11px] text-muted-foreground">생산된 전체 수량</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-2 text-xs font-medium">
|
||||
<Checkbox
|
||||
checked={formData.work_qty_auto_collect === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
updateField("work_qty_auto_collect", checked ? "Y" : "N");
|
||||
if (!checked) updateField("work_qty_plc_data", "");
|
||||
}}
|
||||
/>
|
||||
자동수집
|
||||
</label>
|
||||
<Select
|
||||
value={formData.work_qty_plc_data || ""}
|
||||
onValueChange={(v) => updateField("work_qty_plc_data", v)}
|
||||
disabled={formData.work_qty_auto_collect !== "Y"}
|
||||
>
|
||||
<SelectTrigger className={`h-8 w-[180px] text-xs ${formData.work_qty_auto_collect !== "Y" ? "opacity-50" : ""}`}>
|
||||
<SelectValue placeholder="수집데이터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLC_PRODUCTION_OPTIONS.work_qty.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 불량수량 */}
|
||||
<div className="flex items-center justify-between py-3">
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-semibold">🚫 불량수량</span>
|
||||
<span className="ml-2 text-[11px] text-muted-foreground">불량으로 판정된 수량</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-2 text-xs font-medium">
|
||||
<Checkbox
|
||||
checked={formData.defect_qty_auto_collect === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
updateField("defect_qty_auto_collect", checked ? "Y" : "N");
|
||||
if (!checked) updateField("defect_qty_plc_data", "");
|
||||
}}
|
||||
/>
|
||||
자동수집
|
||||
</label>
|
||||
<Select
|
||||
value={formData.defect_qty_plc_data || ""}
|
||||
onValueChange={(v) => updateField("defect_qty_plc_data", v)}
|
||||
disabled={formData.defect_qty_auto_collect !== "Y"}
|
||||
>
|
||||
<SelectTrigger className={`h-8 w-[180px] text-xs ${formData.defect_qty_auto_collect !== "Y" ? "opacity-50" : ""}`}>
|
||||
<SelectValue placeholder="수집데이터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLC_PRODUCTION_OPTIONS.defect_qty.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 양품수량 */}
|
||||
<div className="flex items-center justify-between py-3 last:pb-0">
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-semibold">✅ 양품수량</span>
|
||||
<span className="ml-2 text-[11px] text-muted-foreground">작업수량 - 불량수량</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-2 text-xs font-medium">
|
||||
<Checkbox
|
||||
checked={formData.good_qty_auto_collect === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
updateField("good_qty_auto_collect", checked ? "Y" : "N");
|
||||
if (!checked) updateField("good_qty_plc_data", "");
|
||||
}}
|
||||
/>
|
||||
자동수집
|
||||
</label>
|
||||
<Select
|
||||
value={formData.good_qty_plc_data || ""}
|
||||
onValueChange={(v) => updateField("good_qty_plc_data", v)}
|
||||
disabled={formData.good_qty_auto_collect !== "Y"}
|
||||
>
|
||||
<SelectTrigger className={`h-8 w-[180px] text-xs ${formData.good_qty_auto_collect !== "Y" ? "opacity-50" : ""}`}>
|
||||
<SelectValue placeholder="수집데이터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLC_PRODUCTION_OPTIONS.good_qty.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ============ 자재투입 ============ */}
|
||||
{currentType === "material_input" && (
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">자재투입 설정</Label>
|
||||
<div className="mt-2 rounded-lg border border-sky-200 bg-sky-50 p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-xs font-semibold text-sky-800">
|
||||
📦 BOM 구성 자재 (자동 연동)
|
||||
</p>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{bomMaterials.length > 0 ? `${bomMaterials.length}건` : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-h-[300px] overflow-y-auto rounded-lg border bg-white">
|
||||
{bomLoading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-6">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">BOM 데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
) : !selectedItemCode ? (
|
||||
<div className="py-5 text-center text-xs text-muted-foreground">
|
||||
품목을 먼저 선택하세요.
|
||||
</div>
|
||||
) : bomMaterials.length === 0 ? (
|
||||
<div className="py-5 text-center text-xs text-muted-foreground">
|
||||
해당 품목의 BOM 구성 자재가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
bomMaterials.map((mat) => {
|
||||
const typeColor =
|
||||
mat.child_item_type === "원자재" ? "#16a34a"
|
||||
: mat.child_item_type === "반제품" ? "#2563eb"
|
||||
: "#6b7280";
|
||||
return (
|
||||
<div
|
||||
key={mat.id}
|
||||
className="flex items-center gap-2.5 border-b px-3 py-2.5 last:border-b-0 hover:bg-sky-50/50"
|
||||
>
|
||||
<Checkbox
|
||||
checked={bomChecked.has(mat.child_item_id)}
|
||||
onCheckedChange={(checked) => {
|
||||
setBomChecked((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (checked) next.add(mat.child_item_id);
|
||||
else next.delete(mat.child_item_id);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{mat.child_item_type && (
|
||||
<span
|
||||
className="rounded px-1.5 py-0.5 text-[10px] font-semibold text-white"
|
||||
style={{ backgroundColor: typeColor }}
|
||||
>
|
||||
{mat.child_item_type}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{mat.child_item_code || "-"}
|
||||
</span>
|
||||
<span className="text-xs font-medium">
|
||||
{mat.child_item_name || "(이름 없음)"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
||||
소요량: {mat.quantity || "0"} {mat.detail_unit || mat.item_unit || ""}
|
||||
{mat.process_type ? ` | 공정: ${mat.process_type}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필수 여부 (모든 유형 공통) */}
|
||||
{currentType && (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface WorkItemDetailListProps {
|
|||
details: WorkItemDetail[];
|
||||
detailTypes: DetailTypeDefinition[];
|
||||
readonly?: boolean;
|
||||
selectedItemCode?: string;
|
||||
onCreateDetail: (data: Partial<WorkItemDetail>) => void;
|
||||
onUpdateDetail: (id: string, data: Partial<WorkItemDetail>) => void;
|
||||
onDeleteDetail: (id: string) => void;
|
||||
|
|
@ -23,6 +24,7 @@ export function WorkItemDetailList({
|
|||
details,
|
||||
detailTypes,
|
||||
readonly,
|
||||
selectedItemCode,
|
||||
onCreateDetail,
|
||||
onUpdateDetail,
|
||||
onDeleteDetail,
|
||||
|
|
@ -66,11 +68,12 @@ export function WorkItemDetailList({
|
|||
|
||||
const getContentSummary = (detail: WorkItemDetail): string => {
|
||||
const type = detail.detail_type;
|
||||
if (type === "inspect" && detail.inspection_code) {
|
||||
if (type === "inspection") {
|
||||
if (detail.process_inspection_apply === "apply") return "품목별 검사정보 (자동 연동)";
|
||||
const parts = [detail.content];
|
||||
if (detail.inspection_method) parts.push(`[${detail.inspection_method}]`);
|
||||
if (detail.lower_limit || detail.upper_limit) {
|
||||
parts.push(`(${detail.lower_limit || "-"} ~ ${detail.upper_limit || "-"} ${detail.unit || ""})`);
|
||||
if (detail.base_value) {
|
||||
parts.push(`(기준: ${detail.base_value}${detail.tolerance ? ` ±${detail.tolerance}` : ""} ${detail.unit || ""})`);
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
|
@ -83,20 +86,24 @@ export function WorkItemDetailList({
|
|||
number: "숫자",
|
||||
date: "날짜",
|
||||
textarea: "장문",
|
||||
select: "선택형",
|
||||
};
|
||||
return `${detail.content} [${typeMap[detail.input_type] || detail.input_type}]`;
|
||||
}
|
||||
if (type === "info" && detail.lookup_target) {
|
||||
const targetMap: Record<string, string> = {
|
||||
equipment: "설비정보",
|
||||
material: "자재정보",
|
||||
worker: "작업자정보",
|
||||
tool: "공구정보",
|
||||
document: "문서정보",
|
||||
};
|
||||
return `${targetMap[detail.lookup_target] || detail.lookup_target} 조회`;
|
||||
if (type === "lookup") return "품목 등록 문서 (자동 연동)";
|
||||
if (type === "equip_inspection") {
|
||||
return detail.equip_inspection_apply === "apply"
|
||||
? "설비 점검항목 (설비정보 연동)"
|
||||
: detail.content || "설비점검";
|
||||
}
|
||||
if (type === "equip_condition") {
|
||||
const parts = [detail.content];
|
||||
if (detail.condition_base_value) {
|
||||
parts.push(`(기준: ${detail.condition_base_value}${detail.condition_tolerance ? ` ±${detail.condition_tolerance}` : ""} ${detail.condition_unit || ""})`);
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
if (type === "production_result") return "작업수량 / 불량수량 / 양품수량";
|
||||
if (type === "material_input") return "BOM 구성 자재 (자동 연동)";
|
||||
return detail.content || "-";
|
||||
};
|
||||
|
||||
|
|
@ -214,6 +221,7 @@ export function WorkItemDetailList({
|
|||
detailTypes={detailTypes}
|
||||
editData={editTarget}
|
||||
mode={modalMode}
|
||||
selectedItemCode={selectedItemCode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ interface WorkPhaseSectionProps {
|
|||
selectedWorkItemDetails: WorkItemDetail[];
|
||||
detailTypes: DetailTypeDefinition[];
|
||||
readonly?: boolean;
|
||||
selectedItemCode?: string;
|
||||
onSelectWorkItem: (workItemId: string, phaseKey: string) => void;
|
||||
onAddWorkItem: (phase: string) => void;
|
||||
onEditWorkItem: (item: WorkItem) => void;
|
||||
|
|
@ -36,6 +37,7 @@ export function WorkPhaseSection({
|
|||
selectedWorkItemDetails,
|
||||
detailTypes,
|
||||
readonly,
|
||||
selectedItemCode,
|
||||
onSelectWorkItem,
|
||||
onAddWorkItem,
|
||||
onEditWorkItem,
|
||||
|
|
@ -107,6 +109,7 @@ export function WorkPhaseSection({
|
|||
details={selectedWorkItemDetails}
|
||||
detailTypes={detailTypes}
|
||||
readonly={readonly}
|
||||
selectedItemCode={selectedItemCode}
|
||||
onCreateDetail={(data) =>
|
||||
selectedWorkItemId && onCreateDetail(selectedWorkItemId, data, phase.key)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,15 @@ export const defaultConfig: ProcessWorkStandardConfig = {
|
|||
{ key: "POST", label: "작업 후 (Post-Work)", sortOrder: 3 },
|
||||
],
|
||||
detailTypes: [
|
||||
{ value: "check", label: "체크리스트" },
|
||||
{ value: "inspect", label: "검사항목" },
|
||||
{ value: "checklist", label: "체크리스트" },
|
||||
{ value: "inspection", label: "검사항목" },
|
||||
{ value: "procedure", label: "작업절차" },
|
||||
{ value: "input", label: "직접입력" },
|
||||
{ value: "lookup", label: "문서참조" },
|
||||
{ value: "equip_inspection", label: "설비점검" },
|
||||
{ value: "equip_condition", label: "설비조건" },
|
||||
{ value: "production_result", label: "실적등록" },
|
||||
{ value: "material_input", label: "자재투입" },
|
||||
],
|
||||
splitRatio: 30,
|
||||
leftPanelTitle: "품목 및 공정 선택",
|
||||
|
|
|
|||
|
|
@ -91,19 +91,53 @@ export interface WorkItemDetail {
|
|||
sort_order: number;
|
||||
remark?: string;
|
||||
created_date?: string;
|
||||
// 검사항목 전용
|
||||
|
||||
// 검사항목(inspection) 전용
|
||||
process_inspection_apply?: string; // "apply" | "none"
|
||||
inspection_code?: string;
|
||||
inspection_method?: string;
|
||||
unit?: string;
|
||||
base_value?: string;
|
||||
tolerance?: string;
|
||||
lower_limit?: string;
|
||||
upper_limit?: string;
|
||||
// 작업절차 전용
|
||||
auto_collect?: string; // "Y" | "N"
|
||||
plc_data?: string;
|
||||
|
||||
// 작업절차(procedure) 전용
|
||||
duration_minutes?: number;
|
||||
// 직접입력 전용
|
||||
|
||||
// 직접입력(input) 전용
|
||||
input_type?: string;
|
||||
// 정보조회 전용
|
||||
|
||||
// 문서참조(lookup) 전용
|
||||
lookup_target?: string;
|
||||
display_fields?: string;
|
||||
|
||||
// 설비점검(equip_inspection) 전용
|
||||
equip_inspection_apply?: string; // "apply" | "none"
|
||||
|
||||
// 설비조건(equip_condition) 전용
|
||||
condition_name?: string;
|
||||
condition_unit?: string;
|
||||
condition_base_value?: string;
|
||||
condition_tolerance?: string;
|
||||
condition_auto_collect?: string;
|
||||
condition_plc_data?: string;
|
||||
|
||||
// 실적등록(production_result) 전용
|
||||
work_qty_auto_collect?: string;
|
||||
work_qty_plc_data?: string;
|
||||
defect_qty_auto_collect?: string;
|
||||
defect_qty_plc_data?: string;
|
||||
good_qty_auto_collect?: string;
|
||||
good_qty_plc_data?: string;
|
||||
|
||||
// 자재투입(material_input) 전용 - BOM 자동연동
|
||||
material_code?: string;
|
||||
material_name?: string;
|
||||
quantity?: string;
|
||||
material_unit?: string;
|
||||
}
|
||||
|
||||
export interface InspectionStandard {
|
||||
|
|
|
|||
Loading…
Reference in New Issue