بنقرة واحدة
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 테스트