Files
sam-react-prod/src/components/molecules/DateRangeSelector.tsx
유병철 c2ed71540f feat(WEB): DatePicker 공통화 및 공정관리/작업자화면 대폭 개선
DatePicker 공통화:
- date-picker.tsx 공통 컴포넌트 신규 추가
- 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일)
- DateRangeSelector 개선

공정관리:
- RuleModal 대폭 리팩토링 (-592줄 → 간소화)
- ProcessForm, StepForm 개선
- ProcessDetail 수정, actions 확장

작업자화면:
- WorkerScreen 기능 대폭 확장 (+543줄)
- WorkItemCard 개선
- types 확장

회계/인사/영업/품질:
- BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용
- EmployeeForm, VacationDialog 등 DatePicker 적용
- OrderRegistration, QuoteRegistration DatePicker 적용
- InspectionCreate, InspectionDetail DatePicker 적용

공사관리/CEO대시보드:
- BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용
- ScheduleDetailModal, TodayIssueSection 개선

기타:
- WorkOrderCreate/Edit/Detail/List 개선
- ShipmentCreate/Edit, ReceivingDetail 개선
- calendar, calendarEvents 수정
- datepicker 마이그레이션 체크리스트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:48:00 +09:00

217 lines
6.6 KiB
TypeScript

