Add guest mode functionality for read-only access
- Implement guest mode for unauthenticated users - Allow file viewing and downloading without login - Hide create/edit/delete functions for guests - Add guest mode banner with login prompt - Add read-only badges for guest accessible files - Include permission checks for all CRUD operations - Add responsive guest mode styling - Support both online (Supabase) and offline (localStorage) modes Features: • Guest users can view all files and download attachments • Authentication required for create, edit, delete operations • Seamless transition between guest and authenticated modes • User-friendly guest experience with clear login prompts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
16
.claude/settings.local.json
Normal file
16
.claude/settings.local.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
86
script.js
86
script.js
@@ -80,6 +80,9 @@ class FileManager {
|
|||||||
this.setupRealtimeSubscription();
|
this.setupRealtimeSubscription();
|
||||||
} else {
|
} else {
|
||||||
this.updateAuthUI(false);
|
this.updateAuthUI(false);
|
||||||
|
// 게스트 모드: 공개 파일 로드
|
||||||
|
await this.loadPublicFiles();
|
||||||
|
this.showGuestMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAuthListener((event, session) => {
|
setupAuthListener((event, session) => {
|
||||||
@@ -91,8 +94,8 @@ class FileManager {
|
|||||||
} else if (event === 'SIGNED_OUT') {
|
} else if (event === 'SIGNED_OUT') {
|
||||||
this.currentUser = null;
|
this.currentUser = null;
|
||||||
this.updateAuthUI(false);
|
this.updateAuthUI(false);
|
||||||
this.files = [];
|
this.loadPublicFiles();
|
||||||
this.renderFiles();
|
this.showGuestMode();
|
||||||
this.cleanupRealtimeSubscription();
|
this.cleanupRealtimeSubscription();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -105,15 +108,19 @@ class FileManager {
|
|||||||
const authButtons = document.getElementById('authButtons');
|
const authButtons = document.getElementById('authButtons');
|
||||||
const userInfo = document.getElementById('userInfo');
|
const userInfo = document.getElementById('userInfo');
|
||||||
const userEmail = document.getElementById('userEmail');
|
const userEmail = document.getElementById('userEmail');
|
||||||
|
const formSection = document.querySelector('.form-section');
|
||||||
|
|
||||||
if (isAuthenticated && this.currentUser) {
|
if (isAuthenticated && this.currentUser) {
|
||||||
authButtons.style.display = 'none';
|
authButtons.style.display = 'none';
|
||||||
userInfo.style.display = 'flex';
|
userInfo.style.display = 'flex';
|
||||||
userEmail.textContent = this.currentUser.email;
|
userEmail.textContent = this.currentUser.email;
|
||||||
|
formSection.style.display = 'block';
|
||||||
this.updateSyncStatus();
|
this.updateSyncStatus();
|
||||||
|
this.hideGuestMode();
|
||||||
} else {
|
} else {
|
||||||
authButtons.style.display = 'flex';
|
authButtons.style.display = 'flex';
|
||||||
userInfo.style.display = 'none';
|
userInfo.style.display = 'none';
|
||||||
|
formSection.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +265,53 @@ class FileManager {
|
|||||||
container.insertBefore(offlineNotice, container.firstChild.nextSibling);
|
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 = `
|
||||||
|
<div class="guest-mode-content">
|
||||||
|
<span>👤 게스트 모드 - 파일 보기 및 다운로드만 가능합니다</span>
|
||||||
|
<button onclick="fileManager.openAuthModal('login')" class="guest-login-btn">🔑 로그인하여 편집하기</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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() {
|
setupOnlineStatusListener() {
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
this.isOnline = true;
|
this.isOnline = true;
|
||||||
@@ -490,6 +544,11 @@ class FileManager {
|
|||||||
handleSubmit(e) {
|
handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!this.currentUser) {
|
||||||
|
this.showNotification('로그인이 필요합니다.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const title = document.getElementById('fileTitle').value.trim();
|
const title = document.getElementById('fileTitle').value.trim();
|
||||||
const description = document.getElementById('fileDescription').value.trim();
|
const description = document.getElementById('fileDescription').value.trim();
|
||||||
const category = document.getElementById('fileCategory').value;
|
const category = document.getElementById('fileCategory').value;
|
||||||
@@ -637,9 +696,12 @@ class FileManager {
|
|||||||
${filesHTML}
|
${filesHTML}
|
||||||
|
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
|
${!file.isReadOnly && this.currentUser ? `
|
||||||
<button class="edit-btn" onclick="fileManager.editFile('${file.id}')">✏️ 수정</button>
|
<button class="edit-btn" onclick="fileManager.editFile('${file.id}')">✏️ 수정</button>
|
||||||
<button class="delete-btn" onclick="fileManager.deleteFile('${file.id}')">🗑️ 삭제</button>
|
<button class="delete-btn" onclick="fileManager.deleteFile('${file.id}')">🗑️ 삭제</button>
|
||||||
|
` : ''}
|
||||||
${file.files.length > 0 ? `<button class="download-btn" onclick="fileManager.downloadFiles('${file.id}')">💾 다운로드</button>` : ''}
|
${file.files.length > 0 ? `<button class="download-btn" onclick="fileManager.downloadFiles('${file.id}')">💾 다운로드</button>` : ''}
|
||||||
|
${file.isReadOnly ? `<span class="read-only-badge">👁️ 읽기 전용</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -652,9 +714,19 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editFile(id) {
|
editFile(id) {
|
||||||
|
if (!this.currentUser) {
|
||||||
|
this.showNotification('로그인이 필요합니다.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const file = this.files.find(f => f.id === id);
|
const file = this.files.find(f => f.id === id);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
if (file.isReadOnly) {
|
||||||
|
this.showNotification('읽기 전용 파일은 편집할 수 없습니다.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.currentEditId = id;
|
this.currentEditId = id;
|
||||||
|
|
||||||
document.getElementById('editTitle').value = file.title;
|
document.getElementById('editTitle').value = file.title;
|
||||||
@@ -697,9 +769,19 @@ class FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteFile(id) {
|
deleteFile(id) {
|
||||||
|
if (!this.currentUser) {
|
||||||
|
this.showNotification('로그인이 필요합니다.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const file = this.files.find(f => f.id === id);
|
const file = this.files.find(f => f.id === id);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
if (file.isReadOnly) {
|
||||||
|
this.showNotification('읽기 전용 파일은 삭제할 수 없습니다.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (confirm(`"${file.title}" 자료를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) {
|
if (confirm(`"${file.title}" 자료를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) {
|
||||||
this.deleteFileFromSupabase(id);
|
this.deleteFileFromSupabase(id);
|
||||||
}
|
}
|
||||||
|
53
styles.css
53
styles.css
@@ -120,6 +120,50 @@ header p {
|
|||||||
color: #856404;
|
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 {
|
.sync-status {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -569,4 +613,13 @@ header p {
|
|||||||
.file-actions button {
|
.file-actions button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guest-mode-content {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-login-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -100,14 +100,19 @@ const SupabaseHelper = {
|
|||||||
async getFiles(userId) {
|
async getFiles(userId) {
|
||||||
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
|
||||||
|
|
||||||
const { data, error } = await supabase
|
let query = supabase
|
||||||
.from('files')
|
.from('files')
|
||||||
.select(`
|
.select(`
|
||||||
*,
|
*,
|
||||||
file_attachments (*)
|
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;
|
if (error) throw error;
|
||||||
return data;
|
return data;
|
||||||
|
Reference in New Issue
Block a user