feature/v2-unified-renewal #379
|
|
@ -141,21 +141,12 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
selectedIds,
|
selectedIds,
|
||||||
} = event.detail;
|
} = event.detail;
|
||||||
|
|
||||||
console.log("📦 [ScreenModal] 모달 열기 이벤트 수신:", {
|
|
||||||
screenId,
|
|
||||||
title,
|
|
||||||
selectedData: eventSelectedData,
|
|
||||||
selectedIds,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🆕 모달 열린 시간 기록
|
// 🆕 모달 열린 시간 기록
|
||||||
modalOpenedAtRef.current = Date.now();
|
modalOpenedAtRef.current = Date.now();
|
||||||
console.log("🕐 [ScreenModal] 모달 열림 시간 기록:", modalOpenedAtRef.current);
|
|
||||||
|
|
||||||
// 🆕 선택된 데이터 저장 (RepeatScreenModal 등에서 사용)
|
// 🆕 선택된 데이터 저장 (RepeatScreenModal 등에서 사용)
|
||||||
if (eventSelectedData && Array.isArray(eventSelectedData)) {
|
if (eventSelectedData && Array.isArray(eventSelectedData)) {
|
||||||
setSelectedData(eventSelectedData);
|
setSelectedData(eventSelectedData);
|
||||||
console.log("📦 [ScreenModal] 선택된 데이터 저장:", eventSelectedData.length, "건");
|
|
||||||
} else {
|
} else {
|
||||||
setSelectedData([]);
|
setSelectedData([]);
|
||||||
}
|
}
|
||||||
|
|
@ -168,12 +159,10 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
});
|
});
|
||||||
// pushState로 URL 변경 (페이지 새로고침 없이)
|
// pushState로 URL 변경 (페이지 새로고침 없이)
|
||||||
window.history.pushState({}, "", currentUrl.toString());
|
window.history.pushState({}, "", currentUrl.toString());
|
||||||
console.log("✅ URL 파라미터 추가:", urlParams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 editData가 있으면 formData와 originalData로 설정 (수정 모드)
|
// 🆕 editData가 있으면 formData와 originalData로 설정 (수정 모드)
|
||||||
if (editData) {
|
if (editData) {
|
||||||
console.log("📝 [ScreenModal] 수정 데이터 설정:", editData);
|
|
||||||
setFormData(editData);
|
setFormData(editData);
|
||||||
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -204,9 +193,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const sourceValue = rawParentData[mapping.sourceColumn];
|
const sourceValue = rawParentData[mapping.sourceColumn];
|
||||||
if (sourceValue !== undefined && sourceValue !== null) {
|
if (sourceValue !== undefined && sourceValue !== null) {
|
||||||
parentData[mapping.targetColumn] = sourceValue;
|
parentData[mapping.targetColumn] = sourceValue;
|
||||||
console.log(
|
|
||||||
`🔗 [ScreenModal] 매핑 필드 전달: ${mapping.sourceColumn} → ${mapping.targetColumn} = ${sourceValue}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,13 +217,11 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
||||||
if (isLinkField) {
|
if (isLinkField) {
|
||||||
parentData[key] = value;
|
parentData[key] = value;
|
||||||
console.log(`🔗 [ScreenModal] 연결 필드 자동 감지: ${key} = ${value}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(parentData).length > 0) {
|
if (Object.keys(parentData).length > 0) {
|
||||||
console.log("🔗 [ScreenModal] 분할 패널 부모 데이터 초기값 설정 (연결 필드만):", parentData);
|
|
||||||
setFormData(parentData);
|
setFormData(parentData);
|
||||||
} else {
|
} else {
|
||||||
setFormData({});
|
setFormData({});
|
||||||
|
|
@ -261,7 +245,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// dataSourceId 파라미터 제거
|
// dataSourceId 파라미터 제거
|
||||||
currentUrl.searchParams.delete("dataSourceId");
|
currentUrl.searchParams.delete("dataSourceId");
|
||||||
window.history.pushState({}, "", currentUrl.toString());
|
window.history.pushState({}, "", currentUrl.toString());
|
||||||
console.log("🧹 URL 파라미터 제거");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalState({
|
setModalState({
|
||||||
|
|
@ -276,8 +259,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
setOriginalData(null); // 🆕 원본 데이터 초기화
|
setOriginalData(null); // 🆕 원본 데이터 초기화
|
||||||
setSelectedData([]); // 🆕 선택된 데이터 초기화
|
setSelectedData([]); // 🆕 선택된 데이터 초기화
|
||||||
setContinuousMode(false);
|
setContinuousMode(false);
|
||||||
localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장
|
localStorage.setItem("screenModal_continuousMode", "false");
|
||||||
console.log("🔄 연속 모드 초기화: false");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 저장 성공 이벤트 처리 (연속 등록 모드 지원)
|
// 저장 성공 이벤트 처리 (연속 등록 모드 지원)
|
||||||
|
|
@ -285,36 +267,24 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// 🆕 모달이 열린 후 500ms 이내의 저장 성공 이벤트는 무시 (이전 이벤트 방지)
|
// 🆕 모달이 열린 후 500ms 이내의 저장 성공 이벤트는 무시 (이전 이벤트 방지)
|
||||||
const timeSinceOpen = Date.now() - modalOpenedAtRef.current;
|
const timeSinceOpen = Date.now() - modalOpenedAtRef.current;
|
||||||
if (timeSinceOpen < 500) {
|
if (timeSinceOpen < 500) {
|
||||||
console.log("⏭️ [ScreenModal] 모달 열린 직후 저장 성공 이벤트 무시:", { timeSinceOpen });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isContinuousMode = continuousMode;
|
const isContinuousMode = continuousMode;
|
||||||
console.log("💾 저장 성공 이벤트 수신");
|
|
||||||
console.log("📌 현재 연속 모드 상태:", isContinuousMode);
|
|
||||||
console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode"));
|
|
||||||
|
|
||||||
if (isContinuousMode) {
|
if (isContinuousMode) {
|
||||||
// 연속 모드: 폼만 초기화하고 모달은 유지
|
// 연속 모드: 폼만 초기화하고 모달은 유지
|
||||||
console.log("✅ 연속 모드 활성화 - 폼 초기화 및 화면 리셋");
|
|
||||||
|
|
||||||
// 1. 폼 데이터 초기화
|
|
||||||
setFormData({});
|
setFormData({});
|
||||||
|
|
||||||
// 2. 리셋 키 변경 (컴포넌트 강제 리마운트)
|
|
||||||
setResetKey((prev) => prev + 1);
|
setResetKey((prev) => prev + 1);
|
||||||
console.log("🔄 resetKey 증가 - 컴포넌트 리마운트");
|
|
||||||
|
|
||||||
// 3. 화면 데이터 다시 로드 (채번 규칙 새로 생성)
|
// 화면 데이터 다시 로드 (채번 규칙 새로 생성)
|
||||||
if (modalState.screenId) {
|
if (modalState.screenId) {
|
||||||
console.log("🔄 화면 데이터 다시 로드:", modalState.screenId);
|
|
||||||
loadScreenData(modalState.screenId);
|
loadScreenData(modalState.screenId);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("저장되었습니다. 계속 입력하세요.");
|
toast.success("저장되었습니다. 계속 입력하세요.");
|
||||||
} else {
|
} else {
|
||||||
// 일반 모드: 모달 닫기
|
// 일반 모드: 모달 닫기
|
||||||
console.log("❌ 일반 모드 - 모달 닫기");
|
|
||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -341,16 +311,12 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
console.log("화면 데이터 로딩 시작:", screenId);
|
|
||||||
|
|
||||||
// 화면 정보와 레이아웃 데이터 로딩
|
// 화면 정보와 레이아웃 데이터 로딩
|
||||||
const [screenInfo, layoutData] = await Promise.all([
|
const [screenInfo, layoutData] = await Promise.all([
|
||||||
screenApi.getScreen(screenId),
|
screenApi.getScreen(screenId),
|
||||||
screenApi.getLayout(screenId),
|
screenApi.getLayout(screenId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log("API 응답:", { screenInfo, layoutData });
|
|
||||||
|
|
||||||
// 🆕 URL 파라미터 확인 (수정 모드)
|
// 🆕 URL 파라미터 확인 (수정 모드)
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
@ -359,36 +325,19 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const tableName = urlParams.get("tableName") || screenInfo.tableName;
|
const tableName = urlParams.get("tableName") || screenInfo.tableName;
|
||||||
const groupByColumnsParam = urlParams.get("groupByColumns");
|
const groupByColumnsParam = urlParams.get("groupByColumns");
|
||||||
|
|
||||||
console.log("📋 URL 파라미터 확인:", { mode, editId, tableName, groupByColumnsParam });
|
|
||||||
|
|
||||||
// 수정 모드이고 editId가 있으면 해당 레코드 조회
|
// 수정 모드이고 editId가 있으면 해당 레코드 조회
|
||||||
if (mode === "edit" && editId && tableName) {
|
if (mode === "edit" && editId && tableName) {
|
||||||
try {
|
try {
|
||||||
console.log("🔍 수정 데이터 조회 시작:", { tableName, editId, groupByColumnsParam });
|
|
||||||
|
|
||||||
const { dataApi } = await import("@/lib/api/data");
|
|
||||||
|
|
||||||
// groupByColumns 파싱
|
// groupByColumns 파싱
|
||||||
let groupByColumns: string[] = [];
|
let groupByColumns: string[] = [];
|
||||||
if (groupByColumnsParam) {
|
if (groupByColumnsParam) {
|
||||||
try {
|
try {
|
||||||
groupByColumns = JSON.parse(groupByColumnsParam);
|
groupByColumns = JSON.parse(groupByColumnsParam);
|
||||||
console.log("✅ [ScreenModal] groupByColumns 파싱 성공:", groupByColumns);
|
} catch {
|
||||||
} catch (e) {
|
// groupByColumns 파싱 실패 시 무시
|
||||||
console.warn("groupByColumns 파싱 실패:", e);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn("⚠️ [ScreenModal] groupByColumnsParam이 없습니다!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🚀 [ScreenModal] API 호출 직전:", {
|
|
||||||
tableName,
|
|
||||||
editId,
|
|
||||||
enableEntityJoin: true,
|
|
||||||
groupByColumns,
|
|
||||||
groupByColumnsLength: groupByColumns.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🆕 apiClient를 named import로 가져오기
|
// 🆕 apiClient를 named import로 가져오기
|
||||||
const { apiClient } = await import("@/lib/api/client");
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
const params: any = {
|
const params: any = {
|
||||||
|
|
@ -396,37 +345,12 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
};
|
};
|
||||||
if (groupByColumns.length > 0) {
|
if (groupByColumns.length > 0) {
|
||||||
params.groupByColumns = JSON.stringify(groupByColumns);
|
params.groupByColumns = JSON.stringify(groupByColumns);
|
||||||
console.log("✅ [ScreenModal] groupByColumns를 params에 추가:", params.groupByColumns);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("📡 [ScreenModal] 실제 API 요청:", {
|
|
||||||
url: `/data/${tableName}/${editId}`,
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params });
|
const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params });
|
||||||
const response = apiResponse.data;
|
const response = apiResponse.data;
|
||||||
|
|
||||||
console.log("📩 [ScreenModal] API 응답 받음:", {
|
|
||||||
success: response.success,
|
|
||||||
hasData: !!response.data,
|
|
||||||
dataType: response.data ? (Array.isArray(response.data) ? "배열" : "객체") : "없음",
|
|
||||||
dataLength: Array.isArray(response.data) ? response.data.length : 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// 배열인 경우 (그룹핑) vs 단일 객체
|
|
||||||
const isArray = Array.isArray(response.data);
|
|
||||||
|
|
||||||
if (isArray) {
|
|
||||||
console.log(`✅ 수정 데이터 로드 완료 (그룹 레코드: ${response.data.length}개)`);
|
|
||||||
console.log("📦 전체 데이터 (JSON):", JSON.stringify(response.data, null, 2));
|
|
||||||
} else {
|
|
||||||
console.log("✅ 수정 데이터 로드 완료 (필드 수:", Object.keys(response.data).length, ")");
|
|
||||||
console.log("📊 모든 필드 키:", Object.keys(response.data));
|
|
||||||
console.log("📦 전체 데이터 (JSON):", JSON.stringify(response.data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔧 날짜 필드 정규화 (타임존 제거)
|
// 🔧 날짜 필드 정규화 (타임존 제거)
|
||||||
const normalizeDates = (data: any): any => {
|
const normalizeDates = (data: any): any => {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
|
|
@ -441,10 +365,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
||||||
// ISO 날짜 형식 감지: YYYY-MM-DD만 추출
|
// ISO 날짜 형식 감지: YYYY-MM-DD만 추출
|
||||||
const before = value;
|
normalized[key] = value.split("T")[0];
|
||||||
const after = value.split("T")[0];
|
|
||||||
console.log(`🔧 [날짜 정규화] ${key}: ${before} → ${after}`);
|
|
||||||
normalized[key] = after;
|
|
||||||
} else {
|
} else {
|
||||||
normalized[key] = value;
|
normalized[key] = value;
|
||||||
}
|
}
|
||||||
|
|
@ -452,31 +373,21 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
return normalized;
|
return normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📥 [ScreenModal] API 응답 원본:", JSON.stringify(response.data, null, 2));
|
|
||||||
const normalizedData = normalizeDates(response.data);
|
const normalizedData = normalizeDates(response.data);
|
||||||
console.log("📥 [ScreenModal] 정규화 후:", JSON.stringify(normalizedData, null, 2));
|
|
||||||
|
|
||||||
// 🔧 배열 데이터는 formData로 설정하지 않음 (SelectedItemsDetailInput만 사용)
|
// 🔧 배열 데이터는 formData로 설정하지 않음 (SelectedItemsDetailInput만 사용)
|
||||||
if (Array.isArray(normalizedData)) {
|
if (Array.isArray(normalizedData)) {
|
||||||
console.log(
|
|
||||||
"⚠️ [ScreenModal] 그룹 레코드(배열)는 formData로 설정하지 않음. SelectedItemsDetailInput만 사용합니다.",
|
|
||||||
);
|
|
||||||
setFormData(normalizedData); // SelectedItemsDetailInput이 직접 사용
|
setFormData(normalizedData); // SelectedItemsDetailInput이 직접 사용
|
||||||
setOriginalData(normalizedData[0] || null); // 🆕 첫 번째 레코드를 원본으로 저장
|
setOriginalData(normalizedData[0] || null); // 🆕 첫 번째 레코드를 원본으로 저장
|
||||||
} else {
|
} else {
|
||||||
setFormData(normalizedData);
|
setFormData(normalizedData);
|
||||||
setOriginalData(normalizedData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
setOriginalData(normalizedData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setFormData 직후 확인
|
|
||||||
console.log("🔄 setFormData 호출 완료 (날짜 정규화됨)");
|
|
||||||
console.log("🔄 setOriginalData 호출 완료 (UPDATE 판단용)");
|
|
||||||
} else {
|
} else {
|
||||||
console.error("❌ 수정 데이터 로드 실패:", response.error);
|
|
||||||
toast.error("데이터를 불러올 수 없습니다.");
|
toast.error("데이터를 불러올 수 없습니다.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 수정 데이터 조회 오류:", error);
|
console.error("수정 데이터 조회 오류:", error);
|
||||||
toast.error("데이터를 불러오는 중 오류가 발생했습니다.");
|
toast.error("데이터를 불러오는 중 오류가 발생했습니다.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -498,11 +409,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
offsetX: 0,
|
offsetX: 0,
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
};
|
};
|
||||||
console.log("✅ 화면 관리 해상도 사용:", dimensions);
|
|
||||||
} else {
|
} else {
|
||||||
// 해상도 정보가 없으면 자동 계산
|
// 해상도 정보가 없으면 자동 계산
|
||||||
dimensions = calculateScreenDimensions(components);
|
dimensions = calculateScreenDimensions(components);
|
||||||
console.log("⚠️ 자동 계산된 크기 사용:", dimensions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setScreenDimensions(dimensions);
|
setScreenDimensions(dimensions);
|
||||||
|
|
@ -511,11 +420,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
components,
|
components,
|
||||||
screenInfo: screenInfo,
|
screenInfo: screenInfo,
|
||||||
});
|
});
|
||||||
console.log("화면 데이터 설정 완료:", {
|
|
||||||
componentsCount: components.length,
|
|
||||||
dimensions,
|
|
||||||
screenInfo,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("화면 데이터가 없습니다");
|
throw new Error("화면 데이터가 없습니다");
|
||||||
}
|
}
|
||||||
|
|
@ -537,7 +441,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
currentUrl.searchParams.delete("tableName");
|
currentUrl.searchParams.delete("tableName");
|
||||||
currentUrl.searchParams.delete("groupByColumns");
|
currentUrl.searchParams.delete("groupByColumns");
|
||||||
window.history.pushState({}, "", currentUrl.toString());
|
window.history.pushState({}, "", currentUrl.toString());
|
||||||
console.log("🧹 [ScreenModal] URL 파라미터 제거 (모달 닫힘)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalState({
|
setModalState({
|
||||||
|
|
@ -695,15 +598,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 formData 전달 확인 로그
|
|
||||||
console.log("📝 [ScreenModal] InteractiveScreenViewerDynamic에 formData 전달:", {
|
|
||||||
componentId: component.id,
|
|
||||||
componentType: component.type,
|
|
||||||
componentComponentType: (component as any).componentType, // 🆕 실제 componentType 확인
|
|
||||||
hasFormData: !!formData,
|
|
||||||
formDataKeys: formData ? Object.keys(formData) : [],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractiveScreenViewerDynamic
|
<InteractiveScreenViewerDynamic
|
||||||
key={`${component.id}-${resetKey}`}
|
key={`${component.id}-${resetKey}`}
|
||||||
|
|
@ -712,19 +606,16 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
formData={formData}
|
formData={formData}
|
||||||
originalData={originalData} // 🆕 원본 데이터 전달 (UPDATE 판단용)
|
originalData={originalData} // 🆕 원본 데이터 전달 (UPDATE 판단용)
|
||||||
onFormDataChange={(fieldName, value) => {
|
onFormDataChange={(fieldName, value) => {
|
||||||
console.log("🔧 [ScreenModal] onFormDataChange 호출:", { fieldName, value });
|
|
||||||
setFormData((prev) => {
|
setFormData((prev) => {
|
||||||
const newFormData = {
|
const newFormData = {
|
||||||
...prev,
|
...prev,
|
||||||
[fieldName]: value,
|
[fieldName]: value,
|
||||||
};
|
};
|
||||||
console.log("🔧 [ScreenModal] formData 업데이트:", { prev, newFormData });
|
|
||||||
return newFormData;
|
return newFormData;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
// 부모 화면의 테이블 새로고침 이벤트 발송
|
// 부모 화면의 테이블 새로고침 이벤트 발송
|
||||||
console.log("🔄 모달에서 부모 화면 테이블 새로고침 이벤트 발송");
|
|
||||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||||
}}
|
}}
|
||||||
screenInfo={{
|
screenInfo={{
|
||||||
|
|
@ -758,7 +649,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const isChecked = checked === true;
|
const isChecked = checked === true;
|
||||||
setContinuousMode(isChecked);
|
setContinuousMode(isChecked);
|
||||||
localStorage.setItem("screenModal_continuousMode", String(isChecked));
|
localStorage.setItem("screenModal_continuousMode", String(isChecked));
|
||||||
console.log("🔄 연속 모드 변경:", isChecked);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="continuous-mode" className="cursor-pointer text-sm font-normal select-none">
|
<Label htmlFor="continuous-mode" className="cursor-pointer text-sm font-normal select-none">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -644,9 +645,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||||
<SelectItem value="transferData">데이터 전달</SelectItem>
|
<SelectItem value="transferData">데이터 전달</SelectItem>
|
||||||
<SelectItem value="openModalWithData">데이터 전달 + 모달 열기</SelectItem>
|
|
||||||
<SelectItem value="openRelatedModal">연관 데이터 버튼 모달 열기</SelectItem>
|
|
||||||
<SelectItem value="modal">모달 열기</SelectItem>
|
<SelectItem value="modal">모달 열기</SelectItem>
|
||||||
|
<SelectItem value="openRelatedModal">연관 데이터 버튼 모달 열기</SelectItem>
|
||||||
|
<SelectItem value="openModalWithData" className="text-muted-foreground">
|
||||||
|
(deprecated) 데이터 전달 + 모달 열기
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="quickInsert">즉시 저장</SelectItem>
|
<SelectItem value="quickInsert">즉시 저장</SelectItem>
|
||||||
<SelectItem value="control">제어 흐름</SelectItem>
|
<SelectItem value="control">제어 흐름</SelectItem>
|
||||||
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
||||||
|
|
@ -722,8 +725,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={modalScreenOpen}
|
aria-expanded={modalScreenOpen}
|
||||||
className="h-6 w-full justify-between px-2 py-0"
|
className="h-6 w-full justify-between px-2 py-0 text-xs"
|
||||||
className="text-xs"
|
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -781,40 +783,36 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 선택된 데이터 전달 옵션 */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="auto-detect-data-source"
|
||||||
|
checked={component.componentConfig?.action?.autoDetectDataSource === true}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdateProperty("componentConfig.action.autoDetectDataSource", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Label htmlFor="auto-detect-data-source" className="text-sm cursor-pointer">
|
||||||
|
선택된 데이터 전달
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
TableList/SplitPanel에서 선택된 데이터를 모달에 자동으로 전달합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 🆕 데이터 전달 + 모달 열기 액션 설정 */}
|
{/* 🆕 데이터 전달 + 모달 열기 액션 설정 (deprecated - 하위 호환성 유지) */}
|
||||||
{component.componentConfig?.action?.type === "openModalWithData" && (
|
{component.componentConfig?.action?.type === "openModalWithData" && (
|
||||||
<div className="mt-4 space-y-4 rounded-lg border bg-blue-50 p-4 dark:bg-blue-950/20">
|
<div className="mt-4 space-y-4 rounded-lg border bg-amber-50 p-4 dark:bg-amber-950/20">
|
||||||
<h4 className="text-sm font-medium text-foreground">데이터 전달 + 모달 설정</h4>
|
<h4 className="text-sm font-medium text-foreground">데이터 전달 + 모달 설정</h4>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
TableList에서 선택된 데이터를 다음 모달로 전달합니다
|
이 옵션은 "모달 열기" 액션으로 통합되었습니다. 새 개발에서는 "모달 열기" + "선택된 데이터 전달"을 사용하세요.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="data-source-id">
|
|
||||||
데이터 소스 ID <span className="text-primary">(선택사항)</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="data-source-id"
|
|
||||||
placeholder="비워두면 자동으로 감지됩니다"
|
|
||||||
value={component.componentConfig?.action?.dataSourceId || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
onUpdateProperty("componentConfig.action.dataSourceId", e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-primary font-medium">
|
|
||||||
✨ 비워두면 현재 화면의 TableList를 자동으로 감지합니다
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
• 자동 감지: 현재 화면의 TableList 선택 데이터<br/>
|
|
||||||
• 누적 전달: 이전 모달의 모든 데이터도 자동으로 함께 전달<br/>
|
|
||||||
• 다음 화면에서 tableName으로 바로 사용 가능<br/>
|
|
||||||
• 수동 설정: 필요시 직접 테이블명 입력 (예: item_info)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 🆕 블록 기반 제목 빌더 */}
|
{/* 🆕 블록 기반 제목 빌더 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export const UnifiedList = forwardRef<HTMLDivElement, UnifiedListProps>((props,
|
||||||
showHeader: config.viewMode !== "card", // 카드 모드에서는 테이블 헤더 숨김
|
showHeader: config.viewMode !== "card", // 카드 모드에서는 테이블 헤더 숨김
|
||||||
showFooter: false,
|
showFooter: false,
|
||||||
checkbox: {
|
checkbox: {
|
||||||
enabled: !!onRowSelect,
|
enabled: true, // 항상 체크박스 활성화 (modalDataStore에 자동 저장)
|
||||||
position: "left" as const,
|
position: "left" as const,
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -127,11 +127,8 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({ childr
|
||||||
* Context Hook
|
* Context Hook
|
||||||
*/
|
*/
|
||||||
export const useTableOptions = () => {
|
export const useTableOptions = () => {
|
||||||
console.log("🔍🔍🔍 [useTableOptions] Hook 호출됨");
|
|
||||||
const context = useContext(TableOptionsContext);
|
const context = useContext(TableOptionsContext);
|
||||||
console.log("🔍 [useTableOptions] context 확인:", { hasContext: !!context });
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
console.error("❌ [useTableOptions] Context가 없습니다! TableOptionsProvider 외부에서 호출됨");
|
|
||||||
throw new Error("useTableOptions must be used within TableOptionsProvider");
|
throw new Error("useTableOptions must be used within TableOptionsProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
|
|
|
||||||
|
|
@ -66,13 +66,6 @@ export function useTableColumnHierarchy(tableName?: string, columnName?: string)
|
||||||
let hierarchyRole: ColumnHierarchyInfo["hierarchyRole"];
|
let hierarchyRole: ColumnHierarchyInfo["hierarchyRole"];
|
||||||
let hierarchyParentField: string | undefined;
|
let hierarchyParentField: string | undefined;
|
||||||
|
|
||||||
console.log("🔍 [useTableColumnHierarchy] 컬럼 정보:", {
|
|
||||||
columnName,
|
|
||||||
detailSettings: targetColumn.detailSettings,
|
|
||||||
detailSettingsType: typeof targetColumn.detailSettings,
|
|
||||||
codeCategory: targetColumn.codeCategory,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (targetColumn.detailSettings) {
|
if (targetColumn.detailSettings) {
|
||||||
try {
|
try {
|
||||||
const settings =
|
const settings =
|
||||||
|
|
@ -80,12 +73,9 @@ export function useTableColumnHierarchy(tableName?: string, columnName?: string)
|
||||||
? JSON.parse(targetColumn.detailSettings)
|
? JSON.parse(targetColumn.detailSettings)
|
||||||
: targetColumn.detailSettings;
|
: targetColumn.detailSettings;
|
||||||
|
|
||||||
console.log("🔍 [useTableColumnHierarchy] 파싱된 settings:", settings);
|
|
||||||
|
|
||||||
hierarchyRole = settings.hierarchyRole;
|
hierarchyRole = settings.hierarchyRole;
|
||||||
hierarchyParentField = settings.hierarchyParentField;
|
hierarchyParentField = settings.hierarchyParentField;
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.log("🔍 [useTableColumnHierarchy] JSON 파싱 실패:", e);
|
|
||||||
// JSON 파싱 실패 시 무시
|
// JSON 파싱 실패 시 무시
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,25 +128,11 @@ export function useCodeOptions(codeCategory?: string, enabled: boolean = true, m
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!codeCategory || codeCategory === "none") return [];
|
if (!codeCategory || codeCategory === "none") return [];
|
||||||
|
|
||||||
console.log("🔍 [useCodeOptions] 코드 옵션 조회 시작:", {
|
|
||||||
codeCategory,
|
|
||||||
menuObjid,
|
|
||||||
hasMenuObjid: !!menuObjid,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await commonCodeApi.codes.getList(codeCategory, {
|
const response = await commonCodeApi.codes.getList(codeCategory, {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
menuObjid,
|
menuObjid,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("📦 [useCodeOptions] API 응답:", {
|
|
||||||
codeCategory,
|
|
||||||
menuObjid,
|
|
||||||
success: response.success,
|
|
||||||
dataCount: response.data?.length || 0,
|
|
||||||
rawData: response.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
const options = response.data.map((code: any) => {
|
const options = response.data.map((code: any) => {
|
||||||
const actualValue = code.code || code.CODE || code.value || code.code_value || code.codeValue;
|
const actualValue = code.code || code.CODE || code.value || code.code_value || code.codeValue;
|
||||||
|
|
@ -185,13 +161,6 @@ export function useCodeOptions(codeCategory?: string, enabled: boolean = true, m
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ [useCodeOptions] 옵션 변환 완료:", {
|
|
||||||
codeCategory,
|
|
||||||
menuObjid,
|
|
||||||
optionsCount: options.length,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -298,19 +298,19 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
}}
|
}}
|
||||||
data={listData}
|
data={listData}
|
||||||
selectedRows={props.selectedRowsData || []}
|
selectedRows={props.selectedRowsData || []}
|
||||||
onRowSelect={
|
onRowSelect={(rows) => {
|
||||||
props.onSelectedRowsChange
|
// 항상 선택된 데이터를 전달 (modalDataStore에 자동 저장됨)
|
||||||
? (rows) =>
|
if (props.onSelectedRowsChange) {
|
||||||
props.onSelectedRowsChange?.(
|
props.onSelectedRowsChange(
|
||||||
rows.map((r: any) => r.id || r.objid),
|
rows.map((r: any) => r.id || r.objid),
|
||||||
rows,
|
rows,
|
||||||
props.sortBy,
|
props.sortBy,
|
||||||
props.sortOrder,
|
props.sortOrder,
|
||||||
undefined,
|
undefined,
|
||||||
props.tableDisplayData,
|
props.tableDisplayData,
|
||||||
)
|
);
|
||||||
: undefined
|
}
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -306,11 +306,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
|
|
||||||
// 동적 import로 modalDataStore 구독
|
// 동적 import로 modalDataStore 구독
|
||||||
let unsubscribe: (() => void) | undefined;
|
let unsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
||||||
// 초기값 설정
|
// 초기값 설정
|
||||||
setModalStoreData(useModalDataStore.getState().dataRegistry);
|
setModalStoreData(useModalDataStore.getState().dataRegistry);
|
||||||
|
|
||||||
// 상태 변경 구독
|
// 상태 변경 구독
|
||||||
unsubscribe = useModalDataStore.subscribe((state) => {
|
unsubscribe = useModalDataStore.subscribe((state) => {
|
||||||
setModalStoreData(state.dataRegistry);
|
setModalStoreData(state.dataRegistry);
|
||||||
|
|
@ -325,7 +325,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
// 🆕 행 선택 기반 비활성화 조건 계산
|
// 🆕 행 선택 기반 비활성화 조건 계산
|
||||||
const isRowSelectionDisabled = useMemo(() => {
|
const isRowSelectionDisabled = useMemo(() => {
|
||||||
const actionConfig = component.componentConfig?.action;
|
const actionConfig = component.componentConfig?.action;
|
||||||
|
|
||||||
// requireRowSelection이 활성화되어 있지 않으면 비활성화하지 않음
|
// requireRowSelection이 활성화되어 있지 않으면 비활성화하지 않음
|
||||||
if (!actionConfig?.requireRowSelection) {
|
if (!actionConfig?.requireRowSelection) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -365,7 +365,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
selectionSource = "splitPanelLeft (context)";
|
selectionSource = "splitPanelLeft (context)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 modalDataStore에서도 확인 (SplitPanelLayoutComponent에서 저장)
|
// 🆕 modalDataStore에서도 확인 (SplitPanelLayoutComponent에서 저장)
|
||||||
if (!hasSelection && Object.keys(modalStoreData).length > 0) {
|
if (!hasSelection && Object.keys(modalStoreData).length > 0) {
|
||||||
// modalDataStore에서 데이터가 있는지 확인
|
// modalDataStore에서 데이터가 있는지 확인
|
||||||
|
|
@ -390,39 +390,16 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 디버깅 로그
|
|
||||||
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) {
|
if (!hasSelection) {
|
||||||
console.log("🚫 [ButtonPrimary] 행 선택 필요 → 비활성화:", component.label);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 다중 선택 허용하지 않는 경우, 정확히 1개만 선택되어야 함
|
// 다중 선택 허용하지 않는 경우, 정확히 1개만 선택되어야 함
|
||||||
if (!allowMultiRowSelection && selectionCount !== 1) {
|
if (!allowMultiRowSelection && selectionCount !== 1) {
|
||||||
console.log("🚫 [ButtonPrimary] 정확히 1개 행 선택 필요 → 비활성화:", component.label, {
|
|
||||||
selectionCount,
|
|
||||||
allowMultiRowSelection,
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ [ButtonPrimary] 행 선택 조건 충족:", component.label, {
|
|
||||||
selectionCount,
|
|
||||||
selectionSource,
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}, [
|
}, [
|
||||||
component.componentConfig?.action,
|
component.componentConfig?.action,
|
||||||
|
|
@ -526,7 +503,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
dataflowTiming: component.webTypeConfig?.dataflowTiming,
|
dataflowTiming: component.webTypeConfig?.dataflowTiming,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 스타일 계산
|
// 스타일 계산
|
||||||
// height: 100%로 부모(RealtimePreviewDynamic의 내부 div)의 높이를 따라감
|
// height: 100%로 부모(RealtimePreviewDynamic의 내부 div)의 높이를 따라감
|
||||||
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
|
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
|
||||||
|
|
@ -699,7 +676,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
// (조건부 컨테이너의 다른 섹션으로 전환했을 때 이전 컴포넌트 ID가 남아있는 경우 대응)
|
// (조건부 컨테이너의 다른 섹션으로 전환했을 때 이전 컴포넌트 ID가 남아있는 경우 대응)
|
||||||
if (!sourceProvider) {
|
if (!sourceProvider) {
|
||||||
console.log(`⚠️ [ButtonPrimary] 지정된 소스 컴포넌트를 찾을 수 없음: ${dataTransferConfig.sourceComponentId}`);
|
console.log(`⚠️ [ButtonPrimary] 지정된 소스 컴포넌트를 찾을 수 없음: ${dataTransferConfig.sourceComponentId}`);
|
||||||
console.log(`🔍 [ButtonPrimary] 현재 화면에서 DataProvider 자동 탐색...`);
|
console.log("🔍 [ButtonPrimary] 현재 화면에서 DataProvider 자동 탐색...");
|
||||||
|
|
||||||
const allProviders = screenContext.getAllDataProviders();
|
const allProviders = screenContext.getAllDataProviders();
|
||||||
|
|
||||||
|
|
@ -1024,9 +1001,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
if (groupByColumns && groupByColumns.length > 0 && effectiveSelectedRowsData.length > 1) {
|
if (groupByColumns && groupByColumns.length > 0 && effectiveSelectedRowsData.length > 1) {
|
||||||
// 첫 번째 그룹핑 컬럼 기준으로 중복 체크 (예: order_no)
|
// 첫 번째 그룹핑 컬럼 기준으로 중복 체크 (예: order_no)
|
||||||
const groupByColumn = groupByColumns[0];
|
const groupByColumn = groupByColumns[0];
|
||||||
const uniqueValues = new Set(
|
const uniqueValues = new Set(effectiveSelectedRowsData.map((row: any) => row[groupByColumn]).filter(Boolean));
|
||||||
effectiveSelectedRowsData.map((row: any) => row[groupByColumn]).filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (uniqueValues.size > 1) {
|
if (uniqueValues.size > 1) {
|
||||||
// 컬럼명을 한글로 변환 (order_no -> 수주번호)
|
// 컬럼명을 한글로 변환 (order_no -> 수주번호)
|
||||||
|
|
@ -1109,10 +1084,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||||
splitPanelParentData,
|
splitPanelParentData,
|
||||||
// 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
|
// 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
|
||||||
splitPanelContext: splitPanelContext ? {
|
splitPanelContext: splitPanelContext
|
||||||
selectedLeftData: splitPanelContext.selectedLeftData,
|
? {
|
||||||
refreshRightPanel: splitPanelContext.refreshRightPanel,
|
selectedLeftData: splitPanelContext.selectedLeftData,
|
||||||
} : undefined,
|
refreshRightPanel: splitPanelContext.refreshRightPanel,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
} as ButtonActionContext;
|
} as ButtonActionContext;
|
||||||
|
|
||||||
// 확인이 필요한 액션인지 확인
|
// 확인이 필요한 액션인지 확인
|
||||||
|
|
@ -1233,15 +1210,16 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화 + 행 선택 필수)
|
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화 + 행 선택 필수)
|
||||||
const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading;
|
const finalDisabled =
|
||||||
|
componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading;
|
||||||
|
|
||||||
// 공통 버튼 스타일
|
// 공통 버튼 스타일
|
||||||
// 🔧 component.style에서 background/backgroundColor 충돌 방지
|
// 🔧 component.style에서 background/backgroundColor 충돌 방지
|
||||||
const userStyle = component.style
|
const userStyle = component.style
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
Object.entries(component.style).filter(
|
Object.entries(component.style).filter(
|
||||||
([key]) => !["width", "height", "background", "backgroundColor"].includes(key)
|
([key]) => !["width", "height", "background", "backgroundColor"].includes(key),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,15 +63,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// 화면 컨텍스트 (데이터 제공자로 등록)
|
// 화면 컨텍스트 (데이터 제공자로 등록)
|
||||||
const screenContext = useScreenContextOptional();
|
const screenContext = useScreenContextOptional();
|
||||||
|
|
||||||
// 🚨 최초 렌더링 확인용 (테스트 후 제거)
|
|
||||||
console.log("🚨🚨🚨 [SelectBasicComponent] 렌더링됨!!!!", {
|
|
||||||
componentId: component.id,
|
|
||||||
componentType: (component as any).componentType,
|
|
||||||
columnName: (component as any).columnName,
|
|
||||||
"props.multiple": (props as any).multiple,
|
|
||||||
"componentConfig.multiple": componentConfig?.multiple,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
|
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
|
||||||
|
|
@ -80,30 +71,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// 🆕 multiple 값: props.multiple (spread된 값) > config.multiple 순서로 우선순위
|
// 🆕 multiple 값: props.multiple (spread된 값) > config.multiple 순서로 우선순위
|
||||||
const isMultiple = (props as any).multiple ?? config?.multiple ?? false;
|
const isMultiple = (props as any).multiple ?? config?.multiple ?? false;
|
||||||
|
|
||||||
// 🔍 디버깅: config 및 multiple 확인
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("🔍 [SelectBasicComponent] ========== 다중선택 디버깅 ==========");
|
|
||||||
console.log(" 컴포넌트 ID:", component.id);
|
|
||||||
console.log(" 최종 isMultiple 값:", isMultiple);
|
|
||||||
console.log(" ----------------------------------------");
|
|
||||||
console.log(" props.multiple:", (props as any).multiple);
|
|
||||||
console.log(" config.multiple:", config?.multiple);
|
|
||||||
console.log(" componentConfig.multiple:", componentConfig?.multiple);
|
|
||||||
console.log(" component.componentConfig.multiple:", component.componentConfig?.multiple);
|
|
||||||
console.log(" ----------------------------------------");
|
|
||||||
console.log(" config 전체:", config);
|
|
||||||
console.log(" componentConfig 전체:", componentConfig);
|
|
||||||
console.log(" component.componentConfig 전체:", component.componentConfig);
|
|
||||||
console.log(" =======================================");
|
|
||||||
|
|
||||||
// 다중선택이 활성화되었는지 알림
|
|
||||||
if (isMultiple) {
|
|
||||||
console.log("✅ 다중선택 모드 활성화됨!");
|
|
||||||
} else {
|
|
||||||
console.log("❌ 단일선택 모드 (다중선택 비활성화)");
|
|
||||||
}
|
|
||||||
}, [(props as any).multiple, config?.multiple, componentConfig?.multiple, component.componentConfig?.multiple]);
|
|
||||||
|
|
||||||
// webType에 따른 세부 타입 결정 (TextInputComponent와 동일한 방식)
|
// webType에 따른 세부 타입 결정 (TextInputComponent와 동일한 방식)
|
||||||
const webType = component.componentConfig?.webType || "select";
|
const webType = component.componentConfig?.webType || "select";
|
||||||
|
|
||||||
|
|
@ -176,17 +143,8 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// 1순위: 동적으로 조회된 값 (테이블 타입관리에서 설정)
|
// 1순위: 동적으로 조회된 값 (테이블 타입관리에서 설정)
|
||||||
// 2순위: config에서 전달된 값
|
// 2순위: config에서 전달된 값
|
||||||
const hierarchyRole = columnHierarchy?.hierarchyRole || config?.hierarchyRole || componentConfig?.hierarchyRole;
|
const hierarchyRole = columnHierarchy?.hierarchyRole || config?.hierarchyRole || componentConfig?.hierarchyRole;
|
||||||
const hierarchyParentField = columnHierarchy?.hierarchyParentField || config?.hierarchyParentField || componentConfig?.hierarchyParentField;
|
const hierarchyParentField =
|
||||||
|
columnHierarchy?.hierarchyParentField || config?.hierarchyParentField || componentConfig?.hierarchyParentField;
|
||||||
// 디버깅 로그
|
|
||||||
console.log("🔍 [SelectBasic] 계층구조 설정:", {
|
|
||||||
columnName: component.columnName,
|
|
||||||
tableName: component.tableName,
|
|
||||||
columnHierarchy,
|
|
||||||
hierarchyRole,
|
|
||||||
hierarchyParentField,
|
|
||||||
codeCategory,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🆕 자식 역할일 때 부모 값 추출 (단일 또는 다중)
|
// 🆕 자식 역할일 때 부모 값 추출 (단일 또는 다중)
|
||||||
const rawParentValue =
|
const rawParentValue =
|
||||||
|
|
@ -206,27 +164,9 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// 최종 비활성화 상태
|
// 최종 비활성화 상태
|
||||||
const isFieldDisabled = isFieldDisabledBase || isHierarchyDisabled;
|
const isFieldDisabled = isFieldDisabledBase || isHierarchyDisabled;
|
||||||
|
|
||||||
console.log("🔍 [SelectBasic] 비활성화 상태:", {
|
|
||||||
columnName: component.columnName,
|
|
||||||
hierarchyRole,
|
|
||||||
hierarchyParentValue,
|
|
||||||
isHierarchyDisabled,
|
|
||||||
isFieldDisabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🆕 계층구조 역할에 따라 옵션 필터링
|
// 🆕 계층구조 역할에 따라 옵션 필터링
|
||||||
const filteredCodeOptions = useMemo(() => {
|
const filteredCodeOptions = useMemo(() => {
|
||||||
console.log("🔍 [SelectBasic] 옵션 필터링:", {
|
|
||||||
columnName: component.columnName,
|
|
||||||
hierarchyRole,
|
|
||||||
hierarchyParentField,
|
|
||||||
hierarchyParentValue,
|
|
||||||
codeOptionsCount: codeOptions?.length || 0,
|
|
||||||
sampleOptions: codeOptions?.slice(0, 3),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hierarchyRole || !codeOptions || codeOptions.length === 0) {
|
if (!hierarchyRole || !codeOptions || codeOptions.length === 0) {
|
||||||
console.log("🔍 [SelectBasic] 필터링 스킵 - hierarchyRole 없음 또는 옵션 없음");
|
|
||||||
return codeOptions;
|
return codeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,7 +177,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
const parentCodeValue = opt.parentCodeValue || opt.parent_code_value;
|
const parentCodeValue = opt.parentCodeValue || opt.parent_code_value;
|
||||||
return depth === 1 || !parentCodeValue;
|
return depth === 1 || !parentCodeValue;
|
||||||
});
|
});
|
||||||
console.log("🔍 [SelectBasic] 대분류 필터링 결과:", filtered.length, "개");
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,18 +186,16 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
const parentCodeValue = opt.parentCodeValue || opt.parent_code_value;
|
const parentCodeValue = opt.parentCodeValue || opt.parent_code_value;
|
||||||
return parentCodeValue === hierarchyParentValue;
|
return parentCodeValue === hierarchyParentValue;
|
||||||
});
|
});
|
||||||
console.log("🔍 [SelectBasic] 중/소분류 필터링 결과:", filtered.length, "개");
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 부모 값이 없으면 빈 배열 반환 (선택 불가 상태)
|
// 부모 값이 없으면 빈 배열 반환 (선택 불가 상태)
|
||||||
if (hierarchyRole === "medium" || hierarchyRole === "small") {
|
if (hierarchyRole === "medium" || hierarchyRole === "small") {
|
||||||
console.log("🔍 [SelectBasic] 중/소분류 - 부모값 없음, 빈 배열 반환");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return codeOptions;
|
return codeOptions;
|
||||||
}, [codeOptions, hierarchyRole, hierarchyParentValue, hierarchyParentField, component.columnName]);
|
}, [codeOptions, hierarchyRole, hierarchyParentValue]);
|
||||||
|
|
||||||
// 🆕 부모값이 콤마로 구분된 문자열이면 배열로 변환 (다중 선택 지원)
|
// 🆕 부모값이 콤마로 구분된 문자열이면 배열로 변환 (다중 선택 지원)
|
||||||
const parentValues: string[] | undefined = useMemo(() => {
|
const parentValues: string[] | undefined = useMemo(() => {
|
||||||
|
|
@ -295,44 +232,22 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (webType === "category" && component.tableName && component.columnName) {
|
if (webType === "category" && component.tableName && component.columnName) {
|
||||||
console.log("🔍 [SelectBasic] 카테고리 값 로딩 시작:", {
|
|
||||||
tableName: component.tableName,
|
|
||||||
columnName: component.columnName,
|
|
||||||
webType,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsLoadingCategories(true);
|
setIsLoadingCategories(true);
|
||||||
|
|
||||||
import("@/lib/api/tableCategoryValue").then(({ getCategoryValues }) => {
|
import("@/lib/api/tableCategoryValue").then(({ getCategoryValues }) => {
|
||||||
getCategoryValues(component.tableName!, component.columnName!)
|
getCategoryValues(component.tableName!, component.columnName!)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log("🔍 [SelectBasic] 카테고리 API 응답:", response);
|
if (response.success && "data" in response && response.data) {
|
||||||
|
const activeValues = response.data.filter((v: any) => v.isActive !== false);
|
||||||
if (response.success && response.data) {
|
const options = activeValues.map((v: any) => ({
|
||||||
console.log("🔍 [SelectBasic] 원본 데이터 샘플:", {
|
|
||||||
firstItem: response.data[0],
|
|
||||||
keys: response.data[0] ? Object.keys(response.data[0]) : [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeValues = response.data.filter((v) => v.isActive !== false);
|
|
||||||
const options = activeValues.map((v) => ({
|
|
||||||
value: v.valueCode,
|
value: v.valueCode,
|
||||||
label: v.valueLabel || v.valueCode,
|
label: v.valueLabel || v.valueCode,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("✅ [SelectBasic] 카테고리 옵션 설정:", {
|
|
||||||
activeValuesCount: activeValues.length,
|
|
||||||
options,
|
|
||||||
sampleOption: options[0],
|
|
||||||
});
|
|
||||||
|
|
||||||
setCategoryOptions(options);
|
setCategoryOptions(options);
|
||||||
} else {
|
|
||||||
console.error("❌ [SelectBasic] 카테고리 응답 실패:", response);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
console.error("❌ [SelectBasic] 카테고리 값 조회 실패:", error);
|
// 카테고리 값 조회 실패 시 무시
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoadingCategories(false);
|
setIsLoadingCategories(false);
|
||||||
|
|
@ -341,35 +256,10 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
}
|
}
|
||||||
}, [webType, component.tableName, component.columnName]);
|
}, [webType, component.tableName, component.columnName]);
|
||||||
|
|
||||||
// 디버깅: menuObjid가 제대로 전달되는지 확인
|
|
||||||
useEffect(() => {
|
|
||||||
if (codeCategory && codeCategory !== "none") {
|
|
||||||
console.log(`🎯 [SelectBasicComponent ${component.id}] 코드 옵션 로드:`, {
|
|
||||||
codeCategory,
|
|
||||||
menuObjid,
|
|
||||||
hasMenuObjid: !!menuObjid,
|
|
||||||
isCodeCategoryValid,
|
|
||||||
codeOptionsCount: codeOptions.length,
|
|
||||||
isLoading: isLoadingCodes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [component.id, codeCategory, menuObjid, codeOptions.length, isLoadingCodes, isCodeCategoryValid]);
|
|
||||||
|
|
||||||
// 외부 value prop 변경 시 selectedValue 업데이트
|
// 외부 value prop 변경 시 selectedValue 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newValue = externalValue || config?.value || "";
|
const newValue = externalValue || config?.value || "";
|
||||||
|
|
||||||
console.log("🔍 [SelectBasic] 외부 값 변경 감지:", {
|
|
||||||
componentId: component.id,
|
|
||||||
columnName: (component as any).columnName,
|
|
||||||
isMultiple,
|
|
||||||
newValue,
|
|
||||||
selectedValue,
|
|
||||||
selectedValues,
|
|
||||||
externalValue,
|
|
||||||
"config.value": config?.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 다중선택 모드인 경우
|
// 다중선택 모드인 경우
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
if (typeof newValue === "string" && newValue) {
|
if (typeof newValue === "string" && newValue) {
|
||||||
|
|
@ -380,14 +270,9 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
const currentValuesStr = selectedValues.join(",");
|
const currentValuesStr = selectedValues.join(",");
|
||||||
|
|
||||||
if (newValue !== currentValuesStr) {
|
if (newValue !== currentValuesStr) {
|
||||||
console.log("✅ [SelectBasic] 다중선택 값 업데이트:", {
|
|
||||||
from: selectedValues,
|
|
||||||
to: values,
|
|
||||||
});
|
|
||||||
setSelectedValues(values);
|
setSelectedValues(values);
|
||||||
}
|
}
|
||||||
} else if (!newValue && selectedValues.length > 0) {
|
} else if (!newValue && selectedValues.length > 0) {
|
||||||
console.log("✅ [SelectBasic] 다중선택 값 초기화");
|
|
||||||
setSelectedValues([]);
|
setSelectedValues([]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -544,32 +429,12 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
|
|
||||||
const configOptions = config.options || [];
|
const configOptions = config.options || [];
|
||||||
// 🆕 계층구조 역할이 설정된 경우 필터링된 옵션 사용
|
// 🆕 계층구조 역할이 설정된 경우 필터링된 옵션 사용
|
||||||
console.log("🔍 [SelectBasic] getAllOptions 호출:", {
|
|
||||||
columnName: component.columnName,
|
|
||||||
hierarchyRole,
|
|
||||||
codeOptionsCount: codeOptions?.length || 0,
|
|
||||||
filteredCodeOptionsCount: filteredCodeOptions?.length || 0,
|
|
||||||
categoryOptionsCount: categoryOptions?.length || 0,
|
|
||||||
configOptionsCount: configOptions?.length || 0,
|
|
||||||
});
|
|
||||||
return [...filteredCodeOptions, ...categoryOptions, ...configOptions];
|
return [...filteredCodeOptions, ...categoryOptions, ...configOptions];
|
||||||
};
|
};
|
||||||
|
|
||||||
const allOptions = getAllOptions();
|
const allOptions = getAllOptions();
|
||||||
const placeholder = componentConfig.placeholder || "선택하세요";
|
const placeholder = componentConfig.placeholder || "선택하세요";
|
||||||
|
|
||||||
// 🔍 디버깅: 최종 옵션 확인
|
|
||||||
useEffect(() => {
|
|
||||||
if (webType === "category" && allOptions.length > 0) {
|
|
||||||
console.log("🔍 [SelectBasic] 최종 allOptions:", {
|
|
||||||
count: allOptions.length,
|
|
||||||
categoryOptionsCount: categoryOptions.length,
|
|
||||||
codeOptionsCount: codeOptions.length,
|
|
||||||
sampleOptions: allOptions.slice(0, 3),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [webType, allOptions.length, categoryOptions.length, codeOptions.length]);
|
|
||||||
|
|
||||||
// DOM props에서 React 전용 props 필터링
|
// DOM props에서 React 전용 props 필터링
|
||||||
const {
|
const {
|
||||||
component: _component,
|
component: _component,
|
||||||
|
|
|
||||||
|
|
@ -2069,6 +2069,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 modalDataStore에 선택된 데이터 자동 저장 (테이블명 기반 dataSourceId)
|
// 🆕 modalDataStore에 선택된 데이터 자동 저장 (테이블명 기반 dataSourceId)
|
||||||
|
console.log("🔍 [TableList] modalDataStore 저장 조건:", {
|
||||||
|
selectedTable: tableConfig.selectedTable,
|
||||||
|
selectedRowsCount: selectedRowsData.length,
|
||||||
|
});
|
||||||
if (tableConfig.selectedTable && selectedRowsData.length > 0) {
|
if (tableConfig.selectedTable && selectedRowsData.length > 0) {
|
||||||
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
||||||
const modalItems = selectedRowsData.map((row, idx) => ({
|
const modalItems = selectedRowsData.map((row, idx) => ({
|
||||||
|
|
@ -2077,18 +2081,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
additionalData: {},
|
additionalData: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
|
|
||||||
console.log("✅ [TableList] modalDataStore에 데이터 저장:", {
|
console.log("✅ [TableList] modalDataStore에 데이터 저장:", {
|
||||||
dataSourceId: tableConfig.selectedTable,
|
sourceId: tableConfig.selectedTable,
|
||||||
count: modalItems.length,
|
itemCount: modalItems.length,
|
||||||
});
|
});
|
||||||
|
useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
|
||||||
});
|
});
|
||||||
} else if (tableConfig.selectedTable && selectedRowsData.length === 0) {
|
} else if (tableConfig.selectedTable && selectedRowsData.length === 0) {
|
||||||
// 선택 해제 시 데이터 제거
|
// 선택 해제 시 데이터 제거
|
||||||
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
||||||
useModalDataStore.getState().clearData(tableConfig.selectedTable!);
|
useModalDataStore.getState().clearData(tableConfig.selectedTable!);
|
||||||
console.log("🗑️ [TableList] modalDataStore 데이터 제거:", tableConfig.selectedTable);
|
|
||||||
});
|
});
|
||||||
|
} else if (!tableConfig.selectedTable) {
|
||||||
|
console.warn("⚠️ [TableList] selectedTable이 없어 modalDataStore에 저장하지 않음");
|
||||||
}
|
}
|
||||||
|
|
||||||
const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index)));
|
const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index)));
|
||||||
|
|
@ -2122,10 +2127,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
|
useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems);
|
||||||
console.log("✅ [TableList] modalDataStore에 전체 데이터 저장:", {
|
|
||||||
dataSourceId: tableConfig.selectedTable,
|
|
||||||
count: modalItems.length,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2143,7 +2144,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
if (tableConfig.selectedTable) {
|
if (tableConfig.selectedTable) {
|
||||||
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
||||||
useModalDataStore.getState().clearData(tableConfig.selectedTable!);
|
useModalDataStore.getState().clearData(tableConfig.selectedTable!);
|
||||||
console.log("🗑️ [TableList] modalDataStore 전체 데이터 제거:", tableConfig.selectedTable);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,10 +92,6 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
const generateAutoValue = async () => {
|
const generateAutoValue = async () => {
|
||||||
// 이미 생성 중이거나 생성 완료된 경우 중복 실행 방지
|
// 이미 생성 중이거나 생성 완료된 경우 중복 실행 방지
|
||||||
if (isGeneratingRef.current || hasGeneratedRef.current) {
|
if (isGeneratingRef.current || hasGeneratedRef.current) {
|
||||||
console.log("⏭️ 중복 실행 방지:", {
|
|
||||||
isGenerating: isGeneratingRef.current,
|
|
||||||
hasGenerated: hasGeneratedRef.current,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,28 +131,24 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (generatedValue) {
|
if (generatedValue) {
|
||||||
console.log("✅ 자동생성 값 설정:", generatedValue);
|
|
||||||
setAutoGeneratedValue(generatedValue);
|
setAutoGeneratedValue(generatedValue);
|
||||||
setOriginalAutoGeneratedValue(generatedValue); // 🆕 원본 값 저장
|
setOriginalAutoGeneratedValue(generatedValue); // 🆕 원본 값 저장
|
||||||
hasGeneratedRef.current = true; // 생성 완료 플래그
|
hasGeneratedRef.current = true; // 생성 완료 플래그
|
||||||
|
|
||||||
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
|
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
console.log("📝 formData 업데이트:", component.columnName, generatedValue);
|
|
||||||
onFormDataChange(component.columnName, generatedValue);
|
onFormDataChange(component.columnName, generatedValue);
|
||||||
|
|
||||||
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
|
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
|
||||||
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
|
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
|
||||||
const ruleIdKey = `${component.columnName}_numberingRuleId`;
|
const ruleIdKey = `${component.columnName}_numberingRuleId`;
|
||||||
onFormDataChange(ruleIdKey, testAutoGeneration.options.numberingRuleId);
|
onFormDataChange(ruleIdKey, testAutoGeneration.options.numberingRuleId);
|
||||||
console.log("📝 채번 규칙 ID 저장:", ruleIdKey, testAutoGeneration.options.numberingRuleId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
|
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
|
||||||
// 디자인 모드에서도 미리보기용 자동생성 값 표시
|
// 디자인 모드에서도 미리보기용 자동생성 값 표시
|
||||||
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
|
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
|
||||||
console.log("👁️ 미리보기 값 설정:", previewValue);
|
|
||||||
setAutoGeneratedValue(previewValue);
|
setAutoGeneratedValue(previewValue);
|
||||||
hasGeneratedRef.current = true;
|
hasGeneratedRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ export type ButtonActionType =
|
||||||
| "copy" // 복사 (품목코드 초기화)
|
| "copy" // 복사 (품목코드 초기화)
|
||||||
| "navigate" // 페이지 이동
|
| "navigate" // 페이지 이동
|
||||||
| "openRelatedModal" // 연관 데이터 버튼의 선택 데이터로 모달 열기
|
| "openRelatedModal" // 연관 데이터 버튼의 선택 데이터로 모달 열기
|
||||||
| "openModalWithData" // 데이터를 전달하면서 모달 열기
|
| "openModalWithData" // 데이터를 전달하면서 모달 열기 (deprecated: modal로 통합)
|
||||||
| "modal" // 모달 열기
|
| "modal" // 모달 열기 (선택된 데이터 전달 옵션 포함)
|
||||||
| "control" // 제어 흐름
|
| "control" // 제어 흐름
|
||||||
| "view_table_history" // 테이블 이력 보기
|
| "view_table_history" // 테이블 이력 보기
|
||||||
| "excel_download" // 엑셀 다운로드
|
| "excel_download" // 엑셀 다운로드
|
||||||
|
|
@ -60,8 +60,12 @@ export interface ButtonActionConfig {
|
||||||
modalSize?: "sm" | "md" | "lg" | "xl";
|
modalSize?: "sm" | "md" | "lg" | "xl";
|
||||||
popupWidth?: number;
|
popupWidth?: number;
|
||||||
popupHeight?: number;
|
popupHeight?: number;
|
||||||
dataSourceId?: string; // 🆕 modalDataStore에서 데이터를 가져올 ID (openModalWithData용)
|
|
||||||
fieldMappings?: Array<{ sourceField: string; targetField: string }>; // 🆕 필드 매핑 (openModalWithData용)
|
// 🆕 모달 데이터 전달 옵션 (modal 액션 통합)
|
||||||
|
passSelectedData?: boolean; // 선택된 데이터 전달 여부 (기본: true)
|
||||||
|
autoDetectDataSource?: boolean; // 데이터 소스 자동 감지 (TableList/SplitPanel 등)
|
||||||
|
dataSourceId?: string; // modalDataStore에서 데이터를 가져올 ID
|
||||||
|
fieldMappings?: Array<{ sourceField: string; targetField: string }>; // 필드 매핑
|
||||||
|
|
||||||
// 확인 메시지
|
// 확인 메시지
|
||||||
confirmMessage?: string;
|
confirmMessage?: string;
|
||||||
|
|
@ -366,7 +370,8 @@ export class ButtonActionExecutor {
|
||||||
return this.handleNavigate(config, context);
|
return this.handleNavigate(config, context);
|
||||||
|
|
||||||
case "openModalWithData":
|
case "openModalWithData":
|
||||||
return await this.handleOpenModalWithData(config, context);
|
// deprecated: modal로 통합 (하위 호환성 유지)
|
||||||
|
return await this.handleModal({ ...config, passSelectedData: true, autoDetectDataSource: true }, context);
|
||||||
|
|
||||||
case "openRelatedModal":
|
case "openRelatedModal":
|
||||||
return await this.handleOpenRelatedModal(config, context);
|
return await this.handleOpenRelatedModal(config, context);
|
||||||
|
|
@ -1095,23 +1100,20 @@ export class ButtonActionExecutor {
|
||||||
// @ts-ignore - window에 동적 속성 사용
|
// @ts-ignore - window에 동적 속성 사용
|
||||||
const unifiedRepeaterTables = Array.from(window.__unifiedRepeaterInstances || []);
|
const unifiedRepeaterTables = Array.from(window.__unifiedRepeaterInstances || []);
|
||||||
|
|
||||||
// 메인 저장 건너뛰기 조건:
|
// 메인 저장 건너뛰기 조건:
|
||||||
// 1. RepeatScreenModal 또는 RepeaterFieldGroup에서 같은 테이블 처리
|
// 1. RepeatScreenModal 또는 RepeaterFieldGroup에서 같은 테이블 처리
|
||||||
// 2. UnifiedRepeater가 같은 테이블에 존재 (리피터 데이터에 메인 폼 데이터 병합되어 저장됨)
|
// 2. UnifiedRepeater가 같은 테이블에 존재 (리피터 데이터에 메인 폼 데이터 병합되어 저장됨)
|
||||||
const shouldSkipMainSave =
|
const shouldSkipMainSave =
|
||||||
repeatScreenModalTables.includes(tableName) ||
|
repeatScreenModalTables.includes(tableName) ||
|
||||||
repeaterFieldGroupTables.includes(tableName) ||
|
repeaterFieldGroupTables.includes(tableName) ||
|
||||||
unifiedRepeaterTables.includes(tableName);
|
unifiedRepeaterTables.includes(tableName);
|
||||||
|
|
||||||
if (shouldSkipMainSave) {
|
if (shouldSkipMainSave) {
|
||||||
console.log(
|
console.log(`⏭️ [handleSave] ${tableName} 메인 저장 건너뜀`, {
|
||||||
`⏭️ [handleSave] ${tableName} 메인 저장 건너뜀`,
|
repeatScreenModalTables,
|
||||||
{
|
repeaterFieldGroupTables,
|
||||||
repeatScreenModalTables,
|
unifiedRepeaterTables,
|
||||||
repeaterFieldGroupTables,
|
});
|
||||||
unifiedRepeaterTables,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal/UnifiedRepeater에서 처리" };
|
saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal/UnifiedRepeater에서 처리" };
|
||||||
} else {
|
} else {
|
||||||
saveResult = await DynamicFormApi.saveFormData({
|
saveResult = await DynamicFormApi.saveFormData({
|
||||||
|
|
@ -1241,11 +1243,11 @@ export class ButtonActionExecutor {
|
||||||
// 저장 성공 후 이벤트 발생
|
// 저장 성공 후 이벤트 발생
|
||||||
window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal 닫기
|
window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal 닫기
|
||||||
window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal 연속 등록 모드 처리
|
window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal 연속 등록 모드 처리
|
||||||
|
|
||||||
// UnifiedRepeater 저장 이벤트 발생 (메인 폼 데이터 + 리피터 데이터 병합 저장)
|
// UnifiedRepeater 저장 이벤트 발생 (메인 폼 데이터 + 리피터 데이터 병합 저장)
|
||||||
// 🔧 formData를 리피터에 전달하여 각 행에 병합 저장
|
// 🔧 formData를 리피터에 전달하여 각 행에 병합 저장
|
||||||
const savedId = saveResult?.data?.id || saveResult?.data?.data?.id || formData.id || context.formData?.id;
|
const savedId = saveResult?.data?.id || saveResult?.data?.data?.id || formData.id || context.formData?.id;
|
||||||
|
|
||||||
// 메인 폼 데이터 구성 (사용자 정보 포함)
|
// 메인 폼 데이터 구성 (사용자 정보 포함)
|
||||||
const mainFormData = {
|
const mainFormData = {
|
||||||
...formData,
|
...formData,
|
||||||
|
|
@ -1254,27 +1256,29 @@ export class ButtonActionExecutor {
|
||||||
updated_by: context.userId,
|
updated_by: context.userId,
|
||||||
company_code: formData.company_code || context.companyCode,
|
company_code: formData.company_code || context.companyCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
// _numberingRuleId 등 메타 필드 제거
|
// _numberingRuleId 등 메타 필드 제거
|
||||||
for (const key of Object.keys(mainFormData)) {
|
for (const key of Object.keys(mainFormData)) {
|
||||||
if (key.endsWith("_numberingRuleId") || key.startsWith("_")) {
|
if (key.endsWith("_numberingRuleId") || key.startsWith("_")) {
|
||||||
delete mainFormData[key];
|
delete mainFormData[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔗 [handleSave] repeaterSave 이벤트 발생:", {
|
console.log("🔗 [handleSave] repeaterSave 이벤트 발생:", {
|
||||||
savedId,
|
savedId,
|
||||||
tableName: context.tableName,
|
tableName: context.tableName,
|
||||||
mainFormDataKeys: Object.keys(mainFormData),
|
mainFormDataKeys: Object.keys(mainFormData),
|
||||||
saveResultData: saveResult?.data
|
saveResultData: saveResult?.data,
|
||||||
});
|
});
|
||||||
window.dispatchEvent(new CustomEvent("repeaterSave", {
|
window.dispatchEvent(
|
||||||
detail: {
|
new CustomEvent("repeaterSave", {
|
||||||
parentId: savedId,
|
detail: {
|
||||||
tableName: context.tableName,
|
parentId: savedId,
|
||||||
mainFormData, // 🆕 메인 폼 데이터 전달
|
tableName: context.tableName,
|
||||||
}
|
mainFormData, // 🆕 메인 폼 데이터 전달
|
||||||
}));
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -2550,57 +2554,232 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모달 액션 처리
|
* 모달 액션 처리 (통합)
|
||||||
* 선택된 데이터가 있으면 함께 전달 (출하계획 등에서 사용)
|
* - passSelectedData: true (기본) → 선택된 데이터를 모달에 전달
|
||||||
|
* - autoDetectDataSource: true → TableList/SplitPanel 등에서 데이터 소스 자동 감지
|
||||||
|
* - fieldMappings → 필드 이름 매핑 지원
|
||||||
*/
|
*/
|
||||||
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
// 모달 열기 로직
|
const passSelectedData = config.passSelectedData !== false; // 기본: true
|
||||||
console.log("모달 열기:", {
|
const autoDetectDataSource = config.autoDetectDataSource === true;
|
||||||
title: config.modalTitle,
|
|
||||||
size: config.modalSize,
|
|
||||||
targetScreenId: config.targetScreenId,
|
|
||||||
selectedRowsData: context.selectedRowsData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.targetScreenId) {
|
if (!config.targetScreenId) {
|
||||||
// 1. config에 modalDescription이 있으면 우선 사용
|
toast.error("모달로 열 화면이 지정되지 않았습니다.");
|
||||||
let description = config.modalDescription || "";
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. config에 없으면 화면 정보에서 가져오기
|
// 1. 화면 설명 가져오기
|
||||||
if (!description) {
|
let description = config.modalDescription || "";
|
||||||
|
if (!description) {
|
||||||
|
try {
|
||||||
|
const screenInfo = await screenApi.getScreen(config.targetScreenId);
|
||||||
|
description = screenInfo?.description || "";
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("화면 설명을 가져오지 못했습니다:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 데이터 소스 및 선택된 데이터 수집
|
||||||
|
let selectedData: any[] = [];
|
||||||
|
let parentData: Record<string, any> = {};
|
||||||
|
let dataSourceId: string | undefined;
|
||||||
|
|
||||||
|
if (passSelectedData) {
|
||||||
|
// 2-1. 자동 감지 모드
|
||||||
|
if (autoDetectDataSource) {
|
||||||
|
dataSourceId = config.dataSourceId;
|
||||||
|
|
||||||
|
// TableList, UnifiedList 또는 SplitPanelLayout에서 자동 감지
|
||||||
|
if (!dataSourceId && context.allComponents) {
|
||||||
|
// 1. table-list 컴포넌트 찾기
|
||||||
|
const tableListComponent = context.allComponents.find(
|
||||||
|
(comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tableListComponent) {
|
||||||
|
dataSourceId = tableListComponent.componentConfig.tableName;
|
||||||
|
} else {
|
||||||
|
// 2. unified-list 컴포넌트 찾기
|
||||||
|
const unifiedListComponent = context.allComponents.find(
|
||||||
|
(comp: any) =>
|
||||||
|
comp.componentType === "unified-list" &&
|
||||||
|
(comp.componentConfig?.dataSource?.table || comp.componentConfig?.tableName),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unifiedListComponent) {
|
||||||
|
dataSourceId =
|
||||||
|
unifiedListComponent.componentConfig.dataSource?.table ||
|
||||||
|
unifiedListComponent.componentConfig.tableName;
|
||||||
|
console.log("✨ UnifiedList 자동 감지:", {
|
||||||
|
componentId: unifiedListComponent.id,
|
||||||
|
tableName: dataSourceId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 3. split-panel-layout 컴포넌트 찾기
|
||||||
|
const splitPanelComponent = context.allComponents.find(
|
||||||
|
(comp: any) =>
|
||||||
|
comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName,
|
||||||
|
);
|
||||||
|
if (splitPanelComponent) {
|
||||||
|
dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSourceId = dataSourceId || context.tableName || "default";
|
||||||
|
|
||||||
|
console.log("🔍 [handleModal] 데이터 조회 시도:", {
|
||||||
|
dataSourceId,
|
||||||
|
autoDetectDataSource,
|
||||||
|
hasAllComponents: !!context.allComponents,
|
||||||
|
allComponentsCount: context.allComponents?.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// modalDataStore에서 데이터 가져오기
|
||||||
try {
|
try {
|
||||||
const screenInfo = await screenApi.getScreen(config.targetScreenId);
|
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||||
description = screenInfo?.description || "";
|
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||||
|
|
||||||
|
console.log("📦 [handleModal] modalDataStore 상태:", {
|
||||||
|
registryKeys: Object.keys(dataRegistry),
|
||||||
|
targetKey: dataSourceId,
|
||||||
|
hasData: !!dataRegistry[dataSourceId],
|
||||||
|
dataLength: dataRegistry[dataSourceId]?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalData = dataRegistry[dataSourceId] || [];
|
||||||
|
|
||||||
|
if (modalData.length === 0) {
|
||||||
|
console.warn("⚠️ 선택된 데이터가 없습니다:", dataSourceId);
|
||||||
|
toast.warning("선택된 데이터가 없습니다. 먼저 항목을 선택해주세요.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedData = modalData.map((item: any) => item.originalData || item);
|
||||||
|
const rawParentData = modalData[0]?.originalData || modalData[0] || {};
|
||||||
|
parentData = { ...rawParentData };
|
||||||
|
|
||||||
|
// 필드 매핑 적용
|
||||||
|
if (config.fieldMappings?.length) {
|
||||||
|
config.fieldMappings.forEach((mapping) => {
|
||||||
|
if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) {
|
||||||
|
parentData[mapping.targetField] = rawParentData[mapping.sourceField];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("화면 설명을 가져오지 못했습니다:", error);
|
console.error("❌ 데이터 확인 실패:", error);
|
||||||
|
toast.error("데이터 확인 중 오류가 발생했습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2-2. 기본 모드: context에서 직접 가져오기
|
||||||
|
selectedData = context.selectedRowsData || [];
|
||||||
|
parentData = context.splitPanelParentData || {};
|
||||||
|
|
||||||
|
// 필드 매핑 적용
|
||||||
|
if (config.fieldMappings?.length && selectedData.length > 0) {
|
||||||
|
const rawParentData = selectedData[0] || {};
|
||||||
|
config.fieldMappings.forEach((mapping) => {
|
||||||
|
if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) {
|
||||||
|
parentData[mapping.targetField] = rawParentData[mapping.sourceField];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 선택된 행 데이터 수집
|
// 3. 동적 모달 제목 생성
|
||||||
const selectedData = context.selectedRowsData || [];
|
let finalTitle = config.modalTitle || "화면";
|
||||||
console.log("📦 [handleModal] 선택된 데이터:", selectedData);
|
|
||||||
console.log("📦 [handleModal] 분할 패널 부모 데이터:", context.splitPanelParentData);
|
|
||||||
|
|
||||||
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
// 블록 기반 제목 처리
|
||||||
const modalEvent = new CustomEvent("openScreenModal", {
|
if (config.modalTitleBlocks?.length) {
|
||||||
detail: {
|
try {
|
||||||
screenId: config.targetScreenId,
|
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||||
title: config.modalTitle || "화면",
|
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||||
description: description,
|
const titleParts: string[] = [];
|
||||||
size: config.modalSize || "md",
|
|
||||||
// 선택된 행 데이터 전달
|
|
||||||
selectedData: selectedData,
|
|
||||||
selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
|
|
||||||
// 분할 패널 부모 데이터 전달 (탭 안 모달에서 사용)
|
|
||||||
splitPanelParentData: context.splitPanelParentData || {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
window.dispatchEvent(modalEvent);
|
config.modalTitleBlocks.forEach((block) => {
|
||||||
// 모달 열기는 조용히 처리 (토스트 불필요)
|
if (block.type === "text") {
|
||||||
} else {
|
titleParts.push(block.value);
|
||||||
console.error("모달로 열 화면이 지정되지 않았습니다.");
|
} else if (block.type === "field" && block.tableName && block.value) {
|
||||||
return false;
|
const tableData = dataRegistry[block.tableName];
|
||||||
|
if (tableData?.length > 0) {
|
||||||
|
const firstItem = tableData[0].originalData || tableData[0];
|
||||||
|
const value = firstItem[block.value];
|
||||||
|
titleParts.push(value !== undefined && value !== null ? String(value) : block.label || block.value);
|
||||||
|
} else {
|
||||||
|
titleParts.push(block.label || block.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
finalTitle = titleParts.join("");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("동적 제목 생성 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// {tableName.columnName} 패턴 처리
|
||||||
|
else if (finalTitle.includes("{")) {
|
||||||
|
try {
|
||||||
|
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||||
|
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||||
|
const matches = finalTitle.match(/\{([^}]+)\}/g);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
matches.forEach((match) => {
|
||||||
|
const path = match.slice(1, -1);
|
||||||
|
const [tableName, columnName] = path.split(".");
|
||||||
|
if (tableName && columnName) {
|
||||||
|
const tableData = dataRegistry[tableName];
|
||||||
|
if (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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 동적 제목 변환 실패 시 무시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 모달 열기 이벤트 발생
|
||||||
|
console.log("🚀 [handleModal] 모달 열기 이벤트 발생:", {
|
||||||
|
screenId: config.targetScreenId,
|
||||||
|
title: finalTitle,
|
||||||
|
selectedDataCount: selectedData.length,
|
||||||
|
parentDataKeys: Object.keys(parentData),
|
||||||
|
parentData: parentData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// passSelectedData가 true이면 editData로 전달 (수정 모드처럼 모든 필드 표시)
|
||||||
|
const isPassDataMode = passSelectedData && selectedData.length > 0;
|
||||||
|
|
||||||
|
const modalEvent = new CustomEvent("openScreenModal", {
|
||||||
|
detail: {
|
||||||
|
screenId: config.targetScreenId,
|
||||||
|
title: finalTitle,
|
||||||
|
description: description,
|
||||||
|
size: config.modalSize || "md",
|
||||||
|
selectedData: selectedData,
|
||||||
|
selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
|
||||||
|
// 🆕 데이터 전달 모드일 때는 editData로 전달하여 모든 필드가 표시되도록 함
|
||||||
|
editData: isPassDataMode ? parentData : undefined,
|
||||||
|
splitPanelParentData: isPassDataMode ? undefined : parentData,
|
||||||
|
urlParams: dataSourceId ? { dataSourceId } : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.dispatchEvent(modalEvent);
|
||||||
|
|
||||||
|
// 성공 메시지 (autoDetectDataSource 모드에서만)
|
||||||
|
if (autoDetectDataSource && config.successMessage) {
|
||||||
|
toast.success(config.successMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -6701,15 +6880,18 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
|
||||||
type: "navigate",
|
type: "navigate",
|
||||||
},
|
},
|
||||||
openModalWithData: {
|
openModalWithData: {
|
||||||
|
// deprecated: modal로 통합 (하위 호환성 유지)
|
||||||
type: "openModalWithData",
|
type: "openModalWithData",
|
||||||
modalSize: "md",
|
modalSize: "md",
|
||||||
confirmMessage: "다음 단계로 진행하시겠습니까?",
|
passSelectedData: true,
|
||||||
successMessage: "데이터가 전달되었습니다.",
|
autoDetectDataSource: true,
|
||||||
errorMessage: "데이터 전달 중 오류가 발생했습니다.",
|
successMessage: "다음 단계로 진행합니다.",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
type: "modal",
|
type: "modal",
|
||||||
modalSize: "md",
|
modalSize: "md",
|
||||||
|
passSelectedData: true, // 기본: 선택된 데이터 전달
|
||||||
|
autoDetectDataSource: false, // 기본: 자동 감지 비활성화
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
type: "edit",
|
type: "edit",
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ export const useModalDataStore = create<ModalDataState>()(
|
||||||
dataRegistry: {},
|
dataRegistry: {},
|
||||||
|
|
||||||
setData: (sourceId, items) => {
|
setData: (sourceId, items) => {
|
||||||
console.log("📦 [ModalDataStore] 데이터 저장:", { sourceId, itemCount: items.length, items });
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
dataRegistry: {
|
dataRegistry: {
|
||||||
...state.dataRegistry,
|
...state.dataRegistry,
|
||||||
|
|
@ -88,12 +87,10 @@ export const useModalDataStore = create<ModalDataState>()(
|
||||||
|
|
||||||
getData: (sourceId) => {
|
getData: (sourceId) => {
|
||||||
const items = get().dataRegistry[sourceId] || [];
|
const items = get().dataRegistry[sourceId] || [];
|
||||||
console.log("📭 [ModalDataStore] 데이터 조회:", { sourceId, itemCount: items.length });
|
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
|
|
||||||
clearData: (sourceId) => {
|
clearData: (sourceId) => {
|
||||||
console.log("🗑️ [ModalDataStore] 데이터 정리:", { sourceId });
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const { [sourceId]: _, ...rest } = state.dataRegistry;
|
const { [sourceId]: _, ...rest } = state.dataRegistry;
|
||||||
return { dataRegistry: rest };
|
return { dataRegistry: rest };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue