Compare commits
No commits in common. "d218fd7a1aa6d000bb8480a4550b25e337b6f505" and "5bf3c0fcd764927d30ba68d98ade4185d3dda25d" have entirely different histories.
d218fd7a1a
...
5bf3c0fcd7
|
|
@ -187,16 +187,6 @@ export const deleteCategoryValue = async (req: AuthenticatedRequest, res: Respon
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`카테고리 값 삭제 실패: ${error.message}`);
|
logger.error(`카테고리 값 삭제 실패: ${error.message}`);
|
||||||
|
|
||||||
// 사용 중인 경우 상세 에러 메시지 반환 (400)
|
|
||||||
if (error.message.includes("삭제할 수 없습니다")) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기타 에러 (500)
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || "카테고리 값 삭제 중 오류가 발생했습니다",
|
message: error.message || "카테고리 값 삭제 중 오류가 발생했습니다",
|
||||||
|
|
|
||||||
|
|
@ -445,129 +445,7 @@ class TableCategoryValueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 사용 여부 확인
|
* 카테고리 값 삭제 (비활성화)
|
||||||
* 실제 데이터 테이블에서 해당 카테고리 값이 사용되고 있는지 확인
|
|
||||||
*/
|
|
||||||
async checkCategoryValueUsage(
|
|
||||||
valueId: number,
|
|
||||||
companyCode: string
|
|
||||||
): Promise<{ isUsed: boolean; usedInTables: any[]; totalCount: number }> {
|
|
||||||
const pool = getPool();
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info("카테고리 값 사용 여부 확인", { valueId, companyCode });
|
|
||||||
|
|
||||||
// 1. 카테고리 값 정보 조회
|
|
||||||
let valueQuery: string;
|
|
||||||
let valueParams: any[];
|
|
||||||
|
|
||||||
if (companyCode === "*") {
|
|
||||||
valueQuery = `
|
|
||||||
SELECT table_name, column_name, value_code
|
|
||||||
FROM table_column_category_values
|
|
||||||
WHERE value_id = $1
|
|
||||||
`;
|
|
||||||
valueParams = [valueId];
|
|
||||||
} else {
|
|
||||||
valueQuery = `
|
|
||||||
SELECT table_name, column_name, value_code
|
|
||||||
FROM table_column_category_values
|
|
||||||
WHERE value_id = $1
|
|
||||||
AND company_code = $2
|
|
||||||
`;
|
|
||||||
valueParams = [valueId, companyCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueResult = await pool.query(valueQuery, valueParams);
|
|
||||||
|
|
||||||
if (valueResult.rowCount === 0) {
|
|
||||||
throw new Error("카테고리 값을 찾을 수 없습니다");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { table_name, column_name, value_code } = valueResult.rows[0];
|
|
||||||
|
|
||||||
// 2. 실제 데이터 테이블에서 사용 여부 확인
|
|
||||||
// 테이블이 존재하는지 먼저 확인
|
|
||||||
const tableExistsQuery = `
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = $1
|
|
||||||
) as exists
|
|
||||||
`;
|
|
||||||
|
|
||||||
const tableExistsResult = await pool.query(tableExistsQuery, [table_name]);
|
|
||||||
|
|
||||||
if (!tableExistsResult.rows[0].exists) {
|
|
||||||
logger.info("테이블이 존재하지 않음", { table_name });
|
|
||||||
return { isUsed: false, usedInTables: [], totalCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 해당 테이블에서 value_code를 사용하는 데이터 개수 확인
|
|
||||||
let dataCountQuery: string;
|
|
||||||
let dataCountParams: any[];
|
|
||||||
|
|
||||||
if (companyCode === "*") {
|
|
||||||
dataCountQuery = `
|
|
||||||
SELECT COUNT(*) as count
|
|
||||||
FROM ${table_name}
|
|
||||||
WHERE ${column_name} = $1
|
|
||||||
`;
|
|
||||||
dataCountParams = [value_code];
|
|
||||||
} else {
|
|
||||||
dataCountQuery = `
|
|
||||||
SELECT COUNT(*) as count
|
|
||||||
FROM ${table_name}
|
|
||||||
WHERE ${column_name} = $1
|
|
||||||
AND company_code = $2
|
|
||||||
`;
|
|
||||||
dataCountParams = [value_code, companyCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataCountResult = await pool.query(dataCountQuery, dataCountParams);
|
|
||||||
const totalCount = parseInt(dataCountResult.rows[0].count);
|
|
||||||
const isUsed = totalCount > 0;
|
|
||||||
|
|
||||||
// 4. 사용 중인 메뉴 목록 조회 (해당 테이블을 사용하는 화면/메뉴)
|
|
||||||
const menuQuery = `
|
|
||||||
SELECT DISTINCT
|
|
||||||
mi.objid as menu_objid,
|
|
||||||
mi.menu_name_kor as menu_name,
|
|
||||||
mi.menu_url
|
|
||||||
FROM menu_info mi
|
|
||||||
INNER JOIN screen_menu_assignments sma ON sma.menu_objid = mi.objid
|
|
||||||
INNER JOIN screen_definitions sd ON sd.screen_id = sma.screen_id
|
|
||||||
WHERE sd.table_name = $1
|
|
||||||
AND mi.company_code = $2
|
|
||||||
ORDER BY mi.menu_name_kor
|
|
||||||
`;
|
|
||||||
|
|
||||||
const menuResult = await pool.query(menuQuery, [table_name, companyCode]);
|
|
||||||
|
|
||||||
const usedInTables = menuResult.rows.map((row) => ({
|
|
||||||
menuObjid: row.menu_objid,
|
|
||||||
menuName: row.menu_name,
|
|
||||||
menuUrl: row.menu_url,
|
|
||||||
tableName: table_name,
|
|
||||||
columnName: column_name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
logger.info("카테고리 값 사용 여부 확인 완료", {
|
|
||||||
valueId,
|
|
||||||
isUsed,
|
|
||||||
totalCount,
|
|
||||||
usedInMenusCount: usedInTables.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { isUsed, usedInTables, totalCount };
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error(`카테고리 값 사용 여부 확인 실패: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 카테고리 값 삭제 (물리적 삭제)
|
|
||||||
*/
|
*/
|
||||||
async deleteCategoryValue(
|
async deleteCategoryValue(
|
||||||
valueId: number,
|
valueId: number,
|
||||||
|
|
@ -577,24 +455,7 @@ class TableCategoryValueService {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 사용 여부 확인
|
// 하위 값 체크 (멀티테넌시 적용)
|
||||||
const usage = await this.checkCategoryValueUsage(valueId, companyCode);
|
|
||||||
|
|
||||||
if (usage.isUsed) {
|
|
||||||
let errorMessage = "이 카테고리 값을 삭제할 수 없습니다.\n";
|
|
||||||
errorMessage += `\n현재 ${usage.totalCount}개의 데이터에서 사용 중입니다.`;
|
|
||||||
|
|
||||||
if (usage.usedInTables.length > 0) {
|
|
||||||
const menuNames = usage.usedInTables.map((t) => t.menuName).join(", ");
|
|
||||||
errorMessage += `\n\n다음 메뉴에서 사용 중입니다:\n${menuNames}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage += "\n\n메뉴에서 사용하는 카테고리 항목을 수정한 후 다시 삭제해주세요.";
|
|
||||||
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 하위 값 체크 (멀티테넌시 적용)
|
|
||||||
let checkQuery: string;
|
let checkQuery: string;
|
||||||
let checkParams: any[];
|
let checkParams: any[];
|
||||||
|
|
||||||
|
|
@ -604,6 +465,7 @@ class TableCategoryValueService {
|
||||||
SELECT COUNT(*) as count
|
SELECT COUNT(*) as count
|
||||||
FROM table_column_category_values
|
FROM table_column_category_values
|
||||||
WHERE parent_value_id = $1
|
WHERE parent_value_id = $1
|
||||||
|
AND is_active = true
|
||||||
`;
|
`;
|
||||||
checkParams = [valueId];
|
checkParams = [valueId];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -613,6 +475,7 @@ class TableCategoryValueService {
|
||||||
FROM table_column_category_values
|
FROM table_column_category_values
|
||||||
WHERE parent_value_id = $1
|
WHERE parent_value_id = $1
|
||||||
AND company_code = $2
|
AND company_code = $2
|
||||||
|
AND is_active = true
|
||||||
`;
|
`;
|
||||||
checkParams = [valueId, companyCode];
|
checkParams = [valueId, companyCode];
|
||||||
}
|
}
|
||||||
|
|
@ -623,25 +486,27 @@ class TableCategoryValueService {
|
||||||
throw new Error("하위 카테고리 값이 있어 삭제할 수 없습니다");
|
throw new Error("하위 카테고리 값이 있어 삭제할 수 없습니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 물리적 삭제 (멀티테넌시 적용)
|
// 비활성화 (멀티테넌시 적용)
|
||||||
let deleteQuery: string;
|
let deleteQuery: string;
|
||||||
let deleteParams: any[];
|
let deleteParams: any[];
|
||||||
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
// 최고 관리자: 모든 카테고리 값 삭제 가능
|
// 최고 관리자: 모든 카테고리 값 삭제 가능
|
||||||
deleteQuery = `
|
deleteQuery = `
|
||||||
DELETE FROM table_column_category_values
|
UPDATE table_column_category_values
|
||||||
|
SET is_active = false, updated_at = NOW(), updated_by = $2
|
||||||
WHERE value_id = $1
|
WHERE value_id = $1
|
||||||
`;
|
`;
|
||||||
deleteParams = [valueId];
|
deleteParams = [valueId, userId];
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 카테고리 값만 삭제 가능
|
// 일반 회사: 자신의 카테고리 값만 삭제 가능
|
||||||
deleteQuery = `
|
deleteQuery = `
|
||||||
DELETE FROM table_column_category_values
|
UPDATE table_column_category_values
|
||||||
|
SET is_active = false, updated_at = NOW(), updated_by = $3
|
||||||
WHERE value_id = $1
|
WHERE value_id = $1
|
||||||
AND company_code = $2
|
AND company_code = $2
|
||||||
`;
|
`;
|
||||||
deleteParams = [valueId, companyCode];
|
deleteParams = [valueId, companyCode, userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool.query(deleteQuery, deleteParams);
|
const result = await pool.query(deleteQuery, deleteParams);
|
||||||
|
|
@ -650,7 +515,7 @@ class TableCategoryValueService {
|
||||||
throw new Error("카테고리 값을 찾을 수 없거나 권한이 없습니다");
|
throw new Error("카테고리 값을 찾을 수 없거나 권한이 없습니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("카테고리 값 삭제 완료", {
|
logger.info("카테고리 값 삭제(비활성화) 완료", {
|
||||||
valueId,
|
valueId,
|
||||||
companyCode,
|
companyCode,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -184,18 +184,11 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
title: "성공",
|
title: "성공",
|
||||||
description: "카테고리 값이 삭제되었습니다",
|
description: "카테고리 값이 삭제되었습니다",
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// 백엔드에서 반환한 상세 에러 메시지 표시
|
|
||||||
toast({
|
|
||||||
title: "삭제 불가",
|
|
||||||
description: response.error || response.message || "카테고리 값 삭제에 실패했습니다",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
title: "오류",
|
title: "오류",
|
||||||
description: "카테고리 값 삭제 중 오류가 발생했습니다",
|
description: "카테고리 값 삭제에 실패했습니다",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,10 +109,7 @@ export async function deleteCategoryValue(valueId: number) {
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 값 삭제 실패:", error);
|
console.error("카테고리 값 삭제 실패:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
// 백엔드에서 반환한 에러 메시지 전달
|
|
||||||
const errorMessage = error.response?.data?.message || error.message;
|
|
||||||
return { success: false, error: errorMessage, message: errorMessage };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,14 @@ export interface ComponentRenderer {
|
||||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
selectedRows?: any[];
|
selectedRows?: any[];
|
||||||
selectedRowsData?: any[];
|
selectedRowsData?: any[];
|
||||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
|
onSelectedRowsChange?: (
|
||||||
|
selectedRows: any[],
|
||||||
|
selectedRowsData: any[],
|
||||||
|
sortBy?: string,
|
||||||
|
sortOrder?: "asc" | "desc",
|
||||||
|
columnOrder?: string[],
|
||||||
|
tableDisplayData?: any[],
|
||||||
|
) => void;
|
||||||
// 테이블 정렬 정보 (엑셀 다운로드용)
|
// 테이블 정렬 정보 (엑셀 다운로드용)
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortOrder?: "asc" | "desc";
|
sortOrder?: "asc" | "desc";
|
||||||
|
|
@ -98,6 +105,8 @@ export interface DynamicComponentRendererProps {
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
menuId?: number; // 🆕 메뉴 ID (카테고리 관리 등에 필요)
|
menuId?: number; // 🆕 메뉴 ID (카테고리 관리 등에 필요)
|
||||||
|
// 🆕 조건부 컨테이너 높이 변화 콜백
|
||||||
|
onHeightChange?: (componentId: string, newHeight: number) => void;
|
||||||
menuObjid?: number; // 🆕 메뉴 OBJID (메뉴 스코프 - 카테고리/채번)
|
menuObjid?: number; // 🆕 메뉴 OBJID (메뉴 스코프 - 카테고리/채번)
|
||||||
selectedScreen?: any; // 🆕 화면 정보 전체 (menuId 등 추출용)
|
selectedScreen?: any; // 🆕 화면 정보 전체 (menuId 등 추출용)
|
||||||
userId?: string; // 🆕 현재 사용자 ID
|
userId?: string; // 🆕 현재 사용자 ID
|
||||||
|
|
@ -108,7 +117,14 @@ export interface DynamicComponentRendererProps {
|
||||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
selectedRows?: any[];
|
selectedRows?: any[];
|
||||||
selectedRowsData?: any[];
|
selectedRowsData?: any[];
|
||||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
|
onSelectedRowsChange?: (
|
||||||
|
selectedRows: any[],
|
||||||
|
selectedRowsData: any[],
|
||||||
|
sortBy?: string,
|
||||||
|
sortOrder?: "asc" | "desc",
|
||||||
|
columnOrder?: string[],
|
||||||
|
tableDisplayData?: any[],
|
||||||
|
) => void;
|
||||||
// 테이블 정렬 정보 (엑셀 다운로드용)
|
// 테이블 정렬 정보 (엑셀 다운로드용)
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortOrder?: "asc" | "desc";
|
sortOrder?: "asc" | "desc";
|
||||||
|
|
@ -148,14 +164,14 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
const webType = (component as any).componentConfig?.webType;
|
const webType = (component as any).componentConfig?.webType;
|
||||||
const tableName = (component as any).tableName;
|
const tableName = (component as any).tableName;
|
||||||
const columnName = (component as any).columnName;
|
const columnName = (component as any).columnName;
|
||||||
|
|
||||||
// 카테고리 셀렉트: webType이 "category"이고 tableName과 columnName이 있는 경우만
|
// 카테고리 셀렉트: webType이 "category"이고 tableName과 columnName이 있는 경우만
|
||||||
if ((inputType === "category" || webType === "category") && tableName && columnName) {
|
if ((inputType === "category" || webType === "category") && tableName && columnName) {
|
||||||
try {
|
try {
|
||||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||||
const fieldName = columnName || component.id;
|
const fieldName = columnName || component.id;
|
||||||
const currentValue = props.formData?.[fieldName] || "";
|
const currentValue = props.formData?.[fieldName] || "";
|
||||||
|
|
||||||
const handleChange = (value: any) => {
|
const handleChange = (value: any) => {
|
||||||
if (props.onFormDataChange) {
|
if (props.onFormDataChange) {
|
||||||
props.onFormDataChange(fieldName, value);
|
props.onFormDataChange(fieldName, value);
|
||||||
|
|
@ -254,6 +270,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
isPreview,
|
isPreview,
|
||||||
autoGeneration,
|
autoGeneration,
|
||||||
|
onHeightChange, // 🆕 높이 변화 콜백
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -290,12 +307,12 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
// 숨김 값 추출
|
// 숨김 값 추출
|
||||||
const hiddenValue = component.hidden || component.componentConfig?.hidden;
|
const hiddenValue = component.hidden || component.componentConfig?.hidden;
|
||||||
|
|
||||||
// size.width와 size.height를 style.width와 style.height로 변환
|
// 🆕 조건부 컨테이너용 높이 변화 핸들러
|
||||||
const finalStyle: React.CSSProperties = {
|
const handleHeightChange = props.onHeightChange
|
||||||
...component.style,
|
? (newHeight: number) => {
|
||||||
width: component.size?.width ? `${component.size.width}px` : component.style?.width,
|
props.onHeightChange!(component.id, newHeight);
|
||||||
height: component.size?.height ? `${component.size.height}px` : component.style?.height,
|
}
|
||||||
};
|
: undefined;
|
||||||
|
|
||||||
const rendererProps = {
|
const rendererProps = {
|
||||||
component,
|
component,
|
||||||
|
|
@ -305,7 +322,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
size: component.size || newComponent.defaultSize,
|
size: component.size || newComponent.defaultSize,
|
||||||
position: component.position,
|
position: component.position,
|
||||||
style: finalStyle, // size를 포함한 최종 style
|
style: component.style, // 컴포넌트 스타일 전달
|
||||||
config: component.componentConfig,
|
config: component.componentConfig,
|
||||||
componentConfig: component.componentConfig,
|
componentConfig: component.componentConfig,
|
||||||
value: currentValue, // formData에서 추출한 현재 값 전달
|
value: currentValue, // formData에서 추출한 현재 값 전달
|
||||||
|
|
@ -345,6 +362,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
tableDisplayData, // 🆕 화면 표시 데이터
|
tableDisplayData, // 🆕 화면 표시 데이터
|
||||||
// 플로우 선택된 데이터 정보 전달
|
// 플로우 선택된 데이터 정보 전달
|
||||||
flowSelectedData,
|
flowSelectedData,
|
||||||
|
// 🆕 조건부 컨테이너 높이 변화 콜백
|
||||||
|
onHeightChange: handleHeightChange,
|
||||||
|
componentId: component.id,
|
||||||
flowSelectedStepId,
|
flowSelectedStepId,
|
||||||
onFlowSelectedDataChange,
|
onFlowSelectedDataChange,
|
||||||
// 설정 변경 핸들러 전달
|
// 설정 변경 핸들러 전달
|
||||||
|
|
@ -370,7 +390,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
return rendererInstance.render();
|
return rendererInstance.render();
|
||||||
} else {
|
} else {
|
||||||
// 함수형 컴포넌트
|
// 함수형 컴포넌트
|
||||||
return <NewComponentRenderer {...rendererProps} />;
|
// config 내부 속성도 펼쳐서 전달 (tableName, displayField 등)
|
||||||
|
const configProps = component.componentConfig?.config || component.componentConfig || {};
|
||||||
|
return <NewComponentRenderer {...rendererProps} {...configProps} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -392,10 +414,10 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
|
|
||||||
// 폴백 렌더링 - 기본 플레이스홀더
|
// 폴백 렌더링 - 기본 플레이스홀더
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-border bg-muted p-4">
|
<div className="border-border bg-muted flex h-full w-full items-center justify-center rounded border-2 border-dashed p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-2 text-sm font-medium text-muted-foreground">{component.label || component.id}</div>
|
<div className="text-muted-foreground mb-2 text-sm font-medium">{component.label || component.id}</div>
|
||||||
<div className="text-xs text-muted-foreground/70">미구현 컴포넌트: {componentType}</div>
|
<div className="text-muted-foreground/70 text-xs">미구현 컴포넌트: {componentType}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue