# -*- coding: utf-8 -*-
"""
TeamWorks 宛 確認事項（第8回）を PDF にする。

第1回〜第7回（make_ddl_review_pdf.py / make_teamworks_qa2_pdf.py 〜
make_teamworks_qa7_pdf.py）の体裁をそのまま踏襲した続編。
A4 縦 / reportlab / 日本語フォント埋め込み / ヘッダー罫線とページ番号。

フォントは BIZ UDGothic（日本語フォント）を埋め込む。
中国語向けフォントは漢字の字形が異なるため使わない。
フォールバックにも混ぜないこと。

BIZ UDGothic は等幅（半角は 0.5em 固定）なので、コードの枠にもこのフォントを使う。
コードブロック内の空白は &nbsp; に置換して桁を保持する。

原稿は docs/handoff/TeamWorks確認事項_第8回_20260911.md。
文言・件数・表の内容は原稿から変更していない。

★第7回から足したもの（第8回の原稿がそういう構成のため）★
  1. qhead() — 「Q1-1」形式の設問見出しを薄い背景の帯にする。
     原稿が 章 → 節 → 設問 の3階層で、設問が25問あるため、
     S_H3（節）と同じ見た目だと設問が探せない。
     色・罫線・背景は第7回の callout() と同じパレットを使っている。
  2. form_table() — 空欄にご記入いただく表（末尾のご回答欄と 4-1 の
     「ご回答（分子 ÷ 分母）」列）。table() に GRID を足しただけで、
     空のセルに枠が見えるようにしている。上下パディングは記入用に 7pt。
  3. blank_box() — 自由記入欄の空枠。

★列幅の決め方（第7回と同じ）★
wordWrap="CJK" は ASCII の語中でも改行する。そのため
`robot_error_reports.clean_timetable_id` /
`clean_status_histories.started_at` /
`security_status_histories.reported_at` /
`control_error_histories` / `clean_timetables.precheck`
のような長い識別子は
  (1) 行頭に来るよう <br/> で明示的に改行してから置く
  (2) 列の内寸（列幅 - 左右パディング 12pt）が識別子の幅
      （BIZ UDGothic は半角 0.5em なので フォントサイズ × 文字数 ÷ 2）
      を上回るように列幅を決める
の2点で語中改行を避けている。check_teamworks_qa8_pdf.py の
「4. 識別子の語中改行」がこれを機械的に検査する。

具体的には、いちばん長い識別子 robot_error_reports.clean_timetable_id
（38字）が 8.8pt で 167.2pt = 59.0mm 必要なので、
この識別子が入る列は 63mm 以上にしてある
（前文のサマリ表 66mm / おわりにの優先度表 68mm / ご回答欄 96mm）。
"""
import os

from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.fonts import addMapping
from reportlab.platypus import (
    BaseDocTemplate, CondPageBreak, Frame, PageBreak, PageTemplate, Paragraph,
    Spacer, Table, TableStyle,
)

FONT_DIR = "C:/Windows/Fonts/"
pdfmetrics.registerFont(TTFont("JP", FONT_DIR + "BIZ-UDGothicR.ttc", subfontIndex=0))
pdfmetrics.registerFont(TTFont("JP-B", FONT_DIR + "BIZ-UDGothicB.ttc", subfontIndex=0))
# <b> タグを太字フォントへ対応づける
addMapping("JP", 0, 0, "JP")
addMapping("JP", 1, 0, "JP-B")

# 出力先。既定はこのスクリプトと同じディレクトリ。
# 環境変数 DWATCH_PDF_OUT で変えられる（例: 送付前に Downloads へ出す）。
OUT_DIR = os.environ.get("DWATCH_PDF_OUT",
                         os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(OUT_DIR, "DWatch_確認事項_第8回_20260911.pdf")

# 第1回 PDF と同一のパレット
INK = colors.HexColor("#1e293b")
SUB = colors.HexColor("#475569")
MUT = colors.HexColor("#94a3b8")
LINE = colors.HexColor("#cbd5e1")
HEAD_BG = colors.HexColor("#f1f5f9")
CODE_BG = colors.HexColor("#f8fafc")
NG = colors.HexColor("#b91c1c")   # 実装が止まる区分の強調色
WN = colors.HexColor("#b45309")   # 定義の確認区分の強調色

ss = getSampleStyleSheet()


def st(name, size, leading, font="JP", color=INK, space_after=0, space_before=0,
       left=0, first=0):
    """段落スタイルを作る。left/first でぶら下げインデントを表現する。"""
    s = ParagraphStyle(
        name, parent=ss["Normal"], fontName=font, fontSize=size, leading=leading,
        textColor=color, alignment=TA_LEFT, spaceAfter=space_after,
        spaceBefore=space_before, wordWrap="CJK",
        leftIndent=left, firstLineIndent=first,
    )
    s.bulletFontName = font
    return s


S_TITLE = st("t", 17, 24, "JP-B", INK, 2)
S_SUB = st("s", 9.5, 15, "JP", SUB, 0)
S_H2 = st("h2", 12.5, 18, "JP-B", INK, 4, 10)
S_H3 = st("h3", 10.5, 16, "JP-B", INK, 3, 8)
S_H4 = st("h4", 9.8, 15, "JP-B", SUB, 2, 6)
S_BODY = st("b", 9.5, 15.5, "JP", INK, 4)
S_LEAD = st("ld", 9.5, 15.5, "JP", INK, 2)          # 直後にコード枠や表が続く導入文
S_NOTE = st("n", 9, 14.5, "JP", SUB, 4)
S_LIST = st("li", 9.5, 15.5, "JP", INK, 2, 0, 15, -15)   # 「1. 」のぶら下げ
S_BULL = st("bu", 9.5, 15.5, "JP", INK, 2, 0, 14, -14)   # 「・」のぶら下げ
S_CELL = st("c", 8.8, 13.8, "JP", INK)
S_CELL_S = st("cs", 8.8, 13.8, "JP", SUB)
S_CODE = st("cd", 8.2, 12.6, "JP", INK)
S_TH = st("th", 8.4, 12, "JP-B", SUB)
S_QUOTE = st("q", 9.2, 14.8, "JP", SUB)
S_CALL = st("ca", 10.5, 17, "JP-B", INK)   # 目立たせたい一文を囲む枠の中身
S_QH = st("qh", 10, 15.5, "JP-B", INK)     # 設問（Q1-1 …）の見出し帯


def P(t, s=S_CELL):
    return Paragraph(t, s)


def on_page(canvas, doc):
    """全ページ共通のヘッダー罫線・フッター・ページ番号。第1回の PDF と同一。"""
    canvas.saveState()
    canvas.setFont("JP", 8)
    canvas.setFillColor(MUT)
    canvas.drawString(18 * mm, 12 * mm, "D-Watch / DFA Robotics")
    canvas.drawRightString(A4[0] - 18 * mm, 12 * mm, "%d" % doc.page)
    canvas.setStrokeColor(LINE)
    canvas.setLineWidth(0.4)
    canvas.line(18 * mm, 15.5 * mm, A4[0] - 18 * mm, 15.5 * mm)
    canvas.restoreState()


TBL_STYLE = [
    ("FONTNAME", (0, 0), (-1, -1), "JP"),
    ("BACKGROUND", (0, 0), (-1, 0), HEAD_BG),
    ("LINEBELOW", (0, 0), (-1, 0), 0.6, LINE),
    ("LINEBELOW", (0, 1), (-1, -2), 0.3, LINE),
    ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ("TOPPADDING", (0, 0), (-1, -1), 5),
    ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
    ("LEFTPADDING", (0, 0), (-1, -1), 6),
    ("RIGHTPADDING", (0, 0), (-1, -1), 6),
]

# 空欄にご記入いただく表。空のセルにも枠が見えるよう GRID を引く。
FORM_STYLE = [
    ("FONTNAME", (0, 0), (-1, -1), "JP"),
    ("BACKGROUND", (0, 0), (-1, 0), HEAD_BG),
    ("GRID", (0, 0), (-1, -1), 0.3, LINE),
    ("LINEBELOW", (0, 0), (-1, 0), 0.6, LINE),
    ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ("TOPPADDING", (0, 0), (-1, -1), 7),
    ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
    ("LEFTPADDING", (0, 0), (-1, -1), 6),
    ("RIGHTPADDING", (0, 0), (-1, -1), 6),
]


def table(rows, widths, cell_styles=None, head_color=None):
    """rows[0] を見出し行とする表。cell_styles で列ごとの段落スタイルを指定する。"""
    if cell_styles is None:
        cell_styles = [S_CELL] * len(rows[0])
    data = [[P(c, S_TH) for c in rows[0]]]
    for r in rows[1:]:
        data.append([P(c, cell_styles[i]) for i, c in enumerate(r)])
    t = Table(data, colWidths=widths, repeatRows=1)
    style = list(TBL_STYLE)
    if head_color is not None:
        style.append(("TEXTCOLOR", (0, 1), (0, -1), head_color))
    t.setStyle(TableStyle(style))
    return t


def form_table(rows, widths, cell_styles=None):
    """ご記入いただく表。table() に GRID を足しただけ。空セルは &nbsp; で高さを持たせる。"""
    if cell_styles is None:
        cell_styles = [S_CELL] * len(rows[0])
    data = [[P(c, S_TH) for c in rows[0]]]
    for r in rows[1:]:
        data.append([P(c if c else "&nbsp;", cell_styles[i])
                     for i, c in enumerate(r)])
    t = Table(data, colWidths=widths, repeatRows=1)
    t.setStyle(TableStyle(list(FORM_STYLE)))
    return t


def code(lines, width):
    """コードを等幅フォントの枠に入れる。空白は &nbsp; にして桁を保つ。"""
    esc = []
    for ln in lines:
        ln = ln.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        esc.append(ln.replace(" ", "&nbsp;"))
    t = Table([[P("<br/>".join(esc), S_CODE)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), CODE_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, LINE),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 6),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 6),
    ]))
    return t


