feat: v2.2.0 - 중복 카드 문제 해결 및 삭제 기능 개선
### 주요 변경사항 1. 작업 현황 모달 중복 카드 문제 근본 해결 - monthlyStatusModel.getDailyWorkerStatus() 리팩토링 - 집계 테이블 대신 daily_work_reports에서 직접 조회 - GROUP BY로 작업자별 1개 카드 보장 2. 삭제 권한 강화 - 작업보고서 삭제는 그룹장/시스템/관리자만 가능 - 권한 없는 사용자는 403 에러 반환 3. 작업 입력 UI 개선 - 작업 항목 삭제 버튼 스타일 개선 (이모지 + 빨간색) - 삭제 버튼 호버 효과 추가 4. 작업 현황 모달에 삭제 기능 추가 - 관리자/그룹장만 삭제 버튼 표시 - 작업자의 해당 날짜 전체 작업 삭제 가능 5. 시놀로지 배포 스크립트 추가 - update.sh: DB 보존하면서 코드만 업데이트 - 안전한 배포 절차 자동화
This commit is contained in:
@@ -368,17 +368,28 @@ const updateWorkReport = async (req, res) => {
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업보고서 삭제 (V2 - Service Layer 사용)
|
||||
* 권한: 그룹장(group_leader), 시스템(system), 관리자(admin)만 가능
|
||||
*/
|
||||
const removeDailyWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id: reportId } = req.params;
|
||||
const userInfo = {
|
||||
user_id: req.user?.user_id || req.user?.id,
|
||||
access_level: req.user?.access_level || req.user?.role,
|
||||
};
|
||||
|
||||
if (!userInfo.user_id) {
|
||||
return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' });
|
||||
}
|
||||
|
||||
// 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능
|
||||
const allowedRoles = ['admin', 'system', 'group_leader'];
|
||||
if (!allowedRoles.includes(userInfo.access_level)) {
|
||||
return res.status(403).json({
|
||||
error: '작업보고서 삭제 권한이 없습니다.',
|
||||
details: '그룹장 이상의 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await dailyWorkReportService.removeDailyWorkReportService(reportId, userInfo);
|
||||
|
||||
@@ -405,6 +416,7 @@ const removeDailyWorkReport = async (req, res) => {
|
||||
const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
const { date, worker_id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
const access_level = req.user?.access_level || req.user?.role;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
@@ -412,6 +424,15 @@ const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능
|
||||
const allowedRoles = ['admin', 'system', 'group_leader'];
|
||||
if (!allowedRoles.includes(access_level)) {
|
||||
return res.status(403).json({
|
||||
error: '작업보고서 삭제 권한이 없습니다.',
|
||||
details: '그룹장 이상의 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
-- 009_fix_duplicate_monthly_status.sql
|
||||
-- monthly_worker_status 테이블의 중복 데이터 정리
|
||||
|
||||
-- 1. 중복 데이터 확인 (디버깅용)
|
||||
-- SELECT worker_id, date, COUNT(*) as cnt
|
||||
-- FROM monthly_worker_status
|
||||
-- GROUP BY worker_id, date
|
||||
-- HAVING cnt > 1;
|
||||
|
||||
-- 2. 중복 데이터 정리: 같은 worker_id, date에 대해 최신 데이터만 남기고 나머지 삭제
|
||||
DELETE mws1 FROM monthly_worker_status mws1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
worker_id,
|
||||
date,
|
||||
MAX(id) as keep_id
|
||||
FROM monthly_worker_status
|
||||
GROUP BY worker_id, date
|
||||
) mws2 ON mws1.worker_id = mws2.worker_id
|
||||
AND mws1.date = mws2.date
|
||||
AND mws1.id < mws2.keep_id;
|
||||
|
||||
-- 3. 중복 제거 후 데이터 재집계 (선택사항)
|
||||
-- 만약 합산이 필요하다면 다음 프로시저를 실행
|
||||
DELIMITER $$
|
||||
|
||||
CREATE OR REPLACE PROCEDURE ConsolidateDuplicateMonthlyStatus()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
DECLARE v_worker_id INT;
|
||||
DECLARE v_date DATE;
|
||||
|
||||
-- 중복이 있는 worker_id, date 조합 찾기
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT worker_id, date
|
||||
FROM monthly_worker_status
|
||||
GROUP BY worker_id, date
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
OPEN cur;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO v_worker_id, v_date;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
-- 해당 작업자의 해당 날짜 데이터를 재계산하여 업데이트
|
||||
CALL UpdateMonthlyWorkerStatus(v_date, v_worker_id);
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 4. 프로시저 실행하여 중복 데이터 통합
|
||||
-- CALL ConsolidateDuplicateMonthlyStatus();
|
||||
|
||||
-- 5. 확인: 중복이 남아있는지 체크
|
||||
SELECT
|
||||
'중복 체크 완료' as message,
|
||||
COUNT(*) as remaining_duplicates
|
||||
FROM (
|
||||
SELECT worker_id, date, COUNT(*) as cnt
|
||||
FROM monthly_worker_status
|
||||
GROUP BY worker_id, date
|
||||
HAVING cnt > 1
|
||||
) duplicates;
|
||||
|
||||
@@ -41,36 +41,47 @@ class MonthlyStatusModel {
|
||||
}
|
||||
|
||||
// 특정 날짜의 작업자별 상태 조회 (모달용)
|
||||
// ✅ 리팩토링: 집계 테이블 대신 daily_work_reports에서 직접 조회 (중복 문제 완전 해결)
|
||||
static async getDailyWorkerStatus(date) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 중복 방지: worker_id와 date로 그룹화하고 최신 데이터만 조회
|
||||
const [rows] = await db.execute(`
|
||||
// daily_work_reports에서 직접 집계하여 조회 (중복 없음 보장)
|
||||
const [rows] = await db.query(`
|
||||
SELECT
|
||||
mws.worker_id,
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
MAX(mws.year) as year,
|
||||
MAX(mws.month) as month,
|
||||
mws.date,
|
||||
SUM(mws.total_work_hours) as total_work_hours,
|
||||
SUM(mws.actual_work_hours) as actual_work_hours,
|
||||
SUM(mws.vacation_hours) as vacation_hours,
|
||||
SUM(mws.total_work_count) as total_work_count,
|
||||
SUM(mws.regular_work_count) as regular_work_count,
|
||||
SUM(mws.error_work_count) as error_work_count,
|
||||
MAX(mws.work_status) as work_status,
|
||||
MAX(mws.has_vacation) as has_vacation,
|
||||
MAX(mws.has_error) as has_error,
|
||||
MAX(mws.has_issues) as has_issues,
|
||||
MAX(mws.last_updated) as last_updated
|
||||
FROM monthly_worker_status mws
|
||||
JOIN workers w ON mws.worker_id = w.worker_id
|
||||
WHERE mws.date = ?
|
||||
GROUP BY mws.worker_id, mws.date, w.worker_name, w.job_type
|
||||
YEAR(?) as year,
|
||||
MONTH(?) as month,
|
||||
? as date,
|
||||
COALESCE(SUM(dwr.work_hours), 0) as total_work_hours,
|
||||
COALESCE(SUM(CASE WHEN dwr.project_id != 13 THEN dwr.work_hours ELSE 0 END), 0) as actual_work_hours,
|
||||
COALESCE(SUM(CASE WHEN dwr.project_id = 13 THEN dwr.work_hours ELSE 0 END), 0) as vacation_hours,
|
||||
COUNT(*) as total_work_count,
|
||||
COUNT(CASE WHEN dwr.project_id != 13 AND dwr.work_status_id != 2 THEN 1 END) as regular_work_count,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_work_count,
|
||||
CASE
|
||||
WHEN MAX(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) = 1 THEN 'error'
|
||||
WHEN SUM(dwr.work_hours) > 12 THEN 'overtime-warning'
|
||||
WHEN SUM(dwr.work_hours) > 8 THEN 'overtime'
|
||||
WHEN SUM(dwr.work_hours) = 8 THEN 'complete'
|
||||
WHEN SUM(dwr.work_hours) > 0 THEN 'partial'
|
||||
ELSE 'incomplete'
|
||||
END as work_status,
|
||||
MAX(CASE WHEN dwr.project_id = 13 THEN 1 ELSE 0 END) as has_vacation,
|
||||
MAX(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as has_error,
|
||||
CASE
|
||||
WHEN SUM(dwr.work_hours) < 8 AND SUM(dwr.work_hours) > 0 THEN 1
|
||||
ELSE 0
|
||||
END as has_issues,
|
||||
MAX(dwr.created_at) as last_updated
|
||||
FROM daily_work_reports dwr
|
||||
JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.report_date = ?
|
||||
GROUP BY dwr.worker_id, w.worker_name, w.job_type
|
||||
ORDER BY w.worker_name ASC
|
||||
`, [date]);
|
||||
`, [date, date, date, date]);
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hyungi-api",
|
||||
"version": "1.0.0",
|
||||
"version": "2.2.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "pm2-runtime start ecosystem.config.js --env production",
|
||||
|
||||
Reference in New Issue
Block a user