diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index 574f1cf8..a7757397 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -606,7 +606,7 @@ router.get( }); } - const { enableEntityJoin, groupByColumns } = req.query; + const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query; const enableEntityJoinFlag = enableEntityJoin === "true" || (typeof enableEntityJoin === "boolean" && enableEntityJoin); @@ -626,17 +626,22 @@ router.get( } } + // ๐Ÿ†• primaryKeyColumn ํŒŒ์‹ฑ + const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined; + console.log(`๐Ÿ” ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ: ${tableName}/${id}`, { enableEntityJoin: enableEntityJoinFlag, groupByColumns: groupByColumnsArray, + primaryKeyColumn: primaryKeyColumnStr, }); - // ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ (Entity Join ์˜ต์…˜ + ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ ํฌํ•จ) + // ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ (Entity Join ์˜ต์…˜ + ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ + Primary Key ์ปฌ๋Ÿผ ํฌํ•จ) const result = await dataService.getRecordDetail( tableName, id, enableEntityJoinFlag, - groupByColumnsArray + groupByColumnsArray, + primaryKeyColumnStr // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ ); if (!result.success) { diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index 8c6e63f0..60de20db 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -490,7 +490,8 @@ class DataService { tableName: string, id: string | number, enableEntityJoin: boolean = false, - groupByColumns: string[] = [] + groupByColumns: string[] = [], + primaryKeyColumn?: string // ๐Ÿ†• ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌํ•œ Primary Key ์ปฌ๋Ÿผ๋ช… ): Promise> { try { // ํ…Œ์ด๋ธ” ์ ‘๊ทผ ๊ฒ€์ฆ @@ -499,20 +500,30 @@ class DataService { return validation.error!; } - // Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ - const pkResult = await query<{ attname: string }>( - `SELECT a.attname - FROM pg_index i - JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) - WHERE i.indrelid = $1::regclass AND i.indisprimary`, - [tableName] - ); + // ๐Ÿ†• ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌํ•œ Primary Key ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + let pkColumn = primaryKeyColumn || ""; + + // Primary Key ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์ž๋™ ๊ฐ์ง€ + if (!pkColumn) { + const pkResult = await query<{ attname: string }>( + `SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = $1::regclass AND i.indisprimary`, + [tableName] + ); - let pkColumn = "id"; // ๊ธฐ๋ณธ๊ฐ’ - if (pkResult.length > 0) { - pkColumn = pkResult[0].attname; + pkColumn = "id"; // ๊ธฐ๋ณธ๊ฐ’ + if (pkResult.length > 0) { + pkColumn = pkResult[0].attname; + } + console.log(`๐Ÿ”‘ [getRecordDetail] ์ž๋™ ๊ฐ์ง€๋œ Primary Key:`, pkResult); + } else { + console.log(`๐Ÿ”‘ [getRecordDetail] ํด๋ผ์ด์–ธํŠธ ์ œ๊ณต Primary Key: ${pkColumn}`); } + console.log(`๐Ÿ”‘ [getRecordDetail] ํ…Œ์ด๋ธ”: ${tableName}, Primary Key ์ปฌ๋Ÿผ: ${pkColumn}, ์กฐํšŒ ID: ${id}`); + // ๐Ÿ†• Entity Join์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ if (enableEntityJoin) { const { EntityJoinService } = await import("./entityJoinService"); diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 44685dc0..fdd104df 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -374,8 +374,9 @@ export const ScreenModal: React.FC = ({ className }) => { const editId = urlParams.get("editId"); const tableName = urlParams.get("tableName") || screenInfo.tableName; const groupByColumnsParam = urlParams.get("groupByColumns"); + const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… - console.log("๐Ÿ“‹ URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ:", { mode, editId, tableName, groupByColumnsParam }); + console.log("๐Ÿ“‹ URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ:", { mode, editId, tableName, groupByColumnsParam, primaryKeyColumn }); // ์ˆ˜์ • ๋ชจ๋“œ์ด๊ณ  editId๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ ์กฐํšŒ if (mode === "edit" && editId && tableName) { @@ -414,6 +415,11 @@ export const ScreenModal: React.FC = ({ className }) => { params.groupByColumns = JSON.stringify(groupByColumns); console.log("โœ… [ScreenModal] groupByColumns๋ฅผ params์— ์ถ”๊ฐ€:", params.groupByColumns); } + // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ (๋ฐฑ์—”๋“œ ์ž๋™ ๊ฐ์ง€ ์‹คํŒจ ์‹œ ์‚ฌ์šฉ) + if (primaryKeyColumn) { + params.primaryKeyColumn = primaryKeyColumn; + console.log("โœ… [ScreenModal] primaryKeyColumn์„ params์— ์ถ”๊ฐ€:", primaryKeyColumn); + } console.log("๐Ÿ“ก [ScreenModal] ์‹ค์ œ API ์š”์ฒญ:", { url: `/data/${tableName}/${editId}`, diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx index 5045a43b..f1604337 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx @@ -11,6 +11,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { dynamicFormApi } from "@/lib/api/dynamicForm"; import { cascadingRelationApi } from "@/lib/api/cascadingRelation"; +import { AutoFillMapping } from "./config"; export function EntitySearchInputComponent({ tableName, @@ -37,6 +38,8 @@ export function EntitySearchInputComponent({ formData, // ๋‹ค์ค‘์„ ํƒ props multiple: multipleProp, + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ props + autoFillMappings: autoFillMappingsProp, // ์ถ”๊ฐ€ props component, isInteractive, @@ -47,6 +50,7 @@ export function EntitySearchInputComponent({ isInteractive?: boolean; onFormDataChange?: (fieldName: string, value: any) => void; webTypeConfig?: any; // ์›นํƒ€์ž… ์„ค์ • (์—ฐ์‡„๊ด€๊ณ„ ๋“ฑ) + autoFillMappings?: AutoFillMapping[]; // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ }) { // uiMode๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด modeProp ์‚ฌ์šฉ, ๊ธฐ๋ณธ๊ฐ’ "combo" const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete"; @@ -54,6 +58,18 @@ export function EntitySearchInputComponent({ // ๋‹ค์ค‘์„ ํƒ ๋ฐ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • (props > webTypeConfig > componentConfig ์ˆœ์„œ) const config = component?.componentConfig || component?.webTypeConfig || {}; const isMultiple = multipleProp ?? config.multiple ?? false; + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • (props > config) + const autoFillMappings: AutoFillMapping[] = autoFillMappingsProp ?? config.autoFillMappings ?? []; + + // ๋””๋ฒ„๊ทธ: ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • ํ™•์ธ + console.log("๐Ÿ”ง [EntitySearchInput] ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ •:", { + autoFillMappingsProp, + configAutoFillMappings: config.autoFillMappings, + effectiveAutoFillMappings: autoFillMappings, + isInteractive, + hasOnFormDataChange: !!onFormDataChange, + }); // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ์ถ”์ถœ const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; @@ -309,6 +325,23 @@ export function EntitySearchInputComponent({ console.log("๐Ÿ“ค EntitySearchInput -> onFormDataChange:", component.columnName, newValue); } } + + // ๐Ÿ†• ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์ ์šฉ + if (autoFillMappings.length > 0 && isInteractive && onFormDataChange && fullData) { + console.log("๐Ÿ”„ ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์ ์šฉ:", { mappings: autoFillMappings, fullData }); + + for (const mapping of autoFillMappings) { + if (mapping.sourceField && mapping.targetField) { + const sourceValue = fullData[mapping.sourceField]; + if (sourceValue !== undefined) { + onFormDataChange(mapping.targetField, sourceValue); + console.log(` โœ… ${mapping.sourceField} โ†’ ${mapping.targetField}:`, sourceValue); + } else { + console.log(` โš ๏ธ ${mapping.sourceField} ๊ฐ’์ด ์—†์Œ`); + } + } + } + } }; // ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ์—์„œ ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ œ๊ฑฐ @@ -436,7 +469,7 @@ export function EntitySearchInputComponent({ const isSelected = selectedValues.includes(String(option[valueField])); return ( handleSelectOption(option)} className="text-xs sm:text-sm" @@ -509,7 +542,7 @@ export function EntitySearchInputComponent({ {effectiveOptions.map((option, index) => ( handleSelectOption(option)} className="text-xs sm:text-sm" diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx index fb75daa4..22a52aab 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx @@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react"; // allComponents๋Š” ํ˜„์žฌ ์‚ฌ์šฉ๋˜์ง€ ์•Š์ง€๋งŒ ํ–ฅํ›„ ํ™•์žฅ์„ ์œ„ํ•ด props์— ์œ ์ง€ -import { EntitySearchInputConfig } from "./config"; +import { EntitySearchInputConfig, AutoFillMapping } from "./config"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { tableTypeApi } from "@/lib/api/screen"; import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation"; @@ -236,6 +236,7 @@ export function EntitySearchInputConfigPanel({ const newConfig = { ...localConfig, ...updates }; setLocalConfig(newConfig); onConfigChange(newConfig); + console.log("๐Ÿ“ [EntitySearchInput] ์„ค์ • ์—…๋ฐ์ดํŠธ:", { updates, newConfig }); }; // ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™” @@ -636,9 +637,9 @@ export function EntitySearchInputConfigPanel({ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {tableColumns.map((column) => ( + {tableColumns.map((column, idx) => ( { updateConfig({ displayField: column.columnName }); @@ -690,9 +691,9 @@ export function EntitySearchInputConfigPanel({ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {tableColumns.map((column) => ( + {tableColumns.map((column, idx) => ( { updateConfig({ valueField: column.columnName }); @@ -812,8 +813,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -860,8 +861,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -919,8 +920,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -939,6 +940,105 @@ export function EntitySearchInputConfigPanel({ )} + + {/* ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • */} +
+
+
+ +

