refactor(backend): 일일 이슈 보고 API 전체 리팩토링

- dailyIssueReport 기능을 Controller-Service-Model 아키텍처로 재구성
- Model 계층을 Promise 기반으로 전환하고 트랜잭션을 적용하여 안정성 확보
- API 전반의 코드 품질 및 유지보수성 향상
This commit is contained in:
2025-07-28 12:41:41 +09:00
parent 5268fec1ef
commit e3b2718767
3 changed files with 200 additions and 128 deletions

View File

@@ -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,
};

View File

@@ -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
};

View 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,
};