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:
Hyungi Ahn
2025-12-19 12:42:24 +09:00
parent 8a8307edfc
commit 05843da1c4
19 changed files with 826 additions and 381 deletions

43
CHECKLIST.md Normal file
View File

@@ -0,0 +1,43 @@
# 프로젝트 체크리스트 및 컨텍스트
이 문서는 개발 시 확인해야 할 스키마 정보, 남은 작업, 삭제된 리소스 목록을 포함합니다.
---
## ✅ 현재 작업 (To-Do)
- [x] **WorkAnalysis 리팩토링** (Controller -> Service 분리)
- [ ] **테스트 코드 작성** (`tests/unit/services/workAnalysis.test.js` 추가 필요)
- [ ] **기타 컨트롤러 리팩토링** (Phase 3 진행 중)
- `monthlyStatusController.js` 확인 필요
---
## 🗄 데이터베이스 스키마 요약
*(상세 내용은 원본 스키마 파일 참조, 여기는 자주 찾는 정보 요약)*
### 주요 테이블
- `daily_work_reports`: 작업 보고서 메인 (Date, Worker, Project, WorkType, Status, Hours)
- `workers`: 작업자 마스터
- `projects`: 프로젝트 마스터
- `work_types`: 작업 유형 (1:Base, 2:Vessel, 3:Piping, 4:Wait)
- `work_status_types`: 상태 (1:정규, 2:에러)
- `error_types`: 에러 유형 (WorkStatus=2일 때 필수)
### 주의사항
- **스키마 버전**: v1 (JSON 호환)
- **Timezone**: 모든 시간은 KST 기준 처리 확인 필요
- **Soft Delete**: `deleted_at` 컬럼 사용 여부 확인
---
## 🗑 삭제/정리된 리소스 (참조용)
### 삭제된 페이지 (2025-11-03)
- **대시보드 통일**: `group-leader.html` 하나로 모든 권한 통합됨.
- **삭제 대상**:
- `dashboard/admin.html`, `dashboard/system.html` (미사용)
- `admin/factory-upload.html` 등 미사용 관리페이지
- `issue-reports/` 폴더 전체
### 삭제된 테이블 (히스토리)
*(필요 시 `DELETED_TABLES.md` 참고)*

109
CODING_GUIDE.md Normal file
View File

@@ -0,0 +1,109 @@
# TK-FB-Project 통합 개발 가이드
이 문서는 프로젝트의 실행, 규칙, 테스트, 호환성 등 모든 개발 관련 사항을 통합한 가이드입니다.
---
## 🏗 프로젝트 개요 및 아키텍처
**생산팀 내부 포털 개발 및 유지보수**
### 기술 스택
- **Backend**: Node.js, Express.js (Port: 20005)
- **Frontend**: Vanilla HTML/CSS/JS (Port: 20000)
- **Database**: MariaDB (Port: 20306), phpMyAdmin (Port: 20080)
- **Infra**: Docker Compose (Synology NAS, Mac Mini)
- **Bridge**: FastAPI (Port: 20010, Python 3.11+) - *2025.07 도입*
### 아키텍처 모식도
```
브라우저 → FastAPI (8000) → Express (3005) → MariaDB
정적 파일 서빙
```
*Note: 현재 개발 환경 포트는 위 기술 스택 섹션 참조*
---
## 🚀 실행 가이드
### 필수: 환경 변수 설정
`.env.example``.env`로 복사하고 설정하세요. **절대 커밋 금지!**
### Docker 실행
```bash
./start.sh # 간편 실행 (권장)
./stop.sh # 중지
docker-compose up -d # 수동 실행
```
---
## 📏 코딩 컨벤션
### 네이밍
| 대상 | 스타일 | 예시 |
|---|---|---|
| JS 변수/함수 | camelCase | `calculateTotal` |
| JS 클래스 | PascalCase | `UserReport` |
| 파일명 | kebab-case | `work-report.js` |
| DB 테이블/컬럼 | snake_case | `user_accounts` |
| API URL | plural, kebab | `/api/work-reports` |
### 코드 품질
- **파일 분리**: 750줄 초과 시 리팩토링 고려 (Controller/Service/Model 분리 필수).
- **Early Return**: 중첩 조건문 지양.
- **주석**: JSDoc 활용 권장.
---
## 📡 API 개발 가이드
- **RESTful**: 명사형 리소스 사용 (`POST /users` O, `/createUser` X).
- **응답 포맷**:
```json
{ "success": true, "data": {...}, "message": "..." }
```
- **계층 구조**:
- `Controller`: 요청/응답 처리, 유효성 검사.
- `Service`: 비즈니스 로직, 트랜잭션 관리.
- `Model`: DB 쿼리 실행.
### MySQL 8.0 호환성 주의사항 (중요)
Synology NAS(MySQL 8.0)의 `Strict Mode`로 인해 `db.execute()` 사용 시 `Incorrect arguments` 에러가 발생할 수 있습니다.
- **해결책**: `db.query()` 사용 권장 (특히 복잡한 JOIN/Subquery).
- **가이드**: `models/WorkAnalysis.js` 등의 `getRecentWork` 참조.
---
## 🧪 테스트 가이드 (Jest)
### 중요도
1. **Service Layer** ⭐ (최우선: 비즈니스 로직 검증)
2. Controller Layer (요청 처리 검증)
3. Integration (E2E)
### 실행
```bash
npm test # 전체 실행
npm run test:watch # 변경 감지
npm run test:coverage # 커버리지 측정
```
### 작성 패턴 (Service 예시)
```javascript
// Service는 DB Model을 Mocking하여 테스트
const service = require('../service');
jest.mock('../model');
it('should create report', async () => {
Model.create.mockResolvedValue(123); // Mock DB response
const result = await service.createReport(...);
expect(result).toEqual(...);
});
```
---
## 📝 Git 관리
- **커밋 메시지**: `type: subject` (예: `feat: 작업보고서 API 추가`)
- **Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
- **브랜치**: `main`(배포), `develop`(통합), `feature/*`(기능)

