초기 커밋
This commit is contained in:
666
ZELLYY/zellyy note/02_기술_문서/API_명세서.md
Normal file
666
ZELLYY/zellyy note/02_기술_문서/API_명세서.md
Normal file
@@ -0,0 +1,666 @@
|
||||
# Zellyy API 명세서
|
||||
|
||||
이 문서는 Zellyy 프로젝트의 API 엔드포인트와 기능에 대한 상세 명세를 제공합니다.
|
||||
|
||||
## 기본 정보
|
||||
|
||||
- **기본 URL**: `https://a11.ism.kr/api`
|
||||
- **API 버전**: v1
|
||||
- **인증 방식**: JWT 토큰 (Bearer Authentication)
|
||||
- **응답 형식**: JSON
|
||||
|
||||
## 인증 API
|
||||
|
||||
### 회원가입
|
||||
|
||||
```
|
||||
POST /auth/signup
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword",
|
||||
"username": "username"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"email": "user@example.com",
|
||||
"username": "username"
|
||||
},
|
||||
"session": {
|
||||
"access_token": "jwt_token",
|
||||
"refresh_token": "refresh_token",
|
||||
"expires_at": 1672531200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 로그인
|
||||
|
||||
```
|
||||
POST /auth/login
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"email": "user@example.com",
|
||||
"username": "username"
|
||||
},
|
||||
"session": {
|
||||
"access_token": "jwt_token",
|
||||
"refresh_token": "refresh_token",
|
||||
"expires_at": 1672531200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 토큰 갱신
|
||||
|
||||
```
|
||||
POST /auth/refresh
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"refresh_token": "refresh_token"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"access_token": "new_jwt_token",
|
||||
"refresh_token": "new_refresh_token",
|
||||
"expires_at": 1672531200
|
||||
}
|
||||
```
|
||||
|
||||
### 로그아웃
|
||||
|
||||
```
|
||||
POST /auth/logout
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"message": "Successfully logged out"
|
||||
}
|
||||
```
|
||||
|
||||
## 사용자 API
|
||||
|
||||
### 사용자 정보 조회
|
||||
|
||||
```
|
||||
GET /users/me
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"email": "user@example.com",
|
||||
"username": "username",
|
||||
"display_name": "Display Name",
|
||||
"avatar_url": "https://example.com/avatar.jpg",
|
||||
"is_premium": false,
|
||||
"premium_until": null,
|
||||
"created_at": "2023-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 사용자 정보 업데이트
|
||||
|
||||
```
|
||||
PATCH /users/me
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"display_name": "New Display Name",
|
||||
"avatar_url": "https://example.com/new-avatar.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"email": "user@example.com",
|
||||
"username": "username",
|
||||
"display_name": "New Display Name",
|
||||
"avatar_url": "https://example.com/new-avatar.jpg",
|
||||
"updated_at": "2023-01-02T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 카드 API
|
||||
|
||||
### 카드 생성
|
||||
|
||||
```
|
||||
POST /cards
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"content": "This is a card content",
|
||||
"background_color": "#FFFFFF",
|
||||
"text_color": "#000000",
|
||||
"font_family": "system",
|
||||
"font_size": 16,
|
||||
"text_align": "center",
|
||||
"is_public": false,
|
||||
"tags": ["personal", "ideas"]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (201 Created)**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"content": "This is a card content",
|
||||
"background_color": "#FFFFFF",
|
||||
"text_color": "#000000",
|
||||
"font_family": "system",
|
||||
"font_size": 16,
|
||||
"text_align": "center",
|
||||
"is_public": false,
|
||||
"is_synced": false,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"tags": ["personal", "ideas"]
|
||||
}
|
||||
```
|
||||
|
||||
### 카드 목록 조회
|
||||
|
||||
```
|
||||
GET /cards
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**쿼리 파라미터**:
|
||||
- `page`: 페이지 번호 (기본값: 1)
|
||||
- `limit`: 페이지당 항목 수 (기본값: 20, 최대: 100)
|
||||
- `sort`: 정렬 기준 (options: created_at, updated_at, 기본값: created_at)
|
||||
- `order`: 정렬 순서 (options: asc, desc, 기본값: desc)
|
||||
- `tag`: 태그로 필터링 (선택 사항)
|
||||
- `search`: 내용 검색 (선택 사항)
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "uuid1",
|
||||
"content": "Card 1 content",
|
||||
"background_color": "#FFFFFF",
|
||||
"text_color": "#000000",
|
||||
"font_family": "system",
|
||||
"font_size": 16,
|
||||
"text_align": "center",
|
||||
"is_public": false,
|
||||
"is_synced": false,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"tags": ["personal"]
|
||||
},
|
||||
{
|
||||
"id": "uuid2",
|
||||
"content": "Card 2 content",
|
||||
"background_color": "#F0F0F0",
|
||||
"text_color": "#333333",
|
||||
"font_family": "arial",
|
||||
"font_size": 18,
|
||||
"text_align": "left",
|
||||
"is_public": true,
|
||||
"is_synced": true,
|
||||
"created_at": "2023-01-02T00:00:00Z",
|
||||
"updated_at": "2023-01-02T00:00:00Z",
|
||||
"tags": ["ideas", "public"]
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 42,
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total_pages": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 카드 상세 조회
|
||||
|
||||
```
|
||||
GET /cards/{card_id}
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"content": "This is a card content",
|
||||
"background_color": "#FFFFFF",
|
||||
"text_color": "#000000",
|
||||
"font_family": "system",
|
||||
"font_size": 16,
|
||||
"text_align": "center",
|
||||
"is_public": false,
|
||||
"is_synced": false,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"tags": ["personal", "ideas"],
|
||||
"share_count": 2,
|
||||
"share_platforms": ["facebook", "instagram"]
|
||||
}
|
||||
```
|
||||
|
||||
### 카드 업데이트
|
||||
|
||||
```
|
||||
PATCH /cards/{card_id}
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"content": "Updated card content",
|
||||
"background_color": "#F0F0F0",
|
||||
"is_public": true,
|
||||
"tags": ["personal", "ideas", "updated"]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"content": "Updated card content",
|
||||
"background_color": "#F0F0F0",
|
||||
"text_color": "#000000",
|
||||
"font_family": "system",
|
||||
"font_size": 16,
|
||||
"text_align": "center",
|
||||
"is_public": true,
|
||||
"is_synced": false,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-02T00:00:00Z",
|
||||
"tags": ["personal", "ideas", "updated"]
|
||||
}
|
||||
```
|
||||
|
||||
### 카드 삭제
|
||||
|
||||
```
|
||||
DELETE /cards/{card_id}
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (204 No Content)**
|
||||
|
||||
## 소셜 공유 API
|
||||
|
||||
### 소셜 계정 연동
|
||||
|
||||
```
|
||||
POST /social/connect/{platform}
|
||||
```
|
||||
|
||||
**지원 플랫폼**: `facebook`, `instagram`, `twitter`
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"access_token": "platform_access_token",
|
||||
"refresh_token": "platform_refresh_token",
|
||||
"expires_at": 1672531200
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"platform": "facebook",
|
||||
"connected": true,
|
||||
"platform_user_id": "platform_user_id",
|
||||
"expires_at": 1672531200
|
||||
}
|
||||
```
|
||||
|
||||
### 소셜 계정 연동 해제
|
||||
|
||||
```
|
||||
DELETE /social/connect/{platform}
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (204 No Content)**
|
||||
|
||||
### 연동된 소셜 계정 목록
|
||||
|
||||
```
|
||||
GET /social/accounts
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"platform": "facebook",
|
||||
"platform_user_id": "facebook_user_id",
|
||||
"connected_at": "2023-01-01T00:00:00Z",
|
||||
"expires_at": 1672531200
|
||||
},
|
||||
{
|
||||
"platform": "instagram",
|
||||
"platform_user_id": "instagram_user_id",
|
||||
"connected_at": "2023-01-02T00:00:00Z",
|
||||
"expires_at": 1672617600
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 카드 소셜 공유
|
||||
|
||||
```
|
||||
POST /cards/{card_id}/share/{platform}
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"message": "Check out my new card!" // 선택적 메시지
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "share_id",
|
||||
"card_id": "card_id",
|
||||
"platform": "facebook",
|
||||
"status": "success",
|
||||
"share_url": "https://facebook.com/post/123456",
|
||||
"shared_at": "2023-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 구독 API
|
||||
|
||||
### 구독 플랜 목록
|
||||
|
||||
```
|
||||
GET /subscriptions/plans
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"plans": [
|
||||
{
|
||||
"id": "monthly",
|
||||
"name": "Monthly Premium",
|
||||
"description": "Monthly subscription with cloud sync",
|
||||
"price": 4.99,
|
||||
"currency": "USD",
|
||||
"interval": "month",
|
||||
"features": [
|
||||
"Cloud sync",
|
||||
"Unlimited cards",
|
||||
"Premium templates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "yearly",
|
||||
"name": "Yearly Premium",
|
||||
"description": "Yearly subscription with cloud sync (save 20%)",
|
||||
"price": 47.99,
|
||||
"currency": "USD",
|
||||
"interval": "year",
|
||||
"features": [
|
||||
"Cloud sync",
|
||||
"Unlimited cards",
|
||||
"Premium templates",
|
||||
"Priority support"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 구독 생성
|
||||
|
||||
```
|
||||
POST /subscriptions
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"plan_id": "monthly",
|
||||
"payment_method_id": "payment_method_id",
|
||||
"payment_provider": "stripe"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (201 Created)**:
|
||||
```json
|
||||
{
|
||||
"id": "subscription_id",
|
||||
"plan_id": "monthly",
|
||||
"status": "active",
|
||||
"start_date": "2023-01-01T00:00:00Z",
|
||||
"end_date": "2023-02-01T00:00:00Z",
|
||||
"payment_provider": "stripe",
|
||||
"payment_id": "payment_id"
|
||||
}
|
||||
```
|
||||
|
||||
### 현재 구독 정보
|
||||
|
||||
```
|
||||
GET /subscriptions/current
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "subscription_id",
|
||||
"plan_id": "monthly",
|
||||
"status": "active",
|
||||
"start_date": "2023-01-01T00:00:00Z",
|
||||
"end_date": "2023-02-01T00:00:00Z",
|
||||
"auto_renew": true,
|
||||
"payment_provider": "stripe",
|
||||
"payment_id": "payment_id"
|
||||
}
|
||||
```
|
||||
|
||||
### 구독 취소
|
||||
|
||||
```
|
||||
POST /subscriptions/cancel
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer jwt_token
|
||||
```
|
||||
|
||||
**응답 (200 OK)**:
|
||||
```json
|
||||
{
|
||||
"id": "subscription_id",
|
||||
"status": "canceled",
|
||||
"end_date": "2023-02-01T00:00:00Z",
|
||||
"message": "Subscription will be active until the end date"
|
||||
}
|
||||
```
|
||||
|
||||
## 오류 응답
|
||||
|
||||
모든 API 엔드포인트는 오류 발생 시 다음과 같은 형식으로 응답합니다:
|
||||
|
||||
**응답 (4xx/5xx)**:
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "error_code",
|
||||
"message": "Error message description",
|
||||
"details": {} // 추가 오류 정보 (선택 사항)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 공통 오류 코드
|
||||
|
||||
- `invalid_request`: 잘못된 요청 형식
|
||||
- `authentication_required`: 인증 필요
|
||||
- `invalid_credentials`: 잘못된 인증 정보
|
||||
- `permission_denied`: 권한 없음
|
||||
- `resource_not_found`: 리소스를 찾을 수 없음
|
||||
- `rate_limit_exceeded`: 요청 한도 초과
|
||||
- `internal_server_error`: 서버 내부 오류
|
||||
|
||||
## 웹훅 (Webhook)
|
||||
|
||||
Zellyy는 다음 이벤트에 대한 웹훅을 제공합니다:
|
||||
|
||||
### 웹훅 등록
|
||||
|
||||
```
|
||||
POST /webhooks
|
||||
```
|
||||
|
||||
**요청 헤더**:
|
||||
```
|
||||
Authorization: Bearer admin_token
|
||||
```
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"url": "https://your-service.com/webhook",
|
||||
"events": ["user.created", "subscription.created", "subscription.canceled"],
|
||||
"secret": "your_webhook_secret"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (201 Created)**:
|
||||
```json
|
||||
{
|
||||
"id": "webhook_id",
|
||||
"url": "https://your-service.com/webhook",
|
||||
"events": ["user.created", "subscription.created", "subscription.canceled"],
|
||||
"created_at": "2023-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 웹훅 이벤트 형식
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event_id",
|
||||
"type": "event.type",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"data": {
|
||||
// 이벤트 관련 데이터
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 결론
|
||||
|
||||
이 API 명세서는 Zellyy 프로젝트의 기본 기능을 구현하기 위한 엔드포인트를 정의합니다. 프로젝트가 발전함에 따라 추가 엔드포인트와 기능이 확장될 수 있습니다.
|
||||
245
ZELLYY/zellyy note/02_기술_문서/ERD_다이어그램.md
Normal file
245
ZELLYY/zellyy note/02_기술_문서/ERD_다이어그램.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# ERD 다이어그램
|
||||
|
||||
이 문서는 Zellyy 프로젝트의 데이터베이스 구조를 Entity-Relationship Diagram(ERD)으로 설명합니다.
|
||||
|
||||
## 개요
|
||||
|
||||
Zellyy 프로젝트는 사용자가 카드를 작성하고 관리하며 소셜 미디어에 공유하는 기능을 제공합니다. 이를 위한 데이터베이스 구조는 다음과 같은 주요 엔티티로 구성됩니다:
|
||||
|
||||
1. 사용자 (Users)
|
||||
2. 카드 (Cards)
|
||||
3. 카드 태그 (Card Tags)
|
||||
4. 소셜 계정 (Social Accounts)
|
||||
5. 소셜 공유 (Social Shares)
|
||||
6. 구독 (Subscriptions)
|
||||
|
||||
## ERD 다이어그램
|
||||
|
||||
아래는 Zellyy 프로젝트의 ERD 다이어그램입니다. 이 다이어그램은 [dbdiagram.io](https://dbdiagram.io)를 사용하여 생성되었습니다.
|
||||
|
||||
```
|
||||
// Zellyy 데이터베이스 ERD
|
||||
// 이 다이어그램은 dbdiagram.io에서 생성되었습니다.
|
||||
|
||||
Table zellyy.users {
|
||||
id UUID [pk, ref: > auth.users.id]
|
||||
email TEXT [not null, unique]
|
||||
username TEXT [unique]
|
||||
display_name TEXT
|
||||
avatar_url TEXT
|
||||
created_at TIMESTAMP [default: `NOW()`]
|
||||
updated_at TIMESTAMP [default: `NOW()`]
|
||||
last_login TIMESTAMP
|
||||
is_premium BOOLEAN [default: false]
|
||||
premium_until TIMESTAMP
|
||||
}
|
||||
|
||||
Table zellyy.cards {
|
||||
id UUID [pk, default: `uuid_generate_v4()`]
|
||||
user_id UUID [not null, ref: > zellyy.users.id]
|
||||
content TEXT [not null]
|
||||
background_color TEXT [default: '#FFFFFF']
|
||||
text_color TEXT [default: '#000000']
|
||||
font_family TEXT [default: 'system']
|
||||
font_size INTEGER [default: 16]
|
||||
text_align TEXT [default: 'center']
|
||||
is_public BOOLEAN [default: false]
|
||||
is_synced BOOLEAN [default: false]
|
||||
created_at TIMESTAMP [default: `NOW()`]
|
||||
updated_at TIMESTAMP [default: `NOW()`]
|
||||
deleted_at TIMESTAMP
|
||||
}
|
||||
|
||||
Table zellyy.card_tags {
|
||||
id UUID [pk, default: `uuid_generate_v4()`]
|
||||
card_id UUID [not null, ref: > zellyy.cards.id]
|
||||
tag_name TEXT [not null]
|
||||
created_at TIMESTAMP [default: `NOW()`]
|
||||
|
||||
indexes {
|
||||
(card_id, tag_name) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Table zellyy.social_accounts {
|
||||
id UUID [pk, default: `uuid_generate_v4()`]
|
||||
user_id UUID [not null, ref: > zellyy.users.id]
|
||||
platform TEXT [not null]
|
||||
platform_user_id TEXT
|
||||
access_token TEXT
|
||||
refresh_token TEXT
|
||||
token_expires_at TIMESTAMP
|
||||
created_at TIMESTAMP [default: `NOW()`]
|
||||
updated_at TIMESTAMP [default: `NOW()`]
|
||||
|
||||
indexes {
|
||||
(user_id, platform) [unique]
|
||||
}
|
||||
}
|
||||
|
||||
Table zellyy.social_shares {
|
||||
id UUID [pk, default: `uuid_generate_v4()`]
|
||||
card_id UUID [not null, ref: > zellyy.cards.id]
|
||||
user_id UUID [not null, ref: > zellyy.users.id]
|
||||
platform TEXT [not null]
|
||||
share_url TEXT
|
||||
shared_at TIMESTAMP [default: `NOW()`]
|
||||
status TEXT [default: 'pending']
|
||||
response_data JSONB
|
||||
}
|
||||
|
||||
Table zellyy.subscriptions {
|
||||
id UUID [pk, default: `uuid_generate_v4()`]
|
||||
user_id UUID [not null, ref: > zellyy.users.id]
|
||||
plan_type TEXT [not null]
|
||||
status TEXT [not null]
|
||||
start_date TIMESTAMP [not null]
|
||||
end_date TIMESTAMP [not null]
|
||||
payment_provider TEXT
|
||||
payment_id TEXT
|
||||
created_at TIMESTAMP [default: `NOW()`]
|
||||
updated_at TIMESTAMP [default: `NOW()`]
|
||||
}
|
||||
|
||||
// 참조 테이블 (Supabase Auth)
|
||||
Table auth.users {
|
||||
id UUID [pk]
|
||||
email TEXT [unique]
|
||||
// 기타 Supabase Auth 필드
|
||||
}
|
||||
```
|
||||
|
||||
## 엔티티 설명
|
||||
|
||||
### 1. 사용자 (zellyy.users)
|
||||
|
||||
사용자 정보를 저장하는 테이블입니다. Supabase Auth와 연동됩니다.
|
||||
|
||||
- **id**: 사용자 고유 식별자 (UUID), Supabase Auth의 사용자 ID와 연결
|
||||
- **email**: 사용자 이메일 주소
|
||||
- **username**: 사용자 이름 (고유)
|
||||
- **display_name**: 표시 이름
|
||||
- **avatar_url**: 프로필 이미지 URL
|
||||
- **created_at**: 계정 생성 시간
|
||||
- **updated_at**: 계정 정보 업데이트 시간
|
||||
- **last_login**: 마지막 로그인 시간
|
||||
- **is_premium**: 프리미엄 사용자 여부
|
||||
- **premium_until**: 프리미엄 구독 만료 시간
|
||||
|
||||
### 2. 카드 (zellyy.cards)
|
||||
|
||||
사용자가 작성한 카드 정보를 저장하는 테이블입니다.
|
||||
|
||||
- **id**: 카드 고유 식별자 (UUID)
|
||||
- **user_id**: 카드 작성자 ID (users 테이블 참조)
|
||||
- **content**: 카드 내용
|
||||
- **background_color**: 배경색 (HEX 코드)
|
||||
- **text_color**: 텍스트 색상 (HEX 코드)
|
||||
- **font_family**: 폰트 패밀리
|
||||
- **font_size**: 폰트 크기
|
||||
- **text_align**: 텍스트 정렬 방식 ('left', 'center', 'right')
|
||||
- **is_public**: 공개 여부
|
||||
- **is_synced**: 클라우드 동기화 여부
|
||||
- **created_at**: 카드 생성 시간
|
||||
- **updated_at**: 카드 업데이트 시간
|
||||
- **deleted_at**: 카드 삭제 시간 (소프트 삭제)
|
||||
|
||||
### 3. 카드 태그 (zellyy.card_tags)
|
||||
|
||||
카드에 적용된 태그 정보를 저장하는 테이블입니다.
|
||||
|
||||
- **id**: 태그 고유 식별자 (UUID)
|
||||
- **card_id**: 카드 ID (cards 테이블 참조)
|
||||
- **tag_name**: 태그 이름
|
||||
- **created_at**: 태그 생성 시간
|
||||
|
||||
### 4. 소셜 계정 (zellyy.social_accounts)
|
||||
|
||||
사용자의 소셜 미디어 계정 연동 정보를 저장하는 테이블입니다.
|
||||
|
||||
- **id**: 소셜 계정 고유 식별자 (UUID)
|
||||
- **user_id**: 사용자 ID (users 테이블 참조)
|
||||
- **platform**: 플랫폼 이름 ('facebook', 'twitter', 'instagram' 등)
|
||||
- **platform_user_id**: 플랫폼에서의 사용자 ID
|
||||
- **access_token**: 액세스 토큰
|
||||
- **refresh_token**: 리프레시 토큰
|
||||
- **token_expires_at**: 토큰 만료 시간
|
||||
- **created_at**: 연동 생성 시간
|
||||
- **updated_at**: 연동 업데이트 시간
|
||||
|
||||
### 5. 소셜 공유 (zellyy.social_shares)
|
||||
|
||||
카드의 소셜 미디어 공유 기록을 저장하는 테이블입니다.
|
||||
|
||||
- **id**: 공유 기록 고유 식별자 (UUID)
|
||||
- **card_id**: 카드 ID (cards 테이블 참조)
|
||||
- **user_id**: 사용자 ID (users 테이블 참조)
|
||||
- **platform**: 공유된 플랫폼 이름
|
||||
- **share_url**: 공유된 URL
|
||||
- **shared_at**: 공유 시간
|
||||
- **status**: 공유 상태 ('pending', 'success', 'failed')
|
||||
- **response_data**: 플랫폼 응답 데이터 (JSON)
|
||||
|
||||
### 6. 구독 (zellyy.subscriptions)
|
||||
|
||||
사용자의 구독 정보를 저장하는 테이블입니다.
|
||||
|
||||
- **id**: 구독 고유 식별자 (UUID)
|
||||
- **user_id**: 사용자 ID (users 테이블 참조)
|
||||
- **plan_type**: 구독 플랜 유형 ('monthly', 'yearly' 등)
|
||||
- **status**: 구독 상태 ('active', 'canceled', 'expired')
|
||||
- **start_date**: 구독 시작 날짜
|
||||
- **end_date**: 구독 종료 날짜
|
||||
- **payment_provider**: 결제 제공자 ('apple', 'google', 'stripe' 등)
|
||||
- **payment_id**: 결제 ID
|
||||
- **created_at**: 구독 생성 시간
|
||||
- **updated_at**: 구독 업데이트 시간
|
||||
|
||||
## 관계 설명
|
||||
|
||||
1. **사용자와 카드**: 일대다 관계. 한 사용자는 여러 카드를 가질 수 있습니다.
|
||||
2. **카드와 태그**: 일대다 관계. 한 카드는 여러 태그를 가질 수 있습니다.
|
||||
3. **사용자와 소셜 계정**: 일대다 관계. 한 사용자는 여러 소셜 계정을 연동할 수 있습니다.
|
||||
4. **카드와 소셜 공유**: 일대다 관계. 한 카드는 여러 소셜 미디어에 공유될 수 있습니다.
|
||||
5. **사용자와 구독**: 일대다 관계. 한 사용자는 여러 구독 기록을 가질 수 있습니다.
|
||||
|
||||
## 인덱스
|
||||
|
||||
성능 최적화를 위해 다음과 같은 인덱스를 생성합니다:
|
||||
|
||||
1. **zellyy.cards**:
|
||||
- `user_id`: 사용자별 카드 조회 최적화
|
||||
- `created_at`: 시간순 정렬 최적화
|
||||
- `is_public`: 공개 카드 필터링 최적화
|
||||
|
||||
2. **zellyy.card_tags**:
|
||||
- `(card_id, tag_name)`: 고유 제약 조건 및 카드별 태그 조회 최적화
|
||||
- `tag_name`: 태그별 카드 검색 최적화
|
||||
|
||||
3. **zellyy.social_accounts**:
|
||||
- `(user_id, platform)`: 고유 제약 조건 및 사용자별 플랫폼 계정 조회 최적화
|
||||
|
||||
4. **zellyy.social_shares**:
|
||||
- `user_id`: 사용자별 공유 기록 조회 최적화
|
||||
- `card_id`: 카드별 공유 기록 조회 최적화
|
||||
|
||||
## 데이터 무결성
|
||||
|
||||
데이터 무결성을 보장하기 위해 다음과 같은 제약 조건을 적용합니다:
|
||||
|
||||
1. **외래 키 제약 조건**: 모든 관계는 외래 키로 연결되어 참조 무결성을 보장합니다.
|
||||
2. **고유 제약 조건**: 이메일, 사용자 이름 등은 고유해야 합니다.
|
||||
3. **NOT NULL 제약 조건**: 필수 필드는 NULL이 될 수 없습니다.
|
||||
4. **기본값**: 많은 필드에 기본값을 제공하여 데이터 일관성을 유지합니다.
|
||||
|
||||
## 확장성 고려사항
|
||||
|
||||
1. **샤딩**: 사용자 수가 크게 증가할 경우, 사용자 ID를 기준으로 데이터를 샤딩하는 전략을 고려할 수 있습니다.
|
||||
2. **아카이빙**: 오래된 카드 데이터는 별도의 아카이브 테이블로 이동하여 주 테이블의 성능을 유지할 수 있습니다.
|
||||
3. **인덱스 최적화**: 실제 쿼리 패턴에 따라 추가 인덱스를 생성하거나 기존 인덱스를 조정할 수 있습니다.
|
||||
|
||||
## 결론
|
||||
|
||||
이 ERD는 Zellyy 프로젝트의 데이터 모델을 시각적으로 표현합니다. 이 구조는 사용자가 카드를 작성하고, 태그를 지정하며, 소셜 미디어에 공유하는 핵심 기능을 지원합니다. 또한 구독 관리와 소셜 계정 연동을 위한 테이블도 포함되어 있습니다.
|
||||
|
||||
프로젝트가 발전함에 따라 이 ERD는 새로운 요구사항을 반영하여 업데이트될 수 있습니다.
|
||||
335
ZELLYY/zellyy note/02_기술_문서/React_Native_설정_가이드.md
Normal file
335
ZELLYY/zellyy note/02_기술_문서/React_Native_설정_가이드.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# React Native 설정 가이드
|
||||
|
||||
이 문서는 Zellyy 모바일 앱 개발을 위한 React Native 개발 환경 설정 방법을 안내합니다.
|
||||
|
||||
## 개발 환경 요구사항
|
||||
|
||||
- Node.js 16.0.0 이상
|
||||
- npm 8.0.0 이상 또는 Yarn 1.22.0 이상
|
||||
- Git
|
||||
- Watchman (macOS 사용자)
|
||||
- Xcode 14.0 이상 (iOS 개발용, macOS 필요)
|
||||
- Android Studio (Android 개발용)
|
||||
- JDK 11 이상
|
||||
|
||||
## 개발 환경 설정
|
||||
|
||||
### 1. Node.js 및 npm 설치
|
||||
|
||||
#### macOS (Homebrew 사용)
|
||||
|
||||
```bash
|
||||
brew install node
|
||||
```
|
||||
|
||||
#### Windows (Chocolatey 사용)
|
||||
|
||||
```bash
|
||||
choco install nodejs
|
||||
```
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
### 2. React Native CLI 설치
|
||||
|
||||
```bash
|
||||
npm install -g react-native-cli
|
||||
```
|
||||
|
||||
### 3. Expo CLI 설치 (권장)
|
||||
|
||||
Expo는 React Native 개발을 더 쉽게 만들어주는 도구 세트입니다. Zellyy 프로젝트는 Expo를 사용하여 개발합니다.
|
||||
|
||||
```bash
|
||||
npm install -g expo-cli
|
||||
```
|
||||
|
||||
### 4. iOS 개발 환경 설정 (macOS 필요)
|
||||
|
||||
#### Xcode 설치
|
||||
|
||||
App Store에서 Xcode를 설치하거나 [Apple 개발자 웹사이트](https://developer.apple.com/xcode/)에서 다운로드합니다.
|
||||
|
||||
#### iOS 시뮬레이터 설정
|
||||
|
||||
Xcode를 설치한 후, 다음 명령어로 iOS 시뮬레이터를 설치합니다:
|
||||
|
||||
1. Xcode 실행
|
||||
2. Xcode > Preferences > Components 메뉴 선택
|
||||
3. 원하는 iOS 시뮬레이터 버전 설치
|
||||
|
||||
#### CocoaPods 설치
|
||||
|
||||
```bash
|
||||
sudo gem install cocoapods
|
||||
```
|
||||
|
||||
### 5. Android 개발 환경 설정
|
||||
|
||||
#### Android Studio 설치
|
||||
|
||||
[Android Studio 다운로드 페이지](https://developer.android.com/studio)에서 Android Studio를 다운로드하고 설치합니다.
|
||||
|
||||
#### Android SDK 설치
|
||||
|
||||
Android Studio를 설치한 후, 다음 단계를 따릅니다:
|
||||
|
||||
1. Android Studio 실행
|
||||
2. SDK Manager 열기 (Tools > SDK Manager)
|
||||
3. "SDK Platforms" 탭에서 Android 12 (API Level 31) 이상 설치
|
||||
4. "SDK Tools" 탭에서 다음 항목 설치:
|
||||
- Android SDK Build-Tools
|
||||
- Android Emulator
|
||||
- Android SDK Platform-Tools
|
||||
- Google Play services
|
||||
|
||||
#### 환경 변수 설정
|
||||
|
||||
##### macOS/Linux
|
||||
|
||||
`~/.bash_profile` 또는 `~/.zshrc` 파일에 다음 내용을 추가합니다:
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME=$HOME/Library/Android/sdk
|
||||
export PATH=$PATH:$ANDROID_HOME/emulator
|
||||
export PATH=$PATH:$ANDROID_HOME/tools
|
||||
export PATH=$PATH:$ANDROID_HOME/tools/bin
|
||||
export PATH=$PATH:$ANDROID_HOME/platform-tools
|
||||
```
|
||||
|
||||
##### Windows
|
||||
|
||||
시스템 환경 변수에 다음 내용을 추가합니다:
|
||||
|
||||
- 변수 이름: `ANDROID_HOME`
|
||||
- 변수 값: `C:\Users\USERNAME\AppData\Local\Android\Sdk`
|
||||
|
||||
PATH 환경 변수에 다음 경로를 추가합니다:
|
||||
- `%ANDROID_HOME%\platform-tools`
|
||||
- `%ANDROID_HOME%\emulator`
|
||||
- `%ANDROID_HOME%\tools`
|
||||
- `%ANDROID_HOME%\tools\bin`
|
||||
|
||||
## Zellyy 프로젝트 설정
|
||||
|
||||
### 1. 프로젝트 클론
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-organization/zellyy-app.git
|
||||
cd zellyy-app
|
||||
```
|
||||
|
||||
### 2. 의존성 설치
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# 또는
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 3. 환경 변수 설정
|
||||
|
||||
`.env.example` 파일을 복사하여 `.env` 파일을 생성하고 필요한 환경 변수를 설정합니다:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
`.env` 파일을 열고 다음 변수를 설정합니다:
|
||||
|
||||
```
|
||||
API_URL=https://a11.ism.kr/api
|
||||
SUPABASE_URL=https://a11.ism.kr
|
||||
SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||
```
|
||||
|
||||
### 4. iOS 설정 완료 (macOS 필요)
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
pod install
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 5. 앱 실행
|
||||
|
||||
#### Expo 개발 서버 시작
|
||||
|
||||
```bash
|
||||
expo start
|
||||
```
|
||||
|
||||
이 명령어는 개발 서버를 시작하고 QR 코드를 표시합니다. Expo Go 앱(iOS/Android)으로 QR 코드를 스캔하여 앱을 실행할 수 있습니다.
|
||||
|
||||
#### iOS 시뮬레이터에서 실행
|
||||
|
||||
```bash
|
||||
expo run:ios
|
||||
# 또는
|
||||
npm run ios
|
||||
```
|
||||
|
||||
#### Android 에뮬레이터에서 실행
|
||||
|
||||
```bash
|
||||
expo run:android
|
||||
# 또는
|
||||
npm run android
|
||||
```
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
Zellyy 앱 프로젝트는 다음과 같은 구조로 구성됩니다:
|
||||
|
||||
```
|
||||
zellyy-app/
|
||||
├── App.js # 앱의 진입점
|
||||
├── app.json # Expo 설정
|
||||
├── assets/ # 이미지, 폰트 등의 정적 자산
|
||||
├── babel.config.js # Babel 설정
|
||||
├── components/ # 재사용 가능한 UI 컴포넌트
|
||||
│ ├── Card/ # 카드 관련 컴포넌트
|
||||
│ ├── Common/ # 공통 UI 컴포넌트
|
||||
│ └── ...
|
||||
├── constants/ # 상수 정의
|
||||
├── hooks/ # 커스텀 React 훅
|
||||
├── navigation/ # 네비게이션 설정
|
||||
├── screens/ # 앱 화면
|
||||
│ ├── Auth/ # 인증 관련 화면
|
||||
│ ├── Cards/ # 카드 관련 화면
|
||||
│ ├── Profile/ # 프로필 관련 화면
|
||||
│ └── ...
|
||||
├── services/ # API 및 외부 서비스 통합
|
||||
│ ├── api.js # API 클라이언트
|
||||
│ ├── supabase.js # Supabase 클라이언트
|
||||
│ └── ...
|
||||
├── store/ # 상태 관리 (Redux 또는 Context API)
|
||||
├── styles/ # 글로벌 스타일 정의
|
||||
└── utils/ # 유틸리티 함수
|
||||
```
|
||||
|
||||
## 주요 라이브러리
|
||||
|
||||
Zellyy 앱은 다음과 같은 주요 라이브러리를 사용합니다:
|
||||
|
||||
- **React Navigation**: 화면 간 네비게이션
|
||||
- **Expo**: 개발 도구 및 네이티브 기능 접근
|
||||
- **Supabase JS Client**: Supabase 백엔드 연동
|
||||
- **React Native Paper**: Material Design 컴포넌트
|
||||
- **Formik & Yup**: 폼 관리 및 유효성 검사
|
||||
- **React Native Share**: 소셜 미디어 공유 기능
|
||||
- **Async Storage**: 로컬 데이터 저장
|
||||
- **React Native SVG**: SVG 이미지 지원
|
||||
- **React Native Reanimated**: 고급 애니메이션
|
||||
|
||||
## 코딩 스타일 가이드
|
||||
|
||||
Zellyy 프로젝트는 다음과 같은 코딩 스타일을 따릅니다:
|
||||
|
||||
- **ESLint & Prettier**: 코드 품질 및 포맷팅
|
||||
- **함수형 컴포넌트**: 클래스 컴포넌트 대신 함수형 컴포넌트와 훅 사용
|
||||
- **명명 규칙**:
|
||||
- 컴포넌트: PascalCase (예: `CardItem.js`)
|
||||
- 함수 및 변수: camelCase (예: `getUserData`)
|
||||
- 상수: UPPER_SNAKE_CASE (예: `API_URL`)
|
||||
- **파일 구조**: 관련 기능끼리 폴더로 그룹화
|
||||
- **주석**: 복잡한 로직에 주석 추가
|
||||
|
||||
## 디버깅
|
||||
|
||||
### React Native Debugger 설정
|
||||
|
||||
[React Native Debugger](https://github.com/jhen0409/react-native-debugger)를 사용하여 앱을 디버깅할 수 있습니다:
|
||||
|
||||
1. React Native Debugger 설치:
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
brew install --cask react-native-debugger
|
||||
|
||||
# Windows/Linux
|
||||
# GitHub 릴리스 페이지에서 다운로드
|
||||
```
|
||||
|
||||
2. 디버거 실행:
|
||||
```bash
|
||||
open "rndebugger://set-debugger-loc?host=localhost&port=19000"
|
||||
```
|
||||
|
||||
3. 앱에서 디버깅 활성화:
|
||||
- iOS 시뮬레이터: `Cmd + D` 누르고 "Debug JS Remotely" 선택
|
||||
- Android 에뮬레이터: `Cmd + M` 또는 `Ctrl + M` 누르고 "Debug JS Remotely" 선택
|
||||
|
||||
### Flipper (선택 사항)
|
||||
|
||||
네이티브 코드 디버깅을 위해 [Flipper](https://fbflipper.com/)를 사용할 수 있습니다.
|
||||
|
||||
## 빌드 및 배포
|
||||
|
||||
### iOS 앱 빌드
|
||||
|
||||
1. 앱 버전 업데이트:
|
||||
`app.json` 파일에서 `version` 및 `buildNumber` 업데이트
|
||||
|
||||
2. 배포용 빌드 생성:
|
||||
```bash
|
||||
expo build:ios
|
||||
```
|
||||
|
||||
3. 생성된 IPA 파일을 App Store Connect에 업로드
|
||||
|
||||
### Android 앱 빌드
|
||||
|
||||
1. 앱 버전 업데이트:
|
||||
`app.json` 파일에서 `version` 및 `versionCode` 업데이트
|
||||
|
||||
2. 배포용 빌드 생성:
|
||||
```bash
|
||||
expo build:android
|
||||
```
|
||||
|
||||
3. 생성된 APK 또는 AAB 파일을 Google Play Console에 업로드
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### 일반적인 문제 및 해결 방법
|
||||
|
||||
1. **Metro 번들러 캐시 문제**:
|
||||
```bash
|
||||
expo start --clear
|
||||
```
|
||||
|
||||
2. **의존성 문제**:
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **iOS 빌드 실패**:
|
||||
```bash
|
||||
cd ios
|
||||
pod install --repo-update
|
||||
cd ..
|
||||
```
|
||||
|
||||
4. **Android 빌드 실패**:
|
||||
```bash
|
||||
cd android
|
||||
./gradlew clean
|
||||
cd ..
|
||||
```
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- [React Native 공식 문서](https://reactnative.dev/docs/getting-started)
|
||||
- [Expo 문서](https://docs.expo.dev/)
|
||||
- [React Navigation 문서](https://reactnavigation.org/docs/getting-started)
|
||||
- [Supabase 문서](https://supabase.io/docs)
|
||||
|
||||
## 결론
|
||||
|
||||
이 가이드는 Zellyy 앱 개발을 위한 React Native 환경 설정 및 프로젝트 구조에 대한 기본 정보를 제공합니다. 개발 과정에서 추가적인 질문이나 문제가 있으면 개발팀에 문의하세요.
|
||||
802
ZELLYY/zellyy note/02_기술_문서/SNS_통합_가이드.md
Normal file
802
ZELLYY/zellyy note/02_기술_문서/SNS_통합_가이드.md
Normal file
@@ -0,0 +1,802 @@
|
||||
# SNS 통합 가이드
|
||||
|
||||
이 문서는 Zellyy 앱에서 다양한 소셜 미디어 플랫폼과의 통합 방법을 안내합니다.
|
||||
|
||||
## 개요
|
||||
|
||||
Zellyy 앱의 핵심 기능 중 하나는 사용자가 작성한 카드를 다양한 소셜 미디어 플랫폼에 공유하는 기능입니다. 이 문서는 다음 플랫폼과의 통합 방법을 설명합니다:
|
||||
|
||||
- Facebook
|
||||
- Instagram
|
||||
- Twitter (X)
|
||||
- LinkedIn
|
||||
- Pinterest
|
||||
|
||||
## 사전 요구사항
|
||||
|
||||
- 각 소셜 미디어 플랫폼의 개발자 계정
|
||||
- 각 플랫폼에 등록된 앱
|
||||
- React Native 개발 환경
|
||||
- Supabase 설정 완료
|
||||
|
||||
## 1. 소셜 미디어 앱 등록
|
||||
|
||||
### 1.1 Facebook 앱 등록
|
||||
|
||||
1. [Facebook 개발자 포털](https://developers.facebook.com/)에 접속
|
||||
2. "내 앱" > "앱 만들기" 클릭
|
||||
3. "소비자" 유형 선택
|
||||
4. 앱 이름 입력 (예: "Zellyy")
|
||||
5. 앱 생성 후 다음 제품 추가:
|
||||
- Facebook 로그인
|
||||
- Instagram API
|
||||
- 공유 API
|
||||
|
||||
설정 완료 후 다음 정보를 기록:
|
||||
- 앱 ID
|
||||
- 앱 시크릿
|
||||
|
||||
### 1.2 Twitter 앱 등록
|
||||
|
||||
1. [Twitter 개발자 포털](https://developer.twitter.com/)에 접속
|
||||
2. "Projects & Apps" > "Overview" > "Create App" 클릭
|
||||
3. 앱 이름, 설명 입력
|
||||
4. 앱 권한 설정: "Read and Write"
|
||||
5. 리디렉션 URL 설정: `zellyy://auth/twitter`
|
||||
|
||||
설정 완료 후 다음 정보를 기록:
|
||||
- API 키
|
||||
- API 시크릿 키
|
||||
- 액세스 토큰
|
||||
- 액세스 토큰 시크릿
|
||||
|
||||
### 1.3 LinkedIn 앱 등록
|
||||
|
||||
1. [LinkedIn 개발자 포털](https://www.linkedin.com/developers/)에 접속
|
||||
2. "Create App" 클릭
|
||||
3. 앱 이름, 설명, 로고 등 입력
|
||||
4. 제품 추가: "Share on LinkedIn"
|
||||
5. OAuth 2.0 설정:
|
||||
- 리디렉션 URL: `zellyy://auth/linkedin`
|
||||
- 권한: `r_liteprofile`, `w_member_social`
|
||||
|
||||
설정 완료 후 다음 정보를 기록:
|
||||
- 클라이언트 ID
|
||||
- 클라이언트 시크릿
|
||||
|
||||
### 1.4 Pinterest 앱 등록
|
||||
|
||||
1. [Pinterest 개발자 포털](https://developers.pinterest.com/)에 접속
|
||||
2. "Apps" > "Create app" 클릭
|
||||
3. 앱 이름, 설명 입력
|
||||
4. 리디렉션 URL 설정: `zellyy://auth/pinterest`
|
||||
5. 권한 설정: `read_public`, `write_public`
|
||||
|
||||
설정 완료 후 다음 정보를 기록:
|
||||
- 앱 ID
|
||||
- 앱 시크릿
|
||||
|
||||
## 2. 환경 변수 설정
|
||||
|
||||
앱의 `.env` 파일에 소셜 미디어 API 키를 추가합니다:
|
||||
|
||||
```
|
||||
# Facebook
|
||||
FACEBOOK_APP_ID=your_facebook_app_id
|
||||
FACEBOOK_APP_SECRET=your_facebook_app_secret
|
||||
|
||||
# Twitter
|
||||
TWITTER_CONSUMER_KEY=your_twitter_api_key
|
||||
TWITTER_CONSUMER_SECRET=your_twitter_api_secret
|
||||
|
||||
# LinkedIn
|
||||
LINKEDIN_CLIENT_ID=your_linkedin_client_id
|
||||
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret
|
||||
|
||||
# Pinterest
|
||||
PINTEREST_APP_ID=your_pinterest_app_id
|
||||
PINTEREST_APP_SECRET=your_pinterest_app_secret
|
||||
```
|
||||
|
||||
## 3. 필요한 패키지 설치
|
||||
|
||||
```bash
|
||||
# 소셜 로그인 및 공유 라이브러리
|
||||
npm install react-native-fbsdk-next
|
||||
npm install @react-native-twitter-signin/twitter-signin
|
||||
npm install react-native-linkedin
|
||||
npm install react-native-pinterest
|
||||
|
||||
# 일반 공유 기능
|
||||
npm install react-native-share
|
||||
|
||||
# 딥링크 처리
|
||||
npm install react-native-app-auth
|
||||
```
|
||||
|
||||
## 4. iOS 설정
|
||||
|
||||
### 4.1 Info.plist 설정
|
||||
|
||||
`ios/YourApp/Info.plist` 파일에 다음 내용을 추가합니다:
|
||||
|
||||
```xml
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<!-- Facebook -->
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>fb{FACEBOOK_APP_ID}</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- Twitter -->
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>twitterkit-{TWITTER_CONSUMER_KEY}</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- LinkedIn -->
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>zellyy</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- Pinterest -->
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>pdk{PINTEREST_APP_ID}</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<!-- Facebook 설정 -->
|
||||
<key>FacebookAppID</key>
|
||||
<string>{FACEBOOK_APP_ID}</string>
|
||||
<key>FacebookDisplayName</key>
|
||||
<string>Zellyy</string>
|
||||
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>fbapi</string>
|
||||
<string>fb-messenger-share-api</string>
|
||||
<string>fbauth2</string>
|
||||
<string>fbshareextension</string>
|
||||
<string>twitter</string>
|
||||
<string>twitterauth</string>
|
||||
<string>linkedin</string>
|
||||
<string>linkedin-sdk2</string>
|
||||
<string>pinterestsdk</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
### 4.2 AppDelegate.m 수정
|
||||
|
||||
`ios/YourApp/AppDelegate.m` 파일에 다음 내용을 추가합니다:
|
||||
|
||||
```objective-c
|
||||
#import <FBSDKCoreKit/FBSDKCoreKit-swift.h>
|
||||
#import <TwitterKit/TwitterKit.h>
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
// ...
|
||||
|
||||
// Facebook SDK 초기화
|
||||
[[FBSDKApplicationDelegate sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
|
||||
// Twitter SDK 초기화
|
||||
[[Twitter sharedInstance] startWithConsumerKey:@"TWITTER_CONSUMER_KEY" consumerSecret:@"TWITTER_CONSUMER_SECRET"];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// URL 스킴 처리
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
|
||||
{
|
||||
// Facebook URL 처리
|
||||
BOOL handled = [[FBSDKApplicationDelegate sharedInstance] application:app openURL:url options:options];
|
||||
|
||||
// Twitter URL 처리
|
||||
if (!handled) {
|
||||
handled = [[Twitter sharedInstance] application:app openURL:url options:options];
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
## 5. Android 설정
|
||||
|
||||
### 5.1 AndroidManifest.xml 수정
|
||||
|
||||
`android/app/src/main/AndroidManifest.xml` 파일에 다음 내용을 추가합니다:
|
||||
|
||||
```xml
|
||||
<manifest ...>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application ...>
|
||||
<!-- Facebook 설정 -->
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.ApplicationId"
|
||||
android:value="@string/facebook_app_id" />
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.ClientToken"
|
||||
android:value="@string/facebook_client_token" />
|
||||
|
||||
<activity
|
||||
android:name="com.facebook.FacebookActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation"
|
||||
android:label="@string/app_name" />
|
||||
<activity
|
||||
android:name="com.facebook.CustomTabActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="@string/fb_login_protocol_scheme" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Twitter 설정 -->
|
||||
<activity
|
||||
android:name="com.twitter.sdk.android.core.identity.OAuthActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
|
||||
<!-- 딥링크 처리 -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="zellyy" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
### 5.2 strings.xml 설정
|
||||
|
||||
`android/app/src/main/res/values/strings.xml` 파일에 다음 내용을 추가합니다:
|
||||
|
||||
```xml
|
||||
<resources>
|
||||
<string name="app_name">Zellyy</string>
|
||||
<string name="facebook_app_id">FACEBOOK_APP_ID</string>
|
||||
<string name="fb_login_protocol_scheme">fbFACEBOOK_APP_ID</string>
|
||||
<string name="facebook_client_token">FACEBOOK_CLIENT_TOKEN</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
### 5.3 build.gradle 설정
|
||||
|
||||
`android/app/build.gradle` 파일에 다음 내용을 추가합니다:
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
// ...
|
||||
implementation 'com.facebook.android:facebook-android-sdk:latest.release'
|
||||
implementation 'com.twitter.sdk.android:twitter-core:3.3.0'
|
||||
implementation 'com.twitter.sdk.android:tweet-composer:3.3.0'
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 소셜 로그인 구현
|
||||
|
||||
### 6.1 Facebook 로그인
|
||||
|
||||
```javascript
|
||||
import { LoginManager, AccessToken, GraphRequest, GraphRequestManager } from 'react-native-fbsdk-next';
|
||||
import supabase from '../services/supabase';
|
||||
|
||||
const loginWithFacebook = async () => {
|
||||
try {
|
||||
// 로그인 권한 요청
|
||||
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
|
||||
|
||||
if (result.isCancelled) {
|
||||
throw new Error('User cancelled login');
|
||||
}
|
||||
|
||||
// 액세스 토큰 가져오기
|
||||
const data = await AccessToken.getCurrentAccessToken();
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Failed to get access token');
|
||||
}
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
const profileRequest = new GraphRequest(
|
||||
'/me',
|
||||
{
|
||||
accessToken: data.accessToken,
|
||||
parameters: {
|
||||
fields: {
|
||||
string: 'id,name,email,picture.type(large)',
|
||||
},
|
||||
},
|
||||
},
|
||||
async (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error fetching profile:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Supabase에 소셜 계정 연결
|
||||
const { data: socialAccount, error: socialError } = await supabase
|
||||
.from('zellyy.social_accounts')
|
||||
.upsert({
|
||||
user_id: supabase.auth.user().id,
|
||||
platform: 'facebook',
|
||||
platform_user_id: result.id,
|
||||
access_token: data.accessToken,
|
||||
token_expires_at: new Date(data.expirationTime),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (socialError) {
|
||||
console.error('Error connecting Facebook account:', socialError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
new GraphRequestManager().addRequest(profileRequest).start();
|
||||
} catch (error) {
|
||||
console.error('Facebook login error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 Twitter 로그인
|
||||
|
||||
```javascript
|
||||
import { TwitterSignIn } from '@react-native-twitter-signin/twitter-signin';
|
||||
import supabase from '../services/supabase';
|
||||
|
||||
const loginWithTwitter = async () => {
|
||||
try {
|
||||
// Twitter SDK 초기화
|
||||
TwitterSignIn.init(
|
||||
'TWITTER_CONSUMER_KEY',
|
||||
'TWITTER_CONSUMER_SECRET'
|
||||
);
|
||||
|
||||
// 로그인 요청
|
||||
const { authToken, authTokenSecret, userName, userID } = await TwitterSignIn.logIn();
|
||||
|
||||
// Supabase에 소셜 계정 연결
|
||||
const { data: socialAccount, error: socialError } = await supabase
|
||||
.from('zellyy.social_accounts')
|
||||
.upsert({
|
||||
user_id: supabase.auth.user().id,
|
||||
platform: 'twitter',
|
||||
platform_user_id: userID,
|
||||
access_token: authToken,
|
||||
refresh_token: authTokenSecret,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (socialError) {
|
||||
console.error('Error connecting Twitter account:', socialError);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Twitter login error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 7. 소셜 공유 구현
|
||||
|
||||
### 7.1 일반 공유 기능
|
||||
|
||||
```javascript
|
||||
import Share from 'react-native-share';
|
||||
|
||||
const shareToDefault = async (card) => {
|
||||
try {
|
||||
// 카드 이미지 생성 (별도 함수로 구현)
|
||||
const imageUrl = await generateCardImage(card);
|
||||
|
||||
const options = {
|
||||
title: 'Share via',
|
||||
message: card.content,
|
||||
url: imageUrl,
|
||||
};
|
||||
|
||||
const result = await Share.open(options);
|
||||
console.log('Share result:', result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error sharing:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 7.2 Facebook 공유
|
||||
|
||||
```javascript
|
||||
import { ShareDialog } from 'react-native-fbsdk-next';
|
||||
import supabase from '../services/supabase';
|
||||
|
||||
const shareToFacebook = async (card) => {
|
||||
try {
|
||||
// 카드 이미지 생성 (별도 함수로 구현)
|
||||
const imageUrl = await generateCardImage(card);
|
||||
|
||||
const shareContent = {
|
||||
contentType: 'link',
|
||||
contentUrl: imageUrl,
|
||||
contentDescription: card.content,
|
||||
};
|
||||
|
||||
// 공유 다이얼로그 표시
|
||||
const result = await ShareDialog.show(shareContent);
|
||||
|
||||
if (result.isCancelled) {
|
||||
throw new Error('User cancelled sharing');
|
||||
}
|
||||
|
||||
// 공유 기록 저장
|
||||
const { data: share, error: shareError } = await supabase
|
||||
.from('zellyy.social_shares')
|
||||
.insert({
|
||||
card_id: card.id,
|
||||
user_id: supabase.auth.user().id,
|
||||
platform: 'facebook',
|
||||
status: 'success',
|
||||
response_data: result,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (shareError) {
|
||||
console.error('Error recording share:', shareError);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error sharing to Facebook:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 7.3 Twitter 공유
|
||||
|
||||
```javascript
|
||||
import { TwitterSignIn } from '@react-native-twitter-signin/twitter-signin';
|
||||
import supabase from '../services/supabase';
|
||||
|
||||
const shareToTwitter = async (card) => {
|
||||
try {
|
||||
// 소셜 계정 정보 가져오기
|
||||
const { data: socialAccount, error: socialError } = await supabase
|
||||
.from('zellyy.social_accounts')
|
||||
.select('*')
|
||||
.eq('user_id', supabase.auth.user().id)
|
||||
.eq('platform', 'twitter')
|
||||
.single();
|
||||
|
||||
if (socialError || !socialAccount) {
|
||||
throw new Error('Twitter account not connected');
|
||||
}
|
||||
|
||||
// 카드 이미지 생성 (별도 함수로 구현)
|
||||
const imageUrl = await generateCardImage(card);
|
||||
|
||||
// Edge Function 호출하여 트윗 게시
|
||||
const response = await fetch('https://a11.ism.kr/functions/v1/social-share', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${supabase.auth.session().access_token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cardId: card.id,
|
||||
platform: 'twitter',
|
||||
message: card.content,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to share to Twitter');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error sharing to Twitter:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 8. 소셜 계정 관리
|
||||
|
||||
### 8.1 연결된 계정 목록 조회
|
||||
|
||||
```javascript
|
||||
import { useEffect, useState } from 'react';
|
||||
import supabase from '../services/supabase';
|
||||
|
||||
const SocialAccountsScreen = () => {
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts();
|
||||
}, []);
|
||||
|
||||
const fetchAccounts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('zellyy.social_accounts')
|
||||
.select('*')
|
||||
.eq('user_id', supabase.auth.user().id);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
setAccounts(data || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching social accounts:', error);
|
||||
alert('Failed to load social accounts');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 렌더링 코드
|
||||
};
|
||||
```
|
||||
|
||||
### 8.2 계정 연결 해제
|
||||
|
||||
```javascript
|
||||
const disconnectAccount = async (platform) => {
|
||||
try {
|
||||
// 소셜 계정 삭제
|
||||
const { error } = await supabase
|
||||
.from('zellyy.social_accounts')
|
||||
.delete()
|
||||
.match({
|
||||
user_id: supabase.auth.user().id,
|
||||
platform,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 플랫폼별 로그아웃 처리
|
||||
if (platform === 'facebook') {
|
||||
LoginManager.logOut();
|
||||
} else if (platform === 'twitter') {
|
||||
TwitterSignIn.logOut();
|
||||
}
|
||||
|
||||
// 계정 목록 새로고침
|
||||
fetchAccounts();
|
||||
|
||||
alert(`${platform} account disconnected`);
|
||||
} catch (error) {
|
||||
console.error(`Error disconnecting ${platform} account:`, error);
|
||||
alert(`Failed to disconnect ${platform} account`);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 9. 공유 기록 관리
|
||||
|
||||
### 9.1 공유 기록 조회
|
||||
|
||||
```javascript
|
||||
import { useEffect, useState } from 'react';
|
||||
import supabase from '../services/supabase';
|
||||
|
||||
const ShareHistoryScreen = () => {
|
||||
const [shares, setShares] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchShareHistory();
|
||||
}, []);
|
||||
|
||||
const fetchShareHistory = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('zellyy.social_shares')
|
||||
.select(`
|
||||
id,
|
||||
platform,
|
||||
share_url,
|
||||
shared_at,
|
||||
status,
|
||||
cards:card_id (
|
||||
id,
|
||||
content,
|
||||
background_color,
|
||||
text_color
|
||||
)
|
||||
`)
|
||||
.eq('user_id', supabase.auth.user().id)
|
||||
.order('shared_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
setShares(data || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching share history:', error);
|
||||
alert('Failed to load share history');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 렌더링 코드
|
||||
};
|
||||
```
|
||||
|
||||
## 10. 카드 이미지 생성
|
||||
|
||||
카드를 이미지로 변환하여 소셜 미디어에 공유하기 위한 함수입니다.
|
||||
|
||||
```javascript
|
||||
import ViewShot from 'react-native-view-shot';
|
||||
import { useRef } from 'react';
|
||||
import supabase from '../services/supabase';
|
||||
|
||||
// 카드 컴포넌트
|
||||
const Card = ({ card, viewShotRef }) => {
|
||||
return (
|
||||
<ViewShot ref={viewShotRef} options={{ format: 'jpg', quality: 0.9 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 300,
|
||||
height: 300,
|
||||
backgroundColor: card.background_color,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: card.text_color,
|
||||
fontFamily: card.font_family,
|
||||
fontSize: card.font_size,
|
||||
textAlign: card.text_align,
|
||||
}}
|
||||
>
|
||||
{card.content}
|
||||
</Text>
|
||||
</View>
|
||||
</ViewShot>
|
||||
);
|
||||
};
|
||||
|
||||
// 이미지 생성 및 업로드 함수
|
||||
const generateCardImage = async (card) => {
|
||||
try {
|
||||
// ViewShot 참조
|
||||
const viewShotRef = useRef();
|
||||
|
||||
// 카드 이미지 캡처
|
||||
const uri = await viewShotRef.current.capture();
|
||||
|
||||
// 파일 이름 생성
|
||||
const fileName = `${supabase.auth.user().id}/${card.id}_${Date.now()}.jpg`;
|
||||
|
||||
// 이미지 파일 생성
|
||||
const formData = new FormData();
|
||||
formData.append('file', {
|
||||
uri,
|
||||
name: fileName,
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
|
||||
// Supabase Storage에 업로드
|
||||
const { data, error } = await supabase
|
||||
.storage
|
||||
.from('card-images')
|
||||
.upload(fileName, formData);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 공개 URL 생성
|
||||
const { publicURL, error: urlError } = supabase
|
||||
.storage
|
||||
.from('card-images')
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
if (urlError) {
|
||||
throw urlError;
|
||||
}
|
||||
|
||||
return publicURL;
|
||||
} catch (error) {
|
||||
console.error('Error generating card image:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 11. 문제 해결
|
||||
|
||||
### 11.1 일반적인 문제 및 해결 방법
|
||||
|
||||
1. **소셜 로그인 실패**:
|
||||
- 앱 ID와 시크릿이 올바른지 확인
|
||||
- 리디렉션 URL이 올바르게 설정되었는지 확인
|
||||
- 앱 권한이 올바르게 설정되었는지 확인
|
||||
|
||||
2. **공유 기능 실패**:
|
||||
- 소셜 미디어 앱이 기기에 설치되어 있는지 확인
|
||||
- 액세스 토큰이 유효한지 확인
|
||||
- 필요한 권한이 부여되었는지 확인
|
||||
|
||||
3. **이미지 업로드 실패**:
|
||||
- 스토리지 버킷 권한 설정 확인
|
||||
- 파일 크기 및 형식 확인
|
||||
|
||||
### 11.2 플랫폼별 문제 해결
|
||||
|
||||
#### Facebook
|
||||
- 앱 검토 상태 확인
|
||||
- 개발 모드에서는 테스트 사용자만 앱에 접근 가능
|
||||
|
||||
#### Twitter
|
||||
- API 키와 시크릿이 올바른지 확인
|
||||
- 개발자 계정 상태 확인
|
||||
|
||||
#### Instagram
|
||||
- Facebook 개발자 계정과 연결되어 있는지 확인
|
||||
- Instagram Graph API 권한 확인
|
||||
|
||||
## 12. 보안 고려사항
|
||||
|
||||
1. **토큰 보안**:
|
||||
- 액세스 토큰과 리프레시 토큰은 안전하게 저장
|
||||
- 서버 측에서만 API 시크릿 사용
|
||||
|
||||
2. **사용자 데이터 보호**:
|
||||
- 필요한 최소한의 권한만 요청
|
||||
- 사용자 동의 없이 데이터를 공유하지 않음
|
||||
|
||||
3. **API 키 관리**:
|
||||
- 환경 변수로 관리하고 소스 코드에 하드코딩하지 않음
|
||||
- 프로덕션과 개발 환경에 별도의 API 키 사용
|
||||
|
||||
## 결론
|
||||
|
||||
이 가이드는 Zellyy 앱에서 다양한 소셜 미디어 플랫폼과의 통합 방법을 제공합니다. 각 플랫폼의 API는 지속적으로 변경될 수 있으므로, 최신 문서를 참조하여 구현을 업데이트하는 것이 중요합니다. 소셜 미디어 통합은 사용자 경험을 향상시키고 앱의 확산을 촉진하는 중요한 기능입니다.
|
||||
750
ZELLYY/zellyy note/02_기술_문서/Supabase_연동_가이드.md
Normal file
750
ZELLYY/zellyy note/02_기술_문서/Supabase_연동_가이드.md
Normal file
@@ -0,0 +1,750 @@
|
||||
# Supabase 연동 가이드
|
||||
|
||||
이 문서는 Zellyy 프로젝트에서 Supabase를 연동하고 설정하는 방법을 안내합니다.
|
||||
|
||||
## 개요
|
||||
|
||||
Zellyy 프로젝트는 백엔드로 자체 호스팅된 Supabase를 사용합니다. 이 가이드는 기존에 설치된 Supabase 인스턴스에 Zellyy 프로젝트를 위한 스키마와 테이블을 설정하는 방법을 설명합니다.
|
||||
|
||||
## 사전 요구사항
|
||||
|
||||
- 설치된 Supabase 인스턴스 (참조: [[Nginx Supabase 설치 가이드]])
|
||||
- PostgreSQL 기본 지식
|
||||
- Supabase API 키 및 URL
|
||||
|
||||
## 1. Supabase 프로젝트 설정
|
||||
|
||||
### 1.1 스키마 생성
|
||||
|
||||
Zellyy 프로젝트를 위한 별도의 스키마를 생성합니다. 이는 기존 프로젝트와의 데이터 분리를 위한 것입니다.
|
||||
|
||||
```sql
|
||||
-- Supabase SQL 편집기에서 실행
|
||||
CREATE SCHEMA IF NOT EXISTS zellyy;
|
||||
```
|
||||
|
||||
### 1.2 확장 활성화
|
||||
|
||||
필요한 PostgreSQL 확장을 활성화합니다.
|
||||
|
||||
```sql
|
||||
-- UUID 생성을 위한 확장
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- 전문 검색을 위한 확장
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
```
|
||||
|
||||
## 2. 데이터베이스 테이블 생성
|
||||
|
||||
### 2.1 사용자 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.users (
|
||||
id UUID REFERENCES auth.users(id) PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
username TEXT UNIQUE,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
last_login TIMESTAMP WITH TIME ZONE,
|
||||
is_premium BOOLEAN DEFAULT FALSE,
|
||||
premium_until TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- 트리거 설정
|
||||
CREATE OR REPLACE FUNCTION zellyy.update_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_users_timestamp
|
||||
BEFORE UPDATE ON zellyy.users
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_timestamp();
|
||||
```
|
||||
|
||||
### 2.2 카드 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.cards (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
background_color TEXT DEFAULT '#FFFFFF',
|
||||
text_color TEXT DEFAULT '#000000',
|
||||
font_family TEXT DEFAULT 'system',
|
||||
font_size INTEGER DEFAULT 16,
|
||||
text_align TEXT DEFAULT 'center',
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
is_synced BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
deleted_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_cards_timestamp
|
||||
BEFORE UPDATE ON zellyy.cards
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_timestamp();
|
||||
```
|
||||
|
||||
### 2.3 카드 태그 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.card_tags (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
card_id UUID REFERENCES zellyy.cards(id) NOT NULL,
|
||||
tag_name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(card_id, tag_name)
|
||||
);
|
||||
```
|
||||
|
||||
### 2.4 소셜 공유 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.social_shares (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
card_id UUID REFERENCES zellyy.cards(id) NOT NULL,
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
share_url TEXT,
|
||||
shared_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status TEXT DEFAULT 'pending',
|
||||
response_data JSONB
|
||||
);
|
||||
```
|
||||
|
||||
### 2.5 소셜 계정 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.social_accounts (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
platform_user_id TEXT,
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_expires_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(user_id, platform)
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_social_accounts_timestamp
|
||||
BEFORE UPDATE ON zellyy.social_accounts
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_timestamp();
|
||||
```
|
||||
|
||||
### 2.6 구독 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
plan_type TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
start_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
payment_provider TEXT,
|
||||
payment_id TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_subscriptions_timestamp
|
||||
BEFORE UPDATE ON zellyy.subscriptions
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_timestamp();
|
||||
|
||||
-- 구독 상태에 따라 사용자의 프리미엄 상태 업데이트
|
||||
CREATE OR REPLACE FUNCTION zellyy.update_premium_status()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status = 'active' THEN
|
||||
UPDATE zellyy.users
|
||||
SET is_premium = TRUE, premium_until = NEW.end_date
|
||||
WHERE id = NEW.user_id;
|
||||
ELSIF NEW.status IN ('canceled', 'expired') AND NEW.end_date <= NOW() THEN
|
||||
UPDATE zellyy.users
|
||||
SET is_premium = FALSE, premium_until = NULL
|
||||
WHERE id = NEW.user_id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_premium_status_trigger
|
||||
AFTER INSERT OR UPDATE ON zellyy.subscriptions
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_premium_status();
|
||||
```
|
||||
|
||||
## 3. 인덱스 생성
|
||||
|
||||
성능 최적화를 위한 인덱스를 생성합니다.
|
||||
|
||||
```sql
|
||||
-- 카드 검색 최적화
|
||||
CREATE INDEX idx_cards_user_id ON zellyy.cards(user_id);
|
||||
CREATE INDEX idx_cards_created_at ON zellyy.cards(created_at);
|
||||
CREATE INDEX idx_cards_is_public ON zellyy.cards(is_public);
|
||||
|
||||
-- 태그 검색 최적화
|
||||
CREATE INDEX idx_card_tags_card_id ON zellyy.card_tags(card_id);
|
||||
CREATE INDEX idx_card_tags_tag_name ON zellyy.card_tags(tag_name);
|
||||
|
||||
-- 소셜 공유 검색 최적화
|
||||
CREATE INDEX idx_social_shares_user_id ON zellyy.social_shares(user_id);
|
||||
CREATE INDEX idx_social_shares_card_id ON zellyy.social_shares(card_id);
|
||||
```
|
||||
|
||||
## 4. Row Level Security (RLS) 설정
|
||||
|
||||
Supabase의 Row Level Security를 사용하여 데이터 접근을 제한합니다.
|
||||
|
||||
### 4.1 사용자 테이블 정책
|
||||
|
||||
```sql
|
||||
-- RLS 활성화
|
||||
ALTER TABLE zellyy.users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 사용자는 자신의 정보만 읽을 수 있음
|
||||
CREATE POLICY "사용자는 자신의 정보만 읽을 수 있음" ON zellyy.users
|
||||
FOR SELECT USING (auth.uid() = id);
|
||||
|
||||
-- 사용자는 자신의 정보만 업데이트할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 정보만 업데이트할 수 있음" ON zellyy.users
|
||||
FOR UPDATE USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
### 4.2 카드 테이블 정책
|
||||
|
||||
```sql
|
||||
-- RLS 활성화
|
||||
ALTER TABLE zellyy.cards ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 사용자는 자신의 카드만 읽을 수 있음 (또는 공개 카드)
|
||||
CREATE POLICY "사용자는 자신의 카드만 읽을 수 있음" ON zellyy.cards
|
||||
FOR SELECT USING (auth.uid() = user_id OR is_public = TRUE);
|
||||
|
||||
-- 사용자는 자신의 카드만 생성할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 카드만 생성할 수 있음" ON zellyy.cards
|
||||
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- 사용자는 자신의 카드만 업데이트할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 카드만 업데이트할 수 있음" ON zellyy.cards
|
||||
FOR UPDATE USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자는 자신의 카드만 삭제할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 카드만 삭제할 수 있음" ON zellyy.cards
|
||||
FOR DELETE USING (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
### 4.3 기타 테이블 정책
|
||||
|
||||
다른 테이블에도 유사한 방식으로 RLS 정책을 적용합니다.
|
||||
|
||||
```sql
|
||||
-- 카드 태그 테이블
|
||||
ALTER TABLE zellyy.card_tags ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "카드 태그 읽기 정책" ON zellyy.card_tags
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM zellyy.cards
|
||||
WHERE cards.id = card_tags.card_id
|
||||
AND (cards.user_id = auth.uid() OR cards.is_public = TRUE)
|
||||
)
|
||||
);
|
||||
|
||||
-- 소셜 공유 테이블
|
||||
ALTER TABLE zellyy.social_shares ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "소셜 공유 읽기 정책" ON zellyy.social_shares
|
||||
FOR SELECT USING (user_id = auth.uid());
|
||||
|
||||
-- 소셜 계정 테이블
|
||||
ALTER TABLE zellyy.social_accounts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "소셜 계정 읽기 정책" ON zellyy.social_accounts
|
||||
FOR SELECT USING (user_id = auth.uid());
|
||||
|
||||
-- 구독 테이블
|
||||
ALTER TABLE zellyy.subscriptions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "구독 읽기 정책" ON zellyy.subscriptions
|
||||
FOR SELECT USING (user_id = auth.uid());
|
||||
```
|
||||
|
||||
## 5. 저장 프로시저 및 함수
|
||||
|
||||
자주 사용되는 작업을 위한 저장 프로시저와 함수를 생성합니다.
|
||||
|
||||
### 5.1 카드 생성 함수
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION zellyy.create_card(
|
||||
content TEXT,
|
||||
background_color TEXT DEFAULT '#FFFFFF',
|
||||
text_color TEXT DEFAULT '#000000',
|
||||
font_family TEXT DEFAULT 'system',
|
||||
font_size INTEGER DEFAULT 16,
|
||||
text_align TEXT DEFAULT 'center',
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
tags TEXT[] DEFAULT '{}'
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
card_id UUID;
|
||||
tag TEXT;
|
||||
BEGIN
|
||||
-- 카드 생성
|
||||
INSERT INTO zellyy.cards (
|
||||
user_id, content, background_color, text_color,
|
||||
font_family, font_size, text_align, is_public
|
||||
) VALUES (
|
||||
auth.uid(), content, background_color, text_color,
|
||||
font_family, font_size, text_align, is_public
|
||||
) RETURNING id INTO card_id;
|
||||
|
||||
-- 태그 추가
|
||||
FOREACH tag IN ARRAY tags
|
||||
LOOP
|
||||
INSERT INTO zellyy.card_tags (card_id, tag_name)
|
||||
VALUES (card_id, tag);
|
||||
END LOOP;
|
||||
|
||||
RETURN card_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
```
|
||||
|
||||
### 5.2 카드 검색 함수
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION zellyy.search_cards(
|
||||
search_query TEXT DEFAULT NULL,
|
||||
tag_filter TEXT DEFAULT NULL,
|
||||
page_number INTEGER DEFAULT 1,
|
||||
items_per_page INTEGER DEFAULT 20
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
content TEXT,
|
||||
background_color TEXT,
|
||||
text_color TEXT,
|
||||
font_family TEXT,
|
||||
font_size INTEGER,
|
||||
text_align TEXT,
|
||||
is_public BOOLEAN,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
tags TEXT[]
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH filtered_cards AS (
|
||||
SELECT c.id, c.content, c.background_color, c.text_color,
|
||||
c.font_family, c.font_size, c.text_align, c.is_public,
|
||||
c.created_at, c.updated_at
|
||||
FROM zellyy.cards c
|
||||
WHERE (c.user_id = auth.uid() OR c.is_public = TRUE)
|
||||
AND (search_query IS NULL OR c.content ILIKE '%' || search_query || '%')
|
||||
AND c.deleted_at IS NULL
|
||||
AND (
|
||||
tag_filter IS NULL
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM zellyy.card_tags ct
|
||||
WHERE ct.card_id = c.id AND ct.tag_name = tag_filter
|
||||
)
|
||||
)
|
||||
),
|
||||
cards_with_tags AS (
|
||||
SELECT fc.*, array_agg(ct.tag_name) as tags
|
||||
FROM filtered_cards fc
|
||||
LEFT JOIN zellyy.card_tags ct ON fc.id = ct.card_id
|
||||
GROUP BY fc.id, fc.content, fc.background_color, fc.text_color,
|
||||
fc.font_family, fc.font_size, fc.text_align, fc.is_public,
|
||||
fc.created_at, fc.updated_at
|
||||
)
|
||||
SELECT * FROM cards_with_tags
|
||||
ORDER BY created_at DESC
|
||||
LIMIT items_per_page
|
||||
OFFSET (page_number - 1) * items_per_page;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
```
|
||||
|
||||
## 6. Supabase 클라이언트 설정
|
||||
|
||||
### 6.1 React Native 앱에서 Supabase 클라이언트 설정
|
||||
|
||||
`services/supabase.js` 파일을 생성하고 다음 코드를 추가합니다:
|
||||
|
||||
```javascript
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { SUPABASE_URL, SUPABASE_ANON_KEY } from '@env';
|
||||
|
||||
const supabaseUrl = SUPABASE_URL;
|
||||
const supabaseAnonKey = SUPABASE_ANON_KEY;
|
||||
|
||||
// AsyncStorage 어댑터 생성
|
||||
const AsyncStorageAdapter = {
|
||||
getItem: (key) => AsyncStorage.getItem(key),
|
||||
setItem: (key, value) => AsyncStorage.setItem(key, value),
|
||||
removeItem: (key) => AsyncStorage.removeItem(key),
|
||||
};
|
||||
|
||||
// Supabase 클라이언트 생성
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
storage: AsyncStorageAdapter,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default supabase;
|
||||
```
|
||||
|
||||
### 6.2 환경 변수 설정
|
||||
|
||||
`.env` 파일에 Supabase URL과 익명 키를 추가합니다:
|
||||
|
||||
```
|
||||
SUPABASE_URL=https://a11.ism.kr
|
||||
SUPABASE_ANON_KEY=your_anon_key
|
||||
```
|
||||
|
||||
## 7. 인증 설정
|
||||
|
||||
### 7.1 이메일/비밀번호 인증 활성화
|
||||
|
||||
Supabase 대시보드에서:
|
||||
|
||||
1. Authentication > Settings로 이동
|
||||
2. Email Auth를 활성화
|
||||
3. 필요한 경우 이메일 템플릿 커스터마이징
|
||||
|
||||
### 7.2 소셜 로그인 설정 (선택 사항)
|
||||
|
||||
소셜 로그인을 활성화하려면:
|
||||
|
||||
1. Authentication > Settings > OAuth Providers로 이동
|
||||
2. 원하는 소셜 로그인 제공자 활성화 (예: Google, Facebook)
|
||||
3. 각 제공자의 클라이언트 ID와 시크릿 설정
|
||||
|
||||
## 8. 스토리지 설정
|
||||
|
||||
### 8.1 버킷 생성
|
||||
|
||||
Supabase 대시보드에서:
|
||||
|
||||
1. Storage로 이동
|
||||
2. "New Bucket" 클릭
|
||||
3. 다음 버킷 생성:
|
||||
- `avatars`: 사용자 프로필 이미지용
|
||||
- `card-images`: 카드 이미지용
|
||||
|
||||
### 8.2 스토리지 정책 설정
|
||||
|
||||
각 버킷에 대한 접근 정책을 설정합니다:
|
||||
|
||||
```sql
|
||||
-- avatars 버킷 정책
|
||||
CREATE POLICY "사용자는 자신의 아바타만 업로드할 수 있음"
|
||||
ON storage.objects FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'avatars' AND
|
||||
auth.uid() = (storage.foldername(name)::uuid)
|
||||
);
|
||||
|
||||
CREATE POLICY "사용자는 자신의 아바타만 업데이트할 수 있음"
|
||||
ON storage.objects FOR UPDATE
|
||||
USING (
|
||||
bucket_id = 'avatars' AND
|
||||
auth.uid() = (storage.foldername(name)::uuid)
|
||||
);
|
||||
|
||||
CREATE POLICY "아바타는 공개적으로 접근 가능"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
-- card-images 버킷 정책
|
||||
CREATE POLICY "사용자는 자신의 카드 이미지만 업로드할 수 있음"
|
||||
ON storage.objects FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'card-images' AND
|
||||
auth.uid() = (storage.foldername(name)::uuid)
|
||||
);
|
||||
|
||||
CREATE POLICY "사용자는 자신의 카드 이미지만 업데이트할 수 있음"
|
||||
ON storage.objects FOR UPDATE
|
||||
USING (
|
||||
bucket_id = 'card-images' AND
|
||||
auth.uid() = (storage.foldername(name)::uuid)
|
||||
);
|
||||
|
||||
CREATE POLICY "카드 이미지는 공개적으로 접근 가능"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'card-images');
|
||||
```
|
||||
|
||||
## 9. Edge Functions 설정 (선택 사항)
|
||||
|
||||
소셜 미디어 공유와 같은 복잡한 기능을 위해 Supabase Edge Functions를 사용할 수 있습니다.
|
||||
|
||||
### 9.1 Edge Function 생성
|
||||
|
||||
```bash
|
||||
# Supabase CLI 설치
|
||||
npm install -g supabase
|
||||
|
||||
# 로그인
|
||||
supabase login
|
||||
|
||||
# 프로젝트 연결
|
||||
supabase link --project-ref your-project-ref
|
||||
|
||||
# 소셜 공유 함수 생성
|
||||
supabase functions new social-share
|
||||
|
||||
# 함수 배포
|
||||
supabase functions deploy social-share
|
||||
```
|
||||
|
||||
### 9.2 소셜 공유 함수 예시
|
||||
|
||||
`supabase/functions/social-share/index.ts` 파일:
|
||||
|
||||
```typescript
|
||||
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0';
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '';
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '';
|
||||
|
||||
serve(async (req) => {
|
||||
// CORS 헤더
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 요청 본문 파싱
|
||||
const { cardId, platform, message } = await req.json();
|
||||
|
||||
// Supabase 클라이언트 생성
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// 인증 확인
|
||||
const authHeader = req.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser(token);
|
||||
|
||||
if (authError || !user) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 카드 정보 가져오기
|
||||
const { data: card, error: cardError } = await supabase
|
||||
.from('zellyy.cards')
|
||||
.select('*')
|
||||
.eq('id', cardId)
|
||||
.single();
|
||||
|
||||
if (cardError || !card) {
|
||||
return new Response(JSON.stringify({ error: 'Card not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 소셜 계정 정보 가져오기
|
||||
const { data: socialAccount, error: socialError } = await supabase
|
||||
.from('zellyy.social_accounts')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.eq('platform', platform)
|
||||
.single();
|
||||
|
||||
if (socialError || !socialAccount) {
|
||||
return new Response(JSON.stringify({ error: 'Social account not connected' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 소셜 미디어 공유 로직 (플랫폼별로 다름)
|
||||
let shareResult;
|
||||
let shareUrl;
|
||||
|
||||
if (platform === 'facebook') {
|
||||
// Facebook API 호출 로직
|
||||
// ...
|
||||
shareUrl = 'https://facebook.com/post/123456';
|
||||
} else if (platform === 'instagram') {
|
||||
// Instagram API 호출 로직
|
||||
// ...
|
||||
shareUrl = 'https://instagram.com/p/123456';
|
||||
} else if (platform === 'twitter') {
|
||||
// Twitter API 호출 로직
|
||||
// ...
|
||||
shareUrl = 'https://twitter.com/user/status/123456';
|
||||
} else {
|
||||
return new Response(JSON.stringify({ error: 'Unsupported platform' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 공유 기록 저장
|
||||
const { data: share, error: shareError } = await supabase
|
||||
.from('zellyy.social_shares')
|
||||
.insert({
|
||||
card_id: cardId,
|
||||
user_id: user.id,
|
||||
platform,
|
||||
share_url: shareUrl,
|
||||
status: 'success',
|
||||
response_data: shareResult
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (shareError) {
|
||||
return new Response(JSON.stringify({ error: 'Failed to record share' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success: true, share }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 10. 백업 및 유지 관리
|
||||
|
||||
### 10.1 정기 백업 설정
|
||||
|
||||
기존 백업 스크립트를 활용하여 Zellyy 스키마도 백업합니다:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/home/ism-admin/backups"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
BACKUP_FILE="$BACKUP_DIR/postgres_backup_$TIMESTAMP.sql.gz"
|
||||
|
||||
# 백업 디렉토리 생성
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# 데이터베이스 백업
|
||||
docker exec supabase-db pg_dumpall -U postgres | gzip > $BACKUP_FILE
|
||||
|
||||
# 30일 이상 된 백업 삭제
|
||||
find $BACKUP_DIR -name "postgres_backup_*.sql.gz" -type f -mtime +30 -delete
|
||||
```
|
||||
|
||||
### 10.2 성능 모니터링
|
||||
|
||||
PostgreSQL 성능을 모니터링하기 위한 쿼리:
|
||||
|
||||
```sql
|
||||
-- 느린 쿼리 확인
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
total_time,
|
||||
mean_time,
|
||||
rows
|
||||
FROM pg_stat_statements
|
||||
ORDER BY total_time DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- 테이블 크기 확인
|
||||
SELECT
|
||||
schemaname,
|
||||
relname,
|
||||
pg_size_pretty(pg_total_relation_size(relid)) as total_size
|
||||
FROM pg_catalog.pg_statio_user_tables
|
||||
WHERE schemaname = 'zellyy'
|
||||
ORDER BY pg_total_relation_size(relid) DESC;
|
||||
```
|
||||
|
||||
## 11. 문제 해결
|
||||
|
||||
### 11.1 일반적인 문제 및 해결 방법
|
||||
|
||||
1. **RLS 정책 문제**:
|
||||
- 정책이 너무 제한적인지 확인
|
||||
- `auth.uid()`가 올바르게 작동하는지 확인
|
||||
|
||||
2. **성능 문제**:
|
||||
- 인덱스가 올바르게 생성되었는지 확인
|
||||
- 쿼리 실행 계획 분석
|
||||
|
||||
3. **인증 문제**:
|
||||
- JWT 토큰이 올바른지 확인
|
||||
- 세션 만료 시간 확인
|
||||
|
||||
### 11.2 로그 확인
|
||||
|
||||
```bash
|
||||
# Supabase 로그 확인
|
||||
docker logs supabase-db
|
||||
docker logs supabase-auth
|
||||
docker logs supabase-rest
|
||||
```
|
||||
|
||||
## 12. 보안 고려사항
|
||||
|
||||
1. **API 키 관리**:
|
||||
- 익명 키와 서비스 키를 안전하게 보관
|
||||
- 서비스 키는 서버 측 코드에서만 사용
|
||||
|
||||
2. **데이터 암호화**:
|
||||
- 민감한 정보는 암호화하여 저장
|
||||
- 소셜 미디어 토큰과 같은 민감한 정보는 암호화
|
||||
|
||||
3. **정기적인 보안 감사**:
|
||||
- RLS 정책 검토
|
||||
- 권한 설정 검토
|
||||
|
||||
## 결론
|
||||
|
||||
이 가이드는 Zellyy 프로젝트를 위한 Supabase 설정 방법을 제공합니다. 기존 Supabase 인프라를 활용하면서도 데이터를 분리하여 관리함으로써 효율적인 개발과 운영이 가능합니다. 추가적인 기능이나 요구사항이 생기면 이 문서를 업데이트하여 최신 상태를 유지하세요.
|
||||
26
ZELLYY/zellyy note/02_기술_문서/index.md
Normal file
26
ZELLYY/zellyy note/02_기술_문서/index.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 기술 문서
|
||||
|
||||
이 폴더에는 Zellyy 프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다.
|
||||
|
||||
## 문서 목록
|
||||
|
||||
1. [[데이터 모델 설계]] - 데이터 모델 설계 문서
|
||||
2. [[ERD 다이어그램]] - 엔티티 관계 다이어그램
|
||||
3. [[API 명세서]] - API 엔드포인트 및 기능 명세
|
||||
4. [[React Native 설정 가이드]] - React Native 개발 환경 설정 가이드
|
||||
5. [[Supabase 연동 가이드]] - Supabase 연동 및 설정 방법
|
||||
6. [[SNS 통합 가이드]] - 소셜 미디어 플랫폼 연동 방법
|
||||
|
||||
## 기술 스택 요약
|
||||
|
||||
Zellyy 프로젝트는 다음과 같은 기술 스택을 사용합니다:
|
||||
|
||||
- **모바일 앱 프론트엔드**: React Native (iOS, Android)
|
||||
- **웹 프론트엔드**: React.js 또는 Next.js (2단계)
|
||||
- **백엔드**: Supabase (자체 호스팅)
|
||||
- **데이터베이스**: PostgreSQL (Supabase 내장)
|
||||
- **인증**: Supabase Auth
|
||||
- **스토리지**: Supabase Storage
|
||||
- **배포 환경**: Debian 12 서버 (기존 인프라 활용)
|
||||
|
||||
이 문서들은 프로젝트의 기술적 구현에 필요한 상세 정보를 제공하며, 개발자가 시스템을 이해하고 구현하는 데 도움을 줍니다.
|
||||
265
ZELLYY/zellyy note/02_기술_문서/데이터_모델_설계.md
Normal file
265
ZELLYY/zellyy note/02_기술_문서/데이터_모델_설계.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Zellyy 데이터 모델 설계
|
||||
|
||||
이 문서는 Zellyy 프로젝트의 데이터베이스 모델 설계에 대한 상세 내용을 제공합니다.
|
||||
|
||||
## 스키마 구조
|
||||
|
||||
Zellyy 프로젝트는 Supabase의 PostgreSQL 데이터베이스를 사용하며, 모든 테이블은 `zellyy` 스키마 아래에 생성됩니다. 이는 기존 프로젝트와의 분리를 위한 것입니다.
|
||||
|
||||
## 핵심 테이블
|
||||
|
||||
### 1. zellyy.users
|
||||
|
||||
사용자 정보를 저장하는 테이블입니다. Supabase Auth와 연동됩니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.users (
|
||||
id UUID REFERENCES auth.users(id) PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
username TEXT UNIQUE,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
last_login TIMESTAMP WITH TIME ZONE,
|
||||
is_premium BOOLEAN DEFAULT FALSE,
|
||||
premium_until TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
```
|
||||
|
||||
### 2. zellyy.cards
|
||||
|
||||
사용자가 작성한 카드 정보를 저장하는 테이블입니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.cards (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
background_color TEXT DEFAULT '#FFFFFF',
|
||||
text_color TEXT DEFAULT '#000000',
|
||||
font_family TEXT DEFAULT 'system',
|
||||
font_size INTEGER DEFAULT 16,
|
||||
text_align TEXT DEFAULT 'center',
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
is_synced BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
deleted_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
```
|
||||
|
||||
### 3. zellyy.card_tags
|
||||
|
||||
카드에 적용된 태그 정보를 저장하는 테이블입니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.card_tags (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
card_id UUID REFERENCES zellyy.cards(id) NOT NULL,
|
||||
tag_name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(card_id, tag_name)
|
||||
);
|
||||
```
|
||||
|
||||
### 4. zellyy.social_shares
|
||||
|
||||
카드의 소셜 미디어 공유 기록을 저장하는 테이블입니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.social_shares (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
card_id UUID REFERENCES zellyy.cards(id) NOT NULL,
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
platform TEXT NOT NULL, -- 'facebook', 'instagram', 'twitter', etc.
|
||||
share_url TEXT,
|
||||
shared_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status TEXT DEFAULT 'pending', -- 'pending', 'success', 'failed'
|
||||
response_data JSONB
|
||||
);
|
||||
```
|
||||
|
||||
### 5. zellyy.social_accounts
|
||||
|
||||
사용자의 소셜 미디어 계정 연동 정보를 저장하는 테이블입니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.social_accounts (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
platform TEXT NOT NULL, -- 'facebook', 'instagram', 'twitter', etc.
|
||||
platform_user_id TEXT,
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_expires_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(user_id, platform)
|
||||
);
|
||||
```
|
||||
|
||||
### 6. zellyy.subscriptions
|
||||
|
||||
사용자의 구독 정보를 저장하는 테이블입니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE zellyy.subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES zellyy.users(id) NOT NULL,
|
||||
plan_type TEXT NOT NULL, -- 'monthly', 'yearly', etc.
|
||||
status TEXT NOT NULL, -- 'active', 'canceled', 'expired'
|
||||
start_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
payment_provider TEXT, -- 'apple', 'google', 'stripe', etc.
|
||||
payment_id TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
## 보안 정책 (RLS)
|
||||
|
||||
Supabase의 Row Level Security(RLS)를 사용하여 데이터 접근을 제한합니다.
|
||||
|
||||
### zellyy.users 테이블 정책
|
||||
|
||||
```sql
|
||||
-- 사용자는 자신의 정보만 읽을 수 있음
|
||||
CREATE POLICY "사용자는 자신의 정보만 읽을 수 있음" ON zellyy.users
|
||||
FOR SELECT USING (auth.uid() = id);
|
||||
|
||||
-- 사용자는 자신의 정보만 업데이트할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 정보만 업데이트할 수 있음" ON zellyy.users
|
||||
FOR UPDATE USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
### zellyy.cards 테이블 정책
|
||||
|
||||
```sql
|
||||
-- 사용자는 자신의 카드만 읽을 수 있음 (또는 공개 카드)
|
||||
CREATE POLICY "사용자는 자신의 카드만 읽을 수 있음" ON zellyy.cards
|
||||
FOR SELECT USING (auth.uid() = user_id OR is_public = TRUE);
|
||||
|
||||
-- 사용자는 자신의 카드만 생성할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 카드만 생성할 수 있음" ON zellyy.cards
|
||||
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- 사용자는 자신의 카드만 업데이트할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 카드만 업데이트할 수 있음" ON zellyy.cards
|
||||
FOR UPDATE USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자는 자신의 카드만 삭제할 수 있음
|
||||
CREATE POLICY "사용자는 자신의 카드만 삭제할 수 있음" ON zellyy.cards
|
||||
FOR DELETE USING (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
## 인덱스
|
||||
|
||||
성능 최적화를 위한 인덱스를 생성합니다.
|
||||
|
||||
```sql
|
||||
-- 카드 검색 최적화
|
||||
CREATE INDEX idx_cards_user_id ON zellyy.cards(user_id);
|
||||
CREATE INDEX idx_cards_created_at ON zellyy.cards(created_at);
|
||||
CREATE INDEX idx_cards_is_public ON zellyy.cards(is_public);
|
||||
|
||||
-- 태그 검색 최적화
|
||||
CREATE INDEX idx_card_tags_card_id ON zellyy.card_tags(card_id);
|
||||
CREATE INDEX idx_card_tags_tag_name ON zellyy.card_tags(tag_name);
|
||||
|
||||
-- 소셜 공유 검색 최적화
|
||||
CREATE INDEX idx_social_shares_user_id ON zellyy.social_shares(user_id);
|
||||
CREATE INDEX idx_social_shares_card_id ON zellyy.social_shares(card_id);
|
||||
```
|
||||
|
||||
## 트리거 및 함수
|
||||
|
||||
자동화된 작업을 위한 트리거와 함수를 정의합니다.
|
||||
|
||||
### 업데이트 타임스탬프 트리거
|
||||
|
||||
```sql
|
||||
-- 업데이트 타임스탬프 함수
|
||||
CREATE OR REPLACE FUNCTION zellyy.update_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 사용자 테이블에 트리거 적용
|
||||
CREATE TRIGGER update_users_timestamp
|
||||
BEFORE UPDATE ON zellyy.users
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_timestamp();
|
||||
|
||||
-- 카드 테이블에 트리거 적용
|
||||
CREATE TRIGGER update_cards_timestamp
|
||||
BEFORE UPDATE ON zellyy.cards
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_timestamp();
|
||||
```
|
||||
|
||||
### 프리미엄 상태 업데이트 함수
|
||||
|
||||
```sql
|
||||
-- 구독 상태에 따라 사용자의 프리미엄 상태 업데이트
|
||||
CREATE OR REPLACE FUNCTION zellyy.update_premium_status()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status = 'active' THEN
|
||||
UPDATE zellyy.users
|
||||
SET is_premium = TRUE, premium_until = NEW.end_date
|
||||
WHERE id = NEW.user_id;
|
||||
ELSIF NEW.status IN ('canceled', 'expired') AND NEW.end_date <= NOW() THEN
|
||||
UPDATE zellyy.users
|
||||
SET is_premium = FALSE, premium_until = NULL
|
||||
WHERE id = NEW.user_id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 구독 테이블에 트리거 적용
|
||||
CREATE TRIGGER update_premium_status_trigger
|
||||
AFTER INSERT OR UPDATE ON zellyy.subscriptions
|
||||
FOR EACH ROW EXECUTE FUNCTION zellyy.update_premium_status();
|
||||
```
|
||||
|
||||
## 데이터 마이그레이션 및 초기 설정
|
||||
|
||||
프로젝트 초기 설정을 위한 SQL 스크립트입니다.
|
||||
|
||||
```sql
|
||||
-- 스키마 생성
|
||||
CREATE SCHEMA IF NOT EXISTS zellyy;
|
||||
|
||||
-- UUID 확장 활성화
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- 테이블 생성 (위에서 정의한 모든 테이블)
|
||||
|
||||
-- 기본 관리자 사용자 생성
|
||||
INSERT INTO zellyy.users (id, email, username, display_name, is_premium)
|
||||
VALUES ('00000000-0000-0000-0000-000000000000', 'admin@zellyy.com', 'admin', 'Zellyy Admin', TRUE);
|
||||
```
|
||||
|
||||
## 확장성 고려사항
|
||||
|
||||
1. **샤딩**: 사용자 수가 크게 증가할 경우, 사용자 ID를 기준으로 데이터를 샤딩하는 전략을 고려할 수 있습니다.
|
||||
|
||||
2. **아카이빙**: 오래된 카드 데이터는 별도의 아카이브 테이블로 이동하여 주 테이블의 성능을 유지할 수 있습니다.
|
||||
|
||||
3. **캐싱**: 자주 접근하는 데이터(예: 공개 카드, 인기 태그)는 Redis와 같은 캐시 시스템을 도입하여 성능을 향상시킬 수 있습니다.
|
||||
|
||||
4. **읽기 복제본**: 쿼리 부하가 높아질 경우, 읽기 전용 복제본을 설정하여 부하를 분산할 수 있습니다.
|
||||
|
||||
## 데이터 백업 전략
|
||||
|
||||
1. **정기 백업**: 매일 전체 데이터베이스 백업을 수행합니다.
|
||||
2. **증분 백업**: 시간별 WAL(Write-Ahead Log) 백업을 통해 세부적인 복구 지점을 제공합니다.
|
||||
3. **지리적 복제**: 재해 복구를 위해 다른 지역에 백업을 저장합니다.
|
||||
|
||||
## 결론
|
||||
|
||||
이 데이터 모델은 Zellyy 프로젝트의 초기 요구사항을 충족하도록 설계되었습니다. 사용자 피드백과 실제 사용 패턴에 따라 모델을 지속적으로 개선하고 확장할 계획입니다.
|
||||
217
ZELLYY/zellyy note/02_기술_문서/시스템_아키텍처.md
Normal file
217
ZELLYY/zellyy note/02_기술_문서/시스템_아키텍처.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# ZELLYY 시스템 아키텍처
|
||||
|
||||
## 아키텍처 개요
|
||||
|
||||
ZELLYY는 모바일 중심의 카드 제작 애플리케이션으로, 클라이언트-서버 아키텍처를 기반으로 설계되었습니다. 초기 MVP 단계에서는 로컬 저장소 중심으로 구현하고, 이후 클라우드 동기화 기능을 추가하는 방식으로 발전할 예정입니다.
|
||||
|
||||
## 시스템 구성 요소
|
||||
|
||||
### 클라이언트 애플리케이션
|
||||
|
||||
#### 모바일 앱 (React Native)
|
||||
- **UI 레이어**: React Native 컴포넌트, 스타일링 (Styled Components)
|
||||
- **상태 관리**: Redux/Redux Toolkit, Context API
|
||||
- **네비게이션**: React Navigation
|
||||
- **로컬 저장소**: AsyncStorage, SQLite
|
||||
- **네트워크**: Axios, React Query
|
||||
- **오프라인 지원**: 로컬 캐싱, 오프라인 작업 큐
|
||||
|
||||
#### 웹 애플리케이션 (향후 개발)
|
||||
- **프레임워크**: Next.js
|
||||
- **UI 라이브러리**: React
|
||||
- **상태 관리**: Redux/Redux Toolkit
|
||||
- **스타일링**: Tailwind CSS, Styled Components
|
||||
- **API 통신**: Axios, React Query
|
||||
|
||||
### 백엔드 서비스
|
||||
|
||||
#### Supabase 기반 백엔드
|
||||
- **데이터베이스**: PostgreSQL
|
||||
- **인증**: Supabase Auth (이메일, 소셜 로그인)
|
||||
- **스토리지**: Supabase Storage (이미지, 에셋 저장)
|
||||
- **실시간 기능**: Supabase Realtime (향후 협업 기능용)
|
||||
- **API**: RESTful API, GraphQL (선택적)
|
||||
|
||||
#### 추가 서버 컴포넌트 (필요시)
|
||||
- **Node.js 서버**: 복잡한 비즈니스 로직, 서드파티 API 통합
|
||||
- **이미지 처리 서비스**: 이미지 최적화, 변환
|
||||
- **푸시 알림 서비스**: Firebase Cloud Messaging 연동
|
||||
|
||||
### 인프라
|
||||
|
||||
- **호스팅**: Supabase 자체 호스팅 (Debian 12 서버)
|
||||
- **CDN**: Cloudflare (정적 자산 배포)
|
||||
- **CI/CD**: GitHub Actions
|
||||
- **모니터링**: Sentry, Datadog
|
||||
- **분석**: Google Analytics, Firebase Analytics
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
### 기본 데이터 흐름
|
||||
1. 사용자가 모바일 앱에서 카드 제작/편집
|
||||
2. 로컬 저장소에 변경사항 저장
|
||||
3. 온라인 상태일 때 Supabase에 동기화
|
||||
4. 다른 기기에서 접속 시 Supabase에서 최신 데이터 가져오기
|
||||
|
||||
### 오프라인 지원
|
||||
1. 오프라인 상태에서 모든 변경사항 로컬에 저장
|
||||
2. 변경사항을 작업 큐에 추가
|
||||
3. 온라인 상태가 되면 작업 큐 처리
|
||||
4. 충돌 발생 시 해결 전략 적용 (최신 타임스탬프 우선, 사용자 선택 등)
|
||||
|
||||
## 주요 API 엔드포인트
|
||||
|
||||
### 사용자 관리
|
||||
- `POST /auth/signup`: 회원가입
|
||||
- `POST /auth/login`: 로그인
|
||||
- `POST /auth/logout`: 로그아웃
|
||||
- `GET /auth/user`: 현재 사용자 정보 조회
|
||||
- `PUT /auth/user`: 사용자 정보 업데이트
|
||||
|
||||
### 카드 관리
|
||||
- `GET /cards`: 사용자의 카드 목록 조회
|
||||
- `GET /cards/:id`: 특정 카드 상세 조회
|
||||
- `POST /cards`: 새 카드 생성
|
||||
- `PUT /cards/:id`: 카드 업데이트
|
||||
- `DELETE /cards/:id`: 카드 삭제
|
||||
|
||||
### 템플릿 관리
|
||||
- `GET /templates`: 템플릿 목록 조회
|
||||
- `GET /templates/:id`: 특정 템플릿 상세 조회
|
||||
- `POST /templates`: 사용자 커스텀 템플릿 생성
|
||||
- `PUT /templates/:id`: 템플릿 업데이트
|
||||
- `DELETE /templates/:id`: 템플릿 삭제
|
||||
|
||||
### 에셋 관리
|
||||
- `GET /assets`: 에셋(이미지, 아이콘 등) 목록 조회
|
||||
- `POST /assets`: 새 에셋 업로드
|
||||
- `DELETE /assets/:id`: 에셋 삭제
|
||||
|
||||
## 보안 아키텍처
|
||||
|
||||
### 인증 및 권한 관리
|
||||
- JWT 기반 인증
|
||||
- 역할 기반 접근 제어 (RBAC)
|
||||
- API 요청 제한 (Rate Limiting)
|
||||
- HTTPS 전용 통신
|
||||
|
||||
### 데이터 보안
|
||||
- 저장 데이터 암호화 (Encryption at Rest)
|
||||
- 전송 데이터 암호화 (Encryption in Transit)
|
||||
- 민감 정보 마스킹
|
||||
- 정기적인 보안 감사
|
||||
|
||||
## 확장성 고려사항
|
||||
|
||||
### 수평적 확장
|
||||
- 마이크로서비스 아키텍처로의 점진적 전환 가능성
|
||||
- 서버리스 함수 활용 (AWS Lambda, Supabase Edge Functions)
|
||||
- 데이터베이스 샤딩 전략
|
||||
|
||||
### 성능 최적화
|
||||
- 이미지 최적화 및 압축
|
||||
- CDN 활용한 정적 자산 배포
|
||||
- 캐싱 전략 (메모리 캐시, 디스크 캐시)
|
||||
- 데이터베이스 인덱싱 및 쿼리 최적화
|
||||
|
||||
## 시스템 아키텍처 다이어그램
|
||||
|
||||
```
|
||||
+------------------+ +------------------+
|
||||
| | | |
|
||||
| 모바일 앱 | | 웹 애플리케이션 |
|
||||
| (React Native) | | (Next.js) |
|
||||
| | | |
|
||||
+--------+---------+ +--------+---------+
|
||||
| |
|
||||
| |
|
||||
v v
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| API Gateway |
|
||||
| |
|
||||
+------------------+---------------------+
|
||||
|
|
||||
|
|
||||
v
|
||||
+------------------+---------------------+
|
||||
| |
|
||||
| Supabase |
|
||||
| |
|
||||
| +-------------+ +-------------+ |
|
||||
| | | | | |
|
||||
| | PostgreSQL | | Storage | |
|
||||
| | | | | |
|
||||
| +-------------+ +-------------+ |
|
||||
| |
|
||||
| +-------------+ +-------------+ |
|
||||
| | | | | |
|
||||
| | Auth | | Realtime | |
|
||||
| | | | | |
|
||||
| +-------------+ +-------------+ |
|
||||
| |
|
||||
+------------------+---------------------+
|
||||
|
|
||||
|
|
||||
v
|
||||
+------------------+---------------------+
|
||||
| |
|
||||
| 추가 서비스 (필요시) |
|
||||
| |
|
||||
| +-------------+ +-------------+ |
|
||||
| | | | | |
|
||||
| | Node.js | | 이미지 처리 | |
|
||||
| | 서버 | | 서비스 | |
|
||||
| +-------------+ +-------------+ |
|
||||
| |
|
||||
| +-------------+ +-------------+ |
|
||||
| | | | | |
|
||||
| | 푸시 알림 | | 분석 서비스 | |
|
||||
| | 서비스 | | | |
|
||||
| +-------------+ +-------------+ |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## 기술 스택 선택 이유
|
||||
|
||||
### React Native
|
||||
- 크로스 플랫폼 개발로 iOS와 Android 동시 지원
|
||||
- 네이티브 성능과 사용자 경험 제공
|
||||
- JavaScript/TypeScript 기반으로 개발 효율성 높음
|
||||
- 풍부한 커뮤니티와 라이브러리 생태계
|
||||
|
||||
### Supabase
|
||||
- Firebase의 오픈소스 대안으로 자체 호스팅 가능
|
||||
- PostgreSQL 기반으로 강력한 쿼리 기능 제공
|
||||
- 인증, 스토리지, 실시간 기능 통합 제공
|
||||
- RESTful API와 GraphQL 지원
|
||||
- 확장성과 커스터마이징 용이
|
||||
|
||||
### Redux/Redux Toolkit
|
||||
- 예측 가능한 상태 관리
|
||||
- 디버깅 및 개발 도구 지원 우수
|
||||
- 미들웨어를 통한 비동기 작업 처리 용이
|
||||
- 대규모 애플리케이션에서의 상태 관리 효율성
|
||||
|
||||
## 향후 아키텍처 발전 계획
|
||||
|
||||
### 1단계: MVP (로컬 중심)
|
||||
- 기본 기능 로컬 저장소 기반 구현
|
||||
- 최소한의 서버 의존성
|
||||
|
||||
### 2단계: 클라우드 동기화
|
||||
- Supabase 연동 및 데이터 동기화
|
||||
- 사용자 인증 및 권한 관리 강화
|
||||
|
||||
### 3단계: 협업 기능
|
||||
- 실시간 데이터 동기화
|
||||
- 팀 워크스페이스 및 공유 기능
|
||||
|
||||
### 4단계: 웹 서비스 확장
|
||||
- Next.js 기반 웹 애플리케이션 개발
|
||||
- 모바일-웹 간 일관된 경험 제공
|
||||
|
||||
### 5단계: AI 기능 통합
|
||||
- 디자인 추천 및 자동화
|
||||
- 콘텐츠 분석 및 최적화
|
||||
Reference in New Issue
Block a user