냠냠픽업 — 개발자 핸드오프 가이드
Figma 디자인을 실제 iOS/모바일 앱 코드로 옮길 때 참고할 구현 가이드
1. 기술 스택 (권장)
본 프로젝트의 정확한 기술 스택은 팀 결정사항입니다. 아래는 디자인 전제(iOS 390×844)와 멤버 구성(POS 프론트, 백엔드)을 기반으로 한 권장 사항입니다.
| 영역 | 권장 기술 | 비고 |
|---|---|---|
| 사용자 앱 | iOS Native (Swift/SwiftUI) or React Native + Expo | 디자인이 iOS 기준으로 정밀 |
| 사장님 앱 | iOS / iPad Native, 또는 별도 POS 앱 | 신석재님이 POS 프론트 담당 |
| 백엔드 API | Node.js / Python (FastAPI) / Spring | 권세영, 김완태님이 백엔드 |
| 인프라 | AWS 또는 GCP | 김완태님이 인프라 |
| DB | PostgreSQL (관계형, 주문/매장 데이터) | — |
| 지도 | Naver Map SDK / Kakao Map SDK | 한국 시장 우선 |
| 결제 | 토스페이먼츠 / 포트원(아임포트) | — |
| 푸시 | FCM (Firebase Cloud Messaging) | — |
| 소셜 로그인 | Apple, Kakao | Figma 화면에 두 가지만 노출 |
| 분석 | Amplitude / Firebase Analytics | — |
2. 디자인 토큰 변환 표
2-1. iOS Swift (UIColor / Color)
swift
// Brand Colors
extension Color {
static let yumyumYellow = Color(hex: "#FFDE36")
static let yumyumYellowLight = Color(hex: "#FFF6CF")
static let yumyumYellowDark = Color(hex: "#F5C51D")
static let yumyumOrange = Color(hex: "#FF8500")
static let yumyumOrangeLight = Color(hex: "#FFE6B8")
static let yumyumOrangeDark = Color(hex: "#EB7501")
static let yumyumPink = Color(hex: "#FF91C7")
// Semantic
static let yumyumError = Color(hex: "#EB2323")
static let yumyumSuccess = Color(hex: "#3CDE75")
static let yumyumInfo = Color(hex: "#2597BA")
}
// Hex 헬퍼
extension Color {
init(hex: String) {
let scanner = Scanner(string: hex.replacingOccurrences(of: "#", with: ""))
var rgb: UInt64 = 0
scanner.scanHexInt64(&rgb)
self.init(
red: Double((rgb >> 16) & 0xFF) / 255,
green: Double((rgb >> 8) & 0xFF) / 255,
blue: Double( rgb & 0xFF) / 255
)
}
}2-2. React Native (StyleSheet)
typescript
export const colors = {
// Brand
yellow: '#FFDE36',
yellowLight: '#FFF6CF',
yellowDark: '#F5C51D',
orange: '#FF8500',
orangeLight: '#FFE6B8',
pink: '#FF91C7',
// Neutral
black: '#000000',
white: '#FFFFFF',
gray: {
50: '#F7F7F7',
100: '#EDEDED',
200: '#CECECE',
300: '#999999',
400: '#848484',
500: '#777777',
600: '#6B6B6B',
700: '#464646',
800: '#333333',
900: '#222222',
},
// Semantic
error: '#EB2323',
errorBg: '#FCDEDE',
success: '#3CDE75',
info: '#2597BA',
link: '#2763FF',
} as const;
export const spacing = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
'2xl': 24,
'3xl': 32,
} as const;
export const radius = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
pill: 100,
full: 10000,
} as const;
export const typography = {
display: { fontFamily: 'Pretendard-Bold', fontSize: 28, lineHeight: 36 },
heading: { fontFamily: 'Pretendard-Bold', fontSize: 18, lineHeight: 25 },
headingSm: { fontFamily: 'Pretendard-SemiBold', fontSize: 17, lineHeight: 24 },
body: { fontFamily: 'Pretendard-Regular', fontSize: 16, lineHeight: 24 },
bodyBold: { fontFamily: 'Pretendard-Bold', fontSize: 16, lineHeight: 24 },
bodySm: { fontFamily: 'Pretendard-Regular', fontSize: 14, lineHeight: 21 },
caption: { fontFamily: 'Pretendard-Regular', fontSize: 12, lineHeight: 17 },
captionXs: { fontFamily: 'Pretendard-Regular', fontSize: 10, lineHeight: 14 },
} as const;2-3. CSS Custom Properties (Web/Hybrid)
→ 03-design-system.md 참고
3. 화면 구현 우선순위
Phase 1 — MVP (1차 출시)
| 우선순위 | 화면 그룹 | 화면 수 |
|---|---|---|
| 🔴 P0 | 온보딩 + 회원가입 (Apple/이메일) | 8 |
| 🔴 P0 | 홈 — 리스트 뷰 | 3 |
| 🔴 P0 | 매장 상세 + 포장 카트 | 4 |
| 🔴 P0 | 주문 결제 (별도 정의 필요) | 3~5 |
| 🔴 P0 | 주문 내역 / 픽업 알림 | 3 |
Phase 2 — 핵심 기능 보강
| 우선순위 | 화면 그룹 | 화면 수 |
|---|---|---|
| 🟡 P1 | 카카오 소셜 로그인 | 2 |
| 🟡 P1 | 홈 — 지도 뷰 | 2 |
| 🟡 P1 | 카테고리 필터 | 4 |
| 🟡 P1 | 즐겨찾기 + 정렬 | 3 |
| 🟡 P1 | 위치 선택 / 주소 등록 | 3 |
| 🟡 P1 | 매장식사 카트 | 1 |
| 🟡 P1 | FAQ | 2 |
| 🟡 P1 | 푸시 알림 + 설정 | 3 |
Phase 3 — 보조 기능
| 우선순위 | 화면 그룹 | 화면 수 |
|---|---|---|
| 🟢 P2 | 아이디 / 비밀번호 찾기 | 6 |
| 🟢 P2 | 회원탈퇴 | 1 |
| 🟢 P2 | 약관 동의 변형 (4종) | 4 |
| 🟢 P2 | 매장 Empty States | 3 |
4. 데이터 모델 (제안)
4-1. User
typescript
interface User {
id: string;
email: string;
authProvider: 'apple' | 'kakao' | 'email';
name: string;
phone: string;
avatar: string; // 마스코트 캐릭터 ID 또는 URL
defaultAddress?: Address;
createdAt: Date;
}
interface Address {
id: string;
type: 'home' | 'office' | 'school' | 'custom';
label: string; // "우리집", "회사" 등
fullAddress: string;
detailAddress: string;
latitude: number;
longitude: number;
}4-2. Store
typescript
interface Store {
id: string;
name: string;
category: FoodCategory;
thumbnail: string;
images: string[];
rating: number; // 0~5
reviewCount: number;
distance?: number; // 미터, 사용자 위치 기준
status: StoreStatus;
openingHours: OpeningHours;
address: Address;
phone: string;
description: string;
acceptsTakeout: boolean;
acceptsDineIn: boolean;
ownerCharacter: string; // '효희' 등 캐릭터 ID
}
type FoodCategory =
| 'korean' | 'chinese' | 'japanese' | 'asian' | 'sashimi'
| 'meat' | 'chicken' | 'pizza' | 'burger' | 'kimbap'
| 'lunchbox' | 'sandwich' | 'salad' | 'coffee' | 'bakery';
type StoreStatus =
| 'open' // 영업중
| 'closed' // 영업종료
| 'preparing' // 영업준비중
| 'busy' // 주문 폭주
| 'paused'; // 일시 중단4-3. Menu
typescript
interface MenuItem {
id: string;
storeId: string;
name: string;
description: string;
price: number; // 원
originalPrice?: number; // 할인 시
thumbnail: string;
category: string; // '인기메뉴', '메인', '사이드' 등
isPopular: boolean;
isSoldOut: boolean;
options?: MenuOption[];
}
interface MenuOption {
id: string;
name: string;
type: 'single' | 'multi';
required: boolean;
choices: {
id: string;
name: string;
additionalPrice: number;
}[];
}4-4. Order
typescript
interface Order {
id: string;
userId: string;
storeId: string;
mode: 'takeout' | 'dineIn'; // 포장 / 매장식사
status: OrderStatus;
items: OrderItem[];
subtotal: number;
discount: number; // 쿠폰 할인
total: number;
pickupTime?: Date; // 픽업 예정 시간
coupon?: Coupon;
paymentMethod: 'card' | 'kakaopay' | 'tosspay';
createdAt: Date;
acceptedAt?: Date;
readyAt?: Date;
pickedUpAt?: Date;
}
type OrderStatus =
| 'pending' // 결제 전
| 'recieved' // 접수 (오타 그대로 — Figma 컴포넌트 명칭 일치)
| 'accepted' // 수락
| 'preparing' // 준비 중
| 'done' // 픽업 가능
| 'completed' // 픽업 완료
| 'cancelled'; // 취소
interface OrderItem {
menuId: string;
name: string;
quantity: number;
unitPrice: number;
selectedOptions: {
optionId: string;
choiceId: string;
additionalPrice: number;
}[];
}4-5. 사장님 앱 — Store State
typescript
interface StoreState {
id: string;
ownerId: string;
operationStatus: 'active' | 'paused' | 'busy' | 'closed';
newOrders: Order[]; // 신규 주문
activeOrders: Order[]; // 진행 중
todayStats: {
totalOrders: number;
totalRevenue: number;
averagePrepTime: number;
};
}5. API 엔드포인트 (제안)
인증
POST /api/auth/signup/email
POST /api/auth/signup/social # provider: 'apple' | 'kakao'
POST /api/auth/login/email
POST /api/auth/login/social
POST /api/auth/verify-identity # 본인인증
POST /api/auth/find-id
POST /api/auth/reset-password
DELETE /api/auth/account # 회원탈퇴매장 / 탐색
GET /api/stores # ?lat=&lng=&category=&sort=
GET /api/stores/:id
GET /api/stores/:id/menu
GET /api/stores/nearby # 지도 뷰용
GET /api/stores/categories
POST /api/favorites/:storeId
DELETE /api/favorites/:storeId
GET /api/favorites주문
POST /api/orders # 주문 생성
GET /api/orders # 사용자 주문 목록
GET /api/orders/:id
PATCH /api/orders/:id/cancel
POST /api/orders/:id/repeat # 재주문사장님 앱
GET /api/owner/orders # 신규/진행 주문
PATCH /api/owner/orders/:id/accept
PATCH /api/owner/orders/:id/reject
PATCH /api/owner/orders/:id/ready # 준비 완료
PATCH /api/owner/store/status # busy/paused/closed
GET /api/owner/stats/today위치 / 알림
GET /api/locations/search # 주소 검색
POST /api/locations/save # 자주 가는 주소 저장
GET /api/notifications
PATCH /api/notifications/settings
POST /api/notifications/token # FCM 토큰 등록6. 컴포넌트 매핑 (Figma → Code)
| Figma Component | iOS / RN 컴포넌트 명 | 비고 |
|---|---|---|
Basic_screen | <Screen> | iOS Safe Area 자동 처리 |
Button - Large | <PrimaryButton> | 295 × 55 |
Button - Small | <SecondaryButton> | — |
Bottom bnt | <BottomFixedButton> | Safe area bottom 보정 |
Input field | <TextInput> | — |
Login / input | <AuthInput> | 에러 상태 포함 |
Check box | <Checkbox> | — |
Toggle | <Switch> | — |
Quantity selector | <QuantityStepper> | - / + 버튼 |
Dropdown | <Select> | open/close 상태 |
Bottom Tab Bar | <TabBar> | 5탭 고정 |
Title Bar | <NavBar> | back 버튼 옵션 |
store card - home | <StoreCard> | 358 × 195 |
Meal card | <MenuCard> | — |
Order card | <OrderCard> | 상태별 variant |
Store status | <StatusBadge> | 5가지 variant |
Map_pin-open/close | <MapPin> | 영업/마감 |
Toast | <Toast> | 글로벌 컨텍스트 |
Popup, Popup_2 | <Modal> | 1-line / 2-line / 3-pic / no-text |
Bottom sheet | <BottomSheet> | 5가지 사이즈 |
Coupon | <CouponBadge> | added/not added |
User avatar | <MascotAvatar> | 마스코트 ID 매핑 |
7. 라우팅 구조 (제안)
/ # 스플래시 → 자동 분기
├─ /onboarding/1
├─ /onboarding/2
├─ /onboarding/3
├─ /auth # 로그인 랜딩
│ ├─ /auth/signup/email
│ ├─ /auth/signup/verify
│ ├─ /auth/signup/terms
│ ├─ /auth/signup/complete
│ ├─ /auth/login/email
│ ├─ /auth/find-id
│ └─ /auth/reset-password
│
└─ /main # 메인 (Bottom Tab)
├─ /home
│ ├─ /home/category/:slug
│ ├─ /home/all
│ ├─ /home/map
│ └─ /home/notifications
├─ /search
├─ /favorites
├─ /orders
│ ├─ /orders/:id
│ └─ /orders/:id/track
└─ /profile
├─ /profile/faq
├─ /profile/notifications
└─ /profile/delete-account/stores/:id (모달/풀스크린):
/stores/:id # 매장 상세
/stores/:id/menu/:menuId # 메뉴 상세
/stores/:id/cart # 카트
/stores/:id/checkout # 결제8. 상태 관리 (제안)
typescript
// Redux Toolkit / Zustand 권장
interface AppState {
auth: {
user: User | null;
token: string | null;
isAuthenticated: boolean;
};
location: {
current: { lat: number; lng: number } | null;
selected: Address | null;
saved: Address[];
};
stores: {
list: Store[];
selectedCategory: FoodCategory | 'all';
viewMode: 'list' | 'map';
favorites: string[]; // store IDs
};
cart: {
storeId: string | null; // 단일 매장 카트 정책
mode: 'takeout' | 'dineIn';
items: CartItem[];
coupon: Coupon | null;
};
orders: {
active: Order[];
history: Order[];
};
notifications: {
list: Notification[];
unreadCount: number;
settings: NotificationSettings;
};
}카트 정책: 단일 매장 카트 (다른 매장 메뉴를 담으려면 기존 카트 비움 확인 필요)
9. 핵심 인터랙션 구현 노트
9-1. iOS Safe Area
모든 화면 컨테이너는 iOS Safe Area 보정 필수:
swift
// SwiftUI
.safeAreaInset(edge: .top) { Color.clear.frame(height: 0) }
.safeAreaInset(edge: .bottom) { Color.clear.frame(height: 0) }typescript
// React Native
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const insets = useSafeAreaInsets();
<View style={{ paddingTop: insets.top, paddingBottom: insets.bottom }} />- Status Bar: 54px (Figma 기준, 실제 단말마다 다름)
- Home Indicator: 21px (iPhone 13~)
9-2. 카트 추가 애니메이션
typescript
// React Native + Reanimated
const scale = useSharedValue(1);
const onAddToCart = () => {
scale.value = withSequence(
withTiming(1.15, { duration: 160, easing: Easing.bezier(0.34, 1.56, 0.64, 1) }),
withTiming(1.0, { duration: 240, easing: Easing.bezier(0.34, 1.56, 0.64, 1) })
);
// 카트 상태 업데이트 + 토스트
};9-3. 매장 영업 상태 분기
typescript
function getStatusBadge(store: Store): StatusBadgeProps {
const now = new Date();
if (store.status === 'closed') {
return { variant: 'closed', label: '영업종료' };
}
if (store.status === 'preparing') {
const opensAt = store.openingHours.todayOpen;
return { variant: 'preparing', label: `${formatTime(opensAt)} 오픈` };
}
if (store.status === 'busy') {
return { variant: 'busy', label: '주문 폭주' };
}
if (store.status === 'open') {
return { variant: 'open', label: '영업중' };
}
return { variant: 'closed', label: '준비 중' };
}9-4. 주문 상태 실시간 업데이트
typescript
// WebSocket or FCM 기반 실시간 상태 동기화
// 주문 상태가 'done'으로 바뀔 때:
// 1. Push 알림 발송 ("픽업 준비 완료!")
// 2. 앱 내 토스트 + 진동
// 3. 주문 카드에 pulse 애니메이션
// 4. Bottom Tab의 Orders에 빨간 점 표시9-5. 지도 ↔ 리스트 토글
typescript
// 동일 매장 데이터를 두 view에서 공유
// 토글 시 애니메이션: cross-fade (200ms)
// 지도 뷰에서 핀 탭 → 매장 카드 bottom sheet (260px)10. 에러 처리 / Empty States
| 상황 | UI 처리 |
|---|---|
| 위치 권한 거부 | "위치 권한이 필요해요" + 설정 이동 버튼 + 마스코트 일러스트 |
| 네트워크 오류 | "연결을 확인해주세요" 풀스크린 + 재시도 버튼 |
| 매장 없음 (반경 내) | Home / 매장없음 화면 (390 × 1145) — 마스코트 + 위치 변경 CTA |
| 인기메뉴 없음 | Store / 상세 / 인기메뉴 없음 (390 × 1209) |
| 영업준비중 | Store / 영업준비중 / 담기불가능 — 카트 비활성 |
| 알림 없음 | Alert Center / Empty — 마스코트 + "알림이 없어요" |
| 주문 내역 없음 | "첫 주문을 시작해보세요" + 홈으로 이동 CTA |
11. 접근성 (Accessibility)
필수 체크리스트
- [ ] 모든 버튼에
accessibilityLabel지정 - [ ] 이미지에
alt/accessibilityLabel(장식 이미지는 hidden) - [ ] 컬러 대비: WCAG AA 기준 (텍스트 4.5:1 이상)
- ⚠️ Yellow
#FFDE36+ White 조합은 대비 부족 → 항상 Black 텍스트와 조합
- ⚠️ Yellow
- [ ] 폰트 사이즈 동적 확대 지원 (최소 12px → 최대 200%)
- [ ] 탭 영역 최소 44 × 44pt
- [ ] VoiceOver / TalkBack 라벨링
- [ ] 색상에 의존하지 않는 상태 표시 (영업중/마감 → 텍스트 + 색상 함께)
컬러 대비 검증 결과
| 조합 | 대비 | WCAG AA | 사용 |
|---|---|---|---|
#000 on #FFDE36 | 14.31:1 | ✅ | Primary CTA |
#000 on #FFFFFF | 21:1 | ✅ | 본문 |
#FFDE36 on #FFFFFF | 1.42:1 | ❌ | 텍스트 사용 금지 |
#FF8500 on #FFFFFF | 2.79:1 | ❌ (대형 텍스트만) | 18px+ Bold만 |
#777777 on #FFFFFF | 4.48:1 | ⚠️ | 캡션만 |
#464646 on #FFFFFF | 8.44:1 | ✅ | Secondary text |
12. 폰트 / 에셋 다운로드
Pretendard 폰트
- 공식: https://github.com/orioncactus/pretendard
- CDN: https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css
- iOS 번들: Pretendard-Regular.ttf, Pretendard-Medium.ttf, Pretendard-SemiBold.ttf, Pretendard-Bold.ttf
Figma 에셋 export
각 화면 / 컴포넌트의 에셋은 Figma의 Export 패널에서 다음 형식으로 추출 권장:
| 에셋 종류 | 포맷 | 해상도 |
|---|---|---|
| 아이콘 | SVG | 1x |
| 마스코트 일러스트 | PNG | 2x, 3x |
| 음식 카테고리 아이콘 | PNG | 2x, 3x |
| 매장 썸네일 (placeholder) | PNG | 2x |
| 로고 | SVG | 1x |
13. 알려진 이슈 / 정리 필요 항목
Figma 파일에서 발견된 정리 필요 항목입니다. 디자이너(이화랑님)와 협의 후 정리 권장.
- [ ] Component naming 일관성:
Resauratn→Store(오타)Opnenig time→Opening time(오타)Bussiness owner reply→Business owner reply(오타)Copoun input→Coupon input(오타)recieved→received(주문 상태)
- [ ] 익명 variant property:
속성 1=…,Property 1=…같은 placeholder를 의미 있는 이름으로 - [ ] 중복 화면 통합:
Login / email / 1-2가 아이디 찾기/비번 변경 양쪽에 중복 → 코드에서는 단일 화면 + 상태로 - [ ] Color/Text style 등록: 현재 paint style 1개, text style 0개 → 디자인 토큰 등록
- [ ] Final-Brand design-Guide 페이지: 섹션 컨테이너만 있고 콘텐츠 비어있음 → 채우거나 다른 페이지 참조
14. 참고 링크
- 디자인 파일: YumYum-v1 (Figma)
- BM Flow: YumYum BM Flow / 기획 (FigJam)
- 서비스 페이지: https://www.yumyum.im
- Pretendard: https://github.com/orioncactus/pretendard
- 화면 명세서:
02-screen-specs.md - 디자인 시스템:
03-design-system.md - 프로젝트 개요:
01-project-overview.md