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