[agent-pipeline] pipe-20260317063830-0nfs round-1

This commit is contained in:
DDD1542
2026-03-17 16:20:24 +09:00
parent 80cd95e683
commit 128872b766
8 changed files with 1013 additions and 518 deletions

View File

@@ -2,18 +2,19 @@
/**
* V2 카테고리 관리 컴포넌트
* - 트리 구조 기반 카테고리 값 관리
* - 대시보드 레이아웃: Stat Strip + 좌측 테이블 nav + 칩 바 + 트리/목록 편집기
* - 3단계 계층 구조 지원 (대분류/중분류/소분류)
*/
import React, { useState, useCallback, useEffect } from "react";
import React, { useState, useCallback, useMemo } from "react";
import { CategoryColumnList } from "@/components/table-category/CategoryColumnList";
import type { CategoryColumn } from "@/components/table-category/CategoryColumnList";
import { CategoryValueManager } from "@/components/table-category/CategoryValueManager";
import { CategoryValueManagerTree } from "@/components/table-category/CategoryValueManagerTree";
import { LayoutList, TreeDeciduous } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { ResponsiveSplitPanel } from "@/components/common/ResponsiveSplitPanel";
import { V2CategoryManagerConfig, defaultV2CategoryManagerConfig, ViewMode } from "./types";
interface V2CategoryManagerComponentProps {
@@ -33,53 +34,62 @@ export function V2CategoryManagerComponent({
componentConfig,
...props
}: V2CategoryManagerComponentProps) {
// 설정 병합 (componentConfig도 포함)
const config: V2CategoryManagerConfig = {
...defaultV2CategoryManagerConfig,
...externalConfig,
...componentConfig,
};
// tableName 우선순위: props > selectedScreen > componentConfig
const effectiveTableName = tableName || selectedScreen?.tableName || (componentConfig as any)?.tableName || "";
// menuObjid 우선순위: props > selectedScreen
const effectiveTableName =
tableName || selectedScreen?.tableName || (componentConfig as any)?.tableName || "";
const propsMenuObjid = typeof props.menuObjid === "number" ? props.menuObjid : undefined;
const effectiveMenuObjid = menuObjid || propsMenuObjid || selectedScreen?.menuObjid;
// 디버그 로그
useEffect(() => {
console.log("🔍 V2CategoryManagerComponent props:", {
tableName,
menuObjid,
selectedScreen,
effectiveTableName,
effectiveMenuObjid,
config,
});
}, [tableName, menuObjid, selectedScreen, effectiveTableName, effectiveMenuObjid, config]);
// 선택된 컬럼 상태
const [columns, setColumns] = useState<CategoryColumn[]>([]);
const [selectedTable, setSelectedTable] = useState<string | null>(null);
const [selectedColumn, setSelectedColumn] = useState<{
uniqueKey: string;
columnName: string;
columnLabel: string;
tableName: string;
} | null>(null);
// 뷰 모드 상태
const [viewMode, setViewMode] = useState<ViewMode>(config.viewMode);
// 컬럼 선택 핸들러
const handleColumnSelect = useCallback((uniqueKey: string, columnLabel: string, tableName: string) => {
const columnName = uniqueKey.split(".")[1];
setSelectedColumn({ uniqueKey, columnName, columnLabel, tableName });
const handleColumnsLoaded = useCallback((loaded: CategoryColumn[]) => {
setColumns(loaded);
if (loaded.length > 0) {
setSelectedTable((prev) => prev ?? loaded[0].tableName);
}
}, []);
// 우측 패널 콘텐츠
const handleTableSelect = useCallback((tableName: string) => {
setSelectedTable(tableName);
setSelectedColumn(null);
}, []);
const handleColumnSelect = useCallback(
(uniqueKey: string, columnLabel: string, colTableName: string) => {
const columnName = uniqueKey.includes(".") ? uniqueKey.split(".")[1] : uniqueKey;
setSelectedColumn({ uniqueKey: uniqueKey.includes(".") ? uniqueKey : `${colTableName}.${uniqueKey}`, columnName, columnLabel, tableName: colTableName });
},
[],
);
const stats = useMemo(() => {
const columnCount = columns.length;
const totalValues = columns.reduce((sum, c) => sum + (c.valueCount ?? 0), 0);
const tableCount = new Set(columns.map((c) => c.tableName)).size;
const inactiveCount = 0;
return { columnCount, totalValues, tableCount, inactiveCount };
}, [columns]);
const columnsForSelectedTable = useMemo(
() => (selectedTable ? columns.filter((c) => c.tableName === selectedTable) : []),
[columns, selectedTable],
);
const rightContent = (
<>
{/* 뷰 모드 토글 */}
{config.showViewModeToggle && (
<div className="mb-2 flex items-center justify-end gap-1">
<span className="text-muted-foreground mr-2 text-xs"> :</span>
@@ -105,8 +115,6 @@ export function V2CategoryManagerComponent({
</div>
</div>
)}
{/* 카테고리 값 관리 */}
<div className="min-h-0 flex-1 overflow-y-auto">
{selectedColumn ? (
viewMode === "tree" ? (
@@ -130,7 +138,9 @@ export function V2CategoryManagerComponent({
<div className="flex flex-col items-center gap-2 text-center">
<TreeDeciduous className="text-muted-foreground/30 h-10 w-10" />
<p className="text-muted-foreground text-sm">
{config.showColumnList ? "좌측에서 관리할 카테고리 컬럼을 선택하세요" : "카테고리 컬럼이 설정되지 않았습니다"}
{config.showColumnList
? "칩에서 카테고리 컬럼을 선택하세요"
: "카테고리 컬럼이 설정되지 않았습니다"}
</p>
</div>
</div>
@@ -148,24 +158,107 @@ export function V2CategoryManagerComponent({
}
return (
<ResponsiveSplitPanel
left={
<CategoryColumnList
tableName={effectiveTableName}
selectedColumn={selectedColumn?.uniqueKey || null}
onColumnSelect={handleColumnSelect}
menuObjid={effectiveMenuObjid}
/>
}
right={rightContent}
leftTitle="카테고리 컬럼"
leftWidth={config.leftPanelWidth}
minLeftWidth={10}
maxLeftWidth={40}
height={config.height}
/>
<div
className="flex h-full flex-col overflow-hidden rounded-lg border bg-card text-card-foreground shadow-sm"
style={{ height: config.height }}
>
{/* Stat Strip */}
<div className="grid grid-cols-4 border-b bg-background">
<div className="border-r py-3.5 text-center last:border-r-0">
<div className="text-[22px] font-extrabold leading-none tracking-tight text-primary">
{stats.columnCount}
</div>
<div className="mt-1 text-[9px] font-semibold uppercase tracking-widest text-muted-foreground">
</div>
</div>
<div className="border-r py-3.5 text-center last:border-r-0">
<div className="text-[22px] font-extrabold leading-none tracking-tight text-primary">
{stats.totalValues}
</div>
<div className="mt-1 text-[9px] font-semibold uppercase tracking-widest text-muted-foreground">
</div>
</div>
<div className="border-r py-3.5 text-center last:border-r-0">
<div className="text-[22px] font-extrabold leading-none tracking-tight text-primary">
{stats.tableCount}
</div>
<div className="mt-1 text-[9px] font-semibold uppercase tracking-widest text-muted-foreground">
</div>
</div>
<div className="py-3.5 text-center">
<div className="text-[22px] font-extrabold leading-none tracking-tight text-primary">
{stats.inactiveCount}
</div>
<div className="mt-1 text-[9px] font-semibold uppercase tracking-widest text-muted-foreground">
</div>
</div>
</div>
<div className="flex min-h-0 flex-1">
{/* 좌측 테이블 nav: 240px */}
<div className="flex w-[240px] shrink-0 flex-col border-r">
<CategoryColumnList
tableName={effectiveTableName}
selectedColumn={selectedColumn?.uniqueKey ?? null}
onColumnSelect={handleColumnSelect}
menuObjid={effectiveMenuObjid}
selectedTable={selectedTable}
onTableSelect={setSelectedTable}
onColumnsLoaded={handleColumnsLoaded}
/>
</div>
{/* 우측: 칩 바 + 편집기 */}
<div className="flex min-w-0 flex-1 flex-col">
{/* 칩 바 */}
<div className="flex flex-wrap gap-1.5 border-b bg-background px-4 py-3">
{columnsForSelectedTable.map((col) => {
const uniqueKey = `${col.tableName}.${col.columnName}`;
const isActive = selectedColumn?.uniqueKey === uniqueKey;
return (
<button
key={uniqueKey}
type="button"
onClick={() =>
handleColumnSelect(uniqueKey, col.columnLabel || col.columnName, col.tableName)
}
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1.5 text-[11px] font-semibold transition-colors",
isActive
? "border-primary bg-primary/5 text-primary"
: "border-border bg-muted/50 hover:border-primary hover:bg-primary/5 hover:text-primary",
)}
>
<span>{col.columnLabel || col.columnName}</span>
<Badge
variant="secondary"
className={cn(
"h-4 rounded-full px-1.5 text-[9px] font-bold",
isActive ? "bg-primary/15 text-primary" : "bg-muted text-muted-foreground",
)}
>
{col.valueCount ?? 0}
</Badge>
</button>
);
})}
{selectedTable && columnsForSelectedTable.length === 0 && (
<span className="text-muted-foreground text-xs"> </span>
)}
</div>
{/* 편집기 영역 */}
<div className="flex min-h-0 flex-1 flex-col overflow-hidden p-3">
{rightContent}
</div>
</div>
</div>
</div>
);
}
export default V2CategoryManagerComponent;

View File

@@ -10,7 +10,6 @@ import { Badge } from "@/components/ui/badge";
import {
Plus,
Search,
GripVertical,
Loader2,
ChevronDown,
ChevronUp,
@@ -21,6 +20,8 @@ import {
Settings,
Move,
FileSpreadsheet,
List,
LayoutPanelRight,
} from "lucide-react";
import { dataApi } from "@/lib/api/data";
import { entityJoinApi } from "@/lib/api/entityJoin";
@@ -325,6 +326,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
// 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 순서 변경)
const [rightDraggedColumnIndex, setRightDraggedColumnIndex] = useState<number | null>(null);
const [rightDropTargetColumnIndex, setRightDropTargetColumnIndex] = useState<number | null>(null);
const [rightDragSource, setRightDragSource] = useState<"main" | number | null>(null);
// 데이터 상태
const [leftData, setLeftData] = useState<any[]>([]);
@@ -2631,6 +2636,95 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}
}, [selectedLeftItem, customLeftSelectedData, componentConfig, companyCode, toast, loadLeftData]);
// 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 컬럼 순서 변경)
const handleRightColumnDragStart = useCallback(
(columnIndex: number, source: "main" | number) => {
setRightDraggedColumnIndex(columnIndex);
setRightDragSource(source);
},
[],
);
const handleRightColumnDragOver = useCallback((e: React.DragEvent, columnIndex: number) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setRightDropTargetColumnIndex(columnIndex);
}, []);
const handleRightColumnDragEnd = useCallback(() => {
setRightDraggedColumnIndex(null);
setRightDropTargetColumnIndex(null);
setRightDragSource(null);
}, []);
const handleRightColumnDrop = useCallback(
(e: React.DragEvent, targetIndex: number, source: "main" | number) => {
e.preventDefault();
const fromIdx = rightDraggedColumnIndex;
if (fromIdx === null || rightDragSource !== source || fromIdx === targetIndex) {
handleRightColumnDragEnd();
return;
}
if (!onUpdateComponent) {
handleRightColumnDragEnd();
return;
}
const rightPanel = componentConfig.rightPanel || {};
if (source === "main") {
const allColumns = rightPanel.columns || [];
const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false);
const hiddenColumns = allColumns.filter((c: any) => c.showInSummary === false);
if (fromIdx < 0 || fromIdx >= visibleColumns.length || targetIndex < 0 || targetIndex >= visibleColumns.length) {
handleRightColumnDragEnd();
return;
}
const reordered = [...visibleColumns];
const [removed] = reordered.splice(fromIdx, 1);
reordered.splice(targetIndex, 0, removed);
const columns = [...reordered, ...hiddenColumns];
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
rightPanel: { ...rightPanel, columns },
},
});
} else {
const tabs = [...(rightPanel.additionalTabs || [])];
const tabConfig = tabs[source];
if (!tabConfig || !Array.isArray(tabConfig.columns)) {
handleRightColumnDragEnd();
return;
}
const allTabCols = tabConfig.columns;
const visibleTabCols = allTabCols.filter((c: any) => c.showInSummary !== false);
const hiddenTabCols = allTabCols.filter((c: any) => c.showInSummary === false);
if (fromIdx < 0 || fromIdx >= visibleTabCols.length || targetIndex < 0 || targetIndex >= visibleTabCols.length) {
handleRightColumnDragEnd();
return;
}
const reordered = [...visibleTabCols];
const [removed] = reordered.splice(fromIdx, 1);
reordered.splice(targetIndex, 0, removed);
const columns = [...reordered, ...hiddenTabCols];
const newTabs = tabs.map((t, i) => (i === source ? { ...t, columns } : t));
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
rightPanel: { ...rightPanel, additionalTabs: newTabs },
},
});
}
handleRightColumnDragEnd();
},
[
rightDraggedColumnIndex,
rightDragSource,
componentConfig,
component,
onUpdateComponent,
handleRightColumnDragEnd,
],
);
// 수정 모달 저장
const handleEditModalSave = useCallback(async () => {
const tableName =
@@ -3212,10 +3306,18 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
alignItems: "center",
}}
>
<div className="flex w-full items-center justify-between">
<CardTitle className="text-base font-semibold">
{componentConfig.leftPanel?.title || "좌측 패널"}
</CardTitle>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<List className="h-4 w-4 shrink-0 text-muted-foreground" />
<CardTitle className="truncate text-base font-semibold">
{componentConfig.leftPanel?.title || "좌측 패널"}
</CardTitle>
{!isDesignMode && (
<Badge variant="secondary" className="shrink-0 text-xs">
{summedLeftData.length}
</Badge>
)}
</div>
<div className="flex items-center gap-1">
{!isDesignMode && (componentConfig.leftPanel as any)?.showBomExcelUpload && (
<Button size="sm" variant="outline" onClick={() => setBomExcelUploadOpen(true)}>
@@ -4011,13 +4113,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</Card>
</div>
{/* 리사이저 */}
{/* 리사이저: 6px 너비, 그립 핸들(2x28px bar), hover 시 primary 하이라이트 */}
{resizable && (
<div
onMouseDown={handleMouseDown}
className="group bg-border hover:bg-primary flex w-1 cursor-col-resize items-center justify-center transition-colors"
className="group flex w-1.5 cursor-col-resize flex-col items-center justify-center gap-0.5 bg-border transition-colors hover:bg-primary"
aria-label="분할선 드래그"
>
<GripVertical className="text-muted-foreground group-hover:text-primary-foreground h-4 w-4" />
<div className="h-7 w-0.5 rounded-full bg-muted-foreground/40 transition-colors group-hover:bg-primary-foreground/80" />
<div className="h-7 w-0.5 rounded-full bg-muted-foreground/40 transition-colors group-hover:bg-primary-foreground/80" />
</div>
)}
@@ -4037,9 +4141,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
alignItems: "center",
}}
>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-0">
{/* 탭이 없으면 제목만, 있으면 탭으로 전환 */}
<div className="flex w-full items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<LayoutPanelRight className="h-4 w-4 shrink-0 text-muted-foreground" />
{/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */}
{(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
<div className="flex items-center gap-0">
<button
@@ -4069,10 +4174,19 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
))}
</div>
) : (
<CardTitle className="text-base font-semibold">
<CardTitle className="truncate text-base font-semibold">
{componentConfig.rightPanel?.title || "우측 패널"}
</CardTitle>
)}
{!isDesignMode && (
<Badge variant="secondary" className="shrink-0 text-xs">
{activeTabIndex === 0
? Array.isArray(rightData)
? rightData.length
: rightData ? 1 : 0
: (tabsData[activeTabIndex]?.length ?? 0)}
</Badge>
)}
</div>
{!isDesignMode && (
<div className="flex items-center gap-2">
@@ -4163,16 +4277,35 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete;
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
const tabIndex = activeTabIndex - 1;
const canDragTabColumns = isDesignMode && tabSummaryColumns.length > 0 && !!onUpdateComponent;
return (
<div className="h-full overflow-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 z-10 bg-background">
<tr className="border-b-2 border-border/60">
{tabSummaryColumns.map((col: any) => (
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
{col.label || col.name}
</th>
))}
{tabSummaryColumns.map((col: any, idx: number) => {
const isDropTarget = rightDragSource === tabIndex && rightDropTargetColumnIndex === idx;
const isDragging = rightDragSource === tabIndex && rightDraggedColumnIndex === idx;
return (
<th
key={col.name}
className={cn(
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold",
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
canDragTabColumns && "cursor-grab active:cursor-grabbing",
isDragging && "opacity-50",
)}
draggable={canDragTabColumns}
onDragStart={() => canDragTabColumns && handleRightColumnDragStart(idx, tabIndex)}
onDragOver={(e) => canDragTabColumns && handleRightColumnDragOver(e, idx)}
onDragEnd={handleRightColumnDragEnd}
onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)}
>
{col.label || col.name}
</th>
);
})}
{hasTabActions && (
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold"></th>
)}
@@ -4280,16 +4413,35 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete;
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
const listTabIndex = activeTabIndex - 1;
const canDragListTabColumns = isDesignMode && listSummaryColumns.length > 0 && !!onUpdateComponent;
return (
<div className="h-full overflow-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 z-10 bg-background">
<tr className="border-b-2 border-border/60">
{listSummaryColumns.map((col: any) => (
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
{col.label || col.name}
</th>
))}
{listSummaryColumns.map((col: any, idx: number) => {
const isDropTarget = rightDragSource === listTabIndex && rightDropTargetColumnIndex === idx;
const isDragging = rightDragSource === listTabIndex && rightDraggedColumnIndex === idx;
return (
<th
key={col.name}
className={cn(
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold",
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
isDragging && "opacity-50",
)}
draggable={canDragListTabColumns}
onDragStart={() => canDragListTabColumns && handleRightColumnDragStart(idx, listTabIndex)}
onDragOver={(e) => canDragListTabColumns && handleRightColumnDragOver(e, idx)}
onDragEnd={handleRightColumnDragEnd}
onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)}
>
{col.label || col.name}
</th>
);
})}
{hasTabActions && (
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold"></th>
)}
@@ -4672,24 +4824,43 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return sum + w;
}, 0);
const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length;
const canDragRightColumns = isDesignMode && displayColumns.length > 0 && !!onUpdateComponent;
return (
<div className="flex h-full w-full flex-col">
<div className="min-h-0 flex-1 overflow-auto">
<table className="table-fixed" style={{ width: rightTotalColWidth > 100 ? `${rightTotalColWidth}%` : '100%' }}>
<thead className="sticky top-0 z-10">
<tr className="border-b-2 border-border/60">
{columnsToShow.map((col, idx) => (
<th
key={idx}
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
style={{
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
textAlign: col.align || "left",
}}
>
{col.label}
</th>
))}
{columnsToShow.map((col, idx) => {
const configColIndex = idx - rightConfigColumnStart;
const isDraggable = canDragRightColumns && !col._isKeyColumn;
const isDropTarget = rightDragSource === "main" && rightDropTargetColumnIndex === configColIndex;
const isDragging = rightDragSource === "main" && rightDraggedColumnIndex === configColIndex;
return (
<th
key={idx}
className={cn(
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap",
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
isDraggable && "cursor-grab active:cursor-grabbing",
isDragging && "opacity-50",
)}
style={{
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
textAlign: col.align || "left",
}}
draggable={isDraggable}
onDragStart={() => isDraggable && handleRightColumnDragStart(configColIndex, "main")}
onDragOver={(e) => isDraggable && handleRightColumnDragOver(e, configColIndex)}
onDragEnd={handleRightColumnDragEnd}
onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")}
>
{col.label}
</th>
);
})}
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 컬럼 표시 */}
{!isDesignMode &&
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
@@ -4705,7 +4876,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const itemId = item.id || item.ID || idx;
return (
<tr key={itemId} className={cn("border-b border-border/40 transition-colors hover:bg-muted/30", idx % 2 === 1 && "bg-muted/10")}>
<tr key={itemId} className={cn("group/action border-b border-border/40 transition-colors hover:bg-muted/30", idx % 2 === 1 && "bg-muted/10")}>
{columnsToShow.map((col, colIdx) => (
<td
key={colIdx}
@@ -4726,8 +4897,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{!isDesignMode &&
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
<td className="px-3 py-2 text-right text-sm whitespace-nowrap">
<div className="flex justify-end gap-1">
<td className="px-3 py-2 text-right text-sm whitespace-nowrap group/action">
<div className="flex justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
<Button
variant={
@@ -4850,7 +5021,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<React.Fragment key={itemId}>
<tr
className={cn(
"cursor-pointer border-b border-border/40 transition-colors",
"group/action cursor-pointer border-b border-border/40 transition-colors",
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
)}
onClick={() => toggleRightItemExpansion(itemId)}
@@ -4867,7 +5038,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
))}
{hasActions && (
<td className="px-3 py-2 text-right">
<div className="flex items-center justify-end gap-1">
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
{hasEditButton && (
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
onClick={(e) => {
@@ -4984,8 +5155,31 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
console.log(" ⚠️ 컬럼 설정 없음, 모든 컬럼 표시");
}
const hasDetailEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
const hasDetailDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
return (
<div className="space-y-2">
{(hasDetailEditButton || hasDetailDeleteButton) && (
<div className="flex items-center justify-end gap-1 pb-1">
{hasDetailEditButton && (
<Button size="sm" variant="outline" className="h-7 gap-1 px-2 text-xs"
onClick={() => handleEditClick("right", rightData)}
>
<Pencil className="h-3 w-3" />
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
</Button>
)}
{hasDetailDeleteButton && (
<Button size="sm" variant="ghost" className="text-destructive h-7 gap-1 px-2 text-xs"
onClick={() => handleDeleteClick("right", rightData)}
>
<Trash2 className="h-3 w-3" />
{componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
</Button>
)}
</div>
)}
{displayEntries.map(([key, value, label]) => (
<div key={key} className="bg-card rounded-lg border p-4 shadow-sm">
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">

View File

@@ -35,7 +35,11 @@ interface SingleTableWithStickyProps {
editingValue?: string;
onEditingValueChange?: (value: string) => void;
onEditKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
editInputRef?: React.RefObject<HTMLInputElement>;
onEditSave?: () => void;
editInputRef?: React.RefObject<HTMLInputElement | HTMLSelectElement>;
// 인라인 편집 타입별 옵션 (select/category/code, number, date 지원)
columnMeta?: Record<string, { inputType?: string }>;
categoryMappings?: Record<string, Record<string, { label: string }>>;
// 검색 하이라이트 관련 props
searchHighlights?: Set<string>;
currentSearchIndex?: number;
@@ -69,7 +73,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
editingValue,
onEditingValueChange,
onEditKeyDown,
onEditSave,
editInputRef,
columnMeta,
categoryMappings,
// 검색 하이라이트 관련 props
searchHighlights,
currentSearchIndex = 0,
@@ -350,15 +357,19 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
{column.columnName === "__checkbox__" ? (
renderCheckboxCell?.(row, index)
) : isEditing ? (
// 인라인 편집 입력 필드
<input
ref={editInputRef}
type="text"
value={editingValue ?? ""}
onChange={(e) => onEditingValueChange?.(e.target.value)}
onKeyDown={onEditKeyDown}
onBlur={() => {
// blur 시 저장 (Enter와 동일)
// 인라인 편집: inputType에 따라 select(category/code), number, date, text
(() => {
const meta = columnMeta?.[column.columnName];
const inputType = meta?.inputType ?? (column as { inputType?: string }).inputType;
const isNumeric = inputType === "number" || inputType === "decimal";
const isCategoryType = inputType === "category" || inputType === "code";
const categoryOptions = categoryMappings?.[column.columnName];
const hasCategoryOptions =
isCategoryType && categoryOptions && Object.keys(categoryOptions).length > 0;
const commonInputClass =
"border-primary bg-background focus:ring-primary h-8 w-full rounded border px-2 text-xs focus:ring-2 focus:outline-none sm:text-sm";
const handleBlurSave = () => {
if (onEditKeyDown) {
const fakeEvent = {
key: "Enter",
@@ -366,10 +377,78 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
} as React.KeyboardEvent<HTMLInputElement>;
onEditKeyDown(fakeEvent);
}
}}
className="border-primary bg-background focus:ring-primary h-8 w-full rounded border px-2 text-xs focus:ring-2 focus:outline-none sm:text-sm"
onClick={(e) => e.stopPropagation()}
/>
onEditSave?.();
};
if (hasCategoryOptions) {
const selectOptions = Object.entries(categoryOptions).map(([value, info]) => ({
value,
label: info.label,
}));
return (
<select
ref={editInputRef as React.RefObject<HTMLSelectElement>}
value={editingValue ?? ""}
onChange={(e) => onEditingValueChange?.(e.target.value)}
onKeyDown={onEditKeyDown}
onBlur={handleBlurSave}
className={commonInputClass}
onClick={(e) => e.stopPropagation()}
>
<option value=""></option>
{selectOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}
if (inputType === "date" || inputType === "datetime") {
try {
const { InlineCellDatePicker } = require("@/components/screen/filters/InlineCellDatePicker");
return (
<InlineCellDatePicker
value={editingValue ?? ""}
onChange={(v) => onEditingValueChange?.(v)}
onSave={() => {
handleBlurSave();
}}
onKeyDown={onEditKeyDown}
inputRef={editInputRef as React.RefObject<HTMLInputElement>}
/>
);
} catch {
return (
<input
ref={editInputRef as React.RefObject<HTMLInputElement>}
type="text"
value={editingValue ?? ""}
onChange={(e) => onEditingValueChange?.(e.target.value)}
onKeyDown={onEditKeyDown}
onBlur={handleBlurSave}
className={commonInputClass}
onClick={(e) => e.stopPropagation()}
/>
);
}
}
return (
<input
ref={editInputRef as React.RefObject<HTMLInputElement>}
type={isNumeric ? "number" : "text"}
value={editingValue ?? ""}
onChange={(e) => onEditingValueChange?.(e.target.value)}
onKeyDown={onEditKeyDown}
onBlur={handleBlurSave}
className={commonInputClass}
style={isNumeric ? { textAlign: "right" } : undefined}
onClick={(e) => e.stopPropagation()}
/>
);
})()
) : (
renderCellContent()
)}

View File

@@ -5463,6 +5463,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}}
getColumnWidth={getColumnWidth}
containerWidth={calculatedWidth}
onCellDoubleClick={handleCellDoubleClick}
editingCell={editingCell}
editingValue={editingValue}
onEditingValueChange={setEditingValue}
onEditKeyDown={handleEditKeyDown}
onEditSave={saveEditing}
editInputRef={editInputRef}
columnMeta={columnMeta}
categoryMappings={categoryMappings}
/>
</div>
@@ -6410,7 +6419,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onChange={(e) => setEditingValue(e.target.value)}
onKeyDown={handleEditKeyDown}
onBlur={saveEditing}
className="border-primary bg-background h-full w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm"
className="border-primary bg-background h-8 w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm"
autoFocus
>
<option value=""></option>
@@ -6447,7 +6456,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onChange={(e) => setEditingValue(e.target.value)}
onKeyDown={handleEditKeyDown}
onBlur={saveEditing}
className="border-primary bg-background h-full w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm"
className="border-primary bg-background h-8 w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm"
style={{
textAlign: isNumeric ? "right" : column.align || "left",
}}