- Removed the "정보조회" option from the default configuration. - Refactored the ProcessWorkStandardComponent to handle work item selection independently for each phase. - Updated the WorkPhaseSection to pass phase-specific parameters for work item selection and detail management. - Enhanced the useProcessWorkStandard hook to maintain separate states for selected work items and details by phase, improving data handling and user experience.
242 lines
8.0 KiB
TypeScript
242 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useMemo, useCallback } from "react";
|
|
import { Save, Loader2, ClipboardCheck } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { cn } from "@/lib/utils";
|
|
import { ProcessWorkStandardConfig, WorkItem } from "./types";
|
|
import { defaultConfig } from "./config";
|
|
import { useProcessWorkStandard } from "./hooks/useProcessWorkStandard";
|
|
import { ItemProcessSelector } from "./components/ItemProcessSelector";
|
|
import { WorkPhaseSection } from "./components/WorkPhaseSection";
|
|
import { WorkItemAddModal } from "./components/WorkItemAddModal";
|
|
|
|
interface ProcessWorkStandardComponentProps {
|
|
config?: Partial<ProcessWorkStandardConfig>;
|
|
formData?: Record<string, any>;
|
|
isPreview?: boolean;
|
|
tableName?: string;
|
|
}
|
|
|
|
export function ProcessWorkStandardComponent({
|
|
config: configProp,
|
|
isPreview,
|
|
}: ProcessWorkStandardComponentProps) {
|
|
const config: ProcessWorkStandardConfig = useMemo(
|
|
() => ({
|
|
...defaultConfig,
|
|
...configProp,
|
|
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource },
|
|
phases: configProp?.phases?.length
|
|
? configProp.phases
|
|
: defaultConfig.phases,
|
|
detailTypes: configProp?.detailTypes?.length
|
|
? configProp.detailTypes
|
|
: defaultConfig.detailTypes,
|
|
}),
|
|
[configProp]
|
|
);
|
|
|
|
const {
|
|
items,
|
|
routings,
|
|
workItems,
|
|
selectedWorkItemIdByPhase,
|
|
selectedDetailsByPhase,
|
|
selection,
|
|
loading,
|
|
fetchItems,
|
|
selectItem,
|
|
selectProcess,
|
|
fetchWorkItemDetails,
|
|
createWorkItem,
|
|
updateWorkItem,
|
|
deleteWorkItem,
|
|
createDetail,
|
|
updateDetail,
|
|
deleteDetail,
|
|
} = useProcessWorkStandard(config);
|
|
|
|
// 모달 상태
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [modalPhaseKey, setModalPhaseKey] = useState("");
|
|
const [editingItem, setEditingItem] = useState<WorkItem | null>(null);
|
|
|
|
// phase별 작업 항목 그룹핑
|
|
const workItemsByPhase = useMemo(() => {
|
|
const map: Record<string, WorkItem[]> = {};
|
|
for (const phase of config.phases) {
|
|
map[phase.key] = workItems.filter((wi) => wi.work_phase === phase.key);
|
|
}
|
|
return map;
|
|
}, [workItems, config.phases]);
|
|
|
|
const sortedPhases = useMemo(
|
|
() => [...config.phases].sort((a, b) => a.sortOrder - b.sortOrder),
|
|
[config.phases]
|
|
);
|
|
|
|
const handleAddWorkItem = useCallback((phaseKey: string) => {
|
|
setModalPhaseKey(phaseKey);
|
|
setEditingItem(null);
|
|
setModalOpen(true);
|
|
}, []);
|
|
|
|
const handleEditWorkItem = useCallback((item: WorkItem) => {
|
|
setModalPhaseKey(item.work_phase);
|
|
setEditingItem(item);
|
|
setModalOpen(true);
|
|
}, []);
|
|
|
|
const handleModalSave = useCallback(
|
|
async (data: Parameters<typeof createWorkItem>[0]) => {
|
|
if (editingItem) {
|
|
await updateWorkItem(editingItem.id, {
|
|
title: data.title,
|
|
is_required: data.is_required,
|
|
description: data.description,
|
|
} as any);
|
|
} else {
|
|
await createWorkItem(data);
|
|
}
|
|
},
|
|
[editingItem, createWorkItem, updateWorkItem]
|
|
);
|
|
|
|
const handleSelectWorkItem = useCallback(
|
|
(workItemId: string, phaseKey: string) => {
|
|
fetchWorkItemDetails(workItemId, phaseKey);
|
|
},
|
|
[fetchWorkItemDetails]
|
|
);
|
|
|
|
const handleInit = useCallback(() => {
|
|
fetchItems();
|
|
}, [fetchItems]);
|
|
|
|
const splitRatio = config.splitRatio || 30;
|
|
|
|
if (isPreview) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/20 bg-muted/10 p-4">
|
|
<div className="text-center">
|
|
<ClipboardCheck className="mx-auto mb-2 h-8 w-8 text-muted-foreground/50" />
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
공정 작업기준
|
|
</p>
|
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
|
{sortedPhases.map((p) => p.label).join(" / ")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col overflow-hidden rounded-lg border bg-background">
|
|
{/* 메인 콘텐츠 */}
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* 좌측 패널 */}
|
|
<div style={{ width: `${splitRatio}%` }} className="shrink-0 overflow-hidden">
|
|
<ItemProcessSelector
|
|
title={config.leftPanelTitle || "품목 및 공정 선택"}
|
|
items={items}
|
|
routings={routings}
|
|
selection={selection}
|
|
onSearch={(keyword) => fetchItems(keyword)}
|
|
onSelectItem={selectItem}
|
|
onSelectProcess={selectProcess}
|
|
onInit={handleInit}
|
|
/>
|
|
</div>
|
|
|
|
{/* 우측 패널 */}
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
{/* 우측 헤더 */}
|
|
{selection.routingDetailId ? (
|
|
<>
|
|
<div className="flex items-center justify-between border-b px-4 py-2.5">
|
|
<div>
|
|
<h2 className="text-base font-bold">
|
|
{selection.itemName} - {selection.processName}
|
|
</h2>
|
|
<div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
|
<span>품목: {selection.itemCode}</span>
|
|
<span>공정: {selection.processName}</span>
|
|
<span>버전: {selection.routingVersionName}</span>
|
|
</div>
|
|
</div>
|
|
{!config.readonly && (
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
className="gap-1.5"
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<Save className="h-3.5 w-3.5" />
|
|
)}
|
|
전체 저장
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 작업 단계별 섹션 */}
|
|
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
|
{sortedPhases.map((phase) => (
|
|
<WorkPhaseSection
|
|
key={phase.key}
|
|
phase={phase}
|
|
items={workItemsByPhase[phase.key] || []}
|
|
selectedWorkItemId={selectedWorkItemIdByPhase[phase.key] || null}
|
|
selectedWorkItemDetails={selectedDetailsByPhase[phase.key] || []}
|
|
detailTypes={config.detailTypes}
|
|
readonly={config.readonly}
|
|
onSelectWorkItem={handleSelectWorkItem}
|
|
onAddWorkItem={handleAddWorkItem}
|
|
onEditWorkItem={handleEditWorkItem}
|
|
onDeleteWorkItem={deleteWorkItem}
|
|
onCreateDetail={createDetail}
|
|
onUpdateDetail={updateDetail}
|
|
onDeleteDetail={deleteDetail}
|
|
/>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex flex-1 flex-col items-center justify-center text-center">
|
|
<ClipboardCheck className="mb-3 h-12 w-12 text-muted-foreground/30" />
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
좌측에서 품목과 공정을 선택하세요
|
|
</p>
|
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
|
품목을 펼쳐 라우팅별 공정을 선택하면 작업기준을 관리할 수
|
|
있습니다
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 작업 항목 추가/수정 모달 */}
|
|
<WorkItemAddModal
|
|
open={modalOpen}
|
|
onClose={() => {
|
|
setModalOpen(false);
|
|
setEditingItem(null);
|
|
}}
|
|
onSave={handleModalSave}
|
|
phaseKey={modalPhaseKey}
|
|
phaseLabel={
|
|
config.phases.find((p) => p.key === modalPhaseKey)?.label || ""
|
|
}
|
|
detailTypes={config.detailTypes}
|
|
editItem={editingItem}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|