# 2026-04-19 대시보드 런타임 버그 수정 + 헤더 버튼 추가

## 배경
사용자 보고: "대시보드 메뉴 클릭해도 아무것도 안 뜬다" / "이전 메뉴(DTG 이력관리, 물류 통합관제, 카테고리) 가 지웠는데도 사이드바에 남아있다" / "404 가 뜬다".

## 원인 진단 타임라인

### 1) 도커 백엔드가 엉뚱한 DB 를 바라보고 있었음
`application.yml` 은 `183.99.177.40:5432/vexplor` 로 커밋됨(`4d39d70f1`).
하지만 `docker/dev/docker-compose.invyone.yml` 의 env 에 **옛 주소 `211.115.91.141:11134/test_dev`** 가 박혀있었고, Spring 은 env 를 yml 보다 우선하므로 컨테이너가 18 시간 동안 옛 DB 바라봄.

"지워진 줄 알았던 DTG/물류/카테고리 메뉴" 는 **옛 DB 의 메뉴 데이터**가 그대로 뜨던 것. invyone 신 DB 에는 없는 데이터.

**조치**:
- compose 에서 `SPRING_DATASOURCE_URL/USERNAME/PASSWORD` env 3줄 **제거** → yml 이 단일 소스
- `docker compose -f docker/dev/docker-compose.invyone.yml up -d` 로 컨테이너 recreate (단순 restart 아님 — env 변경 반영 안 됨)

### 2) 대시보드 조회 쿼리가 옛 URL 포맷을 기대
`insertDashboard` 는 `MENU_URL = "/3"` 식의 **숫자 URL** 로 저장.
하지만 `getDashboardList` / `getDashboardListCnt` 의 WHERE 조건은 **`MENU_URL LIKE '/dashboard/%'`** (옛 포맷).
→ 조회 결과 0 건 → `[menuSeq]/page.tsx` 가 매칭 실패 → `notFound()` → 404

**조치** (`backend-spring/src/main/resources/mapper/dashboard.xml`):
- 조건 3군데 `LIKE '/dashboard/%'` → `~ '^/\d+$'` 로 교체
- 주석도 같이 업데이트

### 3) Spring 이 소스 아닌 build 산출물에서 XML 로드
위 조치 후 재시작해도 여전히 쿼리가 안 바뀜.
이유: bootRun 은 XML 을 `/app/build/resources/main/mapper/dashboard.xml` 에서 읽음. `src/main/resources/` 는 classpath 에 없음.
Syncthing 이 `src` 를 전파해도 `build/resources/main/` 은 옛날 그대로.

**조치**:
```bash
docker exec invyone-backend-spring sh -c 'cd /app && ./gradlew processResources --quiet'
docker restart invyone-backend-spring
```

### 4) getDashboardList SELECT 에 MENU_URL 컬럼 누락
조건은 고쳤는데 여전히 404. 프론트 디버그 화면 출력: `'/3' 에 해당하는 대시보드를 목록(5개)에서 찾지 못했습니다.`
즉 API 가 5 건 돌려주지만 **menu_url 필드가 응답에 없었음**. SELECT 절에서 빠짐.

**조치**: `getDashboardList`, `getDashboardInfo` SELECT 에 `MENU_URL` 추가 → processResources → restart.

### 5) AppLayout 이 `/숫자` 경로를 탭 시스템으로 보냄
AppLayout body 가 `pathname.startsWith('/dashboard')` 등만 children 직접 렌더, 나머지는 `<TabContent />` 렌더.
`/3` 은 이 조건에 안 걸려서 TabContent 가 떴고, 탭 `"screen" | "admin"` 타입만 지원하므로 "열린 탭이 없습니다" 화면.

**조치**: AppLayout 의 조건에 `/^\/\d+$/.test(pathname)` 추가.

### 6) 클라이언트 컴포넌트에서 notFound() 는 디버깅이 어려움
`notFound()` 가 호출되면 단순히 전역 not-found 페이지가 뜨고, 왜 그런지 알 길이 없음. 네트워크 탭 없이는 API 실패인지 매칭 실패인지 구분 불가.

