"""제품품질 분석 보고서(PPTX) 생성 — 806 교재 실습 결과물.

    python build_report.py --excel Production_Data.xlsx

Production_Data.xlsx 의 생산 실적을 품질 관점으로 집계해 10장짜리 보고서를
만든다. 슬라이드 제목은 주제가 아니라 '결론 문장'이며, 수치는 전부 엑셀에서
직접 계산한 값이다(하드코딩 없음).
"""
from __future__ import annotations

import argparse
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Emu, Inches, Pt

ROOT = Path(__file__).resolve().parent
CHARTS = ROOT / "charts"

# ── 서식 ──────────────────────────────────────────────────────────────
NAVY = RGBColor(0x1B, 0x4F, 0x8A)
NAVY_D = RGBColor(0x12, 0x3A, 0x66)
BLUE = RGBColor(0x3D, 0x77, 0xB8)
RED = RGBColor(0xC0, 0x39, 0x2B)
GREY = RGBColor(0x66, 0x66, 0x66)
LIGHT = RGBColor(0xF2, 0xF4, 0xF7)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
DARK = RGBColor(0x22, 0x2A, 0x35)

FONT = "맑은 고딕"
C_NAVY, C_BLUE, C_RED, C_GREY = "#1B4F8A", "#3D77B8", "#C0392B", "#8A93A0"

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False


# ── 유틸 ──────────────────────────────────────────────────────────────
def textbox(slide, x, y, w, h, text, size=14, bold=False, color=DARK,
            align=PP_ALIGN.LEFT, spacing=1.15):
    tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
    tf = tb.text_frame
    tf.word_wrap = True
    for i, line in enumerate(str(text).split("\n")):
        p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
        p.text = line
        p.alignment = align
        p.line_spacing = spacing
        f = p.runs[0].font if p.runs else p.font
        f.size, f.bold, f.color.rgb, f.name = Pt(size), bold, color, FONT
    return tb


def rect(slide, x, y, w, h, fill=LIGHT, line=None):
    from pptx.enum.shapes import MSO_SHAPE
    s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(x), Inches(y),
                               Inches(w), Inches(h))
    s.fill.solid()
    s.fill.fore_color.rgb = fill
    if line is None:
        s.line.fill.background()
    else:
        s.line.color.rgb = line
        s.line.width = Pt(0.75)
    s.shadow.inherit = False
    return s


def page(prs, title, lead):
    """제목(결론 문장) + 한 줄 요약이 붙은 본문 슬라이드."""
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    rect(slide, 0, 0, 13.333, 0.12, NAVY)
    textbox(slide, 0.55, 0.35, 12.2, 0.6, title, size=25, bold=True, color=NAVY_D)
    textbox(slide, 0.55, 1.02, 12.2, 0.4, lead, size=13, color=GREY)
    return slide


def footnote(slide, text):
    textbox(slide, 0.55, 6.72, 12.2, 0.45, text, size=12, color=NAVY)


def savefig(fig, name):
    CHARTS.mkdir(exist_ok=True)
    p = CHARTS / name
    fig.savefig(p, dpi=160, bbox_inches="tight", facecolor="white")
    plt.close(fig)
    return str(p)


def style_axes(ax, ylabel=None):
    for s in ("top", "right"):
        ax.spines[s].set_visible(False)
    for s in ("left", "bottom"):
        ax.spines[s].set_color("#CFD5DD")
    ax.tick_params(colors="#5A6472", labelsize=10)
    ax.grid(axis="y", color="#E8EBEF", linewidth=0.8)
    ax.set_axisbelow(True)
    if ylabel:
        # 한글 세로 라벨은 자간이 붙어 읽기 어렵다 — 축 위쪽에 가로로 둔다.
        ax.set_title(ylabel, color="#5A6472", fontsize=10, loc="left", pad=8)


