ERP-node/POPUPDATE.md

36 KiB

POP 화면 관리 시스템 개발 기록

AI 에이전트 안내: 이 문서는 Progressive Disclosure 방식으로 구성되어 있습니다.

  1. 먼저 Quick Reference에서 필요한 정보 확인
  2. 상세 내용이 필요하면 해당 섹션으로 이동
  3. 코드가 필요하면 파일 직접 참조

Quick Reference

POP이란?

Point of Production - 현장 작업자용 모바일/태블릿 화면 시스템

핵심 결정사항

  • 분리 방식: 레이아웃 기반 구분 (screen_layouts_pop 테이블)
  • 식별 방법: screen_layouts_pop에 레코드 존재 여부로 POP 화면 판별
  • 데스크톱 영향: 없음 (모든 isPop 기본값 = false)

주요 경로

용도 경로
POP 뷰어 URL /pop/screens/{screenId}?preview=true&device=tablet
POP 관리 페이지 /admin/screenMng/popScreenMngList
POP 레이아웃 API /api/screen-management/layout-pop/:screenId

파일 찾기 가이드

작업 파일
POP 레이아웃 DB 스키마 db/migrations/052_create_screen_layouts_pop.sql
POP API 서비스 로직 backend-node/src/services/screenManagementService.ts (getLayoutPop, saveLayoutPop)
POP API 라우트 backend-node/src/routes/screenManagementRoutes.ts
프론트엔드 API 클라이언트 frontend/lib/api/screen.ts (screenApi.getLayoutPop 등)
POP 화면 관리 UI frontend/app/(main)/admin/screenMng/popScreenMngList/page.tsx
POP 뷰어 페이지 frontend/app/(pop)/pop/screens/[screenId]/page.tsx
미리보기 URL 분기 frontend/components/screen/ScreenSettingModal.tsx (PreviewTab)
POP 컴포넌트 설계서 docs/pop/components-spec.md (13개 컴포넌트 상세)

섹션 목차

# 섹션 한 줄 요약
1 아키텍처 레이아웃 테이블로 POP/데스크톱 분리
2 데이터베이스 screen_layouts_pop 테이블 (FK 없음)
3 백엔드 API CRUD 4개 엔드포인트
4 프론트엔드 API screenApi에 4개 함수 추가
5 관리 페이지 POP 화면만 필터링하여 표시
6 뷰어 모바일/태블릿 프레임 미리보기
7 미리보기 isPop prop으로 URL 분기
8 파일 목록 생성 3개, 수정 9개
9 반응형 전략 Flow 레이아웃 (세로 쌓기) 채택
10 POP 사용자 앱 대시보드 카드 → 화면 뷰어
11 POP 디자이너 좌(탭패널) + 우(팬캔버스), 반응형 편집
12 데이터 구조 PopLayoutData, mobileOverride
13 컴포넌트 재사용성 2개 재사용, 4개 부분, 7개 신규

1. 아키텍처

결정: Option B (레이아웃 기반 구분)

screen_definitions (공용)
    ├── screen_layouts_v2 (데스크톱)
    └── screen_layouts_pop (POP)

선택 이유: 기존 테이블 변경 없음, 데스크톱 영향 없음, 향후 통합 가능


2. 데이터베이스

테이블: screen_layouts_pop

컬럼 타입 설명
id SERIAL PK
screen_id INTEGER 화면 ID (unique)
layout_data JSONB 컴포넌트 JSON

특이사항: FK 없음 (soft-delete 지원)

파일: db/migrations/052_create_screen_layouts_pop.sql


3. 백엔드 API

Method Endpoint 용도
GET /api/screen-management/layout-pop/:screenId 조회
POST /api/screen-management/layout-pop/:screenId 저장
DELETE /api/screen-management/layout-pop/:screenId 삭제
GET /api/screen-management/pop-layout-screen-ids ID 목록

파일: backend-node/src/services/screenManagementService.ts


4. 프론트엔드 API

파일: frontend/lib/api/screen.ts

screenApi.getLayoutPop(screenId)           // 조회
screenApi.saveLayoutPop(screenId, data)    // 저장
screenApi.deleteLayoutPop(screenId)        // 삭제
screenApi.getScreenIdsWithPopLayout()      // ID 목록

5. 관리 페이지

파일: frontend/app/(main)/admin/screenMng/popScreenMngList/page.tsx

핵심 로직:

const popIds = await screenApi.getScreenIdsWithPopLayout();
const filteredScreens = screens.filter(s => new Set(popIds).has(s.screenId));

