feat: Introduce knex for database migrations

- Adds knex.js to manage database schema changes systematically.
- Creates an initial migration file based on `hyungi_schema_v2.sql` to represent the current database state.
- Adds npm scripts (`db:migrate`, `db:rollback`, etc.) for easy execution of migration tasks.
- Archives legacy SQL files and old migration scripts into the `db_archive/` directory to prevent confusion and clean up the project structure.
This commit is contained in:
Hyungi Ahn
2025-12-19 09:43:09 +09:00
parent 9206672b63
commit b67362a733
28 changed files with 5665 additions and 9 deletions

View File

@@ -0,0 +1,49 @@
const fs = require('fs');
const path = require('path');
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
const schemaSql = fs.readFileSync(path.join(__dirname, '../../hyungi_schema_v2.sql'), 'utf8');
return knex.raw(schemaSql);
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
// down 마이그레이션은 모든 테이블을 역순으로 삭제하도록 구현합니다.
const tables = [
'cutting_plans',
'daily_issue_reports',
'daily_work_reports',
'codes',
'code_types',
'factory_info',
'equipment_list',
'pipe_specs',
'tasks',
'worker_groups',
'workers',
'projects',
'password_change_logs',
'login_logs',
'users'
];
// 외래 키 제약 조건을 먼저 비활성화합니다.
return knex.raw('SET FOREIGN_KEY_CHECKS = 0;')
.then(() => {
// 각 테이블을 순회하며 drop table if exists를 실행합니다.
return tables.reduce((promise, tableName) => {
return promise.then(() => knex.schema.dropTableIfExists(tableName));
}, Promise.resolve());
})
.finally(() => {
// 외래 키 제약 조건을 다시 활성화합니다.
return knex.raw('SET FOREIGN_KEY_CHECKS = 1;');
});
};

View File

@@ -0,0 +1,91 @@
/**
* Jest 설정 파일
*
* TK-FB-Project 테스트 환경 설정
*/
module.exports = {
// Node.js 환경에서 테스트 실행
testEnvironment: 'node',
// 커버리지 리포트 저장 디렉토리
coverageDirectory: 'coverage',
// 커버리지 수집 대상 파일
collectCoverageFrom: [
'controllers/**/*.js',
'services/**/*.js',
'models/**/*.js',
'middlewares/**/*.js',
'utils/**/*.js',
'!**/node_modules/**',
'!**/coverage/**',
'!**/tests/**',
'!index.js' // 서버 시작 파일 제외
],
// 테스트 파일 패턴
testMatch: [
'**/tests/**/*.test.js',
'**/tests/**/*.spec.js'
],
// 테스트 실행 전 설정 파일
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
// 상세 출력 모드
verbose: true,
// 커버리지 수집 활성화
collectCoverage: false, // 기본은 false, npm run test:coverage에서만 true
// 커버리지 리포트 형식
coverageReporters: ['text', 'lcov', 'html', 'json-summary'],
// 커버리지 임계값 (이 값 이하면 테스트 실패)
coverageThreshold: {
global: {
branches: 50,
functions: 50,
lines: 50,
statements: 50
}
},
// 테스트 타임아웃 (밀리초)
testTimeout: 10000,
// 모듈 경로 매핑 (선택사항)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'^@controllers/(.*)$': '<rootDir>/controllers/$1',
'^@services/(.*)$': '<rootDir>/services/$1',
'^@models/(.*)$': '<rootDir>/models/$1',
'^@utils/(.*)$': '<rootDir>/utils/$1',
'^@middlewares/(.*)$': '<rootDir>/middlewares/$1'
},
// 무시할 경로
testPathIgnorePatterns: [
'/node_modules/',
'/coverage/',
'/dist/'
],
// 병렬 테스트 실행 워커 수 (기본값: CPU 코어 수 - 1)
// maxWorkers: 4,
// 실패한 테스트만 재실행 옵션
// bail: 1, // 첫 번째 실패 시 중단
// 테스트 결과 리포터
reporters: [
'default',
// ['jest-junit', { outputDirectory: 'coverage', outputName: 'junit.xml' }]
],
// 글로벌 설정
globals: {
'NODE_ENV': 'test'
}
};

