← 파이썬 강의 목록으로
📔
실습 프로젝트 (Practice) · ★★★
날짜별 파일 · 분기·HP·다중 엔딩 · subparsers + JSON · 자동 통계 + 리포트

Lv3 · 일기장 · Lv3 · 텍스트 어드벤처 · Lv4 · argparse 기반 CLI · Lv4 · CSV 데이터 분석 CLI

diary/YYYY-MM-DD.txt 파일로 일기를 관리. 작성·조회·검색 + 다중행 입력과 키워드 매치 기능을 갖춥니다. 장면-선택지로 진행되는 분기형 텍스트 어드벤처. HP/아이템 상태와 다중 엔딩을 구현합니다. argparse 의 subparsers 로 진짜 CLI 도구처럼 동작하는 To-Do. add/list/done/del + --version + --help 를 갖춥니다. CSV 파일을 받아 컬럼을 자동 감지 — 숫자는 평균/최대/최소/합계, 문자는 빈도 Top 5. 콘솔과 report.txt 양쪽에 출력합니다.

pathlib파일 I/O검색dict상태 관리분기argparsesubparserJSONcsvCounter분석
소요 시간
⏱ 3~5시간 + 5~8시간 + 5~8시간 + 3~5시간
난이도
📊 중급 종합
선수 조건
🎯 중급 4, 6주차
결과물
Path 와 글롭으로 파일 컬렉션 관리 / 장면 그래프와 상태 머신 / 실제 배포 가능한 CLI 형태 / 표준 라이브러리만으로 데이터 분석

이 강의에서 배우는 것

  • 1pathlib.Path 로 파일 읽기/쓰기
  • 2glob 으로 파일 컬렉션 순회
  • 3다중행 입력 (빈 줄 두 번 종료)
  • 4장면 dict 로 시나리오 그래프 표현
  • 5HP/아이템 상태 머신을 관리한다
  • 6엔딩을 분기와 자원으로 분리
  • 7argparse subparsers 로 서브 커맨드 구현
  • 8각 커맨드를 함수로 분리하고 set_defaults(func=...)
  • 9--version, --help 자동 생성
  • 10csv.DictReader 로 CSV 읽기
  • 11컬럼별 데이터 타입 자동 감지
  • 12Counter 로 문자열 빈도 집계

Lv3 · 일기장

diary/YYYY-MM-DD.txt 파일로 일기를 관리. 작성·조회·검색 + 다중행 입력과 키워드 매치 기능을 갖춥니다.

프로젝트 개요

  • 디렉토리 diary/ 안에 YYYY-MM-DD.txt
  • 명령: write / read <날짜> / list / search <키워드> / quit
  • 같은 날짜 재작성 시 덮어쓸지 확인

도전 과제

  • 키워드 검색 시 매치 라인 + 날짜
  • 통계 (총 작성일, 평균 길이)

채점 체크리스트

  • 디렉토리 자동 생성
  • 다중행 입력
  • 검색 결과

Lv3 · 텍스트 어드벤처

장면-선택지로 진행되는 분기형 텍스트 어드벤처. HP/아이템 상태와 다중 엔딩을 구현합니다.

프로젝트 개요

  • 최소 5장면, 분기 3회 이상
  • HP/아이템 등 상태 관리
  • 엔딩 2개 이상 (GOOD/BAD/NEUTRAL 등)

도전 과제

  • 진행 저장/불러오기
  • 시나리오 편집기
  • 외부 JSON 시나리오

채점 체크리스트

  • 분기 정확
  • HP 0 시 게임 오버
  • 엔딩 도달

Lv4 · argparse 기반 CLI

argparse 의 subparsers 로 진짜 CLI 도구처럼 동작하는 To-Do. add/list/done/del + --version + --help 를 갖춥니다.

프로젝트 개요

  • todo add '우유 사기' --priority high
  • todo list --only-pending
  • todo done 3 / todo del 3
  • todo --version

도전 과제

  • click 으로 재작성 비교
  • rich 컬러 출력

채점 체크리스트

  • subparsers 사용
  • 함수 분리
  • JSON 영구 저장

