# POP 화면 시스템 구현 계획서 ## 개요 Vexplor 서비스 내에서 POP(Point of Production) 화면을 구성할 수 있는 시스템을 구현합니다. 기존 Vexplor와 충돌 없이 별도 공간에서 개발하되, 장기적으로 통합 가능하도록 동일한 서비스 로직을 사용합니다. --- ## 핵심 원칙 | 원칙 | 설명 | |------|------| | **충돌 방지** | POP 전용 공간에서 개발 | | **통합 준비** | 기본 서비스 로직은 Vexplor와 동일 | | **데이터 공유** | 같은 DB, 같은 데이터 소스 사용 | --- ## 아키텍처 개요 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ [데이터베이스] │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ screen_ │ │ screen_layouts_ │ │ screen_layouts_ │ │ │ │ definitions │ │ v2 (데스크톱) │ │ pop (POP) │ │ │ │ (공통) │ └─────────────────┘ └─────────────────┘ │ │ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ [백엔드 API] │ │ /screen-management/screens/:id/layout-v2 (데스크톱) │ │ /screen-management/screens/:id/layout-pop (POP) │ └─────────────────────────────────────────────────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ┌─────────────────────────┐ ┌─────────────────────────┐ │ [프론트엔드 - 데스크톱] │ │ [프론트엔드 - POP] │ │ │ │ │ │ app/(main)/ │ │ app/(pop)/ │ │ lib/registry/ │ │ lib/registry/ │ │ components/ │ │ pop-components/ │ │ components/screen/ │ │ components/pop/ │ └─────────────────────────┘ └─────────────────────────┘ │ │ ▼ ▼ ┌─────────────────────────┐ ┌─────────────────────────┐ │ PC 브라우저 │ │ 모바일/태블릿 브라우저 │ │ (마우스 + 키보드) │ │ (터치 + 스캐너) │ └─────────────────────────┘ └─────────────────────────┘ ``` --- ## 1. 데이터베이스 변경사항 ### 1-1. 테이블 추가/유지 현황 | 구분 | 테이블명 | 변경 내용 | 비고 | |------|----------|----------|------| | **추가** | `screen_layouts_pop` | POP 레이아웃 저장용 | 신규 테이블 | | **유지** | `screen_definitions` | 변경 없음 | 공통 사용 | | **유지** | `screen_layouts_v2` | 변경 없음 | 데스크톱 전용 | ### 1-2. 신규 테이블 DDL ```sql -- 마이그레이션 파일: db/migrations/XXX_create_screen_layouts_pop.sql CREATE TABLE screen_layouts_pop ( layout_id SERIAL PRIMARY KEY, screen_id INTEGER NOT NULL REFERENCES screen_definitions(screen_id), company_code VARCHAR(20) NOT NULL, layout_data JSONB NOT NULL DEFAULT '{}'::jsonb, -- 반응형 레이아웃 JSON created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by VARCHAR(50), updated_by VARCHAR(50), UNIQUE(screen_id, company_code) ); CREATE INDEX idx_pop_screen_id ON screen_layouts_pop(screen_id); CREATE INDEX idx_pop_company_code ON screen_layouts_pop(company_code); COMMENT ON TABLE screen_layouts_pop IS 'POP 화면 레이아웃 저장 테이블 (모바일/태블릿 반응형)'; COMMENT ON COLUMN screen_layouts_pop.layout_data IS 'V2 형식의 레이아웃 JSON (반응형 구조)'; ``` ### 1-3. 레이아웃 JSON 구조 (V2 형식 동일) ```json { "version": "2.0", "components": [ { "id": "comp_xxx", "url": "@/lib/registry/pop-components/pop-card-list", "position": { "x": 0, "y": 0 }, "size": { "width": 100, "height": 50 }, "displayOrder": 0, "overrides": { "tableName": "user_info", "columns": ["id", "name", "status"], "cardStyle": "compact" } } ], "updatedAt": "2026-01-29T12:00:00Z" } ``` --- ## 2. 백엔드 변경사항 ### 2-1. 파일 수정 목록 | 구분 | 파일 경로 | 변경 내용 | |------|----------|----------| | **수정** | `backend-node/src/services/screenManagementService.ts` | POP 레이아웃 CRUD 함수 추가 | | **수정** | `backend-node/src/routes/screenManagementRoutes.ts` | POP API 엔드포인트 추가 | ### 2-2. 추가 API 엔드포인트 ``` GET /screen-management/screens/:screenId/layout-pop # POP 레이아웃 조회 POST /screen-management/screens/:screenId/layout-pop # POP 레이아웃 저장 DELETE /screen-management/screens/:screenId/layout-pop # POP 레이아웃 삭제 ``` ### 2-3. screenManagementService.ts 추가 함수 ```typescript // 기존 함수 (유지) getScreenLayoutV2(screenId, companyCode) saveLayoutV2(screenId, companyCode, layoutData) // 추가 함수 (신규) - 로직은 V2와 동일, 테이블명만 다름 getScreenLayoutPop(screenId, companyCode) saveLayoutPop(screenId, companyCode, layoutData) deleteLayoutPop(screenId, companyCode) ``` --- ## 3. 프론트엔드 변경사항 ### 3-1. 폴더 구조 ``` frontend/ ├── app/ │ └── (pop)/ # [기존] POP 라우팅 그룹 │ ├── layout.tsx # [수정] POP 전용 레이아웃 │ ├── pop/ │ │ └── page.tsx # [기존] POP 메인 │ └── screens/ # [추가] POP 화면 뷰어 │ └── [screenId]/ │ └── page.tsx # [추가] POP 동적 화면 │ ├── lib/ │ ├── api/ │ │ └── screen.ts # [수정] POP API 함수 추가 │ │ │ ├── registry/ │ │ ├── pop-components/ # [추가] POP 전용 컴포넌트 │ │ │ ├── pop-card-list/ │ │ │ │ ├── PopCardListComponent.tsx │ │ │ │ ├── PopCardListConfigPanel.tsx │ │ │ │ └── index.ts │ │ │ ├── pop-touch-button/ │ │ │ ├── pop-scanner-input/ │ │ │ └── index.ts # POP 컴포넌트 내보내기 │ │ │ │ │ ├── PopComponentRegistry.ts # [추가] POP 컴포넌트 레지스트리 │ │ └── ComponentRegistry.ts # [유지] 기존 유지 │ │ │ ├── schemas/ │ │ └── popComponentConfig.ts # [추가] POP용 Zod 스키마 │ │ │ └── utils/ │ └── layoutPopConverter.ts # [추가] POP 레이아웃 변환기 │ └── components/ └── pop/ # [기존] POP UI 컴포넌트 ├── PopScreenDesigner.tsx # [추가] POP 화면 설계 도구 ├── PopPreview.tsx # [추가] POP 미리보기 └── PopDynamicRenderer.tsx # [추가] POP 동적 렌더러 ``` ### 3-2. 파일별 상세 내용 #### A. 신규 파일 (추가) | 파일 | 역할 | 기반 | |------|------|------| | `app/(pop)/screens/[screenId]/page.tsx` | POP 화면 뷰어 | `app/(main)/screens/[screenId]/page.tsx` 참고 | | `lib/registry/PopComponentRegistry.ts` | POP 컴포넌트 등록 | `ComponentRegistry.ts` 구조 동일 | | `lib/registry/pop-components/*` | POP 전용 컴포넌트 | 신규 개발 | | `lib/schemas/popComponentConfig.ts` | POP Zod 스키마 | `componentConfig.ts` 구조 동일 | | `lib/utils/layoutPopConverter.ts` | POP 레이아웃 변환 | `layoutV2Converter.ts` 구조 동일 | | `components/pop/PopScreenDesigner.tsx` | POP 화면 설계 | 신규 개발 | | `components/pop/PopDynamicRenderer.tsx` | POP 동적 렌더러 | `DynamicComponentRenderer.tsx` 참고 | #### B. 수정 파일 | 파일 | 변경 내용 | |------|----------| | `lib/api/screen.ts` | `getLayoutPop()`, `saveLayoutPop()` 함수 추가 | | `app/(pop)/layout.tsx` | POP 전용 레이아웃 스타일 적용 | #### C. 유지 파일 (변경 없음) | 파일 | 이유 | |------|------| | `lib/registry/ComponentRegistry.ts` | 데스크톱 전용, 분리 유지 | | `lib/schemas/componentConfig.ts` | 데스크톱 전용, 분리 유지 | | `lib/utils/layoutV2Converter.ts` | 데스크톱 전용, 분리 유지 | | `app/(main)/*` | 데스크톱 전용, 변경 없음 | --- ## 4. 서비스 로직 흐름 ### 4-1. 데스크톱 (기존 - 변경 없음) ``` [사용자] → /screens/123 접속 ↓ [app/(main)/screens/[screenId]/page.tsx] ↓ [getLayoutV2(screenId)] → API 호출 ↓ [screen_layouts_v2 테이블] → 레이아웃 JSON 반환 ↓ [DynamicComponentRenderer] → 컴포넌트 렌더링 ↓ [ComponentRegistry] → 컴포넌트 찾기 ↓ [lib/registry/components/table-list] → 컴포넌트 실행 ↓ [화면 표시] ``` ### 4-2. POP (신규 - 동일 로직) ``` [사용자] → /pop/screens/123 접속 ↓ [app/(pop)/screens/[screenId]/page.tsx] ↓ [getLayoutPop(screenId)] → API 호출 ↓ [screen_layouts_pop 테이블] → 레이아웃 JSON 반환 ↓ [PopDynamicRenderer] → 컴포넌트 렌더링 ↓ [PopComponentRegistry] → 컴포넌트 찾기 ↓ [lib/registry/pop-components/pop-card-list] → 컴포넌트 실행 ↓ [화면 표시] ``` --- ## 5. 로직 변경 여부 | 구분 | 로직 변경 | 설명 | |------|----------|------| | 데이터베이스 CRUD | **없음** | 동일한 SELECT/INSERT/UPDATE 패턴 | | API 호출 방식 | **없음** | 동일한 REST API 패턴 | | 컴포넌트 렌더링 | **없음** | 동일한 URL 기반 + overrides 방식 | | Zod 스키마 검증 | **없음** | 동일한 검증 로직 | | 레이아웃 JSON 구조 | **없음** | 동일한 V2 JSON 구조 사용 | **결론: 로직 변경 없음, 파일/테이블 분리만 진행** --- ## 6. 데스크톱 vs POP 비교 | 구분 | Vexplor (데스크톱) | POP (모바일/태블릿) | |------|-------------------|---------------------| | **타겟 기기** | PC (마우스+키보드) | 모바일/태블릿 (터치) | | **화면 크기** | 1920x1080 고정 | 반응형 (다양한 크기) | | **UI 스타일** | 테이블 중심, 작은 버튼 | 카드 중심, 큰 터치 버튼 | | **입력 방식** | 키보드 타이핑 | 터치, 스캐너, 음성 | | **사용 환경** | 사무실 | 현장, 창고, 공장 | | **레이아웃 테이블** | `screen_layouts_v2` | `screen_layouts_pop` | | **컴포넌트 경로** | `lib/registry/components/` | `lib/registry/pop-components/` | | **레지스트리** | `ComponentRegistry.ts` | `PopComponentRegistry.ts` | --- ## 7. 장기 통합 시나리오 ### Phase 1: 분리 개발 (현재 목표) ``` [데스크톱] [POP] ComponentRegistry PopComponentRegistry components/ pop-components/ screen_layouts_v2 screen_layouts_pop ``` ### Phase 2: 부분 통합 (향후) ``` [통합 가능한 부분] - 공통 유틸리티 함수 - 공통 Zod 스키마 - 공통 타입 정의 [분리 유지] - 플랫폼별 컴포넌트 - 플랫폼별 레이아웃 ``` ### Phase 3: 완전 통합 (최종) ``` [단일 컴포넌트 레지스트리] ComponentRegistry ├── components/ (공통) ├── desktop-components/ (데스크톱 전용) └── pop-components/ (POP 전용) [단일 레이아웃 테이블] (선택사항) screen_layouts ├── platform = 'desktop' └── platform = 'pop' ``` --- ## 8. V2 공통 요소 (통합 핵심) POP과 데스크톱이 장기적으로 통합될 수 있는 **핵심 기반**입니다. ### 8-1. 공통 유틸리티 함수 **파일 위치:** `frontend/lib/schemas/componentConfig.ts`, `frontend/lib/utils/layoutV2Converter.ts` #### 핵심 병합/추출 함수 (가장 중요!) | 함수명 | 역할 | 사용 시점 | |--------|------|----------| | `deepMerge()` | 객체 깊은 병합 | 기본값 + overrides 합칠 때 | | `mergeComponentConfig()` | 기본값 + 커스텀 병합 | **렌더링 시** (화면 표시) | | `extractCustomConfig()` | 기본값과 다른 부분만 추출 | **저장 시** (DB 저장) | | `isDeepEqual()` | 두 객체 깊은 비교 | 변경 여부 판단 | ```typescript // 예시: 저장 시 차이값만 추출 const defaults = { showHeader: true, pageSize: 20 }; const fullConfig = { showHeader: true, pageSize: 50, customField: "test" }; const overrides = extractCustomConfig(fullConfig, defaults); // 결과: { pageSize: 50, customField: "test" } (차이값만!) ``` #### URL 처리 함수 | 함수명 | 역할 | 예시 | |--------|------|------| | `getComponentUrl()` | 타입 → URL 변환 | `"v2-table-list"` → `"@/lib/registry/components/v2-table-list"` | | `getComponentTypeFromUrl()` | URL → 타입 추출 | `"@/lib/registry/components/v2-table-list"` → `"v2-table-list"` | #### 기본값 조회 함수 | 함수명 | 역할 | |--------|------| | `getComponentDefaults()` | 컴포넌트 타입으로 기본값 조회 | | `getDefaultsByUrl()` | URL로 기본값 조회 | #### V2 로드/저장 함수 (핵심!) | 함수명 | 역할 | 사용 시점 | |--------|------|----------| | `loadComponentV2()` | 컴포넌트 로드 (기본값 병합) | DB → 화면 | | `saveComponentV2()` | 컴포넌트 저장 (차이값 추출) | 화면 → DB | | `loadLayoutV2()` | 레이아웃 전체 로드 | DB → 화면 | | `saveLayoutV2()` | 레이아웃 전체 저장 | 화면 → DB | #### 변환 함수 | 함수명 | 역할 | |--------|------| | `convertV2ToLegacy()` | V2 → Legacy 변환 (하위 호환) | | `convertLegacyToV2()` | Legacy → V2 변환 | | `isValidV2Layout()` | V2 레이아웃인지 검증 | | `isLegacyLayout()` | 레거시 레이아웃인지 확인 | ### 8-2. 공통 Zod 스키마 **파일 위치:** `frontend/lib/schemas/componentConfig.ts` #### 핵심 스키마 (필수!) ```typescript // 컴포넌트 기본 구조 export const componentV2Schema = z.object({ id: z.string(), url: z.string(), position: z.object({ x: z.number(), y: z.number() }), size: z.object({ width: z.number(), height: z.number() }), displayOrder: z.number().default(0), overrides: z.record(z.string(), z.any()).default({}), }); // 레이아웃 기본 구조 export const layoutV2Schema = z.object({ version: z.string().default("2.0"), components: z.array(componentV2Schema).default([]), updatedAt: z.string().optional(), screenResolution: z.object({...}).optional(), gridSettings: z.any().optional(), }); ``` #### 컴포넌트별 overrides 스키마 (25개+) | 스키마명 | 컴포넌트 | 주요 기본값 | |----------|----------|------------| | `v2TableListOverridesSchema` | 테이블 리스트 | displayMode: "table", pageSize: 20 | | `v2ButtonPrimaryOverridesSchema` | 버튼 | text: "저장", variant: "primary" | | `v2SplitPanelLayoutOverridesSchema` | 분할 레이아웃 | splitRatio: 30, resizable: true | | `v2SectionCardOverridesSchema` | 섹션 카드 | padding: "md", collapsible: false | | `v2TabsWidgetOverridesSchema` | 탭 위젯 | orientation: "horizontal" | | `v2RepeaterOverridesSchema` | 리피터 | renderMode: "inline" | #### 스키마 레지스트리 (자동 매핑) ```typescript const componentOverridesSchemaRegistry = { "v2-table-list": v2TableListOverridesSchema, "v2-button-primary": v2ButtonPrimaryOverridesSchema, "v2-split-panel-layout": v2SplitPanelLayoutOverridesSchema, // ... 25개+ 컴포넌트 }; ``` ### 8-3. 공통 타입 정의 **파일 위치:** `frontend/types/v2-core.ts`, `frontend/types/v2-components.ts` #### 핵심 공통 타입 (v2-core.ts) ```typescript // 웹 입력 타입 export type WebType = | "text" | "textarea" | "email" | "tel" | "url" | "number" | "decimal" | "date" | "datetime" | "select" | "dropdown" | "radio" | "checkbox" | "boolean" | "code" | "entity" | "file" | "image" | "button" | "container" | "group" | "list" | "tree" | "custom"; // 버튼 액션 타입 export type ButtonActionType = | "save" | "cancel" | "delete" | "edit" | "copy" | "add" | "search" | "reset" | "submit" | "close" | "popup" | "modal" | "navigate" | "newWindow" | "control" | "transferData" | "quickInsert"; // 위치/크기 export interface Position { x: number; y: number; z?: number; } export interface Size { width: number; height: number; } // 공통 스타일 export interface CommonStyle { margin?: string; padding?: string; border?: string; backgroundColor?: string; color?: string; fontSize?: string; // ... 30개+ 속성 } // 유효성 검사 export interface ValidationRule { type: "required" | "minLength" | "maxLength" | "pattern" | "min" | "max" | "email" | "url"; value?: unknown; message: string; } ``` #### V2 컴포넌트 타입 (v2-components.ts) ```typescript // 10개 통합 컴포넌트 타입 export type V2ComponentType = | "V2Input" | "V2Select" | "V2Date" | "V2Text" | "V2Media" | "V2List" | "V2Layout" | "V2Group" | "V2Biz" | "V2Hierarchy"; // 공통 속성 export interface V2BaseProps { id: string; label?: string; required?: boolean; readonly?: boolean; disabled?: boolean; tableName?: string; columnName?: string; position?: Position; size?: Size; style?: CommonStyle; validation?: ValidationRule[]; } ``` ### 8-4. POP 통합 시 공유/분리 기준 #### 반드시 공유 (그대로 사용) | 구분 | 파일/요소 | 이유 | |------|----------|------| | **유틸리티** | `deepMerge`, `extractCustomConfig`, `mergeComponentConfig` | 저장/로드 로직 동일 | | **스키마** | `componentV2Schema`, `layoutV2Schema` | JSON 구조 동일 | | **타입** | `Position`, `Size`, `WebType`, `ButtonActionType` | 기본 구조 동일 | #### POP 전용으로 분리 | 구분 | 파일/요소 | 이유 | |------|----------|------| | **overrides 스키마** | `popCardListOverridesSchema` 등 | POP 컴포넌트 전용 기본값 | | **스키마 레지스트리** | `popComponentOverridesSchemaRegistry` | POP 컴포넌트 매핑 | | **기본값 레지스트리** | `popComponentDefaultsRegistry` | POP 컴포넌트 기본값 | ### 8-5. 추천 폴더 구조 (공유 분리) ``` frontend/lib/schemas/ ├── componentConfig.ts # 기존 (데스크톱) ├── popComponentConfig.ts # 신규 (POP) - 구조는 동일 └── shared/ # 신규 (공유) - 향후 통합 시 ├── baseSchemas.ts # componentV2Schema, layoutV2Schema ├── mergeUtils.ts # deepMerge, extractCustomConfig 등 └── types.ts # Position, Size 등 ``` --- ## 9. 작업 우선순위 ### [ ] 1단계: 데이터베이스 - [ ] `screen_layouts_pop` 테이블 생성 마이그레이션 작성 - [ ] 마이그레이션 실행 및 검증 ### [ ] 2단계: 백엔드 API - [ ] `screenManagementService.ts`에 POP 함수 추가 - [ ] `getScreenLayoutPop()` - [ ] `saveLayoutPop()` - [ ] `deleteLayoutPop()` - [ ] `screenManagementRoutes.ts`에 엔드포인트 추가 - [ ] `GET /screens/:screenId/layout-pop` - [ ] `POST /screens/:screenId/layout-pop` - [ ] `DELETE /screens/:screenId/layout-pop` ### [ ] 3단계: 프론트엔드 기반 - [ ] `lib/api/screen.ts`에 POP API 함수 추가 - [ ] `getLayoutPop()` - [ ] `saveLayoutPop()` - [ ] `lib/registry/PopComponentRegistry.ts` 생성 - [ ] `lib/schemas/popComponentConfig.ts` 생성 - [ ] `lib/utils/layoutPopConverter.ts` 생성 ### [ ] 4단계: POP 컴포넌트 개발 - [ ] `lib/registry/pop-components/` 폴더 구조 생성 - [ ] 기본 컴포넌트 개발 - [ ] `pop-card-list` (카드형 리스트) - [ ] `pop-touch-button` (터치 버튼) - [ ] `pop-scanner-input` (스캐너 입력) - [ ] `pop-status-badge` (상태 배지) ### [ ] 5단계: POP 화면 페이지 - [ ] `app/(pop)/screens/[screenId]/page.tsx` 생성 - [ ] `components/pop/PopDynamicRenderer.tsx` 생성 - [ ] `app/(pop)/layout.tsx` 수정 (POP 전용 스타일) ### [ ] 6단계: POP 화면 디자이너 (선택) - [ ] `components/pop/PopScreenDesigner.tsx` 생성 - [ ] `components/pop/PopPreview.tsx` 생성 - [ ] 관리자 메뉴에 POP 화면 설계 기능 추가 --- ## 10. 참고 파일 위치 ### 데스크톱 참고 파일 (기존) | 구분 | 파일 경로 | |------|----------| | 화면 페이지 | `frontend/app/(main)/screens/[screenId]/page.tsx` | | 컴포넌트 레지스트리 | `frontend/lib/registry/ComponentRegistry.ts` | | 동적 렌더러 | `frontend/lib/registry/DynamicComponentRenderer.tsx` | | Zod 스키마 | `frontend/lib/schemas/componentConfig.ts` | | 레이아웃 변환기 | `frontend/lib/utils/layoutV2Converter.ts` | | 화면 API | `frontend/lib/api/screen.ts` | | 백엔드 서비스 | `backend-node/src/services/screenManagementService.ts` | | 백엔드 라우트 | `backend-node/src/routes/screenManagementRoutes.ts` | ### 관련 문서 | 문서 | 경로 | |------|------| | V2 아키텍처 | `docs/DDD1542/COMPONENT_LAYOUT_V2_ARCHITECTURE.md` | | 화면관리 설계 | `docs/kjs/화면관리_시스템_설계.md` | --- ## 11. 주의사항 ### 멀티테넌시 - 모든 테이블에 `company_code` 필수 - 모든 쿼리에 `company_code` 필터링 적용 - 최고 관리자(`company_code = "*"`)는 모든 데이터 조회 가능 ### 충돌 방지 - 기존 데스크톱 파일 수정 최소화 - POP 전용 폴더/파일에서 작업 - 공통 로직은 별도 유틸리티로 분리 ### 테스트 - 데스크톱 기능 회귀 테스트 필수 - POP 반응형 테스트 (모바일/태블릿) - 멀티테넌시 격리 테스트 --- ## 변경 이력 | 날짜 | 버전 | 내용 | |------|------|------| | 2026-01-29 | 1.0 | 초기 계획서 작성 | | 2026-01-29 | 1.1 | V2 공통 요소 (통합 핵심) 섹션 추가 | --- ## 작성자 - 작성일: 2026-01-29 - 프로젝트: Vexplor POP 화면 시스템