feat: tkuser 통합 관리 서비스 + 전체 시스템 SSO 쿠키 인증 통합

- tkuser 서비스 신규 추가 (API + Web)
  - 사용자/권한/프로젝트/부서/작업자/작업장/설비/작업/휴가 통합 관리
  - 작업장 탭: 공장→작업장 드릴다운 네비게이션 + 구역지도 클릭 연동
  - 작업 탭: 공정(work_types)→작업(tasks) 계층 관리
  - 휴가 탭: 유형 관리 + 연차 배정(근로기준법 자동계산)
- 전 시스템 SSO 쿠키 인증으로 통합 (.technicalkorea.net 공유)
- System 2: 작업 이슈 리포트 기능 강화
- System 3: tkuser API 연동, 페이지 권한 체계 적용
- docker-compose에 tkuser-api, tkuser-web 서비스 추가
- ARCHITECTURE.md, DEPLOYMENT.md 문서 작성

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-12 13:45:52 +09:00
parent 6495b8af32
commit 733bb0cb35
96 changed files with 9721 additions and 825 deletions

View File

@@ -2,7 +2,7 @@
* 신고 상세 페이지 JavaScript
*/
const API_BASE = window.API_BASE_URL || 'http://localhost:20005/api';
const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api';
let reportId = null;
let reportData = null;

View File

