Add complete Jaryo File Manager with Synology NAS deployment support

This commit is contained in:
2025-08-21 11:22:54 +09:00
parent 122d0e2582
commit a8a31b696a
39 changed files with 9026 additions and 1678 deletions

View File

@@ -0,0 +1,119 @@
// Supabase configuration (오프라인 모드)
// ⚠️ 오프라인 모드로 강제 설정됨
const SUPABASE_CONFIG = {
url: 'https://kncudtzthmjegowbgnto.supabase.co',
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtuY3VkdHp0aG1qZWdvd2JnbnRvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU1Njc5OTksImV4cCI6MjA3MTE0Mzk5OX0.NlJN2vdgM96RvyVJE6ILQeDVUOU9X2F9vUn-jr_xlKc'
};
// Supabase 클라이언트 초기화 (강제 비활성화)
let supabase = null;
// 설정이 유효한지 확인
function isSupabaseConfigured() {
return false; // 강제로 false 반환
}
// Supabase 클라이언트 초기화 함수 (오프라인 모드 강제)
function initializeSupabase() {
console.log('⚠️ 오프라인 모드로 강제 설정되었습니다.');
return false;
}
// 인증 상태 변경 리스너 (오프라인 모드용 - 빈 함수)
function setupAuthListener(callback) {
// 오프라인 모드에서는 아무것도 하지 않음
return;
}
// 현재 사용자 가져오기 (오프라인 모드용 - null 반환)
async function getCurrentUser() {
return null;
}
// 로그인 (오프라인 모드용 - 빈 함수)
async function signIn(email, password) {
throw new Error('오프라인 모드에서는 로그인할 수 없습니다.');
}
// 회원가입 (오프라인 모드용 - 빈 함수)
async function signUp(email, password, metadata = {}) {
throw new Error('오프라인 모드에서는 회원가입할 수 없습니다.');
}
// 로그아웃 (오프라인 모드용 - 빈 함수)
async function signOut() {
throw new Error('오프라인 모드에서는 로그아웃할 수 없습니다.');
}
// 데이터베이스 헬퍼 함수들 (오프라인 모드용)
const SupabaseHelper = {
// 파일 목록 가져오기 (오프라인 모드용)
async getFiles(userId) {
console.log('🔍 SupabaseHelper.getFiles 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
},
// 파일 추가 (오프라인 모드용)
async addFile(fileData, userId) {
console.log('🔍 SupabaseHelper.addFile 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
},
// 파일 수정 (오프라인 모드용)
async updateFile(id, updates, userId) {
console.log('🔍 SupabaseHelper.updateFile 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
},
// 파일 삭제 (오프라인 모드용)
async deleteFile(id, userId) {
console.log('🔍 SupabaseHelper.deleteFile 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
},
// 실시간 구독 설정 (오프라인 모드용)
subscribeToFiles(userId, callback) {
console.log('🔍 SupabaseHelper.subscribeToFiles 호출됨 (오프라인 모드)');
return null;
},
// 파일 업로드 (오프라인 모드용)
async uploadFile(file, filePath) {
console.log('🔍 SupabaseHelper.uploadFile 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase Storage를 사용할 수 없습니다.');
},
// 파일 다운로드 URL 가져오기 (오프라인 모드용)
async getFileUrl(filePath) {
console.log('🔍 SupabaseHelper.getFileUrl 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase Storage를 사용할 수 없습니다.');
},
// 파일 삭제 (Storage) (오프라인 모드용)
async deleteStorageFile(filePath) {
console.log('🔍 SupabaseHelper.deleteStorageFile 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase Storage를 사용할 수 없습니다.');
},
// 첨부파일 정보 추가 (오프라인 모드용)
async addFileAttachment(fileId, attachmentData) {
console.log('🔍 SupabaseHelper.addFileAttachment 호출됨 (오프라인 모드)');
throw new Error('오프라인 모드에서는 Supabase 데이터베이스를 사용할 수 없습니다.');
},
// Storage 버킷 확인 및 생성 (오프라인 모드용)
async checkOrCreateBucket() {
console.log('🔍 SupabaseHelper.checkOrCreateBucket 호출됨 (오프라인 모드)');
return false;
}
};
// 전역으로 내보내기
window.SupabaseHelper = SupabaseHelper;
window.initializeSupabase = initializeSupabase;
window.isSupabaseConfigured = isSupabaseConfigured;
window.setupAuthListener = setupAuthListener;
window.getCurrentUser = getCurrentUser;
window.signIn = signIn;
window.signUp = signUp;
window.signOut = signOut;

View File

@@ -0,0 +1,62 @@
-- 완전 초기화 후 Storage 정책 재설정
-- 1단계: 모든 기존 Storage 정책 삭제
DROP POLICY IF EXISTS "Users can upload to own folder" ON storage.objects;
DROP POLICY IF EXISTS "Users can view own files" ON storage.objects;
DROP POLICY IF EXISTS "Users can update own files" ON storage.objects;
DROP POLICY IF EXISTS "Users can delete own files" ON storage.objects;
DROP POLICY IF EXISTS "Public upload for testing" ON storage.objects;
DROP POLICY IF EXISTS "Public read for testing" ON storage.objects;
-- 혹시 다른 이름으로 생성된 정책들도 삭제
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON storage.objects;
DROP POLICY IF EXISTS "Enable select for authenticated users only" ON storage.objects;
DROP POLICY IF EXISTS "Enable update for authenticated users only" ON storage.objects;
DROP POLICY IF EXISTS "Enable delete for authenticated users only" ON storage.objects;
-- 2단계: RLS 활성화 확인 (보통 이미 활성화되어 있음)
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
-- 3단계: 새 정책 생성
-- 업로드 정책
CREATE POLICY "Users can upload to own folder"
ON storage.objects
FOR INSERT
WITH CHECK (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 조회 정책
CREATE POLICY "Users can view own files"
ON storage.objects
FOR SELECT
USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 업데이트 정책
CREATE POLICY "Users can update own files"
ON storage.objects
FOR UPDATE
USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 삭제 정책
CREATE POLICY "Users can delete own files"
ON storage.objects
FOR DELETE
USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 4단계: 정책 생성 확인
SELECT
'Storage policies created successfully!' as message,
COUNT(*) as policy_count
FROM pg_policies
WHERE schemaname = 'storage' AND tablename = 'objects';

View File

@@ -0,0 +1,150 @@
# Supabase 설정 가이드
이 문서는 자료실 시스템을 Supabase와 연동하기 위한 설정 가이드입니다.
## 1. Supabase 프로젝트 생성
1. [Supabase](https://supabase.com)에 접속하여 계정을 생성합니다.
2. 새 프로젝트를 생성합니다.
3. 프로젝트 이름과 비밀번호를 설정합니다.
4. 리전은 `ap-northeast-1` (Asia Pacific - Tokyo)를 선택하는 것을 권장합니다.
## 2. 데이터베이스 스키마 설정
1. Supabase 대시보드에서 **SQL Editor**로 이동합니다.
2. `supabase-schema.sql` 파일의 내용을 복사하여 실행합니다.
3. 스키마가 성공적으로 생성되었는지 **Table Editor**에서 확인합니다.
### 생성되는 테이블
- `files`: 파일 메타데이터 저장
- `file_attachments`: 첨부파일 정보 저장
## 3. Storage 버킷 설정
1. Supabase 대시보드에서 **Storage**로 이동합니다.
2. **New bucket** 버튼을 클릭합니다.
3. 버킷 이름을 `files`로 설정합니다.
4. **Public bucket** 체크박스는 해제합니다 (보안상 권장).
5. 버킷을 생성합니다.
### Storage 정책 설정
버킷 생성 후 **Policies** 탭에서 다음 정책들을 추가합니다:
#### SELECT 정책 (파일 조회)
```sql
CREATE POLICY "Users can view their own files" ON storage.objects
FOR SELECT USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
```
#### INSERT 정책 (파일 업로드)
```sql
CREATE POLICY "Users can upload their own files" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
```
#### DELETE 정책 (파일 삭제)
```sql
CREATE POLICY "Users can delete their own files" ON storage.objects
FOR DELETE USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
```
## 4. API 키 및 URL 설정
1. Supabase 대시보드에서 **Settings** > **API**로 이동합니다.
2. 다음 정보를 확인합니다:
- **Project URL**: `https://your-project-id.supabase.co`
- **Project API keys** > **anon public**: `eyJ...`
3. `supabase-config.js` 파일을 수정합니다:
```javascript
const SUPABASE_CONFIG = {
url: 'https://your-project-id.supabase.co', // 실제 Project URL로 교체
anonKey: 'eyJ...' // 실제 anon public key로 교체
};
```
## 5. 인증 설정 (선택사항)
### 이메일 인증 비활성화 (개발용)
개발 환경에서 빠른 테스트를 위해 이메일 인증을 비활성화할 수 있습니다:
1. **Authentication** > **Settings**로 이동
2. **Enable email confirmations** 체크박스 해제
3. **Save** 클릭
⚠️ **주의**: 프로덕션 환경에서는 이메일 인증을 활성화하는 것을 강력히 권장합니다.
### 이메일 템플릿 설정 (프로덕션용)
1. **Authentication** > **Email Templates**에서 이메일 템플릿을 커스터마이징할 수 있습니다.
2. 회사 브랜드에 맞게 이메일 디자인을 수정하세요.
## 6. 보안 설정
### Row Level Security (RLS)
스키마 실행 시 자동으로 설정되지만, 다음 사항을 확인하세요:
1. **Authentication** > **Policies**에서 정책이 올바르게 설정되었는지 확인
2. 각 테이블에 사용자별 접근 제한이 적용되어 있는지 확인
### 환경변수 보안
프로덕션 환경에서는 API 키를 환경변수로 관리하세요:
```javascript
const SUPABASE_CONFIG = {
url: process.env.SUPABASE_URL || 'YOUR_SUPABASE_PROJECT_URL',
anonKey: process.env.SUPABASE_ANON_KEY || 'YOUR_SUPABASE_ANON_KEY'
};
```
## 7. 테스트
설정 완료 후 다음 기능들을 테스트하세요:
1. **회원가입/로그인** - 새 계정 생성 및 로그인
2. **파일 추가** - 새 자료 추가 (첨부파일 포함)
3. **파일 수정** - 기존 자료 수정
4. **파일 삭제** - 자료 삭제 (첨부파일도 함께 삭제되는지 확인)
5. **파일 다운로드** - 첨부파일 다운로드
6. **실시간 동기화** - 다른 브라우저에서 같은 계정으로 로그인하여 실시간 동기화 확인
## 8. 문제 해결
### 연결 오류
- Supabase URL과 API 키가 올바른지 확인
- 브라우저 콘솔에서 오류 메시지 확인
- CORS 설정 확인 (대부분 자동으로 설정됨)
### 권한 오류
- RLS 정책이 올바르게 설정되었는지 확인
- 사용자가 올바르게 인증되었는지 확인
### 파일 업로드 오류
- Storage 버킷이 올바르게 생성되었는지 확인
- Storage 정책이 올바르게 설정되었는지 확인
- 파일 크기 제한 확인 (Supabase 기본값: 50MB)
## 9. 추가 개선사항
### 성능 최적화
- 대용량 파일 처리를 위한 chunk 업로드 구현
- 이미지 최적화 및 썸네일 생성
- CDN 연동 고려
### 기능 확장
- 파일 공유 기능
- 버전 관리
- 협업 기능
- 백업 및 복원 기능
---
설정 중 문제가 발생하면 [Supabase 공식 문서](https://supabase.com/docs)를 참고하거나 이슈를 등록해주세요.

View File

@@ -0,0 +1,39 @@
-- Storage 전용 정책 (Supabase Dashboard → Storage → Policies에서 실행)
-- 1. 파일 업로드 정책
CREATE POLICY "Users can upload to own folder"
ON storage.objects
FOR INSERT
WITH CHECK (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 2. 파일 조회 정책
CREATE POLICY "Users can view own files"
ON storage.objects
FOR SELECT
USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 3. 파일 업데이트 정책
CREATE POLICY "Users can update own files"
ON storage.objects
FOR UPDATE
USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 4. 파일 삭제 정책
CREATE POLICY "Users can delete own files"
ON storage.objects
FOR DELETE
USING (
bucket_id = 'files' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- 참고: storage.foldername(name)[1]은 'user_id/filename.txt'에서 'user_id' 부분을 추출합니다.

View File

@@ -0,0 +1,309 @@
// Supabase configuration
// ⚠️ 실제 사용 시에는 이 값들을 환경변수나 설정 파일로 관리하세요
const SUPABASE_CONFIG = {
// 실제 Supabase 프로젝트 URL로 교체하세요
url: 'https://kncudtzthmjegowbgnto.supabase.co',
// 실제 Supabase anon key로 교체하세요
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtuY3VkdHp0aG1qZWdvd2JnbnRvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU1Njc5OTksImV4cCI6MjA3MTE0Mzk5OX0.NlJN2vdgM96RvyVJE6ILQeDVUOU9X2F9vUn-jr_xlKc'
};
// Supabase 클라이언트 초기화
let supabase;
// 설정이 유효한지 확인
function isSupabaseConfigured() {
return SUPABASE_CONFIG.url !== 'YOUR_SUPABASE_PROJECT_URL' &&
SUPABASE_CONFIG.anonKey !== 'YOUR_SUPABASE_ANON_KEY';
}
// Supabase 클라이언트 초기화 함수
function initializeSupabase() {
if (!isSupabaseConfigured()) {
console.warn('⚠️ Supabase가 설정되지 않았습니다. localStorage를 사용합니다.');
return false;
}
try {
supabase = window.supabase.createClient(SUPABASE_CONFIG.url, SUPABASE_CONFIG.anonKey);
console.log('✅ Supabase 클라이언트가 초기화되었습니다.');
return true;
} catch (error) {
console.error('❌ Supabase 초기화 오류:', error);
return false;
}
}
// 인증 상태 변경 리스너
function setupAuthListener(callback) {
if (!supabase) return;
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth state changed:', event, session);
if (callback) callback(event, session);
});
}
// 현재 사용자 가져오기
async function getCurrentUser() {
if (!supabase) return null;
try {
const { data: { user }, error } = await supabase.auth.getUser();
if (error) throw error;
return user;
} catch (error) {
console.error('사용자 정보 가져오기 오류:', error);
return null;
}
}
// 로그인
async function signIn(email, password) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
});
if (error) throw error;
return data;
}
// 회원가입
async function signUp(email, password, metadata = {}) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: metadata
}
});
if (error) throw error;
return data;
}
// 로그아웃
async function signOut() {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
const { error } = await supabase.auth.signOut();
if (error) throw error;
}
// 데이터베이스 헬퍼 함수들
const SupabaseHelper = {
// 파일 목록 가져오기
async getFiles(userId) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
let query = supabase
.from('files')
.select(`
*,
file_attachments (*)
`);
// 공개 파일 요청이 아닌 경우에만 사용자 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;
},
// 파일 추가
async addFile(fileData, userId) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
// 데이터베이스 스키마에 맞는 필드만 추출
const dbFileData = {
title: fileData.title,
description: fileData.description || '',
category: fileData.category,
tags: fileData.tags || [],
user_id: userId
// created_at, updated_at은 데이터베이스에서 자동 생성
};
const { data, error } = await supabase
.from('files')
.insert([dbFileData])
.select()
.single();
if (error) throw error;
return data;
},
// 파일 수정
async updateFile(id, updates, userId) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
// 데이터베이스 스키마에 맞는 필드만 추출
const dbUpdates = {
title: updates.title,
description: updates.description,
category: updates.category,
tags: updates.tags || []
// updated_at은 트리거에 의해 자동 업데이트됨
};
const { data, error } = await supabase
.from('files')
.update(dbUpdates)
.eq('id', id)
.eq('user_id', userId)
.select()
.single();
if (error) throw error;
return data;
},
// 파일 삭제
async deleteFile(id, userId) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
const { error } = await supabase
.from('files')
.delete()
.eq('id', id)
.eq('user_id', userId);
if (error) throw error;
},
// 실시간 구독 설정
subscribeToFiles(userId, callback) {
if (!supabase) return null;
return supabase
.channel('files')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'files',
filter: `user_id=eq.${userId}`
}, callback)
.subscribe();
},
// 파일 업로드 (Storage)
async uploadFile(file, filePath) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
const { data, error } = await supabase.storage
.from('files')
.upload(filePath, file);
if (error) throw error;
return data;
},
// 파일 다운로드 URL 가져오기
async getFileUrl(filePath) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
try {
// 먼저 파일이 존재하는지 확인
const { data: fileExists, error: checkError } = await supabase.storage
.from('files')
.list(filePath.substring(0, filePath.lastIndexOf('/')), {
search: filePath.substring(filePath.lastIndexOf('/') + 1)
});
if (checkError) {
throw new Error(`Storage 버킷 오류: ${checkError.message}`);
}
if (!fileExists || fileExists.length === 0) {
throw new Error('파일을 찾을 수 없습니다.');
}
// 파일이 존재하면 URL 생성
const { data } = supabase.storage
.from('files')
.getPublicUrl(filePath);
return data.publicUrl;
} catch (error) {
console.error('파일 URL 생성 오류:', error);
throw error;
}
},
// 파일 삭제 (Storage)
async deleteStorageFile(filePath) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
const { error } = await supabase.storage
.from('files')
.remove([filePath]);
if (error) throw error;
},
// 첨부파일 정보 추가
async addFileAttachment(fileId, attachmentData) {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
const { data, error } = await supabase
.from('file_attachments')
.insert([{
file_id: fileId,
...attachmentData
}])
.select()
.single();
if (error) throw error;
return data;
},
// Storage 버킷 확인 및 생성
async checkOrCreateBucket() {
if (!supabase) throw new Error('Supabase가 초기화되지 않았습니다.');
try {
// 버킷 목록 확인
const { data: buckets, error: listError } = await supabase.storage.listBuckets();
if (listError) {
console.error('버킷 목록 조회 오류:', listError);
return false;
}
// 'files' 버킷이 있는지 확인
const filesBucket = buckets.find(bucket => bucket.name === 'files');
if (filesBucket) {
console.log('✅ files 버킷이 존재합니다.');
return true;
} else {
console.warn('⚠️ files 버킷이 존재하지 않습니다.');
console.log('Supabase Dashboard에서 files 버킷을 생성해주세요.');
return false;
}
} catch (error) {
console.error('버킷 확인 오류:', error);
return false;
}
}
};
// 전역으로 내보내기
window.SupabaseHelper = SupabaseHelper;
window.initializeSupabase = initializeSupabase;
window.isSupabaseConfigured = isSupabaseConfigured;
window.setupAuthListener = setupAuthListener;
window.getCurrentUser = getCurrentUser;
window.signIn = signIn;
window.signUp = signUp;
window.signOut = signOut;

