Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9b8b022
과제 제출을 위한 빈 커밋 날리기
seunghoonKang Oct 27, 2025
342b073
[feat] bmad-method 활용한 에이전트 생성
seunghoonKang Oct 29, 2025
85edc3b
[feat] 에이전트 내용 한글로 변경
seunghoonKang Oct 29, 2025
497f18c
[feat] 현재 상태로 테스트 한 번 돌려보기
seunghoonKang Oct 29, 2025
7483e7b
[feat] 현재 상태 테스트 돌린 결과
seunghoonKang Oct 29, 2025
1c91426
[feat] 프롬프트 수정하여 에이전트 재작성
seunghoonKang Oct 30, 2025
b1cf86d
[기능설계] docs: API 구조 정보 추가 - server.js 참고 가이드 포함
seunghoonKang Oct 30, 2025
3ceb6a0
[feat] server.js 가 있다는 것을 추가로 알려줌
seunghoonKang Oct 30, 2025
155c957
[테스트설계] docs: 테스트 네이밍 규칙 추가 - describe 영어, it 한글
seunghoonKang Oct 30, 2025
54c9a2a
[에이전트구조] refactor: TDD 사이클 명확화 - Red-Green-Refactor
seunghoonKang Oct 30, 2025
9166e67
[에이전트구조] docs: 기존 리소스 활용 가이드 추가 - hooks & utils
seunghoonKang Oct 30, 2025
b6a96af
[기능설계] docs: 반복 일정 기능 명세서 작성
seunghoonKang Oct 30, 2025
11b1a1a
[테스트설계] docs: 반복 일정 기능 테스트 계획서 작성
seunghoonKang Oct 30, 2025
e98deed
test: 반복 일정 핵심 기능 테스트 추가 (Phase 1)
seunghoonKang Oct 30, 2025
522e39d
test: 반복 일정 에러 처리 테스트 추가 (Phase 2)
seunghoonKang Oct 30, 2025
dfbe1ad
test: 반복 일정 통합 테스트 추가 (Phase 3)
seunghoonKang Oct 30, 2025
1aaeca4
test: 반복 일정 UI 엣지 케이스 테스트 추가 (Phase 4)
seunghoonKang Oct 30, 2025
389ad90
docs: 테스트 작성 완료 및 코드 작성 에이전트 인수인계 문서 작성
seunghoonKang Oct 30, 2025
2af4b95
feat: 반복 일정 기능 구현
seunghoonKang Oct 30, 2025
d6945c2
[fix] mui icons import 오류 해결
seunghoonKang Oct 30, 2025
30b2061
fix: 스낵바 메시지 통일 및 반복 유형 초기화 처리
seunghoonKang Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 156 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Notifications, ChevronLeft, ChevronRight, Delete, Edit, Close } from '@mui/icons-material';
import ChevronLeft from '@mui/icons-material/ChevronLeft';
import ChevronRight from '@mui/icons-material/ChevronRight';
import Close from '@mui/icons-material/Close';
import Delete from '@mui/icons-material/Delete';
import Edit from '@mui/icons-material/Edit';
import Notifications from '@mui/icons-material/Notifications';
import Repeat from '@mui/icons-material/Repeat';
import {
Alert,
AlertTitle,
Expand Down Expand Up @@ -35,8 +41,7 @@ import { useEventForm } from './hooks/useEventForm.ts';
import { useEventOperations } from './hooks/useEventOperations.ts';
import { useNotifications } from './hooks/useNotifications.ts';
import { useSearch } from './hooks/useSearch.ts';
// import { Event, EventForm, RepeatType } from './types';
import { Event, EventForm } from './types';
import { Event, EventForm, RepeatType } from './types';
import {
formatDate,
formatMonth,
Expand All @@ -46,6 +51,7 @@ import {
getWeeksAtMonth,
} from './utils/dateUtils';
import { findOverlappingEvents } from './utils/eventOverlap';
import { generateInstancesForEvent } from './utils/recurrenceUtils';
import { getTimeErrorMessage } from './utils/timeValidation';

const categories = ['업무', '개인', '가족', '기타'];
Expand Down Expand Up @@ -77,11 +83,11 @@ function App() {
isRepeating,
setIsRepeating,
repeatType,
// setRepeatType,
setRepeatType,
repeatInterval,
// setRepeatInterval,
setRepeatInterval,
repeatEndDate,
// setRepeatEndDate,
setRepeatEndDate,
notificationTime,
setNotificationTime,
startTimeError,
Expand All @@ -94,19 +100,80 @@ function App() {
editEvent,
} = useEventForm();

const { events, saveEvent, deleteEvent } = useEventOperations(Boolean(editingEvent), () =>
setEditingEvent(null)
);
const {
events,
saveEvent,
deleteEvent,
saveRecurringEvents,
updateRecurringSeries,
deleteRecurringSeries,
} = useEventOperations(Boolean(editingEvent), () => setEditingEvent(null));

const { notifications, notifiedEvents, setNotifications } = useNotifications(events);
const { view, setView, currentDate, holidays, navigate } = useCalendarView();
const { searchTerm, filteredEvents, setSearchTerm } = useSearch(events, currentDate, view);

const [isOverlapDialogOpen, setIsOverlapDialogOpen] = useState(false);
const [overlappingEvents, setOverlappingEvents] = useState<Event[]>([]);
const [isRecurringDialogOpen, setIsRecurringDialogOpen] = useState(false);
const [recurringDialogType, setRecurringDialogType] = useState<'edit' | 'delete'>('edit');
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);

const { enqueueSnackbar } = useSnackbar();

const handleEditClick = (event: Event) => {
if (event.repeat.type !== 'none' && event.repeat.id) {
// 반복 일정인 경우 다이얼로그 표시
setSelectedEvent(event);
setRecurringDialogType('edit');
setIsRecurringDialogOpen(true);
} else {
// 단일 일정인 경우 바로 수정 폼 표시
editEvent(event);
}
};

const handleDeleteClick = (event: Event) => {
if (event.repeat.type !== 'none' && event.repeat.id) {
// 반복 일정인 경우 다이얼로그 표시
setSelectedEvent(event);
setRecurringDialogType('delete');
setIsRecurringDialogOpen(true);
} else {
// 단일 일정인 경우 바로 삭제
deleteEvent(event.id);
}
};

const handleSingleEdit = () => {
if (!selectedEvent) return;
setIsRecurringDialogOpen(false);
// repeat.type을 'none'으로 변경하여 단일 일정으로 전환
editEvent({
...selectedEvent,
repeat: { type: 'none', interval: 1 },
});
};

const handleSeriesEdit = () => {
if (!selectedEvent) return;
setIsRecurringDialogOpen(false);
// 전체 시리즈 수정을 위해 수정 폼 표시
editEvent(selectedEvent);
};

const handleSingleDelete = async () => {
if (!selectedEvent) return;
setIsRecurringDialogOpen(false);
await deleteEvent(selectedEvent.id);
};

const handleSeriesDelete = async () => {
if (!selectedEvent || !selectedEvent.repeat.id) return;
setIsRecurringDialogOpen(false);
await deleteRecurringSeries(selectedEvent.repeat.id);
};

const addOrUpdateEvent = async () => {
if (!title || !date || !startTime || !endTime) {
enqueueSnackbar('필수 정보를 모두 입력해주세요.', { variant: 'error' });
Expand Down Expand Up @@ -135,6 +202,42 @@ function App() {
notificationTime,
};

// 반복 일정 생성
if (isRepeating && !editingEvent && repeatEndDate) {
const baseEvent: Event = {
...(eventData as EventForm),
id: '',
};

const rangeStart = new Date(date);
const rangeEnd = new Date(repeatEndDate);

const instances = generateInstancesForEvent(baseEvent, rangeStart, rangeEnd);

// 반복 일정은 겹침 검사를 하지 않음
await saveRecurringEvents(instances);
resetForm();
return;
}

// 반복 시리즈 전체 수정
if (editingEvent && editingEvent.repeat.type !== 'none' && editingEvent.repeat.id) {
const updateData: Partial<Event> = {
title,
date,
startTime,
endTime,
description,
location,
category,
notificationTime,
};
await updateRecurringSeries(editingEvent.repeat.id, updateData);
resetForm();
return;
}

// 단일 일정 또는 수정
const overlapping = findOverlappingEvents(eventData, events);
if (overlapping.length > 0) {
setOverlappingEvents(overlapping);
Expand Down Expand Up @@ -414,7 +517,12 @@ function App() {
control={
<Checkbox
checked={isRepeating}
onChange={(e) => setIsRepeating(e.target.checked)}
onChange={(e) => {
setIsRepeating(e.target.checked);
if (e.target.checked && repeatType === 'none') {
setRepeatType('weekly');
}
}}
/>
}
label="반복 일정"
Expand All @@ -437,15 +545,16 @@ function App() {
</Select>
</FormControl>

{/* ! 반복은 8주차 과제에 포함됩니다. 구현하고 싶어도 참아주세요~ */}
{/* {isRepeating && (
{isRepeating && (
<Stack spacing={2}>
<FormControl fullWidth>
<FormLabel>반복 유형</FormLabel>
<FormLabel htmlFor="repeat-type">반복 유형</FormLabel>
<Select
id="repeat-type"
size="small"
value={repeatType}
onChange={(e) => setRepeatType(e.target.value as RepeatType)}
aria-label="반복 유형"
>
<MenuItem value="daily">매일</MenuItem>
<MenuItem value="weekly">매주</MenuItem>
Expand All @@ -455,27 +564,32 @@ function App() {
</FormControl>
<Stack direction="row" spacing={2}>
<FormControl fullWidth>
<FormLabel>반복 간격</FormLabel>
<FormLabel htmlFor="repeat-interval">반복 간격</FormLabel>
<TextField
id="repeat-interval"
size="small"
type="number"
value={repeatInterval}
onChange={(e) => setRepeatInterval(Number(e.target.value))}
slotProps={{ htmlInput: { min: 1 } }}
aria-label="반복 간격"
/>
</FormControl>
<FormControl fullWidth>
<FormLabel>반복 종료일</FormLabel>
<FormLabel htmlFor="repeat-end-date">반복 종료일</FormLabel>
<TextField
id="repeat-end-date"
size="small"
type="date"
value={repeatEndDate}
onChange={(e) => setRepeatEndDate(e.target.value)}
aria-label="반복 종료일"
slotProps={{ htmlInput: { max: '2025-12-31' } }}
/>
</FormControl>
</Stack>
</Stack>
)} */}
)}

<Button
data-testid="event-submit-button"
Expand Down Expand Up @@ -541,6 +655,9 @@ function App() {
<Stack>
<Stack direction="row" spacing={1} alignItems="center">
{notifiedEvents.includes(event.id) && <Notifications color="error" />}
{event.repeat.type !== 'none' && (
<Repeat aria-label="반복 일정 아이콘" fontSize="small" />
)}
<Typography
fontWeight={notifiedEvents.includes(event.id) ? 'bold' : 'normal'}
color={notifiedEvents.includes(event.id) ? 'error' : 'inherit'}
Expand Down Expand Up @@ -576,10 +693,10 @@ function App() {
</Typography>
</Stack>
<Stack>
<IconButton aria-label="Edit event" onClick={() => editEvent(event)}>
<IconButton aria-label="Edit event" onClick={() => handleEditClick(event)}>
<Edit />
</IconButton>
<IconButton aria-label="Delete event" onClick={() => deleteEvent(event.id)}>
<IconButton aria-label="Delete event" onClick={() => handleDeleteClick(event)}>
<Delete />
</IconButton>
</Stack>
Expand Down Expand Up @@ -632,6 +749,27 @@ function App() {
</DialogActions>
</Dialog>

<Dialog open={isRecurringDialogOpen} onClose={() => setIsRecurringDialogOpen(false)}>
<DialogTitle>
{recurringDialogType === 'edit' ? '반복 일정 수정' : '반복 일정 삭제'}
</DialogTitle>
<DialogContent>
<DialogContentText>
{recurringDialogType === 'edit'
? '해당 일정만 수정하시겠어요?'
: '해당 일정만 삭제하시겠어요?'}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={recurringDialogType === 'edit' ? handleSingleEdit : handleSingleDelete}>
</Button>
<Button onClick={recurringDialogType === 'edit' ? handleSeriesEdit : handleSeriesDelete}>
아니오
</Button>
</DialogActions>
</Dialog>

{notifications.length > 0 && (
<Stack position="fixed" top={16} right={16} spacing={2} alignItems="flex-end">
{notifications.map((notification, index) => (
Expand Down
Loading
Loading