업무용 스크립트 요청문에 입력·출력·오류 원칙 담기
“자동화해 줘”를 실행 가능한 작업 설명으로 바꿉니다. 민감정보 없는 합성 샘플과 손으로 확인한 기대 결과를 붙여, 팀별 작업 기록 집계 스크립트를 요청하는 문장을 완성합니다.
손계산할 수 있는 네 행과 12개 단위 테스트로 집계 함수를 확인합니다. 정상 값뿐 아니라 빈 입력, 0, 소수, 잘못된 입력까지 검증합니다.
이런 분께AI에게 받은 Python 코드가 실행된다는 사실만으로 업무에 쓰기 망설여지는 입문자
AI에게 받은 코드가 오류 없이 끝났다고 계산이 맞는 것은 아닙니다. 이 글에서는 실제 AI 서비스에 접속하지 않고, 독립적으로 작성한 집계 함수와 작은 합성 데이터를 사용해 검증 순서를 연습합니다. 같은 방법을 자신이 검토할 함수에 적용할 수 있습니다. API 키나 외부 패키지는 필요하지 않습니다.
python example.py| item | quantity | unit_price | 행별 금액 |
|---|---|---|---|
| 노트 | 2 | 2500.00 | 5000.00 |
| 펜 | 3 | 1200.50 | 3601.50 |
| 노트 | 1 | 2500.00 | 2500.00 |
| 샘플 | 0 | 999.99 | 0.00 |
행 수는 4이지만 수량 합계는 2 + 3 + 1 + 0 = 6입니다. 금액 합계는 5000.00 + 3601.50 + 2500.00 = 11101.50입니다. 수량이 0인 마지막 행도 입력 행 수에는 포함됩니다. 품목별로는 노트 수량 3·금액 7500.00, 펜 수량 3·금액 3601.50, 샘플 수량 0·금액 0.00입니다.
이처럼 합계만 비교하지 말고 행 수, 총수량, 품목별 값을 함께 봅니다. 노트처럼 같은 품목을 두 행으로 나눠 넣으면 묶어서 더하는 기능도 확인할 수 있습니다. 품목별 결과의 순서는 이름 정렬이며 판매순위나 입력 순서를 의미하지 않습니다.
| 항목 | 허용 규칙 | 거부 예시 |
|---|---|---|
| 열 | item, quantity, unit_price 세 개 | 열 누락 또는 추가 열 |
| item | 앞뒤 공백 제거 후 비어 있지 않은 문자열 | 빈 문자열, 공백뿐인 값 |
| quantity | 0~1,000,000의 정수 문자열 | -1, 1.5, 1e2 |
| unit_price | 0~1,000,000,000, 소수점 둘째 자리까지 | NaN, Infinity, 0.001, 1,000 |
금액은 가상의 동일 통화라는 가정을 둡니다. 수량과 단가의 음수를 허용하지 않으므로 반품을 표현하는 규칙은 포함되지 않습니다. 입력 규칙을 먼저 정하면 코드가 마음대로 0으로 바꾸거나 일부 행을 건너뛰는지 구별할 수 있습니다. 이 함수는 잘못된 행을 발견하면 ValueError로 중지합니다.
aggregate_sales는 입력 행을 변경하지 않고 결과만 반환합니다. 파일 읽기와 결과 쓰기는 main에 분리했습니다. 이렇게 하면 테스트에서 작은 딕셔너리를 직접 넣어 계산 부분만 확인할 수 있습니다. 각 기대값은 assertEqual로 비교하고, 거부해야 할 값은 assertRaises로 확인합니다.
"""집계 함수를 손계산 기대값과 unittest로 검증합니다. 외부 AI 호출은 없습니다."""
import argparse
import csv
import json
import re
import sys
import unittest
from decimal import Decimal
from pathlib import Path
BASE = Path(__file__).resolve().parent
OUTPUT = BASE / "outputs"
FIELDS = {"item", "quantity", "unit_price"}
def money(cents):
return f"{cents // 100}.{cents % 100:02d}"
def aggregate_sales(rows):
"""문자열 딕셔너리 행을 검증하고 집계합니다. rows와 원본 파일을 변경하지 않습니다."""
totals = {}
row_count = total_quantity = total_cents = 0
for number, row in enumerate(rows, start=1):
if not isinstance(row, dict) or set(row) != FIELDS:
raise ValueError(f"{number}행: item,quantity,unit_price 세 열이 필요합니다.")
if any(not isinstance(value, str) for value in row.values()):
raise ValueError(f"{number}행: 모든 값은 문자열이어야 합니다.")
item, quantity_text, price_text = (row[key].strip() for key in ("item", "quantity", "unit_price"))
if not item:
raise ValueError(f"{number}행: 품목이 비어 있습니다.")
if not re.fullmatch(r"[0-9]{1,7}", quantity_text):
raise ValueError(f"{number}행: 수량은 0 이상의 정수여야 합니다.")
quantity = int(quantity_text)
if quantity > 1_000_000:
raise ValueError(f"{number}행: 수량은 1,000,000 이하여야 합니다.")
if not re.fullmatch(r"[0-9]{1,10}(?:\.[0-9]{1,2})?", price_text):
raise ValueError(f"{number}행: 단가는 0 이상, 소수점 둘째 자리까지 입력하세요.")
price = Decimal(price_text) # 문자열에서 직접 생성해 float 오차를 피합니다.
if price > Decimal("1000000000"):
raise ValueError(f"{number}행: 단가는 1,000,000,000 이하여야 합니다.")
cents = int(price * 100) * quantity
bucket = totals.setdefault(item, {"quantity": 0, "cents": 0})
bucket["quantity"] += quantity
bucket["cents"] += cents
total_quantity += quantity
total_cents += cents
row_count += 1
return {
"row_count": row_count,
"quantity": total_quantity,
"amount": money(total_cents),
"by_item": [
{"item": item, "quantity": totals[item]["quantity"], "amount": money(totals[item]["cents"])}
for item in sorted(totals)
],
}
def sale(item="펜", quantity="1", unit_price="1200.50"):
return {"item": item, "quantity": quantity, "unit_price": unit_price}
class TestAggregate(unittest.TestCase):
# 기대값은 검증 대상 함수로 만들지 않고 손계산한 상수로 적습니다.
def test_empty_input(self):
self.assertEqual(aggregate_sales([]), {"row_count": 0, "quantity": 0, "amount": "0.00", "by_item": []})
def test_single_row(self):
self.assertEqual(aggregate_sales([sale(quantity="3")])["amount"], "3601.50")
def test_zero_quantity(self):
result = aggregate_sales([sale(quantity="0")])
self.assertEqual((result["row_count"], result["quantity"], result["amount"]), (1, 0, "0.00"))
def test_repeated_item(self):
result = aggregate_sales([sale(quantity="2"), sale(quantity="1")])
self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 3, "amount": "3601.50"}])
def test_decimal_addition(self):
result = aggregate_sales([sale(unit_price="0.10"), sale(unit_price="0.20")])
self.assertEqual(result["amount"], "0.30")
def test_whitespace(self):
result = aggregate_sales([sale(item=" 펜 ", quantity=" 1 ", unit_price=" 2.00 ")])
self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 1, "amount": "2.00"}])
def test_bad_quantity(self):
for value in ("-1", "1.5", "", "1000001", "1e2", "1"):
with self.subTest(value=value), self.assertRaises(ValueError):
aggregate_sales([sale(quantity=value)])
def test_bad_price(self):
for value in ("-1", "0.001", "NaN", "Infinity", "", "1,000", "1000000001"):
with self.subTest(value=value), self.assertRaises(ValueError):
aggregate_sales([sale(unit_price=value)])
def test_blank_item(self):
with self.assertRaises(ValueError):
aggregate_sales([sale(item=" ")])
def test_wrong_columns(self):
for row in ({"item": "펜"}, {**sale(), "extra": "x"}):
with self.subTest(row=row), self.assertRaises(ValueError):
aggregate_sales([row])
def test_non_string(self):
with self.assertRaises(ValueError):
aggregate_sales([sale(quantity=True)])
def test_upper_boundary(self):
self.assertEqual(
aggregate_sales([sale(quantity="1000000", unit_price="1000000000")])["amount"],
"1000000000000000.00",
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--test", action="store_true", help="단위 테스트만 실행합니다.")
args = parser.parse_args()
# unittest의 명시적 비교는 python -O에서도 생략되지 않습니다.
result = unittest.TextTestRunner(verbosity=2, stream=sys.stdout).run(
unittest.defaultTestLoader.loadTestsFromTestCase(TestAggregate)
)
if not result.wasSuccessful():
return 1
if args.test:
return 0
if OUTPUT.exists() or OUTPUT.is_symlink():
raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
with (BASE / "input.csv").open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream, strict=True)
if reader.fieldnames != ["item", "quantity", "unit_price"]:
raise ValueError("input.csv의 열은 item,quantity,unit_price 순서여야 합니다.")
actual = aggregate_sales(reader)
expected = json.loads((BASE / "expected.json").read_text(encoding="utf-8"))
if actual != expected:
raise ValueError("예제 집계가 expected.json의 손계산 기대값과 다릅니다.")
OUTPUT.mkdir()
with (OUTPUT / "summary.json").open("x", encoding="utf-8") as stream:
json.dump(actual, stream, ensure_ascii=False, indent=2)
stream.write("\n")
with (OUTPUT / "verification.txt").open("x", encoding="utf-8") as stream:
stream.write(f"unittest: {result.testsRun}개 통과\n예제 기대값: 일치\n외부 AI API 호출: 없음\n")
print(f"완료: {actual['row_count']}행, 수량 {actual['quantity']}, 금액 {actual['amount']}")
print(OUTPUT)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, csv.Error) as error:
print(f"중지: {error}", file=sys.stderr)
raise SystemExit(2)
단가는 문자열에서 Decimal로 만들고 100을 곱해 정수 단위로 바꾼 뒤 합칩니다. 결과 금액도 두 자리 소수 형식의 문자열로 반환합니다. 이 방식은 예제의 두 자리 단가 규칙에 맞춘 선택이며 모든 수치 계산에 그대로 적용하는 만능 규칙은 아닙니다.
| 분류 | 내장 테스트 | 확인 목적 |
|---|---|---|
| 정상 | 한 행, 같은 품목 반복, 앞뒤 공백 | 기본 계산과 묶음 처리 |
| 경계 | 빈 입력, 수량 0, 허용 상한 | 작거나 큰 값에서의 정의 |
| 소수 | 0.10 + 0.20 = 0.30 | 금액 표현과 덧셈 |
| 오류 | 잘못된 수량·단가·빈 품목·열·타입 | 잘못된 값을 조용히 통과시키지 않음 |
테스트 이름 옆의 ok와 마지막 ‘Ran 12 tests’, ‘OK’를 확인합니다. 그 뒤 출력되는 ‘완료: 4행, 수량 6, 금액 11101.50’까지 보여야 CSV 기대값 비교도 통과한 것입니다. 단위 테스트만 성공하고 expected.json과 다르면 기본 실행은 실패하며 새 결과를 저장하지 않습니다.
python example.py --test--test는 단위 테스트만 반복하며 outputs를 만들지 않습니다. 이미 결과 폴더가 있어도 사용할 수 있으므로 함수를 수정하면서 빠르게 확인하기 좋습니다. 반대로 기본 명령은 기존 outputs를 덮어쓰지 않고 중지합니다.
이 실험의 목적은 테스트가 오류를 발견하는지 확인하는 것입니다. 제작 과정에서도 같은 종류의 오류를 임시 복사본에 주입해 테스트가 실패하고 결과가 쓰이지 않는 것을 검증했습니다. 의미 있는 실패를 한 번 보면 ‘테스트 코드도 실행은 되지만 아무것도 검사하지 않는’ 상황을 피하기 쉽습니다.
새 코드에서는 먼저 입력과 출력의 예시를 고정합니다. 원본 파일에 쓰는 부분이 있다면 사본을 대상으로 제한하고, 계산을 맡는 함수부터 분리하세요. 정상 데이터 한 묶음, 빈 데이터, 0, 반복 항목, 허용하지 않을 값을 각각 준비한 다음 기대값을 자신이 계산합니다.
input.csv를 자신의 자료로 바꾸면 expected.json도 별도로 계산해 갱신해야 합니다. 입력만 바꾼 뒤 기대값 불일치를 코드 결함으로 단정하지 마세요. 어떤 규칙과 어떤 데이터가 바뀌었는지부터 확인하는 것이 재현 가능한 검증의 시작입니다.
| 결과 | 의미 | 다음 확인 |
|---|---|---|
| FAIL 또는 FAILED | 계산 결과가 테스트 기대값과 다름 | 실패한 test 이름과 두 값을 읽습니다. |
| 입력 규칙 관련 중지 | CSV 값이 허용 범위를 벗어남 | 표시된 행과 원래 데이터를 확인합니다. |
| 손계산 기대값과 다름 | CSV 집계와 expected.json 불일치 | 데이터 변경 여부와 손계산을 각각 재확인합니다. |
| outputs가 이미 있음 | 이전 성공 결과가 존재 | --test로 검사만 하거나 결과를 옮겨 보관합니다. |
이 예제의 테스트 통과는 명시한 규칙에 대한 증거입니다. 실제 AI 모델의 성능을 측정한 결과나 모든 회계 업무에서 정확하다는 보증은 아닙니다. 세금, 할인, 환율, 반품, 여러 통화, 대량 데이터 성능은 별도 규칙과 테스트가 필요합니다. 실패 사례를 늘릴수록 적용 가능한 범위를 더 명확히 설명할 수 있습니다.
2026-09-19 · Windows 11 · CPython 3.12.14 · 추가 패키지 없음 · 배포본의 임시 복사본에서 실행
코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.
예제 ZIP 다운로드직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.