Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 68 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ app.post('/api/events', async (req, res) => {

app.put('/api/events/:id', async (req, res) => {
const events = await getEvents();
const { id } = req.params;
const id = req.params.id;
const eventIndex = events.events.findIndex((event) => event.id === id);
if (eventIndex > -1) {
const newEvents = [...events.events];
Expand All @@ -59,7 +59,7 @@ app.put('/api/events/:id', async (req, res) => {

app.delete('/api/events/:id', async (req, res) => {
const events = await getEvents();
const { id } = req.params;
const id = req.params.id;

fs.writeFileSync(
`${__dirname}/src/__mocks__/response/realEvents.json`,
Expand All @@ -71,6 +71,72 @@ app.delete('/api/events/:id', async (req, res) => {
res.status(204).send();
});

app.post('/api/events-list', async (req, res) => {
const events = await getEvents();
const repeatId = randomUUID();
const newEvents = req.body.events.map((event) => {
const isRepeatEvent = event.repeat.type !== 'none';
return {
id: randomUUID(),
...event,
repeat: {
...event.repeat,
id: isRepeatEvent ? repeatId : undefined,
},
};
});

fs.writeFileSync(
`${__dirname}/src/__mocks__/response/realEvents.json`,
JSON.stringify({
events: [...events.events, ...newEvents],
})
);

res.status(201).json(newEvents);
});

app.put('/api/events-list', async (req, res) => {
const events = await getEvents();
let isUpdated = false;

const newEvents = [...events.events];
req.body.events.forEach((event) => {
const eventIndex = events.events.findIndex((target) => target.id === event.id);
if (eventIndex > -1) {
isUpdated = true;
newEvents[eventIndex] = { ...events.events[eventIndex], ...event };
}
});

if (isUpdated) {
fs.writeFileSync(
`${__dirname}/src/__mocks__/response/realEvents.json`,
JSON.stringify({
events: newEvents,
})
);

res.json(events.events);
} else {
res.status(404).send('Event not found');
}
});

app.delete('/api/events-list', async (req, res) => {
const events = await getEvents();
const newEvents = events.events.filter((event) => !req.body.eventIds.includes(event.id)); // ? ids를 전달하면 해당 아이디를 기준으로 events에서 제거

fs.writeFileSync(
`${__dirname}/src/__mocks__/response/realEvents.json`,
JSON.stringify({
events: newEvents,
})
);

res.status(204).send();
});

app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
13 changes: 6 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,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 Down Expand Up @@ -77,11 +76,11 @@ function App() {
isRepeating,
setIsRepeating,
repeatType,
// setRepeatType,
setRepeatType,
repeatInterval,
// setRepeatInterval,
setRepeatInterval,
repeatEndDate,
// setRepeatEndDate,
setRepeatEndDate,
notificationTime,
setNotificationTime,
startTimeError,
Expand Down Expand Up @@ -438,7 +437,7 @@ function App() {
</FormControl>

{/* ! 반복은 8주차 과제에 포함됩니다. 구현하고 싶어도 참아주세요~ */}
{/* {isRepeating && (
{isRepeating && (
<Stack spacing={2}>
<FormControl fullWidth>
<FormLabel>반복 유형</FormLabel>
Expand Down Expand Up @@ -475,7 +474,7 @@ function App() {
</FormControl>
</Stack>
</Stack>
)} */}
)}

<Button
data-testid="event-submit-button"
Expand Down
194 changes: 194 additions & 0 deletions src/__tests__/unit/repeatEventUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { EventForm, Event } from '../../types.ts';
import { formatDate } from '../../utils/dateUtils.ts';
import {
isLeapYear,
fixEndDate,
REPEAT_MAX_END_DATE,
generateDailyDates,
generateWeeklyDates,
generateMonthlyDates,
generateYearlyDates,
buildRecurringEvents,
isRecurring,
toSingleEventForm, removeEventById,

Check failure on line 13 in src/__tests__/unit/repeatEventUtils.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Insert `⏎·`
} from '../../utils/repeatUtils.ts';
import { makeEventForm } from '../utils.ts';

describe('반복 일정 Unit Test', () => {
it('해가 윤년이라면 true를 반환한다.', () => {
expect(isLeapYear(2024)).toBe(true);
expect(isLeapYear(2025)).toBe(false);
expect(isLeapYear(2000)).toBe(true);
expect(isLeapYear(1900)).toBe(false);
});

it('종료일 미지정/상한 초과 시 2025-10-30으로 반영한다.', () => {
expect(formatDate(fixEndDate())).toBe('2025-10-30');
expect(formatDate(fixEndDate('2025-10-15'))).toBe('2025-10-15');
expect(formatDate(fixEndDate('2026-01-01'))).toBe('2025-10-30');
expect(formatDate(REPEAT_MAX_END_DATE)).toBe('2025-10-30');
});

it('일정 날짜 구하기 - 일 단위로 날짜를 반환한다.', () => {
const startDate = '2025-10-28';
const endDate = '2025-10-30';
const dates = generateDailyDates(startDate, endDate);
expect(dates).toEqual(['2025-10-28', '2025-10-29', '2025-10-30']);
});

it('일정 날짜 구하기 - 주 단위로 날짜를 반환한다.', () => {
const startDate = '2025-10-10';
const endDate = '2025-10-30';
const dates = generateWeeklyDates(startDate, endDate);
expect(dates).toEqual(['2025-10-10', '2025-10-17', '2025-10-24']);
});

it('일정 날짜 구하기 - 월 단위로 날짜를 반환한다.', () => {
const datesWith31 = generateMonthlyDates('2025-08-31', '2025-11-30');
expect(datesWith31).toEqual(['2025-08-31', '2025-10-31']);
expect(datesWith31).toHaveLength(2);
expect(datesWith31).not.toContain('2025-09-30');
expect(datesWith31).not.toContain('2025-11-30');

const datesWith30 = generateMonthlyDates('2025-04-30', '2025-06-30');
expect(datesWith30).toEqual(['2025-04-30', '2025-05-30', '2025-06-30']);
expect(datesWith30).toContain('2025-05-30');
expect(datesWith30).not.toContain('2025-05-31');
});

it('일정 날짜 구하기 - 연 단위로 날짜를 반환한다.', () => {
const datesWithLeap = generateYearlyDates('2024-02-29', '2030-12-31');
expect(datesWithLeap).toEqual(['2024-02-29', '2028-02-29']);
expect(datesWithLeap).toHaveLength(2);
expect(datesWithLeap).not.toContain('2025-02-29');
expect(datesWithLeap).not.toContain('2026-02-29');

const datesWithoutLeap = generateYearlyDates('2025-02-28', '2027-03-01');
expect(datesWithoutLeap).toEqual(['2025-02-28', '2026-02-28', '2027-02-28']);
expect(datesWithoutLeap).toContain('2026-02-28');
});

it('반복된 일정에 맞춰 이벤트 생성하기 - daily', () => {
const eventFormData: EventForm = {
title: 'Daily 일정 만들어랏',
date: '2025-10-28', // 시작일
startTime: '09:00',
endTime: '10:00',
description: '',
location: '',
category: 'default',
repeat: { type: 'daily', interval: 1 },
notificationTime: 0,
};

const events = buildRecurringEvents(eventFormData);

// 만들어진 날짜 확인
expect(events.map((event: EventForm) => event.date)).toEqual([
'2025-10-28',
'2025-10-29',
'2025-10-30',
]);
// 개수 확인
expect(events).toHaveLength(3);
// 일부 필드 보존 확인
expect(events[0].title).toBe('Daily 일정 만들어랏');
expect(events[0].startTime).toBe('09:00');
expect(events[0].endTime).toBe('10:00');
// 반복 타입은 유지 (아이콘 표시용)
expect(events.every((event: EventForm) => event.repeat?.type === 'daily')).toBe(true);
});

it('단일 수정/아이콘 - 반복 해제 시 반복 아이콘이 사라진다.', () => {
const eventFormData = makeEventForm({
date: '2025-10-01',
repeat: { type: 'daily', endDate: '2025-10-22', interval: 77 },
title: 'daily인척 daily인 일정',
});

// 반복 아이콘 flag용
expect(isRecurring(eventFormData)).toBe(true);

const singleEventForm = toSingleEventForm(eventFormData);

// 반복 설정 해제
expect(singleEventForm.repeat.type).toBe('none');
expect(singleEventForm.repeat.interval).toBe(1);
expect(singleEventForm.repeat.endDate).toBeUndefined();

// 나머지 필드는 그대로 유지
expect(singleEventForm.title).toBe(eventFormData.title);
expect(singleEventForm.date).toBe(eventFormData.date);
expect(singleEventForm.startTime).toBe(eventFormData.startTime);
expect(singleEventForm.endTime).toBe(eventFormData.endTime);

// 반복 아이콘 해제
expect(isRecurring(singleEventForm)).toBe(false);
});

it('이미 비반복인 일정에 단일 수정 적용해도 동일하게 아이콘은 보이지 않는다.', () => {
const nonRepeatEventForm = makeEventForm({
repeat: { type: 'none', interval: 1, endDate: undefined },
});

expect(isRecurring(nonRepeatEventForm)).toBe(false);

const singleEventForm = toSingleEventForm(nonRepeatEventForm);

expect(singleEventForm.repeat.type).toBe('none');
expect(singleEventForm.repeat.endDate).toBeUndefined();
expect(singleEventForm.date).toBe(nonRepeatEventForm.date);
expect(singleEventForm.title).toBe(nonRepeatEventForm.title);
});

it('단일 삭제 - 해당 id만 제거된다.', () => {
const event1: Event = {
id: '1',
title: '반복1-id1',
date: '2025-10-01',
startTime: '09:00',
endTime: '10:00',
description: '',
location: '',
category: 'default',
repeat: { type: 'weekly', interval: 1, endDate: '2025-10-22' },
notificationTime: 0,
};
const event2: Event = {
id: '2',
title: '반복1-id2',
date: '2025-10-08',
startTime: '09:00',
endTime: '10:00',
description: '',
location: '',
category: 'default',
repeat: { type: 'weekly', interval: 1, endDate: '2025-10-22' },
notificationTime: 0,
};
const event3: Event = {
id: '3',
title: '단일-id3',
date: '2025-10-05',
startTime: '13:00',
endTime: '14:00',
description: '',
location: '',
category: 'default',
repeat: { type: 'none', interval: 1 },
notificationTime: 0,
};

const original = [event1, event2, event3];
const next = removeEventById(original, '2');

// id가 2인 이벤트만 제거됨
expect(next).toHaveLength(2);
expect(next.some((event: Event) => event.id === '1')).toBe(true);
expect(next.some((event: Event) => event.id === '2')).toBe(false);
expect(next.some((event: Event) => event.id === '3')).toBe(true);

// 원본은 변하지 않는다
expect(original).toHaveLength(3);
});
});
13 changes: 13 additions & 0 deletions src/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EventForm } from '../types';
import { fillZero } from '../utils/dateUtils';

export const assertDate = (date1: Date, date2: Date) => {
Expand All @@ -10,3 +11,15 @@ export const parseHM = (timestamp: number) => {
const m = fillZero(date.getMinutes());
return `${h}:${m}`;
};

export const makeEventForm = (overrides: Partial<EventForm> = {}): EventForm => ({
title: overrides.title ?? '테스트 일정',
date: overrides.date ?? '2025-10-28',
startTime: overrides.startTime ?? '09:00',
endTime: overrides.endTime ?? '10:00',
description: overrides.description ?? '',
location: overrides.location ?? '',
category: overrides.category ?? 'default',
repeat: overrides.repeat ?? { type: 'none', interval: 1, endDate: undefined },
notificationTime: overrides.notificationTime ?? 0,
});
Loading
Loading