def quote(text, width):
    """補足・お断りの引用。左に縦罫線を引く。"""
    t = Table([[P(text, S_QUOTE)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 3),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 0),
    ]))
    return t


def callout(text, width):
    """一番伝えたい一文を薄い背景の枠に入れる（引用ではないので本文色）。"""
    t = Table([[P(text, S_CALL)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), CODE_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 7),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
        ("LEFTPADDING", (0, 0), (-1, -1), 10),
        ("RIGHTPADDING", (0, 0), (-1, -1), 8),
    ]))
    return t


def qhead(text, width):
    """設問（Q1-1 …）の見出し帯。25問あるので節見出しと見分けがつくようにする。"""
    t = Table([[P(text, S_QH)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), HEAD_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 2.2, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 8),
    ]))
    return t


def blank_box(width, height):
    """自由記入欄の空枠。"""
    t = Table([[P("&nbsp;", S_CELL)]], colWidths=[width], rowHeights=[height])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BOX", (0, 0), (-1, -1), 0.5, LINE),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ]))
    return t


def bull(items):
    """「・」形式の箇条書き。"""
    return [Paragraph("・%s" % t, S_BULL) for t in items]


# ---------------------------------------------------------------- 表データ
# はじめに: 第7回の未回答3件
PENDING7_ROWS = [
    ["第7回でお願いしていたこと", "状態"],
    ["<b>接続先（ホスト名・ポート）</b>",
     "<b>未回答。全体を通じて最優先です</b>"],
    ["clean_timetables.precheck の<br/>値の範囲と反映時期", "未回答"],
    ["DDL の未反映4件<br/>（A-2 / A-3b / B-2 / B-3b）",
     "未回答（お手すきの折で結構です）"],
]

# 前文: 10部構成
# 1列目に robot_error_reports.clean_timetable_id（38字）が入るので 66mm 必要。
SUMMARY_ROWS = [
    ["#", "内容", "優先度"],
    ["<b>1. 未実施の清掃にも<br/>履歴の行は作られるか</b>",
     "<b>当方の設計の土台です</b>", "<b>最優先</b>"],
    ["<b>2. DATETIME の<br/>タイムゾーン</b>",
     "<b>日本時間の壁時計値か、UTC か</b>", "<b>最優先</b>"],
    ["<b>3. 実際の開始・終了時刻は<br/>どこにあるか</b>",
     "started_at / ended_at は<br/>予定時刻に見えます", "高"],
    ["<b>4. サマリの率・分子・分母の<br/>関係</b>",
     "計算式と件数列の定義を<br/>まとめてお伺いします", "高"],
    ["<b>5. 未実施行<br/>（clean_error = 1）の<br/>他カラムの値</b>",
     "一部は<b>お願い</b><br/>（収集処理の変更を伴います）", "中"],
    ["<b>6. clean_error が<br/>2 / 3 / 4 のときの扱い</b>",
     "エラー報告・完了率・優先順位", "中"],
    ["<b>7.<br/>robot_error_reports.clean_timetable_id<br/>"
     "はどちらの行を指すか</b>",
     "エラー率が 0% になる<br/>可能性があります", "高"],
    ["<b>8. control_error_histories は<br/>実際に使われるか</b>",
     "D-Secアラート画面の<br/>作り直しに関わります", "中"],
    ["<b>9. 軽い確認2件</b>",
     "バッテリー履歴 / エリア別完了率", "低"],
    ["<b>10. 継続のお願い:<br/>DB更新用アカウント</b>",
     "第5回・第6回からの継続。<br/>時期の目安だけでも", "<b>最古の待ち</b>"],
]

# 1-3. Q1-1
Q11_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>作られる</b>（clean_error = 1 の行が1行できる）"],
    ["B", "<b>作られない</b>（実施された清掃にだけ行ができる）"],
    ["C", "場合による（条件をお教えください）"],
]

Q12_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>必ず1行</b>"],
    ["B", "複数行になることがある（どういうときかをお教えください）"],
]

# 2-1. 何が起きるか
TZ_MECH_ROWS = [
    ["#", "何が起きるか"],
    # 「new Date(値 + "Z")」は 行末に来ると "Z") が次行へ割れる。<br/> で行頭に置く。
    ["1", "<b>当方が使っている Prisma の MariaDB 用アダプタは、"
          "DATETIME を UTC の値として読みます</b><br/>"
          "（new Date(値 + \"Z\") という実装です）。"
          "<b>この挙動は設定で変更できません。接続文字列に timezone を"
          "指定しても効きません</b>"
          "（アダプタがドライバ側の変換を通らないためです）"],
    ["2", "<b>その値を画面用の日本時間に直す処理が、さらに9時間足します</b>"],
]

TZ_EX_ROWS = [
    ["DBの clean_timetables.start_at の生値", "DBが日本時間の場合、実際は",
     "当方の画面に出る時刻"],
    ["2026-09-11 02:00:00", "9月11日 深夜2時の清掃", "<b>9月11日 11:00</b>"],
]

Q21_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>日本時間の壁時計値</b>（深夜2時の清掃なら 02:00:00 が入っている）"],
    ["<b>B</b>", "<b>UTC の壁時計値</b>（深夜2時の清掃なら 17:00:00（前日）が入っている）"],
    ["C", "列によって違う（違う場合、どの列がどちらかをお教えください）"],
]

TZ_COL_ROWS = [
    ["テーブル", "列"],
    ["clean_timetables", "start_at / end_at"],
    ["clean_status_histories", "started_at / ended_at"],
    ["clean_statuses", "started_at / ended_at"],
    ["robot_error_reports", "errored_at / responded_at / resolved_at"],
    ["control_error_histories", "同種の日時列"],
]

