feat: 프리뷰 모드에서 회사 코드 오버라이드 기능 추가

- 최고 관리자만 다른 회사 코드로 오버라이드 가능하도록 로직 개선
- entityJoinController 및 tableManagementController에서 회사 코드 오버라이드 처리 추가
- 관련 API 호출 시 오버라이드된 회사 코드 적용
- 프리뷰 모드 감지 및 UI 개선을 위한 코드 추가
This commit is contained in:
DDD1542 2026-01-13 13:28:50 +09:00
parent 0773989c74
commit 905a9f62c3
16 changed files with 3328 additions and 297 deletions

View File

@ -1043,6 +1043,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@ -2370,6 +2371,7 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@ -3463,6 +3465,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
"integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@ -3699,6 +3702,7 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@ -3916,6 +3920,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -4442,6 +4447,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@ -5652,6 +5658,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@ -7414,6 +7421,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@ -8383,7 +8391,6 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -9272,6 +9279,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@ -10122,7 +10130,6 @@
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@ -10931,6 +10938,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@ -11036,6 +11044,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -66,11 +66,23 @@ export class EntityJoinController {
const userField = parsedAutoFilter.userField || "companyCode";
const userValue = ((req as any).user as any)[userField];
if (userValue) {
searchConditions[filterColumn] = userValue;
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 허용)
let finalCompanyCode = userValue;
if (parsedAutoFilter.companyCodeOverride && userValue === "*") {
// 최고 관리자만 다른 회사 코드로 오버라이드 가능
finalCompanyCode = parsedAutoFilter.companyCodeOverride;
logger.info("🔓 최고 관리자 회사 코드 오버라이드:", {
originalCompanyCode: userValue,
overrideCompanyCode: parsedAutoFilter.companyCodeOverride,
tableName,
});
}
if (finalCompanyCode) {
searchConditions[filterColumn] = finalCompanyCode;
logger.info("🔒 Entity 조인에 멀티테넌시 필터 적용:", {
filterColumn,
userValue,
finalCompanyCode,
tableName,
});
}

View File

@ -775,13 +775,25 @@ export async function getTableData(
const userField = autoFilter?.userField || "companyCode";
const userValue = (req.user as any)[userField];
if (userValue) {
enhancedSearch[filterColumn] = userValue;
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 허용)
let finalCompanyCode = userValue;
if (autoFilter?.companyCodeOverride && userValue === "*") {
// 최고 관리자만 다른 회사 코드로 오버라이드 가능
finalCompanyCode = autoFilter.companyCodeOverride;
logger.info("🔓 최고 관리자 회사 코드 오버라이드:", {
originalCompanyCode: userValue,
overrideCompanyCode: autoFilter.companyCodeOverride,
tableName,
});
}
if (finalCompanyCode) {
enhancedSearch[filterColumn] = finalCompanyCode;
logger.info("🔍 현재 사용자 필터 적용:", {
filterColumn,
userField,
userValue,
userValue: finalCompanyCode,
tableName,
});
} else {

View File

@ -1682,6 +1682,61 @@ frontend/
---
## 화면 설정 모달 개선 (2026-01-12)
### 개요
화면 노드 우클릭 시 열리는 설정 모달을 대폭 개선했습니다.
### 주요 변경 사항
#### 1. 테이블 정보 시각화 개선
| 항목 | 변경 내용 |
|------|----------|
| 메인 테이블 | 아코디언 형식으로 모든 컬럼 표시 |
| 필터 테이블 | 아코디언 형식 + 필터/조인 키 색상 구분 |
| 사용 중 컬럼 | 파란색 배경 + "필드" 배지로 강조 |
#### 2. 화면 프리뷰 상시 표시
- 모달 레이아웃: 좌측 40% (탭) / 우측 60% (프리뷰)
- 탭 전환해도 프리뷰 항상 표시
#### 3. 줌/드래그 기능 (react-zoom-pan-pinch 라이브러리)
```bash
npm install react-zoom-pan-pinch
```
| 기능 | 동작 |
|------|------|
| 휠 스크롤 | 마우스 포인터 기준 확대/축소 (20%~300%) |
| 드래그 | 화면 이동 |
| 클릭 | iframe 내부 버튼/목록 상호작용 |
#### 4. 프리뷰 company_code 전달 문제 해결
| 문제 | 해결 |
|------|------|
| 최고 관리자로 다른 회사 프리뷰 불가 | `companyCodeOverride` 파라미터 도입 |
| URL 파라미터 무시됨 | 백엔드에서 admin 전용 오버라이드 처리 |
### 관련 파일
| 파일 | 변경 내용 |
|------|----------|
| `ScreenSettingModal.tsx` | 전체 UI 개선, 줌/드래그 기능 |
| `entityJoin.ts` | `companyCodeOverride` 파라미터 추가 |
| `SplitPanelLayoutComponent.tsx` | `companyCode` prop 추가 |
| `entityJoinController.ts` | `companyCodeOverride` 처리 로직 |
### 상세 문서
- [화면설정모달_개선_완료_보고서.md](./화면설정모달_개선_완료_보고서.md)
---
## 관련 문서
- [멀티테넌시 구현 가이드](.cursor/rules/multi-tenancy-guide.mdc)

View File

@ -0,0 +1,535 @@
# 화면 설정 모달 개선 완료 보고서
## 개요
화면 관리에서 화면 노드 우클릭 시 열리는 설정 모달을 대폭 개선하여, 테이블 정보 시각화, 필드 매핑 확인, 컬럼 변경/추가/제거 기능, 조인 설정 기능, 실시간 프리뷰 기능을 강화했습니다.
## 주요 개선 사항
### 1. 화면 개요 탭 통합 개선
#### 1.1 필드 매핑 탭 → 개요 탭 통합
- 기존 "필드 매핑" 탭 제거
- 필드 매핑 정보를 개요 탭의 메인/필터 테이블 아코디언에 통합 표시
- 더 직관적이고 간결한 UI 제공
#### 1.2 메인 테이블 아코디언
- 메인 테이블(예: `customer_mng`)을 아코디언 형식으로 표시
- 클릭 시 테이블의 모든 컬럼 정보 표시
- **1열 레이아웃**: 컬럼 정보를 세로로 배치
- 화면에서 사용 중인 컬럼은 **파란색 배경 + "필드" 배지**로 강조
- **컬럼 정렬**:
- 사용중인 필드가 상단에 표시
- 화면에 표시되는 순서대로 정렬 (y좌표 기준)
- 미사용 컬럼은 하단에 표시
#### 1.3 필터 테이블 아코디언
- 필터 테이블(예: `customer_item_mapping`)을 아코디언 형식으로 표시
- 클릭 시 테이블의 모든 컬럼 정보 표시
- 컬럼별 색상 구분:
- **파란색**: 화면에서 사용 중인 컬럼 (필드)
- **보라색**: 필터 키 컬럼 (WHERE 절에 사용)
- **주황색**: 조인 키 컬럼 (JOIN 조건에 사용)
- **다중 배지 표시**: 컬럼이 필드이면서 조인/필터 키인 경우 배지 동시 표시
- 필터 연결 정보 표시 (예: `→ customer_mng`)
#### 1.4 컬럼 레이아웃 순서
- **순서**: `컬럼명 | 배지 | 데이터타입`
- 예: `거래처 코드` `[필드]` `character varying`
- 데이터타입은 오른쪽 정렬
#### 1.5 클릭 스타일 개선
- **테두리 제거**: ring-2, ring-offset 등 제거
- **강조 색상 연하게**:
- 선택됨: `bg-blue-100 border-blue-300`
- 미선택: `bg-blue-50 border-blue-200`
- 더 부드러운 시각적 피드백
#### 1.6 패널 높이 동기화
- 왼쪽(컬럼 목록)과 오른쪽(설정 패널) 동일한 `max-h-[350px]` 적용
- `overflow-y-auto`로 스크롤 처리
- `items-stretch`로 양쪽 패널 높이 동기화
### 2. 컬럼 변경 기능
#### 2.1 인라인 컬럼 편집
- 사용중인 필드(파란색 배경)를 클릭하면 우측에 "컬럼 설정" 패널 표시
- 패널 정보:
- **화면 필드**: 컬럼 한글명 표시 (예: "거래처 코드")
- **현재 컬럼**: 영문 컬럼명 표시 (예: `customer_code`)
- **컬럼 변경**: 드롭다운으로 다른 컬럼 선택
- 검색 기능으로 컬럼 빠르게 찾기
#### 2.2 실시간 반영
- 컬럼 변경 후 **페이지 새로고침 없이** 실시간 반영
- `onRefresh` 콜백으로 데이터 리로드 + iframe 새로고침
- 더 빠른 사용자 경험
#### 2.3 변경사항 저장
- `screenApi.saveLayout()` 사용하여 **화면 디자이너와 동일한 테이블에 저장**
- 저장 위치:
- `componentConfig.leftPanel.columns` (분할 패널)
- `componentConfig.rightPanel.columns` (분할 패널)
- `usedColumns` 배열
- `bindField` 필드
- `fieldMapping` 배열
### 3. 필드 추가/제거 기능 (신규)
#### 3.1 필드 추가
- 비필드 컬럼(회색/흰색 배경) 클릭
- "컬럼 설정" 패널에 컬럼 정보 표시
- **"필드로 추가"** 버튼 클릭 → 해당 컬럼이 화면 필드로 추가됨
- 버튼 스타일: `text-blue-600 border-blue-300 hover:bg-blue-50` (파란색 테두리)
#### 3.2 필드 제거
- 기존 필드(파란색 배경) 클릭
- "컬럼 설정" 패널에 필드 정보 표시
- **"필드에서 제거"** 버튼 클릭 → 해당 필드가 화면에서 제거됨
- 버튼 스타일: `text-red-600 border-red-300 hover:bg-red-50` (빨간색 테두리)
#### 3.3 저장 로직
```typescript
// 필드 추가: 배열에 새 컬럼 추가
if (isAddingField) {
return {
...comp,
componentConfig: {
...comp.componentConfig,
leftPanel: {
...comp.componentConfig.leftPanel,
columns: [...leftColumns, { name: newColumn, columnName: newColumn }],
},
},
};
}
// 필드 제거: 배열에서 해당 컬럼 제거
if (isRemovingField) {
const filteredColumns = leftColumns.filter((_, i) => i !== columnIdx);
return {
...comp,
componentConfig: {
...comp.componentConfig,
leftPanel: {
...comp.componentConfig.leftPanel,
columns: filteredColumns,
},
},
};
}
```
#### 3.4 적용 범위
- 메인 테이블 아코디언: 필드 추가/제거 가능
- 필터 테이블 아코디언: 필드 추가/제거 가능
- `usedColumns`, `componentConfig.usedColumns`, `componentConfig.columns`, `leftPanel.columns`, `rightPanel.columns` 모두 지원
### 4. 화면 프리뷰 상시 표시
#### 4.1 레이아웃 변경
- 기존: 탭으로 프리뷰 전환
- 개선: **모달 우측에 프리뷰 상시 표시**
- 모달 크기 확대 (1600px 최대 너비)
- 좌측 40% (탭 콘텐츠) / 우측 60% (프리뷰)
#### 4.2 줌/드래그/클릭 기능 (react-zoom-pan-pinch 라이브러리)
- **휠 스크롤**: 마우스 포인터 위치 기준 확대/축소 (20% ~ 300%)
- **드래그**: 마우스 왼쪽 버튼으로 화면 이동 (5px 이상 이동 시)
- **클릭**: iframe 내부 요소 정상 클릭 가능
- 버튼, 셀렉트박스, 체크박스, 테이블 행 클릭
- 인풋박스/텍스트박스 포커스 및 입력
- X버튼(닫기) 등 SVG 아이콘 버튼 클릭
#### 4.3 클릭 좌표 보정 시스템
- 줌 상태에서도 정확한 클릭 위치 계산
- `designWidth / rect.width` 비율로 좌표 변환
- 오버레이 방식으로 드래그와 클릭 분리 처리
### 5. 필드+조인 컬럼 스타일 개선
#### 5.1 다중 역할 컬럼 표시
- 컬럼이 **필드이면서 조인 키**인 경우:
- **파란색 배경** (필드 기준)
- **왼쪽에 주황색 세로 선** (`border-l-4 border-l-orange-500`)
- 배지: `조인` `필드` 동시 표시
- 컬럼이 **필드이면서 필터 키**인 경우:
- **파란색 배경** (필드 기준)
- **왼쪽에 보라색 세로 선** (`border-l-4 border-l-purple-400`)
- 배지: `필터` `필드` 동시 표시
#### 5.2 조인 컬럼도 필드로 인식
- `filterTableColumnMappings` 생성 시 조인 컬럼(`ft.joinColumnRefs`)도 포함
- 조인 테이블 데이터를 화면에서 보여주므로 필드로 간주
#### 5.3 컬럼 설정 패널 - 조인 정보 표시
- 조인 키 클릭 시 패널에 조인 정보 표시:
- **대상 테이블**: item_info (실제 참조 테이블)
- **연결 컬럼**: item_number (참조 컬럼)
### 6. 조인 관계 설정/수정 기능
#### 6.1 기능 설명
- 컬럼 설정 패널에서 **조인 관계 직접 수정** 가능
- **모든 컬럼에서 조인 설정 가능** (기존 조인 키가 아닌 컬럼도 포함)
- 테이블 타입 관리(`column_labels` 테이블)와 동일한 저장 위치 사용
#### 6.2 저장 테이블
```
column_labels 테이블:
├── reference_table (참조 테이블명)
├── reference_column (참조 컬럼 - 보통 PK)
└── display_column (화면에 표시할 컬럼)
```
#### 6.3 구현된 UI
1. **컬럼 클릭** → 컬럼 설정 패널 표시
2. **"조인" 섹션 확인**:
- 조인 설정 있음: "편집" 버튼
- 조인 설정 없음: "추가" 버튼
3. **드롭다운으로 설정** (모두 검색 가능):
- 대상 테이블: 전체 테이블 목록에서 검색/선택
- 연결 컬럼 (PK): 선택한 테이블의 컬럼 중 검색/선택
- 표시 컬럼: 화면에 표시할 컬럼 검색/선택
4. **저장 버튼**`column_labels` 테이블에 저장
5. **취소 버튼** → 편집 취소
#### 6.4 검색 가능한 드롭다운
- Popover + Command 컴포넌트 사용
- 실시간 텍스트 검색 지원
- 대상 테이블, 연결 컬럼, 표시 컬럼 모두 검색 가능
#### 6.5 API 연동
- **테이블 목록 조회**: `tableManagementApi.getTableList()`
- **컬럼 목록 조회**: `tableManagementApi.getColumnList(tableName)`
- **저장**: `tableManagementApi.updateColumnSettings(tableName, columnName, settings)`
#### 6.6 메인 테이블에도 조인 설정 적용
- 메인 테이블 아코디언에서도 조인 설정 가능
- 필터 테이블과 동일한 UI/기능 제공
#### 6.7 조인 데이터 소스 수정
- 기존 조인 키 클릭 시 `joinRef.refTable` 값을 사용
- 예: `품목 ID``item_info` (실제 참조 테이블)
- `mainTable` 대신 `joinRef.refTable` 사용으로 정확한 테이블 표시
### 7. 배지 순서 및 스타일
- **배지 순서**: `필터``조인``필드` (필드가 맨 뒤)
- **조인 배지**: 주황색 배경 (`bg-orange-200 text-orange-700`)
- **필터 배지**: 보라색 배경 (`bg-purple-200 text-purple-700`)
- **필드 배지**: 파란색 배경 (`bg-blue-500 text-white`)
### 8. 컴포넌트 통합 리팩토링
#### 8.1 `TableColumnAccordion` 통합 컴포넌트
- 기존 `MainTableAccordion``FilterTableAccordion`을 하나의 컴포넌트로 통합
- `tableType` prop으로 "main" 또는 "filter" 구분
- 코드 중복 제거 및 유지보수성 향상
#### 8.2 Props 구조
```typescript
interface TableColumnAccordionProps {
tableName: string;
tableLabel?: string;
tableType: "main" | "filter";
columnMappings?: ColumnMapping[];
onColumnChange?: (fieldLabel: string, oldColumn: string, newColumn: string) => void;
onColumnReorder?: (newOrder: string[]) => void;
onJoinSettingSaved?: () => void;
// 필터 테이블 전용 props
mainTable?: string;
filterKeyMapping?: FilterKeyMapping;
joinColumnRefs?: JoinColumnRef[];
}
```
#### 8.3 동적 테마 적용
- 메인 테이블: 파란색 테마 (`blue`)
- 필터 테이블: 보라색 테마 (`purple`)
- `themeColor`, `themeIcon`, `themeBadge` 변수로 동적 스타일 적용
### 9. 드래그 앤 드롭 컬럼 순서 변경
#### 9.1 기능 설명
- 사용 중인 컬럼(필드)을 드래그하여 순서 변경 가능
- 드래그 중에는 시각적으로만 순서 변경, **드롭 시에만 저장**
- 드래그 취소(영역 밖으로 나간 경우) 시 원래 순서로 복원
#### 9.2 드래그 상태 관리
```typescript
// 드래그 상태
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [localColumnOrder, setLocalColumnOrder] = useState<string[] | null>(null);
```
#### 9.3 드래그 핸들러
```typescript
// 드래그 시작: 현재 순서를 로컬 상태로 저장
const handleDragStart = (e: React.DragEvent, index: number) => {
setDraggedIndex(index);
const usedColumns = sortedColumns.filter(col => columnMappingMap.has(col.columnName.toLowerCase()));
setLocalColumnOrder(usedColumns.map(col => col.columnName));
};
// 드래그 중: 로컬 순서만 변경 (저장하지 않음)
const handleDragOver = (e: React.DragEvent, hoverIndex: number) => {
if (draggedIndex === null || draggedIndex === hoverIndex || !localColumnOrder) return;
const newOrder = [...localColumnOrder];
const draggedItem = newOrder[draggedIndex];
newOrder.splice(draggedIndex, 1);
newOrder.splice(hoverIndex, 0, draggedItem);
setDraggedIndex(hoverIndex);
setLocalColumnOrder(newOrder);
};
// 드롭: 최종 순서로 저장
const handleDrop = (e: React.DragEvent) => {
if (localColumnOrder && onColumnReorder) {
onColumnReorder(localColumnOrder);
}
setDraggedIndex(null);
setLocalColumnOrder(null);
};
// 드래그 취소
const handleDragEnd = () => {
setDraggedIndex(null);
setLocalColumnOrder(null);
};
```
#### 9.4 시각적 피드백
- 드래그 가능한 컬럼: `cursor-grab active:cursor-grabbing`
- 드래그 중인 컬럼: `opacity-50 scale-95`
- 드래그 중 실시간 순서 변경 표시
#### 9.5 저장 로직 (`handleColumnReorder`)
```typescript
const handleColumnReorder = async (tableType: "main" | "filter", newOrder: string[]) => {
const currentLayout = await screenApi.getLayout(screenId);
const updatedComponents = currentLayout.components.map((comp: any) => {
// leftPanel.columns 순서 변경
if (comp.componentConfig?.leftPanel?.columns) {
const leftColumns = comp.componentConfig.leftPanel.columns;
const reorderedColumns = newOrder.map(colName =>
leftColumns.find((col: any) => col.name?.toLowerCase() === colName.toLowerCase())
).filter(Boolean);
const remainingColumns = leftColumns.filter((col: any) =>
!newOrder.some(n => n.toLowerCase() === col.name?.toLowerCase())
);
return {
...comp,
componentConfig: {
...comp.componentConfig,
leftPanel: {
...comp.componentConfig.leftPanel,
columns: [...reorderedColumns, ...remainingColumns],
},
},
};
}
return comp;
});
await screenApi.saveLayout(screenId, { ...currentLayout, components: updatedComponents });
onRefresh?.();
};
```
#### 9.6 지원 범위
- 메인 테이블: `onColumnReorder={(newOrder) => handleColumnReorder("main", newOrder)}`
- 필터 테이블: `onColumnReorder={(newOrder) => handleColumnReorder("filter", newOrder)}`
- 지원 배열:
- `componentConfig.leftPanel.columns`
- `componentConfig.rightPanel.columns`
- `componentConfig.usedColumns`
- `componentConfig.columns`
## 기술 스택
### 신규 의존성
```bash
npm install react-zoom-pan-pinch
```
### 사용된 컴포넌트
- `TransformWrapper`, `TransformComponent` - 줌/드래그 기능
- `Accordion`, `AccordionContent`, `AccordionItem`, `AccordionTrigger` - 아코디언 UI
- `Popover`, `PopoverTrigger`, `PopoverContent` - 드롭다운 컨테이너
- `Command`, `CommandInput`, `CommandList`, `CommandItem`, `CommandEmpty` - 검색 가능한 선택 UI
- `tableManagementApi.getColumnList()` - 테이블 컬럼 정보 조회
- `tableManagementApi.getTableList()` - 테이블 목록 조회
- `tableManagementApi.updateColumnSettings()` - 조인 설정 저장
- `screenApi.saveLayout()` - 레이아웃 저장
- `screenApi.getLayout()` - 레이아웃 조회
### 핵심 로직
#### 컬럼 변경/추가/제거
```typescript
const handleColumnChange = async (fieldLabel: string, oldColumn: string, newColumn: string) => {
const isAddingField = fieldLabel === "__NEW_FIELD__";
const isRemovingField = newColumn === "__REMOVE_FIELD__";
const currentLayout = await screenApi.getLayout(screenId);
const updatedComponents = currentLayout.components.map((comp: any) => {
if (comp.componentConfig?.leftPanel?.columns) {
const leftColumns = comp.componentConfig.leftPanel.columns;
// 필드 추가
if (isAddingField) {
return {
...comp,
componentConfig: {
...comp.componentConfig,
leftPanel: {
...comp.componentConfig.leftPanel,
columns: [...leftColumns, { name: newColumn, columnName: newColumn }],
},
},
};
}
// 필드 제거
const columnIdx = leftColumns.findIndex((col: any) => ...);
if (columnIdx !== -1 && isRemovingField) {
return {
...comp,
componentConfig: {
...comp.componentConfig,
leftPanel: {
...comp.componentConfig.leftPanel,
columns: leftColumns.filter((_, i) => i !== columnIdx),
},
},
};
}
// 컬럼 변경
if (columnIdx !== -1) {
return {
...comp,
componentConfig: {
...comp.componentConfig,
leftPanel: {
...comp.componentConfig.leftPanel,
columns: leftColumns.map((col, i) =>
i === columnIdx ? { ...col, name: newColumn } : col
),
},
},
};
}
}
return comp;
});
await screenApi.saveLayout(screenId, { ...currentLayout, components: updatedComponents });
onRefresh?.();
};
```
#### 조인 설정 편집기 (JoinSettingEditor)
```tsx
<JoinSettingEditor
editingJoin={editingJoin}
setEditingJoin={setEditingJoin}
allTables={allTables}
refTableColumns={refTableColumns}
loadingRefColumns={loadingRefColumns}
savingJoinSetting={savingJoinSetting}
loadRefTableColumns={loadRefTableColumns}
handleSaveJoinSetting={handleSaveJoinSetting}
/>
```
## 파일 변경 목록
| 파일 | 변경 내용 |
|------|----------|
| `frontend/components/screen/ScreenSettingModal.tsx` | 전체 UI 개선, 줌/드래그 기능, 컬럼 변경/추가/제거 기능, 조인 설정 기능, 필드 매핑 통합, 실시간 반영 |
| `frontend/components/screen/ScreenRelationFlow.tsx` | `filterKeyMapping`, `joinColumnRefs` 데이터 전달 |
| `frontend/lib/api/entityJoin.ts` | `companyCodeOverride` 파라미터 추가 |
| `frontend/lib/api/screen.ts` | `saveLayout`, `getLayout` API 사용 |
| `frontend/lib/api/tableManagement.ts` | `getTableList`, `getColumnList`, `updateColumnSettings` API |
| `frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx` | `companyCode` prop 추가 |
| `backend-node/src/controllers/entityJoinController.ts` | `companyCodeOverride` 처리 로직 추가 |
## 사용 방법
### 화면 설정 모달 열기
1. 화면 관리 페이지에서 화면 그룹 선택
2. 화면 노드 우클릭 → 컨텍스트 메뉴 표시
3. "화면 설정" 선택 → 모달 열림
4. 좌측 탭에서 정보 확인/수정, 우측에서 실시간 프리뷰
### 프리뷰 영역 조작
- **휠 스크롤**: 확대/축소 (5% 단위)
- **마우스 드래그**: 화면 이동 (5px 이상 움직여야 드래그로 인식)
- **짧은 클릭**: iframe 내부 요소 클릭
### 컬럼 변경
1. 메인/필터 테이블 아코디언 펼치기
2. 파란색 배경의 "필드" 컬럼 클릭
3. 우측 "컬럼 설정" 패널 확인
4. "컬럼 변경" 드롭다운에서 새 컬럼 선택
5. **실시간 반영** (페이지 새로고침 없음)
### 필드 추가
1. 메인/필터 테이블 아코디언 펼치기
2. 회색/흰색 배경의 비필드 컬럼 클릭
3. 우측 패널에서 **"필드로 추가"** 버튼 클릭
4. 해당 컬럼이 화면 필드로 추가됨
### 필드 제거
1. 메인/필터 테이블 아코디언 펼치기
2. 파란색 배경의 필드 컬럼 클릭
3. 우측 패널에서 **"필드에서 제거"** 버튼 클릭
4. 해당 필드가 화면에서 제거됨
### 조인 설정 추가/편집
1. 메인/필터 테이블 아코디언 펼치기
2. 아무 컬럼 클릭 (조인 키가 아니어도 됨)
3. 우측 패널의 "조인" 섹션에서:
- 조인 없음: **"추가"** 버튼 클릭
- 조인 있음: **"편집"** 버튼 클릭
4. 대상 테이블 선택 (검색 가능)
5. 연결 컬럼 (PK) 선택 (검색 가능)
6. 표시 컬럼 선택 (검색 가능)
7. **"저장"** 버튼 클릭
### 컬럼 순서 변경 (드래그 앤 드롭)
1. 메인/필터 테이블 아코디언 펼치기
2. 파란색 배경의 "필드" 컬럼을 드래그 시작
3. 원하는 위치로 드래그하여 이동 (실시간으로 순서 변경 표시)
4. 마우스를 놓으면 (드롭) 순서가 저장됨
5. 드래그 취소하려면 컬럼 영역 밖으로 드래그
**참고:**
- 사용 중인 필드만 드래그 가능 (파란색 배경)
- 미사용 컬럼은 드래그 불가
- 드래그 중에는 저장되지 않고, 드롭 시에만 저장됨
---
## 완료일
2026-01-13
## 변경 이력
- 2026-01-12: 최초 작성 (줌/드래그/클릭, company_code 전달)
- 2026-01-12: 컬럼 변경 기능 추가, 필드 매핑 통합, UI 개선 (1열 레이아웃, 배지 변경)
- 2026-01-12: 실시간 반영 구현 (reload 제거), 레이아웃 순서 변경, 스타일 개선
- 2026-01-12: 필드+조인 컬럼 스타일 개선 (파란배경 + 왼쪽 주황선), 조인 정보 패널 표시
- 2026-01-12: 조인 관계 설정/수정 기능 구현 완료 (column_labels 테이블 저장)
- 2026-01-13: 필드 추가/제거 기능 구현
- 2026-01-13: 검색 가능한 조인 설정 드롭다운 (Command 컴포넌트)
- 2026-01-13: 모든 컬럼에서 조인 설정 가능 (범용성 패치)
- 2026-01-13: 메인 테이블에도 조인 설정 기능 추가
- 2026-01-13: 조인 라인 색상 주황색으로 변경 (`border-l-orange-500`)
- 2026-01-13: 조인 데이터 소스 수정 (`joinRef.refTable` 사용)
- 2026-01-13: 패널 높이 동기화 (`max-h-[350px]`, `items-stretch`)
- 2026-01-13: `MainTableAccordion``FilterTableAccordion``TableColumnAccordion`으로 통합
- 2026-01-13: 드래그 앤 드롭 컬럼 순서 변경 기능 구현
- 2026-01-13: 드래그 중에는 로컬 상태만 변경, 드롭 시에만 저장하도록 최적화

View File

@ -36,6 +36,9 @@ function ScreenViewPage() {
// URL 쿼리에서 프리뷰용 company_code 가져오기
const previewCompanyCode = searchParams.get("company_code");
// 프리뷰 모드 감지 (iframe에서 로드될 때)
const isPreviewMode = searchParams.get("preview") === "true";
// 🆕 현재 로그인한 사용자 정보
const { user, userName, companyCode: authCompanyCode } = useAuth();
@ -239,27 +242,40 @@ function ScreenViewPage() {
const designWidth = layout?.screenResolution?.width || 1200;
const designHeight = layout?.screenResolution?.height || 800;
// 컨테이너의 실제 크기
const containerWidth = containerRef.current.offsetWidth;
const containerHeight = containerRef.current.offsetHeight;
// 컨테이너의 실제 크기 (프리뷰 모드에서는 window 크기 사용)
let containerWidth: number;
let containerHeight: number;
// 여백 설정: 좌우 16px씩 (총 32px), 상단 패딩 32px (pt-8)
const MARGIN_X = 32;
const availableWidth = containerWidth - MARGIN_X;
if (isPreviewMode) {
// iframe에서는 window 크기를 직접 사용
containerWidth = window.innerWidth;
containerHeight = window.innerHeight;
} else {
containerWidth = containerRef.current.offsetWidth;
containerHeight = containerRef.current.offsetHeight;
}
// 가로 기준 스케일 계산 (좌우 여백 16px씩 고정)
const newScale = availableWidth / designWidth;
let newScale: number;
if (isPreviewMode) {
// 프리뷰 모드: 가로/세로 모두 fit하도록 (여백 없이)
const scaleX = containerWidth / designWidth;
const scaleY = containerHeight / designHeight;
newScale = Math.min(scaleX, scaleY, 1); // 최대 1배율
} else {
// 일반 모드: 가로 기준 스케일 (좌우 여백 16px씩 고정)
const MARGIN_X = 32;
const availableWidth = containerWidth - MARGIN_X;
newScale = availableWidth / designWidth;
}
// console.log("📐 스케일 계산:", {
// containerWidth,
// containerHeight,
// MARGIN_X,
// availableWidth,
// designWidth,
// designHeight,
// finalScale: newScale,
// "스케일된 화면 크기": `${designWidth * newScale}px × ${designHeight * newScale}px`,
// "실제 좌우 여백": `${(containerWidth - designWidth * newScale) / 2}px씩`,
// isPreviewMode,
// });
setScale(newScale);
@ -278,7 +294,7 @@ function ScreenViewPage() {
return () => {
clearTimeout(timer);
};
}, [layout, isMobile]);
}, [layout, isMobile, isPreviewMode]);
if (loading) {
return (
@ -316,7 +332,7 @@ function ScreenViewPage() {
<ScreenPreviewProvider isPreviewMode={false}>
<ActiveTabProvider>
<TableOptionsProvider>
<div ref={containerRef} className="bg-background h-full w-full overflow-auto p-3">
<div ref={containerRef} className={`bg-background h-full w-full ${isPreviewMode ? "overflow-hidden p-0" : "overflow-auto p-3"}`}>
{/* 레이아웃 준비 중 로딩 표시 */}
{!layoutReady && (
<div className="from-muted to-muted/50 flex h-full w-full items-center justify-center bg-gradient-to-br">

View File

@ -1107,10 +1107,37 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
);
const tableNodeData = tableNode?.data as TableNodeData | undefined;
// 필터 키 매핑 정보 추출 (leftColumn → foreignKey)
let filterKeyMapping: {
mainTableColumn: string;
mainTableColumnLabel?: string;
filterTableColumn: string;
filterTableColumnLabel?: string;
} | undefined = undefined;
if (subTableData?.leftColumn && subTableData?.foreignKey) {
// 메인 테이블 컬럼 한글명 조회
const mainTable = subTablesDataMap[screenId]?.mainTable;
const mainTableCols = mainTable ? tableColumns[mainTable] : [];
const mainColInfo = mainTableCols?.find(c => c.columnName === subTableData.leftColumn);
// 필터 테이블 컬럼 한글명 조회
const filterTableCols = tableColumns[tableName] || [];
const filterColInfo = filterTableCols?.find(c => c.columnName === subTableData.foreignKey);
filterKeyMapping = {
mainTableColumn: subTableData.leftColumn,
mainTableColumnLabel: mainColInfo?.displayName,
filterTableColumn: subTableData.foreignKey,
filterTableColumnLabel: filterColInfo?.displayName,
};
}
return {
tableName,
tableLabel: subTableData?.tableLabel || tableNodeData?.label || tableName,
filterColumns: subTableData?.filterColumns || tableNodeData?.filterColumns || [],
filterKeyMapping,
joinColumnRefs: subTableData?.joinColumnRefs || tableNodeData?.joinColumnRefs || [],
};
});

File diff suppressed because it is too large Load Diff

View File

@ -459,3 +459,5 @@ export default function DataFlowPanel({ groupId, screenId, screens = [] }: DataF

View File

@ -411,3 +411,5 @@ export default function FieldJoinPanel({ screenId, componentId, layoutId }: Fiel

View File

@ -77,15 +77,26 @@ export const entityJoinApi = {
filterColumn?: string;
filterValue?: any;
}; // 🆕 제외 필터 (다른 테이블에 이미 존재하는 데이터 제외)
companyCodeOverride?: string; // 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 사용 가능)
} = {},
): Promise<EntityJoinResponse> => {
// 🔒 멀티테넌시: company_code 자동 필터링 활성화
const autoFilter = {
const autoFilter: {
enabled: boolean;
filterColumn: string;
userField: string;
companyCodeOverride?: string;
} = {
enabled: true,
filterColumn: "company_code",
userField: "companyCode",
};
// 🆕 프리뷰 모드에서 회사 코드 오버라이드 (최고 관리자만 백엔드에서 허용)
if (params.companyCodeOverride) {
autoFilter.companyCodeOverride = params.companyCodeOverride;
}
const response = await apiClient.get(`/table-management/tables/${tableName}/data-with-joins`, {
params: {
page: params.page,
@ -96,7 +107,7 @@ export const entityJoinApi = {
search: params.search ? JSON.stringify(params.search) : undefined,
additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined,
screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정
autoFilter: JSON.stringify(autoFilter), // 🔒 멀티테넌시 필터링
autoFilter: JSON.stringify(autoFilter), // 🔒 멀티테넌시 필터링 (오버라이드 포함)
dataFilter: params.dataFilter ? JSON.stringify(params.dataFilter) : undefined, // 🆕 데이터 필터
excludeFilter: params.excludeFilter ? JSON.stringify(params.excludeFilter) : undefined, // 🆕 제외 필터
},

View File

@ -55,6 +55,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
...props
}) => {
const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig;
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 사용 가능)
const companyCode = (props as any).companyCode as string | undefined;
// 기본 설정값
const splitRatio = componentConfig.splitRatio || 30;
@ -766,6 +768,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
enableEntityJoin: true, // 엔티티 조인 활성화
dataFilter: componentConfig.leftPanel?.dataFilter, // 🆕 데이터 필터 전달
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 🆕 추가 조인 컬럼
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
});
// 🔍 디버깅: API 응답 데이터의 키 확인
@ -828,6 +831,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
search: { id: primaryKey },
enableEntityJoin: true, // 엔티티 조인 활성화
size: 1,
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
});
const detail = result.items && result.items.length > 0 ? result.items[0] : null;
@ -885,6 +889,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
search: searchConditions,
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
});
if (result.data) {
allResults.push(...result.data);
@ -919,6 +924,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
search: searchConditions,
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
});
console.log("🔗 [분할패널] 복합키 조회 결과:", result);

