AI 개발 · 기본
바이브 코딩 따라하기 - 요청문 그대로 따라 만드는 가계부 앱
월별 보고서 - 날짜로 묶어 합계 내기
datetime 으로 날짜 해석, 잘못된 날짜 거르기, 월별·분류별 표 출력, 정렬 순서 정하기
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 지출에 정해진 분류를 붙이고 잘못된 입력을 거르는 기능을 만들었다. 이제 기록을 날짜순으로 모아 한 달 동안 어디에 얼마를 썼는지 확인한다. 이번 장에서는 날짜처럼 보이는 문자열을 실제 날짜로 검사한 뒤, 같은 달과 같은 분류의 금액을 더한다. 사용자가 키보드로 입력하는 대신 정해진 기록 목록을 사용하므로 실행할 때마다 같은 결과를 얻는다.
- datetime으로 날짜 문자열을 해석하고 존재하지 않는 날짜를 거른다.
- 날짜 표기를 YYYY-MM-DD 형식으로 통일한다.
- 지출을 월별로 묶고 각 월의 분류별 합계를 구한다.
- 월은 시간순으로, 분류는 정해진 목록 순서로 출력한다.
- 경계 날짜와 합계 검사를 통해 AI가 만든 코드를 확인한다.
이번 장에서 만들 것
기록이 몇 건일 때는 목록을 읽으며 지출을 짐작할 수 있다. 기록이 늘어나면 같은 식비라도 서로 다른 달에 흩어지고, 월말과 다음 달 첫날이 섞인다. 날짜 문자열 앞부분만 잘라 묶으면 10월 32일처럼 존재하지 않는 날짜도 보고서에 들어갈 수 있다. 먼저 날짜를 검사하고, 통과한 기록만 합계에 포함해야 한다.
완성할 main.py는 준비된 지출 열 건을 검사한다. 올바른 여섯 건은 보고서에 포함하고, 잘못된 네 건은 원래 입력 번호와 이유를 표시한다. 이어서 9월부터 11월까지 월별 표를 출력한다. 각 표에는 식비, 교통, 생활이 같은 순서로 나타나며 지출이 없는 분류도 0원으로 표시한다.
앞 장의 파일은 필요하지 않다. 분류 검사와 금액 검사를 이번 파일에 포함한다. 저장 파일을 읽는 과정은 이번 목표에 필요하지 않으므로 입력 목록을 코드 안에 둔다. 외부 패키지나 네트워크 연결 없이 Python 3.12 이상에서 실행한다.
| 항목 | 기준 | 확인할 예 |
|---|---|---|
| 날짜 입력 | 존재하는 날짜이며 YYYY-MM-DD 표기 | 2026-10-31은 통과, 2026-10-32는 제외 |
| 월 순서 | 연도와 월이 빠른 순서 | 2026-09, 2026-10, 2026-11 |
| 분류 순서 | 식비, 교통, 생활 | 기록이 들어온 순서와 무관 |
| 합계 대상 | 날짜·분류·금액 검사를 모두 통과한 기록 | 제외된 기록의 금액은 더하지 않음 |
따라하기
아래 단계는 같은 AI 대화에서 차례로 진행한다. 각 단계의 요청문을 보내고, 돌려받은 코드를 main.py에 반영한 뒤 직접 확인한다. AI의 답은 매번 다를 수 있다. 답의 문장이나 함수 이름이 같아야 하는 것은 아니지만, 입력 기준과 최종 출력은 이 장의 약속과 맞아야 한다. 중간 결과가 크게 달라졌다면 뒤의 완성 코드를 기준으로 차이를 살핀다.
단계 1. 보고서의 입력과 출력 약속을 정한다
① 보낼 요청문
Python 3.12 이상 표준 라이브러리만 사용하는 main.py를 만들어 줘. 혼자 쓰는 가계부의 월별 보고서를 만들 것이다. 사용자 입력, 현재 시각, 외부 파일, 네트워크는 사용하지 마. 분류 순서는 식비, 교통, 생활로 고정해 줘. 우선 CATEGORIES 튜플과 빈 main 함수, 직접 실행할 때 main을 호출하는 부분을 작성해 줘. 뒤에서 날짜 검사와 월별 합계를 추가할 예정이다.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 분류 이름을 순서대로 담은 CATEGORIES와 실행 시작점이 있는 짧은 파일을 제시할 것이다. 이 단계에서는 보고서를 출력하지 않는다.
튜플(tuple)은 여러 값을 정해진 순서로 담는 자료형이다. 여기서는 분류 목록을 실행 중에 바꾸지 않겠다는 뜻으로 사용한다. 식비를 첫 칸에 두겠다는 결정은 나중의 표 출력 순서에도 쓰인다.
③ 직접 확인할 것
터미널에서 python3 main.py를 실행한다.
아무것도 출력하지 않고 실행이 끝나면 된다. 입력을 기다리거나 창을 열어서는 안 된다. 변경 전후의 차이(diff)를 확인해 AI가 파일 저장이나 다른 기능까지 덧붙이지 않았는지 살핀다.
④ 체크포인트
한 파일로 실행이 끝나고 분류 순서가 코드에 명시되어 있으면 다음 단계로 간다.
단계 2. 날짜의 존재와 표기를 함께 검사한다
① 보낼 요청문
main.py에 parse_date(text) 함수를 추가해 줘. datetime.strptime으로 YYYY-MM-DD 날짜를 해석하고 date 객체를 반환해 줘. 존재하지 않는 날짜는 ValueError로 알려 줘. 해석한 날짜를 같은 형식의 문자열로 되돌려 원본과 비교하고, 2026-2-03처럼 자릿수가 다른 입력도 거절해 줘. 날짜 관련 오류 문구는 '날짜는 YYYY-MM-DD 형식의 실제 날짜여야 한다'로 통일해 줘.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 날짜를 해석하는 부분과 다시 문자열로 만들어 비교하는 부분을 가진 함수를 제시할 것이다. 날짜를 해석하지 못했을 때는 정해진 오류 문구로 바꿔 전달할 것이다.
문자열(string)은 글자를 순서대로 담은 값이다. 날짜 문자열만으로는 달력에 그날이 있는지 알 수 없다. datetime.strptime은 지정한 형식으로 문자열을 읽으며 날짜의 유효성도 검사한다. 뒤의 .date()는 시각 부분이 없는 날짜 값만 꺼낸다.
여기서 %Y는 네 자리 연도, %m은 월, %d는 일을 뜻한다. 다만 해석에 성공했다는 사실만으로 입력 자릿수까지 요구와 같다고 판단하면 안 된다. strftime으로 다시 만든 문자열과 원본을 비교하면 날짜가 맞더라도 표기가 다른 입력을 거를 수 있다.
③ 직접 확인할 것
터미널에서 python3 -c 'from main import parse_date; print(parse_date("2026-10-31"))'을 실행한다.
2026-10-31이 출력되어야 한다. 같은 명령에서 날짜를 2026-10-32로 바꾸면 ValueError와 약속한 문구가 나타나야 한다. 2026-2-03으로 바꿔도 거절되어야 한다. 이 두 오류 실행은 검사 동작을 확인하기 위해 일부러 수행하는 것이다.
④ 체크포인트
올바른 날짜는 반환하고, 없는 날짜와 자릿수가 다른 날짜는 모두 거르면 다음 단계로 간다.
단계 3. 통과한 기록과 제외한 기록을 나눈다
① 보낼 요청문
prepare_records(rows)를 추가해 줘. 각 입력은 날짜 문자열, 분류, 정수 금액의 세 값으로 된 튜플이다. 날짜, 분류, 금액 순서로 검사해 줘. 분류는 CATEGORIES 안에 있어야 하고 금액은 양의 정수여야 한다. 통과한 기록에는 날짜 객체를 넣어 줘. 실패한 기록은 입력 번호와 이유를 errors에 담고 다음 기록을 계속 검사해 줘. 반환값은 records, errors로 해 줘. 모든 오류를 한꺼번에 숨기는 처리는 넣지 마.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 각 기록을 하나씩 검사하고 ValueError가 생긴 기록만 errors에 담는 코드를 제시할 것이다. 통과한 기록은 records에 쌓아 두 목록을 함께 반환할 것이다.
예외(exception)는 처리 중 문제가 생겼다는 신호다. 여기서는 예상한 입력 오류를 ValueError로 알린다. try 안에서 검사하고 except ValueError에서 그 기록의 실패 이유를 받아 둔다. 코드 오타까지 입력 오류로 취급하지 않도록 모든 종류의 예외를 잡지는 않는다.
③ 직접 확인할 것
터미널에서 python3 -c 'from main import prepare_records; print(prepare_records([("2026-10-01", "식비", 9000), ("2026-10-32", "식비", 5000)])[1])'을 실행한다.
오류 목록에는 2번 입력과 날짜 오류 문구만 있어야 한다. 이어서 잘못된 분류와 0원도 각각 넣어 본다. 분류 오류에는 '허용하지 않은 분류: 취미', 금액 오류에는 '금액은 양의 정수여야 한다'가 들어가야 한다.
④ 체크포인트
잘못된 기록 뒤에 올바른 기록을 놓아도 검사가 이어지고, 오류 번호가 원래 입력 위치를 가리키면 다음 단계로 간다.
단계 4. 월과 분류를 기준으로 금액을 더한다
① 보낼 요청문
build_report(records)를 추가해 줘. 검사된 날짜에서 strftime("%Y-%m")으로 월 이름을 만들어 줘. 처음 만난 월에는 CATEGORIES의 모든 분류를 0원으로 준비해 줘. 같은 월의 같은 분류에 금액을 누적하고 월별 사전을 반환해 줘. 입력 기록을 직접 바꾸거나 출력하는 일은 하지 마.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 월을 바깥쪽 이름표로, 분류를 안쪽 이름표로 쓰는 사전을 제시할 것이다. 해당 칸의 기존 금액에 새 금액을 더하는 구조가 된다.
사전(dictionary)은 이름표인 키에 값을 연결하는 자료형이다. 이번 보고서에서는 2026-10이라는 키로 그달의 분류별 금액을 찾는다. 다시 식비라는 키를 사용하면 식비 합계를 얻는다. 이처럼 두 번 찾아 들어가므로 월과 분류가 서로 섞이지 않는다.
③ 직접 확인할 것
터미널에서 python3 -c 'from main import prepare_records, build_report; records, errors = prepare_records([("2026-10-01", "식비", 9000), ("2026-10-31", "식비", 11000)]); print(build_report(records)["2026-10"]["식비"])'을 실행한다.
20000이 출력되어야 한다. 11000만 나오면 기존 금액을 새 금액으로 덮어쓴 것이다. 교통과 생활 칸도 확인하면 각각 0이어야 한다. 이번에는 기록이 존재하는 월만 만들며, 지출이 전혀 없는 달을 새로 만드는 기능은 넣지 않는다.
④ 체크포인트
같은 달의 같은 분류는 더해지고 다른 분류는 따로 유지되면 다음 단계로 간다.
단계 5. 월 순서와 분류 순서를 고정한다
① 보낼 요청문
print_report(report)를 추가해 줘. 월은 sorted(report) 순서로 출력하고, 분류는 CATEGORIES 순서로 출력해 줘. 월 제목은 대괄호로 감싸고 다음 줄에 '분류 | 금액(원)'을 써 줘. 금액에는 천 단위 쉼표를 붙여 줘. 각 월 끝에는 월 합계를, 전체 끝에는 전체 합계를 출력해 줘. 기록이 없으면 '보고할 지출이 없다' 한 줄만 출력해 줘.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 월 이름을 정렬하는 반복문 안에서 고정된 분류 목록을 순회하는 코드를 제시할 것이다. 월 합계를 더해 전체 합계도 계산할 것이다.
sorted는 값을 정렬한 새 목록을 만든다. 월 문자열은 네 자리 연도와 두 자리 월로 통일했으므로 문자열 순서가 시간순과 일치한다. 한편 분류는 가나다순으로 정렬하지 않는다. 독자가 매번 같은 위치에서 식비를 찾을 수 있도록 앞에서 정한 목록을 그대로 따른다.
③ 직접 확인할 것
터미널에서 python3 -c 'from main import print_report; print_report({"2026-11": {"식비": 0, "교통": 3000, "생활": 0}, "2026-09": {"식비": 12000, "교통": 0, "생활": 0}})'을 실행한다.
먼저 2026-09, 다음에 2026-11이 나와야 한다. 두 표 모두 식비, 교통, 생활 순서여야 한다. 전체 합계는 15,000원이어야 한다. 사전에 넣은 순서와 보고서 순서가 다르다는 점을 직접 확인한다.
④ 체크포인트
입력 순서가 바뀌어도 표의 순서와 합계가 유지되면 다음 단계로 간다.
단계 6. 고정 시연과 검사를 붙여 완성한다
① 보낼 요청문
지금까지의 함수를 한 파일로 합쳐 줘. 이 장의 완성 코드에 있는 열 건의 입력을 같은 순서로 사용해 줘. run_checks에서 윤년의 2월 29일, 평년의 잘못된 2월 29일, 잘못된 날짜 표기, 빈 기록, 월 경계, 입력 순서 변경을 assert로 검사해 줘. 시연에서는 제외 사유, 통과 건수, 확인 통과 문구, 월별 표를 출력해 줘. 월별 예상 합계는 12,000원, 41,500원, 3,000원이며 전체는 56,500원이다. 최종 코드와 앞 단계 코드의 주요 차이도 설명해 줘.
② AI가 돌려줄 내용의 요지
AI의 답은 매번 다를 수 있다. 독립 실행 가능한 main.py와 검사 함수를 제시할 것이다. 날짜를 먼저 검사하는지, 월을 정렬하는지, 제외 금액이 합계에서 빠지는지 확인할 위치도 설명할 것이다.
assert는 뒤의 조건이 참인지 확인하는 문장이다. 조건이 거짓이면 실행을 멈춰 예상과 실제가 다름을 알린다. 이번에는 이 기능으로 입력 경계와 합계를 확인한다. 사용자의 잘못된 입력을 거르는 일은 계속 ValueError가 담당한다.
③ 직접 확인할 것
터미널에서 python3 -W error -m py_compile main.py를 실행한다. 이어서 python3 main.py를 실행한다.
첫 명령은 실행하지 않고 문법을 검사하며, 경고도 오류로 취급한다. 출력 없이 끝나면 다음 명령으로 시연을 실행한다. 결과는 뒤의 실행 결과와 같아야 한다. 코드를 바꿨다면 차이에서 날짜 형식, 더하기 연산, 정렬 기준, 오류 처리 범위를 먼저 읽는다. 실행 결과가 맞더라도 필요 없는 기능이 추가되었는지 살핀다.
④ 체크포인트
문법 검사와 내부 검사를 통과하고, 제외 네 건과 전체 합계 56,500원을 확인하면 이 장의 작업이 끝난다.
완성 코드
아래 내용을 main.py로 저장한다. 함수는 이름을 붙여 둔 처리 묶음이다. 위쪽 함수부터 읽으면 검사, 집계, 출력이 어떻게 연결되는지 따라갈 수 있다. 실제 시연은 맨 아래에서 main을 호출할 때 시작한다.
from datetime import date, datetime
CATEGORIES = ("식비", "교통", "생활")
def parse_date(text: str) -> date:
message = "날짜는 YYYY-MM-DD 형식의 실제 날짜여야 한다"
try:
parsed = datetime.strptime(text, "%Y-%m-%d").date()
except ValueError:
raise ValueError(message) from None
if parsed.strftime("%Y-%m-%d") != text:
raise ValueError(message)
return parsed
def prepare_records(rows):
records = []
errors = []
for number, (text, category, amount) in enumerate(rows, start=1):
try:
day = parse_date(text)
if category not in CATEGORIES:
raise ValueError(f"허용하지 않은 분류: {category}")
if type(amount) is not int or amount <= 0:
raise ValueError("금액은 양의 정수여야 한다")
except ValueError as error:
errors.append((number, str(error)))
continue
records.append((day, category, amount))
return records, errors
def build_report(records):
report = {}
for day, category, amount in records:
month = day.strftime("%Y-%m")
if month not in report:
report[month] = {name: 0 for name in CATEGORIES}
report[month][category] += amount
return report
def print_report(report):
if not report:
print("보고할 지출이 없다")
return
grand_total = 0
for month in sorted(report):
print(f"\n[{month}]")
print("분류 | 금액(원)")
for category in CATEGORIES:
print(f"{category} | {report[month][category]:,}")
month_total = sum(report[month].values())
grand_total += month_total
print(f"월 합계 | {month_total:,}")
print(f"\n전체 합계 | {grand_total:,}")
def run_checks():
assert parse_date("2024-02-29") == date(2024, 2, 29)
for text in ("2026-02-29", "2026-10-32", "2026-2-03"):
try:
parse_date(text)
except ValueError:
pass
else:
raise AssertionError(f"거절해야 하는 날짜: {text}")
assert prepare_records([]) == ([], [])
assert build_report([]) == {}
rows = [
("2026-11-01", "교통", 3000),
("2026-10-31", "식비", 11000),
("2026-10-01", "식비", 9000),
]
records, errors = prepare_records(rows)
assert errors == []
report = build_report(records)
assert sorted(report) == ["2026-10", "2026-11"]
assert report["2026-10"] == {
"식비": 20000, "교통": 0, "생활": 0
}
assert report["2026-11"]["교통"] == 3000
assert build_report(list(reversed(records))) == report
def main():
run_checks()
rows = [
("2026-09-30", "식비", 12000),
("2026-10-01", "식비", 9000),
("2026-10-03", "교통", 1500),
("2026-10-15", "생활", 20000),
("2026-10-31", "식비", 11000),
("2026-11-01", "교통", 3000),
("2026-10-32", "식비", 5000),
("2026-2-03", "교통", 7000),
("2026-10-20", "취미", 8000),
("2026-10-21", "생활", 0),
]
records, errors = prepare_records(rows)
report = build_report(records)
assert len(records) == 6
assert [number for number, _ in errors] == [7, 8, 9, 10]
assert [sum(report[month].values()) for month in sorted(report)] == [
12000, 41500, 3000
]
assert sum(amount for _, _, amount in records) == 56500
for number, reason in errors:
print(f"제외 {number}번: {reason}")
print(f"통과 {len(records)}건 / 제외 {len(errors)}건")
print("확인 통과")
print_report(report)
if __name__ == "__main__":
main()
줄별 해설
첫 줄은 날짜만 나타내는 date와 날짜를 해석하는 데 사용할 datetime을 가져온다. parse_date의 text: str은 문자열을 받고, -> date는 날짜 값을 반환한다는 표시다. 이 표시는 코드를 읽는 사람을 돕는다. 표시 자체가 입력을 검사하지는 않는다. 이번 입력 목록의 날짜 칸은 모두 문자열이라는 전제로 작성했다.
parse_date의 try 안에서는 실제 날짜 해석을 시도한다. 날짜가 존재하지 않으면 ValueError가 발생한다. from None은 오류를 다시 알릴 때 앞선 오류의 연결 설명을 생략하게 한다. 이렇게 하면 날짜 입력 오류를 같은 문구로 전달할 수 있다. 이어지는 문자열 비교는 표기 검사를 담당한다. 두 검사를 모두 통과한 parsed만 반환한다.
prepare_records의 enumerate는 기록과 함께 번호를 꺼낸다. start=1을 주었으므로 독자에게 보여 주는 번호는 1부터 시작한다. 괄호 안의 text, category, amount는 한 기록의 세 값을 각각 받는다. 이번 코드는 이 세 칸이 갖춰진 입력을 대상으로 한다. 칸 수가 틀린 임의 자료까지 처리하는 기능은 포함하지 않는다.
분류 검사 다음의 type(amount) is not int는 금액이 정수인지 확인한다. 뒤의 amount <= 0은 0원과 음수를 거른다. Python에서는 참과 거짓을 나타내는 값도 일부 정수 검사에 통과할 수 있으므로, 여기서는 정확히 정수 자료형인지 확인한다. 금액을 원 단위 정수로 유지하면 이번 합계에 소수 계산이 필요하지 않다.
except 안의 continue는 현재 기록 처리를 끝내고 다음 기록으로 넘어간다. 따라서 오류가 난 기록은 아래의 records.append에 도달하지 않는다. 통과 목록에는 원래 날짜 문자열 대신 해석한 날짜를 넣는다. 집계할 때 같은 날짜를 다시 해석할 필요가 없어진다.
build_report의 month는 연도와 월을 함께 가진 문자열이다. 월 숫자만 사용하면 서로 다른 해의 같은 달이 섞이므로 연도도 포함한다. 월을 처음 만났을 때 만드는 사전은 모든 분류의 값을 0으로 시작한다. +=는 현재 값에 새 금액을 더해 다시 저장한다. 보고서 생성 함수에는 print가 없어 계산 결과를 별도로 검사하기 쉽다.
print_report의 첫 조건은 빈 보고서를 처리한다. 기록이 없다는 안내를 출력한 뒤 return으로 함수를 끝낸다. 기록이 있으면 sorted(report)가 월 이름을 시간순으로 제공한다. 안쪽 반복문은 사전의 저장 순서를 따르지 않고 CATEGORIES를 사용한다. 월 순서와 분류 순서를 서로 다른 기준으로 정한 것이다.
금액 뒤의 :,는 숫자를 천 단위 쉼표가 있는 문자열로 표시한다. 실제 저장된 금액은 계속 정수다. sum은 여러 숫자를 더한다. 월별 사전의 values()로 금액만 꺼내 월 합계를 구하고, 그 값을 grand_total에 더한다. 월 제목 앞의 \n은 빈 줄을 한 줄 넣어 표를 구분한다.
run_checks는 날짜 경계와 집계 규칙을 확인한다. 날짜 검사 반복문의 else는 try 안에서 예외가 생기지 않았을 때 실행된다. 거절해야 할 날짜를 함수가 받아들이면 AssertionError를 발생시킨다. 마지막 검사는 기록 순서를 뒤집어도 집계 결과가 같은지 확인한다.
main에서는 시연 자료의 결과도 검사한다. 통과 건수와 제외 번호, 월 합계, 통과 금액의 전체 합계를 각각 비교한다. 서로 다른 확인 기준을 두면 건수만 맞고 합계가 틀린 경우도 발견할 수 있다. 마지막 조건문은 이 파일을 직접 실행할 때만 main을 호출한다. 앞 단계에서 함수를 가져와 확인할 때 전체 보고서가 함께 출력되지 않는 이유다.
실행 결과
main.py가 있는 폴더에서 다음 명령을 차례로 실행한다. 명령은 터미널에 입력하는 내용이며 Python 코드에 붙여 넣지 않는다.
python3 -W error -m py_compile main.py
python3 main.py
첫 명령은 성공하면 출력이 없다. 두 번째 명령의 예상 출력은 다음과 같다. '확인 통과'는 코드에 적은 검사 조건을 만족했다는 뜻이다. 날짜와 합계에 관한 모든 경우를 검사했다는 뜻으로 넓혀 읽지 않는다.
제외 7번: 날짜는 YYYY-MM-DD 형식의 실제 날짜여야 한다
제외 8번: 날짜는 YYYY-MM-DD 형식의 실제 날짜여야 한다
제외 9번: 허용하지 않은 분류: 취미
제외 10번: 금액은 양의 정수여야 한다
통과 6건 / 제외 4건
확인 통과
[2026-09]
분류 | 금액(원)
식비 | 12,000
교통 | 0
생활 | 0
월 합계 | 12,000
[2026-10]
분류 | 금액(원)
식비 | 20,000
교통 | 1,500
생활 | 20,000
월 합계 | 41,500
[2026-11]
분류 | 금액(원)
식비 | 0
교통 | 3,000
생활 | 0
월 합계 | 3,000
전체 합계 | 56,500
10월 식비는 9,000원과 11,000원을 더한 20,000원이다. 여기에 교통 1,500원과 생활 20,000원을 더하면 41,500원이다. 제외된 5,000원, 7,000원, 8,000원은 어느 월에도 들어가지 않는다. 출력이 다르면 기대 숫자를 실제 출력에 맞춰 고치지 말고, 처음 다른 부분부터 입력과 처리 코드를 대조한다.
실무에서 자주 틀리는 것
날짜를 확인하기 전에 앞부분만 자른다
다음 코드는 실행되지만 잘못된 날짜를 10월로 분류한다. 문자열을 자르는 일은 달력 검사가 아니다.
text = "2026-10-32"
print(text[:7])
고친 코드는 날짜를 해석한 뒤 월을 얻는다. 잘못된 날짜가 들어오면 월을 만들기 전에 오류가 발생한다.
from datetime import datetime
text = "2026-10-31"
day = datetime.strptime(text, "%Y-%m-%d").date()
print(day.strftime("%Y-%m"))
완성 코드에서는 여기에 원본 표기와의 비교도 추가했다. 실제 날짜인지와 약속한 형식인지를 각각 확인해야 한다.
누적해야 할 금액을 덮어쓴다
다음 코드는 마지막 금액만 남겨 11,000원을 출력한다. 기록이 한 건일 때는 문제가 보이지 않는다.
total = 0
for amount in (9000, 11000):
total = amount
print(total)
고친 코드는 기존 값에 새 금액을 더한다. 같은 분류 두 건을 준비한 검사가 이 차이를 드러낸다.
total = 0
for amount in (9000, 11000):
total += amount
print(total)
자릿수가 다른 월 이름을 정렬한다
다음 코드는 10월을 9월보다 먼저 출력한다. 글자 순서로 비교할 때 1이 9보다 앞서기 때문이다.
months = ["2026-9", "2026-10"]
print(sorted(months))
고친 코드는 날짜에서 같은 자릿수의 월 이름을 만든다. 연도도 포함하므로 해가 바뀌어도 구별된다.
from datetime import date
days = [date(2026, 9, 1), date(2026, 10, 1)]
months = [day.strftime("%Y-%m") for day in days]
print(sorted(months))
각 월이 같은 분류 사전을 함께 쓰게 한다
다음 코드는 두 월에 같은 사전을 연결한다. 10월 식비를 바꾸면 11월 식비도 9,000원으로 보인다.
base = {"식비": 0, "교통": 0, "생활": 0}
report = {"2026-10": base, "2026-11": base}
report["2026-10"]["식비"] += 9000
print(report["2026-11"]["식비"])
고친 코드는 월마다 새 사전을 만든다. 완성 코드도 새로운 월을 만날 때마다 이 작업을 수행한다.
categories = ("식비", "교통", "생활")
report = {}
for month in ("2026-10", "2026-11"):
report[month] = {name: 0 for name in categories}
report["2026-10"]["식비"] += 9000
print(report["2026-11"]["식비"])
한눈에 보기
| 처리 | 사용한 방법 | 확인 기준 |
|---|---|---|
| 날짜 해석 | datetime.strptime | 없는 날짜를 거절한다 |
| 표기 검사 | strftime 결과와 원본 비교 | 2026-2-03을 거절한다 |
| 월 구분 | %Y-%m | 연도가 다른 같은 달을 분리한다 |
| 분류별 누적 | 월별 사전의 금액에 += | 식비 두 건이 20,000원이다 |
| 출력 순서 | 월은 sorted, 분류는 CATEGORIES | 입력 순서를 바꿔도 순서가 같다 |
| 결과 검사 | assert와 예상 출력 대조 | 월 합계와 전체 합계가 맞는다 |
이 보고서는 통과한 기록이 있는 월만 표시한다. 그 월 안에서는 지출이 없는 분류도 0원으로 표시한다. 두 규칙을 구별해 두면 빈칸과 누락을 혼동하지 않는다. 다음 장에서는 이렇게 구한 분류별 합계를 한도와 비교하는 데 사용한다.
연습 문제
- 시연 입력에 2026-10-31, 교통, 2,500원 기록을 추가한다. 통과 건수, 10월 교통, 10월 합계, 전체 합계를 먼저 종이에 적고 코드를 수정한다. 관련 assert도 바꾼다.
- 2026-12-31의 식비 4,000원과 2027-01-01의 식비 6,000원만 넣은 별도 자료로 보고서를 만든다. 두 기록이 서로 다른 월에 들어가는지 확인하는 assert를 작성한다.
- 빈 목록을 prepare_records, build_report, print_report에 차례로 전달한다. 화면에 어떤 문구가 나와야 하는지 예상하고 확인한다. 빈 입력일 때 합계 표를 출력해야 하는지도 현재 기준과 대조한다.
- 처음 만난 날짜에 0원 분류를 미리 준비하는 부분을 지워 달라는 AI의 제안을 받았다. 그대로 반영하면 어떤 출력 규칙에 영향이 생기는지 설명하고, 유지해야 할 동작을 담은 요청문을 작성한다.
정답과 해설
1. 통과 건수는 7건, 10월 교통은 4,000원, 10월 합계는 44,000원, 전체 합계는 59,000원이다. 기존 오류 번호를 유지하려면 새 기록을 목록 맨 뒤에 추가한다. len(records)의 기대값과 월 합계 목록, 전체 합계 기대값을 바꾼다. 오류 네 건은 그대로 남아야 한다. 입력을 늘렸다는 이유로 검사를 지우면 이후 변경을 확인할 기준도 줄어든다.
2. 연도를 포함한 월 이름을 사용하므로 두 달은 분리된다. 다음 코드는 main.py의 함수를 가져와 별도로 실행할 수 있는 확인 코드다.
from main import build_report, prepare_records
rows = [
("2027-01-01", "식비", 6000),
("2026-12-31", "식비", 4000),
]
records, errors = prepare_records(rows)
report = build_report(records)
assert errors == []
assert sorted(report) == ["2026-12", "2027-01"]
assert report["2026-12"]["식비"] == 4000
assert report["2027-01"]["식비"] == 6000
print("연도 경계 확인 통과")
3. '보고할 지출이 없다' 한 줄이 출력된다. 날짜가 없는 상태에서 현재 달을 임의로 만들지 않는다. 이번 보고서는 입력에 나타난 월만 대상으로 하기 때문이다. 아래 코드에서 records와 errors는 빈 목록이며 report는 빈 사전이다.
from main import build_report, prepare_records, print_report
records, errors = prepare_records([])
report = build_report(records)
assert records == []
assert errors == []
assert report == {}
print_report(report)
4. 0원 칸을 없애면 지출이 없는 분류를 출력할 때 키를 찾지 못할 수 있다. 출력 코드도 함께 바꾸더라도 모든 분류를 같은 순서로 보여 준다는 약속을 지켜야 한다. 코드를 짧게 만들자는 제안만으로 출력 기준을 바꾸지 않는다. 다음처럼 요청할 수 있다.
각 월의 표에는 식비, 교통, 생활이 항상 이 순서로 나와야 한다. 기록이 없는 분류도 0원으로 표시해 줘. 월마다 별도의 분류 사전을 만들고, 기존 날짜 검사와 월 합계 검사도 유지해 줘. 수정 전후 차이에서 이 동작을 유지하는 부분을 설명해 줘.
AI가 수정한 뒤에는 다시 문법 검사와 실행을 수행한다. 월에 기록이 한 건뿐인 9월과 11월 표를 보면 0원 분류의 유지 여부를 쉽게 확인할 수 있다. 요청문에 기준을 적고, 코드 차이를 읽고, 정해진 결과와 비교하는 과정까지가 이번 기능을 만드는 작업이다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.