Skip to content

Commit 2058987

Browse files
geonhwiiiclaude
andcommitted
feat: 반복 일정 생성/관리 로직 구현
TDD Red-Green-Refactor 사이클로 구현: - createRepeatEvents: EventForm을 반복 설정에 따라 여러 Event로 생성 - updateSingleRepeatEvent: 반복 일정 중 하나를 단일 일정으로 변경 - deleteSingleRepeatEvent: 반복 일정 중 특정 일정만 삭제 - markRepeatEvents: 반복 일정 시각적 구분을 위한 메타데이터 추가 - EventWithDisplay 타입 추가로 UI 표시 지원 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent bb75252 commit 2058987

File tree

2 files changed

+324
-1
lines changed

2 files changed

+324
-1
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { describe, test, expect } from 'vitest';
2+
3+
import { Event, EventForm } from '../../types';
4+
import { createRepeatEvents, updateSingleRepeatEvent, deleteSingleRepeatEvent, markRepeatEvents } from '../../utils/dateUtils';
5+
6+
describe('반복 일정 분할 생성', () => {
7+
test('반복 설정이 있는 이벤트는 여러 개의 개별 이벤트로 생성된다', () => {
8+
// Given: 매월 반복 설정이 있는 EventForm
9+
const baseEventForm: EventForm = {
10+
title: '매월 회의',
11+
date: '2024-01-15',
12+
startTime: '10:00',
13+
endTime: '11:00',
14+
description: '매월 정기 회의',
15+
location: '회의실 A',
16+
category: '업무',
17+
repeat: {
18+
type: 'monthly',
19+
interval: 1,
20+
endDate: '2024-04-15',
21+
},
22+
notificationTime: 10,
23+
};
24+
25+
// When: createRepeatEvents 함수 호출
26+
const events = createRepeatEvents(baseEventForm);
27+
28+
// Then: 반복 날짜별로 개별 Event 객체들이 생성됨 (각각 고유 ID 포함)
29+
expect(events).toHaveLength(4); // 1월, 2월, 3월, 4월
30+
expect(events[0].date).toBe('2024-01-15');
31+
expect(events[1].date).toBe('2024-02-15');
32+
expect(events[2].date).toBe('2024-03-15');
33+
expect(events[3].date).toBe('2024-04-15');
34+
35+
// 모든 이벤트는 고유 ID를 가져야 함
36+
const ids = events.map(event => event.id);
37+
const uniqueIds = new Set(ids);
38+
expect(uniqueIds.size).toBe(events.length);
39+
40+
// 모든 이벤트는 동일한 기본 정보를 가져야 함
41+
events.forEach(event => {
42+
expect(event.title).toBe(baseEventForm.title);
43+
expect(event.startTime).toBe(baseEventForm.startTime);
44+
expect(event.endTime).toBe(baseEventForm.endTime);
45+
expect(event.description).toBe(baseEventForm.description);
46+
expect(event.location).toBe(baseEventForm.location);
47+
expect(event.category).toBe(baseEventForm.category);
48+
expect(event.notificationTime).toBe(baseEventForm.notificationTime);
49+
});
50+
});
51+
52+
test('반복 없음 설정 이벤트는 단일 이벤트로만 생성된다', () => {
53+
// Given: repeat.type이 'none'인 EventForm
54+
const baseEventForm: EventForm = {
55+
title: '단일 일정',
56+
date: '2024-01-15',
57+
startTime: '14:00',
58+
endTime: '15:00',
59+
description: '한번만 실행되는 일정',
60+
location: '카페',
61+
category: '개인',
62+
repeat: {
63+
type: 'none',
64+
interval: 1,
65+
},
66+
notificationTime: 5,
67+
};
68+
69+
// When: createRepeatEvents 함수 호출
70+
const events = createRepeatEvents(baseEventForm);
71+
72+
// Then: 원본 이벤트 하나만 반환됨
73+
expect(events).toHaveLength(1);
74+
expect(events[0].date).toBe('2024-01-15');
75+
expect(events[0].title).toBe(baseEventForm.title);
76+
expect(events[0].repeat.type).toBe('none');
77+
});
78+
});
79+
80+
describe('반복 일정 개별 처리', () => {
81+
test('반복 일정 중 하나를 수정하면 단일 일정으로 변경된다', () => {
82+
// Given: 반복으로 생성된 여러 이벤트 중 특정 이벤트 ID
83+
const existingEvents: Event[] = [
84+
{
85+
id: 'repeat-1',
86+
title: '매주 운동',
87+
date: '2024-01-01',
88+
startTime: '07:00',
89+
endTime: '08:00',
90+
description: '주간 운동',
91+
location: '체육관',
92+
category: '건강',
93+
repeat: { type: 'weekly', interval: 1 },
94+
notificationTime: 30,
95+
},
96+
{
97+
id: 'repeat-2',
98+
title: '매주 운동',
99+
date: '2024-01-08',
100+
startTime: '07:00',
101+
endTime: '08:00',
102+
description: '주간 운동',
103+
location: '체육관',
104+
category: '건강',
105+
repeat: { type: 'weekly', interval: 1 },
106+
notificationTime: 30,
107+
},
108+
{
109+
id: 'repeat-3',
110+
title: '매주 운동',
111+
date: '2024-01-15',
112+
startTime: '07:00',
113+
endTime: '08:00',
114+
description: '주간 운동',
115+
location: '체육관',
116+
category: '건강',
117+
repeat: { type: 'weekly', interval: 1 },
118+
notificationTime: 30,
119+
},
120+
];
121+
122+
const updates = {
123+
title: '특별 운동 세션',
124+
startTime: '08:00',
125+
endTime: '09:30',
126+
description: '개별 수정된 운동',
127+
};
128+
129+
// When: updateSingleRepeatEvent 함수로 해당 이벤트 수정
130+
const updatedEvents = updateSingleRepeatEvent(existingEvents, 'repeat-2', updates);
131+
132+
// Then: 해당 이벤트의 repeat.type이 'none'으로 변경되고 내용이 업데이트됨
133+
const updatedEvent = updatedEvents.find(event => event.id === 'repeat-2');
134+
expect(updatedEvent).toBeDefined();
135+
expect(updatedEvent!.repeat.type).toBe('none');
136+
expect(updatedEvent!.title).toBe('특별 운동 세션');
137+
expect(updatedEvent!.startTime).toBe('08:00');
138+
expect(updatedEvent!.endTime).toBe('09:30');
139+
expect(updatedEvent!.description).toBe('개별 수정된 운동');
140+
141+
// 다른 이벤트들은 변경되지 않아야 함
142+
const otherEvents = updatedEvents.filter(event => event.id !== 'repeat-2');
143+
otherEvents.forEach(event => {
144+
expect(event.repeat.type).toBe('weekly');
145+
expect(event.title).toBe('매주 운동');
146+
});
147+
});
148+
149+
test('반복 일정 중 하나를 삭제하면 해당 이벤트만 삭제된다', () => {
150+
// Given: 반복으로 생성된 여러 이벤트가 저장된 상태
151+
const existingEvents: Event[] = [
152+
{
153+
id: 'daily-1',
154+
title: '매일 독서',
155+
date: '2024-01-01',
156+
startTime: '20:00',
157+
endTime: '21:00',
158+
description: '일일 독서 시간',
159+
location: '집',
160+
category: '자기계발',
161+
repeat: { type: 'daily', interval: 1 },
162+
notificationTime: 15,
163+
},
164+
{
165+
id: 'daily-2',
166+
title: '매일 독서',
167+
date: '2024-01-02',
168+
startTime: '20:00',
169+
endTime: '21:00',
170+
description: '일일 독서 시간',
171+
location: '집',
172+
category: '자기계발',
173+
repeat: { type: 'daily', interval: 1 },
174+
notificationTime: 15,
175+
},
176+
{
177+
id: 'daily-3',
178+
title: '매일 독서',
179+
date: '2024-01-03',
180+
startTime: '20:00',
181+
endTime: '21:00',
182+
description: '일일 독서 시간',
183+
location: '집',
184+
category: '자기계발',
185+
repeat: { type: 'daily', interval: 1 },
186+
notificationTime: 15,
187+
},
188+
];
189+
190+
// When: deleteSingleRepeatEvent 함수로 특정 이벤트 삭제
191+
const remainingEvents = deleteSingleRepeatEvent(existingEvents, 'daily-2');
192+
193+
// Then: 해당 이벤트만 삭제되고 나머지 반복 이벤트들은 유지됨
194+
expect(remainingEvents).toHaveLength(2);
195+
expect(remainingEvents.find(event => event.id === 'daily-2')).toBeUndefined();
196+
expect(remainingEvents.find(event => event.id === 'daily-1')).toBeDefined();
197+
expect(remainingEvents.find(event => event.id === 'daily-3')).toBeDefined();
198+
199+
// 남은 이벤트들은 여전히 반복 설정을 유지해야 함
200+
remainingEvents.forEach(event => {
201+
expect(event.repeat.type).toBe('daily');
202+
});
203+
});
204+
});
205+
206+
describe('반복 일정 시각적 구분 처리', () => {
207+
test('반복 이벤트는 시각적 구분을 위한 플래그를 가진다', () => {
208+
// Given: 반복 설정으로 생성된 이벤트들
209+
const events: Event[] = [
210+
{
211+
id: 'repeat-1',
212+
title: '반복 일정',
213+
date: '2024-01-01',
214+
startTime: '10:00',
215+
endTime: '11:00',
216+
description: '',
217+
location: '',
218+
category: '',
219+
repeat: { type: 'weekly', interval: 1 },
220+
notificationTime: 0,
221+
},
222+
{
223+
id: 'single-1',
224+
title: '단일 일정',
225+
date: '2024-01-02',
226+
startTime: '14:00',
227+
endTime: '15:00',
228+
description: '',
229+
location: '',
230+
category: '',
231+
repeat: { type: 'none', interval: 1 },
232+
notificationTime: 0,
233+
},
234+
];
235+
236+
// When: 이벤트 목록을 확인할 때
237+
const markedEvents = markRepeatEvents(events);
238+
239+
// Then: 각 반복 이벤트는 isRepeatEvent 플래그가 true로 설정됨
240+
const repeatEvent = markedEvents.find(event => event.id === 'repeat-1');
241+
const singleEvent = markedEvents.find(event => event.id === 'single-1');
242+
243+
expect(repeatEvent).toHaveProperty('isRepeatEvent', true);
244+
expect(singleEvent).toHaveProperty('isRepeatEvent', false);
245+
});
246+
247+
test('단일 수정된 반복 이벤트는 반복 표시가 제거된다', () => {
248+
// Given: 반복으로 생성된 이벤트 중 하나가 수정된 상태
249+
const events: Event[] = [
250+
{
251+
id: 'modified-repeat',
252+
title: '수정된 일정',
253+
date: '2024-01-01',
254+
startTime: '10:00',
255+
endTime: '11:00',
256+
description: '단일 수정됨',
257+
location: '',
258+
category: '',
259+
repeat: { type: 'none', interval: 1 }, // 단일 수정으로 인해 'none'으로 변경됨
260+
notificationTime: 0,
261+
},
262+
];
263+
264+
// When: 해당 이벤트의 상태를 확인할 때
265+
const markedEvents = markRepeatEvents(events);
266+
267+
// Then: isRepeatEvent 플래그가 false로 변경됨
268+
const modifiedEvent = markedEvents.find(event => event.id === 'modified-repeat');
269+
expect(modifiedEvent).toHaveProperty('isRepeatEvent', false);
270+
});
271+
});

