refactor: System2/3, User Management SSO 인증 통합

- System2 신고: SSO JWT 인증 전환, API base 정리
- System3 부적합: SSO 인증 매니저 통합, 권한 체계 정비
- User Management: SSO 토큰 기반 사용자 관리 API 연동

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-06 23:18:23 +09:00
parent 61c810bd47
commit 11cffbd920
26 changed files with 528 additions and 1824 deletions

View File

@@ -1,3 +1,4 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
@@ -32,11 +33,19 @@ app = FastAPI(
lifespan=lifespan
)
# CORS 설정 (완전 개방 - CORS 문제 해결)
ALLOWED_ORIGINS = [
"https://tkfb.technicalkorea.net",
"https://tkreport.technicalkorea.net",
"https://tkqc.technicalkorea.net",
"https://tkuser.technicalkorea.net",
]
if os.getenv("ENV", "production") == "development":
ALLOWED_ORIGINS += ["http://localhost:30080", "http://localhost:30180", "http://localhost:30280"]
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False, # * origin과 credentials는 함께 사용 불가
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
expose_headers=["*"]

View File

@@ -53,32 +53,28 @@ const API_BASE_URL = (() => {
// 토큰 관리 (SSO 쿠키 + localStorage 이중 지원)
const TokenManager = {
getToken: () => {
// SSO 쿠키 우선 (sso_token), localStorage 폴백 (access_token)
return _cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token');
// SSO 쿠키 우선, localStorage 폴백
return _cookieGet('sso_token') || localStorage.getItem('sso_token');
},
setToken: (token) => localStorage.setItem('access_token', token),
setToken: (token) => localStorage.setItem('sso_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;
return null;
},
setUser: (user) => localStorage.setItem('current_user', JSON.stringify(user)),
setUser: (user) => localStorage.setItem('sso_user', JSON.stringify(user)),
removeUser: () => {
localStorage.removeItem('current_user');
localStorage.removeItem('currentUser');
localStorage.removeItem('sso_user');
}
};

View File

@@ -47,22 +47,16 @@ class App {
*/
async checkAuth() {
// SSO 쿠키 우선, localStorage 폴백
const token = this._cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token');
const token = this._cookieGet('sso_token') || localStorage.getItem('sso_token');
if (!token) {
throw new Error('토큰 없음');
}
// SSO 쿠키에서 사용자 정보 시도
const ssoUser = this._cookieGet('sso_user') || localStorage.getItem('sso_user');
if (ssoUser) {
try { this.currentUser = JSON.parse(ssoUser); return; } catch(e) {}
}
const storedUser = localStorage.getItem('currentUser');
if (storedUser) {
this.currentUser = JSON.parse(storedUser);
} else {
throw new Error('사용자 정보 없음');
}
throw new Error('사용자 정보 없음');
}
_cookieGet(name) {
@@ -371,10 +365,8 @@ class App {
if (window.authManager) {
window.authManager.clearAuth();
} else {
localStorage.removeItem('access_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('currentUser');
}
this.redirectToLogin();
}

View File

@@ -628,10 +628,10 @@ class CommonHeader {
if (window.authManager) {
window.authManager.logout();
} else {
localStorage.removeItem('access_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('currentUser');
localStorage.removeItem('sso_user');
var hostname = window.location.hostname;
if (hostname.includes('technicalkorea.net')) {
window.location.href = window.location.protocol + '//tkfb.technicalkorea.net/login';
@@ -667,7 +667,6 @@ class CommonHeader {
initializeKeyboardShortcuts() {
if (window.keyboardShortcuts) {
window.keyboardShortcuts.setUser(this.currentUser);
console.log('⌨️ 키보드 단축키 사용자 설정 완료');
}
}
@@ -679,7 +678,6 @@ class CommonHeader {
// 사용자 설정 후 프리로더 초기화
setTimeout(() => {
window.pagePreloader.init();
console.log('🚀 페이지 프리로더 초기화 완료');
}, 1000); // 권한 시스템 로드 후 실행
}
}

View File

@@ -18,7 +18,6 @@ class AuthManager {
* 초기화
*/
init() {
console.log('🔐 AuthManager 초기화');
// localStorage에서 사용자 정보 복원
this.restoreUserFromStorage();
@@ -57,7 +56,7 @@ class AuthManager {
* SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백)
*/
_getToken() {
return this._cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token');
return this._cookieGet('sso_token') || localStorage.getItem('sso_token');
}
/**
@@ -68,10 +67,6 @@ class AuthManager {
if (ssoUser && ssoUser !== 'undefined' && ssoUser !== 'null') {
try { return JSON.parse(ssoUser); } catch(e) {}
}
const userStr = localStorage.getItem('currentUser');
if (userStr && userStr !== 'undefined' && userStr !== 'null') {
try { return JSON.parse(userStr); } catch(e) {}
}
return null;
}
@@ -148,7 +143,7 @@ class AuthManager {
this.lastAuthCheck = Date.now();
// localStorage 업데이트
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
this.notifyListeners('auth-success', user);
return user;
@@ -191,11 +186,10 @@ class AuthManager {
this._cookieRemove('sso_user');
this._cookieRemove('sso_refresh_token');
// localStorage 삭제
localStorage.removeItem('access_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('currentUser');
// localStorage 삭제 (전 시스템 키 통일)
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(k => {
localStorage.removeItem(k);
});
this.notifyListeners('auth-cleared');
}
@@ -212,9 +206,9 @@ class AuthManager {
this.isAuthenticated = true;
this.lastAuthCheck = Date.now();
// localStorage 저장
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('currentUser', JSON.stringify(data.user));
// localStorage 저장 (sso_token/sso_user로 통일)
localStorage.setItem('sso_token', data.access_token);
localStorage.setItem('sso_user', JSON.stringify(data.user));
this.notifyListeners('login-success', data.user);
return data;
@@ -293,4 +287,3 @@ class AuthManager {
// 전역 인스턴스 생성
window.authManager = new AuthManager();
console.log('🎯 AuthManager 로드 완료');

View File

@@ -36,7 +36,6 @@ class KeyboardShortcutManager {
this.register('r', () => this.triggerRefreshAction(), '새로고침');
this.register('f', () => this.focusSearchField(), '검색 포커스');
console.log('⌨️ 키보드 단축키 등록 완료');
}
/**
@@ -178,7 +177,6 @@ class KeyboardShortcutManager {
// 콜백 실행
try {
shortcut.callback(event);
console.log(`⌨️ 단축키 실행: ${combination}`);
} catch (error) {
console.error('단축키 실행 실패:', combination, error);
}
@@ -599,7 +597,6 @@ class KeyboardShortcutManager {
*/
setEnabled(enabled) {
this.isEnabled = enabled;
console.log(`⌨️ 키보드 단축키 ${enabled ? '활성화' : '비활성화'}`);
}
/**

View File

@@ -52,7 +52,7 @@ class PageManager {
* 사용자 인증 확인
*/
async checkAuthentication() {
const token = localStorage.getItem('access_token');
const token = localStorage.getItem('sso_token');
if (!token) {
window.location.href = '/index.html';
return null;
@@ -63,12 +63,12 @@ class PageManager {
await this.waitForAPI();
const user = await AuthAPI.getCurrentUser();
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
return user;
} catch (error) {
console.error('인증 실패:', error);
localStorage.removeItem('access_token');
localStorage.removeItem('currentUser');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
window.location.href = '/index.html';
return null;
}

View File

@@ -83,7 +83,6 @@ class PagePreloader {
if (this.isPreloading) return;
this.isPreloading = true;
console.log('🚀 페이지 프리로딩 시작:', pages.map(p => p.id));
for (const page of pages) {
if (this.preloadedPages.has(page.url)) continue;
@@ -93,7 +92,6 @@ class PagePreloader {
// 네트워크 상태 확인 (느린 연결에서는 중단)
if (this.isSlowConnection()) {
console.log('⚠️ 느린 연결 감지, 프리로딩 중단');
break;
}
@@ -106,7 +104,6 @@ class PagePreloader {
}
this.isPreloading = false;
console.log('✅ 페이지 프리로딩 완료');
}
/**
@@ -128,7 +125,6 @@ class PagePreloader {
await this.preloadPageResources(html, page.url);
this.preloadedPages.add(page.url);
console.log(`📄 프리로드 완료: ${page.id}`);
}
} catch (error) {
@@ -251,7 +247,6 @@ class PagePreloader {
const html = await response.text();
this.preloadCache.set(url, html);
this.preloadedPages.add(url);
console.log('🖱️ 호버 프리로드 완료:', url);
}
} catch (error) {
console.warn('호버 프리로드 실패:', url, error);
@@ -285,7 +280,6 @@ class PagePreloader {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('🔧 서비스 워커 등록 완료:', registration);
} catch (error) {
console.log('서비스 워커 등록 실패:', error);
}
@@ -306,7 +300,6 @@ class PagePreloader {
this.preloadCache.clear();
this.resourceCache.clear();
this.preloadedPages.clear();
console.log('🗑️ 프리로드 캐시 정리 완료');
}
}

View File

@@ -47,7 +47,7 @@ class PagePermissionManager {
const match = document.cookie.match(/(?:^|; )sso_token=([^;]*)/);
if (match) return decodeURIComponent(match[1]);
// 3) localStorage 폴백
return localStorage.getItem('sso_token') || localStorage.getItem('access_token');
return localStorage.getItem('sso_token');
}
async loadPagePermissions() {

View File

@@ -47,7 +47,7 @@ async function loadProjects() {
var sel = document.getElementById('projectFilter');
sel.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(function (p) {
sel.innerHTML += '<option value="' + p.id + '">' + p.project_name + '</option>';
sel.innerHTML += '<option value="' + p.id + '">' + escapeHtml(p.project_name) + '</option>';
});
}
} catch (e) { console.error('프로젝트 로드 실패:', e); }

View File

@@ -53,7 +53,7 @@ async function initializeIssueView() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_view');
@@ -713,7 +713,7 @@ async function handlePasswordChange(e) {
// 현재 사용자 정보도 업데이트
currentUser.password = newPassword;
localStorage.setItem('currentUser', JSON.stringify(currentUser));
localStorage.setItem('sso_user', JSON.stringify(currentUser));
alert('비밀번호가 성공적으로 변경되었습니다.');
document.querySelector('.fixed').remove(); // 모달 닫기

View File

@@ -18,7 +18,7 @@ async function initializeArchive() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_archive');

View File

@@ -81,7 +81,7 @@ async function initializeInbox() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_inbox');
@@ -123,7 +123,7 @@ async function initializeInbox() {
// 공통 헤더만이라도 초기화
try {
const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
if (user.id) {
await window.commonHeader.init(user, 'issues_inbox');
// 에러 상황에서도 애니메이션 적용

View File

@@ -27,7 +27,7 @@ async function initializeManagement() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_management');
@@ -694,9 +694,9 @@ function createCompletedRow(issue, project) {
// 입력 여부 아이콘 생성
function getStatusIcon(value) {
if (value && value.toString().trim() !== '') {
return '<span class="text-green-500 text-lg"></span>';
return '<span class="text-green-500 text-lg"></span>';
} else {
return '<span class="text-gray-400 text-lg"></span>';
return '<span class="text-gray-400 text-lg"></span>';
}
}
@@ -704,9 +704,9 @@ function getStatusIcon(value) {
function getPhotoStatusIcon(photo1, photo2) {
const count = (photo1 ? 1 : 0) + (photo2 ? 1 : 0);
if (count > 0) {
return `<span class="text-green-500 text-lg"></span><span class="text-xs ml-1">${count}장</span>`;
return `<span class="text-green-500 text-lg"></span><span class="text-xs ml-1">${count}장</span>`;
} else {
return '<span class="text-gray-400 text-lg"></span>';
return '<span class="text-gray-400 text-lg"></span>';
}
}
@@ -1194,7 +1194,6 @@ async function saveModalChanges() {
updates[fieldName] = base64;
}
console.log(`📸 ${maxPhotos}장의 완료 사진 처리 완료`);
}
console.log('Modal sending updates:', updates);
@@ -1744,11 +1743,10 @@ async function saveIssueFromModal(issueId) {
const files = completionPhotoElement.files;
const maxPhotos = Math.min(files.length, 5);
console.log(`🔍 총 ${maxPhotos}개의 완료 사진 업로드 시작`);
for (let i = 0; i < maxPhotos; i++) {
const file = files[i];
console.log(`🔍 파일 ${i + 1} 정보:`, {
console.log(` 파일 ${i + 1} 정보:`, {
name: file.name,
size: file.size,
type: file.type
@@ -1760,7 +1758,6 @@ async function saveIssueFromModal(issueId) {
const fieldName = i === 0 ? 'completion_photo' : `completion_photo${i + 1}`;
completionPhotos[fieldName] = base64Data;
console.log(`✅ 파일 ${i + 1} 변환 완료 (${fieldName})`);
}
} catch (error) {
console.error('파일 변환 오류:', error);
@@ -1814,9 +1811,9 @@ async function saveIssueFromModal(issueId) {
if (updatedIssue) {
// 완료 사진이 저장되었는지 확인
if (updatedIssue.completion_photo_path) {
alert(' 완료 사진이 성공적으로 저장되었습니다!');
alert(' 완료 사진이 성공적으로 저장되었습니다!');
} else {
alert('⚠️ 저장은 완료되었지만 완료 사진 저장에 실패했습니다. 다시 시도해주세요.');
alert(' 저장은 완료되었지만 완료 사진 저장에 실패했습니다. 다시 시도해주세요.');
}
// 모달 내용 업데이트 (완료 사진 표시 갱신)
@@ -2075,7 +2072,7 @@ async function saveAndCompleteIssue(issueId) {
if (completionPhotoElement && completionPhotoElement.files[0]) {
try {
const file = completionPhotoElement.files[0];
console.log('🔍 업로드할 파일 정보:', {
console.log(' 업로드할 파일 정보:', {
name: file.name,
size: file.size,
type: file.type,
@@ -2083,12 +2080,8 @@ async function saveAndCompleteIssue(issueId) {
});
const base64 = await fileToBase64(file);
console.log('🔍 Base64 변환 완료 - 전체 길이:', base64.length);
console.log('🔍 Base64 헤더:', base64.substring(0, 50));
completionPhoto = base64.split(',')[1]; // Base64 데이터만 추출
console.log('🔍 헤더 제거 후 길이:', completionPhoto.length);
console.log('🔍 전송할 Base64 시작 부분:', completionPhoto.substring(0, 50));
} catch (error) {
console.error('파일 변환 오류:', error);
alert('완료 사진 업로드 중 오류가 발생했습니다.');

View File

@@ -3,20 +3,28 @@
* M-Project 작업보고서 시스템
*/
const CACHE_NAME = 'mproject-v1.0.3';
const STATIC_CACHE = 'mproject-static-v1.0.3';
const DYNAMIC_CACHE = 'mproject-dynamic-v1.0.3';
const CACHE_NAME = 'mproject-v1.1.0';
const STATIC_CACHE = 'mproject-static-v1.1.0';
const DYNAMIC_CACHE = 'mproject-dynamic-v1.1.0';
// 캐시할 정적 리소스
const STATIC_ASSETS = [
'/',
'/index.html',
'/app.html',
'/issue-view.html',
'/daily-work.html',
'/project-management.html',
'/admin.html',
'/issues-dashboard.html',
'/issues-inbox.html',
'/issues-management.html',
'/issues-archive.html',
'/ai-assistant.html',
'/reports.html',
'/reports-daily.html',
'/reports-weekly.html',
'/reports-monthly.html',
'/static/js/api.js',
'/static/js/app.js',
'/static/js/core/permissions.js',
'/static/js/core/auth-manager.js',
'/static/js/components/common-header.js',
'/static/js/core/page-manager.js',
'/static/js/core/page-preloader.js',
@@ -60,20 +68,20 @@ const CACHE_STRATEGIES = {
* 서비스 워커 설치
*/
self.addEventListener('install', (event) => {
console.log('🔧 서비스 워커 설치 중...');
console.log(' 서비스 워커 설치 중...');
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('📦 정적 리소스 캐싱 중...');
console.log(' 정적 리소스 캐싱 중...');
return cache.addAll(STATIC_ASSETS);
})
.then(() => {
console.log(' 서비스 워커 설치 완료');
console.log(' 서비스 워커 설치 완료');
return self.skipWaiting();
})
.catch((error) => {
console.error(' 서비스 워커 설치 실패:', error);
console.error(' 서비스 워커 설치 실패:', error);
})
);
});
@@ -82,7 +90,7 @@ self.addEventListener('install', (event) => {
* 서비스 워커 활성화
*/
self.addEventListener('activate', (event) => {
console.log('🚀 서비스 워커 활성화 중...');
console.log(' 서비스 워커 활성화 중...');
event.waitUntil(
caches.keys()
@@ -93,14 +101,14 @@ self.addEventListener('activate', (event) => {
if (cacheName !== STATIC_CACHE &&
cacheName !== DYNAMIC_CACHE &&
cacheName !== CACHE_NAME) {
console.log('🗑️ 이전 캐시 삭제:', cacheName);
console.log(' 이전 캐시 삭제:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log(' 서비스 워커 활성화 완료');
console.log(' 서비스 워커 활성화 완료');
return self.clients.claim();
})
);
@@ -251,7 +259,7 @@ function isCDNResource(url) {
async function handleOffline(request) {
// HTML 요청에 대한 오프라인 페이지
if (request.destination === 'document') {
const offlinePage = await caches.match('/index.html');
const offlinePage = await caches.match('/app.html');
if (offlinePage) {
return offlinePage;
}
@@ -308,7 +316,7 @@ async function clearAllCaches() {
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
console.log('🗑️ 모든 캐시 정리 완료');
console.log(' 모든 캐시 정리 완료');
}
/**
@@ -318,7 +326,7 @@ async function cachePage(url) {
try {
const cache = await caches.open(DYNAMIC_CACHE);
await cache.add(url);
console.log('📦 페이지 캐시 완료:', url);
console.log(' 페이지 캐시 완료:', url);
} catch (error) {
console.error('페이지 캐시 실패:', url, error);
}