View File

@@ -0,0 +1,128 @@
-- Supabase 데이터베이스 스키마
-- 이 파일을 Supabase SQL 에디터에서 실행하세요
-- 1. files 테이블 생성
CREATE TABLE IF NOT EXISTS public.files (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
tags TEXT[] DEFAULT '{}',
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);
-- 2. file_attachments 테이블 생성 (파일 첨부 정보)
CREATE TABLE IF NOT EXISTS public.file_attachments (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
file_id UUID REFERENCES public.files(id) ON DELETE CASCADE NOT NULL,
original_name TEXT NOT NULL,
storage_path TEXT NOT NULL,
file_size INTEGER NOT NULL,
mime_type TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);
-- 3. Row Level Security (RLS) 정책 활성화
ALTER TABLE public.files ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.file_attachments ENABLE ROW LEVEL SECURITY;
-- 4. files 테이블 RLS 정책
-- 사용자는 자신의 파일만 조회할 수 있음
CREATE POLICY "Users can view their own files" ON public.files
FOR SELECT USING (auth.uid() = user_id);
-- 사용자는 자신의 파일만 생성할 수 있음
CREATE POLICY "Users can create their own files" ON public.files
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- 사용자는 자신의 파일만 수정할 수 있음
CREATE POLICY "Users can update their own files" ON public.files
FOR UPDATE USING (auth.uid() = user_id);
-- 사용자는 자신의 파일만 삭제할 수 있음
CREATE POLICY "Users can delete their own files" ON public.files
FOR DELETE USING (auth.uid() = user_id);
-- 5. file_attachments 테이블 RLS 정책
-- 사용자는 자신의 파일 첨부만 조회할 수 있음
CREATE POLICY "Users can view their own file attachments" ON public.file_attachments
FOR SELECT USING (
auth.uid() = (
SELECT user_id FROM public.files WHERE id = file_attachments.file_id
)
);
-- 사용자는 자신의 파일에만 첨부를 생성할 수 있음
CREATE POLICY "Users can create attachments for their own files" ON public.file_attachments
FOR INSERT WITH CHECK (
auth.uid() = (
SELECT user_id FROM public.files WHERE id = file_attachments.file_id
)
);
-- 사용자는 자신의 파일 첨부만 삭제할 수 있음
CREATE POLICY "Users can delete their own file attachments" ON public.file_attachments
FOR DELETE USING (
auth.uid() = (
SELECT user_id FROM public.files WHERE id = file_attachments.file_id
)
);
-- 6. 인덱스 생성 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_files_user_id ON public.files(user_id);
CREATE INDEX IF NOT EXISTS idx_files_created_at ON public.files(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_files_category ON public.files(category);
CREATE INDEX IF NOT EXISTS idx_files_tags ON public.files USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_file_attachments_file_id ON public.file_attachments(file_id);
-- 7. 업데이트 트리거 함수 (updated_at 자동 갱신)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- 8. updated_at 자동 갱신 트리거
CREATE TRIGGER update_files_updated_at
BEFORE UPDATE ON public.files
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 9. Storage 버킷 생성 (실제로는 Supabase Dashboard에서 생성)
-- 버킷 이름: 'files'
-- 공개 액세스: false (인증된 사용자만 접근)
--
-- Storage 정책은 Supabase Dashboard에서 다음과 같이 설정:
-- SELECT: 사용자는 자신의 파일만 조회 가능
-- INSERT: 사용자는 자신의 폴더에만 업로드 가능
-- UPDATE: 사용자는 자신의 파일만 수정 가능
-- DELETE: 사용자는 자신의 파일만 삭제 가능
-- 10. 유용한 뷰 생성 (파일과 첨부 정보 조인)
-- 주의: 뷰는 자동으로 기본 테이블의 RLS 정책을 상속받으므로 별도 정책 설정 불필요
CREATE OR REPLACE VIEW public.files_with_attachments AS
SELECT
f.*,
COALESCE(
JSON_AGG(
JSON_BUILD_OBJECT(
'id', fa.id,
'original_name', fa.original_name,
'storage_path', fa.storage_path,
'file_size', fa.file_size,
'mime_type', fa.mime_type,
'created_at', fa.created_at
)
) FILTER (WHERE fa.id IS NOT NULL),
'[]'::json
) AS attachments
FROM public.files f
LEFT JOIN public.file_attachments fa ON f.id = fa.file_id
GROUP BY f.id, f.title, f.description, f.category, f.tags, f.user_id, f.created_at, f.updated_at;
-- 설정 완료 메시지
SELECT 'Supabase 스키마 설정이 완료되었습니다!' as message;

View File

@@ -0,0 +1,16 @@
-- 임시 공개 접근 정책 (테스트용만 사용!)
-- 보안상 권장하지 않음 - 운영환경에서는 사용하지 마세요
-- 모든 사용자가 files 버킷에 업로드 가능 (임시)
CREATE POLICY "Public upload for testing"
ON storage.objects
FOR INSERT
WITH CHECK (bucket_id = 'files');
-- 모든 사용자가 files 버킷 파일 조회 가능 (임시)
CREATE POLICY "Public read for testing"
ON storage.objects
FOR SELECT
USING (bucket_id = 'files');
-- 주의: 이 정책들은 테스트 후 반드시 삭제하고 위의 사용자별 정책으로 교체하세요!