Compare commits

...

2 Commits

6 changed files with 156 additions and 93 deletions

View File

@ -24,6 +24,12 @@ export default function ScreenViewPage() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [formData, setFormData] = useState<Record<string, unknown>>({});
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
// 테이블 새로고침을 위한 키 (값이 변경되면 테이블이 리렌더링됨)
const [tableRefreshKey, setTableRefreshKey] = useState(0);
// 편집 모달 상태
const [editModalOpen, setEditModalOpen] = useState(false);
const [editModalConfig, setEditModalConfig] = useState<{
@ -172,6 +178,19 @@ export default function ScreenViewPage() {
isSelected={false}
isDesignMode={false}
onClick={() => {}}
screenId={screenId}
tableName={screen?.tableName}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
console.log("🔄 테이블 새로고침 요청됨");
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
>
{/* 자식 컴포넌트들 */}
{(component.type === "group" || component.type === "container" || component.type === "area") &&
@ -195,6 +214,19 @@ export default function ScreenViewPage() {
isSelected={false}
isDesignMode={false}
onClick={() => {}}
screenId={screenId}
tableName={screen?.tableName}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
setSelectedRowsData(selectedData);
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
console.log("🔄 테이블 새로고침 요청됨 (자식)");
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
/>
);
})}

View File

