← 파이썬 강의 목록으로
📇
실습 프로젝트 (Practice) · ★★
검색·수정·부분일치 · 다인원 + 분류 + 정렬 표 · JSON 영구 저장 + 카테고리 · OOP + JSON

Lv2 · 주소록 · Lv2 · BMI 추적기 · Lv3 · 가계부 · Lv3 · 도서 관리 시스템

이름·전화·이메일을 저장하고 부분 일치 검색을 지원하는 주소록. dict 안의 dict 패턴을 다룹니다. 여러 명의 키/몸무게를 입력 받아 BMI 계산, 분류, 정렬된 표로 출력. f-string 정렬과 분류별 집계까지 다룹니다. 수입/지출 기록을 JSON 으로 영구 저장. 카테고리별 합계, 잔액, 막대그래프 시각화까지 포함합니다. Book/Member/Library 클래스로 도서 등록·대출·반납·검색을 구현. dataclass 와 JSON 직렬화까지 통합합니다.

dict검색정렬f-stringJSON예외집계OOPdataclass
소요 시간
⏱ 2~3시간 + 2~3시간 + 3~5시간 + 5~8시간
난이도
📊 자료구조 활용
선수 조건
🎯 기초 7-8주차
결과물
중첩 dict 와 부분 일치 검색 / 표 형식 출력과 집계 / 영구 저장 패턴과 집계 / OOP 도메인 모델링과 영속화

이 강의에서 배우는 것

  • 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 패턴을 다룹니다.

데이터 모델

python
{"이름": {"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)

실제로 실행해 결과를 확인할 수 있는 예제 코드입니다.

solution.py— 전체 동작 코드
CODE
"""주소록 — 부분 일치 검색"""

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
solution.py— 전체 동작 코드
CODE
"""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명
solution.py— 전체 동작 코드
CODE
"""가계부 — 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  ########################################
solution.py— 전체 동작 코드 (OOP 버전)
CODE
"""도서 관리 — 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)

직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.

과제 1

프로젝트 구현

목표: 위 사양대로 직접 작성한다.

요구사항
  • 중첩 dict
  • 부분 일치 (소문자 비교)
  • 5개 명령
💡 힌트

query.lower() in n.lower()

입출력 예시
홍길동 | 010-1234-5678 | hong@x.com
채점
  • · 검색 정확
  • · 정렬 출력
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
과제 2

프로젝트 구현

목표: 위 사양대로 직접 작성한다.

요구사항
  • classify 함수
  • people 정렬
  • f-string 정렬
💡 힌트

{val:>7.1f} 로 우측 정렬 1자리

입출력 예시
평균 BMI: 24.76 (과체중)
채점
  • · 분류 정확
  • · 정렬
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
과제 3

프로젝트 구현

목표: 위 사양대로 직접 작성한다.

요구사항
  • JSON 영구 저장
  • summary 막대그래프
  • 예외 처리
💡 힌트

json.dumps(..., ensure_ascii=False, indent=2)

입출력 예시
잔액:       -5,000원
채점
  • · 저장 안전
  • · 집계 정확
  • · 예외 안전
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
과제 4

프로젝트 구현

목표: 위 사양대로 직접 작성한다.

요구사항
  • 3개 dataclass/class
  • JSON load/save
  • borrow/return 일관성
💡 힌트

next((b for b in ... if ...), None)

입출력 예시
대출 완료
채점
  • · OOP 분리
  • · 예외 처리
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침