2025-08-19 12:51:49 +09:00
class FileManager {
constructor ( ) {
this . files = [ ] ;
this . currentEditId = null ;
this . currentUser = null ;
this . isOnline = navigator . onLine ;
this . realtimeSubscription = null ;
this . authMode = 'login' ; // 'login' or 'signup'
this . init ( ) ;
}
async init ( ) {
// Supabase 초기화
const supabaseInitialized = initializeSupabase ( ) ;
if ( supabaseInitialized ) {
console . log ( '✅ Supabase 모드로 실행합니다.' ) ;
await this . initializeAuth ( ) ;
} else {
console . log ( '⚠️ 오프라인 모드로 실행합니다.' ) ;
this . files = this . loadFiles ( ) ;
this . showOfflineMode ( ) ;
}
this . bindEvents ( ) ;
this . renderFiles ( ) ;
this . updateEmptyState ( ) ;
this . setupOnlineStatusListener ( ) ;
}
bindEvents ( ) {
// 기존 이벤트
document . getElementById ( 'fileForm' ) . addEventListener ( 'submit' , ( e ) => this . handleSubmit ( e ) ) ;
document . getElementById ( 'cancelBtn' ) . addEventListener ( 'click' , ( ) => this . clearForm ( ) ) ;
document . getElementById ( 'searchBtn' ) . addEventListener ( 'click' , ( ) => this . handleSearch ( ) ) ;
document . getElementById ( 'searchInput' ) . addEventListener ( 'keyup' , ( e ) => {
if ( e . key === 'Enter' ) this . handleSearch ( ) ;
} ) ;
document . getElementById ( 'categoryFilter' ) . addEventListener ( 'change' , ( ) => this . handleSearch ( ) ) ;
document . getElementById ( 'sortBy' ) . addEventListener ( 'change' , ( ) => this . renderFiles ( ) ) ;
document . getElementById ( 'editForm' ) . addEventListener ( 'submit' , ( e ) => this . handleEditSubmit ( e ) ) ;
document . getElementById ( 'closeModal' ) . addEventListener ( 'click' , ( ) => this . closeEditModal ( ) ) ;
document . getElementById ( 'fileUpload' ) . addEventListener ( 'change' , ( e ) => this . handleFileUpload ( e ) ) ;
// 인증 이벤트
document . getElementById ( 'loginBtn' ) . addEventListener ( 'click' , ( ) => this . openAuthModal ( 'login' ) ) ;
document . getElementById ( 'signupBtn' ) . addEventListener ( 'click' , ( ) => this . openAuthModal ( 'signup' ) ) ;
document . getElementById ( 'logoutBtn' ) . addEventListener ( 'click' , ( ) => this . handleLogout ( ) ) ;
document . getElementById ( 'authForm' ) . addEventListener ( 'submit' , ( e ) => this . handleAuthSubmit ( e ) ) ;
document . getElementById ( 'authCancelBtn' ) . addEventListener ( 'click' , ( ) => this . closeAuthModal ( ) ) ;
document . getElementById ( 'authSwitchLink' ) . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
this . toggleAuthMode ( ) ;
} ) ;
// 모달 이벤트
window . addEventListener ( 'click' , ( e ) => {
if ( e . target === document . getElementById ( 'editModal' ) ) {
this . closeEditModal ( ) ;
}
if ( e . target === document . getElementById ( 'authModal' ) ) {
this . closeAuthModal ( ) ;
}
} ) ;
}
generateId ( ) {
return Date . now ( ) . toString ( 36 ) + Math . random ( ) . toString ( 36 ) . substr ( 2 ) ;
}
2025-08-19 14:16:52 +09:00
// 파일 확장자 추출 (안전한 형태로)
getFileExtension ( fileName ) {
const lastDotIndex = fileName . lastIndexOf ( '.' ) ;
if ( lastDotIndex > 0 && lastDotIndex < fileName . length - 1 ) {
return fileName . substring ( lastDotIndex ) . toLowerCase ( ) ;
}
return '' ; // 확장자가 없는 경우
}
2025-08-19 14:45:29 +09:00
// 브라우저별 다운로드 폴더 경로 추정
getDownloadFolderPath ( ) {
const userAgent = navigator . userAgent . toLowerCase ( ) ;
const platform = navigator . platform . toLowerCase ( ) ;
if ( platform . includes ( 'win' ) ) {
return '다운로드 폴더 (C:\\Users\\사용자명\\Downloads)' ;
} else if ( platform . includes ( 'mac' ) ) {
return '다운로드 폴더 (~/Downloads)' ;
} else if ( platform . includes ( 'linux' ) ) {
return '다운로드 폴더 (~/Downloads)' ;
} else {
return '브라우저 기본 다운로드 폴더' ;
}
}
2025-08-19 12:51:49 +09:00
// 인증 관련 메서드들
async initializeAuth ( ) {
try {
const user = await getCurrentUser ( ) ;
if ( user ) {
this . currentUser = user ;
this . updateAuthUI ( true ) ;
await this . loadUserFiles ( ) ;
this . setupRealtimeSubscription ( ) ;
} else {
this . updateAuthUI ( false ) ;
2025-08-19 14:02:13 +09:00
// 게스트 모드: 공개 파일 로드
await this . loadPublicFiles ( ) ;
this . showGuestMode ( ) ;
2025-08-19 12:51:49 +09:00
}
setupAuthListener ( ( event , session ) => {
if ( event === 'SIGNED_IN' ) {
this . currentUser = session . user ;
this . updateAuthUI ( true ) ;
this . loadUserFiles ( ) ;
this . setupRealtimeSubscription ( ) ;
} else if ( event === 'SIGNED_OUT' ) {
this . currentUser = null ;
this . updateAuthUI ( false ) ;
2025-08-19 14:02:13 +09:00
this . loadPublicFiles ( ) ;
this . showGuestMode ( ) ;
2025-08-19 12:51:49 +09:00
this . cleanupRealtimeSubscription ( ) ;
}
} ) ;
} catch ( error ) {
console . error ( '인증 초기화 오류:' , error ) ;
}
}
updateAuthUI ( isAuthenticated ) {
const authButtons = document . getElementById ( 'authButtons' ) ;
const userInfo = document . getElementById ( 'userInfo' ) ;
const userEmail = document . getElementById ( 'userEmail' ) ;
2025-08-19 14:02:13 +09:00
const formSection = document . querySelector ( '.form-section' ) ;
2025-08-19 12:51:49 +09:00
if ( isAuthenticated && this . currentUser ) {
authButtons . style . display = 'none' ;
userInfo . style . display = 'flex' ;
userEmail . textContent = this . currentUser . email ;
2025-08-19 14:02:13 +09:00
formSection . style . display = 'block' ;
2025-08-19 12:51:49 +09:00
this . updateSyncStatus ( ) ;
2025-08-19 14:02:13 +09:00
this . hideGuestMode ( ) ;
2025-08-19 12:51:49 +09:00
} else {
authButtons . style . display = 'flex' ;
userInfo . style . display = 'none' ;
2025-08-19 14:02:13 +09:00
formSection . style . display = 'none' ;
2025-08-19 12:51:49 +09:00
}
}
updateSyncStatus ( status = 'auto' ) {
const syncStatusElement = document . getElementById ( 'syncStatus' ) ;
if ( ! syncStatusElement ) return ;
// 자동 상태 판단
if ( status === 'auto' ) {
if ( ! isSupabaseConfigured ( ) ) {
status = 'offline' ;
} else if ( this . currentUser ) {
status = 'online' ;
} else {
status = 'offline' ;
}
}
// 상태 업데이트
syncStatusElement . className = ` sync-status ${ status } ` ;
switch ( status ) {
case 'online' :
syncStatusElement . textContent = '🟢 온라인' ;
break ;
case 'offline' :
syncStatusElement . textContent = '🟡 오프라인' ;
break ;
case 'syncing' :
syncStatusElement . textContent = '🔄 동기화 중' ;
break ;
case 'error' :
syncStatusElement . textContent = '🔴 오류' ;
break ;
}
}
openAuthModal ( mode ) {
this . authMode = mode ;
const modal = document . getElementById ( 'authModal' ) ;
const title = document . getElementById ( 'authModalTitle' ) ;
const submitBtn = document . getElementById ( 'authSubmitBtn' ) ;
const confirmPasswordGroup = document . getElementById ( 'confirmPasswordGroup' ) ;
const switchText = document . getElementById ( 'authSwitchText' ) ;
if ( mode === 'signup' ) {
title . textContent = '👤 회원가입' ;
submitBtn . textContent = '👤 회원가입' ;
confirmPasswordGroup . style . display = 'block' ;
switchText . innerHTML = '이미 계정이 있으신가요? <a href="#" id="authSwitchLink">로그인하기</a>' ;
} else {
title . textContent = '🔑 로그인' ;
submitBtn . textContent = '🔑 로그인' ;
confirmPasswordGroup . style . display = 'none' ;
switchText . innerHTML = '계정이 없으신가요? <a href="#" id="authSwitchLink">회원가입하기</a>' ;
}
// 이벤트 리스너 재바인딩
const newSwitchLink = document . getElementById ( 'authSwitchLink' ) ;
newSwitchLink . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
this . toggleAuthMode ( ) ;
} ) ;
modal . style . display = 'block' ;
}
closeAuthModal ( ) {
document . getElementById ( 'authModal' ) . style . display = 'none' ;
document . getElementById ( 'authForm' ) . reset ( ) ;
document . getElementById ( 'authLoading' ) . style . display = 'none' ;
}
toggleAuthMode ( ) {
this . authMode = this . authMode === 'login' ? 'signup' : 'login' ;
this . openAuthModal ( this . authMode ) ;
}
async handleAuthSubmit ( e ) {
e . preventDefault ( ) ;
const email = document . getElementById ( 'authEmail' ) . value . trim ( ) ;
const password = document . getElementById ( 'authPassword' ) . value ;
const confirmPassword = document . getElementById ( 'authConfirmPassword' ) . value ;
if ( ! email || ! password ) {
alert ( '이메일과 비밀번호를 입력해주세요.' ) ;
return ;
}
if ( this . authMode === 'signup' && password !== confirmPassword ) {
alert ( '비밀번호가 일치하지 않습니다.' ) ;
return ;
}
this . showAuthLoading ( true ) ;
try {
if ( this . authMode === 'signup' ) {
await signUp ( email , password ) ;
alert ( '회원가입이 완료되었습니다! 이메일을 확인해주세요.' ) ;
} else {
await signIn ( email , password ) ;
}
this . closeAuthModal ( ) ;
} catch ( error ) {
console . error ( '인증 오류:' , error ) ;
alert ( ` ${ this . authMode === 'signup' ? '회원가입' : '로그인' } 중 오류가 발생했습니다: ${ error . message } ` ) ;
} finally {
this . showAuthLoading ( false ) ;
}
}
async handleLogout ( ) {
try {
await signOut ( ) ;
this . showNotification ( '로그아웃되었습니다.' , 'success' ) ;
} catch ( error ) {
console . error ( '로그아웃 오류:' , error ) ;
alert ( '로그아웃 중 오류가 발생했습니다.' ) ;
}
}
showAuthLoading ( show ) {
const loading = document . getElementById ( 'authLoading' ) ;
const form = document . getElementById ( 'authForm' ) ;
if ( show ) {
loading . style . display = 'block' ;
form . style . display = 'none' ;
} else {
loading . style . display = 'none' ;
form . style . display = 'block' ;
}
}
// 오프라인 모드 관련
showOfflineMode ( ) {
const container = document . querySelector ( '.container' ) ;
const offlineNotice = document . createElement ( 'div' ) ;
offlineNotice . className = 'offline-mode' ;
offlineNotice . innerHTML = '⚠️ 오프라인 모드: 로컬 저장소를 사용합니다. Supabase 설정을 확인해주세요.' ;
container . insertBefore ( offlineNotice , container . firstChild . nextSibling ) ;
}
2025-08-19 14:02:13 +09:00
// 게스트 모드 관련
showGuestMode ( ) {
this . hideGuestMode ( ) ; // 기존 알림 제거
const container = document . querySelector ( '.container' ) ;
const guestNotice = document . createElement ( 'div' ) ;
guestNotice . className = 'guest-mode' ;
guestNotice . id = 'guestModeNotice' ;
guestNotice . innerHTML = `
< div class = "guest-mode-content" >
< span > 👤 게스트 모드 - 파일 보기 및 다운로드만 가능합니다 < / s p a n >
< button onclick = "fileManager.openAuthModal('login')" class = "guest-login-btn" > 🔑 로그인하여 편집하기 < / b u t t o n >
< / d i v >
` ;
container . insertBefore ( guestNotice , container . firstChild . nextSibling ) ;
}
hideGuestMode ( ) {
const guestNotice = document . getElementById ( 'guestModeNotice' ) ;
if ( guestNotice ) {
guestNotice . remove ( ) ;
}
}
// 공개 파일 로드 (게스트용)
async loadPublicFiles ( ) {
if ( isSupabaseConfigured ( ) ) {
try {
// Supabase에서 모든 파일 로드 (RLS로 공개 파일만 접근 가능)
const data = await SupabaseHelper . getFiles ( 'public' ) ;
this . files = data . map ( file => ( {
... file ,
files : file . file _attachments || [ ] ,
isReadOnly : true
} ) ) ;
} catch ( error ) {
console . error ( '공개 파일 로딩 오류:' , error ) ;
// localStorage 폴백
this . files = this . loadFiles ( ) . map ( file => ( { ... file , isReadOnly : true } ) ) ;
}
} else {
// 오프라인 모드: localStorage의 파일을 읽기 전용으로 로드
this . files = this . loadFiles ( ) . map ( file => ( { ... file , isReadOnly : true } ) ) ;
}
this . renderFiles ( ) ;
this . updateEmptyState ( ) ;
}
2025-08-19 12:51:49 +09:00
setupOnlineStatusListener ( ) {
window . addEventListener ( 'online' , ( ) => {
this . isOnline = true ;
this . showNotification ( '온라인 상태가 되었습니다.' , 'success' ) ;
} ) ;
window . addEventListener ( 'offline' , ( ) => {
this . isOnline = false ;
this . showNotification ( '오프라인 상태입니다.' , 'info' ) ;
} ) ;
}
// Supabase 데이터베이스 연동 메서드들
async loadUserFiles ( ) {
if ( ! this . currentUser || ! isSupabaseConfigured ( ) ) {
this . files = this . loadFiles ( ) ; // localStorage 폴백
this . updateSyncStatus ( 'offline' ) ;
return ;
}
try {
this . updateSyncStatus ( 'syncing' ) ;
const data = await SupabaseHelper . getFiles ( this . currentUser . id ) ;
this . files = data . map ( file => ( {
... file ,
files : file . file _attachments || [ ] // 첨부파일 정보 매핑
} ) ) ;
this . renderFiles ( ) ;
this . updateEmptyState ( ) ;
this . updateSyncStatus ( 'online' ) ;
} catch ( error ) {
console . error ( '파일 로딩 오류:' , error ) ;
this . showNotification ( '파일을 불러오는 중 오류가 발생했습니다.' , 'error' ) ;
this . updateSyncStatus ( 'error' ) ;
// 오류 시 localStorage 폴백
this . files = this . loadFiles ( ) ;
}
}
async addFileToSupabase ( fileData ) {
if ( ! this . currentUser || ! isSupabaseConfigured ( ) ) {
return this . addFileLocally ( fileData ) ;
}
try {
this . updateSyncStatus ( 'syncing' ) ;
2025-08-19 14:08:28 +09:00
console . log ( '파일 데이터 추가 중...' , fileData ) ;
2025-08-19 12:51:49 +09:00
const result = await SupabaseHelper . addFile ( fileData , this . currentUser . id ) ;
2025-08-19 14:08:28 +09:00
console . log ( '파일 데이터 추가 성공:' , result ) ;
2025-08-19 12:51:49 +09:00
// 첨부파일이 있는 경우 파일 업로드 처리
if ( fileData . files && fileData . files . length > 0 ) {
2025-08-19 14:08:28 +09:00
console . log ( ` ${ fileData . files . length } 개의 첨부파일 업로드 시작... ` ) ;
2025-08-19 12:51:49 +09:00
await this . uploadAttachments ( result . id , fileData . files ) ;
2025-08-19 14:08:28 +09:00
console . log ( '모든 첨부파일 업로드 완료' ) ;
2025-08-19 12:51:49 +09:00
}
this . showNotification ( '새 자료가 성공적으로 추가되었습니다!' , 'success' ) ;
await this . loadUserFiles ( ) ; // 목록 새로고침
this . updateSyncStatus ( 'online' ) ;
2025-08-19 14:08:28 +09:00
this . clearForm ( ) ; // 폼 초기화
2025-08-19 12:51:49 +09:00
} catch ( error ) {
console . error ( '파일 추가 오류:' , error ) ;
2025-08-19 14:08:28 +09:00
// 더 구체적인 에러 메시지 제공
let errorMessage = '파일 추가 중 오류가 발생했습니다.' ;
if ( error . message ) {
errorMessage += ` ( ${ error . message } ) ` ;
}
this . showNotification ( errorMessage , 'error' ) ;
2025-08-19 12:51:49 +09:00
this . updateSyncStatus ( 'error' ) ;
2025-08-19 14:08:28 +09:00
// 콘솔에 상세 오류 정보 출력
if ( error . details ) {
console . error ( '오류 상세:' , error . details ) ;
}
if ( error . hint ) {
console . error ( '오류 힌트:' , error . hint ) ;
}
2025-08-19 12:51:49 +09:00
}
}
async updateFileInSupabase ( id , updates ) {
if ( ! this . currentUser || ! isSupabaseConfigured ( ) ) {
return this . updateFileLocally ( id , updates ) ;
}
try {
await SupabaseHelper . updateFile ( id , updates , this . currentUser . id ) ;
this . showNotification ( '자료가 성공적으로 수정되었습니다!' , 'success' ) ;
await this . loadUserFiles ( ) ; // 목록 새로고침
} catch ( error ) {
console . error ( '파일 수정 오류:' , error ) ;
this . showNotification ( '파일 수정 중 오류가 발생했습니다.' , 'error' ) ;
}
}
async deleteFileFromSupabase ( id ) {
if ( ! this . currentUser || ! isSupabaseConfigured ( ) ) {
return this . deleteFileLocally ( id ) ;
}
try {
// 첨부파일들을 Storage에서 삭제
await this . deleteAttachmentsFromStorage ( id ) ;
// 데이터베이스에서 파일 삭제 (CASCADE로 첨부파일 정보도 함께 삭제)
await SupabaseHelper . deleteFile ( id , this . currentUser . id ) ;
this . showNotification ( '자료가 성공적으로 삭제되었습니다!' , 'success' ) ;
await this . loadUserFiles ( ) ; // 목록 새로고침
} catch ( error ) {
console . error ( '파일 삭제 오류:' , error ) ;
this . showNotification ( '파일 삭제 중 오류가 발생했습니다.' , 'error' ) ;
}
}
// localStorage 폴백 메서드들
addFileLocally ( fileData ) {
2025-08-19 14:14:30 +09:00
// 로컬 저장용 데이터 생성 (ID와 타임스탬프 추가)
2025-08-19 14:11:28 +09:00
const localFileData = {
2025-08-19 14:14:30 +09:00
id : this . generateId ( ) ,
2025-08-19 14:11:28 +09:00
... fileData ,
2025-08-19 14:14:30 +09:00
created _at : new Date ( ) . toISOString ( ) ,
updated _at : new Date ( ) . toISOString ( )
2025-08-19 14:11:28 +09:00
} ;
this . files . push ( localFileData ) ;
2025-08-19 12:51:49 +09:00
this . saveFiles ( ) ;
this . renderFiles ( ) ;
this . updateEmptyState ( ) ;
this . showNotification ( '새 자료가 성공적으로 추가되었습니다! (로컬 저장)' , 'success' ) ;
2025-08-19 14:14:30 +09:00
this . clearForm ( ) ; // 폼 초기화
2025-08-19 12:51:49 +09:00
}
updateFileLocally ( id , updates ) {
const fileIndex = this . files . findIndex ( f => f . id === id ) ;
if ( fileIndex !== - 1 ) {
this . files [ fileIndex ] = {
... this . files [ fileIndex ] ,
... updates ,
updated _at : new Date ( ) . toISOString ( )
} ;
this . saveFiles ( ) ;
this . renderFiles ( ) ;
this . showNotification ( '자료가 성공적으로 수정되었습니다! (로컬 저장)' , 'success' ) ;
}
}
deleteFileLocally ( id ) {
this . files = this . files . filter ( f => f . id !== id ) ;
this . saveFiles ( ) ;
this . renderFiles ( ) ;
this . updateEmptyState ( ) ;
this . showNotification ( '자료가 성공적으로 삭제되었습니다! (로컬 저장)' , 'success' ) ;
}
// 실시간 구독 관련
setupRealtimeSubscription ( ) {
if ( ! this . currentUser || ! isSupabaseConfigured ( ) ) return ;
this . realtimeSubscription = SupabaseHelper . subscribeToFiles (
this . currentUser . id ,
( payload ) => {
console . log ( '실시간 업데이트:' , payload ) ;
this . loadUserFiles ( ) ; // 변경사항이 있으면 목록 새로고침
}
) ;
}
cleanupRealtimeSubscription ( ) {
if ( this . realtimeSubscription ) {
this . realtimeSubscription . unsubscribe ( ) ;
this . realtimeSubscription = null ;
}
}
// 파일 업로드 관련 메서드들
async uploadAttachments ( fileId , attachments ) {
if ( ! isSupabaseConfigured ( ) || ! this . currentUser ) {
2025-08-19 14:08:28 +09:00
console . log ( '오프라인 모드: 첨부파일을 base64로 저장합니다.' ) ;
2025-08-19 12:51:49 +09:00
return ; // 오프라인 모드에서는 base64로 저장된 상태 유지
}
2025-08-19 14:08:28 +09:00
const uploadedFiles = [ ] ;
2025-08-19 12:51:49 +09:00
try {
2025-08-19 14:08:28 +09:00
for ( let i = 0 ; i < attachments . length ; i ++ ) {
const attachment = attachments [ i ] ;
2025-08-19 12:51:49 +09:00
2025-08-19 14:08:28 +09:00
try {
console . log ( ` 파일 업로드 중... ( ${ i + 1 } / ${ attachments . length } ): ${ attachment . name } ` ) ;
// base64 데이터를 Blob으로 변환
const response = await fetch ( attachment . data ) ;
const blob = await response . blob ( ) ;
2025-08-19 14:16:52 +09:00
// 안전한 파일명 생성 (고유한 이름으로 저장, 원본명은 DB에 저장)
const fileExtension = this . getFileExtension ( attachment . name ) ;
const safeFileName = ` ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ${ fileExtension } ` ;
const filePath = ` ${ this . currentUser . id } / ${ fileId } / ${ safeFileName } ` ;
2025-08-19 14:08:28 +09:00
2025-08-19 14:20:16 +09:00
// Storage 버킷 확인
const bucketExists = await SupabaseHelper . checkOrCreateBucket ( ) ;
if ( ! bucketExists ) {
2025-08-19 14:25:12 +09:00
throw new Error ( 'Storage 버킷 접근 권한이 없습니다. Storage 정책을 확인해주세요.' ) ;
2025-08-19 14:20:16 +09:00
}
2025-08-19 14:08:28 +09:00
// Supabase Storage에 업로드
const uploadResult = await SupabaseHelper . uploadFile ( blob , filePath ) ;
console . log ( 'Storage 업로드 성공:' , uploadResult ) ;
// 데이터베이스에 첨부파일 정보 저장
const attachmentResult = await this . addFileAttachment ( fileId , {
2025-08-19 12:51:49 +09:00
original _name : attachment . name ,
storage _path : filePath ,
file _size : attachment . size || blob . size ,
mime _type : attachment . type || blob . type
} ) ;
2025-08-19 14:08:28 +09:00
uploadedFiles . push ( attachmentResult ) ;
console . log ( '첨부파일 정보 저장 성공:' , attachmentResult ) ;
} catch ( fileError ) {
console . error ( ` 파일 " ${ attachment . name } " 업로드 실패: ` , fileError ) ;
throw new Error ( ` 파일 " ${ attachment . name } " 업로드에 실패했습니다: ${ fileError . message } ` ) ;
2025-08-19 12:51:49 +09:00
}
}
2025-08-19 14:08:28 +09:00
console . log ( '모든 첨부파일 업로드 완료:' , uploadedFiles ) ;
return uploadedFiles ;
2025-08-19 12:51:49 +09:00
} catch ( error ) {
console . error ( '파일 업로드 오류:' , error ) ;
2025-08-19 14:08:28 +09:00
// 부분적으로 업로드된 파일들 정리 (선택사항)
try {
for ( const uploadedFile of uploadedFiles ) {
if ( uploadedFile . storage _path ) {
await SupabaseHelper . deleteStorageFile ( uploadedFile . storage _path ) ;
}
}
} catch ( cleanupError ) {
console . error ( '업로드 실패 파일 정리 중 오류:' , cleanupError ) ;
}
throw error ;
}
}
async addFileAttachment ( fileId , attachmentData ) {
if ( ! isSupabaseConfigured ( ) ) {
return ; // 오프라인 모드에서는 처리하지 않음
}
try {
// SupabaseHelper를 통해 첨부파일 정보 저장
const result = await SupabaseHelper . addFileAttachment ( fileId , attachmentData ) ;
return result ;
} catch ( error ) {
console . error ( '첨부파일 정보 저장 오류:' , error ) ;
2025-08-19 12:51:49 +09:00
throw error ;
}
}
async downloadFileFromStorage ( filePath , originalName ) {
if ( ! isSupabaseConfigured ( ) ) {
return ; // 오프라인 모드에서는 처리하지 않음
}
try {
2025-08-19 14:20:16 +09:00
console . log ( '파일 다운로드 시도:' , filePath , originalName ) ;
// Storage 버킷 확인
const bucketExists = await SupabaseHelper . checkOrCreateBucket ( ) ;
if ( ! bucketExists ) {
2025-08-19 14:25:12 +09:00
throw new Error ( 'Storage 버킷 접근 권한이 없습니다. Storage 정책을 확인해주세요.' ) ;
2025-08-19 14:20:16 +09:00
}
2025-08-19 12:51:49 +09:00
const url = await SupabaseHelper . getFileUrl ( filePath ) ;
2025-08-19 14:20:16 +09:00
console . log ( '다운로드 URL 생성:' , url ) ;
2025-08-19 12:51:49 +09:00
// 다운로드 링크 생성
const link = document . createElement ( 'a' ) ;
link . href = url ;
link . download = originalName ;
2025-08-19 14:45:29 +09:00
// Ctrl/Cmd 키를 누른 상태에서 클릭하면 "다른 이름으로 저장" 대화상자 표시
if ( window . event && ( window . event . ctrlKey || window . event . metaKey ) ) {
link . target = '_blank' ;
// 브라우저의 다운로드 관리자로 보내기
}
2025-08-19 12:51:49 +09:00
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
2025-08-19 14:20:16 +09:00
console . log ( '파일 다운로드 완료:' , originalName ) ;
2025-08-19 12:51:49 +09:00
} catch ( error ) {
console . error ( '파일 다운로드 오류:' , error ) ;
2025-08-19 14:20:16 +09:00
// 더 구체적인 오류 메시지 제공
let errorMessage = '파일 다운로드 중 오류가 발생했습니다.' ;
if ( error . message . includes ( 'Bucket not found' ) ) {
errorMessage = 'Storage 버킷이 설정되지 않았습니다. 관리자에게 문의하세요.' ;
} else if ( error . message . includes ( '파일을 찾을 수 없습니다' ) ) {
errorMessage = '파일을 찾을 수 없습니다. 파일이 삭제되었을 수 있습니다.' ;
}
this . showNotification ( errorMessage , 'error' ) ;
2025-08-19 12:51:49 +09:00
}
}
async deleteAttachmentsFromStorage ( fileId ) {
if ( ! isSupabaseConfigured ( ) || ! this . currentUser ) {
return ; // 오프라인 모드에서는 처리하지 않음
}
try {
// 파일의 모든 첨부파일 경로 가져오기
const { data : attachments , error } = await supabase
. from ( 'file_attachments' )
. select ( 'storage_path' )
. eq ( 'file_id' , fileId ) ;
if ( error ) throw error ;
// 각 파일을 Storage에서 삭제
for ( const attachment of attachments ) {
await SupabaseHelper . deleteStorageFile ( attachment . storage _path ) ;
}
} catch ( error ) {
console . error ( '첨부파일 삭제 오류:' , error ) ;
}
}
handleSubmit ( e ) {
e . preventDefault ( ) ;
2025-08-19 14:02:13 +09:00
if ( ! this . currentUser ) {
this . showNotification ( '로그인이 필요합니다.' , 'error' ) ;
return ;
}
2025-08-19 12:51:49 +09:00
const title = document . getElementById ( 'fileTitle' ) . value . trim ( ) ;
const description = document . getElementById ( 'fileDescription' ) . value . trim ( ) ;
const category = document . getElementById ( 'fileCategory' ) . value ;
const tags = document . getElementById ( 'fileTags' ) . value . split ( ',' ) . map ( tag => tag . trim ( ) ) . filter ( tag => tag ) ;
const fileInput = document . getElementById ( 'fileUpload' ) ;
if ( ! title || ! category ) {
alert ( '제목과 카테고리는 필수 입력 항목입니다.' ) ;
return ;
}
const fileData = {
title ,
description ,
category ,
tags ,
2025-08-19 14:14:30 +09:00
files : [ ] // 첨부파일 임시 저장용 (Supabase 전송시 제외됨)
2025-08-19 12:51:49 +09:00
} ;
if ( fileInput . files . length > 0 ) {
Array . from ( fileInput . files ) . forEach ( file => {
const reader = new FileReader ( ) ;
reader . onload = ( e ) => {
fileData . files . push ( {
name : file . name ,
size : file . size ,
type : file . type ,
data : e . target . result
} ) ;
if ( fileData . files . length === fileInput . files . length ) {
this . addFileToSupabase ( fileData ) ;
}
} ;
reader . readAsDataURL ( file ) ;
} ) ;
} else {
this . addFileToSupabase ( fileData ) ;
}
}
async addFile ( fileData ) {
// 호환성을 위해 유지, 실제로는 addFileToSupabase 사용
await this . addFileToSupabase ( fileData ) ;
this . clearForm ( ) ;
}
handleFileUpload ( e ) {
const files = Array . from ( e . target . files ) ;
const filesList = document . querySelector ( '.files-list' ) || this . createFilesList ( ) ;
filesList . innerHTML = '' ;
files . forEach ( ( file , index ) => {
const fileItem = document . createElement ( 'div' ) ;
fileItem . className = 'file-attachment' ;
fileItem . innerHTML = `
< span > 📎 $ { file . name } ( $ { this . formatFileSize ( file . size ) } ) < / s p a n >
< button type = "button" class = "remove-file" onclick = "fileManager.removeFileFromInput(${index})" > 제거 < / b u t t o n >
` ;
filesList . appendChild ( fileItem ) ;
} ) ;
}
createFilesList ( ) {
const fileGroup = document . querySelector ( '#fileUpload' ) . closest ( '.form-group' ) ;
const filesList = document . createElement ( 'div' ) ;
filesList . className = 'files-list' ;
fileGroup . appendChild ( filesList ) ;
return filesList ;
}
removeFileFromInput ( index ) {
const fileInput = document . getElementById ( 'fileUpload' ) ;
const dt = new DataTransfer ( ) ;
const files = Array . from ( fileInput . files ) ;
files . forEach ( ( file , i ) => {
if ( i !== index ) {
dt . items . add ( file ) ;
}
} ) ;
fileInput . files = dt . files ;
this . handleFileUpload ( { target : fileInput } ) ;
}
renderFiles ( ) {
const container = document . getElementById ( 'fileList' ) ;
const sortBy = document . getElementById ( 'sortBy' ) . value ;
let sortedFiles = [ ... this . files ] ;
switch ( sortBy ) {
case 'title' :
sortedFiles . sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) ;
break ;
case 'category' :
sortedFiles . sort ( ( a , b ) => a . category . localeCompare ( b . category ) ) ;
break ;
case 'date' :
default :
2025-08-19 14:11:28 +09:00
sortedFiles . sort ( ( a , b ) => new Date ( b . created _at ) - new Date ( a . created _at ) ) ;
2025-08-19 12:51:49 +09:00
break ;
}
if ( sortedFiles . length === 0 ) {
container . innerHTML = '<div class="empty-state"><p>📂 등록된 자료가 없습니다. 새 자료를 추가해보세요!</p></div>' ;
return ;
}
container . innerHTML = sortedFiles . map ( file => this . createFileHTML ( file ) ) . join ( '' ) ;
}
createFileHTML ( file ) {
2025-08-19 14:11:28 +09:00
const createdDate = new Date ( file . created _at ) . toLocaleDateString ( 'ko-KR' ) ;
const updatedDate = new Date ( file . updated _at ) . toLocaleDateString ( 'ko-KR' ) ;
2025-08-19 12:51:49 +09:00
const tagsHTML = file . tags . map ( tag => ` <span class="tag"> ${ tag } </span> ` ) . join ( '' ) ;
const filesHTML = file . files . length > 0 ?
` <div class="file-attachments">
< strong > 첨부파일 ( $ { file . files . length } 개 ) : < / s t r o n g >
$ { file . files . map ( f => ` <span class="file-name">📎 ${ f . name } </span> ` ) . join ( ', ' ) }
< / d i v > ` : ' ' ;
return `
< div class = "file-item" data - id = "${file.id}" >
< div class = "file-header" >
< div >
< div class = "file-title" > $ { this . escapeHtml ( file . title ) } < / d i v >
< div class = "file-meta" >
< span class = "category-badge" > $ { file . category } < / s p a n >
< span > 📅 생성 : $ { createdDate } < / s p a n >
$ { createdDate !== updatedDate ? ` <span>✏️ 수정: ${ updatedDate } </span> ` : '' }
< / d i v >
< / d i v >
< / d i v >
$ { file . description ? ` <div class="file-description"> ${ this . escapeHtml ( file . description ) } </div> ` : '' }
$ { file . tags . length > 0 ? ` <div class="file-tags"> ${ tagsHTML } </div> ` : '' }
$ { filesHTML }
< div class = "file-actions" >
2025-08-19 14:02:13 +09:00
$ { ! file . isReadOnly && this . currentUser ? `
< button class = "edit-btn" onclick = "fileManager.editFile('${file.id}')" > ✏ ️ 수정 < / b u t t o n >
< button class = "delete-btn" onclick = "fileManager.deleteFile('${file.id}')" > 🗑 ️ 삭제 < / b u t t o n >
` : ''}
2025-08-19 14:45:29 +09:00
$ { file . files . length > 0 ? ` <button class="download-btn" onclick="fileManager.downloadFiles(' ${ file . id } ')" title="클릭: 기본 다운로드 폴더로 저장 Ctrl+클릭: 저장 위치 선택">💾 다운로드</button> ` : '' }
2025-08-19 14:02:13 +09:00
$ { file . isReadOnly ? ` <span class="read-only-badge">👁️ 읽기 전용</span> ` : '' }
2025-08-19 12:51:49 +09:00
< / d i v >
< / d i v >
` ;
}
escapeHtml ( text ) {
const div = document . createElement ( 'div' ) ;
div . textContent = text ;
return div . innerHTML ;
}
editFile ( id ) {
2025-08-19 14:02:13 +09:00
if ( ! this . currentUser ) {
this . showNotification ( '로그인이 필요합니다.' , 'error' ) ;
return ;
}
2025-08-19 12:51:49 +09:00
const file = this . files . find ( f => f . id === id ) ;
if ( ! file ) return ;
2025-08-19 14:02:13 +09:00
if ( file . isReadOnly ) {
this . showNotification ( '읽기 전용 파일은 편집할 수 없습니다.' , 'error' ) ;
return ;
}
2025-08-19 12:51:49 +09:00
this . currentEditId = id ;
document . getElementById ( 'editTitle' ) . value = file . title ;
document . getElementById ( 'editDescription' ) . value = file . description ;
document . getElementById ( 'editCategory' ) . value = file . category ;
document . getElementById ( 'editTags' ) . value = file . tags . join ( ', ' ) ;
document . getElementById ( 'editModal' ) . style . display = 'block' ;
}
handleEditSubmit ( e ) {
e . preventDefault ( ) ;
if ( ! this . currentEditId ) return ;
const title = document . getElementById ( 'editTitle' ) . value . trim ( ) ;
const description = document . getElementById ( 'editDescription' ) . value . trim ( ) ;
const category = document . getElementById ( 'editCategory' ) . value ;
const tags = document . getElementById ( 'editTags' ) . value . split ( ',' ) . map ( tag => tag . trim ( ) ) . filter ( tag => tag ) ;
if ( ! title || ! category ) {
alert ( '제목과 카테고리는 필수 입력 항목입니다.' ) ;
return ;
}
const updates = {
title ,
description ,
category ,
tags
} ;
this . updateFileInSupabase ( this . currentEditId , updates ) ;
this . closeEditModal ( ) ;
}
closeEditModal ( ) {
document . getElementById ( 'editModal' ) . style . display = 'none' ;
this . currentEditId = null ;
}
deleteFile ( id ) {
2025-08-19 14:02:13 +09:00
if ( ! this . currentUser ) {
this . showNotification ( '로그인이 필요합니다.' , 'error' ) ;
return ;
}
2025-08-19 12:51:49 +09:00
const file = this . files . find ( f => f . id === id ) ;
if ( ! file ) return ;
2025-08-19 14:02:13 +09:00
if ( file . isReadOnly ) {
this . showNotification ( '읽기 전용 파일은 삭제할 수 없습니다.' , 'error' ) ;
return ;
}
2025-08-19 12:51:49 +09:00
if ( confirm ( ` " ${ file . title } " 자료를 삭제하시겠습니까? \n 이 작업은 되돌릴 수 없습니다. ` ) ) {
this . deleteFileFromSupabase ( id ) ;
}
}
async downloadFiles ( id ) {
const file = this . files . find ( f => f . id === id ) ;
if ( ! file || file . files . length === 0 ) return ;
try {
for ( const fileData of file . files ) {
if ( fileData . storage _path && isSupabaseConfigured ( ) ) {
// Supabase Storage에서 다운로드
await this . downloadFileFromStorage ( fileData . storage _path , fileData . original _name || fileData . name ) ;
} else if ( fileData . data ) {
// localStorage 데이터 다운로드 (base64)
const link = document . createElement ( 'a' ) ;
link . href = fileData . data ;
link . download = fileData . name ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
}
}
2025-08-19 14:39:08 +09:00
const fileNames = file . files . map ( f => f . original _name || f . name ) . join ( ', ' ) ;
2025-08-19 14:45:29 +09:00
const downloadFolder = this . getDownloadFolderPath ( ) ;
// 첫 번째 다운로드인지 확인
const isFirstDownload = ! localStorage . getItem ( 'hasDownloadedBefore' ) ;
if ( isFirstDownload ) {
localStorage . setItem ( 'hasDownloadedBefore' , 'true' ) ;
this . showNotification ( ` 파일 다운로드 완료: ${ fileNames } \n 저장 위치: ${ downloadFolder } \n \n 💡 팁: 브라우저 설정에서 다운로드 폴더를 변경할 수 있습니다. ` , 'success' ) ;
} else {
this . showNotification ( ` 파일 다운로드 완료: ${ fileNames } ` , 'success' ) ;
}
2025-08-19 12:51:49 +09:00
} catch ( error ) {
console . error ( '파일 다운로드 오류:' , error ) ;
this . showNotification ( '파일 다운로드 중 오류가 발생했습니다.' , 'error' ) ;
}
}
handleSearch ( ) {
const searchTerm = document . getElementById ( 'searchInput' ) . value . toLowerCase ( ) . trim ( ) ;
const categoryFilter = document . getElementById ( 'categoryFilter' ) . value ;
let filteredFiles = this . files ;
if ( searchTerm ) {
filteredFiles = filteredFiles . filter ( file =>
file . title . toLowerCase ( ) . includes ( searchTerm ) ||
file . description . toLowerCase ( ) . includes ( searchTerm ) ||
file . tags . some ( tag => tag . toLowerCase ( ) . includes ( searchTerm ) )
) ;
}
if ( categoryFilter ) {
filteredFiles = filteredFiles . filter ( file => file . category === categoryFilter ) ;
}
this . renderFilteredFiles ( filteredFiles ) ;
}
renderFilteredFiles ( files ) {
const container = document . getElementById ( 'fileList' ) ;
const sortBy = document . getElementById ( 'sortBy' ) . value ;
let sortedFiles = [ ... files ] ;
switch ( sortBy ) {
case 'title' :
sortedFiles . sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) ;
break ;
case 'category' :
sortedFiles . sort ( ( a , b ) => a . category . localeCompare ( b . category ) ) ;
break ;
case 'date' :
default :
2025-08-19 14:11:28 +09:00
sortedFiles . sort ( ( a , b ) => new Date ( b . created _at ) - new Date ( a . created _at ) ) ;
2025-08-19 12:51:49 +09:00
break ;
}
if ( sortedFiles . length === 0 ) {
container . innerHTML = '<div class="empty-state"><p>🔍 검색 결과가 없습니다. 다른 키워드로 검색해보세요!</p></div>' ;
return ;
}
container . innerHTML = sortedFiles . map ( file => this . createFileHTML ( file ) ) . join ( '' ) ;
}
clearForm ( ) {
document . getElementById ( 'fileForm' ) . reset ( ) ;
const filesList = document . querySelector ( '.files-list' ) ;
if ( filesList ) {
filesList . innerHTML = '' ;
}
}
formatFileSize ( bytes ) {
if ( bytes === 0 ) return '0 Bytes' ;
const k = 1024 ;
const sizes = [ 'Bytes' , 'KB' , 'MB' , 'GB' ] ;
const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ] ;
}
updateEmptyState ( ) {
const container = document . getElementById ( 'fileList' ) ;
if ( this . files . length === 0 ) {
container . innerHTML = '<div class="empty-state"><p>📂 등록된 자료가 없습니다. 새 자료를 추가해보세요!</p></div>' ;
}
}
showNotification ( message , type = 'info' ) {
2025-08-19 14:39:08 +09:00
// 기존 알림이 있으면 제거
const existingNotification = document . querySelector ( '.notification' ) ;
if ( existingNotification ) {
existingNotification . remove ( ) ;
}
2025-08-19 12:51:49 +09:00
const notification = document . createElement ( 'div' ) ;
2025-08-19 14:39:08 +09:00
notification . className = ` notification ${ type } ` ;
2025-08-19 12:51:49 +09:00
notification . textContent = message ;
document . body . appendChild ( notification ) ;
2025-08-19 14:39:08 +09:00
// 3초 후 자동 제거
2025-08-19 12:51:49 +09:00
setTimeout ( ( ) => {
2025-08-19 14:39:08 +09:00
if ( notification . parentNode ) {
notification . remove ( ) ;
}
2025-08-19 12:51:49 +09:00
} , 3000 ) ;
}
loadFiles ( ) {
try {
const stored = localStorage . getItem ( 'fileManagerData' ) ;
2025-08-19 14:11:28 +09:00
const files = stored ? JSON . parse ( stored ) : [ ] ;
// 기존 localStorage 데이터의 호환성을 위해 컬럼명 변환
return files . map ( file => ( {
... file ,
created _at : file . created _at || file . createdAt || new Date ( ) . toISOString ( ) ,
updated _at : file . updated _at || file . updatedAt || new Date ( ) . toISOString ( )
} ) ) ;
2025-08-19 12:51:49 +09:00
} catch ( error ) {
console . error ( '파일 데이터를 불러오는 중 오류가 발생했습니다:' , error ) ;
return [ ] ;
}
}
saveFiles ( ) {
try {
localStorage . setItem ( 'fileManagerData' , JSON . stringify ( this . files ) ) ;
} catch ( error ) {
console . error ( '파일 데이터를 저장하는 중 오류가 발생했습니다:' , error ) ;
alert ( '데이터 저장 중 오류가 발생했습니다. 브라우저의 저장공간을 확인해주세요.' ) ;
}
}
exportData ( ) {
const dataStr = JSON . stringify ( this . files , null , 2 ) ;
const dataBlob = new Blob ( [ dataStr ] , { type : 'application/json' } ) ;
const url = URL . createObjectURL ( dataBlob ) ;
const link = document . createElement ( 'a' ) ;
link . href = url ;
link . download = ` 자료실_백업_ ${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .json ` ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
URL . revokeObjectURL ( url ) ;
this . showNotification ( '데이터가 성공적으로 내보내기되었습니다!' , 'success' ) ;
}
importData ( event ) {
const file = event . target . files [ 0 ] ;
if ( ! file ) return ;
const reader = new FileReader ( ) ;
reader . onload = ( e ) => {
try {
const importedData = JSON . parse ( e . target . result ) ;
if ( Array . isArray ( importedData ) ) {
if ( confirm ( '기존 데이터를 모두 삭제하고 새 데이터를 가져오시겠습니까?' ) ) {
this . files = importedData ;
this . saveFiles ( ) ;
this . renderFiles ( ) ;
this . updateEmptyState ( ) ;
this . showNotification ( '데이터가 성공적으로 가져와졌습니다!' , 'success' ) ;
}
} else {
alert ( '올바르지 않은 파일 형식입니다.' ) ;
}
} catch ( error ) {
alert ( '파일을 읽는 중 오류가 발생했습니다.' ) ;
console . error ( error ) ;
}
} ;
reader . readAsText ( file ) ;
}
}
const style = document . createElement ( 'style' ) ;
style . textContent = `
@ keyframes slideIn {
from { transform : translateX ( 100 % ) ; opacity : 0 ; }
to { transform : translateX ( 0 ) ; opacity : 1 ; }
}
@ keyframes slideOut {
from { transform : translateX ( 0 ) ; opacity : 1 ; }
to { transform : translateX ( 100 % ) ; opacity : 0 ; }
}
` ;
document . head . appendChild ( style ) ;
const fileManager = new FileManager ( ) ;
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
console . log ( '📚 자료실 관리 시스템이 초기화되었습니다.' ) ;
} ) ;