Add UI/UX design philosophy document combining Palantir's information density and Toss's user-centric approach
This commit is contained in:
@@ -57,12 +57,20 @@ export interface GroupSetting {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface BaseFilter {
|
||||
columnName: string;
|
||||
operator: "equals" | "contains" | "in";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TableSettings {
|
||||
columns: ColumnSetting[];
|
||||
filters: FilterSetting[];
|
||||
groups: GroupSetting[];
|
||||
frozenCount: number;
|
||||
groupSumEnabled: boolean;
|
||||
/** 기본 데이터 필터 (예: division = '판매') */
|
||||
baseFilter?: BaseFilter;
|
||||
}
|
||||
|
||||
export interface TableSettingsModalProps {
|
||||
@@ -181,16 +189,17 @@ function SortableColumnRow({
|
||||
<div className="text-xs text-muted-foreground truncate">{col.columnName}</div>
|
||||
</div>
|
||||
|
||||
{/* 너비 입력 */}
|
||||
{/* 너비 입력 (0 = 자동) */}
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<span className="text-xs text-muted-foreground">너비:</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={col.width}
|
||||
onChange={(e) => onWidthChange(col._idx, Number(e.target.value) || 100)}
|
||||
value={col.width || ""}
|
||||
onChange={(e) => onWidthChange(col._idx, Number(e.target.value) || 0)}
|
||||
className="h-8 w-[70px] text-xs text-center"
|
||||
min={50}
|
||||
min={0}
|
||||
max={500}
|
||||
placeholder="자동"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,6 +226,8 @@ export function TableSettingsModal({
|
||||
const [tempGroups, setTempGroups] = useState<GroupSetting[]>([]);
|
||||
const [tempFrozenCount, setTempFrozenCount] = useState(0);
|
||||
const [tempGroupSum, setTempGroupSum] = useState(false);
|
||||
const [tempBaseFilter, setTempBaseFilter] = useState<BaseFilter | undefined>();
|
||||
const [baseFilterOptions, setBaseFilterOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
// 원본 컬럼 (초기화용)
|
||||
const [defaultColumns, setDefaultColumns] = useState<ColumnSetting[]>([]);
|
||||
@@ -245,7 +256,7 @@ export function TableSettingsModal({
|
||||
columnName: t.columnName,
|
||||
displayName: t.displayName || t.columnLabel || t.columnName,
|
||||
visible: defaultVisibleKeys ? defaultVisibleKeys.includes(t.columnName) : true,
|
||||
width: 120,
|
||||
width: 0, // 0 = 자동 너비
|
||||
}));
|
||||
|
||||
// 활성 컬럼을 GRID_COLUMNS 순서대로 위에, 비활성을 아래에 정렬
|
||||
@@ -310,12 +321,14 @@ export function TableSettingsModal({
|
||||
}));
|
||||
setTempFrozenCount(saved.frozenCount || 0);
|
||||
setTempGroupSum(saved.groupSumEnabled || false);
|
||||
setTempBaseFilter(saved.baseFilter);
|
||||
} else {
|
||||
setTempColumns(freshColumns);
|
||||
setTempFilters(freshFilters);
|
||||
setTempGroups(freshGroups);
|
||||
setTempFrozenCount(0);
|
||||
setTempGroupSum(false);
|
||||
setTempBaseFilter(undefined);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("테이블 설정 로드 실패:", err);
|
||||
@@ -332,6 +345,7 @@ export function TableSettingsModal({
|
||||
groups: tempGroups,
|
||||
frozenCount: tempFrozenCount,
|
||||
groupSumEnabled: tempGroupSum,
|
||||
baseFilter: tempBaseFilter,
|
||||
};
|
||||
localStorage.setItem(getStorageKey(settingsId), JSON.stringify(settings));
|
||||
onSave?.(settings);
|
||||
@@ -499,32 +513,41 @@ export function TableSettingsModal({
|
||||
|
||||
{/* ===== 탭 2: 필터 설정 ===== */}
|
||||
<TabsContent value="filters" className="mt-0 pt-3 overflow-y-auto max-h-[calc(80vh-220px)]">
|
||||
{/* 전체 선택 */}
|
||||
<div
|
||||
className="flex items-center gap-2 px-2 pb-3 border-b mb-2 cursor-pointer"
|
||||
onClick={() => toggleFilterAll(!allFiltersEnabled)}
|
||||
>
|
||||
<Checkbox checked={allFiltersEnabled} />
|
||||
<span className="text-sm">전체 선택</span>
|
||||
{/* 검색 필터 설정 (상단) */}
|
||||
<div className="flex items-center justify-between px-2 pb-2 border-b mb-2 shrink-0">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => toggleFilterAll(!allFiltersEnabled)}
|
||||
>
|
||||
<Checkbox checked={allFiltersEnabled} />
|
||||
<span className="text-sm font-medium">검색 필터</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{tempFilters.filter((f) => f.enabled).length}/{tempFilters.length}개 활성
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 필터 목록 */}
|
||||
<div className="space-y-1">
|
||||
{/* 필터 목록 — 2열 그리드 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{tempFilters.map((filter, idx) => (
|
||||
<div
|
||||
key={filter.columnName}
|
||||
className="flex items-center gap-3 py-1.5 px-2 hover:bg-muted/50 rounded"
|
||||
className={cn(
|
||||
"flex items-center gap-2 py-1.5 px-2 rounded hover:bg-muted/50 cursor-pointer",
|
||||
filter.enabled && "bg-primary/5",
|
||||
)}
|
||||
onClick={() => toggleFilter(idx)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={filter.enabled}
|
||||
onCheckedChange={() => toggleFilter(idx)}
|
||||
/>
|
||||
<Checkbox checked={filter.enabled} className="shrink-0" />
|
||||
<div className="flex-1 text-sm min-w-0 truncate">{filter.displayName}</div>
|
||||
<Select
|
||||
value={filter.filterType}
|
||||
onValueChange={(v) => changeFilterType(idx, v as any)}
|
||||
onValueChange={(v) => { changeFilterType(idx, v as any); }}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[90px] text-xs">
|
||||
<SelectTrigger
|
||||
className="h-7 w-[70px] text-[11px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -535,23 +558,99 @@ export function TableSettingsModal({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Input
|
||||
type="number"
|
||||
value={filter.width}
|
||||
onChange={(e) => changeFilterWidth(idx, Number(e.target.value) || 25)}
|
||||
className="h-8 w-[55px] text-xs text-center"
|
||||
min={10}
|
||||
max={100}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 기본 데이터 필터 (하단) */}
|
||||
<div className="mt-4 rounded-lg border p-3 space-y-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">기본 데이터 필터</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
이 화면에서 항상 적용되는 데이터 필터 조건
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tempBaseFilter ? (
|
||||
<div className="flex items-center gap-2 rounded-md bg-primary/5 px-3 py-2">
|
||||
<div className="flex-1 text-sm">
|
||||
<span className="font-mono text-xs text-muted-foreground">{tableName}</span>
|
||||
{" — "}
|
||||
<span className="font-medium">{tempBaseFilter.columnName}</span>
|
||||
{" = "}
|
||||
<span className="text-primary font-medium">{tempBaseFilter.value || "(미설정)"}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive"
|
||||
onClick={() => setTempBaseFilter(undefined)}
|
||||
>
|
||||
해제
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground px-1">필터 없음</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={tempBaseFilter?.columnName || ""}
|
||||
onValueChange={async (col) => {
|
||||
setTempBaseFilter({ columnName: col, operator: "equals", value: "" });
|
||||
// 해당 컬럼의 카테고리 옵션 로드
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${tableName}/${col}/values`);
|
||||
const vals = res.data?.data || [];
|
||||
const flatten = (arr: any[]): { label: string; value: string }[] => {
|
||||
const result: { label: string; value: string }[] = [];
|
||||
for (const v of arr) {
|
||||
result.push({ value: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
setBaseFilterOptions(flatten(vals));
|
||||
} catch {
|
||||
setBaseFilterOptions([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||
<SelectValue placeholder="필터 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tempFilters.map((f) => (
|
||||
<SelectItem key={f.columnName} value={f.columnName}>
|
||||
{f.displayName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={tempBaseFilter?.value || ""}
|
||||
onValueChange={(val) =>
|
||||
setTempBaseFilter((prev) => prev ? { ...prev, value: val } : prev)
|
||||
}
|
||||
disabled={!tempBaseFilter?.columnName}
|
||||
>
|
||||
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||
<SelectValue placeholder="필터 값 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{baseFilterOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 그룹별 합산 토글 */}
|
||||
<div className="mt-4 flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="mt-3 flex items-center justify-between rounded-lg border p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">그룹별 합산</div>
|
||||
<div className="text-xs text-muted-foreground">같은 값끼리 그룹핑하여 합산</div>
|
||||
|
||||
Reference in New Issue
Block a user