ERP-node/reportdocs/PLAN.md

1154 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 리포트 디자이너 고도화 — 구현 계획서
> **최초 작성**: 2026-02-25 | **최종 업데이트**: 2026-03-04
> **프로젝트**: `/Users/shin/ERP-node`
> **스택**: Next.js 15 + TypeScript (`frontend/`) | Node.js Express (`backend-node/`)
---
## 용어 통일 원칙 (전체 파일 적용)
| 기존 표현 | 변경 표현 | 비고 |
|----------|----------|------|
| 조건부 표시 | **노출 규칙** | UI 레이블·문서 전체 |
| IF … THEN | 섹션 헤더 없이, 자연어 문장 | 우측 패널 빌더 |
| AND / OR | **모든 조건 충족** / **하나라도 충족** | 결합 방식 선택 UI |
| "같음" 등 텍스트 연산자 | **= ≠ > < ≥ ≤ 포함 값있음 값없음** | 조건 행 기호 표시 |
| `ConditionalRule` (타입명) | 유지 — 내부 코드 변경 최소화 | `types/report.ts` |
---
## Phase 완료 현황
| Phase | 이름 | 상태 |
|-------|------|------|
| Phase 1 | 리포트 관리 페이지 (Admin) | ✅ 완료 |
| Phase 2 | 내부 리포트 목록 (컨텍스트 뷰어) | ✅ 완료 |
| Phase 3 | 화면관리 컴포넌트화 (드래그&드롭) | ✅ 완료 |
| Phase 4 | 디자이너 고도화 (RightPanel 분리, 신규 컴포넌트) | ✅ 완료 |
| Phase 5 | 디자이너 UX 전환 (인캔버스 설정 모달) | ✅ 완료 |
| Phase 5-E | 속성 설정 분리 (모달 ↔ 우측 패널) | ✅ 완료 |
| Phase 6 | 테이블 비주얼 쿼리 빌더 | ✅ 완료 |
| Phase 7 | 카드 컴포넌트 고도화 + 디자이너 UX 개선 | 🔄 진행 중 |
| Phase 8 | 노출 규칙 빌더 (전체 컴포넌트 공통) | ⬜ 대기 |
| Phase 9 | 컴포넌트 팔레트 고도화 (테이블 컬럼 팔레트 + querySummary) | ⬜ 대기 |
| Phase 10 | 테이블 설정 UI 리팩터링 (2탭 + D&D + 컬럼 타입별 속성) | ⬜ 대기 |
| Phase 11 | 컴포넌트 설정 모달 디자인 통일화 (섹션 카드 + 필드 높이) | ⬜ 대기 |
---
---
# Phase 10: 테이블 설정 UI 리팩터링
> **목표**: `TableLayoutTabs.tsx`를 3탭→2탭으로 재편하고, 열 순서 D&D, 컬럼 타입별 속성 지원, DB 필드 매핑 확인 섹션을 추가한다.
> **핵심 플로우**: 테이블 선택 → 열 구성 → (다른 페이지에서) 레코드 검색·선택 → 견적서 등 문서 구성
## 탭 구조 변경
| 탭 (AS-IS) | 탭 (TO-BE) | 내용 |
|---|---|---|
| 레이아웃 구성 | **레이아웃 구성** | 컬럼 구성 테이블 (D&D 핸들 + 헤더/유형/데이터/정렬/너비/타입별 속성/집계/삭제) |
| 데이터 연결 | **데이터 연결** | VisualQueryBuilder + DB 필드 매핑 확인 + 푸터 설정 |
| 표시 조건 | (제거) | ConditionalProperties → 우측 패널 분리 예정 |
## 구현 단계
| 단계 | 내용 | 상태 |
|------|------|------|
| 10-A | `types/report.ts` — tableColumns 요소에 `columnType`, `dateFormat` 추가 | ⬜ 대기 |
| 10-B | `TableLayoutTabs.tsx` — SortableColumnRow 교체 + 탭 재구성 | ⬜ 대기 |
## 수정 파일
| 파일 경로 | 변경 내용 | 난이도 |
|---|---|---|
| `frontend/types/report.ts` | tableColumns 요소에 `columnType?`, `dateFormat?` 추가 | 낮음 |
| `frontend/components/report/designer/modals/TableLayoutTabs.tsx` | 전체 리팩터링 (3탭→2탭, D&D, 컬럼타입 분기) | 중간 |
## 신규 이름 목록 (충돌 검사 대상)
- `SortableColumnRow` (TableLayoutTabs.tsx 내부 서브컴포넌트)
- `handleDragEnd` (함수)
- `columnIds` (변수)
- `renderFieldMappingTable` (함수)
- `columnType` (타입 필드: `"text" | "number" | "date" | "amount"`)
- `dateFormat` (타입 필드)
## 주요 기술 결정
- D&D: `@dnd-kit/sortable` (이미 설치됨) — SortableContext + useSortable, listeners를 GripVertical 핸들에만 적용
- VisualQueryBuilder → 데이터 연결 탭으로 이동 (Props 변경 없음)
- columnType 미설정 → 기존 numberFormat 기반 동작 유지 (하위 호환)
- 참조 패턴: `split-panel-layout2/components/SortableColumnItem.tsx` (drag handle 분리)
---
# Phase 7: 카드 컴포넌트 고도화 + 디자이너 UX 개선
> **목표**: 전체 컴포넌트 스타일을 우측 패널로 일원화하고, 카드 컴포넌트의 요소 팔레트를 11종으로 확장하며, 모달에 Draft 기반 저장/취소/미리보기 흐름을 도입한다.
## 핵심 설계 원칙
| 항목 | AS-IS | TO-BE |
|------|-------|-------|
| 스타일 설정 위치 | 모달 스타일 탭 + 카드 내부 스타일 탭 + 우측 패널 (3중 중복) | **우측 패널 전담** ← 전체 컴포넌트 공통 |
| 모달 탭 구성 | `[기능설정][데이터연결][조건부표시][스타일]` | `[기능설정][데이터연결][노출 규칙]` |
| 카드 기능 탭 | `[레이아웃][데이터바인딩][스타일]` | `[레이아웃 구성][데이터 연결]` |
| 모달 저장 방식 | onChange → 즉시 캔버스 반영 (취소 불가) | Draft 상태 → **[미리보기][저장][취소]** |
| 카드 요소 팔레트 | 4종 (헤더/데이터셀/구분선/배지) | **11종** (+이미지/숫자/날짜/링크/상태/빈공간/고정텍스트) |
| 우측 패널 배치 설정 | 모달 스타일 탭에만 존재 | **우측 패널 최상단** (X/Y/W/H) ← 전체 컴포넌트 공통 |
## 구현 순서
```
Step 1 types/report.ts ← 모든 파일의 기반, 최우선 [카드 전용]
Step 2 CardElementPalette.tsx ← CardCanvasEditor가 import [카드 전용]
Step 3 CardCanvasEditor.tsx ← CardElementPalette 의존 [카드 전용]
Step 4 CardPreviewPanel.tsx [신규] ← CardRenderer 재활용 [카드 전용]
Step 5 CardLayoutTabs.tsx ← 스타일 탭 제거 + UX 개선 [카드 전용]
Step 6 ComponentSettingsModal.tsx ← Draft + 버튼 + 스타일 탭 제거
Step 7 CardProperties.tsx ← 스타일 섹션 전면 재설계 [카드 전용]
Step 8 ComponentStylePanel.tsx ← 배치 섹션 + borderRadius 추가 ⚠️ 전체 컴포넌트 공통
Step 9 CardRenderer.tsx ← 신규 스타일 CSS + 신규 요소 렌더러 [카드 전용]
Step 10 StyleTab.tsx ← [삭제] ⚠️ 전체 컴포넌트 공통
```
---
## Step 1 — `frontend/types/report.ts`
### 1-1. CardElementType 확장
```typescript
// 변경 전
export type CardElementType = "header" | "dataCell" | "divider" | "badge";
// 변경 후
export type CardElementType =
| "header" | "dataCell" | "divider" | "badge"
| "image" | "number" | "date" | "link" | "status" | "spacer" | "staticText";
```
### 1-2. 신규 요소 interface 7개 추가
`CardBadgeElement` 정의 바로 아래에 삽입:
```typescript
export interface CardImageElement extends CardElementBase {
type: "image";
columnName?: string;
altText?: string;
objectFit?: "contain" | "cover" | "fill";
height?: number;
}
export interface CardNumberElement extends CardElementBase {
type: "number";
label: string;
columnName?: string;
numberFormat?: "none" | "comma" | "currency";
currencySuffix?: string;
labelFontSize?: number;
labelColor?: string;
valueFontSize?: number;
valueColor?: string;
}
export interface CardDateElement extends CardElementBase {
type: "date";
label: string;
columnName?: string;
dateFormat?: string;
labelFontSize?: number;
labelColor?: string;
valueFontSize?: number;
valueColor?: string;
}
export interface CardLinkElement extends CardElementBase {
type: "link";
label: string;
columnName?: string;
linkText?: string;
openInNewTab?: boolean;
}
export interface CardStatusElement extends CardElementBase {
type: "status";
columnName?: string;
statusMappings?: Array<{ value: string; label: string; color: string }>;
}
export interface CardSpacerElement extends CardElementBase {
type: "spacer";
height?: number;
}
export interface CardStaticTextElement extends CardElementBase {
type: "staticText";
text: string;
fontSize?: number;
color?: string;
fontWeight?: "normal" | "bold";
textAlign?: "left" | "center" | "right";
}
```
### 1-3. CardElement 유니온 타입 확장
```typescript
// 변경 후
export type CardElement =
| CardHeaderElement | CardDataCellElement | CardDividerElement | CardBadgeElement
| CardImageElement | CardNumberElement | CardDateElement | CardLinkElement
| CardStatusElement | CardSpacerElement | CardStaticTextElement;
```
### 1-4. CardLayoutConfig 신규 필드 추가
기존 `dividerColor?: string;` 아래에 추가:
```typescript
accentBorderColor?: string;
accentBorderWidth?: number;
borderRadius?: string;
borderWidth?: number;
headerFontWeight?: "normal" | "bold";
valueFontWeight?: "normal" | "bold";
```
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 2 — `frontend/components/report/designer/modals/CardElementPalette.tsx`
신규 7종 팔레트 항목 추가. 레이아웃: `flex gap-2``grid grid-cols-4 gap-2`.
신규 아이콘 import: `ImageIcon`, `Hash`, `Calendar`, `Link2`, `Circle`, `Minus2`, `FileText`
```typescript
{ type: "image", label: "이미지", icon: ImageIcon },
{ type: "number", label: "숫자/금액", icon: Hash },
{ type: "date", label: "날짜", icon: Calendar },
{ type: "link", label: "링크", icon: Link2 },
{ type: "status", label: "상태", icon: Circle },
{ type: "spacer", label: "빈 공간", icon: Minus2 },
{ type: "staticText", label: "고정 텍스트", icon: FileText },
```
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 3 — `frontend/components/report/designer/modals/CardCanvasEditor.tsx`
**`createDefaultElement` switch에 신규 케이스 추가:**
```typescript
case "image":
return { id, type: "image", colspan: 1, objectFit: "contain", height: 80 } as CardImageElement;
case "number":
return { id, type: "number", label: "금액", colspan: 1 } as CardNumberElement;
case "date":
return { id, type: "date", label: "날짜", colspan: 1, dateFormat: "YYYY-MM-DD" } as CardDateElement;
case "link":
return { id, type: "link", label: "링크", colspan: 1, openInNewTab: true } as CardLinkElement;
case "status":
return { id, type: "status", colspan: 1, statusMappings: [] } as CardStatusElement;
case "spacer":
return { id, type: "spacer", colspan: 1, height: 16 } as CardSpacerElement;
case "staticText":
return { id, type: "staticText", text: "텍스트", colspan: 1, fontSize: 13 } as CardStaticTextElement;
```
`getElementIcon` / `getElementLabel` 신규 케이스 추가
**`CellSettingsPanel` 분기 추가 — 신규 7종 설정 UI:**
- `image`: columnName(드롭다운), objectFit(Select), height(Input)
- `number`: label(Input), columnName(드롭다운), numberFormat(Select), currencySuffix(Input)
- `date`: label(Input), columnName(드롭다운), dateFormat(Input)
- `link`: label(Input), columnName(드롭다운), linkText(Input), openInNewTab(Checkbox)
- `status`: columnName(드롭다운), statusMappings 인라인 편집 (값/라벨/색상 행 추가/삭제)
- `spacer`: height(Input number)
- `staticText`: text(Textarea), fontSize(Input), color(color picker), fontWeight(Select), textAlign(Select)
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 4 — `frontend/components/report/designer/modals/CardPreviewPanel.tsx` [신규]
ComponentSettingsModal 내 Draft 상태의 카드를 실제 렌더러로 미리보여주는 인라인 패널.
```typescript
interface CardPreviewPanelProps {
component: ComponentConfig;
}
export function CardPreviewPanel({ component }: CardPreviewPanelProps) {
const dummyGetQueryResult = useCallback(() => null, []);
return (
<div className="border-t border-dashed bg-gray-50 p-4 shrink-0">
<div className="flex items-center gap-1.5 mb-3 text-xs font-medium text-muted-foreground">
<Eye className="w-3.5 h-3.5" />
미리보기 (저장 상태)
</div>
<p className="text-xs text-muted-foreground mb-2">
실제 데이터는 저장 확인 가능합니다.
</p>
<div
className="border border-gray-200 rounded-lg overflow-hidden mx-auto"
style={{ width: Math.min(component.width, 480), height: Math.min(component.height, 300) }}
>
<CardRenderer component={component} getQueryResult={dummyGetQueryResult} />
</div>
</div>
);
}
```
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 5 — `frontend/components/report/designer/modals/CardLayoutTabs.tsx`
**탭 구조 변경:**
```typescript
// 변경 전
type TabType = "layout" | "binding" | "style";
// 변경 후
type TabType = "layout" | "binding";
```
**제거 대상:** `Palette` 아이콘, `activeTab === "style"` 버튼, `renderStyleTab()` 전체
**텍스트 변경:** "레이아웃" → "레이아웃 구성", "데이터 바인딩" → "데이터 연결"
**카드 모달 구분 배지 추가** (탭 버튼 그룹 위):
```tsx
<div className="flex items-center gap-2 mb-3 px-1 py-2 bg-blue-50 border border-blue-200 rounded-lg">
<CreditCard className="w-3.5 h-3.5 text-blue-600" />
<span className="text-xs font-medium text-blue-700">카드 레이아웃 설정</span>
</div>
```
**`needsBinding` 조건 확장:**
```typescript
// 변경 후
const needsBinding = ["dataCell", "badge", "number", "date", "link", "status", "image"]
.includes(element.type);
```
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 6 — `frontend/components/report/designer/modals/ComponentSettingsModal.tsx`
**제거:** `StyleTab` import, `Palette` 아이콘, 스타일 탭 `TabsTrigger`/`TabsContent`
**추가 import:** `useState`, `useCallback`, `useEffect`, `Eye`, `Button`, `CardPreviewPanel`
**Draft 상태 추가:**
```typescript
const [localDraft, setLocalDraft] = useState<ComponentConfig | null>(null);
const [showPreview, setShowPreview] = useState(false);
useEffect(() => {
if (component) { setLocalDraft(component); setShowPreview(false); }
}, [componentModalTargetId]);
```
**card 타입 DataTab에 Draft 콜백 전달:**
```typescript
case "card":
return (
<CardProperties
component={localDraft ?? component}
section="data"
onConfigChange={(updates) =>
setLocalDraft((prev) => ({ ...(prev ?? component), ...updates }))
}
/>
);
```
**저장/취소 핸들러:**
```typescript
const handleSave = useCallback(() => {
if (component.type === "card" && localDraft) updateComponent(localDraft.id, localDraft);
closeComponentModal();
}, [component, localDraft, updateComponent, closeComponentModal]);
const handleCancel = useCallback(() => {
setLocalDraft(null);
closeComponentModal();
}, [closeComponentModal]);
```
**모달 하단 footer 추가** (`</Tabs>` 아래, `</DialogContent>` 위):
```tsx
{showPreview && component.type === "card" && (
<CardPreviewPanel component={localDraft ?? component} />
)}
<div className="flex items-center justify-between border-t px-6 py-3 shrink-0">
{component.type === "card" ? (
<Button variant="outline" size="sm" onClick={() => setShowPreview((v) => !v)} className="gap-1.5 text-xs">
<Eye className="h-3.5 w-3.5" />
{showPreview ? "미리보기 닫기" : "미리보기"}
</Button>
) : <span />}
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleCancel}>취소</Button>
<Button size="sm" onClick={handleSave}>저장</Button>
</div>
</div>
```
**주의:** `QuerySettingsTab`, `ConditionalSettingsTab`은 Draft 외부 → 기존 즉시 반영 유지.
취소 시 "쿼리/노출 규칙 변경사항은 취소되지 않습니다" 문구 표시.
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 7 — `frontend/components/report/designer/properties/CardProperties.tsx`
### Props 변경
```typescript
interface Props {
component: ComponentConfig;
section?: "style" | "data";
onConfigChange?: (updates: Partial<ComponentConfig>) => void; // Draft용 신규
}
const handleConfigChange = (newConfig: CardLayoutConfig) => {
const updates = { cardLayoutConfig: newConfig };
if (onConfigChange) onConfigChange(updates); // Draft 모드
else updateComponent(component.id, updates); // 즉시 반영 모드
};
```
### `section="style"` 전면 재설계
**① 프리셋 선택** (버튼 3개: 인포 카드 / 컴팩트 카드 / 커스텀)
```typescript
const CARD_STYLE_PRESETS = {
info: {
backgroundColor: "#ffffff",
borderStyle: "solid", borderColor: "#e5e7eb", borderWidth: 1,
accentBorderWidth: 0, borderRadius: "12px",
headerFontWeight: "bold" as const, headerTitleColor: "#111827",
labelColor: "#6b7280",
valueFontWeight: "normal" as const, valueColor: "#111827",
dividerColor: "#3b82f6", dividerThickness: 1,
},
compact: {
backgroundColor: "#eff6ff",
borderStyle: "none", borderWidth: 0,
accentBorderColor: "#3b82f6", accentBorderWidth: 4,
borderRadius: "8px",
headerFontWeight: "normal" as const, headerTitleColor: "#6b7280",
labelColor: "#6b7280",
valueFontWeight: "bold" as const, valueColor: "#111827",
dividerColor: "#e5e7eb", dividerThickness: 1,
},
} as const;
```
**섹션 구성 순서:**
1. 프리셋 선택
2. 카드 외형 (배경색, 모서리, 패딩, 행 간격)
3. 전체 테두리 (스타일, 두께, 색상)
4. 좌측 액센트 보더 (ON/OFF Switch + 두께 + 색상)
5. 헤더 스타일 (폰트 크기, 색상, 굵기)
6. 라벨 스타일 (폰트 크기, 색상)
7. 값 스타일 (폰트 크기, 색상, 굵기)
8. 구분선 스타일 (두께, 색상, 스타일)
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8 — `frontend/components/report/designer/properties/ComponentStylePanel.tsx`
> ⚠️ **전체 컴포넌트 공통 적용** — 어떤 컴포넌트를 선택해도 동일하게 동작.
### ① 위치/크기 섹션 최상단 추가 (기존 "글꼴" 섹션 위)
```tsx
<section className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
<h4 className="text-sm font-semibold text-gray-700">배치</h4>
<div className="grid grid-cols-4 gap-2">
{[
{ label: "X", key: "x", min: undefined },
{ label: "Y", key: "y", min: undefined },
{ label: "너비", key: "width", min: 50 },
{ label: "높이", key: "height", min: 30 },
].map(({ label, key, min }) => (
<div key={key}>
<Label className="text-xs">{label}</Label>
<Input type="number" value={Math.round(component[key as keyof ComponentConfig] as number)}
onChange={(e) => update({ [key]: parseInt(e.target.value) || min || 0 })}
className="h-9 text-sm" />
</div>
))}
</div>
</section>
```
### ② 테두리 섹션에 모서리 반경 추가
```tsx
<div>
<Label className="text-xs">모서리 반경</Label>
<Select
value={String(component.borderRadius || 0)}
onValueChange={(v) => update({ borderRadius: parseInt(v) || 0 })}
>
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
<SelectContent>
{[0, 2, 4, 8, 12].map((v) => (
<SelectItem key={v} value={String(v)}>{v === 0 ? "없음" : `${v}px`}</SelectItem>
))}
</SelectContent>
</Select>
</div>
```
**주의:** `ComponentConfig.borderRadius``number`, `CardLayoutConfig.borderRadius``string`. 혼용 금지.
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 9 — `frontend/components/report/designer/renderers/CardRenderer.tsx`
### ① 카드 컨테이너 신규 CSS 적용
```tsx
style={{
padding: config.padding || "8px",
gap: config.gap || "8px",
backgroundColor: config.backgroundColor || "#ffffff",
borderRadius: config.borderRadius || "0",
border: config.borderStyle !== "none"
? `${config.borderWidth || 1}px ${config.borderStyle || "solid"} ${config.borderColor || "#e5e7eb"}`
: "none",
...(config.accentBorderWidth && config.accentBorderWidth > 0
? { borderLeft: `${config.accentBorderWidth}px solid ${config.accentBorderColor || "#3b82f6"}` }
: {}),
}}
```
### ② 신규 요소 7종 렌더러 추가
```typescript
case "image":
return <CardImageRenderer element={el as CardImageElement} config={config} getQueryResult={getQueryResult} />;
case "number":
return <CardNumberRenderer element={el as CardNumberElement} config={config} getQueryResult={getQueryResult} />;
case "date":
return <CardDateRenderer element={el as CardDateElement} config={config} getQueryResult={getQueryResult} />;
case "link":
return <CardLinkRenderer element={el as CardLinkElement} config={config} getQueryResult={getQueryResult} />;
case "status":
return <CardStatusRenderer element={el as CardStatusElement} config={config} getQueryResult={getQueryResult} />;
case "spacer":
return <div key={el.id} style={{ height: (el as CardSpacerElement).height || 16 }} />;
case "staticText":
return <CardStaticTextRenderer element={el as CardStaticTextElement} />;
```
**신규 렌더러 구현 요약:**
- `CardImageRenderer`: `<img>` 태그, objectFit 적용, columnName으로 값 조회
- `CardNumberRenderer`: DataCell 구조 + numberFormat (none/comma/currency)
- `CardDateRenderer`: DataCell 구조 + dateFormat 문자열 포맷
- `CardLinkRenderer`: `<a>` 태그, openInNewTab → `target="_blank" rel="noopener noreferrer"`
- `CardStatusRenderer`: 색상 dot + 라벨 텍스트, statusMappings 매핑
- `CardStaticTextRenderer`: `<span>` 태그, 직접 스타일 적용
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 10 — `frontend/components/report/designer/modals/StyleTab.tsx` [삭제]
> ⚠️ **전체 컴포넌트 공통 영향** — 모달의 스타일 탭이 모든 컴포넌트에서 사라지고 우측 패널로 통합됨.
```bash
# 사용처 0건 확인 후 삭제
grep -r "StyleTab" frontend/components/report/
rm frontend/components/report/designer/modals/StyleTab.tsx
```
- 삭제 후 `cd frontend && npx tsc --noEmit` 최종 확인
---
## Phase 7 파일 변경 요약
### 수정 파일 (8개)
| 경로 | 핵심 변경 | 범위 |
|------|---------|------|
| `frontend/types/report.ts` | CardElementType +7, 신규 interface 7개, CardLayoutConfig 필드 +6 | 카드 전용 |
| `frontend/components/report/designer/modals/CardElementPalette.tsx` | 팔레트 4→11종, grid 레이아웃 | 카드 전용 |
| `frontend/components/report/designer/modals/CardCanvasEditor.tsx` | createDefaultElement/icon/label/설정패널 +7 케이스 | 카드 전용 |
| `frontend/components/report/designer/modals/CardLayoutTabs.tsx` | 스타일 탭 제거, 용어 변경, 카드 배지, needsBinding 확장 | 카드 전용 |
| `frontend/components/report/designer/modals/ComponentSettingsModal.tsx` | 스타일 탭 제거, Draft 상태, 저장/취소/미리보기 footer | 전체 공통 |
| `frontend/components/report/designer/properties/CardProperties.tsx` | onConfigChange prop, section="style" 전면 재설계 | 카드 전용 |
| `frontend/components/report/designer/properties/ComponentStylePanel.tsx` | 배치(X/Y/W/H) 섹션 + borderRadius | ⚠️ 전체 공통 |
| `frontend/components/report/designer/renderers/CardRenderer.tsx` | accentBorder/borderRadius CSS + 신규 요소 7종 렌더러 | 카드 전용 |
### 신규 파일 (1개)
| 경로 | 역할 |
|------|------|
| `frontend/components/report/designer/modals/CardPreviewPanel.tsx` | Draft 상태 카드 인라인 미리보기 |
### 삭제 파일 (1개)
| 경로 | 이유 | 범위 |
|------|------|------|
| `frontend/components/report/designer/modals/StyleTab.tsx` | 우측 패널로 완전 통합 | ⚠️ 전체 공통 |
## Phase 7 충돌 사전 검사
구현 시작 전 아래 이름들이 현재 코드베이스에 **0건**인지 Grep 확인 필수:
```
CardImageElement, CardNumberElement, CardDateElement, CardLinkElement,
CardStatusElement, CardSpacerElement, CardStaticTextElement,
accentBorderColor, accentBorderWidth, headerFontWeight, valueFontWeight,
CardPreviewPanel, CARD_STYLE_PRESETS
```
## Phase 7 주의사항
1. **exhaustive check**: `createDefaultElement`, `getElementIcon`, `getElementLabel`, `CardElementRenderer` switch 모두 신규 7케이스 추가 필수. Step 1 직후 `npx tsc --noEmit` 실행하여 영향 파일 목록 확인.
2. **Draft 범위**: card 기능 설정 탭에만 적용. `QuerySettingsTab` / `ConditionalSettingsTab`은 기존 즉시 반영 유지.
3. **borderWidth 이름 충돌**: `ComponentConfig.borderWidth`(컴포넌트 테두리)와 `CardLayoutConfig.borderWidth`(카드 컨테이너 테두리)는 별개.
4. **borderRadius 타입 차이**: `ComponentConfig.borderRadius``number`, `CardLayoutConfig.borderRadius``string`. 혼용 금지.
5. **CardPreviewPanel 데이터 미표시**: `getQueryResult → null`이므로 데이터 셀은 "-" 표시. 안내 문구 필수.
6. **각 Step 완료 시 필수**: `cd frontend && npx tsc --noEmit`
---
---
# Phase 8: 노출 규칙 빌더 (모든 컴포넌트 공통)
> **선행 조건**: Phase 7 완료 후 진행.
> **목표**: 쿼리 없이도 노출 규칙을 설정할 수 있도록 소스 타입을 4가지로 확장하고, 우측 패널에 멀티 조건 인라인 빌더를 도입한다.
## 핵심 설계 원칙
| 항목 | AS-IS | TO-BE |
|------|-------|-------|
| 명칭 | 조건부 표시 | **노출 규칙** |
| 조건 수 | 1개 고정 | **복수 조건** |
| 조건 소스 | 쿼리 결과만 | **쿼리 / 파라미터 / 컴포넌트 / 사용자** |
| 접근 경로 | 모달 탭 (3단계 클릭) | **우측 패널 인라인 (1단계)** |
| 모달 탭 | 존재 | **유지 (고급 편집용)** |
| 결합 표현 | IF … AND … THEN | **"모든 조건 충족" / "하나라도 충족"** |
| 연산자 표시 | 텍스트 ("같음") | **기호 (= ≠ > < ≥ ≤ 포함 값있음 값없음)** |
## 소스 타입 4가지
| 소스 타입 | 표시명 | 사용 예 |
|---------|------|--------|
| `query` | 쿼리 결과 | DB 조회 필드로 조건 |
| `parameter` | 리포트 파라미터 | 호출 시 넘어온 값 (mode, docType 등) |
| `component` | 컴포넌트 값 | 같은 리포트 내 다른 컴포넌트의 바인딩 값 |
| `user` | 사용자 정보 | 로그인 사용자의 역할 / 부서 / 회사코드 |
## 구현 순서
```
Step 8-A types/report.ts ← VisibilityRule 신규 타입 추가
Step 8-B VisibilityRuleStrip.tsx [신규] ← 우측 패널 인라인 빌더
Step 8-C ComponentStylePanel.tsx ← VisibilityRuleStrip 하단 통합
Step 8-D ConditionalProperties.tsx ← 멀티 조건 + 소스 타입 지원
Step 8-E ConditionalSettingsTab.tsx ← 탭 레이블 "노출 규칙"
Step 8-F evaluateVisibility.ts [신규] ← 조건 평가 유틸 (공용)
Step 8-G CanvasComponent / renderers ← VisibilityRule 평가 로직
Step 8-H ReportPreviewModal ← VisibilityRule 평가 로직
```
---
## Step 8-A — `frontend/types/report.ts`
기존 `ConditionalRule` 유지 (하위 호환). 신규 타입 추가.
```typescript
// 조건 소스 타입
export type ConditionSourceType = "query" | "parameter" | "component" | "user";
// 사용자 컨텍스트 필드
export type UserContextField = "role" | "department" | "companyCode" | "userId";
// 연산자 (기존 ConditionalRule["operator"]와 동일 목록)
export type ConditionOperator =
| "eq" | "ne" | "gt" | "lt" | "gte" | "lte"
| "contains" | "notEmpty" | "empty";
// 개별 조건 단위
export interface VisibilityCondition {
id: string;
sourceType: ConditionSourceType;
// sourceType = "query"
queryId?: string;
field?: string;
// sourceType = "parameter"
parameterName?: string;
// sourceType = "component"
componentId?: string;
// sourceType = "user"
userField?: UserContextField;
operator: ConditionOperator;
value: string;
}
// 노출 규칙 전체
export interface VisibilityRule {
conditions: VisibilityCondition[];
combinator: "AND" | "OR"; // "모든 조건 충족" | "하나라도 충족"
action: "show" | "hide";
}
```
`ComponentConfig`에 신규 필드 추가 (기존 `conditionalRule` 유지):
```typescript
conditionalRule?: ConditionalRule; // 기존 유지 (하위 호환)
visibilityRule?: VisibilityRule; // 신규 (Phase 8)
```
렌더링 우선순위: `visibilityRule` 있으면 우선 → 없으면 `conditionalRule` fallback.
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8-B — `VisibilityRuleStrip.tsx` [신규]
경로: `frontend/components/report/designer/properties/VisibilityRuleStrip.tsx`
역할: 우측 패널 하단 인라인 노출 규칙 빌더. 모달 없이 직접 편집. 모든 컴포넌트 공통.
### UI — 규칙 없을 때
```
┌─────────────────────────────────────────┐
│ 노출 규칙 [+ 설정] │
│ 모든 조건에서 표시됩니다 │
└─────────────────────────────────────────┘
```
### UI — 규칙 있을 때
```
┌─────────────────────────────────────────────┐
│ 노출 규칙 [규칙 삭제] │
│ 적용 방식: [모든 조건 충족 ▼] │
│ ┌───────────────────────────────────────┐ │
│ │ [쿼리결과 ▼][status ▼] [= ▼] [완료][×] │ │
│ │ [파라미터 ▼][mode ] [= ▼] [편집][×] │ │
│ │ [사용자 ▼][역할 ▼] [= ▼] [관리자][×]│ │
│ └───────────────────────────────────────┘ │
│ [+ 조건 추가] │
│ 이 컴포넌트를 [숨김 ▼]합니다 │
│ ───────────────────────────────────────── │
│ 📋 status가 '완료'이고, mode가 '편집'이고 │
│ 역할이 '관리자'일 때 이 컴포넌트를 │
│ 숨깁니다 │
└─────────────────────────────────────────────┘
```
### 소스 타입별 조건 행 렌더링
| 소스 타입 | 이후 표시 |
|---------|---------|
| 쿼리 결과 | [쿼리명 ▼] [필드 ▼] |
| 리포트 파라미터 | [파라미터명 텍스트 입력] |
| 컴포넌트 값 | [컴포넌트 선택 ▼] (현재 페이지 내 목록) |
| 사용자 정보 | [역할 / 부서 / 회사코드 / 사용자ID ▼] |
### 연산자 기호 표
| 코드 | 표시 | 코드 | 표시 |
|------|------|------|------|
| eq | = | ne | ≠ |
| gt | > | lt | < |
| gte | ≥ | lte | ≤ |
| contains | 포함 | notEmpty | 값 있음 |
| empty | 값 없음 | | |
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8-C — `frontend/components/report/designer/properties/ComponentStylePanel.tsx`
`VisibilityRuleStrip`을 ScrollArea 최하단에 구분선과 함께 추가:
```tsx
import { VisibilityRuleStrip } from "./VisibilityRuleStrip";
// ScrollArea 내 space-y-5 마지막에 추가
<div className="border-t border-dashed border-gray-200 pt-4">
<VisibilityRuleStrip component={component} />
</div>
```
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8-D — `frontend/components/report/designer/properties/ConditionalProperties.tsx`
기존 단일 조건 UI → `VisibilityRule` 기반 멀티 조건 UI로 교체. (모달 탭에서 사용)
**마이그레이션 헬퍼** (구형 `conditionalRule` → 신형 `visibilityRule` 자동 변환):
```typescript
function migrateConditionalRule(rule: ConditionalRule): VisibilityRule {
return {
conditions: [{
id: generateId(),
sourceType: "query",
queryId: rule.queryId,
field: rule.field,
operator: rule.operator,
value: rule.value,
}],
combinator: "AND",
action: rule.action,
};
}
```
컴포넌트 최초 열릴 때: `visibilityRule` 없고 `conditionalRule` 있으면 마이그레이션 후 저장.
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8-E — `frontend/components/report/designer/modals/ConditionalSettingsTab.tsx`
탭 레이블만 변경:
```tsx
// 변경 전
<h2>조건부 표시</h2>
// 변경 후
<h2>노출 규칙</h2>
```
`ComponentSettingsModal`의 탭 트리거 레이블도 동일하게 변경.
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8-F — `frontend/lib/report/evaluateVisibility.ts` [신규]
CanvasComponent와 ReportPreviewModal이 공용으로 사용하는 조건 평가 유틸.
```typescript
export interface VisibilityContext {
getQueryResult: (queryId: string) => QueryResult | null;
parameters: Record<string, string>; // 리포트 호출 파라미터
components: ComponentConfig[]; // 컴포넌트 값 참조용
user: {
role: string;
department: string;
companyCode: string;
userId: string;
};
}
// 신규: VisibilityRule 평가
export function evaluateVisibilityRule(
rule: VisibilityRule,
context: VisibilityContext,
): boolean {
const results = rule.conditions.map((cond) => evaluateSingleCondition(cond, context));
return rule.combinator === "AND"
? results.every(Boolean)
: results.some(Boolean);
}
// 기존 유지: ConditionalRule fallback
export function evaluateConditionalRule(
rule: ConditionalRule,
getQueryResult: (id: string) => QueryResult | null,
): boolean { /* 기존 로직 이관 */ }
```
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8-G — CanvasComponent / renderers
조건 평가를 `evaluateVisibility.ts`에서 import 후 우선순위 적용:
```typescript
const isHidden = (() => {
if (component.visibilityRule) {
return !evaluateVisibilityRule(component.visibilityRule, context);
}
if (component.conditionalRule) {
return !evaluateConditionalRule(component.conditionalRule, getQueryResult);
}
return false;
})();
```
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Step 8-H — `frontend/components/report/ReportPreviewModal.tsx`
Step 8-G와 동일한 평가 로직 적용. `evaluateVisibility.ts` import 후 렌더링 시 적용.
- 완료 확인: `cd frontend && npx tsc --noEmit`
---
## Phase 8 파일 변경 요약
### 수정 파일 (5개)
| 경로 | 핵심 변경 |
|------|---------|
| `frontend/types/report.ts` | VisibilityRule, VisibilityCondition, ConditionSourceType 추가 |
| `frontend/components/report/designer/properties/ComponentStylePanel.tsx` | VisibilityRuleStrip 하단 통합 |
| `frontend/components/report/designer/properties/ConditionalProperties.tsx` | 멀티 조건 + 소스 타입 UI + 마이그레이션 헬퍼 |
| `frontend/components/report/designer/modals/ConditionalSettingsTab.tsx` | 탭 레이블 "노출 규칙" |
| `frontend/components/report/ReportPreviewModal.tsx` | VisibilityRule 평가 로직 |
### 신규 파일 (2개)
| 경로 | 역할 |
|------|------|
| `frontend/components/report/designer/properties/VisibilityRuleStrip.tsx` | 우측 패널 인라인 노출 규칙 빌더 |
| `frontend/lib/report/evaluateVisibility.ts` | 조건 평가 유틸 (CanvasComponent + Preview 공용) |
## Phase 8 충돌 사전 검사
구현 시작 전 아래 이름들이 현재 코드베이스에 **0건**인지 Grep 확인 필수:
```
VisibilityRule, VisibilityCondition, ConditionSourceType, UserContextField,
ConditionOperator, visibilityRule, VisibilityRuleStrip,
evaluateVisibilityRule, VisibilityContext, evaluateVisibility
```
## Phase 8 주의사항
1. **하위 호환 필수**: `conditionalRule` 필드와 기존 평가 로직 삭제 금지. `visibilityRule`이 없으면 자동으로 구형 처리.
2. **마이그레이션 자동화**: `ConditionalProperties` 최초 열릴 때 구형 `conditionalRule` 있으면 `migrateConditionalRule()`로 신형 변환 후 `visibilityRule`에 저장.
3. **파라미터 전달 경로**: 리포트 호출 시 파라미터를 Context 또는 props로 `VisibilityContext`까지 흘려야 함. Step 8-F 구현 전 전달 경로 확인 필수.
4. **사용자 정보 조회**: 평가 시점에 `AuthContext` 또는 `localStorage`에서 role/department 조회. 전달 경로 확정 후 구현.
5. **각 Step 완료 시 필수**: `cd frontend && npx tsc --noEmit`
---
---
# Phase 9: 컴포넌트 팔레트 고도화 — 테이블 컬럼 팔레트 + querySummary
> **목표**: 테이블(Table) 컴포넌트의 컬럼 설정 UI에 타입별 컬럼 팔레트와 드래그&드롭 순서 변경을 도입하여, Card 컴포넌트와 동일한 팔레트+캔버스 편집 패턴을 적용한다.
## Phase 9 단계별 계획
| 단계 | 내용 | 상태 |
|------|------|------|
| 9-A | `types/report.ts``TableColumnDisplayType` 타입 추가 + `tableColumns` 필드 확장 | ⬜ 대기 |
| 9-B | `TableColumnPalette.tsx` 신규 생성 (9종 컬럼 타입 팔레트) | ⬜ 대기 |
| 9-C | `TableColumnRow.tsx` 분리 (TableProperties에서 ColumnRow 서브 컴포넌트 추출) | ⬜ 대기 |
| 9-D | `TableProperties.tsx` 수정 — 팔레트 드롭존 + 행 간 드래그 순서 변경 | ⬜ 대기 |
---
## Phase 9 핵심 설계
### 테이블 컬럼 팔레트 — 9종 타입 정의
| `displayType` | 한글 라벨 | 아이콘 | 자동 기본값 |
|---|---|---|---|
| `text` | 텍스트 | `Type` | `align: "left"`, `numberFormat: "none"` |
| `number` | 숫자 | `Hash` | `align: "right"`, `numberFormat: "comma"` |
| `currency` | 금액 | `Banknote` | `align: "right"`, `numberFormat: "currency"`, `currencySuffix: "원"` |
| `date` | 날짜 | `Calendar` | `align: "center"`, `numberFormat: "none"` |
| `link` | 링크 | `Link2` | `align: "left"`, `numberFormat: "none"` |
| `status` | 상태 | `Circle` | `align: "center"`, `numberFormat: "none"` |
| `image` | 이미지 | `ImageIcon` | `align: "center"`, `numberFormat: "none"`, `width: 60` |
| `formula` | 수식 | `FunctionSquare` | `align: "right"`, `mappingType: "formula"`, `numberFormat: "comma"` |
| `sequence` | 순번 | `ListOrdered` | `align: "center"`, `mappingType: "formula"`, `numberFormat: "none"` |
### AS-IS vs TO-BE
| 항목 | AS-IS | TO-BE |
|------|-------|-------|
| 컬럼 추가 | "추가" 버튼 → 빈 컬럼 → 수동 입력 | 타입별 팔레트 칩 드래그 → 타입에 맞는 기본값 자동 세팅 |
| 컬럼 순서 변경 | ▲▼ 버튼 클릭 | 드래그&드롭 (▲▼ 버튼도 유지) |
| 파일 크기 | `TableProperties.tsx` 637줄 | 3파일로 분리 (각 ~250~300줄) |
---
## Phase 9 신규/수정 파일 목록
### 신규 생성 파일
| 파일 | 역할 |
|------|------|
| `frontend/components/report/designer/modals/TableColumnPalette.tsx` | 9종 컬럼 타입 드래그 가능 팔레트 칩 + `TABLE_COLUMN_DND_TYPE` 상수 |
| `frontend/components/report/designer/properties/TableColumnRow.tsx` | `ColumnRow` 서브 컴포넌트 독립 파일 (`useDrag` 드래그 핸들 포함) |
### 수정 파일
| 파일 | 변경 내용 |
|------|-----------|
| `frontend/types/report.ts` | `TableColumnDisplayType` union type 추가, `tableColumns` 배열 요소에 `displayType?: TableColumnDisplayType` 필드 추가 |
| `frontend/components/report/designer/properties/TableProperties.tsx` | `TableColumnPalette` import + 렌더링, `useDrop` 드롭존 추가, `addColumnByType()` 헬퍼, `ColumnRow` import 교체 |
---
## Phase 9 구현 순서 (의존성)
```
1. types/report.ts — TableColumnDisplayType 타입 추가
2. TableColumnPalette.tsx — 신규 생성 (TABLE_COLUMN_DND_TYPE 상수 포함)
3. TableColumnRow.tsx — ColumnRow 분리 (useDrag 드래그 핸들 추가)
4. TableProperties.tsx — 팔레트 import + 드롭존 + addColumnByType 추가
```
---
## Phase 9 충돌 검사 대상
| 이름 | 종류 |
|------|------|
| `TABLE_COLUMN_DND_TYPE` | 상수 |
| `TableColumnPalette` | 컴포넌트명 |
| `TableColumnDisplayType` | 타입명 |
| `TableColumnRow` | 컴포넌트명 |
| `addColumnByType` | 함수명 |
| `TABLE_COLUMN_DEFAULTS` | 상수명 |
---
## Phase 9 주의사항
---
---
# Phase 11: 컴포넌트 설정 모달 디자인 통일화
> **목표**: Properties 파일 5개에서 사용 중인 shadcn `Card` 패턴을 커스텀 `div` 섹션 패턴으로 교체하고, `h-8` 필드 높이를 `h-9`으로 통일하여 모든 컴포넌트 설정 모달의 디자인을 일관되게 만든다.
> **기능 로직은 일절 변경하지 않는다.**
## 표준 섹션 패턴 (통일 기준)
```tsx
// 강조(accent) 섹션
<div className="mt-4 rounded-xl border border-{color}-200 bg-{color}-50/50 p-4 space-y-3">
<div className="flex items-center gap-2 text-sm font-semibold text-{color}-700">
<Icon className="h-4 w-4" />
섹션 제목
</div>
{/* 콘텐츠 */}
</div>
```
## 수정 파일 목록 (5개)
| 파일 | 현재 패턴 | 수정 내용 | 색상 |
|------|-----------|-----------|------|
| `properties/SignatureProperties.tsx` | shadcn Card 2개, h-8 8곳 | Card → div, h-9 | indigo |
| `properties/PageNumberProperties.tsx` | shadcn Card 1개, h-8 1곳 | Card → div, h-9 | purple |
| `properties/CalculationProperties.tsx` | shadcn Card 2개, h-8 11곳 | Card → div, h-9 | orange |
| `properties/BarcodeProperties.tsx` | shadcn Card 2개, h-8 다수 | Card → div, h-9 | cyan |
| `properties/CheckboxProperties.tsx` | shadcn Card 2개, h-8 6곳 | Card → div, h-9 | purple |
**기준 파일**: `modals/ImageLayoutTabs.tsx` — 이미 커스텀 div 패턴 사용 (참조용, 수정 없음)
## 구현 순서 (단순 → 복잡)
```
1. PageNumberProperties.tsx (섹션 1개, h-8 1곳)
2. CheckboxProperties.tsx (섹션 2개, h-8 6곳)
3. SignatureProperties.tsx (섹션 2개, h-8 8곳)
4. BarcodeProperties.tsx (섹션 2개, h-8 다수)
5. CalculationProperties.tsx (섹션 2개, h-8 11곳)
```
## 추가할 아이콘 (lucide-react)
| 파일 | 추가 아이콘 | 기존 import 충돌 |
|------|------------|----------------|
| SignatureProperties | `PenLine` | 없음 |
| PageNumberProperties | `Hash` | 없음 |
| CalculationProperties | `Calculator` | 없음 |
| BarcodeProperties | `QrCode` | 없음 (`X` 기존 사용) |
| CheckboxProperties | `CheckSquare` | 없음 |
## 제거할 import (각 파일)
```typescript
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
```
## 신규 생성 파일
없음. 공통 Section 컴포넌트 추출 하지 않음 (컬러 테마가 파일마다 달라 오히려 복잡도 증가).
## 주의사항
1. **안내 블록 유지**: `BarcodeProperties``rounded border border-cyan-200 bg-cyan-100` 안내 박스는 섹션 카드가 아닌 인라인 안내 텍스트 — 수정하지 않음
2. **`CalculationProperties` 항목 카드 유지**: 개별 계산 항목의 `rounded border bg-white p-2` 카드는 섹션 카드가 아님 — 수정하지 않음
3. **우측 패널 동시 적용**: 5개 파일 모두 `section` prop을 받아 우측 패널에서도 렌더링됨 → 이번 수정은 모달+패널 양쪽 모두 개선
4. **각 파일 완료 후**: `cd frontend && npx tsc --noEmit` 필수
## 충돌 검사 대상
없음 (신규 이름 추가 없음, 아이콘 import만 추가)
1. **하위 호환**: `displayType`은 optional — 기존 저장 데이터에 없어도 작동해야 함. `numberFormat`으로 타입 추론하는 `inferDisplayType()` 헬퍼 추가 권장.
2. **DndProvider 스코프**: 테이블 팔레트는 `ComponentSettingsModal` 내부 → designer 페이지 `DndProvider` 하위이므로 추가 `DndProvider` 불필요.
3. **ColumnRow useDrag + useDrop 중복 ref**: `<tr>`에 drop ref, 드래그 핸들 `<td>`에 drag ref를 분리 바인딩하여 UX 명확화.
4. **파일 분리 시 순환 import 주의**: `TableColumnRow.tsx``TableColumnPalette.tsx`의 DND_TYPE 상수를 import해야 하므로 상수는 별도 export 또는 `TableColumnPalette.tsx`에서 export.
5. **각 Step 완료 시 필수**: `cd frontend && npx tsc --noEmit`