feature/v2-renewal #400
|
|
@ -251,7 +251,7 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
|
|||
<Select
|
||||
value={selectedDefinitionId}
|
||||
onValueChange={(v) => {
|
||||
setSelectedDefinitionId(v);
|
||||
setSelectedDefinitionId(v === "none" ? "" : v);
|
||||
setSelectedTemplateId("");
|
||||
setApprovers([]);
|
||||
}}
|
||||
|
|
@ -260,7 +260,7 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
|
|||
<SelectValue placeholder={isLoadingDefs ? "로딩 중..." : "결재 유형 선택 (선택사항)"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">유형 없음</SelectItem>
|
||||
<SelectItem value="none">유형 없음</SelectItem>
|
||||
{definitions.map((def) => (
|
||||
<SelectItem key={def.definition_id} value={String(def.definition_id)}>
|
||||
{def.definition_name}
|
||||
|
|
@ -276,13 +276,13 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
|
|||
<Label className="text-xs sm:text-sm">결재선 템플릿</Label>
|
||||
<Select
|
||||
value={selectedTemplateId}
|
||||
onValueChange={setSelectedTemplateId}
|
||||
onValueChange={(v) => setSelectedTemplateId(v === "none" ? "" : v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder={isLoadingTemplates ? "로딩 중..." : "템플릿 선택 (선택사항)"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">직접 입력</SelectItem>
|
||||
<SelectItem value="none">직접 입력</SelectItem>
|
||||
{templates.map((tmpl) => (
|
||||
<SelectItem key={tmpl.template_id} value={String(tmpl.template_id)}>
|
||||
{tmpl.template_name}
|
||||
|
|
|
|||
|
|
@ -3770,14 +3770,14 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
<Select
|
||||
value={String(component.componentConfig?.action?.approvalDefinitionId || "")}
|
||||
onValueChange={(value) => {
|
||||
onUpdateProperty("componentConfig.action.approvalDefinitionId", value ? Number(value) : null);
|
||||
onUpdateProperty("componentConfig.action.approvalDefinitionId", value === "none" ? null : Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder={approvalDefinitionsLoading ? "로딩 중..." : "결재 유형 선택 (선택사항)"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">유형 없음 (직접 설정)</SelectItem>
|
||||
<SelectItem value="none">유형 없음 (직접 설정)</SelectItem>
|
||||
{approvalDefinitions.map((def) => (
|
||||
<SelectItem key={def.definition_id} value={String(def.definition_id)}>
|
||||
{def.definition_name}
|
||||
|
|
|
|||
|
|
@ -420,10 +420,7 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
|||
</div>
|
||||
|
||||
{entityJoinTables.map((joinTable, idx) => {
|
||||
// 같은 테이블이 여러 FK로 조인될 수 있으므로 sourceColumn으로 고유 키 생성
|
||||
const uniqueKey = joinTable.joinConfig?.sourceColumn
|
||||
? `entity-join-${joinTable.tableName}-${joinTable.joinConfig.sourceColumn}`
|
||||
: `entity-join-${joinTable.tableName}-${idx}`;
|
||||
const uniqueKey = `entity-join-${joinTable.tableName}-${joinTable.joinConfig?.sourceColumn || ''}-${idx}`;
|
||||
const isExpanded = expandedJoinTables.has(joinTable.tableName);
|
||||
// 검색어로 필터링
|
||||
const filteredColumns = searchTerm
|
||||
|
|
|
|||
|
|
@ -1,367 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { BaseLayoutRenderer, LayoutRendererProps } from "./BaseLayoutRenderer";
|
||||
import { LayoutDefinition } from "@/types/layout";
|
||||
import { LayoutRegistry } from "../LayoutRegistry";
|
||||
import { ComponentData, LayoutComponent } from "@/types/screen";
|
||||
import { LayoutZone } from "@/types/layout";
|
||||
import React from "react";
|
||||
import { DynamicComponentRenderer } from "../DynamicComponentRenderer";
|
||||
|
||||
/**
|
||||
* 자동 등록 기능을 제공하는 베이스 레이아웃 렌더러
|
||||
*
|
||||
* 사용 방법:
|
||||
* 1. 이 클래스를 상속받습니다
|
||||
* 2. static layoutDefinition을 정의합니다
|
||||
* 3. 파일을 import하면 자동으로 등록됩니다
|
||||
*/
|
||||
export class AutoRegisteringLayoutRenderer {
|
||||
protected props: LayoutRendererProps;
|
||||
|
||||
constructor(props: LayoutRendererProps) {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 정의 - 각 구현 클래스에서 반드시 정의해야 함
|
||||
*/
|
||||
static readonly layoutDefinition: LayoutDefinition;
|
||||
|
||||
/**
|
||||
* 렌더링 메서드 - 각 구현 클래스에서 오버라이드해야 함
|
||||
*/
|
||||
render(): React.ReactElement {
|
||||
throw new Error("render() method must be implemented by subclass");
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 컨테이너 스타일을 계산합니다.
|
||||
*/
|
||||
getLayoutContainerStyle(): React.CSSProperties {
|
||||
const { layout, style: propStyle } = this.props;
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
width: layout.size.width,
|
||||
height: layout.size.height,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
...propStyle,
|
||||
};
|
||||
|
||||
// 레이아웃 커스텀 스타일 적용
|
||||
if (layout.style) {
|
||||
Object.assign(style, this.convertComponentStyleToCSS(layout.style));
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 스타일을 CSS 스타일로 변환합니다.
|
||||
*/
|
||||
protected convertComponentStyleToCSS(componentStyle: any): React.CSSProperties {
|
||||
const cssStyle: React.CSSProperties = {};
|
||||
|
||||
if (componentStyle.backgroundColor) {
|
||||
cssStyle.backgroundColor = componentStyle.backgroundColor;
|
||||
}
|
||||
if (componentStyle.borderColor) {
|
||||
cssStyle.borderColor = componentStyle.borderColor;
|
||||
}
|
||||
if (componentStyle.borderWidth) {
|
||||
cssStyle.borderWidth = `${componentStyle.borderWidth}px`;
|
||||
}
|
||||
if (componentStyle.borderStyle) {
|
||||
cssStyle.borderStyle = componentStyle.borderStyle;
|
||||
}
|
||||
if (componentStyle.borderRadius) {
|
||||
cssStyle.borderRadius = `${componentStyle.borderRadius}px`;
|
||||
}
|
||||
|
||||
return cssStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 존별 자식 컴포넌트들을 분류합니다.
|
||||
*/
|
||||
getZoneChildren(zoneId: string): ComponentData[] {
|
||||
return this.props.allComponents.filter((comp) => comp.parentId === this.props.layout.id && comp.zoneId === zoneId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 존을 렌더링합니다.
|
||||
*/
|
||||
renderZone(
|
||||
zone: LayoutZone,
|
||||
zoneChildren: ComponentData[] = [],
|
||||
additionalProps: Record<string, any> = {},
|
||||
): React.ReactElement {
|
||||
const { isDesignMode, onZoneClick, onComponentDrop } = this.props;
|
||||
|
||||
// 존 스타일 계산 - 항상 구역 경계 표시
|
||||
const zoneStyle: React.CSSProperties = {
|
||||
position: "relative",
|
||||
// 구역 경계 시각화 - 항상 표시
|
||||
border: "1px solid #e2e8f0",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "rgba(248, 250, 252, 0.5)",
|
||||
transition: "all 0.2s ease",
|
||||
...this.getZoneStyle(zone),
|
||||
...additionalProps.style,
|
||||
};
|
||||
|
||||
// 디자인 모드일 때 더 강조된 스타일
|
||||
if (isDesignMode) {
|
||||
zoneStyle.border = "2px dashed #cbd5e1";
|
||||
zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)";
|
||||
}
|
||||
|
||||
// 호버 효과를 위한 추가 스타일
|
||||
const dropZoneStyle: React.CSSProperties = {
|
||||
minHeight: isDesignMode ? "60px" : "40px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: zoneChildren.length === 0 ? "center" : "stretch",
|
||||
justifyContent: zoneChildren.length === 0 ? "flex-start" : "flex-start",
|
||||
color: "#64748b",
|
||||
fontSize: "12px",
|
||||
transition: "all 0.2s ease",
|
||||
padding: "8px",
|
||||
position: "relative",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={zone.id}
|
||||
className={`layout-zone ${additionalProps.className || ""}`}
|
||||
style={zoneStyle}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onZoneClick?.(zone.id, e);
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const componentData = e.dataTransfer.getData("application/json");
|
||||
if (componentData) {
|
||||
try {
|
||||
const component = JSON.parse(componentData);
|
||||
onComponentDrop?.(zone.id, component, e);
|
||||
} catch (error) {
|
||||
console.error("컴포넌트 드롭 데이터 파싱 오류:", error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#3b82f6";
|
||||
e.currentTarget.style.backgroundColor = "rgba(59, 130, 246, 0.05)";
|
||||
e.currentTarget.style.boxShadow = "0 0 0 2px rgba(59, 130, 246, 0.1)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0";
|
||||
e.currentTarget.style.backgroundColor = isDesignMode
|
||||
? "rgba(241, 245, 249, 0.8)"
|
||||
: "rgba(248, 250, 252, 0.5)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
{...additionalProps}
|
||||
>
|
||||
{/* 존 라벨 */}
|
||||
<div
|
||||
className="zone-label"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-2px",
|
||||
left: "8px",
|
||||
backgroundColor: isDesignMode ? "#3b82f6" : "#64748b",
|
||||
color: "white",
|
||||
fontSize: "10px",
|
||||
padding: "2px 6px",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
fontWeight: "500",
|
||||
zIndex: 10,
|
||||
opacity: isDesignMode ? 1 : 0.7,
|
||||
}}
|
||||
>
|
||||
{zone.name || zone.id}
|
||||
</div>
|
||||
|
||||
{/* 드롭존 */}
|
||||
<div className="drop-zone" style={dropZoneStyle}>
|
||||
{zoneChildren.length > 0 ? (
|
||||
zoneChildren.map((child) => (
|
||||
<DynamicComponentRenderer
|
||||
key={child.id}
|
||||
component={child}
|
||||
allComponents={this.props.allComponents}
|
||||
isDesignMode={isDesignMode}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-zone-indicator" style={{ textAlign: "center", opacity: 0.6 }}>
|
||||
{isDesignMode ? "컴포넌트를 드래그하여 추가하세요" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 존의 스타일을 계산합니다.
|
||||
*/
|
||||
protected getZoneStyle(zone: LayoutZone): React.CSSProperties {
|
||||
const style: React.CSSProperties = {};
|
||||
|
||||
if (zone.size) {
|
||||
if (zone.size.width) {
|
||||
style.width = typeof zone.size.width === "number" ? `${zone.size.width}px` : zone.size.width;
|
||||
}
|
||||
if (zone.size.height) {
|
||||
style.height = typeof zone.size.height === "number" ? `${zone.size.height}px` : zone.size.height;
|
||||
}
|
||||
if (zone.size.minWidth) {
|
||||
style.minWidth = typeof zone.size.minWidth === "number" ? `${zone.size.minWidth}px` : zone.size.minWidth;
|
||||
}
|
||||
if (zone.size.minHeight) {
|
||||
style.minHeight = typeof zone.size.minHeight === "number" ? `${zone.size.minHeight}px` : zone.size.minHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 등록 상태 추적
|
||||
*/
|
||||
private static registeredLayouts = new Set<string>();
|
||||
|
||||
/**
|
||||
* 클래스가 정의될 때 자동으로 레지스트리에 등록
|
||||
*/
|
||||
static registerSelf(): void {
|
||||
const definition = this.layoutDefinition;
|
||||
|
||||
if (!definition) {
|
||||
console.error(`❌ ${this.name}: layoutDefinition이 정의되지 않았습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registeredLayouts.has(definition.id)) {
|
||||
console.warn(`⚠️ ${definition.id} 레이아웃이 이미 등록되어 있습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 레지스트리에 등록
|
||||
LayoutRegistry.registerLayout(definition);
|
||||
this.registeredLayouts.add(definition.id);
|
||||
|
||||
console.log(`✅ 자동 등록 완료: ${definition.id} (${definition.name})`);
|
||||
|
||||
// 개발 모드에서 추가 정보 출력
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`📦 ${definition.id}:`, {
|
||||
name: definition.name,
|
||||
category: definition.category,
|
||||
zones: definition.defaultZones?.length || 0,
|
||||
tags: definition.tags?.join(", ") || "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ ${definition.id} 레이아웃 등록 실패:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 등록 해제 (개발 모드에서 Hot Reload용)
|
||||
*/
|
||||
static unregisterSelf(): void {
|
||||
const definition = this.layoutDefinition;
|
||||
|
||||
if (definition && this.registeredLayouts.has(definition.id)) {
|
||||
LayoutRegistry.unregisterLayout(definition.id);
|
||||
this.registeredLayouts.delete(definition.id);
|
||||
console.log(`🗑️ 등록 해제: ${definition.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot Reload 지원 (개발 모드)
|
||||
*/
|
||||
static reloadSelf(): void {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
this.unregisterSelf();
|
||||
this.registerSelf();
|
||||
console.log(`🔄 Hot Reload: ${this.layoutDefinition?.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록된 레이아웃 목록 조회
|
||||
*/
|
||||
static getRegisteredLayouts(): string[] {
|
||||
return Array.from(this.registeredLayouts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 정의 유효성 검사
|
||||
*/
|
||||
static validateDefinition(): { isValid: boolean; errors: string[]; warnings: string[] } {
|
||||
const definition = this.layoutDefinition;
|
||||
|
||||
if (!definition) {
|
||||
return {
|
||||
isValid: false,
|
||||
errors: ["layoutDefinition이 정의되지 않았습니다."],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// 필수 필드 검사
|
||||
if (!definition.id) errors.push("ID가 필요합니다.");
|
||||
if (!definition.name) errors.push("이름이 필요합니다.");
|
||||
if (!definition.component) errors.push("컴포넌트가 필요합니다.");
|
||||
if (!definition.category) errors.push("카테고리가 필요합니다.");
|
||||
|
||||
// 권장사항 검사
|
||||
if (!definition.description || definition.description.length < 10) {
|
||||
warnings.push("설명은 10자 이상 권장됩니다.");
|
||||
}
|
||||
if (!definition.defaultZones || definition.defaultZones.length === 0) {
|
||||
warnings.push("기본 존 정의가 권장됩니다.");
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개발 모드에서 Hot Module Replacement 지원
|
||||
*/
|
||||
if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
|
||||
// HMR API가 있는 경우 등록
|
||||
if ((module as any).hot) {
|
||||
(module as any).hot.accept();
|
||||
|
||||
// 글로벌 Hot Reload 함수 등록
|
||||
(window as any).__reloadLayout__ = (layoutId: string) => {
|
||||
const layouts = AutoRegisteringLayoutRenderer.getRegisteredLayouts();
|
||||
console.log(`🔄 Available layouts for reload:`, layouts);
|
||||
|
||||
// TODO: 특정 레이아웃만 리로드하는 로직 구현
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
/**
|
||||
* card-layout 기본 설정
|
||||
*/
|
||||
export const Card-layoutLayoutConfig = {
|
||||
export const CardLayoutConfig = {
|
||||
defaultConfig: {
|
||||
card-layout: {
|
||||
// TODO: 레이아웃 전용 설정 정의
|
||||
// 예시:
|
||||
// spacing: 16,
|
||||
// orientation: "vertical",
|
||||
// allowResize: true,
|
||||
"card-layout": {
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -51,14 +46,12 @@ export const Card-layoutLayoutConfig = {
|
|||
}
|
||||
],
|
||||
|
||||
// 설정 스키마 (검증용)
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
card-layout: {
|
||||
"card-layout": {
|
||||
type: "object",
|
||||
properties: {
|
||||
// TODO: 설정 스키마 정의
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,26 +3,21 @@ import { LayoutRendererProps } from "../BaseLayoutRenderer";
|
|||
/**
|
||||
* card-layout 설정 타입
|
||||
*/
|
||||
export interface Card-layoutConfig {
|
||||
export interface CardLayoutConfig {
|
||||
// TODO: 레이아웃 전용 설정 타입 정의
|
||||
// 예시:
|
||||
// spacing?: number;
|
||||
// orientation?: "vertical" | "horizontal";
|
||||
// allowResize?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* card-layout Props 타입
|
||||
*/
|
||||
export interface Card-layoutLayoutProps extends LayoutRendererProps {
|
||||
renderer: any; // Card-layoutLayoutRenderer 타입
|
||||
export interface CardLayoutLayoutProps extends LayoutRendererProps {
|
||||
renderer: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* card-layout 존 타입
|
||||
*/
|
||||
export interface Card-layoutZone {
|
||||
export interface CardLayoutZone {
|
||||
id: string;
|
||||
name: string;
|
||||
// TODO: 존별 전용 속성 정의
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
export const HeroSectionLayoutConfig = {
|
||||
defaultConfig: {
|
||||
hero-section: {
|
||||
"hero-section": {
|
||||
// TODO: 레이아웃 전용 설정 정의
|
||||
// 예시:
|
||||
// spacing: 16,
|
||||
|
|
@ -37,7 +37,7 @@ export const HeroSectionLayoutConfig = {
|
|||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
hero-section: {
|
||||
"hero-section": {
|
||||
type: "object",
|
||||
properties: {
|
||||
// TODO: 설정 스키마 정의
|
||||
|
|
|
|||
|
|
@ -24,5 +24,12 @@
|
|||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", ".next"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"components/screen/ScreenDesigner_old.tsx",
|
||||
"components/admin/dashboard/widgets/yard-3d/Yard3DCanvas_NEW.tsx",
|
||||
"components/flow/FlowDataListModal.tsx",
|
||||
"test-scenarios"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
TimestampFields,
|
||||
BaseApiResponse,
|
||||
} from "./v2-core";
|
||||
import { FlowVisibilityConfig } from "./screen-management";
|
||||
|
||||
// ===== 버튼 제어 관련 =====
|
||||
|
||||
|
|
@ -58,23 +59,6 @@ export interface ExtendedButtonTypeConfig {
|
|||
borderColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 단계별 버튼 표시 설정
|
||||
*/
|
||||
export interface FlowVisibilityConfig {
|
||||
enabled: boolean;
|
||||
targetFlowComponentId: string;
|
||||
targetFlowId?: number;
|
||||
targetFlowName?: string;
|
||||
mode: "whitelist" | "blacklist" | "all";
|
||||
visibleSteps?: number[];
|
||||
hiddenSteps?: number[];
|
||||
layoutBehavior: "preserve-position" | "auto-compact";
|
||||
groupId?: string;
|
||||
groupDirection?: "horizontal" | "vertical";
|
||||
groupGap?: number;
|
||||
groupAlign?: "start" | "center" | "end" | "space-between" | "space-around";
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 단순화된 버튼 데이터플로우 설정
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
// ===== 핵심 공통 타입들 =====
|
||||
export * from "./v2-core";
|
||||
import type { WebType, ButtonActionType, CompanyCode } from "./v2-core";
|
||||
import type { ComponentData } from "./screen-management";
|
||||
|
||||
// ===== 시스템별 전용 타입들 =====
|
||||
export * from "./screen-management";
|
||||
|
|
@ -258,86 +260,3 @@ export type SelectedRowData = Record<string, unknown>;
|
|||
*/
|
||||
export type TableData = Record<string, unknown>[];
|
||||
|
||||
// ===== 마이그레이션 도우미 =====
|
||||
|
||||
/**
|
||||
* 기존 screen.ts 타입을 새로운 통합 타입으로 마이그레이션하는 도우미
|
||||
*/
|
||||
export namespace Migration {
|
||||
/**
|
||||
* 기존 screen.ts의 WebType을 새로운 WebType으로 변환
|
||||
*/
|
||||
export const migrateWebType = (oldWebType: string): WebType => {
|
||||
// 기존 타입이 새로운 WebType에 포함되어 있는지 확인
|
||||
if (isWebType(oldWebType)) {
|
||||
return oldWebType as WebType;
|
||||
}
|
||||
|
||||
// 호환되지 않는 타입의 경우 기본값 반환
|
||||
console.warn(`Unknown WebType: ${oldWebType}, defaulting to 'text'`);
|
||||
return "text";
|
||||
};
|
||||
|
||||
/**
|
||||
* 기존 ButtonActionType을 새로운 ButtonActionType으로 변환
|
||||
*/
|
||||
export const migrateButtonActionType = (oldActionType: string): ButtonActionType => {
|
||||
if (isButtonActionType(oldActionType)) {
|
||||
return oldActionType as ButtonActionType;
|
||||
}
|
||||
|
||||
console.warn(`Unknown ButtonActionType: ${oldActionType}, defaulting to 'submit'`);
|
||||
return "submit";
|
||||
};
|
||||
|
||||
/**
|
||||
* Y/N 문자열을 boolean으로 변환 (DB 호환성)
|
||||
*/
|
||||
export const migrateYNToBoolean = (value: string | undefined): boolean => {
|
||||
return value === "Y";
|
||||
};
|
||||
|
||||
/**
|
||||
* boolean을 Y/N 문자열로 변환 (DB 호환성)
|
||||
*/
|
||||
export const migrateBooleanToYN = (value: boolean): string => {
|
||||
return value ? "Y" : "N";
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 타입 검증 도우미 =====
|
||||
|
||||
/**
|
||||
* 런타임에서 타입 안전성을 보장하는 검증 함수들
|
||||
*/
|
||||
export namespace TypeValidation {
|
||||
/**
|
||||
* 객체가 BaseComponent 인터페이스를 만족하는지 검증
|
||||
*/
|
||||
export const validateBaseComponent = (obj: unknown): obj is BaseComponent => {
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
|
||||
const component = obj as Record<string, unknown>;
|
||||
return (
|
||||
typeof component.id === "string" &&
|
||||
typeof component.type === "string" &&
|
||||
isComponentType(component.type as string) &&
|
||||
typeof component.position === "object" &&
|
||||
typeof component.size === "object"
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 객체가 WebTypeConfig를 만족하는지 검증
|
||||
*/
|
||||
export const validateWebTypeConfig = (obj: unknown): obj is WebTypeConfig => {
|
||||
return typeof obj === "object" && obj !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 문자열이 유효한 CompanyCode인지 검증
|
||||
*/
|
||||
export const validateCompanyCode = (code: unknown): code is CompanyCode => {
|
||||
return typeof code === "string" && code.length > 0;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,12 +88,18 @@ export interface ExternalDBSourceNodeData {
|
|||
|
||||
// REST API 소스 노드
|
||||
export interface RestAPISourceNodeData {
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
body?: any;
|
||||
responseFields: FieldDefinition[];
|
||||
displayName?: string;
|
||||
authentication?: {
|
||||
type: "none" | "bearer" | "basic" | "apikey";
|
||||
token?: string;
|
||||
};
|
||||
timeout?: number;
|
||||
responseMapping?: string;
|
||||
}
|
||||
|
||||
// 조건 연산자 타입
|
||||
|
|
@ -510,21 +516,6 @@ export interface UpsertActionNodeData {
|
|||
};
|
||||
}
|
||||
|
||||
// REST API 소스 노드
|
||||
export interface RestAPISourceNodeData {
|
||||
url: string;
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
authentication?: {
|
||||
type: "none" | "bearer" | "basic" | "apikey";
|
||||
token?: string;
|
||||
};
|
||||
timeout?: number;
|
||||
responseMapping?: string; // JSON 경로 (예: "data.items")
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
// 주석 노드
|
||||
export interface CommentNodeData {
|
||||
content: string;
|
||||
|
|
@ -788,7 +779,7 @@ export type EdgeType =
|
|||
| "conditionalTrue" // 조건 TRUE
|
||||
| "conditionalFalse"; // 조건 FALSE
|
||||
|
||||
export interface FlowEdge extends ReactFlowEdge {
|
||||
export interface FlowEdge extends Omit<ReactFlowEdge, 'type' | 'data'> {
|
||||
type?: EdgeType;
|
||||
data?: {
|
||||
dataType?: string;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export interface V2ColumnInfo {
|
|||
* 백엔드 호환용 컬럼 타입 정보 (기존 ColumnTypeInfo)
|
||||
*/
|
||||
export interface ColumnTypeInfo {
|
||||
tableName?: string;
|
||||
columnName: string;
|
||||
displayName: string;
|
||||
dataType: string;
|
||||
|
|
@ -367,6 +368,9 @@ export const WEB_TYPE_OPTIONS = [
|
|||
{ value: "email", label: "email", description: "이메일 입력" },
|
||||
{ value: "tel", label: "tel", description: "전화번호 입력" },
|
||||
{ value: "url", label: "url", description: "URL 입력" },
|
||||
{ value: "checkbox-group", label: "checkbox-group", description: "체크박스 그룹" },
|
||||
{ value: "radio-horizontal", label: "radio-horizontal", description: "가로 라디오" },
|
||||
{ value: "radio-vertical", label: "radio-vertical", description: "세로 라디오" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -23,21 +23,40 @@ export type WebType =
|
|||
// 숫자 입력
|
||||
| "number"
|
||||
| "decimal"
|
||||
| "percentage"
|
||||
| "currency"
|
||||
// 날짜/시간 입력
|
||||
| "date"
|
||||
| "datetime"
|
||||
| "month"
|
||||
| "year"
|
||||
| "time"
|
||||
| "daterange"
|
||||
// 선택 입력
|
||||
| "select"
|
||||
| "dropdown"
|
||||
| "radio"
|
||||
| "radio-horizontal"
|
||||
| "radio-vertical"
|
||||
| "checkbox"
|
||||
| "checkbox-group"
|
||||
| "boolean"
|
||||
| "multiselect"
|
||||
| "autocomplete"
|
||||
// 특수 입력
|
||||
| "code" // 공통코드 참조
|
||||
| "code-radio" // 공통코드 라디오
|
||||
| "code-autocomplete" // 공통코드 자동완성
|
||||
| "entity" // 엔티티 참조
|
||||
| "file" // 파일 업로드
|
||||
| "image" // 이미지 표시
|
||||
| "password" // 비밀번호
|
||||
| "button" // 버튼 컴포넌트
|
||||
| "category" // 카테고리
|
||||
| "component" // 컴포넌트 참조
|
||||
| "form" // 폼
|
||||
| "table" // 테이블
|
||||
| "array" // 배열
|
||||
// 레이아웃/컨테이너 타입
|
||||
| "container" // 컨테이너
|
||||
| "group" // 그룹
|
||||
|
|
|
|||
Loading…
Reference in New Issue