예제 연습하기-2!

주혜림·2026년 2월 13일

이어서 라이브 코딩을 조금씩 난이도를 올려서 진행해보도록 했다.
조금 더 빠르게 코드를 구현하고, 문제에 대한 해결능력이 올라가고 있는 것 같아서 기쁘다.


260213 금요일

난이도 1.5

📌 과제: 콘솔 기반 할 일(TODO) 관리 프로그램 만들기

🎯 목표

  • 사용자로부터 할 일을 입력받아 저장한다.
  • 저장된 할 일 목록을 확인할 수 있다.
  • 특정 번호의 할 일을 삭제할 수 있다.
  • 프로그램을 종료할 수 있다.

🧩 프로그램 동작 방식

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

===== TODO 프로그램 =====
1. 할 일 추가
2. 할 일 목록 보기
3. 할 일 삭제

  1. 종료

사용자가 번호를 입력하면 해당 기능을 실행한다.
0을 입력하면 프로그램을 종료한다.


1️⃣ 할 일 추가

  • 문자열로 할 일을 입력받는다.
  • 입력받은 내용을 List에 저장한다.
  • 빈 문자열은 저장하지 않는다.

예시:
추가할 할 일을 입력하세요: 운동하기


2️⃣ 할 일 목록 보기

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

예시:
1. 운동하기
2. 책 읽기
3. 코딩 공부하기

  • 만약 저장된 할 일이 없다면:
    "등록된 할 일이 없습니다." 출력

3️⃣ 할 일 삭제

  • 삭제할 번호를 입력받는다.
  • 해당 번호의 할 일을 삭제한다.
  • 존재하지 않는 번호를 입력하면 안내 메시지 출력

예시:
삭제할 번호를 입력하세요: 2


📌 구현 조건

  • 모든 코드는 이 main.dart 파일 하나에 작성
  • 반드시 List 사용
  • 반드시 반복문 사용 (while)
  • 반드시 조건문 사용 (if / switch)
  • 반드시 함수 3개 이상으로 분리
  • int.tryParse 사용하여 숫자 변환 처리
  • null 처리 반드시 포함

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

void main() {
  List<String> todoList = [];

  runTodoProgram(todoList);
}

📌 전체 프로그램을 실행하는 함수

  • 메뉴를 반복 출력한다.
  • 사용자 입력을 받아 기능을 실행한다.
