From 1d87b6c3ac1f923314adbec0ca67ef43e095027a Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:09:28 +0900
Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?=
=?UTF-8?q?=EB=A6=AC=20=EA=B0=92=EC=97=90=20=EB=B0=B0=EC=A7=80=20=EC=83=89?=
=?UTF-8?q?=EC=83=81=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?=
=?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 카테고리 값 추가/편집 다이얼로그에 색상 선택기 추가
- 18가지 기본 색상 팔레트 제공
- 선택한 색상의 실시간 배지 미리보기
- color 필드를 통해 DB에 저장
- 테이블 리스트에서 배지 형태로 표시할 준비 완료
---
.../table-category/CategoryValueAddDialog.tsx | 93 +++++++++++++++----
.../CategoryValueEditDialog.tsx | 92 ++++++++++++++----
2 files changed, 152 insertions(+), 33 deletions(-)
diff --git a/frontend/components/table-category/CategoryValueAddDialog.tsx b/frontend/components/table-category/CategoryValueAddDialog.tsx
index 99aa02b1..c9a2b222 100644
--- a/frontend/components/table-category/CategoryValueAddDialog.tsx
+++ b/frontend/components/table-category/CategoryValueAddDialog.tsx
@@ -11,9 +11,33 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
import { TableCategoryValue } from "@/types/tableCategoryValue";
+// 기본 색상 팔레트
+const DEFAULT_COLORS = [
+ "#ef4444", // red
+ "#f97316", // orange
+ "#f59e0b", // amber
+ "#eab308", // yellow
+ "#84cc16", // lime
+ "#22c55e", // green
+ "#10b981", // emerald
+ "#14b8a6", // teal
+ "#06b6d4", // cyan
+ "#0ea5e9", // sky
+ "#3b82f6", // blue
+ "#6366f1", // indigo
+ "#8b5cf6", // violet
+ "#a855f7", // purple
+ "#d946ef", // fuchsia
+ "#ec4899", // pink
+ "#64748b", // slate
+ "#6b7280", // gray
+];
+
interface CategoryValueAddDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -26,6 +50,7 @@ export const CategoryValueAddDialog: React.FC<
> = ({ open, onOpenChange, onAdd, columnLabel }) => {
const [valueLabel, setValueLabel] = useState("");
const [description, setDescription] = useState("");
+ const [color, setColor] = useState("#3b82f6");
// 라벨에서 코드 자동 생성
const generateCode = (label: string): string => {
@@ -59,13 +84,14 @@ export const CategoryValueAddDialog: React.FC<
valueCode,
valueLabel: valueLabel.trim(),
description: description.trim(),
- color: "#3b82f6",
+ color: color,
isDefault: false,
});
// 초기화
setValueLabel("");
setDescription("");
+ setColor("#3b82f6");
};
return (
@@ -81,23 +107,56 @@ export const CategoryValueAddDialog: React.FC<
diff --git a/frontend/components/table-category/CategoryValueEditDialog.tsx b/frontend/components/table-category/CategoryValueEditDialog.tsx
index e06c17f2..6c7c6060 100644
--- a/frontend/components/table-category/CategoryValueEditDialog.tsx
+++ b/frontend/components/table-category/CategoryValueEditDialog.tsx
@@ -11,7 +11,9 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
import { TableCategoryValue } from "@/types/tableCategoryValue";
interface CategoryValueEditDialogProps {
@@ -22,15 +24,39 @@ interface CategoryValueEditDialogProps {
columnLabel: string;
}
+// 기본 색상 팔레트
+const DEFAULT_COLORS = [
+ "#ef4444", // red
+ "#f97316", // orange
+ "#f59e0b", // amber
+ "#eab308", // yellow
+ "#84cc16", // lime
+ "#22c55e", // green
+ "#10b981", // emerald
+ "#14b8a6", // teal
+ "#06b6d4", // cyan
+ "#0ea5e9", // sky
+ "#3b82f6", // blue
+ "#6366f1", // indigo
+ "#8b5cf6", // violet
+ "#a855f7", // purple
+ "#d946ef", // fuchsia
+ "#ec4899", // pink
+ "#64748b", // slate
+ "#6b7280", // gray
+];
+
export const CategoryValueEditDialog: React.FC<
CategoryValueEditDialogProps
> = ({ open, onOpenChange, value, onUpdate, columnLabel }) => {
const [valueLabel, setValueLabel] = useState(value.valueLabel);
const [description, setDescription] = useState(value.description || "");
+ const [color, setColor] = useState(value.color || "#3b82f6");
useEffect(() => {
setValueLabel(value.valueLabel);
setDescription(value.description || "");
+ setColor(value.color || "#3b82f6");
}, [value]);
const handleSubmit = () => {
@@ -41,6 +67,7 @@ export const CategoryValueEditDialog: React.FC<
onUpdate(value.valueId!, {
valueLabel: valueLabel.trim(),
description: description.trim(),
+ color: color,
});
};
@@ -57,23 +84,56 @@ export const CategoryValueEditDialog: React.FC<
-
setValueLabel(e.target.value)}
- className="h-8 text-xs sm:h-10 sm:text-sm"
- autoFocus
- />
+
+
+ setValueLabel(e.target.value)}
+ className="h-8 text-xs sm:h-10 sm:text-sm mt-1.5"
+ autoFocus
+ />
+
-
From 7581cd1582a41b535345d838bafdb84f6eb9bb27 Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:12:19 +0900
Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?=
=?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=B9=B4?=
=?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B0=92=EC=9D=84=20=EB=B0=B0?=
=?UTF-8?q?=EC=A7=80=EB=A1=9C=20=ED=91=9C=EC=8B=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 카테고리 타입 컬럼을 배지 형태로 렌더링
- 사용자가 설정한 색상 적용
- categoryMappings에 라벨과 색상 모두 저장
- 기본 색상: #3b82f6 (파란색)
- 텍스트 색상: 흰색으로 고정하여 가독성 확보
---
.../screen/InteractiveDataTable.tsx | 33 ++++++++++++++-----
1 file changed, 24 insertions(+), 9 deletions(-)
diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx
index 3165bfa8..7a150e57 100644
--- a/frontend/components/screen/InteractiveDataTable.tsx
+++ b/frontend/components/screen/InteractiveDataTable.tsx
@@ -144,8 +144,8 @@ export const InteractiveDataTable: React.FC = ({
const [filteredData, setFilteredData] = useState([]); // 필터링된 데이터
const [columnLabels, setColumnLabels] = useState>({}); // 컬럼명 -> 라벨 매핑
- // 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> 라벨})
- const [categoryMappings, setCategoryMappings] = useState>>({});
+ // 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> {라벨, 색상}})
+ const [categoryMappings, setCategoryMappings] = useState>>({});
// 공통코드 옵션 가져오기
const loadCodeOptions = useCallback(
@@ -208,7 +208,7 @@ export const InteractiveDataTable: React.FC = ({
if (!categoryColumns || categoryColumns.length === 0) return;
// 각 카테고리 컬럼의 값 목록 조회
- const mappings: Record> = {};
+ const mappings: Record> = {};
for (const col of categoryColumns) {
try {
@@ -217,10 +217,13 @@ export const InteractiveDataTable: React.FC = ({
);
if (response.data.success && response.data.data) {
- // valueCode -> valueLabel 매핑 생성
- const mapping: Record = {};
+ // valueCode -> {label, color} 매핑 생성
+ const mapping: Record = {};
response.data.data.forEach((item: any) => {
- mapping[item.valueCode] = item.valueLabel;
+ mapping[item.valueCode] = {
+ label: item.valueLabel,
+ color: item.color,
+ };
});
mappings[col.columnName] = mapping;
}
@@ -1911,11 +1914,23 @@ export const InteractiveDataTable: React.FC = ({
// 실제 웹 타입으로 스위치 (input_type="category"도 포함됨)
switch (actualWebType) {
case "category": {
- // 카테고리 타입: 코드값 -> 라벨로 변환
+ // 카테고리 타입: 배지로 표시
const mapping = categoryMappings[column.columnName];
if (mapping && value) {
- const label = mapping[String(value)];
- return label || String(value);
+ const categoryData = mapping[String(value)];
+ if (categoryData) {
+ return (
+
+ {categoryData.label}
+
+ );
+ }
}
return String(value || "");
}
From b526d8ea2c9e3769b506c16ad197750fd2e8556d Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:15:47 +0900
Subject: [PATCH 03/16] =?UTF-8?q?fix:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?=
=?UTF-8?q?=EB=A6=AC=20=EB=B0=B0=EC=A7=80=20=ED=91=9C=EC=8B=9C=20=EA=B0=9C?=
=?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EB=94=94=EB=B2=84=EA=B9=85=20=EB=A1=9C?=
=?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 매핑이 없어도 항상 배지로 표시
- 매핑 없을 시 코드값 그대로 + 기본 slate 색상 사용
- 카테고리 매핑 로드 과정 로그 추가
- 기존 데이터에 기본 색상 추가하는 마이그레이션 스크립트 생성
---
.../screen/InteractiveDataTable.tsx | 40 ++++++++++---------
1 file changed, 22 insertions(+), 18 deletions(-)
diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx
index 7a150e57..b0b8dc59 100644
--- a/frontend/components/screen/InteractiveDataTable.tsx
+++ b/frontend/components/screen/InteractiveDataTable.tsx
@@ -226,12 +226,14 @@ export const InteractiveDataTable: React.FC = ({
};
});
mappings[col.columnName] = mapping;
+ console.log(`✅ 카테고리 매핑 로드 성공 [${col.columnName}]:`, mapping);
}
} catch (error) {
- // 카테고리 값 로드 실패 시 무시
+ console.error(`❌ 카테고리 값 로드 실패 [${col.columnName}]:`, error);
}
}
+ console.log("📊 전체 카테고리 매핑:", mappings);
setCategoryMappings(mappings);
} catch (error) {
console.error("카테고리 매핑 로드 실패:", error);
@@ -1915,24 +1917,26 @@ export const InteractiveDataTable: React.FC = ({
switch (actualWebType) {
case "category": {
// 카테고리 타입: 배지로 표시
+ if (!value) return "";
+
const mapping = categoryMappings[column.columnName];
- if (mapping && value) {
- const categoryData = mapping[String(value)];
- if (categoryData) {
- return (
-
- {categoryData.label}
-
- );
- }
- }
- return String(value || "");
+ const categoryData = mapping?.[String(value)];
+
+ // 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
+ const displayLabel = categoryData?.label || String(value);
+ const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
+
+ return (
+
+ {displayLabel}
+
+ );
}
case "date":
From 939a8696c89e40ec8f02951adae568f42658050b Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:18:43 +0900
Subject: [PATCH 04/16] =?UTF-8?q?feat:=20TableListComponent=EC=97=90?=
=?UTF-8?q?=EC=84=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B0=92?=
=?UTF-8?q?=EC=9D=84=20=EB=B0=B0=EC=A7=80=EB=A1=9C=20=ED=91=9C=EC=8B=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- categoryMappings 타입을 색상 포함하도록 수정
- 카테고리 값 로드 시 color 필드 포함
- formatValue에서 카테고리를 Badge 컴포넌트로 렌더링
- 매핑 없을 시에도 기본 slate 색상의 배지로 표시
- 디버깅 로그 추가
---
.../table-list/TableListComponent.tsx | 44 +++++++++++++------
1 file changed, 31 insertions(+), 13 deletions(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index f88ebba1..98654471 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -243,7 +243,7 @@ export const TableListComponent: React.FC = ({
const [displayColumns, setDisplayColumns] = useState([]);
const [joinColumnMapping, setJoinColumnMapping] = useState>({});
const [columnMeta, setColumnMeta] = useState>({});
- const [categoryMappings, setCategoryMappings] = useState>>({});
+ const [categoryMappings, setCategoryMappings] = useState>>({});
const [searchValues, setSearchValues] = useState>({});
const [selectedRows, setSelectedRows] = useState>(new Set());
const [columnWidths, setColumnWidths] = useState>({});
@@ -437,7 +437,7 @@ export const TableListComponent: React.FC = ({
if (categoryColumns.length === 0) return;
- const mappings: Record> = {};
+ const mappings: Record> = {};
for (const columnName of categoryColumns) {
try {
@@ -447,17 +447,22 @@ export const TableListComponent: React.FC = ({
);
if (response.data.success && response.data.data) {
- const mapping: Record = {};
+ const mapping: Record = {};
response.data.data.forEach((item: any) => {
- mapping[item.valueCode] = item.valueLabel;
+ mapping[item.valueCode] = {
+ label: item.valueLabel,
+ color: item.color,
+ };
});
mappings[columnName] = mapping;
+ console.log(`✅ [TableList] 카테고리 매핑 로드 [${columnName}]:`, mapping);
}
} catch (error) {
- // 카테고리 값 로드 실패 시 무시
+ console.error(`❌ [TableList] 카테고리 값 로드 실패 [${columnName}]:`, error);
}
}
+ console.log("📊 [TableList] 전체 카테고리 매핑:", mappings);
setCategoryMappings(mappings);
} catch (error) {
console.error("TableListComponent 카테고리 매핑 로드 실패:", error);
@@ -920,16 +925,29 @@ export const TableListComponent: React.FC = ({
// inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
const inputType = meta?.inputType || column.inputType;
- // 카테고리 타입: 코드값 → 라벨로 변환
+ // 카테고리 타입: 배지로 표시
if (inputType === "category") {
+ if (!value) return "";
+
const mapping = categoryMappings[column.columnName];
- if (mapping && value) {
- const label = mapping[String(value)];
- if (label) {
- return label;
- }
- }
- return String(value);
+ const categoryData = mapping?.[String(value)];
+
+ // 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
+ const displayLabel = categoryData?.label || String(value);
+ const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상
+
+ const { Badge } = require("@/components/ui/badge");
+ return (
+
+ {displayLabel}
+
+ );
}
// 코드 타입: 코드 값 → 코드명 변환
From 49935189b64d227923e003efb6951f46f763c4e8 Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:20:58 +0900
Subject: [PATCH 05/16] =?UTF-8?q?fix:=20=ED=99=94=EB=A9=B4=20=EC=A0=84?=
=?UTF-8?q?=ED=99=98=20=ED=9B=84=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?=
=?UTF-8?q?=EB=A7=A4=ED=95=91=20=EA=B0=B1=EC=8B=A0=20=EB=AC=B8=EC=A0=9C=20?=
=?UTF-8?q?=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- useEffect 의존성 배열에 refreshTrigger 추가
- 데이터 새로고침 시 카테고리 매핑도 자동 갱신
- 매핑 로드 시작/종료 로그 추가하여 디버깅 용이성 향상
---
.../components/table-list/TableListComponent.tsx | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index 98654471..3ef31c10 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -435,7 +435,16 @@ export const TableListComponent: React.FC = ({
.filter(([_, meta]) => meta.inputType === "category")
.map(([columnName, _]) => columnName);
- if (categoryColumns.length === 0) return;
+ if (categoryColumns.length === 0) {
+ console.log("⚠️ [TableList] 카테고리 컬럼 없음");
+ return;
+ }
+
+ console.log("🔄 [TableList] 카테고리 매핑 로드 시작:", {
+ table: tableConfig.selectedTable,
+ categoryColumns,
+ refreshTrigger,
+ });
const mappings: Record> = {};
@@ -470,7 +479,7 @@ export const TableListComponent: React.FC = ({
};
loadCategoryMappings();
- }, [tableConfig.selectedTable, columnMeta]);
+ }, [tableConfig.selectedTable, columnMeta, refreshTrigger]);
// ========================================
// 데이터 가져오기
From 95b341df7992b3f56b7b05c4377351d91b15a1dd Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:22:24 +0900
Subject: [PATCH 06/16] =?UTF-8?q?fix:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?=
=?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0?=
=?UTF-8?q?=EB=A6=AC=20=EB=A7=A4=ED=95=91=20=EC=9E=90=EB=8F=99=20=EA=B0=B1?=
=?UTF-8?q?=EC=8B=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- useEffect 의존성을 refreshTrigger에서 data.length로 변경
- 데이터가 추가/삭제/변경될 때마다 자동으로 매핑 갱신
- 화면 전환 후 데이터 로드 완료 시점에 매핑도 함께 갱신
---
.../lib/registry/components/table-list/TableListComponent.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index 3ef31c10..127cc08b 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -479,7 +479,7 @@ export const TableListComponent: React.FC = ({
};
loadCategoryMappings();
- }, [tableConfig.selectedTable, columnMeta, refreshTrigger]);
+ }, [tableConfig.selectedTable, columnMeta, data.length]); // data.length로 데이터 변경 감지
// ========================================
// 데이터 가져오기
From cd961a2162975f41f93432ec2d99c0c9846fb961 Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:24:12 +0900
Subject: [PATCH 07/16] =?UTF-8?q?fix:=20=ED=99=94=EB=A9=B4=20=EB=B3=B5?=
=?UTF-8?q?=EA=B7=80=20=EC=8B=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?=
=?UTF-8?q?=EB=A7=A4=ED=95=91=20=EA=B0=B1=EC=8B=A0=20=EB=B3=B4=EC=9E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- loading 상태를 의존성으로 변경
- 데이터 로드 완료 시점(loading: false)에 매핑 갱신
- 화면 전환 후 복귀 시에도 최신 카테고리 데이터 반영
- 로딩 중에는 매핑 로드하지 않도록 가드 추가
---
.../registry/components/table-list/TableListComponent.tsx | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index 127cc08b..7b374507 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -430,6 +430,9 @@ export const TableListComponent: React.FC = ({
const loadCategoryMappings = async () => {
if (!tableConfig.selectedTable || !columnMeta) return;
+ // 로딩 중에는 매핑 로드하지 않음 (데이터 로드 완료 후에만 실행)
+ if (loading) return;
+
try {
const categoryColumns = Object.entries(columnMeta)
.filter(([_, meta]) => meta.inputType === "category")
@@ -443,7 +446,8 @@ export const TableListComponent: React.FC = ({
console.log("🔄 [TableList] 카테고리 매핑 로드 시작:", {
table: tableConfig.selectedTable,
categoryColumns,
- refreshTrigger,
+ dataLength: data.length,
+ loading,
});
const mappings: Record> = {};
@@ -479,7 +483,7 @@ export const TableListComponent: React.FC = ({
};
loadCategoryMappings();
- }, [tableConfig.selectedTable, columnMeta, data.length]); // data.length로 데이터 변경 감지
+ }, [tableConfig.selectedTable, columnMeta, loading]); // loading이 false가 될 때마다 갱신!
// ========================================
// 데이터 가져오기
From 70dc24f7a1a06d1e5f6b7a955137b0f02e53ae93 Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:26:07 +0900
Subject: [PATCH 08/16] =?UTF-8?q?fix:=20columnMeta=20=EB=A1=9C=EB=94=A9=20?=
=?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=9B=84=20=EC=B9=B4=ED=85=8C=EA=B3=A0?=
=?UTF-8?q?=EB=A6=AC=20=EB=A7=A4=ED=95=91=20=EB=A1=9C=EB=93=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- columnMeta가 비어있을 때 로딩 대기 로그 출력
- columnMeta 준비 완료 후에만 카테고리 매핑 시도
- 카테고리 컬럼 없음 로그에 디버깅 정보 추가
- 화면 전환 시 columnMeta → 카테고리 매핑 순서 보장
---
.../components/table-list/TableListComponent.tsx | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index 7b374507..8b94ef15 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -428,18 +428,29 @@ export const TableListComponent: React.FC = ({
useEffect(() => {
const loadCategoryMappings = async () => {
- if (!tableConfig.selectedTable || !columnMeta) return;
+ if (!tableConfig.selectedTable) return;
// 로딩 중에는 매핑 로드하지 않음 (데이터 로드 완료 후에만 실행)
if (loading) return;
+ // columnMeta가 비어있으면 대기
+ const columnMetaKeys = Object.keys(columnMeta || {});
+ if (columnMetaKeys.length === 0) {
+ console.log("⏳ [TableList] columnMeta 로딩 대기 중...");
+ return;
+ }
+
try {
const categoryColumns = Object.entries(columnMeta)
.filter(([_, meta]) => meta.inputType === "category")
.map(([columnName, _]) => columnName);
if (categoryColumns.length === 0) {
- console.log("⚠️ [TableList] 카테고리 컬럼 없음");
+ console.log("⚠️ [TableList] 카테고리 컬럼 없음:", {
+ table: tableConfig.selectedTable,
+ columnMetaKeys,
+ columnMeta,
+ });
return;
}
From 4cd08c3900d52b89dc430aa0890cb8974ee54e1b Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:27:22 +0900
Subject: [PATCH 09/16] =?UTF-8?q?fix:=20webType=EB=8F=84=20=EC=B2=B4?=
=?UTF-8?q?=ED=81=AC=ED=95=98=EC=97=AC=20=EC=B9=B4=ED=85=8C=EA=B3=A0?=
=?UTF-8?q?=EB=A6=AC=20=EC=BB=AC=EB=9F=BC=20=EA=B0=90=EC=A7=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- inputType과 webType 모두 'category'인 경우 처리
- columnMeta에 inputType이 없어도 webType으로 감지 가능
- material 컬럼 등 webType만 있는 경우도 정상 동작
---
.../lib/registry/components/table-list/TableListComponent.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index 8b94ef15..dd450621 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -442,7 +442,7 @@ export const TableListComponent: React.FC = ({
try {
const categoryColumns = Object.entries(columnMeta)
- .filter(([_, meta]) => meta.inputType === "category")
+ .filter(([_, meta]) => meta.inputType === "category" || meta.webType === "category")
.map(([columnName, _]) => columnName);
if (categoryColumns.length === 0) {
From 85e1b532fa888057afec45f1550c1ad88117f33d Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:28:39 +0900
Subject: [PATCH 10/16] =?UTF-8?q?fix:=20=EC=BA=90=EC=8B=9C=EC=97=90?=
=?UTF-8?q?=EC=84=9C=20inputType=20=EB=88=84=EB=9D=BD=20=EB=AC=B8=EC=A0=9C?=
=?UTF-8?q?=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 캐시된 데이터 사용 시 inputType이 설정되지 않던 문제 수정
- cached.inputTypes를 올바르게 매핑하여 meta에 포함
- webType 체크 제거, inputType만 사용하도록 변경
- 화면 전환 후 캐시 사용 시에도 카테고리 타입 정상 인식
---
.../components/table-list/TableListComponent.tsx | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index dd450621..58055ca6 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -338,13 +338,22 @@ export const TableListComponent: React.FC = ({
const cached = tableColumnCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) {
const labels: Record = {};
- const meta: Record = {};
+ const meta: Record = {};
+
+ // 캐시된 inputTypes 맵 생성
+ const inputTypeMap: Record = {};
+ if (cached.inputTypes) {
+ cached.inputTypes.forEach((col: any) => {
+ inputTypeMap[col.columnName] = col.inputType;
+ });
+ }
cached.columns.forEach((col: any) => {
labels[col.columnName] = col.displayName || col.comment || col.columnName;
meta[col.columnName] = {
webType: col.webType,
codeCategory: col.codeCategory,
+ inputType: inputTypeMap[col.columnName], // 캐시된 inputType 사용!
};
});
@@ -442,7 +451,7 @@ export const TableListComponent: React.FC = ({
try {
const categoryColumns = Object.entries(columnMeta)
- .filter(([_, meta]) => meta.inputType === "category" || meta.webType === "category")
+ .filter(([_, meta]) => meta.inputType === "category")
.map(([columnName, _]) => columnName);
if (categoryColumns.length === 0) {
From b5a83bb0f3ce72104d363b1c2922af8d4bc20e21 Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:32:17 +0900
Subject: [PATCH 11/16] =?UTF-8?q?docs:=20inputType=20=EC=82=AC=EC=9A=A9=20?=
=?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- webType은 레거시, inputType만 사용해야 함을 명시
- API 호출 및 캐시 처리 방법 설명
- 실제 적용 사례 및 마이그레이션 체크리스트 포함
- 디버깅 팁 및 주요 inputType 종류 문서화
---
.cursor/rules/inputtype-usage-guide.mdc | 279 ++++++++++++++++++++++++
1 file changed, 279 insertions(+)
create mode 100644 .cursor/rules/inputtype-usage-guide.mdc
diff --git a/.cursor/rules/inputtype-usage-guide.mdc b/.cursor/rules/inputtype-usage-guide.mdc
new file mode 100644
index 00000000..4acd021a
--- /dev/null
+++ b/.cursor/rules/inputtype-usage-guide.mdc
@@ -0,0 +1,279 @@
+# inputType 사용 가이드
+
+## 핵심 원칙
+
+**컬럼 타입 판단 시 반드시 `inputType`을 사용해야 합니다. `webType`은 레거시이며 더 이상 사용하지 않습니다.**
+
+---
+
+## 올바른 사용법
+
+### ✅ inputType 사용 (권장)
+
+```typescript
+// 카테고리 타입 체크
+if (columnMeta.inputType === "category") {
+ // 카테고리 처리 로직
+}
+
+// 코드 타입 체크
+if (meta.inputType === "code") {
+ // 코드 처리 로직
+}
+
+// 필터링
+const categoryColumns = Object.entries(columnMeta)
+ .filter(([_, meta]) => meta.inputType === "category")
+ .map(([columnName, _]) => columnName);
+```
+
+### ❌ webType 사용 (금지)
+
+```typescript
+// ❌ 절대 사용 금지!
+if (columnMeta.webType === "category") { ... }
+
+// ❌ 이것도 금지!
+const categoryColumns = columns.filter(col => col.webType === "category");
+```
+
+---
+
+## API에서 inputType 가져오기
+
+### Backend API
+
+```typescript
+// 컬럼 입력 타입 정보 가져오기
+const inputTypes = await tableTypeApi.getColumnInputTypes(tableName);
+
+// inputType 맵 생성
+const inputTypeMap: Record = {};
+inputTypes.forEach((col: any) => {
+ inputTypeMap[col.columnName] = col.inputType;
+});
+```
+
+### columnMeta 구조
+
+```typescript
+interface ColumnMeta {
+ webType?: string; // 레거시, 사용 금지
+ codeCategory?: string;
+ inputType?: string; // ✅ 반드시 이것 사용!
+}
+
+const columnMeta: Record = {
+ material: {
+ webType: "category", // 무시
+ codeCategory: "",
+ inputType: "category" // ✅ 이것만 사용
+ }
+};
+```
+
+---
+
+## 캐시 사용 시 주의사항
+
+### ❌ 잘못된 캐시 처리 (inputType 누락)
+
+```typescript
+const cached = tableColumnCache.get(cacheKey);
+if (cached) {
+ const meta: Record = {};
+
+ cached.columns.forEach((col: any) => {
+ meta[col.columnName] = {
+ webType: col.webType,
+ codeCategory: col.codeCategory,
+ // ❌ inputType 누락!
+ };
+ });
+}
+```
+
+### ✅ 올바른 캐시 처리 (inputType 포함)
+
+```typescript
+const cached = tableColumnCache.get(cacheKey);
+if (cached) {
+ const meta: Record = {};
+
+ // 캐시된 inputTypes 맵 생성
+ const inputTypeMap: Record = {};
+ if (cached.inputTypes) {
+ cached.inputTypes.forEach((col: any) => {
+ inputTypeMap[col.columnName] = col.inputType;
+ });
+ }
+
+ cached.columns.forEach((col: any) => {
+ meta[col.columnName] = {
+ webType: col.webType,
+ codeCategory: col.codeCategory,
+ inputType: inputTypeMap[col.columnName], // ✅ inputType 포함!
+ };
+ });
+}
+```
+
+---
+
+## 주요 inputType 종류
+
+| inputType | 설명 | 사용 예시 |
+|-----------|------|-----------|
+| `text` | 일반 텍스트 입력 | 이름, 설명 등 |
+| `number` | 숫자 입력 | 금액, 수량 등 |
+| `date` | 날짜 입력 | 생성일, 수정일 등 |
+| `datetime` | 날짜+시간 입력 | 타임스탬프 등 |
+| `category` | 카테고리 선택 | 분류, 상태 등 |
+| `code` | 공통 코드 선택 | 코드 마스터 데이터 |
+| `boolean` | 예/아니오 | 활성화 여부 등 |
+| `email` | 이메일 입력 | 이메일 주소 |
+| `url` | URL 입력 | 웹사이트 주소 |
+| `image` | 이미지 업로드 | 프로필 사진 등 |
+| `file` | 파일 업로드 | 첨부파일 등 |
+
+---
+
+## 실제 적용 사례
+
+### 1. TableListComponent - 카테고리 매핑 로드
+
+```typescript
+// ✅ inputType으로 카테고리 컬럼 필터링
+const categoryColumns = Object.entries(columnMeta)
+ .filter(([_, meta]) => meta.inputType === "category")
+ .map(([columnName, _]) => columnName);
+
+// 각 카테고리 컬럼의 값 목록 조회
+for (const columnName of categoryColumns) {
+ const response = await apiClient.get(
+ `/table-categories/${tableName}/${columnName}/values`
+ );
+ // 매핑 처리...
+}
+```
+
+### 2. InteractiveDataTable - 셀 값 렌더링
+
+```typescript
+// ✅ inputType으로 렌더링 분기
+const inputType = columnMeta[column.columnName]?.inputType;
+
+switch (inputType) {
+ case "category":
+ // 카테고리 배지 렌더링
+ return {categoryLabel};
+
+ case "code":
+ // 코드명 표시
+ return codeName;
+
+ case "date":
+ // 날짜 포맷팅
+ return formatDate(value);
+
+ default:
+ return value;
+}
+```
+
+### 3. 검색 필터 생성
+
+```typescript
+// ✅ inputType에 따라 다른 검색 UI 제공
+const renderSearchInput = (column: ColumnConfig) => {
+ const inputType = columnMeta[column.columnName]?.inputType;
+
+ switch (inputType) {
+ case "category":
+ return ;
+
+ case "code":
+ return ;
+
+ case "date":
+ return ;
+
+ case "number":
+ return ;
+
+ default:
+ return ;
+ }
+};
+```
+
+---
+
+## 마이그레이션 체크리스트
+
+기존 코드에서 `webType`을 `inputType`으로 전환할 때:
+
+- [ ] `webType` 참조를 모두 `inputType`으로 변경
+- [ ] API 호출 시 `getColumnInputTypes()` 포함 확인
+- [ ] 캐시 사용 시 `cached.inputTypes` 매핑 확인
+- [ ] 타입 정의에서 `inputType` 필드 포함
+- [ ] 조건문에서 `inputType` 체크로 변경
+- [ ] 테스트 실행하여 정상 동작 확인
+
+---
+
+## 디버깅 팁
+
+### inputType이 undefined인 경우
+
+```typescript
+// 디버깅 로그 추가
+console.log("columnMeta:", columnMeta);
+console.log("inputType:", columnMeta[columnName]?.inputType);
+
+// 체크 포인트:
+// 1. getColumnInputTypes() 호출 확인
+// 2. inputTypeMap 생성 확인
+// 3. meta 객체에 inputType 할당 확인
+// 4. 캐시 사용 시 cached.inputTypes 확인
+```
+
+### webType만 있고 inputType이 없는 경우
+
+```typescript
+// ❌ 잘못된 데이터 구조
+{
+ material: {
+ webType: "category",
+ codeCategory: "",
+ // inputType 누락!
+ }
+}
+
+// ✅ 올바른 데이터 구조
+{
+ material: {
+ webType: "category", // 레거시, 무시됨
+ codeCategory: "",
+ inputType: "category" // ✅ 필수!
+ }
+}
+```
+
+---
+
+## 참고 자료
+
+- **컴포넌트**: `/frontend/lib/registry/components/table-list/TableListComponent.tsx`
+- **API 클라이언트**: `/frontend/lib/api/tableType.ts`
+- **타입 정의**: `/frontend/types/table.ts`
+
+---
+
+## 요약
+
+1. **항상 `inputType` 사용**, `webType` 사용 금지
+2. **API에서 `getColumnInputTypes()` 호출** 필수
+3. **캐시 사용 시 `inputTypes` 포함** 확인
+4. **디버깅 시 `inputType` 값 확인**
+5. **기존 코드 마이그레이션** 시 체크리스트 활용
From 4affe623a5650bf4d6851469aa9c2b5331e9d07c Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:43:01 +0900
Subject: [PATCH 12/16] =?UTF-8?q?fix:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?=
=?UTF-8?q?=EB=A6=AC=20=EB=A7=A4=ED=95=91=20=EB=A1=9C=EB=94=A9=20=ED=83=80?=
=?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- loading 의존성 제거 (불필요한 재로드 방지)
- columnMeta 길이 변화로 매핑 로드 트리거
- 매핑 로드 전후 상태 디버깅 로그 추가
- categoryMappings 빈 객체 문제 해결
---
.../table-list/TableListComponent.tsx | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx
index 9bb33a1e..c46fe1fe 100644
--- a/frontend/lib/registry/components/table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx
@@ -459,15 +459,18 @@ export const TableListComponent: React.FC = ({
const loadCategoryMappings = async () => {
if (!tableConfig.selectedTable) return;
- // 로딩 중에는 매핑 로드하지 않음 (데이터 로드 완료 후에만 실행)
- if (loading) return;
-
// columnMeta가 비어있으면 대기
const columnMetaKeys = Object.keys(columnMeta || {});
if (columnMetaKeys.length === 0) {
console.log("⏳ [TableList] columnMeta 로딩 대기 중...");
return;
}
+
+ console.log("🚀 [TableList] 카테고리 매핑 로드 트리거:", {
+ table: tableConfig.selectedTable,
+ columnMetaKeys,
+ dataLength: data.length,
+ });
try {
const categoryColumns = Object.entries(columnMeta)
@@ -514,15 +517,21 @@ export const TableListComponent: React.FC = ({
}
console.log("📊 [TableList] 전체 카테고리 매핑:", mappings);
+ console.log("🔄 [TableList] setCategoryMappings 호출 전:", categoryMappings);
setCategoryMappings(mappings);
setCategoryMappingsKey((prev) => prev + 1); // 리렌더링 트리거
+
+ // 상태 업데이트 확인을 위한 setTimeout
+ setTimeout(() => {
+ console.log("✅ [TableList] setCategoryMappings 호출 후 (비동기):", categoryMappings);
+ }, 100);
} catch (error) {
console.error("TableListComponent 카테고리 매핑 로드 실패:", error);
}
};
loadCategoryMappings();
- }, [tableConfig.selectedTable, columnMeta, loading]); // loading이 false가 될 때마다 갱신!
+ }, [tableConfig.selectedTable, Object.keys(columnMeta || {}).length]); // columnMeta가 로드되면 실행!
// ========================================
// 데이터 가져오기
@@ -1054,6 +1063,8 @@ export const TableListComponent: React.FC = ({
categoryData,
hasMapping: !!mapping,
hasCategoryData: !!categoryData,
+ allCategoryMappings: categoryMappings, // 전체 매핑 확인
+ categoryMappingsKeys: Object.keys(categoryMappings),
});
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
From bc826e8e49752d33998aa1d3c3ff6e522aa459cc Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 12:46:08 +0900
Subject: [PATCH 13/16] =?UTF-8?q?chore:=20resizable-dialog=20=EB=94=94?=
=?UTF-8?q?=EB=B2=84=EA=B9=85=20=EB=A1=9C=EA=B7=B8=20=EB=AA=A8=EB=91=90=20?=
=?UTF-8?q?=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- console.log 20개 주석 처리
- 콘솔 스팸 방지
- 불필요한 로그 제거로 성능 개선
---
frontend/components/ui/resizable-dialog.tsx | 40 ++++++++++-----------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/frontend/components/ui/resizable-dialog.tsx b/frontend/components/ui/resizable-dialog.tsx
index 9ce42b46..0bf369de 100644
--- a/frontend/components/ui/resizable-dialog.tsx
+++ b/frontend/components/ui/resizable-dialog.tsx
@@ -87,7 +87,7 @@ const ResizableDialogContent = React.forwardRef<
if (!stableIdRef.current) {
if (modalId) {
stableIdRef.current = modalId;
- console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
+ // // console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
} else {
// className 기반 ID 생성
if (className) {
@@ -95,7 +95,7 @@ const ResizableDialogContent = React.forwardRef<
return ((acc << 5) - acc) + char.charCodeAt(0);
}, 0);
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
- console.log("🔄 ResizableDialog - className 기반 ID 생성:", {
+ // console.log("🔄 ResizableDialog - className 기반 ID 생성:", {
className,
generatedId: stableIdRef.current,
});
@@ -106,14 +106,14 @@ const ResizableDialogContent = React.forwardRef<
return ((acc << 5) - acc) + char.charCodeAt(0);
}, 0);
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
- console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", {
+ // console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", {
userStyle,
generatedId: stableIdRef.current,
});
} else {
// 기본 ID
stableIdRef.current = 'modal-default';
- console.log("⚠️ ResizableDialog - 기본 ID 사용 (모든 모달이 같은 크기 공유)");
+ // console.log("⚠️ ResizableDialog - 기본 ID 사용 (모든 모달이 같은 크기 공유)");
}
}
}
@@ -171,7 +171,7 @@ const ResizableDialogContent = React.forwardRef<
const [wasOpen, setWasOpen] = React.useState(false);
React.useEffect(() => {
- console.log("🔍 모달 상태 변화 감지:", {
+ // console.log("🔍 모달 상태 변화 감지:", {
actualOpen,
wasOpen,
externalOpen,
@@ -181,12 +181,12 @@ const ResizableDialogContent = React.forwardRef<
if (actualOpen && !wasOpen) {
// 모달이 방금 열림
- console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
+ // console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
setIsInitialized(false);
setWasOpen(true);
} else if (!actualOpen && wasOpen) {
// 모달이 방금 닫힘
- console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
+ // console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
setWasOpen(false);
}
}, [actualOpen, wasOpen, effectiveModalId, externalOpen, context.open]);
@@ -194,7 +194,7 @@ const ResizableDialogContent = React.forwardRef<
// modalId가 변경되면 초기화 리셋 (다른 모달이 열린 경우)
React.useEffect(() => {
if (effectiveModalId !== lastModalId) {
- console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", {
+ // console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", {
이전: lastModalId,
현재: effectiveModalId,
isInitialized,
@@ -207,7 +207,7 @@ const ResizableDialogContent = React.forwardRef<
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
React.useEffect(() => {
- console.log("🔍 초기 크기 설정 useEffect 실행:", {
+ // console.log("🔍 초기 크기 설정 useEffect 실행:", {
isInitialized,
hasContentRef: !!contentRef.current,
effectiveModalId,
@@ -231,7 +231,7 @@ const ResizableDialogContent = React.forwardRef<
contentWidth = contentRef.current.scrollWidth || defaultWidth;
contentHeight = contentRef.current.scrollHeight || defaultHeight;
- console.log("📏 모달 내용 크기 측정:", {
+ // console.log("📏 모달 내용 크기 측정:", {
attempt: attempts,
scrollWidth: contentRef.current.scrollWidth,
scrollHeight: contentRef.current.scrollHeight,
@@ -241,7 +241,7 @@ const ResizableDialogContent = React.forwardRef<
contentHeight,
});
} else {
- console.log("⚠️ contentRef 없음, 재시도:", {
+ // console.log("⚠️ contentRef 없음, 재시도:", {
attempt: attempts,
maxAttempts,
defaultWidth,
@@ -265,7 +265,7 @@ const ResizableDialogContent = React.forwardRef<
height: Math.max(minHeight, Math.min(maxHeight, Math.max(contentHeight + paddingAndMargin, initialSize.height))),
};
- console.log("📐 내용 기반 크기:", contentBasedSize);
+ // console.log("📐 내용 기반 크기:", contentBasedSize);
// localStorage에서 저장된 크기 확인
let finalSize = contentBasedSize;
@@ -275,7 +275,7 @@ const ResizableDialogContent = React.forwardRef<
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
const saved = localStorage.getItem(storageKey);
- console.log("📦 localStorage 확인:", {
+ // console.log("📦 localStorage 확인:", {
effectiveModalId,
userId,
storageKey,
@@ -292,27 +292,27 @@ const ResizableDialogContent = React.forwardRef<
height: Math.max(minHeight, Math.min(maxHeight, parsed.height)),
};
- console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
+ // console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
// ✅ 중요: 사용자가 명시적으로 리사이징한 경우, 사용자 크기를 우선 사용
// (사용자가 의도적으로 작게 만든 것을 존중)
finalSize = savedSize;
setUserResized(true);
- console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", {
+ // console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", {
savedSize,
contentBasedSize,
finalSize,
note: "사용자가 리사이징한 크기를 그대로 사용합니다",
});
} else {
- console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
+ // console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
}
} else {
- console.log("ℹ️ localStorage에 저장된 크기 없음, 내용 크기 사용");
+ // console.log("ℹ️ localStorage에 저장된 크기 없음, 내용 크기 사용");
}
} catch (error) {
- console.error("❌ 모달 크기 복원 실패:", error);
+ // console.error("❌ 모달 크기 복원 실패:", error);
}
}
@@ -384,7 +384,7 @@ const ResizableDialogContent = React.forwardRef<
userResized: true, // 사용자가 직접 리사이징했음을 표시
};
localStorage.setItem(storageKey, JSON.stringify(currentSize));
- console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", {
+ // console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", {
effectiveModalId,
userId,
storageKey,
@@ -392,7 +392,7 @@ const ResizableDialogContent = React.forwardRef<
stateSize: { width: size.width, height: size.height },
});
} catch (error) {
- console.error("❌ 모달 크기 저장 실패:", error);
+ // console.error("❌ 모달 크기 저장 실패:", error);
}
}
};
From 2e674e13d073bc5aeb96816f6cec886b55c69901 Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 13:26:54 +0900
Subject: [PATCH 14/16] =?UTF-8?q?fix:=20resizable-dialog=20=EC=A3=BC?=
=?UTF-8?q?=EC=84=9D=20=EC=B2=98=EB=A6=AC=EB=90=9C=20=EA=B0=9D=EC=B2=B4=20?=
=?UTF-8?q?=EB=A6=AC=ED=84=B0=EB=9F=B4=20=ED=8C=8C=EC=8B=B1=20=EC=97=90?=
=?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 여러 줄 객체 리터럴을 한 줄로 변경
- console.log 주석이 파싱 에러를 일으키는 문제 해결
- 빌드 에러 해결
---
frontend/components/ui/resizable-dialog.tsx | 69 +++------------------
1 file changed, 10 insertions(+), 59 deletions(-)
diff --git a/frontend/components/ui/resizable-dialog.tsx b/frontend/components/ui/resizable-dialog.tsx
index 0bf369de..facb07ca 100644
--- a/frontend/components/ui/resizable-dialog.tsx
+++ b/frontend/components/ui/resizable-dialog.tsx
@@ -95,10 +95,7 @@ const ResizableDialogContent = React.forwardRef<
return ((acc << 5) - acc) + char.charCodeAt(0);
}, 0);
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
- // console.log("🔄 ResizableDialog - className 기반 ID 생성:", {
- className,
- generatedId: stableIdRef.current,
- });
+ // console.log("🔄 ResizableDialog - className 기반 ID 생성:", { className, generatedId: stableIdRef.current });
} else if (userStyle) {
// userStyle 기반 ID 생성
const styleStr = JSON.stringify(userStyle);
@@ -106,10 +103,7 @@ const ResizableDialogContent = React.forwardRef<
return ((acc << 5) - acc) + char.charCodeAt(0);
}, 0);
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
- // console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", {
- userStyle,
- generatedId: stableIdRef.current,
- });
+ // console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", { userStyle, generatedId: stableIdRef.current });
} else {
// 기본 ID
stableIdRef.current = 'modal-default';
@@ -171,13 +165,7 @@ const ResizableDialogContent = React.forwardRef<
const [wasOpen, setWasOpen] = React.useState(false);
React.useEffect(() => {
- // console.log("🔍 모달 상태 변화 감지:", {
- actualOpen,
- wasOpen,
- externalOpen,
- contextOpen: context.open,
- effectiveModalId
- });
+ // console.log("🔍 모달 상태 변화 감지:", { actualOpen, wasOpen, externalOpen, contextOpen: context.open, effectiveModalId });
if (actualOpen && !wasOpen) {
// 모달이 방금 열림
@@ -194,11 +182,7 @@ const ResizableDialogContent = React.forwardRef<
// modalId가 변경되면 초기화 리셋 (다른 모달이 열린 경우)
React.useEffect(() => {
if (effectiveModalId !== lastModalId) {
- // console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", {
- 이전: lastModalId,
- 현재: effectiveModalId,
- isInitialized,
- });
+ // console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", { 이전: lastModalId, 현재: effectiveModalId, isInitialized });
setIsInitialized(false);
setUserResized(false); // 사용자 리사이징 플래그도 리셋
setLastModalId(effectiveModalId);
@@ -207,11 +191,7 @@ const ResizableDialogContent = React.forwardRef<
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
React.useEffect(() => {
- // console.log("🔍 초기 크기 설정 useEffect 실행:", {
- isInitialized,
- hasContentRef: !!contentRef.current,
- effectiveModalId,
- });
+ // console.log("🔍 초기 크기 설정 useEffect 실행:", { isInitialized, hasContentRef: !!contentRef.current, effectiveModalId });
if (!isInitialized) {
// 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
@@ -231,22 +211,9 @@ const ResizableDialogContent = React.forwardRef<
contentWidth = contentRef.current.scrollWidth || defaultWidth;
contentHeight = contentRef.current.scrollHeight || defaultHeight;
- // console.log("📏 모달 내용 크기 측정:", {
- attempt: attempts,
- scrollWidth: contentRef.current.scrollWidth,
- scrollHeight: contentRef.current.scrollHeight,
- clientWidth: contentRef.current.clientWidth,
- clientHeight: contentRef.current.clientHeight,
- contentWidth,
- contentHeight,
- });
+ // console.log("📏 모달 내용 크기 측정:", { attempt: attempts, scrollWidth: contentRef.current.scrollWidth, scrollHeight: contentRef.current.scrollHeight, clientWidth: contentRef.current.clientWidth, clientHeight: contentRef.current.clientHeight, contentWidth, contentHeight });
} else {
- // console.log("⚠️ contentRef 없음, 재시도:", {
- attempt: attempts,
- maxAttempts,
- defaultWidth,
- defaultHeight
- });
+ // console.log("⚠️ contentRef 없음, 재시도:", { attempt: attempts, maxAttempts, defaultWidth, defaultHeight });
// contentRef가 아직 없으면 재시도
if (attempts < maxAttempts) {
@@ -275,12 +242,7 @@ const ResizableDialogContent = React.forwardRef<
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
const saved = localStorage.getItem(storageKey);
- // console.log("📦 localStorage 확인:", {
- effectiveModalId,
- userId,
- storageKey,
- saved: saved ? "있음" : "없음",
- });
+ // console.log("📦 localStorage 확인:", { effectiveModalId, userId, storageKey, saved: saved ? "있음" : "없음" });
if (saved) {
const parsed = JSON.parse(saved);
@@ -299,12 +261,7 @@ const ResizableDialogContent = React.forwardRef<
finalSize = savedSize;
setUserResized(true);
- // console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", {
- savedSize,
- contentBasedSize,
- finalSize,
- note: "사용자가 리사이징한 크기를 그대로 사용합니다",
- });
+ // console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", { savedSize, contentBasedSize, finalSize, note: "사용자가 리사이징한 크기를 그대로 사용합니다" });
} else {
// console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
}
@@ -384,13 +341,7 @@ const ResizableDialogContent = React.forwardRef<
userResized: true, // 사용자가 직접 리사이징했음을 표시
};
localStorage.setItem(storageKey, JSON.stringify(currentSize));
- // console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", {
- effectiveModalId,
- userId,
- storageKey,
- size: currentSize,
- stateSize: { width: size.width, height: size.height },
- });
+ // console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", { effectiveModalId, userId, storageKey, size: currentSize, stateSize: { width: size.width, height: size.height } });
} catch (error) {
// console.error("❌ 모달 크기 저장 실패:", error);
}
From 832e80cd7f1e43f8cec44251a7f03836368b13d3 Mon Sep 17 00:00:00 2001
From: kjs
Date: Thu, 6 Nov 2025 14:18:36 +0900
Subject: [PATCH 15/16] =?UTF-8?q?=EB=B0=B0=EC=A7=80=20=ED=91=9C=EC=8B=9C?=
=?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/components/admin/RoleDeleteModal.tsx | 2 +-
frontend/components/admin/RoleFormModal.tsx | 2 +-
.../table-list/TableListComponent.tsx | 137 ++++++++++++------
3 files changed, 94 insertions(+), 47 deletions(-)
diff --git a/frontend/components/admin/RoleDeleteModal.tsx b/frontend/components/admin/RoleDeleteModal.tsx
index cf363dbd..9d178351 100644
--- a/frontend/components/admin/RoleDeleteModal.tsx
+++ b/frontend/components/admin/RoleDeleteModal.tsx
@@ -133,7 +133,7 @@ export function RoleDeleteModal({ isOpen, onClose, onSuccess, role }: RoleDelete
)}
-
+
- setIsAddDialogOpen(true)} size="sm">
-
- 새 값 추가
-
+
+ {/* 비활성 항목 표시 옵션 */}
+
+ setShowInactive(checked as boolean)}
+ />
+
+
+
+
setIsAddDialogOpen(true)} size="sm">
+
+ 새 값 추가
+
+
{/* 검색바 */}
@@ -294,73 +319,90 @@ export const CategoryValueManager: React.FC = ({
) : (
- {filteredValues.map((value) => (
-
-
handleSelectValue(value.valueId!)}
- />
-
-
-
- {value.valueCode}
-
-
- {value.valueLabel}
-
- {value.description && (
-
- - {value.description}
-
- )}
- {value.isDefault && (
-
- 기본값
-
- )}
- {value.color && (
-
- )}
-
-
-
-
- handleToggleActive(
- value.valueId!,
- value.isActive !== false
- )
- }
- className="data-[state=checked]:bg-emerald-500"
+ {filteredValues.map((value) => {
+ const isInactive = value.isActive === false;
+
+ return (
+
+
handleSelectValue(value.valueId!)}
/>
- setEditingValue(value)}
- className="h-8 w-8"
- >
-
-
+
+ {/* 색상 표시 (앞쪽으로 이동) */}
+ {value.color && (
+
+ )}
+
+ {/* 라벨 */}
+
+ {value.valueLabel}
+
+
+ {/* 설명 */}
+ {value.description && (
+
+ - {value.description}
+
+ )}
+
+ {/* 기본값 배지 */}
+ {value.isDefault && (
+
+ 기본값
+
+ )}
+
+ {/* 비활성 배지 */}
+ {isInactive && (
+
+ 비활성
+
+ )}
+
- handleDeleteValue(value.valueId!)}
- className="h-8 w-8 text-destructive"
- >
-
-
+
+
+ handleToggleActive(
+ value.valueId!,
+ value.isActive !== false
+ )
+ }
+ className="data-[state=checked]:bg-emerald-500"
+ />
+
+ setEditingValue(value)}
+ className="h-8 w-8"
+ >
+
+
+
+ handleDeleteValue(value.valueId!)}
+ className="h-8 w-8 text-destructive"
+ >
+
+
+
-
- ))}
+ );
+ })}
)}
diff --git a/frontend/lib/utils/multilang.ts b/frontend/lib/utils/multilang.ts
index 1f9ef866..2bbb0e6b 100644
--- a/frontend/lib/utils/multilang.ts
+++ b/frontend/lib/utils/multilang.ts
@@ -288,9 +288,19 @@ function getDefaultText(key: string): string {
[MENU_MANAGEMENT_KEYS.USER_MENU]: "사용자 메뉴",
[MENU_MANAGEMENT_KEYS.ADMIN_DESCRIPTION]: "시스템 관리 및 설정 메뉴",
[MENU_MANAGEMENT_KEYS.USER_DESCRIPTION]: "일반 사용자 업무 메뉴",
+ [MENU_MANAGEMENT_KEYS.LIST_TITLE]: "메뉴 목록",
+ [MENU_MANAGEMENT_KEYS.LIST_TOTAL]: "전체",
+ [MENU_MANAGEMENT_KEYS.LIST_SEARCH_RESULT]: "검색 결과",
+ [MENU_MANAGEMENT_KEYS.FILTER_COMPANY]: "회사 필터",
+ [MENU_MANAGEMENT_KEYS.FILTER_COMPANY_ALL]: "전체",
+ [MENU_MANAGEMENT_KEYS.FILTER_COMPANY_COMMON]: "공통",
+ [MENU_MANAGEMENT_KEYS.FILTER_COMPANY_SEARCH]: "회사 검색...",
+ [MENU_MANAGEMENT_KEYS.FILTER_SEARCH]: "검색",
+ [MENU_MANAGEMENT_KEYS.FILTER_SEARCH_PLACEHOLDER]: "메뉴명 검색...",
+ [MENU_MANAGEMENT_KEYS.FILTER_RESET]: "초기화",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD]: "추가",
[MENU_MANAGEMENT_KEYS.BUTTON_ADD_TOP_LEVEL]: "최상위 메뉴 추가",
- [MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위 메뉴 추가",
+ [MENU_MANAGEMENT_KEYS.BUTTON_ADD_SUB]: "하위",
[MENU_MANAGEMENT_KEYS.BUTTON_EDIT]: "수정",
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE]: "삭제",
[MENU_MANAGEMENT_KEYS.BUTTON_DELETE_SELECTED]: "선택 삭제",
@@ -340,9 +350,48 @@ function getDefaultText(key: string): string {
[MENU_MANAGEMENT_KEYS.STATUS_ACTIVE]: "활성화",
[MENU_MANAGEMENT_KEYS.STATUS_INACTIVE]: "비활성화",
[MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED]: "미지정",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_LOADING]: "로딩 중...",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_PROCESSING]: "메뉴를 삭제하는 중...",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_SUCCESS]: "메뉴가 성공적으로 저장되었습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_SAVE_FAILED]: "메뉴 저장에 실패했습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_SUCCESS]: "메뉴가 성공적으로 삭제되었습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_FAILED]: "메뉴 삭제에 실패했습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_SUCCESS]: "{count}개의 메뉴가 성공적으로 삭제되었습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_DELETE_BATCH_PARTIAL]: "{success}개 삭제됨, {failed}개 실패",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_SUCCESS]: "메뉴 상태가 변경되었습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_MENU_STATUS_TOGGLE_FAILED]: "메뉴 상태 변경에 실패했습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_MENU_NAME_REQUIRED]: "메뉴명을 입력하세요.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_COMPANY_REQUIRED]: "회사를 선택하세요.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_VALIDATION_SELECT_MENU_DELETE]: "삭제할 메뉴를 선택하세요.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_LIST]: "메뉴 목록을 불러오는데 실패했습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_MENU_INFO]: "메뉴 정보를 불러오는데 실패했습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_COMPANY_LIST]: "회사 목록을 불러오는데 실패했습니다.",
+ [MENU_MANAGEMENT_KEYS.MESSAGE_ERROR_LOAD_LANG_KEY_LIST]: "다국어 키 목록을 불러오는데 실패했습니다.",
+ [MENU_MANAGEMENT_KEYS.UI_EXPAND]: "펼치기",
+ [MENU_MANAGEMENT_KEYS.UI_COLLAPSE]: "접기",
+ [MENU_MANAGEMENT_KEYS.UI_MENU_COLLAPSE]: "메뉴 접기",
+ [MENU_MANAGEMENT_KEYS.UI_LANGUAGE]: "언어",
+ // 추가 매핑: key 문자열 자체도 한글로 매핑
+ "menu.type.title": "메뉴 타입",
+ "menu.management.admin": "관리자",
+ "menu.management.admin.description": "시스템 관리 및 설정 메뉴",
+ "menu.management.user": "사용자",
+ "menu.management.user.description": "일반 사용자 업무 메뉴",
+ "menu.list.title": "메뉴 목록",
+ "filter.company.all": "전체",
+ "filter.search.placeholder": "메뉴명 검색...",
+ "filter.reset": "초기화",
+ "button.add.top.level": "최상위 메뉴 추가",
+ "button.delete.selected": "선택 삭제",
+ "table.header.menu.name": "메뉴명",
+ "table.header.sequence": "순서",
+ "table.header.company": "회사",
+ "table.header.menu.url": "URL",
+ "table.header.status": "상태",
+ "table.header.actions": "작업",
};
- return defaultTexts[key] || key;
+ return defaultTexts[key] || "";
}
/**