43 KiB
리포트 디자이너 고도화 — 구현 계획서
최초 작성: 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 확장
// 변경 전
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 정의 바로 아래에 삽입:
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 유니온 타입 확장
// 변경 후
export type CardElement =
| CardHeaderElement | CardDataCellElement | CardDividerElement | CardBadgeElement
| CardImageElement | CardNumberElement | CardDateElement | CardLinkElement
| CardStatusElement | CardSpacerElement | CardStaticTextElement;
1-4. CardLayoutConfig 신규 필드 추가
기존 dividerColor?: string; 아래에 추가:
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
{ 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에 신규 케이스 추가:
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 상태의 카드를 실제 렌더러로 미리보여주는 인라인 패널.
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
탭 구조 변경:
// 변경 전
type TabType = "layout" | "binding" | "style";
// 변경 후
type TabType = "layout" | "binding";
제거 대상: Palette 아이콘, activeTab === "style" 버튼, renderStyleTab() 전체
텍스트 변경: "레이아웃" → "레이아웃 구성", "데이터 바인딩" → "데이터 연결"
카드 모달 구분 배지 추가 (탭 버튼 그룹 위):
<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 조건 확장:
// 변경 후
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 상태 추가:
const [localDraft, setLocalDraft] = useState<ComponentConfig | null>(null);
const [showPreview, setShowPreview] = useState(false);
useEffect(() => {
if (component) { setLocalDraft(component); setShowPreview(false); }
}, [componentModalTargetId]);
card 타입 DataTab에 Draft 콜백 전달:
case "card":
return (
<CardProperties
component={localDraft ?? component}
section="data"
onConfigChange={(updates) =>
setLocalDraft((prev) => ({ ...(prev ?? component), ...updates }))
}
/>
);
저장/취소 핸들러:
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> 위):
{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 변경
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개: 인포 카드 / 컴팩트 카드 / 커스텀)
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;
섹션 구성 순서:
- 프리셋 선택
- 카드 외형 (배경색, 모서리, 패딩, 행 간격)
- 전체 테두리 (스타일, 두께, 색상)
- 좌측 액센트 보더 (ON/OFF Switch + 두께 + 색상)
- 헤더 스타일 (폰트 크기, 색상, 굵기)
- 라벨 스타일 (폰트 크기, 색상)
- 값 스타일 (폰트 크기, 색상, 굵기)
- 구분선 스타일 (두께, 색상, 스타일)
- 완료 확인:
cd frontend && npx tsc --noEmit
Step 8 — frontend/components/report/designer/properties/ComponentStylePanel.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>
② 테두리 섹션에 모서리 반경 추가
<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 적용
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종 렌더러 추가
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 [삭제]
⚠️ 전체 컴포넌트 공통 영향 — 모달의 스타일 탭이 모든 컴포넌트에서 사라지고 우측 패널로 통합됨.
# 사용처 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 주의사항
- exhaustive check:
createDefaultElement,getElementIcon,getElementLabel,CardElementRendererswitch 모두 신규 7케이스 추가 필수. Step 1 직후npx tsc --noEmit실행하여 영향 파일 목록 확인. - Draft 범위: card 기능 설정 탭에만 적용.
QuerySettingsTab/ConditionalSettingsTab은 기존 즉시 반영 유지. - borderWidth 이름 충돌:
ComponentConfig.borderWidth(컴포넌트 테두리)와CardLayoutConfig.borderWidth(카드 컨테이너 테두리)는 별개. - borderRadius 타입 차이:
ComponentConfig.borderRadius는number,CardLayoutConfig.borderRadius는string. 혼용 금지. - CardPreviewPanel 데이터 미표시:
getQueryResult → null이므로 데이터 셀은 "-" 표시. 안내 문구 필수. - 각 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 유지 (하위 호환). 신규 타입 추가.
// 조건 소스 타입
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 유지):
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 최하단에 구분선과 함께 추가:
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 자동 변환):
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
탭 레이블만 변경:
// 변경 전
<h2>조건부 표시</h2>
// 변경 후
<h2>노출 규칙</h2>
ComponentSettingsModal의 탭 트리거 레이블도 동일하게 변경.
- 완료 확인:
cd frontend && npx tsc --noEmit
Step 8-F — frontend/lib/report/evaluateVisibility.ts [신규]
CanvasComponent와 ReportPreviewModal이 공용으로 사용하는 조건 평가 유틸.
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 후 우선순위 적용:
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 주의사항
- 하위 호환 필수:
conditionalRule필드와 기존 평가 로직 삭제 금지.visibilityRule이 없으면 자동으로 구형 처리. - 마이그레이션 자동화:
ConditionalProperties최초 열릴 때 구형conditionalRule있으면migrateConditionalRule()로 신형 변환 후visibilityRule에 저장. - 파라미터 전달 경로: 리포트 호출 시 파라미터를 Context 또는 props로
VisibilityContext까지 흘려야 함. Step 8-F 구현 전 전달 경로 확인 필수. - 사용자 정보 조회: 평가 시점에
AuthContext또는localStorage에서 role/department 조회. 전달 경로 확정 후 구현. - 각 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으로 통일하여 모든 컴포넌트 설정 모달의 디자인을 일관되게 만든다. 기능 로직은 일절 변경하지 않는다.
표준 섹션 패턴 (통일 기준)
// 강조(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 (각 파일)
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
신규 생성 파일
없음. 공통 Section 컴포넌트 추출 하지 않음 (컬러 테마가 파일마다 달라 오히려 복잡도 증가).
주의사항
- 안내 블록 유지:
BarcodeProperties의rounded border border-cyan-200 bg-cyan-100안내 박스는 섹션 카드가 아닌 인라인 안내 텍스트 — 수정하지 않음 CalculationProperties항목 카드 유지: 개별 계산 항목의rounded border bg-white p-2카드는 섹션 카드가 아님 — 수정하지 않음- 우측 패널 동시 적용: 5개 파일 모두
sectionprop을 받아 우측 패널에서도 렌더링됨 → 이번 수정은 모달+패널 양쪽 모두 개선 - 각 파일 완료 후:
cd frontend && npx tsc --noEmit필수
충돌 검사 대상
없음 (신규 이름 추가 없음, 아이콘 import만 추가)
- 하위 호환:
displayType은 optional — 기존 저장 데이터에 없어도 작동해야 함.numberFormat으로 타입 추론하는inferDisplayType()헬퍼 추가 권장. - DndProvider 스코프: 테이블 팔레트는
ComponentSettingsModal내부 → designer 페이지DndProvider하위이므로 추가DndProvider불필요. - ColumnRow useDrag + useDrop 중복 ref:
<tr>에 drop ref, 드래그 핸들<td>에 drag ref를 분리 바인딩하여 UX 명확화. - 파일 분리 시 순환 import 주의:
TableColumnRow.tsx가TableColumnPalette.tsx의 DND_TYPE 상수를 import해야 하므로 상수는 별도 export 또는TableColumnPalette.tsx에서 export. - 각 Step 완료 시 필수:
cd frontend && npx tsc --noEmit