엑셀·문서 업무

업무 기록을 월별 집계표와 그래프로 만들기

CSV의 날짜와 작업 시간을 검증하고, 월별 기록 수와 합계 시간을 CSV와 막대그래프로 저장합니다.

목차 보기

이런 분께반복되는 월간 업무 집계를 간단한 파이썬 코드로 처리하고 싶은 사무 담당자

준비사항
  • Python 3.12와 터미널을 준비합니다. python --version으로 버전을 확인합니다.
  • 다운로드한 ZIP을 모두 풀고 example.py가 있는 폴더에서 터미널을 엽니다.
  • 처음에는 동봉된 연습 데이터로 실행한 뒤 결과와 원본을 비교합니다.
  • work_log.csv와 Matplotlib를 사용합니다. CSV는 UTF-8 또는 UTF-8 BOM 인코딩이어야 합니다.

01한 행의 의미부터 정하기

업무 기록 한 행을 하나의 작업 기록으로 보고, date가 속한 달에 hours를 더합니다. record_count는 행 수이며 사람 수나 근무 일수가 아닙니다. hours는 십진수 시간입니다. 1.50은 1시간 30분이며 1시간 50분이 아닙니다.

동봉 데이터는 2026년 1~3월의 가상 업무 기록 9개입니다. 작업 시간을 측정하거나 성과를 평가하는 도구가 아니라, 이미 입력된 숫자를 월별로 합치는 연습입니다.

02입력 열과 값 확인하기

형식확인 기준
record_idR001 같은 텍스트파일 안에서 고유해야 함
date2026-01-06YYYY-MM-DD, 실제 존재하는 날짜
task자료 정리빈 값 불가
hours2.500 이상 유한수, 소수 둘째 자리까지

제목 행, 합계 행, 쉼표가 들어간 숫자는 넣지 않습니다. 중복 ID를 발견하면 이중 집계를 막기 위해 중지합니다. 작업 내용이 같더라도 ID가 다르면 서로 다른 기록으로 합산합니다.

03파일을 준비하고 실행하기

  1. ZIP을 풀고 README.txt에 적힌 입력 파일과 example.py가 같은 위치인지 확인합니다. ZIP 안에서 바로 실행하지 않습니다.
  2. example.py가 있는 폴더에서 아래 명령을 한 줄씩 실행합니다. Windows에서 python 명령이 없고 py가 있으면 각 명령의 python을 py -3.12로 바꿉니다.
  3. 완료 메시지가 나온 뒤 새 outputs 폴더를 엽니다. 기존 outputs가 있으면 먼저 다른 이름으로 보관하고 다시 실행합니다.
bash
python --version
python -m pip install -r requirements.txt
python example.py

설치는 인터넷 연결이 필요합니다. 코드의 BASE는 현재 터미널 위치가 아니라 example.py의 위치를 기준으로 입력과 결과 경로를 정합니다.

04전체 코드와 집계 방식

날짜는 date.fromisoformat으로 확인하고 YYYY-MM 형식의 월 키를 만듭니다. Decimal로 시간을 더한 뒤 표에 소수 둘째 자리까지 기록합니다. 그래프를 그릴 때만 일반 실수로 바꿉니다. Agg 모드를 사용하므로 그래프 창 대신 PNG가 저장됩니다.

example.py
"""Summarize fictional work-log records by calendar month."""
import csv
import math
from collections import defaultdict
from datetime import date
from decimal import Decimal, InvalidOperation
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

BASE = Path(__file__).resolve().parent
HEADERS = ["record_id", "date", "task", "hours"]


def main():
    destination = BASE / "outputs"
    if destination.exists():
        raise ValueError("outputs already exists; rename it before running again.")
    totals = defaultdict(lambda: {"records": 0, "hours": Decimal("0")})
    seen = set()
    with (BASE / "work_log.csv").open(encoding="utf-8-sig", newline="") as file:
        reader = csv.DictReader(file)
        if reader.fieldnames != HEADERS:
            raise ValueError(f"Expected headers: {HEADERS}")
        for line, row in enumerate(reader, start=2):
            if None in row or any(value is None or not value.strip() for value in row.values()):
                raise ValueError(f"Row {line}: missing or extra field.")
            if row["record_id"] in seen:
                raise ValueError(f"Row {line}: duplicate record_id.")
            seen.add(row["record_id"])
            parsed_date = date.fromisoformat(row["date"])
            if parsed_date.isoformat() != row["date"]:
                raise ValueError(f"Row {line}: use YYYY-MM-DD dates.")
            try:
                hours = Decimal(row["hours"])
            except InvalidOperation as error:
                raise ValueError(f"Row {line}: hours must be a number.") from error
            if not hours.is_finite() or hours < 0 or hours.as_tuple().exponent < -2:
                raise ValueError(f"Row {line}: hours must be finite, nonnegative, with at most 2 decimals.")
            month = parsed_date.strftime("%Y-%m")
            totals[month]["records"] += 1
            totals[month]["hours"] += hours
    if not totals:
        raise ValueError("No work records were found.")

    months = sorted(totals)
    figure, axis = plt.subplots(figsize=(8, 4.6), layout="constrained")
    values = [float(totals[month]["hours"]) for month in months]
    if not all(math.isfinite(value) and math.isfinite(value * 1.2) for value in values):
        plt.close(figure)
        raise ValueError("Monthly total is too large to plot as a finite number.")
    bars = axis.bar(months, values, color="#2B6578", width=0.55)
    axis.bar_label(bars, labels=[f"{value:.2f}" for value in values], padding=4)
    axis.set(title="Monthly work hours | Fictional data", xlabel="Calendar month", ylabel="Hours")
    axis.set_ylim(0, max(values) * 1.2 if max(values) > 0 else 1)
    axis.spines[["top", "right"]].set_visible(False)
    axis.set_axisbelow(True)
    axis.grid(axis="y", alpha=0.2)
    destination.mkdir(exist_ok=False)
    with (destination / "monthly_summary.csv").open("x", encoding="utf-8-sig", newline="") as file:
        writer = csv.writer(file)
        writer.writerow(["month", "record_count", "total_hours"])
        for month in months:
            writer.writerow([month, totals[month]["records"], f"{totals[month]['hours']:.2f}"])
    figure.savefig(destination / "monthly_hours.png", dpi=160)
    plt.close(figure)
    total_hours = sum((values["hours"] for values in totals.values()), Decimal("0"))
    print(f"Records: {len(seen)}; months: {len(months)}; hours: {total_hours:.2f}")
    print("Created outputs/monthly_summary.csv and outputs/monthly_hours.png.")