Q22_ROWS = [
    ["選択肢", "内容"],
    ["A", "サーバのローカル時刻（日本時間）で書いている"],
    ["B", "UTC に直してから書いている"],
    ["C", "Pudu Cloud から返ってきた値をそのまま書いている"
          "（この場合、Pudu 側がどちらかもお分かりでしたら）"],
]

Q23_ROWS = [
    ["#", "教えていただきたいもの"],
    ["1", "start_at の<b>生値</b>（例: 2026-09-11 02:00:00）"],
    ["2", "それが<b>実際には何時の清掃を指すか</b>（例: 9月11日の深夜2時の清掃）"],
]

Q24_ROWS = [
    ["#", "教えていただきたいもの"],
    ["1", "本番DBの time_zone の設定値（global と session の既定値）"],
    ["2", "データ収集処理が接続する際に time_zone を設定していますか"
          "（していれば、その値）"],
]

# 3-1. 定義書の備考
DEF_REMARK_ROWS = [
    ["列", "定義書の備考"],
    ["clean_status_histories.started_at",
     "<b>「清掃スケジュールの清掃開始日時を格納する」</b>"],
    ["clean_status_histories.ended_at",
     "<b>「清掃スケジュールの清掃終了日時を格納する」</b>"],
]

Q31_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>合っている。予定時刻が入る</b>"],
    ["B", "実績時刻が入る（定義書の備考が実態と違う）"],
    ["C", "開始と終了で違う（違う場合、どちらがどちらかをお教えください）"],
]

Q32_ROWS = [
    ["選択肢", "内容"],
    ["A", "clean_statuses.started_at / ended_at が実績時刻"],
    ["B", "clean_task_reports 側に実績時刻がある（列名をお教えください）"],
    ["C", "<b>実績時刻は保持していない</b>"],
    ["D", "その他（お教えください）"],
]

# 4-2. Q4-1 率4列
Q41_ROWS = [
    ["列", "当方の理解", "ご回答（分子 ÷ 分母）"],
    ["operation_ratio<br/>（稼働率）", "清掃実施回数 ÷ 清掃予定回数", ""],
    ["launch_ratio<br/>（出発成功率）", "定刻出発回数 ÷ 清掃予定回数", ""],
    ["clean_ratio<br/>（清掃完了率）",
     "<b>Σ清掃面積 ÷ Σ清掃対象面積<br/>（面積ベース）</b>", ""],
    ["error_ratio<br/>（エラー発生率）", "エラー発生回数 ÷ 清掃実施回数", ""],
]

Q42_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>同じ。clean_error != 1 の件数</b>"],
    ["B", "clean_error = 0（正常）のみを数える（2 / 3 / 4 も出発していない扱い）"],
    ["C", "<b>「定刻」を判定している</b>"
          "（予定時刻から一定時間内に開始したものだけ。"
          "その場合、猶予時間もお教えください）"],
    ["D", "その他（お教えください）"],
]

Q43_MEASURE_ROWS = [
    ["項目", "実測値"],
    ["突き合わせた行数", "施設サマリ 24行"],
    ["食い違った行数", "<b>6行</b>"],
    ["最大の差", "<b>0.5 ポイント</b>"],
    ["例", "facility_id = 4:<br/>"
           "launch_ratio = <b>84.5</b> に対し、<br/>"
           "launch_count / task_count × 100 = <b>85.00</b>"],
]

Q43_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>一致する</b>"],
    ["B", "丸めのぶんだけ、わずかにずれることがある"],
    ["C", "率と件数は別の元データから作っているため、必ずしも一致しない"],
]

Q44_ROWS = [
    ["選択肢", "内容"],
    ["A", "四捨五入"],
    ["B", "切り捨て"],
    ["C", "切り上げ"],
]

# 5-1
Q51_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>既定値の 0 が入る</b>"],
    ["B", "2 が入る"],
    ["C", "-1 が入る"],
    ["D", "その他（お教えください）"],
]

Q52_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>3列とも 0</b>"],
    ["B", "task_area には予定していた清掃対象面積が入る"
          "（clean_area / clean_ratio は 0）"],
    ["C", "その他（お教えください）"],
]

# 6-2
Q61_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>2 / 3 / 4 のいずれも作られる</b>"],
    ["B", "4（清掃ロボット異常）のときだけ作られる"],
    ["C", "<b>作られない</b>（履歴の clean_error にだけ記録される）"],
    ["D", "場合による（条件をお教えください）"],
]

Q62_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>埋まる</b>（どの清掃のエラーかを対応づけられる）"],
    ["B", "空（NULL）のことがある"],
]

Q63_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>途中まで清掃した実際の値が入る</b>"
          "（例: 40% まで進んで異常終了したら clean_ratio = 40）"],
    ["B", "<b>0 が入る</b>（異常なので実績は記録しない）"],
    ["C", "値によって違う（違う場合、どれがどちらかをお教えください）"],
]

Q65_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>0〜4 のみ。5 以上は入らない</b>"],
    ["B", "入りうる（予定されている値がありましたらお教えください）"],
]

Q66_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>同じ意味。clean_statuses が最新、履歴側が過去分</b>"],
    ["B", "意味が違う（違いをお教えください）"],
]

# 7-2
Q71_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>事前チェックの行を指す</b>（precheck が 1 / 2 の行）"],
    ["<b>B</b>", "<b>本清掃の行を指す</b>（precheck が 0 の行）"],
    ["C", "エラーの発生時点による"
          "（清掃中に発生したエラーがどちらに付くかをお教えください）"],
]

# 8-1
CEH_SRC_ROWS = [
    ["出典", "記載"],
    ["テーブル定義書の役割欄",
     "<b>「現時点では未使用. 清掃状態, 清掃状態履歴で確認できるため.」</b>"],
    ["第2回のご回答",
     "このテーブルの status の値域"
     "（<b>0: 未対応 / 1: 対応中 / 2: 対応済</b>）を<br/>"
     "直接お教えいただいております"],
    ["同・制御盤の生死報告について",
     "「制御盤の生死報告が来ない場合は、サーバーから<br/>"
     "『制御盤異常』としてエラーが上がる」"],
    ["DB更新権限のお願い（10章）",
     "このテーブルの4カラムを更新対象に含めております"],
]

Q81_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>入る</b>（定義書の役割欄の注記が古い）"],
    ["<b>B</b>", "<b>入らない・未使用</b>"
                 "（制御盤異常はどのテーブルで確認すればよいかをお教えください）"],
    ["C", "今は入らないが、今後入れる予定（時期の目安がありましたら）"],
]

# 9-1
Q91_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>保持する予定は無い</b>"],
    ["B", "保持する予定がある（どのテーブル・列になるかをお教えください）"],
]

Q92_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>取得できる</b>（どのテーブルに入る想定かをお教えください）"],
    ["<b>B</b>", "<b>取得できない</b>（Pudu Cloud API が返さない）"],
    ["C", "未確認（その旨をお知らせいただければ、当方は B と同じ扱いで進めます）"],
]

# 10-1
UPDATE_ACCT_ROWS = [
    ["テーブル", "更新するカラム"],
    ["robot_error_reports",
     "status / comment / responded_at / resolved_at"],
    ["control_error_histories",
     "status / comment / responded_at / resolved_at"],
]

