이어서 라이브 코딩을 조금씩 난이도를 올려서 진행해보도록 했다.
조금 더 빠르게 코드를 구현하고, 문제에 대한 해결능력이 올라가고 있는 것 같아서 기쁘다.
260213 금요일
🎯 목표
🧩 프로그램 동작 방식
메뉴를 반복해서 보여준다.
===== TODO 프로그램 =====
1. 할 일 추가
2. 할 일 목록 보기
3. 할 일 삭제
사용자가 번호를 입력하면 해당 기능을 실행한다.
0을 입력하면 프로그램을 종료한다.
1️⃣ 할 일 추가
예시:
추가할 할 일을 입력하세요: 운동하기
2️⃣ 할 일 목록 보기
예시:
1. 운동하기
2. 책 읽기
3. 코딩 공부하기
3️⃣ 할 일 삭제
예시:
삭제할 번호를 입력하세요: 2
📌 구현 조건
📌 main 함수에는 흐름만 작성할 것
void main() {
List<String> todoList = [];
runTodoProgram(todoList);
}
📌 전체 프로그램을 실행하는 함수
void runTodoProgram(List<String> todoList) {
// TODO: 구현
throw UnimplementedError();
}
📌 할 일 추가 함수
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;
}
}
인덱스 시스템과 맞지 않는다.
잘못된 메서드 사용:
todoList.indexWhere((list) => list.length == number) 코드는
"할 일 글자 수가 내가 입력한 숫자와 같은 것"을 찾으라는 명령으로 인식한다.
예를 들어 사용자가 1을 입력하면, 할 일 목록 중 글자 수가 1글자인 것의 위치를 찾게 됨
인덱스 범위 비교:
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);로 코드를 작성했더니 입력한 넘버와 실제 인덱스의 위치와 맞지 않아서 코드가 정상작동하지 않았었다.
항상 특성에 대해 고려하고 적용할 수 있도록 주의해야겠다. ***
🎯 목표
🧩 프로그램 동작 방식
메뉴를 반복해서 보여준다.
========== 가계부 ==========
1. 내역 추가 (수입/지출)
2. 전체 내역 보기
3. 요약 보기 (총 수입/총 지출/잔액)
4. 카테고리별 지출 합계 보기
📦 저장 구조 (클래스 없이! 난이도는 조금만 올림)
각 내역(Map)은 아래 키를 반드시 가진다:
※ 날짜는 실제 DateTime을 써도 되지만, 초급+라서 문자열로 저장해도 OK
1️⃣ 내역 추가 (수입/지출)
입력 흐름:
예시:
유형을 선택하세요 (1:수입, 2:지출): 2
제목을 입력하세요: 점심
금액을 입력하세요: 9000
카테고리를 입력하세요(예: 식비/교통/쇼핑): 식비
저장 완료!
2️⃣ 전체 내역 보기
저장된 모든 내역을 번호와 함께 출력한다.
예시 출력 포맷(자유):
1) [지출] 점심 - 9000원 (식비) / 2026-02-13 14:20
내역이 없으면:
"저장된 내역이 없습니다."
3️⃣ 요약 보기
예시:
총 수입: 3000000원
총 지출: 45000원
잔액: 2955000원
4️⃣ 카테고리별 지출 합계 보기 (난이도 쪼~끔 업!)
예시:
[카테고리별 지출]
식비: 18000원
교통: 2500원
쇼핑: 15000원
📌 구현 조건
📌 main 함수에는 흐름만 작성할 것
void main() {
final List<Map<String, dynamic>> ledgerEntryList = [];
runLedgerProgram(ledgerEntryList);
}
📌 프로그램 루프 함수
void runLedgerProgram(List<Map<String, dynamic>> ledgerEntryList) {
// TODO: 구현
throw UnimplementedError();
}
📌 메뉴 출력 함수
void printMenu() {
// TODO: 구현
throw UnimplementedError();
}
📌 내역 추가 함수
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();
}
📌 카테고리별 지출 합계 출력 함수
void printExpenseSummaryByCategory(List<Map<String, dynamic>> ledgerEntryList) {
// TODO: 구현
throw UnimplementedError();
}
📌 (유틸) 문자열 입력 받기
String readNonEmptyString({required String prompt}) {
// TODO: 구현
throw UnimplementedError();
}
📌 (유틸) 양수 정수 입력 받기
int readPositiveInt({required String prompt}) {
// TODO: 구현
throw UnimplementedError();
}
📌 (유틸) 현재 시간을 문자열로 만드는 함수
String buildNowTimestampString() {
// TODO: 구현
throw UnimplementedError();
}
- 유틸리티 함수 제작. 데이터를 먼저 필터로 거른 뒤
- 맵을 채우고 등 함수 완성 후
- 스위치문에 연결하기
코드를 이해하는 3가지 포인트
데이터의 흐름:
addLedgerEntry에서 Map을 만들어 ledgerEntryList에 넣고, 다른 함수(printAllEntries, printSummary 등)에서는 그 리스트를 돌면서 데이터를 계산하거나 보여줍니다.
유틸리티 함수의 힘:
readNonEmptyString과 readPositiveInt는 while(true)를 사용하여 사용자가 올바른 값을 입력할 때까지 계속 물어봅니다. 이 덕분에 프로그램이 숫자가 아닌 값 때문에 갑자기 꺼지는 것을 방지합니다.
Map 다루기:
entry['amount']처럼 Key 값을 사용해 원하는 정보를 꺼내옵니다.
반복적으로 사용되는 로직, 공통 기능, 데이터 변환, 검증 등의 작업을 재사용 가능하게 묶어 놓은 클래스나 함수(라이브러리/패키지)를 의미
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원');
});
}
가계부 프로그램을 통해 배우는 흐름
데이터를 수집(입력) -> 검증(유틸리티) -> 분류/연산(출력)하는 흐름
문제를 풀다가 어떻게 코드를 구현해야 될지 막히는 부분이 많아서 힌트를 보고 풀어냈다..
다시 비슷한 예제를 풀어보자