ERP-node/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md

1681 lines
43 KiB
Markdown
Raw Permalink Normal View History

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