Lv4 · CSV 데이터 분석 CLI

CSV 파일을 받아 컬럼을 자동 감지 — 숫자는 평균/최대/최소/합계, 문자는 빈도 Top 5. 콘솔과 report.txt 양쪽에 출력합니다.

프로젝트 개요

  • 사용법: python solution.py sales.csv
  • 숫자 컬럼: 평균/최대/최소/합계
  • 문자 컬럼: 빈도 Top 5
  • 결과를 콘솔과 report.txt 양쪽 출력

도전 과제

  • 필터 옵션 (--column price --min 10000)
  • matplotlib PNG 차트

채점 체크리스트

  • 타입 자동 감지
  • 리포트 파일 생성
  • 에러 안전

💻 예제 (examples)

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

solution.py— 전체 동작 코드
CODE
"""일기장 — 날짜별 파일"""
from pathlib import Path
from datetime import date

DIARY = Path("diary")
DIARY.mkdir(exist_ok=True)

def write_today():
    today = date.today().isoformat()
    f = DIARY / f"{today}.txt"
    if f.exists():
        if input(f"{today} 이미 있음. 덮어쓸까? (y/n): ").lower() != "y":
            return
    print("내용 입력 (빈 줄 두 번이면 종료):")
    lines = []
    blank = 0
    while True:
        line = input()
        if line == "":
            blank += 1
            if blank >= 2: break
            lines.append("")
        else:
            blank = 0
            lines.append(line)
    f.write_text("\n".join(lines), encoding="utf-8")
    print(f"저장됨 ({f})")

def read(date_str):
    f = DIARY / f"{date_str}.txt"
    if not f.exists():
        print("없음"); return
    print(f.read_text(encoding="utf-8"))

def list_all():
    files = sorted(DIARY.glob("*.txt"))
    for f in files:
        size = len(f.read_text(encoding="utf-8"))
        print(f"  {f.stem} ({size}자)")
    print(f"\n총 {len(files)}일")

def search(keyword):
    for f in sorted(DIARY.glob("*.txt")):
        text = f.read_text(encoding="utf-8")
        for i, line in enumerate(text.splitlines(), 1):
            if keyword in line:
                print(f"[{f.stem} L{i}] {line}")

def main():
    while True:
        raw = input("\n명령(write/read <date>/list/search <q>/quit): ").strip()
        if not raw: continue
        if raw == "quit": break
        parts = raw.split(maxsplit=1)
        cmd = parts[0]
        arg = parts[1] if len(parts) > 1 else ""
        if cmd == "write": write_today()
        elif cmd == "read":
            read(arg if arg else date.today().isoformat())
        elif cmd == "list": list_all()
        elif cmd == "search":
            if arg: search(arg)

if __name__ == "__main__":
    main()
▶ 실행 결과
명령: write
내용 입력 (빈 줄 두 번이면 종료):
오늘은 좋은 날.

저장됨 (diary/2026-05-10.txt)
명령: list
  2026-05-10 (10자)

총 1일
명령: search 좋은
[2026-05-10 L1] 오늘은 좋은 날.
solution.py— 전체 동작 코드 (인라인 시나리오)
CODE
"""텍스트 어드벤처 — 분기 + HP"""

SCENES = {
    "start": {
        "text": "어두운 숲에 들어섰다. 길은 둘로 갈라진다.",
        "choices": {
            "1": {"label": "왼쪽으로", "next": "left", "effect": {"hp": 0}},
            "2": {"label": "오른쪽으로", "next": "right", "effect": {"hp": 0}},
        },
    },
    "left": {
        "text": "늑대를 만났다! 싸울까 도망갈까?",
        "choices": {
            "1": {"label": "싸운다", "next": "fight", "effect": {"hp": -30}},
            "2": {"label": "도망친다", "next": "escape", "effect": {"hp": -10}},
        },
    },
    "right": {
        "text": "낡은 오두막을 발견. 들어갈까?",
        "choices": {
            "1": {"label": "들어간다", "next": "treasure", "effect": {"hp": 0, "item": "검"}},
            "2": {"label": "지나친다", "next": "lost", "effect": {"hp": 0}},
        },
    },
    "fight": {"text": "늑대를 물리치고 보물 발견! 🏆", "ending": True, "result": "GOOD"},
    "escape": {"text": "안전하게 마을로. 평범한 결말.", "ending": True, "result": "NEUTRAL"},
    "treasure": {"text": "오두막에서 검을 얻고 영웅이 됨! 🏆", "ending": True, "result": "GOOD"},
    "lost": {"text": "숲에서 길을 잃고 사라짐...", "ending": True, "result": "BAD"},
}