@@ -4,7 +4,7 @@
*/
// API 설정
const API_BASE = window.API_BASE_URL || 'http://localhost:20005/api';
const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api';
// 상태 변수
let selectedFactoryId = null;
@@ -175,7 +175,7 @@ async function loadMapImage() {
if (data.success && data.data) {
const selectedCategory = data.data.find(c => c.category_id == selectedFactoryId);
if (selectedCategory && selectedCategory.layout_image) {
const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', '');
const baseUrl = (window.API_BASE_URL || 'http://localhost:30005').replace('/api', '');
const fullImageUrl = selectedCategory.layout_image.startsWith('http')
? selectedCategory.layout_image
: `${baseUrl}${selectedCategory.layout_image}`;

View File

@@ -3,7 +3,7 @@
* category_type=safety 고정 필터
*/
const API_BASE = window.API_BASE_URL || 'http://localhost:20005/api';
const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api';
const CATEGORY_TYPE = 'safety';
// 상태 한글 변환
@@ -110,7 +110,7 @@ function renderIssues(issues) {
return;
}
const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', '');
const baseUrl = (window.API_BASE_URL || 'http://localhost:30005').replace('/api', '');
issueList.innerHTML = issues.map(issue => {
const reportDate = new Date(issue.report_date).toLocaleString('ko-KR', {

View File

@@ -3,16 +3,17 @@
*/
// API 설정
const API_BASE = window.API_BASE_URL || 'http://localhost:20005/api';
const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api';
// 상태 변수
let selectedFactoryId = null;
let selectedWorkplaceId = null;
let selectedWorkplaceName = null;
let selectedType = null; // 'nonconformity' | 'safety'
let selectedType = null; // 'nonconformity' | 'safety' | 'facility'
let selectedCategoryId = null;
let selectedCategoryName = null;
let selectedItemId = null;
let customItemName = null;
let selectedTbmSessionId = null;
let selectedVisitRequestId = null;
let photos = [null, null, null, null, null];
@@ -166,10 +167,9 @@ async function loadMapImage() {
if (data.success && data.data) {
const selectedCategory = data.data.find(c => c.category_id == selectedFactoryId);
if (selectedCategory && selectedCategory.layout_image) {
const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', '');
const fullImageUrl = selectedCategory.layout_image.startsWith('http')
? selectedCategory.layout_image
: `${baseUrl}${selectedCategory.layout_image}`;
: selectedCategory.layout_image;
canvasImage = new Image();
canvasImage.onload = () => renderMap();
@@ -251,7 +251,7 @@ function renderMap() {
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 배치도 이미지
if (canvasImage && canvasImage.complete) {
if (canvasImage && canvasImage.complete && canvasImage.naturalWidth > 0) {
const scale = Math.min(canvas.width / canvasImage.width, canvas.height / canvasImage.height);
const x = (canvas.width - canvasImage.width * scale) / 2;
const y = (canvas.height - canvasImage.height * scale) / 2;
@@ -587,6 +587,14 @@ function renderItems(items) {
btn.onclick = () => onItemSelect(item, btn);
grid.appendChild(btn);
});
// 직접 입력 버튼 추가
const customBtn = document.createElement('button');
customBtn.type = 'button';
customBtn.className = 'item-btn custom-input-btn';
customBtn.textContent = '+ 직접 입력';
customBtn.onclick = () => showCustomItemInput(customBtn);
grid.appendChild(customBtn);
}
/**
@@ -598,6 +606,21 @@ function onItemSelect(item, btn) {
btn.classList.add('selected');
selectedItemId = item.item_id;
customItemName = null;
// 직접 입력 영역 숨기기
const customInput = document.getElementById('customItemInput');
if (customInput) {
customInput.style.display = 'none';
document.getElementById('customItemName').value = '';
}
// 직접 입력 버튼 텍스트 초기화
const customBtn = document.querySelector('.item-btn.custom-input-btn');
if (customBtn) {
customBtn.textContent = '+ 직접 입력';
}
updateStepStatus();
}
@@ -667,9 +690,9 @@ function updateStepStatus() {
steps[2].classList.toggle('active', step2Complete);
// Step 3: 항목
const step3Complete = selectedItemId;
steps[2].classList.toggle('completed', step3Complete);
steps[3].classList.toggle('active', step3Complete);
const step3Complete = selectedItemId || (selectedItemId === 'custom' && customItemName);
steps[2].classList.toggle('completed', !!step3Complete);
steps[3].classList.toggle('active', !!step3Complete);
// 제출 버튼 활성화
const submitBtn = document.getElementById('submitBtn');
@@ -697,7 +720,8 @@ async function submitReport() {
tbm_session_id: selectedTbmSessionId,
visit_request_id: selectedVisitRequestId,
issue_category_id: selectedCategoryId,
issue_item_id: selectedItemId,
issue_item_id: selectedItemId === 'custom' ? null : selectedItemId,
custom_item_name: customItemName || null,
additional_description: additionalDescription || null,
photos: photos.filter(p => p !== null)
};
@@ -728,6 +752,77 @@ async function submitReport() {
}
}
/**
* 직접 입력 버튼 클릭
*/
function showCustomItemInput(btn) {
// 기존 항목 선택 해제
document.querySelectorAll('.item-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
selectedItemId = null;
customItemName = null;
const customInput = document.getElementById('customItemInput');
if (customInput) {
customInput.style.display = 'flex';
document.getElementById('customItemName').focus();
}
updateStepStatus();
}
/**
* 직접 입력 확인
*/
function confirmCustomItem() {
const input = document.getElementById('customItemName');
const name = input.value.trim();
if (!name) {
input.focus();
return;
}
customItemName = name;
selectedItemId = 'custom';
updateStepStatus();
// 직접 입력 UI 숨기되 값은 유지
const customInput = document.getElementById('customItemInput');
if (customInput) {
customInput.style.display = 'none';
}
// 직접 입력 버튼 텍스트 업데이트
const customBtn = document.querySelector('.item-btn.custom-input-btn');
if (customBtn) {
customBtn.textContent = `${name}`;
customBtn.classList.add('selected');
}
}
/**
* 직접 입력 취소
*/
function cancelCustomItem() {
const customInput = document.getElementById('customItemInput');
if (customInput) {
customInput.style.display = 'none';
document.getElementById('customItemName').value = '';
}
customItemName = null;
if (selectedItemId === 'custom') {
selectedItemId = null;
}
// 직접 입력 버튼 상태 초기화
const customBtn = document.querySelector('.item-btn.custom-input-btn');
if (customBtn) {
customBtn.textContent = '+ 직접 입력';
customBtn.classList.remove('selected');
}
updateStepStatus();
}
// 기타 위치 입력 시 위치 정보 업데이트
document.addEventListener('DOMContentLoaded', () => {
const customLocationInput = document.getElementById('customLocation');