예제 연습하기-3

주혜림·2026년 2월 18일

데이터를 담고(입력, List/Map) -> 검증(유틸리티, if) -> 함수로 쪼개서 분류/연산(출력)하는 흐름을 이해하는 예제를 다시 풀어보자

실제 앱 프로젝트에서 가장 흔하게 쓰이는
"재고 관리(Inventory) 또는 장바구니" 예제
데이터의 유무 확인, 수량 증감, 총액 계산 등 실제 앱의 핵심 로직을 모두 포함하고 있음


📌 과제: 콘솔 기반 "간단 가계부" 프로그램 만들기 (난이도: 초급+)

🎯 목표

  • 수입/지출 내역을 입력받아 저장한다.
  • 저장된 내역을 목록으로 확인한다.
  • 총 수입/총 지출/잔액을 계산해서 보여준다.
  • 카테고리별(예: 식비/교통/쇼핑) 지출 합계를 보여준다. ← (난이도 +1)

🧩 프로그램 동작 방식

메뉴를 반복해서 보여준다.

========== 가계부 ==========
1. 내역 추가 (수입/지출)
2. 전체 내역 보기
3. 요약 보기 (총 수입/총 지출/잔액)
4. 카테고리별 지출 합계 보기

  1. 종료


📦 저장 구조 (클래스 없이! 난이도는 조금만 올림)

  • 내역 1개는 Map<String, dynamic> 로 표현한다.
  • 모든 내역은 List<Map<String, dynamic>> 에 저장한다.

각 내역(Map)은 아래 키를 반드시 가진다:

  • 'type' : String ('income' 또는 'expense')
  • 'title' : String (예: "월급", "점심", "지하철")
  • 'amount' : int (예: 12000)
  • 'category' : String (예: "식비", "교통", "쇼핑")
  • 'createdAt' : String (예: "2026-02-13 14:20") // 간단히 문자열로 저장

※ 날짜는 실제 DateTime을 써도 되지만, 초급+라서 문자열로 저장해도 OK


1️⃣ 내역 추가 (수입/지출)

입력 흐름:

  • 유형을 입력: 1) 수입 2) 지출
  • 제목(title) 입력 (빈 값 불가)
  • 금액(amount) 입력 (양수 정수만 허용)
  • 카테고리(category) 입력 (빈 값 불가)
  • createdAt 은 현재 시간으로 자동 저장

예시:
유형을 선택하세요 (1:수입, 2:지출): 2
제목을 입력하세요: 점심
금액을 입력하세요: 9000
카테고리를 입력하세요(예: 식비/교통/쇼핑): 식비
저장 완료!


2️⃣ 전체 내역 보기

  • 저장된 모든 내역을 번호와 함께 출력한다.

  • 예시 출력 포맷(자유):
    1) [지출] 점심 - 9000원 (식비) / 2026-02-13 14:20

  • 내역이 없으면:
    "저장된 내역이 없습니다."


3️⃣ 요약 보기

  • 총 수입 합계
  • 총 지출 합계
  • 잔액 = 총 수입 - 총 지출

예시:
총 수입: 3000000원
총 지출: 45000원
잔액: 2955000원


4️⃣ 카테고리별 지출 합계 보기 (난이도 쪼~끔 업!)

  • 지출(expense)만 대상으로 한다.
  • category 별로 금액을 합산해서 출력한다.
  • Map<String, int> 형태로 합계를 만들어도 된다.

예시:
[카테고리별 지출]
식비: 18000원
교통: 2500원
쇼핑: 15000원


📌 구현 조건

  • 모든 코드는 이 main.dart 파일 하나에 작성
  • 클래스 금지 (이번 과제는 Map/List로 연습)
  • 반드시 List<Map<String, dynamic>> 사용
  • 반드시 Map<String, int> (카테고리 합계) 사용
  • 반복문(while) + switch 사용
  • 함수 5개 이상으로 분리
  • int.tryParse 사용
  • null 처리 반드시 포함
  • 금액은 0 이하 입력 시 다시 입력받기

📌 main 함수에는 흐름만 작성할 것

void main() {
  final List<Map<String, dynamic>> ledgerEntryList = [];

  runLedgerProgram(ledgerEntryList);
}

📌 프로그램 루프 함수

  • 메뉴 출력
  • 사용자 입력 처리
  • switch로 기능 분기
