🧩 콘솔 과제: ☕ 카페 주문 키오스크 (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는 장바구니의 상태를 캡슐화하여 관리한다.
복잡한 구조보다는 '비즈니스 규칙'을 코드로 옮기는 논리력 테스트에 가깝다.
(14)번 구현: 쿠폰 할인율 결정
: 조건문을 사용할 때 if-else도 좋지만, Enum은 switch 문을 쓰면 훨씬 깔끔하고 안전하다.
모든 케이스를 다 다뤘는지 컴퓨터가 체크해줌!
(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원 보정 로직을 추가하여 시스템의 안정성을 높였다.
/// (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}');
}