feat: SSO 쿠키 인증 통합 + 서브도메인 라우팅 아키텍처
- Path-based 라우팅을 서브도메인 기반으로 전환 (tkfb/tkreport/tkqc.technicalkorea.net) - 3개 시스템 프론트엔드에 SSO 쿠키 인증 통합 (domain=.technicalkorea.net, localStorage 폴백) - Gateway: 포털+로그인+System1 프록시, 쿠키 SSO 설정 - System 1: 토큰키 통일, nginx.conf 생성, 신고페이지 리다이렉트 - System 2: api-base.js/app-init.js 생성, getSSOToken() 통합 - System 3: TokenManager 쿠키 지원, 중앙 로그인 리다이렉트 - docker-compose.yml에 cloudflared 서비스 추가 - DEPLOY-GUIDE.md 배포 가이드 작성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
// ✅ /js/admin.js (수정됨 - 중복 로딩 제거)
|
||||
async function initDashboard() {
|
||||
// 로그인 토큰 확인
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) {
|
||||
location.href = '/index.html';
|
||||
location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ function getApiBaseUrl() {
|
||||
export const API = getApiBaseUrl();
|
||||
|
||||
export function ensureAuthenticated() {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token || token === 'undefined') {
|
||||
alert('로그인이 필요합니다');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('sso_token');
|
||||
window.location.href = '/';
|
||||
return null;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export function ensureAuthenticated() {
|
||||
}
|
||||
|
||||
export function getAuthHeaders() {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
@@ -70,7 +70,7 @@ export async function apiCall(url, options = {}) {
|
||||
// 인증 만료 처리
|
||||
if (response.status === 401) {
|
||||
console.error('❌ 인증 만료');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('sso_token');
|
||||
alert('인증이 만료되었습니다. 다시 로그인해주세요.');
|
||||
window.location.href = '/';
|
||||
return;
|
||||
|
||||
@@ -37,7 +37,7 @@ async function authFetch(endpoint, options = {}) {
|
||||
if (!token) {
|
||||
console.error('토큰이 없습니다. 로그인이 필요합니다.');
|
||||
clearAuthData(); // 인증 정보 정리
|
||||
window.location.href = '/index.html'; // 로그인 페이지로 리디렉션
|
||||
window.location.href = '/login'; // 로그인 페이지로 리디렉션
|
||||
// 에러를 던져서 후속 실행을 중단
|
||||
throw new Error('인증 토큰이 없습니다.');
|
||||
}
|
||||
@@ -59,7 +59,7 @@ async function authFetch(endpoint, options = {}) {
|
||||
if (response.status === 401) {
|
||||
console.error('인증 실패. 토큰이 만료되었거나 유효하지 않습니다.');
|
||||
clearAuthData(); // 만료된 인증 정보 정리
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/login';
|
||||
throw new Error('인증에 실패했습니다.');
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ function getKoreaDateString(date = new Date()) {
|
||||
*/
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return null;
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
@@ -118,7 +118,7 @@ function getCurrentUser() {
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo');
|
||||
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
return JSON.parse(userInfo);
|
||||
}
|
||||
@@ -190,7 +190,7 @@ async function makeRateLimitedRequest(url, options = {}, retryCount = 0) {
|
||||
|
||||
if (response.status === 401) {
|
||||
showMessage('인증이 만료되었습니다. 다시 로그인해주세요.', 'error');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('sso_token');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
@@ -972,10 +972,10 @@ function renderWorkersList(workers) {
|
||||
async function init() {
|
||||
try {
|
||||
// 인증 확인 (api-config.js의 ensureAuthenticated 대신 직접 확인)
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token || token === 'undefined') {
|
||||
showMessage('로그인이 필요합니다.', 'error');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('sso_token');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isLoggedIn, getUser, clearAuthData } from './auth.js';
|
||||
if (!isLoggedIn()) {
|
||||
console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.');
|
||||
clearAuthData(); // 만약을 위해 한번 더 정리
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/login';
|
||||
return; // 이후 코드 실행 방지
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { isLoggedIn, getUser, clearAuthData } from './auth.js';
|
||||
if (!currentUser || !currentUser.username || !currentUser.role) {
|
||||
console.error('🚨 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
|
||||
clearAuthData();
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function parseJwt(token) {
|
||||
* @returns {string|null} - 저장된 토큰 또는 토큰이 없을 경우 null
|
||||
*/
|
||||
export function getToken() {
|
||||
return localStorage.getItem('token');
|
||||
return localStorage.getItem('sso_token');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ export function getToken() {
|
||||
* @returns {object|null} - 저장된 사용자 객체 또는 정보가 없을 경우 null
|
||||
*/
|
||||
export function getUser() {
|
||||
const user = localStorage.getItem('user');
|
||||
const user = localStorage.getItem('sso_user');
|
||||
try {
|
||||
return user ? JSON.parse(user) : null;
|
||||
} catch(e) {
|
||||
@@ -43,16 +43,16 @@ export function getUser() {
|
||||
* @param {object} user - 서버에서 받은 사용자 정보 객체
|
||||
*/
|
||||
export function saveAuthData(token, user) {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
localStorage.setItem('sso_token', token);
|
||||
localStorage.setItem('sso_user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃 시 localStorage에서 인증 정보를 제거합니다.
|
||||
*/
|
||||
export function clearAuthData() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -184,9 +184,9 @@ form?.addEventListener('submit', async (e) => {
|
||||
|
||||
if (countdown < 0) {
|
||||
clearInterval(countdownInterval);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/index.html';
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@@ -205,7 +205,7 @@ form?.addEventListener('submit', async (e) => {
|
||||
|
||||
// 페이지 로드 시 현재 사용자 정보 표시
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
console.log('🔐 비밀번호 변경 페이지 로드됨');
|
||||
console.log('👤 현재 사용자:', user.username || 'Unknown');
|
||||
});
|
||||
@@ -65,7 +65,7 @@ function initializePage() {
|
||||
const user = getUser();
|
||||
if (!user) {
|
||||
showError('로그인이 필요합니다. 2초 후 로그인 페이지로 이동합니다.');
|
||||
setTimeout(() => window.location.href = '/index.html', 2000);
|
||||
setTimeout(() => window.location.href = '/login', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ function getKoreaToday() {
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return null;
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
@@ -42,7 +42,7 @@ function getCurrentUser() {
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
|
||||
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
|
||||
if (userInfo) {
|
||||
const parsed = JSON.parse(userInfo);
|
||||
console.log('localStorage에서 가져온 사용자 정보:', parsed);
|
||||
@@ -861,10 +861,10 @@ function setupEventListeners() {
|
||||
// 초기화
|
||||
async function init() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token || token === 'undefined') {
|
||||
showMessage('로그인이 필요합니다.', 'error');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('sso_token');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
|
||||
@@ -6,7 +6,7 @@ document.getElementById('uploadForm').addEventListener('submit', async (e) => {
|
||||
|
||||
try {
|
||||
// FormData를 사용할 때는 Content-Type을 설정하지 않음 (자동 설정됨)
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
const res = await fetch(`${API}/factoryinfo`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -69,7 +69,7 @@ function updateTeamStatusUI() {
|
||||
|
||||
// 환영 메시지 개인화
|
||||
function personalizeWelcome() {
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
const welcomeMsg = document.getElementById('welcome-message');
|
||||
|
||||
if (user && user.name && welcomeMsg) {
|
||||
@@ -83,7 +83,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('🚀 그룹장 대시보드 초기화 시작');
|
||||
|
||||
// 사용자 정보 확인
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
console.log('👤 현재 사용자:', user);
|
||||
|
||||
// 권한 확인
|
||||
|
||||
@@ -79,7 +79,7 @@ function setupNavbarEvents() {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
if (confirm('로그아웃 하시겠습니까?')) {
|
||||
clearAuthData();
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/login';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const accessLabels = {
|
||||
};
|
||||
|
||||
// 현재 사용자 정보 가져오기
|
||||
const currentUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const currentUser = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
const isSystemUser = currentUser.access_level === 'system';
|
||||
|
||||
function createRow(item, cols, delHandler) {
|
||||
@@ -72,9 +72,9 @@ myPasswordForm?.addEventListener('submit', async e => {
|
||||
// 3초 후 로그인 페이지로 이동
|
||||
setTimeout(() => {
|
||||
alert('비밀번호가 변경되어 다시 로그인해주세요.');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/index.html';
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('❌ 비밀번호 변경 실패: ' + (result.error || '현재 비밀번호가 올바르지 않습니다.'));
|
||||
|
||||
@@ -33,7 +33,7 @@ function getKoreaToday() {
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return null;
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
@@ -47,7 +47,7 @@ function getCurrentUser() {
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
|
||||
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
|
||||
if (userInfo) {
|
||||
const parsed = JSON.parse(userInfo);
|
||||
console.log('localStorage에서 가져온 사용자 정보:', parsed);
|
||||
@@ -929,10 +929,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('permission-check-message').style.display = 'block';
|
||||
|
||||
// 토큰 확인
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token || token === 'undefined') {
|
||||
showMessage('로그인이 필요합니다.', 'error');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('sso_token');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
|
||||
@@ -19,7 +19,7 @@ const accessLevelMap = {
|
||||
async function loadProfile() {
|
||||
try {
|
||||
// 먼저 로컬 스토리지에서 기본 정보 표시
|
||||
const storedUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const storedUser = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
if (storedUser) {
|
||||
updateProfileUI(storedUser);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ async function loadProfile() {
|
||||
...storedUser,
|
||||
...userData
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
localStorage.setItem('sso_user', JSON.stringify(updatedUser));
|
||||
|
||||
// UI 업데이트
|
||||
updateProfileUI(userData);
|
||||
|
||||
@@ -19,7 +19,7 @@ let basicData = {
|
||||
// 현재 사용자 정보 가져오기
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return null;
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
@@ -747,7 +747,7 @@ window.saveEditedWork = saveEditedWork;
|
||||
// 초기화
|
||||
async function init() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token || token === 'undefined') {
|
||||
showMessage('로그인이 필요합니다.', 'error');
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -40,7 +40,7 @@ const AttendanceValidationPage = () => {
|
||||
try {
|
||||
const response = await fetch(`/api/workreports/date/${date}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${localStorage.getItem('sso_token')}`
|
||||
}
|
||||
});
|
||||
return await response.json();
|
||||
@@ -54,7 +54,7 @@ const AttendanceValidationPage = () => {
|
||||
try {
|
||||
const response = await fetch(`/api/daily-work-reports/date/${date}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${localStorage.getItem('sso_token')}`
|
||||
}
|
||||
});
|
||||
return await response.json();
|
||||
@@ -72,10 +72,10 @@ const AttendanceValidationPage = () => {
|
||||
try {
|
||||
const [workReports, dailyReports] = await Promise.all([
|
||||
fetch(`/api/workreports?start=${start}&end=${end}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` }
|
||||
}).then(res => res.json()),
|
||||
fetch(`/api/daily-work-reports/search?start_date=${start}&end_date=${end}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` }
|
||||
}).then(res => res.json())
|
||||
]);
|
||||
|
||||
|
||||
@@ -623,7 +623,7 @@
|
||||
|
||||
// 토큰 확인 함수
|
||||
function checkToken() {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) {
|
||||
showError('로그인이 필요합니다.');
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
|
||||
// 토큰 가져오기
|
||||
function getToken() {
|
||||
return localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
return localStorage.getItem('sso_token') || sessionStorage.getItem('token');
|
||||
}
|
||||
|
||||
// 로딩 상태 설정
|
||||
|
||||
@@ -541,7 +541,7 @@
|
||||
<script>
|
||||
// 디버깅용 콘솔 로그
|
||||
console.log('📊 그룹장 대시보드 로딩됨');
|
||||
console.log('👤 현재 사용자:', JSON.parse(localStorage.getItem('user') || '{}'));
|
||||
console.log('👤 현재 사용자:', JSON.parse(localStorage.getItem('sso_user') || '{}'));
|
||||
|
||||
// 팀 현황 새로고침
|
||||
function refreshTeamStatus() {
|
||||
@@ -551,7 +551,7 @@
|
||||
|
||||
// 환영 메시지 개인화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
if (user && user.name) {
|
||||
const welcomeMsg = document.getElementById('welcome-message');
|
||||
if (welcomeMsg) {
|
||||
|
||||
Reference in New Issue
Block a user