# おわりに: まずはこの3つ
TOP3_ROWS = [
    ["順", "項目", "何が起きるか"],
    ["<b>1</b>",
     "<b>1. 未実施の清掃にも<br/>履歴の行は作られるか<br/>（Q1-1）</b>",
     "<b>A / B のどちらかを選んでいただくだけです。</b>"
     "外れていると、正常な清掃まで「出発失敗」として並びます（過剰検知）"],
    ["<b>2</b>",
     "<b>2. DATETIME の<br/>タイムゾーン<br/>"
     "（特に Q2-3 の実データ1行）</b>",
     "<b>ずれていると、翌朝の確認時に深夜の清掃が画面から消えます。"
     "運用そのものが成り立ちません</b>。"
     "Q2-3 の1行をいただければ、あとは当方で検証できます"],
    ["<b>3</b>",
     "<b>4. サマリの率・分子・分母<br/>（Q4-1〜Q4-4）</b>",
     "レポート画面の数字が、清掃詳細ログの件数と食い違います。"
     "「補足」の文書があれば、それをお送りいただくだけで解決します"],
]

# おわりに: 残りの優先度
# 2列目に robot_error_reports.clean_timetable_id（38字）が入るので 68mm 必要。
REST_ROWS = [
    ["順", "項目", "理由"],
    ["4", "7.<br/>robot_error_reports.clean_timetable_id<br/>の指す先",
     "エラー発生率が 0% と表示される可能性があります。"
     "例外が出ないため気づけません"],
    ["5", "3. 実際の開始・終了時刻の所在",
     "画面に予定時刻を実績として出してしまうためです"],
    ["6", "6. clean_error 2 / 3 / 4 の扱い",
     "アラートに上がるか、完了率がどうなるかが変わります"],
    ["7", "8. control_error_histories は<br/>使われるか",
     "未使用でしたら D-Secアラート画面を作り直します"],
    ["8", "5. 未実施行の他カラムの値",
     "値が分かれば十分です。変更は二の次です"],
    ["9", "<b>10. DB更新用アカウント</b>",
     "<b>最古の待ち（2026-09-03〜）ですが、急ぎません。"
     "時期の目安だけで結構です</b>"],
    ["10", "9. 軽い確認2件", "「無い」と分かれば当方で判断できます"],
]

# ご回答欄（25問）
ANSWER_ROWS = [
    ["質問番号", "質問の要約", "ご回答"],
    ["<b>Q1-1</b>",
     "<b>未実施の清掃にも clean_status_histories の行は作られるか<br/>"
     "（A / B / C）</b>", ""],
    ["<b>Q1-2</b>",
     "1つの clean_timetable_id に履歴は必ず1行か（A / B）", ""],
    ["<b>Q2-1</b>", "DATETIME は日本時間の壁時計値か、UTC か（A / B / C）", ""],
    ["<b>Q2-2</b>", "収集処理が書き込むときの基準（A / B / C）", ""],
    ["<b>Q2-3</b>",
     "<b>clean_timetables の1行の start_at の生値と、<br/>"
     "それが指す実際の時刻</b>", ""],
    ["<b>Q2-4</b>",
     "本番DBの time_zone 設定と、収集処理の接続時の設定", ""],
    ["Q3-1", "clean_status_histories.started_at / ended_at は<br/>"
             "予定時刻か（A / B / C）", ""],
    ["Q3-2", "実績時刻の所在（A / B / C / D）", ""],
    ["<b>Q4-1</b>",
     "<b>率4列の計算式（分子 ÷ 分母）。または「補足」の文書</b>", ""],
    ["<b>Q4-2</b>",
     "task_count / launch_count / error_count の定義<br/>"
     "（launch_count は A / B / C / D）", ""],
    ["<b>Q4-3</b>", "率と件数から再計算した値は一致するか（A / B / C）", ""],
    ["<b>Q4-4</b>", "率の丸め方（A / B / C）＋検算のお願いの可否", ""],
    ["Q5-1", "clean_error = 1 の行の clean_status（A / B / C / D）", ""],
    ["Q5-2", "同・task_area / clean_area / clean_ratio（A / B / C）<br/>"
             "＋面積のお願いの可否", ""],
    ["Q6-1", "clean_error 2/3/4 でエラー報告も作られるか<br/>（A / B / C / D）",
     ""],
    ["Q6-2", "その行の clean_timetable_id は埋まるか（A / B）", ""],
    ["Q6-3", "2/3/4 の行の clean_ratio と面積（A / B / C）", ""],
    ["Q6-4", "複数の異常が同時に起きた場合の clean_error の優先順位", ""],
    ["Q6-5", "clean_error に 5 以上が入りうるか（A / B）", ""],
    ["Q6-6", "clean_statuses.clean_error と履歴側の使い分け（A / B）", ""],
    ["<b>Q7-1</b>",
     "robot_error_reports.clean_timetable_id は<br/>"
     "事前チェック行か本清掃行か（A / B / C）", ""],
    ["<b>Q8-1</b>", "control_error_histories にデータは入るか（A / B / C）", ""],
    ["Q9-1", "事前チェック時点のバッテリー残量を保持する予定（A / B）", ""],
    ["Q9-2", "Pudu Cloud からエリア別完了率は取得できるか（A / B / C）", ""],
    ["<b>Q10-1</b>",
     "DB更新用アカウントの発行時期の目安（未定でも結構です）", ""],
]

# ご回答欄: 第7回の3件
ANSWER7_ROWS = [
    ["項目", "ご回答"],
    ["<b>接続先（ホスト名またはIP / ポート）</b>", ""],
    ["接続元IP制限の有無 / SSL-TLS の要否 / ステージングか本番か", ""],
    ["clean_timetables.precheck の値の範囲・反映時期・<br/>"
     "既存データの扱い・新旧混在期間の有無", ""],
    ["DDL の未反映4件<br/>（A-2 採番 / A-3b 桁数 / B-2 索引 / B-3b DEFAULT）", ""],
]


