Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map

This commit is contained in:
dohyeons 2025-11-25 14:58:39 +09:00
commit d6b9372e1f
72 changed files with 2905 additions and 1106 deletions

View File

@ -1097,7 +1097,11 @@ export async function saveMenu(
let requestCompanyCode = menuData.companyCode || menuData.company_code; let requestCompanyCode = menuData.companyCode || menuData.company_code;
// "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용 // "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용
if (requestCompanyCode === "none" || requestCompanyCode === "" || !requestCompanyCode) { if (
requestCompanyCode === "none" ||
requestCompanyCode === "" ||
!requestCompanyCode
) {
requestCompanyCode = undefined; requestCompanyCode = undefined;
} }
@ -1252,7 +1256,8 @@ export async function updateMenu(
} }
} }
const requestCompanyCode = menuData.companyCode || menuData.company_code || currentMenu.company_code; const requestCompanyCode =
menuData.companyCode || menuData.company_code || currentMenu.company_code;
// company_code 변경 시도하는 경우 권한 체크 // company_code 변경 시도하는 경우 권한 체크
if (requestCompanyCode !== currentMenu.company_code) { if (requestCompanyCode !== currentMenu.company_code) {
@ -1268,7 +1273,10 @@ export async function updateMenu(
} }
} }
// 회사 관리자는 자기 회사로만 변경 가능 // 회사 관리자는 자기 회사로만 변경 가능
else if (userCompanyCode !== "*" && requestCompanyCode !== userCompanyCode) { else if (
userCompanyCode !== "*" &&
requestCompanyCode !== userCompanyCode
) {
res.status(403).json({ res.status(403).json({
success: false, success: false,
message: "해당 회사로 변경할 권한이 없습니다.", message: "해당 회사로 변경할 권한이 없습니다.",
@ -1324,7 +1332,7 @@ export async function updateMenu(
if (!menuUrl) { if (!menuUrl) {
await query( await query(
`UPDATE screen_menu_assignments `UPDATE screen_menu_assignments
SET is_active = 'N', updated_date = NOW() SET is_active = 'N'
WHERE menu_objid = $1 AND company_code = $2`, WHERE menu_objid = $1 AND company_code = $2`,
[Number(menuId), companyCode] [Number(menuId), companyCode]
); );
@ -1493,8 +1501,13 @@ export async function deleteMenusBatch(
); );
// 권한 체크: 공통 메뉴 포함 여부 확인 // 권한 체크: 공통 메뉴 포함 여부 확인
const hasCommonMenu = menusToDelete.some((menu: any) => menu.company_code === "*"); const hasCommonMenu = menusToDelete.some(
if (hasCommonMenu && (userCompanyCode !== "*" || userType !== "SUPER_ADMIN")) { (menu: any) => menu.company_code === "*"
);
if (
hasCommonMenu &&
(userCompanyCode !== "*" || userType !== "SUPER_ADMIN")
) {
res.status(403).json({ res.status(403).json({
success: false, success: false,
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.", message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
@ -1506,7 +1519,8 @@ export async function deleteMenusBatch(
// 회사 관리자는 자기 회사 메뉴만 삭제 가능 // 회사 관리자는 자기 회사 메뉴만 삭제 가능
if (userCompanyCode !== "*") { if (userCompanyCode !== "*") {
const unauthorizedMenus = menusToDelete.filter( const unauthorizedMenus = menusToDelete.filter(
(menu: any) => menu.company_code !== userCompanyCode && menu.company_code !== "*" (menu: any) =>
menu.company_code !== userCompanyCode && menu.company_code !== "*"
); );
if (unauthorizedMenus.length > 0) { if (unauthorizedMenus.length > 0) {
res.status(403).json({ res.status(403).json({
@ -2674,7 +2688,10 @@ export const getCompanyByCode = async (
res.status(200).json(response); res.status(200).json(response);
} catch (error) { } catch (error) {
logger.error("회사 정보 조회 실패", { error, companyCode: req.params.companyCode }); logger.error("회사 정보 조회 실패", {
error,
companyCode: req.params.companyCode,
});
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "회사 정보 조회 중 오류가 발생했습니다.", message: "회사 정보 조회 중 오류가 발생했습니다.",
@ -2740,7 +2757,9 @@ export const updateCompany = async (
// 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외) // 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외)
if (business_registration_number && business_registration_number.trim()) { if (business_registration_number && business_registration_number.trim()) {
// 유효성 검증 // 유효성 검증
const businessNumberValidation = validateBusinessNumber(business_registration_number.trim()); const businessNumberValidation = validateBusinessNumber(
business_registration_number.trim()
);
if (!businessNumberValidation.isValid) { if (!businessNumberValidation.isValid) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
@ -3283,7 +3302,9 @@ export async function copyMenu(
// 권한 체크: 최고 관리자만 가능 // 권한 체크: 최고 관리자만 가능
if (!isSuperAdmin && userType !== "SUPER_ADMIN") { if (!isSuperAdmin && userType !== "SUPER_ADMIN") {
logger.warn(`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`); logger.warn(
`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`
);
res.status(403).json({ res.status(403).json({
success: false, success: false,
message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다", message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다",

View File

@ -148,11 +148,11 @@ export const updateScreenInfo = async (
try { try {
const { id } = req.params; const { id } = req.params;
const { companyCode } = req.user as any; const { companyCode } = req.user as any;
const { screenName, description, isActive } = req.body; const { screenName, tableName, description, isActive } = req.body;
await screenManagementService.updateScreenInfo( await screenManagementService.updateScreenInfo(
parseInt(id), parseInt(id),
{ screenName, description, isActive }, { screenName, tableName, description, isActive },
companyCode companyCode
); );
res.json({ success: true, message: "화면 정보가 수정되었습니다." }); res.json({ success: true, message: "화면 정보가 수정되었습니다." });

View File

@ -7,6 +7,7 @@ import { query, queryOne } from "../../database/db";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService"; import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService";
import { AuthenticatedRequest } from "../../types/auth"; import { AuthenticatedRequest } from "../../types/auth";
import { authenticateToken } from "../../middleware/authMiddleware";
const router = Router(); const router = Router();
@ -217,19 +218,29 @@ router.delete("/:flowId", async (req: Request, res: Response) => {
* *
* POST /api/dataflow/node-flows/:flowId/execute * POST /api/dataflow/node-flows/:flowId/execute
*/ */
router.post("/:flowId/execute", async (req: Request, res: Response) => { router.post("/:flowId/execute", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
try { try {
const { flowId } = req.params; const { flowId } = req.params;
const contextData = req.body; const contextData = req.body;
logger.info(`🚀 플로우 실행 요청: flowId=${flowId}`, { logger.info(`🚀 플로우 실행 요청: flowId=${flowId}`, {
contextDataKeys: Object.keys(contextData), contextDataKeys: Object.keys(contextData),
userId: req.user?.userId,
companyCode: req.user?.companyCode,
}); });
// 사용자 정보를 contextData에 추가
const enrichedContextData = {
...contextData,
userId: req.user?.userId,
userName: req.user?.userName,
companyCode: req.user?.companyCode,
};
// 플로우 실행 // 플로우 실행
const result = await NodeFlowExecutionService.executeFlow( const result = await NodeFlowExecutionService.executeFlow(
parseInt(flowId, 10), parseInt(flowId, 10),
contextData enrichedContextData
); );
return res.json({ return res.json({

View File

@ -811,9 +811,39 @@ export class DynamicFormService {
const primaryKeyColumn = primaryKeys[0]; const primaryKeyColumn = primaryKeys[0];
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`); console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
// 동적 UPDATE SQL 생성 (변경된 필드만) // 🆕 컬럼 타입 조회 (타입 캐스팅용)
const columnTypesQuery = `
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1 AND table_schema = 'public'
`;
const columnTypesResult = await query<{ column_name: string; data_type: string }>(
columnTypesQuery,
[tableName]
);
const columnTypes: Record<string, string> = {};
columnTypesResult.forEach((row) => {
columnTypes[row.column_name] = row.data_type;
});
console.log("📊 컬럼 타입 정보:", columnTypes);
// 🆕 동적 UPDATE SQL 생성 (타입 캐스팅 포함)
const setClause = Object.keys(changedFields) const setClause = Object.keys(changedFields)
.map((key, index) => `${key} = $${index + 1}`) .map((key, index) => {
const dataType = columnTypes[key];
// 숫자 타입인 경우 명시적 캐스팅
if (dataType === 'integer' || dataType === 'bigint' || dataType === 'smallint') {
return `${key} = $${index + 1}::integer`;
} else if (dataType === 'numeric' || dataType === 'decimal' || dataType === 'real' || dataType === 'double precision') {
return `${key} = $${index + 1}::numeric`;
} else if (dataType === 'boolean') {
return `${key} = $${index + 1}::boolean`;
} else {
// 문자열 타입은 캐스팅 불필요
return `${key} = $${index + 1}`;
}
})
.join(", "); .join(", ");
const values: any[] = Object.values(changedFields); const values: any[] = Object.values(changedFields);

View File

@ -938,6 +938,30 @@ export class NodeFlowExecutionService {
insertedData[mapping.targetField] = value; insertedData[mapping.targetField] = value;
}); });
// 🆕 writer와 company_code 자동 추가 (필드 매핑에 없는 경우)
const hasWriterMapping = fieldMappings.some((m: any) => m.targetField === "writer");
const hasCompanyCodeMapping = fieldMappings.some((m: any) => m.targetField === "company_code");
// 컨텍스트에서 사용자 정보 추출
const userId = context.buttonContext?.userId;
const companyCode = context.buttonContext?.companyCode;
// writer 자동 추가 (매핑에 없고, 컨텍스트에 userId가 있는 경우)
if (!hasWriterMapping && userId) {
fields.push("writer");
values.push(userId);
insertedData.writer = userId;
console.log(` 🔧 자동 추가: writer = ${userId}`);
}
// company_code 자동 추가 (매핑에 없고, 컨텍스트에 companyCode가 있는 경우)
if (!hasCompanyCodeMapping && companyCode && companyCode !== "*") {
fields.push("company_code");
values.push(companyCode);
insertedData.company_code = companyCode;
console.log(` 🔧 자동 추가: company_code = ${companyCode}`);
}
const sql = ` const sql = `
INSERT INTO ${targetTable} (${fields.join(", ")}) INSERT INTO ${targetTable} (${fields.join(", ")})
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")}) VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})

View File

@ -321,7 +321,7 @@ export class ScreenManagementService {
*/ */
async updateScreenInfo( async updateScreenInfo(
screenId: number, screenId: number,
updateData: { screenName: string; description?: string; isActive: string }, updateData: { screenName: string; tableName?: string; description?: string; isActive: string },
userCompanyCode: string userCompanyCode: string
): Promise<void> { ): Promise<void> {
// 권한 확인 // 권한 확인
@ -343,16 +343,18 @@ export class ScreenManagementService {
throw new Error("이 화면을 수정할 권한이 없습니다."); throw new Error("이 화면을 수정할 권한이 없습니다.");
} }
// 화면 정보 업데이트 // 화면 정보 업데이트 (tableName 포함)
await query( await query(
`UPDATE screen_definitions `UPDATE screen_definitions
SET screen_name = $1, SET screen_name = $1,
description = $2, table_name = $2,
is_active = $3, description = $3,
updated_date = $4 is_active = $4,
WHERE screen_id = $5`, updated_date = $5
WHERE screen_id = $6`,
[ [
updateData.screenName, updateData.screenName,
updateData.tableName || null,
updateData.description || null, updateData.description || null,
updateData.isActive, updateData.isActive,
new Date(), new Date(),

View File

@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge";
*/ */
export default function MainPage() { export default function MainPage() {
return ( return (
<div className="space-y-6 px-4 pt-10"> <div className="space-y-6 p-4">
{/* 메인 컨텐츠 */} {/* 메인 컨텐츠 */}
{/* Welcome Message */} {/* Welcome Message */}
<Card> <Card>

View File

@ -1,6 +1,6 @@
export default function MainHomePage() { export default function MainHomePage() {
return ( return (
<div className="space-y-6 px-4 pt-10"> <div className="space-y-6 p-4">
{/* 대시보드 컨텐츠 */} {/* 대시보드 컨텐츠 */}
<div className="rounded-lg border bg-background p-6 shadow-sm"> <div className="rounded-lg border bg-background p-6 shadow-sm">
<h3 className="mb-4 text-lg font-semibold">WACE !</h3> <h3 className="mb-4 text-lg font-semibold">WACE !</h3>

View File

@ -178,7 +178,7 @@ function ScreenViewPage() {
for (const comp of layout.components) { for (const comp of layout.components) {
// type: "component" 또는 type: "widget" 모두 처리 // type: "component" 또는 type: "widget" 모두 처리
if (comp.type === 'widget' || comp.type === 'component') { if (comp.type === "widget" || comp.type === "component") {
const widget = comp as any; const widget = comp as any;
const fieldName = widget.columnName || widget.id; const fieldName = widget.columnName || widget.id;
@ -187,7 +187,7 @@ function ScreenViewPage() {
const autoFillConfig = widget.autoFill || (comp as any).autoFill; const autoFillConfig = widget.autoFill || (comp as any).autoFill;
const currentValue = formData[fieldName]; const currentValue = formData[fieldName];
if (currentValue === undefined || currentValue === '') { if (currentValue === undefined || currentValue === "") {
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
// 사용자 정보에서 필터 값 가져오기 // 사용자 정보에서 필터 값 가져오기
@ -196,12 +196,7 @@ function ScreenViewPage() {
if (userValue && sourceTable && filterColumn && displayColumn) { if (userValue && sourceTable && filterColumn && displayColumn) {
try { try {
const { tableTypeApi } = await import("@/lib/api/screen"); const { tableTypeApi } = await import("@/lib/api/screen");
const result = await tableTypeApi.getTableRecord( const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn);
sourceTable,
filterColumn,
userValue,
displayColumn
);
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
@ -233,24 +228,27 @@ function ScreenViewPage() {
const designWidth = layout?.screenResolution?.width || 1200; const designWidth = layout?.screenResolution?.width || 1200;
const designHeight = layout?.screenResolution?.height || 800; const designHeight = layout?.screenResolution?.height || 800;
// containerRef는 이미 패딩이 적용된 영역 내부이므로 offsetWidth는 패딩을 제외한 크기입니다 // 컨테이너의 실제 크기
const containerWidth = containerRef.current.offsetWidth; const containerWidth = containerRef.current.offsetWidth;
const containerHeight = containerRef.current.offsetHeight; const containerHeight = containerRef.current.offsetHeight;
// 화면이 잘리지 않도록 가로/세로 중 작은 쪽 기준으로 스케일 조정 // 여백 설정: 좌우 16px씩 (총 32px), 상단 패딩 32px (pt-8)
const scaleX = containerWidth / designWidth; const MARGIN_X = 32;
const scaleY = containerHeight / designHeight; const availableWidth = containerWidth - MARGIN_X;
// 전체 화면이 보이도록 작은 쪽 기준으로 스케일 설정
const newScale = Math.min(scaleX, scaleY); // 가로 기준 스케일 계산 (좌우 여백 16px씩 고정)
const newScale = availableWidth / designWidth;
console.log("📐 스케일 계산:", { console.log("📐 스케일 계산:", {
containerWidth, containerWidth,
containerHeight, containerHeight,
MARGIN_X,
availableWidth,
designWidth, designWidth,
designHeight, designHeight,
scaleX,
scaleY,
finalScale: newScale, finalScale: newScale,
"스케일된 화면 크기": `${designWidth * newScale}px × ${designHeight * newScale}px`,
"실제 좌우 여백": `${(containerWidth - designWidth * newScale) / 2}px씩`,
}); });
setScale(newScale); setScale(newScale);
@ -307,7 +305,10 @@ function ScreenViewPage() {
return ( return (
<ScreenPreviewProvider isPreviewMode={false}> <ScreenPreviewProvider isPreviewMode={false}>
<TableOptionsProvider> <TableOptionsProvider>
<div ref={containerRef} className="bg-background flex h-full w-full items-center justify-center overflow-hidden"> <div
ref={containerRef}
className="bg-background flex h-full w-full items-center justify-center overflow-auto pt-8"
>
{/* 레이아웃 준비 중 로딩 표시 */} {/* 레이아웃 준비 중 로딩 표시 */}
{!layoutReady && ( {!layoutReady && (
<div className="from-muted to-muted/50 flex h-full w-full items-center justify-center bg-gradient-to-br"> <div className="from-muted to-muted/50 flex h-full w-full items-center justify-center bg-gradient-to-br">
@ -402,27 +403,35 @@ function ScreenViewPage() {
// TableSearchWidget들을 먼저 찾기 // TableSearchWidget들을 먼저 찾기
const tableSearchWidgets = regularComponents.filter( const tableSearchWidgets = regularComponents.filter(
(c) => (c as any).componentId === "table-search-widget" (c) => (c as any).componentId === "table-search-widget",
); );
// 디버그: 모든 컴포넌트 타입 확인 // 디버그: 모든 컴포넌트 타입 확인
console.log("🔍 전체 컴포넌트 타입:", regularComponents.map(c => ({ console.log(
"🔍 전체 컴포넌트 타입:",
regularComponents.map((c) => ({
id: c.id, id: c.id,
type: c.type, type: c.type,
componentType: (c as any).componentType, componentType: (c as any).componentType,
componentId: (c as any).componentId, componentId: (c as any).componentId,
}))); })),
);
// 🆕 조건부 컨테이너들을 찾기 // 🆕 조건부 컨테이너들을 찾기
const conditionalContainers = regularComponents.filter( const conditionalContainers = regularComponents.filter(
(c) => (c as any).componentId === "conditional-container" || (c as any).componentType === "conditional-container" (c) =>
(c as any).componentId === "conditional-container" ||
(c as any).componentType === "conditional-container",
); );
console.log("🔍 조건부 컨테이너 발견:", conditionalContainers.map(c => ({ console.log(
"🔍 조건부 컨테이너 발견:",
conditionalContainers.map((c) => ({
id: c.id, id: c.id,
y: c.position.y, y: c.position.y,
size: c.size, size: c.size,
}))); })),
);
// TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정 // TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정
const adjustedComponents = regularComponents.map((component) => { const adjustedComponents = regularComponents.map((component) => {
@ -451,7 +460,7 @@ function ScreenViewPage() {
const isBelow = component.position.y > container.position.y; const isBelow = component.position.y > container.position.y;
const actualHeight = conditionalContainerHeights[container.id]; const actualHeight = conditionalContainerHeights[container.id];
const originalHeight = container.size?.height || 200; const originalHeight = container.size?.height || 200;
const heightDiff = actualHeight ? (actualHeight - originalHeight) : 0; const heightDiff = actualHeight ? actualHeight - originalHeight : 0;
console.log(`🔍 높이 조정 체크:`, { console.log(`🔍 높이 조정 체크:`, {
componentId: component.id, componentId: component.id,
@ -467,7 +476,9 @@ function ScreenViewPage() {
if (isBelow && heightDiff > 0) { if (isBelow && heightDiff > 0) {
totalHeightAdjustment += heightDiff; totalHeightAdjustment += heightDiff;
console.log(`📐 컴포넌트 ${component.id} 위치 조정: ${heightDiff}px (조건부 컨테이너 ${container.id})`); console.log(
`📐 컴포넌트 ${component.id} 위치 조정: ${heightDiff}px (조건부 컨테이너 ${container.id})`,
);
} }
} }
@ -511,7 +522,10 @@ function ScreenViewPage() {
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => {
console.log("🔍 화면에서 선택된 행 데이터:", selectedData); console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder }); console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder });
console.log("📊 화면 표시 데이터:", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] }); console.log("📊 화면 표시 데이터:", {
count: tableDisplayData?.length,
firstRow: tableDisplayData?.[0],
});
setSelectedRowsData(selectedData); setSelectedRowsData(selectedData);
setTableSortBy(sortBy); setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc"); setTableSortOrder(sortOrder || "asc");
@ -547,7 +561,9 @@ function ScreenViewPage() {
}} }}
> >
{/* 자식 컴포넌트들 */} {/* 자식 컴포넌트들 */}
{(component.type === "group" || component.type === "container" || component.type === "area") && {(component.type === "group" ||
component.type === "container" ||
component.type === "area") &&
layout.components layout.components
.filter((child) => child.parentId === component.id) .filter((child) => child.parentId === component.id)
.map((child) => { .map((child) => {
@ -580,10 +596,20 @@ function ScreenViewPage() {
sortOrder={tableSortOrder} sortOrder={tableSortOrder}
columnOrder={tableColumnOrder} columnOrder={tableColumnOrder}
tableDisplayData={tableDisplayData} tableDisplayData={tableDisplayData}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { onSelectedRowsChange={(
_,
selectedData,
sortBy,
sortOrder,
columnOrder,
tableDisplayData,
) => {
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder }); console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder });
console.log("📊 화면 표시 데이터 (자식):", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] }); console.log("📊 화면 표시 데이터 (자식):", {
count: tableDisplayData?.length,
firstRow: tableDisplayData?.[0],
});
setSelectedRowsData(selectedData); setSelectedRowsData(selectedData);
setTableSortBy(sortBy); setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc"); setTableSortOrder(sortOrder || "asc");

View File

@ -470,7 +470,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */} {/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white"> <main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">
<div className="h-full w-full p-4">{children}</div> {children}
</main> </main>
</div> </div>

View File

@ -305,69 +305,143 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
} }
try { try {
// 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 수정 // 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 처리 (추가/수정/삭제)
if (groupData.length > 0) { if (groupData.length > 0 || originalGroupData.length > 0) {
console.log("🔄 그룹 데이터 일괄 수정 시작:", { console.log("🔄 그룹 데이터 일괄 처리 시작:", {
groupDataLength: groupData.length, groupDataLength: groupData.length,
originalGroupDataLength: originalGroupData.length, originalGroupDataLength: originalGroupData.length,
groupData,
originalGroupData,
tableName: screenData.screenInfo.tableName,
screenId: modalState.screenId,
}); });
let insertedCount = 0;
let updatedCount = 0; let updatedCount = 0;
let deletedCount = 0;
for (let i = 0; i < groupData.length; i++) { // 1⃣ 신규 품목 추가 (id가 없는 항목)
const currentData = groupData[i]; for (const currentData of groupData) {
const originalItemData = originalGroupData[i]; if (!currentData.id) {
console.log(" 신규 품목 추가:", currentData);
console.log("📋 [신규 품목] currentData 키 목록:", Object.keys(currentData));
// 🆕 모든 데이터를 포함 (id 제외)
const insertData: Record<string, any> = { ...currentData };
console.log("📦 [신규 품목] 복사 직후 insertData:", insertData);
console.log("📋 [신규 품목] insertData 키 목록:", Object.keys(insertData));
delete insertData.id; // id는 자동 생성되므로 제거
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
modalState.groupByColumns.forEach((colName) => {
// 기존 품목(originalGroupData[0])에서 groupByColumns 값 가져오기
const referenceData = originalGroupData[0] || groupData.find((item) => item.id);
if (referenceData && referenceData[colName]) {
insertData[colName] = referenceData[colName];
console.log(`🔑 [신규 품목] ${colName} 값 추가:`, referenceData[colName]);
}
});
}
// 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등)
// formData에서 품목별 필드가 아닌 공통 필드를 복사
const commonFields = [
'partner_id', // 거래처
'manager_id', // 담당자
'delivery_partner_id', // 납품처
'delivery_address', // 납품장소
'memo', // 메모
'order_date', // 주문일
'due_date', // 납기일
'shipping_method', // 배송방법
'status', // 상태
'sales_type', // 영업유형
];
commonFields.forEach((fieldName) => {
// formData에 값이 있으면 추가
if (formData[fieldName] !== undefined && formData[fieldName] !== null) {
insertData[fieldName] = formData[fieldName];
console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
}
});
console.log("📦 [신규 품목] 최종 insertData:", insertData);
console.log("📋 [신규 품목] 최종 insertData 키 목록:", Object.keys(insertData));
try {
const response = await dynamicFormApi.saveFormData({
screenId: modalState.screenId || 0,
tableName: screenData.screenInfo.tableName,
data: insertData,
});
if (response.success) {
insertedCount++;
console.log("✅ 신규 품목 추가 성공:", response.data);
} else {
console.error("❌ 신규 품목 추가 실패:", response.message);
}
} catch (error: any) {
console.error("❌ 신규 품목 추가 오류:", error);
}
}
}
// 2⃣ 기존 품목 수정 (id가 있는 항목)
for (const currentData of groupData) {
if (currentData.id) {
// id 기반 매칭 (인덱스 기반 X)
const originalItemData = originalGroupData.find(
(orig) => orig.id === currentData.id
);
if (!originalItemData) { if (!originalItemData) {
console.warn(`원본 데이터가 없습니다 (index: ${i})`); console.warn(`원본 데이터를 찾을 수 없습니다 (id: ${currentData.id})`);
continue; continue;
} }
// 변경된 필드만 추출 // 🆕 값 정규화 함수 (타입 통일)
const normalizeValue = (val: any): any => {
if (val === null || val === undefined || val === "") return null;
if (typeof val === "string" && !isNaN(Number(val))) {
// 숫자로 변환 가능한 문자열은 숫자로
return Number(val);
}
return val;
};
// 변경된 필드만 추출 (id 제외)
const changedData: Record<string, any> = {}; const changedData: Record<string, any> = {};
// 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외)
const salesOrderColumns = [
"id",
"order_no",
"customer_code",
"customer_name",
"order_date",
"delivery_date",
"item_code",
"quantity",
"unit_price",
"amount",
"status",
"notes",
"created_at",
"updated_at",
"company_code",
];
Object.keys(currentData).forEach((key) => { Object.keys(currentData).forEach((key) => {
// sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외) // id는 변경 불가
if (!salesOrderColumns.includes(key)) { if (key === "id") {
return; return;
} }
if (currentData[key] !== originalItemData[key]) { // 🆕 타입 정규화 후 비교
changedData[key] = currentData[key]; const currentValue = normalizeValue(currentData[key]);
const originalValue = normalizeValue(originalItemData[key]);
// 값이 변경된 경우만 포함
if (currentValue !== originalValue) {
console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue}${currentValue}`);
changedData[key] = currentData[key]; // 원본 값 사용 (문자열 그대로)
} }
}); });
// 변경사항이 없으면 스킵 // 변경사항이 없으면 스킵
if (Object.keys(changedData).length === 0) { if (Object.keys(changedData).length === 0) {
console.log(`변경사항 없음 (index: ${i})`); console.log(`변경사항 없음 (id: ${currentData.id})`);
continue; continue;
} }
// 기본키 확인
const recordId = originalItemData.id || Object.values(originalItemData)[0];
// UPDATE 실행 // UPDATE 실행
try {
const response = await dynamicFormApi.updateFormDataPartial( const response = await dynamicFormApi.updateFormDataPartial(
recordId, currentData.id,
originalItemData, originalItemData,
changedData, changedData,
screenData.screenInfo.tableName, screenData.screenInfo.tableName,
@ -375,14 +449,50 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
if (response.success) { if (response.success) {
updatedCount++; updatedCount++;
console.log(`✅ 품목 ${i + 1} 수정 성공 (id: ${recordId})`); console.log(`✅ 품목 수정 성공 (id: ${currentData.id})`);
} else { } else {
console.error(`❌ 품목 ${i + 1} 수정 실패 (id: ${recordId}):`, response.message); console.error(`❌ 품목 수정 실패 (id: ${currentData.id}):`, response.message);
}
} catch (error: any) {
console.error(`❌ 품목 수정 오류 (id: ${currentData.id}):`, error);
}
} }
} }
if (updatedCount > 0) { // 3⃣ 삭제된 품목 제거 (원본에는 있지만 현재 데이터에는 없는 항목)
toast.success(`${updatedCount}개의 품목이 수정되었습니다.`); const currentIds = new Set(groupData.map((item) => item.id).filter(Boolean));
const deletedItems = originalGroupData.filter(
(orig) => orig.id && !currentIds.has(orig.id)
);
for (const deletedItem of deletedItems) {
console.log("🗑️ 품목 삭제:", deletedItem);
try {
const response = await dynamicFormApi.deleteFormDataFromTable(
deletedItem.id,
screenData.screenInfo.tableName
);
if (response.success) {
deletedCount++;
console.log(`✅ 품목 삭제 성공 (id: ${deletedItem.id})`);
} else {
console.error(`❌ 품목 삭제 실패 (id: ${deletedItem.id}):`, response.message);
}
} catch (error: any) {
console.error(`❌ 품목 삭제 오류 (id: ${deletedItem.id}):`, error);
}
}
// 결과 메시지
const messages: string[] = [];
if (insertedCount > 0) messages.push(`${insertedCount}개 추가`);
if (updatedCount > 0) messages.push(`${updatedCount}개 수정`);
if (deletedCount > 0) messages.push(`${deletedCount}개 삭제`);
if (messages.length > 0) {
toast.success(`품목이 저장되었습니다 (${messages.join(", ")})`);
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침) // 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
if (modalState.onSave) { if (modalState.onSave) {
@ -585,8 +695,11 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
tableName: screenData.screenInfo?.tableName, tableName: screenData.screenInfo?.tableName,
}} }}
onSave={handleSave} onSave={handleSave}
isInModal={true}
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달 // 🆕 그룹 데이터를 ModalRepeaterTable에 전달
groupedData={groupData.length > 0 ? groupData : undefined} groupedData={groupData.length > 0 ? groupData : undefined}
// 🆕 수정 모달에서 읽기 전용 필드 지정 (수주번호, 거래처)
disabledFields={["order_no", "partner_id"]}
/> />
); );
})} })}

View File

@ -346,6 +346,14 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
// 실제 사용 가능한 위젯 렌더링 // 실제 사용 가능한 위젯 렌더링
const renderInteractiveWidget = (comp: ComponentData) => { const renderInteractiveWidget = (comp: ComponentData) => {
console.log("🎯 renderInteractiveWidget 호출:", {
type: comp.type,
id: comp.id,
componentId: (comp as any).componentId,
hasComponentConfig: !!(comp as any).componentConfig,
componentConfig: (comp as any).componentConfig,
});
// 데이터 테이블 컴포넌트 처리 // 데이터 테이블 컴포넌트 처리
if (comp.type === "datatable") { if (comp.type === "datatable") {
return ( return (
@ -397,6 +405,39 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
); );
} }
// 탭 컴포넌트 처리
const componentType = (comp as any).componentType || (comp as any).componentId;
if (comp.type === "tabs" || (comp.type === "component" && componentType === "tabs-widget")) {
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
// componentConfig에서 탭 정보 추출
const tabsConfig = comp.componentConfig || {};
const tabsComponent = {
...comp,
type: "tabs" as const,
tabs: tabsConfig.tabs || [],
defaultTab: tabsConfig.defaultTab,
orientation: tabsConfig.orientation || "horizontal",
variant: tabsConfig.variant || "default",
allowCloseable: tabsConfig.allowCloseable || false,
persistSelection: tabsConfig.persistSelection || false,
};
console.log("🔍 탭 컴포넌트 렌더링:", {
originalType: comp.type,
componentType,
componentId: (comp as any).componentId,
tabs: tabsComponent.tabs,
tabsConfig,
});
return (
<div className="h-full w-full">
<TabsWidget component={tabsComponent as any} />
</div>
);
}
const { widgetType, label, placeholder, required, readonly, columnName } = comp; const { widgetType, label, placeholder, required, readonly, columnName } = comp;
const fieldName = columnName || comp.id; const fieldName = columnName || comp.id;
const currentValue = formData[fieldName] || ""; const currentValue = formData[fieldName] || "";

View File

@ -48,6 +48,10 @@ interface InteractiveScreenViewerProps {
companyCode?: string; companyCode?: string;
// 🆕 그룹 데이터 (EditModal에서 전달) // 🆕 그룹 데이터 (EditModal에서 전달)
groupedData?: Record<string, any>[]; groupedData?: Record<string, any>[];
// 🆕 비활성화할 필드 목록 (EditModal에서 전달)
disabledFields?: string[];
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
isInModal?: boolean;
} }
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
@ -64,6 +68,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
userName: externalUserName, userName: externalUserName,
companyCode: externalCompanyCode, companyCode: externalCompanyCode,
groupedData, groupedData,
disabledFields = [],
isInModal = false,
}) => { }) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { userName: authUserName, user: authUser } = useAuth(); const { userName: authUserName, user: authUser } = useAuth();
@ -329,6 +335,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
userId={user?.userId} // ✅ 사용자 ID 전달 userId={user?.userId} // ✅ 사용자 ID 전달
userName={user?.userName} // ✅ 사용자 이름 전달 userName={user?.userName} // ✅ 사용자 이름 전달
companyCode={user?.companyCode} // ✅ 회사 코드 전달 companyCode={user?.companyCode} // ✅ 회사 코드 전달
onSave={onSave} // 🆕 EditModal의 handleSave 콜백 전달
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용) allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
selectedRowsData={selectedRowsData} selectedRowsData={selectedRowsData}
onSelectedRowsChange={(selectedRows, selectedData) => { onSelectedRowsChange={(selectedRows, selectedData) => {
@ -337,6 +344,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
}} }}
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable) // 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
groupedData={groupedData} groupedData={groupedData}
// 🆕 비활성화 필드 전달 (EditModal → 각 컴포넌트)
disabledFields={disabledFields}
flowSelectedData={flowSelectedData} flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId} flowSelectedStepId={flowSelectedStepId}
onFlowSelectedDataChange={(selectedData, stepId) => { onFlowSelectedDataChange={(selectedData, stepId) => {
@ -401,6 +410,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
required: required, required: required,
placeholder: placeholder, placeholder: placeholder,
className: "w-full h-full", className: "w-full h-full",
isInModal: isInModal, // 🆕 EditModal 내부 여부 전달
onSave: onSave, // 🆕 EditModal의 handleSave 콜백 전달
}} }}
config={widget.webTypeConfig} config={widget.webTypeConfig}
onEvent={(event: string, data: any) => { onEvent={(event: string, data: any) => {

View File

@ -554,6 +554,73 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
); );
})()} })()}
{/* 탭 컴포넌트 타입 */}
{(type === "tabs" || (type === "component" && ((component as any).componentType === "tabs-widget" || (component as any).componentId === "tabs-widget"))) &&
(() => {
console.log("🎯 탭 컴포넌트 조건 충족:", {
type,
componentType: (component as any).componentType,
componentId: (component as any).componentId,
isDesignMode,
});
if (isDesignMode) {
// 디자인 모드: 미리보기 표시
const tabsComponent = component as any;
const tabs = tabsComponent.componentConfig?.tabs || tabsComponent.tabs || [];
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<div className="text-center">
<div className="flex items-center justify-center">
<Folder className="h-8 w-8 text-gray-400" />
</div>
<p className="text-muted-foreground mt-2 text-sm font-medium"> </p>
<p className="text-xs text-gray-400">
{tabs.length > 0
? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)`
: "탭이 없습니다. 설정 패널에서 탭을 추가하세요"}
</p>
{tabs.length > 0 && (
<div className="mt-2 flex flex-wrap justify-center gap-1">
{tabs.map((tab: any, index: number) => (
<Badge key={tab.id} variant="outline" className="text-xs">
{tab.label || `${index + 1}`}
{tab.screenName && (
<span className="ml-1 text-[10px] text-gray-400">
({tab.screenName})
</span>
)}
</Badge>
))}
</div>
)}
</div>
</div>
);
} else {
// 실제 화면: TabsWidget 렌더링
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
const tabsConfig = (component as any).componentConfig || {};
const tabsComponent = {
...component,
type: "tabs" as const,
tabs: tabsConfig.tabs || [],
defaultTab: tabsConfig.defaultTab,
orientation: tabsConfig.orientation || "horizontal",
variant: tabsConfig.variant || "default",
allowCloseable: tabsConfig.allowCloseable || false,
persistSelection: tabsConfig.persistSelection || false,
};
return (
<div className="h-full w-full">
<TabsWidget component={tabsComponent as any} />
</div>
);
}
})()}
{/* 그룹 타입 */} {/* 그룹 타입 */}
{type === "group" && ( {type === "group" && (
<div className="relative h-full w-full"> <div className="relative h-full w-full">

View File

@ -35,9 +35,9 @@ export const ResponsiveDesignerContainer: React.FC<ResponsiveDesignerContainerPr
switch (viewMode) { switch (viewMode) {
case "fit": case "fit":
// 컨테이너에 맞춰 비율 유지하며 조정 (여백 허용) // 컨테이너에 맞춰 비율 유지하며 조정 (좌우 여백 16px씩 유지)
const scaleX = (containerSize.width - 40) / designWidth; const scaleX = (containerSize.width - 32) / designWidth;
const scaleY = (containerSize.height - 40) / designHeight; const scaleY = (containerSize.height - 64) / designHeight;
return Math.min(scaleX, scaleY, 2); // 최대 2배까지 허용 return Math.min(scaleX, scaleY, 2); // 최대 2배까지 허용
case "custom": case "custom":
@ -154,7 +154,7 @@ export const ResponsiveDesignerContainer: React.FC<ResponsiveDesignerContainerPr
{/* 디자인 영역 */} {/* 디자인 영역 */}
<div <div
ref={containerRef} ref={containerRef}
className="flex-1 overflow-auto p-8" className="flex-1 overflow-auto px-4 py-8"
style={{ style={{
justifyContent: "center", justifyContent: "center",
alignItems: "flex-start", alignItems: "flex-start",

View File

@ -981,7 +981,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
// 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트 // 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
// 입력 필드에서는 스페이스바 무시 // 입력 필드에서는 스페이스바 무시 (activeElement로 정확하게 체크)
const activeElement = document.activeElement;
if (
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
activeElement?.getAttribute('contenteditable') === 'true' ||
activeElement?.getAttribute('role') === 'textbox'
) {
return;
}
// e.target도 함께 체크 (이중 방어)
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return; return;
} }
@ -997,6 +1008,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
}; };
const handleKeyUp = (e: KeyboardEvent) => { const handleKeyUp = (e: KeyboardEvent) => {
// 입력 필드에서는 스페이스바 무시
const activeElement = document.activeElement;
if (
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
activeElement?.getAttribute('contenteditable') === 'true' ||
activeElement?.getAttribute('role') === 'textbox'
) {
return;
}
if (e.code === "Space") { if (e.code === "Space") {
e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단 e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단
setIsPanMode(false); setIsPanMode(false);

View File

@ -35,7 +35,10 @@ import {
DialogDescription, DialogDescription,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash, Check, ChevronsUpDown } from "lucide-react";
import { ScreenDefinition } from "@/types/screen"; import { ScreenDefinition } from "@/types/screen";
import { screenApi } from "@/lib/api/screen"; import { screenApi } from "@/lib/api/screen";
import CreateScreenModal from "./CreateScreenModal"; import CreateScreenModal from "./CreateScreenModal";
@ -127,8 +130,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
isActive: "Y", isActive: "Y",
tableName: "", tableName: "",
}); });
const [tables, setTables] = useState<string[]>([]); const [tables, setTables] = useState<Array<{ tableName: string; tableLabel: string }>>([]);
const [loadingTables, setLoadingTables] = useState(false); const [loadingTables, setLoadingTables] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
// 미리보기 관련 상태 // 미리보기 관련 상태
const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
@ -279,9 +283,12 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
const { tableManagementApi } = await import("@/lib/api/tableManagement"); const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableList(); const response = await tableManagementApi.getTableList();
if (response.success && response.data) { if (response.success && response.data) {
// tableName만 추출 (camelCase) // tableName과 displayName 매핑 (백엔드에서 displayName으로 라벨을 반환함)
const tableNames = response.data.map((table: any) => table.tableName); const tableList = response.data.map((table: any) => ({
setTables(tableNames); tableName: table.tableName,
tableLabel: table.displayName || table.tableName,
}));
setTables(tableList);
} }
} catch (error) { } catch (error) {
console.error("테이블 목록 조회 실패:", error); console.error("테이블 목록 조회 실패:", error);
@ -297,6 +304,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
// 화면 정보 업데이트 API 호출 // 화면 정보 업데이트 API 호출
await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData); await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData);
// 선택된 테이블의 라벨 찾기
const selectedTable = tables.find((t) => t.tableName === editFormData.tableName);
const tableLabel = selectedTable?.tableLabel || editFormData.tableName;
// 목록에서 해당 화면 정보 업데이트 // 목록에서 해당 화면 정보 업데이트
setScreens((prev) => setScreens((prev) =>
prev.map((s) => prev.map((s) =>
@ -304,6 +315,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
? { ? {
...s, ...s,
screenName: editFormData.screenName, screenName: editFormData.screenName,
tableName: editFormData.tableName,
tableLabel: tableLabel,
description: editFormData.description, description: editFormData.description,
isActive: editFormData.isActive, isActive: editFormData.isActive,
} }
@ -1202,22 +1215,62 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-tableName"> *</Label> <Label htmlFor="edit-tableName"> *</Label>
<Select <Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
value={editFormData.tableName} <PopoverTrigger asChild>
onValueChange={(value) => setEditFormData({ ...editFormData, tableName: value })} <Button
variant="outline"
role="combobox"
aria-expanded={tableComboboxOpen}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
disabled={loadingTables} disabled={loadingTables}
> >
<SelectTrigger id="edit-tableName"> {loadingTables
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"} /> ? "로딩 중..."
</SelectTrigger> : editFormData.tableName
<SelectContent> ? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
{tables.map((tableName) => ( : "테이블을 선택하세요"}
<SelectItem key={tableName} value={tableName}> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
{tableName} </Button>
</SelectItem> </PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm">
.
</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.tableName} ${table.tableLabel}`}
onSelect={() => {
setEditFormData({ ...editFormData, tableName: table.tableName });
setTableComboboxOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
editFormData.tableName === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{table.tableLabel}</span>
<span className="text-[10px] text-gray-500">{table.tableName}</span>
</div>
</CommandItem>
))} ))}
</SelectContent> </CommandGroup>
</Select> </CommandList>
</Command>
</PopoverContent>
</Popover>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-description"></Label> <Label htmlFor="edit-description"></Label>

View File

@ -49,7 +49,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderWidth || ""} value={localStyle.borderWidth || ""}
onChange={(e) => handleStyleChange("borderWidth", e.target.value)} onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -60,20 +59,20 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderStyle || "solid"} value={localStyle.borderStyle || "solid"}
onValueChange={(value) => handleStyleChange("borderStyle", value)} onValueChange={(value) => handleStyleChange("borderStyle", value)}
> >
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}> <SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="solid" style={{ fontSize: "12px" }}> <SelectItem value="solid" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="dashed" style={{ fontSize: "12px" }}> <SelectItem value="dashed" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="dotted" style={{ fontSize: "12px" }}> <SelectItem value="dotted" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="none" style={{ fontSize: "12px" }}> <SelectItem value="none" className="text-xs">
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -93,7 +92,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderColor || "#000000"} value={localStyle.borderColor || "#000000"}
onChange={(e) => handleStyleChange("borderColor", e.target.value)} onChange={(e) => handleStyleChange("borderColor", e.target.value)}
className="h-6 w-12 p-1" className="h-6 w-12 p-1"
style={{ fontSize: "12px" }} className="text-xs"
/> />
<Input <Input
type="text" type="text"
@ -101,7 +100,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
onChange={(e) => handleStyleChange("borderColor", e.target.value)} onChange={(e) => handleStyleChange("borderColor", e.target.value)}
placeholder="#000000" placeholder="#000000"
className="h-6 flex-1 text-xs" className="h-6 flex-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -116,7 +114,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderRadius || ""} value={localStyle.borderRadius || ""}
onChange={(e) => handleStyleChange("borderRadius", e.target.value)} onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -142,7 +139,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.backgroundColor || "#ffffff"} value={localStyle.backgroundColor || "#ffffff"}
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)} onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
className="h-6 w-12 p-1" className="h-6 w-12 p-1"
style={{ fontSize: "12px" }} className="text-xs"
/> />
<Input <Input
type="text" type="text"
@ -150,7 +147,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)} onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
placeholder="#ffffff" placeholder="#ffffff"
className="h-6 flex-1 text-xs" className="h-6 flex-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -166,7 +162,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.backgroundImage || ""} value={localStyle.backgroundImage || ""}
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)} onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
( ) ( )
@ -195,7 +190,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.color || "#000000"} value={localStyle.color || "#000000"}
onChange={(e) => handleStyleChange("color", e.target.value)} onChange={(e) => handleStyleChange("color", e.target.value)}
className="h-6 w-12 p-1" className="h-6 w-12 p-1"
style={{ fontSize: "12px" }} className="text-xs"
/> />
<Input <Input
type="text" type="text"
@ -203,7 +198,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
onChange={(e) => handleStyleChange("color", e.target.value)} onChange={(e) => handleStyleChange("color", e.target.value)}
placeholder="#000000" placeholder="#000000"
className="h-6 flex-1 text-xs" className="h-6 flex-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -218,7 +212,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.fontSize || ""} value={localStyle.fontSize || ""}
onChange={(e) => handleStyleChange("fontSize", e.target.value)} onChange={(e) => handleStyleChange("fontSize", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -232,29 +225,29 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.fontWeight || "normal"} value={localStyle.fontWeight || "normal"}
onValueChange={(value) => handleStyleChange("fontWeight", value)} onValueChange={(value) => handleStyleChange("fontWeight", value)}
> >
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}> <SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="normal" style={{ fontSize: "12px" }}> <SelectItem value="normal" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="bold" style={{ fontSize: "12px" }}> <SelectItem value="bold" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="100" style={{ fontSize: "12px" }}> <SelectItem value="100" className="text-xs">
100 100
</SelectItem> </SelectItem>
<SelectItem value="400" style={{ fontSize: "12px" }}> <SelectItem value="400" className="text-xs">
400 400
</SelectItem> </SelectItem>
<SelectItem value="500" style={{ fontSize: "12px" }}> <SelectItem value="500" className="text-xs">
500 500
</SelectItem> </SelectItem>
<SelectItem value="600" style={{ fontSize: "12px" }}> <SelectItem value="600" className="text-xs">
600 600
</SelectItem> </SelectItem>
<SelectItem value="700" style={{ fontSize: "12px" }}> <SelectItem value="700" className="text-xs">
700 700
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -268,20 +261,20 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.textAlign || "left"} value={localStyle.textAlign || "left"}
onValueChange={(value) => handleStyleChange("textAlign", value)} onValueChange={(value) => handleStyleChange("textAlign", value)}
> >
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}> <SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="left" style={{ fontSize: "12px" }}> <SelectItem value="left" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="center" style={{ fontSize: "12px" }}> <SelectItem value="center" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="right" style={{ fontSize: "12px" }}> <SelectItem value="right" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="justify" style={{ fontSize: "12px" }}> <SelectItem value="justify" className="text-xs">
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>

View File

@ -509,7 +509,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox" role="combobox"
aria-expanded={modalScreenOpen} aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0" className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }} className="text-xs"
disabled={screensLoading} disabled={screensLoading}
> >
{config.action?.targetScreenId {config.action?.targetScreenId
@ -900,7 +900,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox" role="combobox"
aria-expanded={modalScreenOpen} aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0" className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }} className="text-xs"
disabled={screensLoading} disabled={screensLoading}
> >
{config.action?.targetScreenId {config.action?.targetScreenId
@ -978,7 +978,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox" role="combobox"
aria-expanded={modalScreenOpen} aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0" className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }} className="text-xs"
disabled={screensLoading} disabled={screensLoading}
> >
{config.action?.targetScreenId {config.action?.targetScreenId
@ -1132,7 +1132,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox" role="combobox"
aria-expanded={modalScreenOpen} aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0" className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }} className="text-xs"
disabled={screensLoading} disabled={screensLoading}
> >
{config.action?.targetScreenId {config.action?.targetScreenId
@ -1286,7 +1286,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox" role="combobox"
aria-expanded={displayColumnOpen} aria-expanded={displayColumnOpen}
className="mt-2 h-8 w-full justify-between text-xs" className="mt-2 h-8 w-full justify-between text-xs"
style={{ fontSize: "12px" }}
disabled={columnsLoading || tableColumns.length === 0} disabled={columnsLoading || tableColumns.length === 0}
> >
{columnsLoading {columnsLoading
@ -1301,9 +1300,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start"> <PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command> <Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" style={{ fontSize: "12px" }} /> <CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList> <CommandList>
<CommandEmpty className="text-xs" style={{ fontSize: "12px" }}> <CommandEmpty className="text-xs">
. .
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
@ -1316,7 +1315,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
setDisplayColumnOpen(false); setDisplayColumnOpen(false);
}} }}
className="text-xs" className="text-xs"
style={{ fontSize: "12px" }}
> >
<Check <Check
className={cn( className={cn(
@ -1350,7 +1348,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox" role="combobox"
aria-expanded={navScreenOpen} aria-expanded={navScreenOpen}
className="h-6 w-full justify-between px-2 py-0" className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }} className="text-xs"
disabled={screensLoading} disabled={screensLoading}
> >
{config.action?.targetScreenId {config.action?.targetScreenId
@ -1424,7 +1422,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
onUpdateProperty("componentConfig.action.targetUrl", newValue); onUpdateProperty("componentConfig.action.targetUrl", newValue);
}} }}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
<p className="mt-1 text-xs text-muted-foreground">URL을 </p> <p className="mt-1 text-xs text-muted-foreground">URL을 </p>
</div> </div>

View File

@ -117,7 +117,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<CheckSquare className="h-4 w-4" /> <CheckSquare className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -173,7 +173,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.label || ""} value={localConfig.label || ""}
onChange={(e) => updateConfig("label", e.target.value)} onChange={(e) => updateConfig("label", e.target.value)}
placeholder="체크박스 라벨" placeholder="체크박스 라벨"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -187,7 +187,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.checkedValue || ""} value={localConfig.checkedValue || ""}
onChange={(e) => updateConfig("checkedValue", e.target.value)} onChange={(e) => updateConfig("checkedValue", e.target.value)}
placeholder="Y" placeholder="Y"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -199,7 +199,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.uncheckedValue || ""} value={localConfig.uncheckedValue || ""}
onChange={(e) => updateConfig("uncheckedValue", e.target.value)} onChange={(e) => updateConfig("uncheckedValue", e.target.value)}
placeholder="N" placeholder="N"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>
@ -232,7 +232,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupLabel || ""} value={localConfig.groupLabel || ""}
onChange={(e) => updateConfig("groupLabel", e.target.value)} onChange={(e) => updateConfig("groupLabel", e.target.value)}
placeholder="체크박스 그룹 제목" placeholder="체크박스 그룹 제목"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -244,19 +244,19 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel} value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)} onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={newOptionValue} value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)} onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값" placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Button <Button
size="sm" size="sm"
onClick={addOption} onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()} disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
</Button> </Button>
@ -277,13 +277,13 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label} value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)} onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={option.value} value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)} onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값" placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Switch <Switch
checked={!option.disabled} checked={!option.disabled}
@ -361,7 +361,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly} disabled={localConfig.readonly}
required={localConfig.required} required={localConfig.required}
defaultChecked={localConfig.defaultChecked} defaultChecked={localConfig.defaultChecked}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<Label htmlFor="preview-single" className="text-xs"> <Label htmlFor="preview-single" className="text-xs">
{localConfig.label || "체크박스 라벨"} {localConfig.label || "체크박스 라벨"}
@ -380,7 +380,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly || option.disabled} disabled={localConfig.readonly || option.disabled}
required={localConfig.required && index === 0} // 첫 번째에만 required 표시 required={localConfig.required && index === 0} // 첫 번째에만 required 표시
defaultChecked={option.checked} defaultChecked={option.checked}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<Label htmlFor={`preview-group-${index}`} className="text-xs"> <Label htmlFor={`preview-group-${index}`} className="text-xs">
{option.label} {option.label}

