ERP-node/popdocs/PROBLEMS.md

575 lines
30 KiB
Markdown

# 문제-해결 색인
> **용도**: "이전에 비슷한 문제 어떻게 해결했어?"
> **검색 팁**: 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<PopComponentType, string>`)에 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<any>` 타입이므로 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 (
<div>
<span>{component.label || typeLabel}</span>
</div>
);
}
```
**원인 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 <ActualComp config={component.config} label={component.label} />;
}
// 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 (
<DraggableComponent ... /> // useDrag 사용
);
}
// 뷰어 모드: 드래그 없는 일반 렌더링
return (
<div className="..." style={positionStyle}>
<ComponentContent ... />
</div>
);
```
### 교훈
> 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
// 변경 전
<Tabs defaultValue="position" className="flex flex-1 flex-col">
<TabsList className="w-full justify-start ...">
<TabsContent value="settings" className="flex-1 overflow-auto p-4">
// 변경 후
<Tabs defaultValue="position" className="flex min-h-0 flex-1 flex-col">
<TabsList className="w-full shrink-0 justify-start ...">
<TabsContent value="settings" className="min-h-0 flex-1 overflow-auto p-4">
```
### 교훈
> 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<PopComponentType, string>` -> `Record<string, string>`으로 변경한 후, `PopComponentType`의 유일한 사용처가 사라졌으나 import가 남아있었음 |
| **해결** | import에서 `PopComponentType` 제거 |
| **날짜** | 2026-02-10 |
| **키워드** | 미사용 import, 타입 변경, PopComponentType |
### 교훈
> 타입을 변경하거나 제거할 때, 해당 타입이 import된 것이라면 변경 후 파일 내 다른 참조가 남아있는지 Grep으로 확인해야 한다.
---
*새 문제-해결 추가 시 해당 카테고리 테이블에 행 추가*