fix: 화면 복제 기능 개선 및 관련 버그 수정
- 화면 복제 기능을 개선하여 DB 구조 개편 후의 효율적인 화면 관리를 지원합니다. - 그룹 복제 시 버튼의 `targetScreenId`가 새 화면으로 매핑되지 않는 버그를 수정하였습니다. - 관련된 서비스 및 쿼리에서 `table_type_columns`를 사용하여 라벨 정보를 조회하도록 변경하였습니다. - 여러 컨트롤러 및 서비스에서 `column_labels` 대신 `table_type_columns`를 참조하도록 업데이트하였습니다.
This commit is contained in:
436
docs/COMPONENT_URL_ZOD_ARCHITECTURE_ANALYSIS.md
Normal file
436
docs/COMPONENT_URL_ZOD_ARCHITECTURE_ANALYSIS.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# 방안 1: 컴포넌트 URL 참조 + Zod 스키마 관리
|
||||
|
||||
## 1. 현재 문제점 정리
|
||||
|
||||
### 1.1 JSON 구조 불일치
|
||||
|
||||
```
|
||||
현재 상태:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ v2-table-list 컴포넌트 │
|
||||
│ 화면 A: { pageSize: 20, showCheckbox: true } │
|
||||
│ 화면 B: { pagination: { size: 20 }, checkbox: true } │
|
||||
│ 화면 C: { paging: { pageSize: 20 }, hasCheckbox: true } │
|
||||
│ │
|
||||
│ → 같은 설정인데 키 이름이 다름 │
|
||||
│ → 타입 검증 없음 (런타임 에러 발생) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 컴포넌트 수정 시 마이그레이션 필요
|
||||
|
||||
```
|
||||
컴포넌트 구조 변경:
|
||||
pageSize → pagination.pageSize 로 변경하면?
|
||||
|
||||
→ 100개 화면의 JSON 전부 마이그레이션 필요
|
||||
→ 테스트 공수 발생
|
||||
→ 누락 시 런타임 에러
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 방안 1 + Zod 아키텍처
|
||||
|
||||
### 2.1 전체 구조
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. 컴포넌트 코드 + Zod 스키마 (프론트엔드) │
|
||||
│ │
|
||||
│ @/lib/registry/components/v2-table-list/ │
|
||||
│ ├── index.ts # 컴포넌트 등록 │
|
||||
│ ├── TableListRenderer.tsx # 렌더링 로직 │
|
||||
│ ├── schema.ts # ⭐ Zod 스키마 정의 │
|
||||
│ └── defaults.ts # ⭐ 기본값 정의 │
|
||||
│ │
|
||||
│ 코드 수정 → 빌드 → 전 회사 즉시 적용 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ URL로 참조
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. DB (최소한의 차이점만 저장) │
|
||||
│ │
|
||||
│ screen_layouts.properties = { │
|
||||
│ "componentUrl": "@/registry/v2-table-list", │
|
||||
│ "config": { │
|
||||
│ "pageSize": 50 ← 기본값(20)과 다른 것만 │
|
||||
│ } │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ 설정 병합
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. 런타임: 기본값 + 오버라이드 병합 + Zod 검증 │
|
||||
│ │
|
||||
│ 최종 설정 = deepMerge(기본값, 오버라이드) │
|
||||
│ 검증된 설정 = schema.parse(최종 설정) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Zod 스키마 예시
|
||||
|
||||
```typescript
|
||||
// @/lib/registry/components/v2-table-list/schema.ts
|
||||
import { z } from "zod";
|
||||
|
||||
// 컬럼 설정 스키마
|
||||
const columnSchema = z.object({
|
||||
columnName: z.string(),
|
||||
displayName: z.string(),
|
||||
visible: z.boolean().default(true),
|
||||
sortable: z.boolean().default(true),
|
||||
width: z.number().optional(),
|
||||
align: z.enum(["left", "center", "right"]).default("left"),
|
||||
format: z.enum(["text", "number", "date", "currency"]).default("text"),
|
||||
order: z.number().default(0),
|
||||
});
|
||||
|
||||
// 페이지네이션 스키마
|
||||
const paginationSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
pageSize: z.number().default(20),
|
||||
showSizeSelector: z.boolean().default(true),
|
||||
pageSizeOptions: z.array(z.number()).default([10, 20, 50, 100]),
|
||||
});
|
||||
|
||||
// 체크박스 스키마
|
||||
const checkboxSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
multiple: z.boolean().default(true),
|
||||
position: z.enum(["left", "right"]).default("left"),
|
||||
});
|
||||
|
||||
// 테이블 리스트 전체 스키마
|
||||
export const tableListSchema = z.object({
|
||||
tableName: z.string(),
|
||||
columns: z.array(columnSchema).default([]),
|
||||
pagination: paginationSchema.default({}),
|
||||
checkbox: checkboxSchema.default({}),
|
||||
showHeader: z.boolean().default(true),
|
||||
autoLoad: z.boolean().default(true),
|
||||
});
|
||||
|
||||
// 타입 자동 추론
|
||||
export type TableListConfig = z.infer<typeof tableListSchema>;
|
||||
```
|
||||
|
||||
### 2.3 기본값 정의
|
||||
|
||||
```typescript
|
||||
// @/lib/registry/components/v2-table-list/defaults.ts
|
||||
import { TableListConfig } from "./schema";
|
||||
|
||||
export const defaultConfig: Partial<TableListConfig> = {
|
||||
pagination: {
|
||||
enabled: true,
|
||||
pageSize: 20,
|
||||
showSizeSelector: true,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
},
|
||||
checkbox: {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
position: "left",
|
||||
},
|
||||
showHeader: true,
|
||||
autoLoad: true,
|
||||
};
|
||||
```
|
||||
|
||||
### 2.4 설정 로드 로직
|
||||
|
||||
```typescript
|
||||
// @/lib/registry/utils/configLoader.ts
|
||||
import { deepMerge } from "@/lib/utils";
|
||||
|
||||
export function loadComponentConfig<T>(
|
||||
componentUrl: string,
|
||||
overrideConfig: Partial<T>
|
||||
): T {
|
||||
// 1. 컴포넌트 모듈에서 스키마와 기본값 가져오기
|
||||
const { schema, defaultConfig } = getComponentModule(componentUrl);
|
||||
|
||||
// 2. 기본값 + 오버라이드 병합
|
||||
const mergedConfig = deepMerge(defaultConfig, overrideConfig);
|
||||
|
||||
// 3. Zod 스키마로 검증 + 기본값 자동 적용
|
||||
const validatedConfig = schema.parse(mergedConfig);
|
||||
|
||||
return validatedConfig;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 현재 시스템 적응도 분석
|
||||
|
||||
### 3.1 변경이 필요한 부분
|
||||
|
||||
| 영역 | 현재 | 변경 후 | 공수 |
|
||||
|-----|-----|--------|-----|
|
||||
| **컴포넌트 폴더 구조** | types.ts만 있음 | schema.ts, defaults.ts 추가 | 중간 |
|
||||
| **screen_layouts** | 모든 설정 저장 | URL + 차이점만 저장 | 중간 |
|
||||
| **화면 저장 로직** | JSON 통째로 저장 | 차이점 추출 후 저장 | 중간 |
|
||||
| **화면 로드 로직** | JSON 그대로 사용 | 기본값 병합 + Zod 검증 | 낮음 |
|
||||
| **기존 데이터** | - | 마이그레이션 필요 | 높음 |
|
||||
|
||||
### 3.2 기존 코드와의 호환성
|
||||
|
||||
```
|
||||
현재 Zod 사용 현황:
|
||||
✅ zod v4.1.5 이미 설치됨
|
||||
✅ @hookform/resolvers 설치됨 (react-hook-form + Zod 연동)
|
||||
✅ 공통코드 관리에 Zod 스키마 사용 중 (lib/schemas/commonCode.ts)
|
||||
|
||||
→ Zod 패턴이 이미 프로젝트에 존재함
|
||||
→ 동일한 패턴으로 컴포넌트 스키마 추가 가능
|
||||
```
|
||||
|
||||
### 3.3 점진적 마이그레이션 가능 여부
|
||||
|
||||
```
|
||||
Phase 1: 새 컴포넌트만 적용
|
||||
- 신규 컴포넌트는 schema.ts + defaults.ts 구조로 생성
|
||||
- 기존 컴포넌트는 그대로 유지
|
||||
|
||||
Phase 2: 핵심 컴포넌트 마이그레이션
|
||||
- v2-table-list, v2-button-primary 등 자주 사용하는 것 먼저
|
||||
- 기존 JSON 데이터 → 차이점만 남기고 정리
|
||||
|
||||
Phase 3: 전체 마이그레이션
|
||||
- 나머지 컴포넌트 순차 적용
|
||||
|
||||
→ 점진적 적용 가능 ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 향후 장점
|
||||
|
||||
### 4.1 컴포넌트 수정 시
|
||||
|
||||
```
|
||||
변경 전:
|
||||
컴포넌트 수정 → 100개 화면 JSON 마이그레이션 → 테스트 → 배포
|
||||
|
||||
변경 후:
|
||||
컴포넌트 수정 → 빌드 → 배포 → 끝
|
||||
|
||||
왜?
|
||||
- 기본값/로직은 코드에 있음
|
||||
- DB에는 "다른 것만" 저장되어 있음
|
||||
- 코드 변경이 자동으로 모든 화면에 적용됨
|
||||
```
|
||||
|
||||
### 4.2 새 설정 추가 시
|
||||
|
||||
```
|
||||
변경 전:
|
||||
1. types.ts 수정
|
||||
2. 100개 화면 JSON에 새 필드 추가 (마이그레이션)
|
||||
3. 기본값 없으면 에러 발생
|
||||
|
||||
변경 후:
|
||||
1. schema.ts에 필드 추가 + .default() 설정
|
||||
2. 끝. 기존 데이터는 자동으로 기본값 적용됨
|
||||
|
||||
// 예시
|
||||
const schema = z.object({
|
||||
// 기존 필드
|
||||
pageSize: z.number().default(20),
|
||||
|
||||
// 🆕 새 필드 추가 - 기본값 있으면 마이그레이션 불필요
|
||||
showRowNumber: z.boolean().default(false),
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 타입 안정성
|
||||
|
||||
```typescript
|
||||
// 현재: 타입 검증 없음
|
||||
const config = component.componentConfig; // any 타입
|
||||
config.pageSize; // 있을 수도, 없을 수도...
|
||||
config.pagination.pageSize; // 구조가 다를 수도...
|
||||
|
||||
// 변경 후: Zod로 검증 + TypeScript 타입 추론
|
||||
const config = tableListSchema.parse(rawConfig);
|
||||
config.pagination.pageSize; // ✅ 타입 보장
|
||||
config.unknownField; // ❌ 컴파일 에러
|
||||
```
|
||||
|
||||
### 4.4 런타임 에러 방지
|
||||
|
||||
```typescript
|
||||
// Zod 검증 실패 시 명확한 에러 메시지
|
||||
try {
|
||||
const config = tableListSchema.parse(rawConfig);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error("설정 오류:", error.errors);
|
||||
// [
|
||||
// { path: ["pagination", "pageSize"], message: "Expected number, received string" },
|
||||
// { path: ["columns", 0, "align"], message: "Invalid enum value" }
|
||||
// ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 문서화 자동화
|
||||
|
||||
```typescript
|
||||
// Zod 스키마에서 자동으로 문서 생성 가능
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
|
||||
const jsonSchema = zodToJsonSchema(tableListSchema);
|
||||
// → JSON Schema 형식으로 변환 → 문서화 도구에서 사용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 유지보수 측면
|
||||
|
||||
### 5.1 컴포넌트 개발자 입장
|
||||
|
||||
| 작업 | 현재 | 변경 후 |
|
||||
|-----|-----|--------|
|
||||
| 새 컴포넌트 생성 | types.ts 작성 (선택) | schema.ts + defaults.ts 작성 (필수) |
|
||||
| 설정 구조 변경 | 마이그레이션 스크립트 작성 | schema 수정 + 기본값 설정 |
|
||||
| 타입 체크 | 수동 검증 | Zod가 자동 검증 |
|
||||
| 디버깅 | console.log로 추적 | Zod 에러 메시지로 바로 파악 |
|
||||
|
||||
### 5.2 화면 개발자 입장
|
||||
|
||||
| 작업 | 현재 | 변경 후 |
|
||||
|-----|-----|--------|
|
||||
| 화면 생성 | 모든 설정 직접 지정 | 필요한 것만 오버라이드 |
|
||||
| 설정 실수 | 런타임 에러 | 저장 시 Zod 검증 에러 |
|
||||
| 기본값 확인 | 코드 뒤져보기 | defaults.ts 확인 |
|
||||
|
||||
### 5.3 운영자 입장
|
||||
|
||||
| 작업 | 현재 | 변경 후 |
|
||||
|-----|-----|--------|
|
||||
| 일괄 설정 변경 | 100개 JSON 수정 | defaults.ts 수정 → 전체 적용 |
|
||||
| 회사별 기본값 | 불가능 | 회사별 defaults 테이블 추가 가능 |
|
||||
| 오류 추적 | 어려움 | Zod 검증 로그 확인 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 데이터 마이그레이션 계획
|
||||
|
||||
### 6.1 차이점 추출 스크립트
|
||||
|
||||
```typescript
|
||||
// 기존 JSON에서 기본값과 다른 것만 추출
|
||||
async function extractDiff(componentUrl: string, fullConfig: any): Promise<any> {
|
||||
const { defaultConfig } = getComponentModule(componentUrl);
|
||||
|
||||
function getDiff(defaults: any, current: any): any {
|
||||
const diff: any = {};
|
||||
|
||||
for (const key of Object.keys(current)) {
|
||||
if (defaults[key] === undefined) {
|
||||
// 기본값에 없는 키 = 그대로 유지
|
||||
diff[key] = current[key];
|
||||
} else if (typeof current[key] === 'object' && !Array.isArray(current[key])) {
|
||||
// 중첩 객체 = 재귀 비교
|
||||
const nestedDiff = getDiff(defaults[key], current[key]);
|
||||
if (Object.keys(nestedDiff).length > 0) {
|
||||
diff[key] = nestedDiff;
|
||||
}
|
||||
} else if (JSON.stringify(defaults[key]) !== JSON.stringify(current[key])) {
|
||||
// 값이 다름 = 저장
|
||||
diff[key] = current[key];
|
||||
}
|
||||
// 값이 같음 = 저장 안 함 (기본값 사용)
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
return getDiff(defaultConfig, fullConfig);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 마이그레이션 순서
|
||||
|
||||
```
|
||||
1. 컴포넌트별 schema.ts, defaults.ts 작성
|
||||
2. 기존 데이터 분석 (어떤 설정이 자주 사용되는지)
|
||||
3. 가장 많이 사용되는 값을 기본값으로 설정
|
||||
4. 차이점 추출 스크립트 실행
|
||||
5. 새 구조로 데이터 업데이트
|
||||
6. 테스트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 예상 공수
|
||||
|
||||
| 단계 | 작업 | 예상 공수 |
|
||||
|-----|-----|---------|
|
||||
| **Phase 1** | 아키텍처 설계 + 유틸리티 함수 | 1주 |
|
||||
| **Phase 2** | 핵심 컴포넌트 5개 스키마 작성 | 1주 |
|
||||
| **Phase 3** | 데이터 마이그레이션 스크립트 | 1주 |
|
||||
| **Phase 4** | 테스트 + 버그 수정 | 1주 |
|
||||
| **Phase 5** | 나머지 컴포넌트 순차 적용 | 2-3주 |
|
||||
| **총계** | | **6-7주** |
|
||||
|
||||
---
|
||||
|
||||
## 8. 위험 요소 및 대응
|
||||
|
||||
### 8.1 위험 요소
|
||||
|
||||
| 위험 | 영향 | 대응 |
|
||||
|-----|-----|-----|
|
||||
| 기존 데이터 손실 | 높음 | 마이그레이션 전 백업 필수 |
|
||||
| 스키마 설계 실수 | 중간 | 충분한 리뷰 + 테스트 |
|
||||
| 런타임 성능 저하 | 낮음 | Zod는 충분히 빠름 |
|
||||
| 개발자 학습 비용 | 낮음 | Zod는 직관적, 이미 사용 중 |
|
||||
|
||||
### 8.2 롤백 계획
|
||||
|
||||
```
|
||||
문제 발생 시:
|
||||
1. 기존 JSON 구조로 데이터 복원 (백업에서)
|
||||
2. 새 로직 비활성화 (feature flag)
|
||||
3. 원인 분석 후 재시도
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 결론
|
||||
|
||||
### 9.1 방안 1 + Zod 조합의 평가
|
||||
|
||||
| 항목 | 점수 | 이유 |
|
||||
|-----|-----|-----|
|
||||
| **현재 시스템 적응도** | ★★★★☆ | Zod 이미 사용 중, 점진적 적용 가능 |
|
||||
| **향후 확장성** | ★★★★★ | 새 설정 추가 용이, 타입 안정성 |
|
||||
| **유지보수성** | ★★★★★ | 코드 수정 → 전 회사 적용, 명확한 에러 |
|
||||
| **마이그레이션 공수** | ★★★☆☆ | 6-7주 소요, 점진적 적용으로 리스크 분산 |
|
||||
| **안정성** | ★★★★☆ | Zod 검증으로 런타임 에러 방지 |
|
||||
|
||||
### 9.2 최종 권장
|
||||
|
||||
```
|
||||
✅ 방안 1 (URL 참조 + Zod 스키마) 적용 권장
|
||||
|
||||
이유:
|
||||
1. 컴포넌트 수정 → 코드만 변경 → 전 회사 자동 적용
|
||||
2. Zod로 JSON 구조 일관성 보장
|
||||
3. 타입 안정성 + 런타임 검증
|
||||
4. 기존 시스템과 호환 (Zod 이미 사용 중)
|
||||
5. 점진적 마이그레이션 가능
|
||||
```
|
||||
|
||||
### 9.3 다음 단계
|
||||
|
||||
1. 핵심 컴포넌트 1개로 PoC (Proof of Concept)
|
||||
2. 팀 리뷰 및 피드백
|
||||
3. 표준 패턴 확정
|
||||
4. 순차적 적용
|
||||
Reference in New Issue
Block a user