์ž๋™ ์ฑ„์›€ ๋งคํ•‘

+
+ +
+

+ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•˜๋ฉด ์†Œ์Šค ํ•„๋“œ์˜ ๊ฐ’์ด ๋Œ€์ƒ ํ•„๋“œ์— ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค. +

+ + {(localConfig.autoFillMappings || []).length > 0 && ( +
+ {(localConfig.autoFillMappings || []).map((mapping, index) => ( +
+ {/* ์†Œ์Šค ํ•„๋“œ (์„ ํƒ๋œ ์—”ํ‹ฐํ‹ฐ) */} +
+ + +
+ + {/* ํ™”์‚ดํ‘œ */} +
+ โ†’ +
+ + {/* ๋Œ€์ƒ ํ•„๋“œ (ํผ) */} +
+ + { + const mappings = [...(localConfig.autoFillMappings || [])]; + mappings[index] = { ...mappings[index], targetField: e.target.value }; + updateConfig({ autoFillMappings: mappings }); + }} + placeholder="ํผ ํ•„๋“œ๋ช…" + className="h-8 text-xs" + /> +
+ + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
+ ))} +
+ )} + + {(localConfig.autoFillMappings || []).length === 0 && ( +
+ ๋งคํ•‘์ด ์—†์Šต๋‹ˆ๋‹ค. + ์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ๋งคํ•‘์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +
+ )} +
); } diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx index dd6ed5c4..f8a3a22e 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx @@ -37,6 +37,9 @@ export const EntitySearchInputWrapper: React.FC = ({ // placeholder const placeholder = config.placeholder || widget?.placeholder || "ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”"; + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • + const autoFillMappings = config.autoFillMappings || []; console.log("๐Ÿข EntitySearchInputWrapper ๋ Œ๋”๋ง:", { tableName, @@ -44,6 +47,7 @@ export const EntitySearchInputWrapper: React.FC = ({ valueField, uiMode, multiple, + autoFillMappings, value, config, }); @@ -68,6 +72,7 @@ export const EntitySearchInputWrapper: React.FC = ({ value={value} onChange={onChange} multiple={multiple} + autoFillMappings={autoFillMappings} component={component} isInteractive={props.isInteractive} onFormDataChange={props.onFormDataChange} diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx index 555efe9b..422dfbfa 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx @@ -148,9 +148,9 @@ export function EntitySearchModal({ ์„ ํƒ )} - {displayColumns.map((col) => ( + {displayColumns.map((col, colIdx) => ( {col} @@ -179,7 +179,8 @@ export function EntitySearchModal({ ) : ( results.map((item, index) => { - const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`; + // null๊ณผ undefined ๋ชจ๋‘ ์ฒดํฌํ•˜์—ฌ ์œ ๋‹ˆํฌ ํ‚ค ์ƒ์„ฑ + const uniqueKey = item[valueField] != null ? `${item[valueField]}` : `row-${index}`; const isSelected = isItemSelected(item); return ( )} - {displayColumns.map((col) => ( - + {displayColumns.map((col, colIdx) => ( + {item[col] || "-"} ))} diff --git a/frontend/lib/registry/components/entity-search-input/config.ts b/frontend/lib/registry/components/entity-search-input/config.ts index fab81c9f..3dae8779 100644 --- a/frontend/lib/registry/components/entity-search-input/config.ts +++ b/frontend/lib/registry/components/entity-search-input/config.ts @@ -1,3 +1,9 @@ +// ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ํƒ€์ž… +export interface AutoFillMapping { + sourceField: string; // ์„ ํƒ๋œ ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ (์˜ˆ: customer_name) + targetField: string; // ํผ์˜ ํ•„๋“œ (์˜ˆ: partner_name) +} + export interface EntitySearchInputConfig { tableName: string; displayField: string; @@ -18,5 +24,8 @@ export interface EntitySearchInputConfig { cascadingRelationCode?: string; // ์—ฐ์‡„๊ด€๊ณ„ ์ฝ”๋“œ (WAREHOUSE_LOCATION ๋“ฑ) cascadingRole?: "parent" | "child"; // ์—ญํ•  (๋ถ€๋ชจ/์ž์‹) cascadingParentField?: string; // ๋ถ€๋ชจ ํ•„๋“œ์˜ ์ปฌ๋Ÿผ๋ช… (์ž์‹ ์—ญํ• ์ผ ๋•Œ๋งŒ ์‚ฌ์šฉ) + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • + autoFillMappings?: AutoFillMapping[]; // ์—”ํ‹ฐํ‹ฐ ์„ ํƒ ์‹œ ๋‹ค๋ฅธ ํ•„๋“œ์— ์ž๋™์œผ๋กœ ๊ฐ’ ์ฑ„์šฐ๊ธฐ } diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 53ad204d..13cb1a68 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -7,6 +7,8 @@ import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; import { PivotGridProps, PivotResult, @@ -50,6 +52,10 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; +// ==================== ์ƒ์ˆ˜ ==================== + +const PIVOT_STATE_VERSION = "1.0"; // ์ƒํƒœ ์ €์žฅ ๋ฒ„์ „ (ํ˜ธํ™˜์„ฑ ์ฒดํฌ์šฉ) + // ==================== ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ==================== // ์…€ ๋ณ‘ํ•ฉ ์ •๋ณด ๊ณ„์‚ฐ @@ -128,7 +134,10 @@ const RowHeaderCell: React.FC = ({
{row.hasChildren && ( )} + + {/* ์ƒํƒœ ์œ ์ง€ ์ฒดํฌ๋ฐ•์Šค */} +
+ setPersistState(checked === true)} + className="h-3.5 w-3.5" + /> + +
{/* ์ฐจํŠธ ํ† ๊ธ€ */} {chartConfig && ( @@ -1685,137 +1782,224 @@ export const PivotGridComponent: React.FC = ({ > - {/* ์—ด ํ—ค๋” */} - - {/* ์ขŒ์ƒ๋‹จ ์ฝ”๋„ˆ (ํ–‰ ํ•„๋“œ ๋ผ๋ฒจ + ํ•„ํ„ฐ) */} - + {/* ์ขŒ์ƒ๋‹จ ์ฝ”๋„ˆ (์ฒซ ๋ฒˆ์งธ ๋ ˆ๋ฒจ์—๋งŒ ํ‘œ์‹œ) */} + {levelIdx === 0 && ( + + )} + + {/* ์—ด ํ—ค๋” ์…€ - ํ•ด๋‹น ๋ ˆ๋ฒจ */} + {levelCells.map((cell, cellIdx) => ( + ))} - {rowFields.length === 0 && ํ•ญ๋ชฉ} - - - {/* ์—ด ํ—ค๋” ์…€ */} - {flatColumns.map((col, idx) => ( - )} - colSpan={dataFields.length || 1} - style={{ width: columnWidths[idx] || "auto", minWidth: 50 }} - onClick={dataFields.length === 1 ? () => handleSort(dataFields[0].field) : undefined} - > -
- {col.caption || "(์ „์ฒด)"} - {dataFields.length === 1 && } -
- {/* ์—ด ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค */} -
handleResizeStart(idx, e)} - /> - - ))} - - {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} - {totals?.showRowGrandTotals && ( -
)} - colSpan={dataFields.length || 1} - rowSpan={dataFields.length > 1 ? 2 : 1} - > - ์ด๊ณ„ - - )} - - {/* ์—ด ํ•„๋“œ ํ•„ํ„ฐ (ํ—ค๋” ์˜ค๋ฅธ์ชฝ ๋์— ํ‘œ์‹œ) */} - {columnFields.length > 0 && ( + + )) + ) : ( + // ์—ด ํ•„๋“œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ: ๋‹จ์ผ ํ–‰ + - )} - + + {/* ์—ด ํ—ค๋” ์…€ (์—ด ํ•„๋“œ ์—†์„ ๋•Œ) */} + {flatColumns.map((col, idx) => ( + + ))} + + {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} + {totals?.showRowGrandTotals && ( + + )} + + )} {/* ๋ฐ์ดํ„ฐ ํ•„๋“œ ๋ผ๋ฒจ (๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ํ•„๋“œ์ธ ๊ฒฝ์šฐ) */} {dataFields.length > 1 && ( diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx index 37f0862b..448c92a5 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx @@ -16,6 +16,7 @@ import { PivotAreaType, AggregationType, FieldDataType, + DateGroupInterval, } from "./types"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; @@ -202,6 +203,28 @@ const AreaDropZone: React.FC = ({ )} + {/* ํ–‰/์—ด ์˜์—ญ์—์„œ ๋‚ ์งœ ํƒ€์ž…์ผ ๋•Œ ๊ทธ๋ฃนํ™” ์˜ต์…˜ */} + {(area === "row" || area === "column") && field.dataType === "date" && ( + + )} + + {/* ํ•„ํ„ฐ ์•„์ด์ฝ˜ (ํ•„ํ„ฐ ์ ์šฉ ์‹œ) */} + {hasFilter && ( + + )} + {/* ํ•„๋“œ ๋ผ๋ฒจ */} + + + + + ํ•„ํ„ฐ๋งŒ ์ดˆ๊ธฐํ™” + {filteredFieldCount > 0 && ( + + ({filteredFieldCount}๊ฐœ) + + )} + + + + ํ•„๋“œ ๋ฐฐ์น˜ ์ดˆ๊ธฐํ™” + + + + + ์ „์ฒด ์ดˆ๊ธฐํ™” + + + + + {/* ์ ‘๊ธฐ ๋ฒ„ํŠผ */} + {onToggleCollapse && ( - - )} + )} + {/* ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„๋ ˆ์ด */} diff --git a/frontend/lib/registry/components/pivot-grid/types.ts b/frontend/lib/registry/components/pivot-grid/types.ts index 87ba2414..d4d8b1e5 100644 --- a/frontend/lib/registry/components/pivot-grid/types.ts +++ b/frontend/lib/registry/components/pivot-grid/types.ts @@ -304,6 +304,7 @@ export interface PivotHeaderNode { level: number; // ๊นŠ์ด children?: PivotHeaderNode[]; // ์ž์‹ ๋…ธ๋“œ isExpanded: boolean; // ํ™•์žฅ ์ƒํƒœ + hasChildren: boolean; // ์ž์‹ ์กด์žฌ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ ์žˆ์Œ) path: string[]; // ๊ฒฝ๋กœ (๋“œ๋ฆด๋‹ค์šด์šฉ) subtotal?: PivotCellValue[]; // ์†Œ๊ณ„ span?: number; // colspan/rowspan @@ -330,8 +331,11 @@ export interface PivotResult { // ํ”Œ๋žซ ํ–‰ ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) flatRows: PivotFlatRow[]; - // ํ”Œ๋žซ ์—ด ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) + // ํ”Œ๋žซ ์—ด ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) - ๋ฆฌํ”„ ๋…ธ๋“œ๋งŒ flatColumns: PivotFlatColumn[]; + + // ์—ด ํ—ค๋” ๋ ˆ๋ฒจ๋ณ„ (๋‹ค์ค‘ ํ–‰ ํ—ค๋”์šฉ) + columnHeaderLevels: PivotColumnHeaderCell[][]; // ์ดํ•ฉ๊ณ„ grandTotals: { @@ -360,6 +364,14 @@ export interface PivotFlatColumn { isTotal?: boolean; } +// ์—ด ํ—ค๋” ์…€ (๋‹ค์ค‘ ํ–‰ ํ—ค๋”์šฉ) +export interface PivotColumnHeaderCell { + caption: string; // ํ‘œ์‹œ ํ…์ŠคํŠธ + colSpan: number; // ๋ณ‘ํ•ฉํ•  ์—ด ์ˆ˜ + path: string[]; // ์ „์ฒด ๊ฒฝ๋กœ + level: number; // ๋ ˆ๋ฒจ (0๋ถ€ํ„ฐ ์‹œ์ž‘) +} + // ==================== ์ƒํƒœ ๊ด€๋ฆฌ ==================== export interface PivotGridState { diff --git a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts index 02dd4608..35893dea 100644 --- a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts +++ b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts @@ -10,6 +10,7 @@ import { PivotFlatRow, PivotFlatColumn, PivotCellValue, + PivotColumnHeaderCell, DateGroupInterval, AggregationType, SummaryDisplayMode, @@ -76,6 +77,31 @@ export function pathToKey(path: string[]): string { return path.join("||"); } +/** + * ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ƒ์„ฑ (์—ด ์ „์ฒด ํ™•์žฅ์šฉ) + */ +function generateAllPaths( + data: Record[], + fields: PivotFieldConfig[] +): string[] { + const allPaths: string[] = []; + + // ๊ฐ ๋ ˆ๋ฒจ๊นŒ์ง€์˜ ๊ณ ์œ  ๊ฒฝ๋กœ ์ˆ˜์ง‘ + for (let depth = 1; depth <= fields.length; depth++) { + const fieldsAtDepth = fields.slice(0, depth); + const pathSet = new Set(); + + data.forEach((row) => { + const path = fieldsAtDepth.map((f) => getFieldValue(row, f)); + pathSet.add(pathToKey(path)); + }); + + pathSet.forEach((pathKey) => allPaths.push(pathKey)); + } + + return allPaths; +} + /** * ํ‚ค๋ฅผ ๊ฒฝ๋กœ๋กœ ๋ณ€ํ™˜ */ @@ -129,6 +155,7 @@ function buildHeaderTree( caption: key, level: 0, isExpanded: expandedPaths.has(pathKey), + hasChildren: remainingFields.length > 0, // ๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ž์‹ ์žˆ์Œ path: path, span: 1, }; @@ -195,6 +222,7 @@ function buildChildNodes( caption: key, level: level, isExpanded: expandedPaths.has(pathKey), + hasChildren: remainingFields.length > 0, // ๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ž์‹ ์žˆ์Œ path: path, span: 1, }; @@ -238,7 +266,7 @@ function flattenRows(nodes: PivotHeaderNode[]): PivotFlatRow[] { level: node.level, caption: node.caption, isExpanded: node.isExpanded, - hasChildren: !!(node.children && node.children.length > 0), + hasChildren: node.hasChildren, // ๋…ธ๋“œ์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ด (๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ ์กด์žฌ ์—ฌ๋ถ€ ๊ธฐ์ค€) }); if (node.isExpanded && node.children) { @@ -324,6 +352,66 @@ function getMaxColumnLevel( return Math.min(maxLevel, totalFields - 1); } +/** + * ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” ์ƒ์„ฑ + * ๊ฐ ๋ ˆ๋ฒจ๋ณ„๋กœ ์…€๊ณผ colSpan ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ + */ +function buildColumnHeaderLevels( + nodes: PivotHeaderNode[], + totalLevels: number +): PivotColumnHeaderCell[][] { + if (totalLevels === 0 || nodes.length === 0) { + return []; + } + + const levels: PivotColumnHeaderCell[][] = Array.from( + { length: totalLevels }, + () => [] + ); + + // ๋ฆฌํ”„ ๋…ธ๋“œ ์ˆ˜ ๊ณ„์‚ฐ (colSpan ๊ณ„์‚ฐ์šฉ) + function countLeaves(node: PivotHeaderNode): number { + if (!node.children || node.children.length === 0 || !node.isExpanded) { + return 1; + } + return node.children.reduce((sum, child) => sum + countLeaves(child), 0); + } + + // ํŠธ๋ฆฌ ์ˆœํšŒํ•˜๋ฉฐ ๊ฐ ๋ ˆ๋ฒจ์— ์…€ ์ถ”๊ฐ€ + function traverse(node: PivotHeaderNode, level: number) { + const colSpan = countLeaves(node); + + levels[level].push({ + caption: node.caption, + colSpan, + path: node.path, + level, + }); + + if (node.children && node.isExpanded) { + for (const child of node.children) { + traverse(child, level + 1); + } + } else if (level < totalLevels - 1) { + // ํ™•์žฅ๋˜์ง€ ์•Š์€ ๋…ธ๋“œ๋Š” ๋‹ค์Œ ๋ ˆ๋ฒจ๋“ค์— ๋นˆ ์…€๋กœ ์ฑ„์›€ + for (let i = level + 1; i < totalLevels; i++) { + levels[i].push({ + caption: "", + colSpan, + path: node.path, + level: i, + }); + } + } + } + + for (const node of nodes) { + traverse(node, 0); + } + + return levels; +} + // ==================== ๋ฐ์ดํ„ฐ ๋งคํŠธ๋ฆญ์Šค ์ƒ์„ฑ ==================== /** @@ -733,12 +821,11 @@ export function processPivotData( uniqueValues.forEach((val) => expandedRowSet.add(val)); } - if (expandedColumnPaths.length === 0 && columnFields.length > 0) { - const firstField = columnFields[0]; - const uniqueValues = new Set( - filteredData.map((row) => getFieldValue(row, firstField)) - ); - uniqueValues.forEach((val) => expandedColSet.add(val)); + // ์—ด์€ ํ•ญ์ƒ ์ „์ฒด ํ™•์žฅ (์—ด ํ—ค๋”๋Š” ํ™•์žฅ/์ถ•์†Œ UI๊ฐ€ ์—†์Œ) + // ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ์—ด ๊ฒฝ๋กœ๋ฅผ ํ™•์žฅ ์ƒํƒœ๋กœ ์„ค์ • + if (columnFields.length > 0) { + const allColumnPaths = generateAllPaths(filteredData, columnFields); + allColumnPaths.forEach((pathKey) => expandedColSet.add(pathKey)); } // ํ—ค๋” ํŠธ๋ฆฌ ์ƒ์„ฑ @@ -786,6 +873,12 @@ export function processPivotData( grandTotals.grand ); + // ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” ์ƒ์„ฑ + const columnHeaderLevels = buildColumnHeaderLevels( + columnHeaders, + columnFields.length + ); + return { rowHeaders, columnHeaders, @@ -797,6 +890,7 @@ export function processPivotData( caption: path[path.length - 1] || "", span: 1, })), + columnHeaderLevels, grandTotals, }; } diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index ab387348..9b8e7cf0 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1590,21 +1590,40 @@ export const SplitPanelLayoutComponent: React.FC // ์ปค์Šคํ…€ ๋ชจ๋‹ฌ ํ™”๋ฉด ์—ด๊ธฐ const rightTableName = componentConfig.rightPanel?.tableName || ""; - // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: id > ID > ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ) + // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: ์„ค์ •๊ฐ’ > id > ID > non-null ํ•„๋“œ) + // ๐Ÿ”ง ์„ค์ •์—์„œ primaryKeyColumn ์ง€์ • ๊ฐ€๋Šฅ + const configuredPrimaryKey = componentConfig.rightPanel?.editButton?.primaryKeyColumn; + let primaryKeyName = "id"; let primaryKeyValue: any; - if (item.id !== undefined && item.id !== null) { + if (configuredPrimaryKey && item[configuredPrimaryKey] !== undefined && item[configuredPrimaryKey] !== null) { + // ์„ค์ •๋œ Primary Key ์‚ฌ์šฉ + primaryKeyName = configuredPrimaryKey; + primaryKeyValue = item[configuredPrimaryKey]; + } else if (item.id !== undefined && item.id !== null) { primaryKeyName = "id"; primaryKeyValue = item.id; } else if (item.ID !== undefined && item.ID !== null) { primaryKeyName = "ID"; primaryKeyValue = item.ID; } else { - // ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ - const firstKey = Object.keys(item)[0]; - primaryKeyName = firstKey; - primaryKeyValue = item[firstKey]; + // ๐Ÿ”ง ์ฒซ ๋ฒˆ์งธ non-null ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ + const keys = Object.keys(item); + let found = false; + for (const key of keys) { + if (item[key] !== undefined && item[key] !== null) { + primaryKeyName = key; + primaryKeyValue = item[key]; + found = true; + break; + } + } + // ๋ชจ๋“  ํ•„๋“œ๊ฐ€ null์ด๋ฉด ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ ์‚ฌ์šฉ + if (!found && keys.length > 0) { + primaryKeyName = keys[0]; + primaryKeyValue = item[keys[0]]; + } } console.log("โœ… ์ˆ˜์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { @@ -1629,7 +1648,7 @@ export const SplitPanelLayoutComponent: React.FC hasGroupByColumns: groupByColumns.length > 0, }); - // ScreenModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ID + groupByColumns ์ „๋‹ฌ) + // ScreenModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ID + groupByColumns + primaryKeyColumn ์ „๋‹ฌ) window.dispatchEvent( new CustomEvent("openScreenModal", { detail: { @@ -1638,6 +1657,7 @@ export const SplitPanelLayoutComponent: React.FC mode: "edit", editId: primaryKeyValue, tableName: rightTableName, + primaryKeyColumn: primaryKeyName, // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ ...(groupByColumns.length > 0 && { groupByColumns: JSON.stringify(groupByColumns), }), @@ -1650,6 +1670,7 @@ export const SplitPanelLayoutComponent: React.FC screenId: modalScreenId, editId: primaryKeyValue, tableName: rightTableName, + primaryKeyColumn: primaryKeyName, groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "์—†์Œ", }); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 366aa05b..9793acd8 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2688,19 +2688,41 @@ export const TableListComponent: React.FC = ({ const value = row[mappedColumnName]; // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘๋œ ๊ฐ’ ์ฒ˜๋ฆฌ - if (categoryMappings[col.columnName] && value !== null && value !== undefined) { - const mapping = categoryMappings[col.columnName][String(value)]; - if (mapping) { - return mapping.label; + if (value !== null && value !== undefined) { + const valueStr = String(value); + + // ๋””๋ฒ„๊ทธ ๋กœ๊ทธ (์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ธ ๊ฒฝ์šฐ๋งŒ) + if (valueStr.startsWith("CATEGORY_")) { + console.log("๐Ÿ” [์—‘์…€๋‹ค์šด๋กœ๋“œ] ์นดํ…Œ๊ณ ๋ฆฌ ๋ณ€ํ™˜ ์‹œ๋„:", { + columnName: col.columnName, + value: valueStr, + hasMappings: !!categoryMappings[col.columnName], + mappingsKeys: categoryMappings[col.columnName] ? Object.keys(categoryMappings[col.columnName]).slice(0, 5) : [], + }); } + + if (categoryMappings[col.columnName]) { + // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ค‘๋ณต ๊ฐ’ ์ฒ˜๋ฆฌ + if (valueStr.includes(",")) { + const values = valueStr.split(",").map((v) => v.trim()).filter((v) => v); + const labels = values.map((v) => { + const mapping = categoryMappings[col.columnName][v]; + return mapping ? mapping.label : v; + }); + return labels.join(", "); + } + // ๋‹จ์ผ ๊ฐ’ ์ฒ˜๋ฆฌ + const mapping = categoryMappings[col.columnName][valueStr]; + if (mapping) { + return mapping.label; + } + } + + return value; } // null/undefined ์ฒ˜๋ฆฌ - if (value === null || value === undefined) { - return ""; - } - - return value; + return ""; }); }); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index af342a1f..a4b6074c 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4801,7 +4801,24 @@ export class ButtonActionExecutor { const filteredRow: Record = {}; visibleColumns!.forEach((columnName: string) => { const label = columnLabels?.[columnName] || columnName; - filteredRow[label] = row[columnName]; + let value = row[columnName]; + + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋ฅผ ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (CATEGORY_๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฐ’) + if (value && typeof value === "string" && value.includes("CATEGORY_")) { + // ๋จผ์ € _label ํ•„๋“œ ํ™•์ธ (API์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ) + const labelFieldName = `${columnName}_label`; + if (row[labelFieldName]) { + value = row[labelFieldName]; + } else { + // _value_label ํ•„๋“œ ํ™•์ธ + const valueLabelFieldName = `${columnName}_value_label`; + if (row[valueLabelFieldName]) { + value = row[valueLabelFieldName]; + } + } + } + + filteredRow[label] = value; }); return filteredRow; }); @@ -5074,8 +5091,15 @@ export class ButtonActionExecutor { value = row[`${columnName}_name`]; } // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ํ•„๋“œ๋Š” ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (๋ฐฑ์—”๋“œ์—์„œ ์ •์˜๋œ ์ปฌ๋Ÿผ๋งŒ) - else if (categoryMap[columnName] && typeof value === "string" && categoryMap[columnName][value]) { - value = categoryMap[columnName][value]; + else if (categoryMap[columnName] && typeof value === "string") { + // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ + if (value.includes(",")) { + const values = value.split(",").map((v) => v.trim()).filter((v) => v); + const labels = values.map((v) => categoryMap[columnName][v] || v); + value = labels.join(", "); + } else if (categoryMap[columnName][value]) { + value = categoryMap[columnName][value]; + } } filteredRow[label] = value; diff --git a/frontend/lib/utils/excelExport.ts b/frontend/lib/utils/excelExport.ts index 52c22f5a..6bd97624 100644 --- a/frontend/lib/utils/excelExport.ts +++ b/frontend/lib/utils/excelExport.ts @@ -116,8 +116,10 @@ export async function importFromExcel( return; } - // JSON์œผ๋กœ ๋ณ€ํ™˜ - const jsonData = XLSX.utils.sheet_to_json(worksheet); + // JSON์œผ๋กœ ๋ณ€ํ™˜ (๋นˆ ์…€๋„ ํฌํ•จํ•˜์—ฌ ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‚ค ์œ ์ง€) + const jsonData = XLSX.utils.sheet_to_json(worksheet, { + defval: "", // ๋นˆ ์…€์— ๋นˆ ๋ฌธ์ž์—ด ํ• ๋‹น + }); console.log("โœ… ์—‘์…€ ๊ฐ€์ ธ์˜ค๊ธฐ ์™„๋ฃŒ:", { sheetName: targetSheetName,
0 ? 2 : 1} - > -
- {rowFields.map((f, idx) => ( -
- {f.caption} - { - const newFields = fields.map((fld) => - fld.field === field.field && fld.area === "row" - ? { ...fld, filterValues: values, filterType: type } - : fld - ); - handleFieldsChange(newFields); - }} - trigger={ - - } - /> - {idx < rowFields.length - 1 && /} -
+ {/* ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” */} + {columnHeaderLevels.length > 0 ? ( + // ์—ด ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ: ๊ฐ ๋ ˆ๋ฒจ๋ณ„๋กœ ํ–‰ ์ƒ์„ฑ + columnHeaderLevels.map((levelCells, levelIdx) => ( +
1 ? 1 : 0)} + > +
+ {rowFields.map((f, idx) => ( +
+ {f.caption} + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "row" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + {idx < rowFields.length - 1 && /} +
+ ))} + {rowFields.length === 0 && ํ•ญ๋ชฉ} +
+
+
+ {cell.caption || "(์ „์ฒด)"} + {levelIdx === columnHeaderLevels.length - 1 && dataFields.length === 1 && ( + + )} +
+
1 ? 1 : 0)} + > + ์ด๊ณ„ + 0 && ( + 1 ? 1 : 0)} + > +
+ {columnFields.map((f) => ( + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "column" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + ))} +
+
1 ? 2 : 1} > -
- {columnFields.map((f) => ( - { - const newFields = fields.map((fld) => - fld.field === field.field && fld.area === "column" - ? { ...fld, filterValues: values, filterType: type } - : fld - ); - handleFieldsChange(newFields); - }} - trigger={ - - } - /> +
+ {rowFields.map((f, idx) => ( +
+ {f.caption} + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "row" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + {idx < rowFields.length - 1 && /} +
))} + {rowFields.length === 0 && ํ•ญ๋ชฉ}
handleSort(dataFields[0].field) : undefined} + > +
+ {col.caption || "(์ „์ฒด)"} + {dataFields.length === 1 && } +
+
handleResizeStart(idx, e)} + /> +
1 ? 2 : 1} + > + ์ด๊ณ„ +