feat: V2 레이아웃 동기화 및 컴포넌트 개선
- TableManagementService에서 V2 레이아웃 동기화 로직을 추가하여, 새로운 입력 타입에 따라 화면 레이아웃을 자동으로 업데이트하도록 개선하였습니다. - syncScreenLayoutsV2InputType 메서드를 통해 V2 레이아웃의 컴포넌트 source를 동기화하는 기능을 구현하였습니다. - EditModal에서 배열 데이터를 쉼표 구분 문자열로 변환하는 로직을 추가하여, 손상된 값을 필터링하고 데이터 저장 시 일관성을 높였습니다. - CategorySelectComponent에서 불필요한 스타일 및 높이 관련 props를 제거하여 코드 간결성을 개선하였습니다. - V2Select 및 관련 컴포넌트에서 height 스타일을 통일하여 사용자 경험을 향상시켰습니다.
This commit is contained in:
253
docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md
Normal file
253
docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 다중 선택(Multi-Select) 배열 직렬화 문제 해결 보고서
|
||||
|
||||
## 문제 요약
|
||||
|
||||
**증상**: 다중 선택 컴포넌트(TagboxSelect, 체크박스 등)로 선택한 값이 DB에 저장될 때 손상되거나 `null`로 저장됨
|
||||
|
||||
**영향받는 기능**:
|
||||
- 품목정보의 `division` (구분) 필드
|
||||
- 모든 다중 선택 카테고리 필드
|
||||
|
||||
**손상된 데이터 예시**:
|
||||
```
|
||||
{"{\"{\\\"CAT_ML7SR2T9_IM7H\\\",\\\"CAT_ML8ZFQFU_EE5Z\\\"}\"}",...}
|
||||
```
|
||||
|
||||
**정상 데이터 예시**:
|
||||
```
|
||||
CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z,CAT_ML8ZFVEL_1TOR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문제 원인 분석
|
||||
|
||||
### 1. PostgreSQL의 배열 자동 변환
|
||||
|
||||
Node.js의 `node-pg` 라이브러리는 JavaScript 배열을 PostgreSQL 배열 리터럴(`{...}`)로 자동 변환합니다.
|
||||
|
||||
```javascript
|
||||
// JavaScript
|
||||
["CAT_1", "CAT_2", "CAT_3"]
|
||||
|
||||
// PostgreSQL로 자동 변환됨
|
||||
{"CAT_1","CAT_2","CAT_3"}
|
||||
```
|
||||
|
||||
하지만 우리 시스템은 커스텀 테이블에서 **쉼표 구분 문자열**을 기대합니다:
|
||||
```
|
||||
CAT_1,CAT_2,CAT_3
|
||||
```
|
||||
|
||||
### 2. 여러 저장 경로의 존재
|
||||
|
||||
코드를 분석한 결과, 저장 로직이 여러 경로로 나뉘어 있었습니다:
|
||||
|
||||
| 경로 | 파일 | 설명 |
|
||||
|------|------|------|
|
||||
| 1 | `buttonActions.ts` | 기본 저장 로직 (INSERT/UPDATE) |
|
||||
| 2 | `EditModal.tsx` | 모달 내 직접 저장 (CREATE/UPDATE) |
|
||||
| 3 | `nodeFlowExecutionService.ts` | 백엔드 노드 플로우 저장 |
|
||||
|
||||
### 3. 왜 초기 수정이 실패했는가?
|
||||
|
||||
#### 시도 1: `buttonActions.ts`에 배열 변환 추가
|
||||
```typescript
|
||||
// buttonActions.ts (라인 1002-1025)
|
||||
if (isUpdate) {
|
||||
for (const key of Object.keys(formData)) {
|
||||
if (Array.isArray(value)) {
|
||||
formData[key] = value.join(",");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**실패 이유**: `EditModal`이 `onSave` 콜백을 제공하면, `buttonActions.ts`는 이 콜백을 바로 호출하고 내부 저장 로직을 건너뜀
|
||||
|
||||
```typescript
|
||||
// buttonActions.ts (라인 545-552)
|
||||
if (onSave) {
|
||||
await onSave(); // 바로 여기서 EditModal.handleSave()가 호출됨
|
||||
return true; // 아래 배열 변환 로직에 도달하지 않음!
|
||||
}
|
||||
```
|
||||
|
||||
#### 시도 2: `nodeFlowExecutionService.ts`에 `normalizeValueForDB` 추가
|
||||
|
||||
**부분 성공**: INSERT에서는 동작했으나, EditModal의 UPDATE 경로는 여전히 문제
|
||||
|
||||
---
|
||||
|
||||
## 최종 해결 방법
|
||||
|
||||
### 핵심 수정: `EditModal.tsx`에 직접 배열 변환 추가
|
||||
|
||||
EditModal이 직접 `dynamicFormApi.updateFormDataPartial`을 호출하므로, **저장 직전**에 배열을 변환해야 했습니다.
|
||||
|
||||
#### 수정 위치 1: UPDATE 경로 (라인 957-1002)
|
||||
|
||||
```typescript
|
||||
// EditModal.tsx - UPDATE 모드
|
||||
Object.keys(formData).forEach((key) => {
|
||||
if (formData[key] !== originalData[key]) {
|
||||
let value = formData[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// 리피터 데이터 제외
|
||||
const isRepeaterData = value.length > 0 &&
|
||||
typeof value[0] === "object" &&
|
||||
("_targetTable" in value[0] || "_isNewItem" in value[0]);
|
||||
|
||||
if (!isRepeaterData) {
|
||||
// 🔧 손상된 값 필터링
|
||||
const isValidValue = (v: any): boolean => {
|
||||
if (typeof v === "number") return true;
|
||||
if (typeof v !== "string") return false;
|
||||
if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\"))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// 유효한 값만 쉼표로 연결
|
||||
const validValues = value.filter(isValidValue);
|
||||
value = validValues.join(",");
|
||||
}
|
||||
}
|
||||
|
||||
changedData[key] = value;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 수정 위치 2: CREATE 경로 (라인 855-875)
|
||||
|
||||
```typescript
|
||||
// EditModal.tsx - CREATE 모드
|
||||
Object.entries(dataToSave).forEach(([key, value]) => {
|
||||
if (!Array.isArray(value)) {
|
||||
masterDataToSave[key] = value;
|
||||
} else {
|
||||
const isRepeaterData = /* 리피터 체크 */;
|
||||
|
||||
if (isRepeaterData) {
|
||||
// 리피터 데이터는 제외 (별도 저장)
|
||||
} else {
|
||||
// 다중 선택 배열 → 쉼표 구분 문자열
|
||||
const validValues = value.filter(isValidValue);
|
||||
masterDataToSave[key] = validValues.join(",");
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 수정 위치 3: 그룹 UPDATE 경로 (라인 630-650)
|
||||
|
||||
그룹 품목 수정 시에도 동일한 로직 적용
|
||||
|
||||
---
|
||||
|
||||
## 손상된 데이터 필터링
|
||||
|
||||
기존에 손상된 데이터가 배열에 포함될 수 있어서, 변환 전 필터링이 필요했습니다:
|
||||
|
||||
```typescript
|
||||
const isValidValue = (v: any): boolean => {
|
||||
// 숫자는 유효
|
||||
if (typeof v === "number" && !isNaN(v)) return true;
|
||||
// 문자열이 아니면 무효
|
||||
if (typeof v !== "string") return false;
|
||||
// 빈 값 무효
|
||||
if (!v || v.trim() === "") return false;
|
||||
// PostgreSQL 배열 형식 감지 → 무효
|
||||
if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\"))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
**필터링 예시**:
|
||||
```
|
||||
입력 배열: ['{"CAT_1","CAT_2"}', 'CAT_ML7SR2T9_IM7H', 'CAT_ML8ZFQFU_EE5Z']
|
||||
↑ 손상됨 (필터링) ↑ 유효 ↑ 유효
|
||||
|
||||
출력: 'CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정된 파일 목록
|
||||
|
||||
| 파일 | 수정 내용 |
|
||||
|------|-----------|
|
||||
| `frontend/components/screen/EditModal.tsx` | CREATE/UPDATE/그룹UPDATE 경로에 배열→문자열 변환 + 손상값 필터링 |
|
||||
| `frontend/lib/utils/buttonActions.ts` | INSERT 경로에 배열→문자열 변환 (이미 수정됨) |
|
||||
| `frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx` | handleChange에서 배열→문자열 변환 |
|
||||
| `backend-node/src/services/nodeFlowExecutionService.ts` | normalizeValueForDB 헬퍼 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 교훈 및 향후 주의사항
|
||||
|
||||
### 1. 저장 경로 파악의 중요성
|
||||
|
||||
프론트엔드에서 저장 로직이 여러 경로로 분기될 수 있으므로, **모든 경로를 추적**해야 합니다.
|
||||
|
||||
```
|
||||
사용자 저장 버튼 클릭
|
||||
↓
|
||||
ButtonPrimaryComponent
|
||||
↓
|
||||
buttonActions.handleSave()
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ onSave 콜백이 있으면? │
|
||||
│ → EditModal.handleSave() 직접 호출│ ← 이 경로를 놓침!
|
||||
│ onSave 콜백이 없으면? │
|
||||
│ → buttonActions 내부 저장 로직 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. 로그 기반 디버깅
|
||||
|
||||
로그가 어디까지 찍히고 어디서 안 찍히는지를 통해 코드 경로를 추적:
|
||||
|
||||
```
|
||||
[예상한 로그]
|
||||
buttonActions.ts:512 🔍 [handleSave] 진입
|
||||
buttonActions.ts:1021 🔧 배열→문자열 변환 ← 이게 안 나옴!
|
||||
|
||||
[실제 로그]
|
||||
buttonActions.ts:512 🔍 [handleSave] 진입
|
||||
dynamicForm.ts:140 🔄 폼 데이터 부분 업데이트 ← 바로 여기로 점프!
|
||||
```
|
||||
|
||||
### 3. 리피터 데이터 vs 다중 선택 구분
|
||||
|
||||
배열이라고 모두 쉼표 문자열로 변환하면 안 됩니다:
|
||||
|
||||
| 타입 | 예시 | 처리 방법 |
|
||||
|------|------|-----------|
|
||||
| 다중 선택 | `["CAT_1", "CAT_2"]` | 쉼표 문자열로 변환 |
|
||||
| 리피터 데이터 | `[{id: 1, _targetTable: "..."}]` | 별도 테이블에 저장, 마스터에서 제외 |
|
||||
|
||||
---
|
||||
|
||||
## 확인된 정상 동작
|
||||
|
||||
```
|
||||
EditModal.tsx:1002 🔧 [EditModal UPDATE] 배열→문자열 변환: division
|
||||
{original: 3, valid: 3, converted: 'CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z,CAT_ML8ZFVEL_1TOR'}
|
||||
|
||||
dynamicForm.ts:153 ✅ 폼 데이터 부분 업데이트 성공
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작성일
|
||||
|
||||
2026-02-05
|
||||
|
||||
## 작성자
|
||||
|
||||
AI Assistant (Claude)
|
||||
Reference in New Issue
Block a user