하이라이트 색상 문제 해결 및 다중 하이라이트 렌더링 개선

주요 수정사항:
- 하이라이트 생성 시 color → highlight_color 필드명 수정으로 색상 전달 문제 해결
- 분홍색을 더 연하게 변경하여 글씨 가독성 향상
- 다중 하이라이트 렌더링을 위아래 균등 분할로 개선
- CSS highlight-span 클래스 추가 및 색상 적용 강화
- 하이라이트 생성/렌더링 과정에 상세한 디버깅 로그 추가

UI 개선:
- 단일 하이라이트: 선택한 색상으로 정확히 표시
- 다중 하이라이트: 위아래로 균등하게 색상 분할 표시
- 메모 입력 모달에서 선택된 텍스트 표시 개선

버그 수정:
- 프론트엔드-백엔드 API 스키마 불일치 해결
- CSS 스타일 우선순위 문제 해결
- 하이라이트 색상이 노랑색으로만 표시되던 문제 해결
This commit is contained in:
Hyungi Ahn
2025-08-28 07:13:00 +09:00
parent 3e0a03f149
commit 5d4465b15c
18 changed files with 5569 additions and 648 deletions

View File

@@ -0,0 +1,396 @@
/**
* CacheManager - 데이터 캐싱 및 로컬 스토리지 관리
* API 응답, 문서 데이터, 사용자 설정 등을 효율적으로 캐싱합니다.
*/
class CacheManager {
constructor() {
console.log('💾 CacheManager 초기화 시작');
// 캐시 설정
this.config = {
// 캐시 만료 시간 (밀리초)
ttl: {
document: 30 * 60 * 1000, // 문서: 30분
highlights: 10 * 60 * 1000, // 하이라이트: 10분
notes: 10 * 60 * 1000, // 메모: 10분
bookmarks: 15 * 60 * 1000, // 북마크: 15분
links: 15 * 60 * 1000, // 링크: 15분
navigation: 60 * 60 * 1000, // 네비게이션: 1시간
userSettings: 24 * 60 * 60 * 1000 // 사용자 설정: 24시간
},
// 캐시 키 접두사
prefix: 'docviewer_',
// 최대 캐시 크기 (항목 수)
maxItems: 100,
// 로컬 스토리지 사용 여부
useLocalStorage: true
};
// 메모리 캐시 (빠른 접근용)
this.memoryCache = new Map();
// 캐시 통계
this.stats = {
hits: 0,
misses: 0,
sets: 0,
evictions: 0
};
// 초기화 시 오래된 캐시 정리
this.cleanupExpiredCache();
console.log('✅ CacheManager 초기화 완료');
}
/**
* 캐시에서 데이터 가져오기
*/
get(key, category = 'default') {
const fullKey = this.getFullKey(key, category);
// 1. 메모리 캐시에서 먼저 확인
if (this.memoryCache.has(fullKey)) {
const cached = this.memoryCache.get(fullKey);
if (this.isValid(cached)) {
this.stats.hits++;
console.log(`💾 메모리 캐시 HIT: ${fullKey}`);
return cached.data;
} else {
// 만료된 캐시 제거
this.memoryCache.delete(fullKey);
}
}
// 2. 로컬 스토리지에서 확인
if (this.config.useLocalStorage) {
try {
const stored = localStorage.getItem(fullKey);
if (stored) {
const cached = JSON.parse(stored);
if (this.isValid(cached)) {
// 메모리 캐시에도 저장
this.memoryCache.set(fullKey, cached);
this.stats.hits++;
console.log(`💾 로컬 스토리지 캐시 HIT: ${fullKey}`);
return cached.data;
} else {
// 만료된 캐시 제거
localStorage.removeItem(fullKey);
}
}
} catch (error) {
console.warn('로컬 스토리지 읽기 오류:', error);
}
}
this.stats.misses++;
console.log(`💾 캐시 MISS: ${fullKey}`);
return null;
}
/**
* 캐시에 데이터 저장
*/
set(key, data, category = 'default', customTtl = null) {
const fullKey = this.getFullKey(key, category);
const ttl = customTtl || this.config.ttl[category] || this.config.ttl.default || 10 * 60 * 1000;
const cached = {
data: data,
timestamp: Date.now(),
ttl: ttl,
category: category,
size: this.estimateSize(data)
};
// 메모리 캐시에 저장
this.memoryCache.set(fullKey, cached);
// 로컬 스토리지에 저장
if (this.config.useLocalStorage) {
try {
localStorage.setItem(fullKey, JSON.stringify(cached));
} catch (error) {
console.warn('로컬 스토리지 저장 오류 (용량 부족?):', error);
// 용량 부족 시 오래된 캐시 정리 후 재시도
this.cleanupOldCache();
try {
localStorage.setItem(fullKey, JSON.stringify(cached));
} catch (retryError) {
console.error('로컬 스토리지 저장 재시도 실패:', retryError);
}
}
}
this.stats.sets++;
console.log(`💾 캐시 저장: ${fullKey} (TTL: ${ttl}ms)`);
// 캐시 크기 제한 확인
this.enforceMaxItems();
}
/**
* 특정 키 또는 카테고리의 캐시 삭제
*/
delete(key, category = 'default') {
const fullKey = this.getFullKey(key, category);
// 메모리 캐시에서 삭제
this.memoryCache.delete(fullKey);
// 로컬 스토리지에서 삭제
if (this.config.useLocalStorage) {
localStorage.removeItem(fullKey);
}
console.log(`💾 캐시 삭제: ${fullKey}`);
}
/**
* 카테고리별 캐시 전체 삭제
*/
deleteCategory(category) {
const prefix = this.getFullKey('', category);
// 메모리 캐시에서 삭제
for (const key of this.memoryCache.keys()) {
if (key.startsWith(prefix)) {
this.memoryCache.delete(key);
}
}
// 로컬 스토리지에서 삭제
if (this.config.useLocalStorage) {
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key && key.startsWith(prefix)) {
localStorage.removeItem(key);
}
}
}
console.log(`💾 카테고리 캐시 삭제: ${category}`);
}
/**
* 모든 캐시 삭제
*/
clear() {
// 메모리 캐시 삭제
this.memoryCache.clear();
// 로컬 스토리지에서 관련 캐시만 삭제
if (this.config.useLocalStorage) {
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key && key.startsWith(this.config.prefix)) {
localStorage.removeItem(key);
}
}
}
// 통계 초기화
this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 };
console.log('💾 모든 캐시 삭제 완료');
}
/**
* 캐시 유효성 검사
*/
isValid(cached) {
if (!cached || !cached.timestamp || !cached.ttl) {
return false;
}
const age = Date.now() - cached.timestamp;
return age < cached.ttl;
}
/**
* 만료된 캐시 정리
*/
cleanupExpiredCache() {
console.log('🧹 만료된 캐시 정리 시작');
let cleanedCount = 0;
// 메모리 캐시 정리
for (const [key, cached] of this.memoryCache.entries()) {
if (!this.isValid(cached)) {
this.memoryCache.delete(key);
cleanedCount++;
}
}
// 로컬 스토리지 정리
if (this.config.useLocalStorage) {
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key && key.startsWith(this.config.prefix)) {
try {
const stored = localStorage.getItem(key);
if (stored) {
const cached = JSON.parse(stored);
if (!this.isValid(cached)) {
localStorage.removeItem(key);
cleanedCount++;
}
}
} catch (error) {
// 파싱 오류 시 해당 캐시 삭제
localStorage.removeItem(key);
cleanedCount++;
}
}
}
}
console.log(`🧹 만료된 캐시 ${cleanedCount}개 정리 완료`);
}
/**
* 오래된 캐시 정리 (용량 부족 시)
*/
cleanupOldCache() {
console.log('🧹 오래된 캐시 정리 시작');
const items = [];
// 로컬 스토리지의 모든 캐시 항목 수집
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(this.config.prefix)) {
try {
const stored = localStorage.getItem(key);
if (stored) {
const cached = JSON.parse(stored);
items.push({ key, cached });
}
} catch (error) {
// 파싱 오류 시 해당 캐시 삭제
localStorage.removeItem(key);
}
}
}
// 타임스탬프 기준으로 정렬 (오래된 것부터)
items.sort((a, b) => a.cached.timestamp - b.cached.timestamp);
// 오래된 항목의 절반 삭제
const deleteCount = Math.floor(items.length / 2);
for (let i = 0; i < deleteCount; i++) {
localStorage.removeItem(items[i].key);
this.memoryCache.delete(items[i].key);
}
console.log(`🧹 오래된 캐시 ${deleteCount}개 정리 완료`);
}
/**
* 최대 항목 수 제한 적용
*/
enforceMaxItems() {
if (this.memoryCache.size > this.config.maxItems) {
const excess = this.memoryCache.size - this.config.maxItems;
const keys = Array.from(this.memoryCache.keys());
// 오래된 항목부터 삭제
for (let i = 0; i < excess; i++) {
this.memoryCache.delete(keys[i]);
this.stats.evictions++;
}
console.log(`💾 캐시 크기 제한으로 ${excess}개 항목 제거`);
}
}
/**
* 전체 키 생성
*/
getFullKey(key, category) {
return `${this.config.prefix}${category}_${key}`;
}
/**
* 데이터 크기 추정
*/
estimateSize(data) {
try {
return JSON.stringify(data).length;
} catch {
return 0;
}
}
/**
* 캐시 통계 조회
*/
getStats() {
const hitRate = this.stats.hits + this.stats.misses > 0
? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2)
: 0;
return {
...this.stats,
hitRate: `${hitRate}%`,
memoryItems: this.memoryCache.size,
localStorageItems: this.getLocalStorageItemCount()
};
}
/**
* 로컬 스토리지 항목 수 조회
*/
getLocalStorageItemCount() {
let count = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(this.config.prefix)) {
count++;
}
}
return count;
}
/**
* 캐시 상태 리포트
*/
getReport() {
const stats = this.getStats();
const memoryUsage = Array.from(this.memoryCache.values())
.reduce((total, cached) => total + (cached.size || 0), 0);
return {
stats,
memoryUsage: `${(memoryUsage / 1024).toFixed(2)} KB`,
categories: this.getCategoryStats(),
config: this.config
};
}
/**
* 카테고리별 통계
*/
getCategoryStats() {
const categories = {};
for (const [key, cached] of this.memoryCache.entries()) {
const category = cached.category || 'default';
if (!categories[category]) {
categories[category] = { count: 0, size: 0 };
}
categories[category].count++;
categories[category].size += cached.size || 0;
}
return categories;
}
}
// 전역 캐시 매니저 인스턴스
window.cacheManager = new CacheManager();
// 전역으로 내보내기
window.CacheManager = CacheManager;