View File

@ -106,7 +106,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<Code className="h-4 w-4" /> <Code className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -174,7 +174,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
step={50} step={50}
value={localConfig.height || 300} value={localConfig.height || 300}
onChange={(e) => updateConfig("height", parseInt(e.target.value))} onChange={(e) => updateConfig("height", parseInt(e.target.value))}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<div className="text-muted-foreground flex justify-between text-xs"> <div className="text-muted-foreground flex justify-between text-xs">
<span>150px</span> <span>150px</span>
@ -199,7 +199,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("fontSize", parseInt(e.target.value))} onChange={(e) => updateConfig("fontSize", parseInt(e.target.value))}
min={10} min={10}
max={24} max={24}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -214,7 +214,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("tabSize", parseInt(e.target.value))} onChange={(e) => updateConfig("tabSize", parseInt(e.target.value))}
min={1} min={1}
max={8} max={8}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>
@ -308,7 +308,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""} value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="코드를 입력하세요..." placeholder="코드를 입력하세요..."
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -330,7 +330,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.defaultValue || ""} value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)} onChange={(e) => updateConfig("defaultValue", e.target.value)}
placeholder="기본 코드 내용" placeholder="기본 코드 내용"
className="font-mono text-xs" style={{ fontSize: "12px" }} className="font-mono text-xs"
rows={4} rows={4}
/> />
</div> </div>

