refactor(db,frontend): Improve queries and modularize frontend
- Replaced SELECT* queries in 8 models with explicit columns. - Began modularizing work-report-calendar.js by creating CalendarAPI.js, CalendarState.js, and CalendarView.js. - Refactored manage-project.js to use global API helpers. - Fixed API container crash by adding missing volume mounts to docker-compose.yml. - Added new migration for missing columns in the projects table. - Documented current DB schema and deployment notes.
This commit is contained in:
166
_archive/DATABASE_SCHEMA.md
Normal file
166
_archive/DATABASE_SCHEMA.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# TK-FB 프로젝트 데이터베이스 스키마
|
||||
|
||||
**업데이트 날짜**: 2025-11-03
|
||||
**데이터베이스**: hyungi
|
||||
**구조**: v1 (원본 JSON 데이터 호환)
|
||||
|
||||
## 📊 메인 테이블
|
||||
|
||||
### `daily_work_reports` - 일일 작업 보고서
|
||||
```sql
|
||||
- id (PK, AUTO_INCREMENT)
|
||||
- report_date (DATE, NOT NULL) - 작업 날짜
|
||||
- worker_id (INT, NOT NULL) - 작업자 ID
|
||||
- project_id (INT, NOT NULL) - 프로젝트 ID
|
||||
- work_type_id (INT, NOT NULL) - 작업 유형 ID
|
||||
- work_status_id (INT, DEFAULT 1) - 작업 상태 ID (1:정규, 2:에러)
|
||||
- error_type_id (INT, NULL) - 에러 유형 ID
|
||||
- work_hours (DECIMAL(4,2), NOT NULL) - 작업 시간
|
||||
- created_at, updated_at (TIMESTAMP)
|
||||
- created_by (INT, DEFAULT 1) - 작성자 user_id
|
||||
- updated_by (INT, NULL) - 수정자 user_id
|
||||
```
|
||||
|
||||
## 👥 마스터 데이터 테이블
|
||||
|
||||
### `workers` - 작업자 정보
|
||||
```sql
|
||||
- worker_id (PK, AUTO_INCREMENT)
|
||||
- worker_name (VARCHAR(100), NOT NULL) - 작업자명
|
||||
- join_date (DATE) - 입사일
|
||||
- job_type (VARCHAR(100)) - 직종
|
||||
- salary (DECIMAL(10,2)) - 급여
|
||||
- annual_leave (INT) - 연차
|
||||
- status (TEXT, DEFAULT 'active') - 상태
|
||||
- created_at, updated_at (TIMESTAMP)
|
||||
```
|
||||
|
||||
### `projects` - 프로젝트 정보
|
||||
```sql
|
||||
- project_id (PK, AUTO_INCREMENT)
|
||||
- job_no (VARCHAR(50), NOT NULL) - 작업 번호
|
||||
- project_name (VARCHAR(255), NOT NULL) - 프로젝트명
|
||||
- contract_date (DATE) - 계약일
|
||||
- due_date (DATE) - 완료 예정일
|
||||
- delivery_method (VARCHAR(100)) - 납품 방법
|
||||
- site (VARCHAR(100)) - 현장
|
||||
- pm (VARCHAR(100)) - 프로젝트 매니저
|
||||
- created_at, updated_at (TIMESTAMP)
|
||||
```
|
||||
|
||||
### `users` - 사용자 정보
|
||||
```sql
|
||||
- user_id (PK, AUTO_INCREMENT)
|
||||
- username (VARCHAR(100), UNIQUE, NOT NULL) - 사용자명
|
||||
- password (VARCHAR(255), NOT NULL) - 비밀번호 (해시)
|
||||
- role (VARCHAR(30)) - 역할
|
||||
- name (VARCHAR(50)) - 실명
|
||||
- email (VARCHAR(255), UNIQUE) - 이메일
|
||||
- worker_id (INT, FK) - 연결된 작업자 ID
|
||||
- is_active (TINYINT(1), DEFAULT 1) - 활성 상태
|
||||
- access_level (VARCHAR(30)) - 접근 레벨
|
||||
- last_login_at (DATETIME) - 마지막 로그인
|
||||
- password_changed_at (DATETIME) - 비밀번호 변경일
|
||||
- failed_login_attempts (INT, DEFAULT 0) - 로그인 실패 횟수
|
||||
- locked_until (DATETIME) - 잠금 해제 시간
|
||||
- created_at, updated_at (TIMESTAMP)
|
||||
```
|
||||
|
||||
## 📋 코드 테이블
|
||||
|
||||
### `work_types` - 작업 유형
|
||||
```sql
|
||||
- id (PK, AUTO_INCREMENT)
|
||||
- name (VARCHAR(100), NOT NULL) - 유형명
|
||||
- description (TEXT) - 설명
|
||||
- category (VARCHAR(50)) - 카테고리
|
||||
- created_at, updated_at (TIMESTAMP)
|
||||
|
||||
데이터:
|
||||
1. Base(구조물)
|
||||
2. Vessel(용기)
|
||||
3. Piping Assembly(배관)
|
||||
4. 작업대기
|
||||
```
|
||||
|
||||
### `work_status_types` - 작업 상태 유형
|
||||
```sql
|
||||
- id (PK, AUTO_INCREMENT)
|
||||
- name (VARCHAR(50), NOT NULL) - 상태명
|
||||
- description (TEXT) - 설명
|
||||
- is_error (TINYINT(1), DEFAULT 0) - 에러 여부
|
||||
- created_at (TIMESTAMP)
|
||||
|
||||
데이터:
|
||||
1. 정규 (is_error: 0)
|
||||
2. 에러 (is_error: 1)
|
||||
```
|
||||
|
||||
### `error_types` - 에러 유형
|
||||
```sql
|
||||
- id (PK, AUTO_INCREMENT)
|
||||
- name (VARCHAR(100), NOT NULL) - 에러명
|
||||
- description (TEXT) - 설명
|
||||
- severity (ENUM: low/medium/high/critical) - 심각도
|
||||
- solution_guide (TEXT) - 해결 가이드
|
||||
- created_at, updated_at (TIMESTAMP)
|
||||
|
||||
데이터:
|
||||
1. 설계미스
|
||||
2. 외주작업 불량
|
||||
3. 입고지연
|
||||
4. 작업불량
|
||||
5. 설비고장
|
||||
6. 검사불량
|
||||
```
|
||||
|
||||
### `tasks` - 작업 정보
|
||||
```sql
|
||||
- task_id (PK, AUTO_INCREMENT)
|
||||
- category (VARCHAR(255), NOT NULL) - 카테고리
|
||||
- subcategory (VARCHAR(255)) - 하위 카테고리
|
||||
- task_name (VARCHAR(255), NOT NULL) - 작업명
|
||||
- description (TEXT) - 설명
|
||||
- created_at, updated_at (TIMESTAMP)
|
||||
```
|
||||
|
||||
## 📝 기타 테이블
|
||||
|
||||
### `IssueTypes` - 이슈 유형
|
||||
### `WorkReports` - 작업 보고서 (별도)
|
||||
### `login_logs` - 로그인 로그
|
||||
### `password_change_logs` - 비밀번호 변경 로그
|
||||
### `work_report_audit_log` - 작업 보고서 감사 로그
|
||||
|
||||
## 🔗 관계 (Foreign Keys)
|
||||
|
||||
```
|
||||
daily_work_reports.worker_id → workers.worker_id
|
||||
daily_work_reports.project_id → projects.project_id
|
||||
daily_work_reports.work_type_id → work_types.id
|
||||
daily_work_reports.work_status_id → work_status_types.id
|
||||
daily_work_reports.error_type_id → error_types.id
|
||||
daily_work_reports.created_by → users.user_id
|
||||
|
||||
users.worker_id → workers.worker_id
|
||||
```
|
||||
|
||||
## 📈 인덱스
|
||||
|
||||
```sql
|
||||
daily_work_reports:
|
||||
- idx_report_date (report_date)
|
||||
- idx_worker_date (worker_id, report_date)
|
||||
- idx_project_date (project_id, report_date)
|
||||
- idx_work_type (work_type_id)
|
||||
- idx_work_status (work_status_id)
|
||||
- idx_error_type (error_type_id)
|
||||
- idx_created_by (created_by)
|
||||
```
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
1. **v1 구조 사용**: 원본 JSON 데이터와 완전 호환
|
||||
2. **테이블명**: 소문자 사용 (workers, projects, users)
|
||||
3. **작업 상태**: 1=정규, 2=에러
|
||||
4. **에러 유형**: work_status_id=2일 때만 error_type_id 사용
|
||||
83
_archive/DELETED_PAGES.md
Normal file
83
_archive/DELETED_PAGES.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 삭제된 웹 페이지 목록
|
||||
|
||||
**삭제 날짜**: 2025-11-03
|
||||
**사유**: 사용하지 않는 페이지들 정리 및 단일 대시보드 통일
|
||||
|
||||
## 삭제된 페이지들
|
||||
|
||||
### 테스트/임시 페이지
|
||||
1. `common/12.html` - 테스트 페이지
|
||||
2. `common/123.html` - 테스트 페이지
|
||||
3. `common/123456.html` - 테스트 페이지
|
||||
|
||||
### 사용하지 않는 관리 페이지
|
||||
4. `admin/factory-upload.html` - 공장 업로드
|
||||
5. `admin/manage-pipespec.html` - 파이프 사양 관리
|
||||
6. `admin/attendance-validation.html` - 출석 검증
|
||||
7. `admin/work-review.html` - 작업 검토
|
||||
|
||||
### 사용하지 않는 공통 페이지
|
||||
8. `common/factory-upload.html` - 공장 업로드
|
||||
9. `common/factory-view.html` - 공장 보기
|
||||
10. `common/attendance.html` - 출석
|
||||
|
||||
### 전체 폴더 삭제
|
||||
11. `issue-reports/` - 이슈 보고서 관련 전체 폴더
|
||||
|
||||
## 현재 사용 중인 페이지들
|
||||
|
||||
### 📊 대시보드 (통일됨)
|
||||
- `dashboard/group-leader.html` - **메인 대시보드** (모든 권한 사용) ✅
|
||||
- `dashboard/admin.html` - 관리자 대시보드 (미사용)
|
||||
- `dashboard/system.html` - 시스템 대시보드 (미사용)
|
||||
- `dashboard/user.html` - 사용자 대시보드 (미사용)
|
||||
|
||||
### 📋 관리 페이지
|
||||
- `admin/admin dashboard.html` - 관리자 대시보드
|
||||
- `admin/dashboard.html` - 대시보드
|
||||
- `admin/manage-daily-work.html` - 일일 작업 관리 ✅
|
||||
- `admin/manage-issue.html` - 이슈 관리
|
||||
- `admin/manage-project.html` - 프로젝트 관리 ✅
|
||||
- `admin/manage-task.html` - 작업 관리 ✅
|
||||
- `admin/manage-user.html` - 사용자 관리 ✅
|
||||
- `admin/manage-worker.html` - 작업자 관리 ✅
|
||||
|
||||
### 📈 분석 페이지
|
||||
- `analysis/daily_work_analysis.html` - 일일 작업 분석 ✅
|
||||
- `analysis/project-worktype-analysis.html` - 프로젝트-작업유형 분석 ✅
|
||||
- `analysis/work-report-analytics.html` - 작업보고서 분석 ✅
|
||||
|
||||
### 📄 공통 페이지
|
||||
- `common/management-dashboard.html` - 관리 대시보드 ✅
|
||||
- `common/daily-work-report-viewer.html` - 일일 작업보고서 뷰어 ✅
|
||||
- `common/daily-work-report.html` - 일일 작업보고서 ✅
|
||||
- `common/project-analysis.html` - 프로젝트 분석 ✅
|
||||
- `common/work-report-review.html` - 작업보고서 검토 ✅
|
||||
- `common/work-report-validation.html` - 작업보고서 검증 ✅
|
||||
|
||||
### 👤 프로필 페이지
|
||||
- `profile/change-password.html` - 비밀번호 변경 ✅
|
||||
- `profile/my-profile.html` - 내 프로필 ✅
|
||||
|
||||
### 📝 작업보고서 페이지
|
||||
- `work-reports/work-report-create.html` - 작업보고서 생성 ✅
|
||||
- `work-reports/work-report-manage.html` - 작업보고서 관리 ✅
|
||||
|
||||
## 변경사항
|
||||
|
||||
### 로그인 리다이렉트 통일
|
||||
- **이전**: 권한별로 다른 대시보드 (`system.html`, `admin.html`, `user.html` 등)
|
||||
- **현재**: 모든 권한이 `group-leader.html`로 통일 ✅
|
||||
|
||||
### 권한 구조
|
||||
- `system` → `group-leader.html`
|
||||
- `admin` → `group-leader.html`
|
||||
- `leader` → `group-leader.html`
|
||||
- `support` → `group-leader.html`
|
||||
- `user` → `group-leader.html`
|
||||
|
||||
## 주의사항
|
||||
|
||||
- 삭제된 페이지들을 참조하는 링크가 있다면 수정 필요
|
||||
- 모든 사용자가 동일한 대시보드를 사용하므로 권한별 기능 제한은 백엔드에서 처리
|
||||
- 향후 개선 시 권한별 대시보드 분리 가능
|
||||
39
_archive/DELETED_TABLES.md
Normal file
39
_archive/DELETED_TABLES.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 삭제된 테이블 목록
|
||||
|
||||
**삭제 날짜**: 2025-11-03
|
||||
**사유**: 사용하지 않는 테이블들 정리
|
||||
|
||||
## 삭제된 테이블들
|
||||
|
||||
1. `activity_logs` - 활동 로그
|
||||
2. `CuttingPlan` - 커팅 계획 (대문자)
|
||||
3. `DailyIssueReports` - 일일 이슈 보고서 (대문자)
|
||||
4. `daily_worker_summary` - 일일 작업자 요약
|
||||
5. `EquipmentList` - 장비 목록 (대문자)
|
||||
6. `FactoryInfo` - 공장 정보 (대문자)
|
||||
7. `PipeSpecs` - 파이프 사양 (대문자)
|
||||
8. `Processes` - 프로세스 (대문자)
|
||||
9. `uploaded_documents` - 업로드된 문서
|
||||
10. `worker_groups` - 작업자 그룹
|
||||
11. `WorkReports` - 작업 보고서 (별도, 새로 만들 예정)
|
||||
12. `work_report_audit_log` - 작업 보고서 감사 로그
|
||||
|
||||
## 현재 사용 중인 테이블들
|
||||
|
||||
- `daily_work_reports` - 일일 작업 보고서 ✅
|
||||
- `workers` - 작업자 정보 ✅
|
||||
- `projects` - 프로젝트 정보 ✅
|
||||
- `tasks` - 작업 정보 ✅
|
||||
- `users` - 사용자 정보 ✅
|
||||
- `work_types` - 작업 유형 ✅
|
||||
- `work_status_types` - 작업 상태 유형 ✅
|
||||
- `error_types` - 에러 유형 ✅
|
||||
- `IssueTypes` - 이슈 유형
|
||||
- `login_logs` - 로그인 로그
|
||||
- `password_change_logs` - 비밀번호 변경 로그
|
||||
|
||||
## 주의사항
|
||||
|
||||
- 삭제된 테이블들을 참조하는 코드가 있다면 제거해야 함
|
||||
- 향후 필요시 백업에서 복구 가능
|
||||
- 데이터베이스 구조가 단순해져서 성능 향상 기대
|
||||
81
_archive/MYSQL_COMPATIBILITY_NOTES.md
Normal file
81
_archive/MYSQL_COMPATIBILITY_NOTES.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# MySQL 8.0 호환성 문제 해결 가이드
|
||||
|
||||
## 🚨 문제 상황
|
||||
- **환경**: 시놀로지 NAS Docker 환경 (MySQL 8.0.44)
|
||||
- **오류**: `Incorrect arguments to mysqld_stmt_execute`
|
||||
- **증상**: 개발 환경(맥미니)에서는 정상 작동하지만 프로덕션 환경에서만 실패
|
||||
|
||||
## 🔍 원인 분석
|
||||
|
||||
### MySQL 설정 차이
|
||||
```sql
|
||||
-- 시놀로지 MySQL 8.0 설정
|
||||
@@sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
|
||||
@@version: 8.0.44
|
||||
character_set_database: utf8mb4
|
||||
collation_database: utf8mb4_unicode_ci
|
||||
```
|
||||
|
||||
### Node.js MySQL 드라이버 호환성 문제
|
||||
- **문제**: `db.execute()` 메서드의 파라미터 바인딩이 MySQL 8.0의 엄격한 모드에서 호환성 문제 발생
|
||||
- **해결**: `db.query()` 메서드 사용으로 변경
|
||||
|
||||
## 🛠️ 해결 방법
|
||||
|
||||
### 변경 전 (문제 있음)
|
||||
```javascript
|
||||
const [results] = await this.db.execute(query, [startDate, endDate, parseInt(limit)]);
|
||||
```
|
||||
|
||||
### 변경 후 (해결됨)
|
||||
```javascript
|
||||
const [results] = await this.db.query(query, [startDate, endDate, parseInt(limit)]);
|
||||
```
|
||||
|
||||
## 📊 db.execute vs db.query 차이점
|
||||
|
||||
| 구분 | db.execute | db.query |
|
||||
|------|------------|----------|
|
||||
| **파라미터 바인딩** | Prepared Statement 방식 | 전통적인 쿼리 방식 |
|
||||
| **성능** | 반복 실행 시 더 빠름 | 단발성 실행에 적합 |
|
||||
| **보안** | SQL Injection 방지 강화 | 기본적인 방지 |
|
||||
| **MySQL 8.0 호환성** | 엄격한 모드에서 문제 발생 가능 | 안정적 |
|
||||
| **메모리 사용** | 더 효율적 | 상대적으로 많음 |
|
||||
|
||||
## 🎯 권장사항
|
||||
|
||||
### 1. 환경별 대응
|
||||
- **개발 환경**: `db.execute` 사용 가능
|
||||
- **프로덕션 환경 (MySQL 8.0)**: `db.query` 사용 권장
|
||||
|
||||
### 2. 코드 작성 가이드
|
||||
```javascript
|
||||
// ✅ 권장: 환경에 따른 분기 처리
|
||||
const executeQuery = async (db, query, params) => {
|
||||
try {
|
||||
// MySQL 8.0 엄격 모드 대응
|
||||
return await db.query(query, params);
|
||||
} catch (error) {
|
||||
console.error('Query execution failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 테스트 전략
|
||||
- 개발 환경과 프로덕션 환경에서 모두 테스트
|
||||
- MySQL 버전별 호환성 확인
|
||||
- 복잡한 JOIN 쿼리는 특히 주의
|
||||
|
||||
## 🔧 적용된 파일
|
||||
- `synology_deployment/api/models/WorkAnalysis.js` - `getRecentWork()` 함수
|
||||
|
||||
## 📝 참고사항
|
||||
- 이 문제는 MySQL 8.0의 `ONLY_FULL_GROUP_BY` 모드와 Node.js MySQL2 드라이버의 호환성 문제로 추정
|
||||
- 향후 유사한 문제 발생 시 이 가이드 참조
|
||||
- 다른 복잡한 쿼리에서도 동일한 문제가 발생할 수 있음
|
||||
|
||||
---
|
||||
**작성일**: 2025-11-05
|
||||
**해결 완료**: ✅
|
||||
**테스트 완료**: ✅
|
||||
85
_archive/README.md
Normal file
85
_archive/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# TK-FB-Project - 통합 실행 가이드
|
||||
|
||||
## ⚙️ 사전 준비
|
||||
|
||||
### 환경 변수 설정 (필수)
|
||||
처음 실행하기 전에 환경 변수 파일을 생성해야 합니다:
|
||||
|
||||
```bash
|
||||
# 1. .env.example을 복사하여 .env 파일 생성
|
||||
cp .env.example .env
|
||||
|
||||
# 2. .env 파일을 편집하여 실제 비밀번호로 변경
|
||||
nano .env # 또는 vi, code 등 사용
|
||||
|
||||
# 3. 강력한 비밀번호로 변경 (예시)
|
||||
# MYSQL_ROOT_PASSWORD=your_secure_password_here
|
||||
# MYSQL_PASSWORD=your_secure_password_here
|
||||
# JWT_SECRET=your_random_jwt_secret_min_32_chars
|
||||
```
|
||||
|
||||
**중요**: `.env` 파일은 절대 Git에 커밋하지 마세요!
|
||||
|
||||
## 🚀 한 번에 모든 서비스 실행
|
||||
|
||||
### 🎯 간편 실행 (권장)
|
||||
```bash
|
||||
cd /Users/hyungi/docker/TK-FB-Project
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### 🛑 간편 중지
|
||||
```bash
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
### 📋 직접 실행
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## 📊 서비스 목록
|
||||
|
||||
| 서비스 | 포트 | 접속 URL | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| **웹 UI** | 20000 | http://localhost:20000 | 메인 웹 인터페이스 |
|
||||
| **API 서버** | 20005 | http://localhost:20005 | Node.js API 서버 ✅ |
|
||||
| **FastAPI 브릿지** | 20010 | http://localhost:20010 | Python FastAPI 서비스 |
|
||||
| **phpMyAdmin** | 20080 | http://localhost:20080 | DB 관리도구 |
|
||||
| **MariaDB** | 20306 | - | 데이터베이스 서버 |
|
||||
|
||||
## 🛠️ 관리 명령어
|
||||
|
||||
### 모든 서비스 중지
|
||||
```bash
|
||||
cd /Users/hyungi/docker/TK-FB-Project
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 서비스 상태 확인
|
||||
```bash
|
||||
docker ps | grep fb_
|
||||
```
|
||||
|
||||
### 로그 확인
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
## 💾 데이터베이스 정보
|
||||
|
||||
- **호스트**: localhost:20306
|
||||
- **데이터베이스**: hyungi (`.env` 파일의 `MYSQL_DATABASE`)
|
||||
- **사용자**: hyungi_user (`.env` 파일의 `MYSQL_USER`)
|
||||
- **비밀번호**: `.env` 파일에서 설정한 `MYSQL_PASSWORD`
|
||||
- **Root 비밀번호**: `.env` 파일에서 설정한 `MYSQL_ROOT_PASSWORD`
|
||||
|
||||
**참고**: 실제 비밀번호는 `.env` 파일을 확인하세요.
|
||||
|
||||
## ✨ 주요 개선사항
|
||||
|
||||
1. **통합 실행**: 한 번의 명령으로 모든 서비스 실행
|
||||
2. **깔끔한 DB 초기화**: 마이그레이션 오류 해결
|
||||
3. **일관된 네이밍**: fb_ 접두사로 컨테이너 구분
|
||||
4. **안정적인 포트**: 20000번대 포트 사용
|
||||
917
_archive/TESTING_GUIDE.md
Normal file
917
_archive/TESTING_GUIDE.md
Normal file
@@ -0,0 +1,917 @@
|
||||
# TK-FB-Project 테스트 가이드
|
||||
|
||||
## 목차
|
||||
1. [개요](#개요)
|
||||
2. [테스트 환경 설정](#테스트-환경-설정)
|
||||
3. [테스트 작성 가이드](#테스트-작성-가이드)
|
||||
4. [실전 예제](#실전-예제)
|
||||
5. [테스트 실행](#테스트-실행)
|
||||
6. [모범 사례](#모범-사례)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
이 프로젝트는 **Jest**를 사용하여 테스트를 작성합니다.
|
||||
|
||||
### 테스트 계층
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ API 통합 테스트 (E2E) │ ← supertest로 실제 HTTP 요청
|
||||
├─────────────────────────────────┤
|
||||
│ 컨트롤러 테스트 │ ← req/res 모킹
|
||||
├─────────────────────────────────┤
|
||||
│ 서비스 레이어 테스트 │ ← 비즈니스 로직 (DB 모킹)
|
||||
├─────────────────────────────────┤
|
||||
│ 모델 테스트 │ ← DB 쿼리 로직
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 우선순위
|
||||
1. **서비스 레이어 테스트** ⭐ (비즈니스 로직, 가장 중요)
|
||||
2. **컨트롤러 테스트** (API 엔드포인트)
|
||||
3. **통합 테스트** (실제 DB 사용)
|
||||
|
||||
---
|
||||
|
||||
## 테스트 환경 설정
|
||||
|
||||
### 1단계: 필요한 패키지 설치
|
||||
|
||||
```bash
|
||||
cd /Users/hyungiahn/Documents/code/TK-FB-Project/api.hyungi.net
|
||||
|
||||
npm install --save-dev jest supertest @types/jest
|
||||
npm install --save-dev jest-mock-extended
|
||||
```
|
||||
|
||||
### 2단계: Jest 설정 파일 생성
|
||||
|
||||
**`jest.config.js`** 파일을 프로젝트 루트에 생성:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
coverageDirectory: 'coverage',
|
||||
collectCoverageFrom: [
|
||||
'controllers/**/*.js',
|
||||
'services/**/*.js',
|
||||
'models/**/*.js',
|
||||
'!**/node_modules/**',
|
||||
'!**/coverage/**',
|
||||
'!**/tests/**'
|
||||
],
|
||||
testMatch: [
|
||||
'**/tests/**/*.test.js',
|
||||
'**/tests/**/*.spec.js'
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
|
||||
verbose: true,
|
||||
collectCoverage: true,
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
testTimeout: 10000
|
||||
};
|
||||
```
|
||||
|
||||
### 3단계: 테스트 디렉토리 구조
|
||||
|
||||
```
|
||||
api.hyungi.net/
|
||||
├── tests/
|
||||
│ ├── setup.js # 전역 테스트 설정
|
||||
│ ├── helpers/
|
||||
│ │ ├── dbHelper.js # DB 모킹 헬퍼
|
||||
│ │ └── mockData.js # 테스트용 더미 데이터
|
||||
│ ├── unit/
|
||||
│ │ ├── services/ # 서비스 단위 테스트
|
||||
│ │ │ ├── workReportService.test.js
|
||||
│ │ │ ├── attendanceService.test.js
|
||||
│ │ │ └── ...
|
||||
│ │ ├── controllers/ # 컨트롤러 테스트
|
||||
│ │ │ ├── workReportController.test.js
|
||||
│ │ │ └── ...
|
||||
│ │ └── models/ # 모델 테스트
|
||||
│ │ └── ...
|
||||
│ └── integration/ # 통합 테스트
|
||||
│ ├── workReport.test.js
|
||||
│ └── ...
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### 4단계: package.json에 스크립트 추가
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"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를 모킹**하여 테스트합니다.
|
||||
|
||||
#### 기본 구조
|
||||
|
||||
```javascript
|
||||
const serviceName = require('../../services/serviceName');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../../utils/errors');
|
||||
|
||||
// DB 모킹
|
||||
jest.mock('../../models/modelName');
|
||||
const ModelName = require('../../models/modelName');
|
||||
|
||||
describe('ServiceName', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('functionName', () => {
|
||||
it('should ... (성공 케이스)', async () => {
|
||||
// Arrange (준비)
|
||||
const mockData = { /* ... */ };
|
||||
ModelName.methodName.mockResolvedValue(mockData);
|
||||
|
||||
// Act (실행)
|
||||
const result = await serviceName.functionName(params);
|
||||
|
||||
// Assert (검증)
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(ModelName.methodName).toHaveBeenCalledWith(expectedParams);
|
||||
});
|
||||
|
||||
it('should throw ValidationError when ... (실패 케이스)', async () => {
|
||||
// Arrange
|
||||
const invalidParams = null;
|
||||
|
||||
// Act & Assert
|
||||
await expect(serviceName.functionName(invalidParams))
|
||||
.rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 컨트롤러 테스트 패턴
|
||||
|
||||
컨트롤러는 **서비스 레이어를 모킹**하여 테스트합니다.
|
||||
|
||||
```javascript
|
||||
const controllerName = require('../../controllers/controllerName');
|
||||
|
||||
// 서비스 모킹
|
||||
jest.mock('../../services/serviceName');
|
||||
const serviceName = require('../../services/serviceName');
|
||||
|
||||
describe('ControllerName', () => {
|
||||
let req, res;
|
||||
|
||||
beforeEach(() => {
|
||||
// req, res 모킹 객체 초기화
|
||||
req = {
|
||||
body: {},
|
||||
params: {},
|
||||
query: {},
|
||||
user: { id: 1, role: 'admin' }
|
||||
};
|
||||
res = {
|
||||
json: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis()
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /endpoint', () => {
|
||||
it('should return data successfully', async () => {
|
||||
// Arrange
|
||||
const mockData = { /* ... */ };
|
||||
serviceName.getData.mockResolvedValue(mockData);
|
||||
|
||||
// Act
|
||||
await controllerName.getData(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockData,
|
||||
message: expect.any(String)
|
||||
});
|
||||
expect(serviceName.getData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 통합 테스트 패턴 (E2E)
|
||||
|
||||
실제 HTTP 요청으로 테스트합니다.
|
||||
|
||||
```javascript
|
||||
const request = require('supertest');
|
||||
const app = require('../../index');
|
||||
const { getDb } = require('../../dbPool');
|
||||
|
||||
describe('WorkReport API Integration Tests', () => {
|
||||
let db;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await getDb();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.end();
|
||||
});
|
||||
|
||||
describe('POST /api/work-reports', () => {
|
||||
it('should create work report', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/work-reports')
|
||||
.set('Authorization', 'Bearer ' + testToken)
|
||||
.send({
|
||||
report_date: '2025-12-11',
|
||||
worker_id: 1,
|
||||
// ... 기타 필드
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('workReport_ids');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 실전 예제
|
||||
|
||||
### 예제 1: workReportService 테스트
|
||||
|
||||
**`tests/unit/services/workReportService.test.js`**
|
||||
|
||||
```javascript
|
||||
const workReportService = require('../../../services/workReportService');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../../../utils/errors');
|
||||
|
||||
// 모델 모킹
|
||||
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_hours: 8
|
||||
};
|
||||
|
||||
// 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('getWorkReportByIdService', () => {
|
||||
it('ID로 보고서를 조회해야 함', async () => {
|
||||
// Arrange
|
||||
const mockReport = {
|
||||
id: 123,
|
||||
report_date: '2025-12-11',
|
||||
worker_id: 1,
|
||||
work_hours: 8
|
||||
};
|
||||
|
||||
workReportModel.getById = jest.fn((id, callback) => {
|
||||
callback(null, mockReport);
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await workReportService.getWorkReportByIdService(123);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockReport);
|
||||
expect(workReportModel.getById).toHaveBeenCalledWith(123, 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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 예제 2: workReportController 테스트
|
||||
|
||||
**`tests/unit/controllers/workReportController.test.js`**
|
||||
|
||||
```javascript
|
||||
const workReportController = require('../../../controllers/workReportController');
|
||||
|
||||
// 서비스 모킹
|
||||
jest.mock('../../../services/workReportService');
|
||||
const workReportService = require('../../../services/workReportService');
|
||||
|
||||
describe('WorkReportController', () => {
|
||||
let req, res;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
body: {},
|
||||
params: {},
|
||||
query: {},
|
||||
user: { id: 1, username: 'test_user', role: 'admin' }
|
||||
};
|
||||
res = {
|
||||
json: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis()
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createWorkReport', () => {
|
||||
it('단일 작업 보고서를 생성해야 함', async () => {
|
||||
// Arrange
|
||||
req.body = {
|
||||
report_date: '2025-12-11',
|
||||
worker_id: 1,
|
||||
work_hours: 8
|
||||
};
|
||||
|
||||
const mockResult = { workReport_ids: [123] };
|
||||
workReportService.createWorkReportService.mockResolvedValue(mockResult);
|
||||
|
||||
// Act
|
||||
await workReportController.createWorkReport(req, res);
|
||||
|
||||
// Assert
|
||||
expect(workReportService.createWorkReportService).toHaveBeenCalledWith(req.body);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockResult,
|
||||
message: '작업 보고서가 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkReportsByDate', () => {
|
||||
it('날짜별 작업 보고서를 조회해야 함', async () => {
|
||||
// Arrange
|
||||
req.params = { date: '2025-12-11' };
|
||||
const mockReports = [
|
||||
{ id: 1, report_date: '2025-12-11', work_hours: 8 },
|
||||
{ id: 2, report_date: '2025-12-11', work_hours: 7 }
|
||||
];
|
||||
|
||||
workReportService.getWorkReportsByDateService.mockResolvedValue(mockReports);
|
||||
|
||||
// Act
|
||||
await workReportController.getWorkReportsByDate(req, res);
|
||||
|
||||
// Assert
|
||||
expect(workReportService.getWorkReportsByDateService).toHaveBeenCalledWith('2025-12-11');
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockReports,
|
||||
message: '작업 보고서 조회 성공'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkReportById', () => {
|
||||
it('ID로 작업 보고서를 조회해야 함', async () => {
|
||||
// Arrange
|
||||
req.params = { id: '123' };
|
||||
const mockReport = { id: 123, work_hours: 8 };
|
||||
|
||||
workReportService.getWorkReportByIdService.mockResolvedValue(mockReport);
|
||||
|
||||
// Act
|
||||
await workReportController.getWorkReportById(req, res);
|
||||
|
||||
// Assert
|
||||
expect(workReportService.getWorkReportByIdService).toHaveBeenCalledWith('123');
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockReport,
|
||||
message: '작업 보고서 조회 성공'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateWorkReport', () => {
|
||||
it('작업 보고서를 수정해야 함', async () => {
|
||||
// Arrange
|
||||
req.params = { id: '123' };
|
||||
req.body = { work_hours: 9 };
|
||||
const mockResult = { changes: 1 };
|
||||
|
||||
workReportService.updateWorkReportService.mockResolvedValue(mockResult);
|
||||
|
||||
// Act
|
||||
await workReportController.updateWorkReport(req, res);
|
||||
|
||||
// Assert
|
||||
expect(workReportService.updateWorkReportService).toHaveBeenCalledWith('123', req.body);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockResult,
|
||||
message: '작업 보고서가 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeWorkReport', () => {
|
||||
it('작업 보고서를 삭제해야 함', async () => {
|
||||
// Arrange
|
||||
req.params = { id: '123' };
|
||||
const mockResult = { changes: 1 };
|
||||
|
||||
workReportService.removeWorkReportService.mockResolvedValue(mockResult);
|
||||
|
||||
// Act
|
||||
await workReportController.removeWorkReport(req, res);
|
||||
|
||||
// Assert
|
||||
expect(workReportService.removeWorkReportService).toHaveBeenCalledWith('123');
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockResult,
|
||||
message: '작업 보고서가 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 예제 3: 통합 테스트 (E2E)
|
||||
|
||||
**`tests/integration/workReport.test.js`**
|
||||
|
||||
```javascript
|
||||
const request = require('supertest');
|
||||
const app = require('../../index');
|
||||
const { getDb } = require('../../dbPool');
|
||||
|
||||
describe('WorkReport API Integration Tests', () => {
|
||||
let db;
|
||||
let authToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await getDb();
|
||||
|
||||
// 테스트용 인증 토큰 생성 (실제 로그인 API 호출 또는 JWT 직접 생성)
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
username: 'test_admin',
|
||||
password: 'test_password'
|
||||
});
|
||||
|
||||
authToken = loginResponse.body.token;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// 테스트 데이터 정리
|
||||
await db.query('DELETE FROM daily_work_reports WHERE report_date = ?', ['2025-12-11']);
|
||||
await db.end();
|
||||
});
|
||||
|
||||
describe('POST /api/work-reports', () => {
|
||||
it('작업 보고서를 생성해야 함', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/work-reports')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
report_date: '2025-12-11',
|
||||
worker_id: 1,
|
||||
project_id: 1,
|
||||
work_type_id: 1,
|
||||
work_hours: 8,
|
||||
work_content: '테스트 작업'
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty('workReport_ids');
|
||||
expect(response.body.data.workReport_ids).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('인증 토큰 없이 요청하면 401을 반환해야 함', async () => {
|
||||
await request(app)
|
||||
.post('/api/work-reports')
|
||||
.send({
|
||||
report_date: '2025-12-11',
|
||||
worker_id: 1
|
||||
})
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/work-reports/:date', () => {
|
||||
it('날짜별 작업 보고서를 조회해야 함', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/work-reports/2025-12-11')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 테스트 실행
|
||||
|
||||
### 기본 실행
|
||||
|
||||
```bash
|
||||
# 모든 테스트 실행
|
||||
npm test
|
||||
|
||||
# Watch 모드 (파일 변경 시 자동 재실행)
|
||||
npm run test:watch
|
||||
|
||||
# 커버리지 리포트와 함께 실행
|
||||
npm run test:coverage
|
||||
|
||||
# 특정 파일만 테스트
|
||||
npm test -- tests/unit/services/workReportService.test.js
|
||||
|
||||
# 특정 describe 블록만 테스트
|
||||
npm test -- --testNamePattern="createWorkReportService"
|
||||
```
|
||||
|
||||
### 커버리지 확인
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
|
||||
# 커버리지 리포트 HTML 보기
|
||||
open coverage/lcov-report/index.html
|
||||
```
|
||||
|
||||
**목표 커버리지:**
|
||||
- 서비스 레이어: 80% 이상
|
||||
- 컨트롤러: 70% 이상
|
||||
- 전체: 75% 이상
|
||||
|
||||
---
|
||||
|
||||
## 모범 사례
|
||||
|
||||
### 1. 테스트 이름 규칙
|
||||
|
||||
**Good:**
|
||||
```javascript
|
||||
it('should create work report when valid data is provided', async () => {});
|
||||
it('should throw ValidationError when report_date is missing', async () => {});
|
||||
it('should return 404 when work report not found', async () => {});
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```javascript
|
||||
it('test1', async () => {});
|
||||
it('works', async () => {});
|
||||
```
|
||||
|
||||
### 2. AAA 패턴 (Arrange-Act-Assert)
|
||||
|
||||
```javascript
|
||||
it('should ...', async () => {
|
||||
// Arrange: 테스트 데이터 및 모킹 설정
|
||||
const mockData = { ... };
|
||||
service.method.mockResolvedValue(mockData);
|
||||
|
||||
// Act: 실제 테스트 대상 실행
|
||||
const result = await functionUnderTest(params);
|
||||
|
||||
// Assert: 결과 검증
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(service.method).toHaveBeenCalledWith(expectedParams);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 독립적인 테스트
|
||||
|
||||
각 테스트는 다른 테스트에 의존하지 않아야 합니다.
|
||||
|
||||
```javascript
|
||||
describe('Service', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks(); // 각 테스트 전에 모킹 초기화
|
||||
});
|
||||
|
||||
it('test 1', () => { /* ... */ });
|
||||
it('test 2', () => { /* ... */ }); // test 1의 영향을 받지 않음
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 엣지 케이스 테스트
|
||||
|
||||
```javascript
|
||||
describe('getWorkReportsByDateService', () => {
|
||||
it('should handle empty date', async () => { /* ... */ });
|
||||
it('should handle invalid date format', async () => { /* ... */ });
|
||||
it('should handle null date', async () => { /* ... */ });
|
||||
it('should handle future date', async () => { /* ... */ });
|
||||
it('should return empty array when no reports found', async () => { /* ... */ });
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 에러 케이스 테스트
|
||||
|
||||
```javascript
|
||||
it('should throw ValidationError when required field is missing', async () => {
|
||||
await expect(service.method(invalidData))
|
||||
.rejects.toThrow(ValidationError);
|
||||
|
||||
await expect(service.method(invalidData))
|
||||
.rejects.toThrow('보고서 데이터가 필요합니다');
|
||||
});
|
||||
```
|
||||
|
||||
### 6. 비동기 테스트
|
||||
|
||||
```javascript
|
||||
// Good: async/await 사용
|
||||
it('should ...', async () => {
|
||||
const result = await asyncFunction();
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
// Good: return Promise
|
||||
it('should ...', () => {
|
||||
return asyncFunction().then(result => {
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// Bad: Promise를 반환하지 않음
|
||||
it('should ...', () => {
|
||||
asyncFunction().then(result => {
|
||||
expect(result).toBe(expected); // 실행되지 않을 수 있음!
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 7. 모킹 복원
|
||||
|
||||
```javascript
|
||||
describe('Service', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks(); // 모든 모킹 복원
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 다음 단계
|
||||
|
||||
### Phase 1: 서비스 레이어 테스트 (우선)
|
||||
```bash
|
||||
tests/unit/services/
|
||||
├── workReportService.test.js ⭐ 시작하기 좋음
|
||||
├── attendanceService.test.js
|
||||
├── dailyWorkReportService.test.js
|
||||
├── workerService.test.js
|
||||
├── projectService.test.js
|
||||
├── issueTypeService.test.js
|
||||
├── toolsService.test.js
|
||||
├── dailyIssueReportService.test.js
|
||||
├── uploadService.test.js
|
||||
└── analysisService.test.js
|
||||
```
|
||||
|
||||
### Phase 2: 컨트롤러 테스트
|
||||
```bash
|
||||
tests/unit/controllers/
|
||||
├── workReportController.test.js
|
||||
├── attendanceController.test.js
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Phase 3: 통합 테스트
|
||||
```bash
|
||||
tests/integration/
|
||||
├── workReport.test.js
|
||||
├── attendance.test.js
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 유용한 Jest 명령어
|
||||
|
||||
```bash
|
||||
# 특정 파일만 watch
|
||||
npm test -- --watch tests/unit/services/workReportService.test.js
|
||||
|
||||
# 실패한 테스트만 재실행
|
||||
npm test -- --onlyFailures
|
||||
|
||||
# 병렬 실행 비활성화 (디버깅 시)
|
||||
npm test -- --runInBand
|
||||
|
||||
# 상세 출력
|
||||
npm test -- --verbose
|
||||
|
||||
# 특정 describe만 실행
|
||||
npm test -- --testNamePattern="createWorkReportService"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
### 문제 1: "Cannot find module"
|
||||
```bash
|
||||
# node_modules 재설치
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
### 문제 2: 타임아웃 에러
|
||||
```javascript
|
||||
// jest.config.js에서 타임아웃 증가
|
||||
module.exports = {
|
||||
testTimeout: 10000 // 10초
|
||||
};
|
||||
|
||||
// 또는 개별 테스트에서
|
||||
it('should ...', async () => {
|
||||
// ...
|
||||
}, 15000); // 15초
|
||||
```
|
||||
|
||||
### 문제 3: DB 연결 오류 (통합 테스트)
|
||||
```javascript
|
||||
// tests/setup.js에서 테스트 DB 설정
|
||||
process.env.DB_HOST = 'localhost';
|
||||
process.env.DB_NAME = 'test_database';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- [Jest 공식 문서](https://jestjs.io/docs/getting-started)
|
||||
- [Supertest GitHub](https://github.com/visionmedia/supertest)
|
||||
- [Testing Best Practices](https://github.com/goldbergyoni/javascript-testing-best-practices)
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-12-11
|
||||
**작성자**: TK-FB-Project Team
|
||||
**버전**: 1.0
|
||||
26
_archive/검토_리포트.md
Normal file
26
_archive/검토_리포트.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 생산팀 내부 포털 프로젝트 검토 리포트
|
||||
|
||||
## 1. 보안 취약점 분석 (2025-07-29)
|
||||
|
||||
### 1.1. 개요
|
||||
|
||||
`api.hyungi.net` 백엔드 서버의 NPM 패키지들을 대상으로 보안 취약점 분석을 수행한 결과, **총 3개의 취약점 (높음 1, 낮음 2)** 이 발견되었습니다.
|
||||
|
||||
### 1.2. 취약점 상세 내역
|
||||
|
||||
| 라이브러리 | 심각도 | 문제 내용 | 해결 방안 |
|
||||
| ----------------- | ------ | ------------------------------------------- | --------------------------------------------- |
|
||||
| `tar-fs` | **높음** | 악의적인 tar 파일 압축 해제 시 경로 조작 가능 | `npm audit fix` 명령어로 즉시 해결 가능 |
|
||||
| `brace-expansion` | 낮음 | 정규식 서비스 거부(ReDoS) 공격에 취약 | `npm audit fix` 명령어로 즉시 해결 가능 |
|
||||
| `pm2` | 낮음 | 정규식 서비스 거부(ReDoS) 공격에 취약 | 현재 해결 버전 없음. 향후 `pm2` 업데이트 필요 |
|
||||
|
||||
### 1.3. 권고 사항
|
||||
|
||||
1. **즉시 조치:** 개발 환경 또는 다음 배포 시, `api.hyungi.net` 디렉토리에서 아래 명령어를 실행하여 `tar-fs`와 `brace-expansion` 라이브러리를 안전한 버전으로 업데이트해야 합니다.
|
||||
```bash
|
||||
npm audit fix
|
||||
```
|
||||
2. **지속적인 모니터링:** `pm2` 패키지의 취약점은 현재 해결책이 없으므로, 주기적으로 새로운 버전이 릴리즈되었는지 확인하고 업데이트를 적용해야 합니다.
|
||||
|
||||
---
|
||||
*본 문서는 지속적으로 업데이트될 수 있습니다.*
|
||||
360
_archive/룰.md
Normal file
360
_archive/룰.md
Normal file
@@ -0,0 +1,360 @@
|
||||
|
||||
# 생산팀 내부 포털 개발 및 유지보수 규칙
|
||||
|
||||
## 1. 프로젝트 개요
|
||||
|
||||
본 문서는 (주)테크니컬코리아 생산팀 내부 포털의 원활한 개발 및 유지보수를 위한 규칙을 정의합니다. 모든 개발자는 본 규칙을 숙지하고 준수해야 합니다.
|
||||
|
||||
## 2. 기술 스택 및 개발 환경
|
||||
|
||||
### 2.1. 개발 환경 (Docker 기반)
|
||||
|
||||
**🐳 현재 Docker Compose로 개발 중**
|
||||
|
||||
```bash
|
||||
# 컨테이너 상태 확인
|
||||
docker ps
|
||||
|
||||
# 주요 컨테이너들:
|
||||
- api_hyungi_dev (포트: 20005) - 백엔드 API 서버
|
||||
- web_hyungi_dev (포트: 20000) - 프론트엔드 웹 서버
|
||||
- db_hyungi_dev (포트: 20306) - MariaDB 데이터베이스
|
||||
- pma_hyungi_dev (포트: 20080) - phpMyAdmin
|
||||
```
|
||||
|
||||
### 2.2. 백엔드 (api.hyungi.net)
|
||||
|
||||
- **런타임:** Node.js
|
||||
- **프레임워크:** Express.js
|
||||
- **데이터베이스:** MariaDB (MySQL)
|
||||
- **인증:** JWT (JSON Web Token)
|
||||
- **프로세스 매니저:** PM2
|
||||
- **컨테이너:** Docker
|
||||
- **포트:** 20005 (API 서버)
|
||||
- **API Base URL:** `http://localhost:20005`
|
||||
|
||||
### 2.3. 프론트엔드 (web-ui)
|
||||
|
||||
- **언어:** HTML, CSS, JavaScript (ES6+ Modules)
|
||||
- **라이브러리:** 별도 프레임워크 없이 순수 JS/HTML/CSS 사용
|
||||
- **웹서버:** Nginx
|
||||
- **포트:** 20000 (웹 서버)
|
||||
- **Web UI URL:** `http://localhost:20000`
|
||||
|
||||
### 2.4. 데이터베이스 관리
|
||||
|
||||
- **MariaDB:** `localhost:20306`
|
||||
- **사용자명:** `hyungi`
|
||||
- **비밀번호:** `tycdoq-Kawcug-8wesfa`
|
||||
- **데이터베이스:** `hyungi_dev`
|
||||
- **phpMyAdmin:** `http://localhost:20080`
|
||||
- **시스템 계정:** 동일한 계정 정보 사용
|
||||
|
||||
## 3. 코딩 컨벤션
|
||||
|
||||
### 3.1. 네이밍 컨벤션
|
||||
|
||||
- **JavaScript (백엔드/프론트엔드):**
|
||||
- 변수/함수: `camelCase` (예: `calculateTotal`)
|
||||
- 클래스: `PascalCase` (예: `UserReport`)
|
||||
- 상수: `UPPER_SNAKE_CASE` (예: `MAX_RETRIES`)
|
||||
- **파일:**
|
||||
- `kebab-case` (예: `daily-work-report.js`)
|
||||
- **CSS:**
|
||||
- `kebab-case` (예: `.main-container`)
|
||||
- **데이터베이스:**
|
||||
- 테이블/컬럼: `snake_case` (예: `user_accounts`, `created_at`)
|
||||
|
||||
### 3.2. 코드 포맷팅
|
||||
|
||||
- **들여쓰기:** 스페이스 2칸
|
||||
- **세미콜론:** 문장 끝에 항상 사용
|
||||
- ** Prettier 와 같은 코드 포맷터를 사용하여 일관성을 유지하는 것을 강력히 권장합니다.
|
||||
|
||||
### 3.3. 주석
|
||||
|
||||
- 복잡한 로직이나 설명이 필요한 부분에 간결하고 명확한 주석을 작성합니다.
|
||||
- JSDoc 형식을 사용하여 함수/메서드의 목적, 파라미터, 반환 값을 설명하는 것을 권장합니다.
|
||||
|
||||
### 3.4. 파일 길이 가이드라인
|
||||
|
||||
- 가독성과 유지보수성을 높이기 위해 파일이 단일 책임을 갖도록 관리하는 것을 목표로 합니다.
|
||||
- 파일의 길이가 **750줄**을 초과하기 시작하면, 해당 파일이 너무 많은 역할을 하고 있을 수 있다는 신호로 간주합니다.
|
||||
- 이 경우, 파일을 역할(라우팅, 컨트롤러, 서비스, 모델 등)에 따라 분리하는 리팩토링을 적극적으로 권장합니다.
|
||||
|
||||
### 3.5. 구조도 문서화
|
||||
|
||||
- 하나의 기능이 여러 파일(예: 컨트롤러, 서비스, 모델)로 분리되는 복잡한 구조를 가질 경우, 코드만으로 데이터 흐름이나 파일 간의 상호작용을 파악하기 어려울 수 있습니다.
|
||||
- 이 경우, 기능의 메인 폴더나 관련 문서(`README.md` 등)에 **간단한 구조도나 설명**을 추가하여 다른 개발자들이 구조를 쉽게 이해할 수 있도록 돕는 것을 권장합니다.
|
||||
- 간단한 텍스트 설명이나 `Mermaid.js`와 같은 도구를 사용한 다이어그램으로 구조를 명시합니다.
|
||||
|
||||
## 4. API 개발 가이드
|
||||
|
||||
### 4.1. 엔드포인트
|
||||
|
||||
- **RESTful API** 원칙을 따릅니다.
|
||||
- 리소스는 복수형 명사 사용 (예: `/users`, `/reports`)
|
||||
- 동사보다는 명사를 사용 (예: `POST /users` (O), `POST /createUser` (X))
|
||||
|
||||
### 4.2. 요청/응답
|
||||
|
||||
- 데이터 형식은 **JSON**을 사용합니다.
|
||||
- 성공 응답:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
- 실패 응답:
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "에러 메시지"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3. HTTP 상태 코드
|
||||
|
||||
- `200 OK`: 요청 성공
|
||||
- `201 Created`: 리소스 생성 성공
|
||||
- `400 Bad Request`: 클라이언트 요청 오류
|
||||
- `401 Unauthorized`: 인증 실패
|
||||
- `403 Forbidden`: 인가(권한) 실패
|
||||
- `404 Not Found`: 리소스 없음
|
||||
- `500 Internal Server Error`: 서버 내부 오류
|
||||
|
||||
### 4.4. 성능 및 자원 관리
|
||||
|
||||
- **최소한의 데이터 조회:** API는 반드시 필요한 데이터만 조회하고 반환해야 합니다. `SELECT *` 사용을 지양하고, 실제 클라이언트에서 사용하는 컬럼만 명시적으로 조회합니다.
|
||||
- **효율적인 쿼리 작성:** 복잡한 `JOIN`이나 비효율적인 `WHERE` 조건으로 인해 데이터베이스에 과도한 부하를 주는 쿼리가 없는지 항상 확인합니다.
|
||||
- **코드 리뷰:** 새로운 API를 개발하거나 기존 API를 수정할 때, 동료 개발자는 기능의 정확성뿐만 아니라 성능 측면(쿼리 효율, 불필요한 로직 등)도 함께 검토해야 합니다. 성능 저하가 의심되는 코드는 즉시 개선하는 것을 원칙으로 합니다.
|
||||
|
||||
## 5. 데이터베이스 관리
|
||||
|
||||
- **테이블/컬럼 네이밍:** `snake_case` 사용
|
||||
- **스키마 변경:**
|
||||
- 반드시 `migrations` 디렉토리에 마이그레이션 스크립트(`*.sql`)를 작성하여 변경 이력을 관리합니다.
|
||||
- 직접 DB에 `ALTER TABLE` 등을 실행하지 않습니다.
|
||||
|
||||
## 6. Git 관리
|
||||
|
||||
### 6.1. 커밋 메시지 규칙
|
||||
|
||||
- **형식:** `타입: 제목`
|
||||
- **타입:**
|
||||
- `feat`: 새로운 기능 추가
|
||||
- `fix`: 버그 수정
|
||||
- `docs`: 문서 수정
|
||||
- `style`: 코드 스타일 변경 (포맷팅, 세미콜론 등)
|
||||
- `refactor`: 코드 리팩토링
|
||||
- `test`: 테스트 코드 추가/수정
|
||||
- `chore`: 빌드, 패키지 매니저 등 설정 변경
|
||||
- **예시:** `feat: 일일 업무 보고서 조회 API 추가`
|
||||
|
||||
### 6.2. 브랜치 전략
|
||||
|
||||
- `main`: 최종 배포 버전 브랜치
|
||||
- `develop`: 개발 브랜치
|
||||
- `feature/기능명`: 기능 개발 브랜치 (예: `feature/login-jwt`)
|
||||
- `hotfix/이슈번호`: 긴급 버그 수정 브랜치
|
||||
|
||||
## 7. FastAPI 브릿지 아키텍처 (2025.07 도입)
|
||||
|
||||
### 7.1. 개요
|
||||
|
||||
웹서비스 확장성과 성능 향상을 위해 FastAPI 브릿지를 도입합니다. DS923+ 환경에서 최적의 성능을 발휘하도록 설계되었습니다.
|
||||
|
||||
### 7.2. 아키텍처
|
||||
|
||||
```
|
||||
브라우저 → FastAPI (포트 8000) → Express.js API (포트 3005)
|
||||
↓
|
||||
정적 파일 서빙
|
||||
API 게이트웨이
|
||||
캐싱 (Redis)
|
||||
AI/ML 처리
|
||||
```
|
||||
|
||||
### 7.3. 기술 스택 (추가)
|
||||
|
||||
#### 7.3.1. FastAPI 브릿지
|
||||
- **런타임:** Python 3.11+
|
||||
- **프레임워크:** FastAPI
|
||||
- **비동기:** asyncio, aiohttp
|
||||
- **캐싱:** Redis
|
||||
- **문서화:** Swagger/OpenAPI 자동 생성
|
||||
|
||||
### 7.4. 개발 단계
|
||||
|
||||
#### Phase 1: 기본 프록시 (2-3주)
|
||||
- FastAPI 기본 설정
|
||||
- Express.js API 프록시
|
||||
- 헬스체크 및 모니터링
|
||||
|
||||
#### Phase 2: 정적 파일 이관 (1주)
|
||||
- 웹 UI 정적 파일 서빙
|
||||
- 단일 포트 통합 (8000)
|
||||
|
||||
#### Phase 3: 캐싱 계층 (1-2주)
|
||||
- Redis 캐싱 구현
|
||||
- 성능 최적화
|
||||
|
||||
#### Phase 4: 확장 기능 (지속적)
|
||||
- 데이터 분석 API
|
||||
- AI/ML 기능
|
||||
- 외부 시스템 연동
|
||||
|
||||
### 7.5. 성능 목표 (DS923+ 기준)
|
||||
|
||||
- **동시 사용자:** 200명 이상
|
||||
- **응답 시간:** 50-150ms
|
||||
- **파일 처리:** 대용량 지원
|
||||
- **가용성:** 99.9% 이상
|
||||
|
||||
## 8. Docker 환경 구성
|
||||
|
||||
### 8.1. 컨테이너 구성
|
||||
|
||||
현재 시스템은 Docker로 완전히 컨테이너화되어 있으며, 다음과 같은 서비스들로 구성됩니다:
|
||||
|
||||
#### 8.1.1. 개발 환경 (현재 실행 중)
|
||||
|
||||
| 서비스 | 컨테이너명 | 포트 | 설명 |
|
||||
|--------|------------|------|------|
|
||||
| 웹 서버 | `web_hyungi_dev` | 20000 | Nginx 기반 프론트엔드 |
|
||||
| API 서버 | `api_hyungi_dev` | 20005 | Node.js Express.js 백엔드 |
|
||||
| 데이터베이스 | `db_hyungi_dev` | 20306 | MariaDB 10.9 |
|
||||
| DB 관리 | `pma_hyungi_dev` | 20080 | phpMyAdmin |
|
||||
|
||||
#### 8.1.2. 서비스 접속 방법
|
||||
|
||||
- **메인 웹사이트**: http://localhost:20000
|
||||
- **API 엔드포인트**: http://localhost:20005
|
||||
- **데이터베이스 관리**: http://localhost:20080 (phpMyAdmin)
|
||||
- **데이터베이스 직접 접속**: localhost:20306
|
||||
|
||||
#### 8.1.3. Docker Compose 실행
|
||||
|
||||
```bash
|
||||
# api.hyungi.net 디렉토리에서 실행
|
||||
cd api.hyungi.net
|
||||
docker-compose up -d
|
||||
|
||||
# 상태 확인
|
||||
docker ps | grep hyungi
|
||||
|
||||
# 로그 확인
|
||||
docker logs api_hyungi_dev
|
||||
docker logs web_hyungi_dev
|
||||
```
|
||||
|
||||
#### 8.1.4. 환경 변수
|
||||
|
||||
시스템 설정은 `api.hyungi.net/.env` 파일에서 관리됩니다:
|
||||
- 데이터베이스 연결 정보
|
||||
- API 포트 설정
|
||||
- JWT 시크릿 키
|
||||
- 기타 환경별 설정
|
||||
|
||||
#### 8.1.5. 기본 계정 정보
|
||||
|
||||
**웹 포털 관리자 계정:**
|
||||
- **사용자명:** `hyungi`
|
||||
- **비밀번호:** `tycdoq-Kawcug-8wesfa`
|
||||
- **권한:** `admin`
|
||||
- **용도:** 시스템 관리 및 개발/테스트
|
||||
|
||||
**주의사항:**
|
||||
- 이 계정은 개발 및 관리 목적으로만 사용
|
||||
- 운영 환경에서는 별도의 보안 계정 사용 권장
|
||||
- 비밀번호 변경 시 본 문서도 함께 업데이트
|
||||
|
||||
### 8.2. 배포
|
||||
|
||||
- **환경:** `development` (개발), `production` (운영)
|
||||
- **배포 절차:**
|
||||
1. `feature` 브랜치 -> `develop` 브랜치로 Pull Request (PR)
|
||||
2. `develop` 브랜치에서 충분한 테스트
|
||||
3. `develop` 브랜치 -> `main` 브랜치로 PR
|
||||
4. `main` 브랜치에 머지되면 Docker 이미지를 빌드하여 배포
|
||||
|
||||
## 9. API 구조 및 엔드포인트
|
||||
|
||||
### 9.1. 작업보고서 분석 API
|
||||
|
||||
#### 9.1.1. 개요
|
||||
- **목적**: 데일리 워크 레포트 데이터의 종합 분석 및 시각화
|
||||
- **권한**: Admin 등급 이상 (`admin`, `system`)
|
||||
- **베이스 URL**: `/api/daily-work-reports-analysis`
|
||||
|
||||
#### 9.1.2. 엔드포인트 목록
|
||||
|
||||
| 메서드 | 엔드포인트 | 설명 | 권한 |
|
||||
|--------|------------|------|------|
|
||||
| `GET` | `/filters` | 분석용 필터 데이터 조회 | Admin+ |
|
||||
| `GET` | `/period` | 기간별 종합 분석 | Admin+ |
|
||||
| `GET` | `/project` | 프로젝트별 상세 분석 | Admin+ |
|
||||
| `GET` | `/worker` | 작업자별 상세 분석 | Admin+ |
|
||||
|
||||
#### 9.1.3. 요청/응답 예시
|
||||
|
||||
**필터 데이터 조회:**
|
||||
```http
|
||||
GET /api/daily-work-reports-analysis/filters
|
||||
Authorization: Bearer {jwt_token}
|
||||
```
|
||||
|
||||
**응답:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"projects": [
|
||||
{"project_id": 1, "project_name": "프로젝트A"}
|
||||
],
|
||||
"workers": [
|
||||
{"worker_id": 1, "worker_name": "홍길동"}
|
||||
],
|
||||
"workTypes": [
|
||||
{"work_type_id": 1, "work_type_name": "설계"}
|
||||
],
|
||||
"dateRange": {
|
||||
"min_date": "2025-01-01",
|
||||
"max_date": "2025-12-31"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**기간별 분석:**
|
||||
```http
|
||||
GET /api/daily-work-reports-analysis/period?start_date=2025-08-01&end_date=2025-08-31&project_id=1
|
||||
Authorization: Bearer {jwt_token}
|
||||
```
|
||||
|
||||
#### 9.1.4. 보안 정책
|
||||
- **인증**: JWT 토큰 필수
|
||||
- **권한**: `admin` 또는 `system` 권한 필요
|
||||
- **접근 제어**: 프론트엔드에서 권한 체크 후 페이지 접근 허용
|
||||
- **오류 응답**: 권한 부족 시 403 Forbidden 반환
|
||||
|
||||
#### 9.1.5. 프론트엔드 연동
|
||||
- **페이지**: `/pages/analysis/work-report-analytics.html`
|
||||
- **네비게이션**: Admin 섹션에 `ADMIN` 배지와 함께 표시
|
||||
- **권한 체크**: 페이지 로드 시 localStorage의 사용자 권한 확인
|
||||
|
||||
### 9.2. API 네이밍 규칙
|
||||
|
||||
#### 9.2.1. 라우트 네이밍
|
||||
- **kebab-case** 사용 (예: `daily-work-reports-analysis`)
|
||||
- **복수형 명사** 사용 (예: `reports`, `users`)
|
||||
- **RESTful** 원칙 준수
|
||||
|
||||
#### 9.2.2. 파일 네이밍
|
||||
- **컨트롤러**: `{resource}Controller.js` (camelCase)
|
||||
- **라우트**: `{resource}Routes.js` (camelCase)
|
||||
- **모델**: `{resource}Model.js` (camelCase)
|
||||
|
||||
---
|
||||
본 규칙은 프로젝트 진행 상황에 따라 지속적으로 개선될 수 있습니다.
|
||||
Reference in New Issue
Block a user