# 화면 임베딩 및 데이터 전달 시스템 구현 계획서 ## 📋 목차 1. [개요](#개요) 2. [현재 문제점](#현재-문제점) 3. [목표](#목표) 4. [시스템 아키텍처](#시스템-아키텍처) 5. [데이터베이스 설계](#데이터베이스-설계) 6. [타입 정의](#타입-정의) 7. [컴포넌트 구조](#컴포넌트-구조) 8. [API 설계](#api-설계) 9. [구현 단계](#구현-단계) 10. [사용 시나리오](#사용-시나리오) --- ## 개요 ### 배경 현재 화면관리 시스템은 단일 화면 단위로만 동작하며, 화면 간 데이터 전달이나 화면 임베딩이 불가능합니다. 실무에서는 "입고 등록"과 같이 **좌측에서 데이터를 선택하고 우측으로 전달하여 처리하는** 복잡한 워크플로우가 필요합니다. ### 핵심 요구사항 - **화면 임베딩**: 기존 화면을 다른 화면 안에 재사용 - **데이터 전달**: 한 화면에서 선택한 데이터를 다른 화면의 컴포넌트로 전달 - **유연한 매핑**: 테이블뿐만 아니라 입력 필드, 셀렉트 박스, 리피터 등 모든 컴포넌트에 데이터 주입 가능 - **변환 함수**: 합계, 평균, 개수 등 데이터 변환 지원 --- ## 현재 문제점 ### 1. 화면 재사용 불가 - 각 화면은 독립적으로만 동작 - 동일한 기능을 여러 화면에서 중복 구현 ### 2. 화면 간 데이터 전달 불가 - 한 화면에서 선택한 데이터를 다른 화면으로 전달할 수 없음 - 사용자가 수동으로 복사/붙여넣기 해야 함 ### 3. 복잡한 워크플로우 구현 불가 - "발주 목록 조회 → 품목 선택 → 입고 등록"과 같은 프로세스를 단일 화면에서 처리 불가 - 여러 화면을 오가며 작업해야 하는 불편함 ### 4. 컴포넌트별 데이터 주입 불가 - 테이블에만 데이터를 추가할 수 있음 - 입력 필드, 셀렉트 박스 등에 자동으로 값을 설정할 수 없음 --- ## 목표 ### 주요 목표 1. **화면 임베딩 시스템 구축**: 기존 화면을 컨테이너로 사용 2. **범용 데이터 전달 시스템**: 모든 컴포넌트 타입 지원 3. **시각적 매핑 설정 UI**: 드래그앤드롭으로 매핑 규칙 설정 4. **실시간 미리보기**: 데이터 전달 결과를 즉시 확인 ### 부가 목표 - 조건부 데이터 전달 (필터링) - 데이터 변환 함수 (합계, 평균, 개수 등) - 양방향 데이터 동기화 - 트랜잭션 지원 (전체 성공 또는 전체 실패) --- ## 시스템 아키텍처 ### 전체 구조 ``` ┌─────────────────────────────────────────────────────────┐ │ Screen Split Panel │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ Left Screen │ │ Right Screen │ │ │ │ (Source) │ │ (Target) │ │ │ │ │ │ │ │ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ │ │ │ Table │ │ │ │ Form │ │ │ │ │ │ (Select) │ │ │ │ │ │ │ │ │ └────────────┘ │ │ └────────────┘ │ │ │ │ │ │ │ │ │ │ [✓] Row 1 │ │ Input: ____ │ │ │ │ [✓] Row 2 │ │ Select: [ ] │ │ │ │ [ ] Row 3 │ │ │ │ │ │ │ │ ┌────────────┐ │ │ │ └──────────────────┘ │ │ Table │ │ │ │ │ │ │ (Append) │ │ │ │ │ │ └────────────┘ │ │ │ ▼ │ │ │ │ [선택 품목 추가] ──────────▶│ Row 1 (Added) │ │ │ │ Row 2 (Added) │ │ │ └──────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 레이어 구조 ``` ┌─────────────────────────────────────────┐ │ Presentation Layer (UI) │ │ - ScreenSplitPanel │ │ - EmbeddedScreen │ │ - DataMappingConfig │ └─────────────────────────────────────────┘ │ ┌─────────────────────────────────────────┐ │ Business Logic Layer │ │ - DataTransferService │ │ - MappingEngine │ │ - TransformFunctions │ └─────────────────────────────────────────┘ │ ┌─────────────────────────────────────────┐ │ Data Access Layer │ │ - screen_embedding (테이블) │ │ - screen_data_transfer (테이블) │ │ - component_data_receiver (인터페이스) │ └─────────────────────────────────────────┘ ``` --- ## 데이터베이스 설계 ### 1. screen_embedding (화면 임베딩 설정) ```sql CREATE TABLE screen_embedding ( id SERIAL PRIMARY KEY, -- 부모 화면 (컨테이너) parent_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), -- 자식 화면 (임베드될 화면) child_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), -- 임베딩 위치 position VARCHAR(20) NOT NULL, -- 'left', 'right', 'top', 'bottom', 'center' -- 임베딩 모드 mode VARCHAR(20) NOT NULL, -- 'view', 'select', 'form', 'edit' -- 추가 설정 config JSONB, -- { -- "width": "50%", -- "height": "100%", -- "resizable": true, -- "multiSelect": true, -- "showToolbar": true -- } -- 멀티테넌시 company_code VARCHAR(20) NOT NULL, -- 메타데이터 created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by VARCHAR(50), CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE, CONSTRAINT fk_child_screen FOREIGN KEY (child_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE ); -- 인덱스 CREATE INDEX idx_screen_embedding_parent ON screen_embedding(parent_screen_id, company_code); CREATE INDEX idx_screen_embedding_child ON screen_embedding(child_screen_id, company_code); ``` ### 2. screen_data_transfer (데이터 전달 설정) ```sql CREATE TABLE screen_data_transfer ( id SERIAL PRIMARY KEY, -- 소스 화면 (데이터 제공) source_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), -- 타겟 화면 (데이터 수신) target_screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), -- 소스 컴포넌트 (선택 영역) source_component_id VARCHAR(100), source_component_type VARCHAR(50), -- 'table', 'list', 'grid' -- 데이터 수신자 설정 (JSONB 배열) data_receivers JSONB NOT NULL, -- [ -- { -- "targetComponentId": "table-입고처리품목", -- "targetComponentType": "table", -- "mode": "append", -- "mappingRules": [ -- { -- "sourceField": "품목코드", -- "targetField": "품목코드", -- "transform": null -- } -- ], -- "condition": { -- "field": "상태", -- "operator": "equals", -- "value": "승인" -- } -- } -- ] -- 전달 버튼 설정 button_config JSONB, -- { -- "label": "선택 품목 추가", -- "position": "center", -- "icon": "ArrowRight", -- "validation": { -- "requireSelection": true, -- "minSelection": 1, -- "maxSelection": 100, -- "customValidation": "function(rows) { return rows.length > 0; }" -- } -- } -- 멀티테넌시 company_code VARCHAR(20) NOT NULL, -- 메타데이터 created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by VARCHAR(50), CONSTRAINT fk_source_screen FOREIGN KEY (source_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE, CONSTRAINT fk_target_screen FOREIGN KEY (target_screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE ); -- 인덱스 CREATE INDEX idx_screen_data_transfer_source ON screen_data_transfer(source_screen_id, company_code); CREATE INDEX idx_screen_data_transfer_target ON screen_data_transfer(target_screen_id, company_code); ``` ### 3. screen_split_panel (분할 패널 설정) ```sql CREATE TABLE screen_split_panel ( id SERIAL PRIMARY KEY, -- 부모 화면 (분할 패널 컨테이너) screen_id INTEGER NOT NULL REFERENCES screen_info(screen_id), -- 좌측 화면 임베딩 left_embedding_id INTEGER REFERENCES screen_embedding(id), -- 우측 화면 임베딩 right_embedding_id INTEGER REFERENCES screen_embedding(id), -- 데이터 전달 설정 data_transfer_id INTEGER REFERENCES screen_data_transfer(id), -- 레이아웃 설정 layout_config JSONB, -- { -- "splitRatio": 50, // 좌:우 비율 (0-100) -- "resizable": true, -- "minLeftWidth": 300, -- "minRightWidth": 400, -- "orientation": "horizontal" // 'horizontal' | 'vertical' -- } -- 멀티테넌시 company_code VARCHAR(20) NOT NULL, -- 메타데이터 created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT fk_screen FOREIGN KEY (screen_id) REFERENCES screen_info(screen_id) ON DELETE CASCADE, CONSTRAINT fk_left_embedding FOREIGN KEY (left_embedding_id) REFERENCES screen_embedding(id) ON DELETE SET NULL, CONSTRAINT fk_right_embedding FOREIGN KEY (right_embedding_id) REFERENCES screen_embedding(id) ON DELETE SET NULL, CONSTRAINT fk_data_transfer FOREIGN KEY (data_transfer_id) REFERENCES screen_data_transfer(id) ON DELETE SET NULL ); -- 인덱스 CREATE INDEX idx_screen_split_panel_screen ON screen_split_panel(screen_id, company_code); ``` --- ## 타입 정의 ### 1. 화면 임베딩 타입 ```typescript // 임베딩 모드 type EmbeddingMode = | "view" // 읽기 전용 | "select" // 선택 모드 (체크박스) | "form" // 폼 입력 모드 | "edit"; // 편집 모드 // 임베딩 위치 type EmbeddingPosition = | "left" | "right" | "top" | "bottom" | "center"; // 화면 임베딩 설정 interface ScreenEmbedding { id: number; parentScreenId: number; childScreenId: number; position: EmbeddingPosition; mode: EmbeddingMode; config: { width?: string; // "50%", "400px" height?: string; // "100%", "600px" resizable?: boolean; multiSelect?: boolean; showToolbar?: boolean; showSearch?: boolean; showPagination?: boolean; }; companyCode: string; } ``` ### 2. 데이터 전달 타입 ```typescript // 컴포넌트 타입 type ComponentType = | "table" // 테이블 | "input" // 입력 필드 | "select" // 셀렉트 박스 | "textarea" // 텍스트 영역 | "checkbox" // 체크박스 | "radio" // 라디오 버튼 | "date" // 날짜 선택 | "repeater" // 리피터 (반복 그룹) | "form-group" // 폼 그룹 | "hidden"; // 히든 필드 // 데이터 수신 모드 type DataReceiveMode = | "append" // 기존 데이터에 추가 | "replace" // 기존 데이터 덮어쓰기 | "merge"; // 기존 데이터와 병합 (키 기준) // 변환 함수 type TransformFunction = | "none" // 변환 없음 | "sum" // 합계 | "average" // 평균 | "count" // 개수 | "min" // 최소값 | "max" // 최대값 | "first" // 첫 번째 값 | "last" // 마지막 값 | "concat" // 문자열 결합 | "join" // 배열 결합 | "custom"; // 커스텀 함수 // 조건 연산자 type ConditionOperator = | "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "in" | "notIn"; // 매핑 규칙 interface MappingRule { sourceField: string; // 소스 필드명 targetField: string; // 타겟 필드명 transform?: TransformFunction; // 변환 함수 transformConfig?: any; // 변환 함수 설정 defaultValue?: any; // 기본값 required?: boolean; // 필수 여부 } // 조건 interface Condition { field: string; operator: ConditionOperator; value: any; } // 데이터 수신자 interface DataReceiver { targetComponentId: string; // 타겟 컴포넌트 ID targetComponentType: ComponentType; mode: DataReceiveMode; mappingRules: MappingRule[]; condition?: Condition; // 조건부 전달 validation?: { required?: boolean; minRows?: number; maxRows?: number; customValidation?: string; // JavaScript 함수 문자열 }; } // 버튼 설정 interface TransferButtonConfig { label: string; position: "left" | "right" | "center"; icon?: string; variant?: "default" | "outline" | "ghost"; size?: "sm" | "default" | "lg"; validation?: { requireSelection: boolean; minSelection?: number; maxSelection?: number; confirmMessage?: string; customValidation?: string; }; } // 데이터 전달 설정 interface ScreenDataTransfer { id: number; sourceScreenId: number; targetScreenId: number; sourceComponentId?: string; sourceComponentType?: string; dataReceivers: DataReceiver[]; buttonConfig: TransferButtonConfig; companyCode: string; } ``` ### 3. 분할 패널 타입 ```typescript // 레이아웃 설정 interface LayoutConfig { splitRatio: number; // 0-100 (좌측 비율) resizable: boolean; minLeftWidth?: number; // 최소 좌측 너비 (px) minRightWidth?: number; // 최소 우측 너비 (px) orientation: "horizontal" | "vertical"; } // 분할 패널 설정 interface ScreenSplitPanel { id: number; screenId: number; leftEmbedding: ScreenEmbedding; rightEmbedding: ScreenEmbedding; dataTransfer: ScreenDataTransfer; layoutConfig: LayoutConfig; companyCode: string; } ``` ### 4. 컴포넌트 인터페이스 ```typescript // 모든 데이터 수신 가능 컴포넌트가 구현해야 하는 인터페이스 interface DataReceivable { // 컴포넌트 ID componentId: string; // 컴포넌트 타입 componentType: ComponentType; // 데이터 수신 receiveData(data: any[], mode: DataReceiveMode): Promise; // 현재 데이터 가져오기 getData(): any; // 데이터 초기화 clearData(): void; // 검증 validate(): boolean; // 이벤트 리스너 onDataReceived?: (data: any[]) => void; onDataCleared?: () => void; } // 선택 가능 컴포넌트 인터페이스 interface Selectable { // 선택된 행/항목 가져오기 getSelectedRows(): any[]; // 선택 초기화 clearSelection(): void; // 전체 선택 selectAll(): void; // 선택 이벤트 onSelectionChanged?: (selectedRows: any[]) => void; } ``` --- ## 컴포넌트 구조 ### 1. ScreenSplitPanel (최상위 컨테이너) ```tsx interface ScreenSplitPanelProps { config: ScreenSplitPanel; onDataTransferred?: (data: any[]) => void; } export function ScreenSplitPanel({ config, onDataTransferred }: ScreenSplitPanelProps) { const leftScreenRef = useRef(null); const rightScreenRef = useRef(null); const [splitRatio, setSplitRatio] = useState(config.layoutConfig.splitRatio); // 데이터 전달 핸들러 const handleTransferData = async () => { // 1. 좌측 화면에서 선택된 데이터 가져오기 const selectedRows = leftScreenRef.current?.getSelectedRows() || []; if (selectedRows.length === 0) { toast.error("선택된 항목이 없습니다."); return; } // 2. 검증 if (config.dataTransfer.buttonConfig.validation) { const validation = config.dataTransfer.buttonConfig.validation; if (validation.minSelection && selectedRows.length < validation.minSelection) { toast.error(`최소 ${validation.minSelection}개 이상 선택해야 합니다.`); return; } if (validation.maxSelection && selectedRows.length > validation.maxSelection) { toast.error(`최대 ${validation.maxSelection}개까지만 선택할 수 있습니다.`); return; } if (validation.confirmMessage) { const confirmed = await confirm(validation.confirmMessage); if (!confirmed) return; } } // 3. 데이터 전달 try { await rightScreenRef.current?.receiveData( selectedRows, config.dataTransfer.dataReceivers ); toast.success("데이터가 전달되었습니다."); onDataTransferred?.(selectedRows); // 4. 좌측 선택 초기화 (옵션) if (config.dataTransfer.buttonConfig.clearAfterTransfer) { leftScreenRef.current?.clearSelection(); } } catch (error) { toast.error("데이터 전달 중 오류가 발생했습니다."); console.error(error); } }; return (
{/* 좌측 패널 */}
{/* 리사이저 */} {config.layoutConfig.resizable && ( setSplitRatio(newRatio)} /> )} {/* 전달 버튼 */}
{/* 우측 패널 */}
); } ``` ### 2. EmbeddedScreen (임베드된 화면) ```tsx interface EmbeddedScreenProps { embedding: ScreenEmbedding; } export interface EmbeddedScreenHandle { getSelectedRows(): any[]; clearSelection(): void; receiveData(data: any[], receivers: DataReceiver[]): Promise; getData(): any; } export const EmbeddedScreen = forwardRef( ({ embedding }, ref) => { const [screenData, setScreenData] = useState(null); const [selectedRows, setSelectedRows] = useState([]); const componentRefs = useRef>(new Map()); // 화면 데이터 로드 useEffect(() => { loadScreenData(embedding.childScreenId); }, [embedding.childScreenId]); // 외부에서 호출 가능한 메서드 useImperativeHandle(ref, () => ({ getSelectedRows: () => selectedRows, clearSelection: () => { setSelectedRows([]); }, receiveData: async (data: any[], receivers: DataReceiver[]) => { // 각 데이터 수신자에게 데이터 전달 for (const receiver of receivers) { const component = componentRefs.current.get(receiver.targetComponentId); if (!component) { console.warn(`컴포넌트를 찾을 수 없습니다: ${receiver.targetComponentId}`); continue; } // 조건 확인 let filteredData = data; if (receiver.condition) { filteredData = filterData(data, receiver.condition); } // 매핑 적용 const mappedData = applyMappingRules(filteredData, receiver.mappingRules); // 데이터 전달 await component.receiveData(mappedData, receiver.mode); } }, getData: () => { const allData: Record = {}; componentRefs.current.forEach((component, id) => { allData[id] = component.getData(); }); return allData; } })); // 컴포넌트 등록 const registerComponent = (id: string, component: DataReceivable) => { componentRefs.current.set(id, component); }; return (
{screenData && ( )}
); } ); ``` ### 3. DataReceivable 구현 예시 #### TableComponent ```typescript class TableComponent implements DataReceivable { componentId: string; componentType: ComponentType = "table"; private rows: any[] = []; async receiveData(data: any[], mode: DataReceiveMode): Promise { switch (mode) { case "append": this.rows = [...this.rows, ...data]; break; case "replace": this.rows = data; break; case "merge": // 키 기반 병합 (예: id 필드) const existingIds = new Set(this.rows.map(r => r.id)); const newRows = data.filter(r => !existingIds.has(r.id)); this.rows = [...this.rows, ...newRows]; break; } this.render(); this.onDataReceived?.(data); } getData(): any { return this.rows; } clearData(): void { this.rows = []; this.render(); this.onDataCleared?.(); } validate(): boolean { return this.rows.length > 0; } private render() { // 테이블 리렌더링 } } ``` #### InputComponent ```typescript class InputComponent implements DataReceivable { componentId: string; componentType: ComponentType = "input"; private value: any = ""; async receiveData(data: any[], mode: DataReceiveMode): Promise { // 입력 필드는 단일 값이므로 첫 번째 항목만 사용 if (data.length > 0) { this.value = data[0]; this.render(); this.onDataReceived?.(data); } } getData(): any { return this.value; } clearData(): void { this.value = ""; this.render(); this.onDataCleared?.(); } validate(): boolean { return this.value !== null && this.value !== undefined && this.value !== ""; } private render() { // 입력 필드 리렌더링 } } ``` --- ## API 설계 ### 1. 화면 임베딩 API ```typescript // GET /api/screen-embedding/:parentScreenId export async function getScreenEmbeddings( parentScreenId: number, companyCode: string ): Promise> { const query = ` SELECT * FROM screen_embedding WHERE parent_screen_id = $1 AND company_code = $2 ORDER BY position `; const result = await pool.query(query, [parentScreenId, companyCode]); return { success: true, data: result.rows }; } // POST /api/screen-embedding export async function createScreenEmbedding( embedding: Omit, companyCode: string ): Promise> { const query = ` INSERT INTO screen_embedding ( parent_screen_id, child_screen_id, position, mode, config, company_code ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; const result = await pool.query(query, [ embedding.parentScreenId, embedding.childScreenId, embedding.position, embedding.mode, JSON.stringify(embedding.config), companyCode ]); return { success: true, data: result.rows[0] }; } // PUT /api/screen-embedding/:id export async function updateScreenEmbedding( id: number, embedding: Partial, companyCode: string ): Promise> { const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (embedding.position) { updates.push(`position = $${paramIndex++}`); values.push(embedding.position); } if (embedding.mode) { updates.push(`mode = $${paramIndex++}`); values.push(embedding.mode); } if (embedding.config) { updates.push(`config = $${paramIndex++}`); values.push(JSON.stringify(embedding.config)); } updates.push(`updated_at = NOW()`); values.push(id, companyCode); const query = ` UPDATE screen_embedding SET ${updates.join(", ")} WHERE id = $${paramIndex++} AND company_code = $${paramIndex++} RETURNING * `; const result = await pool.query(query, values); if (result.rowCount === 0) { return { success: false, message: "임베딩 설정을 찾을 수 없습니다." }; } return { success: true, data: result.rows[0] }; } // DELETE /api/screen-embedding/:id export async function deleteScreenEmbedding( id: number, companyCode: string ): Promise> { const query = ` DELETE FROM screen_embedding WHERE id = $1 AND company_code = $2 `; const result = await pool.query(query, [id, companyCode]); if (result.rowCount === 0) { return { success: false, message: "임베딩 설정을 찾을 수 없습니다." }; } return { success: true }; } ``` ### 2. 데이터 전달 API ```typescript // GET /api/screen-data-transfer?sourceScreenId=1&targetScreenId=2 export async function getScreenDataTransfer( sourceScreenId: number, targetScreenId: number, companyCode: string ): Promise> { const query = ` SELECT * FROM screen_data_transfer WHERE source_screen_id = $1 AND target_screen_id = $2 AND company_code = $3 `; const result = await pool.query(query, [sourceScreenId, targetScreenId, companyCode]); if (result.rowCount === 0) { return { success: false, message: "데이터 전달 설정을 찾을 수 없습니다." }; } return { success: true, data: result.rows[0] }; } // POST /api/screen-data-transfer export async function createScreenDataTransfer( transfer: Omit, companyCode: string ): Promise> { const query = ` INSERT INTO screen_data_transfer ( source_screen_id, target_screen_id, source_component_id, source_component_type, data_receivers, button_config, company_code ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; const result = await pool.query(query, [ transfer.sourceScreenId, transfer.targetScreenId, transfer.sourceComponentId, transfer.sourceComponentType, JSON.stringify(transfer.dataReceivers), JSON.stringify(transfer.buttonConfig), companyCode ]); return { success: true, data: result.rows[0] }; } // PUT /api/screen-data-transfer/:id export async function updateScreenDataTransfer( id: number, transfer: Partial, companyCode: string ): Promise> { const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (transfer.dataReceivers) { updates.push(`data_receivers = $${paramIndex++}`); values.push(JSON.stringify(transfer.dataReceivers)); } if (transfer.buttonConfig) { updates.push(`button_config = $${paramIndex++}`); values.push(JSON.stringify(transfer.buttonConfig)); } updates.push(`updated_at = NOW()`); values.push(id, companyCode); const query = ` UPDATE screen_data_transfer SET ${updates.join(", ")} WHERE id = $${paramIndex++} AND company_code = $${paramIndex++} RETURNING * `; const result = await pool.query(query, values); if (result.rowCount === 0) { return { success: false, message: "데이터 전달 설정을 찾을 수 없습니다." }; } return { success: true, data: result.rows[0] }; } ``` ### 3. 분할 패널 API ```typescript // GET /api/screen-split-panel/:screenId export async function getScreenSplitPanel( screenId: number, companyCode: string ): Promise> { const query = ` SELECT ssp.*, le.* as left_embedding, re.* as right_embedding, sdt.* as data_transfer FROM screen_split_panel ssp LEFT JOIN screen_embedding le ON ssp.left_embedding_id = le.id LEFT JOIN screen_embedding re ON ssp.right_embedding_id = re.id LEFT JOIN screen_data_transfer sdt ON ssp.data_transfer_id = sdt.id WHERE ssp.screen_id = $1 AND ssp.company_code = $2 `; const result = await pool.query(query, [screenId, companyCode]); if (result.rowCount === 0) { return { success: false, message: "분할 패널 설정을 찾을 수 없습니다." }; } return { success: true, data: result.rows[0] }; } // POST /api/screen-split-panel export async function createScreenSplitPanel( panel: Omit, companyCode: string ): Promise> { const client = await pool.connect(); try { await client.query("BEGIN"); // 1. 좌측 임베딩 생성 const leftEmbedding = await createScreenEmbedding(panel.leftEmbedding, companyCode); // 2. 우측 임베딩 생성 const rightEmbedding = await createScreenEmbedding(panel.rightEmbedding, companyCode); // 3. 데이터 전달 설정 생성 const dataTransfer = await createScreenDataTransfer(panel.dataTransfer, companyCode); // 4. 분할 패널 생성 const query = ` INSERT INTO screen_split_panel ( screen_id, left_embedding_id, right_embedding_id, data_transfer_id, layout_config, company_code ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; const result = await client.query(query, [ panel.screenId, leftEmbedding.data!.id, rightEmbedding.data!.id, dataTransfer.data!.id, JSON.stringify(panel.layoutConfig), companyCode ]); await client.query("COMMIT"); return { success: true, data: result.rows[0] }; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } ``` --- ## 구현 단계 ### Phase 1: 기본 인프라 구축 (1-2주) #### 1.1 데이터베이스 마이그레이션 - [ ] `screen_embedding` 테이블 생성 - [ ] `screen_data_transfer` 테이블 생성 - [ ] `screen_split_panel` 테이블 생성 - [ ] 인덱스 및 외래키 설정 - [ ] 샘플 데이터 삽입 #### 1.2 타입 정의 - [ ] TypeScript 인터페이스 작성 - [ ] `types/screen-embedding.ts` - [ ] `types/data-transfer.ts` - [ ] `types/split-panel.ts` #### 1.3 백엔드 API - [ ] 화면 임베딩 CRUD API - [ ] 데이터 전달 설정 CRUD API - [ ] 분할 패널 CRUD API - [ ] 컨트롤러 및 서비스 레이어 구현 ### Phase 2: 화면 임베딩 기능 (2-3주) #### 2.1 EmbeddedScreen 컴포넌트 - [ ] 기본 임베딩 기능 - [ ] 모드별 렌더링 (view, select, form, edit) - [ ] 선택 모드 구현 (체크박스) - [ ] 이벤트 핸들링 #### 2.2 DataReceivable 인터페이스 구현 - [ ] TableComponent - [ ] InputComponent - [ ] SelectComponent - [ ] TextareaComponent - [ ] RepeaterComponent - [ ] FormGroupComponent - [ ] HiddenComponent #### 2.3 컴포넌트 등록 시스템 - [ ] 컴포넌트 마운트 시 자동 등록 - [ ] 컴포넌트 ID 관리 - [ ] 컴포넌트 참조 관리 ### Phase 3: 데이터 전달 시스템 (2-3주) #### 3.1 매핑 엔진 - [ ] 매핑 규칙 파싱 - [ ] 필드 매핑 적용 - [ ] 변환 함수 구현 - [ ] sum, average, count - [ ] min, max - [ ] first, last - [ ] concat, join #### 3.2 조건부 전달 - [ ] 조건 파싱 - [ ] 필터링 로직 - [ ] 복합 조건 지원 #### 3.3 검증 시스템 - [ ] 필수 필드 검증 - [ ] 최소/최대 행 수 검증 - [ ] 커스텀 검증 함수 실행 ### Phase 4: 분할 패널 UI (2-3주) #### 4.1 ScreenSplitPanel 컴포넌트 - [ ] 기본 레이아웃 - [ ] 리사이저 구현 - [ ] 전달 버튼 - [ ] 반응형 디자인 #### 4.2 설정 UI - [ ] 화면 선택 드롭다운 - [ ] 매핑 규칙 설정 UI - [ ] 드래그앤드롭 매핑 - [ ] 미리보기 기능 #### 4.3 시각적 피드백 - [ ] 데이터 전달 애니메이션 - [ ] 로딩 상태 표시 - [ ] 성공/실패 토스트 ### Phase 5: 고급 기능 (2-3주) #### 5.1 양방향 동기화 - [ ] 우측 → 좌측 데이터 반영 - [ ] 실시간 업데이트 #### 5.2 트랜잭션 지원 - [ ] 전체 성공 또는 전체 실패 - [ ] 롤백 기능 #### 5.3 성능 최적화 - [ ] 대량 데이터 처리 - [ ] 가상 스크롤링 - [ ] 메모이제이션 ### Phase 6: 테스트 및 문서화 (1-2주) #### 6.1 단위 테스트 - [ ] 매핑 엔진 테스트 - [ ] 변환 함수 테스트 - [ ] 검증 로직 테스트 #### 6.2 통합 테스트 - [ ] 전체 워크플로우 테스트 - [ ] 실제 시나리오 테스트 #### 6.3 문서화 - [ ] 사용자 가이드 - [ ] 개발자 문서 - [ ] API 문서 --- ## 사용 시나리오 ### 시나리오 1: 입고 등록 #### 요구사항 - 발주 목록에서 품목을 선택하여 입고 등록 - 선택된 품목의 정보를 입고 처리 품목 테이블에 추가 - 공급자 정보를 자동으로 입력 필드에 설정 - 총 품목 수를 자동 계산 #### 설정 ```typescript const 입고등록_설정: ScreenSplitPanel = { screenId: 100, leftEmbedding: { childScreenId: 10, // 발주 목록 조회 화면 position: "left", mode: "select", config: { width: "50%", multiSelect: true, showSearch: true, showPagination: true } }, rightEmbedding: { childScreenId: 20, // 입고 등록 폼 화면 position: "right", mode: "form", config: { width: "50%" } }, dataTransfer: { sourceScreenId: 10, targetScreenId: 20, sourceComponentId: "table-발주목록", sourceComponentType: "table", dataReceivers: [ { targetComponentId: "table-입고처리품목", targetComponentType: "table", mode: "append", mappingRules: [ { sourceField: "품목코드", targetField: "품목코드" }, { sourceField: "품목명", targetField: "품목명" }, { sourceField: "발주수량", targetField: "발주수량" }, { sourceField: "미입고수량", targetField: "입고수량" } ] }, { targetComponentId: "input-공급자", targetComponentType: "input", mode: "replace", mappingRules: [ { sourceField: "공급자", targetField: "value", transform: "first" } ] }, { targetComponentId: "input-품목수", targetComponentType: "input", mode: "replace", mappingRules: [ { sourceField: "품목코드", targetField: "value", transform: "count" } ] } ], buttonConfig: { label: "선택 품목 추가", position: "center", icon: "ArrowRight", validation: { requireSelection: true, minSelection: 1 } } }, layoutConfig: { splitRatio: 50, resizable: true, minLeftWidth: 400, minRightWidth: 600, orientation: "horizontal" } }; ``` ### 시나리오 2: 수주 등록 #### 요구사항 - 견적서 목록에서 품목을 선택하여 수주 등록 - 고객 정보를 자동으로 폼에 설정 - 품목별 수량 및 금액 자동 계산 - 총 금액 합계 표시 #### 설정 ```typescript const 수주등록_설정: ScreenSplitPanel = { screenId: 101, leftEmbedding: { childScreenId: 30, // 견적서 목록 조회 화면 position: "left", mode: "select", config: { width: "40%", multiSelect: true } }, rightEmbedding: { childScreenId: 40, // 수주 등록 폼 화면 position: "right", mode: "form", config: { width: "60%" } }, dataTransfer: { sourceScreenId: 30, targetScreenId: 40, dataReceivers: [ { targetComponentId: "table-수주품목", targetComponentType: "table", mode: "append", mappingRules: [ { sourceField: "품목코드", targetField: "품목코드" }, { sourceField: "품목명", targetField: "품목명" }, { sourceField: "수량", targetField: "수량" }, { sourceField: "단가", targetField: "단가" }, { sourceField: "수량", targetField: "금액", transform: "custom", transformConfig: { formula: "수량 * 단가" } } ] }, { targetComponentId: "input-고객명", targetComponentType: "input", mode: "replace", mappingRules: [ { sourceField: "고객명", targetField: "value", transform: "first" } ] }, { targetComponentId: "input-총금액", targetComponentType: "input", mode: "replace", mappingRules: [ { sourceField: "금액", targetField: "value", transform: "sum" } ] } ], buttonConfig: { label: "견적서 불러오기", position: "center", icon: "Download" } }, layoutConfig: { splitRatio: 40, resizable: true, orientation: "horizontal" } }; ``` ### 시나리오 3: 출고 등록 #### 요구사항 - 재고 목록에서 품목을 선택하여 출고 등록 - 재고 수량 확인 및 경고 - 출고 가능 수량만 필터링 - 창고별 재고 정보 표시 #### 설정 ```typescript const 출고등록_설정: ScreenSplitPanel = { screenId: 102, leftEmbedding: { childScreenId: 50, // 재고 목록 조회 화면 position: "left", mode: "select", config: { width: "45%", multiSelect: true } }, rightEmbedding: { childScreenId: 60, // 출고 등록 폼 화면 position: "right", mode: "form", config: { width: "55%" } }, dataTransfer: { sourceScreenId: 50, targetScreenId: 60, dataReceivers: [ { targetComponentId: "table-출고품목", targetComponentType: "table", mode: "append", mappingRules: [ { sourceField: "품목코드", targetField: "품목코드" }, { sourceField: "품목명", targetField: "품목명" }, { sourceField: "재고수량", targetField: "가용수량" }, { sourceField: "창고", targetField: "출고창고" } ], condition: { field: "재고수량", operator: "greaterThan", value: 0 } }, { targetComponentId: "input-총출고수량", targetComponentType: "input", mode: "replace", mappingRules: [ { sourceField: "재고수량", targetField: "value", transform: "sum" } ] } ], buttonConfig: { label: "출고 품목 추가", position: "center", icon: "ArrowRight", validation: { requireSelection: true, confirmMessage: "선택한 품목을 출고 처리하시겠습니까?" } } }, layoutConfig: { splitRatio: 45, resizable: true, orientation: "horizontal" } }; ``` --- ## 기술적 고려사항 ### 1. 성능 최적화 #### 대량 데이터 처리 - 가상 스크롤링 적용 - 청크 단위 데이터 전달 - 백그라운드 처리 #### 메모리 관리 - 컴포넌트 언마운트 시 참조 해제 - 이벤트 리스너 정리 - 메모이제이션 활용 ### 2. 보안 #### 권한 검증 - 화면 접근 권한 확인 - 데이터 전달 권한 확인 - 멀티테넌시 격리 #### 데이터 검증 - 입력값 검증 - SQL 인젝션 방지 - XSS 방지 ### 3. 에러 처리 #### 사용자 친화적 메시지 - 명확한 오류 메시지 - 복구 방법 안내 - 로그 기록 #### 트랜잭션 롤백 - 부분 실패 시 전체 롤백 - 데이터 일관성 유지 ### 4. 확장성 #### 플러그인 시스템 - 커스텀 변환 함수 등록 - 커스텀 검증 함수 등록 - 커스텀 컴포넌트 타입 추가 #### 이벤트 시스템 - 데이터 전달 전/후 이벤트 - 커스텀 이벤트 핸들러 --- ## 마일스톤 ### M1: 기본 인프라 (2주) - 데이터베이스 스키마 완성 - 백엔드 API 완성 - 타입 정의 완성 ### M2: 화면 임베딩 (3주) - EmbeddedScreen 컴포넌트 완성 - DataReceivable 인터페이스 구현 완료 - 선택 모드 동작 확인 ### M3: 데이터 전달 (3주) - 매핑 엔진 완성 - 변환 함수 구현 완료 - 조건부 전달 동작 확인 ### M4: 분할 패널 UI (3주) - ScreenSplitPanel 컴포넌트 완성 - 설정 UI 완성 - 입고 등록 시나리오 완성 ### M5: 고급 기능 및 최적화 (3주) - 양방향 동기화 완성 - 성능 최적화 완료 - 전체 테스트 통과 ### M6: 문서화 및 배포 (1주) - 사용자 가이드 작성 - 개발자 문서 작성 - 프로덕션 배포 --- ## 예상 일정 **총 소요 기간**: 약 15주 (3.5개월) - Week 1-2: Phase 1 (기본 인프라) - Week 3-5: Phase 2 (화면 임베딩) - Week 6-8: Phase 3 (데이터 전달) - Week 9-11: Phase 4 (분할 패널 UI) - Week 12-14: Phase 5 (고급 기능) - Week 15: Phase 6 (테스트 및 문서화) --- ## 성공 지표 ### 기능적 지표 - [ ] 입고 등록 시나리오 완벽 동작 - [ ] 수주 등록 시나리오 완벽 동작 - [ ] 출고 등록 시나리오 완벽 동작 - [ ] 모든 컴포넌트 타입 데이터 수신 가능 - [ ] 모든 변환 함수 정상 동작 ### 성능 지표 - [ ] 1000개 행 데이터 전달 < 1초 - [ ] 화면 로딩 시간 < 2초 - [ ] 메모리 사용량 < 100MB ### 사용성 지표 - [ ] 설정 UI 직관적 - [ ] 에러 메시지 명확 - [ ] 문서 완성도 90% 이상 --- ## 리스크 관리 ### 기술적 리스크 - **복잡도 증가**: 단계별 구현으로 관리 - **성능 문제**: 초기부터 최적화 고려 - **호환성 문제**: 기존 시스템과 충돌 방지 ### 일정 리스크 - **예상 기간 초과**: 버퍼 2주 확보 - **우선순위 변경**: 핵심 기능 먼저 구현 ### 인력 리스크 - **담당자 부재**: 문서화 철저히 - **지식 공유**: 주간 리뷰 미팅 --- ## 결론 화면 임베딩 및 데이터 전달 시스템은 복잡한 업무 워크플로우를 효율적으로 처리할 수 있는 강력한 기능입니다. 단계별로 체계적으로 구현하면 약 3.5개월 내에 완성할 수 있으며, 이를 통해 사용자 경험을 크게 향상시킬 수 있습니다.