Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0f8cc19
과제시작
xxziiko Aug 23, 2025
916737c
fix: server 파일 수정
xxziiko Aug 27, 2025
1bf65c9
test: generateRepeatDates 테스트 케이스
xxziiko Aug 27, 2025
0f78436
chore: repeatEvent -> repeatEventUtils파일 이름 변경
xxziiko Aug 27, 2025
d837931
feat: 매일 반복 로직 추가
xxziiko Aug 27, 2025
e2a1428
feat: 매주 반복 로직 추가
xxziiko Aug 27, 2025
6e97e49
test: 중복 테스트 제거
xxziiko Aug 27, 2025
4df8acc
feat: 매월, 매년 반복일정 생성 로직 추가
xxziiko Aug 27, 2025
384e70e
test: 반복일정 표시 테스트 작성
xxziiko Aug 27, 2025
53a9bbf
test: 반복일정 아이콘 테스트 케이스 작성
xxziiko Aug 27, 2025
3e7078c
feat: 반복 일정 아이콘 표시 기능
xxziiko Aug 27, 2025
79e0b7d
test: 반복 종료 테스트 케이스 작성
xxziiko Aug 27, 2025
0ed5d25
test: 반복 일정 단일 수정 유닛 테스트 추가
xxziiko Aug 27, 2025
c5b1be3
test: 반복일정 함수 테스트 추가
xxziiko Aug 28, 2025
508657c
feat: 반복일정 유닛 함수 추가
xxziiko Aug 28, 2025
2c0c66a
refactor: createEvent 함수 적용
xxziiko Aug 28, 2025
f0a2f5a
chore: 불필요한 테스트 삭제
xxziiko Aug 28, 2025
07c9fbc
test: 반복 일정 단일 수정 유닛 테스트 수정
xxziiko Aug 28, 2025
41348ce
test: 반복 일정 단일 수정 유닛 테스트 추가
xxziiko Aug 28, 2025
4517bef
feat: updateEventRepeat 생성
xxziiko Aug 28, 2025
d97362c
test: 테스트 expect 추가
xxziiko Aug 28, 2025
979c01e
feat: 반복 유형 선택 기능 테스트 추가 - 매일/매주/매월/매년 반복 유형 선택 - 반복 간격 설정 - 반복 종료일 설정
xxziiko Aug 28, 2025
e1240ba
feat: 반복 일정 생성 로직 구현 완성 - 매일/매주/매월/매년 반복 일정 생성 로직 구현 - 윤년 및 경계값 처리 로직…
xxziiko Aug 28, 2025
0cd9660
test: 단일 이벤트 수정 유닛 테스트
xxziiko Aug 28, 2025
280a8b6
feat: 반복 일정 단일 수정 기능 구현 완성 - isRepeatEvent 함수로 반복 일정 여부 확인 - createSi…
xxziiko Aug 28, 2025
eaf31db
test: 반복 일정 단일 삭제 유닛 테스트
xxziiko Aug 28, 2025
e851e95
feat: 반복 일정 중 특정 일정만 삭제하는 함수
xxziiko Aug 28, 2025
d6648c5
fix: 반복 일정 생성 로직을 요구사항에 맞게 수정 - 31일에 매월 반복 시 31일이 없는 달은 건너뛰기 - 2월 29일…
xxziiko Aug 28, 2025
fce10a9
refactor: 테스트 코드 수정
xxziiko Aug 28, 2025
d4cc985
test: 반복일정 통합테스트 추가
xxziiko Aug 28, 2025
16c2f0a
test: 반복 일정 통합 기능
xxziiko Aug 28, 2025
82a950f
refactor: set 적용
xxziiko Aug 28, 2025
caa61d5
refactor: 통합테스트 개선
xxziiko Aug 28, 2025
95cb3b2
refactor: 통합테스트 개선
xxziiko Aug 28, 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
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}`);
});
42 changes: 25 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ import {
import { useSnackbar } from 'notistack';
import { useState } from 'react';

import { RepeatEventIcon } from './components/RepeatEventIcon';
import { useCalendarView } from './hooks/useCalendarView.ts';
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 +46,7 @@ import {
getWeeksAtMonth,
} from './utils/dateUtils';
import { findOverlappingEvents } from './utils/eventOverlap';
import { getRepeatDisplayInfo } from './utils/eventStateUtils';
import { getTimeErrorMessage } from './utils/timeValidation';

const categories = ['업무', '개인', '가족', '기타'];
Expand Down Expand Up @@ -77,11 +78,11 @@ function App() {
isRepeating,
setIsRepeating,
repeatType,
// setRepeatType,
setRepeatType,
repeatInterval,
// setRepeatInterval,
setRepeatInterval,
repeatEndDate,
// setRepeatEndDate,
setRepeatEndDate,
notificationTime,
setNotificationTime,
startTimeError,
Expand Down Expand Up @@ -201,6 +202,11 @@ function App() {
>
<Stack direction="row" spacing={1} alignItems="center">
{isNotified && <Notifications fontSize="small" />}
<RepeatEventIcon
repeatType={event.repeat.type}
interval={event.repeat.interval}
endDate={event.repeat.endDate}
/>
<Typography
variant="caption"
noWrap
Expand Down Expand Up @@ -288,6 +294,11 @@ function App() {
>
<Stack direction="row" spacing={1} alignItems="center">
{isNotified && <Notifications fontSize="small" />}
<RepeatEventIcon
repeatType={event.repeat.type}
interval={event.repeat.interval}
endDate={event.repeat.endDate}
/>
<Typography
variant="caption"
noWrap
Expand Down Expand Up @@ -437,12 +448,12 @@ 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)}
Expand All @@ -455,8 +466,9 @@ 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}
Expand All @@ -465,8 +477,9 @@ function App() {
/>
</FormControl>
<FormControl fullWidth>
<FormLabel>반복 종료일</FormLabel>
<FormLabel htmlFor="repeat-end-date">반복 종료일</FormLabel>
<TextField
id="repeat-end-date"
size="small"
type="date"
value={repeatEndDate}
Expand All @@ -475,7 +488,7 @@ function App() {
</FormControl>
</Stack>
</Stack>
)} */}
)}

<Button
data-testid="event-submit-button"
Expand Down Expand Up @@ -557,12 +570,7 @@ function App() {
<Typography>카테고리: {event.category}</Typography>
{event.repeat.type !== 'none' && (
<Typography>
반복: {event.repeat.interval}
{event.repeat.type === 'daily' && '일'}
{event.repeat.type === 'weekly' && '주'}
{event.repeat.type === 'monthly' && '월'}
{event.repeat.type === 'yearly' && '년'}
마다
반복: {getRepeatDisplayInfo(event).repeatText}
{event.repeat.endDate && ` (종료: ${event.repeat.endDate})`}
</Typography>
)}
Expand Down
105 changes: 105 additions & 0 deletions src/__tests__/medium.integration.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,108 @@ it('notificationTime을 10으로 하면 지정 시간 10분 전 알람 텍스트

expect(screen.getByText('10분 후 기존 회의 일정이 시작됩니다.')).toBeInTheDocument();
});

describe('반복 일정 통합 기능', () => {
it('반복 일정 체크박스가 존재한다', async () => {
setup(<App />);

// 반복 일정 체크박스가 존재해야 함
expect(screen.getByLabelText('반복 일정')).toBeInTheDocument();
});

it('반복 일정 체크박스 클릭 후 상태를 확인한다', async () => {
const { user } = setup(<App />);

// 반복 일정 체크박스 클릭
const repeatCheckbox = screen.getByLabelText('반복 일정');
expect(repeatCheckbox).toBeInTheDocument();

// 클릭 전 상태 확인
expect(repeatCheckbox).not.toBeChecked();

// 클릭
await user.click(repeatCheckbox);

// 클릭 후 상태 확인
expect(repeatCheckbox).toBeChecked();

// 현재 DOM 상태 출력 (디버깅용)
console.log('Current DOM after checkbox click:', document.body.innerHTML);
});

it('반복 일정 체크박스를 클릭하면 반복 설정 UI가 표시된다', async () => {
const { user } = setup(<App />);

// 반복 일정 체크박스 클릭
await user.click(screen.getByLabelText('반복 일정'));

// 반복 유형 선택 UI가 표시되어야 함
expect(screen.getByText('반복 유형')).toBeInTheDocument();
expect(screen.getByText('반복 간격')).toBeInTheDocument();
expect(screen.getByText('반복 종료일')).toBeInTheDocument();
});

it('매일 반복 일정을 생성할 수 있다', async () => {
server.resetHandlers();

// 기존 이벤트가 있는 상태에서 시작 (현재 시스템 시간과 일치하는 날짜 사용)
const initialEvents = [
{
id: '1',
title: '기존 일정',
date: '2025-10-01', // 현재 시스템 시간과 일치
startTime: '09:00',
endTime: '10:00',
description: '기존 일정입니다',
location: '회의실',
category: '업무',
repeat: { type: 'none' as const, interval: 0 },
notificationTime: 10,
},
];

setupMockHandlerCreation(initialEvents);

const { user } = setup(<App />);

// 기존 이벤트가 표시되는지 확인 (이벤트 리스트에서)
const eventListContainer = within(screen.getByTestId('event-list'));
await eventListContainer.findByText('기존 일정');
// 반복 일정 생성
await user.click(screen.getAllByText('일정 추가')[0]);

await user.type(screen.getByLabelText('제목'), '매일 운동');
await user.type(screen.getByLabelText('날짜'), '2025-10-01'); // 현재 시스템 시간과 일치
await user.type(screen.getByLabelText('시작 시간'), '06:00');
await user.type(screen.getByLabelText('종료 시간'), '07:00');
await user.type(screen.getByLabelText('설명'), '매일 아침 운동');
await user.type(screen.getByLabelText('위치'), '집');
await user.click(screen.getByLabelText('카테고리'));
await user.click(within(screen.getByLabelText('카테고리')).getByRole('combobox'));
await user.click(screen.getByRole('option', { name: '개인-option' }));

// 반복 설정
await user.click(screen.getByLabelText('반복 일정'));

// 반복 유형 선택: Select 클릭 후 옵션 선택
const repeatTypeSelect = screen
.getByText('반복 유형')
.closest('.MuiFormControl-root')
?.querySelector('[role="combobox"]');
if (repeatTypeSelect) {
await user.click(repeatTypeSelect);
// 옵션이 나타날 때까지 대기
await screen.findByRole('option', { name: '매일' });
await user.click(screen.getByRole('option', { name: '매일' }));
}

await user.type(screen.getByLabelText('반복 종료일'), '2025-10-05'); // 현재 시스템 시간과 일치
await user.click(screen.getByTestId('event-submit-button'));

// 이벤트 리스트에서 반복 정보 확인
const eventList = within(screen.getByTestId('event-list'));
expect(eventList.getByText('매일 운동')).toBeInTheDocument();
expect(eventList.getByText(/반복: 1일마다/)).toBeInTheDocument();
expect(eventList.getByText(/종료: 2025-10-05/)).toBeInTheDocument();
});
});
60 changes: 60 additions & 0 deletions src/__tests__/unit/eventStateUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
getRepeatingEvents,
groupEventsByRepeatType,
isRepeatingEvent,
getRepeatDisplayInfo,
} from '../../utils/eventStateUtils';
import { createEvent } from '../../utils/eventUtils';

describe('반복 일정 판별 함수 테스트', () => {
it('반복 일정을 올바르게 판별한다', () => {
const repeatingEvent = createEvent('repeating', 'weekly', 1);
const singleEvent = createEvent('single', 'none', 1);

expect(isRepeatingEvent(repeatingEvent)).toBe(true);
expect(isRepeatingEvent(singleEvent)).toBe(false);
});

it('반복 일정 표시 정보를 올바르게 생성한다', () => {
const weeklyEvent = createEvent('repeating', 'weekly', 2);
const result = getRepeatDisplayInfo(weeklyEvent);

expect(result.isRepeating).toBe(true);
expect(result.repeatText).toBe('2주마다');
expect(result.shouldShowIcon).toBe(true);
});

it('단일 일정의 경우 아이콘을 표시하지 않는다', () => {
const singleEvent = createEvent('single', 'none', 1);
const result = getRepeatDisplayInfo(singleEvent);

expect(result.isRepeating).toBe(false);
expect(result.repeatText).toBe('');
expect(result.shouldShowIcon).toBe(false);
});
});

describe('반복 일정 필터링 함수 테스트', () => {
it('반복 일정만 필터링한다', () => {
const events = [
createEvent('repeating', 'weekly', 1),
createEvent('single', 'none', 1),
createEvent('repeating', 'monthly', 1),
];

const repeatingEvents = getRepeatingEvents(events);
expect(repeatingEvents).toHaveLength(2);
});

it('반복 유형별로 일정을 그룹화한다', () => {
const events = [
createEvent('repeating', 'weekly', 1),
createEvent('repeating', 'monthly', 1),
createEvent('repeating', 'weekly', 2),
];

const groups = groupEventsByRepeatType(events);
expect(groups.weekly).toHaveLength(2);
expect(groups.monthly).toHaveLength(1);
});
});
Loading
Loading