575 lines
30 KiB
Markdown
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으로 확인해야 한다.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*새 문제-해결 추가 시 해당 카테고리 테이블에 행 추가*
|