def main():
    state = {"hp": 100, "items": []}
    current = "start"
    while True:
        scene = SCENES[current]
        print(f"\n{scene['text']}")
        print(f"[HP: {state['hp']}, 아이템: {state['items']}]")

        if scene.get("ending"):
            print(f"\n=== 엔딩: {scene['result']} ===")
            break

        for k, ch in scene["choices"].items():
            print(f"  {k}. {ch['label']}")
        pick = input("> ").strip()
        if pick not in scene["choices"]:
            print("잘못된 선택"); continue

        choice = scene["choices"][pick]
        effect = choice.get("effect", {})
        state["hp"] += effect.get("hp", 0)
        if "item" in effect:
            state["items"].append(effect["item"])
        if state["hp"] <= 0:
            print("\n=== HP 0! 게임 오버 ===")
            break
        current = choice["next"]

if __name__ == "__main__":
    main()
▶ 실행 결과
어두운 숲에 들어섰다. 길은 둘로 갈라진다.
[HP: 100, 아이템: []]
  1. 왼쪽으로
  2. 오른쪽으로
> 2

낡은 오두막을 발견. 들어갈까?
[HP: 100, 아이템: []]
  1. 들어간다
  2. 지나친다
> 1

오두막에서 검을 얻고 영웅이 됨! 🏆
[HP: 100, 아이템: ['검']]

=== 엔딩: GOOD ===
solution.py— 전체 동작 코드
CODE
"""argparse 기반 To-Do CLI"""
import argparse
import json
from pathlib import Path

DB = Path("todos_cli.json")

def load():
    return json.loads(DB.read_text(encoding="utf-8")) if DB.exists() else []

def save(items):
    DB.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")

def cmd_add(args):
    items = load()
    items.append({
        "id": len(items) + 1,
        "task": args.task,
        "priority": args.priority,
        "done": False,
    })
    save(items)
    print(f"추가됨 #{items[-1]['id']}: {args.task}")

def cmd_list(args):
    items = load()
    if args.only_pending:
        items = [i for i in items if not i["done"]]
    if not items:
        print("(비어있음)"); return
    for it in items:
        mark = "[x]" if it["done"] else "[ ]"
        print(f"{it['id']:>3}. {mark}[{it['priority']}] {it['task']}")

def cmd_done(args):
    items = load()
    for it in items:
        if it["id"] == args.id:
            it["done"] = True
            save(items)
            print(f"#{args.id} 완료")
            return
    print(f"#{args.id} 없음")

def cmd_del(args):
    items = load()
    new_items = [i for i in items if i["id"] != args.id]
    if len(new_items) == len(items):
        print(f"#{args.id} 없음"); return
    save(new_items)
    print(f"#{args.id} 삭제")

def build_parser():
    p = argparse.ArgumentParser(prog="todo")
    p.add_argument("--version", action="version", version="todo 1.0.0")
    sub = p.add_subparsers(dest="cmd", required=True)

    add = sub.add_parser("add")
    add.add_argument("task")
    add.add_argument("--priority", choices=["low", "high"], default="low")
    add.set_defaults(func=cmd_add)

    lst = sub.add_parser("list")
    lst.add_argument("--only-pending", action="store_true")
    lst.set_defaults(func=cmd_list)

    done = sub.add_parser("done")
    done.add_argument("id", type=int)
    done.set_defaults(func=cmd_done)

    delcmd = sub.add_parser("del")
    delcmd.add_argument("id", type=int)
    delcmd.set_defaults(func=cmd_del)

    return p

def main():
    args = build_parser().parse_args()
    args.func(args)

if __name__ == "__main__":
    main()
▶ 실행 결과
$ python solution.py add "우유 사기" --priority high
추가됨 #1: 우유 사기

$ python solution.py list
  1. [ ][high] 우유 사기

$ python solution.py done 1
#1 완료

$ python solution.py --version
todo 1.0.0
solution.py— 전체 동작 코드
CODE
"""CSV 자동 분석 — 표준 csv 모듈만 사용"""
import csv
import sys
from collections import Counter
from pathlib import Path

def is_number(s):
    try:
        float(s); return True
    except ValueError:
        return False

def analyze(path):
    with open(path, encoding="utf-8") as f:
        reader = csv.DictReader(f)
        rows = list(reader)
        cols = reader.fieldnames

    if not rows:
        return ["(빈 파일)"]

    out = [f"[행 수] {len(rows)}"]

    for col in cols:
        values = [r[col] for r in rows if r[col] != ""]
        if not values: continue

        if all(is_number(v) for v in values):
            nums = [float(v) for v in values]
            out.append(f"\n[숫자: {col}]")
            out.append(f"  평균: {sum(nums) / len(nums):,.2f}")
            out.append(f"  최대: {max(nums):,.2f}")
            out.append(f"  최소: {min(nums):,.2f}")
            out.append(f"  합계: {sum(nums):,.2f}")
        else:
            counter = Counter(values)
            out.append(f"\n[문자: {col}]")
            for val, cnt in counter.most_common(5):
                out.append(f"  {val:<20} {cnt:>6}")

    return out

def main():
    if len(sys.argv) != 2:
        print("사용법: python solution.py <csv 파일>")
        sys.exit(1)
    path = Path(sys.argv[1])
    if not path.exists():
        print(f"파일 없음: {path}")
        sys.exit(1)

    result = analyze(path)
    text = "\n".join(result)
    print(text)
    Path("report.txt").write_text(text, encoding="utf-8")
    print("\nreport.txt 저장됨")

if __name__ == "__main__":
    main()
▶ 실행 결과
[행 수] 4

[문자: name]
  Alice                     1
  Bob                       1
  Charlie                   1
  Dave                      1

[숫자: score]
  평균: 80.50
  최대: 92.00
  최소: 67.00
  합계: 322.00

report.txt 저장됨

📝 과제 (exercises)

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

과제 1

프로젝트 구현

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

요구사항
  • pathlib.Path
  • 다중행 입력
  • glob 검색
💡 힌트

DIARY.glob('*.txt')

f.stem 으로 확장자 제거

입출력 예시
[2026-05-10 L1] 오늘은 좋은 날.
채점
  • · 디렉토리 생성
  • · 검색 정확
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
과제 2

프로젝트 구현

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

요구사항
  • SCENES dict
  • 상태 관리
  • 엔딩 2개 이상
💡 힌트

scene.get('ending') 으로 종료 체크

입출력 예시
=== 엔딩: GOOD ===
채점
  • · 분기 동작
  • · HP/아이템 누적
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
과제 3

프로젝트 구현

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

요구사항
  • subparsers
  • set_defaults(func=...)
  • --version
💡 힌트

sub = p.add_subparsers(dest='cmd', required=True)

입출력 예시
todo 1.0.0
채점
  • · 서브커맨드 동작
  • · JSON 저장
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
과제 4

프로젝트 구현

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

요구사항
  • csv.DictReader
  • 타입 자동 감지
  • report.txt 생성
💡 힌트

all(is_number(v) for v in values) 로 숫자 컬럼 판별

입출력 예시
report.txt 저장됨
채점
  • · 타입 감지
  • · 리포트 생성
▶정답 코드 펼치기 / 접기
SOLUTION
# 위 solution.py 와 동일
▶ 실행 결과
(예제 참고)
예제 코드 / 강의 자료

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

GitHub에서 보기 ↗

댓글 0

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

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

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