View File

@ -75,7 +75,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -95,7 +95,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""} value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="날짜를 선택하세요" placeholder="날짜를 선택하세요"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -149,7 +149,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"} type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.minDate || ""} value={localConfig.minDate || ""}
onChange={(e) => updateConfig("minDate", e.target.value)} onChange={(e) => updateConfig("minDate", e.target.value)}
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Button size="sm" variant="outline" onClick={() => setCurrentDate("minDate")} className="text-xs"> <Button size="sm" variant="outline" onClick={() => setCurrentDate("minDate")} className="text-xs">
@ -167,7 +167,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"} type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.maxDate || ""} value={localConfig.maxDate || ""}
onChange={(e) => updateConfig("maxDate", e.target.value)} onChange={(e) => updateConfig("maxDate", e.target.value)}
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Button size="sm" variant="outline" onClick={() => setCurrentDate("maxDate")} className="text-xs"> <Button size="sm" variant="outline" onClick={() => setCurrentDate("maxDate")} className="text-xs">
@ -190,7 +190,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"} type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.defaultValue || ""} value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)} onChange={(e) => updateConfig("defaultValue", e.target.value)}
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Button size="sm" variant="outline" onClick={() => setCurrentDate("defaultValue")} className="text-xs"> <Button size="sm" variant="outline" onClick={() => setCurrentDate("defaultValue")} className="text-xs">
@ -245,7 +245,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={localConfig.minDate} min={localConfig.minDate}
max={localConfig.maxDate} max={localConfig.maxDate}
defaultValue={localConfig.defaultValue} defaultValue={localConfig.defaultValue}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<div className="text-muted-foreground mt-2 text-xs"> <div className="text-muted-foreground mt-2 text-xs">
: {localConfig.format} : {localConfig.format}

View File

@ -51,9 +51,11 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
const [newFieldName, setNewFieldName] = useState(""); const [newFieldName, setNewFieldName] = useState("");
const [newFieldLabel, setNewFieldLabel] = useState(""); const [newFieldLabel, setNewFieldLabel] = useState("");
const [newFieldType, setNewFieldType] = useState("string"); const [newFieldType, setNewFieldType] = useState("string");
const [isUserEditing, setIsUserEditing] = useState(false);
// 컴포넌트 변경 시 로컬 상태 동기화 // 컴포넌트 변경 시 로컬 상태 동기화 (사용자가 입력 중이 아닐 때만)
useEffect(() => { useEffect(() => {
if (!isUserEditing) {
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {}; const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
setLocalConfig({ setLocalConfig({
entityType: currentConfig.entityType || "", entityType: currentConfig.entityType || "",
@ -73,15 +75,28 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
apiEndpoint: currentConfig.apiEndpoint || "", apiEndpoint: currentConfig.apiEndpoint || "",
filters: currentConfig.filters || {}, filters: currentConfig.filters || {},
}); });
}, [widget.webTypeConfig]); }
}, [widget.webTypeConfig, isUserEditing]);
// 설정 업데이트 핸들러 // 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
const updateConfig = (field: keyof EntityTypeConfig, value: any) => { const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
const newConfig = { ...localConfig, [field]: value }; const newConfig = { ...localConfig, [field]: value };
setLocalConfig(newConfig); setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig); onUpdateProperty("webTypeConfig", newConfig);
}; };
// 입력 필드용 업데이트 (로컬 상태만)
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
setIsUserEditing(true);
setLocalConfig({ ...localConfig, [field]: value });
};
// 입력 완료 시 부모에게 전달
const handleInputBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig);
};
// 필드 추가 // 필드 추가
const addDisplayField = () => { const addDisplayField = () => {
if (!newFieldName.trim() || !newFieldLabel.trim()) return; if (!newFieldName.trim() || !newFieldLabel.trim()) return;
@ -106,11 +121,18 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
updateConfig("displayFields", newFields); updateConfig("displayFields", newFields);
}; };
// 필드 업데이트 // 필드 업데이트 (입력 중)
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => { const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
setIsUserEditing(true);
const newFields = [...localConfig.displayFields]; const newFields = [...localConfig.displayFields];
newFields[index] = { ...newFields[index], [field]: value }; newFields[index] = { ...newFields[index], [field]: value };
updateConfig("displayFields", newFields); setLocalConfig({ ...localConfig, displayFields: newFields });
};
// 필드 업데이트 완료 (onBlur)
const handleFieldBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig);
}; };
// 검색 필드 토글 // 검색 필드 토글
@ -163,7 +185,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<Database className="h-4 w-4" /> <Database className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -181,9 +203,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input <Input
id="entityType" id="entityType"
value={localConfig.entityType || ""} value={localConfig.entityType || ""}
onChange={(e) => updateConfig("entityType", e.target.value)} onChange={(e) => updateConfigLocal("entityType", e.target.value)}
onBlur={handleInputBlur}
placeholder="user, product, department..." placeholder="user, product, department..."
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -196,7 +219,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => applyEntityType(entity.value)} onClick={() => applyEntityType(entity.value)}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
> >
{entity.label} {entity.label}
</Button> </Button>
@ -211,9 +234,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input <Input
id="apiEndpoint" id="apiEndpoint"
value={localConfig.apiEndpoint || ""} value={localConfig.apiEndpoint || ""}
onChange={(e) => updateConfig("apiEndpoint", e.target.value)} onChange={(e) => updateConfigLocal("apiEndpoint", e.target.value)}
onBlur={handleInputBlur}
placeholder="/api/entities/user" placeholder="/api/entities/user"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>
@ -230,9 +254,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input <Input
id="valueField" id="valueField"
value={localConfig.valueField || ""} value={localConfig.valueField || ""}
onChange={(e) => updateConfig("valueField", e.target.value)} onChange={(e) => updateConfigLocal("valueField", e.target.value)}
onBlur={handleInputBlur}
placeholder="id" placeholder="id"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -243,9 +268,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input <Input
id="labelField" id="labelField"
value={localConfig.labelField || ""} value={localConfig.labelField || ""}
onChange={(e) => updateConfig("labelField", e.target.value)} onChange={(e) => updateConfigLocal("labelField", e.target.value)}
onBlur={handleInputBlur}
placeholder="name" placeholder="name"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>
@ -263,13 +289,13 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newFieldName} value={newFieldName}
onChange={(e) => setNewFieldName(e.target.value)} onChange={(e) => setNewFieldName(e.target.value)}
placeholder="필드명" placeholder="필드명"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={newFieldLabel} value={newFieldLabel}
onChange={(e) => setNewFieldLabel(e.target.value)} onChange={(e) => setNewFieldLabel(e.target.value)}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Select value={newFieldType} onValueChange={setNewFieldType}> <Select value={newFieldType} onValueChange={setNewFieldType}>
<SelectTrigger className="w-24 text-xs"> <SelectTrigger className="w-24 text-xs">
@ -287,7 +313,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm" size="sm"
onClick={addDisplayField} onClick={addDisplayField}
disabled={!newFieldName.trim() || !newFieldLabel.trim()} disabled={!newFieldName.trim() || !newFieldLabel.trim()}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
</Button> </Button>
@ -302,19 +328,24 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<div key={index} className="flex items-center gap-2 rounded border p-2"> <div key={index} className="flex items-center gap-2 rounded border p-2">
<Switch <Switch
checked={field.visible} checked={field.visible}
onCheckedChange={(checked) => updateDisplayField(index, "visible", checked)} onCheckedChange={(checked) => {
updateDisplayField(index, "visible", checked);
handleFieldBlur();
}}
/> />
<Input <Input
value={field.name} value={field.name}
onChange={(e) => updateDisplayField(index, "name", e.target.value)} onChange={(e) => updateDisplayField(index, "name", e.target.value)}
onBlur={handleFieldBlur}
placeholder="필드명" placeholder="필드명"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={field.label} value={field.label}
onChange={(e) => updateDisplayField(index, "label", e.target.value)} onChange={(e) => updateDisplayField(index, "label", e.target.value)}
onBlur={handleFieldBlur}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}> <Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}>
<SelectTrigger className="w-24 text-xs"> <SelectTrigger className="w-24 text-xs">
@ -332,7 +363,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm" size="sm"
variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"} variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"}
onClick={() => toggleSearchField(field.name)} onClick={() => toggleSearchField(field.name)}
className="p-1 text-xs" style={{ fontSize: "12px" }} className="p-1 text-xs"
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"} title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
> >
<Search className="h-3 w-3" /> <Search className="h-3 w-3" />
@ -341,7 +372,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm" size="sm"
variant="destructive" variant="destructive"
onClick={() => removeDisplayField(index)} onClick={() => removeDisplayField(index)}
className="p-1 text-xs" style={{ fontSize: "12px" }} className="p-1 text-xs"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
@ -362,9 +393,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input <Input
id="placeholder" id="placeholder"
value={localConfig.placeholder || ""} value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
onBlur={handleInputBlur}
placeholder="엔티티를 선택하세요" placeholder="엔티티를 선택하세요"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -375,9 +407,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input <Input
id="emptyMessage" id="emptyMessage"
value={localConfig.emptyMessage || ""} value={localConfig.emptyMessage || ""}
onChange={(e) => updateConfig("emptyMessage", e.target.value)} onChange={(e) => updateConfigLocal("emptyMessage", e.target.value)}
onBlur={handleInputBlur}
placeholder="검색 결과가 없습니다" placeholder="검색 결과가 없습니다"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -393,7 +426,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))} onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))}
min={0} min={0}
max={10} max={10}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -408,7 +441,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))} onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))}
min={5} min={5}
max={100} max={100}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>
@ -462,7 +495,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
} }
}} }}
placeholder='{"status": "active", "department": "IT"}' placeholder='{"status": "active", "department": "IT"}'
className="font-mono text-xs" style={{ fontSize: "12px" }} className="font-mono text-xs"
rows={3} rows={3}
/> />
<p className="text-muted-foreground text-xs">API JSON .</p> <p className="text-muted-foreground text-xs">API JSON .</p>

View File

@ -113,7 +113,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<Upload className="h-4 w-4" /> <Upload className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -133,7 +133,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.uploadText || ""} value={localConfig.uploadText || ""}
onChange={(e) => updateConfig("uploadText", e.target.value)} onChange={(e) => updateConfig("uploadText", e.target.value)}
placeholder="파일을 선택하거나 여기에 드래그하세요" placeholder="파일을 선택하거나 여기에 드래그하세요"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -146,7 +146,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.browseText || ""} value={localConfig.browseText || ""}
onChange={(e) => updateConfig("browseText", e.target.value)} onChange={(e) => updateConfig("browseText", e.target.value)}
placeholder="파일 선택" placeholder="파일 선택"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -196,7 +196,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={0.1} min={0.1}
max={1024} max={1024}
step={0.1} step={0.1}
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<span className="text-muted-foreground text-xs">MB</span> <span className="text-muted-foreground text-xs">MB</span>
</div> </div>
@ -214,7 +214,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("maxFiles", parseInt(e.target.value))} onChange={(e) => updateConfig("maxFiles", parseInt(e.target.value))}
min={1} min={1}
max={100} max={100}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
)} )}
@ -257,7 +257,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newFileType} value={newFileType}
onChange={(e) => setNewFileType(e.target.value)} onChange={(e) => setNewFileType(e.target.value)}
placeholder=".pdf 또는 pdf" placeholder=".pdf 또는 pdf"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs"> <Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs">

View File

@ -236,11 +236,11 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<h4 className="flex items-center gap-2 text-xs font-medium" style={{ fontSize: "12px" }}> <h4 className="flex items-center gap-2 text-xs font-medium">
<Workflow className="h-4 w-4" /> <Workflow className="h-4 w-4" />
</h4> </h4>
<p className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> <p className="text-muted-foreground text-xs">
</p> </p>
</div> </div>
@ -256,7 +256,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
setTimeout(() => applyConfig(), 0); setTimeout(() => applyConfig(), 0);
}} }}
/> />
<Label htmlFor="flow-control-enabled" className="text-xs font-medium" style={{ fontSize: "12px" }}> <Label htmlFor="flow-control-enabled" className="text-xs font-medium">
</Label> </Label>
</div> </div>
@ -265,7 +265,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
<> <>
{/* 대상 플로우 선택 */} {/* 대상 플로우 선택 */}
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}> <Label className="text-xs font-medium">
</Label> </Label>
<Select <Select
@ -275,7 +275,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
setTimeout(() => applyConfig(), 0); setTimeout(() => applyConfig(), 0);
}} }}
> >
<SelectTrigger className="h-6 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="플로우 위젯 선택" /> <SelectValue placeholder="플로우 위젯 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -283,7 +283,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
const flowConfig = (fw as any).componentConfig || {}; const flowConfig = (fw as any).componentConfig || {};
const flowName = flowConfig.flowName || `플로우 ${fw.id}`; const flowName = flowConfig.flowName || `플로우 ${fw.id}`;
return ( return (
<SelectItem key={fw.id} value={fw.id} style={{ fontSize: "12px" }}> <SelectItem key={fw.id} value={fw.id} className="text-xs">
{flowName} {flowName}
</SelectItem> </SelectItem>
); );
@ -298,7 +298,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
{/* 단계 선택 */} {/* 단계 선택 */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}> <Label className="text-xs font-medium">
</Label> </Label>
<div className="flex gap-1"> <div className="flex gap-1">
@ -307,7 +307,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
size="sm" size="sm"
onClick={selectAll} onClick={selectAll}
className="h-7 px-2 text-xs" className="h-7 px-2 text-xs"
style={{ fontSize: "12px" }}
> >
</Button> </Button>
@ -316,7 +315,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
size="sm" size="sm"
onClick={selectNone} onClick={selectNone}
className="h-7 px-2 text-xs" className="h-7 px-2 text-xs"
style={{ fontSize: "12px" }}
> >
</Button> </Button>
@ -325,7 +323,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
size="sm" size="sm"
onClick={invertSelection} onClick={invertSelection}
className="h-7 px-2 text-xs" className="h-7 px-2 text-xs"
style={{ fontSize: "12px" }}
> >
</Button> </Button>
@ -347,9 +344,8 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
<Label <Label
htmlFor={`step-${step.id}`} htmlFor={`step-${step.id}`}
className="flex flex-1 items-center gap-2 text-xs" className="flex flex-1 items-center gap-2 text-xs"
style={{ fontSize: "12px" }}
> >
<Badge variant="outline" className="text-xs" style={{ fontSize: "12px" }}> <Badge variant="outline" className="text-xs">
Step {step.stepOrder} Step {step.stepOrder}
</Badge> </Badge>
<span>{step.stepName}</span> <span>{step.stepName}</span>
@ -363,7 +359,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
{/* 정렬 방식 */} {/* 정렬 방식 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="group-align" className="text-xs font-medium" style={{ fontSize: "12px" }}> <Label htmlFor="group-align" className="text-xs font-medium">
</Label> </Label>
<Select <Select
@ -373,23 +369,23 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
onUpdateProperty("webTypeConfig.flowVisibilityConfig.groupAlign", value); onUpdateProperty("webTypeConfig.flowVisibilityConfig.groupAlign", value);
}} }}
> >
<SelectTrigger id="group-align" className="h-6 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger id="group-align" className="h-6 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="start" style={{ fontSize: "12px" }}> <SelectItem value="start" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="center" style={{ fontSize: "12px" }}> <SelectItem value="center" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="end" style={{ fontSize: "12px" }}> <SelectItem value="end" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="space-between" style={{ fontSize: "12px" }}> <SelectItem value="space-between" className="text-xs">
</SelectItem> </SelectItem>
<SelectItem value="space-around" style={{ fontSize: "12px" }}> <SelectItem value="space-around" className="text-xs">
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>

View File

@ -55,7 +55,7 @@ export function FlowWidgetConfigPanel({ config = {}, onChange }: FlowWidgetConfi
{loading ? ( {loading ? (
<div className="flex items-center gap-2 rounded-md border px-3 py-2"> <div className="flex items-center gap-2 rounded-md border px-3 py-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> ...</span> <span className="text-muted-foreground text-xs"> ...</span>
</div> </div>
) : ( ) : (
<> <>

View File

@ -56,7 +56,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> </CardTitle> <CardTitle className="flex items-center gap-2 text-xs"> </CardTitle>
<CardDescription className="text-xs"> .</CardDescription> <CardDescription className="text-xs"> .</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -73,7 +73,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""} value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="숫자를 입력하세요" placeholder="숫자를 입력하세요"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -88,7 +88,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.min ?? ""} value={localConfig.min ?? ""}
onChange={(e) => updateConfig("min", e.target.value ? parseFloat(e.target.value) : undefined)} onChange={(e) => updateConfig("min", e.target.value ? parseFloat(e.target.value) : undefined)}
placeholder="0" placeholder="0"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -101,7 +101,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.max ?? ""} value={localConfig.max ?? ""}
onChange={(e) => updateConfig("max", e.target.value ? parseFloat(e.target.value) : undefined)} onChange={(e) => updateConfig("max", e.target.value ? parseFloat(e.target.value) : undefined)}
placeholder="100" placeholder="100"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>
@ -118,7 +118,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
placeholder="1" placeholder="1"
min="0" min="0"
step="0.01" step="0.01"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<p className="text-muted-foreground text-xs">/ </p> <p className="text-muted-foreground text-xs">/ </p>
</div> </div>
@ -158,7 +158,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
placeholder="2" placeholder="2"
min="0" min="0"
max="10" max="10"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
)} )}
@ -223,7 +223,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={localConfig.min} min={localConfig.min}
max={localConfig.max} max={localConfig.max}
step={localConfig.step} step={localConfig.step}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<div className="text-muted-foreground mt-2 text-xs"> <div className="text-muted-foreground mt-2 text-xs">
{localConfig.format === "currency" && "통화 형식으로 표시됩니다."} {localConfig.format === "currency" && "통화 형식으로 표시됩니다."}

View File

@ -168,7 +168,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<Radio className="h-4 w-4" /> <Radio className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -188,7 +188,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupLabel || ""} value={localConfig.groupLabel || ""}
onChange={(e) => updateConfig("groupLabel", e.target.value)} onChange={(e) => updateConfig("groupLabel", e.target.value)}
placeholder="라디오버튼 그룹 제목" placeholder="라디오버튼 그룹 제목"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -201,7 +201,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupName || ""} value={localConfig.groupName || ""}
onChange={(e) => updateConfig("groupName", e.target.value)} onChange={(e) => updateConfig("groupName", e.target.value)}
placeholder="자동 생성 (필드명 기반)" placeholder="자동 생성 (필드명 기반)"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<p className="text-muted-foreground text-xs"> .</p> <p className="text-muted-foreground text-xs"> .</p>
</div> </div>
@ -252,19 +252,19 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel} value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)} onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={newOptionValue} value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)} onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값" placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Button <Button
size="sm" size="sm"
onClick={addOption} onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()} disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
</Button> </Button>
@ -278,7 +278,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={bulkOptions} value={bulkOptions}
onChange={(e) => setBulkOptions(e.target.value)} onChange={(e) => setBulkOptions(e.target.value)}
placeholder="한 줄당 하나씩 입력하세요.&#10;라벨만 입력하면 값과 동일하게 설정됩니다.&#10;라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다.&#10;&#10;예시:&#10;서울&#10;부산&#10;대구시|daegu" placeholder="한 줄당 하나씩 입력하세요.&#10;라벨만 입력하면 값과 동일하게 설정됩니다.&#10;라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다.&#10;&#10;예시:&#10;서울&#10;부산&#10;대구시|daegu"
className="h-20 text-xs" style={{ fontSize: "12px" }} className="h-20 text-xs"
/> />
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs"> <Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
@ -295,13 +295,13 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label} value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)} onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={option.value} value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)} onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값" placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Switch <Switch
checked={!option.disabled} checked={!option.disabled}
@ -328,7 +328,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
id="defaultValue" id="defaultValue"
value={localConfig.defaultValue || ""} value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)} onChange={(e) => updateConfig("defaultValue", e.target.value)}
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }} className="w-full rounded-md border px-3 py-1 text-xs"
> >
<option value=""> </option> <option value=""> </option>
{localConfig.options.map((option, index) => ( {localConfig.options.map((option, index) => (
@ -390,7 +390,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly || option.disabled} disabled={localConfig.readonly || option.disabled}
required={localConfig.required && index === 0} // 첫 번째에만 required 표시 required={localConfig.required && index === 0} // 첫 번째에만 required 표시
defaultChecked={localConfig.defaultValue === option.value} defaultChecked={localConfig.defaultValue === option.value}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<Label htmlFor={`preview-radio-${index}`} className="text-xs"> <Label htmlFor={`preview-radio-${index}`} className="text-xs">
{option.label} {option.label}

View File

@ -153,7 +153,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<List className="h-4 w-4" /> <List className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -173,7 +173,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""} value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="선택하세요" placeholder="선택하세요"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -186,7 +186,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.emptyMessage || ""} value={localConfig.emptyMessage || ""}
onChange={(e) => updateConfig("emptyMessage", e.target.value)} onChange={(e) => updateConfig("emptyMessage", e.target.value)}
placeholder="선택 가능한 옵션이 없습니다" placeholder="선택 가능한 옵션이 없습니다"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -247,19 +247,19 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel} value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)} onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={newOptionValue} value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)} onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값" placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Button <Button
size="sm" size="sm"
onClick={addOption} onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()} disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
</Button> </Button>
@ -273,7 +273,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={bulkOptions} value={bulkOptions}
onChange={(e) => setBulkOptions(e.target.value)} onChange={(e) => setBulkOptions(e.target.value)}
placeholder="한 줄당 하나씩 입력하세요.&#10;라벨만 입력하면 값과 동일하게 설정됩니다.&#10;라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다.&#10;&#10;예시:&#10;서울&#10;부산&#10;대구시|daegu" placeholder="한 줄당 하나씩 입력하세요.&#10;라벨만 입력하면 값과 동일하게 설정됩니다.&#10;라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다.&#10;&#10;예시:&#10;서울&#10;부산&#10;대구시|daegu"
className="h-20 text-xs" style={{ fontSize: "12px" }} className="h-20 text-xs"
/> />
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs"> <Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
@ -290,13 +290,13 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label} value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)} onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Input <Input
value={option.value} value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)} onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값" placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }} className="flex-1 text-xs"
/> />
<Switch <Switch
checked={!option.disabled} checked={!option.disabled}
@ -323,7 +323,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
id="defaultValue" id="defaultValue"
value={localConfig.defaultValue || ""} value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)} onChange={(e) => updateConfig("defaultValue", e.target.value)}
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }} className="w-full rounded-md border px-3 py-1 text-xs"
> >
<option value=""> </option> <option value=""> </option>
{localConfig.options.map((option, index) => ( {localConfig.options.map((option, index) => (
@ -376,7 +376,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly} disabled={localConfig.readonly}
required={localConfig.required} required={localConfig.required}
multiple={localConfig.multiple} multiple={localConfig.multiple}
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }} className="w-full rounded-md border px-3 py-1 text-xs"
defaultValue={localConfig.defaultValue} defaultValue={localConfig.defaultValue}
> >
<option value="" disabled> <option value="" disabled>

View File

@ -0,0 +1,404 @@
"use client";
import React, { useEffect, useState } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Check, ChevronsUpDown, Plus, X, GripVertical, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import type { TabItem, TabsComponent } from "@/types/screen-management";
interface TabsConfigPanelProps {
config: any;
onChange: (config: any) => void;
}
interface ScreenInfo {
screenId: number;
screenName: string;
screenCode: string;
tableName: string;
}
export function TabsConfigPanel({ config, onChange }: TabsConfigPanelProps) {
const [screens, setScreens] = useState<ScreenInfo[]>([]);
const [loading, setLoading] = useState(false);
const [localTabs, setLocalTabs] = useState<TabItem[]>(config.tabs || []);
// 화면 목록 로드
useEffect(() => {
const loadScreens = async () => {
try {
setLoading(true);
// API 클라이언트 동적 import (named export 사용)
const { apiClient } = await import("@/lib/api/client");
// 전체 화면 목록 조회 (페이징 사이즈 크게)
const response = await apiClient.get("/screen-management/screens", {
params: { size: 1000 }
});
console.log("화면 목록 조회 성공:", response.data);
if (response.data.success && response.data.data) {
setScreens(response.data.data);
}
} catch (error: any) {
console.error("Failed to load screens:", error);
console.error("Error response:", error.response?.data);
} finally {
setLoading(false);
}
};
loadScreens();
}, []);
// 컴포넌트 변경 시 로컬 상태 동기화 (초기화만, 입력 중에는 동기화하지 않음)
const [isUserEditing, setIsUserEditing] = useState(false);
useEffect(() => {
// 사용자가 입력 중이 아닐 때만 동기화
if (!isUserEditing) {
setLocalTabs(config.tabs || []);
}
}, [config.tabs, isUserEditing]);
// 탭 추가
const handleAddTab = () => {
const newTab: TabItem = {
id: `tab-${Date.now()}`,
label: `새 탭 ${localTabs.length + 1}`,
order: localTabs.length,
disabled: false,
};
const updatedTabs = [...localTabs, newTab];
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 제거
const handleRemoveTab = (tabId: string) => {
const updatedTabs = localTabs.filter((tab) => tab.id !== tabId);
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 라벨 변경 (입력 중)
const handleLabelChange = (tabId: string, label: string) => {
setIsUserEditing(true);
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, label } : tab));
setLocalTabs(updatedTabs);
// onChange는 onBlur에서 호출
};
// 탭 라벨 변경 완료 (포커스 아웃 시)
const handleLabelBlur = () => {
setIsUserEditing(false);
onChange({ ...config, tabs: localTabs });
};
// 탭 화면 선택
const handleScreenSelect = (tabId: string, screenId: number, screenName: string) => {
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, screenId, screenName } : tab));
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 비활성화 토글
const handleDisabledToggle = (tabId: string, disabled: boolean) => {
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, disabled } : tab));
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 순서 변경
const handleMoveTab = (tabId: string, direction: "up" | "down") => {
const index = localTabs.findIndex((tab) => tab.id === tabId);
if (
(direction === "up" && index === 0) ||
(direction === "down" && index === localTabs.length - 1)
) {
return;
}
const newTabs = [...localTabs];
const targetIndex = direction === "up" ? index - 1 : index + 1;
[newTabs[index], newTabs[targetIndex]] = [newTabs[targetIndex], newTabs[index]];
// order 값 재조정
const updatedTabs = newTabs.map((tab, idx) => ({ ...tab, order: idx }));
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
return (
<div className="space-y-6 p-4">
<div>
<h3 className="mb-4 text-sm font-semibold"> </h3>
<div className="space-y-4">
{/* 탭 방향 */}
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={config.orientation || "horizontal"}
onValueChange={(value: "horizontal" | "vertical") =>
onChange({ ...config, orientation: value })
}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="horizontal"></SelectItem>
<SelectItem value="vertical"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 탭 스타일 */}
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={config.variant || "default"}
onValueChange={(value: "default" | "pills" | "underline") =>
onChange({ ...config, variant: value })
}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"></SelectItem>
<SelectItem value="pills"></SelectItem>
<SelectItem value="underline"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 선택 상태 유지 */}
<div className="flex items-center justify-between">
<div>
<Label className="text-xs sm:text-sm"> </Label>
<p className="text-muted-foreground text-[10px] sm:text-xs">
</p>
</div>
<Switch
checked={config.persistSelection || false}
onCheckedChange={(checked) => onChange({ ...config, persistSelection: checked })}
/>
</div>
{/* 탭 닫기 버튼 */}
<div className="flex items-center justify-between">
<div>
<Label className="text-xs sm:text-sm"> </Label>
<p className="text-muted-foreground text-[10px] sm:text-xs">
</p>
</div>
<Switch
checked={config.allowCloseable || false}
onCheckedChange={(checked) => onChange({ ...config, allowCloseable: checked })}
/>
</div>
</div>
</div>
<div>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold"> </h3>
<Button
onClick={handleAddTab}
size="sm"
variant="outline"
className="h-8 text-xs sm:h-9 sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
{localTabs.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-muted-foreground mt-1 text-xs">
</p>
</div>
) : (
<div className="space-y-3">
{localTabs.map((tab, index) => (
<div
key={tab.id}
className="rounded-lg border bg-card p-3 shadow-sm"
>
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium"> {index + 1}</span>
</div>
<div className="flex items-center gap-1">
<Button
onClick={() => handleMoveTab(tab.id, "up")}
disabled={index === 0}
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
>
</Button>
<Button
onClick={() => handleMoveTab(tab.id, "down")}
disabled={index === localTabs.length - 1}
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
>
</Button>
<Button
onClick={() => handleRemoveTab(tab.id)}
size="sm"
variant="ghost"
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
<div className="space-y-3">
{/* 탭 라벨 */}
<div>
<Label className="text-xs"> </Label>
<Input
value={tab.label}
onChange={(e) => handleLabelChange(tab.id, e.target.value)}
onBlur={handleLabelBlur}
placeholder="탭 이름"
className="h-8 text-xs sm:h-9 sm:text-sm"
/>
</div>
{/* 화면 선택 */}
<div>
<Label className="text-xs"> </Label>
{loading ? (
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
<Loader2 className="h-3 w-3 animate-spin" />
<span className="text-muted-foreground text-xs"> ...</span>
</div>
) : (
<ScreenSelectCombobox
screens={screens}
selectedScreenId={tab.screenId}
onSelect={(screenId, screenName) =>
handleScreenSelect(tab.id, screenId, screenName)
}
/>
)}
{tab.screenName && (
<p className="text-muted-foreground mt-1 text-xs">
: {tab.screenName}
</p>
)}
</div>
{/* 비활성화 */}
<div className="flex items-center justify-between">
<Label className="text-xs"></Label>
<Switch
checked={tab.disabled || false}
onCheckedChange={(checked) => handleDisabledToggle(tab.id, checked)}
/>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
// 화면 선택 Combobox 컴포넌트
function ScreenSelectCombobox({
screens,
selectedScreenId,
onSelect,
}: {
screens: ScreenInfo[];
selectedScreenId?: number;
onSelect: (screenId: number, screenName: string) => void;
}) {
const [open, setOpen] = useState(false);
const selectedScreen = screens.find((s) => s.screenId === selectedScreenId);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="h-8 w-full justify-between text-xs sm:h-9 sm:text-sm"
>
{selectedScreen ? selectedScreen.screenName : "화면 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="화면 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm">
.
</CommandEmpty>
<CommandGroup>
{screens.map((screen) => (
<CommandItem
key={screen.screenId}
value={screen.screenName}
onSelect={() => {
onSelect(screen.screenId, screen.screenName);
setOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
selectedScreenId === screen.screenId ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{screen.screenName}</span>
<span className="text-muted-foreground text-[10px]">
: {screen.screenCode} | : {screen.tableName}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -55,7 +55,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> </CardTitle> <CardTitle className="flex items-center gap-2 text-xs"> </CardTitle>
<CardDescription className="text-xs"> .</CardDescription> <CardDescription className="text-xs"> .</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -72,7 +72,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""} value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="입력 안내 텍스트" placeholder="입력 안내 텍스트"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -88,7 +88,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)} onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="0" placeholder="0"
min="0" min="0"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -102,7 +102,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("maxLength", e.target.value ? parseInt(e.target.value) : undefined)} onChange={(e) => updateConfig("maxLength", e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="100" placeholder="100"
min="1" min="1"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>
@ -141,7 +141,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.pattern || ""} value={localConfig.pattern || ""}
onChange={(e) => updateConfig("pattern", e.target.value)} onChange={(e) => updateConfig("pattern", e.target.value)}
placeholder="예: [A-Za-z0-9]+" placeholder="예: [A-Za-z0-9]+"
className="font-mono text-xs" style={{ fontSize: "12px" }} className="font-mono text-xs"
/> />
<p className="text-muted-foreground text-xs">JavaScript .</p> <p className="text-muted-foreground text-xs">JavaScript .</p>
</div> </div>
@ -219,7 +219,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
minLength={localConfig.minLength} minLength={localConfig.minLength}
pattern={localConfig.pattern} pattern={localConfig.pattern}
autoComplete={localConfig.autoComplete} autoComplete={localConfig.autoComplete}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
</div> </div>

View File

@ -68,7 +68,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center gap-2 text-xs">
<AlignLeft className="h-4 w-4" /> <AlignLeft className="h-4 w-4" />
</CardTitle> </CardTitle>
@ -88,7 +88,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""} value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="내용을 입력하세요" placeholder="내용을 입력하세요"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -101,7 +101,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.defaultValue || ""} value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)} onChange={(e) => updateConfig("defaultValue", e.target.value)}
placeholder="기본 텍스트 내용" placeholder="기본 텍스트 내용"
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
rows={3} rows={3}
/> />
{localConfig.showCharCount && ( {localConfig.showCharCount && (
@ -151,7 +151,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
placeholder="자동 (CSS로 제어)" placeholder="자동 (CSS로 제어)"
min={10} min={10}
max={200} max={200}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
<p className="text-muted-foreground text-xs"> CSS width로 .</p> <p className="text-muted-foreground text-xs"> CSS width로 .</p>
</div> </div>
@ -203,7 +203,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
}} }}
placeholder="제한 없음" placeholder="제한 없음"
min={0} min={0}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -221,7 +221,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
}} }}
placeholder="제한 없음" placeholder="제한 없음"
min={1} min={1}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
@ -333,7 +333,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
resize: localConfig.resizable ? "both" : "none", resize: localConfig.resizable ? "both" : "none",
minHeight: localConfig.autoHeight ? "auto" : undefined, minHeight: localConfig.autoHeight ? "auto" : undefined,
}} }}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
wrap={localConfig.wrap} wrap={localConfig.wrap}
/> />
{localConfig.showCharCount && ( {localConfig.showCharCount && (

View File

@ -94,7 +94,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
max={100} max={100}
value={gap} value={gap}
onChange={(e) => setGap(Number(e.target.value))} onChange={(e) => setGap(Number(e.target.value))}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{gap}px {gap}px
@ -109,7 +109,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
</Label> </Label>
<Select value={align} onValueChange={(value: any) => setAlign(value)}> <Select value={align} onValueChange={(value: any) => setAlign(value)}>
<SelectTrigger id="align" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger id="align" className="h-6 w-full px-2 py-0 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -181,7 +181,7 @@ export function ComponentsPanel({
onSearchChange(value); onSearchChange(value);
} }
}} }}
className="h-8 pl-8 text-xs" style={{ fontSize: "12px" }} className="h-8 pl-8 text-xs"
/> />
</div> </div>
</div> </div>

View File

@ -458,7 +458,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
updateSettings({ options: newOptions }); updateSettings({ options: newOptions });
}} }}
placeholder="옵션명" placeholder="옵션명"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
<Button <Button
type="button" type="button"
@ -483,7 +483,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
const newOption = { label: "", value: "" }; const newOption = { label: "", value: "" };
updateSettings({ options: [...(localSettings.options || []), newOption] }); updateSettings({ options: [...(localSettings.options || []), newOption] });
}} }}
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
> >
<Plus className="mr-1 h-3 w-3" /> <Plus className="mr-1 h-3 w-3" />
@ -548,7 +548,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.min || ""} value={localSettings.min || ""}
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })} onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최소값" placeholder="최소값"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -558,7 +558,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.max || ""} value={localSettings.max || ""}
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })} onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대값" placeholder="최대값"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
</div> </div>
@ -571,7 +571,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.step || "0.01"} value={localSettings.step || "0.01"}
onChange={(e) => updateSettings({ step: e.target.value })} onChange={(e) => updateSettings({ step: e.target.value })}
placeholder="0.01" placeholder="0.01"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
)} )}
@ -589,7 +589,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
type="date" type="date"
value={localSettings.minDate || ""} value={localSettings.minDate || ""}
onChange={(e) => updateSettings({ minDate: e.target.value })} onChange={(e) => updateSettings({ minDate: e.target.value })}
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -598,7 +598,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
type="date" type="date"
value={localSettings.maxDate || ""} value={localSettings.maxDate || ""}
onChange={(e) => updateSettings({ maxDate: e.target.value })} onChange={(e) => updateSettings({ maxDate: e.target.value })}
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
</div> </div>
@ -626,7 +626,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxLength || ""} value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })} onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수" placeholder="최대 문자 수"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -635,7 +635,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.placeholder || ""} value={localSettings.placeholder || ""}
onChange={(e) => updateSettings({ placeholder: e.target.value })} onChange={(e) => updateSettings({ placeholder: e.target.value })}
placeholder="입력 안내 텍스트" placeholder="입력 안내 텍스트"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
</div> </div>
@ -652,7 +652,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.rows || "3"} value={localSettings.rows || "3"}
onChange={(e) => updateSettings({ rows: Number(e.target.value) })} onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
placeholder="3" placeholder="3"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -662,7 +662,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxLength || ""} value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })} onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수" placeholder="최대 문자 수"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
</div> </div>
@ -678,7 +678,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.accept || ""} value={localSettings.accept || ""}
onChange={(e) => updateSettings({ accept: e.target.value })} onChange={(e) => updateSettings({ accept: e.target.value })}
placeholder=".jpg,.png,.pdf" placeholder=".jpg,.png,.pdf"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -688,7 +688,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"} value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })} onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
placeholder="10" placeholder="10"
className="h-7 text-xs" style={{ fontSize: "12px" }} className="h-7 text-xs"
/> />
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -1132,7 +1132,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{/* 기본 설정 */} {/* 기본 설정 */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center space-x-2 text-xs">
<Settings className="h-4 w-4" /> <Settings className="h-4 w-4" />
<span> </span> <span> </span>
</CardTitle> </CardTitle>
@ -1184,7 +1184,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableAdd: checked as boolean }); onUpdateComponent({ enableAdd: checked as boolean });
}} }}
/> />
<Label htmlFor="enable-add" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="enable-add" className="text-xs">
</Label> </Label>
</div> </div>
@ -1198,7 +1198,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableEdit: checked as boolean }); onUpdateComponent({ enableEdit: checked as boolean });
}} }}
/> />
<Label htmlFor="enable-edit" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="enable-edit" className="text-xs">
</Label> </Label>
</div> </div>
@ -1212,7 +1212,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableDelete: checked as boolean }); onUpdateComponent({ enableDelete: checked as boolean });
}} }}
/> />
<Label htmlFor="enable-delete" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="enable-delete" className="text-xs">
</Label> </Label>
</div> </div>
@ -1220,7 +1220,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="add-button-text" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="add-button-text" className="text-xs">
</Label> </Label>
<Input <Input
@ -1233,12 +1233,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}} }}
placeholder="추가" placeholder="추가"
disabled={!localValues.enableAdd} disabled={!localValues.enableAdd}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-button-text" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="edit-button-text" className="text-xs">
</Label> </Label>
<Input <Input
@ -1251,12 +1251,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}} }}
placeholder="수정" placeholder="수정"
disabled={!localValues.enableEdit} disabled={!localValues.enableEdit}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="delete-button-text" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="delete-button-text" className="text-xs">
</Label> </Label>
<Input <Input
@ -1269,7 +1269,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}} }}
placeholder="삭제" placeholder="삭제"
disabled={!localValues.enableDelete} disabled={!localValues.enableDelete}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
</div> </div>
@ -1284,7 +1284,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="modal-title" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="modal-title" className="text-xs">
</Label> </Label>
<Input <Input
@ -1298,12 +1298,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
placeholder="새 데이터 추가" placeholder="새 데이터 추가"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="modal-width" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="modal-width" className="text-xs">
</Label> </Label>
<select <select
@ -1328,7 +1328,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="modal-description" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="modal-description" className="text-xs">
</Label> </Label>
<Input <Input
@ -1342,13 +1342,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
placeholder="모달에 표시될 설명을 입력하세요" placeholder="모달에 표시될 설명을 입력하세요"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="modal-layout" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="modal-layout" className="text-xs">
</Label> </Label>
<select <select
@ -1370,7 +1370,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{localValues.modalLayout === "grid" && ( {localValues.modalLayout === "grid" && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="modal-grid-columns" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="modal-grid-columns" className="text-xs">
</Label> </Label>
<select <select
@ -1394,7 +1394,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="modal-submit-text" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="modal-submit-text" className="text-xs">
</Label> </Label>
<Input <Input
@ -1408,12 +1408,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
placeholder="추가" placeholder="추가"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="modal-cancel-text" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="modal-cancel-text" className="text-xs">
</Label> </Label>
<Input <Input
@ -1427,7 +1427,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
placeholder="취소" placeholder="취소"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
</div> </div>
@ -1441,7 +1441,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-modal-title" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="edit-modal-title" className="text-xs">
</Label> </Label>
<Input <Input
@ -1455,13 +1455,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
placeholder="데이터 수정" placeholder="데이터 수정"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
<p className="text-xs text-gray-500"> </p> <p className="text-xs text-gray-500"> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-modal-description" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="edit-modal-description" className="text-xs">
</Label> </Label>
<Input <Input
@ -1475,7 +1475,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
placeholder="선택한 데이터를 수정합니다" placeholder="선택한 데이터를 수정합니다"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
<p className="text-xs text-gray-500"> </p> <p className="text-xs text-gray-500"> </p>
</div> </div>
@ -1494,7 +1494,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ showSearchButton: checked as boolean }); onUpdateComponent({ showSearchButton: checked as boolean });
}} }}
/> />
<Label htmlFor="show-search-button" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="show-search-button" className="text-xs">
</Label> </Label>
</div> </div>
@ -1509,7 +1509,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableExport: checked as boolean }); onUpdateComponent({ enableExport: checked as boolean });
}} }}
/> />
<Label htmlFor="enable-export" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="enable-export" className="text-xs">
</Label> </Label>
</div> </div>
@ -1521,7 +1521,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto"> <TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center space-x-2 text-xs">
<Columns className="h-4 w-4" /> <Columns className="h-4 w-4" />
<span> </span> <span> </span>
</CardTitle> </CardTitle>
@ -1654,7 +1654,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
} }
}} }}
placeholder="표시명을 입력하세요" placeholder="표시명을 입력하세요"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
@ -1947,7 +1947,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
placeholder="고정값 입력..." placeholder="고정값 입력..."
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
)} )}
@ -1967,7 +1967,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto"> <TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center space-x-2 text-xs">
<Filter className="h-4 w-4" /> <Filter className="h-4 w-4" />
<span> </span> <span> </span>
</CardTitle> </CardTitle>
@ -1995,7 +1995,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{component.filters.length === 0 ? ( {component.filters.length === 0 ? (
<div className="text-muted-foreground py-8 text-center"> <div className="text-muted-foreground py-8 text-center">
<Filter className="mx-auto mb-2 h-8 w-8 opacity-50" /> <Filter className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p className="text-xs" style={{ fontSize: "12px" }}> </p> <p className="text-xs"> </p>
<p className="text-xs"> </p> <p className="text-xs"> </p>
</div> </div>
) : ( ) : (
@ -2073,7 +2073,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
updateFilter(index, { label: newValue }); updateFilter(index, { label: newValue });
}} }}
placeholder="필터 이름 입력..." placeholder="필터 이름 입력..."
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-xs">
@ -2192,7 +2192,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto"> <TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}> <CardTitle className="flex items-center space-x-2 text-xs">
<Settings className="h-4 w-4" /> <Settings className="h-4 w-4" />
<span> </span> <span> </span>
</CardTitle> </CardTitle>
@ -2342,7 +2342,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
/> />
<Label htmlFor="show-page-size-selector" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="show-page-size-selector" className="text-xs">
</Label> </Label>
</div> </div>
@ -2362,7 +2362,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
/> />
<Label htmlFor="show-page-info" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="show-page-info" className="text-xs">
</Label> </Label>
</div> </div>
@ -2382,7 +2382,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}); });
}} }}
/> />
<Label htmlFor="show-first-last" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="show-first-last" className="text-xs">
/ /
</Label> </Label>
</div> </div>

View File

@ -158,7 +158,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
} }
}} }}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
<div> <div>
@ -196,7 +195,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
} }
}} }}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -211,7 +209,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value)) onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value))
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -256,7 +253,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
} }
}} }}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
<option value="row"> (row)</option> <option value="row"> (row)</option>
<option value="column"> (column)</option> <option value="column"> (column)</option>
@ -316,7 +312,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
} }
}} }}
className="w-20 rounded border border-gray-300 px-2 py-1 text-xs" className="w-20 rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
<span className="text-xs text-gray-500"></span> <span className="text-xs text-gray-500"></span>
</div> </div>
@ -332,7 +327,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value)) onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value))
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -348,7 +342,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
value={layoutComponent.layoutConfig?.split?.direction || "horizontal"} value={layoutComponent.layoutConfig?.split?.direction || "horizontal"}
onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.split.direction", e.target.value)} onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.split.direction", e.target.value)}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
<option value="horizontal"> </option> <option value="horizontal"> </option>
<option value="vertical"> </option> <option value="vertical"> </option>
@ -398,7 +391,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
) )
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
<option value=""> </option> <option value=""> </option>
{currentTable.columns?.map((column) => ( {currentTable.columns?.map((column) => (
@ -421,7 +413,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
) )
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
<option value=""> </option> <option value=""> </option>
{currentTable.columns?.map((column) => ( {currentTable.columns?.map((column) => (
@ -444,7 +435,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
) )
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
<option value=""> </option> <option value=""> </option>
{currentTable.columns?.map((column) => ( {currentTable.columns?.map((column) => (
@ -467,7 +457,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
) )
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
<option value=""> </option> <option value=""> </option>
{currentTable.columns?.map((column) => ( {currentTable.columns?.map((column) => (
@ -495,7 +484,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
); );
}} }}
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs" className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
+ +
</button> </button>
@ -519,7 +507,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
); );
}} }}
className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs" className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
<option value=""> </option> <option value=""> </option>
{currentTable.columns?.map((col) => ( {currentTable.columns?.map((col) => (
@ -542,7 +529,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
); );
}} }}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs" className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
> >
</button> </button>
@ -578,7 +564,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value)) onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value))
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
@ -593,7 +578,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value)) onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value))
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -683,7 +667,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
) )
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -711,7 +694,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value) onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value)
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
placeholder="100%" placeholder="100%"
/> />
</div> </div>
@ -724,7 +706,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value) onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value)
} }
className="w-full rounded border border-gray-300 px-2 py-1 text-xs" className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
placeholder="auto" placeholder="auto"
/> />
</div> </div>
@ -1007,7 +988,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3> <h3 className="font-medium text-gray-900"> </h3>
</div> </div>
<div className="mt-2 flex items-center space-x-2"> <div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> <span className="text-muted-foreground text-xs">
: :
</span> </span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentType}</span> <span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentType}</span>
@ -1057,7 +1038,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3> <h3 className="font-medium text-gray-900"> </h3>
</div> </div>
<div className="mt-2 flex items-center space-x-2"> <div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> <span className="text-muted-foreground text-xs">
: :
</span> </span>
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800"> </span> <span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800"> </span>
@ -1146,14 +1127,14 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
{/* 컴포넌트 정보 */} {/* 컴포넌트 정보 */}
<div className="mb-4 space-y-2"> <div className="mb-4 space-y-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> <span className="text-muted-foreground text-xs">
: :
</span> </span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span> <span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
</div> </div>
{webType && currentBaseInputType && ( {webType && currentBaseInputType && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> <span className="text-muted-foreground text-xs">
: :
</span> </span>
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800"> <span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
@ -1163,7 +1144,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
)} )}
{selectedComponent.columnName && ( {selectedComponent.columnName && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> <span className="text-muted-foreground text-xs">
: :
</span> </span>
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span> <span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
@ -1375,7 +1356,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3> <h3 className="font-medium text-gray-900"> </h3>
</div> </div>
<div className="mt-2 flex items-center space-x-2"> <div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> <span className="text-muted-foreground text-xs">
: :
</span> </span>
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800"> <span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
@ -1390,7 +1371,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> </label> <label className="block text-sm font-medium text-gray-700"> </label>
<Select value={localDetailType} onValueChange={handleDetailTypeChange}> <Select value={localDetailType} onValueChange={handleDetailTypeChange}>
<SelectTrigger className="h-6 w-full px-2 py-0 bg-white text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="h-6 w-full px-2 py-0 bg-white text-xs">
<SelectValue placeholder="세부 타입을 선택하세요" /> <SelectValue placeholder="세부 타입을 선택하세요" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -98,7 +98,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => onSelectGroup(groupInfo.buttons.map((b) => b.id))} onClick={() => onSelectGroup(groupInfo.buttons.map((b) => b.id))}
className="h-7 px-2 text-xs" style={{ fontSize: "12px" }} className="h-7 px-2 text-xs"
> >
</Button> </Button>
@ -152,7 +152,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
{groupInfo.buttons.map((button) => ( {groupInfo.buttons.map((button) => (
<div <div
key={button.id} key={button.id}
className="flex items-center gap-2 rounded bg-white px-2 py-1.5 text-xs" style={{ fontSize: "12px" }} className="flex items-center gap-2 rounded bg-white px-2 py-1.5 text-xs"
> >
<div className="h-2 w-2 rounded-full bg-blue-500" /> <div className="h-2 w-2 rounded-full bg-blue-500" />
<span className="flex-1 truncate font-medium"> <span className="flex-1 truncate font-medium">

View File

@ -214,7 +214,7 @@ export default function LayoutsPanel({
</Badge> </Badge>
</div> </div>
</div> </div>
<CardTitle className="text-xs" style={{ fontSize: "12px" }}>{layout.name}</CardTitle> <CardTitle className="text-xs">{layout.name}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
{layout.description && ( {layout.description && (

View File

@ -652,7 +652,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
}} }}
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
/> />
<Label htmlFor="required" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="required" className="text-xs">
</Label> </Label>
</div> </div>
@ -668,7 +668,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
}} }}
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
/> />
<Label htmlFor="readonly" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="readonly" className="text-xs">
</Label> </Label>
</div> </div>
@ -990,7 +990,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
</> </>
) : ( ) : (
<div className="bg-accent col-span-2 rounded-lg p-3 text-center"> <div className="bg-accent col-span-2 rounded-lg p-3 text-center">
<p className="text-primary text-xs" style={{ fontSize: "12px" }}> </p> <p className="text-primary text-xs"> </p>
<p className="mt-1 text-xs text-blue-500"> </p> <p className="mt-1 text-xs text-blue-500"> </p>
</div> </div>
)} )}

View File

@ -84,7 +84,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium"> </Label> <Label className="text-xs font-medium"> </Label>
<Select value={selectedPreset} onValueChange={handlePresetChange}> <Select value={selectedPreset} onValueChange={handlePresetChange}>
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}> <SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue placeholder="해상도를 선택하세요" /> <SelectValue placeholder="해상도를 선택하세요" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -147,8 +147,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
placeholder="1920" placeholder="1920"
min="1" min="1"
step="1" step="1"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -160,16 +159,14 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
placeholder="1080" placeholder="1080"
min="1" min="1"
step="1" step="1"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
<Button <Button
onClick={handleCustomResolution} onClick={handleCustomResolution}
size="sm" size="sm"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
> >
</Button> </Button>

View File

@ -109,7 +109,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
variant={row.gap === preset ? "default" : "outline"} variant={row.gap === preset ? "default" : "outline"}
size="sm" size="sm"
onClick={() => onUpdateRow({ gap: preset })} onClick={() => onUpdateRow({ gap: preset })}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
> >
{GAP_PRESETS[preset].label} {GAP_PRESETS[preset].label}
</Button> </Button>
@ -130,7 +130,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
variant={row.padding === preset ? "default" : "outline"} variant={row.padding === preset ? "default" : "outline"}
size="sm" size="sm"
onClick={() => onUpdateRow({ padding: preset })} onClick={() => onUpdateRow({ padding: preset })}
className="text-xs" style={{ fontSize: "12px" }} className="text-xs"
> >
{GAP_PRESETS[preset].label} {GAP_PRESETS[preset].label}
</Button> </Button>

View File

@ -528,7 +528,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
<div className="flex items-center justify-between rounded-xl bg-amber-50/80 border border-amber-200/60 p-3 text-amber-800 mb-4"> <div className="flex items-center justify-between rounded-xl bg-amber-50/80 border border-amber-200/60 p-3 text-amber-800 mb-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
<span className="text-xs" style={{ fontSize: "12px" }}>릿 , 릿 </span> <span className="text-xs">릿 , 릿 </span>
</div> </div>
<Button size="sm" variant="outline" onClick={() => refetch()} className="border-amber-300 text-amber-700 hover:bg-amber-100"> <Button size="sm" variant="outline" onClick={() => refetch()} className="border-amber-300 text-amber-700 hover:bg-amber-100">
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, Suspense } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -341,6 +341,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<Settings className="h-4 w-4 text-primary" /> <Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3> <h3 className="text-sm font-semibold">{definition.name} </h3>
</div> </div>
<Suspense fallback={
<div className="flex items-center justify-center py-8">
<div className="text-sm text-muted-foreground"> ...</div>
</div>
}>
<ConfigPanelComponent <ConfigPanelComponent
config={config} config={config}
onChange={handleConfigChange} onChange={handleConfigChange}
@ -349,6 +354,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달 screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달 tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
/> />
</Suspense>
</div> </div>
); );
}; };
@ -712,8 +718,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("label", e.target.value)} onChange={(e) => handleUpdate("label", e.target.value)}
placeholder="라벨" placeholder="라벨"
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -749,7 +753,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
step={1} step={1}
placeholder="10" placeholder="10"
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -763,8 +766,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("placeholder", e.target.value)} onChange={(e) => handleUpdate("placeholder", e.target.value)}
placeholder="입력 안내 텍스트" placeholder="입력 안내 텍스트"
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/> />
</div> </div>
)} )}
@ -778,8 +779,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("title", e.target.value)} onChange={(e) => handleUpdate("title", e.target.value)}
placeholder="제목" placeholder="제목"
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/> />
</div> </div>
)} )}
@ -793,8 +792,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("description", e.target.value)} onChange={(e) => handleUpdate("description", e.target.value)}
placeholder="설명" placeholder="설명"
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/> />
</div> </div>
)} )}
@ -836,7 +833,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
} }
}} }}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -848,8 +844,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={currentPosition.z || 1} value={currentPosition.z || 1}
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)} onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }} className="text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -867,8 +862,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelText || selectedComponent.label || ""} value={selectedComponent.style?.labelText || selectedComponent.label || ""}
onChange={(e) => handleUpdate("style.labelText", e.target.value)} onChange={(e) => handleUpdate("style.labelText", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }} className="text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@ -878,8 +872,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelFontSize || "12px"} value={selectedComponent.style?.labelFontSize || "12px"}
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)} onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }} className="text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -889,8 +882,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelColor || "#212121"} value={selectedComponent.style?.labelColor || "#212121"}
onChange={(e) => handleUpdate("style.labelColor", e.target.value)} onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }} className="text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
</div> </div>
@ -901,8 +893,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelMarginBottom || "4px"} value={selectedComponent.style?.labelMarginBottom || "4px"}
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)} onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs" className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }} className="text-xs"
style={{ fontSize: "12px" }}
/> />
</div> </div>
<div className="flex items-center space-x-2 pt-5"> <div className="flex items-center space-x-2 pt-5">
@ -1053,7 +1044,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<div> <div>
<Label> </Label> <Label> </Label>
<Select value={localComponentDetailType || webType} onValueChange={handleDetailTypeChange}> <Select value={localComponentDetailType || webType} onValueChange={handleDetailTypeChange}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="세부 타입 선택" /> <SelectValue placeholder="세부 타입 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -1260,7 +1251,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<div> <div>
<Label> </Label> <Label> </Label>
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}> <Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -109,7 +109,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
<> <>
<Separator /> <Separator />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="multiple" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="multiple" className="text-xs">
</Label> </Label>
<Checkbox <Checkbox
@ -121,7 +121,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
/> />
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="searchable" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="searchable" className="text-xs">
</Label> </Label>
<Checkbox <Checkbox
@ -259,7 +259,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
{baseType === "date" && ( {baseType === "date" && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="showTime" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="showTime" className="text-xs">
</Label> </Label>
<Checkbox <Checkbox
@ -395,7 +395,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="fileMultiple" className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor="fileMultiple" className="text-xs">
</Label> </Label>
<Checkbox <Checkbox

View File

@ -122,7 +122,7 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
</Label> </Label>
<Select value={localValues.labelPosition} onValueChange={(value) => updateConfig("labelPosition", value)}> <Select value={localValues.labelPosition} onValueChange={(value) => updateConfig("labelPosition", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="라벨 위치 선택" /> <SelectValue placeholder="라벨 위치 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -194,18 +194,18 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
<Label className="text-sm font-medium text-gray-700"></Label> <Label className="text-sm font-medium text-gray-700"></Label>
<div className="mt-2 flex items-center space-x-2"> <div className="mt-2 flex items-center space-x-2">
{localValues.labelPosition === "left" && localValues.checkboxText && ( {localValues.labelPosition === "left" && localValues.checkboxText && (
<span className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span> <span className="text-xs">{localValues.checkboxText}</span>
)} )}
{localValues.labelPosition === "top" && localValues.checkboxText && ( {localValues.labelPosition === "top" && localValues.checkboxText && (
<div className="w-full"> <div className="w-full">
<div className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</div> <div className="text-xs">{localValues.checkboxText}</div>
<Checkbox checked={localValues.defaultChecked} className="mt-1" /> <Checkbox checked={localValues.defaultChecked} className="mt-1" />
</div> </div>
)} )}
{(localValues.labelPosition === "right" || localValues.labelPosition === "bottom") && ( {(localValues.labelPosition === "right" || localValues.labelPosition === "bottom") && (
<> <>
<Checkbox checked={localValues.defaultChecked} /> <Checkbox checked={localValues.defaultChecked} />
{localValues.checkboxText && <span className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span>} {localValues.checkboxText && <span className="text-xs">{localValues.checkboxText}</span>}
</> </>
)} )}
{localValues.labelPosition === "left" && <Checkbox checked={localValues.defaultChecked} />} {localValues.labelPosition === "left" && <Checkbox checked={localValues.defaultChecked} />}

View File

@ -121,7 +121,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
</Label> </Label>
<Select value={localValues.language} onValueChange={(value) => updateConfig("language", value)}> <Select value={localValues.language} onValueChange={(value) => updateConfig("language", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="언어 선택" /> <SelectValue placeholder="언어 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-60"> <SelectContent className="max-h-60">
@ -140,7 +140,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
</Label> </Label>
<Select value={localValues.theme} onValueChange={(value) => updateConfig("theme", value)}> <Select value={localValues.theme} onValueChange={(value) => updateConfig("theme", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="테마 선택" /> <SelectValue placeholder="테마 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -193,7 +193,7 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
}, 0); }, 0);
}} }}
> >
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="날짜 형식 선택" /> <SelectValue placeholder="날짜 형식 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -233,7 +233,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
</Label> </Label>
<Select value={localValues.displayFormat} onValueChange={(value) => updateConfig("displayFormat", value)}> <Select value={localValues.displayFormat} onValueChange={(value) => updateConfig("displayFormat", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="형식 선택" /> <SelectValue placeholder="형식 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -267,7 +267,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
{/* 기존 필터 목록 */} {/* 기존 필터 목록 */}
<div className="max-h-40 space-y-2 overflow-y-auto"> <div className="max-h-40 space-y-2 overflow-y-auto">
{Object.entries(safeConfig.filters || {}).map(([field, value]) => ( {Object.entries(safeConfig.filters || {}).map(([field, value]) => (
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-xs" style={{ fontSize: "12px" }}> <div key={field} className="flex items-center space-x-2 rounded border p-2 text-xs">
<Input <Input
value={field} value={field}
onChange={(e) => updateFilter(field, e.target.value, value as string)} onChange={(e) => updateFilter(field, e.target.value, value as string)}
@ -317,7 +317,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
<div className="mt-2"> <div className="mt-2">
<div className="flex items-center space-x-2 rounded border bg-white p-2"> <div className="flex items-center space-x-2 rounded border bg-white p-2">
<Search className="h-4 w-4 text-gray-400" /> <Search className="h-4 w-4 text-gray-400" />
<div className="text-muted-foreground flex-1 text-xs" style={{ fontSize: "12px" }}> <div className="text-muted-foreground flex-1 text-xs">
{localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`} {localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
</div> </div>
<Database className="h-4 w-4 text-gray-400" /> <Database className="h-4 w-4 text-gray-400" />

View File

@ -111,7 +111,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
</Label> </Label>
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}> <Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="숫자 형식 선택" /> <SelectValue placeholder="숫자 형식 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -259,7 +259,7 @@ export const RadioTypeConfigPanel: React.FC<RadioTypeConfigPanelProps> = ({ conf
{(safeConfig.options || []).map((option) => ( {(safeConfig.options || []).map((option) => (
<div key={option.value} className="flex items-center space-x-2"> <div key={option.value} className="flex items-center space-x-2">
<RadioGroupItem value={option.value} id={`preview-${option.value}`} /> <RadioGroupItem value={option.value} id={`preview-${option.value}`} />
<Label htmlFor={`preview-${option.value}`} className="text-xs" style={{ fontSize: "12px" }}> <Label htmlFor={`preview-${option.value}`} className="text-xs">
{option.label} {option.label}
</Label> </Label>
</div> </div>

View File

@ -170,7 +170,7 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
value={localValues.placeholder} value={localValues.placeholder}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="옵션을 선택하세요" placeholder="옵션을 선택하세요"
className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }} className="mt-1 h-6 w-full px-2 py-0 text-xs"
/> />
</div> </div>

View File

@ -218,7 +218,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
</Label> </Label>
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}> <Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="입력 형식 선택" /> <SelectValue placeholder="입력 형식 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -332,7 +332,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
</Label> </Label>
<Select value={localValues.autoValueType} onValueChange={(value) => updateConfig("autoValueType", value)}> <Select value={localValues.autoValueType} onValueChange={(value) => updateConfig("autoValueType", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="자동값 타입 선택" /> <SelectValue placeholder="자동값 타입 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -202,7 +202,7 @@ export const TextareaTypeConfigPanel: React.FC<TextareaTypeConfigPanelProps> = (
<Label className="text-sm font-medium text-gray-700"></Label> <Label className="text-sm font-medium text-gray-700"></Label>
<div className="mt-2"> <div className="mt-2">
<textarea <textarea
className="w-full rounded border border-gray-300 p-2 text-xs" style={{ fontSize: "12px" }} className="w-full rounded border border-gray-300 p-2 text-xs"
rows={localValues.rows} rows={localValues.rows}
placeholder={localValues.placeholder || "텍스트를 입력하세요..."} placeholder={localValues.placeholder || "텍스트를 입력하세요..."}
style={{ style={{

View File

@ -1194,7 +1194,7 @@ export function FlowWidget({
setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로 setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로
}} }}
> >
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -34,7 +34,7 @@ export default function InputWidget({ widget, value, onChange, className }: Inpu
required={widget.required} required={widget.required}
readOnly={widget.readonly} readOnly={widget.readonly}
className={cn("h-6 w-full text-xs", widget.readonly && "bg-muted/50 cursor-not-allowed")} className={cn("h-6 w-full text-xs", widget.readonly && "bg-muted/50 cursor-not-allowed")}
style={{ fontSize: "12px" }} className="text-xs"
/> />
</div> </div>
); );

View File

@ -53,7 +53,7 @@ export default function SelectWidget({ widget, value, onChange, options = [], cl
</Label> </Label>
)} )}
<Select value={value} onValueChange={handleChange} disabled={widget.readonly}> <Select value={value} onValueChange={handleChange} disabled={widget.readonly}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}> <SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder={widget.placeholder || "선택해주세요"} /> <SelectValue placeholder={widget.placeholder || "선택해주세요"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -1,210 +1,258 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { TabsComponent, TabItem, ScreenDefinition } from "@/types";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { X, Loader2 } from "lucide-react";
import { Loader2, FileQuestion } from "lucide-react"; import type { TabsComponent, TabItem } from "@/types/screen-management";
import { screenApi } from "@/lib/api/screen";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
interface TabsWidgetProps { interface TabsWidgetProps {
component: TabsComponent; component: TabsComponent;
isPreview?: boolean; className?: string;
style?: React.CSSProperties;
} }
/** export function TabsWidget({ component, className, style }: TabsWidgetProps) {
* const {
* tabs = [],
*/ defaultTab,
export const TabsWidget: React.FC<TabsWidgetProps> = ({ component, isPreview = false }) => { orientation = "horizontal",
// componentConfig에서 설정 읽기 (새 컴포넌트 시스템) variant = "default",
const config = (component as any).componentConfig || component; allowCloseable = false,
const { tabs = [], defaultTab, orientation = "horizontal", variant = "default" } = config; persistSelection = false,
} = component;
// console.log("🔍 TabsWidget 렌더링:", { console.log("🎨 TabsWidget 렌더링:", {
// component, componentId: component.id,
// componentConfig: (component as any).componentConfig, tabs,
// tabs, tabsLength: tabs.length,
// tabsLength: tabs.length component,
// }); });
const [activeTab, setActiveTab] = useState<string>(defaultTab || tabs[0]?.id || ""); const storageKey = `tabs-${component.id}-selected`;
const [loadedScreens, setLoadedScreens] = useState<Record<string, any>>({});
// 초기 선택 탭 결정
const getInitialTab = () => {
if (persistSelection && typeof window !== "undefined") {
const saved = localStorage.getItem(storageKey);
if (saved && tabs.some((t) => t.id === saved)) {
return saved;
}
}
return defaultTab || tabs[0]?.id || "";
};
const [selectedTab, setSelectedTab] = useState<string>(getInitialTab());
const [visibleTabs, setVisibleTabs] = useState<TabItem[]>(tabs);
const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({}); const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({});
const [screenErrors, setScreenErrors] = useState<Record<string, string>>({}); const [screenLayouts, setScreenLayouts] = useState<Record<number, any>>({});
// 탭 변경 시 화면 로드 // 컴포넌트 목록 변경 시 동기
useEffect(() => { useEffect(() => {
if (!activeTab) return; setVisibleTabs(tabs.filter((tab) => !tab.disabled));
}, [tabs]);
const currentTab = tabs.find((tab) => tab.id === activeTab); // 선택된 탭 변경 시 localStorage에 저장
if (!currentTab || !currentTab.screenId) return; useEffect(() => {
if (persistSelection && typeof window !== "undefined") {
localStorage.setItem(storageKey, selectedTab);
}
}, [selectedTab, persistSelection, storageKey]);
// 이미 로드된 화면이면 스킵 // 초기 로드 시 선택된 탭의 화면 불러오기
if (loadedScreens[activeTab]) return; useEffect(() => {
const currentTab = visibleTabs.find((t) => t.id === selectedTab);
console.log("🔄 초기 탭 로드:", {
selectedTab,
currentTab,
hasScreenId: !!currentTab?.screenId,
screenId: currentTab?.screenId,
});
// 이미 로딩 중이면 스킵 if (currentTab && currentTab.screenId && !screenLayouts[currentTab.screenId]) {
if (loadingScreens[activeTab]) return; console.log("📥 초기 화면 로딩 시작:", currentTab.screenId);
loadScreenLayout(currentTab.screenId);
}
}, [selectedTab, visibleTabs]);
// 화면 로드 시작 // 화면 레이아웃 로드
loadScreen(activeTab, currentTab.screenId); const loadScreenLayout = async (screenId: number) => {
}, [activeTab, tabs]); if (screenLayouts[screenId]) {
console.log("✅ 이미 로드된 화면:", screenId);
return; // 이미 로드됨
}
const loadScreen = async (tabId: string, screenId: number) => { console.log("📥 화면 레이아웃 로딩 시작:", screenId);
setLoadingScreens((prev) => ({ ...prev, [tabId]: true })); setLoadingScreens((prev) => ({ ...prev, [screenId]: true }));
setScreenErrors((prev) => ({ ...prev, [tabId]: "" }));
try { try {
const layoutData = await screenApi.getLayout(screenId); const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`);
console.log("📦 API 응답:", { screenId, success: response.data.success, hasData: !!response.data.data });
if (layoutData) { if (response.data.success && response.data.data) {
setLoadedScreens((prev) => ({ console.log("✅ 화면 레이아웃 로드 완료:", screenId);
...prev, setScreenLayouts((prev) => ({ ...prev, [screenId]: response.data.data }));
[tabId]: {
screenId,
layout: layoutData,
},
}));
} else { } else {
setScreenErrors((prev) => ({ console.error("❌ 화면 레이아웃 로드 실패 - success false");
...prev,
[tabId]: "화면을 불러올 수 없습니다",
}));
} }
} catch (error: any) { } catch (error) {
setScreenErrors((prev) => ({ console.error(`❌ 화면 레이아웃 로드 실패 ${screenId}:`, error);
...prev,
[tabId]: error.message || "화면 로드 중 오류가 발생했습니다",
}));
} finally { } finally {
setLoadingScreens((prev) => ({ ...prev, [tabId]: false })); setLoadingScreens((prev) => ({ ...prev, [screenId]: false }));
} }
}; };
// 탭 콘텐츠 렌더링 // 탭 변경 핸들러
const renderTabContent = (tab: TabItem) => { const handleTabChange = (tabId: string) => {
const isLoading = loadingScreens[tab.id]; console.log("🔄 탭 변경:", tabId);
const error = screenErrors[tab.id]; setSelectedTab(tabId);
const screenData = loadedScreens[tab.id];
// 로딩 중 // 해당 탭의 화면 로드
if (isLoading) { const tab = visibleTabs.find((t) => t.id === tabId);
console.log("🔍 선택된 탭 정보:", { tab, hasScreenId: !!tab?.screenId, screenId: tab?.screenId });
if (tab && tab.screenId && !screenLayouts[tab.screenId]) {
console.log("📥 탭 변경 시 화면 로딩:", tab.screenId);
loadScreenLayout(tab.screenId);
}
};
// 탭 닫기 핸들러
const handleCloseTab = (tabId: string, e: React.MouseEvent) => {
e.stopPropagation();
const updatedTabs = visibleTabs.filter((tab) => tab.id !== tabId);
setVisibleTabs(updatedTabs);
// 닫은 탭이 선택된 탭이었다면 다음 탭 선택
if (selectedTab === tabId && updatedTabs.length > 0) {
setSelectedTab(updatedTabs[0].id);
}
};
// 탭 스타일 클래스
const getTabsListClass = () => {
const baseClass = orientation === "vertical" ? "flex-col" : "";
const variantClass =
variant === "pills"
? "bg-muted p-1 rounded-lg"
: variant === "underline"
? "border-b"
: "bg-muted p-1";
return `${baseClass} ${variantClass}`;
};
if (visibleTabs.length === 0) {
console.log("⚠️ 보이는 탭이 없음");
return ( return (
<div className="flex h-full flex-col items-center justify-center space-y-4"> <div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm"> </p>
</div>
);
}
console.log("🎨 TabsWidget 최종 렌더링:", {
visibleTabsCount: visibleTabs.length,
selectedTab,
screenLayoutsKeys: Object.keys(screenLayouts),
loadingScreensKeys: Object.keys(loadingScreens),
});
return (
<div className="flex h-full w-full flex-col pt-4" style={style}>
<Tabs
value={selectedTab}
onValueChange={handleTabChange}
orientation={orientation}
className="flex h-full w-full flex-col"
>
<div className="relative z-10">
<TabsList className={getTabsListClass()}>
{visibleTabs.map((tab) => (
<div key={tab.id} className="relative">
<TabsTrigger value={tab.id} disabled={tab.disabled} className="relative pr-8">
{tab.label}
</TabsTrigger>
{allowCloseable && (
<Button
onClick={(e) => handleCloseTab(tab.id, e)}
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 p-0 hover:bg-destructive/10"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
))}
</TabsList>
</div>
<div className="relative flex-1 overflow-hidden">
{visibleTabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id} className="h-full">
{tab.screenId ? (
loadingScreens[tab.screenId] ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" /> <Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground text-sm"> ...</p> <span className="text-muted-foreground ml-2"> ...</span>
</div> </div>
); ) : screenLayouts[tab.screenId] ? (
} (() => {
const layoutData = screenLayouts[tab.screenId];
const { components = [], screenResolution } = layoutData;
// 에러 발생 console.log("🎯 렌더링할 화면 데이터:", {
if (error) { screenId: tab.screenId,
return ( componentsCount: components.length,
<div className="flex h-full flex-col items-center justify-center space-y-4"> screenResolution,
<FileQuestion className="h-12 w-12 text-destructive" /> });
<div className="text-center">
<p className="mb-2 font-medium text-destructive"> </p>
<p className="text-muted-foreground text-sm">{error}</p>
</div>
</div>
);
}
// 화면 ID가 없는 경우 const designWidth = screenResolution?.width || 1920;
if (!tab.screenId) { const designHeight = screenResolution?.height || 1080;
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="text-muted-foreground h-12 w-12" />
<div className="text-center">
<p className="text-muted-foreground mb-2 text-sm"> </p>
<p className="text-xs text-gray-400"> </p>
</div>
</div>
);
}
// 화면 렌더링 - 원본 화면의 모든 컴포넌트를 그대로 렌더링
if (screenData && screenData.layout && screenData.layout.components) {
const components = screenData.layout.components;
const screenResolution = screenData.layout.screenResolution || { width: 1920, height: 1080 };
return ( return (
<div className="bg-white" style={{ width: `${screenResolution.width}px`, height: '100%' }}> <div
<div className="relative h-full"> className="relative h-full w-full overflow-auto bg-background"
{components.map((comp) => ( style={{
minHeight: `${designHeight}px`,
}}
>
<div
className="relative"
style={{
width: `${designWidth}px`,
height: `${designHeight}px`,
margin: "0 auto",
}}
>
{components.map((component: any) => (
<InteractiveScreenViewerDynamic <InteractiveScreenViewerDynamic
key={comp.id} key={component.id}
component={comp} component={component}
allComponents={components} allComponents={components}
screenInfo={{ id: tab.screenId }}
/> />
))} ))}
</div> </div>
</div> </div>
); );
} })()
) : (
return ( <div className="flex h-full w-full items-center justify-center">
<div className="flex h-full flex-col items-center justify-center space-y-4"> <p className="text-muted-foreground text-sm"> </p>
<FileQuestion className="text-muted-foreground h-12 w-12" />
<div className="text-center">
<p className="text-muted-foreground text-sm"> </p>
</div> </div>
)
) : (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm"> </p>
</div> </div>
);
};
// 빈 탭 목록
if (tabs.length === 0) {
return (
<Card className="flex h-full w-full items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-xs text-gray-400"> </p>
</div>
</Card>
);
}
return (
<div className="h-full w-full overflow-auto">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
orientation={orientation}
className="flex h-full w-full flex-col"
>
<TabsList className={orientation === "horizontal" ? "justify-start shrink-0" : "flex-col shrink-0"}>
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
disabled={tab.disabled}
className={orientation === "horizontal" ? "" : "w-full justify-start"}
>
<span>{tab.label}</span>
{tab.screenName && (
<Badge variant="secondary" className="ml-2 text-[10px]">
{tab.screenName}
</Badge>
)} )}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent
key={tab.id}
value={tab.id}
className="flex-1 mt-0 data-[state=inactive]:hidden"
>
{renderTabContent(tab)}
</TabsContent> </TabsContent>
))} ))}
</div>
</Tabs> </Tabs>
</div> </div>
); );
}; }

View File

@ -105,10 +105,13 @@ export interface DynamicComponentRendererProps {
companyCode?: string; // 🆕 현재 사용자의 회사 코드 companyCode?: string; // 🆕 현재 사용자의 회사 코드
onRefresh?: () => void; onRefresh?: () => void;
onClose?: () => void; onClose?: () => void;
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
// 테이블 선택된 행 정보 (다중 선택 액션용) // 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[]; selectedRows?: any[];
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable) // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
groupedData?: Record<string, any>[]; groupedData?: Record<string, any>[];
// 🆕 비활성화할 필드 목록 (EditModal → 각 컴포넌트)
disabledFields?: string[];
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;
// 테이블 정렬 정보 (엑셀 다운로드용) // 테이블 정렬 정보 (엑셀 다운로드용)
@ -167,6 +170,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
} }
}; };
// 🆕 disabledFields 체크
const isFieldDisabled = props.disabledFields?.includes(columnName) || (component as any).readonly;
return ( return (
<CategorySelectComponent <CategorySelectComponent
tableName={tableName} tableName={tableName}
@ -175,7 +181,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
onChange={handleChange} onChange={handleChange}
placeholder={component.componentConfig?.placeholder || "선택하세요"} placeholder={component.componentConfig?.placeholder || "선택하세요"}
required={(component as any).required} required={(component as any).required}
disabled={(component as any).readonly} disabled={isFieldDisabled}
className="w-full" className="w-full"
/> />
); );
@ -244,6 +250,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
selectedScreen, // 🆕 화면 정보 selectedScreen, // 🆕 화면 정보
onRefresh, onRefresh,
onClose, onClose,
onSave, // 🆕 EditModal의 handleSave 콜백
screenId, screenId,
userId, // 🆕 사용자 ID userId, // 🆕 사용자 ID
userName, // 🆕 사용자 이름 userName, // 🆕 사용자 이름
@ -269,6 +276,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
onConfigChange, onConfigChange,
isPreview, isPreview,
autoGeneration, autoGeneration,
disabledFields, // 🆕 비활성화 필드 목록
...restProps ...restProps
} = props; } = props;
@ -358,6 +366,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
selectedScreen, // 🆕 화면 정보 selectedScreen, // 🆕 화면 정보
onRefresh, onRefresh,
onClose, onClose,
onSave, // 🆕 EditModal의 handleSave 콜백
screenId, screenId,
userId, // 🆕 사용자 ID userId, // 🆕 사용자 ID
userName, // 🆕 사용자 이름 userName, // 🆕 사용자 이름
@ -365,7 +374,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
mode, mode,
isInModal, isInModal,
readonly: component.readonly, readonly: component.readonly,
disabled: component.readonly, // 🆕 disabledFields 체크 또는 기존 readonly
disabled: disabledFields?.includes(fieldName) || component.readonly,
originalData, originalData,
allComponents, allComponents,
onUpdateLayout, onUpdateLayout,

View File

@ -35,6 +35,7 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
onRefresh?: () => void; onRefresh?: () => void;
onClose?: () => void; onClose?: () => void;
onFlowRefresh?: () => void; onFlowRefresh?: () => void;
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
// 폼 데이터 관련 // 폼 데이터 관련
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터 originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
@ -83,6 +84,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
onRefresh, onRefresh,
onClose, onClose,
onFlowRefresh, onFlowRefresh,
onSave, // 🆕 EditModal의 handleSave 콜백
sortBy, // 🆕 정렬 컬럼 sortBy, // 🆕 정렬 컬럼
sortOrder, // 🆕 정렬 방향 sortOrder, // 🆕 정렬 방향
columnOrder, // 🆕 컬럼 순서 columnOrder, // 🆕 컬럼 순서
@ -96,6 +98,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}) => { }) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
const finalOnSave = onSave || propsOnSave;
// 🆕 플로우 단계별 표시 제어 // 🆕 플로우 단계별 표시 제어
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig; const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId); const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
@ -415,6 +421,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
onRefresh, onRefresh,
onClose, onClose,
onFlowRefresh, // 플로우 새로고침 콜백 추가 onFlowRefresh, // 플로우 새로고침 콜백 추가
onSave: finalOnSave, // 🆕 EditModal의 handleSave 콜백 (props에서도 추출)
// 테이블 선택된 행 정보 추가 // 테이블 선택된 행 정보 추가
selectedRows, selectedRows,
selectedRowsData, selectedRowsData,

View File

@ -41,6 +41,7 @@ export function ConditionalContainerComponent({
style, style,
className, className,
groupedData, // 🆕 그룹 데이터 groupedData, // 🆕 그룹 데이터
onSave, // 🆕 EditModal의 handleSave 콜백
}: ConditionalContainerProps) { }: ConditionalContainerProps) {
console.log("🎯 ConditionalContainerComponent 렌더링!", { console.log("🎯 ConditionalContainerComponent 렌더링!", {
isDesignMode, isDesignMode,
@ -179,6 +180,7 @@ export function ConditionalContainerComponent({
formData={formData} formData={formData}
onFormDataChange={onFormDataChange} onFormDataChange={onFormDataChange}
groupedData={groupedData} groupedData={groupedData}
onSave={onSave}
/> />
))} ))}
</div> </div>
@ -199,6 +201,7 @@ export function ConditionalContainerComponent({
formData={formData} formData={formData}
onFormDataChange={onFormDataChange} onFormDataChange={onFormDataChange}
groupedData={groupedData} groupedData={groupedData}
onSave={onSave}
/> />
) : null ) : null
) )

View File

@ -26,6 +26,7 @@ export function ConditionalSectionViewer({
formData, formData,
onFormDataChange, onFormDataChange,
groupedData, // 🆕 그룹 데이터 groupedData, // 🆕 그룹 데이터
onSave, // 🆕 EditModal의 handleSave 콜백
}: ConditionalSectionViewerProps) { }: ConditionalSectionViewerProps) {
const { userId, userName, user } = useAuth(); const { userId, userName, user } = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -163,6 +164,7 @@ export function ConditionalSectionViewer({
formData={formData} formData={formData}
onFormDataChange={onFormDataChange} onFormDataChange={onFormDataChange}
groupedData={groupedData} groupedData={groupedData}
onSave={onSave}
/> />
</div> </div>
); );

View File

@ -46,6 +46,7 @@ export interface ConditionalContainerProps {
formData?: Record<string, any>; formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void; onFormDataChange?: (fieldName: string, value: any) => void;
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable) groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
// 화면 편집기 관련 // 화면 편집기 관련
isDesignMode?: boolean; // 디자인 모드 여부 isDesignMode?: boolean; // 디자인 모드 여부
@ -77,5 +78,6 @@ export interface ConditionalSectionViewerProps {
formData?: Record<string, any>; formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void; onFormDataChange?: (fieldName: string, value: any) => void;
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 groupedData?: Record<string, any>[]; // 🆕 그룹 데이터
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
} }

View File

@ -59,6 +59,9 @@ import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식 import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식
import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식 import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식
// 🆕 탭 컴포넌트
import "./tabs/tabs-component"; // 탭 기반 화면 전환 컴포넌트
/** /**
* *
*/ */

View File

@ -195,13 +195,18 @@ export function ModalRepeaterTableComponent({
const columnName = component?.columnName; const columnName = component?.columnName;
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
// ✅ onChange 래퍼 (기존 onChange 콜백만 호출, formData는 beforeFormSave에서 처리) // ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출)
const handleChange = (newData: any[]) => { const handleChange = (newData: any[]) => {
// 기존 onChange 콜백 호출 (호환성) // 기존 onChange 콜백 호출 (호환성)
const externalOnChange = componentConfig?.onChange || propOnChange; const externalOnChange = componentConfig?.onChange || propOnChange;
if (externalOnChange) { if (externalOnChange) {
externalOnChange(newData); externalOnChange(newData);
} }
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
if (onFormDataChange && columnName) {
onFormDataChange(columnName, newData);
}
}; };
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경 // uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경

View File

@ -322,6 +322,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [sortColumn, setSortColumn] = useState<string | null>(null); const [sortColumn, setSortColumn] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const hasInitializedSort = useRef(false);
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
const [tableLabel, setTableLabel] = useState<string>(""); const [tableLabel, setTableLabel] = useState<string>("");
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20); const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
@ -508,6 +509,28 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
unregisterTable, unregisterTable,
]); ]);
// 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기
useEffect(() => {
if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return;
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
const savedSort = localStorage.getItem(storageKey);
if (savedSort) {
try {
const { column, direction } = JSON.parse(savedSort);
if (column && direction) {
setSortColumn(column);
setSortDirection(direction);
hasInitializedSort.current = true;
console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction });
}
} catch (error) {
console.error("❌ 정렬 상태 복원 실패:", error);
}
}
}, [tableConfig.selectedTable, userId]);
// 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기 // 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기
useEffect(() => { useEffect(() => {
if (!tableConfig.selectedTable || !userId) return; if (!tableConfig.selectedTable || !userId) return;
@ -955,6 +978,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
newSortDirection = "asc"; newSortDirection = "asc";
} }
// 🎯 정렬 상태를 localStorage에 저장 (사용자별)
if (tableConfig.selectedTable && userId) {
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
try {
localStorage.setItem(storageKey, JSON.stringify({
column: newSortColumn,
direction: newSortDirection
}));
console.log("💾 정렬 상태 저장:", { column: newSortColumn, direction: newSortDirection });
} catch (error) {
console.error("❌ 정렬 상태 저장 실패:", error);
}
}
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection }); console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange); console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
@ -1876,11 +1913,59 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}; };
}, [tableConfig.selectedTable, isDesignMode]); }, [tableConfig.selectedTable, isDesignMode]);
// 초기 컬럼 너비 측정 (한 번만) // 🎯 컬럼 너비 자동 계산 (내용 기반)
const calculateOptimalColumnWidth = useCallback((columnName: string, displayName: string): number => {
// 기본 너비 설정
const MIN_WIDTH = 100;
const MAX_WIDTH = 400;
const PADDING = 48; // 좌우 패딩 + 여유 공간
const HEADER_PADDING = 60; // 헤더 추가 여유 (정렬 아이콘 등)
// 헤더 텍스트 너비 계산 (대략 8px per character)
const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING;
// 데이터 셀 너비 계산 (상위 50개 샘플링)
const sampleSize = Math.min(50, data.length);
let maxDataWidth = headerWidth;
for (let i = 0; i < sampleSize; i++) {
const cellValue = data[i]?.[columnName];
if (cellValue !== null && cellValue !== undefined) {
const cellText = String(cellValue);
// 숫자는 좁게, 텍스트는 넓게 계산
const isNumber = !isNaN(Number(cellValue)) && cellValue !== "";
const charWidth = isNumber ? 8 : 9;
const cellWidth = cellText.length * charWidth + PADDING;
maxDataWidth = Math.max(maxDataWidth, cellWidth);
}
}
// 최소/최대 범위 내로 제한
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth)));
}, [data]);
// 🎯 localStorage에서 컬럼 너비 불러오기 및 초기 계산
useEffect(() => { useEffect(() => {
if (!hasInitializedWidths.current && visibleColumns.length > 0) { if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) {
// 약간의 지연을 두고 DOM이 완전히 렌더링된 후 측정
const timer = setTimeout(() => { const timer = setTimeout(() => {
const storageKey = tableConfig.selectedTable && userId
? `table_column_widths_${tableConfig.selectedTable}_${userId}`
: null;
// 1. localStorage에서 저장된 너비 불러오기
let savedWidths: Record<string, number> = {};
if (storageKey) {
try {
const saved = localStorage.getItem(storageKey);
if (saved) {
savedWidths = JSON.parse(saved);
}
} catch (error) {
console.error("컬럼 너비 불러오기 실패:", error);
}
}
// 2. 자동 계산 또는 저장된 너비 적용
const newWidths: Record<string, number> = {}; const newWidths: Record<string, number> = {};
let hasAnyWidth = false; let hasAnyWidth = false;
@ -1888,13 +1973,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 체크박스 컬럼은 제외 (고정 48px) // 체크박스 컬럼은 제외 (고정 48px)
if (column.columnName === "__checkbox__") return; if (column.columnName === "__checkbox__") return;
const thElement = columnRefs.current[column.columnName]; // 저장된 너비가 있으면 우선 사용
if (thElement) { if (savedWidths[column.columnName]) {
const measuredWidth = thElement.offsetWidth; newWidths[column.columnName] = savedWidths[column.columnName];
if (measuredWidth > 0) { hasAnyWidth = true;
newWidths[column.columnName] = measuredWidth; } else {
// 저장된 너비가 없으면 자동 계산
const optimalWidth = calculateOptimalColumnWidth(
column.columnName,
columnLabels[column.columnName] || column.displayName
);
newWidths[column.columnName] = optimalWidth;
hasAnyWidth = true; hasAnyWidth = true;
}
} }
}); });
@ -1902,11 +1992,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setColumnWidths(newWidths); setColumnWidths(newWidths);
hasInitializedWidths.current = true; hasInitializedWidths.current = true;
} }
}, 100); }, 150); // DOM 렌더링 대기
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [visibleColumns]); }, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]);
// ======================================== // ========================================
// 페이지네이션 JSX // 페이지네이션 JSX
@ -2241,7 +2331,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 최종 너비를 state에 저장 // 최종 너비를 state에 저장
if (thElement) { if (thElement) {
const finalWidth = Math.max(80, thElement.offsetWidth); const finalWidth = Math.max(80, thElement.offsetWidth);
setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth })); setColumnWidths((prev) => {
const newWidths = { ...prev, [column.columnName]: finalWidth };
// 🎯 localStorage에 컬럼 너비 저장 (사용자별)
if (tableConfig.selectedTable && userId) {
const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`;
try {
localStorage.setItem(storageKey, JSON.stringify(newWidths));
} catch (error) {
console.error("컬럼 너비 저장 실패:", error);
}
}
return newWidths;
});
} }
// 텍스트 선택 복원 // 텍스트 선택 복원

View File

@ -0,0 +1,165 @@
"use client";
import React from "react";
import { ComponentRegistry } from "../../ComponentRegistry";
import { ComponentCategory } from "@/types/component";
import { Folder } from "lucide-react";
import type { TabsComponent, TabItem } from "@/types/screen-management";
// TabsWidget 래퍼 컴포넌트
const TabsWidgetWrapper: React.FC<any> = (props) => {
const { component, ...restProps } = props;
// componentConfig에서 탭 정보 추출
const tabsConfig = component.componentConfig || {};
const tabsComponent = {
...component,
type: "tabs" as const,
tabs: tabsConfig.tabs || [],
defaultTab: tabsConfig.defaultTab,
orientation: tabsConfig.orientation || "horizontal",
variant: tabsConfig.variant || "default",
allowCloseable: tabsConfig.allowCloseable || false,
persistSelection: tabsConfig.persistSelection || false,
};
console.log("🎨 TabsWidget 렌더링:", {
componentId: component.id,
tabs: tabsComponent.tabs,
tabsLength: tabsComponent.tabs.length,
component,
});
// TabsWidget 동적 로드
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
return (
<div className="h-full w-full">
<TabsWidget component={tabsComponent} {...restProps} />
</div>
);
};
/**
*
*
*
*/
ComponentRegistry.registerComponent({
id: "tabs-widget",
name: "탭 컴포넌트",
description: "화면을 탭으로 전환할 수 있는 컴포넌트입니다. 각 탭마다 다른 화면을 연결할 수 있습니다.",
category: ComponentCategory.LAYOUT,
webType: "text" as any, // 레이아웃 컴포넌트이므로 임시값
component: TabsWidgetWrapper, // ✅ 실제 TabsWidget 렌더러
defaultConfig: {},
tags: ["tabs", "navigation", "layout", "screen"],
icon: Folder,
version: "1.0.0",
defaultSize: {
width: 800,
height: 600,
},
defaultProps: {
type: "tabs" as const,
tabs: [
{
id: "tab-1",
label: "탭 1",
order: 0,
disabled: false,
},
{
id: "tab-2",
label: "탭 2",
order: 1,
disabled: false,
},
] as TabItem[],
defaultTab: "tab-1",
orientation: "horizontal" as const,
variant: "default" as const,
allowCloseable: false,
persistSelection: false,
},
// 에디터 모드에서의 렌더링
renderEditor: ({ component, isSelected, onClick, onDragStart, onDragEnd, children }) => {
const tabsComponent = component as TabsComponent;
const tabs = tabsComponent.tabs || [];
return (
<div
className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50"
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<div className="text-center">
<div className="flex items-center justify-center">
<Folder className="h-8 w-8 text-gray-400" />
</div>
<p className="text-muted-foreground mt-2 text-sm font-medium"> </p>
<p className="text-xs text-gray-400">
{tabs.length > 0
? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)`
: "탭이 없습니다. 설정 패널에서 탭을 추가하세요"}
</p>
{tabs.length > 0 && (
<div className="mt-2 flex flex-wrap justify-center gap-1">
{tabs.map((tab: TabItem, index: number) => (
<span
key={tab.id}
className="rounded-md border bg-white px-2 py-1 text-xs"
>
{tab.label || `${index + 1}`}
</span>
))}
</div>
)}
</div>
</div>
);
},
// 인터랙티브 모드에서의 렌더링 (실제 동작)
renderInteractive: ({ component }) => {
// InteractiveScreenViewer에서 TabsWidget을 사용하므로 여기서는 null 반환
return null;
},
// 설정 패널 (동적 로딩)
configPanel: React.lazy(() =>
import("@/components/screen/config-panels/TabsConfigPanel").then(module => ({
default: module.TabsConfigPanel
}))
),
// 검증 함수
validate: (component) => {
const tabsComponent = component as TabsComponent;
const errors: string[] = [];
if (!tabsComponent.tabs || tabsComponent.tabs.length === 0) {
errors.push("최소 1개 이상의 탭이 필요합니다.");
}
if (tabsComponent.tabs) {
const tabIds = tabsComponent.tabs.map((t) => t.id);
const uniqueIds = new Set(tabIds);
if (tabIds.length !== uniqueIds.size) {
errors.push("탭 ID가 중복되었습니다.");
}
}
return {
isValid: errors.length === 0,
errors,
};
},
});
console.log("✅ 탭 컴포넌트 등록 완료");

View File

@ -112,6 +112,7 @@ export interface ButtonActionContext {
onClose?: () => void; onClose?: () => void;
onRefresh?: () => void; onRefresh?: () => void;
onFlowRefresh?: () => void; // 플로우 새로고침 콜백 onFlowRefresh?: () => void; // 플로우 새로고침 콜백
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
// 테이블 선택된 행 정보 (다중 선택 액션용) // 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[]; selectedRows?: any[];
@ -213,9 +214,23 @@ export class ButtonActionExecutor {
* (INSERT/UPDATE - DB ) * (INSERT/UPDATE - DB )
*/ */
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> { private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
const { formData, originalData, tableName, screenId } = context; const { formData, originalData, tableName, screenId, onSave } = context;
console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId }); console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId, hasOnSave: !!onSave });
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
if (onSave) {
console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행");
try {
await onSave();
return true;
} catch (error) {
console.error("❌ [handleSave] onSave 콜백 실행 오류:", error);
throw error;
}
}
console.log("⚠️ [handleSave] onSave 콜백 없음 - 기본 저장 로직 실행");
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집) // 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
// context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함 // context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함
@ -1666,7 +1681,11 @@ export class ButtonActionExecutor {
}); });
// 🔥 새로운 버튼 액션 실행 시스템 사용 // 🔥 새로운 버튼 액션 실행 시스템 사용
if (config.dataflowConfig?.controlMode === "flow" && config.dataflowConfig?.flowConfig) { // flowConfig가 있으면 controlMode가 명시되지 않아도 플로우 모드로 간주
const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId;
const isFlowMode = config.dataflowConfig?.controlMode === "flow" || hasFlowConfig;
if (isFlowMode && config.dataflowConfig?.flowConfig) {
console.log("🎯 노드 플로우 실행:", config.dataflowConfig.flowConfig); console.log("🎯 노드 플로우 실행:", config.dataflowConfig.flowConfig);
const { flowId, executionTiming } = config.dataflowConfig.flowConfig; const { flowId, executionTiming } = config.dataflowConfig.flowConfig;
@ -1704,6 +1723,8 @@ export class ButtonActionExecutor {
}); });
} else { } else {
console.warn("⚠️ flow-selection 모드이지만 선택된 플로우 데이터가 없습니다."); console.warn("⚠️ flow-selection 모드이지만 선택된 플로우 데이터가 없습니다.");
toast.error("플로우에서 데이터를 먼저 선택해주세요.");
return false;
} }
break; break;
@ -1716,6 +1737,8 @@ export class ButtonActionExecutor {
}); });
} else { } else {
console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다."); console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다.");
toast.error("테이블에서 처리할 항목을 먼저 선택해주세요.");
return false;
} }
break; break;

View File

