1154 lines
43 KiB
Markdown
1154 lines
43 KiB
Markdown
# 리포트 디자이너 고도화 — 구현 계획서
|
||
|
||
> **최초 작성**: 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`
|