/** * 임베드된 화면 컴포넌트 * 다른 화면 안에 임베드되어 표시되는 화면 */ "use client"; import React, { forwardRef, useImperativeHandle, useState, useEffect, useRef, useCallback } from "react"; import type { ScreenEmbedding, DataReceiver, DataReceivable, EmbeddedScreenHandle, DataReceiveMode, } from "@/types/screen-embedding"; import type { ComponentData } from "@/types/screen"; import { logger } from "@/lib/utils/logger"; import { applyMappingRules, filterDataByCondition } from "@/lib/utils/dataMapping"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { screenApi } from "@/lib/api/screen"; interface EmbeddedScreenProps { embedding: ScreenEmbedding; onSelectionChanged?: (selectedRows: any[]) => void; } /** * 임베드된 화면 컴포넌트 */ export const EmbeddedScreen = forwardRef( ({ embedding, onSelectionChanged }, ref) => { const [layout, setLayout] = useState([]); const [selectedRows, setSelectedRows] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // 컴포넌트 참조 맵 const componentRefs = useRef>(new Map()); // 화면 데이터 로드 useEffect(() => { loadScreenData(); }, [embedding.childScreenId]); // 선택 변경 이벤트 전파 useEffect(() => { onSelectionChanged?.(selectedRows); }, [selectedRows, onSelectionChanged]); /** * 화면 레이아웃 로드 */ const loadScreenData = async () => { try { setLoading(true); setError(null); // 화면 레이아웃 로드 (별도 API) const layoutData = await screenApi.getLayout(embedding.childScreenId); logger.info("📦 화면 레이아웃 로드 완료", { screenId: embedding.childScreenId, mode: embedding.mode, hasLayoutData: !!layoutData, componentsCount: layoutData?.components?.length || 0, }); if (layoutData && layoutData.components && Array.isArray(layoutData.components)) { setLayout(layoutData.components); logger.info("✅ 임베드 화면 컴포넌트 설정 완료", { screenId: embedding.childScreenId, componentsCount: layoutData.components.length, }); } else { logger.warn("⚠️ 화면에 컴포넌트가 없습니다", { screenId: embedding.childScreenId, layoutData, }); setLayout([]); } } catch (err: any) { logger.error("화면 레이아웃 로드 실패", err); setError(err.message || "화면을 불러올 수 없습니다."); } finally { setLoading(false); } }; /** * 컴포넌트 등록 */ const registerComponent = useCallback((id: string, component: DataReceivable) => { componentRefs.current.set(id, component); logger.debug("컴포넌트 등록", { componentId: id, componentType: component.componentType, }); }, []); /** * 컴포넌트 등록 해제 */ const unregisterComponent = useCallback((id: string) => { componentRefs.current.delete(id); logger.debug("컴포넌트 등록 해제", { componentId: id, }); }, []); /** * 선택된 행 업데이트 */ const handleSelectionChange = useCallback((rows: any[]) => { setSelectedRows(rows); }, []); // 외부에서 호출 가능한 메서드 useImperativeHandle(ref, () => ({ /** * 선택된 행 가져오기 */ getSelectedRows: () => { return selectedRows; }, /** * 선택 초기화 */ clearSelection: () => { setSelectedRows([]); }, /** * 데이터 수신 */ receiveData: async (data: any[], receivers: DataReceiver[]) => { logger.info("데이터 수신 시작", { dataCount: data.length, receiversCount: receivers.length, }); const errors: Array<{ componentId: string; error: string }> = []; // 각 데이터 수신자에게 데이터 전달 for (const receiver of receivers) { try { const component = componentRefs.current.get(receiver.targetComponentId); if (!component) { const errorMsg = `컴포넌트를 찾을 수 없습니다: ${receiver.targetComponentId}`; logger.warn(errorMsg); errors.push({ componentId: receiver.targetComponentId, error: errorMsg, }); continue; } // 1. 조건 필터링 let filteredData = data; if (receiver.condition) { filteredData = filterDataByCondition(data, receiver.condition); logger.debug("조건 필터링 적용", { componentId: receiver.targetComponentId, originalCount: data.length, filteredCount: filteredData.length, }); } // 2. 매핑 규칙 적용 const mappedData = applyMappingRules(filteredData, receiver.mappingRules); logger.debug("매핑 규칙 적용", { componentId: receiver.targetComponentId, mappingRulesCount: receiver.mappingRules.length, }); // 3. 검증 if (receiver.validation) { if (receiver.validation.required && mappedData.length === 0) { throw new Error("필수 데이터가 없습니다."); } if (receiver.validation.minRows && mappedData.length < receiver.validation.minRows) { throw new Error(`최소 ${receiver.validation.minRows}개의 데이터가 필요합니다.`); } if (receiver.validation.maxRows && mappedData.length > receiver.validation.maxRows) { throw new Error(`최대 ${receiver.validation.maxRows}개까지만 허용됩니다.`); } } // 4. 데이터 전달 await component.receiveData(mappedData, receiver.mode); logger.info("데이터 전달 성공", { componentId: receiver.targetComponentId, componentType: receiver.targetComponentType, mode: receiver.mode, dataCount: mappedData.length, }); } catch (err: any) { logger.error("데이터 전달 실패", { componentId: receiver.targetComponentId, error: err.message, }); errors.push({ componentId: receiver.targetComponentId, error: err.message, }); } } if (errors.length > 0) { throw new Error(`일부 컴포넌트에 데이터 전달 실패: ${errors.map((e) => e.componentId).join(", ")}`); } }, /** * 현재 데이터 가져오기 */ getData: () => { const allData: Record = {}; componentRefs.current.forEach((component, id) => { allData[id] = component.getData(); }); return allData; }, })); // 로딩 상태 if (loading) { return (

화면을 불러오는 중...

); } // 에러 상태 if (error) { return (

화면을 불러올 수 없습니다

{error}

); } // 화면 렌더링 - 레이아웃 기반 렌더링 return (
{layout.length === 0 ? (

화면에 컴포넌트가 없습니다.

) : (
{layout.map((component) => ( ))}
)}
); }, ); EmbeddedScreen.displayName = "EmbeddedScreen";