Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-02-04 22:41:04 +09:00
12 changed files with 205 additions and 122 deletions

View File

@@ -0,0 +1,52 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
const GAP_MAP = {
sm: 'gap-0.5',
md: 'gap-1',
lg: 'gap-2',
} as const;
const SCROLL_UNTIL_MAP = {
sm: 'sm:overflow-visible',
md: 'md:overflow-visible',
lg: 'lg:overflow-visible',
xl: 'xl:overflow-visible',
} as const;
interface ScrollableButtonGroupProps {
children: ReactNode;
/** 버튼 간격 (default: 'md') */
gap?: keyof typeof GAP_MAP;
/** 이 breakpoint 이상에서 스크롤 해제 (default: 'xl') */
scrollUntil?: keyof typeof SCROLL_UNTIL_MAP;
className?: string;
}
/**
* 가로 스크롤 버튼 컨테이너
*
* 좁은 화면에서 overflow-x-auto + 히든 스크롤바,
* scrollUntil breakpoint 이상에서 일반 레이아웃 전환.
*/
export function ScrollableButtonGroup({
children,
gap = 'md',
scrollUntil = 'xl',
className,
}: ScrollableButtonGroupProps) {
return (
<div
className={cn(
'overflow-x-auto min-w-0',
SCROLL_UNTIL_MAP[scrollUntil],
className,
)}
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<div className={cn('flex items-center min-w-max [&::-webkit-scrollbar]:hidden', GAP_MAP[gap])}>
{children}
</div>
</div>
);
}

View File

@@ -2,4 +2,6 @@ export { BadgeSm, getQuoteStatusBadge } from "./BadgeSm";
export type { BadgeSmVariant } from "./BadgeSm";
export { TabChip } from "./TabChip";
export type { TabChipProps } from "./TabChip";
export type { TabChipProps } from "./TabChip";
export { ScrollableButtonGroup } from "./ScrollableButtonGroup";

View File

