diff --git a/user-management/api/controllers/partnerController.js b/user-management/api/controllers/partnerController.js index 479330f..17e9905 100644 --- a/user-management/api/controllers/partnerController.js +++ b/user-management/api/controllers/partnerController.js @@ -119,7 +119,32 @@ async function deactivateWorker(req, res) { } } +async function getDeleteInfo(req, res) { + try { + const company = await partnerModel.findById(req.params.id); + if (!company) return res.status(404).json({ success: false, error: '업체를 찾을 수 없습니다' }); + const info = await partnerModel.getDeleteInfo(req.params.id); + res.json({ success: true, data: { company_name: company.company_name, ...info } }); + } catch (err) { + console.error('Partner getDeleteInfo error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +async function permanentDelete(req, res) { + try { + const company = await partnerModel.findById(req.params.id); + if (!company) return res.status(404).json({ success: false, error: '업체를 찾을 수 없습니다' }); + await partnerModel.permanentDelete(req.params.id, req.user.id); + res.json({ success: true, message: `"${company.company_name}" 업체가 완전히 삭제되었습니다` }); + } catch (err) { + console.error('Partner permanentDelete error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + module.exports = { list, getById, create, update, deactivate, - listWorkers, createWorker, updateWorker, deactivateWorker + listWorkers, createWorker, updateWorker, deactivateWorker, + getDeleteInfo, permanentDelete }; diff --git a/user-management/api/models/partnerModel.js b/user-management/api/models/partnerModel.js index 98d1b3f..a3169ef 100644 --- a/user-management/api/models/partnerModel.js +++ b/user-management/api/models/partnerModel.js @@ -109,7 +109,108 @@ async function deactivateWorker(id) { await db.query('UPDATE partner_workers SET is_active = FALSE WHERE id = ?', [id]); } +async function getDeleteInfo(companyId) { + const db = getPool(); + const [[workers]] = await db.query('SELECT COUNT(*) as cnt FROM partner_workers WHERE company_id = ?', [companyId]); + const [[schedules]] = await db.query('SELECT COUNT(*) as cnt FROM partner_schedules WHERE company_id = ?', [companyId]); + const [[checkins]] = await db.query('SELECT COUNT(*) as cnt FROM partner_work_checkins WHERE company_id = ?', [companyId]); + const [[reports]] = await db.query('SELECT COUNT(*) as cnt FROM partner_work_reports WHERE company_id = ?', [companyId]); + const [[visits]] = await db.query('SELECT COUNT(*) as cnt FROM daily_visits WHERE company_id = ?', [companyId]); + const [[accounts]] = await db.query('SELECT COUNT(*) as cnt FROM sso_users WHERE partner_company_id = ?', [companyId]); + + // 삭제 차단 조건: 해당 업체 SSO 계정이 구매 이력을 가지고 있는지 + const [userRows] = await db.query('SELECT id FROM sso_users WHERE partner_company_id = ?', [companyId]); + const userIds = userRows.map(r => r.id); + let purchaseRequests = 0; + let purchases = 0; + if (userIds.length > 0) { + const [[pr]] = await db.query('SELECT COUNT(*) as cnt FROM purchase_requests WHERE requester_id IN (?)', [userIds]); + const [[pu]] = await db.query('SELECT COUNT(*) as cnt FROM purchases WHERE purchaser_id IN (?)', [userIds]); + purchaseRequests = pr.cnt; + purchases = pu.cnt; + } + + return { + workers: workers.cnt, + schedules: schedules.cnt, + checkins: checkins.cnt, + reports: reports.cnt, + visits: visits.cnt, + accounts: accounts.cnt, + purchaseRequests, + purchases + }; +} + +async function permanentDelete(companyId, requestUserId) { + const db = getPool(); + + // 사전 체크: 구매 이력 확인 + const [userRows] = await db.query('SELECT id FROM sso_users WHERE partner_company_id = ?', [companyId]); + const userIds = userRows.map(r => r.id); + if (userIds.length > 0) { + const [[pr]] = await db.query('SELECT COUNT(*) as cnt FROM purchase_requests WHERE requester_id IN (?)', [userIds]); + const [[pu]] = await db.query('SELECT COUNT(*) as cnt FROM purchases WHERE purchaser_id IN (?)', [userIds]); + if (pr.cnt > 0 || pu.cnt > 0) { + throw new Error('구매 이력이 있어 삭제할 수 없습니다. 비활성화를 이용해주세요.'); + } + } + + // 작업자 ID 조회 + const [workerRows] = await db.query('SELECT id FROM partner_workers WHERE company_id = ?', [companyId]); + const workerIds = workerRows.map(r => r.id); + + const conn = await db.getConnection(); + try { + await conn.beginTransaction(); + + // 1. work_report_workers에서 해당 업체 작업자 참조 SET NULL + if (workerIds.length > 0) { + await conn.query('UPDATE work_report_workers SET partner_worker_id = NULL WHERE partner_worker_id IN (?)', [workerIds]); + } + + // 2. partner_work_reports 삭제 (→ work_report_workers CASCADE 자동) + await conn.query('DELETE FROM partner_work_reports WHERE company_id = ?', [companyId]); + + // 3. partner_work_checkins 삭제 + await conn.query('DELETE FROM partner_work_checkins WHERE company_id = ?', [companyId]); + + // 4. partner_schedules 삭제 + await conn.query('DELETE FROM partner_schedules WHERE company_id = ?', [companyId]); + + // 5. daily_visit_workers에서 해당 업체 작업자 참조 SET NULL + if (workerIds.length > 0) { + await conn.query('UPDATE daily_visit_workers SET partner_worker_id = NULL WHERE partner_worker_id IN (?)', [workerIds]); + } + + // 6. daily_visits는 SET NULL 자동 (방문기록 보존) + + // 7. partner_workers 삭제 + await conn.query('DELETE FROM partner_workers WHERE company_id = ?', [companyId]); + + // 8. 협력업체 SSO 계정 처리 + if (userIds.length > 0) { + await conn.query('UPDATE sp_vacation_requests SET reviewed_by = NULL WHERE reviewed_by IN (?)', [userIds]); + await conn.query('DELETE FROM sp_vacation_requests WHERE user_id IN (?)', [userIds]); + await conn.query('UPDATE sp_vacation_balances SET created_by = ? WHERE created_by IN (?)', [requestUserId, userIds]); + await conn.query('DELETE FROM sp_vacation_balances WHERE user_id IN (?)', [userIds]); + await conn.query('DELETE FROM sso_users WHERE partner_company_id = ?', [companyId]); + } + + // 9. partner_companies 삭제 + await conn.query('DELETE FROM partner_companies WHERE id = ?', [companyId]); + + await conn.commit(); + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } +} + module.exports = { findAll, findById, create, update, deactivate, - findWorkersByCompany, findWorkerById, createWorker, updateWorker, deactivateWorker + findWorkersByCompany, findWorkerById, createWorker, updateWorker, deactivateWorker, + getDeleteInfo, permanentDelete }; diff --git a/user-management/api/routes/partnerRoutes.js b/user-management/api/routes/partnerRoutes.js index a3d8c72..95b7921 100644 --- a/user-management/api/routes/partnerRoutes.js +++ b/user-management/api/routes/partnerRoutes.js @@ -6,6 +6,8 @@ const ctrl = require('../controllers/partnerController'); router.use(requireAuth); router.get('/', ctrl.list); +router.get('/:id/delete-info', requireAdmin, ctrl.getDeleteInfo); +router.delete('/:id/permanent', requireAdmin, ctrl.permanentDelete); router.get('/:id', ctrl.getById); router.post('/', requireAdmin, ctrl.create); router.put('/:id', requireAdmin, ctrl.update); diff --git a/user-management/web/index.html b/user-management/web/index.html index 5c73df9..3fc0023 100644 --- a/user-management/web/index.html +++ b/user-management/web/index.html @@ -2009,7 +2009,7 @@ - + diff --git a/user-management/web/static/js/tkuser-partners.js b/user-management/web/static/js/tkuser-partners.js index 587c0ba..7e9dfa9 100644 --- a/user-management/web/static/js/tkuser-partners.js +++ b/user-management/web/static/js/tkuser-partners.js @@ -56,6 +56,7 @@ function renderPartnersListTkuser() { ${isAdmin ? `
${p.is_active ? `` : ''} +
` : ''} `; }).join(''); @@ -82,7 +83,10 @@ function renderPartnerDetailTkuser(p) { const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role); document.getElementById('partnerDetailTkuser').innerHTML = `
-

${escHtml(p.company_name)}

+
+

${escHtml(p.company_name)}

+ ${isAdmin ? `` : ''} +
사업자번호: ${escHtml(p.business_number) || '-'}
대표자: ${escHtml(p.representative) || '-'}
@@ -121,6 +125,39 @@ function renderPartnerDetailTkuser(p) {
`; } +/* ===== 업체 완전삭제 (admin) ===== */ +async function hardDeletePartnerTkuser(id) { + try { + const r = await api(`/partners/${id}/delete-info`); + const info = r.data; + if (info.purchaseRequests > 0 || info.purchases > 0) { + alert(`"${info.company_name}" 업체는 구매 이력이 있어 삭제할 수 없습니다.\n(구매요청 ${info.purchaseRequests}건, 구매 ${info.purchases}건)\n\n비활성화를 이용해주세요.`); + return; + } + const lines = []; + if (info.workers > 0) lines.push(`작업자 ${info.workers}명`); + if (info.schedules > 0) lines.push(`스케줄 ${info.schedules}건`); + if (info.checkins > 0) lines.push(`출근기록 ${info.checkins}건`); + if (info.reports > 0) lines.push(`작업보고 ${info.reports}건`); + if (info.visits > 0) lines.push(`방문기록 ${info.visits}건 (보존, 업체연결 해제)`); + if (info.accounts > 0) lines.push(`SSO 계정 ${info.accounts}개`); + const summary = lines.length > 0 ? `\n\n삭제될 데이터:\n- ${lines.join('\n- ')}` : '\n\n관련 데이터가 없습니다.'; + const input = prompt(`"${info.company_name}" 업체를 완전히 삭제합니다.\n이 작업은 되돌릴 수 없습니다.${summary}\n\n계속하려면 "삭제"를 입력하세요:`); + if (input !== '삭제') { + if (input !== null) showToast('삭제가 취소되었습니다', 'error'); + return; + } + await api(`/partners/${id}/permanent`, { method: 'DELETE' }); + showToast('업체가 완전히 삭제되었습니다'); + await loadPartnersList(); + if (selectedPartnerIdTkuser === id) { + document.getElementById('partnerDetailTkuser').classList.add('hidden'); + document.getElementById('partnerEmptyTkuser').classList.remove('hidden'); + selectedPartnerIdTkuser = null; + } + } catch (e) { showToast(e.message, 'error'); } +} + /* ===== 업체 등록 ===== */ function openAddPartnerTkuser() { document.getElementById('addPartnerModalTkuser').classList.remove('hidden'); } function closeAddPartnerTkuser() { document.getElementById('addPartnerModalTkuser').classList.add('hidden'); document.getElementById('addPartnerFormTkuser').reset(); }