feat: 수주등록 모달 및 범용 컴포넌트 개발

- 범용 컴포넌트 3종 개발 및 레지스트리 등록:
  * AutocompleteSearchInput: 자동완성 검색 입력 컴포넌트
  * EntitySearchInput: 엔티티 검색 모달 컴포넌트
  * ModalRepeaterTable: 모달 기반 반복 테이블 컴포넌트

- 수주등록 전용 컴포넌트:
  * OrderCustomerSearch: 거래처 검색 (AutocompleteSearchInput 래퍼)
  * OrderItemRepeaterTable: 품목 관리 (ModalRepeaterTable 래퍼)
  * OrderRegistrationModal: 수주등록 메인 모달

- 백엔드 API:
  * Entity 검색 API (멀티테넌시 지원)
  * 수주 등록 API (자동 채번)

- 화면 편집기 통합:
  * 컴포넌트 레지스트리에 등록
  * ConfigPanel을 통한 설정 기능
  * 드래그앤드롭으로 배치 가능

- 개발 문서:
  * 수주등록_화면_개발_계획서.md (상세 설계 문서)
This commit is contained in:
kjs
2025-11-14 14:43:53 +09:00
parent 075869c89c
commit 64e6fd1920
46 changed files with 6086 additions and 8 deletions

View File

@@ -70,6 +70,9 @@ import { toast } from "sonner";
import { MenuAssignmentModal } from "./MenuAssignmentModal";
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
import { initializeComponents } from "@/lib/registry/components";
import { AutocompleteSearchInputRenderer } from "@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputRenderer";
import { EntitySearchInputRenderer } from "@/lib/registry/components/entity-search-input/EntitySearchInputRenderer";
import { ModalRepeaterTableRenderer } from "@/lib/registry/components/modal-repeater-table/ModalRepeaterTableRenderer";
import { ScreenFileAPI } from "@/lib/api/screenFile";
import { safeMigrateLayout, needsMigration } from "@/lib/utils/widthToColumnSpan";
@@ -4935,6 +4938,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
)}
</div>
</TableOptionsProvider>
{/* 숨겨진 컴포넌트 렌더러들 (레지스트리 등록용) */}
<div style={{ display: "none" }}>
<AutocompleteSearchInputRenderer />
<EntitySearchInputRenderer />
<ModalRepeaterTableRenderer />
</div>
</ScreenPreviewProvider>
);
}

View File

@@ -63,8 +63,9 @@ export function ComponentsPanel({
),
action: allComponents.filter((c) => c.category === ComponentCategory.ACTION),
display: allComponents.filter((c) => c.category === ComponentCategory.DISPLAY),
data: allComponents.filter((c) => c.category === ComponentCategory.DATA), // 🆕 데이터 카테고리 추가
layout: allComponents.filter((c) => c.category === ComponentCategory.LAYOUT),
utility: allComponents.filter((c) => c.category === ComponentCategory.UTILITY), // 🆕 유틸리티 카테고리 추가
utility: allComponents.filter((c) => c.category === ComponentCategory.UTILITY),
};
}, [allComponents]);
@@ -92,6 +93,8 @@ export function ComponentsPanel({
return <Palette className="h-6 w-6" />;
case "action":
return <Zap className="h-6 w-6" />;
case "data":
return <Database className="h-6 w-6" />;
case "layout":
return <Layers className="h-6 w-6" />;
case "utility":
@@ -185,7 +188,7 @@ export function ComponentsPanel({
{/* 카테고리 탭 */}
<Tabs defaultValue="input" className="flex min-h-0 flex-1 flex-col">
<TabsList className="mb-3 grid h-8 w-full flex-shrink-0 grid-cols-6 gap-1 p-1">
<TabsList className="mb-3 grid h-8 w-full flex-shrink-0 grid-cols-7 gap-1 p-1">
<TabsTrigger
value="tables"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
@@ -198,6 +201,14 @@ export function ComponentsPanel({
<Edit3 className="h-3 w-3" />
<span className="hidden"></span>
</TabsTrigger>
<TabsTrigger
value="data"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
title="데이터"
>
<Grid className="h-3 w-3" />
<span className="hidden"></span>
</TabsTrigger>
<TabsTrigger
value="action"
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
@@ -260,6 +271,13 @@ export function ComponentsPanel({
: renderEmptyState()}
</TabsContent>
{/* 데이터 컴포넌트 */}
<TabsContent value="data" className="mt-0 flex-1 space-y-2 overflow-y-auto">
{getFilteredComponents("data").length > 0
? getFilteredComponents("data").map(renderComponentCard)
: renderEmptyState()}
</TabsContent>
{/* 액션 컴포넌트 */}
<TabsContent value="action" className="mt-0 flex-1 space-y-2 overflow-y-auto">
{getFilteredComponents("action").length > 0

View File

@@ -36,6 +36,9 @@ import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
// 동적 컴포넌트 설정 패널
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
interface DetailSettingsPanelProps {
selectedComponent?: ComponentData;
onUpdateProperty: (componentId: string, path: string, value: any) => void;
@@ -859,6 +862,55 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(selectedComponent.id, path, value);
};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
if (componentId) {
const definition = ComponentRegistry.getComponent(componentId);
if (definition?.configPanel) {
const ConfigPanelComponent = definition.configPanel;
const currentConfig = selectedComponent.componentConfig || {};
console.log("✅ ConfigPanel 표시:", {
componentId,
definitionName: definition.name,
hasConfigPanel: !!definition.configPanel,
currentConfig,
});
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
const config = currentConfig.config || definition.defaultConfig || {};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
</div>
);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
} else {
console.warn("⚠️ ConfigPanel 없음:", {
componentId,
definitionName: definition?.name,
hasDefinition: !!definition,
});
}
}
// 기존 하드코딩된 설정 패널들 (레거시)
switch (componentType) {
case "button":
case "button-primary":
@@ -904,8 +956,10 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500"> "{componentType}" .</p>
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500">
"{componentId || componentType}" .
</p>
</div>
);
}

View File

@@ -48,6 +48,9 @@ import { ButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel";
import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel";
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel";
import { ChartConfigPanel } from "../config-panels/ChartConfigPanel";
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
@@ -269,6 +272,55 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onUpdateProperty(selectedComponent.id, path, value);
};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
if (componentId) {
const definition = ComponentRegistry.getComponent(componentId);
if (definition?.configPanel) {
const ConfigPanelComponent = definition.configPanel;
const currentConfig = selectedComponent.componentConfig || {};
console.log("✅ ConfigPanel 표시:", {
componentId,
definitionName: definition.name,
hasConfigPanel: !!definition.configPanel,
currentConfig,
});
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
const config = currentConfig.config || definition.defaultConfig || {};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
</div>
);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
} else {
console.warn("⚠️ ConfigPanel 없음:", {
componentId,
definitionName: definition?.name,
hasDefinition: !!definition,
});
}
}
// 기존 하드코딩된 설정 패널들 (레거시)
switch (componentType) {
case "button":
case "button-primary":
@@ -312,7 +364,16 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
default:
return null;
// ConfigPanel이 없는 경우 경고 표시
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-muted-foreground" />
<h3 className="mb-2 text-base font-medium"> </h3>
<p className="text-sm text-muted-foreground">
"{componentId || componentType}" .
</p>
</div>
);
}
};