feat: tkpurchase 시스템 Phase 1 - 협력업체 마스터 + 당일 방문 관리

신규 독립 시스템 tkpurchase (구매/방문 관리) 구축:
- 협력업체 CRUD + 소속 작업자 관리 (마스터 데이터 소유)
- 당일 방문 등록/체크인/체크아웃 + 일괄 마감
- 업체 자동완성, CSV 내보내기, 집계 통계
- 자정 자동 체크아웃 (node-cron)
- tkuser 협력업체 읽기 전용 탭 + 권한 그리드(tkpurchase-perms) 추가
- docker-compose에 tkpurchase-api/web 서비스 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-12 15:45:37 +09:00
parent 5b1b89254c
commit 281f5d35d1
29 changed files with 2641 additions and 7 deletions

View File

@@ -59,6 +59,13 @@ const SYSTEM3_PAGES = {
]
};
const TKPURCHASE_PAGES = {
'구매 관리': [
{ key: 'purchasing_visit', title: '방문 관리', icon: 'fa-door-open', def: false },
{ key: 'purchasing_partner', title: '협력업체 관리', icon: 'fa-building', def: false },
]
};
/* ===== Permissions Tab State ===== */
let permissionsTabLoaded = false;
@@ -184,7 +191,7 @@ document.getElementById('permissionUserSelect').addEventListener('change', async
async function loadUserPermissions(userId) {
currentPermissions = {};
currentPermSources = {};
const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES };
const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES, ...TKPURCHASE_PAGES };
Object.values(allDefs).flat().forEach(p => { currentPermissions[p.key] = p.def; currentPermSources[p.key] = 'default'; });
try {
const result = await api(`/permissions/users/${userId}/effective-permissions`);
@@ -200,6 +207,7 @@ async function loadUserPermissions(userId) {
function renderPermissionGrid() {
renderSystemPerms('s1-perms', SYSTEM1_PAGES, 'blue');
renderSystemPerms('s3-perms', SYSTEM3_PAGES, 'purple');
renderSystemPerms('tkpurchase-perms', TKPURCHASE_PAGES, 'green');
}
function sourceLabel(src) {
@@ -277,7 +285,8 @@ function toggleGroupAll(groupId, checked) {
}
function toggleSystemAll(prefix, checked) {
const containerId = prefix === 's1' ? 's1-perms' : 's3-perms';
const containerMap = { s1: 's1-perms', s3: 's3-perms', tkpurchase: 'tkpurchase-perms' };
const containerId = containerMap[prefix] || prefix + '-perms';
document.querySelectorAll(`#${containerId} input[type="checkbox"]`).forEach(cb => {
cb.checked = checked;
onPermChange(cb);
@@ -294,7 +303,7 @@ document.getElementById('savePermissionsBtn').addEventListener('click', async ()
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>저장 중...';
try {
const allPages = [...Object.values(SYSTEM1_PAGES).flat(), ...Object.values(SYSTEM3_PAGES).flat()];
const allPages = [...Object.values(SYSTEM1_PAGES).flat(), ...Object.values(SYSTEM3_PAGES).flat(), ...Object.values(TKPURCHASE_PAGES).flat()];
const permissions = allPages.map(p => {
const cb = document.getElementById('perm_' + p.key);
return { page_name: p.key, can_access: cb ? cb.checked : false };
@@ -342,7 +351,7 @@ document.addEventListener('DOMContentLoaded', () => {
async function loadDeptPermissions(deptId) {
deptPermissions = {};
const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES };
const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES, ...TKPURCHASE_PAGES };
Object.values(allDefs).flat().forEach(p => { deptPermissions[p.key] = p.def; });
try {
const result = await api(`/permissions/departments/${deptId}/permissions`);
@@ -353,6 +362,7 @@ async function loadDeptPermissions(deptId) {
function renderDeptPermissionGrid() {
renderDeptSystemPerms('dept-s1-perms', SYSTEM1_PAGES, 'blue');
renderDeptSystemPerms('dept-s3-perms', SYSTEM3_PAGES, 'purple');
renderDeptSystemPerms('dept-tkpurchase-perms', TKPURCHASE_PAGES, 'green');
}
function renderDeptSystemPerms(containerId, pageDef, color) {
@@ -415,7 +425,8 @@ function toggleDeptGroupAll(groupId, checked) {
}
function toggleDeptSystemAll(prefix, checked) {
const containerId = prefix === 's1' ? 'dept-s1-perms' : 'dept-s3-perms';
const containerMap = { s1: 'dept-s1-perms', s3: 'dept-s3-perms', tkpurchase: 'dept-tkpurchase-perms' };
const containerId = containerMap[prefix] || 'dept-' + prefix + '-perms';
document.querySelectorAll(`#${containerId} input[type="checkbox"]`).forEach(cb => {
cb.checked = checked;
onDeptPermChange(cb);
@@ -430,7 +441,7 @@ async function saveDeptPermissions() {
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>저장 중...';
try {
const allPages = [...Object.values(SYSTEM1_PAGES).flat(), ...Object.values(SYSTEM3_PAGES).flat()];
const allPages = [...Object.values(SYSTEM1_PAGES).flat(), ...Object.values(SYSTEM3_PAGES).flat(), ...Object.values(TKPURCHASE_PAGES).flat()];
const permissions = allPages.map(p => {
const cb = document.getElementById('dperm_' + p.key);
return { page_name: p.key, can_access: cb ? cb.checked : false };