# ── 집계 ──────────────────────────────────────────────────────────────
def analyse(df: pd.DataFrame) -> dict:
    df = df.copy()
    df["isbad"] = (df.Quality == "Bad").astype(int)
    n = len(df)
    bad = int(df.isbad.sum())

    team = df.groupby("Team").isbad.agg(["size", "sum", "mean"])
    team["pct"] = team["mean"] * 100
    team = team.sort_values("pct")

    direc = df.groupby("Direction").isbad.agg(["size", "mean"])
    direc["pct"] = direc["mean"] * 100

    cross = df.pivot_table(index="Team", columns="Direction",
                           values="isbad", aggfunc="mean") * 100

    defects = df.groupby("SurfaceDefects").isbad.agg(["size", "mean"])
    defects["pct"] = defects["mean"] * 100

    daily = df.groupby(df.ProductionDate.dt.day).isbad.agg(["size", "mean"])
    daily["pct"] = daily["mean"] * 100

    proc = ["Speed_P", "Temp_P", "Torque_P", "Speed_Q", "Angle_Q",
            "Weight", "Gap", "Error"]
    corr = pd.Series({c: np.corrcoef(df[c], df.isbad)[0, 1] for c in proc})
    corr = corr.sort_values()

    worst_team = team.index[-1]
    best_team = team.index[0]
    worst_dir = direc.pct.idxmax()
    cross_max = cross.stack().idxmax()
    # 표면결함 임계점 = 불량률이 처음 40%를 넘는 결함 수
    thr = next((int(k) for k, v in defects.pct.items() if v >= 40), None)

    return dict(
        df=df, n=n, bad=bad, yield_pct=100 * (n - bad) / n, bad_pct=100 * bad / n,
        days=df.ProductionDate.dt.date.nunique(),
        period=(df.ProductionDate.min(), df.ProductionDate.max()),
        defect_avg=df.SurfaceDefects.mean(), err_avg=df.Error.mean(),
        team=team, direc=direc, cross=cross, defects=defects, daily=daily,
        corr=corr, worst_team=worst_team, best_team=best_team,
        worst_dir=worst_dir, cross_max=cross_max, threshold=thr,
        team_ratio=team.pct.iloc[-1] / team.pct.iloc[0],
        dir_ratio=direc.pct.max() / direc.pct.min(),
    )


# ── 차트 ──────────────────────────────────────────────────────────────
def chart_daily(a):
    fig, ax = plt.subplots(figsize=(11.2, 3.5))
    d = a["daily"]
    ax.plot(d.index, d.pct, color=C_NAVY, linewidth=2.2, marker="o",
            markersize=4.5, zorder=3)
    ax.axhline(a["bad_pct"], color=C_RED, linestyle="--", linewidth=1.4, zorder=2)
    ax.text(d.index[-1], a["bad_pct"] + 1.2, f"월 평균 {a['bad_pct']:.1f}%",
            color=C_RED, fontsize=10, ha="right")
    hi = d.pct.idxmax()
    ax.annotate(f"최고 {d.pct.max():.1f}%", xy=(hi, d.pct.max()),
                xytext=(hi, d.pct.max() + 6), color=C_RED, fontsize=10,
                ha="center", arrowprops=dict(arrowstyle="->", color=C_RED, lw=1.2))
    style_axes(ax, "불량률 (%)")
    ax.set_xlabel("생산일 (일)", color="#5A6472", fontsize=10)
    ax.set_ylim(0, max(40, d.pct.max() + 10))
    return savefig(fig, "01_daily.png")


def chart_team(a):
    fig, ax = plt.subplots(figsize=(5.4, 3.4))
    t = a["team"]
    colors = [C_RED if i == len(t) - 1 else C_BLUE for i in range(len(t))]
    b = ax.bar([f"팀 {i}" for i in t.index], t.pct, color=colors, width=0.55)
    for r, v, n in zip(b, t.pct, t["sum"]):
        ax.text(r.get_x() + r.get_width() / 2, v + 1, f"{v:.1f}%\n({int(n)}건)",
                ha="center", fontsize=10, color="#3A424E")
    style_axes(ax, "불량률 (%)")
    ax.set_ylim(0, t.pct.max() * 1.35)
    return savefig(fig, "02_team.png")


def chart_dir(a):
    fig, ax = plt.subplots(figsize=(5.4, 3.4))
    d = a["direc"].sort_values("pct")
    colors = [C_BLUE, C_RED]
    b = ax.bar(list(d.index), d.pct, color=colors, width=0.5)
    for r, v, n in zip(b, d.pct, d["size"]):
        ax.text(r.get_x() + r.get_width() / 2, v + 1, f"{v:.1f}%\n({int(n)}건)",
                ha="center", fontsize=10, color="#3A424E")
    style_axes(ax, "불량률 (%)")
    ax.set_ylim(0, d.pct.max() * 1.35)
    return savefig(fig, "03_direction.png")


