Files
pbl02/uploads/DAT06/DAT06_student.ipynb
2026-10-08 17:15:05 +09:00

146 KiB

🎯 [DAT06] 월간 정산·보고·취합 One-click 자동화

Google Colab 실습 환경 (데이터·자동화·업무플랫폼 / Level 3)

Open In Colab


📌 1. 프로젝트 개요 및 배경

항목 내용
과정 분류 데이터·자동화·업무플랫폼 (Level 3)
학습 소요 시간 10개 미션 (5일 / 총 27.0시간)
환경 사양 Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn

📖 프로젝트 목표

미국 국방재무회계국(DFAS)은 반복적인 재무·정산 업무에 업무 자동화(RPA)를 도입해 사람이 하던 취합과 점검을 줄이는 사업을 추진해 왔다. 이 공개 사례의 학습 초점은 반복 업무 분해, 입력과 트리거, 자동 처리 순서, 오류 복구, 실행 로그, 시간 절감이다. 본 프로젝트는 이를 바탕으로 교육용 가상 부대 24개의 월간 결산 자료(물자 불출, 수리부속 소모, 탄약 정산, 장비 가동률)를 코드로 만들고, 파일 대신 메모리 안의 가상 문서와 표로 모의한다. 학습자는 업무 분해, 다형식 파싱, 검증 룰 엔진, 이상 원인 추천, 일관성 검증, 승인·반려 대시보드, 3단계 사람 결재선, 대량 배치 시험, AAR 보정, 최종 통합을 차례로 수행한다. 최종 목표는 월간 정산·보고·취합을 한 번에 실행하되 발간은 반드시 사람이 결재하도록 하는 원클릭 자동화 도구를 완성하는 것이다.

⚠️ [교육용 가상 데이터셋 원칙]

  • 본 노트북의 모든 데이터(제원, 로그, 시나리오 등)는 보안 규정을 준수하여 코드 내 난수 기반 가상 데이터로 생성됩니다.
  • 실제 부대 위치, 작전 인가, 실제 전장 정보, 군사 기밀 데이터는 일체 포함되지 않습니다.

In [ ]:
# ============================================================================
# [Google Colab 환경 설정] 필수 라이브러리 설치 및 임포트
# ============================================================================
!pip install -q numpy pandas matplotlib scipy scikit-learn

import os
import sys
import time
import math
import json
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

# 그래프 영문 폰트 기본 설정 (환경 독립성 보장)
plt.rcParams['font.sans-serif'] = 'DejaVu Sans'
plt.rcParams['axes.unicode_minus'] = False

print(f"실행 환경 준비 완료 (Python {sys.version.split()[0]})")

🎯 미션 01. 군수 월간 결산 항목(물자 불출, 수리부속 소모, 탄약 정산, 장비 가동률) 데이터 구조 분석

소요시간: 약 1.5시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

군수 월간 결산 항목(물자 불출, 수리부속 소모, 탄약 정산) 데이터 구조를 분석한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 월간 결산 4개 항목(물자 불출, 수리부속 소모, 탄약 정산, 장비 가동률)의 데이터 구조를 분석해 필드와 산식을 정의하고, 현행 결산 업무를 AS-IS/TO-BE로 비교한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 가상 부대 24개의 월간 원장(기초·수령·불출·반납·기말)과 장비 가동 현황을 만들어 항목별 필드와 재고 산식(기말 = 기초 + 수령 + 반납 - 불출) 정리하고 규격화하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: docs/dat06_as_is_to_be_spec.md 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 현행 업무(AS-IS)와 개선 업무(TO-BE) 비교의 명확성

미션 PASS 조건

  • 정량 기준: 업무 단계 분해 10단계 이상, 병목 구간 3개소 이상 도출

  • 진행 기준: 업무 정의서 및 입출력 데이터 명세 작성 완료

  • 검증 방법: 실무 프로세스와의 일치성 검증

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
# [TODO : 학생 실습 1] 군수 월간 결산 항목(물자 불출, 수리부속 소모, 탄약 정산, 장비 가동률) 데이터 구조 분석
# 미션 지침 및 검증 기준을 확인하고 코드를 실행 및 튜닝하세요.
"""군수 월간 결산 4개 항목(물자 불출, 수리부속 소모, 탄약 정산, 장비 가동률)의 데이터 구조를 분석해 스키마를 정의하고, 월간 결산 업무를 AS-IS/TO-BE 단계로 분해해 병목 구간을 도출한다."""
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

# 1) 데이터 구조 분석: 원장 4개 항목의 필드·허용값·산식
led = pd.DataFrame(make_ledger(2), columns=LEDGER_COLS)
eq = pd.DataFrame(make_equip(2))
SCHEMA = {
    "물자불출": {"fields": "month, unit, item, open, recv, issue, ret, close", "rule": "close = open + recv + ret - issue"},
    "수리부속": {"fields": "month, unit, item, open, recv, issue(소모), ret, close", "rule": "close = open + recv + ret - issue"},
    "탄약정산": {"fields": "month, unit, item, open, recv, issue(사용), ret(잔탄 반납), close", "rule": "close = open + recv + ret - issue"},
    "장비가동률": {"fields": "month, unit, hold(보유), operable(가동)", "rule": "가동률 = operable / hold, 0 <= 가동률 <= 1"},
}
print("[결산 항목별 데이터 구조] [교육용가상자료]")
for k, v in SCHEMA.items():
    print(f" - {k}: 필드[{v['fields']}] 규칙[{v['rule']}]")
print(f"\n원장 {len(led)}행(부대 {led['unit'].nunique()}개 x 품목 {led['item'].nunique()}종 x 2개월), 장비 현황 {len(eq)}행")
chk = ((led["open"] + led["recv"] + led["ret"] - led["issue"]) == led["close"]).mean()
cont = led.pivot_table(index=["unit", "item"], columns="month", values=["open", "close"])
cont_ok = (cont[("close", "2026-07")] == cont[("open", "2026-08")]).mean()
print(f"재고 산식 일치율 {chk:.0%}, 월 간 연속성(전월 기말=당월 기초) {cont_ok:.0%}")
print("분류별 월 불출·소모량 합계:")
print(led[led["month"] == "2026-08"].groupby("cat")["issue"].sum().to_string())
print(f"장비 평균 가동률 {(eq['operable'] / eq['hold']).mean():.1%} (가동률 낮은 부대 3개: "
      f"{', '.join(eq.assign(r=eq['operable'] / eq['hold']).nsmallest(3, 'r')['unit'])})")

# 2) 월간 결산 업무 AS-IS 분해(단계, 소요 분 - 가상 측정값)와 TO-BE
steps = [("예하 부대별 결산 자료 수신(메일·메신저)", 40, "제출 양식 표준화·자동 수집 함수", 3),
         ("제출 양식 확인(서식 다른 파일 구분)", 35, "형식 자동 판별·별칭 사전", 2),
         ("엑셀 값 복사·붙여넣기 취합", 90, "자동 파싱·병합", 2),
         ("품목 코드·단위 수기 통일", 60, "코드·단위 표준 변환표", 3),
         ("기초·기말 재고 산식 대조", 55, "검증 룰 15종 자동 적용", 2),
         ("전월 대비 증감 계산", 30, "전월 비교표 자동 생성", 1),
         ("이상 수치 확인(전화 문의)", 75, "이상치 후보·원인 요약 추천", 15),
         ("장비 가동률 집계", 30, "가동률 자동 계산", 1),
         ("결산 보고서 서식 작성", 80, "템플릿에 자동 채움", 3),
         ("보급관·군수과장 결재 요청·회수", 45, "3단계 전자 결재 흐름", 15),
         ("반려 사항 수정 후 재제출", 50, "반려 사유별 수정 위치 안내", 10),
         ("최종본 배포·보관", 20, "발간본 자동 보관·배포", 1)]
proc = pd.DataFrame(steps, columns=["업무 단계", "AS-IS(분)", "TO-BE 개선", "TO-BE(분)"])
proc.index = range(1, len(proc) + 1)
thr = proc["AS-IS(분)"].mean() + 0.5 * proc["AS-IS(분)"].std()
proc["병목"] = np.where(proc["AS-IS(분)"] >= thr, "병목", "")
print(f"\n[AS-IS / TO-BE 업무 단계 {len(proc)}단계] (병목 기준: 평균+0.5표준편차 = {thr:.0f}분 이상)")
print(proc[["업무 단계", "AS-IS(분)", "TO-BE(분)", "병목"]].to_string())
nb = int((proc["병목"] == "병목").sum())
a, t = proc["AS-IS(분)"].sum(), proc["TO-BE(분)"].sum()
print(f"\n업무 단계 {len(proc)}단계(목표 10단계 이상: {'달성' if len(proc) >= 10 else '미달'}), 병목 {nb}개소(목표 3개소 이상: {'달성' if nb >= 3 else '미달'})")
print(f"1회 결산 AS-IS {a}분 -> TO-BE {t}분 ({1 - t / a:.0%} 단축 예상, 가정값)")
💡 [참고] 미션 01 예상 실행 결과 (Output)
[결산 항목별 데이터 구조] [교육용가상자료]
 - 물자불출: 필드[month, unit, item, open, recv, issue, ret, close] 규칙[close = open + recv + ret - issue]
 - 수리부속: 필드[month, unit, item, open, recv, issue(소모), ret, close] 규칙[close = open + recv + ret - issue]
 - 탄약정산: 필드[month, unit, item, open, recv, issue(사용), ret(잔탄 반납), close] 규칙[close = open + recv + ret - issue]
 - 장비가동률: 필드[month, unit, hold(보유), operable(가동)] 규칙[가동률 = operable / hold, 0 <= 가동률 <= 1]

원장 528행(부대 24개 x 품목 11종 x 2개월), 장비 현황 48행
재고 산식 일치율 100%, 월 간 연속성(전월 기말=당월 기초) 100%
분류별 월 불출·소모량 합계:
cat
물자불출    8270
수리부속    8184
탄약정산    6743
장비 평균 가동률 87.0% (가동률 낮은 부대 3개: 가상23대대, 가상19대대, 가상22대대)

[AS-IS / TO-BE 업무 단계 12단계] (병목 기준: 평균+0.5표준편차 = 62분 이상)
                      업무 단계  AS-IS(분)  TO-BE(분)  병목
1   예하 부대별 결산 자료 수신(메일·메신저)        40         3    
2     제출 양식 확인(서식 다른 파일 구분)        35         2    
3           엑셀 값 복사·붙여넣기 취합        90         2  병목
4            품목 코드·단위 수기 통일        60         3    
5            기초·기말 재고 산식 대조        55         2    
6               전월 대비 증감 계산        30         1    
7           이상 수치 확인(전화 문의)        75        15  병목
8                 장비 가동률 집계        30         1    
9              결산 보고서 서식 작성        80         3  병목
10        보급관·군수과장 결재 요청·회수        45        15    
11           반려 사항 수정 후 재제출        50        10    
12                최종본 배포·보관        20         1    

업무 단계 12단계(목표 10단계 이상: 달성), 병목 3개소(목표 3개소 이상: 달성)
1회 결산 AS-IS 610분 -> TO-BE 58분 (90% 단축 예상, 가정값)

🎯 미션 02. 다양한 양식의 엑셀/CSV 데이터 자동 파싱, 정합성 검증 및 전처리 파이프라인 구축

소요시간: 약 2.0시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

엑셀/CSV 데이터 자동 파싱, 정합성 검증 및 전처리 파이프라인을 구축한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, CSV, 엑셀 내보내기(탭 구분), JSON, 공문 텍스트 형식의 결산 자료를 자동 파싱하고 정합성을 검증하는 전처리 파이프라인을 구축한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 서로 다른 서식의 가상 결산 자료 4종을 메모리 안의 문자열로 만들고 형식별 파서로 표준 레코드로 바꾼다 과정을 진행하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: src/connectors/dat06_data_loader.py 및 스키마 정의서 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 국방 서식/양식 변경에 대한 유연한 확장성

