# POP 화면 관리 시스템 개발 기록 > **AI 에이전트 안내**: 이 문서는 Progressive Disclosure 방식으로 구성되어 있습니다. > 1. 먼저 [Quick Reference](#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 | [아키텍처](#1-아키텍처) | 레이아웃 테이블로 POP/데스크톱 분리 | | 2 | [데이터베이스](#2-데이터베이스) | screen_layouts_pop 테이블 (FK 없음) | | 3 | [백엔드 API](#3-백엔드-api) | CRUD 4개 엔드포인트 | | 4 | [프론트엔드 API](#4-프론트엔드-api) | screenApi에 4개 함수 추가 | | 5 | [관리 페이지](#5-관리-페이지) | POP 화면만 필터링하여 표시 | | 6 | [뷰어](#6-뷰어) | 모바일/태블릿 프레임 미리보기 | | 7 | [미리보기](#7-미리보기) | isPop prop으로 URL 분기 | | 8 | [파일 목록](#8-파일-목록) | 생성 3개, 수정 9개 | | 9 | [반응형 전략](#9-반응형-전략-신규-결정사항) | Flow 레이아웃 (세로 쌓기) 채택 | | 10 | [POP 사용자 앱](#10-pop-사용자-앱-구조-신규-결정사항) | 대시보드 카드 → 화면 뷰어 | | 11 | [POP 디자이너](#11-pop-디자이너-신규-계획) | 좌(탭패널) + 우(팬캔버스), 반응형 편집 | | 12 | [데이터 구조](#12-pop-레이아웃-데이터-구조-신규) | PopLayoutData, mobileOverride | | 13 | [컴포넌트 재사용성](#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` ```typescript screenApi.getLayoutPop(screenId) // 조회 screenApi.saveLayoutPop(screenId, data) // 저장 screenApi.deleteLayoutPop(screenId) // 삭제 screenApi.getScreenIdsWithPopLayout() // ID 목록 ``` --- ## 5. 관리 페이지 **파일**: `frontend/app/(main)/admin/screenMng/popScreenMngList/page.tsx` **핵심 로직**: ```typescript 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 레이아웃 데이터 구조 ```typescript { 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 ```typescript interface PopLayoutData { version: "pop-1.0"; layoutMode: "flow"; // 항상 flow (절대좌표 없음) deviceTarget: "mobile" | "tablet" | "both"; components: PopComponentData[]; } ``` ### PopComponentData ```typescript 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; } ``` ### 데스크톱 vs POP 데이터 비교 | 항목 | 데스크톱 (LayoutData) | POP (PopLayoutData) | |-----|----------------------|---------------------| | 배치 | `position: { x, y, z }` | `order: number` | | 크기 | `size: { width, height }` (픽셀) | `size: "S" | "M" | "L"` (프리셋) | | 컨테이너 | 없음 (자유 배치) | `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):** ```typescript 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는 드롭 직후에도 호출되어 섹션 크기가 자동 확대됨 **해결:** ```typescript // 변경 전 // 변경 후 ``` 상태 업데이트는 드래그/리사이즈 완료 후에만 실행 --- ## 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/...`로 중복 **해결:** ```typescript // 변경 전 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` 컬럼이 존재 **해결:** ```sql -- 변경 전 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자)를 직접 사용 **해결:** ```sql -- 변경 전 INSERT INTO screen_groups (..., is_active) VALUES (..., true) -- 변경 후 INSERT INTO screen_groups (..., is_active) VALUES (..., 'Y') ``` **교훈:** - 기존 테이블 스키마를 반드시 확인 후 쿼리 작성 - `is_active`는 `VARCHAR(1)` 타입으로 'Y'/'N' 값 사용 - `created_by` 대신 `writer` 컬럼명 사용 ### 카테고리 트리 UI 개선 **문제:** 하위 폴더와 상위 폴더의 계층 관계가 시각적으로 불명확 **해결:** 1. 들여쓰기 증가: `level * 16px` → `level * 24px` 2. 트리 연결 표시: "ㄴ" 문자로 하위 항목 명시 3. 루트 폴더 강조: 주황색 아이콘 + 볼드 텍스트, 하위는 노란색 아이콘 ```tsx // 하위 레벨에 연결 표시 추가 {level > 0 && ( )} // 루트와 하위 폴더 시각적 구분 {group.group_name} ``` ### 미분류 화면 이동 기능 추가 **기능:** 미분류 화면을 특정 카테고리로 이동하는 드롭다운 메뉴 **구현:** ```tsx // 이동 드롭다운 메뉴 {treeData.map((g) => ( handleMoveScreenToGroup(screen, g)}> {g.group_name} ))} // 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단계 처리 - 기존 연결 삭제 후 새 연결 추가 ```tsx 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. 폴더 메뉴에도 위로/아래로 이동 추가 **순서 변경 구현:** ```tsx // 그룹 순서 변경 (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(); }; ``` ### 카테고리 이동 모달 (서브메뉴 → 모달 방식) **문제:** 카테고리가 많아지면 서브메뉴 방식은 관리 어려움 **해결:** 검색 기능이 있는 모달로 변경 **구현:** ```tsx // 이동 모달 상태 const [isMoveModalOpen, setIsMoveModalOpen] = useState(false); const [movingScreen, setMovingScreen] = useState(null); const [movingFromGroupId, setMovingFromGroupId] = useState(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.rowHeight` → `rows`, `DEFAULT_CANVAS_GRID.rows = 24` | | `renderers/PopLayoutRenderer.tsx` | `gridAutoRows` → `gridTemplateRows: 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*