Add UI/UX design philosophy document combining Palantir's information density and Toss's user-centric approach

This commit is contained in:
DDD1542
2026-04-03 15:51:33 +09:00
parent 1348ad118d
commit f92e8729ae
48 changed files with 9797 additions and 446 deletions

View File

@@ -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>