549 lines
19 KiB
Markdown
549 lines
19 KiB
Markdown
# 현재 구현 계획: pop-dashboard 4가지 아이템 모드 완성
|
|
|
|
> **작성일**: 2026-02-10
|
|
> **상태**: 코딩 완료 (방어 로직 패치 포함)
|
|
> **목적**: 대시보드 설정 패널의 미구현/버그 4건을 수정하여 KPI카드, 차트, 게이지, 통계카드 모두 실제 데이터로 동작하도록 완성
|
|
|
|
---
|
|
|
|
## 1. 문제 요약
|
|
|
|
pop-dashboard 컴포넌트의 4가지 아이템 모드 중 **설정 UI가 누락**되거나 **데이터 처리 로직에 버그**가 있어 실제 테스트 불가.
|
|
|
|
| # | 문제 | 심각도 | 영향 |
|
|
|---|------|--------|------|
|
|
| BUG-1 | 차트: groupBy 설정 UI 없음 | 높음 | 차트가 단일 값만 표시, X축 카테고리 분류 불가 |
|
|
| BUG-2 | 차트: xAxisColumn 설정 UI 없음 | 높음 | groupBy 결과의 컬럼명과 xKey 불일치로 차트 빈 화면 |
|
|
| BUG-3 | 통계 카드: 카테고리 설정 UI 없음 | 높음 | statConfig.categories를 설정할 방법 없음 |
|
|
| BUG-4 | 통계 카드: 카테고리별 필터 미적용 | 높음 | 모든 카테고리에 동일 값(rows.length) 입력되는 버그 |
|
|
|
|
---
|
|
|
|
## 2. 수정 대상 파일 (2개)
|
|
|
|
### 파일 A: `frontend/lib/registry/pop-components/pop-dashboard/PopDashboardConfig.tsx`
|
|
|
|
**변경 유형**: 설정 UI 추가 3건
|
|
|
|
#### 변경 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>
|
|
)}
|
|
```
|
|
|
|
**필요한 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}
|
|
/>
|
|
);
|
|
```
|
|
|
|
**변경 코드**:
|
|
```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}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 변경 B-2: 통계 카드 카테고리별 독립 데이터 조회 (라인 286~297)
|
|
|
|
**현재 코드** (버그):
|
|
```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} />
|
|
);
|
|
}
|
|
```
|
|
|
|
**변경 코드**:
|
|
```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} />
|
|
);
|
|
}
|
|
```
|
|
|
|
**주의**: 이 방식은 rows에 전체 데이터가 있어야 작동함. 통계 카드의 dataSource에 집계 함수를 설정하지 않고(또는 groupBy를 사용하여) 원시 rows를 가져와야 한다.
|
|
|
|
---
|
|
|
|
## 3. 구현 순서 (의존성 기반)
|
|
|
|
| 순서 | 작업 | 파일 | 의존성 | 상태 |
|
|
|------|------|------|--------|------|
|
|
| 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. 사전 충돌 검사 결과
|
|
|
|
### 새로 추가할 식별자 목록
|
|
|
|
| 식별자 | 타입 | 정의 파일 | 사용 파일 | 충돌 여부 |
|
|
|--------|------|-----------|-----------|-----------|
|
|
| `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: 차트에 groupBy만 설정하고 xAxisColumn을 비우면
|
|
ChartItem은 기본 xKey로 `"name"`을 사용하는데, groupBy 결과 행은 `{status: "수주", value: 79}` 형태.
|
|
`name` 키가 없으므로 X축이 빈 채로 렌더링됨.
|
|
**B-1의 자동 보정 로직이 필수**. 순서 4를 빠뜨리면 차트가 깨짐.
|
|
|
|
### 함정 2: 통계 카드에 집계 함수를 설정하면
|
|
집계(COUNT 등)가 설정되면 rows에 `[{value: 87}]` 하나만 들어옴.
|
|
카테고리별 필터링이 작동하려면 **집계 함수를 "없음"으로 두거나, groupBy를 설정**해야 개별 행이 rows에 포함됨.
|
|
통계 카드에서는 **집계를 사용하지 않는 것이 올바른 사용법**.
|
|
설정 가이드 문서에 이 점을 명시해야 함.
|
|
|
|
### 함정 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. 검증 방법
|
|
|
|
### 차트 (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] 린트 검사 통과
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary>V2/V2 컴포넌트 설정 스키마 정비 (완료)</summary>
|
|
|
|
- [x] 레거시 컴포넌트 스키마 제거
|
|
- [x] V2 컴포넌트 overrides 스키마 정의 (16개)
|
|
- [x] V2 컴포넌트 overrides 스키마 정의 (9개)
|
|
- [x] componentConfig.ts 한 파일에서 통합 관리
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary>화면 복제 기능 개선 (진행 중)</summary>
|
|
|
|
- [완료] DB 구조 개편 (menu_objid 의존성 제거)
|
|
- [완료] 복제 옵션 정리
|
|
- [완료] 화면 간 연결 복제 버그 수정
|
|
- [대기] 화면 간 연결 복제 테스트
|
|
- [대기] 제어관리 복제 테스트
|
|
- [대기] 추가 옵션 복제 테스트
|
|
|
|
</details>
|