void runTodoProgram(List<String> todoList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 할 일 추가 함수

  • 사용자로부터 문자열을 입력받는다.
  • 빈 값이면 추가하지 않는다.
  • 정상 입력이면 todoList에 추가한다.
void addTodo(List<String> todoList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 할 일 목록 출력 함수

  • 저장된 모든 할 일을 번호와 함께 출력한다.
  • 비어있으면 안내 메시지 출력
void printTodoList(List<String> todoList) {
  // TODO: 구현
  throw UnimplementedError();
}

📌 할 일 삭제 함수

  • 삭제할 번호를 입력받는다.
  • 해당 번호의 항목을 삭제한다.
  • 잘못된 번호 입력 시 안내 메시지 출력
void removeTodo(List<String> todoList) {
  // TODO: 구현
  throw UnimplementedError();
}

[ 첫번째 시도 ]

09:30 ~ 12:00

할 일 삭제 함수에서 무언가 논리에 오류가 나서 리스트의 번호를 입력해도 '존재하지 않는 목록 번호입니다.'가 뜨면서 정상적으로 삭제가 되지 않는다.

// 할 일 삭제 함수
void removeTodo(List<String> todoList) {
  // 삭제할 번호를 입력받는다
  // 해당 번호의 항목을 삭제한다
  // 잘못된 번호 입력 시 안내 메시지 출력
  printTodoList(todoList);
  stdout.write('삭제할 번호를 입력하세요: ');
  int? number = int.tryParse(stdin.readLineSync()?.toString() ?? '');
  // 목록을 돌면서 찾아야 하는데.
  int findNumber = todoList.indexWhere((list) => list.length == number);

  if (findNumber == todoList.length) {
    todoList.remove(findNumber);
    return;
  } else {
    print('존재하지 않는 목록 번호입니다.');
    return;
  }
}

[ 이유 ]

인덱스 시스템과 맞지 않는다.

  1. 잘못된 메서드 사용:
    todoList.indexWhere((list) => list.length == number) 코드는
    "할 일 글자 수가 내가 입력한 숫자와 같은 것"을 찾으라는 명령으로 인식한다.
    예를 들어 사용자가 1을 입력하면, 할 일 목록 중 글자 수가 1글자인 것의 위치를 찾게 됨

  2. 인덱스 범위 비교:
    if (findNumber == todoList.length) 는 논리적으로 거의 항상 참이 될 수 없다.
    리스트의 마지막 인덱스는 항상 length - 1이기 때문!!

[ 해결 ]

인덱스로 삭제를 하는 함수를 사용해보자.

void removeTodo(List<String> todoList) {
  // 삭제할 번호를 입력받는다
  // 해당 번호의 항목을 삭제한다
  // 잘못된 번호 입력 시 안내 메시지 출력
  printTodoList(todoList);
  stdout.write('삭제할 번호를 입력하세요: ');
  int? number = int.tryParse(stdin.readLineSync() ?? '');

  // 인덱스를 삭제하는 함수를 사용
  // 사용자가 입력한 인덱스 값이 리스트 사이에 있는지 확인해야 한다. (구조에 대한 힌트를 잘 모르겠어서 방법을 찾아봄)
  if (number != null && number > 0 && number <= todoList.length) {
    // !! 실제 0부터 시작하는 1
    // 인덱스 순서 값을 고려해서 number를 1씩 빼주어야 함
    todoList.removeAt(number - 1);
    print('할 일 목록에서 $number번 항목이 삭제되었습니다.');
  } else {
    print('존재하지 않는 목록입니다. 번호를 다시 입력해주세요.');
  }
}

[ 배운점 ]

목록의 인덱스가 0부터 시작하는 것은 알고 있었지만, 출력되는 목록리스트의 번호를 삭제하는 부분에서 고려하지 못해서
todoList.removeAt(number);로 코드를 작성했더니 입력한 넘버와 실제 인덱스의 위치와 맞지 않아서 코드가 정상작동하지 않았었다.

항상 특성에 대해 고려하고 적용할 수 있도록 주의해야겠다. ***

난이도: 2

📌 과제: 콘솔 기반 "간단 가계부" 프로그램 만들기

🎯 목표

  • 수입/지출 내역을 입력받아 저장한다.
  • 저장된 내역을 목록으로 확인한다.
  • 총 수입/총 지출/잔액을 계산해서 보여준다.
  • 카테고리별(예: 식비/교통/쇼핑) 지출 합계를 보여준다. ← (난이도 +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();
}
  1. 유틸리티 함수 제작. 데이터를 먼저 필터로 거른 뒤
  2. 맵을 채우고 등 함수 완성 후
  3. 스위치문에 연결하기

[ 첫번째 시도 ]

코드를 이해하는 3가지 포인트

데이터의 흐름:
addLedgerEntry에서 Map을 만들어 ledgerEntryList에 넣고, 다른 함수(printAllEntries, printSummary 등)에서는 그 리스트를 돌면서 데이터를 계산하거나 보여줍니다.
유틸리티 함수의 힘:
readNonEmptyString과 readPositiveInt는 while(true)를 사용하여 사용자가 올바른 값을 입력할 때까지 계속 물어봅니다. 이 덕분에 프로그램이 숫자가 아닌 값 때문에 갑자기 꺼지는 것을 방지합니다.
Map 다루기:
entry['amount']처럼 Key 값을 사용해 원하는 정보를 꺼내옵니다.

유틸리티(Utility):

반복적으로 사용되는 로직, 공통 기능, 데이터 변환, 검증 등의 작업을 재사용 가능하게 묶어 놓은 클래스나 함수(라이브러리/패키지)를 의미


1. addLedgerEntry함수의 단계별 흐름
이 함수가 해야 할 일을 순서대로 정리하면
질문하기:
"수입인가요, 지출인가요?", "어디에 쓰셨나요?" 등을 물어봅니다.
답변 받기:
사용자가 입력한 글자나 숫자를 변수에 담습니다.
포장하기 (Map):
변수들에 담긴 내용들을 Map이라는 봉투에 한꺼번에 담습니다.
저장하기 (List):
그 봉투(Map)를 전체 내역 리스트(ledgerEntryList)에 추가(.add)합니다.

// 메인에는 함수의 흐름만 작성
import 'dart:io';

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

  runLedgerProgram(ledgerEntryList);
}

// 프로그램 루프 함수. 스위치로 각 함수를 연결해주는 역할
void runLedgerProgram(List<Map<String, dynamic>> ledgerEntryList) {
  while (true) {
    printMenu();

    stdout.write('원하시는 메뉴의 번호를 입력해주세요.');
    int? input = int.tryParse(stdin.readLineSync() ?? '');

    switch (input) {
      case 1:
        // 내역추가 수입지출
        addLedgerEntry(ledgerEntryList);
        break;

      case 2:
        // 전체내역 보기
        printAllEntries(ledgerEntryList);
        break;

      case 3:
        // 요약 보기 (총 수입/총 지출/잔액)
        printSummary(ledgerEntryList);
        break;

      case 4:
        // 카테고리별 지출 합계 보기
        printExpenseSummaryByCategory(ledgerEntryList);
        break;

      case 0:
        print('가계부 프로그램을 종료합니다.');
        return;

      default:
        print('잘못된 메뉴 입력입니다. 다시 입력해주세요.');
        break;
    }
  }
}

// 메뉴 출력 함수
void printMenu() {
  // 무엇을 도와줄지 선택지를 보여주는 역할
  print('========== 가계부 ==========');
  print('1. 내역 추가 (수입/지출)');
  print('2. 전체 내역 보기');
  print('3. 요약 보기 (총 수입/총 지출/잔액)');
  print('4. 카테고리별 지출 합계 보기');
  print('0. 종료');
  print('============================');
}

// 유틸리티: 공통으로 적용되는 반복되는 기능, 로직, 변환 등을 재사용하게 묶어놓은 함수
// (잘못된 입력을 걸러주는 역할)문자열 입력 받기. addLedgerEntry가 편해지려면 여기를 동작하도록 바꿔준다.
String readNonEmptyString({required String prompt}) {
  // prompt 출력, null/빈 문자열이면 다시 입력받기 옵션 처리. 올바른 입력값을 받도록 처리해야 함
  while (true) {
    // 사용자에게 어떤 값을 입력해야 될지 알려주고
    stdout.write('$prompt: ');
    String input = stdin.readLineSync() ?? '';
    // 입력값이 null이 아닐 때, 공백을 제거했을 때 비어있지 않은지 확인. 스페이스바만 입력한 경우에도
    if (input != null && input.trim().isNotEmpty) {
      // 올바른 입력값이면 반환하고 종료
      return input.trim();
    }
    print('올바르지 않은 입력값입니다. 다시 입력해주세요.');
  }
}

// (유틸) 양수 정수 입력 받기
int readPositiveInt({required String prompt}) {
  // 와일문을 써야됨
  while (true) {
    // prompt 출력, 숫자 변환 실패 or 0 이하이면 다시 입력받기.
    stdout.write('$prompt: ');
    String input = stdin.readLineSync() ?? '';
    // 문자열을 숫자로 변환 가능하게
    int? value = int.tryParse(input);

    if (value != null && value > 0) {
      return value;
    }
    print('올바르지 않은 입력값입니다. 다시 입력해주세요.');
  }
}

// (유틸) 현재 시간을 문자열로 만드는 함수(방법을 모르겠음.)
String? buildNowTimestampString() {
  DateTime now = DateTime.now();
  // padLeft(2, '0')은 "1"월을 "01"월로 맞춰주는 아주 유용한 도구입니다.
  String y = now.year.toString();
  String m = now.month.toString().padLeft(2, '0');
  String d = now.day.toString().padLeft(2, '0');
  String h = now.hour.toString().padLeft(2, '0');
  String min = now.minute.toString().padLeft(2, '0');

  return '$y-$m-$d $h:$min'; // 반환 타입이 String이므로 return이 꼭 필요해요!
}

// 내역 추가 함수.수입/지출 선택-지출. 유틸 필터로 걸러진 깨끗한 데이터를 map으로 만들어 장부 List에 담는다.
void addLedgerEntry(List<Map<String, dynamic>> ledgerEntryList) {
  // 수입/지출 선택
  // 빈 값 불가, 정수만 허용. 리스트에 반환값을 변수에 담아서 넘겨줌!
  // 공통의 유틸리티 활용
  // 사용자가 아래 부분을 입력할 수 있게 만들고, 배열에 추가
  print('1. 수입 / 2. 지출');
  String typeInput = readNonEmptyString(prompt: '유형을 선택하세요. (1번 / 2번)');
  String type = (typeInput == '1') ? '수입' : '지출';
  //
  String title = readNonEmptyString(prompt: '내역명');
  int amount = readPositiveInt(prompt: '금액');
  String category = readNonEmptyString(prompt: '카테고리');
  String? createdAt = buildNowTimestampString(); // 수정해야됨!
  // 유틸함수 사용해서 4가지를 입력받고, 마지막은 현재시간으로 들어갈 수 있게

  // ledgerEntryList에 Map으로 추가
  Map<String, dynamic> addMap = {
    'type': type,
    'title': title,
    'amount': amount,
    'category': category,
    'createdAt': createdAt,
  };
  ledgerEntryList.add(addMap);
  // 사용자가 빈 값이나 잘못된 숫자를 입력했을 때 다시 물어보는 기능
}

// 전체 내역 출력 함수
void printAllEntries(List<Map<String, dynamic>> ledgerEntryList) {
  // 리스트가 비어있을 경우
  if (ledgerEntryList.isEmpty) {
    print('리스트가 비어있습니다.');
    return;
  }
  // 리스트 안의 내역들을 출력해야 함
  print('===== 전체 리스트 내역 =====');
  for (var entry in ledgerEntryList) {
    print(
      '[${entry['createdAt']}] [${entry['type']}] ${entry['title']} | ${entry['category']} | ${entry['amount']}원',
    );
  }
}

// 요약 출력 함수
void printSummary(List<Map<String, dynamic>> ledgerEntryList) {
  // 총 수입, 총 지출, 잔액 계산 및 출력
  // 총 수입은 리스트 안의 amount값들을 더하고, 지출은 빼서 최종 잔액을 계산
  int totalIncome = 0;
  int totalExpence = 0;

  for (var entry in ledgerEntryList) {
    if (entry['type'] == '수입') {
      // totalIncome += entry['amount'] as int; // as: 형변환. dynamic은 뭐든지 담을 수 있는 상자로 연산을 하려면 int타입만 가능하기 때문
      int amount = entry['amount'];
      totalIncome += amount;
    } else if (entry['type'] == '지출') {
      totalExpence += entry['amount'] as int;
    }
  }
}

// 카테고리별 지출 합계 출력 함수
void printExpenseSummaryByCategory(List<Map<String, dynamic>> ledgerEntryList) {
  // 지출(expense)만 대상으로 category별 합산 후 출력
  // map 각 카테고리 이름: 키, 금액: 값
  Map<String, int> categoryTotal = {};

  // 장부 전체의 리스트를 한번 씩 돈다.
  for (var entry in ledgerEntryList) {
    // 지출 내역만 계산
    if (entry['type'] == '지출') {
      // 현재 카테고리에서 이름과 지출만 꺼냄
      String category = entry['category'];
      int amount = entry['amount'] as int;

      // 이 카테고리가 맵에 등록이 되어있는지 확인
      if (categoryTotal.containsKey(category)) {
        // 이미 있다면 기존 금액에 현재 금액을 더한다. !: 확실히 이 값이 존재한다.
        categoryTotal[category] = categoryTotal[category]! + amount;
      } else {
        // 처음 나타난 카테고리면 현재 금액으로 새로 등록함
        categoryTotal[category] = amount;
      }
    }
  }
  // 지출내역이 하나도 없을 시
  if (categoryTotal.isEmpty) {
    print('지출내역이 없습니다.');
    return;
  }
  // 계산이 끝난 맵을 출력함
  categoryTotal.forEach((category, total) {
    print('[$category] : $total원');
  });
}

가계부 프로그램을 통해 배우는 흐름
데이터를 수집(입력) -> 검증(유틸리티) -> 분류/연산(출력)하는 흐름

문제를 풀다가 어떻게 코드를 구현해야 될지 막히는 부분이 많아서 힌트를 보고 풀어냈다..
다시 비슷한 예제를 풀어보자

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

0개의 댓글