View File

@@ -0,0 +1,57 @@
require('dotenv').config();
module.exports = {
development: {
client: 'mysql2',
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
socketPath: process.env.DB_SOCKET
},
migrations: {
directory: './db/migrations'
},
seeds: {
directory: './db/seeds'
}
},
staging: {
client: 'mysql2',
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
socketPath: process.env.DB_SOCKET
},
migrations: {
directory: './db/migrations'
},
seeds: {
directory: './db/seeds'
}
},
production: {
client: 'mysql2',
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
socketPath: process.env.DB_SOCKET
},
migrations: {
directory: './db/migrations'
},
seeds: {
directory: './db/seeds'
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,18 @@
"main": "index.js",
"scripts": {
"start": "pm2-runtime start ecosystem.config.js --env production",
"dev": "pm2-runtime start ecosystem.config.js --env development"
"dev": "pm2-runtime start ecosystem.config.js --env development",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest tests/unit",
"test:integration": "jest tests/integration",
"test:verbose": "jest --verbose",
"db:migrate": "knex migrate:latest --knexfile knexfile.js",
"db:migrate:make": "knex migrate:make --knexfile knexfile.js",
"db:rollback": "knex migrate:rollback --knexfile knexfile.js",
"db:seed": "knex seed:run --knexfile knexfile.js",
"db:seed:make": "knex seed:make --knexfile knexfile.js"
},
"dependencies": {
"@simplewebauthn/server": "^13.1.1",
@@ -19,6 +30,7 @@
"express-validator": "^7.2.1",
"helmet": "^7.2.0",
"jsonwebtoken": "^9.0.0",
"knex": "^3.1.0",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.14.1",
"node-cache": "^5.1.2",
@@ -28,5 +40,10 @@
"sqlite3": "^5.1.6",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"supertest": "^7.0.0"
}
}

View File

@@ -0,0 +1,170 @@
# 테스트 시작하기
## 빠른 시작
### 1단계: 테스트 패키지 설치
```bash
cd /Users/hyungiahn/Documents/code/TK-FB-Project/api.hyungi.net
npm install
```
### 2단계: 첫 번째 테스트 실행
```bash
# 모든 테스트 실행
npm test
# 특정 파일만 테스트
npm test -- tests/unit/services/workReportService.test.js
# Watch 모드 (파일 변경 시 자동 재실행)
npm run test:watch
```
### 3단계: 커버리지 확인
```bash
# 커버리지와 함께 테스트 실행
npm run test:coverage
# 커버리지 리포트 HTML 보기
open coverage/lcov-report/index.html
```
---
## 디렉토리 구조
```
tests/
├── README.md # 이 파일
├── setup.js # Jest 전역 설정
├── helpers/
│ └── mockData.js # 테스트용 더미 데이터
├── unit/
│ ├── services/ # 서비스 레이어 테스트
│ │ └── workReportService.test.js # ✅ 작성 완료 (예제)
│ ├── controllers/ # 컨트롤러 테스트 (미작성)
│ └── models/ # 모델 테스트 (미작성)
└── integration/ # 통합 테스트 (미작성)
```
---
## 다음 단계: 테스트 파일 작성
### 우선순위 1: 서비스 레이어 테스트 작성
다음 서비스들에 대한 테스트 파일을 작성하세요:
```bash
tests/unit/services/
├── workReportService.test.js # ✅ 완료
├── attendanceService.test.js # ⬜ TODO
├── dailyWorkReportService.test.js # ⬜ TODO
├── workerService.test.js # ⬜ TODO (간단)
├── projectService.test.js # ⬜ TODO (간단)
├── issueTypeService.test.js # ⬜ TODO (간단)
├── toolsService.test.js # ⬜ TODO
├── dailyIssueReportService.test.js # ⬜ TODO
├── uploadService.test.js # ⬜ TODO (간단)
└── analysisService.test.js # ⬜ TODO
```
**추천 작성 순서:**
1. **workReportService.test.js** ✅ (이미 완료 - 참고용)
2. **workerService.test.js** (간단한 CRUD, 시작하기 좋음)
3. **projectService.test.js** (간단한 CRUD)
4. **attendanceService.test.js** (복잡도 중간)
5. **나머지 서비스들...**
### 우선순위 2: 컨트롤러 테스트
서비스 레이어 테스트가 어느정도 완료되면 컨트롤러 테스트를 시작하세요.
### 우선순위 3: 통합 테스트
마지막으로 실제 HTTP 요청을 테스트하는 통합 테스트를 작성하세요.
---
## 예제 테스트 작성 방법
### 서비스 레이어 테스트 템플릿
```javascript
const serviceName = require('../../../services/serviceName');
const { ValidationError, NotFoundError, DatabaseError } = require('../../../utils/errors');
// 모델 모킹
jest.mock('../../../models/modelName');
const modelName = require('../../../models/modelName');
describe('ServiceName', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('methodName', () => {
it('should succeed when valid data provided', async () => {
// Arrange
const mockData = { /* ... */ };
modelName.method.mockResolvedValue(mockData);
// Act
const result = await serviceName.methodName(params);
// Assert
expect(result).toEqual(expectedResult);
expect(modelName.method).toHaveBeenCalledWith(expectedParams);
});
it('should throw ValidationError when invalid data', async () => {
// Act & Assert
await expect(serviceName.methodName(invalidParams))
.rejects.toThrow(ValidationError);
});
});
});
```
---
## 유용한 명령어
```bash
# 특정 describe 블록만 테스트
npm test -- --testNamePattern="createWorkReportService"
# 실패한 테스트만 재실행
npm test -- --onlyFailures
# 병렬 실행 비활성화 (디버깅 시)
npm test -- --runInBand
# 상세 출력
npm run test:verbose
```
---
## 목표 커버리지
- **서비스 레이어**: 80% 이상
- **컨트롤러**: 70% 이상
- **전체**: 75% 이상
---
## 도움말
전체 가이드는 프로젝트 루트의 `TESTING_GUIDE.md`를 참고하세요:
```bash
cat /Users/hyungiahn/Documents/code/TK-FB-Project/TESTING_GUIDE.md
```
---
**Happy Testing! 🧪**