@ -48,6 +48,9 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
// 팝업 화면 상태
const [popupScreen, setPopupScreen] = useState<{
screenId: number;
@ -186,6 +189,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
onFormDataChange={handleFormDataChange}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(selectedRows, selectedData) => {
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
onRefresh={() => {
console.log("🔄 버튼에서 테이블 새로고침 요청됨");
// 테이블 컴포넌트는 자체적으로 loadData 호출

View File

@ -34,6 +34,14 @@ interface RealtimePreviewProps {
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러
onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러
onConfigChange?: (config: any) => void; // 설정 변경 핸들러
// 버튼 액션을 위한 props
screenId?: number;
tableName?: string;
selectedRowsData?: any[];
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
refreshKey?: number;
onRefresh?: () => void;
}
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
@ -77,6 +85,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onZoneComponentDrop,
onZoneClick,
onConfigChange,
screenId,
tableName,
selectedRowsData,
onSelectedRowsChange,
refreshKey,
onRefresh,
}) => {
const { id, type, position, size, style: componentStyle } = component;
@ -178,6 +192,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onZoneComponentDrop={onZoneComponentDrop}
onZoneClick={onZoneClick}
onConfigChange={onConfigChange}
screenId={screenId}
tableName={tableName}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={onSelectedRowsChange}
refreshKey={refreshKey}
onRefresh={onRefresh}
/>
</div>

View File

@ -66,12 +66,18 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [component.id]);
// 화면 목록 가져오기
// 화면 목록 가져오기 (전체 목록)
useEffect(() => {
const fetchScreens = async () => {
try {
setScreensLoading(true);
const response = await apiClient.get("/screen-management/screens");
// 전체 목록을 가져오기 위해 size를 큰 값으로 설정
const response = await apiClient.get("/screen-management/screens", {
params: {
page: 1,
size: 9999, // 매우 큰 값으로 설정하여 전체 목록 가져오기
},
});
if (response.data.success && Array.isArray(response.data.data)) {
const screenList = response.data.data.map((screen: any) => ({
@ -194,17 +200,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
</SelectTrigger>
<SelectContent>
<SelectItem value="save"></SelectItem>
<SelectItem value="cancel"></SelectItem>
<SelectItem value="delete"></SelectItem>
<SelectItem value="edit"></SelectItem>
<SelectItem value="add"></SelectItem>
<SelectItem value="search"></SelectItem>
<SelectItem value="reset"></SelectItem>
<SelectItem value="submit"></SelectItem>
<SelectItem value="close"></SelectItem>
<SelectItem value="modal"> </SelectItem>
<SelectItem value="edit"></SelectItem>
<SelectItem value="navigate"> </SelectItem>
<SelectItem value="control"> ( )</SelectItem>
<SelectItem value="modal"> </SelectItem>
<SelectItem value="control"> </SelectItem>
</SelectContent>
</Select>
</div>

View File

@ -64,6 +64,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
selectedRowsData,
...props
}) => {
console.log("🔵 ButtonPrimaryComponent 렌더링, 받은 props:", {
componentId: component.id,
hasSelectedRowsData: !!selectedRowsData,
selectedRowsDataLength: selectedRowsData?.length,
selectedRowsData,
tableName,
screenId,
});
// 확인 다이얼로그 상태
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingAction, setPendingAction] = useState<{
@ -204,7 +213,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}
// 확인 다이얼로그가 필요한 액션 타입들
const confirmationRequiredActions: ButtonActionType[] = ["save", "submit", "delete"];
const confirmationRequiredActions: ButtonActionType[] = ["save", "delete"];
// 실제 액션 실행 함수
const executeAction = async (actionConfig: any, context: ButtonActionContext) => {
@ -221,8 +230,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 추가 안전장치: 모든 로딩 토스트 제거
toast.dismiss();
// edit 액션을 제외하고만 로딩 토스트 표시
if (actionConfig.type !== "edit") {
// UI 전환 액션(edit, modal, navigate)을 제외하고만 로딩 토스트 표시
const silentActions = ["edit", "modal", "navigate"];
if (!silentActions.includes(actionConfig.type)) {
console.log("📱 로딩 토스트 표시 시작");
currentLoadingToastRef.current = toast.loading(
actionConfig.type === "save"
@ -237,9 +247,16 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
},
);
console.log("📱 로딩 토스트 ID:", currentLoadingToastRef.current);
} else {
console.log("🔕 UI 전환 액션은 로딩 토스트 표시 안함:", actionConfig.type);
}
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
console.log("🔍 actionConfig 확인:", {
type: actionConfig.type,
successMessage: actionConfig.successMessage,
errorMessage: actionConfig.errorMessage,
});
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
@ -252,37 +269,70 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 실패한 경우 오류 처리
if (!success) {
// UI 전환 액션(edit, modal, navigate)은 에러도 조용히 처리
const silentActions = ["edit", "modal", "navigate"];
if (silentActions.includes(actionConfig.type)) {
console.log("🔕 UI 전환 액션 실패지만 에러 토스트 표시 안함:", actionConfig.type);
return;
}
console.log("❌ 액션 실패, 오류 토스트 표시");
const errorMessage =
actionConfig.errorMessage ||
(actionConfig.type === "save"
// 기본 에러 메시지 결정
const defaultErrorMessage =
actionConfig.type === "save"
? "저장 중 오류가 발생했습니다."
: actionConfig.type === "delete"
? "삭제 중 오류가 발생했습니다."
: actionConfig.type === "submit"
? "제출 중 오류가 발생했습니다."
: "처리 중 오류가 발생했습니다.");
: "처리 중 오류가 발생했습니다.";
// 커스텀 메시지 사용 조건:
// 1. 커스텀 메시지가 있고
// 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우)
const useCustomMessage =
actionConfig.errorMessage &&
(actionConfig.type === "save" || !actionConfig.errorMessage.includes("저장"));
const errorMessage = useCustomMessage ? actionConfig.errorMessage : defaultErrorMessage;
console.log("🔍 에러 메시지 결정:", {
actionType: actionConfig.type,
customMessage: actionConfig.errorMessage,
useCustom: useCustomMessage,
finalMessage: errorMessage
});
toast.error(errorMessage);
return;
}
// 성공한 경우에만 성공 토스트 표시
// edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
if (actionConfig.type !== "edit") {
const successMessage =
actionConfig.successMessage ||
(actionConfig.type === "save"
// edit, modal, navigate 액션은 조용히 처리 (UI 전환만 하므로 토스트 불필요)
if (actionConfig.type !== "edit" && actionConfig.type !== "modal" && actionConfig.type !== "navigate") {
// 기본 성공 메시지 결정
const defaultSuccessMessage =
actionConfig.type === "save"
? "저장되었습니다."
: actionConfig.type === "delete"
? "삭제되었습니다."
: actionConfig.type === "submit"
? "제출되었습니다."
: "완료되었습니다.");
: "완료되었습니다.";
// 커스텀 메시지 사용 조건:
// 1. 커스텀 메시지가 있고
// 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우)
const useCustomMessage =
actionConfig.successMessage &&
(actionConfig.type === "save" || !actionConfig.successMessage.includes("저장"));
const successMessage = useCustomMessage ? actionConfig.successMessage : defaultSuccessMessage;
console.log("🎉 성공 토스트 표시:", successMessage);
toast.success(successMessage);
} else {
console.log("🔕 edit 액션은 조용히 처리 (토스트 없음)");
console.log("🔕 UI 전환 액션은 조용히 처리 (토스트 없음):", actionConfig.type);
}
console.log("✅ 버튼 액션 실행 성공:", actionConfig.type);
@ -357,6 +407,13 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
requiresConfirmation: confirmationRequiredActions.includes(processedConfig.action.type),
});
// 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단
if (processedConfig.action.type === "delete" && (!selectedRowsData || selectedRowsData.length === 0)) {
console.log("⚠️ 삭제할 데이터가 선택되지 않았습니다.");
toast.warning("삭제할 항목을 먼저 선택해주세요.");
return;
}
const context: ButtonActionContext = {
formData: formData || {},
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
@ -370,6 +427,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
selectedRowsData,
};
console.log("🔍 버튼 액션 실행 전 context 확인:", {
hasSelectedRowsData: !!selectedRowsData,
selectedRowsDataLength: selectedRowsData?.length,
selectedRowsData,
tableName,
screenId,
formData,
});
// 확인이 필요한 액션인지 확인
if (confirmationRequiredActions.includes(processedConfig.action.type)) {
console.log("📋 확인 다이얼로그 표시 중...");

View File

@ -11,18 +11,11 @@ import type { ExtendedControlContext } from "@/types/control-management";
*/
export type ButtonActionType =
| "save" // 저장
| "cancel" // 취소
| "delete" // 삭제
| "edit" // 편집
| "add" // 추가
| "search" // 검색
| "reset" // 초기화
| "submit" // 제출
| "close" // 닫기
| "popup" // 팝업 열기
| "navigate" // 페이지 이동
| "modal" // 모달 열기
| "newWindow"; // 새 창 열기
| "control"; // 제어 흐름
/**
*
@ -92,42 +85,18 @@ export class ButtonActionExecutor {
case "save":
return await this.handleSave(config, context);
case "submit":
return await this.handleSubmit(config, context);
case "delete":
return await this.handleDelete(config, context);
case "reset":
return this.handleReset(config, context);
case "cancel":
return this.handleCancel(config, context);
case "navigate":
return this.handleNavigate(config, context);
case "modal":
return this.handleModal(config, context);
case "newWindow":
return this.handleNewWindow(config, context);
case "popup":
return this.handlePopup(config, context);
case "search":
return this.handleSearch(config, context);
case "add":
return this.handleAdd(config, context);
case "edit":
return this.handleEdit(config, context);
case "close":
return this.handleClose(config, context);
case "control":
return this.handleControl(config, context);
@ -515,9 +484,9 @@ export class ButtonActionExecutor {
});
window.dispatchEvent(modalEvent);
toast.success("모달 화면이 열렸습니다.");
// 모달 열기는 조용히 처리 (토스트 불필요)
} else {
toast.error("모달로 열 화면이 지정되지 않았습니다.");
console.error("모달로 열 화면이 지정되지 않았습니다.");
return false;
}
@ -1421,26 +1390,12 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
successMessage: "저장되었습니다.",
errorMessage: "저장 중 오류가 발생했습니다.",
},
submit: {
type: "submit",
validateForm: true,
successMessage: "제출되었습니다.",
errorMessage: "제출 중 오류가 발생했습니다.",
},
delete: {
type: "delete",
confirmMessage: "정말 삭제하시겠습니까?",
successMessage: "삭제되었습니다.",
errorMessage: "삭제 중 오류가 발생했습니다.",
},
reset: {
type: "reset",
confirmMessage: "초기화하시겠습니까?",
successMessage: "초기화되었습니다.",
},
cancel: {
type: "cancel",
},
navigate: {
type: "navigate",
},
@ -1448,29 +1403,11 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
type: "modal",
modalSize: "md",
},
newWindow: {
type: "newWindow",
popupWidth: 800,
popupHeight: 600,
},
popup: {
type: "popup",
popupWidth: 600,
popupHeight: 400,
},
search: {
type: "search",
successMessage: "검색을 실행했습니다.",
},
add: {
type: "add",
successMessage: "추가되었습니다.",
},
edit: {
type: "edit",
successMessage: "편집되었습니다.",
},
close: {
type: "close",
control: {
type: "control",
},
};