원클릭으로
toss-apps
Toss 미니앱 개발 기초 — 프레임워크, TDS 컴포넌트, mTLS 보안, S2S API, Supabase 연동 패턴.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Toss 미니앱 개발 기초 — 프레임워크, TDS 컴포넌트, mTLS 보안, S2S API, Supabase 연동 패턴.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Toss Mock 인증 스킬 — stable userKey 설정, survey 루프 근본 원인 진단, adb 기반 진단 순서를 표준화한 스킬.
Toss IAP 검증 스킬 — Edge Function 404 우회, Service Role 감지, mTLS mock 설정, 토큰 헬퍼 분리, subscriptions 활성화 E2E.
Toss 수익화 운영 스킬 — Ads/IAP/TossPay 콘솔 설정, 샌드박스 시나리오, QA 증적 수집을 표준화.
AIT .ait 번들 빌드 → 콘솔 업로드 → 테스트 진입 성공 패턴. env var 인라인 실패(supabase url is required), babel/esbuild 충돌, __dirname 경로 문제 해결책 포함.
FastAPI + SQLAlchemy + Supabase 모델 작성 규칙 — DB 타입 불일치 방지, async cascade 패턴, Pydantic enum 검증. 2026-05-19 E2E 테스트(Wave 1~9)에서 발견한 BUG-01~05 재발 방지 지침.
TaillogToss 로컬 개발 서버(Metro + FastAPI) 시작/재시작 절차. "서버 켜줘", "서버 재시작", "앱 안됨", "번들 안됨" 등 서버 기동 관련 모든 상황에 사용한다.
| name | toss_apps |
| description | Toss 미니앱 개발 기초 — 프레임워크, TDS 컴포넌트, mTLS 보안, S2S API, Supabase 연동 패턴. |
Toss 미니앱 개발에 필요한 프레임워크, TDS(Toss Design System), 보안, API, Supabase 연동을 다룬다.
화면 와이어프레임은 /toss_wireframes, 사용자 여정은 /toss_journey를 참조.
Apps in Toss run within the Toss application, leveraging its massive traffic.
@apps-in-toss/web-framework. Mandatory TDS WebView for non-game apps.@granite-js/react-native as the runtime baseline (the @apps-in-toss/react-native-framework template can be used when scaffolding), with file-based routing.AppsInToss object)The AppsInToss SDK (likely available globally or via the framework) provides:
window.open, etc.) are generally supported within the WebView context.@toss/tds-react-native → colors.*)Perceptually uniform color space. import { colors } from '@toss/tds-react-native'
| 카테고리 | 토큰 범위 | 용도 |
|---|---|---|
| Grey 50~900 | #f9fafb ~ #191f28 | 중립 배경/텍스트 |
| Blue 50~900 | #e8f3ff ~ #194aa6 | 프라이머리 액션 |
| Red 50~900 | #ffeeee ~ #a51926 | 에러/경고 |
| Orange/Yellow/Green/Teal/Purple | 각 10단계 | 보조 색상 |
| Grey Opacity 50~900 | 반투명 | 오버레이/딤 |
Semantic 토큰 (하드코딩 금지 — 반드시 토큰 사용):
background → #FFFFFF (기본 배경)greyBackground → 중립 서피스layeredBackground → 오버레이 백floatedBackground → 플로팅 요소프로젝트 컬러 매핑 (현재 하드코딩 → 토큰 전환 대상):
| 현재 하드코딩 | TDS 토큰 대체 |
|---|---|
#0064FF | colors.blue500 (Primary CTA) |
#202632 | colors.grey900 (텍스트 기본) |
#333D4B | colors.grey800 (텍스트 보조) |
#8B95A1 | colors.grey500 (라벨/힌트) |
#E5E8EB | colors.grey200 (보더) |
#F4F4F5 | colors.grey100 (디바이더) |
#F8F9FA | colors.grey50 (서브 배경) |
동적 접근성 스케일링을 위해 반드시 토큰 사용. fontSize 직접 입력 금지.
| 토큰 | 크기 | 행간 | 용도 |
|---|---|---|---|
| Typography 1 | 30px | 40px | 대형 헤딩 (survey-result 타이틀) |
| Typography 2 | 26px | 35px | 페이지 헤딩 (Top 컴포넌트) |
| Typography 3 | 22px | 31px | 섹션 헤딩 (ListHeader) |
| Typography 4 | 20px | 29px | 소형 헤딩 (카드 타이틀) |
| Typography 5 | 17px | 25.5px | 본문 기본 (ListRow 텍스트) |
| Typography 6 | 15px | 22.5px | 본문 소형 (보조 설명) |
| Typography 7 | 13px | 19.5px | 캡션/라벨 (Badge, 타임스탬프) |
접근성 스케일링: iOS Large~A11y_xxxLarge (100%310%), Android 연속 스케일.
기저 토큰 F11F42 (42단계) → 위 7개 Typography로 조합. 하드코딩 시 동적 스케일 불가.
| 컴포넌트 | 용도 | 핵심 Props |
|---|---|---|
Button | 기본 CTA 버튼 | size, variant("primary"│"secondary"│"outline"), loading |
TextButton | 텍스트형 버튼 (링크 스타일) | size, color |
IconButton | 아이콘 단독 버튼 | icon, size, accessibilityLabel |
Checkbox | 다중 선택 | checked, onChange, label |
Radio | 단일 선택 | value, selected, onChange |
Switch | 토글 on/off | value, onValueChange |
SegmentedControl | 탭형 세그먼트 선택 | segments[], selectedIndex, onChange |
Dropdown | 드롭다운 선택기 | options[], value, onChange |
TextField | 텍스트 입력 | label, value, placeholder, error, maxLength |
SearchField | 검색 전용 입력 | value, onSearch, placeholder |
NumericSpinner | 숫자 증감 입력 | value, min, max, step |
Stepper | 단계 증감 (±) | value, min, max, onValueChange |
Slider | 범위 슬라이더 | value, min, max, onValueChange |
| 컴포넌트 | 용도 | 핵심 Props / 서브컴포넌트 |
|---|---|---|
Navbar | 상단 네비게이션 바 | .BackButton, .CloseButton, .TextButton, .Title |
Tab | 탭 네비게이션 | tabs[], selectedIndex, onChange |
List | 목록 컨테이너 | children: ListRow[] |
ListRow | 목록 행 | .Texts(title, subtitle), .RightTexts, .Icon, .Image |
ListHeader | 목록 섹션 헤더 | title, description |
ListFooter | 목록 하단 정보 | text |
GridList | 그리드형 목록 | columns, gap, children |
Carousel | 가로 슬라이드 | gap, snapToInterval, children |
BoardRow | 보드형 카드 행 | title, description, image |
TableRow | 테이블 행 (key-value) | label, value |
Top | 화면 최상단 영역 | title, subtitle, children |
BottomCTA | 하단 고정 CTA 버튼 | children: Button |
| 컴포넌트 | 용도 | 핵심 Props |
|---|---|---|
Badge | 상태 뱃지 | variant("info"│"success"│"warning"│"danger"), text |
Toast | 토스트 알림 | message, duration, action |
Dialog | 대화상자 | title, description, primaryButton, secondaryButton |
Loader | 로딩 인디케이터 | size, color |
Skeleton | 로딩 플레이스홀더 | pattern: topList, cardOnly, listOnly |
ProgressBar | 진행률 바 | value(0-1), color |
Result | 결과/완료 화면 | icon, title, description, children |
ErrorPage | 에러 전체 화면 | title, description, retryButton |
BottomInfo | 하단 부가 정보 | text, icon |
Post | 게시글/콘텐츠 뷰 | title, body, author, date |
Rating | 별점 표시/입력 | value, max, onChange |
Keypad | 커스텀 키패드 | variant: Alphabet, Secure, Number |
| 훅 | 용도 | 반환 |
|---|---|---|
useDialog | 프로그래밍 방식 Dialog 표시 | { open(options), close() } |
useToast | 프로그래밍 방식 Toast 표시 | { show(message, options) } |
useBottomSheet | 바텀시트 표시/제어 | { open(content, options), close() } |
useOverlay | 범용 오버레이 관리 | { open(component), close() } |
| 컴포넌트 | 용도 |
|---|---|
Border | 구분선/테두리 데코레이터 |
Shadow | 그림자 효과 래퍼 |
Gradient | 그래디언트 배경 |
Highlight | 강조 텍스트 래퍼 |
Asset | 아이콘/이미지/비디오/Lottie 통합 미디어 |
AmountTop | 금액 강조 상단 영역 |
| 컴포넌트 | 용도 | 핵심 Props |
|---|---|---|
BarChart | 막대 차트 | data[], xKey, yKey, color |
useVisualViewport: 키보드 등에 의한 뷰포트 변화 감지Backend/app/... and migrations at Backend/alembic/..., while Toss bridge functions stay in supabase/functions/....GET /api-partner/v1/apps-in-toss/user/oauth2/login-meBearer {AccessToken}Apps in Toss provides an MCP (Model Context Protocol) server for Cursor and Codex.
llms-full.txt index.$skill-installer using repo toss/apps-in-toss-skills.npm run dev로 로컬 개발 서버 실행adb reverse tcp:8081 tcp:8081 + adb reverse tcp:5173 tcp:5173Bedrock 열기) 기준으로 개발 서버 진입verify-iap-order 실패 복구 케이스 포함.ait 번들 업로드 + QR 테스트로 실환경 플로우 확인intoss-private:// 스킴 기반 사설 번들 테스트 활용| # | 원칙 | ❌ 안 됨 | ✅ 올바름 |
|---|---|---|---|
| 1 | 해요체 통일 | "입력하세요", "~합니다" | "입력해주세요", "~해요" |
| 2 | 능동적 말하기 | "저장됐어요", "등록되었어요" | "저장했어요", "등록했어요" |
| 3 | 긍정적 말하기 | "기록이 없어요" | "첫 기록을 남겨보세요" |
| 4 | 캐주얼 경어 | "확인하시겠어요?", "계시다" | "확인할까요?", "있다" |
| 5 | 명사+명사 피하기 | "결제 정보 입력 완료" | "결제 정보를 입력했어요" |
예외 허용: 서비스 종료/기간 만료, 사용자 영향 설명, 안심 문구에서는 수동형("~돼요") 허용. "되어요" → 모두 "돼요" 로 통일.
TaillogToss 주요 점검 문구:
토스 미니앱 샌드박스 런타임에서 네이티브 모듈 링킹이 불가능하므로, TDS에 없는 UI 패턴은 아래 대안으로 구현한다.
| 누락 패턴 | 대안 구현 | 비고 |
|---|---|---|
| Chip/Tag (인터랙티브) | TouchableOpacity + Badge 래퍼 | quick-log 행동 칩 8개 |
| Accordion/Collapsible | Animated.View height 보간 커스텀 | quick-log 상세탭, dog-profile |
| DatePicker/TimePicker | SegmentedControl + Dropdown + NumericSpinner 조합 | 네이티브 모듈 제한으로 서드파티 불가 |
| Radar/Heatmap 차트 | WebView(@granite-js/native/react-native-webview) + Chart.js | analysis 화면 |
| Speech Bubble (말풍선) | View + Shadow + Border(radius) 커스텀 | coaching-result dog_voice |
// components/Chip.tsx — TouchableOpacity + Badge 래퍼
import { TouchableOpacity } from 'react-native';
import { Badge } from '@toss/tds';
interface ChipProps {
label: string;
selected?: boolean;
onPress: () => void;
}
export function Chip({ label, selected, onPress }: ChipProps) {
return (
<TouchableOpacity onPress={onPress}>
<Badge
text={label}
variant={selected ? 'info' : 'default'}
/>
</TouchableOpacity>
);
}
// components/Accordion.tsx — Animated.View height 보간
import { Animated, LayoutAnimation } from 'react-native';
export function useAccordion(initialExpanded = false) {
const [expanded, setExpanded] = useState(initialExpanded);
const toggle = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setExpanded(!expanded);
};
return { expanded, toggle };
}
react-native-svg, react-native-reanimated 등 네이티브 모듈 링킹 불가 (토스 미니앱 샌드박스 런타임)BarChart 1개만 존재 (Section 3-6 참조)| 차트 유형 | 구현 방식 | 사용 화면 |
|---|---|---|
| 막대 차트 (행동 빈도) | TDS BarChart | analysis |
| Radar 차트 (원인 분석 5차원) | WebView + Chart.js | analysis |
| Heatmap (요일×시간 밀도) | WebView + Chart.js | analysis |
| Line 차트 (트렌드) | WebView + Chart.js | analysis (향후) |
// lib/charts/ChartWebView.tsx
import WebView from '@granite-js/native/react-native-webview';
import { useColorScheme } from '@toss/tds';
interface ChartWebViewProps {
type: 'radar' | 'heatmap' | 'line';
data: unknown;
height?: number;
}
export function ChartWebView({ type, data, height = 250 }: ChartWebViewProps) {
const colorScheme = useColorScheme();
// TDS 색상 토큰에 맞춰 Chart.js 테마 자동 적용
const html = generateChartHTML(type, data, colorScheme);
return <WebView source={{ html }} style={{ height }} />;
}
useColorScheme() → Chart.js 테마 전환| 유형 | 지원 | 용도 |
|---|---|---|
| Interstitial (전면) | ✓ | 미사용 (UX 방해) |
| Rewarded (보상형) | ✓ | R1/R2/R3 터치포인트 |
| Banner (배너) | ✓ | 미사용 (v1) |
react-native-google-mobile-ads 직접 사용 불가 → 토스 통합 SDK 필수ait-ad-test-rewarded-id| ID | 화면 | CTA 텍스트 | 보상 |
|---|---|---|---|
| R1 | survey-result | "광고 보고 전체 분석 보기" | 상세 리포트 1회 해제 |
| R2 | dashboard | "광고 보고 코칭 열기" | 오늘의 코칭 1회 열기 |
| R3 | coaching-result | "광고 보고 오늘의 코칭 열기" | 잠긴 3블록(④⑤⑥) 1회 해제 |
Skeleton 블러 → TextButton("광고 보고 열기") → Rewarded 시청 → 1회 해제 → "매번 광고 귀찮다 → PRO 구독" 자연 전환
Follow this runtime path:
appLogin() from @apps-in-toss/framework.{ authorizationCode, referrer } to Supabase Edge Function (login-with-toss).POST /api-partner/v1/apps-in-toss/user/oauth2/generate-tokenGET /api-partner/v1/apps-in-toss/user/oauth2/login-meuserKey to Supabase Auth account.supabase.auth.setSession() and continues onboarding.EXPO_PUBLIC_SUPABASE_URL, EXPO_PUBLIC_SUPABASE_ANON_KEYimport { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.EXPO_PUBLIC_SUPABASE_URL!, process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!);
const { data } = await supabase.functions.invoke('login-with-toss', {
body: { authorizationCode, referrer },
});
await supabase.auth.setSession({
access_token: data.access_token,
refresh_token: data.refresh_token,
});
Use server secrets only:
SUPER_SECRET_PEPPERSUPABASE_URLSUPABASE_SERVICE_ROLE_KEYTOSS_CLIENT_CERT_BASE64TOSS_CLIENT_KEY_BASE64Function requirements:
Deno.createHttpClient({ cert, key }).client: tossHttpClient.tossUserKey + pepper (PBKDF2).public.users.access_token, refresh_token).supabase/functions/...; FastAPI server code lives under Backend/app/... in this repository.Example supabase/config.toml:
[functions.login-with-toss]
enabled = true
verify_jwt = true
import_map = "./functions/login-with-toss/deno.json"
entrypoint = "./functions/login-with-toss/index.ts"
If pre-login invoke gets 401, use verify_jwt = false for this endpoint or ensure JWT exists before invoke.
Use UUID identity linked to auth.users:
CREATE TABLE IF NOT EXISTS public.users (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
toss_user_key TEXT UNIQUE NOT NULL,
role TEXT DEFAULT 'user' NOT NULL CHECK (role IN ('user', 'trainer', 'org_owner', 'org_staff'))
);
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow individual read access" ON public.users FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Allow individual update access" ON public.users FOR UPDATE USING (auth.uid() = id);
Never mix integer users.id schema with Supabase Auth UUID schema.
agreedTerms 배열로 사용자가 동의한 약관 목록 반환| 콘솔 필드 | 필수 | 형식 |
|---|---|---|
| 서비스 이용약관 | ★필수 | 외부 웹 URL (토스가 WebView로 열음) |
| 개인정보 수집·이용 동의 | 선택 | 외부 웹 URL |
| 마케팅 정보 수신 동의 | 선택 | 외부 웹 URL |
| 전자적 전송매체 광고 수신 동의 | 선택 | 외부 웹 URL |
토스 → 파트너 서버 방향. Basic Auth 인증 (mTLS 아님).
POST {콜백URL}
Headers: Authorization: Basic {base64(id:password)}
Content-Type: application/json
Body: {"userKey": 443731103, "referrer": "UNLINK"}
Response: 200 OK
| referrer | 의미 | 처리 |
|---|---|---|
UNLINK | 사용자가 직접 연결 해제 | toss_user_key → NULL, 재연결 가능 |
WITHDRAWAL_TERMS | 동의 철회 | PII 삭제 + 익명화 |
WITHDRAWAL_TOSS | 토스 계정 탈퇴 | 전체 CASCADE 삭제 |
https://apps-in-toss.toss.im 허용 (콘솔 테스트 버튼용)모든 S2S 호출은 mTLS 클라이언트 인증서 필요.
| API | Method | Path |
|---|---|---|
| 토큰 발급 | POST | /api-partner/v1/apps-in-toss/user/oauth2/generate-token |
| 프로필 조회 | GET | /api-partner/v1/apps-in-toss/user/oauth2/login-me |
| 토큰 갱신 | POST | /api-partner/v1/apps-in-toss/user/oauth2/refresh-token |
| 연결해제(토큰) | POST | .../access/remove-by-access-token |
| 연결해제(키) | POST | .../access/remove-by-user-key |
scope: 동의된 권한 (예: user_ci, user_name, user_phone)Toss가 반환하는 사용자 정보는 AES-256-GCM 암호화 상태.
| 필드 | 암호화 | 비고 |
|---|---|---|
| userKey | ✗ | 숫자, 고유 식별자 |
| name | ✓ | 동의 시만 반환 |
| phone | ✓ | 동의 시만 반환 |
| birthday | ✓ | yyyyMMdd |
| ci | ✓ | 본인확인정보 |
| gender | ✓ | |
| nationality | ✓ | |
| ✓ | 미인증 상태일 수 있음 | |
| scope | ✗ | 동의된 권한 배열 |
| agreedTerms | ✗ | 동의한 약관 배열 |
// 복호화 예시 (Deno Edge Function)
function decryptTossPII(encryptedBase64: string, keyHex: string, aad: Uint8Array): string {
const encrypted = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
const iv = encrypted.slice(0, 12);
const ciphertext = encrypted.slice(12);
// AES-256-GCM decrypt with iv + aad
}
TOSS_CLIENT_CERT_BASE64 = base64(taillog_public.crt)TOSS_CLIENT_KEY_BASE64 = base64(taillog_private.key)const cert = atob(Deno.env.get('TOSS_CLIENT_CERT_BASE64')!);
const key = atob(Deno.env.get('TOSS_CLIENT_KEY_BASE64')!);
const httpClient = Deno.createHttpClient({ certChain: cert, privateKey: key });
.gitignore)// 기존 v1: loadRewardedAd / showRewardedAd → 폐기
// 공식 ver2: loadFullScreenAd / showFullScreenAd + adGroupId + destroy
interface TossAdsSdk {
loadFullScreenAd(options: { adGroupId: string }): Promise<void>;
showFullScreenAd(): Promise<{ rewarded: boolean }>;
isAdLoaded(): boolean;
destroy(): void; // cleanup — useEffect return에서 호출
}
adGroupId: 콘솔에서 발급받는 광고 그룹 ID (기존 unitId 대체)destroy(): 광고 리소스 정리 — 컴포넌트 언마운트 시 필수 호출ait-ad-test-rewarded-idimport { createOneTimePurchaseOrder } from '@apps-in-toss/framework';
const cleanup = createOneTimePurchaseOrder({
options: { sku: 'pro_monthly' },
processProductGrant: async (receipt) => {
// Edge Function으로 서버 검증 + 상품 지급
const result = await supabase.functions.invoke('verify-iap-order', {
body: { orderId: receipt.orderId, productId: receipt.productId,
transactionId: receipt.transactionId }
});
return result.data?.ok === true; // true=완료, false=환불 트리거
},
onEvent: (event) => { /* PURCHASE_STARTED, PAYMENT_COMPLETED, GRANT_COMPLETED */ },
onError: (error) => { /* 결제 에러 UI */ },
});
// cleanup() — 결제 프로세스 취소/정리
import { getPendingOrders, completeProductGrant } from '@apps-in-toss/framework';
const pending = await getPendingOrders();
for (const order of pending) {
await completeProductGrant(order.orderId);
}
ONE_TIME 반복 대신 SUBSCRIPTION으로 등록 가능@apps-in-toss/framework 패키지가 @granite-js/react-native에 포함인지 별도 설치인지 확인 필요| # | 항목 | 기준 | 구현 |
|---|---|---|---|
| 1 | 동적 텍스트 | iOS Large~xxxLarge, Android 연속 스케일 지원 | Typography 토큰 사용 (fontSize 하드코딩 금지) |
| 2 | VoiceOver 라벨 | 모든 인터랙티브 요소에 accessibilityLabel | IconButton, Chip, 커스텀 터치 영역 |
| 3 | VoiceOver 역할 | accessibilityRole 지정 | button, checkbox, tab, header, link |
| 4 | 최소 터치 영역 | 44×44pt 이상 | hitSlop 또는 패딩으로 보정 |
| 5 | 줄바꿈 | 음절(character) 단위 줄바꿈 | 한글 텍스트는 word-break 대신 character-break |
| 원칙 | 설명 | TaillogToss 적용 |
|---|---|---|
| Worst-case 먼저 | 모든 옵션 최대 사용 상태를 먼저 확인 | 긴 강아지이름, 8개 행동칩 전체 선택, 긴 코칭 텍스트 |
| 블러/투명도 | 오버레이에 blur 효과로 가벼운 시각 | BottomSheet dimmed 배경, ModalLayout |
| 애니메이션 일관성 | 진입 300ms ease-out, 퇴장 200ms ease-in | Accordion, BottomSheet, 화면 전환 |
| 스크롤 인디케이터 | 긴 목록에 스크롤 여부 시각 힌트 | Dashboard 기록 목록, 설정 목록 |
| 빈 상태 ≠ 에러 | EmptyState(안내+CTA) vs ErrorState(재시도) 명확 분리 | 기록 0건 vs 네트워크 에러 |
| Skeleton 즉시 표시 | 데이터 로딩 시 2초 이내 Skeleton 또는 Loader | 모든 API 호출 화면 |
| 항목 | 요구사항 | 비고 |
|---|---|---|
| 뒤로가기 | 모든 하위 화면에 ← 또는 × 필수 | 메인 탭/로그인 제외 |
| 응답 시간 | 모든 인터랙션 2초 이내 응답 | Loader/Skeleton 즉시 표시 |
| 라이트 모드 | 라이트 모드만 지원 (다크 모드 미지원) | useColorScheme 불필요 |
| 번들 크기 | .ait 파일 압축 해제 후 100MB 이하 | 이미지/폰트 최적화 |
| 오디오 | 광고/결제 중 앱 오디오 일시 정지 | 해당 시 구현 |
| 인앱 기능 | 콘솔에 최소 1개 인앱 기능 등록 | 등록 안 하면 심사 거부 |
| UX Writing | 공식 가이드 준수 | 릴리즈 전 점검 |
| Rate Limit | 공식 기본 3,000 QPM | 초과 시 429 응답 |
https://apps-in-toss.toss.im — 콘솔 테스트 버튼https://{appName}.private-apps.tossmini.com — Sandbox 테스트