View File

@@ -0,0 +1,225 @@
/**
* 테스트용 더미 데이터
*
* 테스트에서 재사용 가능한 모킹 데이터
*/
// 사용자 더미 데이터
const mockUsers = {
admin: {
id: 1,
username: 'admin',
role: 'admin',
access_level: 5
},
groupLeader: {
id: 2,
username: 'group_leader',
role: 'group_leader',
access_level: 3
},
worker: {
id: 3,
username: 'worker',
role: 'worker',
access_level: 1
}
};
// 작업자 더미 데이터
const mockWorkers = [
{
worker_id: 1,
worker_name: '김철수',
job_type: 'developer',
is_active: 1,
created_at: '2025-01-01 00:00:00'
},
{
worker_id: 2,
worker_name: '이영희',
job_type: 'designer',
is_active: 1,
created_at: '2025-01-01 00:00:00'
},
{
worker_id: 3,
worker_name: '박민수',
job_type: 'developer',
is_active: 0,
created_at: '2025-01-01 00:00:00'
}
];
// 프로젝트 더미 데이터
const mockProjects = [
{
project_id: 1,
project_name: '프로젝트 A',
job_no: 'PRJ-001',
created_at: '2025-01-01 00:00:00'
},
{
project_id: 2,
project_name: '프로젝트 B',
job_no: 'PRJ-002',
created_at: '2025-01-15 00:00:00'
}
];
// 작업 유형 더미 데이터
const mockWorkTypes = [
{ id: 1, name: '개발' },
{ id: 2, name: '디자인' },
{ id: 3, name: '테스트' },
{ id: 4, name: '회의' }
];
// 에러 유형 더미 데이터
const mockErrorTypes = [
{ id: 1, name: '버그 수정' },
{ id: 2, name: '재작업' },
{ id: 3, name: '긴급 수정' }
];
// 작업 보고서 더미 데이터
const mockWorkReports = [
{
id: 1,
report_date: '2025-12-11',
worker_id: 1,
project_id: 1,
work_type_id: 1,
work_hours: 8.0,
work_content: '기능 개발',
error_type_id: null,
work_status_id: 1,
created_by: 1,
created_at: '2025-12-11 09:00:00',
updated_at: '2025-12-11 09:00:00'
},
{
id: 2,
report_date: '2025-12-11',
worker_id: 2,
project_id: 1,
work_type_id: 2,
work_hours: 7.5,
work_content: 'UI 디자인',
error_type_id: null,
work_status_id: 1,
created_by: 2,
created_at: '2025-12-11 09:00:00',
updated_at: '2025-12-11 09:00:00'
},
{
id: 3,
report_date: '2025-12-11',
worker_id: 1,
project_id: 2,
work_type_id: 1,
work_hours: 2.0,
work_content: '버그 수정',
error_type_id: 1,
work_status_id: 2,
created_by: 1,
created_at: '2025-12-11 14:00:00',
updated_at: '2025-12-11 14:00:00'
}
];
// 출석 더미 데이터
const mockAttendance = [
{
id: 1,
worker_id: 1,
attendance_date: '2025-12-11',
status: 'work',
regular_hours: 8.0,
overtime_hours: 0.0,
created_by: 1,
created_at: '2025-12-11 09:00:00'
},
{
id: 2,
worker_id: 2,
attendance_date: '2025-12-11',
status: 'vacation',
regular_hours: 0.0,
overtime_hours: 0.0,
created_by: 2,
created_at: '2025-12-11 09:00:00'
}
];
// 분석 데이터 더미
const mockAnalysisData = {
summary: {
total_entries: 100,
total_hours: 800.0,
unique_workers: 10,
unique_projects: 5,
working_days: 20,
avg_hours_per_entry: 8.0,
contributors: 3,
error_entries: 5,
error_rate: 5.0
},
dailyStats: [
{ report_date: '2025-12-01', daily_hours: 40.0, daily_entries: 5, daily_workers: 5 },
{ report_date: '2025-12-02', daily_hours: 35.0, daily_entries: 5, daily_workers: 4 }
]
};
// API 응답 형식
const createSuccessResponse = (data, message = '성공') => ({
success: true,
data,
message
});
const createErrorResponse = (error, message = '오류 발생') => ({
success: false,
error,
message
});
// 날짜 헬퍼
const createDateString = (daysFromToday = 0) => {
const date = new Date();
date.setDate(date.getDate() + daysFromToday);
return date.toISOString().split('T')[0];
};
// DB 쿼리 결과 모킹 헬퍼
const createDbResult = (data, affectedRows = 1, insertId = 1) => {
if (Array.isArray(data)) {
return [data, []]; // [rows, fields]
}
return [
{
affectedRows,
insertId,
changedRows: affectedRows
},
[]
];
};
module.exports = {
// 더미 데이터
mockUsers,
mockWorkers,
mockProjects,
mockWorkTypes,
mockErrorTypes,
mockWorkReports,
mockAttendance,
mockAnalysisData,
// 헬퍼 함수
createSuccessResponse,
createErrorResponse,
createDateString,
createDbResult
};

