diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx
index e3d97088..bf7426e5 100644
--- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx
+++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx
@@ -217,10 +217,16 @@ export default function TableManagementPage() {
// 메모이제이션된 입력타입 옵션
const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []);
- // 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
+ // 참조 테이블 옵션 (한글라벨 (영어명) 동시 표시)
const referenceTableOptions = [
{ value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NONE, "선택 안함") },
- ...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })),
+ ...tables.map((table) => ({
+ value: table.tableName,
+ label:
+ table.displayName && table.displayName !== table.tableName
+ ? `${table.displayName} (${table.tableName})`
+ : table.tableName,
+ })),
];
// 공통 코드 카테고리 목록 상태
@@ -1610,6 +1616,8 @@ export default function TableManagementPage() {
onIndexToggle={(columnName, checked) =>
handleIndexToggle(columnName, "index", checked)
}
+ tables={tables}
+ referenceTableColumns={referenceTableColumns}
/>
>
)}
@@ -1809,11 +1817,16 @@ export default function TableManagementPage() {
diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx
index 1d053775..0d770dc9 100644
--- a/frontend/components/admin/table-type/ColumnDetailPanel.tsx
+++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx
@@ -76,9 +76,34 @@ export function ColumnDetailPanel({
if (!column) return null;
- const refTableOpts = referenceTableOptions.length
- ? referenceTableOptions
- : [{ value: "none", label: "선택 안함" }, ...tables.map((t) => ({ value: t.tableName, label: t.displayName || t.tableName }))];
+ const refTableOpts = useMemo(() => {
+ const hasKorean = (s: string) => /[가-힣]/.test(s);
+ const raw = referenceTableOptions.length
+ ? [...referenceTableOptions]
+ : [
+ { value: "none", label: "없음" },
+ ...tables.map((t) => ({
+ value: t.tableName,
+ label:
+ t.displayName && t.displayName !== t.tableName
+ ? `${t.displayName} (${t.tableName})`
+ : t.tableName,
+ })),
+ ];
+
+ const noneOpt = raw.find((o) => o.value === "none");
+ const rest = raw.filter((o) => o.value !== "none");
+
+ rest.sort((a, b) => {
+ const aK = hasKorean(a.label);
+ const bK = hasKorean(b.label);
+ if (aK && !bK) return -1;
+ if (!aK && bK) return 1;
+ return a.label.localeCompare(b.label, "ko");
+ });
+
+ return noneOpt ? [noneOpt, ...rest] : rest;
+ }, [referenceTableOptions, tables]);
return (
@@ -90,7 +115,11 @@ export function ColumnDetailPanel({
{typeConf.label}
)}
- {column.columnName}
+
+ {column.displayName && column.displayName !== column.columnName
+ ? `${column.displayName} (${column.columnName})`
+ : column.columnName}
+
@@ -245,7 +289,14 @@ export function ColumnDetailPanel({
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
)}
/>
- {refCol.columnName}
+ {refCol.displayName && refCol.displayName !== refCol.columnName ? (
+
+ {refCol.displayName}
+ {refCol.columnName}
+
+ ) : (
+
{refCol.columnName}
+ )}
))}
@@ -259,12 +310,20 @@ export function ColumnDetailPanel({
{/* 참조 요약 미니맵 */}
{column.referenceTable && column.referenceTable !== "none" && column.referenceColumn && (
-
- {column.referenceTable}
+
+ {(() => {
+ const tbl = refTableOpts.find((o) => o.value === column.referenceTable);
+ return tbl?.label ?? column.referenceTable;
+ })()}
→
-
- {column.referenceColumn}
+
+ {(() => {
+ const col = refColumns.find((c) => c.columnName === column.referenceColumn);
+ return col?.displayName && col.displayName !== column.referenceColumn
+ ? `${col.displayName} (${column.referenceColumn})`
+ : column.referenceColumn;
+ })()}
)}
diff --git a/frontend/components/admin/table-type/ColumnGrid.tsx b/frontend/components/admin/table-type/ColumnGrid.tsx
index c03c7516..825dbd36 100644
--- a/frontend/components/admin/table-type/ColumnGrid.tsx
+++ b/frontend/components/admin/table-type/ColumnGrid.tsx
@@ -5,8 +5,9 @@ import { MoreHorizontal, Database, Layers, FileStack } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
-import type { ColumnTypeInfo } from "./types";
+import type { ColumnTypeInfo, TableInfo } from "./types";
import { INPUT_TYPE_COLORS, getColumnGroup } from "./types";
+import type { ReferenceTableColumn } from "@/lib/api/entityJoin";
export interface ColumnGridConstraints {
primaryKey: { columns: string[] };
@@ -23,6 +24,9 @@ export interface ColumnGridProps {
getColumnIndexState?: (columnName: string) => { isPk: boolean; hasIndex: boolean };
onPkToggle?: (columnName: string, checked: boolean) => void;
onIndexToggle?: (columnName: string, checked: boolean) => void;
+ /** 호버 시 한글 라벨 표시용 (Badge title) */
+ tables?: TableInfo[];
+ referenceTableColumns?: Record
;
}
function getIndexState(
@@ -53,6 +57,8 @@ export function ColumnGrid({
getColumnIndexState: externalGetIndexState,
onPkToggle,
onIndexToggle,
+ tables,
+ referenceTableColumns,
}: ColumnGridProps) {
const getIdxState = useMemo(
() => externalGetIndexState ?? ((name: string) => getIndexState(name, constraints)),
@@ -136,13 +142,12 @@ export function ColumnGrid({
{/* 4px 색상바 (타입별 진한 색) */}
- {/* 라벨 + 컬럼명 */}
+ {/* 라벨 + 컬럼명 (한글라벨 (영어명) 동시 표시) */}
- {column.displayName || column.columnName}
-
-
- {column.columnName}
+ {column.displayName && column.displayName !== column.columnName
+ ? `${column.displayName} (${column.columnName})`
+ : column.columnName}
@@ -150,11 +155,38 @@ export function ColumnGrid({
{column.inputType === "entity" && column.referenceTable && column.referenceTable !== "none" && (
<>
-
+ {
+ const t = tables.find((tb) => tb.tableName === column.referenceTable);
+ return t?.displayName && t.displayName !== t.tableName
+ ? `${t.displayName} (${column.referenceTable})`
+ : column.referenceTable;
+ })()
+ : column.referenceTable
+ }
+ >
{column.referenceTable}
→
-
+ {
+ const refCols = referenceTableColumns[column.referenceTable];
+ const c = refCols.find((rc) => rc.columnName === (column.referenceColumn ?? ""));
+ return c?.displayName && c.displayName !== c.columnName
+ ? `${c.displayName} (${column.referenceColumn})`
+ : column.referenceColumn ?? "—";
+ })()
+ : column.referenceColumn ?? "—"
+ }
+ >
{column.referenceColumn || "—"}
>
diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx
index c80cb581..2014d535 100644
--- a/frontend/components/layout/AppLayout.tsx
+++ b/frontend/components/layout/AppLayout.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, Suspense, useEffect } from "react";
+import { useState, Suspense, useEffect, useCallback } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
@@ -341,6 +341,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const currentMenus = isAdminMode ? adminMenus : userMenus;
+ const currentTabs = useTabStore((s) => s[s.mode].tabs);
+ const currentActiveTabId = useTabStore((s) => s[s.mode].activeTabId);
+ const activeTab = currentTabs.find((t) => t.id === currentActiveTabId);
+
const toggleMenu = (menuId: string) => {
const newExpanded = new Set(expandedMenus);
if (newExpanded.has(menuId)) {
@@ -478,6 +482,26 @@ function AppLayoutInner({ children }: AppLayoutProps) {
}
};
+ // pathname + 활성 탭 기반 활성 메뉴 판별 (탭 네비게이션에서도 사이드바 활성 표시)
+ const isMenuActive = useCallback(
+ (menu: any): boolean => {
+ if (pathname === menu.url) return true;
+ if (!activeTab) return false;
+
+ const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0");
+
+ if (activeTab.type === "admin" && activeTab.adminUrl) {
+ return menu.url === activeTab.adminUrl;
+ }
+ if (activeTab.type === "screen") {
+ if (activeTab.menuObjid != null && menuObjid === activeTab.menuObjid) return true;
+ if (activeTab.screenId != null && menu.screenId === activeTab.screenId) return true;
+ }
+ return false;
+ },
+ [pathname, activeTab],
+ );
+
// 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용)
const renderMenu = (menu: any, level: number = 0) => {
const isExpanded = expandedMenus.has(menu.id);
@@ -489,8 +513,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
draggable={isLeaf}
onDragStart={(e) => handleMenuDragStart(e, menu)}
className={`group flex min-h-[44px] cursor-pointer items-center justify-between rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 ease-in-out sm:min-h-[40px] ${
- pathname === menu.url
- ? "border-primary bg-primary/8 text-primary border-l-3 font-semibold"
+ isMenuActive(menu)
+ ? "border-l-[3px] border-l-primary bg-primary/10 dark:bg-primary/15 text-primary font-semibold"
: isExpanded
? "bg-accent/60 text-foreground"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
@@ -518,8 +542,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
draggable={!child.hasChildren}
onDragStart={(e) => handleMenuDragStart(e, child)}
className={`flex min-h-[44px] cursor-pointer items-center rounded-md px-3 py-2 text-sm transition-colors duration-150 hover:cursor-pointer sm:min-h-[40px] ${
- pathname === child.url
- ? "border-primary bg-primary/8 text-primary border-l-3 font-semibold"
+ isMenuActive(child)
+ ? "border-l-[3px] border-l-primary bg-primary/10 dark:bg-primary/15 text-primary font-semibold"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
}`}
onClick={() => handleMenuClick(child)}
@@ -544,6 +568,30 @@ function AppLayoutInner({ children }: AppLayoutProps) {
);
}
+ const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : [];
+
+ // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장
+ useEffect(() => {
+ if (!activeTab || uiMenus.length === 0) return;
+
+ const toExpand: string[] = [];
+ for (const menu of uiMenus) {
+ if (menu.hasChildren && menu.children) {
+ const hasActiveChild = menu.children.some((child: any) => isMenuActive(child));
+ if (hasActiveChild && !expandedMenus.has(menu.id)) {
+ toExpand.push(menu.id);
+ }
+ }
+ }
+ if (toExpand.length > 0) {
+ setExpandedMenus((prev) => {
+ const next = new Set(prev);
+ toExpand.forEach((id) => next.add(id));
+ return next;
+ });
+ }
+ }, [activeTab, uiMenus, isMenuActive, expandedMenus]);
+
if (!user) {
return (
@@ -555,8 +603,6 @@ function AppLayoutInner({ children }: AppLayoutProps) {
);
}
- const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
-
return (
{/* 모바일 헤더 */}
diff --git a/frontend/components/layout/TabBar.tsx b/frontend/components/layout/TabBar.tsx
index e86ada2e..1ac5144e 100644
--- a/frontend/components/layout/TabBar.tsx
+++ b/frontend/components/layout/TabBar.tsx
@@ -493,8 +493,8 @@ export function TabBar() {
className={cn(
"group relative flex h-7 shrink-0 cursor-pointer items-center gap-0.5 rounded-t-md border border-b-0 px-3 select-none",
isActive
- ? "text-foreground z-10 -mb-px h-[30px] bg-white"
- : "bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-transparent",
+ ? "text-primary z-10 -mb-px h-[30px] bg-primary/15 dark:bg-primary/20 border-primary/40 border-t-[3px] border-t-primary font-semibold"
+ : "bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground border-transparent",
)}
style={{
width: TAB_WIDTH,
diff --git a/frontend/components/numbering-rule/AutoConfigPanel.tsx b/frontend/components/numbering-rule/AutoConfigPanel.tsx
index ac4d2ffa..0586006a 100644
--- a/frontend/components/numbering-rule/AutoConfigPanel.tsx
+++ b/frontend/components/numbering-rule/AutoConfigPanel.tsx
@@ -478,7 +478,7 @@ const DateConfigPanel: React.FC
= ({
{sourceTableName && columns.length === 0 && !loadingColumns && (
-
+
이 테이블에 날짜 타입 컬럼이 없습니다
)}
diff --git a/frontend/components/numbering-rule/NumberingRuleCard.tsx b/frontend/components/numbering-rule/NumberingRuleCard.tsx
index e3dbc3ab..6a941eda 100644
--- a/frontend/components/numbering-rule/NumberingRuleCard.tsx
+++ b/frontend/components/numbering-rule/NumberingRuleCard.tsx
@@ -1,7 +1,6 @@
"use client";
import React from "react";
-import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@@ -27,25 +26,24 @@ export const NumberingRuleCard: React.FC = ({
tableName,
}) => {
return (
-
-
-
-
- 규칙 {part.order}
-
-
-
-
+
+
+
+ 규칙 {part.order}
+
+
+
-
+
+
);
};
diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
index 406cd009..f6856823 100644
--- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
+++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx
@@ -5,28 +5,16 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Plus, Save, Edit2, FolderTree } from "lucide-react";
+import { Plus, Save, ListOrdered } from "lucide-react";
import { toast } from "sonner";
import { showErrorToast } from "@/lib/utils/toastUtils";
import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorType } from "@/types/numbering-rule";
+import { CODE_PART_TYPE_OPTIONS } from "@/types/numbering-rule";
import { NumberingRuleCard } from "./NumberingRuleCard";
-import { NumberingRulePreview } from "./NumberingRulePreview";
-import { saveNumberingRuleToTest } from "@/lib/api/numberingRule";
-import { apiClient } from "@/lib/api/client";
+import { NumberingRulePreview, computePartDisplayItems, getPartTypeColorClass } from "./NumberingRulePreview";
+import { getNumberingRules, saveNumberingRuleToTest } from "@/lib/api/numberingRule";
import { cn } from "@/lib/utils";
-interface NumberingColumn {
- tableName: string;
- tableLabel: string;
- columnName: string;
- columnLabel: string;
-}
-
-interface GroupedColumns {
- tableLabel: string;
- columns: NumberingColumn[];
-}
-
interface NumberingRuleDesignerProps {
initialConfig?: NumberingRuleConfig;
onSave?: (config: NumberingRuleConfig) => void;
@@ -34,8 +22,8 @@ interface NumberingRuleDesignerProps {
maxRules?: number;
isPreview?: boolean;
className?: string;
- currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용)
- menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프)
+ currentTableName?: string;
+ menuObjid?: number;
}
export const NumberingRuleDesigner: React.FC = ({
@@ -48,124 +36,84 @@ export const NumberingRuleDesigner: React.FC = ({
currentTableName,
menuObjid,
}) => {
- const [numberingColumns, setNumberingColumns] = useState([]);
- const [selectedColumn, setSelectedColumn] = useState<{ tableName: string; columnName: string } | null>(null);
+ const [rulesList, setRulesList] = useState([]);
+ const [selectedRuleId, setSelectedRuleId] = useState(null);
const [currentRule, setCurrentRule] = useState(null);
+ const [selectedPartOrder, setSelectedPartOrder] = useState(null);
const [loading, setLoading] = useState(false);
- const [columnSearch, setColumnSearch] = useState("");
- const [rightTitle, setRightTitle] = useState("규칙 편집");
- const [editingRightTitle, setEditingRightTitle] = useState(false);
-
- // 구분자 관련 상태 (개별 파트 사이 구분자)
const [separatorTypes, setSeparatorTypes] = useState>({});
const [customSeparators, setCustomSeparators] = useState>({});
- // 좌측: 채번 타입 컬럼 목록 로드
+ const selectedRule = rulesList.find((r) => r.ruleId === selectedRuleId) ?? currentRule;
+
+ // 좌측: 규칙 목록 로드
useEffect(() => {
- loadNumberingColumns();
+ loadRules();
}, []);
- const loadNumberingColumns = async () => {
+ const loadRules = async () => {
setLoading(true);
try {
- const response = await apiClient.get("/table-management/numbering-columns");
- if (response.data.success && response.data.data) {
- setNumberingColumns(response.data.data);
+ const response = await getNumberingRules();
+ if (response.success && response.data) {
+ setRulesList(response.data);
+ if (response.data.length > 0 && !selectedRuleId) {
+ const first = response.data[0];
+ setSelectedRuleId(first.ruleId);
+ setCurrentRule(JSON.parse(JSON.stringify(first)));
+ }
}
- } catch (error: any) {
- console.error("채번 컬럼 목록 로드 실패:", error);
+ } catch (e) {
+ console.error("채번 규칙 목록 로드 실패:", e);
} finally {
setLoading(false);
}
};
- // 컬럼 선택 시 해당 컬럼의 채번 규칙 로드
- const handleSelectColumn = async (tableName: string, columnName: string) => {
- setSelectedColumn({ tableName, columnName });
- setLoading(true);
- try {
- const response = await apiClient.get(`/numbering-rules/by-column/${tableName}/${columnName}`);
- if (response.data.success && response.data.data) {
- const rule = response.data.data as NumberingRuleConfig;
- setCurrentRule(JSON.parse(JSON.stringify(rule)));
- } else {
- // 규칙 없으면 신규 생성 모드
- const newRule: NumberingRuleConfig = {
- ruleId: `rule-${Date.now()}`,
- ruleName: `${columnName} 채번`,
- parts: [],
- separator: "-",
- resetPeriod: "none",
- currentSequence: 1,
- scopeType: "table",
- tableName,
- columnName,
- };
- setCurrentRule(newRule);
- }
- } catch {
- const newRule: NumberingRuleConfig = {
- ruleId: `rule-${Date.now()}`,
- ruleName: `${columnName} 채번`,
- parts: [],
- separator: "-",
- resetPeriod: "none",
- currentSequence: 1,
- scopeType: "table",
- tableName,
- columnName,
- };
- setCurrentRule(newRule);
- } finally {
- setLoading(false);
- }
+ const handleSelectRule = (rule: NumberingRuleConfig) => {
+ setSelectedRuleId(rule.ruleId);
+ setCurrentRule(JSON.parse(JSON.stringify(rule)));
+ setSelectedPartOrder(null);
};
- // 테이블별로 그룹화
- const groupedColumns = numberingColumns.reduce>((acc, col) => {
- if (!acc[col.tableName]) {
- acc[col.tableName] = { tableLabel: col.tableLabel, columns: [] };
- }
- acc[col.tableName].columns.push(col);
- return acc;
- }, {});
-
- // 검색 필터 적용
- const filteredGroups = Object.entries(groupedColumns).filter(([tableName, group]) => {
- if (!columnSearch) return true;
- const search = columnSearch.toLowerCase();
- return (
- tableName.toLowerCase().includes(search) ||
- group.tableLabel.toLowerCase().includes(search) ||
- group.columns.some(
- (c) => c.columnName.toLowerCase().includes(search) || c.columnLabel.toLowerCase().includes(search)
- )
- );
- });
+ const handleAddNewRule = () => {
+ const newRule: NumberingRuleConfig = {
+ ruleId: `rule-${Date.now()}`,
+ ruleName: "새 규칙",
+ parts: [],
+ separator: "-",
+ resetPeriod: "none",
+ currentSequence: 1,
+ scopeType: "global",
+ tableName: currentTableName ?? "",
+ columnName: "",
+ };
+ setRulesList((prev) => [...prev, newRule]);
+ setSelectedRuleId(newRule.ruleId);
+ setCurrentRule(JSON.parse(JSON.stringify(newRule)));
+ setSelectedPartOrder(null);
+ toast.success("새 규칙이 추가되었습니다");
+ };
useEffect(() => {
- if (currentRule) {
- onChange?.(currentRule);
- }
+ if (currentRule) onChange?.(currentRule);
}, [currentRule, onChange]);
- // currentRule이 변경될 때 파트별 구분자 상태 동기화
useEffect(() => {
if (currentRule && currentRule.parts.length > 0) {
const newSepTypes: Record = {};
const newCustomSeps: Record = {};
-
currentRule.parts.forEach((part) => {
const sep = part.separatorAfter ?? currentRule.separator ?? "-";
if (sep === "") {
newSepTypes[part.order] = "none";
newCustomSeps[part.order] = "";
} else {
- const predefinedOption = SEPARATOR_OPTIONS.find(
- opt => opt.value !== "custom" && opt.value !== "none" && opt.displayValue === sep
+ const opt = SEPARATOR_OPTIONS.find(
+ (o) => o.value !== "custom" && o.value !== "none" && o.displayValue === sep
);
- if (predefinedOption) {
- newSepTypes[part.order] = predefinedOption.value;
+ if (opt) {
+ newSepTypes[part.order] = opt.value;
newCustomSeps[part.order] = "";
} else {
newSepTypes[part.order] = "custom";
@@ -173,54 +121,45 @@ export const NumberingRuleDesigner: React.FC = ({
}
}
});
-
setSeparatorTypes(newSepTypes);
setCustomSeparators(newCustomSeps);
}
}, [currentRule?.ruleId]);
- // 개별 파트 구분자 변경 핸들러
const handlePartSeparatorChange = useCallback((partOrder: number, type: SeparatorType) => {
- setSeparatorTypes(prev => ({ ...prev, [partOrder]: type }));
+ setSeparatorTypes((prev) => ({ ...prev, [partOrder]: type }));
if (type !== "custom") {
- const option = SEPARATOR_OPTIONS.find(opt => opt.value === type);
+ const option = SEPARATOR_OPTIONS.find((opt) => opt.value === type);
const newSeparator = option?.displayValue ?? "";
- setCustomSeparators(prev => ({ ...prev, [partOrder]: "" }));
+ setCustomSeparators((prev) => ({ ...prev, [partOrder]: "" }));
setCurrentRule((prev) => {
if (!prev) return null;
return {
...prev,
- parts: prev.parts.map((part) =>
- part.order === partOrder ? { ...part, separatorAfter: newSeparator } : part
- ),
+ parts: prev.parts.map((p) => (p.order === partOrder ? { ...p, separatorAfter: newSeparator } : p)),
};
});
}
}, []);
- // 개별 파트 직접 입력 구분자 변경 핸들러
const handlePartCustomSeparatorChange = useCallback((partOrder: number, value: string) => {
const trimmedValue = value.slice(0, 2);
- setCustomSeparators(prev => ({ ...prev, [partOrder]: trimmedValue }));
+ setCustomSeparators((prev) => ({ ...prev, [partOrder]: trimmedValue }));
setCurrentRule((prev) => {
if (!prev) return null;
return {
...prev,
- parts: prev.parts.map((part) =>
- part.order === partOrder ? { ...part, separatorAfter: trimmedValue } : part
- ),
+ parts: prev.parts.map((p) => (p.order === partOrder ? { ...p, separatorAfter: trimmedValue } : p)),
};
});
}, []);
const handleAddPart = useCallback(() => {
if (!currentRule) return;
-
if (currentRule.parts.length >= maxRules) {
toast.error(`최대 ${maxRules}개까지 추가할 수 있습니다`);
return;
}
-
const newPart: NumberingRulePart = {
id: `part-${Date.now()}`,
order: currentRule.parts.length + 1,
@@ -229,40 +168,33 @@ export const NumberingRuleDesigner: React.FC = ({
autoConfig: { textValue: "CODE" },
separatorAfter: "-",
};
-
- setCurrentRule((prev) => {
- if (!prev) return null;
- return { ...prev, parts: [...prev.parts, newPart] };
- });
-
- // 새 파트의 구분자 상태 초기화
- setSeparatorTypes(prev => ({ ...prev, [newPart.order]: "-" }));
- setCustomSeparators(prev => ({ ...prev, [newPart.order]: "" }));
-
+ setCurrentRule((prev) => (prev ? { ...prev, parts: [...prev.parts, newPart] } : null));
+ setSeparatorTypes((prev) => ({ ...prev, [newPart.order]: "-" }));
+ setCustomSeparators((prev) => ({ ...prev, [newPart.order]: "" }));
toast.success(`규칙 ${newPart.order}가 추가되었습니다`);
}, [currentRule, maxRules]);
- // partOrder 기반으로 파트 업데이트 (id가 null일 수 있으므로 order 사용)
const handleUpdatePart = useCallback((partOrder: number, updates: Partial) => {
setCurrentRule((prev) => {
if (!prev) return null;
return {
...prev,
- parts: prev.parts.map((part) => (part.order === partOrder ? { ...part, ...updates } : part)),
+ parts: prev.parts.map((p) => (p.order === partOrder ? { ...p, ...updates } : p)),
};
});
}, []);
- // partOrder 기반으로 파트 삭제 (id가 null일 수 있으므로 order 사용)
const handleDeletePart = useCallback((partOrder: number) => {
setCurrentRule((prev) => {
if (!prev) return null;
return {
...prev,
- parts: prev.parts.filter((part) => part.order !== partOrder).map((part, index) => ({ ...part, order: index + 1 })),
+ parts: prev.parts
+ .filter((p) => p.order !== partOrder)
+ .map((p, i) => ({ ...p, order: i + 1 })),
};
});
-
+ setSelectedPartOrder(null);
toast.success("규칙이 삭제되었습니다");
}, []);
@@ -271,246 +203,283 @@ export const NumberingRuleDesigner: React.FC = ({
toast.error("저장할 규칙이 없습니다");
return;
}
-
if (currentRule.parts.length === 0) {
toast.error("최소 1개 이상의 규칙을 추가해주세요");
return;
}
-
setLoading(true);
try {
- // 파트별 기본 autoConfig 정의
const defaultAutoConfigs: Record = {
sequence: { sequenceLength: 3, startFrom: 1 },
number: { numberLength: 4, numberValue: 1 },
date: { dateFormat: "YYYYMMDD" },
text: { textValue: "" },
};
-
- // 저장 전에 각 파트의 autoConfig에 기본값 채우기
const partsWithDefaults = currentRule.parts.map((part) => {
if (part.generationMethod === "auto") {
const defaults = defaultAutoConfigs[part.partType] || {};
- return {
- ...part,
- autoConfig: { ...defaults, ...part.autoConfig },
- };
+ return { ...part, autoConfig: { ...defaults, ...part.autoConfig } };
}
return part;
});
-
const ruleToSave = {
...currentRule,
parts: partsWithDefaults,
- scopeType: "table" as const,
- tableName: selectedColumn?.tableName || currentRule.tableName || "",
- columnName: selectedColumn?.columnName || currentRule.columnName || "",
+ scopeType: "global" as const,
+ tableName: currentRule.tableName || currentTableName || "",
+ columnName: currentRule.columnName || "",
};
-
- // 테스트 테이블에 저장 (numbering_rules)
const response = await saveNumberingRuleToTest(ruleToSave);
-
if (response.success && response.data) {
- const currentData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig;
- setCurrentRule(currentData);
+ const saved: NumberingRuleConfig = JSON.parse(JSON.stringify(response.data));
+ setCurrentRule(saved);
+ setRulesList((prev) => {
+ const idx = prev.findIndex((r) => r.ruleId === currentRule.ruleId);
+ if (idx >= 0) {
+ const next = [...prev];
+ next[idx] = saved;
+ return next;
+ }
+ return [...prev, saved];
+ });
+ setSelectedRuleId(saved.ruleId);
await onSave?.(response.data);
toast.success("채번 규칙이 저장되었습니다");
} else {
- showErrorToast("채번 규칙 저장에 실패했습니다", response.error, { guidance: "설정을 확인하고 다시 시도해 주세요." });
+ showErrorToast("채번 규칙 저장에 실패했습니다", response.error, {
+ guidance: "설정을 확인하고 다시 시도해 주세요.",
+ });
}
- } catch (error: any) {
- showErrorToast("채번 규칙 저장에 실패했습니다", error, { guidance: "설정을 확인하고 다시 시도해 주세요." });
+ } catch (error: unknown) {
+ showErrorToast("채번 규칙 저장에 실패했습니다", error, {
+ guidance: "설정을 확인하고 다시 시도해 주세요.",
+ });
} finally {
setLoading(false);
}
- }, [currentRule, onSave, selectedColumn]);
+ }, [currentRule, onSave, currentTableName]);
+
+ const selectedPart = currentRule?.parts.find((p) => p.order === selectedPartOrder) ?? null;
+ const globalSep = currentRule?.separator ?? "-";
+ const partItems = currentRule ? computePartDisplayItems(currentRule) : [];
return (
-
- {/* 좌측: 채번 컬럼 목록 (카테고리 패턴) */}
-
-
채번 컬럼
-
-
setColumnSearch(e.target.value)}
- placeholder="검색..."
- className="h-8 text-xs"
- />
-
-
- {loading && numberingColumns.length === 0 ? (
-
-
로딩 중...
+
+ {/* 좌측: 규칙 리스트 (code-nav, 220px) */}
+
+
+
+
+ 채번 규칙 ({rulesList.length})
+
+
+
+
+ {loading && rulesList.length === 0 ? (
+
+ 로딩 중...
- ) : filteredGroups.length === 0 ? (
-
-
- {numberingColumns.length === 0
- ? "채번 타입 컬럼이 없습니다"
- : "검색 결과가 없습니다"}
-
+ ) : rulesList.length === 0 ? (
+
+ 규칙이 없습니다
) : (
- filteredGroups.map(([tableName, group]) => (
-
-
-
- {group.tableLabel}
- ({group.columns.length})
-
- {group.columns.map((col) => {
- const isSelected =
- selectedColumn?.tableName === col.tableName &&
- selectedColumn?.columnName === col.columnName;
- return (
-
handleSelectColumn(col.tableName, col.columnName)}
- >
- {col.columnLabel}
-
- );
- })}
-
- ))
+ rulesList.map((rule) => {
+ const isSelected = selectedRuleId === rule.ruleId;
+ return (
+
+ );
+ })
)}
- {/* 구분선 */}
-
-
- {/* 우측: 편집 영역 */}
-
+ {/* 우측: 미리보기 + 파이프라인 + 설정 + 저장 바 (code-main) */}
+
{!currentRule ? (
-
-
-
-
컬럼을 선택해주세요
-
좌측에서 채번 컬럼을 선택하면 규칙을 편집할 수 있습니다
-
+
+
+
규칙을 선택하세요
+
+ 좌측에서 채번 규칙을 선택하거나 "추가"로 새 규칙을 만드세요
+
) : (
<>
-
- {editingRightTitle ? (
-
setRightTitle(e.target.value)}
- onBlur={() => setEditingRightTitle(false)}
- onKeyDown={(e) => e.key === "Enter" && setEditingRightTitle(false)}
- className="h-8 text-sm font-semibold"
- autoFocus
- />
- ) : (
-
{rightTitle}
- )}
-
+
+
+ setCurrentRule((prev) => (prev ? { ...prev, ruleName: e.target.value } : null))}
+ placeholder="예: 프로젝트 코드"
+ className="h-9 text-sm"
+ />
-
- {/* 첫 번째 줄: 규칙명 + 미리보기 */}
-
-
-
- setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))}
- className="h-9"
- placeholder="예: 프로젝트 코드"
- />
-
-
-
-
-
-
-
-
+ {/* 큰 미리보기 스트립 (code-preview-strip) */}
+
+
-
-
-
코드 구성
-
+ {/* 파이프라인 영역 (code-pipeline-area) */}
+
+
+ 코드 구성
+
{currentRule.parts.length}/{maxRules}
-
- {currentRule.parts.length === 0 ? (
-
- ) : (
-
- {currentRule.parts.map((part, index) => (
-
-
-
handleUpdatePart(part.order, updates)}
- onDelete={() => handleDeletePart(part.order)}
- isPreview={isPreview}
- tableName={selectedColumn?.tableName}
- />
- {/* 카드 하단에 구분자 설정 (마지막 파트 제외) */}
- {index < currentRule.parts.length - 1 && (
-
-
뒤 구분자
-
- {separatorTypes[part.order] === "custom" && (
-
handlePartCustomSeparatorChange(part.order, e.target.value)}
- className="h-6 w-14 text-center text-[10px]"
- placeholder="2자"
- maxLength={2}
- />
+
+ {currentRule.parts.length === 0 ? (
+
+ 규칙을 추가하여 코드를 구성하세요
+
+ ) : (
+ <>
+ {currentRule.parts.map((part, index) => {
+ const item = partItems.find((i) => i.order === part.order);
+ const sep = part.separatorAfter ?? globalSep;
+ const isSelected = selectedPartOrder === part.order;
+ const typeLabel = CODE_PART_TYPE_OPTIONS.find((o) => o.value === part.partType)?.label ?? part.partType;
+ return (
+
+
-
- ))}
-
- )}
+ onClick={() => setSelectedPartOrder(part.order)}
+ >
+
+ {typeLabel}
+
+
+ {item?.displayValue ?? "-"}
+
+
+ {index < currentRule.parts.length - 1 && (
+
+ →
+
+ {sep || "-"}
+
+
+ )}
+
+ );
+ })}
+
+ >
+ )}
+
-
+ {/* 설정 패널 (선택된 세그먼트 상세, code-config-panel) */}
+ {selectedPart && (
+
+
+ handleUpdatePart(selectedPart.order, updates)}
+ onDelete={() => handleDeletePart(selectedPart.order)}
+ isPreview={isPreview}
+ tableName={currentRule.tableName ?? currentTableName}
+ />
+
+ {currentRule.parts.some((p) => p.order === selectedPart.order) && (
+
+ 뒤 구분자
+
+ {separatorTypes[selectedPart.order] === "custom" && (
+ handlePartCustomSeparatorChange(selectedPart.order, e.target.value)}
+ className="h-7 w-14 text-center text-[10px]"
+ placeholder="2자"
+ maxLength={2}
+ />
+ )}
+
+ )}
+
+ )}
+
+ {/* 저장 바 (code-save-bar) */}
+
+
+ {currentRule.tableName && (
+ 테이블: {currentRule.tableName}
+ )}
+ {currentRule.columnName && (
+ 컬럼: {currentRule.columnName}
+ )}
+ 구분자: {globalSep || "-"}
+ {currentRule.resetPeriod && currentRule.resetPeriod !== "none" && (
+ 리셋: {currentRule.resetPeriod}
+ )}
+
-
diff --git a/frontend/components/numbering-rule/NumberingRulePreview.tsx b/frontend/components/numbering-rule/NumberingRulePreview.tsx
index eff551a1..6a7f9732 100644
--- a/frontend/components/numbering-rule/NumberingRulePreview.tsx
+++ b/frontend/components/numbering-rule/NumberingRulePreview.tsx
@@ -1,88 +1,163 @@
"use client";
import React, { useMemo } from "react";
-import { NumberingRuleConfig } from "@/types/numbering-rule";
+import { cn } from "@/lib/utils";
+import { NumberingRuleConfig, NumberingRulePart, CodePartType } from "@/types/numbering-rule";
+import { CODE_PART_TYPE_OPTIONS } from "@/types/numbering-rule";
+
+/** 파트별 표시값 + 타입 (미리보기 스트립/세그먼트용) */
+export interface PartDisplayItem {
+ partType: CodePartType;
+ displayValue: string;
+ order: number;
+}
+
+/** config에서 파트별 표시값 배열 계산 (정렬된 parts 기준) */
+export function computePartDisplayItems(config: NumberingRuleConfig): PartDisplayItem[] {
+ if (!config.parts || config.parts.length === 0) return [];
+ const sorted = [...config.parts].sort((a, b) => a.order - b.order);
+ const globalSep = config.separator ?? "-";
+ return sorted.map((part) => ({
+ order: part.order,
+ partType: part.partType,
+ displayValue: getPartDisplayValue(part),
+ }));
+}
+
+function getPartDisplayValue(part: NumberingRulePart): string {
+ if (part.generationMethod === "manual") {
+ return part.manualConfig?.value || "XXX";
+ }
+ const c = part.autoConfig || {};
+ switch (part.partType) {
+ case "sequence":
+ return String(c.startFrom ?? 1).padStart(c.sequenceLength ?? 3, "0");
+ case "number":
+ return String(c.numberValue ?? 0).padStart(c.numberLength ?? 4, "0");
+ case "date": {
+ const format = c.dateFormat || "YYYYMMDD";
+ if (c.useColumnValue && c.sourceColumnName) {
+ return format === "YYYY" ? "[YYYY]" : format === "YY" ? "[YY]" : format === "YYYYMM" ? "[YYYYMM]" : format === "YYMM" ? "[YYMM]" : format === "YYMMDD" ? "[YYMMDD]" : "[DATE]";
+ }
+ const now = new Date();
+ const y = now.getFullYear();
+ const m = String(now.getMonth() + 1).padStart(2, "0");
+ const d = String(now.getDate()).padStart(2, "0");
+ if (format === "YYYY") return String(y);
+ if (format === "YY") return String(y).slice(-2);
+ if (format === "YYYYMM") return `${y}${m}`;
+ if (format === "YYMM") return `${String(y).slice(-2)}${m}`;
+ if (format === "YYYYMMDD") return `${y}${m}${d}`;
+ if (format === "YYMMDD") return `${String(y).slice(-2)}${m}${d}`;
+ return `${y}${m}${d}`;
+ }
+ case "text":
+ return c.textValue || "TEXT";
+ default:
+ return "XXX";
+ }
+}
+
+/** 파트 타입별 미리보기용 텍스트 색상 클래스 (CSS 변수 기반) */
+export function getPartTypeColorClass(partType: CodePartType): string {
+ switch (partType) {
+ case "date":
+ return "text-warning";
+ case "text":
+ return "text-primary";
+ case "sequence":
+ return "text-primary";
+ case "number":
+ return "text-muted-foreground";
+ case "category":
+ case "reference":
+ return "text-muted-foreground";
+ default:
+ return "text-foreground";
+ }
+}
+
+/** 파트 타입별 점(dot) 배경 색상 (범례용) */
+export function getPartTypeDotClass(partType: CodePartType): string {
+ switch (partType) {
+ case "date":
+ return "bg-warning";
+ case "text":
+ case "sequence":
+ return "bg-primary";
+ case "number":
+ case "category":
+ case "reference":
+ return "bg-muted-foreground";
+ default:
+ return "bg-foreground";
+ }
+}
interface NumberingRulePreviewProps {
config: NumberingRuleConfig;
compact?: boolean;
+ /** 큰 미리보기 스트립: 28px, 파트별 색상, 하단 범례 */
+ variant?: "default" | "strip";
}
export const NumberingRulePreview: React.FC
= ({
config,
- compact = false
+ compact = false,
+ variant = "default",
}) => {
+ const partItems = useMemo(() => computePartDisplayItems(config), [config]);
+ const sortedParts = useMemo(
+ () => (config.parts ? [...config.parts].sort((a, b) => a.order - b.order) : []),
+ [config.parts]
+ );
const generatedCode = useMemo(() => {
- if (!config.parts || config.parts.length === 0) {
- return "규칙을 추가해주세요";
- }
-
- const sortedParts = config.parts.sort((a, b) => a.order - b.order);
-
- const partValues = sortedParts.map((part) => {
- if (part.generationMethod === "manual") {
- return part.manualConfig?.value || "XXX";
- }
-
- const autoConfig = part.autoConfig || {};
-
- switch (part.partType) {
- case "sequence": {
- const length = autoConfig.sequenceLength || 3;
- const startFrom = autoConfig.startFrom || 1;
- return String(startFrom).padStart(length, "0");
- }
- case "number": {
- const length = autoConfig.numberLength || 4;
- const value = autoConfig.numberValue || 0;
- return String(value).padStart(length, "0");
- }
- case "date": {
- const format = autoConfig.dateFormat || "YYYYMMDD";
- if (autoConfig.useColumnValue && autoConfig.sourceColumnName) {
- switch (format) {
- case "YYYY": return "[YYYY]";
- case "YY": return "[YY]";
- case "YYYYMM": return "[YYYYMM]";
- case "YYMM": return "[YYMM]";
- case "YYYYMMDD": return "[YYYYMMDD]";
- case "YYMMDD": return "[YYMMDD]";
- default: return "[DATE]";
- }
- }
- const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, "0");
- const day = String(now.getDate()).padStart(2, "0");
- switch (format) {
- case "YYYY": return String(year);
- case "YY": return String(year).slice(-2);
- case "YYYYMM": return `${year}${month}`;
- case "YYMM": return `${String(year).slice(-2)}${month}`;
- case "YYYYMMDD": return `${year}${month}${day}`;
- case "YYMMDD": return `${String(year).slice(-2)}${month}${day}`;
- default: return `${year}${month}${day}`;
- }
- }
- case "text":
- return autoConfig.textValue || "TEXT";
- default:
- return "XXX";
- }
- });
-
- // 파트별 개별 구분자로 결합
+ if (partItems.length === 0) return "규칙을 추가해주세요";
const globalSep = config.separator ?? "-";
let result = "";
- partValues.forEach((val, idx) => {
- result += val;
- if (idx < partValues.length - 1) {
- const sep = sortedParts[idx].separatorAfter ?? globalSep;
- result += sep;
+ partItems.forEach((item, idx) => {
+ result += item.displayValue;
+ if (idx < partItems.length - 1) {
+ const part = sortedParts.find((p) => p.order === item.order);
+ result += part?.separatorAfter ?? globalSep;
}
});
return result;
- }, [config]);
+ }, [config.separator, partItems, sortedParts]);
+
+ if (variant === "strip") {
+ const globalSep = config.separator ?? "-";
+ return (
+
+
+ {partItems.length === 0 ? (
+ 규칙을 추가해주세요
+ ) : (
+ partItems.map((item, idx) => (
+
+ {item.displayValue}
+ {idx < partItems.length - 1 && (
+
+ {sortedParts.find((p) => p.order === item.order)?.separatorAfter ?? globalSep}
+
+ )}
+
+ ))
+ )}
+
+ {partItems.length > 0 && (
+
+ {CODE_PART_TYPE_OPTIONS.filter((opt) => partItems.some((p) => p.partType === opt.value)).map((opt) => (
+
+
+ {opt.label}
+
+ ))}
+
+ )}
+
+ );
+ }
if (compact) {
return (
diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx
index 51a9e34e..87579061 100644
--- a/frontend/components/screen/RealtimePreviewDynamic.tsx
+++ b/frontend/components/screen/RealtimePreviewDynamic.tsx
@@ -362,15 +362,15 @@ const RealtimePreviewDynamicComponent: React.FC = ({
// 런타임 모드에서 컴포넌트 타입별 높이 처리
if (!isDesignMode) {
const compType = (component as any).componentType || component.componentConfig?.type || "";
- // 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1)
- const flexGrowTypes = [
+ // 레이아웃 계열: 부모 래퍼를 꽉 채움 (ResponsiveGridRenderer가 % 높이 관리)
+ const fillParentTypes = [
"table-list", "v2-table-list",
"split-panel-layout", "split-panel-layout2",
"v2-split-panel-layout", "screen-split-panel",
"v2-tab-container", "tab-container",
"tabs-widget", "v2-tabs-widget",
];
- if (flexGrowTypes.some(t => compType === t)) {
+ if (fillParentTypes.some(t => compType === t)) {
return "100%";
}
const autoHeightTypes = [
diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx
index 1322ee99..47a2cd52 100644
--- a/frontend/components/screen/ResponsiveGridRenderer.tsx
+++ b/frontend/components/screen/ResponsiveGridRenderer.tsx
@@ -23,8 +23,9 @@ function getComponentTypeId(component: ComponentData): string {
}
/**
- * 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링.
- * 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대.
+ * 디자이너 절대좌표를 캔버스 대비 비율(%)로 변환하여 렌더링.
+ * 가로: 컨테이너 너비 대비 % → 반응형 스케일
+ * 세로: 컨테이너 높이 대비 % → 뷰포트에 맞게 자동 조절
*/
function ProportionalRenderer({
components,
@@ -47,19 +48,12 @@ function ProportionalRenderer({
}, []);
const topLevel = components.filter((c) => !c.parentId);
- const ratio = containerW > 0 ? containerW / canvasWidth : 1;
-
- const maxBottom = topLevel.reduce((max, c) => {
- const bottom = c.position.y + (c.size?.height || 40);
- return Math.max(max, bottom);
- }, 0);
return (
0 ? `${maxBottom * ratio}px` : "200px" }}
+ className="bg-background relative h-full w-full overflow-hidden"
>
{containerW > 0 &&
topLevel.map((component) => {
@@ -72,9 +66,9 @@ function ProportionalRenderer({
style={{
position: "absolute",
left: `${(component.position.x / canvasWidth) * 100}%`,
- top: `${component.position.y * ratio}px`,
+ top: `${(component.position.y / canvasHeight) * 100}%`,
width: `${((component.size?.width || 100) / canvasWidth) * 100}%`,
- height: `${(component.size?.height || 40) * ratio}px`,
+ height: `${((component.size?.height || 40) / canvasHeight) * 100}%`,
zIndex: component.position.z || 1,
}}
>
diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx
index ef739b27..a18f3bda 100644
--- a/frontend/components/screen/panels/V2PropertiesPanel.tsx
+++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx
@@ -17,7 +17,6 @@ import {
GroupComponent,
DataTableComponent,
TableInfo,
- LayoutComponent,
FileComponent,
AreaComponent,
} from "@/types/screen";
@@ -47,7 +46,7 @@ import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
-import { columnMetaCache } from "@/lib/registry/DynamicComponentRenderer";
+import { columnMetaCache, loadColumnMeta } from "@/lib/registry/DynamicComponentRenderer";
import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
import StyleEditor from "../StyleEditor";
import { Slider } from "@/components/ui/slider";
@@ -98,6 +97,24 @@ export const V2PropertiesPanel: React.FC
= ({
// 🆕 전체 테이블 목록 (selected-items-detail-input 등에서 사용)
const [allTables, setAllTables] = useState>([]);
+ // 🆕 선택된 컴포넌트의 테이블에 대한 columnMeta 캐시가 비어 있으면 로드 후 재렌더
+ const [columnMetaVersion, setColumnMetaVersion] = useState(0);
+ useEffect(() => {
+ if (!selectedComponent) return;
+ const tblName =
+ (selectedComponent as any).tableName ||
+ currentTable?.tableName ||
+ tables?.[0]?.tableName;
+ if (!tblName) return;
+ if (columnMetaCache[tblName]) return;
+ loadColumnMeta(tblName).then(() => setColumnMetaVersion((v) => v + 1));
+ }, [
+ selectedComponent?.id,
+ (selectedComponent as any)?.tableName,
+ currentTable?.tableName,
+ tables?.[0]?.tableName,
+ ]);
+
// 🆕 전체 테이블 목록 로드
useEffect(() => {
const loadAllTables = async () => {
@@ -211,20 +228,20 @@ export const V2PropertiesPanel: React.FC = ({
// 현재 화면의 테이블명 가져오기
const currentTableName = tables?.[0]?.tableName;
- // DB input_type 가져오기 (columnMetaCache에서 최신값 조회)
- const colName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName;
- const tblName = selectedComponent.tableName || currentTable?.tableName || currentTableName;
+ // DB input_type만 조회 (saved config와 분리하여 전달)
+ const colName = (selectedComponent as any).columnName || currentConfig.fieldKey || currentConfig.columnName;
+ const tblName = (selectedComponent as any).tableName || currentTable?.tableName || currentTableName;
const dbMeta = colName && tblName && !colName.includes(".") ? columnMetaCache[tblName]?.[colName] : undefined;
const dbInputType = dbMeta ? (() => { const raw = dbMeta.input_type || dbMeta.inputType; return raw === "direct" || raw === "auto" ? undefined : raw; })() : undefined;
- const inputType = dbInputType || currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType;
// 컴포넌트별 추가 props
const extraProps: Record = {};
- const resolvedTableName = selectedComponent.tableName || currentTable?.tableName || currentTableName;
- const resolvedColumnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName;
+ const resolvedTableName = (selectedComponent as any).tableName || currentTable?.tableName || currentTableName;
+ const resolvedColumnName = (selectedComponent as any).columnName || currentConfig.fieldKey || currentConfig.columnName;
if (componentId === "v2-input" || componentId === "v2-select") {
- extraProps.inputType = inputType;
+ extraProps.componentType = componentId;
+ extraProps.inputType = dbInputType;
extraProps.tableName = resolvedTableName;
extraProps.columnName = resolvedColumnName;
extraProps.screenTableName = resolvedTableName;
@@ -256,7 +273,7 @@ export const V2PropertiesPanel: React.FC = ({
const currentConfig = selectedComponent.componentConfig || {};
// 🔧 ConfigPanelWrapper를 인라인 함수 대신 직접 JSX 반환 (리마운트 방지)
- const config = currentConfig || definition.defaultProps?.componentConfig || {};
+ const config = currentConfig || (definition as any).defaultProps?.componentConfig || {};
const handlePanelConfigChange = (newConfig: any) => {
// 🔧 Partial 업데이트: 기존 componentConfig를 유지하면서 새 설정만 병합
@@ -282,14 +299,14 @@ export const V2PropertiesPanel: React.FC = ({
onConfigChange={handlePanelConfigChange}
tables={tables}
allTables={allTables}
- screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
- tableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
+ screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
+ tableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
columnName={
(selectedComponent as any).columnName || currentConfig?.columnName || currentConfig?.fieldName
}
inputType={(selectedComponent as any).inputType || currentConfig?.inputType}
componentType={componentType}
- tableColumns={currentTable?.columns || []}
+ tableColumns={(currentTable as any)?.columns || []}
allComponents={allComponents}
currentComponent={selectedComponent}
menuObjid={menuObjid}
@@ -323,8 +340,8 @@ export const V2PropertiesPanel: React.FC = ({
componentType={componentType}
config={selectedComponent.componentConfig || {}}
onChange={handleDynamicConfigChange}
- screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
- tableColumns={currentTable?.columns || []}
+ screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
+ tableColumns={(currentTable as any)?.columns || []}
tables={tables}
menuObjid={menuObjid}
allComponents={allComponents}
@@ -491,7 +508,7 @@ export const V2PropertiesPanel: React.FC = ({
제목
handleUpdate("title", e.target.value)}
placeholder="제목"
className="h-7 text-xs"
@@ -503,7 +520,7 @@ export const V2PropertiesPanel: React.FC
= ({
설명
handleUpdate("description", e.target.value)}
placeholder="설명"
className="h-7 text-xs"
@@ -519,9 +536,9 @@ export const V2PropertiesPanel: React.FC
= ({
OPTIONS
{(isInputField || widget.required !== undefined) &&
(() => {
- const colName = widget.columnName || selectedComponent?.columnName;
+ const colName = widget.columnName || (selectedComponent as any)?.columnName;
const colMeta = colName
- ? currentTable?.columns?.find(
+ ? (currentTable as any)?.columns?.find(
(c: any) => (c.columnName || c.column_name || "").toLowerCase() === colName.toLowerCase(),
)
: null;
@@ -568,7 +585,7 @@ export const V2PropertiesPanel: React.FC = ({
숨김
{
handleUpdate("hidden", checked);
handleUpdate("componentConfig.hidden", checked);
@@ -689,7 +706,7 @@ export const V2PropertiesPanel: React.FC = ({
표시
{
const boolValue = checked === true;
handleUpdate("style.labelDisplay", boolValue);
@@ -785,7 +802,7 @@ export const V2PropertiesPanel: React.FC = ({
const webType = selectedComponent.componentConfig?.webType;
// 테이블 패널에서 드래그한 컴포넌트인지 확인
- const isFromTablePanel = !!(selectedComponent.tableName && selectedComponent.columnName);
+ const isFromTablePanel = !!((selectedComponent as any).tableName && (selectedComponent as any).columnName);
if (!componentId) {
return (
@@ -845,8 +862,8 @@ export const V2PropertiesPanel: React.FC = ({
= ({
= ({
return (
{/* WebType 선택 (있는 경우만) */}
- {widget.webType && (
+ {(widget as any).webType && (
-