Merge branch 'ksh'

This commit is contained in:
SeongHyun Kim
2025-11-25 13:15:13 +09:00
9 changed files with 321 additions and 89 deletions

View File

@@ -35,6 +35,7 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
onRefresh?: () => void;
onClose?: () => void;
onFlowRefresh?: () => void;
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
// 폼 데이터 관련
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
@@ -83,6 +84,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
onRefresh,
onClose,
onFlowRefresh,
onSave, // 🆕 EditModal의 handleSave 콜백
sortBy, // 🆕 정렬 컬럼
sortOrder, // 🆕 정렬 방향
columnOrder, // 🆕 컬럼 순서
@@ -95,6 +97,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
...props
}) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
const finalOnSave = onSave || propsOnSave;
// 🆕 플로우 단계별 표시 제어
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
@@ -415,6 +421,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
onRefresh,
onClose,
onFlowRefresh, // 플로우 새로고침 콜백 추가
onSave: finalOnSave, // 🆕 EditModal의 handleSave 콜백 (props에서도 추출)
// 테이블 선택된 행 정보 추가
selectedRows,
selectedRowsData,

View File

@@ -41,6 +41,7 @@ export function ConditionalContainerComponent({
style,
className,
groupedData, // 🆕 그룹 데이터
onSave, // 🆕 EditModal의 handleSave 콜백
}: ConditionalContainerProps) {
console.log("🎯 ConditionalContainerComponent 렌더링!", {
isDesignMode,
@@ -179,6 +180,7 @@ export function ConditionalContainerComponent({
formData={formData}
onFormDataChange={onFormDataChange}
groupedData={groupedData}
onSave={onSave}
/>
))}
</div>
@@ -199,6 +201,7 @@ export function ConditionalContainerComponent({
formData={formData}
onFormDataChange={onFormDataChange}
groupedData={groupedData}
onSave={onSave}
/>
) : null
)

View File

@@ -26,6 +26,7 @@ export function ConditionalSectionViewer({
formData,
onFormDataChange,
groupedData, // 🆕 그룹 데이터
onSave, // 🆕 EditModal의 handleSave 콜백
}: ConditionalSectionViewerProps) {
const { userId, userName, user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
@@ -153,17 +154,18 @@ export function ConditionalSectionViewer({
}}
>
<DynamicComponentRenderer
component={component}
component={component}
isInteractive={true}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
userId={userId}
userName={userName}
companyCode={user?.companyCode}
formData={formData}
onFormDataChange={onFormDataChange}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
userId={userId}
userName={userName}
companyCode={user?.companyCode}
formData={formData}
onFormDataChange={onFormDataChange}
groupedData={groupedData}
/>
onSave={onSave}
/>
</div>
);
})}

View File

