- EnhancedSections 공통 컴포넌트 추출 (SectionCard, StatItem, StatusBadge 등) - 전 섹션(매출/매입/생산/출근/미출하/건설/캘린더/일보 등) 공통 패턴 적용 - components.tsx 공통 UI 컴포넌트 강화 - CLAUDE.md Git Workflow 섹션 추가 (develop/stage/main 플로우) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
598 lines
23 KiB
TypeScript
598 lines
23 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useMemo, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Plus, ExternalLink, ChevronLeft, ChevronRight, CalendarDays } from 'lucide-react';
|
|
import { ScheduleCalendar } from '@/components/common/ScheduleCalendar';
|
|
import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types';
|
|
import { getCalendarEventsForYear, type CalendarEvent } from '@/constants/calendarEvents';
|
|
import { useCalendarScheduleStore } from '@/stores/useCalendarScheduleStore';
|
|
import { CollapsibleDashboardCard } from '../components';
|
|
import type {
|
|
CalendarScheduleItem,
|
|
CalendarViewType,
|
|
CalendarDeptFilterType,
|
|
CalendarTaskFilterType,
|
|
TodayIssueListItem,
|
|
TodayIssueListBadgeType,
|
|
} from '../types';
|
|
|
|
interface CalendarSectionProps {
|
|
schedules: CalendarScheduleItem[];
|
|
issues?: TodayIssueListItem[];
|
|
onScheduleClick?: (schedule: CalendarScheduleItem) => void;
|
|
onScheduleEdit?: (schedule: CalendarScheduleItem) => void;
|
|
}
|
|
|
|
// 일정 타입별 색상
|
|
const SCHEDULE_TYPE_COLORS: Record<string, string> = {
|
|
schedule: 'blue',
|
|
order: 'green',
|
|
construction: 'purple',
|
|
issue: 'red',
|
|
other: 'gray',
|
|
holiday: 'red',
|
|
tax: 'orange',
|
|
};
|
|
|
|
// 이슈 뱃지별 색상
|
|
const ISSUE_BADGE_COLORS: Record<TodayIssueListBadgeType, string> = {
|
|
'수주등록': 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
|
'추심이슈': 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
|
'안전재고': 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
|
|
'지출 승인대기': 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
|
|
'세금 신고': 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
|
|
'결재 요청': 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300',
|
|
'신규거래처': 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
|
'입금': 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300',
|
|
'출금': 'bg-pink-100 text-pink-700 dark:bg-pink-900/40 dark:text-pink-300',
|
|
'기타': 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
|
|
};
|
|
|
|
// 부서 필터 옵션
|
|
const DEPT_FILTER_OPTIONS: { value: CalendarDeptFilterType; label: string }[] = [
|
|
{ value: 'all', label: '전체' },
|
|
{ value: 'department', label: '부' },
|
|
{ value: 'personal', label: '개인' },
|
|
];
|
|
|
|
// 업무 필터 옵션 (이슈 추가)
|
|
type ExtendedTaskFilterType = CalendarTaskFilterType | 'issue';
|
|
const TASK_FILTER_OPTIONS: { value: ExtendedTaskFilterType; label: string }[] = [
|
|
{ value: 'all', label: '전체' },
|
|
{ value: 'schedule', label: '일정' },
|
|
{ value: 'order', label: '발주' },
|
|
{ value: 'construction', label: '시공' },
|
|
{ value: 'issue', label: '이슈' },
|
|
];
|
|
|
|
export function CalendarSection({
|
|
schedules,
|
|
issues = [],
|
|
onScheduleClick,
|
|
onScheduleEdit,
|
|
}: CalendarSectionProps) {
|
|
const router = useRouter();
|
|
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
|
|
const [currentDate, setCurrentDate] = useState(new Date());
|
|
const [viewType, setViewType] = useState<CalendarViewType>('month');
|
|
const [deptFilter, setDeptFilter] = useState<CalendarDeptFilterType>('all');
|
|
const [taskFilter, setTaskFilter] = useState<ExtendedTaskFilterType>('all');
|
|
|
|
// 스토어에서 공휴일/세무일정 가져오기 (API 연동)
|
|
const schedulesByYear = useCalendarScheduleStore((s) => s.schedulesByYear);
|
|
// TODO: 백엔드 API(/api/v1/calendar-schedules) 구현 후 주석 해제
|
|
// const fetchSchedules = useCalendarScheduleStore((s) => s.fetchSchedules);
|
|
// useEffect(() => {
|
|
// const year = currentDate.getFullYear();
|
|
// fetchSchedules(year);
|
|
// }, [currentDate, fetchSchedules]);
|
|
|
|
// 날짜가 있는 이슈만 필터링
|
|
const issuesWithDate = useMemo(() => {
|
|
return issues.filter((issue) => issue.date);
|
|
}, [issues]);
|
|
|
|
// 필터링된 스케줄
|
|
const filteredSchedules = useMemo(() => {
|
|
// 이슈 필터일 경우 스케줄 제외
|
|
if (taskFilter === 'issue') {
|
|
return [];
|
|
}
|
|
|
|
let result = schedules;
|
|
|
|
// 업무 필터
|
|
if (taskFilter !== 'all') {
|
|
result = result.filter((s) => s.type === taskFilter);
|
|
}
|
|
|
|
// 부서 필터 (실제 구현시 부서/개인 로직 추가 필요)
|
|
// 현재는 기본 구현만
|
|
|
|
return result;
|
|
}, [schedules, taskFilter, deptFilter]);
|
|
|
|
// 필터링된 이슈
|
|
const filteredIssues = useMemo(() => {
|
|
// 이슈 필터가 아니고 all도 아닌 경우 이슈 제외
|
|
if (taskFilter !== 'all' && taskFilter !== 'issue') {
|
|
return [];
|
|
}
|
|
return issuesWithDate;
|
|
}, [issuesWithDate, taskFilter]);
|
|
|
|
// 현재 연도의 공휴일/세금일정 (스토어 우선, 폴백 상수)
|
|
const staticEvents: ScheduleEvent[] = useMemo(() => {
|
|
const year = currentDate.getFullYear();
|
|
const events = getCalendarEventsForYear(year);
|
|
|
|
return events.map((event: CalendarEvent) => ({
|
|
id: `${event.type}-${event.date}`,
|
|
title: event.type === 'holiday' ? `🔴 ${event.name}` : `🟠 ${event.name}`,
|
|
startDate: event.date,
|
|
endDate: event.date,
|
|
color: SCHEDULE_TYPE_COLORS[event.type] || 'gray',
|
|
data: { ...event, _type: event.type as 'holiday' | 'tax' },
|
|
}));
|
|
// schedulesByYear를 의존성에 추가하여 스토어 갱신 시 리렌더링
|
|
}, [currentDate, schedulesByYear]);
|
|
|
|
// ScheduleCalendar용 이벤트 변환 (스케줄 + 이슈 + 공휴일/세금 통합)
|
|
const calendarEvents: ScheduleEvent[] = useMemo(() => {
|
|
const scheduleEvents = filteredSchedules.map((schedule) => ({
|
|
id: schedule.id,
|
|
// 기획서: [부서명] 제목 형식
|
|
title: schedule.department ? `[${schedule.department}] ${schedule.title}` : schedule.title,
|
|
startDate: schedule.startDate,
|
|
endDate: schedule.endDate,
|
|
color: SCHEDULE_TYPE_COLORS[schedule.type] || 'gray',
|
|
data: { ...schedule, _type: 'schedule' as const },
|
|
}));
|
|
|
|
const issueEvents = filteredIssues.map((issue) => ({
|
|
id: issue.id,
|
|
title: `[${issue.badge}] ${issue.content}`,
|
|
startDate: issue.date!,
|
|
endDate: issue.date!,
|
|
color: 'red',
|
|
data: { ...issue, _type: 'issue' as const },
|
|
}));
|
|
|
|
return [...staticEvents, ...scheduleEvents, ...issueEvents];
|
|
}, [staticEvents, filteredSchedules, filteredIssues]);
|
|
|
|
// 선택된 날짜의 일정 + 이슈 + 공휴일/세금 목록
|
|
const selectedDateItems = useMemo(() => {
|
|
if (!selectedDate) return { schedules: [], issues: [], staticEvents: [] };
|
|
// 로컬 타임존 기준으로 날짜 문자열 생성 (UTC 변환 방지)
|
|
const year = selectedDate.getFullYear();
|
|
const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
|
|
const day = String(selectedDate.getDate()).padStart(2, '0');
|
|
const dateStr = `${year}-${month}-${day}`;
|
|
|
|
const dateSchedules = filteredSchedules.filter((schedule) => {
|
|
return schedule.startDate <= dateStr && schedule.endDate >= dateStr;
|
|
});
|
|
|
|
const dateIssues = filteredIssues.filter((issue) => issue.date === dateStr);
|
|
|
|
// 공휴일/세금일정
|
|
const dateStaticEvents = staticEvents.filter((event) => event.startDate === dateStr);
|
|
|
|
return { schedules: dateSchedules, issues: dateIssues, staticEvents: dateStaticEvents };
|
|
}, [selectedDate, filteredSchedules, filteredIssues, staticEvents]);
|
|
|
|
// 총 건수 계산
|
|
const totalItemCount = selectedDateItems.schedules.length + selectedDateItems.issues.length + selectedDateItems.staticEvents.length;
|
|
|
|
// 날짜 포맷 (기획서: "1월 6일 화요일")
|
|
const formatSelectedDate = (date: Date) => {
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
const dayNames = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'];
|
|
const dayName = dayNames[date.getDay()];
|
|
return `${month}월 ${day}일 ${dayName}`;
|
|
};
|
|
|
|
// 일정 상세 정보 포맷 (기획서: 부서명 | 날짜 | 시간)
|
|
const formatScheduleDetail = (schedule: CalendarScheduleItem) => {
|
|
const parts: string[] = [];
|
|
|
|
// 부서명 또는 담당자명
|
|
if (schedule.department) {
|
|
parts.push(schedule.department);
|
|
} else if (schedule.personName) {
|
|
parts.push(schedule.personName);
|
|
}
|
|
|
|
// 날짜 (여러 날인 경우)
|
|
if (schedule.startDate !== schedule.endDate) {
|
|
parts.push(`${schedule.startDate}~${schedule.endDate}`);
|
|
}
|
|
|
|
// 시간
|
|
if (schedule.startTime && schedule.endTime) {
|
|
parts.push(`${schedule.startTime} ~ ${schedule.endTime}`);
|
|
} else if (schedule.startTime) {
|
|
parts.push(schedule.startTime);
|
|
}
|
|
|
|
return parts.join(' | ');
|
|
};
|
|
|
|
const handleDateClick = (date: Date) => {
|
|
setSelectedDate(date);
|
|
};
|
|
|
|
const handleEventClick = (event: ScheduleEvent) => {
|
|
const schedule = event.data as CalendarScheduleItem;
|
|
onScheduleClick?.(schedule);
|
|
};
|
|
|
|
const handleMonthChange = (date: Date) => {
|
|
setCurrentDate(date);
|
|
};
|
|
|
|
// 모바일 리스트뷰: 현재 월의 모든 날짜와 이벤트
|
|
const monthDaysWithEvents = useMemo(() => {
|
|
const year = currentDate.getFullYear();
|
|
const month = currentDate.getMonth();
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
const days: Array<{
|
|
date: Date;
|
|
dateStr: string;
|
|
label: string;
|
|
isToday: boolean;
|
|
isWeekend: boolean;
|
|
events: ScheduleEvent[];
|
|
}> = [];
|
|
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
const date = new Date(year, month, d);
|
|
const mm = String(month + 1).padStart(2, '0');
|
|
const dd = String(d).padStart(2, '0');
|
|
const dateStr = `${year}-${mm}-${dd}`;
|
|
const dayOfWeek = date.getDay();
|
|
|
|
const dayEvents = calendarEvents.filter(
|
|
(ev) => ev.startDate <= dateStr && ev.endDate >= dateStr
|
|
);
|
|
|
|
days.push({
|
|
date,
|
|
dateStr,
|
|
label: `${d}일 ${dayNames[dayOfWeek]}요일`,
|
|
isToday: date.getTime() === today.getTime(),
|
|
isWeekend: dayOfWeek === 0 || dayOfWeek === 6,
|
|
events: dayEvents,
|
|
});
|
|
}
|
|
return days;
|
|
}, [currentDate, calendarEvents]);
|
|
|
|
const handleMobilePrevMonth = () => {
|
|
const prev = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1);
|
|
setCurrentDate(prev);
|
|
};
|
|
|
|
const handleMobileNextMonth = () => {
|
|
const next = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
|
|
setCurrentDate(next);
|
|
};
|
|
|
|
return (
|
|
<CollapsibleDashboardCard
|
|
icon={<CalendarDays style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
|
title="캘린더"
|
|
subtitle="일정 관리"
|
|
>
|
|
{/* 모바일: 필터+월네비 */}
|
|
<div className="lg:hidden mb-3">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
{/* 부서 필터 */}
|
|
<Select
|
|
value={deptFilter}
|
|
onValueChange={(value) => setDeptFilter(value as CalendarDeptFilterType)}
|
|
>
|
|
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DEPT_FILTER_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 업무 필터 */}
|
|
<Select
|
|
value={taskFilter}
|
|
onValueChange={(value) => setTaskFilter(value as CalendarTaskFilterType)}
|
|
>
|
|
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TASK_FILTER_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 월 네비게이션 */}
|
|
<div className="flex items-center justify-center gap-4">
|
|
<Button variant="outline" size="sm" className="h-8 px-2" onClick={handleMobilePrevMonth}>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
<span className="text-sm font-semibold text-foreground whitespace-nowrap">
|
|
{currentDate.getFullYear()}년 {currentDate.getMonth() + 1}월
|
|
</span>
|
|
<Button variant="outline" size="sm" className="h-8 px-2" onClick={handleMobileNextMonth}>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 데스크탑: 필터 */}
|
|
<div className="hidden lg:flex items-center justify-end mb-4 gap-2">
|
|
<Select
|
|
value={deptFilter}
|
|
onValueChange={(value) => setDeptFilter(value as CalendarDeptFilterType)}
|
|
>
|
|
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DEPT_FILTER_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select
|
|
value={taskFilter}
|
|
onValueChange={(value) => setTaskFilter(value as CalendarTaskFilterType)}
|
|
>
|
|
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TASK_FILTER_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 모바일: 리스트뷰 */}
|
|
<div className="lg:hidden pt-3">
|
|
|
|
{/* 일별 리스트 */}
|
|
<div className="divide-y divide-border">
|
|
{monthDaysWithEvents.map((day) => {
|
|
const hasEvents = day.events.length > 0;
|
|
const isSelected = selectedDate && day.date.getTime() === selectedDate.getTime();
|
|
return (
|
|
<div
|
|
key={day.dateStr}
|
|
className={`px-3 py-2.5 cursor-pointer transition-colors ${
|
|
isSelected ? 'bg-blue-50 dark:bg-blue-900/30' :
|
|
day.isToday ? 'bg-amber-50 dark:bg-amber-900/30' : ''
|
|
} ${!hasEvents && !day.isToday && !isSelected ? 'opacity-50' : ''}`}
|
|
onClick={() => handleDateClick(day.date)}
|
|
>
|
|
{/* 날짜 + 일정등록 버튼 */}
|
|
<div className="flex items-center justify-between mb-1">
|
|
<div className={`text-sm font-medium ${
|
|
day.isWeekend ? 'text-red-500' : 'text-foreground'
|
|
} ${day.isToday ? 'font-bold' : ''}`}>
|
|
{day.label}
|
|
{day.isToday && <span className="ml-1 text-amber-600 dark:text-amber-400 text-xs font-semibold">(오늘)</span>}
|
|
</div>
|
|
{isSelected && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 text-xs gap-0.5 px-2"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onScheduleEdit?.({
|
|
id: '',
|
|
title: '',
|
|
startDate: day.dateStr,
|
|
endDate: day.dateStr,
|
|
type: 'schedule',
|
|
});
|
|
}}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
일정등록
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 이벤트 목록 (날짜 아래) */}
|
|
{hasEvents ? (
|
|
<div className="space-y-1 pl-1">
|
|
{(isSelected ? day.events : day.events.slice(0, 3)).map((ev) => {
|
|
const evData = ev.data as Record<string, unknown>;
|
|
const evType = evData?._type as string;
|
|
const colorMap: Record<string, string> = {
|
|
holiday: 'bg-red-500',
|
|
tax: 'bg-orange-500',
|
|
schedule: 'bg-blue-500',
|
|
order: 'bg-green-500',
|
|
construction: 'bg-purple-500',
|
|
issue: 'bg-red-400',
|
|
};
|
|
const dotColor = colorMap[evType] || 'bg-gray-400';
|
|
const title = evData?.name as string || evData?.title as string || ev.title;
|
|
const cleanTitle = title?.replace(/^[🔴🟠]\s*/, '') || '';
|
|
return (
|
|
<div key={ev.id} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
<span className={`w-2 h-2 rounded-full shrink-0 ${dotColor}`} />
|
|
<span className={isSelected ? '' : 'truncate'}>{cleanTitle}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
{!isSelected && day.events.length > 3 && (
|
|
<div className="text-xs text-muted-foreground pl-3.5">+{day.events.length - 3}건</div>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 데스크탑: 기존 캘린더 + 상세 */}
|
|
<div className="hidden lg:grid lg:grid-cols-2 gap-6">
|
|
{/* 캘린더 영역 */}
|
|
<div>
|
|
<ScheduleCalendar
|
|
events={calendarEvents}
|
|
currentDate={currentDate}
|
|
selectedDate={selectedDate}
|
|
onDateClick={handleDateClick}
|
|
onEventClick={handleEventClick}
|
|
onMonthChange={handleMonthChange}
|
|
maxEventsPerDay={4}
|
|
weekStartsOn={1}
|
|
className="[&_.weekend]:bg-yellow-50 dark:[&_.weekend]:bg-yellow-900/20"
|
|
/>
|
|
</div>
|
|
|
|
{/* 선택된 날짜 일정 + 이슈 목록 */}
|
|
<div className="border border-border rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h4 className="text-lg font-semibold text-foreground">
|
|
{selectedDate ? formatSelectedDate(selectedDate) : '날짜를 선택하세요'}
|
|
</h4>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 text-xs gap-1"
|
|
onClick={() => {
|
|
const year = selectedDate?.getFullYear() || new Date().getFullYear();
|
|
const month = String((selectedDate?.getMonth() || new Date().getMonth()) + 1).padStart(2, '0');
|
|
const day = String(selectedDate?.getDate() || new Date().getDate()).padStart(2, '0');
|
|
const dateStr = `${year}-${month}-${day}`;
|
|
onScheduleEdit?.({
|
|
id: '',
|
|
title: '',
|
|
startDate: dateStr,
|
|
endDate: dateStr,
|
|
type: 'schedule',
|
|
});
|
|
}}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
일정등록
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground mb-4">
|
|
총 {totalItemCount}건
|
|
</div>
|
|
|
|
{totalItemCount === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
선택한 날짜에 일정이 없습니다.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3 max-h-[calc(100vh-400px)] overflow-y-auto pr-1">
|
|
{selectedDateItems.staticEvents.map((event) => {
|
|
const eventData = event.data as CalendarEvent & { _type: string };
|
|
const isHoliday = eventData.type === 'holiday';
|
|
return (
|
|
<div
|
|
key={event.id}
|
|
className={`p-3 rounded-lg ${
|
|
isHoliday
|
|
? 'bg-red-50 border border-red-200 dark:bg-red-900/30 dark:border-red-800'
|
|
: 'bg-orange-50 border border-orange-200 dark:bg-orange-900/30 dark:border-orange-800'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg">{isHoliday ? '🔴' : '🟠'}</span>
|
|
<span className="font-medium text-foreground">{eventData.name}</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mt-1">
|
|
{isHoliday ? '공휴일' : '세금 신고 마감일'}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{selectedDateItems.schedules.map((schedule) => (
|
|
<div
|
|
key={schedule.id}
|
|
className="p-3 border border-border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer"
|
|
onClick={() => onScheduleClick?.(schedule)}
|
|
>
|
|
<div className="font-medium text-base text-foreground mb-1">{schedule.title}</div>
|
|
<div className="text-sm text-muted-foreground">{formatScheduleDetail(schedule)}</div>
|
|
</div>
|
|
))}
|
|
|
|
{selectedDateItems.issues.map((issue) => (
|
|
<div
|
|
key={issue.id}
|
|
className="p-3 border border-red-200 dark:border-red-800 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors cursor-pointer"
|
|
onClick={() => {
|
|
if (issue.path) router.push(`/ko${issue.path}`);
|
|
}}
|
|
>
|
|
<div className="flex items-start gap-2 mb-1">
|
|
<Badge
|
|
variant="secondary"
|
|
className={`shrink-0 text-xs ${ISSUE_BADGE_COLORS[issue.badge]}`}
|
|
>
|
|
{issue.badge}
|
|
</Badge>
|
|
<span className="font-medium text-sm text-foreground flex-1">{issue.content}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span>{issue.time}</span>
|
|
{issue.path && (
|
|
<span className="flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline">
|
|
상세보기
|
|
<ExternalLink className="h-3 w-3" />
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CollapsibleDashboardCard>
|
|
);
|
|
}
|