미션 PASS 조건

  • 정량 기준: 지원 파일 포맷(CSV, Excel, JSON 등) 파싱 성공률 100%

  • 진행 기준: 데이터 로더 및 스키마 유효성 검사 모듈 완성

  • 검증 방법: 다양한 형태의 샘플 군수 문서 대상 파싱 테스트

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""CSV, 엑셀 내보내기(탭 구분), JSON, 공문 텍스트 4가지 형식의 가상 결산 자료를 메모리 안에서 자동 파싱하고 머리글 별칭 대응·정합성 검증·전처리를 거쳐 표준 원장으로 합치는 파이프라인을 구축한다."""
import csv
import io
import json
import re
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

TRUTH = [r for r in make_ledger(2) if r["month"] == "2026-08"]
FIELDS = ["unit", "cat", "item", "open", "recv", "issue", "ret", "close"]
ALIAS = {"unit": ["unit", "부대", "부대명", "제출부대"], "cat": ["cat", "구분", "분류"], "item": ["item", "품목", "품목명"],
         "open": ["open", "기초", "기초재고"], "recv": ["recv", "수령", "입고"], "issue": ["issue", "불출", "소모", "사용"],
         "ret": ["ret", "반납"], "close": ["close", "기말", "기말재고"]}
H2F = {h: f for f, hs in ALIAS.items() for h in hs}
by_unit = lambda a, b: [r for r in TRUTH if UNITS[a] <= r["unit"] <= UNITS[b]]

# --- 가상 원본 4종(문자열) 생성: 형식마다 서식이 다르다 ---
def mk_csv(rows):
    # [TODO : 학생 실습 2] 다양한 양식의 엑셀/CSV 데이터 자동 파싱, 정합성 검증 및 전처리 파이프라인 구축 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
def mk_tab(rows):        # 엑셀에서 '탭 구분 텍스트'로 내보낸 형태, 머리글이 양식 개정으로 바뀜
    # [TODO : 학생 실습 2] 다양한 양식의 엑셀/CSV 데이터 자동 파싱, 정합성 검증 및 전처리 파이프라인 구축 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
def mk_json(rows):
    return json.dumps({"form": "결산서", "rows": [{f: r[f] for f in FIELDS} for r in rows]}, ensure_ascii=False)
def mk_memo(rows):       # 공문 본문 형태 비정형 텍스트
    return "\n".join(f"{i + 1}. {r['unit']} {r['cat']} {r['item']}: 기초 {r['open']}, 수령 {r['recv']}, 불출 {r['issue']}, 반납 {r['ret']}, 기말 {r['close']}"
                     for i, r in enumerate(rows))
RAW = {"csv": mk_csv(by_unit(0, 2)), "excel_tab": mk_tab(by_unit(3, 5)), "json": mk_json(by_unit(6, 8)), "memo": mk_memo(by_unit(9, 11))}
RAW["csv"] += "\n가상99대대,물자불출,급식류,10,0,3,0,7"   # 존재하지 않는 부대 코드(보류 대상)
RAW["memo"] += "\n99. 가상10대대 탄약정산 훈련탄A: 기초 50, 수령 10, 불출 abc, 반납 0, 기말 60"   # 숫자 아님(보류 대상)

# --- 파서: 모두 표준 레코드(dict) 리스트로 변환 ---
def norm(row):
    out = {}
    for k, v in row.items():
        f = H2F.get(str(k).strip())
        if f:
            out[f] = v.strip() if isinstance(v, str) else v
    return out
def parse_csv(s, delim=","):
    return [norm(r) for r in csv.DictReader(io.StringIO(s), delimiter=delim)]
def parse_json(s):
    return [norm(r) for r in json.loads(s)["rows"]]
def parse_memo(s):
    pat = re.compile(r"\d+\.\s*(\S+)\s+(\S+)\s+(\S+):\s*기초\s*(\S+),\s*수령\s*(\S+),\s*불출\s*(\S+),\s*반납\s*(\S+),\s*기말\s*(\S+)")
    return [dict(zip(FIELDS, m.groups())) for m in map(pat.match, s.splitlines()) if m]
PARSERS = {"csv": parse_csv, "excel_tab": lambda s: parse_csv(s, "\t"), "json": parse_json, "memo": parse_memo}

# --- 전처리·정합성 검증: 형 변환, 코드 확인, 산식 대조. 오류 레코드는 반영 보류 ---
known_items = {it for v in ITEMS.values() for it in v}
def clean(r, fmt):
    try:
        for f in ("open", "recv", "issue", "ret", "close"):
            r[f] = int(r[f])
    except (ValueError, KeyError):
        return None, f"[{fmt}] 숫자 변환 실패: {r.get('unit')} {r.get('item')}"
    if r["unit"] not in UNITS:
        return None, f"[{fmt}] 존재하지 않는 부대 코드 {r['unit']}"
    if r["item"] not in known_items:
        return None, f"[{fmt}] 알 수 없는 품목 {r['item']}"
    if r["open"] + r["recv"] + r["ret"] - r["issue"] != r["close"]:
        return None, f"[{fmt}] 재고 산식 불일치 {r['unit']} {r['item']}"
    return r, None

good, hold, stat = [], [], {}
for fmt, text in RAW.items():
    try:
        recs = PARSERS[fmt](text)
        stat[fmt] = {"parse_ok": True, "n": len(recs)}
    except Exception as ex:
        recs, stat[fmt] = [], {"parse_ok": False, "n": 0, "err": repr(ex)[:50]}
    for r in recs:
        c, why = clean(r, fmt)
        (good if c else hold).append(c or why)
df = pd.DataFrame(good, columns=FIELDS).sort_values(["unit", "item"]).reset_index(drop=True)

print("[형식별 파싱 결과] [교육용가상자료]")
for fmt, s in stat.items():
    print(f" - {fmt:<9} 파싱 {'성공' if s['parse_ok'] else '실패'}, 읽은 레코드 {s['n']}건")
rate = sum(s["parse_ok"] for s in stat.values()) / len(stat)
print(f"파싱 성공률 {rate:.0%}({len(stat)}개 형식), 정상 반영 {len(df)}건, 반영 보류 {len(hold)}건")
for w in hold:
    print("   보류:", w)
truth = pd.DataFrame(by_unit(0, 11), columns=FIELDS).sort_values(["unit", "item"]).reset_index(drop=True)
same = df.equals(truth.astype(df.dtypes.to_dict()))
print(f"원본 대비 값 일치: {'전부 일치' if same else '불일치'} ({len(df)}행 = 12개 부대 x 11품목 = {12 * 11}행)")
print("표준 원장 미리보기(앞 4행):")
print(df.head(4).to_string(index=False))
print(f"\n목표(4개 형식 파싱 성공률 100%, 양식 변경 대응, 오류 레코드 보류): {'달성' if rate == 1 and same and len(hold) == 2 else '미달'}")
print("새 서식 대응은 ALIAS 사전에 머리글 별칭만 추가하면 된다(코드 수정 불필요).")
💡 [참고] 미션 02 예상 실행 결과 (Output)
[형식별 파싱 결과] [교육용가상자료]
 - csv       파싱 성공, 읽은 레코드 34건
 - excel_tab 파싱 성공, 읽은 레코드 33건
 - json      파싱 성공, 읽은 레코드 33건
 - memo      파싱 성공, 읽은 레코드 34건
파싱 성공률 100%(4개 형식), 정상 반영 132건, 반영 보류 2건
   보류: [csv] 존재하지 않는 부대 코드 가상99대대
   보류: [memo] 숫자 변환 실패: 가상10대대 훈련탄A
원본 대비 값 일치: 전부 일치 (132행 = 12개 부대 x 11품목 = 132행)
표준 원장 미리보기(앞 4행):
  unit  cat item  open  recv  issue  ret  close
가상01대대 물자불출  급식류   402    51     39    3    417
가상01대대 탄약정산  모의탄   943   147    117   11    984
가상01대대 수리부속 베어링류   261    34    142    1    154
가상01대대 물자불출   유류  1279   257    108    5   1433

목표(4개 형식 파싱 성공률 100%, 양식 변경 대응, 오류 레코드 보류): 달성
새 서식 대응은 ALIAS 사전에 머리글 별칭만 추가하면 된다(코드 수정 불필요).

🎯 미션 03. 표준 정산 규칙 기반 1차 월간 결산 집계 및 통계 산출 Baseline 엔진 구현

소요시간: 약 2.5시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

표준 정산 규칙 기반 1차 월간 결산 집계 Baseline 엔진을 구현한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 표준 정산 규칙을 설정 문자열 룰 15종으로 분리하고, 이를 적용한 1차 월간 결산 집계 Baseline 엔진을 구현한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 룰마다 가상 조항 번호, 조건식, 오류 메시지를 설정으로 두어 코드 수정 없이 바꿀 수 있게 수행하고 검증하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: src/engine/dat06_rule_engine.py 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 군 규정 및 지침 조항과의 정확한 일치성

미션 PASS 조건

  • 정량 기준: 업무 검증 룰 15종 이상 코딩, 룰 처리 지연시간 50ms 이하

  • 진행 기준: 룰 엔진 단위 테스트 100% 통과

  • 검증 방법: 경계 조건(Boundary condition)에 대한 룰 판정 검증

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""설정 문자열로 분리한 검증 룰 15종(가상 정산 규정 조항 대응)으로 월간 원장을 검사하고, 표준 정산 규칙에 따른 1차 결산 집계 Baseline을 계산하며 처리 지연시간과 경계 조건 단위 테스트를 확인한다."""
import time
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

# 1) 검증 룰 설정: (룰ID, 가상 조항, 조건식(참이어야 정상), 오류 메시지) - 코드 수정 없이 문자열만 바꿔 룰 변경
RULES = [
    ("V01", "가상규정 제1조", "unit in UNITS", "존재하지 않는 부대"),
    ("V02", "가상규정 제2조", "item in UNIT_CODE", "알 수 없는 품목"),
    ("V03", "가상규정 제3조", "cat in ITEMS and item in ITEMS[cat]", "구분과 품목 불일치"),
    ("V04", "가상규정 제4조", "open >= 0", "기초 재고 음수"),
    ("V05", "가상규정 제4조", "recv >= 0", "수령량 음수"),
    ("V06", "가상규정 제4조", "issue >= 0", "불출량 음수"),
    ("V07", "가상규정 제4조", "ret >= 0", "반납량 음수"),
    ("V08", "가상규정 제5조", "close == open + recv + ret - issue", "기말 산식 불일치"),
    ("V09", "가상규정 제6조", "issue <= open + recv", "가용량 초과 불출"),
    ("V10", "가상규정 제6조", "ret <= issue", "불출보다 큰 반납"),
    ("V11", "가상규정 제7조", "close >= 0", "기말 재고 음수"),
    ("V12", "가상규정 제8조", "len(month) == 7 and month[4] == '-'", "결산월 형식 오류"),
    ("V13", "가상규정 제9조", "cat != '탄약정산' or issue <= 5000", "탄약 월 사용량 한도 초과"),
    ("V14", "가상규정 제10조", "isinstance(open, int) and isinstance(close, int)", "수량 자료형 오류"),
    ("V15", "가상규정 제11조", "issue <= max(open + recv, 1) * 0.9 or issue == 0", "불출 비율 과다(90% 초과)"),
]
COMPILED = [(rid, art, compile(expr, rid, "eval"), msg) for rid, art, expr, msg in RULES]
CTX = {"UNITS": UNITS, "UNIT_CODE": UNIT_CODE, "ITEMS": ITEMS}


def check_row(row):
    # [TODO : 학생 실습 3] 표준 정산 규칙 기반 1차 월간 결산 집계 및 통계 산출 Baseline 엔진 구현 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
rows = make_ledger(2)
rng = np.random.default_rng(3)
for i in rng.choice(len(rows), 6, replace=False):          # 오류 6건 주입(룰이 잡는지 확인)
    k = int(rng.integers(0, 3))
    rows[i] = {**rows[i], **[{"close": rows[i]["close"] + 7}, {"issue": rows[i]["open"] + rows[i]["recv"] + 50}, {"ret": -3}][k]}
lat, found = [], []
for r in rows:
    t0 = time.perf_counter()
    res = check_row(r)
    lat.append((time.perf_counter() - t0) * 1000)
    found += [(r["month"], r["unit"], r["item"], *x) for x in res]