View File

@ -211,6 +211,8 @@ export interface TableListComponentProps {
// 탭 관련 정보 (탭 내부의 테이블에서 사용)
parentTabId?: string; // 부모 탭 ID
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
// 🆕 프리뷰용 회사 코드 (DynamicComponentRenderer에서 전달, 최고 관리자만 오버라이드 가능)
companyCode?: string;
}
// ========================================
@ -238,6 +240,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
screenId,
parentTabId,
parentTabsComponentId,
companyCode,
}) => {
// ========================================
// 설정 및 스타일
@ -1780,6 +1783,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 전달
dataFilter: tableConfig.dataFilter, // 🆕 데이터 필터 전달
excludeFilter: excludeFilterParam, // 🆕 제외 필터 전달
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만)
});
// 실제 데이터의 item_number만 추출하여 중복 확인
@ -1850,6 +1854,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 🆕 RelatedDataButtons 필터 추가
relatedButtonFilter,
isRelatedButtonTarget,
// 🆕 프리뷰용 회사 코드 오버라이드
companyCode,
]);
const fetchTableDataDebounced = useCallback(

View File

@ -80,6 +80,7 @@
"react-resizable-panels": "^3.0.6",
"react-webcam": "^7.2.0",
"react-window": "^2.1.0",
"react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4",
"recharts": "^3.2.1",
"sheetjs-style": "^0.15.8",
@ -255,6 +256,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -296,6 +298,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@ -329,6 +332,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@ -2590,6 +2594,7 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz",
"integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.32.0",
@ -3243,6 +3248,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz",
"integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.90.6"
},
@ -3310,6 +3316,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz",
"integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@ -3623,6 +3630,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz",
"integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
@ -6123,6 +6131,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -6133,6 +6142,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -6166,6 +6176,7 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
@ -6248,6 +6259,7 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2",
@ -6880,6 +6892,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -7809,7 +7822,8 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/d3": {
"version": "7.9.0",
@ -8131,6 +8145,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@ -8866,6 +8881,7 @@
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -8954,6 +8970,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@ -9055,6 +9072,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -10089,6 +10107,7 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@ -10847,7 +10866,8 @@
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/levn": {
"version": "0.4.1",
@ -12033,6 +12053,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -12328,6 +12349,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT",
"peer": true,
"dependencies": {
"orderedmap": "^2.0.0"
}
@ -12357,6 +12379,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
@ -12405,6 +12428,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
"license": "MIT",
"peer": true,
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
@ -12531,6 +12555,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -12600,6 +12625,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -12618,6 +12644,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@ -12796,6 +12823,20 @@
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-zoom-pan-pinch": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz",
"integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==",
"license": "MIT",
"engines": {
"node": ">=8",
"npm": ">=5"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/reactflow": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
@ -12901,6 +12942,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@ -12923,7 +12965,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/recharts/node_modules/redux-thunk": {
"version": "3.1.0",
@ -13904,7 +13947,8 @@
"version": "0.180.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/three-mesh-bvh": {
"version": "0.8.3",
@ -13992,6 +14036,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -14322,6 +14367,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -88,6 +88,7 @@
"react-resizable-panels": "^3.0.6",
"react-webcam": "^7.2.0",
"react-window": "^2.1.0",
"react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4",
"recharts": "^3.2.1",
"sheetjs-style": "^0.15.8",