fix: 화면 편집기 높이 입력 필드 1px 단위 조절 가능하도록 수정
- 문제: 높이 입력 시 10 단위로만 입력 가능 (예: 1080 입력 불가) - 원인: 격자 스냅 로직이 onChange마다 높이를 10/20 단위로 강제 반올림 - 해결: 1. 모든 number input 필드에 step="1" 추가 2. ScreenDesigner.tsx의 격자 스냅 로직 수정 (높이 스냅 제거) 3. UnifiedPropertiesPanel.tsx에 로컬 상태 추가하여 입력 중 스냅 방지 4. onBlur/Enter 시에만 실제 값 업데이트 수정 파일: - frontend/components/screen/ScreenDesigner.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/panels/PropertiesPanel.tsx - frontend/components/screen/panels/ResolutionPanel.tsx - frontend/components/screen/panels/RowSettingsPanel.tsx - frontend/components/screen/panels/webtype-configs/NumberTypeConfigPanel.tsx - frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx
This commit is contained in:
parent
cf9e81a216
commit
44def0979c
|
|
@ -181,7 +181,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
// 🆕 전역 테이블 새로고침 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleRefreshTable = () => {
|
||||
console.log("🔄 InteractiveDataTable: 전역 새로고침 이벤트 수신");
|
||||
if (component.tableName) {
|
||||
loadData(currentPage, searchValues);
|
||||
}
|
||||
|
|
@ -206,15 +205,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
return webType === "category";
|
||||
});
|
||||
|
||||
console.log(`🔍 InteractiveDataTable 카테고리 컬럼 확인:`, {
|
||||
tableName: component.tableName,
|
||||
totalColumns: component.columns?.length,
|
||||
categoryColumns: categoryColumns?.map(c => ({
|
||||
name: c.columnName,
|
||||
webType: getColumnWebType(c.columnName)
|
||||
}))
|
||||
});
|
||||
|
||||
if (!categoryColumns || categoryColumns.length === 0) return;
|
||||
|
||||
// 각 카테고리 컬럼의 값 목록 조회
|
||||
|
|
@ -239,12 +229,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
console.log(`✅ InteractiveDataTable 카테고리 매핑 완료:`, {
|
||||
tableName: component.tableName,
|
||||
mappedColumns: Object.keys(mappings),
|
||||
mappings
|
||||
});
|
||||
|
||||
setCategoryMappings(mappings);
|
||||
} catch (error) {
|
||||
console.error("카테고리 매핑 로드 실패:", error);
|
||||
|
|
@ -403,7 +387,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
// 대체 URL 생성 (직접 파일 경로 사용)
|
||||
if (previewImage.path) {
|
||||
const altUrl = getDirectFileUrl(previewImage.path);
|
||||
// console.log("대체 URL 시도:", altUrl);
|
||||
setAlternativeImageUrl(altUrl);
|
||||
} else {
|
||||
toast.error("이미지를 불러올 수 없습니다.");
|
||||
|
|
@ -469,7 +452,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
try {
|
||||
return tableColumn?.detailSettings ? JSON.parse(tableColumn.detailSettings) : {};
|
||||
} catch {
|
||||
// console.warn("상세 설정 파싱 실패:", tableColumn?.detailSettings);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
|
@ -672,15 +654,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
const handleRefreshFileStatus = async (event: CustomEvent) => {
|
||||
const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail;
|
||||
|
||||
// console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", {
|
||||
// tableName,
|
||||
// recordId,
|
||||
// columnName,
|
||||
// targetObjid,
|
||||
// fileCount,
|
||||
// currentTableName: component.tableName
|
||||
// });
|
||||
|
||||
// 현재 테이블과 일치하는지 확인
|
||||
if (tableName === component.tableName) {
|
||||
// 해당 행의 파일 상태 업데이트
|
||||
|
|
@ -690,13 +663,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
[recordId]: { hasFiles: fileCount > 0, fileCount },
|
||||
[columnKey]: { hasFiles: fileCount > 0, fileCount },
|
||||
}));
|
||||
|
||||
// console.log("✅ 파일 상태 업데이트 완료:", {
|
||||
// recordId,
|
||||
// columnKey,
|
||||
// hasFiles: fileCount > 0,
|
||||
// fileCount
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1104,7 +1070,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
setIsAdding(true);
|
||||
|
||||
// 실제 API 호출로 데이터 추가
|
||||
// console.log("🔥 추가할 데이터:", addFormData);
|
||||
await tableTypeApi.addTableData(component.tableName, addFormData);
|
||||
|
||||
// 모달 닫기 및 폼 초기화
|
||||
|
|
@ -1127,9 +1092,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
setIsEditing(true);
|
||||
|
||||
// 실제 API 호출로 데이터 수정
|
||||
// console.log("🔥 수정할 데이터:", editFormData);
|
||||
// console.log("🔥 원본 데이터:", editingRowData);
|
||||
|
||||
if (editingRowData) {
|
||||
await tableTypeApi.editTableData(component.tableName, editingRowData, editFormData);
|
||||
|
||||
|
|
@ -1200,7 +1162,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
const selectedData = Array.from(selectedRows).map((index) => data[index]);
|
||||
|
||||
// 실제 삭제 API 호출
|
||||
// console.log("🗑️ 삭제할 데이터:", selectedData);
|
||||
await tableTypeApi.deleteTableData(component.tableName, selectedData);
|
||||
|
||||
// 선택 해제 및 다이얼로그 닫기
|
||||
|
|
@ -1488,12 +1449,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
case "category": {
|
||||
// 카테고리 셀렉트 (동적 import)
|
||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
console.log("🎯 카테고리 렌더링 (편집 폼):", {
|
||||
tableName: component.tableName,
|
||||
columnName: column.columnName,
|
||||
columnLabel: column.label,
|
||||
value,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<CategorySelectComponent
|
||||
|
|
@ -1775,12 +1730,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
case "category": {
|
||||
// 카테고리 셀렉트 (동적 import)
|
||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
console.log("🎯 카테고리 렌더링 (추가 폼):", {
|
||||
tableName: component.tableName,
|
||||
columnName: column.columnName,
|
||||
columnLabel: column.label,
|
||||
value,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<CategorySelectComponent
|
||||
|
|
@ -1868,8 +1817,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
const handleDeleteLinkedFile = useCallback(
|
||||
async (fileId: string, fileName: string) => {
|
||||
try {
|
||||
// console.log("🗑️ 파일 삭제 시작:", { fileId, fileName });
|
||||
|
||||
// 삭제 확인 다이얼로그
|
||||
if (!confirm(`"${fileName}" 파일을 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
|
|
@ -1884,7 +1831,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
});
|
||||
|
||||
const result = response.data;
|
||||
// console.log("📡 파일 삭제 API 응답:", result);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "파일 삭제 실패");
|
||||
|
|
@ -1901,15 +1847,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
try {
|
||||
const response = await getLinkedFiles(component.tableName, recordId);
|
||||
setLinkedFiles(response.files || []);
|
||||
// console.log("📁 파일 목록 새로고침 완료:", response.files?.length || 0);
|
||||
} catch (error) {
|
||||
// console.error("파일 목록 새로고침 실패:", error);
|
||||
// 파일 목록 새로고침 실패 시 무시
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("✅ 파일 삭제 완료:", fileName);
|
||||
} catch (error) {
|
||||
// console.error("❌ 파일 삭제 실패:", error);
|
||||
toast.error(`"${fileName}" 파일 삭제에 실패했습니다.`);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -619,7 +619,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
||||
const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth));
|
||||
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
|
||||
const snappedHeight = Math.max(10, Math.round(newComp.size.height / 10) * 10);
|
||||
// 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거)
|
||||
const snappedHeight = Math.max(10, newComp.size.height);
|
||||
|
||||
newComp.position = {
|
||||
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
||||
|
|
@ -3028,7 +3029,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
||||
const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth));
|
||||
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
|
||||
const snappedHeight = Math.max(40, Math.round(comp.size.height / 20) * 20);
|
||||
// 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거)
|
||||
const snappedHeight = Math.max(40, comp.size.height);
|
||||
|
||||
newPosition = {
|
||||
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
||||
|
|
|
|||
|
|
@ -695,6 +695,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
<Input
|
||||
id="positionX"
|
||||
type="number"
|
||||
step="1"
|
||||
value={(() => {
|
||||
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
|
||||
if (isDragging) {
|
||||
|
|
@ -725,6 +726,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
<Input
|
||||
id="positionY"
|
||||
type="number"
|
||||
step="1"
|
||||
value={(() => {
|
||||
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
|
||||
if (isDragging) {
|
||||
|
|
@ -762,6 +764,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
type="number"
|
||||
min={1}
|
||||
max={gridSettings?.columns || 12}
|
||||
step="1"
|
||||
value={(selectedComponent as any)?.gridColumns || 1}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
|
|
@ -961,27 +964,27 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="height" className="text-sm font-medium">
|
||||
최소 높이 (10px 단위)
|
||||
최소 높이
|
||||
</Label>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Input
|
||||
id="height"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={Math.round((localInputs.height || 10) / 10)}
|
||||
min="10"
|
||||
max="2000"
|
||||
step="1"
|
||||
value={localInputs.height || 40}
|
||||
onChange={(e) => {
|
||||
const units = Math.max(1, Math.min(100, Number(e.target.value)));
|
||||
const newHeight = units * 10;
|
||||
const newHeight = Math.max(10, Number(e.target.value));
|
||||
setLocalInputs((prev) => ({ ...prev, height: newHeight.toString() }));
|
||||
onUpdateProperty("size.height", newHeight);
|
||||
}}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">단위 = {localInputs.height || 10}px</span>
|
||||
<span className="text-sm text-gray-500">{localInputs.height || 40}px</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
1단위 = 10px (현재 {Math.round((localInputs.height || 10) / 10)}단위) - 내부 콘텐츠에 맞춰 늘어남
|
||||
높이 자유 조절 (10px ~ 2000px, 1px 단위)
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -996,11 +999,12 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
<Label htmlFor="zIndex" className="text-sm font-medium">
|
||||
Z-Index (레이어 순서)
|
||||
</Label>
|
||||
<Input
|
||||
<Input
|
||||
id="zIndex"
|
||||
type="number"
|
||||
min="0"
|
||||
max="9999"
|
||||
step="1"
|
||||
value={localInputs.positionZ}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
|
|
@ -1266,6 +1270,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
type="number"
|
||||
min="1"
|
||||
max="12"
|
||||
step="1"
|
||||
value={(selectedComponent as AreaComponent).layoutConfig?.gridColumns || 3}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
|
|
@ -1279,6 +1284,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={(selectedComponent as AreaComponent).layoutConfig?.gridGap || 16}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
|
|
@ -1315,6 +1321,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={(selectedComponent as AreaComponent).layoutConfig?.gap || 16}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
|
|
@ -1345,6 +1352,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
<Input
|
||||
type="number"
|
||||
min="100"
|
||||
step="1"
|
||||
value={(selectedComponent as AreaComponent).layoutConfig?.sidebarWidth || 200}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
|||
onChange={(e) => setCustomWidth(e.target.value)}
|
||||
placeholder="1920"
|
||||
min="1"
|
||||
step="1"
|
||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
|
|
@ -158,6 +159,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
|||
onChange={(e) => setCustomHeight(e.target.value)}
|
||||
placeholder="1080"
|
||||
min="1"
|
||||
step="1"
|
||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
|||
placeholder="100"
|
||||
min={50}
|
||||
max={1000}
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -73,6 +74,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
|||
placeholder="50"
|
||||
min={0}
|
||||
max={1000}
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -89,6 +91,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
|||
placeholder="500"
|
||||
min={0}
|
||||
max={2000}
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
||||
|
||||
// 높이 입력 로컬 상태 (격자 스냅 방지)
|
||||
const [localHeight, setLocalHeight] = useState<string>("");
|
||||
|
||||
// 새로운 컴포넌트 시스템의 webType 동기화
|
||||
useEffect(() => {
|
||||
if (selectedComponent?.type === "component") {
|
||||
|
|
@ -115,6 +118,13 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
}
|
||||
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
|
||||
|
||||
// 높이 값 동기화
|
||||
useEffect(() => {
|
||||
if (selectedComponent?.size?.height !== undefined) {
|
||||
setLocalHeight(String(selectedComponent.size.height));
|
||||
}
|
||||
}, [selectedComponent?.size?.height, selectedComponent?.id]);
|
||||
|
||||
// 격자 설정 업데이트 함수 (early return 이전에 정의)
|
||||
const updateGridSetting = (key: string, value: any) => {
|
||||
if (onGridSettingsChange && gridSettings) {
|
||||
|
|
@ -180,6 +190,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
id="columns"
|
||||
type="number"
|
||||
min={1}
|
||||
step="1"
|
||||
value={gridSettings.columns}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
|
|
@ -361,11 +372,27 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
<Label className="text-xs">높이</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.size?.height || 0}
|
||||
value={localHeight}
|
||||
onChange={(e) => {
|
||||
// 입력 중에는 로컬 상태만 업데이트 (격자 스냅 방지)
|
||||
setLocalHeight(e.target.value);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// 포커스를 잃을 때만 실제로 업데이트
|
||||
const value = parseInt(e.target.value) || 0;
|
||||
// 최소값 제한 없이, 1px 단위로 조절 가능
|
||||
handleUpdate("size.height", Math.max(1, value));
|
||||
if (value >= 1) {
|
||||
handleUpdate("size.height", value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Enter 키를 누르면 즉시 적용
|
||||
if (e.key === "Enter") {
|
||||
const value = parseInt(e.currentTarget.value) || 0;
|
||||
if (value >= 1) {
|
||||
handleUpdate("size.height", value);
|
||||
}
|
||||
e.currentTarget.blur(); // 포커스 제거
|
||||
}
|
||||
}}
|
||||
step={1}
|
||||
placeholder="10"
|
||||
|
|
@ -430,6 +457,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
type="number"
|
||||
min={1}
|
||||
max={gridSettings?.columns || 12}
|
||||
step="1"
|
||||
value={(selectedComponent as any).gridColumns || 1}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
|
|
@ -455,6 +483,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
<Label className="text-xs">Z-Index</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
value={currentPosition.z || 1}
|
||||
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
||||
className="h-6 w-full px-2 py-0 text-xs"
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
|||
<Input
|
||||
id="min"
|
||||
type="number"
|
||||
step="1"
|
||||
value={localValues.min}
|
||||
onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="mt-1"
|
||||
|
|
@ -146,6 +147,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
|||
<Input
|
||||
id="max"
|
||||
type="number"
|
||||
step="1"
|
||||
value={localValues.max}
|
||||
onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="mt-1"
|
||||
|
|
@ -181,6 +183,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
|||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
step="1"
|
||||
value={localValues.decimalPlaces}
|
||||
onChange={(e) => updateConfig("decimalPlaces", e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="mt-1"
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
|||
id="minLength"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={localValues.minLength}
|
||||
onChange={(e) => updateConfig("minLength", e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="mt-1"
|
||||
|
|
@ -183,6 +184,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
|||
id="maxLength"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={localValues.maxLength}
|
||||
onChange={(e) => updateConfig("maxLength", e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="mt-1"
|
||||
|
|
|
|||
|
|
@ -75,3 +75,4 @@ export const numberingRuleTemplate = {
|
|||
],
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,35 @@ export function FlowWidget({
|
|||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const { user } = useAuth(); // 사용자 정보 가져오기
|
||||
|
||||
// 🆕 전역 상태 관리
|
||||
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
||||
const resetFlow = useFlowStepStore((state) => state.resetFlow);
|
||||
|
||||
const [flowData, setFlowData] = useState<FlowDefinition | null>(null);
|
||||
const [steps, setSteps] = useState<FlowStep[]>([]);
|
||||
const [stepCounts, setStepCounts] = useState<Record<number, number>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connections, setConnections] = useState<any[]>([]); // 플로우 연결 정보
|
||||
|
||||
// 선택된 스텝의 데이터 리스트 상태
|
||||
const [selectedStepId, setSelectedStepId] = useState<number | null>(null);
|
||||
const [stepData, setStepData] = useState<any[]>([]);
|
||||
const [stepDataColumns, setStepDataColumns] = useState<string[]>([]);
|
||||
const [stepDataLoading, setStepDataLoading] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||
|
||||
// 🆕 검색 필터 관련 상태
|
||||
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set()); // 검색 필터로 사용할 컬럼
|
||||
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그
|
||||
const [searchValues, setSearchValues] = useState<Record<string, string>>({}); // 검색 값
|
||||
const [allAvailableColumns, setAllAvailableColumns] = useState<string[]>([]); // 전체 컬럼 목록
|
||||
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
|
||||
|
||||
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> 라벨})
|
||||
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, string>>>({});
|
||||
|
||||
// 값 포맷팅 함수 (숫자, 카테고리 등)
|
||||
const formatValue = useCallback((value: any, columnName?: string): string => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
|
|
@ -97,35 +126,6 @@ export function FlowWidget({
|
|||
return String(value);
|
||||
}, [categoryMappings]);
|
||||
|
||||
// 🆕 전역 상태 관리
|
||||
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
||||
const resetFlow = useFlowStepStore((state) => state.resetFlow);
|
||||
|
||||
const [flowData, setFlowData] = useState<FlowDefinition | null>(null);
|
||||
const [steps, setSteps] = useState<FlowStep[]>([]);
|
||||
const [stepCounts, setStepCounts] = useState<Record<number, number>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connections, setConnections] = useState<any[]>([]); // 플로우 연결 정보
|
||||
|
||||
// 선택된 스텝의 데이터 리스트 상태
|
||||
const [selectedStepId, setSelectedStepId] = useState<number | null>(null);
|
||||
const [stepData, setStepData] = useState<any[]>([]);
|
||||
const [stepDataColumns, setStepDataColumns] = useState<string[]>([]);
|
||||
const [stepDataLoading, setStepDataLoading] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||
|
||||
// 🆕 검색 필터 관련 상태
|
||||
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set()); // 검색 필터로 사용할 컬럼
|
||||
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그
|
||||
const [searchValues, setSearchValues] = useState<Record<string, string>>({}); // 검색 값
|
||||
const [allAvailableColumns, setAllAvailableColumns] = useState<string[]>([]); // 전체 컬럼 목록
|
||||
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
|
||||
|
||||
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> 라벨})
|
||||
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, string>>>({});
|
||||
|
||||
// 🆕 그룹 설정 관련 상태
|
||||
const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); // 그룹 설정 다이얼로그
|
||||
const [groupByColumns, setGroupByColumns] = useState<string[]>([]); // 그룹화할 컬럼 목록
|
||||
|
|
@ -382,12 +382,6 @@ export function FlowWidget({
|
|||
});
|
||||
|
||||
setFilteredData(filtered);
|
||||
console.log("🔍 검색 실행:", {
|
||||
totalRows: stepData.length,
|
||||
filteredRows: filtered.length,
|
||||
searchValues,
|
||||
hasSearchValue,
|
||||
});
|
||||
}, [searchValues, stepData]); // stepData와 searchValues가 변경될 때마다 실행
|
||||
|
||||
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
||||
|
|
@ -471,7 +465,6 @@ export function FlowWidget({
|
|||
|
||||
// 프리뷰 모드에서는 샘플 데이터만 표시
|
||||
if (isPreviewMode) {
|
||||
console.log("🔒 프리뷰 모드: 플로우 데이터 로드 차단 - 샘플 데이터 표시");
|
||||
setFlowData({
|
||||
id: flowId || 0,
|
||||
flowName: flowName || "샘플 플로우",
|
||||
|
|
@ -648,16 +641,9 @@ export function FlowWidget({
|
|||
try {
|
||||
// 컬럼 라벨 조회
|
||||
const labelsResponse = await getStepColumnLabels(flowId!, stepId);
|
||||
console.log("🏷️ 컬럼 라벨 조회 결과:", {
|
||||
stepId,
|
||||
success: labelsResponse.success,
|
||||
labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0,
|
||||
labels: labelsResponse.data,
|
||||
});
|
||||
if (labelsResponse.success && labelsResponse.data) {
|
||||
setColumnLabels(labelsResponse.data);
|
||||
} else {
|
||||
console.warn("⚠️ 컬럼 라벨 조회 실패 또는 데이터 없음:", labelsResponse);
|
||||
setColumnLabels({});
|
||||
}
|
||||
|
||||
|
|
@ -761,13 +747,6 @@ export function FlowWidget({
|
|||
|
||||
// 선택된 데이터를 상위로 전달
|
||||
const selectedData = Array.from(newSelected).map((index) => stepData[index]);
|
||||
console.log("🌊 FlowWidget - 체크박스 토글, 상위로 전달:", {
|
||||
rowIndex,
|
||||
newSelectedSize: newSelected.size,
|
||||
selectedData,
|
||||
selectedStepId,
|
||||
hasCallback: !!onSelectedDataChange,
|
||||
});
|
||||
onSelectedDataChange?.(selectedData, selectedStepId);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
|||
const loadCategoryValues = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await getCategoryValues(tableName, columnName);
|
||||
// includeInactive: true로 비활성 값도 포함
|
||||
const response = await getCategoryValues(tableName, columnName, true);
|
||||
if (response.success && response.data) {
|
||||
setValues(response.data);
|
||||
setFilteredValues(response.data);
|
||||
|
|
|
|||
|
|
@ -133,3 +133,4 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -186,9 +186,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 객체인 경우 tableName 속성 추출 시도
|
||||
if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
|
||||
console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable);
|
||||
finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName;
|
||||
console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable);
|
||||
}
|
||||
|
||||
tableConfig.selectedTable = finalSelectedTable;
|
||||
|
|
@ -282,13 +280,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
if (savedOrder) {
|
||||
try {
|
||||
const parsedOrder = JSON.parse(savedOrder);
|
||||
console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder });
|
||||
setColumnOrder(parsedOrder);
|
||||
|
||||
// 부모 컴포넌트에 초기 컬럼 순서 전달
|
||||
if (onSelectedRowsChange && parsedOrder.length > 0) {
|
||||
console.log("✅ 초기 컬럼 순서 전달:", parsedOrder);
|
||||
|
||||
// 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬)
|
||||
const initialData = data.map((row: any) => {
|
||||
const reordered: any = {};
|
||||
|
|
@ -306,8 +301,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return reordered;
|
||||
});
|
||||
|
||||
console.log("📊 초기 화면 표시 데이터 전달:", { count: initialData.length, firstRow: initialData[0] });
|
||||
|
||||
// 전역 저장소에 데이터 저장
|
||||
if (tableConfig.selectedTable) {
|
||||
tableDisplayStore.setTableData(
|
||||
|
|
@ -584,8 +577,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
};
|
||||
|
||||
const handleSort = (column: string) => {
|
||||
console.log("🔄 정렬 클릭:", { column, currentSortColumn: sortColumn, currentSortDirection: sortDirection });
|
||||
|
||||
let newSortColumn = column;
|
||||
let newSortDirection: "asc" | "desc" = "asc";
|
||||
|
||||
|
|
@ -599,9 +590,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
newSortDirection = "asc";
|
||||
}
|
||||
|
||||
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
|
||||
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
|
||||
|
||||
// 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달
|
||||
if (onSelectedRowsChange) {
|
||||
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
|
||||
|
|
@ -651,16 +639,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return reordered;
|
||||
});
|
||||
|
||||
console.log("✅ 정렬 정보 전달:", {
|
||||
selectedRowsCount: selectedRows.size,
|
||||
selectedRowsDataCount: selectedRowsData.length,
|
||||
sortBy: newSortColumn,
|
||||
sortOrder: newSortDirection,
|
||||
columnOrder: columnOrder.length > 0 ? columnOrder : undefined,
|
||||
tableDisplayDataCount: reorderedData.length,
|
||||
firstRowAfterSort: reorderedData[0]?.[newSortColumn],
|
||||
lastRowAfterSort: reorderedData[reorderedData.length - 1]?.[newSortColumn]
|
||||
});
|
||||
onSelectedRowsChange(
|
||||
Array.from(selectedRows),
|
||||
selectedRowsData,
|
||||
|
|
@ -681,8 +659,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
newSortDirection
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -774,8 +750,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||
|
||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
||||
|
||||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||
};
|
||||
|
||||
// 컬럼 드래그앤드롭 기능 제거됨 (테이블 옵션 모달에서 컬럼 순서 변경 가능)
|
||||
|
|
@ -820,12 +794,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// columnOrder에 없는 새로운 컬럼들 추가
|
||||
const remainingCols = cols.filter(c => !columnOrder.includes(c.columnName));
|
||||
|
||||
console.log("🔄 columnOrder 기반 정렬:", {
|
||||
columnOrder,
|
||||
orderedColsCount: orderedCols.length,
|
||||
remainingColsCount: remainingCols.length
|
||||
});
|
||||
|
||||
return [...orderedCols, ...remainingCols];
|
||||
}
|
||||
|
||||
|
|
@ -836,19 +804,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const lastColumnOrderRef = useRef<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", {
|
||||
hasCallback: !!onSelectedRowsChange,
|
||||
visibleColumnsLength: visibleColumns.length,
|
||||
visibleColumnsNames: visibleColumns.map(c => c.columnName),
|
||||
});
|
||||
|
||||
if (!onSelectedRowsChange) {
|
||||
console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleColumns.length === 0) {
|
||||
console.warn("⚠️ visibleColumns가 비어있습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -856,23 +816,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
.map(col => col.columnName)
|
||||
.filter(name => name !== "__checkbox__"); // 체크박스 컬럼 제외
|
||||
|
||||
console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder);
|
||||
|
||||
// 컬럼 순서가 실제로 변경되었을 때만 전달 (무한 루프 방지)
|
||||
const columnOrderString = currentColumnOrder.join(",");
|
||||
console.log("🔍 [컬럼 순서] 비교:", {
|
||||
current: columnOrderString,
|
||||
last: lastColumnOrderRef.current,
|
||||
isDifferent: columnOrderString !== lastColumnOrderRef.current,
|
||||
});
|
||||
|
||||
if (columnOrderString === lastColumnOrderRef.current) {
|
||||
console.log("⏭️ 컬럼 순서 변경 없음, 전달 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
lastColumnOrderRef.current = columnOrderString;
|
||||
console.log("📊 현재 화면 컬럼 순서 전달:", currentColumnOrder);
|
||||
|
||||
// 선택된 행 데이터 가져오기
|
||||
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
|
||||
|
|
|
|||
|
|
@ -375,3 +375,4 @@ interface TablePermission {
|
|||
|
||||
**이제 테이블을 만들 때마다 코드를 수정할 필요가 없습니다!**
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue