Compare commits
No commits in common. "ff2a069b79f1c2f03c98e99d86741971ce9fc214" and "cf9e81a216c9838c32512857f7b7d4ce09138b91" have entirely different histories.
ff2a069b79
...
cf9e81a216
|
|
@ -203,7 +203,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
separator: "-",
|
separator: "-",
|
||||||
resetPeriod: "none",
|
resetPeriod: "none",
|
||||||
currentSequence: 1,
|
currentSequence: 1,
|
||||||
scopeType: "menu",
|
scopeType: "global",
|
||||||
};
|
};
|
||||||
|
|
||||||
setSelectedRuleId(newRule.ruleId);
|
setSelectedRuleId(newRule.ruleId);
|
||||||
|
|
@ -251,15 +251,16 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
savedRules.map((rule) => (
|
savedRules.map((rule) => (
|
||||||
<Card
|
<Card
|
||||||
key={rule.ruleId}
|
key={rule.ruleId}
|
||||||
className={`py-2 border-border hover:bg-accent cursor-pointer transition-colors ${
|
className={`border-border hover:bg-accent cursor-pointer transition-colors ${
|
||||||
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
|
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelectRule(rule)}
|
onClick={() => handleSelectRule(rule)}
|
||||||
>
|
>
|
||||||
<CardHeader className="px-3 py-0">
|
<CardHeader className="p-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
|
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">규칙 {rule.parts.length}개</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -274,6 +275,9 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent className="p-3 pt-0">
|
||||||
|
<NumberingRulePreview config={rule} compact />
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
@ -312,22 +316,47 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="space-y-2">
|
||||||
<div className="flex-1 space-y-2">
|
<Label className="text-sm font-medium">규칙명</Label>
|
||||||
<Label className="text-sm font-medium">규칙명</Label>
|
<Input
|
||||||
<Input
|
value={currentRule.ruleName}
|
||||||
value={currentRule.ruleName}
|
onChange={(e) => setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))}
|
||||||
onChange={(e) => setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))}
|
className="h-9"
|
||||||
className="h-9"
|
placeholder="예: 프로젝트 코드"
|
||||||
placeholder="예: 프로젝트 코드"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 space-y-2">
|
|
||||||
<Label className="text-sm font-medium">미리보기</Label>
|
|
||||||
<NumberingRulePreview config={currentRule} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">적용 범위</Label>
|
||||||
|
<Select
|
||||||
|
value={currentRule.scopeType || "global"}
|
||||||
|
onValueChange={(value: "global" | "menu") => setCurrentRule((prev) => ({ ...prev!, scopeType: value }))}
|
||||||
|
disabled={isPreview}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="global">회사 전체</SelectItem>
|
||||||
|
<SelectItem value="menu">메뉴별</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
|
{currentRule.scopeType === "menu"
|
||||||
|
? "⚠️ 현재 화면이 속한 2레벨 메뉴와 그 하위 메뉴(3레벨 이상)에서만 사용됩니다. 형제 메뉴와 구분하여 채번 규칙을 관리할 때 유용합니다."
|
||||||
|
: "회사 내 모든 메뉴에서 사용 가능한 전역 규칙입니다"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="border-border bg-card">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium">미리보기</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<NumberingRulePreview config={currentRule} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold">코드 구성</h3>
|
<h3 className="text-sm font-semibold">코드 구성</h3>
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,11 @@ export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md bg-muted px-3 py-2">
|
<div className="space-y-2">
|
||||||
<code className="text-sm font-mono text-foreground">{generatedCode}</code>
|
<p className="text-xs text-muted-foreground sm:text-sm">코드 미리보기</p>
|
||||||
|
<div className="rounded-md bg-muted p-3 sm:p-4">
|
||||||
|
<code className="text-sm font-mono text-foreground sm:text-base">{generatedCode}</code>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
// 🆕 전역 테이블 새로고침 이벤트 리스너
|
// 🆕 전역 테이블 새로고침 이벤트 리스너
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRefreshTable = () => {
|
const handleRefreshTable = () => {
|
||||||
|
console.log("🔄 InteractiveDataTable: 전역 새로고침 이벤트 수신");
|
||||||
if (component.tableName) {
|
if (component.tableName) {
|
||||||
loadData(currentPage, searchValues);
|
loadData(currentPage, searchValues);
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +206,15 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
return webType === "category";
|
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;
|
if (!categoryColumns || categoryColumns.length === 0) return;
|
||||||
|
|
||||||
// 각 카테고리 컬럼의 값 목록 조회
|
// 각 카테고리 컬럼의 값 목록 조회
|
||||||
|
|
@ -229,6 +239,12 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`✅ InteractiveDataTable 카테고리 매핑 완료:`, {
|
||||||
|
tableName: component.tableName,
|
||||||
|
mappedColumns: Object.keys(mappings),
|
||||||
|
mappings
|
||||||
|
});
|
||||||
|
|
||||||
setCategoryMappings(mappings);
|
setCategoryMappings(mappings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("카테고리 매핑 로드 실패:", error);
|
console.error("카테고리 매핑 로드 실패:", error);
|
||||||
|
|
@ -387,6 +403,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
// 대체 URL 생성 (직접 파일 경로 사용)
|
// 대체 URL 생성 (직접 파일 경로 사용)
|
||||||
if (previewImage.path) {
|
if (previewImage.path) {
|
||||||
const altUrl = getDirectFileUrl(previewImage.path);
|
const altUrl = getDirectFileUrl(previewImage.path);
|
||||||
|
// console.log("대체 URL 시도:", altUrl);
|
||||||
setAlternativeImageUrl(altUrl);
|
setAlternativeImageUrl(altUrl);
|
||||||
} else {
|
} else {
|
||||||
toast.error("이미지를 불러올 수 없습니다.");
|
toast.error("이미지를 불러올 수 없습니다.");
|
||||||
|
|
@ -452,6 +469,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
try {
|
try {
|
||||||
return tableColumn?.detailSettings ? JSON.parse(tableColumn.detailSettings) : {};
|
return tableColumn?.detailSettings ? JSON.parse(tableColumn.detailSettings) : {};
|
||||||
} catch {
|
} catch {
|
||||||
|
// console.warn("상세 설정 파싱 실패:", tableColumn?.detailSettings);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -654,6 +672,15 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const handleRefreshFileStatus = async (event: CustomEvent) => {
|
const handleRefreshFileStatus = async (event: CustomEvent) => {
|
||||||
const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail;
|
const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail;
|
||||||
|
|
||||||
|
// console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", {
|
||||||
|
// tableName,
|
||||||
|
// recordId,
|
||||||
|
// columnName,
|
||||||
|
// targetObjid,
|
||||||
|
// fileCount,
|
||||||
|
// currentTableName: component.tableName
|
||||||
|
// });
|
||||||
|
|
||||||
// 현재 테이블과 일치하는지 확인
|
// 현재 테이블과 일치하는지 확인
|
||||||
if (tableName === component.tableName) {
|
if (tableName === component.tableName) {
|
||||||
// 해당 행의 파일 상태 업데이트
|
// 해당 행의 파일 상태 업데이트
|
||||||
|
|
@ -663,6 +690,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
[recordId]: { hasFiles: fileCount > 0, fileCount },
|
[recordId]: { hasFiles: fileCount > 0, fileCount },
|
||||||
[columnKey]: { hasFiles: fileCount > 0, fileCount },
|
[columnKey]: { hasFiles: fileCount > 0, fileCount },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// console.log("✅ 파일 상태 업데이트 완료:", {
|
||||||
|
// recordId,
|
||||||
|
// columnKey,
|
||||||
|
// hasFiles: fileCount > 0,
|
||||||
|
// fileCount
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1070,6 +1104,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
setIsAdding(true);
|
setIsAdding(true);
|
||||||
|
|
||||||
// 실제 API 호출로 데이터 추가
|
// 실제 API 호출로 데이터 추가
|
||||||
|
// console.log("🔥 추가할 데이터:", addFormData);
|
||||||
await tableTypeApi.addTableData(component.tableName, addFormData);
|
await tableTypeApi.addTableData(component.tableName, addFormData);
|
||||||
|
|
||||||
// 모달 닫기 및 폼 초기화
|
// 모달 닫기 및 폼 초기화
|
||||||
|
|
@ -1092,6 +1127,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
|
|
||||||
// 실제 API 호출로 데이터 수정
|
// 실제 API 호출로 데이터 수정
|
||||||
|
// console.log("🔥 수정할 데이터:", editFormData);
|
||||||
|
// console.log("🔥 원본 데이터:", editingRowData);
|
||||||
|
|
||||||
if (editingRowData) {
|
if (editingRowData) {
|
||||||
await tableTypeApi.editTableData(component.tableName, editingRowData, editFormData);
|
await tableTypeApi.editTableData(component.tableName, editingRowData, editFormData);
|
||||||
|
|
||||||
|
|
@ -1162,6 +1200,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const selectedData = Array.from(selectedRows).map((index) => data[index]);
|
const selectedData = Array.from(selectedRows).map((index) => data[index]);
|
||||||
|
|
||||||
// 실제 삭제 API 호출
|
// 실제 삭제 API 호출
|
||||||
|
// console.log("🗑️ 삭제할 데이터:", selectedData);
|
||||||
await tableTypeApi.deleteTableData(component.tableName, selectedData);
|
await tableTypeApi.deleteTableData(component.tableName, selectedData);
|
||||||
|
|
||||||
// 선택 해제 및 다이얼로그 닫기
|
// 선택 해제 및 다이얼로그 닫기
|
||||||
|
|
@ -1449,6 +1488,12 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
case "category": {
|
case "category": {
|
||||||
// 카테고리 셀렉트 (동적 import)
|
// 카테고리 셀렉트 (동적 import)
|
||||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||||
|
console.log("🎯 카테고리 렌더링 (편집 폼):", {
|
||||||
|
tableName: component.tableName,
|
||||||
|
columnName: column.columnName,
|
||||||
|
columnLabel: column.label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CategorySelectComponent
|
<CategorySelectComponent
|
||||||
|
|
@ -1730,6 +1775,12 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
case "category": {
|
case "category": {
|
||||||
// 카테고리 셀렉트 (동적 import)
|
// 카테고리 셀렉트 (동적 import)
|
||||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||||
|
console.log("🎯 카테고리 렌더링 (추가 폼):", {
|
||||||
|
tableName: component.tableName,
|
||||||
|
columnName: column.columnName,
|
||||||
|
columnLabel: column.label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CategorySelectComponent
|
<CategorySelectComponent
|
||||||
|
|
@ -1817,6 +1868,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const handleDeleteLinkedFile = useCallback(
|
const handleDeleteLinkedFile = useCallback(
|
||||||
async (fileId: string, fileName: string) => {
|
async (fileId: string, fileName: string) => {
|
||||||
try {
|
try {
|
||||||
|
// console.log("🗑️ 파일 삭제 시작:", { fileId, fileName });
|
||||||
|
|
||||||
// 삭제 확인 다이얼로그
|
// 삭제 확인 다이얼로그
|
||||||
if (!confirm(`"${fileName}" 파일을 삭제하시겠습니까?`)) {
|
if (!confirm(`"${fileName}" 파일을 삭제하시겠습니까?`)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1831,6 +1884,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = response.data;
|
const result = response.data;
|
||||||
|
// console.log("📡 파일 삭제 API 응답:", result);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.message || "파일 삭제 실패");
|
throw new Error(result.message || "파일 삭제 실패");
|
||||||
|
|
@ -1847,11 +1901,15 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
try {
|
try {
|
||||||
const response = await getLinkedFiles(component.tableName, recordId);
|
const response = await getLinkedFiles(component.tableName, recordId);
|
||||||
setLinkedFiles(response.files || []);
|
setLinkedFiles(response.files || []);
|
||||||
|
// console.log("📁 파일 목록 새로고침 완료:", response.files?.length || 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 파일 목록 새로고침 실패 시 무시
|
// console.error("파일 목록 새로고침 실패:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log("✅ 파일 삭제 완료:", fileName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// console.error("❌ 파일 삭제 실패:", error);
|
||||||
toast.error(`"${fileName}" 파일 삭제에 실패했습니다.`);
|
toast.error(`"${fileName}" 파일 삭제에 실패했습니다.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -619,8 +619,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
||||||
const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth));
|
const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth));
|
||||||
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
|
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 = {
|
newComp.position = {
|
||||||
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
||||||
|
|
@ -3029,8 +3028,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
||||||
const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth));
|
const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth));
|
||||||
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
|
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 = {
|
newPosition = {
|
||||||
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
||||||
|
|
|
||||||
|
|
@ -695,7 +695,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="positionX"
|
id="positionX"
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
|
||||||
value={(() => {
|
value={(() => {
|
||||||
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
|
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
|
|
@ -726,7 +725,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="positionY"
|
id="positionY"
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
|
||||||
value={(() => {
|
value={(() => {
|
||||||
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
|
const isDragging = dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id;
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
|
|
@ -764,7 +762,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={gridSettings?.columns || 12}
|
max={gridSettings?.columns || 12}
|
||||||
step="1"
|
|
||||||
value={(selectedComponent as any)?.gridColumns || 1}
|
value={(selectedComponent as any)?.gridColumns || 1}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = parseInt(e.target.value, 10);
|
const value = parseInt(e.target.value, 10);
|
||||||
|
|
@ -964,27 +961,27 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label htmlFor="height" className="text-sm font-medium">
|
<Label htmlFor="height" className="text-sm font-medium">
|
||||||
최소 높이
|
최소 높이 (10px 단위)
|
||||||
</Label>
|
</Label>
|
||||||
<div className="mt-1 flex items-center space-x-2">
|
<div className="mt-1 flex items-center space-x-2">
|
||||||
<Input
|
<Input
|
||||||
id="height"
|
id="height"
|
||||||
type="number"
|
type="number"
|
||||||
min="10"
|
min="1"
|
||||||
max="2000"
|
max="100"
|
||||||
step="1"
|
value={Math.round((localInputs.height || 10) / 10)}
|
||||||
value={localInputs.height || 40}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newHeight = Math.max(10, Number(e.target.value));
|
const units = Math.max(1, Math.min(100, Number(e.target.value)));
|
||||||
|
const newHeight = units * 10;
|
||||||
setLocalInputs((prev) => ({ ...prev, height: newHeight.toString() }));
|
setLocalInputs((prev) => ({ ...prev, height: newHeight.toString() }));
|
||||||
onUpdateProperty("size.height", newHeight);
|
onUpdateProperty("size.height", newHeight);
|
||||||
}}
|
}}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-500">{localInputs.height || 40}px</span>
|
<span className="text-sm text-gray-500">단위 = {localInputs.height || 10}px</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
높이 자유 조절 (10px ~ 2000px, 1px 단위)
|
1단위 = 10px (현재 {Math.round((localInputs.height || 10) / 10)}단위) - 내부 콘텐츠에 맞춰 늘어남
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -999,12 +996,11 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
<Label htmlFor="zIndex" className="text-sm font-medium">
|
<Label htmlFor="zIndex" className="text-sm font-medium">
|
||||||
Z-Index (레이어 순서)
|
Z-Index (레이어 순서)
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="zIndex"
|
id="zIndex"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="9999"
|
max="9999"
|
||||||
step="1"
|
|
||||||
value={localInputs.positionZ}
|
value={localInputs.positionZ}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
|
|
@ -1270,7 +1266,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="12"
|
max="12"
|
||||||
step="1"
|
|
||||||
value={(selectedComponent as AreaComponent).layoutConfig?.gridColumns || 3}
|
value={(selectedComponent as AreaComponent).layoutConfig?.gridColumns || 3}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
@ -1284,7 +1279,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
|
||||||
value={(selectedComponent as AreaComponent).layoutConfig?.gridGap || 16}
|
value={(selectedComponent as AreaComponent).layoutConfig?.gridGap || 16}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
@ -1321,7 +1315,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
|
||||||
value={(selectedComponent as AreaComponent).layoutConfig?.gap || 16}
|
value={(selectedComponent as AreaComponent).layoutConfig?.gap || 16}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
@ -1352,7 +1345,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="100"
|
min="100"
|
||||||
step="1"
|
|
||||||
value={(selectedComponent as AreaComponent).layoutConfig?.sidebarWidth || 200}
|
value={(selectedComponent as AreaComponent).layoutConfig?.sidebarWidth || 200}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,6 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
onChange={(e) => setCustomWidth(e.target.value)}
|
onChange={(e) => setCustomWidth(e.target.value)}
|
||||||
placeholder="1920"
|
placeholder="1920"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
style={{ fontSize: "12px" }}
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -159,7 +158,6 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
onChange={(e) => setCustomHeight(e.target.value)}
|
onChange={(e) => setCustomHeight(e.target.value)}
|
||||||
placeholder="1080"
|
placeholder="1080"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
style={{ fontSize: "12px" }}
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
min={50}
|
min={50}
|
||||||
max={1000}
|
max={1000}
|
||||||
step="1"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -74,7 +73,6 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
||||||
placeholder="50"
|
placeholder="50"
|
||||||
min={0}
|
min={0}
|
||||||
max={1000}
|
max={1000}
|
||||||
step="1"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -91,7 +89,6 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
||||||
placeholder="500"
|
placeholder="500"
|
||||||
min={0}
|
min={0}
|
||||||
max={2000}
|
max={2000}
|
||||||
step="1"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
const { webTypes } = useWebTypes({ active: "Y" });
|
const { webTypes } = useWebTypes({ active: "Y" });
|
||||||
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
||||||
|
|
||||||
// 높이 입력 로컬 상태 (격자 스냅 방지)
|
|
||||||
const [localHeight, setLocalHeight] = useState<string>("");
|
|
||||||
|
|
||||||
// 새로운 컴포넌트 시스템의 webType 동기화
|
// 새로운 컴포넌트 시스템의 webType 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedComponent?.type === "component") {
|
if (selectedComponent?.type === "component") {
|
||||||
|
|
@ -118,13 +115,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}
|
}
|
||||||
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
|
}, [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 이전에 정의)
|
// 격자 설정 업데이트 함수 (early return 이전에 정의)
|
||||||
const updateGridSetting = (key: string, value: any) => {
|
const updateGridSetting = (key: string, value: any) => {
|
||||||
if (onGridSettingsChange && gridSettings) {
|
if (onGridSettingsChange && gridSettings) {
|
||||||
|
|
@ -190,7 +180,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
id="columns"
|
id="columns"
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
step="1"
|
|
||||||
value={gridSettings.columns}
|
value={gridSettings.columns}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = parseInt(e.target.value, 10);
|
const value = parseInt(e.target.value, 10);
|
||||||
|
|
@ -372,27 +361,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<Label className="text-xs">높이</Label>
|
<Label className="text-xs">높이</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={localHeight}
|
value={selectedComponent.size?.height || 0}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
// 입력 중에는 로컬 상태만 업데이트 (격자 스냅 방지)
|
|
||||||
setLocalHeight(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// 포커스를 잃을 때만 실제로 업데이트
|
|
||||||
const value = parseInt(e.target.value) || 0;
|
const value = parseInt(e.target.value) || 0;
|
||||||
if (value >= 1) {
|
// 최소값 제한 없이, 1px 단위로 조절 가능
|
||||||
handleUpdate("size.height", value);
|
handleUpdate("size.height", Math.max(1, 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}
|
step={1}
|
||||||
placeholder="10"
|
placeholder="10"
|
||||||
|
|
@ -457,7 +430,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={gridSettings?.columns || 12}
|
max={gridSettings?.columns || 12}
|
||||||
step="1"
|
|
||||||
value={(selectedComponent as any).gridColumns || 1}
|
value={(selectedComponent as any).gridColumns || 1}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = parseInt(e.target.value, 10);
|
const value = parseInt(e.target.value, 10);
|
||||||
|
|
@ -483,7 +455,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<Label className="text-xs">Z-Index</Label>
|
<Label className="text-xs">Z-Index</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
|
||||||
value={currentPosition.z || 1}
|
value={currentPosition.z || 1}
|
||||||
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,6 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
||||||
<Input
|
<Input
|
||||||
id="min"
|
id="min"
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
|
||||||
value={localValues.min}
|
value={localValues.min}
|
||||||
onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
|
@ -147,7 +146,6 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
||||||
<Input
|
<Input
|
||||||
id="max"
|
id="max"
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
|
||||||
value={localValues.max}
|
value={localValues.max}
|
||||||
onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
|
@ -183,7 +181,6 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="10"
|
max="10"
|
||||||
step="1"
|
|
||||||
value={localValues.decimalPlaces}
|
value={localValues.decimalPlaces}
|
||||||
onChange={(e) => updateConfig("decimalPlaces", e.target.value ? Number(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("decimalPlaces", e.target.value ? Number(e.target.value) : undefined)}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,6 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
id="minLength"
|
id="minLength"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
|
||||||
value={localValues.minLength}
|
value={localValues.minLength}
|
||||||
onChange={(e) => updateConfig("minLength", e.target.value ? Number(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("minLength", e.target.value ? Number(e.target.value) : undefined)}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
|
@ -184,7 +183,6 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
id="maxLength"
|
id="maxLength"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
|
||||||
value={localValues.maxLength}
|
value={localValues.maxLength}
|
||||||
onChange={(e) => updateConfig("maxLength", e.target.value ? Number(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("maxLength", e.target.value ? Number(e.target.value) : undefined)}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
|
|
||||||
|
|
@ -75,4 +75,3 @@ export const numberingRuleTemplate = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useRef, useCallback } from "react";
|
import React, { useState } from "react";
|
||||||
import { CategoryColumnList } from "@/components/table-category/CategoryColumnList";
|
import { CategoryColumnList } from "@/components/table-category/CategoryColumnList";
|
||||||
import { CategoryValueManager } from "@/components/table-category/CategoryValueManager";
|
import { CategoryValueManager } from "@/components/table-category/CategoryValueManager";
|
||||||
import { GripVertical } from "lucide-react";
|
|
||||||
|
|
||||||
interface CategoryWidgetProps {
|
interface CategoryWidgetProps {
|
||||||
widgetId: string;
|
widgetId: string;
|
||||||
|
|
@ -21,48 +20,10 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [leftWidth, setLeftWidth] = useState(15); // 초기값 15%
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const isDraggingRef = useRef(false);
|
|
||||||
|
|
||||||
const handleMouseDown = useCallback(() => {
|
|
||||||
isDraggingRef.current = true;
|
|
||||||
document.body.style.cursor = "col-resize";
|
|
||||||
document.body.style.userSelect = "none";
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
||||||
if (!isDraggingRef.current || !containerRef.current) return;
|
|
||||||
|
|
||||||
const containerRect = containerRef.current.getBoundingClientRect();
|
|
||||||
const newLeftWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
||||||
|
|
||||||
// 최소 10%, 최대 40%로 제한
|
|
||||||
if (newLeftWidth >= 10 && newLeftWidth <= 40) {
|
|
||||||
setLeftWidth(newLeftWidth);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseUp = useCallback(() => {
|
|
||||||
isDraggingRef.current = false;
|
|
||||||
document.body.style.cursor = "";
|
|
||||||
document.body.style.userSelect = "";
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
};
|
|
||||||
}, [handleMouseMove, handleMouseUp]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="flex h-full min-h-[10px] gap-0">
|
<div className="flex h-full min-h-[10px] gap-6">
|
||||||
{/* 좌측: 카테고리 컬럼 리스트 */}
|
{/* 좌측: 카테고리 컬럼 리스트 (30%) */}
|
||||||
<div style={{ width: `${leftWidth}%` }} className="pr-3">
|
<div className="w-[30%] border-r pr-6">
|
||||||
<CategoryColumnList
|
<CategoryColumnList
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
selectedColumn={selectedColumn?.columnName || null}
|
selectedColumn={selectedColumn?.columnName || null}
|
||||||
|
|
@ -72,16 +33,8 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 리사이저 */}
|
{/* 우측: 카테고리 값 관리 (70%) */}
|
||||||
<div
|
<div className="w-[70%]">
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
className="group relative flex w-3 cursor-col-resize items-center justify-center border-r hover:bg-accent/50 transition-colors"
|
|
||||||
>
|
|
||||||
<GripVertical className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 우측: 카테고리 값 관리 */}
|
|
||||||
<div style={{ width: `${100 - leftWidth - 1}%` }} className="pl-3">
|
|
||||||
{selectedColumn ? (
|
{selectedColumn ? (
|
||||||
<CategoryValueManager
|
<CategoryValueManager
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
|
|
|
||||||
|
|
@ -66,35 +66,6 @@ export function FlowWidget({
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
const { user } = useAuth(); // 사용자 정보 가져오기
|
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 => {
|
const formatValue = useCallback((value: any, columnName?: string): string => {
|
||||||
if (value === null || value === undefined || value === "") {
|
if (value === null || value === undefined || value === "") {
|
||||||
|
|
@ -126,6 +97,35 @@ export function FlowWidget({
|
||||||
return String(value);
|
return String(value);
|
||||||
}, [categoryMappings]);
|
}, [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 [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); // 그룹 설정 다이얼로그
|
||||||
const [groupByColumns, setGroupByColumns] = useState<string[]>([]); // 그룹화할 컬럼 목록
|
const [groupByColumns, setGroupByColumns] = useState<string[]>([]); // 그룹화할 컬럼 목록
|
||||||
|
|
@ -382,6 +382,12 @@ export function FlowWidget({
|
||||||
});
|
});
|
||||||
|
|
||||||
setFilteredData(filtered);
|
setFilteredData(filtered);
|
||||||
|
console.log("🔍 검색 실행:", {
|
||||||
|
totalRows: stepData.length,
|
||||||
|
filteredRows: filtered.length,
|
||||||
|
searchValues,
|
||||||
|
hasSearchValue,
|
||||||
|
});
|
||||||
}, [searchValues, stepData]); // stepData와 searchValues가 변경될 때마다 실행
|
}, [searchValues, stepData]); // stepData와 searchValues가 변경될 때마다 실행
|
||||||
|
|
||||||
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
||||||
|
|
@ -465,6 +471,7 @@ export function FlowWidget({
|
||||||
|
|
||||||
// 프리뷰 모드에서는 샘플 데이터만 표시
|
// 프리뷰 모드에서는 샘플 데이터만 표시
|
||||||
if (isPreviewMode) {
|
if (isPreviewMode) {
|
||||||
|
console.log("🔒 프리뷰 모드: 플로우 데이터 로드 차단 - 샘플 데이터 표시");
|
||||||
setFlowData({
|
setFlowData({
|
||||||
id: flowId || 0,
|
id: flowId || 0,
|
||||||
flowName: flowName || "샘플 플로우",
|
flowName: flowName || "샘플 플로우",
|
||||||
|
|
@ -641,9 +648,16 @@ export function FlowWidget({
|
||||||
try {
|
try {
|
||||||
// 컬럼 라벨 조회
|
// 컬럼 라벨 조회
|
||||||
const labelsResponse = await getStepColumnLabels(flowId!, stepId);
|
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) {
|
if (labelsResponse.success && labelsResponse.data) {
|
||||||
setColumnLabels(labelsResponse.data);
|
setColumnLabels(labelsResponse.data);
|
||||||
} else {
|
} else {
|
||||||
|
console.warn("⚠️ 컬럼 라벨 조회 실패 또는 데이터 없음:", labelsResponse);
|
||||||
setColumnLabels({});
|
setColumnLabels({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -747,6 +761,13 @@ export function FlowWidget({
|
||||||
|
|
||||||
// 선택된 데이터를 상위로 전달
|
// 선택된 데이터를 상위로 전달
|
||||||
const selectedData = Array.from(newSelected).map((index) => stepData[index]);
|
const selectedData = Array.from(newSelected).map((index) => stepData[index]);
|
||||||
|
console.log("🌊 FlowWidget - 체크박스 토글, 상위로 전달:", {
|
||||||
|
rowIndex,
|
||||||
|
newSelectedSize: newSelected.size,
|
||||||
|
selectedData,
|
||||||
|
selectedStepId,
|
||||||
|
hasCallback: !!onSelectedDataChange,
|
||||||
|
});
|
||||||
onSelectedDataChange?.(selectedData, selectedStepId);
|
onSelectedDataChange?.(selectedData, selectedStepId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,12 @@
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
|
||||||
import { FolderTree, Loader2 } from "lucide-react";
|
import { FolderTree, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
interface CategoryColumn {
|
interface CategoryColumn {
|
||||||
columnName: string;
|
columnName: string;
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
inputType: string;
|
inputType: string;
|
||||||
valueCount?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryColumnListProps {
|
interface CategoryColumnListProps {
|
||||||
|
|
@ -81,37 +79,20 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const columnsWithCount = await Promise.all(
|
setColumns(
|
||||||
categoryColumns.map(async (col: any) => {
|
categoryColumns.map((col: any) => ({
|
||||||
const colName = col.columnName || col.column_name;
|
columnName: col.columnName || col.column_name,
|
||||||
const colLabel = col.columnLabel || col.column_label || col.displayName || colName;
|
columnLabel: col.columnLabel || col.column_label || col.displayName || col.columnName || col.column_name,
|
||||||
|
inputType: col.inputType || col.input_type,
|
||||||
// 각 컬럼의 값 개수 가져오기
|
})),
|
||||||
let valueCount = 0;
|
|
||||||
try {
|
|
||||||
const valuesResult = await getCategoryValues(tableName, colName, false);
|
|
||||||
if (valuesResult.success && valuesResult.data) {
|
|
||||||
valueCount = valuesResult.data.length;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`항목 개수 조회 실패 (${colName}):`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
columnName: colName,
|
|
||||||
columnLabel: colLabel,
|
|
||||||
inputType: col.inputType || col.input_type,
|
|
||||||
valueCount,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
setColumns(columnsWithCount);
|
|
||||||
|
|
||||||
// 첫 번째 컬럼 자동 선택
|
// 첫 번째 컬럼 자동 선택
|
||||||
if (columnsWithCount.length > 0 && !selectedColumn) {
|
if (categoryColumns.length > 0 && !selectedColumn) {
|
||||||
const firstCol = columnsWithCount[0];
|
const firstCol = categoryColumns[0];
|
||||||
onColumnSelect(firstCol.columnName, firstCol.columnLabel);
|
const colName = firstCol.columnName || firstCol.column_name;
|
||||||
|
const colLabel = firstCol.columnLabel || firstCol.column_label || firstCol.displayName || colName;
|
||||||
|
onColumnSelect(colName, colLabel);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 카테고리 컬럼 조회 실패:", error);
|
console.error("❌ 카테고리 컬럼 조회 실패:", error);
|
||||||
|
|
@ -156,7 +137,7 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
|
||||||
<div
|
<div
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
onClick={() => onColumnSelect(column.columnName, column.columnLabel || column.columnName)}
|
onClick={() => onColumnSelect(column.columnName, column.columnLabel || column.columnName)}
|
||||||
className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${
|
className={`cursor-pointer rounded-lg border p-4 transition-all ${
|
||||||
selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -166,10 +147,8 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
|
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
|
||||||
|
<p className="text-muted-foreground text-xs">{column.columnName}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground text-xs font-medium">
|
|
||||||
{column.valueCount !== undefined ? `${column.valueCount}개` : "..."}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
const loadCategoryValues = async () => {
|
const loadCategoryValues = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// includeInactive: true로 비활성 값도 포함
|
const response = await getCategoryValues(tableName, columnName);
|
||||||
const response = await getCategoryValues(tableName, columnName, true);
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setValues(response.data);
|
setValues(response.data);
|
||||||
setFilteredValues(response.data);
|
setFilteredValues(response.data);
|
||||||
|
|
|
||||||
|
|
@ -133,4 +133,3 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,17 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
|
|
||||||
const buttonColor = getLabelColor();
|
const buttonColor = getLabelColor();
|
||||||
|
|
||||||
|
// 그라데이션용 어두운 색상 계산
|
||||||
|
const getDarkColor = (baseColor: string) => {
|
||||||
|
const hex = baseColor.replace("#", "");
|
||||||
|
const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40);
|
||||||
|
const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40);
|
||||||
|
const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40);
|
||||||
|
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonDarkColor = getDarkColor(buttonColor);
|
||||||
|
|
||||||
// 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
|
// 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
|
||||||
const processedConfig = { ...componentConfig };
|
const processedConfig = { ...componentConfig };
|
||||||
if (componentConfig.action && typeof componentConfig.action === "string") {
|
if (componentConfig.action && typeof componentConfig.action === "string") {
|
||||||
|
|
@ -534,14 +545,16 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
<button
|
<button
|
||||||
type={componentConfig.actionType || "button"}
|
type={componentConfig.actionType || "button"}
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
className="transition-colors duration-150 hover:opacity-90 active:scale-95 transition-transform"
|
className="transition-all duration-200"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: "40px",
|
minHeight: "40px",
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
background: componentConfig.disabled ? "#e5e7eb" : buttonColor,
|
background: componentConfig.disabled
|
||||||
|
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
|
||||||
|
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
|
||||||
color: componentConfig.disabled ? "#9ca3af" : "white",
|
color: componentConfig.disabled ? "#9ca3af" : "white",
|
||||||
// 🔧 크기 설정 적용 (sm/md/lg)
|
// 🔧 크기 설정 적용 (sm/md/lg)
|
||||||
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
||||||
|
|
@ -557,7 +570,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
|
componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
|
||||||
margin: "0",
|
margin: "0",
|
||||||
lineHeight: "1.25",
|
lineHeight: "1.25",
|
||||||
boxShadow: componentConfig.disabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 1px 3px 0 ${buttonColor}40`,
|
||||||
// isInteractive 모드에서는 사용자 스타일 우선 적용 (width/height 제외)
|
// isInteractive 모드에서는 사용자 스타일 우선 적용 (width/height 제외)
|
||||||
...(isInteractive && component.style ? Object.fromEntries(
|
...(isInteractive && component.style ? Object.fromEntries(
|
||||||
Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height')
|
Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height')
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,11 @@ export const CardModeRenderer: React.FC<CardModeRendererProps> = ({
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mb-4">
|
<div className="w-16 h-16 bg-gradient-to-br from-slate-100 to-slate-200 rounded-2xl flex items-center justify-center mb-4">
|
||||||
<div className="w-8 h-8 bg-muted-foreground/20 rounded-lg"></div>
|
<div className="w-8 h-8 bg-slate-300 rounded-lg"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-1">표시할 데이터가 없습니다</div>
|
<div className="text-sm font-medium text-slate-600 mb-1">표시할 데이터가 없습니다</div>
|
||||||
<div className="text-xs text-muted-foreground/60">조건을 변경하거나 새로운 데이터를 추가해보세요</div>
|
<div className="text-xs text-slate-400">조건을 변경하거나 새로운 데이터를 추가해보세요</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// 객체인 경우 tableName 속성 추출 시도
|
// 객체인 경우 tableName 속성 추출 시도
|
||||||
if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
|
if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
|
||||||
|
console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable);
|
||||||
finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName;
|
finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName;
|
||||||
|
console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
tableConfig.selectedTable = finalSelectedTable;
|
tableConfig.selectedTable = finalSelectedTable;
|
||||||
|
|
@ -280,10 +282,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
if (savedOrder) {
|
if (savedOrder) {
|
||||||
try {
|
try {
|
||||||
const parsedOrder = JSON.parse(savedOrder);
|
const parsedOrder = JSON.parse(savedOrder);
|
||||||
|
console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder });
|
||||||
setColumnOrder(parsedOrder);
|
setColumnOrder(parsedOrder);
|
||||||
|
|
||||||
// 부모 컴포넌트에 초기 컬럼 순서 전달
|
// 부모 컴포넌트에 초기 컬럼 순서 전달
|
||||||
if (onSelectedRowsChange && parsedOrder.length > 0) {
|
if (onSelectedRowsChange && parsedOrder.length > 0) {
|
||||||
|
console.log("✅ 초기 컬럼 순서 전달:", parsedOrder);
|
||||||
|
|
||||||
// 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬)
|
// 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬)
|
||||||
const initialData = data.map((row: any) => {
|
const initialData = data.map((row: any) => {
|
||||||
const reordered: any = {};
|
const reordered: any = {};
|
||||||
|
|
@ -301,6 +306,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return reordered;
|
return reordered;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("📊 초기 화면 표시 데이터 전달:", { count: initialData.length, firstRow: initialData[0] });
|
||||||
|
|
||||||
// 전역 저장소에 데이터 저장
|
// 전역 저장소에 데이터 저장
|
||||||
if (tableConfig.selectedTable) {
|
if (tableConfig.selectedTable) {
|
||||||
tableDisplayStore.setTableData(
|
tableDisplayStore.setTableData(
|
||||||
|
|
@ -577,6 +584,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = (column: string) => {
|
const handleSort = (column: string) => {
|
||||||
|
console.log("🔄 정렬 클릭:", { column, currentSortColumn: sortColumn, currentSortDirection: sortDirection });
|
||||||
|
|
||||||
let newSortColumn = column;
|
let newSortColumn = column;
|
||||||
let newSortDirection: "asc" | "desc" = "asc";
|
let newSortDirection: "asc" | "desc" = "asc";
|
||||||
|
|
||||||
|
|
@ -590,6 +599,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
newSortDirection = "asc";
|
newSortDirection = "asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
|
||||||
|
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
|
||||||
|
|
||||||
// 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달
|
// 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달
|
||||||
if (onSelectedRowsChange) {
|
if (onSelectedRowsChange) {
|
||||||
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
|
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
|
||||||
|
|
@ -639,6 +651,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return reordered;
|
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(
|
onSelectedRowsChange(
|
||||||
Array.from(selectedRows),
|
Array.from(selectedRows),
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
|
|
@ -659,6 +681,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
newSortDirection
|
newSortDirection
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -750,6 +774,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const isCurrentlySelected = selectedRows.has(rowKey);
|
const isCurrentlySelected = selectedRows.has(rowKey);
|
||||||
|
|
||||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
handleRowSelection(rowKey, !isCurrentlySelected);
|
||||||
|
|
||||||
|
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 컬럼 드래그앤드롭 기능 제거됨 (테이블 옵션 모달에서 컬럼 순서 변경 가능)
|
// 컬럼 드래그앤드롭 기능 제거됨 (테이블 옵션 모달에서 컬럼 순서 변경 가능)
|
||||||
|
|
@ -794,6 +820,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// columnOrder에 없는 새로운 컬럼들 추가
|
// columnOrder에 없는 새로운 컬럼들 추가
|
||||||
const remainingCols = cols.filter(c => !columnOrder.includes(c.columnName));
|
const remainingCols = cols.filter(c => !columnOrder.includes(c.columnName));
|
||||||
|
|
||||||
|
console.log("🔄 columnOrder 기반 정렬:", {
|
||||||
|
columnOrder,
|
||||||
|
orderedColsCount: orderedCols.length,
|
||||||
|
remainingColsCount: remainingCols.length
|
||||||
|
});
|
||||||
|
|
||||||
return [...orderedCols, ...remainingCols];
|
return [...orderedCols, ...remainingCols];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -804,11 +836,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const lastColumnOrderRef = useRef<string>("");
|
const lastColumnOrderRef = useRef<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", {
|
||||||
|
hasCallback: !!onSelectedRowsChange,
|
||||||
|
visibleColumnsLength: visibleColumns.length,
|
||||||
|
visibleColumnsNames: visibleColumns.map(c => c.columnName),
|
||||||
|
});
|
||||||
|
|
||||||
if (!onSelectedRowsChange) {
|
if (!onSelectedRowsChange) {
|
||||||
|
console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibleColumns.length === 0) {
|
if (visibleColumns.length === 0) {
|
||||||
|
console.warn("⚠️ visibleColumns가 비어있습니다!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -816,14 +856,23 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
.map(col => col.columnName)
|
.map(col => col.columnName)
|
||||||
.filter(name => name !== "__checkbox__"); // 체크박스 컬럼 제외
|
.filter(name => name !== "__checkbox__"); // 체크박스 컬럼 제외
|
||||||
|
|
||||||
|
console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder);
|
||||||
|
|
||||||
// 컬럼 순서가 실제로 변경되었을 때만 전달 (무한 루프 방지)
|
// 컬럼 순서가 실제로 변경되었을 때만 전달 (무한 루프 방지)
|
||||||
const columnOrderString = currentColumnOrder.join(",");
|
const columnOrderString = currentColumnOrder.join(",");
|
||||||
|
console.log("🔍 [컬럼 순서] 비교:", {
|
||||||
|
current: columnOrderString,
|
||||||
|
last: lastColumnOrderRef.current,
|
||||||
|
isDifferent: columnOrderString !== lastColumnOrderRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
if (columnOrderString === lastColumnOrderRef.current) {
|
if (columnOrderString === lastColumnOrderRef.current) {
|
||||||
|
console.log("⏭️ 컬럼 순서 변경 없음, 전달 스킵");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastColumnOrderRef.current = columnOrderString;
|
lastColumnOrderRef.current = columnOrderString;
|
||||||
|
console.log("📊 현재 화면 컬럼 순서 전달:", currentColumnOrder);
|
||||||
|
|
||||||
// 선택된 행 데이터 가져오기
|
// 선택된 행 데이터 가져오기
|
||||||
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
|
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
|
||||||
|
|
@ -1579,7 +1628,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
>
|
>
|
||||||
{/* 스크롤 영역 */}
|
{/* 스크롤 영역 */}
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-full flex-1 overflow-y-auto overflow-x-auto bg-background"
|
className="w-full max-w-full h-[400px] overflow-y-scroll overflow-x-auto bg-background sm:h-[500px]"
|
||||||
>
|
>
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<table
|
<table
|
||||||
|
|
@ -1597,7 +1646,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<thead
|
<thead
|
||||||
className="sticky top-0 z-10"
|
className="sticky top-0 z-10"
|
||||||
>
|
>
|
||||||
<tr className="h-10 border-b-2 border-primary/20 bg-muted sm:h-12">
|
<tr className="h-10 border-b-2 border-primary/20 bg-gradient-to-b from-muted/50 to-muted sm:h-12">
|
||||||
{visibleColumns.map((column, columnIndex) => {
|
{visibleColumns.map((column, columnIndex) => {
|
||||||
const columnWidth = columnWidths[column.columnName];
|
const columnWidth = columnWidths[column.columnName];
|
||||||
const isFrozen = frozenColumns.includes(column.columnName);
|
const isFrozen = frozenColumns.includes(column.columnName);
|
||||||
|
|
@ -1755,11 +1804,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={visibleColumns.length}
|
colSpan={visibleColumns.length}
|
||||||
className="bg-muted border-b border-border sticky top-[48px] z-[5]"
|
className="bg-muted/50 border-b border-border sticky top-[48px] z-[5]"
|
||||||
style={{ top: "48px" }}
|
style={{ top: "48px" }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/80"
|
className="flex items-center gap-3 p-3 cursor-pointer hover:bg-muted"
|
||||||
onClick={() => toggleGroupCollapse(group.groupKey)}
|
onClick={() => toggleGroupCollapse(group.groupKey)}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
|
|
|
||||||
|
|
@ -375,4 +375,3 @@ interface TablePermission {
|
||||||
|
|
||||||
**이제 테이블을 만들 때마다 코드를 수정할 필요가 없습니다!**
|
**이제 테이블을 만들 때마다 코드를 수정할 필요가 없습니다!**
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue