feature/screen-management #220

Merged
kjs merged 10 commits from feature/screen-management into main 2025-11-25 13:05:28 +09:00
62 changed files with 2521 additions and 1024 deletions

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import { query, queryOne } from "../../database/db";
import { logger } from "../../utils/logger";
import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService";
import { AuthenticatedRequest } from "../../types/auth";
import { authenticateToken } from "../../middleware/authMiddleware";
const router = Router();
@ -217,19 +218,29 @@ router.delete("/:flowId", async (req: Request, res: Response) => {
*
* 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 {
const { flowId } = req.params;
const contextData = req.body;
logger.info(`🚀 플로우 실행 요청: flowId=${flowId}`, {
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(
parseInt(flowId, 10),
contextData
enrichedContextData
);
return res.json({

View File

@ -938,6 +938,30 @@ export class NodeFlowExecutionService {
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 = `
INSERT INTO ${targetTable} (${fields.join(", ")})
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})

View File

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

View File

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

View File

@ -1,6 +1,6 @@
export default function MainHomePage() {
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">
<h3 className="mb-4 text-lg font-semibold">WACE !</h3>

File diff suppressed because it is too large Load Diff

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">
<div className="h-full w-full p-4">{children}</div>
{children}
</main>
</div>

View File

@ -346,6 +346,14 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
// 실제 사용 가능한 위젯 렌더링
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") {
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 fieldName = columnName || comp.id;
const currentValue = formData[fieldName] || "";

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" && (
<div className="relative h-full w-full">

View File

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

View File

@ -981,7 +981,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
// 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트
useEffect(() => {
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) {
return;
}
@ -997,6 +1008,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
};
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") {
e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단
setIsPanMode(false);

View File

@ -35,7 +35,10 @@ import {
DialogDescription,
} from "@/components/ui/dialog";
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 { screenApi } from "@/lib/api/screen";
import CreateScreenModal from "./CreateScreenModal";
@ -127,8 +130,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
isActive: "Y",
tableName: "",
});
const [tables, setTables] = useState<string[]>([]);
const [tables, setTables] = useState<Array<{ tableName: string; tableLabel: string }>>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = 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 response = await tableManagementApi.getTableList();
if (response.success && response.data) {
// tableName만 추출 (camelCase)
const tableNames = response.data.map((table: any) => table.tableName);
setTables(tableNames);
// tableName과 displayName 매핑 (백엔드에서 displayName으로 라벨을 반환함)
const tableList = response.data.map((table: any) => ({
tableName: table.tableName,
tableLabel: table.displayName || table.tableName,
}));
setTables(tableList);
}
} catch (error) {
console.error("테이블 목록 조회 실패:", error);
@ -297,6 +304,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
// 화면 정보 업데이트 API 호출
await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData);
// 선택된 테이블의 라벨 찾기
const selectedTable = tables.find((t) => t.tableName === editFormData.tableName);
const tableLabel = selectedTable?.tableLabel || editFormData.tableName;
// 목록에서 해당 화면 정보 업데이트
setScreens((prev) =>
prev.map((s) =>
@ -304,6 +315,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
? {
...s,
screenName: editFormData.screenName,
tableName: editFormData.tableName,
tableLabel: tableLabel,
description: editFormData.description,
isActive: editFormData.isActive,
}
@ -1202,22 +1215,62 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
</div>
<div className="space-y-2">
<Label htmlFor="edit-tableName"> *</Label>
<Select
value={editFormData.tableName}
onValueChange={(value) => setEditFormData({ ...editFormData, tableName: value })}
disabled={loadingTables}
>
<SelectTrigger id="edit-tableName">
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"} />
</SelectTrigger>
<SelectContent>
{tables.map((tableName) => (
<SelectItem key={tableName} value={tableName}>
{tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
<PopoverTrigger asChild>
<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}
>
{loadingTables
? "로딩 중..."
: editFormData.tableName
? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
: "테이블을 선택하세요"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</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>
{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>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label htmlFor="edit-description"></Label>

View File

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

View File

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

View File

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

View File

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

View File

@ -75,7 +75,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<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" />
</CardTitle>
@ -95,7 +95,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="날짜를 선택하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@ -149,7 +149,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.minDate || ""}
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">
@ -167,7 +167,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.maxDate || ""}
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">
@ -190,7 +190,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.defaultValue || ""}
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">
@ -245,7 +245,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={localConfig.minDate}
max={localConfig.maxDate}
defaultValue={localConfig.defaultValue}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<div className="text-muted-foreground mt-2 text-xs">
: {localConfig.format}

View File

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

View File

@ -113,7 +113,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<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" />
</CardTitle>
@ -133,7 +133,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.uploadText || ""}
onChange={(e) => updateConfig("uploadText", e.target.value)}
placeholder="파일을 선택하거나 여기에 드래그하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@ -146,7 +146,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.browseText || ""}
onChange={(e) => updateConfig("browseText", e.target.value)}
placeholder="파일 선택"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@ -196,7 +196,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={0.1}
max={1024}
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>
</div>
@ -214,7 +214,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("maxFiles", parseInt(e.target.value))}
min={1}
max={100}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
)}
@ -257,7 +257,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newFileType}
onChange={(e) => setNewFileType(e.target.value)}
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">

View File

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

View File

@ -55,7 +55,7 @@ export function FlowWidgetConfigPanel({ config = {}, onChange }: FlowWidgetConfi
{loading ? (
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
<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>
) : (
<>

View File

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

View File

@ -168,7 +168,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<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" />
</CardTitle>
@ -188,7 +188,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupLabel || ""}
onChange={(e) => updateConfig("groupLabel", e.target.value)}
placeholder="라디오버튼 그룹 제목"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@ -201,7 +201,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupName || ""}
onChange={(e) => updateConfig("groupName", e.target.value)}
placeholder="자동 생성 (필드명 기반)"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<p className="text-muted-foreground text-xs"> .</p>
</div>
@ -252,19 +252,19 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button
size="sm"
onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
<Plus className="h-3 w-3" />
</Button>
@ -278,7 +278,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={bulkOptions}
onChange={(e) => setBulkOptions(e.target.value)}
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">
@ -295,13 +295,13 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Switch
checked={!option.disabled}
@ -328,7 +328,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
id="defaultValue"
value={localConfig.defaultValue || ""}
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>
{localConfig.options.map((option, index) => (
@ -390,7 +390,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly || option.disabled}
required={localConfig.required && index === 0} // 첫 번째에만 required 표시
defaultChecked={localConfig.defaultValue === option.value}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<Label htmlFor={`preview-radio-${index}`} className="text-xs">
{option.label}

View File

@ -153,7 +153,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<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" />
</CardTitle>
@ -173,7 +173,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="선택하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@ -186,7 +186,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.emptyMessage || ""}
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
placeholder="선택 가능한 옵션이 없습니다"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@ -247,19 +247,19 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button
size="sm"
onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
<Plus className="h-3 w-3" />
</Button>
@ -273,7 +273,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={bulkOptions}
onChange={(e) => setBulkOptions(e.target.value)}
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">
@ -290,13 +290,13 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Switch
checked={!option.disabled}
@ -323,7 +323,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
id="defaultValue"
value={localConfig.defaultValue || ""}
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>
{localConfig.options.map((option, index) => (
@ -376,7 +376,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly}
required={localConfig.required}
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}
>
<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 (
<Card>
<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>
</CardHeader>
<CardContent className="space-y-4">
@ -72,7 +72,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="입력 안내 텍스트"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@ -88,7 +88,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="0"
min="0"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
<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)}
placeholder="100"
min="1"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@ -141,7 +141,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.pattern || ""}
onChange={(e) => updateConfig("pattern", e.target.value)}
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>
</div>
@ -219,7 +219,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
minLength={localConfig.minLength}
pattern={localConfig.pattern}
autoComplete={localConfig.autoComplete}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>

View File

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

View File

@ -94,7 +94,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
max={100}
value={gap}
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">
{gap}px
@ -109,7 +109,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
</Label>
<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 />
</SelectTrigger>
<SelectContent>

View File

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

View File

@ -458,7 +458,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
updateSettings({ options: newOptions });
}}
placeholder="옵션명"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
<Button
type="button"
@ -483,7 +483,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
const newOption = { label: "", value: "" };
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" />
@ -548,7 +548,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.min || ""}
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최소값"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@ -558,7 +558,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.max || ""}
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대값"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@ -571,7 +571,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.step || "0.01"}
onChange={(e) => updateSettings({ step: e.target.value })}
placeholder="0.01"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
)}
@ -589,7 +589,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
type="date"
value={localSettings.minDate || ""}
onChange={(e) => updateSettings({ minDate: e.target.value })}
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@ -598,7 +598,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
type="date"
value={localSettings.maxDate || ""}
onChange={(e) => updateSettings({ maxDate: e.target.value })}
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@ -626,7 +626,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@ -635,7 +635,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.placeholder || ""}
onChange={(e) => updateSettings({ placeholder: e.target.value })}
placeholder="입력 안내 텍스트"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@ -652,7 +652,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.rows || "3"}
onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
placeholder="3"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@ -662,7 +662,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@ -678,7 +678,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.accept || ""}
onChange={(e) => updateSettings({ accept: e.target.value })}
placeholder=".jpg,.png,.pdf"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@ -688,7 +688,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
placeholder="10"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="flex items-center space-x-2">
@ -1132,7 +1132,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{/* 기본 설정 */}
<Card>
<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" />
<span> </span>
</CardTitle>
@ -1184,7 +1184,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableAdd: checked as boolean });
}}
/>
<Label htmlFor="enable-add" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-add" className="text-xs">
</Label>
</div>
@ -1198,7 +1198,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableEdit: checked as boolean });
}}
/>
<Label htmlFor="enable-edit" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-edit" className="text-xs">
</Label>
</div>
@ -1212,7 +1212,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableDelete: checked as boolean });
}}
/>
<Label htmlFor="enable-delete" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-delete" className="text-xs">
</Label>
</div>
@ -1220,7 +1220,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-3 gap-4">
<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>
<Input
@ -1233,12 +1233,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}}
placeholder="추가"
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 className="space-y-2">
<Label htmlFor="edit-button-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="edit-button-text" className="text-xs">
</Label>
<Input
@ -1251,12 +1251,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}}
placeholder="수정"
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 className="space-y-2">
<Label htmlFor="delete-button-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="delete-button-text" className="text-xs">
</Label>
<Input
@ -1269,7 +1269,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}}
placeholder="삭제"
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>
@ -1284,7 +1284,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-title" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-title" className="text-xs">
</Label>
<Input
@ -1298,12 +1298,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
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 className="space-y-2">
<Label htmlFor="modal-width" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-width" className="text-xs">
</Label>
<select
@ -1328,7 +1328,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
</div>
<div className="space-y-2">
<Label htmlFor="modal-description" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-description" className="text-xs">
</Label>
<Input
@ -1342,13 +1342,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
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 className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-layout" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-layout" className="text-xs">
</Label>
<select
@ -1370,7 +1370,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{localValues.modalLayout === "grid" && (
<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>
<select
@ -1394,7 +1394,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-2 gap-4">
<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>
<Input
@ -1408,12 +1408,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
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 className="space-y-2">
<Label htmlFor="modal-cancel-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-cancel-text" className="text-xs">
</Label>
<Input
@ -1427,7 +1427,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
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>
@ -1441,7 +1441,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="space-y-3">
<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>
<Input
@ -1455,13 +1455,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
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>
</div>
<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>
<Input
@ -1475,7 +1475,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
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>
</div>
@ -1494,7 +1494,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
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>
</div>
@ -1509,7 +1509,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableExport: checked as boolean });
}}
/>
<Label htmlFor="enable-export" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-export" className="text-xs">
</Label>
</div>
@ -1521,7 +1521,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<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" />
<span> </span>
</CardTitle>
@ -1654,7 +1654,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}
}}
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>
@ -1947,7 +1947,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
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>
)}
@ -1967,7 +1967,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<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" />
<span> </span>
</CardTitle>
@ -1995,7 +1995,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{component.filters.length === 0 ? (
<div className="text-muted-foreground py-8 text-center">
<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>
</div>
) : (
@ -2073,7 +2073,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
updateFilter(index, { label: newValue });
}}
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>
<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">
<Card>
<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" />
<span> </span>
</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>
</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>
</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>
</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"
style={{ fontSize: "12px" }}
/>
</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"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@ -211,7 +209,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</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"
style={{ fontSize: "12px" }}
>
<option value="row"> (row)</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"
style={{ fontSize: "12px" }}
/>
<span className="text-xs text-gray-500"></span>
</div>
@ -332,7 +327,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@ -348,7 +342,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
value={layoutComponent.layoutConfig?.split?.direction || "horizontal"}
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"
style={{ fontSize: "12px" }}
>
<option value="horizontal"> </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"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{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"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{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"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{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"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{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"
style={{ fontSize: "12px" }}
>
+
</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"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{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"
style={{ fontSize: "12px" }}
>
</button>
@ -578,7 +564,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
@ -593,7 +578,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</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"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@ -711,7 +694,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
placeholder="100%"
/>
</div>
@ -724,7 +706,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
placeholder="auto"
/>
</div>
@ -1007,7 +988,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3>
</div>
<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 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>
</div>
<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 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="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 className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
</div>
{webType && currentBaseInputType && (
<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 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 && (
<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 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>
</div>
<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 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">
<label className="block text-sm font-medium text-gray-700"> </label>
<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="세부 타입을 선택하세요" />
</SelectTrigger>
<SelectContent>

View File

@ -98,7 +98,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
size="sm"
variant="ghost"
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>
@ -152,7 +152,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
{groupInfo.buttons.map((button) => (
<div
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" />
<span className="flex-1 truncate font-medium">

View File

@ -214,7 +214,7 @@ export default function LayoutsPanel({
</Badge>
</div>
</div>
<CardTitle className="text-xs" style={{ fontSize: "12px" }}>{layout.name}</CardTitle>
<CardTitle className="text-xs">{layout.name}</CardTitle>
</CardHeader>
<CardContent className="pt-0">
{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"
/>
<Label htmlFor="required" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="required" className="text-xs">
</Label>
</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"
/>
<Label htmlFor="readonly" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="readonly" className="text-xs">
</Label>
</div>
@ -990,7 +990,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
</>
) : (
<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>
</div>
)}

View File

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

View File

@ -109,7 +109,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
variant={row.gap === preset ? "default" : "outline"}
size="sm"
onClick={() => onUpdateRow({ gap: preset })}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
{GAP_PRESETS[preset].label}
</Button>
@ -130,7 +130,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
variant={row.padding === preset ? "default" : "outline"}
size="sm"
onClick={() => onUpdateRow({ padding: preset })}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
{GAP_PRESETS[preset].label}
</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 space-x-2">
<Info className="h-4 w-4" />
<span className="text-xs" style={{ fontSize: "12px" }}>릿 , 릿 </span>
<span className="text-xs">릿 , 릿 </span>
</div>
<Button size="sm" variant="outline" onClick={() => refetch()} className="border-amber-300 text-amber-700 hover:bg-amber-100">
<RefreshCw className="h-4 w-4" />

View File

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

View File

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

View File

@ -122,7 +122,7 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
</Label>
<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="라벨 위치 선택" />
</SelectTrigger>
<SelectContent>
@ -194,18 +194,18 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
<Label className="text-sm font-medium text-gray-700"></Label>
<div className="mt-2 flex items-center space-x-2">
{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 && (
<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" />
</div>
)}
{(localValues.labelPosition === "right" || localValues.labelPosition === "bottom") && (
<>
<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} />}

View File

@ -121,7 +121,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
</Label>
<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="언어 선택" />
</SelectTrigger>
<SelectContent className="max-h-60">
@ -140,7 +140,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
</Label>
<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="테마 선택" />
</SelectTrigger>
<SelectContent>

View File

@ -193,7 +193,7 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
}, 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="날짜 형식 선택" />
</SelectTrigger>
<SelectContent>

View File

@ -233,7 +233,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
</Label>
<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="형식 선택" />
</SelectTrigger>
<SelectContent>
@ -267,7 +267,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
{/* 기존 필터 목록 */}
<div className="max-h-40 space-y-2 overflow-y-auto">
{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
value={field}
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="flex items-center space-x-2 rounded border bg-white p-2">
<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 || "엔터티"}를 선택하세요`}
</div>
<Database className="h-4 w-4 text-gray-400" />

View File

@ -111,7 +111,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
</Label>
<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="숫자 형식 선택" />
</SelectTrigger>
<SelectContent>

View File

@ -259,7 +259,7 @@ export const RadioTypeConfigPanel: React.FC<RadioTypeConfigPanelProps> = ({ conf
{(safeConfig.options || []).map((option) => (
<div key={option.value} className="flex items-center space-x-2">
<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}
</Label>
</div>

View File

@ -170,7 +170,7 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
value={localValues.placeholder}
onChange={(e) => updateConfig("placeholder", e.target.value)}
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>

View File

@ -218,7 +218,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
</Label>
<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="입력 형식 선택" />
</SelectTrigger>
<SelectContent>
@ -332,7 +332,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
</Label>
<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="자동값 타입 선택" />
</SelectTrigger>
<SelectContent>

View File

@ -202,7 +202,7 @@ export const TextareaTypeConfigPanel: React.FC<TextareaTypeConfigPanelProps> = (
<Label className="text-sm font-medium text-gray-700"></Label>
<div className="mt-2">
<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}
placeholder={localValues.placeholder || "텍스트를 입력하세요..."}
style={{

View File

@ -1194,7 +1194,7 @@ export function FlowWidget({
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 />
</SelectTrigger>
<SelectContent>

View File

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

View File

@ -53,7 +53,7 @@ export default function SelectWidget({ widget, value, onChange, options = [], cl
</Label>
)}
<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 || "선택해주세요"} />
</SelectTrigger>
<SelectContent>

View File

@ -1,210 +1,258 @@
"use client";
import React, { useState, useEffect } from "react";
import { TabsComponent, TabItem, ScreenDefinition } from "@/types";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Loader2, FileQuestion } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
import { Button } from "@/components/ui/button";
import { X, Loader2 } from "lucide-react";
import type { TabsComponent, TabItem } from "@/types/screen-management";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
interface TabsWidgetProps {
component: TabsComponent;
isPreview?: boolean;
className?: string;
style?: React.CSSProperties;
}
/**
*
*
*/
export const TabsWidget: React.FC<TabsWidgetProps> = ({ component, isPreview = false }) => {
// componentConfig에서 설정 읽기 (새 컴포넌트 시스템)
const config = (component as any).componentConfig || component;
const { tabs = [], defaultTab, orientation = "horizontal", variant = "default" } = config;
// console.log("🔍 TabsWidget 렌더링:", {
// component,
// componentConfig: (component as any).componentConfig,
// tabs,
// tabsLength: tabs.length
// });
export function TabsWidget({ component, className, style }: TabsWidgetProps) {
const {
tabs = [],
defaultTab,
orientation = "horizontal",
variant = "default",
allowCloseable = false,
persistSelection = false,
} = component;
const [activeTab, setActiveTab] = useState<string>(defaultTab || tabs[0]?.id || "");
const [loadedScreens, setLoadedScreens] = useState<Record<string, any>>({});
console.log("🎨 TabsWidget 렌더링:", {
componentId: component.id,
tabs,
tabsLength: tabs.length,
component,
});
const storageKey = `tabs-${component.id}-selected`;
// 초기 선택 탭 결정
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 [screenErrors, setScreenErrors] = useState<Record<string, string>>({});
const [screenLayouts, setScreenLayouts] = useState<Record<number, any>>({});
// 탭 변경 시 화면 로드
// 컴포넌트 목록 변경 시 동기
useEffect(() => {
if (!activeTab) return;
setVisibleTabs(tabs.filter((tab) => !tab.disabled));
}, [tabs]);
const currentTab = tabs.find((tab) => tab.id === activeTab);
if (!currentTab || !currentTab.screenId) return;
// 선택된 탭 변경 시 localStorage에 저장
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]) {
console.log("📥 초기 화면 로딩 시작:", currentTab.screenId);
loadScreenLayout(currentTab.screenId);
}
}, [selectedTab, visibleTabs]);
// 이미 로딩 중이면 스킵
if (loadingScreens[activeTab]) return;
// 화면 레이아웃 로드
const loadScreenLayout = async (screenId: number) => {
if (screenLayouts[screenId]) {
console.log("✅ 이미 로드된 화면:", screenId);
return; // 이미 로드됨
}
// 화면 로드 시작
loadScreen(activeTab, currentTab.screenId);
}, [activeTab, tabs]);
const loadScreen = async (tabId: string, screenId: number) => {
setLoadingScreens((prev) => ({ ...prev, [tabId]: true }));
setScreenErrors((prev) => ({ ...prev, [tabId]: "" }));
console.log("📥 화면 레이아웃 로딩 시작:", screenId);
setLoadingScreens((prev) => ({ ...prev, [screenId]: true }));
try {
const layoutData = await screenApi.getLayout(screenId);
if (layoutData) {
setLoadedScreens((prev) => ({
...prev,
[tabId]: {
screenId,
layout: layoutData,
},
}));
} else {
setScreenErrors((prev) => ({
...prev,
[tabId]: "화면을 불러올 수 없습니다",
}));
}
} catch (error: any) {
setScreenErrors((prev) => ({
...prev,
[tabId]: error.message || "화면 로드 중 오류가 발생했습니다",
}));
} finally {
setLoadingScreens((prev) => ({ ...prev, [tabId]: false }));
}
};
// 탭 콘텐츠 렌더링
const renderTabContent = (tab: TabItem) => {
const isLoading = loadingScreens[tab.id];
const error = screenErrors[tab.id];
const screenData = loadedScreens[tab.id];
// 로딩 중
if (isLoading) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground text-sm"> ...</p>
</div>
);
}
// 에러 발생
if (error) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<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가 없는 경우
if (!tab.screenId) {
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 };
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 });
return (
<div className="bg-white" style={{ width: `${screenResolution.width}px`, height: '100%' }}>
<div className="relative h-full">
{components.map((comp) => (
<InteractiveScreenViewerDynamic
key={comp.id}
component={comp}
allComponents={components}
screenInfo={{ id: tab.screenId }}
/>
))}
</div>
</div>
);
if (response.data.success && response.data.data) {
console.log("✅ 화면 레이아웃 로드 완료:", screenId);
setScreenLayouts((prev) => ({ ...prev, [screenId]: response.data.data }));
} else {
console.error("❌ 화면 레이아웃 로드 실패 - success false");
}
} catch (error) {
console.error(`❌ 화면 레이아웃 로드 실패 ${screenId}:`, error);
} finally {
setLoadingScreens((prev) => ({ ...prev, [screenId]: false }));
}
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 text-sm"> </p>
</div>
</div>
);
};
// 빈 탭 목록
if (tabs.length === 0) {
// 탭 변경 핸들러
const handleTabChange = (tabId: string) => {
console.log("🔄 탭 변경:", tabId);
setSelectedTab(tabId);
// 해당 탭의 화면 로드
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 (
<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>
<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="h-full w-full overflow-auto">
<div className="flex h-full w-full flex-col pt-4" style={style}>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
value={selectedTab}
onValueChange={handleTabChange}
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>
<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>
{tabs.map((tab) => (
<TabsContent
key={tab.id}
value={tab.id}
className="flex-1 mt-0 data-[state=inactive]:hidden"
>
{renderTabContent(tab)}
</TabsContent>
))}
<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" />
<span className="text-muted-foreground ml-2"> ...</span>
</div>
) : screenLayouts[tab.screenId] ? (
(() => {
const layoutData = screenLayouts[tab.screenId];
const { components = [], screenResolution } = layoutData;
console.log("🎯 렌더링할 화면 데이터:", {
screenId: tab.screenId,
componentsCount: components.length,
screenResolution,
});
const designWidth = screenResolution?.width || 1920;
const designHeight = screenResolution?.height || 1080;
return (
<div
className="relative h-full w-full overflow-auto bg-background"
style={{
minHeight: `${designHeight}px`,
}}
>
<div
className="relative"
style={{
width: `${designWidth}px`,
height: `${designHeight}px`,
margin: "0 auto",
}}
>
{components.map((component: any) => (
<InteractiveScreenViewerDynamic
key={component.id}
component={component}
allComponents={components}
/>
))}
</div>
</div>
);
})()
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
</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>
)}
</TabsContent>
))}
</div>
</Tabs>
</div>
);
};
}

View File

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

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

@ -1666,7 +1666,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);
const { flowId, executionTiming } = config.dataflowConfig.flowConfig;
@ -1704,6 +1708,8 @@ export class ButtonActionExecutor {
});
} else {
console.warn("⚠️ flow-selection 모드이지만 선택된 플로우 데이터가 없습니다.");
toast.error("플로우에서 데이터를 먼저 선택해주세요.");
return false;
}
break;
@ -1716,6 +1722,8 @@ export class ButtonActionExecutor {
});
} else {
console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다.");
toast.error("테이블에서 처리할 항목을 먼저 선택해주세요.");
return false;
}
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-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
// 🆕 탭 컴포넌트
"tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"),
};
// ConfigPanel 컴포넌트 캐시
@ -76,6 +78,7 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
module.ButtonConfigPanel || // button-primary의 export명
module.SectionCardConfigPanel || // section-card의 export명
module.SectionPaperConfigPanel || // section-paper의 export명
module.TabsConfigPanel || // tabs-widget의 export명
module.default;
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 { CardConfigPanel } from "@/components/screen/config-panels/CardConfigPanel";
import { DashboardConfigPanel } from "@/components/screen/config-panels/DashboardConfigPanel";
import { TabsConfigPanel } from "@/components/screen/config-panels/TabsConfigPanel";
// 설정 패널 컴포넌트 타입
export type ConfigPanelComponent = React.ComponentType<{
@ -83,6 +84,26 @@ const DashboardConfigPanelWrapper: ConfigPanelComponent = ({ config, onConfigCha
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 필드용)
export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent | null => {
console.log(`🔧 getConfigPanelComponent 호출: panelName="${panelName}"`);
@ -128,6 +149,9 @@ export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent
case "DashboardConfigPanel":
console.log(`🔧 DashboardConfigPanel 래퍼 컴포넌트 반환`);
return DashboardConfigPanelWrapper;
case "TabsConfigPanel":
console.log(`🔧 TabsConfigPanel 래퍼 컴포넌트 반환`);
return TabsConfigPanelWrapper;
default:
console.warn(`🔧 알 수 없는 설정 패널: ${panelName}, 기본 설정 사용`);
return null; // 기본 설정 (패널 없음)

View File

@ -190,6 +190,32 @@ export interface ComponentComponent extends BaseComponent {
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
| FileComponent
| FlowComponent
| ComponentComponent;
| ComponentComponent
| TabsComponent;
// ===== 웹타입별 설정 인터페이스 =====
@ -791,6 +818,13 @@ export const isFlowComponent = (component: ComponentData): component is FlowComp
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;
};
/**
* 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"
| "layout"
| "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