Merge pull request 'feature/screen-management' (#215) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/215
This commit is contained in:
commit
3aee36515a
|
|
@ -1227,18 +1227,24 @@ class DataService {
|
|||
|
||||
// 새 레코드 처리 (INSERT or UPDATE)
|
||||
for (const newRecord of records) {
|
||||
console.log(`🔍 처리할 새 레코드:`, newRecord);
|
||||
|
||||
// 날짜 필드 정규화
|
||||
const normalizedRecord: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(newRecord)) {
|
||||
normalizedRecord[key] = normalizeDateValue(value);
|
||||
}
|
||||
|
||||
console.log(`🔄 정규화된 레코드:`, normalizedRecord);
|
||||
|
||||
// 전체 레코드 데이터 (parentKeys + normalizedRecord)
|
||||
const fullRecord = { ...parentKeys, ...normalizedRecord };
|
||||
|
||||
// 고유 키: parentKeys 제외한 나머지 필드들
|
||||
const uniqueFields = Object.keys(normalizedRecord);
|
||||
|
||||
console.log(`🔑 고유 필드들:`, uniqueFields);
|
||||
|
||||
// 기존 레코드에서 일치하는 것 찾기
|
||||
const existingRecord = existingRecords.rows.find((existing) => {
|
||||
return uniqueFields.every((field) => {
|
||||
|
|
|
|||
|
|
@ -134,23 +134,32 @@ export class EntityJoinService {
|
|||
`🔧 기존 display_column 사용: ${column.column_name} → ${displayColumn}`
|
||||
);
|
||||
} else {
|
||||
// display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정
|
||||
let defaultDisplayColumn = referenceColumn;
|
||||
if (referenceTable === "dept_info") {
|
||||
defaultDisplayColumn = "dept_name";
|
||||
} else if (referenceTable === "company_info") {
|
||||
defaultDisplayColumn = "company_name";
|
||||
} else if (referenceTable === "user_info") {
|
||||
defaultDisplayColumn = "user_name";
|
||||
} else if (referenceTable === "category_values") {
|
||||
defaultDisplayColumn = "category_name";
|
||||
}
|
||||
// display_column이 "none"이거나 없는 경우 참조 테이블의 모든 컬럼 가져오기
|
||||
logger.info(`🔍 ${referenceTable}의 모든 컬럼 조회 중...`);
|
||||
|
||||
displayColumns = [defaultDisplayColumn];
|
||||
logger.info(
|
||||
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${referenceTable})`
|
||||
// 참조 테이블의 모든 컬럼 이름 가져오기
|
||||
const tableColumnsResult = await query<{ column_name: string }>(
|
||||
`SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1
|
||||
AND table_schema = 'public'
|
||||
ORDER BY ordinal_position`,
|
||||
[referenceTable]
|
||||
);
|
||||
logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns);
|
||||
|
||||
if (tableColumnsResult.length > 0) {
|
||||
displayColumns = tableColumnsResult.map((col) => col.column_name);
|
||||
logger.info(
|
||||
`✅ ${referenceTable}의 모든 컬럼 자동 포함 (${displayColumns.length}개):`,
|
||||
displayColumns.join(", ")
|
||||
);
|
||||
} else {
|
||||
// 테이블 컬럼을 못 찾으면 기본값 사용
|
||||
displayColumns = [referenceColumn];
|
||||
logger.warn(
|
||||
`⚠️ ${referenceTable}의 컬럼 조회 실패, 기본값 사용: ${referenceColumn}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 별칭 컬럼명 생성 (writer -> writer_name)
|
||||
|
|
@ -346,25 +355,26 @@ export class EntityJoinService {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
// 여러 컬럼인 경우 CONCAT으로 연결
|
||||
// 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리
|
||||
const concatParts = displayColumns
|
||||
.map((col) => {
|
||||
// ✅ 개선: referenceTable이 설정되어 있으면 조인 테이블에서 가져옴
|
||||
const isJoinTableColumn =
|
||||
config.referenceTable && config.referenceTable !== tableName;
|
||||
// 🆕 여러 컬럼인 경우 각 컬럼을 개별 alias로 반환 (합치지 않음)
|
||||
// 예: item_info.standard_price → sourceColumn_standard_price (item_id_standard_price)
|
||||
displayColumns.forEach((col) => {
|
||||
const isJoinTableColumn =
|
||||
config.referenceTable && config.referenceTable !== tableName;
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
// 조인 테이블 컬럼은 조인 별칭 사용
|
||||
return `COALESCE(${alias}.${col}::TEXT, '')`;
|
||||
} else {
|
||||
// 기본 테이블 컬럼은 main 별칭 사용
|
||||
return `COALESCE(main.${col}::TEXT, '')`;
|
||||
}
|
||||
})
|
||||
.join(` || '${separator}' || `);
|
||||
const individualAlias = `${config.sourceColumn}_${col}`;
|
||||
|
||||
resultColumns.push(`(${concatParts}) AS ${config.aliasColumn}`);
|
||||
if (isJoinTableColumn) {
|
||||
// 조인 테이블 컬럼은 조인 별칭 사용
|
||||
resultColumns.push(
|
||||
`COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}`
|
||||
);
|
||||
} else {
|
||||
// 기본 테이블 컬럼은 main 별칭 사용
|
||||
resultColumns.push(
|
||||
`COALESCE(main.${col}::TEXT, '') AS ${individualAlias}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용)
|
||||
const isJoinTableColumn =
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ interface ScreenModalProps {
|
|||
}
|
||||
|
||||
export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||
const { userId } = useAuth();
|
||||
const { userId, userName, user } = useAuth();
|
||||
|
||||
const [modalState, setModalState] = useState<ScreenModalState>({
|
||||
isOpen: false,
|
||||
|
|
@ -262,7 +262,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
// 🆕 apiClient를 named import로 가져오기
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const params: any = {
|
||||
enableEntityJoin: true,
|
||||
enableEntityJoin: true, // 엔티티 조인 활성화 (모든 엔티티 컬럼 자동 포함)
|
||||
};
|
||||
if (groupByColumns.length > 0) {
|
||||
params.groupByColumns = JSON.stringify(groupByColumns);
|
||||
|
|
@ -325,7 +325,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
console.log("📥 [ScreenModal] API 응답 원본:", JSON.stringify(response.data, null, 2));
|
||||
const normalizedData = normalizeDates(response.data);
|
||||
console.log("📥 [ScreenModal] 정규화 후:", JSON.stringify(normalizedData, null, 2));
|
||||
setFormData(normalizedData);
|
||||
|
||||
// 🔧 배열 데이터는 formData로 설정하지 않음 (SelectedItemsDetailInput만 사용)
|
||||
if (Array.isArray(normalizedData)) {
|
||||
console.log("⚠️ [ScreenModal] 그룹 레코드(배열)는 formData로 설정하지 않음. SelectedItemsDetailInput만 사용합니다.");
|
||||
setFormData(normalizedData); // SelectedItemsDetailInput이 직접 사용
|
||||
} else {
|
||||
setFormData(normalizedData);
|
||||
}
|
||||
|
||||
// setFormData 직후 확인
|
||||
console.log("🔄 setFormData 호출 완료 (날짜 정규화됨)");
|
||||
|
|
@ -580,6 +587,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
id: modalState.screenId!,
|
||||
tableName: screenData.screenInfo?.tableName,
|
||||
}}
|
||||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={user?.companyCode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ interface InteractiveScreenViewerProps {
|
|||
onSave?: () => Promise<void>;
|
||||
onRefresh?: () => void;
|
||||
onFlowRefresh?: () => void;
|
||||
// 🆕 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용)
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
companyCode?: string;
|
||||
}
|
||||
|
||||
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
||||
|
|
@ -54,9 +58,24 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
onSave,
|
||||
onRefresh,
|
||||
onFlowRefresh,
|
||||
userId: externalUserId,
|
||||
userName: externalUserName,
|
||||
companyCode: externalCompanyCode,
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const { userName, user } = useAuth();
|
||||
const { userName: authUserName, user: authUser } = useAuth();
|
||||
|
||||
// 외부에서 전달받은 사용자 정보가 있으면 우선 사용 (ScreenModal 등에서)
|
||||
const userName = externalUserName || authUserName;
|
||||
const user =
|
||||
externalUserId && externalUserId !== authUser?.userId
|
||||
? {
|
||||
userId: externalUserId,
|
||||
userName: externalUserName || authUserName || "",
|
||||
companyCode: externalCompanyCode || authUser?.companyCode || "",
|
||||
isAdmin: authUser?.isAdmin || false,
|
||||
}
|
||||
: authUser;
|
||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||
|
||||
|
|
@ -130,59 +149,55 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
const handleEnterKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
|
||||
// 한글 조합 중이면 무시 (한글 입력 문제 방지)
|
||||
if ((e as any).isComposing || e.keyCode === 229) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// textarea는 제외 (여러 줄 입력)
|
||||
if (target.tagName === "TEXTAREA") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// input, select 등의 폼 요소에서만 작동
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "SELECT" ||
|
||||
target.getAttribute("role") === "combobox"
|
||||
) {
|
||||
if (target.tagName === "INPUT" || target.tagName === "SELECT" || target.getAttribute("role") === "combobox") {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// 모든 포커스 가능한 요소 찾기
|
||||
const focusableElements = document.querySelectorAll<HTMLElement>(
|
||||
'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])'
|
||||
'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])',
|
||||
);
|
||||
|
||||
|
||||
// 화면에 보이는 순서(Y 좌표 → X 좌표)대로 정렬
|
||||
const focusableArray = Array.from(focusableElements).sort((a, b) => {
|
||||
const rectA = a.getBoundingClientRect();
|
||||
const rectB = b.getBoundingClientRect();
|
||||
|
||||
|
||||
// Y 좌표 차이가 10px 이상이면 Y 좌표로 정렬 (위에서 아래로)
|
||||
if (Math.abs(rectA.top - rectB.top) > 10) {
|
||||
return rectA.top - rectB.top;
|
||||
}
|
||||
|
||||
|
||||
// 같은 줄이면 X 좌표로 정렬 (왼쪽에서 오른쪽으로)
|
||||
return rectA.left - rectB.left;
|
||||
});
|
||||
|
||||
|
||||
const currentIndex = focusableArray.indexOf(target);
|
||||
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < focusableArray.length - 1) {
|
||||
// 다음 요소로 포커스 이동
|
||||
const nextElement = focusableArray[currentIndex + 1];
|
||||
nextElement.focus();
|
||||
|
||||
|
||||
// select() 제거: 한글 입력 시 이전 필드의 마지막 글자가 복사되는 버그 방지
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
document.addEventListener("keydown", handleEnterKey);
|
||||
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEnterKey);
|
||||
};
|
||||
|
|
@ -193,31 +208,26 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
const initAutoInputFields = async () => {
|
||||
for (const comp of allComponents) {
|
||||
// type: "component" 또는 type: "widget" 모두 처리
|
||||
if (comp.type === 'widget' || comp.type === 'component') {
|
||||
if (comp.type === "widget" || comp.type === "component") {
|
||||
const widget = comp as any;
|
||||
const fieldName = widget.columnName || widget.id;
|
||||
|
||||
|
||||
// autoFill 처리 (테이블 조회 기반 자동 입력)
|
||||
if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) {
|
||||
const autoFillConfig = widget.autoFill || (comp as any).autoFill;
|
||||
const currentValue = formData[fieldName];
|
||||
|
||||
if (currentValue === undefined || currentValue === '') {
|
||||
|
||||
if (currentValue === undefined || currentValue === "") {
|
||||
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
|
||||
|
||||
|
||||
// 사용자 정보에서 필터 값 가져오기
|
||||
const userValue = user?.[userField];
|
||||
|
||||
|
||||
if (userValue && sourceTable && filterColumn && displayColumn) {
|
||||
try {
|
||||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||||
const result = await tableTypeApi.getTableRecord(
|
||||
sourceTable,
|
||||
filterColumn,
|
||||
userValue,
|
||||
displayColumn
|
||||
);
|
||||
|
||||
const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn);
|
||||
|
||||
updateFormData(fieldName, result.value);
|
||||
} catch (error) {
|
||||
console.error(`autoFill 조회 실패: ${fieldName}`, error);
|
||||
|
|
@ -329,10 +339,13 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
setFlowSelectedData(selectedData);
|
||||
setFlowSelectedStepId(stepId);
|
||||
}}
|
||||
onRefresh={onRefresh || (() => {
|
||||
// 부모로부터 전달받은 onRefresh 또는 기본 동작
|
||||
console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출");
|
||||
})}
|
||||
onRefresh={
|
||||
onRefresh ||
|
||||
(() => {
|
||||
// 부모로부터 전달받은 onRefresh 또는 기본 동작
|
||||
console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출");
|
||||
})
|
||||
}
|
||||
onFlowRefresh={onFlowRefresh}
|
||||
onClose={() => {
|
||||
// buttonActions.ts가 이미 처리함
|
||||
|
|
@ -357,7 +370,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
return React.cloneElement(element, {
|
||||
style: {
|
||||
...element.props.style,
|
||||
...styleWithoutSize, // width/height 제외한 스타일만 적용
|
||||
...styleWithoutSize, // width/height 제외한 스타일만 적용
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: "100%",
|
||||
|
|
@ -563,8 +576,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
backgroundColor: config?.backgroundColor || comp.style?.backgroundColor,
|
||||
color: config?.textColor || comp.style?.color,
|
||||
// 부모 컨테이너 크기에 맞춤
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{label || "버튼"}
|
||||
|
|
@ -689,18 +702,18 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
|
||||
// ✅ 격자 시스템 잔재 제거: style.width, style.height 무시
|
||||
const { width: styleWidth, height: styleHeight, ...styleWithoutSize } = style;
|
||||
|
||||
|
||||
// TableSearchWidget의 경우 높이를 자동으로 설정
|
||||
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
||||
|
||||
|
||||
const componentStyle = {
|
||||
position: "absolute" as const,
|
||||
left: position?.x || 0,
|
||||
top: position?.y || 0,
|
||||
zIndex: position?.z || 1,
|
||||
...styleWithoutSize, // width/height 제외한 스타일만 먼저 적용
|
||||
width: size?.width || 200, // size의 픽셀 값이 최종 우선순위
|
||||
height: isTableSearchWidget ? "auto" : (size?.height || 10),
|
||||
...styleWithoutSize, // width/height 제외한 스타일만 먼저 적용
|
||||
width: size?.width || 200, // size의 픽셀 값이 최종 우선순위
|
||||
height: isTableSearchWidget ? "auto" : size?.height || 10,
|
||||
minHeight: isTableSearchWidget ? "48px" : undefined,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
screenName: "",
|
||||
description: "",
|
||||
isActive: "Y",
|
||||
tableName: "",
|
||||
});
|
||||
const [tables, setTables] = useState<string[]>([]);
|
||||
const [loadingTables, setLoadingTables] = useState(false);
|
||||
|
||||
// 미리보기 관련 상태
|
||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||
|
|
@ -260,14 +263,31 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
onScreenSelect(screen);
|
||||
};
|
||||
|
||||
const handleEdit = (screen: ScreenDefinition) => {
|
||||
const handleEdit = async (screen: ScreenDefinition) => {
|
||||
setScreenToEdit(screen);
|
||||
setEditFormData({
|
||||
screenName: screen.screenName,
|
||||
description: screen.description || "",
|
||||
isActive: screen.isActive,
|
||||
tableName: screen.tableName || "",
|
||||
});
|
||||
setEditDialogOpen(true);
|
||||
|
||||
// 테이블 목록 로드
|
||||
try {
|
||||
setLoadingTables(true);
|
||||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||
const response = await tableManagementApi.getTableList();
|
||||
if (response.success && response.data) {
|
||||
// tableName만 추출 (camelCase)
|
||||
const tableNames = response.data.map((table: any) => table.tableName);
|
||||
setTables(tableNames);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("테이블 목록 조회 실패:", error);
|
||||
} finally {
|
||||
setLoadingTables(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSave = async () => {
|
||||
|
|
@ -1180,6 +1200,25 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
placeholder="화면명을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-tableName">테이블 *</Label>
|
||||
<Select
|
||||
value={editFormData.tableName}
|
||||
onValueChange={(value) => setEditFormData({ ...editFormData, tableName: value })}
|
||||
disabled={loadingTables}
|
||||
>
|
||||
<SelectTrigger id="edit-tableName">
|
||||
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((tableName) => (
|
||||
<SelectItem key={tableName} value={tableName}>
|
||||
{tableName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-description">설명</Label>
|
||||
<Textarea
|
||||
|
|
@ -1210,7 +1249,7 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleEditSave} disabled={!editFormData.screenName.trim()}>
|
||||
<Button onClick={handleEditSave} disabled={!editFormData.screenName.trim() || !editFormData.tableName.trim()}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
|
|||
import { Loader2 } from "lucide-react";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
/**
|
||||
* 조건부 섹션 뷰어 컴포넌트
|
||||
|
|
@ -24,6 +25,7 @@ export function ConditionalSectionViewer({
|
|||
formData,
|
||||
onFormDataChange,
|
||||
}: ConditionalSectionViewerProps) {
|
||||
const { userId, userName, user } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [components, setComponents] = useState<ComponentData[]>([]);
|
||||
const [screenInfo, setScreenInfo] = useState<{ id: number; tableName?: string } | null>(null);
|
||||
|
|
@ -142,6 +144,9 @@ export function ConditionalSectionViewer({
|
|||
onClick={() => {}}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={user?.companyCode}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -225,6 +225,7 @@ export class ButtonActionExecutor {
|
|||
|
||||
// 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조)
|
||||
console.log("🔍 [handleSave] formData 구조 확인:", {
|
||||
isFormDataArray: Array.isArray(context.formData),
|
||||
keys: Object.keys(context.formData),
|
||||
values: Object.entries(context.formData).map(([key, value]) => ({
|
||||
key,
|
||||
|
|
@ -238,6 +239,14 @@ export class ButtonActionExecutor {
|
|||
}))
|
||||
});
|
||||
|
||||
// 🔧 formData 자체가 배열인 경우 (ScreenModal의 그룹 레코드 수정)
|
||||
if (Array.isArray(context.formData)) {
|
||||
console.log("⚠️ [handleSave] formData가 배열입니다 - SelectedItemsDetailInput이 이미 처리했으므로 일반 저장 건너뜀");
|
||||
console.log("⚠️ [handleSave] formData 배열:", context.formData);
|
||||
// ✅ SelectedItemsDetailInput이 이미 UPSERT를 실행했으므로 일반 저장을 건너뜀
|
||||
return true; // 성공으로 반환
|
||||
}
|
||||
|
||||
const selectedItemsKeys = Object.keys(context.formData).filter(key => {
|
||||
const value = context.formData[key];
|
||||
console.log(`🔍 [handleSave] 필터링 체크 - ${key}:`, {
|
||||
|
|
|
|||
Loading…
Reference in New Issue