feat(tksupport): Sprint 001 Section C — 전사 휴가관리 구현
- 전사 휴가 부여/관리 (company-holidays) CRUD + 연차차감 트랜잭션 - 전체 휴가관리 대시보드 (vacation-dashboard) 부서별/직원별 현황 - 내 휴가 현황 개선 (/my-status) balance_type별 카드, 전사 휴가일 - requireSupportTeam 미들웨어, 부서명 JOIN, 마이그레이션 002 추가 - 사이드바 roles 기반 메뉴 필터링 (하위호환 유지) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
76
tksupport/api/controllers/companyHolidayController.js
Normal file
76
tksupport/api/controllers/companyHolidayController.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const companyHolidayModel = require('../models/companyHolidayModel');
|
||||
|
||||
const companyHolidayController = {
|
||||
async getHolidays(req, res) {
|
||||
try {
|
||||
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||
const holidays = await companyHolidayModel.getByYear(year);
|
||||
res.json({ success: true, data: holidays });
|
||||
} catch (error) {
|
||||
console.error('전사 휴가 조회 오류:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
||||
}
|
||||
},
|
||||
|
||||
async createHoliday(req, res) {
|
||||
try {
|
||||
const { holiday_date, holiday_name, holiday_type, description } = req.body;
|
||||
if (!holiday_date || !holiday_name || !holiday_type) {
|
||||
return res.status(400).json({ success: false, error: '필수 필드가 누락되었습니다' });
|
||||
}
|
||||
if (!['PAID', 'ANNUAL_DEDUCT'].includes(holiday_type)) {
|
||||
return res.status(400).json({ success: false, error: '유효하지 않은 휴가 유형입니다' });
|
||||
}
|
||||
|
||||
const created_by = req.user.user_id || req.user.id;
|
||||
const result = await companyHolidayModel.create({ holiday_date, holiday_name, holiday_type, description, created_by });
|
||||
res.status(201).json({ success: true, message: '전사 휴가가 등록되었습니다', data: { id: result.insertId } });
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_ENTRY') {
|
||||
return res.status(400).json({ success: false, error: '해당 날짜에 이미 전사 휴가가 등록되어 있습니다' });
|
||||
}
|
||||
console.error('전사 휴가 등록 오류:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
||||
}
|
||||
},
|
||||
|
||||
async deleteHoliday(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const holiday = await companyHolidayModel.getById(id);
|
||||
if (!holiday) {
|
||||
return res.status(404).json({ success: false, error: '해당 전사 휴가를 찾을 수 없습니다' });
|
||||
}
|
||||
if (holiday.deduction_applied_at) {
|
||||
return res.status(400).json({ success: false, error: '차감이 실행된 휴가는 삭제할 수 없습니다' });
|
||||
}
|
||||
|
||||
await companyHolidayModel.delete(id);
|
||||
res.json({ success: true, message: '전사 휴가가 삭제되었습니다' });
|
||||
} catch (error) {
|
||||
console.error('전사 휴가 삭제 오류:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
||||
}
|
||||
},
|
||||
|
||||
async applyDeduction(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const result = await companyHolidayModel.applyAnnualDeduction(id);
|
||||
const response = {
|
||||
success: true,
|
||||
message: `연차 차감이 완료되었습니다 (${result.affected_count}명 적용)`,
|
||||
data: result
|
||||
};
|
||||
if (result.missing_balance_count > 0) {
|
||||
response.warning = `${result.missing_balance_count}명의 사원은 연차 잔여일 데이터가 없어 차감되지 않았습니다`;
|
||||
}
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('연차 차감 오류:', error);
|
||||
res.status(400).json({ success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = companyHolidayController;
|
||||
@@ -1,5 +1,6 @@
|
||||
const vacationRequestModel = require('../models/vacationRequestModel');
|
||||
const vacationBalanceModel = require('../models/vacationBalanceModel');
|
||||
const companyHolidayModel = require('../models/companyHolidayModel');
|
||||
const { getPool } = require('../middleware/auth');
|
||||
|
||||
const vacationController = {
|
||||
@@ -228,6 +229,29 @@ const vacationController = {
|
||||
}
|
||||
},
|
||||
|
||||
// ─── 내 휴가 현황 ───
|
||||
|
||||
async getMyStatus(req, res) {
|
||||
try {
|
||||
const userId = req.user.user_id || req.user.id;
|
||||
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||
const startOfYear = `${year}-01-01`;
|
||||
const endOfYear = `${year}-12-31`;
|
||||
const [balances, requests, holidays] = await Promise.all([
|
||||
vacationBalanceModel.getByUserAndYear(userId, year),
|
||||
vacationRequestModel.getAll({ user_id: userId, start_date: startOfYear, end_date: endOfYear }),
|
||||
companyHolidayModel.getByYear(year)
|
||||
]);
|
||||
res.json({
|
||||
success: true,
|
||||
data: { balances, requests, company_holidays: holidays, overtime_hours: null }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('내 휴가 현황 조회 오류:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
||||
}
|
||||
},
|
||||
|
||||
// ─── 잔여일 ───
|
||||
|
||||
async getMyBalance(req, res) {
|
||||
|
||||
52
tksupport/api/controllers/vacationDashboardController.js
Normal file
52
tksupport/api/controllers/vacationDashboardController.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const vacationDashboardModel = require('../models/vacationDashboardModel');
|
||||
|
||||
const vacationDashboardController = {
|
||||
async getDashboard(req, res) {
|
||||
try {
|
||||
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||
const filters = {
|
||||
department_id: req.query.department_id || null,
|
||||
search_name: req.query.search_name || null
|
||||
};
|
||||
|
||||
const [summary, rows] = await Promise.all([
|
||||
vacationDashboardModel.getSummary(year),
|
||||
vacationDashboardModel.getEmployeeList(year, filters)
|
||||
]);
|
||||
|
||||
// user_id별 그룹핑
|
||||
const employeeMap = {};
|
||||
rows.forEach(row => {
|
||||
if (!employeeMap[row.user_id]) {
|
||||
employeeMap[row.user_id] = {
|
||||
user_id: row.user_id,
|
||||
name: row.name,
|
||||
username: row.username,
|
||||
department_name: row.department_name,
|
||||
hire_date: row.hire_date,
|
||||
balances: []
|
||||
};
|
||||
}
|
||||
if (row.id) {
|
||||
employeeMap[row.user_id].balances.push({
|
||||
balance_type: row.balance_type,
|
||||
type_name: row.type_name,
|
||||
type_code: row.type_code,
|
||||
total_days: row.total_days,
|
||||
used_days: row.used_days,
|
||||
remaining_days: row.remaining_days,
|
||||
expires_at: row.expires_at
|
||||
});
|
||||
}
|
||||
});
|
||||
const employees = Object.values(employeeMap);
|
||||
|
||||
res.json({ success: true, data: { summary, employees } });
|
||||
} catch (error) {
|
||||
console.error('휴가 대시보드 조회 오류:', error);
|
||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationDashboardController;
|
||||
25
tksupport/api/db/migrations/002_section_c_additions.sql
Normal file
25
tksupport/api/db/migrations/002_section_c_additions.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Section A 동시 개발 대비: 필요 테이블/컬럼 IF NOT EXISTS로 안전 생성
|
||||
|
||||
CREATE TABLE IF NOT EXISTS company_holidays (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
holiday_date DATE NOT NULL,
|
||||
holiday_name VARCHAR(100) NOT NULL,
|
||||
holiday_type ENUM('PAID', 'ANNUAL_DEDUCT') NOT NULL,
|
||||
description TEXT,
|
||||
created_by INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES sso_users(user_id),
|
||||
UNIQUE KEY uq_holiday_date (holiday_date)
|
||||
);
|
||||
|
||||
-- Section C 전용: 중복 차감 방지
|
||||
ALTER TABLE company_holidays
|
||||
ADD COLUMN IF NOT EXISTS deduction_applied_at TIMESTAMP NULL
|
||||
COMMENT '연차차감 실행 시각 (NULL=미실행)';
|
||||
|
||||
-- sp_vacation_balances 컬럼 추가 (Section A 대비)
|
||||
ALTER TABLE sp_vacation_balances
|
||||
ADD COLUMN IF NOT EXISTS balance_type ENUM('AUTO','MANUAL','CARRY_OVER','LONG_SERVICE','COMPANY_GRANT')
|
||||
DEFAULT 'AUTO';
|
||||
ALTER TABLE sp_vacation_balances
|
||||
ADD COLUMN IF NOT EXISTS expires_at DATE NULL;
|
||||
@@ -1,6 +1,8 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const vacationRoutes = require('./routes/vacationRoutes');
|
||||
const companyHolidayRoutes = require('./routes/companyHolidayRoutes');
|
||||
const vacationDashboardRoutes = require('./routes/vacationDashboardRoutes');
|
||||
const vacationRequestModel = require('./models/vacationRequestModel');
|
||||
const { requireAuth } = require('./middleware/auth');
|
||||
|
||||
@@ -34,6 +36,8 @@ app.get('/health', (req, res) => {
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/vacation/company', companyHolidayRoutes);
|
||||
app.use('/api/vacation/dashboard', vacationDashboardRoutes);
|
||||
app.use('/api/vacation', vacationRoutes);
|
||||
|
||||
// 404
|
||||
|
||||
@@ -75,4 +75,21 @@ function requirePage(pageName) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getPool, extractToken, requireAuth, requireAdmin, requirePage };
|
||||
function requireSupportTeam(req, res, next) {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
if (!['support_team', 'admin', 'system'].includes((decoded.role || '').toLowerCase())) {
|
||||
return res.status(403).json({ success: false, error: '지원팀 이상 권한이 필요합니다' });
|
||||
}
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getPool, extractToken, requireAuth, requireAdmin, requireSupportTeam, requirePage };
|
||||
|
||||
93
tksupport/api/models/companyHolidayModel.js
Normal file
93
tksupport/api/models/companyHolidayModel.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { getPool } = require('../middleware/auth');
|
||||
|
||||
const companyHolidayModel = {
|
||||
async getByYear(year) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM company_holidays WHERE YEAR(holiday_date) = ? ORDER BY holiday_date',
|
||||
[year]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query('SELECT * FROM company_holidays WHERE id = ?', [id]);
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
},
|
||||
|
||||
async create(data) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
'INSERT INTO company_holidays (holiday_date, holiday_name, holiday_type, description, created_by) VALUES (?, ?, ?, ?, ?)',
|
||||
[data.holiday_date, data.holiday_name, data.holiday_type, data.description || null, data.created_by]
|
||||
);
|
||||
return result;
|
||||
},
|
||||
|
||||
async delete(id) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query('DELETE FROM company_holidays WHERE id = ?', [id]);
|
||||
return result;
|
||||
},
|
||||
|
||||
async applyAnnualDeduction(holidayId) {
|
||||
const db = getPool();
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// 1. SELECT FOR UPDATE — 동시 실행 방지
|
||||
const [holidays] = await conn.query(
|
||||
'SELECT * FROM company_holidays WHERE id = ? FOR UPDATE', [holidayId]
|
||||
);
|
||||
if (holidays.length === 0) {
|
||||
throw new Error('해당 전사 휴가를 찾을 수 없습니다');
|
||||
}
|
||||
const holiday = holidays[0];
|
||||
if (holiday.holiday_type !== 'ANNUAL_DEDUCT') {
|
||||
throw new Error('연차차감 유형의 휴가만 차감할 수 있습니다');
|
||||
}
|
||||
if (holiday.deduction_applied_at) {
|
||||
throw new Error('이미 차감이 실행된 휴가입니다');
|
||||
}
|
||||
|
||||
// 2. vacation_types에서 ANNUAL_FULL id 조회
|
||||
const [types] = await conn.query(
|
||||
"SELECT id FROM vacation_types WHERE type_code = 'ANNUAL_FULL'"
|
||||
);
|
||||
if (types.length === 0) {
|
||||
throw new Error('연차(ANNUAL_FULL) 유형이 존재하지 않습니다');
|
||||
}
|
||||
const typeId = types[0].id;
|
||||
|
||||
// 3. 전 활성 사원 연차 차감 (잔여일 무관 일괄 적용)
|
||||
const [result] = await conn.query(`
|
||||
UPDATE sp_vacation_balances SET used_days = used_days + 1, updated_at = NOW()
|
||||
WHERE vacation_type_id = ? AND year = YEAR(?) AND balance_type = 'AUTO'
|
||||
AND user_id IN (SELECT user_id FROM sso_users WHERE is_active = 1)
|
||||
`, [typeId, holiday.holiday_date]);
|
||||
|
||||
// 4. 차감 완료 표시
|
||||
await conn.query(
|
||||
'UPDATE company_holidays SET deduction_applied_at = NOW() WHERE id = ?', [holidayId]
|
||||
);
|
||||
|
||||
// 5. balance 없는 사원 수 체크 (경고용)
|
||||
const [activeUsers] = await conn.query(
|
||||
'SELECT COUNT(*) as cnt FROM sso_users WHERE is_active = 1'
|
||||
);
|
||||
const missing = activeUsers[0].cnt - result.affectedRows;
|
||||
|
||||
await conn.commit();
|
||||
return { affected_count: result.affectedRows, missing_balance_count: missing };
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = companyHolidayModel;
|
||||
@@ -6,6 +6,8 @@ const vacationBalanceModel = {
|
||||
const [rows] = await db.query(`
|
||||
SELECT
|
||||
vb.*,
|
||||
vb.balance_type,
|
||||
vb.expires_at,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority,
|
||||
@@ -13,7 +15,7 @@ const vacationBalanceModel = {
|
||||
(vb.total_days - vb.used_days) as remaining_days
|
||||
FROM sp_vacation_balances vb
|
||||
INNER JOIN vacation_types vt ON vb.vacation_type_id = vt.id
|
||||
WHERE vb.user_id = ? AND vb.year = ?
|
||||
WHERE vb.user_id = ? AND (vb.year = ? OR (vb.balance_type = 'LONG_SERVICE' AND vb.expires_at IS NULL))
|
||||
ORDER BY vt.priority ASC, vt.type_name ASC
|
||||
`, [userId, year]);
|
||||
return rows;
|
||||
@@ -24,9 +26,12 @@ const vacationBalanceModel = {
|
||||
const [rows] = await db.query(`
|
||||
SELECT
|
||||
vb.*,
|
||||
vb.balance_type,
|
||||
su.name as user_name,
|
||||
su.username,
|
||||
su.hire_date,
|
||||
su.department_id,
|
||||
COALESCE(d.department_name, '미배정') as department_name,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority,
|
||||
@@ -34,7 +39,8 @@ const vacationBalanceModel = {
|
||||
FROM sp_vacation_balances vb
|
||||
INNER JOIN sso_users su ON vb.user_id = su.user_id
|
||||
INNER JOIN vacation_types vt ON vb.vacation_type_id = vt.id
|
||||
WHERE vb.year = ? AND su.is_active = 1
|
||||
LEFT JOIN departments d ON su.department_id = d.department_id
|
||||
WHERE (vb.year = ? OR (vb.balance_type = 'LONG_SERVICE' AND vb.expires_at IS NULL)) AND su.is_active = 1
|
||||
ORDER BY su.name ASC, vt.priority ASC
|
||||
`, [year]);
|
||||
return rows;
|
||||
@@ -42,14 +48,16 @@ const vacationBalanceModel = {
|
||||
|
||||
async allocate(data) {
|
||||
const db = getPool();
|
||||
const balanceType = data.balance_type || 'AUTO';
|
||||
const expiresAt = data.expires_at || null;
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO sp_vacation_balances (user_id, vacation_type_id, year, total_days, used_days, notes, created_by)
|
||||
VALUES (?, ?, ?, ?, 0, ?, ?)
|
||||
INSERT INTO sp_vacation_balances (user_id, vacation_type_id, year, total_days, used_days, notes, created_by, balance_type, expires_at)
|
||||
VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_days = VALUES(total_days),
|
||||
notes = VALUES(notes),
|
||||
updated_at = NOW()
|
||||
`, [data.user_id, data.vacation_type_id, data.year, data.total_days, data.notes || null, data.created_by]);
|
||||
`, [data.user_id, data.vacation_type_id, data.year, data.total_days, data.notes || null, data.created_by, balanceType, expiresAt]);
|
||||
return result;
|
||||
},
|
||||
|
||||
|
||||
55
tksupport/api/models/vacationDashboardModel.js
Normal file
55
tksupport/api/models/vacationDashboardModel.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { getPool } = require('../middleware/auth');
|
||||
|
||||
const vacationDashboardModel = {
|
||||
async getSummary(year) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(`
|
||||
SELECT
|
||||
d.department_id, COALESCE(d.department_name, '미배정') as department_name,
|
||||
COUNT(DISTINCT su.user_id) as employee_count,
|
||||
AVG(vb.total_days - vb.used_days) as avg_remaining,
|
||||
SUM(CASE WHEN (vb.total_days - vb.used_days) <= 2 THEN 1 ELSE 0 END) as low_balance_count
|
||||
FROM sso_users su
|
||||
LEFT JOIN departments d ON su.department_id = d.department_id
|
||||
LEFT JOIN sp_vacation_balances vb ON su.user_id = vb.user_id
|
||||
AND vb.year = ? AND vb.balance_type = 'AUTO'
|
||||
WHERE su.is_active = 1
|
||||
GROUP BY d.department_id
|
||||
`, [year]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getEmployeeList(year, filters = {}) {
|
||||
const db = getPool();
|
||||
let query = `
|
||||
SELECT su.user_id, su.name, su.username, su.hire_date, su.department_id,
|
||||
COALESCE(d.department_name, '미배정') as department_name,
|
||||
vb.id, vb.balance_type, vb.total_days, vb.used_days,
|
||||
(vb.total_days - vb.used_days) as remaining_days,
|
||||
vb.expires_at, vt.type_name, vt.type_code
|
||||
FROM sso_users su
|
||||
LEFT JOIN departments d ON su.department_id = d.department_id
|
||||
LEFT JOIN sp_vacation_balances vb ON su.user_id = vb.user_id
|
||||
AND (vb.year = ? OR (vb.balance_type = 'LONG_SERVICE' AND vb.expires_at IS NULL))
|
||||
LEFT JOIN vacation_types vt ON vb.vacation_type_id = vt.id
|
||||
WHERE su.is_active = 1
|
||||
`;
|
||||
const params = [year];
|
||||
|
||||
if (filters.department_id) {
|
||||
query += ' AND su.department_id = ?';
|
||||
params.push(filters.department_id);
|
||||
}
|
||||
if (filters.search_name) {
|
||||
query += ' AND su.name LIKE ?';
|
||||
params.push(`%${filters.search_name}%`);
|
||||
}
|
||||
|
||||
query += ' ORDER BY su.name ASC';
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
return rows;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationDashboardModel;
|
||||
@@ -14,11 +14,13 @@ const vacationRequestModel = {
|
||||
vr.*,
|
||||
su.name as user_name,
|
||||
su.username,
|
||||
COALESCE(d.department_name, '미배정') as department_name,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code,
|
||||
reviewer.name as reviewer_name
|
||||
FROM sp_vacation_requests vr
|
||||
INNER JOIN sso_users su ON vr.user_id = su.user_id
|
||||
LEFT JOIN departments d ON su.department_id = d.department_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN sso_users reviewer ON vr.reviewed_by = reviewer.user_id
|
||||
WHERE 1=1
|
||||
@@ -45,6 +47,10 @@ const vacationRequestModel = {
|
||||
query += ' AND vr.vacation_type_id = ?';
|
||||
params.push(filters.vacation_type_id);
|
||||
}
|
||||
if (filters.department_id) {
|
||||
query += ' AND su.department_id = ?';
|
||||
params.push(filters.department_id);
|
||||
}
|
||||
|
||||
query += ' ORDER BY vr.created_at DESC';
|
||||
|
||||
@@ -59,11 +65,13 @@ const vacationRequestModel = {
|
||||
vr.*,
|
||||
su.name as user_name,
|
||||
su.username,
|
||||
COALESCE(d.department_name, '미배정') as department_name,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code,
|
||||
reviewer.name as reviewer_name
|
||||
FROM sp_vacation_requests vr
|
||||
INNER JOIN sso_users su ON vr.user_id = su.user_id
|
||||
LEFT JOIN departments d ON su.department_id = d.department_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN sso_users reviewer ON vr.reviewed_by = reviewer.user_id
|
||||
WHERE vr.request_id = ?
|
||||
@@ -113,10 +121,12 @@ const vacationRequestModel = {
|
||||
vr.*,
|
||||
su.name as user_name,
|
||||
su.username,
|
||||
COALESCE(d.department_name, '미배정') as department_name,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code
|
||||
FROM sp_vacation_requests vr
|
||||
INNER JOIN sso_users su ON vr.user_id = su.user_id
|
||||
LEFT JOIN departments d ON su.department_id = d.department_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
WHERE vr.status = 'pending'
|
||||
ORDER BY vr.created_at ASC
|
||||
@@ -128,17 +138,21 @@ const vacationRequestModel = {
|
||||
const db = getPool();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sqlFile = path.join(__dirname, '..', 'db', 'migrations', '001_create_sp_tables.sql');
|
||||
const sql = fs.readFileSync(sqlFile, 'utf8');
|
||||
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
|
||||
for (const stmt of statements) {
|
||||
try {
|
||||
await db.query(stmt);
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_FIELDNAME' || err.code === 'ER_TABLE_EXISTS_ERROR') {
|
||||
// Already migrated
|
||||
} else {
|
||||
throw err;
|
||||
const migrationFiles = ['001_create_sp_tables.sql', '002_section_c_additions.sql'];
|
||||
for (const file of migrationFiles) {
|
||||
const sqlFile = path.join(__dirname, '..', 'db', 'migrations', file);
|
||||
if (!fs.existsSync(sqlFile)) continue;
|
||||
const sql = fs.readFileSync(sqlFile, 'utf8');
|
||||
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
|
||||
for (const stmt of statements) {
|
||||
try {
|
||||
await db.query(stmt);
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_FIELDNAME' || err.code === 'ER_TABLE_EXISTS_ERROR') {
|
||||
// Already migrated
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
tksupport/api/routes/companyHolidayRoutes.js
Normal file
13
tksupport/api/routes/companyHolidayRoutes.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { requireAuth, requireSupportTeam } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/companyHolidayController');
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.get('/holidays', ctrl.getHolidays);
|
||||
router.post('/holidays', requireSupportTeam, ctrl.createHoliday);
|
||||
router.delete('/holidays/:id', requireSupportTeam, ctrl.deleteHoliday);
|
||||
router.post('/holidays/:id/apply-deduction', requireSupportTeam, ctrl.applyDeduction);
|
||||
|
||||
module.exports = router;
|
||||
10
tksupport/api/routes/vacationDashboardRoutes.js
Normal file
10
tksupport/api/routes/vacationDashboardRoutes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { requireAuth, requireSupportTeam } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/vacationDashboardController');
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.get('/', requireSupportTeam, ctrl.getDashboard);
|
||||
|
||||
module.exports = router;
|
||||
@@ -20,6 +20,9 @@ router.get('/pending', requireAdmin, ctrl.getPending);
|
||||
router.patch('/requests/:id/approve', requireAdmin, ctrl.approveRequest);
|
||||
router.patch('/requests/:id/reject', requireAdmin, ctrl.rejectRequest);
|
||||
|
||||
// 내 휴가 현황
|
||||
router.get('/my-status', ctrl.getMyStatus);
|
||||
|
||||
// 잔여일
|
||||
router.get('/balance', ctrl.getMyBalance);
|
||||
router.get('/balance/all', requireAdmin, ctrl.getAllBalances);
|
||||
|
||||
Reference in New Issue
Block a user