ERP-node/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailConfig.tsx

861 lines
30 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Trash2, ChevronUp, ChevronDown, Zap, Loader2 } from "lucide-react";
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
import type {
PopWorkDetailConfig,
WorkDetailInfoBarField,
ResultSectionConfig,
ResultSectionType,
PlcDataConfig,
} from "../types";
interface PopWorkDetailConfigPanelProps {
config?: PopWorkDetailConfig;
onChange?: (config: PopWorkDetailConfig) => void;
}
const SECTION_TYPE_META: Record<ResultSectionType, { label: string }> = {
"total-qty": { label: "생산수량" },
"good-defect": { label: "양품/불량" },
"defect-types": { label: "불량 유형 상세" },
"note": { label: "비고" },
"box-packing": { label: "박스 포장" },
"label-print": { label: "라벨 출력" },
"photo": { label: "사진" },
"document": { label: "문서" },
"material-input": { label: "자재 투입" },
"barcode-scan": { label: "바코드 스캔" },
"plc-data": { label: "PLC 데이터" },
};
const ALL_SECTION_TYPES = Object.keys(SECTION_TYPE_META) as ResultSectionType[];
const DEFAULT_PHASE_LABELS: Record<string, string> = {
PRE: "작업 전",
IN: "작업 중",
POST: "작업 후",
};
const DEFAULT_INFO_BAR = {
enabled: true,
fields: [] as WorkDetailInfoBarField[],
};
const DEFAULT_STEP_CONTROL = {
requireStartBeforeInput: false,
autoAdvance: true,
};
const DEFAULT_NAVIGATION = {
showPrevNext: true,
showCompleteButton: true,
};
const DEFAULT_PLC_CONFIG: PlcDataConfig = {
connectionId: "",
tableName: "",
deviceColumn: "",
valueColumn: "",
timestampColumn: "",
deviceFilter: "",
tagFilter: "",
label: "",
unit: "EA",
refreshInterval: 30,
displayMode: "number",
};
export function PopWorkDetailConfigPanel({
config,
onChange,
}: PopWorkDetailConfigPanelProps) {
const cfg: PopWorkDetailConfig = {
showTimer: config?.showTimer ?? true,
showQuantityInput: config?.showQuantityInput ?? false,
displayMode: config?.displayMode ?? "list",
phaseLabels: config?.phaseLabels ?? { ...DEFAULT_PHASE_LABELS },
infoBar: config?.infoBar ?? { ...DEFAULT_INFO_BAR },
stepControl: config?.stepControl ?? { ...DEFAULT_STEP_CONTROL },
navigation: config?.navigation ?? { ...DEFAULT_NAVIGATION },
resultSections: config?.resultSections ?? [],
};
const update = (partial: Partial<PopWorkDetailConfig>) => {
onChange?.({ ...cfg, ...partial });
};
const [newFieldLabel, setNewFieldLabel] = useState("");
const [newFieldColumn, setNewFieldColumn] = useState("");
const addInfoBarField = () => {
if (!newFieldLabel || !newFieldColumn) return;
const fields = [...(cfg.infoBar.fields ?? []), { label: newFieldLabel, column: newFieldColumn }];
update({ infoBar: { ...cfg.infoBar, fields } });
setNewFieldLabel("");
setNewFieldColumn("");
};
const removeInfoBarField = (idx: number) => {
const fields = (cfg.infoBar.fields ?? []).filter((_, i) => i !== idx);
update({ infoBar: { ...cfg.infoBar, fields } });
};
// --- 실적 입력 섹션 관리 ---
const sections = cfg.resultSections ?? [];
const usedTypes = new Set(sections.map((s) => s.type));
const availableTypes = ALL_SECTION_TYPES.filter((t) => !usedTypes.has(t));
const updateSections = (next: ResultSectionConfig[]) => {
update({ resultSections: next });
};
const addSection = (type: ResultSectionType) => {
const newSection: ResultSectionConfig = {
id: type,
type,
enabled: true,
showCondition: { type: "always" },
};
if (type === "plc-data") {
newSection.plcConfig = { ...DEFAULT_PLC_CONFIG };
}
updateSections([...sections, newSection]);
};
const removeSection = (idx: number) => {
updateSections(sections.filter((_, i) => i !== idx));
};
const toggleSection = (idx: number, enabled: boolean) => {
const next = [...sections];
next[idx] = { ...next[idx], enabled };
updateSections(next);
};
const moveSection = (idx: number, dir: -1 | 1) => {
const target = idx + dir;
if (target < 0 || target >= sections.length) return;
const next = [...sections];
[next[idx], next[target]] = [next[target], next[idx]];
updateSections(next);
};
const updatePlcConfig = (idx: number, partial: Partial<PlcDataConfig>) => {
const next = [...sections];
next[idx] = {
...next[idx],
plcConfig: { ...(next[idx].plcConfig ?? DEFAULT_PLC_CONFIG), ...partial },
};
updateSections(next);
};
// PLC 섹션의 인덱스 찾기
const plcSectionIdx = sections.findIndex((s) => s.type === "plc-data" && s.enabled);
const plcSection = plcSectionIdx >= 0 ? sections[plcSectionIdx] : null;
return (
<div className="space-y-5">
{/* 기본 설정 */}
<Section title="기본 설정">
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Select value={cfg.displayMode} onValueChange={(v) => update({ displayMode: v as "list" | "step" })}>
<SelectTrigger className="h-7 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="list"></SelectItem>
<SelectItem value="step"></SelectItem>
</SelectContent>
</Select>
</div>
<ToggleRow label="타이머 표시" checked={cfg.showTimer} onChange={(v) => update({ showTimer: v })} />
</Section>
{/* 실적 입력 섹션 */}
<Section title="실적 입력 섹션">
{sections.length === 0 ? (
<p className="text-xs text-muted-foreground py-1"> </p>
) : (
<div className="space-y-1">
{sections.map((s, i) => (
<div
key={s.id}
className={
s.type === "plc-data"
? "flex items-center gap-1 rounded-md border-2 border-blue-300 bg-blue-50 px-2 py-1"
: "flex items-center gap-1 rounded-md border px-2 py-1"
}
>
<div className="flex flex-col">
<button
type="button"
className="h-3.5 text-muted-foreground hover:text-foreground disabled:opacity-30"
disabled={i === 0}
onClick={() => moveSection(i, -1)}
>
<ChevronUp className="h-3 w-3" />
</button>
<button
type="button"
className="h-3.5 text-muted-foreground hover:text-foreground disabled:opacity-30"
disabled={i === sections.length - 1}
onClick={() => moveSection(i, 1)}
>
<ChevronDown className="h-3 w-3" />
</button>
</div>
<span className={
s.type === "plc-data"
? "flex-1 truncate text-xs font-medium text-blue-700"
: "flex-1 truncate text-xs font-medium"
}>
{SECTION_TYPE_META[s.type]?.label ?? s.type}
</span>
<Switch
checked={s.enabled}
onCheckedChange={(v) => toggleSection(i, v)}
className="scale-75"
/>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 shrink-0"
onClick={() => removeSection(i)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
{availableTypes.length > 0 && <SectionAdder types={availableTypes} onAdd={addSection} />}
</Section>
{/* PLC 데이터 상세 설정 */}
{plcSection && plcSectionIdx >= 0 && (
<PlcDataSettingsPanel
plcConfig={plcSection.plcConfig ?? DEFAULT_PLC_CONFIG}
onChange={(partial) => updatePlcConfig(plcSectionIdx, partial)}
/>
)}
{/* 정보 바 */}
<Section title="작업지시 정보 바">
<ToggleRow
label="정보 바 표시"
checked={cfg.infoBar.enabled}
onChange={(v) => update({ infoBar: { ...cfg.infoBar, enabled: v } })}
/>
{cfg.infoBar.enabled && (
<div className="space-y-2 pt-1">
{(cfg.infoBar.fields ?? []).map((f, i) => (
<div key={i} className="flex items-center gap-1">
<span className="w-16 truncate text-xs text-muted-foreground">{f.label}</span>
<span className="flex-1 truncate text-xs font-mono">{f.column}</span>
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => removeInfoBarField(i)}>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
<div className="flex items-center gap-1">
<Input className="h-7 text-xs" placeholder="라벨" value={newFieldLabel} onChange={(e) => setNewFieldLabel(e.target.value)} />
<Input className="h-7 text-xs" placeholder="컬럼명" value={newFieldColumn} onChange={(e) => setNewFieldColumn(e.target.value)} />
<Button size="icon" variant="outline" className="h-7 w-7 shrink-0" onClick={addInfoBarField}>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)}
</Section>
{/* 단계 제어 */}
<Section title="단계 제어">
<ToggleRow
label="시작 전 입력 잠금"
checked={cfg.stepControl.requireStartBeforeInput}
onChange={(v) => update({ stepControl: { ...cfg.stepControl, requireStartBeforeInput: v } })}
/>
<ToggleRow
label="완료 시 자동 다음 이동"
checked={cfg.stepControl.autoAdvance}
onChange={(v) => update({ stepControl: { ...cfg.stepControl, autoAdvance: v } })}
/>
</Section>
{/* 네비게이션 */}
<Section title="네비게이션">
<ToggleRow
label="이전/다음 버튼"
checked={cfg.navigation.showPrevNext}
onChange={(v) => update({ navigation: { ...cfg.navigation, showPrevNext: v } })}
/>
<ToggleRow
label="공정 완료 버튼"
checked={cfg.navigation.showCompleteButton}
onChange={(v) => update({ navigation: { ...cfg.navigation, showCompleteButton: v } })}
/>
</Section>
{/* 단계 라벨 */}
<Section title="단계 라벨">
{(["PRE", "IN", "POST"] as const).map((phase) => (
<div key={phase} className="flex items-center gap-2">
<span className="w-12 text-xs font-medium text-muted-foreground">{phase}</span>
<Input
className="h-7 text-xs"
value={cfg.phaseLabels[phase] ?? DEFAULT_PHASE_LABELS[phase]}
onChange={(e) => update({ phaseLabels: { ...cfg.phaseLabels, [phase]: e.target.value } })}
/>
</div>
))}
</Section>
</div>
);
}
// ========================================
// PLC 데이터 상세 설정 패널
// ========================================
interface PlcDataSettingsPanelProps {
plcConfig: PlcDataConfig;
onChange: (partial: Partial<PlcDataConfig>) => void;
}
interface DbConnectionOption {
id: number;
connection_name: string;
}
function PlcDataSettingsPanel({ plcConfig, onChange }: PlcDataSettingsPanelProps) {
// 외부 DB 연결 목록
const [connections, setConnections] = useState<DbConnectionOption[]>([]);
const [connectionsLoading, setConnectionsLoading] = useState(false);
// 테이블 목록
const [tables, setTables] = useState<string[]>([]);
const [tablesLoading, setTablesLoading] = useState(false);
// 컬럼 목록
const [columns, setColumns] = useState<string[]>([]);
const [columnsLoading, setColumnsLoading] = useState(false);
// 디바이스/태그 유니크 값
const [devices, setDevices] = useState<string[]>([]);
const [devicesLoading, setDevicesLoading] = useState(false);
const [tags, setTags] = useState<string[]>([]);
const [tagsLoading, setTagsLoading] = useState(false);
// 1. 외부 DB 연결 목록 로드
useEffect(() => {
let cancelled = false;
setConnectionsLoading(true);
ExternalDbConnectionAPI.getConnections({ is_active: "Y" })
.then((list) => {
if (!cancelled) {
setConnections(
list.map((c) => ({ id: c.id!, connection_name: c.connection_name }))
);
}
})
.catch(() => {
if (!cancelled) setConnections([]);
})
.finally(() => {
if (!cancelled) setConnectionsLoading(false);
});
return () => { cancelled = true; };
}, []);
// 2. 연결 선택 시 테이블 목록 로드
const loadTables = useCallback(async (connId: string) => {
if (!connId) {
setTables([]);
return;
}
setTablesLoading(true);
try {
const res = await ExternalDbConnectionAPI.getTables(Number(connId));
setTables(res.data ?? []);
} catch {
setTables([]);
} finally {
setTablesLoading(false);
}
}, []);
useEffect(() => {
if (plcConfig.connectionId) {
loadTables(plcConfig.connectionId);
}
}, [plcConfig.connectionId, loadTables]);
// 3. 테이블 선택 시 컬럼 목록 로드
const loadColumns = useCallback(async (connId: string, tableName: string) => {
if (!connId || !tableName) {
setColumns([]);
return;
}
setColumnsLoading(true);
try {
const res = await ExternalDbConnectionAPI.getTableColumns(Number(connId), tableName);
const cols = (res.data ?? []).map((c: { column_name?: string; COLUMN_NAME?: string }) =>
c.column_name ?? c.COLUMN_NAME ?? ""
).filter(Boolean);
setColumns(cols);
} catch {
setColumns([]);
} finally {
setColumnsLoading(false);
}
}, []);
useEffect(() => {
if (plcConfig.connectionId && plcConfig.tableName) {
loadColumns(plcConfig.connectionId, plcConfig.tableName);
}
}, [plcConfig.connectionId, plcConfig.tableName, loadColumns]);
// 4. 디바이스/태그 유니크값 로드
const loadUniqueValues = useCallback(async (
connId: string,
tableName: string,
column: string,
setter: (v: string[]) => void,
setLoading: (v: boolean) => void,
) => {
if (!connId || !tableName || !column) {
setter([]);
return;
}
setLoading(true);
try {
const query = `SELECT DISTINCT "${column}" FROM "${tableName}" WHERE "${column}" IS NOT NULL ORDER BY "${column}" LIMIT 100`;
const res = await ExternalDbConnectionAPI.executeQuery(Number(connId), query);
const values = (res.data ?? []).map((row: Record<string, unknown>) => String(row[column] ?? "")).filter(Boolean);
setter(values);
} catch {
setter([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (plcConfig.connectionId && plcConfig.tableName && plcConfig.deviceColumn) {
loadUniqueValues(plcConfig.connectionId, plcConfig.tableName, plcConfig.deviceColumn, setDevices, setDevicesLoading);
} else {
setDevices([]);
}
}, [plcConfig.connectionId, plcConfig.tableName, plcConfig.deviceColumn, loadUniqueValues]);
// 태그 필터 값 로드 - deviceColumn과 다른 텍스트 컬럼이면 유니크 조회
// 실제로는 device_id 선택 후 해당 디바이스의 tag_name 유니크값을 로드
useEffect(() => {
if (!plcConfig.connectionId || !plcConfig.tableName || !plcConfig.deviceColumn) {
setTags([]);
return;
}
// tag는 deviceColumn이 아닌 나머지 text-like 컬럼에서 가져오거나,
// 사용자가 직접 입력 가능. 여기서는 device filter가 선택된 경우 해당 device의 남은 텍스트 컬럼에서 unique를 가져옴
// 간단히: deviceColumn 외의 첫 텍스트 컬럼 또는 사용자가 지정하도록 함
// 지금은 deviceFilter가 선택되었을 때 deviceColumn 조건으로 다른 유니크값 조회
const tagColumn = columns.find(
(c) => c !== plcConfig.deviceColumn && c !== plcConfig.valueColumn && c !== plcConfig.timestampColumn
);
if (tagColumn && plcConfig.deviceFilter) {
const query = `SELECT DISTINCT "${tagColumn}" FROM "${plcConfig.tableName}" WHERE "${plcConfig.deviceColumn}" = '${plcConfig.deviceFilter}' AND "${tagColumn}" IS NOT NULL ORDER BY "${tagColumn}" LIMIT 100`;
setTagsLoading(true);
ExternalDbConnectionAPI.executeQuery(Number(plcConfig.connectionId), query)
.then((res) => {
setTags((res.data ?? []).map((row: Record<string, unknown>) => String(row[tagColumn] ?? "")).filter(Boolean));
})
.catch(() => setTags([]))
.finally(() => setTagsLoading(false));
} else {
setTags([]);
}
}, [plcConfig.connectionId, plcConfig.tableName, plcConfig.deviceColumn, plcConfig.deviceFilter, plcConfig.valueColumn, plcConfig.timestampColumn, columns]);
return (
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 items-center justify-center rounded bg-blue-500">
<Zap className="h-3 w-3 text-white" />
</div>
<span className="text-xs font-semibold text-blue-800">PLC </span>
</div>
{/* PLC 연동 - DB 연결 */}
<div className="space-y-2">
<div className="text-[11px] font-medium text-blue-700">PLC </div>
{/* DB 연결 */}
<div className="space-y-1">
<span className="text-xs text-gray-600">DB </span>
<Select
value={plcConfig.connectionId}
onValueChange={(v) => onChange({ connectionId: v, tableName: "", deviceColumn: "", valueColumn: "", timestampColumn: "", deviceFilter: "", tagFilter: "" })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={connectionsLoading ? "불러오는 중..." : "외부 DB 선택"} />
</SelectTrigger>
<SelectContent>
{connections.map((c) => (
<SelectItem key={c.id} value={String(c.id)} className="text-xs">
{c.connection_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 테이블 */}
<div className="space-y-1">
<span className="text-xs text-gray-600"></span>
<Select
value={plcConfig.tableName}
onValueChange={(v) => onChange({ tableName: v, deviceColumn: "", valueColumn: "", timestampColumn: "", deviceFilter: "", tagFilter: "" })}
disabled={!plcConfig.connectionId}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={tablesLoading ? "불러오는 중..." : "테이블 선택"} />
</SelectTrigger>
<SelectContent>
{tables.map((t) => (
<SelectItem key={t} value={t} className="text-xs">{t}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 디바이스 컬럼 */}
<div className="space-y-1">
<span className="text-xs text-gray-600"> </span>
<Select
value={plcConfig.deviceColumn}
onValueChange={(v) => onChange({ deviceColumn: v, deviceFilter: "", tagFilter: "" })}
disabled={!plcConfig.tableName}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={columnsLoading ? "불러오는 중..." : "컬럼 선택"} />
</SelectTrigger>
<SelectContent>
{columns.map((c) => (
<SelectItem key={c} value={c} className="text-xs">{c}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 값 컬럼 */}
<div className="space-y-1">
<span className="text-xs text-gray-600"> </span>
<Select
value={plcConfig.valueColumn}
onValueChange={(v) => onChange({ valueColumn: v })}
disabled={!plcConfig.tableName}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns.map((c) => (
<SelectItem key={c} value={c} className="text-xs">{c}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 타임스탬프 컬럼 */}
<div className="space-y-1">
<span className="text-xs text-gray-600"> </span>
<Select
value={plcConfig.timestampColumn}
onValueChange={(v) => onChange({ timestampColumn: v })}
disabled={!plcConfig.tableName}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{columns.map((c) => (
<SelectItem key={c} value={c} className="text-xs">{c}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 디바이스 필터 */}
<div className="space-y-2">
<div className="text-[11px] font-medium text-blue-700"> </div>
<div className="space-y-1">
<span className="text-xs text-gray-600"></span>
{devicesLoading ? (
<div className="flex items-center gap-1 text-xs text-gray-400">
<Loader2 className="h-3 w-3 animate-spin" /> ...
</div>
) : devices.length > 0 ? (
<Select
value={plcConfig.deviceFilter}
onValueChange={(v) => onChange({ deviceFilter: v, tagFilter: "" })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="디바이스 선택" />
</SelectTrigger>
<SelectContent>
{devices.map((d) => (
<SelectItem key={d} value={d} className="text-xs">{d}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
className="h-8 text-xs"
placeholder="디바이스 ID 직접 입력"
value={plcConfig.deviceFilter}
onChange={(e) => onChange({ deviceFilter: e.target.value })}
/>
)}
</div>
<div className="space-y-1">
<span className="text-xs text-gray-600"></span>
{tagsLoading ? (
<div className="flex items-center gap-1 text-xs text-gray-400">
<Loader2 className="h-3 w-3 animate-spin" /> ...
</div>
) : tags.length > 0 ? (
<Select
value={plcConfig.tagFilter}
onValueChange={(v) => onChange({ tagFilter: v })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="태그 선택" />
</SelectTrigger>
<SelectContent>
{tags.map((t) => (
<SelectItem key={t} value={t} className="text-xs">{t}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
className="h-8 text-xs"
placeholder="태그명 직접 입력"
value={plcConfig.tagFilter}
onChange={(e) => onChange({ tagFilter: e.target.value })}
/>
)}
</div>
</div>
{/* 표시 설정 */}
<div className="space-y-2">
<div className="text-[11px] font-medium text-blue-700"> </div>
<div className="space-y-1">
<span className="text-xs text-gray-600"></span>
<Input
className="h-8 text-xs"
placeholder="예: 총 생산수량"
value={plcConfig.label}
onChange={(e) => onChange({ label: e.target.value })}
/>
</div>
<div className="space-y-1">
<span className="text-xs text-gray-600"></span>
<Input
className="h-8 text-xs"
placeholder="예: EA"
value={plcConfig.unit}
onChange={(e) => onChange({ unit: e.target.value })}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-600"> </span>
<Select
value={String(plcConfig.refreshInterval)}
onValueChange={(v) => onChange({ refreshInterval: Number(v) })}
>
<SelectTrigger className="h-7 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10" className="text-xs">10</SelectItem>
<SelectItem value="30" className="text-xs">30</SelectItem>
<SelectItem value="60" className="text-xs">60</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-600"> </span>
<Select
value={plcConfig.displayMode}
onValueChange={(v) => onChange({ displayMode: v as "number" | "gauge" })}
>
<SelectTrigger className="h-7 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="number" className="text-xs"></SelectItem>
<SelectItem value="gauge" className="text-xs"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="border-t border-blue-200" />
{/* 매핑 저장 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-700"> </span>
<Switch
checked={plcConfig.mapping?.enabled ?? false}
onCheckedChange={(v) =>
onChange({
mapping: {
enabled: v,
targetTable: plcConfig.mapping?.targetTable ?? "",
targetColumn: plcConfig.mapping?.targetColumn ?? "",
mode: plcConfig.mapping?.mode ?? "latest",
},
})
}
/>
</div>
{plcConfig.mapping?.enabled && (
<>
<div className="space-y-1">
<span className="text-xs text-gray-600"> </span>
<div className="flex gap-1">
<Input
className="h-7 flex-1 text-xs"
placeholder="테이블명"
value={plcConfig.mapping.targetTable}
onChange={(e) =>
onChange({
mapping: { ...plcConfig.mapping!, targetTable: e.target.value },
})
}
/>
<Input
className="h-7 flex-1 text-xs"
placeholder="컬럼명"
value={plcConfig.mapping.targetColumn}
onChange={(e) =>
onChange({
mapping: { ...plcConfig.mapping!, targetColumn: e.target.value },
})
}
/>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-600"> </span>
<Select
value={plcConfig.mapping.mode}
onValueChange={(v) =>
onChange({
mapping: { ...plcConfig.mapping!, mode: v as "latest" | "accumulated" | "delta" },
})
}
>
<SelectTrigger className="h-7 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="latest" className="text-xs">latest</SelectItem>
<SelectItem value="accumulated" className="text-xs">accumulated</SelectItem>
<SelectItem value="delta" className="text-xs">delta</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
</div>
</div>
);
}
// ========================================
// 공통 하위 컴포넌트
// ========================================
function SectionAdder({
types,
onAdd,
}: {
types: ResultSectionType[];
onAdd: (type: ResultSectionType) => void;
}) {
const [selected, setSelected] = useState<string>("");
const handleAdd = () => {
if (!selected) return;
onAdd(selected as ResultSectionType);
setSelected("");
};
return (
<div className="flex items-center gap-1 pt-1">
<Select value={selected} onValueChange={setSelected}>
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue placeholder="섹션 선택" />
</SelectTrigger>
<SelectContent>
{types.map((t) => (
<SelectItem key={t} value={t} className="text-xs">
{SECTION_TYPE_META[t]?.label ?? t}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
variant="outline"
className="h-7 shrink-0 gap-1 px-2 text-xs"
disabled={!selected}
onClick={handleAdd}
>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="space-y-2">
<div className="text-xs font-semibold text-muted-foreground">{title}</div>
{children}
</div>
);
}
function ToggleRow({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
return (
<div className="flex items-center justify-between">
<Label className="text-xs">{label}</Label>
<Switch checked={checked} onCheckedChange={onChange} />
</div>
);
}