diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..ff9fae69
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,91 @@
+###
+# Place your Prettier ignore content here
+
+###
+# .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506
+
+# Created by .ignore support plugin (hsz.mobi)
+
+### Node template
+
+# Logs
+/logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage
+coverage
+.nyc_output
+
+# Grunt intermediate storage
+.grunt
+
+# Bower dependency directory
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons
+build/Release
+
+# Dependency directories
+node_modules/
+custom_modules
+
+# dotenv environment variables
+env/
+
+# Build artifacts
+build
+dist
+
+# Next.js
+.next
+# .next is already above (uncommented version preferred)
+
+# Deployment
+.vercel
+
+# Editor / IDE
+.idea
+.vscode
+*.code-workspace
+
+# Ignore dependency locks
+package-lock.json
+pnpm-lock.yaml
+yarn.lock
+
+# Static/public assets
+public
+next-env.d.ts
+
+# Business-specific files
+gtag.ts
+
+# Jest snapshots
+*.test.ts.snap
+*.test.tsx.snap
+
+# macOS / OS metadata
+.DS_Store
+Thumbs.db
+
+# Prettier config
+.prettierignore
+.prettierrc
+
+# Husky setting
+.husky/
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
index 0a019971..36ca9cdd 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -70,7 +70,8 @@ export default [
...typescriptPlugin.configs.recommended.rules,
// ESLint rules
- 'no-unused-vars': 'warn',
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unused-vars': ['error'],
// React rules
'react/prop-types': 'off',
diff --git a/index.html b/index.html
index 11222ae1..9ed34aa1 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,12 @@
-
-
-
- 일정관리 앱으로 학습하는 테스트 코드
-
-
-
-
-
+
+
+
+ 일정관리 앱으로 학습하는 테스트 코드
+
+
+
+
+
diff --git a/package.json b/package.json
index b01b2b4b..0dee0853 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,9 @@
"build": "tsc -b && vite build",
"lint:eslint": "eslint . --ext ts,tsx --report-unused-disable-directives",
"lint:tsc": "tsc --pretty",
- "lint": "pnpm lint:eslint && pnpm lint:tsc"
+ "lint": "pnpm lint:eslint && pnpm lint:tsc",
+ "format": "prettier --check .",
+ "format:fix": "prettier --write --list-different ."
},
"dependencies": {
"@emotion/react": "^11.11.4",
@@ -29,6 +31,7 @@
"react-dom": "19.1.0"
},
"devDependencies": {
+ "@eslint/js": "^9.33.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.5.2",
@@ -51,6 +54,7 @@
"eslint-plugin-vitest": "^0.5.4",
"globals": "16.3.0",
"jsdom": "^26.1.0",
+ "prettier": "3.6.2",
"typescript": "^5.2.2",
"vite": "^7.0.2",
"vite-plugin-eslint": "^1.8.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 093f3ec7..cd3c64eb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -39,6 +39,9 @@ importers:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
devDependencies:
+ '@eslint/js':
+ specifier: ^9.33.0
+ version: 9.33.0
'@testing-library/jest-dom':
specifier: ^6.6.3
version: 6.6.3
@@ -86,7 +89,7 @@ importers:
version: 2.32.0(@typescript-eslint/parser@8.35.0(eslint@9.30.0)(typescript@5.6.3))(eslint@9.30.0)
eslint-plugin-prettier:
specifier: ^5.5.1
- version: 5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.3.3)
+ version: 5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.6.2)
eslint-plugin-react:
specifier: ^7.37.0
version: 7.37.2(eslint@9.30.0)
@@ -95,7 +98,7 @@ importers:
version: 5.2.0(eslint@9.30.0)
eslint-plugin-storybook:
specifier: ^9.0.14
- version: 9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3))(typescript@5.6.3)
+ version: 9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.6.3)
eslint-plugin-vitest:
specifier: ^0.5.4
version: 0.5.4(@typescript-eslint/eslint-plugin@8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.30.0)(typescript@5.6.3))(eslint@9.30.0)(typescript@5.6.3))(eslint@9.30.0)(typescript@5.6.3)(vitest@3.2.4)
@@ -105,6 +108,9 @@ importers:
jsdom:
specifier: ^26.1.0
version: 26.1.0
+ prettier:
+ specifier: 3.6.2
+ version: 3.6.2
typescript:
specifier: ^5.2.2
version: 5.6.3
@@ -468,6 +474,10 @@ packages:
resolution: {integrity: sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@eslint/js@9.33.0':
+ resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@eslint/object-schema@2.1.6':
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2562,8 +2572,8 @@ packages:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
- prettier@3.3.3:
- resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
+ prettier@3.6.2:
+ resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
@@ -3628,6 +3638,8 @@ snapshots:
'@eslint/js@9.30.0': {}
+ '@eslint/js@9.33.0': {}
+
'@eslint/object-schema@2.1.6': {}
'@eslint/plugin-kit@0.3.3':
@@ -4958,10 +4970,10 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-prettier@5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.3.3):
+ eslint-plugin-prettier@5.5.1(@types/eslint@8.56.12)(eslint-config-prettier@10.1.5(eslint@9.30.0))(eslint@9.30.0)(prettier@3.6.2):
dependencies:
eslint: 9.30.0
- prettier: 3.3.3
+ prettier: 3.6.2
prettier-linter-helpers: 1.0.0
synckit: 0.11.8
optionalDependencies:
@@ -4994,11 +5006,11 @@ snapshots:
string.prototype.matchall: 4.0.11
string.prototype.repeat: 1.0.0
- eslint-plugin-storybook@9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3))(typescript@5.6.3):
+ eslint-plugin-storybook@9.0.14(eslint@9.30.0)(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.6.3):
dependencies:
'@typescript-eslint/utils': 8.35.0(eslint@9.30.0)(typescript@5.6.3)
eslint: 9.30.0
- storybook: 9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3)
+ storybook: 9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2)
transitivePeerDependencies:
- supports-color
- typescript
@@ -5984,7 +5996,7 @@ snapshots:
dependencies:
fast-diff: 1.3.0
- prettier@3.3.3: {}
+ prettier@3.6.2: {}
pretty-format@27.5.1:
dependencies:
@@ -6331,7 +6343,7 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
- storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.3.3):
+ storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.6.2):
dependencies:
'@storybook/global': 5.0.0
'@testing-library/jest-dom': 6.6.3
@@ -6345,7 +6357,7 @@ snapshots:
semver: 7.6.3
ws: 8.18.0
optionalDependencies:
- prettier: 3.3.3
+ prettier: 3.6.2
transitivePeerDependencies:
- '@testing-library/dom'
- bufferutil
diff --git a/src/App.tsx b/src/App.tsx
index 195c5b05..05ee3358 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,14 +4,12 @@ import {
AlertTitle,
Box,
Button,
- Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControl,
- FormControlLabel,
FormLabel,
IconButton,
MenuItem,
@@ -24,19 +22,19 @@ import {
TableHead,
TableRow,
TextField,
- Tooltip,
Typography,
} from '@mui/material';
import { useSnackbar } from 'notistack';
import { useState } from 'react';
+import EventForm from './components/EventForm';
+import { notificationOptions } from './constants/notifications';
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 as EventFormType } from './types';
import {
formatDate,
formatMonth,
@@ -46,20 +44,9 @@ import {
getWeeksAtMonth,
} from './utils/dateUtils';
import { findOverlappingEvents } from './utils/eventOverlap';
-import { getTimeErrorMessage } from './utils/timeValidation';
-
-const categories = ['업무', '개인', '가족', '기타'];
const weekDays = ['일', '월', '화', '수', '목', '금', '토'];
-const notificationOptions = [
- { value: 1, label: '1분 전' },
- { value: 10, label: '10분 전' },
- { value: 60, label: '1시간 전' },
- { value: 120, label: '2시간 전' },
- { value: 1440, label: '1일 전' },
-];
-
function App() {
const {
title,
@@ -77,11 +64,8 @@ function App() {
isRepeating,
setIsRepeating,
repeatType,
- // setRepeatType,
repeatInterval,
- // setRepeatInterval,
repeatEndDate,
- // setRepeatEndDate,
notificationTime,
setNotificationTime,
startTimeError,
@@ -118,7 +102,7 @@ function App() {
return;
}
- const eventData: Event | EventForm = {
+ const eventData: Event | EventFormType = {
id: editingEvent ? editingEvent.id : undefined,
title,
date,
@@ -316,176 +300,30 @@ function App() {
return (
-
- {editingEvent ? '일정 수정' : '일정 추가'}
-
-
- 제목
- setTitle(e.target.value)}
- />
-
-
-
- 날짜
- setDate(e.target.value)}
- />
-
-
-
-
- 시작 시간
-
- getTimeErrorMessage(startTime, endTime)}
- error={!!startTimeError}
- />
-
-
-
- 종료 시간
-
- getTimeErrorMessage(startTime, endTime)}
- error={!!endTimeError}
- />
-
-
-
-
-
- 설명
- setDescription(e.target.value)}
- />
-
-
-
- 위치
- setLocation(e.target.value)}
- />
-
-
-
- 카테고리
-
-
-
-
- setIsRepeating(e.target.checked)}
- />
- }
- label="반복 일정"
- />
-
-
-
- 알림 설정
-
-
-
- {/* ! 반복은 8주차 과제에 포함됩니다. 구현하고 싶어도 참아주세요~ */}
- {/* {isRepeating && (
-
-
- 반복 유형
-
-
-
-
- 반복 간격
- setRepeatInterval(Number(e.target.value))}
- slotProps={{ htmlInput: { min: 1 } }}
- />
-
-
- 반복 종료일
- setRepeatEndDate(e.target.value)}
- />
-
-
-
- )} */}
-
-
-
+
일정 보기
diff --git a/src/__mocks__/handlers.ts b/src/__mocks__/handlers.ts
index 42d6d4b7..f274bf28 100644
--- a/src/__mocks__/handlers.ts
+++ b/src/__mocks__/handlers.ts
@@ -1,16 +1,78 @@
import { http, HttpResponse } from 'msw';
import { events } from '../__mocks__/response/events.json' assert { type: 'json' };
-import { Event } from '../types';
+import { Event, EventForm } from '../types';
+import { defaultMockEvents } from './mockData';
+
+// 테스트용 에러 핸들러 생성 헬퍼 함수
+export const createErrorHandler = (method: string, endpoint: string, statusCode: number = 500) => {
+ return http[method as keyof typeof http](endpoint, () => {
+ return HttpResponse.json({ error: 'Server Error' }, { status: statusCode });
+ });
+};
// ! HARD
// ! 각 응답에 대한 MSW 핸들러를 작성해주세요. GET 요청은 이미 작성되어 있는 events json을 활용해주세요.
export const handlers = [
- http.get('/api/events', () => {}),
+ http.get('/api/events', () => {
+ return HttpResponse.json({
+ events: defaultMockEvents,
+ });
+ }),
+
+ http.post('/api/events', async ({ request }) => {
+ const newEventData = (await request.json()) as EventForm;
+
+ const newEvent: Event = {
+ id: crypto.randomUUID(),
+ ...newEventData,
+ };
+
+ return HttpResponse.json(newEvent, {
+ status: 201,
+ });
+ }),
+
+ http.put('/api/events/:id', async ({ params, request }) => {
+ const eventId = params.id;
+ const updatedEventData = (await request.json()) as EventForm;
+
+ const existingEvent = events.find((event) => event.id === eventId);
+
+ if (!existingEvent) {
+ return HttpResponse.json(
+ {
+ error: '이벤트를 찾을 수 없습니다.',
+ },
+ {
+ status: 404,
+ }
+ );
+ }
+
+ const updatedEvent: Event = {
+ ...existingEvent,
+ ...updatedEventData,
+ };
+
+ return HttpResponse.json(updatedEvent);
+ }),
- http.post('/api/events', async ({ request }) => {}),
+ http.delete('/api/events/:id', ({ params }) => {
+ const eventId = params.id;
+ const existingEvent = events.find((event) => event.id === eventId);
- http.put('/api/events/:id', async ({ params, request }) => {}),
+ if (!existingEvent) {
+ return HttpResponse.json(
+ {
+ error: '삭제할 이벤트가 없습니다.',
+ },
+ {
+ status: 404,
+ }
+ );
+ }
- http.delete('/api/events/:id', ({ params }) => {}),
+ return new HttpResponse(null, { status: 204 });
+ }),
];
diff --git a/src/__mocks__/handlersUtils.ts b/src/__mocks__/handlersUtils.ts
index 405837ec..0a1757bb 100644
--- a/src/__mocks__/handlersUtils.ts
+++ b/src/__mocks__/handlersUtils.ts
@@ -1,10 +1,81 @@
-import { Event } from '../types';
+import { http, HttpResponse } from 'msw';
+
+import { Event, EventForm } from '../types';
+import { defaultMockEvents } from './mockData';
// ! Hard
// ! 이벤트는 생성, 수정 되면 fetch를 다시 해 상태를 업데이트 합니다. 이를 위한 제어가 필요할 것 같은데요. 어떻게 작성해야 테스트가 병렬로 돌아도 안정적이게 동작할까요?
// ! 아래 이름을 사용하지 않아도 되니, 독립적이게 테스트를 구동할 수 있는 방법을 찾아보세요. 그리고 이 로직을 PR에 설명해주세요.
-export const setupMockHandlerCreation = (initEvents = [] as Event[]) => {};
-export const setupMockHandlerUpdating = () => {};
+export const createMockEventHandler = (initEvents = defaultMockEvents as Event[]) => {
+ const testEvents = [...initEvents];
+
+ return {
+ get: () => {
+ return http.get('/api/events', () => {
+ return HttpResponse.json({
+ events: testEvents,
+ });
+ });
+ },
+ post: () => {
+ return http.post('/api/events', async ({ request }) => {
+ const newEventData = (await request.json()) as EventForm;
+
+ const newEvent: Event = {
+ id: crypto.randomUUID(),
+ ...newEventData,
+ };
+
+ testEvents.push(newEvent);
+
+ return HttpResponse.json(newEvent, {
+ status: 201,
+ });
+ });
+ },
+ put: () => {
+ return http.put('/api/events/:id', async ({ params, request }) => {
+ const eventId = params.id;
+ const updatedEventData = (await request.json()) as EventForm;
+ const existingEventIndex = testEvents.findIndex((event) => event.id === eventId);
+
+ if (existingEventIndex === -1) {
+ return HttpResponse.json(
+ {
+ error: '이벤트를 찾을 수 없습니다.',
+ },
+ {
+ status: 404,
+ }
+ );
+ }
+
+ testEvents[existingEventIndex] = { ...testEvents[existingEventIndex], ...updatedEventData };
+
+ return HttpResponse.json(testEvents[existingEventIndex]);
+ });
+ },
+ delete: () => {
+ return http.delete('/api/events/:id', ({ params }) => {
+ const eventId = params.id;
+ const existingEventIndex = testEvents.findIndex((event) => event.id === eventId);
+
+ if (existingEventIndex === -1) {
+ return HttpResponse.json(
+ {
+ error: '삭제할 이벤트가 없습니다.',
+ },
+ {
+ status: 404,
+ }
+ );
+ }
+
+ testEvents.splice(existingEventIndex, 1);
-export const setupMockHandlerDeletion = () => {};
+ return new HttpResponse(null, { status: 204 });
+ });
+ },
+ };
+};
diff --git a/src/__mocks__/mockData.ts b/src/__mocks__/mockData.ts
new file mode 100644
index 00000000..600ea3dc
--- /dev/null
+++ b/src/__mocks__/mockData.ts
@@ -0,0 +1,16 @@
+import { Event } from '../types';
+
+export const defaultMockEvent: Event = {
+ id: '1',
+ title: '모각코',
+ date: '2025-08-21',
+ startTime: '10:00',
+ endTime: '11:00',
+ description: '모각코 스터디',
+ location: '카페',
+ category: '개인',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+};
+
+export const defaultMockEvents: Event[] = [defaultMockEvent];
diff --git a/src/__mocks__/response/realEvents.json b/src/__mocks__/response/realEvents.json
index 5ab618a0..0ee14f7c 100644
--- a/src/__mocks__/response/realEvents.json
+++ b/src/__mocks__/response/realEvents.json
@@ -1,64 +1,16 @@
{
"events": [
{
- "id": "2b7545a6-ebee-426c-b906-2329bc8d62bd",
- "title": "팀 회의",
- "date": "2025-08-20",
- "startTime": "10:00",
- "endTime": "11:00",
- "description": "주간 팀 미팅",
- "location": "회의실 A",
+ "id": "bf502edf-bb1d-44b0-932e-bd349a7e8508",
+ "title": "123",
+ "date": "2025-08-17",
+ "startTime": "22:58",
+ "endTime": "23:59",
+ "description": "11",
+ "location": "11",
"category": "업무",
- "repeat": { "type": "none", "interval": 0 },
- "notificationTime": 1
- },
- {
- "id": "09702fb3-a478-40b3-905e-9ab3c8849dcd",
- "title": "점심 약속",
- "date": "2025-08-21",
- "startTime": "12:30",
- "endTime": "13:30",
- "description": "동료와 점심 식사",
- "location": "회사 근처 식당",
- "category": "개인",
- "repeat": { "type": "none", "interval": 0 },
- "notificationTime": 1
- },
- {
- "id": "da3ca408-836a-4d98-b67a-ca389d07552b",
- "title": "프로젝트 마감",
- "date": "2025-08-25",
- "startTime": "09:00",
- "endTime": "18:00",
- "description": "분기별 프로젝트 마감",
- "location": "사무실",
- "category": "업무",
- "repeat": { "type": "none", "interval": 0 },
- "notificationTime": 1
- },
- {
- "id": "dac62941-69e5-4ec0-98cc-24c2a79a7f81",
- "title": "생일 파티",
- "date": "2025-08-28",
- "startTime": "19:00",
- "endTime": "22:00",
- "description": "친구 생일 축하",
- "location": "친구 집",
- "category": "개인",
- "repeat": { "type": "none", "interval": 0 },
- "notificationTime": 1
- },
- {
- "id": "80d85368-b4a4-47b3-b959-25171d49371f",
- "title": "운동",
- "date": "2025-08-22",
- "startTime": "18:00",
- "endTime": "19:00",
- "description": "주간 운동",
- "location": "헬스장",
- "category": "개인",
- "repeat": { "type": "none", "interval": 0 },
- "notificationTime": 1
+ "repeat": { "type": "none", "interval": 1 },
+ "notificationTime": 10
}
]
}
diff --git a/src/__tests__/hooks/easy.useCalendarView.spec.ts b/src/__tests__/hooks/easy.useCalendarView.spec.ts
index 93b57f0e..efd54f4d 100644
--- a/src/__tests__/hooks/easy.useCalendarView.spec.ts
+++ b/src/__tests__/hooks/easy.useCalendarView.spec.ts
@@ -1,24 +1,124 @@
import { act, renderHook } from '@testing-library/react';
+import { expect } from 'vitest';
import { useCalendarView } from '../../hooks/useCalendarView.ts';
-import { assertDate } from '../utils.ts';
describe('초기 상태', () => {
- it('view는 "month"이어야 한다', () => {});
+ it('view는 "month"이어야 한다', () => {
+ const { result } = renderHook(() => useCalendarView());
- it('currentDate는 오늘 날짜인 "2025-10-01"이어야 한다', () => {});
+ expect(result.current.view).toBe('month');
+ });
- it('holidays는 10월 휴일인 개천절, 한글날, 추석이 지정되어 있어야 한다', () => {});
+ it('currentDate는 오늘 날짜인 "2025-10-01"이어야 한다', () => {
+ const { result } = renderHook(() => useCalendarView());
+
+ expect(result.current.currentDate.getFullYear()).toBe(new Date().getFullYear());
+ expect(result.current.currentDate.getMonth()).toBe(new Date().getMonth());
+ expect(result.current.currentDate.getDay()).toBe(new Date().getDay());
+ });
+
+ it('holidays는 10월 휴일인 개천절, 한글날, 추석이 지정되어 있어야 한다', () => {
+ const { result } = renderHook(() => useCalendarView());
+
+ act(() => {
+ result.current.setCurrentDate(new Date('2025-10-01'));
+ });
+
+ expect(result.current.holidays).toEqual({
+ '2025-10-05': '추석',
+ '2025-10-06': '추석',
+ '2025-10-07': '추석',
+ '2025-10-03': '개천절',
+ '2025-10-09': '한글날',
+ });
+ });
+});
+
+it("view를 'week'으로 변경 시 view 상태가 'week'가 된다 된다", () => {
+ const { result } = renderHook(() => useCalendarView());
+
+ act(() => {
+ result.current.setView('week');
+ });
+
+ expect(result.current.view).toBe('week');
});
-it("view를 'week'으로 변경 시 적절하게 반영된다", () => {});
+it("주간 뷰에서 다음으로 navigate시 7일 후 '2025-10-08' 날짜로 지정이 된다", () => {
+ const { result } = renderHook(() => useCalendarView());
-it("주간 뷰에서 다음으로 navigate시 7일 후 '2025-10-08' 날짜로 지정이 된다", () => {});
+ act(() => {
+ result.current.setCurrentDate(new Date('2025-10-01'));
+ });
-it("주간 뷰에서 이전으로 navigate시 7일 후 '2025-09-24' 날짜로 지정이 된다", () => {});
+ act(() => {
+ result.current.setView('week');
+ });
-it("월간 뷰에서 다음으로 navigate시 한 달 후 '2025-11-01' 날짜여야 한다", () => {});
+ act(() => {
+ result.current.navigate('next');
+ });
-it("월간 뷰에서 이전으로 navigate시 한 달 전 '2025-09-01' 날짜여야 한다", () => {});
+ expect(result.current.currentDate).toEqual(new Date('2025-10-08'));
+});
+
+it("주간 뷰에서 이전으로 navigate시 7일 전 '2025-09-24' 날짜로 지정이 된다", () => {
+ const { result } = renderHook(() => useCalendarView());
+
+ act(() => {
+ result.current.setCurrentDate(new Date('2025-10-01'));
+ });
+
+ act(() => {
+ result.current.setView('week');
+ });
+
+ act(() => {
+ result.current.navigate('prev');
+ });
+
+ expect(result.current.currentDate).toEqual(new Date('2025-09-24'));
+});
-it("currentDate가 '2025-03-01' 변경되면 3월 휴일 '삼일절'로 업데이트되어야 한다", async () => {});
+it("월간 뷰에서 다음으로 navigate시 한 달 후 '2025-11-01' 날짜여야 한다", () => {
+ const { result } = renderHook(() => useCalendarView());
+
+ act(() => {
+ result.current.setCurrentDate(new Date('2025-10-01'));
+ });
+
+ act(() => {
+ result.current.navigate('next');
+ });
+
+ expect(result.current.currentDate).toEqual(new Date('2025-11-01'));
+});
+
+it("월간 뷰에서 이전으로 navigate시 한 달 전 '2025-09-01' 날짜여야 한다", () => {
+ const { result } = renderHook(() => useCalendarView());
+
+ act(() => {
+ result.current.setCurrentDate(new Date('2025-10-01'));
+ });
+
+ act(() => {
+ result.current.navigate('prev');
+ });
+
+ expect(result.current.currentDate).toEqual(new Date('2025-09-01'));
+});
+
+it("currentDate가 '2025-03-01' 변경되면 3월 휴일 '삼일절'로 업데이트되어야 한다", async () => {
+ const { result } = renderHook(() => useCalendarView());
+
+ act(() => {
+ result.current.setCurrentDate(new Date('2025-03-01'));
+ });
+
+ console.log('result.current.holidays', result.current.holidays);
+
+ expect(result.current.holidays).toEqual({
+ '2025-03-01': '삼일절',
+ });
+});
diff --git a/src/__tests__/hooks/easy.useSearch.spec.ts b/src/__tests__/hooks/easy.useSearch.spec.ts
index 80f57fa3..00686b1e 100644
--- a/src/__tests__/hooks/easy.useSearch.spec.ts
+++ b/src/__tests__/hooks/easy.useSearch.spec.ts
@@ -1,14 +1,318 @@
import { act, renderHook } from '@testing-library/react';
+import { expect } from 'vitest';
import { useSearch } from '../../hooks/useSearch.ts';
import { Event } from '../../types.ts';
-it('검색어가 비어있을 때 모든 이벤트를 반환해야 한다', () => {});
+it('검색어가 비어있을 때 현재 주간 view 범위의 모든 이벤트를 반환해야 한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-20',
+ },
+ {
+ id: '3',
+ title: '이벤트 3',
+ date: '2025-08-25',
+ },
+ ] as Event[];
+ const currentDate = new Date('2025-08-20');
-it('검색어에 맞는 이벤트만 필터링해야 한다', () => {});
+ const { result } = renderHook(() => useSearch(events, currentDate, 'week'));
-it('검색어가 제목, 설명, 위치 중 하나라도 일치하면 해당 이벤트를 반환해야 한다', () => {});
+ expect(result.current.filteredEvents).toEqual([
+ { id: '1', title: '이벤트 1', date: '2025-08-20' },
+ { id: '2', title: '이벤트 2', date: '2025-08-20' },
+ ]);
+});
-it('현재 뷰(주간/월간)에 해당하는 이벤트만 반환해야 한다', () => {});
+it('검색어에 맞는 이벤트만 필터링해야 한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '첫 번째 이벤트',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-20',
+ startTime: '11:00',
+ endTime: '13:00',
+ description: '두 번째 이벤트',
+ location: '회의실 B',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ {
+ id: '3',
+ title: '이벤트 3',
+ date: '2025-08-25',
+ startTime: '12:00',
+ endTime: '14:00',
+ description: '세 번째 이벤트',
+ location: '회의실 C',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 30,
+ },
+ ];
+ const currentDate = new Date('2025-08-20');
-it("검색어를 '회의'에서 '점심'으로 변경하면 필터링된 결과가 즉시 업데이트되어야 한다", () => {});
+ const { result } = renderHook(() => useSearch(events, currentDate, 'week'));
+
+ act(() => {
+ result.current.setSearchTerm('이벤트 1');
+ });
+
+ expect(result.current.filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '첫 번째 이벤트',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ ]);
+});
+
+it('검색어가 제목, 설명, 위치 중 하나라도 일치하면 해당 이벤트를 반환해야 한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '첫 번째 이벤트',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-20',
+ startTime: '11:00',
+ endTime: '13:00',
+ description: '두 번째 이벤트',
+ location: '회의실 B',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ {
+ id: '3',
+ title: '이벤트 3',
+ date: '2025-08-25',
+ startTime: '12:00',
+ endTime: '14:00',
+ description: '세 번째 이벤트',
+ location: '회의실 C',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 30,
+ },
+ ];
+ const currentDate = new Date('2025-08-20');
+
+ const { result } = renderHook(() => useSearch(events, currentDate, 'week'));
+
+ act(() => {
+ result.current.setSearchTerm('회의실 A');
+ });
+
+ expect(result.current.filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '첫 번째 이벤트',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ ]);
+});
+
+it('현재 월간에 해당하는 이벤트만 반환해야 한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '첫 번째 이벤트',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-20',
+ startTime: '11:00',
+ endTime: '13:00',
+ description: '두 번째 이벤트',
+ location: '회의실 B',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ {
+ id: '3',
+ title: '이벤트 3',
+ date: '2025-09-25',
+ startTime: '12:00',
+ endTime: '14:00',
+ description: '세 번째 이벤트',
+ location: '회의실 C',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 30,
+ },
+ ];
+ const currentDate = new Date('2025-08-20');
+
+ const { result } = renderHook(() => useSearch(events, currentDate, 'month'));
+
+ act(() => {
+ result.current.setSearchTerm('이벤트');
+ });
+
+ console.log('result.current.filteredEvents', result.current.filteredEvents);
+
+ expect(result.current.filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '첫 번째 이벤트',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-20',
+ startTime: '11:00',
+ endTime: '13:00',
+ description: '두 번째 이벤트',
+ location: '회의실 B',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ ]);
+});
+
+it("검색어를 '회의'에서 '점심'으로 변경하면 필터링된 결과가 즉시 업데이트되어야 한다", () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '회의',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-20',
+ startTime: '11:00',
+ endTime: '13:00',
+ description: '점심',
+ location: '식당',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ {
+ id: '3',
+ title: '이벤트 3',
+ date: '2025-09-25',
+ startTime: '12:00',
+ endTime: '14:00',
+ description: '세 번째 이벤트',
+ location: '회의실 C',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 30,
+ },
+ ];
+ const currentDate = new Date('2025-08-20');
+
+ const { result } = renderHook(() => useSearch(events, currentDate, 'month'));
+
+ act(() => {
+ result.current.setSearchTerm('회의');
+ });
+
+ expect(result.current.filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-20',
+ startTime: '08:00',
+ endTime: '10:00',
+ description: '회의',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 15,
+ },
+ ]);
+
+ act(() => {
+ result.current.setSearchTerm('점심');
+ });
+
+ expect(result.current.filteredEvents).toEqual([
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-20',
+ startTime: '11:00',
+ endTime: '13:00',
+ description: '점심',
+ location: '식당',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ ]);
+});
diff --git a/src/__tests__/hooks/medium.useEventOperations.spec.ts b/src/__tests__/hooks/medium.useEventOperations.spec.ts
index 566ecbb0..c0fe3aaa 100644
--- a/src/__tests__/hooks/medium.useEventOperations.spec.ts
+++ b/src/__tests__/hooks/medium.useEventOperations.spec.ts
@@ -1,14 +1,10 @@
-import { act, renderHook } from '@testing-library/react';
-import { http, HttpResponse } from 'msw';
-
-import {
- setupMockHandlerCreation,
- setupMockHandlerDeletion,
- setupMockHandlerUpdating,
-} from '../../__mocks__/handlersUtils.ts';
+import { renderHook, waitFor, act } from '@testing-library/react';
+import { expect } from 'vitest';
+
+import { createErrorHandler } from '../../__mocks__/handlers.ts';
+import { createMockEventHandler } from '../../__mocks__/handlersUtils.ts';
import { useEventOperations } from '../../hooks/useEventOperations.ts';
import { server } from '../../setupTests.ts';
-import { Event } from '../../types.ts';
const enqueueSnackbarFn = vi.fn();
@@ -22,16 +18,201 @@ vi.mock('notistack', async () => {
};
});
-it('저장되어있는 초기 이벤트 데이터를 적절하게 불러온다', async () => {});
+it('GET /api/events 호출하여 이벤트 목록을 불러오고, 로딩 완료시 스낵바에 "일정 로딩 완료!" 메시지를 띄운다', async () => {
+ const { result } = renderHook(() => useEventOperations(false));
+
+ expect(result.current.events).toEqual([]);
+
+ await waitFor(() => {
+ expect(result.current.events).toHaveLength(1);
+ });
+
+ expect(result.current.events[0]).toEqual(
+ expect.objectContaining({
+ id: '1',
+ title: '모각코',
+ date: '2025-08-21',
+ startTime: '10:00',
+ endTime: '11:00',
+ })
+ );
+
+ expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정 로딩 완료!', {
+ variant: 'info',
+ });
+});
+
+it('POST /api/events로 새 이벤트 생성 후, GET 재호출하여 목록을 업데이트하고 "일정이 추가되었습니다." 스낵바를 표시한다', async () => {
+ const newEventData = {
+ title: '새로운 회의',
+ date: '2025-08-22',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '새로운 팀 미팅',
+ location: '회의실 C',
+ category: '업무',
+ repeat: { type: 'none' as const, interval: 1 },
+ notificationTime: 5,
+ };
+
+ const mockHandler = createMockEventHandler();
+ server.use(mockHandler.get(), mockHandler.post());
+
+ const { result } = renderHook(() => useEventOperations(false));
+
+ await waitFor(() => {
+ expect(result.current.events).toHaveLength(1);
+ });
+
+ enqueueSnackbarFn.mockClear();
+
+ await act(async () => {
+ await result.current.saveEvent(newEventData);
+ });
+
+ expect(result.current.events).toHaveLength(2);
+
+ const addedEvent = result.current.events.find((e) => e.title === '새로운 회의');
+ expect(addedEvent).toEqual(expect.objectContaining(newEventData));
+
+ expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정이 추가되었습니다.', {
+ variant: 'success',
+ });
+});
+
+it('PUT /api/events/:id로 기존 이벤트 수정 후, GET 재호출하여 목록을 업데이트하고 "일정이 수정되었습니다." 스낵바를 표시한다', async () => {
+ const updatedEventData = {
+ id: '1',
+ title: '수정된 모각코', // 타이틀 변경
+ date: '2025-08-21',
+ startTime: '10:00',
+ endTime: '12:00', // 11:00 → 12:00로 변경
+ description: '수정된 모각코 스터디',
+ location: '도서관', // 카페 → 도서관으로 변경
+ category: '개인',
+ repeat: { type: 'none' as const, interval: 1 },
+ notificationTime: 10,
+ };
+
+ const mockHandler = createMockEventHandler();
+ server.use(mockHandler.get(), mockHandler.put());
-it('정의된 이벤트 정보를 기준으로 적절하게 저장이 된다', async () => {});
+ const { result } = renderHook(() => useEventOperations(true));
-it("새로 정의된 'title', 'endTime' 기준으로 적절하게 일정이 업데이트 된다", async () => {});
+ await waitFor(() => {
+ expect(result.current.events).toHaveLength(1);
+ });
+
+ enqueueSnackbarFn.mockClear();
+
+ await act(async () => {
+ await result.current.saveEvent(updatedEventData);
+ });
+
+ expect(result.current.events).toHaveLength(1);
+
+ const updatedEvent = result.current.events[0];
+ expect(updatedEvent.title).toBe('수정된 모각코');
+ expect(updatedEvent.endTime).toBe('12:00');
+ expect(updatedEvent.location).toBe('도서관');
+
+ expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정이 수정되었습니다.', {
+ variant: 'success',
+ });
+});
-it('존재하는 이벤트 삭제 시 에러없이 아이템이 삭제된다.', async () => {});
+it('DELETE /api/events/:id로 이벤트 삭제 후, GET 재호출하여 목록을 업데이트하고 "일정이 삭제되었습니다." 스낵바를 표시한다', async () => {
+ const mockHandler = createMockEventHandler();
+ server.use(mockHandler.get(), mockHandler.delete());
-it("이벤트 로딩 실패 시 '이벤트 로딩 실패'라는 텍스트와 함께 에러 토스트가 표시되어야 한다", async () => {});
+ const { result } = renderHook(() => useEventOperations(false));
-it("존재하지 않는 이벤트 수정 시 '일정 저장 실패'라는 토스트가 노출되며 에러 처리가 되어야 한다", async () => {});
+ await waitFor(() => {
+ expect(result.current.events).toHaveLength(1);
+ });
-it("네트워크 오류 시 '일정 삭제 실패'라는 텍스트가 노출되며 이벤트 삭제가 실패해야 한다", async () => {});
+ enqueueSnackbarFn.mockClear();
+
+ await act(async () => {
+ await result.current.deleteEvent('1');
+ });
+
+ expect(result.current.events).toHaveLength(0);
+
+ expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정이 삭제되었습니다.', {
+ variant: 'info',
+ });
+});
+
+it('GET /api/events 요청 실패 시, "이벤트 로딩 실패" 에러 스낵바를 표시한다', async () => {
+ server.use(createErrorHandler('get', '/api/events', 500));
+
+ const { result } = renderHook(() => useEventOperations(false));
+
+ expect(result.current.events).toEqual([]);
+
+ await waitFor(() => {
+ expect(enqueueSnackbarFn).toHaveBeenCalledWith('이벤트 로딩 실패', {
+ variant: 'error',
+ });
+ });
+});
+
+it('PUT /api/events/:id 요청이 404로 실패 시, "일정 저장 실패" 에러 스낵바를 표시한다', async () => {
+ const mockHandler = createMockEventHandler();
+ server.use(mockHandler.get(), createErrorHandler('put', '/api/events/:id', 404));
+
+ const { result } = renderHook(() => useEventOperations(true));
+
+ await waitFor(() => {
+ expect(result.current.events).toHaveLength(1);
+ });
+
+ enqueueSnackbarFn.mockClear();
+
+ const nonExistentEventData = {
+ id: '999', // 존재하지 않는 ID
+ title: '존재하지 않는 이벤트',
+ date: '2025-08-21',
+ startTime: '10:00',
+ endTime: '11:00',
+ description: '테스트',
+ location: '테스트',
+ category: '업무',
+ repeat: { type: 'none' as const, interval: 1 },
+ notificationTime: 10,
+ };
+
+ await act(async () => {
+ await result.current.saveEvent(nonExistentEventData);
+ });
+
+ await waitFor(() => {
+ expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정 저장 실패', {
+ variant: 'error',
+ });
+ });
+});
+
+it('DELETE /api/events/:id 요청이 네트워크 오류로 실패 시, "일정 삭제 실패" 에러 스낵바를 표시한다', async () => {
+ const mockHandler = createMockEventHandler();
+ server.use(mockHandler.get(), createErrorHandler('delete', '/api/events/:id', 500));
+
+ const { result } = renderHook(() => useEventOperations(false));
+
+ await waitFor(() => {
+ expect(result.current.events).toHaveLength(1);
+ });
+
+ enqueueSnackbarFn.mockClear();
+
+ await act(async () => {
+ await result.current.deleteEvent('1');
+ });
+
+ await waitFor(() => {
+ expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정 삭제 실패', {
+ variant: 'error',
+ });
+ });
+});
diff --git a/src/__tests__/hooks/medium.useNotifications.spec.ts b/src/__tests__/hooks/medium.useNotifications.spec.ts
index 7f585ea8..923b4478 100644
--- a/src/__tests__/hooks/medium.useNotifications.spec.ts
+++ b/src/__tests__/hooks/medium.useNotifications.spec.ts
@@ -1,14 +1,87 @@
import { act, renderHook } from '@testing-library/react';
+import { expect } from 'vitest';
+import { defaultMockEvents } from '../../__mocks__/mockData.ts';
import { useNotifications } from '../../hooks/useNotifications.ts';
import { Event } from '../../types.ts';
-import { formatDate } from '../../utils/dateUtils.ts';
-import { parseHM } from '../utils.ts';
-it('초기 상태에서는 알림이 없어야 한다', () => {});
+it('이벤트 데이터가 빈 배열일 때 알림 배열과 알람된 이벤트 배열이 비어있어야 한다.', () => {
+ const events: Event[] = [];
-it('지정된 시간이 된 경우 알림이 새롭게 생성되어 추가된다', () => {});
+ const { result } = renderHook(() => useNotifications(events));
-it('index를 기준으로 알림을 적절하게 제거할 수 있다', () => {});
+ expect(result.current.notifications).toEqual([]);
+ expect(result.current.notifiedEvents).toEqual([]);
+});
-it('이미 알림이 발생한 이벤트에 대해서는 중복 알림이 발생하지 않아야 한다', () => {});
+it('지정된 시간이 된 경우 알림이 새롭게 생성되어 추가된다', () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-08-21T09:55:00'));
+
+ const events: Event[] = defaultMockEvents;
+
+ const { result } = renderHook(() => useNotifications(events));
+
+ act(() => {
+ vi.advanceTimersByTime(1000);
+ });
+
+ expect(result.current.notifications).toHaveLength(1);
+ expect(result.current.notifications[0]).toEqual({
+ id: '1',
+ message: '10분 후 모각코 일정이 시작됩니다.',
+ });
+
+ expect(result.current.notifiedEvents).toEqual(['1']);
+
+ vi.useRealTimers();
+});
+
+it('index를 기준으로 알림을 적절하게 제거할 수 있다', () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-08-21T09:55:00'));
+
+ const events: Event[] = defaultMockEvents;
+
+ const { result } = renderHook(() => useNotifications(events));
+
+ act(() => {
+ vi.advanceTimersByTime(1000);
+ });
+
+ expect(result.current.notifications).toHaveLength(1);
+
+ act(() => {
+ result.current.removeNotification(0);
+ });
+
+ expect(result.current.notifications).toHaveLength(0);
+ expect(result.current.notifications).toEqual([]);
+
+ vi.useRealTimers();
+});
+
+it('이미 알림이 발생한 이벤트에 대해서는 중복 알림이 발생하지 않아야 한다', () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-08-21T09:55:00'));
+
+ const events: Event[] = defaultMockEvents;
+
+ const { result } = renderHook(() => useNotifications(events));
+
+ act(() => {
+ vi.advanceTimersByTime(1000);
+ });
+
+ expect(result.current.notifications).toHaveLength(1);
+ expect(result.current.notifiedEvents).toEqual(['1']);
+
+ act(() => {
+ vi.advanceTimersByTime(1000);
+ });
+
+ expect(result.current.notifications).toHaveLength(1);
+ expect(result.current.notifiedEvents).toEqual(['1']);
+
+ vi.useRealTimers();
+});
diff --git a/src/__tests__/medium.integration.spec.tsx b/src/__tests__/medium.integration.spec.tsx
index 15901d4e..cbcab123 100644
--- a/src/__tests__/medium.integration.spec.tsx
+++ b/src/__tests__/medium.integration.spec.tsx
@@ -1,49 +1,546 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
-import { render, screen, within, act } from '@testing-library/react';
-import { UserEvent, userEvent } from '@testing-library/user-event';
-import { http, HttpResponse } from 'msw';
+import { render, screen, within, waitFor } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
import { SnackbarProvider } from 'notistack';
-import { ReactElement } from 'react';
+import { createMockEventHandler } from '../__mocks__/handlersUtils.ts';
import App from '../App';
import { server } from '../setupTests';
import { Event } from '../types';
+const renderApp = () => {
+ const theme = createTheme();
+ return render(
+
+
+
+
+
+
+ );
+};
+
describe('일정 CRUD 및 기본 기능', () => {
it('입력한 새로운 일정 정보에 맞춰 모든 필드가 이벤트 리스트에 정확히 저장된다.', async () => {
// ! HINT. event를 추가 제거하고 저장하는 로직을 잘 살펴보고, 만약 그대로 구현한다면 어떤 문제가 있을 지 고민해보세요.
+
+ const newEvent: Event = {
+ id: 'new-test-event',
+ title: '새로운 회의',
+ date: '2025-08-21',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '중요한 회의입니다',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ const mockHandler = createMockEventHandler([]);
+ server.use(mockHandler.get(), mockHandler.post());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ // 필수 필드만 입력
+ await user.type(await screen.findByLabelText('제목'), newEvent.title);
+ await user.type(await screen.findByLabelText('날짜'), newEvent.date);
+ await user.type(await screen.findByLabelText('시작 시간'), newEvent.startTime);
+ await user.type(await screen.findByLabelText('종료 시간'), newEvent.endTime);
+ await user.type(await screen.findByLabelText('설명'), newEvent.description);
+ await user.type(await screen.findByLabelText('위치'), newEvent.location);
+ await user.type(await screen.findByLabelText('카테고리'), newEvent.category);
+
+ const eventSubmitButton = screen.getByTestId('event-submit-button');
+ await user.click(eventSubmitButton);
+
+ expect(await screen.findByText('일정이 추가되었습니다.')).toBeInTheDocument();
+
+ const eventList = screen.getByTestId('event-list');
+ expect(await within(eventList).findByText(newEvent.title)).toBeInTheDocument();
});
- it('기존 일정의 세부 정보를 수정하고 변경사항이 정확히 반영된다', async () => {});
+ it('기존 일정의 세부 정보를 수정하고 변경사항이 정확히 반영된다', async () => {
+ const existingEvent: Event = {
+ id: 'test-event-1',
+ title: '기존 회의',
+ date: '2025-08-21',
+ startTime: '10:00',
+ endTime: '11:00',
+ description: '기존 설명',
+ location: '회의실 B',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ const updatedEvent: Event = {
+ ...existingEvent,
+ title: '수정된 회의',
+ description: '수정된 설명',
+ location: '회의실 C',
+ };
+
+ // 기존 이벤트가 있는 상태로 시작
+ const mockHandler = createMockEventHandler([existingEvent]);
+ server.use(mockHandler.get(), mockHandler.put());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ // 기존 이벤트가 표시되는지 확인
+ const eventList = screen.getByTestId('event-list');
+ expect(await within(eventList).findByText(existingEvent.title)).toBeInTheDocument();
+
+ // 수정 버튼 클릭
+ const editButton = await screen.findByLabelText('Edit event');
+ await user.click(editButton);
+
+ // 폼이 기존 값으로 채워졌는지 확인
+ const titleField = await screen.findByLabelText('제목');
+ expect(titleField).toHaveValue(existingEvent.title);
+
+ // 값 수정
+ await user.clear(titleField);
+ await user.type(titleField, updatedEvent.title);
+
+ const descriptionField = await screen.findByLabelText('설명');
+ await user.clear(descriptionField);
+ await user.type(descriptionField, updatedEvent.description);
+
+ const locationField = await screen.findByLabelText('위치');
+ await user.clear(locationField);
+ await user.type(locationField, updatedEvent.location);
+
+ // 수정 버튼 클릭
+ const submitButton = screen.getByTestId('event-submit-button');
+ expect(submitButton).toHaveTextContent('일정 수정');
+ await user.click(submitButton);
+
+ // 수정 완료 메시지 확인
+ expect(await screen.findByText('일정이 수정되었습니다.')).toBeInTheDocument();
+
+ // 수정된 내용이 목록에 반영되었는지 확인
+ expect(await within(eventList).findByText(updatedEvent.title)).toBeInTheDocument();
+ expect(await within(eventList).findByText(updatedEvent.description)).toBeInTheDocument();
+ expect(await within(eventList).findByText(updatedEvent.location)).toBeInTheDocument();
+ });
+
+ it('일정을 삭제하고 더 이상 조회되지 않는지 확인한다', async () => {
+ const existingEvent: Event = {
+ id: 'test-event-1',
+ title: '삭제될 회의',
+ date: '2025-08-21',
+ startTime: '10:00',
+ endTime: '11:00',
+ description: '삭제될 설명',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ // 기존 이벤트가 있는 상태로 시작
+ const mockHandler = createMockEventHandler([existingEvent]);
+ server.use(mockHandler.get(), mockHandler.delete());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ // 기존 이벤트가 표시되는지 확인
+ const eventList = screen.getByTestId('event-list');
+ expect(await within(eventList).findByText(existingEvent.title)).toBeInTheDocument();
- it('일정을 삭제하고 더 이상 조회되지 않는지 확인한다', async () => {});
+ // 삭제 버튼 클릭
+ const deleteButton = await screen.findByLabelText('Delete event');
+ await user.click(deleteButton);
+
+ // 삭제 완료 메시지 확인
+ expect(await screen.findByText('일정이 삭제되었습니다.')).toBeInTheDocument();
+
+ // 이벤트가 목록에서 사라졌는지 확인
+ expect(within(eventList).queryByText(existingEvent.title)).not.toBeInTheDocument();
+ expect(await within(eventList).findByText('검색 결과가 없습니다.')).toBeInTheDocument();
+ });
});
describe('일정 뷰', () => {
- it('주별 뷰를 선택 후 해당 주에 일정이 없으면, 일정이 표시되지 않는다.', async () => {});
+ it('주별 뷰를 선택 후 해당 주에 일정이 없으면, 일정이 표시되지 않는다.', async () => {
+ const mockHandler = createMockEventHandler([]);
+ server.use(mockHandler.get());
+
+ renderApp();
+
+ const monthView = await screen.findByTestId('month-view');
+ expect(monthView).toBeInTheDocument();
+
+ const calendarCells = monthView.querySelectorAll('td');
+ calendarCells.forEach((cell) => {
+ const eventBoxes = cell.querySelectorAll('.MuiBox-root');
+ eventBoxes.forEach((box) => {
+ expect(box.textContent).not.toMatch(/회의|미팅|약속/);
+ });
+ });
+ });
+
+ it('주별 뷰 선택 후 해당 일자에 일정이 존재한다면 해당 일정이 정확히 표시된다', async () => {
+ const testEvent: Event = {
+ id: 'week-test-event',
+ title: '주간 회의',
+ date: '2025-08-21',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '주간 회의입니다',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ const mockHandler = createMockEventHandler([testEvent]);
+ server.use(mockHandler.get());
+
+ renderApp();
+
+ const monthView = await screen.findByTestId('month-view');
+ expect(monthView).toBeInTheDocument();
+
+ expect(await within(monthView).findByText(testEvent.title)).toBeInTheDocument();
+ });
+
+ it('월별 뷰에 일정이 없으면, 일정이 표시되지 않아야 한다.', async () => {
+ const mockHandler = createMockEventHandler([]);
+ server.use(mockHandler.get());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ const viewSelect = await screen.findByLabelText('뷰 타입 선택');
+ await user.click(viewSelect);
+
+ await waitFor(async () => {
+ const monthOption = screen.getByText('Month');
+ expect(monthOption).toBeInTheDocument();
+ });
+
+ const monthOption = screen.getByText('Month');
+ await user.click(monthOption);
+
+ const monthView = await screen.findByTestId('month-view');
+ expect(monthView).toBeInTheDocument();
+
+ const calendarCells = monthView.querySelectorAll('td');
+ calendarCells.forEach((cell) => {
+ const eventBoxes = cell.querySelectorAll('.MuiBox-root');
+ eventBoxes.forEach((box) => {
+ expect(box.textContent).not.toMatch(/회의|미팅|약속/);
+ });
+ });
+ });
+
+ it('월별 뷰에 일정이 정확히 표시되는지 확인한다', async () => {
+ const testEvent: Event = {
+ id: 'month-test-event',
+ title: '월간 회의',
+ date: '2025-08-21',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '월간 회의입니다',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ const mockHandler = createMockEventHandler([testEvent]);
+ server.use(mockHandler.get());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ const viewSelect = await screen.findByLabelText('뷰 타입 선택');
+ await user.click(viewSelect);
- it('주별 뷰 선택 후 해당 일자에 일정이 존재한다면 해당 일정이 정확히 표시된다', async () => {});
+ await waitFor(async () => {
+ const monthOption = screen.getByText('Month');
+ expect(monthOption).toBeInTheDocument();
+ });
- it('월별 뷰에 일정이 없으면, 일정이 표시되지 않아야 한다.', async () => {});
+ const monthOption = screen.getByText('Month');
+ await user.click(monthOption);
- it('월별 뷰에 일정이 정확히 표시되는지 확인한다', async () => {});
+ const monthView = await screen.findByTestId('month-view');
+ expect(monthView).toBeInTheDocument();
- it('달력에 1월 1일(신정)이 공휴일로 표시되는지 확인한다', async () => {});
+ expect(await within(monthView).findByText(testEvent.title)).toBeInTheDocument();
+ });
+
+ it('달력에 1월 1일(신정)이 공휴일로 표시되는지 확인한다', async () => {
+ const mockHandler = createMockEventHandler([]);
+ server.use(mockHandler.get());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ const viewSelect = await screen.findByLabelText('뷰 타입 선택');
+ await user.click(viewSelect);
+
+ await waitFor(async () => {
+ const monthOption = screen.getByText('Month');
+ expect(monthOption).toBeInTheDocument();
+ });
+
+ const monthOption = screen.getByText('Month');
+ await user.click(monthOption);
+
+ const prevButton = await screen.findByLabelText('Previous');
+ for (let i = 0; i < 7; i++) {
+ await user.click(prevButton);
+ }
+
+ const monthView = await screen.findByTestId('month-view');
+ expect(monthView).toBeInTheDocument();
+
+ expect(await within(monthView).findByText('신정')).toBeInTheDocument();
+
+ expect(await within(monthView).findByText('1')).toBeInTheDocument();
+ });
});
describe('검색 기능', () => {
- it('검색 결과가 없으면, "검색 결과가 없습니다."가 표시되어야 한다.', async () => {});
+ it('검색 결과가 없으면, "검색 결과가 없습니다."가 표시되어야 한다.', async () => {
+ const mockHandler = createMockEventHandler([]);
+ server.use(mockHandler.get());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ const searchField = await screen.findByLabelText('일정 검색');
+ await user.type(searchField, '존재하지 않는 일정');
+
+ const eventList = screen.getByTestId('event-list');
+ expect(await within(eventList).findByText('검색 결과가 없습니다.')).toBeInTheDocument();
+ });
+
+ it("'팀 회의'를 검색하면 해당 제목을 가진 일정이 리스트에 노출된다", async () => {
+ const testEvents: Event[] = [
+ {
+ id: 'search-test-event-1',
+ title: '팀 회의',
+ date: '2025-08-21',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '주간 팀 회의',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ {
+ id: 'search-test-event-2',
+ title: '개인 일정',
+ date: '2025-08-21',
+ startTime: '16:00',
+ endTime: '17:00',
+ description: '개인적인 일정',
+ location: '카페',
+ category: '개인',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ ];
+
+ const mockHandler = createMockEventHandler(testEvents);
+ server.use(mockHandler.get());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ const searchField = await screen.findByLabelText('일정 검색');
+ await user.type(searchField, '팀 회의');
+
+ const eventList = screen.getByTestId('event-list');
+ expect(await within(eventList).findByText('팀 회의')).toBeInTheDocument();
+ expect(within(eventList).queryByText('개인 일정')).not.toBeInTheDocument();
+ });
+
+ it('검색어를 지우면 모든 일정이 다시 표시되어야 한다', async () => {
+ const testEvents: Event[] = [
+ {
+ id: 'search-test-event-1',
+ title: '팀 회의',
+ date: '2025-08-21',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '주간 팀 회의',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ {
+ id: 'search-test-event-2',
+ title: '개인 일정',
+ date: '2025-08-21',
+ startTime: '16:00',
+ endTime: '17:00',
+ description: '개인적인 일정',
+ location: '카페',
+ category: '개인',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ ];
+
+ const mockHandler = createMockEventHandler(testEvents);
+ server.use(mockHandler.get());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ const searchField = await screen.findByLabelText('일정 검색');
+ await user.type(searchField, '팀 회의');
- it("'팀 회의'를 검색하면 해당 제목을 가진 일정이 리스트에 노출된다", async () => {});
+ const eventList = screen.getByTestId('event-list');
+ expect(await within(eventList).findByText('팀 회의')).toBeInTheDocument();
+ expect(within(eventList).queryByText('개인 일정')).not.toBeInTheDocument();
- it('검색어를 지우면 모든 일정이 다시 표시되어야 한다', async () => {});
+ await user.clear(searchField);
+
+ expect(await within(eventList).findByText('팀 회의')).toBeInTheDocument();
+ expect(await within(eventList).findByText('개인 일정')).toBeInTheDocument();
+ });
});
describe('일정 충돌', () => {
- it('겹치는 시간에 새 일정을 추가할 때 경고가 표시된다', async () => {});
+ it('겹치는 시간에 새 일정을 추가할 때 경고가 표시된다', async () => {
+ const existingEvent: Event = {
+ id: 'overlap-test-event',
+ title: '기존 회의',
+ date: '2025-08-21',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '기존 회의입니다',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ const mockHandler = createMockEventHandler([existingEvent]);
+ server.use(mockHandler.get(), mockHandler.post());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ await user.type(await screen.findByLabelText('제목'), '새로운 회의');
+ await user.type(await screen.findByLabelText('날짜'), '2025-08-21');
+ await user.type(await screen.findByLabelText('시작 시간'), '14:30');
+ await user.type(await screen.findByLabelText('종료 시간'), '15:30');
+ await user.type(await screen.findByLabelText('설명'), '겹치는 회의');
+ await user.type(await screen.findByLabelText('위치'), '회의실 B');
+ await user.type(await screen.findByLabelText('카테고리'), '업무');
+
+ const eventSubmitButton = screen.getByTestId('event-submit-button');
+ await user.click(eventSubmitButton);
+
+ expect(await screen.findByText('일정 겹침 경고')).toBeInTheDocument();
+ expect(await screen.findByText(/다음 일정과 겹칩니다/)).toBeInTheDocument();
+ expect(await screen.findByText(/기존 회의.*2025-08-21 14:00-15:00/)).toBeInTheDocument();
+
+ expect(await screen.findByText('취소')).toBeInTheDocument();
+ expect(await screen.findByText('계속 진행')).toBeInTheDocument();
+ });
+
+ it('기존 일정의 시간을 수정하여 충돌이 발생하면 경고가 노출된다', async () => {
+ const existingEvents: Event[] = [
+ {
+ id: 'overlap-test-event-1',
+ title: '첫 번째 회의',
+ date: '2025-08-21',
+ startTime: '14:00',
+ endTime: '15:00',
+ description: '첫 번째 회의입니다',
+ location: '회의실 A',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ {
+ id: 'overlap-test-event-2',
+ title: '두 번째 회의',
+ date: '2025-08-21',
+ startTime: '16:00',
+ endTime: '17:00',
+ description: '두 번째 회의입니다',
+ location: '회의실 B',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ },
+ ];
+
+ const mockHandler = createMockEventHandler(existingEvents);
+ server.use(mockHandler.get(), mockHandler.put());
+
+ const user = userEvent.setup();
+ renderApp();
+
+ const editButtons = await screen.findAllByLabelText('Edit event');
+ await user.click(editButtons[0]);
- it('기존 일정의 시간을 수정하여 충돌이 발생하면 경고가 노출된다', async () => {});
+ const startTimeField = await screen.findByLabelText('시작 시간');
+ const endTimeField = await screen.findByLabelText('종료 시간');
+
+ await user.clear(startTimeField);
+ await user.type(startTimeField, '16:30');
+ await user.clear(endTimeField);
+ await user.type(endTimeField, '17:30');
+
+ const submitButton = screen.getByTestId('event-submit-button');
+ await user.click(submitButton);
+
+ expect(await screen.findByText('일정 겹침 경고')).toBeInTheDocument();
+ expect(await screen.findByText(/다음 일정과 겹칩니다/)).toBeInTheDocument();
+ expect(await screen.findByText(/두 번째 회의.*2025-08-21 16:00-17:00/)).toBeInTheDocument();
+ });
});
-it('notificationTime을 10으로 하면 지정 시간 10분 전 알람 텍스트가 노출된다', async () => {});
+it('notificationTime을 10으로 하면 지정 시간 10분 전 알람 텍스트가 노출된다', async () => {
+ const now = new Date();
+ const eventTime = new Date(now.getTime() + 10 * 60 * 1000);
+
+ const testEvent: Event = {
+ id: 'notification-test-event',
+ title: '알림 테스트 회의',
+ date: eventTime.toISOString().split('T')[0],
+ startTime: eventTime.toTimeString().slice(0, 5),
+ endTime: new Date(eventTime.getTime() + 60 * 60 * 1000).toTimeString().slice(0, 5),
+ description: '알림 테스트용 회의',
+ location: '테스트 룸',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ const mockHandler = createMockEventHandler([testEvent]);
+ server.use(mockHandler.get());
+
+ renderApp();
+
+ const eventList = screen.getByTestId('event-list');
+
+ await waitFor(
+ () => {
+ const notificationIcon = within(eventList).queryByTestId('notification-icon');
+ const alertMessage = screen.queryByText(/10분 전/);
+
+ expect(notificationIcon || alertMessage).toBeTruthy();
+ },
+ { timeout: 3000 }
+ );
+
+ expect(await within(eventList).findByText('알림 테스트 회의')).toBeInTheDocument();
+});
diff --git a/src/__tests__/unit/EventForm.spec.tsx b/src/__tests__/unit/EventForm.spec.tsx
new file mode 100644
index 00000000..37ca94c1
--- /dev/null
+++ b/src/__tests__/unit/EventForm.spec.tsx
@@ -0,0 +1,173 @@
+import { render, screen } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { vi } from 'vitest';
+
+import EventForm from '../../components/EventForm';
+import { Event } from '../../types';
+
+describe('EventForm', () => {
+ const mockProps = {
+ title: '',
+ setTitle: vi.fn(),
+ date: '',
+ setDate: vi.fn(),
+ startTime: '',
+ endTime: '',
+ description: '',
+ setDescription: vi.fn(),
+ location: '',
+ setLocation: vi.fn(),
+ category: '업무',
+ setCategory: vi.fn(),
+ isRepeating: false,
+ setIsRepeating: vi.fn(),
+ notificationTime: 10,
+ setNotificationTime: vi.fn(),
+ startTimeError: null,
+ endTimeError: null,
+ editingEvent: null,
+ handleStartTimeChange: vi.fn(),
+ handleEndTimeChange: vi.fn(),
+ onSubmit: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('새 일정 추가 모드에서 올바른 제목이 표시된다', () => {
+ render();
+
+ expect(screen.getByRole('heading', { name: '일정 추가' })).toBeInTheDocument();
+ });
+
+ it('일정 수정 모드에서 올바른 제목이 표시된다', () => {
+ const editingEvent: Event = {
+ id: 'test-id',
+ title: '기존 일정',
+ date: '2025-08-21',
+ startTime: '10:00',
+ endTime: '11:00',
+ description: '',
+ location: '',
+ category: '업무',
+ repeat: { type: 'none', interval: 1 },
+ notificationTime: 10,
+ };
+
+ render();
+
+ expect(screen.getByRole('heading', { name: '일정 수정' })).toBeInTheDocument();
+ });
+
+ it('제목 입력 시 setTitle이 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const titleInput = screen.getByLabelText('제목');
+ await user.type(titleInput, '회의');
+
+ expect(mockProps.setTitle).toHaveBeenCalled();
+ });
+
+ it('날짜 입력 시 setDate가 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const dateInput = screen.getByLabelText('날짜');
+ await user.type(dateInput, '2025-08-21');
+
+ expect(mockProps.setDate).toHaveBeenCalled();
+ });
+
+ it('시작 시간 변경 시 handleStartTimeChange가 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const startTimeInput = screen.getByLabelText('시작 시간');
+ await user.type(startTimeInput, '14:30');
+
+ expect(mockProps.handleStartTimeChange).toHaveBeenCalled();
+ });
+
+ it('종료 시간 변경 시 handleEndTimeChange가 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const endTimeInput = screen.getByLabelText('종료 시간');
+ await user.type(endTimeInput, '15:30');
+
+ expect(mockProps.handleEndTimeChange).toHaveBeenCalled();
+ });
+
+ it('설명 입력 시 setDescription이 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const descriptionInput = screen.getByLabelText('설명');
+ await user.type(descriptionInput, '회의');
+
+ expect(mockProps.setDescription).toHaveBeenCalled();
+ });
+
+ it('위치 입력 시 setLocation이 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const locationInput = screen.getByLabelText('위치');
+ await user.type(locationInput, '회의실');
+
+ expect(mockProps.setLocation).toHaveBeenCalled();
+ });
+
+ it('카테고리 Select 컴포넌트가 렌더링된다', () => {
+ render();
+
+ const categorySelect = screen.getByDisplayValue('업무');
+ expect(categorySelect).toBeInTheDocument();
+ });
+
+ it('반복 일정 체크박스 변경 시 setIsRepeating이 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const repeatCheckbox = screen.getByRole('checkbox', { name: '반복 일정' });
+ await user.click(repeatCheckbox);
+
+ expect(mockProps.setIsRepeating).toHaveBeenCalledWith(true);
+ });
+
+ it('시작 시간 에러가 있을 때 에러 상태가 표시된다', () => {
+ const propsWithError = {
+ ...mockProps,
+ startTimeError: '시간을 확인해주세요',
+ };
+
+ render();
+
+ const startTimeInput = screen.getByLabelText('시작 시간');
+ expect(startTimeInput).toHaveAttribute('aria-invalid', 'true');
+ });
+
+ it('종료 시간 에러가 있을 때 에러 상태가 표시된다', () => {
+ const propsWithError = {
+ ...mockProps,
+ endTimeError: '시간을 확인해주세요',
+ };
+
+ render();
+
+ const endTimeInput = screen.getByLabelText('종료 시간');
+ expect(endTimeInput).toHaveAttribute('aria-invalid', 'true');
+ });
+
+ it('폼 제출 버튼 클릭 시 onSubmit이 호출된다', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const submitButton = screen.getByTestId('event-submit-button');
+ await user.click(submitButton);
+
+ expect(mockProps.onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/__tests__/unit/easy.dateUtils.spec.ts b/src/__tests__/unit/easy.dateUtils.spec.ts
index 967bfacd..7e366d2a 100644
--- a/src/__tests__/unit/easy.dateUtils.spec.ts
+++ b/src/__tests__/unit/easy.dateUtils.spec.ts
@@ -1,3 +1,5 @@
+import { expect } from 'vitest';
+
import { Event } from '../../types';
import {
fillZero,
@@ -12,105 +14,425 @@ import {
} from '../../utils/dateUtils';
describe('getDaysInMonth', () => {
- it('1월은 31일 수를 반환한다', () => {});
+ it('1월은 31일 수를 반환한다', () => {
+ const daysInMonth = getDaysInMonth(2025, 1);
- it('4월은 30일 일수를 반환한다', () => {});
+ expect(daysInMonth).toBe(31);
+ });
- it('윤년의 2월에 대해 29일을 반환한다', () => {});
+ it('4월은 30일 일수를 반환한다', () => {
+ const daysInMonth = getDaysInMonth(2025, 4);
- it('평년의 2월에 대해 28일을 반환한다', () => {});
+ expect(daysInMonth).toBe(30);
+ });
- it('유효하지 않은 월에 대해 적절히 처리한다', () => {});
-});
+ it('윤년의 2월에 대해 29일을 반환한다', () => {
+ const daysInMonth = getDaysInMonth(2024, 2);
-describe('getWeekDates', () => {
- it('주중의 날짜(수요일)에 대해 올바른 주의 날짜들을 반환한다', () => {});
+ expect(daysInMonth).toBe(29);
+ });
- it('주의 시작(월요일)에 대해 올바른 주의 날짜들을 반환한다', () => {});
+ it('평년의 2월에 대해 28일을 반환한다', () => {
+ const daysInMonth = getDaysInMonth(2025, 2);
- it('주의 끝(일요일)에 대해 올바른 주의 날짜들을 반환한다', () => {});
+ expect(daysInMonth).toBe(28);
+ });
- it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연말)', () => {});
+ it('유효하지 않은 월에 대해 0을 반환한다.', () => {
+ const daysInMonth = getDaysInMonth(2025, 0);
- it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연초)', () => {});
-
- it('윤년의 2월 29일을 포함한 주를 올바르게 처리한다', () => {});
+ expect(daysInMonth).toBe(0);
+ });
+});
- it('월의 마지막 날짜를 포함한 주를 올바르게 처리한다', () => {});
+describe('getWeekDates', () => {
+ it('주중의 날짜(수요일)에 대해 올바른 주의 날짜들을 반환한다', () => {
+ const weekDates = getWeekDates(new Date('2025-08-20'));
+
+ const expected = [
+ new Date('2025-08-17').toDateString(),
+ new Date('2025-08-18').toDateString(),
+ new Date('2025-08-19').toDateString(),
+ new Date('2025-08-20').toDateString(),
+ new Date('2025-08-21').toDateString(),
+ new Date('2025-08-22').toDateString(),
+ new Date('2025-08-23').toDateString(),
+ ];
+
+ expect(weekDates.map((date) => date.toDateString())).toEqual(expected);
+ });
+
+ it('월요일 날짜에 대해 해당 주의 7일을 반환한다', () => {
+ const weekDates = getWeekDates(new Date('2025-08-18'));
+
+ const expected = [
+ new Date('2025-08-17').toDateString(),
+ new Date('2025-08-18').toDateString(),
+ new Date('2025-08-19').toDateString(),
+ new Date('2025-08-20').toDateString(),
+ new Date('2025-08-21').toDateString(),
+ new Date('2025-08-22').toDateString(),
+ new Date('2025-08-23').toDateString(),
+ ];
+
+ expect(weekDates.map((date) => date.toDateString())).toEqual(expected);
+ });
+
+ it('주 시작일(일요일)에 대해 해당 주의 모든 날짜를 \n' + ' 반환한다', () => {
+ const weekDates = getWeekDates(new Date('2025-08-17'));
+
+ const expected = [
+ new Date('2025-08-17').toDateString(),
+ new Date('2025-08-18').toDateString(),
+ new Date('2025-08-19').toDateString(),
+ new Date('2025-08-20').toDateString(),
+ new Date('2025-08-21').toDateString(),
+ new Date('2025-08-22').toDateString(),
+ new Date('2025-08-23').toDateString(),
+ ];
+
+ expect(weekDates.map((date) => date.toDateString())).toEqual(expected);
+ });
+
+ it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연말)', () => {
+ const weekDates = getWeekDates(new Date('2025-12-31'));
+
+ const expected = [
+ new Date('2025-12-28').toDateString(),
+ new Date('2025-12-29').toDateString(),
+ new Date('2025-12-30').toDateString(),
+ new Date('2025-12-31').toDateString(),
+ new Date('2026-01-01').toDateString(),
+ new Date('2026-01-02').toDateString(),
+ new Date('2026-01-03').toDateString(),
+ ];
+
+ expect(weekDates.map((date) => date.toDateString())).toEqual(expected);
+ });
+
+ it('연도를 넘어가는 주의 날짜를 정확히 처리한다 (연초)', () => {
+ const weekDates = getWeekDates(new Date('2025-01-01'));
+
+ const expected = [
+ new Date('2024-12-29').toDateString(),
+ new Date('2024-12-30').toDateString(),
+ new Date('2024-12-31').toDateString(),
+ new Date('2025-01-01').toDateString(),
+ new Date('2025-01-02').toDateString(),
+ new Date('2025-01-03').toDateString(),
+ new Date('2025-01-04').toDateString(),
+ ];
+
+ expect(weekDates.map((date) => date.toDateString())).toEqual(expected);
+ });
+
+ it('윤년의 2월 29일을 포함한 주를 올바르게 처리한다', () => {
+ const weekDates = getWeekDates(new Date('2024-02-29'));
+
+ const expected = [
+ new Date('2024-02-25').toDateString(),
+ new Date('2024-02-26').toDateString(),
+ new Date('2024-02-27').toDateString(),
+ new Date('2024-02-28').toDateString(),
+ new Date('2024-02-29').toDateString(),
+ new Date('2024-03-01').toDateString(),
+ new Date('2024-03-02').toDateString(),
+ ];
+
+ expect(weekDates.map((date) => date.toDateString())).toEqual(expected);
+ });
+
+ it('월의 마지막 날짜를 포함한 주를 올바르게 처리한다', () => {
+ const weekDates = getWeekDates(new Date('2025-08-31'));
+
+ const expected = [
+ new Date('2025-08-31').toDateString(),
+ new Date('2025-09-01').toDateString(),
+ new Date('2025-09-02').toDateString(),
+ new Date('2025-09-03').toDateString(),
+ new Date('2025-09-04').toDateString(),
+ new Date('2025-09-05').toDateString(),
+ new Date('2025-09-06').toDateString(),
+ ];
+
+ expect(weekDates.map((date) => date.toDateString())).toEqual(expected);
+ });
});
describe('getWeeksAtMonth', () => {
- it('2025년 7월 1일의 올바른 주 정보를 반환해야 한다', () => {});
+ it('2025년 7월의 주별 날짜 배열을 반환한다', () => {
+ const weeksAtMonth = getWeeksAtMonth(new Date('2025-07-01'));
+
+ const expected = [
+ [null, null, 1, 2, 3, 4, 5],
+ [6, 7, 8, 9, 10, 11, 12],
+ [13, 14, 15, 16, 17, 18, 19],
+ [20, 21, 22, 23, 24, 25, 26],
+ [27, 28, 29, 30, 31, null, null],
+ ];
+
+ expect(weeksAtMonth).toEqual(expected);
+ });
});
describe('getEventsForDay', () => {
- it('특정 날짜(1일)에 해당하는 이벤트만 정확히 반환한다', () => {});
+ it('특정 날짜(1일)에 해당하는 이벤트만 정확히 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ },
+ {
+ id: '2',
+ date: '2025-08-21',
+ },
+ ] as Event[];
+
+ const result = getEventsForDay(events, 1);
+ expect(result).toHaveLength(1);
+ expect(result[0].date).toBe('2025-08-01');
+ expect(result[0].id).toBe('1');
+ });
+
+ it('해당 날짜에 이벤트가 없을 경우 빈 배열을 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ },
+ {
+ id: '2',
+ date: '2025-08-21',
+ },
+ ] as Event[];
+
+ const result = getEventsForDay(events, 2);
+ expect(result).toHaveLength(0);
+ expect(result).toEqual([]);
+ });
+
+ it('날짜가 0일 경우 빈 배열을 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ },
+ {
+ id: '2',
+ date: '2025-08-21',
+ },
+ ] as Event[];
+
+ const result = getEventsForDay(events, 0);
+ expect(result).toHaveLength(0);
+ expect(result).toEqual([]);
+ });
+
+ it('날짜가 32일 이상인 경우 빈 배열을 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ },
+ {
+ id: '2',
+ date: '2025-08-31',
+ },
+ ] as Event[];
+
+ const result = getEventsForDay(events, 32);
+
+ expect(result).toHaveLength(0);
+ expect(result).toEqual([]);
+ });
+});
- it('해당 날짜에 이벤트가 없을 경우 빈 배열을 반환한다', () => {});
+describe('formatWeek', () => {
+ it('월의 중간 날짜에 대해 올바른 주 정보를 반환한다', () => {
+ const week = formatWeek(new Date('2025-08-15'));
- it('날짜가 0일 경우 빈 배열을 반환한다', () => {});
+ expect(week).toBe('2025년 8월 2주');
+ });
- it('날짜가 32일 이상인 경우 빈 배열을 반환한다', () => {});
-});
+ it('월의 첫 주에 포함된 주의 소속 월과 주차를 반환한다', () => {
+ const week = formatWeek(new Date('2025-08-01'));
-describe('formatWeek', () => {
- it('월의 중간 날짜에 대해 올바른 주 정보를 반환한다', () => {});
+ expect(week).toBe('2025년 7월 5주');
+ });
- it('월의 첫 주에 대해 올바른 주 정보를 반환한다', () => {});
+ it('월의 마지막 주에 포함된 주의 소속 월과 주차를 반환한다', () => {
+ const week = formatWeek(new Date('2025-08-31'));
- it('월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => {});
+ expect(week).toBe('2025년 9월 1주');
+ });
- it('연도가 바뀌는 주에 대해 올바른 주 정보를 반환한다', () => {});
+ it('연도가 바뀌는 주에 포함된 주의 소속 월과 주차를 반환한다', () => {
+ const week = formatWeek(new Date('2025-01-01'));
- it('윤년 2월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => {});
+ expect(week).toBe('2025년 1월 1주');
+ });
- it('평년 2월의 마지막 주에 대해 올바른 주 정보를 반환한다', () => {});
+ it('윤년 2월의 마지막 주에 포함된 주의 소속 월과 주차를 반환한다', () => {
+ const week = formatWeek(new Date('2024-02-29'));
+
+ expect(week).toBe('2024년 2월 5주');
+ });
+
+ it('평년 2월의 마지막 주에 포함된 주의 소속 월과 주차를 반환한다', () => {
+ const week = formatWeek(new Date('2025-02-29'));
+
+ expect(week).toBe('2025년 2월 4주');
+ });
});
describe('formatMonth', () => {
- it("2025년 7월 10일을 '2025년 7월'로 반환한다", () => {});
+ it("2025년 7월 10일을 '2025년 7월'로 반환한다", () => {
+ const month = formatMonth(new Date('2025-07-10'));
+
+ expect(month).toBe('2025년 7월');
+ });
});
describe('isDateInRange', () => {
- it('범위 내의 날짜 2025-07-10에 대해 true를 반환한다', () => {});
+ it('범위 내의 날짜 2025-07-10에 대해 true를 반환한다', () => {
+ const date = new Date('2025-07-10');
+ const startDate = new Date('2025-07-01');
+ const endDate = new Date('2025-07-31');
+
+ const isInRange = isDateInRange(date, startDate, endDate);
+
+ expect(isInRange).toBe(true);
+ });
+
+ it('범위의 시작일 2025-07-01에 대해 true를 반환한다', () => {
+ const date = new Date('2025-07-01');
+ const startDate = new Date('2025-07-01');
+ const endDate = new Date('2025-07-31');
+
+ const isInRange = isDateInRange(date, startDate, endDate);
+
+ expect(isInRange).toBe(true);
+ });
+
+ it('범위의 종료일 2025-07-31에 대해 true를 반환한다', () => {
+ const date = new Date('2025-07-31');
+ const startDate = new Date('2025-07-01');
+ const endDate = new Date('2025-07-31');
- it('범위의 시작일 2025-07-01에 대해 true를 반환한다', () => {});
+ const isInRange = isDateInRange(date, startDate, endDate);
- it('범위의 종료일 2025-07-31에 대해 true를 반환한다', () => {});
+ expect(isInRange).toBe(true);
+ });
- it('범위 이전의 날짜 2025-06-30에 대해 false를 반환한다', () => {});
+ it('범위 이전의 날짜 2025-06-30에 대해 false를 반환한다', () => {
+ const date = new Date('2025-06-30');
+ const startDate = new Date('2025-07-01');
+ const endDate = new Date('2025-07-31');
- it('범위 이후의 날짜 2025-08-01에 대해 false를 반환한다', () => {});
+ const isInRange = isDateInRange(date, startDate, endDate);
- it('시작일이 종료일보다 늦은 경우 모든 날짜에 대해 false를 반환한다', () => {});
+ expect(isInRange).toBe(false);
+ });
+
+ it('범위 이후의 날짜 2025-08-01에 대해 false를 반환한다', () => {
+ const date = new Date('2025-08-01');
+ const startDate = new Date('2025-07-01');
+ const endDate = new Date('2025-07-31');
+
+ const isInRange = isDateInRange(date, startDate, endDate);
+
+ expect(isInRange).toBe(false);
+ });
+
+ it('시작일이 종료일(시작일 > 종료일)보다 늦은 경우 모든 날짜에 대해 false를 반환한다', () => {
+ const date = new Date('2025-07-01');
+ const startDate = new Date('2025-07-31');
+ const endDate = new Date('2025-07-01');
+
+ const isInRange = isDateInRange(date, startDate, endDate);
+
+ expect(isInRange).toBe(false);
+ });
});
describe('fillZero', () => {
- it("5를 2자리로 변환하면 '05'를 반환한다", () => {});
+ it("5를 2자리로 변환하면 '05'를 반환한다", () => {
+ const filled = fillZero(5);
+
+ expect(filled).toBe('05');
+ });
+
+ it("10을 2자리로 변환하면 '10'을 반환한다", () => {
+ const filled = fillZero(10);
+
+ expect(filled).toBe('10');
+ });
+
+ it("3을 3자리로 변환하면 '003'을 반환한다", () => {
+ const filled = fillZero(3, 3);
- it("10을 2자리로 변환하면 '10'을 반환한다", () => {});
+ expect(filled).toBe('003');
+ });
- it("3을 3자리로 변환하면 '003'을 반환한다", () => {});
+ it("100을 2자리로 변환하면 '100'을 반환한다", () => {
+ const filled = fillZero(100);
- it("100을 2자리로 변환하면 '100'을 반환한다", () => {});
+ expect(filled).toBe('100');
+ });
- it("0을 2자리로 변환하면 '00'을 반환한다", () => {});
+ it("0을 2자리로 변환하면 '00'을 반환한다", () => {
+ const filled = fillZero(0);
- it("1을 5자리로 변환하면 '00001'을 반환한다", () => {});
+ expect(filled).toBe('00');
+ });
- it("소수점이 있는 3.14를 5자리로 변환하면 '03.14'를 반환한다", () => {});
+ it("1을 5자리로 변환하면 '00001'을 반환한다", () => {
+ const filled = fillZero(1, 5);
- it('size 파라미터를 생략하면 기본값 2를 사용한다', () => {});
+ expect(filled).toBe('00001');
+ });
- it('value가 지정된 size보다 큰 자릿수를 가지면 원래 값을 그대로 반환한다', () => {});
+ it("소수점이 있는 3.14를 5자리로 변환하면 '03.14'를 반환한다", () => {
+ const filled = fillZero(3.14, 5);
+
+ expect(filled).toBe('03.14');
+ });
+
+ it('size 파라미터를 생략하면 기본값 2를 사용한다', () => {
+ const filled = fillZero(3);
+
+ expect(filled).toBe('03');
+ });
+
+ it('value가 지정된 size보다 큰 자릿수를 가지면 원래 값을 그대로 반환한다', () => {
+ const filled = fillZero(123456789, 5);
+
+ expect(filled).toBe('123456789');
+ });
});
describe('formatDate', () => {
- it('날짜를 YYYY-MM-DD 형식으로 포맷팅한다', () => {});
+ it('날짜를 YYYY-MM-DD 형식으로 포맷팅한다', () => {
+ const formatted = formatDate(new Date('2025-08-10'));
+
+ expect(formatted).toBe('2025-08-10');
+ });
+
+ it('day 파라미터가 제공되면 해당 일자로 포맷팅한다', () => {
+ const formatted = formatDate(new Date('2025-08-10'), 1);
+
+ expect(formatted).toBe('2025-08-01');
+ });
+
+ it('월이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {
+ const formatted = formatDate(new Date('2025-01-11'));
- it('day 파라미터가 제공되면 해당 일자로 포맷팅한다', () => {});
+ expect(formatted).toBe('2025-01-11');
+ });
- it('월이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {});
+ it('일이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {
+ const formatted = formatDate(new Date('2025-12-01'));
- it('일이 한 자리 수일 때 앞에 0을 붙여 포맷팅한다', () => {});
+ expect(formatted).toBe('2025-12-01');
+ });
});
diff --git a/src/__tests__/unit/easy.eventOverlap.spec.ts b/src/__tests__/unit/easy.eventOverlap.spec.ts
index 5e5f6497..bf01e1d8 100644
--- a/src/__tests__/unit/easy.eventOverlap.spec.ts
+++ b/src/__tests__/unit/easy.eventOverlap.spec.ts
@@ -1,4 +1,6 @@
-import { Event } from '../../types';
+import { expect } from 'vitest';
+
+import { Event } from '../../types.ts';
import {
convertEventToDateRange,
findOverlappingEvents,
@@ -6,31 +8,177 @@ import {
parseDateTime,
} from '../../utils/eventOverlap';
describe('parseDateTime', () => {
- it('2025-07-01 14:30을 정확한 Date 객체로 변환한다', () => {});
+ it('2025-07-01 14:30을 정확한 Date 객체로 변환한다', () => {
+ const parsedDate = parseDateTime('2025-07-01', '14:30');
+
+ expect(parsedDate).toEqual(new Date('2025-07-01T14:30:00'));
+ });
+
+ it('잘못된 날짜 형식에 대해 Invalid Date를 반환한다', () => {
+ const parsedDate = parseDateTime('2025-13-01', '14:30');
- it('잘못된 날짜 형식에 대해 Invalid Date를 반환한다', () => {});
+ expect(isNaN(parsedDate.getTime())).toBe(true);
+ });
- it('잘못된 시간 형식에 대해 Invalid Date를 반환한다', () => {});
+ it('잘못된 시간 형식에 대해 Invalid Date를 반환한다', () => {
+ const parsedDate = parseDateTime('2025-13-01', '142:300');
- it('날짜 문자열이 비어있을 때 Invalid Date를 반환한다', () => {});
+ expect(isNaN(parsedDate.getTime())).toBe(true);
+ });
+
+ it('날짜 문자열이 비어있을 때 Invalid Date를 반환한다', () => {
+ const parsedDate = parseDateTime('', '14:30');
+
+ expect(isNaN(parsedDate.getTime())).toBe(true);
+ });
});
describe('convertEventToDateRange', () => {
- it('일반적인 이벤트를 올바른 시작 및 종료 시간을 가진 객체로 변환한다', () => {});
+ it('일반적인 이벤트를 올바른 시작 및 종료 시간을 가진 객체로 변환한다', () => {
+ const event: Event = {
+ id: '1',
+ date: '2025-08-01',
+ startTime: '14:30',
+ endTime: '15:30',
+ } as Event;
+
+ const dateRange = convertEventToDateRange(event);
+
+ const expected = {
+ start: new Date(2025, 7, 1, 14, 30),
+ end: new Date(2025, 7, 1, 15, 30),
+ };
+
+ expect(dateRange).toEqual(expected);
+ });
+
+ it('잘못된 날짜 형식의 이벤트에 대해 Invalid Date를 반환한다', () => {
+ const event: Event = {
+ id: '1',
+ date: '2025-8-1',
+ startTime: '14:30',
+ endTime: '15:30',
+ } as Event;
+
+ const dateRange = convertEventToDateRange(event);
+
+ expect(Number.isNaN(dateRange.start.getTime())).toBe(true);
+ expect(Number.isNaN(dateRange.end.getTime())).toBe(true);
+ });
- it('잘못된 날짜 형식의 이벤트에 대해 Invalid Date를 반환한다', () => {});
+ it('잘못된 시간 형식의 이벤트에 대해 Invalid Date를 반환한다', () => {
+ const event: Event = {
+ id: '1',
+ date: '2025-8-1',
+ startTime: '14-30',
+ endTime: '15-30',
+ } as Event;
- it('잘못된 시간 형식의 이벤트에 대해 Invalid Date를 반환한다', () => {});
+ const dateRange = convertEventToDateRange(event);
+
+ expect(Number.isNaN(dateRange.start.getTime())).toBe(true);
+ expect(Number.isNaN(dateRange.end.getTime())).toBe(true);
+ });
});
describe('isOverlapping', () => {
- it('두 이벤트가 겹치는 경우 true를 반환한다', () => {});
+ it('두 이벤트가 겹치는 경우 true를 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ startTime: '14:30',
+ endTime: '15:30',
+ },
+ {
+ id: '2',
+ date: '2025-08-01',
+ startTime: '14:30',
+ endTime: '15:30',
+ },
+ ] as Event[];
+
+ const result = isOverlapping(events[0], events[1]);
+
+ expect(result).toBe(true);
+ });
- it('두 이벤트가 겹치지 않는 경우 false를 반환한다', () => {});
+ it('두 이벤트가 겹치지 않는 경우 false를 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ startTime: '14:30',
+ endTime: '15:30',
+ },
+ {
+ id: '2',
+ date: '2025-08-05',
+ startTime: '08:30',
+ endTime: '20:30',
+ },
+ ] as Event[];
+
+ const result = isOverlapping(events[0], events[1]);
+
+ expect(result).toBe(false);
+ });
});
describe('findOverlappingEvents', () => {
- it('새 이벤트와 겹치는 모든 이벤트를 반환한다', () => {});
+ it('새 이벤트와 겹치는 모든 이벤트를 반환한다', () => {
+ const newEvent: Event = {
+ id: '3',
+ date: '2025-08-01',
+ startTime: '14:30',
+ endTime: '15:30',
+ } as Event;
+
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ startTime: '14:30',
+ endTime: '15:30',
+ },
+ {
+ id: '2',
+ date: '2025-08-05',
+ startTime: '08:30',
+ endTime: '20:30',
+ },
+ ] as Event[];
+
+ const overlappingEvents = findOverlappingEvents(newEvent, events);
+
+ expect(overlappingEvents).toEqual([events[0]]);
+ });
+
+ it('겹치는 이벤트가 없으면 빈 배열을 반환한다', () => {
+ const newEvent: Event = {
+ id: '3',
+ date: '2025-08-03',
+ startTime: '02:00',
+ endTime: '05:00',
+ } as Event;
+
+ const events: Event[] = [
+ {
+ id: '1',
+ date: '2025-08-01',
+ startTime: '14:30',
+ endTime: '15:30',
+ },
+ {
+ id: '2',
+ date: '2025-08-05',
+ startTime: '08:30',
+ endTime: '20:30',
+ },
+ ] as Event[];
+
+ const overlappingEvents = findOverlappingEvents(newEvent, events);
- it('겹치는 이벤트가 없으면 빈 배열을 반환한다', () => {});
+ expect(overlappingEvents).toEqual([]);
+ });
});
diff --git a/src/__tests__/unit/easy.eventUtils.spec.ts b/src/__tests__/unit/easy.eventUtils.spec.ts
index 8eef6371..cc28eab1 100644
--- a/src/__tests__/unit/easy.eventUtils.spec.ts
+++ b/src/__tests__/unit/easy.eventUtils.spec.ts
@@ -1,20 +1,266 @@
+import { expect } from 'vitest';
+
import { Event } from '../../types';
import { getFilteredEvents } from '../../utils/eventUtils';
describe('getFilteredEvents', () => {
- it("검색어 '이벤트 2'에 맞는 이벤트만 반환한다", () => {});
+ it("검색어 '이벤트 2'에 맞는 이벤트만 반환한다", () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+ const currentDate = new Date('2025-08-01');
+ const filteredEvents = getFilteredEvents(events, '이벤트 2', currentDate, 'week');
+ expect(filteredEvents).toEqual([
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
+
+ it('주간 뷰에서 2025-07-01 주의 이벤트만 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-07-20',
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+ const currentDate = new Date('2025-07-01');
+
+ const filteredEvents = getFilteredEvents(events, '', currentDate, 'week');
+
+ expect(filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
+
+ it('월간 뷰에서 2025년 7월의 모든 이벤트를 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-07-20',
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+ const currentDate = new Date('2025-07-01');
+
+ const filteredEvents = getFilteredEvents(events, '', currentDate, 'month');
+
+ expect(filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-07-20',
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
+
+ it("검색어 '이벤트'와 주간 뷰 필터링을 동시에 적용한다", () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '회의',
+ date: '2025-07-03',
+ description: '회의',
+ location: '회사',
+ },
+ {
+ id: '3',
+ title: '이벤트 2',
+ date: '2025-07-15',
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+ const currentDate = new Date('2025-07-01');
+
+ const filteredEvents = getFilteredEvents(events, '이벤트', currentDate, 'week');
+
+ expect(filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
+
+ it('검색어가 없을 때 모든 이벤트를 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+ const currentDate = new Date('2025-08-01');
+ const filteredEvents = getFilteredEvents(events, '', currentDate, 'week');
+ expect(filteredEvents).toEqual([
+ {
+ id: '1',
+ title: '이벤트 1',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '이벤트 2',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
- it('주간 뷰에서 2025-07-01 주의 이벤트만 반환한다', () => {});
+ it('검색어가 대소문자를 구분하지 않고 작동한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: 'Event 1',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: 'event 2',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+ const currentDate = new Date('2025-08-01');
+ const filteredEvents = getFilteredEvents(events, 'Event 2', currentDate, 'week');
+ expect(filteredEvents).toEqual([
+ {
+ id: '2',
+ title: 'event 2',
+ date: '2025-08-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
- it('월간 뷰에서 2025년 7월의 모든 이벤트를 반환한다', () => {});
+ it('월의 경계에 있는 이벤트를 올바르게 필터링한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '6월 마지막날',
+ date: '2025-06-30',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '7월 첫날',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '3',
+ title: '7월 다른주',
+ date: '2025-07-08',
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
- it("검색어 '이벤트'와 주간 뷰 필터링을 동시에 적용한다", () => {});
+ const result = getFilteredEvents(events, '', new Date('2025-07-01'), 'week');
- it('검색어가 없을 때 모든 이벤트를 반환한다', () => {});
+ expect(result).toEqual([
+ {
+ id: '1',
+ title: '6월 마지막날',
+ date: '2025-06-30',
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '7월 첫날',
+ date: '2025-07-01',
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
- it('검색어가 대소문자를 구분하지 않고 작동한다', () => {});
+ it('빈 이벤트 리스트에 대해 빈 배열을 반환한다', () => {
+ const events: Event[] = [];
- it('월의 경계에 있는 이벤트를 올바르게 필터링한다', () => {});
+ const result = getFilteredEvents(events, '', new Date('2025-07-01'), 'week');
- it('빈 이벤트 리스트에 대해 빈 배열을 반환한다', () => {});
+ expect(result).toEqual([]);
+ });
});
diff --git a/src/__tests__/unit/easy.fetchHolidays.spec.ts b/src/__tests__/unit/easy.fetchHolidays.spec.ts
index 013e87f0..93c4f82e 100644
--- a/src/__tests__/unit/easy.fetchHolidays.spec.ts
+++ b/src/__tests__/unit/easy.fetchHolidays.spec.ts
@@ -1,8 +1,36 @@
+import { expect } from 'vitest';
+
import { fetchHolidays } from '../../apis/fetchHolidays';
+
describe('fetchHolidays', () => {
- it('주어진 월의 공휴일만 반환한다', () => {});
+ it('주어진 월의 공휴일만 반환한다', () => {
+ const date = new Date('2025-01-01');
+ const holidays = fetchHolidays(date);
+
+ expect(holidays).toEqual({
+ '2025-01-01': '신정',
+ '2025-01-29': '설날',
+ '2025-01-30': '설날',
+ '2025-01-31': '설날',
+ });
+ });
+
+ it('공휴일이 없는 월에 대해 빈 객체를 반환한다', () => {
+ const date = new Date('2025-02-01');
+ const holidays = fetchHolidays(date);
+
+ expect(holidays).toEqual({});
+ });
- it('공휴일이 없는 월에 대해 빈 객체를 반환한다', () => {});
+ it('여러 공휴일이 있는 월에 대해 모든 공휴일을 반환한다', () => {
+ const date = new Date('2025-01-01');
+ const holidays = fetchHolidays(date);
- it('여러 공휴일이 있는 월에 대해 모든 공휴일을 반환한다', () => {});
+ expect(holidays).toEqual({
+ '2025-01-01': '신정',
+ '2025-01-29': '설날',
+ '2025-01-30': '설날',
+ '2025-01-31': '설날',
+ });
+ });
});
diff --git a/src/__tests__/unit/easy.notificationUtils.spec.ts b/src/__tests__/unit/easy.notificationUtils.spec.ts
index 2fe10360..9fe53d47 100644
--- a/src/__tests__/unit/easy.notificationUtils.spec.ts
+++ b/src/__tests__/unit/easy.notificationUtils.spec.ts
@@ -1,16 +1,164 @@
+import { expect } from 'vitest';
+
import { Event } from '../../types';
import { createNotificationMessage, getUpcomingEvents } from '../../utils/notificationUtils';
describe('getUpcomingEvents', () => {
- it('알림 시간이 정확히 도래한 이벤트를 반환한다', () => {});
+ it('알림 시간이 정확히 도래한 이벤트를 반환한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '알림 1',
+ date: '2025-08-20',
+ startTime: '14:15',
+ notificationTime: 15,
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '알림 2',
+ date: '2025-08-20',
+ startTime: '16:00',
+ notificationTime: 10,
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+
+ const now = new Date('2025-08-20T14:00:00');
+ const notifiedEvents: string[] = [];
+
+ const result = getUpcomingEvents(events, now, notifiedEvents);
+
+ expect(result).toEqual([
+ {
+ id: '1',
+ title: '알림 1',
+ date: '2025-08-20',
+ startTime: '14:15',
+ notificationTime: 15,
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
+
+ it('이미 알림이 간 이벤트는 제외한다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '이미 보낸 알림',
+ date: '2025-08-20',
+ startTime: '14:15',
+ notificationTime: 15,
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '아직 보내지 않은 알림',
+ date: '2025-08-20',
+ startTime: '14:15',
+ notificationTime: 10,
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+
+ const now = new Date('2025-08-20T14:05:00');
+ const notifiedEvents: string[] = ['1'];
+
+ const result = getUpcomingEvents(events, now, notifiedEvents);
+
+ expect(result).toEqual([
+ {
+ id: '2',
+ title: '아직 보내지 않은 알림',
+ date: '2025-08-20',
+ startTime: '14:15',
+ notificationTime: 10,
+ description: '이벤트',
+ location: '회사',
+ },
+ ]);
+ });
- it('이미 알림이 간 이벤트는 제외한다', () => {});
+ it('알림 시간이 아직 도래하지 않은 이벤트는 반환하지 않는다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '알림 1',
+ date: '2025-08-20',
+ startTime: '15:15',
+ notificationTime: 15,
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '알림 2',
+ date: '2025-08-20',
+ startTime: '16:00',
+ notificationTime: 10,
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
- it('알림 시간이 아직 도래하지 않은 이벤트는 반환하지 않는다', () => {});
+ const now = new Date('2025-08-20T14:00:00');
+ const notifiedEvents: string[] = [];
- it('알림 시간이 지난 이벤트는 반환하지 않는다', () => {});
+ const result = getUpcomingEvents(events, now, notifiedEvents);
+
+ expect(result).toEqual([]);
+ });
+
+ it('알림 시간이 지난 이벤트는 반환하지 않는다', () => {
+ const events: Event[] = [
+ {
+ id: '1',
+ title: '알림 1',
+ date: '2025-08-20',
+ startTime: '14:15',
+ notificationTime: 15,
+ description: '이벤트',
+ location: '회사',
+ },
+ {
+ id: '2',
+ title: '알림 2',
+ date: '2025-08-20',
+ startTime: '14:00',
+ notificationTime: 10,
+ description: '이벤트',
+ location: '회사',
+ },
+ ] as Event[];
+
+ const now = new Date('2025-08-20T14:30:00');
+ const notifiedEvents: string[] = [];
+
+ const result = getUpcomingEvents(events, now, notifiedEvents);
+
+ expect(result).toEqual([]);
+ });
});
describe('createNotificationMessage', () => {
- it('올바른 알림 메시지를 생성해야 한다', () => {});
+ it('올바른 알림 메시지를 생성해야 한다', () => {
+ const event: Event = {
+ id: '1',
+ title: '알림 1',
+ date: '2025-08-20',
+ startTime: '14:15',
+ notificationTime: 15,
+ description: '이벤트',
+ location: '회사',
+ } as Event;
+
+ const notificationMessage = createNotificationMessage(event);
+
+ expect(notificationMessage).toBe('15분 후 알림 1 일정이 시작됩니다.');
+ });
});
diff --git a/src/__tests__/unit/easy.timeValidation.spec.ts b/src/__tests__/unit/easy.timeValidation.spec.ts
index 9dda1954..bbbe6de2 100644
--- a/src/__tests__/unit/easy.timeValidation.spec.ts
+++ b/src/__tests__/unit/easy.timeValidation.spec.ts
@@ -1,15 +1,71 @@
+import { expect } from 'vitest';
+
import { getTimeErrorMessage } from '../../utils/timeValidation';
describe('getTimeErrorMessage >', () => {
- it('시작 시간이 종료 시간보다 늦을 때 에러 메시지를 반환한다', () => {});
+ it('시작 시간이 종료 시간보다 늦을 때 에러 메시지를 반환한다', () => {
+ const startTime = '14:00';
+ const endTime = '13:00';
+ const timeErrorMessage = getTimeErrorMessage(startTime, endTime);
+
+ expect(timeErrorMessage).toEqual({
+ startTimeError: '시작 시간은 종료 시간보다 빨라야 합니다.',
+ endTimeError: '종료 시간은 시작 시간보다 늦어야 합니다.',
+ });
+ });
+
+ it('시작 시간과 종료 시간이 같을 때 에러 메시지를 반환한다', () => {
+ const startTime = '14:00';
+ const endTime = '14:00';
+ const timeErrorMessage = getTimeErrorMessage(startTime, endTime);
+
+ expect(timeErrorMessage).toEqual({
+ startTimeError: '시작 시간은 종료 시간보다 빨라야 합니다.',
+ endTimeError: '종료 시간은 시작 시간보다 늦어야 합니다.',
+ });
+ });
+
+ it('시작 시간이 종료 시간보다 빠를 때 null을 반환한다', () => {
+ const startTime = '13:00';
+ const endTime = '14:00';
+ const timeErrorMessage = getTimeErrorMessage(startTime, endTime);
+
+ expect(timeErrorMessage).toEqual({
+ startTimeError: null,
+ endTimeError: null,
+ });
+ });
+
+ it('시작 시간이 비어있을 때 null을 반환한다', () => {
+ const startTime = '';
+ const endTime = '14:00';
+ const timeErrorMessage = getTimeErrorMessage(startTime, endTime);
- it('시작 시간과 종료 시간이 같을 때 에러 메시지를 반환한다', () => {});
+ expect(timeErrorMessage).toEqual({
+ startTimeError: null,
+ endTimeError: null,
+ });
+ });
- it('시작 시간이 종료 시간보다 빠를 때 null을 반환한다', () => {});
+ it('종료 시간이 비어있을 때 null을 반환한다', () => {
+ const startTime = '13:00';
+ const endTime = '';
+ const timeErrorMessage = getTimeErrorMessage(startTime, endTime);
- it('시작 시간이 비어있을 때 null을 반환한다', () => {});
+ expect(timeErrorMessage).toEqual({
+ startTimeError: null,
+ endTimeError: null,
+ });
+ });
- it('종료 시간이 비어있을 때 null을 반환한다', () => {});
+ it('시작 시간과 종료 시간이 모두 비어있을 때 null을 반환한다', () => {
+ const startTime = '';
+ const endTime = '';
+ const timeErrorMessage = getTimeErrorMessage(startTime, endTime);
- it('시작 시간과 종료 시간이 모두 비어있을 때 null을 반환한다', () => {});
+ expect(timeErrorMessage).toEqual({
+ startTimeError: null,
+ endTimeError: null,
+ });
+ });
});
diff --git a/src/components/EventForm.tsx b/src/components/EventForm.tsx
new file mode 100644
index 00000000..8104f65b
--- /dev/null
+++ b/src/components/EventForm.tsx
@@ -0,0 +1,202 @@
+import {
+ Button,
+ Checkbox,
+ FormControl,
+ FormControlLabel,
+ FormLabel,
+ MenuItem,
+ Select,
+ Stack,
+ TextField,
+ Tooltip,
+ Typography,
+} from '@mui/material';
+import React from 'react';
+
+import { notificationOptions } from '../constants/notifications';
+import { Event } from '../types';
+import { getTimeErrorMessage } from '../utils/timeValidation';
+
+const categories = ['업무', '개인', '가족', '기타'];
+
+interface EventFormProps {
+ title: string;
+ setTitle: (title: string) => void;
+ date: string;
+ setDate: (date: string) => void;
+ startTime: string;
+ endTime: string;
+ description: string;
+ setDescription: (description: string) => void;
+ location: string;
+ setLocation: (location: string) => void;
+ category: string;
+ setCategory: (category: string) => void;
+ isRepeating: boolean;
+ setIsRepeating: (isRepeating: boolean) => void;
+ notificationTime: number;
+ setNotificationTime: (time: number) => void;
+ startTimeError: string | null;
+ endTimeError: string | null;
+ editingEvent: Event | null;
+ handleStartTimeChange: (e: React.ChangeEvent) => void;
+ handleEndTimeChange: (e: React.ChangeEvent) => void;
+ onSubmit: () => void;
+}
+
+const EventForm = ({
+ title,
+ setTitle,
+ date,
+ setDate,
+ startTime,
+ endTime,
+ description,
+ setDescription,
+ location,
+ setLocation,
+ category,
+ setCategory,
+ isRepeating,
+ setIsRepeating,
+ notificationTime,
+ setNotificationTime,
+ startTimeError,
+ endTimeError,
+ editingEvent,
+ handleStartTimeChange,
+ handleEndTimeChange,
+ onSubmit,
+}: EventFormProps) => {
+ return (
+
+ {editingEvent ? '일정 수정' : '일정 추가'}
+
+
+ 제목
+ setTitle(e.target.value)}
+ />
+
+
+
+ 날짜
+ setDate(e.target.value)}
+ />
+
+
+
+
+ 시작 시간
+
+ getTimeErrorMessage(startTime, endTime)}
+ error={!!startTimeError}
+ />
+
+
+
+ 종료 시간
+
+ getTimeErrorMessage(startTime, endTime)}
+ error={!!endTimeError}
+ />
+
+
+
+
+
+ 설명
+ setDescription(e.target.value)}
+ />
+
+
+
+ 위치
+ setLocation(e.target.value)}
+ />
+
+
+
+ 카테고리
+
+
+
+
+ setIsRepeating(e.target.checked)} />
+ }
+ label="반복 일정"
+ />
+
+
+
+ 알림 설정
+
+
+
+
+
+ );
+};
+
+export default EventForm;
diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts
new file mode 100644
index 00000000..145d5b5c
--- /dev/null
+++ b/src/constants/notifications.ts
@@ -0,0 +1,7 @@
+export const notificationOptions = [
+ { value: 1, label: '1분 전' },
+ { value: 10, label: '10분 전' },
+ { value: 60, label: '1시간 전' },
+ { value: 120, label: '2시간 전' },
+ { value: 1440, label: '1일 전' },
+];
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index f9ec573b..6adbe79a 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -3,6 +3,10 @@ import { useEffect, useState } from 'react';
import { Event } from '../types';
import { createNotificationMessage, getUpcomingEvents } from '../utils/notificationUtils';
+/**
+ * 일정 알림 시스템을 관리하는 훅
+ * 이벤트 배열을 받아서 현재 시간 기준으로 알림이 필요한 이벤트들을 감지하고 알림을 생성/관리
+ */
export const useNotifications = (events: Event[]) => {
const [notifications, setNotifications] = useState<{ id: string; message: string }[]>([]);
const [notifiedEvents, setNotifiedEvents] = useState([]);
diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts
index be78512c..d31672bf 100644
--- a/src/utils/dateUtils.ts
+++ b/src/utils/dateUtils.ts
@@ -4,6 +4,7 @@ import { Event } from '../types.ts';
* 주어진 년도와 월의 일수를 반환합니다.
*/
export function getDaysInMonth(year: number, month: number): number {
+ if (month < 1 || month > 12) return 0;
return new Date(year, month, 0).getDate();
}
@@ -23,6 +24,9 @@ export function getWeekDates(date: Date): Date[] {
return weekDates;
}
+/**
+ * 특정 월의 모든 주를 2차원 배열로 반환
+ */
export function getWeeksAtMonth(currentDate: Date) {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
@@ -51,10 +55,16 @@ export function getWeeksAtMonth(currentDate: Date) {
return weeks;
}
+/**
+ * 특정 날짜(일)에 해당하는 이벤트들만 필터링하여 반환
+ */
export function getEventsForDay(events: Event[], date: number): Event[] {
return events.filter((event) => new Date(event.date).getDate() === date);
}
+/**
+ * 주어진 날짜가 속한 월의 몇 번쨰 주인지를 "YYYY년 M월 N주" 형식으로 반환
+ */
export function formatWeek(targetDate: Date) {
const dayOfWeek = targetDate.getDay();
const diffToThursday = 4 - dayOfWeek;
@@ -97,10 +107,16 @@ export function isDateInRange(date: Date, rangeStart: Date, rangeEnd: Date): boo
return normalizedDate >= normalizedStart && normalizedDate <= normalizedEnd;
}
+/**
+ * 숫자를 지정된 자릿수로 만들기 위해 앞에 0을 채워주는 함수
+ */
export function fillZero(value: number, size = 2) {
return String(value).padStart(size, '0');
}
+/**
+ * Date 객체를 'YYYY-MM-DD" 형식의 문자열로 변환
+ */
export function formatDate(currentDate: Date, day?: number) {
return [
currentDate.getFullYear(),
diff --git a/src/utils/eventOverlap.ts b/src/utils/eventOverlap.ts
index 8412cee7..85dce904 100644
--- a/src/utils/eventOverlap.ts
+++ b/src/utils/eventOverlap.ts
@@ -4,6 +4,9 @@ export function parseDateTime(date: string, time: string) {
return new Date(`${date}T${time}`);
}
+/**
+ * 이벤트 객체의 날짜와 시작/종료 시간을 완전한 Date 객체 범위로 변환
+ */
export function convertEventToDateRange({ date, startTime, endTime }: Event | EventForm) {
return {
start: parseDateTime(date, startTime),
diff --git a/src/utils/eventUtils.ts b/src/utils/eventUtils.ts
index 9e75e947..49fc77c2 100644
--- a/src/utils/eventUtils.ts
+++ b/src/utils/eventUtils.ts
@@ -38,6 +38,9 @@ function filterEventsByDateRangeAtMonth(events: Event[], currentDate: Date) {
return filterEventsByDateRange(events, monthStart, monthEnd);
}
+/**
+ * 주어진 이벤트 배열에서 검색어와 날짜 밤위를 기준으로 필터링된 이벤트들을 반환
+ */
export function getFilteredEvents(
events: Event[],
searchTerm: string,
diff --git a/src/utils/notificationUtils.ts b/src/utils/notificationUtils.ts
index 16ba8bdd..291063f3 100644
--- a/src/utils/notificationUtils.ts
+++ b/src/utils/notificationUtils.ts
@@ -3,6 +3,9 @@ import { Event } from '../types';
const 초 = 1000;
const 분 = 초 * 60;
+/**
+ * 현재 시간을 기준으로 알림이 필요한 임박한 이벤트들을 필터링하요 반환
+ */
export function getUpcomingEvents(events: Event[], now: Date, notifiedEvents: string[]) {
return events.filter((event) => {
const eventStart = new Date(`${event.date}T${event.startTime}`);