View File

@@ -0,0 +1,67 @@
/**
* Jest 전역 테스트 설정
*
* 모든 테스트 실행 전 자동으로 로드됩니다
*/
// 환경 변수 설정 (테스트 환경)
process.env.NODE_ENV = 'test';
process.env.LOG_LEVEL = 'error'; // 테스트 시 로그 최소화
// 타임존 설정
process.env.TZ = 'Asia/Seoul';
// 전역 타임아웃 확장 (필요 시)
jest.setTimeout(10000);
// 콘솔 출력 모킹 (선택사항 - 테스트 출력을 깔끔하게)
// global.console = {
// ...console,
// log: jest.fn(),
// debug: jest.fn(),
// info: jest.fn(),
// warn: jest.fn(),
// error: jest.fn(),
// };
// 모든 테스트 실행 전
beforeAll(() => {
// 전역 설정
});
// 각 테스트 파일 실행 전
beforeEach(() => {
// 각 테스트 전 초기화
});
// 각 테스트 파일 실행 후
afterEach(() => {
// 모킹 정리
jest.clearAllMocks();
});
// 모든 테스트 실행 후
afterAll(() => {
// 정리 작업
});
// 커스텀 매처 (선택사항)
expect.extend({
// 날짜 문자열 검증
toBeValidDate(received) {
const isValid = !isNaN(Date.parse(received));
return {
message: () => `expected ${received} to be a valid date string`,
pass: isValid
};
},
// ID 검증
toBeValidId(received) {
const isValid = Number.isInteger(received) && received > 0;
return {
message: () => `expected ${received} to be a valid ID (positive integer)`,
pass: isValid
};
}
});

