초기 커밋

This commit is contained in:
hansoo
2025-03-26 18:16:46 +09:00
commit 266674cc0e
67 changed files with 14235 additions and 0 deletions

View 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 프로젝트의 기본 기능을 구현하기 위한 엔드포인트를 정의합니다. 프로젝트가 발전함에 따라 추가 엔드포인트와 기능이 확장될 수 있습니다.

View 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는 새로운 요구사항을 반영하여 업데이트될 수 있습니다.

View 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 환경 설정 및 프로젝트 구조에 대한 기본 정보를 제공합니다. 개발 과정에서 추가적인 질문이나 문제가 있으면 개발팀에 문의하세요.

View 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는 지속적으로 변경될 수 있으므로, 최신 문서를 참조하여 구현을 업데이트하는 것이 중요합니다. 소셜 미디어 통합은 사용자 경험을 향상시키고 앱의 확산을 촉진하는 중요한 기능입니다.

View 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 인프라를 활용하면서도 데이터를 분리하여 관리함으로써 효율적인 개발과 운영이 가능합니다. 추가적인 기능이나 요구사항이 생기면 이 문서를 업데이트하여 최신 상태를 유지하세요.

View 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 서버 (기존 인프라 활용)
이 문서들은 프로젝트의 기술적 구현에 필요한 상세 정보를 제공하며, 개발자가 시스템을 이해하고 구현하는 데 도움을 줍니다.

View 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 프로젝트의 초기 요구사항을 충족하도록 설계되었습니다. 사용자 피드백과 실제 사용 패턴에 따라 모델을 지속적으로 개선하고 확장할 계획입니다.

View 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 기능 통합
- 디자인 추천 및 자동화
- 콘텐츠 분석 및 최적화