@@ -46,6 +46,7 @@ export interface ConditionalContainerProps {
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
// 화면 편집기 관련
isDesignMode?: boolean; // 디자인 모드 여부
@@ -77,5 +78,6 @@ export interface ConditionalSectionViewerProps {
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
}

View File

@@ -322,6 +322,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [searchTerm, setSearchTerm] = useState("");
const [sortColumn, setSortColumn] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const hasInitializedSort = useRef(false);
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
const [tableLabel, setTableLabel] = useState<string>("");
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
@@ -508,6 +509,28 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
unregisterTable,
]);
// 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기
useEffect(() => {
if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return;
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
const savedSort = localStorage.getItem(storageKey);
if (savedSort) {
try {
const { column, direction } = JSON.parse(savedSort);
if (column && direction) {
setSortColumn(column);
setSortDirection(direction);
hasInitializedSort.current = true;
console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction });
}
} catch (error) {
console.error("❌ 정렬 상태 복원 실패:", error);
}
}
}, [tableConfig.selectedTable, userId]);
// 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기
useEffect(() => {
if (!tableConfig.selectedTable || !userId) return;
@@ -955,6 +978,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
newSortDirection = "asc";
}
// 🎯 정렬 상태를 localStorage에 저장 (사용자별)
if (tableConfig.selectedTable && userId) {
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
try {
localStorage.setItem(storageKey, JSON.stringify({
column: newSortColumn,
direction: newSortDirection
}));
console.log("💾 정렬 상태 저장:", { column: newSortColumn, direction: newSortDirection });
} catch (error) {
console.error("❌ 정렬 상태 저장 실패:", error);
}
}
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
@@ -1876,11 +1913,59 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
};
}, [tableConfig.selectedTable, isDesignMode]);
// 초기 컬럼 너비 측정 (한 번만)
// 🎯 컬럼 너비 자동 계산 (내용 기반)
const calculateOptimalColumnWidth = useCallback((columnName: string, displayName: string): number => {
// 기본 너비 설정
const MIN_WIDTH = 100;
const MAX_WIDTH = 400;
const PADDING = 48; // 좌우 패딩 + 여유 공간
const HEADER_PADDING = 60; // 헤더 추가 여유 (정렬 아이콘 등)
// 헤더 텍스트 너비 계산 (대략 8px per character)
const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING;
// 데이터 셀 너비 계산 (상위 50개 샘플링)
const sampleSize = Math.min(50, data.length);
let maxDataWidth = headerWidth;
for (let i = 0; i < sampleSize; i++) {
const cellValue = data[i]?.[columnName];
if (cellValue !== null && cellValue !== undefined) {
const cellText = String(cellValue);
// 숫자는 좁게, 텍스트는 넓게 계산
const isNumber = !isNaN(Number(cellValue)) && cellValue !== "";
const charWidth = isNumber ? 8 : 9;
const cellWidth = cellText.length * charWidth + PADDING;
maxDataWidth = Math.max(maxDataWidth, cellWidth);
}
}
// 최소/최대 범위 내로 제한
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth)));
}, [data]);
// 🎯 localStorage에서 컬럼 너비 불러오기 및 초기 계산
useEffect(() => {
if (!hasInitializedWidths.current && visibleColumns.length > 0) {
// 약간의 지연을 두고 DOM이 완전히 렌더링된 후 측정
if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) {
const timer = setTimeout(() => {
const storageKey = tableConfig.selectedTable && userId
? `table_column_widths_${tableConfig.selectedTable}_${userId}`
: null;
// 1. localStorage에서 저장된 너비 불러오기
let savedWidths: Record<string, number> = {};
if (storageKey) {
try {
const saved = localStorage.getItem(storageKey);
if (saved) {
savedWidths = JSON.parse(saved);
}
} catch (error) {
console.error("컬럼 너비 불러오기 실패:", error);
}
}
// 2. 자동 계산 또는 저장된 너비 적용
const newWidths: Record<string, number> = {};
let hasAnyWidth = false;
@@ -1888,13 +1973,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 체크박스 컬럼은 제외 (고정 48px)
if (column.columnName === "__checkbox__") return;
const thElement = columnRefs.current[column.columnName];
if (thElement) {
const measuredWidth = thElement.offsetWidth;
if (measuredWidth > 0) {
newWidths[column.columnName] = measuredWidth;
hasAnyWidth = true;
}
// 저장된 너비가 있으면 우선 사용
if (savedWidths[column.columnName]) {
newWidths[column.columnName] = savedWidths[column.columnName];
hasAnyWidth = true;
} else {
// 저장된 너비가 없으면 자동 계산
const optimalWidth = calculateOptimalColumnWidth(
column.columnName,
columnLabels[column.columnName] || column.displayName
);
newWidths[column.columnName] = optimalWidth;
hasAnyWidth = true;
}
});
@@ -1902,11 +1992,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setColumnWidths(newWidths);
hasInitializedWidths.current = true;
}
}, 100);
}, 150); // DOM 렌더링 대기
return () => clearTimeout(timer);
}
}, [visibleColumns]);
}, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]);
// ========================================
// 페이지네이션 JSX
@@ -2241,7 +2331,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 최종 너비를 state에 저장
if (thElement) {
const finalWidth = Math.max(80, thElement.offsetWidth);
setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth }));
setColumnWidths((prev) => {
const newWidths = { ...prev, [column.columnName]: finalWidth };
// 🎯 localStorage에 컬럼 너비 저장 (사용자별)
if (tableConfig.selectedTable && userId) {
const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`;
try {
localStorage.setItem(storageKey, JSON.stringify(newWidths));
} catch (error) {
console.error("컬럼 너비 저장 실패:", error);
}
}
return newWidths;
});
}
// 텍스트 선택 복원