@@ -473,9 +473,12 @@ function ItemMasterDataManagementContent() {
icon={Database}
/>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<div className="flex items-center gap-2 mb-4">
<TabsList className="flex-1">
<Tabs value={activeTab} onValueChange={setActiveTab} className="min-w-0 overflow-x-hidden">
<div
className="mb-4 min-w-0 overflow-x-auto [&::-webkit-scrollbar]:hidden"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<TabsList>
{customTabs.sort((a, b) => a.order - b.order).map(tab => {
const Icon = getTabIcon(tab.icon);
return (

View File

@@ -4,6 +4,7 @@ import { ReactNode, useCallback } from 'react';
import { format, startOfYear, endOfYear, subMonths, startOfMonth, endOfMonth, subDays } from 'date-fns';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { ScrollableButtonGroup } from '@/components/atoms/ScrollableButtonGroup';
/**
* 날짜 범위 프리셋 타입
@@ -126,24 +127,19 @@ export function DateRangeSelector({
const renderPresets = () => {
if (hidePresets || presets.length === 0) return null;
return (
<div
className="overflow-x-auto -mx-1 px-1 xl:overflow-visible xl:mx-0 xl:px-0"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<div className="flex items-center gap-1 min-w-max [&::-webkit-scrollbar]:hidden">
{presets.map((preset) => (
<Button
key={preset}
variant="outline"
size="sm"
onClick={() => handlePresetClick(preset)}
className="shrink-0 text-xs sm:text-sm px-2 sm:px-3"
>
{PRESET_LABELS[preset]}
</Button>
))}
</div>
</div>
<ScrollableButtonGroup>
{presets.map((preset) => (
<Button
key={preset}
variant="outline"
size="sm"
onClick={() => handlePresetClick(preset)}
className="shrink-0 text-xs sm:text-sm px-2 sm:px-3"
>
{PRESET_LABELS[preset]}
</Button>
))}
</ScrollableButtonGroup>
);
};

View File

@@ -0,0 +1,97 @@
'use client';
import { useMemo } from 'react';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { ScrollableButtonGroup } from '@/components/atoms/ScrollableButtonGroup';
import { cn } from '@/lib/utils';
/** 분기 타입 */
export type Quarter = 'Q1' | 'Q2' | 'Q3' | 'Q4';
const QUARTER_OPTIONS = ['전체', 'Q1', 'Q2', 'Q3', 'Q4'] as const;
function quarterLabel(q: typeof QUARTER_OPTIONS[number]): string {
return q === '전체' ? '전체' : `${q.replace('Q', '')}분기`;
}
interface YearQuarterFilterProps {
/** 선택된 연도 */
year: number;
/** 선택된 분기 ('전체' 포함) */
quarter: Quarter | '전체';
/** 연도 변경 핸들러 */
onYearChange: (year: number) => void;
/** 분기 변경 핸들러 */
onQuarterChange: (quarter: Quarter | '전체') => void;
/** 연도 범위 [시작, 끝] (default: [현재-5, 현재]) */
yearRange?: [number, number];
/** '전체' 옵션 포함 여부 (default: true) */
includeAll?: boolean;
className?: string;
}
/**
* 연도 Select + 분기 버튼 그룹 필터
*
* Controlled 컴포넌트. 연도/분기 state는 부모에서 관리.
*/
export function YearQuarterFilter({
year,
quarter,
onYearChange,
onQuarterChange,
yearRange,
includeAll = true,
className,
}: YearQuarterFilterProps) {
const currentYear = new Date().getFullYear();
const [startYear, endYear] = yearRange ?? [currentYear - 5, currentYear];
const yearOptions = useMemo(() => {
const years: number[] = [];
for (let y = endYear; y >= startYear; y--) {
years.push(y);
}
return years;
}, [startYear, endYear]);
const quarters = includeAll ? QUARTER_OPTIONS : QUARTER_OPTIONS.slice(1);
return (
<div className={cn('flex items-center gap-2 flex-wrap min-w-0', className)}>
<Select value={String(year)} onValueChange={(v) => onYearChange(Number(v))}>
<SelectTrigger className="w-[100px] h-9 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{yearOptions.map((y) => (
<SelectItem key={y} value={String(y)}>
{y}
</SelectItem>
))}
</SelectContent>
</Select>
<ScrollableButtonGroup>
{quarters.map((q) => (
<Button
key={q}
size="sm"
variant={quarter === q ? 'default' : 'outline'}
className="h-8 px-3 text-xs shrink-0"
onClick={() => onQuarterChange(q)}
>
{quarterLabel(q)}
</Button>
))}
</ScrollableButtonGroup>
</div>
);
}

View File

@@ -7,4 +7,7 @@ export { TableActions } from "./TableActions";
export type { TableAction } from "./TableActions";
export { StandardDialog, ConfirmDialog, FormDialog } from "./StandardDialog";
export type { StandardDialogProps, ConfirmDialogProps, FormDialogProps } from "./StandardDialog";
export type { StandardDialogProps, ConfirmDialogProps, FormDialogProps } from "./StandardDialog";
export { YearQuarterFilter } from "./YearQuarterFilter";
export type { Quarter } from "./YearQuarterFilter";

View File

@@ -29,13 +29,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { TableCell, TableRow } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { YearQuarterFilter } from '@/components/molecules/YearQuarterFilter';
import {
UniversalListPage,
type UniversalListConfig,
@@ -93,15 +87,6 @@ export function PerformanceReportList() {
// ===== 현재 데이터 추적 (메모 모달 개소 계산용) =====
const [currentData, setCurrentData] = useState<ReportListItem[]>([]);
// ===== 연도 옵션 =====
const yearOptions = useMemo(() => {
const years = [];
for (let y = currentYear; y >= currentYear - 5; y--) {
years.push(y);
}
return years;
}, [currentYear]);
// ===== 통계 로드 =====
useEffect(() => {
const loadStats = async () => {
@@ -118,11 +103,6 @@ export function PerformanceReportList() {
loadStats();
}, [year, quarter, refreshKey]);
// ===== 분기 버튼 클릭 =====
const handleQuarterChange = useCallback((q: Quarter | '전체') => {
setQuarter(q);
}, []);
// ===== 액션 핸들러 =====
const handleConfirm = useCallback(async (
selectedItems: Set<string>,
@@ -267,35 +247,15 @@ export function PerformanceReportList() {
// ===== 연도/분기 필터 슬롯 (dateRangeSelector.extraActions) =====
const quarterFilterSlot = useMemo(
() => (
<div className="flex items-center gap-2 flex-wrap order-first">
<Select value={String(year)} onValueChange={(v) => setYear(Number(v))}>
<SelectTrigger className="w-[100px] h-9 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{yearOptions.map((y) => (
<SelectItem key={y} value={String(y)}>
{y}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex gap-1">
{(['전체', 'Q1', 'Q2', 'Q3', 'Q4'] as const).map((q) => (
<Button
key={q}
size="sm"
variant={quarter === q ? 'default' : 'outline'}
className="h-8 px-3 text-xs"
onClick={() => handleQuarterChange(q)}
>
{q === '전체' ? '전체' : `${q.replace('Q', '')}분기`}
</Button>
))}
</div>
</div>
<YearQuarterFilter
year={year}
quarter={quarter}
onYearChange={setYear}
onQuarterChange={setQuarter}
className="order-first"
/>
),
[year, quarter, yearOptions, handleQuarterChange]
[year, quarter]
);
// ===== UniversalListPage Config =====