Skip to content

냠냠픽업 — 개발자 핸드오프 가이드

Figma 디자인을 실제 iOS/모바일 앱 코드로 옮길 때 참고할 구현 가이드


1. 기술 스택 (권장)

본 프로젝트의 정확한 기술 스택은 팀 결정사항입니다. 아래는 디자인 전제(iOS 390×844)와 멤버 구성(POS 프론트, 백엔드)을 기반으로 한 권장 사항입니다.

영역권장 기술비고
사용자 앱iOS Native (Swift/SwiftUI) or React Native + Expo디자인이 iOS 기준으로 정밀
사장님 앱iOS / iPad Native, 또는 별도 POS 앱신석재님이 POS 프론트 담당
백엔드 APINode.js / Python (FastAPI) / Spring권세영, 김완태님이 백엔드
인프라AWS 또는 GCP김완태님이 인프라
DBPostgreSQL (관계형, 주문/매장 데이터)
지도Naver Map SDK / Kakao Map SDK한국 시장 우선
결제토스페이먼츠 / 포트원(아임포트)
푸시FCM (Firebase Cloud Messaging)
소셜 로그인Apple, KakaoFigma 화면에 두 가지만 노출
분석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
🟡 P1FAQ2
🟡 P1푸시 알림 + 설정3

Phase 3 — 보조 기능

우선순위화면 그룹화면 수
🟢 P2아이디 / 비밀번호 찾기6
🟢 P2회원탈퇴1
🟢 P2약관 동의 변형 (4종)4
🟢 P2매장 Empty States3

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 ComponentiOS / 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 텍스트와 조합
  • [ ] 폰트 사이즈 동적 확대 지원 (최소 12px → 최대 200%)
  • [ ] 탭 영역 최소 44 × 44pt
  • [ ] VoiceOver / TalkBack 라벨링
  • [ ] 색상에 의존하지 않는 상태 표시 (영업중/마감 → 텍스트 + 색상 함께)

컬러 대비 검증 결과

조합대비WCAG AA사용
#000 on #FFDE3614.31:1Primary CTA
#000 on #FFFFFF21:1본문
#FFDE36 on #FFFFFF1.42:1텍스트 사용 금지
#FF8500 on #FFFFFF2.79:1❌ (대형 텍스트만)18px+ Bold만
#777777 on #FFFFFF4.48:1⚠️캡션만
#464646 on #FFFFFF8.44:1Secondary text

12. 폰트 / 에셋 다운로드

Pretendard 폰트

Figma 에셋 export

각 화면 / 컴포넌트의 에셋은 Figma의 Export 패널에서 다음 형식으로 추출 권장:

에셋 종류포맷해상도
아이콘SVG1x
마스코트 일러스트PNG2x, 3x
음식 카테고리 아이콘PNG2x, 3x
매장 썸네일 (placeholder)PNG2x
로고SVG1x

13. 알려진 이슈 / 정리 필요 항목

Figma 파일에서 발견된 정리 필요 항목입니다. 디자이너(이화랑님)와 협의 후 정리 권장.

  • [ ] Component naming 일관성:
    • ResauratnStore (오타)
    • Opnenig timeOpening time (오타)
    • Bussiness owner replyBusiness owner reply (오타)
    • Copoun inputCoupon input (오타)
    • recievedreceived (주문 상태)
  • [ ] 익명 variant property: 속성 1=…, Property 1=… 같은 placeholder를 의미 있는 이름으로
  • [ ] 중복 화면 통합: Login / email / 1-2가 아이디 찾기/비번 변경 양쪽에 중복 → 코드에서는 단일 화면 + 상태로
  • [ ] Color/Text style 등록: 현재 paint style 1개, text style 0개 → 디자인 토큰 등록
  • [ ] Final-Brand design-Guide 페이지: 섹션 컨테이너만 있고 콘텐츠 비어있음 → 채우거나 다른 페이지 참조

14. 참고 링크

마지막 업데이트:

냠냠픽업 — 지속가능한 중개수수료 2% 음식 픽업 서비스