'use client';
import { ReactNode, useCallback } from 'react';
import { format, startOfYear, endOfYear, subMonths, startOfMonth, endOfMonth, subDays } from 'date-fns';
import { Button } from '@/components/ui/button';
import { DatePicker } from '@/components/ui/date-picker';
import { ScrollableButtonGroup } from '@/components/atoms/ScrollableButtonGroup';
/**
* 날짜 범위 프리셋 타입
*/
export type DatePreset = 'thisYear' | 'twoMonthsAgo' | 'lastMonth' | 'thisMonth' | 'yesterday' | 'today';
/**
* 프리셋 레이블 (한국어)
*/
const PRESET_LABELS: Record<DatePreset, string> = {
thisYear: '당해년도',
twoMonthsAgo: '전전월',
lastMonth: '전월',
thisMonth: '당월',
yesterday: '어제',
today: '오늘',
};
/**
* 기본 프리셋 순서
*/
const DEFAULT_PRESETS: DatePreset[] = ['today', 'yesterday', 'thisMonth', 'lastMonth', 'twoMonthsAgo', 'thisYear' ];
interface DateRangeSelectorProps {
/** 시작 날짜 (yyyy-MM-dd 형식) */
startDate: string;
/** 종료 날짜 (yyyy-MM-dd 형식) */
endDate: string;
/** 시작 날짜 변경 핸들러 */
onStartDateChange: (date: string) => void;
/** 종료 날짜 변경 핸들러 */
onEndDateChange: (date: string) => void;
/** 표시할 프리셋 목록 (기본: 전체) */
presets?: DatePreset[];
/** 추가 액션 (엑셀 다운로드, 등록 버튼 등) */
extraActions?: ReactNode;
/** 프리셋 버튼 숨김 */
hidePresets?: boolean;
/** 날짜 입력 숨김 */
hideDateInputs?: boolean;
/** 날짜 입력 너비 */
dateInputWidth?: string;
/** 프리셋 버튼 위치: 'inline' (날짜 옆), 'below' (별도 줄) */
presetsPosition?: 'inline' | 'below';
}
/**
* 날짜 범위 선택 컴포넌트
*
* 달력(날짜 입력) + 기간 프리셋 버튼 (당해년도, 전전월, 전월, 당월, 어제, 오늘)
*
* @example
* ```tsx
* <DateRangeSelector
* startDate={startDate}
* endDate={endDate}
* onStartDateChange={setStartDate}
* onEndDateChange={setEndDate}
* extraActions={
* <>
* <Button variant="outline">엑셀 다운로드</Button>
* <Button>등록</Button>
* </>
* }
* />
* ```
*/
export function DateRangeSelector({
startDate,
endDate,
onStartDateChange,
onEndDateChange,
presets = DEFAULT_PRESETS,
extraActions,
hidePresets = false,
hideDateInputs = false,
dateInputWidth = 'w-[140px]',
presetsPosition = 'inline',
}: DateRangeSelectorProps) {
// 프리셋 클릭 핸들러
const handlePresetClick = useCallback((preset: DatePreset) => {
const today = new Date();
switch (preset) {
case 'thisYear':
onStartDateChange(format(startOfYear(today), 'yyyy-MM-dd'));
onEndDateChange(format(endOfYear(today), 'yyyy-MM-dd'));
break;
case 'twoMonthsAgo': {
const twoMonthsAgo = subMonths(today, 2);
onStartDateChange(format(startOfMonth(twoMonthsAgo), 'yyyy-MM-dd'));
onEndDateChange(format(endOfMonth(twoMonthsAgo), 'yyyy-MM-dd'));
break;
}
case 'lastMonth': {
const lastMonth = subMonths(today, 1);
onStartDateChange(format(startOfMonth(lastMonth), 'yyyy-MM-dd'));
onEndDateChange(format(endOfMonth(lastMonth), 'yyyy-MM-dd'));
break;
}
case 'thisMonth':
onStartDateChange(format(startOfMonth(today), 'yyyy-MM-dd'));
onEndDateChange(format(endOfMonth(today), 'yyyy-MM-dd'));
break;
case 'yesterday': {
const yesterday = subDays(today, 1);
onStartDateChange(format(yesterday, 'yyyy-MM-dd'));
onEndDateChange(format(yesterday, 'yyyy-MM-dd'));
break;
}
case 'today':
onStartDateChange(format(today, 'yyyy-MM-dd'));
onEndDateChange(format(today, 'yyyy-MM-dd'));
break;
}
}, [onStartDateChange, onEndDateChange]);
// 프리셋 버튼 렌더링
const renderPresets = () => {
if (hidePresets || presets.length === 0) return null;
return (
<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>
);
};
// presetsPosition이 'below'일 때: 달력+extraActions 같은 줄, 프리셋은 아래 줄
if (presetsPosition === 'below') {
return (
<div className="flex flex-col gap-2 w-full">
{/* 1줄: 날짜 + extraActions */}
<div className="flex items-center gap-2">
{/* 날짜 범위 선택 */}
{!hideDateInputs && (
<div className="flex items-center gap-1 shrink-0">
<DatePicker
value={startDate}
onChange={onStartDateChange}
className="w-[170px]"
size="sm"
displayFormat="yyyy년 MM월 dd일"
/>
<span className="text-muted-foreground shrink-0">~</span>
<DatePicker
value={endDate}
onChange={onEndDateChange}
className="w-[170px]"
size="sm"
displayFormat="yyyy년 MM월 dd일"
/>
</div>
)}
{/* extraActions (검색창 등) */}
{extraActions}
</div>
{/* 2줄: 프리셋 버튼들 */}
{renderPresets()}
</div>
);
}
// presetsPosition이 'inline' (기본값)
// PC(1280px+): 달력 | 프리셋버튼 | 검색창 (한 줄, 넘치면 줄바꿈)
// 태블릿: 달력 / 프리셋버튼 / 검색창 (세 줄)
// Note: w-full 제거 - 부모 컨테이너에서 다른 요소들과 자연스럽게 한 줄에 배치되도록 함
return (
<div className="flex flex-col xl:flex-row xl:flex-wrap xl:items-center gap-2">
{/* 날짜 범위 선택 */}
{!hideDateInputs && (
<div className="flex items-center gap-1 shrink-0">
<DatePicker
value={startDate}
onChange={onStartDateChange}
className="w-[170px]"
size="sm"
displayFormat="yyyy년 MM월 dd일"
/>
<span className="text-muted-foreground shrink-0">~</span>
<DatePicker
value={endDate}
onChange={onEndDateChange}
className="w-[170px]"
size="sm"
displayFormat="yyyy년 MM월 dd일"
/>
</div>
)}
{/* 기간 버튼들 - 달력 바로 옆 */}
{renderPresets()}
{/* extraActions (검색창 등) - 마지막에 배치 */}
{extraActions}
</div>
);
}