void runLedgerProgram(List<Map<String, dynamic>> ledgerEntryList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 메뉴 출력 함수

void printMenu() {
  // TODO: 구현
  throw UnimplementedError();
}

📌 내역 추가 함수

  • 수입/지출 선택
  • title 입력
  • amount 입력
  • category 입력
  • createdAt 저장
  • ledgerEntryList에 Map으로 추가
void addLedgerEntry(List<Map<String, dynamic>> ledgerEntryList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 전체 내역 출력 함수

void printAllEntries(List<Map<String, dynamic>> ledgerEntryList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 요약 출력 함수

  • 총 수입, 총 지출, 잔액 계산 및 출력
void printSummary(List<Map<String, dynamic>> ledgerEntryList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 카테고리별 지출 합계 출력 함수

  • 지출(expense)만 대상으로 category별 합산 후 출력
void printExpenseSummaryByCategory(List<Map<String, dynamic>> ledgerEntryList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 (유틸) 문자열 입력 받기

  • prompt 출력
  • null/빈 문자열이면 다시 입력받기 옵션 처리
String readNonEmptyString({required String prompt}) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 (유틸) 양수 정수 입력 받기

  • prompt 출력
  • 숫자 변환 실패 or 0 이하이면 다시 입력받기
int readPositiveInt({required String prompt}) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 (유틸) 현재 시간을 문자열로 만드는 함수

  • 예: "2026-02-13 14:20"
  • DateTime.now()를 사용해서 직접 포맷팅(패딩 포함)
String buildNowTimestampString() {
  // TODO: 구현
  throw UnimplementedError();
}

📌 과제: 콘솔 기반 "스마트 키오스크(장바구니) 프로그램"
🎯 목표

상품 목록에서 메뉴를 골라 장바구니에 담는다.

장바구니에 담긴 상품의 수량을 조절하거나 삭제한다.

최종 주문 시 총액을 계산하고 할인 혜택을 적용한다.

🧩 프로그램 동작 방식
메뉴를 반복해서 보여줍니다.

Plaintext

========== 🍔 SMART KIOSK ==========
1. 메뉴 보기 (상품 목록)
2. 장바구니 담기
3. 장바구니 보기 및 수량 조절
4. 주문하기 (최종 결제)

  1. 종료

    📦 데이터 구조 (Map/List 활용)
    1️⃣ 상품 목록 (고정 데이터)

프로그램 시작 시 아래 3개 이상의 상품을 List<Map<String, dynamic>>에 넣어둡니다.

키: 'id'(int), 'name'(String), 'price'(int)

2️⃣ 장바구니 (사용자 입력 데이터)

List<Map<String, dynamic>> cartList에 저장합니다.

각 아이템은 아래 키를 가집니다:

'name' : 상품명

'price' : 단가

'count' : 수량 (기본 1)

🛠 주요 기능 요구사항
1️⃣ 메뉴 보기

준비된 상품 목록을 번호와 함께 출력합니다.

2️⃣ 장바구니 담기

상품 번호를 입력받아 cartList에 추가합니다.

(중요) 이미 장바구니에 있는 상품이라면 새로 추가하지 않고, 기존 항목의 'count'(수량)만 1 증가시킵니다. (데이터 흐름 이해의 핵심!)

3️⃣ 장바구니 보기 및 수량 조절

현재 담긴 상품명, 단가, 수량, 소계(단가 * 수량)를 출력합니다.

추가로 "수량을 변경하시겠습니까? (y/n)"를 물어보고, 'y'인 경우 상품 번호와 새 수량을 입력받아 업데이트합니다.

수량을 0으로 입력하면 해당 항목을 삭제합니다.

4️⃣ 주문하기 (최종 결제)

모든 상품의 총액을 계산합니다.

할인 조건: 총액이 30,000원 이상이면 10% 할인을 적용합니다.

최종 결제 금액을 출력하고 장바구니를 비웁니다.

📌 구현 조건
모든 코드는 main.dart 파일 하나에 작성합니다.

클래스 사용 금지 (Map과 List의 구조에 익숙해지는 것이 목적입니다).

함수를 6개 이상으로 분리하세요.

int.tryParse를 사용하여 잘못된 입력(문자 등) 시 예외 처리를 반드시 포함하세요.

main 함수에는 프로그램 전체 흐름만 작성하세요.

💻 시작 코드 (스켈레톤)
이 구조를 복사해서 // TODO 부분을 채워보세요.

import 'dart:io';

void main() {
  // 상품 목록 데이터 (초기 세팅)
  final List<Map<String, dynamic>> products = [
    {'id': 1, 'name': '불고기버거', 'price': 6500},
    {'id': 2, 'name': '치즈버거', 'price': 5500},
    {'id': 3, 'name': '감자튀김', 'price': 2500},
    {'id': 4, 'name': '콜라', 'price': 2000},
  ];

  // 장바구니 데이터
  List<Map<String, dynamic>> cartList = [];

  runKioskProgram(products, cartList);
}

📌 프로그램 전체 루프

void runKioskProgram(List<Map<String, dynamic>> products, List<Map<String, dynamic>> cartList) {
  // TODO: while문과 switch-case를 사용하여 메뉴를 구성하세요.
}

📌 메뉴(상품 목록) 출력 함수

void printProductList(List<Map<String, dynamic>> products) {
  // TODO: products 리스트를 순회하며 상품 정보를 출력하세요.
}

📌 장바구니 담기 함수

  • 상품 번호 입력 받기
  • 이미 있다면 수량만 증가, 없다면 새로 추가
void addToCart(List<Map<String, dynamic>> products, List<Map<String, dynamic>> cartList) {
  // TODO: 상품 번호를 입력받아 cartList를 업데이트하세요.
}

📌 장바구니 보기 및 수량 변경 함수

  • 현재 장바구니 내용 출력
  • 수량 변경 로직 (0이면 삭제)
void showCart(List<Map<String, dynamic>> cartList) {
  // TODO: 장바구니 목록을 보여주고 수량 수정 여부를 물어보세요.
}

📌 주문 및 결제 함수

  • 총액 계산 -> 할인 적용 -> 장바구니 비우기
void processOrder(List<Map<String, dynamic>> cartList) {
  // TODO: 결제 로직을 구현하세요.
}

📌 (유틸) 숫자 입력 함수

  • 숫자 이외의 값 입력 시 재입력 요청
  • 양수만 허용 등의 조건 추가 가능
int readIntInput(String prompt) {
  // TODO: 사용자로부터 숫자를 안전하게 입력받으세요.
  throw UnimplementedError();
}

💡 힌트 및 응원
수량 증가 로직: cartList를 for문으로 돌면서 사용자가 선택한 상품 이름이 이미 존재하는지 먼저 확인하는 것이 포인트입니다.

ToDo 앱과의 연결: ToDo 앱에서 '완료 여부(isDone)'를 바꾸는 것이나, 이 예제에서 '수량(count)'을 바꾸는 것이나 결국 "리스트 안의 특정 맵(Map) 데이터에 접근해 값을 수정하는 것"으로 원리는 같습니다.


  1. 메뉴를 출력하는 함수

[ 문제점 ]

상품리스트를 출력할 때 id의 값을 따로 주고 반복해서 출력할 방법을 생각하다가 2중으로 작성했다.

[ 원인 ]

이중으로 반복문을 사용하면 상품이 4개일 경우 4*4 총 16번이 출력이 된게 된다!
상품 하나당 전체 리스트 길이만큼 출력이 되기 때문
for 반복문이 작동하는 방식의 이해도 부족

// 이미 있는 메뉴(상품 목록) 출력 함수
void printProductList(List<Map<String, dynamic>> products) {
  // products 리스트를 순회하며 상품 정보를 출력하세요.
  // 리스트 안의 상품 정보를 하나씩 꺼내서 출력을 해야됨
  int id = 0;

  for (var prd in products) {
    for (var id = 0; id <= products.length; id++) {
      // 리스트 순회하는 함수 사용?
      print(
        '${prd['id'] + 1}번: ${prd['name']} ${prd['price']}원, ${prd['count']}개',
      );
    }
  }
}

[ 해결방법 ]

이중 반복문을 삭제하고 코드를 더 간결하게 변경했다.

void printProductList(List<Map<String, dynamic>> products) {
  for (var prd in products) {
    print('${prd['id']}번: ${prd['name']} ${prd['price']}원');
  }
}

장바구니 담기 함수

흐름이해를 위해 구조를 어떻게 짜야할지 많이 참고해서 작성을 해보았다.
! if (selectProduct == null) {}을 for문 안쪽에서 작성 시 첫번째 상품만 검사하고 없다고 판단을 내리고 종료시킬 수 있다.
for문 밖으로 꺼내면 '!' 이 없이도 에러가 해결 될 수 있다.

// 장바구니 담기 함수. todo앱의 수정로직과 같음!
void addToCart(
  List<Map<String, dynamic>> products,
  List<Map<String, dynamic>> cartList,
) {
  // 상품 번호를 입력받아 cartList를 업데이트하세요.
  int productId = readIntInput('장바구니에 담을 상품번호를 입력하세요.');
  // 1. 선택한 번호가 실제 상품목록 안에 있는지 확인
  // 맵의 변수를 만들어주고
  Map<String, dynamic>? selectProduct;
  for (var prd in products) {
    // 만약 입력한 상품번호가 목록의 제품 아이디와 같다면, 상품목록에 있는게 맞다.
    if (prd['id'] == productId) {
      selectProduct = prd;
      break;
    }
    // 만약 입력한 상품번호가 목록의 제품 아이디에 없다면 존재하지 않음을 알려주고 종료한다.
    if (selectProduct == null) {
      print('존재하지 않는 상품정보 입니다. 다시 입력해주세요.');
      return;
    }
  }
  // 2. 장바구니에 이미 이 상품이 있는지 확인하기. 있으면 기존 항목의 count만 1증가시키기
  bool isExist = false;
  for (var item in cartList) {
    // 위에서 selectProduct의 값이 null이 아닐 경우 넘어오는 것이기 때문에 절대 null이 아니라고 !를 붙여준다.
    if (item['name'] == selectProduct!['name']) {
      // 이미 있다면
      item['count']++;
      isExist = true;
      print('${item['name']}의 수량이 추가되었습니다.');
      break;
    }
  }
  // 3. 장바구니에 없다면 추가하기
  if (!isExist) {
    cartList.add({
      'name': selectProduct!['name'],
      'price': selectProduct['price'],
      'count': 1,
    });
    print('${selectProduct['name']}을 장바구니에 담았습니다.');
  }
}

코드 수정

void addToCart(
  List<Map<String, dynamic>> products,
  List<Map<String, dynamic>> cartList,
) {
  int productId = readIntInput('장바구니에 담을 상품번호를 입력하세요.');
  
  Map<String, dynamic>? selectProduct;
  for (var prd in products) {
    if (prd['id'] == productId) {
      selectProduct = prd;
      break;
    }
  }
  // 루프가 끝나고 한번만 검사한다!
  if (selectProduct == null) {
    print('존재하지 않는 상품정보 입니다. 다시 입력해주세요.');
    return;
  }

  bool isExist = false;
  for (var item in cartList) {
    if (item['name'] == selectProduct['name']) {
      item['count']++;
      isExist = true;
      print('${item['name']}의 수량이 추가되었습니다.');
      break;
    }
  }

  if (!isExist) {
    cartList.add({
      'name': selectProduct['name'],
      'price': selectProduct['price'],
      'count': 1,
    });
    print('${selectProduct['name']}을 장바구니에 담았습니다.');
  }
}

장바구니 로직 함수

[ 문제점 ]

기본 구조 로직의 흐름 '번호로 항목을 고르는 법''수량 수정 로직'구조를 짜는데 아직 이해도가 부족해서 도움을 받기로 했다.

[ 원인 ]

계산식 에러:
${item['price'] * ['count']}에서 ['count']는 리스트 형태라 숫자가 아닙니다. item['count']라고 써야 합니다.

비교 대상 오류:
if (itemNumber == cartList)는 "숫자 하나"와 "장바구니 리스트 전체"를 비교하는 것이라 항상 거짓이 됩니다. "리스트의 몇 번째(인덱스)인가"를 확인해야 합니다.

수정 로직:
사용자가 '1'번 상품을 고르면, 컴퓨터 기준으로는 0번 인덱스(1 - 1)를 수정하는 방식이 가장 편합니다.

// 장바구니 보기 및 수량 변경 함수
void showCart(List<Map<String, dynamic>> cartList) {
  // 장바구니 목록을 번호로 고르고 수량 수정 여부를 물어보기
  if (cartList.isEmpty) {
    print('장바구니에 담긴 상품이 없습니다.');
    return;
  }
  // 현재 장바구니에 담긴 정보를 출력
  for (var item in cartList) {
    print(
      '${item['name']} ${item['price']} ${item['count']} = ${item['price'] * ['count']}원',
    );
  }
  // 수량변경 y/n 선택
  // y일 경우 상품 번호와 새 수량을 받아서 업데이트한다. 수량을 0으로 입력 시 해당 항목을 삭제
  stdout.write('수량을 변경하시겠습니까? (y/n)');
  String input = stdin.readLineSync() ?? '';

  if (input == 'y' && input == 'yes') {
    // 상품 번호를 입력해서 선택한 뒤, 변경수량을 또 입력??
    int itemNumber = readIntInput('수량을 변경할 상품 번호와 새 수량을 입력해주세요.');

    if (itemNumber == cartList) {
      print('${itemNumber['count']}+${cartList['count']}');
    }
  } else if (input == 'n' && input == 'no') {
    print('결제 항목으로 넘어갑니다.');
  }
}

[ 흐름 이해 ]

장바구니에 담긴 정보를 보여주려면 리스트의 길이만큼 출력하는게 편하다.
수량 변경에 대해 의사를 먼저 물어본 뒤 y일 경우 n일 경우 분기처리
인덱스 값을 가져와서 변경할 정보를 선택하고, 삭제할지(0) 수정할지
장바구니 안에 있을 경우 - 리스트 안에서 새로운 값을 더해줘서 수정해주기

! 리스트에서 제거할 때는 removeAt()을 사용, 추가할 때는 add(), 수정할 때는 입력한 값을 리스트에 새로 담아주면 됨.

void showCart(List<Map<String, dynamic>> cartList) {
  // 장바구니 목록을 번호로 고르고 수량 수정 여부를 물어보기
  if (cartList.isEmpty) {
    print('장바구니에 담긴 상품이 없습니다.');
    return;
  }
  // 현재 장바구니에 담긴 정보를 출력
  for (var i = 0; i < cartList.length; i++) {
    var item = cartList[i];
    // i + 1 사용자에게 1번부터 보여주기. 총 합이 얼마인지
    int subTotal = item['price'] * item['count'];
    print(
      '${i + 1}번 ${item['name']} | ${item['price']}원 | ${item['count']}개 | 소계: ${subTotal}원',
    );
  }
  // 수량변경 y/n 선택
  // y일 경우 상품 번호와 새 수량을 받아서 업데이트한다. 수량을 0으로 입력 시 해당 항목을 삭제
  stdout.write('수량을 변경하시겠습니까? (y/n)');
  String input = stdin.readLineSync()?.toLowerCase() ?? '';

  if (input == 'y') {
    // 상품 번호를 입력해서 선택.
    int itemNum = readIntInput('수량을 변경할 상품 번호를 입력해주세요.');
    // 컴퓨터가 이해하는 인덱스 번호로 변환한 것
    int index = itemNum - 1;

    // 입력한 번호가 장바구니 범위 안에 있는지 확인하기
    if (index >= 0 && index < cartList.length) {
      int newCount = readIntInput('새로운 수량을 입력하세요.(0 입력시 삭제)');

      if (newCount <= 0) {
        // Todo 삭제 로직과 동일함
        print('${cartList[index]['name']} 항목이 삭제되었습니다.');
        cartList.removeAt(index);
      } else {
        // 수정 로직 동일함.
        // newCount의 값을 리스트 안에 있는 목록 안 카운트의 값에 담아준다.
        cartList[index]['count'] = newCount;
        print('수량이 변경되었습니다.');
      }
    } else {
      print('잘못된 번호입니다.');
    }
  } else if (input == 'n') {
    print('수량변경이 취소되었습니다.');
    return;
  } else {
    print('잘못 입력하셨습니다. (y/n)중 선택해주세요.');
    return;
  }
}

주문 및 결제 함수

데이터를 어떻게 처리하고 최종적으로 마무리하는지의 흐름
통계 로직의 기초

// 주문 및 결제 함수
void processOrder(List<Map<String, dynamic>> cartList) {
  // 결제 로직을 구현하세요. 할인조건 총 액이 30,000원 이상이면 10% 할인 적용
  // 최종 결제 금액 출력하고 장바구니를 비우기
  // 장바구니가 비어있을 경우
  if (cartList.isEmpty) {
    print('결제할 상품이 없습니다.');
    return;
  }

  int total = 0;
  // 반복문으로 모든 상품 가격*수량 total에 더하기
  for (var i = 0; i < cartList.length; i++) {
    // 반복되는 카트리스트의 값을 아이템에 담아주고, 토탈값을 계산해준다.
    var item = cartList[i];
    total += (item['price'] as int) * (item['count'] as int);
  }
  print('최종합계: $total원 입니다.');
  // 할인적용
  if (total >= 30000) {
    total = (total * 0.9).toInt();
  }

  print('3만원 이상 구매시 10% 할인되어서 최종금액: $total원 입니다.');
  print('결제가 완료되었습니다. 이용해주셔서 감사합니다.');

  // 장바구니 비우기
  cartList.clear();
  return;
}

ToDo 앱에 대입해본다면?
누적 합산:
"전체 할 일 중 완료된 할 일은 몇 개인가?" 계산할 때 사용.
조건 처리:
"할 일을 다 완료했으면 '축하합니다' 메시지 띄우기" 할 때 사용.
초기화:
"완료된 항목만 전체 삭제하기" 기능을 만들 때 사용.

profile
앱 개발을 공부중입니다.

0개의 댓글