데이터 수정이 안되는 문제 해결

This commit is contained in:
kjs 2025-12-01 15:21:03 +09:00
parent 93b92960e7
commit da6ac92391
14 changed files with 621 additions and 409 deletions

View File

@ -203,7 +203,7 @@ export const updateFormDataPartial = async (
};
const result = await dynamicFormService.updateFormDataPartial(
parseInt(id),
id, // 🔧 parseInt 제거 - UUID 문자열도 지원
tableName,
originalData,
newDataWithMeta

View File

@ -746,7 +746,7 @@ export class DynamicFormService {
* ( )
*/
async updateFormDataPartial(
id: number,
id: string | number, // 🔧 UUID 문자열도 지원
tableName: string,
originalData: Record<string, any>,
newData: Record<string, any>

View File

@ -57,6 +57,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 폼 데이터 상태 추가
const [formData, setFormData] = useState<Record<string, any>>({});
// 🆕 원본 데이터 상태 (수정 모드에서 UPDATE 판단용)
const [originalData, setOriginalData] = useState<Record<string, any> | null>(null);
// 연속 등록 모드 상태 (state로 변경 - 체크박스 UI 업데이트를 위해)
const [continuousMode, setContinuousMode] = useState(false);
@ -143,10 +146,13 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
console.log("✅ URL 파라미터 추가:", urlParams);
}
// 🆕 editData가 있으면 formData로 설정 (수정 모드)
// 🆕 editData가 있으면 formData와 originalData로 설정 (수정 모드)
if (editData) {
console.log("📝 [ScreenModal] 수정 데이터 설정:", editData);
setFormData(editData);
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
} else {
setOriginalData(null); // 신규 등록 모드
}
setModalState({
@ -177,6 +183,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
});
setScreenData(null);
setFormData({});
setOriginalData(null); // 🆕 원본 데이터 초기화
setContinuousMode(false);
localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장
console.log("🔄 연속 모드 초기화: false");
@ -364,12 +371,15 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
"⚠️ [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("데이터를 불러올 수 없습니다.");
@ -618,11 +628,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
component={adjustedComponent}
allComponents={screenData.components}
formData={formData}
originalData={originalData} // 🆕 원본 데이터 전달 (UPDATE 판단용)
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
console.log("🔧 [ScreenModal] onFormDataChange 호출:", { fieldName, value });
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
console.log("🔧 [ScreenModal] formData 업데이트:", { prev, newFormData });
return newFormData;
});
}}
onRefresh={() => {
// 부모 화면의 테이블 새로고침 이벤트 발송

View File

@ -53,6 +53,8 @@ interface InteractiveScreenViewerProps {
disabledFields?: string[];
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
isInModal?: boolean;
// 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
originalData?: Record<string, any> | null;
}
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
@ -72,6 +74,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
groupedData,
disabledFields = [],
isInModal = false,
originalData, // 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
}) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { userName: authUserName, user: authUser } = useAuth();
@ -331,6 +334,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
component={comp}
isInteractive={true}
formData={formData}
originalData={originalData || undefined} // 🆕 원본 데이터 전달 (UPDATE 판단용)
onFormDataChange={handleFormDataChange}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}

View File

@ -360,6 +360,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<ConfigPanelComponent
config={config}
onChange={handlePanelConfigChange}
onConfigChange={handlePanelConfigChange} // 🔧 autocomplete-search-input 등 일부 컴포넌트용
tables={tables} // 테이블 정보 전달
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달

View File

