Phase 2 ScreenManagementService 전환 완료
This commit is contained in:
parent
4637680de0
commit
1a640850c5
|
|
@ -6,21 +6,21 @@ ScreenManagementService는 **46개의 Prisma 호출**이 있는 가장 복잡한
|
|||
|
||||
### 📊 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 파일 위치 | `backend-node/src/services/screenManagementService.ts` |
|
||||
| 파일 크기 | 1,700+ 라인 |
|
||||
| Prisma 호출 | 46개 |
|
||||
| **현재 진행률** | **17/46 (37.0%)** ✅ |
|
||||
| 복잡도 | 매우 높음 |
|
||||
| 우선순위 | 🔴 최우선 |
|
||||
| 항목 | 내용 |
|
||||
| --------------- | ------------------------------------------------------ |
|
||||
| 파일 위치 | `backend-node/src/services/screenManagementService.ts` |
|
||||
| 파일 크기 | 1,700+ 라인 |
|
||||
| Prisma 호출 | 46개 |
|
||||
| **현재 진행률** | **46/46 (100%)** ✅ **완료** |
|
||||
| 복잡도 | 매우 높음 |
|
||||
| 우선순위 | 🔴 최우선 |
|
||||
|
||||
### 🎯 전환 현황 (2025-09-30 업데이트)
|
||||
|
||||
- ✅ **Stage 1 완료**: 기본 CRUD (8개 함수) - Commit: 13c1bc4, 0e8d1d4
|
||||
- ✅ **Stage 2 완료**: 레이아웃 관리 (2개 함수, 4 Prisma 호출) - Commit: 67dced7
|
||||
- ✅ **Stage 3 완료**: 템플릿 & 메뉴 관리 (5개 함수) - Commit: 74351e8
|
||||
- 🔄 **Stage 4 진행 중**: 복잡한 기능 (트랜잭션)
|
||||
- ✅ **Stage 4 완료**: 복잡한 기능 (트랜잭션) - **모든 46개 Prisma 호출 전환 완료**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -100,84 +100,84 @@ await prisma.screen_definitions.findMany({ where })
|
|||
|
||||
```typescript
|
||||
// Line 1096: 레이아웃 삭제
|
||||
await prisma.screen_layouts.deleteMany({ where: { screen_id } })
|
||||
await prisma.screen_layouts.deleteMany({ where: { screen_id } });
|
||||
|
||||
// Line 1107: 레이아웃 생성 (단일)
|
||||
await prisma.screen_layouts.create({ data })
|
||||
await prisma.screen_layouts.create({ data });
|
||||
|
||||
// Line 1152: 레이아웃 생성 (다중)
|
||||
await prisma.screen_layouts.create({ data })
|
||||
await prisma.screen_layouts.create({ data });
|
||||
|
||||
// Line 1193: 레이아웃 조회
|
||||
await prisma.screen_layouts.findMany({ where })
|
||||
await prisma.screen_layouts.findMany({ where });
|
||||
```
|
||||
|
||||
### 3. 템플릿 관리 (Screen Templates) - 2개
|
||||
|
||||
```typescript
|
||||
// Line 1303: 템플릿 목록 조회
|
||||
await prisma.screen_templates.findMany({ where })
|
||||
await prisma.screen_templates.findMany({ where });
|
||||
|
||||
// Line 1317: 템플릿 생성
|
||||
await prisma.screen_templates.create({ data })
|
||||
await prisma.screen_templates.create({ data });
|
||||
```
|
||||
|
||||
### 4. 메뉴 할당 (Screen Menu Assignments) - 5개
|
||||
|
||||
```typescript
|
||||
// Line 446: 메뉴 할당 조회
|
||||
await prisma.screen_menu_assignments.findMany({ where })
|
||||
await prisma.screen_menu_assignments.findMany({ where });
|
||||
|
||||
// Line 1346: 메뉴 할당 중복 확인
|
||||
await prisma.screen_menu_assignments.findFirst({ where })
|
||||
await prisma.screen_menu_assignments.findFirst({ where });
|
||||
|
||||
// Line 1358: 메뉴 할당 생성
|
||||
await prisma.screen_menu_assignments.create({ data })
|
||||
await prisma.screen_menu_assignments.create({ data });
|
||||
|
||||
// Line 1376: 화면별 메뉴 할당 조회
|
||||
await prisma.screen_menu_assignments.findMany({ where })
|
||||
await prisma.screen_menu_assignments.findMany({ where });
|
||||
|
||||
// Line 1401: 메뉴 할당 삭제
|
||||
await prisma.screen_menu_assignments.deleteMany({ where })
|
||||
await prisma.screen_menu_assignments.deleteMany({ where });
|
||||
```
|
||||
|
||||
### 5. 테이블 레이블 (Table Labels) - 3개
|
||||
|
||||
```typescript
|
||||
// Line 117: 테이블 레이블 조회 (페이징)
|
||||
await prisma.table_labels.findMany({ where, skip, take })
|
||||
await prisma.table_labels.findMany({ where, skip, take });
|
||||
|
||||
// Line 713: 테이블 레이블 조회 (전체)
|
||||
await prisma.table_labels.findMany({ where })
|
||||
await prisma.table_labels.findMany({ where });
|
||||
```
|
||||
|
||||
### 6. 컬럼 레이블 (Column Labels) - 2개
|
||||
|
||||
```typescript
|
||||
// Line 948: 웹타입 정보 조회
|
||||
await prisma.column_labels.findMany({ where, select })
|
||||
await prisma.column_labels.findMany({ where, select });
|
||||
|
||||
// Line 1456: 컬럼 레이블 UPSERT
|
||||
await prisma.column_labels.upsert({ where, create, update })
|
||||
await prisma.column_labels.upsert({ where, create, update });
|
||||
```
|
||||
|
||||
### 7. Raw Query 사용 (이미 있음) - 6개
|
||||
|
||||
```typescript
|
||||
// Line 627: 화면 순서 변경 (일괄 업데이트)
|
||||
await prisma.$executeRaw`UPDATE screen_definitions SET display_order = ...`
|
||||
await prisma.$executeRaw`UPDATE screen_definitions SET display_order = ...`;
|
||||
|
||||
// Line 833: 테이블 목록 조회
|
||||
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`
|
||||
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`;
|
||||
|
||||
// Line 876: 테이블 존재 확인
|
||||
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`
|
||||
await prisma.$queryRaw<Array<{ table_name: string }>>`SELECT tablename ...`;
|
||||
|
||||
// Line 922: 테이블 컬럼 정보 조회
|
||||
await prisma.$queryRaw<Array<ColumnInfo>>`SELECT column_name, data_type ...`
|
||||
await prisma.$queryRaw<Array<ColumnInfo>>`SELECT column_name, data_type ...`;
|
||||
|
||||
// Line 1418: 컬럼 정보 조회 (상세)
|
||||
await prisma.$queryRaw`SELECT column_name, data_type ...`
|
||||
await prisma.$queryRaw`SELECT column_name, data_type ...`;
|
||||
```
|
||||
|
||||
### 8. 트랜잭션 사용 - 3개
|
||||
|
|
@ -210,6 +210,7 @@ await prisma.$transaction(async (tx) => { ... })
|
|||
### 전략 2: 함수별 전환 우선순위
|
||||
|
||||
#### 🔴 최우선 (기본 CRUD)
|
||||
|
||||
- `createScreen()` - Line 70
|
||||
- `getScreensByCompany()` - Line 99-105
|
||||
- `getScreenByCode()` - Line 178
|
||||
|
|
@ -218,11 +219,13 @@ await prisma.$transaction(async (tx) => { ... })
|
|||
- `deleteScreen()` - Line 672
|
||||
|
||||
#### 🟡 2순위 (레이아웃)
|
||||
|
||||
- `saveLayout()` - Line 1096-1152
|
||||
- `getLayout()` - Line 1193
|
||||
- `deleteLayout()` - Line 1096
|
||||
|
||||
#### 🟢 3순위 (템플릿 & 메뉴)
|
||||
|
||||
- `getTemplates()` - Line 1303
|
||||
- `createTemplate()` - Line 1317
|
||||
- `assignToMenu()` - Line 1358
|
||||
|
|
@ -230,6 +233,7 @@ await prisma.$transaction(async (tx) => { ... })
|
|||
- `removeMenuAssignment()` - Line 1401
|
||||
|
||||
#### 🔵 4순위 (복잡한 기능)
|
||||
|
||||
- `copyScreen()` - Line 593 (트랜잭션)
|
||||
- `applyTemplate()` - Line 521 (트랜잭션)
|
||||
- `bulkDelete()` - Line 788 (트랜잭션)
|
||||
|
|
@ -242,6 +246,7 @@ await prisma.$transaction(async (tx) => { ... })
|
|||
### 예시 1: createScreen() 전환
|
||||
|
||||
**기존 Prisma 코드:**
|
||||
|
||||
```typescript
|
||||
// Line 53: 중복 확인
|
||||
const existingScreen = await prisma.screen_definitions.findFirst({
|
||||
|
|
@ -265,6 +270,7 @@ const screen = await prisma.screen_definitions.create({
|
|||
```
|
||||
|
||||
**새로운 Raw Query 코드:**
|
||||
|
||||
```typescript
|
||||
import { query } from "../database/db";
|
||||
|
||||
|
|
@ -300,6 +306,7 @@ const [screen] = await query<ScreenDefinition>(
|
|||
### 예시 2: getScreensByCompany() 전환 (페이징)
|
||||
|
||||
**기존 Prisma 코드:**
|
||||
|
||||
```typescript
|
||||
const [screens, total] = await Promise.all([
|
||||
prisma.screen_definitions.findMany({
|
||||
|
|
@ -313,12 +320,15 @@ const [screens, total] = await Promise.all([
|
|||
```
|
||||
|
||||
**새로운 Raw Query 코드:**
|
||||
|
||||
```typescript
|
||||
const offset = (page - 1) * size;
|
||||
const whereSQL = companyCode !== "*"
|
||||
? "WHERE company_code = $1 AND is_active != 'D'"
|
||||
: "WHERE is_active != 'D'";
|
||||
const params = companyCode !== "*" ? [companyCode, size, offset] : [size, offset];
|
||||
const whereSQL =
|
||||
companyCode !== "*"
|
||||
? "WHERE company_code = $1 AND is_active != 'D'"
|
||||
: "WHERE is_active != 'D'";
|
||||
const params =
|
||||
companyCode !== "*" ? [companyCode, size, offset] : [size, offset];
|
||||
|
||||
const [screens, totalResult] = await Promise.all([
|
||||
query<ScreenDefinition>(
|
||||
|
|
@ -340,6 +350,7 @@ const total = totalResult[0]?.count || 0;
|
|||
### 예시 3: 트랜잭션 전환
|
||||
|
||||
**기존 Prisma 코드:**
|
||||
|
||||
```typescript
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const newScreen = await tx.screen_definitions.create({ data: { ... } });
|
||||
|
|
@ -348,6 +359,7 @@ await prisma.$transaction(async (tx) => {
|
|||
```
|
||||
|
||||
**새로운 Raw Query 코드:**
|
||||
|
||||
```typescript
|
||||
import { transaction } from "../database/db";
|
||||
|
||||
|
|
@ -406,6 +418,7 @@ describe("화면 관리 통합 테스트", () => {
|
|||
## 📋 체크리스트
|
||||
|
||||
### 1단계: 기본 CRUD (8개 함수) ✅ **완료**
|
||||
|
||||
- [x] `createScreen()` - 화면 생성
|
||||
- [x] `getScreensByCompany()` - 화면 목록 (페이징)
|
||||
- [x] `getScreenByCode()` - 화면 코드로 조회
|
||||
|
|
@ -416,11 +429,13 @@ describe("화면 관리 통합 테스트", () => {
|
|||
- [x] `getScreen()` - 회사 코드 필터링 포함 조회
|
||||
|
||||
### 2단계: 레이아웃 관리 (2개 함수) ✅ **완료**
|
||||
|
||||
- [x] `saveLayout()` - 레이아웃 저장 (메타데이터 + 컴포넌트)
|
||||
- [x] `getLayout()` - 레이아웃 조회
|
||||
- [x] 레이아웃 삭제 로직 (saveLayout 내부에 포함)
|
||||
|
||||
### 3단계: 템플릿 & 메뉴 (5개 함수) ✅ **완료**
|
||||
|
||||
- [x] `getTemplatesByCompany()` - 템플릿 목록
|
||||
- [x] `createTemplate()` - 템플릿 생성
|
||||
- [x] `assignScreenToMenu()` - 메뉴 할당
|
||||
|
|
@ -428,32 +443,124 @@ describe("화면 관리 통합 테스트", () => {
|
|||
- [x] `unassignScreenFromMenu()` - 메뉴 할당 해제
|
||||
- [ ] 테이블 레이블 조회 (getScreensByCompany 내부에 포함됨)
|
||||
|
||||
### 4단계: 복잡한 기능 (4개 함수) 🔄 **진행 중**
|
||||
- [ ] `copyScreen()` - 화면 복사 (트랜잭션)
|
||||
- [ ] `applyTemplate()` - 템플릿 적용 (트랜잭션)
|
||||
- [ ] `bulkDelete()` - 일괄 삭제 (트랜잭션)
|
||||
- [ ] `reorderScreens()` - 순서 변경 (Raw Query)
|
||||
### 4단계: 복잡한 기능 (4개 함수) ✅ **완료**
|
||||
|
||||
### 5단계: 테스트 & 검증
|
||||
- [ ] 단위 테스트 작성 (20개 이상)
|
||||
- [ ] 통합 테스트 작성 (5개 이상)
|
||||
- [ ] 성능 테스트
|
||||
- [ ] Prisma import 제거 확인
|
||||
- [x] `copyScreen()` - 화면 복사 (트랜잭션)
|
||||
- [x] `generateScreenCode()` - 화면 코드 자동 생성
|
||||
- [x] `checkScreenDependencies()` - 화면 의존성 체크 (메뉴 할당 포함)
|
||||
- [x] 모든 유틸리티 메서드 Raw Query 전환
|
||||
|
||||
### 5단계: 테스트 & 검증 ✅ **완료**
|
||||
|
||||
- [x] 단위 테스트 작성 (18개 테스트 통과)
|
||||
- createScreen, updateScreen, deleteScreen
|
||||
- getScreensByCompany, getScreenById
|
||||
- saveLayout, getLayout
|
||||
- getTemplatesByCompany, assignScreenToMenu
|
||||
- copyScreen, generateScreenCode
|
||||
- getTableColumns
|
||||
- [x] 통합 테스트 작성 (6개 시나리오)
|
||||
- 화면 생명주기 테스트 (생성 → 조회 → 수정 → 삭제 → 복원 → 영구삭제)
|
||||
- 화면 복사 및 레이아웃 테스트
|
||||
- 테이블 정보 조회 테스트
|
||||
- 일괄 작업 테스트
|
||||
- 화면 코드 자동 생성 테스트
|
||||
- [x] Prisma import 완전 제거 확인
|
||||
- [ ] 성능 테스트 (추후 실행 예정)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 완료 기준
|
||||
|
||||
- ✅ 46개 Prisma 호출 모두 Raw Query로 전환
|
||||
- ✅ 모든 단위 테스트 통과
|
||||
- ✅ 모든 통합 테스트 통과
|
||||
- ✅ 성능 저하 없음 (기존 대비 ±10% 이내)
|
||||
- ✅ 트랜잭션 정상 동작 확인
|
||||
- ✅ 에러 처리 및 롤백 정상 동작
|
||||
- ✅ **46개 Prisma 호출 모두 Raw Query로 전환 완료**
|
||||
- ✅ **모든 TypeScript 컴파일 오류 해결**
|
||||
- ✅ **트랜잭션 정상 동작 확인**
|
||||
- ✅ **에러 처리 및 롤백 정상 동작**
|
||||
- ✅ **모든 단위 테스트 통과 (18개)**
|
||||
- ✅ **모든 통합 테스트 작성 완료 (6개 시나리오)**
|
||||
- ✅ **Prisma import 완전 제거**
|
||||
- [ ] 성능 저하 없음 (기존 대비 ±10% 이내) - 추후 측정 예정
|
||||
|
||||
## 📊 테스트 결과
|
||||
|
||||
### 단위 테스트 (18개)
|
||||
|
||||
```
|
||||
✅ createScreen - 화면 생성 (2개 테스트)
|
||||
✅ getScreensByCompany - 화면 목록 페이징 (2개 테스트)
|
||||
✅ updateScreen - 화면 업데이트 (2개 테스트)
|
||||
✅ deleteScreen - 화면 삭제 (2개 테스트)
|
||||
✅ saveLayout - 레이아웃 저장 (2개 테스트)
|
||||
- 기본 저장, 소수점 좌표 반올림 처리
|
||||
✅ getLayout - 레이아웃 조회 (1개 테스트)
|
||||
✅ getTemplatesByCompany - 템플릿 목록 (1개 테스트)
|
||||
✅ assignScreenToMenu - 메뉴 할당 (2개 테스트)
|
||||
✅ copyScreen - 화면 복사 (1개 테스트)
|
||||
✅ generateScreenCode - 화면 코드 자동 생성 (2개 테스트)
|
||||
✅ getTableColumns - 테이블 컬럼 정보 (1개 테스트)
|
||||
|
||||
Test Suites: 1 passed
|
||||
Tests: 18 passed
|
||||
Time: 1.922s
|
||||
```
|
||||
|
||||
### 통합 테스트 (6개 시나리오)
|
||||
|
||||
```
|
||||
✅ 화면 생명주기 테스트
|
||||
- 생성 → 조회 → 수정 → 삭제 → 복원 → 영구삭제
|
||||
✅ 화면 복사 및 레이아웃 테스트
|
||||
- 화면 복사 → 레이아웃 저장 → 레이아웃 확인 → 레이아웃 수정
|
||||
✅ 테이블 정보 조회 테스트
|
||||
- 테이블 목록 조회 → 특정 테이블 정보 조회
|
||||
✅ 일괄 작업 테스트
|
||||
- 여러 화면 생성 → 일괄 삭제
|
||||
✅ 화면 코드 자동 생성 테스트
|
||||
- 순차적 화면 코드 생성 검증
|
||||
✅ 메뉴 할당 테스트 (skip - 실제 메뉴 데이터 필요)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 버그 수정 및 개선사항
|
||||
|
||||
### 실제 운영 환경에서 발견된 이슈
|
||||
|
||||
#### 1. 소수점 좌표 저장 오류 (해결 완료)
|
||||
|
||||
**문제**:
|
||||
|
||||
```
|
||||
invalid input syntax for type integer: "1602.666666666667"
|
||||
```
|
||||
|
||||
- `position_x`, `position_y`, `width`, `height` 컬럼이 `integer` 타입
|
||||
- 격자 계산 시 소수점 값이 발생하여 저장 실패
|
||||
|
||||
**해결**:
|
||||
|
||||
```typescript
|
||||
Math.round(component.position.x), // 정수로 반올림
|
||||
Math.round(component.position.y),
|
||||
Math.round(component.size.width),
|
||||
Math.round(component.size.height),
|
||||
```
|
||||
|
||||
**테스트 추가**:
|
||||
|
||||
- 소수점 좌표 저장 테스트 케이스 추가
|
||||
- 반올림 처리 검증
|
||||
|
||||
**영향 범위**:
|
||||
|
||||
- `saveLayout()` 함수
|
||||
- `copyScreen()` 함수 (레이아웃 복사 시)
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-09-30
|
||||
**예상 소요 시간**: 2-3일
|
||||
**완료일**: 2025-09-30
|
||||
**예상 소요 시간**: 2-3일 → **실제 소요 시간**: 1일
|
||||
**담당자**: 백엔드 개발팀
|
||||
**우선순위**: 🔴 최우선 (Phase 2.1)
|
||||
**우선순위**: 🔴 최우선 (Phase 2.1)
|
||||
**상태**: ✅ **완료**
|
||||
|
|
|
|||
|
|
@ -127,16 +127,19 @@ export class ScreenManagementService {
|
|||
const total = parseInt(totalResult[0]?.count || "0", 10);
|
||||
|
||||
// 테이블 라벨 정보를 한 번에 조회 (Raw Query)
|
||||
const tableNames = [
|
||||
...new Set(screens.map((s: any) => s.table_name).filter(Boolean)),
|
||||
];
|
||||
const tableNames = Array.from(
|
||||
new Set(screens.map((s: any) => s.table_name).filter(Boolean))
|
||||
);
|
||||
|
||||
let tableLabelMap = new Map<string, string>();
|
||||
|
||||
if (tableNames.length > 0) {
|
||||
try {
|
||||
const placeholders = tableNames.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const tableLabels = await query<{ table_name: string; table_label: string | null }>(
|
||||
const tableLabels = await query<{
|
||||
table_name: string;
|
||||
table_label: string | null;
|
||||
}>(
|
||||
`SELECT table_name, table_label FROM table_labels
|
||||
WHERE table_name IN (${placeholders})`,
|
||||
tableNames
|
||||
|
|
@ -339,7 +342,9 @@ export class ScreenManagementService {
|
|||
const params: any[] = [];
|
||||
|
||||
if (userCompanyCode !== "*") {
|
||||
whereConditions.push(`sd.company_code IN ($${params.length + 1}, $${params.length + 2})`);
|
||||
whereConditions.push(
|
||||
`sd.company_code IN ($${params.length + 1}, $${params.length + 2})`
|
||||
);
|
||||
params.push(userCompanyCode, "*");
|
||||
}
|
||||
|
||||
|
|
@ -371,13 +376,9 @@ export class ScreenManagementService {
|
|||
if (screen.screen_id === screenId) continue; // 자기 자신은 제외
|
||||
|
||||
try {
|
||||
// screen_layouts 테이블에서 버튼 컴포넌트 확인
|
||||
const buttonLayouts = screen.layouts.filter(
|
||||
(layout) => layout.component_type === "widget"
|
||||
);
|
||||
|
||||
for (const layout of buttonLayouts) {
|
||||
const properties = layout.properties as any;
|
||||
// screen_layouts 테이블에서 버튼 컴포넌트 확인 (위젯 타입만)
|
||||
if (screen.component_type === "widget") {
|
||||
const properties = screen.properties as any;
|
||||
|
||||
// 버튼 컴포넌트인지 확인
|
||||
if (properties?.widgetType === "button") {
|
||||
|
|
@ -393,7 +394,7 @@ export class ScreenManagementService {
|
|||
screenId: screen.screen_id,
|
||||
screenName: screen.screen_name,
|
||||
screenCode: screen.screen_code,
|
||||
componentId: layout.component_id,
|
||||
componentId: screen.component_id,
|
||||
componentType: "button",
|
||||
referenceType: "popup",
|
||||
});
|
||||
|
|
@ -408,7 +409,7 @@ export class ScreenManagementService {
|
|||
screenId: screen.screen_id,
|
||||
screenName: screen.screen_name,
|
||||
screenCode: screen.screen_code,
|
||||
componentId: layout.component_id,
|
||||
componentId: screen.component_id,
|
||||
componentType: "button",
|
||||
referenceType: "navigate",
|
||||
});
|
||||
|
|
@ -423,7 +424,7 @@ export class ScreenManagementService {
|
|||
screenId: screen.screen_id,
|
||||
screenName: screen.screen_name,
|
||||
screenCode: screen.screen_code,
|
||||
componentId: layout.component_id,
|
||||
componentId: screen.component_id,
|
||||
componentType: "button",
|
||||
referenceType: "url",
|
||||
});
|
||||
|
|
@ -431,67 +432,8 @@ export class ScreenManagementService {
|
|||
}
|
||||
}
|
||||
|
||||
// 기존 layout_metadata도 확인 (하위 호환성)
|
||||
const layoutMetadata = screen.layout_metadata as any;
|
||||
if (layoutMetadata?.components) {
|
||||
const components = layoutMetadata.components;
|
||||
|
||||
for (const component of components) {
|
||||
// 버튼 컴포넌트인지 확인
|
||||
if (
|
||||
component.type === "widget" &&
|
||||
component.widgetType === "button"
|
||||
) {
|
||||
const config = component.webTypeConfig;
|
||||
if (!config) continue;
|
||||
|
||||
// popup 액션에서 targetScreenId 확인
|
||||
if (
|
||||
config.actionType === "popup" &&
|
||||
config.targetScreenId === screenId
|
||||
) {
|
||||
dependencies.push({
|
||||
screenId: screen.screen_id,
|
||||
screenName: screen.screen_name,
|
||||
screenCode: screen.screen_code,
|
||||
componentId: component.id,
|
||||
componentType: "button",
|
||||
referenceType: "popup",
|
||||
});
|
||||
}
|
||||
|
||||
// navigate 액션에서 targetScreenId 확인
|
||||
if (
|
||||
config.actionType === "navigate" &&
|
||||
config.targetScreenId === screenId
|
||||
) {
|
||||
dependencies.push({
|
||||
screenId: screen.screen_id,
|
||||
screenName: screen.screen_name,
|
||||
screenCode: screen.screen_code,
|
||||
componentId: component.id,
|
||||
componentType: "button",
|
||||
referenceType: "navigate",
|
||||
});
|
||||
}
|
||||
|
||||
// navigateUrl에서 화면 ID 패턴 확인 (예: /screens/123)
|
||||
if (
|
||||
config.navigateUrl &&
|
||||
config.navigateUrl.includes(`/screens/${screenId}`)
|
||||
) {
|
||||
dependencies.push({
|
||||
screenId: screen.screen_id,
|
||||
screenName: screen.screen_name,
|
||||
screenCode: screen.screen_code,
|
||||
componentId: component.id,
|
||||
componentType: "button",
|
||||
referenceType: "url",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 기존 layout_metadata도 확인 (하위 호환성) - 현재는 사용하지 않음
|
||||
// 실제 데이터는 screen_layouts 테이블에서 개별적으로 조회해야 함
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`화면 ${screen.screen_id}의 레이아웃 분석 중 오류:`,
|
||||
|
|
@ -501,31 +443,35 @@ export class ScreenManagementService {
|
|||
}
|
||||
}
|
||||
|
||||
// 메뉴 할당 확인
|
||||
// 메뉴에 할당된 화면인지 확인 (임시 주석 처리)
|
||||
/*
|
||||
const menuAssignments = await prisma.screen_menu_assignments.findMany({
|
||||
where: {
|
||||
screen_id: screenId,
|
||||
is_active: "Y",
|
||||
},
|
||||
include: {
|
||||
menu_info: true, // 메뉴 정보도 함께 조회
|
||||
},
|
||||
});
|
||||
// 메뉴 할당 확인 (Raw Query)
|
||||
try {
|
||||
const menuAssignments = await query<{
|
||||
assignment_id: number;
|
||||
menu_objid: number;
|
||||
menu_name_kor?: string;
|
||||
}>(
|
||||
`SELECT sma.assignment_id, sma.menu_objid, mi.menu_name_kor
|
||||
FROM screen_menu_assignments sma
|
||||
LEFT JOIN menu_info mi ON sma.menu_objid = mi.objid
|
||||
WHERE sma.screen_id = $1 AND sma.is_active = 'Y'`,
|
||||
[screenId]
|
||||
);
|
||||
|
||||
// 메뉴에 할당된 경우 의존성에 추가
|
||||
for (const assignment of menuAssignments) {
|
||||
dependencies.push({
|
||||
screenId: 0, // 메뉴는 화면이 아니므로 0으로 설정
|
||||
screenName: assignment.menu_info?.menu_name_kor || "알 수 없는 메뉴",
|
||||
screenCode: `MENU_${assignment.menu_objid}`,
|
||||
componentId: `menu_${assignment.assignment_id}`,
|
||||
componentType: "menu",
|
||||
referenceType: "menu_assignment",
|
||||
});
|
||||
// 메뉴에 할당된 경우 의존성에 추가
|
||||
for (const assignment of menuAssignments) {
|
||||
dependencies.push({
|
||||
screenId: 0, // 메뉴는 화면이 아니므로 0으로 설정
|
||||
screenName: assignment.menu_name_kor || "알 수 없는 메뉴",
|
||||
screenCode: `MENU_${assignment.menu_objid}`,
|
||||
componentId: `menu_${assignment.assignment_id}`,
|
||||
componentType: "menu",
|
||||
referenceType: "menu_assignment",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("메뉴 할당 확인 중 오류:", error);
|
||||
// 메뉴 할당 확인 실패해도 다른 의존성 체크는 계속 진행
|
||||
}
|
||||
*/
|
||||
|
||||
return {
|
||||
hasDependencies: dependencies.length > 0,
|
||||
|
|
@ -544,7 +490,10 @@ export class ScreenManagementService {
|
|||
force: boolean = false
|
||||
): Promise<void> {
|
||||
// 권한 확인 (Raw Query)
|
||||
const existingResult = await query<{ company_code: string | null; is_active: string }>(
|
||||
const existingResult = await query<{
|
||||
company_code: string | null;
|
||||
is_active: string;
|
||||
}>(
|
||||
`SELECT company_code, is_active FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId]
|
||||
);
|
||||
|
|
@ -593,7 +542,14 @@ export class ScreenManagementService {
|
|||
updated_date = $4,
|
||||
updated_by = $5
|
||||
WHERE screen_id = $6`,
|
||||
[new Date(), deletedBy, deleteReason || null, new Date(), deletedBy, screenId]
|
||||
[
|
||||
new Date(),
|
||||
deletedBy,
|
||||
deleteReason || null,
|
||||
new Date(),
|
||||
deletedBy,
|
||||
screenId,
|
||||
]
|
||||
);
|
||||
|
||||
// 메뉴 할당도 비활성화
|
||||
|
|
@ -607,7 +563,7 @@ export class ScreenManagementService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 화면 복원 (휴지통에서 복원)
|
||||
* 화면 복원 (휴지통에서 복원) (✅ Raw Query 전환 완료)
|
||||
*/
|
||||
async restoreScreen(
|
||||
screenId: number,
|
||||
|
|
@ -615,14 +571,21 @@ export class ScreenManagementService {
|
|||
restoredBy: string
|
||||
): Promise<void> {
|
||||
// 권한 확인
|
||||
const existingScreen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_id: screenId },
|
||||
});
|
||||
const screens = await query<{
|
||||
company_code: string | null;
|
||||
is_active: string;
|
||||
screen_code: string;
|
||||
}>(
|
||||
`SELECT company_code, is_active, screen_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId]
|
||||
);
|
||||
|
||||
if (!existingScreen) {
|
||||
if (screens.length === 0) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const existingScreen = screens[0];
|
||||
|
||||
if (
|
||||
userCompanyCode !== "*" &&
|
||||
existingScreen.company_code !== userCompanyCode
|
||||
|
|
@ -704,7 +667,10 @@ export class ScreenManagementService {
|
|||
userCompanyCode: string
|
||||
): Promise<void> {
|
||||
// 권한 확인
|
||||
const screens = await query<{ company_code: string | null; is_active: string }>(
|
||||
const screens = await query<{
|
||||
company_code: string | null;
|
||||
is_active: string;
|
||||
}>(
|
||||
`SELECT company_code, is_active FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId]
|
||||
);
|
||||
|
|
@ -729,9 +695,17 @@ export class ScreenManagementService {
|
|||
|
||||
// 물리적 삭제 (수동으로 관련 데이터 삭제)
|
||||
await transaction(async (client) => {
|
||||
await client.query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);
|
||||
await client.query(`DELETE FROM screen_menu_assignments WHERE screen_id = $1`, [screenId]);
|
||||
await client.query(`DELETE FROM screen_definitions WHERE screen_id = $1`, [screenId]);
|
||||
await client.query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [
|
||||
screenId,
|
||||
]);
|
||||
await client.query(
|
||||
`DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
|
||||
[screenId]
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM screen_definitions WHERE screen_id = $1`,
|
||||
[screenId]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -779,21 +753,27 @@ export class ScreenManagementService {
|
|||
const total = parseInt(totalResult[0]?.count || "0", 10);
|
||||
|
||||
// 테이블 라벨 정보를 한 번에 조회
|
||||
const tableNames = [
|
||||
...new Set(screens.map((s: any) => s.table_name).filter(Boolean)),
|
||||
];
|
||||
const tableNames = Array.from(
|
||||
new Set(screens.map((s: any) => s.table_name).filter(Boolean))
|
||||
);
|
||||
|
||||
let tableLabelMap = new Map<string, string>();
|
||||
|
||||
if (tableNames.length > 0) {
|
||||
const placeholders = tableNames.map((_, i) => `$${i + 1}`).join(", ");
|
||||
const tableLabels = await query<{ table_name: string; table_label: string | null }>(
|
||||
const tableLabels = await query<{
|
||||
table_name: string;
|
||||
table_label: string | null;
|
||||
}>(
|
||||
`SELECT table_name, table_label FROM table_labels WHERE table_name IN (${placeholders})`,
|
||||
tableNames
|
||||
);
|
||||
|
||||
tableLabelMap = new Map(
|
||||
tableLabels.map((tl: any) => [tl.table_name, tl.table_label || tl.table_name])
|
||||
tableLabels.map((tl: any) => [
|
||||
tl.table_name,
|
||||
tl.table_label || tl.table_name,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -918,18 +898,19 @@ export class ScreenManagementService {
|
|||
// ========================================
|
||||
|
||||
/**
|
||||
* 테이블 목록 조회 (모든 테이블)
|
||||
* 테이블 목록 조회 (모든 테이블) (✅ Raw Query 전환 완료)
|
||||
*/
|
||||
async getTables(companyCode: string): Promise<TableInfo[]> {
|
||||
try {
|
||||
// PostgreSQL에서 사용 가능한 테이블 목록 조회
|
||||
const tables = await prisma.$queryRaw<Array<{ table_name: string }>>`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name
|
||||
`;
|
||||
const tables = await query<{ table_name: string }>(
|
||||
`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name`,
|
||||
[]
|
||||
);
|
||||
|
||||
// 각 테이블의 컬럼 정보도 함께 조회
|
||||
const tableInfos: TableInfo[] = [];
|
||||
|
|
@ -966,13 +947,14 @@ export class ScreenManagementService {
|
|||
console.log(`=== 단일 테이블 조회 시작: ${tableName} ===`);
|
||||
|
||||
// 테이블 존재 여부 확인
|
||||
const tableExists = await prisma.$queryRaw<Array<{ table_name: string }>>`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
AND table_name = ${tableName}
|
||||
`;
|
||||
const tableExists = await query<{ table_name: string }>(
|
||||
`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
AND table_name = $1`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
if (tableExists.length === 0) {
|
||||
console.log(`테이블 ${tableName}이 존재하지 않습니다.`);
|
||||
|
|
@ -1004,7 +986,7 @@ export class ScreenManagementService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 정보 조회
|
||||
* 테이블 컬럼 정보 조회 (✅ Raw Query 전환 완료)
|
||||
*/
|
||||
async getTableColumns(
|
||||
tableName: string,
|
||||
|
|
@ -1012,18 +994,16 @@ export class ScreenManagementService {
|
|||
): Promise<ColumnInfo[]> {
|
||||
try {
|
||||
// 테이블 컬럼 정보 조회
|
||||
const columns = await prisma.$queryRaw<
|
||||
Array<{
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
is_nullable: string;
|
||||
column_default: string | null;
|
||||
character_maximum_length: number | null;
|
||||
numeric_precision: number | null;
|
||||
numeric_scale: number | null;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
const columns = await query<{
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
is_nullable: string;
|
||||
column_default: string | null;
|
||||
character_maximum_length: number | null;
|
||||
numeric_precision: number | null;
|
||||
numeric_scale: number | null;
|
||||
}>(
|
||||
`SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
|
|
@ -1031,25 +1011,28 @@ export class ScreenManagementService {
|
|||
character_maximum_length,
|
||||
numeric_precision,
|
||||
numeric_scale
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = ${tableName}
|
||||
ORDER BY ordinal_position
|
||||
`;
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = $1
|
||||
ORDER BY ordinal_position`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
// column_labels 테이블에서 웹타입 정보 조회 (있는 경우)
|
||||
const webTypeInfo = await prisma.column_labels.findMany({
|
||||
where: { table_name: tableName },
|
||||
select: {
|
||||
column_name: true,
|
||||
web_type: true,
|
||||
column_label: true,
|
||||
detail_settings: true,
|
||||
},
|
||||
});
|
||||
const webTypeInfo = await query<{
|
||||
column_name: string;
|
||||
web_type: string | null;
|
||||
column_label: string | null;
|
||||
detail_settings: any;
|
||||
}>(
|
||||
`SELECT column_name, web_type, column_label, detail_settings
|
||||
FROM column_labels
|
||||
WHERE table_name = $1`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
// 컬럼 정보 매핑
|
||||
return columns.map((column) => {
|
||||
return columns.map((column: any) => {
|
||||
const webTypeData = webTypeInfo.find(
|
||||
(wt) => wt.column_name === column.column_name
|
||||
);
|
||||
|
|
@ -1189,10 +1172,7 @@ export class ScreenManagementService {
|
|||
}
|
||||
|
||||
// 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두)
|
||||
await query(
|
||||
`DELETE FROM screen_layouts WHERE screen_id = $1`,
|
||||
[screenId]
|
||||
);
|
||||
await query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);
|
||||
|
||||
// 1. 메타데이터 저장 (격자 설정과 해상도 정보)
|
||||
if (layoutData.gridSettings || layoutData.screenResolution) {
|
||||
|
|
@ -1260,10 +1240,10 @@ export class ScreenManagementService {
|
|||
component.type,
|
||||
component.id,
|
||||
component.parentId || null,
|
||||
component.position.x,
|
||||
component.position.y,
|
||||
component.size.width,
|
||||
component.size.height,
|
||||
Math.round(component.position.x), // 정수로 반올림
|
||||
Math.round(component.position.y), // 정수로 반올림
|
||||
Math.round(component.size.width), // 정수로 반올림
|
||||
Math.round(component.size.height), // 정수로 반올림
|
||||
JSON.stringify(properties),
|
||||
]
|
||||
);
|
||||
|
|
@ -1414,7 +1394,10 @@ export class ScreenManagementService {
|
|||
params.push(isPublic);
|
||||
}
|
||||
|
||||
const whereSQL = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
|
||||
const whereSQL =
|
||||
whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
const templates = await query<any>(
|
||||
`SELECT * FROM screen_templates
|
||||
|
|
@ -1531,11 +1514,11 @@ export class ScreenManagementService {
|
|||
// ========================================
|
||||
|
||||
/**
|
||||
* 컬럼 정보 조회 (웹 타입 포함)
|
||||
* 컬럼 정보 조회 (웹 타입 포함) (✅ Raw Query 전환 완료)
|
||||
*/
|
||||
async getColumnInfo(tableName: string): Promise<ColumnInfo[]> {
|
||||
const columns = await prisma.$queryRaw`
|
||||
SELECT
|
||||
const columns = await query<any>(
|
||||
`SELECT
|
||||
c.column_name,
|
||||
COALESCE(cl.column_label, c.column_name) as column_label,
|
||||
c.data_type,
|
||||
|
|
@ -1553,18 +1536,19 @@ export class ScreenManagementService {
|
|||
cl.is_visible,
|
||||
cl.display_order,
|
||||
cl.description
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN column_labels cl ON c.table_name = cl.table_name
|
||||
AND c.column_name = cl.column_name
|
||||
WHERE c.table_name = ${tableName}
|
||||
ORDER BY COALESCE(cl.display_order, c.ordinal_position)
|
||||
`;
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN column_labels cl ON c.table_name = cl.table_name
|
||||
AND c.column_name = cl.column_name
|
||||
WHERE c.table_name = $1
|
||||
ORDER BY COALESCE(cl.display_order, c.ordinal_position)`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return columns as ColumnInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹 타입 설정
|
||||
* 웹 타입 설정 (✅ Raw Query 전환 완료)
|
||||
*/
|
||||
async setColumnWebType(
|
||||
tableName: string,
|
||||
|
|
@ -1572,44 +1556,45 @@ export class ScreenManagementService {
|
|||
webType: WebType,
|
||||
additionalSettings?: Partial<ColumnWebTypeSetting>
|
||||
): Promise<void> {
|
||||
await prisma.column_labels.upsert({
|
||||
where: {
|
||||
table_name_column_name: {
|
||||
table_name: tableName,
|
||||
column_name: columnName,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
web_type: webType,
|
||||
column_label: additionalSettings?.columnLabel,
|
||||
detail_settings: additionalSettings?.detailSettings
|
||||
// UPSERT를 INSERT ... ON CONFLICT로 변환
|
||||
await query(
|
||||
`INSERT INTO column_labels (
|
||||
table_name, column_name, column_label, web_type, detail_settings,
|
||||
code_category, reference_table, reference_column, display_column,
|
||||
is_visible, display_order, description, created_date, updated_date
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
ON CONFLICT (table_name, column_name)
|
||||
DO UPDATE SET
|
||||
web_type = $4,
|
||||
column_label = $3,
|
||||
detail_settings = $5,
|
||||
code_category = $6,
|
||||
reference_table = $7,
|
||||
reference_column = $8,
|
||||
display_column = $9,
|
||||
is_visible = $10,
|
||||
display_order = $11,
|
||||
description = $12,
|
||||
updated_date = $14`,
|
||||
[
|
||||
tableName,
|
||||
columnName,
|
||||
additionalSettings?.columnLabel || null,
|
||||
webType,
|
||||
additionalSettings?.detailSettings
|
||||
? JSON.stringify(additionalSettings.detailSettings)
|
||||
: null,
|
||||
code_category: additionalSettings?.codeCategory,
|
||||
reference_table: additionalSettings?.referenceTable,
|
||||
reference_column: additionalSettings?.referenceColumn,
|
||||
is_visible: additionalSettings?.isVisible ?? true,
|
||||
display_order: additionalSettings?.displayOrder ?? 0,
|
||||
description: additionalSettings?.description,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
create: {
|
||||
table_name: tableName,
|
||||
column_name: columnName,
|
||||
column_label: additionalSettings?.columnLabel,
|
||||
web_type: webType,
|
||||
detail_settings: additionalSettings?.detailSettings
|
||||
? JSON.stringify(additionalSettings.detailSettings)
|
||||
: null,
|
||||
code_category: additionalSettings?.codeCategory,
|
||||
reference_table: additionalSettings?.referenceTable,
|
||||
reference_column: additionalSettings?.referenceColumn,
|
||||
is_visible: additionalSettings?.isVisible ?? true,
|
||||
display_order: additionalSettings?.displayOrder ?? 0,
|
||||
description: additionalSettings?.description,
|
||||
created_date: new Date(),
|
||||
},
|
||||
});
|
||||
additionalSettings?.codeCategory || null,
|
||||
additionalSettings?.referenceTable || null,
|
||||
additionalSettings?.referenceColumn || null,
|
||||
(additionalSettings as any)?.displayColumn || null,
|
||||
additionalSettings?.isVisible ?? true,
|
||||
additionalSettings?.displayOrder ?? 0,
|
||||
additionalSettings?.description || null,
|
||||
new Date(),
|
||||
new Date(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1767,20 +1752,16 @@ export class ScreenManagementService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 화면 코드 자동 생성 (회사코드 + '_' + 순번)
|
||||
* 화면 코드 자동 생성 (회사코드 + '_' + 순번) (✅ Raw Query 전환 완료)
|
||||
*/
|
||||
async generateScreenCode(companyCode: string): Promise<string> {
|
||||
// 해당 회사의 기존 화면 코드들 조회
|
||||
const existingScreens = await prisma.screen_definitions.findMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
screen_code: {
|
||||
startsWith: companyCode,
|
||||
},
|
||||
},
|
||||
select: { screen_code: true },
|
||||
orderBy: { screen_code: "desc" },
|
||||
});
|
||||
// 해당 회사의 기존 화면 코드들 조회 (Raw Query)
|
||||
const existingScreens = await query<{ screen_code: string }>(
|
||||
`SELECT screen_code FROM screen_definitions
|
||||
WHERE company_code = $1 AND screen_code LIKE $2
|
||||
ORDER BY screen_code DESC`,
|
||||
[companyCode, `${companyCode}%`]
|
||||
);
|
||||
|
||||
// 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기
|
||||
let maxNumber = 0;
|
||||
|
|
@ -1902,11 +1883,11 @@ export class ScreenManagementService {
|
|||
sourceLayout.component_type,
|
||||
newComponentId,
|
||||
newParentId,
|
||||
sourceLayout.position_x,
|
||||
sourceLayout.position_y,
|
||||
sourceLayout.width,
|
||||
sourceLayout.height,
|
||||
typeof sourceLayout.properties === 'string'
|
||||
Math.round(sourceLayout.position_x), // 정수로 반올림
|
||||
Math.round(sourceLayout.position_y), // 정수로 반올림
|
||||
Math.round(sourceLayout.width), // 정수로 반올림
|
||||
Math.round(sourceLayout.height), // 정수로 반올림
|
||||
typeof sourceLayout.properties === "string"
|
||||
? sourceLayout.properties
|
||||
: JSON.stringify(sourceLayout.properties),
|
||||
sourceLayout.display_order,
|
||||
|
|
|
|||
Loading…
Reference in New Issue