View File

@@ -0,0 +1,489 @@
/**
* CachedAPI - 캐싱이 적용된 API 래퍼
* 기존 DocumentServerAPI를 확장하여 캐싱 기능을 추가합니다.
*/
class CachedAPI {
constructor(baseAPI) {
this.api = baseAPI;
this.cache = window.cacheManager;
console.log('🚀 CachedAPI 초기화 완료');
}
/**
* 캐싱이 적용된 GET 요청
*/
async get(endpoint, params = {}, options = {}) {
const {
useCache = true,
category = 'api',
ttl = null,
forceRefresh = false
} = options;
// 캐시 키 생성
const cacheKey = this.generateCacheKey(endpoint, params);
// 강제 새로고침이 아니고 캐시 사용 설정인 경우 캐시 확인
if (useCache && !forceRefresh) {
const cached = this.cache.get(cacheKey, category);
if (cached) {
console.log(`🚀 API 캐시 사용: ${endpoint}`);
return cached;
}
}
try {
console.log(`🌐 API 호출: ${endpoint}`);
// 실제 백엔드 API 엔드포인트로 매핑
let response;
if (endpoint === '/highlights' && params.document_id) {
// 실제: /highlights/document/{documentId}
response = await this.api.get(`/highlights/document/${params.document_id}`);
} else if (endpoint === '/notes' && params.document_id) {
// 실제: /notes/document/{documentId}
response = await this.api.get(`/notes/document/${params.document_id}`);
} else if (endpoint === '/bookmarks' && params.document_id) {
// 실제: /bookmarks/document/{documentId}
response = await this.api.get(`/bookmarks/document/${params.document_id}`);
} else if (endpoint === '/document-links' && params.document_id) {
// 실제: /documents/{documentId}/links
response = await this.api.get(`/documents/${params.document_id}/links`);
} else if (endpoint === '/document-links/backlinks' && params.target_document_id) {
// 실제: /documents/{documentId}/backlinks
response = await this.api.get(`/documents/${params.target_document_id}/backlinks`);
} else if (endpoint.startsWith('/documents/') && endpoint.includes('/')) {
const documentId = endpoint.split('/')[2];
response = await this.api.getDocument(documentId);
} else {
// 기본 API 호출 (기존 방식)
response = await this.api.get(endpoint, params);
}
// 성공적인 응답만 캐시에 저장
if (useCache && response) {
this.cache.set(cacheKey, response, category, ttl);
console.log(`💾 API 응답 캐시 저장: ${endpoint}`);
}
return response;
} catch (error) {
console.error(`❌ API 호출 실패: ${endpoint}`, error);
throw error;
}
}
/**
* 캐싱이 적용된 POST 요청 (일반적으로 캐시하지 않음)
*/
async post(endpoint, data = {}, options = {}) {
const {
invalidateCache = true,
invalidateCategories = []
} = options;
try {
const response = await this.api.post(endpoint, data);
// POST 후 관련 캐시 무효화
if (invalidateCache) {
this.invalidateRelatedCache(endpoint, invalidateCategories);
}
return response;
} catch (error) {
console.error(`❌ API POST 실패: ${endpoint}`, error);
throw error;
}
}
/**
* 캐싱이 적용된 PUT 요청
*/
async put(endpoint, data = {}, options = {}) {
const {
invalidateCache = true,
invalidateCategories = []
} = options;
try {
const response = await this.api.put(endpoint, data);
// PUT 후 관련 캐시 무효화
if (invalidateCache) {
this.invalidateRelatedCache(endpoint, invalidateCategories);
}
return response;
} catch (error) {
console.error(`❌ API PUT 실패: ${endpoint}`, error);
throw error;
}
}
/**
* 캐싱이 적용된 DELETE 요청
*/
async delete(endpoint, options = {}) {
const {
invalidateCache = true,
invalidateCategories = []
} = options;
try {
const response = await this.api.delete(endpoint);
// DELETE 후 관련 캐시 무효화
if (invalidateCache) {
this.invalidateRelatedCache(endpoint, invalidateCategories);
}
return response;
} catch (error) {
console.error(`❌ API DELETE 실패: ${endpoint}`, error);
throw error;
}
}
/**
* 문서 데이터 조회 (캐싱 최적화)
*/
async getDocument(documentId, contentType = 'document') {
const cacheKey = `document_${documentId}_${contentType}`;
// 캐시 확인
const cached = this.cache.get(cacheKey, 'document');
if (cached) {
console.log(`🚀 문서 캐시 사용: ${documentId}`);
return cached;
}
// 기존 API 메서드 직접 사용
try {
const result = await this.api.getDocument(documentId);
// 캐시에 저장
this.cache.set(cacheKey, result, 'document', 30 * 60 * 1000);
console.log(`💾 문서 캐시 저장: ${documentId}`);
return result;
} catch (error) {
console.error('문서 로드 실패:', error);
throw error;
}
}
/**
* 하이라이트 조회 (캐싱 최적화)
*/
async getHighlights(documentId, contentType = 'document') {
const cacheKey = `highlights_${documentId}_${contentType}`;
// 캐시 확인
const cached = this.cache.get(cacheKey, 'highlights');
if (cached) {
console.log(`🚀 하이라이트 캐시 사용: ${documentId}`);
return cached;
}
// 기존 API 메서드 직접 사용
try {
let result;
if (contentType === 'note') {
result = await this.api.get(`/note/${documentId}/highlights`).catch(() => []);
} else {
result = await this.api.getDocumentHighlights(documentId).catch(() => []);
}
// 캐시에 저장
this.cache.set(cacheKey, result, 'highlights', 10 * 60 * 1000);
console.log(`💾 하이라이트 캐시 저장: ${documentId}`);
return result;
} catch (error) {
console.error('하이라이트 로드 실패:', error);
return [];
}
}
/**
* 메모 조회 (캐싱 최적화)
*/
async getNotes(documentId, contentType = 'document') {
const cacheKey = `notes_${documentId}_${contentType}`;
// 캐시 확인
const cached = this.cache.get(cacheKey, 'notes');
if (cached) {
console.log(`🚀 메모 캐시 사용: ${documentId}`);
return cached;
}
// 기존 API 메서드 직접 사용
try {
let result;
if (contentType === 'note') {
result = await this.api.get(`/note/${documentId}/notes`).catch(() => []);
} else {
result = await this.api.getDocumentNotes(documentId).catch(() => []);
}
// 캐시에 저장
this.cache.set(cacheKey, result, 'notes', 10 * 60 * 1000);
console.log(`💾 메모 캐시 저장: ${documentId}`);
return result;
} catch (error) {
console.error('메모 로드 실패:', error);
return [];
}
}
/**
* 북마크 조회 (캐싱 최적화)
*/
async getBookmarks(documentId) {
const cacheKey = `bookmarks_${documentId}`;
// 캐시 확인
const cached = this.cache.get(cacheKey, 'bookmarks');
if (cached) {
console.log(`🚀 북마크 캐시 사용: ${documentId}`);
return cached;
}
// 기존 API 메서드 직접 사용
try {
const result = await this.api.getDocumentBookmarks(documentId).catch(() => []);
// 캐시에 저장
this.cache.set(cacheKey, result, 'bookmarks', 15 * 60 * 1000);
console.log(`💾 북마크 캐시 저장: ${documentId}`);
return result;
} catch (error) {
console.error('북마크 로드 실패:', error);
return [];
}
}
/**
* 문서 링크 조회 (캐싱 최적화)
*/
async getDocumentLinks(documentId) {
const cacheKey = `links_${documentId}`;
// 캐시 확인
const cached = this.cache.get(cacheKey, 'links');
if (cached) {
console.log(`🚀 문서 링크 캐시 사용: ${documentId}`);
return cached;
}
// 기존 API 메서드 직접 사용
try {
const result = await this.api.getDocumentLinks(documentId).catch(() => []);
// 캐시에 저장
this.cache.set(cacheKey, result, 'links', 15 * 60 * 1000);
console.log(`💾 문서 링크 캐시 저장: ${documentId}`);
return result;
} catch (error) {
console.error('문서 링크 로드 실패:', error);
return [];
}
}
/**
* 백링크 조회 (캐싱 최적화)
*/
async getBacklinks(documentId) {
const cacheKey = `backlinks_${documentId}`;
// 캐시 확인
const cached = this.cache.get(cacheKey, 'links');
if (cached) {
console.log(`🚀 백링크 캐시 사용: ${documentId}`);
return cached;
}
// 기존 API 메서드 직접 사용
try {
const result = await this.api.getDocumentBacklinks(documentId).catch(() => []);
// 캐시에 저장
this.cache.set(cacheKey, result, 'links', 15 * 60 * 1000);
console.log(`💾 백링크 캐시 저장: ${documentId}`);
return result;
} catch (error) {
console.error('백링크 로드 실패:', error);
return [];
}
}
/**
* 네비게이션 정보 조회 (캐싱 최적화)
*/
async getNavigation(documentId, contentType = 'document') {
return await this.get('/documents/navigation', { document_id: documentId, content_type: contentType }, {
category: 'navigation',
ttl: 60 * 60 * 1000 // 1시간
});
}
/**
* 하이라이트 생성 (캐시 무효화)
*/
async createHighlight(data) {
return await this.post('/highlights/', data, {
invalidateCategories: ['highlights', 'notes']
});
}
/**
* 메모 생성 (캐시 무효화)
*/
async createNote(data) {
return await this.post('/notes/', data, {
invalidateCategories: ['notes', 'highlights']
});
}
/**
* 북마크 생성 (캐시 무효화)
*/
async createBookmark(data) {
return await this.post('/bookmarks/', data, {
invalidateCategories: ['bookmarks']
});
}
/**
* 링크 생성 (캐시 무효화)
*/
async createDocumentLink(data) {
return await this.post('/document-links/', data, {
invalidateCategories: ['links']
});
}
/**
* 캐시 키 생성
*/
generateCacheKey(endpoint, params) {
const sortedParams = Object.keys(params)
.sort()
.reduce((result, key) => {
result[key] = params[key];
return result;
}, {});
return `${endpoint}_${JSON.stringify(sortedParams)}`;
}
/**
* 관련 캐시 무효화
*/
invalidateRelatedCache(endpoint, categories = []) {
console.log(`🗑️ 캐시 무효화: ${endpoint}`);
// 기본 무효화 규칙
const defaultInvalidations = {
'/highlights': ['highlights', 'notes'],
'/notes': ['notes', 'highlights'],
'/bookmarks': ['bookmarks'],
'/document-links': ['links']
};
// 엔드포인트별 기본 무효화 적용
for (const [pattern, cats] of Object.entries(defaultInvalidations)) {
if (endpoint.includes(pattern)) {
cats.forEach(cat => this.cache.deleteCategory(cat));
}
}
// 추가 무효화 카테고리 적용
categories.forEach(category => {
this.cache.deleteCategory(category);
});
}
/**
* 특정 문서의 모든 캐시 무효화
*/
invalidateDocumentCache(documentId) {
console.log(`🗑️ 문서 캐시 무효화: ${documentId}`);
const categories = ['document', 'highlights', 'notes', 'bookmarks', 'links', 'navigation'];
categories.forEach(category => {
// 해당 문서 ID가 포함된 캐시만 삭제하는 것이 이상적이지만,
// 간단하게 전체 카테고리를 무효화
this.cache.deleteCategory(category);
});
}
/**
* 캐시 강제 새로고침
*/
async refreshCache(endpoint, params = {}, category = 'api') {
return await this.get(endpoint, params, {
category,
forceRefresh: true
});
}
/**
* 캐시 통계 조회
*/
getCacheStats() {
return this.cache.getStats();
}
/**
* 캐시 리포트 조회
*/
getCacheReport() {
return this.cache.getReport();
}
/**
* 모든 캐시 삭제
*/
clearAllCache() {
this.cache.clear();
console.log('🗑️ 모든 API 캐시 삭제 완료');
}
// 기존 API 메서드들을 그대로 위임 (캐싱이 필요 없는 경우)
setToken(token) {
return this.api.setToken(token);
}
getHeaders() {
return this.api.getHeaders();
}
/**
* 문서 네비게이션 정보 조회 (캐싱 최적화)
*/
async getDocumentNavigation(documentId) {
const cacheKey = `navigation_${documentId}`;
return await this.get(`/documents/${documentId}/navigation`, {}, {
category: 'navigation',
cacheKey,
ttl: 30 * 60 * 1000 // 30분 (네비게이션은 자주 변경되지 않음)
});
}
}
// 기존 api 인스턴스를 캐싱 API로 래핑
if (window.api) {
window.cachedApi = new CachedAPI(window.api);
console.log('🚀 CachedAPI 래퍼 생성 완료');
}
// 전역으로 내보내기
window.CachedAPI = CachedAPI;