@ -42,6 +42,8 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
// 🆕 섹션 그룹화 레이아웃 // 🆕 섹션 그룹화 레이아웃
"section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"), "section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"),
"section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"), "section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
// 🆕 탭 컴포넌트
"tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"),
}; };
// ConfigPanel 컴포넌트 캐시 // ConfigPanel 컴포넌트 캐시
@ -76,6 +78,7 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
module.ButtonConfigPanel || // button-primary의 export명 module.ButtonConfigPanel || // button-primary의 export명
module.SectionCardConfigPanel || // section-card의 export명 module.SectionCardConfigPanel || // section-card의 export명
module.SectionPaperConfigPanel || // section-paper의 export명 module.SectionPaperConfigPanel || // section-paper의 export명
module.TabsConfigPanel || // tabs-widget의 export명
module.default; module.default;
if (!ConfigPanelComponent) { if (!ConfigPanelComponent) {

View File

@ -14,6 +14,7 @@ import { RatingTypeConfigPanel } from "@/components/screen/panels/webtype-config
import { ButtonConfigPanel as OriginalButtonConfigPanel } from "@/components/screen/config-panels/ButtonConfigPanel"; import { ButtonConfigPanel as OriginalButtonConfigPanel } from "@/components/screen/config-panels/ButtonConfigPanel";
import { CardConfigPanel } from "@/components/screen/config-panels/CardConfigPanel"; import { CardConfigPanel } from "@/components/screen/config-panels/CardConfigPanel";
import { DashboardConfigPanel } from "@/components/screen/config-panels/DashboardConfigPanel"; import { DashboardConfigPanel } from "@/components/screen/config-panels/DashboardConfigPanel";
import { TabsConfigPanel } from "@/components/screen/config-panels/TabsConfigPanel";
// 설정 패널 컴포넌트 타입 // 설정 패널 컴포넌트 타입
export type ConfigPanelComponent = React.ComponentType<{ export type ConfigPanelComponent = React.ComponentType<{
@ -83,6 +84,26 @@ const DashboardConfigPanelWrapper: ConfigPanelComponent = ({ config, onConfigCha
return <DashboardConfigPanel component={mockComponent as any} onUpdateProperty={handleUpdateProperty} />; return <DashboardConfigPanel component={mockComponent as any} onUpdateProperty={handleUpdateProperty} />;
}; };
// TabsConfigPanel 래퍼
const TabsConfigPanelWrapper: ConfigPanelComponent = ({ config, onConfigChange }) => {
const mockComponent = {
id: "temp",
type: "tabs" as const,
tabs: config.tabs || [],
defaultTab: config.defaultTab,
orientation: config.orientation || "horizontal",
variant: config.variant || "default",
allowCloseable: config.allowCloseable || false,
persistSelection: config.persistSelection || false,
};
const handleUpdate = (updates: any) => {
onConfigChange({ ...config, ...updates });
};
return <TabsConfigPanel component={mockComponent as any} onUpdate={handleUpdate} />;
};
// 설정 패널 이름으로 직접 매핑하는 함수 (DB의 config_panel 필드용) // 설정 패널 이름으로 직접 매핑하는 함수 (DB의 config_panel 필드용)
export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent | null => { export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent | null => {
console.log(`🔧 getConfigPanelComponent 호출: panelName="${panelName}"`); console.log(`🔧 getConfigPanelComponent 호출: panelName="${panelName}"`);
@ -128,6 +149,9 @@ export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent
case "DashboardConfigPanel": case "DashboardConfigPanel":
console.log(`🔧 DashboardConfigPanel 래퍼 컴포넌트 반환`); console.log(`🔧 DashboardConfigPanel 래퍼 컴포넌트 반환`);
return DashboardConfigPanelWrapper; return DashboardConfigPanelWrapper;
case "TabsConfigPanel":
console.log(`🔧 TabsConfigPanel 래퍼 컴포넌트 반환`);
return TabsConfigPanelWrapper;
default: default:
console.warn(`🔧 알 수 없는 설정 패널: ${panelName}, 기본 설정 사용`); console.warn(`🔧 알 수 없는 설정 패널: ${panelName}, 기본 설정 사용`);
return null; // 기본 설정 (패널 없음) return null; // 기본 설정 (패널 없음)

View File

@ -190,6 +190,32 @@ export interface ComponentComponent extends BaseComponent {
componentConfig: any; // 컴포넌트별 설정 componentConfig: any; // 컴포넌트별 설정
} }
/**
*
*/
export interface TabItem {
id: string;
label: string;
screenId?: number; // 연결된 화면 ID
screenName?: string; // 화면 이름 (표시용)
icon?: string; // 아이콘 (선택사항)
disabled?: boolean; // 비활성화 여부
order: number; // 탭 순서
}
/**
*
*/
export interface TabsComponent extends BaseComponent {
type: "tabs";
tabs: TabItem[]; // 탭 목록
defaultTab?: string; // 기본 선택 탭 ID
orientation?: "horizontal" | "vertical"; // 탭 방향
variant?: "default" | "pills" | "underline"; // 탭 스타일
allowCloseable?: boolean; // 탭 닫기 버튼 표시 여부
persistSelection?: boolean; // 선택 상태 유지 (localStorage)
}
/** /**
* *
*/ */
@ -200,7 +226,8 @@ export type ComponentData =
| DataTableComponent | DataTableComponent
| FileComponent | FileComponent
| FlowComponent | FlowComponent
| ComponentComponent; | ComponentComponent
| TabsComponent;
// ===== 웹타입별 설정 인터페이스 ===== // ===== 웹타입별 설정 인터페이스 =====
@ -791,6 +818,13 @@ export const isFlowComponent = (component: ComponentData): component is FlowComp
return component.type === "flow"; return component.type === "flow";
}; };
/**
* TabsComponent
*/
export const isTabsComponent = (component: ComponentData): component is TabsComponent => {
return component.type === "tabs";
};
// ===== 안전한 타입 캐스팅 유틸리티 ===== // ===== 안전한 타입 캐스팅 유틸리티 =====
/** /**
@ -852,3 +886,13 @@ export const asFlowComponent = (component: ComponentData): FlowComponent => {
} }
return component; return component;
}; };
/**
* ComponentData를 TabsComponent로
*/
export const asTabsComponent = (component: ComponentData): TabsComponent => {
if (!isTabsComponent(component)) {
throw new Error(`Expected TabsComponent, got ${component.type}`);
}
return component;
};

View File

@ -85,7 +85,8 @@ export type ComponentType =
| "area" | "area"
| "layout" | "layout"
| "flow" | "flow"
| "component"; | "component"
| "tabs";
/** /**
* *

542
시연_시나리오.md Normal file
View File

@ -0,0 +1,542 @@
# ERP-node 시스템 시연 시나리오
## 전체 개요
**주제**: 발주 → 입고 프로세스 자동화
**목표**: 버튼 클릭 한 번으로 발주 데이터가 입고 테이블로 자동 이동하는 것을 보여주기
**총 시간**: 10분
---
## Part 1: 테이블 2개 생성 (2분)
### 1-1. 발주 테이블 생성
**화면 조작**:
1. 테이블 관리 메뉴 접속
2. "새 테이블" 버튼 클릭
3. 테이블 정보 입력:
- **테이블명(영문)**: `purchase_order`
- **테이블명(한글)**: `발주`
- **설명**: `발주 관리`
4. 컬럼 추가 (4개):
| 컬럼명(영문) | 컬럼명(한글) | 타입 | 필수 여부 |
| ------------ | ------------ | ------ | --------- |
| order_no | 발주번호 | text | ✓ |
| item_name | 품목명 | text | ✓ |
| quantity | 수량 | number | ✓ |
| unit_price | 단가 | number | ✓ |
5. "테이블 생성" 버튼 클릭
6. 성공 메시지 확인
---
### 1-2. 입고 테이블 생성
**화면 조작**:
1. "새 테이블" 버튼 클릭
2. 테이블 정보 입력:
- **테이블명(영문)**: `receiving`
- **테이블명(한글)**: `입고`
- **설명**: `입고 관리`
3. 컬럼 추가 (5개):
| 컬럼명(영문) | 컬럼명(한글) | 타입 | 필수 여부 | 비고 |
| -------------- | ------------ | ------ | --------- | ------------------- |
| receiving_no | 입고번호 | text | ✓ | 자동 생성 |
| order_no | 발주번호 | text | ✓ | 발주 테이블 참조 |
| item_name | 품목명 | text | ✓ | |
| quantity | 수량 | number | ✓ | |
| receiving_date | 입고일자 | date | ✓ | 오늘 날짜 자동 입력 |
4. "테이블 생성" 버튼 클릭
5. 성공 메시지 확인
**포인트 강조**:
- 클릭만으로 데이터베이스 테이블 자동 생성
- Input Type에 따라 적절한 UI 자동 설정
---
## Part 2: 메뉴 2개 생성 (1분)
### 2-1. 발주 관리 메뉴 생성
**화면 조작**:
1. 관리자 메뉴 > 메뉴 관리 접속
2. "새 메뉴 추가" 버튼 클릭
3. 메뉴 정보 입력:
- **메뉴명**: `발주 관리`
- **순서**: 1
4. "저장" 클릭
---
### 2-2. 입고 관리 메뉴 생성
**화면 조작**:
1. "새 메뉴 추가" 버튼 클릭
2. 메뉴 정보 입력:
- **메뉴명**: `입고 관리`
- **순서**: 2
3. "저장" 클릭
4. 좌측 메뉴바에서 새로 생성된 메뉴 2개 확인
**포인트 강조**:
- URL 기반 자동 라우팅
- 아이콘으로 직관적인 메뉴 구성
---
## Part 3: 플로우 생성 (2분)
### 3-1. 플로우 생성
**화면 조작**:
1. 제어 관리 메뉴 접속
2. "새 플로우 생성" 버튼 클릭
3. 플로우 생성 모달에서 입력:
- **플로우명**: `발주-입고 프로세스`
- **설명**: `발주에서 입고로 데이터 자동 이동`
4. "생성" 버튼 클릭
5. 플로우 편집 화면(캔버스)으로 자동 이동
---
### 3-2. 노드 구성
**내레이션**:
"플로우는 소스 테이블과 액션 노드로 구성합니다. 발주 테이블에서 입고 테이블로 데이터를 INSERT하는 구조입니다."
**노드 1: 발주 테이블 소스**
**화면 조작**:
1. 캔버스 좌측 팔레트에서 "테이블 소스" 에서 테이블 노드 드래그
2. 캔버스에 드롭
3. 생성된 노드 클릭 → 우측 속성 패널 표시
4. 속성 패널에서 설정:
- **노드명**: `발주 테이블`
- **소스 테이블**: `purchase_order` 선택
- **색상**: 파란색 (#3b82f6)
5. 데이터 소스 타입 컨텍스트 데이터 선택
---
**노드 2: 입고 INSERT 액션**
**화면 조작**:
1. 좌측 팔레트에서 "INSERT 액션" 노드 드래그
2. 캔버스의 발주 테이블 오른쪽에 드롭
3. 노드 클릭 → 우측 속성 패널 표시
4. 속성 패널에서 설정:
- **노드명**: `입고 처리`
- **타겟 테이블**: `receiving`(입고) 선택
- **액션 타입**: INSERT
- **색상**: 초록색 (#22c55e)
---
### 3-3. 노드 연결 및 필드 매핑
**내레이션**:
"소스 테이블과 액션 노드를 연결하고 필드 매핑을 설정합니다."
**화면 조작**:
1. "발주 테이블" 노드의 오른쪽 연결점(핸들)에 마우스 올리기
2. 연결점에서 드래그 시작
3. "입고 처리" 노드의 왼쪽 연결점으로 드래그
4. 연결선 자동 생성됨
5. "입고 처리" (INSERT 액션) 노드 클릭
6. 우측 속성 패널에서 "필드 매핑" 탭 선택
7. 필드 매핑 설정:
| 소스 필드 (발주) | 타겟 필드 (입고) | 비고 |
| ---------------- | ---------------- | ------------- |
| order_no | order_no | 발주번호 복사 |
| item_name | item_name | 품목명 복사 |
| quantity | quantity | 수량 복사 |
| (자동 생성) | receiving_no | 입고번호 |
| (현재 날짜) | receiving_date | 입고일자 |
8. 우측 상단 "저장" 버튼 클릭
9. 성공 메시지: "플로우가 저장되었습니다"
**포인트 강조**:
- 테이블 소스 → 액션 노드 구조
- 필드 매핑으로 데이터 자동 복사 설정
- INSERT 액션으로 새 테이블에 데이터 생성
**참고**:
- `receiving_no``receiving_date`는 자동 생성 필드로 설정
- 같은 이름의 필드는 자동 매핑됨
---
## Part 4: 화면 설계 (2분)
### 4-1. 발주 관리 화면 설계
**화면 조작**:
1. 화면 관리 > 화면 설계 메뉴 접속
2. "발주 관리" 메뉴의 "화면 할당" 클릭
3. "새 화면 생성" 선택
4. 테이블 선택: `purchase_order` (발주)
**화면 구성**:
**전체: 테이블 리스트 컴포넌트 (CRUD 기능 포함)**
1. 컴포넌트 팔레트에서 "테이블 리스트" 드래그
2. 테이블 설정:
- **연결 테이블**: `purchase_order`
- **컬럼 표시**:
| 컬럼 | 표시 | 정렬 가능 | 너비 |
| ---------- | ---- | --------- | ----- |
| order_no | ✓ | ✓ | 150px |
| item_name | ✓ | ✓ | 200px |
| quantity | ✓ | | 100px |
| unit_price | ✓ | | 120px |
3. 기능 설정:
- **조회**: 활성화
- **등록**: 활성화 (신규 버튼)
- **수정**: 활성화
- **삭제**: 활성화
- **페이징**: 10개씩
- **입고 처리 버튼**: 커스텀 액션 추가
4. 입고 처리 버튼 설정:
- **버튼 라벨**: `입고 처리`
- **버튼 위치**: 행 액션
- **연결 플로우**: `발주-입고 프로세스` 선택
- **플로우 액션**: `입고 처리` (Connection에서 정의한 액션)
5. "화면 저장" 버튼 클릭
---
### 4-2. 입고 관리 화면 설계
**화면 조작**:
1. "입고 관리" 메뉴의 "화면 할당" 클릭
2. "새 화면 생성" 선택
3. 테이블 선택: `receiving` (입고)
**화면 구성**:
**전체: 테이블 리스트 컴포넌트 (조회 전용)**
1. 컴포넌트 팔레트에서 "테이블 리스트" 드래그
2. 테이블 설정:
- **연결 테이블**: `receiving`
- **컬럼 표시**:
| 컬럼 | 표시 | 정렬 가능 | 너비 |
| -------------- | ---- | --------- | ----- |
| receiving_no | ✓ | ✓ | 150px |
| order_no | ✓ | ✓ | 150px |
| item_name | ✓ | ✓ | 200px |
| quantity | ✓ | | 100px |
| receiving_date | ✓ | ✓ | 120px |
3. 기능 설정:
- **조회**: 활성화
- **등록**: 비활성화 (플로우로만 데이터 생성)
- **수정**: 비활성화
- **삭제**: 비활성화
- **페이징**: 20개씩
- **정렬**: 입고일자 내림차순
4. "화면 저장" 버튼 클릭
**포인트 강조**:
- 테이블 리스트 컴포넌트로 CRUD 자동 구성
- 발주 화면에는 "입고 처리" 버튼으로 플로우 실행
- 입고 화면은 조회 전용 (플로우로만 데이터 생성)
---
## Part 5: 실행 및 동작 확인 (3분)
### 5-1. 발주 등록
**화면 조작**:
1. 좌측 메뉴에서 "발주 관리" 클릭
2. 화면 구성 확인:
- 테이블 리스트 컴포넌트 (빈 테이블)
- 상단에 "신규" 버튼
3. "신규" 버튼 클릭
4. 입력 모달 창 표시
5. 데이터 입력:
- **발주번호**: PO-001
- **품목명**: 노트북 (LG Gram 17)
- **수량**: 10
- **단가**: 2,000,000
6. "저장" 버튼 클릭
7. 성공 메시지 확인: "저장되었습니다"
8. 결과 확인:
- 테이블에 새 행 추가됨
- 행 우측에 "입고 처리" 버튼 표시됨
**추가 발주 등록 (옵션)**:
9. "신규" 버튼 클릭
10. 2번째 데이터 입력:
- **발주번호**: PO-002
- **품목명**: 모니터 (삼성 27인치)
- **수량**: 5
- **단가**: 300,000
11. "저장" 클릭
12. 테이블에 2개 행 확인
---
### 5-2. 입고 처리 실행 ⭐ (핵심 데모)
**화면 조작**:
1. 발주 테이블에서 첫 번째 행(PO-001 노트북) 확인
2. 행 우측의 **"입고 처리"** 버튼 클릭
3. 확인 대화상자:
- "이 발주를 입고 처리하시겠습니까?"
- **"예"** 클릭
4. 성공 메시지: "입고 처리되었습니다"
---
### 5-3. 자동 데이터 이동 확인 ⭐⭐⭐
**실시간 변화 확인**:
**1) 발주 테이블 자동 업데이트**
- PO-001 항목이 테이블에서 **즉시 사라짐**
- PO-002만 남아있음 (추가로 등록했다면)
**2) 입고 관리 화면으로 이동**
1. 좌측 메뉴에서 **"입고 관리"** 클릭
2. 입고 테이블에 **자동으로 데이터 생성됨**:
| 입고번호 | 발주번호 | 품목명 | 수량 | 입고일자 |
| ---------------- | -------- | ------------------- | ---- | ---------- |
| RCV-20250124-001 | PO-001 | 노트북 (LG Gram 17) | 10 | 2025-01-24 |
3. **데이터 자동 생성 확인**:
- 입고번호: 자동 생성됨 (RCV-20250124-001)
- 발주번호: PO-001 복사됨
- 품목명: 노트북 (LG Gram 17) 복사됨
- 수량: 10 복사됨
- 입고일자: 오늘 날짜 자동 입력
**3) 다시 발주 관리로 돌아가기**
1. 좌측 메뉴 "발주 관리" 클릭
2. PO-001은 여전히 사라진 상태 확인
3. PO-002만 남아있음
**4) 제어 관리에서 확인**
1. 제어 관리 > 플로우 목록 접속
2. "발주-입고 프로세스" 클릭
3. 플로우 현황 확인:
- **발주 완료**: 1건 (PO-002)
- **입고 완료**: 1건 (PO-001)
---
### 5-4. 추가 입고 처리 (옵션)
**화면 조작**:
1. "발주 관리" 화면에서 PO-002 (모니터) 선택
2. "입고 처리" 버튼 클릭
3. 확인 후 입고 완료
4. 최종 확인:
- 발주 관리: 0건 (모두 입고 처리됨)
- 입고 관리: 2건 (PO-001, PO-002)
- 제어 관리 플로우:
- **발주 완료: 0건**
- **입고 완료: 2건**
---
## 시연 마무리 (30초)
**화면 정리 및 요약**:
**보여준 핵심 기능**:
- ✅ **코딩 없이 테이블 생성**: 클릭만으로 DB 테이블 자동 생성
- ✅ **시각적 플로우 구성**: 드래그앤드롭으로 업무 흐름 설계
- ✅ **자동 데이터 이동**: 버튼 클릭 한 번으로 테이블 간 데이터 자동 복사 및 이동
- ✅ **실시간 상태 추적**: 제어 관리에서 플로우 현황 확인
- ✅ **빠른 화면 구성**: 테이블 리스트 컴포넌트로 CRUD 자동 완성
**마지막 화면**:
- 대시보드 또는 시스템 전체 구성도
- 로고 및 연락처 정보
**자막**:
"개발자 없이도 비즈니스 담당자가 직접 업무 시스템을 구축할 수 있습니다."
---
## 시간 배분 요약
| 파트 | 시간 | 주요 내용 |
| -------- | ---------- | ---------------------------- |
| Part 1 | 2분 | 테이블 2개 생성 (발주, 입고) |
| Part 2 | 1분 | 메뉴 2개 생성 |
| Part 3 | 2분 | 플로우 구성 및 연결 설정 |
| Part 4 | 2분 | 화면 2개 디자인 |
| Part 5 | 3분 | 발주 등록 → 입고 처리 실행 |
| 마무리 | 0.5분 | 요약 및 정리 |
| **합계** | **10.5분** | |
---
## 시연 준비사항
### 사전 설정
1. 개발 서버 실행: `http://localhost:9771`
2. 로그인 정보: `wace / qlalfqjsgh11`
3. 데이터베이스 초기화 (테스트 데이터 제거)
### 녹화 설정
- **해상도**: 1920x1080 (Full HD)
- **프레임**: 30fps
- **마우스 효과**: 클릭 하이라이트 활성화
- **배경음악**: 부드러운 BGM (옵션)
- **자막**: 주요 포인트마다 표시
### 시연 팁
- 각 단계마다 2-3초 대기 (시청자 이해 시간)
- 중요한 버튼 클릭 시 화면 확대 효과
- 플로우 위젯 카운트 변화는 빨간색 박스로 강조
- 성공 메시지는 충분히 길게 보여주기 (최소 3초)
- 입고 테이블에 데이터 들어오는 순간 화면 확대
---
## 시연 스크립트 (참고용)
### 오프닝 (10초)
"안녕하세요. 오늘은 ERP-node 시스템의 핵심 기능을 시연하겠습니다. 발주에서 입고까지 데이터가 자동으로 이동하는 과정을 보여드립니다."
### Part 1 (2분)
"먼저 발주와 입고를 관리할 테이블을 생성합니다. 코딩 없이 클릭만으로 데이터베이스 테이블이 자동으로 만들어집니다."
### Part 2 (1분)
"이제 사용자가 접근할 메뉴를 추가합니다. URL만 지정하면 자동으로 라우팅이 연결됩니다."
### Part 3 (2분)
"발주에서 입고로 데이터가 이동하는 흐름을 제어 플로우로 정의합니다. 두 테이블을 연결하고 버튼을 누르면 자동으로 데이터가 복사 및 이동하도록 설정합니다."
### Part 4 (2분)
"실제 사용자가 볼 화면을 디자인합니다. 테이블 리스트 컴포넌트를 사용하면 CRUD 기능이 자동으로 구성되고, 각 행에 입고 처리 버튼을 추가하여 플로우를 실행할 수 있습니다."
### Part 5 (3분)
"이제 실제로 작동하는 모습을 보겠습니다. 발주를 등록하고... (데이터 입력) 저장하면 테이블에 추가됩니다. 입고 처리 버튼을 누르면... (클릭) 발주 테이블에서 데이터가 사라지고 입고 테이블에 자동으로 생성됩니다!"
### 클로징 (10초)
"이처럼 ERP-node는 코딩 없이 비즈니스 로직을 구현할 수 있는 노코드 플랫폼입니다. 감사합니다."
---
## 체크리스트
### 시연 전
- [ ] 개발 서버 실행 확인
- [ ] 로그인 테스트
- [ ] 기존 테스트 데이터 삭제
- [ ] 브라우저 창 크기 조정 (1920x1080)
- [ ] 녹화 프로그램 설정
- [ ] 마이크 테스트
- [ ] 시나리오 1회 이상 리허설
### 시연 중
- [ ] 천천히 명확하게 진행
- [ ] 각 단계마다 결과 확인
- [ ] 플로우 위젯 카운트 강조
- [ ] 입고 테이블 데이터 자동 생성 강조
### 시연 후
- [ ] 녹화 파일 확인
- [ ] 자막 추가 (필요 시)
- [ ] 배경음악 삽입 (옵션)
- [ ] 인트로/아웃트로 편집
- [ ] 최종 영상 검수
---
## 추가 개선 아이디어
### 시연 버전 2 (고급)
- 발주 승인 단계 추가 (발주 요청 → 승인 → 입고)
- 입고 수량 불일치 처리 (일부 입고)
- 대시보드에서 통계 차트 표시
### 시연 버전 3 (실전)
- 실제 업무: 구매 요청 → 견적 → 발주 → 입고 → 검수
- 권한 관리: 요청자, 승인자, 구매담당자 역할 분리
- 알림: 각 단계 변경 시 담당자에게 알림
---
**작성일**: 2025-01-24
**버전**: 1.0
**작성자**: AI Assistant