기능: POP 화면만 표시, 새 POP 화면 생성):, 보기/설계 버튼


6. 뷰어

파일: frontend/app/(pop)/pop/screens/[screenId]/page.tsx

URL 파라미터:

파라미터 설명
preview true 툴바 표시
device mobile/tablet 디바이스 크기 (기본: tablet)

디바이스 크기: mobile(375x812), tablet(768x1024)


7. 미리보기

핵심: isPop prop으로 URL 분기

popScreenMngList
    └─► ScreenRelationFlow(isPop=true)
            └─► ScreenSettingModal
                    └─► PreviewTab → /pop/screens/{id}

screenMngList (데스크톱)
    └─► ScreenRelationFlow(isPop=false 기본값)
            └─► ScreenSettingModal
                    └─► PreviewTab → /screens/{id}

안전성: isPop 기본값 = false → 데스크톱 영향 없음


8. 파일 목록

생성 (3개)

파일 용도
db/migrations/052_create_screen_layouts_pop.sql DB 스키마
frontend/app/(pop)/pop/screens/[screenId]/page.tsx POP 뷰어
frontend/app/(main)/admin/screenMng/popScreenMngList/page.tsx POP 관리

수정 (9개)

파일 변경 내용
backend-node/src/services/screenManagementService.ts POP CRUD 함수
backend-node/src/controllers/screenManagementController.ts 컨트롤러
backend-node/src/routes/screenManagementRoutes.ts 라우트
frontend/lib/api/screen.ts API 클라이언트
frontend/components/screen/CreateScreenModal.tsx isPop prop
frontend/components/screen/ScreenSettingModal.tsx isPop, PreviewTab
frontend/components/screen/ScreenRelationFlow.tsx isPop 전달
frontend/components/screen/ScreenDesigner.tsx isPop, 미리보기
frontend/components/screen/toolbar/SlimToolbar.tsx POP 미리보기 버튼

9. 반응형 전략 (신규 결정사항)

문제점

  • 데스크톱은 절대 좌표(position: { x, y }) 사용
  • 모바일 화면 크기가 달라지면 레이아웃 깨짐

결정: Flow 레이아웃 채택

항목 데스크톱 POP
배치 방식 position: { x, y } order: number (순서)
컨테이너 자유 배치 중첩 구조 (섹션 > 필드)
렌더러 절대 좌표 계산 Flexbox column (세로 쌓기)

Flow 레이아웃 데이터 구조

{
  layoutMode: "flow",  // flow | absolute
  components: [
    {
      id: "section-1",
      type: "pop-section",
      order: 0,  // 순서로 배치
      children: [...]
    }
  ]
}

10. POP 사용자 앱 구조 (신규 결정사항)

데스크톱 vs POP 진입 구조

데스크톱 POP
메뉴 왼쪽 사이드바 대시보드 카드
네비게이션 복잡한 트리 구조 화면 → 뒤로가기
URL /screens/{id} /pop/screens/{id}

POP 화면 흐름

/pop/login (POP 로그인)
    ↓
/pop/dashboard (화면 목록 - 카드형)
    ↓
/pop/screens/{id} (화면 뷰어)

11. POP 디자이너 (신규 계획)

진입 경로

popScreenMngList → [설계] 버튼 → PopDesigner 컴포넌트

레이아웃 구조 (2026-02-02 수정)

데스크톱 Screen Designer와 유사하게 좌측 탭 패널 + 우측 캔버스:

┌─────────────────────────────────────────────────────────────────┐
│  [툴바] ← 목록 | 화면명 | 📱모바일 📱태블릿 | 🔄 | 💾저장     │
├────────────────┬────────────────────────────────────────────────┤
│    [패널]      │              [캔버스 영역]                      │
│    ◀━━━━▶     │                                                │
│   (리사이즈)   │    ┌────────────────────────┐                 │
│                │    │   디바이스 프레임       │ ← 드래그로      │
│ ┌────────────┐ │    │                        │   팬 이동       │
│ │컴포넌트│편집│ │    │  [섹션 1]              │                 │
│ └────────────┘ │    │    ├─ 필드 A           │                 │
│                │    │    └─ 필드 B           │                 │
│  (컴포넌트 탭) │    │                        │                 │
│  📦 섹션       │    │  [섹션 2]              │                 │
│  📝 필드       │    │    ├─ 버튼1 ─ 버튼2    │                 │
│  🔘 버튼       │    │                        │                 │
│  📋 리스트     │    └────────────────────────┘                 │
│  📊 인디케이터 │                                                │
│                │                                                │
│  (편집 탭)     │                                                │
│  선택된 컴포   │                                                │
│  넌트 설정     │                                                │
└────────────────┴────────────────────────────────────────────────┘

