diff --git a/CLAUDE.md b/CLAUDE.md index ab938687..fb02e6c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,7 @@ sam_project: frontend: sam_project/sam-next/sma-next-project/sam-react-prod # Next.js (현재) backend_api: sam_project/sam-api/sam-api # PHP Laravel design: sam_project/sam-design/sam-design # React 디자인 시스템 + hotfix: sam_project/sam-hotfix/sam-hotfix # E2E 테스트 결과/핫픽스 관리 특성: 인증 필수 폐쇄형 ERP 시스템 (SEO 불필요) ``` diff --git a/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx index 55dd8cb4..544080de 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/AuditSettingsPanel.tsx @@ -197,7 +197,7 @@ export function SettingsButton({ onClick }: SettingsButtonProps) { - ))} - - + {/* 하단: 검색 입력 + 버튼 */} @@ -85,4 +54,4 @@ export const Filters = ({ ); -}; \ No newline at end of file +}; diff --git a/src/app/[locale]/(protected)/quality/qms/components/Header.tsx b/src/app/[locale]/(protected)/quality/qms/components/Header.tsx index 97b824b0..f36118c5 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/Header.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/Header.tsx @@ -6,13 +6,13 @@ interface HeaderProps { export const Header = ({ rightContent }: HeaderProps) => { return ( -
-
-

품질인정심사 시스템

-

SAM - Smart Automation Management

+
+
+

품질인정심사 시스템

+

SAM - Smart Automation Management

{rightContent && ( -
+
{rightContent}
)} diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx index 0c6d65ee..c92289eb 100644 --- a/src/app/[locale]/(protected)/quality/qms/page.tsx +++ b/src/app/[locale]/(protected)/quality/qms/page.tsx @@ -49,7 +49,7 @@ export default function QualityInspectionPage() { // 2일차(로트추적) 필터 상태 const [selectedYear, setSelectedYear] = useState(2025); - const [selectedQuarter, setSelectedQuarter] = useState('전체'); + const [selectedQuarter, setSelectedQuarter] = useState<'Q1' | 'Q2' | 'Q3' | 'Q4' | '전체'>('전체'); const [searchTerm, setSearchTerm] = useState(''); // 2일차 선택 상태 @@ -153,7 +153,7 @@ export default function QualityInspectionPage() { return MOCK_REPORTS.filter((report) => { if (report.year !== selectedYear) return false; if (selectedQuarter !== '전체') { - const quarterNum = parseInt(selectedQuarter.replace('분기', '')); + const quarterNum = parseInt(selectedQuarter.replace('Q', '')); if (report.quarterNum !== quarterNum) return false; } if (searchTerm) { @@ -198,7 +198,7 @@ export default function QualityInspectionPage() { setSelectedRoute(null); }; - const handleQuarterChange = (quarter: string) => { + const handleQuarterChange = (quarter: 'Q1' | 'Q2' | 'Q3' | 'Q4' | '전체') => { setSelectedQuarter(quarter); setSelectedReport(null); setSelectedRoute(null); diff --git a/src/components/atoms/ScrollableButtonGroup.tsx b/src/components/atoms/ScrollableButtonGroup.tsx new file mode 100644 index 00000000..4cbcce8e --- /dev/null +++ b/src/components/atoms/ScrollableButtonGroup.tsx @@ -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 ( +
+
+ {children} +
+
+ ); +} diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index 180124c3..038b6415 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -2,4 +2,6 @@ export { BadgeSm, getQuoteStatusBadge } from "./BadgeSm"; export type { BadgeSmVariant } from "./BadgeSm"; export { TabChip } from "./TabChip"; -export type { TabChipProps } from "./TabChip"; \ No newline at end of file +export type { TabChipProps } from "./TabChip"; + +export { ScrollableButtonGroup } from "./ScrollableButtonGroup"; \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement.tsx b/src/components/items/ItemMasterDataManagement.tsx index c689d1e4..ec50b3dc 100644 --- a/src/components/items/ItemMasterDataManagement.tsx +++ b/src/components/items/ItemMasterDataManagement.tsx @@ -473,9 +473,12 @@ function ItemMasterDataManagementContent() { icon={Database} /> - -
- + +
+ {customTabs.sort((a, b) => a.order - b.order).map(tab => { const Icon = getTabIcon(tab.icon); return ( diff --git a/src/components/molecules/DateRangeSelector.tsx b/src/components/molecules/DateRangeSelector.tsx index e5a925aa..7a4de51d 100644 --- a/src/components/molecules/DateRangeSelector.tsx +++ b/src/components/molecules/DateRangeSelector.tsx @@ -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 ( -
-
- {presets.map((preset) => ( - - ))} -
-
+ + {presets.map((preset) => ( + + ))} + ); }; diff --git a/src/components/molecules/YearQuarterFilter.tsx b/src/components/molecules/YearQuarterFilter.tsx new file mode 100644 index 00000000..8589affc --- /dev/null +++ b/src/components/molecules/YearQuarterFilter.tsx @@ -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 ( +
+ + + + {quarters.map((q) => ( + + ))} + +
+ ); +} diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index cd0d5be5..45531d36 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -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"; \ No newline at end of file +export type { StandardDialogProps, ConfirmDialogProps, FormDialogProps } from "./StandardDialog"; + +export { YearQuarterFilter } from "./YearQuarterFilter"; +export type { Quarter } from "./YearQuarterFilter"; \ No newline at end of file diff --git a/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx b/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx index 7d578901..948830da 100644 --- a/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx +++ b/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx @@ -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([]); - // ===== 연도 옵션 ===== - 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, @@ -267,35 +247,15 @@ export function PerformanceReportList() { // ===== 연도/분기 필터 슬롯 (dateRangeSelector.extraActions) ===== const quarterFilterSlot = useMemo( () => ( -
- -
- {(['전체', 'Q1', 'Q2', 'Q3', 'Q4'] as const).map((q) => ( - - ))} -
-
+ ), - [year, quarter, yearOptions, handleQuarterChange] + [year, quarter] ); // ===== UniversalListPage Config =====