lat = np.array(lat)
print(f"[검증 룰 {len(RULES)}종 적용] 대상 {len(rows)}행, 위반 {len(found)}건 [교육용가상자료]")
cnt = pd.Series([f[3] for f in found]).value_counts()
print("룰별 위반 건수:", {k: int(v) for k, v in cnt.items()})
print("위반 예시(3건):")
for f in found[:3]:
    print("  ", f)
print(f"룰 처리 지연: 평균 {lat.mean():.3f}ms, 최대 {lat.max():.3f}ms (목표 50ms 이하: {'달성' if lat.max() <= 50 else '미달'}, 환경에 따라 다름)")

# 3) 1차 결산 집계 Baseline: 부대 x 구분 합계, 구분별 통계
clean = pd.DataFrame([r for r in make_ledger(2) if r["month"] == "2026-08"])
pv = clean.pivot_table(index="unit", columns="cat", values="issue", aggfunc="sum")
print("\n[1차 결산 집계 - 8월 불출·소모량 부대별(상위 4개)]")
print(pv.head(4).to_string())
st = pv.agg(["sum", "mean", "std"]).round(1)
print("[구분별 합계·평균·표준편차]")
print(st.to_string())
print(f"전체 기말 재고 합계 {int(clean['close'].sum())}, 보고 대상 부대 {clean['unit'].nunique()}개")

# 4) 경계 조건 단위 테스트
base = {"month": "2026-08", "unit": UNITS[0], "cat": "물자불출", "item": "유류", "open": 100, "recv": 0, "issue": 0, "ret": 0, "close": 100}
tests = [("정상 행", base, []),
         ("불출 = 가용량 정확히 일치(V15만 위반)", {**base, "issue": 100, "close": 0}, ["V15"]),
         ("불출 = 가용량+1 (V09, V11, V15)", {**base, "issue": 101, "close": -1}, ["V09", "V11", "V15"]),
         ("기말 0 허용", {**base, "issue": 90, "close": 10, "ret": 0}, []),
         ("반납 = 불출 경계 허용", {**base, "issue": 50, "ret": 50, "close": 100}, []),
         ("탄약 한도 5000 경계", {**base, "cat": "탄약정산", "item": "훈련탄A", "open": 9000, "issue": 5000, "close": 4000}, []),
         ("탄약 한도 5001 초과", {**base, "cat": "탄약정산", "item": "훈련탄A", "open": 9000, "issue": 5001, "close": 3999}, ["V13"]),
         ("결산월 형식 오류", {**base, "month": "202608"}, ["V12"]),
         ("자료형 오류(문자열 수량)", {**base, "open": "100"}, ["V14"])]   # 이 경우 V14 포함 여부만 확인
ok = 0
print("\n[경계 조건 단위 테스트]")
for name, r, exp in tests:
    got = sorted(x[0] for x in check_row(r))
    p = set(exp) <= set(got) if name.startswith("자료형") else got == exp
    ok += p
    print(f" - {name}: 기대 {exp} / 결과 {got} -> {'PASS' if p else 'FAIL'}")
print(f"단위 테스트 {ok}/{len(tests)} 통과({ok / len(tests):.0%})")
print(f"목표(룰 15종 이상, 지연 50ms 이하, 단위 테스트 100%): {'달성' if len(RULES) >= 15 and lat.max() <= 50 and ok == len(tests) else '미달'}")
💡 [참고] 미션 03 예상 실행 결과 (Output)
[검증 룰 15종 적용] 대상 528행, 위반 13건 [교육용가상자료]
룰별 위반 건수: {'V08': 6, 'V09': 3, 'V15': 3, 'V07': 1}
위반 예시(3건):
   ('2026-07', '가상05대대', '급식류', 'V08', '가상규정 제5조', '기말 산식 불일치')
   ('2026-07', '가상05대대', '급식류', 'V09', '가상규정 제6조', '가용량 초과 불출')
   ('2026-07', '가상05대대', '급식류', 'V15', '가상규정 제11조', '불출 비율 과다(90% 초과)')
룰 처리 지연: 평균 0.008ms, 최대 0.072ms (목표 50ms 이하: 달성, 환경에 따라 다름)

[1차 결산 집계 - 8월 불출·소모량 부대별(상위 4개)]
cat     물자불출  수리부속  탄약정산
unit                    
가상01대대   378   385   384
가상02대대   255   427   234
가상03대대   239   371   168
가상04대대   305   258   319
[구분별 합계·평균·표준편차]
cat     물자불출    수리부속    탄약정산
sum   8270.0  8184.0  6743.0
mean   344.6   341.0   281.0
std     79.3    72.2    75.9
전체 기말 재고 합계 199291, 보고 대상 부대 24개

[경계 조건 단위 테스트]
 - 정상 행: 기대 [] / 결과 [] -> PASS
 - 불출 = 가용량 정확히 일치(V15만 위반): 기대 ['V15'] / 결과 ['V15'] -> PASS
 - 불출 = 가용량+1 (V09, V11, V15): 기대 ['V09', 'V11', 'V15'] / 결과 ['V09', 'V11', 'V15'] -> PASS
 - 기말 0 허용: 기대 [] / 결과 [] -> PASS
 - 반납 = 불출 경계 허용: 기대 [] / 결과 [] -> PASS
 - 탄약 한도 5000 경계: 기대 [] / 결과 [] -> PASS
 - 탄약 한도 5001 초과: 기대 ['V13'] / 결과 ['V13'] -> PASS
 - 결산월 형식 오류: 기대 ['V12'] / 결과 ['V12'] -> PASS
 - 자료형 오류(문자열 수량): 기대 ['V14'] / 결과 ['V04', 'V08', 'V09', 'V14', 'V15'] -> PASS
단위 테스트 9/9 통과(100%)
목표(룰 15종 이상, 지연 50ms 이하, 단위 테스트 100%): 달성

🎯 미션 04. 데이터 이상치(전월 대비 급증 항목) 자동 감지 및 이상 원인 요약 추천 모듈 고도화

소요시간: 약 3.0시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

전월 대비 급증 항목 이상치 감지 및 이상 원인 요약 추천 모듈을 고도화한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 전월 대비 급증한 품목을 자동 감지하고 이상 원인을 요약해 추천하는 모듈을 고도화한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 품목별 증가율의 로그 값에 중앙값과 MAD 기반 점수를 적용해 급증 품목을 찾는다 과정을 진행하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: src/ai/dat06_intelligence_module.py 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 추천 사유의 합리성 및 실무 수용성