@ -124,7 +124,7 @@ export class DynamicFormApi {
* @returns
*/
static async updateFormDataPartial(
id: number,
id: string | number, // 🔧 UUID 문자열도 지원
originalData: Record<string, any>,
newData: Record<string, any>,
tableName: string,

View File

@ -337,6 +337,11 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
const handleChange = (value: any) => {
// autocomplete-search-input, entity-search-input은 자체적으로 onFormDataChange를 호출하므로 중복 저장 방지
if (componentType === "autocomplete-search-input" || componentType === "entity-search-input") {
return;
}
// React 이벤트 객체인 경우 값 추출
let actualValue = value;
if (value && typeof value === "object" && value.nativeEvent && value.target) {

View File

@ -57,20 +57,42 @@ export function AutocompleteSearchInputComponent({
filterCondition,
});
// 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지)
const selectedDataRef = useRef<EntitySearchResult | null>(null);
const inputValueRef = useRef<string>("");
// formData에서 현재 값 가져오기 (isInteractive 모드)
const currentValue = isInteractive && formData && component?.columnName
? formData[component.columnName]
: value;
// value가 변경되면 표시값 업데이트
// selectedData 변경 시 ref도 업데이트
useEffect(() => {
if (currentValue && selectedData) {
setInputValue(selectedData[displayField] || "");
} else if (!currentValue) {
setInputValue("");
setSelectedData(null);
if (selectedData) {
selectedDataRef.current = selectedData;
inputValueRef.current = inputValue;
}
}, [currentValue, displayField, selectedData]);
}, [selectedData, inputValue]);
// 리렌더링 시 ref에서 값 복원
useEffect(() => {
if (!selectedData && selectedDataRef.current) {
setSelectedData(selectedDataRef.current);
setInputValue(inputValueRef.current);
}
}, []);
// value가 변경되면 표시값 업데이트 - 단, selectedData가 있으면 유지
useEffect(() => {
// selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우)
if (selectedData || selectedDataRef.current) {
return;
}
if (!currentValue) {
setInputValue("");
}
}, [currentValue, selectedData]);
// 외부 클릭 감지
useEffect(() => {

View File

@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@ -21,7 +21,9 @@ export function AutocompleteSearchInputConfigPanel({
config,
onConfigChange,
}: AutocompleteSearchInputConfigPanelProps) {
const [localConfig, setLocalConfig] = useState(config);
// 초기화 여부 추적 (첫 마운트 시에만 config로 초기화)
const isInitialized = useRef(false);
const [localConfig, setLocalConfig] = useState<AutocompleteSearchInputConfig>(config);
const [allTables, setAllTables] = useState<any[]>([]);
const [sourceTableColumns, setSourceTableColumns] = useState<any[]>([]);
const [targetTableColumns, setTargetTableColumns] = useState<any[]>([]);
@ -32,12 +34,21 @@ export function AutocompleteSearchInputConfigPanel({
const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false);
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
// 첫 마운트 시에만 config로 초기화 (이후에는 localConfig 유지)
useEffect(() => {
setLocalConfig(config);
if (!isInitialized.current && config) {
setLocalConfig(config);
isInitialized.current = true;
}
}, [config]);
const updateConfig = (updates: Partial<AutocompleteSearchInputConfig>) => {
const newConfig = { ...localConfig, ...updates };
console.log("🔧 [AutocompleteConfigPanel] updateConfig:", {
updates,
localConfig,
newConfig,
});
setLocalConfig(newConfig);
onConfigChange(newConfig);
};
@ -325,10 +336,11 @@ export function AutocompleteSearchInputConfigPanel({
<div className="space-y-1.5">
<Label className="text-xs"> *</Label>
<Select
value={mapping.sourceField}
onValueChange={(value) =>
updateFieldMapping(index, { sourceField: value })
}
value={mapping.sourceField || undefined}
onValueChange={(value) => {
console.log("🔧 [Select] sourceField 변경:", value);
updateFieldMapping(index, { sourceField: value });
}}
disabled={!localConfig.tableName || isLoadingSourceColumns}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
@ -347,10 +359,11 @@ export function AutocompleteSearchInputConfigPanel({
<div className="space-y-1.5">
<Label className="text-xs"> *</Label>
<Select
value={mapping.targetField}
onValueChange={(value) =>
updateFieldMapping(index, { targetField: value })
}
value={mapping.targetField || undefined}
onValueChange={(value) => {
console.log("🔧 [Select] targetField 변경:", value);
updateFieldMapping(index, { targetField: value });
}}
disabled={!localConfig.targetTable || isLoadingTargetColumns}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">

View File

@ -694,7 +694,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
const context: ButtonActionContext = {
formData: formData || {},
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
originalData: originalData, // 🔧 빈 객체 대신 undefined 유지 (UPDATE 판단에 사용)
screenId: effectiveScreenId, // 🆕 ScreenContext에서 가져온 값 사용
tableName: effectiveTableName, // 🆕 ScreenContext에서 가져온 값 사용
userId, // 🆕 사용자 ID

View File

@ -414,9 +414,20 @@ export class ButtonActionExecutor {
const primaryKeys = primaryKeyResult.data || [];
const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys);
// 단순히 기본키 값 존재 여부로 판단 (임시)
// TODO: 실제 테이블에서 기본키로 레코드 존재 여부 확인하는 API 필요
const isUpdate = false; // 현재는 항상 INSERT로 처리
// 🔧 수정: originalData가 있고 실제 데이터가 있으면 UPDATE 모드로 처리
// originalData는 수정 버튼 클릭 시 editData로 전달되어 context.originalData로 설정됨
// 빈 객체 {}도 truthy이므로 Object.keys로 실제 데이터 유무 확인
const hasRealOriginalData = originalData && Object.keys(originalData).length > 0;
const isUpdate = hasRealOriginalData && !!primaryKeyValue;
console.log("🔍 [handleSave] INSERT/UPDATE 판단:", {
hasOriginalData: !!originalData,
hasRealOriginalData,
originalDataKeys: originalData ? Object.keys(originalData) : [],
primaryKeyValue,
isUpdate,
primaryKeys,
});
let saveResult;

View File

@ -21,12 +21,14 @@
**생성된 테이블**:
1. **screen_embedding** (화면 임베딩 설정)
- 한 화면을 다른 화면 안에 임베드
- 위치 (left, right, top, bottom, center)
- 모드 (view, select, form, edit)
- 설정 (width, height, multiSelect 등)
2. **screen_data_transfer** (데이터 전달 설정)
- 소스 화면 → 타겟 화면 데이터 전달
- 데이터 수신자 배열 (JSONB)
- 매핑 규칙, 조건, 검증
@ -38,6 +40,7 @@
- 레이아웃 설정 (splitRatio, resizable 등)
**샘플 데이터**:
- 입고 등록 시나리오 샘플 데이터 포함
- 발주 목록 → 입고 처리 품목 매핑 예시
@ -46,6 +49,7 @@
**파일**: `frontend/types/screen-embedding.ts`
**주요 타입**:
```typescript
// 화면 임베딩
- EmbeddingMode: "view" | "select" | "form" | "edit"
@ -67,13 +71,15 @@
#### 1.3 백엔드 API
**파일**:
**파일**:
- `backend-node/src/controllers/screenEmbeddingController.ts`
- `backend-node/src/routes/screenEmbeddingRoutes.ts`
**API 엔드포인트**:
**화면 임베딩**:
- `GET /api/screen-embedding?parentScreenId=1` - 목록 조회
- `GET /api/screen-embedding/:id` - 상세 조회
- `POST /api/screen-embedding` - 생성
@ -81,18 +87,21 @@
- `DELETE /api/screen-embedding/:id` - 삭제
**데이터 전달**:
- `GET /api/screen-data-transfer?sourceScreenId=1&targetScreenId=2` - 조회
- `POST /api/screen-data-transfer` - 생성
- `PUT /api/screen-data-transfer/:id` - 수정
- `DELETE /api/screen-data-transfer/:id` - 삭제
**분할 패널**:
- `GET /api/screen-split-panel/:screenId` - 조회
- `POST /api/screen-split-panel` - 생성 (트랜잭션)
- `PUT /api/screen-split-panel/:id` - 수정
- `DELETE /api/screen-split-panel/:id` - 삭제 (CASCADE)
**특징**:
- ✅ 멀티테넌시 지원 (company_code 필터링)
- ✅ 트랜잭션 처리 (분할 패널 생성/삭제)
- ✅ 외래키 CASCADE 처리
@ -103,25 +112,24 @@
**파일**: `frontend/lib/api/screenEmbedding.ts`
**함수**:
```typescript
// 화면 임베딩
- getScreenEmbeddings(parentScreenId)
- getScreenEmbeddingById(id)
- createScreenEmbedding(data)
- updateScreenEmbedding(id, data)
- deleteScreenEmbedding(id)
// 데이터 전달
- getScreenDataTransfer(sourceScreenId, targetScreenId)
- createScreenDataTransfer(data)
- updateScreenDataTransfer(id, data)
- deleteScreenDataTransfer(id)
// 분할 패널
- getScreenSplitPanel(screenId)
- createScreenSplitPanel(data)
- updateScreenSplitPanel(id, layoutConfig)
- deleteScreenSplitPanel(id)
-getScreenEmbeddings(parentScreenId) -
getScreenEmbeddingById(id) -
createScreenEmbedding(data) -
updateScreenEmbedding(id, data) -
deleteScreenEmbedding(id) -
// 데이터 전달
getScreenDataTransfer(sourceScreenId, targetScreenId) -
createScreenDataTransfer(data) -
updateScreenDataTransfer(id, data) -
deleteScreenDataTransfer(id) -
// 분할 패널
getScreenSplitPanel(screenId) -
createScreenSplitPanel(data) -
updateScreenSplitPanel(id, layoutConfig) -
deleteScreenSplitPanel(id);
```
---
@ -133,6 +141,7 @@
**파일**: `frontend/components/screen-embedding/EmbeddedScreen.tsx`
**주요 기능**:
- ✅ 화면 데이터 로드
- ✅ 모드별 렌더링 (view, select, form, edit)
- ✅ 선택 모드 지원 (체크박스)
@ -141,6 +150,7 @@
- ✅ 로딩/에러 상태 UI
**외부 인터페이스** (useImperativeHandle):
```typescript
- getSelectedRows(): any[]
- clearSelection(): void
@ -149,6 +159,7 @@
```
**데이터 수신 프로세스**:
1. 조건 필터링 (condition)
2. 매핑 규칙 적용 (mappingRules)
3. 검증 (validation)
@ -165,10 +176,12 @@
**주요 함수**:
1. **applyMappingRules(data, rules)**
- 일반 매핑: 각 행에 대해 필드 매핑
- 변환 매핑: 집계 함수 적용
2. **변환 함수 지원**:
- `sum`: 합계
- `average`: 평균
- `count`: 개수
@ -177,15 +190,18 @@
- `concat`, `join`: 문자열 결합
3. **filterDataByCondition(data, condition)**
- 조건 연산자: equals, notEquals, contains, greaterThan, lessThan, in, notIn
4. **validateMappingResult(data, rules)**
- 필수 필드 검증
5. **previewMapping(sampleData, rules)**
- 매핑 결과 미리보기
**특징**:
- ✅ 중첩 객체 지원 (`user.address.city`)
- ✅ 타입 안전성
- ✅ 에러 처리
@ -195,6 +211,7 @@
**파일**: `frontend/lib/utils/logger.ts`
**기능**:
- debug, info, warn, error 레벨
- 개발 환경에서만 debug 출력
- 타임스탬프 포함
@ -208,6 +225,7 @@
**파일**: `frontend/components/screen-embedding/ScreenSplitPanel.tsx`
**주요 기능**:
- ✅ 좌우 화면 임베딩
- ✅ 리사이저 (드래그로 비율 조정)
- ✅ 데이터 전달 버튼
@ -218,6 +236,7 @@
- ✅ 전달 후 선택 초기화 (옵션)
**UI 구조**:
```
┌─────────────────────────────────────────────────────────┐
│ [좌측 패널 50%] │ [버튼] │ [우측 패널 50%] │
@ -230,6 +249,7 @@
```
**이벤트 흐름**:
1. 좌측에서 행 선택 → 선택 카운트 업데이트
2. 전달 버튼 클릭 → 검증
3. 우측 화면의 컴포넌트들에 데이터 전달
@ -281,7 +301,7 @@ ERP-node/
const inboundConfig: ScreenSplitPanel = {
screenId: 100,
leftEmbedding: {
childScreenId: 10, // 발주 목록 조회
childScreenId: 10, // 발주 목록 조회
position: "left",
mode: "select",
config: {
@ -290,7 +310,7 @@ const inboundConfig: ScreenSplitPanel = {
},
},
rightEmbedding: {
childScreenId: 20, // 입고 등록 폼
childScreenId: 20, // 입고 등록 폼
position: "right",
mode: "form",
config: {
@ -352,7 +372,7 @@ const inboundConfig: ScreenSplitPanel = {
onDataTransferred={(data) => {
console.log("전달된 데이터:", data);
}}
/>
/>;
```
---
@ -395,6 +415,7 @@ const inboundConfig: ScreenSplitPanel = {
### Phase 5: 고급 기능 (예정)
1. **DataReceivable 인터페이스 구현**
- TableComponent
- InputComponent
- SelectComponent
@ -402,6 +423,7 @@ const inboundConfig: ScreenSplitPanel = {
- 기타 컴포넌트들
2. **양방향 동기화**
- 우측 → 좌측 데이터 반영
- 실시간 업데이트
@ -412,6 +434,7 @@ const inboundConfig: ScreenSplitPanel = {
### Phase 6: 설정 UI (예정)
1. **시각적 매핑 설정 UI**
- 드래그앤드롭으로 필드 매핑
- 변환 함수 선택
- 조건 설정
@ -463,7 +486,7 @@ import { getScreenSplitPanel } from "@/lib/api/screenEmbedding";
const { data: config } = await getScreenSplitPanel(screenId);
// 렌더링
<ScreenSplitPanel config={config} />
<ScreenSplitPanel config={config} />;
```
---
@ -471,6 +494,7 @@ const { data: config } = await getScreenSplitPanel(screenId);
## ✅ 체크리스트
### 구현 완료
- [x] 데이터베이스 스키마 (3개 테이블)
- [x] TypeScript 타입 정의
- [x] 백엔드 API (15개 엔드포인트)
@ -481,6 +505,7 @@ const { data: config } = await getScreenSplitPanel(screenId);
- [x] 로거 유틸리티
### 다음 단계
- [ ] DataReceivable 구현 (각 컴포넌트 타입별)
- [ ] 설정 UI (드래그앤드롭 매핑)
- [ ] 미리보기 기능
@ -500,4 +525,3 @@ const { data: config } = await getScreenSplitPanel(screenId);
- ✅ 매핑 엔진 완성
이제 입고 등록과 같은 복잡한 워크플로우를 구현할 수 있습니다. 다음 단계는 각 컴포넌트 타입별 DataReceivable 인터페이스 구현과 설정 UI 개발입니다.

View File

@ -11,6 +11,7 @@
### 1. 데이터베이스 스키마
#### 새로운 테이블 (독립적)
```sql
- screen_embedding (신규)
- screen_data_transfer (신규)
@ -18,11 +19,13 @@
```
**충돌 없는 이유**:
- ✅ 완전히 새로운 테이블명
- ✅ 기존 테이블과 이름 중복 없음
- ✅ 외래키는 기존 `screen_definitions`만 참조 (읽기 전용)
#### 기존 테이블 (영향 없음)
```sql
- screen_definitions (변경 없음)
- screen_layouts (변경 없음)
@ -32,6 +35,7 @@
```
**확인 사항**:
- ✅ 기존 테이블 구조 변경 없음
- ✅ 기존 데이터 마이그레이션 불필요
- ✅ 기존 쿼리 영향 없음
@ -41,6 +45,7 @@
### 2. API 엔드포인트
#### 새로운 엔드포인트 (독립적)
```
POST /api/screen-embedding
GET /api/screen-embedding
@ -59,11 +64,13 @@ DELETE /api/screen-split-panel/:id
```
**충돌 없는 이유**:
- ✅ 기존 `/api/screen-management/*` 와 다른 경로
- ✅ 새로운 라우트 추가만 (기존 라우트 수정 없음)
- ✅ 독립적인 컨트롤러 파일
#### 기존 엔드포인트 (영향 없음)
```
/api/screen-management/* (변경 없음)
/api/screen/* (변경 없음)
@ -75,16 +82,19 @@ DELETE /api/screen-split-panel/:id
### 3. TypeScript 타입
#### 새로운 타입 파일 (독립적)
```typescript
frontend/types/screen-embedding.ts (신규)
frontend / types / screen - embedding.ts(신규);
```
**충돌 없는 이유**:
- ✅ 기존 `screen.ts`, `screen-management.ts` 와 별도 파일
- ✅ 타입명 중복 없음
- ✅ 독립적인 네임스페이스
#### 기존 타입 (영향 없음)
```typescript
frontend/types/screen.ts (변경 없음)
frontend/types/screen-management.ts (변경 없음)
@ -96,6 +106,7 @@ backend-node/src/types/screen.ts (변경 없음)
### 4. 프론트엔드 컴포넌트
#### 새로운 컴포넌트 (독립적)
```
frontend/components/screen-embedding/
├── EmbeddedScreen.tsx (신규)
@ -104,11 +115,13 @@ frontend/components/screen-embedding/
```
**충돌 없는 이유**:
- ✅ 별도 디렉토리 (`screen-embedding/`)
- ✅ 기존 컴포넌트 수정 없음
- ✅ 독립적으로 import 가능
#### 기존 컴포넌트 (영향 없음)
```
frontend/components/screen/ (변경 없음)
frontend/app/(main)/screens/[screenId]/page.tsx (변경 없음)
@ -121,17 +134,20 @@ frontend/app/(main)/screens/[screenId]/page.tsx (변경 없음)
### 1. screen_definitions 테이블 참조
**현재 구조**:
```sql
-- 새 테이블들이 screen_definitions를 참조
CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id)
CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id)
REFERENCES screen_definitions(screen_id) ON DELETE CASCADE
```
**잠재적 문제**:
- ⚠️ 기존 화면 삭제 시 임베딩 설정도 함께 삭제됨 (CASCADE)
- ⚠️ 화면 ID 변경 시 임베딩 설정이 깨질 수 있음
**해결 방법**:
```sql
-- 이미 구현됨: ON DELETE CASCADE
-- 화면 삭제 시 자동으로 관련 임베딩도 삭제
@ -139,6 +155,7 @@ CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id)
```
**권장 사항**:
- ✅ 화면 삭제 전 임베딩 사용 여부 확인 UI 추가 (Phase 6)
- ✅ 삭제 시 경고 메시지 표시
@ -147,21 +164,23 @@ CONSTRAINT fk_parent_screen FOREIGN KEY (parent_screen_id)
### 2. 화면 렌더링 로직
**현재 화면 렌더링**:
```typescript
// frontend/app/(main)/screens/[screenId]/page.tsx
function ScreenViewPage() {
// 기존: 단일 화면 렌더링
const screenId = parseInt(params.screenId as string);
// 레이아웃 로드
const layout = await screenApi.getScreenLayout(screenId);
// 컴포넌트 렌더링
<DynamicComponentRenderer components={layout.components} />
<DynamicComponentRenderer components={layout.components} />;
}
```
**새로운 렌더링 (분할 패널)**:
```typescript
// 분할 패널 화면인 경우
if (isSplitPanelScreen) {
@ -174,10 +193,12 @@ return <DynamicComponentRenderer components={layout.components} />;
```
**잠재적 문제**:
- ⚠️ 화면 타입 구분 로직 필요
- ⚠️ 기존 화면 렌더링 로직 수정 필요
**해결 방법**:
```typescript
// 1. screen_definitions에 screen_type 컬럼 추가 (선택사항)
ALTER TABLE screen_definitions ADD COLUMN screen_type VARCHAR(20) DEFAULT 'normal';
@ -191,40 +212,45 @@ if (splitPanelConfig.success && splitPanelConfig.data) {
```
**권장 구현**:
```typescript
// frontend/app/(main)/screens/[screenId]/page.tsx 수정
useEffect(() => {
const loadScreen = async () => {
// 1. 분할 패널 확인
const splitPanelResult = await getScreenSplitPanel(screenId);
if (splitPanelResult.success && splitPanelResult.data) {
// 분할 패널 화면
setScreenType('split_panel');
setScreenType("split_panel");
setSplitPanelConfig(splitPanelResult.data);
return;
}
// 2. 일반 화면
const screenResult = await screenApi.getScreen(screenId);
const layoutResult = await screenApi.getScreenLayout(screenId);
setScreenType('normal');
setScreenType("normal");
setScreen(screenResult.data);
setLayout(layoutResult.data);
};
loadScreen();
}, [screenId]);
// 렌더링
{screenType === 'split_panel' && splitPanelConfig && (
<ScreenSplitPanel config={splitPanelConfig} />
)}
{
screenType === "split_panel" && splitPanelConfig && (
<ScreenSplitPanel config={splitPanelConfig} />
);
}
{screenType === 'normal' && layout && (
<DynamicComponentRenderer components={layout.components} />
)}
{
screenType === "normal" && layout && (
<DynamicComponentRenderer components={layout.components} />
);
}
```
---
@ -232,6 +258,7 @@ useEffect(() => {
### 3. 컴포넌트 등록 시스템
**현재 시스템**:
```typescript
// frontend/lib/registry/components.ts
const componentRegistry = new Map<string, ComponentDefinition>();
@ -242,6 +269,7 @@ export function registerComponent(id: string, component: any) {
```
**새로운 요구사항**:
```typescript
// DataReceivable 인터페이스 구현 필요
interface DataReceivable {
@ -254,29 +282,31 @@ interface DataReceivable {
```
**잠재적 문제**:
- ⚠️ 기존 컴포넌트들이 DataReceivable 인터페이스 미구현
- ⚠️ 데이터 수신 기능 없음
**해결 방법**:
```typescript
// Phase 5에서 구현 예정
// 기존 컴포넌트를 래핑하는 어댑터 패턴 사용
class TableComponentAdapter implements DataReceivable {
constructor(private tableComponent: any) {}
async receiveData(data: any[], mode: DataReceiveMode) {
if (mode === 'append') {
if (mode === "append") {
this.tableComponent.addRows(data);
} else if (mode === 'replace') {
} else if (mode === "replace") {
this.tableComponent.setRows(data);
}
}
getData() {
return this.tableComponent.getRows();
}
clearData() {
this.tableComponent.clearRows();
}
@ -284,6 +314,7 @@ class TableComponentAdapter implements DataReceivable {
```
**권장 사항**:
- ✅ 기존 컴포넌트 수정 없이 어댑터로 래핑
- ✅ 점진적으로 DataReceivable 구현
- ✅ 하위 호환성 유지
@ -297,38 +328,41 @@ class TableComponentAdapter implements DataReceivable {
**파일**: `frontend/app/(main)/screens/[screenId]/page.tsx`
**수정 내용**:
```typescript
import { getScreenSplitPanel } from "@/lib/api/screenEmbedding";
import { ScreenSplitPanel } from "@/components/screen-embedding";
function ScreenViewPage() {
const [screenType, setScreenType] = useState<'normal' | 'split_panel'>('normal');
const [screenType, setScreenType] = useState<"normal" | "split_panel">(
"normal"
);
const [splitPanelConfig, setSplitPanelConfig] = useState<any>(null);
useEffect(() => {
const loadScreen = async () => {
// 분할 패널 확인
const splitResult = await getScreenSplitPanel(screenId);
if (splitResult.success && splitResult.data) {
setScreenType('split_panel');
setScreenType("split_panel");
setSplitPanelConfig(splitResult.data);
setLoading(false);
return;
}
// 일반 화면 로드 (기존 로직)
// ...
};
loadScreen();
}, [screenId]);
// 렌더링
if (screenType === 'split_panel' && splitPanelConfig) {
if (screenType === "split_panel" && splitPanelConfig) {
return <ScreenSplitPanel config={splitPanelConfig} />;
}
// 기존 렌더링 로직
// ...
}
@ -343,6 +377,7 @@ function ScreenViewPage() {
**파일**: 화면 관리 페이지
**추가 기능**:
- 화면 생성 시 "분할 패널" 타입 선택
- 분할 패널 설정 UI
- 임베딩 설정 UI
@ -354,15 +389,15 @@ function ScreenViewPage() {
## 📊 충돌 위험도 평가
| 항목 | 위험도 | 설명 | 조치 필요 |
|------|--------|------|-----------|
| 데이터베이스 스키마 | 🟢 낮음 | 독립적인 새 테이블 | ❌ 불필요 |
| API 엔드포인트 | 🟢 낮음 | 새로운 경로 추가 | ❌ 불필요 |
| TypeScript 타입 | 🟢 낮음 | 별도 파일 | ❌ 불필요 |
| 프론트엔드 컴포넌트 | 🟢 낮음 | 별도 디렉토리 | ❌ 불필요 |
| 화면 렌더링 로직 | 🟡 중간 | 조건 분기 추가 필요 | ✅ 필요 |
| 컴포넌트 등록 시스템 | 🟡 중간 | 어댑터 패턴 필요 | ✅ 필요 (Phase 5) |
| 외래키 CASCADE | 🟡 중간 | 화면 삭제 시 주의 | ⚠️ 주의 |
| 항목 | 위험도 | 설명 | 조치 필요 |
| -------------------- | ------- | ------------------- | ----------------- |
| 데이터베이스 스키마 | 🟢 낮음 | 독립적인 새 테이블 | ❌ 불필요 |
| API 엔드포인트 | 🟢 낮음 | 새로운 경로 추가 | ❌ 불필요 |
| TypeScript 타입 | 🟢 낮음 | 별도 파일 | ❌ 불필요 |
| 프론트엔드 컴포넌트 | 🟢 낮음 | 별도 디렉토리 | ❌ 불필요 |
| 화면 렌더링 로직 | 🟡 중간 | 조건 분기 추가 필요 | ✅ 필요 |
| 컴포넌트 등록 시스템 | 🟡 중간 | 어댑터 패턴 필요 | ✅ 필요 (Phase 5) |
| 외래키 CASCADE | 🟡 중간 | 화면 삭제 시 주의 | ⚠️ 주의 |
**전체 위험도**: 🟢 **낮음** (대부분 독립적)
@ -371,24 +406,28 @@ function ScreenViewPage() {
## ✅ 안전성 체크리스트
### 데이터베이스
- [x] 새 테이블명이 기존과 중복되지 않음
- [x] 기존 테이블 구조 변경 없음
- [x] 외래키 CASCADE 설정 완료
- [x] 멀티테넌시 (company_code) 지원
### 백엔드
- [x] 새 라우트가 기존과 충돌하지 않음
- [x] 독립적인 컨트롤러 파일
- [x] 기존 API 수정 없음
- [x] 에러 핸들링 완료
### 프론트엔드
- [x] 새 컴포넌트가 별도 디렉토리
- [x] 기존 컴포넌트 수정 없음
- [x] 독립적인 타입 정의
- [ ] 화면 페이지 수정 필요 (조건 분기)
### 호환성
- [x] 기존 화면 동작 영향 없음
- [x] 하위 호환성 유지
- [ ] 컴포넌트 어댑터 구현 (Phase 5)
@ -400,6 +439,7 @@ function ScreenViewPage() {
### 즉시 조치 (필수)
1. **화면 페이지 수정**
```typescript
// frontend/app/(main)/screens/[screenId]/page.tsx
// 분할 패널 확인 로직 추가
@ -421,11 +461,13 @@ function ScreenViewPage() {
### 단계적 조치 (Phase 5-6)
1. **컴포넌트 어댑터 구현**
- TableComponent → DataReceivable
- InputComponent → DataReceivable
- 기타 컴포넌트들
2. **설정 UI 개발**
- 분할 패널 생성 UI
- 매핑 규칙 설정 UI
- 미리보기 기능
@ -442,6 +484,7 @@ function ScreenViewPage() {
### ✅ 안전성 평가: 높음
**이유**:
1. ✅ 대부분의 코드가 독립적으로 추가됨
2. ✅ 기존 시스템 수정 최소화
3. ✅ 하위 호환성 유지
@ -450,10 +493,12 @@ function ScreenViewPage() {
### ⚠️ 주의 사항
1. **화면 페이지 수정 필요**
- 분할 패널 확인 로직 추가
- 조건부 렌더링 구현
2. **점진적 구현 권장**
- Phase 5: 컴포넌트 어댑터
- Phase 6: 설정 UI
- 단계별 테스트
@ -467,4 +512,3 @@ function ScreenViewPage() {
**충돌 위험도: 낮음 (🟢)**
새로운 시스템은 기존 시스템과 **독립적으로 동작**하며, 최소한의 수정만으로 통합 가능합니다. 화면 페이지에 조건 분기만 추가하면 바로 사용할 수 있습니다.