// 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'), getUsers: () => { return apiRequest('/auth/users'); }, changePassword: (currentPassword, newPassword) => apiRequest('/auth/change-password', { method: 'POST', body: JSON.stringify({ current_password: currentPassword, 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') }; // 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}`); } }; // 권한 체크 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; } // AI API const AiAPI = { getSimilarIssues: async (issueId, limit = 5) => { try { const res = await fetch(`/ai-api/similar/${issueId}?n_results=${limit}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!res.ok) return { available: false, results: [] }; return await res.json(); } catch (e) { console.warn('AI 유사 검색 실패:', e); return { available: false, results: [] }; } }, searchSimilar: async (query, limit = 5, filters = {}) => { try { const res = await fetch('/ai-api/similar/search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TokenManager.getToken()}` }, body: JSON.stringify({ query, n_results: limit, ...filters }) }); if (!res.ok) return { available: false, results: [] }; return await res.json(); } catch (e) { console.warn('AI 검색 실패:', e); return { available: false, results: [] }; } }, classifyIssue: async (description, detailNotes = '') => { try { const res = await fetch('/ai-api/classify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TokenManager.getToken()}` }, body: JSON.stringify({ description, detail_notes: detailNotes }) }); if (!res.ok) return { available: false }; return await res.json(); } catch (e) { console.warn('AI 분류 실패:', e); return { available: false }; } }, generateDailyReport: async (date, projectId) => { try { const res = await fetch('/ai-api/report/daily', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TokenManager.getToken()}` }, body: JSON.stringify({ date, project_id: projectId }) }); if (!res.ok) return { available: false }; return await res.json(); } catch (e) { console.warn('AI 보고서 생성 실패:', e); return { available: false }; } }, syncEmbeddings: async () => { try { const res = await fetch('/ai-api/embeddings/sync', { method: 'POST', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!res.ok) return { status: 'error' }; return await res.json(); } catch (e) { return { status: 'error' }; } }, checkHealth: async () => { try { const res = await fetch('/ai-api/health'); return await res.json(); } catch (e) { return { status: 'disconnected' }; } }, // RAG: 해결방안 제안 suggestSolution: async (issueId) => { try { const res = await fetch(`/ai-api/rag/suggest-solution/${issueId}`, { method: 'POST', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!res.ok) return { available: false }; return await res.json(); } catch (e) { console.warn('AI 해결방안 제안 실패:', e); return { available: false }; } }, // RAG: 자연어 질의 askQuestion: async (question, projectId = null) => { try { const res = await fetch('/ai-api/rag/ask', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TokenManager.getToken()}` }, body: JSON.stringify({ question, project_id: projectId }) }); if (!res.ok) return { available: false }; return await res.json(); } catch (e) { console.warn('AI 질의 실패:', e); return { available: false }; } }, // RAG: 패턴 분석 analyzePattern: async (description) => { try { const res = await fetch('/ai-api/rag/pattern', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TokenManager.getToken()}` }, body: JSON.stringify({ description }) }); if (!res.ok) return { available: false }; return await res.json(); } catch (e) { console.warn('AI 패턴 분석 실패:', e); return { available: false }; } }, // RAG: 강화 분류 (과거 사례 참고) classifyWithRAG: async (description, detailNotes = '') => { try { const res = await fetch('/ai-api/rag/classify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TokenManager.getToken()}` }, body: JSON.stringify({ description, detail_notes: detailNotes }) }); if (!res.ok) return { available: false }; return await res.json(); } catch (e) { console.warn('AI RAG 분류 실패:', e); return { available: false }; } } }; // 프로젝트 API const ProjectsAPI = { getAll: (activeOnly = false) => { const params = `?active_only=${activeOnly}`; return apiRequest(`/projects/${params}`); }, get: (id) => apiRequest(`/projects/${id}`) };