초기 커밋

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