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:
Hyungi Ahn
2026-03-13 07:22:25 +09:00
parent b800792152
commit efc3c14db5
11 changed files with 77 additions and 29 deletions

View 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'] },

View File

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

View File

@@ -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');