src/utils/dateUtils.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Event } from '../types.ts';
1+
import { Event, EventForm } from '../types.ts';
22

33
/**
44
* 주어진 년도와 월의 일수를 반환합니다.
@@ -188,3 +188,55 @@ export function generateRepeatDates(event: Event, endDate: Date): string[] {
188188

189189
return dates;
190190
}
191+
192+
function generateId(): string {
193+
return Math.random().toString(36).substr(2, 9);
194+
}
195+
196+
export function createRepeatEvents(eventForm: EventForm): Event[] {
197+
const baseEvent: Event = {
198+
...eventForm,
199+
id: generateId(),
200+
};
201+
202+
if (eventForm.repeat.type === 'none') {
203+
return [baseEvent];
204+
}
205+
206+
const endDate = eventForm.repeat.endDate ? new Date(eventForm.repeat.endDate) : new Date('2025-06-30');
207+
const repeatDates = generateRepeatDates(baseEvent, endDate);
208+
209+
return repeatDates.map(date => ({
210+
...baseEvent,
211+
id: generateId(),
212+
date,
213+
}));
214+
}
215+
216+
export function updateSingleRepeatEvent(events: Event[], eventId: string, updates: Partial<Event>): Event[] {
217+
return events.map(event => {
218+
if (event.id === eventId) {
219+
return {
220+
...event,
221+
...updates,
222+
repeat: { type: 'none', interval: 1 },
223+
};
224+
}
225+
return event;
226+
});
227+
}
228+
229+
export function deleteSingleRepeatEvent(events: Event[], eventId: string): Event[] {
230+
return events.filter(event => event.id !== eventId);
231+
}
232+
233+
export interface EventWithDisplay extends Event {
234+
isRepeatEvent?: boolean;
235+
}
236+
237+
export function markRepeatEvents(events: Event[]): EventWithDisplay[] {
238+
return events.map(event => ({
239+
...event,
240+
isRepeatEvent: event.repeat.type !== 'none',
241+
}));
242+
}

0 commit comments

Comments
 (0)