카테고리값 자동감지
This commit is contained in:
@@ -43,7 +43,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||
import { getCurrentUser, UserInfo } from "@/lib/api/client";
|
||||
import { apiClient, getCurrentUser, UserInfo } from "@/lib/api/client";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -101,11 +101,7 @@ const CascadingDropdownInForm: React.FC<CascadingDropdownInFormProps> = ({
|
||||
const isDisabled = !parentValue || loading;
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value || ""}
|
||||
onValueChange={(newValue) => onChange?.(newValue)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Select value={value || ""} onValueChange={(newValue) => onChange?.(newValue)} disabled={isDisabled}>
|
||||
<SelectTrigger className={className}>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -188,7 +184,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
|
||||
const screenContext = useScreenContextOptional(); // 화면 컨텍스트 (좌측/우측 위치 확인용)
|
||||
const splitPanelPosition = screenContext?.splitPanelPosition; // 분할 패널 내 위치
|
||||
|
||||
|
||||
// URL에서 menuObjid 가져오기 (카테고리 값 조회 시 필요)
|
||||
const searchParams = useSearchParams();
|
||||
const menuObjid = useMemo(() => {
|
||||
@@ -198,7 +194,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const urlMenuObjid = searchParams.get("menuObjid");
|
||||
return urlMenuObjid ? parseInt(urlMenuObjid) : undefined;
|
||||
}, [screenContext?.menuObjid, searchParams]);
|
||||
|
||||
|
||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
||||
@@ -210,7 +206,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const hasInitializedWidthsRef = useRef(false);
|
||||
const columnRefs = useRef<Record<string, HTMLTableCellElement | null>>({});
|
||||
const isResizingRef = useRef(false);
|
||||
|
||||
|
||||
// TableOptions 상태
|
||||
const [filters, setFilters] = useState<TableFilter[]>([]);
|
||||
const [grouping, setGrouping] = useState<string[]>([]);
|
||||
@@ -247,14 +243,19 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||
|
||||
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> {라벨, 색상}})
|
||||
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, { label: string; color?: string }>>>({});
|
||||
const [categoryMappings, setCategoryMappings] = useState<
|
||||
Record<string, Record<string, { label: string; color?: string }>>
|
||||
>({});
|
||||
|
||||
// 카테고리 코드 라벨 캐시 (CATEGORY_* 코드 -> 라벨)
|
||||
const [categoryCodeLabels, setCategoryCodeLabels] = useState<Record<string, string>>({});
|
||||
|
||||
// 테이블 등록 (Context에 등록)
|
||||
const tableId = `datatable-${component.id}`;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!component.tableName || !component.columns) return;
|
||||
|
||||
|
||||
registerTable({
|
||||
tableId,
|
||||
label: component.title || "데이터 테이블",
|
||||
@@ -331,7 +332,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
useEffect(() => {
|
||||
const handleRelatedButtonSelect = (event: CustomEvent) => {
|
||||
const { targetTable, filterColumn, filterValue } = event.detail || {};
|
||||
|
||||
|
||||
// 이 테이블이 대상 테이블인지 확인
|
||||
if (targetTable === component.tableName) {
|
||||
console.log("📌 [InteractiveDataTable] RelatedDataButtons 필터 적용:", {
|
||||
@@ -379,7 +380,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
// menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용)
|
||||
const queryParams = menuObjid ? `?menuObjid=${menuObjid}` : "";
|
||||
const response = await apiClient.get(
|
||||
`/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`
|
||||
`/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`,
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
@@ -596,13 +597,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
// 없으면 테이블 타입 관리에서 설정된 값 찾기
|
||||
const tableColumn = tableColumns.find((col) => col.columnName === columnName);
|
||||
|
||||
|
||||
// input_type 우선 사용 (category 등)
|
||||
const inputType = (tableColumn as any)?.input_type || (tableColumn as any)?.inputType;
|
||||
if (inputType) {
|
||||
return inputType;
|
||||
}
|
||||
|
||||
|
||||
// 없으면 webType 사용
|
||||
return tableColumn?.webType || "text";
|
||||
},
|
||||
@@ -709,19 +710,19 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
let linkedFilterValues: Record<string, any> = {};
|
||||
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
|
||||
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
|
||||
|
||||
|
||||
if (splitPanelContext) {
|
||||
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
|
||||
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
|
||||
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
|
||||
(filter) => filter.targetColumn?.startsWith(component.tableName + ".") ||
|
||||
filter.targetColumn === component.tableName
|
||||
(filter) =>
|
||||
filter.targetColumn?.startsWith(component.tableName + ".") || filter.targetColumn === component.tableName,
|
||||
);
|
||||
|
||||
|
||||
// 좌측 데이터 선택 여부 확인
|
||||
hasSelectedLeftData = splitPanelContext.selectedLeftData &&
|
||||
Object.keys(splitPanelContext.selectedLeftData).length > 0;
|
||||
|
||||
hasSelectedLeftData =
|
||||
splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0;
|
||||
|
||||
linkedFilterValues = splitPanelContext.getLinkedFilterValues();
|
||||
// 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
|
||||
const tableSpecificFilters: Record<string, any> = {};
|
||||
@@ -740,7 +741,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
}
|
||||
linkedFilterValues = tableSpecificFilters;
|
||||
}
|
||||
|
||||
|
||||
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
|
||||
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
||||
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
|
||||
@@ -752,9 +753,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 🆕 RelatedDataButtons 필터 적용
|
||||
let relatedButtonFilterValues: Record<string, any> = {};
|
||||
const relatedButtonFilterValues: Record<string, any> = {};
|
||||
if (relatedButtonFilter) {
|
||||
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue;
|
||||
}
|
||||
@@ -765,16 +766,16 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
...linkedFilterValues,
|
||||
...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
|
||||
};
|
||||
|
||||
console.log("🔍 데이터 조회 시작:", {
|
||||
tableName: component.tableName,
|
||||
page,
|
||||
|
||||
console.log("🔍 데이터 조회 시작:", {
|
||||
tableName: component.tableName,
|
||||
page,
|
||||
pageSize,
|
||||
linkedFilterValues,
|
||||
relatedButtonFilterValues,
|
||||
mergedSearchParams,
|
||||
});
|
||||
|
||||
|
||||
const result = await tableTypeApi.getTableData(component.tableName, {
|
||||
page,
|
||||
size: pageSize,
|
||||
@@ -782,11 +783,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
||||
});
|
||||
|
||||
console.log("✅ 데이터 조회 완료:", {
|
||||
console.log("✅ 데이터 조회 완료:", {
|
||||
tableName: component.tableName,
|
||||
dataLength: result.data.length,
|
||||
dataLength: result.data.length,
|
||||
total: result.total,
|
||||
page: result.page
|
||||
page: result.page,
|
||||
});
|
||||
|
||||
setData(result.data);
|
||||
@@ -794,6 +795,45 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
setTotalPages(result.totalPages);
|
||||
setCurrentPage(result.page);
|
||||
|
||||
// 카테고리 코드 패턴(CATEGORY_*) 검출 및 라벨 조회
|
||||
const detectAndLoadCategoryLabels = async () => {
|
||||
const categoryCodes = new Set<string>();
|
||||
result.data.forEach((row: Record<string, any>) => {
|
||||
Object.values(row).forEach((value) => {
|
||||
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
|
||||
categoryCodes.add(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log("🏷️ [InteractiveDataTable] 감지된 카테고리 코드:", Array.from(categoryCodes));
|
||||
|
||||
// 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외)
|
||||
const newCodes = Array.from(categoryCodes);
|
||||
|
||||
if (newCodes.length > 0) {
|
||||
try {
|
||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes);
|
||||
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
|
||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data);
|
||||
if (response.data.success && response.data.data) {
|
||||
setCategoryCodeLabels((prev) => {
|
||||
const newLabels = {
|
||||
...prev,
|
||||
...response.data.data,
|
||||
};
|
||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 캐시 업데이트:", newLabels);
|
||||
return newLabels;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("카테고리 라벨 조회 실패:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
detectAndLoadCategoryLabels();
|
||||
|
||||
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
|
||||
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
|
||||
const primaryKeyField = Object.keys(rowData)[0];
|
||||
@@ -929,18 +969,18 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
try {
|
||||
const columns = await tableTypeApi.getColumns(component.tableName);
|
||||
setTableColumns(columns);
|
||||
|
||||
|
||||
// 🆕 전체 컬럼 목록 설정
|
||||
const columnNames = columns.map(col => col.columnName);
|
||||
const columnNames = columns.map((col) => col.columnName);
|
||||
setAllAvailableColumns(columnNames);
|
||||
|
||||
|
||||
// 🆕 컬럼명 -> 라벨 매핑 생성
|
||||
const labels: Record<string, string> = {};
|
||||
columns.forEach(col => {
|
||||
columns.forEach((col) => {
|
||||
labels[col.columnName] = col.displayName || col.columnName;
|
||||
});
|
||||
setColumnLabels(labels);
|
||||
|
||||
|
||||
// 🆕 localStorage에서 필터 설정 복원
|
||||
if (user?.userId && component.componentId) {
|
||||
const storageKey = `table-search-filter-${user.userId}-${component.componentId}`;
|
||||
@@ -996,28 +1036,31 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
);
|
||||
|
||||
// 행 선택 핸들러
|
||||
const handleRowSelect = useCallback((rowIndex: number, isSelected: boolean) => {
|
||||
setSelectedRows((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (isSelected) {
|
||||
newSet.add(rowIndex);
|
||||
} else {
|
||||
newSet.delete(rowIndex);
|
||||
const handleRowSelect = useCallback(
|
||||
(rowIndex: number, isSelected: boolean) => {
|
||||
setSelectedRows((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (isSelected) {
|
||||
newSet.add(rowIndex);
|
||||
} else {
|
||||
newSet.delete(rowIndex);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// 분할 패널 좌측에서 선택 시 컨텍스트에 데이터 저장 (연결 필터용)
|
||||
if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (isSelected && data[rowIndex]) {
|
||||
splitPanelContext.setSelectedLeftData(data[rowIndex]);
|
||||
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 저장:", data[rowIndex]);
|
||||
} else if (!isSelected) {
|
||||
splitPanelContext.setSelectedLeftData(null);
|
||||
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 초기화");
|
||||
}
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// 분할 패널 좌측에서 선택 시 컨텍스트에 데이터 저장 (연결 필터용)
|
||||
if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (isSelected && data[rowIndex]) {
|
||||
splitPanelContext.setSelectedLeftData(data[rowIndex]);
|
||||
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 저장:", data[rowIndex]);
|
||||
} else if (!isSelected) {
|
||||
splitPanelContext.setSelectedLeftData(null);
|
||||
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 초기화");
|
||||
}
|
||||
}
|
||||
}, [data, splitPanelContext, splitPanelPosition]);
|
||||
},
|
||||
[data, splitPanelContext, splitPanelPosition],
|
||||
);
|
||||
|
||||
// 전체 선택/해제 핸들러
|
||||
const handleSelectAll = useCallback(
|
||||
@@ -1599,7 +1642,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 상세 설정에서 옵션 목록 가져오기
|
||||
const options = detailSettings?.options || [];
|
||||
if (options.length > 0) {
|
||||
@@ -1726,7 +1769,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
case "category": {
|
||||
// 카테고리 셀렉트 (동적 import)
|
||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
const {
|
||||
CategorySelectComponent,
|
||||
} = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
return (
|
||||
<div>
|
||||
<CategorySelectComponent
|
||||
@@ -1854,7 +1899,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 상세 설정에서 옵션 목록 가져오기
|
||||
const optionsAdd = detailSettings?.options || [];
|
||||
if (optionsAdd.length > 0) {
|
||||
@@ -2026,7 +2071,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
case "category": {
|
||||
// 카테고리 셀렉트 (동적 import)
|
||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
const {
|
||||
CategorySelectComponent,
|
||||
} = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
return (
|
||||
<div>
|
||||
<CategorySelectComponent
|
||||
@@ -2164,8 +2211,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const actualWebType = getColumnWebType(column.columnName);
|
||||
|
||||
// 파일 타입 컬럼 처리 (가상 파일 컬럼 포함)
|
||||
const isFileColumn =
|
||||
actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn;
|
||||
const isFileColumn = actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn;
|
||||
|
||||
// 파일 타입 컬럼은 파일 아이콘으로 표시 (컬럼별 파일 관리)
|
||||
if (isFileColumn && rowData) {
|
||||
@@ -2210,25 +2256,25 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
case "category": {
|
||||
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원)
|
||||
if (!value) return "";
|
||||
|
||||
|
||||
const mapping = categoryMappings[column.columnName];
|
||||
const categoryData = mapping?.[String(value)];
|
||||
|
||||
|
||||
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값만 텍스트로 표시
|
||||
const displayLabel = categoryData?.label || String(value);
|
||||
const displayColor = categoryData?.color;
|
||||
|
||||
|
||||
// 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시
|
||||
if (!displayColor || displayColor === "none" || !categoryData) {
|
||||
return <span className="text-sm">{displayLabel}</span>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: displayColor,
|
||||
borderColor: displayColor
|
||||
}}
|
||||
borderColor: displayColor,
|
||||
}}
|
||||
className="text-white"
|
||||
>
|
||||
{displayLabel}
|
||||
@@ -2268,8 +2314,41 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return String(value);
|
||||
default: {
|
||||
// 카테고리 코드 패턴 감지 (CATEGORY_로 시작하는 값)
|
||||
const strValue = String(value);
|
||||
if (strValue.startsWith("CATEGORY_")) {
|
||||
// 1. categoryMappings에서 해당 코드 검색 (색상 정보 포함)
|
||||
for (const columnName of Object.keys(categoryMappings)) {
|
||||
const mapping = categoryMappings[columnName];
|
||||
const categoryData = mapping?.[strValue];
|
||||
if (categoryData) {
|
||||
// 색상이 있으면 배지로, 없으면 텍스트로 표시
|
||||
if (categoryData.color && categoryData.color !== "none") {
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: categoryData.color,
|
||||
borderColor: categoryData.color,
|
||||
}}
|
||||
className="text-white"
|
||||
>
|
||||
{categoryData.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return <span className="text-sm">{categoryData.label}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. categoryCodeLabels에서 검색 (API로 조회한 라벨)
|
||||
const cachedLabel = categoryCodeLabels[strValue];
|
||||
if (cachedLabel) {
|
||||
return <span className="text-sm">{cachedLabel}</span>;
|
||||
}
|
||||
}
|
||||
return strValue;
|
||||
}
|
||||
}
|
||||
|
||||
return String(value);
|
||||
@@ -2405,15 +2484,12 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
{visibleColumns.length > 0 ? (
|
||||
<>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200/60 bg-white shadow-sm">
|
||||
<Table style={{ tableLayout: 'fixed' }}>
|
||||
<TableHeader className="bg-gradient-to-b from-muted/50 to-muted border-b-2 border-primary/20">
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<TableHeader className="from-muted/50 to-muted border-primary/20 border-b-2 bg-gradient-to-b">
|
||||
<TableRow>
|
||||
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */}
|
||||
{component.enableDelete && (
|
||||
<TableHead
|
||||
className="px-4"
|
||||
style={{ width: '48px', minWidth: '48px', maxWidth: '48px' }}
|
||||
>
|
||||
<TableHead className="px-4" style={{ width: "48px", minWidth: "48px", maxWidth: "48px" }}>
|
||||
<Checkbox
|
||||
checked={selectedRows.size === data.length && data.length > 0}
|
||||
onCheckedChange={handleSelectAll}
|
||||
@@ -2422,74 +2498,74 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
)}
|
||||
{visibleColumns.map((column: DataTableColumn, columnIndex) => {
|
||||
const columnWidth = columnWidths[column.id];
|
||||
|
||||
|
||||
return (
|
||||
<TableHead
|
||||
key={column.id}
|
||||
ref={(el) => (columnRefs.current[column.id] = el)}
|
||||
className="relative px-4 font-bold text-foreground/90 select-none text-center hover:bg-muted/70 transition-colors"
|
||||
style={{
|
||||
className="text-foreground/90 hover:bg-muted/70 relative px-4 text-center font-bold transition-colors select-none"
|
||||
style={{
|
||||
width: columnWidth ? `${columnWidth}px` : undefined,
|
||||
userSelect: 'none'
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
{/* 리사이즈 핸들 */}
|
||||
{columnIndex < visibleColumns.length - 1 && (
|
||||
<div
|
||||
className="absolute right-0 top-0 h-full w-2 cursor-col-resize hover:bg-blue-500 z-20"
|
||||
style={{ marginRight: '-4px', paddingLeft: '4px', paddingRight: '4px' }}
|
||||
className="absolute top-0 right-0 z-20 h-full w-2 cursor-col-resize hover:bg-blue-500"
|
||||
style={{ marginRight: "-4px", paddingLeft: "4px", paddingRight: "4px" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
const thElement = columnRefs.current[column.id];
|
||||
if (!thElement) return;
|
||||
|
||||
|
||||
isResizingRef.current = true;
|
||||
|
||||
|
||||
const startX = e.clientX;
|
||||
const startWidth = columnWidth || thElement.offsetWidth;
|
||||
|
||||
|
||||
// 드래그 중 텍스트 선택 방지
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'col-resize';
|
||||
|
||||
document.body.style.userSelect = "none";
|
||||
document.body.style.cursor = "col-resize";
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
moveEvent.preventDefault();
|
||||
|
||||
|
||||
const diff = moveEvent.clientX - startX;
|
||||
const newWidth = Math.max(80, startWidth + diff);
|
||||
|
||||
|
||||
// 직접 DOM 스타일 변경 (리렌더링 없음)
|
||||
if (thElement) {
|
||||
thElement.style.width = `${newWidth}px`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleMouseUp = () => {
|
||||
// 최종 너비를 state에 저장
|
||||
if (thElement) {
|
||||
const finalWidth = Math.max(80, thElement.offsetWidth);
|
||||
setColumnWidths(prev => ({ ...prev, [column.id]: finalWidth }));
|
||||
setColumnWidths((prev) => ({ ...prev, [column.id]: finalWidth }));
|
||||
}
|
||||
|
||||
|
||||
// 텍스트 선택 복원
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
|
||||
document.body.style.userSelect = "";
|
||||
document.body.style.cursor = "";
|
||||
|
||||
// 약간의 지연 후 리사이즈 플래그 해제
|
||||
setTimeout(() => {
|
||||
isResizingRef.current = false;
|
||||
}, 100);
|
||||
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -2517,10 +2593,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
<TableRow key={rowIndex} className="transition-all duration-200 hover:bg-orange-100">
|
||||
{/* 체크박스 셀 (삭제 기능이 활성화된 경우) */}
|
||||
{component.enableDelete && (
|
||||
<TableCell
|
||||
className="px-4"
|
||||
style={{ width: '48px', minWidth: '48px', maxWidth: '48px' }}
|
||||
>
|
||||
<TableCell className="px-4" style={{ width: "48px", minWidth: "48px", maxWidth: "48px" }}>
|
||||
<Checkbox
|
||||
checked={selectedRows.has(rowIndex)}
|
||||
onCheckedChange={(checked) => handleRowSelect(rowIndex, checked as boolean)}
|
||||
@@ -2530,10 +2603,10 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
{visibleColumns.map((column: DataTableColumn) => {
|
||||
const isNumeric = column.widgetType === "number" || column.widgetType === "decimal";
|
||||
return (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
className="px-4 text-sm font-medium text-gray-900 whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
style={{ textAlign: isNumeric ? 'right' : 'left' }}
|
||||
<TableCell
|
||||
key={column.id}
|
||||
className="overflow-hidden px-4 text-sm font-medium text-ellipsis whitespace-nowrap text-gray-900"
|
||||
style={{ textAlign: isNumeric ? "right" : "left" }}
|
||||
>
|
||||
{formatCellValue(row[column.columnName], column, row)}
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user