diff --git a/scripts/migration-purchase-safety-patch.sql b/scripts/migration-purchase-safety-patch.sql new file mode 100644 index 0000000..093bbf3 --- /dev/null +++ b/scripts/migration-purchase-safety-patch.sql @@ -0,0 +1,11 @@ +-- migration-purchase-safety-patch.sql +-- 배포 후 스키마 보완 패치 +-- 생성일: 2026-03-13 + +-- 4-a. check_in_time NOT NULL (체크인 시 시간은 항상 존재) +ALTER TABLE partner_work_checkins + MODIFY check_in_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- 4-b. 일정당 하루 1건 업무보고 보장 +ALTER TABLE daily_work_reports + ADD UNIQUE INDEX uq_schedule_report_date (schedule_id, report_date); diff --git a/scripts/migration-purchase-safety.sql b/scripts/migration-purchase-safety.sql index 4b17e42..3bccef2 100644 --- a/scripts/migration-purchase-safety.sql +++ b/scripts/migration-purchase-safety.sql @@ -86,7 +86,7 @@ CREATE TABLE IF NOT EXISTS partner_work_checkins ( schedule_id INT NOT NULL, company_id INT NOT NULL, checked_by INT NOT NULL, - check_in_time DATETIME, + check_in_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, check_out_time DATETIME, worker_names TEXT, actual_worker_count INT, @@ -118,6 +118,7 @@ CREATE TABLE IF NOT EXISTS daily_work_reports ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_daily_report_date (report_date), INDEX idx_daily_report_schedule (schedule_id), + UNIQUE INDEX uq_schedule_report_date (schedule_id, report_date), CONSTRAINT fk_daily_reports_schedule FOREIGN KEY (schedule_id) REFERENCES partner_schedules(id), CONSTRAINT fk_daily_reports_checkin diff --git a/tkpurchase/api/controllers/checkinController.js b/tkpurchase/api/controllers/checkinController.js index 0631082..28a05ce 100644 --- a/tkpurchase/api/controllers/checkinController.js +++ b/tkpurchase/api/controllers/checkinController.js @@ -1,4 +1,5 @@ const checkinModel = require('../models/checkinModel'); +const workReportModel = require('../models/workReportModel'); // 일정별 체크인 목록 async function list(req, res) { @@ -56,6 +57,10 @@ async function checkIn(req, res) { // 체크아웃 async function checkOut(req, res) { try { + const report = await workReportModel.findByCheckin(req.params.id); + if (!report) { + return res.status(400).json({ success: false, error: '업무현황을 먼저 입력해주세요' }); + } const row = await checkinModel.checkOut(req.params.id); if (!row) return res.status(404).json({ success: false, error: '체크인 기록을 찾을 수 없습니다' }); res.json({ success: true, data: row }); @@ -77,4 +82,15 @@ async function update(req, res) { } } -module.exports = { list, myCheckins, checkIn, checkOut, update }; +// 대시보드 통계 +async function stats(req, res) { + try { + const activeCount = await checkinModel.countActive(); + res.json({ success: true, total: activeCount }); + } catch (err) { + console.error('Checkin stats error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +module.exports = { list, myCheckins, checkIn, checkOut, update, stats }; diff --git a/tkpurchase/api/controllers/workReportController.js b/tkpurchase/api/controllers/workReportController.js index 7cb3d28..8e5e511 100644 --- a/tkpurchase/api/controllers/workReportController.js +++ b/tkpurchase/api/controllers/workReportController.js @@ -58,18 +58,23 @@ async function myReports(req, res) { // 작업보고 등록 async function create(req, res) { try { - const { checkin_id, company_id, report_date } = req.body; + const { checkin_id, schedule_id, company_id, report_date } = req.body; if (!report_date) { return res.status(400).json({ success: false, error: '보고일은 필수입니다' }); } - // checkin_id가 있으면 유효성 검증 - if (checkin_id) { - const checkin = await checkinModel.findById(checkin_id); - if (!checkin) { - return res.status(400).json({ success: false, error: '유효하지 않은 체크인 ID입니다' }); - } + if (!checkin_id) { + return res.status(400).json({ success: false, error: '체크인 ID는 필수입니다' }); + } + + const checkin = await checkinModel.findById(checkin_id); + if (!checkin) { + return res.status(400).json({ success: false, error: '유효하지 않은 체크인 ID입니다' }); + } + + if (schedule_id && checkin.schedule_id !== schedule_id) { + return res.status(400).json({ success: false, error: '체크인의 일정 정보가 일치하지 않습니다' }); } const resolvedCompanyId = company_id || req.user.partner_company_id; diff --git a/tkpurchase/api/models/checkinModel.js b/tkpurchase/api/models/checkinModel.js index a4291a1..100e4b1 100644 --- a/tkpurchase/api/models/checkinModel.js +++ b/tkpurchase/api/models/checkinModel.js @@ -4,7 +4,7 @@ async function findBySchedule(scheduleId) { const db = getPool(); const [rows] = await db.query( `SELECT pc.*, pco.company_name, su.name AS checked_by_name - FROM partner_checkins pc + FROM partner_work_checkins pc LEFT JOIN partner_companies pco ON pc.company_id = pco.id LEFT JOIN sso_users su ON pc.checked_by = su.user_id WHERE pc.schedule_id = ? @@ -16,7 +16,7 @@ async function findById(id) { const db = getPool(); const [rows] = await db.query( `SELECT pc.*, pco.company_name, su.name AS checked_by_name - FROM partner_checkins pc + FROM partner_work_checkins pc LEFT JOIN partner_companies pco ON pc.company_id = pco.id LEFT JOIN sso_users su ON pc.checked_by = su.user_id WHERE pc.id = ?`, [id]); @@ -26,9 +26,11 @@ async function findById(id) { async function findTodayByCompany(companyId) { const db = getPool(); const [rows] = await db.query( - `SELECT pc.*, ps.work_description, ps.workplace_name - FROM partner_checkins pc + `SELECT pc.*, ps.work_description, ps.workplace_name, + (dwr.id IS NOT NULL) AS has_work_report + FROM partner_work_checkins pc LEFT JOIN partner_schedules ps ON pc.schedule_id = ps.id + LEFT JOIN daily_work_reports dwr ON dwr.checkin_id = pc.id WHERE pc.company_id = ? AND DATE(pc.check_in_time) = CURDATE() ORDER BY pc.check_in_time DESC`, [companyId]); return rows; @@ -37,7 +39,7 @@ async function findTodayByCompany(companyId) { async function checkIn(data) { const db = getPool(); const [result] = await db.query( - `INSERT INTO partner_checkins (schedule_id, company_id, checked_by, check_in_time, worker_names, actual_worker_count, notes) + `INSERT INTO partner_work_checkins (schedule_id, company_id, checked_by, check_in_time, worker_names, actual_worker_count, notes) VALUES (?, ?, ?, NOW(), ?, ?, ?)`, [data.schedule_id, data.company_id, data.checked_by, data.worker_names ? JSON.stringify(data.worker_names) : null, @@ -47,7 +49,7 @@ async function checkIn(data) { async function checkOut(id) { const db = getPool(); - await db.query('UPDATE partner_checkins SET check_out_time = NOW() WHERE id = ? AND check_out_time IS NULL', [id]); + await db.query('UPDATE partner_work_checkins SET check_out_time = NOW() WHERE id = ? AND check_out_time IS NULL', [id]); return findById(id); } @@ -60,8 +62,16 @@ async function update(id, data) { if (data.notes !== undefined) { fields.push('notes = ?'); values.push(data.notes || null); } if (fields.length === 0) return findById(id); values.push(id); - await db.query(`UPDATE partner_checkins SET ${fields.join(', ')} WHERE id = ?`, values); + await db.query(`UPDATE partner_work_checkins SET ${fields.join(', ')} WHERE id = ?`, values); return findById(id); } -module.exports = { findBySchedule, findById, findTodayByCompany, checkIn, checkOut, update }; +async function countActive() { + const db = getPool(); + const [rows] = await db.query( + `SELECT COUNT(*) AS cnt FROM partner_work_checkins + WHERE check_out_time IS NULL AND DATE(check_in_time) = CURDATE()`); + return rows[0].cnt; +} + +module.exports = { findBySchedule, findById, findTodayByCompany, checkIn, checkOut, update, countActive }; diff --git a/tkpurchase/api/models/workReportModel.js b/tkpurchase/api/models/workReportModel.js index 1b90801..4c8fdac 100644 --- a/tkpurchase/api/models/workReportModel.js +++ b/tkpurchase/api/models/workReportModel.js @@ -4,7 +4,7 @@ async function findAll({ company_id, date_from, date_to, schedule_id, confirmed, const db = getPool(); let sql = `SELECT wr.*, pc.company_name, ps.work_description AS schedule_description, su_reporter.name AS reporter_name, su_confirmer.name AS confirmed_by_name - FROM partner_work_reports wr + FROM daily_work_reports wr LEFT JOIN partner_companies pc ON wr.company_id = pc.id LEFT JOIN partner_schedules ps ON wr.schedule_id = ps.id LEFT JOIN sso_users su_reporter ON wr.reporter_id = su_reporter.user_id @@ -30,7 +30,7 @@ async function findById(id) { const [rows] = await db.query( `SELECT wr.*, pc.company_name, ps.work_description AS schedule_description, su_reporter.name AS reporter_name, su_confirmer.name AS confirmed_by_name - FROM partner_work_reports wr + FROM daily_work_reports wr LEFT JOIN partner_companies pc ON wr.company_id = pc.id LEFT JOIN partner_schedules ps ON wr.schedule_id = ps.id LEFT JOIN sso_users su_reporter ON wr.reporter_id = su_reporter.user_id @@ -43,7 +43,7 @@ async function findByCheckin(checkinId) { const db = getPool(); const [rows] = await db.query( `SELECT wr.*, pc.company_name - FROM partner_work_reports wr + FROM daily_work_reports wr LEFT JOIN partner_companies pc ON wr.company_id = pc.id WHERE wr.checkin_id = ?`, [checkinId]); return rows[0] || null; @@ -52,7 +52,7 @@ async function findByCheckin(checkinId) { async function create(data) { const db = getPool(); const [result] = await db.query( - `INSERT INTO partner_work_reports (schedule_id, checkin_id, company_id, report_date, reporter_id, actual_workers, work_content, progress_rate, issues, next_plan) + `INSERT INTO daily_work_reports (schedule_id, checkin_id, company_id, report_date, reporter_id, actual_workers, work_content, progress_rate, issues, next_plan) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [data.schedule_id || null, data.checkin_id || null, data.company_id, data.report_date, data.reporter_id, data.actual_workers || null, @@ -72,14 +72,14 @@ async function update(id, data) { if (data.next_plan !== undefined) { fields.push('next_plan = ?'); values.push(data.next_plan || null); } if (fields.length === 0) return findById(id); values.push(id); - await db.query(`UPDATE partner_work_reports SET ${fields.join(', ')} WHERE id = ?`, values); + await db.query(`UPDATE daily_work_reports SET ${fields.join(', ')} WHERE id = ?`, values); return findById(id); } async function confirm(id, confirmedBy) { const db = getPool(); await db.query( - 'UPDATE partner_work_reports SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ? AND confirmed_by IS NULL', + 'UPDATE daily_work_reports SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ? AND confirmed_by IS NULL', [confirmedBy, id]); return findById(id); } diff --git a/tkpurchase/api/routes/checkinRoutes.js b/tkpurchase/api/routes/checkinRoutes.js index 255e8cf..204613e 100644 --- a/tkpurchase/api/routes/checkinRoutes.js +++ b/tkpurchase/api/routes/checkinRoutes.js @@ -5,6 +5,7 @@ const ctrl = require('../controllers/checkinController'); router.use(requireAuth); +router.get('/', ctrl.stats); // dashboard stats router.get('/schedule/:scheduleId', ctrl.list); router.get('/my', ctrl.myCheckins); // partner portal router.post('/', ctrl.checkIn); // partner can do this diff --git a/tkpurchase/web/nginx.conf b/tkpurchase/web/nginx.conf index 4e2c51c..44ca615 100644 --- a/tkpurchase/web/nginx.conf +++ b/tkpurchase/web/nginx.conf @@ -34,6 +34,10 @@ server { add_header Cache-Control "public, no-transform"; } + location = /visit.html { + return 301 https://tksafety.technicalkorea.net/; + } + location / { try_files $uri $uri/ /index.html; } diff --git a/tkpurchase/web/static/js/tkpurchase-core.js b/tkpurchase/web/static/js/tkpurchase-core.js index b30641a..4903eb1 100644 --- a/tkpurchase/web/static/js/tkpurchase-core.js +++ b/tkpurchase/web/static/js/tkpurchase-core.js @@ -86,7 +86,7 @@ function doLogout() { function renderNavbar() { const currentPage = location.pathname.replace(/\//g, '') || 'index.html'; const links = [ - { href: '/', icon: 'fa-chart-line', label: '대시보드', match: ['', 'index.html'] }, + { href: '/', icon: 'fa-chart-line', label: '대시보드', match: ['index.html'] }, { href: '/daylabor.html', icon: 'fa-hard-hat', label: '일용공 신청', match: ['daylabor.html'] }, { href: '/schedule.html', icon: 'fa-calendar-alt', label: '작업일정', match: ['schedule.html'] }, { href: '/workreport.html', icon: 'fa-clipboard-list', label: '업무현황', match: ['workreport.html'] }, diff --git a/tkpurchase/web/static/js/tkpurchase-partner-portal.js b/tkpurchase/web/static/js/tkpurchase-partner-portal.js index 36f9c40..0f1b4ab 100644 --- a/tkpurchase/web/static/js/tkpurchase-partner-portal.js +++ b/tkpurchase/web/static/js/tkpurchase-partner-portal.js @@ -44,8 +44,8 @@ async function renderScheduleCards() { container.innerHTML = portalSchedules.map(s => { const checkin = portalCheckins[s.id]; - const isCheckedIn = checkin && !checkin.check_out_at; - const isCheckedOut = checkin && checkin.check_out_at; + const isCheckedIn = checkin && !checkin.check_out_time; + const isCheckedOut = checkin && checkin.check_out_time; const hasReport = checkin && checkin.has_work_report; // Step indicators @@ -108,7 +108,7 @@ async function renderScheduleCards() { ` : `
- 체크인 완료 (${formatTime(checkin.check_in_at)}) + 체크인 완료 (${formatTime(checkin.check_in_time)}) · ${checkin.actual_worker_count || 0}명
`} @@ -161,7 +161,7 @@ async function renderScheduleCards() { ${isCheckedOut ? `
- 작업 종료 완료 (${formatTime(checkin.check_out_at)}) + 작업 종료 완료 (${formatTime(checkin.check_out_time)})
${hasReport ? '
업무현황 제출 완료
' : ''}
diff --git a/tksafety/web/static/js/tksafety-core.js b/tksafety/web/static/js/tksafety-core.js index 251a16e..17e0faf 100644 --- a/tksafety/web/static/js/tksafety-core.js +++ b/tksafety/web/static/js/tksafety-core.js @@ -83,7 +83,7 @@ function doLogout() { function renderNavbar() { const currentPage = location.pathname.replace(/\//g, '') || 'index.html'; const links = [ - { href: '/', icon: 'fa-door-open', label: '방문 관리', match: ['', 'index.html'] }, + { href: '/', icon: 'fa-door-open', label: '방문 관리', match: ['index.html'] }, { href: '/education.html', icon: 'fa-graduation-cap', label: '안전교육', match: ['education.html'] }, ]; const nav = document.getElementById('sideNav');