- 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>
393 lines
12 KiB
JavaScript
393 lines
12 KiB
JavaScript
// SSO 쿠키 헬퍼
|
|
function _cookieGet(name) {
|
|
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
function _cookieRemove(name) {
|
|
let cookie = name + '=; path=/; max-age=0';
|
|
if (window.location.hostname.includes('technicalkorea.net')) {
|
|
cookie += '; domain=.technicalkorea.net';
|
|
}
|
|
document.cookie = cookie;
|
|
}
|
|
|
|
// 중앙 로그인 URL
|
|
function _getLoginUrl() {
|
|
const hostname = window.location.hostname;
|
|
if (hostname.includes('technicalkorea.net')) {
|
|
return window.location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(window.location.href);
|
|
}
|
|
return window.location.protocol + '//' + hostname + ':30000/login?redirect=' + encodeURIComponent(window.location.href);
|
|
}
|
|
|
|
// API 기본 설정 (통합 환경 지원)
|
|
const API_BASE_URL = (() => {
|
|
const hostname = window.location.hostname;
|
|
const protocol = window.location.protocol;
|
|
const port = window.location.port;
|
|
|
|
// 프로덕션 (technicalkorea.net) - 같은 도메인 /api
|
|
if (hostname.includes('technicalkorea.net')) {
|
|
return protocol + '//' + hostname + '/api';
|
|
}
|
|
|
|
// 통합 개발 환경 (포트 30280)
|
|
if (port === '30280' || port === '30000') {
|
|
return protocol + '//' + hostname + ':30200/api';
|
|
}
|
|
|
|
// 기존 TKQC 로컬 환경 (포트 16080)
|
|
if (port === '16080') {
|
|
return protocol + '//' + hostname + ':16080/api';
|
|
}
|
|
|
|
// 통합 Docker 환경에서 직접 접근 (포트 30280)
|
|
if (port === '30280') {
|
|
return protocol + '//' + hostname + ':30200/api';
|
|
}
|
|
|
|
// 기타 환경
|
|
return '/api';
|
|
})();
|
|
|
|
// 토큰 관리 (SSO 쿠키 + localStorage 이중 지원)
|
|
const TokenManager = {
|
|
getToken: () => {
|
|
// SSO 쿠키 우선 (sso_token), localStorage 폴백 (access_token)
|
|
return _cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token');
|
|
},
|
|
setToken: (token) => localStorage.setItem('access_token', token),
|
|
removeToken: () => {
|
|
_cookieRemove('sso_token');
|
|
_cookieRemove('sso_user');
|
|
_cookieRemove('sso_refresh_token');
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('sso_token');
|
|
localStorage.removeItem('sso_user');
|
|
},
|
|
|
|
getUser: () => {
|
|
// SSO 쿠키 우선, localStorage 폴백
|
|
const ssoUser = _cookieGet('sso_user') || localStorage.getItem('sso_user');
|
|
if (ssoUser) {
|
|
try { return JSON.parse(ssoUser); } catch(e) {}
|
|
}
|
|
const userStr = localStorage.getItem('currentUser') || localStorage.getItem('current_user');
|
|
return userStr ? JSON.parse(userStr) : null;
|
|
},
|
|
setUser: (user) => localStorage.setItem('current_user', JSON.stringify(user)),
|
|
removeUser: () => {
|
|
localStorage.removeItem('current_user');
|
|
localStorage.removeItem('currentUser');
|
|
}
|
|
};
|
|
|
|
// 전역 노출 (permissions.js 등 다른 스크립트에서 접근)
|
|
window.TokenManager = TokenManager;
|
|
window.API_BASE_URL = API_BASE_URL;
|
|
|
|
// API 요청 헬퍼
|
|
async function apiRequest(endpoint, options = {}) {
|
|
const token = TokenManager.getToken();
|
|
|
|
const defaultHeaders = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (token) {
|
|
defaultHeaders['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const config = {
|
|
...options,
|
|
headers: {
|
|
...defaultHeaders,
|
|
...options.headers
|
|
}
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, config);
|
|
|
|
if (response.status === 401) {
|
|
// 인증 실패 시 중앙 로그인 페이지로
|
|
TokenManager.removeToken();
|
|
TokenManager.removeUser();
|
|
window.location.href = _getLoginUrl();
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
console.error('API Error Response:', error);
|
|
console.error('Error details:', JSON.stringify(error, null, 2));
|
|
|
|
// 422 에러의 경우 validation 에러 메시지 추출
|
|
if (response.status === 422 && error.detail && Array.isArray(error.detail)) {
|
|
const validationErrors = error.detail.map(err =>
|
|
`${err.loc ? err.loc.join('.') : 'field'}: ${err.msg}`
|
|
).join(', ');
|
|
throw new Error(`입력값 검증 오류: ${validationErrors}`);
|
|
}
|
|
|
|
throw new Error(error.detail || 'API 요청 실패');
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('API 요청 에러:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Auth API
|
|
const AuthAPI = {
|
|
login: async (username, password) => {
|
|
const formData = new URLSearchParams();
|
|
formData.append('username', username);
|
|
formData.append('password', password);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: formData.toString()
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
console.error('로그인 에러:', error);
|
|
throw new Error(error.detail || '로그인 실패');
|
|
}
|
|
|
|
const data = await response.json();
|
|
TokenManager.setToken(data.access_token);
|
|
TokenManager.setUser(data.user);
|
|
return data;
|
|
} catch (error) {
|
|
console.error('로그인 요청 에러:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
logout: () => {
|
|
TokenManager.removeToken();
|
|
TokenManager.removeUser();
|
|
window.location.href = _getLoginUrl();
|
|
},
|
|
|
|
getMe: () => apiRequest('/auth/me'),
|
|
|
|
getCurrentUser: () => apiRequest('/auth/me'),
|
|
|
|
createUser: (userData) => apiRequest('/auth/users', {
|
|
method: 'POST',
|
|
body: JSON.stringify(userData)
|
|
}),
|
|
|
|
getUsers: () => {
|
|
console.log('🔍 AuthAPI.getUsers 호출 - 엔드포인트: /auth/users');
|
|
return apiRequest('/auth/users');
|
|
},
|
|
|
|
updateUser: (userId, userData) => apiRequest(`/auth/users/${userId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(userData)
|
|
}),
|
|
|
|
deleteUser: (userId) => apiRequest(`/auth/users/${userId}`, {
|
|
method: 'DELETE'
|
|
}),
|
|
|
|
changePassword: (currentPassword, newPassword) => apiRequest('/auth/change-password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
current_password: currentPassword,
|
|
new_password: newPassword
|
|
})
|
|
}),
|
|
|
|
resetPassword: (userId, newPassword = '000000') => apiRequest(`/auth/users/${userId}/reset-password`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
new_password: newPassword
|
|
})
|
|
}),
|
|
|
|
// 부서 목록 가져오기
|
|
getDepartments: () => [
|
|
{ value: 'production', label: '생산' },
|
|
{ value: 'quality', label: '품질' },
|
|
{ value: 'purchasing', label: '구매' },
|
|
{ value: 'design', label: '설계' },
|
|
{ value: 'sales', label: '영업' }
|
|
],
|
|
|
|
// 부서명 변환
|
|
getDepartmentLabel: (departmentValue) => {
|
|
const departments = AuthAPI.getDepartments();
|
|
const dept = departments.find(d => d.value === departmentValue);
|
|
return dept ? dept.label : departmentValue || '미지정';
|
|
}
|
|
};
|
|
|
|
// Issues API
|
|
const IssuesAPI = {
|
|
create: async (issueData) => {
|
|
// photos 배열 처리 (최대 5장)
|
|
const dataToSend = {
|
|
category: issueData.category,
|
|
description: issueData.description,
|
|
project_id: issueData.project_id,
|
|
photo: issueData.photos && issueData.photos.length > 0 ? issueData.photos[0] : null,
|
|
photo2: issueData.photos && issueData.photos.length > 1 ? issueData.photos[1] : null,
|
|
photo3: issueData.photos && issueData.photos.length > 2 ? issueData.photos[2] : null,
|
|
photo4: issueData.photos && issueData.photos.length > 3 ? issueData.photos[3] : null,
|
|
photo5: issueData.photos && issueData.photos.length > 4 ? issueData.photos[4] : null
|
|
};
|
|
|
|
return apiRequest('/issues/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(dataToSend)
|
|
});
|
|
},
|
|
|
|
getAll: (params = {}) => {
|
|
const queryString = new URLSearchParams(params).toString();
|
|
return apiRequest(`/issues/${queryString ? '?' + queryString : ''}`);
|
|
},
|
|
|
|
get: (id) => apiRequest(`/issues/${id}`),
|
|
|
|
update: (id, issueData) => apiRequest(`/issues/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(issueData)
|
|
}),
|
|
|
|
delete: (id) => apiRequest(`/issues/${id}`, {
|
|
method: 'DELETE'
|
|
}),
|
|
|
|
getStats: () => apiRequest('/issues/stats/summary')
|
|
};
|
|
|
|
// Daily Work API
|
|
const DailyWorkAPI = {
|
|
create: (workData) => apiRequest('/daily-work/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(workData)
|
|
}),
|
|
|
|
getAll: (params = {}) => {
|
|
const queryString = new URLSearchParams(params).toString();
|
|
return apiRequest(`/daily-work/${queryString ? '?' + queryString : ''}`);
|
|
},
|
|
|
|
get: (id) => apiRequest(`/daily-work/${id}`),
|
|
|
|
update: (id, workData) => apiRequest(`/daily-work/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(workData)
|
|
}),
|
|
|
|
delete: (id) => apiRequest(`/daily-work/${id}`, {
|
|
method: 'DELETE'
|
|
}),
|
|
|
|
getStats: (params = {}) => {
|
|
const queryString = new URLSearchParams(params).toString();
|
|
return apiRequest(`/daily-work/stats/summary${queryString ? '?' + queryString : ''}`);
|
|
}
|
|
};
|
|
|
|
// Reports API
|
|
const ReportsAPI = {
|
|
getSummary: (startDate, endDate) => apiRequest('/reports/summary', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
start_date: startDate,
|
|
end_date: endDate
|
|
})
|
|
}),
|
|
|
|
getIssues: (startDate, endDate) => {
|
|
const params = new URLSearchParams({
|
|
start_date: startDate,
|
|
end_date: endDate
|
|
}).toString();
|
|
return apiRequest(`/reports/issues?${params}`);
|
|
},
|
|
|
|
getDailyWorks: (startDate, endDate) => {
|
|
const params = new URLSearchParams({
|
|
start_date: startDate,
|
|
end_date: endDate
|
|
}).toString();
|
|
return apiRequest(`/reports/daily-works?${params}`);
|
|
}
|
|
};
|
|
|
|
// 권한 체크
|
|
function checkAuth() {
|
|
const user = TokenManager.getUser();
|
|
if (!user) {
|
|
window.location.href = _getLoginUrl();
|
|
return null;
|
|
}
|
|
return user;
|
|
}
|
|
|
|
function checkAdminAuth() {
|
|
const user = checkAuth();
|
|
if (user && user.role !== 'admin') {
|
|
alert('관리자 권한이 필요합니다.');
|
|
window.location.href = _getLoginUrl();
|
|
return null;
|
|
}
|
|
return user;
|
|
}
|
|
|
|
// 페이지 접근 권한 체크 함수
|
|
function checkPageAccess(pageName) {
|
|
const user = checkAuth();
|
|
if (!user) return null;
|
|
|
|
// admin은 모든 페이지 접근 가능
|
|
if (user.role === 'admin') return user;
|
|
|
|
// 페이지별 권한 체크는 pagePermissionManager에서 처리
|
|
if (window.pagePermissionManager && !window.pagePermissionManager.canAccessPage(pageName)) {
|
|
alert('이 페이지에 접근할 권한이 없습니다.');
|
|
window.location.href = _getLoginUrl();
|
|
return null;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
// 프로젝트 API
|
|
const ProjectsAPI = {
|
|
getAll: (activeOnly = false) => {
|
|
const params = `?active_only=${activeOnly}`;
|
|
return apiRequest(`/projects/${params}`);
|
|
},
|
|
|
|
get: (id) => apiRequest(`/projects/${id}`),
|
|
|
|
create: (projectData) => apiRequest('/projects/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(projectData)
|
|
}),
|
|
|
|
update: (id, projectData) => apiRequest(`/projects/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(projectData)
|
|
}),
|
|
|
|
delete: (id) => apiRequest(`/projects/${id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
};
|