패널 기능

기능 설명
리사이즈 드래그로 패널 너비 조절 (min: 200px, max: 400px)
컴포넌트 탭 POP 전용 컴포넌트만 표시
편집 탭 선택된 컴포넌트 설정 (프리셋 기반)

캔버스 기능

기능 설명
팬(Pan) 마우스 드래그로 보는 위치 이동
마우스 휠로 확대/축소 (선택사항)
디바이스 탭 📱모바일 / 📱태블릿 전환
나란히 보기 옵션으로 둘 다 표시 가능
실시간 미리보기 편집 = 미리보기 (별도 창 불필요)

캔버스 방식: 블록 쌓기

  • 섹션끼리는 위→아래로 쌓임
  • 섹션 안에서는 가로(row) 또는 세로(column) 선택 가능
  • 드래그앤드롭으로 순서 변경
  • 캔버스 자체가 실시간 미리보기

기준 해상도

디바이스 논리적 크기 (dp) 용도
모바일 360 x 640 Zebra TC52/57 등 산업용 핸드헬드
태블릿 768 x 1024 8~10인치 산업용 태블릿

터치 타겟 (장갑 착용 고려)

  • 최소 버튼 크기: 60dp (일반 앱 48dp보다 큼)
  • 버튼 간격: 16dp 이상

반응형 편집 방식

모드 설명
기준 디바이스 태블릿 (메인 편집)
자동 조정 CSS flex-wrap, grid로 모바일 자동 줄바꿈
수동 조정 모바일 탭에서 그리드 열 수, 숨기기 설정

흐름:

1. 태블릿 탭에서 편집 (기준)
   → 모든 컴포넌트, 섹션, 순서, 데이터 바인딩 설정

2. 모바일 탭에서 확인
   A) 자동 조정 OK → 그대로 저장
   B) 배치 어색함 → 그리드 열 수 조정 또는 숨기기

섹션 내 컴포넌트 배치 옵션

설정 옵션
배치 방향 row / column
순서 드래그로 변경
비율 flex (1:1, 2:1, 1:2 등)
정렬 start / center / end
간격 none / small / medium / large
줄바꿈 wrap / nowrap
그리드 열 수 태블릿용, 모바일용 각각 설정 가능

관리자가 설정 가능한 것

항목 설정 방식
섹션 순서 드래그로 위/아래 이동
섹션 내 배치 가로(row) / 세로(column)
정렬 왼쪽/가운데/오른쪽, 위/가운데/아래
컴포넌트 비율 1:1, 2:1, 1:2 등 (flex)
크기 S/M/L/XL 프리셋
여백/간격 작음/보통/넓음 프리셋
아이콘 선택 가능
테마/색상 프리셋 또는 커스텀
그리드 열 수 태블릿/모바일 각각
모바일 숨기기 특정 컴포넌트 숨김

관리자가 설정 불가능한 것 (반응형 유지)

  • 정확한 x, y 좌표
  • 정확한 픽셀 크기 (예: 347px)
  • 고정 위치 (예: 왼쪽에서 100px)

스타일 분리 원칙

뼈대 (변경 어려움 - 처음부터 잘 설계):
- 데이터 바인딩 구조 (columnName, dataSource)
- 컴포넌트 계층 (섹션 > 필드)
- 액션 로직

옷 (변경 쉬움 - 나중에 조정 가능):
- 색상, 폰트 크기 → CSS 변수/테마
- 버튼 모양 → 프리셋
- 아이콘 → 선택

다국어 연동 (준비)

  • 상태: showMultilangSettingsModal 미리 추가
  • 버튼: 툴바에 자리만 (비활성)
  • 연결: 추후 MultilangSettingsModal import

데스크톱 시스템 재사용

기능 재사용 비고
formData 관리 O 그대로
필드간 연결 O cascading, hierarchy
테이블 참조 O dataSource, filter
저장 이벤트 O beforeFormSave
집계 O 스타일만 변경
설정 패널 O 탭 방식 참고
CRUD API O 그대로
buttonActions O 그대로
다국어 O MultilangSettingsModal

파일 구조 (신규 생성 예정)

frontend/components/pop/
├── PopDesigner.tsx          # 메인 (좌: 패널, 우: 캔버스)
├── PopCanvas.tsx            # 캔버스 (팬/줌 + 프레임)
├── PopToolbar.tsx           # 상단 툴바
│
├── panels/
│   └── PopPanel.tsx         # 통합 패널 (컴포넌트/편집 탭)
│
├── components/              # POP 전용 컴포넌트
│   ├── PopSection.tsx
│   ├── PopField.tsx
│   ├── PopButton.tsx
│   └── ...
│
└── types/
    └── pop-layout.ts        # PopLayoutData, PopComponentData

