Compare commits
8 Commits
d154514fa0
...
e3b2718767
| Author | SHA1 | Date | |
|---|---|---|---|
| e3b2718767 | |||
| 5268fec1ef | |||
| 71c06f38b1 | |||
| 5a68ced13b | |||
| ef85a880e5 | |||
| 892215a15d | |||
| 8d7422d376 | |||
| e0e0b1ad99 |
@@ -19,7 +19,33 @@ const login = async (req, res) => {
|
||||
return res.status(result.status || 400).json({ error: result.error });
|
||||
}
|
||||
|
||||
res.json(result.data);
|
||||
// 로그인 성공 후, 역할에 따라 리디렉션 URL을 결정
|
||||
const user = result.data.user;
|
||||
let redirectUrl;
|
||||
|
||||
switch (user.role) {
|
||||
case 'admin':
|
||||
case 'system': // 'system'도 관리자로 취급
|
||||
redirectUrl = '/pages/dashboard/admin.html';
|
||||
break;
|
||||
case 'leader':
|
||||
redirectUrl = '/pages/dashboard/group-leader.html';
|
||||
break;
|
||||
case 'support':
|
||||
// 예시: 지원팀 대시보드가 있다면
|
||||
// redirectUrl = '/pages/dashboard/support.html';
|
||||
// 없다면 일반 사용자 대시보드로
|
||||
redirectUrl = '/pages/dashboard/user.html';
|
||||
break;
|
||||
default:
|
||||
redirectUrl = '/pages/dashboard/user.html';
|
||||
}
|
||||
|
||||
// 최종 응답에 redirectUrl을 포함하여 전달
|
||||
res.json({
|
||||
...result.data,
|
||||
redirectUrl: redirectUrl
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login controller error:', error);
|
||||
|
||||
@@ -1,110 +1,58 @@
|
||||
const dailyIssueReportModel = require('../models/dailyIssueReportModel');
|
||||
// /controllers/dailyIssueReportController.js
|
||||
const dailyIssueReportService = require('../services/dailyIssueReportService');
|
||||
|
||||
// 1. CREATE: 단일 또는 다중 등록 (worker_id 배열 지원)
|
||||
exports.createDailyIssueReport = async (req, res) => {
|
||||
/**
|
||||
* 1. CREATE: 일일 이슈 보고서 생성 (Service Layer 사용)
|
||||
*/
|
||||
const createDailyIssueReport = async (req, res) => {
|
||||
try {
|
||||
const body = req.body;
|
||||
|
||||
// 기본 필드
|
||||
const base = {
|
||||
date: body.date,
|
||||
project_id: body.project_id,
|
||||
start_time: body.start_time,
|
||||
end_time: body.end_time,
|
||||
issue_type_id: body.issue_type_id
|
||||
};
|
||||
|
||||
if (!base.date || !base.project_id || !base.start_time || !base.end_time || !base.issue_type_id || !body.worker_id) {
|
||||
return res.status(400).json({ error: '필수 필드 누락' });
|
||||
}
|
||||
|
||||
// worker_id 배열화
|
||||
const workers = Array.isArray(body.worker_id) ? body.worker_id : [body.worker_id];
|
||||
const insertedIds = [];
|
||||
|
||||
for (const wid of workers) {
|
||||
const payload = { ...base, worker_id: wid };
|
||||
|
||||
const insertId = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.create(payload, (err, id) => {
|
||||
if (err) reject(err);
|
||||
else resolve(id);
|
||||
});
|
||||
});
|
||||
|
||||
insertedIds.push(insertId);
|
||||
}
|
||||
|
||||
res.json({ success: true, issue_report_ids: insertedIds });
|
||||
// 프론트엔드에서 worker_ids로 보내주기로 약속함
|
||||
const issueData = { ...req.body, worker_ids: req.body.worker_ids || req.body.worker_id };
|
||||
|
||||
const result = await dailyIssueReportService.createDailyIssueReportService(issueData);
|
||||
|
||||
res.status(201).json({ success: true, ...result });
|
||||
} catch (err) {
|
||||
console.error('🔥 createDailyIssueReport error:', err);
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
console.error('💥 이슈 보고서 생성 컨트롤러 오류:', err);
|
||||
res.status(400).json({ success: false, error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. READ BY DATE
|
||||
exports.getDailyIssuesByDate = async (req, res) => {
|
||||
/**
|
||||
* 2. READ BY DATE: 날짜별 이슈 조회 (Service Layer 사용)
|
||||
*/
|
||||
const getDailyIssuesByDate = async (req, res) => {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.getAllByDate(date, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
const issues = await dailyIssueReportService.getDailyIssuesByDateService(date);
|
||||
res.json(issues);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
console.error('💥 이슈 보고서 조회 컨트롤러 오류:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. READ ONE
|
||||
exports.getDailyIssueById = async (req, res) => {
|
||||
/**
|
||||
* 3. DELETE: 이슈 보고서 삭제 (Service Layer 사용)
|
||||
*/
|
||||
const removeDailyIssue = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.getById(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'DailyIssueReport not found' });
|
||||
res.json(row);
|
||||
const result = await dailyIssueReportService.removeDailyIssueService(id);
|
||||
res.json({ success: true, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
console.error('💥 이슈 보고서 삭제 컨트롤러 오류:', err);
|
||||
const statusCode = err.statusCode || 500;
|
||||
res.status(statusCode).json({ success: false, error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. UPDATE
|
||||
exports.updateDailyIssue = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.update(id, req.body, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
// 레거시 함수들은 더 이상 라우팅되지 않으므로 제거하거나 주석 처리 가능
|
||||
// exports.getDailyIssueById = ...
|
||||
// exports.updateDailyIssue = ...
|
||||
|
||||
// 5. DELETE
|
||||
exports.removeDailyIssue = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.remove(id, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'DailyIssueReport not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
module.exports = {
|
||||
createDailyIssueReport,
|
||||
getDailyIssuesByDate,
|
||||
removeDailyIssue,
|
||||
};
|
||||
@@ -31,41 +31,6 @@ const createDailyWorkReport = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* <20><> 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getAccumulatedReports = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회 결과: ${data.length}개`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
total_entries: data.length,
|
||||
accumulated_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회 (새로운 기능)
|
||||
*/
|
||||
@@ -298,85 +263,35 @@ const searchWorkReports = (req, res) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 📈 통계 조회 (작성자별 필터링)
|
||||
* 📈 통계 조회 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const getWorkReportStats = (req, res) => {
|
||||
const { start_date, end_date } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31'
|
||||
const getWorkReportStats = async (req, res) => {
|
||||
try {
|
||||
const statsData = await dailyWorkReportService.getStatisticsService(req.query);
|
||||
res.json(statsData);
|
||||
} catch (error) {
|
||||
console.error('💥 통계 조회 컨트롤러 오류:', error.message);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: '통계 조회에 실패했습니다.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📈 통계 조회: ${start_date} ~ ${end_date}, 요청자: ${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getStatistics(start_date, end_date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '통계 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
...data,
|
||||
metadata: {
|
||||
note: '현재는 전체 통계입니다. 개인별 통계는 추후 구현 예정',
|
||||
requested_by: created_by,
|
||||
period: `${start_date} ~ ${end_date}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 일일 근무 요약 조회
|
||||
* 📊 일일 근무 요약 조회 (V2 - Service Layer 사용)
|
||||
*/
|
||||
const getDailySummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (date) {
|
||||
console.log(`📊 일일 요약 조회: date=${date}`);
|
||||
dailyWorkReportModel.getSummaryByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('일일 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '일일 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else if (worker_id) {
|
||||
console.log(`📊 작업자별 요약 조회: worker_id=${worker_id}`);
|
||||
dailyWorkReportModel.getSummaryByWorker(worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: 'date 또는 worker_id 파라미터가 필요합니다.',
|
||||
examples: [
|
||||
'date=2024-06-16',
|
||||
'worker_id=1'
|
||||
]
|
||||
const getDailySummary = async (req, res) => {
|
||||
try {
|
||||
const summaryData = await dailyWorkReportService.getSummaryService(req.query);
|
||||
res.json(summaryData);
|
||||
} catch (error) {
|
||||
console.error('💥 일일 요약 조회 컨트롤러 오류:', error.message);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: '일일 요약 조회에 실패했습니다.',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -576,30 +491,28 @@ const getErrorTypes = (req, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 모든 컨트롤러 함수 내보내기 (권한별 조회 지원)
|
||||
// 모든 컨트롤러 함수 내보내기 (리팩토링된 함수 위주로 재구성)
|
||||
module.exports = {
|
||||
// 📝 핵심 CRUD 함수들 (권한별 전체 조회 지원)
|
||||
createDailyWorkReport, // 누적 추가 (덮어쓰기 없음)
|
||||
getDailyWorkReports, // 조회 (권한별 필터링 개선)
|
||||
getDailyWorkReportsByDate, // 날짜별 조회 (권한별 필터링 개선)
|
||||
searchWorkReports, // 검색 (페이지네이션)
|
||||
updateWorkReport, // 수정
|
||||
removeDailyWorkReport, // 개별 삭제
|
||||
removeDailyWorkReportByDateAndWorker, // 전체 삭제
|
||||
// 📝 V2 핵심 CRUD 함수
|
||||
createDailyWorkReport,
|
||||
getDailyWorkReports,
|
||||
updateWorkReport,
|
||||
removeDailyWorkReport,
|
||||
|
||||
// 🔄 누적 관련 새로운 함수들
|
||||
getAccumulatedReports, // 누적 현황 조회
|
||||
getContributorsSummary, // 기여자별 요약
|
||||
getMyAccumulatedData, // 개인 누적 현황
|
||||
removeMyEntry, // 개별 항목 삭제 (본인 것만)
|
||||
|
||||
// 📊 요약 및 통계 함수들
|
||||
getDailySummary, // 일일 요약
|
||||
getMonthlySummary, // 월간 요약
|
||||
getWorkReportStats, // 통계
|
||||
|
||||
// 📋 마스터 데이터 함수들
|
||||
getWorkTypes, // 작업 유형 목록
|
||||
getWorkStatusTypes, // 업무 상태 유형 목록
|
||||
getErrorTypes // 에러 유형 목록
|
||||
// 📊 V2 통계 및 요약 함수
|
||||
getWorkReportStats,
|
||||
getDailySummary,
|
||||
|
||||
// 🔽 아직 리팩토링되지 않은 레거시 함수들
|
||||
getAccumulatedReports,
|
||||
getContributorsSummary,
|
||||
getMyAccumulatedData,
|
||||
removeMyEntry,
|
||||
getDailyWorkReportsByDate,
|
||||
searchWorkReports,
|
||||
getMonthlySummary,
|
||||
removeDailyWorkReportByDateAndWorker,
|
||||
getWorkTypes,
|
||||
getWorkStatusTypes,
|
||||
getErrorTypes
|
||||
};
|
||||
@@ -1,51 +1,50 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 1. 등록 (단일 레코드)
|
||||
* 1. 여러 개의 이슈 보고서를 트랜잭션으로 생성합니다.
|
||||
* @param {Array<object>} reports - 생성할 보고서 데이터 배열
|
||||
* @returns {Promise<Array<number>>} - 삽입된 ID 배열
|
||||
*/
|
||||
const create = async (report, callback) => {
|
||||
const createMany = async (reports) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
start_time,
|
||||
end_time,
|
||||
issue_type_id,
|
||||
description = null // 선택값 처리
|
||||
} = report;
|
||||
await conn.beginTransaction();
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO DailyIssueReports
|
||||
(date, worker_id, project_id, start_time, end_time, issue_type_id, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[date, worker_id, project_id, start_time, end_time, issue_type_id, description]
|
||||
);
|
||||
const insertedIds = [];
|
||||
const sql = `
|
||||
INSERT INTO DailyIssueReports
|
||||
(date, worker_id, project_id, start_time, end_time, issue_type_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
callback(null, result.insertId);
|
||||
for (const report of reports) {
|
||||
const { date, worker_id, project_id, start_time, end_time, issue_type_id } = report;
|
||||
const [result] = await conn.query(sql, [date, worker_id, project_id, start_time, end_time, issue_type_id]);
|
||||
insertedIds.push(result.insertId);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
return insertedIds;
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
await conn.rollback();
|
||||
console.error('[Model] 여러 이슈 보고서 생성 중 오류:', err);
|
||||
throw new Error('데이터베이스에 이슈 보고서를 생성하는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. 특정 날짜의 전체 이슈 목록 조회
|
||||
* 2. 특정 날짜의 전체 이슈 목록 조회 (Promise 기반)
|
||||
*/
|
||||
const getAllByDate = async (date, callback) => {
|
||||
const getAllByDate = async (date) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
d.id,
|
||||
d.date,
|
||||
w.worker_name,
|
||||
p.project_name,
|
||||
d.start_time,
|
||||
d.end_time,
|
||||
t.category,
|
||||
t.subcategory,
|
||||
d.description
|
||||
d.id, d.date, w.worker_name, p.project_name, d.start_time, d.end_time,
|
||||
t.category, t.subcategory, d.description
|
||||
FROM DailyIssueReports d
|
||||
LEFT JOIN Workers w ON d.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON d.project_id = p.project_id
|
||||
@@ -54,9 +53,10 @@ const getAllByDate = async (date, callback) => {
|
||||
ORDER BY d.start_time ASC`,
|
||||
[date]
|
||||
);
|
||||
callback(null, rows);
|
||||
return rows;
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
console.error(`[Model] ${date} 이슈 목록 조회 오류:`, err);
|
||||
throw new Error('데이터베이스에서 이슈 목록을 조회하는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,22 +102,53 @@ const update = async (id, data, callback) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 5. 삭제
|
||||
* 5. 삭제 (Promise 기반)
|
||||
*/
|
||||
const remove = async (id, callback) => {
|
||||
const remove = async (id) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`DELETE FROM DailyIssueReports WHERE id = ?`, [id]);
|
||||
callback(null, result.affectedRows);
|
||||
return result.affectedRows;
|
||||
} catch (err) {
|
||||
console.error(`[Model] 이슈(id: ${id}) 삭제 오류:`, err);
|
||||
throw new Error('데이터베이스에서 이슈를 삭제하는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// V1 함수들은 점진적으로 제거 예정
|
||||
const create = async (report, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
start_time,
|
||||
end_time,
|
||||
issue_type_id,
|
||||
description = null // 선택값 처리
|
||||
} = report;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO DailyIssueReports
|
||||
(date, worker_id, project_id, start_time, end_time, issue_type_id, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[date, worker_id, project_id, start_time, end_time, issue_type_id, description]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
createMany, // 신규
|
||||
getAllByDate,
|
||||
remove,
|
||||
// 레거시 호환성을 위해 V1 함수들 임시 유지
|
||||
create: (report, callback) => createMany([report]).then(ids => callback(null, ids[0])).catch(err => callback(err)),
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
@@ -745,33 +745,26 @@ const removeByDateAndWorker = async (date, worker_id, deletedBy, callback) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 20. 통계 조회
|
||||
* 20. 통계 조회 (Promise 기반)
|
||||
*/
|
||||
const getStatistics = async (start_date, end_date, callback) => {
|
||||
const getStatistics = async (start_date, end_date) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const sql = `
|
||||
const overallSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_reports,
|
||||
SUM(work_hours) as total_hours,
|
||||
COUNT(DISTINCT worker_id) as unique_workers,
|
||||
COUNT(DISTINCT project_id) as unique_projects,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count,
|
||||
AVG(work_hours) as avg_hours_per_entry,
|
||||
MIN(work_hours) as min_hours,
|
||||
MAX(work_hours) as max_hours
|
||||
COUNT(DISTINCT project_id) as unique_projects
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [start_date, end_date]);
|
||||
|
||||
// 추가 통계 - 날짜별 집계
|
||||
const [overallRows] = await db.query(overallSql, [start_date, end_date]);
|
||||
|
||||
const dailyStatsSql = `
|
||||
SELECT
|
||||
report_date,
|
||||
COUNT(*) as daily_reports,
|
||||
SUM(work_hours) as daily_hours,
|
||||
COUNT(DISTINCT worker_id) as daily_workers
|
||||
FROM daily_work_reports
|
||||
@@ -779,18 +772,15 @@ const getStatistics = async (start_date, end_date, callback) => {
|
||||
GROUP BY report_date
|
||||
ORDER BY report_date DESC
|
||||
`;
|
||||
|
||||
const [dailyStats] = await db.query(dailyStatsSql, [start_date, end_date]);
|
||||
|
||||
const result = {
|
||||
overall: rows[0],
|
||||
return {
|
||||
overall: overallRows[0],
|
||||
daily_breakdown: dailyStats
|
||||
};
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
console.error('통계 조회 오류:', err);
|
||||
callback(err);
|
||||
throw new Error('데이터베이스에서 통계 정보를 조회하는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1002,43 +992,38 @@ const removeReportById = async (reportId, deletedByUserId) => {
|
||||
};
|
||||
|
||||
|
||||
// 모든 함수 내보내기 (기존 기능 + 누적 기능)
|
||||
// 모든 함수 내보내기 (Promise 기반 함수 위주로 재구성)
|
||||
module.exports = {
|
||||
// 📋 마스터 데이터
|
||||
// 새로 추가된 V2 함수 (Promise 기반)
|
||||
createReportEntries,
|
||||
getReportsWithOptions,
|
||||
updateReportById,
|
||||
removeReportById,
|
||||
|
||||
// Promise 기반으로 리팩토링된 함수
|
||||
getStatistics,
|
||||
getSummaryByDate,
|
||||
getSummaryByWorker,
|
||||
|
||||
// 아직 리팩토링되지 않았지만 필요한 기존 함수들...
|
||||
// (점진적으로 아래 함수들도 Promise 기반으로 전환해야 함)
|
||||
getAllWorkTypes,
|
||||
getAllWorkStatusTypes,
|
||||
getAllErrorTypes,
|
||||
|
||||
// 🔄 핵심 생성 함수 (누적 방식)
|
||||
createDailyReport, // 누적 추가 (덮어쓰기 없음)
|
||||
|
||||
// 📊 누적 관련 새로운 함수들
|
||||
getMyAccumulatedHours, // 개인 누적 현황
|
||||
getAccumulatedReportsByDate, // 날짜별 누적 현황
|
||||
getContributorsByDate, // 기여자별 요약
|
||||
removeSpecificEntry, // 개별 항목 삭제
|
||||
|
||||
// 📊 기존 조회 함수들 (모두 유지)
|
||||
createDailyReport,
|
||||
getMyAccumulatedHours,
|
||||
getAccumulatedReportsByDate,
|
||||
getContributorsByDate,
|
||||
removeSpecificEntry,
|
||||
getById,
|
||||
getByDate,
|
||||
getByDateAndCreator, // 날짜+작성자별 조회
|
||||
getByDateAndCreator,
|
||||
getByWorker,
|
||||
getByDateAndWorker,
|
||||
getByRange,
|
||||
searchWithDetails,
|
||||
getSummaryByDate,
|
||||
getSummaryByWorker,
|
||||
getMonthlySummary,
|
||||
|
||||
// ✏️ 수정/삭제 함수들 (기존 유지)
|
||||
updateById,
|
||||
removeById,
|
||||
removeByDateAndWorker,
|
||||
getStatistics,
|
||||
|
||||
// 새로 추가된 V2 함수
|
||||
createReportEntries,
|
||||
getReportsWithOptions,
|
||||
updateReportById,
|
||||
removeReportById
|
||||
};
|
||||
93
api.hyungi.net/services/dailyIssueReportService.js
Normal file
93
api.hyungi.net/services/dailyIssueReportService.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// /services/dailyIssueReportService.js
|
||||
const dailyIssueReportModel = require('../models/dailyIssueReportModel');
|
||||
|
||||
/**
|
||||
* 일일 이슈 보고서를 생성하는 비즈니스 로직을 처리합니다.
|
||||
* 한 번에 여러 작업자에 대해 동일한 이슈를 등록할 수 있습니다.
|
||||
* @param {object} issueData - 컨트롤러에서 전달된 이슈 데이터
|
||||
* @returns {Promise<object>} 생성 결과
|
||||
*/
|
||||
const createDailyIssueReportService = async (issueData) => {
|
||||
const { date, project_id, start_time, end_time, issue_type_id, worker_ids } = issueData;
|
||||
|
||||
// 1. 유효성 검사
|
||||
if (!date || !project_id || !start_time || !end_time || !issue_type_id || !worker_ids) {
|
||||
throw new Error('필수 필드가 누락되었습니다.');
|
||||
}
|
||||
if (!Array.isArray(worker_ids) || worker_ids.length === 0) {
|
||||
throw new Error('worker_ids는 최소 한 명 이상의 작업자를 포함하는 배열이어야 합니다.');
|
||||
}
|
||||
|
||||
// 2. 모델에 전달할 데이터 준비
|
||||
const reportsToCreate = worker_ids.map(worker_id => ({
|
||||
date,
|
||||
project_id,
|
||||
start_time,
|
||||
end_time,
|
||||
issue_type_id,
|
||||
worker_id
|
||||
}));
|
||||
|
||||
// 3. 모델 함수 호출 (모델에 createMany와 같은 함수가 필요)
|
||||
try {
|
||||
const insertedIds = await dailyIssueReportModel.createMany(reportsToCreate);
|
||||
return {
|
||||
message: `${insertedIds.length}개의 이슈 보고서가 성공적으로 생성되었습니다.`,
|
||||
issue_report_ids: insertedIds
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Service] 이슈 보고서 생성 중 오류 발생:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 날짜의 모든 이슈 보고서를 조회합니다.
|
||||
* @param {string} date - 조회할 날짜 (YYYY-MM-DD)
|
||||
* @returns {Promise<Array>} 조회된 이슈 보고서 배열
|
||||
*/
|
||||
const getDailyIssuesByDateService = async (date) => {
|
||||
if (!date) {
|
||||
throw new Error('조회를 위해 날짜(date)는 필수입니다.');
|
||||
}
|
||||
try {
|
||||
const issues = await dailyIssueReportModel.getAllByDate(date);
|
||||
return issues;
|
||||
} catch (error) {
|
||||
console.error(`[Service] ${date}의 이슈 보고서 조회 중 오류 발생:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 ID의 이슈 보고서를 삭제합니다.
|
||||
* @param {string} issueId - 삭제할 이슈 보고서의 ID
|
||||
* @returns {Promise<object>} 삭제 결과
|
||||
*/
|
||||
const removeDailyIssueService = async (issueId) => {
|
||||
if (!issueId) {
|
||||
throw new Error('삭제를 위해 이슈 보고서 ID가 필요합니다.');
|
||||
}
|
||||
try {
|
||||
const affectedRows = await dailyIssueReportModel.remove(issueId);
|
||||
if (affectedRows === 0) {
|
||||
const notFoundError = new Error('삭제할 이슈 보고서를 찾을 수 없습니다.');
|
||||
notFoundError.statusCode = 404;
|
||||
throw notFoundError;
|
||||
}
|
||||
return {
|
||||
message: '이슈 보고서가 성공적으로 삭제되었습니다.',
|
||||
deleted_id: issueId,
|
||||
affected_rows: affectedRows
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`[Service] 이슈 보고서(id: ${issueId}) 삭제 중 오류 발생:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createDailyIssueReportService,
|
||||
getDailyIssuesByDateService,
|
||||
removeDailyIssueService,
|
||||
};
|
||||
@@ -204,10 +204,71 @@ const removeDailyWorkReportService = async (reportId, userInfo) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 기간별 작업 보고서 통계를 조회하는 비즈니스 로직을 처리합니다.
|
||||
* @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (start_date, end_date)
|
||||
* @returns {Promise<object>} 통계 데이터
|
||||
*/
|
||||
const getStatisticsService = async (queryParams) => {
|
||||
const { start_date, end_date } = queryParams;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
throw new Error('통계 조회를 위해 시작일(start_date)과 종료일(end_date)이 모두 필요합니다.');
|
||||
}
|
||||
|
||||
console.log(`📈 [Service] 통계 조회 요청: ${start_date} ~ ${end_date}`);
|
||||
|
||||
try {
|
||||
// 모델의 getStatistics 함수가 Promise를 반환하도록 수정 필요
|
||||
const statsData = await dailyWorkReportModel.getStatistics(start_date, end_date);
|
||||
|
||||
console.log('✅ [Service] 통계 조회 성공');
|
||||
return {
|
||||
...statsData,
|
||||
metadata: {
|
||||
period: `${start_date} ~ ${end_date}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Service] 통계 조회 중 오류 발생:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 일일 또는 작업자별 작업 요약 정보를 조회하는 비즈니스 로직을 처리합니다.
|
||||
* @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (date 또는 worker_id)
|
||||
* @returns {Promise<object>} 요약 데이터
|
||||
*/
|
||||
const getSummaryService = async (queryParams) => {
|
||||
const { date, worker_id } = queryParams;
|
||||
|
||||
if (!date && !worker_id) {
|
||||
throw new Error('일일 또는 작업자별 요약 조회를 위해 날짜(date) 또는 작업자 ID(worker_id)가 필요합니다.');
|
||||
}
|
||||
|
||||
try {
|
||||
if (date) {
|
||||
console.log(`📊 [Service] 일일 요약 조회 요청: date=${date}`);
|
||||
// 모델의 getSummaryByDate 함수가 Promise를 반환하도록 수정 필요
|
||||
return await dailyWorkReportModel.getSummaryByDate(date);
|
||||
} else { // worker_id
|
||||
console.log(`📊 [Service] 작업자별 요약 조회 요청: worker_id=${worker_id}`);
|
||||
// 모델의 getSummaryByWorker 함수가 Promise를 반환하도록 수정 필요
|
||||
return await dailyWorkReportModel.getSummaryByWorker(worker_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Service] 요약 정보 조회 중 오류 발생:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createDailyWorkReportService,
|
||||
getDailyWorkReportsService,
|
||||
updateWorkReportService,
|
||||
removeDailyWorkReportService,
|
||||
getStatisticsService,
|
||||
getSummaryService,
|
||||
};
|
||||
@@ -1,18 +1,45 @@
|
||||
// /public/js/api-helper.js
|
||||
|
||||
// API 기본 URL 설정
|
||||
const API_BASE = location.hostname.includes('localhost')
|
||||
? 'http://localhost:3005/api'
|
||||
: 'https://api.hyungi.net/api';
|
||||
import { API_BASE_URL } from './api-config.js';
|
||||
import { getToken, clearAuthData } from './auth.js';
|
||||
|
||||
// 인증된 fetch 함수
|
||||
async function authFetch(url, options = {}) {
|
||||
const token = localStorage.getItem('token');
|
||||
/**
|
||||
* 로그인 API를 호출합니다. (인증이 필요 없는 public 요청)
|
||||
* @param {string} username - 사용자 아이디
|
||||
* @param {string} password - 사용자 비밀번호
|
||||
* @returns {Promise<object>} - API 응답 결과
|
||||
*/
|
||||
export async function login(username, password) {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
// API 에러 응답을 그대로 에러 객체로 던져서 호출부에서 처리하도록 함
|
||||
throw new Error(result.error || '로그인에 실패했습니다.');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 인증이 필요한 API 요청을 위한 fetch 래퍼 함수
|
||||
* @param {string} endpoint - /로 시작하는 API 엔드포인트
|
||||
* @param {object} options - fetch 함수에 전달할 옵션
|
||||
* @returns {Promise<Response>} - fetch 응답 객체
|
||||
*/
|
||||
async function authFetch(endpoint, options = {}) {
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
console.error('토큰이 없습니다. 로그인이 필요합니다.');
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
clearAuthData(); // 인증 정보 정리
|
||||
window.location.href = '/index.html'; // 로그인 페이지로 리디렉션
|
||||
// 에러를 던져서 후속 실행을 중단
|
||||
throw new Error('인증 토큰이 없습니다.');
|
||||
}
|
||||
|
||||
const defaultHeaders = {
|
||||
@@ -20,7 +47,7 @@ async function authFetch(url, options = {}) {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
@@ -28,59 +55,61 @@ async function authFetch(url, options = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
// 401 에러 시 로그인 페이지로
|
||||
// 401 Unauthorized 에러 발생 시, 토큰이 유효하지 않다는 의미
|
||||
if (response.status === 401) {
|
||||
console.error('인증 실패. 다시 로그인해주세요.');
|
||||
localStorage.removeItem('token');
|
||||
console.error('인증 실패. 토큰이 만료되었거나 유효하지 않습니다.');
|
||||
clearAuthData(); // 만료된 인증 정보 정리
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
throw new Error('인증에 실패했습니다.');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// GET 요청 헬퍼
|
||||
async function apiGet(endpoint) {
|
||||
const response = await authFetch(`${API_BASE}${endpoint}`);
|
||||
if (!response) return null;
|
||||
// 공통 API 요청 함수들
|
||||
|
||||
/**
|
||||
* GET 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
*/
|
||||
export async function apiGet(endpoint) {
|
||||
const response = await authFetch(endpoint);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// POST 요청 헬퍼
|
||||
async function apiPost(endpoint, data) {
|
||||
const response = await authFetch(`${API_BASE}${endpoint}`, {
|
||||
/**
|
||||
* POST 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
* @param {object} data - 전송할 데이터
|
||||
*/
|
||||
export async function apiPost(endpoint, data) {
|
||||
const response = await authFetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!response) return null;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// PUT 요청 헬퍼
|
||||
async function apiPut(endpoint, data) {
|
||||
const response = await authFetch(`${API_BASE}${endpoint}`, {
|
||||
/**
|
||||
* PUT 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
* @param {object} data - 전송할 데이터
|
||||
*/
|
||||
export async function apiPut(endpoint, data) {
|
||||
const response = await authFetch(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!response) return null;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// DELETE 요청 헬퍼
|
||||
async function apiDelete(endpoint) {
|
||||
const response = await authFetch(`${API_BASE}${endpoint}`, {
|
||||
/**
|
||||
* DELETE 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
*/
|
||||
export async function apiDelete(endpoint) {
|
||||
const response = await authFetch(endpoint, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response) return null;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 내보내기 (다른 파일에서 사용 가능)
|
||||
window.API = {
|
||||
get: apiGet,
|
||||
post: apiPost,
|
||||
put: apiPut,
|
||||
delete: apiDelete,
|
||||
fetch: authFetch,
|
||||
BASE: API_BASE
|
||||
};
|
||||
}
|
||||
@@ -1,79 +1,27 @@
|
||||
// ✅ /js/auth-check.js
|
||||
// 토큰 검증과 권한 체크
|
||||
// /js/auth-check.js
|
||||
import { isLoggedIn, getUser, clearAuthData } from './auth.js';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
function isValidJWT(token) {
|
||||
return typeof token === 'string' && token.split('.').length === 3;
|
||||
}
|
||||
|
||||
function getPayload(token) {
|
||||
try {
|
||||
return JSON.parse(atob(token.split('.')[1]));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!token || !isValidJWT(token)) {
|
||||
console.log('🚨 토큰이 없거나 유효하지 않음');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/index.html';
|
||||
} else {
|
||||
const user = getPayload(token);
|
||||
const storedUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
|
||||
console.log('🔐 JWT 사용자 정보:', user);
|
||||
console.log('💾 저장된 사용자 정보:', storedUser);
|
||||
|
||||
// 사용자 정보 우선순위: localStorage > JWT payload
|
||||
const currentUser = storedUser.access_level ? storedUser : user;
|
||||
|
||||
if (!currentUser || !currentUser.username || !currentUser.access_level) {
|
||||
console.log('🚨 사용자 정보가 유효하지 않음');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
// 즉시 실행 함수로 스코프를 보호하고 로직을 실행
|
||||
(function() {
|
||||
if (!isLoggedIn()) {
|
||||
console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.');
|
||||
clearAuthData(); // 만약을 위해 한번 더 정리
|
||||
window.location.href = '/index.html';
|
||||
} else {
|
||||
console.log('✅ 인증 성공:', currentUser.username, currentUser.access_level);
|
||||
|
||||
// 사용자 이름 표시
|
||||
const userNameElements = document.querySelectorAll('#user-name, .user-name');
|
||||
userNameElements.forEach(el => {
|
||||
if (el) el.textContent = currentUser.name || currentUser.username;
|
||||
});
|
||||
|
||||
// 🎯 역할별 메뉴 표시/숨김 처리
|
||||
const accessLevel = currentUser.access_level;
|
||||
|
||||
// 관리자 전용 메뉴
|
||||
if (accessLevel !== 'admin' && accessLevel !== 'system') {
|
||||
const adminOnly = document.querySelectorAll('.admin-only, .system-only');
|
||||
adminOnly.forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// 그룹장 전용 메뉴
|
||||
if (accessLevel !== 'group_leader') {
|
||||
const groupLeaderOnly = document.querySelectorAll('.group-leader-only');
|
||||
groupLeaderOnly.forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// 지원팀 전용 메뉴
|
||||
if (accessLevel !== 'support') {
|
||||
const supportOnly = document.querySelectorAll('.support-only');
|
||||
supportOnly.forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// 일반 작업자 전용 메뉴
|
||||
if (accessLevel !== 'worker' && accessLevel !== 'user') {
|
||||
const workerOnly = document.querySelectorAll('.worker-only');
|
||||
workerOnly.forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// 전역 사용자 정보 저장
|
||||
window.currentUser = currentUser;
|
||||
|
||||
console.log('🎭 역할별 메뉴 필터링 완료:', accessLevel);
|
||||
return; // 이후 코드 실행 방지
|
||||
}
|
||||
}
|
||||
|
||||
const currentUser = getUser();
|
||||
|
||||
// 사용자 정보가 유효한지 확인 (토큰은 있지만 유저 정보가 깨졌을 경우)
|
||||
if (!currentUser || !currentUser.username || !currentUser.role) {
|
||||
console.error('🚨 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
|
||||
clearAuthData();
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ ${currentUser.username}(${currentUser.role})님 인증 성공.`);
|
||||
|
||||
// 역할 기반 메뉴 제어 로직은 각 컴포넌트 로더(load-navbar.js 등)로 이전함.
|
||||
// 전역 변수 할당(window.currentUser) 제거.
|
||||
})();
|
||||
76
web-ui/js/auth.js
Normal file
76
web-ui/js/auth.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// js/auth.js
|
||||
|
||||
/**
|
||||
* JWT 토큰을 디코딩하여 페이로드(내용)를 반환합니다.
|
||||
* @param {string} token - JWT 토큰
|
||||
* @returns {object|null} - 디코딩된 페이로드 객체 또는 파싱 실패 시 null
|
||||
*/
|
||||
export function parseJwt(token) {
|
||||
try {
|
||||
// 토큰의 두 번째 부분(payload)을 base64 디코딩하고 JSON으로 파싱
|
||||
return JSON.parse(atob(token.split('.')[1]));
|
||||
} catch (e) {
|
||||
console.error("잘못된 토큰입니다.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* localStorage에서 인증 토큰을 가져옵니다.
|
||||
* @returns {string|null} - 저장된 토큰 또는 토큰이 없을 경우 null
|
||||
*/
|
||||
export function getToken() {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
/**
|
||||
* localStorage에서 사용자 정보를 가져옵니다.
|
||||
* @returns {object|null} - 저장된 사용자 객체 또는 정보가 없을 경우 null
|
||||
*/
|
||||
export function getUser() {
|
||||
const user = localStorage.getItem('user');
|
||||
try {
|
||||
return user ? JSON.parse(user) : null;
|
||||
} catch(e) {
|
||||
console.error("사용자 정보를 파싱하는 데 실패했습니다.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 성공 후 토큰과 사용자 정보를 localStorage에 저장합니다.
|
||||
* @param {string} token - 서버에서 받은 JWT 토큰
|
||||
* @param {object} user - 서버에서 받은 사용자 정보 객체
|
||||
*/
|
||||
export function saveAuthData(token, user) {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃 시 localStorage에서 인증 정보를 제거합니다.
|
||||
*/
|
||||
export function clearAuthData() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 로그인 상태인지 확인합니다.
|
||||
* @returns {boolean} - 로그인 상태이면 true, 아니면 false
|
||||
*/
|
||||
export function isLoggedIn() {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 선택 사항: 토큰 만료 여부 확인 로직 추가 가능
|
||||
// const payload = parseJwt(token);
|
||||
// if (payload && payload.exp * 1000 > Date.now()) {
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
|
||||
return !!token;
|
||||
}
|
||||
66
web-ui/js/daily-issue-api.js
Normal file
66
web-ui/js/daily-issue-api.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// /js/daily-issue-api.js
|
||||
import { apiGet, apiPost } from './api-helper.js';
|
||||
|
||||
/**
|
||||
* 이슈 보고서 작성을 위해 필요한 초기 데이터(프로젝트, 이슈 유형)를 가져옵니다.
|
||||
* @returns {Promise<{projects: Array, issueTypes: Array}>}
|
||||
*/
|
||||
export async function getInitialData() {
|
||||
try {
|
||||
const [projects, issueTypes] = await Promise.all([
|
||||
apiGet('/projects'),
|
||||
apiGet('/issue-types')
|
||||
]);
|
||||
return { projects, issueTypes };
|
||||
} catch (error) {
|
||||
console.error('이슈 보고서 초기 데이터 로딩 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜에 근무한 작업자 목록을 가져옵니다.
|
||||
* @param {string} date - 조회할 날짜 (YYYY-MM-DD)
|
||||
* @returns {Promise<Array>} - 작업자 목록
|
||||
*/
|
||||
export async function getWorkersByDate(date) {
|
||||
try {
|
||||
// 백엔드에 해당 날짜의 작업자 목록을 요청하는 API가 있다고 가정합니다.
|
||||
// (예: /api/workers?work_date=YYYY-MM-DD)
|
||||
// 현재는 기존 로직을 최대한 활용하여 구현합니다.
|
||||
let workers = [];
|
||||
const reports = await apiGet(`/daily-work-reports?date=${date}`);
|
||||
|
||||
if (reports && reports.length > 0) {
|
||||
const workerMap = new Map();
|
||||
reports.forEach(r => {
|
||||
if (!workerMap.has(r.worker_id)) {
|
||||
workerMap.set(r.worker_id, { worker_id: r.worker_id, worker_name: r.worker_name });
|
||||
}
|
||||
});
|
||||
workers = Array.from(workerMap.values());
|
||||
} else {
|
||||
// 보고서가 없으면 전체 작업자 목록을 가져옵니다.
|
||||
workers = await apiGet('/workers');
|
||||
}
|
||||
return workers.sort((a, b) => a.worker_name.localeCompare(b.worker_name));
|
||||
} catch (error) {
|
||||
console.error(`${date}의 작업자 목록 로딩 실패:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성된 이슈 보고서 데이터를 서버에 전송합니다.
|
||||
* @param {object} issueData - 전송할 이슈 데이터
|
||||
* @returns {Promise<object>} - 서버 응답 결과
|
||||
*/
|
||||
export async function createIssueReport(issueData) {
|
||||
try {
|
||||
const result = await apiPost('/issue-reports', issueData);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('이슈 보고서 생성 요청 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
103
web-ui/js/daily-issue-ui.js
Normal file
103
web-ui/js/daily-issue-ui.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// /js/daily-issue-ui.js
|
||||
|
||||
const DOM = {
|
||||
dateSelect: document.getElementById('dateSelect'),
|
||||
projectSelect: document.getElementById('projectSelect'),
|
||||
issueTypeSelect: document.getElementById('issueTypeSelect'),
|
||||
timeStart: document.getElementById('timeStart'),
|
||||
timeEnd: document.getElementById('timeEnd'),
|
||||
workerList: document.getElementById('workerList'),
|
||||
form: document.getElementById('issueForm'),
|
||||
submitBtn: document.getElementById('submitBtn'),
|
||||
};
|
||||
|
||||
function createOption(value, text) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = text;
|
||||
return option;
|
||||
}
|
||||
|
||||
export function populateProjects(projects) {
|
||||
DOM.projectSelect.innerHTML = '<option value="">-- 프로젝트 선택 --</option>';
|
||||
if (Array.isArray(projects)) {
|
||||
projects.forEach(p => DOM.projectSelect.appendChild(createOption(p.project_id, p.project_name)));
|
||||
}
|
||||
}
|
||||
|
||||
export function populateIssueTypes(issueTypes) {
|
||||
DOM.issueTypeSelect.innerHTML = '<option value="">-- 이슈 유형 선택 --</option>';
|
||||
if (Array.isArray(issueTypes)) {
|
||||
issueTypes.forEach(t => DOM.issueTypeSelect.appendChild(createOption(t.issue_type_id, `${t.category}:${t.subcategory}`)));
|
||||
}
|
||||
}
|
||||
|
||||
export function populateTimeOptions() {
|
||||
for (let h = 0; h < 24; h++) {
|
||||
for (let m of [0, 30]) {
|
||||
const time = `${String(h).padStart(2, '0')}:${m === 0 ? '00' : '30'}`;
|
||||
DOM.timeStart.appendChild(createOption(time, time));
|
||||
DOM.timeEnd.appendChild(createOption(time, time.replace('00:00', '24:00')));
|
||||
}
|
||||
}
|
||||
DOM.timeEnd.value = "24:00"; // 기본값 설정
|
||||
}
|
||||
|
||||
export function renderWorkerList(workers) {
|
||||
DOM.workerList.innerHTML = '';
|
||||
if (!Array.isArray(workers) || workers.length === 0) {
|
||||
DOM.workerList.textContent = '선택 가능한 작업자가 없습니다.';
|
||||
return;
|
||||
}
|
||||
workers.forEach(worker => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn';
|
||||
btn.textContent = worker.worker_name;
|
||||
btn.dataset.id = worker.worker_id;
|
||||
btn.addEventListener('click', () => btn.classList.toggle('selected'));
|
||||
DOM.workerList.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
export function getFormData() {
|
||||
const selectedWorkers = [...DOM.workerList.querySelectorAll('.btn.selected')].map(b => b.dataset.id);
|
||||
|
||||
if (selectedWorkers.length === 0) {
|
||||
alert('작업자를 한 명 이상 선택해주세요.');
|
||||
return null;
|
||||
}
|
||||
if (DOM.timeEnd.value <= DOM.timeStart.value) {
|
||||
alert('종료 시간은 시작 시간보다 이후여야 합니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = new FormData(DOM.form);
|
||||
const data = {
|
||||
date: formData.get('dateSelect'), // input name 속성이 없어 직접 가져옴
|
||||
project_id: DOM.projectSelect.value,
|
||||
issue_type_id: DOM.issueTypeSelect.value,
|
||||
start_time: DOM.timeStart.value,
|
||||
end_time: DOM.timeEnd.value,
|
||||
worker_ids: selectedWorkers, // worker_id -> worker_ids 로 명확하게 변경
|
||||
};
|
||||
|
||||
for (const key in data) {
|
||||
if (!data[key] || (Array.isArray(data[key]) && data[key].length === 0)) {
|
||||
alert('모든 필수 항목을 입력해주세요.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function setSubmitButtonState(isLoading) {
|
||||
if (isLoading) {
|
||||
DOM.submitBtn.disabled = true;
|
||||
DOM.submitBtn.textContent = '등록 중...';
|
||||
} else {
|
||||
DOM.submitBtn.disabled = false;
|
||||
DOM.submitBtn.textContent = '등록';
|
||||
}
|
||||
}
|
||||
@@ -1,154 +1,89 @@
|
||||
// /js/daily-issue.js
|
||||
|
||||
import { API, getAuthHeaders } from '/js/api-config.js';
|
||||
import { getInitialData, getWorkersByDate, createIssueReport } from './daily-issue-api.js';
|
||||
import {
|
||||
populateProjects,
|
||||
populateIssueTypes,
|
||||
populateTimeOptions,
|
||||
renderWorkerList,
|
||||
getFormData,
|
||||
setSubmitButtonState
|
||||
} from './daily-issue-ui.js';
|
||||
|
||||
const dateInput = document.getElementById('dateSelect');
|
||||
const projectSel = document.getElementById('projectSelect');
|
||||
const issueTypeSel = document.getElementById('issueTypeSelect');
|
||||
const timeStartSel = document.getElementById('timeStart');
|
||||
const timeEndSel = document.getElementById('timeEnd');
|
||||
const workerList = document.getElementById('workerList');
|
||||
const dateSelect = document.getElementById('dateSelect');
|
||||
const form = document.getElementById('issueForm');
|
||||
|
||||
// 오늘 날짜 기본 설정
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
dateInput.value = today;
|
||||
|
||||
// 시간 옵션 생성
|
||||
function populateTimeOptions(startEl, endEl) {
|
||||
for (let h = 0; h < 24; h++) {
|
||||
for (let m of [0, 30]) {
|
||||
const time = `${String(h).padStart(2, '0')}:${m === 0 ? '00' : '30'}`;
|
||||
const option = new Option(time, time);
|
||||
startEl.appendChild(option);
|
||||
endEl.appendChild(option.cloneNode(true));
|
||||
}
|
||||
/**
|
||||
* 날짜가 변경될 때마다 해당 날짜의 작업자 목록을 다시 불러옵니다.
|
||||
*/
|
||||
async function handleDateChange() {
|
||||
const selectedDate = dateSelect.value;
|
||||
if (!selectedDate) {
|
||||
document.getElementById('workerList').textContent = '날짜를 먼저 선택하세요.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
populateTimeOptions(timeStartSel, timeEndSel);
|
||||
|
||||
// 📌 프로젝트 목록
|
||||
async function loadProjects() {
|
||||
|
||||
document.getElementById('workerList').textContent = '작업자 목록을 불러오는 중...';
|
||||
try {
|
||||
const res = await fetch(`${API}/projects`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(p => {
|
||||
projectSel.appendChild(new Option(p.project_name, p.project_id));
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('프로젝트 로딩 오류:', err);
|
||||
const workers = await getWorkersByDate(selectedDate);
|
||||
renderWorkerList(workers);
|
||||
} catch (error) {
|
||||
document.getElementById('workerList').textContent = '작업자 목록 로딩에 실패했습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
// 📌 이슈 유형 목록
|
||||
async function loadIssueTypes() {
|
||||
/**
|
||||
* 폼 제출 이벤트를 처리합니다.
|
||||
*/
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const issueData = getFormData();
|
||||
|
||||
if (!issueData) return; // 유효성 검사 실패
|
||||
|
||||
setSubmitButtonState(true);
|
||||
try {
|
||||
const res = await fetch(`${API}/issue-types`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(t => {
|
||||
issueTypeSel.appendChild(new Option(`${t.category}:${t.subcategory}`, t.issue_type_id));
|
||||
});
|
||||
const result = await createIssueReport(issueData);
|
||||
if (result.success) {
|
||||
alert('✅ 이슈가 성공적으로 등록되었습니다.');
|
||||
form.reset(); // 폼 초기화
|
||||
dateSelect.value = new Date().toISOString().split('T')[0]; // 날짜 오늘로 리셋
|
||||
handleDateChange(); // 작업자 목록 새로고침
|
||||
} else {
|
||||
throw new Error(result.error || '알 수 없는 오류가 발생했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('이슈 타입 로딩 오류:', err);
|
||||
} catch (error) {
|
||||
alert(`🚨 등록 실패: ${error.message}`);
|
||||
} finally {
|
||||
setSubmitButtonState(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 📌 작업자 목록
|
||||
async function loadWorkers() {
|
||||
const d = dateInput.value;
|
||||
workerList.textContent = '로딩 중...';
|
||||
/**
|
||||
* 페이지 초기화 함수
|
||||
*/
|
||||
async function initializePage() {
|
||||
// 오늘 날짜 기본 설정
|
||||
dateSelect.value = new Date().toISOString().split('T')[0];
|
||||
|
||||
populateTimeOptions();
|
||||
|
||||
// 프로젝트, 이슈유형, 작업자 목록을 병렬로 로드
|
||||
try {
|
||||
let res = await fetch(`${API}/workreports/date/${d}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
let reports = await res.json();
|
||||
|
||||
if (!reports.length) {
|
||||
const wRes = await fetch(`${API}/workers`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
const allWorkers = await wRes.json();
|
||||
if (Array.isArray(allWorkers)) {
|
||||
reports = allWorkers.map(w => ({
|
||||
worker_id: w.worker_id,
|
||||
worker_name: w.worker_name
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
workerList.innerHTML = '';
|
||||
reports.forEach(r => {
|
||||
if (!seen.has(r.worker_id)) {
|
||||
seen.add(r.worker_id);
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn';
|
||||
btn.textContent = r.worker_name;
|
||||
btn.dataset.id = r.worker_id;
|
||||
btn.addEventListener('click', () => btn.classList.toggle('selected'));
|
||||
workerList.appendChild(btn);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('👿 작업자 로딩 오류:', err);
|
||||
workerList.textContent = '작업자 로딩 실패';
|
||||
const [initialData] = await Promise.all([
|
||||
getInitialData(),
|
||||
handleDateChange() // 초기 작업자 목록 로드
|
||||
]);
|
||||
populateProjects(initialData.projects);
|
||||
populateIssueTypes(initialData.issueTypes);
|
||||
} catch (error) {
|
||||
alert('페이지 초기화 중 오류가 발생했습니다. 새로고침 해주세요.');
|
||||
}
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
dateSelect.addEventListener('change', handleDateChange);
|
||||
form.addEventListener('submit', handleSubmit);
|
||||
}
|
||||
|
||||
// 📌 초기 실행
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadProjects();
|
||||
loadIssueTypes();
|
||||
loadWorkers();
|
||||
|
||||
dateInput.addEventListener('change', loadWorkers);
|
||||
|
||||
form.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const workerIds = [...workerList.querySelectorAll('.btn.selected')].map(b => b.dataset.id);
|
||||
if (!workerIds.length) return alert('작업자를 선택하세요.');
|
||||
|
||||
const projectId = projectSel.value;
|
||||
const issueTypeId = issueTypeSel.value;
|
||||
const start = timeStartSel.value;
|
||||
const end = timeEndSel.value;
|
||||
|
||||
if (!projectId || !issueTypeId || !start || !end) return alert('모든 값을 입력하세요.');
|
||||
if (end <= start) return alert('종료 시간은 시작 시간 이후여야 합니다.');
|
||||
|
||||
const payload = {
|
||||
date: dateInput.value,
|
||||
worker_id: workerIds,
|
||||
project_id: projectId,
|
||||
start_time: timeStartSel.value,
|
||||
end_time: timeEndSel.value,
|
||||
issue_type_id: issueTypeId
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/issue-reports`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const json = await res.json();
|
||||
if (res.ok && json.success) {
|
||||
alert('✅ 등록 완료!');
|
||||
loadWorkers();
|
||||
} else {
|
||||
alert(json.error || '등록 실패');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('🚨 서버 오류: ' + err.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
// DOM이 로드되면 페이지 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
@@ -1,765 +1,81 @@
|
||||
// daily-report-viewer.js - 통합 API 설정 적용 버전
|
||||
// /js/daily-report-viewer.js
|
||||
|
||||
// =================================================================
|
||||
// 🌐 통합 API 설정 import
|
||||
// =================================================================
|
||||
import { API, getAuthHeaders, apiCall } from '/js/api-config.js';
|
||||
import { fetchReportData } from './report-viewer-api.js';
|
||||
import { renderReport, processReportData, showLoading, showError } from './report-viewer-ui.js';
|
||||
import { exportToExcel, printReport } from './report-viewer-export.js';
|
||||
import { getUser } from './auth.js';
|
||||
|
||||
// =================================================================
|
||||
// 🌐 전역 변수 및 기본 설정
|
||||
// =================================================================
|
||||
let currentReportData = null;
|
||||
let workTypes = [];
|
||||
let workStatusTypes = [];
|
||||
let errorTypes = [];
|
||||
// 전역 상태: 현재 화면에 표시된 데이터
|
||||
let currentProcessedData = null;
|
||||
|
||||
// =================================================================
|
||||
// 🔧 유틸리티 함수들 (입력 페이지와 동일)
|
||||
// =================================================================
|
||||
/**
|
||||
* 날짜를 기준으로 보고서를 검색하고 화면에 렌더링합니다.
|
||||
*/
|
||||
async function searchReports() {
|
||||
const dateInput = document.getElementById('reportDate');
|
||||
const selectedDate = dateInput.value;
|
||||
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return null;
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
if (payloadBase64) {
|
||||
const payload = JSON.parse(atob(payloadBase64));
|
||||
console.log('토큰에서 추출한 사용자 정보:', payload);
|
||||
return payload;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('토큰에서 사용자 정보 추출 실패:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
|
||||
if (userInfo) {
|
||||
const parsed = JSON.parse(userInfo);
|
||||
console.log('localStorage에서 가져온 사용자 정보:', parsed);
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
if (!selectedDate) {
|
||||
showError('날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
currentProcessedData = null; // 새 검색이 시작되면 이전 데이터 초기화
|
||||
|
||||
try {
|
||||
const rawData = await fetchReportData(selectedDate);
|
||||
currentProcessedData = processReportData(rawData, selectedDate);
|
||||
renderReport(currentProcessedData);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
renderReport(null); // 에러 발생 시 데이터 없는 화면 표시
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 한국 시간 기준 오늘 날짜 가져기기
|
||||
function getKoreaToday() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
/**
|
||||
* 페이지의 모든 이벤트 리스너를 설정합니다.
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
document.getElementById('searchBtn')?.addEventListener('click', searchReports);
|
||||
document.getElementById('todayBtn')?.addEventListener('click', () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('reportDate').value = today;
|
||||
searchReports();
|
||||
});
|
||||
|
||||
document.getElementById('reportDate')?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') searchReports();
|
||||
});
|
||||
|
||||
document.getElementById('exportExcelBtn')?.addEventListener('click', () => {
|
||||
exportToExcel(currentProcessedData);
|
||||
});
|
||||
|
||||
document.getElementById('printBtn')?.addEventListener('click', printReport);
|
||||
}
|
||||
|
||||
// 권한 확인 함수 (수정된 버전)
|
||||
function checkUserPermission(user) {
|
||||
if (!user || !user.access_level) {
|
||||
return { level: 'none', canViewAll: false, description: '권한 없음' };
|
||||
}
|
||||
|
||||
const accessLevel = user.access_level.toLowerCase();
|
||||
|
||||
// 🎯 권한 레벨 정의 (더 유연하게)
|
||||
if (accessLevel === 'system' || accessLevel === 'admin') {
|
||||
return {
|
||||
level: 'admin',
|
||||
canViewAll: true,
|
||||
description: '시스템/관리자 (전체 조회 시도 → 실패 시 본인 데이터)'
|
||||
};
|
||||
} else if (accessLevel === 'manager' || accessLevel === 'group_leader' || accessLevel === '그룹장') {
|
||||
return {
|
||||
level: 'manager',
|
||||
canViewAll: false,
|
||||
description: '그룹장 (본인 입력 데이터만)'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
level: 'user',
|
||||
canViewAll: false,
|
||||
description: '일반 사용자 (본인 입력 데이터만)'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 🚀 초기화 및 이벤트 설정
|
||||
// =================================================================
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
console.log('🔥 ===== 통합 API 설정 적용 일일보고서 뷰어 시작 =====');
|
||||
|
||||
// 사용자 정보 및 권한 확인
|
||||
const userInfo = getCurrentUser();
|
||||
const permission = checkUserPermission(userInfo);
|
||||
|
||||
console.log('👤 사용자 정보:', userInfo);
|
||||
console.log('🔐 권한 정보:', permission);
|
||||
|
||||
// 토큰 확인
|
||||
const mainToken = localStorage.getItem('token');
|
||||
if (!mainToken) {
|
||||
console.error('❌ 토큰이 없습니다.');
|
||||
alert('로그인이 필요합니다.');
|
||||
/**
|
||||
* 페이지가 처음 로드될 때 실행되는 초기화 함수
|
||||
*/
|
||||
function initializePage() {
|
||||
// auth.js를 사용하여 인증 상태 확인
|
||||
const user = getUser();
|
||||
if (!user) {
|
||||
showError('로그인이 필요합니다. 2초 후 로그인 페이지로 이동합니다.');
|
||||
setTimeout(() => window.location.href = '/index.html', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showMessage('시스템을 초기화하는 중...', 'loading');
|
||||
|
||||
// 기본 설정
|
||||
setupEventListeners();
|
||||
setTodayDate();
|
||||
|
||||
// 마스터 데이터 로드
|
||||
await loadMasterData();
|
||||
|
||||
// 권한 표시
|
||||
displayUserPermission(permission);
|
||||
|
||||
hideMessage();
|
||||
console.log('✅ 초기화 완료!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 초기화 실패:', error);
|
||||
showError(`초기화 오류: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('searchBtn')?.addEventListener('click', searchReports);
|
||||
document.getElementById('todayBtn')?.addEventListener('click', setTodayDate);
|
||||
document.getElementById('reportDate')?.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchReports();
|
||||
}
|
||||
});
|
||||
document.getElementById('exportExcelBtn')?.addEventListener('click', exportToExcel);
|
||||
document.getElementById('printBtn')?.addEventListener('click', printReport);
|
||||
}
|
||||
setupEventListeners();
|
||||
|
||||
function setTodayDate() {
|
||||
const today = getKoreaToday();
|
||||
// 페이지 로드 시 오늘 날짜로 자동 검색
|
||||
const dateInput = document.getElementById('reportDate');
|
||||
|
||||
if (dateInput) {
|
||||
dateInput.value = today;
|
||||
searchReports();
|
||||
}
|
||||
dateInput.value = new Date().toISOString().split('T')[0];
|
||||
searchReports();
|
||||
}
|
||||
|
||||
// 권한 표시 함수 (더 상세하게)
|
||||
function displayUserPermission(permission) {
|
||||
// 권한 정보를 UI에 표시
|
||||
const headerElement = document.querySelector('h1');
|
||||
if (headerElement) {
|
||||
headerElement.innerHTML += ` <small style="color: #666; font-size: 0.6em;">(${permission.description})</small>`;
|
||||
}
|
||||
|
||||
console.log(`🔐 현재 권한: ${permission.description}`);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 📊 마스터 데이터 로드 (통합 API 사용)
|
||||
// =================================================================
|
||||
async function loadMasterData() {
|
||||
try {
|
||||
console.log('📋 마스터 데이터 로딩...');
|
||||
|
||||
await loadWorkTypes();
|
||||
await loadWorkStatusTypes();
|
||||
await loadErrorTypes();
|
||||
|
||||
console.log('✅ 마스터 데이터 로드 완료');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 마스터 데이터 로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkTypes() {
|
||||
try {
|
||||
const data = await apiCall(`${API}/daily-work-reports/work-types`);
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
workTypes = data;
|
||||
return;
|
||||
}
|
||||
throw new Error('API 실패');
|
||||
} catch (error) {
|
||||
workTypes = [
|
||||
{id: 1, name: 'Base'},
|
||||
{id: 2, name: 'Vessel'},
|
||||
{id: 3, name: 'Piping'}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkStatusTypes() {
|
||||
try {
|
||||
const data = await apiCall(`${API}/daily-work-reports/work-status-types`);
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
workStatusTypes = data;
|
||||
return;
|
||||
}
|
||||
throw new Error('API 실패');
|
||||
} catch (error) {
|
||||
workStatusTypes = [
|
||||
{id: 1, name: '정규'},
|
||||
{id: 2, name: '에러'}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadErrorTypes() {
|
||||
try {
|
||||
const data = await apiCall(`${API}/daily-work-reports/error-types`);
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
errorTypes = data;
|
||||
return;
|
||||
}
|
||||
throw new Error('API 실패');
|
||||
} catch (error) {
|
||||
errorTypes = [
|
||||
{id: 1, name: '설계미스'},
|
||||
{id: 2, name: '외주작업 불량'},
|
||||
{id: 3, name: '입고지연'},
|
||||
{id: 4, name: '작업 불량'}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 🔍 스마트 권한별 데이터 조회 시스템 (통합 API 사용)
|
||||
// =================================================================
|
||||
async function searchReports() {
|
||||
const selectedDate = document.getElementById('reportDate')?.value;
|
||||
|
||||
if (!selectedDate) {
|
||||
showError('날짜를 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n🔍 ===== ${selectedDate} 스마트 권한별 조회 시작 =====`);
|
||||
|
||||
try {
|
||||
hideAllMessages();
|
||||
showLoading(true);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
const permission = checkUserPermission(currentUser);
|
||||
|
||||
console.log('🔐 권한 확인:', permission);
|
||||
|
||||
let data = [];
|
||||
let queryMethod = '';
|
||||
|
||||
if (permission.canViewAll) {
|
||||
// 🌍 관리자/시스템: 전체 데이터 조회 시도 → 실패 시 본인 데이터로 폴백
|
||||
console.log('🌍 관리자 권한으로 전체 데이터 조회 시도');
|
||||
data = await fetchAllDataWithFallback(selectedDate, currentUser);
|
||||
queryMethod = '관리자 권한 (폴백 포함)';
|
||||
} else {
|
||||
// 🔒 일반 사용자/그룹장: 처음부터 본인 데이터만 조회
|
||||
console.log('🔒 제한 권한으로 본인 데이터만 조회');
|
||||
data = await fetchMyData(selectedDate, currentUser);
|
||||
queryMethod = '제한 권한 (본인 데이터만)';
|
||||
}
|
||||
|
||||
console.log(`📊 최종 조회된 데이터: ${data.length}개`);
|
||||
|
||||
if (data.length > 0) {
|
||||
const processedData = processRawData(data, selectedDate);
|
||||
currentReportData = processedData;
|
||||
displayReportData(processedData);
|
||||
showExportSection(true);
|
||||
|
||||
showMessage(`${queryMethod}으로 ${data.length}개 데이터를 표시했습니다.`, 'success');
|
||||
} else {
|
||||
const helpMessage = permission.canViewAll ?
|
||||
'전체 조회 및 본인 데이터 조회 모두 실패했습니다.' :
|
||||
'해당 날짜에 본인이 입력한 데이터가 없습니다.';
|
||||
|
||||
showNoDataWithHelp(selectedDate, helpMessage);
|
||||
showExportSection(false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 조회 오류:', error);
|
||||
showError(`데이터 조회 오류: ${error.message}`);
|
||||
showExportSection(false);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
console.log('🔍 ===== 조회 완료 =====\n');
|
||||
}
|
||||
}
|
||||
|
||||
// 전체 데이터 조회 + 본인 데이터 폴백 (시스템/관리자용) - 통합 API 사용
|
||||
async function fetchAllDataWithFallback(selectedDate, currentUser) {
|
||||
console.log('📡 전체 데이터 조회 시도 (폴백 지원)');
|
||||
|
||||
// 1단계: 전체 데이터 조회 시도
|
||||
const allData = await fetchAllData(selectedDate);
|
||||
if (allData.length > 0) {
|
||||
console.log(`✅ 전체 데이터 조회 성공: ${allData.length}개`);
|
||||
return allData;
|
||||
}
|
||||
|
||||
// 2단계: 전체 조회 실패 시 본인 데이터로 폴백
|
||||
console.log('⚠️ 전체 조회 실패, 본인 데이터로 폴백');
|
||||
const myData = await fetchMyData(selectedDate, currentUser);
|
||||
if (myData.length > 0) {
|
||||
console.log(`✅ 폴백 성공: 본인 데이터 ${myData.length}개`);
|
||||
showMessage('⚠️ 전체 조회 권한이 없어 본인 입력 데이터만 표시합니다.', 'warning');
|
||||
return myData;
|
||||
}
|
||||
|
||||
console.log('❌ 전체 조회 및 폴백 모두 실패');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 전체 데이터 조회 (시스템/관리자용) - 통합 API 사용
|
||||
async function fetchAllData(selectedDate) {
|
||||
console.log('📡 전체 데이터 API 호출');
|
||||
|
||||
// 여러 방법으로 시도
|
||||
const endpoints = [
|
||||
`/daily-work-reports?date=${selectedDate}`,
|
||||
`/daily-work-reports/date/${selectedDate}`
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
console.log(`🔍 시도: ${API}${endpoint}`);
|
||||
|
||||
const rawData = await apiCall(`${API}${endpoint}`);
|
||||
let data = Array.isArray(rawData) ? rawData : (rawData?.data || []);
|
||||
|
||||
if (data.length > 0) {
|
||||
console.log(`✅ 전체 조회 성공: ${data.length}개 데이터`);
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ 오류: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('❌ 모든 전체 조회 방법 실패');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 본인 데이터 조회 (모든 사용자 공통) - 통합 API 사용
|
||||
async function fetchMyData(selectedDate, currentUser) {
|
||||
console.log('📡 본인 데이터 API 호출');
|
||||
|
||||
if (!currentUser?.user_id && !currentUser?.id) {
|
||||
console.error('❌ 사용자 ID가 없습니다');
|
||||
return [];
|
||||
}
|
||||
|
||||
const userId = currentUser.user_id || currentUser.id;
|
||||
|
||||
console.log(`🔍 본인 데이터 URL: ${API}/daily-work-reports?date=${selectedDate}&created_by=${userId}`);
|
||||
|
||||
try {
|
||||
const rawData = await apiCall(`${API}/daily-work-reports?date=${selectedDate}&created_by=${userId}`);
|
||||
let data = Array.isArray(rawData) ? rawData : (rawData?.data || []);
|
||||
|
||||
console.log(`✅ 본인 데이터: ${data.length}개`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('❌ 본인 데이터 조회 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 원시 데이터를 구조화된 형태로 변환
|
||||
function processRawData(rawData, selectedDate) {
|
||||
console.log('🔄 데이터 구조 변환 시작');
|
||||
|
||||
if (!Array.isArray(rawData) || rawData.length === 0) {
|
||||
return {
|
||||
summary: {
|
||||
date: selectedDate,
|
||||
total_workers: 0,
|
||||
total_hours: 0,
|
||||
total_entries: 0,
|
||||
error_count: 0
|
||||
},
|
||||
workers: []
|
||||
};
|
||||
}
|
||||
|
||||
// 작업자별로 그룹화
|
||||
const workerGroups = {};
|
||||
let totalHours = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
rawData.forEach(item => {
|
||||
const workerName = item.worker_name || '미지정';
|
||||
const workHours = parseFloat(item.work_hours || 0);
|
||||
totalHours += workHours;
|
||||
|
||||
if (item.work_status_id === 2) {
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
if (!workerGroups[workerName]) {
|
||||
workerGroups[workerName] = {
|
||||
worker_name: workerName,
|
||||
worker_id: item.worker_id,
|
||||
total_hours: 0,
|
||||
work_entries: []
|
||||
};
|
||||
}
|
||||
|
||||
workerGroups[workerName].total_hours += workHours;
|
||||
workerGroups[workerName].work_entries.push({
|
||||
project_name: item.project_name,
|
||||
work_type_name: item.work_type_name,
|
||||
work_status_name: item.work_status_name,
|
||||
error_type_name: item.error_type_name,
|
||||
work_hours: workHours,
|
||||
work_status_id: item.work_status_id,
|
||||
created_by_name: item.created_by_name || '입력자 미지정'
|
||||
});
|
||||
});
|
||||
|
||||
const processedData = {
|
||||
summary: {
|
||||
date: selectedDate,
|
||||
total_workers: Object.keys(workerGroups).length,
|
||||
total_hours: totalHours,
|
||||
total_entries: rawData.length,
|
||||
error_count: errorCount
|
||||
},
|
||||
workers: Object.values(workerGroups)
|
||||
};
|
||||
|
||||
console.log('✅ 데이터 변환 완료:', {
|
||||
작업자수: processedData.workers.length,
|
||||
총항목수: rawData.length,
|
||||
총시간: totalHours,
|
||||
에러수: errorCount
|
||||
});
|
||||
|
||||
return processedData;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 🎨 UI 표시 함수들 (기존과 동일)
|
||||
// =================================================================
|
||||
function displayReportData(data) {
|
||||
console.log('🎨 리포트 데이터 표시');
|
||||
|
||||
displaySummary(data.summary);
|
||||
displayWorkersDetails(data.workers);
|
||||
|
||||
document.getElementById('reportSummary').style.display = 'block';
|
||||
document.getElementById('workersReport').style.display = 'block';
|
||||
}
|
||||
|
||||
function displaySummary(summary) {
|
||||
const elements = {
|
||||
totalWorkers: summary?.total_workers || 0,
|
||||
totalHours: `${summary?.total_hours || 0}시간`,
|
||||
totalEntries: `${summary?.total_entries || 0}개`,
|
||||
errorCount: `${summary?.error_count || 0}개`
|
||||
};
|
||||
|
||||
Object.entries(elements).forEach(([id, value]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) element.textContent = value;
|
||||
});
|
||||
|
||||
// 에러 카드 스타일링
|
||||
const errorCard = document.querySelector('.summary-card.error-card');
|
||||
if (errorCard) {
|
||||
const hasErrors = (summary?.error_count || 0) > 0;
|
||||
errorCard.style.borderLeftColor = hasErrors ? '#e74c3c' : '#28a745';
|
||||
errorCard.style.backgroundColor = hasErrors ? '#fff5f5' : '#f8fff9';
|
||||
}
|
||||
}
|
||||
|
||||
function displayWorkersDetails(workers) {
|
||||
const workersList = document.getElementById('workersList');
|
||||
if (!workersList) return;
|
||||
|
||||
workersList.innerHTML = '';
|
||||
|
||||
workers.forEach(worker => {
|
||||
const workerCard = createWorkerCard(worker);
|
||||
workersList.appendChild(workerCard);
|
||||
});
|
||||
}
|
||||
|
||||
function createWorkerCard(worker) {
|
||||
const workerDiv = document.createElement('div');
|
||||
workerDiv.className = 'worker-card';
|
||||
|
||||
const workerHeader = document.createElement('div');
|
||||
workerHeader.className = 'worker-header';
|
||||
workerHeader.innerHTML = `
|
||||
<div class="worker-name">👤 ${worker.worker_name || '미지정'}</div>
|
||||
<div class="worker-total-hours">총 ${worker.total_hours || 0}시간</div>
|
||||
`;
|
||||
|
||||
const workEntries = document.createElement('div');
|
||||
workEntries.className = 'work-entries';
|
||||
|
||||
if (worker.work_entries && Array.isArray(worker.work_entries)) {
|
||||
worker.work_entries.forEach(entry => {
|
||||
const entryDiv = createWorkEntryCard(entry);
|
||||
workEntries.appendChild(entryDiv);
|
||||
});
|
||||
}
|
||||
|
||||
workerDiv.appendChild(workerHeader);
|
||||
workerDiv.appendChild(workEntries);
|
||||
|
||||
return workerDiv;
|
||||
}
|
||||
|
||||
function createWorkEntryCard(entry) {
|
||||
const entryDiv = document.createElement('div');
|
||||
entryDiv.className = 'work-entry';
|
||||
|
||||
if (entry.work_status_id === 2) {
|
||||
entryDiv.classList.add('error-entry');
|
||||
}
|
||||
|
||||
const entryHeader = document.createElement('div');
|
||||
entryHeader.className = 'entry-header';
|
||||
entryHeader.innerHTML = `
|
||||
<div class="project-name">${entry.project_name || '프로젝트 미지정'}</div>
|
||||
<div class="work-hours">${entry.work_hours || 0}시간</div>
|
||||
`;
|
||||
|
||||
const entryDetails = document.createElement('div');
|
||||
entryDetails.className = 'entry-details';
|
||||
|
||||
const details = [
|
||||
['작업 유형', entry.work_type_name || '-'],
|
||||
['작업 상태', entry.work_status_name || '정상'],
|
||||
['입력자', entry.created_by_name || '미지정']
|
||||
];
|
||||
|
||||
if (entry.work_status_id === 2 && entry.error_type_name) {
|
||||
details.push(['에러 유형', entry.error_type_name, 'error-type']);
|
||||
}
|
||||
|
||||
details.forEach(([label, value, valueClass]) => {
|
||||
const detailRow = createDetailRow(label, value, valueClass);
|
||||
entryDetails.appendChild(detailRow);
|
||||
});
|
||||
|
||||
entryDiv.appendChild(entryHeader);
|
||||
entryDiv.appendChild(entryDetails);
|
||||
|
||||
return entryDiv;
|
||||
}
|
||||
|
||||
function createDetailRow(label, value, valueClass = '') {
|
||||
const detailDiv = document.createElement('div');
|
||||
detailDiv.className = 'entry-detail';
|
||||
detailDiv.innerHTML = `
|
||||
<span class="detail-label">${label}:</span>
|
||||
<span class="detail-value ${valueClass}">${value}</span>
|
||||
`;
|
||||
return detailDiv;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 🎭 UI 상태 관리
|
||||
// =================================================================
|
||||
function showLoading(show) {
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
if (spinner) {
|
||||
spinner.style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('errorMessage');
|
||||
if (errorDiv) {
|
||||
const errorText = errorDiv.querySelector('.error-text');
|
||||
if (errorText) errorText.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function showMessage(message, type = 'info') {
|
||||
const messageContainer = document.getElementById('message-container');
|
||||
if (messageContainer) {
|
||||
messageContainer.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||||
|
||||
if (type === 'success' || type === 'info') {
|
||||
setTimeout(() => {
|
||||
messageContainer.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
} else {
|
||||
console.log(`📢 ${type.toUpperCase()}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function hideMessage() {
|
||||
const messageContainer = document.getElementById('message-container');
|
||||
if (messageContainer) {
|
||||
messageContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function showNoDataWithHelp(selectedDate, helpMessage = '해당 날짜에 데이터가 없습니다.') {
|
||||
const noDataDiv = document.getElementById('noDataMessage');
|
||||
if (noDataDiv) {
|
||||
noDataDiv.innerHTML = `
|
||||
<div class="no-data-content">
|
||||
<span class="no-data-icon">📭</span>
|
||||
<h3>${selectedDate} 작업보고서가 없습니다</h3>
|
||||
<div class="help-section">
|
||||
<p><strong>💡 ${helpMessage}</strong></p>
|
||||
<ul style="text-align: left; margin: 10px 0;">
|
||||
<li>다른 날짜를 선택해보세요 (예: ${getKoreaToday()})</li>
|
||||
<li><a href="/pages/common/daily-work-report.html" target="_blank" style="color: #3498db;">📝 작업보고서 입력 페이지</a>에서 데이터를 먼저 입력해보세요</li>
|
||||
<li>입력 후 잠시 기다린 다음 다시 시도해보세요</li>
|
||||
</ul>
|
||||
<p style="margin-top: 15px;">
|
||||
<button onclick="window.location.reload()" style="padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
🔄 새로고침
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
noDataDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function showExportSection(show) {
|
||||
const exportSection = document.getElementById('exportSection');
|
||||
if (exportSection) {
|
||||
exportSection.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function hideAllMessages() {
|
||||
const elements = [
|
||||
'errorMessage',
|
||||
'noDataMessage',
|
||||
'reportSummary',
|
||||
'workersReport'
|
||||
];
|
||||
|
||||
elements.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) element.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 📤 내보내기 기능
|
||||
// =================================================================
|
||||
function exportToExcel() {
|
||||
if (!currentReportData?.workers?.length) {
|
||||
alert('내보낼 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📊 Excel 내보내기 시작');
|
||||
|
||||
try {
|
||||
let csvContent = "\uFEFF작업자명,프로젝트명,작업유형,작업상태,에러유형,작업시간,입력자\n";
|
||||
|
||||
currentReportData.workers.forEach(worker => {
|
||||
if (worker.work_entries && Array.isArray(worker.work_entries)) {
|
||||
worker.work_entries.forEach(entry => {
|
||||
const row = [
|
||||
worker.worker_name || '',
|
||||
entry.project_name || '',
|
||||
entry.work_type_name || '',
|
||||
entry.work_status_name || '',
|
||||
entry.error_type_name || '',
|
||||
entry.work_hours || 0,
|
||||
entry.created_by_name || ''
|
||||
].map(field => `"${String(field).replace(/"/g, '""')}"`).join(',');
|
||||
|
||||
csvContent += row + "\n";
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const fileName = `작업보고서_${currentReportData.summary?.date || '날짜미지정'}.csv`;
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', fileName);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('✅ Excel 내보내기 완료');
|
||||
showMessage('Excel 파일이 다운로드되었습니다.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Excel 내보내기 실패:', error);
|
||||
showError('Excel 내보내기 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
function printReport() {
|
||||
console.log('🖨️ 인쇄 시작');
|
||||
|
||||
if (!currentReportData?.workers?.length) {
|
||||
alert('인쇄할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.print();
|
||||
console.log('✅ 인쇄 대화상자 표시');
|
||||
} catch (error) {
|
||||
console.error('❌ 인쇄 실패:', error);
|
||||
showError('인쇄 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 🔄 전역 함수 및 디버깅
|
||||
// =================================================================
|
||||
|
||||
// 개발 모드 디버깅
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
console.log('🐛 개발 모드 활성화');
|
||||
|
||||
window.DEBUG = {
|
||||
currentReportData,
|
||||
getCurrentUser,
|
||||
checkUserPermission,
|
||||
fetchAllData,
|
||||
fetchMyData,
|
||||
fetchAllDataWithFallback,
|
||||
searchReports
|
||||
};
|
||||
}
|
||||
|
||||
// 전역 함수 노출
|
||||
window.searchReports = searchReports;
|
||||
window.exportToExcel = exportToExcel;
|
||||
window.printReport = printReport;
|
||||
|
||||
// 페이지 정리
|
||||
window.addEventListener('beforeunload', function() {
|
||||
console.log('📋 페이지 종료');
|
||||
});
|
||||
// DOM이 로드되면 페이지 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
@@ -1,252 +1,144 @@
|
||||
// js/load-navbar.js
|
||||
// 네비게이션바 로드 및 프로필 드롭다운 기능 구현
|
||||
import { getUser, clearAuthData } from './auth.js';
|
||||
|
||||
// 역할 이름을 한글로 변환하는 맵
|
||||
const ROLE_NAMES = {
|
||||
admin: '관리자',
|
||||
system: '시스템 관리자',
|
||||
leader: '그룹장',
|
||||
user: '작업자',
|
||||
support: '지원팀',
|
||||
default: '사용자',
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 역할에 따라 메뉴 항목을 필터링합니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {string} userRole - 현재 사용자의 역할
|
||||
*/
|
||||
function filterMenuByRole(doc, userRole) {
|
||||
const selectors = [
|
||||
{ role: 'admin', selector: '.admin-only' },
|
||||
{ role: 'system', selector: '.system-only' },
|
||||
{ role: 'leader', selector: '.leader-only' },
|
||||
];
|
||||
|
||||
selectors.forEach(({ role, selector }) => {
|
||||
// 사용자가 해당 역할을 가지고 있지 않으면 메뉴 항목을 제거
|
||||
if (userRole !== role) {
|
||||
doc.querySelectorAll(selector).forEach(el => el.remove());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 바에 사용자 정보를 채웁니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {object} user - 현재 사용자 객체
|
||||
*/
|
||||
function populateUserInfo(doc, user) {
|
||||
const displayName = user.name || user.username;
|
||||
const roleName = ROLE_NAMES[user.role] || ROLE_NAMES.default;
|
||||
|
||||
// 상단 바 사용자 이름
|
||||
const userNameEl = doc.getElementById('user-name');
|
||||
if (userNameEl) userNameEl.textContent = displayName;
|
||||
|
||||
// 상단 바 사용자 역할
|
||||
const userRoleEl = doc.getElementById('user-role');
|
||||
if (userRoleEl) userRoleEl.textContent = roleName;
|
||||
|
||||
// 드롭다운 메뉴 사용자 이름
|
||||
const dropdownNameEl = doc.getElementById('dropdown-user-fullname');
|
||||
if (dropdownNameEl) dropdownNameEl.textContent = displayName;
|
||||
|
||||
// 드롭다운 메뉴 사용자 아이디
|
||||
const dropdownIdEl = doc.getElementById('dropdown-user-id');
|
||||
if (dropdownIdEl) dropdownIdEl.textContent = `@${user.username}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 바와 관련된 모든 이벤트를 설정합니다.
|
||||
*/
|
||||
function setupNavbarEvents() {
|
||||
const userInfoDropdown = document.getElementById('user-info-dropdown');
|
||||
const profileDropdownMenu = document.getElementById('profile-dropdown-menu');
|
||||
|
||||
// 드롭다운 토글
|
||||
if (userInfoDropdown && profileDropdownMenu) {
|
||||
userInfoDropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
profileDropdownMenu.classList.toggle('show');
|
||||
userInfoDropdown.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
|
||||
// 로그아웃 버튼
|
||||
const logoutButton = document.getElementById('dropdown-logout');
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
if (confirm('로그아웃 하시겠습니까?')) {
|
||||
clearAuthData();
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 외부 클릭 시 드롭다운 닫기
|
||||
document.addEventListener('click', (e) => {
|
||||
if (profileDropdownMenu && !userInfoDropdown.contains(e.target) && !profileDropdownMenu.contains(e.target)) {
|
||||
profileDropdownMenu.classList.remove('show');
|
||||
userInfoDropdown.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 시간을 업데이트하는 함수
|
||||
*/
|
||||
function updateTime() {
|
||||
const timeElement = document.getElementById('current-time');
|
||||
if (timeElement) {
|
||||
const now = new Date();
|
||||
timeElement.textContent = now.toLocaleTimeString('ko-KR', { hour12: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 메인 로직: DOMContentLoaded 시 실행
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
if (!navbarContainer) return;
|
||||
|
||||
const currentUser = getUser();
|
||||
if (!currentUser) return; // 사용자가 없으면 아무 작업도 하지 않음
|
||||
|
||||
try {
|
||||
// navbar.html 파일 로드
|
||||
const res = await fetch('/components/navbar.html');
|
||||
const html = await res.text();
|
||||
|
||||
// navbar 컨테이너 찾기
|
||||
const container = document.getElementById('navbar-container') || document.getElementById('navbar-placeholder');
|
||||
if (!container) {
|
||||
console.error('네비게이션 컨테이너를 찾을 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML 삽입
|
||||
container.innerHTML = html;
|
||||
const response = await fetch('/components/navbar.html');
|
||||
const htmlText = await response.text();
|
||||
|
||||
// 토큰 확인
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
// 1. 텍스트를 가상 DOM으로 파싱
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlText, 'text/html');
|
||||
|
||||
// 역할 매핑 테이블
|
||||
const roleMap = {
|
||||
worker: '작업자',
|
||||
group_leader: '그룹장',
|
||||
groupleader: '그룹장',
|
||||
leader: '리더',
|
||||
supervisor: '감독자',
|
||||
team_leader: '팀장',
|
||||
support_team: '지원팀',
|
||||
support: '지원팀',
|
||||
admin_ceo: '업무관리자',
|
||||
admin_plant: '시스템관리자',
|
||||
admin: '관리자',
|
||||
administrator: '관리자',
|
||||
system: '시스템관리자'
|
||||
};
|
||||
// 2. DOM에 삽입하기 *전*에 내용 수정
|
||||
filterMenuByRole(doc, currentUser.role);
|
||||
populateUserInfo(doc, currentUser);
|
||||
|
||||
// JWT 토큰 파싱
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(atob(token.split('.')[1]));
|
||||
} catch (err) {
|
||||
console.warn('JWT 파싱 실패:', err);
|
||||
return;
|
||||
}
|
||||
// 3. 수정 완료된 HTML을 실제 DOM에 삽입 (깜빡임 방지)
|
||||
navbarContainer.innerHTML = doc.body.innerHTML;
|
||||
|
||||
// 저장된 사용자 정보 확인
|
||||
const storedUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const currentUser = storedUser.access_level ? storedUser : payload;
|
||||
// 4. DOM에 삽입된 후에 이벤트 리스너 설정
|
||||
setupNavbarEvents();
|
||||
|
||||
// ✅ 사용자 정보 표시
|
||||
const nameEl = document.getElementById('user-name');
|
||||
if (nameEl) {
|
||||
nameEl.textContent = currentUser.name || currentUser.username || '사용자';
|
||||
}
|
||||
|
||||
const roleEl = document.getElementById('user-role');
|
||||
if (roleEl) {
|
||||
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
||||
const roleName = roleMap[accessLevel] || '사용자';
|
||||
roleEl.textContent = roleName;
|
||||
}
|
||||
|
||||
// ✅ 드롭다운 헤더 사용자 정보
|
||||
const dropdownFullname = document.getElementById('dropdown-user-fullname');
|
||||
if (dropdownFullname) {
|
||||
dropdownFullname.textContent = currentUser.name || currentUser.username || '사용자';
|
||||
}
|
||||
|
||||
const dropdownUserId = document.getElementById('dropdown-user-id');
|
||||
if (dropdownUserId) {
|
||||
dropdownUserId.textContent = `@${currentUser.username || 'user'}`;
|
||||
}
|
||||
|
||||
// ✅ 현재 시간 업데이트 시작
|
||||
// 5. 실시간 시간 업데이트 시작
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
|
||||
// ✅ 프로필 드롭다운 이벤트 설정
|
||||
const userInfoDropdown = document.getElementById('user-info-dropdown');
|
||||
const profileDropdownMenu = document.getElementById('profile-dropdown-menu');
|
||||
|
||||
if (userInfoDropdown && profileDropdownMenu) {
|
||||
// 드롭다운 토글
|
||||
userInfoDropdown.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const isOpen = profileDropdownMenu.classList.contains('show');
|
||||
|
||||
if (isOpen) {
|
||||
closeProfileDropdown();
|
||||
} else {
|
||||
openProfileDropdown();
|
||||
}
|
||||
});
|
||||
console.log('✅ 네비게이션 바 로딩 완료');
|
||||
|
||||
// 드롭다운 외부 클릭 시 닫기
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!userInfoDropdown.contains(e.target) && !profileDropdownMenu.contains(e.target)) {
|
||||
closeProfileDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// ESC 키로 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeProfileDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 대시보드 버튼 이벤트
|
||||
const dashboardBtn = document.querySelector('.dashboard-btn');
|
||||
if (dashboardBtn) {
|
||||
dashboardBtn.addEventListener('click', function() {
|
||||
navigateToDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 드롭다운 로그아웃 버튼
|
||||
const dropdownLogout = document.getElementById('dropdown-logout');
|
||||
if (dropdownLogout) {
|
||||
dropdownLogout.addEventListener('click', function() {
|
||||
logout();
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 네비게이션 바 로딩 및 이벤트 설정 완료:', {
|
||||
name: currentUser.name || currentUser.username,
|
||||
role: currentUser.access_level,
|
||||
dashboardBtn: !!dashboardBtn,
|
||||
profileDropdown: !!userInfoDropdown
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('🔴 네비게이션 바 로딩 실패:', err);
|
||||
} catch (error) {
|
||||
console.error('🔴 네비게이션 바 로딩 중 오류 발생:', error);
|
||||
navbarContainer.innerHTML = '<p>네비게이션 바를 불러오는 데 실패했습니다.</p>';
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ 프로필 드롭다운 열기
|
||||
function openProfileDropdown() {
|
||||
const userInfo = document.getElementById('user-info-dropdown');
|
||||
const dropdown = document.getElementById('profile-dropdown-menu');
|
||||
|
||||
if (userInfo && dropdown) {
|
||||
userInfo.classList.add('active');
|
||||
dropdown.classList.add('show');
|
||||
console.log('📂 프로필 드롭다운 열림');
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 프로필 드롭다운 닫기
|
||||
function closeProfileDropdown() {
|
||||
const userInfo = document.getElementById('user-info-dropdown');
|
||||
const dropdown = document.getElementById('profile-dropdown-menu');
|
||||
|
||||
if (userInfo && dropdown) {
|
||||
userInfo.classList.remove('active');
|
||||
dropdown.classList.remove('show');
|
||||
console.log('📁 프로필 드롭다운 닫힘');
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 시간 업데이트 함수
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString('ko-KR', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
const timeElement = document.getElementById('current-time');
|
||||
if (timeElement) {
|
||||
timeElement.textContent = timeString;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 역할별 대시보드 네비게이션
|
||||
function navigateToDashboard() {
|
||||
console.log('🏠 대시보드 버튼 클릭됨');
|
||||
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const accessLevel = (user.access_level || '').toLowerCase().trim();
|
||||
|
||||
console.log('👤 현재 사용자:', user);
|
||||
console.log('🔑 access_level:', accessLevel);
|
||||
|
||||
// 그룹장/리더 관련 키워드들
|
||||
const leaderKeywords = [
|
||||
'group_leader', 'groupleader', 'group-leader',
|
||||
'leader', 'supervisor', 'team_leader', 'teamleader',
|
||||
'그룹장', '팀장', '현장책임자'
|
||||
];
|
||||
|
||||
// 관리자 관련 키워드들
|
||||
const adminKeywords = [
|
||||
'admin', 'administrator', 'system',
|
||||
'관리자', '시스템관리자'
|
||||
];
|
||||
|
||||
// 지원팀 관련 키워드들
|
||||
const supportKeywords = [
|
||||
'support', 'support_team', 'supportteam',
|
||||
'지원팀', '지원'
|
||||
];
|
||||
|
||||
let targetUrl = '/pages/dashboard/user.html';
|
||||
|
||||
// 키워드 매칭
|
||||
if (leaderKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
|
||||
targetUrl = '/pages/dashboard/group-leader.html';
|
||||
console.log('✅ 그룹장 페이지로 이동');
|
||||
} else if (adminKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
|
||||
targetUrl = '/pages/dashboard/admin.html';
|
||||
console.log('✅ 관리자 페이지로 이동');
|
||||
} else if (supportKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
|
||||
targetUrl = '/pages/dashboard/support.html';
|
||||
console.log('✅ 지원팀 페이지로 이동');
|
||||
} else {
|
||||
console.log('✅ 일반 사용자 페이지로 이동');
|
||||
}
|
||||
|
||||
console.log('🎯 이동할 URL:', targetUrl);
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
|
||||
// ✅ 로그아웃 함수
|
||||
function logout() {
|
||||
console.log('🚪 로그아웃 버튼 클릭됨');
|
||||
|
||||
if (confirm('로그아웃 하시겠습니까?')) {
|
||||
console.log('✅ 로그아웃 확인됨');
|
||||
|
||||
// 로컬 스토리지 정리
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
|
||||
console.log('🗑️ 로컬 스토리지 정리 완료');
|
||||
|
||||
// 부드러운 전환 효과
|
||||
document.body.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
console.log('🏠 로그인 페이지로 이동');
|
||||
window.location.href = '/index.html';
|
||||
}, 300);
|
||||
} else {
|
||||
console.log('❌ 로그아웃 취소됨');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,187 +1,104 @@
|
||||
// ✅ /js/load-sections.js - 확장 가능한 구조 (개선됨)
|
||||
import { API, getAuthHeaders } from '/js/api-config.js';
|
||||
// /js/load-sections.js
|
||||
import { getUser } from './auth.js';
|
||||
import { apiGet } from './api-helper.js';
|
||||
|
||||
// 역할별 섹션 매핑 (쉽게 추가/수정 가능)
|
||||
// 역할에 따라 불러올 섹션 HTML 파일을 매핑합니다.
|
||||
const SECTION_MAP = {
|
||||
'admin': '/components/sections/admin-sections.html',
|
||||
'system': '/components/sections/admin-sections.html',
|
||||
'leader': '/components/sections/leader-sections.html',
|
||||
'group_leader': '/components/sections/leader-sections.html',
|
||||
'support': '/components/sections/support-sections.html',
|
||||
'support_team': '/components/sections/support-sections.html',
|
||||
'user': '/components/sections/user-sections.html',
|
||||
'worker': '/components/sections/user-sections.html'
|
||||
admin: '/components/sections/admin-sections.html',
|
||||
system: '/components/sections/admin-sections.html', // system도 admin과 동일한 섹션을 사용
|
||||
leader: '/components/sections/leader-sections.html',
|
||||
user: '/components/sections/user-sections.html',
|
||||
default: '/components/sections/user-sections.html', // 역할이 없는 경우 기본값
|
||||
};
|
||||
|
||||
// 공통 섹션 (모든 사용자에게 표시)
|
||||
const COMMON_SECTIONS = '/components/sections/common-sections.html';
|
||||
|
||||
async function loadSections() {
|
||||
/**
|
||||
* API를 통해 대시보드 통계 데이터를 가져옵니다.
|
||||
* @returns {Promise<object|null>} 통계 데이터 또는 에러 시 null
|
||||
*/
|
||||
async function fetchDashboardStats() {
|
||||
try {
|
||||
console.log('🔄 섹션 로딩 시작');
|
||||
|
||||
// 사용자 정보 확인
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.log('❌ 토큰 없음, 로그인 페이지로 이동');
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
let userInfo = { role: 'user', access_level: 'worker' };
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
userInfo = {
|
||||
role: payload.role || 'user',
|
||||
access_level: payload.access_level || 'worker'
|
||||
};
|
||||
console.log('👤 사용자 정보:', userInfo);
|
||||
} catch (err) {
|
||||
console.warn('⚠️ JWT 파싱 실패:', err);
|
||||
}
|
||||
|
||||
// ✅ 컨테이너 찾기 - 더 안전한 방식
|
||||
const possibleContainers = [
|
||||
'#sections-container',
|
||||
'#admin-sections',
|
||||
'#user-sections',
|
||||
'main[id$="-sections"]',
|
||||
'#content-container main'
|
||||
];
|
||||
|
||||
let container = null;
|
||||
for (const selector of possibleContainers) {
|
||||
container = document.querySelector(selector);
|
||||
if (container) {
|
||||
console.log(`✅ 컨테이너 발견: ${selector}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
console.error('❌ 섹션 컨테이너를 찾을 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="loading">콘텐츠를 불러오는 중...</div>';
|
||||
|
||||
// 역할별 섹션 파일 결정 (수정된 버전)
|
||||
console.log('🔍 사용자 정보 디버깅:');
|
||||
console.log('- userInfo.role:', userInfo.role);
|
||||
console.log('- userInfo.access_level:', userInfo.access_level);
|
||||
|
||||
// role이 없으므로 access_level을 우선 사용
|
||||
const effectiveRole = userInfo.access_level || userInfo.role || 'user';
|
||||
const sectionFile = SECTION_MAP[effectiveRole] || SECTION_MAP['user'];
|
||||
|
||||
console.log(`📄 실제 사용될 역할: ${effectiveRole}`);
|
||||
console.log(`📄 로딩할 섹션 파일: ${sectionFile}`);
|
||||
|
||||
try {
|
||||
// 1. 공통 섹션 로드 (있을 경우)
|
||||
let commonHtml = '';
|
||||
try {
|
||||
console.log('📄 공통 섹션 로딩 시도');
|
||||
const commonRes = await fetch(COMMON_SECTIONS);
|
||||
if (commonRes.ok) {
|
||||
commonHtml = await commonRes.text();
|
||||
console.log('✅ 공통 섹션 로딩 성공');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('ℹ️ 공통 섹션 없음 (정상)');
|
||||
}
|
||||
|
||||
// 2. 역할별 섹션 로드
|
||||
console.log('📄 역할별 섹션 로딩 시도');
|
||||
const res = await fetch(sectionFile);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: 섹션 파일을 찾을 수 없습니다 (${sectionFile})`);
|
||||
}
|
||||
|
||||
const roleHtml = await res.text();
|
||||
console.log('✅ 역할별 섹션 로딩 성공');
|
||||
|
||||
// 3. 조합하여 표시
|
||||
container.innerHTML = commonHtml + roleHtml;
|
||||
console.log('✅ 섹션 HTML 렌더링 완료');
|
||||
|
||||
// 4. 추가 데이터 로드 (필요시)
|
||||
await loadDynamicData(userInfo);
|
||||
console.log('✅ 섹션 로딩 완료');
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ 섹션 로드 실패:', err);
|
||||
container.innerHTML = `
|
||||
<div class="error-state">
|
||||
<h3>❌ 콘텐츠를 불러올 수 없습니다</h3>
|
||||
<p>오류: ${err.message}</p>
|
||||
<p>잠시 후 다시 시도해주세요.</p>
|
||||
<button onclick="location.reload()" class="btn btn-primary">🔄 새로고침</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('🔴 섹션 로딩 실패:', err);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
// 실제 백엔드 엔드포인트는 /api/dashboard/stats 와 같은 형태로 구현될 수 있습니다.
|
||||
const stats = await apiGet(`/workreports?start=${today}&end=${today}`);
|
||||
// 필요한 데이터 형태로 가공 (예시)
|
||||
return {
|
||||
today_reports_count: stats.length,
|
||||
today_workers_count: new Set(stats.map(d => d.worker_id)).size,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('대시보드 통계 데이터 로드 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 동적 데이터 로드 (예: 대시보드 통계)
|
||||
async function loadDynamicData(userInfo) {
|
||||
console.log('📊 동적 데이터 로딩 시작');
|
||||
/**
|
||||
* 가상 DOM에 통계 데이터를 채워 넣습니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {object} stats - 통계 데이터
|
||||
*/
|
||||
function populateStatsData(doc, stats) {
|
||||
if (!stats) return;
|
||||
|
||||
const todayStatsEl = doc.getElementById('today-stats');
|
||||
if (todayStatsEl) {
|
||||
todayStatsEl.innerHTML = `
|
||||
<p>📝 오늘 등록된 작업: ${stats.today_reports_count}건</p>
|
||||
<p>👥 참여 작업자: ${stats.today_workers_count}명</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 로직: 페이지에 역할별 섹션을 로드하고 내용을 채웁니다.
|
||||
*/
|
||||
async function initializeSections() {
|
||||
const mainContainer = document.querySelector('main[id$="-sections"]');
|
||||
if (!mainContainer) {
|
||||
console.error('섹션을 담을 메인 컨테이너를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
mainContainer.innerHTML = '<div class="loading">콘텐츠를 불러오는 중...</div>';
|
||||
|
||||
const currentUser = getUser();
|
||||
if (!currentUser) {
|
||||
mainContainer.innerHTML = '<div class="error-state">사용자 정보를 찾을 수 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 오늘의 작업 현황
|
||||
const todayStats = document.getElementById('today-stats');
|
||||
if (todayStats) {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const res = await fetch(`${API}/workreports?start=${today}&end=${today}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
todayStats.innerHTML = `
|
||||
<p>📝 오늘 등록된 작업: ${data.length}건</p>
|
||||
<p>👥 참여 작업자: ${new Set(data.map(d => d.worker_id)).size}명</p>
|
||||
`;
|
||||
console.log('✅ 오늘 통계 로딩 완료');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ 통계 로드 실패:', e);
|
||||
if (todayStats) {
|
||||
todayStats.innerHTML = '<p>⚠️ 통계를 불러올 수 없습니다</p>';
|
||||
}
|
||||
const sectionFile = SECTION_MAP[currentUser.role] || SECTION_MAP.default;
|
||||
|
||||
try {
|
||||
// 1. 역할에 맞는 HTML 템플릿과 동적 데이터를 동시에 로드 (Promise.all 활용)
|
||||
const [htmlResponse, statsData] = await Promise.all([
|
||||
fetch(sectionFile),
|
||||
fetchDashboardStats()
|
||||
]);
|
||||
|
||||
if (!htmlResponse.ok) {
|
||||
throw new Error(`섹션 파일(${sectionFile})을 불러오는 데 실패했습니다.`);
|
||||
}
|
||||
}
|
||||
const htmlText = await htmlResponse.text();
|
||||
|
||||
// 빠른 링크 활성화
|
||||
initializeQuickLinks(userInfo);
|
||||
// 2. 텍스트를 가상 DOM으로 파싱
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlText, 'text/html');
|
||||
|
||||
// 3. (필요 시) 역할 기반으로 가상 DOM 필터링 - 현재는 파일 자체가 역할별로 나뉘어 불필요
|
||||
// filterByRole(doc, currentUser.role);
|
||||
|
||||
// 4. 가상 DOM에 동적 데이터 채우기
|
||||
populateStatsData(doc, statsData);
|
||||
|
||||
// 5. 모든 수정이 완료된 HTML을 실제 DOM에 한 번에 삽입
|
||||
mainContainer.innerHTML = doc.body.innerHTML;
|
||||
|
||||
console.log(`✅ ${currentUser.role} 역할의 섹션 로딩 완료.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('섹션 로딩 중 오류 발생:', error);
|
||||
mainContainer.innerHTML = `<div class="error-state">콘텐츠 로딩에 실패했습니다: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 권한별 빠른 링크 표시/숨김
|
||||
function initializeQuickLinks(userInfo) {
|
||||
console.log('🔗 빠른 링크 초기화');
|
||||
|
||||
// 권한에 따라 특정 링크 숨기기
|
||||
if (userInfo.role !== 'admin' && userInfo.access_level !== 'admin') {
|
||||
document.querySelectorAll('.admin-only').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
console.log('🔒 관리자 전용 링크 숨김');
|
||||
});
|
||||
}
|
||||
|
||||
if (userInfo.access_level !== 'group_leader') {
|
||||
document.querySelectorAll('.leader-only').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
console.log('🔒 그룹장 전용 링크 숨김');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 빠른 링크 초기화 완료');
|
||||
}
|
||||
|
||||
// 페이지 로드 시 실행
|
||||
document.addEventListener('DOMContentLoaded', loadSections);
|
||||
|
||||
// 수동 새로고침 함수 (다른 곳에서 호출 가능)
|
||||
window.refreshSections = loadSections;
|
||||
// DOM이 로드되면 섹션 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializeSections);
|
||||
@@ -1,46 +1,67 @@
|
||||
// ✅ /js/load-sidebar.js (access_level 기반 메뉴 필터링)
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
// 1) 사이드바 HTML 로딩
|
||||
const res = await fetch('/components/sidebar.html');
|
||||
const html = await res.text();
|
||||
document.getElementById('sidebar-container').innerHTML = html;
|
||||
// /js/load-sidebar.js
|
||||
import { getUser } from './auth.js';
|
||||
|
||||
// 2) 토큰 존재 확인
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
/**
|
||||
* 사용자 역할에 따라 사이드바 메뉴 항목을 필터링합니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {string} userRole - 현재 사용자의 역할
|
||||
*/
|
||||
function filterSidebarByRole(doc, userRole) {
|
||||
// 'system' 역할은 모든 메뉴를 볼 수 있으므로 필터링하지 않음
|
||||
if (userRole === 'system') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 역할과 그에 해당하는 클래스 선택자 매핑
|
||||
const roleClassMap = {
|
||||
admin: '.admin-only',
|
||||
leader: '.leader-only',
|
||||
user: '.user-only', // 또는 'worker-only' 등, sidebar.html에 정의된 클래스에 맞춰야 함
|
||||
support: '.support-only'
|
||||
};
|
||||
|
||||
// 3) JWT 파싱해서 access_level 추출
|
||||
let access;
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
access = payload.access_level;
|
||||
} catch (err) {
|
||||
console.warn('JWT 파싱 실패:', err);
|
||||
return;
|
||||
// 모든 역할 기반 선택자를 가져옴
|
||||
const allRoleSelectors = Object.values(roleClassMap).join(', ');
|
||||
const allRoleElements = doc.querySelectorAll(allRoleSelectors);
|
||||
|
||||
allRoleElements.forEach(el => {
|
||||
// 요소가 현재 사용자 역할에 해당하는 클래스를 가지고 있는지 확인
|
||||
const userRoleSelector = roleClassMap[userRole];
|
||||
if (!userRoleSelector || !el.matches(userRoleSelector)) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4) 시스템 계정은 전부 유지
|
||||
if (access === 'system') return;
|
||||
|
||||
// 5) 클래스 이름 목록
|
||||
const classMap = [
|
||||
'worker-only',
|
||||
'group-leader-only',
|
||||
'support-only',
|
||||
'admin-only',
|
||||
'system-only'
|
||||
];
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const sidebarContainer = document.getElementById('sidebar-container');
|
||||
if (!sidebarContainer) return;
|
||||
|
||||
// 6) 본인 권한에 해당하지 않는 요소 제거
|
||||
classMap.forEach(cls => {
|
||||
const required = cls.replace('-only', '').replace('-', '_'); // 'group-leader-only' → 'group_leader'
|
||||
if (access !== required) {
|
||||
document.querySelectorAll(`.${cls}`).forEach(el => el.remove());
|
||||
}
|
||||
});
|
||||
const currentUser = getUser();
|
||||
if (!currentUser) return; // 비로그인 상태면 사이드바를 로드하지 않음
|
||||
|
||||
} catch (err) {
|
||||
console.error('🔴 사이드바 로딩 실패:', err);
|
||||
try {
|
||||
const response = await fetch('/components/sidebar.html');
|
||||
if (!response.ok) {
|
||||
throw new Error(`사이드바 파일을 불러올 수 없습니다: ${response.statusText}`);
|
||||
}
|
||||
const htmlText = await response.text();
|
||||
|
||||
// 1. 텍스트를 가상 DOM으로 파싱
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlText, 'text/html');
|
||||
|
||||
// 2. DOM에 삽입하기 *전*에 역할에 따라 메뉴 필터링
|
||||
filterSidebarByRole(doc, currentUser.role);
|
||||
|
||||
// 3. 수정 완료된 HTML을 실제 DOM에 삽입
|
||||
sidebarContainer.innerHTML = doc.body.innerHTML;
|
||||
|
||||
console.log('✅ 사이드바 로딩 및 필터링 완료');
|
||||
|
||||
} catch (error) {
|
||||
console.error('🔴 사이드바 로딩 실패:', error);
|
||||
sidebarContainer.innerHTML = '<p>메뉴 로딩 실패</p>';
|
||||
}
|
||||
});
|
||||
@@ -1,52 +1,7 @@
|
||||
// 깔끔한 로그인 로직 (login.js)
|
||||
import { API } from './api-config.js';
|
||||
// /js/login.js
|
||||
|
||||
function parseJwt(token) {
|
||||
try {
|
||||
return JSON.parse(atob(token.split('.')[1]));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 역할별 대시보드 라우팅
|
||||
function routeToDashboard(user) {
|
||||
const accessLevel = (user.access_level || '').toLowerCase().trim();
|
||||
|
||||
// 그룹장/리더 관련 키워드들
|
||||
const leaderKeywords = [
|
||||
'group_leader', 'groupleader', 'group-leader',
|
||||
'leader', 'supervisor', 'team_leader', 'teamleader',
|
||||
'그룹장', '팀장', '현장책임자'
|
||||
];
|
||||
|
||||
// 관리자 관련 키워드들
|
||||
const adminKeywords = [
|
||||
'admin', 'administrator', 'system',
|
||||
'관리자', '시스템관리자'
|
||||
];
|
||||
|
||||
// 지원팀 관련 키워드들
|
||||
const supportKeywords = [
|
||||
'support', 'support_team', 'supportteam',
|
||||
'지원팀', '지원'
|
||||
];
|
||||
|
||||
// 키워드 매칭
|
||||
if (leaderKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
|
||||
return '/pages/dashboard/group-leader.html';
|
||||
}
|
||||
|
||||
if (adminKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
|
||||
return '/pages/dashboard/admin.html';
|
||||
}
|
||||
|
||||
if (supportKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
|
||||
return '/pages/dashboard/support.html';
|
||||
}
|
||||
|
||||
return '/pages/dashboard/user.html';
|
||||
}
|
||||
import { login } from './api-helper.js';
|
||||
import { saveAuthData, clearAuthData } from './auth.js';
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
@@ -55,48 +10,45 @@ document.getElementById('loginForm').addEventListener('submit', async function (
|
||||
const password = document.getElementById('password').value;
|
||||
const errorDiv = document.getElementById('error');
|
||||
|
||||
// 로딩 상태 표시
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.textContent;
|
||||
|
||||
// 로딩 상태 시작
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '로그인 중...';
|
||||
errorDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
// API 헬퍼를 통해 로그인 요청
|
||||
const result = await login(username, password);
|
||||
|
||||
const result = await res.json();
|
||||
if (result.success && result.token) {
|
||||
// 인증 정보 저장
|
||||
saveAuthData(result.token, result.user);
|
||||
|
||||
if (res.ok && result.success && result.token) {
|
||||
localStorage.setItem('token', result.token);
|
||||
localStorage.setItem('user', JSON.stringify(result.user));
|
||||
|
||||
// 역할별 대시보드로 리다이렉트
|
||||
const redirectUrl = routeToDashboard(result.user);
|
||||
// 백엔드가 지정한 URL로 리디렉션
|
||||
const redirectUrl = result.redirectUrl || '/pages/dashboard/user.html'; // 혹시 모를 예외처리
|
||||
|
||||
// 부드러운 전환 효과
|
||||
// 부드러운 화면 전환 효과
|
||||
document.body.style.transition = 'opacity 0.3s ease-out';
|
||||
document.body.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = redirectUrl;
|
||||
}, 300);
|
||||
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
// 이 케이스는 api-helper에서 throw new Error()로 처리되어 catch 블록으로 바로 이동합니다.
|
||||
// 하지만, 만약의 경우를 대비해 방어 코드를 남겨둡니다.
|
||||
clearAuthData();
|
||||
errorDiv.textContent = result.error || '로그인에 실패했습니다.';
|
||||
errorDiv.style.display = 'block';
|
||||
|
||||
// 에러 메시지 자동 숨김
|
||||
setTimeout(() => {
|
||||
errorDiv.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('로그인 오류:', err);
|
||||
errorDiv.textContent = '서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요.';
|
||||
clearAuthData();
|
||||
// api-helper에서 보낸 에러 메시지를 표시
|
||||
errorDiv.textContent = err.message || '서버 연결에 실패했습니다.';
|
||||
errorDiv.style.display = 'block';
|
||||
} finally {
|
||||
// 로딩 상태 해제
|
||||
|
||||
91
web-ui/js/report-viewer-api.js
Normal file
91
web-ui/js/report-viewer-api.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// /js/report-viewer-api.js
|
||||
import { apiGet } from './api-helper.js';
|
||||
import { getUser } from './auth.js';
|
||||
|
||||
/**
|
||||
* 보고서 조회를 위한 마스터 데이터를 로드합니다. (작업 유형, 상태 등)
|
||||
* 실패 시 기본값을 반환할 수 있도록 개별적으로 처리합니다.
|
||||
* @returns {Promise<object>} - 각 마스터 데이터 배열을 포함하는 객체
|
||||
*/
|
||||
export async function loadMasterData() {
|
||||
const masterData = {
|
||||
workTypes: [],
|
||||
workStatusTypes: [],
|
||||
errorTypes: []
|
||||
};
|
||||
try {
|
||||
// Promise.allSettled를 사용해 일부 API가 실패해도 전체가 중단되지 않도록 함
|
||||
const results = await Promise.allSettled([
|
||||
apiGet('/daily-work-reports/work-types'),
|
||||
apiGet('/daily-work-reports/work-status-types'),
|
||||
apiGet('/daily-work-reports/error-types')
|
||||
]);
|
||||
|
||||
if (results[0].status === 'fulfilled') masterData.workTypes = results[0].value;
|
||||
if (results[1].status === 'fulfilled') masterData.workStatusTypes = results[1].value;
|
||||
if (results[2].status === 'fulfilled') masterData.errorTypes = results[2].value;
|
||||
|
||||
return masterData;
|
||||
} catch (error) {
|
||||
console.error('마스터 데이터 로딩 중 심각한 오류 발생:', error);
|
||||
// 최소한의 기본값이라도 반환
|
||||
return masterData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 사용자의 권한을 확인하여 적절한 API 엔드포인트와 파라미터를 결정합니다.
|
||||
* @param {string} selectedDate - 조회할 날짜
|
||||
* @returns {string} - 호출할 API URL
|
||||
*/
|
||||
function getReportApiUrl(selectedDate) {
|
||||
const user = getUser();
|
||||
|
||||
// 관리자(admin, system)는 모든 데이터를 조회
|
||||
if (user && (user.role === 'admin' || user.role === 'system')) {
|
||||
// 백엔드에서 GET /daily-work-reports?date=YYYY-MM-DD 요청 시
|
||||
// 권한을 확인하고 모든 데이터를 내려준다고 가정
|
||||
return `/daily-work-reports?date=${selectedDate}`;
|
||||
}
|
||||
|
||||
// 그 외 사용자(leader, user)는 본인이 생성한 데이터만 조회
|
||||
// 백엔드에서 동일한 엔드포인트로 요청 시, 권한을 확인하고
|
||||
// 본인 데이터만 필터링해서 내려준다고 가정
|
||||
// (만약 엔드포인트가 다르다면 이 부분을 수정해야 함)
|
||||
return `/daily-work-reports?date=${selectedDate}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 특정 날짜의 작업 보고서 데이터를 서버에서 가져옵니다.
|
||||
* @param {string} selectedDate - 조회할 날짜 (YYYY-MM-DD)
|
||||
* @returns {Promise<Array>} - 작업 보고서 데이터 배열
|
||||
*/
|
||||
export async function fetchReportData(selectedDate) {
|
||||
if (!selectedDate) {
|
||||
throw new Error('조회할 날짜가 선택되지 않았습니다.');
|
||||
}
|
||||
|
||||
const apiUrl = getReportApiUrl(selectedDate);
|
||||
|
||||
try {
|
||||
const rawData = await apiGet(apiUrl);
|
||||
|
||||
// 서버 응답이 { success: true, data: [...] } 형태일 경우와 [...] 형태일 경우 모두 처리
|
||||
if (rawData && rawData.success && Array.isArray(rawData.data)) {
|
||||
return rawData.data;
|
||||
}
|
||||
if (Array.isArray(rawData)) {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
// 예상치 못한 형식의 응답
|
||||
console.warn('예상치 못한 형식의 API 응답:', rawData);
|
||||
return [];
|
||||
|
||||
} catch (error) {
|
||||
console.error(`${selectedDate}의 작업 보고서 조회 실패:`, error);
|
||||
throw new Error('서버에서 데이터를 가져오는 데 실패했습니다.');
|
||||
}
|
||||
}
|
||||
72
web-ui/js/report-viewer-export.js
Normal file
72
web-ui/js/report-viewer-export.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// /js/report-viewer-export.js
|
||||
|
||||
/**
|
||||
* 주어진 데이터를 CSV 형식의 문자열로 변환합니다.
|
||||
* @param {object} reportData - 요약 및 작업자별 데이터
|
||||
* @returns {string} - CSV 형식의 문자열
|
||||
*/
|
||||
function convertToCsv(reportData) {
|
||||
let csvContent = "\uFEFF"; // UTF-8 BOM
|
||||
csvContent += "작업자명,프로젝트명,작업유형,작업상태,에러유형,작업시간,입력자\n";
|
||||
|
||||
reportData.workers.forEach(worker => {
|
||||
worker.entries.forEach(entry => {
|
||||
const row = [
|
||||
worker.worker_name,
|
||||
entry.project_name,
|
||||
entry.work_type_name,
|
||||
entry.work_status_name,
|
||||
entry.error_type_name,
|
||||
entry.work_hours,
|
||||
entry.created_by_name
|
||||
].map(field => `"${String(field || '').replace(/"/g, '""')}"`).join(',');
|
||||
csvContent += row + "\n";
|
||||
});
|
||||
});
|
||||
return csvContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 가공된 보고서 데이터를 CSV 파일로 다운로드합니다.
|
||||
* @param {object|null} reportData - UI에 표시된 가공된 데이터
|
||||
*/
|
||||
export function exportToExcel(reportData) {
|
||||
if (!reportData || !reportData.workers || reportData.workers.length === 0) {
|
||||
alert('내보낼 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const csv = convertToCsv(reportData);
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const fileName = `작업보고서_${reportData.summary.date}.csv`;
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', fileName);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Excel 내보내기 실패:', error);
|
||||
alert('Excel 파일을 생성하는 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 페이지의 인쇄 기능을 호출합니다.
|
||||
*/
|
||||
export function printReport() {
|
||||
try {
|
||||
window.print();
|
||||
} catch (error) {
|
||||
console.error('인쇄 실패:', error);
|
||||
alert('인쇄 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
144
web-ui/js/report-viewer-ui.js
Normal file
144
web-ui/js/report-viewer-ui.js
Normal file
@@ -0,0 +1,144 @@
|
||||
// /js/report-viewer-ui.js
|
||||
|
||||
/**
|
||||
* 데이터를 가공하여 UI에 표시하기 좋은 요약 형태로 변환합니다.
|
||||
* @param {Array} rawData - 서버에서 받은 원시 데이터 배열
|
||||
* @param {string} selectedDate - 선택된 날짜
|
||||
* @returns {object} - 요약 정보와 작업자별로 그룹화된 데이터를 포함하는 객체
|
||||
*/
|
||||
export function processReportData(rawData, selectedDate) {
|
||||
if (!Array.isArray(rawData) || rawData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workerGroups = {};
|
||||
let totalHours = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
rawData.forEach(item => {
|
||||
const workerName = item.worker_name || '미지정';
|
||||
const workHours = parseFloat(item.work_hours || 0);
|
||||
totalHours += workHours;
|
||||
if (item.work_status_id === 2) errorCount++; // '에러' 상태 ID가 2라고 가정
|
||||
|
||||
if (!workerGroups[workerName]) {
|
||||
workerGroups[workerName] = {
|
||||
worker_name: workerName,
|
||||
total_hours: 0,
|
||||
entries: []
|
||||
};
|
||||
}
|
||||
workerGroups[workerName].total_hours += workHours;
|
||||
workerGroups[workerName].entries.push(item);
|
||||
});
|
||||
|
||||
return {
|
||||
summary: {
|
||||
date: selectedDate,
|
||||
total_workers: Object.keys(workerGroups).length,
|
||||
total_hours: totalHours,
|
||||
total_entries: rawData.length,
|
||||
error_count: errorCount
|
||||
},
|
||||
workers: Object.values(workerGroups)
|
||||
};
|
||||
}
|
||||
|
||||
function displaySummary(summary) {
|
||||
const elements = {
|
||||
totalWorkers: summary.total_workers,
|
||||
totalHours: `${summary.total_hours}시간`,
|
||||
totalEntries: `${summary.total_entries}개`,
|
||||
errorCount: `${summary.error_count}개`
|
||||
};
|
||||
Object.entries(elements).forEach(([id, value]) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value;
|
||||
});
|
||||
document.getElementById('reportSummary').style.display = 'block';
|
||||
}
|
||||
|
||||
function createWorkEntryElement(entry) {
|
||||
const entryDiv = document.createElement('div');
|
||||
entryDiv.className = `work-entry ${entry.work_status_id === 2 ? 'error-entry' : ''}`;
|
||||
entryDiv.innerHTML = `
|
||||
<div class="entry-header">
|
||||
<div class="project-name">${entry.project_name || '프로젝트 미지정'}</div>
|
||||
<div class="work-hours">${entry.work_hours || 0}시간</div>
|
||||
</div>
|
||||
<div class="entry-details">
|
||||
<div class="entry-detail">
|
||||
<span class="detail-label">작업 유형:</span>
|
||||
<span class="detail-value">${entry.work_type_name || '-'}</span>
|
||||
</div>
|
||||
${entry.work_status_id === 2 ? `
|
||||
<div class="entry-detail">
|
||||
<span class="detail-label">에러 유형:</span>
|
||||
<span class="detail-value error-type">${entry.error_type_name || '에러'}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
return entryDiv;
|
||||
}
|
||||
|
||||
function displayWorkersDetails(workers) {
|
||||
const workersListEl = document.getElementById('workersList');
|
||||
workersListEl.innerHTML = '';
|
||||
workers.forEach(worker => {
|
||||
const workerCard = document.createElement('div');
|
||||
workerCard.className = 'worker-card';
|
||||
workerCard.innerHTML = `
|
||||
<div class="worker-header">
|
||||
<div class="worker-name">👤 ${worker.worker_name}</div>
|
||||
<div class="worker-total-hours">총 ${worker.total_hours}시간</div>
|
||||
</div>
|
||||
`;
|
||||
const entriesContainer = document.createElement('div');
|
||||
entriesContainer.className = 'work-entries';
|
||||
worker.entries.forEach(entry => entriesContainer.appendChild(createWorkEntryElement(entry)));
|
||||
workerCard.appendChild(entriesContainer);
|
||||
workersListEl.appendChild(workerCard);
|
||||
});
|
||||
document.getElementById('workersReport').style.display = 'block';
|
||||
}
|
||||
|
||||
const hideElement = (id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
};
|
||||
|
||||
/**
|
||||
* 가공된 데이터를 받아 화면 전체를 렌더링합니다.
|
||||
* @param {object|null} processedData - 가공된 데이터 또는 데이터가 없을 경우 null
|
||||
*/
|
||||
export function renderReport(processedData) {
|
||||
hideElement('loadingSpinner');
|
||||
hideElement('errorMessage');
|
||||
hideElement('noDataMessage');
|
||||
hideElement('reportSummary');
|
||||
hideElement('workersReport');
|
||||
hideElement('exportSection');
|
||||
|
||||
if (!processedData) {
|
||||
document.getElementById('noDataMessage').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
displaySummary(processedData.summary);
|
||||
displayWorkersDetails(processedData.workers);
|
||||
document.getElementById('exportSection').style.display = 'block';
|
||||
}
|
||||
|
||||
export function showLoading(isLoading) {
|
||||
document.getElementById('loadingSpinner').style.display = isLoading ? 'flex' : 'none';
|
||||
if(isLoading) {
|
||||
hideElement('errorMessage');
|
||||
hideElement('noDataMessage');
|
||||
}
|
||||
}
|
||||
|
||||
export function showError(message) {
|
||||
const errorEl = document.getElementById('errorMessage');
|
||||
errorEl.querySelector('.error-text').textContent = message;
|
||||
errorEl.style.display = 'block';
|
||||
hideElement('loadingSpinner');
|
||||
}
|
||||
@@ -1,31 +1,93 @@
|
||||
import { API, getAuthHeaders } from '/js/api-config.js';
|
||||
// /js/user-dashboard.js
|
||||
import { getUser } from './auth.js';
|
||||
import { apiGet } from './api-helper.js'; // 개선된 api-helper를 사용합니다.
|
||||
|
||||
// 오늘 일정 로드
|
||||
/**
|
||||
* API를 호출하여 오늘의 작업 일정을 불러와 화면에 표시합니다.
|
||||
*/
|
||||
async function loadTodaySchedule() {
|
||||
// 구현 필요
|
||||
document.getElementById('today-schedule').innerHTML =
|
||||
'<p>오늘의 작업 일정이 여기에 표시됩니다.</p>';
|
||||
}
|
||||
const scheduleContainer = document.getElementById('today-schedule');
|
||||
scheduleContainer.innerHTML = '<p>📅 오늘의 작업 일정을 불러오는 중...</p>';
|
||||
|
||||
// 작업 통계 로드
|
||||
async function loadWorkStats() {
|
||||
// 구현 필요
|
||||
document.getElementById('work-stats').innerHTML =
|
||||
'<p>이번 달 작업 시간: 160시간</p>';
|
||||
}
|
||||
|
||||
// 환영 메시지 개인화
|
||||
function personalizeWelcome() {
|
||||
const user = window.currentUser;
|
||||
if (user) {
|
||||
document.getElementById('welcome-message').textContent =
|
||||
`${user.name}님, 환영합니다!`;
|
||||
try {
|
||||
// 예시: /api/dashboard/today-schedule 엔드포인트에서 데이터를 가져옵니다.
|
||||
// 실제 엔드포인트는 백엔드 구현에 따라 달라질 수 있습니다.
|
||||
const scheduleData = await apiGet('/dashboard/today-schedule');
|
||||
|
||||
if (scheduleData && scheduleData.length > 0) {
|
||||
const scheduleHtml = scheduleData.map(item => `
|
||||
<div class="schedule-item">
|
||||
<span class="time">${item.time}</span>
|
||||
<span class="task">${item.task_name}</span>
|
||||
<span class="status ${item.status}">${item.status_kor}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
scheduleContainer.innerHTML = scheduleHtml;
|
||||
} else {
|
||||
scheduleContainer.innerHTML = '<p>오늘 예정된 작업이 없습니다.</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('오늘의 작업 일정 로드 실패:', error);
|
||||
scheduleContainer.innerHTML = '<p class="error">일정 정보를 불러오는 데 실패했습니다.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* API를 호출하여 현재 사용자의 작업 통계를 불러와 화면에 표시합니다.
|
||||
*/
|
||||
async function loadWorkStats() {
|
||||
const statsContainer = document.getElementById('work-stats');
|
||||
statsContainer.innerHTML = '<p>📈 내 작업 현황을 불러오는 중...</p>';
|
||||
|
||||
try {
|
||||
// 예시: /api/dashboard/my-stats 엔드포인트에서 데이터를 가져옵니다.
|
||||
const statsData = await apiGet('/dashboard/my-stats');
|
||||
|
||||
if (statsData) {
|
||||
const statsHtml = `
|
||||
<div class="stat-item">
|
||||
<span>이번 주 작업 시간:</span>
|
||||
<strong>${statsData.weekly_hours || 0} 시간</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>이번 달 작업 시간:</span>
|
||||
<strong>${statsData.monthly_hours || 0} 시간</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>완료한 작업 수:</span>
|
||||
<strong>${statsData.completed_tasks || 0} 건</strong>
|
||||
</div>
|
||||
`;
|
||||
statsContainer.innerHTML = statsHtml;
|
||||
} else {
|
||||
statsContainer.innerHTML = '<p>표시할 통계 정보가 없습니다.</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업 통계 로드 실패:', error);
|
||||
statsContainer.innerHTML = '<p class="error">통계 정보를 불러오는 데 실패했습니다.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 환영 메시지를 사용자 이름으로 개인화합니다.
|
||||
*/
|
||||
function personalizeWelcome() {
|
||||
// 전역 변수 대신 auth.js 모듈을 통해 사용자 정보를 가져옵니다.
|
||||
const user = getUser();
|
||||
if (user) {
|
||||
const welcomeEl = document.getElementById('welcome-message');
|
||||
if (welcomeEl) {
|
||||
welcomeEl.textContent = `${user.name || user.username}님, 환영합니다! 오늘 하루도 안전하게 작업하세요.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 초기화 함수
|
||||
function initializeDashboard() {
|
||||
personalizeWelcome();
|
||||
loadTodaySchedule();
|
||||
loadWorkStats();
|
||||
});
|
||||
}
|
||||
|
||||
// DOM이 로드되면 대시보드 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializeDashboard);
|
||||
46
web-ui/js/work-report-api.js
Normal file
46
web-ui/js/work-report-api.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// /js/work-report-api.js
|
||||
import { apiGet, apiPost } from './api-helper.js';
|
||||
|
||||
/**
|
||||
* 작업 보고서 작성을 위해 필요한 초기 데이터(작업자, 프로젝트, 태스크)를 가져옵니다.
|
||||
* Promise.all을 사용하여 병렬로 API를 호출합니다.
|
||||
* @returns {Promise<{workers: Array, projects: Array, tasks: Array}>}
|
||||
*/
|
||||
export async function getInitialData() {
|
||||
try {
|
||||
const [workers, projects, tasks] = await Promise.all([
|
||||
apiGet('/workers'),
|
||||
apiGet('/projects'),
|
||||
apiGet('/tasks')
|
||||
]);
|
||||
|
||||
// 데이터 형식 검증
|
||||
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
|
||||
throw new Error('서버에서 받은 데이터 형식이 올바르지 않습니다.');
|
||||
}
|
||||
|
||||
// 작업자 목록은 ID 기준으로 정렬
|
||||
workers.sort((a, b) => a.worker_id - b.worker_id);
|
||||
|
||||
return { workers, projects, tasks };
|
||||
} catch (error) {
|
||||
console.error('초기 데이터 로딩 중 오류 발생:', error);
|
||||
// 에러를 다시 던져서 호출한 쪽에서 처리할 수 있도록 함
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성된 작업 보고서 데이터를 서버에 전송합니다.
|
||||
* @param {Array<object>} reportData - 전송할 작업 보고서 데이터 배열
|
||||
* @returns {Promise<object>} - 서버의 응답 결과
|
||||
*/
|
||||
export async function createWorkReport(reportData) {
|
||||
try {
|
||||
const result = await apiPost('/workreports', reportData);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('작업 보고서 생성 요청 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,185 +1,79 @@
|
||||
import { renderCalendar } from '/js/calendar.js'; // 날짜 캘린더 모듈
|
||||
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
|
||||
// /js/work-report-create.js
|
||||
import { renderCalendar } from './calendar.js';
|
||||
import { getInitialData, createWorkReport } from './work-report-api.js';
|
||||
import { initializeReportTable, getReportData } from './work-report-ui.js';
|
||||
|
||||
// 인증 확인
|
||||
ensureAuthenticated();
|
||||
// 전역 상태 변수
|
||||
let selectedDate = '';
|
||||
|
||||
// ✅ DOM 요소
|
||||
const reportBody = document.getElementById('reportBody');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const defaultProjectId = '13';
|
||||
const defaultTaskId = '15';
|
||||
let selectedDateStr = '';
|
||||
|
||||
// ✅ 페이지 로드시 초기 렌더링
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetch('/components/navbar.html')
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
document.getElementById('navbar-container').innerHTML = html;
|
||||
})
|
||||
.catch(err => console.error('🔴 네비게이션 바 로딩 실패:', err));
|
||||
|
||||
renderCalendar('calendar', date => {
|
||||
selectedDateStr = date;
|
||||
loadWorkers();
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 작업자, 프로젝트, 작업 불러오기
|
||||
async function loadWorkers() {
|
||||
if (!selectedDateStr) return;
|
||||
/**
|
||||
* 날짜가 선택되었을 때 실행되는 콜백 함수.
|
||||
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
|
||||
* @param {string} date - 선택된 날짜 (YYYY-MM-DD 형식)
|
||||
*/
|
||||
async function onDateSelect(date) {
|
||||
selectedDate = date;
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">데이터를 불러오는 중...</td></tr>';
|
||||
|
||||
try {
|
||||
const [wrRes, prRes, tkRes] = await Promise.all([
|
||||
fetch(`${API}/workers`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/projects`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/tasks`, { headers: getAuthHeaders() })
|
||||
]);
|
||||
|
||||
if (!wrRes.ok || !prRes.ok || !tkRes.ok) {
|
||||
throw new Error('데이터 불러오기 실패');
|
||||
}
|
||||
|
||||
const workers = await wrRes.json();
|
||||
const projects = await prRes.json();
|
||||
const tasks = await tkRes.json();
|
||||
|
||||
// 배열 체크
|
||||
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
|
||||
throw new Error('잘못된 데이터 형식');
|
||||
}
|
||||
|
||||
workers.sort((a, b) => a.worker_id - b.worker_id);
|
||||
reportBody.innerHTML = '';
|
||||
|
||||
workers.forEach((w, i) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${i + 1}</td>
|
||||
<td>
|
||||
<input type="hidden" name="worker_id" value="${w.worker_id}">
|
||||
${w.worker_name}
|
||||
</td>
|
||||
<td>
|
||||
<select name="project_id">
|
||||
${projects.map(p =>
|
||||
`<option value="${p.project_id}">${p.project_name}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="task_id">
|
||||
${tasks.map(t =>
|
||||
`<option value="${t.task_id}">${t.category}:${t.subcategory}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="overtime">
|
||||
<option value="">없음</option>
|
||||
<option>1</option><option>2</option>
|
||||
<option>3</option><option>4</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="work_type">
|
||||
<option>근무</option><option>연차</option><option>유급</option>
|
||||
<option>반차</option><option>반반차</option><option>조퇴</option>
|
||||
<option>휴무</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="memo" placeholder="메모">
|
||||
</td>
|
||||
<td>
|
||||
<button class="remove-btn">x</button>
|
||||
</td>
|
||||
`;
|
||||
reportBody.appendChild(tr);
|
||||
|
||||
// 근무형태 변경시 프로젝트/작업 필드 비활성화
|
||||
const workSel = tr.querySelector('[name="work_type"]');
|
||||
const projSel = tr.querySelector('[name="project_id"]');
|
||||
const taskSel = tr.querySelector('[name="task_id"]');
|
||||
|
||||
workSel.addEventListener('change', () => {
|
||||
const disabled = ['연차','휴무','유급'].includes(workSel.value);
|
||||
projSel.value = disabled ? defaultProjectId : projSel.value;
|
||||
taskSel.value = disabled ? defaultTaskId : taskSel.value;
|
||||
projSel.disabled = taskSel.disabled = disabled;
|
||||
});
|
||||
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
updateRowNumbers();
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert(err.message || '작업자 불러오기 중 오류 발생');
|
||||
const initialData = await getInitialData();
|
||||
initializeReportTable(initialData);
|
||||
} catch (error) {
|
||||
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 행 번호 다시 매기기
|
||||
function updateRowNumbers() {
|
||||
reportBody.querySelectorAll('tr').forEach((tr, i) => {
|
||||
tr.children[0].textContent = i + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 전체 등록 처리
|
||||
submitBtn.addEventListener('click', async () => {
|
||||
if (!selectedDateStr) {
|
||||
alert('날짜를 먼저 선택하세요.');
|
||||
/**
|
||||
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
|
||||
* 폼 데이터를 서버에 전송합니다.
|
||||
*/
|
||||
async function handleSubmit() {
|
||||
if (!selectedDate) {
|
||||
alert('먼저 달력에서 날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = Array.from(reportBody.querySelectorAll('tr'));
|
||||
if (rows.length === 0) {
|
||||
alert('등록할 작업자가 없습니다.');
|
||||
const reportData = getReportData();
|
||||
if (!reportData) {
|
||||
// getReportData 내부에서 이미 alert으로 사용자에게 알림
|
||||
return;
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const payload = [];
|
||||
// 각 항목에 선택된 날짜 추가
|
||||
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
|
||||
|
||||
for (let tr of rows) {
|
||||
const wid = tr.querySelector('[name="worker_id"]').value;
|
||||
if (seen.has(wid)) {
|
||||
alert('중복된 작업자가 있습니다.');
|
||||
return;
|
||||
}
|
||||
seen.add(wid);
|
||||
|
||||
payload.push({
|
||||
date: selectedDateStr,
|
||||
worker_id: wid,
|
||||
project_id: tr.querySelector('[name="project_id"]').value,
|
||||
task_id: tr.querySelector('[name="task_id"]').value,
|
||||
overtime_hours: tr.querySelector('[name="overtime"]').value,
|
||||
work_details: tr.querySelector('[name="work_type"]').value,
|
||||
memo: tr.querySelector('[name="memo"]').value
|
||||
});
|
||||
}
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '등록 중...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/workreports`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
const result = await createWorkReport(payload);
|
||||
if (result.success) {
|
||||
alert('✅ 등록 완료!');
|
||||
// 선택적: 페이지 새로고침 또는 다른 날짜로 이동
|
||||
// loadWorkers();
|
||||
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
|
||||
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
|
||||
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
|
||||
} else {
|
||||
alert('❌ 등록 실패: ' + (result.error || '알 수 없는 오류'));
|
||||
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('서버 오류가 발생했습니다: ' + err.message);
|
||||
} catch (error) {
|
||||
alert('❌ 등록 실패: ' + error.message);
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '전체 등록';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 초기화 함수
|
||||
*/
|
||||
function initializePage() {
|
||||
renderCalendar('calendar', onDateSelect);
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.addEventListener('click', handleSubmit);
|
||||
}
|
||||
|
||||
// DOM이 로드되면 페이지 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
141
web-ui/js/work-report-ui.js
Normal file
141
web-ui/js/work-report-ui.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// /js/work-report-ui.js
|
||||
|
||||
const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음
|
||||
const DEFAULT_TASK_ID = '15';
|
||||
|
||||
/**
|
||||
* 주어진 데이터를 바탕으로 <select> 요소의 <option>들을 생성합니다.
|
||||
* @param {Array<object>} items - 옵션으로 만들 데이터 배열
|
||||
* @param {string} valueField - <option>의 value 속성에 사용할 필드 이름
|
||||
* @param {string} textField - <option>의 텍스트에 사용할 필드 이름
|
||||
* @returns {string} - 생성된 HTML 옵션 문자열
|
||||
*/
|
||||
function createOptions(items, valueField, textField) {
|
||||
return items.map(item => `<option value="${item[valueField]}">${textField(item)}</option>`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 모든 행 번호를 다시 매깁니다.
|
||||
* @param {HTMLTableSectionElement} tableBody - tbody 요소
|
||||
*/
|
||||
function updateRowNumbers(tableBody) {
|
||||
tableBody.querySelectorAll('tr').forEach((tr, index) => {
|
||||
tr.cells[0].textContent = index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 하나의 작업 보고서 행(tr)을 생성합니다.
|
||||
* @param {object} worker - 작업자 정보
|
||||
* @param {Array} projects - 전체 프로젝트 목록
|
||||
* @param {Array} tasks - 전체 태스크 목록
|
||||
* @param {number} index - 행 번호
|
||||
* @returns {HTMLTableRowElement} - 생성된 tr 요소
|
||||
*/
|
||||
function createReportRow(worker, projects, tasks, index) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${index + 1}</td>
|
||||
<td>
|
||||
<input type="hidden" name="worker_id" value="${worker.worker_id}">
|
||||
${worker.worker_name}
|
||||
</td>
|
||||
<td><select name="project_id">${createOptions(projects, 'project_id', p => p.project_name)}</select></td>
|
||||
<td><select name="task_id">${createOptions(tasks, 'task_id', t => `${t.category}:${t.subcategory}`)}</select></td>
|
||||
<td>
|
||||
<select name="overtime">
|
||||
<option value="">없음</option>
|
||||
${[1, 2, 3, 4].map(n => `<option>${n}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="work_type">
|
||||
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(t => `<option>${t}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="memo" placeholder="메모"></td>
|
||||
<td><button type="button" class="remove-btn">x</button></td>
|
||||
`;
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
const workTypeSelect = tr.querySelector('[name="work_type"]');
|
||||
const projectSelect = tr.querySelector('[name="project_id"]');
|
||||
const taskSelect = tr.querySelector('[name="task_id"]');
|
||||
|
||||
workTypeSelect.addEventListener('change', () => {
|
||||
const isDisabled = ['연차', '휴무', '유급'].includes(workTypeSelect.value);
|
||||
projectSelect.disabled = isDisabled;
|
||||
taskSelect.disabled = isDisabled;
|
||||
if (isDisabled) {
|
||||
projectSelect.value = DEFAULT_PROJECT_ID;
|
||||
taskSelect.value = DEFAULT_TASK_ID;
|
||||
}
|
||||
});
|
||||
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
updateRowNumbers(tr.parentElement);
|
||||
});
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 보고서 테이블을 초기화하고 데이터를 채웁니다.
|
||||
* @param {{workers: Array, projects: Array, tasks: Array}} initialData - 초기 데이터
|
||||
*/
|
||||
export function initializeReportTable(initialData) {
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = ''; // 기존 내용 초기화
|
||||
const { workers, projects, tasks } = initialData;
|
||||
|
||||
if (!workers || workers.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">등록할 작업자 정보가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
workers.forEach((worker, index) => {
|
||||
const row = createReportRow(worker, projects, tasks, index);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블에서 폼 데이터를 추출하여 배열로 반환합니다.
|
||||
* @returns {Array<object>|null} - 추출된 데이터 배열 또는 유효성 검사 실패 시 null
|
||||
*/
|
||||
export function getReportData() {
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
const rows = tableBody.querySelectorAll('tr');
|
||||
|
||||
if (rows.length === 0 || (rows.length === 1 && rows[0].cells.length < 2)) {
|
||||
alert('등록할 내용이 없습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const reportData = [];
|
||||
const workerIds = new Set();
|
||||
|
||||
for (const tr of rows) {
|
||||
const workerId = tr.querySelector('[name="worker_id"]').value;
|
||||
if (workerIds.has(workerId)) {
|
||||
alert(`오류: 작업자 '${tr.cells[1].textContent.trim()}'가 중복 등록되었습니다.`);
|
||||
return null;
|
||||
}
|
||||
workerIds.add(workerId);
|
||||
|
||||
reportData.push({
|
||||
worker_id: workerId,
|
||||
project_id: tr.querySelector('[name="project_id"]').value,
|
||||
task_id: tr.querySelector('[name="task_id"]').value,
|
||||
overtime_hours: tr.querySelector('[name="overtime"]').value || 0,
|
||||
work_details: tr.querySelector('[name="work_type"]').value,
|
||||
memo: tr.querySelector('[name="memo"]').value
|
||||
});
|
||||
}
|
||||
|
||||
return reportData;
|
||||
}
|
||||
@@ -29,7 +29,6 @@
|
||||
<script type="module" src="/js/load-sidebar.js"></script>
|
||||
<script type="module" src="/js/load-sections.js"></script>
|
||||
<!-- ✅ admin.js는 다른 모듈들이 로딩된 후 실행되도록 순서 조정 -->
|
||||
<script type="module" src="/components/sections/admin-sections.html"></script>
|
||||
<script type="module" src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user