Merge pull request 'feature/screen-management' (#318) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/318
This commit is contained in:
commit
4421ccaa71
|
|
@ -3081,6 +3081,79 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 🆕 행 선택 시에만 활성화 설정 */}
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-foreground">행 선택 활성화 조건</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
테이블 리스트나 분할 패널에서 데이터가 선택되었을 때만 버튼을 활성화합니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>행 선택 시에만 활성화</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
체크하면 테이블에서 행을 선택해야만 버튼이 활성화됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={component.componentConfig?.action?.requireRowSelection || false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdateProperty("componentConfig.action.requireRowSelection", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{component.componentConfig?.action?.requireRowSelection && (
|
||||||
|
<div className="space-y-3 pl-4 border-l-2 border-primary/20">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="row-selection-source">선택 데이터 소스</Label>
|
||||||
|
<Select
|
||||||
|
value={component.componentConfig?.action?.rowSelectionSource || "auto"}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
onUpdateProperty("componentConfig.action.rowSelectionSource", value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="데이터 소스 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="auto">자동 감지 (권장)</SelectItem>
|
||||||
|
<SelectItem value="tableList">테이블 리스트 선택</SelectItem>
|
||||||
|
<SelectItem value="splitPanelLeft">분할 패널 좌측 선택</SelectItem>
|
||||||
|
<SelectItem value="flowWidget">플로우 위젯 선택</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
자동 감지: 테이블, 분할 패널, 플로우 위젯 중 선택된 항목이 있으면 활성화
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>다중 선택 허용</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
여러 행이 선택되어도 활성화 (기본: 1개 이상 선택 시 활성화)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={component.componentConfig?.action?.allowMultiRowSelection ?? true}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdateProperty("componentConfig.action.allowMultiRowSelection", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!(component.componentConfig?.action?.allowMultiRowSelection ?? true) && (
|
||||||
|
<div className="rounded-md bg-yellow-50 p-2 dark:bg-yellow-950/20">
|
||||||
|
<p className="text-xs text-yellow-800 dark:text-yellow-200">
|
||||||
|
정확히 1개의 행만 선택되어야 버튼이 활성화됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 제어 기능 섹션 */}
|
{/* 제어 기능 섹션 */}
|
||||||
<div className="mt-8 border-t border-border pt-6">
|
<div className="mt-8 border-t border-border pt-6">
|
||||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,145 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
return false;
|
return false;
|
||||||
}, [component.componentConfig?.action, formData, vehicleStatus, statusLoading, component.label]);
|
}, [component.componentConfig?.action, formData, vehicleStatus, statusLoading, component.label]);
|
||||||
|
|
||||||
|
// 🆕 modalDataStore에서 선택된 데이터 확인 (분할 패널 등에서 저장됨)
|
||||||
|
const [modalStoreData, setModalStoreData] = useState<Record<string, any[]>>({});
|
||||||
|
|
||||||
|
// modalDataStore 상태 구독 (실시간 업데이트)
|
||||||
|
useEffect(() => {
|
||||||
|
const actionConfig = component.componentConfig?.action;
|
||||||
|
if (!actionConfig?.requireRowSelection) return;
|
||||||
|
|
||||||
|
// 동적 import로 modalDataStore 구독
|
||||||
|
let unsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
|
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
||||||
|
// 초기값 설정
|
||||||
|
setModalStoreData(useModalDataStore.getState().dataRegistry);
|
||||||
|
|
||||||
|
// 상태 변경 구독
|
||||||
|
unsubscribe = useModalDataStore.subscribe((state) => {
|
||||||
|
setModalStoreData(state.dataRegistry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe?.();
|
||||||
|
};
|
||||||
|
}, [component.componentConfig?.action?.requireRowSelection]);
|
||||||
|
|
||||||
|
// 🆕 행 선택 기반 비활성화 조건 계산
|
||||||
|
const isRowSelectionDisabled = useMemo(() => {
|
||||||
|
const actionConfig = component.componentConfig?.action;
|
||||||
|
|
||||||
|
// requireRowSelection이 활성화되어 있지 않으면 비활성화하지 않음
|
||||||
|
if (!actionConfig?.requireRowSelection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowSelectionSource = actionConfig.rowSelectionSource || "auto";
|
||||||
|
const allowMultiRowSelection = actionConfig.allowMultiRowSelection ?? true;
|
||||||
|
|
||||||
|
// 선택된 데이터 확인
|
||||||
|
let hasSelection = false;
|
||||||
|
let selectionCount = 0;
|
||||||
|
let selectionSource = "";
|
||||||
|
|
||||||
|
// 1. 자동 감지 모드 또는 테이블 리스트 모드
|
||||||
|
if (rowSelectionSource === "auto" || rowSelectionSource === "tableList") {
|
||||||
|
// TableList에서 선택된 행 확인 (props로 전달됨)
|
||||||
|
if (selectedRowsData && selectedRowsData.length > 0) {
|
||||||
|
hasSelection = true;
|
||||||
|
selectionCount = selectedRowsData.length;
|
||||||
|
selectionSource = "tableList (selectedRowsData)";
|
||||||
|
}
|
||||||
|
// 또는 selectedRows prop 확인
|
||||||
|
else if (selectedRows && selectedRows.length > 0) {
|
||||||
|
hasSelection = true;
|
||||||
|
selectionCount = selectedRows.length;
|
||||||
|
selectionSource = "tableList (selectedRows)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 분할 패널 좌측 선택 데이터 확인
|
||||||
|
if (rowSelectionSource === "auto" || rowSelectionSource === "splitPanelLeft") {
|
||||||
|
// SplitPanelContext에서 확인
|
||||||
|
if (splitPanelContext?.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0) {
|
||||||
|
if (!hasSelection) {
|
||||||
|
hasSelection = true;
|
||||||
|
selectionCount = 1;
|
||||||
|
selectionSource = "splitPanelLeft (context)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 modalDataStore에서도 확인 (SplitPanelLayoutComponent에서 저장)
|
||||||
|
if (!hasSelection && Object.keys(modalStoreData).length > 0) {
|
||||||
|
// modalDataStore에서 데이터가 있는지 확인
|
||||||
|
for (const [sourceId, items] of Object.entries(modalStoreData)) {
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
hasSelection = true;
|
||||||
|
selectionCount = items.length;
|
||||||
|
selectionSource = `modalDataStore (${sourceId})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 플로우 위젯 선택 데이터 확인
|
||||||
|
if (rowSelectionSource === "auto" || rowSelectionSource === "flowWidget") {
|
||||||
|
// 플로우 위젯 선택 데이터 확인
|
||||||
|
if (!hasSelection && flowSelectedData && flowSelectedData.length > 0) {
|
||||||
|
hasSelection = true;
|
||||||
|
selectionCount = flowSelectedData.length;
|
||||||
|
selectionSource = "flowWidget";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디버깅 로그
|
||||||
|
console.log("🔍 [ButtonPrimary] 행 선택 체크:", component.label, {
|
||||||
|
rowSelectionSource,
|
||||||
|
hasSelection,
|
||||||
|
selectionCount,
|
||||||
|
selectionSource,
|
||||||
|
hasSplitPanelContext: !!splitPanelContext,
|
||||||
|
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||||
|
selectedRowsData: selectedRowsData?.length,
|
||||||
|
selectedRows: selectedRows?.length,
|
||||||
|
flowSelectedData: flowSelectedData?.length,
|
||||||
|
modalStoreDataKeys: Object.keys(modalStoreData),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 선택된 데이터가 없으면 비활성화
|
||||||
|
if (!hasSelection) {
|
||||||
|
console.log("🚫 [ButtonPrimary] 행 선택 필요 → 비활성화:", component.label);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다중 선택 허용하지 않는 경우, 정확히 1개만 선택되어야 함
|
||||||
|
if (!allowMultiRowSelection && selectionCount !== 1) {
|
||||||
|
console.log("🚫 [ButtonPrimary] 정확히 1개 행 선택 필요 → 비활성화:", component.label, {
|
||||||
|
selectionCount,
|
||||||
|
allowMultiRowSelection,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ [ButtonPrimary] 행 선택 조건 충족:", component.label, {
|
||||||
|
selectionCount,
|
||||||
|
selectionSource,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}, [
|
||||||
|
component.componentConfig?.action,
|
||||||
|
component.label,
|
||||||
|
selectedRows,
|
||||||
|
selectedRowsData,
|
||||||
|
splitPanelContext?.selectedLeftData,
|
||||||
|
flowSelectedData,
|
||||||
|
splitPanelContext,
|
||||||
|
modalStoreData,
|
||||||
|
]);
|
||||||
|
|
||||||
// 확인 다이얼로그 상태
|
// 확인 다이얼로그 상태
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const [pendingAction, setPendingAction] = useState<{
|
const [pendingAction, setPendingAction] = useState<{
|
||||||
|
|
@ -832,7 +971,14 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// modalDataStore에서 선택된 데이터 가져오기 (분할 패널 등에서 선택한 데이터)
|
// modalDataStore에서 선택된 데이터 가져오기 (분할 패널 등에서 선택한 데이터)
|
||||||
if ((!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) && effectiveTableName) {
|
// 단, 모달(modal) 액션은 신규 등록이므로 modalDataStore 데이터를 가져오지 않음
|
||||||
|
// (다른 화면에서 선택한 데이터가 남아있을 수 있으므로)
|
||||||
|
const shouldFetchFromModalDataStore =
|
||||||
|
processedConfig.action.type !== "modal" &&
|
||||||
|
(!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) &&
|
||||||
|
effectiveTableName;
|
||||||
|
|
||||||
|
if (shouldFetchFromModalDataStore) {
|
||||||
try {
|
try {
|
||||||
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||||
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||||
|
|
@ -860,12 +1006,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모달 액션인데 선택된 데이터가 있으면 경고 메시지 표시하고 중단
|
// 🔧 모달 액션 시 선택 데이터 경고 제거
|
||||||
// (신규 등록 모달에서 선택된 데이터가 초기값으로 전달되는 것을 방지)
|
// 이전에는 "신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요" 경고를 표시했으나,
|
||||||
if (processedConfig.action.type === "modal" && effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) {
|
// 다른 화면에서 선택한 데이터가 남아있는 경우 오탐이 발생하여 제거함.
|
||||||
toast.warning("신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요.");
|
// 모달 화면 내부에서 필요 시 자체적으로 선택 데이터를 무시하도록 처리하면 됨.
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 수정(edit) 액션 검증
|
// 수정(edit) 액션 검증
|
||||||
if (processedConfig.action.type === "edit") {
|
if (processedConfig.action.type === "edit") {
|
||||||
|
|
@ -1088,17 +1232,26 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화)
|
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화 + 행 선택 필수)
|
||||||
const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || statusLoading;
|
const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading;
|
||||||
|
|
||||||
// 공통 버튼 스타일
|
// 공통 버튼 스타일
|
||||||
|
// 🔧 component.style에서 background/backgroundColor 충돌 방지
|
||||||
|
const userStyle = component.style
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(component.style).filter(
|
||||||
|
([key]) => !["width", "height", "background", "backgroundColor"].includes(key)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
|
||||||
const buttonElementStyle: React.CSSProperties = {
|
const buttonElementStyle: React.CSSProperties = {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: "40px",
|
minHeight: "40px",
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
background: finalDisabled ? "#e5e7eb" : buttonColor,
|
backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor, // 🔧 background → backgroundColor로 변경
|
||||||
color: finalDisabled ? "#9ca3af" : "white",
|
color: finalDisabled ? "#9ca3af" : "white",
|
||||||
// 🔧 크기 설정 적용 (sm/md/lg)
|
// 🔧 크기 설정 적용 (sm/md/lg)
|
||||||
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
||||||
|
|
@ -1114,10 +1267,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
margin: "0",
|
margin: "0",
|
||||||
lineHeight: "1.25",
|
lineHeight: "1.25",
|
||||||
boxShadow: finalDisabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
boxShadow: finalDisabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||||
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height 제외)
|
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height/background 제외)
|
||||||
...(component.style
|
...userStyle,
|
||||||
? Object.fromEntries(Object.entries(component.style).filter(([key]) => key !== "width" && key !== "height"))
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
||||||
|
|
|
||||||
|
|
@ -2035,9 +2035,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
style={{
|
style={{
|
||||||
height: componentConfig.leftPanel?.panelHeaderHeight || 48,
|
height: componentConfig.leftPanel?.panelHeaderHeight || 48,
|
||||||
minHeight: componentConfig.leftPanel?.panelHeaderHeight || 48,
|
minHeight: componentConfig.leftPanel?.panelHeaderHeight || 48,
|
||||||
padding: '0 1rem',
|
padding: "0 1rem",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center'
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
|
|
@ -2526,9 +2526,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
style={{
|
style={{
|
||||||
height: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
height: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
||||||
minHeight: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
minHeight: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
||||||
padding: '0 1rem',
|
padding: "0 1rem",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center'
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,7 @@ import { ItemSelectionModal } from "../modal-repeater-table/ItemSelectionModal";
|
||||||
import { RepeaterColumnConfig, CalculationRule } from "../modal-repeater-table/types";
|
import { RepeaterColumnConfig, CalculationRule } from "../modal-repeater-table/types";
|
||||||
|
|
||||||
// 타입 정의
|
// 타입 정의
|
||||||
import {
|
import { TableSectionConfig, TableColumnConfig, TableJoinCondition, FormDataState } from "./types";
|
||||||
TableSectionConfig,
|
|
||||||
TableColumnConfig,
|
|
||||||
TableJoinCondition,
|
|
||||||
FormDataState,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
interface TableSectionRendererProps {
|
interface TableSectionRendererProps {
|
||||||
sectionId: string;
|
sectionId: string;
|
||||||
|
|
@ -81,11 +76,13 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig {
|
||||||
// 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우)
|
// 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우)
|
||||||
externalLookup: cond.externalLookup,
|
externalLookup: cond.externalLookup,
|
||||||
// 값 변환 설정 전달 (레거시 호환)
|
// 값 변환 설정 전달 (레거시 호환)
|
||||||
transform: cond.transform?.enabled ? {
|
transform: cond.transform?.enabled
|
||||||
|
? {
|
||||||
tableName: cond.transform.tableName,
|
tableName: cond.transform.tableName,
|
||||||
matchColumn: cond.transform.matchColumn,
|
matchColumn: cond.transform.matchColumn,
|
||||||
resultColumn: cond.transform.resultColumn,
|
resultColumn: cond.transform.resultColumn,
|
||||||
} : undefined,
|
}
|
||||||
|
: undefined,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
// 조회 유형 정보 추가
|
// 조회 유형 정보 추가
|
||||||
|
|
@ -122,7 +119,11 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig {
|
||||||
/**
|
/**
|
||||||
* TableCalculationRule을 CalculationRule로 변환
|
* TableCalculationRule을 CalculationRule로 변환
|
||||||
*/
|
*/
|
||||||
function convertToCalculationRule(calc: { resultField: string; formula: string; dependencies: string[] }): CalculationRule {
|
function convertToCalculationRule(calc: {
|
||||||
|
resultField: string;
|
||||||
|
formula: string;
|
||||||
|
dependencies: string[];
|
||||||
|
}): CalculationRule {
|
||||||
return {
|
return {
|
||||||
result: calc.resultField,
|
result: calc.resultField,
|
||||||
formula: calc.formula,
|
formula: calc.formula,
|
||||||
|
|
@ -136,7 +137,7 @@ function convertToCalculationRule(calc: { resultField: string; formula: string;
|
||||||
*/
|
*/
|
||||||
async function transformValue(
|
async function transformValue(
|
||||||
value: any,
|
value: any,
|
||||||
transform: { tableName: string; matchColumn: string; resultColumn: string }
|
transform: { tableName: string; matchColumn: string; resultColumn: string },
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (!value || !transform.tableName || !transform.matchColumn || !transform.resultColumn) {
|
if (!value || !transform.tableName || !transform.matchColumn || !transform.resultColumn) {
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -144,19 +145,16 @@ async function transformValue(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 정확히 일치하는 검색
|
// 정확히 일치하는 검색
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${transform.tableName}/data`, {
|
||||||
`/table-management/tables/${transform.tableName}/data`,
|
|
||||||
{
|
|
||||||
search: {
|
search: {
|
||||||
[transform.matchColumn]: {
|
[transform.matchColumn]: {
|
||||||
value: value,
|
value: value,
|
||||||
operator: "equals"
|
operator: "equals",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
size: 1,
|
size: 1,
|
||||||
page: 1
|
page: 1,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||||
const transformedValue = response.data.data.data[0][transform.resultColumn];
|
const transformedValue = response.data.data.data[0][transform.resultColumn];
|
||||||
|
|
@ -186,7 +184,7 @@ async function fetchExternalLookupValue(
|
||||||
},
|
},
|
||||||
rowData: any,
|
rowData: any,
|
||||||
sourceData: any,
|
sourceData: any,
|
||||||
formData: FormDataState
|
formData: FormDataState,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// 1. 비교 값 가져오기
|
// 1. 비교 값 가져오기
|
||||||
let matchValue: any;
|
let matchValue: any;
|
||||||
|
|
@ -199,31 +197,32 @@ async function fetchExternalLookupValue(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchValue === undefined || matchValue === null || matchValue === "") {
|
if (matchValue === undefined || matchValue === null || matchValue === "") {
|
||||||
console.warn(`외부 테이블 조회: 비교 값이 없습니다. (${externalLookup.matchSourceType}.${externalLookup.matchSourceField})`);
|
console.warn(
|
||||||
|
`외부 테이블 조회: 비교 값이 없습니다. (${externalLookup.matchSourceType}.${externalLookup.matchSourceField})`,
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 외부 테이블에서 값 조회 (정확히 일치하는 검색)
|
// 2. 외부 테이블에서 값 조회 (정확히 일치하는 검색)
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${externalLookup.tableName}/data`, {
|
||||||
`/table-management/tables/${externalLookup.tableName}/data`,
|
|
||||||
{
|
|
||||||
search: {
|
search: {
|
||||||
[externalLookup.matchColumn]: {
|
[externalLookup.matchColumn]: {
|
||||||
value: matchValue,
|
value: matchValue,
|
||||||
operator: "equals"
|
operator: "equals",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
size: 1,
|
size: 1,
|
||||||
page: 1
|
page: 1,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||||
return response.data.data.data[0][externalLookup.resultColumn];
|
return response.data.data.data[0][externalLookup.resultColumn];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(`외부 테이블 조회: ${externalLookup.tableName}.${externalLookup.matchColumn} = "${matchValue}" 인 행을 찾을 수 없습니다.`);
|
console.warn(
|
||||||
|
`외부 테이블 조회: ${externalLookup.tableName}.${externalLookup.matchColumn} = "${matchValue}" 인 행을 찾을 수 없습니다.`,
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("외부 테이블 조회 오류:", error);
|
console.error("외부 테이블 조회 오류:", error);
|
||||||
|
|
@ -247,7 +246,7 @@ async function fetchExternalValue(
|
||||||
joinConditions: TableJoinCondition[],
|
joinConditions: TableJoinCondition[],
|
||||||
rowData: any,
|
rowData: any,
|
||||||
sourceData: any,
|
sourceData: any,
|
||||||
formData: FormDataState
|
formData: FormDataState,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (joinConditions.length === 0) {
|
if (joinConditions.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -298,15 +297,16 @@ async function fetchExternalValue(
|
||||||
// 정확히 일치하는 검색을 위해 operator: "equals" 사용
|
// 정확히 일치하는 검색을 위해 operator: "equals" 사용
|
||||||
whereConditions[condition.targetColumn] = {
|
whereConditions[condition.targetColumn] = {
|
||||||
value: convertedValue,
|
value: convertedValue,
|
||||||
operator: "equals"
|
operator: "equals",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 호출
|
// API 호출
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, {
|
||||||
`/table-management/tables/${tableName}/data`,
|
search: whereConditions,
|
||||||
{ search: whereConditions, size: 1, page: 1 }
|
size: 1,
|
||||||
);
|
page: 1,
|
||||||
|
});
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||||
return response.data.data.data[0][valueColumn];
|
return response.data.data.data[0][valueColumn];
|
||||||
|
|
@ -389,14 +389,11 @@ export function TableSectionRenderer({
|
||||||
setDynamicOptionsLoading(true);
|
setDynamicOptionsLoading(true);
|
||||||
try {
|
try {
|
||||||
// DISTINCT 값을 가져오기 위한 API 호출
|
// DISTINCT 값을 가져오기 위한 API 호출
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, {
|
||||||
`/table-management/tables/${tableName}/data`,
|
|
||||||
{
|
|
||||||
search: filterCondition ? { _raw: filterCondition } : {},
|
search: filterCondition ? { _raw: filterCondition } : {},
|
||||||
size: 1000,
|
size: 1000,
|
||||||
page: 1,
|
page: 1,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data) {
|
if (response.data.success && response.data.data?.data) {
|
||||||
const rows = response.data.data.data;
|
const rows = response.data.data.data;
|
||||||
|
|
@ -406,7 +403,7 @@ export function TableSectionRenderer({
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const value = row[valueColumn];
|
const value = row[valueColumn];
|
||||||
if (value && !uniqueValues.has(value)) {
|
if (value && !uniqueValues.has(value)) {
|
||||||
const label = labelColumn ? (row[labelColumn] || value) : value;
|
const label = labelColumn ? row[labelColumn] || value : value;
|
||||||
uniqueValues.set(value, label);
|
uniqueValues.set(value, label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -448,7 +445,7 @@ export function TableSectionRenderer({
|
||||||
|
|
||||||
// 동적 Select 옵션이 있는 컬럼 확인
|
// 동적 Select 옵션이 있는 컬럼 확인
|
||||||
const hasDynamicSelectColumns = useMemo(() => {
|
const hasDynamicSelectColumns = useMemo(() => {
|
||||||
return tableConfig.columns?.some(col => col.dynamicSelectOptions?.enabled);
|
return tableConfig.columns?.some((col) => col.dynamicSelectOptions?.enabled);
|
||||||
}, [tableConfig.columns]);
|
}, [tableConfig.columns]);
|
||||||
|
|
||||||
// 소스 테이블 데이터 로드 (동적 Select 옵션용)
|
// 소스 테이블 데이터 로드 (동적 Select 옵션용)
|
||||||
|
|
@ -467,14 +464,11 @@ export function TableSectionRenderer({
|
||||||
filterCondition[conditionalConfig.sourceFilter.filterColumn] = activeConditionTab;
|
filterCondition[conditionalConfig.sourceFilter.filterColumn] = activeConditionTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${tableConfig.source.tableName}/data`, {
|
||||||
`/table-management/tables/${tableConfig.source.tableName}/data`,
|
|
||||||
{
|
|
||||||
search: filterCondition,
|
search: filterCondition,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
page: 1,
|
page: 1,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data) {
|
if (response.data.success && response.data.data?.data) {
|
||||||
setSourceDataCache(response.data.data.data);
|
setSourceDataCache(response.data.data.data);
|
||||||
|
|
@ -510,14 +504,11 @@ export function TableSectionRenderer({
|
||||||
const filterCondition: Record<string, any> = {};
|
const filterCondition: Record<string, any> = {};
|
||||||
filterCondition[conditionalConfig.sourceFilter!.filterColumn] = activeConditionTab;
|
filterCondition[conditionalConfig.sourceFilter!.filterColumn] = activeConditionTab;
|
||||||
|
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${tableConfig.source!.tableName}/data`, {
|
||||||
`/table-management/tables/${tableConfig.source!.tableName}/data`,
|
|
||||||
{
|
|
||||||
search: filterCondition,
|
search: filterCondition,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
page: 1,
|
page: 1,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data?.data) {
|
if (response.data.success && response.data.data?.data) {
|
||||||
setSourceDataCache(response.data.data.data);
|
setSourceDataCache(response.data.data.data);
|
||||||
|
|
@ -534,7 +525,13 @@ export function TableSectionRenderer({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSourceData();
|
loadSourceData();
|
||||||
}, [activeConditionTab, hasDynamicSelectColumns, conditionalConfig?.sourceFilter?.enabled, conditionalConfig?.sourceFilter?.filterColumn, tableConfig.source?.tableName]);
|
}, [
|
||||||
|
activeConditionTab,
|
||||||
|
hasDynamicSelectColumns,
|
||||||
|
conditionalConfig?.sourceFilter?.enabled,
|
||||||
|
conditionalConfig?.sourceFilter?.filterColumn,
|
||||||
|
tableConfig.source?.tableName,
|
||||||
|
]);
|
||||||
|
|
||||||
// 컬럼별 동적 Select 옵션 생성
|
// 컬럼별 동적 Select 옵션 생성
|
||||||
const dynamicSelectOptionsMap = useMemo(() => {
|
const dynamicSelectOptionsMap = useMemo(() => {
|
||||||
|
|
@ -562,7 +559,7 @@ export function TableSectionRenderer({
|
||||||
if (distinct && seenValues.has(stringValue)) continue;
|
if (distinct && seenValues.has(stringValue)) continue;
|
||||||
seenValues.add(stringValue);
|
seenValues.add(stringValue);
|
||||||
|
|
||||||
const label = labelField ? (row[labelField] || stringValue) : stringValue;
|
const label = labelField ? row[labelField] || stringValue : stringValue;
|
||||||
options.push({ value: stringValue, label: String(label) });
|
options.push({ value: stringValue, label: String(label) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,9 +575,7 @@ export function TableSectionRenderer({
|
||||||
let processedData = newData;
|
let processedData = newData;
|
||||||
|
|
||||||
// 날짜 일괄 적용 로직: batchApply가 활성화된 날짜 컬럼 처리
|
// 날짜 일괄 적용 로직: batchApply가 활성화된 날짜 컬럼 처리
|
||||||
const batchApplyColumns = tableConfig.columns.filter(
|
const batchApplyColumns = tableConfig.columns.filter((col) => col.type === "date" && col.batchApply === true);
|
||||||
(col) => col.type === "date" && col.batchApply === true
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const dateCol of batchApplyColumns) {
|
for (const dateCol of batchApplyColumns) {
|
||||||
// 이미 일괄 적용된 컬럼은 건너뜀
|
// 이미 일괄 적용된 컬럼은 건너뜀
|
||||||
|
|
@ -608,20 +603,20 @@ export function TableSectionRenderer({
|
||||||
setTableData(processedData);
|
setTableData(processedData);
|
||||||
onTableDataChange(processedData);
|
onTableDataChange(processedData);
|
||||||
},
|
},
|
||||||
[onTableDataChange, tableConfig.columns, batchAppliedFields]
|
[onTableDataChange, tableConfig.columns, batchAppliedFields],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 행 선택 모드: 드롭다운 값 변경 시 같은 소스 행의 다른 컬럼들 자동 채움
|
// 행 선택 모드: 드롭다운 값 변경 시 같은 소스 행의 다른 컬럼들 자동 채움
|
||||||
const handleDynamicSelectChange = useCallback(
|
const handleDynamicSelectChange = useCallback(
|
||||||
(rowIndex: number, columnField: string, selectedValue: string, conditionValue?: string) => {
|
(rowIndex: number, columnField: string, selectedValue: string, conditionValue?: string) => {
|
||||||
const column = tableConfig.columns?.find(col => col.field === columnField);
|
const column = tableConfig.columns?.find((col) => col.field === columnField);
|
||||||
if (!column?.dynamicSelectOptions?.rowSelectionMode?.enabled) {
|
if (!column?.dynamicSelectOptions?.rowSelectionMode?.enabled) {
|
||||||
// 행 선택 모드가 아니면 일반 값 변경만
|
// 행 선택 모드가 아니면 일반 값 변경만
|
||||||
if (conditionValue && isConditionalMode) {
|
if (conditionValue && isConditionalMode) {
|
||||||
const currentData = conditionalTableData[conditionValue] || [];
|
const currentData = conditionalTableData[conditionValue] || [];
|
||||||
const newData = [...currentData];
|
const newData = [...currentData];
|
||||||
newData[rowIndex] = { ...newData[rowIndex], [columnField]: selectedValue };
|
newData[rowIndex] = { ...newData[rowIndex], [columnField]: selectedValue };
|
||||||
setConditionalTableData(prev => ({ ...prev, [conditionValue]: newData }));
|
setConditionalTableData((prev) => ({ ...prev, [conditionValue]: newData }));
|
||||||
onConditionalTableDataChange?.(conditionValue, newData);
|
onConditionalTableDataChange?.(conditionValue, newData);
|
||||||
} else {
|
} else {
|
||||||
const newData = [...tableData];
|
const newData = [...tableData];
|
||||||
|
|
@ -635,7 +630,7 @@ export function TableSectionRenderer({
|
||||||
const { sourceField } = column.dynamicSelectOptions;
|
const { sourceField } = column.dynamicSelectOptions;
|
||||||
const { autoFillColumns, sourceIdColumn, targetIdField } = column.dynamicSelectOptions.rowSelectionMode;
|
const { autoFillColumns, sourceIdColumn, targetIdField } = column.dynamicSelectOptions.rowSelectionMode;
|
||||||
|
|
||||||
const sourceRow = sourceDataCache.find(row => String(row[sourceField]) === selectedValue);
|
const sourceRow = sourceDataCache.find((row) => String(row[sourceField]) === selectedValue);
|
||||||
|
|
||||||
if (!sourceRow) {
|
if (!sourceRow) {
|
||||||
console.warn(`[TableSectionRenderer] 소스 행을 찾을 수 없음: ${sourceField} = ${selectedValue}`);
|
console.warn(`[TableSectionRenderer] 소스 행을 찾을 수 없음: ${sourceField} = ${selectedValue}`);
|
||||||
|
|
@ -672,7 +667,7 @@ export function TableSectionRenderer({
|
||||||
|
|
||||||
// 데이터 업데이트
|
// 데이터 업데이트
|
||||||
if (conditionValue && isConditionalMode) {
|
if (conditionValue && isConditionalMode) {
|
||||||
setConditionalTableData(prev => ({ ...prev, [conditionValue]: newData }));
|
setConditionalTableData((prev) => ({ ...prev, [conditionValue]: newData }));
|
||||||
onConditionalTableDataChange?.(conditionValue, newData);
|
onConditionalTableDataChange?.(conditionValue, newData);
|
||||||
} else {
|
} else {
|
||||||
handleDataChange(newData);
|
handleDataChange(newData);
|
||||||
|
|
@ -685,14 +680,23 @@ export function TableSectionRenderer({
|
||||||
updatedRow,
|
updatedRow,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[tableConfig.columns, sourceDataCache, tableData, conditionalTableData, isConditionalMode, handleDataChange, onConditionalTableDataChange]
|
[
|
||||||
|
tableConfig.columns,
|
||||||
|
sourceDataCache,
|
||||||
|
tableData,
|
||||||
|
conditionalTableData,
|
||||||
|
isConditionalMode,
|
||||||
|
handleDataChange,
|
||||||
|
onConditionalTableDataChange,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 참조 컬럼 값 조회 함수 (saveToTarget: false인 컬럼에 대해 소스 테이블 조회)
|
// 참조 컬럼 값 조회 함수 (saveToTarget: false인 컬럼에 대해 소스 테이블 조회)
|
||||||
const loadReferenceColumnValues = useCallback(async (data: any[]) => {
|
const loadReferenceColumnValues = useCallback(
|
||||||
|
async (data: any[]) => {
|
||||||
// saveToTarget: false이고 referenceDisplay가 설정된 컬럼 찾기
|
// saveToTarget: false이고 referenceDisplay가 설정된 컬럼 찾기
|
||||||
const referenceColumns = (tableConfig.columns || []).filter(
|
const referenceColumns = (tableConfig.columns || []).filter(
|
||||||
(col) => col.saveConfig?.saveToTarget === false && col.saveConfig?.referenceDisplay
|
(col) => col.saveConfig?.saveToTarget === false && col.saveConfig?.referenceDisplay,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (referenceColumns.length === 0) return;
|
if (referenceColumns.length === 0) return;
|
||||||
|
|
@ -721,14 +725,11 @@ export function TableSectionRenderer({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 소스 테이블에서 참조 ID에 해당하는 데이터 조회
|
// 소스 테이블에서 참조 ID에 해당하는 데이터 조회
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(`/table-management/tables/${sourceTableName}/data`, {
|
||||||
`/table-management/tables/${sourceTableName}/data`,
|
|
||||||
{
|
|
||||||
search: { id: Array.from(referenceIdSet) }, // ID 배열로 조회
|
search: { id: Array.from(referenceIdSet) }, // ID 배열로 조회
|
||||||
size: 1000,
|
size: 1000,
|
||||||
page: 1,
|
page: 1,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.data?.success || !response.data?.data?.data) {
|
if (!response.data?.success || !response.data?.data?.data) {
|
||||||
console.warn("[TableSectionRenderer] 참조 데이터 조회 실패");
|
console.warn("[TableSectionRenderer] 참조 데이터 조회 실패");
|
||||||
|
|
@ -771,7 +772,9 @@ export function TableSectionRenderer({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[TableSectionRenderer] 참조 데이터 조회 실패:", error);
|
console.error("[TableSectionRenderer] 참조 데이터 조회 실패:", error);
|
||||||
}
|
}
|
||||||
}, [tableConfig.columns, tableConfig.source?.tableName]);
|
},
|
||||||
|
[tableConfig.columns, tableConfig.source?.tableName],
|
||||||
|
);
|
||||||
|
|
||||||
// formData에서 초기 테이블 데이터 로드 (수정 모드에서 _groupedData 표시)
|
// formData에서 초기 테이블 데이터 로드 (수정 모드에서 _groupedData 표시)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -796,7 +799,7 @@ export function TableSectionRenderer({
|
||||||
|
|
||||||
// RepeaterColumnConfig로 변환 (동적 Select 옵션 반영)
|
// RepeaterColumnConfig로 변환 (동적 Select 옵션 반영)
|
||||||
const columns: RepeaterColumnConfig[] = useMemo(() => {
|
const columns: RepeaterColumnConfig[] = useMemo(() => {
|
||||||
return (tableConfig.columns || []).map(col => {
|
return (tableConfig.columns || []).map((col) => {
|
||||||
const baseColumn = convertToRepeaterColumn(col);
|
const baseColumn = convertToRepeaterColumn(col);
|
||||||
|
|
||||||
// 동적 Select 옵션이 있으면 적용
|
// 동적 Select 옵션이 있으면 적용
|
||||||
|
|
@ -840,22 +843,23 @@ export function TableSectionRenderer({
|
||||||
|
|
||||||
return updatedRow;
|
return updatedRow;
|
||||||
},
|
},
|
||||||
[calculationRules]
|
[calculationRules],
|
||||||
);
|
);
|
||||||
|
|
||||||
const calculateAll = useCallback(
|
const calculateAll = useCallback(
|
||||||
(data: any[]): any[] => {
|
(data: any[]): any[] => {
|
||||||
return data.map((row) => calculateRow(row));
|
return data.map((row) => calculateRow(row));
|
||||||
},
|
},
|
||||||
[calculateRow]
|
[calculateRow],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 행 변경 핸들러 (동적 Select 행 선택 모드 지원)
|
// 행 변경 핸들러 (동적 Select 행 선택 모드 지원)
|
||||||
const handleRowChange = useCallback(
|
const handleRowChange = useCallback(
|
||||||
(index: number, newRow: any, conditionValue?: string) => {
|
(index: number, newRow: any, conditionValue?: string) => {
|
||||||
const oldRow = conditionValue && isConditionalMode
|
const oldRow =
|
||||||
? (conditionalTableData[conditionValue]?.[index] || {})
|
conditionValue && isConditionalMode
|
||||||
: (tableData[index] || {});
|
? conditionalTableData[conditionValue]?.[index] || {}
|
||||||
|
: tableData[index] || {};
|
||||||
|
|
||||||
// 변경된 필드 찾기
|
// 변경된 필드 찾기
|
||||||
const changedFields: string[] = [];
|
const changedFields: string[] = [];
|
||||||
|
|
@ -867,7 +871,7 @@ export function TableSectionRenderer({
|
||||||
|
|
||||||
// 동적 Select 컬럼의 행 선택 모드 확인
|
// 동적 Select 컬럼의 행 선택 모드 확인
|
||||||
for (const changedField of changedFields) {
|
for (const changedField of changedFields) {
|
||||||
const column = tableConfig.columns?.find(col => col.field === changedField);
|
const column = tableConfig.columns?.find((col) => col.field === changedField);
|
||||||
if (column?.dynamicSelectOptions?.rowSelectionMode?.enabled) {
|
if (column?.dynamicSelectOptions?.rowSelectionMode?.enabled) {
|
||||||
// 행 선택 모드 처리 (자동 채움)
|
// 행 선택 모드 처리 (자동 채움)
|
||||||
handleDynamicSelectChange(index, changedField, newRow[changedField], conditionValue);
|
handleDynamicSelectChange(index, changedField, newRow[changedField], conditionValue);
|
||||||
|
|
@ -882,7 +886,7 @@ export function TableSectionRenderer({
|
||||||
const currentData = conditionalTableData[conditionValue] || [];
|
const currentData = conditionalTableData[conditionValue] || [];
|
||||||
const newData = [...currentData];
|
const newData = [...currentData];
|
||||||
newData[index] = calculatedRow;
|
newData[index] = calculatedRow;
|
||||||
setConditionalTableData(prev => ({ ...prev, [conditionValue]: newData }));
|
setConditionalTableData((prev) => ({ ...prev, [conditionValue]: newData }));
|
||||||
onConditionalTableDataChange?.(conditionValue, newData);
|
onConditionalTableDataChange?.(conditionValue, newData);
|
||||||
} else {
|
} else {
|
||||||
const newData = [...tableData];
|
const newData = [...tableData];
|
||||||
|
|
@ -890,7 +894,16 @@ export function TableSectionRenderer({
|
||||||
handleDataChange(newData);
|
handleDataChange(newData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tableData, conditionalTableData, isConditionalMode, tableConfig.columns, calculateRow, handleDataChange, handleDynamicSelectChange, onConditionalTableDataChange]
|
[
|
||||||
|
tableData,
|
||||||
|
conditionalTableData,
|
||||||
|
isConditionalMode,
|
||||||
|
tableConfig.columns,
|
||||||
|
calculateRow,
|
||||||
|
handleDataChange,
|
||||||
|
handleDynamicSelectChange,
|
||||||
|
onConditionalTableDataChange,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 행 삭제 핸들러
|
// 행 삭제 핸들러
|
||||||
|
|
@ -899,7 +912,7 @@ export function TableSectionRenderer({
|
||||||
const newData = tableData.filter((_, i) => i !== index);
|
const newData = tableData.filter((_, i) => i !== index);
|
||||||
handleDataChange(newData);
|
handleDataChange(newData);
|
||||||
},
|
},
|
||||||
[tableData, handleDataChange]
|
[tableData, handleDataChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 선택된 항목 일괄 삭제
|
// 선택된 항목 일괄 삭제
|
||||||
|
|
@ -969,11 +982,13 @@ export function TableSectionRenderer({
|
||||||
// 외부 테이블 조회 설정
|
// 외부 테이블 조회 설정
|
||||||
externalLookup: cond.externalLookup,
|
externalLookup: cond.externalLookup,
|
||||||
// 값 변환 설정 전달 (레거시 호환)
|
// 값 변환 설정 전달 (레거시 호환)
|
||||||
transform: cond.transform?.enabled ? {
|
transform: cond.transform?.enabled
|
||||||
|
? {
|
||||||
tableName: cond.transform.tableName,
|
tableName: cond.transform.tableName,
|
||||||
matchColumn: cond.transform.matchColumn,
|
matchColumn: cond.transform.matchColumn,
|
||||||
resultColumn: cond.transform.resultColumn,
|
resultColumn: cond.transform.resultColumn,
|
||||||
} : undefined,
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -984,7 +999,7 @@ export function TableSectionRenderer({
|
||||||
joinConditions,
|
joinConditions,
|
||||||
{ ...sourceItem, ...newItem }, // rowData (현재 행)
|
{ ...sourceItem, ...newItem }, // rowData (현재 행)
|
||||||
sourceItem, // sourceData (소스 테이블 원본)
|
sourceItem, // sourceData (소스 테이블 원본)
|
||||||
formData
|
formData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
|
|
@ -1046,7 +1061,7 @@ export function TableSectionRenderer({
|
||||||
joinConditions,
|
joinConditions,
|
||||||
{ ...sourceItem, ...newItem }, // rowData
|
{ ...sourceItem, ...newItem }, // rowData
|
||||||
sourceItem, // sourceData
|
sourceItem, // sourceData
|
||||||
formData
|
formData,
|
||||||
);
|
);
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
newItem[col.field] = value;
|
newItem[col.field] = value;
|
||||||
|
|
@ -1070,7 +1085,7 @@ export function TableSectionRenderer({
|
||||||
}
|
}
|
||||||
|
|
||||||
return newItem;
|
return newItem;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 계산 필드 업데이트
|
// 계산 필드 업데이트
|
||||||
|
|
@ -1080,7 +1095,7 @@ export function TableSectionRenderer({
|
||||||
const newData = [...tableData, ...calculatedItems];
|
const newData = [...tableData, ...calculatedItems];
|
||||||
handleDataChange(newData);
|
handleDataChange(newData);
|
||||||
},
|
},
|
||||||
[tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources]
|
[tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 컬럼 모드/조회 옵션 변경 핸들러
|
// 컬럼 모드/조회 옵션 변경 핸들러
|
||||||
|
|
@ -1140,11 +1155,13 @@ export function TableSectionRenderer({
|
||||||
// 외부 테이블 조회 설정
|
// 외부 테이블 조회 설정
|
||||||
externalLookup: cond.externalLookup,
|
externalLookup: cond.externalLookup,
|
||||||
// 값 변환 설정 전달 (레거시 호환)
|
// 값 변환 설정 전달 (레거시 호환)
|
||||||
transform: cond.transform?.enabled ? {
|
transform: cond.transform?.enabled
|
||||||
|
? {
|
||||||
tableName: cond.transform.tableName,
|
tableName: cond.transform.tableName,
|
||||||
matchColumn: cond.transform.matchColumn,
|
matchColumn: cond.transform.matchColumn,
|
||||||
resultColumn: cond.transform.resultColumn,
|
resultColumn: cond.transform.resultColumn,
|
||||||
} : undefined,
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1156,7 +1173,7 @@ export function TableSectionRenderer({
|
||||||
joinConditions,
|
joinConditions,
|
||||||
row,
|
row,
|
||||||
sourceData,
|
sourceData,
|
||||||
formData
|
formData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
|
|
@ -1164,7 +1181,7 @@ export function TableSectionRenderer({
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...row, [columnField]: newValue };
|
return { ...row, [columnField]: newValue };
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 계산 필드 업데이트
|
// 계산 필드 업데이트
|
||||||
|
|
@ -1199,14 +1216,14 @@ export function TableSectionRenderer({
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...row, [columnField]: newValue };
|
return { ...row, [columnField]: newValue };
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 계산 필드 업데이트
|
// 계산 필드 업데이트
|
||||||
const calculatedData = calculateAll(updatedData);
|
const calculatedData = calculateAll(updatedData);
|
||||||
handleDataChange(calculatedData);
|
handleDataChange(calculatedData);
|
||||||
},
|
},
|
||||||
[tableConfig.columns, tableData, formData, calculateAll, handleDataChange]
|
[tableConfig.columns, tableData, formData, calculateAll, handleDataChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 소스 테이블 정보
|
// 소스 테이블 정보
|
||||||
|
|
@ -1216,10 +1233,16 @@ export function TableSectionRenderer({
|
||||||
const sourceSearchFields = source.searchColumns;
|
const sourceSearchFields = source.searchColumns;
|
||||||
const columnLabels = source.columnLabels || {};
|
const columnLabels = source.columnLabels || {};
|
||||||
const modalTitle = uiConfig?.modalTitle || "항목 검색 및 선택";
|
const modalTitle = uiConfig?.modalTitle || "항목 검색 및 선택";
|
||||||
const addButtonType = uiConfig?.addButtonType || "search";
|
|
||||||
const addButtonText = uiConfig?.addButtonText || (addButtonType === "addRow" ? "항목 추가" : "항목 검색");
|
|
||||||
const multiSelect = uiConfig?.multiSelect ?? true;
|
const multiSelect = uiConfig?.multiSelect ?? true;
|
||||||
|
|
||||||
|
// 버튼 표시 설정 (두 버튼 동시 표시 가능)
|
||||||
|
// 레거시 호환: 기존 addButtonType 설정이 있으면 그에 맞게 변환
|
||||||
|
const legacyAddButtonType = uiConfig?.addButtonType;
|
||||||
|
const showSearchButton = legacyAddButtonType === "addRow" ? false : (uiConfig?.showSearchButton ?? true);
|
||||||
|
const showAddRowButton = legacyAddButtonType === "addRow" ? true : (uiConfig?.showAddRowButton ?? false);
|
||||||
|
const searchButtonText = uiConfig?.searchButtonText || uiConfig?.addButtonText || "품목 검색";
|
||||||
|
const addRowButtonText = uiConfig?.addRowButtonText || "직접 입력";
|
||||||
|
|
||||||
// 기본 필터 조건 생성 (사전 필터만 - 모달 필터는 ItemSelectionModal에서 처리)
|
// 기본 필터 조건 생성 (사전 필터만 - 모달 필터는 ItemSelectionModal에서 처리)
|
||||||
const baseFilterCondition: Record<string, any> = useMemo(() => {
|
const baseFilterCondition: Record<string, any> = useMemo(() => {
|
||||||
const condition: Record<string, any> = {};
|
const condition: Record<string, any> = {};
|
||||||
|
|
@ -1253,7 +1276,7 @@ export function TableSectionRenderer({
|
||||||
column: filter.column,
|
column: filter.column,
|
||||||
label: filter.label || filter.column,
|
label: filter.label || filter.column,
|
||||||
// category 타입을 select로 변환 (ModalFilterConfig 호환)
|
// category 타입을 select로 변환 (ModalFilterConfig 호환)
|
||||||
type: filter.type === "category" ? "select" as const : filter.type as "text" | "select",
|
type: filter.type === "category" ? ("select" as const) : (filter.type as "text" | "select"),
|
||||||
options: filter.options,
|
options: filter.options,
|
||||||
categoryRef: filter.categoryRef,
|
categoryRef: filter.categoryRef,
|
||||||
defaultValue: filter.defaultValue,
|
defaultValue: filter.defaultValue,
|
||||||
|
|
@ -1265,7 +1288,8 @@ export function TableSectionRenderer({
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// 조건부 테이블: 조건 체크박스 토글
|
// 조건부 테이블: 조건 체크박스 토글
|
||||||
const handleConditionToggle = useCallback((conditionValue: string, checked: boolean) => {
|
const handleConditionToggle = useCallback(
|
||||||
|
(conditionValue: string, checked: boolean) => {
|
||||||
setSelectedConditions((prev) => {
|
setSelectedConditions((prev) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
const newConditions = [...prev, conditionValue];
|
const newConditions = [...prev, conditionValue];
|
||||||
|
|
@ -1283,10 +1307,13 @@ export function TableSectionRenderer({
|
||||||
return newConditions;
|
return newConditions;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [activeConditionTab]);
|
},
|
||||||
|
[activeConditionTab],
|
||||||
|
);
|
||||||
|
|
||||||
// 조건부 테이블: 조건별 데이터 변경
|
// 조건부 테이블: 조건별 데이터 변경
|
||||||
const handleConditionalDataChange = useCallback((conditionValue: string, newData: any[]) => {
|
const handleConditionalDataChange = useCallback(
|
||||||
|
(conditionValue: string, newData: any[]) => {
|
||||||
setConditionalTableData((prev) => ({
|
setConditionalTableData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[conditionValue]: newData,
|
[conditionValue]: newData,
|
||||||
|
|
@ -1315,26 +1342,35 @@ export function TableSectionRenderer({
|
||||||
}
|
}
|
||||||
|
|
||||||
onTableDataChange(allData);
|
onTableDataChange(allData);
|
||||||
}, [conditionalTableData, conditionalConfig?.conditionColumn, onConditionalTableDataChange, onTableDataChange]);
|
},
|
||||||
|
[conditionalTableData, conditionalConfig?.conditionColumn, onConditionalTableDataChange, onTableDataChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 조건부 테이블: 조건별 행 변경
|
// 조건부 테이블: 조건별 행 변경
|
||||||
const handleConditionalRowChange = useCallback((conditionValue: string, index: number, newRow: any) => {
|
const handleConditionalRowChange = useCallback(
|
||||||
|
(conditionValue: string, index: number, newRow: any) => {
|
||||||
const calculatedRow = calculateRow(newRow);
|
const calculatedRow = calculateRow(newRow);
|
||||||
const currentData = conditionalTableData[conditionValue] || [];
|
const currentData = conditionalTableData[conditionValue] || [];
|
||||||
const newData = [...currentData];
|
const newData = [...currentData];
|
||||||
newData[index] = calculatedRow;
|
newData[index] = calculatedRow;
|
||||||
handleConditionalDataChange(conditionValue, newData);
|
handleConditionalDataChange(conditionValue, newData);
|
||||||
}, [conditionalTableData, calculateRow, handleConditionalDataChange]);
|
},
|
||||||
|
[conditionalTableData, calculateRow, handleConditionalDataChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 조건부 테이블: 조건별 행 삭제
|
// 조건부 테이블: 조건별 행 삭제
|
||||||
const handleConditionalRowDelete = useCallback((conditionValue: string, index: number) => {
|
const handleConditionalRowDelete = useCallback(
|
||||||
|
(conditionValue: string, index: number) => {
|
||||||
const currentData = conditionalTableData[conditionValue] || [];
|
const currentData = conditionalTableData[conditionValue] || [];
|
||||||
const newData = currentData.filter((_, i) => i !== index);
|
const newData = currentData.filter((_, i) => i !== index);
|
||||||
handleConditionalDataChange(conditionValue, newData);
|
handleConditionalDataChange(conditionValue, newData);
|
||||||
}, [conditionalTableData, handleConditionalDataChange]);
|
},
|
||||||
|
[conditionalTableData, handleConditionalDataChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 조건부 테이블: 조건별 선택 행 일괄 삭제
|
// 조건부 테이블: 조건별 선택 행 일괄 삭제
|
||||||
const handleConditionalBulkDelete = useCallback((conditionValue: string) => {
|
const handleConditionalBulkDelete = useCallback(
|
||||||
|
(conditionValue: string) => {
|
||||||
const selected = conditionalSelectedRows[conditionValue] || new Set();
|
const selected = conditionalSelectedRows[conditionValue] || new Set();
|
||||||
if (selected.size === 0) return;
|
if (selected.size === 0) return;
|
||||||
|
|
||||||
|
|
@ -1347,10 +1383,13 @@ export function TableSectionRenderer({
|
||||||
...prev,
|
...prev,
|
||||||
[conditionValue]: new Set(),
|
[conditionValue]: new Set(),
|
||||||
}));
|
}));
|
||||||
}, [conditionalTableData, conditionalSelectedRows, handleConditionalDataChange]);
|
},
|
||||||
|
[conditionalTableData, conditionalSelectedRows, handleConditionalDataChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 조건부 테이블: 아이템 추가 (특정 조건에)
|
// 조건부 테이블: 아이템 추가 (특정 조건에)
|
||||||
const handleConditionalAddItems = useCallback(async (items: any[]) => {
|
const handleConditionalAddItems = useCallback(
|
||||||
|
async (items: any[]) => {
|
||||||
if (!modalCondition) return;
|
if (!modalCondition) return;
|
||||||
|
|
||||||
// 기존 handleAddItems 로직을 재사용하여 매핑된 아이템 생성
|
// 기존 handleAddItems 로직을 재사용하여 매핑된 아이템 생성
|
||||||
|
|
@ -1387,7 +1426,7 @@ export function TableSectionRenderer({
|
||||||
newItem._sourceData = sourceItem;
|
newItem._sourceData = sourceItem;
|
||||||
|
|
||||||
return newItem;
|
return newItem;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 현재 조건의 데이터에 추가
|
// 현재 조건의 데이터에 추가
|
||||||
|
|
@ -1396,7 +1435,9 @@ export function TableSectionRenderer({
|
||||||
handleConditionalDataChange(modalCondition, newData);
|
handleConditionalDataChange(modalCondition, newData);
|
||||||
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
}, [modalCondition, tableConfig.columns, formData, conditionalTableData, handleConditionalDataChange]);
|
},
|
||||||
|
[modalCondition, tableConfig.columns, formData, conditionalTableData, handleConditionalDataChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 조건부 테이블: 모달 열기 (특정 조건에 대해)
|
// 조건부 테이블: 모달 열기 (특정 조건에 대해)
|
||||||
const openConditionalModal = useCallback((conditionValue: string) => {
|
const openConditionalModal = useCallback((conditionValue: string) => {
|
||||||
|
|
@ -1405,7 +1446,8 @@ export function TableSectionRenderer({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 조건부 테이블: 빈 행 추가 (addRow 모드에서 사용)
|
// 조건부 테이블: 빈 행 추가 (addRow 모드에서 사용)
|
||||||
const addEmptyRowToCondition = useCallback((conditionValue: string) => {
|
const addEmptyRowToCondition = useCallback(
|
||||||
|
(conditionValue: string) => {
|
||||||
const newRow: Record<string, any> = {};
|
const newRow: Record<string, any> = {};
|
||||||
|
|
||||||
// 각 컬럼의 기본값으로 빈 행 생성
|
// 각 컬럼의 기본값으로 빈 행 생성
|
||||||
|
|
@ -1430,20 +1472,25 @@ export function TableSectionRenderer({
|
||||||
const currentData = conditionalTableData[conditionValue] || [];
|
const currentData = conditionalTableData[conditionValue] || [];
|
||||||
const newData = [...currentData, newRow];
|
const newData = [...currentData, newRow];
|
||||||
handleConditionalDataChange(conditionValue, newData);
|
handleConditionalDataChange(conditionValue, newData);
|
||||||
}, [tableConfig.columns, conditionalConfig?.conditionColumn, conditionalTableData, handleConditionalDataChange]);
|
},
|
||||||
|
[tableConfig.columns, conditionalConfig?.conditionColumn, conditionalTableData, handleConditionalDataChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 버튼 클릭 핸들러 (addButtonType에 따라 다르게 동작)
|
// 검색 버튼 클릭 핸들러
|
||||||
const handleAddButtonClick = useCallback((conditionValue: string) => {
|
const handleSearchButtonClick = useCallback(
|
||||||
const addButtonType = tableConfig.uiConfig?.addButtonType || "search";
|
(conditionValue: string) => {
|
||||||
|
|
||||||
if (addButtonType === "addRow") {
|
|
||||||
// 빈 행 직접 추가
|
|
||||||
addEmptyRowToCondition(conditionValue);
|
|
||||||
} else {
|
|
||||||
// 검색 모달 열기
|
|
||||||
openConditionalModal(conditionValue);
|
openConditionalModal(conditionValue);
|
||||||
}
|
},
|
||||||
}, [tableConfig.uiConfig?.addButtonType, addEmptyRowToCondition, openConditionalModal]);
|
[openConditionalModal],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 행 추가 버튼 클릭 핸들러
|
||||||
|
const handleAddRowButtonClick = useCallback(
|
||||||
|
(conditionValue: string) => {
|
||||||
|
addEmptyRowToCondition(conditionValue);
|
||||||
|
},
|
||||||
|
[addEmptyRowToCondition],
|
||||||
|
);
|
||||||
|
|
||||||
// 조건부 테이블: 초기 데이터 로드 (수정 모드)
|
// 조건부 테이블: 초기 데이터 로드 (수정 모드)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1498,17 +1545,19 @@ export function TableSectionRenderer({
|
||||||
|
|
||||||
// 정적 옵션과 동적 옵션 병합 (동적 옵션이 있으면 우선 사용)
|
// 정적 옵션과 동적 옵션 병합 (동적 옵션이 있으면 우선 사용)
|
||||||
// 빈 value를 가진 옵션은 제외 (Select.Item은 빈 문자열 value를 허용하지 않음)
|
// 빈 value를 가진 옵션은 제외 (Select.Item은 빈 문자열 value를 허용하지 않음)
|
||||||
const effectiveOptions = (conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0
|
const effectiveOptions = (
|
||||||
|
conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0
|
||||||
? dynamicOptions
|
? dynamicOptions
|
||||||
: conditionalConfig.options || []).filter(opt => opt.value && opt.value.trim() !== "");
|
: conditionalConfig.options || []
|
||||||
|
).filter((opt) => opt.value && opt.value.trim() !== "");
|
||||||
|
|
||||||
// 로딩 중이면 로딩 표시
|
// 로딩 중이면 로딩 표시
|
||||||
if (dynamicOptionsLoading) {
|
if (dynamicOptionsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-4", className)}>
|
<div className={cn("space-y-4", className)}>
|
||||||
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
<div className="text-muted-foreground flex items-center justify-center py-8 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
<div className="border-primary h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
|
||||||
조건 옵션을 불러오는 중...
|
조건 옵션을 불러오는 중...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1525,7 +1574,7 @@ export function TableSectionRenderer({
|
||||||
{effectiveOptions.map((option) => (
|
{effectiveOptions.map((option) => (
|
||||||
<label
|
<label
|
||||||
key={option.id}
|
key={option.id}
|
||||||
className="flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 transition-colors hover:bg-muted/50"
|
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 transition-colors"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedConditions.includes(option.value)}
|
checked={selectedConditions.includes(option.value)}
|
||||||
|
|
@ -1542,7 +1591,7 @@ export function TableSectionRenderer({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedConditions.length > 0 && (
|
{selectedConditions.length > 0 && (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-muted-foreground text-xs">
|
||||||
{selectedConditions.length}개 유형 선택됨, 총 {totalConditionalItems}개 항목
|
{selectedConditions.length}개 유형 선택됨, 총 {totalConditionalItems}개 항목
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1604,7 +1653,7 @@ export function TableSectionRenderer({
|
||||||
{/* 테이블 상단 컨트롤 */}
|
{/* 테이블 상단 컨트롤 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-muted-foreground text-sm">
|
||||||
{data.length > 0 && `${data.length}개 항목`}
|
{data.length > 0 && `${data.length}개 항목`}
|
||||||
{selected.size > 0 && ` (${selected.size}개 선택됨)`}
|
{selected.size > 0 && ` (${selected.size}개 선택됨)`}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1642,17 +1691,22 @@ export function TableSectionRenderer({
|
||||||
선택 삭제 ({selected.size})
|
선택 삭제 ({selected.size})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{showSearchButton && (
|
||||||
|
<Button onClick={() => handleSearchButtonClick(conditionValue)} className="h-8 text-xs">
|
||||||
|
<Search className="mr-2 h-4 w-4" />
|
||||||
|
{searchButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showAddRowButton && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAddButtonClick(conditionValue)}
|
variant="outline"
|
||||||
|
onClick={() => handleAddRowButtonClick(conditionValue)}
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
>
|
>
|
||||||
{addButtonType === "addRow" ? (
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
) : (
|
{addRowButtonText}
|
||||||
<Search className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{addButtonText}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1711,7 +1765,7 @@ export function TableSectionRenderer({
|
||||||
<TabsContent key={option.id} value={option.value} className="mt-4 space-y-4">
|
<TabsContent key={option.id} value={option.value} className="mt-4 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-muted-foreground text-sm">
|
||||||
{data.length > 0 && `${data.length}개 항목`}
|
{data.length > 0 && `${data.length}개 항목`}
|
||||||
{selected.size > 0 && ` (${selected.size}개 선택됨)`}
|
{selected.size > 0 && ` (${selected.size}개 선택됨)`}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1728,17 +1782,22 @@ export function TableSectionRenderer({
|
||||||
선택 삭제 ({selected.size})
|
선택 삭제 ({selected.size})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{showSearchButton && (
|
||||||
|
<Button onClick={() => handleSearchButtonClick(option.value)} className="h-8 text-xs">
|
||||||
|
<Search className="mr-2 h-4 w-4" />
|
||||||
|
{searchButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showAddRowButton && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAddButtonClick(option.value)}
|
variant="outline"
|
||||||
|
onClick={() => handleAddRowButtonClick(option.value)}
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
>
|
>
|
||||||
{addButtonType === "addRow" ? (
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
) : (
|
{addRowButtonText}
|
||||||
<Search className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{addButtonText}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1768,10 +1827,8 @@ export function TableSectionRenderer({
|
||||||
{/* 조건이 선택되지 않은 경우 안내 메시지 (checkbox/dropdown 모드에서만) */}
|
{/* 조건이 선택되지 않은 경우 안내 메시지 (checkbox/dropdown 모드에서만) */}
|
||||||
{selectedConditions.length === 0 && triggerType !== "tabs" && (
|
{selectedConditions.length === 0 && triggerType !== "tabs" && (
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-muted-foreground text-sm">
|
||||||
{triggerType === "checkbox"
|
{triggerType === "checkbox" ? "위에서 유형을 선택하여 검사항목을 추가하세요." : "유형을 선택하세요."}
|
||||||
? "위에서 유형을 선택하여 검사항목을 추가하세요."
|
|
||||||
: "유형을 선택하세요."}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1779,9 +1836,7 @@ export function TableSectionRenderer({
|
||||||
{/* 옵션이 없는 경우 안내 메시지 */}
|
{/* 옵션이 없는 경우 안내 메시지 */}
|
||||||
{effectiveOptions.length === 0 && (
|
{effectiveOptions.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-muted-foreground text-sm">조건 옵션이 설정되지 않았습니다.</p>
|
||||||
조건 옵션이 설정되지 않았습니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1811,9 +1866,9 @@ export function TableSectionRenderer({
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-4", className)}>
|
<div className={cn("space-y-4", className)}>
|
||||||
{/* 추가 버튼 영역 */}
|
{/* 추가 버튼 영역 */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-muted-foreground text-sm">
|
||||||
{tableData.length > 0 && `${tableData.length}개 항목`}
|
{tableData.length > 0 && `${tableData.length}개 항목`}
|
||||||
{selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`}
|
{selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1822,17 +1877,17 @@ export function TableSectionRenderer({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setWidthTrigger((prev) => prev + 1)}
|
onClick={() => setWidthTrigger((prev) => prev + 1)}
|
||||||
className="h-7 text-xs px-2"
|
className="h-7 px-2 text-xs"
|
||||||
title={widthTrigger % 2 === 0 ? "내용에 맞게 자동 조정" : "균등 분배"}
|
title={widthTrigger % 2 === 0 ? "내용에 맞게 자동 조정" : "균등 분배"}
|
||||||
>
|
>
|
||||||
{widthTrigger % 2 === 0 ? (
|
{widthTrigger % 2 === 0 ? (
|
||||||
<>
|
<>
|
||||||
<AlignJustify className="h-3.5 w-3.5 mr-1" />
|
<AlignJustify className="mr-1 h-3.5 w-3.5" />
|
||||||
자동 맞춤
|
자동 맞춤
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Columns className="h-3.5 w-3.5 mr-1" />
|
<Columns className="mr-1 h-3.5 w-3.5" />
|
||||||
균등 분배
|
균등 분배
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1841,17 +1896,20 @@ export function TableSectionRenderer({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{selectedRows.size > 0 && (
|
{selectedRows.size > 0 && (
|
||||||
<Button
|
<Button variant="destructive" onClick={handleBulkDelete} className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
variant="destructive"
|
|
||||||
onClick={handleBulkDelete}
|
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
||||||
>
|
|
||||||
선택 삭제 ({selectedRows.size})
|
선택 삭제 ({selectedRows.size})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{showSearchButton && (
|
||||||
|
<Button onClick={() => setModalOpen(true)} className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
|
<Search className="mr-2 h-4 w-4" />
|
||||||
|
{searchButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showAddRowButton && (
|
||||||
<Button
|
<Button
|
||||||
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (addButtonType === "addRow") {
|
|
||||||
// 빈 행 추가
|
// 빈 행 추가
|
||||||
const newRow: Record<string, any> = {};
|
const newRow: Record<string, any> = {};
|
||||||
for (const col of columns) {
|
for (const col of columns) {
|
||||||
|
|
@ -1866,20 +1924,13 @@ export function TableSectionRenderer({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleDataChange([...tableData, newRow]);
|
handleDataChange([...tableData, newRow]);
|
||||||
} else {
|
|
||||||
// 검색 모달 열기
|
|
||||||
setModalOpen(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
>
|
>
|
||||||
{addButtonType === "addRow" ? (
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
{addRowButtonText}
|
||||||
) : (
|
|
||||||
<Search className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{addButtonText}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -963,6 +963,13 @@ export function UniversalFormModalComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 별도 테이블에 저장해야 하는 테이블 섹션 목록
|
||||||
|
const tableSectionsForSeparateTable = config.sections.filter(
|
||||||
|
(s) => s.type === "table" &&
|
||||||
|
s.tableConfig?.saveConfig?.targetTable &&
|
||||||
|
s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName
|
||||||
|
);
|
||||||
|
|
||||||
// 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장)
|
// 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장)
|
||||||
// targetTable이 없거나 메인 테이블과 같은 경우
|
// targetTable이 없거나 메인 테이블과 같은 경우
|
||||||
const tableSectionsForMainTable = config.sections.filter(
|
const tableSectionsForMainTable = config.sections.filter(
|
||||||
|
|
@ -971,6 +978,12 @@ export function UniversalFormModalComponent({
|
||||||
s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName)
|
s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("[saveSingleRow] 메인 테이블:", config.saveConfig.tableName);
|
||||||
|
console.log("[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", tableSectionsForMainTable.map(s => s.id));
|
||||||
|
console.log("[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", tableSectionsForSeparateTable.map(s => s.id));
|
||||||
|
console.log("[saveSingleRow] 테이블 섹션 데이터 키:", Object.keys(tableSectionData));
|
||||||
|
console.log("[saveSingleRow] dataToSave 키:", Object.keys(dataToSave));
|
||||||
|
|
||||||
if (tableSectionsForMainTable.length > 0) {
|
if (tableSectionsForMainTable.length > 0) {
|
||||||
// 공통 저장 필드 수집 (sectionSaveModes 설정에 따라)
|
// 공통 저장 필드 수집 (sectionSaveModes 설정에 따라)
|
||||||
const commonFieldsData: Record<string, any> = {};
|
const commonFieldsData: Record<string, any> = {};
|
||||||
|
|
@ -1050,16 +1063,17 @@ export function UniversalFormModalComponent({
|
||||||
// 메인 레코드 ID가 필요한 경우 (response.data에서 가져오기)
|
// 메인 레코드 ID가 필요한 경우 (response.data에서 가져오기)
|
||||||
const mainRecordId = response.data?.data?.id;
|
const mainRecordId = response.data?.data?.id;
|
||||||
|
|
||||||
// 공통 저장 필드 수집 (sectionSaveModes 설정에 따라)
|
// 공통 저장 필드 수집: 다른 섹션(필드 타입)에서 공통 저장으로 설정된 필드 값
|
||||||
|
// 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual'
|
||||||
const commonFieldsData: Record<string, any> = {};
|
const commonFieldsData: Record<string, any> = {};
|
||||||
const { sectionSaveModes } = config.saveConfig;
|
const { sectionSaveModes } = config.saveConfig;
|
||||||
|
|
||||||
if (sectionSaveModes && sectionSaveModes.length > 0) {
|
|
||||||
// 다른 섹션에서 공통 저장으로 설정된 필드 값 수집
|
// 다른 섹션에서 공통 저장으로 설정된 필드 값 수집
|
||||||
for (const otherSection of config.sections) {
|
for (const otherSection of config.sections) {
|
||||||
if (otherSection.id === section.id) continue; // 현재 테이블 섹션은 건너뛰기
|
if (otherSection.id === section.id) continue; // 현재 테이블 섹션은 건너뛰기
|
||||||
|
|
||||||
const sectionMode = sectionSaveModes.find((s) => s.sectionId === otherSection.id);
|
const sectionMode = sectionSaveModes?.find((s) => s.sectionId === otherSection.id);
|
||||||
|
// 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual'
|
||||||
const defaultMode = otherSection.type === "table" ? "individual" : "common";
|
const defaultMode = otherSection.type === "table" ? "individual" : "common";
|
||||||
const sectionSaveMode = sectionMode?.saveMode || defaultMode;
|
const sectionSaveMode = sectionMode?.saveMode || defaultMode;
|
||||||
|
|
||||||
|
|
@ -1076,8 +1090,23 @@ export function UniversalFormModalComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 선택적 필드 그룹 (optionalFieldGroups)도 처리
|
||||||
|
if (otherSection.optionalFieldGroups && otherSection.optionalFieldGroups.length > 0) {
|
||||||
|
for (const optGroup of otherSection.optionalFieldGroups) {
|
||||||
|
if (optGroup.fields) {
|
||||||
|
for (const field of optGroup.fields) {
|
||||||
|
// 선택적 필드 그룹은 기본적으로 common 저장
|
||||||
|
if (formData[field.columnName] !== undefined) {
|
||||||
|
commonFieldsData[field.columnName] = formData[field.columnName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[saveSingleRow] 별도 테이블 저장 - 공통 필드:", Object.keys(commonFieldsData));
|
||||||
|
|
||||||
for (const item of sectionData) {
|
for (const item of sectionData) {
|
||||||
// 공통 필드 병합 + 개별 품목 데이터
|
// 공통 필드 병합 + 개별 품목 데이터
|
||||||
|
|
@ -1091,15 +1120,26 @@ export function UniversalFormModalComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _sourceData 등 내부 메타데이터 제거
|
||||||
|
Object.keys(itemToSave).forEach((key) => {
|
||||||
|
if (key.startsWith("_")) {
|
||||||
|
delete itemToSave[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 메인 레코드와 연결이 필요한 경우
|
// 메인 레코드와 연결이 필요한 경우
|
||||||
if (mainRecordId && config.saveConfig.primaryKeyColumn) {
|
if (mainRecordId && config.saveConfig.primaryKeyColumn) {
|
||||||
itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId;
|
itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiClient.post(
|
const saveResponse = await apiClient.post(
|
||||||
`/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`,
|
`/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`,
|
||||||
itemToSave
|
itemToSave
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!saveResponse.data?.success) {
|
||||||
|
throw new Error(saveResponse.data?.message || `${section.title || "테이블 섹션"} 저장 실패`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2928,54 +2928,74 @@ export function TableSectionSettingsModal({
|
||||||
{/* UI 설정 */}
|
{/* UI 설정 */}
|
||||||
<div className="space-y-3 border rounded-lg p-4">
|
<div className="space-y-3 border rounded-lg p-4">
|
||||||
<h4 className="text-sm font-medium">UI 설정</h4>
|
<h4 className="text-sm font-medium">UI 설정</h4>
|
||||||
|
|
||||||
|
{/* 버튼 표시 설정 */}
|
||||||
|
<div className="space-y-2 p-3 bg-muted/30 rounded-lg">
|
||||||
|
<Label className="text-xs font-medium">표시할 버튼 선택</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground mb-2">
|
||||||
|
두 버튼을 동시에 표시할 수 있습니다.
|
||||||
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={tableConfig.uiConfig?.showSearchButton ?? true}
|
||||||
|
onCheckedChange={(checked) => updateUiConfig({ showSearchButton: checked })}
|
||||||
|
className="scale-75"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">추가 버튼 동작</Label>
|
<span className="text-xs font-medium">검색 버튼</span>
|
||||||
<Select
|
<p className="text-[10px] text-muted-foreground">기존 데이터에서 선택</p>
|
||||||
value={tableConfig.uiConfig?.addButtonType || "search"}
|
|
||||||
onValueChange={(value) => updateUiConfig({ addButtonType: value as "search" | "addRow" })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs mt-1">
|
|
||||||
<SelectValue placeholder="버튼 동작 선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="search">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>검색 모달 열기</span>
|
|
||||||
<span className="text-[10px] text-muted-foreground">기존 데이터에서 선택</span>
|
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="addRow">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>빈 행 추가</span>
|
|
||||||
<span className="text-[10px] text-muted-foreground">새 데이터 직접 입력</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={tableConfig.uiConfig?.showAddRowButton ?? false}
|
||||||
|
onCheckedChange={(checked) => updateUiConfig({ showAddRowButton: checked })}
|
||||||
|
className="scale-75"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">추가 버튼 텍스트</Label>
|
<span className="text-xs font-medium">행 추가 버튼</span>
|
||||||
|
<p className="text-[10px] text-muted-foreground">빈 행 직접 입력</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* 검색 버튼 텍스트 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">검색 버튼 텍스트</Label>
|
||||||
<Input
|
<Input
|
||||||
value={tableConfig.uiConfig?.addButtonText || ""}
|
value={tableConfig.uiConfig?.searchButtonText || ""}
|
||||||
onChange={(e) => updateUiConfig({ addButtonText: e.target.value })}
|
onChange={(e) => updateUiConfig({ searchButtonText: e.target.value })}
|
||||||
placeholder={tableConfig.uiConfig?.addButtonType === "addRow" ? "항목 추가" : "항목 검색"}
|
placeholder="품목 검색"
|
||||||
className="h-8 text-xs mt-1"
|
className="h-8 text-xs mt-1"
|
||||||
|
disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 행 추가 버튼 텍스트 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">모달 제목</Label>
|
<Label className="text-xs">행 추가 버튼 텍스트</Label>
|
||||||
|
<Input
|
||||||
|
value={tableConfig.uiConfig?.addRowButtonText || ""}
|
||||||
|
onChange={(e) => updateUiConfig({ addRowButtonText: e.target.value })}
|
||||||
|
placeholder="직접 입력"
|
||||||
|
className="h-8 text-xs mt-1"
|
||||||
|
disabled={!tableConfig.uiConfig?.showAddRowButton}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 모달 제목 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">검색 모달 제목</Label>
|
||||||
<Input
|
<Input
|
||||||
value={tableConfig.uiConfig?.modalTitle || ""}
|
value={tableConfig.uiConfig?.modalTitle || ""}
|
||||||
onChange={(e) => updateUiConfig({ modalTitle: e.target.value })}
|
onChange={(e) => updateUiConfig({ modalTitle: e.target.value })}
|
||||||
placeholder="항목 검색 및 선택"
|
placeholder="항목 검색 및 선택"
|
||||||
className="h-8 text-xs mt-1"
|
className="h-8 text-xs mt-1"
|
||||||
disabled={tableConfig.uiConfig?.addButtonType === "addRow"}
|
disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)}
|
||||||
/>
|
/>
|
||||||
{tableConfig.uiConfig?.addButtonType === "addRow" && (
|
|
||||||
<p className="text-[10px] text-muted-foreground mt-0.5">빈 행 추가 모드에서는 모달이 열리지 않습니다</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* 테이블 최대 높이 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">테이블 최대 높이</Label>
|
<Label className="text-xs">테이블 최대 높이</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -2985,13 +3005,14 @@ export function TableSectionSettingsModal({
|
||||||
className="h-8 text-xs mt-1"
|
className="h-8 text-xs mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 다중 선택 허용 */}
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||||
<Switch
|
<Switch
|
||||||
checked={tableConfig.uiConfig?.multiSelect ?? true}
|
checked={tableConfig.uiConfig?.multiSelect ?? true}
|
||||||
onCheckedChange={(checked) => updateUiConfig({ multiSelect: checked })}
|
onCheckedChange={(checked) => updateUiConfig({ multiSelect: checked })}
|
||||||
className="scale-75"
|
className="scale-75"
|
||||||
disabled={tableConfig.uiConfig?.addButtonType === "addRow"}
|
disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)}
|
||||||
/>
|
/>
|
||||||
<span>다중 선택 허용</span>
|
<span>다중 선택 허용</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -253,15 +253,19 @@ export interface TableSectionConfig {
|
||||||
|
|
||||||
// 6. UI 설정
|
// 6. UI 설정
|
||||||
uiConfig?: {
|
uiConfig?: {
|
||||||
addButtonText?: string; // 추가 버튼 텍스트 (기본: "품목 검색")
|
|
||||||
modalTitle?: string; // 모달 제목 (기본: "항목 검색 및 선택")
|
modalTitle?: string; // 모달 제목 (기본: "항목 검색 및 선택")
|
||||||
multiSelect?: boolean; // 다중 선택 허용 (기본: true)
|
multiSelect?: boolean; // 다중 선택 허용 (기본: true)
|
||||||
maxHeight?: string; // 테이블 최대 높이 (기본: "400px")
|
maxHeight?: string; // 테이블 최대 높이 (기본: "400px")
|
||||||
|
|
||||||
// 추가 버튼 타입
|
// 버튼 표시 설정 (동시 표시 가능)
|
||||||
// - search: 검색 모달 열기 (기본값) - 기존 데이터에서 선택
|
showSearchButton?: boolean; // 검색 버튼 표시 (기본: true)
|
||||||
// - addRow: 빈 행 직접 추가 - 새 데이터 직접 입력
|
showAddRowButton?: boolean; // 행 추가 버튼 표시 (기본: false)
|
||||||
|
searchButtonText?: string; // 검색 버튼 텍스트 (기본: "품목 검색")
|
||||||
|
addRowButtonText?: string; // 행 추가 버튼 텍스트 (기본: "직접 입력")
|
||||||
|
|
||||||
|
// 레거시 호환용 (deprecated)
|
||||||
addButtonType?: "search" | "addRow";
|
addButtonType?: "search" | "addRow";
|
||||||
|
addButtonText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 7. 조건부 테이블 설정 (고급)
|
// 7. 조건부 테이블 설정 (고급)
|
||||||
|
|
|
||||||
|
|
@ -1491,6 +1491,7 @@ export class ButtonActionExecutor {
|
||||||
* 🆕 Universal Form Modal 테이블 섹션 병합 저장 처리
|
* 🆕 Universal Form Modal 테이블 섹션 병합 저장 처리
|
||||||
* 범용_폼_모달 내부의 공통 필드 + _tableSection_ 데이터를 병합하여 품목별로 저장
|
* 범용_폼_모달 내부의 공통 필드 + _tableSection_ 데이터를 병합하여 품목별로 저장
|
||||||
* 수정 모드: INSERT/UPDATE/DELETE 지원
|
* 수정 모드: INSERT/UPDATE/DELETE 지원
|
||||||
|
* 🆕 섹션별 저장 테이블(targetTable) 지원 추가
|
||||||
*/
|
*/
|
||||||
private static async handleUniversalFormModalTableSectionSave(
|
private static async handleUniversalFormModalTableSectionSave(
|
||||||
config: ButtonActionConfig,
|
config: ButtonActionConfig,
|
||||||
|
|
@ -1515,6 +1516,65 @@ export class ButtonActionExecutor {
|
||||||
|
|
||||||
const modalData = formData[universalFormModalKey];
|
const modalData = formData[universalFormModalKey];
|
||||||
|
|
||||||
|
// 🆕 universal-form-modal 컴포넌트 설정 가져오기
|
||||||
|
// 1. componentConfigs에서 컴포넌트 ID로 찾기
|
||||||
|
// 2. allComponents에서 columnName으로 찾기
|
||||||
|
// 3. 화면 레이아웃 API에서 가져오기
|
||||||
|
let modalComponentConfig = context.componentConfigs?.[universalFormModalKey];
|
||||||
|
|
||||||
|
// componentConfigs에서 직접 찾지 못한 경우, allComponents에서 columnName으로 찾기
|
||||||
|
if (!modalComponentConfig && context.allComponents) {
|
||||||
|
const modalComponent = context.allComponents.find(
|
||||||
|
(comp: any) =>
|
||||||
|
comp.columnName === universalFormModalKey || comp.properties?.columnName === universalFormModalKey,
|
||||||
|
);
|
||||||
|
if (modalComponent) {
|
||||||
|
modalComponentConfig = modalComponent.componentConfig || modalComponent.properties?.componentConfig;
|
||||||
|
console.log("🎯 [handleUniversalFormModalTableSectionSave] allComponents에서 설정 찾음:", modalComponent.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 아직도 설정을 찾지 못했으면 화면 레이아웃 API에서 가져오기
|
||||||
|
if (!modalComponentConfig && screenId) {
|
||||||
|
try {
|
||||||
|
console.log("🔍 [handleUniversalFormModalTableSectionSave] 화면 레이아웃 API에서 설정 조회:", screenId);
|
||||||
|
const { screenApi } = await import("@/lib/api/screen");
|
||||||
|
const layoutData = await screenApi.getLayout(screenId);
|
||||||
|
|
||||||
|
if (layoutData && layoutData.components) {
|
||||||
|
// 레이아웃에서 universal-form-modal 컴포넌트 찾기
|
||||||
|
const modalLayout = (layoutData.components as any[]).find(
|
||||||
|
(comp) =>
|
||||||
|
comp.properties?.columnName === universalFormModalKey || comp.columnName === universalFormModalKey,
|
||||||
|
);
|
||||||
|
if (modalLayout) {
|
||||||
|
modalComponentConfig = modalLayout.properties?.componentConfig || modalLayout.componentConfig;
|
||||||
|
console.log(
|
||||||
|
"🎯 [handleUniversalFormModalTableSectionSave] 화면 레이아웃에서 설정 찾음:",
|
||||||
|
modalLayout.componentId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("⚠️ [handleUniversalFormModalTableSectionSave] 화면 레이아웃 조회 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections: any[] = modalComponentConfig?.sections || [];
|
||||||
|
const saveConfig = modalComponentConfig?.saveConfig || {};
|
||||||
|
|
||||||
|
console.log("🎯 [handleUniversalFormModalTableSectionSave] 컴포넌트 설정:", {
|
||||||
|
hasComponentConfig: !!modalComponentConfig,
|
||||||
|
sectionsCount: sections.length,
|
||||||
|
mainTableName: saveConfig.tableName || tableName,
|
||||||
|
sectionSaveModes: saveConfig.sectionSaveModes,
|
||||||
|
sectionDetails: sections.map((s: any) => ({
|
||||||
|
id: s.id,
|
||||||
|
type: s.type,
|
||||||
|
targetTable: s.tableConfig?.saveConfig?.targetTable,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
// _tableSection_ 데이터 추출
|
// _tableSection_ 데이터 추출
|
||||||
const tableSectionData: Record<string, any[]> = {};
|
const tableSectionData: Record<string, any[]> = {};
|
||||||
const commonFieldsData: Record<string, any> = {};
|
const commonFieldsData: Record<string, any> = {};
|
||||||
|
|
@ -1564,10 +1624,64 @@ export class ButtonActionExecutor {
|
||||||
let insertedCount = 0;
|
let insertedCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
|
let mainRecordId: number | null = null;
|
||||||
|
|
||||||
|
// 🆕 먼저 메인 테이블에 공통 데이터 저장 (별도 테이블이 있는 경우에만)
|
||||||
|
const hasSeparateTargetTable = sections.some(
|
||||||
|
(s) =>
|
||||||
|
s.type === "table" &&
|
||||||
|
s.tableConfig?.saveConfig?.targetTable &&
|
||||||
|
s.tableConfig.saveConfig.targetTable !== tableName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasSeparateTargetTable && Object.keys(commonFieldsData).length > 0) {
|
||||||
|
console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블에 공통 데이터 저장:", tableName);
|
||||||
|
|
||||||
|
const mainRowToSave = { ...commonFieldsData, ...userInfo };
|
||||||
|
|
||||||
|
// 메타데이터 제거
|
||||||
|
Object.keys(mainRowToSave).forEach((key) => {
|
||||||
|
if (key.startsWith("_")) {
|
||||||
|
delete mainRowToSave[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave);
|
||||||
|
|
||||||
|
const mainSaveResult = await DynamicFormApi.saveFormData({
|
||||||
|
screenId: screenId!,
|
||||||
|
tableName: tableName!,
|
||||||
|
data: mainRowToSave,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mainSaveResult.success) {
|
||||||
|
throw new Error(mainSaveResult.message || "메인 데이터 저장 실패");
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRecordId = mainSaveResult.data?.id || null;
|
||||||
|
console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 완료, ID:", mainRecordId);
|
||||||
|
}
|
||||||
|
|
||||||
// 각 테이블 섹션 처리
|
// 각 테이블 섹션 처리
|
||||||
for (const [sectionId, currentItems] of Object.entries(tableSectionData)) {
|
for (const [sectionId, currentItems] of Object.entries(tableSectionData)) {
|
||||||
console.log(`🔄 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 처리 시작: ${currentItems.length}개 품목`);
|
console.log(
|
||||||
|
`🔄 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 처리 시작: ${currentItems.length}개 품목`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🆕 해당 섹션의 설정 찾기
|
||||||
|
const sectionConfig = sections.find((s) => s.id === sectionId);
|
||||||
|
const targetTableName = sectionConfig?.tableConfig?.saveConfig?.targetTable;
|
||||||
|
|
||||||
|
// 🆕 실제 저장할 테이블 결정
|
||||||
|
// - targetTable이 있으면 해당 테이블에 저장
|
||||||
|
// - targetTable이 없으면 메인 테이블에 저장
|
||||||
|
const saveTableName = targetTableName || tableName!;
|
||||||
|
|
||||||
|
console.log(`📊 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 저장 테이블:`, {
|
||||||
|
targetTableName,
|
||||||
|
saveTableName,
|
||||||
|
isMainTable: saveTableName === tableName,
|
||||||
|
});
|
||||||
|
|
||||||
// 1️⃣ 신규 품목 INSERT (id가 없는 항목)
|
// 1️⃣ 신규 품목 INSERT (id가 없는 항목)
|
||||||
const newItems = currentItems.filter((item) => !item.id);
|
const newItems = currentItems.filter((item) => !item.id);
|
||||||
|
|
@ -1581,11 +1695,16 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("➕ [INSERT] 신규 품목:", rowToSave);
|
// 🆕 메인 레코드 ID 연결 (별도 테이블에 저장하는 경우)
|
||||||
|
if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) {
|
||||||
|
rowToSave[saveConfig.primaryKeyColumn] = mainRecordId;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("➕ [INSERT] 신규 품목:", { tableName: saveTableName, data: rowToSave });
|
||||||
|
|
||||||
const saveResult = await DynamicFormApi.saveFormData({
|
const saveResult = await DynamicFormApi.saveFormData({
|
||||||
screenId: screenId!,
|
screenId: screenId!,
|
||||||
tableName: tableName!,
|
tableName: saveTableName,
|
||||||
data: rowToSave,
|
data: rowToSave,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1612,9 +1731,14 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
delete rowToSave.id; // id 제거하여 INSERT
|
delete rowToSave.id; // id 제거하여 INSERT
|
||||||
|
|
||||||
|
// 🆕 메인 레코드 ID 연결 (별도 테이블에 저장하는 경우)
|
||||||
|
if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) {
|
||||||
|
rowToSave[saveConfig.primaryKeyColumn] = mainRecordId;
|
||||||
|
}
|
||||||
|
|
||||||
const saveResult = await DynamicFormApi.saveFormData({
|
const saveResult = await DynamicFormApi.saveFormData({
|
||||||
screenId: screenId!,
|
screenId: screenId!,
|
||||||
tableName: tableName!,
|
tableName: saveTableName,
|
||||||
data: rowToSave,
|
data: rowToSave,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1631,14 +1755,14 @@ export class ButtonActionExecutor {
|
||||||
const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon);
|
const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon);
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
console.log(`🔄 [UPDATE] 품목 수정: id=${item.id}`);
|
console.log(`🔄 [UPDATE] 품목 수정: id=${item.id}, tableName=${saveTableName}`);
|
||||||
|
|
||||||
// 변경된 필드만 추출하여 부분 업데이트
|
// 변경된 필드만 추출하여 부분 업데이트
|
||||||
const updateResult = await DynamicFormApi.updateFormDataPartial(
|
const updateResult = await DynamicFormApi.updateFormDataPartial(
|
||||||
item.id,
|
item.id,
|
||||||
originalItem,
|
originalItem,
|
||||||
currentDataWithCommon,
|
currentDataWithCommon,
|
||||||
tableName!,
|
saveTableName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!updateResult.success) {
|
if (!updateResult.success) {
|
||||||
|
|
@ -1656,9 +1780,9 @@ export class ButtonActionExecutor {
|
||||||
const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(orig.id));
|
const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(orig.id));
|
||||||
|
|
||||||
for (const deletedItem of deletedItems) {
|
for (const deletedItem of deletedItems) {
|
||||||
console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}`);
|
console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}, tableName=${saveTableName}`);
|
||||||
|
|
||||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(tableName!, deletedItem.id);
|
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(saveTableName, deletedItem.id);
|
||||||
|
|
||||||
if (!deleteResult.success) {
|
if (!deleteResult.success) {
|
||||||
throw new Error(deleteResult.message || "품목 삭제 실패");
|
throw new Error(deleteResult.message || "품목 삭제 실패");
|
||||||
|
|
@ -1670,6 +1794,7 @@ export class ButtonActionExecutor {
|
||||||
|
|
||||||
// 결과 메시지 생성
|
// 결과 메시지 생성
|
||||||
const resultParts: string[] = [];
|
const resultParts: string[] = [];
|
||||||
|
if (mainRecordId) resultParts.push("메인 데이터 저장");
|
||||||
if (insertedCount > 0) resultParts.push(`${insertedCount}개 추가`);
|
if (insertedCount > 0) resultParts.push(`${insertedCount}개 추가`);
|
||||||
if (updatedCount > 0) resultParts.push(`${updatedCount}개 수정`);
|
if (updatedCount > 0) resultParts.push(`${updatedCount}개 수정`);
|
||||||
if (deletedCount > 0) resultParts.push(`${deletedCount}개 삭제`);
|
if (deletedCount > 0) resultParts.push(`${deletedCount}개 삭제`);
|
||||||
|
|
@ -2145,7 +2270,10 @@ export class ButtonActionExecutor {
|
||||||
* 연관 데이터 버튼의 선택 데이터로 모달 열기
|
* 연관 데이터 버튼의 선택 데이터로 모달 열기
|
||||||
* RelatedDataButtons 컴포넌트에서 선택된 버튼 데이터를 모달로 전달
|
* RelatedDataButtons 컴포넌트에서 선택된 버튼 데이터를 모달로 전달
|
||||||
*/
|
*/
|
||||||
private static async handleOpenRelatedModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleOpenRelatedModal(
|
||||||
|
config: ButtonActionConfig,
|
||||||
|
context: ButtonActionContext,
|
||||||
|
): Promise<boolean> {
|
||||||
// 버튼 설정에서 targetScreenId 가져오기 (여러 위치에서 확인)
|
// 버튼 설정에서 targetScreenId 가져오기 (여러 위치에서 확인)
|
||||||
const targetScreenId = config.relatedModalConfig?.targetScreenId || config.targetScreenId;
|
const targetScreenId = config.relatedModalConfig?.targetScreenId || config.targetScreenId;
|
||||||
|
|
||||||
|
|
@ -2188,7 +2316,7 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (relatedConfig?.modalLink?.dataMapping && relatedConfig.modalLink.dataMapping.length > 0) {
|
if (relatedConfig?.modalLink?.dataMapping && relatedConfig.modalLink.dataMapping.length > 0) {
|
||||||
relatedConfig.modalLink.dataMapping.forEach(mapping => {
|
relatedConfig.modalLink.dataMapping.forEach((mapping) => {
|
||||||
console.log("🔍 [openRelatedModal] 매핑 처리:", {
|
console.log("🔍 [openRelatedModal] 매핑 처리:", {
|
||||||
mapping,
|
mapping,
|
||||||
sourceField: mapping.sourceField,
|
sourceField: mapping.sourceField,
|
||||||
|
|
@ -2219,7 +2347,8 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 모달 열기 이벤트 발생 (ScreenModal은 editData를 사용)
|
// 모달 열기 이벤트 발생 (ScreenModal은 editData를 사용)
|
||||||
window.dispatchEvent(new CustomEvent("openScreenModal", {
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("openScreenModal", {
|
||||||
detail: {
|
detail: {
|
||||||
screenId: targetScreenId,
|
screenId: targetScreenId,
|
||||||
title: config.modalTitle,
|
title: config.modalTitle,
|
||||||
|
|
@ -2230,7 +2359,8 @@ export class ButtonActionExecutor {
|
||||||
window.dispatchEvent(new CustomEvent("refreshTableData"));
|
window.dispatchEvent(new CustomEvent("refreshTableData"));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -3296,10 +3426,7 @@ export class ButtonActionExecutor {
|
||||||
* EditModal 등 외부에서도 호출 가능하도록 public으로 변경
|
* EditModal 등 외부에서도 호출 가능하도록 public으로 변경
|
||||||
* 다중 제어 순차 실행 지원
|
* 다중 제어 순차 실행 지원
|
||||||
*/
|
*/
|
||||||
public static async executeAfterSaveControl(
|
public static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<void> {
|
||||||
config: ButtonActionConfig,
|
|
||||||
context: ButtonActionContext,
|
|
||||||
): Promise<void> {
|
|
||||||
console.log("🎯 저장 후 제어 실행:", {
|
console.log("🎯 저장 후 제어 실행:", {
|
||||||
enableDataflowControl: config.enableDataflowControl,
|
enableDataflowControl: config.enableDataflowControl,
|
||||||
dataflowConfig: config.dataflowConfig,
|
dataflowConfig: config.dataflowConfig,
|
||||||
|
|
@ -4763,19 +4890,20 @@ export class ButtonActionExecutor {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
try {
|
try {
|
||||||
const { apiClient } = await import("@/lib/api/client");
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
const statusTableName = config.trackingStatusTableName || this.trackingConfig?.trackingStatusTableName || context.tableName || "vehicles";
|
const statusTableName =
|
||||||
|
config.trackingStatusTableName ||
|
||||||
|
this.trackingConfig?.trackingStatusTableName ||
|
||||||
|
context.tableName ||
|
||||||
|
"vehicles";
|
||||||
const keyField = config.trackingStatusKeyField || this.trackingConfig?.trackingStatusKeyField || "user_id";
|
const keyField = config.trackingStatusKeyField || this.trackingConfig?.trackingStatusKeyField || "user_id";
|
||||||
|
|
||||||
// DB에서 현재 차량 정보 조회
|
// DB에서 현재 차량 정보 조회
|
||||||
const vehicleResponse = await apiClient.post(
|
const vehicleResponse = await apiClient.post(`/table-management/tables/${statusTableName}/data`, {
|
||||||
`/table-management/tables/${statusTableName}/data`,
|
|
||||||
{
|
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 1,
|
size: 1,
|
||||||
search: { [keyField]: userId },
|
search: { [keyField]: userId },
|
||||||
autoFilter: true,
|
autoFilter: true,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const vehicleData = vehicleResponse.data?.data?.data?.[0] || vehicleResponse.data?.data?.rows?.[0];
|
const vehicleData = vehicleResponse.data?.data?.data?.[0] || vehicleResponse.data?.data?.rows?.[0];
|
||||||
if (vehicleData) {
|
if (vehicleData) {
|
||||||
|
|
@ -4792,14 +4920,18 @@ export class ButtonActionExecutor {
|
||||||
// 마지막 위치 저장 (추적 중이었던 경우에만)
|
// 마지막 위치 저장 (추적 중이었던 경우에만)
|
||||||
if (isTrackingActive) {
|
if (isTrackingActive) {
|
||||||
// DB 값 우선, 없으면 formData 사용
|
// DB 값 우선, 없으면 formData 사용
|
||||||
const departure = dbDeparture ||
|
const departure =
|
||||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || null;
|
dbDeparture ||
|
||||||
const arrival = dbArrival ||
|
this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] ||
|
||||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null;
|
null;
|
||||||
|
const arrival =
|
||||||
|
dbArrival || this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null;
|
||||||
const departureName = this.trackingContext?.formData?.["departure_name"] || null;
|
const departureName = this.trackingContext?.formData?.["departure_name"] || null;
|
||||||
const destinationName = this.trackingContext?.formData?.["destination_name"] || null;
|
const destinationName = this.trackingContext?.formData?.["destination_name"] || null;
|
||||||
const vehicleId = dbVehicleId ||
|
const vehicleId =
|
||||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null;
|
dbVehicleId ||
|
||||||
|
this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] ||
|
||||||
|
null;
|
||||||
|
|
||||||
await this.saveLocationToHistory(
|
await this.saveLocationToHistory(
|
||||||
tripId,
|
tripId,
|
||||||
|
|
@ -5681,10 +5813,10 @@ export class ButtonActionExecutor {
|
||||||
const columnMappings = quickInsertConfig.columnMappings || [];
|
const columnMappings = quickInsertConfig.columnMappings || [];
|
||||||
|
|
||||||
for (const mapping of columnMappings) {
|
for (const mapping of columnMappings) {
|
||||||
console.log(`📍 매핑 처리 시작:`, mapping);
|
console.log("📍 매핑 처리 시작:", mapping);
|
||||||
|
|
||||||
if (!mapping.targetColumn) {
|
if (!mapping.targetColumn) {
|
||||||
console.log(`📍 targetColumn 없음, 스킵`);
|
console.log("📍 targetColumn 없음, 스킵");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5692,7 +5824,7 @@ export class ButtonActionExecutor {
|
||||||
|
|
||||||
switch (mapping.sourceType) {
|
switch (mapping.sourceType) {
|
||||||
case "component":
|
case "component":
|
||||||
console.log(`📍 component 타입 처리:`, {
|
console.log("📍 component 타입 처리:", {
|
||||||
sourceComponentId: mapping.sourceComponentId,
|
sourceComponentId: mapping.sourceComponentId,
|
||||||
sourceColumnName: mapping.sourceColumnName,
|
sourceColumnName: mapping.sourceColumnName,
|
||||||
targetColumn: mapping.targetColumn,
|
targetColumn: mapping.targetColumn,
|
||||||
|
|
@ -5715,7 +5847,7 @@ export class ButtonActionExecutor {
|
||||||
// 3. 없으면 allComponents에서 컴포넌트를 찾아 columnName으로 시도
|
// 3. 없으면 allComponents에서 컴포넌트를 찾아 columnName으로 시도
|
||||||
if (value === undefined && context.allComponents) {
|
if (value === undefined && context.allComponents) {
|
||||||
const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
||||||
console.log(`📍 방법3 찾은 컴포넌트:`, comp);
|
console.log("📍 방법3 찾은 컴포넌트:", comp);
|
||||||
if (comp?.columnName) {
|
if (comp?.columnName) {
|
||||||
value = formData?.[comp.columnName];
|
value = formData?.[comp.columnName];
|
||||||
console.log(`📍 방법3 (allComponents): ${mapping.sourceComponentId} → ${comp.columnName} = ${value}`);
|
console.log(`📍 방법3 (allComponents): ${mapping.sourceComponentId} → ${comp.columnName} = ${value}`);
|
||||||
|
|
@ -5742,7 +5874,7 @@ export class ButtonActionExecutor {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "leftPanel":
|
case "leftPanel":
|
||||||
console.log(`📍 leftPanel 타입 처리:`, {
|
console.log("📍 leftPanel 타입 처리:", {
|
||||||
sourceColumn: mapping.sourceColumn,
|
sourceColumn: mapping.sourceColumn,
|
||||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||||
});
|
});
|
||||||
|
|
@ -5786,7 +5918,7 @@ export class ButtonActionExecutor {
|
||||||
insertData[mapping.targetColumn] = value;
|
insertData[mapping.targetColumn] = value;
|
||||||
console.log(`📍 insertData에 추가됨: ${mapping.targetColumn} = ${value}`);
|
console.log(`📍 insertData에 추가됨: ${mapping.targetColumn} = ${value}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`📍 값이 비어있어서 insertData에 추가 안됨`);
|
console.log("📍 값이 비어있어서 insertData에 추가 안됨");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5799,7 +5931,7 @@ export class ButtonActionExecutor {
|
||||||
let targetTableColumns: string[] = [];
|
let targetTableColumns: string[] = [];
|
||||||
try {
|
try {
|
||||||
const columnsResponse = await apiClient.get(
|
const columnsResponse = await apiClient.get(
|
||||||
`/table-management/tables/${quickInsertConfig.targetTable}/columns`
|
`/table-management/tables/${quickInsertConfig.targetTable}/columns`,
|
||||||
);
|
);
|
||||||
if (columnsResponse.data?.success && columnsResponse.data?.data) {
|
if (columnsResponse.data?.success && columnsResponse.data?.data) {
|
||||||
const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data;
|
const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data;
|
||||||
|
|
@ -5824,20 +5956,20 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 시스템 컬럼 제외 (id, created_date, updated_date, writer 등)
|
// 시스템 컬럼 제외 (id, created_date, updated_date, writer 등)
|
||||||
const systemColumns = ['id', 'created_date', 'updated_date', 'writer', 'writer_name'];
|
const systemColumns = ["id", "created_date", "updated_date", "writer", "writer_name"];
|
||||||
if (systemColumns.includes(key)) {
|
if (systemColumns.includes(key)) {
|
||||||
console.log(`📍 자동 매핑 스킵 (시스템 컬럼): ${key}`);
|
console.log(`📍 자동 매핑 스킵 (시스템 컬럼): ${key}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// _label, _name 으로 끝나는 표시용 컬럼 제외
|
// _label, _name 으로 끝나는 표시용 컬럼 제외
|
||||||
if (key.endsWith('_label') || key.endsWith('_name')) {
|
if (key.endsWith("_label") || key.endsWith("_name")) {
|
||||||
console.log(`📍 자동 매핑 스킵 (표시용 컬럼): ${key}`);
|
console.log(`📍 자동 매핑 스킵 (표시용 컬럼): ${key}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 값이 있으면 자동 추가
|
// 값이 있으면 자동 추가
|
||||||
if (val !== undefined && val !== null && val !== '') {
|
if (val !== undefined && val !== null && val !== "") {
|
||||||
insertData[key] = val;
|
insertData[key] = val;
|
||||||
console.log(`📍 자동 매핑 추가: ${key} = ${val}`);
|
console.log(`📍 자동 매핑 추가: ${key} = ${val}`);
|
||||||
}
|
}
|
||||||
|
|
@ -5877,14 +6009,19 @@ export class ButtonActionExecutor {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 1,
|
pageSize: 1,
|
||||||
search: duplicateCheckData,
|
search: duplicateCheckData,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("📍 중복 체크 응답:", checkResponse.data);
|
console.log("📍 중복 체크 응답:", checkResponse.data);
|
||||||
|
|
||||||
// 응답 구조: { success: true, data: { data: [...], total: N } } 또는 { success: true, data: [...] }
|
// 응답 구조: { success: true, data: { data: [...], total: N } } 또는 { success: true, data: [...] }
|
||||||
const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || [];
|
const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || [];
|
||||||
console.log("📍 기존 데이터:", existingData, "길이:", Array.isArray(existingData) ? existingData.length : 0);
|
console.log(
|
||||||
|
"📍 기존 데이터:",
|
||||||
|
existingData,
|
||||||
|
"길이:",
|
||||||
|
Array.isArray(existingData) ? existingData.length : 0,
|
||||||
|
);
|
||||||
|
|
||||||
if (Array.isArray(existingData) && existingData.length > 0) {
|
if (Array.isArray(existingData) && existingData.length > 0) {
|
||||||
toast.error(quickInsertConfig.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다.");
|
toast.error(quickInsertConfig.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다.");
|
||||||
|
|
@ -5902,7 +6039,7 @@ export class ButtonActionExecutor {
|
||||||
// 데이터 저장
|
// 데이터 저장
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(
|
||||||
`/table-management/tables/${quickInsertConfig.targetTable}/add`,
|
`/table-management/tables/${quickInsertConfig.targetTable}/add`,
|
||||||
insertData
|
insertData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data?.success) {
|
if (response.data?.success) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue