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
-- 마이그레이션 파일: 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 형식 동일)
{
"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 추가 함수
// 기존 함수 (유지)
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() |
두 객체 깊은 비교 |
변경 여부 판단 |
// 예시: 저장 시 차이값만 추출
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
핵심 스키마 (필수!)
// 컴포넌트 기본 구조
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" |
스키마 레지스트리 (자동 매핑)
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)
// 웹 입력 타입
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)
// 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단계: 데이터베이스
[ ] 2단계: 백엔드 API
[ ] 3단계: 프론트엔드 기반
[ ] 4단계: POP 컴포넌트 개발
[ ] 5단계: POP 화면 페이지
[ ] 6단계: 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 화면 시스템