View File

@@ -0,0 +1,321 @@
/**
* WorkReportService 테스트
*
* 작업 보고서 서비스 레이어 단위 테스트
*/
const workReportService = require('../../../services/workReportService');
const { ValidationError, NotFoundError, DatabaseError } = require('../../../utils/errors');
const { mockWorkReports } = require('../../helpers/mockData');
// 모델 모킹
jest.mock('../../../models/workReportModel');
const workReportModel = require('../../../models/workReportModel');
describe('WorkReportService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('createWorkReportService', () => {
it('단일 보고서를 성공적으로 생성해야 함', async () => {
// Arrange
const reportData = {
report_date: '2025-12-11',
worker_id: 1,
project_id: 1,
work_type_id: 1,
work_hours: 8.0,
work_content: '기능 개발'
};
// workReportModel.create가 콜백 형태이므로 모킹 설정
workReportModel.create = jest.fn((data, callback) => {
callback(null, 123); // insertId = 123
});
// Act
const result = await workReportService.createWorkReportService(reportData);
// Assert
expect(result).toEqual({ workReport_ids: [123] });
expect(workReportModel.create).toHaveBeenCalledTimes(1);
expect(workReportModel.create).toHaveBeenCalledWith(
reportData,
expect.any(Function)
);
});
it('다중 보고서를 성공적으로 생성해야 함', async () => {
// Arrange
const reportsData = [
{ report_date: '2025-12-11', worker_id: 1, work_hours: 8 },
{ report_date: '2025-12-11', worker_id: 2, work_hours: 7 }
];
let callCount = 0;
workReportModel.create = jest.fn((data, callback) => {
callCount++;
callback(null, 100 + callCount);
});
// Act
const result = await workReportService.createWorkReportService(reportsData);
// Assert
expect(result).toEqual({ workReport_ids: [101, 102] });
expect(workReportModel.create).toHaveBeenCalledTimes(2);
});
it('빈 배열이면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.createWorkReportService([]))
.rejects.toThrow(ValidationError);
await expect(workReportService.createWorkReportService([]))
.rejects.toThrow('보고서 데이터가 필요합니다');
});
it('DB 오류 시 DatabaseError를 던져야 함', async () => {
// Arrange
const reportData = { report_date: '2025-12-11', worker_id: 1 };
workReportModel.create = jest.fn((data, callback) => {
callback(new Error('DB connection failed'), null);
});
// Act & Assert
await expect(workReportService.createWorkReportService(reportData))
.rejects.toThrow(DatabaseError);
});
});
describe('getWorkReportsByDateService', () => {
it('날짜로 보고서를 조회해야 함', async () => {
// Arrange
const date = '2025-12-11';
const mockReports = mockWorkReports.filter(r => r.report_date === date);
workReportModel.getAllByDate = jest.fn((date, callback) => {
callback(null, mockReports);
});
// Act
const result = await workReportService.getWorkReportsByDateService(date);
// Assert
expect(result).toEqual(mockReports);
expect(workReportModel.getAllByDate).toHaveBeenCalledWith(date, expect.any(Function));
});
it('날짜가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportsByDateService(null))
.rejects.toThrow(ValidationError);
await expect(workReportService.getWorkReportsByDateService(null))
.rejects.toThrow('날짜가 필요합니다');
});
it('DB 오류 시 DatabaseError를 던져야 함', async () => {
// Arrange
workReportModel.getAllByDate = jest.fn((date, callback) => {
callback(new Error('DB error'), null);
});
// Act & Assert
await expect(workReportService.getWorkReportsByDateService('2025-12-11'))
.rejects.toThrow(DatabaseError);
});
});
describe('getWorkReportByIdService', () => {
it('ID로 보고서를 조회해야 함', async () => {
// Arrange
const mockReport = mockWorkReports[0];
workReportModel.getById = jest.fn((id, callback) => {
callback(null, mockReport);
});
// Act
const result = await workReportService.getWorkReportByIdService(1);
// Assert
expect(result).toEqual(mockReport);
expect(workReportModel.getById).toHaveBeenCalledWith(1, expect.any(Function));
});
it('보고서가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.getById = jest.fn((id, callback) => {
callback(null, null);
});
// Act & Assert
await expect(workReportService.getWorkReportByIdService(999))
.rejects.toThrow(NotFoundError);
await expect(workReportService.getWorkReportByIdService(999))
.rejects.toThrow('작업 보고서를 찾을 수 없습니다');
});
it('ID가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportByIdService(null))
.rejects.toThrow(ValidationError);
});
});
describe('updateWorkReportService', () => {
it('보고서를 성공적으로 수정해야 함', async () => {
// Arrange
const updateData = { work_hours: 9 };
workReportModel.update = jest.fn((id, data, callback) => {
callback(null, 1); // affectedRows = 1
});
// Act
const result = await workReportService.updateWorkReportService(123, updateData);
// Assert
expect(result).toEqual({ changes: 1 });
expect(workReportModel.update).toHaveBeenCalledWith(
123,
updateData,
expect.any(Function)
);
});
it('수정할 보고서가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.update = jest.fn((id, data, callback) => {
callback(null, 0); // affectedRows = 0
});
// Act & Assert
await expect(workReportService.updateWorkReportService(999, {}))
.rejects.toThrow(NotFoundError);
});
it('ID가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.updateWorkReportService(null, {}))
.rejects.toThrow(ValidationError);
});
});
describe('removeWorkReportService', () => {
it('보고서를 성공적으로 삭제해야 함', async () => {
// Arrange
workReportModel.remove = jest.fn((id, callback) => {
callback(null, 1);
});
// Act
const result = await workReportService.removeWorkReportService(123);
// Assert
expect(result).toEqual({ changes: 1 });
expect(workReportModel.remove).toHaveBeenCalledWith(123, expect.any(Function));
});
it('삭제할 보고서가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.remove = jest.fn((id, callback) => {
callback(null, 0);
});
// Act & Assert
await expect(workReportService.removeWorkReportService(999))
.rejects.toThrow(NotFoundError);
});
it('ID가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.removeWorkReportService(null))
.rejects.toThrow(ValidationError);
});
});
describe('getWorkReportsInRangeService', () => {
it('기간으로 보고서를 조회해야 함', async () => {
// Arrange
const start = '2025-12-01';
const end = '2025-12-31';
workReportModel.getByRange = jest.fn((start, end, callback) => {
callback(null, mockWorkReports);
});
// Act
const result = await workReportService.getWorkReportsInRangeService(start, end);
// Assert
expect(result).toEqual(mockWorkReports);
expect(workReportModel.getByRange).toHaveBeenCalledWith(
start,
end,
expect.any(Function)
);
});
it('시작일이 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportsInRangeService(null, '2025-12-31'))
.rejects.toThrow(ValidationError);
});
it('종료일이 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportsInRangeService('2025-12-01', null))
.rejects.toThrow(ValidationError);
});
});
describe('getSummaryService', () => {
it('월간 요약을 조회해야 함', async () => {
// Arrange
const year = '2025';
const month = '12';
workReportModel.getByRange = jest.fn((start, end, callback) => {
callback(null, mockWorkReports);
});
// Act
const result = await workReportService.getSummaryService(year, month);
// Assert
expect(result).toEqual(mockWorkReports);
expect(workReportModel.getByRange).toHaveBeenCalled();
});
it('연도가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getSummaryService(null, '12'))
.rejects.toThrow(ValidationError);
});
it('월이 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getSummaryService('2025', null))
.rejects.toThrow(ValidationError);
});
it('데이터가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.getByRange = jest.fn((start, end, callback) => {
callback(null, []);
});
// Act & Assert
await expect(workReportService.getSummaryService('2025', '12'))
.rejects.toThrow(NotFoundError);
await expect(workReportService.getSummaryService('2025', '12'))
.rejects.toThrow('해당 기간의 작업 보고서가 없습니다');
});
});
});