def build():
    doc = BaseDocTemplate(
        OUT, pagesize=A4,
        leftMargin=18 * mm, rightMargin=18 * mm,
        topMargin=18 * mm, bottomMargin=20 * mm,
        title="D-Watch 確認事項（第8回）",
        author="DFA Robotics",
        subject="未実施行の有無・DATETIME のタイムゾーンほか10件のご確認",
    )
    frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="f")
    doc.addPageTemplates([PageTemplate(id="p", frames=[frame], onPage=on_page)])

    W = doc.width          # 174mm
    CW = W - 6 * mm        # 引用・コード枠の幅
    story = []

    # ---------------------------------------------------------- 表題と前文
    story.append(Paragraph("確認事項（第8回）", S_TITLE))
    story.append(Paragraph(
        "宛先: TeamWorks（DB開発側）　作成日: 2026-09-11　"
        "作成者: DFA Robotics", S_SUB))
    story.append(Spacer(1, 9))

    story.append(Paragraph("はじめに（第7回がまだ未回答です）", S_H2))
    story.append(Paragraph(
        "<b>第7回（2026-09-10 送付）へのご回答は、"
        "本書作成時点でまだいただいておりません。</b>"
        "<b>第7回の3件は取り下げません。引き続きお願いいたします。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(PENDING7_ROWS, [W - 60 * mm, 60 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>あわせて、DB更新用アカウントの発行は、"
        "第5回（2026-09-07）・第6回（2026-09-08）から"
        "継続してお願いしている件です。</b>"
        "<b>第7回では書き漏らしてしまいましたが、"
        "取り下げたわけではありません。</b>"
        "<b>本書の10章に、あらためて記載しております。"
        "急ぎではございません。</b>", S_BODY))
    story.append(Paragraph(
        "<b>本書は第7回の追加です。第7回の内容を差し替えるものではありません。</b>",
        S_BODY))
    story.append(Paragraph(
        "そのうえで、<b>ローカルの検証環境でレポート画面まで動かし、"
        "あわせて未確認事項の棚卸しを行ったところ、"
        "確認事項が10件になりました。</b>"
        "<b>うち2件（1章・2章）は、"
        "放置すると画面に出る内容そのものが壊れる性質のものです。</b>", S_BODY))
    story.append(Paragraph(
        "本書は次の10部構成です。"
        "<b>すべて質問です。1問1答の形にしております。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(SUMMARY_ROWS, [66 * mm, W - 92 * mm, 26 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>なお、5と4の後段は貴社の担当領域（DBとバッチ）への"
        "変更を伴いますので、「お願い」として書いております。</b>"
        "<b>そのままで差し支えない、というご判断であれば、"
        "その旨をお知らせいただければ当方で対応します。</b>", S_BODY))

    # ---------------------------------------------------------- 1. 未実施行
    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "1. 清掃が実施されなかった場合も、clean_status_histories に"
        "行は作られますでしょうか — 最優先", S_H2))

    story.append(Paragraph(
        "1-1. 当方は「作られる」という前提で実装しております", S_H3))
    story.append(Paragraph(
        "当方は、清掃詳細ログの「出発したかどうか」を次のように判定し、"
        "<b>すでに実装済み</b>です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "出発しなかった",
        "  = clean_status_histories に行がある",
        "    かつ その行の clean_error が 1（異常(清掃未実施)）",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>つまり「清掃が実施されなかった場合も、clean_error = 1 の行が"
        "1行作られる」という前提です。</b>"
        "<b>この前提が、本作業の設計そのものの土台になっております。</b>",
        S_BODY))

    story.append(Paragraph(
        "1-2. 前提が外れた場合に何が起きるか（正直に書きます）", S_H3))
    story.append(Paragraph(
        "<b>当方は「履歴の行が無い過去の清掃」を『出発失敗』として"
        "画面に出す設計にしております。</b>", S_BODY))
    story.append(Paragraph(
        "したがって、<b>未実施のときに行が作られない運用ですと、"
        "「結果がまだ書き込まれていないだけの清掃」まで"
        "出発失敗として並びます</b>（過剰検知）。", S_BODY))
    story.append(Paragraph(
        "<b>画面が真っ赤になる一方で、例外もエラーログも出ません。</b>"
        "<b>そのため、当方だけでは正誤を判断できません。</b>", S_BODY))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph("1-3. お伺いしたいこと（2問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q1-1. 清掃が実施されなかった場合、clean_status_histories に"
        "行は作られますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q11_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(55 * mm))
    story.append(qhead(
        "Q1-2. 1つの clean_timetable_id に対し、clean_status_histories は"
        "必ず1行でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q12_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(quote(
        "<b>補足</b>: 当方は現在、"
        "<b>複数行あった場合は ended_at が最新の1件を採用する</b> "
        "という規則で実装しております。B の場合、"
        "この選び方で差し支えないかもあわせてお教えいただけますと助かります。",
        CW))
    story.append(Spacer(1, 4))

    story.append(Paragraph("1-4. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>前提が外れても「出発失敗」という機能自体は残せますが、"
        "上記のとおり過剰検知になります。</b>",
        "<b>運用担当が毎朝見る画面ですので、"
        "誤検知が続くと画面そのものが信用されなくなります。</b>",
        "<b>本書のなかで、当方の実装への影響がいちばん大きい項目です。"
        "このため冒頭に置いております。</b>",
    ]))

    # ---------------------------------------------------------- 2. タイムゾーン
    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph(
        "2. DATETIME に入っているのは日本時間の壁時計値でしょうか、"
        "UTC でしょうか — 最優先", S_H2))

    story.append(Paragraph("2-1. 当方の側で起きていること（実測）", S_H3))
    story.append(Paragraph(
        "<b>当方のアプリは、DATETIME の値を UTC として読んだうえで、"
        "さらに9時間足しています。</b>"
        "<b>DBが日本時間の壁時計値を持っている場合、"
        "画面に出る時刻が9時間ずれます。</b>", S_BODY))
    story.append(Paragraph(
        "内訳は次の2段です。いずれも当方のコードを実測して確認したものです。",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(TZ_MECH_ROWS, [10 * mm, W - 10 * mm], [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph("具体例です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(TZ_EX_ROWS, [62 * mm, 60 * mm, W - 122 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))

    story.append(Paragraph("2-2. 表示だけの問題ではありません", S_H3))
    story.append(Paragraph(
        "<b>未来の予定を一覧から外す判定にも、この時刻を使っています。</b>",
        S_BODY))
    story.append(Paragraph(
        "D-Watch は「<b>夜間に自動実行された清掃を、"
        "翌朝 DFA の運用担当が確認する</b>」という使い方を想定しています。"
        "上の例で、<b>運用担当が翌朝8時に画面を開くと、"
        "深夜2時の清掃が「11:00の予定」と読まれます。</b>"
        "<b>まだ来ていない予定として扱われ、一覧から消えます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>つまり、確認したい清掃がちょうど画面に出ない、"
        "という壊れ方をします。</b>"
        "<b>このため最優先でお伺いしております。</b>", S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph("2-3. お伺いしたいこと（4問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q2-1. DATETIME 列に格納されているのは、次のどちらでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q21_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("対象として特に知りたい列は次のとおりです。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(TZ_COL_ROWS, [46 * mm, W - 46 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(55 * mm))
    story.append(qhead(
        "Q2-2. データ収集処理が書き込むときの基準は、次のどれでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q22_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(70 * mm))
    story.append(qhead(
        "Q2-3. 実データを1行だけご提示いただけませんでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>clean_timetables の任意の1行について、"
        "次の2つをお教えください。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q23_ROWS, [10 * mm, W - 10 * mm], [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>この2つがあれば、当方で機械的に検証できます。</b>"
        "<b>1行で結構です。以後この件でお伺いすることはありません。</b>",
        S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "なお、第7回でお願いしている接続先をいただければ、"
        "生値そのものは当方で見られます。"
        "ただし <b>その値が実際に何時の清掃を指すのかは、"
        "当方には分かりません</b> ので、"
        "2番だけはどうしてもお伺いする必要があります。", CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(70 * mm))
    story.append(qhead(
        "Q2-4. TIMESTAMP 型の列について、本番DBの time_zone 設定を"
        "お教えください", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "created_at / updated_at のほか、control_reports.reported_at / "
        "security_status_histories.reported_at は <b>TIMESTAMP 型</b> です。",
        S_BODY))
    story.append(Paragraph(
        "<b>TIMESTAMP は DATETIME と違い、セッションの time_zone 設定によって"
        "読み出される値が変わります。</b>"
        "このため、DATETIME とは別にお伺いしております。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q24_ROWS, [10 * mm, W - 10 * mm], [S_CODE, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("2-4. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>当方から伺っているのは「おそらく日本時間です」までで、"
        "確定しておりません。</b>",
        "<b>推測で実装すると、ずれていた場合に"
        "「画面には出ているが1つもデータが無い日がある」という"
        "形で表面化します。例外もエラーも出ないため、気づくのが遅れます。</b>",
        "<b>上の 2-2 のとおり、運用そのものが成り立たなくなる"
        "性質の問題ですので、当方の判断で決めてしまわず、"
        "お伺いすることにしました。</b>",
    ]))

    # ---------------------------------------------------------- 3. 実績時刻
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "3. clean_status_histories の started_at / ended_at は"
        "予定時刻でしょうか", S_H2))

    story.append(Paragraph("3-1. 現状の理解", S_H3))
    story.append(Paragraph(
        "テーブル定義書の備考に、次のようにございました。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(DEF_REMARK_ROWS, [64 * mm, W - 64 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>この書き方ですと、実際に清掃を始めた時刻ではなく、"
        "予定の時刻に読めます。</b>", S_BODY))
    story.append(Paragraph(
        "いっぽう clean_statuses 側の同名の列（started_at / ended_at）は "
        "<b>備考が空欄で、NULL 許可</b> になっております。"
        "<b>こちらが実績時刻なのかもしれない、とも読めます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>当方には、どちらが正か判断できませんでした。</b>", S_BODY))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph("3-2. お伺いしたいこと（2問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q3-1. clean_status_histories.started_at / ended_at は予定時刻という"
        "理解で合っていますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q31_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(60 * mm))
    story.append(qhead(
        "Q3-2. A の場合、実際に開始・終了した時刻はどこかに"
        "保持されますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q32_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("3-3. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>清掃詳細ログの画面に、清掃の開始・終了時刻を表示します。</b>"
        "<b>予定時刻を実績として出してしまうと、"
        "運用担当が「定刻どおりに動いた」と誤読します。</b>",
        "同じ清掃に履歴が複数ある場合、当方は ended_at が最新の1件を"
        "採用する実装にしています（Q1-2）。"
        "<b>予定時刻だとすると、この選び方の根拠が変わります。</b>",
        "C（保持していない）でも当方の実装は動きます。"
        "<b>その場合は、画面に予定時刻である旨を明示する形に直します。</b>",
    ]))

    # ---------------------------------------------------------- 4. サマリ
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph("4. サマリの率・分子・分母の関係をお教えください",
                           S_H2))
    story.append(Paragraph(
        "<b>本節の後段のお願いは、バッチの変更を伴う可能性があります。"
        "そうと分かるように書いております。</b>", S_BODY))

    story.append(Paragraph("4-1. お伺いしたいことの全体像", S_H3))
    story.append(Paragraph(
        "サマリテーブル（customer_summaries / facility_summaries）には "
        "率4列（operation_ratio / launch_ratio / clean_ratio / error_ratio）と "
        "件数3列（task_count / launch_count / error_count）がございます。",
        S_BODY))
    story.append(Paragraph(
        "<b>当方は、この率と件数の関係が読み取れずにおります。</b>"
        "<b>関連する内容が分散しないよう、"
        "1つの節にまとめてお伺いいたします。</b>", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>補足</b>: 定義書の比率4列の備考は <b>「補足を参照.」</b> と"
        "のみ記載されており、"
        "<b>その「補足」の該当部分が、当方の受領範囲に"
        "含まれておりませんでした。</b>"
        "<b>もし文書としてお持ちでしたら、それをお送りいただくだけで"
        "本節（Q4-1〜Q4-4）はすべて解決します。</b>", CW))
    story.append(Spacer(1, 4))

    # Q4-1 の記入表（5行）が改ページで割れないよう、見出しごと高さを確保する
    story.append(CondPageBreak(115 * mm))
    story.append(Paragraph("4-2. お伺いしたいこと（4問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q4-1. 率4列は、それぞれどの式で算出されていますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>分子と分母が何かを、1行ずつお教えいただけますと助かります。</b>",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(form_table(Q41_ROWS, [40 * mm, 56 * mm, W - 96 * mm],
                            [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(quote(
        "<b>特に分からずにおります点</b>: "
        "<b>件数列は task_count / launch_count / error_count の3つだけで、"
        "operation_ratio の分子にあたる「清掃実施回数」の列が"
        "見当たりません。</b>"
        "<b>このため当方の実装では、当期（進行中の週・月）の稼働率と"
        "出発成功率が必ず同じ値になっております。</b>"
        "<b>これが正しいのかどうかを判断できずにおります。</b>", CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(85 * mm))
    story.append(qhead(
        "Q4-2. 件数3列の定義をお教えください（特に launch_count）", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "当方は、清掃詳細ログの画面で「その清掃が出発したかどうか」を"
        "次のように判定しており、実装済みです。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "出発した",
        "  = clean_status_histories に行がある",
        "    かつ その行の clean_error が 1（異常(清掃未実施)）ではない",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>サマリの launch_count も同じ定義でしょうか。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q42_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>あわせて、task_count（清掃予定回数と理解しております）と "
        "error_count の数え方も一言でお教えいただけますと助かります。</b>",
        S_BODY))

    story.append(CondPageBreak(90 * mm))
    story.append(qhead(
        "Q4-3. 率カラムと、件数カラムから再計算した値は"
        "一致しますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>当方の検証用ダミーデータで検算したところ、"
        "一致しない行がございました。</b>", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>先にお断りいたします</b>: "
        "<b>当方はまだ実データに接続できておりません。</b>"
        "<b>下記はいただいたDDLから当方がローカルに作った"
        "ダミーデータに対する検算です。</b>"
        "<b>差が出た原因が当方の作ったデータ側にある可能性が高いと"
        "考えております。</b>"
        "<b>貴社のバッチに問題がある、という指摘ではありません。</b>", CW))
    story.append(Spacer(1, 5))
    story.append(table(Q43_MEASURE_ROWS, [40 * mm, W - 40 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(table(Q43_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(75 * mm))
    story.append(qhead("Q4-4. 率カラムの丸め方をお教えください", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "double(4,1)（小数第一位）に格納する際、"
        "小数第二位以下をどう処理されていますでしょうか。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q44_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph("お願い（Q4-3 が B / C の場合）", S_H4))
    story.append(Paragraph(
        "<b>もし率と件数が一致しない作りでしたら、"
        "一致する形にしていただけないでしょうか。</b>"
        "<b>バッチの変更を伴うお願いですので、"
        "難しければ「一致しません」とお知らせいただくだけで結構です。</b>"
        "<b>その場合、当方は差が出ても異常とみなさない実装にします。</b>",
        S_BODY))

    story.append(CondPageBreak(55 * mm))
    story.append(Paragraph("4-3. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>定義が違うと、レポート画面の「出発成功率」と、"
        "清掃詳細ログの「出発」の件数が食い違います。</b>"
        "<b>同じ画面群の中で数が合わないと、"
        "運用担当はどちらを信じてよいか分かりません。</b>",
        "<b>当方は、サマリの値を当方の計算で上書きしない方針です。</b>"
        "<b>貴社のバッチが出した値をそのまま表示しますので、"
        "定義を合わせておく必要があります。</b>",
        "<b>当方の設計方針では、サマリは「丸める前の小数」と「分子・分母」の"
        "両方を持ち、検算できることを前提としております。</b>"
        "<b>貴社が件数列を用意してくださっているのも、"
        "この趣旨と理解しております。</b>",
        "<b>実データに接続したあと差が出たときに、"
        "「当方の実装の誤り」なのか「バッチの仕様どおり」なのかを"
        "切り分けられません。</b>"
        "<b>先に「一致するはず／しないはず」が分かっていれば、"
        "当方だけで判断できます。</b>",
    ]))

    # ---------------------------------------------------------- 5. 未実施行の値
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "5. 未実施の行（clean_error = 1）の、"
        "ほかの列には何が入りますでしょうか", S_H2))
    story.append(Paragraph(
        "<b>本節は、1章の Q1-1 が A（行が作られる）だった場合の"
        "お伺いです。</b>"
        "<b>Q5-2 の後半は、収集処理の変更を伴う「お願い」です。"
        "そうと分かるように書いております。</b>", S_BODY))

    # Q5-1 の選択肢表（4行）が割れないよう、見出しごと高さを確保する
    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("5-1. お伺いしたいこと（2問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q5-1. clean_error = 1 の行の clean_status には"
        "何が入りますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "clean_status_histories.clean_status の備考は、定義書上 "
        "<b>「2: 清掃終了(セキュリティ有効), -1: 清掃完了(セキュリティ無効)」の"
        "2値のみ</b> で、<b>未実施を表す値がありません。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q51_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(80 * mm))
    story.append(qhead(
        "Q5-2. task_area / clean_area / clean_ratio は 0 でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q52_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "あわせて、お願いがございます（収集処理の変更を伴います）", S_H4))
    story.append(Paragraph(
        "<b>もし A（3列とも 0）でしたら、task_area にだけは"
        "「予定していた清掃対象面積」を入れていただけないでしょうか。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>難しい場合は、現状のままで結構です。</b>"
        "<b>当方は clean_error を見て未実施を判別する実装にしておりますので、"
        "「0 が入る」と確定していれば、それに合わせて実装します。</b>"
        "<b>値が分かることが第一で、変更は二の次です。</b>", S_BODY))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("5-2. なぜお伺いするか", S_H3))
    story.append(Paragraph(
        "<b>清掃完了率が面積ベース（Σ清掃実施面積 ÷ Σ清掃対象面積）"
        "だからです。</b>", S_BODY))
    story.append(Paragraph(
        "<b>未実施の行の task_area が 0 だと、"
        "その清掃が分母から丸ごと抜けます。</b>"
        "結果として <b>「実施できなかった日が多い施設ほど、"
        "完了率が高く出る」</b> という逆転が起きます。"
        "予定面積が入っていれば、分母に残り、実態に近い率になります。",
        S_BODY))
    story.append(Paragraph(
        "また、clean_ratio についても理由がございます。", S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(bull([
        "<b>当方は clean_ratio = 0 を「出発はしたが、"
        "清掃がほとんど進まなかった」として×判定で表示しております。</b>",
        "<b>つまり clean_ratio だけを見ても、"
        "「0% しか清掃できなかった」と「そもそも実施していない」を"
        "区別できません。</b>",
        "現在は clean_error で区別できておりますので、支障はありません。"
        "<b>ただし、未実施の行に何が入るかを取り違えると、"
        "この区別が崩れます。</b>",
    ]))

    # ---------------------------------------------------------- 6. clean_error
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "6. clean_error が 2 / 3 / 4 のときの扱いについて", S_H2))

    story.append(Paragraph("6-1. 現状の理解", S_H3))
    story.append(Paragraph(
        "clean_status_histories.clean_error には、"
        "次の値があると伺っております。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "0: 正常",
        "1: 異常(清掃未実施)",
        "2: 異常(セキュリティ有効)",
        "3: 異常(D-S制御盤異常)",
        "4: 異常(清掃ロボット異常)",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>当方は現在、「1 以外は未実施ではない（＝出発した）」として"
        "扱っております。</b>"
        "<b>そのうえで、2 / 3 / 4 の行について"
        "次の点が分かりませんでした。</b>", S_BODY))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph("6-2. お伺いしたいこと（6問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q6-1. clean_error が 2 / 3 / 4 のとき、robot_error_reports にも"
        "行は作られますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q61_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(60 * mm))
    story.append(qhead(
        "Q6-2. 作られる場合、その行の clean_timetable_id は"
        "埋まりますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q62_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(quote(
        "埋まる場合、それが「事前チェックの行」と「本清掃の行」の"
        "どちらを指すかは、<b>7章の Q7-1</b> でお伺いしております。", CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(60 * mm))
    story.append(qhead(
        "Q6-3. 2 / 3 / 4 の行の clean_ratio と面積"
        "（task_area / clean_area）には何が入りますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q63_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(45 * mm))
    story.append(qhead(
        "Q6-4. 1回の清掃で複数の異常が同時に起きた場合、clean_error は"
        "どれになりますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>clean_error は1カラムですので、1つの値しか入りません。</b>"
        "<b>優先順位の規則がございましたらお教えください</b>"
        "（例:「1 が最優先」「後から起きたもので上書き」など）。", S_BODY))
    story.append(Spacer(1, 4))

    story.append(CondPageBreak(55 * mm))
    story.append(qhead(
        "Q6-5. clean_error に 5 以上の値が入る可能性は"
        "ございますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "unsigned tinyint（0〜255）ですので、"
        "将来の拡張余地があると理解しております。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q65_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(65 * mm))
    story.append(qhead(
        "Q6-6. clean_statuses.clean_error と "
        "clean_status_histories.clean_error の使い分けをお教えください", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>同じコード表ですが、定義書の論理名が異なっております</b>"
        "（clean_statuses 側が「エラー」、"
        "clean_status_histories 側が「結果」）。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q66_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(quote(
        "<b>補足</b>: <b>当方は現在、"
        "履歴側（clean_status_histories）だけを読んでおります。</b>"
        "<b>これで差し支えないかもあわせてお教えいただけますと助かります。</b>",
        CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph("6-3. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>当方のアラート画面は、robot_error_reports を元に"
        "作っております。</b>",
        "<b>したがって Q6-1 が C（作られない）の場合、"
        "履歴側にだけ異常が記録され、"
        "その清掃はアラートに一切上がってきません。</b>"
        "<b>運用担当は、清掃詳細ログを1行ずつ見に行かないと"
        "気づけないことになります。</b>"
        "<b>この場合は、当方が履歴側も見てアラートを組み立てる実装に"
        "変えます。</b>",
        "<b>逆に A（両方に作られる）の場合は、"
        "同じ出来事を二重に数えないよう気をつける必要があります。</b>"
        "<b>Q6-2 で clean_timetable_id が埋まるのであれば、"
        "当方で突き合わせて重複を除けます。</b>",
        "<b>Q6-3 は、清掃詳細ログの完了率と◎○△×判定の意味が変わります。</b>"
        "<b>B（0が入る）ですと、途中まで清掃できた清掃も一律 × になります。</b>",
        "<b>Q6-5 は、当方が「1 以外は出発した」という実装にしているためです。</b>"
        "<b>5 以上に「未実施」を意味する値が増えると、"
        "その清掃が出発した扱いになってしまいます。</b>",
    ]))
    story.append(Spacer(1, 2))
    story.append(Paragraph(
        "<b>いずれも当方で対応できます。どちらなのかが分かれば十分です。</b>",
        S_BODY))

    # ---------------------------------------------------------- 7. 紐づく行
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "7. robot_error_reports.clean_timetable_id は、"
        "事前チェックの行と本清掃の行のどちらを指しますでしょうか", S_H2))

    story.append(Paragraph("7-1. 現状の理解", S_H3))
    story.append(Paragraph(
        "clean_timetables には、<b>本清掃の行</b> と "
        "<b>その事前チェックの行（precheck が 1 / 2、"
        "parent_id で本清掃を指す）</b> があると理解しております。", S_BODY))
    story.append(Paragraph(
        "<b>当方は「robot_error_reports.clean_timetable_id は"
        "事前チェックの行を指す」という前提で実装しております。</b>", S_BODY))

    story.append(Paragraph("7-2. お伺いしたいこと（1問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q7-1. robot_error_reports.clean_timetable_id は"
        "どちらの行を指しますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q71_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>あわせて、清掃中（本清掃の実行中）に発生したエラーは、"
        "どちらの行に紐づくかをお教えください。</b>", S_BODY))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("7-3. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>レポート画面のエラー発生率の分子を、"
        "clean_timetable_id から本清掃の行へたどり直して数えております。</b>",
        "<b>このため、本清掃の行に直接付く運用ですと、"
        "たどり直しが空振りし、そのエラーが1件も数えられません。</b>",
        "<b>結果としてエラー発生率が 0% と表示されますが、"
        "例外もエラーログも出ません。</b>"
        "<b>「エラーが無い」と読める画面になってしまうため、"
        "お伺いしております。</b>",
    ]))

    # ---------------------------------------------------------- 8. 制御盤履歴
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "8. control_error_histories は、実際に使われるテーブルでしょうか",
        S_H2))

    story.append(Paragraph("8-1. 資料のあいだで食い違いがございます", S_H3))
    story.append(Paragraph(
        "<b>当方の受領資料のなかで、このテーブルの位置づけが"
        "食い違っております。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(CEH_SRC_ROWS, [46 * mm, W - 46 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>当方は「使われる」と解釈し、D-Secアラート画面（F-07）を"
        "このテーブルから作っております。</b>", S_BODY))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("8-2. お伺いしたいこと（1問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q8-1. control_error_histories には、"
        "実際にデータが入りますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q81_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(50 * mm))
    story.append(Paragraph("8-3. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>B（未使用）の場合、D-Secアラート画面が常に空になります。</b>"
        "<b>画面としては表示されるものの、1件も出ないという状態です。</b>"
        "<b>この場合は、当方が別のテーブルから組み立て直す"
        "作り替えが必要になります。</b>",
        "<b>実データに接続すれば件数は当方で数えられますが、"
        "「たまたま0件」なのか「そもそも入らない設計」なのかは、"
        "当方には判断できません。</b>",
        "<b>このテーブルについては、これまで一度も"
        "お伺いしておりませんでした。恐れ入ります。</b>",
    ]))

    # ---------------------------------------------------------- 9. 軽い確認
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph("9. 軽い確認を2件、お願いいたします", S_H2))
    story.append(Paragraph(
        "<b>どちらも「無ければ無いで結構です」という性質のものです。</b>",
        S_BODY))

    story.append(Paragraph("9-1. お伺いしたいこと（2問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q9-1. 事前チェック時点のバッテリー残量を保持するご予定は"
        "ございますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "robots.battery は <b>現在値のみ</b> で、"
        "履歴が無いと理解しております。"
        "このため、<b>事前チェック（60分前 / 30分前）の時点で"
        "バッテリーが何%だったか</b> を、"
        "当方から取得する手段がございません。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q91_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "<b>A（予定が無い）でしたら、当方は事前チェック画面のバッテリー欄を"
        "恒久的に空欄として確定いたします。</b>"
        "<b>その確認だけができれば十分です。追加のご対応は不要です。</b>",
        S_BODY))

    story.append(CondPageBreak(70 * mm))
    story.append(qhead(
        "Q9-2. Pudu Cloud から、清掃エリア別の完了率は"
        "取得できますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "要件定義の段階から未解決のまま持ち越している項目です。"
        "<b>清掃1回のなかで、エリアごとに何%清掃できたか</b> という"
        "粒度のデータです。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q92_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "<b>B または C でしたら、当方はこの機能を Phase B 送りとする"
        "判断材料にいたします。</b>"
        "<b>現在は、該当のAPIを「未実装」を返す形で仮置きしております。</b>",
        S_BODY))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("9-2. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>どちらも「無い」と分かれば、当方は画面から外す・"
        "Phase B に送る、という判断ができます。</b>",
        "<b>分からないままですと、実装するかどうかを決められず、"
        "仮置きのまま残り続けます。</b>",
        "<b>貴社に何かをご対応いただきたいという趣旨ではございません。</b>",
    ]))

    # ---------------------------------------------------------- 10. 更新用アカウント
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "10. 継続のお願い: DB更新用アカウントの発行"
        "（第5回・第6回からの継続）", S_H2))

    story.append(Paragraph(
        "10-1. お願いしている内容（既に範囲は合意済みです）", S_H3))
    story.append(Paragraph(
        "<b>下記の2テーブル・4カラムのみ UPDATE 可、という"
        "アカウントの発行をお願いしております。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(UPDATE_ACCT_ROWS, [56 * mm, W - 56 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>範囲についてはすでに合意いただいており、"
        "変更のお願いはございません。</b>"
        "<b>第6回では「後で必要なテーブルに更新権限を付与する」との"
        "ご連絡をいただいております。</b>", S_BODY))

    story.append(CondPageBreak(50 * mm))
    story.append(Paragraph("10-2. お伺いしたいこと（1問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q10-1. 発行の時期の目安をお教えいただけませんでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>急ぎではございません。</b>"
        "<b>「いつ頃になりそうか」という目安だけで結構です。</b>"
        "<b>未定でしたら「未定」とお知らせいただくだけでも助かります。</b>",
        S_BODY))

    story.append(CondPageBreak(50 * mm))
    story.append(Paragraph("10-3. なぜお伺いするか", S_H3))
    story.extend(bull([
        "<b>2026-09-03 からお願いしている件で、"
        "当方の外部待ちのなかでいちばん古いものです。</b>"
        "<b>第7回では書き漏らしてしまい、失礼いたしました。</b>",
        "<b>当方の作業は止まっておりません。</b>"
        "<b>アラート（F-03）と D-Secアラート（F-07）の"
        "「対応状況を更新する」機能は、"
        "画面まで実装済みで、書き込み先を差し替えるだけの状態です。</b>",
        "<b>ただし、対応状況を更新するAPIだけは、"
        "書き込み先が無いため未実装のままとなっております。</b>"
        "<b>時期の目安が分かれば、そこに合わせて段取りいたします。</b>",
    ]))

    # ---------------------------------------------------------- おわりに
    # 「まずは、この3つだけでも」の表（3行・セルが厚い）まで割れないようにする
    story.append(CondPageBreak(100 * mm))
    story.append(Paragraph("おわりに", S_H2))
    story.append(Paragraph(
        "<b>本書では10件をお伺いしております。"
        "数が多くなってしまい、恐れ入ります。</b>", S_BODY))

    story.append(Paragraph("まずは、この3つだけでも", S_H3))
    story.append(Paragraph(
        "<b>お忙しいと存じますので、優先度の高い3つを先に挙げます。</b>"
        "<b>この3つにご回答いただければ、当方は先へ進めます。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(TOP3_ROWS, [10 * mm, 54 * mm, W - 64 * mm],
                       [S_CODE, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("残りの優先度", S_H3))
    story.append(Spacer(1, 3))
    story.append(table(REST_ROWS, [10 * mm, 68 * mm, W - 78 * mm],
                       [S_CODE, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(50 * mm))
    story.append(Paragraph("第7回の3件について", S_H3))
    story.append(Paragraph(
        "<b>第7回でお願いしている3件"
        "（接続先 / precheck の値域 / DDL の未反映4件）は、"
        "本書によって取り下げるものではありません。</b>", S_BODY))
    story.append(Paragraph(
        "<b>とりわけ接続先は、本書の10件を含めた全体を通じて最優先です。</b>"
        "<b>接続できれば、当方で数えられるものは当方で確認しますので、"
        "その分だけ貴社への照会が減ります。</b>", S_BODY))
    story.append(Paragraph(
        "（本書の1・2・4・5・6・8 も、接続できていれば当方で実データを見て "
        "<b>部分的には</b> 自力で確認できたものです。"
        "ただし「その値が何を意味するか」"
        "「たまたま0件なのか、そもそも入らないのか」は"
        "当方には判断できませんので、"
        "いずれにせよお伺いすることになります。）", S_BODY))
    story.append(Spacer(1, 2))
    story.append(Paragraph(
        "お忙しいところ恐れ入りますが、よろしくお願いいたします。", S_BODY))

    # ---------------------------------------------------------- ご回答欄
    # ご回答欄は印刷してご記入いただく想定なので、必ず新しいページから始める
    story.append(PageBreak())
    story.append(Paragraph("ご回答欄", S_TITLE))
    story.append(Paragraph(
        "<b>この表に一言だけご記入いただく形でも、"
        "下の自由記入欄にまとめてご記入いただく形でも結構です。</b>"
        "<b>上位3つ（Q1-1 / Q2-3 / Q4-1〜Q4-4）だけでも助かります。</b>",
        S_LEAD))
    story.append(Spacer(1, 4))
    story.append(form_table(ANSWER_ROWS, [18 * mm, 96 * mm, W - 114 * mm],
                            [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("自由記入欄", S_H3))
    story.append(Spacer(1, 3))
    story.append(blank_box(W, 42 * mm))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(55 * mm))
    story.append(Paragraph("第7回の3件（未回答分）", S_H3))
    story.append(Spacer(1, 3))
    story.append(form_table(ANSWER7_ROWS, [W - 60 * mm, 60 * mm],
                            [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph("引き続きよろしくお願いします。", S_NOTE))

    doc.build(story)
    print("生成:", OUT)


if __name__ == "__main__":
    build()