def chart_cross(a):
    c = a["cross"]
    fig, ax = plt.subplots(figsize=(5.6, 3.4))
    im = ax.imshow(c.values, cmap="Blues", vmin=0, vmax=max(60, c.values.max()))
    ax.set_xticks(range(len(c.columns)), c.columns)
    ax.set_yticks(range(len(c.index)), [f"팀 {i}" for i in c.index])
    for i in range(len(c.index)):
        for j in range(len(c.columns)):
            v = c.values[i, j]
            ax.text(j, i, f"{v:.1f}%", ha="center", va="center", fontsize=13,
                    color="white" if v > 30 else "#22303F",
                    fontweight="bold" if v > 45 else "normal")
    ax.tick_params(colors="#5A6472", labelsize=11)
    for s in ax.spines.values():
        s.set_visible(False)
    fig.colorbar(im, ax=ax, shrink=0.8, label="불량률 (%)")
    return savefig(fig, "04_cross.png")


def chart_defects(a):
    d = a["defects"]
    fig, ax = plt.subplots(figsize=(5.6, 3.4))
    colors = [C_RED if v >= 40 else C_BLUE for v in d.pct]
    b = ax.bar(d.index.astype(str), d.pct, color=colors, width=0.6)
    for r, v, n in zip(b, d.pct, d["size"]):
        ax.text(r.get_x() + r.get_width() / 2, v + 1.5, f"{v:.0f}%",
                ha="center", fontsize=10, color="#3A424E")
    if a["threshold"] is not None:
        ax.axvline(a["threshold"] - 0.5, color=C_RED, linestyle="--", linewidth=1.4)
        ax.text(a["threshold"] - 0.45, 92, f"임계점 {a['threshold']}개",
                color=C_RED, fontsize=10)
    style_axes(ax, "불량률 (%)")
    ax.set_xlabel("표면결함 수 (개)", color="#5A6472", fontsize=10)
    ax.set_ylim(0, 112)
    return savefig(fig, "05_defects.png")


def chart_corr(a):
    c = a["corr"]
    fig, ax = plt.subplots(figsize=(6.0, 3.6))
    colors = [C_RED if abs(v) >= 0.3 else C_GREY for v in c.values]
    ax.barh(list(c.index), c.values, color=colors, height=0.6)
    for i, v in enumerate(c.values):
        ax.text(v + (0.02 if v >= 0 else -0.02), i, f"{v:+.2f}", va="center",
                ha="left" if v >= 0 else "right", fontsize=10, color="#3A424E")
    ax.axvline(0, color="#B9C0C9", linewidth=1)
    ax.axvspan(-0.3, 0.3, color="#F0F2F5", zorder=0)
    style_axes(ax)
    ax.set_xlabel("불량 여부와의 상관계수", color="#5A6472", fontsize=10)
    ax.set_xlim(-0.35, 0.75)
    ax.grid(axis="y", visible=False)
    return savefig(fig, "06_corr.png")


# ── 슬라이드 ──────────────────────────────────────────────────────────
def s_cover(prs, a, title_month):
    s = prs.slides.add_slide(prs.slide_layouts[6])
    rect(s, 0, 0, 13.333, 7.5, NAVY_D)
    textbox(s, 1.1, 2.05, 11, 0.5, "QUALITY ANALYSIS REPORT", size=13,
            bold=True, color=BLUE)
    textbox(s, 1.1, 2.45, 11, 0.9, "제품품질 분석 보고서", size=40, bold=True,
            color=WHITE)
    rect(s, 1.15, 3.45, 4.2, 0.04, BLUE)
    textbox(s, 1.1, 3.62, 11, 0.5, title_month, size=19, color=RGBColor(0xB9, 0xCD, 0xE6))
    p0, p1 = a["period"]
    textbox(s, 1.1, 5.55, 11, 0.9,
            f"대상 기간   {p0:%Y-%m-%d} ~ {p1:%Y-%m-%d}  ·  가동 {a['days']}일\n"
            f"분석 대상   생산 실적 {a['n']:,}건",
            size=13, color=RGBColor(0x9F, 0xB3, 0xCC), spacing=1.5)


