feat: 기간별 단가 설정 기능 구현 - 자동 계산 시스템
- 선택항목 상세입력 컴포넌트 확장 - 실시간 가격 계산 기능 추가 (할인율/할인금액, 반올림 방식) - 카테고리 값 기반 연산 매핑 시스템 - 3단계 드릴다운 방식 설정 UI (메뉴 → 카테고리 → 값 매핑) - 설정 가능한 계산 로직 - autoCalculation 설정으로 계산 필드명 동적 지정 - valueMapping으로 카테고리 코드와 연산 타입 매핑 - 할인 방식: none/rate/amount - 반올림 방식: none/round/floor/ceil - 반올림 단위: 1/10/100/1000 - UI 개선 - 입력 필드 가로 배치 (반응형 Grid) - 카테고리 타입 필드 옵션 로딩 개선 - 계산 결과 필드 자동 표시 및 읽기 전용 처리 - 날짜 입력 필드 네이티브 피커 지원 - API 연동 - 2레벨 메뉴 목록 조회 - 메뉴별 카테고리 컬럼 조회 - 카테고리별 값 목록 조회 - 문서화 - 기간별 단가 설정 가이드 작성
This commit is contained in:
@@ -41,6 +41,13 @@ export interface ButtonActionConfig {
|
||||
|
||||
// 모달/팝업 관련
|
||||
modalTitle?: string;
|
||||
modalTitleBlocks?: Array<{ // 🆕 블록 기반 제목 (우선순위 높음)
|
||||
id: string;
|
||||
type: "text" | "field";
|
||||
value: string; // type=text: 텍스트 내용, type=field: 컬럼명
|
||||
tableName?: string; // type=field일 때 테이블명
|
||||
label?: string; // type=field일 때 표시용 라벨
|
||||
}>;
|
||||
modalDescription?: string;
|
||||
modalSize?: "sm" | "md" | "lg" | "xl";
|
||||
popupWidth?: number;
|
||||
@@ -207,6 +214,20 @@ export class ButtonActionExecutor {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조)
|
||||
console.log("🔍 [handleSave] formData 구조 확인:", {
|
||||
keys: Object.keys(context.formData),
|
||||
values: Object.entries(context.formData).map(([key, value]) => ({
|
||||
key,
|
||||
isArray: Array.isArray(value),
|
||||
length: Array.isArray(value) ? value.length : 0,
|
||||
firstItem: Array.isArray(value) && value.length > 0 ? {
|
||||
hasOriginalData: !!value[0]?.originalData,
|
||||
hasFieldGroups: !!value[0]?.fieldGroups,
|
||||
keys: Object.keys(value[0] || {})
|
||||
} : null
|
||||
}))
|
||||
});
|
||||
|
||||
const selectedItemsKeys = Object.keys(context.formData).filter(key => {
|
||||
const value = context.formData[key];
|
||||
return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups;
|
||||
@@ -215,6 +236,8 @@ export class ButtonActionExecutor {
|
||||
if (selectedItemsKeys.length > 0) {
|
||||
console.log("🔄 [handleSave] SelectedItemsDetailInput 배치 저장 감지:", selectedItemsKeys);
|
||||
return await this.handleBatchSave(config, context, selectedItemsKeys);
|
||||
} else {
|
||||
console.log("⚠️ [handleSave] SelectedItemsDetailInput 데이터 감지 실패 - 일반 저장 진행");
|
||||
}
|
||||
|
||||
// 폼 유효성 검사
|
||||
@@ -830,11 +853,11 @@ export class ButtonActionExecutor {
|
||||
dataSourceId: config.dataSourceId,
|
||||
});
|
||||
|
||||
// 🆕 1. dataSourceId 자동 결정
|
||||
// 🆕 1. 현재 화면의 TableList 또는 SplitPanelLayout 자동 감지
|
||||
let dataSourceId = config.dataSourceId;
|
||||
|
||||
// dataSourceId가 없으면 같은 화면의 TableList 자동 감지
|
||||
if (!dataSourceId && context.allComponents) {
|
||||
// TableList 우선 감지
|
||||
const tableListComponent = context.allComponents.find(
|
||||
(comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName
|
||||
);
|
||||
@@ -845,6 +868,19 @@ export class ButtonActionExecutor {
|
||||
componentId: tableListComponent.id,
|
||||
tableName: dataSourceId,
|
||||
});
|
||||
} else {
|
||||
// TableList가 없으면 SplitPanelLayout의 좌측 패널 감지
|
||||
const splitPanelComponent = context.allComponents.find(
|
||||
(comp: any) => comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName
|
||||
);
|
||||
|
||||
if (splitPanelComponent) {
|
||||
dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName;
|
||||
console.log("✨ 분할 패널 좌측 테이블 자동 감지:", {
|
||||
componentId: splitPanelComponent.id,
|
||||
tableName: dataSourceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,21 +889,30 @@ export class ButtonActionExecutor {
|
||||
dataSourceId = context.tableName || "default";
|
||||
}
|
||||
|
||||
// 2. modalDataStore에서 데이터 확인
|
||||
// 🆕 2. modalDataStore에서 현재 선택된 데이터 확인
|
||||
try {
|
||||
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||
const modalData = useModalDataStore.getState().dataRegistry[dataSourceId] || [];
|
||||
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||
|
||||
const modalData = dataRegistry[dataSourceId] || [];
|
||||
|
||||
console.log("📊 현재 화면 데이터 확인:", {
|
||||
dataSourceId,
|
||||
count: modalData.length,
|
||||
allKeys: Object.keys(dataRegistry), // 🆕 전체 데이터 키 확인
|
||||
});
|
||||
|
||||
if (modalData.length === 0) {
|
||||
console.warn("⚠️ 전달할 데이터가 없습니다:", dataSourceId);
|
||||
console.warn("⚠️ 선택된 데이터가 없습니다:", dataSourceId);
|
||||
toast.warning("선택된 데이터가 없습니다. 먼저 항목을 선택해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("✅ 전달할 데이터:", {
|
||||
dataSourceId,
|
||||
count: modalData.length,
|
||||
data: modalData,
|
||||
console.log("✅ 모달 데이터 준비 완료:", {
|
||||
currentData: { id: dataSourceId, count: modalData.length },
|
||||
previousData: Object.entries(dataRegistry)
|
||||
.filter(([key]) => key !== dataSourceId)
|
||||
.map(([key, data]: [string, any]) => ({ id: key, count: data.length })),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ 데이터 확인 실패:", error);
|
||||
@@ -875,7 +920,79 @@ export class ButtonActionExecutor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 모달 열기 + URL 파라미터로 dataSourceId 전달
|
||||
// 6. 동적 모달 제목 생성
|
||||
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||
|
||||
let finalTitle = "데이터 입력";
|
||||
|
||||
// 🆕 블록 기반 제목 (우선순위 1)
|
||||
if (config.modalTitleBlocks && config.modalTitleBlocks.length > 0) {
|
||||
const titleParts: string[] = [];
|
||||
|
||||
config.modalTitleBlocks.forEach((block) => {
|
||||
if (block.type === "text") {
|
||||
// 텍스트 블록: 그대로 추가
|
||||
titleParts.push(block.value);
|
||||
} else if (block.type === "field") {
|
||||
// 필드 블록: 데이터에서 값 가져오기
|
||||
const tableName = block.tableName;
|
||||
const columnName = block.value;
|
||||
|
||||
if (tableName && columnName) {
|
||||
const tableData = dataRegistry[tableName];
|
||||
if (tableData && tableData.length > 0) {
|
||||
const firstItem = tableData[0].originalData || tableData[0];
|
||||
const value = firstItem[columnName];
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
titleParts.push(String(value));
|
||||
console.log(`✨ 동적 필드: ${tableName}.${columnName} → ${value}`);
|
||||
} else {
|
||||
// 데이터 없으면 라벨 표시
|
||||
titleParts.push(block.label || columnName);
|
||||
}
|
||||
} else {
|
||||
// 테이블 데이터 없으면 라벨 표시
|
||||
titleParts.push(block.label || columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
finalTitle = titleParts.join("");
|
||||
console.log("📋 블록 기반 제목 생성:", finalTitle);
|
||||
}
|
||||
// 기존 방식: {tableName.columnName} 패턴 (우선순위 2)
|
||||
else if (config.modalTitle) {
|
||||
finalTitle = config.modalTitle;
|
||||
|
||||
if (finalTitle.includes("{")) {
|
||||
const matches = finalTitle.match(/\{([^}]+)\}/g);
|
||||
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
const path = match.slice(1, -1); // {item_info.item_name} → item_info.item_name
|
||||
const [tableName, columnName] = path.split(".");
|
||||
|
||||
if (tableName && columnName) {
|
||||
const tableData = dataRegistry[tableName];
|
||||
if (tableData && tableData.length > 0) {
|
||||
const firstItem = tableData[0].originalData || tableData[0];
|
||||
const value = firstItem[columnName];
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
finalTitle = finalTitle.replace(match, String(value));
|
||||
console.log(`✨ 동적 제목: ${match} → ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 모달 열기 + URL 파라미터로 dataSourceId 전달
|
||||
if (config.targetScreenId) {
|
||||
// config에 modalDescription이 있으면 우선 사용
|
||||
let description = config.modalDescription || "";
|
||||
@@ -894,10 +1011,10 @@ export class ButtonActionExecutor {
|
||||
const modalEvent = new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
screenId: config.targetScreenId,
|
||||
title: config.modalTitle || "데이터 입력",
|
||||
title: finalTitle, // 🆕 동적 제목 사용
|
||||
description: description,
|
||||
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
|
||||
urlParams: { dataSourceId }, // 🆕 URL 파라미터로 dataSourceId 전달
|
||||
urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음)
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user