if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError, csv.Error) as error:
        raise SystemExit(f"Stopped: {error}") from error

05집계표와 그래프 읽기

outputs/monthly_summary.csv와 outputs/monthly_hours.png가 만들어집니다. 그래프는 입력이 있는 달만 연도·월 순서로 표시합니다. 영어 축은 별도 한글 글꼴 설치 없이 실행하기 위한 선택입니다.

monthrecord_counttotal_hours
2026-0136.50
2026-0237.50
2026-0339.50
전체 확인 합계923.50
가상 업무 기록의 월별 시간 그래프. 1월 6.5시간, 2월 7.5시간, 3월 9.5시간.
가상 업무 기록의 월별 시간 그래프. 1월 6.5시간, 2월 7.5시간, 3월 9.5시간.

06합계와 월 경계를 확인하기

  1. 집계 CSV에서 1월이 2.50 + 1.00 + 3.00 = 6.50시간인지 확인합니다.
  2. 월별 record_count 합이 입력 기록 9개와 같은지 확인합니다.
  3. total_hours 합이 23.50인지 확인하고 그래프의 각 막대 숫자와 맞춰 봅니다.
  4. 연습 폴더를 복사한 뒤 마지막 기록 날짜를 2026-04-01로 바꿔 다시 실행합니다. 3월 5.50시간, 4월 4.00시간으로 나뉘는지 확인합니다.

07날짜나 숫자 때문에 멈췄을 때

문제수정 방법
duplicate record_id같은 기록을 두 번 넣었는지 확인합니다.
day is out of range / Invalid isoformat2026-02-30 같은 불가능한 날짜나 날짜 형식을 확인합니다.
hours must be a number시간에는 2.5처럼 숫자만 넣습니다. 시간 단위나 천 단위 쉼표를 빼세요.
hours must be finite…음수·NaN·Infinity·소수 셋째 자리 이상을 확인합니다.
한글이 깨짐원본 CSV를 UTF-8로 다시 저장합니다.
outputs already exists이전 결과를 보관한 뒤 결과 폴더 이름을 바꿉니다.

08보고서에 붙이기 전에 알아둘 범위

  • 날짜만 있는 기록을 달력 월로 집계합니다. 시작·종료 시각, 야간 근무, 시간대, 자정을 넘는 작업은 처리하지 않습니다.
  • 입력이 없는 달은 0시간 행으로 만들지 않습니다. 기록 누락과 실제 0시간을 구분해야 하기 때문입니다.
  • 중복 ID만 차단합니다. 서로 다른 ID로 같은 작업을 두 번 넣은 문제는 원본 확인이 필요합니다.
  • 집계 CSV와 PNG는 실행 시점의 결과입니다. CSV를 수정한 뒤 새 결과를 얻으려면 다시 실행해야 합니다.

실행·검증 기록

Windows 11 (10.0.26200), CPython 3.12.14 (64비트). openpyxl 3.1.5, Matplotlib 3.10.8. 예제별 사용 라이브러리는 requirements.txt에 고정했습니다.

  • 월별 3행·23.50시간·9개 기록 확인
  • PNG 생성 및 월 경계 변경 결과 검증
  • 잘못된 날짜·중복 ID·음수/NaN 시간·헤더 오류 검사
  • 원본 해시 불변과 기존 결과 보존 확인
검증 범위의 한계
  • 실제 인사·근태 시스템의 집계 규칙과 일치 여부는 검증 대상이 아닙니다.

사이트 전체의 작성·검증 원칙

직접 실행할 예제 파일

코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.

예제 ZIP 다운로드

직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.