feat(pop): 뷰어 스크롤 수정 및 컴포넌트 레지스트리 연동

- page.tsx: overflow-hidden 제거로 뷰어 스크롤 활성화,
  pop-components 레지스트리 자동 등록 import 추가
- PopRenderer.tsx: 레지스트리에서 실제 컴포넌트 조회 후 렌더링,
  미등록 컴포넌트는 플레이스홀더 fallback 표시
- PLAN.MD: POP 뷰어 스크롤 수정 계획으로 업데이트
- POPUPDATE_2.md: v8.0 - 모달 화면 설계 규칙(제9조) 추가,
  버튼 modal action 스펙 확장 (inline/screen-ref 모드)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SeongHyun Kim 2026-02-09 19:31:46 +09:00
parent a017c1352a
commit f825d65bfc
4 changed files with 418 additions and 139 deletions

259
PLAN.MD
View File

@ -1,139 +1,156 @@
# 프로젝트: V2/V2 컴포넌트 설정 스키마 정비 # 현재 구현 계획: POP 뷰어 스크롤 수정
## 개요 > **작성일**: 2026-02-09
> **상태**: 계획 완료, 코딩 대기
레거시 컴포넌트를 제거하고, V2/V2 컴포넌트 전용 Zod 스키마와 기본값 레지스트리를 한 곳에서 관리한다. > **목적**: 뷰어에서 화면 높이를 초과하는 컴포넌트가 잘리지 않고 스크롤 가능하도록 수정
## 핵심 기능
1. [x] 레거시 컴포넌트 스키마 제거
2. [x] V2 컴포넌트 overrides 스키마 정의 (16개)
3. [x] V2 컴포넌트 overrides 스키마 정의 (9개)
4. [x] componentConfig.ts 한 파일에서 통합 관리
## 정의된 V2 컴포넌트 (18개)
- v2-table-list, v2-button-primary, v2-text-display
- v2-split-panel-layout, v2-section-card, v2-section-paper
- v2-divider-line, v2-repeat-container, v2-rack-structure
- v2-numbering-rule, v2-category-manager, v2-pivot-grid
- v2-location-swap-selector, v2-aggregation-widget
- v2-card-display, v2-table-search-widget, v2-tabs-widget
- v2-v2-repeater
## 정의된 V2 컴포넌트 (9개)
- v2-input, v2-select, v2-date
- v2-list, v2-layout, v2-group
- v2-media, v2-biz, v2-hierarchy
## 테스트 계획
### 1단계: 기본 기능
- [x] V2 레이아웃 저장 시 컴포넌트별 overrides 스키마 검증 통과
- [x] V2 컴포넌트 기본값과 스키마가 매칭됨
### 2단계: 에러 케이스
- [x] 잘못된 overrides 입력 시 Zod 검증 실패 처리 (safeParse + console.warn + graceful fallback)
- [x] 누락된 기본값 컴포넌트 저장 시 안전한 기본값 적용 (레지스트리 조회 → 빈 객체)
## 에러 처리 계획
- 스키마 파싱 실패 시 로그/에러 메시지 표준화
- 기본값 누락 시 안전한 fallback 적용
## 진행 상태
- [x] 레거시 컴포넌트 제거 완료
- [x] V2/V2 스키마 정의 완료
- [x] 한 파일 통합 관리 완료
# 프로젝트: 화면 복제 기능 개선 (DB 구조 개편 후)
## 개요
채번/카테고리에서 `menu_objid` 의존성 제거 완료 후, 화면 복제 기능을 새 DB 구조에 맞게 수정하고 테스트합니다.
## 핵심 변경사항
### DB 구조 변경 (완료)
- 채번규칙: `menu_objid` 의존성 제거 → `table_name + column_name + company_code` 기반
- 카테고리: `menu_objid` 의존성 제거 → `table_name + column_name + company_code` 기반
- 복제 순서 의존성 문제 해결
### 복제 옵션 정리 (완료)
- [x] **삭제**: 코드 카테고리 + 코드 복사 옵션
- [x] **삭제**: 연쇄관계 설정 복사 옵션
- [x] **이름 변경**: "카테고리 매핑 + 값 복사" → "카테고리 값 복사"
### 현재 복제 옵션 (3개)
1. **채번 규칙 복사** - 채번규칙 복제
2. **카테고리 값 복사** - 카테고리 값 복제 (table_column_category_values)
3. **테이블 타입관리 입력타입 설정 복사** - table_type_columns 복제
--- ---
## 테스트 계획 ## 1. 문제 요약
### 1. 화면 간 연결 복제 테스트 설계(디자이너)에서 컴포넌트를 아래로 배치하면 캔버스가 늘어나고 스크롤이 되지만,
뷰어(`/pop/screens/4114`)에서는 화면 높이를 초과하는 컴포넌트가 잘려서 안 보임.
- [ ] 수주관리 1번→2번→3번→4번 화면 연결 상태에서 복제 **근본 원인**: CSS 컨테이너 구조가 스크롤을 차단
- [ ] 복제 후 연결 관계가 유지되는지 확인
- [ ] 각 화면의 고유 키값이 새로운 화면을 참조하도록 변경되는지 확인
### 2. 제어관리 복제 테스트 | # | 컨테이너 (라인) | 현재 클래스 | 문제 |
|---|----------------|-------------|------|
- [ ] 다른 회사로 제어관리 복제 | 1 | 최외곽 (185) | `h-screen ... overflow-hidden` | 넘치는 콘텐츠를 잘라냄 |
- [ ] 복제된 플로우 스텝/연결이 정상 작동하는지 확인 | 2 | 컨텐츠 영역 (266) | 일반 모드에 `overflow-auto` 없음 | 스크롤 불가 |
| 3 | 백색 배경 (275) | 일반 모드에 `min-h-full` 없음 | 짧은 콘텐츠 시 배경 불완전 |
### 3. 추가 옵션 복제 테스트
- [ ] 채번규칙 복사 정상 작동 확인
- [ ] 카테고리 값 복사 정상 작동 확인
- [ ] 테이블 타입관리 입력타입 설정 복사 정상 작동 확인
### 4. 기본 복제 테스트
- [ ] 단일 화면 복제 (모달 포함)
- [ ] 그룹 전체 복제 (재귀적)
- [ ] 메뉴 동기화 정상 작동
--- ---
## 관련 파일 ## 2. 수정 대상 파일 (1개)
- `frontend/components/screen/CopyScreenModal.tsx` - 복제 모달 ### `frontend/app/(pop)/pop/screens/[screenId]/page.tsx`
- `frontend/components/screen/ScreenGroupTreeView.tsx` - 트리 뷰 + 컨텍스트 메뉴
- `backend-node/src/services/screenManagementService.ts` - 복제 서비스
- `backend-node/src/services/numberingRuleService.ts` - 채번규칙 서비스
- `docs/DB_STRUCTURE_DIAGRAM.md` - DB 구조 문서
## 진행 상태 **변경 유형**: CSS 클래스 문자열 수정 3곳 (새 변수/함수/타입 추가 없음)
#### 변경 1: 라인 185 - 최외곽 컨테이너
**현재 코드**:
```
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
```
**변경 코드**:
```
<div className="h-screen bg-gray-100 flex flex-col">
```
**변경 내용**: `overflow-hidden` 제거
**이유**: 이 div는 프리뷰 툴바 + 컨텐츠의 flex 컨테이너 역할만 하면 됨. `overflow-hidden`이 자식의 스크롤까지 차단하므로 제거
#### 변경 2: 라인 266 - 컨텐츠 영역
**현재 코드**:
```
<div className={`flex-1 flex flex-col ${isPreviewMode ? "py-4 overflow-auto items-center" : ""}`}>
```
**변경 코드**:
```
<div className={`flex-1 flex flex-col overflow-auto ${isPreviewMode ? "py-4 items-center" : ""}`}>
```
**변경 내용**: `overflow-auto`를 조건문 밖으로 이동 (공통 적용)
**이유**: 프리뷰/일반 모드 모두 스크롤이 필요함
#### 변경 3: 라인 275 - 백색 배경 컨테이너
**현재 코드**:
```
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full"}`}
```
**변경 코드**:
```
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full min-h-full"}`}
```
**변경 내용**: 일반 모드에 `min-h-full` 추가
**이유**: 컴포넌트가 적어 콘텐츠가 짧을 때에도 흰색 배경이 화면 전체를 채우도록 보장
---
## 3. 구현 순서 (의존성 기반)
| 순서 | 작업 | 라인 | 의존성 | 상태 |
|------|------|------|--------|------|
| 1 | 라인 185: `overflow-hidden` 제거 | 185 | 없음 | [x] 완료 |
| 2 | 라인 266: `overflow-auto` 공통 적용 | 266 | 순서 1 | [x] 완료 |
| 3 | 라인 275: 일반 모드 `min-h-full` 추가 | 275 | 순서 2 | [x] 완료 |
| 4 | 린트 검사 | - | 순서 1~3 | [x] 통과 |
| 5 | 브라우저 검증 | - | 순서 4 | [ ] 대기 |
---
## 4. 사전 충돌 검사 결과
**새로 추가할 변수/함수/타입: 없음**
이번 수정은 기존 Tailwind CSS 클래스 문자열만 변경합니다.
새로운 식별자(변수, 함수, 타입)를 추가하지 않으므로 충돌 검사 대상이 없습니다.
---
## 5. 에러 함정 경고
### 함정 1: 순서 1만 하고 순서 2를 빼먹으면
`overflow-hidden`만 제거하면 콘텐츠가 화면 밖으로 넘쳐 보이지만 스크롤은 안 됨.
부모는 열었지만 자식에 스크롤 속성이 없는 상태.
### 함정 2: 순서 2만 하고 순서 1을 빼먹으면
자식에 `overflow-auto`를 넣어도 부모가 `overflow-hidden`으로 잘라내므로 여전히 스크롤 안 됨.
**반드시 순서 1과 2를 함께 적용해야 함.**
### 함정 3: 프리뷰 모드 영향
프리뷰 모드는 이미 자체적으로 `overflow-auto`가 있으므로 이 수정에 영향 없음.
`overflow-auto`가 중복 적용되어도 CSS에서 문제 없음.
---
## 6. 검증 방법
1. `localhost:9771/pop/screens/4114` 접속 (iPhone SE 375px 기준)
2. 화면 아래로 스크롤 가능한지 확인
3. 맨 아래에 이미지(pop-text 5, 6)가 보이는지 확인
4. 프리뷰 모드(`?preview=true`)에서도 기존처럼 정상 동작하는지 확인
5. 컴포넌트가 적은 화면에서 흰색 배경이 화면 전체를 채우는지 확인
---
## 이전 완료 계획 (아카이브)
<details>
<summary>POP 뷰어 실제 컴포넌트 렌더링 (완료)</summary>
- [x] 뷰어 페이지에 레지스트리 초기화 import 추가
- [x] `renderActualComponent()` 실제 컴포넌트 렌더링으로 교체
- [x] 린트 검사 통과
- 브라우저 검증: 컴포넌트 표시 정상, 스크롤 문제 발견 -> 별도 수정
</details>
<details>
<summary>V2/V2 컴포넌트 설정 스키마 정비 (완료)</summary>
- [x] 레거시 컴포넌트 스키마 제거
- [x] V2 컴포넌트 overrides 스키마 정의 (16개)
- [x] V2 컴포넌트 overrides 스키마 정의 (9개)
- [x] componentConfig.ts 한 파일에서 통합 관리
</details>
<details>
<summary>화면 복제 기능 개선 (진행 중)</summary>
- [완료] DB 구조 개편 (menu_objid 의존성 제거) - [완료] DB 구조 개편 (menu_objid 의존성 제거)
- [완료] 복제 옵션 정리 (코드카테고리/연쇄관계 삭제, 이름 변경) - [완료] 복제 옵션 정리
- [완료] 화면 간 연결 복제 버그 수정 (targetScreenId 매핑 추가) - [완료] 화면 간 연결 복제 버그 수정
- [대기] 화면 간 연결 복제 테스트 - [대기] 화면 간 연결 복제 테스트
- [대기] 제어관리 복제 테스트 - [대기] 제어관리 복제 테스트
- [대기] 추가 옵션 복제 테스트 - [대기] 추가 옵션 복제 테스트
--- </details>
## 수정 이력
### 2026-01-26: 버튼 targetScreenId 매핑 버그 수정
**문제**: 그룹 복제 시 버튼의 `targetScreenId`가 새 화면으로 매핑되지 않음
- 수주관리 1→2→3→4 화면 복제 시 연결이 깨지는 문제
**수정 파일**: `backend-node/src/services/screenManagementService.ts`
- `updateTabScreenReferences` 함수에 `targetScreenId` 처리 로직 추가
- 쿼리에 `targetScreenId` 검색 조건 추가
- 문자열/숫자 타입 모두 처리

View File

@ -1,4 +1,4 @@
# POP 컴포넌트 정의서 v7.0 # POP 컴포넌트 정의서 v8.0
## POP 헌법 (공통 규칙) ## POP 헌법 (공통 규칙)
@ -50,6 +50,14 @@
- 모든 컴포넌트는 레지스트리에 등록해야 디자이너에 나타난다 - 모든 컴포넌트는 레지스트리에 등록해야 디자이너에 나타난다
- 모든 컴포넌트 인스턴스는 userConfigurable, displayName 공통 속성을 가진다 - 모든 컴포넌트 인스턴스는 userConfigurable, displayName 공통 속성을 가진다
### 제9조. 모달 화면의 설계
- 모달은 인라인(컴포넌트 설정만으로 구성)과 외부 참조(별도 POP 화면 연결) 두 가지 방식이 있다
- 단순한 목록 선택은 인라인 모달을 사용한다 (설정만으로 완결)
- 복잡한 검색/필터가 필요하거나 여러 곳에서 재사용하는 모달은 별도 POP 화면을 만들어 참조한다
- 모달 안의 화면도 동일한 POP 컴포넌트 시스템으로 구성된다 (같은 그리드, 같은 컴포넌트)
- 모달 화면의 layout_data는 기존 screen_layouts_pop 테이블에 저장한다 (DB 변경 불필요)
--- ---
## 현재 상태 ## 현재 상태
@ -203,7 +211,12 @@ DataSourceConfig를 받아서 기존 API를 호출하고 결과를 반환:
- `type`: "navigate" | "modal" | "save" | "delete" | "api" | "event" | "refresh" - `type`: "navigate" | "modal" | "save" | "delete" | "api" | "event" | "refresh"
- `navigate`: { screenId, url } - `navigate`: { screenId, url }
- `modal`: { title, dataSource } - `modal`: { mode, title, screenId, inlineConfig, modalSize }
- mode: "inline" (설정만으로 구성) | "screen-ref" (별도 화면 참조)
- title: 모달 제목
- screenId: mode가 "screen-ref"일 때 참조할 POP 화면 ID
- inlineConfig: mode가 "inline"일 때 사용할 DataSourceConfig + 표시 설정
- modalSize: { width, height } 모달 크기
- `save`: { targetColumns } - `save`: { targetColumns }
- `delete`: { confirmMessage } - `delete`: { confirmMessage }
- `api`: { method, endpoint, body } - `api`: { method, endpoint, body }
@ -224,22 +237,97 @@ DataSourceConfig를 받아서 기존 API를 호출하고 결과를 반환:
- **이벤트**: 발행 없음, 수신 없음 - **이벤트**: 발행 없음, 수신 없음
- **설정**: 내용, 폰트 크기/굵기, 좌우/상하 정렬, 이미지 URL/맞춤/크기, 날짜 포맷 빌더 - **설정**: 내용, 폰트 크기/굵기, 좌우/상하 정렬, 이미지 URL/맞춤/크기, 날짜 포맷 빌더
### 2. pop-dashboard (신규) ### 2. pop-dashboard (신규 - 2026-02-09 토의 결과 반영)
- **한 줄 정의**: 숫자를 집계해서 보여줌 - **한 줄 정의**: 여러 집계 아이템을 묶어서 다양한 방식으로 보여줌
- **카테고리**: display - **카테고리**: display
- **역할**: 숫자 데이터를 집계/계산하여 시각화 - **역할**: 숫자 데이터를 집계/계산하여 시각화. 하나의 컴포넌트 안에 여러 집계 아이템을 담는 컨테이너
- **서브타입**: - **구조**: 1개 pop-dashboard = 여러 DashboardItem의 묶음. 각 아이템은 독립적으로 데이터 소스/서브타입/보이기숨기기 설정 가능
- **서브타입** (아이템별로 선택, 한 묶음에 혼합 가능):
- kpi-card: 숫자 + 단위 + 라벨 + 증감 표시 - kpi-card: 숫자 + 단위 + 라벨 + 증감 표시
- chart: 막대/원형/라인 차트 - chart: 막대/원형/라인 차트
- gauge: 게이지 (목표 대비 달성률) - gauge: 게이지 (목표 대비 달성률)
- stat-card: 통계 카드 (건수 + 대기 + 링크) - stat-card: 통계 카드 (건수 + 대기 + 링크)
- **데이터**: DataSourceConfig (조인/집계 자유) - **표시 모드** (디자이너가 선택):
- arrows: 좌우 버튼으로 아이템 넘기기
- auto-slide: 전광판처럼 자동 전환 (터치 시 멈춤, 일정 시간 후 재개)
- grid: 컴포넌트 영역 내부를 행/열로 쪼개서 여러 아이템 동시 표시 (디자이너가 각 아이템 위치 직접 지정)
- scroll: 좌우 또는 상하 스와이프
- **데이터**: 각 아이템별 독립 DataSourceConfig (조인/집계 자유)
- **계산식 지원**: "생산량/총재고량", "출고량/현재고량" 같은 복합 표현 가능
- 값 A, B를 각각 다른 테이블/집계로 설정
- 표시 형태: 분수(1,234/5,678), 퍼센트(21.7%), 비율(1,234:5,678)
- **CRUD**: 주로 읽기. 목표값 수정 등 필요 시 write 컬럼으로 저장 가능 - **CRUD**: 주로 읽기. 목표값 수정 등 필요 시 write 컬럼으로 저장 가능
- **이벤트**: - **이벤트**:
- 수신: filter_changed, data_ready - 수신: filter_changed, data_ready
- 발행: kpi_clicked (KPI 카드 클릭 시 상세 데이터 전달) - 발행: kpi_clicked (아이템 클릭 시 상세 데이터 전달)
- **설정**: 데이터 소스, 집계 함수, 라벨, 단위, 색상 구간, 차트 타입, 새로고침 주기, 목표값, 표시 모드(slide/scroll/grid) - **설정**: 데이터 소스(드롭다운 기반 쉬운 집계), 집계 함수, 계산식, 라벨, 단위, 색상 구간, 차트 타입, 새로고침 주기, 목표값, 표시 모드, 아이템별 보이기/숨기기
- **보이기/숨기기**: 각 아이템별로 pop-system에서 개별 on/off 가능 (userConfigurable)
- **기존 POP 대시보드 폐기**: `frontend/components/pop/dashboard/` 폴더 전체를 이 컴포넌트로 대체 예정 (Phase 1~3 완료 후)
#### pop-dashboard 데이터 구조
```
PopDashboardConfig {
items: DashboardItem[] // 아이템 목록 (각각 독립 설정)
displayMode: "arrows" | "auto-slide" | "grid" | "scroll"
autoSlideInterval: number // 자동 슬라이드 간격(초)
gridLayout: { columns: number, rows: number } // 행열 그리드 설정
showIndicator: boolean // 페이지 인디케이터 표시
gap: number // 아이템 간 간격
}
DashboardItem {
id: string
label: string // pop-system에서 보이기/숨기기용 이름
visible: boolean // 보이기/숨기기
subType: "kpi-card" | "chart" | "gauge" | "stat-card"
dataSource: DataSourceConfig // 각 아이템별 독립 데이터 소스
// 행열 그리드 모드에서의 위치 (디자이너가 직접 지정)
gridPosition: { col: number, row: number, colSpan: number, rowSpan: number }
// 계산식 (선택사항)
formula?: {
enabled: boolean
values: [
{ id: "A", dataSource: DataSourceConfig, label: "생산량" },
{ id: "B", dataSource: DataSourceConfig, label: "총재고량" },
]
expression: string // "A / B", "A + B", "A / B * 100"
displayFormat: "value" | "fraction" | "percent" | "ratio"
}
// 서브타입별 설정
kpiConfig?: { unit, colorRanges, showTrend, trendPeriod }
chartConfig?: { chartType, xAxis, yAxis, colors }
gaugeConfig?: { min, max, target, colorRanges }
statConfig?: { categories, showLink }
}
```
#### 설정 패널 흐름 (드롭다운 기반 쉬운 집계)
```
1. [+ 아이템 추가] 버튼 클릭
2. 서브타입 선택: kpi-card / chart / gauge / stat-card
3. 데이터 모드 선택: [단일 집계] 또는 [계산식]
[단일 집계]
- 테이블 선택 (table-schema API로 목록)
- 조인할 테이블 추가 (선택사항)
- 컬럼 선택 → 집계 함수 선택 (합계/건수/평균/최소/최대)
- 필터 조건 추가
[계산식] (예: 생산량/총재고량)
- 값 A: 테이블 -> 컬럼 -> 집계함수
- 값 B: 테이블 -> 컬럼 -> 집계함수 (다른 테이블도 가능)
- 계산식: A / B
- 표시 형태: 분수 / 퍼센트 / 비율
4. 라벨, 단위, 색상 등 외형 설정
5. 행열 그리드 위치 설정 (grid 모드일 때)
```
### 3. pop-table (신규 - 가장 복잡) ### 3. pop-table (신규 - 가장 복잡)
@ -335,6 +423,59 @@ DataSourceConfig를 받아서 기존 API를 호출하고 결과를 반환:
- **pop-icon과의 차이**: pop-icon은 이동/실행만 하고 값이 안 돌아옴. pop-lookup은 값을 골라서 돌려줌 - **pop-icon과의 차이**: pop-icon은 이동/실행만 하고 값이 안 돌아옴. pop-lookup은 값을 골라서 돌려줌
- **pop-search와의 차이**: pop-search는 텍스트/날짜/드롭다운으로 필터링. pop-lookup은 모달을 열어서 목록에서 선택 - **pop-search와의 차이**: pop-search는 텍스트/날짜/드롭다운으로 필터링. pop-lookup은 모달을 열어서 목록에서 선택
#### pop-lookup 모달 화면 설계 방식
pop-lookup이 열리는 모달의 내부 화면은 **두 가지 방식** 중 선택할 수 있다:
**방식 A: 인라인 모달 (기본)**
- pop-lookup 컴포넌트의 설정 패널에서 직접 모달 내부 화면을 구성
- DataSourceConfig + 표시 컬럼 + 검색 필터 설정만으로 동작
- 별도 화면 생성 없이 컴포넌트 설정만으로 완결
- 적합한 경우: 단순 목록 선택 (거래처 목록, 품목 목록 등)
**방식 B: 외부 화면 참조 (고급)**
- 별도의 POP 화면(screen_id)을 모달로 연결
- 모달 안에서 검색/필터/테이블 등 복잡한 화면을 디자이너로 자유롭게 구성
- 여러 pop-lookup에서 같은 모달 화면을 재사용 가능
- 적합한 경우: 복잡한 검색/필터가 필요한 선택 화면, 여러 화면에서 공유하는 모달
**설정 구조:**
```
modalConfig: {
mode: "inline" | "screen-ref"
// mode = "inline"일 때 사용
dataSource: DataSourceConfig
displayColumns: ColumnBinding[]
searchFilter: { enabled: boolean, targetColumns: string[] }
modalSize: { width: number, height: number }
// mode = "screen-ref"일 때 사용
screenId: number // 참조할 POP 화면 ID
returnMapping: { // 모달 화면에서 선택된 값을 어떻게 매핑할지
sourceColumn: string // 모달 화면에서 반환하는 컬럼
targetField: string // pop-lookup 필드에 표시할 값
}[]
modalSize: { width: number, height: number }
}
```
**기존 시스템과의 호환성 (검증 완료):**
| 항목 | 현재 상태 | pop-lookup 지원 여부 |
|------|-----------|---------------------|
| DB: layout_data JSONB | 유연한 JSON 구조 | modalConfig를 layout_data에 저장 가능 (스키마 변경 불필요) |
| DB: screen_layouts_pop 테이블 | screen_id + company_code 기반 | 모달 화면도 별도 screen_id로 저장 가능 |
| 프론트: TabsWidget | screenId로 외부 화면 참조 지원 | 같은 패턴으로 모달에서 외부 화면 로드 가능 |
| 프론트: detectLinkedModals API | 연결된 모달 화면 감지 기능 있음 | 화면 간 참조 관계 추적에 활용 가능 |
| 백엔드: saveLayoutPop/getLayoutPop | POP 전용 저장/조회 API 있음 | 모달 화면도 동일 API로 저장/조회 가능 |
| 레이어 시스템 | layer_id 기반 다중 레이어 지원 | 모달 내부 레이아웃을 레이어로 관리 가능 |
**DB 마이그레이션 불필요**: layout_data가 JSONB이므로 modalConfig를 컴포넌트 overrides에 포함하면 됨.
**백엔드 변경 불필요**: 기존 saveLayoutPop/getLayoutPop API가 그대로 사용 가능.
**프론트엔드 참고 패턴**: TabsWidget의 screenId 참조 방식을 그대로 차용.
### 9. pop-system (신규) ### 9. pop-system (신규)
- **한 줄 정의**: 시스템 설정을 하나로 통합한 컴포넌트 (프로필, 테마, 보이기/숨기기) - **한 줄 정의**: 시스템 설정을 하나로 통합한 컴포넌트 (프로필, 테마, 보이기/숨기기)
@ -413,7 +554,34 @@ sequenceDiagram
Note over Table: 발주 품목 3건 표시 Note over Table: 발주 품목 3건 표시
``` ```
### 예시 4: 컬럼별 읽기/쓰기 분리 동작 ### 예시 4: pop-lookup 인라인 모달 vs 외부 화면 참조
```mermaid
sequenceDiagram
participant User as 사용자
participant Lookup as pop-lookup (거래처)
participant Modal as 모달
Note over User,Modal: [방식 A: 인라인 모달]
User->>Lookup: 거래처 필드 클릭
Lookup->>Modal: 인라인 모달 열림 (DataSourceConfig 기반)
Note over Modal: supplier 테이블에서 목록 조회
Note over Modal: 테이블형 목록 표시
User->>Modal: "대한금속" 선택
Modal->>Lookup: value_selected { supplier_code: "DH001", name: "대한금속" }
Note over Lookup: 필드에 "대한금속" 표시
Note over User,Modal: [방식 B: 외부 화면 참조]
User->>Lookup: 거래처 필드 클릭
Lookup->>Modal: 모달 열림 (screenId=42 화면 로드)
Note over Modal: 별도 POP 화면 렌더링
Note over Modal: pop-search(검색) + pop-table(목록) 등 배치된 컴포넌트 동작
User->>Modal: 검색 후 "대한금속" 선택
Modal->>Lookup: returnMapping 기반으로 값 반환
Note over Lookup: 필드에 "대한금속" 표시
```
### 예시 5: 컬럼별 읽기/쓰기 분리 동작
5개 컬럼이 있는 발주 화면: 5개 컬럼이 있는 발주 화면:
@ -431,13 +599,91 @@ sequenceDiagram
## 구현 우선순위 ## 구현 우선순위
- Phase 0 (공통 인프라): ColumnBinding, JoinConfig, DataSourceConfig 타입, useDataSource 훅 (CRUD 포함), usePopEvent 훅 (데이터 전달 포함), PopActionConfig 타입 - Phase 0 (공통 인프라): ColumnBinding, JoinConfig, DataSourceConfig 타입, useDataSource 훅 (CRUD 포함), usePopEvent 훅 (데이터 전달 포함), PopActionConfig 타입
- Phase 1 (기본 표시): pop-dashboard (KPI 카드 서브타입부터) - Phase 1 (기본 표시): pop-dashboard (4개 서브타입 전부 + 멀티 아이템 컨테이너 + 4개 표시 모드 + 계산식)
- Phase 2 (기본 액션): pop-button, pop-icon - Phase 2 (기본 액션): pop-button, pop-icon
- Phase 3 (데이터 목록): pop-table (테이블형부터, 카드형은 후순위) - Phase 3 (데이터 목록): pop-table (테이블형부터, 카드형은 후순위)
- Phase 4 (입력/연동): pop-search, pop-field, pop-lookup - Phase 4 (입력/연동): pop-search, pop-field, pop-lookup
- Phase 5 (고도화): pop-table 카드 템플릿, 차트, 게이지 - Phase 5 (고도화): pop-table 카드 템플릿
- Phase 6 (시스템): pop-system (프로필, 테마, 대시보드 보이기/숨기기 통합) - Phase 6 (시스템): pop-system (프로필, 테마, 대시보드 보이기/숨기기 통합)
### Phase 1 상세 변경 (2026-02-09 토의 결정)
기존 계획에서 "KPI 카드 우선"이었으나, 토의 결과 **4개 서브타입 전부를 Phase 1에서 구현**으로 변경:
- kpi-card, chart, gauge, stat-card 모두 Phase 1
- 멀티 아이템 컨테이너 (arrows, auto-slide, grid, scroll)
- 계산식 지원 (formula)
- 드롭다운 기반 쉬운 집계 설정
- 기존 `frontend/components/pop/dashboard/` 폴더는 Phase 1 완료 후 폐기/삭제
### 백엔드 API 현황 (호환성 점검 완료)
기존 백엔드에 이미 구현되어 있어 새로 만들 필요 없는 API:
| API | 용도 | 비고 |
|-----|------|------|
| `dataApi.getTableData()` | 동적 테이블 조회 | 페이징, 검색, 정렬, 필터 |
| `dataApi.getJoinedData()` | 2개 테이블 조인 | Entity 조인, 필터링, 중복제거 |
| `entityJoinApi.getTableDataWithJoins()` | Entity 조인 전용 | ID->이름 자동 변환 |
| `dataApi.createRecord/updateRecord/deleteRecord()` | 동적 CRUD | - |
| `dataApi.upsertGroupedRecords()` | 그룹 UPSERT | - |
| `dashboardApi.executeQuery()` | SELECT SQL 직접 실행 | 집계/복합조인용 |
| `dashboardApi.getTableSchema()` | 테이블/컬럼 목록 | 설정 패널 드롭다운용 |
**백엔드 신규 개발 불필요** - 기존 API만으로 모든 데이터 연동 가능
### useDataSource의 API 선택 전략
```
단순 조회 (조인/집계 없음) -> dataApi.getTableData() 또는 entityJoinApi
2개 테이블 조인 -> dataApi.getJoinedData()
3개+ 테이블 조인 또는 집계 -> DataSourceConfig를 SQL로 변환 -> dashboardApi.executeQuery()
CRUD -> dataApi.createRecord/updateRecord/deleteRecord()
```
### POP 전용 훅 분리 (2026-02-09 결정)
데스크탑과의 완전 분리를 위해 POP 전용 훅은 별도 폴더:
- `frontend/hooks/pop/usePopEvent.ts` (POP 전용)
- `frontend/hooks/pop/useDataSource.ts` (POP 전용)
## 기존 시스템 호환성 검증 결과 (v8.0 추가)
v8.0에서 추가된 모달 설계 방식에 대해 기존 시스템과의 호환성을 검증한 결과:
### DB 스키마 (변경 불필요)
| 테이블 | 현재 구조 | 호환성 |
|--------|-----------|--------|
| screen_layouts_v2 | layout_data JSONB + screen_id + company_code + layer_id | modalConfig를 컴포넌트 overrides에 포함하면 됨 |
| screen_layouts_pop | 동일 구조 (POP 전용) | 모달 화면도 별도 screen_id로 저장 가능 |
- layout_data가 JSONB 타입이므로 어떤 JSON 구조든 저장 가능
- 모달 화면을 별도 screen_id로 만들어도 기존 UNIQUE(screen_id, company_code, layer_id) 제약조건과 충돌 없음
- DB 마이그레이션 불필요
### 백엔드 API (변경 불필요)
| API | 엔드포인트 | 호환성 |
|-----|-----------|--------|
| POP 레이아웃 저장 | POST /api/screen-management/screens/:screenId/layout-pop | 모달 화면도 동일 API로 저장 |
| POP 레이아웃 조회 | GET /api/screen-management/screens/:screenId/layout-pop | 모달 화면도 동일 API로 조회 |
| 연결 모달 감지 | detectLinkedModals(screenId) | 화면 간 참조 관계 추적에 활용 |
### 프론트엔드 (참고 패턴 존재)
| 기존 기능 | 위치 | 활용 방안 |
|-----------|------|-----------|
| TabsWidget screenId 참조 | frontend/components/screen/widgets/TabsWidget.tsx | 같은 패턴으로 모달에서 외부 화면 로드 |
| TabsConfigPanel | frontend/components/screen/config-panels/TabsConfigPanel.tsx | pop-lookup 설정 패널의 모달 화면 선택 UI 참조 |
| ScreenDesigner 탭 내부 컴포넌트 | frontend/components/screen/ScreenDesigner.tsx | 모달 내부 컴포넌트 편집 패턴 참조 |
### 결론
- DB 마이그레이션: 불필요
- 백엔드 변경: 불필요
- 프론트엔드: pop-lookup 컴포넌트 구현 시 기존 TabsWidget의 screenId 참조 패턴을 그대로 차용
- 새로운 API: 불필요 (기존 saveLayoutPop/getLayoutPop로 충분)
## 참고 파일 ## 참고 파일
- 레지스트리: `frontend/lib/registry/PopComponentRegistry.ts` - 레지스트리: `frontend/lib/registry/PopComponentRegistry.ts`
@ -445,3 +691,6 @@ sequenceDiagram
- 공통 스타일 타입: `frontend/lib/registry/pop-components/types.ts` - 공통 스타일 타입: `frontend/lib/registry/pop-components/types.ts`
- POP 타입 정의: `frontend/components/pop/designer/types/pop-layout.ts` - POP 타입 정의: `frontend/components/pop/designer/types/pop-layout.ts`
- 기존 스펙 (v4): `popdocs/components-spec.md` - 기존 스펙 (v4): `popdocs/components-spec.md`
- 탭 위젯 (모달 참조 패턴): `frontend/components/screen/widgets/TabsWidget.tsx`
- POP 레이아웃 API: `frontend/lib/api/screen.ts` (saveLayoutPop, getLayoutPop)
- 백엔드 화면관리: `backend-node/src/controllers/screenManagementController.ts`

View File

@ -24,6 +24,8 @@ import {
GRID_BREAKPOINTS, GRID_BREAKPOINTS,
detectGridMode, detectGridMode,
} from "@/components/pop/designer/types/pop-layout"; } from "@/components/pop/designer/types/pop-layout";
// POP 컴포넌트 자동 등록 (레지스트리 초기화 - PopRenderer보다 먼저 import)
import "@/lib/registry/pop-components";
import PopRenderer from "@/components/pop/designer/renderers/PopRenderer"; import PopRenderer from "@/components/pop/designer/renderers/PopRenderer";
import { import {
useResponsiveModeWithOverride, useResponsiveModeWithOverride,
@ -180,7 +182,7 @@ function PopScreenViewPage() {
<ScreenPreviewProvider isPreviewMode={isPreviewMode}> <ScreenPreviewProvider isPreviewMode={isPreviewMode}>
<ActiveTabProvider> <ActiveTabProvider>
<TableOptionsProvider> <TableOptionsProvider>
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden"> <div className="h-screen bg-gray-100 flex flex-col">
{/* 상단 툴바 (프리뷰 모드에서만) */} {/* 상단 툴바 (프리뷰 모드에서만) */}
{isPreviewMode && ( {isPreviewMode && (
<div className="sticky top-0 z-50 bg-white border-b shadow-sm"> <div className="sticky top-0 z-50 bg-white border-b shadow-sm">
@ -261,7 +263,7 @@ function PopScreenViewPage() {
)} )}
{/* POP 화면 컨텐츠 */} {/* POP 화면 컨텐츠 */}
<div className={`flex-1 flex flex-col ${isPreviewMode ? "py-4 overflow-auto items-center" : ""}`}> <div className={`flex-1 flex flex-col overflow-auto ${isPreviewMode ? "py-4 items-center" : ""}`}>
{/* 현재 모드 표시 (일반 모드) */} {/* 현재 모드 표시 (일반 모드) */}
{!isPreviewMode && ( {!isPreviewMode && (
<div className="absolute top-2 right-2 z-10 bg-black/50 text-white text-xs px-2 py-1 rounded"> <div className="absolute top-2 right-2 z-10 bg-black/50 text-white text-xs px-2 py-1 rounded">
@ -270,7 +272,7 @@ function PopScreenViewPage() {
)} )}
<div <div
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full"}`} className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full min-h-full"}`}
style={isPreviewMode ? { style={isPreviewMode ? {
width: currentDevice.width, width: currentDevice.width,
maxHeight: "80vh", maxHeight: "80vh",

View File

@ -553,9 +553,20 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect
// ======================================== // ========================================
function renderActualComponent(component: PopComponentDefinitionV5): React.ReactNode { function renderActualComponent(component: PopComponentDefinitionV5): React.ReactNode {
const typeLabel = COMPONENT_TYPE_LABELS[component.type]; // 레지스트리에서 등록된 실제 컴포넌트 조회
const registeredComp = PopComponentRegistry.getComponent(component.type);
// 샘플 박스 렌더링 const ActualComp = registeredComp?.component;
if (ActualComp) {
return (
<div className="h-full w-full overflow-hidden">
<ActualComp config={component.config} label={component.label} />
</div>
);
}
// 미등록 컴포넌트: 플레이스홀더 (fallback)
const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type;
return ( return (
<div className="flex h-full w-full items-center justify-center p-2"> <div className="flex h-full w-full items-center justify-center p-2">
<span className="text-xs text-gray-500">{component.label || typeLabel}</span> <span className="text-xs text-gray-500">{component.label || typeLabel}</span>