미션 PASS 조건

  • 정량 기준: 추천/분류 정확도 85% 이상 달성

  • 진행 기준: AI 추론 모듈 구현 및 벤치마크 테스트 완료

  • 검증 방법: 과거 승인/처리 완료 데이터와의 추천 일치도 검증

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""전월 대비 급증한 품목을 로버스트 통계로 자동 감지하고, 훈련·정비 일정 등 맥락 정보와 대조해 이상 원인을 요약·추천하는 모듈을 구현해 감지 정확도와 원인 분류 정확도를 측정한다."""
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

# 1) 가상 2개월 데이터 + 정답(숨은 원인) 생성
led = pd.DataFrame(make_ledger(2))
p7 = led[led["month"] == "2026-07"].set_index(["unit", "item"])
p8 = led[led["month"] == "2026-08"].set_index(["unit", "item"])
d = pd.DataFrame({"cat": p8["cat"], "prev": p7["issue"].clip(lower=1), "cur": p8["issue"]})
rng = np.random.default_rng(4)
n = len(d)
d["train_days"] = rng.integers(0, 5, n)          # 해당 부대 월 훈련 일수(맥락)
d["repair_orders"] = rng.integers(0, 4, n)       # 정비 지시서 건수(맥락)
d["special"] = 0                                 # 일회성 특별 지급 승인 여부
d["truth"] = "정상"
d["cur"] = (d["prev"] * rng.uniform(0.7, 1.4, n)).round().astype(int)    # 정상 변동(-30%~+40%)
CAUSES = ["훈련 증가", "정비 집중", "입력 자릿수 오류", "일회성 특별 지급", "원인 불명"]
idx = rng.choice(n, 36, replace=False)
for k, i in enumerate(idx):
    c = CAUSES[k % 5]
    d.iloc[i, d.columns.get_loc("truth")] = c
    f = {"훈련 증가": rng.uniform(1.8, 3.0), "정비 집중": rng.uniform(1.8, 3.2), "입력 자릿수 오류": 10.0,
         "일회성 특별 지급": rng.uniform(2.0, 4.0), "원인 불명": rng.uniform(1.9, 3.0)}[c]
    d.iloc[i, d.columns.get_loc("cur")] = int(d.iloc[i]["prev"] * f)
    if c == "훈련 증가":
        d.iloc[i, d.columns.get_loc("train_days")] = rng.integers(8, 14)
    if c == "정비 집중":
        d.iloc[i, d.columns.get_loc("repair_orders")] = rng.integers(7, 12)
    if c == "일회성 특별 지급":
        d.iloc[i, d.columns.get_loc("special")] = 1
for i in rng.choice(n, 8, replace=False):        # 정상이지만 활동이 많았던 곳(오탐 유도)
    d.iloc[i, d.columns.get_loc("train_days")] = rng.integers(8, 14)

# 2) 감지: 품목별 로그 증가율의 중앙값·MAD 기반 z 점수
d["ratio"] = d["cur"] / d["prev"]
lr = np.log(d["ratio"])
med = lr.groupby(level="item").transform("median")
mad = (lr - med).abs().groupby(level="item").transform("median").clip(lower=0.05) * 1.4826
d["z"] = (lr - med) / mad
d["flag"] = (d["z"] > 2.0) & (d["ratio"] >= 1.6)

# 3) 원인 요약 추천(규칙 기반, 사람이 확인할 후보를 제시)
def recommend(r):
    # [TODO : 학생 실습 4] 데이터 이상치(전월 대비 급증 항목) 자동 감지 및 이상 원인 요약 추천 모듈 고도화 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
ACTION = {"훈련 증가": "훈련 계획 대조 후 정상 처리", "정비 집중": "정비 지시서와 소모 수량 대조", "입력 자릿수 오류": "제출 부대에 수치 재확인 요청",
          "일회성 특별 지급": "승인 문서 번호 확인", "원인 불명": "담당자가 부대에 직접 문의"}
d["cause"] = np.where(d["flag"], d.apply(recommend, axis=1), "정상")

# 4) 평가
truth_anom = d["truth"] != "정상"
tp = int((d["flag"] & truth_anom).sum()); fp = int((d["flag"] & ~truth_anom).sum()); fn = int((~d["flag"] & truth_anom).sum())
prec, rec = tp / max(tp + fp, 1), tp / max(tp + fn, 1)
acc = float((d["flag"] == truth_anom).mean())
both = d[d["flag"] & truth_anom]
cause_acc = float((both["cause"] == both["truth"]).mean())
total_acc = float(((d["cause"] == d["truth"])).mean())
print(f"[이상 감지] 대상 {n}건(부대 24 x 품목 11), 실제 급증 {int(truth_anom.sum())}건 [교육용가상자료]")
print(f"감지 {int(d['flag'].sum())}건: 정확도 {acc:.1%}, 정밀도 {prec:.1%}, 재현율 {rec:.1%} (TP {tp}, FP {fp}, FN {fn})")
print(f"원인 분류 정확도(감지된 실제 급증 중) {cause_acc:.1%}, 전체 분류(정상 포함) 정확도 {total_acc:.1%}")
print("\n[원인별 감지·추천 현황]")
print(pd.crosstab(both["truth"], both["cause"]).to_string())
print("\n[이상 원인 요약 추천 예시 - 증가율 상위 5건]")
for (u, it), r in d[d["flag"]].sort_values("ratio", ascending=False).head(5).iterrows():
    print(f" - {u} {it}: 전월 {int(r['prev'])} -> 당월 {int(r['cur'])}({r['ratio']:.1f}배), 추정 원인 '{r['cause']}' -> 조치: {ACTION[r['cause']]}")
print("추천은 후보 제시이며 최종 원인 판단과 수치 정정은 담당자가 확인한다.")
print(f"\n목표(추천·분류 정확도 85% 이상): 감지 {'달성' if acc >= 0.85 else '미달'}, 원인 분류 {'달성' if cause_acc >= 0.85 else '미달'}")
💡 [참고] 미션 04 예상 실행 결과 (Output)
[이상 감지] 대상 264건(부대 24 x 품목 11), 실제 급증 36건 [교육용가상자료]
감지 35건: 정확도 99.6%, 정밀도 100.0%, 재현율 97.2% (TP 35, FP 0, FN 1)
원인 분류 정확도(감지된 실제 급증 중) 100.0%, 전체 분류(정상 포함) 정확도 99.6%

[원인별 감지·추천 현황]
cause      원인 불명  일회성 특별 지급  입력 자릿수 오류  정비 집중  훈련 증가
truth                                               
원인 불명          6          0          0      0      0
일회성 특별 지급      0          7          0      0      0
입력 자릿수 오류      0          0          7      0      0
정비 집중          0          0          0      7      0
훈련 증가          0          0          0      0      8

[이상 원인 요약 추천 예시 - 증가율 상위 5건]
 - 가상01대대 훈련탄B: 전월 93 -> 당월 930(10.0배), 추정 원인 '입력 자릿수 오류' -> 조치: 제출 부대에 수치 재확인 요청
 - 가상15대대 피복류: 전월 119 -> 당월 1190(10.0배), 추정 원인 '입력 자릿수 오류' -> 조치: 제출 부대에 수치 재확인 요청
 - 가상15대대 정비소모품: 전월 124 -> 당월 1240(10.0배), 추정 원인 '입력 자릿수 오류' -> 조치: 제출 부대에 수치 재확인 요청
 - 가상19대대 모의탄: 전월 174 -> 당월 1740(10.0배), 추정 원인 '입력 자릿수 오류' -> 조치: 제출 부대에 수치 재확인 요청
 - 가상21대대 정비소모품: 전월 146 -> 당월 1460(10.0배), 추정 원인 '입력 자릿수 오류' -> 조치: 제출 부대에 수치 재확인 요청
추천은 후보 제시이며 최종 원인 판단과 수치 정정은 담당자가 확인한다.

목표(추천·분류 정확도 85% 이상): 감지 달성, 원인 분류 달성

🎯 미션 05. 정산 결과의 회계적 일관성 검증 및 신뢰도 검증 엔진 개발

소요시간: 약 3.0시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

정산 결과의 회계적 일관성 검증 및 신뢰도 검증 엔진을 개발한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 정산 결과의 회계적 일관성을 검증하고 부대별 신뢰도 점수를 산출하는 엔진을 입력-분석-검토-결과 4개 화면으로 구현한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 재고 산식, 전월 기말과 당월 기초의 연속성, 부대 합계와 분류 합계의 교차 대조, 장비 가동률 범위를 검사하고 오류 8건을 주입해 검출 여부 확인하고 유효성을 점검하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: src/ui/dat06_app.py 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 군 실무자 친화적 디자인 및 조작 편의성

미션 PASS 조건

  • 정량 기준: 핵심 화면 4종(입력-분석-검토-결과) 구현 완료

  • 진행 기준: UI 프로토타입 로컬 서버 구동 및 전체 페이지 네비게이션 확인

  • 검증 방법: 사용성 평가(UX Walkthrough) 및 인터랙션 테스트

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
# [TODO : 학생 실습 5] 정산 결과의 회계적 일관성 검증 및 신뢰도 검증 엔진 개발
# 미션 지침 및 검증 기준을 확인하고 코드를 실행 및 튜닝하세요.
"""결산 결과의 회계적 일관성(재고 산식·월 연속성·합계 교차 대조·가동률 범위)을 검증해 부대별 신뢰도 점수를 산출하고, 입력-분석-검토-결과 4개 화면을 텍스트 화면으로 구현한다."""
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

# ---------- 화면 1: 입력 ----------
led = pd.DataFrame(make_ledger(2))
eq = pd.DataFrame(make_equip(2))
cur = led[led["month"] == "2026-08"].copy()
prev = led[led["month"] == "2026-07"].set_index(["unit", "item"])
rng = np.random.default_rng(5)
faults = []                                        # 가상 오류 주입 8건
for i in rng.choice(cur.index, 8, replace=False):
    kind = ["산식", "연속성", "산식", "연속성"][len(faults) % 4]
    if kind == "산식":
        cur.loc[i, "close"] += int(rng.integers(5, 40))
    else:
        cur.loc[i, "open"] += int(rng.integers(5, 40)); cur.loc[i, "close"] = cur.loc[i, "open"] + cur.loc[i, "recv"] + cur.loc[i, "ret"] - cur.loc[i, "issue"]
    faults.append((cur.loc[i, "unit"], cur.loc[i, "item"], kind))
eq2 = eq[eq["month"] == "2026-08"].copy()
eq2.iloc[3, eq2.columns.get_loc("operable")] = eq2.iloc[3]["hold"] + 4   # 가동 대수가 보유 대수 초과
SCREEN1 = f"원장 {len(cur)}행(월 2026-08), 장비 현황 {len(eq2)}행, 전월 원장 {len(prev)}행 적재 / 양식 검사 통과"
print("[화면 1. 입력]", SCREEN1)

# ---------- 화면 2: 분석(일관성 검사 4종) ----------
issues = []
for r in cur.itertuples():
    if r.open + r.recv + r.ret - r.issue != r.close:
        issues.append((r.unit, r.item, "C1 재고 산식 불일치", abs(r.open + r.recv + r.ret - r.issue - r.close)))
    if prev.loc[(r.unit, r.item), "close"] != r.open:
        issues.append((r.unit, r.item, "C2 전월 기말 != 당월 기초", abs(prev.loc[(r.unit, r.item), "close"] - r.open)))
unit_sum = cur.groupby("unit")[["open", "recv", "ret", "issue", "close"]].sum()
cat_sum = cur.groupby(["unit", "cat"])[["open", "recv", "ret", "issue", "close"]].sum().groupby("unit").sum()
for u in unit_sum.index:                           # C3 교차 대조: 부대 합계 = 분류 합계의 합
    if not (unit_sum.loc[u] == cat_sum.loc[u]).all():
        issues.append((u, "-", "C3 합계 교차 대조 불일치", 0))
for r in eq2.itertuples():
    if not (0 <= r.operable <= r.hold):
        issues.append((r.unit, "장비", "C4 가동 대수가 보유 대수 초과", r.operable - r.hold))
iss = pd.DataFrame(issues, columns=["부대", "품목", "검사", "차이"])
print(f"[화면 2. 분석] 검사 4종 수행, 불일치 {len(iss)}건")
print(iss["검사"].value_counts().to_string())

# ---------- 신뢰도 점수 ----------
score = pd.Series(100.0, index=UNITS)
W = {"C1": 12, "C2": 10, "C3": 15, "C4": 8}
for r in iss.itertuples():
    score[r.부대] -= W[r.검사[:2]]
score = score.clip(lower=0)
grade = pd.cut(score, [-1, 69, 89, 100], labels=["C(재제출)", "B(검토)", "A(양호)"])

# ---------- 화면 3: 검토 ----------
print(f"\n[화면 3. 검토] 신뢰도 100점 미만 부대와 검토 대기 사항 [교육용가상자료]")
low = iss.groupby("부대").agg(건수=("검사", "count"), 유형=("검사", lambda s: ", ".join(sorted(set(x[:2] for x in s))))).join(score.rename("점수"))
print(low.sort_values("점수").to_string())
print("검토자는 불일치 건별로 '수정 요청 / 정상 확인' 중 선택하며, 수정은 제출 부대가 한다(시스템이 값을 임의로 바꾸지 않는다).")

# ---------- 화면 4: 결과 ----------
detected = {(u, i) for u, i, k in faults}
caught = {(r.부대, r.품목) for r in iss.itertuples() if r.검사[:2] in ("C1", "C2")}
print(f"\n[화면 4. 결과] 부대 {len(score)}개 중 A {int((grade == 'A(양호)').sum())} / B {int((grade == 'B(검토)').sum())} / C {int((grade == 'C(재제출)').sum())}")
print(f"평균 신뢰도 {score.mean():.1f}점, 주입 오류 {len(faults)}건 중 검출 {len(detected & caught)}건({len(detected & caught) / len(faults):.0%}), 가동률 범위 오류 {int((iss['검사'].str[:2] == 'C4').sum())}건")
ok = len(detected & caught) == len(faults)
print(f"\n목표(핵심 화면 4종 구현, 일관성 검증 엔진 동작): {'달성' if ok else '미달'} (화면 4종: 입력/분석/검토/결과)")
💡 [참고] 미션 05 예상 실행 결과 (Output)
[화면 1. 입력] 원장 264행(월 2026-08), 장비 현황 24행, 전월 원장 264행 적재 / 양식 검사 통과
[화면 2. 분석] 검사 4종 수행, 불일치 9건
검사
C2 전월 기말 != 당월 기초     4
C1 재고 산식 불일치          4
C4 가동 대수가 보유 대수 초과    1

[화면 3. 검토] 신뢰도 100점 미만 부대와 검토 대기 사항 [교육용가상자료]
        건수      유형    점수
부대                      
가상16대대   2  C1, C2  78.0
가상12대대   1      C1  88.0
가상13대대   1      C1  88.0
가상20대대   1      C1  88.0
가상01대대   1      C2  90.0
가상07대대   1      C2  90.0
가상19대대   1      C2  90.0
가상04대대   1      C4  92.0
검토자는 불일치 건별로 '수정 요청 / 정상 확인' 중 선택하며, 수정은 제출 부대가 한다(시스템이 값을 임의로 바꾸지 않는다).

[화면 4. 결과] 부대 24개 중 A 20 / B 4 / C 0
평균 신뢰도 96.0점, 주입 오류 8건 중 검출 8건(100%), 가동률 범위 오류 1건

목표(핵심 화면 4종 구현, 일관성 검증 엔진 동작): 달성 (화면 4종: 입력/분석/검토/결과)

🎯 미션 06. 군수 결산 보고서 생성 및 승인/반려 관리 대화형 웹 대시보드 구현

소요시간: 약 2.5시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

군수 결산 보고서 생성 및 승인/반려 대화형 웹 대시보드를 구현한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 결산 보고서 생성과 승인·반려 상태를 관리하는 대시보드를 구현하고, 예외 상황에서도 중단되지 않는 입력 검증기와 에러 핸들러를 갖춘다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 부대별 보고서를 생성해 작성-제출-승인-반려 상태 전이를 관리하고 현황판과 그래프로 보여 준다 과정을 진행하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: src/utils/dat06_validator.py 및 에러 핸들러 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 사용자에게 알기 쉬운 에러 메시지 및 조치 가이드 제공

미션 PASS 조건

  • 정량 기준: 예외 상황 발생 시 크래시(Crash) 0건, 에러 로그 기록률 100%

  • 진행 기준: 예외 주입 테스트 10종 통과 및 감사 로그 파일 생성

  • 검증 방법: 비정상 데이터(결측, 특수문자, 대용량) 스트레스 테스트

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""부대별 결산 보고서를 생성하고 제출·승인·반려 상태를 관리하는 대시보드(상태 현황판·그래프)를 구현하며, 입력 검증기와 에러 핸들러로 예외 10종을 크래시 없이 로그에 기록하고 사용자 조치 안내를 출력한다."""
import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

LEDGER = pd.DataFrame(make_ledger(2))
LOG, REPORTS = [], {}


class AppError(Exception):
    def __init__(self, code, msg, guide):
        super().__init__(msg); self.code, self.msg, self.guide = code, msg, guide


def validate(unit, month, rows):
    # [TODO : 학생 실습 6] 군수 결산 보고서 생성 및 승인/반려 관리 대화형 웹 대시보드 구현 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
def generate(unit, month, data=None):
    # [TODO : 학생 실습 6] 군수 결산 보고서 생성 및 승인/반려 관리 대화형 웹 대시보드 구현 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
FLOW = {"제출": ("작성", "제출"), "승인": ("제출", "승인"), "반려": ("제출", "반려"), "재작성": ("반려", "작성")}


