feat(pop-dashboard): 4가지 아이템 모드 완성 - 설정 UI 추가 및 버그 수정

설정 패널 (PopDashboardConfig):
- groupBy(X축 분류) Combobox 설정 UI 추가
- 차트 xAxisColumn/yAxisColumn 입력 UI 추가
- 통계 카드 카테고리 추가/삭제/편집 인라인 에디터 추가
- 대상 컬럼 Select를 Combobox(검색 가능)로 개선

데이터 처리 버그 수정 (PopDashboardComponent):
- 차트: groupBy 있을 때 xAxisColumn 자동 보정 로직 추가
- 통계 카드: 카테고리별 필터 실제 적용 (기존: 모든 카테고리에 rows.length 동일 입력)
- useCallback 의존성 안정화 (visibleItemIds 문자열 키 사용)
- refreshInterval 최소 5초 강제

데이터 fetcher 방어 로직 (dataFetcher.ts):
- validateDataSourceConfig() 추가: 설정 미완료 시 SQL 전송 차단
- 빈 필터/불완전 조인 건너뜀 처리
- COUNT 컬럼 미선택 시 COUNT(*) 자동 처리
- fetchTableColumns() 이중 폴백 (tableManagementApi -> dashboardApi)

아이템 UI 개선:
- KPI/차트/게이지/통계 카드 패딩 및 폰트 크기 조정
- 작은 셀에서도 라벨/단위/증감율 표시되도록 hidden 제거