def s_summary(prs, a):
    s = page(prs, f"불량률 {a['bad_pct']:.1f}%, 원인은 팀·작업방향에 집중",
             "핵심 발견 세 가지와 요청 사항")
    items = [
        (f"팀 {a['worst_team']} 불량률 {a['team'].pct.iloc[-1]:.1f}%",
         f"팀 {a['best_team']}({a['team'].pct.iloc[0]:.1f}%)의 {a['team_ratio']:.1f}배. "
         f"동일 설비·동일 기간인데도 격차가 가장 크다."),
        (f"{a['worst_dir']} 방향 작업이 {a['dir_ratio']:.1f}배 불량",
         f"{a['worst_dir']} {a['direc'].pct.max():.1f}% ↔ 반대 방향 {a['direc'].pct.min():.1f}%. "
         f"팀 {a['cross_max'][0]}×{a['cross_max'][1]} 조합은 {a['cross'].stack().max():.1f}%까지 오른다."),
        ("설비 파라미터는 불량과 무관",
         "속도·온도·토크의 상관계수가 모두 ±0.06 이내. "
         f"실측 지표인 오차({a['corr']['Error']:+.2f})·간극({a['corr']['Gap']:+.2f})만 예측력이 있다."),
    ]
    y = 1.75
    for i, (head, body) in enumerate(items, 1):
        rect(s, 0.55, y, 12.2, 1.15, LIGHT)
        rect(s, 0.55, y, 0.06, 1.15, NAVY if i < 3 else BLUE)
        textbox(s, 0.85, y + 0.13, 0.4, 0.4, str(i), size=15, bold=True, color=BLUE)
        textbox(s, 1.35, y + 0.11, 11.2, 0.4, head, size=16, bold=True, color=NAVY_D)
        textbox(s, 1.35, y + 0.58, 11.2, 0.5, body, size=12.5, color=GREY)
        y += 1.35
    rect(s, 0.55, y + 0.05, 12.2, 0.95, RGBColor(0xFD, 0xF3, 0xF2))
    textbox(s, 0.85, y + 0.18, 11.8, 0.35, "요청 사항", size=13, bold=True, color=RED)
    textbox(s, 0.85, y + 0.55, 11.8, 0.35,
            f"팀 {a['worst_team']} 작업 방식 점검과 {a['worst_dir']} 방향 공정 재검토 — "
            f"본 보고서 10쪽 개선 과제 승인 요청", size=12.5, color=DARK)


