예제 연습하기-5 카페 키오스크 콘솔앱

주혜림·2026년 2월 20일

🧩 콘솔 과제: ☕ 카페 주문 키오스크 (OOP + enum + Generic)

✅ 목표 (필수 사용)

  • 객체지향(Class) 기반 설계
  • enum 사용
  • 제네릭(Generic) 함수 사용
  • 조건문(if/switch), 반복문(while/for), 컬렉션(List/Map), 주석 사용

폴더생성

lib/
 ├─ main.dart
 ├─ enums/
 │    └─ enums.dart
 ├─ models/
 │    ├─ menu_item.dart
 │    └─ cart_item.dart
 ├─ repository/
 │    └─ menu_repository.dart
 ├─ services/
 │    ├─ cart_service.dart
 │    └─ pricing_service.dart
 └─ utils/
      ├─ console_io.dart
      └─ generic_utils.dart

각 파일의 역할

'콘솔 과제'에서는 UI(view)가 따로 없고 print문으로 처리하기 때문에 서비스가 사실상 뷰모델처럼 비즈니스 로직의 중심이 됨

enums: 열거형 클래스. 가독성, 타입안정성, 실수방지(전용 보관함)
models: 설계도. 속성을 정의
repository: 데이터 보관 및 관리. (메뉴판 목록)
services: 비즈니스 로직. 순수한 기능제공 (계산, 저장, 통신)
utils: 공통 도구. 보조기능

레포지토리 생성

깃 허브 링크

스켈레톤 코딩

예제에서 요구하는 방향대로 순서를 따라가며 로직을 완성하자!
잘 모르는 부분은 주석을 달아서 AI에게 로직을 구성하는 진행방향이 맞는지 체크하며 함수를 구현했다.


장바구니 담기 함수

트러블슈팅

[ 문제발생 ]

장바구니에 메뉴를 담는 함수를 구현하기 위해서 기존 영화예매 다트콘솔 프로그램을 구현했던 부분을 참고해서 방향이 맞을지 확인했더니

void _addToCartFlow() 함수 내에서는 사용자에게 보여주기만 하는 부분이기 때문에
실제 데이터 계산은 CartService 클래스 내부에서 처리하는 것이 좋다.

void _addToCartFlow() {
    print('\n[장바구니 담기]');
    _showMenuItems();

    final int menuId = ConsoleIO.readPositiveInt('담을 메뉴 번호');
    final int count = ConsoleIO.readPositiveInt('수량');

    final MenuItem? menuItem = menuRepository.findById(menuId);
    if (menuItem == null) {
      print('존재하지 않는 메뉴입니다.');
      return;
    }

    // (1): 품절이면 담을 수 없도록 막기
    // 힌트: menuItem.status enum값이 담겨있음
    if (menuItem.status == ItemStatus.soldOut) {
      print('선택하신 ${menuItem.name} 해당 메뉴는 현재 품절입니다.');
      return;
    }
    // (2): cartService.add(menuItem, count) 호출
    // 단, add 내부에서 재고 초과 체크까지 되도록 구현할 것
    // 이미 장바구니 안에 같은 메뉴가 있는지 확인하기. 있다면 기존 count에 누적하기
    
    // 조건이 참일 경우 인덱스를 반환하고, 거짓일 경우 -1을 반환

    // 남은 재고를 초과하지 못하도록 하기. 이미 담긴 메뉴 + 새로 담는 메뉴

    // 장바구니에 추가 또는 수정 후 안내

    // 이미 장바구니에 있다면 수량만 추가하기
    
    // 장바구니에 없다면 새로 추가하기 
    
  }

[ 이유 ]

클래스가 기능과 역할로 나뉘어져 있는 부분들에 대해서 또 생각하지 못하고 흐름을 이해하는 예제인데 기능적 기술적 구현에 치중해서 잘못 생각을 하고 있었다.

[ 문제해결 ]

main에서 CartService 클래스를 호출해서 더해주기만 하면 된다.
내부 함수에 검증하고 추가하는 기능구현은 CartService클래스에서 구현하면 된다.

 // (1): 품절이면 담을 수 없도록 막기
    // 힌트: menuItem.status enum값이 담겨있음
    if (menuItem.status == ItemStatus.soldOut) {
      print('선택하신 ${menuItem.name} 해당 메뉴는 현재 품절입니다.');
      return;
    }
    // (2): cartService.add(menuItem, count) 호출만 해주면 됨!
    // 단, add 내부에서 재고 초과 체크까지 되도록 구현할 것(cart_service에서 구현)
    cartService.add(menuItem, count);

