Lv2 · 주소록 · Lv2 · BMI 추적기 · Lv3 · 가계부 · Lv3 · 도서 관리 시스템
이름·전화·이메일을 저장하고 부분 일치 검색을 지원하는 주소록. dict 안의 dict 패턴을 다룹니다. 여러 명의 키/몸무게를 입력 받아 BMI 계산, 분류, 정렬된 표로 출력. f-string 정렬과 분류별 집계까지 다룹니다. 수입/지출 기록을 JSON 으로 영구 저장. 카테고리별 합계, 잔액, 막대그래프 시각화까지 포함합니다. Book/Member/Library 클래스로 도서 등록·대출·반납·검색을 구현. dataclass 와 JSON 직렬화까지 통합합니다.
이 강의에서 배우는 것
- 1이름을 키로 한 중첩 dict 를 다룬다
- 2부분 일치 검색을 case-insensitive 로 한다
- 3정렬된 list 를 출력한다
- 4BMI 공식: weight / (height/100) ** 2
- 5구간별 분류 함수를 작성한다
- 6f-string 정렬로 표를 만든다
- 7JSON 으로 영구 저장
- 8카테고리별 집계와 잔액 계산
- 9잘못된 입력에 대한 친절한 예외 처리
- 10Book / Member / Library 도메인 분리
- 11asdict 로 dataclass → JSON
- 12대출·반납 시 양쪽 상태 일관성
Lv2 · 주소록
이름·전화·이메일을 저장하고 부분 일치 검색을 지원하는 주소록. dict 안의 dict 패턴을 다룹니다.
데이터 모델
{"이름": {"phone": "...", "email": "..."}}명령
- add
- find <키워드>
- del <name>
- list
- quit
도전 과제
- 같은 이름 처리 (자동 번호)
- 전화번호 형식 검증
채점 체크리스트
- 부분 일치
- 정렬 출력
- 잘못된 명령 안전
Lv2 · BMI 추적기
여러 명의 키/몸무게를 입력 받아 BMI 계산, 분류, 정렬된 표로 출력. f-string 정렬과 분류별 집계까지 다룹니다.
프로젝트 개요
- 이름 자리에 done 입력 시 입력 종료
- 분류: 저체중(<18.5), 정상(18.5~23), 과체중(23~25), 비만(>=25)
- BMI 내림차순 정렬 표 + 분류별 집계
도전 과제
- 분류별 인원 집계
- CSV 저장
채점 체크리스트
- 입력 검증
- 정렬 정확
- 분류 함수
Lv3 · 가계부
수입/지출 기록을 JSON 으로 영구 저장. 카테고리별 합계, 잔액, 막대그래프 시각화까지 포함합니다.
프로젝트 개요
- ledger.json 에 기록 영구 저장
- 명령: add / list / summary / del <번호> / quit
- summary: 카테고리별 합계 + 막대그래프
도전 과제
- 월별 집계 (2026-05 필터)
- 한 줄 명령 (add 지출 5000 식비 점심)
채점 체크리스트
- JSON 손상 안전 처리
- 예외 처리
- 막대그래프 출력
Lv3 · 도서 관리 시스템
Book/Member/Library 클래스로 도서 등록·대출·반납·검색을 구현. dataclass 와 JSON 직렬화까지 통합합니다.
도메인
- Book(title, author, isbn, is_borrowed, borrower)
- Member(member_id, name, borrowed: list[str])
- Library: 두 컬렉션 + load/save
명령
- book add / book list
- member add
- borrow <isbn> <member_id>
- return <isbn>
- search <키워드>
도전 과제
- 대출 기간/연체 일수 (datetime)
- 회원별 대출 이력
채점 체크리스트
- 3개 클래스 분리
- JSON 영구 저장
- 대출 일관성
💻 예제 (examples)
실제로 실행해 결과를 확인할 수 있는 예제 코드입니다.
"""주소록 — 부분 일치 검색"""
contacts = {}
def add():
name = input("이름: ").strip()
if not name: return
phone = input("전화: ").strip()
email = input("이메일: ").strip()
contacts[name] = {"phone": phone, "email": email}
print("추가됨")
def find(query):
matched = [(n, c) for n, c in contacts.items() if query.lower() in n.lower()]
if not matched:
print("없음"); return
for name, c in matched:
print(f" {name} | {c['phone']} | {c['email']}")
def show_all():
for name in sorted(contacts):
c = contacts[name]
print(f" {name}: {c['phone']}, {c['email']}")
def main():
while True:
raw = input("\n명령(add/find <q>/del <name>/list/quit): ").strip()
if not raw: continue
parts = raw.split(maxsplit=1)
cmd = parts[0]
arg = parts[1] if len(parts) > 1 else ""
if cmd == "quit": break
elif cmd == "add": add()
elif cmd == "find":
if arg: find(arg)
else: print("키워드 입력")
elif cmd == "del":
if arg in contacts:
del contacts[arg]; print("삭제")
else:
print("없음")
elif cmd == "list":
show_all()
if __name__ == "__main__":
main()명령(add/find <q>/del <name>/list/quit): add
이름: 홍길동
전화: 010-1234-5678
이메일: hong@x.com
추가됨
명령: find 길동
홍길동 | 010-1234-5678 | hong@x.com"""BMI 추적기 — 다인원 + 정렬 표"""
def classify(bmi):
if bmi < 18.5: return "저체중"
if bmi < 23: return "정상"
if bmi < 25: return "과체중"
return "비만"
def main():
people = []
print("이름 자리에 'done' 입력 시 종료")
while True:
name = input("\n이름: ").strip()
if name.lower() == "done": break
try:
h = float(input("키(cm): "))
w = float(input("몸무게(kg): "))
except ValueError:
print("숫자만"); continue
bmi = w / (h / 100) ** 2
people.append({"name": name, "h": h, "w": w, "bmi": bmi})
if not people:
return
people.sort(key=lambda p: -p["bmi"])
print(f"\n{'이름':<10}{'키':>7}{'몸무게':>9}{'BMI':>9} 분류")
for p in people:
print(f"{p['name']:<10}{p['h']:>7.1f}{p['w']:>9.1f}{p['bmi']:>9.2f} {classify(p['bmi'])}")
avg = sum(p["bmi"] for p in people) / len(people)
print(f"\n평균 BMI: {avg:.2f} ({classify(avg)})")
counts = {}
for p in people:
c = classify(p["bmi"])
counts[c] = counts.get(c, 0) + 1
print("\n[분류별]")
for c in ["저체중", "정상", "과체중", "비만"]:
print(f" {c}: {counts.get(c, 0)}명")
if __name__ == "__main__":
main()이름 키 몸무게 BMI 분류
홍길동 180.0 95.0 29.32 비만
Alice 165.0 55.0 20.20 정상
평균 BMI: 24.76 (과체중)
[분류별]
저체중: 0명
정상: 1명
과체중: 0명
비만: 1명"""가계부 — JSON 영구 저장"""
import json
from pathlib import Path
from datetime import date
DB = Path("ledger.json")
def load():
if not DB.exists():
return []
try:
return json.loads(DB.read_text(encoding="utf-8"))
except json.JSONDecodeError:
print("⚠️ ledger.json 손상 — 빈 장부로 시작")
return []
def save(records):
DB.write_text(
json.dumps(records, ensure_ascii=False, indent=2),
encoding="utf-8"
)
def add(records):
t = input("수입/지출: ").strip()
if t not in ("수입", "지출"):
print("'수입' 또는 '지출'"); return
try:
amount = int(input("금액: "))
if amount < 0:
raise ValueError
except ValueError:
print("양의 정수 입력"); return
category = input("카테고리: ").strip()
memo = input("메모: ").strip()
records.append({
"type": t, "amount": amount, "category": category,
"memo": memo, "date": date.today().isoformat(),
})
save(records)
print("추가됨")
def show(records):
if not records:
print("(비어있음)"); return
for i, r in enumerate(records, 1):
sign = "+" if r["type"] == "수입" else "-"
print(f"{i:>3}. {r['date']} {sign}{r['amount']:>10,}원 [{r['category']:<6}] {r['memo']}")
def summary(records):
income = sum(r["amount"] for r in records if r["type"] == "수입")
expense = sum(r["amount"] for r in records if r["type"] == "지출")
print(f"수입: {income:>12,}원")
print(f"지출: {expense:>12,}원")
print(f"잔액: {income - expense:>12,}원")
by_cat = {}
for r in records:
if r["type"] == "지출":
by_cat[r["category"]] = by_cat.get(r["category"], 0) + r["amount"]
if by_cat:
max_amt = max(by_cat.values())
print("\n[지출 카테고리별]")
for cat, amt in sorted(by_cat.items(), key=lambda x: -x[1]):
bar = "#" * int(40 * amt / max_amt)
print(f" {cat:<8} {amt:>10,} {bar}")
def main():
records = load()
print(f"불러옴: {len(records)}건")
while True:
cmd = input("\n명령(add/list/summary/del <n>/quit): ").strip()
if not cmd: continue
if cmd == "quit": break
elif cmd == "add": add(records)
elif cmd == "list": show(records)
elif cmd == "summary": summary(records)
elif cmd.startswith("del "):
try:
idx = int(cmd.split()[1]) - 1
if 0 <= idx < len(records):
del records[idx]; save(records); print("삭제")
except (ValueError, IndexError):
print("올바른 번호 입력")
if __name__ == "__main__":
main()불러옴: 0건
명령: add
수입/지출: 지출
금액: 5000
카테고리: 식비
메모: 점심
추가됨
명령: summary
수입: 0원
지출: 5,000원
잔액: -5,000원
[지출 카테고리별]
식비 5,000 ########################################"""도서 관리 — OOP + JSON"""
import json
from pathlib import Path
from dataclasses import dataclass, asdict, field
from typing import Optional
DB = Path("library.json")
@dataclass
class Book:
title: str
author: str
isbn: str
is_borrowed: bool = False
borrower: Optional[str] = None
@dataclass
class Member:
member_id: str
name: str
borrowed: list[str] = field(default_factory=list)
class Library:
def __init__(self):
self.books: list[Book] = []
self.members: list[Member] = []
self.load()
def load(self):
if not DB.exists(): return
data = json.loads(DB.read_text(encoding="utf-8"))
self.books = [Book(**b) for b in data.get("books", [])]
self.members = [Member(**m) for m in data.get("members", [])]
def save(self):
DB.write_text(json.dumps({
"books": [asdict(b) for b in self.books],
"members": [asdict(m) for m in self.members],
}, ensure_ascii=False, indent=2), encoding="utf-8")
def add_book(self, title, author, isbn):
self.books.append(Book(title, author, isbn))
self.save()
def add_member(self, mid, name):
self.members.append(Member(mid, name))
self.save()
def borrow(self, isbn, member_id):
book = next((b for b in self.books if b.isbn == isbn and not b.is_borrowed), None)
member = next((m for m in self.members if m.member_id == member_id), None)
if not book: raise ValueError(f"대출 불가: {isbn}")
if not member: raise ValueError(f"회원 없음: {member_id}")
book.is_borrowed = True
book.borrower = member_id
member.borrowed.append(isbn)
self.save()
def return_book(self, isbn):
book = next((b for b in self.books if b.isbn == isbn and b.is_borrowed), None)
if not book: raise ValueError(f"반납 불가: {isbn}")
member = next((m for m in self.members if m.member_id == book.borrower), None)
if member: member.borrowed.remove(isbn)
book.is_borrowed = False
book.borrower = None
self.save()
def search(self, keyword):
return [b for b in self.books if keyword.lower() in b.title.lower() or keyword.lower() in b.author.lower()]
def main():
lib = Library()
while True:
raw = input("\n> ").strip()
if not raw: continue
if raw == "quit": break
try:
parts = raw.split()
cmd = parts[0]
if cmd == "book" and parts[1] == "add":
t = input("제목: "); a = input("저자: "); i = input("ISBN: ")
lib.add_book(t, a, i); print("등록됨")
elif cmd == "book" and parts[1] == "list":
for b in lib.books:
status = f"대출중({b.borrower})" if b.is_borrowed else "보유"
print(f" [{status}] {b.title} / {b.author} / {b.isbn}")
elif cmd == "member" and parts[1] == "add":
m = input("ID: "); n = input("이름: ")
lib.add_member(m, n); print("등록됨")
elif cmd == "borrow":
lib.borrow(parts[1], parts[2]); print("대출 완료")
elif cmd == "return":
lib.return_book(parts[1]); print("반납 완료")
elif cmd == "search":
results = lib.search(parts[1])
for b in results:
print(f" {b.title} / {b.author}")
except (IndexError, ValueError) as e:
print(f"에러: {e}")
if __name__ == "__main__":
main()> book add
제목: 파이썬
저자: 홍길동
ISBN: 978-0-1
등록됨
> member add
ID: M001
이름: Alice
등록됨
> borrow 978-0-1 M001
대출 완료
> book list
[대출중(M001)] 파이썬 / 홍길동 / 978-0-1📝 과제 (exercises)
직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.
프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- 중첩 dict
- 부분 일치 (소문자 비교)
- 5개 명령
query.lower() in n.lower()
홍길동 | 010-1234-5678 | hong@x.com- · 검색 정확
- · 정렬 출력
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- classify 함수
- people 정렬
- f-string 정렬
{val:>7.1f} 로 우측 정렬 1자리
평균 BMI: 24.76 (과체중)- · 분류 정확
- · 정렬
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- JSON 영구 저장
- summary 막대그래프
- 예외 처리
json.dumps(..., ensure_ascii=False, indent=2)
잔액: -5,000원- · 저장 안전
- · 집계 정확
- · 예외 안전
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- 3개 dataclass/class
- JSON load/save
- borrow/return 일관성
next((b for b in ... if ...), None)
대출 완료- · OOP 분리
- · 예외 처리
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침