diff --git a/docker-compose.yml b/docker-compose.yml index a645dff..12adeb7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -325,6 +325,46 @@ services: networks: - tk-network + # ================================================================= + # Safety Management (tksafety) + # ================================================================= + + tksafety-api: + build: + context: ./tksafety/api + dockerfile: Dockerfile + container_name: tk-tksafety-api + restart: unless-stopped + ports: + - "30500:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - DB_HOST=mariadb + - DB_PORT=3306 + - DB_USER=${MYSQL_USER:-hyungi_user} + - DB_PASSWORD=${MYSQL_PASSWORD} + - DB_NAME=${MYSQL_DATABASE:-hyungi} + - SSO_JWT_SECRET=${SSO_JWT_SECRET} + depends_on: + mariadb: + condition: service_healthy + networks: + - tk-network + + tksafety-web: + build: + context: ./tksafety/web + dockerfile: Dockerfile + container_name: tk-tksafety-web + restart: unless-stopped + ports: + - "30580:80" + depends_on: + - tksafety-api + networks: + - tk-network + # ================================================================= # AI Service — 맥미니로 이전됨 (~/docker/tk-ai-service/) # ================================================================= @@ -386,6 +426,7 @@ services: - system2-web - system3-web - tkpurchase-web + - tksafety-web networks: - tk-network diff --git a/scripts/migration-purchase-safety.sql b/scripts/migration-purchase-safety.sql new file mode 100644 index 0000000..4b17e42 --- /dev/null +++ b/scripts/migration-purchase-safety.sql @@ -0,0 +1,146 @@ +-- migration-purchase-safety.sql +-- 협력업체/일용직 관리 및 안전교육 테이블 마이그레이션 +-- MariaDB용, 재실행 안전 (IF NOT EXISTS / ADD COLUMN IF NOT EXISTS) +-- 생성일: 2026-03-12 + +-- ============================================================ +-- 1. sso_users 테이블에 협력업체 관련 컬럼 추가 +-- ============================================================ +ALTER TABLE sso_users + ADD COLUMN IF NOT EXISTS partner_company_id INT DEFAULT NULL + COMMENT '협력업체 소속 시 partner_companies.id, 내부직원은 NULL'; + +ALTER TABLE sso_users + ADD COLUMN IF NOT EXISTS account_expires_at DATETIME DEFAULT NULL + COMMENT '협력업체 계정 만료일, 내부직원은 NULL'; + +-- 외래키는 IF NOT EXISTS 구문이 없으므로 프로시저로 안전하게 추가 +DELIMITER // +DROP PROCEDURE IF EXISTS __add_fk_sso_users_partner_company// +CREATE PROCEDURE __add_fk_sso_users_partner_company() +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = DATABASE() + AND TABLE_NAME = 'sso_users' + AND CONSTRAINT_NAME = 'fk_sso_users_partner_company' + ) THEN + ALTER TABLE sso_users + ADD CONSTRAINT fk_sso_users_partner_company + FOREIGN KEY (partner_company_id) REFERENCES partner_companies(id) + ON DELETE SET NULL; + END IF; +END// +DELIMITER ; +CALL __add_fk_sso_users_partner_company(); +DROP PROCEDURE IF EXISTS __add_fk_sso_users_partner_company; + +-- ============================================================ +-- 2. day_labor_requests (일용직 작업 요청) +-- ============================================================ +CREATE TABLE IF NOT EXISTS day_labor_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + requester_id INT NOT NULL COMMENT 'sso_users.user_id', + department_id INT, + work_date DATE NOT NULL, + worker_count INT NOT NULL DEFAULT 1, + work_description TEXT, + workplace_name VARCHAR(100), + status ENUM('pending','approved','rejected','completed') DEFAULT 'pending', + approved_by INT, + approved_at DATETIME, + safety_reported BOOLEAN DEFAULT FALSE, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_day_labor_work_date (work_date), + INDEX idx_day_labor_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- 3. partner_schedules (협력업체 작업 일정) +-- ============================================================ +CREATE TABLE IF NOT EXISTS partner_schedules ( + id INT AUTO_INCREMENT PRIMARY KEY, + company_id INT NOT NULL, + work_date DATE NOT NULL, + work_description TEXT, + workplace_name VARCHAR(100), + expected_workers INT DEFAULT 1, + registered_by INT NOT NULL, + status ENUM('scheduled','in_progress','completed','cancelled') DEFAULT 'scheduled', + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_partner_sched_work_date (work_date), + INDEX idx_partner_sched_company_date (company_id, work_date), + CONSTRAINT fk_partner_schedules_company + FOREIGN KEY (company_id) REFERENCES partner_companies(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- 4. partner_work_checkins (협력업체 출퇴근 체크) +-- ============================================================ +CREATE TABLE IF NOT EXISTS partner_work_checkins ( + id INT AUTO_INCREMENT PRIMARY KEY, + schedule_id INT NOT NULL, + company_id INT NOT NULL, + checked_by INT NOT NULL, + check_in_time DATETIME, + check_out_time DATETIME, + worker_names TEXT, + actual_worker_count INT, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_checkin_schedule_time (schedule_id, check_in_time), + CONSTRAINT fk_checkins_schedule + FOREIGN KEY (schedule_id) REFERENCES partner_schedules(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- 5. daily_work_reports (일일 작업 보고) +-- ============================================================ +CREATE TABLE IF NOT EXISTS daily_work_reports ( + id INT AUTO_INCREMENT PRIMARY KEY, + schedule_id INT NOT NULL, + checkin_id INT NOT NULL, + company_id INT NOT NULL, + report_date DATE NOT NULL, + reporter_id INT NOT NULL, + actual_workers INT, + work_content TEXT, + progress_rate TINYINT CHECK (progress_rate BETWEEN 0 AND 100), + issues TEXT, + next_plan TEXT, + confirmed_by INT, + confirmed_at DATETIME, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_daily_report_date (report_date), + INDEX idx_daily_report_schedule (schedule_id), + CONSTRAINT fk_daily_reports_schedule + FOREIGN KEY (schedule_id) REFERENCES partner_schedules(id), + CONSTRAINT fk_daily_reports_checkin + FOREIGN KEY (checkin_id) REFERENCES partner_work_checkins(id), + CONSTRAINT fk_daily_reports_company + FOREIGN KEY (company_id) REFERENCES partner_companies(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- 6. safety_education_reports (안전교육 보고) +-- ============================================================ +CREATE TABLE IF NOT EXISTS safety_education_reports ( + id INT AUTO_INCREMENT PRIMARY KEY, + target_type ENUM('day_labor','partner_schedule','manual') NOT NULL, + target_id INT, + education_date DATE NOT NULL, + educator VARCHAR(50), + attendees JSON, + status ENUM('planned','completed','cancelled') DEFAULT 'planned', + notes TEXT, + registered_by INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_safety_edu_date (education_date), + INDEX idx_safety_edu_target (target_type, target_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/sso-auth-service/controllers/authController.js b/sso-auth-service/controllers/authController.js index 5175d64..243387f 100644 --- a/sso-auth-service/controllers/authController.js +++ b/sso-auth-service/controllers/authController.js @@ -32,6 +32,7 @@ function createTokenPayload(user) { role: user.role, access_level: user.role, sub: user.username, + partner_company_id: user.partner_company_id || null, system_access: { system1: user.system1_access, system2: user.system2_access, @@ -65,6 +66,11 @@ async function login(req, res, next) { return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' }); } + // 협력업체 계정 만료일 체크 + if (user.account_expires_at && new Date(user.account_expires_at) < new Date()) { + return res.status(401).json({ success: false, error: '계정이 만료되었습니다. 관리자에게 문의하세요.' }); + } + const valid = await userModel.verifyPassword(password, user.password_hash); if (!valid) { await redis.incr(attemptKey); @@ -126,6 +132,11 @@ async function loginForm(req, res, next) { return res.status(401).json({ detail: 'Incorrect username or password' }); } + // 협력업체 계정 만료일 체크 + if (user.account_expires_at && new Date(user.account_expires_at) < new Date()) { + return res.status(401).json({ detail: '계정이 만료되었습니다' }); + } + const valid = await userModel.verifyPassword(password, user.password_hash); if (!valid) { return res.status(401).json({ detail: 'Incorrect username or password' }); diff --git a/system2-report/web/js/app-init.js b/system2-report/web/js/app-init.js index 9cef057..97fc474 100644 --- a/system2-report/web/js/app-init.js +++ b/system2-report/web/js/app-init.js @@ -54,6 +54,21 @@ return; } + // 협력업체 계정 차단 (JWT에서 partner_company_id 확인) + var token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'); + if (token) { + try { + var payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); + if (payload.partner_company_id) { + var h = window.location.hostname; + window.location.href = h.includes('technicalkorea.net') + ? 'https://tkpurchase.technicalkorea.net/partner-portal.html' + : window.location.protocol + '//' + h + ':30480/partner-portal.html'; + return; + } + } catch(e) { /* ignore decode errors */ } + } + // 인증 성공 — 루프 카운터 리셋 + localStorage 백업 sessionStorage.removeItem(REDIRECT_KEY); var token = window.getSSOToken ? window.getSSOToken() : null; diff --git a/tkpurchase/api/controllers/checkinController.js b/tkpurchase/api/controllers/checkinController.js new file mode 100644 index 0000000..0631082 --- /dev/null +++ b/tkpurchase/api/controllers/checkinController.js @@ -0,0 +1,80 @@ +const checkinModel = require('../models/checkinModel'); + +// 일정별 체크인 목록 +async function list(req, res) { + try { + const rows = await checkinModel.findBySchedule(req.params.scheduleId); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('Checkin list error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 내 체크인 (협력업체 포탈 - 오늘) +async function myCheckins(req, res) { + try { + const companyId = req.user.partner_company_id; + if (!companyId) { + return res.status(403).json({ success: false, error: '협력업체 계정이 아닙니다' }); + } + const rows = await checkinModel.findTodayByCompany(companyId); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('Checkin myCheckins error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 체크인 +async function checkIn(req, res) { + try { + const { schedule_id, company_id, worker_names, actual_worker_count } = req.body; + if (!schedule_id) { + return res.status(400).json({ success: false, error: '일정을 선택해주세요' }); + } + const resolvedCompanyId = company_id || req.user.partner_company_id; + if (!resolvedCompanyId) { + return res.status(400).json({ success: false, error: '업체 정보가 필요합니다' }); + } + const data = { + schedule_id, + company_id: resolvedCompanyId, + checked_by: req.user.user_id || req.user.id, + worker_names, + actual_worker_count, + notes: req.body.notes + }; + const row = await checkinModel.checkIn(data); + res.status(201).json({ success: true, data: row }); + } catch (err) { + console.error('Checkin checkIn error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 체크아웃 +async function checkOut(req, res) { + try { + const row = await checkinModel.checkOut(req.params.id); + if (!row) return res.status(404).json({ success: false, error: '체크인 기록을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Checkin checkOut error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 체크인 정보 수정 +async function update(req, res) { + try { + const row = await checkinModel.update(req.params.id, req.body); + if (!row) return res.status(404).json({ success: false, error: '체크인 기록을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Checkin update error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +module.exports = { list, myCheckins, checkIn, checkOut, update }; diff --git a/tkpurchase/api/controllers/dayLaborController.js b/tkpurchase/api/controllers/dayLaborController.js new file mode 100644 index 0000000..0d18530 --- /dev/null +++ b/tkpurchase/api/controllers/dayLaborController.js @@ -0,0 +1,126 @@ +const dayLaborModel = require('../models/dayLaborModel'); +const { getPool } = require('../models/partnerModel'); + +// 일용직 요청 목록 +async function list(req, res) { + try { + const { status, date_from, date_to, department_id, page, limit } = req.query; + const rows = await dayLaborModel.findAll({ + status, + date_from, + date_to, + department_id: department_id ? parseInt(department_id) : undefined, + page: page ? parseInt(page) : 1, + limit: limit ? parseInt(limit) : 50 + }); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('DayLabor list error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일용직 요청 상세 +async function getById(req, res) { + try { + const row = await dayLaborModel.findById(req.params.id); + if (!row) return res.status(404).json({ success: false, error: '요청을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('DayLabor get error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일용직 요청 등록 +async function create(req, res) { + try { + const { work_date, worker_count } = req.body; + if (!work_date) { + return res.status(400).json({ success: false, error: '작업일은 필수입니다' }); + } + if (!worker_count || worker_count < 1) { + return res.status(400).json({ success: false, error: '작업인원은 1명 이상이어야 합니다' }); + } + const data = { + ...req.body, + requester_id: req.user.user_id || req.user.id + }; + const row = await dayLaborModel.create(data); + res.status(201).json({ success: true, data: row }); + } catch (err) { + console.error('DayLabor create error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일용직 요청 승인 +async function approve(req, res) { + try { + const id = req.params.id; + const approvedBy = req.user.user_id || req.user.id; + const row = await dayLaborModel.approve(id, approvedBy); + if (!row) return res.status(404).json({ success: false, error: '요청을 찾을 수 없습니다' }); + + // 승인 시 안전교육 보고서 자동 생성 + if (row.status === 'approved') { + try { + const db = getPool(); + await db.query( + `INSERT INTO safety_education_reports (target_type, target_id, education_date, status, registered_by) + VALUES ('day_labor', ?, ?, 'planned', ?)`, + [id, row.work_date, approvedBy] + ); + } catch (safetyErr) { + console.error('Safety report auto-create error:', safetyErr); + // 안전교육 보고서 생성 실패해도 승인은 유지 + } + } + + res.json({ success: true, data: row }); + } catch (err) { + console.error('DayLabor approve error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일용직 요청 거절 +async function reject(req, res) { + try { + const id = req.params.id; + const approvedBy = req.user.user_id || req.user.id; + const { notes } = req.body; + const row = await dayLaborModel.reject(id, approvedBy, notes); + if (!row) return res.status(404).json({ success: false, error: '요청을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('DayLabor reject error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일용직 요청 완료 +async function complete(req, res) { + try { + const row = await dayLaborModel.complete(req.params.id); + if (!row) return res.status(404).json({ success: false, error: '요청을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('DayLabor complete error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 통계 +async function stats(req, res) { + try { + const { date_from, date_to } = req.query; + const rows = await dayLaborModel.getStats({ date_from, date_to }); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('DayLabor stats error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +module.exports = { list, getById, create, approve, reject, complete, stats }; diff --git a/tkpurchase/api/controllers/partnerAccountController.js b/tkpurchase/api/controllers/partnerAccountController.js new file mode 100644 index 0000000..d685690 --- /dev/null +++ b/tkpurchase/api/controllers/partnerAccountController.js @@ -0,0 +1,80 @@ +const partnerAccountModel = require('../models/partnerAccountModel'); +const { getPool } = require('../models/partnerModel'); + +// 업체별 계정 목록 +async function listByCompany(req, res) { + try { + const rows = await partnerAccountModel.findByCompany(req.params.companyId); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('PartnerAccount listByCompany error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 계정 생성 +async function create(req, res) { + try { + const { username, password, name, partner_company_id, account_expires_at } = req.body; + if (!username || !username.trim()) { + return res.status(400).json({ success: false, error: '아이디는 필수입니다' }); + } + if (!password || password.length < 4) { + return res.status(400).json({ success: false, error: '비밀번호는 4자 이상이어야 합니다' }); + } + if (!name || !name.trim()) { + return res.status(400).json({ success: false, error: '이름은 필수입니다' }); + } + if (!partner_company_id) { + return res.status(400).json({ success: false, error: '업체를 선택해주세요' }); + } + + // 아이디 중복 확인 + const db = getPool(); + const [existing] = await db.query('SELECT user_id FROM sso_users WHERE username = ?', [username]); + if (existing.length > 0) { + return res.status(400).json({ success: false, error: '이미 사용 중인 아이디입니다' }); + } + + const account = await partnerAccountModel.create({ + username, password, name, partner_company_id, account_expires_at + }); + + // 기본 권한 부여 + await partnerAccountModel.grantDefaultPermissions(account.user_id); + + res.status(201).json({ success: true, data: account }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ success: false, error: '이미 사용 중인 아이디입니다' }); + } + console.error('PartnerAccount create error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 계정 수정 +async function update(req, res) { + try { + const account = await partnerAccountModel.update(req.params.id, req.body); + if (!account) return res.status(404).json({ success: false, error: '계정을 찾을 수 없습니다' }); + res.json({ success: true, data: account }); + } catch (err) { + console.error('PartnerAccount update error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 계정 비활성화 +async function deactivate(req, res) { + try { + const account = await partnerAccountModel.update(req.params.id, { is_active: false }); + if (!account) return res.status(404).json({ success: false, error: '계정을 찾을 수 없습니다' }); + res.json({ success: true, message: '비활성화 완료' }); + } catch (err) { + console.error('PartnerAccount deactivate error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +module.exports = { listByCompany, create, update, deactivate }; diff --git a/tkpurchase/api/controllers/scheduleController.js b/tkpurchase/api/controllers/scheduleController.js new file mode 100644 index 0000000..9ae4a3b --- /dev/null +++ b/tkpurchase/api/controllers/scheduleController.js @@ -0,0 +1,110 @@ +const scheduleModel = require('../models/scheduleModel'); + +// 일정 목록 +async function list(req, res) { + try { + const { company_id, date_from, date_to, status, page, limit } = req.query; + const rows = await scheduleModel.findAll({ + company_id: company_id ? parseInt(company_id) : undefined, + date_from, + date_to, + status, + page: page ? parseInt(page) : 1, + limit: limit ? parseInt(limit) : 50 + }); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('Schedule list error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일정 상세 +async function getById(req, res) { + try { + const row = await scheduleModel.findById(req.params.id); + if (!row) return res.status(404).json({ success: false, error: '일정을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Schedule get error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 내 일정 (협력업체 포탈) +async function mySchedules(req, res) { + try { + const companyId = req.user.partner_company_id; + if (!companyId) { + return res.status(403).json({ success: false, error: '협력업체 계정이 아닙니다' }); + } + const rows = await scheduleModel.findByCompanyToday(companyId); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('Schedule mySchedules error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일정 등록 +async function create(req, res) { + try { + const { company_id, work_date } = req.body; + if (!company_id) { + return res.status(400).json({ success: false, error: '업체를 선택해주세요' }); + } + if (!work_date) { + return res.status(400).json({ success: false, error: '작업일은 필수입니다' }); + } + const data = { + ...req.body, + registered_by: req.user.user_id || req.user.id + }; + const row = await scheduleModel.create(data); + res.status(201).json({ success: true, data: row }); + } catch (err) { + console.error('Schedule create error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일정 수정 +async function update(req, res) { + try { + const row = await scheduleModel.update(req.params.id, req.body); + if (!row) return res.status(404).json({ success: false, error: '일정을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Schedule update error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일정 상태 변경 +async function updateStatus(req, res) { + try { + const { status } = req.body; + if (!status) { + return res.status(400).json({ success: false, error: '상태값은 필수입니다' }); + } + const row = await scheduleModel.updateStatus(req.params.id, status); + if (!row) return res.status(404).json({ success: false, error: '일정을 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Schedule updateStatus error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 일정 삭제 +async function deleteSchedule(req, res) { + try { + await scheduleModel.deleteSchedule(req.params.id); + res.json({ success: true, message: '삭제 완료' }); + } catch (err) { + console.error('Schedule delete error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +module.exports = { list, getById, mySchedules, create, update, updateStatus, deleteSchedule }; diff --git a/tkpurchase/api/controllers/workReportController.js b/tkpurchase/api/controllers/workReportController.js new file mode 100644 index 0000000..7cb3d28 --- /dev/null +++ b/tkpurchase/api/controllers/workReportController.js @@ -0,0 +1,118 @@ +const workReportModel = require('../models/workReportModel'); +const checkinModel = require('../models/checkinModel'); + +// 작업보고 목록 +async function list(req, res) { + try { + const { company_id, date_from, date_to, schedule_id, confirmed, page, limit } = req.query; + const rows = await workReportModel.findAll({ + company_id: company_id ? parseInt(company_id) : undefined, + date_from, + date_to, + schedule_id: schedule_id ? parseInt(schedule_id) : undefined, + confirmed, + page: page ? parseInt(page) : 1, + limit: limit ? parseInt(limit) : 50 + }); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('WorkReport list error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 작업보고 상세 +async function getById(req, res) { + try { + const row = await workReportModel.findById(req.params.id); + if (!row) return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('WorkReport get error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 내 작업보고 (협력업체 포탈) +async function myReports(req, res) { + try { + const companyId = req.user.partner_company_id; + if (!companyId) { + return res.status(403).json({ success: false, error: '협력업체 계정이 아닙니다' }); + } + const { date_from, date_to, page, limit } = req.query; + const rows = await workReportModel.findAll({ + company_id: companyId, + date_from, + date_to, + page: page ? parseInt(page) : 1, + limit: limit ? parseInt(limit) : 50 + }); + res.json({ success: true, data: rows }); + } catch (err) { + console.error('WorkReport myReports error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 작업보고 등록 +async function create(req, res) { + try { + const { checkin_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입니다' }); + } + } + + const resolvedCompanyId = company_id || req.user.partner_company_id; + if (!resolvedCompanyId) { + return res.status(400).json({ success: false, error: '업체 정보가 필요합니다' }); + } + + const data = { + ...req.body, + company_id: resolvedCompanyId, + reporter_id: req.user.user_id || req.user.id + }; + const row = await workReportModel.create(data); + res.status(201).json({ success: true, data: row }); + } catch (err) { + console.error('WorkReport create error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 작업보고 수정 +async function update(req, res) { + try { + const row = await workReportModel.update(req.params.id, req.body); + if (!row) return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('WorkReport update error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 작업보고 확인 +async function confirm(req, res) { + try { + const confirmedBy = req.user.user_id || req.user.id; + const row = await workReportModel.confirm(req.params.id, confirmedBy); + if (!row) return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' }); + res.json({ success: true, data: row }); + } catch (err) { + console.error('WorkReport confirm error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +module.exports = { list, getById, myReports, create, update, confirm }; diff --git a/tkpurchase/api/index.js b/tkpurchase/api/index.js index b5f6c9e..588f223 100644 --- a/tkpurchase/api/index.js +++ b/tkpurchase/api/index.js @@ -2,8 +2,11 @@ const express = require('express'); const cors = require('cors'); const cron = require('node-cron'); const partnerRoutes = require('./routes/partnerRoutes'); -const dailyVisitRoutes = require('./routes/dailyVisitRoutes'); -const dailyVisitModel = require('./models/dailyVisitModel'); +const dayLaborRoutes = require('./routes/dayLaborRoutes'); +const scheduleRoutes = require('./routes/scheduleRoutes'); +const checkinRoutes = require('./routes/checkinRoutes'); +const workReportRoutes = require('./routes/workReportRoutes'); +const partnerAccountRoutes = require('./routes/partnerAccountRoutes'); const app = express(); const PORT = process.env.PORT || 3000; @@ -14,6 +17,7 @@ const allowedOrigins = [ 'https://tkqc.technicalkorea.net', 'https://tkuser.technicalkorea.net', 'https://tkpurchase.technicalkorea.net', + 'https://tksafety.technicalkorea.net', ]; if (process.env.NODE_ENV === 'development') { allowedOrigins.push('http://localhost:30080', 'http://localhost:30480'); @@ -34,7 +38,11 @@ app.get('/health', (req, res) => { // Routes app.use('/api/partners', partnerRoutes); -app.use('/api/daily-visits', dailyVisitRoutes); +app.use('/api/day-labor', dayLaborRoutes); +app.use('/api/schedules', scheduleRoutes); +app.use('/api/checkins', checkinRoutes); +app.use('/api/work-reports', workReportRoutes); +app.use('/api/partner-accounts', partnerAccountRoutes); // 404 app.use((req, res) => { @@ -50,16 +58,6 @@ app.use((err, req, res, next) => { }); }); -// 자정 자동 체크아웃 (매일 23:59 KST) -cron.schedule('59 23 * * *', async () => { - try { - const result = await dailyVisitModel.autoCheckoutAll(); - console.log(`Auto checkout: ${result.affectedRows} visits`); - } catch (e) { - console.error('Auto checkout failed:', e); - } -}, { timezone: 'Asia/Seoul' }); - app.listen(PORT, () => { console.log(`tkpurchase-api running on port ${PORT}`); }); diff --git a/tkpurchase/api/models/checkinModel.js b/tkpurchase/api/models/checkinModel.js new file mode 100644 index 0000000..a4291a1 --- /dev/null +++ b/tkpurchase/api/models/checkinModel.js @@ -0,0 +1,67 @@ +const { getPool } = require('./partnerModel'); + +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 + 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 = ? + ORDER BY pc.check_in_time DESC`, [scheduleId]); + return rows; +} + +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 + 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]); + return rows[0] || null; +} + +async function findTodayByCompany(companyId) { + const db = getPool(); + const [rows] = await db.query( + `SELECT pc.*, ps.work_description, ps.workplace_name + FROM partner_checkins pc + LEFT JOIN partner_schedules ps ON pc.schedule_id = ps.id + WHERE pc.company_id = ? AND DATE(pc.check_in_time) = CURDATE() + ORDER BY pc.check_in_time DESC`, [companyId]); + return rows; +} + +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) + VALUES (?, ?, ?, NOW(), ?, ?, ?)`, + [data.schedule_id, data.company_id, data.checked_by, + data.worker_names ? JSON.stringify(data.worker_names) : null, + data.actual_worker_count || null, data.notes || null]); + return findById(result.insertId); +} + +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]); + return findById(id); +} + +async function update(id, data) { + const db = getPool(); + const fields = []; + const values = []; + if (data.worker_names !== undefined) { fields.push('worker_names = ?'); values.push(data.worker_names ? JSON.stringify(data.worker_names) : null); } + if (data.actual_worker_count !== undefined) { fields.push('actual_worker_count = ?'); values.push(data.actual_worker_count || null); } + 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); + return findById(id); +} + +module.exports = { findBySchedule, findById, findTodayByCompany, checkIn, checkOut, update }; diff --git a/tkpurchase/api/models/dayLaborModel.js b/tkpurchase/api/models/dayLaborModel.js new file mode 100644 index 0000000..7692683 --- /dev/null +++ b/tkpurchase/api/models/dayLaborModel.js @@ -0,0 +1,85 @@ +const { getPool } = require('./partnerModel'); + +async function findAll({ status, date_from, date_to, department_id, page = 1, limit = 50 } = {}) { + const db = getPool(); + let sql = `SELECT dlr.*, su.name AS requester_name, sa.name AS approver_name, d.department_name + FROM day_labor_requests dlr + LEFT JOIN sso_users su ON dlr.requester_id = su.user_id + LEFT JOIN sso_users sa ON dlr.approved_by = sa.user_id + LEFT JOIN departments d ON dlr.department_id = d.department_id + WHERE 1=1`; + const params = []; + if (status) { sql += ' AND dlr.status = ?'; params.push(status); } + if (date_from) { sql += ' AND dlr.work_date >= ?'; params.push(date_from); } + if (date_to) { sql += ' AND dlr.work_date <= ?'; params.push(date_to); } + if (department_id) { sql += ' AND dlr.department_id = ?'; params.push(department_id); } + sql += ' ORDER BY dlr.work_date DESC, dlr.created_at DESC'; + const offset = (page - 1) * limit; + sql += ' LIMIT ? OFFSET ?'; + params.push(limit, offset); + const [rows] = await db.query(sql, params); + return rows; +} + +async function findById(id) { + const db = getPool(); + const [rows] = await db.query( + `SELECT dlr.*, su.name AS requester_name, sa.name AS approver_name, d.department_name + FROM day_labor_requests dlr + LEFT JOIN sso_users su ON dlr.requester_id = su.user_id + LEFT JOIN sso_users sa ON dlr.approved_by = sa.user_id + LEFT JOIN departments d ON dlr.department_id = d.department_id + WHERE dlr.id = ?`, [id]); + return rows[0] || null; +} + +async function create(data) { + const db = getPool(); + const [result] = await db.query( + `INSERT INTO day_labor_requests (requester_id, department_id, work_date, worker_count, work_description, workplace_name, notes) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [data.requester_id, data.department_id || null, data.work_date, data.worker_count || 1, + data.work_description || null, data.workplace_name || null, data.notes || null]); + return findById(result.insertId); +} + +async function approve(id, approvedBy) { + const db = getPool(); + await db.query( + `UPDATE day_labor_requests SET status = 'approved', approved_by = ?, approved_at = NOW() WHERE id = ? AND status = 'pending'`, + [approvedBy, id]); + return findById(id); +} + +async function reject(id, approvedBy, notes) { + const db = getPool(); + await db.query( + `UPDATE day_labor_requests SET status = 'rejected', approved_by = ?, approved_at = NOW(), notes = CONCAT(IFNULL(notes,''), ?, '') WHERE id = ? AND status = 'pending'`, + [approvedBy, notes ? '\n[거절사유] ' + notes : '', id]); + return findById(id); +} + +async function complete(id) { + const db = getPool(); + await db.query(`UPDATE day_labor_requests SET status = 'completed' WHERE id = ? AND status = 'approved'`, [id]); + return findById(id); +} + +async function markSafetyReported(id) { + const db = getPool(); + await db.query(`UPDATE day_labor_requests SET safety_reported = TRUE WHERE id = ?`, [id]); +} + +async function getStats({ date_from, date_to } = {}) { + const db = getPool(); + let dateFilter = ''; + const params = []; + if (date_from) { dateFilter += ' AND work_date >= ?'; params.push(date_from); } + if (date_to) { dateFilter += ' AND work_date <= ?'; params.push(date_to); } + const [rows] = await db.query( + `SELECT status, COUNT(*) AS cnt, SUM(worker_count) AS total_workers + FROM day_labor_requests WHERE 1=1 ${dateFilter} GROUP BY status`, params); + return rows; +} + +module.exports = { findAll, findById, create, approve, reject, complete, markSafetyReported, getStats }; diff --git a/tkpurchase/api/models/partnerAccountModel.js b/tkpurchase/api/models/partnerAccountModel.js new file mode 100644 index 0000000..b7e9948 --- /dev/null +++ b/tkpurchase/api/models/partnerAccountModel.js @@ -0,0 +1,62 @@ +const { getPool } = require('./partnerModel'); +const bcrypt = require('bcrypt'); + +async function findByCompany(companyId) { + const db = getPool(); + const [rows] = await db.query( + `SELECT user_id, username, name, role, partner_company_id, account_expires_at, is_active, created_at + FROM sso_users WHERE partner_company_id = ? + ORDER BY name`, [companyId]); + return rows; +} + +async function findById(userId) { + const db = getPool(); + const [rows] = await db.query( + `SELECT user_id, username, name, role, partner_company_id, account_expires_at, is_active, created_at + FROM sso_users WHERE user_id = ?`, [userId]); + return rows[0] || null; +} + +async function create(data) { + const db = getPool(); + const hash = await bcrypt.hash(data.password, 10); + const [result] = await db.query( + `INSERT INTO sso_users (username, password_hash, name, role, partner_company_id, account_expires_at, is_active) + VALUES (?, ?, ?, 'user', ?, ?, TRUE)`, + [data.username, hash, data.name, data.partner_company_id, + data.account_expires_at || null]); + return findById(result.insertId); +} + +async function update(userId, data) { + const db = getPool(); + const fields = []; + const values = []; + if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); } + if (data.account_expires_at !== undefined) { fields.push('account_expires_at = ?'); values.push(data.account_expires_at || null); } + if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); } + if (data.password) { + const hash = await bcrypt.hash(data.password, 10); + fields.push('password_hash = ?'); + values.push(hash); + } + if (fields.length === 0) return findById(userId); + values.push(userId); + await db.query(`UPDATE sso_users SET ${fields.join(', ')} WHERE user_id = ?`, values); + return findById(userId); +} + +async function grantDefaultPermissions(userId) { + const db = getPool(); + const pages = ['purchasing_partner_portal', 'purchasing_partner_checkin']; + for (const page of pages) { + await db.query( + `INSERT INTO user_page_permissions (user_id, page_name, can_access) + VALUES (?, ?, TRUE) + ON DUPLICATE KEY UPDATE can_access = TRUE`, + [userId, page]); + } +} + +module.exports = { findByCompany, findById, create, update, grantDefaultPermissions }; diff --git a/tkpurchase/api/models/scheduleModel.js b/tkpurchase/api/models/scheduleModel.js new file mode 100644 index 0000000..65adbdf --- /dev/null +++ b/tkpurchase/api/models/scheduleModel.js @@ -0,0 +1,84 @@ +const { getPool } = require('./partnerModel'); + +async function findAll({ company_id, date_from, date_to, status, page = 1, limit = 50 } = {}) { + const db = getPool(); + let sql = `SELECT ps.*, pc.company_name, su.name AS registered_by_name + FROM partner_schedules ps + LEFT JOIN partner_companies pc ON ps.company_id = pc.id + LEFT JOIN sso_users su ON ps.registered_by = su.user_id + WHERE 1=1`; + const params = []; + if (company_id) { sql += ' AND ps.company_id = ?'; params.push(company_id); } + if (date_from) { sql += ' AND ps.work_date >= ?'; params.push(date_from); } + if (date_to) { sql += ' AND ps.work_date <= ?'; params.push(date_to); } + if (status) { sql += ' AND ps.status = ?'; params.push(status); } + sql += ' ORDER BY ps.work_date DESC, ps.created_at DESC'; + const offset = (page - 1) * limit; + sql += ' LIMIT ? OFFSET ?'; + params.push(limit, offset); + const [rows] = await db.query(sql, params); + return rows; +} + +async function findById(id) { + const db = getPool(); + const [rows] = await db.query( + `SELECT ps.*, pc.company_name, su.name AS registered_by_name + FROM partner_schedules ps + LEFT JOIN partner_companies pc ON ps.company_id = pc.id + LEFT JOIN sso_users su ON ps.registered_by = su.user_id + WHERE ps.id = ?`, [id]); + return rows[0] || null; +} + +async function findByCompanyToday(companyId) { + const db = getPool(); + const [rows] = await db.query( + `SELECT ps.*, pc.company_name + FROM partner_schedules ps + LEFT JOIN partner_companies pc ON ps.company_id = pc.id + WHERE ps.company_id = ? AND ps.work_date = CURDATE() + ORDER BY ps.created_at DESC`, [companyId]); + return rows; +} + +async function create(data) { + const db = getPool(); + const [result] = await db.query( + `INSERT INTO partner_schedules (company_id, work_date, work_description, workplace_name, expected_workers, registered_by, notes) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [data.company_id, data.work_date, data.work_description || null, + data.workplace_name || null, data.expected_workers || null, + data.registered_by, data.notes || null]); + return findById(result.insertId); +} + +async function update(id, data) { + const db = getPool(); + const fields = []; + const values = []; + if (data.company_id !== undefined) { fields.push('company_id = ?'); values.push(data.company_id); } + if (data.work_date !== undefined) { fields.push('work_date = ?'); values.push(data.work_date); } + if (data.work_description !== undefined) { fields.push('work_description = ?'); values.push(data.work_description || null); } + if (data.workplace_name !== undefined) { fields.push('workplace_name = ?'); values.push(data.workplace_name || null); } + if (data.expected_workers !== undefined) { fields.push('expected_workers = ?'); values.push(data.expected_workers || null); } + if (data.notes !== undefined) { fields.push('notes = ?'); values.push(data.notes || null); } + if (data.status !== undefined) { fields.push('status = ?'); values.push(data.status); } + if (fields.length === 0) return findById(id); + values.push(id); + await db.query(`UPDATE partner_schedules SET ${fields.join(', ')} WHERE id = ?`, values); + return findById(id); +} + +async function updateStatus(id, status) { + const db = getPool(); + await db.query('UPDATE partner_schedules SET status = ? WHERE id = ?', [status, id]); + return findById(id); +} + +async function deleteSchedule(id) { + const db = getPool(); + await db.query('DELETE FROM partner_schedules WHERE id = ?', [id]); +} + +module.exports = { findAll, findById, findByCompanyToday, create, update, updateStatus, deleteSchedule }; diff --git a/tkpurchase/api/models/workReportModel.js b/tkpurchase/api/models/workReportModel.js new file mode 100644 index 0000000..1b90801 --- /dev/null +++ b/tkpurchase/api/models/workReportModel.js @@ -0,0 +1,87 @@ +const { getPool } = require('./partnerModel'); + +async function findAll({ company_id, date_from, date_to, schedule_id, confirmed, page = 1, limit = 50 } = {}) { + 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 + 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 + LEFT JOIN sso_users su_confirmer ON wr.confirmed_by = su_confirmer.user_id + WHERE 1=1`; + const params = []; + if (company_id) { sql += ' AND wr.company_id = ?'; params.push(company_id); } + if (date_from) { sql += ' AND wr.report_date >= ?'; params.push(date_from); } + if (date_to) { sql += ' AND wr.report_date <= ?'; params.push(date_to); } + if (schedule_id) { sql += ' AND wr.schedule_id = ?'; params.push(schedule_id); } + if (confirmed === 'true' || confirmed === '1') { sql += ' AND wr.confirmed_by IS NOT NULL'; } + if (confirmed === 'false' || confirmed === '0') { sql += ' AND wr.confirmed_by IS NULL'; } + sql += ' ORDER BY wr.report_date DESC, wr.created_at DESC'; + const offset = (page - 1) * limit; + sql += ' LIMIT ? OFFSET ?'; + params.push(limit, offset); + const [rows] = await db.query(sql, params); + return rows; +} + +async function findById(id) { + const db = getPool(); + 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 + 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 + LEFT JOIN sso_users su_confirmer ON wr.confirmed_by = su_confirmer.user_id + WHERE wr.id = ?`, [id]); + return rows[0] || null; +} + +async function findByCheckin(checkinId) { + const db = getPool(); + const [rows] = await db.query( + `SELECT wr.*, pc.company_name + FROM partner_work_reports wr + LEFT JOIN partner_companies pc ON wr.company_id = pc.id + WHERE wr.checkin_id = ?`, [checkinId]); + return rows[0] || null; +} + +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) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [data.schedule_id || null, data.checkin_id || null, data.company_id, + data.report_date, data.reporter_id, data.actual_workers || null, + data.work_content || null, data.progress_rate || null, + data.issues || null, data.next_plan || null]); + return findById(result.insertId); +} + +async function update(id, data) { + const db = getPool(); + const fields = []; + const values = []; + if (data.actual_workers !== undefined) { fields.push('actual_workers = ?'); values.push(data.actual_workers || null); } + if (data.work_content !== undefined) { fields.push('work_content = ?'); values.push(data.work_content || null); } + if (data.progress_rate !== undefined) { fields.push('progress_rate = ?'); values.push(data.progress_rate || null); } + if (data.issues !== undefined) { fields.push('issues = ?'); values.push(data.issues || null); } + 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); + 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', + [confirmedBy, id]); + return findById(id); +} + +module.exports = { findAll, findById, findByCheckin, create, update, confirm }; diff --git a/tkpurchase/api/package.json b/tkpurchase/api/package.json index d6c8466..5a2c700 100644 --- a/tkpurchase/api/package.json +++ b/tkpurchase/api/package.json @@ -8,6 +8,7 @@ "dev": "node --watch index.js" }, "dependencies": { + "bcrypt": "^5.1.1", "cors": "^2.8.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.0", diff --git a/tkpurchase/api/routes/checkinRoutes.js b/tkpurchase/api/routes/checkinRoutes.js new file mode 100644 index 0000000..255e8cf --- /dev/null +++ b/tkpurchase/api/routes/checkinRoutes.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const { requireAuth } = require('../middleware/auth'); +const ctrl = require('../controllers/checkinController'); + +router.use(requireAuth); + +router.get('/schedule/:scheduleId', ctrl.list); +router.get('/my', ctrl.myCheckins); // partner portal +router.post('/', ctrl.checkIn); // partner can do this +router.put('/:id/checkout', ctrl.checkOut); +router.put('/:id', ctrl.update); + +module.exports = router; diff --git a/tkpurchase/api/routes/dayLaborRoutes.js b/tkpurchase/api/routes/dayLaborRoutes.js new file mode 100644 index 0000000..5af5595 --- /dev/null +++ b/tkpurchase/api/routes/dayLaborRoutes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const { requireAuth, requirePage } = require('../middleware/auth'); +const ctrl = require('../controllers/dayLaborController'); + +router.use(requireAuth); + +router.get('/', ctrl.list); +router.get('/stats', ctrl.stats); +router.get('/:id', ctrl.getById); +router.post('/', ctrl.create); // any authenticated user can request +router.put('/:id/approve', requirePage('purchasing_daylabor'), ctrl.approve); +router.put('/:id/reject', requirePage('purchasing_daylabor'), ctrl.reject); +router.put('/:id/complete', requirePage('purchasing_daylabor'), ctrl.complete); + +module.exports = router; diff --git a/tkpurchase/api/routes/partnerAccountRoutes.js b/tkpurchase/api/routes/partnerAccountRoutes.js new file mode 100644 index 0000000..5007e77 --- /dev/null +++ b/tkpurchase/api/routes/partnerAccountRoutes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const { requireAuth, requirePage } = require('../middleware/auth'); +const ctrl = require('../controllers/partnerAccountController'); + +router.use(requireAuth); + +router.get('/company/:companyId', requirePage('purchasing_accounts'), ctrl.listByCompany); +router.post('/', requirePage('purchasing_accounts'), ctrl.create); +router.put('/:id', requirePage('purchasing_accounts'), ctrl.update); +router.delete('/:id', requirePage('purchasing_accounts'), ctrl.deactivate); + +module.exports = router; diff --git a/tkpurchase/api/routes/partnerRoutes.js b/tkpurchase/api/routes/partnerRoutes.js index ae8e1df..57c9a41 100644 --- a/tkpurchase/api/routes/partnerRoutes.js +++ b/tkpurchase/api/routes/partnerRoutes.js @@ -1,20 +1,14 @@ const express = require('express'); const router = express.Router(); -const { requireAuth, requirePage } = require('../middleware/auth'); +const { requireAuth } = require('../middleware/auth'); const ctrl = require('../controllers/partnerController'); router.use(requireAuth); +// Read-only: CRUD는 tkuser로 이관됨 router.get('/search', ctrl.searchCompanies); router.get('/', ctrl.list); router.get('/:id', ctrl.getById); -router.post('/', requirePage('purchasing_partner'), ctrl.create); -router.put('/:id', requirePage('purchasing_partner'), ctrl.update); -router.delete('/:id', requirePage('purchasing_partner'), ctrl.deactivate); - router.get('/:id/workers', ctrl.listWorkers); -router.post('/:id/workers', requirePage('purchasing_partner'), ctrl.createWorker); -router.put('/workers/:id', requirePage('purchasing_partner'), ctrl.updateWorker); -router.delete('/workers/:id', requirePage('purchasing_partner'), ctrl.deactivateWorker); module.exports = router; diff --git a/tkpurchase/api/routes/scheduleRoutes.js b/tkpurchase/api/routes/scheduleRoutes.js new file mode 100644 index 0000000..33feb92 --- /dev/null +++ b/tkpurchase/api/routes/scheduleRoutes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const { requireAuth, requirePage } = require('../middleware/auth'); +const ctrl = require('../controllers/scheduleController'); + +router.use(requireAuth); + +router.get('/', ctrl.list); +router.get('/my', ctrl.mySchedules); // partner portal +router.get('/:id', ctrl.getById); +router.post('/', requirePage('purchasing_schedule'), ctrl.create); +router.put('/:id', requirePage('purchasing_schedule'), ctrl.update); +router.put('/:id/status', requirePage('purchasing_schedule'), ctrl.updateStatus); +router.delete('/:id', requirePage('purchasing_schedule'), ctrl.deleteSchedule); + +module.exports = router; diff --git a/tkpurchase/api/routes/workReportRoutes.js b/tkpurchase/api/routes/workReportRoutes.js new file mode 100644 index 0000000..9e54ce9 --- /dev/null +++ b/tkpurchase/api/routes/workReportRoutes.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const { requireAuth, requirePage } = require('../middleware/auth'); +const ctrl = require('../controllers/workReportController'); + +router.use(requireAuth); + +router.get('/', ctrl.list); +router.get('/my', ctrl.myReports); // partner portal +router.get('/:id', ctrl.getById); +router.post('/', ctrl.create); // partner can create +router.put('/:id', ctrl.update); +router.put('/:id/confirm', requirePage('purchasing_workreport'), ctrl.confirm); + +module.exports = router; diff --git a/tkpurchase/web/Dockerfile b/tkpurchase/web/Dockerfile index 7dbe51e..dde77e5 100644 --- a/tkpurchase/web/Dockerfile +++ b/tkpurchase/web/Dockerfile @@ -2,7 +2,11 @@ FROM nginx:alpine COPY nginx.conf /etc/nginx/conf.d/default.conf COPY index.html /usr/share/nginx/html/index.html -COPY partner.html /usr/share/nginx/html/partner.html +COPY daylabor.html /usr/share/nginx/html/daylabor.html +COPY schedule.html /usr/share/nginx/html/schedule.html +COPY workreport.html /usr/share/nginx/html/workreport.html +COPY accounts.html /usr/share/nginx/html/accounts.html +COPY partner-portal.html /usr/share/nginx/html/partner-portal.html COPY static/ /usr/share/nginx/html/static/ EXPOSE 80 diff --git a/tkpurchase/web/accounts.html b/tkpurchase/web/accounts.html new file mode 100644 index 0000000..55261e9 --- /dev/null +++ b/tkpurchase/web/accounts.html @@ -0,0 +1,137 @@ + + +
+ + +로딩 중...
+왼쪽에서 업체를 선택하세요
+| 신청일 | +작업일 | +신청자 | + +인원 | +작업장 | +상태 | +관리 | +|
|---|---|---|---|---|---|---|---|
| 로딩 중... | |||||||