결제하기 함수

'Guard Clause(보호 구문)' 패턴
: 부정적인 조건(confirm != 'y')을 먼저 처리해서 함수를 일찍 종료시키는 방식이다.
이렇게 하면 들여쓰기가 깊어지지 않아 코드가 훨씬 깔끔해진다!


/// (8): Generic findFirst를 활용해 id로 메뉴 찾기
  // 메뉴판에서 특정 번호의 메뉴를 가리켜서 가져오는 기능: findById
  MenuItem? findById(int id) {
    // 구현
    
    return null;
  }

Generic findFirst 활용하기
: 어떤 리스트에서도 쓸 수 있는 공용 돋보기"**를 만들라는 뜻
utils/generic_utils.dart에 공통 함수를 만들고 여기서 호출하라는 의도!!

[ 구조의 이해 ]

지금 이 구조는 실제 실무에서도 똑같이 쓰인다.

View(main): "사용자가 1번을 눌렀어!"
Repository(여기): "잠깐만, 우리 메뉴판(_menuItems)에서 1번 객체 찾아올게."
결과: "찾았어! 이건 이름이 '아메리카노'고 가격이 4,500원인 객체야."


CartService 클래스

CartService는 장바구니의 상태를 캡슐화하여 관리한다.

  1. 리스트 추가
  2. 비즈니스 규칙(재고 확인, 중복 아이템 처리)을 서비스 계층에서 엄격히 검증하도록 설계
  3. 제네릭 유틸리티(sumBy, findFirst)를 활용하여 데이터 계산 로직의 중복을 제거하고 코드의 재사용성을 높임

PricingService 클래스

복잡한 구조보다는 '비즈니스 규칙'을 코드로 옮기는 논리력 테스트에 가깝다.

  1. (14)번 구현: 쿠폰 할인율 결정
    : 조건문을 사용할 때 if-else도 좋지만, Enum은 switch 문을 쓰면 훨씬 깔끔하고 안전하다.
    모든 케이스를 다 다뤘는지 컴퓨터가 체크해줌!

  2. (15)번 구현: 최종 금액 계산
    : 할인 로직은 순서가 중요. 주석에 적힌 단계별로 계산해보자

 /// (15): 최종 금액 계산. 위에서 만든 할인율 메서드를 호출하고 추가조건(할인) 적용하는 것이 핵심
  /// - 쿠폰 할인율만큼 퍼센트 할인 적용
  /// - 할인 적용 후 금액이 30,000원 이상이면 1,000원 추가 할인 (중복 적용)
  /// - 최종 금액은 0원 미만으로 내려가지 않게 보정
  int calculateFinalPrice({
    required int originalTotal,
    required CouponGrade couponGrade,
  }) {
    // 구현
    var discountRate = couponDiscountRate(couponGrade);
    int discountCount = (originalTotal * discountRate).toInt();
    int total = originalTotal - discountCount;
    if (total >= 30000) {
      // 둘 중 큰 것만 반환함!!
      return max(total - 1000, 0);
    } else {
      return total;
    }
  }
}

Q. 할인 로직을 구현할 때 가장 주의한 점은 무엇인가요?

A.
첫째, 데이터 타입의 변환
할인율은 소수점(double)이지만 최종 금액은 원화 단위인 정수(int)여야 하므로 정확한 형 변환
둘째, 예외 케이스 처리
중복 할인이 적용될 경우 발생할 수 있는 음수(-) 결제 금액을 방지하기 위해 0원 보정 로직을 추가하여 시스템의 안정성을 높였다.


generic_utils.dart

/// (16): 조건에 맞는 첫 번째 요소 반환 (없으면 null)
// 어떤 타입의 리스트든 조건에 맞는 것을 찾아주는 공용 함수
T? findFirst<T>(List<T> items, bool Function(T item) condition) {
  // 구현. 조건에 맞는 하나를 찾는거. 첫번째 요소
  // 컨디션 인자로 받아온 함수를 넣어준 것.
  items.firstWhere(condition);
  return null;
}

1) 널값을 먼저 리턴해줄 경우
문제점: 만약 리스트의 첫번째 요소가 거짓일 경우 바로 함수가 끝나버리게 된다.
리스트 뒤쪽에 맞는 조건이 있을 수 있음

T? findFirst<T>(List<T> items, bool Function(T item) condition) {
  // 구현. 조건에 맞는 하나를 찾는거. 첫번째 요소
  // 컨디션 인자로 받아온 함수를 넣어준 것.
  for (var item in items) {
    // 만약 컨디션에 들어있는 아이템이 참일 경우
    if (!condition(item)) {
      return null;
    }
  }
  return items.firstWhere(condition);
}

요소 하나하나를 불러와서 맞으면 가져오고 없다면 널값을 반환하는 방향으로 가야한다.

위 함수를 메인으로 불러와서 사용한다면
사용예시

void main2() {
  List<int> list = [1, 2, 3, 4, 5];

  list.firstWhere((element) {
    return element > 3;
  });

  list.firstWhere(greaterThanThree); // condition 인자로 받아온 함수
}

bool greaterThanThree(int element) {
  return element > 3;
}

실력향상에 도움이 되는 학습 방식
함수를 먼저 작성한 뒤 메인에서 적용시켜서 어떻게 동작되는지 확인해가며 이해를 해보는 것이 중요하다.


[ 문제발생 ]

콘솔 실행 시 장바구니에 목록이 담겼다고 출력은 되는데 3번 장바구니보기를 눌러보면 장바구니 안에 리스트가 하나도 없다고 뜬다.

3번에서 로직에 논리가 오류가 있는 것 같다.
이미 리스트에 존재할 때 새로운 아이템을 계속 추가하게 됨
리스트에 아무것도 없을 땐 비어있어서
새로 담는 아이템은 추가가 되지 않음

void add(MenuItem menuItem, int count) {
    // 구현
    // 1. 이미 담겨 있는지 확인
    final existItem = findCartItemByMenuId(menuItem.id);
    int currentInCart = existItem?.count ?? 0;
    // 2. 재고 체크 (기존 수량 + 새로 추가할 수량)
    if (currentInCart + count > menuItem.stock) {
      print('재고가 부족합니다. 남은 재고: ${menuItem.stock}개 | 장바구니: $currentInCart개');
      return;
    }
    // 3. 추가 또는 누적
    // 카트에 아무것도 없으면 추가해줘야 됨
    // 이미 있으면 추가수량 담아주기
    if (existItem != null) {
      _cartItems.add(CartItem(menuItem: menuItem, count: count));
    } else {
      
    }
    print('${menuItem.name} $count개가 장바구니에 담겼습니다.');
  }

[ 원인 ]

코드를 하나하나 실행해보니 카트서비스 클래스의 add함수에서 추가/누적 하는 부분의 코드가 온전히 구현되지 못했고, 추가하는 부분은 아무것도 없을 때 추가가 되어야 해서 논리가 잘못 되었다.

[ 문제해결 ]

추가수량은 이미 존재하는 아이템의 카운트 값에 새 카운트 값을 더해주고,
add함수는 null값일 경우 분기처리 한 부분으로 옮겨주었더니 정상적으로 동작한다.

    if (existItem != null) {
      // 이미 있으면 추가수량 담아주기
      existItem.count += count;
    } else {
      // 없으면 새로 추가해주기
      _cartItems.add(CartItem(menuItem: menuItem, count: count));
    }
    print('${menuItem.name} $count개가 장바구니에 담겼습니다.');

[ 문제발생 ]

장바구니에서 수량을 변경하려고 했더니 재고가 있음에도 재고가 초과한 것으로 로직에 오류가 발생했다.


카트서비스 updateCount 로직에서 수정이 필요해보인다.

[ 원인 ]

else if에서 새로운 수량이 재고보다 작거나 같을 때 에러메시지를 출력하도록 되어있어서 정상적인 수량을 입력해도 여기서 걸려버리게 된다!

void updateCount(int cartIndex, int newCount) {
    if (cartIndex < 0 || cartIndex >= _cartItems.length) return;

    final item = _cartItems[cartIndex];

    if (newCount <= 0) {
      // 수량이 0 이하면 삭제
      _cartItems.removeAt(cartIndex);
      print('해당 메뉴를 삭제했습니다.');
    } else if (newCount <= item.menuItem.stock) {
      // 재고 초과 시 금지
      print('재고를 초과할 수 없습니다.');
    } else {
      // 수량변경
      item.count = newCount;
      print('수량이 변경되었습니다.');
    }
  }

[ 문제해결 ]

부등호를 잘못 넣은 부분을 수정했다.
천천히 코드를 한줄 씩 읽어가야 오류가 난 부분이 보이는 것 같다.

else if (newCount > item.menuItem.stock) {
      // 재고 초과 시 금지
      print('재고를 초과할 수 없습니다. 현재 재고량: ${item.menuItem.stock}');
    }
profile
앱 개발을 공부중입니다.

0개의 댓글