ERP-node/reportdocs/PLAN.md

43 KiB
Raw Blame History

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

최초 작성: 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-2grid 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;

섹션 구성 순서:

  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

⚠️ 전체 컴포넌트 공통 적용 — 어떤 컴포넌트를 선택해도 동일하게 동작.

① 위치/크기 섹션 최상단 추가 (기존 "글꼴" 섹션 위)

<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.borderRadiusnumber, CardLayoutConfig.borderRadiusstring. 혼용 금지.

  • 완료 확인: 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 주의사항

  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.borderRadiusnumber, CardLayoutConfig.borderRadiusstring. 혼용 금지.
  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 유지 (하위 호환). 신규 타입 추가.

// 조건 소스 타입
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 주의사항

  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.tsTableColumnDisplayType 타입 추가 + 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 컴포넌트 추출 하지 않음 (컬러 테마가 파일마다 달라 오히려 복잡도 증가).

주의사항

  1. 안내 블록 유지: BarcodePropertiesrounded 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.tsxTableColumnPalette.tsx의 DND_TYPE 상수를 import해야 하므로 상수는 별도 export 또는 TableColumnPalette.tsx에서 export.
  5. 각 Step 완료 시 필수: cd frontend && npx tsc --noEmit