# 문제-해결 색인 > **용도**: "이전에 비슷한 문제 어떻게 해결했어?" > **검색 팁**: 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으로 확인해야 한다. --- *새 문제-해결 추가 시 해당 카테고리 테이블에 행 추가*