기타:
- GridMode MIN_CELL_WIDTH 160 -> 80 축소
- PLAN.MD: 대시보드 4가지 아이템 모드 완성 계획으로 갱신
- STATUS.md: 프로젝트 상태 추적 파일 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SeongHyun Kim
2026-02-10 16:12:29 +09:00
parent dc523d86c3
commit 578cca2687
11 changed files with 1030 additions and 150 deletions
+464 -72
View File
@@ -1,135 +1,527 @@
# 현재 구현 계획: POP 뷰어 스크롤 수정
# 현재 구현 계획: pop-dashboard 4가지 아이템 모드 완성
> **작성일**: 2026-02-09
> **상태**: 계획 완료, 코딩 대기
> **목적**: 뷰어에서 화면 높이를 초과하는 컴포넌트가 잘리지 않고 스크롤 가능하도록 수정
> **작성일**: 2026-02-10
> **상태**: 코딩 완료 (방어 로직 패치 포함)
> **목적**: 대시보드 설정 패널의 미구현/버그 4건을 수정하여 KPI카드, 차트, 게이지, 통계카드 모두 실제 데이터로 동작하도록 완성
---
## 1. 문제 요약
설계(디자이너)에서 컴포넌트를 아래로 배치하면 캔버스가 늘어나고 스크롤이 되지만,
뷰어(`/pop/screens/4114`)에서는 화면 높이를 초과하는 컴포넌트가 잘려서 안 보임.
pop-dashboard 컴포넌트의 4가지 아이템 모드 중 **설정 UI가 누락**되거나 **데이터 처리 로직에 버그**가 있어 실제 테스트 불가.
**근본 원인**: CSS 컨테이너 구조가 스크롤을 차단
| # | 컨테이너 (라인) | 현재 클래스 | 문제 |
|---|----------------|-------------|------|
| 1 | 최외곽 (185) | `h-screen ... overflow-hidden` | 넘치는 콘텐츠를 잘라냄 |
| 2 | 컨텐츠 영역 (266) | 일반 모드에 `overflow-auto` 없음 | 스크롤 불가 |
| 3 | 백색 배경 (275) | 일반 모드에 `min-h-full` 없음 | 짧은 콘텐츠 시 배경 불완전 |
| # | 문제 | 심각도 | 영향 |
|---|------|--------|------|
| BUG-1 | 차트: groupBy 설정 UI 없음 | 높음 | 차트가 단일 값만 표시, X축 카테고리 분류 불가 |
| BUG-2 | 차트: xAxisColumn 설정 UI 없음 | 높음 | groupBy 결과의 컬럼명과 xKey 불일치로 차트 빈 화면 |
| BUG-3 | 통계 카드: 카테고리 설정 UI 없음 | 높음 | statConfig.categories를 설정할 방법 없음 |
| BUG-4 | 통계 카드: 카테고리별 필터 미적용 | 높음 | 모든 카테고리에 동일 값(rows.length) 입력되는 버그 |
---
## 2. 수정 대상 파일 (1개)
## 2. 수정 대상 파일 (2개)
### `frontend/app/(pop)/pop/screens/[screenId]/page.tsx`
### 파일 A: `frontend/lib/registry/pop-components/pop-dashboard/PopDashboardConfig.tsx`
**변경 유형**: CSS 클래스 문자열 수정 3곳 (새 변수/함수/타입 추가 없음)
**변경 유형**: 설정 UI 추가 3건
#### 변경 1: 라인 185 - 최외곽 컨테이너
#### 변경 A-1: DataSourceEditor에 groupBy 설정 추가 (라인 253~362 부근, 집계 함수 아래)
**현재 코드**:
집계 함수가 선택된 상태에서 "그룹핑 컬럼"을 선택할 수 있는 드롭다운 추가.
**추가할 위치**: `{/* 집계 함수 + 대상 컬럼 */}` 블록 다음, `{/* 자동 새로고침 */}` 블록 이전
**추가할 코드** (약 50줄):
```tsx
{/* 그룹핑 (차트용 X축 분류) */}
{dataSource.aggregation && (
<div>
<Label className="text-xs"> (X축)</Label>
<Popover open={groupByOpen} onOpenChange={setGroupByOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={groupByOpen}
disabled={loadingCols}
className="h-8 w-full justify-between text-xs"
>
{dataSource.aggregation.groupBy?.length
? dataSource.aggregation.groupBy.join(", ")
: "없음 (단일 값)"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
{columns.map((col) => (
<CommandItem
key={col.name}
value={col.name}
onSelect={() => {
const current = dataSource.aggregation?.groupBy ?? [];
const isSelected = current.includes(col.name);
const newGroupBy = isSelected
? current.filter((g) => g !== col.name)
: [...current, col.name];
onChange({
...dataSource,
aggregation: {
...dataSource.aggregation!,
groupBy: newGroupBy.length > 0 ? newGroupBy : undefined,
},
});
setGroupByOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
dataSource.aggregation?.groupBy?.includes(col.name)
? "opacity-100"
: "opacity-0"
)}
/>
<span className="font-medium">{col.name}</span>
<span className="ml-1 text-muted-foreground">({col.type})</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="mt-0.5 text-[10px] text-muted-foreground">
X축
</p>
</div>
)}
```
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
**필요한 state 추가** (DataSourceEditor 내부, 기존 state 옆):
```tsx
const [groupByOpen, setGroupByOpen] = useState(false);
```
#### 변경 A-2: 차트 타입 설정에 xAxisColumn/yAxisColumn 추가 (라인 1187~1212 부근)
**추가할 위치**: `{item.subType === "chart" && (` 블록 내부, 차트 유형 Select 다음
**추가할 코드** (약 30줄):
```tsx
{/* X축 컬럼 */}
<div>
<Label className="text-xs">X축 </Label>
<Input
value={item.chartConfig?.xAxisColumn ?? ""}
onChange={(e) =>
onUpdate({
...item,
chartConfig: {
...item.chartConfig,
chartType: item.chartConfig?.chartType ?? "bar",
xAxisColumn: e.target.value || undefined,
},
})
}
placeholder="groupBy 컬럼명 (비우면 자동)"
className="h-8 text-xs"
/>
<p className="mt-0.5 text-[10px] text-muted-foreground">
. groupBy
</p>
</div>
```
#### 변경 A-3: 통계 카드 타입에 카테고리 설정 UI 추가 (라인 1272 부근, 게이지 설정 다음)
**추가할 위치**: `{item.subType === "gauge" && (` 블록 다음에 새 블록 추가
**추가할 코드** (약 100줄): `StatCategoryEditor` 인라인 블록
```tsx
{item.subType === "stat-card" && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium"> </Label>
<Button
variant="outline"
size="sm"
className="h-6 text-[10px]"
onClick={() => {
const currentCats = item.statConfig?.categories ?? [];
onUpdate({
...item,
statConfig: {
...item.statConfig,
categories: [
...currentCats,
{
label: `카테고리 ${currentCats.length + 1}`,
filter: { column: "", operator: "=", value: "" },
},
],
},
});
}}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{(item.statConfig?.categories ?? []).map((cat, catIdx) => (
<div key={catIdx} className="space-y-1 rounded border p-2">
<div className="flex items-center gap-1">
<Input
value={cat.label}
onChange={(e) => {
const newCats = [...(item.statConfig?.categories ?? [])];
newCats[catIdx] = { ...cat, label: e.target.value };
onUpdate({
...item,
statConfig: { ...item.statConfig, categories: newCats },
});
}}
placeholder="라벨 (예: 수주)"
className="h-6 flex-1 text-xs"
/>
<Input
value={cat.color ?? ""}
onChange={(e) => {
const newCats = [...(item.statConfig?.categories ?? [])];
newCats[catIdx] = { ...cat, color: e.target.value || undefined };
onUpdate({
...item,
statConfig: { ...item.statConfig, categories: newCats },
});
}}
placeholder="#색상코드"
className="h-6 w-20 text-xs"
/>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive"
onClick={() => {
const newCats = (item.statConfig?.categories ?? []).filter(
(_, i) => i !== catIdx
);
onUpdate({
...item,
statConfig: { ...item.statConfig, categories: newCats },
});
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 필터 조건: 컬럼 / 연산자 / 값 */}
<div className="flex items-center gap-1 text-[10px]">
<Input
value={cat.filter.column}
onChange={(e) => {
const newCats = [...(item.statConfig?.categories ?? [])];
newCats[catIdx] = {
...cat,
filter: { ...cat.filter, column: e.target.value },
};
onUpdate({
...item,
statConfig: { ...item.statConfig, categories: newCats },
});
}}
placeholder="컬럼"
className="h-6 w-20 text-[10px]"
/>
<Select
value={cat.filter.operator}
onValueChange={(val) => {
const newCats = [...(item.statConfig?.categories ?? [])];
newCats[catIdx] = {
...cat,
filter: { ...cat.filter, operator: val as FilterOperator },
};
onUpdate({
...item,
statConfig: { ...item.statConfig, categories: newCats },
});
}}
>
<SelectTrigger className="h-6 w-16 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=" className="text-xs">= </SelectItem>
<SelectItem value="!=" className="text-xs">!= </SelectItem>
<SelectItem value="like" className="text-xs">LIKE</SelectItem>
</SelectContent>
</Select>
<Input
value={String(cat.filter.value ?? "")}
onChange={(e) => {
const newCats = [...(item.statConfig?.categories ?? [])];
newCats[catIdx] = {
...cat,
filter: { ...cat.filter, value: e.target.value },
};
onUpdate({
...item,
statConfig: { ...item.statConfig, categories: newCats },
});
}}
placeholder="값"
className="h-6 flex-1 text-[10px]"
/>
</div>
</div>
))}
{(item.statConfig?.categories ?? []).length === 0 && (
<p className="text-[10px] text-muted-foreground">
</p>
)}
</div>
)}
```
---
### 파일 B: `frontend/lib/registry/pop-components/pop-dashboard/PopDashboardComponent.tsx`
**변경 유형**: 데이터 처리 로직 수정 2건
#### 변경 B-1: 차트 xAxisColumn 자동 설정 (라인 276~283 부근)
차트에 groupBy가 있지만 xAxisColumn이 설정되지 않은 경우, 첫 번째 groupBy 컬럼을 자동으로 xAxisColumn에 반영.
**현재 코드** (라인 276~283):
```tsx
case "chart":
return (
<ChartItemComponent
item={item}
rows={itemData.rows}
containerWidth={containerWidth}
/>
);
```
**변경 코드**:
```
<div className="h-screen bg-gray-100 flex flex-col">
```tsx
case "chart": {
// groupBy가 있지만 xAxisColumn이 미설정이면 자동 보정
const chartItem = { ...item };
if (
item.dataSource.aggregation?.groupBy?.length &&
!item.chartConfig?.xAxisColumn
) {
chartItem.chartConfig = {
...chartItem.chartConfig,
chartType: chartItem.chartConfig?.chartType ?? "bar",
xAxisColumn: item.dataSource.aggregation.groupBy[0],
};
}
return (
<ChartItemComponent
item={chartItem}
rows={itemData.rows}
containerWidth={containerWidth}
/>
);
}
```
**변경 내용**: `overflow-hidden` 제거
**이유**: 이 div는 프리뷰 툴바 + 컨텐츠의 flex 컨테이너 역할만 하면 됨. `overflow-hidden`이 자식의 스크롤까지 차단하므로 제거
#### 변경 B-2: 통계 카드 카테고리별 독립 데이터 조회 (라인 286~297)
#### 변경 2: 라인 266 - 컨텐츠 영역
**현재 코드**:
```
<div className={`flex-1 flex flex-col ${isPreviewMode ? "py-4 overflow-auto items-center" : ""}`}>
**현재 코드** (버그):
```tsx
case "stat-card": {
const categoryData: Record<string, number> = {};
if (item.statConfig?.categories) {
for (const cat of item.statConfig.categories) {
categoryData[cat.label] = itemData.rows.length; // BUG: 모든 카테고리에 동일 값
}
}
return (
<StatCardComponent item={item} categoryData={categoryData} />
);
}
```
**변경 코드**:
```
<div className={`flex-1 flex flex-col overflow-auto ${isPreviewMode ? "py-4 items-center" : ""}`}>
```tsx
case "stat-card": {
const categoryData: Record<string, number> = {};
if (item.statConfig?.categories) {
for (const cat of item.statConfig.categories) {
if (cat.filter.column && cat.filter.value) {
// 카테고리 필터로 rows 필터링
const filtered = itemData.rows.filter((row) => {
const cellValue = String(row[cat.filter.column] ?? "");
const filterValue = String(cat.filter.value ?? "");
switch (cat.filter.operator) {
case "=":
return cellValue === filterValue;
case "!=":
return cellValue !== filterValue;
case "like":
return cellValue.toLowerCase().includes(filterValue.toLowerCase());
default:
return cellValue === filterValue;
}
});
categoryData[cat.label] = filtered.length;
} else {
categoryData[cat.label] = itemData.rows.length;
}
}
}
return (
<StatCardComponent item={item} categoryData={categoryData} />
);
}
```
**변경 내용**: `overflow-auto`를 조건문 밖으로 이동 (공통 적용)
**이유**: 프리뷰/일반 모드 모두 스크롤이 필요함
#### 변경 3: 라인 275 - 백색 배경 컨테이너
**현재 코드**:
```
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full"}`}
```
**변경 코드**:
```
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full min-h-full"}`}
```
**변경 내용**: 일반 모드에 `min-h-full` 추가
**이유**: 컴포넌트가 적어 콘텐츠가 짧을 때에도 흰색 배경이 화면 전체를 채우도록 보장
**주의**: 이 방식은 rows에 전체 데이터가 있어야 작동함. 통계 카드의 dataSource에 집계 함수를 설정하지 않고(또는 groupBy를 사용하여) 원시 rows를 가져와야 한다.
---
## 3. 구현 순서 (의존성 기반)
| 순서 | 작업 | 라인 | 의존성 | 상태 |
| 순서 | 작업 | 파일 | 의존성 | 상태 |
|------|------|------|--------|------|
| 1 | 라인 185: `overflow-hidden` 제거 | 185 | 없음 | [x] 완료 |
| 2 | 라인 266: `overflow-auto` 공통 적용 | 266 | 순서 1 | [x] 완료 |
| 3 | 라인 275: 일반 모드 `min-h-full` 추가 | 275 | 순서 2 | [x] 완료 |
| 4 | 린트 검사 | - | 순서 1~3 | [x] 통과 |
| 5 | 브라우저 검증 | - | 순서 4 | [ ] 대기 |
| 1 | A-1: DataSourceEditor에 groupBy UI 추가 | PopDashboardConfig.tsx | 없음 | [x] |
| 2 | A-2: 차트 xAxisColumn 입력 UI 추가 | PopDashboardConfig.tsx | 없음 | [x] |
| 3 | A-3: 통계 카드 카테고리 설정 UI 추가 | PopDashboardConfig.tsx | 없음 | [x] |
| 4 | B-1: 차트 xAxisColumn 자동 보정 로직 | PopDashboardComponent.tsx | 순서 1 | [x] |
| 5 | B-2: 통계 카드 카테고리별 필터 적용 | PopDashboardComponent.tsx | 순서 3 | [x] |
| 6 | 린트 검사 | 전체 | 순서 1~5 | [x] |
| 7 | C-1: SQL 빌더 방어 로직 (빈 컬럼/테이블 차단) | dataFetcher.ts | 없음 | [x] |
| 8 | C-2: refreshInterval 최소값 강제 (5초) | PopDashboardComponent.tsx | 없음 | [x] |
| 9 | 브라우저 테스트 | - | 순서 1~8 | [ ] |
순서 1, 2, 3은 서로 독립이므로 병렬 가능.
순서 4는 순서 1의 groupBy 값이 있어야 의미 있음.
순서 5는 순서 3의 카테고리 설정이 있어야 의미 있음.
순서 7, 8은 백엔드 부하 방지를 위한 방어 패치.
---
## 4. 사전 충돌 검사 결과
**새로 추가할 변수/함수/타입: 없음**
### 새로 추가할 식별자 목록
이번 수정은 기존 Tailwind CSS 클래스 문자열만 변경합니다.
새로운 식별자(변수, 함수, 타입)를 추가하지 않으므로 충돌 검사 대상이 없습니다.
| 식별자 | 타입 | 정의 파일 | 사용 파일 | 충돌 여부 |
|--------|------|-----------|-----------|-----------|
| `groupByOpen` | state (boolean) | PopDashboardConfig.tsx DataSourceEditor 내부 | 동일 함수 내부 | 충돌 없음 |
| `setGroupByOpen` | state setter | PopDashboardConfig.tsx DataSourceEditor 내부 | 동일 함수 내부 | 충돌 없음 |
| `chartItem` | const (DashboardItem) | PopDashboardComponent.tsx renderSingleItem 내부 | 동일 함수 내부 | 충돌 없음 |
**Grep 검색 결과** (전체 pop-dashboard 폴더):
- `groupByOpen`: 0건 - 충돌 없음
- `setGroupByOpen`: 0건 - 충돌 없음
- `groupByColumns`: 0건 - 충돌 없음
- `chartItem`: 0건 - 충돌 없음
- `StatCategoryEditor`: 0건 - 충돌 없음
- `loadCategoryData`: 0건 - 충돌 없음
### 기존 타입/함수 재사용 목록
| 기존 식별자 | 정의 위치 | 이번 수정에서 사용하는 곳 |
|------------|-----------|------------------------|
| `DataSourceConfig.aggregation.groupBy` | types.ts 라인 155 | A-1 UI에서 읽기/쓰기 |
| `ChartItemConfig.xAxisColumn` | types.ts 라인 248 | A-2 UI, B-1 자동 보정 |
| `StatCategory` | types.ts 라인 261 | A-3 카테고리 편집 |
| `StatCardConfig.categories` | types.ts 라인 268 | A-3 UI에서 읽기/쓰기 |
| `FilterOperator` | types.ts (import 이미 존재) | A-3 카테고리 필터 Select |
| `columns` (state) | PopDashboardConfig.tsx DataSourceEditor 내부 | A-1 groupBy 컬럼 목록 |
**사용처 있는데 정의 누락된 항목: 없음**
---
## 5. 에러 함정 경고
### 함정 1: 순서 1만 하고 순서 2를 빼먹으
`overflow-hidden`만 제거하면 콘텐츠가 화면 밖으로 넘쳐 보이지만 스크롤은 안 됨.
부모는 열었지만 자식에 스크롤 속성이 없는 상태.
### 함정 1: 차트에 groupBy만 설정하고 xAxisColumn을 비우
ChartItem은 기본 xKey로 `"name"`을 사용하는데, groupBy 결과 행은 `{status: "수주", value: 79}` 형태.
`name` 키가 없으므로 X축이 빈 채로 렌더링됨.
**B-1의 자동 보정 로직이 필수**. 순서 4를 빠뜨리면 차트가 깨짐.
### 함정 2: 순서 2만 하고 순서 1을 빼먹으
자식에 `overflow-auto`를 넣어도 부모가 `overflow-hidden`으로 잘라내므로 여전히 스크롤 안 됨.
**반드시 순서 1과 2를 함께 적용해야 함.**
### 함정 2: 통계 카드에 집계 함수를 설정하
집계(COUNT 등)가 설정되면 rows에 `[{value: 87}]` 하나만 들어옴.
카테고리별 필터링이 작동하려면 **집계 함수를 "없음"으로 두거나, groupBy를 설정**해야 개별 행이 rows에 포함됨.
통계 카드에서는 **집계를 사용하지 않는 것이 올바른 사용법**.
설정 가이드 문서에 이 점을 명시해야 함.
### 함정 3: 프리뷰 모드 영향
프리뷰 모드는 이미 자체적으로 `overflow-auto`가 있으므로 이 수정에 영향 없음.
`overflow-auto`가 중복 적용되어도 CSS에서 문제 없음.
### 함정 3: PopDashboardConfig.tsx의 import 누락
현재 `FilterOperator`는 이미 import되어 있음 (라인 54).
`StatCategory`는 직접 사용하지 않고 `item.statConfig.categories` 구조로 접근하므로 import 불필요.
**새로운 import 추가 필요 없음.**
### 함정 4: 통계 카드 카테고리 필터에서 숫자 비교
`String(row[col])` vs `String(filter.value)` 비교이므로, 숫자 컬럼도 문자열로 비교됨.
`"100" === "100"`은 정상 동작하지만, `"100.00" !== "100"`이 될 수 있음.
현재 대부분 컬럼이 varchar이므로 문제없지만, numeric 컬럼 사용 시 주의.
### 함정 5: DataSourceEditor의 columns state 타이밍
`groupByOpen` Popover에서 `columns` 배열을 사용하는데, 테이블 선택 직후 columns가 아직 로딩 중일 수 있음.
기존 코드에서 `loadingCols` 상태로 버튼을 disabled 처리하고 있으므로 문제없음.
---
## 6. 검증 방법
1. `localhost:9771/pop/screens/4114` 접속 (iPhone SE 375px 기준)
2. 화면 아래로 스크롤 가능한지 확인
3. 맨 아래에 이미지(pop-text 5, 6)가 보이는지 확인
4. 프리뷰 모드(`?preview=true`)에서도 기존처럼 정상 동작하는지 확인
5. 컴포넌트가 적은 화면에서 흰색 배경이 화면 전체를 채우는지 확인
### 차트 (BUG-1, BUG-2)
1. 아이템 추가 > "차트" 선택
2. 테이블: `sales_order_mng`, 집계: COUNT, 컬럼: `id`, 그룹핑: `status`
3. 차트 유형: 막대 차트
4. 기대 결과: X축에 "수주", "진행중", "완료" / Y축에 79, 7, 1
### 통계 카드 (BUG-3, BUG-4)
1. 아이템 추가 > "통계 카드" 선택
2. 테이블: `sales_order_mng`, **집계: 없음** (중요!)
3. 카테고리 추가:
- "수주" / status / = / 수주
- "진행중" / status / = / 진행중
- "완료" / status / = / 완료
4. 기대 결과: 수주 79, 진행중 7, 완료 1
---
## 이전 완료 계획 (아카이브)
<details>
<summary>POP 뷰어 스크롤 수정 (완료)</summary>
- [x] 라인 185: overflow-hidden 제거
- [x] 라인 266: overflow-auto 공통 적용
- [x] 라인 275: 일반 모드 min-h-full 추가
- [x] 린트 검사 통과
</details>
<details>
<summary>POP 뷰어 실제 컴포넌트 렌더링 (완료)</summary>
- [x] 뷰어 페이지에 레지스트리 초기화 import 추가
- [x] `renderActualComponent()` 실제 컴포넌트 렌더링으로 교체
- [x] renderActualComponent() 실제 컴포넌트 렌더링으로 교체
- [x] 린트 검사 통과
- 브라우저 검증: 컴포넌트 표시 정상, 스크롤 문제 발견 -> 별도 수정
</details>