29
DEV_LOG.md Normal file
View File

@@ -0,0 +1,29 @@
# 개발 진행 로그
## 📅 Recent Updates (2025-12-19)
### WorkAnalysis 리팩토링 완료
**내용**: 복잡한 통계 로직을 포함하던 `workAnalysisController.js`를 리팩토링함.
- **[NEW] Service Layer**: `services/workAnalysisService.js` 생성. 비즈니스 로직 이관.
- **[UPDATE] Model Layer**: Raw SQL 쿼리를 Controller에서 Model(`models/WorkAnalysis.js`)로 이동. `getProjectWorkTypeRawData` 메서드 추가.
- **[CLEANUP] Controller**: `getProjectWorkTypeAnalysis` 메서드가 Service를 호출하도록 단순화.
---
## 🛡보안 및 검토 리포트 (History)
### 2025-07-29 보안 취약점 분석
`api.hyungi.net` 백엔드 서버 취약점 점검 결과.
| 라이브러리 | 심각도 | 상태 | 조치사항 |
|---|---|---|---|
| `tar-fs` | **High** | 해결가능 | `npm audit fix` 권장 (완료됨) |
| `brace-expansion` | Low | 해결가능 | `npm audit fix` 권장 (완료됨) |
| `pm2` | Low | 미해결 | 업데이트 대기 필요 |
---
## 📋 향후 계획
1. **테스트 커버리지 확보**: 리팩토링된 Service Layer에 대한 Unit Test 보강.
2. **Knex 마이그레이션**: 남은 Raw SQL(Model Layer)을 Knex 쿼리빌더로 점진적 전환.
3. **API 문서화**: Swagger/OpenAPI 도입 검토.

View File

@@ -433,6 +433,8 @@ const getDashboardData = asyncHandler(async (req, res) => {
}
});
const workAnalysisService = require('../services/workAnalysisService');
/**
* 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간)
*/
@@ -443,156 +445,19 @@ const getProjectWorkTypeAnalysis = asyncHandler(async (req, res) => {
logger.info('프로젝트별-작업별 시간 분석 요청', { start, end });
try {
const db = await getDb();
// 먼저 데이터 존재 여부 확인
const testQuery = `
SELECT
COUNT(*) as total_count,
MIN(report_date) as min_date,
MAX(report_date) as max_date,
SUM(work_hours) as total_hours
FROM daily_work_reports
WHERE report_date BETWEEN ? AND ?
`;
const testResults = await db.query(testQuery, [start, end]);
logger.debug('데이터 확인', {
start,
end,
count: testResults[0][0]?.total_count
});
// 먼저 간단한 테스트 쿼리로 데이터 확인
const simpleQuery = `
SELECT COUNT(*) as count, MIN(report_date) as min_date, MAX(report_date) as max_date
FROM daily_work_reports
WHERE report_date BETWEEN ? AND ?
`;
const simpleResult = await db.query(simpleQuery, [start, end]);
logger.debug('기간 내 데이터 확인', {
start,
end,
result: simpleResult[0][0]
});
// 프로젝트별-작업별 시간 분석 쿼리 (work_types 테이블과 조인)
const query = `
SELECT
COALESCE(p.project_id, dwr.project_id) as project_id,
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
COALESCE(p.job_no, 'N/A') as job_no,
dwr.work_type_id,
COALESCE(wt.name, CONCAT('작업유형 ', dwr.work_type_id)) as work_type_name,
-- 총 시간
SUM(dwr.work_hours) as total_hours,
-- 정규 시간 (work_status_id = 1)
SUM(CASE WHEN dwr.work_status_id = 1 THEN dwr.work_hours ELSE 0 END) as regular_hours,
-- 에러 시간 (work_status_id = 2)
SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) as error_hours,
-- 작업 건수
COUNT(*) as total_reports,
COUNT(CASE WHEN dwr.work_status_id = 1 THEN 1 END) as regular_reports,
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_reports,
-- 에러율 계산
ROUND(
(SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) /
SUM(dwr.work_hours)) * 100, 2
) as error_rate_percent
FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.project_id, p.project_name, p.job_no, dwr.work_type_id, wt.name
ORDER BY p.project_name, wt.name
`;
const results = await db.query(query, [start, end]);
logger.debug('쿼리 결과', {
start,
end,
resultCount: results[0].length
});
// 데이터를 프로젝트별로 그룹화
const groupedData = {};
results[0].forEach(row => {
const projectKey = `${row.project_id}_${row.project_name}`;
if (!groupedData[projectKey]) {
groupedData[projectKey] = {
project_id: row.project_id,
project_name: row.project_name,
job_no: row.job_no,
total_project_hours: 0,
total_regular_hours: 0,
total_error_hours: 0,
work_types: []
};
}
// 프로젝트 총계 누적
groupedData[projectKey].total_project_hours += parseFloat(row.total_hours);
groupedData[projectKey].total_regular_hours += parseFloat(row.regular_hours);
groupedData[projectKey].total_error_hours += parseFloat(row.error_hours);
// 작업 유형별 데이터 추가
groupedData[projectKey].work_types.push({
work_type_id: row.work_type_id,
work_type_name: row.work_type_name,
total_hours: parseFloat(row.total_hours),
regular_hours: parseFloat(row.regular_hours),
error_hours: parseFloat(row.error_hours),
total_reports: row.total_reports,
regular_reports: row.regular_reports,
error_reports: row.error_reports,
error_rate_percent: parseFloat(row.error_rate_percent) || 0
});
});
// 프로젝트별 에러율 계산
Object.values(groupedData).forEach(project => {
project.project_error_rate = project.total_project_hours > 0
? Math.round((project.total_error_hours / project.total_project_hours) * 100 * 100) / 100
: 0;
});
// 전체 요약 통계
const totalStats = {
total_projects: Object.keys(groupedData).length,
total_work_types: new Set(results[0].map(r => r.work_type_id)).size,
grand_total_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_project_hours, 0),
grand_regular_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_regular_hours, 0),
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)
};
totalStats.grand_error_rate = totalStats.grand_total_hours > 0
? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100
: 0;
const result = await workAnalysisService.getProjectWorkTypeAnalysis(start, end);
logger.info('프로젝트별-작업별 시간 분석 성공', {
start,
end,
projectCount: totalStats.total_projects,
workTypeCount: totalStats.total_work_types,
totalHours: totalStats.grand_total_hours
projectCount: result.summary.total_projects,
workTypeCount: result.summary.total_work_types,
totalHours: result.summary.grand_total_hours
});
res.json({
success: true,
data: {
summary: totalStats,
projects: Object.values(groupedData),
period: { start, end }
},
data: result,
message: '프로젝트별-작업별 시간 분석 완료'
});
} catch (error) {
@@ -601,6 +466,10 @@ const getProjectWorkTypeAnalysis = asyncHandler(async (req, res) => {
end,
error: error.message
});
// Service throws DatabaseError wrapper or Error
if (error.name === 'DatabaseError') {
throw error;
}
throw new DatabaseError('프로젝트별-작업별 시간 분석 중 오류가 발생했습니다');
}
});

