fix: 배포 후 버그 수정 — 테이블명/컬럼명 불일치, navbar active, API 검증 강화, 대시보드 통계 라우트 추가
- checkinModel: partner_checkins → partner_work_checkins, countActive() 추가 - workReportModel: partner_work_reports → daily_work_reports - partner-portal: check_out_at/check_in_at → check_out_time/check_in_time - checkinModel findTodayByCompany: LEFT JOIN has_work_report - tkpurchase-core/tksafety-core: navbar match '' 제거 - checkinController: checkOut에 업무현황 검증, stats() 추가 - workReportController: checkin_id 필수 + schedule 일치 검증 - checkinRoutes: GET / 대시보드 통계 라우트 추가 - nginx.conf: visit.html → tksafety 리다이렉트 - migration-purchase-safety.sql: DDL 동기화 - migration-purchase-safety-patch.sql: 신규 패치 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
11
scripts/migration-purchase-safety-patch.sql
Normal file
11
scripts/migration-purchase-safety-patch.sql
Normal file
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'] },
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
` : `
|
||||
<div class="text-sm text-emerald-600 mb-1">
|
||||
<i class="fas fa-check-circle mr-1"></i>체크인 완료 (${formatTime(checkin.check_in_at)})
|
||||
<i class="fas fa-check-circle mr-1"></i>체크인 완료 (${formatTime(checkin.check_in_time)})
|
||||
· ${checkin.actual_worker_count || 0}명
|
||||
</div>
|
||||
`}
|
||||
@@ -161,7 +161,7 @@ async function renderScheduleCards() {
|
||||
${isCheckedOut ? `
|
||||
<div class="p-5 border-t bg-gray-50">
|
||||
<div class="text-sm text-blue-600">
|
||||
<i class="fas fa-check-double mr-1"></i>작업 종료 완료 (${formatTime(checkin.check_out_at)})
|
||||
<i class="fas fa-check-double mr-1"></i>작업 종료 완료 (${formatTime(checkin.check_out_time)})
|
||||
</div>
|
||||
${hasReport ? '<div class="text-xs text-emerald-600 mt-1"><i class="fas fa-clipboard-check mr-1"></i>업무현황 제출 완료</div>' : ''}
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user