**조치**: `[menuSeq]/page.tsx` 에서 `notFound()` 호출 제거. 대신 **inline 에러 메시지 페이지**로 렌더하고 사유(응답 카운트, HTTP 상태 코드) 를 화면에 직접 표시. 디버그 편의 + 사용자도 상황 알 수 있음.

## 새 기능 — 헤더 버튼 3개 (Light/Dark 토글 왼쪽)

### 전역 상태 (`frontend/stores/dashboardStore.ts`)
```ts
createOpen, openCreate, closeCreate  // 대시보드 생성 모달
libOpen, openLib, closeLib            // 템플릿 라이브러리 모달
```

### 버튼 (`AppLayout.tsx`)
- **대시보드** (`+`) — `openCreate()` → 전역 CreateDashboardModal
- **템플릿 추가** (`+`) — `openLib()`. activeDashboardId 없으면 disabled
- **편집** (edit3) — `toggleEditMode()`. activeDashboardId 없으면 disabled. on 상태 때 primary 배경

### 스타일 (`styles/v5-layout.css`)
`.v5-dash-btn` 클래스 추가 — v5 톤(glass + primary on 상태).

### 템플릿 배치 위치
`handleSelectTemplate` 에서 canvas 정중앙 좌표 계산 + 기존 카드 수 `% 8 * 28px` stagger.

### 빈 상태 UI
- `layout/EmptyDashboard.tsx` (탭 0 개) — "이제 템플릿을 추가합니다" + 중앙 클릭/Enter → openLib + 버튼
- `dash/DashboardEmpty.tsx` (대시보드에 카드 0 개) — 동일 톤

## ButtonComponent 런타임 방어
템플릿 렌더 시 `Objects are not valid as a React child (found: object with keys {name, size, type})` 발생.
`componentConfig.text`, `componentConfig.icon` 이 object 로 들어올 수 있음. `typeof === 'string'` 아니면 각각 `"버튼"` / `null` 로 폴백.

## DB 정리 (사용자 승인 후 hard DELETE)
테스트 대시보드 5 개 (`테스트용`, `테스트`, `123213`, `새 대시보드ㅎㅇ`, `새 대시보드ㅈㅈ`) 완전 삭제.

트랜잭션 순서:
1. `business_rules WHERE dashboard_id IN (...)` (0 건)
2. `dashboard_elements WHERE dashboard_id IN (...)` (0 건)
3. `dashboard_cards WHERE menu_objid IN (...)` (7 건) ← `dashboard_id` 아님, **`menu_objid`**
4. `MENU_INFO WHERE OBJID IN (...) AND MENU_TYPE='1'` (5 건)

남은 대시보드: 0 개.

## 수정된 파일

### backend-spring
- `src/main/resources/mapper/dashboard.xml` — 조회 조건 + SELECT 절

### frontend
- `stores/dashboardStore.ts` — createOpen / libOpen 전역 state
- `components/layout/AppLayout.tsx` — 헤더 3 버튼 + `/숫자` 라우팅 조건 + 전역 CreateDashboardModal
- `components/layout/EmptyDashboard.tsx` — 메시지 + 클릭 핸들러
- `components/dash/DashboardLayout.tsx` — 로컬 libOpen → store. 템플릿 배치 중앙
- `components/dash/DashboardEmpty.tsx` — 메시지 통일
- `app/(main)/[menuSeq]/page.tsx` — notFound → inline 에러
- `lib/registry/components/button/ButtonComponent.tsx` — text/icon 방어
- `styles/v5-layout.css` — `.v5-dash-btn` 추가

### docker
- `docker/dev/docker-compose.invyone.yml` — DATASOURCE env 3 줄 제거

## 다음 번에 기억할 함정

1. **XML 수정 → processResources → restart** 순서 필수 (src 수정만으론 안 됨)
2. **docker compose env 가 yml 을 덮어씀** — 이상 증상 시 러닝 컨테이너 env 확인
3. **클라이언트 컴포넌트에서 notFound() 남발 금지** — inline 에러 렌더가 디버깅 훨씬 편함
4. **dashboard_cards 는 `menu_objid` 컬럼** (dashboard_id 아님)