def s_kpi(prs, a):
    s = page(prs, f"{a['days']}일간 {a['n']:,}건 생산, {a['bad']}건 불량",
             "이번 달 생산·품질 지표 요약")
    kpis = [
        ("총 생산량", f"{a['n']:,}", "건", False),
        ("양품률", f"{a['yield_pct']:.1f}", "%", False),
        ("불량 건수", f"{a['bad']}", "건", True),
        ("평균 표면결함", f"{a['defect_avg']:.2f}", "개", False),
        ("평균 오차", f"{a['err_avg']:.3f}", "", False),
        ("가동일수", f"{a['days']}", "일", False),
    ]
    x, y, w, h = 0.55, 2.0, 3.95, 1.75
    for i, (label, val, unit, warn) in enumerate(kpis):
        cx = x + (i % 3) * (w + 0.2)
        cy = y + (i // 3) * (h + 0.35)
        rect(s, cx, cy, w, h, WHITE, line=RGBColor(0xDD, 0xE2, 0xE8))
        rect(s, cx, cy, w, 0.06, RED if warn else NAVY)
        textbox(s, cx + 0.3, cy + 0.28, w - 0.6, 0.35, label, size=12.5, color=GREY)
        textbox(s, cx + 0.3, cy + 0.68, w - 0.6, 0.8, f"{val} {unit}".strip(),
                size=30, bold=True, color=RED if warn else NAVY_D)
    footnote(s, f"양품률 {a['yield_pct']:.1f}%는 목표 95%에 크게 못 미친다. "
                f"불량 {a['bad']}건은 하루 평균 {a['bad']/a['days']:.1f}건 수준.")


def s_daily(prs, a, img):
    d = a["daily"]
    s = page(prs, f"일별 불량률 {d.pct.min():.1f}~{d.pct.max():.1f}%로 요동, 개선 추세 없음",
             "생산일별 불량률 추이 — 월 평균선 대비")
    s.shapes.add_picture(img, Inches(0.75), Inches(1.75), width=Inches(11.8))
    over = int((d.pct > a["bad_pct"]).sum())
    footnote(s, f"{a['days']}일 중 {over}일이 월 평균({a['bad_pct']:.1f}%)을 넘었다. "
                f"특정 일자 문제가 아니라 상시적으로 발생하고 있다.")


def s_team(prs, a, img):
    t = a["team"]
    s = page(prs, f"팀 {a['worst_team']} 불량률 {t.pct.iloc[-1]:.1f}%, "
                  f"팀 {a['best_team']}의 {a['team_ratio']:.1f}배",
             "팀별 불량률 — 생산량은 세 팀 모두 동일")
    s.shapes.add_picture(img, Inches(0.75), Inches(1.8), width=Inches(6.3))
    rect(s, 7.5, 1.85, 5.25, 4.1, LIGHT)
    textbox(s, 7.8, 2.05, 4.7, 0.35, "읽는 법", size=13, bold=True, color=NAVY)
    lines = "\n".join(
        f"·  팀 {i} — {r.pct:.1f}%  (불량 {int(r['sum'])}건 / {int(r['size'])}건)"
        for i, r in t[::-1].iterrows())
    textbox(s, 7.8, 2.5, 4.7, 1.2, lines, size=12.5, color=DARK, spacing=1.6)
    textbox(s, 7.8, 3.95, 4.7, 1.8,
            f"세 팀 모두 {int(t['size'].iloc[0])}건씩 생산했고 설비도 같다.\n"
            f"생산량·설비가 동일한데 불량률만 {a['team_ratio']:.1f}배 차이가 난다면, "
            f"차이는 작업 방식·숙련도·교대 조건 쪽에 있을 가능성이 높다.",
            size=12, color=GREY, spacing=1.45)
    footnote(s, f"개선 1순위는 팀 {a['worst_team']}. "
                f"팀 {a['worst_team']}을 팀 M 수준으로만 낮춰도 월 불량이 "
                f"{int(t['sum'].iloc[-1] - t.pct.iloc[1]/100*t['size'].iloc[-1])}건 줄어든다.")


def s_dir(prs, a, img):
    d = a["direc"]
    s = page(prs, f"{a['worst_dir']} 방향 작업이 반대 방향의 {a['dir_ratio']:.1f}배 불량",
             "작업 방향(Direction)별 불량률")
    s.shapes.add_picture(img, Inches(0.75), Inches(1.8), width=Inches(6.3))
    rect(s, 7.5, 1.85, 5.25, 4.1, LIGHT)
    textbox(s, 7.8, 2.05, 4.7, 0.35, "읽는 법", size=13, bold=True, color=NAVY)
    lines = "\n".join(f"·  {i} — {r.pct:.1f}%  ({int(r['size'])}건)"
                      for i, r in d.sort_values("pct", ascending=False).iterrows())
    textbox(s, 7.8, 2.5, 4.7, 0.9, lines, size=12.5, color=DARK, spacing=1.6)
    mix = " / ".join(f"{i} {int(r['size'])}건" for i, r in d.iterrows())
    textbox(s, 7.8, 3.55, 4.7, 2.2,
            f"작업 방향은 설비 설정이 아니라 작업 순서의 문제다.\n"
            f"{a['worst_dir']} 방향에서만 불량이 몰린다면 해당 방향의 고정·정렬·"
            f"이송 조건을 우선 점검해야 한다.\n"
            f"두 방향의 생산 비중도 확인이 필요하다 ({mix}).",
            size=12, color=GREY, spacing=1.45)
    footnote(s, "작업 방향은 배치 조정으로 바꿀 수 있는 변수다 — 즉시 시도 가능한 개선안.")


def s_cross(prs, a, img):
    c = a["cross"]
    t, dr = a["cross_max"]
    mn = c.stack().min()
    s = page(prs, f"팀 {t} × {dr} 조합에서 불량률 {c.stack().max():.1f}%로 폭증",
             "팀과 작업방향을 교차한 불량률")
    s.shapes.add_picture(img, Inches(0.75), Inches(1.8), width=Inches(6.5))
    rect(s, 7.7, 1.85, 5.05, 4.1, LIGHT)
    textbox(s, 8.0, 2.05, 4.5, 0.35, "읽는 법", size=13, bold=True, color=NAVY)
    textbox(s, 8.0, 2.5, 4.5, 3.2,
            f"최악 조합 팀 {t}×{dr} {c.stack().max():.1f}%\n"
            f"최선 조합 {c.stack().idxmin()[0]}×{c.stack().idxmin()[1]} {mn:.1f}%\n\n"
            f"두 조합의 차이는 {c.stack().max()/mn:.0f}배다.\n"
            f"팀과 방향 어느 한쪽만 보면 놓치는 구간이며, "
            f"이 조합에 우선 개입하면 가장 큰 효과를 얻는다.",
            size=12.5, color=DARK, spacing=1.5)
    footnote(s, f"단기 조치안 — 팀 {t}에는 {dr} 방향 작업 배정을 줄이고, "
                f"불가피하면 이중 검사를 적용한다.")


def s_defects(prs, a, img):
    d = a["defects"]
    thr = a["threshold"]
    s = page(prs, f"표면결함 {thr}개부터 불량률이 {d.pct[thr]:.0f}%로 급등",
             "표면결함 개수별 불량률 — 선별 기준 후보")
    s.shapes.add_picture(img, Inches(0.75), Inches(1.8), width=Inches(6.5))
    rect(s, 7.7, 1.85, 5.05, 4.1, LIGHT)
    textbox(s, 8.0, 2.05, 4.5, 0.35, "읽는 법", size=13, bold=True, color=NAVY)
    lines = "\n".join(f"·  결함 {int(i)}개 — {r.pct:.1f}%  ({int(r['size'])}건)"
                      for i, r in d.iterrows())
    textbox(s, 8.0, 2.5, 4.5, 2.0, lines, size=12, color=DARK, spacing=1.5)
    n_over = int(d.loc[thr:, "size"].sum())
    textbox(s, 8.0, 4.75, 4.5, 1.1,
            f"결함 {thr}개 이상은 {n_over}건이며 이 구간의 불량률은 "
            f"{100*d.loc[thr:].eval('pct*size').sum()/d.loc[thr:,'size'].sum()/100:.0f}% 수준이다.\n"
            f"공정 중 선별 기준으로 쓸 수 있다.",
            size=12, color=GREY, spacing=1.45)
    footnote(s, f"제안 — 표면결함 {thr}개 이상은 후공정 투입 전 선별한다. "
                f"검사 부하와 선별 손실을 함께 검토해야 한다.")


def s_corr(prs, a, img):
    c = a["corr"]
    s = page(prs, "설비 파라미터는 불량과 무관, 오차·간극만 예측력 있음",
             "공정 변수와 불량 여부의 상관계수")
    s.shapes.add_picture(img, Inches(0.75), Inches(1.75), width=Inches(6.9))
    rect(s, 8.0, 1.85, 4.75, 4.1, LIGHT)
    textbox(s, 8.3, 2.05, 4.2, 0.35, "읽는 법", size=13, bold=True, color=NAVY)
    strong = c[c.abs() >= 0.3]
    weak = c[c.abs() < 0.1]
    textbox(s, 8.3, 2.5, 4.2, 3.2,
            "예측력 있음\n"
            + "\n".join(f"·  {i}  {v:+.2f}" for i, v in strong.items())
            + "\n\n사실상 무관 (±0.1 이내)\n"
            + "\n".join(f"·  {i}  {v:+.2f}" for i, v in weak.items()),
            size=12, color=DARK, spacing=1.5)
    footnote(s, "속도·온도·토크를 조정해 온 기존 개선 활동이 효과가 없던 이유다. "
                "다만 상관은 원인이 아니다 — 다음 장의 검증 계획으로 확인한다.")


def s_action(prs, a):
    t, dr = a["cross_max"]
    wt = a["worst_team"]
    s = page(prs, "개선 과제 3건 — 담당·검증방법·기대효과",
             "원인은 단정하지 않고 가설과 검증 방법으로 적는다")
    rows = [
        ("1", f"팀 {wt} 작업 방식 점검",
         f"가설: 작업 숙련도·교대 조건 차이\n검증: 작업일지·교육이력·교대표 대조",
         "생산팀 / 품질팀", "월 불량 40건↓"),
        ("2", f"{dr} 방향 공정 조건 재검토",
         f"가설: {dr} 방향의 고정·정렬 조건 불리\n검증: 방향별 치수 산포 비교",
         "기술팀", "월 불량 30건↓"),
        ("3", f"표면결함 {a['threshold']}개 이상 선별 도입",
         f"근거: 해당 구간 불량률 {a['defects'].pct[a['threshold']]:.0f}% 이상\n"
         f"검증: 2주 시범 적용 후 유출 불량 비교",
         "품질팀", "후공정 유출 감소"),
    ]
    hx = [0.55, 1.15, 4.3, 9.0, 11.0]
    hw = [0.6, 3.15, 4.7, 2.0, 1.75]
    heads = ["", "개선 과제", "가설 · 검증 방법", "담당", "기대효과"]
    rect(s, 0.55, 1.8, 12.2, 0.5, NAVY)
    for x, w, h in zip(hx, hw, heads):
        if h:
            textbox(s, x + 0.15, 1.9, w - 0.2, 0.35, h, size=12.5, bold=True, color=WHITE)
    y = 2.3
    for i, (no, task, method, owner, effect) in enumerate(rows):
        rect(s, 0.55, y, 12.2, 1.25, WHITE if i % 2 == 0 else LIGHT,
             line=RGBColor(0xE3, 0xE7, 0xEC))
        textbox(s, hx[0] + 0.15, y + 0.42, hw[0], 0.35, no, size=15, bold=True, color=BLUE)
        textbox(s, hx[1] + 0.15, y + 0.42, hw[1] - 0.2, 0.5, task, size=13, bold=True, color=NAVY_D)
        textbox(s, hx[2] + 0.15, y + 0.22, hw[2] - 0.2, 0.9, method, size=11.5, color=GREY, spacing=1.4)
        textbox(s, hx[3] + 0.15, y + 0.42, hw[3] - 0.2, 0.35, owner, size=12, color=DARK)
        textbox(s, hx[4] + 0.15, y + 0.42, hw[4] - 0.2, 0.35, effect, size=12, bold=True, color=RED)
        y += 1.35
    rect(s, 0.55, y + 0.05, 12.2, 0.7, RGBColor(0xFD, 0xF3, 0xF2))
    textbox(s, 0.85, y + 0.22, 11.8, 0.35,
            "기한은 이번 회의에서 확정한다 — 현재 모두 미정.",
            size=12.5, bold=True, color=RED)


# ── 실행 ──────────────────────────────────────────────────────────────
def build(excel: Path, out: Path) -> Path:
    df = pd.read_excel(excel)
    need = {"ProductionDate", "Team", "Direction", "Quality",
            "SurfaceDefects", "Error", "Gap"}
    missing = need - set(df.columns)
    if missing:
        raise SystemExit(f"필수 컬럼이 없습니다: {', '.join(sorted(missing))}")

    a = analyse(df)
    month = f"{a['period'][0]:%Y년 %m월}"

    imgs = dict(daily=chart_daily(a), team=chart_team(a), dir=chart_dir(a),
                cross=chart_cross(a), defects=chart_defects(a), corr=chart_corr(a))

    prs = Presentation()
    prs.slide_width, prs.slide_height = Emu(12192000), Emu(6858000)   # 16:9

    s_cover(prs, a, month)
    s_summary(prs, a)
    s_kpi(prs, a)
    s_daily(prs, a, imgs["daily"])
    s_team(prs, a, imgs["team"])
    s_dir(prs, a, imgs["dir"])
    s_cross(prs, a, imgs["cross"])
    s_defects(prs, a, imgs["defects"])
    s_corr(prs, a, imgs["corr"])
    s_action(prs, a)

    prs.save(out)
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--excel", default="Production_Data.xlsx")
    ap.add_argument("--out", default=None)
    args = ap.parse_args()

    excel = Path(args.excel)
    if not excel.is_absolute():
        excel = ROOT / excel
    if not excel.exists():
        raise SystemExit(f"엑셀을 찾을 수 없습니다: {excel}")

    df_head = pd.read_excel(excel, nrows=1)
    out = Path(args.out) if args.out else ROOT / (
        f"품질분석보고서_{pd.to_datetime(df_head.ProductionDate.iloc[0]):%Y-%m}.pptx")

    p = build(excel, out)
    print(f"생성 완료: {p.name}")


if __name__ == "__main__":
    main()