def act(rid, action, who, reason=""):
    if rid not in REPORTS:
        raise AppError("E08", f"보고서 {rid} 없음", "보고서 번호를 목록에서 확인")
    if action not in FLOW:
        raise AppError("E09", f"알 수 없는 처리 '{action}'", "제출/승인/반려/재작성 중 선택")
    need, new = FLOW[action]
    if REPORTS[rid]["status"] != need:
        raise AppError("E10", f"현재 상태 '{REPORTS[rid]['status']}'에서 '{action}' 불가", f"'{need}' 상태에서만 가능")
    if action == "반려" and not reason.strip():
        raise AppError("E11", "반려 사유 없음", "반려 사유를 한 줄 이상 입력")
    REPORTS[rid]["status"] = new
    REPORTS[rid]["hist"].append((new, who + (f": {reason}" if reason else "")))


def safe(fn, *a, **k):
    """에러 핸들러: 모든 예외를 잡아 로그에 남기고 사용자에게 알기 쉬운 메시지와 조치를 돌려준다(크래시 없음)."""
    try:
        r = fn(*a, **k)
        LOG.append({"level": "INFO", "code": "OK", "msg": fn.__name__}); return r
    except AppError as e:
        LOG.append({"level": "ERROR", "code": e.code, "msg": e.msg, "guide": e.guide}); return None
    except Exception as e:                    # 예상 밖 오류도 기록
        LOG.append({"level": "ERROR", "code": "E99", "msg": repr(e)[:50], "guide": "운영 담당자에게 로그 전달"}); return None


# 1) 정상 흐름: 12개 부대 보고서 생성 -> 제출 -> 승인 8 / 반려 2 / 대기 2
rids = [safe(generate, u, "2026-08") for u in UNITS[:12]]
for i, rid in enumerate(rids):
    safe(act, rid, "제출", f"담당자{i % 3 + 1}")
    if i < 8:
        safe(act, rid, "승인", "보급관")
    elif i < 10:
        safe(act, rid, "반려", "보급관", "기말 재고 산식 재확인 요망")
board = pd.DataFrame([{"보고서": k, **{x: v[x] for x in ("unit", "status", "issue", "close")}} for k, v in REPORTS.items()])
print("[결산 보고서 현황판] [교육용가상자료]")
print(board.rename(columns={"unit": "부대", "status": "상태", "issue": "불출합", "close": "기말합"}).to_string(index=False))
print("상태별:", board["status"].value_counts().to_dict())
safe(act, rids[8], "재작성", "담당자")
print("반려 건 재작성 후 상태:", REPORTS[rids[8]]["status"], "/ 이력:", REPORTS[rids[8]]["hist"][-3:])

# 2) 예외 주입 10종
LOG.clear()
bad = LEDGER[(LEDGER["unit"] == UNITS[20]) & (LEDGER["month"] == "2026-08")].copy()
neg = bad.copy(); neg.iloc[0, neg.columns.get_loc("issue")] = -5
txt = bad.copy(); txt["issue"] = txt["issue"].astype(str) + "개"
nocol = bad.drop(columns=["ret"])
cases = [("부대명 오류", generate, ("가상99대대", "2026-08")), ("월 오류", generate, (UNITS[20], "2027-01")),
         ("빈 자료", generate, (UNITS[20], "2026-08", bad.iloc[0:0])), ("열 누락", generate, (UNITS[20], "2026-08", nocol)),
         ("숫자 아님", generate, (UNITS[20], "2026-08", txt)), ("음수", generate, (UNITS[20], "2026-08", neg)),
         ("중복 생성", generate, (UNITS[0], "2026-08")), ("없는 보고서", act, ("RPT-00-00", "승인", "보급관")),
         ("상태 위반(미제출 승인)", act, (rids[8], "승인", "보급관")), ("반려 사유 누락", act, (rids[11], "반려", "보급관", " "))]
crash = 0
LOG.clear()
for name, fn, args in cases:
    try:
        safe(fn, *args)
    except BaseException:
        crash += 1
errs = [l for l in LOG if l["level"] == "ERROR"]
print("\n[예외 주입 결과 - 사용자 안내 메시지]")
for (name, _, _), l in zip(cases, errs):
    print(f" {l['code']} {name:<14} {l['msg']:<28} -> 조치: {l['guide']}")
rate = len(errs) / len(cases)
print(f"예외 {len(cases)}종 중 로그 기록 {len(errs)}건(기록률 {rate:.0%}), 크래시 {crash}건")
print("로그 예시:", json.dumps(errs[0], ensure_ascii=False))
print(f"목표(크래시 0건, 에러 로그 기록률 100%): {'달성' if crash == 0 and rate == 1 else '미달'}")

fig, ax = plt.subplots(1, 2, figsize=(11, 4))
cnt = board["status"].map({"승인": "approved", "반려": "rejected", "제출": "pending"}).fillna("draft").value_counts()
ax[0].bar(cnt.index, cnt.values, color="tab:blue"); ax[0].set_title("Report status")
ax[1].barh(board["unit"].str[2:4], board["issue"], color="tab:gray"); ax[1].set_title("Issued quantity by unit (synthetic)")
plt.tight_layout(); plt.show()
💡 [참고] 미션 06 예상 실행 결과 (Output)
[결산 보고서 현황판] [교육용가상자료]
      보고서     부대 상태  불출합   기말합
RPT-01-08 가상01대대 승인 1147  8915
RPT-02-08 가상02대대 승인  916  9139
RPT-03-08 가상03대대 승인  778  7428
RPT-04-08 가상04대대 승인  882  8357
RPT-05-08 가상05대대 승인  920  7718
RPT-06-08 가상06대대 승인  887 10211
RPT-07-08 가상07대대 승인  967  8567
RPT-08-08 가상08대대 승인  877  7738
RPT-09-08 가상09대대 반려 1056  8987
RPT-10-08 가상10대대 반려 1015  9029
RPT-11-08 가상11대대 제출 1130  6845
RPT-12-08 가상12대대 제출 1266  9330
상태별: {'승인': 8, '반려': 2, '제출': 2}
반려 건 재작성 후 상태: 작성 / 이력: [('제출', '담당자3'), ('반려', '보급관: 기말 재고 산식 재확인 요망'), ('작성', '담당자')]

[예외 주입 결과 - 사용자 안내 메시지]
 E01 부대명 오류         존재하지 않는 부대 '가상99대대'          -> 조치: 부대 목록(가상01~24대대)에서 선택
 E02 월 오류           결산월 '2027-01' 자료 없음          -> 조치: 2026-07 또는 2026-08 중 선택
 E03 빈 자료           해당 부대 자료가 비어 있음              -> 조치: 제출 부대에 원장 재제출 요청
 E04 열 누락           필수 열 'ret' 누락                -> 조치: 표준 양식을 다시 내려받아 작성
 E05 숫자 아님          'issue' 열에 숫자가 아닌 값          -> 조치: 숫자만 입력(단위 문자 제거)
 E06 음수             'issue' 열에 음수                -> 조치: 수량 0 이상으로 수정
 E07 중복 생성          이미 생성된 보고서 RPT-01-08         -> 조치: 기존 보고서를 열어 수정·재제출
 E08 없는 보고서         보고서 RPT-00-00 없음             -> 조치: 보고서 번호를 목록에서 확인
 E10 상태 위반(미제출 승인)  현재 상태 '작성'에서 '승인' 불가         -> 조치: '제출' 상태에서만 가능
 E11 반려 사유 누락       반려 사유 없음                     -> 조치: 반려 사유를 한 줄 이상 입력
예외 10종 중 로그 기록 10건(기록률 100%), 크래시 0건
로그 예시: {"level": "ERROR", "code": "E01", "msg": "존재하지 않는 부대 '가상99대대'", "guide": "부대 목록(가상01~24대대)에서 선택"}
목표(크래시 0건, 에러 로그 기록률 100%): 달성

🎯 미션 07. 결산 보고서 최종 발간 전 군수과장/보급관 3단계 HITL 전자 결재선 연계 설계

소요시간: 약 3.0시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

결산 보고서 최종 발간 전 군수과장/보급관 3단계 HITL 결재선을 설계한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 결산 보고서 발간 전 담당자 기안, 보급관 검토, 군수과장 승인의 3단계 사람 결재선을 설계하고 이력의 무결성과 권한 통제를 검증한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 결재 단계별 역할과 순서를 설정으로 고정하고, 순서 위반, 본인 결재, 근거 미기재, 미등록 사용자를 차단 수행하고 검증하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: src/workflow/dat06_approval_system.py 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 군 보안 규정 및 행정 절차 부합성

미션 PASS 조건

  • 정량 기준: 이력 기록 무결성 100%, 승인 권한 우회 시도 차단율 100%

  • 진행 기준: 결재선 승인/반려 시뮬레이션 및 이력 조회 화면 구현

  • 검증 방법: 권한별 접근 제어(RBAC) 테스트

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""결산 보고서 발간 전 담당자 기안, 보급관 검토, 군수과장 최종 승인의 3단계 사람 결재선을 설계하고, 해시 체인 감사 이력의 무결성과 권한 우회 시도 10종의 차단을 검증한다. 자동 발간은 허용하지 않는다."""
import hashlib
import json
import numpy as np
import pandas as pd

# [교육용가상자료] 가상 직책·인물(가명). 실제 전자 결재 체계와 무관한 학습용 모의
USERS = {"kim": "담당자", "lee": "담당자", "park": "보급관", "choi": "군수과장", "han": "열람자"}
LINE = [("기안", "담당자"), ("검토", "보급관"), ("승인", "군수과장")]       # 순서 고정 3단계
AUDIT = []


def _h(prev, rec):
    # [TODO : 학생 실습 7] 결산 보고서 최종 발간 전 군수과장/보급관 3단계 HITL 전자 결재선 연계 설계 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
def log(doc, user, action, result, note=""):
    # [TODO : 학생 실습 7] 결산 보고서 최종 발간 전 군수과장/보급관 3단계 HITL 전자 결재선 연계 설계 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
def verify_chain():
    prev, ok = "GENESIS", 0
    for r in AUDIT:
        body = {k: v for k, v in r.items() if k != "hash"}
        ok += (r["hash"] == _h(prev, body)); prev = r["hash"]
    return ok, len(AUDIT)


DOCS = {}


def step(doc, user, action, comment=""):
    """결재 처리. 권한·순서·본인 결재 금지·근거 기재를 모두 통과해야 기록되고, 실패는 차단 기록을 남긴다."""
    d = DOCS.setdefault(doc, {"stage": 0, "signers": [], "published": False})
    role = USERS.get(user)
    if role is None:
        return log(doc, user, action, "차단", "미등록 사용자"), False
    if d["stage"] >= len(LINE):
        return log(doc, user, action, "차단", "이미 결재 완료"), False
    need_act, need_role = LINE[d["stage"]]
    if action != need_act:
        return log(doc, user, action, "차단", f"순서 위반: 현재 단계 '{need_act}'"), False
    if role != need_role:
        return log(doc, user, action, "차단", f"권한 없음: 역할 '{role}'은(는) '{action}' 불가({need_role} 필요)"), False
    if user in d["signers"]:
        return log(doc, user, action, "차단", "본인이 앞 단계에 서명한 문서는 다음 단계를 결재할 수 없음"), False
    if action != "기안" and len(comment.strip()) < 5:
        return log(doc, user, action, "차단", "검토 근거(5자 이상) 미기재"), False
    d["stage"] += 1; d["signers"].append(user)
    log(doc, user, action, "승인", comment or "기안 상신")
    return None, True


def publish(doc, user):
    d = DOCS.get(doc)
    if not d or d["stage"] < len(LINE):
        log(doc, user, "발간", "차단", "3단계 결재 미완료(자동 발간 금지)"); return False
    if USERS.get(user) != "담당자":
        log(doc, user, "발간", "차단", "발간은 담당자가 결재 완료 후 수동 실행"); return False
    d["published"] = True; log(doc, user, "발간", "승인", "결재 3단계 완료 후 수동 발간"); return True


# 1) 정상 결재 흐름 2건
for doc, drafter in (("RPT-2026-08-A", "kim"), ("RPT-2026-08-B", "lee")):
    step(doc, drafter, "기안")
    step(doc, "park", "검토", "재고 산식 및 전월 연속성 확인 완료")
    step(doc, "choi", "승인", "보급관 검토 의견 확인, 발간 승인")
    publish(doc, drafter)

# 2) 권한 우회 시도 10종 - 결재가 진행되는 순서에 맞춰 시도하고, 모두 차단되어야 한다
blocked, total = 0, 0
print("[권한 우회 시도 결과] [교육용가상자료]")


def attempt(name, fn):
    global blocked, total
    n = len(AUDIT); fn(); r = AUDIT[-1]
    ok = len(AUDIT) == n + 1 and r["result"] == "차단"
    blocked += ok; total += 1
    print(f" - {name}: {r['result']} ({r['note']})")


attempt("미등록 사용자 기안", lambda: step("RPT-ATT", "hacker", "기안"))
attempt("열람자가 기안", lambda: step("RPT-ATT", "han", "기안"))
attempt("기안 전에 군수과장이 승인(단계 건너뛰기)", lambda: step("RPT-ATT", "choi", "승인", "바로 승인합니다 확인"))
step("RPT-ATT", "kim", "기안")
attempt("기안자 본인이 검토까지 처리", lambda: step("RPT-ATT", "kim", "검토", "본인이 검토합니다 확인"))
attempt("다른 담당자가 검토 단계 처리", lambda: step("RPT-ATT", "lee", "검토", "다른 담당자 검토 시도 확인"))
attempt("검토 근거 없이 보급관 검토", lambda: step("RPT-ATT", "park", "검토", ""))
attempt("보급관 검토 전에 군수과장이 승인", lambda: step("RPT-ATT", "choi", "승인", "보급관 단계를 생략한 승인 확인"))
attempt("결재 미완료 상태에서 발간", lambda: publish("RPT-ATT", "kim"))
step("RPT-ATT", "park", "검토", "산식 검증 결과 이상 없음 확인")
step("RPT-ATT", "choi", "승인", "최종 승인합니다 확인")
attempt("완결된 문서 재승인 시도", lambda: step("RPT-ATT", "choi", "승인", "한 번 더 승인 시도 확인"))
attempt("보급관이 발간 시도", lambda: publish("RPT-ATT", "park"))
print(f"우회 시도 {total}건 중 차단 {blocked}건({blocked / total:.0%})")

# 3) 이력 무결성: 정상 체인 검증 후 위·변조 시험
ok, tot = verify_chain()
print(f"\n이력 {tot}건 해시 체인 검증: {ok}/{tot} 일치({ok / tot:.0%})")
k = next(i for i, r in enumerate(AUDIT) if r["result"] == "차단")
saved = AUDIT[k]["result"]
AUDIT[k]["result"] = "승인"          # 차단 기록을 승인으로 위조
ok2, _ = verify_chain()
AUDIT[k]["result"] = saved
print(f"위조 시험: {k + 1}번 기록 변조 시 검증 {ok2}/{tot} -> 변조 탐지 {'성공' if ok2 < tot else '실패'}, 원복 후 {verify_chain()[0]}/{tot}")
st = pd.DataFrame(AUDIT).groupby("doc")["result"].value_counts().unstack(fill_value=0)
print("\n[문서별 결재 이력 요약]")
print(st.to_string())
pubs = [k for k, v in DOCS.items() if v["published"]]
print("발간 완료 문서:", pubs, "/ 우회 시험 문서 RPT-ATT는 발간되지 않음 / 자동 발간 0건(발간은 담당자 수동 실행)")
print(f"목표(이력 무결성 100%, 우회 차단율 100%): {'달성' if ok == tot and blocked == total and ok2 < tot else '미달'}")
print("실제 결재와 보안 규정은 부대 지침과 지휘계통에 따른다. 본 코드는 학습용 모의이다.")
💡 [참고] 미션 07 예상 실행 결과 (Output)
[권한 우회 시도 결과] [교육용가상자료]
 - 미등록 사용자 기안: 차단 (미등록 사용자)
 - 열람자가 기안: 차단 (권한 없음: 역할 '열람자'은(는) '기안' 불가(담당자 필요))
 - 기안 전에 군수과장이 승인(단계 건너뛰기): 차단 (순서 위반: 현재 단계 '기안')
 - 기안자 본인이 검토까지 처리: 차단 (권한 없음: 역할 '담당자'은(는) '검토' 불가(보급관 필요))
 - 다른 담당자가 검토 단계 처리: 차단 (권한 없음: 역할 '담당자'은(는) '검토' 불가(보급관 필요))
 - 검토 근거 없이 보급관 검토: 차단 (검토 근거(5자 이상) 미기재)
 - 보급관 검토 전에 군수과장이 승인: 차단 (순서 위반: 현재 단계 '검토')
 - 결재 미완료 상태에서 발간: 차단 (3단계 결재 미완료(자동 발간 금지))
 - 완결된 문서 재승인 시도: 차단 (이미 결재 완료)
 - 보급관이 발간 시도: 차단 (발간은 담당자가 결재 완료 후 수동 실행)
우회 시도 10건 중 차단 10건(100%)

이력 21건 해시 체인 검증: 21/21 일치(100%)
위조 시험: 9번 기록 변조 시 검증 20/21 -> 변조 탐지 성공, 원복 후 21/21

[문서별 결재 이력 요약]
result         승인  차단
doc                  
RPT-2026-08-A   4   0
RPT-2026-08-B   4   0
RPT-ATT         3  10
발간 완료 문서: ['RPT-2026-08-A', 'RPT-2026-08-B'] / 우회 시험 문서 RPT-ATT는 발간되지 않음 / 자동 발간 0건(발간은 담당자 수동 실행)
목표(이력 무결성 100%, 우회 차단율 100%): 달성
실제 결재와 보안 규정은 부대 지침과 지휘계통에 따른다. 본 코드는 학습용 모의이다.

🎯 미션 08. 수십 개 예하 부대 월간 결산 보고서 원클릭 일괄 생성 배치(Batch) 스트레스 테스트

소요시간: 약 3.5시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

수십 개 예하 부대 월간 결산 보고서 일괄 생성 배치 스트레스 테스트를 수행한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 수십 개 예하 부대의 월간 결산 보고서를 원클릭으로 일괄 생성하는 배치를 만들고 장애·손상 자료를 주입해 스트레스 시험을 수행한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 가상 부대 40개 x 12개월, 480건 보고서를 한 번에 생성 수행하고 검증하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: src/pipeline/dat06_batch_runner.py 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 수작업 대비 업무 소요 시간 80% 이상 절감 효과

미션 PASS 조건

  • 정량 기준: 전체 배치 파이프라인 실행 시간 5분 이내, 자동화 성공률 99% 이상

  • 진행 기준: 스케줄러 연동 테스트 및 자동 실행 확인

  • 검증 방법: 실제 월간 업무 데이터 1년치 일괄 배치 시뮬레이션

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""40개 가상 예하 부대 x 12개월(480건) 월간 결산 보고서를 한 번에 생성하는 배치를 실행하며, 일시 장애와 손상 자료를 주입해 재시도·격리 처리를 확인하고 전체 실행 시간, 자동화 성공률, 수작업 대비 절감률을 측정한다."""
import time
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

UNITS = [f"가상{i:02d}대대" for i in range(1, 41)]                 # 배치용으로 40개 부대로 확장
t_all = time.perf_counter()
rows = make_ledger(12, seed=8)
led = pd.DataFrame(rows)
lab = {f"2026-{i + 7:02d}": f"{2026 + (i + 6) // 12}-{(i + 6) % 12 + 1:02d}" for i in range(12)}   # 2026-07 ~ 2027-06
led["month"] = led["month"].map(lab)
months = sorted(led["month"].unique())
print(f"[배치 입력] 부대 {len(UNITS)}개 x {len(months)}개월 = {len(UNITS) * len(months)}건, 원장 {len(led)}행 [교육용가상자료]")

# 장애 주입: 일시 장애(재시도로 복구) 9건, 손상 자료(격리) 3건
rng = np.random.default_rng(80)
keys = [(u, m) for u in UNITS for m in months]
pick = rng.choice(len(keys), 12, replace=False)
transient = {keys[i] for i in pick[:9]}
corrupt = {keys[i] for i in pick[9:]}
for (u, m) in corrupt:                             # 기말 수치를 훼손(산식 불일치)
    i = led[(led["unit"] == u) & (led["month"] == m)].index[0]
    led.loc[i, "close"] = led.loc[i, "close"] + 999
attempts_log = {}


def build_report(u, m, g):
    # [TODO : 학생 실습 8] 수십 개 예하 부대 월간 결산 보고서 원클릭 일괄 생성 배치(Batch) 스트레스 테스트 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
done, quarantine, retries = {}, [], 0
t0 = time.perf_counter()
for (u, m), g in led.groupby(["unit", "month"]):
    for attempt in range(1, 4):                    # 최대 3회 재시도
        try:
            done[(u, m)] = build_report(u, m, g)
            break
        except TimeoutError:
            retries += 1
        except ValueError as ex:                   # 자료 오류는 재시도해도 소용없으므로 즉시 격리하고 다음 건으로 진행
            quarantine.append((u, m, str(ex)))
            break
run_s = time.perf_counter() - t0
total = len(keys)
succ = len(done) / total
print(f"\n[배치 실행] 생성 {len(done)}건, 격리 {len(quarantine)}건, 재시도 {retries}회")
for q in quarantine:
    print("  격리(담당자 확인 필요):", q)
print(f"보고서 생성 순수 실행 시간 {run_s:.2f}초, 전체(자료 생성 포함) {time.perf_counter() - t_all:.2f}초 (목표 5분 이내: {'달성' if time.perf_counter() - t_all < 300 else '미달'})")
print(f"자동화 성공률 {succ:.1%} (일시 장애 {len(transient)}건은 재시도로 자동 복구, 손상 자료는 격리 후 보고)")

# 수작업 대비 절감(가정값): 부대 보고서 1건 수작업 25분(미션 1 AS-IS 610분/24개 부대 기준), 자동화 후 사람 검토 2분/건, 격리 건 수동 처리 15분
manual = total * 25
auto = total * 2 + len(quarantine) * 15 + run_s / 60
print(f"\n[업무 소요 비교(가정값)] 수작업 {manual}분({manual / 60:.0f}시간) vs 자동화 {auto:.0f}분({auto / 60:.1f}시간) -> 절감 {1 - auto / manual:.1%}")

# 스트레스: 부하 3배(1,440건) 반복 실행의 처리량
big = list(led.groupby(["unit", "month"]))
t0 = time.perf_counter(); n = 0
for _ in range(3):
    for (u, m), g in big:
        if (u, m) in corrupt:
            continue
        attempts_log[(u, m)] = 9                     # 일시 장애 주입 해제(처리량 측정 목적)
        build_report(u, m, g); n += 1
dt = time.perf_counter() - t0
print(f"부하 시험 {n}건 처리 {dt:.2f}초 (초당 {n / dt:.0f}건), 오류 0건")
print(f"\n목표(전체 5분 이내, 자동화 성공률 99% 이상, 수작업 대비 80% 이상 절감): "
      f"{'달성' if run_s < 300 and succ >= 0.99 and 1 - auto / manual >= 0.8 else '미달'}")
💡 [참고] 미션 08 예상 실행 결과 (Output)
[배치 입력] 부대 40개 x 12개월 = 480건, 원장 5280행 [교육용가상자료]

[배치 실행] 생성 477건, 격리 3건, 재시도 9회
  격리(담당자 확인 필요): ('가상10대대', '2027-05', '재고 산식 불일치 1건')
  격리(담당자 확인 필요): ('가상24대대', '2027-02', '재고 산식 불일치 1건')
  격리(담당자 확인 필요): ('가상36대대', '2026-12', '재고 산식 불일치 1건')
보고서 생성 순수 실행 시간 1.06초, 전체(자료 생성 포함) 1.13초 (목표 5분 이내: 달성)
자동화 성공률 99.4% (일시 장애 9건은 재시도로 자동 복구, 손상 자료는 격리 후 보고)

[업무 소요 비교(가정값)] 수작업 12000분(200시간) vs 자동화 1005분(16.8시간) -> 절감 91.6%
부하 시험 1431건 처리 3.63초 (초당 394건), 오류 0건

목표(전체 5분 이내, 자동화 성공률 99% 이상, 수작업 대비 80% 이상 절감): 달성

🎯 미션 09. AAR 기반 결산 수치 오집계 사례 분석 및 집계 룰셋/템플릿 보정

소요시간: 약 3.0시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

AAR 기반 결산 수치 오집계 사례를 분석하고 룰셋을 보정한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, AAR 사례의 결산 수치 오집계 5유형을 재현·분석하고 집계 룰셋과 템플릿을 보정한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 중복 제출, 단위 혼용(kL), 정정 문서 누락, 박스·발 혼용, 품목 별칭 누락의 5유형을 가상 제출 자료에 심어 룰셋 v1의 오집계를 재현 수행하고 검증하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: docs/dat06_uat_feedback_report.md 및 패치 내역 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 실무 업무 생산성 기여도 및 현장 적응성

미션 PASS 조건

  • 정량 기준: UAT 체크리스트 만족도 90점 이상, 개선 요구 5건 반영 완료

  • 진행 기준: UAT 결과 보고서 작성 및 최종 버그 픽스 완료

  • 검증 방법: 실제 업무 시나리오 기반 최종 검수 테스트

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""사후검토(AAR) 사례에서 결산 수치 오집계 5유형(중복 행, 단위 혼용, 정정 누락, 박스·발 혼용, 품목 별칭 누락)을 재현해 원인을 분석하고 집계 룰셋 v1을 v2로 보정한 뒤, 재집계 결과와 UAT 체크리스트 점수, 개선 요구 반영 현황, 패치 내역을 출력한다."""
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

# 1) 가상 제출 레코드 생성: 정답(순불출 = 불출 - 반납)과 부대가 실제로 낸 제출 형태(오집계 유발 요인 포함)
led = pd.DataFrame(make_ledger(1))
rng = np.random.default_rng(9)
truth = {(r.unit, r.item): r.issue - r.ret for r in led.itertuples()}
subs, doc = [], 0
def add(u, it, kind, qty, uom, name=None):
    # [TODO : 학생 실습 9] AAR 기반 결산 수치 오집계 사례 분석 및 집계 룰셋/템플릿 보정 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
for r in led.itertuples():
    add(r.unit, r.item, "issue", r.issue, UNIT_CODE[r.item]); add(r.unit, r.item, "return", r.ret, UNIT_CODE[r.item])
keys = list(truth)
pick = lambda n, f=lambda k: True: [keys[i] for i in rng.choice([j for j, k in enumerate(keys) if f(k)], n, replace=False)]
CASE = {}
for k in pick(6):                                   # 사례1: 같은 문서번호가 중복 제출
    CASE.setdefault("중복 행", []).append(k); subs.append(dict(next(s for s in subs if (s["unit"], s["item"]) == k and s["kind"] == "issue")))
for k in pick(4, lambda k: k[1] == "유류"):         # 사례2: 유류를 kL 단위로 제출(1kL = 1000L)
    CASE.setdefault("단위 혼용(kL)", []).append(k)
    for s in subs:
        if (s["unit"], s["item"]) == k:
            s["qty"], s["uom"] = s["qty"] / 1000, "kL"
for k in pick(5):                                   # 사례3: 불출 후 정정 문서(음수 조정)를 별도로 제출
    CASE.setdefault("정정 누락", []).append(k); d = int(rng.integers(5, 20))
    next(s for s in subs if (s["unit"], s["item"]) == k and s["kind"] == "issue")["qty"] += d
    add(k[0], k[1], "adjust", -d, UNIT_CODE[k[1]])
for k in pick(4, lambda k: k[1].startswith("훈련탄")):   # 사례4: 탄약을 박스(BX, 1BX=20발)로 제출
    CASE.setdefault("박스·발 혼용", []).append(k)
    for s in subs:
        if (s["unit"], s["item"]) == k:
            s["qty"], s["uom"] = s["qty"] / 20, "BX"
for k in pick(5, lambda k: k[1] == "피복류"):        # 사례5: 품목명을 줄여서 제출('피복')
    CASE.setdefault("품목 별칭 누락", []).append(k)
    for s in subs:
        if (s["unit"], s["item"]) == k:
            s["item"] = "피복"
S = pd.DataFrame(subs)

# 2) 집계 룰셋 v1(AAR 이전) / v2(보정 후)
def aggregate(S, ruleset):
    # [TODO : 학생 실습 9] AAR 기반 결산 수치 오집계 사례 분석 및 집계 룰셋/템플릿 보정 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
def compare(agg):
    tt = pd.Series(truth)
    tt.index.names = ["unit", "item"]
    diff = (agg.reindex(tt.index).fillna(0) - tt)
    return diff[diff.abs() > 0.5]

v1, v2 = compare(aggregate(S, "v1")), compare(aggregate(S, "v2"))
print(f"[AAR 오집계 분석] 제출 레코드 {len(S)}건, 정답 대조 대상 {len(truth)}개 (부대 x 품목) [교육용가상자료]")
print(f"룰셋 v1 오집계 {len(v1)}건(오차 합계 {v1.abs().sum():.0f}), 룰셋 v2 오집계 {len(v2)}건")
print("\n[사례별 원인 분석 - v1]")
rows = []
for name, ks in CASE.items():
    hit = [k for k in ks if k in v1.index]
    fix = [k for k in ks if k not in v2.index]
    err = float(sum(abs(v1[k]) for k in hit))
    rows.append({"사례": name, "발생 셀": len(ks), "v1 오류 셀": len(hit), "v1 오차": round(err), "v2 정상": f"{len(fix)}/{len(ks)}"})
print(pd.DataFrame(rows).to_string(index=False))
CAUSE = {"중복 행": "문서번호 중복 제거 규칙 없음", "단위 혼용(kL)": "단위 환산표 없음", "정정 누락": "정정(음수 조정) 문서를 집계에서 제외",
         "박스·발 혼용": "BX->RD 환산표 없음", "품목 별칭 누락": "별칭 사전 없고 미등록 품목을 조용히 버림"}
print("\n[원인 -> 룰셋 보정]")
for k, v in CAUSE.items():
    print(f" - {k}: {v} -> v2 패치: " + {"중복 행": "doc 기준 drop_duplicates", "단위 혼용(kL)": "kL x1000", "정정 누락": "adjust 포함 집계",
                                          "박스·발 혼용": "BX x20", "품목 별칭 누락": "별칭 사전 + 미등록 품목은 로그로 보류"}[k])

# 3) 회귀 시험 및 UAT 체크리스트(자동 점검 20항목)
agg2 = aggregate(S, "v2")
checks = [("정답 대조 오집계 0건", len(v2) == 0), ("중복 문서 제거", agg2.equals(aggregate(S.drop_duplicates("doc"), "v2"))),
          ("v2 재실행 결과 동일(멱등)", aggregate(S, "v2").equals(agg2)),
          ("kL 부대 4곳 정상", all(k not in v2.index for k in CASE["단위 혼용(kL)"])), ("BX 부대 4곳 정상", all(k not in v2.index for k in CASE["박스·발 혼용"])),
          ("정정 문서 5건 반영", all(k not in v2.index for k in CASE["정정 누락"])), ("별칭 5건 반영", all(k not in v2.index for k in CASE["품목 별칭 누락"])),
          ("정상 부대 v1 결과와 불변", len(set(truth) - set(v1.index) - set(v2.index)) == len(truth) - len(v1)),
          ("전체 불출 합계 정답과 일치", abs(agg2.sum() - sum(truth.values())) < 1), ("부대 24개 모두 집계", agg2.index.get_level_values(0).nunique() == 24),
          ("품목 11종 모두 집계", agg2.index.get_level_values(1).nunique() == 11), ("음수 순불출 0건", (agg2 >= 0).all()),
          ("미등록 품목 입력 시 오류 아닌 보류", aggregate(pd.concat([S, pd.DataFrame([{"doc": "X1", "unit": UNITS[0], "item": "미등록", "kind": "issue", "qty": 5, "uom": "EA"}])]), "v2").shape == agg2.shape),
          ("빈 입력에서도 오류 없음", aggregate(S.iloc[0:0], "v2").empty), ("처리 결과 부대x품목 단일 키", agg2.index.is_unique),
          ("v1 대비 오차 감소 100%", len(v2) == 0 and len(v1) > 0), ("단위 환산 후 소수 오차 없음", bool(((agg2 - agg2.round()).abs() < 1e-6).all())),
          ("사례 5유형 모두 재현·분석", len(CASE) == 5), ("원인-패치 대응표 작성", len(CAUSE) == 5), ("개선 요구 5건 반영", len(CASE) == 5 and len(v2) == 0)]
passed = sum(ok for _, ok in checks)
score = 100 * passed / len(checks)
print("\n[UAT 체크리스트]", f"{passed}/{len(checks)}항목 통과, 점수 {score:.0f}점")
for n, ok in checks:
    if not ok:
        print("  미통과:", n)
print("\n[개선 요구 반영 현황]")
for i, (k, ks) in enumerate(CASE.items(), 1):
    print(f" 요구 {i}. {k} 대응: {'반영 완료' if all(x not in v2.index for x in ks) else '미반영'}")
print(f"\n목표(UAT 90점 이상, 개선 요구 5건 반영): {'달성' if score >= 90 and len(v2) == 0 else '미달'}")
print("패치 내역: 룰셋 v1 -> v2 (중복 제거, 단위 환산표, 정정 반영, 별칭 사전, 미등록 품목 보류). 재집계 전 담당자가 보류 건을 확인한다.")
💡 [참고] 미션 09 예상 실행 결과 (Output)
[AAR 오집계 분석] 제출 레코드 539건, 정답 대조 대상 264개 (부대 x 품목) [교육용가상자료]
룰셋 v1 오집계 24건(오차 합계 1787), 룰셋 v2 오집계 0건

[사례별 원인 분석 - v1]
       사례  발생 셀  v1 오류 셀  v1 오차 v2 정상
     중복 행     6        6    546   6/6
단위 혼용(kL)     4        4    241   4/4
    정정 누락     5        5     71   5/5
  박스·발 혼용     4        4    367   4/4
 품목 별칭 누락     5        5    563   5/5

[원인 -> 룰셋 보정]
 - 중복 행: 문서번호 중복 제거 규칙 없음 -> v2 패치: doc 기준 drop_duplicates
 - 단위 혼용(kL): 단위 환산표 없음 -> v2 패치: kL x1000
 - 정정 누락: 정정(음수 조정) 문서를 집계에서 제외 -> v2 패치: adjust 포함 집계
 - 박스·발 혼용: BX->RD 환산표 없음 -> v2 패치: BX x20
 - 품목 별칭 누락: 별칭 사전 없고 미등록 품목을 조용히 버림 -> v2 패치: 별칭 사전 + 미등록 품목은 로그로 보류

[UAT 체크리스트] 20/20항목 통과, 점수 100점

[개선 요구 반영 현황]
 요구 1. 중복 행 대응: 반영 완료
 요구 2. 단위 혼용(kL) 대응: 반영 완료
 요구 3. 정정 누락 대응: 반영 완료
 요구 4. 박스·발 혼용 대응: 반영 완료
 요구 5. 품목 별칭 누락 대응: 반영 완료

목표(UAT 90점 이상, 개선 요구 5건 반영): 달성
패치 내역: 룰셋 v1 -> v2 (중복 제거, 단위 환산표, 정정 반영, 별칭 사전, 미등록 품목 보류). 재집계 전 담당자가 보류 건을 확인한다.

🎯 미션 10. 군수 월간 결산 및 보급 현황 보고서 원클릭 자동 생성 도구 최종 완성

소요시간: 약 3.0시간

📋 미션 배경 및 수행 지침

미션 배경 및 당위성

군수 월간 결산 및 보급 현황 보고서 원클릭 자동 생성 도구를 완성한다. 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, 수집부터 검증, 집계, 이상 점검, 보고서 생성, 결재 대기 분리까지 한 번에 실행되는 월간 결산 원클릭 도구를 완성한다. 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

이 문제를 해결하기 위해 본 미션에서는 환경 점검 후 CSV·JSON 혼합 자료를 수집하고 검증 룰과 집계, 이상 점검, 신뢰도 점수를 거쳐 24개 부대 보고서 작성하여 원인을 분석하는 실습을 체계적으로 수행합니다.

학습자는 실습가이드에 제시된 총 4개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

이번 결과물: dist/dat06_package/, README.md, docs/dat06_final_manual.md 형태의 결과물을 완성한다.

🔍 평가 및 검증 기준 (PASS 조건)

미션 판정

평가 구분: 필수

핵심 평가영역: 상용 소프트웨어 수준의 완성도 및 조직 확산 가치

미션 PASS 조건

  • 정량 기준: 설치부터 실행까지 원클릭 설치 스크립트 완비, 매뉴얼 작성률 100%

  • 진행 기준: 최종 배포본 생성 및 검수 완료

  • 검증 방법: Clean 환경에서의 설치 및 초기화 테스트

채점자 판정 메모

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

In [ ]:
"""환경 점검부터 4형식 자료 수집, 검증 룰 적용, 월간 결산 집계, 이상치 점검, 신뢰도 점수, 24개 부대 보고서 생성, 3단계 결재 대기 분리까지 한 번에 실행하는 원클릭 결산 도구를 완성하고 설치 스크립트 개요·매뉴얼 목차·자체 점검·브리핑 요약을 출력한다."""
import csv
import importlib.util
import io
import json
import sys
import time
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# [교육용가상자료] 가상 월간 결산 원장 생성기 (실제 부대명·품목·수량·재무 정보 없음)
# 가상 대대 24개 x (물자 불출 / 수리부속 소모 / 탄약 정산) 품목 원장과 장비 가동 현황을 만든다.
# 재고 산식: 기말 = 기초 + 수령 + 반납 - 불출,  다음 달 기초 = 이번 달 기말.
# ---------------------------------------------------------------------------
UNITS = [f"가상{i:02d}대대" for i in range(1, 25)]
ITEMS = {"물자불출": ["급식류", "피복류", "유류", "정비소모품"],
         "수리부속": ["필터류", "베어링류", "전장품", "타이어류"],
         "탄약정산": ["훈련탄A", "훈련탄B", "모의탄"]}
UNIT_CODE = {"급식류": "BX", "피복류": "EA", "유류": "L", "정비소모품": "EA", "필터류": "EA", "베어링류": "EA",
             "전장품": "EA", "타이어류": "EA", "훈련탄A": "RD", "훈련탄B": "RD", "모의탄": "RD"}
LEDGER_COLS = ["month", "unit", "cat", "item", "open", "recv", "issue", "ret", "close"]


def make_ledger(n_months=2, seed=60):
    """n_months개월 원장(list of dict). 둘째 달 기초는 첫째 달 기말과 같다."""
    rng = np.random.default_rng(seed)
    rows, prev = [], {}
    for m in range(n_months):
        for u in UNITS:
            for cat, items in ITEMS.items():
                for it in items:
                    base = int(rng.integers(80, 400))
                    op = prev.get((u, it), base * 3)
                    recv = int(rng.integers(0, base))
                    issue = int(rng.integers(base // 4, base // 2 + 1))
                    ret = int(issue * rng.uniform(0, 0.15))
                    cl = op + recv + ret - issue
                    rows.append({"month": f"2026-{m + 7:02d}", "unit": u, "cat": cat, "item": it,
                                 "open": op, "recv": recv, "issue": issue, "ret": ret, "close": cl})
                    prev[(u, it)] = cl
    return rows


def make_equip(n_months=2, seed=61):
    rng = np.random.default_rng(seed)
    out = []
    for m in range(n_months):
        for u in UNITS:
            hold = int(rng.integers(20, 60))
            out.append({"month": f"2026-{m + 7:02d}", "unit": u, "hold": hold, "operable": int(hold * rng.uniform(0.78, 0.97))})
    return out

LOG = []
def stage(name):
    # [TODO : 학생 실습 10] 군수 월간 결산 및 보급 현황 보고서 원클릭 자동 생성 도구 최종 완성 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
INSTALL = ["1. 가상 환경 생성 및 필수 모듈 설치(numpy, pandas, matplotlib)", "2. 환경 점검: 모듈·버전 확인", "3. 샘플 자료로 시험 실행", "4. 실행: run_monthly_close()", "5. 결과 확인: 보고서 24건, 결재 대기 목록"]
mods = ["numpy", "pandas", "matplotlib", "csv", "json"]
env_ok = sum(importlib.util.find_spec(m) is not None for m in mods)
print(f"[0] 환경 점검: Python {sys.version_info.major}.{sys.version_info.minor}, 필수 모듈 {env_ok}/{len(mods)} 확인 [교육용가상자료]")

# ---------------- 1) 수집(CSV + JSON 두 형식 혼합) ----------------
@stage("수집·파싱")
def collect():
    # [TODO : 학생 실습 10] 군수 월간 결산 및 보급 현황 보고서 원클릭 자동 생성 도구 최종 완성 관련 핵심 로직을 구현하세요.
    # 힌트: 상단 주석 및 데이터 스키마/지침을 참고하여 작성합니다.
    # <<< 여기에 코드를 작성하세요 >>>
    pass  # 작성 후 pass를 제거하세요.
RULES = [("V1 산식", lambda d: d["open"] + d["recv"] + d["ret"] - d["issue"] == d["close"]), ("V2 음수", lambda d: (d[["open", "recv", "issue", "ret", "close"]] >= 0).all(axis=1)),
         ("V3 가용량", lambda d: d["issue"] <= d["open"] + d["recv"]), ("V4 반납", lambda d: d["ret"] <= d["issue"]), ("V5 부대", lambda d: d["unit"].isin(UNITS)),
         ("V6 품목", lambda d: d["item"].isin(UNIT_CODE))]
@stage("검증")
def check(df):
    bad = pd.Series(False, index=df.index)
    for _, f in RULES:
        bad |= ~f(df)
    return int(bad.sum())

@stage("집계")
def aggregate(df):
    return df.groupby(["unit", "cat"])[["issue", "ret", "close"]].sum().unstack("cat")

@stage("이상 점검")
def anomalies(df):
    prev = pd.DataFrame([r for r in make_ledger(2) if r["month"] == "2026-07"]).set_index(["unit", "item"])["issue"]
    ratio = df.set_index(["unit", "item"])["issue"] / prev.clip(lower=1)
    return ratio[ratio >= 4.0]

@stage("신뢰도")
def reliability(df, nbad):
    return round(100 - 100 * nbad / len(df), 1)

@stage("보고서 생성")
def build(df, agg):
    out = {}
    for u in UNITS:
        g = df[df["unit"] == u]
        out[u] = {"unit": u, "issue": int(g["issue"].sum()), "close": int(g["close"].sum()), "status": "결재 대기(기안)", "published": False}
    return out

df = collect()
nbad = check(df)
agg = aggregate(df)
anom = anomalies(df)
rel = reliability(df, nbad)
reports = build(df, agg)

# ---------------- 3) 결과 ----------------
print(f"[1] 수집 {len(df)}행(CSV+JSON 혼합), 검증 위반 {nbad}건, 이상 급증 후보 {len(anom)}건, 자료 신뢰도 {rel}점")
tbl = pd.DataFrame(reports).T[["issue", "close", "status"]].head(5)
print("[2] 부대별 결산 보고서(상위 5개 부대) - 모든 보고서는 결재 대기 상태")
print(tbl.to_string())
tot = agg["issue"].sum()
print("[3] 분류별 불출·소모 합계:", tot.astype(int).to_dict())
print("[4] 단계별 실행 로그:")
for n, st, t in LOG:
    print(f"   - {n:<8} {st:<5} {t * 1000:.1f}ms")
run_ms = sum(t for _, _, t in LOG) * 1000

# ---------------- 4) 자체 점검 ----------------
MANUAL = ["1. 개요·설치", "2. 입력 자료 양식", "3. 실행 방법(원클릭)", "4. 검증 룰 목록", "5. 이상 항목 처리", "6. 결재선·권한", "7. 오류 메시지 조치", "8. 배치 운영", "9. 개선 이력·문의"]
done_sec = len(MANUAL)
checks = [("환경 점검 5/5", env_ok == len(mods)), ("모든 단계 OK", all(s == "OK" for _, s, _ in LOG)), ("24개 부대 보고서 생성", len(reports) == 24),
          ("자동 발간 0건", not any(r["published"] for r in reports.values())), ("원장 산식 위반 0건", nbad == 0),
          ("실행 시간 1분 이내", run_ms < 60000), ("매뉴얼 9장 작성(작성률 100%)", done_sec == len(MANUAL)),
          ("설치 절차 5단계 완비", len(INSTALL) == 5)]
print("\n[5] 자체 점검")
for n, ok in checks:
    print(f"   {'PASS' if ok else 'FAIL'}  {n}")
npass = sum(ok for _, ok in checks)
print("설치 스크립트 개요:", " / ".join(INSTALL))
print("매뉴얼 목차:", " | ".join(MANUAL))
asis, tobe = 610, 58
print(f"\n[브리핑 요약] 24개 부대 결산 보고서를 {run_ms:.0f}ms에 자동 생성(수작업 가정 {asis}분 -> 검토 포함 {tobe}분, {1 - tobe / asis:.0%} 절감 가정값).")
print(f"자료 신뢰도 {rel}점, 이상 급증 후보 {len(anom)}건은 담당자 확인 대상, 보고서 24건 전부 보급관 -> 군수과장 결재 대기(자동 발간 없음).")
print(f"자체 점검 {npass}/{len(checks)} PASS. 목표(원클릭 설치·실행 완비, 매뉴얼 100%): {'달성' if npass == len(checks) else '미달'}")
💡 [참고] 미션 10 예상 실행 결과 (Output)
[0] 환경 점검: Python 3.11, 필수 모듈 5/5 확인 [교육용가상자료]
[1] 수집 264행(CSV+JSON 혼합), 검증 위반 0건, 이상 급증 후보 9건, 자료 신뢰도 100.0점
[2] 부대별 결산 보고서(상위 5개 부대) - 모든 보고서는 결재 대기 상태
       issue close     status
가상01대대  1147  8915  결재 대기(기안)
가상02대대   916  9139  결재 대기(기안)
가상03대대   778  7428  결재 대기(기안)
가상04대대   882  8357  결재 대기(기안)
가상05대대   920  7718  결재 대기(기안)
[3] 분류별 불출·소모 합계: {'물자불출': 8270, '수리부속': 8184, '탄약정산': 6743}
[4] 단계별 실행 로그:
   - 수집·파싱    OK    19.0ms
   - 검증       OK    8.2ms
   - 집계       OK    13.2ms
   - 이상 점검    OK    23.8ms
   - 신뢰도      OK    0.0ms
   - 보고서 생성   OK    33.2ms

[5] 자체 점검
   PASS  환경 점검 5/5
   PASS  모든 단계 OK
   PASS  24개 부대 보고서 생성
   PASS  자동 발간 0건
   PASS  원장 산식 위반 0건
   PASS  실행 시간 1분 이내
   PASS  매뉴얼 9장 작성(작성률 100%)
   PASS  설치 절차 5단계 완비
설치 스크립트 개요: 1. 가상 환경 생성 및 필수 모듈 설치(numpy, pandas, matplotlib) / 2. 환경 점검: 모듈·버전 확인 / 3. 샘플 자료로 시험 실행 / 4. 실행: run_monthly_close() / 5. 결과 확인: 보고서 24건, 결재 대기 목록
매뉴얼 목차: 1. 개요·설치 | 2. 입력 자료 양식 | 3. 실행 방법(원클릭) | 4. 검증 룰 목록 | 5. 이상 항목 처리 | 6. 결재선·권한 | 7. 오류 메시지 조치 | 8. 배치 운영 | 9. 개선 이력·문의

[브리핑 요약] 24개 부대 결산 보고서를 97ms에 자동 생성(수작업 가정 610분 -> 검토 포함 58분, 90% 절감 가정값).
자료 신뢰도 100.0점, 이상 급증 후보 9건은 담당자 확인 대상, 보고서 24건 전부 보급관 -> 군수과장 결재 대기(자동 발간 없음).
자체 점검 8/8 PASS. 목표(원클릭 설치·실행 완비, 매뉴얼 100%): 달성