diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..359de5b
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,16 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(git push:*)",
+ "Bash(git remote remove:*)",
+ "Bash(git remote add:*)",
+ "Bash(git remote set-url:*)",
+ "WebFetch(domain:ngrok.com)",
+ "Bash(python:*)",
+ "WebFetch(domain:developers.cloudflare.com)",
+ "Bash(git add:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
\ No newline at end of file
diff --git a/script.js b/script.js
index 2aec689..c565c39 100644
--- a/script.js
+++ b/script.js
@@ -80,6 +80,9 @@ class FileManager {
this.setupRealtimeSubscription();
} else {
this.updateAuthUI(false);
+ // 게스트 모드: 공개 파일 로드
+ await this.loadPublicFiles();
+ this.showGuestMode();
}
setupAuthListener((event, session) => {
@@ -91,8 +94,8 @@ class FileManager {
} else if (event === 'SIGNED_OUT') {
this.currentUser = null;
this.updateAuthUI(false);
- this.files = [];
- this.renderFiles();
+ this.loadPublicFiles();
+ this.showGuestMode();
this.cleanupRealtimeSubscription();
}
});
@@ -105,15 +108,19 @@ class FileManager {
const authButtons = document.getElementById('authButtons');
const userInfo = document.getElementById('userInfo');
const userEmail = document.getElementById('userEmail');
+ const formSection = document.querySelector('.form-section');
if (isAuthenticated && this.currentUser) {
authButtons.style.display = 'none';
userInfo.style.display = 'flex';
userEmail.textContent = this.currentUser.email;
+ formSection.style.display = 'block';
this.updateSyncStatus();
+ this.hideGuestMode();
} else {
authButtons.style.display = 'flex';
userInfo.style.display = 'none';
+ formSection.style.display = 'none';
}
}
@@ -258,6 +265,53 @@ class FileManager {
container.insertBefore(offlineNotice, container.firstChild.nextSibling);
}
+ // 게스트 모드 관련
+ showGuestMode() {
+ this.hideGuestMode(); // 기존 알림 제거
+ const container = document.querySelector('.container');
+ const guestNotice = document.createElement('div');
+ guestNotice.className = 'guest-mode';
+ guestNotice.id = 'guestModeNotice';
+ guestNotice.innerHTML = `
+
+ 👤 게스트 모드 - 파일 보기 및 다운로드만 가능합니다
+
+
+ `;
+ 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();
+ }
+
setupOnlineStatusListener() {
window.addEventListener('online', () => {
this.isOnline = true;
@@ -490,6 +544,11 @@ class FileManager {
handleSubmit(e) {
e.preventDefault();
+ if (!this.currentUser) {
+ this.showNotification('로그인이 필요합니다.', 'error');
+ return;
+ }
+
const title = document.getElementById('fileTitle').value.trim();
const description = document.getElementById('fileDescription').value.trim();
const category = document.getElementById('fileCategory').value;
@@ -637,9 +696,12 @@ class FileManager {
${filesHTML}
-
-
+ ${!file.isReadOnly && this.currentUser ? `
+
+
+ ` : ''}
${file.files.length > 0 ? `` : ''}
+ ${file.isReadOnly ? `👁️ 읽기 전용` : ''}
`;
@@ -652,9 +714,19 @@ class FileManager {
}
editFile(id) {
+ if (!this.currentUser) {
+ this.showNotification('로그인이 필요합니다.', 'error');
+ return;
+ }
+
const file = this.files.find(f => f.id === id);
if (!file) return;
+ if (file.isReadOnly) {
+ this.showNotification('읽기 전용 파일은 편집할 수 없습니다.', 'error');
+ return;
+ }
+
this.currentEditId = id;
document.getElementById('editTitle').value = file.title;
@@ -697,9 +769,19 @@ class FileManager {
}
deleteFile(id) {
+ if (!this.currentUser) {
+ this.showNotification('로그인이 필요합니다.', 'error');
+ return;
+ }
+
const file = this.files.find(f => f.id === id);
if (!file) return;
+ if (file.isReadOnly) {
+ this.showNotification('읽기 전용 파일은 삭제할 수 없습니다.', 'error');
+ return;
+ }
+
if (confirm(`"${file.title}" 자료를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) {
this.deleteFileFromSupabase(id);
}
diff --git a/styles.css b/styles.css
index 66cf1c0..4208376 100644
--- a/styles.css
+++ b/styles.css
@@ -120,6 +120,50 @@ header p {
color: #856404;
}
+/* 게스트 모드 스타일 */
+.guest-mode {
+ background: linear-gradient(135deg, #a8e6cf, #88d8a3);
+ color: #2d3436;
+ padding: 15px 20px;
+ border-radius: 8px;
+ margin: 20px 0;
+ border: 2px solid #00b894;
+ box-shadow: 0 2px 10px rgba(0, 184, 148, 0.2);
+}
+
+.guest-mode-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.guest-login-btn {
+ background: #00b894;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 5px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.guest-login-btn:hover {
+ background: #00a085;
+ transform: translateY(-1px);
+}
+
+.read-only-badge {
+ background: #74b9ff;
+ color: white;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.8em;
+ font-weight: 500;
+}
+
.sync-status {
display: inline-flex;
align-items: center;
@@ -569,4 +613,13 @@ header p {
.file-actions button {
width: 100%;
}
+
+ .guest-mode-content {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .guest-login-btn {
+ margin-top: 10px;
+ }
}
\ No newline at end of file
diff --git a/supabase-config.js b/supabase-config.js
index fabf629..69bfc06 100644
--- a/supabase-config.js
+++ b/supabase-config.js
@@ -100,14 +100,19 @@ const SupabaseHelper = {
async getFiles(userId) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
- const { data, error } = await supabase
+ let query = supabase
.from('files')
.select(`
*,
file_attachments (*)
- `)
- .eq('user_id', userId)
- .order('created_at', { ascending: false });
+ `);
+
+ // 공개 파일 요청이 아닌 경우에만 사용자 ID로 필터링
+ if (userId !== 'public') {
+ query = query.eq('user_id', userId);
+ }
+
+ const { data, error } = await query.order('created_at', { ascending: false });
if (error) throw error;
return data;