모달열기 액션 통합

This commit is contained in:
kjs 2026-01-05 17:44:32 +09:00
parent 3fdc9e36f4
commit a34230ae90
12 changed files with 339 additions and 471 deletions

View File

@ -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">

View File

@ -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">

View File

@ -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,
},

View File

@ -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;

View File

@ -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;
}

View File

@ -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
);
}
}}
/>
);

View File

@ -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),
),
)
: {};

View File

@ -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,

View File

@ -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);
});
}
}

View File

@ -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;
}

View File

@ -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",

View File

@ -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 };