# 리포트 디자이너 고도화 — 구현 계획서

> **최초 작성**: 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`
