Add complete Jaryo File Manager with Synology NAS deployment support
This commit is contained in:
119
backup/supabase/admin-supabase-config.js
Normal file
119
backup/supabase/admin-supabase-config.js
Normal 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;
|
62
backup/supabase/clean-storage-setup.sql
Normal file
62
backup/supabase/clean-storage-setup.sql
Normal 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';
|
150
backup/supabase/setup-guide.md
Normal file
150
backup/supabase/setup-guide.md
Normal 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)를 참고하거나 이슈를 등록해주세요.
|
39
backup/supabase/storage-policies.sql
Normal file
39
backup/supabase/storage-policies.sql
Normal 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' 부분을 추출합니다.
|
309
backup/supabase/supabase-config.js
Normal file
309
backup/supabase/supabase-config.js
Normal 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;
|
128
backup/supabase/supabase-schema.sql
Normal file
128
backup/supabase/supabase-schema.sql
Normal 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;
|
16
backup/supabase/temp-public-policy.sql
Normal file
16
backup/supabase/temp-public-policy.sql
Normal 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');
|
||||
|
||||
-- 주의: 이 정책들은 테스트 후 반드시 삭제하고 위의 사용자별 정책으로 교체하세요!
|
Reference in New Issue
Block a user