# 리포트 디자이너 고도화 — 구현 계획서 > **최초 작성**: 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 (
미리보기 (저장 전 상태)

실제 데이터는 저장 후 확인 가능합니다.

); } ``` - 완료 확인: `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
카드 레이아웃 설정
``` **`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(null); const [showPreview, setShowPreview] = useState(false); useEffect(() => { if (component) { setLocalDraft(component); setShowPreview(false); } }, [componentModalTargetId]); ``` **card 타입 DataTab에 Draft 콜백 전달:** ```typescript case "card": return ( 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 추가** (`` 아래, `` 위): ```tsx {showPreview && component.type === "card" && ( )}
{component.type === "card" ? ( ) : }
``` **주의:** `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) => 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

배치

{[ { 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 }) => (
update({ [key]: parseInt(e.target.value) || min || 0 })} className="h-9 text-sm" />
))}
``` ### ② 테두리 섹션에 모서리 반경 추가 ```tsx
``` **주의:** `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 ; case "number": return ; case "date": return ; case "link": return ; case "status": return ; case "spacer": return
; case "staticText": return ; ``` **신규 렌더러 구현 요약:** - `CardImageRenderer`: `` 태그, objectFit 적용, columnName으로 값 조회 - `CardNumberRenderer`: DataCell 구조 + numberFormat (none/comma/currency) - `CardDateRenderer`: DataCell 구조 + dateFormat 문자열 포맷 - `CardLinkRenderer`: `` 태그, openInNewTab → `target="_blank" rel="noopener noreferrer"` - `CardStatusRenderer`: 색상 dot + 라벨 텍스트, statusMappings 매핑 - `CardStaticTextRenderer`: `` 태그, 직접 스타일 적용 - 완료 확인: `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 마지막에 추가
``` - 완료 확인: `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 // 변경 전

조건부 표시

// 변경 후

노출 규칙

``` `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; // 리포트 호출 파라미터 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) 섹션
섹션 제목
{/* 콘텐츠 */}
``` ## 수정 파일 목록 (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**: ``에 drop ref, 드래그 핸들 ``에 drag ref를 분리 바인딩하여 UX 명확화. 4. **파일 분리 시 순환 import 주의**: `TableColumnRow.tsx`가 `TableColumnPalette.tsx`의 DND_TYPE 상수를 import해야 하므로 상수는 별도 export 또는 `TableColumnPalette.tsx`에서 export. 5. **각 Step 완료 시 필수**: `cd frontend && npx tsc --noEmit`