diff --git a/popdocs/ARCHITECTURE.md b/popdocs/ARCHITECTURE.md new file mode 100644 index 00000000..8acbf24b --- /dev/null +++ b/popdocs/ARCHITECTURE.md @@ -0,0 +1,286 @@ +# POP 화면 시스템 아키텍처 + +**최종 업데이트: 2026-02-06 (v5.2 브레이크포인트 재설계 + 세로 자동 확장)** + +POP(Point of Production) 화면은 모바일/태블릿 환경에 최적화된 터치 기반 화면 시스템입니다. + +--- + +## 현재 버전: v5 (CSS Grid) + +| 항목 | v5 (현재) | +|------|----------| +| 레이아웃 | CSS Grid | +| 배치 방식 | 좌표 기반 (col, row, colSpan, rowSpan) | +| 모드 | 4개 (mobile_portrait, mobile_landscape, tablet_portrait, tablet_landscape) | +| 칸 수 | 4/6/8/12칸 | + +--- + +## 폴더 구조 + +``` +frontend/ +├── app/(pop)/ # Next.js App Router +│ ├── layout.tsx # POP 전용 레이아웃 +│ └── pop/ +│ ├── page.tsx # 대시보드 +│ ├── screens/[screenId]/ # 화면 뷰어 (v5) +│ └── work/ # 작업 화면 +│ +├── components/pop/ # POP 컴포넌트 +│ ├── designer/ # 디자이너 모듈 ★ +│ │ ├── PopDesigner.tsx # 메인 (레이아웃 로드/저장) +│ │ ├── PopCanvas.tsx # 캔버스 (DnD, 줌, 모드) +│ │ ├── panels/ +│ │ │ └── ComponentEditorPanel.tsx # 속성 편집 +│ │ ├── renderers/ +│ │ │ └── PopRenderer.tsx # CSS Grid 렌더링 +│ │ ├── types/ +│ │ │ └── pop-layout.ts # v5 타입 정의 +│ │ └── utils/ +│ │ └── gridUtils.ts # 위치 계산 +│ ├── management/ # 화면 관리 +│ └── dashboard/ # 대시보드 +│ +└── lib/ + ├── api/screen.ts # 화면 API + └── registry/ # 컴포넌트 레지스트리 +``` + +--- + +## 핵심 파일 + +### 1. PopDesigner.tsx (메인) + +**역할**: 레이아웃 로드/저장, 컴포넌트 CRUD, 히스토리 + +```typescript +// 상태 관리 +const [layout, setLayout] = useState(createEmptyPopLayoutV5()); +const [selectedComponentId, setSelectedComponentId] = useState(null); +const [currentMode, setCurrentMode] = useState("tablet_landscape"); +const [history, setHistory] = useState([]); + +// 핵심 함수 +handleSave() // 레이아웃 저장 +handleAddComponent() // 컴포넌트 추가 +handleUpdateComponent() // 컴포넌트 수정 +handleDeleteComponent() // 컴포넌트 삭제 +handleUndo() / handleRedo() // 히스토리 +``` + +### 2. PopCanvas.tsx (캔버스) + +**역할**: 그리드 캔버스, DnD, 줌, 패닝, 모드 전환 + +```typescript +// DnD 설정 +const DND_ITEM_TYPES = { COMPONENT: "component" }; + +// 뷰포트 프리셋 (4개 모드) - height 제거됨 (세로 무한 스크롤) +const VIEWPORT_PRESETS = [ + { id: "mobile_portrait", width: 375, columns: 4 }, + { id: "mobile_landscape", width: 600, columns: 6 }, + { id: "tablet_portrait", width: 834, columns: 8 }, + { id: "tablet_landscape", width: 1024, columns: 12 }, +]; + +// 세로 자동 확장 +const MIN_CANVAS_HEIGHT = 600; // 최소 캔버스 높이 +const CANVAS_EXTRA_ROWS = 3; // 항상 유지되는 여유 행 수 +const dynamicCanvasHeight = useMemo(() => { ... }, []); + +// 기능 +- useDrop(): 팔레트에서 컴포넌트 드롭 +- handleWheel(): 줌 (30%~150%) +- Space + 드래그: 패닝 +``` + +### 3. PopRenderer.tsx (렌더러) + +**역할**: CSS Grid 기반 레이아웃 렌더링 + +```typescript +// Props +interface PopRendererProps { + layout: PopLayoutDataV5; + viewportWidth: number; + currentMode: GridMode; + isDesignMode: boolean; + selectedComponentId?: string | null; + onSelectComponent?: (id: string | null) => void; +} + +// CSS Grid 스타일 생성 +const gridStyle = useMemo(() => ({ + display: "grid", + gridTemplateColumns: `repeat(${columns}, 1fr)`, + gridTemplateRows: `repeat(${rows}, 1fr)`, + gap: `${gap}px`, + padding: `${padding}px`, +}), [mode]); + +// 위치 변환 (12칸 → 다른 모드) +const convertPosition = (pos: PopGridPosition, targetMode: GridMode) => { + const ratio = GRID_BREAKPOINTS[targetMode].columns / 12; + return { + col: Math.max(1, Math.round(pos.col * ratio)), + colSpan: Math.max(1, Math.round(pos.colSpan * ratio)), + row: pos.row, + rowSpan: pos.rowSpan, + }; +}; +``` + +### 4. ComponentEditorPanel.tsx (속성 패널) + +**역할**: 선택된 컴포넌트 속성 편집 + +```typescript +// 탭 구조 +- grid: col, row, colSpan, rowSpan (기본 모드에서만 편집) +- settings: label, type 등 +- data: 데이터 바인딩 (미구현) +- visibility: 모드별 표시/숨김 +``` + +### 5. pop-layout.ts (타입 정의) + +**역할**: v5 타입 정의 + +```typescript +// 그리드 모드 +type GridMode = "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape"; + +// 브레이크포인트 설정 (2026-02-06 재설계) +const GRID_BREAKPOINTS = { + mobile_portrait: { columns: 4, maxWidth: 479, gap: 8, padding: 12 }, + mobile_landscape: { columns: 6, minWidth: 480, maxWidth: 767, gap: 8, padding: 16 }, + tablet_portrait: { columns: 8, minWidth: 768, maxWidth: 1023, gap: 12, padding: 20 }, + tablet_landscape: { columns: 12, minWidth: 1024, gap: 12, padding: 24 }, +}; + +// 모드 감지 (순수 너비 기반) +function detectGridMode(viewportWidth: number): GridMode { + if (viewportWidth < 480) return "mobile_portrait"; + if (viewportWidth < 768) return "mobile_landscape"; + if (viewportWidth < 1024) return "tablet_portrait"; + return "tablet_landscape"; +} + +// 레이아웃 데이터 +interface PopLayoutDataV5 { + version: "pop-5.0"; + metadata: PopLayoutMetadata; + gridConfig: PopGridConfig; + components: PopComponentDefinitionV5[]; + globalSettings: PopGlobalSettingsV5; +} + +// 컴포넌트 정의 +interface PopComponentDefinitionV5 { + id: string; + type: PopComponentType; + label: string; + gridPosition: PopGridPosition; // col, row, colSpan, rowSpan + config: PopComponentConfig; + visibility: Record; + modeOverrides?: Record; +} + +// 위치 +interface PopGridPosition { + col: number; // 시작 열 (1부터) + row: number; // 시작 행 (1부터) + colSpan: number; // 열 크기 (1~12) + rowSpan: number; // 행 크기 (1~) +} +``` + +### 6. gridUtils.ts (유틸리티) + +**역할**: 그리드 위치 계산 + +```typescript +// 위치 변환 +convertPositionToMode(pos, targetMode) + +// 겹침 감지 +isOverlapping(posA, posB) + +// 빈 위치 찾기 +findNextEmptyPosition(layout, mode) + +// 마우스 → 그리드 좌표 +mouseToGridPosition(mouseX, mouseY, canvasRect, mode) +``` + +--- + +## 데이터 흐름 + +``` +[사용자 액션] + ↓ +[PopDesigner] ← 상태 관리 (layout, selectedComponentId, history) + ↓ +[PopCanvas] ← DnD, 줌, 모드 전환 + ↓ +[PopRenderer] ← CSS Grid 렌더링 + ↓ +[컴포넌트 표시] +``` + +### 저장 흐름 + +``` +[저장 버튼] + ↓ +PopDesigner.handleSave() + ↓ +screenApi.saveLayoutPop(screenId, layout) + ↓ +[백엔드] screenManagementService.saveLayoutPop() + ↓ +[DB] screen_layouts_pop 테이블 +``` + +### 로드 흐름 + +``` +[페이지 로드] + ↓ +PopDesigner useEffect + ↓ +screenApi.getLayoutPop(screenId) + ↓ +isV5Layout(data) 체크 + ↓ +setLayout(data) 또는 createEmptyPopLayoutV5() +``` + +--- + +## API 엔드포인트 + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| GET | `/api/screen-management/layout-pop/:screenId` | 레이아웃 조회 | +| POST | `/api/screen-management/layout-pop/:screenId` | 레이아웃 저장 | + +--- + +## 삭제된 레거시 (참고용) + +| 파일 | 버전 | 이유 | +|------|------|------| +| PopCanvasV4.tsx | v4 | Flexbox 기반, v5로 대체 | +| PopFlexRenderer.tsx | v4 | Flexbox 렌더러, v5로 대체 | +| PopLayoutRenderer.tsx | v3 | 절대 좌표 기반, v5로 대체 | +| ComponentEditorPanelV4.tsx | v4 | v5 전용으로 통합 | + +--- + +*상세 스펙: [SPEC.md](./SPEC.md) | 파일 목록: [FILES.md](./FILES.md)* diff --git a/popdocs/CHANGELOG.md b/popdocs/CHANGELOG.md new file mode 100644 index 00000000..bba05033 --- /dev/null +++ b/popdocs/CHANGELOG.md @@ -0,0 +1,1764 @@ +# POP 변경 이력 + +형식: [Keep a Changelog](https://keepachangelog.com/) + +--- + +## [미출시] + +- Phase 0: ~~공통 인프라 구현 (useDataSource, usePopEvent)~~ **완료** +- Phase 2: pop-button, pop-icon 구현 +- Phase 3: pop-table 구현 +- Phase 4: pop-search, pop-field, pop-lookup 구현 +- Phase 5: pop-table 카드 템플릿, 차트, 게이지 +- Phase 6: pop-system 구현 +- 워크플로우 연동 + +--- + +## [2026-02-11] Phase 0 공통 인프라: usePopEvent + useDataSource 훅 구현 + +### 배경 + +모든 데이터 연동 POP 컴포넌트(pop-button, pop-table, pop-search 등)가 공유하는 2개 핵심 훅을 구현. +기존 대시보드 `dataFetcher.ts`의 조회 로직과 `dataApi` CRUD를 공통 훅으로 래핑하는 작업. + +### Added + +- **`usePopEvent` 훅** (`frontend/hooks/pop/usePopEvent.ts`) + - screenId 기반 이벤트 버스 (publish/subscribe/sharedData) + - 전역 Map 2개 (screenBuses, sharedDataStore) - 모듈 스코프, SSR 가드 + - `cleanupScreen(screenId)` 유틸 (화면 언마운트 시 메모리 정리) + +- **`popSqlBuilder` 유틸** (`frontend/hooks/pop/popSqlBuilder.ts`) + - `dataFetcher.ts`에서 SQL 빌더 로직 5개 함수 추출 (로직 변경 없이 복사) + - `escapeSQL`, `sanitizeIdentifier`, `validateDataSourceConfig`, `buildWhereClause`, `buildAggregationSQL` + +- **`useDataSource` 훅** (`frontend/hooks/pop/useDataSource.ts`) + - DataSourceConfig 기반 DB 테이블 CRUD 통합 + - 조회 분기: 집계/조인 → SQL 빌더 + executeQuery, 단순 → dataApi.getTableData + - CRUD: save/update/remove → dataApi 래핑 + - 자동 새로고침 (refreshInterval, 최소 5초) + - refetch 필터 병합 (overrideFilters + config.filters) + +- **배럴 파일** (`frontend/hooks/pop/index.ts`) + - usePopEvent, cleanupScreen, useDataSource, MutationResult, DataSourceResult, buildAggregationSQL, validateDataSourceConfig re-export + +### 검수 결과 + +- 린트 에러: 0건 +- 중복 정의: 0건 (20개 함수/타입 전수 Grep) +- 미사용 import: 0건 +- 가상 시뮬레이션 8시나리오: 전부 정상 +- 기존 대시보드 `dataFetcher.ts`: 미수정 (안정성 우선) + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `frontend/hooks/pop/usePopEvent.ts` | 신규 - 이벤트 버스 훅 (190줄) | +| `frontend/hooks/pop/popSqlBuilder.ts` | 신규 - SQL 빌더 유틸 (195줄) | +| `frontend/hooks/pop/useDataSource.ts` | 신규 - 데이터 CRUD 훅 (383줄) | +| `frontend/hooks/pop/index.ts` | 신규 - 배럴 파일 (15줄) | + +### Git + +| 브랜치 | 커밋 | 작업 | +|--------|------|------| +| `ksh-button` | `300542d9` | feat(pop): usePopEvent, useDataSource 공통 훅 구현 | +| `ksh-v2-work` | fast-forward merge | ksh-button 병합 | +| `origin/ksh-v2-work` | push 완료 | 원격 동기화 | + +--- + +## [2026-02-11] 브랜치 병합: ksh-dashboard -> ksh-v2-work (pop-icon + pop-dashboard 통합) + +### 배경 + +`ksh-v2-work` 브랜치에서 pop-icon 컴포넌트를, `ksh-dashboard` 브랜치에서 pop-dashboard 컴포넌트를 각각 독립 개발. +두 브랜치가 공통 확장 포인트(타입 정의, 팔레트, 렌더러, 에디터 패널)를 동시에 수정하여 병합 충돌 발생. + +### 병합 통계 + +- **충돌 파일**: 7개 / 17개 충돌 지점 +- **해결 전략**: 양쪽 기능 통합(union) 14건, ksh-dashboard 채택 2건, ksh-v2-work 채택 1건 +- **의존성 순서 해결**: pop-layout.ts -> index.ts -> pop-text.tsx -> ComponentPalette.tsx -> PopRenderer.tsx -> ComponentEditorPanel.tsx -> PopDesigner.tsx + +### Merged (양쪽 통합) + +- **PopComponentType**: `"pop-icon" | "pop-dashboard"` 양쪽 타입 통합 (pop-layout.ts) +- **DEFAULT_COMPONENT_GRID_SIZE**: pop-icon(1x2) + pop-dashboard(6x3) 기본 크기 추가 +- **COMPONENT_TYPE_LABELS**: PopRenderer, ComponentEditorPanel 양쪽에 4개 항목 완비 +- **PALETTE_ITEMS**: 아이콘(MousePointer) + 대시보드(BarChart3) 팔레트 항목 추가 +- **PopRendererProps**: `currentScreenId`(아이콘용) + `previewPageIndex`(대시보드용) props 통합 +- **ComponentEditorPanelProps**: `previewPageIndex` + `onPreviewPage` props 통합 +- **컴포넌트 등록**: `./pop-icon` + `./pop-dashboard` import 통합 (index.ts) +- **lucide-react import**: `MousePointer` + `BarChart3` 통합 (ComponentPalette.tsx) +- **ComponentContent 렌더링**: pop-icon pointer-events 허용 + previewPageIndex 전달 통합 + +### Adopted from ksh-dashboard + +- **pop-text isRealtime**: `isRealtime` 조건부 interval로 변경 (불필요한 timer 방지) +- **handleUpdateComponent**: 함수적 `setLayout(prev => ...)` 업데이트 (stale closure 방지) + +### Adopted from ksh-v2-work + +- **ComponentEditorPanel CSS**: `overflow-hidden`, `min-h-0`, `m-0` 방어적 CSS (스크롤 안정성) + +### 검증 결과 + +- 충돌 마커 잔여: 0건 +- TypeScript 에러 (병합 파일): 0건 +- 프론트엔드 빌드: 성공 +- 백엔드 빌드 에러: 0건 (기존 2건은 패키지 미설치) +- 린트 에러: 0건 + +--- + +## [2026-02-11] 대시보드 스타일 정리 + 페이지 미리보기 + 차트 디자인 개선 + +### 배경 + +이전 작업에서 구현한 글자 크기 3그룹 커스텀이 `@container` 반응형 자동 크기와 충돌하여 정보 잘림 발생. +또한 `handleUpdateComponent`의 stale closure 버그로 빠른 연속 설정 변경 시 값 유실. +추가로 디자이너 캔버스에서 페이지별 미리보기 기능 부재. + +### Changed + +- **글자 크기 커스텀 제거** (types.ts, KpiCard, StatCard, GaugeItem, ChartItem) + - `FONT_SIZE_PX` 상수 삭제 + - `ItemStyleConfig`를 `{ labelAlign?: TextAlign }` 으로 단순화 + - 기존 `@container` 반응형 자동 크기 복원 (`text-xs @[250px]:text-sm` 등) + +- **라벨 정렬 기능** (4개 아이템 + PopDashboardConfig.tsx) + - `TEXT_ALIGN_CLASSES[itemStyle?.labelAlign ?? "center"]` 로 라벨만 정렬 + - 값/보조 텍스트는 항상 가운데 정렬 고정 + - `ItemStyleEditor` 컴포넌트에 접기/펼치기(Collapsible) 지원 + +- **페이지 미리보기** (PopDashboardComponent, PopDashboardConfig, PopDesigner, PopCanvas, PopRenderer, ComponentEditorPanel) + - 각 페이지 옆에 Eye 버튼 → 클릭 시 해당 페이지만 디자이너 캔버스에 렌더링 + - `previewPageIndex` prop 전달 체인: PopDesigner → PopCanvas → PopRenderer → ComponentContent → PopDashboardComponent + - 스타일 변경 시 자동으로 해당 페이지 미리보기 활성화 + +- **차트 디자인 개선** (ChartItem.tsx) + - `CartesianGrid` 추가 (얇은 격자선) + - `abbreviateNumber` 적용 (Y축 + 파이 라벨 + 툴팁) + - X축 라벨 7자 이상 시 대각선 표시 (`angle: -45`) + - 긴 라벨 시 하단 여백 자동 확보 + +### Fixed + +- **stale closure 버그** (PopDesigner.tsx) + - `handleUpdateComponent`에서 `layout` 직접 참조 → `setLayout(prev => ...)` 함수적 업데이트 + - 의존성 배열에서 `layout` 제거 → `[saveToHistory]`만 유지 + +- **불필요 코드 제거** (PopDashboardComponent.tsx) + - `setRenderTick` state + `itemStyleKey` 변수 + 관련 useEffect 삭제 + +- **디버그 console.log 전량 제거** (8개) + - PopDashboardComponent(3), PopDesigner(3), ChartItem(1), PopDashboardConfig(1) + +- **.next 빌드 캐시 꼬임** (Docker) + - Docker 익명 볼륨에 캐시된 `.next`가 호스트 삭제와 독립적인 문제 + - `docker-compose down -v`로 볼륨 포함 삭제 후 `--build` 재시작 + +--- + +## [2026-02-10] 디자이너 캔버스 UX 개선 (헤더 제거 + 실제 데이터 렌더링 + 컴포넌트 목록) + +### 배경 + +디자이너 캔버스와 실제 뷰어 간의 시각적 차이 해소 요청: +- 디자이너에서 각 컴포넌트마다 5px 헤더 바가 표시되어 실제 뷰어와 다름 +- 대시보드가 더미 아이콘(PreviewComponent)으로만 표시됨 +- 헤더 제거 후 컴포넌트 식별 수단 필요 + +### Changed + +- **디자인모드 실제 데이터 렌더링** (PopRenderer.tsx) + - 헤더(5px) + 위치 정보 div 완전 삭제 + - `PreviewComponent` 대신 `ActualComp`(실제 컴포넌트)로 렌더링 + - `pointer-events-none`으로 내부 클릭 차단 (그리드 선택은 정상 작동) + +- **컴포넌트 목록 UI** (ComponentEditorPanel.tsx) + - "위치" 탭에 "배치된 컴포넌트" 목록 추가 (Layers 아이콘 + 카운트) + - 목록에서 클릭하면 해당 컴포넌트 선택 (그리드 클릭과 동일 효과) + - `COMPONENT_TYPE_LABELS`에 `pop-sample`, `pop-text`, `pop-dashboard` 키 추가 + +- **Props 연결** (PopDesigner.tsx) + - `allComponents`, `onSelectComponent`, `selectedComponentId` 3개 props 전달 + +### Fixed + +- **미사용 import 제거** (ComponentEditorPanel.tsx) + - `COMPONENT_TYPE_LABELS` 타입 변경 후 `PopComponentType` import 누락 정리 + +--- + +## [2026-02-10] pop-dashboard 차트/게이지/UI 디자인 개선 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 파이 차트(Chart 7)가 표시되지 않음 (fetch 기반 API 간헐 실패 + bigint 문자열 타입 문제) +- 파이 차트 슬라이스에 카테고리명/레전드 없어 의미 파악 불가 +- 게이지 최소/최대/목표 값 변경이 적용되지 않음 (스프레드 연산자 순서 버그) +- 게이지가 가로 레이아웃에서 너무 작게 표시됨 (고정 max-width 제한) +- 좌우 버튼/인디케이터가 별도 영역 차지해서 콘텐츠 공간 부족 + 상하 마진 불균형 +- KPI 카드/통계 카드만 왼쪽 정렬 (게이지/차트는 가운데) +- 차트 설정에서 "X축 컬럼"과 "그룹핑(X축)" 중복으로 혼동 + +### Fixed + +- **파이 차트 미표시** (dataFetcher.ts) + - fetch 기반 API가 iframe에서 간헐 실패 -> apiClient(axios) 우선 사용, fetch 폴백 + - PostgreSQL bigint 반환값이 문자열("79") -> `Number()` 변환 추가 + +- **파이 차트 라벨 없음** (ChartItem.tsx) + - `Legend` 컴포넌트 추가, `label` 커스텀 포맷 (`name value (percent%)`) + - `Tooltip` formatter로 이름+값 표시 + +- **gaugeConfig 값 변경 안 됨** (PopDashboardConfig.tsx) + - 스프레드 연산자 순서 버그: `{ max: newValue, ...item.gaugeConfig }` -> `{ ...item.gaugeConfig, max: newValue }` + - min/max/target 3개 모두 동일 패턴 수정 + +- **게이지 가로 레이아웃 비율** (GaugeItem.tsx) + - `max-w-[200px]` 고정 -> `h-full w-auto max-w-full` 높이 기반 스케일링 + - SVG 래퍼를 `flex-1 min-h-0`으로 변경해 가용 높이 최대 활용 + +### Changed + +- **좌우 버튼/인디케이터 오버레이** (ArrowsMode.tsx, AutoSlideMode.tsx) + - 버튼: `px-12` 패딩 제거, 콘텐츠 위에 겹침 (`absolute`, `bg-background/70 backdrop-blur-sm`) + - 인디케이터: 별도 영역 -> `absolute bottom-1`로 콘텐츠 하단 오버레이 + - 상하 마진 불균형 해소 + +- **KPI/통계 카드 가운데 정렬** (KpiCard.tsx, StatCard.tsx) + - KPI: `items-center` 추가 + - 통계: `items-center justify-center` 추가 + - 4개 아이템 모드(KPI/Chart/Gauge/Stat) 정렬 통일 + +- **차트 X/Y축 입력 필드 제거** (PopDashboardConfig.tsx) + - "X축 컬럼"/"Y축 컬럼" 수동 입력 제거 (groupBy와 중복, 혼동 유발) + - "X축: 그룹핑(X축)에서 선택한 컬럼 자동 적용 / Y축: 집계 결과(value) 자동 적용" 안내 텍스트로 교체 + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `PopDashboardConfig.tsx` | gaugeConfig 스프레드 순서 수정, X/Y축 입력 제거 + 안내 텍스트 | +| `utils/dataFetcher.ts` | apiClient 우선 사용, bigint 문자열 -> 숫자 변환 | +| `items/ChartItem.tsx` | PieChart Legend/custom label 추가 | +| `items/GaugeItem.tsx` | SVG 스케일링 높이 기반 변경 | +| `items/KpiCard.tsx` | items-center 추가 | +| `items/StatCard.tsx` | items-center justify-center 추가 | +| `modes/ArrowsMode.tsx` | 좌우 버튼 + 인디케이터 오버레이 | +| `modes/AutoSlideMode.tsx` | 인디케이터 오버레이 | + +--- + +## [2026-02-10] pop-dashboard 아이템 모드 완성 + SQL 방어 로직 + 레이아웃/라벨 수정 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 대시보드 아이템 4개 모드(KPI, Chart, Gauge, Stat Card)가 실제 데이터와 연동되어 동작하려면 설정 UI가 부족함 +- Chart에 groupBy(X축) 설정 UI 없음, StatCard에 카테고리 편집 UI 없음 +- 설정 폼 중간 상태(집계 유형만 선택, 컬럼 미선택)에서 `SUM()`, `COUNT()` 같은 잘못된 SQL이 백엔드로 전송되어 장애 유발 +- 2열 설정이 1열로 렌더링되고, 라벨/단위가 잘려서 안 보임 + +### Added + +- **groupBy(X축) 설정 Combobox** (PopDashboardConfig.tsx) + - DataSourceEditor에 "그룹핑(X축)" Popover Combobox 추가 + - 집계 활성화 시 표시, 차트 X축 카테고리 컬럼 선택 + +- **차트 xAxisColumn / yAxisColumn 입력 UI** (PopDashboardConfig.tsx) + - Chart 아이템 전용 X축/Y축 컬럼 입력 필드 + +- **StatCard 카테고리 인라인 편집기** (PopDashboardConfig.tsx) + - 카테고리별 라벨, 필터 조건(컬럼/연산자/값), 색상 편집 + - 카테고리 추가/삭제 + +- **`validateDataSourceConfig()` 함수** (dataFetcher.ts) + - 테이블/컬럼/조인 미완료 상태 검증 + - 미완료 시 SQL 생성 차단, 에러 메시지 반환 + +### Changed + +- **Chart xAxisColumn 자동 보정** (PopDashboardComponent.tsx) + - groupBy 설정 있지만 xAxisColumn 미설정 시 첫 번째 groupBy 컬럼 자동 적용 + +- **StatCard 카테고리별 독립 데이터 필터링** (PopDashboardComponent.tsx) + - 전체 rows를 카테고리 filter 조건으로 각각 필터링하여 개별 건수 계산 + +- **useEffect 의존성 안정화** (PopDashboardComponent.tsx) + - `visibleItems` 배열 참조 -> `visibleItemIds` JSON 문자열로 교체 + - 매 렌더마다 새 배열 참조 생성으로 인한 불필요한 재호출 방지 + +- **refreshInterval 최소 5초 강제** (PopDashboardComponent.tsx) + - 5초 미만 설정 시 5초로 강제 + +- **fetchTableColumns API 우선순위** (dataFetcher.ts) + - tableManagementApi(axios) 우선, dashboardApi(fetch) 폴백 + +- **buildWhereClause 빈 필터 무시** (dataFetcher.ts) + - 컬럼명이 빈 필터 조건 건너뜀 (설정 중간 상태 방어) + +- **buildAggregationSQL COUNT(*) 처리** (dataFetcher.ts) + - COUNT는 컬럼 없이도 COUNT(*)로 처리, 불완전한 조인 건너뜀 + +### Fixed + +- **2열 설정이 1열로 렌더링** (GridMode.tsx) + - MIN_CELL_WIDTH 160px -> 80px + - 초기 containerWidth=300에서 (300-8)/2=146 < 160이라 1열로 축소되던 문제 + +- **라벨/단위 잘림** (KpiCard, GaugeItem, StatCard, ChartItem) + - `truncate` 클래스 제거 (text-overflow: ellipsis 비활성) + - `hidden @[120px]:inline` 등 조건부 숨김 제거 + - 기본 폰트 크기 text-[10px] -> text-xs (12px) + - 내부 패딩 p-2 -> p-3 (여유 공간 확보) + +- **백엔드 unhealthy 유발하는 잘못된 SQL** (dataFetcher.ts) + - 설정 중간 상태에서 `SUM()`, `COUNT()` 같은 빈 괄호 SQL 전송 차단 + - `validateDataSourceConfig()`으로 사전 검증 + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `PopDashboardConfig.tsx` | groupBy Combobox, 차트 축 입력, 통계 카테고리 편집기 | +| `PopDashboardComponent.tsx` | 차트 자동 보정, StatCard 필터링, useEffect 안정화, refreshInterval 최소값 | +| `utils/dataFetcher.ts` | validateDataSourceConfig, buildWhereClause/buildAggregationSQL 방어, fetchTableColumns API 우선순위 | +| `modes/GridMode.tsx` | MIN_CELL_WIDTH 160 -> 80 | +| `items/KpiCard.tsx` | truncate 제거, hidden 제거, 폰트/패딩 상향 | +| `items/GaugeItem.tsx` | truncate 제거, hidden 제거, 폰트/패딩 상향 | +| `items/StatCard.tsx` | truncate 제거, hidden 제거, 폰트/패딩 상향 | +| `items/ChartItem.tsx` | truncate 제거, 폰트/패딩 상향 | + +--- + +## [2026-02-10] pop-dashboard 페이지(슬라이드) 구조 재설계 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 기존 대시보드는 아이템이 평면 리스트 -> 슬라이드 하나에 아이템 하나만 표시 가능 +- "한 화면에 4개 차트, 다음 화면에 1개 차트" 같은 구성 불가 +- `useGridLayout` 옵션은 전체 아이템을 하나의 그리드에 넣는 것만 가능 + +**해결 방향**: +- `DashboardPage` 개념 도입: 각 페이지가 독립적인 그리드 레이아웃(열/행/셀 배치) 보유 +- 표시 모드(arrows/auto-slide/scroll)는 페이지 간 전환 방식을 결정 +- 기존 `useGridLayout`, `gridCells`, `gridColumns`, `gridRows` 속성 폐기 -> `pages` 배열로 대체 + +### Added + +- **`DashboardPage` 인터페이스** (types.ts) + - `id`, `label`, `gridColumns`, `gridRows`, `gridCells` 속성 + - 각 페이지가 독립적 그리드 레이아웃 보유 + +- **`migrateConfig()` 함수** (PopDashboardComponent.tsx) + - 레거시 config(displayMode="grid", useGridLayout=true)를 pages 기반으로 런타임 변환 + - 저장된 config는 변경하지 않고 런타임에서만 적용 + +- **`PageEditor` 컴포넌트** (PopDashboardConfig.tsx) + - 페이지별 열/행/셀 배치 편집 UI + - 기존 `GridLayoutEditor` 재사용 + +- **아이템 라벨 인라인 편집** (PopDashboardConfig.tsx) + - `ItemEditor` 헤더에서 직접 라벨 편집 가능 (접힌 상태에서도) + +### Changed + +- **`PopDashboardConfig` 타입 구조 변경** (types.ts) + - 삭제: `useGridLayout`, `gridCells`, `gridColumns`, `gridRows` + - 추가: `pages?: DashboardPage[]` + +- **"레이아웃" 탭 -> "페이지" 탭** (PopDashboardConfig.tsx) + - 페이지 추가/삭제 UI + - 페이지별 독립 그리드 편집 + +- **렌더링 로직 전면 교체** (PopDashboardComponent.tsx) + - `migrateConfig()` -> pages 기반 -> 각 모드 컴포넌트에 페이지 단위 전달 + - `GridModeComponent`를 페이지 내부 렌더링에 재사용 + +- **미리보기 로직 변경** (PopDashboardPreview.tsx) + - 첫 번째 페이지의 그리드 미리보기 + "N페이지" 배지 표시 + +- **defaultProps 업데이트** (index.tsx) + - `useGridLayout: false` 제거, `pages: []` 추가 + +### Fixed + +- **설정 탭 세로 스크롤 불가** (ComponentEditorPanel.tsx) + - Flexbox `min-height: auto` 문제로 `overflow-auto` 미작동 + - `Tabs`/`TabsContent`에 `min-h-0` 추가, `TabsList`에 `shrink-0` 추가 + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `types.ts` | `DashboardPage` 추가, `PopDashboardConfig` 구조 변경 | +| `PopDashboardComponent.tsx` | `migrateConfig()` 추가, 페이지 기반 렌더링 | +| `PopDashboardPreview.tsx` | `migrateConfig` 적용, 페이지 미리보기 | +| `PopDashboardConfig.tsx` | "페이지" 탭, `PageEditor`, 인라인 라벨 편집 | +| `pop-dashboard/index.tsx` | defaultProps 업데이트 | +| `ComponentEditorPanel.tsx` | Tabs/TabsContent `min-h-0` 추가 (스크롤 수정) | +| `popdocs/PROBLEMS.md` | 스크롤 버그 상세 기록 | + +--- + +## [2026-02-09] POP 뷰어 스크롤 수정 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 설계 화면(디자이너)에서 컴포넌트를 화면 높이 아래로 배치하면 스크롤로 볼 수 있음 +- 뷰어(`/pop/screens/4114`)에서는 화면 높이를 초과하는 컴포넌트가 잘려서 안 보임 +- 스크롤 자체가 불가능한 상태 + +### Fixed + +- **최외곽 컨테이너 `overflow-hidden` 제거** (page.tsx 185행) + - `overflow-hidden`이 뷰포트를 넘는 모든 콘텐츠를 잘라내고 있었음 + - 제거하여 자식 요소의 스크롤을 허용 + +- **`overflow-auto` 공통 적용** (page.tsx 266행) + - 기존: 프리뷰 모드에서만 `overflow-auto` 적용 + - 수정: 조건문 밖으로 이동하여 일반 뷰어 모드에서도 스크롤 가능 + +- **`min-h-full` 추가** (page.tsx 275행) + - 일반 뷰어 모드에서 짧은 콘텐츠일 때 백색 배경이 전체 높이를 채우도록 보장 + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `frontend/app/(pop)/pop/screens/[screenId]/page.tsx` | CSS 클래스 3곳 수정 (overflow-hidden 제거, overflow-auto 공통 적용, min-h-full 추가) | + +--- + +## [2026-02-09] POP 뷰어 실제 컴포넌트 렌더링 수정 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 설계 화면(디자이너)에서는 이미지/텍스트/타이틀이 정상 표시 +- 뷰어(`/pop/screens/4114`)에 접속하면 "pop-text 1" 같은 라벨만 보이는 플레이스홀더 상태 +- `renderActualComponent()` 함수가 레지스트리의 실제 컴포넌트를 무시하고 라벨 문자열만 반환 +- 뷰어 페이지에서 컴포넌트 레지스트리 초기화 import 누락 + +### Fixed + +- **뷰어 컴포넌트 레지스트리 미초기화** (page.tsx) + - `import "@/lib/registry/pop-components"` 추가 + - `PopRenderer` import 앞에 배치 (순서 중요) + +- **`renderActualComponent()` 플레이스홀더만 반환** (PopRenderer.tsx) + - `PopComponentRegistry.getComponent()`로 실제 컴포넌트 조회 + - 등록된 컴포넌트가 있으면 `config`/`label` props 전달하여 실제 렌더링 + - 미등록 컴포넌트는 기존 플레이스홀더를 fallback으로 유지 + +### 발견된 추가 문제 (미수정) + +- **datetime 실시간 업데이트 기본값 불일치** + - 설정 패널: `config?.isRealtime ?? true` (기본 켜짐) + - 뷰어 컴포넌트: `if (!config?.isRealtime) return` (undefined = 꺼짐) + - DB에 `isRealtime` 미저장 시 시간이 멈춰 보임 + - 별도 작업으로 수정 예정 + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `frontend/app/(pop)/pop/screens/[screenId]/page.tsx` | 레지스트리 초기화 import 추가 (+2줄) | +| `frontend/components/pop/designer/renderers/PopRenderer.tsx` | `renderActualComponent()` 실제 컴포넌트 렌더링으로 교체 (-8줄 +19줄) | + +--- + +## [2026-02-09] pop-dashboard 상세 설계 토의 + +### 배경 (왜 이 작업이 필요했는가) + +**상황**: +- 컴포넌트 정의서 v8.0에서 pop-dashboard가 "숫자를 집계해서 보여줌"으로 정의되어 있으나, 구체적인 데이터 구조/표시 모드/설정 패널 등 상세 설계가 필요 +- Phase 0 공통 인프라와 Phase 1 pop-dashboard를 실제 구현하기 위한 세부 사항 결정 필요 +- 기존 POP 대시보드 프로토타입(`frontend/components/pop/dashboard/`)과의 관계 정리 필요 + +### 토의에서 결정된 사항 + +| # | 결정 항목 | 결정 내용 | 근거 | +|---|----------|----------|------| +| 1 | 컴포넌트 구조 | 1개 pop-dashboard = 여러 DashboardItem 묶음 (멀티 아이템 컨테이너) | 시스템 옵션에서 각 아이템별 보이기/숨기기 가능해야 함 | +| 2 | 서브타입 범위 | 4개 전부 Phase 1에서 구현 (kpi-card, chart, gauge, stat-card) | "시간이 걸리겠지만 4개 결국 전부 필요" | +| 3 | 표시 모드 | arrows, auto-slide, grid, scroll 4가지 | 전부 가능하게, 디자이너가 옵션으로 선택 | +| 4 | 그리드 배치 | 디자이너가 아이템별 행/열 위치 직접 지정 | "행열 그리드 배치"에서 수동 지정 | +| 5 | 계산식 | formula 지원 (값A/값B, A+B, A/B*100 등) | "생산량/총재고량" 같은 복합 표현 필요 | +| 6 | 설정 패널 | 드롭다운 기반 쉬운 집계 설정 (SQL 불필요) | "집계 및 계산 설정이 쉽게 이루어져야 함" | +| 7 | 백엔드 호환성 | 기존 API만 사용, 신규 백엔드 개발 불필요 | dataApi, dashboardApi, entityJoinApi 호환 확인 완료 | +| 8 | POP/데스크탑 분리 | POP 전용 훅은 `frontend/hooks/pop/` 에 완전 분리 | "분리 명확하게 해야해" | +| 9 | 기존 대시보드 | `frontend/components/pop/dashboard/` 폐기 | 정적 하드코딩된 홈 화면, 새 컴포넌트로 완전 대체 | +| 10 | 표시 형태 | value, fraction(1,234/5,678), percent(21.7%), ratio(1,234:5,678) | 계산식 결과 다양한 표현 | + +### 보류/미결 사항 + +| 항목 | 상태 | 비고 | +|------|------|------| +| Recharts 라이브러리 | 확인 필요 | chart 서브타입에 필요, 프로젝트에 이미 있는지 확인 후 추가 | +| 자동 슬라이드 터치 재개 시간 | 미정 | 구현 시 결정 (3~5초 추천) | +| 그리드 모드 겹침 검사 | 미정 | 설정 패널 구현 시 상세 설계 | + +### 변경 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `POPUPDATE_2.md` | pop-dashboard 섹션 전면 개편 (멀티 아이템, 표시 모드, 계산식, 백엔드 API, 훅 분리) | +| `popdocs/PLAN.md` | Phase 1 상세 업데이트, 파일 경로, 백엔드 API, 함정 경고 | +| `popdocs/STATUS.md` | 토의 결과 반영, 다음 작업 업데이트 | +| `popdocs/README.md` | 마지막 대화 요약 업데이트 | +| `popdocs/CHANGELOG.md` | 본 항목 추가 | +| `.cursor/plans/pop-dashboard_*.plan.md` | 상세 설계 plan 파일 (별도 유지) | + +--- + +## [2026-02-09] POP 컴포넌트 정의서 v8.0 확정 + +### 배경 (왜 이 작업이 필요했는가) + +**상황**: +- POP 시스템에 넣을 컴포넌트의 역할, 데이터 흐름, 통신 방식을 사전에 정의해야 함 +- 영업팀으로부터 받은 참조 화면(HTML 샘플)을 기반으로 필요한 컴포넌트 목록 도출 +- 모달 화면 설계 방식에 대한 결정 필요 + +### Added + +- **POP 컴포넌트 정의서 v8.0** (`POPUPDATE_2.md`) + - 9개 컴포넌트 정의: pop-text(완성), pop-dashboard, pop-table, pop-button, pop-icon, pop-search, pop-field, pop-lookup, pop-system + - POP 헌법 9조 (공통 규칙) + - 공통 인프라 설계: DataSourceConfig, ColumnBinding, JoinConfig, useDataSource, usePopEvent, PopActionConfig + - 컴포넌트 간 통신 시퀀스 다이어그램 5개 + - 구현 우선순위 7단계 (Phase 0~6) + +- **모달 화면 설계 방식** (v8.0 추가) + - 방식 A (인라인 모달): DataSourceConfig 기반 단순 목록 선택 + - 방식 B (외부 화면 참조): 별도 screen_id 연결, 복잡한 화면/재사용 + - POP 헌법 제9조: 모달 화면의 설계 원칙 + - PopActionConfig modal 타입 구체화 + +- **기존 시스템 호환성 검증 결과** (v8.0 추가) + - DB: layout_data JSONB로 modalConfig 저장 가능 (마이그레이션 불필요) + - 백엔드: saveLayoutPop/getLayoutPop API 그대로 사용 가능 + - 프론트: TabsWidget의 screenId 참조 패턴 차용 가능 + +### 주요 설계 결정 + +| 결정 | 내용 | +|------|------| +| 컴포넌트 9개 확정 | pop-text~pop-system | +| 역할 분리 | 조회용(search) vs 저장용(field), 이동(icon) vs 값 선택(lookup) | +| 시스템 설정 = 컴포넌트 | pop-system으로 통합 | +| 모달 이중 방식 | 인라인 + 외부 참조 | +| 이벤트 격리 | 화면 단위 (같은 화면 내 자유 통신) | +| 컬럼별 CRUD | read/write/readwrite/hidden 개별 설정 | + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `POPUPDATE_2.md` | v7.0 -> v8.0 (모달 설계, 헌법 제9조, 호환성 검증) | + +--- + +## [2026-02-09] origin/main 병합 (ksh-v2-work-merge-test) + +### 배경 (왜 이 작업이 필요했는가) + +**상황**: +- `ksh-v2-work` 브랜치에서 POP 디자이너 개발이 진행되는 동안, `main` 브랜치에서 데스크톱 ScreenDesigner 관련 대규모 업데이트(`feature/v2-unified-renewal` PR #386)가 병합됨 +- 두 브랜치가 `ScreenDesigner.tsx`를 동시에 수정하여 병합 충돌 발생 +- 안전한 병합을 위해 `ksh-v2-work-merge-test` 테스트 브랜치에서 작업 + +**병합 통계**: +- 소스: `origin/main` (86 커밋) +- 대상: `ksh-v2-work-merge-test` (ksh-v2-work에서 분기, 14 커밋) +- 분기점: `3fca677f` (feat: V2Media 컴포넌트 추가) +- 변경 파일: 207개 (43,535줄 추가, 3,547줄 삭제) +- 충돌 파일: 1개 (`ScreenDesigner.tsx`) + +### Merged (origin/main에서 가져온 주요 변경) + +- **ScreenDesigner 데스크톱 기능 강화** + - 그룹 정렬/분배/크기 맞춤 (handleGroupAlign, handleGroupDistribute, handleMatchSize) + - 라벨 토글 (handleToggleAllLabels) + - 단축키 도움말 모달 (showShortcutsModal) + - 디버그 console.log 정리 + +- **백엔드 신규 기능** + - 스케줄 관리 API (scheduleController, scheduleRoutes, scheduleService) + - 파일 관리 개선 (fileController, fileRoutes) + - 테이블 관리 확장 (tableManagementController) + - 넘버링 규칙 개선 (numberingRuleController) + +- **프론트엔드 신규/수정** + - 레이어 매니저 패널, 레이어 조건 패널 + - 화면 복사 모달, 편집 모달 + - InteractiveDataTable, InteractiveScreenViewer + - 넘버링 규칙 디자이너 + - 화면 그룹 트리뷰, 화면 관계 플로우 + +### 충돌 해결 (ScreenDesigner.tsx) + +3건의 충돌을 수동 해결: + +| 충돌 | 영역 | 해결 방식 | +|------|------|----------| +| 1. 함수 시그니처 | `isPop`, `defaultDevicePreview` props 추가 | ksh-v2-work 유지 (POP 모드 지원), 중복 `usePanelState` 제거 | +| 2. 저장 로직 | POP/V2/Legacy 3단계 분기 | ksh-v2-work 유지 (3단계 분기), console.log 제거 | +| 3. 툴바 props | 정렬/분배/크기맞춤/라벨토글/단축키 | origin/main 채택 (데스크톱 신규 기능 모두 포함) | + +### 검증 결과 + +| 항목 | 결과 | +|------|------| +| 충돌 마커 잔존 | 없음 | +| TypeScript 컴파일 | 신규 에러 없음 (기존 에러만) | +| 프론트엔드 빌드 | 성공 | +| 백엔드 빌드 | 신규 에러 없음 (`docx`/`bwip-js` 기존 이슈만) | +| 시맨틱 충돌 | 없음 | +| 린트 | 기능 에러 없음 (들여쓰기 차이만) | + +### 주의사항 + +- **Conflict 3 영역 들여쓰기**: origin/main에서 가져온 툴바 JSX(L5745~6631)의 들여쓰기가 ksh-v2-work와 2칸 차이. 기능에는 영향 없으나, 추후 포매팅 정리 권장 +- **기존 타입 에러**: `GridSettings`/`GridUtilSettings` 불일치, `SCREEN_RESOLUTIONS` export type 문제 등은 병합 이전부터 존재하던 기술 부채 + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `ScreenDesigner.tsx` | 3건 충돌 수동 해결 (함수 시그니처, 저장 로직, 툴바 props) | +| 외 207개 파일 | origin/main에서 자동 병합 | + +--- + +## [2026-02-06] v5.2.1 그리드 셀 크기 강제 고정 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 4칸 모드에서 특정 행의 가이드 셀이 다른 행보다 작게 표시됨 +- `gridAutoRows`는 최소 높이만 보장하여, 컴포넌트 콘텐츠가 행 높이를 밀어내면 인접 빈 셀도 영향받음 +- "셀의 크기 = 컴포넌트의 크기"라는 핵심 설계 원칙이 시각적으로 깨짐 + +### Changed + +- **gridAutoRows → gridTemplateRows** (PopRenderer.tsx) + ```typescript + // 변경 전: 최소 높이만 보장 (콘텐츠에 따라 늘어남) + gridAutoRows: `${breakpoint.rowHeight}px` + + // 변경 후: 행 높이 강제 고정 + gridTemplateRows: `repeat(${dynamicRowCount}, ${breakpoint.rowHeight}px)` + gridAutoRows: `${breakpoint.rowHeight}px` // 동적 추가행 대비 유지 + ``` + +- **dynamicRowCount 분리** (PopRenderer.tsx) + - gridCells 내부 → 독립 useMemo로 분리 + - gridStyle과 gridCells에서 공유 + +- **컴포넌트 overflow 변경** (PopRenderer.tsx) + - `overflow-visible` → `overflow-hidden` + - 컴포넌트 콘텐츠가 셀 경계를 벗어나지 않도록 강제 + +### Fixed + +- **PopRenderer dynamicRowCount에서 숨김 컴포넌트 포함 문제** + - PopCanvas는 숨김 제외하여 높이 계산, PopRenderer는 포함하여 계산 → 기준 불일치 + - PopRenderer에도 숨김 필터 추가, 여유행 +5 → +3으로 통일 + +- **디버깅 console.log 잔존** (PopCanvas.tsx) + - reviewComponents useMemo 내 console.log 2개 삭제 + +- **뷰어 viewportWidth 선언 순서** (page.tsx) + - currentModeKey보다 뒤에 선언되어 있던 viewportWidth를 앞으로 이동 + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `PopRenderer.tsx` | gridTemplateRows 강제 고정, dynamicRowCount 분리, overflow-hidden, 숨김 필터 추가 | +| `PopCanvas.tsx` | 디버깅 console.log 삭제 | +| `page.tsx (뷰어)` | viewportWidth 선언 순서 수정 | + +--- + +## [2026-02-06] v5.2 브레이크포인트 재설계 + 세로 자동 확장 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 뷰어에서 브라우저 수동 리사이즈 시 768~839px 구간에서 모드 불일치 +- useResponsiveMode 훅과 GRID_BREAKPOINTS 상수 간 기준 불일치 +- 기존 브레이크포인트가 실제 기기 뷰포트와 맞지 않음 + +**사용자 요구사항**: +- "현장 모바일 기기 8~14인치, 핸드폰은 아이폰 미니 ~ 갤럭시 울트라" +- "세로는 신경쓸 필요 없고 무한 스크롤 가능해야 함" + +### Changed + +- **브레이크포인트 재설계** (pop-layout.ts) + | 모드 | 변경 전 | 변경 후 | 근거 | + |------|--------|--------|------| + | mobile_portrait | ~599px | ~479px | 스마트폰 세로 최대 440px | + | mobile_landscape | 600~839px | 480~767px | 스마트폰 가로 | + | tablet_portrait | 840~1023px | 768~1023px | iPad Mini 768px 포함 | + | tablet_landscape | 1024px+ | 동일 | - | + +- **detectGridMode() 조건 수정** (pop-layout.ts) + ```typescript + if (viewportWidth < 480) return "mobile_portrait"; // was 600 + if (viewportWidth < 768) return "mobile_landscape"; // was 840 + if (viewportWidth < 1024) return "tablet_portrait"; + ``` + +- **BREAKPOINTS.TABLET_MIN 변경** (useDeviceOrientation.ts) + - 768 (was 840) + +- **VIEWPORT_PRESETS에서 height 제거** (PopCanvas.tsx) + - width만 유지, 세로는 무한 스크롤 + +### Added + +- **세로 자동 확장** (PopCanvas.tsx) + - `MIN_CANVAS_HEIGHT = 600`: 최소 캔버스 높이 + - `CANVAS_EXTRA_ROWS = 3`: 항상 유지되는 여유 행 수 + - `dynamicCanvasHeight`: 컴포넌트 배치 기반 동적 계산 + +- **격자 셀 동적 계산** (PopRenderer.tsx) + - 고정 20행 → maxRowEnd + 5 동적 계산 + +- **뷰어 일관성 확보** (page.tsx) + - 프리뷰 모드: useResponsiveModeWithOverride 유지 + - 일반 모드: detectGridMode(viewportWidth) 직접 사용 + +### Fixed + +- **뷰어 반응형 모드 불일치** + - 768~839px 구간에서 6칸/8칸 모드 불일치 해결 + +- **hiddenComponentIds 중복 정의 에러** + - 라인 410-412 중복 useMemo 제거 + +### Technical Details + +``` +브레이크포인트 재설계 근거 (실제 기기 CSS 뷰포트): + +| 기기 | CSS 뷰포트 너비 | +|------|----------------| +| iPhone SE | 375px | +| iPhone 16 Pro | 402px | +| Galaxy S25 Ultra | 440px | +| iPad Mini 7 | 768px | +| iPad Pro 11 | 834px (세로), 1194px (가로) | +| iPad Pro 13 | 1024px (세로), 1366px (가로) | + +→ 768px, 1024px가 업계 표준 (Tailwind, Bootstrap 동일) +``` + +``` +세로 자동 확장 로직: + +const dynamicCanvasHeight = useMemo(() => { + const maxRowEnd = visibleComps.reduce((max, comp) => { + return Math.max(max, comp.row + comp.rowSpan); + }, 1); + + const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS; // +3행 여유 + const height = totalRows * (rowHeight + gap) + padding * 2; + + return Math.max(MIN_CANVAS_HEIGHT, height); // 최소 600px +}, [...]); +``` + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `pop-layout.ts` | GRID_BREAKPOINTS 값 수정, detectGridMode() 조건 수정 | +| `useDeviceOrientation.ts` | BREAKPOINTS.TABLET_MIN = 768 | +| `PopCanvas.tsx` | VIEWPORT_PRESETS height 제거, dynamicCanvasHeight 추가 | +| `PopRenderer.tsx` | gridCells 동적 행 수 계산 | +| `page.tsx (뷰어)` | detectGridMode() 사용 | + +--- + +## [2026-02-06] v5.1 자동 줄바꿈 + 검토 필요 시스템 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 12칸에서 배치한 컴포넌트가 4칸 모드로 전환하면 "화면 밖" 패널로 이동하여 뷰어에서 안 보임 +- 사용자가 모든 모드를 수동으로 편집해야 하는 부담 +- "화면 밖" 개념이 실제로는 "검토 필요" 알림 역할이었음 + +**해결 방향**: +- 자동 줄바꿈: col > maxCol인 컴포넌트를 자동으로 맨 아래에 배치 +- 정보 손실 방지: 모든 컴포넌트가 항상 그리드 안에 표시됨 +- 검토 필요 알림: 오버라이드 없으면 "검토 필요" 표시 (자동 배치 상태) + +### Added + +- **자동 줄바꿈 로직** (gridUtils.ts) + - `convertAndResolvePositions()` 수정 + - 원본 col 보존 로직 추가 + - 정상 컴포넌트 vs 초과 컴포넌트 분리 + - 초과 컴포넌트를 맨 아래에 순차 배치 (col=1, row=맨아래+1) + - colSpan 자동 축소 (targetColumns 초과 방지) + +- **검토 필요 판별 함수** (gridUtils.ts) + - `needsReview()` 신규 함수 + - 기준: 12칸 아니고 + 오버라이드 없으면 → 검토 필요 + - 간단한 로직: "이 모드에서 편집했냐 안 했냐" + +- **검토 필요 패널** (PopCanvas.tsx) + - `ReviewPanel`: "화면 밖" → "검토 필요"로 이름 변경 + - `ReviewItem`: 클릭 시 해당 컴포넌트 선택 (드래그 없음) + - 자동 배치 뱃지 표시 + - 파란색 테마 (경고 아닌 안내 느낌) + +### Changed + +- **isOutOfBounds() Deprecated** (gridUtils.ts) + - `@deprecated` 주석 추가 + - needsReview()로 대체 권장 + - 하위 호환을 위해 함수는 유지 + +- **"화면 밖" 패널 역할 변경** (PopCanvas.tsx) + - 기존: col > maxCol → 화면 밖 (드래그로 복원) + - 변경: 오버라이드 없음 → 검토 필요 (클릭으로 선택) + - 숨김 기능과 완전히 분리 (별도 유지) + +### Fixed + +- **정보 손실 문제 해결** + - 모든 컴포넌트가 항상 그리드 안에 배치됨 + - 뷰어에서도 자동 배치가 적용되어 모두 표시됨 + +### Technical Details + +``` +자동 줄바꿈 로직: + +1. convertAndResolvePositions() 호출 + components: [ {id: "A", position: {col:1, ...}}, {id: "B", position: {col:5, ...}} ] + targetMode: "mobile_portrait" (4칸) + +2. 비율 변환 + 원본 col 보존 + converted: [ + {id: "A", position: {col:1, ...}, originalCol: 1}, + {id: "B", position: {col:2, ...}, originalCol: 5} // col은 변환됨, 원본은 5 + ] + +3. 정상 vs 초과 분리 + normalComponents: [A] // originalCol ≤ 4 + overflowComponents: [B] // originalCol > 4 + +4. 맨 아래 배치 + maxRow = A의 (row + rowSpan - 1) = 1 + B: col=1, row=2 (맨 아래에 자동 배치) + +5. 겹침 해결 + resolveOverlaps([A, B], 4) // 최종 위치 확정 + +6. 검토 필요 판별 + needsReview("mobile_portrait", false) // 오버라이드 없음 → true + → ReviewPanel에 B 표시 +``` + +``` +검토 필요 vs 숨김: + +구분 | 검토 필요 | 숨김 +------------- | ---------------------- | ------------------- +역할 | 자동 배치 알림 | 의도적 숨김 +뷰어에서 | 보임 (자동 배치) | 안 보임 +디자이너에서 | ReviewPanel 표시 | HiddenPanel 표시 +판단 기준 | 오버라이드 없음 | hidden 배열에 ID +색상 테마 | 파란색 (안내) | 회색 (제외) +``` + +### 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `gridUtils.ts` | convertAndResolvePositions 자동 줄바꿈, needsReview 추가, isOutOfBounds deprecated | +| `PopCanvas.tsx` | OutOfBoundsPanel → ReviewPanel 변경, needsReview 필터링 | +| `PopRenderer.tsx` | isOutOfBounds import 제거 (사용 안 함) | +| `README.md` | v5.1 버전 표시, 최신 기능 요약 | +| `CHANGELOG.md` | v5.1 항목 추가 | + +--- + +## [2026-02-05 심야] 반응형 레이아웃 + 숨김 기능 완성 + +### 배경 (왜 이 작업이 필요했는가) + +**문제 상황**: +- 12칸 모드에서 배치한 컴포넌트가 4칸 모드에서 초과됨 +- 모드별로 컴포넌트 위치/크기를 다르게 설정할 방법 없음 +- 특정 모드에서만 컴포넌트를 숨길 방법 없음 + +**해결 방향**: +- 모드별 오버라이드 시스템으로 위치/크기 개별 저장 +- 화면 밖 컴포넌트를 별도 패널에 표시하고 드래그로 재배치 +- 숨김 기능으로 특정 모드에서 컴포넌트 제외 + +### Added + +- **모드별 오버라이드 시스템** (PopDesigner.tsx, pop-layout.ts) + - `PopModeOverrideV5.positions`: 모드별 컴포넌트 위치 저장 + - `PopModeOverrideV5.hidden`: 모드별 숨김 컴포넌트 ID 배열 + - `getEffectiveComponentPosition()`: 오버라이드된 위치 반환 + - 드래그/리사이즈 시 자동으로 오버라이드 저장 + +- **화면 밖 컴포넌트 패널** (PopCanvas.tsx) + - `OutOfBoundsPanel`: 현재 모드에서 초과하는 컴포넌트 표시 + - `OutOfBoundsItem`: 드래그 가능한 회색 컴포넌트 카드 + - `isOutOfBounds()`: 컴포넌트가 현재 모드 칸 수 초과 여부 판단 + - 클릭하면 숨김 패널로 이동 + +- **숨김 기능** (PopDesigner.tsx, PopCanvas.tsx) + - `HiddenPanel`: 숨김 처리된 컴포넌트 표시 + - `HiddenItem`: 드래그로 숨김 해제 가능 + - `handleHideComponent()`: 컴포넌트 숨김 처리 + - `handleUnhideComponent()`: 숨김 해제 (handleMoveComponent에 통합) + - 숨김 방법 3가지: + 1. 그리드 → 숨김패널 드래그 + 2. H키 단축키 + 3. 화면밖 컴포넌트 클릭 + +- **리사이즈 겹침 검사** (PopRenderer.tsx) + - `checkResizeOverlap()`: 리사이즈 시 다른 컴포넌트와 겹침 검사 + - 겹치면 리사이즈 취소 및 toast 알림 + +- **원본으로 되돌리기** (PopDesigner.tsx) + - `handleResetToDefault()`: 현재 모드 오버라이드 삭제 + - 자동 위치 계산으로 복원 + +### Fixed + +- **숨김 컴포넌트 드래그 안됨 버그** + - 원인: `onUnhideComponent`와 `onMoveComponent`가 별도로 호출되어 상태 충돌 + - 해결: `handleMoveComponent`에서 숨김 해제 로직 통합 (단일 상태 업데이트) + +- **그리드 범위 초과 에러** + - 원인: 드롭 위치 + colSpan이 칸 수 초과 + - 해결: 드롭 시 `adjustedCol` 계산하여 자동으로 왼쪽으로 밀어서 배치 + +- **getAllEffectivePositions에 숨김 컴포넌트 포함** + - 해결: 숨김 및 화면밖 컴포넌트를 결과에서 제외 + +- **Expected drag drop context 에러 (뷰어 페이지)** + - 원인: `DraggableComponent`에서 `useDrag` 훅이 `DndProvider` 없이 호출됨 + - 해결: `isDesignMode=false`일 때 `DraggableComponent` 대신 일반 `div`로 렌더링 + +### Changed + +- **PopModeOverrideV5 타입 확장** + ```typescript + interface PopModeOverrideV5 { + positions?: Record>; // 위치 오버라이드 + hidden?: string[]; // 숨김 컴포넌트 ID 배열 + } + ``` + +- **12칸 모드(tablet_landscape) 제한** + - 기본 모드이므로 숨김 기능 비활성화 + - 화면밖 패널 표시 안함 + - 위치 변경은 기본 position에 직접 저장 + +- **패널 레이아웃 재구성** (PopCanvas.tsx) + - 오른쪽에 화면밖 패널 + 숨김 패널 세로 배치 + - 12칸 모드에서는 패널 숨김 + +### Technical Details + +``` +오버라이드 데이터 흐름: + +1. 컴포넌트 드래그/리사이즈 + ↓ +2. currentMode 확인 + ↓ +3-a. tablet_landscape → layout.components[id].position 직접 수정 +3-b. 다른 모드 → layout.overrides[mode].positions[id]에 저장 + ↓ +4. getEffectiveComponentPosition()이 우선순위대로 반환 + 우선순위: overrides > autoResolved > 기본 position + +숨김 기능 흐름: + +1. 숨김 요청 (드래그/H키/클릭) + ↓ +2. layout.overrides[mode].hidden 배열에 ID 추가 + ↓ +3. PopRenderer에서 hidden 체크 → 렌더링 제외 + ↓ +4. HiddenPanel에서 표시 + ↓ +5. 드래그로 그리드에 복원 → hidden 배열에서 제거 + 위치 업데이트 (단일 상태 업데이트) +``` + +### 수정 파일 +| 파일 | 변경 내용 | +|------|----------| +| `pop-layout.ts` | PopModeOverrideV5.hidden 추가 | +| `PopDesigner.tsx` | handleHideComponent, handleUnhideComponent 통합, 오버라이드 저장 | +| `PopCanvas.tsx` | OutOfBoundsPanel, HiddenPanel 추가, 드롭 위치 자동 조정 | +| `PopRenderer.tsx` | 숨김 필터링, 리사이즈 겹침 검사 | +| `gridUtils.ts` | getAllEffectivePositions에서 숨김/화면밖 제외, isOutOfBounds 함수 | + +--- + +## [2026-02-05 저녁] 드래그앤드롭 완전 수정 + +### 배경 (왜 좌표 계산이 틀렸는가) + +**문제 상황**: +- 컴포넌트를 아래로 드래그해도 위로 올라감 +- Row 92 같은 비정상적인 좌표로 배치됨 +- 드래그 이동/리사이즈가 전혀 작동하지 않음 + +**핵심 원인**: 캔버스에 `transform: scale(0.8)` 적용 시 좌표 계산 불일치 +``` +문제: +- getBoundingClientRect() → 스케일 적용된 크기 반환 (예: 1024px → 819px) +- getClientOffset() → 뷰포트 기준 실제 마우스 좌표 +- 이 둘을 그대로 계산하면 좌표가 완전히 틀림 +``` + +**해결**: 단순한 상대 좌표 + 스케일 보정 +```typescript +// 캔버스 내 상대 좌표 (스케일 보정) +const relX = (마우스X - 캔버스left) / canvasScale; +const relY = (마우스Y - 캔버스top) / canvasScale; +calcGridPosition(relX, relY, customWidth, ...); // 실제 캔버스 크기 사용 +``` + +### Added +- **`calcGridPosition()` 함수** (PopCanvas.tsx) + - 캔버스 내 상대 좌표를 그리드 좌표로 변환 + - 패딩, gap, 셀 너비를 고려한 정확한 계산 + +- **공통 DND 상수** (constants/dnd.ts) + - `DND_ITEM_TYPES.COMPONENT`: 팔레트에서 새 컴포넌트 + - `DND_ITEM_TYPES.MOVE_COMPONENT`: 기존 컴포넌트 이동 + - 3개 파일에서 중복 정의되던 것을 통합 + +### Fixed +- **스케일 보정 누락** + - 캔버스 줌(scale)이 적용된 상태에서 좌표 계산 오류 + - `(offset - rect.left) / scale`로 보정 + +- **DND 타입 상수 불일치** + - PopCanvas: `"component"`, `"MOVE_COMPONENT"` + - PopRenderer: `"MOVE_COMPONENT"` (하드코딩) + - ComponentPalette: `"component"` (로컬 정의) + - 모두 공통 상수로 통합 + +- **컴포넌트 중첩(겹침) 문제** + - 원인: `toast` import 누락으로 겹침 감지 로직이 실행 안됨 + - 해결: `sonner`에서 toast import 추가 + - 겹침 시 `findNextEmptyPosition()`으로 자동 재배치 + +- **리사이즈 핸들 작동 안됨** + - 원인: `useDrop` 훅 2개가 같은 `canvasRef`에 중복 적용 + - 해결: 단일 `useDrop`으로 통합 (`COMPONENT` + `MOVE_COMPONENT` 모두 처리) + +- **불필요한 toast 메시지 제거** + - "컴포넌트가 이동되었습니다" 알림 삭제 + +### Changed +- **mouseToGridPosition 단순화** + - 복잡한 DOMRect 전달 대신 필요한 값만 직접 전달 + - gridUtils.ts의 함수는 유지 (다른 곳에서 사용) + +### Technical Details +``` +좌표 변환 흐름 (수정 후): + +1. 마우스 드롭 + offset = monitor.getClientOffset() // 뷰포트 기준 {x: 500, y: 300} + +2. 캔버스 위치 + canvasRect = canvasRef.getBoundingClientRect() // {left: 250, top: 100} + +3. 스케일 보정된 상대 좌표 + relX = (500 - 250) / 0.8 = 312.5 // 캔버스 내 실제 X + relY = (300 - 100) / 0.8 = 250 // 캔버스 내 실제 Y + +4. 그리드 좌표 계산 + calcGridPosition(312.5, 250, 1024, 12, 48, 16, 24) + → { col: 5, row: 4 } +``` + +### 수정 파일 +| 파일 | 변경 내용 | +|------|----------| +| `PopCanvas.tsx` | calcGridPosition 추가, 스케일 보정 적용 | +| `PopDesigner.tsx` | toast 메시지 제거 | +| `PopRenderer.tsx` | DND 상수 import | +| `ComponentPalette.tsx` | DND 상수 import | +| `constants/dnd.ts` | 새 파일 (DND 타입 상수) | +| `constants/index.ts` | 새 파일 (export) | + +--- + +## [2026-02-05 오후] 그리드 가이드 CSS Grid 통합 + +### 배경 (왜 재설계했는가) + +**문제 상황**: +- GridGuide.tsx(SVG 기반)와 PopRenderer.tsx(CSS Grid)가 좌표계 불일치 +- 격자선과 컴포넌트가 정렬되지 않음 ("무늬가 따로 논다") +- 행/열 라벨이 4부터 시작하는 등 오류 + +**핵심 원칙**: +> "격자선은 컴포넌트와 같은 좌표계에서 태어나야 한다" + +**결정**: SVG 격자 삭제, CSS Grid 기반 통합 +→ 상세: [decisions/004-grid-guide-integration.md](./decisions/004-grid-guide-integration.md) + +### Breaking Changes +- `GridGuide.tsx` 삭제 (SVG 기반 격자) + +### Added +- **CSS Grid 기반 격자 셀** (PopRenderer.tsx) + - `gridCells`: 12x20 = 240개 실제 DOM 셀 + - `border-dashed border-blue-300/40` 스타일 + - 컴포넌트는 `z-index:10`으로 위에 표시 + - `showGridGuide` prop으로 ON/OFF + +- **행/열 라벨** (PopCanvas.tsx) + - 열 라벨: 1~12 (캔버스 상단) + - 행 라벨: 1~20 (캔버스 좌측) + - absolute positioning으로 정확한 정렬 + - 줌/패닝에 연동 + +- **그리드 토글 버튼** (PopCanvas.tsx) + - "그리드 ON/OFF" 버튼 추가 + - 격자 표시 상태 관리 + +### Changed +- **컴포넌트 타입 단순화** + - `PopComponentType`: `pop-sample` 1개로 단순화 + - `DEFAULT_COMPONENT_GRID_SIZE`: `pop-sample` 전용 + - `ComponentPalette.tsx`: 샘플 박스 1개만 표시 + - `PopRenderer.tsx`: 샘플 박스 렌더링으로 단순화 + +### Technical Details +``` +역할 분담: +- PopRenderer: 격자 셀(div) + 컴포넌트 (같은 CSS Grid 좌표계) +- PopCanvas: 라벨 + 줌/패닝 + 토글 +- GridGuide: 삭제 + +격자 셀 구조: +┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ +│1,1│2,1│3,1│4,1│5,1│6,1│7,1│8,1│9,1│10│11│12 │ ← col +├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ +│1,2│... │ +└───┴───────────────────────────────────────────┘ + ↑ row +``` + +--- + +## [2026-02-05] v5 그리드 시스템 완전 통합 + +### 배경 (왜 v5로 전환했는가) + +**문제 상황**: +- v4 Flexbox로 반응형 구현 시도 → 배치가 예측 불가능 +- 캔버스에 "그리듯이" 배치하면 화면 크기별로 깨짐 + +**상급자 피드백**: +> "이런 식이면 나중에 문제가 생긴다." +> "스크린의 픽셀 규격과 마진 간격 규칙을 설정해라. +> 큰 화면 디자인의 전체 프레임 규격과 사이즈 간격 규칙을 정한 다음에 +> 거기에 컴포넌트를 끼워 맞추듯 우리의 규칙 내로 움직이게 바탕을 잡아라." + +**연구 내용**: +- Softr: 블록 기반, 제약 기반 레이아웃 +- Ant Design: 24열 그리드, 8px 간격 +- Material Design: 4/8/12열, 반응형 브레이크포인트 + +**결정**: CSS Grid 기반 그리드 시스템 (v5) 채택 +→ 상세: [decisions/003-v5-grid-system.md](./decisions/003-v5-grid-system.md) + +### popdocs 문서 구조 재정비 + +**배경**: 문서가 AI 에이전트 진입점 역할을 못함, 컨텍스트 효율화 필요 + +**적용 기법**: Progressive Disclosure (점진적 공개), Token as Currency + +**추가된 파일**: +- `SAVE_RULES.md`: AI 저장/조회 규칙, 템플릿 +- `STATUS.md`: 현재 진행 상태, 중단점 +- `PROBLEMS.md`: 문제-해결 색인 +- `INDEX.md`: 기능별 색인 +- `sessions/`: 날짜별 작업 기록 + +**문서 계층**: +- Layer 1 (진입점): README, STATUS, SAVE_RULES +- Layer 2 (상세): CHANGELOG, PROBLEMS, INDEX, FILES, ARCHITECTURE +- Layer 3 (심화): decisions/, sessions/, archive/ + +### Breaking Changes +- **v1, v2, v3, v4 레이아웃 완전 삭제** +- 기존 POP 화면 데이터 전체 초기화 필요 +- 레거시 컴포넌트 및 타입 삭제 + +### Added +- **CSS Grid 기반 그리드 시스템 (v5)** + - 4개 모드별 칸 수: 4/6/8/12칸 + - 명시적 위치 지정 (col, row, colSpan, rowSpan) + - 모드별 오버라이드 지원 + - 자동 위치 변환 (12칸 기준 → 다른 모드) + +- **통합된 파일 구조** + - `PopCanvas.tsx`: 그리드 캔버스 (DnD + 줌 + 모드 전환) + - `PopRenderer.tsx`: 그리드 렌더링 + - `ComponentEditorPanel.tsx`: 속성 편집 + - `pop-layout.ts`: v5 전용 타입 정의 + - `gridUtils.ts`: 그리드 유틸리티 함수 + +### Removed +- `PopCanvasV4.tsx`, `PopCanvas.tsx (v3)` +- `PopFlexRenderer.tsx`, `PopLayoutRenderer.tsx` +- `ComponentEditorPanelV4.tsx`, `PopPanel.tsx` +- v1, v2, v3, v4 타입 정의 및 유틸리티 함수 +- `test-v4` 테스트 페이지 + +### Changed +- `screenManagementService.ts`: v5 전용으로 단순화 +- `screen_layouts_pop` 테이블: 기존 데이터 삭제, v5 전용 +- `PopDesigner.tsx`: v5 전용으로 리팩토링 +- 뷰어 페이지: v5 렌더러 전용 + +### Technical Details +```typescript +// v5 그리드 모드 +type GridMode = "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape"; + +// 그리드 설정 +const GRID_BREAKPOINTS = { + mobile_portrait: { columns: 4, rowHeight: 48, gap: 8, padding: 12 }, + mobile_landscape: { columns: 6, rowHeight: 44, gap: 8, padding: 16 }, + tablet_portrait: { columns: 8, rowHeight: 52, gap: 12, padding: 20 }, + tablet_landscape: { columns: 12, rowHeight: 56, gap: 12, padding: 24 }, +}; + +// 컴포넌트 위치 +interface PopGridPosition { + col: number; // 시작 열 (1부터) + row: number; // 시작 행 (1부터) + colSpan: number; // 열 크기 + rowSpan: number; // 행 크기 +} +``` + +--- + +## [2026-02-04] Phase 2.1 완료 - 배치 고정 기능 + +### Added +- **현재 모드 추적** (PopDesigner.tsx) + - `currentViewportMode` 상태 추가 + - PopCanvasV4와 양방향 동기화 + - 모드 변경 시 자동 업데이트 + +- **배치 고정 기능** + - "고정" 버튼 추가 (기본 모드 제외) + - `handleLockLayoutV4()` - 현재 배치를 오버라이드에 저장 + - 배치 정보: direction, wrap, gap, alignItems, justifyContent, children 순서 + +- **오버라이드 초기화 기능** + - `handleResetOverrideV4()` - 오버라이드 삭제 + - "자동으로 되돌리기" 버튼 (편집된 모드만 표시) + - 자동 계산으로 되돌림 + +### Changed +- **PopCanvasV4 Props 구조 변경** + - `currentMode` prop 추가 (외부에서 제어) + - `onModeChange` 콜백 추가 + - `onLockLayout` 콜백 추가 + - 내부 `activeViewport` 상태 제거 (부모가 관리) + +- **프리셋 버튼 동작** + - 클릭 시 부모 상태 업데이트 (`onModeChange`) + - `currentMode` prop 기반으로 활성 상태 표시 + +### Technical Details +```typescript +// 고정 로직 +const handleLockLayoutV4 = () => { + const newLayout = { + ...layoutV4, + overrides: { + ...layoutV4.overrides, + [currentViewportMode]: { + containers: { + root: { + direction: layoutV4.root.direction, + wrap: layoutV4.root.wrap, + gap: layoutV4.root.gap, + children: layoutV4.root.children, // 순서 고정 + // ... 기타 배치 속성 + } + } + } + } + }; +}; + +// 초기화 로직 +const handleResetOverrideV4 = (mode) => { + const newOverrides = { ...layoutV4.overrides }; + delete newOverrides[mode]; + // overrides가 비면 undefined로 설정 +}; +``` + +### UI 변경 +``` +툴바: +[모바일↕] [모바일↔] [태블릿↕] [태블릿↔(기본)] [고정] [자동으로 되돌리기] + +조건부 표시: +- "고정" 버튼: 기본 모드가 아닐 때 +- "자동으로 되돌리기": 오버라이드가 있을 때 +``` + +### 주의사항 +- 크기는 고정하지 않음 (여전히 자동 스케일링) +- 배치만 오버라이드 (순서, 방향, 정렬) +- 최소/최대값 기능은 별도 구현 필요 + +--- + +## [2026-02-04] Phase 2 시작 - 오버라이드 UI 표시 + +### Added +- **오버라이드 데이터 구조** (pop-layout.ts) + - `PopModeOverride` 인터페이스 추가 + - `PopLayoutDataV4.overrides` 필드 추가 + - 3개 모드 오버라이드 지원 (mobile_portrait, mobile_landscape, tablet_portrait) + +- **프리셋 버튼 상태 표시** (PopCanvasV4.tsx) + - 기본 모드: "(기본)" 텍스트 표시 + - 편집된 모드: "(편집)" 텍스트 + 노란색 강조 + - 자동 모드: 기본 스타일 + +### Changed +- **hasOverride 함수 구현** + - `layout.overrides` 필드 체크 + - 컴포넌트/컨테이너 오버라이드 존재 여부 확인 + +--- + +## [2026-02-04] 비율 스케일링 시스템 구현 + +### Added +- **비율 스케일링 시스템** (업계 표준 Scale with Fixed Aspect Ratio) + - 기준 너비: 1024px (10인치 태블릿 가로) + - 최대 너비: 1366px (12인치 태블릿) + - 8~12인치 화면에서 배치 유지, 크기만 비례 조정 + +- **뷰포트 감지** (page.tsx) + - `viewportWidth` state 추가 + - resize 이벤트 리스너로 실시간 감지 + - `Math.min(window.innerWidth, 1366)` 최대값 제한 + +### Changed +- **PopFlexRenderer.tsx** + - `BASE_VIEWPORT_WIDTH = 1024` 상수 추가 + - `scale = viewportWidth / BASE_VIEWPORT_WIDTH` 계산 + - `calculateSizeStyle()` 함수에 scale 파라미터 추가 + - 컴포넌트 크기 (fixedWidth, fixedHeight) 스케일 적용 + - 컨테이너 gap, padding 스케일 적용 + - 디자인 모드: scale = 1 (원본), 뷰어 모드: 실제 scale 적용 + +- **ComponentRendererV4** + - `viewportWidth` prop 추가 + - 내부에서 scale 계산하여 sizeStyle에 적용 + +- **ContainerRenderer** + - scaledGap, scaledPadding 계산하여 containerStyle에 적용 + +### Technical Details +``` +비율 스케일링 계산: +scale = 실제 화면 너비 / 기준 너비 (1024px) + +예시: +- 800px (8인치): scale = 0.78 → 200px 컴포넌트 → 156px +- 1024px (10인치): scale = 1.00 → 200px 컴포넌트 → 200px (기준) +- 1366px (12인치): scale = 1.33 → 200px 컴포넌트 → 266px +- 1920px (데스크톱): max-width 1366px 적용 → 12인치와 동일 + 여백 +``` + +### Fixed +- **DndProvider 에러** (뷰어 페이지) + - 원인: isDesignMode=false일 때 useDrag/useDrop 훅 호출 + - 해결: DraggableComponentWrapper에서 isDesignMode 체크 후 early return + +--- + +## [2026-02-04] Flexbox 가로 배치 + Spacer + Undo/Redo 개선 + +### Added +- **Spacer 컴포넌트** (`pop-spacer`) + - 빈 공간을 차지하여 레이아웃 정렬에 사용 + - 기본 크기: `width: fill`, `height: 48px` + - 디자인 모드에서 점선 배경으로 표시 + - 실제 모드에서는 투명 (공간만 차지) + +- **컴포넌트 순서 변경 (드래그 앤 드롭)** + - 같은 컨테이너 내에서 컴포넌트 순서 변경 가능 + - 드래그 중인 컴포넌트는 반투명하게 표시 + - 드롭 위치는 파란색 테두리로 표시 + - `handleReorderComponentV4` 핸들러 추가 + +### Changed +- **기본 레이아웃 방향 변경** (Flexbox 가로 배치) + - `direction: "vertical"` → `direction: "horizontal"` + - `wrap: false` → `wrap: true` (자동 줄바꿈) + - `alignItems: "stretch"` → `alignItems: "start"` + - 컴포넌트가 가로로 나열되고, 공간 부족 시 다음 줄로 이동 + +- **컴포넌트 기본 크기 타입별 설정** + - 필드: 200x48px (fixed) + - 버튼: 120x48px (fixed) + - 리스트: fill x 200px + - 인디케이터: 120x80px (fixed) + - 스캐너: 200x48px (fixed) + - 숫자패드: 200x280px (fixed) + - Spacer: fill x 48px + +- **Undo/Redo 방식 개선** (데스크탑 모드와 동일) + - `useLayoutHistory` 훅 제거 + - 별도 `history[]`, `historyIndex` 상태로 관리 + - `saveToHistoryV4()` 함수로 명시적 히스토리 저장 + - 컴포넌트 추가/삭제/수정/순서변경 시 히스토리 저장 + +- **디바이스 스크린 스크롤** + - `overflow: auto` 추가 (컴포넌트가 넘치면 스크롤) + - `height` → `minHeight` 변경 (컨텐츠에 따라 높이 증가) + +### Technical Details +``` +업계 표준 레이아웃 방식 (Figma, Webflow, FlutterFlow): +1. Flexbox 기반 Row/Column 배치 +2. 크기 제어: Fill / Fixed / Hug +3. Spacer 컴포넌트로 정렬 조정 +4. 화면 크기별 조건 분기 (반응형) + +사용 예시: +[버튼A] [Spacer(fill)] [버튼B] → 버튼B가 오른쪽 끝으로 +[Spacer] [컴포넌트] [Spacer] → 컴포넌트가 가운데로 +``` + +--- + +## [2026-02-04] 드래그 리사이즈 + Undo/Redo 기능 + +### Added +- **useLayoutHistory.ts** - Undo/Redo 히스토리 훅 + - 최대 50개 히스토리 저장 + - `undo()`, `redo()`, `canUndo`, `canRedo` + - `reset()` - 새 레이아웃 로드 시 히스토리 초기화 + +- **드래그 리사이즈 핸들** (PopFlexRenderer) + - 오른쪽 핸들: 너비 조정 (cursor: ew-resize) + - 아래쪽 핸들: 높이 조정 (cursor: ns-resize) + - 오른쪽 아래 핸들: 너비+높이 동시 조정 (cursor: nwse-resize) + - 선택된 컴포넌트에만 표시 + - 최소 크기 보장 (너비 48px, 높이 touchTargetMin) + +### Changed +- **PopDesigner.tsx** + - `useLayoutHistory` 훅 통합 (v3, v4 각각 독립적) + - Undo/Redo 버튼 추가 (툴바 오른쪽) + - 단축키 등록: + - `Ctrl+Z` / `Cmd+Z`: 실행 취소 + - `Ctrl+Shift+Z` / `Cmd+Shift+Z` / `Ctrl+Y`: 다시 실행 + - `handleResizeComponentV4` 핸들러 추가 + +- **PopCanvasV4.tsx** + - `onResizeComponent` prop 추가 + - PopFlexRenderer에 전달 + +- **PopFlexRenderer.tsx** + - `onComponentResize` prop 추가 + - ComponentRendererV4에 리사이즈 핸들 추가 + - 드래그 이벤트 처리 (mousemove, mouseup) + +### 단축키 목록 +| 단축키 | 기능 | +|--------|------| +| `Delete` / `Backspace` | 선택된 컴포넌트 삭제 | +| `Ctrl+Z` / `Cmd+Z` | 실행 취소 (Undo) | +| `Ctrl+Shift+Z` / `Ctrl+Y` | 다시 실행 (Redo) | +| `Space` + 드래그 | 캔버스 패닝 | +| `Ctrl` + 휠 | 줌 인/아웃 | + +--- + +## [2026-02-04] v4 통합 설계 모드 Phase 1 완료 + +### 목표 +v4를 기본 레이아웃 모드로 통합하고, 새 화면은 자동으로 v4로 시작 + +### Added +- **ComponentPaletteV4.tsx** - v4 전용 컴포넌트 팔레트 + - 6개 컴포넌트 (필드, 버튼, 리스트, 인디케이터, 스캐너, 숫자패드) + - 드래그 앤 드롭 지원 + +### Changed +- **PopDesigner.tsx** - v3/v4 통합 디자이너로 리팩토링 + - v3/v4 탭 제거 (자동 판별) + - 새 화면 → v4로 시작 + - 기존 v3 화면 → v3로 로드 (하위 호환) + - 빈 레이아웃 → v4로 시작 (컴포넌트 유무로 판별) + - 레이아웃 버전 텍스트 표시 ("자동 레이아웃 (v4)" / "4모드 레이아웃 (v3)") + +- **PopCanvasV4.tsx** - 4개 프리셋으로 변경 + - 기존: [모바일] [태블릿] [데스크톱] + - 변경: [모바일↕] [모바일↔] [태블릿↕] [태블릿↔] + - 기본 프리셋: 태블릿 가로 (1024x768) + - 슬라이더 범위: 320~1200px + - 비율 유지: 슬라이더 조절 시 높이도 비율에 맞게 자동 조정 + +### Fixed +- 새 화면이 v3로 열리는 문제 + - 원인: 백엔드가 빈 v2 레이아웃 반환 (version 필드 있음) + - 해결: 컴포넌트 유무로 빈 레이아웃 판별 → v4로 시작 + +### Technical Details +``` +레이아웃 로드 로직: +1. version 필드 확인 +2. components 존재 여부 확인 +3. version 있고 components 있음 → 해당 버전으로 로드 +4. version 없거나 components 없음 → v4로 새로 시작 +``` + +--- + +## [2026-02-04] Phase 2.1 완료 - 배치 고정 기능 (버그 수정) + +### 🔥 주요 버그 수정 +- **layoutV4.root 오염 문제 해결**: 다른 모드에서 편집 시 기본 레이아웃이 변경되던 버그 수정 +- **tempLayout 도입**: 고정 전 임시 배치를 별도 상태로 관리하여 root를 보호 +- **렌더러 병합 로직**: `PopFlexRenderer`에 오버라이드 자동 병합 기능 추가 + +### 데이터 흐름 개선 +1. **기본 모드 (태블릿 가로)** + - 드래그/속성 변경 → `layoutV4.root` 직접 수정 ✅ + - 모든 다른 모드의 기본값으로 사용 + +2. **다른 모드 (모바일 세로 등)** + - 드래그 → `tempLayout` 임시 저장 (화면에만 표시) + - "고정" 버튼 → `layoutV4.overrides[mode]`에 저장 + - 속성 패널 → 비활성화 + 안내 메시지 + +3. **렌더링** + - `tempLayout` 있으면 최우선 표시 (고정 전 미리보기) + - 오버라이드 있으면 `root`와 병합 + - 없으면 `root` 그대로 표시 + +### 수정 파일 +- `PopDesigner.tsx`: tempLayout 상태 추가, 핸들러 수정 +- `PopFlexRenderer.tsx`: 병합 로직 추가 (getMergedRoot) +- `PopCanvasV4.tsx`: tempLayout props 전달 +- `ComponentEditorPanelV4.tsx`: 속성 패널 비활성화 로직 + +--- + +## [2026-02-04] Phase 3 완료 - visibility + 줄바꿈 컴포넌트 + +### 추가 기능 +- **visibility 속성**: 모드별 컴포넌트 표시/숨김 제어 +- **pop-break 컴포넌트**: 강제 줄바꿈 (flex-basis: 100%) +- **컴포넌트 오버라이드 병합**: 모드별 컴포넌트 설정 변경 가능 + +### 타입 정의 +```typescript +interface PopComponentDefinitionV4 { + // 기존 속성... + + // 🆕 모드별 표시/숨김 + visibility?: { + tablet_landscape?: boolean; + tablet_portrait?: boolean; + mobile_landscape?: boolean; + mobile_portrait?: boolean; + }; +} + +// 🆕 줄바꿈 컴포넌트 +type PopComponentType = + | "pop-field" + | "pop-button" + | "pop-list" + | "pop-indicator" + | "pop-scanner" + | "pop-numpad" + | "pop-spacer" + | "pop-break"; // 새로 추가 +``` + +### 렌더러 개선 +- `isComponentVisible()`: visibility 체크 로직 +- `getMergedComponent()`: 컴포넌트 오버라이드 병합 +- pop-break 전용 렌더링 (디자인 모드: 점선, 실제: 높이 0) + +### 삭제 함수 개선 +- `cleanupOverridesAfterDelete()`: 컴포넌트 삭제 시 모든 오버라이드 정리 +- containers.root.children 정리 +- components 오버라이드 정리 +- 빈 오버라이드 자동 제거 + +### UI 개선 +- 속성 패널에 "표시" 탭 추가 (Eye 아이콘) +- 모드별 체크박스 UI +- 반응형 숨김 (hideBelow) 유지 +- 팔레트에 "줄바꿈" 컴포넌트 추가 + +### 사용 예시 +``` +태블릿 가로: +[A] [B] [C] [D] [E] ← 한 줄 + +모바일 세로: +[A] [B] +─────── ← 줄바꿈 (visibility: mobile만 true) +[C] [D] [E] +``` + +### 수정 파일 +- `pop-layout.ts`: 타입 추가, 삭제 함수 수정 +- `PopFlexRenderer.tsx`: visibility, 병합, pop-break 렌더링 +- `ComponentEditorPanelV4.tsx`: 표시 탭 추가 +- `ComponentPaletteV4.tsx`: 줄바꿈 추가 + +--- + +## [2026-02-04] v4 타입 및 렌더러 + +### Added +- **v4 타입 정의** (간결 버전) + - `PopLayoutDataV4` - 단일 소스 레이아웃 + - `PopContainerV4` - 스택 컨테이너 (direction, wrap, gap, alignItems) + - `PopComponentDefinitionV4` - 크기 제약 기반 (size: fixed/fill/hug) + - `PopSizeConstraintV4` - 크기 규칙 + - `PopResponsiveRuleV4` - 반응형 규칙 (breakpoint별 변경) + - `PopGlobalSettingsV4` - 전역 설정 + - `createEmptyPopLayoutV4()` - 생성 함수 + - `isV4Layout()` - 타입 가드 + - CRUD 함수들 (add, remove, update, find) + +- **PopFlexRenderer.tsx** - v4 Flexbox 렌더러 + - 컨테이너 재귀 렌더링 + - 반응형 규칙 적용 + - 크기 제약 → CSS 변환 + +- **ComponentEditorPanelV4.tsx** - v4 속성 편집 패널 + - 크기 제약 편집 UI + - 컨테이너 설정 UI + +- **PopCanvasV4.tsx** - v4 전용 캔버스 + - 뷰포트 프리셋 + - 너비 슬라이더 + - 줌/패닝 + +--- + +## [2026-02-04] (earlier) + +### Added +- 저장/조회 시스템 구축 + - rangraph: AI 장기 기억 (시맨틱 검색, 요약) + - popdocs: 상세 기록 (파일 기반, 히스토리) + - 이중 저장 체계로 검색 + 기록 분리 + +### Changed +- popdocs 문서 구조 정리 + - README.md: 저장/조회 규칙 추가 + - 기존 문서 archive/로 이동 +- 문서 관리 전략 확정 + - 저장 시: 파일 형식 자동 파악 → 형식 맞춰 추가 → rangraph 요약 + - 조회 시: rangraph 시맨틱 검색 + +### Removed +- .cursorrules 변경 계획 철회 (Git 커밋 영향) + +--- + +## [2026-02-03] + +### Added +- v4 제약조건 기반 레이아웃 계획 + - 단일 소스 + 자동 적응 + - 3가지 규칙 (크기, 배치, 반응형) + - ADR: `decisions/001-v4-constraint-based.md` + +--- + +## [2026-02-02] + +### Fixed +- 캔버스 rowSpan 문제 + - 원인: gridTemplateRows 고정 px + - 해결: `1fr` 사용 + +--- + +## [2026-02-01] + +### Fixed +- 4모드 자동 전환 문제 + - 해결: useResponsiveMode 훅 추가 + +--- + +## [2026-01-31] + +### Added +- v3 섹션 제거, 순수 그리드 구조 +- 4개 모드 독립 그리드 + +--- + +## [2026-01-30] + +### Added +- POP 디자이너 기본 구조 +- PopDesigner, PopCanvas 컴포넌트 + +--- + +## [2026-01-29] + +### Added +- screen_layouts_pop 테이블 +- POP 레이아웃 API (CRUD) + +--- + +*최신이 위, 시간순 역순* diff --git a/popdocs/FILES.md b/popdocs/FILES.md new file mode 100644 index 00000000..2504a542 --- /dev/null +++ b/popdocs/FILES.md @@ -0,0 +1,646 @@ +# POP 파일 상세 목록 + +**최종 업데이트: 2026-02-06 (v5.2 브레이크포인트 재설계 + 세로 자동 확장)** + +이 문서는 POP 화면 시스템과 관련된 모든 파일을 나열하고 각 파일의 역할을 설명합니다. + +--- + +## 목차 + +1. [App Router 파일](#1-app-router-파일) +2. [Designer 파일](#2-designer-파일) +3. [Panels 파일](#3-panels-파일) +4. [Renderers 파일](#4-renderers-파일) +5. [Types 파일](#5-types-파일) +6. [Utils 파일](#6-utils-파일) +7. [Management 파일](#7-management-파일) +8. [Dashboard 파일](#8-dashboard-파일) +9. [Library 파일](#9-library-파일) +10. [루트 컴포넌트 파일](#10-루트-컴포넌트-파일) + +--- + +## 1. App Router 파일 + +### `frontend/app/(pop)/layout.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 전용 레이아웃 래퍼 | +| 라우트 그룹 | `(pop)` - URL에 포함되지 않음 | +| 특징 | 데스크톱과 분리된 터치 최적화 환경 | + +--- + +### `frontend/app/(pop)/pop/page.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 메인 대시보드 | +| 경로 | `/pop` | +| 사용 컴포넌트 | `PopDashboard` | + +--- + +### `frontend/app/(pop)/pop/screens/[screenId]/page.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | 개별 POP 화면 뷰어 (v5 전용) | +| 경로 | `/pop/screens/:screenId` | +| 버전 | v5 그리드 시스템 전용 | + +**핵심 코드 구조**: + +```typescript +// v5 레이아웃 상태 +const [layout, setLayout] = useState(createEmptyPopLayoutV5()); + +// 레이아웃 로드 +useEffect(() => { + const popLayout = await screenApi.getLayoutPop(screenId); + + if (isV5Layout(popLayout)) { + setLayout(popLayout); + } else { + // 레거시 레이아웃은 빈 v5로 처리 + setLayout(createEmptyPopLayoutV5()); + } +}, [screenId]); + +// v5 그리드 렌더링 +{hasComponents ? ( + +) : ( + // 빈 화면 +)} +``` + +**제공 기능**: +- 반응형 모드 감지 (useResponsiveModeWithOverride) +- 프리뷰 모드 (`?preview=true`) +- 디바이스/방향 수동 전환 (프리뷰 모드) +- 4개 그리드 모드 지원 + +--- + +### `frontend/app/(pop)/pop/work/page.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | 작업 화면 (샘플) | +| 경로 | `/pop/work` | + +--- + +## 2. Designer 파일 + +### `frontend/components/pop/designer/PopDesigner.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 화면 디자이너 메인 (v5 전용) | +| 의존성 | react-dnd, ResizablePanelGroup | + +**핵심 Props**: + +```typescript +interface PopDesignerProps { + selectedScreen: ScreenDefinition; + onBackToList: () => void; + onScreenUpdate?: (updatedScreen: Partial) => void; +} +``` + +**상태 관리**: + +```typescript +// v5 레이아웃 +const [layout, setLayout] = useState(createEmptyPopLayoutV5()); + +// 선택 상태 +const [selectedComponentId, setSelectedComponentId] = useState(null); + +// 그리드 모드 (4개) +const [currentMode, setCurrentMode] = useState("tablet_landscape"); + +// UI 상태 +const [isLoading, setIsLoading] = useState(true); +const [isSaving, setIsSaving] = useState(false); +const [hasChanges, setHasChanges] = useState(false); +``` + +**주요 핸들러**: + +| 핸들러 | 역할 | +|--------|------| +| `handleDropComponent` | 컴포넌트 드롭 (그리드 위치 계산) | +| `handleUpdateComponent` | 컴포넌트 속성 수정 | +| `handleDeleteComponent` | 컴포넌트 삭제 | +| `handleSave` | v5 레이아웃 저장 | + +--- + +### `frontend/components/pop/designer/PopCanvas.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | v5 CSS Grid 기반 캔버스 + 행/열 라벨 | +| 렌더링 | CSS Grid (4/6/8/12칸) | +| 모드 | 4개 (태블릿/모바일 x 가로/세로) | +| 라벨 | 열 라벨 (1~12), 행 라벨 (1~20) | +| 토글 | 그리드 ON/OFF 버튼 | + +**핵심 Props**: + +```typescript +interface PopCanvasProps { + layout: PopLayoutDataV5; + selectedComponentId: string | null; + currentMode: GridMode; + onModeChange: (mode: GridMode) => void; + onSelectComponent: (id: string | null) => void; + onDropComponent: (type: PopComponentType, position: PopGridPosition) => void; + onUpdateComponent: (componentId: string, updates: Partial) => void; + onDeleteComponent: (componentId: string) => void; +} +``` + +**뷰포트 프리셋** (v5.2 - height 제거됨, 세로 자동 확장): + +```typescript +const VIEWPORT_PRESETS = [ + { id: "mobile_portrait", label: "모바일 세로", width: 375, columns: 4 }, + { id: "mobile_landscape", label: "모바일 가로", width: 600, columns: 6 }, + { id: "tablet_portrait", label: "태블릿 세로", width: 834, columns: 8 }, + { id: "tablet_landscape", label: "태블릿 가로", width: 1024, columns: 12 }, +]; + +// 세로 자동 확장 +const MIN_CANVAS_HEIGHT = 600; +const CANVAS_EXTRA_ROWS = 3; +const dynamicCanvasHeight = useMemo(() => { ... }, []); +``` + +**제공 기능**: +- 4개 모드 프리셋 전환 +- 줌 컨트롤 (30% ~ 150%) +- 패닝 (Space + 드래그) +- 컴포넌트 드래그 앤 드롭 +- 그리드 좌표 계산 + +--- + +### `frontend/components/pop/designer/index.ts` + +```typescript +export { default as PopDesigner } from "./PopDesigner"; +export { default as PopCanvas } from "./PopCanvas"; +export { default as ComponentEditorPanel } from "./panels/ComponentEditorPanel"; +export { default as PopRenderer } from "./renderers/PopRenderer"; +export * from "./types"; +export * from "./utils/gridUtils"; +``` + +--- + +## 3. Panels 파일 + +### `frontend/components/pop/designer/panels/ComponentEditorPanel.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | v5 컴포넌트 편집 패널 | +| 위치 | 오른쪽 사이드바 | + +**핵심 Props**: + +```typescript +interface ComponentEditorPanelProps { + component: PopComponentDefinitionV5 | null; + currentMode: GridMode; + onUpdateComponent?: (updates: Partial) => void; + className?: string; +} +``` + +**3개 탭**: + +| 탭 | 아이콘 | 내용 | +|----|--------|------| +| `grid` | Grid3x3 | 그리드 위치 (col, row, colSpan, rowSpan) | +| `settings` | Settings | 라벨, 타입별 설정 | +| `data` | Database | 데이터 바인딩 (Phase 4) | + +--- + +### `frontend/components/pop/designer/panels/index.ts` + +```typescript +export { default as ComponentEditorPanel, default } from "./ComponentEditorPanel"; +``` + +--- + +## 4. Renderers 파일 + +### `frontend/components/pop/designer/renderers/PopRenderer.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | v5 레이아웃 CSS Grid 렌더러 + 격자 셀 | +| 입력 | PopLayoutDataV5, viewportWidth, currentMode, showGridGuide | +| 격자 | 동적 행 수 (컴포넌트 배치에 따라 자동 계산, CSS Grid 좌표계) | + +**핵심 Props**: + +```typescript +interface PopRendererProps { + layout: PopLayoutDataV5; + viewportWidth: number; + currentMode?: GridMode; + isDesignMode?: boolean; + selectedComponentId?: string | null; + showGridGuide?: boolean; // 격자 표시 여부 + onComponentClick?: (componentId: string) => void; + onBackgroundClick?: () => void; + className?: string; +} +``` + +**격자 셀 렌더링**: + +```typescript +// 동적 행 수 계산 (컴포넌트 배치 기반) +const gridCells = useMemo(() => { + const maxRowEnd = Object.values(components).reduce((max, comp) => { + const pos = getEffectivePosition(comp); + return Math.max(max, pos.row + pos.rowSpan); + }, 1); + const rowCount = Math.max(10, maxRowEnd + 5); + + const cells = []; + for (let row = 1; row <= rowCount; row++) { + for (let col = 1; col <= breakpoint.columns; col++) { + cells.push({ id: `cell-${col}-${row}`, col, row }); + } + } + return cells; +}, [components, overrides, mode, breakpoint.columns]); + +// 컴포넌트와 동일한 CSS Grid 좌표계로 렌더링 +{showGridGuide && gridCells.map(cell => ( +
+))} +``` + +**CSS Grid 스타일 생성**: + +```typescript +const gridStyle: React.CSSProperties = { + display: "grid", + gridTemplateColumns: `repeat(${breakpoint.columns}, 1fr)`, + gridAutoRows: `${breakpoint.rowHeight}px`, + gap: `${breakpoint.gap}px`, + padding: `${breakpoint.padding}px`, +}; +``` + +**컴포넌트 위치 변환**: + +```typescript +const convertPosition = (position: PopGridPosition): React.CSSProperties => ({ + gridColumn: `${position.col} / span ${position.colSpan}`, + gridRow: `${position.row} / span ${position.rowSpan}`, +}); +``` + +--- + +### `frontend/components/pop/designer/renderers/index.ts` + +```typescript +export { default as PopRenderer, default } from "./PopRenderer"; +``` + +--- + +## 5. Types 파일 + +### `frontend/components/pop/designer/types/pop-layout.ts` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 레이아웃 v5 타입 시스템 | +| 버전 | v5 전용 (레거시 제거됨) | + +**핵심 타입**: + +```typescript +// 그리드 모드 +type GridMode = "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape"; + +// 그리드 브레이크포인트 +interface GridBreakpoint { + label: string; + columns: number; + minWidth: number; + maxWidth: number; + rowHeight: number; + gap: number; + padding: number; +} + +// v5 레이아웃 +interface PopLayoutDataV5 { + version: "pop-5.0"; + gridConfig: PopGridConfig; + components: Record; + dataFlow: PopDataFlow; + settings: PopGlobalSettingsV5; + metadata?: PopLayoutMetadata; + overrides?: Record; +} + +// 그리드 위치 +interface PopGridPosition { + col: number; // 시작 열 (1부터) + row: number; // 시작 행 (1부터) + colSpan: number; // 열 크기 + rowSpan: number; // 행 크기 +} + +// v5 컴포넌트 정의 +interface PopComponentDefinitionV5 { + id: string; + type: PopComponentType; + label?: string; + position: PopGridPosition; + visibility?: { modes: GridMode[]; defaultVisible: boolean }; + dataBinding?: PopDataBinding; + style?: PopStylePreset; + config?: PopComponentConfig; +} +``` + +**주요 함수**: + +| 함수 | 역할 | +|------|------| +| `createEmptyPopLayoutV5()` | 빈 v5 레이아웃 생성 | +| `addComponentToV5Layout()` | v5에 컴포넌트 추가 | +| `createComponentDefinitionV5()` | v5 컴포넌트 정의 생성 | +| `isV5Layout()` | v5 타입 가드 | +| `detectGridMode()` | 뷰포트 너비로 모드 감지 | + +--- + +### `frontend/components/pop/designer/types/index.ts` + +```typescript +export * from "./pop-layout"; +``` + +--- + +## 5.5. Constants 파일 (신규) + +### `frontend/components/pop/designer/constants/dnd.ts` + +| 항목 | 내용 | +|------|------| +| 역할 | DnD(Drag and Drop) 관련 상수 | +| 생성일 | 2026-02-05 | + +**핵심 상수**: + +```typescript +export const DND_ITEM_TYPES = { + /** 팔레트에서 새 컴포넌트 드래그 */ + COMPONENT: "POP_COMPONENT", + /** 캔버스 내 기존 컴포넌트 이동 */ + MOVE_COMPONENT: "POP_MOVE_COMPONENT", +} as const; +``` + +**사용처**: +- `PopCanvas.tsx` - useDrop accept 타입 +- `PopRenderer.tsx` - useDrag type +- `ComponentPalette.tsx` - useDrag type + +--- + +### `frontend/components/pop/designer/constants/index.ts` + +```typescript +export * from "./dnd"; +``` + +--- + +## 6. Utils 파일 + +### `frontend/components/pop/designer/utils/gridUtils.ts` + +| 항목 | 내용 | +|------|------| +| 역할 | 그리드 위치 계산 유틸리티 | +| 용도 | 좌표 변환, 겹침 감지, 자동 배치 | + +**주요 함수**: + +| 함수 | 역할 | +|------|------| +| `convertPositionToMode()` | 12칸 기준 위치를 다른 모드로 변환 | +| `isOverlapping()` | 두 위치 겹침 여부 확인 | +| `resolveOverlaps()` | 겹침 해결 (아래로 밀기) | +| `mouseToGridPosition()` | 마우스 좌표 → 그리드 좌표 | +| `gridToPixelPosition()` | 그리드 좌표 → 픽셀 좌표 | +| `isValidPosition()` | 위치 유효성 검사 | +| `clampPosition()` | 위치 범위 조정 | +| `findNextEmptyPosition()` | 다음 빈 위치 찾기 | +| `autoLayoutComponents()` | 자동 배치 | + +--- + +## 7. Management 파일 + +### `frontend/components/pop/management/PopCategoryTree.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 화면 카테고리 트리 | +| 기능 | 그룹 추가/수정/삭제, 화면 목록 | + +--- + +### `frontend/components/pop/management/PopScreenSettingModal.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 화면 설정 모달 | +| 기능 | 화면명, 설명, 그룹 설정 | + +--- + +### `frontend/components/pop/management/PopScreenPreview.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 화면 미리보기 | +| 기능 | 썸네일, 기본 정보 표시 | + +--- + +### `frontend/components/pop/management/PopScreenFlowView.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | 화면 간 플로우 시각화 | +| 기능 | 화면 연결 관계 표시 | + +--- + +### `frontend/components/pop/management/index.ts` + +```typescript +export { PopCategoryTree } from "./PopCategoryTree"; +export { PopScreenSettingModal } from "./PopScreenSettingModal"; +export { PopScreenPreview } from "./PopScreenPreview"; +export { PopScreenFlowView } from "./PopScreenFlowView"; +``` + +--- + +## 8. Dashboard 파일 + +### `frontend/components/pop/dashboard/PopDashboard.tsx` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 대시보드 메인 | +| 구성 | 헤더, KPI, 메뉴그리드, 공지, 푸터 | + +--- + +### 기타 Dashboard 컴포넌트 + +| 파일 | 역할 | +|------|------| +| `DashboardHeader.tsx` | 상단 헤더 | +| `DashboardFooter.tsx` | 하단 푸터 | +| `MenuGrid.tsx` | 메뉴 그리드 | +| `KpiBar.tsx` | KPI 요약 바 | +| `NoticeBanner.tsx` | 공지 배너 | +| `NoticeList.tsx` | 공지 목록 | +| `ActivityList.tsx` | 최근 활동 목록 | + +--- + +## 9. Library 파일 + +### `frontend/lib/api/popScreenGroup.ts` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 화면 그룹 API 클라이언트 | + +**API 함수**: + +```typescript +async function getPopScreenGroups(searchTerm?: string): Promise +async function createPopScreenGroup(data: CreatePopScreenGroupRequest): Promise<...> +async function updatePopScreenGroup(id: number, data: UpdatePopScreenGroupRequest): Promise<...> +async function deletePopScreenGroup(id: number): Promise<...> +async function ensurePopRootGroup(): Promise<...> +``` + +--- + +### `frontend/lib/registry/PopComponentRegistry.ts` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 컴포넌트 중앙 레지스트리 | + +--- + +### `frontend/lib/schemas/popComponentConfig.ts` + +| 항목 | 내용 | +|------|------| +| 역할 | POP 컴포넌트 설정 스키마 | +| 검증 | Zod 기반 | + +--- + +## 10. 루트 컴포넌트 파일 + +### `frontend/components/pop/index.ts` + +```typescript +export * from "./designer"; +export * from "./management"; +export * from "./dashboard"; +// 개별 컴포넌트 export +``` + +--- + +### 기타 루트 레벨 컴포넌트 + +| 파일 | 역할 | +|------|------| +| `PopApp.tsx` | POP 앱 셸 | +| `PopHeader.tsx` | 공통 헤더 | +| `PopBottomNav.tsx` | 하단 네비게이션 | +| `PopStatusTabs.tsx` | 상태 탭 | +| `PopWorkCard.tsx` | 작업 카드 | +| `PopProductionPanel.tsx` | 생산 패널 | +| `PopSettingsModal.tsx` | 설정 모달 | +| `PopAcceptModal.tsx` | 수락 모달 | +| `PopProcessModal.tsx` | 프로세스 모달 | +| `PopEquipmentModal.tsx` | 설비 모달 | + +--- + +## 파일 수 통계 + +| 폴더 | 파일 수 | 설명 | +|------|---------|------| +| `app/(pop)` | 4 | App Router 페이지 | +| `components/pop/designer` | 11 | 디자이너 모듈 (v5) - constants 포함 | +| `components/pop/management` | 5 | 관리 모듈 | +| `components/pop/dashboard` | 12 | 대시보드 모듈 | +| `components/pop` (루트) | 15 | 루트 컴포넌트 | +| `lib` | 3 | 라이브러리 | +| **총계** | **50** | | + +--- + +## 삭제된 파일 (v5 통합으로 제거) + +| 파일 | 이유 | +|------|------| +| `PopCanvasV4.tsx` | v4 Flexbox 캔버스 | +| `PopFlexRenderer.tsx` | v4 Flexbox 렌더러 | +| `PopLayoutRenderer.tsx` | v3 CSS Grid 렌더러 | +| `ComponentRenderer.tsx` | 레거시 컴포넌트 렌더러 | +| `ComponentEditorPanelV4.tsx` | v4 편집 패널 | +| `PopPanel.tsx` | 레거시 팔레트 패널 | +| `test-v4/page.tsx` | v4 테스트 페이지 | +| `GridGuide.tsx` | SVG 기반 격자 가이드 (좌표 불일치로 삭제, CSS Grid 통합) | + +--- + +*이 문서는 POP 화면 시스템의 파일 목록을 관리하기 위한 참조용으로 작성되었습니다. (v5 그리드 시스템 기준)* diff --git a/popdocs/INDEX.md b/popdocs/INDEX.md new file mode 100644 index 00000000..c0f87f16 --- /dev/null +++ b/popdocs/INDEX.md @@ -0,0 +1,239 @@ +# 기능별 색인 + +> **용도**: "이 기능 어디있어?", "비슷한 기능 찾아줘" +> **검색 팁**: Ctrl+F로 기능명, 키워드 검색 + +--- + +## 렌더링 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 그리드 렌더링 | PopRenderer.tsx | `PopRenderer` | CSS Grid 기반 v5 렌더링 | +| 격자 셀 렌더링 | PopRenderer.tsx | `gridCells` (useMemo) | 12x20 = 240개 DOM 셀 | +| 위치 변환 | gridUtils.ts | `convertPositionToMode()` | 12칸 → 4/6/8칸 변환 | +| 모드 감지 | pop-layout.ts | `detectGridMode()` | 뷰포트 너비로 모드 판별 | +| 컴포넌트 스타일 | PopRenderer.tsx | `convertPosition()` | 그리드 좌표 → CSS | +| **실제 컴포넌트 렌더링** | PopRenderer.tsx | `renderActualComponent()` | 레지스트리에서 실제 컴포넌트 조회 후 렌더링 (뷰어 모드) | +| **디자인모드 실제 렌더링** | PopRenderer.tsx | `ComponentContent` (디자인 분기) | 디자인 모드에서도 ActualComp로 실제 데이터 렌더링, pointer-events-none | +| **레지스트리 초기화** | page.tsx (뷰어) | `import "@/lib/registry/pop-components"` | 뷰어에서 컴포넌트 레지스트리 초기화 (side-effect import) | + +## 그리드 가이드 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 격자 셀 | PopRenderer.tsx | `gridCells` | CSS Grid 기반 격자선 (동적 행 수) | +| 열 라벨 | PopCanvas.tsx | `gridLabels.columns` | 1~12 표시 | +| 행 라벨 | PopCanvas.tsx | `gridLabels.rows` | 동적 계산 (dynamicCanvasHeight 기반) | +| 토글 | PopCanvas.tsx | `showGridGuide` 상태 | 격자 ON/OFF | + +## 세로 자동 확장 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 동적 높이 | PopCanvas.tsx | `dynamicCanvasHeight` | 컴포넌트 배치 기반 자동 계산 | +| 최소 높이 | PopCanvas.tsx | `MIN_CANVAS_HEIGHT` | 600px 보장 | +| 여유 행 | PopCanvas.tsx | `CANVAS_EXTRA_ROWS` | 항상 3행 추가 | +| 격자 행 수 | PopRenderer.tsx | `gridCells` | maxRowEnd + 5 동적 계산 | + +## 드래그 앤 드롭 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 드롭 영역 | PopCanvas.tsx | `useDrop` | 캔버스에 컴포넌트 드롭 | +| 좌표 계산 | gridUtils.ts | `mouseToGridPosition()` | 마우스 → 그리드 좌표 | +| 빈 위치 찾기 | gridUtils.ts | `findNextEmptyPosition()` | 자동 배치 | +| DnD 타입 정의 | PopCanvas.tsx | `DND_ITEM_TYPES` | 인라인 정의 | + +## 편집 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 위치 편집 | ComponentEditorPanel.tsx | position 탭 | col, row 수정 | +| 크기 편집 | ComponentEditorPanel.tsx | position 탭 | colSpan, rowSpan 수정 | +| 라벨 편집 | ComponentEditorPanel.tsx | settings 탭 | 컴포넌트 라벨 | +| 표시/숨김 | ComponentEditorPanel.tsx | visibility 탭 | 모드별 표시 | + +## 설정 패널 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 속성 편집 탭 | ComponentEditorPanel.tsx | `Tabs` | 위치/설정/표시/데이터 4탭 (min-h-0 스크롤 지원) | +| **배치 컴포넌트 목록** | ComponentEditorPanel.tsx | 위치 탭 내부 | 그리드에 배치된 컴포넌트 리스트 + 클릭 선택 연동 | + +## 레이아웃 관리 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 컴포넌트 추가 | pop-layout.ts | `addComponentToV5Layout()` | v5에 컴포넌트 추가 | +| 빈 레이아웃 | pop-layout.ts | `createEmptyPopLayoutV5()` | 초기 레이아웃 생성 | +| 타입 가드 | pop-layout.ts | `isV5Layout()` | v5 여부 확인 | + +## 상태 관리 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 레이아웃 상태 | PopDesigner.tsx | `useState` | 메인 레이아웃 | +| 히스토리 | PopDesigner.tsx | `history[]`, `historyIndex` | Undo/Redo | +| 선택 상태 | PopDesigner.tsx | `selectedComponentId` | 현재 선택 | +| 모드 상태 | PopDesigner.tsx | `currentMode` | 그리드 모드 | + +## 저장/로드 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 레이아웃 로드 | PopDesigner.tsx | `useEffect` | 화면 로드 시 | +| 레이아웃 저장 | PopDesigner.tsx | `handleSave()` | 저장 버튼 | +| API 호출 | screen.ts (lib/api) | `screenApi.saveLayoutPop()` | 백엔드 통신 | + +## 뷰포트/줌 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 프리셋 전환 | PopCanvas.tsx | `VIEWPORT_PRESETS` | 4개 모드 (width만, height 제거) | +| 줌 컨트롤 | PopCanvas.tsx | `canvasScale` | 30%~150% | +| 패닝 | PopCanvas.tsx | Space + 드래그 | 캔버스 이동 | +| 모드 감지 | pop-layout.ts | `detectGridMode()` | 너비 기반 모드 판별 | + +## 브레이크포인트 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 그리드 설정 | pop-layout.ts | `GRID_BREAKPOINTS` | 모드별 칸 수, gap, padding | +| 모드 감지 | pop-layout.ts | `detectGridMode()` | viewportWidth → GridMode | +| 훅 연동 | useDeviceOrientation.ts | `BREAKPOINTS.TABLET_MIN` | 768px (태블릿 경계) | + +## 자동 줄바꿈/검토 + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 자동 배치 | gridUtils.ts | `convertAndResolvePositions()` | col > maxCol → 맨 아래 배치 | +| 검토 필요 판별 | gridUtils.ts | `needsReview()` | 오버라이드 없으면 true | +| 검토 패널 | PopCanvas.tsx | `ReviewPanel` | 검토 필요 컴포넌트 목록 | + +--- + +## pop-dashboard + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| **메인 컴포넌트** | PopDashboardComponent.tsx | `PopDashboardComponent` | 뷰어용 대시보드 렌더링, Promise.allSettled 병렬 로딩 | +| **디자이너 미리보기** | PopDashboardPreview.tsx | `PopDashboardPreviewComponent` | 더미 데이터 기반 미리보기 | +| **설정 패널** | PopDashboardConfig.tsx | `PopDashboardConfigPanel` | 3탭 (기본/아이템/페이지) | +| **레지스트리 등록** | pop-dashboard/index.tsx | `PopComponentRegistry.registerComponent()` | 컴포넌트 등록 엔트리 | +| **마이그레이션** | PopDashboardComponent.tsx | `migrateConfig()` | 레거시 config -> pages 기반으로 런타임 변환 | +| **페이지 편집기** | PopDashboardConfig.tsx | `PageEditor` | 페이지별 독립 그리드 레이아웃 편집 UI | + +### 서브타입 (items/) + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| KPI 카드 | items/KpiCard.tsx | `KpiCardComponent` | 숫자+단위+증감, Container Query 반응형 | +| 차트 | items/ChartItem.tsx | `ChartItemComponent` | Recharts (bar/pie/line) | +| 게이지 | items/GaugeItem.tsx | `GaugeItemComponent` | SVG 반원형 게이지 | +| 통계 카드 | items/StatCard.tsx | `StatCardComponent` | 카테고리별 건수 | + +### 표시 모드 (modes/) + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 좌우 버튼 | modes/ArrowsMode.tsx | `ArrowsModeComponent` | 좌우 화살표 + 인디케이터 (콘텐츠 위 오버레이) | +| 자동 슬라이드 | modes/AutoSlideMode.tsx | `AutoSlideModeComponent` | 자동 전환 + 터치 일시정지 + 인디케이터 오버레이 | +| 그리드 | modes/GridMode.tsx | `GridModeComponent` | CSS Grid + @container 셀 | +| 스크롤 | modes/ScrollMode.tsx | `ScrollModeComponent` | scroll-snap 가로 스크롤 | + +### 유틸리티 (utils/) + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| 수식 계산 | utils/formula.ts | `evaluateFormula()` | 재귀 하강 파서 (eval 미사용) | +| 수식 포맷 | utils/formula.ts | `formatFormulaResult()` | value/fraction/percent/ratio | +| 수식 검증 | utils/formula.ts | `validateExpression()` | 변수 ID 유효성 검사 | +| 숫자 축약 | utils/formula.ts | `abbreviateNumber()` | 1.2만, 1.2억 등 | +| **데이터 검증** | utils/dataFetcher.ts | `validateDataSourceConfig()` | 테이블/컬럼/조인 미완료 시 SQL 차단 | +| 집계 데이터 | utils/dataFetcher.ts | `fetchAggregatedData()` | @INFRA-EXTRACT API 호출 (validate 포함) | +| SQL 생성 | utils/dataFetcher.ts | `buildAggregationSQL()` | DataSourceConfig -> SQL (COUNT(*) 자동 처리) | +| WHERE 생성 | utils/dataFetcher.ts | `buildWhereClause()` | 필터 조건 -> WHERE (빈 컬럼 무시) | +| 테이블 목록 | utils/dataFetcher.ts | `fetchTableList()` | 설정 패널용 | +| 컬럼 목록 | utils/dataFetcher.ts | `fetchTableColumns()` | tableManagementApi 우선, dashboardApi 폴백 | +| **숫자 변환** | utils/dataFetcher.ts | `fetchAggregatedData()` 내부 | PostgreSQL bigint 문자열 -> Number() 변환 (PieChart 필수) | + +### 타입 (types.ts에 추가) + +| 타입 | 용도 | +|------|------| +| `DataSourceConfig` | 데이터 소스 설정 (Phase 0 공통) | +| `ColumnBinding` | 컬럼 바인딩 (Phase 0 공통) | +| `JoinConfig` | 테이블 조인 (Phase 0 공통) | +| `PopActionConfig` | 액션 설정 (Phase 0 공통) | +| `PopDashboardConfig` | 대시보드 전체 설정 | +| `DashboardItem` | 개별 아이템 설정 | +| `DashboardCell` | 그리드 모드 셀 | +| `DashboardPage` | 페이지(슬라이드) - 독립 그리드 레이아웃 (id, label, gridColumns, gridRows, gridCells) | +| `FormulaConfig` | 계산식 설정 | +| `ItemVisibility` | 아이템 내 요소별 보이기/숨기기 | +| `StatCategory` | 통계 카드 카테고리 (label, filter, color) | +| `GaugeConfig` | 게이지 설정 (min, max, target, colorRanges) | +| `KpiCardConfig` | KPI 카드 설정 (unit, colorRanges, showTrend, trendPeriod) | +| `ChartItemConfig` | 차트 설정 (chartType, xAxisColumn, yAxisColumn, colors) | + +### 설정 패널 UI 요소 (PopDashboardConfig.tsx) + +| 기능 | 컴포넌트/위치 | 설명 | +|------|-------------|------| +| groupBy(X축) Combobox | DataSourceEditor 내부 | 집계 활성 시 X축 카테고리 컬럼 선택 | +| 차트 축 설정 | ItemEditor > Chart 모드 | xAxisColumn, yAxisColumn 입력 | +| 통계 카테고리 편집 | ItemEditor > StatCard 모드 | 카테고리별 라벨/필터/색상 인라인 편집 | +| 표시요소(Visibility) | ItemEditor 하단 | 라벨/값/단위/증감율/보조라벨/목표값 체크박스 | + +--- + +## 파일별 주요 기능 + +| 파일 | 핵심 기능 | +|------|----------| +| PopDesigner.tsx | 레이아웃 로드/저장, 컴포넌트 CRUD, 히스토리 | +| PopCanvas.tsx | DnD, 줌, 패닝, 모드 전환, 행/열 라벨, 격자 토글 | +| PopRenderer.tsx | CSS Grid 렌더링, 격자 셀, 위치 변환, 컴포넌트 표시 | +| ComponentEditorPanel.tsx | 속성 편집 (위치, 크기, 설정, 표시) | +| ComponentPalette.tsx | 컴포넌트 팔레트 (드래그 가능한 컴포넌트 목록) | +| pop-layout.ts | 타입 정의, 유틸리티 함수, 상수 | +| gridUtils.ts | 좌표 계산, 겹침 감지, 자동 배치 | +| pop-components/types.ts | Phase 0 공통 + 대시보드 전용 타입 | +| pop-dashboard/PopDashboardComponent.tsx | 대시보드 뷰어 메인 컴포넌트 | +| pop-dashboard/PopDashboardConfig.tsx | 대시보드 설정 패널 (3탭) | +| pop-dashboard/utils/formula.ts | 수식 파싱/계산/검증/숫자 축약 | +| pop-dashboard/utils/dataFetcher.ts | @INFRA-EXTRACT 데이터 API 호출 | + +--- + +## 공통 훅 (hooks/pop/) + +| 기능 | 파일 | 함수/컴포넌트 | 설명 | +|------|------|--------------|------| +| **이벤트 버스** | hooks/pop/usePopEvent.ts | `usePopEvent(screenId)` | screenId 기반 격리된 이벤트 통신 | +| 이벤트 발행 | hooks/pop/usePopEvent.ts | `publish(eventName, payload)` | 같은 화면 구독자에게 이벤트 전파 | +| 이벤트 구독 | hooks/pop/usePopEvent.ts | `subscribe(eventName, callback)` | 이벤트 구독, unsubscribe 반환 | +| 공유 데이터 조회 | hooks/pop/usePopEvent.ts | `getSharedData(key)` | screenId별 격리된 key-value 조회 | +| 공유 데이터 저장 | hooks/pop/usePopEvent.ts | `setSharedData(key, value)` | screenId별 격리된 key-value 저장 | +| 화면 정리 | hooks/pop/usePopEvent.ts | `cleanupScreen(screenId)` | 리스너 + sharedData 전체 정리 | +| **데이터 CRUD** | hooks/pop/useDataSource.ts | `useDataSource(config)` | DataSourceConfig 기반 DB 통합 훅 | +| 데이터 조회 | hooks/pop/useDataSource.ts | `refetch(options?)` | 필터 오버라이드 가능한 재조회 | +| 데이터 생성 | hooks/pop/useDataSource.ts | `save(record)` | dataApi.createRecord 래핑 | +| 데이터 수정 | hooks/pop/useDataSource.ts | `update(id, record)` | dataApi.updateRecord 래핑 | +| 데이터 삭제 | hooks/pop/useDataSource.ts | `remove(id)` | dataApi.deleteRecord 래핑 | +| SQL 검증 | hooks/pop/popSqlBuilder.ts | `validateDataSourceConfig(config)` | 테이블/컬럼/조인 미완료 시 에러 반환 | +| SQL 생성 | hooks/pop/popSqlBuilder.ts | `buildAggregationSQL(config)` | DataSourceConfig -> SELECT SQL | +| 배럴 파일 | hooks/pop/index.ts | re-export | `@/hooks/pop`으로 import 가능 | + +### 공통 훅 타입 + +| 타입 | 파일 | 용도 | +|------|------|------| +| `DataSourceResult` | hooks/pop/useDataSource.ts | 조회 결과 (rows, value, total) | +| `MutationResult` | hooks/pop/useDataSource.ts | CRUD 결과 (success, data, error) | +| `EventCallback` | hooks/pop/usePopEvent.ts | 이벤트 콜백 타입 | + +--- + +*새 기능 추가 시 해당 카테고리 테이블에 행 추가* diff --git a/popdocs/PLAN.md b/popdocs/PLAN.md new file mode 100644 index 00000000..3020fe0d --- /dev/null +++ b/popdocs/PLAN.md @@ -0,0 +1,551 @@ +# POP 개발 계획 + +--- + +## 현재 상태 (2026-02-11) + +**Phase 0 공통 인프라 (usePopEvent + useDataSource) 구현 완료, ksh-v2-work 병합 + 원격 push 완료. Phase 2 pop-button 설계 진행 중.** + +--- + +## 작업 순서 + +``` +[Phase 1~3] [Phase 5] [정의서] [Phase 0~6] +v4 Flexbox → v5 CSS Grid → 컴포넌트 설계 → 실제 구현 + 완료 완료 (v5.2) 완료 (v8.0) 다음 +``` + +--- + +## 완료된 Phase + +### Phase 1~3: v4 Flexbox 시스템 (완료, 레거시 삭제됨) + +v4 Flexbox 기반 시스템은 v5 CSS Grid로 완전히 대체되었습니다. +v4 관련 파일은 모두 삭제되었습니다. + +- [x] v4 기본 구조, 렌더러, 디자이너 통합 +- [x] Undo/Redo, 드래그 리사이즈, Flexbox 가로 배치 +- [x] 비율 스케일링 시스템 +- [x] 오버라이드 기능 (모드별 배치 고정) +- [x] 컴포넌트 표시/숨김, 줄바꿈 + +### Phase 5: v5 CSS Grid 시스템 (완료) + +#### Phase 5.1: 타입 정의 (완료) +- [x] `PopLayoutDataV5` 인터페이스 +- [x] `PopGridConfig`, `PopGridPosition` 타입 +- [x] `GridMode`, `GRID_BREAKPOINTS` 상수 +- [x] `createEmptyPopLayoutV5()`, `isV5Layout()`, `detectGridMode()` + +#### Phase 5.2: 그리드 렌더러 (완료) +- [x] `PopRenderer.tsx` - CSS Grid 기반 렌더링 +- [x] 격자 셀 렌더링 (CSS Grid 동일 좌표계) +- [x] 위치 변환 (12칸 -> 4/6/8칸) + +#### Phase 5.3: 디자이너 UI (완료) +- [x] `PopCanvas.tsx` - 그리드 캔버스 + 행/열 라벨 +- [x] 드래그 스냅 (칸에 맞춤) +- [x] `ComponentEditorPanel.tsx` - 위치 편집 + +#### Phase 5.4: 반응형 자동화 (완료) +- [x] 자동 변환 알고리즘 (12칸 -> 4칸) +- [x] 겹침 감지 및 재배치 +- [x] 모드별 오버라이드 저장 + +#### v5.1 추가 기능 (완료) +- [x] 자동 줄바꿈 (col > maxCol -> 맨 아래 배치) +- [x] "검토 필요" 알림 시스템 +- [x] Gap 프리셋 (좁게/보통/넓게) +- [x] 숨김 기능 (모드별) + +#### v5.2 브레이크포인트 재설계 + 세로 자동 확장 (완료) +- [x] 기기 기반 브레이크포인트 (479/767/1023px) +- [x] 세로 자동 확장 (dynamicCanvasHeight) +- [x] 뷰어 반응형 일관성 (detectGridMode 사용) +- [x] VIEWPORT_PRESETS에서 height 제거 + +### 컴포넌트 설계 (완료) + +- [x] 9개 컴포넌트 정의 (POPUPDATE_2.md v8.0) +- [x] POP 헌법 9조 작성 +- [x] 공통 인프라 설계 (DataSourceConfig, ColumnBinding, JoinConfig, useDataSource, usePopEvent, PopActionConfig) +- [x] 모달 화면 설계 방식 확정 (인라인 + 외부 참조) +- [x] 기존 시스템 호환성 검증 (DB/백엔드/프론트 변경 불필요 확인) + +--- + +## 다음 작업 + +### Phase 0: 공통 인프라 + +모든 데이터 연동 컴포넌트가 공유하는 기반 시스템: + +- [x] ColumnBinding 타입 정의 (read/write/readwrite/hidden) -- types.ts에 추가 완료 +- [x] JoinConfig 타입 정의 (테이블 조인) -- types.ts에 추가 완료 +- [x] DataSourceConfig 타입 정의 (데이터 소스 설정) -- types.ts에 추가 완료 +- [x] PopActionConfig 타입 정의 (액션 설정) -- types.ts에 추가 완료 +- [x] usePopEvent 훅 구현 (이벤트 버스, 데이터 전달, 화면 단위 격리) -- 완료 +- [x] useDataSource 훅 구현 (CRUD 포함, 기존 dataApi 활용) -- 완료 + +### Phase 1: pop-dashboard -- 완료 + +> **2026-02-10**: 17단계 코딩 + 검수 + 팔레트 등록 완료 + +- [x] PopDashboardConfig, DashboardItem 타입 정의 +- [x] 멀티 아이템 컨테이너 구현 (여러 아이템 묶음) +- [x] 4개 서브타입: kpi-card, chart, gauge, stat-card +- [x] 4개 표시 모드: arrows, auto-slide, grid, scroll +- [x] 계산식(formula) 지원: "생산량/총재고량" 같은 복합 표현 +- [x] 설정 패널: 드롭다운 기반 쉬운 집계 설정 (SQL 불필요) +- [x] PopComponentRegistry 등록 + 디자이너 팔레트 등록 +- [ ] 이벤트: filter_changed 수신, kpi_clicked 발행 -- usePopEvent 완성 후 +- [ ] 기존 `components/pop/dashboard/` 폴더 폐기 (모든 기능 대체 확인 후) + +### Phase 2: pop-button, pop-icon + +- [ ] pop-button: 저장/삭제/API 호출 액션 +- [ ] pop-icon: 화면 이동/URL/새로고침 + +### Phase 3: pop-table (테이블형 우선) + +- [ ] table-list 서브타입: 행/열 장부형 +- [ ] ColumnBinding 기반 컬럼별 read/write +- [ ] card-list 서브타입: 카드 템플릿 (후순위) + +### Phase 4: pop-search, pop-field, pop-lookup + +- [ ] pop-search: 필터 조건 입력 (text/date/select/combo) +- [ ] pop-field: 저장용 입력 (text/number/date/select/numpad) +- [ ] pop-lookup: 모달 값 선택 (인라인 + 외부 참조) + +### Phase 5: 고도화 + +- [ ] pop-table 카드 템플릿 디자이너 + +### Phase 6: pop-system + +- [ ] 프로필, 테마, 대시보드 보이기/숨기기 통합 + +### 후속 작업 + +- [ ] 워크플로우 연동 (버튼 액션, 화면 전환) +- [ ] 실기기 테스트 (아이폰 SE, iPad Mini 등) + +**참고 문서**: [POPUPDATE_2.md](../POPUPDATE_2.md) (컴포넌트 정의서 v8.0) + +--- + +## 현재 구현 계획 + +> **용도**: 이 섹션은 "지금 바로 실행할 구체적 계획"입니다. +> 새 세션에서 이 섹션만 읽으면 코딩을 시작할 수 있어야 합니다. +> 완료되면 다음 기능의 계획으로 **교체**합니다. + +### 대상: Phase 0 공통 인프라 (usePopEvent + useDataSource 훅) + +#### 배경 (2026-02-11) + +모든 데이터 연동 POP 컴포넌트(pop-button, pop-table, pop-search 등)가 공유하는 2개 핵심 훅을 구현한다. + +- **usePopEvent**: 같은 화면(screenId) 안에서 컴포넌트 간 이벤트 통신 (publish/subscribe) +- **useDataSource**: DB 테이블 CRUD 통합 (조회/생성/수정/삭제) + +**핵심 원칙**: 새로 만드는 것이 아니라, 기존 코드를 공통화하는 작업이다. + +- `useDataSource`는 대시보드의 `dataFetcher.ts` 조회 로직 + 기존 `dataApi` CRUD를 훅으로 래핑 +- `usePopEvent`는 신규 구현 (Map 기반 이벤트 버스) +- 대시보드는 **이번에 교체하지 않는다** (훅 안정화 후 별도 교체) + +#### 결정사항 (2026-02-11) + +| 항목 | 결정 | 이유 | +|------|------|------| +| usePopEvent 범위 | 같은 screenId 안에서만 통신 | 화면 간 의존성 방지 | +| usePopEvent 모달 | 같은 screenId면 모달 안 컴포넌트도 통신 가능 | 모달은 별도 화면이 아님 | +| useDataSource 조회 분기 | 집계/조인이면 SQL 빌더 + executeQuery, 단순이면 dataApi.getTableData | 대시보드 dataFetcher.ts와 동일 전략 | +| useDataSource CRUD | dataApi.createRecord/updateRecord/deleteRecord 래핑 | 백엔드 API 이미 완성됨 | +| 대시보드 교체 시점 | 이번에 하지 않음, 훅 안정화 후 별도 작업 | 안정성 우선 | +| SQL 빌더 위치 | dataFetcher.ts에서 추출하여 별도 유틸로 분리 | 훅과 대시보드 모두 사용 | +| 이벤트 버스 저장소 | 전역 Map (screenId -> EventEmitter) | React 외부에서도 접근 가능, GC 관리 용이 | + +--- + +#### 구현 순서 (의존성 기반) + +| 순서 | 파일 | 작업 | 의존성 | 상태 | +|------|------|------|--------|------| +| 1 | `hooks/pop/usePopEvent.ts` | 이벤트 버스 훅 (신규) | 없음 | **완료** | +| 2 | `hooks/pop/popSqlBuilder.ts` | SQL 빌더 유틸 분리 (dataFetcher.ts에서 추출) | 없음 | **완료** | +| 3 | `hooks/pop/useDataSource.ts` | 데이터 CRUD 훅 (신규) | 2 | **완료** | +| 4 | `hooks/pop/index.ts` | 배럴 파일 (re-export) | 1, 3 | **완료** | + +--- + +#### STEP 1: `usePopEvent.ts` (신규 생성) + +**파일**: `frontend/hooks/pop/usePopEvent.ts` + +**역할**: 같은 화면(screenId) 안에서 컴포넌트 간 이벤트 통신 + +**핵심 구조**: + +``` +전역 저장소 (React 외부) +screenBuses: Map>> + │ + └── screenId: "S001" + ├── "supplier-selected" → [콜백A, 콜백B] + ├── "data-saved" → [콜백C] + └── sharedData: Map + +sharedDataStore: Map> + │ + └── screenId: "S001" + ├── "selectedSupplier" → { id: "SUP-001", name: "삼성" } + └── "inputQuantity" → 50 +``` + +**외부 API (훅이 반환하는 것)**: + +```typescript +function usePopEvent(screenId: string) { + return { + publish, // (eventName, payload) => void + subscribe, // (eventName, callback) => unsubscribe 함수 + getSharedData, // (key) => unknown + setSharedData, // (key, value) => void + }; +} +``` + +**상세 구현 명세**: + +1. **전역 Map 저장소** (모듈 스코프, React 외부) + - `screenBuses: Map>>` - 이벤트 리스너 + - `sharedDataStore: Map>` - 공유 데이터 + +2. **`publish(eventName, payload)`** + - 해당 screenId의 eventName에 등록된 모든 콜백을 순회하며 payload 전달 + - 등록된 리스너가 없으면 아무 일도 안 함 (에러 아님) + +3. **`subscribe(eventName, callback)`** + - 해당 screenId의 eventName에 콜백 등록 + - **반환값**: unsubscribe 함수 + - **useEffect 내부에서 호출되어야 함** (cleanup으로 unsubscribe) + +4. **`getSharedData(key)`** / **`setSharedData(key, value)`** + - screenId별 격리된 key-value 저장소 + - publish/subscribe는 "이벤트"(일회성), sharedData는 "상태"(지속) + - 용도: 버튼이 저장할 때 다른 컴포넌트들의 현재 값을 수집 + +5. **`cleanupScreen(screenId)`** (내부 유틸) + - 화면 언마운트 시 해당 screenId의 모든 리스너 + sharedData 정리 + - 메모리 누수 방지 + +**사용 예시**: + +```typescript +// 거래처 선택 버튼 +const { publish, setSharedData } = usePopEvent("S001"); + +const onSelect = (supplier) => { + setSharedData("selectedSupplier", supplier); // 상태 저장 + publish("supplier-selected", { supplierId: supplier.id }); // 이벤트 발행 +}; + +// 발주 테이블 (다른 컴포넌트) +const { subscribe } = usePopEvent("S001"); + +useEffect(() => { + const unsub = subscribe("supplier-selected", (payload) => { + refetch({ filters: { supplier_id: payload.supplierId } }); + }); + return unsub; // cleanup +}, []); + +// 저장 버튼 (다른 컴포넌트) +const { getSharedData } = usePopEvent("S001"); + +const handleSave = () => { + const supplier = getSharedData("selectedSupplier"); // 다른 컴포넌트가 저장한 값 수집 + const quantity = getSharedData("inputQuantity"); + save({ supplier_id: supplier.id, quantity }); +}; +``` + +--- + +#### STEP 2: `popSqlBuilder.ts` (신규 생성 - dataFetcher.ts에서 추출) + +**파일**: `frontend/hooks/pop/popSqlBuilder.ts` + +**역할**: DataSourceConfig를 SQL 문자열로 변환하는 순수 유틸리티 + +**기존 dataFetcher.ts에서 그대로 추출할 함수 5개** (로직 변경 없음): + +| 함수 | 원본 위치 (dataFetcher.ts) | 역할 | +|------|--------------------------|------| +| `escapeSQL(value)` | 라인 41~48 | SQL 값 이스케이프 | +| `sanitizeIdentifier(name)` | 라인 124~127 | 테이블/컬럼명 위험 문자 제거 | +| `validateDataSourceConfig(config)` | 라인 59~88 | 설정 완료 여부 검증 | +| `buildWhereClause(filters)` | 라인 93~118 | 필터 -> WHERE 절 변환 | +| `buildAggregationSQL(config)` | 라인 137~215 | DataSourceConfig -> SELECT SQL 변환 | + +**export 대상**: `validateDataSourceConfig`, `buildAggregationSQL` (나머지는 내부 함수) + +**주의**: dataFetcher.ts는 **수정하지 않는다**. 대시보드가 계속 사용 중이므로, 복사만 한다. 대시보드 교체 시점에 dataFetcher.ts에서 이 파일을 import하도록 변경할 예정. + +--- + +#### STEP 3: `useDataSource.ts` (신규 생성) + +**파일**: `frontend/hooks/pop/useDataSource.ts` + +**역할**: DataSourceConfig 기반 DB 테이블 CRUD 통합 훅 + +**내부 의존성**: +- `popSqlBuilder.ts` - SQL 빌더 (STEP 2) +- `@/lib/api/data` - dataApi (기존, 조회/생성/수정/삭제) +- `@/lib/api/dashboard` - dashboardApi (기존, SQL 직접 실행) +- `@/lib/api/client` - apiClient (기존, axios 기반) + +**외부 API (훅이 반환하는 것)**: + +```typescript +function useDataSource(config: DataSourceConfig) { + return { + // 상태 + data: { rows: [], value: 0, total: 0 }, + loading: boolean, + error: string | null, + + // 조회 + refetch: (overrideFilters?) => Promise, + + // 쓰기 + save: (record) => Promise, + update: (id, record) => Promise, + remove: (id) => Promise, + }; +} +``` + +**MutationResult 타입** (신규): + +```typescript +interface MutationResult { + success: boolean; + data?: any; + error?: string; +} +``` + +**조회 분기 로직 (핵심)**: + +``` +config에 aggregation 또는 joins가 있는가? +├── YES → buildAggregationSQL(config) → apiClient.post("/dashboards/execute-query") +│ (대시보드와 동일한 경로, SQL 직접 실행) +│ 실패 시 → dashboardApi.executeQuery() 폴백 +│ +└── NO → dataApi.getTableData(tableName, { page, size, filters, sortBy, sortOrder }) + (단순 테이블 조회) +``` + +**CRUD 메서드 구현**: + +| 메서드 | 내부 호출 | 비고 | +|--------|----------|------| +| `save(record)` | `dataApi.createRecord(config.tableName, record)` | company_code 자동 추가는 백엔드가 처리 | +| `update(id, record)` | `dataApi.updateRecord(config.tableName, id, record)` | | +| `remove(id)` | `dataApi.deleteRecord(config.tableName, id)` | 복합키 객체도 지원 | + +**자동 새로고침**: + +```typescript +useEffect(() => { + if (config.tableName) refetch(); + + if (config.refreshInterval && config.refreshInterval > 0) { + const sec = Math.max(5, config.refreshInterval); // 최소 5초 + const timer = setInterval(refetch, sec * 1000); + return () => clearInterval(timer); + } +}, [config.tableName, config.refreshInterval]); +``` + +**refetch 오버라이드 필터**: + +```typescript +// 기본 조회 +refetch(); + +// 필터 추가하여 조회 (usePopEvent와 연동 시) +refetch({ filters: { supplier_id: "SUP-001" } }); +``` + +내부적으로 `overrideFilters`가 있으면 `config.filters`에 병합하여 조회한다. + +**사용 예시**: + +```typescript +// 대시보드 스타일 (집계) +const { data, loading } = useDataSource({ + tableName: "sales_order", + aggregation: { type: "sum", column: "amount", groupBy: ["category"] }, + refreshInterval: 30, +}); +// data.rows → [{ category: "A", value: 1500 }, ...] +// data.value → 첫 번째 행의 value + +// 테이블 스타일 (목록) +const { data, refetch } = useDataSource({ + tableName: "purchase_order", + sort: [{ column: "created_at", direction: "desc" }], + limit: 20, +}); +// data.rows → [{ id: 1, item_name: "볼트", ... }, ...] +// data.total → 전체 행 수 + +// 버튼 스타일 (저장만) +const { save, remove, loading } = useDataSource({ + tableName: "inbound_record", +}); +const result = await save({ supplier_id: "SUP-001", quantity: 50 }); +// result.success → true/false +``` + +--- + +#### STEP 4: `index.ts` (신규 생성 - 배럴 파일) + +**파일**: `frontend/hooks/pop/index.ts` + +```typescript +export { usePopEvent, cleanupScreen } from "./usePopEvent"; +export { useDataSource } from "./useDataSource"; +export type { MutationResult } from "./useDataSource"; +export { buildAggregationSQL, validateDataSourceConfig } from "./popSqlBuilder"; +``` + +외부에서 사용할 때: `import { usePopEvent, useDataSource } from "@/hooks/pop";` + +--- + +#### 사전 충돌 검사 결과 (2026-02-11) + +| 이름 | 유형 | 검색 범위 | 검색 결과 | 판정 | +|------|------|-----------|-----------|------| +| `usePopEvent` | 훅 이름 | frontend 전체 | **주석 2건** (PopDashboardComponent.tsx 라인 10, `@INFRA-EXTRACT` 교체 예정 주석) | **충돌 없음** (실제 코드 아님) | +| `useDataSource` | 훅 이름 | frontend 전체 | **주석 4건** (dataFetcher.ts 라인 4,227,355 + PopDashboardComponent.tsx 라인 9, 모두 `@INFRA-EXTRACT` 주석) | **충돌 없음** (실제 코드 아님) | +| `PopEventBus` | 클래스명 | frontend 전체 | **1건** (PopDashboardComponent.tsx 라인 10, `@INFRA-EXTRACT` 주석 내) | **충돌 없음** (주석) | +| `MutationResult` | 타입명 | frontend 전체 | **0건** | **충돌 없음** | +| `popSqlBuilder` | 파일명 | frontend 전체 | **0건** | **충돌 없음** | +| `cleanupScreen` | 함수명 | frontend 전체 | **0건** | **충돌 없음** | +| `screenBuses` | 변수명 | frontend 전체 | **0건** | **충돌 없음** | +| `sharedDataStore` | 변수명 | frontend 전체 | **0건** | **충돌 없음** | +| `buildAggregationSQL` | 함수명 | frontend 전체 | **2건** (dataFetcher.ts 정의 + PopDashboardComponent에서 import) | **충돌 주의**: 동일 이름을 popSqlBuilder.ts에서 재정의. 대시보드는 여전히 dataFetcher.ts 것을 사용하므로 런타임 충돌 없음. 향후 대시보드 교체 시 import 경로만 변경. | +| `validateDataSourceConfig` | 함수명 | frontend 전체 | **1건** (dataFetcher.ts 정의) | 위와 동일 | +| `escapeSQL` | 함수명 | frontend 전체 | **1건** (dataFetcher.ts 내부 함수) | **충돌 없음** (export 안 됨) | +| `sanitizeIdentifier` | 함수명 | frontend 전체 | **1건** (dataFetcher.ts 내부 함수) | **충돌 없음** (export 안 됨) | +| `AggregatedResult` | 타입명 | frontend 전체 | **1건** (dataFetcher.ts 정의) | **충돌 주의**: useDataSource 내부에서 동일 타입 사용. types.ts 정의를 공유하므로 별도 재정의 불필요. | + +#### 정의-사용 매핑 + +| 정의 | 정의 위치 | 사용 위치 | +|------|-----------|-----------| +| `usePopEvent` | `hooks/pop/usePopEvent.ts` (신규) | pop-button (Phase 2), pop-table (Phase 3), pop-search (Phase 4) 등 | +| `useDataSource` | `hooks/pop/useDataSource.ts` (신규) | pop-button (Phase 2), pop-table (Phase 3), pop-dashboard (향후 교체) 등 | +| `MutationResult` | `hooks/pop/useDataSource.ts` (신규) | useDataSource 반환 타입으로 사용 | +| `buildAggregationSQL` | `hooks/pop/popSqlBuilder.ts` (신규) | useDataSource 내부에서 호출 | +| `validateDataSourceConfig` | `hooks/pop/popSqlBuilder.ts` (신규) | useDataSource 내부에서 호출 | +| `cleanupScreen` | `hooks/pop/usePopEvent.ts` (신규) | 화면 언마운트 시 호출 (PopRenderer 또는 뷰어에서) | +| `DataSourceConfig` | `lib/registry/pop-components/types.ts` (기존) | useDataSource 파라미터 타입 | +| `DataSourceFilter` | `lib/registry/pop-components/types.ts` (기존) | popSqlBuilder 내부 | +| `dataApi` | `lib/api/data.ts` (기존) | useDataSource 내부에서 CRUD 호출 | +| `dashboardApi` | `lib/api/dashboard.ts` (기존) | useDataSource 내부에서 SQL 실행 폴백 | +| `apiClient` | `lib/api/client.ts` (기존) | useDataSource 내부에서 SQL 실행 1차 | + +**누락 검사**: 모든 신규 정의에 사용처 있음. 모든 사용처에 정의 존재. +단, `cleanupScreen`의 호출 시점은 Phase 2 이후 뷰어 통합 시 결정 (이번에는 export만 해둠). + +--- + +#### 함정 경고 + +| 번호 | 위험 | 설명 | 해결 방안 | +|------|------|------|-----------| +| W1 | **subscribe를 useEffect 밖에서 호출하면 메모리 누수** | subscribe는 콜백을 등록하므로, 컴포넌트 언마운트 시 해제해야 함 | subscribe의 반환값(unsubscribe)을 useEffect cleanup에서 호출. JSDoc에 사용 패턴 명시. | +| W2 | **DataSourceConfig의 import 경로** | `DataSourceConfig`는 `lib/registry/pop-components/types.ts`에 정의됨. hooks 디렉토리에서 import 시 경로가 김 | `@/lib/registry/pop-components/types`로 import. 별도 re-export 하지 않음 (타입 중복 방지). | +| W3 | **buildAggregationSQL 동일 이름 2곳** | dataFetcher.ts와 popSqlBuilder.ts에 같은 이름의 함수가 존재 | 의도적 복사. 대시보드는 dataFetcher.ts, 새 컴포넌트는 popSqlBuilder.ts 사용. 향후 대시보드 교체 시 dataFetcher.ts를 popSqlBuilder.ts import로 변경. | +| W4 | **apiClient.post와 dashboardApi.executeQuery 이중 경로** | 대시보드 dataFetcher.ts에서 apiClient 우선 + dashboardApi 폴백 패턴을 그대로 복사함 | 동일 패턴 유지 (안정성 검증 완료). 향후 하나로 통합 가능. | +| W5 | **refetch overrideFilters와 config.filters 병합 순서** | overrideFilters가 config.filters를 완전 대체하는지, 추가하는지 모호 | **추가(append) 방식**: config.filters + overrideFilters를 합침. overrideFilters에 같은 column이 있으면 덮어씀. 이 동작을 JSDoc에 명시. | +| W6 | **SSR 환경에서 전역 Map** | Next.js SSR에서 전역 Map이 서버/클라이언트 간 공유될 수 있음 | `typeof window !== "undefined"` 가드. 이벤트 버스는 클라이언트 전용. | +| W7 | **hooks/pop/ 디렉토리 신규** | `frontend/hooks/pop/` 디렉토리가 존재하지 않음 | STEP 1에서 파일 생성 시 디렉토리 자동 생성됨. 수동으로 mkdir 불필요. | + +--- + +#### 작업 완료 후 확인 체크리스트 + +##### 코드 레벨 + +- [ ] `usePopEvent` - publish/subscribe 기본 동작 (같은 screenId) +- [ ] `usePopEvent` - 다른 screenId 간 격리 확인 +- [ ] `usePopEvent` - subscribe cleanup (메모리 누수 없음) +- [ ] `usePopEvent` - sharedData set/get +- [ ] `useDataSource` - 단순 조회 (aggregation 없음 → dataApi.getTableData) +- [ ] `useDataSource` - 집계 조회 (aggregation 있음 → SQL 빌더 → executeQuery) +- [ ] `useDataSource` - save/update/remove +- [ ] `useDataSource` - loading/error 상태 관리 +- [ ] `useDataSource` - refreshInterval 자동 새로고침 +- [ ] `popSqlBuilder` - buildAggregationSQL이 dataFetcher.ts와 동일 결과 생성 +- [ ] TypeScript 컴파일 에러 0건 +- [ ] 린트 에러 0건 +- [ ] 기존 대시보드 동작에 영향 없음 (dataFetcher.ts 미수정) + +##### 구조 레벨 + +- [ ] `frontend/hooks/pop/` 디렉토리 생성됨 +- [ ] index.ts 배럴 파일에서 모든 public API export 됨 +- [ ] `@/hooks/pop`으로 import 가능 + +--- + +#### 이전 완료된 계획 (보관) + +**대시보드 스타일 정리 (2026-02-11, 완료)**: +글자 크기 커스텀 제거, 라벨 정렬만 유지, stale closure 수정, .next 캐시 해결. +상세: `popdocs/sessions/2026-02-11.md` + +**브라우저 확인 체크리스트 (대기)**: +- [ ] 라벨 정렬, 페이지 미리보기, 차트 디자인, 게이지/통계카드 동작 확인 + +--- + +## 브레이크포인트 (v5.2 현재) + +| 모드 | 화면 너비 | 칸 수 | 대상 기기 | +|------|----------|-------|----------| +| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S | +| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로 | +| tablet_portrait | 768~1023px | 8칸 | 8~10인치 태블릿 세로 | +| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 | + +--- + +## 관련 문서 + +| 문서 | 내용 | +|------|------| +| [STATUS.md](./STATUS.md) | 현재 진행 상태 | +| [SPEC.md](./SPEC.md) | 기술 스펙 | +| [ARCHITECTURE.md](./ARCHITECTURE.md) | 코드 구조 | +| [POPUPDATE_2.md](../POPUPDATE_2.md) | 컴포넌트 정의서 v8.0 (최신) | +| [components-spec.md](./components-spec.md) | 컴포넌트 상세 설계 (v4 기준, 갱신 필요) | +| [decisions/005](./decisions/005-breakpoint-redesign.md) | 브레이크포인트 재설계 ADR | + +--- + +*최종 업데이트: 2026-02-11 (Phase 0 공통 인프라 완료, Phase 2 pop-button 설계 시작)* diff --git a/popdocs/PROBLEMS.md b/popdocs/PROBLEMS.md new file mode 100644 index 00000000..806d2a5d --- /dev/null +++ b/popdocs/PROBLEMS.md @@ -0,0 +1,574 @@ +# 문제-해결 색인 + +> **용도**: "이전에 비슷한 문제 어떻게 해결했어?" +> **검색 팁**: Ctrl+F로 키워드 검색 (에러 메시지, 컴포넌트명 등) + +--- + +## 렌더링 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| rowSpan이 적용 안됨 | gridTemplateRows를 `1fr`로 변경 | 2026-02-02 | grid, rowSpan, CSS | +| 컴포넌트 크기 스케일 안됨 | viewportWidth 기반 scale 계산 추가 | 2026-02-04 | scale, viewport, 반응형 | +| **그리드 가이드 셀 크기 불균일** | gridAutoRows → gridTemplateRows로 행 높이 강제 고정 | 2026-02-06 | gridAutoRows, gridTemplateRows, 셀 크기, CSS Grid | +| **컴포넌트 콘텐츠가 셀 경계 벗어남** | overflow-visible → overflow-hidden 변경 | 2026-02-06 | overflow, 셀 크기, 콘텐츠 | +| **뷰어에서 플레이스홀더만 표시** | page.tsx에 레지스트리 초기화 import 추가 + renderActualComponent() 실제 컴포넌트 렌더링으로 교체 | 2026-02-09 | 뷰어, 플레이스홀더, 레지스트리, side-effect import | +| **뷰어에서 스크롤 불가 (콘텐츠 잘림)** | 최외곽 overflow-hidden 제거, overflow-auto 공통 적용, min-h-full 추가 | 2026-02-09 | 스크롤, overflow, h-screen, 뷰어 | +| **글자 크기 커스텀 vs @container 충돌** | 절대 px 글자 크기 전체 제거, @container 반응형 자동 유지 | 2026-02-11 | 글자 크기, @container, 반응형, overflow | +| **.next 빌드 캐시 꼬임 (Docker)** | `docker-compose down -v` + `--build` 재시작 | 2026-02-11 | .next, 캐시, Docker, 익명 볼륨, Turbopack | + +## 상태 관리 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **stale closure (handleUpdateComponent)** | `setLayout(prev => ...)` 함수적 업데이트, layout 의존성 제거 | 2026-02-11 | stale closure, useCallback, setState | + +## DnD (드래그앤드롭) 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| useDrag 에러 (뷰어에서) | isDesignMode 체크 후 early return | 2026-02-04 | DnD, useDrag, 뷰어 | +| DndProvider 중복 에러 | 최상위에서만 Provider 사용 | 2026-02-04 | DndProvider, react-dnd | +| **Expected drag drop context (뷰어)** | isDesignMode=false일 때 DraggableComponent 대신 일반 div 렌더링 | 2026-02-05 | DndProvider, useDrag, 뷰어, context | +| **컴포넌트 중첩(겹침)** | toast import 누락 → `sonner`에서 import | 2026-02-05 | 겹침, overlap, toast | +| **리사이즈 핸들 작동 안됨** | useDrop 2개 중복 → 단일 useDrop으로 통합 | 2026-02-05 | resize, 핸들, useDrop | +| **드래그 좌표 완전 틀림 (Row 92)** | 캔버스 scale 보정 누락 → `(offset - rect.left) / scale` | 2026-02-05 | scale, 좌표, transform | +| **DND 타입 상수 불일치** | 3개 파일에 중복 정의 → `constants/dnd.ts`로 통합 | 2026-02-05 | 상수, DND, 타입 | +| **컴포넌트 이동 안됨** | useDrop accept 타입 불일치 → 공통 상수 사용 | 2026-02-05 | 이동, useDrop, accept | + +## 타입 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| 인터페이스 이름 불일치 | V5 접미사 제거, 통일 | 2026-02-05 | 타입, interface, Props | +| v3/v4 타입 혼재 | v5 전용으로 통합, 레거시 삭제 | 2026-02-05 | 버전, 타입, 마이그레이션 | + +## 레이아웃 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **화면 밖 컴포넌트 정보 손실** | 자동 줄바꿈 로직 추가 (col > maxCol → col=1, row=맨아래+1) | 2026-02-06 | 자동배치, 줄바꿈, 정보손실 | +| Flexbox 배치 예측 불가 | CSS Grid로 전환 (v5) | 2026-02-05 | Flexbox, Grid, 반응형 | +| 4모드 각각 배치 힘듦 | 제약조건 기반 시스템 (v4) | 2026-02-03 | 모드, 반응형, 제약조건 | +| 4모드 자동 전환 안됨 | useResponsiveMode 훅 추가 | 2026-02-01 | 모드, 훅, 반응형 | + +## 브레이크포인트/반응형 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **뷰어 반응형 모드 불일치** | detectGridMode() 사용으로 일관성 확보 | 2026-02-06 | 반응형, 뷰어, 모드 | +| **768~839px 모드 불일치** | TABLET_MIN 768로 변경, 브레이크포인트 재설계 | 2026-02-06 | 브레이크포인트, 768px | +| **useResponsiveMode vs GRID_BREAKPOINTS 불일치** | 뷰어에서 detectGridMode(viewportWidth) 사용 | 2026-02-06 | 훅, 상수, 일관성 | + +## 저장/로드 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| 레이아웃 버전 충돌 | isV5Layout 타입 가드로 분기 | 2026-02-05 | 버전, 로드, 타입가드 | +| 빈 레이아웃 판별 실패 | components 존재 여부로 판별 | 2026-02-04 | 빈 레이아웃, 로드 | + +## UI/UX 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| root 레이아웃 오염 | tempLayout 도입 (임시 상태 분리) | 2026-02-04 | tempLayout, 상태, 오염 | +| 속성 패널 다른 모드 수정 | isDefaultMode 체크로 비활성화 | 2026-02-04 | 속성패널, 모드, 비활성화 | + +## 브랜치 병합 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **ksh-dashboard + ksh-v2-work 병합 시 7파일 17지점 충돌** | 의존성 순서로 해결: 타입 - 등록 - 컴포넌트 - 렌더러 - 패널. 양쪽 기능 통합(union) 전략 | 2026-02-11 | 병합, merge, 충돌, pop-icon, pop-dashboard | +| **PopComponentType 확장 충돌** | 양쪽 타입 union (`pop-icon` + `pop-dashboard`) 결합, DEFAULT_COMPONENT_GRID_SIZE에 양쪽 항목 추가 | 2026-02-11 | PopComponentType, union type, Record | +| **COMPONENT_TYPE_LABELS 불완전** | PopRenderer(`Record`)에 4개 항목 모두 추가하여 타입 안전성 확보 | 2026-02-11 | COMPONENT_TYPE_LABELS, Record, 타입 안전 | +| **isRealtime useEffect 충돌 (pop-text)** | ksh-dashboard 로직 채택 - `isRealtime` 조건부 interval로 불필요한 timer 방지 | 2026-02-11 | isRealtime, useEffect, setInterval, pop-text | +| **CSS 클래스 충돌 (ComponentEditorPanel Tabs)** | ksh-v2-work의 방어적 CSS 채택 (`overflow-hidden`, `min-h-0`, `m-0`) - 스크롤 동작 안정성 우선 | 2026-02-11 | CSS, overflow, Tabs, TabsContent, 스크롤 | +| **레지스트리 패턴에서 불필요 props 전달** | 버그 아님 - `React.ComponentType` 타입이므로 extra props 자동 무시. 레지스트리 아키텍처 의도적 패턴 | 2026-02-11 | 레지스트리, props, ComponentType, any | + +--- + +## 그리드 가이드 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| SVG 격자와 CSS Grid 좌표 불일치 | GridGuide.tsx 삭제, PopRenderer에서 CSS Grid 셀로 격자 렌더링 | 2026-02-05 | 격자, SVG, CSS Grid, 좌표 | +| 행/열 라벨 위치 오류 | PopCanvas에 absolute positioning 라벨 추가 | 2026-02-05 | 라벨, 행, 열, 정렬 | +| 격자선과 컴포넌트 불일치 | 동일한 CSS Grid 좌표계 사용 | 2026-02-05 | 통합, 정렬, 일체감 | + +--- + +## 뷰어 플레이스홀더 버그 상세 (2026-02-09) + +### 증상 +설계 화면(디자이너)에서는 이미지/텍스트/타이틀 정상 표시. +뷰어(`/pop/screens/4114`) 접속 시 "pop-text 1", "pop-text 2" 같은 라벨만 보임. + +### 원인 (2가지) + +**원인 A**: `renderActualComponent()` 함수가 하드코딩된 플레이스홀더만 반환 +```typescript +// 변경 전 (PopRenderer.tsx 555-564) +function renderActualComponent(component) { + const typeLabel = COMPONENT_TYPE_LABELS[component.type]; + return ( +
+ {component.label || typeLabel} +
+ ); +} +``` + +**원인 B**: 뷰어 `page.tsx`에서 `PopComponentRegistry` 초기화 import 누락 +- 디자이너에서는 `PopDesigner.tsx`가 컴포넌트를 import하여 레지스트리 초기화됨 +- 뷰어에서는 아무도 레지스트리를 초기화하지 않아 빈 상태 + +### 해결 +```typescript +// page.tsx - 레지스트리 초기화 (PopRenderer보다 먼저!) +import "@/lib/registry/pop-components"; +import PopRenderer from "@/components/pop/designer/renderers/PopRenderer"; + +// PopRenderer.tsx - 실제 컴포넌트 렌더링 +function renderActualComponent(component) { + const registeredComp = PopComponentRegistry.getComponent(component.type); + const ActualComp = registeredComp?.component; + if (ActualComp) { + return ; + } + // fallback: 플레이스홀더 +} +``` + +### 교훈 +> side-effect import (`import "..."`)는 해당 레지스트리를 사용하는 컴포넌트 import보다 **반드시 앞에** 위치해야 한다. +> JavaScript 모듈은 import 선언 순서대로 실행되므로, 순서가 뒤바뀌면 빈 레지스트리를 참조할 수 있다. +> 새 POP 컴포넌트를 등록할 때, 뷰어 페이지에도 초기화 import가 있는지 반드시 확인. + +--- + +## 해결 완료 (이번 세션) + +| 문제 | 상태 | 해결 방법 | +|------|------|----------| +| PopCanvas 타입 오류 | **해결** | 임시 타입 가드 추가 | +| 팔레트 UI 없음 | **해결** | ComponentPalette.tsx 신규 추가 | +| SVG 격자 좌표 불일치 | **해결** | CSS Grid 기반 통합 | +| 드래그 좌표 완전 틀림 | **해결** | scale 보정 + calcGridPosition 함수 | +| DND 타입 상수 불일치 | **해결** | constants/dnd.ts 통합 | +| 컴포넌트 이동 안됨 | **해결** | useDrop/useDrag 타입 통일 | +| 컴포넌트 중첩(겹침) | **해결** | toast import 추가 → 겹침 감지 로직 정상 작동 | +| 리사이즈 핸들 작동 안됨 | **해결** | useDrop 통합 (2개 → 1개) | +| 숨김 컴포넌트 드래그 안됨 | **해결** | handleMoveComponent에서 숨김 해제 + 위치 저장 단일 상태 업데이트 | +| 그리드 범위 초과 에러 | **해결** | adjustedCol 계산으로 드롭 위치 자동 조정 | +| Expected drag drop context (뷰어) | **해결** | isDesignMode=false일 때 일반 div 렌더링 | +| hiddenComponentIds 중복 정의 | **해결** | 중복 useMemo 제거 (라인 410-412) | +| 뷰어 반응형 모드 불일치 | **해결** | detectGridMode() 사용 | +| 그리드 가이드 셀 크기 불균일 | **해결** | gridTemplateRows로 행 높이 강제 고정 | +| Canvas vs Renderer 행 수 불일치 | **해결** | 숨김 필터 통일, 여유행 +3으로 통일 | +| 디버깅 console.log 잔존 | **해결** | reviewComponents 내 console.log 삭제 | +| 뷰어 실제 컴포넌트 렌더링 안 됨 | **해결** | 레지스트리 초기화 import + renderActualComponent 수정 | + +--- + +## 드래그 좌표 버그 상세 (2026-02-05) + +### 증상 +- 컴포넌트를 아래로 드래그 → 위로 올라감 +- Row 92 같은 비정상 좌표 +- 드래그 이동/리사이즈 전혀 작동 안됨 + +### 원인 +``` +캔버스: transform: scale(0.8) + +getBoundingClientRect() → 스케일 적용된 크기 (1024px → 819px) +getClientOffset() → 뷰포트 기준 실제 마우스 좌표 + +이 둘을 그대로 계산하면 좌표 완전 틀림 +``` + +### 해결 +```typescript +// 스케일 보정된 상대 좌표 계산 +const relX = (offset.x - canvasRect.left) / canvasScale; +const relY = (offset.y - canvasRect.top) / canvasScale; + +// 실제 캔버스 크기로 그리드 계산 +calcGridPosition(relX, relY, customWidth, ...); +``` + +### 교훈 +> CSS `transform: scale()` 적용된 요소에서 좌표 계산 시, +> `getBoundingClientRect()`는 스케일 적용된 값을 반환하지만 +> 마우스 좌표는 뷰포트 기준이므로 **반드시 스케일 보정 필요** + +--- + +## Expected drag drop context 에러 상세 (2026-02-05 심야) + +### 증상 +``` +Invariant Violation: Expected drag drop context +at useDrag (...) +at DraggableComponent (...) +``` +뷰어 페이지(`/pop/viewer/[screenId]`)에서 POP 화면 조회 시 에러 발생 + +### 원인 +``` +PopRenderer의 DraggableComponent에서 useDrag 훅을 무조건 호출 +→ 뷰어 페이지에는 DndProvider가 없음 +→ React 훅은 조건부 호출 불가 (Rules of Hooks) +→ DndProvider 없이 useDrag 호출 시 context 에러 +``` + +### 해결 +```typescript +// PopRenderer.tsx - 컴포넌트 렌더링 부분 +if (isDesignMode) { + return ( + // useDrag 사용 + ); +} + +// 뷰어 모드: 드래그 없는 일반 렌더링 +return ( +
+ +
+); +``` + +### 교훈 +> React DnD의 `useDrag`/`useDrop` 훅은 반드시 `DndProvider` 내부에서만 호출해야 함. +> 디자인 모드와 뷰어 모드를 분기할 때, 훅이 포함된 컴포넌트 자체를 조건부 렌더링해야 함. +> 훅 내부에서 `canDrag: false`로 설정해도 훅 자체는 호출되므로 context 에러 발생. + +### 관련 파일 +- `gridUtils.ts`: convertAndResolvePositions(), needsReview() +- `PopCanvas.tsx`: ReviewPanel, ReviewItem +- `PopRenderer.tsx`: 자동 배치 위치 렌더링 + +--- + +## 뷰어 반응형 모드 불일치 상세 (2026-02-06) + +### 증상 +``` +- 아이폰 SE, iPad Pro 프리셋은 정상 작동 +- 브라우저 수동 리사이즈 시 6칸 모드(mobile_landscape)가 적용 안 됨 +- 768~839px 구간에서 8칸으로 표시됨 (예상: 6칸) +``` + +### 원인 +``` +useResponsiveMode 훅: +- deviceType: width/height 비율로 "mobile"/"tablet" 판정 +- isLandscape: width > height로 판정 +- BREAKPOINTS.TABLET_MIN = 840 (당시) + +GRID_BREAKPOINTS: +- mobile_landscape: 600~839px (6칸) +- tablet_portrait: 840~1023px (8칸) + +결과: +- 768px 화면 → useResponsiveMode: "tablet" (768 < 840이지만 비율 판정) +- 768px 화면 → GRID_BREAKPOINTS: "mobile_landscape" (6칸) +- → 모드 불일치! +``` + +### 해결 + +**1단계: 브레이크포인트 재설계** +```typescript +// 기존 +mobile_landscape: { minWidth: 600, maxWidth: 839 } +tablet_portrait: { minWidth: 840, maxWidth: 1023 } + +// 변경 후 +mobile_landscape: { minWidth: 480, maxWidth: 767 } +tablet_portrait: { minWidth: 768, maxWidth: 1023 } +``` + +**2단계: 훅 연동** +```typescript +// useDeviceOrientation.ts +BREAKPOINTS.TABLET_MIN: 768 // was 840 +``` + +**3단계: 뷰어 모드 감지 방식 변경** +```typescript +// page.tsx (뷰어) +const currentModeKey = isPreviewMode + ? getModeKey(deviceType, isLandscape) // 프리뷰: 수동 선택 + : detectGridMode(viewportWidth); // 일반: 너비 기반 (일관성 확보) +``` + +### 교훈 +> 반응형 모드 판정은 **단일 소스(GRID_BREAKPOINTS)**를 기준으로 해야 함. +> 훅과 상수가 각각 다른 기준을 사용하면 구간별 불일치 발생. +> 뷰어에서는 `detectGridMode(viewportWidth)` 직접 사용으로 일관성 확보. + +--- + +## 뷰어 렌더링 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **뷰어에서 실제 컴포넌트 대신 플레이스홀더 표시** | (1) `page.tsx`에 레지스트리 초기화 import 추가 (2) `renderActualComponent()`에서 `PopComponentRegistry.getComponent()` 조회 후 실제 렌더링 | 2026-02-09 | 뷰어, 플레이스홀더, 레지스트리, import, renderActualComponent | +| **datetime 실시간 업데이트 기본값 불일치** | **미해결** - `DateTimeDisplay`의 `if (!config?.isRealtime) return`에서 undefined가 false로 평가되어 타이머 미작동. 설정 패널은 `?? true`로 기본 켜짐 표시. 권장: `const isRealtime = config?.isRealtime ?? true` | 2026-02-09 | datetime, isRealtime, undefined, 기본값, 타이머 | + +--- + +## 병합 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **ScreenDesigner.tsx 3건 충돌** (origin/main 병합) | 함수 시그니처: ksh-v2-work 유지(isPop/defaultDevicePreview), 저장 로직: 3단계 분기 유지+console.log 제거, 툴바 props: origin/main 채택 | 2026-02-09 | 병합, merge, ScreenDesigner, 충돌 | +| **usePanelState 중복 선언** (병합 시 발견) | 충돌 1 해결 과정에서 L175의 중복 usePanelState 제거, L215의 완전한 버전만 유지 | 2026-02-09 | usePanelState, 중복, 병합 | +| **툴바 JSX 들여쓰기 불일치** (병합 후 린트) | origin/main 코드가 ksh-v2-work와 2칸 들여쓰기 차이. 기능 영향 없음. 추후 포매팅 정리 권장 | 2026-02-09 | 들여쓰기, 포매팅, 린트, prettier | + +--- + +## 컴포넌트 등록 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **pop-dashboard 팔레트 미노출** | PopComponentType/PALETTE_ITEMS/DEFAULT_COMPONENT_GRID_SIZE/COMPONENT_TYPE_LABELS 4곳에 수동 등록 누락. 4곳 모두 추가 | 2026-02-10 | 팔레트, 등록, PopComponentType, 하드코딩, 레지스트리 | +| **미사용 import (AggregatedResult)** | PopDashboardComponent.tsx에서 type AggregatedResult import 제거 | 2026-02-10 | import, 미사용, 코드 품질 | + +## pop-dashboard 데이터/SQL 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **대상 컬럼 조회 안 됨** | fetchTableColumns에서 tableManagementApi(axios) 우선 사용, dashboardApi(fetch) 폴백 | 2026-02-10 | 컬럼, schema, API, 인증, fetch, axios | +| **SUM()/COUNT() 빈 괄호 SQL** | validateDataSourceConfig으로 중간 상태 검증, 미완료 시 SQL 생성 차단 | 2026-02-10 | SQL, 집계, validate, 중간상태, 백엔드 | +| **API 30초 타임아웃 (auth/me 등)** | 잘못된 SQL이 백엔드 과부하 유발 -> SQL 차단 + 브라우저 새로고침 | 2026-02-10 | timeout, auth, 과부하, 브라우저 멈춤 | +| **Docker unhealthy (curl 미설치)** | healthcheck에 curl이 없어 false 양성. 서비스 자체는 정상. 확인만 필요 | 2026-02-10 | docker, healthcheck, curl, unhealthy | + +## pop-dashboard 렌더링 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **2열 설정이 1열로 렌더링** | GridMode.tsx MIN_CELL_WIDTH 160->80. 초기 containerWidth=300에서 (300-8)/2=146 < 160이라 축소됨 | 2026-02-10 | 그리드, 열, 레이아웃, MIN_CELL_WIDTH, containerWidth | +| **라벨/단위/증감율 잘림** | 4개 아이템에서 truncate/hidden 제거, text-[10px]->text-xs, p-2->p-3 | 2026-02-10 | truncate, hidden, 라벨, 잘림, @container | +| **useEffect 불필요 데이터 재호출** | visibleItems 배열 참조 -> visibleItemIds(JSON 문자열) 의존성 안정화 | 2026-02-10 | useEffect, 의존성, 배열참조, JSON.stringify | +| **파이 차트 미표시** | (1) fetch 기반 API가 iframe에서 간헐 실패 -> apiClient(axios) 우선 (2) PostgreSQL bigint 문자열 -> Number() 변환 | 2026-02-10 | PieChart, fetch, axios, bigint, 문자열, Number | +| **파이 차트 라벨/레전드 없음** | ChartItem.tsx에 Legend 컴포넌트 + custom label 포맷(`name value (percent%)`) 추가 | 2026-02-10 | PieChart, Legend, label, Recharts | +| **게이지 가로 레이아웃 비율 깨짐** | max-w-[200px] 고정 -> h-full w-auto max-w-full 높이 기반 스케일링. SVG 래퍼를 flex-1 min-h-0으로 변경 | 2026-02-10 | 게이지, SVG, 비율, max-width, 가로 레이아웃 | +| **KPI/통계 카드 왼쪽 정렬** | KpiCard에 items-center, StatCard에 items-center justify-center 추가. 4개 아이템 정렬 통일 | 2026-02-10 | 정렬, items-center, KPI, StatCard | + +## pop-dashboard 설정 패널 관련 + +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| **설정 패널 표시 모드/아이템 탭 클릭 시 에러** | ConfigPanelProps에서 `onChange`를 `onUpdate`로 변경. ComponentEditorPanel이 `onUpdate`로 전달하는데 대시보드만 `onChange`로 수신하여 TypeError 발생. `onUpdate: onChange` 구조분해로 해결 | 2026-02-10 | onChange, onUpdate, props, TypeError, 설정패널, 컨벤션 | +| **gaugeConfig min/max/target 변경 안 됨** | 스프레드 연산자 순서 버그. `{ max: newValue, ...item.gaugeConfig }` -> `{ ...item.gaugeConfig, max: newValue }`. 나중에 오는 속성이 우선 | 2026-02-10 | 스프레드, 순서, gaugeConfig, 덮어쓰기, onChange | +| **차트 X축/Y축 입력 혼동** | "X축 컬럼"과 "그룹핑(X축)" 중복으로 사용자 혼동. 수동 입력 제거, 자동 설정 안내 텍스트로 교체 | 2026-02-10 | xAxisColumn, groupBy, 중복, UX, 설정패널 | +| **React hooks 규칙 위반 (early return)** | `PopDashboardComponent.tsx`에서 `if (!config)` early return이 hooks보다 앞에 위치. 모든 hooks 선언 이후로 early return 이동, `visibleItems`에 `?? []` 안전 처리 추가 | 2026-02-10 | hooks, early return, Rules of Hooks, useState, useEffect | +| **config가 빈 객체 `{}`로 전달되어 items undefined** | `ComponentEditorPanel`이 `component.config \|\| {}`로 빈 객체 전달 -> `??`가 빈 객체를 통과 -> `cfg.items` undefined -> `.map()`, `.length`, `.filter()` TypeError. ConfigPanel: `{...DEFAULT_CONFIG, ...config}` spread 병합, Preview: `Array.isArray()` 가드 추가, Component: `Array.isArray()` 가드 추가 | 2026-02-10 | config, 빈 객체, nullish coalescing, undefined, items, TypeError | + +|| **설정 탭 세로 스크롤 불가 (페이지 추가 시 콘텐츠 잘림)** | `ComponentEditorPanel.tsx`의 `Tabs`와 모든 `TabsContent`에 `min-h-0` 누락. Flexbox에서 flex 자식의 기본 `min-height: auto`가 콘텐츠 크기 이하로 축소를 막아 `overflow-auto`가 작동하지 않음. `Tabs`에 `min-h-0`, `TabsList`에 `shrink-0`, 4개 `TabsContent`에 `min-h-0` 추가로 해결 | 2026-02-10 | 스크롤, overflow, min-h-0, flex, TabsContent, 설정패널, 페이지 탭 | + +--- + +## 설정 탭 스크롤 버그 상세 (2026-02-10) + +### 증상 +POP 디자이너에서 대시보드 컴포넌트 선택 -> 설정 탭 -> 페이지 탭에서 페이지를 3개 이상 추가하면 아래쪽 페이지가 잘려서 보이지 않음. 세로 스크롤 불가. + +### 잘못된 접근 (실패 기록) +처음에 `PopDashboardConfigPanel`(자식)에서 문제를 해결하려고 시도: +1. 외곽 `div`를 `flex h-full flex-col`로 변경 +2. 탭 콘텐츠에 `overflow-y-auto` 래퍼 추가 + +**실패 이유**: `PopDashboardConfigPanel`은 부모(`ComponentSettingsForm`)의 `overflow-auto`가 담당하는 스크롤 영역 안에 있으므로, 자식이 `h-full`로 높이를 채우면 부모의 스크롤 영역이 깨짐. 문제는 자식이 아니라 **부모의 높이 제약이 제대로 전파되지 않는 것**. + +### 근본 원인 +``` +ResizablePanel (높이 확정) + └ ComponentEditorPanel: div.flex.h-full.flex-col + ├ 헤더 (고정) + └ Tabs: flex.flex-1.flex-col ← min-h-0 누락! + ├ TabsList (고정) + └ TabsContent: flex-1.overflow-auto ← min-h-0 누락! + └ ComponentSettingsForm + └ PopDashboardConfigPanel ← 콘텐츠가 길어짐 +``` + +Flexbox의 `min-height: auto` 기본값 때문에: +- `Tabs`(flex-1)가 콘텐츠에 의해 무한 확장 +- `TabsContent`(overflow-auto)도 콘텐츠에 의해 확장 -> 스크롤 발생 안 함 +- 결과: `overflow-auto`가 있지만 높이 제약이 없어 스크롤바 미생성 + +### 해결 +```typescript +// ComponentEditorPanel.tsx +// 변경 전 + + + + +// 변경 후 + + + +``` + +### 교훈 +> Flexbox에서 `overflow-auto`가 작동하려면, 해당 요소와 **모든 flex 조상에 `min-h-0`(또는 `min-w-0`)이 필요**하다. +> `flex-1`만으로는 높이가 제한되지 않는다. flex 자식의 기본 `min-height: auto`가 콘텐츠 크기 이하로 축소를 막기 때문이다. +> 스크롤 문제가 발생하면 자식 컴포넌트가 아니라 **부모 flex 체인부터 위로 추적**해야 한다. + +--- + +## 폼 중간 상태 -> 잘못된 SQL -> 백엔드 장애 상세 (2026-02-10) + +### 증상 +브라우저 콘솔에 `timeout of 30000ms exceeded` 에러 반복. `/auth/me`, `/auth/status`, `/admin/menus` 등 핵심 API가 모두 30초 타임아웃. 브라우저가 멈추거나 자동 종료. + +### 원인 체인 +``` +1. 대시보드 설정에서 집계 유형(SUM/AVG)만 선택, 대상 컬럼 미선택 + ↓ +2. dataFetcher.ts의 buildAggregationSQL이 "SUM()" 같은 잘못된 SQL 생성 + ↓ +3. 잘못된 SQL이 백엔드로 전송, PostgreSQL "function sum() does not exist" 에러 + ↓ +4. refreshInterval(기본값)로 이 요청이 반복 전송 + ↓ +5. 백엔드 과부하 -> Docker 컨테이너 unhealthy + ↓ +6. 인증/메뉴 등 다른 API도 30초 타임아웃 + ↓ +7. 여러 API가 병렬로 30초씩 대기 -> 브라우저 멈춤 +``` + +### 해결 +```typescript +// dataFetcher.ts - SQL 생성 전 필수값 검증 +function validateDataSourceConfig(ds: DataSourceConfig): string | null { + if (!ds.tableName) return "테이블이 선택되지 않았습니다"; + if (ds.aggregation?.type && ds.aggregation.type !== "COUNT" && !ds.aggregation.column) { + return "집계 대상 컬럼이 선택되지 않았습니다"; + } + // ... 조인 검증 등 + return null; // 유효 +} + +// fetchAggregatedData에서 호출 +const validationError = validateDataSourceConfig(dataSource); +if (validationError) { + return { value: 0, error: validationError }; +} +``` + +### 교훈 +> 프론트엔드 설정 폼의 "중간 상태"(일부만 입력)가 백엔드에 전달되면 연쇄 장애로 이어질 수 있다. +> SQL/API 호출 전에 반드시 `validate` 함수를 배치하고, 미완료 상태에서는 요청 자체를 차단해야 한다. +> 특히 `refreshInterval`이 있는 자동 갱신 기능은 잘못된 요청을 반복 전송할 수 있어 더 위험하다. + +--- + +## 그리드 2열 -> 1열 축소 상세 (2026-02-10) + +### 증상 +디자이너에서 페이지를 2열로 설정했는데, 실제 화면에서는 아이템이 세로로 쌓여 1열로 표시됨. + +### 원인 +``` +GridModeComponent에서 반응형 열 축소 로직: + MIN_CELL_WIDTH = 160px + containerWidth = 300px (초기값, ResizeObserver 발동 전) + gap = 8px + + cellWidth = (300 - 8) / 2 = 146px + 146 < 160 → actualColumns = 1 (축소!) + + ResizeObserver가 실제 너비를 반영해도, + 초기 렌더링에서 이미 1열로 결정됨 +``` + +### 해결 +```typescript +// GridMode.tsx +const MIN_CELL_WIDTH = 80; // was 160 +// 80px이면 146 >= 80 → 2열 유지 +``` + +### 교훈 +> 반응형 열 축소 로직의 MIN_CELL_WIDTH는 실제 사용 시나리오를 고려해야 한다. +> ResizeObserver가 발동하기 전 초기 containerWidth 기본값이 작을 수 있으므로, +> MIN_CELL_WIDTH를 너무 크게 설정하면 의도치 않게 열이 축소된다. + +--- + +## stale closure (handleUpdateComponent) + +| 항목 | 내용 | +|------|------| +| **문제** | `PopDesigner.tsx`의 `handleUpdateComponent`에서 `layout` state를 직접 참조하여, 빠른 연속 설정 변경 시 이전 state가 캡처되어 변경값 유실 | +| **증상** | 정렬 버튼을 빠르게 클릭하면 이전 클릭의 변경이 덮어씌워짐 | +| **원인** | `useCallback`의 의존성 배열에 `layout`이 있지만, `layout` 갱신 전에 다음 호출이 발생하면 이전 클로저가 사용됨 | +| **해결** | `setLayout(prev => ...)` 함수적 업데이트로 변경, 의존성에서 `layout` 제거 → `[saveToHistory]`만 유지 | +| **날짜** | 2026-02-11 | +| **키워드** | stale closure, useCallback, setState, 함수적 업데이트, 빠른 연속 변경 | + +### 교훈 +> `useCallback` 안에서 state를 직접 참조하면 빠른 연속 호출 시 이전 값이 캡처된다. +> 항상 `setState(prev => ...)` 패턴으로 최신 state에 접근해야 한다. +> 특히 사용자 인터랙션(버튼 클릭)이 빠르게 반복될 수 있는 곳에서 주의. + +--- + +## .next 빌드 캐시 꼬임 (Docker 익명 볼륨) + +| 항목 | 내용 | +|------|------| +| **문제** | `ChartItem.tsx`를 수정했으나 브라우저에 반영되지 않음. 디버그 console.log도 콘솔에 나타나지 않음 | +| **증상** | 다른 컴포넌트(StatCard, Gauge)는 최신 코드 실행, 차트만 이전 코드 실행 | +| **원인** | Docker compose에서 `/app/.next`가 익명 볼륨으로 분리되어 호스트의 `.next` 삭제와 독립적. Turbopack 모듈 캐시가 특정 파일 변경을 인식하지 못함 | +| **해결** | `docker-compose -f ... down -v` (볼륨 포함 삭제) + `docker-compose -f ... up -d --build` (재빌드) | +| **날짜** | 2026-02-11 | +| **키워드** | .next, 캐시, Docker, 익명 볼륨, Turbopack, HMR, 빌드 | + +### 교훈 +> Docker compose에서 `/app/.next`가 익명 볼륨인 경우, 호스트에서 `rm -rf .next`를 해도 컨테이너 캐시에 영향 없음. +> `docker-compose down -v`로 볼륨까지 제거해야 캐시가 초기화됨. +> 파일 수정이 브라우저에 반영 안 되면: (1) 디버그 로그 추가 (2) 로그가 안 나오면 캐시 문제 의심 (3) 볼륨 포함 재빌드. + +--- + +## 글자 크기 커스텀 vs @container 반응형 충돌 + +| 항목 | 내용 | +|------|------| +| **문제** | 글자 크기 3그룹(라벨/메인값/보조)을 절대 px(12~64px)로 설정하면, 유동적인 그리드 셀 크기와 충돌하여 텍스트가 잘리거나 넘침 | +| **증상** | "매우 크게(64px)" 설정 시 값이 셀 경계를 벗어남. 작은 셀에 큰 글자가 들어가면서 정보 가독성 저하 | +| **원인** | `@container` 반응형(`text-xs @[200px]:text-3xl`)은 셀 크기에 따라 자동 조절되는데, 절대 px가 이를 덮어씀 | +| **해결** | 글자 크기 커스텀 기능 전체 제거. `@container` 반응형 자동 크기만 유지. 정렬은 라벨만 좌/중/우 선택 | +| **날짜** | 2026-02-11 | +| **키워드** | 글자 크기, @container, 반응형, overflow, 대시보드 | + +### 교훈 +> 대시보드처럼 고밀도 정보를 표시하는 컴포넌트에서는 절대 크기 지정을 피하고, 컨테이너 크기에 따른 자동 반응형이 더 안정적이다. +> "정보를 한눈에 파악"이 목적인 컴포넌트에서는 정보가 잘리게 만드는 기능 자체가 목적에 반한다. + +--- + +## 미사용 import (PopComponentType) + +| 항목 | 내용 | +|------|------| +| **문제** | `ComponentEditorPanel.tsx`에서 `COMPONENT_TYPE_LABELS` 타입을 `Record` -> `Record`으로 변경한 후, `PopComponentType`의 유일한 사용처가 사라졌으나 import가 남아있었음 | +| **해결** | import에서 `PopComponentType` 제거 | +| **날짜** | 2026-02-10 | +| **키워드** | 미사용 import, 타입 변경, PopComponentType | + +### 교훈 +> 타입을 변경하거나 제거할 때, 해당 타입이 import된 것이라면 변경 후 파일 내 다른 참조가 남아있는지 Grep으로 확인해야 한다. + +--- + +*새 문제-해결 추가 시 해당 카테고리 테이블에 행 추가* diff --git a/popdocs/README.md b/popdocs/README.md new file mode 100644 index 00000000..3d565247 --- /dev/null +++ b/popdocs/README.md @@ -0,0 +1,124 @@ +# POP 화면 시스템 + +> **AI 에이전트 시작점**: 이 파일 → STATUS.md 순서로 읽으세요. +> 저장 요청 시: [SAVE_RULES.md](./SAVE_RULES.md) 참조 + +--- + +## 현재 상태 + +| 항목 | 값 | +|------|-----| +| 버전 | **v5.2** (그리드 시스템) + **컴포넌트 정의서 v8.0** + **Phase 0 공통 인프라 완료** | +| 상태 | **Phase 0 완료 (usePopEvent + useDataSource), Phase 2 pop-button 설계 진행 중** | +| 다음 | pop-button 설계/구현 -> pop-icon 검토 -> Phase 3 (pop-table) | + +**마지막 업데이트**: 2026-02-11 + +--- + +## 마지막 대화 요약 + +> **Phase 0 공통 인프라 구현 완료 (2026-02-11)**: +> +> 1. **usePopEvent 훅** (`hooks/pop/usePopEvent.ts`) +> - screenId 기반 이벤트 버스 (publish/subscribe/sharedData) +> - 전역 Map 2개 (screenBuses, sharedDataStore), SSR 가드 +> - cleanupScreen 유틸 (화면 언마운트 시 메모리 정리) +> +> 2. **popSqlBuilder 유틸** (`hooks/pop/popSqlBuilder.ts`) +> - dataFetcher.ts에서 SQL 빌더 로직 5개 함수 추출 (로직 변경 없이 복사) +> +> 3. **useDataSource 훅** (`hooks/pop/useDataSource.ts`) +> - DataSourceConfig 기반 CRUD 통합 (조회 분기 + 자동 새로고침) +> - 집계/조인 -> SQL 빌더 + executeQuery, 단순 -> dataApi.getTableData +> - save/update/remove CRUD 래핑 +> +> 4. **검수**: 린트 0, 중복 0, 시뮬레이션 8시나리오 정상 +> 5. **Git**: ksh-button 커밋 -> ksh-v2-work merge -> origin push +> +> 다음: pop-button 설계/구현 -> pop-icon 검토 -> Phase 3 + +--- + +## 빠른 경로 + +| 알고 싶은 것 | 문서 | +|--------------|------| +| 지금 뭐 해야 해? | [STATUS.md](./STATUS.md) | +| 저장/조회 규칙 | [SAVE_RULES.md](./SAVE_RULES.md) | +| 컴포넌트 정의서 (최신) | [POPUPDATE_2.md](../POPUPDATE_2.md) | +| 왜 v5로 바꿨어? | [decisions/003-v5-grid-system.md](./decisions/003-v5-grid-system.md) | +| 그리드 가이드 설계 | [decisions/004-grid-guide-integration.md](./decisions/004-grid-guide-integration.md) | +| 브레이크포인트 재설계 | [decisions/005-breakpoint-redesign.md](./decisions/005-breakpoint-redesign.md) | +| 자동 줄바꿈 시스템 | [decisions/006-auto-wrap-review-system.md](./decisions/006-auto-wrap-review-system.md) | +| 개발 계획/로드맵 | [PLAN.md](./PLAN.md) | +| 지금 바로 코딩할 계획 | [PLAN.md "현재 구현 계획"](./PLAN.md#현재-구현-계획) | +| 작업 프롬프트 | [WORKFLOW_PROMPTS.md](./WORKFLOW_PROMPTS.md) | +| 컴포넌트 설계 (v4, 갱신 필요) | [components-spec.md](./components-spec.md) | +| 이전 문제 해결 | [PROBLEMS.md](./PROBLEMS.md) | +| 코드 어디 있어? | [FILES.md](./FILES.md) | +| 기능별 색인 | [INDEX.md](./INDEX.md) | +| 변경 히스토리 | [CHANGELOG.md](./CHANGELOG.md) | + +--- + +## 핵심 파일 + +| 파일 | 역할 | 경로 | +|------|------|------| +| 타입 정의 | v5 레이아웃 타입 | `frontend/components/pop/designer/types/pop-layout.ts` | +| 캔버스 | 그리드 캔버스 + DnD + 라벨 | `frontend/components/pop/designer/PopCanvas.tsx` | +| 렌더러 | CSS Grid 렌더링 + 격자 셀 | `frontend/components/pop/designer/renderers/PopRenderer.tsx` | +| 디자이너 | 메인 컴포넌트 | `frontend/components/pop/designer/PopDesigner.tsx` | +| 팔레트 | 컴포넌트 목록 | `frontend/components/pop/designer/panels/ComponentPalette.tsx` | + +--- + +## 문서 구조 + +``` +[Layer 1: 먼저 읽기] +README.md (지금 여기) → STATUS.md + +[Layer 2: 필요시 읽기] +CHANGELOG, PROBLEMS, INDEX, FILES, ARCHITECTURE, SPEC, PLAN + +[Layer 3: 심화] +decisions/, sessions/, archive/ +``` + +**컨텍스트 효율화**: 모든 문서를 읽지 마세요. 필요한 것만 단계적으로. + +--- + +## POP이란? + +**Point of Production** - 현장 작업자용 모바일/태블릿 화면 시스템 + +| 용도 | 경로 | +|------|------| +| 뷰어 | `/pop/screens/{screenId}` | +| 관리 | `/admin/screenMng/popScreenMngList` | +| API | `/api/screen-management/layout-pop/:screenId` | + +--- + +## v5 그리드 시스템 (현재) + +| 모드 | 화면 너비 | 칸 수 | 대상 기기 | +|------|----------|-------|----------| +| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S | +| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로 | +| tablet_portrait | 768~1023px | 8칸 | 8~10인치 태블릿 세로 | +| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 | + +**핵심**: 컴포넌트를 칸 단위로 배치 (col, row, colSpan, rowSpan) + +**세로 무한 스크롤**: 캔버스 높이 자동 확장 (컴포넌트 배치에 따라) + +**그리드 가이드**: CSS Grid 기반 격자 셀 + 행/열 라벨 (ON/OFF 토글) + +--- + +*상세: [SPEC.md](./SPEC.md) | 히스토리: [CHANGELOG.md](./CHANGELOG.md)* diff --git a/popdocs/SAVE_RULES.md b/popdocs/SAVE_RULES.md new file mode 100644 index 00000000..fc445170 --- /dev/null +++ b/popdocs/SAVE_RULES.md @@ -0,0 +1,574 @@ +# popdocs 사용 규칙 + +> **AI 에이전트 필독**: 이 문서는 popdocs 폴더 사용법입니다. +> 사용자가 "@popdocs"와 함께 요청하면 이 규칙을 참조하세요. + +--- + +## 요청 유형 인식 + +### 키워드로 요청 유형 판별 + +| 유형 | 키워드 예시 | 행동 | +|------|------------|------| +| **저장** | 저장해줘, 기록해줘, 정리해줘, 추가해줘 | → 저장 규칙 따르기 | +| **조회** | 찾아줘, 검색해줘, 뭐 있어?, 어디있어? | → 조회 규칙 따르기 | +| **분석** | 분석해줘, 비교해줘, 어떻게 달라? | → 분석 규칙 따르기 | +| **수정** | 수정해줘, 업데이트해줘, 고쳐줘 | → 수정 규칙 따르기 | +| **요약** | 요약해줘, 정리해서 보여줘, 보고서 | → 요약 규칙 따르기 | +| **작업시작** | 시작하자, 이어서 하자, 뭐 해야 해? | → 작업 시작 규칙 | + +### 요청 유형별 행동 + +``` +[저장 요청] +"@popdocs 오늘 작업 저장해줘" +→ SAVE_RULES.md 저장 섹션 → 적절한 파일에 저장 → 동기화 + +[조회 요청] +"@popdocs 이전에 DnD 문제 어떻게 해결했어?" +→ PROBLEMS.md 검색 → 관련 내용만 반환 + +[분석 요청] +"@popdocs v4랑 v5 뭐가 달라?" +→ decisions/ 또는 CHANGELOG 검색 → 비교표 생성 + +[수정 요청] +"@popdocs STATUS 업데이트해줘" +→ STATUS.md 수정 → README.md 동기화 + +[요약 요청] +"@popdocs 이번 주 작업 요약해줘" +→ sessions/ 해당 기간 검색 → 요약 생성 + +[계획 저장] +"@popdocs 구현 계획 저장해줘" +→ PLAN.md "현재 구현 계획" 섹션 교체 → STATUS.md 동기화 + +[작업 시작] +"@popdocs 오늘 작업 시작하자" +→ README → STATUS → PLAN.md "현재 구현 계획" → 중단점 확인 → 작업 시작 +``` + +--- + +## 컨텍스트 효율화 원칙 + +### Progressive Disclosure (점진적 공개) + +**핵심**: 모든 문서를 한 번에 읽지 마세요. 필요한 것만 단계적으로. + +``` +Layer 1 (진입점) → README.md, STATUS.md (먼저 읽기, ~100줄) +Layer 2 (상세) → 필요한 문서만 선택적으로 +Layer 3 (심화) → 코드 파일, archive/ (필요시만) +``` + +### Token as Currency (토큰은 자원) + +| 원칙 | 설명 | +|------|------| +| **관련성 > 최신성** | 모든 히스토리 대신 관련 있는 것만 | +| **요약 > 전문** | 긴 내용 대신 요약 먼저 확인 | +| **링크 > 복사** | 내용 복사 대신 파일 경로 참조 | +| **테이블 > 산문** | 긴 설명 대신 표로 압축 | +| **검색 > 전체읽기** | Ctrl+F 키워드 검색 활용 | + +### Context Bloat 방지 + +``` +❌ 잘못된 방법: +"모든 문서를 읽고 파악한 후 작업하겠습니다" +→ 1,300줄 이상 낭비 + +✅ 올바른 방법: +"README → STATUS → 필요한 섹션만" +→ 평균 50~100줄로 작업 가능 +``` + +--- + +## 문서 구조 (3계층) + +``` +popdocs/ +│ +├── [Layer 1: 진입점] ───────────────────────── +│ ├── README.md ← 시작점 (현재 상태 요약) +│ ├── STATUS.md ← 진행 상태, 다음 작업 +│ └── SAVE_RULES.md ← 사용 규칙 (지금 읽는 문서) +│ +├── [Layer 2: 상세 문서] ───────────────────────── +│ ├── CHANGELOG.md ← 변경 이력 (날짜별) +│ ├── PROBLEMS.md ← 문제-해결 색인 +│ ├── INDEX.md ← 기능별 색인 +│ ├── ARCHITECTURE.md ← 코드 구조 +│ ├── FILES.md ← 파일 목록 +│ ├── SPEC.md ← 기술 스펙 +│ └── PLAN.md ← 계획 +│ +├── [Layer 3: 심화/기록] ───────────────────────── +│ ├── decisions/ ← ADR (결정 기록) +│ ├── sessions/ ← 날짜별 작업 기록 +│ └── archive/ ← 보관 (레거시) +│ +└── [외부 참조] ───────────────────────── + └── 실제 코드 → frontend/components/pop/designer/ +``` + +--- + +## 조회 규칙 (읽기) + +### 작업 시작 시 + +``` +1. README.md 읽기 (~60줄) + └→ 현재 상태, 다음 작업 확인 + +2. STATUS.md 읽기 (~40줄) + └→ 상세 진행 상황, 중단점 확인 + +3. 필요한 문서만 선택적으로 +``` + +### 요청별 조회 경로 + +| 사용자 요청 | 조회 경로 | +|-------------|----------| +| "지금 뭐 해야 해?" | README → STATUS | +| "구현 계획 보여줘" | PLAN.md "현재 구현 계획" 섹션 | +| "어제 뭐 했어?" | sessions/어제날짜.md | +| "이전에 비슷한 문제?" | PROBLEMS.md (키워드 검색) | +| "이 기능 어디있어?" | INDEX.md 또는 FILES.md | +| "왜 이렇게 결정했어?" | decisions/ | +| "전체 히스토리" | CHANGELOG.md (기간 한정) | +| "코드 구조 알려줘" | ARCHITECTURE.md | +| "v4랑 v5 뭐가 달라?" | decisions/003 또는 CHANGELOG | + +### 효율적 검색 + +``` +# 전체 파일 읽지 말고 키워드 검색 +PROBLEMS.md에서 "DnD" 검색 → 관련 행만 +CHANGELOG.md에서 "2026-02-05" 검색 → 해당 날짜만 +FILES.md에서 "렌더러" 검색 → 관련 파일만 +``` + +--- + +## 저장 규칙 (쓰기) + +### 저장 유형별 위치 + +| 요청 패턴 | 저장 위치 | 형식 | +|----------|----------|------| +| "오늘 작업 저장/정리해줘" | sessions/YYYY-MM-DD.md | 세션 템플릿 | +| "이 결정 기록해줘" | decisions/NNN-제목.md | ADR 템플릿 | +| "이 문제 해결 기록해줘" | PROBLEMS.md | 행 추가 | +| "작업 내용 추가해줘" | CHANGELOG.md | 섹션 추가 | +| "현재 상태 업데이트" | STATUS.md | 상태 수정 | +| "기능 색인 추가해줘" | INDEX.md | 행 추가 | +| "구현 계획 저장해줘" | PLAN.md "현재 구현 계획" | 섹션 교체 | + +### 저장 후 필수 동기화 + +``` +저장 완료 후 항상: +1. STATUS.md 업데이트 (진행 상태, 다음 작업) +2. README.md "마지막 대화 요약" 업데이트 (1-2줄) +``` + +--- + +## 분석/비교 규칙 + +### 비교 요청 시 + +``` +사용자: "@popdocs v4랑 v5 뭐가 달라?" + +AI 행동: +1. decisions/003-v5-grid-system.md 확인 (있으면) +2. 없으면 CHANGELOG에서 관련 날짜 검색 +3. 비교표 형식으로 응답 + +응답 형식: +| 항목 | v4 | v5 | +|------|----|----| +| 배치 | Flexbox | CSS Grid | +| ... | ... | ... | +``` + +### 분석 요청 시 + +``` +사용자: "@popdocs 이번 달 작업 분석해줘" + +AI 행동: +1. sessions/ 폴더에서 해당 기간 파일 목록 +2. 각 파일의 "요약" 섹션만 추출 +3. 종합 분석 제공 +``` + +--- + +## 수정 규칙 + +### 문서 수정 요청 시 + +``` +사용자: "@popdocs STATUS 업데이트해줘" + +AI 행동: +1. STATUS.md 읽기 +2. 변경 내용 적용 +3. README.md 동기화 (마지막 대화 요약) +4. 변경 내용 사용자에게 확인 +``` + +### 여러 문서 수정 시 + +``` +수정 순서: +상세 문서 먼저 → STATUS.md → README.md +(역방향: 진입점이 항상 최신 상태 유지) +``` + +--- + +## 요약/보고서 규칙 + +### 요약 요청 시 + +``` +사용자: "@popdocs 이번 주 요약해줘" + +AI 행동: +1. sessions/ 해당 기간 파일 확인 +2. 각 파일의 "요약" + "완료" 섹션 추출 +3. 압축된 형식으로 응답 + +응답 형식: +## 이번 주 요약 (02-01 ~ 02-05) +| 날짜 | 주요 작업 | +|------|----------| +| 02-05 | v5 통합 완료 | +| 02-04 | ... | +``` + +### 보고서 생성 요청 시 + +``` +사용자: "@popdocs 진행 보고서 만들어줘" + +AI 행동: +1. STATUS.md (현재 상태) +2. CHANGELOG.md (최근 작업) +3. sessions/ (상세 기록) +4. 보고서 형식으로 조합 + +→ 새 파일 생성하지 말고 응답으로 제공 +→ 필요시 사용자가 저장 요청 +``` + +--- + +## 예외 처리 + +### 분류 불가 시 + +``` +사용자: "@popdocs 이거 저장해줘" (유형 불명확) + +AI 행동: +→ 사용자에게 질문: "어떤 유형으로 저장할까요?" + - 오늘 작업 기록 (sessions/) + - 문제-해결 기록 (PROBLEMS.md) + - 결정 사항 (decisions/) + - 변경 이력 (CHANGELOG.md) +``` + +### 복합 요청 시 + +``` +사용자: "@popdocs 문제 해결했고 결정도 내렸어. 저장해줘" + +AI 행동 (순서): +1. PROBLEMS.md에 문제-해결 추가 +2. decisions/에 ADR 생성 (필요시) +3. CHANGELOG.md에 섹션 추가 +4. STATUS.md 업데이트 +5. README.md 동기화 +``` + +### 전체 조회 요청 시 + +``` +사용자: "@popdocs 전체 히스토리 보여줘" + +AI 행동: +→ "기간을 지정해주시면 더 정확히 찾아드릴게요." + - 예: "최근 1주일", "2월 작업", "v5 관련" + +→ 기간 없이 강행 시: CHANGELOG.md 최근 5개 항목만 +``` + +### 파일 없음 시 + +``` +사용자: "@popdocs 어제 작업 보여줘" (sessions/어제.md 없음) + +AI 행동: +→ "어제 작업 기록이 없습니다. CHANGELOG.md에서 찾아볼까요?" +``` + +### 키워드 검색 실패 시 + +``` +사용자: "@popdocs DnD 문제 찾아줘" (PROBLEMS.md에 없음) + +AI 행동: +→ "PROBLEMS.md에서 못 찾았습니다. 다른 곳도 검색할까요?" + - CHANGELOG.md + - INDEX.md + - sessions/ +``` + +--- + +## 동기화 규칙 + +### 항상 동기화해야 하는 쌍 + +| 변경 문서 | 동기화 대상 | +|----------|-----------| +| sessions/ 생성 | STATUS.md (최근 세션) | +| PROBLEMS.md 추가 | - | +| decisions/ 생성 | STATUS.md (관련 결정), CHANGELOG.md | +| CHANGELOG.md 추가 | STATUS.md (진행 상태) | +| STATUS.md 수정 | README.md (마지막 요약) | +| PLAN.md 구현 계획 수정 | STATUS.md (다음 작업) | + +### 불일치 발견 시 + +``` +README.md와 STATUS.md 내용이 다르면: +→ STATUS.md를 정본(正本)으로 +→ README.md를 STATUS.md 기준으로 업데이트 +``` + +--- + +## 정리 규칙 + +### 주기적 정리 (수동 요청 시) + +| 대상 | 조건 | 조치 | +|------|------|------| +| sessions/ | 30일 이상 | archive/sessions/로 이동 | +| PROBLEMS.md | 100행 초과 | 카테고리별 분리 검토 | +| CHANGELOG.md | 연도 변경 | 이전 연도 archive/로 | + +### 정리 요청 패턴 + +``` +사용자: "@popdocs 오래된 파일 정리해줘" + +AI 행동: +1. sessions/ 30일 이상 파일 목록 제시 +2. 사용자 확인 후 archive/로 이동 +3. 강제 삭제하지 않음 +``` + +--- + +## 템플릿 + +### 세션 기록 (sessions/YYYY-MM-DD.md) + +```markdown +# YYYY-MM-DD 작업 기록 + +## 요약 +(한 줄 요약 - 50자 이내) + +## 완료 +- [x] 작업1 +- [x] 작업2 + +## 미완료 +- [ ] 작업3 (이유: ...) + +## 중단점 +> (내일 이어서 할 때 바로 시작할 수 있는 정보) + +## 대화 핵심 +- 키워드1: 설명 +- 키워드2: 설명 + +## 관련 링크 +- CHANGELOG: #YYYY-MM-DD +- ADR: decisions/NNN (있으면) +``` + +### 문제-해결 (PROBLEMS.md 행 추가) + +```markdown +| 문제 | 해결 | 날짜 | 키워드 | +|------|------|------|--------| +| (에러/문제 설명) | (해결 방법) | YYYY-MM-DD | 검색용 | +``` + +### ADR (decisions/NNN-제목.md) + +```markdown +# ADR-NNN: 제목 + +**날짜**: YYYY-MM-DD +**상태**: 채택됨 + +## 배경 (왜) +(2-3문장) + +## 결정 (무엇) +(핵심 결정 사항) + +## 대안 +| 옵션 | 장점 | 단점 | 결과 | +|------|------|------|------| + +## 교훈 +- (배운 점) +``` + +### 구현 계획 (PLAN.md "현재 구현 계획" 교체) + +```markdown +### 대상: [기능명] + +#### 구현 순서 (의존성 기반) +1. [ ] 파일명 - 변경 내용 요약 +2. [ ] 파일명 - 변경 내용 요약 + +#### 파일별 변경 사항 +| # | 파일 (경로) | 작업 | 핵심 변경 | 주의사항 | +|---|------------|------|----------|---------| +| 1 | path/file.tsx (신규) | 생성 | 설명 | 주의 | +| 2 | path/file.tsx (수정) | 수정 | 설명 | 주의 | + +#### 함정 경고 +- (빠뜨리면 에러나는 것들) + +#### 참조 +- 관련 문서/파일 경로 +``` + +**라이프사이클**: +- 계획 수립 시: "현재 구현 계획" 섹션을 새 계획으로 **교체** +- 코딩 중: 완료 항목 `[ ]` → `[x]` +- 기능 완료 시: 다음 기능 계획으로 **교체** (이전 계획은 CHANGELOG에 기록됨) +- 항상 1개만 존재 + +### CHANGELOG 섹션 + +```markdown +## [YYYY-MM-DD] 제목 + +### 배경 +(왜 - 2-3문장) + +### 작업 +- [x] 완료1 +- [ ] 미완료 → STATUS.md로 + +### 다음 +→ STATUS.md 참조 +``` + +--- + +## 토큰 비용 가이드 + +| 문서 | 평균 줄 수 | 언제 읽나 | +|------|-----------|----------| +| README.md | ~60 | 항상 (진입점) | +| STATUS.md | ~40 | 항상 (진입점) | +| SAVE_RULES.md | ~350 | 저장/조회 요청 시 | +| sessions/날짜.md | ~30 | 해당 날짜만 | +| PROBLEMS.md | ~100 | 키워드 검색 | +| INDEX.md | ~80 | 키워드 검색 | +| CHANGELOG.md | ~600 | 날짜/키워드 검색 | +| ARCHITECTURE.md | ~300 | 구조 파악 시 | +| FILES.md | ~200 | 파일 찾을 때 | + +**권장**: README + STATUS = ~100줄로 작업 시작 + +--- + +## 예시 워크플로우 + +### 1. 작업 시작 + +``` +사용자: "@popdocs 오늘 작업 시작하자" + +AI: +1. README.md 읽기 (60줄) +2. STATUS.md 읽기 (40줄) +3. 중단점 확인 +4. "중단점: PopCanvas.tsx line 76 타입 오류. 여기서 시작할까요?" +``` + +### 2. 자료 검색 + +``` +사용자: "@popdocs DnD 관련 자료 찾아줘" + +AI: +1. PROBLEMS.md에서 "DnD" 검색 +2. INDEX.md에서 "DnD" 검색 +3. 관련 내용 종합해서 응답 +4. 전체 파일 읽지 않음 +``` + +### 3. 비교 요청 + +``` +사용자: "@popdocs v4랑 v5 비교해줘" + +AI: +1. decisions/003-v5-grid-system.md 확인 +2. "대안 검토" 섹션에서 비교표 추출 +3. 필요시 CHANGELOG 보충 +``` + +### 4. 작업 저장 + +``` +사용자: "@popdocs 오늘 작업 저장해줘" + +AI: +1. sessions/2026-02-05.md 생성 +2. STATUS.md 업데이트 +3. README.md "마지막 대화 요약" 업데이트 +4. "저장 완료. 내일 이어서 할 중단점도 기록했습니다." +``` + +### 5. 불명확한 요청 + +``` +사용자: "@popdocs 이거 저장해줘" + +AI: +"어떤 유형으로 저장할까요? +1. 오늘 작업 기록 (sessions/) +2. 문제-해결 (PROBLEMS.md) +3. 결정 사항 (decisions/) +4. 변경 이력 (CHANGELOG.md)" +``` + +--- + +*2025-2026 AI 컨텍스트 엔지니어링 기반* +*Progressive Disclosure, Agentic Context Engineering, Token as Currency* diff --git a/popdocs/SPEC.md b/popdocs/SPEC.md new file mode 100644 index 00000000..dd391729 --- /dev/null +++ b/popdocs/SPEC.md @@ -0,0 +1,236 @@ +# POP 기술 스펙 + +**버전: v5 (CSS Grid 기반)** + +--- + +## v5 핵심 규칙 + +### 1. 그리드 시스템 + +| 모드 | 화면 너비 | 칸 수 | 대상 기기 | +|------|----------|-------|----------| +| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S (세로) | +| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로, 작은 태블릿 | +| tablet_portrait | 768~1023px | 8칸 | iPad Mini ~ iPad Pro (세로) | +| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 (기본) | + +> **브레이크포인트 기준**: 실제 기기 CSS 뷰포트 너비 기반 (2026-02-06 재설계) + +### 2. 위치 지정 + +```typescript +interface PopGridPosition { + col: number; // 시작 열 (1부터) + row: number; // 시작 행 (1부터) + colSpan: number; // 열 크기 (1~12) + rowSpan: number; // 행 크기 (1~) +} +``` + +### 3. 브레이크포인트 설정 + +```typescript +const GRID_BREAKPOINTS = { + mobile_portrait: { + columns: 4, + rowHeight: 48, + gap: 8, + padding: 12, + maxWidth: 479, // 아이폰 SE (375px) ~ 갤럭시 S (360px) + }, + mobile_landscape: { + columns: 6, + rowHeight: 44, + gap: 8, + padding: 16, + minWidth: 480, + maxWidth: 767, // 스마트폰 가로 + }, + tablet_portrait: { + columns: 8, + rowHeight: 52, + gap: 12, + padding: 20, + minWidth: 768, // iPad Mini 세로 (768px) + maxWidth: 1023, + }, + tablet_landscape: { + columns: 12, + rowHeight: 56, + gap: 12, + padding: 24, + minWidth: 1024, // iPad Pro 11 가로 (1194px), 12.9 가로 (1366px) + }, +}; +``` + +### 4. 세로 자동 확장 + +```typescript +// 캔버스 높이 동적 계산 +const MIN_CANVAS_HEIGHT = 600; // 최소 높이 (px) +const CANVAS_EXTRA_ROWS = 3; // 항상 유지되는 여유 행 수 + +const dynamicCanvasHeight = useMemo(() => { + // 가장 아래 컴포넌트 위치 계산 + const maxRowEnd = visibleComps.reduce((max, comp) => { + const rowEnd = pos.row + pos.rowSpan; + return Math.max(max, rowEnd); + }, 1); + + // 여유 행 추가하여 높이 계산 + const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS; + return Math.max(MIN_CANVAS_HEIGHT, totalRows * rowHeight + padding); +}, [layout.components, ...]); +``` + +**특징**: +- 디자이너: 세로 무한 확장 (컴포넌트 추가에 제한 없음) +- 뷰어: 터치 스크롤로 아래 컴포넌트 접근 가능 + +--- + +## 데이터 구조 + +### v5 레이아웃 + +```typescript +interface PopLayoutDataV5 { + version: "pop-5.0"; + metadata: { + screenId: number; + createdAt: string; + updatedAt: string; + }; + gridConfig: { + defaultMode: GridMode; + maxRows: number; + }; + components: PopComponentDefinitionV5[]; + globalSettings: { + backgroundColor: string; + padding: number; + }; +} +``` + +### v5 컴포넌트 + +```typescript +interface PopComponentDefinitionV5 { + id: string; + type: PopComponentType; // "pop-label" | "pop-button" | ... + label: string; + gridPosition: PopGridPosition; + config: PopComponentConfig; + visibility: Record; // 모드별 표시/숨김 + modeOverrides?: Record; // 모드별 오버라이드 +} +``` + +### 컴포넌트 타입 + +```typescript +type PopComponentType = + | "pop-label" // 텍스트 라벨 + | "pop-button" // 버튼 + | "pop-input" // 입력 필드 + | "pop-select" // 선택 박스 + | "pop-grid" // 데이터 그리드 + | "pop-container"; // 컨테이너 +``` + +--- + +## 크기 프리셋 + +### 터치 요소 + +| 요소 | 일반 | 산업용 | +|------|-----|-------| +| 버튼 높이 | 48px | 60px | +| 입력창 높이 | 48px | 56px | +| 터치 영역 | 48px | 60px | + +### 폰트 (clamp) + +| 용도 | 범위 | CSS | +|------|-----|-----| +| 본문 | 14-18px | `clamp(14px, 1.5vw, 18px)` | +| 제목 | 18-28px | `clamp(18px, 2.5vw, 28px)` | + +### 간격 + +| 이름 | 값 | 용도 | +|------|---|-----| +| sm | 8px | 요소 내부 | +| md | 16px | 컴포넌트 간 | +| lg | 24px | 섹션 간 | + +--- + +## 반응형 원칙 + +``` +누르는 것 → 고정 (48px) - 버튼, 터치 영역 +읽는 것 → 범위 (clamp) - 텍스트 +담는 것 → 칸 (colSpan) - 컨테이너 +``` + +--- + +## 위치 변환 + +12칸 기준으로 설계 → 다른 모드에서 자동 변환 + +```typescript +// 12칸 → 4칸 변환 예시 +const ratio = 4 / 12; // = 0.333 + +original: { col: 1, colSpan: 6 } // 12칸에서 절반 +converted: { col: 1, colSpan: 2 } // 4칸에서 절반 +``` + +--- + +## Troubleshooting + +### 컴포넌트가 얇게 보임 + +- **증상**: rowSpan이 적용 안됨 +- **원인**: gridTemplateRows 고정 px +- **해결**: `1fr` 사용 + +### 모드 전환 안 됨 + +- **증상**: 화면 크기 변경해도 레이아웃 유지 +- **해결**: `detectGridMode()` 사용 + +### 겹침 발생 + +- **증상**: 컴포넌트끼리 겹침 +- **해결**: `resolveOverlaps()` 호출 + +--- + +## 타입 가드 + +```typescript +// v5 레이아웃 판별 +function isV5Layout(data: any): data is PopLayoutDataV5 { + return data?.version === "pop-5.0"; +} + +// 사용 예시 +if (isV5Layout(savedData)) { + setLayout(savedData); +} else { + setLayout(createEmptyPopLayoutV5()); +} +``` + +--- + +*상세 아키텍처: [ARCHITECTURE.md](./ARCHITECTURE.md)* +*파일 목록: [FILES.md](./FILES.md)* diff --git a/popdocs/STATUS.md b/popdocs/STATUS.md new file mode 100644 index 00000000..2dfeffe9 --- /dev/null +++ b/popdocs/STATUS.md @@ -0,0 +1,259 @@ +# 현재 상태 + +> **마지막 업데이트**: 2026-02-11 +> **담당**: POP 화면 디자이너 + +--- + +## 진행 상태 + +| 단계 | 상태 | 설명 | +|------|------|------| +| v5 타입 정의 | 완료 | `pop-layout.ts` | +| v5 렌더러 | 완료 | `PopRenderer.tsx` | +| v5 캔버스 | 완료 | `PopCanvas.tsx` | +| v5 편집 패널 | 완료 | `ComponentEditorPanel.tsx` | +| v5 유틸리티 | 완료 | `gridUtils.ts` | +| 레거시 삭제 | 완료 | v1~v4 코드, 데이터 | +| 문서 정리 | 완료 | popdocs v5 기준 재정비 | +| 컴포넌트 팔레트 | 완료 | `ComponentPalette.tsx` | +| 드래그앤드롭 | 완료 | 스케일 보정, DND 상수 통합 | +| 그리드 가이드 재설계 | 완료 | CSS Grid 기반 통합 | +| 모드별 오버라이드 | 완료 | 위치/크기 모드별 저장 | +| 화면 밖 컴포넌트 | 완료 | 오른쪽 패널 배치, 드래그로 복원 | +| 숨김 기능 | 완료 | 모드별 숨김/숨김해제 | +| 리사이즈 겹침 검사 | 완료 | 실시간 겹침 방지 | +| Gap 프리셋 | 완료 | 좁게/보통/넓게 간격 조정 | +| 자동 줄바꿈 | 완료 | col > maxCol -> 맨 아래 배치 | +| 검토 필요 시스템 | 완료 | 오버라이드 없으면 검토 알림 | +| 브레이크포인트 재설계 | 완료 | 기기 기반 (479/767/1023px) | +| 세로 자동 확장 | 완료 | 캔버스 높이 동적 계산 | +| 그리드 셀 크기 강제 고정 | 완료 | gridTemplateRows로 행 높이 고정, overflow-hidden | +| origin/main 병합 | 완료 | ksh-v2-work-merge-test에서 충돌 3건 해결 | +| **컴포넌트 정의서 v8.0** | **완료** | 9개 컴포넌트 + 헌법 9조 + 모달 설계 + 호환성 검증 | +| **pop-dashboard 상세 설계** | **완료** | 멀티 아이템 + 4 서브타입 + 4 표시 모드 + 계산식 + 백엔드 호환 | +| **pop-dashboard 구현 계획서** | **완료** | 17단계 구현 순서 + 충돌 검사 + 정의-사용 매핑 + 함정 경고 | +| **pop-dashboard 코딩** | **완료** | 15개 신규 파일 + 3개 수정, 17단계 전체 구현 | +| **pop-dashboard 검수** | **완료** | 린트 0, 중복 0, 미사용 import 1건 수정 | +| **pop-dashboard 팔레트 등록** | **완료** | PopComponentType + PALETTE_ITEMS + DEFAULT_SIZE + LABELS 4곳 수정 | +| **POP 뷰어 컴포넌트 렌더링** | **완료** | 레지스트리 초기화 import + renderActualComponent 수정 | +| **POP 뷰어 스크롤 수정** | **완료** | overflow-hidden 제거, overflow-auto 공통 적용, min-h-full 추가 | +| **pop-dashboard 페이지 구조 재설계** | **완료** | DashboardPage 도입, migrateConfig, "페이지" 탭, PageEditor | +| **아이템 라벨 인라인 편집** | **완료** | ItemEditor 헤더에서 직접 라벨 편집 가능 | +| **설정 탭 세로 스크롤 수정** | **완료** | ComponentEditorPanel Tabs/TabsContent에 min-h-0 추가 | +| **대시보드 아이템 모드 설정 UI 보강** | **완료** | groupBy Combobox, 차트 축 입력, 통계 카테고리 편집기 | +| **SQL 빌더 방어 로직** | **완료** | validateDataSourceConfig, 빈 컬럼 방어, COUNT(*) 처리 | +| **대상 컬럼 조회 안 됨 수정** | **완료** | fetchTableColumns API 우선순위 변경 (tableManagementApi 우선) | +| **그리드 2열이 1열로 렌더링** | **완료** | MIN_CELL_WIDTH 160 -> 80 | +| **라벨/단위 잘림 수정** | **완료** | 4개 아이템 컴포넌트에서 truncate/hidden 제거, 폰트/패딩 상향 | +| **useEffect 데이터 리페칭 무한 루프** | **완료** | visibleItems -> visibleItemIds(JSON 문자열)로 의존성 안정화 | +| **파이 차트 미표시 (fetch 실패 + bigint 문자열)** | **완료** | apiClient(axios) 우선 사용 + Number() 변환 | +| **파이 차트 라벨/레전드 없음** | **완료** | Legend 컴포넌트 + custom label 포맷 추가 | +| **gaugeConfig 값 변경 안 됨** | **완료** | 스프레드 연산자 순서 수정 (...기존값 먼저, 새값 나중) | +| **게이지 가로 레이아웃 비율 깨짐** | **완료** | max-w-[200px] 고정 -> h-full w-auto 높이 기반 스케일링 | +| **좌우 버튼/인디케이터 공간 낭비** | **완료** | absolute 오버레이로 변경, 상하 마진 불균형 해소 | +| **KPI/통계 카드 왼쪽 정렬 불일치** | **완료** | items-center (justify-center) 추가로 4개 모드 정렬 통일 | +| **차트 X축/Y축 입력 혼동** | **완료** | 수동 입력 제거, 자동 설정 안내 텍스트 교체 | +| **디자이너 캔버스 헤더 제거 + 실제 데이터 렌더링** | **완료** | PreviewComponent -> ActualComp + pointer-events-none | +| **컴포넌트 목록 UI (위치 탭)** | **완료** | ComponentEditorPanel에 배치된 컴포넌트 리스트 + 선택 연동 | +| **미사용 import (PopComponentType)** | **완료** | COMPONENT_TYPE_LABELS 타입 변경 후 import 정리 누락 수정 | +| **라벨 정렬 + 페이지 미리보기 + 차트 디자인 개선** | **완료** | 라벨 정렬(좌/중/우), Eye 미리보기, CartesianGrid, abbreviateNumber | +| **글자 크기 커스텀 제거 + 반응형 복원** | **완료** | 절대 px 글자 크기 제거, `@container` 반응형 자동 유지 | +| **stale closure (handleUpdateComponent)** | **완료** | `setLayout(prev => ...)` 함수적 업데이트로 수정 | +| **setRenderTick 불필요 이중 렌더링** | **완료** | state + useEffect 삭제 | +| **.next 빌드 캐시 꼬임 (Docker 익명 볼륨)** | **완료** | `docker-compose down -v`로 볼륨 포함 삭제 후 재빌드 | +| **디버그 console.log 잔존 (대시보드)** | **완료** | 8개 전량 제거 | +| **Phase 0: usePopEvent 훅** | **완료** | screenId 기반 이벤트 버스 (publish/subscribe/sharedData) | +| **Phase 0: popSqlBuilder 유틸** | **완료** | dataFetcher.ts에서 SQL 빌더 5개 함수 추출 | +| **Phase 0: useDataSource 훅** | **완료** | DataSourceConfig 기반 CRUD 통합 (조회 분기 + 자동 새로고침) | +| **Phase 0: hooks/pop 배럴 파일** | **완료** | 공통 훅 re-export | + +--- + +## 다음 작업 (우선순위) + +### 현재: Phase 2 pop-button 컴포넌트 구현 + +> Phase 0 공통 인프라 (usePopEvent + useDataSource) 완료 +> pop-button 컴포넌트 설계 및 구현 진행 중 + +### 완료된 Phase 0 (2026-02-11) + +| 순서 | 파일 | 작업 | 상태 | +|------|------|------|------| +| 1 | `hooks/pop/usePopEvent.ts` | 이벤트 버스 훅 | **완료** | +| 2 | `hooks/pop/popSqlBuilder.ts` | SQL 빌더 유틸 | **완료** | +| 3 | `hooks/pop/useDataSource.ts` | 데이터 CRUD 훅 | **완료** | +| 4 | `hooks/pop/index.ts` | 배럴 파일 | **완료** | + +### 대기 + +- 브라우저 확인: 라벨 정렬, 페이지 미리보기, 차트 디자인, 게이지/통계카드 +- Phase 2: pop-icon 검토/개선 + +### 후속 + +- Phase 3: pop-table (table-list 서브타입 우선) +- Phase 4: pop-search, pop-field, pop-lookup +- Phase 6: pop-system +- 대시보드 교체: dataFetcher.ts를 useDataSource로 교체 (훅 안정화 후) +- 레거시 삭제: `frontend/components/pop/dashboard/` +- 디자이너-레지스트리 자동 연동 리팩토링 (하드코딩 제거) + +--- + +## 최근 주요 변경 (2026-02-11) + +### Phase 0 공통 인프라 구현 (완료) + +| 항목 | 내용 | +|------|------| +| usePopEvent | screenId 기반 이벤트 버스 (publish/subscribe/sharedData/cleanupScreen) | +| popSqlBuilder | dataFetcher.ts에서 SQL 빌더 로직 추출 (순수 유틸, 5개 함수) | +| useDataSource | DataSourceConfig 기반 CRUD 통합 (조회 분기 + 자동 새로고침 + 필터 병합) | +| 배럴 파일 | hooks/pop/index.ts - public API re-export | +| 검수 | 린트 0, 중복 0, 시뮬레이션 8시나리오 정상 | +| Git | ksh-button -> ksh-v2-work merge -> origin push | + +### 대시보드 스타일 정리 + 페이지 미리보기 + 차트 디자인 개선 (완료) + +| 항목 | 내용 | +|------|------| +| 글자 크기 제거 | 절대 px 커스텀 삭제, `@container` 반응형 자동 복원 | +| 라벨 정렬 | 4개 아이템에 좌/중/우 정렬 기능 (값은 항상 가운데) | +| 페이지 미리보기 | Eye 버튼으로 디자이너 캔버스에 특정 페이지 렌더링 | +| 차트 디자인 | CartesianGrid, abbreviateNumber(K/M), 대각선 X축 라벨 | +| stale closure | handleUpdateComponent 함수적 setState로 수정 | +| .next 캐시 | Docker 익명 볼륨 문제 → `down -v` + `--build` 재시작 | + +### pop-dashboard 차트/게이지/UI 디자인 개선 (2026-02-10, 완료) + +| 항목 | 내용 | +|------|------| +| 차트 데이터 | apiClient(axios) 우선 사용, bigint 문자열 -> 숫자 변환 | +| 파이 차트 UX | Legend + custom label(`name value (percent%)`) 추가 | +| 게이지 설정 | 스프레드 연산자 순서 버그 수정 (min/max/target 변경 가능) | +| 게이지 비율 | 가로 레이아웃에서 높이 기반 SVG 스케일링 | +| 네비게이션 | 좌우 버튼 + 인디케이터 오버레이 디자인 (공간 절약) | +| 카드 정렬 | KPI/통계 카드 가운데 정렬 통일 | +| 차트 설정 | X/Y축 수동 입력 제거, 자동 설정 안내 텍스트 | + +### pop-dashboard 아이템 모드 완성 + SQL 방어 + 레이아웃/라벨 수정 (완료) + +| 항목 | 내용 | +|------|------| +| 설정 UI 보강 | groupBy(X축) Combobox, 차트 축 입력, 통계 카테고리 편집기 | +| SQL 방어 로직 | validateDataSourceConfig으로 중간 상태 SQL 차단 | +| 그리드 레이아웃 | MIN_CELL_WIDTH 160->80으로 2열 유지 보장 | +| 라벨/단위 잘림 | 4개 아이템에서 truncate/hidden 제거, 폰트/패딩 상향 | +| API 연동 | fetchTableColumns에서 tableManagementApi(axios) 우선 사용 | +| 데이터 리페칭 | useEffect 의존성 visibleItemIds로 안정화 | + +### pop-dashboard 페이지(슬라이드) 구조 재설계 (완료) + +| 항목 | 내용 | +|------|------| +| 변경 구조 | 평면 아이템 리스트 -> 페이지 단위 독립 그리드 레이아웃 | +| 핵심 타입 | `DashboardPage` (id, label, gridColumns, gridRows, gridCells) | +| 마이그레이션 | `migrateConfig()` - 레거시 config 런타임 변환 (저장 데이터 미변경) | +| UI 변경 | "레이아웃" 탭 -> "페이지" 탭, PageEditor, 인라인 라벨 편집 | +| 버그 수정 | ComponentEditorPanel 스크롤 문제 (Flexbox min-h-0) | + +### pop-dashboard 전체 구현 (완료) + +| 항목 | 내용 | +|------|------| +| 신규 파일 | 15개 (컴포넌트 4 + 모드 4 + 유틸 2 + 메인 3 + 등록 2) | +| 수정 파일 | 6개 (types.ts, index.ts, pop-text.tsx, pop-layout.ts, ComponentPalette.tsx, PopRenderer.tsx) | +| 서브타입 | kpi-card, chart, gauge, stat-card | +| 표시 모드 | arrows, auto-slide, scroll (grid 독립 모드 폐기 -> 페이지 내부 그리드로 전환) | +| 계산식 | 재귀 하강 파서 (eval 미사용) | +| 데이터 | @INFRA-EXTRACT 직접 API 호출 (훅 대체 예정) | + +### POP 컴포넌트 정의서 v8.0 확정 + +| 항목 | 내용 | +|------|------| +| 컴포넌트 | 9개 확정 (pop-text~pop-system) | +| 헌법 | 9조 (제9조 모달 화면 설계 추가) | +| 공통 인프라 | DataSourceConfig, ColumnBinding, JoinConfig, useDataSource, usePopEvent, PopActionConfig | +| 모달 설계 | 인라인 + 외부 참조 이중 방식 | +| 호환성 검증 | DB/백엔드/프론트 모두 기존 시스템으로 가능 (마이그레이션 불필요) | + +### 9개 컴포넌트 요약 + +| 컴포넌트 | 카테고리 | 한 줄 정의 | +|----------|---------|-----------| +| pop-text | display | 보여주기만 함 (완성) | +| pop-dashboard | display | 여러 집계 아이템을 묶어서 다양한 방식으로 보여줌 | +| pop-table | display | 데이터 목록을 보여주고 편집함 | +| pop-button | action | 누르면 액션 실행 | +| pop-icon | action | 누르면 어딘가로 이동 | +| pop-search | input | 조건을 입력해서 조회/필터링 | +| pop-field | input | 저장할 값을 입력 | +| pop-lookup | input | 모달에서 값을 골라서 반환 | +| pop-system | system | 시스템 설정 통합 (프로필, 테마, 보이기/숨기기) | + +--- + +## 알려진 문제 + +| 문제 | 상태 | 비고 | +|------|------|------| +| 뷰어에서 실제 컴포넌트 렌더링 안 됨 | **해결됨** | 레지스트리 초기화 import + `renderActualComponent()` 실제 컴포넌트 조회/렌더링으로 교체 | +| 뷰어에서 스크롤 안 됨 (하단 컴포넌트 잘림) | **해결됨** | `overflow-hidden` 제거, `overflow-auto` 공통 적용 | +| datetime 실시간 업데이트 기본값 불일치 | **해결됨** | `isRealtime ?? true`로 기본값 통일 | +| pop-dashboard 팔레트 미노출 | **해결됨** | PopComponentType, PALETTE_ITEMS, DEFAULT_SIZE, LABELS 4곳 수동 등록 | +| 타입 이름 불일치 | 해결됨 | V5 접미사 제거 | +| SVG 격자 좌표 불일치 | 해결됨 | GridGuide 삭제, CSS Grid 통합 | +| 드래그 좌표 계산 오류 | 해결됨 | 스케일 보정 적용 | +| DND 타입 상수 불일치 | 해결됨 | constants/dnd.ts로 통합 | +| 숨김 컴포넌트 드래그 안됨 | 해결됨 | 상태 업데이트 순서 수정 | +| 그리드 범위 초과 에러 | 해결됨 | 드롭 위치 자동 조정 | +| Expected drag drop context | 해결됨 | 뷰어 모드에서 일반 div 렌더링 | +| Gap 프리셋 UI 안 보임 | 해결됨 | 그리드 라벨에 adjustedGap 적용 | +| 화면 밖 컴포넌트 정보 손실 | 해결됨 | 자동 줄바꿈으로 항상 그리드 안에 배치 | +| 뷰어 반응형 모드 불일치 | 해결됨 | detectGridMode() 사용으로 일관성 확보 | +| hiddenComponentIds 중복 정의 | 해결됨 | 중복 useMemo 제거 | +| 그리드 가이드 셀 크기 불균일 | 해결됨 | gridTemplateRows로 행 높이 강제 고정 | +| Canvas/Renderer 행 수 불일치 | 해결됨 | 숨김 필터 통일, 여유행 +3 | +| 디버깅 console.log 잔존 | 해결됨 | reviewComponents 내 삭제 | +| 설정 탭 세로 스크롤 불가 | **해결됨** | ComponentEditorPanel Tabs/TabsContent에 min-h-0 추가 | +| 아이템 라벨 편집 불가 (접힌 상태) | **해결됨** | ItemEditor 헤더 span -> Input 교체 | +| 대상 컬럼 조회 안 됨 | **해결됨** | fetchTableColumns API 우선순위 변경 (tableManagementApi 우선) | +| 잘못된 SQL로 백엔드 장애 | **해결됨** | validateDataSourceConfig으로 중간 상태 SQL 차단 | +| API 30초 타임아웃 (auth/me 등) | **해결됨** | 잘못된 SQL 차단 + 브라우저 새로고침 | +| 2열 설정이 1열로 렌더링 | **해결됨** | MIN_CELL_WIDTH 160 -> 80 | +| 라벨/단위/증감율 잘림 | **해결됨** | truncate/hidden 제거, 폰트/패딩 상향 | +| useEffect 데이터 불필요 재호출 | **해결됨** | visibleItemIds 의존성 안정화 | + +--- + +## 최근 세션 + +| 날짜 | 요약 | 상세 | +|------|------|------| +| 2026-02-11 | 스타일 정리 + 라벨 정렬 + Phase 0 공통 훅 구현 (usePopEvent/useDataSource) | [sessions/2026-02-11.md](./sessions/2026-02-11.md) | +| 2026-02-10 | pop-dashboard 코딩 + 페이지 구조 재설계 + 설정 탭 스크롤 수정 | [sessions/2026-02-10.md](./sessions/2026-02-10.md) | +| 2026-02-09 | 컴포넌트 정의서 v8.0, 뷰어 렌더링 버그 수정, datetime 이슈 분석 | [sessions/2026-02-09.md](./sessions/2026-02-09.md) | +| 2026-02-06 | 브레이크포인트 재설계, 세로 자동 확장, v5.1 자동 줄바꿈 | [sessions/2026-02-06.md](./sessions/2026-02-06.md) | +| 2026-02-05 심야 | 반응형 레이아웃, 숨김 기능, 겹침 검사 | [sessions/2026-02-05.md](./sessions/2026-02-05.md) | +| 2026-02-05 저녁 | v5 통합, 그리드 가이드 재설계 | [sessions/2026-02-05.md](./sessions/2026-02-05.md) | + +--- + +## 관련 결정 + +| ADR | 제목 | 날짜 | +|-----|------|------| +| - | pop-dashboard 상세 설계 토의 (POPUPDATE_2.md + plan 파일) | 2026-02-09 | +| - | POP 컴포넌트 정의서 v8.0 (POPUPDATE_2.md) | 2026-02-09 | +| 006 | v5.1 자동 줄바꿈 + 검토 필요 시스템 | 2026-02-06 | +| 005 | 브레이크포인트 재설계 (기기 기반) + 세로 자동 확장 | 2026-02-06 | +| 004 | 그리드 가이드 CSS Grid 통합 | 2026-02-05 | +| 003 | v5 CSS Grid 채택 | 2026-02-05 | +| 001 | v4 제약조건 기반 | 2026-02-03 | + +--- + +*전체 히스토리: [CHANGELOG.md](./CHANGELOG.md)* diff --git a/popdocs/WORKFLOW_PROMPTS.md b/popdocs/WORKFLOW_PROMPTS.md new file mode 100644 index 00000000..29502eaf --- /dev/null +++ b/popdocs/WORKFLOW_PROMPTS.md @@ -0,0 +1,438 @@ +# 워크플로우 프롬프트 + +> 각 작업 단계에서 AI에게 내리는 표준 프롬프트입니다. +> 상황에 맞는 프롬프트를 복사해서 사용하세요. +> `[괄호]` 안은 상황에 맞게 수정하세요. + +--- + +## 한 번에 복사용 + +``` +===== 토의 중 개념 학습 ===== +지금 설명한 [개념명]을 우리 프로젝트 코드에서 실제 사용되는 예시로 보여줘. +해당 코드가 없으면 어떤 문제가 생기는지 한 문장으로. + +===== 토의 시작 ===== +[주제/아이디어]에 대해 토의하자. + +사전 준비 (토의 시작 전에 반드시): +1. @popdocs/ 의 README → STATUS → PLAN.md 순서로 현재 상태 파악 +2. 관련 기존 문서 확인 (POPUPDATE_2.md, ARCHITECTURE.md 등 해당되는 것만) +3. 현재 계획과 충돌하거나 영향받는 부분이 있으면 먼저 알려줘 + +===== 토의 마무리 ===== +토의 내용을 정리하고 popdocs에 반영해줘. + +정리할 것: +1. 이번 토의에서 결정된 사항 전체를 테이블로 요약 +2. 보류/미결 사항이 있으면 별도로 정리 +3. PLAN.md 반영 방식 제안: "새 계획 생성" / "기존 계획 수정" / "계획 변경 불필요" +4. 설계 문서(POPUPDATE_2.md 등) 업데이트 필요 여부 제안 + +내 확인 후 문서 반영: +- PLAN.md: 새 계획이면 "현재 구현 계획" 교체, 수정이면 해당 부분 갱신 +- STATUS.md: 진행상태/다음 작업 업데이트 +- README.md: "마지막 대화 요약" 동기화 +- sessions/오늘날짜.md: 생성 (결정 사항, 보류/미결, 다음 토의 주제 중심) +- CHANGELOG.md: 설계 결정 섹션 추가 +- decisions/: 중요한 아키텍처 결정이 있으면 ADR 생성 (없으면 생략) + +제외 대상 (토의 세션에서는 불필요): +- PROBLEMS.md (코드 에러 없음) +- INDEX.md (새 함수 없음) + +===== 아이디어/설계 토의 (이어서) ===== +@popdocs/ 의 README → STATUS 읽고, 이전 토의 이어서 하자. + +확인할 것: +1. sessions/최근날짜.md에서 "보류/미결" 항목 확인 +2. 지난번 결정 사항 요약해서 보여줘 +3. 이어서 논의할 주제 제시 + +이후 진행은 "토의 마무리"와 동일한 규칙으로. + +===== 계획 ===== +구현 계획서를 작성해줘. + +포함할 것: +1. 파일별 변경 사항 (추가/수정/삭제할 코드) +2. 구현 순서 (의존성 기반) + +사전 검증 (코딩 전에 반드시): +1. 새로 추가할 변수/함수/타입 각각에 대해 해당 파일에서 Grep으로 동일 이름 검색 +2. 충돌 발견 시 "충돌: [이름] - [파일명] 라인 [X]에 기존 정의 있음" 보고 +3. 충돌 있으면 해결 방안 제시 (이름 변경 or 기존 코드 재사용) +4. 계획서에 명시된 모든 함수/변수/타입을 리스트업하고 "어디서 정의, 어디서 사용" 매핑 +5. 사용처는 있는데 정의가 누락된 항목이 있으면 보고 + +주의사항: +- 이 대화를 못 본 사람도 실행할 수 있을 정도로 구체적으로 +- 빠뜨리면 에러날 만한 함정을 명시적으로 경고해줘 + +문서 정리: +- PLAN.md "현재 구현 계획" 섹션을 이 계획으로 교체해줘 +- STATUS.md "다음 작업"도 동기화해줘 + +===== 계획 이해 (선택) ===== +이 계획에서 가장 복잡한 변경 1개를 골라서, +왜 이렇게 해야 하는지 한 문장으로 설명해줘. + +===== 코딩 ===== +위 계획대로 코딩 진행해줘. + +규칙: +1. 각 파일 수정 전에 해당 파일을 먼저 전체 읽어 +2. 새로 추가할 변수명이 파일에 이미 존재하는지 Grep으로 확인 +3. 기존 코드에 동일 이름이 있으면 재사용하거나 명시적으로 삭제 후 새로 정의해 +4. 한 파일 완료할 때마다 린트 확인 + +각 파일 수정이 끝나면 이것만 알려줘: +- 충돌 검사 결과 +- 추가한 import +- 정의한 함수/변수 +- 이 파일에서 가장 핵심적인 변경 1개와 그 이유 (한 문장) + +코딩 완료 후 자체 검증: +- 새로 추가한 모든 변수/함수가 정의되어 있는가? +- 동일 이름의 변수/함수가 파일 내에 2개 이상 존재하지 않는가? +- import한 모든 것이 실제로 사용되는가? +- 사용하는 모든 것이 import되어 있는가? +- interface의 모든 props가 실제로 전달되는가? +이상 없으면 완료 보고, 이상 있으면 수정 후 보고. + +문서 정리: +- PLAN.md "현재 구현 계획"에서 완료된 항목은 [ ] → [x]로 체크해줘 + +===== 새 세션 코딩 (다른 모델) ===== +@popdocs/ 의 README → STATUS → PLAN.md "현재 구현 계획" 순서로 읽고, +계획대로 코딩을 진행해줘. + +규칙: +1. 각 파일 수정 전에 해당 파일을 먼저 전체 읽어 +2. 새로 추가할 변수명이 파일에 이미 존재하는지 Grep으로 확인 +3. 기존 코드에 동일 이름이 있으면 재사용하거나 명시적으로 삭제 후 새로 정의해 +4. 한 파일 완료할 때마다 린트 확인 + +각 파일 수정이 끝나면 이것만 알려줘: +- 충돌 검사 결과 +- 추가한 import +- 정의한 함수/변수 +- 이 파일에서 가장 핵심적인 변경 1개와 그 이유 (한 문장) + +코딩 완료 후 자체 검증: +- 새로 추가한 모든 변수/함수가 정의되어 있는가? +- 동일 이름의 변수/함수가 파일 내에 2개 이상 존재하지 않는가? +- import한 모든 것이 실제로 사용되는가? +- 사용하는 모든 것이 import되어 있는가? +- interface의 모든 props가 실제로 전달되는가? +이상 없으면 완료 보고, 이상 있으면 수정 후 보고. + +문서 정리: +- PLAN.md "현재 구현 계획"에서 완료된 항목은 [ ] → [x]로 체크해줘 + +===== 검수 ===== +변경된 파일들을 검수해줘. + +검증 항목: +1. 린트 에러 +2. 새로 추가한 변수/함수가 중복 정의되지 않았는지 Grep 확인 +3. import한 것 중 사용 안 하는 것, 사용하는데 import 안 한 것 +4. interface에 정의된 props와 실제 전달되는 props 일치 여부 + +문제 발견 시: +- 고치기 전에 해당 코드를 보여주고 어디가 잘못됐는지 표시해줘 +- 내가 확인한 다음에 고쳐줘 +- 발견된 각 문제의 크기를 판단하고 다음 단계를 추천해줘: + 소형 (1개 파일, 10줄 이내): "바로 수정 가능합니다" → "수정" 프롬프트 + 중형 (2-3개 파일, 수십 줄): "미니 계획이 필요합니다" → "미니 계획" 프롬프트 + 대형 (4개+ 파일, 구조 변경): "계획부터 다시 세워야 합니다" → "계획" 프롬프트 + +문서 정리: +- 발견된 문제가 있으면 PROBLEMS.md에 추가할 내용을 미리 정리해둬 + +===== 미니 계획 (중형 문제용) ===== +검수에서 발견된 중형 문제를 해결할 미니 계획을 세워줘. + +발견된 문제: +[문제 설명 - 또는 위 검수에서 보고된 내용 참조] + +미니 계획에 포함할 것: +1. 원인 분석: 왜 이 문제가 발생했는지 한 문장 +2. 영향 범위: 수정해야 할 파일과 함수 목록 +3. 수정 순서: 의존성 기반 +4. 각 파일별 변경 내용 (구체적으로) +5. 사전 검증: 수정할 변수/함수가 다른 곳에서 사용되는지 Grep 확인 +6. 함정 경고: 이 수정으로 깨질 수 있는 다른 부분 + +미니 계획은 PLAN.md에 반영하지 않아 (너무 작으니까). +대신 수정 완료 후 PROBLEMS.md에 "문제 | 해결 | 날짜 | 키워드"로 기록해줘. + +계획이 확인되면 바로 수정 진행해줘. + +수정 규칙: +1. 각 파일 수정 전에 해당 파일을 먼저 읽어 +2. 수정할 변수/함수가 파일에 이미 존재하는지 Grep 확인 +3. 한 파일 완료할 때마다 린트 확인 + +수정 완료 후: +- 자체 검증 (import 정합성, 중복 정의, props 일치) +- 이상 없으면 "재검수 필요 없음" / 이상 있으면 "재검수 필요 - 검수 프롬프트를 다시 사용해주세요" 보고 + +===== 수정 ===== +발견된 문제를 수정해줘. + +수정 전에 먼저: +1. 이 문제가 왜 발생했는지 원인 한 문장 +2. 다음에 같은 실수를 방지하려면 코딩할 때 뭘 확인했어야 하는지 한 문장 +그다음 수정 진행해. + +문서 정리: +- 수정한 내용을 PROBLEMS.md 형식(문제 | 해결 | 날짜 | 키워드)으로 정리해둬 + +===== 기록 ===== +작업 내용을 popdocs에 기록해줘. + +업데이트 대상: +- sessions/오늘날짜.md 생성 +- CHANGELOG.md 섹션 추가 +- STATUS.md 진행상태 업데이트 +- PLAN.md "현재 구현 계획"에서 완료 항목 최종 확인 +- README.md "마지막 대화 요약" 동기화 +- PROBLEMS.md에 발생한 문제-해결 추가 (있으면) +- INDEX.md에 새로 추가된 기능/함수 색인 추가 (있으면) + +추가로 "이번 작업에서 배운 것" 섹션을 포함해줘: +- 새로 알게 된 기술 개념 (있으면) +- 발생했던 에러와 원인 패턴 (있으면) +- 다음에 비슷한 작업할 때 주의할 점 (있으면) +없으면 생략. + +===== 동기화 확인 ===== +popdocs 문서 간 동기화 상태를 확인해줘. + +확인 항목: +1. README.md "마지막 대화 요약"이 STATUS.md와 일치하는지 +2. STATUS.md "다음 작업"이 PLAN.md "현재 구현 계획"과 일치하는지 +3. PLAN.md 체크박스 상태가 실제 코드 변경과 일치하는지 +4. sessions/오늘날짜.md의 "완료" 항목이 CHANGELOG.md와 일치하는지 + +불일치 발견 시: +- 어떤 문서의 어떤 부분이 다른지 보여줘 +- STATUS.md를 정본으로 맞춰줘 + +===== 주간 복습 (금요일) ===== +이번 주 작업 기록을 보고: +1. 내가 "쉽게 설명해줘"라고 요청했던 개념 중 가장 중요한 3개 +2. 발생했던 에러 중 다시 만날 가능성이 높은 패턴 2개 +3. 각각을 한 문장 정의 + 우리 프로젝트에서 어디에 해당하는지 +정리해줘. + +참조할 문서: +- sessions/ 이번 주 파일들 +- PROBLEMS.md 이번 주 항목들 +- CHANGELOG.md 이번 주 섹션들 + +===== 병합 준비 (merge 전) ===== +[source-branch]를 [target-branch]에 병합하려고 해. + +병합 전 점검해줘: +1. 양쪽 브랜치의 최근 커밋 히스토리 비교 (git log --oneline --left-right [target]...[source]) +2. 충돌 예상 파일 목록 (git merge --no-commit --no-ff [source] 후 git diff --name-only --diff-filter=U) +3. 충돌 예상 파일 중 규모가 큰 파일(500줄 이상) 식별 - 이 파일들은 특별 주의 대상 +4. 양쪽에서 동시에 수정한 파일 목록 (git diff --name-only [target]...[source]) +5. 삭제 vs 수정 충돌 가능성 (한쪽에서 삭제하고 다른 쪽에서 수정한 파일) + +점검 후 위험도를 알려줘: +- 높음: 같은 함수/컴포넌트를 양쪽에서 구조적으로 변경한 경우 +- 중간: 같은 파일이지만 다른 부분을 수정한 경우 +- 낮음: 서로 다른 파일만 수정한 경우 + +충돌 예상 파일이 있으면 각 파일별로: +- 양쪽에서 무엇을 변경했는지 한 줄 요약 +- 어떤 쪽을 기준으로 병합해야 하는지 판단 근거 + +===== 병합 실행 (merge 중) ===== +병합을 진행해줘. + +규칙: +1. diff3 형식으로 충돌 표시 (git config merge.conflictstyle diff3) +2. 충돌 파일 하나씩 순서대로 해결 - 의존성 낮은 파일부터 +3. 각 충돌 파일 해결 전에 반드시: + - 공통 조상(base)을 확인하여 양쪽이 원래 코드에서 무엇을 변경했는지 파악 + - 양쪽 변경의 의도를 모두 보존할 수 있는지 판단 + - 한쪽만 선택해야 하면 그 이유를 명시 +4. 충돌 마커(<<<<<<, ======, >>>>>>)가 모두 제거되었는지 확인 + +각 충돌 파일 해결 후 보고: +- 충돌 위치 (함수명/컴포넌트명) +- 해결 방식: "양쪽 통합" / "ours 선택" / "theirs 선택" / "새로 작성" +- 선택 이유 한 문장 + +===== 병합 후 시맨틱 검증 (merge 후 - 가장 중요) ===== +텍스트 충돌은 해결했지만, Git이 감지 못하는 시맨틱 충돌을 점검해줘. + +검증 항목: +1. 함수/변수 이름 변경 충돌: 한쪽에서 rename한 함수를 다른 쪽에서 기존 이름으로 호출하고 있지 않은지 +2. 타입/인터페이스 변경 충돌: 타입 필드가 변경/삭제되었는데 다른 쪽에서 해당 필드를 사용하는 코드가 추가되지 않았는지 +3. import 정합성: 병합 후 중복 import, 누락 import, 사용하지 않는 import 확인 +4. 함수 시그니처 충돌: 매개변수가 변경되었는데 호출부가 기존 시그니처를 사용하지 않는지 +5. 삭제된 코드 의존성: 한쪽에서 삭제한 함수/변수를 다른 쪽 새 코드가 참조하지 않는지 +6. 전역 상태/설정 변경: 설정값이 바뀌었는데 기존 값 기반 로직이 추가되지 않았는지 + +검증 방법: +- TypeScript 타입 체크: npx tsc --noEmit +- 빌드 확인: npm run build +- 남은 충돌 마커: git diff --check +- 병합으로 변경된 전체 diff: git diff HEAD~1..HEAD + +문제 발견 시: +- 파일명, 라인, 구체적인 문제를 보여줘 +- 수정 방안을 제시하되, 내 확인 후에 수정해줘 + +===== 병합 후 빌드/테스트 검증 ===== +병합 후 프로젝트가 정상 작동하는지 확인해줘. + +순서: +1. 남은 충돌 마커 검색: 프로젝트 전체에서 <<<<<<, ======, >>>>>> 검색 +2. TypeScript 컴파일: npx tsc --noEmit → 타입 에러 목록 +3. 프론트엔드 빌드: npm run build → 빌드 에러 목록 +4. 백엔드 빌드: npm run build (backend-node) → 빌드 에러 목록 +5. 린트 체크: 변경된 파일들에 대해 린트 확인 + +에러 발견 시 각각에 대해: +- 에러 메시지 전문 +- 원인이 병합 때문인지, 기존 코드 문제인지 구분 +- 병합 때문이면 어떤 충돌 해결이 잘못되었는지 추적 + +===== 병합 완료 정리 ===== +병합이 완료되었어. 정리해줘. + +정리 항목: +1. 병합 요약: 어떤 브랜치에서 어떤 브랜치로, 총 충돌 파일 수, 해결 방식 통계 +2. 주의가 필요한 변경사항: 시맨틱 충돌 위험이 있었던 부분 목록 +3. 테스트가 필요한 기능: 병합으로 영향받은 기능 목록 (수동 테스트 대상) +4. 커밋 메시지 작성: 병합 내용을 요약한 적절한 커밋 메시지 제안 + +문서 정리: +- PROBLEMS.md에 병합 중 발견된 문제-해결 추가 (있으면) +- CHANGELOG.md에 병합 내용 기록 +``` + +--- + +## 프롬프트 목록 (총 21개) + +| # | 프롬프트 | 언제 사용 | +|---|---------|----------| +| 1 | 토의 중 개념 학습 | 토의 중 모르는 개념이 나왔을 때 | +| 2 | 토의 시작 | 새 주제로 설계/아이디어 토의 시작 | +| 3 | 토의 마무리 | 토의 끝, 결정사항 문서화 | +| 4 | 아이디어/설계 토의 (이어서) | 이전 토의 이어서 할 때 | +| 5 | 계획 | 구현 계획서 작성 | +| 6 | 계획 이해 (선택) | 계획에서 복잡한 부분 이해 | +| 7 | 코딩 | 계획대로 코딩 실행 | +| 8 | 새 세션 코딩 | 다른 모델/세션에서 코딩 이어서 | +| 9 | 검수 | 수정한 파일 검증 (크기 자동 판단 포함) | +| 10 | 미니 계획 | 검수에서 중형 문제 발견 시 | +| 11 | 수정 | 소형 문제 바로 수정 | +| 12 | 기록 | 작업 내용 popdocs 기록 | +| 13 | 동기화 확인 | 문서 간 일치 여부 점검 | +| 14 | 주간 복습 | 금요일 복습 | +| 15 | 병합 준비 | merge 전 위험도 파악 | +| 16 | 병합 실행 | merge 충돌 해결 | +| 17 | 병합 후 시맨틱 검증 | 숨은 논리 충돌 점검 | +| 18 | 병합 후 빌드/테스트 검증 | 빌드 확인 | +| 19 | 병합 완료 정리 | 병합 기록 및 커밋 | + +--- + +## 워크플로우 흐름도 + +``` +[토의 흐름] + 토의 시작 → (자유 토의) → 토의 마무리 + 또는: 아이디어/설계 토의 (이어서) → (자유 토의) → 토의 마무리 + +[코딩 흐름] + 계획 → (계획 이해) → 코딩 → 검수 → AI가 크기 판단 + │ + ┌──────────────┼──────────────┐ + [소형] [중형] [대형] + 수정 →완료 미니 계획 계획부터 + → 수정+자체검증 새 사이클 + → (재검수 필요 시 검수 다시) + +[기록 흐름] + 기록 → 동기화 확인 + +[병합 흐름] + 병합 준비 → 병합 실행 → 시맨틱 검증 → 빌드/테스트 검증 → 병합 완료 정리 +``` + +--- + +## popdocs 업데이트 시점 요약 + +| 단계 | 업데이트 대상 | 시점 | +|------|-------------|------| +| 토의 시작 | 현재 상태 파악, 관련 문서 확인 | 토의 세션 시작 시 | +| 토의 마무리 | PLAN, STATUS, README, sessions/, CHANGELOG, decisions/ | 토의 세션 종료 시 | +| 계획 수립 | PLAN.md "현재 구현 계획", STATUS.md | 계획 확정 시 | +| 코딩 중 | PLAN.md 완료 체크 `[x]` | 각 파일 완료 시 | +| 검수 | PROBLEMS.md 내용 준비 + 크기 자동 판단 | 문제 발견 시 | +| 미니 계획 (중형) | PROBLEMS.md (수정 완료 후) | 검수에서 중형 문제 발견 시 | +| 수정 (소형) | PROBLEMS.md 내용 준비 | 수정 완료 시 | +| 병합 준비 | (응답으로 제공) | merge 시작 전 | +| 병합 실행 | (충돌 해결 중) | merge 진행 중 | +| 병합 시맨틱 검증 | (응답으로 제공) | 텍스트 충돌 해결 직후 | +| 병합 빌드 검증 | (응답으로 제공) | 시맨틱 검증 후 | +| 병합 완료 정리 | PROBLEMS.md, CHANGELOG.md | 병합 최종 완료 시 | +| 기록 | sessions/, CHANGELOG, STATUS, README, PROBLEMS, INDEX | 코딩 작업 완료 시 | +| 동기화 확인 | 전체 문서 간 불일치 점검 | 기록 직후 | +| 주간 복습 | (응답으로 제공, 파일 저장은 선택) | 금요일 | + +--- + +## 세션 분리 가이드 + +``` +[Opus 세션] 토의 + 계획 + → "토의 시작" 프롬프트 → 자유롭게 토의 + → "토의 마무리" 프롬프트 → 문서 반영 + 동기화 + → 세션 종료 + +[새 세션 - Sonnet/Opus] 코딩 + 검수 + 수정 + → "@popdocs/ 읽고 PLAN.md 계획대로 진행해" + → 15건 이내로 완료 + → 세션 종료 + + 검수에서 새 문제 발견 시 (AI가 크기 자동 판단 후 추천): + 소형 → "수정" 프롬프트 → 완료 + 중형 → "미니 계획" 프롬프트 → 수정+자체검증 → 재검수 필요 시 "검수" 다시 + 대형 → "계획" 프롬프트부터 새 사이클 시작 + +[새 세션 - 아무 모델] 기록 + 동기화 확인 + → "기록" 프롬프트 → "동기화 확인" 프롬프트 + +[병합 세션 - Opus 권장] 브랜치 병합 + → "병합 준비" 프롬프트 → 위험도 파악 + → "병합 실행" 프롬프트 → 텍스트 충돌 해결 + → "병합 후 시맨틱 검증" 프롬프트 → 숨은 버그 점검 + → "병합 후 빌드/테스트 검증" 프롬프트 → 빌드 확인 + → "병합 완료 정리" 프롬프트 → 기록 및 커밋 +``` + +**마무리 프롬프트 선택 기준**: +- **토의 마무리**: 코드 변경 없이 설계/계획/아이디어를 논의한 세션 +- **기록**: 실제 코드를 작성/수정/삭제한 세션 +- 한 세션에서 토의 + 코딩을 모두 했으면 **기록**을 사용 (상위 호환) + +**세션을 끊는 기준**: +- 작업이 15건 이내로 끝나면 한 세션에서 끝까지 (끊을 필요 없음) +- 대화가 15건을 넘어갈 것 같으면 세션 분리 +- 완전히 다른 작업으로 전환할 때 + +--- + +*최종 업데이트: 2026-02-10* diff --git a/popdocs/archive/BUGFIX_CANVAS_ROWS.md b/popdocs/archive/BUGFIX_CANVAS_ROWS.md new file mode 100644 index 00000000..713edce6 --- /dev/null +++ b/popdocs/archive/BUGFIX_CANVAS_ROWS.md @@ -0,0 +1,227 @@ +# POP 레이아웃 canvasGrid.rows 버그 수정 + +## 문제점 + +### 1. 데이터 불일치 +- **DB에 저장된 데이터**: `canvasGrid.rowHeight: 20` (고정 픽셀) +- **코드에서 기대하는 데이터**: `canvasGrid.rows: 24` (비율 기반) +- **결과**: `rows`가 `undefined`로 인한 렌더링 오류 + +### 2. 타입 정의 불일치 +- **PopCanvas.tsx 타입**: `{ columns: number; rowHeight: number; gap: number }` +- **실제 사용**: `canvasGrid.rows`로 계산 +- **결과**: 타입 안정성 저하 + +### 3. 렌더링 오류 +- **디자이너**: `rowHeight = resolution.height / undefined` → `NaN` +- **뷰어**: `gridTemplateRows: repeat(undefined, 1fr)` → CSS 무효 +- **결과**: 섹션이 매우 작게 표시됨 + +--- + +## 수정 내용 + +### 1. ensureV2Layout 강화 +**파일**: `frontend/components/pop/designer/types/pop-layout.ts` + +```typescript +export const ensureV2Layout = (data: PopLayoutData): PopLayoutDataV2 => { + let result: PopLayoutDataV2; + + if (isV2Layout(data)) { + result = data; + } else if (isV1Layout(data)) { + result = migrateV1ToV2(data); + } else { + console.warn("알 수 없는 레이아웃 버전, 빈 v2 레이아웃 생성"); + result = createEmptyPopLayoutV2(); + } + + // ✅ canvasGrid.rows 보장 (구버전 데이터 호환) + if (!result.settings.canvasGrid.rows) { + console.warn("canvasGrid.rows 없음, 기본값 24로 설정"); + result.settings.canvasGrid = { + ...result.settings.canvasGrid, + rows: DEFAULT_CANVAS_GRID.rows, // 24 + }; + } + + return result; +}; +``` + +**효과**: DB에서 로드한 구버전 데이터도 자동으로 `rows: 24` 보장 + +--- + +### 2. PopCanvas.tsx 타입 수정 및 fallback +**파일**: `frontend/components/pop/designer/PopCanvas.tsx` + +**타입 정의 수정**: +```typescript +interface DeviceFrameProps { + canvasGrid: { columns: number; rows: number; gap: number }; // rowHeight → rows + // ... +} +``` + +**fallback 추가**: +```typescript +// ✅ rows가 없으면 24 사용 +const rows = canvasGrid.rows || 24; +const rowHeight = Math.floor(resolution.height / rows); +``` + +**효과**: +- 타입 일관성 확보 +- `NaN` 방지 + +--- + +### 3. PopLayoutRenderer.tsx fallback +**파일**: `frontend/components/pop/designer/renderers/PopLayoutRenderer.tsx` + +```typescript +style={{ + display: "grid", + gridTemplateColumns: `repeat(${canvasGrid.columns}, 1fr)`, + // ✅ fallback 추가 + gridTemplateRows: `repeat(${canvasGrid.rows || 24}, 1fr)`, + gap: `${canvasGrid.gap}px`, + padding: `${canvasGrid.gap}px`, +}} +``` + +**효과**: 뷰어에서도 안전하게 렌더링 + +--- + +### 4. 백엔드 저장 로직 강화 +**파일**: `backend-node/src/services/screenManagementService.ts` + +```typescript +if (isV2) { + dataToSave = { + ...layoutData, + version: "pop-2.0", + }; + + // ✅ canvasGrid.rows 검증 및 보정 + if (dataToSave.settings?.canvasGrid) { + if (!dataToSave.settings.canvasGrid.rows) { + console.warn("canvasGrid.rows 없음, 기본값 24로 설정"); + dataToSave.settings.canvasGrid.rows = 24; + } + // ✅ 구버전 rowHeight 필드 제거 + if (dataToSave.settings.canvasGrid.rowHeight) { + console.warn("구버전 rowHeight 필드 제거"); + delete dataToSave.settings.canvasGrid.rowHeight; + } + } +} +``` + +**효과**: 앞으로 저장되는 모든 데이터는 올바른 구조 보장 + +--- + +## 원칙 준수 여부 + +### 1. 데스크톱과 완전 분리 ✅ +- POP 전용 파일만 수정 +- 데스크톱 코드 0% 영향 + +### 2. 4모드 반응형 디자인 ✅ +- 변경 없음 + +### 3. 비율 기반 그리드 시스템 ✅ +- **오히려 원칙을 바로잡는 수정** +- 고정 픽셀(`rowHeight`) → 비율(`rows`) 강제 + +--- + +## 해결된 문제 + +| 문제 | 수정 전 | 수정 후 | +|------|---------|---------| +| 섹션 크기 | 매우 작게 표시 | 정상 크기 (24x24 그리드) | +| 디자이너 렌더링 | `NaN` 오류 | 정상 계산 | +| 뷰어 렌더링 | CSS 무효 | 비율 기반 렌더링 | +| 타입 안정성 | `rowHeight` vs `rows` 불일치 | `rows`로 통일 | +| 구버전 데이터 | 호환 불가 | 자동 보정 | + +--- + +## 테스트 방법 + +### 1. 기존 화면 확인 (screen_id: 3884) +```bash +# 디자이너 접속 +http://localhost:9771/screen-management/pop-designer/3884 + +# 저장 후 뷰어 확인 +http://localhost:9771/pop/screens/3884 +``` + +**기대 결과**: +- 섹션이 화면 전체 크기로 정상 표시 +- 가로/세로 모드 전환 시 비율 유지 + +### 2. 새로운 화면 생성 +- POP 디자이너에서 새 화면 생성 +- 섹션 추가 및 배치 +- 저장 후 DB 확인 + +**DB 확인**: +```sql +SELECT + screen_id, + layout_data->'settings'->'canvasGrid' as canvas_grid +FROM screen_layouts_pop +WHERE screen_id = 3884; +``` + +**기대 결과**: +```json +{ + "gap": 4, + "rows": 24, + "columns": 24 +} +``` + +--- + +## 추가 조치 사항 + +### 1. 기존 DB 데이터 마이그레이션 (선택) +만약 프론트엔드 자동 보정이 아닌 DB 마이그레이션을 원한다면: + +```sql +UPDATE screen_layouts_pop +SET layout_data = jsonb_set( + jsonb_set( + layout_data, + '{settings,canvasGrid,rows}', + '24' + ), + '{settings,canvasGrid}', + (layout_data->'settings'->'canvasGrid') - 'rowHeight' +) +WHERE layout_data->'settings'->'canvasGrid'->>'rows' IS NULL + AND layout_data->>'version' = 'pop-2.0'; +``` + +### 2. 모드별 컴포넌트 위치 반대 문제 +**별도 이슈**: `activeModeKey` 상태 관리 점검 필요 +- DeviceFrame 클릭 시 모드 전환 +- 저장 시 올바른 `modeKey` 전달 확인 + +--- + +## 결론 + +✅ **원칙 준수**: 데스크톱 분리, 4모드 반응형 유지 +✅ **비율 기반 강제**: 고정 픽셀 제거 +✅ **하위 호환**: 구버전 데이터 자동 보정 +✅ **안정성 향상**: 타입 일관성 확보 diff --git a/popdocs/archive/COMPONENT_ROADMAP.md b/popdocs/archive/COMPONENT_ROADMAP.md new file mode 100644 index 00000000..99c0d616 --- /dev/null +++ b/popdocs/archive/COMPONENT_ROADMAP.md @@ -0,0 +1,389 @@ +# POP 컴포넌트 로드맵 + +## 큰 그림: 3단계 접근 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ │ +│ 1단계: 기초 블록 2단계: 조합 블록 3단계: 완성 화면 │ +│ ─────────────── ─────────────── ─────────────── │ +│ │ +│ [버튼] [입력창] [폼 그룹] [작업지시 화면] │ +│ [아이콘] [라벨] → [카드] → [실적입력 화면] │ +│ [뱃지] [로딩] [리스트] [모니터링 대시보드] │ +│ [테이블] │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 1단계: 기초 블록 (Primitive) + +가장 작은 단위. 다른 곳에서 재사용됩니다. + +### 필수 기초 블록 + +| 컴포넌트 | 역할 | 우선순위 | +|---------|------|---------| +| `PopButton` | 모든 버튼 | 1 | +| `PopInput` | 텍스트 입력 | 1 | +| `PopLabel` | 라벨/제목 | 1 | +| `PopIcon` | 아이콘 표시 | 1 | +| `PopBadge` | 상태 뱃지 | 2 | +| `PopLoading` | 로딩 스피너 | 2 | +| `PopDivider` | 구분선 | 3 | + +### PopButton 예시 + +```typescript +interface PopButtonProps { + children: React.ReactNode; + variant: "primary" | "secondary" | "danger" | "success"; + size: "sm" | "md" | "lg" | "xl"; + disabled?: boolean; + loading?: boolean; + icon?: string; + fullWidth?: boolean; + onClick?: () => void; +} + +// 사용 + + 작업 완료 + +``` + +### PopInput 예시 + +```typescript +interface PopInputProps { + type: "text" | "number" | "date" | "time"; + value: string | number; + onChange: (value: string | number) => void; + label?: string; + placeholder?: string; + required?: boolean; + error?: string; + size: "md" | "lg"; // POP은 lg 기본 +} + +// 사용 + +``` + +--- + +## 2단계: 조합 블록 (Compound) + +기초 블록을 조합한 중간 단위. + +### 조합 블록 목록 + +| 컴포넌트 | 구성 | 용도 | +|---------|------|-----| +| `PopFormField` | Label + Input + Error | 폼 입력 그룹 | +| `PopCard` | Container + Header + Body | 정보 카드 | +| `PopListItem` | Container + Content + Action | 리스트 항목 | +| `PopNumberPad` | Grid + Buttons | 숫자 입력 | +| `PopStatusBox` | Icon + Label + Value | 상태 표시 | + +### PopFormField 예시 + +```typescript +// 기초 블록 조합 +function PopFormField({ label, required, error, children }) { + return ( +
+ {label} + {children} + {error && {error}} +
+ ); +} + +// 사용 + + + +``` + +### PopCard 예시 + +```typescript +function PopCard({ title, badge, children, onClick }) { + return ( +
+
+ {title} + {badge && {badge}} +
+
+ {children} +
+
+ ); +} + +// 사용 + +

목표 수량: 100개

+

완료 수량: 45개

+
+``` + +--- + +## 3단계: 복합 컴포넌트 (Complex) + +비즈니스 로직이 포함된 완성형. + +### 복합 컴포넌트 목록 + +| 컴포넌트 | 기능 | 데이터 | +|---------|------|-------| +| `PopDataTable` | 대량 데이터 표시/편집 | API 연동 | +| `PopCardList` | 카드 형태 리스트 | API 연동 | +| `PopBarcodeScanner` | 바코드/QR 스캔 | 카메라/외부장치 | +| `PopKpiGauge` | KPI 게이지 | 실시간 데이터 | +| `PopAlarmList` | 알람 목록 | 웹소켓 | +| `PopProcessFlow` | 공정 흐름도 | 공정 데이터 | + +### PopDataTable 예시 + +```typescript +interface PopDataTableProps { + // 데이터 + data: any[]; + columns: Column[]; + + // 기능 + selectable?: boolean; + editable?: boolean; + sortable?: boolean; + + // 반응형 (자동) + responsiveColumns?: { + tablet: string[]; + mobile: string[]; + }; + + // 이벤트 + onRowClick?: (row: any) => void; + onSelectionChange?: (selected: any[]) => void; +} + +// 사용 + openDetail(row.id)} +/> +``` + +--- + +## 개발 순서 제안 + +### Phase 1: 기초 (1-2주) + +``` +Week 1: +- PopButton (모든 버튼의 기반) +- PopInput (모든 입력의 기반) +- PopLabel +- PopIcon + +Week 2: +- PopBadge +- PopLoading +- PopDivider +``` + +### Phase 2: 조합 (2-3주) + +``` +Week 3: +- PopFormField (폼의 기본 단위) +- PopCard (카드의 기본 단위) + +Week 4: +- PopListItem +- PopStatusBox +- PopNumberPad + +Week 5: +- PopModal +- PopToast +``` + +### Phase 3: 복합 (3-4주) + +``` +Week 6-7: +- PopDataTable (가장 복잡) +- PopCardList + +Week 8-9: +- PopBarcodeScanner +- PopKpiGauge +- PopAlarmList +- PopProcessFlow +``` + +--- + +## 컴포넌트 설계 원칙 + +### 1. 크기는 외부에서 제어 + +```typescript +// 좋음: 크기를 props로 받음 +확인 + +// 나쁨: 내부에서 크기 고정 + +``` + +### 2. 최소 크기는 내부에서 보장 + +```typescript +// 컴포넌트 내부 +const styles = { + minHeight: 48, // 터치 최소 크기 보장 + minWidth: 80, +}; +``` + +### 3. 반응형은 자동 + +```typescript +// 좋음: 화면 크기에 따라 자동 조절 + + + + +// 나쁨: 모드별로 다른 컴포넌트 +{isMobile ? : } +``` + +### 4. 데이터와 UI 분리 + +```typescript +// 좋음: 데이터 로직은 훅으로 +const { data, loading, error } = useWorkOrders(); + + + +// 나쁨: 컴포넌트 안에서 fetch +function PopDataTable() { + useEffect(() => { + fetch('/api/work-orders')... + }, []); +} +``` + +--- + +## 폴더 구조 제안 + +``` +frontend/components/pop/ +├── primitives/ # 1단계: 기초 블록 +│ ├── PopButton.tsx +│ ├── PopInput.tsx +│ ├── PopLabel.tsx +│ ├── PopIcon.tsx +│ ├── PopBadge.tsx +│ ├── PopLoading.tsx +│ └── index.ts +│ +├── compounds/ # 2단계: 조합 블록 +│ ├── PopFormField.tsx +│ ├── PopCard.tsx +│ ├── PopListItem.tsx +│ ├── PopNumberPad.tsx +│ ├── PopStatusBox.tsx +│ └── index.ts +│ +├── complex/ # 3단계: 복합 컴포넌트 +│ ├── PopDataTable/ +│ │ ├── PopDataTable.tsx +│ │ ├── PopTableHeader.tsx +│ │ ├── PopTableRow.tsx +│ │ └── index.ts +│ ├── PopCardList/ +│ ├── PopBarcodeScanner/ +│ └── index.ts +│ +├── hooks/ # 공용 훅 +│ ├── usePopTheme.ts +│ ├── useResponsiveSize.ts +│ └── useTouchFeedback.ts +│ +└── styles/ # 공용 스타일 + ├── pop-variables.css + └── pop-base.css +``` + +--- + +## 스타일 변수 + +```css +/* pop-variables.css */ + +:root { + /* 터치 크기 */ + --pop-touch-min: 48px; + --pop-touch-industrial: 60px; + + /* 폰트 크기 */ + --pop-font-body: clamp(14px, 1.5vw, 18px); + --pop-font-heading: clamp(18px, 2.5vw, 28px); + --pop-font-caption: clamp(12px, 1vw, 14px); + + /* 간격 */ + --pop-gap-sm: 8px; + --pop-gap-md: 16px; + --pop-gap-lg: 24px; + + /* 색상 */ + --pop-primary: #2563eb; + --pop-success: #16a34a; + --pop-warning: #f59e0b; + --pop-danger: #dc2626; + + /* 고대비 (야외용) */ + --pop-high-contrast-bg: #000000; + --pop-high-contrast-fg: #ffffff; +} +``` + +--- + +## 다음 단계 + +1. **기초 블록부터 시작**: PopButton, PopInput 먼저 만들기 +2. **스토리북 설정**: 컴포넌트별 문서화 +3. **테스트**: 터치 크기, 반응형 확인 +4. **디자이너 연동**: v4 레이아웃 시스템과 통합 + +--- + +*최종 업데이트: 2026-02-03* diff --git a/popdocs/archive/GRID_CODING_PLAN.md b/popdocs/archive/GRID_CODING_PLAN.md new file mode 100644 index 00000000..60760e96 --- /dev/null +++ b/popdocs/archive/GRID_CODING_PLAN.md @@ -0,0 +1,763 @@ +# POP 그리드 시스템 코딩 계획 + +> 작성일: 2026-02-05 +> 상태: 코딩 준비 완료 + +--- + +## 작업 목록 + +``` +Phase 5.1: 타입 정의 ───────────────────────────── + [ ] 1. v5 타입 정의 (PopLayoutDataV5, PopGridConfig 등) + [ ] 2. 브레이크포인트 상수 정의 + [ ] 3. v5 생성/변환 함수 + +Phase 5.2: 그리드 렌더러 ───────────────────────── + [ ] 4. PopGridRenderer.tsx 생성 + [ ] 5. 위치 변환 로직 (12칸→4칸) + +Phase 5.3: 디자이너 UI ─────────────────────────── + [ ] 6. PopCanvasV5.tsx 생성 + [ ] 7. 드래그 스냅 기능 + [ ] 8. ComponentEditorPanelV5.tsx + +Phase 5.4: 통합 ────────────────────────────────── + [ ] 9. 자동 변환 알고리즘 + [ ] 10. PopDesigner.tsx 통합 +``` + +--- + +## Phase 5.1: 타입 정의 + +### 작업 1: v5 타입 정의 + +**파일**: `frontend/components/pop/designer/types/pop-layout.ts` + +**추가할 코드**: + +```typescript +// ======================================== +// v5.0 그리드 기반 레이아웃 +// ======================================== +// 핵심: CSS Grid로 정확한 위치 지정 +// - 열/행 좌표로 배치 (col, row) +// - 칸 단위 크기 (colSpan, rowSpan) + +/** + * v5 레이아웃 (그리드 기반) + */ +export interface PopLayoutDataV5 { + version: "pop-5.0"; + + // 그리드 설정 + gridConfig: PopGridConfig; + + // 컴포넌트 정의 (ID → 정의) + components: Record; + + // 데이터 흐름 (기존과 동일) + dataFlow: PopDataFlow; + + // 전역 설정 + settings: PopGlobalSettingsV5; + + // 메타데이터 + metadata?: PopLayoutMetadata; + + // 모드별 오버라이드 (위치 변경용) + overrides?: { + mobile_portrait?: PopModeOverrideV5; + mobile_landscape?: PopModeOverrideV5; + tablet_portrait?: PopModeOverrideV5; + }; +} + +/** + * 그리드 설정 + */ +export interface PopGridConfig { + // 행 높이 (px) - 1행의 기본 높이 + rowHeight: number; // 기본 48px + + // 간격 (px) + gap: number; // 기본 8px + + // 패딩 (px) + padding: number; // 기본 16px +} + +/** + * v5 컴포넌트 정의 + */ +export interface PopComponentDefinitionV5 { + id: string; + type: PopComponentType; + label?: string; + + // 위치 (열/행 좌표) - 기본 모드(태블릿 가로 12칸) 기준 + position: PopGridPosition; + + // 모드별 표시/숨김 + visibility?: { + tablet_landscape?: boolean; + tablet_portrait?: boolean; + mobile_landscape?: boolean; + mobile_portrait?: boolean; + }; + + // 기존 속성 + dataBinding?: PopDataBinding; + style?: PopStylePreset; + config?: PopComponentConfig; +} + +/** + * 그리드 위치 + */ +export interface PopGridPosition { + col: number; // 시작 열 (1부터, 최대 12) + row: number; // 시작 행 (1부터) + colSpan: number; // 차지할 열 수 (1~12) + rowSpan: number; // 차지할 행 수 (1~) +} + +/** + * v5 전역 설정 + */ +export interface PopGlobalSettingsV5 { + // 터치 최소 크기 (px) + touchTargetMin: number; // 기본 48 + + // 모드 + mode: "normal" | "industrial"; +} + +/** + * v5 모드별 오버라이드 + */ +export interface PopModeOverrideV5 { + // 컴포넌트별 위치 오버라이드 + positions?: Record>; + + // 컴포넌트별 숨김 + hidden?: string[]; +} +``` + +### 작업 2: 브레이크포인트 상수 + +**파일**: `frontend/components/pop/designer/types/pop-layout.ts` + +```typescript +// ======================================== +// 그리드 브레이크포인트 +// ======================================== + +export type GridMode = + | "mobile_portrait" + | "mobile_landscape" + | "tablet_portrait" + | "tablet_landscape"; + +export const GRID_BREAKPOINTS: Record = { + // 4~6인치 모바일 세로 + mobile_portrait: { + maxWidth: 599, + columns: 4, + rowHeight: 40, + gap: 8, + padding: 12, + label: "모바일 세로 (4칸)", + }, + + // 6~8인치 모바일 가로 / 작은 태블릿 + mobile_landscape: { + minWidth: 600, + maxWidth: 839, + columns: 6, + rowHeight: 44, + gap: 8, + padding: 16, + label: "모바일 가로 (6칸)", + }, + + // 8~10인치 태블릿 세로 + tablet_portrait: { + minWidth: 840, + maxWidth: 1023, + columns: 8, + rowHeight: 48, + gap: 12, + padding: 16, + label: "태블릿 세로 (8칸)", + }, + + // 10~14인치 태블릿 가로 (기본) + tablet_landscape: { + minWidth: 1024, + columns: 12, + rowHeight: 48, + gap: 16, + padding: 24, + label: "태블릿 가로 (12칸)", + }, +}; + +// 기본 모드 +export const DEFAULT_GRID_MODE: GridMode = "tablet_landscape"; + +// 뷰포트 너비로 모드 감지 +export function detectGridMode(viewportWidth: number): GridMode { + if (viewportWidth < 600) return "mobile_portrait"; + if (viewportWidth < 840) return "mobile_landscape"; + if (viewportWidth < 1024) return "tablet_portrait"; + return "tablet_landscape"; +} +``` + +### 작업 3: v5 생성/변환 함수 + +**파일**: `frontend/components/pop/designer/types/pop-layout.ts` + +```typescript +// ======================================== +// v5 유틸리티 함수 +// ======================================== + +/** + * 빈 v5 레이아웃 생성 + */ +export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({ + version: "pop-5.0", + gridConfig: { + rowHeight: 48, + gap: 8, + padding: 16, + }, + components: {}, + dataFlow: { connections: [] }, + settings: { + touchTargetMin: 48, + mode: "normal", + }, +}); + +/** + * v5 레이아웃 여부 확인 + */ +export const isV5Layout = (layout: any): layout is PopLayoutDataV5 => { + return layout?.version === "pop-5.0"; +}; + +/** + * v5 컴포넌트 정의 생성 + */ +export const createComponentDefinitionV5 = ( + id: string, + type: PopComponentType, + position: PopGridPosition, + label?: string +): PopComponentDefinitionV5 => ({ + id, + type, + label, + position, +}); + +/** + * 컴포넌트 타입별 기본 크기 (칸 단위) + */ +export const DEFAULT_COMPONENT_SIZE: Record = { + "pop-field": { colSpan: 3, rowSpan: 1 }, + "pop-button": { colSpan: 2, rowSpan: 1 }, + "pop-list": { colSpan: 12, rowSpan: 4 }, + "pop-indicator": { colSpan: 3, rowSpan: 2 }, + "pop-scanner": { colSpan: 6, rowSpan: 2 }, + "pop-numpad": { colSpan: 4, rowSpan: 5 }, + "pop-spacer": { colSpan: 1, rowSpan: 1 }, + "pop-break": { colSpan: 12, rowSpan: 0 }, +}; + +/** + * v4 → v5 마이그레이션 + */ +export const migrateV4ToV5 = (layoutV4: PopLayoutDataV4): PopLayoutDataV5 => { + const componentsV4 = Object.values(layoutV4.components); + const componentsV5: Record = {}; + + // Flexbox 순서 → Grid 위치 변환 + let currentRow = 1; + let currentCol = 1; + const columns = 12; + + componentsV4.forEach((comp) => { + // 픽셀 → 칸 변환 (대략적) + const colSpan = comp.size.width === "fill" + ? columns + : Math.max(1, Math.min(12, Math.round((comp.size.fixedWidth || 100) / 85))); + const rowSpan = Math.max(1, Math.round((comp.size.fixedHeight || 48) / 48)); + + // 줄바꿈 체크 + if (currentCol + colSpan - 1 > columns) { + currentRow += 1; + currentCol = 1; + } + + componentsV5[comp.id] = { + id: comp.id, + type: comp.type, + label: comp.label, + position: { + col: currentCol, + row: currentRow, + colSpan, + rowSpan, + }, + visibility: comp.visibility, + dataBinding: comp.dataBinding, + config: comp.config, + }; + + currentCol += colSpan; + }); + + return { + version: "pop-5.0", + gridConfig: { + rowHeight: 48, + gap: layoutV4.settings.defaultGap, + padding: layoutV4.settings.defaultPadding, + }, + components: componentsV5, + dataFlow: layoutV4.dataFlow, + settings: { + touchTargetMin: layoutV4.settings.touchTargetMin, + mode: layoutV4.settings.mode, + }, + }; +}; +``` + +--- + +## Phase 5.2: 그리드 렌더러 + +### 작업 4: PopGridRenderer.tsx + +**파일**: `frontend/components/pop/designer/renderers/PopGridRenderer.tsx` + +```typescript +"use client"; + +import React, { useMemo } from "react"; +import { cn } from "@/lib/utils"; +import { + PopLayoutDataV5, + PopComponentDefinitionV5, + PopGridPosition, + GridMode, + GRID_BREAKPOINTS, + detectGridMode, +} from "../types/pop-layout"; + +interface PopGridRendererProps { + layout: PopLayoutDataV5; + viewportWidth: number; + currentMode?: GridMode; + isDesignMode?: boolean; + selectedComponentId?: string | null; + onComponentClick?: (componentId: string) => void; + onBackgroundClick?: () => void; + className?: string; +} + +export function PopGridRenderer({ + layout, + viewportWidth, + currentMode, + isDesignMode = false, + selectedComponentId, + onComponentClick, + onBackgroundClick, + className, +}: PopGridRendererProps) { + const { gridConfig, components, overrides } = layout; + + // 현재 모드 (자동 감지 또는 지정) + const mode = currentMode || detectGridMode(viewportWidth); + const breakpoint = GRID_BREAKPOINTS[mode]; + + // CSS Grid 스타일 + const gridStyle = useMemo((): React.CSSProperties => ({ + display: "grid", + gridTemplateColumns: `repeat(${breakpoint.columns}, 1fr)`, + gridAutoRows: `${breakpoint.rowHeight}px`, + gap: `${breakpoint.gap}px`, + padding: `${breakpoint.padding}px`, + minHeight: "100%", + }), [breakpoint]); + + // visibility 체크 + const isVisible = (comp: PopComponentDefinitionV5): boolean => { + if (!comp.visibility) return true; + return comp.visibility[mode] !== false; + }; + + // 위치 변환 (12칸 기준 → 현재 모드 칸 수) + const convertPosition = (position: PopGridPosition): React.CSSProperties => { + const sourceColumns = 12; // 항상 12칸 기준으로 저장 + const targetColumns = breakpoint.columns; + + if (sourceColumns === targetColumns) { + return { + gridColumn: `${position.col} / span ${position.colSpan}`, + gridRow: `${position.row} / span ${position.rowSpan}`, + }; + } + + // 비율 계산 + const ratio = targetColumns / sourceColumns; + let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1); + let newColSpan = Math.max(1, Math.round(position.colSpan * ratio)); + + // 범위 초과 방지 + if (newCol + newColSpan - 1 > targetColumns) { + newColSpan = targetColumns - newCol + 1; + } + + return { + gridColumn: `${newCol} / span ${Math.max(1, newColSpan)}`, + gridRow: `${position.row} / span ${position.rowSpan}`, + }; + }; + + // 오버라이드 적용 + const getEffectivePosition = (comp: PopComponentDefinitionV5): PopGridPosition => { + const override = overrides?.[mode]?.positions?.[comp.id]; + if (override) { + return { ...comp.position, ...override }; + } + return comp.position; + }; + + return ( +
{ + if (e.target === e.currentTarget) { + onBackgroundClick?.(); + } + }} + > + {Object.values(components).map((comp) => { + if (!isVisible(comp)) return null; + + const position = getEffectivePosition(comp); + const positionStyle = convertPosition(position); + + return ( +
{ + e.stopPropagation(); + onComponentClick?.(comp.id); + }} + > + {/* 컴포넌트 내용 */} + +
+ ); + })} +
+ ); +} + +// 컴포넌트 내용 렌더링 +function ComponentContent({ + component, + isDesignMode +}: { + component: PopComponentDefinitionV5; + isDesignMode: boolean; +}) { + const typeLabels: Record = { + "pop-field": "필드", + "pop-button": "버튼", + "pop-list": "리스트", + "pop-indicator": "인디케이터", + "pop-scanner": "스캐너", + "pop-numpad": "숫자패드", + "pop-spacer": "스페이서", + "pop-break": "줄바꿈", + }; + + if (isDesignMode) { + return ( +
+
+ + {component.label || typeLabels[component.type] || component.type} + +
+
+ + {typeLabels[component.type]} + +
+
+ ); + } + + // 실제 컴포넌트 렌더링 (Phase 4에서 구현) + return ( +
+ + {component.label || typeLabels[component.type]} + +
+ ); +} + +export default PopGridRenderer; +``` + +### 작업 5: 위치 변환 유틸리티 + +**파일**: `frontend/components/pop/designer/utils/gridUtils.ts` + +```typescript +import { PopGridPosition, GridMode, GRID_BREAKPOINTS } from "../types/pop-layout"; + +/** + * 12칸 기준 위치를 다른 모드로 변환 + */ +export function convertPositionToMode( + position: PopGridPosition, + targetMode: GridMode +): PopGridPosition { + const sourceColumns = 12; + const targetColumns = GRID_BREAKPOINTS[targetMode].columns; + + if (sourceColumns === targetColumns) { + return position; + } + + const ratio = targetColumns / sourceColumns; + + // 열 위치 변환 + let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1); + let newColSpan = Math.max(1, Math.round(position.colSpan * ratio)); + + // 범위 초과 방지 + if (newCol > targetColumns) { + newCol = 1; + } + if (newCol + newColSpan - 1 > targetColumns) { + newColSpan = targetColumns - newCol + 1; + } + + return { + col: newCol, + row: position.row, + colSpan: Math.max(1, newColSpan), + rowSpan: position.rowSpan, + }; +} + +/** + * 두 위치가 겹치는지 확인 + */ +export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean { + // 열 겹침 + const colOverlap = !(a.col + a.colSpan - 1 < b.col || b.col + b.colSpan - 1 < a.col); + // 행 겹침 + const rowOverlap = !(a.row + a.rowSpan - 1 < b.row || b.row + b.rowSpan - 1 < a.row); + + return colOverlap && rowOverlap; +} + +/** + * 겹침 해결 (아래로 밀기) + */ +export function resolveOverlaps( + positions: Array<{ id: string; position: PopGridPosition }>, + columns: number +): Array<{ id: string; position: PopGridPosition }> { + // row, col 순으로 정렬 + const sorted = [...positions].sort((a, b) => + a.position.row - b.position.row || a.position.col - b.position.col + ); + + const resolved: Array<{ id: string; position: PopGridPosition }> = []; + + sorted.forEach((item) => { + let { row, col, colSpan, rowSpan } = item.position; + + // 기존 배치와 겹치면 아래로 이동 + let attempts = 0; + while (attempts < 100) { + const currentPos: PopGridPosition = { col, row, colSpan, rowSpan }; + const hasOverlap = resolved.some(r => isOverlapping(currentPos, r.position)); + + if (!hasOverlap) break; + + row++; + attempts++; + } + + resolved.push({ + id: item.id, + position: { col, row, colSpan, rowSpan }, + }); + }); + + return resolved; +} + +/** + * 마우스 좌표 → 그리드 좌표 변환 + */ +export function mouseToGridPosition( + mouseX: number, + mouseY: number, + canvasRect: DOMRect, + columns: number, + rowHeight: number, + gap: number, + padding: number +): { col: number; row: number } { + // 캔버스 내 상대 위치 + const relX = mouseX - canvasRect.left - padding; + const relY = mouseY - canvasRect.top - padding; + + // 칸 너비 계산 + const totalGap = gap * (columns - 1); + const colWidth = (canvasRect.width - padding * 2 - totalGap) / columns; + + // 그리드 좌표 계산 (1부터 시작) + const col = Math.max(1, Math.min(columns, Math.floor(relX / (colWidth + gap)) + 1)); + const row = Math.max(1, Math.floor(relY / (rowHeight + gap)) + 1); + + return { col, row }; +} + +/** + * 그리드 좌표 → 픽셀 좌표 변환 + */ +export function gridToPixelPosition( + col: number, + row: number, + canvasWidth: number, + columns: number, + rowHeight: number, + gap: number, + padding: number +): { x: number; y: number; width: number; height: number } { + const totalGap = gap * (columns - 1); + const colWidth = (canvasWidth - padding * 2 - totalGap) / columns; + + return { + x: padding + (col - 1) * (colWidth + gap), + y: padding + (row - 1) * (rowHeight + gap), + width: colWidth, + height: rowHeight, + }; +} +``` + +--- + +## Phase 5.3: 디자이너 UI + +### 작업 6-7: PopCanvasV5.tsx + +**파일**: `frontend/components/pop/designer/PopCanvasV5.tsx` + +핵심 기능: +- 그리드 배경 표시 (바둑판) +- 4개 모드 프리셋 버튼 +- 드래그 앤 드롭 (칸에 스냅) +- 컴포넌트 리사이즈 (칸 단위) + +### 작업 8: ComponentEditorPanelV5.tsx + +**파일**: `frontend/components/pop/designer/panels/ComponentEditorPanelV5.tsx` + +핵심 기능: +- 위치 편집 (col, row 입력) +- 크기 편집 (colSpan, rowSpan 입력) +- visibility 체크박스 + +--- + +## Phase 5.4: 통합 + +### 작업 9: 자동 변환 알고리즘 + +이미 `gridUtils.ts`에 포함 + +### 작업 10: PopDesigner.tsx 통합 + +**수정 파일**: `frontend/components/pop/designer/PopDesigner.tsx` + +변경 사항: +- v5 레이아웃 상태 추가 +- v3/v4/v5 자동 판별 +- 새 화면 → v5로 시작 +- v4 → v5 마이그레이션 옵션 + +--- + +## 파일 목록 + +| 상태 | 파일 | 작업 | +|------|------|------| +| 수정 | `types/pop-layout.ts` | v5 타입, 상수, 함수 추가 | +| 생성 | `renderers/PopGridRenderer.tsx` | 그리드 렌더러 | +| 생성 | `utils/gridUtils.ts` | 유틸리티 함수 | +| 생성 | `PopCanvasV5.tsx` | 그리드 캔버스 | +| 생성 | `panels/ComponentEditorPanelV5.tsx` | 속성 패널 | +| 수정 | `PopDesigner.tsx` | v5 통합 | + +--- + +## 시작 순서 + +``` +1. pop-layout.ts에 v5 타입 추가 (작업 1-3) + ↓ +2. PopGridRenderer.tsx 생성 (작업 4) + ↓ +3. gridUtils.ts 생성 (작업 5) + ↓ +4. PopCanvasV5.tsx 생성 (작업 6-7) + ↓ +5. ComponentEditorPanelV5.tsx 생성 (작업 8) + ↓ +6. PopDesigner.tsx 수정 (작업 9-10) + ↓ +7. 테스트 +``` + +--- + +*다음 단계: Phase 5.1 작업 1 시작 (v5 타입 정의)* diff --git a/popdocs/archive/GRID_SYSTEM_DESIGN.md b/popdocs/archive/GRID_SYSTEM_DESIGN.md new file mode 100644 index 00000000..7203763c --- /dev/null +++ b/popdocs/archive/GRID_SYSTEM_DESIGN.md @@ -0,0 +1,329 @@ +# POP 화면 그리드 시스템 설계 + +> 작성일: 2026-02-05 +> 상태: 계획 (Plan) +> 관련: Softr, Ant Design, Material Design 분석 기반 + +--- + +## 1. 목적 + +POP 화면의 반응형 레이아웃을 **일관성 있고 예측 가능하게** 만들기 위한 그리드 시스템 설계 + +### 현재 문제 +- 픽셀 단위 자유 배치 → 화면 크기별로 깨짐 +- 컴포넌트 크기 규칙 없음 → 디자인 불일치 +- 반응형 규칙 부족 → 모드별 수동 조정 필요 + +### 목표 +- 그리드 기반 배치로 일관성 확보 +- 크기 프리셋으로 디자인 통일 +- 자동 반응형으로 작업량 감소 + +--- + +## 2. 대상 디바이스 + +### 지원 범위 + +| 구분 | 크기 범위 | 기준 해상도 | 비고 | +|------|----------|-------------|------| +| 모바일 | 4~8인치 | 375x667 (세로) | 산업용 PDA 포함 | +| 태블릿 | 8~14인치 | 1024x768 (가로) | 기본 기준 | + +### 참고: 산업용 디바이스 해상도 + +| 디바이스 | 화면 크기 | 해상도 | +|----------|----------|--------| +| Zebra TC57 PDA | 5인치 | 720x1280 | +| Honeywell CT47 | 5.5인치 | 2160x1080 | +| Honeywell RT10A | 10.1인치 | 1920x1200 | + +--- + +## 3. 그리드 시스템 설계 + +### 3.1 브레이크포인트 (Breakpoints) + +Material Design 가이드라인 기반으로 4단계 정의: + +| 모드 | 약어 | 너비 범위 | 대표 디바이스 | 그리드 칸 수 | +|------|------|----------|---------------|-------------| +| 모바일 세로 | `mp` | ~599px | 4~6인치 폰 | **4 columns** | +| 모바일 가로 | `ml` | 600~839px | 폰 가로, 7인치 태블릿 | **6 columns** | +| 태블릿 세로 | `tp` | 840~1023px | 8~10인치 태블릿 세로 | **8 columns** | +| 태블릿 가로 | `tl` | 1024px~ | 10~14인치 태블릿 가로 | **12 columns** | + +### 3.2 기준 해상도 + +| 모드 | 기준 너비 | 기준 높이 | 비고 | +|------|----------|----------|------| +| 모바일 세로 | 375px | 667px | iPhone SE 기준 | +| 모바일 가로 | 667px | 375px | - | +| 태블릿 세로 | 768px | 1024px | iPad 기준 | +| **태블릿 가로** | **1024px** | **768px** | **기본 설계 모드** | + +### 3.3 그리드 구조 + +``` +태블릿 가로 (12 columns) +┌──────────────────────────────────────────────────────────────┐ +│ ← 16px →│ Col │ 16px │ Col │ 16px │ ... │ Col │← 16px →│ +│ margin │ 1 │ gap │ 2 │ gap │ │ 12 │ margin │ +└──────────────────────────────────────────────────────────────┘ + +모바일 세로 (4 columns) +┌────────────────────────┐ +│← 16px →│ Col │ 8px │ Col │ 8px │ Col │ 8px │ Col │← 16px →│ +│ margin │ 1 │ gap │ 2 │ gap │ 3 │ gap │ 4 │ margin │ +└────────────────────────┘ +``` + +### 3.4 마진/간격 규칙 + +8px 기반 간격 시스템 (Material Design 표준): + +| 속성 | 태블릿 | 모바일 | 용도 | +|------|--------|--------|------| +| screenPadding | 24px | 16px | 화면 가장자리 여백 | +| gapSm | 8px | 8px | 컴포넌트 사이 최소 간격 | +| gapMd | 16px | 12px | 기본 간격 | +| gapLg | 24px | 16px | 섹션 간 간격 | +| rowGap | 16px | 12px | 줄 사이 간격 | + +--- + +## 4. 컴포넌트 크기 시스템 + +### 4.1 열 단위 (Span) 크기 + +픽셀 대신 **열 단위(span)** 로 크기 지정: + +| 크기 | 태블릿 가로 (12col) | 태블릿 세로 (8col) | 모바일 (4col) | +|------|--------------------|--------------------|---------------| +| XS | 1 span | 1 span | 1 span | +| S | 2 span | 2 span | 2 span | +| M | 3 span | 2 span | 2 span | +| L | 4 span | 4 span | 4 span (full) | +| XL | 6 span | 4 span | 4 span (full) | +| Full | 12 span | 8 span | 4 span | + +### 4.2 높이 프리셋 + +| 프리셋 | 픽셀값 | 용도 | +|--------|--------|------| +| `xs` | 32px | 배지, 아이콘 버튼 | +| `sm` | 48px | 일반 버튼, 입력 필드 | +| `md` | 80px | 카드, 인디케이터 | +| `lg` | 120px | 큰 카드, 리스트 아이템 | +| `xl` | 200px | 대형 영역 | +| `auto` | 내용 기반 | 가변 높이 | + +### 4.3 컴포넌트별 기본값 + +| 컴포넌트 | 태블릿 span | 모바일 span | 높이 | 비고 | +|----------|------------|-------------|------|------| +| pop-field | 3 (M) | 2 (S) | sm | 입력/표시 | +| pop-button | 2 (S) | 2 (S) | sm | 액션 버튼 | +| pop-list | 12 (Full) | 4 (Full) | auto | 데이터 목록 | +| pop-indicator | 3 (M) | 2 (S) | md | KPI 표시 | +| pop-scanner | 6 (XL) | 4 (Full) | lg | 스캔 영역 | +| pop-numpad | 6 (XL) | 4 (Full) | auto | 숫자 패드 | +| pop-spacer | 1 (XS) | 1 (XS) | - | 빈 공간 | +| pop-break | Full | Full | 0 | 줄바꿈 | + +--- + +## 5. 반응형 규칙 + +### 5.1 자동 조정 + +설계자가 별도 설정하지 않아도 자동 적용: + +``` +태블릿 가로 (12col): [A:3] [B:3] [C:3] [D:3] → 한 줄 +태블릿 세로 (8col): [A:2] [B:2] [C:2] [D:2] → 한 줄 +모바일 (4col): [A:2] [B:2] → 두 줄 + [C:2] [D:2] +``` + +### 5.2 수동 오버라이드 + +필요시 모드별 설정 가능: + +```typescript +interface ResponsiveOverride { + // 크기 변경 + span?: number; + height?: HeightPreset; + + // 표시/숨김 + hidden?: boolean; + + // 내부 요소 숨김 (컴포넌트별) + hideElements?: string[]; +} +``` + +### 5.3 표시/숨김 예시 + +``` +태블릿: [제품명] [수량] [단가] [합계] [비고] +모바일: [제품명] [수량] [비고] ← 단가, 합계 숨김 +``` + +설정: +```typescript +{ + id: "unit-price", + type: "pop-field", + visibility: { + mobile_portrait: false, + mobile_landscape: false + } +} +``` + +--- + +## 6. 데이터 구조 (제안) + +### 6.1 레이아웃 데이터 (v5 제안) + +```typescript +interface PopLayoutDataV5 { + version: "5.0"; + + // 그리드 설정 (전역) + gridConfig: { + tablet: { columns: 12; gap: 16; padding: 24 }; + mobile: { columns: 4; gap: 8; padding: 16 }; + }; + + // 컴포넌트 목록 (순서대로) + components: PopComponentV5[]; + + // 모드별 오버라이드 (선택) + modeOverrides?: { + [mode: string]: { + gridConfig?: Partial; + componentOverrides?: Record; + }; + }; +} +``` + +### 6.2 컴포넌트 데이터 + +```typescript +interface PopComponentV5 { + id: string; + type: PopComponentType; + + // 크기 (span 단위) + size: { + span: number; // 기본 열 개수 (1~12) + height: HeightPreset; // xs, sm, md, lg, xl, auto + }; + + // 반응형 크기 (선택) + responsiveSize?: { + mobile?: { span?: number; height?: HeightPreset }; + tablet_portrait?: { span?: number; height?: HeightPreset }; + }; + + // 표시/숨김 + visibility?: { + [mode: string]: boolean; + }; + + // 컴포넌트별 설정 + config?: any; + + // 데이터 바인딩 + dataBinding?: any; +} +``` + +--- + +## 7. 현재 v4와의 관계 + +### 7.1 v4 유지 사항 +- Flexbox 기반 렌더링 +- 오버라이드 시스템 +- visibility 속성 + +### 7.2 변경 사항 + +| v4 | v5 (제안) | +|----|-----------| +| `fixedWidth: number` | `span: 1~12` | +| `minWidth`, `maxWidth` | 그리드 기반 자동 계산 | +| 자유 픽셀 | 열 단위 프리셋 | + +### 7.3 마이그레이션 방향 + +``` +v4 fixedWidth: 200px +↓ +v5 span: 3 (태블릿 기준 약 25%) +``` + +--- + +## 8. 구현 우선순위 + +### Phase 1: 프리셋만 적용 (최소 변경) +- [ ] 높이 프리셋 드롭다운 +- [ ] 너비 프리셋 드롭다운 (XS~Full) +- [ ] 기존 Flexbox 렌더링 유지 + +### Phase 2: 그리드 시스템 도입 +- [ ] 브레이크포인트 감지 +- [ ] 그리드 칸 수 자동 변경 +- [ ] span → 픽셀 자동 계산 + +### Phase 3: 반응형 자동화 +- [ ] 모드별 자동 span 변환 +- [ ] 줄바꿈 자동 처리 +- [ ] 오버라이드 최소화 + +--- + +## 9. 참고 자료 + +### 분석 대상 + +| 도구 | 핵심 특징 | 적용 가능 요소 | +|------|----------|---------------| +| **Softr** | 블록 기반, 제약 기반 레이아웃 | 컨테이너 슬롯 방식 | +| **Ant Design** | 24열 그리드, 8px 간격 | 그리드 시스템, 간격 규칙 | +| **Material Design** | 4/8/12열, 반응형 브레이크포인트 | 디바이스별 칸 수 | + +### 핵심 원칙 + +1. **Flexbox는 도구**: 그리드 시스템 안에서 사용 +2. **제약은 자유**: 규칙이 있어야 일관된 디자인 가능 +3. **최소 설정, 최대 효과**: 기본값이 좋으면 오버라이드 불필요 + +--- + +## 10. FAQ + +### Q1: 기존 v4 화면은 어떻게 되나요? +A: 하위 호환 유지. v4 화면은 v4로 계속 동작. + +### Q2: 컴포넌트를 그리드 칸 사이에 배치할 수 있나요? +A: 아니요. 칸 단위로만 배치. 이게 일관성의 핵심. + +### Q3: 그리드 칸 수를 바꿀 수 있나요? +A: 기본값(4/6/8/12) 권장. 필요시 프로젝트 레벨 설정 가능. + +### Q4: Flexbox와 Grid 중 뭘 쓰나요? +A: 둘 다. Grid로 칸 나누고, Flexbox로 칸 안에서 정렬. + +--- + +*이 문서는 계획 단계이며, 실제 구현 시 수정될 수 있습니다.* +*최종 업데이트: 2026-02-05* diff --git a/popdocs/archive/GRID_SYSTEM_PLAN.md b/popdocs/archive/GRID_SYSTEM_PLAN.md new file mode 100644 index 00000000..5ee366b5 --- /dev/null +++ b/popdocs/archive/GRID_SYSTEM_PLAN.md @@ -0,0 +1,480 @@ +# POP 그리드 시스템 도입 계획 + +> 작성일: 2026-02-05 +> 상태: 계획 승인, 구현 대기 + +--- + +## 개요 + +### 목표 +현재 Flexbox 기반 v4 시스템을 **CSS Grid 기반 v5 시스템**으로 전환하여 +4~14인치 화면에서 일관된 배치와 예측 가능한 반응형 레이아웃 구현 + +### 핵심 변경점 + +| 항목 | v4 (현재) | v5 (그리드) | +|------|----------|-------------| +| 배치 방식 | Flexbox 흐름 | **Grid 위치 지정** | +| 크기 단위 | 픽셀 (200px) | **칸 (col, row)** | +| 위치 지정 | 순서대로 자동 | **열/행 좌표** | +| 줄바꿈 | 자동 (넘치면) | **명시적 (row 지정)** | + +--- + +## Phase 구조 + +``` +[Phase 5.1] [Phase 5.2] [Phase 5.3] [Phase 5.4] +그리드 타입 정의 → 그리드 렌더러 → 디자이너 UI → 반응형 자동화 + 1주 1주 1~2주 1주 +``` + +--- + +## Phase 5.1: 그리드 타입 정의 + +### 목표 +v5 레이아웃 데이터 구조 설계 + +### 작업 항목 + +- [ ] `PopLayoutDataV5` 인터페이스 정의 +- [ ] `PopGridConfig` 인터페이스 (그리드 설정) +- [ ] `PopComponentPositionV5` 인터페이스 (위치: col, row, colSpan, rowSpan) +- [ ] `PopSizeConstraintV5` 인터페이스 (칸 기반 크기) +- [ ] 브레이크포인트 상수 정의 +- [ ] `createEmptyPopLayoutV5()` 생성 함수 +- [ ] `isV5Layout()` 타입 가드 + +### 데이터 구조 설계 + +```typescript +// v5 레이아웃 +interface PopLayoutDataV5 { + version: "pop-5.0"; + + // 그리드 설정 + gridConfig: PopGridConfig; + + // 컴포넌트 목록 + components: Record; + + // 모드별 오버라이드 + overrides?: { + mobile_portrait?: PopModeOverrideV5; + mobile_landscape?: PopModeOverrideV5; + tablet_portrait?: PopModeOverrideV5; + }; + + // 기존 호환 + dataFlow: PopDataFlow; + settings: PopGlobalSettingsV5; +} + +// 그리드 설정 +interface PopGridConfig { + // 모드별 칸 수 + columns: { + tablet_landscape: 12; // 기본 (10~14인치) + tablet_portrait: 8; // 8~10인치 세로 + mobile_landscape: 6; // 6~8인치 가로 + mobile_portrait: 4; // 4~6인치 세로 + }; + + // 행 높이 (px) - 1행의 기본 높이 + rowHeight: number; // 기본 48px + + // 간격 + gap: number; // 기본 8px + padding: number; // 기본 16px +} + +// 컴포넌트 정의 +interface PopComponentDefinitionV5 { + id: string; + type: PopComponentType; + label?: string; + + // 위치 (열/행 좌표) - 기본 모드(태블릿 가로) 기준 + position: { + col: number; // 시작 열 (1부터) + row: number; // 시작 행 (1부터) + colSpan: number; // 차지할 열 수 (1~12) + rowSpan: number; // 차지할 행 수 (1~) + }; + + // 모드별 표시/숨김 + visibility?: { + tablet_landscape?: boolean; + tablet_portrait?: boolean; + mobile_landscape?: boolean; + mobile_portrait?: boolean; + }; + + // 기존 속성 + dataBinding?: PopDataBinding; + config?: PopComponentConfig; +} +``` + +### 브레이크포인트 정의 + +```typescript +// 브레이크포인트 상수 +const GRID_BREAKPOINTS = { + // 4~6인치 모바일 세로 + mobile_portrait: { + maxWidth: 599, + columns: 4, + rowHeight: 40, + gap: 8, + padding: 12, + }, + + // 6~8인치 모바일 가로 / 작은 태블릿 + mobile_landscape: { + minWidth: 600, + maxWidth: 839, + columns: 6, + rowHeight: 44, + gap: 8, + padding: 16, + }, + + // 8~10인치 태블릿 세로 + tablet_portrait: { + minWidth: 840, + maxWidth: 1023, + columns: 8, + rowHeight: 48, + gap: 12, + padding: 16, + }, + + // 10~14인치 태블릿 가로 (기본) + tablet_landscape: { + minWidth: 1024, + columns: 12, + rowHeight: 48, + gap: 16, + padding: 24, + }, +} as const; +``` + +### 산출물 +- `frontend/components/pop/designer/types/pop-layout-v5.ts` + +--- + +## Phase 5.2: 그리드 렌더러 + +### 목표 +CSS Grid 기반 렌더러 구현 + +### 작업 항목 + +- [ ] `PopGridRenderer.tsx` 생성 +- [ ] CSS Grid 스타일 계산 로직 +- [ ] 브레이크포인트 감지 및 칸 수 자동 변경 +- [ ] 컴포넌트 위치 렌더링 (grid-column, grid-row) +- [ ] 모드별 자동 위치 재계산 (12칸→4칸 변환) +- [ ] visibility 처리 +- [ ] 기존 PopFlexRenderer와 공존 + +### 렌더링 로직 + +```typescript +// CSS Grid 스타일 생성 +function calculateGridStyle(config: PopGridConfig, mode: string): React.CSSProperties { + const columns = config.columns[mode]; + const { rowHeight, gap, padding } = config; + + return { + display: 'grid', + gridTemplateColumns: `repeat(${columns}, 1fr)`, + gridAutoRows: `${rowHeight}px`, + gap: `${gap}px`, + padding: `${padding}px`, + }; +} + +// 컴포넌트 위치 스타일 +function calculatePositionStyle( + position: PopComponentPositionV5['position'], + sourceColumns: number, // 원본 모드 칸 수 (12) + targetColumns: number // 현재 모드 칸 수 (4) +): React.CSSProperties { + // 12칸 → 4칸 변환 예시 + // col: 7, colSpan: 3 → col: 3, colSpan: 1 + const ratio = targetColumns / sourceColumns; + const newCol = Math.max(1, Math.ceil(position.col * ratio)); + const newColSpan = Math.max(1, Math.round(position.colSpan * ratio)); + + return { + gridColumn: `${newCol} / span ${Math.min(newColSpan, targetColumns - newCol + 1)}`, + gridRow: `${position.row} / span ${position.rowSpan}`, + }; +} +``` + +### 산출물 +- `frontend/components/pop/designer/renderers/PopGridRenderer.tsx` + +--- + +## Phase 5.3: 디자이너 UI + +### 목표 +그리드 기반 편집 UI 구현 + +### 작업 항목 + +- [ ] `PopCanvasV5.tsx` 생성 (그리드 캔버스) +- [ ] 그리드 배경 표시 (바둑판 모양) +- [ ] 컴포넌트 드래그 배치 (칸에 스냅) +- [ ] 컴포넌트 리사이즈 (칸 단위) +- [ ] 위치 편집 패널 (col, row, colSpan, rowSpan) +- [ ] 모드 전환 시 그리드 칸 수 변경 표시 +- [ ] v4/v5 자동 판별 및 전환 + +### UI 구조 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ← 목록 화면명 *변경됨 [↶][↷] 그리드 레이아웃 (v5) [저장] │ +├─────────────────────────────────────────────────────────────────┤ +│ 미리보기: [모바일↕ 4칸] [모바일↔ 6칸] [태블릿↕ 8칸] [태블릿↔ 12칸] │ +├────────────┬────────────────────────────────────┬───────────────┤ +│ │ 1 2 3 4 5 6 ... 12 │ │ +│ 컴포넌트 │ ┌───────────┬───────────┐ │ 위치 │ +│ │1│ A │ B │ │ 열: [1-12] │ +│ 필드 │ ├───────────┴───────────┤ │ 행: [1-99] │ +│ 버튼 │2│ C │ │ 너비: [1-12]│ +│ 리스트 │ ├───────────┬───────────┤ │ 높이: [1-10]│ +│ 인디케이터 │3│ D │ E │ │ │ +│ ... │ └───────────┴───────────┘ │ 표시 설정 │ +│ │ │ [x] 태블릿↔ │ +│ │ (그리드 배경 표시) │ [x] 모바일↕ │ +└────────────┴────────────────────────────────────┴───────────────┘ +``` + +### 드래그 앤 드롭 로직 + +```typescript +// 마우스 위치 → 그리드 좌표 변환 +function mouseToGridPosition( + mouseX: number, + mouseY: number, + gridConfig: PopGridConfig, + canvasRect: DOMRect +): { col: number; row: number } { + const { columns, rowHeight, gap, padding } = gridConfig; + + // 캔버스 내 상대 위치 + const relX = mouseX - canvasRect.left - padding; + const relY = mouseY - canvasRect.top - padding; + + // 칸 너비 계산 + const totalGap = gap * (columns - 1); + const colWidth = (canvasRect.width - padding * 2 - totalGap) / columns; + + // 그리드 좌표 계산 + const col = Math.max(1, Math.min(columns, Math.floor(relX / (colWidth + gap)) + 1)); + const row = Math.max(1, Math.floor(relY / (rowHeight + gap)) + 1); + + return { col, row }; +} +``` + +### 산출물 +- `frontend/components/pop/designer/PopCanvasV5.tsx` +- `frontend/components/pop/designer/panels/ComponentEditorPanelV5.tsx` + +--- + +## Phase 5.4: 반응형 자동화 + +### 목표 +모드 전환 시 자동 레이아웃 조정 + +### 작업 항목 + +- [ ] 12칸 → 4칸 자동 변환 알고리즘 +- [ ] 겹침 감지 및 자동 재배치 +- [ ] 모드별 오버라이드 저장 +- [ ] "자동 배치" vs "수동 고정" 선택 +- [ ] 변환 미리보기 + +### 자동 변환 알고리즘 + +```typescript +// 12칸 → 4칸 변환 전략 +function convertLayoutToMode( + components: PopComponentDefinitionV5[], + sourceMode: 'tablet_landscape', // 12칸 + targetMode: 'mobile_portrait' // 4칸 +): PopComponentDefinitionV5[] { + const sourceColumns = 12; + const targetColumns = 4; + const ratio = targetColumns / sourceColumns; // 0.333 + + // 1. 각 컴포넌트 위치 변환 + const converted = components.map(comp => { + const newCol = Math.max(1, Math.ceil(comp.position.col * ratio)); + const newColSpan = Math.max(1, Math.round(comp.position.colSpan * ratio)); + + return { + ...comp, + position: { + ...comp.position, + col: newCol, + colSpan: Math.min(newColSpan, targetColumns), + }, + }; + }); + + // 2. 겹침 감지 및 해결 + return resolveOverlaps(converted, targetColumns); +} + +// 겹침 해결 (아래로 밀기) +function resolveOverlaps( + components: PopComponentDefinitionV5[], + columns: number +): PopComponentDefinitionV5[] { + // 행 단위로 그리드 점유 상태 추적 + const grid: boolean[][] = []; + + // row 순서대로 처리 + const sorted = [...components].sort((a, b) => + a.position.row - b.position.row || a.position.col - b.position.col + ); + + return sorted.map(comp => { + let { row, col, colSpan, rowSpan } = comp.position; + + // 배치 가능한 위치 찾기 + while (isOccupied(grid, row, col, colSpan, rowSpan, columns)) { + row++; // 아래로 이동 + } + + // 그리드에 표시 + markOccupied(grid, row, col, colSpan, rowSpan); + + return { + ...comp, + position: { row, col, colSpan, rowSpan }, + }; + }); +} +``` + +### 산출물 +- `frontend/components/pop/designer/utils/gridLayoutUtils.ts` + +--- + +## 마이그레이션 전략 + +### v4 → v5 변환 + +```typescript +function migrateV4ToV5(layoutV4: PopLayoutDataV4): PopLayoutDataV5 { + const componentsList = Object.values(layoutV4.components); + + // Flexbox 순서 → Grid 위치 변환 + let currentRow = 1; + let currentCol = 1; + const columns = 12; + + const componentsV5: Record = {}; + + componentsList.forEach((comp, index) => { + // 기본 크기 추정 (픽셀 → 칸) + const colSpan = Math.max(1, Math.round((comp.size.fixedWidth || 100) / 85)); + const rowSpan = Math.max(1, Math.round((comp.size.fixedHeight || 48) / 48)); + + // 줄바꿈 체크 + if (currentCol + colSpan - 1 > columns) { + currentRow++; + currentCol = 1; + } + + componentsV5[comp.id] = { + ...comp, + position: { + col: currentCol, + row: currentRow, + colSpan, + rowSpan, + }, + }; + + currentCol += colSpan; + }); + + return { + version: "pop-5.0", + gridConfig: { /* 기본값 */ }, + components: componentsV5, + dataFlow: layoutV4.dataFlow, + settings: { /* 변환 */ }, + }; +} +``` + +### 하위 호환 + +| 버전 | 처리 방식 | +|------|----------| +| v1~v2 | v3로 변환 후 v5로 | +| v3 | v5로 직접 변환 | +| v4 | v5로 직접 변환 | +| v5 | 그대로 사용 | + +--- + +## 일정 (예상) + +| Phase | 작업 | 예상 기간 | +|-------|------|----------| +| 5.1 | 타입 정의 | 2~3일 | +| 5.2 | 그리드 렌더러 | 3~5일 | +| 5.3 | 디자이너 UI | 5~7일 | +| 5.4 | 반응형 자동화 | 3~5일 | +| - | 테스트 및 버그 수정 | 2~3일 | +| **총** | | **약 2~3주** | + +--- + +## 리스크 및 대응 + +| 리스크 | 영향 | 대응 | +|--------|------|------| +| 기존 v4 화면 깨짐 | 높음 | 하위 호환 유지, v4 렌더러 보존 | +| 자동 변환 품질 | 중간 | 수동 오버라이드로 보완 | +| 드래그 UX 복잡 | 중간 | 스냅 기능으로 편의성 확보 | +| 성능 저하 | 낮음 | CSS Grid는 네이티브 성능 | + +--- + +## 성공 기준 + +1. **배치 예측 가능**: "2열 3행"이라고 하면 정확히 그 위치에 표시 +2. **일관된 디자인**: 12칸 → 4칸 전환 시 비율 유지 +3. **쉬운 편집**: 드래그로 칸에 스냅되어 배치 +4. **하위 호환**: 기존 v4 화면이 정상 동작 + +--- + +## 관련 문서 + +- [GRID_SYSTEM_DESIGN.md](./GRID_SYSTEM_DESIGN.md) - 그리드 시스템 설계 상세 +- [PLAN.md](./PLAN.md) - 전체 POP 개발 계획 +- [V4_UNIFIED_DESIGN_SPEC.md](./V4_UNIFIED_DESIGN_SPEC.md) - 현재 v4 스펙 + +--- + +*최종 업데이트: 2026-02-05* diff --git a/popdocs/archive/PHASE3_SUMMARY.md b/popdocs/archive/PHASE3_SUMMARY.md new file mode 100644 index 00000000..4c08ae56 --- /dev/null +++ b/popdocs/archive/PHASE3_SUMMARY.md @@ -0,0 +1,518 @@ +# Phase 3 완료 요약 + +**날짜**: 2026-02-04 +**상태**: 완료 ✅ +**버전**: v4.0 Phase 3 + +--- + +## 🎯 달성 목표 + +Phase 2의 배치 고정 기능 이후, 다음 3가지 핵심 기능 추가: + +1. ✅ **모드별 컴포넌트 표시/숨김** (visibility) +2. ✅ **강제 줄바꿈 컴포넌트** (pop-break) +3. ✅ **컴포넌트 오버라이드 병합** (모드별 설정 변경) + +--- + +## 📦 구현 내용 + +### 1. 타입 정의 + +**파일**: `frontend/components/pop/designer/types/pop-layout.ts` + +```typescript +// pop-break 추가 +export type PopComponentType = + | "pop-field" + | "pop-button" + | "pop-list" + | "pop-indicator" + | "pop-scanner" + | "pop-numpad" + | "pop-spacer" + | "pop-break"; // 🆕 + +// visibility 속성 추가 +export interface PopComponentDefinitionV4 { + id: string; + type: PopComponentType; + size: PopSizeConstraintV4; + + visibility?: { + tablet_landscape?: boolean; + tablet_portrait?: boolean; + mobile_landscape?: boolean; + mobile_portrait?: boolean; + }; + + // ... +} + +// 기본 크기 +defaultSizes["pop-break"] = { + width: "fill", // 100% 너비 + height: "fixed", + fixedHeight: 0, // 높이 0 +}; +``` + +--- + +### 2. 렌더러 로직 + +**파일**: `frontend/components/pop/designer/renderers/PopFlexRenderer.tsx` + +#### visibility 체크 +```typescript +const isComponentVisible = (component: PopComponentDefinitionV4): boolean => { + if (!component.visibility) return true; // 기본값: 표시 + const modeVisibility = component.visibility[currentMode]; + return modeVisibility !== false; // undefined도 true로 취급 +}; +``` + +#### 컴포넌트 오버라이드 병합 +```typescript +const getMergedComponent = (baseComponent: PopComponentDefinitionV4) => { + if (currentMode === "tablet_landscape") return baseComponent; + + const override = overrides?.[currentMode]?.components?.[baseComponent.id]; + if (!override) return baseComponent; + + // 깊은 병합 (config, size) + return { + ...baseComponent, + ...override, + size: { ...baseComponent.size, ...override.size }, + config: { ...baseComponent.config, ...override.config }, + }; +}; +``` + +#### pop-break 렌더링 +```typescript +if (mergedComponent.type === "pop-break") { + return ( +
+ {isDesignMode && 줄바꿈} +
+ ); +} +``` + +--- + +### 3. 삭제 함수 개선 + +**파일**: `frontend/components/pop/designer/types/pop-layout.ts` + +```typescript +export const removeComponentFromV4Layout = ( + layout: PopLayoutDataV4, + componentId: string +): PopLayoutDataV4 => { + // 1. components에서 삭제 + const { [componentId]: _, ...remainingComponents } = layout.components; + + // 2. root.children에서 제거 + const newRoot = removeChildFromContainer(layout.root, componentId); + + // 3. 🆕 모든 오버라이드에서 제거 + const newOverrides = cleanupOverridesAfterDelete(layout.overrides, componentId); + + return { + ...layout, + root: newRoot, + components: remainingComponents, + overrides: newOverrides, + }; +}; +``` + +#### 오버라이드 정리 로직 +```typescript +function cleanupOverridesAfterDelete( + overrides: PopLayoutDataV4["overrides"], + componentId: string +) { + // 각 모드별로: + // 1. containers.root.children에서 componentId 제거 + // 2. components[componentId] 제거 + // 3. 빈 오버라이드 자동 삭제 +} +``` + +--- + +### 4. 속성 패널 UI + +**파일**: `frontend/components/pop/designer/panels/ComponentEditorPanelV4.tsx` + +#### "표시" 탭 추가 +```typescript + + 크기 + 설정 + + + 표시 + + 데이터 + +``` + +#### VisibilityForm 컴포넌트 +```typescript +function VisibilityForm({ component, onUpdate }) { + const modes = [ + { key: "tablet_landscape", label: "태블릿 가로" }, + { key: "tablet_portrait", label: "태블릿 세로" }, + { key: "mobile_landscape", label: "모바일 가로" }, + { key: "mobile_portrait", label: "모바일 세로" }, + ]; + + return ( +
+ {modes.map(({ key, label }) => ( + { + onUpdate?.({ + visibility: { + ...component.visibility, + [key]: e.target.checked, + }, + }); + }} + /> + ))} +
+ ); +} +``` + +--- + +### 5. 팔레트 업데이트 + +**파일**: `frontend/components/pop/designer/panels/ComponentPaletteV4.tsx` + +```typescript +const COMPONENT_PALETTE = [ + // ... 기존 컴포넌트들 + { + type: "pop-break", + label: "줄바꿈", + icon: WrapText, + description: "강제 줄바꿈 (flex-basis: 100%)", + }, +]; +``` + +--- + +## 🎨 UI 변경사항 + +### 컴포넌트 팔레트 +``` +컴포넌트 +├─ 필드 +├─ 버튼 +├─ 리스트 +├─ 인디케이터 +├─ 스캐너 +├─ 숫자패드 +├─ 스페이서 +└─ 줄바꿈 🆕 +``` + +### 속성 패널 +``` +┌─────────────────────┐ +│ 탭: [크기][설정] │ +│ [표시📍][데이터] │ +├─────────────────────┤ +│ 모드별 표시 설정 │ +│ ☑ 태블릿 가로 │ +│ ☑ 태블릿 세로 │ +│ ☐ 모바일 가로 (숨김)│ +│ ☑ 모바일 세로 │ +├─────────────────────┤ +│ 반응형 숨김 │ +│ [500] px 이하 숨김 │ +└─────────────────────┘ +``` + +--- + +## 📖 사용 예시 + +### 예시 1: 모바일 전용 버튼 +```typescript +{ + id: "call-button", + type: "pop-button", + label: "전화 걸기", + visibility: { + tablet_landscape: false, // 태블릿: 숨김 + tablet_portrait: false, + mobile_landscape: true, // 모바일: 표시 + mobile_portrait: true, + }, +} +``` + +**결과**: +- 태블릿 화면: "전화 걸기" 버튼 안 보임 +- 모바일 화면: "전화 걸기" 버튼 보임 + +--- + +### 예시 2: 모드별 줄바꿈 +```typescript +레이아웃: [A] [B] [줄바꿈] [C] [D] + +줄바꿈 설정: +{ + id: "break-1", + type: "pop-break", + visibility: { + tablet_landscape: false, // 태블릿: 줄바꿈 숨김 + mobile_portrait: true, // 모바일: 줄바꿈 표시 + } +} +``` + +**결과**: +``` +태블릿 가로 (1024px): +┌───────────────────────────┐ +│ [A] [B] [C] [D] │ ← 한 줄 +└───────────────────────────┘ + +모바일 세로 (375px): +┌─────────────────┐ +│ [A] [B] │ ← 첫 줄 +│ [C] [D] │ ← 둘째 줄 (줄바꿈 적용) +└─────────────────┘ +``` + +--- + +### 예시 3: 리스트 컬럼 수 변경 (확장 가능) +```typescript +// 기본 (태블릿 가로) +{ + id: "product-list", + type: "pop-list", + config: { + columns: 7, // 7개 컬럼 + } +} + +// 오버라이드 (모바일 세로) +overrides: { + mobile_portrait: { + components: { + "product-list": { + config: { + columns: 3, // 3개 컬럼 + } + } + } + } +} +``` + +**결과**: +- 태블릿: 7개 컬럼 표시 +- 모바일: 3개 컬럼 표시 (자동 병합) + +--- + +## 🧪 테스트 시나리오 + +### ✅ 테스트 1: 줄바꿈 기본 동작 +1. 팔레트에서 "줄바꿈" 드래그 +2. 컴포넌트 사이에 드롭 +3. 디자인 모드에서 점선 "줄바꿈" 표시 확인 +4. 미리보기에서 줄바꿈이 안 보이는지 확인 + +### ✅ 테스트 2: 모드별 줄바꿈 +1. 줄바꿈 컴포넌트 추가 +2. "표시" 탭 → 태블릿 모드 체크 해제 +3. 태블릿 가로: 한 줄 +4. 모바일 세로: 두 줄 + +### ✅ 테스트 3: 삭제 시 오버라이드 정리 +1. 모바일 세로에서 배치 고정 +2. 컴포넌트 삭제 +3. 저장 후 로드 +4. DB 확인: overrides에서도 제거되었는지 + +### ✅ 테스트 4: 컴포넌트 숨김 +1. 컴포넌트 선택 +2. "표시" 탭 → 태블릿 모드 체크 해제 +3. 태블릿: 컴포넌트 안 보임 +4. 모바일: 컴포넌트 보임 + +### ✅ 테스트 5: 속성 패널 UI +1. 컴포넌트 선택 +2. "표시" 탭 클릭 +3. 4개 체크박스 확인 +4. 체크 해제 시 "(숨김)" 표시 +5. 저장 후 로드 → 상태 유지 + +--- + +## 📝 수정된 파일 + +### 코드 파일 (5개) +``` +✅ frontend/components/pop/designer/types/pop-layout.ts + - PopComponentType 확장 (pop-break) + - PopComponentDefinitionV4.visibility 추가 + - cleanupOverridesAfterDelete() 추가 + +✅ frontend/components/pop/designer/renderers/PopFlexRenderer.tsx + - isComponentVisible() 추가 + - getMergedComponent() 추가 + - pop-break 렌더링 추가 + - ContainerRenderer props 확장 + +✅ frontend/components/pop/designer/panels/ComponentEditorPanelV4.tsx + - "표시" 탭 추가 + - VisibilityForm 컴포넌트 추가 + - COMPONENT_TYPE_LABELS 업데이트 + +✅ frontend/components/pop/designer/panels/ComponentPaletteV4.tsx + - "줄바꿈" 컴포넌트 추가 + +✅ frontend/components/pop/designer/PopDesigner.tsx + - (기존 Phase 2 변경사항 유지) +``` + +### 문서 파일 (6개) +``` +✅ popdocs/CHANGELOG.md + - Phase 3 완료 기록 + +✅ popdocs/PLAN.md + - Phase 3 체크 완료 + - Phase 4 계획 추가 + +✅ popdocs/V4_UNIFIED_DESIGN_SPEC.md + - Phase 3 섹션 추가 + +✅ popdocs/components-spec.md + - pop-break 상세 스펙 추가 + - Phase 3 업데이트 노트 + +✅ popdocs/README.md + - 현재 상태 업데이트 + - Phase 3 요약 추가 + +✅ popdocs/decisions/002-phase3-visibility-break.md (신규) + - 상세 설계 문서 + +✅ popdocs/PHASE3_SUMMARY.md (신규) + - 이 문서 +``` + +--- + +## 🎓 핵심 개념 + +### Flexbox 줄바꿈 원리 +```css +/* 컨테이너 */ +.container { + display: flex; + flex-direction: row; + flex-wrap: wrap; /* 필수 */ +} + +/* pop-break */ +.pop-break { + flex-basis: 100%; /* 전체 너비 차지 → 다음 요소는 새 줄로 */ + height: 0; /* 실제로는 안 보임 */ +} +``` + +### visibility vs hideBelow +| 속성 | 제어 방식 | 용도 | +|------|----------|------| +| `visibility` | 모드별 명시적 | 특정 모드에서만 표시 (예: 모바일 전용) | +| `hideBelow` | 픽셀 기반 자동 | 화면 너비에 따라 자동 숨김 (예: 500px 이하) | + +**예시**: +```typescript +{ + visibility: { + tablet_landscape: false, // 태블릿 가로: 무조건 숨김 + }, + hideBelow: 500, // 500px 이하: 자동 숨김 (다른 모드에서도) +} +``` + +--- + +## 🚀 다음 단계 + +### Phase 4: 실제 컴포넌트 구현 +``` +우선순위: +1. pop-field (입력/표시 필드) +2. pop-button (액션 버튼) +3. pop-list (데이터 리스트) +4. pop-indicator (KPI 표시) +5. pop-scanner (바코드/QR) +6. pop-numpad (숫자 입력) +``` + +### 추가 개선 사항 +``` +1. 컴포넌트 오버라이드 UI + - 리스트 컬럼 수 조정 UI + - 버튼 스타일 변경 UI + - 필드 표시 형식 변경 UI + +2. "모든 모드에 적용" 기능 + - 한 번에 모든 모드 체크/해제 + +3. 오버라이드 비교 뷰 + - 기본값 vs 오버라이드 차이 시각화 +``` + +--- + +## ✨ 주요 성과 + +1. ✅ **모드별 컴포넌트 제어**: visibility 속성으로 유연한 표시/숨김 +2. ✅ **Flexbox 줄바꿈 해결**: pop-break 컴포넌트로 업계 표준 달성 +3. ✅ **확장 가능한 구조**: 컴포넌트 오버라이드 병합으로 추후 기능 추가 용이 +4. ✅ **데이터 일관성**: 삭제 시 오버라이드 자동 정리로 데이터 무결성 유지 +5. ✅ **직관적인 UI**: 체크박스 기반 visibility 제어 + +--- + +## 📚 참고 문서 + +- [decisions/002-phase3-visibility-break.md](./decisions/002-phase3-visibility-break.md) - 상세 설계 +- [V4_UNIFIED_DESIGN_SPEC.md](./V4_UNIFIED_DESIGN_SPEC.md) - v4 통합 설계 +- [CHANGELOG.md](./CHANGELOG.md) - 변경 이력 +- [PLAN.md](./PLAN.md) - 로드맵 + +--- + +*Phase 3 완료 - 2026-02-04* +*다음: Phase 4 (실제 컴포넌트 구현)* diff --git a/popdocs/archive/POPREADME.md b/popdocs/archive/POPREADME.md new file mode 100644 index 00000000..d1fe8519 --- /dev/null +++ b/popdocs/archive/POPREADME.md @@ -0,0 +1,658 @@ +# 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 화면 시스템 diff --git a/popdocs/archive/POPUPDATE.md b/popdocs/archive/POPUPDATE.md new file mode 100644 index 00000000..836cdb1f --- /dev/null +++ b/popdocs/archive/POPUPDATE.md @@ -0,0 +1,1041 @@ +# 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* diff --git a/popdocs/archive/POP_V4_CONSTRAINT_SYSTEM_PLAN.md b/popdocs/archive/POP_V4_CONSTRAINT_SYSTEM_PLAN.md new file mode 100644 index 00000000..a0331e73 --- /dev/null +++ b/popdocs/archive/POP_V4_CONSTRAINT_SYSTEM_PLAN.md @@ -0,0 +1,760 @@ +# POP v4.0 제약조건 기반 시스템 구현 계획 + +## 1. 현재 시스템 분석 + +### 1.1 현재 구조 (v3.0) + +```typescript +// 4개 모드별 그리드 위치 기반 +interface PopLayoutDataV3 { + version: "pop-3.0"; + layouts: { + tablet_landscape: { componentPositions: Record }; + tablet_portrait: { componentPositions: Record }; + mobile_landscape: { componentPositions: Record }; + mobile_portrait: { componentPositions: Record }; + }; + components: Record; + // ... +} + +interface GridPosition { + col: number; // 1-based + row: number; // 1-based + colSpan: number; + rowSpan: number; +} +``` + +### 1.2 현재 문제점 + +1. **4배 작업량**: 4개 모드 각각 설계 필요 +2. **수동 동기화**: 컴포넌트 추가/삭제 시 4모드 수동 동기화 +3. **그리드 한계**: col/row 기반이라 자동 재배치 불가 +4. **반응형 미흡**: 화면 크기 변화에 자동 적응 불가 +5. **디바이스 차이 무시**: 태블릿/모바일 물리적 크기 차이 고려 안됨 + +--- + +## 2. 새로운 시스템 설계 (v4.0) + +### 2.1 핵심 철학 + +``` +"하나의 레이아웃 설계 → 제약조건 설정 → 모든 화면 자동 적응" +``` + +- **단일 소스**: 1개 레이아웃만 설계 +- **제약조건 기반**: 컴포넌트가 "어떻게 반응할지" 규칙 정의 +- **Flexbox 렌더링**: CSS Grid에서 Flexbox 기반으로 전환 +- **자동 줄바꿈**: 공간 부족 시 자동 재배치 + +### 2.2 새로운 데이터 구조 + +```typescript +// v4.0 레이아웃 +interface PopLayoutDataV4 { + version: "pop-4.0"; + + // 루트 컨테이너 + root: PopContainer; + + // 컴포넌트 정의 (ID → 정의) + components: Record; + + // 데이터 흐름 + dataFlow: PopDataFlow; + + // 전역 설정 + settings: PopGlobalSettingsV4; + + // 메타데이터 + metadata?: PopLayoutMetadata; +} +``` + +### 2.3 컨테이너 (스택) + +```typescript +// 컨테이너: 컴포넌트들을 담는 그룹 +interface PopContainer { + id: string; + type: "stack"; + + // 스택 방향 + direction: "horizontal" | "vertical"; + + // 줄바꿈 허용 + wrap: boolean; + + // 요소 간 간격 + gap: number; + + // 정렬 + alignItems: "start" | "center" | "end" | "stretch"; + justifyContent: "start" | "center" | "end" | "space-between" | "space-around"; + + // 패딩 + padding?: { + top: number; + right: number; + bottom: number; + left: number; + }; + + // 반응형 규칙 (선택) + responsive?: { + // 브레이크포인트 (이 너비 이하에서 적용) + breakpoint: number; + // 변경할 방향 + direction?: "horizontal" | "vertical"; + // 변경할 간격 + gap?: number; + }[]; + + // 자식 요소 (컴포넌트 ID 또는 중첩 컨테이너) + children: (string | PopContainer)[]; +} +``` + +### 2.4 컴포넌트 제약조건 + +```typescript +interface PopComponentDefinitionV4 { + id: string; + type: PopComponentType; + label?: string; + + // ===== 크기 제약 (핵심) ===== + size: { + // 너비 모드 + width: "fixed" | "fill" | "hug"; + // 높이 모드 + height: "fixed" | "fill" | "hug"; + + // 고정 크기 (width/height가 fixed일 때) + fixedWidth?: number; + fixedHeight?: number; + + // 최소/최대 크기 + minWidth?: number; + maxWidth?: number; + minHeight?: number; + maxHeight?: number; + + // 비율 (fill일 때, 같은 컨테이너 내 다른 요소와의 비율) + flexGrow?: number; // 기본 1 + flexShrink?: number; // 기본 1 + }; + + // ===== 정렬 ===== + alignSelf?: "start" | "center" | "end" | "stretch"; + + // ===== 여백 ===== + margin?: { + top: number; + right: number; + bottom: number; + left: number; + }; + + // ===== 모바일 스케일 (선택) ===== + // 모바일에서 컴포넌트를 더 크게 표시 + mobileScale?: number; // 기본 1.0, 예: 1.2 = 20% 더 크게 + + // ===== 기존 속성 ===== + dataBinding?: PopDataBinding; + style?: PopStylePreset; + config?: PopComponentConfig; +} +``` + +### 2.5 크기 모드 설명 + +| 모드 | 설명 | CSS 변환 | +|------|------|----------| +| `fixed` | 고정 크기 (px) | `width: {fixedWidth}px` | +| `fill` | 부모 공간 채우기 | `flex: {flexGrow} {flexShrink} 0` | +| `hug` | 내용에 맞춤 | `flex: 0 0 auto` | + +### 2.6 전역 설정 + +```typescript +interface PopGlobalSettingsV4 { + // 기본 터치 타겟 크기 + touchTargetMin: number; // 48px + + // 모드 (일반/산업현장) + mode: "normal" | "industrial"; + + // 기본 간격 + defaultGap: number; // 8px + + // 기본 패딩 + defaultPadding: number; // 16px + + // 반응형 브레이크포인트 (전역) + breakpoints: { + tablet: number; // 768px + mobile: number; // 480px + }; +} +``` + +--- + +## 3. 디자이너 UI 변경 + +### 3.1 기존 디자이너 vs 새 디자이너 + +``` +기존 (그리드 기반): +┌──────────────────────────────────────────────┐ +│ [태블릿 가로] [태블릿 세로] [모바일 가로] [모바일 세로] │ +│ │ +│ 24x24 그리드에 컴포넌트 드래그 배치 │ +│ │ +└──────────────────────────────────────────────┘ + +새로운 (제약조건 기반): +┌──────────────────────────────────────────────┐ +│ [단일 캔버스] 미리보기: [태블릿▼] │ +│ │ +│ 스택(컨테이너)에 컴포넌트 배치 │ +│ + 우측 패널에서 제약조건 설정 │ +│ │ +└──────────────────────────────────────────────┘ +``` + +### 3.2 새로운 디자이너 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ POP 화면 디자이너 v4 [저장] [미리보기] │ +├────────────────┬────────────────────────┬───────────────────────┤ +│ │ │ │ +│ 컴포넌트 │ 캔버스 │ 속성 패널 │ +│ │ │ │ +│ ▼ 기본 │ ┌──────────────────┐ │ ▼ 선택됨: 입력창 │ +│ [필드] │ │ ┌──────────────┐ │ │ │ +│ [버튼] │ │ │입력창 │ │ │ ▼ 크기 │ +│ [리스트] │ │ └──────────────┘ │ │ 너비: [채우기 ▼] │ +│ [인디케이터] │ │ │ │ 최소: [100] px │ +│ │ │ ┌─────┐ ┌─────┐ │ │ 최대: [없음] │ +│ ▼ 입력 │ │ │버튼1│ │버튼2│ │ │ │ +│ [스캐너] │ │ └─────┘ └─────┘ │ │ 높이: [고정 ▼] │ +│ [숫자패드] │ │ │ │ 값: [48] px │ +│ │ └──────────────────┘ │ │ +│ ───────── │ │ ▼ 정렬 │ +│ │ 미리보기: │ [늘이기 ▼] │ +│ ▼ 레이아웃 │ ┌──────────────────┐ │ │ +│ [스택 (가로)] │ │[태블릿 가로 ▼] │ │ ▼ 여백 │ +│ [스택 (세로)] │ │[768px] │ │ 상[8] 우[0] 하[8] 좌[0]│ +│ │ └──────────────────┘ │ │ +│ │ │ ▼ 반응형 │ +│ │ │ 모바일 스케일: [1.2] │ +│ │ │ │ +└────────────────┴────────────────────────┴───────────────────────┘ +``` + +### 3.3 컨테이너(스택) 편집 + +``` +┌─ 스택 속성 ─────────────────────┐ +│ │ +│ 방향: [가로 ▼] │ +│ 줄바꿈: [허용 ☑] │ +│ 간격: [8] px │ +│ │ +│ 정렬 (가로): [가운데 ▼] │ +│ 정렬 (세로): [늘이기 ▼] │ +│ │ +│ ▼ 반응형 규칙 │ +│ ┌─────────────────────────────┐ │ +│ │ 768px 이하: 세로 방향 │ │ +│ │ [+ 규칙 추가] │ │ +│ └─────────────────────────────┘ │ +│ │ +└─────────────────────────────────┘ +``` + +--- + +## 4. 렌더링 로직 변경 + +### 4.1 기존 렌더링 (CSS Grid) + +```typescript +// v3: CSS Grid 기반 +
+ {componentIds.map(id => ( +
+ +
+ ))} +
+``` + +### 4.2 새로운 렌더링 (Flexbox) + +```typescript +// v4: Flexbox 기반 +function renderContainer(container: PopContainer, components: Record) { + const direction = useResponsiveValue(container, 'direction'); + const gap = useResponsiveValue(container, 'gap'); + + return ( +
+ {container.children.map(child => { + if (typeof child === "string") { + // 컴포넌트 렌더링 + return renderComponent(components[child]); + } else { + // 중첩 컨테이너 렌더링 + return renderContainer(child, components); + } + })} +
+ ); +} + +function renderComponent(component: PopComponentDefinitionV4) { + const { size, margin, mobileScale } = component; + const isMobile = useIsMobile(); + const scale = isMobile && mobileScale ? mobileScale : 1; + + // 크기 계산 + let width: string; + let flex: string; + + if (size.width === "fixed") { + width = `${(size.fixedWidth || 100) * scale}px`; + flex = "0 0 auto"; + } else if (size.width === "fill") { + width = "auto"; + flex = `${size.flexGrow || 1} ${size.flexShrink || 1} 0`; + } else { // hug + width = "auto"; + flex = "0 0 auto"; + } + + return ( +
+ +
+ ); +} +``` + +### 4.3 반응형 훅 + +```typescript +function useResponsiveValue( + container: PopContainer, + property: keyof PopContainer +): T { + const windowWidth = useWindowWidth(); + + // 기본값 + let value = container[property] as T; + + // 반응형 규칙 적용 (작은 브레이크포인트 우선) + if (container.responsive) { + const sortedRules = [...container.responsive].sort((a, b) => b.breakpoint - a.breakpoint); + for (const rule of sortedRules) { + if (windowWidth <= rule.breakpoint && rule[property] !== undefined) { + value = rule[property] as T; + } + } + } + + return value; +} +``` + +--- + +## 5. 구현 단계 + +### Phase 1: 데이터 구조 (1-2일) + +**파일**: `frontend/components/pop/designer/types/pop-layout.ts` + +1. `PopLayoutDataV4` 인터페이스 정의 +2. `PopContainer` 인터페이스 정의 +3. `PopComponentDefinitionV4` 인터페이스 정의 +4. `createEmptyPopLayoutV4()` 함수 +5. `migrateV3ToV4()` 마이그레이션 함수 +6. `ensureV4Layout()` 함수 +7. 타입 가드 함수들 + +### Phase 2: 렌더러 (2-3일) + +**파일**: `frontend/components/pop/designer/renderers/PopLayoutRendererV4.tsx` + +1. `renderContainer()` 함수 +2. `renderComponent()` 함수 +3. `useResponsiveValue()` 훅 +4. `useWindowWidth()` 훅 +5. CSS 스타일 계산 로직 +6. 반응형 브레이크포인트 처리 + +### Phase 3: 디자이너 UI (3-4일) + +**파일**: `frontend/components/pop/designer/PopDesignerV4.tsx` + +1. 캔버스 영역 (드래그 앤 드롭) +2. 컴포넌트 팔레트 (기존 + 스택) +3. 속성 패널 + - 크기 제약 편집 + - 정렬 편집 + - 여백 편집 + - 반응형 규칙 편집 +4. 미리보기 모드 (다양한 화면 크기) +5. 컨테이너(스택) 관리 + - 컨테이너 추가/삭제 + - 컨테이너 설정 편집 + - 컴포넌트 이동 (컨테이너 간) + +### Phase 4: 뷰어 통합 (1-2일) + +**파일**: `frontend/app/(pop)/pop/screens/[screenId]/page.tsx` + +1. v4 레이아웃 감지 및 렌더링 +2. 기존 v3 호환 유지 +3. 반응형 모드 감지 연동 +4. 성능 최적화 + +### Phase 5: 백엔드 수정 (1일) + +**파일**: `backend-node/src/services/screenManagementService.ts` + +1. `saveLayoutPop` - v4 버전 감지 및 저장 +2. `getLayoutPop` - v4 버전 반환 +3. 버전 마이그레이션 로직 + +### Phase 6: 테스트 및 마이그레이션 (2-3일) + +1. 단위 테스트 +2. 통합 테스트 +3. 기존 v3 레이아웃 마이그레이션 도구 +4. 크로스 디바이스 테스트 + +--- + +## 6. 마이그레이션 전략 + +### 6.1 v3 → v4 자동 변환 + +```typescript +function migrateV3ToV4(v3: PopLayoutDataV3): PopLayoutDataV4 { + // 태블릿 가로 모드 기준으로 변환 + const baseLayout = v3.layouts.tablet_landscape; + const componentIds = Object.keys(baseLayout.componentPositions); + + // 컴포넌트를 row, col 순으로 정렬 + const sortedIds = componentIds.sort((a, b) => { + const posA = baseLayout.componentPositions[a]; + const posB = baseLayout.componentPositions[b]; + if (posA.row !== posB.row) return posA.row - posB.row; + return posA.col - posB.col; + }); + + // 같은 row에 있는 컴포넌트들을 가로 스택으로 그룹화 + const rowGroups = groupByRow(sortedIds, baseLayout.componentPositions); + + // 루트 컨테이너 (세로 스택) + const rootContainer: PopContainer = { + id: "root", + type: "stack", + direction: "vertical", + wrap: false, + gap: v3.settings.canvasGrid.gap, + alignItems: "stretch", + justifyContent: "start", + children: [], + }; + + // 각 행을 가로 스택으로 변환 + for (const [row, ids] of rowGroups) { + if (ids.length === 1) { + // 단일 컴포넌트면 직접 추가 + rootContainer.children.push(ids[0]); + } else { + // 여러 컴포넌트면 가로 스택으로 감싸기 + const rowStack: PopContainer = { + id: `row-${row}`, + type: "stack", + direction: "horizontal", + wrap: true, + gap: v3.settings.canvasGrid.gap, + alignItems: "center", + justifyContent: "start", + children: ids, + }; + rootContainer.children.push(rowStack); + } + } + + // 컴포넌트 정의 변환 + const components: Record = {}; + for (const id of componentIds) { + const v3Comp = v3.components[id]; + const pos = baseLayout.componentPositions[id]; + + components[id] = { + ...v3Comp, + size: { + // colSpan을 기반으로 크기 모드 결정 + width: pos.colSpan >= 20 ? "fill" : "fixed", + height: "fixed", + fixedWidth: pos.colSpan * (1024 / 24), // 대략적인 픽셀 변환 + fixedHeight: pos.rowSpan * (768 / 24), + minWidth: 100, + }, + }; + } + + return { + version: "pop-4.0", + root: rootContainer, + components, + dataFlow: v3.dataFlow, + settings: { + touchTargetMin: v3.settings.touchTargetMin, + mode: v3.settings.mode, + defaultGap: v3.settings.canvasGrid.gap, + defaultPadding: 16, + breakpoints: { + tablet: 768, + mobile: 480, + }, + }, + metadata: v3.metadata, + }; +} +``` + +### 6.2 하위 호환 + +- v3 레이아웃은 계속 지원 +- 디자이너에서 v3 → v4 업그레이드 버튼 제공 +- 새로 생성하는 레이아웃은 v4 + +--- + +## 7. 예상 효과 + +### 7.1 사용자 경험 + +| 항목 | 기존 (v3) | 새로운 (v4) | +|------|-----------|-------------| +| 설계 개수 | 4개 | 1개 | +| 작업 시간 | 4배 | 1배 | +| 반응형 | 수동 | 자동 | +| 디바이스 대응 | 각각 설정 | mobileScale | + +### 7.2 개발자 경험 + +| 항목 | 기존 (v3) | 새로운 (v4) | +|------|-----------|-------------| +| 렌더링 | CSS Grid | Flexbox | +| 위치 계산 | col/row | 자동 | +| 반응형 로직 | 4모드 분기 | 브레이크포인트 | +| 유지보수 | 복잡 | 단순 | + +--- + +## 8. 일정 (예상) + +| Phase | 내용 | 기간 | +|-------|------|------| +| 1 | 데이터 구조 | 1-2일 | +| 2 | 렌더러 | 2-3일 | +| 3 | 디자이너 UI | 3-4일 | +| 4 | 뷰어 통합 | 1-2일 | +| 5 | 백엔드 수정 | 1일 | +| 6 | 테스트/마이그레이션 | 2-3일 | +| **총계** | | **10-15일** | + +--- + +## 9. 리스크 및 대응 + +### 9.1 기존 레이아웃 호환성 + +- **리스크**: v3 → v4 자동 변환이 완벽하지 않을 수 있음 +- **대응**: + - 마이그레이션 미리보기 기능 + - 수동 조정 도구 제공 + - v3 유지 옵션 + +### 9.2 학습 곡선 + +- **리스크**: 제약조건 개념이 익숙하지 않을 수 있음 +- **대응**: + - 프리셋 제공 (예: "화면 전체 채우기", "고정 크기") + - 툴팁/도움말 + - 예제 템플릿 + +### 9.3 성능 + +- **리스크**: Flexbox 중첩으로 렌더링 성능 저하 +- **대응**: + - 컨테이너 중첩 깊이 제한 (최대 3-4) + - React.memo 활용 + - 가상화 (리스트 컴포넌트) + +--- + +## 10. 결론 + +v4.0 제약조건 기반 시스템은 업계 표준(Figma, Flutter, SwiftUI)을 따르며, 사용자의 작업량을 75% 줄이고 자동 반응형을 제공합니다. + +구현 후 POP 디자이너는: +- **1개 레이아웃**만 설계 +- **모든 화면 크기**에 자동 적응 +- **모바일 특화 설정** (mobileScale)으로 세밀한 제어 가능 + +--- + +## 11. 추가 설정 (2026-02-03 업데이트) + +### 11.1 확장된 전역 설정 + +```typescript +interface PopGlobalSettingsV4 { + // 기존 + touchTargetMin: number; // 48 (normal) / 60 (industrial) + mode: "normal" | "industrial"; + defaultGap: number; + defaultPadding: number; + breakpoints: { + tablet: number; // 768 + mobile: number; // 480 + }; + + // 신규 추가 + environment: "indoor" | "outdoor"; // 야외면 대비 높임 + + typography: { + body: { min: number; max: number }; // 14-18px + heading: { min: number; max: number }; // 18-28px + caption: { min: number; max: number }; // 12-14px + }; + + contrast: "normal" | "high"; // outdoor면 자동 high +} +``` + +### 11.2 컴포넌트 기본값 프리셋 + +컴포넌트 추가 시 자동 적용되는 안전한 기본값: + +```typescript +const COMPONENT_DEFAULTS = { + "pop-button": { + minWidth: 80, + minHeight: 48, + height: "fixed", + fixedHeight: 48, + }, + "pop-field": { + minWidth: 120, + minHeight: 40, + height: "fixed", + fixedHeight: 48, + }, + "pop-list": { + minHeight: 200, + itemHeight: 48, + }, + // ... +}; +``` + +### 11.3 리스트 반응형 컬럼 + +```typescript +interface PopListConfig { + // 기존 + listType: PopListType; + displayColumns?: string[]; + + // 신규 추가 + responsiveColumns?: { + tablet: string[]; // 전체 컬럼 + mobile: string[]; // 주요 컬럼만 + }; +} +``` + +### 11.4 라벨 배치 자동화 + +```typescript +interface PopContainer { + // 기존 + direction: "horizontal" | "vertical"; + + // 신규 추가 + labelPlacement?: "auto" | "above" | "beside"; + // auto: 모바일 세로=위, 태블릿 가로=옆 +} +``` + +--- + +## 12. 관련 문서 + +- [v4 핵심 규칙 가이드](./V4_CORE_RULES.md) - **3가지 핵심 규칙 (필독)** +- [반응형 디자인 가이드](./RESPONSIVE_DESIGN_GUIDE.md) +- [컴포넌트 로드맵](./COMPONENT_ROADMAP.md) +- [크기 프리셋 가이드](./SIZE_PRESETS.md) +- [컴포넌트 상세 스펙](./components-spec.md) + +--- + +## 13. 현재 상태 (2026-02-03) + +**구현 대기**: 컴포넌트가 아직 없어서 레이아웃 시스템보다 컴포넌트 개발이 선행되어야 함. + +**권장 진행 순서**: +1. 기초 컴포넌트 개발 (PopButton, PopInput 등) +2. 조합 컴포넌트 개발 (PopFormField, PopCard 등) +3. 복합 컴포넌트 개발 (PopDataTable, PopCardList 등) +4. v4 레이아웃 시스템 구현 +5. 디자이너 UI 개발 + +--- + +*최종 업데이트: 2026-02-03* diff --git a/popdocs/archive/PROJECT_ARCHITECTURE.md b/popdocs/archive/PROJECT_ARCHITECTURE.md new file mode 100644 index 00000000..cb3752fd --- /dev/null +++ b/popdocs/archive/PROJECT_ARCHITECTURE.md @@ -0,0 +1,285 @@ +# VEXPLOR (WACE 솔루션) 프로젝트 아키텍처 + +> AI 에이전트 안내: Quick Reference 먼저 확인 후 필요한 섹션만 참조 + +--- + +## Quick Reference + +### 기술 스택 요약 + +| 영역 | 기술 | +|------|------| +| Frontend | Next.js 14, TypeScript, shadcn/ui, Tailwind CSS | +| Backend | Node.js + Express (주력), Java Spring (레거시) | +| Database | PostgreSQL (173개 테이블) | +| 핵심 기능 | 노코드 화면 빌더, 멀티테넌시, 워크플로우 | + +### 디렉토리 맵 + +``` +ERP-node/ +├── frontend/ # Next.js +│ ├── app/ # 라우팅 (main, auth, pop) +│ ├── components/ # UI 컴포넌트 (281개+) +│ └── lib/ # API, 레지스트리 (463개) +├── backend-node/ # Node.js 백엔드 +│ └── src/ +│ ├── controllers/ # 68개 +│ ├── services/ # 78개 +│ └── routes/ # 47개 +├── db/ # 마이그레이션 +└── docs/ # 문서 +``` + +### 핵심 테이블 + +| 분류 | 테이블 | +|------|--------| +| 화면 | screen_definitions, screen_layouts_v2, screen_layouts_pop | +| 메뉴 | menu_info, authority_master | +| 사용자 | user_info, company_mng | +| 플로우 | flow_definition, flow_step | + +--- + +## 1. 프로젝트 개요 + +### 1.1 제품 소개 +- **WACE 솔루션**: PLM(Product Lifecycle Management) + 노코드 화면 빌더 +- **멀티테넌시**: company_code 기반 회사별 데이터 격리 +- **마이그레이션**: JSP에서 Next.js로 완전 전환 + +### 1.2 핵심 기능 +1. **Screen Designer**: 드래그앤드롭 화면 구성 +2. **워크플로우**: 플로우 기반 업무 자동화 +3. **배치 시스템**: 스케줄 기반 작업 자동화 +4. **외부 연동**: DB, REST API 통합 +5. **리포트**: 동적 보고서 생성 +6. **다국어**: i18n 지원 + +--- + +## 2. Frontend 구조 + +### 2.1 라우트 그룹 + +``` +app/ +├── (main)/ # 메인 레이아웃 +│ ├── admin/ # 관리자 기능 +│ │ ├── screenMng/ # 화면 관리 +│ │ ├── systemMng/ # 시스템 관리 +│ │ ├── userMng/ # 사용자 관리 +│ │ └── automaticMng/ # 자동화 관리 +│ ├── screens/[screenId]/ # 동적 화면 뷰어 +│ └── dashboard/[id]/ # 대시보드 뷰어 +├── (auth)/ # 인증 +│ └── login/ +└── (pop)/ # POP 전용 + └── pop/screens/[screenId]/ +``` + +### 2.2 주요 컴포넌트 + +| 폴더 | 파일 수 | 역할 | +|------|---------|------| +| screen/ | 70+ | 화면 디자이너, 위젯 | +| admin/ | 137 | 테이블, 메뉴, 코드 관리 | +| dataflow/ | 101 | 노드 기반 플로우 에디터 | +| dashboard/ | 32 | 대시보드 빌더 | +| v2/ | 20+ | V2 컴포넌트 시스템 | +| pop/ | 26 | POP 전용 컴포넌트 | + +### 2.3 라이브러리 (lib/) + +``` +lib/ +├── api/ # API 클라이언트 (50개+) +│ ├── screen.ts # 화면 API +│ ├── menu.ts # 메뉴 API +│ └── flow.ts # 플로우 API +├── registry/ # 컴포넌트 레지스트리 (463개) +│ ├── DynamicComponentRenderer.tsx +│ └── components/ +├── v2-core/ # Zod 기반 타입 시스템 +└── utils/ # 유틸리티 (30개+) +``` + +--- + +## 3. Backend 구조 + +### 3.1 디렉토리 + +``` +backend-node/src/ +├── controllers/ # 68개 컨트롤러 +├── services/ # 78개 서비스 +├── routes/ # 47개 라우트 +├── middleware/ # 인증, 에러 처리 +├── database/ # DB 연결 +├── types/ # 26개 타입 정의 +└── utils/ # 16개 유틸 +``` + +### 3.2 주요 서비스 + +| 영역 | 서비스 | +|------|--------| +| 화면 | screenManagementService, layoutService | +| 데이터 | dataService, tableManagementService | +| 플로우 | flowDefinitionService, flowExecutionService | +| 배치 | batchService, batchSchedulerService | +| 외부연동 | externalDbConnectionService, externalCallService | + +### 3.3 API 엔드포인트 + +``` +# 화면 관리 +GET /api/screen-management/screens +GET /api/screen-management/screen/:id +POST /api/screen-management/screen +PUT /api/screen-management/screen/:id +GET /api/screen-management/layout-v2/:screenId +POST /api/screen-management/layout-v2/:screenId +GET /api/screen-management/layout-pop/:screenId +POST /api/screen-management/layout-pop/:screenId + +# 데이터 CRUD +GET /api/data/:tableName +POST /api/data/:tableName +PUT /api/data/:tableName/:id +DELETE /api/data/:tableName/:id + +# 인증 +POST /api/auth/login +POST /api/auth/logout +GET /api/auth/me +``` + +--- + +## 4. Database 구조 + +### 4.1 테이블 분류 (173개) + +| 분류 | 개수 | 주요 테이블 | +|------|------|-------------| +| 화면/레이아웃 | 15 | screen_definitions, screen_layouts_v2, screen_layouts_pop | +| 메뉴/권한 | 7 | menu_info, authority_master, rel_menu_auth | +| 사용자/회사 | 6 | user_info, company_mng, dept_info | +| 테이블/컬럼 | 8 | table_type_columns, column_labels | +| 다국어 | 4 | multi_lang_key_master, multi_lang_text | +| 플로우/배치 | 12 | flow_definition, flow_step, batch_configs | +| 외부연동 | 4 | external_db_connections, external_call_configs | +| 리포트 | 5 | report_master, report_layout, report_query | +| 대시보드 | 2 | dashboards, dashboard_elements | +| 컴포넌트 | 6 | component_standards, web_type_standards | +| 비즈니스 | 100+ | item_info, sales_order_mng, inventory_stock | + +### 4.2 화면 관련 테이블 상세 + +```sql +-- 화면 정의 +screen_definitions: screen_id, screen_name, table_name, company_code + +-- 데스크톱 레이아웃 (V2, 현재 사용) +screen_layouts_v2: id, screen_id, components(JSONB), grid_settings + +-- POP 레이아웃 +screen_layouts_pop: id, screen_id, components(JSONB), grid_settings + +-- 화면 그룹 +screen_groups: group_id, group_name, company_code +screen_group_screens: id, group_id, screen_id +``` + +### 4.3 멀티테넌시 + +```sql +-- 모든 테이블에 company_code 필수 +ALTER TABLE example_table ADD COLUMN company_code VARCHAR(20) NOT NULL; + +-- 모든 쿼리에 company_code 필터링 필수 +SELECT * FROM example_table WHERE company_code = $1; + +-- 예외: company_mng (회사 마스터 테이블) +``` + +--- + +## 5. 핵심 기능 상세 + +### 5.1 노코드 Screen Designer + +**아키텍처**: +``` +screen_definitions (화면 정의) + ↓ +screen_layouts_v2 (레이아웃, JSONB) + ↓ +DynamicComponentRenderer (동적 렌더링) + ↓ +registry/components (컴포넌트 라이브러리) +``` + +**컴포넌트 레지스트리**: +- V2 컴포넌트: Input, Select, Table, Button 등 +- 위젯: FlowWidget, CategoryWidget 등 +- 레이아웃: SplitPanel, TabPanel 등 + +### 5.2 워크플로우 (Flow) + +**테이블 구조**: +``` +flow_definition (플로우 정의) + ↓ +flow_step (단계 정의) + ↓ +flow_step_connection (단계 연결) + ↓ +flow_data_status (데이터 상태 추적) +``` + +### 5.3 POP 시스템 + +**별도 레이아웃 테이블**: +- `screen_layouts_pop`: POP 전용 레이아웃 +- 모바일/태블릿 반응형 지원 +- 제조 현장 최적화 컴포넌트 + +--- + +## 6. 개발 환경 + +### 6.1 로컬 개발 + +```bash +# Docker 실행 +docker-compose -f docker-compose.win.yml up -d + +# 프론트엔드: http://localhost:9771 +# 백엔드: http://localhost:8080 +``` + +### 6.2 데이터베이스 + +``` +Host: 39.117.244.52 +Port: 11132 +Database: plm +Username: postgres +``` + +--- + +## 7. 관련 문서 + +- [POPUPDATE.md](../POPUPDATE.md): POP 개발 기록 +- [docs/pop/components-spec.md](pop/components-spec.md): POP 컴포넌트 설계 +- [.cursorrules](../.cursorrules): 개발 가이드라인 + +--- + +*최종 업데이트: 2026-01-29* diff --git a/popdocs/archive/RESPONSIVE_DESIGN_GUIDE.md b/popdocs/archive/RESPONSIVE_DESIGN_GUIDE.md new file mode 100644 index 00000000..05cf044f --- /dev/null +++ b/popdocs/archive/RESPONSIVE_DESIGN_GUIDE.md @@ -0,0 +1,153 @@ +# POP 반응형 디자인 가이드 + +## 쉬운 요약 + +### 핵심 원칙: 3가지만 기억하세요 + +``` +1. 누르는 것 → 크기 고정 (최소 48px) +2. 읽는 것 → 범위 안에서 자동 조절 +3. 담는 것 → 화면에 맞춰 늘어남 +``` + +--- + +## 1. 터치 요소 (고정 크기) + +손가락 크기는 화면이 커져도 변하지 않습니다. + +| 요소 | 일반 | 산업현장(장갑) | +|------|-----|--------------| +| 버튼 | 48px | 60px | +| 아이콘 (누르는 용) | 48px | 60px | +| 체크박스 | 24px (터치영역 48px) | 24px (터치영역 60px) | +| 리스트 한 줄 높이 | 48px | 56px | +| 입력창 높이 | 40px | 48px | + +--- + +## 2. 텍스트 (범위 조절) + +화면 크기에 따라 자동으로 커지거나 작아집니다. + +| 용도 | 최소 | 최대 | +|------|-----|-----| +| 본문 | 14px | 18px | +| 제목 | 18px | 28px | +| 설명 | 12px | 14px | + +**CSS 예시**: +```css +font-size: clamp(14px, 1.5vw, 18px); +``` + +--- + +## 3. 레이아웃 (비율 기반) + +컨테이너는 화면에 맞춰 늘어납니다. + +| 요소 | 방식 | 예시 | +|------|-----|-----| +| 컨테이너 | 100% | 화면 전체 채움 | +| 카드 2열 | 48% + 48% | 화면 반씩 | +| 입력창 너비 | fill | 부모 채움 | +| 여백 | 8/16/24px | 화면 크기별 | + +--- + +## 4. 환경별 설정 + +### 일반 (실내) +- 터치: 48px +- 대비: 4.5:1 +- 폰트: 14-18px + +### 산업현장 (야외/장갑) +- 터치: 60px (+25%) +- 대비: 7:1 이상 +- 폰트: 18-22px (+25%) + +--- + +## 5. 리스트/테이블 반응형 + +화면이 좁아지면 컬럼을 줄입니다. + +``` +태블릿 (넓음) 모바일 (좁음) +┌──────┬──────┬──────┬──────┐ ┌──────┬──────┐ +│품번 │품명 │수량 │상태 │ │품번 │수량 │ +├──────┼──────┼──────┼──────┤ ├──────┼──────┤ +│A001 │나사 │100 │완료 │ │A001 │100 │ +└──────┴──────┴──────┴──────┘ └──────┴──────┘ + ↳ 터치하면 상세보기 +``` + +--- + +## 6. 폼 라벨 배치 + +| 화면 | 라벨 위치 | 이유 | +|------|----------|-----| +| 모바일 세로 | 위 | 입력창 너비 확보 | +| 태블릿 가로 | 옆 | 공간 여유 | + +``` +모바일 태블릿 +┌─────────────────┐ ┌─────────────────────────┐ +│ 이름 │ │ 이름: [입력____________] │ +│ [입력__________]│ └─────────────────────────┘ +└─────────────────┘ +``` + +--- + +## 7. 그림으로 보는 반응형 + +``` +8인치 태블릿 12인치 태블릿 +┌─────────────────┐ ┌───────────────────────┐ +│ [버튼 48px] │ │ [버튼 48px] │ ← 버튼 크기 동일! +│ │ │ │ +│ ┌─────────────┐ │ │ ┌─────────────────┐ │ +│ │ 입력창 │ │ │ │ 입력창 │ │ ← 너비만 늘어남 +│ └─────────────┘ │ │ └─────────────────┘ │ +│ │ │ │ +│ 글자 14px │ │ 글자 18px │ ← 글자만 커짐 +└─────────────────┘ └───────────────────────┘ +``` + +--- + +## 8. 색상 대비 (야외용) + +| 환경 | 최소 대비 | 권장 | +|------|----------|-----| +| 실내 | 4.5:1 | 7:1 | +| 야외 | 7:1 | 10:1+ | + +**좋은 조합**: +- 흰 배경 + 검정 글자 +- 검정 배경 + 흰 글자 +- 노랑 경고 + 검정 글자 + +**피해야 할 조합**: +- 연한 회색 + 밝은 회색 +- 빨강 + 녹색 (색맹 고려) + +--- + +## 체크리스트 + +### 컴포넌트 만들 때 확인 + +- [ ] 버튼/터치 요소 최소 48px인가? +- [ ] 폰트에 clamp() 적용했나? +- [ ] 색상 대비 4.5:1 이상인가? +- [ ] 모바일에서 라벨이 위에 있나? +- [ ] 리스트가 좁은 화면에서 컬럼 줄어드나? + +--- + +*최종 업데이트: 2026-02-03* diff --git a/popdocs/archive/SIZE_PRESETS.md b/popdocs/archive/SIZE_PRESETS.md new file mode 100644 index 00000000..99de0691 --- /dev/null +++ b/popdocs/archive/SIZE_PRESETS.md @@ -0,0 +1,205 @@ +# POP 크기 프리셋 가이드 + +## 컴포넌트별 기본 크기 + +컴포넌트를 만들면 자동으로 적용되는 크기입니다. + +--- + +## 버튼 (PopButton) + +| 사이즈 | 높이 | 최소 너비 | 폰트 | 용도 | +|-------|------|----------|------|-----| +| sm | 32px | 60px | 12px | 보조 버튼 | +| md | 40px | 80px | 14px | 일반 버튼 | +| **lg** | **48px** | **100px** | **16px** | **POP 기본** | +| xl | 56px | 120px | 18px | 주요 액션 | +| industrial | 60px | 140px | 20px | 장갑 착용 | + +```typescript +// POP에서는 lg가 기본 +확인 + +// 산업현장 +작업 완료 +``` + +--- + +## 입력창 (PopInput) + +| 사이즈 | 높이 | 폰트 | 용도 | +|-------|------|------|-----| +| md | 40px | 14px | 일반 | +| **lg** | **48px** | **16px** | **POP 기본** | +| xl | 56px | 18px | 강조 입력 | + +```typescript +// 입력창 너비는 항상 부모 채움 (fill) + +``` + +--- + +## 리스트 행 (PopListItem) + +| 사이즈 | 높이 | 폰트 | 용도 | +|-------|------|------|-----| +| compact | 40px | 14px | 많은 데이터 | +| **normal** | **48px** | **16px** | **POP 기본** | +| spacious | 56px | 18px | 여유로운 | +| industrial | 64px | 20px | 장갑 착용 | + +```typescript + + 작업지시 #1234 + +``` + +--- + +## 아이콘 (PopIcon) + +| 사이즈 | 크기 | 터치 영역 | 용도 | +|-------|-----|----------|-----| +| sm | 16px | 32px | 뱃지 안 | +| md | 20px | 40px | 텍스트 옆 | +| **lg** | **24px** | **48px** | **POP 기본** | +| xl | 32px | 56px | 강조 | + +```typescript +// 아이콘만 있는 버튼 + + +// 텍스트 + 아이콘 +저장 +``` + +--- + +## 카드 (PopCard) + +| 요소 | 크기 | +|------|-----| +| 패딩 | 16px | +| 제목 폰트 | 18px (heading) | +| 본문 폰트 | 16px (body) | +| 모서리 | 8px | +| 최소 높이 | 100px | + +```typescript + + 작업지시 + 내용 + +``` + +--- + +## 숫자패드 (PopNumberPad) + +| 요소 | 크기 | +|------|-----| +| 버튼 크기 | 60px x 60px | +| 버튼 간격 | 8px | +| 전체 너비 | 240px | +| 폰트 | 24px | + +``` +┌─────────────────────┐ +│ [ 123 ] │ ← 디스플레이 48px +├─────┬─────┬─────────┤ +│ 7 │ 8 │ 9 │ ← │ ← 각 버튼 60x60 +├─────┼─────┼─────────┤ +│ 4 │ 5 │ 6 │ C │ +├─────┼─────┼─────────┤ +│ 1 │ 2 │ 3 │ │ +├─────┼─────┼─────│ OK│ +│ 0 │ . │ +- │ │ +└─────┴─────┴─────────┘ +``` + +--- + +## 상태 표시 (PopStatusBox) + +| 사이즈 | 너비 | 높이 | 아이콘 | 용도 | +|-------|-----|------|-------|-----| +| sm | 80px | 60px | 24px | 여러 개 나열 | +| **md** | **120px** | **80px** | **32px** | **POP 기본** | +| lg | 160px | 100px | 40px | 강조 | + +```typescript + +``` + +--- + +## KPI 게이지 (PopKpiGauge) + +| 사이즈 | 너비 | 높이 | 용도 | +|-------|-----|------|-----| +| sm | 120px | 120px | 여러 개 나열 | +| **md** | **180px** | **180px** | **POP 기본** | +| lg | 240px | 240px | 강조 | + +--- + +## 간격 (Gap/Padding) + +| 이름 | 값 | 용도 | +|------|---|-----| +| xs | 4px | 아이콘-텍스트 | +| sm | 8px | 요소 내부 | +| **md** | **16px** | **컴포넌트 간** | +| lg | 24px | 섹션 간 | +| xl | 32px | 영역 구분 | + +--- + +## 반응형 조절 + +화면 크기에 따라 자동 조절되는 값들: + +| 요소 | 8인치 태블릿 | 12인치 태블릿 | +|------|------------|--------------| +| 본문 폰트 | 14px | 18px | +| 제목 폰트 | 18px | 28px | +| 컨테이너 패딩 | 12px | 24px | +| 카드 간격 | 12px | 16px | + +**고정되는 값들 (변하지 않음)**: +- 버튼 높이: 48px +- 입력창 높이: 48px +- 리스트 행 높이: 48px +- 터치 최소 영역: 48px + +--- + +## 적용 예시 + +```typescript +// 컴포넌트 내부에서 자동 적용 +function PopButton({ size = "lg", ...props }) { + const sizeStyles = { + sm: { height: 32, minWidth: 60, fontSize: 12 }, + md: { height: 40, minWidth: 80, fontSize: 14 }, + lg: { height: 48, minWidth: 100, fontSize: 16 }, // POP 기본 + xl: { height: 56, minWidth: 120, fontSize: 18 }, + industrial: { height: 60, minWidth: 140, fontSize: 20 }, + }; + + return ( +