feat: 신규 페이지 구현 및 HR/설정 기능 개선
신규 페이지: - 회계관리: 거래처, 예상비용, 청구서, 발주서 - 게시판: 공지사항, 자료실, 커뮤니티 - 고객센터: 문의/FAQ - 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역 - 리포트 (차트 시각화) - 개발자 테스트 URL 페이지 기능 개선: - HR 직원관리/휴가관리/카드관리 강화 - IntegratedListTemplateV2 확장 - AuthenticatedLayout 패딩 표준화 - 로그인 페이지 UI 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
src/components/molecules/DateRangeSelector.tsx
Normal file
176
src/components/molecules/DateRangeSelector.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* 날짜 범위 프리셋 타입
|
||||
*/
|
||||
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[] = ['thisYear', 'twoMonthsAgo', 'lastMonth', 'thisMonth', 'yesterday', 'today'];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 범위 선택 컴포넌트
|
||||
*
|
||||
* 달력(날짜 입력) + 기간 프리셋 버튼 (당해년도, 전전월, 전월, 당월, 어제, 오늘)
|
||||
*
|
||||
* @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]',
|
||||
}: 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]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{/* 상단: 날짜 선택 + 기간 버튼 */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
{/* 날짜 범위 선택 (Input type="date") */}
|
||||
{!hideDateInputs && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => onStartDateChange(e.target.value)}
|
||||
className="w-[130px] sm:w-[140px]"
|
||||
/>
|
||||
<span className="text-muted-foreground shrink-0">~</span>
|
||||
<Input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => onEndDateChange(e.target.value)}
|
||||
className="w-[130px] sm:w-[140px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 기간 버튼들 - 모바일에서 가로 스크롤 */}
|
||||
{!hidePresets && presets.length > 0 && (
|
||||
<div
|
||||
className="overflow-x-auto -mx-1 px-1"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단: 추가 액션 버튼들 */}
|
||||
{extraActions && (
|
||||
<div className="flex items-center gap-2 flex-wrap sm:justify-end">
|
||||
{extraActions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user