feat: InvTableConfigPanel pivot/card 옵션 UI (T4)
displayMode 분기 옵션 채움. T3b 에서 흡수한 PivotView + CardView 가 실제로 동작하도록 ConfigPanel UI 노출. cp 프리미티브만 사용. pivot 분기 (placeholder 제거) - columns 별 영역 매핑 (none/row/column/data/filter) — CPSelect - data 영역인 경우 집계 함수 (sum/count/avg/min/max/countDistinct) — CPSelect - pivotFields[] 배열로 자동 변환 (area="none" 선택 시 항목 제거) - 컬럼 미로드 시 안내 Hint card 분기 (신규) - 그리드: cardsPerRow (1~10), cardSpacing (0~64px) — CPNumber - 표시 영역 (CPGroup defaultOpen): 제목/부제/설명/이미지 표시 토글 + 이미지 위치 (top/left/right) + 이미지 크기 (small/medium/large) - 컬럼 매핑 (CPGroup defaultOpen): titleColumn/subtitleColumn/ descriptionColumn/imageColumn — CPSelect from columns - 액션 버튼 (CPGroup defaultOpen=false): showActions 토글 + showView/Edit/ DeleteButton 조건부 노출 cp-panel-standard 룰 준수 (CPSection > CPGroup > CPRow > CPSelect/Switch /Segment/Number, 3-depth 미만, 카드형 외곽 X). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -281,12 +281,273 @@ export const InvTableConfigPanel: React.FC<InvTableConfigPanelProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{displayMode === "pivot" && (
|
{displayMode === "pivot" && (
|
||||||
<CPSection title="④ 피벗 설정" desc="개발 중">
|
<CPSection title="④ 피벗 설정" desc="row · column · data 영역에 컬럼 배치">
|
||||||
<Hint tone="warn">
|
{columns.length === 0 ? (
|
||||||
피벗 row/column/values 편집 UI는 추후 추가 예정.
|
<Hint tone="warn">컬럼을 먼저 자동 로드 또는 추가하세요.</Hint>
|
||||||
<br />
|
) : (
|
||||||
현재는 displayMode 만 저장 (백엔드 키 보존).
|
<>
|
||||||
</Hint>
|
<Hint>
|
||||||
|
각 컬럼의 영역을 지정하면 피벗 필드가 생성됩니다. data 영역만 집계 함수가 필요합니다.
|
||||||
|
</Hint>
|
||||||
|
{columns.map((col, idx) => {
|
||||||
|
const fields = current.pivotFields ?? [];
|
||||||
|
const fieldIdx = fields.findIndex((f) => f.field === col.key);
|
||||||
|
const field = fieldIdx >= 0 ? fields[fieldIdx] : undefined;
|
||||||
|
const area = field?.area ?? "none";
|
||||||
|
const summaryType = field?.summaryType ?? "sum";
|
||||||
|
const updateField = (next: Partial<NonNullable<TableConfig["pivotFields"]>[number]> | "remove") => {
|
||||||
|
const list = [...fields];
|
||||||
|
if (next === "remove") {
|
||||||
|
if (fieldIdx >= 0) list.splice(fieldIdx, 1);
|
||||||
|
} else if (fieldIdx >= 0) {
|
||||||
|
list[fieldIdx] = { ...list[fieldIdx], ...next };
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
field: col.key,
|
||||||
|
caption: col.label || col.key,
|
||||||
|
area: "row",
|
||||||
|
...next,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
patch({ pivotFields: list });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<CPRow key={col.key} label={col.label || col.key}>
|
||||||
|
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
|
||||||
|
<CPSelect
|
||||||
|
value={area}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v === "none") updateField("remove");
|
||||||
|
else updateField({ area: v as any });
|
||||||
|
}}
|
||||||
|
searchable={false}
|
||||||
|
>
|
||||||
|
<option value="none">없음</option>
|
||||||
|
<option value="row">row</option>
|
||||||
|
<option value="column">column</option>
|
||||||
|
<option value="data">data</option>
|
||||||
|
<option value="filter">filter</option>
|
||||||
|
</CPSelect>
|
||||||
|
{area === "data" && (
|
||||||
|
<CPSelect
|
||||||
|
value={summaryType}
|
||||||
|
onChange={(v) => updateField({ summaryType: v as any })}
|
||||||
|
searchable={false}
|
||||||
|
>
|
||||||
|
<option value="sum">합계</option>
|
||||||
|
<option value="count">개수</option>
|
||||||
|
<option value="avg">평균</option>
|
||||||
|
<option value="min">최소</option>
|
||||||
|
<option value="max">최대</option>
|
||||||
|
<option value="countDistinct">고유 개수</option>
|
||||||
|
</CPSelect>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CPRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CPSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{displayMode === "card" && (
|
||||||
|
<CPSection title="④ 카드 설정" desc="grid · 표시 영역 · 컬럼 매핑">
|
||||||
|
<CPRow label="한 줄 카드 수">
|
||||||
|
<CPNumber
|
||||||
|
value={current.cardsPerRow ?? 3}
|
||||||
|
onChange={(v) => patch({ cardsPerRow: v ?? 3 })}
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="간격(px)">
|
||||||
|
<CPNumber
|
||||||
|
value={current.cardSpacing ?? 12}
|
||||||
|
onChange={(v) => patch({ cardSpacing: v ?? 12 })}
|
||||||
|
min={0}
|
||||||
|
max={64}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
|
||||||
|
<CPGroup title="표시 영역" defaultOpen>
|
||||||
|
<CPRow label="제목">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showTitle ?? true}
|
||||||
|
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showTitle: v } })}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="부제">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showSubtitle ?? true}
|
||||||
|
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showSubtitle: v } })}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="설명">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showDescription ?? true}
|
||||||
|
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showDescription: v } })}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="이미지">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showImage ?? true}
|
||||||
|
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showImage: v } })}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
{(current.cardStyle?.showImage ?? true) && (
|
||||||
|
<>
|
||||||
|
<CPRow label="이미지 위치">
|
||||||
|
<CPSegment
|
||||||
|
value={current.cardStyle?.imagePosition || "top"}
|
||||||
|
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, imagePosition: v as any } })}
|
||||||
|
options={[
|
||||||
|
{ value: "top", label: "상단" },
|
||||||
|
{ value: "left", label: "좌측" },
|
||||||
|
{ value: "right", label: "우측" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="이미지 크기">
|
||||||
|
<CPSegment
|
||||||
|
value={current.cardStyle?.imageSize || "medium"}
|
||||||
|
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, imageSize: v as any } })}
|
||||||
|
options={[
|
||||||
|
{ value: "small", label: "작게" },
|
||||||
|
{ value: "medium", label: "보통" },
|
||||||
|
{ value: "large", label: "크게" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CPGroup>
|
||||||
|
|
||||||
|
<CPGroup title="컬럼 매핑" defaultOpen>
|
||||||
|
{columns.length === 0 ? (
|
||||||
|
<Hint tone="warn">컬럼을 먼저 자동 로드 또는 추가하세요.</Hint>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CPRow label="제목 컬럼">
|
||||||
|
<CPSelect
|
||||||
|
value={current.cardColumnMapping?.titleColumn || ""}
|
||||||
|
onChange={(v) =>
|
||||||
|
patch({
|
||||||
|
cardColumnMapping: {
|
||||||
|
...current.cardColumnMapping,
|
||||||
|
titleColumn: v || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">선택...</option>
|
||||||
|
{columns.map((c) => (
|
||||||
|
<option key={c.key} value={c.key}>
|
||||||
|
{c.label || c.key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</CPSelect>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="부제 컬럼">
|
||||||
|
<CPSelect
|
||||||
|
value={current.cardColumnMapping?.subtitleColumn || ""}
|
||||||
|
onChange={(v) =>
|
||||||
|
patch({
|
||||||
|
cardColumnMapping: {
|
||||||
|
...current.cardColumnMapping,
|
||||||
|
subtitleColumn: v || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">선택...</option>
|
||||||
|
{columns.map((c) => (
|
||||||
|
<option key={c.key} value={c.key}>
|
||||||
|
{c.label || c.key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</CPSelect>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="설명 컬럼">
|
||||||
|
<CPSelect
|
||||||
|
value={current.cardColumnMapping?.descriptionColumn || ""}
|
||||||
|
onChange={(v) =>
|
||||||
|
patch({
|
||||||
|
cardColumnMapping: {
|
||||||
|
...current.cardColumnMapping,
|
||||||
|
descriptionColumn: v || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">선택...</option>
|
||||||
|
{columns.map((c) => (
|
||||||
|
<option key={c.key} value={c.key}>
|
||||||
|
{c.label || c.key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</CPSelect>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="이미지 컬럼">
|
||||||
|
<CPSelect
|
||||||
|
value={current.cardColumnMapping?.imageColumn || ""}
|
||||||
|
onChange={(v) =>
|
||||||
|
patch({
|
||||||
|
cardColumnMapping: {
|
||||||
|
...current.cardColumnMapping,
|
||||||
|
imageColumn: v || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">선택...</option>
|
||||||
|
{columns.map((c) => (
|
||||||
|
<option key={c.key} value={c.key}>
|
||||||
|
{c.label || c.key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</CPSelect>
|
||||||
|
</CPRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CPGroup>
|
||||||
|
|
||||||
|
<CPGroup title="액션 버튼" defaultOpen={false}>
|
||||||
|
<CPRow label="액션 표시">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showActions ?? false}
|
||||||
|
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showActions: v } })}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
{current.cardStyle?.showActions && (
|
||||||
|
<>
|
||||||
|
<CPRow label="보기">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showViewButton ?? false}
|
||||||
|
onChange={(v) =>
|
||||||
|
patch({ cardStyle: { ...current.cardStyle, showViewButton: v } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="편집">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showEditButton ?? false}
|
||||||
|
onChange={(v) =>
|
||||||
|
patch({ cardStyle: { ...current.cardStyle, showEditButton: v } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
<CPRow label="삭제">
|
||||||
|
<CPSwitch
|
||||||
|
value={current.cardStyle?.showDeleteButton ?? false}
|
||||||
|
onChange={(v) =>
|
||||||
|
patch({ cardStyle: { ...current.cardStyle, showDeleteButton: v } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CPRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CPGroup>
|
||||||
</CPSection>
|
</CPSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user