12. POP 레이아웃 데이터 구조 (신규)

PopLayoutData

interface PopLayoutData {
  version: "pop-1.0";
  layoutMode: "flow";  // 항상 flow (절대좌표 없음)
  deviceTarget: "mobile" | "tablet" | "both";
  components: PopComponentData[];
}

PopComponentData

interface PopComponentData {
  id: string;
  type: "pop-section" | "pop-field" | "pop-button" | "pop-list" | "pop-indicator";
  order: number;  // 순서 (x, y 좌표 대신)
  
  // 개별 컴포넌트 flex 비율
  flex?: number;  // 기본 1
  
  // 섹션인 경우: 내부 레이아웃 설정
  layout?: {
    direction: "row" | "column";
    justify: "start" | "center" | "end" | "between";
    align: "start" | "center" | "end";
    gap: "none" | "small" | "medium" | "large";
    wrap: boolean;
    grid?: number;  // 태블릿 기준 열 수
  };
  
  // 크기 프리셋
  size?: "S" | "M" | "L" | "XL" | "full";
  
  // 데이터 바인딩
  dataBinding?: {
    tableName: string;
    columnName: string;
    displayField?: string;
  };
  
  // 스타일 프리셋
  style?: {
    variant: "default" | "primary" | "success" | "warning" | "danger";
    padding: "none" | "small" | "medium" | "large";
  };
  
  // 모바일 오버라이드 (선택사항)
  mobileOverride?: {
    grid?: number;      // 모바일 열 수 (없으면 자동)
    hidden?: boolean;   // 모바일에서 숨기기
  };
  
  // 하위 컴포넌트 (섹션 내부)
  children?: PopComponentData[];
  
  // 컴포넌트별 설정
  config?: Record<string, any>;
}

데스크톱 vs POP 데이터 비교

