Compare commits
No commits in common. "cddce40f35c8fd6f6c25cdbfed6fa0866cb6b062" and "84f0a66155601f93ad9073eb12126d25c0006725" have entirely different histories.
cddce40f35
...
84f0a66155
|
|
@ -70,6 +70,9 @@ import { toast } from "sonner";
|
||||||
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
||||||
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
|
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
|
||||||
import { initializeComponents } from "@/lib/registry/components";
|
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 { ScreenFileAPI } from "@/lib/api/screenFile";
|
||||||
import { safeMigrateLayout, needsMigration } from "@/lib/utils/widthToColumnSpan";
|
import { safeMigrateLayout, needsMigration } from "@/lib/utils/widthToColumnSpan";
|
||||||
|
|
||||||
|
|
@ -4964,6 +4967,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableOptionsProvider>
|
</TableOptionsProvider>
|
||||||
|
{/* 숨겨진 컴포넌트 렌더러들 (레지스트리 등록용) */}
|
||||||
|
<div style={{ display: "none" }}>
|
||||||
|
<AutocompleteSearchInputRenderer />
|
||||||
|
<EntitySearchInputRenderer />
|
||||||
|
<ModalRepeaterTableRenderer />
|
||||||
|
</div>
|
||||||
</ScreenPreviewProvider>
|
</ScreenPreviewProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,4 +77,3 @@ export const numberingRuleTemplate = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,19 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
import { ComponentRegistry } from "../../ComponentRegistry";
|
||||||
import { AutocompleteSearchInputDefinition } from "./index";
|
import { AutocompleteSearchInputDefinition } from "./index";
|
||||||
import { AutocompleteSearchInputComponent } from "./AutocompleteSearchInputComponent";
|
|
||||||
|
|
||||||
/**
|
export function AutocompleteSearchInputRenderer() {
|
||||||
* AutocompleteSearchInput 렌더러
|
useEffect(() => {
|
||||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
ComponentRegistry.registerComponent(AutocompleteSearchInputDefinition);
|
||||||
*/
|
console.log("✅ AutocompleteSearchInput 컴포넌트 등록 완료");
|
||||||
export class AutocompleteSearchInputRenderer extends AutoRegisteringComponentRenderer {
|
|
||||||
static componentDefinition = AutocompleteSearchInputDefinition;
|
|
||||||
|
|
||||||
render(): React.ReactElement {
|
return () => {
|
||||||
return <AutocompleteSearchInputComponent {...this.props} renderer={this} />;
|
// 컴포넌트 언마운트 시 해제하지 않음 (싱글톤 패턴)
|
||||||
}
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
return null;
|
||||||
* 값 변경 처리
|
|
||||||
*/
|
|
||||||
protected handleValueChange = (value: any) => {
|
|
||||||
this.updateComponent({ value });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 자동 등록 실행
|
|
||||||
AutocompleteSearchInputRenderer.registerSelf();
|
|
||||||
|
|
||||||
// Hot Reload 지원 (개발 모드)
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
AutocompleteSearchInputRenderer.enableHotReload();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,19 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
import { ComponentRegistry } from "../../ComponentRegistry";
|
||||||
import { EntitySearchInputDefinition } from "./index";
|
import { EntitySearchInputDefinition } from "./index";
|
||||||
import { EntitySearchInputComponent } from "./EntitySearchInputComponent";
|
|
||||||
|
|
||||||
/**
|
export function EntitySearchInputRenderer() {
|
||||||
* EntitySearchInput 렌더러
|
useEffect(() => {
|
||||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
ComponentRegistry.registerComponent(EntitySearchInputDefinition);
|
||||||
*/
|
console.log("✅ EntitySearchInput 컴포넌트 등록 완료");
|
||||||
export class EntitySearchInputRenderer extends AutoRegisteringComponentRenderer {
|
|
||||||
static componentDefinition = EntitySearchInputDefinition;
|
|
||||||
|
|
||||||
render(): React.ReactElement {
|
return () => {
|
||||||
return <EntitySearchInputComponent {...this.props} renderer={this} />;
|
// 컴포넌트 언마운트 시 해제하지 않음 (싱글톤 패턴)
|
||||||
}
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
return null;
|
||||||
* 값 변경 처리
|
|
||||||
*/
|
|
||||||
protected handleValueChange = (value: any) => {
|
|
||||||
this.updateComponent({ value });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 자동 등록 실행
|
|
||||||
EntitySearchInputRenderer.registerSelf();
|
|
||||||
|
|
||||||
// Hot Reload 지원 (개발 모드)
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
EntitySearchInputRenderer.enableHotReload();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,9 @@ import "./table-search-widget"; // 🆕 테이블 검색 필터 위젯
|
||||||
import "./customer-item-mapping/CustomerItemMappingRenderer"; // 🆕 거래처별 품목정보
|
import "./customer-item-mapping/CustomerItemMappingRenderer"; // 🆕 거래처별 품목정보
|
||||||
|
|
||||||
// 🆕 수주 등록 관련 컴포넌트들
|
// 🆕 수주 등록 관련 컴포넌트들
|
||||||
import "./autocomplete-search-input/AutocompleteSearchInputRenderer";
|
import { AutocompleteSearchInputRenderer } from "./autocomplete-search-input/AutocompleteSearchInputRenderer";
|
||||||
import "./entity-search-input/EntitySearchInputRenderer";
|
import { EntitySearchInputRenderer } from "./entity-search-input/EntitySearchInputRenderer";
|
||||||
import "./modal-repeater-table/ModalRepeaterTableRenderer";
|
import { ModalRepeaterTableRenderer } from "./modal-repeater-table/ModalRepeaterTableRenderer";
|
||||||
import "./order-registration-modal/OrderRegistrationModalRenderer";
|
import "./order-registration-modal/OrderRegistrationModalRenderer";
|
||||||
|
|
||||||
// 🆕 조건부 컨테이너 컴포넌트
|
// 🆕 조건부 컨테이너 컴포넌트
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,19 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
import { ComponentRegistry } from "../../ComponentRegistry";
|
||||||
import { ModalRepeaterTableDefinition } from "./index";
|
import { ModalRepeaterTableDefinition } from "./index";
|
||||||
import { ModalRepeaterTableComponent } from "./ModalRepeaterTableComponent";
|
|
||||||
|
|
||||||
/**
|
export function ModalRepeaterTableRenderer() {
|
||||||
* ModalRepeaterTable 렌더러
|
useEffect(() => {
|
||||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
ComponentRegistry.registerComponent(ModalRepeaterTableDefinition);
|
||||||
*/
|
console.log("✅ ModalRepeaterTable 컴포넌트 등록 완료");
|
||||||
export class ModalRepeaterTableRenderer extends AutoRegisteringComponentRenderer {
|
|
||||||
static componentDefinition = ModalRepeaterTableDefinition;
|
|
||||||
|
|
||||||
render(): React.ReactElement {
|
return () => {
|
||||||
return <ModalRepeaterTableComponent {...this.props} renderer={this} />;
|
// 컴포넌트 언마운트 시 해제하지 않음 (싱글톤 패턴)
|
||||||
}
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
return null;
|
||||||
* 값 변경 처리
|
|
||||||
*/
|
|
||||||
protected handleValueChange = (value: any) => {
|
|
||||||
this.updateComponent({ value });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 자동 등록 실행
|
|
||||||
ModalRepeaterTableRenderer.registerSelf();
|
|
||||||
|
|
||||||
// Hot Reload 지원 (개발 모드)
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
ModalRepeaterTableRenderer.enableHotReload();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@
|
||||||
```tsx
|
```tsx
|
||||||
{
|
{
|
||||||
type: "button-primary",
|
type: "button-primary",
|
||||||
config: {
|
config: {
|
||||||
text: "저장",
|
text: "저장",
|
||||||
action: {
|
action: {
|
||||||
type: "save",
|
type: "save",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,7 +8,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Plus, X } from "lucide-react";
|
import { Plus, X } from "lucide-react";
|
||||||
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, FieldGroup } from "./types";
|
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition } from "./types";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Check, ChevronsUpDown } from "lucide-react";
|
import { Check, ChevronsUpDown } from "lucide-react";
|
||||||
|
|
@ -43,9 +43,6 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
const [displayColumns, setDisplayColumns] = useState<Array<{ name: string; label: string; width?: string }>>(config.displayColumns || []);
|
const [displayColumns, setDisplayColumns] = useState<Array<{ name: string; label: string; width?: string }>>(config.displayColumns || []);
|
||||||
const [fieldPopoverOpen, setFieldPopoverOpen] = useState<Record<number, boolean>>({});
|
const [fieldPopoverOpen, setFieldPopoverOpen] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
// 🆕 필드 그룹 상태
|
|
||||||
const [localFieldGroups, setLocalFieldGroups] = useState<FieldGroup[]>(config.fieldGroups || []);
|
|
||||||
|
|
||||||
// 🆕 원본 테이블 선택 상태
|
// 🆕 원본 테이블 선택 상태
|
||||||
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
|
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
|
||||||
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
|
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
|
||||||
|
|
@ -103,38 +100,6 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
handleFieldsChange(newFields);
|
handleFieldsChange(newFields);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 필드 그룹 관리
|
|
||||||
const handleFieldGroupsChange = (groups: FieldGroup[]) => {
|
|
||||||
setLocalFieldGroups(groups);
|
|
||||||
handleChange("fieldGroups", groups);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addFieldGroup = () => {
|
|
||||||
const newGroup: FieldGroup = {
|
|
||||||
id: `group_${localFieldGroups.length + 1}`,
|
|
||||||
title: `그룹 ${localFieldGroups.length + 1}`,
|
|
||||||
order: localFieldGroups.length,
|
|
||||||
};
|
|
||||||
handleFieldGroupsChange([...localFieldGroups, newGroup]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFieldGroup = (groupId: string) => {
|
|
||||||
// 그룹 삭제 시 해당 그룹에 속한 필드들의 groupId도 제거
|
|
||||||
const updatedFields = localFields.map(field =>
|
|
||||||
field.groupId === groupId ? { ...field, groupId: undefined } : field
|
|
||||||
);
|
|
||||||
setLocalFields(updatedFields);
|
|
||||||
handleChange("additionalFields", updatedFields);
|
|
||||||
handleFieldGroupsChange(localFieldGroups.filter(g => g.id !== groupId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => {
|
|
||||||
const newGroups = localFieldGroups.map(g =>
|
|
||||||
g.id === groupId ? { ...g, ...updates } : g
|
|
||||||
);
|
|
||||||
handleFieldGroupsChange(newGroups);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 표시 컬럼 추가
|
// 표시 컬럼 추가
|
||||||
const addDisplayColumn = (columnName: string, columnLabel: string) => {
|
const addDisplayColumn = (columnName: string, columnLabel: string) => {
|
||||||
if (!displayColumns.some(col => col.name === columnName)) {
|
if (!displayColumns.some(col => col.name === columnName)) {
|
||||||
|
|
@ -496,32 +461,6 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🆕 필드 그룹 선택 */}
|
|
||||||
{localFieldGroups.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-[10px] sm:text-xs">필드 그룹 (선택사항)</Label>
|
|
||||||
<Select
|
|
||||||
value={field.groupId || "none"}
|
|
||||||
onValueChange={(value) => updateField(index, { groupId: value === "none" ? undefined : value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-6 text-[10px] sm:h-7 sm:text-xs">
|
|
||||||
<SelectValue placeholder="그룹 없음" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none" className="text-xs">그룹 없음</SelectItem>
|
|
||||||
{localFieldGroups.map((group) => (
|
|
||||||
<SelectItem key={group.id} value={group.id} className="text-xs">
|
|
||||||
{group.title} ({group.id})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-[9px] text-gray-500 sm:text-[10px]">
|
|
||||||
같은 그룹 ID를 가진 필드들은 같은 카드에 표시됩니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`required-${index}`}
|
id={`required-${index}`}
|
||||||
|
|
@ -548,121 +487,6 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🆕 필드 그룹 관리 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs font-semibold sm:text-sm">필드 그룹 설정</Label>
|
|
||||||
<p className="text-[10px] text-gray-500 sm:text-xs">
|
|
||||||
추가 입력 필드를 여러 카드로 나눠서 표시 (예: 거래처 정보, 단가 정보)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{localFieldGroups.map((group, index) => (
|
|
||||||
<Card key={group.id} className="border-2">
|
|
||||||
<CardContent className="space-y-2 pt-3 sm:space-y-3 sm:pt-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs font-semibold text-gray-700 sm:text-sm">그룹 {index + 1}</span>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => removeFieldGroup(group.id)}
|
|
||||||
className="h-6 w-6 text-red-500 hover:text-red-700 sm:h-7 sm:w-7"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 그룹 ID */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-[10px] sm:text-xs">그룹 ID</Label>
|
|
||||||
<Input
|
|
||||||
value={group.id}
|
|
||||||
onChange={(e) => updateFieldGroup(group.id, { id: e.target.value })}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
||||||
placeholder="group_customer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 그룹 제목 */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-[10px] sm:text-xs">그룹 제목</Label>
|
|
||||||
<Input
|
|
||||||
value={group.title}
|
|
||||||
onChange={(e) => updateFieldGroup(group.id, { title: e.target.value })}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
||||||
placeholder="거래처 정보"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 그룹 설명 */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-[10px] sm:text-xs">그룹 설명 (선택사항)</Label>
|
|
||||||
<Input
|
|
||||||
value={group.description || ""}
|
|
||||||
onChange={(e) => updateFieldGroup(group.id, { description: e.target.value })}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
||||||
placeholder="거래처 관련 정보를 입력합니다"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 표시 순서 */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-[10px] sm:text-xs">표시 순서</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={group.order || 0}
|
|
||||||
onChange={(e) => updateFieldGroup(group.id, { order: parseInt(e.target.value) || 0 })}
|
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={addFieldGroup}
|
|
||||||
className="h-7 w-full border-dashed text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
<Plus className="mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4" />
|
|
||||||
그룹 추가
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{localFieldGroups.length > 0 && (
|
|
||||||
<p className="text-[10px] text-amber-600 sm:text-xs">
|
|
||||||
💡 추가 입력 필드의 "필드 그룹 ID"에 위에서 정의한 그룹 ID를 입력하세요
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 입력 모드 설정 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs font-semibold sm:text-sm">입력 모드</Label>
|
|
||||||
<Select
|
|
||||||
value={config.inputMode || "inline"}
|
|
||||||
onValueChange={(value) => handleChange("inputMode", value as "inline" | "modal")}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-7 text-xs sm:h-8 sm:text-sm">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="inline" className="text-xs sm:text-sm">
|
|
||||||
항상 표시 (Inline)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="modal" className="text-xs sm:text-sm">
|
|
||||||
추가 버튼으로 표시 (Modal)
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-[10px] text-muted-foreground sm:text-xs">
|
|
||||||
{config.inputMode === "modal"
|
|
||||||
? "추가 버튼 클릭 시 입력창 표시, 완료 후 작은 카드로 표시"
|
|
||||||
: "모든 항목의 입력창을 항상 표시"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 레이아웃 설정 */}
|
{/* 레이아웃 설정 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-semibold sm:text-sm">레이아웃</Label>
|
<Label className="text-xs font-semibold sm:text-sm">레이아웃</Label>
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ export interface AdditionalFieldDefinition {
|
||||||
options?: Array<{ label: string; value: string }>;
|
options?: Array<{ label: string; value: string }>;
|
||||||
/** 필드 너비 (px 또는 %) */
|
/** 필드 너비 (px 또는 %) */
|
||||||
width?: string;
|
width?: string;
|
||||||
/** 🆕 필드 그룹 ID (같은 그룹ID를 가진 필드들은 같은 카드에 표시) */
|
|
||||||
groupId?: string;
|
|
||||||
/** 검증 규칙 */
|
/** 검증 규칙 */
|
||||||
validation?: {
|
validation?: {
|
||||||
min?: number;
|
min?: number;
|
||||||
|
|
@ -38,20 +36,6 @@ export interface AdditionalFieldDefinition {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 필드 그룹 정의
|
|
||||||
*/
|
|
||||||
export interface FieldGroup {
|
|
||||||
/** 그룹 ID */
|
|
||||||
id: string;
|
|
||||||
/** 그룹 제목 */
|
|
||||||
title: string;
|
|
||||||
/** 그룹 설명 (선택사항) */
|
|
||||||
description?: string;
|
|
||||||
/** 그룹 표시 순서 */
|
|
||||||
order?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SelectedItemsDetailInput 컴포넌트 설정 타입
|
* SelectedItemsDetailInput 컴포넌트 설정 타입
|
||||||
*/
|
*/
|
||||||
|
|
@ -80,12 +64,6 @@ export interface SelectedItemsDetailInputConfig extends ComponentConfig {
|
||||||
*/
|
*/
|
||||||
additionalFields?: AdditionalFieldDefinition[];
|
additionalFields?: AdditionalFieldDefinition[];
|
||||||
|
|
||||||
/**
|
|
||||||
* 🆕 필드 그룹 정의
|
|
||||||
* 추가 입력 필드를 여러 카드로 나눠서 표시
|
|
||||||
*/
|
|
||||||
fieldGroups?: FieldGroup[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 저장 대상 테이블
|
* 저장 대상 테이블
|
||||||
*/
|
*/
|
||||||
|
|
@ -108,13 +86,6 @@ export interface SelectedItemsDetailInputConfig extends ComponentConfig {
|
||||||
*/
|
*/
|
||||||
allowRemove?: boolean;
|
allowRemove?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* 🆕 입력 모드
|
|
||||||
* - inline: 항상 입력창 표시 (기본)
|
|
||||||
* - modal: 추가 버튼 클릭 시 입력창 표시, 완료 후 작은 카드로 표시
|
|
||||||
*/
|
|
||||||
inputMode?: "inline" | "modal";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 빈 상태 메시지
|
* 빈 상태 메시지
|
||||||
*/
|
*/
|
||||||
|
|
@ -125,30 +96,6 @@ export interface SelectedItemsDetailInputConfig extends ComponentConfig {
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 🆕 그룹별 입력 항목 (예: 그룹1의 한 줄)
|
|
||||||
*/
|
|
||||||
export interface GroupEntry {
|
|
||||||
/** 입력 항목 고유 ID */
|
|
||||||
id: string;
|
|
||||||
/** 입력된 필드 데이터 */
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 🆕 품목 + 그룹별 여러 입력 항목
|
|
||||||
* 각 필드 그룹마다 독립적으로 여러 개의 입력을 추가할 수 있음
|
|
||||||
* 예: { "group1": [entry1, entry2], "group2": [entry1, entry2, entry3] }
|
|
||||||
*/
|
|
||||||
export interface ItemData {
|
|
||||||
/** 품목 고유 ID */
|
|
||||||
id: string;
|
|
||||||
/** 원본 데이터 (품목 정보) */
|
|
||||||
originalData: Record<string, any>;
|
|
||||||
/** 필드 그룹별 입력 항목들 { groupId: [entry1, entry2, ...] } */
|
|
||||||
fieldGroups: Record<string, GroupEntry[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SelectedItemsDetailInput 컴포넌트 Props 타입
|
* SelectedItemsDetailInput 컴포넌트 Props 타입
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -198,25 +198,6 @@ export class ButtonActionExecutor {
|
||||||
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
const { formData, originalData, tableName, screenId } = context;
|
const { formData, originalData, tableName, screenId } = context;
|
||||||
|
|
||||||
console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId });
|
|
||||||
|
|
||||||
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
|
|
||||||
window.dispatchEvent(new CustomEvent("beforeFormSave"));
|
|
||||||
|
|
||||||
// 약간의 대기 시간을 주어 이벤트 핸들러가 formData를 업데이트할 수 있도록 함
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
// 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조)
|
|
||||||
const selectedItemsKeys = Object.keys(context.formData).filter(key => {
|
|
||||||
const value = context.formData[key];
|
|
||||||
return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedItemsKeys.length > 0) {
|
|
||||||
console.log("🔄 [handleSave] SelectedItemsDetailInput 배치 저장 감지:", selectedItemsKeys);
|
|
||||||
return await this.handleBatchSave(config, context, selectedItemsKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 폼 유효성 검사
|
// 폼 유효성 검사
|
||||||
if (config.validateForm) {
|
if (config.validateForm) {
|
||||||
const validation = this.validateFormData(formData);
|
const validation = this.validateFormData(formData);
|
||||||
|
|
@ -465,128 +446,6 @@ export class ButtonActionExecutor {
|
||||||
return await this.handleSave(config, context);
|
return await this.handleSave(config, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 🆕 배치 저장 액션 처리 (SelectedItemsDetailInput용 - 새로운 데이터 구조)
|
|
||||||
* ItemData[] → 각 품목의 details 배열을 개별 레코드로 저장
|
|
||||||
*/
|
|
||||||
private static async handleBatchSave(
|
|
||||||
config: ButtonActionConfig,
|
|
||||||
context: ButtonActionContext,
|
|
||||||
selectedItemsKeys: string[]
|
|
||||||
): Promise<boolean> {
|
|
||||||
const { formData, tableName, screenId } = context;
|
|
||||||
|
|
||||||
if (!tableName || !screenId) {
|
|
||||||
toast.error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let successCount = 0;
|
|
||||||
let failCount = 0;
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
// 각 SelectedItemsDetailInput 컴포넌트의 데이터 처리
|
|
||||||
for (const key of selectedItemsKeys) {
|
|
||||||
// 🆕 새로운 데이터 구조: ItemData[] with fieldGroups
|
|
||||||
const items = formData[key] as Array<{
|
|
||||||
id: string;
|
|
||||||
originalData: any;
|
|
||||||
fieldGroups: Record<string, Array<{ id: string; [key: string]: any }>>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
console.log(`📦 [handleBatchSave] ${key} 처리 중 (${items.length}개 품목)`);
|
|
||||||
|
|
||||||
// 각 품목의 모든 그룹의 모든 항목을 개별 저장
|
|
||||||
for (const item of items) {
|
|
||||||
const allGroupEntries = Object.values(item.fieldGroups).flat();
|
|
||||||
console.log(`🔍 [handleBatchSave] 품목 처리: ${item.id} (${allGroupEntries.length}개 입력 항목)`);
|
|
||||||
|
|
||||||
// 모든 그룹의 모든 항목을 개별 레코드로 저장
|
|
||||||
for (const entry of allGroupEntries) {
|
|
||||||
try {
|
|
||||||
// 원본 데이터 + 입력 데이터 병합
|
|
||||||
const mergedData = {
|
|
||||||
...item.originalData,
|
|
||||||
...entry,
|
|
||||||
};
|
|
||||||
|
|
||||||
// id 필드 제거 (entry.id는 임시 ID이므로)
|
|
||||||
delete mergedData.id;
|
|
||||||
|
|
||||||
// 사용자 정보 추가
|
|
||||||
if (!context.userId) {
|
|
||||||
throw new Error("사용자 정보를 불러올 수 없습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const writerValue = context.userId;
|
|
||||||
const companyCodeValue = context.companyCode || "";
|
|
||||||
|
|
||||||
const dataWithUserInfo = {
|
|
||||||
...mergedData,
|
|
||||||
writer: mergedData.writer || writerValue,
|
|
||||||
created_by: writerValue,
|
|
||||||
updated_by: writerValue,
|
|
||||||
company_code: mergedData.company_code || companyCodeValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`💾 [handleBatchSave] 입력 항목 저장:`, {
|
|
||||||
itemId: item.id,
|
|
||||||
entryId: entry.id,
|
|
||||||
data: dataWithUserInfo
|
|
||||||
});
|
|
||||||
|
|
||||||
// INSERT 실행
|
|
||||||
const { DynamicFormApi } = await import("@/lib/api/dynamicForm");
|
|
||||||
const saveResult = await DynamicFormApi.saveFormData({
|
|
||||||
screenId,
|
|
||||||
tableName,
|
|
||||||
data: dataWithUserInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (saveResult.success) {
|
|
||||||
successCount++;
|
|
||||||
console.log(`✅ [handleBatchSave] 입력 항목 저장 성공: ${item.id} > ${entry.id}`);
|
|
||||||
} else {
|
|
||||||
failCount++;
|
|
||||||
errors.push(`품목 ${item.id} > 항목 ${entry.id}: ${saveResult.message}`);
|
|
||||||
console.error(`❌ [handleBatchSave] 입력 항목 저장 실패: ${item.id} > ${entry.id}`, saveResult.message);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
failCount++;
|
|
||||||
errors.push(`품목 ${item.id} > 항목 ${entry.id}: ${error.message}`);
|
|
||||||
console.error(`❌ [handleBatchSave] 입력 항목 저장 오류: ${item.id} > ${entry.id}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 결과 토스트
|
|
||||||
if (failCount === 0) {
|
|
||||||
toast.success(`${successCount}개 항목이 저장되었습니다.`);
|
|
||||||
} else if (successCount === 0) {
|
|
||||||
toast.error(`저장 실패: ${errors.join(", ")}`);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
toast.warning(`${successCount}개 성공, ${failCount}개 실패: ${errors.join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 테이블과 플로우 새로고침
|
|
||||||
context.onRefresh?.();
|
|
||||||
context.onFlowRefresh?.();
|
|
||||||
|
|
||||||
// 저장 성공 후 이벤트 발생
|
|
||||||
window.dispatchEvent(new CustomEvent("closeEditModal"));
|
|
||||||
window.dispatchEvent(new CustomEvent("saveSuccessInModal"));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("배치 저장 오류:", error);
|
|
||||||
toast.error(`저장 오류: ${error.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 삭제 액션 처리
|
* 삭제 액션 처리
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -377,4 +377,3 @@ interface TablePermission {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue