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 양쪽에 출력합니다.
이 강의에서 배우는 것
- 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)
실제로 실행해 결과를 확인할 수 있는 예제 코드입니다.
"""일기장 — 날짜별 파일"""
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] 오늘은 좋은 날."""텍스트 어드벤처 — 분기 + 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 ==="""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"""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)
직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.
프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- pathlib.Path
- 다중행 입력
- glob 검색
DIARY.glob('*.txt')
f.stem 으로 확장자 제거
[2026-05-10 L1] 오늘은 좋은 날.- · 디렉토리 생성
- · 검색 정확
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- SCENES dict
- 상태 관리
- 엔딩 2개 이상
scene.get('ending') 으로 종료 체크
=== 엔딩: GOOD ===- · 분기 동작
- · HP/아이템 누적
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- subparsers
- set_defaults(func=...)
- --version
sub = p.add_subparsers(dest='cmd', required=True)
todo 1.0.0- · 서브커맨드 동작
- · JSON 저장
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)프로젝트 구현
목표: 위 사양대로 직접 작성한다.
- csv.DictReader
- 타입 자동 감지
- report.txt 생성
all(is_number(v) for v in values) 로 숫자 컬럼 판별
report.txt 저장됨- · 타입 감지
- · 리포트 생성
▶정답 코드 펼치기 / 접기
# 위 solution.py 와 동일(예제 참고)댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침