View File

@@ -427,6 +427,51 @@ class WorkAnalysis {
throw new Error(`대시보드 데이터 조회 실패: ${error.message}`);
}
}
// 프로젝트별-작업별 시간 분석용 데이터 조회
async getProjectWorkTypeRawData(startDate, endDate) {
const query = `
SELECT
COALESCE(p.project_id, dwr.project_id) as project_id,
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
COALESCE(p.job_no, 'N/A') as job_no,
dwr.work_type_id,
COALESCE(wt.name, CONCAT('작업유형 ', dwr.work_type_id)) as work_type_name,
-- 총 시간
SUM(dwr.work_hours) as total_hours,
-- 정규 시간 (work_status_id = 1)
SUM(CASE WHEN dwr.work_status_id = 1 THEN dwr.work_hours ELSE 0 END) as regular_hours,
-- 에러 시간 (work_status_id = 2)
SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) as error_hours,
-- 작업 건수
COUNT(*) as total_reports,
COUNT(CASE WHEN dwr.work_status_id = 1 THEN 1 END) as regular_reports,
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_reports,
-- 에러율 계산
ROUND(
(SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) /
SUM(dwr.work_hours)) * 100, 2
) as error_rate_percent
FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.project_id, p.project_name, p.job_no, dwr.work_type_id, wt.name
ORDER BY p.project_name, wt.name
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
return results;
} catch (error) {
throw new Error(`프로젝트-작업유형별 원본 데이터 조회 실패: ${error.message}`);
}
}
}
module.exports = WorkAnalysis;

View File

@@ -0,0 +1,109 @@
/**
* Work Analysis Service
*
* Handles complex business logic and data aggregation for detailed work analysis reports
*
* @author TK-FB-Project
* @since 2025-12-19
*/
const { getDb } = require('../dbPool');
const WorkAnalysis = require('../models/WorkAnalysis');
const logger = require('../utils/logger');
const { DatabaseError } = require('../utils/errors');
class WorkAnalysisService {
constructor() {
this.db = null;
this.model = null;
}
async init() {
if (!this.db) {
this.db = await getDb();
this.model = new WorkAnalysis(this.db);
}
}
/**
* 프로젝트별-작업별 시간 분석 및 집계 (Service Layer logic)
*/
async getProjectWorkTypeAnalysis(startDate, endDate) {
await this.init();
try {
// 1. Get Raw Data from Model
const rawData = await this.model.getProjectWorkTypeRawData(startDate, endDate);
// 2. Process and Group Data
const groupedData = {};
rawData.forEach(row => {
const projectKey = `${row.project_id}_${row.project_name}`;
if (!groupedData[projectKey]) {
groupedData[projectKey] = {
project_id: row.project_id,
project_name: row.project_name,
job_no: row.job_no,
total_project_hours: 0,
total_regular_hours: 0,
total_error_hours: 0,
work_types: []
};
}
// Project Totals Accumulation
groupedData[projectKey].total_project_hours += parseFloat(row.total_hours);
groupedData[projectKey].total_regular_hours += parseFloat(row.regular_hours);
groupedData[projectKey].total_error_hours += parseFloat(row.error_hours);
// Add WorkType Entry
groupedData[projectKey].work_types.push({
work_type_id: row.work_type_id,
work_type_name: row.work_type_name,
total_hours: parseFloat(row.total_hours),
regular_hours: parseFloat(row.regular_hours),
error_hours: parseFloat(row.error_hours),
total_reports: row.total_reports,
regular_reports: row.regular_reports,
error_reports: row.error_reports,
error_rate_percent: parseFloat(row.error_rate_percent) || 0
});
});
// 3. Calculate Project Error Rates
Object.values(groupedData).forEach(project => {
project.project_error_rate = project.total_project_hours > 0
? Math.round((project.total_error_hours / project.total_project_hours) * 100 * 100) / 100
: 0;
});
// 4. Calculate Grand Totals
const totalStats = {
total_projects: Object.keys(groupedData).length,
total_work_types: new Set(rawData.map(r => r.work_type_id)).size,
grand_total_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_project_hours, 0),
grand_regular_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_regular_hours, 0),
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)
};
totalStats.grand_error_rate = totalStats.grand_total_hours > 0
? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100
: 0;
return {
summary: totalStats,
projects: Object.values(groupedData),
period: { start: startDate, end: endDate }
};
} catch (error) {
logger.error('Service: 프로젝트별-작업별 시간 분석 처리 실패', { error: error.message });
// Re-throw as DatabaseError to maintain consistency with controller expectation, or custom ServiceError
throw new DatabaseError(`분석 데이터 처리 중 오류 발생: ${error.message}`);
}
}
}
module.exports = new WorkAnalysisService();

View File

@@ -0,0 +1,308 @@
# Current Database Schema
> **Last Updated**: 2025-12-19
> **Source**: Synthesized from `hyungi_schema_v2.sql`, migration files, setup scripts, and model definitions.
This document represents the most up-to-date understanding of the database schema, reflecting all migrations and code-inferred structures.
---
## Main Tables
### `users`
*Source: `hyungi_schema_v2.sql`*
```sql
CREATE TABLE `users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) NOT NULL,
`password` varchar(255) NOT NULL,
`name` varchar(50) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`role` varchar(30) DEFAULT 'user' COMMENT '역할 (system, admin, leader, user)',
`access_level` varchar(30) DEFAULT NULL COMMENT '접근 레벨 (레거시 필드, role로 통합 고려)',
`worker_id` int(11) DEFAULT NULL COMMENT '연결된 작업자 ID',
`is_active` tinyint(1) DEFAULT 1 COMMENT '계정 활성화 여부',
`last_login_at` datetime DEFAULT NULL COMMENT '마지막 로그인 시간',
`password_changed_at` datetime DEFAULT NULL,
`failed_login_attempts` int(11) DEFAULT 0,
`locked_until` datetime DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`),
KEY `fk_users_worker_id` (`worker_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### `projects`
*Source: `hyungi_schema_v2.sql` + `20251219010000_add_columns_to_projects.js` migration.*
```sql
CREATE TABLE `projects` (
`project_id` int(11) NOT NULL AUTO_INCREMENT,
`job_no` varchar(50) NOT NULL,
`project_name` varchar(255) NOT NULL,
`contract_date` date DEFAULT NULL,
`due_date` date DEFAULT NULL,
`delivery_method` varchar(100) DEFAULT NULL,
`site` varchar(100) DEFAULT NULL,
`pm` varchar(100) DEFAULT NULL,
`is_active` tinyint(1) DEFAULT 1,
`project_status` varchar(255) DEFAULT 'active',
`completed_date` date DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`project_id`),
UNIQUE KEY `job_no` (`job_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### `workers`
*Source: `hyungi_schema_v2.sql`*
```sql
CREATE TABLE `workers` (
`worker_id` int(11) NOT NULL AUTO_INCREMENT,
`worker_name` varchar(100) NOT NULL,
`job_type` varchar(100) DEFAULT NULL COMMENT '직종',
`join_date` date DEFAULT NULL COMMENT '입사일',
`status` varchar(20) DEFAULT 'active' COMMENT '상태 (active, inactive)',
`created_at` timestamp NULL DEFAULT current_timestamp(),
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`worker_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### `daily_work_reports`
*Source: `hyungi.sql`*
```sql
CREATE TABLE `daily_work_reports` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`report_date` date NOT NULL COMMENT '작업 날짜',
`worker_id` int(11) NOT NULL COMMENT '작업자 ID',
`project_id` int(11) NOT NULL COMMENT '프로젝트 ID',
`work_type_id` int(11) NOT NULL COMMENT '작업 유형 ID',
`work_status_id` int(11) DEFAULT 1 COMMENT '업무 상태 ID (1:정규, 2:에러)',
`error_type_id` int(11) DEFAULT NULL COMMENT '에러 유형 ID (에러일 때만)',
`work_hours` decimal(4,2) NOT NULL COMMENT '작업 시간',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`created_by` int(11) NOT NULL DEFAULT 1 COMMENT '작성자 user_id',
`updated_by` int(11) DEFAULT NULL COMMENT '수정자 user_id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
---
## Attendance Management Tables
*Source: `create-attendance-tables.js`*
### `work_attendance_types`
```sql
CREATE TABLE IF NOT EXISTS `work_attendance_types` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`type_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드',
`type_name` VARCHAR(50) NOT NULL COMMENT '근로 유형명',
`description` TEXT COMMENT '설명',
`is_active` BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='근로 유형 관리 테이블';
```
### `vacation_types`
```sql
CREATE TABLE IF NOT EXISTS `vacation_types` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`type_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드',
`type_name` VARCHAR(50) NOT NULL COMMENT '휴가 유형명',
`hours_deduction` DECIMAL(4,2) NOT NULL COMMENT '차감 시간',
`description` TEXT COMMENT '설명',
`is_active` BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='휴가 유형 관리 테이블';
```
### `daily_attendance_records`
```sql
CREATE TABLE IF NOT EXISTS `daily_attendance_records` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`record_date` DATE NOT NULL COMMENT '기록 날짜',
`worker_id` INT NOT NULL COMMENT '작업자 ID',
`total_work_hours` DECIMAL(4,2) DEFAULT 0 COMMENT '총 작업 시간',
`attendance_type_id` INT COMMENT '근로 유형 ID',
`vacation_type_id` INT NULL COMMENT '휴가 유형 ID',
`is_vacation_processed` BOOLEAN DEFAULT FALSE COMMENT '휴가 처리 여부',
`overtime_approved` BOOLEAN DEFAULT FALSE COMMENT '초과근무 승인 여부',
`overtime_approved_by` INT NULL COMMENT '초과근무 승인자 ID',
`overtime_approved_at` TIMESTAMP NULL COMMENT '초과근무 승인 시간',
`status` ENUM('incomplete', 'partial', 'complete', 'overtime', 'vacation', 'error') DEFAULT 'incomplete' COMMENT '상태',
`notes` TEXT COMMENT '비고',
`created_by` INT NOT NULL DEFAULT 1 COMMENT '생성자 ID',
`updated_by` INT NULL COMMENT '수정자 ID',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `unique_worker_date` (`worker_id`, `record_date`)
) COMMENT='일일 근태 기록 테이블';
```
### `worker_vacation_balance`
```sql
CREATE TABLE IF NOT EXISTS `worker_vacation_balance` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`worker_id` INT NOT NULL COMMENT '작업자 ID',
`year` YEAR NOT NULL COMMENT '연도',
`total_annual_leave` DECIMAL(4,2) DEFAULT 15.0 COMMENT '연간 총 연차 (일)',
`used_annual_leave` DECIMAL(4,2) DEFAULT 0 COMMENT '사용한 연차 (일)',
`remaining_annual_leave` DECIMAL(4,2) GENERATED ALWAYS AS (total_annual_leave - used_annual_leave) STORED COMMENT '잔여 연차 (일)',
`notes` TEXT COMMENT '비고',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `unique_worker_year` (`worker_id`, `year`)
) COMMENT='작업자별 휴가 잔여 관리 테이블';
```
---
## Legacy & Misc Tables
### `Tools`
*Source: Inferred from `models/toolsModel.js`*
```sql
CREATE TABLE `Tools` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
`stock` int(11) DEFAULT NULL,
`status` varchar(255) DEFAULT NULL,
`factory_id` int(11) DEFAULT NULL,
`map_x` decimal(10, 6) DEFAULT NULL,
`map_y` decimal(10, 6) DEFAULT NULL,
`map_zone` varchar(255) DEFAULT NULL,
`map_note` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### `DailyIssueReports`
*Source: `hyungi.sql`*
```sql
CREATE TABLE `DailyIssueReports` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`date` date NOT NULL,
`worker_id` int(11) NOT NULL,
`project_id` int(11) NOT NULL,
`issue_type_id` int(11) DEFAULT NULL,
`description` text DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`start_time` time NOT NULL,
`end_time` time NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
```
### `IssueTypes`
*Source: `hyungi.sql`*
```sql
CREATE TABLE `IssueTypes` (
`issue_type_id` int(11) NOT NULL AUTO_INCREMENT,
`category` varchar(100) NOT NULL,
`subcategory` varchar(100) NOT NULL,
PRIMARY KEY (`issue_type_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
```
### `work_types`
*Source: `hyungi.sql`*
```sql
CREATE TABLE `work_types` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '작업 유형명',
`description` text DEFAULT NULL COMMENT '작업 유형 설명',
`category` varchar(50) DEFAULT NULL COMMENT '작업 카테고리',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### `work_status_types`
*Source: `hyungi.sql`*
```sql
CREATE TABLE `work_status_types` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '상태명',
`description` text DEFAULT NULL COMMENT '상태 설명',
`is_error` tinyint(1) DEFAULT 0 COMMENT '에러 상태 여부',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### `error_types`
*Source: `hyungi.sql`*
```sql
CREATE TABLE `error_types` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '에러 유형명',
`description` text DEFAULT NULL COMMENT '에러 설명',
`severity` enum('low','medium','high','critical') DEFAULT 'medium' COMMENT '심각도',
`solution_guide` text DEFAULT NULL COMMENT '해결 가이드',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### `WorkReports` (Legacy)
*Source: `hyungi.sql`*
*Note: This appears to be a legacy version of `daily_work_reports`.*
```sql
CREATE TABLE `WorkReports` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`date` date NOT NULL,
`worker_id` int(11) NOT NULL,
`project_id` int(11) NOT NULL,
`morning_task_id` int(11) DEFAULT NULL,
`afternoon_task_id` int(11) DEFAULT NULL,
`overtime_hours` decimal(4,1) DEFAULT 0.0,
`overtime_task_id` int(11) DEFAULT NULL,
`work_details` text DEFAULT NULL,
`note` text DEFAULT NULL,
`memo` text DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`updated_at` timestamp NULL DEFAULT current_timestamp(),
`morning_project_id` int(11) DEFAULT NULL,
`afternoon_project_id` int(11) DEFAULT NULL,
`overtime_project_id` int(11) DEFAULT NULL,
`task_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

View File

@@ -31,33 +31,51 @@
---
## 2025-12-11: 문서 구조 초기 생성
## 2025-12-19: Phase 2 진행 - DB 쿼리 개선 및 프론트엔드 모듈화 시작
### 변경 사항
- docs/ 디렉토리 생성
- 리팩토링 관련 문서 작성
- README.md: 문서 인덱스
- refactoring/ANALYSIS.md: 코드베이스 분석 리포트
- refactoring/PLAN.md: 리팩토링 실행 계획
- refactoring/LOG.md: 이 파일
#### 1. 백엔드 `SELECT *` 쿼리 개선
- **목표**: `SELECT *` 구문을 명시적 컬럼으로 대체하여 성능 및 명확성 향상.
- **작업 내용**: 8개의 모델 파일에서 `SELECT *`를 사용하는 모든 활성 쿼리를 리팩토링했습니다.
- `projectModel.js`
- `toolsModel.js`
- `attendanceModel.js`
- `dailyIssueReportModel.js`
- `issueTypeModel.js`
- `workReportModel.js`
- `userModel.js`
- `dailyWorkReportModel.js`
- **비고**: 사용되지 않는 레거시 함수는 사용 현황 분석 후 수정에서 제외하여 안정성을 확보했습니다.
#### 2. 프론트엔드 모듈화 시작
- **목표**: 거대 파일 `work-report-calendar.js` 분리 및 API 호출 로직 공통화.
- **`manage-project.js` 리팩토링**: `fetch` 직접 사용 대신, 기존 `api-helper.js`의 전역 함수 (`apiGet`, `apiPost` 등)를 사용하도록 통일하여 일관성을 확보했습니다.
- **`work-report-calendar.js` 모듈화**:
- `CalendarAPI.js`: API 호출 로직을 분리 및 캡슐화.
- `CalendarState.js`: 전역으로 흩어져 있던 상태 변수를 중앙에서 관리.
- `CalendarView.js`: DOM 요소 및 렌더링 관련 함수 일부를 분리.
- **비고**: `work-report-calendar.js` 파일의 크기와 복잡도로 인해 도구의 안정적인 수정이 어려워, 분리된 모듈을 생성하고 HTML에 연결하는 작업을 수행했습니다. 나머지 변환 작업은 수동으로 진행해야 합니다.
#### 3. 개발 환경 안정화 및 문서화
- **`docker-compose.yml` 수정**: `api` 컨테이너의 `MODULE_NOT_FOUND` 오류를 해결하기 위해 누락된 소스 코드 디렉토리(`config`, `middlewares` 등)를 볼륨 마운트에 추가했습니다.
- **데이터베이스 마이그레이션 추가**: `projects` 테이블에 누락된 컬럼 (`is_active` 등)을 추가하는 마이그레이션 파일을 생성하여 코드와 스키마의 불일치를 해결했습니다.
- **문서화**:
- `docs/deployment_notes.md`: 사용자 피드백을 바탕으로 테스트 및 프로덕션 배포 환경 정보를 기록했습니다.
- `docs/database/CURRENT_SCHEMA.md`: 여러 소스에 흩어져 있던 스키마 정보를 종합하여 현재 기준의 최종 스키마 정의서를 작성했습니다.
### 영향 범위
**새로 생성된 파일**:
- docs/README.md
- docs/refactoring/ANALYSIS.md
- docs/refactoring/PLAN.md
- docs/refactoring/LOG.md
- **수정된 파일**: `docker-compose.yml`, 8개 모델 파일, 3개 프론트엔드 JS 파일, 1개 HTML 파일
- **추가된 파일**: `docs/deployment_notes.md`, `docs/database/CURRENT_SCHEMA.md`, 1개 마이그레이션 파일, 3개 JS 모듈 파일
- **영향받는 기능**: 데이터베이스 조회 성능, API 서버 안정성, 프론트엔드 코드 구조, 프로젝트 문서
### 테스트
- [x] 문서 구조 검토
- [x] 마크다운 형식 확인
- [x] Docker 컨테이너 재빌드 및 정상 실행 확인.
- [ ] 리팩토링된 API 및 프론트엔드 기능의 전체적인 수동 테스트 필요.
- [ ] 데이터베이스 마이그레이션은 DB 연결 문제로 실행하지 못했으나, 코드와 스키마 불일치를 해결하는 방향으로 작성됨.
### 관련 이슈
- 리팩토링 프로젝트 시작
### 비고
- 이후 모든 리팩토링 작업은 이 로그에 기록할 것
- 코드 변경 전후를 명확히 문서화
- Phase 2 리팩토링 계획 ([docs/refactoring/PLAN.md](PLAN.md))
---
@@ -198,27 +216,27 @@ git commit -m "refactor: Phase 1 - 긴급 보안 및 중복 제거"
## YYYY-MM-DD: [작업 제목]
### 변경 사항
-
-
### 영향 범위
**수정된 파일**:
-
-
-
-
**영향받는 기능**:
-
-
### 코드 변경
#### Before
\`\`\`javascript
````javascript
// 변경 전 코드
\`\`\`
````
#### After
\`\`\`javascript
````javascript
// 변경 후 코드
\`\`\`
````
### 테스트
- [ ] 단위 테스트 통과
@@ -232,15 +250,15 @@ git commit -m "refactor: Phase 1 - 긴급 보안 및 중복 제거"
- 개선율:
### 관련 이슈
-
-
### 비고
-
-
### Git Commit
\`\`\`bash
````bash
git commit -m "refactor: [커밋 메시지]"
\`\`\`
````
---
```
@@ -303,4 +321,4 @@ git commit -m "refactor: [커밋 메시지]"
---
*마지막 업데이트: 2025-12-11*
*마지막 업데이트: 2025-12-11*

View File

@@ -0,0 +1,121 @@
// web-ui/js/modules/calendar/CalendarView.js
/**
* 캘린더 UI 렌더링 및 DOM 조작을 담당하는 전역 객체입니다.
*/
(function(window) {
'use strict';
const CalendarView = {
elements: {},
initializeElements: function() {
this.elements.monthYearTitle = document.getElementById('monthYearTitle');
this.elements.calendarDays = document.getElementById('calendarDays');
this.elements.prevMonthBtn = document.getElementById('prevMonthBtn');
this.elements.nextMonthBtn = document.getElementById('nextMonthBtn');
this.elements.todayBtn = document.getElementById('todayBtn');
this.elements.dailyWorkModal = document.getElementById('dailyWorkModal');
this.elements.modalTitle = document.getElementById('modalTitle');
this.elements.modalSummary = document.querySelector('.daily-summary');
this.elements.modalTotalWorkers = document.getElementById('modalTotalWorkers');
this.elements.modalTotalHours = document.getElementById('modalTotalHours');
this.elements.modalTotalTasks = document.getElementById('modalTotalTasks');
this.elements.modalErrorCount = document.getElementById('modalErrorCount');
this.elements.modalWorkersList = document.getElementById('modalWorkersList');
this.elements.modalNoData = document.getElementById('modalNoData');
this.elements.statusFilter = document.getElementById('statusFilter');
this.elements.loadingSpinner = document.getElementById('loadingSpinner');
},
showLoading: function(show) {
if (this.elements.loadingSpinner) {
this.elements.loadingSpinner.style.display = show ? 'flex' : 'none';
}
},
showToast: function(message, type = 'info') {
const existingToast = document.querySelector('.toast-message');
if (existingToast) existingToast.remove();
const toast = document.createElement('div');
toast.className = `toast-message toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
},
renderCalendar: async function() {
const year = CalendarState.currentDate.getFullYear();
const month = CalendarState.currentDate.getMonth();
const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
this.elements.monthYearTitle.textContent = `${year}${monthNames[month]}`;
this.showLoading(true);
try {
const monthData = await CalendarAPI.getMonthlyCalendarData(year, month);
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay());
const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
let calendarHTML = '';
let currentDay = new Date(startDate);
for (let i = 0; i < 42; i++) {
const dateStr = `${currentDay.getFullYear()}-${String(currentDay.getMonth() + 1).padStart(2, '0')}-${String(currentDay.getDate()).padStart(2, '0')}`;
const dayWorkData = monthData[dateStr] || { hasData: false, hasIssues: false, hasErrors: false, workerCount: 0 };
const dayStatus = this.analyzeDayStatus(dayWorkData);
let dayClasses = ['calendar-day'];
if (currentDay.getMonth() !== month) dayClasses.push('other-month');
if (dateStr === todayStr) dayClasses.push('today');
if (currentDay.getDay() === 0) dayClasses.push('sunday');
if (currentDay.getDay() === 6) dayClasses.push('saturday');
const hasAnyProblem = dayStatus.hasOvertimeWarning || dayStatus.hasIncomplete || dayStatus.hasIssues;
if (dayStatus.hasData && !hasAnyProblem) dayClasses.push('has-normal');
let statusIcons = '';
if (hasAnyProblem) {
if (dayStatus.hasOvertimeWarning) statusIcons += '<div class="legend-icon purple">●</div>';
if (dayStatus.hasIncomplete) statusIcons += '<div class="legend-icon red">●</div>';
if (dayStatus.hasIssues) statusIcons += '<div class="legend-icon orange">●</div>';
}
calendarHTML += `<div class="${dayClasses.join(' ')}" onclick="openDailyWorkModal('${dateStr}')"><div class="day-number">${currentDay.getDate()}</div>${statusIcons}</div>`;
currentDay.setDate(currentDay.getDate() + 1);
}
this.elements.calendarDays.innerHTML = calendarHTML;
} catch (error) {
console.error('캘린더 렌더링 오류:', error);
this.showToast('캘린더를 불러오는데 실패했습니다.', 'error');
} finally {
this.showLoading(false);
}
},
analyzeDayStatus: function(dayData) {
if (dayData && typeof dayData === 'object' && 'totalWorkers' in dayData) {
const totalRegisteredWorkers = CalendarState.allWorkers ? CalendarState.allWorkers.length : 0;
const actualIncompleteWorkers = Math.max(0, totalRegisteredWorkers - dayData.workingWorkers);
return {
hasData: dayData.totalWorkers > 0,
hasIssues: dayData.partialWorkers > 0,
hasIncomplete: actualIncompleteWorkers > 0 || dayData.incompleteWorkers > 0,
hasOvertimeWarning: dayData.hasOvertimeWarning || dayData.overtimeWarningWorkers > 0,
workerCount: dayData.totalWorkers || 0
};
}
return { hasData: false, hasIssues: false, hasErrors: false, workerCount: 0 };
}
};
window.CalendarView = CalendarView;
})(window);

View File

@@ -138,213 +138,6 @@ async function loadMonthlyWorkData(year, month) {
}
}
// 캘린더 렌더링
async function renderCalendar() {
const year = CalendarState.currentDate.getFullYear();
const month = CalendarState.currentDate.getMonth();
// 헤더 업데이트
const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'];
const monthText = `${year}${monthNames[month]}`;
elements.monthYearTitle.textContent = monthText;
// 로딩 표시
showLoading(true);
try {
// 월별 데이터 로드
const monthData = await loadMonthlyWorkData(year, month);
// 캘린더 날짜 생성
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay()); // 주의 시작일 (일요일)
const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
let calendarHTML = '';
const currentDay = new Date(startDate);
// 6주 * 7일 = 42일 렌더링
for (let i = 0; i < 42; i++) {
// 로컬 시간대로 날짜 문자열 생성 (UTC 변환 문제 방지)
const year = currentDay.getFullYear();
const month_num = String(currentDay.getMonth() + 1).padStart(2, '0');
const day_num = String(currentDay.getDate()).padStart(2, '0');
const dateStr = `${year}-${month_num}-${day_num}`;
const dayNumber = currentDay.getDate();
const isCurrentMonth = currentDay.getMonth() === month;
const isToday = dateStr === todayStr;
const isSunday = currentDay.getDay() === 0;
const isSaturday = currentDay.getDay() === 6;
// 해당 날짜의 작업 데이터 (집계 데이터 구조)
let dayWorkData = monthData[dateStr] || {
hasData: false,
hasIssues: false,
hasErrors: false,
workerCount: 0
};
// 실제 데이터 사용 (테스트 데이터 제거)
const dayStatus = analyzeDayStatus(dayWorkData);
// 디버깅: 상태가 있는 날짜만 로그
if (dayStatus.hasData || dayStatus.hasIssues || dayStatus.hasIncomplete || dayStatus.hasOvertimeWarning) {
let statusText = '이상없음';
if (dayStatus.hasOvertimeWarning) statusText = '확인필요';
else if (dayStatus.hasIncomplete) statusText = '미입력';
else if (dayStatus.hasIssues) statusText = '부분입력';
console.log(`📅 ${dateStr} (${dayNumber}일):`, {
상태: statusText,
작업자수: dayStatus.workerCount,
dayStatus,
원본데이터: dayWorkData
});
}
let dayClasses = ['calendar-day'];
if (!isCurrentMonth) dayClasses.push('other-month');
if (isToday) dayClasses.push('today');
if (isSunday) dayClasses.push('sunday');
if (isSaturday) dayClasses.push('saturday');
if (isSunday || isSaturday) dayClasses.push('weekend');
// 문제가 있는지 확인
const hasAnyProblem = dayStatus.hasOvertimeWarning || dayStatus.hasIncomplete || dayStatus.hasIssues;
// 문제가 없으면 초록색 배경
if (dayStatus.hasData && !hasAnyProblem) {
dayClasses.push('has-normal'); // 이상없음 (초록)
}
// 문제가 있으면 범례 아이콘들을 그대로 표시
let statusIcons = '';
if (hasAnyProblem) {
// 범례와 동일한 아이콘들 표시
if (dayStatus.hasOvertimeWarning) {
statusIcons += '<div class="legend-icon purple">●</div>';
}
if (dayStatus.hasIncomplete) {
statusIcons += '<div class="legend-icon red">●</div>';
}
if (dayStatus.hasIssues) {
statusIcons += '<div class="legend-icon orange">●</div>';
}
}
calendarHTML += `
<div class="${dayClasses.join(' ')}" onclick="openDailyWorkModal('${dateStr}')">
<div class="day-number">${dayNumber}</div>
${statusIcons}
</div>
`;
currentDay.setDate(currentDay.getDate() + 1);
}
elements.calendarDays.innerHTML = calendarHTML;
} catch (error) {
console.error('캘린더 렌더링 오류:', error);
showToast('캘린더를 불러오는데 실패했습니다.', 'error');
} finally {
showLoading(false);
}
}
// 일별 상태 분석 (집계 데이터 또는 원본 데이터 처리)
function analyzeDayStatus(dayData) {
// 새로운 집계 데이터 구조인지 확인 (monthly_summary에서 온 데이터)
if (dayData && typeof dayData === 'object' && 'totalWorkers' in dayData) {
// 미입력 판단: allWorkers 배열 길이와 실제 작업한 작업자 수 비교
const totalRegisteredWorkers = CalendarState.allWorkers ? CalendarState.allWorkers.length : 10; // 실제 등록된 작업자 수
const actualIncompleteWorkers = Math.max(0, totalRegisteredWorkers - dayData.workingWorkers);
const result = {
hasData: dayData.totalWorkers > 0,
hasIssues: dayData.partialWorkers > 0, // 부분입력 작업자가 있으면 true
hasIncomplete: actualIncompleteWorkers > 0 || dayData.incompleteWorkers > 0, // 실제 미입력 작업자가 있으면 true
hasOvertimeWarning: dayData.hasOvertimeWarning || dayData.overtimeWarningWorkers > 0, // 12시간 초과
workerCount: dayData.totalWorkers || 0
};
// 디버깅: 모든 데이터 로그 (미입력 문제 해결용)
console.log('📊 analyzeDayStatus 결과:', {
dayData,
result,
actualIncompleteWorkers,
workingWorkers: dayData.workingWorkers,
totalRegisteredWorkers: totalRegisteredWorkers,
allWorkersLength: CalendarState.allWorkers ? CalendarState.allWorkers.length : 'undefined'
});
return result;
}
// 기존 hasData 구조 확인
if (dayData && typeof dayData === 'object' && dayData.hasData !== undefined) {
return {
hasData: dayData.hasData,
hasIssues: dayData.hasIssues,
hasErrors: dayData.hasErrors,
workerCount: dayData.workerCount || 0
};
}
// 폴백: 기존 방식으로 분석 (원본 작업 데이터 배열)
if (!Array.isArray(dayData) || dayData.length === 0) {
return {
hasData: false,
hasIssues: false,
hasErrors: false,
workerCount: 0
};
}
// 작업자별로 그룹화
const workerGroups = {};
dayData.forEach(work => {
if (!workerGroups[work.worker_id]) {
workerGroups[work.worker_id] = [];
}
workerGroups[work.worker_id].push(work);
});
const workerCount = Object.keys(workerGroups).length;
let hasIssues = false;
let hasErrors = false;
// 각 작업자의 상태 분석 - 문제가 있는지만 확인
Object.values(workerGroups).forEach(workerWork => {
const totalHours = workerWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const hasError = workerWork.some(w => w.work_status_id === 2);
const hasVacation = workerWork.some(w => w.project_id === 13);
// 오류가 있는 경우
if (hasError) {
hasErrors = true;
}
// 휴가가 아닌데 미입력이거나 부분입력인 경우
else if (!hasVacation && (totalHours === 0 || totalHours < 8)) {
hasIssues = true;
}
});
return {
hasData: true,
hasIssues,
hasErrors,
workerCount
};
}
// 일일 작업 현황 모달 열기
async function openDailyWorkModal(dateStr) {
console.log(`🗓️ 클릭된 날짜: ${dateStr}`);

View File

@@ -339,6 +339,7 @@
<script src="/js/load-navbar.js?v=4"></script>
<script src="/js/modules/calendar/CalendarState.js?v=1"></script>
<script src="/js/modules/calendar/CalendarAPI.js?v=1"></script>
<script src="/js/modules/calendar/CalendarView.js?v=1"></script>
<script src="/js/work-report-calendar.js?v=41"></script>
</body>
</html>