View File

@@ -0,0 +1,223 @@
/**
* ModuleLoader - 지연 로딩 및 모듈 관리
* 필요한 모듈만 동적으로 로드하여 성능을 최적화합니다.
*/
class ModuleLoader {
constructor() {
console.log('🔧 ModuleLoader 초기화 시작');
// 로드된 모듈 캐시
this.loadedModules = new Map();
// 로딩 중인 모듈 Promise 캐시 (중복 로딩 방지)
this.loadingPromises = new Map();
// 모듈 의존성 정의
this.moduleDependencies = {
'DocumentLoader': [],
'HighlightManager': ['DocumentLoader'],
'BookmarkManager': ['DocumentLoader'],
'LinkManager': ['DocumentLoader'],
'UIManager': []
};
// 모듈 경로 정의
this.modulePaths = {
'DocumentLoader': '/static/js/viewer/core/document-loader.js',
'HighlightManager': '/static/js/viewer/features/highlight-manager.js',
'BookmarkManager': '/static/js/viewer/features/bookmark-manager.js',
'LinkManager': '/static/js/viewer/features/link-manager.js',
'UIManager': '/static/js/viewer/features/ui-manager.js'
};
// 캐시 버스팅을 위한 버전
this.version = '2025012607';
console.log('✅ ModuleLoader 초기화 완료');
}
/**
* 모듈 동적 로드
*/
async loadModule(moduleName) {
// 이미 로드된 모듈인지 확인
if (this.loadedModules.has(moduleName)) {
console.log(`✅ 모듈 캐시에서 반환: ${moduleName}`);
return this.loadedModules.get(moduleName);
}
// 이미 로딩 중인 모듈인지 확인 (중복 로딩 방지)
if (this.loadingPromises.has(moduleName)) {
console.log(`⏳ 모듈 로딩 대기 중: ${moduleName}`);
return await this.loadingPromises.get(moduleName);
}
console.log(`🔄 모듈 로딩 시작: ${moduleName}`);
// 로딩 Promise 생성 및 캐시
const loadingPromise = this._loadModuleScript(moduleName);
this.loadingPromises.set(moduleName, loadingPromise);
try {
const moduleClass = await loadingPromise;
// 로딩 완료 후 캐시에 저장
this.loadedModules.set(moduleName, moduleClass);
this.loadingPromises.delete(moduleName);
console.log(`✅ 모듈 로딩 완료: ${moduleName}`);
return moduleClass;
} catch (error) {
console.error(`❌ 모듈 로딩 실패: ${moduleName}`, error);
this.loadingPromises.delete(moduleName);
throw error;
}
}
/**
* 의존성을 포함한 모듈 로드
*/
async loadModuleWithDependencies(moduleName) {
console.log(`🔗 의존성 포함 모듈 로딩: ${moduleName}`);
// 의존성 먼저 로드
const dependencies = this.moduleDependencies[moduleName] || [];
if (dependencies.length > 0) {
console.log(`📦 의존성 로딩: ${dependencies.join(', ')}`);
await Promise.all(dependencies.map(dep => this.loadModule(dep)));
}
// 메인 모듈 로드
return await this.loadModule(moduleName);
}
/**
* 여러 모듈 병렬 로드
*/
async loadModules(moduleNames) {
console.log(`🚀 병렬 모듈 로딩: ${moduleNames.join(', ')}`);
const loadPromises = moduleNames.map(name => this.loadModuleWithDependencies(name));
const results = await Promise.all(loadPromises);
console.log(`✅ 병렬 모듈 로딩 완료: ${moduleNames.join(', ')}`);
return results;
}
/**
* 스크립트 동적 로딩
*/
async _loadModuleScript(moduleName) {
return new Promise((resolve, reject) => {
// 이미 전역에 클래스가 있는지 확인
if (window[moduleName]) {
console.log(`✅ 모듈 이미 로드됨: ${moduleName}`);
resolve(window[moduleName]);
return;
}
console.log(`📥 스크립트 로딩 시작: ${moduleName}`);
const script = document.createElement('script');
script.src = `${this.modulePaths[moduleName]}?v=${this.version}`;
script.async = true;
script.onload = () => {
console.log(`📥 스크립트 로드 완료: ${moduleName}`);
// 스크립트 로드 후 잠시 대기 (클래스 등록 시간)
setTimeout(() => {
if (window[moduleName]) {
console.log(`✅ 모듈 클래스 확인: ${moduleName}`);
resolve(window[moduleName]);
} else {
console.error(`❌ 모듈 클래스 없음: ${moduleName}`, Object.keys(window).filter(k => k.includes('Manager') || k.includes('Loader')));
reject(new Error(`모듈 클래스를 찾을 수 없음: ${moduleName}`));
}
}, 10); // 10ms 대기
};
script.onerror = (error) => {
console.error(`❌ 스크립트 로딩 실패: ${moduleName}`, error);
reject(new Error(`스크립트 로딩 실패: ${moduleName}`));
};
document.head.appendChild(script);
});
}
/**
* 모듈 인스턴스 생성 (팩토리 패턴)
*/
async createModuleInstance(moduleName, ...args) {
const ModuleClass = await this.loadModuleWithDependencies(moduleName);
return new ModuleClass(...args);
}
/**
* 모듈 프리로딩 (백그라운드에서 미리 로드)
*/
async preloadModules(moduleNames) {
console.log(`🔮 모듈 프리로딩: ${moduleNames.join(', ')}`);
// 백그라운드에서 로드 (에러 무시)
const preloadPromises = moduleNames.map(async (name) => {
try {
await this.loadModuleWithDependencies(name);
console.log(`✅ 프리로딩 완료: ${name}`);
} catch (error) {
console.warn(`⚠️ 프리로딩 실패: ${name}`, error);
}
});
// 모든 프리로딩이 완료될 때까지 기다리지 않음
Promise.all(preloadPromises);
}
/**
* 사용하지 않는 모듈 언로드 (메모리 최적화)
*/
unloadModule(moduleName) {
if (this.loadedModules.has(moduleName)) {
this.loadedModules.delete(moduleName);
console.log(`🗑️ 모듈 언로드: ${moduleName}`);
}
}
/**
* 모든 모듈 언로드
*/
unloadAllModules() {
this.loadedModules.clear();
this.loadingPromises.clear();
console.log('🗑️ 모든 모듈 언로드 완료');
}
/**
* 로드된 모듈 상태 확인
*/
getLoadedModules() {
return Array.from(this.loadedModules.keys());
}
/**
* 메모리 사용량 추정
*/
getMemoryUsage() {
const loadedCount = this.loadedModules.size;
const loadingCount = this.loadingPromises.size;
return {
loadedModules: loadedCount,
loadingModules: loadingCount,
totalModules: Object.keys(this.modulePaths).length,
memoryEstimate: `${loadedCount * 50}KB` // 대략적인 추정
};
}
}
// 전역 모듈 로더 인스턴스
window.moduleLoader = new ModuleLoader();
// 전역으로 내보내기
window.ModuleLoader = ModuleLoader;