항목 데스크톱 (LayoutData) POP (PopLayoutData)
배치 position: { x, y, z } order: number
크기 size: { width, height } (픽셀) `size: "S"
컨테이너 없음 (자유 배치) layout: { direction, grid }
반응형 없음 mobileOverride

13. 컴포넌트 재사용성 분석

최종 분류

분류 개수 컴포넌트
완전 재사용 2 form-field, action-button
부분 재사용 4 tab-panel, data-table, kpi-gauge, process-flow
신규 개발 7 section, card-list, status-indicator, number-pad, barcode-scanner, timer, alarm-list

핵심 컴포넌트 7개 (최소 필수)

컴포넌트 역할 포함 기능
pop-section 레이아웃 컨테이너 카드, 그룹핑, 접기/펼치기
pop-field 데이터 입력/표시 텍스트, 숫자, 드롭다운, 바코드, 숫자패드
pop-button 액션 실행 저장, 삭제, API 호출, 화면이동
pop-list 데이터 목록 카드리스트, 선택목록, 테이블 참조
pop-indicator 상태/수치 표시 KPI, 게이지, 신호등, 진행률
pop-scanner 바코드/QR 입력 카메라, 외부 스캐너
pop-numpad 숫자 입력 특화 큰 버튼, 계산기 모드

TODO

Phase 1: POP 디자이너 개발 (현재 진행)

# 작업 설명 상태
1 PopLayoutData 타입 정의 order, layout, mobileOverride 완료
2 PopDesigner.tsx 좌: 리사이즈 패널, 우: 팬 가능 캔버스 완료
3 PopPanel.tsx 탭 (컴포넌트/편집), POP 컴포넌트만 완료
4 PopCanvas.tsx 팬/줌 + 디바이스 프레임 + 블록 렌더링 완료
5 SectionGrid.tsx 섹션 내부 컴포넌트 배치 (react-grid-layout) 완료
6 드래그앤드롭 팔레트→캔버스 (섹션), 팔레트→섹션 (컴포넌트) 완료
7 컴포넌트 자유 배치/리사이즈 고정 셀 크기(40px) 기반 자동 그리드 완료
8 편집 탭 그리드 설정, 모바일 오버라이드 완료 (기본)
9 저장/로드 기존 API 재사용 (saveLayoutPop) 완료

Phase 2: POP 컴포넌트 개발

상세: docs/pop/components-spec.md

1단계 (우선):

  • pop-section (레이아웃 컨테이너)
  • pop-field (범용 입력)
  • pop-button (액션)

2단계:

  • pop-list (카드형 목록)
  • pop-indicator (상태/KPI)
  • pop-numpad (숫자패드)

3단계:

  • pop-scanner (바코드)
  • pop-timer (타이머)
  • pop-alarm (알람)

Phase 3: POP 사용자 앱

  • /pop/login - POP 전용 로그인
  • /pop/dashboard - 화면 목록 (카드형)
  • /pop/screens/[id] - Flow 렌더러 적용

기타

  • POP 컴포넌트 레지스트리
  • POP 메뉴/폴더 관리
  • POP 인증 분리
  • 다국어 연동

핵심 파일 참조

기존 파일 (참고용)

파일 용도
frontend/app/(main)/admin/screenMng/popScreenMngList/page.tsx 진입점, PopDesigner 호출 위치
frontend/components/screen/ScreenDesigner.tsx 데스크톱 디자이너 (구조 참고)
frontend/components/screen/modals/MultilangSettingsModal.tsx 다국어 모달 (추후 연동)
frontend/lib/api/screen.ts API (getLayoutPop, saveLayoutPop)
backend-node/src/services/screenManagementService.ts POP CRUD (4720~4920행)

신규 생성 예정

파일 용도
frontend/components/pop/PopDesigner.tsx 메인 디자이너
frontend/components/pop/PopCanvas.tsx 캔버스 (팬/줌)
frontend/components/pop/PopToolbar.tsx 툴바
frontend/components/pop/panels/PopPanel.tsx 통합 패널
frontend/components/pop/types/pop-layout.ts 타입 정의
frontend/components/pop/components/PopSection.tsx 섹션 컴포넌트


14. 그리드 시스템 단순화 (2026-02-02 변경)

기존 문제: 이중 그리드 구조

캔버스 (24열, rowHeight 20px)
  └─ 섹션 (colSpan/rowSpan으로 크기 지정)
       └─ 내부 그리드 (columns/rows로 컴포넌트 배치)

문제점:

  1. 섹션 크기와 내부 그리드가 독립적이라 동기화 안됨
  2. 섹션을 늘려도 내부 그리드 점은 그대로 (비례 확대만)
  3. 사용자가 두 가지 단위를 이해해야 함

변경: 단일 자동계산 그리드

핵심 변경사항:

  • 그리드 점(dot) 제거
  • 고정 셀 크기(40px) 기반으로 섹션 크기에 따라 열/행 수 자동 계산
  • 컴포넌트는 react-grid-layout으로 자유롭게 드래그/리사이즈

코드 (SectionGrid.tsx):

const CELL_SIZE = 40;
const cols = Math.max(1, Math.floor((availableWidth + gap) / (CELL_SIZE + gap)));
const rows = Math.max(1, Math.floor((availableHeight + gap) / (CELL_SIZE + gap)));

결과:

  • 섹션 크기 변경 → 내부 셀 개수 자동 조정
  • 컴포넌트 자유 배치/리사이즈 가능
  • 직관적인 사용자 경험

onLayoutChange 대신 onDragStop/onResizeStop 사용

문제: onLayoutChange는 드롭 직후에도 호출되어 섹션 크기가 자동 확대됨

해결:

// 변경 전
<GridLayout onLayoutChange={handleLayoutChange} ... />

// 변경 후
<GridLayout onDragStop={handleDragResizeStop} onResizeStop={handleDragResizeStop} ... />

상태 업데이트는 드래그/리사이즈 완료 후에만 실행


POP 화면 관리 페이지 개발 (2026-02-02)

POP 카테고리 트리 API 구현

기능:

  • POP 화면을 카테고리별로 관리하는 트리 구조 구현
  • 기존 screen_groups 테이블을 hierarchy_path LIKE 'POP/%' 조건으로 필터링하여 재사용
  • 데스크탑 화면 관리와 별도로 POP 전용 카테고리 체계 구성

백엔드 API:

  • GET /api/screen-groups/pop/groups - POP 그룹 목록 조회
  • POST /api/screen-groups/pop/groups - POP 그룹 생성
  • PUT /api/screen-groups/pop/groups/:id - POP 그룹 수정
  • DELETE /api/screen-groups/pop/groups/:id - POP 그룹 삭제
  • POST /api/screen-groups/pop/ensure-root - POP 루트 그룹 자동 생성

트러블슈팅: API 경로 중복 문제

문제: 카테고리 생성 시 404 에러 발생

원인:

  • apiClient의 baseURL이 이미 http://localhost:8080/api로 설정됨
  • API 호출 경로에 /api/screen-groups/...를 사용하여 최종 URL이 /api/api/screen-groups/...로 중복

해결:

// 변경 전
const response = await apiClient.post("/api/screen-groups/pop/groups", data);

// 변경 후
const response = await apiClient.post("/screen-groups/pop/groups", data);

트러블슈팅: created_by 컬럼 오류

문제: column "created_by" of relation "screen_groups" does not exist

원인:

  • 신규 작성 코드에서 created_by 컬럼을 사용했으나
  • 기존 screen_groups 테이블 스키마에는 writer 컬럼이 존재

해결:

-- 변경 전
INSERT INTO screen_groups (..., created_by) VALUES (..., $9)

-- 변경 후
INSERT INTO screen_groups (..., writer) VALUES (..., $9)

트러블슈팅: is_active 컬럼 타입 불일치

문제: value too long for type character varying(1) 에러로 카테고리 생성 실패

원인:

  • is_active 컬럼이 VARCHAR(1) 타입
  • INSERT 쿼리에서 true(boolean, 4자)를 직접 사용

해결:

-- 변경 전
INSERT INTO screen_groups (..., is_active) VALUES (..., true)

-- 변경 후  
INSERT INTO screen_groups (..., is_active) VALUES (..., 'Y')

교훈:

  • 기존 테이블 스키마를 반드시 확인 후 쿼리 작성
  • is_activeVARCHAR(1) 타입으로 'Y'/'N' 값 사용
  • created_by 대신 writer 컬럼명 사용

카테고리 트리 UI 개선

문제: 하위 폴더와 상위 폴더의 계층 관계가 시각적으로 불명확

해결:

  1. 들여쓰기 증가: level * 16pxlevel * 24px
  2. 트리 연결 표시: "ㄴ" 문자로 하위 항목 명시
  3. 루트 폴더 강조: 주황색 아이콘 + 볼드 텍스트, 하위는 노란색 아이콘
// 하위 레벨에 연결 표시 추가
{level > 0 && (
  <span className="text-muted-foreground/50 text-xs mr-1"></span>
)}

// 루트와 하위 폴더 시각적 구분
<Folder className={cn("h-4 w-4 shrink-0", isRootLevel ? "text-orange-500" : "text-amber-500")} />
<span className={cn("flex-1 text-sm truncate", isRootLevel && "font-semibold")}>{group.group_name}</span>

미분류 화면 이동 기능 추가

기능: 미분류 화면을 특정 카테고리로 이동하는 드롭다운 메뉴

구현:

// 이동 드롭다운 메뉴
<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" size="sm">
      <MoveRight className="h-3 w-3 mr-1" />
      이동
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    {treeData.map((g) => (
      <DropdownMenuItem onClick={() => handleMoveScreenToGroup(screen, g)}>
        <Folder className="h-4 w-4 mr-2" />
        {g.group_name}
      </DropdownMenuItem>
    ))}
  </DropdownMenuContent>
</DropdownMenu>

// API 호출 (apiClient 사용)
const handleMoveScreenToGroup = async (screen, group) => {
  await apiClient.post("/screen-groups/group-screens", { 
    group_id: group.id, 
    screen_id: screen.screenId,
    screen_role: "main",
    display_order: 0,
    is_default: false,
  });
};

주의: API 호출 시 apiClient를 사용해야 환경별 URL이 자동 처리됨

화면 이동 로직 수정 (복사 → 이동)

문제: 화면을 다른 카테고리로 이동할 때 복사가 되어 중복 발생

원인: 기존 그룹 연결 삭제 없이 새 그룹에만 연결 추가

해결: 2단계 처리 - 기존 연결 삭제 후 새 연결 추가

const handleMoveScreenToGroup = async (screen: ScreenDefinition, targetGroup: PopScreenGroup) => {
  // 1. 기존 연결 찾기 및 삭제
  for (const g of groups) {
    const existingLink = g.screens?.find((s) => s.screen_id === screen.screenId);
    if (existingLink) {
      await apiClient.delete(`/screen-groups/group-screens/${existingLink.id}`);
      break;
    }
  }
  
  // 2. 새 그룹에 연결 추가
  await apiClient.post("/screen-groups/group-screens", { 
    group_id: targetGroup.id, 
    screen_id: screen.screenId,
    screen_role: "main",
    display_order: 0,
    is_default: false,
  });
  
  loadGroups(); // 목록 새로고침
};

화면/카테고리 메뉴 UI 개선

변경 사항:

  1. 화면에 "..." 더보기 메뉴 추가 (폴더와 동일한 스타일)
  2. 메뉴 항목: 설계, 위로 이동, 아래로 이동, 다른 카테고리로 이동, 그룹에서 제거
  3. 폴더 메뉴에도 위로/아래로 이동 추가

순서 변경 구현:

// 그룹 순서 변경 (display_order 교환)
const handleMoveGroupUp = async (targetGroup: PopScreenGroup) => {
  const siblingGroups = groups
    .filter((g) => g.parent_id === targetGroup.parent_id)
    .sort((a, b) => (a.display_order || 0) - (b.display_order || 0));
  
  const currentIndex = siblingGroups.findIndex((g) => g.id === targetGroup.id);
  if (currentIndex <= 0) return;

  const prevGroup = siblingGroups[currentIndex - 1];
  
  await Promise.all([
    apiClient.put(`/screen-groups/groups/${targetGroup.id}`, { display_order: prevGroup.display_order }),
    apiClient.put(`/screen-groups/groups/${prevGroup.id}`, { display_order: targetGroup.display_order }),
  ]);
  
  loadGroups();
};

// 화면 순서 변경 (screen_group_screens의 display_order 교환)
const handleMoveScreenUp = async (screen: ScreenDefinition, groupId: number) => {
  const targetGroup = groups.find((g) => g.id === groupId);
  const sortedScreens = [...targetGroup.screens].sort((a, b) => a.display_order - b.display_order);
  const currentIndex = sortedScreens.findIndex((s) => s.screen_id === screen.screenId);
  
  if (currentIndex <= 0) return;

  const currentLink = sortedScreens[currentIndex];
  const prevLink = sortedScreens[currentIndex - 1];
  
  await Promise.all([
    apiClient.put(`/screen-groups/group-screens/${currentLink.id}`, { display_order: prevLink.display_order }),
    apiClient.put(`/screen-groups/group-screens/${prevLink.id}`, { display_order: currentLink.display_order }),
  ]);
  
  loadGroups();
};

카테고리 이동 모달 (서브메뉴 → 모달 방식)

문제: 카테고리가 많아지면 서브메뉴 방식은 관리 어려움

해결: 검색 기능이 있는 모달로 변경

구현:

// 이동 모달 상태
const [isMoveModalOpen, setIsMoveModalOpen] = useState(false);
const [movingScreen, setMovingScreen] = useState<ScreenDefinition | null>(null);
const [movingFromGroupId, setMovingFromGroupId] = useState<number | null>(null);
const [moveSearchTerm, setMoveSearchTerm] = useState("");

// 필터링된 그룹 목록
const filteredMoveGroups = useMemo(() => {
  if (!moveSearchTerm) return flattenedGroups;
  const searchLower = moveSearchTerm.toLowerCase();
  return flattenedGroups.filter((g) => 
    (g._displayName || g.group_name).toLowerCase().includes(searchLower)
  );
}, [flattenedGroups, moveSearchTerm]);

// 모달 UI 특징:
// 1. 검색 입력창 (Search 아이콘 포함)
// 2. 트리 구조 표시 (depth에 따라 들여쓰기)
// 3. 현재 소속 그룹 표시 및 선택 불가 처리
// 4. ScrollArea로 긴 목록 스크롤 지원

모달 구조:

┌─────────────────────────────┐
│ 카테고리로 이동              │
│ "화면명" 화면을 이동할...    │
├─────────────────────────────┤
│ 🔍 카테고리 검색...          │
├─────────────────────────────┤
│ 📁 POP 화면                  │
│   📁 홈 관리                 │
│     📁 출고관리              │
│     📁 수주관리              │
│   📁 생산 관리 (현재)        │
├─────────────────────────────┤
│                    [ 취소 ] │
└─────────────────────────────┘

14. 비율 기반 그리드 시스템 (2026-02-03)

문제 발견

POP 디자이너에서 섹션을 크게 설정해도 뷰어에서 매우 얇게(약 20px) 렌더링되는 문제 발생.

근본 원인 분석

  1. 기존 구조: canvasGrid.rowHeight = 20 (고정 픽셀)
  2. react-grid-layout 동작: 작은 리사이즈 → rowSpan: 1로 반올림 → DB 저장
  3. 뷰어 렌더링: gridAutoRows: 20px → 섹션 높이 = 20px (매우 얇음)
  4. 비교: 가로(columns)는 1fr 비율 기반으로 잘 작동

해결책: 비율 기반 행 시스템

구분 이전 이후
타입 rowHeight: number (px) rows: number (개수)
기본값 rowHeight: 20 rows: 24
뷰어 CSS gridAutoRows: 20px gridTemplateRows: repeat(24, 1fr)
디자이너 계산 고정 20px resolution.height / 24

수정된 파일

파일 변경 내용
types/pop-layout.ts PopCanvasGrid.rowHeightrows, DEFAULT_CANVAS_GRID.rows = 24
renderers/PopLayoutRenderer.tsx gridAutoRowsgridTemplateRows: repeat(rows, 1fr)
PopCanvas.tsx rowHeight = Math.floor(resolution.height / canvasGrid.rows)

모드별 행 높이 계산

모드 해상도 높이 행 높이 (24행 기준)
tablet_landscape 768px 32px
tablet_portrait 1024px 42.7px
mobile_landscape 375px 15.6px
mobile_portrait 667px 27.8px

기존 데이터 호환성

  • 기존 rowHeight: 20 데이터는 rows || 24 fallback으로 처리
  • 기존 rowSpan: 1 데이터는 1/24 = 4.17%로 렌더링 (여전히 작음)
  • 권장: 디자이너에서 섹션 재조정 후 재저장

15. 화면 삭제 기능 추가 (2026-02-03)

추가된 기능

POP 카테고리 트리에서 화면 자체를 삭제하는 기능 추가.

UI 변경

위치 메뉴 항목 동작
그룹 내 화면 드롭다운 "화면 삭제" 휴지통으로 이동
미분류 화면 드롭다운 "화면 삭제" 휴지통으로 이동

삭제 흐름

1. 드롭다운 메뉴에서 "화면 삭제" 클릭
2. 확인 다이얼로그 표시 ("삭제된 화면은 휴지통으로 이동됩니다")
3. 확인 → DELETE /api/screen-management/screens/:id
4. 화면 is_deleted = 'Y'로 변경 (soft delete)
5. 그룹 목록 새로고침

완전 삭제 vs 휴지통 이동

API 동작 복원 가능
DELETE /screens/:id 휴지통으로 이동 (is_deleted='Y') O
DELETE /screens/:id/permanent DB에서 완전 삭제 X

수정된 파일

파일 변경 내용
PopCategoryTree.tsx handleDeleteScreen, confirmDeleteScreen 함수 추가
PopCategoryTree.tsx isScreenDeleteDialogOpen, deletingScreen 상태 추가
PopCategoryTree.tsx TreeNode에 onDeleteScreen prop 추가
PopCategoryTree.tsx 화면 삭제 확인 AlertDialog 추가

16. 멀티테넌시 이슈 해결 (2026-02-03)

문제

화면 그룹에서 제거 시 404 에러 발생.

원인

  • DB 데이터: company_code = "*" (최고 관리자 전용)
  • 현재 세션: company_code = "COMPANY_7"
  • 컨트롤러 WHERE 조건: id = $1 AND company_code = $2 → 0 rows

해결

세션 불일치 문제로 DB에서 직접 삭제 처리.

교훈

  • 최고 관리자로 생성한 데이터는 일반 회사 사용자가 삭제 불가
  • 로그인 후 토큰 갱신 필요 시 브라우저 완전 새로고침

트러블슈팅

Export default doesn't exist in target module

문제: import apiClient from "@/lib/api/client" 에러

원인: apiClient가 named export로 정의됨

해결: import { apiClient } from "@/lib/api/client" 사용

섹션이 매우 얇게 렌더링되는 문제

문제: 디자이너에서 크게 설정한 섹션이 뷰어에서 20px 높이로 표시

원인: canvasGrid.rowHeight = 20 고정값 + react-grid-layout의 rowSpan 반올림

해결: 비율 기반 rows 시스템으로 변경 (섹션 14 참조)

화면 삭제 404 에러

문제: 화면 그룹에서 제거 시 404 에러

원인: company_code 불일치 (세션 vs DB)

해결: 브라우저 새로고침으로 토큰 갱신 또는 DB 직접 처리

관련 파일

파일 역할
frontend/components/pop/management/PopCategoryTree.tsx POP 카테고리 트리 (전체 UI)
frontend/lib/api/popScreenGroup.ts POP 그룹 API 클라이언트
backend-node/src/controllers/screenGroupController.ts 그룹 CRUD 컨트롤러
backend-node/src/routes/screenGroupRoutes.ts 그룹 API 라우트
frontend/components/pop/designer/types/pop-layout.ts POP 레이아웃 타입 정의
frontend/components/pop/designer/renderers/PopLayoutRenderer.tsx CSS Grid 기반 렌더러
frontend/components/pop/designer/PopCanvas.tsx react-grid-layout 디자이너 캔버스

최종 업데이트: 2026-02-03