# -*- coding: utf-8 -*-
"""
TeamWorks 宛 確認事項(第9回)を PDF にする。
第1回〜第8回(make_ddl_review_pdf.py / make_teamworks_qa2_pdf.py 〜
make_teamworks_qa8_pdf.py)の体裁をそのまま踏襲した続編。
A4 縦 / reportlab / 日本語フォント埋め込み / フッター罫線とページ番号。
フォントは BIZ UDGothic(日本語フォント)を埋め込む。
中国語向けフォント(SimSun / Microsoft YaHei / Noto Sans SC / STSong 等)は
漢字の字形が異なるため使わない。フォールバックにも混ぜないこと(CLAUDE.md §0-3)。
BIZ UDGothic は等幅(半角は 0.5em 固定)なので、コードの枠にもこのフォントを使う。
コードブロック内の空白は に置換して桁を保持する。
原稿は docs/handoff/TeamWorks確認事項_第9回_20260915.md。
文言・件数・表の内容は原稿から変更していない。
原稿の ★(星)は PDF には出さず、太字で表現する(第2回からの作法)。
★2026-09-16 の改訂★
原稿に12章「DB接続経路について — お願い3件・確認6問」が加わった
(設問 20問 → 26問 / お願い 10件 → 13件)。
初版(2026-09-15)の末尾にあった「参考: テスト環境について(申し送り)」は、
★12章に吸収したので削除した★(重複させない)。
このスクリプトは .md を読まず本文を転記しているため、原稿を直したら
★必ずこちらも直すこと★。直さないと PDF だけ古いまま残る。
★2026-09-17(2回目)の改訂★
★★テスト環境のDBに入っているデータは、1行残らず当方(DFA)が投入した
検証用のダミーだと判明した★★(調査は docs/handoff/テストDBのデータ出所_20260917.md)。
★先方由来なのは「器」=スキーマ・索引・型・サーバー変数・権限だけ★なので、
★「行の中身」を根拠にしていた記述を原稿が全部取り下げた★。追随させたのは次のとおり。
1. ★文書ヘッダの改訂点ブロックを2つにした★
(新設の「2026-09-17(2回目)」+既存を「(1回目)」に降格)。
2. ★はじめに に「おことわり — テスト環境のデータは当方が入れたものでした」
(2行の表つき)と「本書の構成」を新設★。中心は「11章・12章」→
★「12章。あわせて3章・4章・6章」★。
3. 構成表の 2 / 5 / 6 / 10 / 11 章を差し替え(5章・10章は種別「ご報告」・
優先度「—」)。★件数は 質問25問 / お願い5件 / 参考情報1章★。
4. 1章 … 引用ブロック3つ(行数はダミー由来で、見るべきは type/possible_keys/key
という構造)+1-4・1-5 の注記。計算過程の「1.45回」は★実測→当方の置き値★へ。
5. ★2章 … 実測表(6列4行)を削除★。2-1 を全面差し替え、★Q2-2 を削除★、
Q2-1 の選択肢を3行→2行。
6. ★5章 … 章ごと差し替え(設問・表なし)★。
7. 6章 … 「なぜ「清掃実施回数」が要るのか(Q6-2 の理由)」を新設。
8. 7章 … 7-1 のコード枠を削除、Q7-4 のコード枠を2クエリに減らし引用ブロックを追加。
9. 8章 … 8-1 の表を差し替え、★Q8-3 を「固定値」→「下限・上限・変更の頻度」★へ。
10. 9章 … Q9-1 の末尾2行を差し替え。
11. ★10章 … 章ごと差し替え(すでに発行済み。SHOW GRANTS のコード枠つき)★。
★IDENTIFIED BY PASSWORD のハッシュは絶対に載せない★(原稿も伏せてある)。
12. ★11章 … 章タイトルを「清掃の開始時刻と、リアルタイム性について」に。
Q11-1 は「取り下げ」ではなく★実装の確認★として作り直し、delay 集計表と
SQL枠を削除、★Q11-4 を新設★★。
★★Q11-1 は生きた設問である(質問25問に数える)。「取り下げました」
「解決しましたので取り下げ」「残り3問に縮小」と書かないこと★★ —
★初版の改訂ではそう書いてしまい、同じ節に生きた Q11-1 がある矛盾になった。
2026-09-17 に本文側が直し、こちらも追随している。
点検スクリプトの 12. の FORBIDDEN が旧文言の残存を見る★
13. 末尾に「取り下げた項目」と★「取り下げではなく、お伺いする内容を改めた項目」★の
2段落を追加(★Q11-1 は後者★)。
★12章は1文字も変えていない。★
★点検スクリプトの 12. の FORBIDDEN が、取り下げた語(2章の実測表・Q2-2・Q5-1・
旧Q11-1・10章の依頼文)の残存を見る。文言を戻す場合はそちらも直すこと。★
★2026-09-17(1回目)の改訂★
原稿が2点の新情報で書き換わった(★索引は TeamWorks が対応中★ /
★現状のロボット台数は30台未満★)。追随させたのは次のとおり。
1. ★1章を「ご依頼(7件)・最優先」から「索引について(参考情報)」へ格下げ★。
1-0(本章の位置づけ)を新設し、1-2〜1-5 の見出しから「ご依頼」を外し、
1-6 を「①〜⑦の一覧(突き合わせ用)」の表に差し替えた。
2. ★規模の前提を「施設200件」から「ロボット30台」に置き直した★
(1-2 の走査行数表 2年=約193万行 / 1-4 / 2-4 の桁あふれ 約9,100㎡)。
計算過程のコード枠を 1-2・1-4・2-4 に足している。
3. ★8章を「本番規模の見積もり」から「制御盤の報告間隔」に差し替え★。
Q8-1・Q8-2 を削除(設問 28問 → ★26問★ / お願い 13件 → ★6件★)。
★番号は詰めず Q8-3 のまま★(原稿の断り書きどおり)。
4. 文書ヘッダに「2026-09-17 の改訂点」の引用ブロックを足し、はじめに・
構成表・おわりにを差し替えた。
★12章は1文字も変えていない。★
★点検スクリプト(check_teamworks_qa9_pdf.py)の 12. が、取り下げた語
(「索引のご依頼」「ご依頼①②」「本番規模の見積もり」等)が PDF に
残っていないかを見る。文言を戻す場合はそちらも直すこと。★
★第8回から足したもの★
なし。部品(st / table / form_table / code / quote / callout / qhead /
blank_box / bull / on_page)は make_teamworks_qa8_pdf.py をそのまま流用し、
表データと build() の本文だけ差し替えている。
★原稿との差分(意図的なもの。文言は足していない)★
1. 巻末に「ご回答欄」を付けた。第2回〜第8回と同じ作法。
第9回の原稿には節としては無いが、設問の採番と要約だけを転記したもので、
新しい主張は書いていない。
2. 原稿の「## 2-2.」直下は Q2-1 / Q2-3 の2問で、見出しの「(2問)」と一致する
(★2026-09-17 の2回目の改訂で Q2-2 を取り下げた。番号は詰めていない★)。
3. 原稿の7章は Q7-5 が Q7-4 より前に置かれている。★原稿の順序のまま出す★。
★2026-09-17 の改訂で、Q7-5 の直前にその断り書き(引用ブロック)が
原稿に加わった。順序を入れ替えるのではなく、断り書きごと出すこと★
4. 原稿のはじめにの構成表は 1〜12 の12行。
「次の12部構成です。」という本文も含め、原稿のまま出す。
★「N部構成」の N・構成表の行数・本文の章数の3つは必ず一致させること★
(点検スクリプトの 7. がこの3つを別々に数えて突き合わせる)。
★列幅の決め方(第7回・第8回と同じ)★
wordWrap="CJK" は ASCII の語中でも改行する。そのため長い識別子は
(1) 行頭に来るよう <br/> で明示的に改行してから置く
(2) 列の内寸(列幅 - 左右パディング 12pt)が識別子の幅
(BIZ UDGothic は半角 0.5em なので フォントサイズ × 文字数 ÷ 2)
を上回るように列幅を決める
の2点で語中改行を避ける。
第9回でいちばん長い識別子は
clean_status_histories_clean_timetable_id_index (47字)
robot_error_reports_clean_timetable_id_index (43字)
の2本で、いずれも ALTER TABLE 文(コード枠)の中にしか出てこない。
コード枠の内寸は 168mm - 14pt = 462pt、8.2pt での 47字は 192.7pt なので余裕がある。
本文・表に出る長い識別子は
customers_group_id_foreign (26字 → 8.8pt で 114.4pt = 40.4mm)
control_reports_control_id_foreign(34字 → 9.5pt で 161.5pt = 57.0mm)
facilities.customer_id (22字 → 8.8pt で 96.8pt = 34.2mm)
clean_status_histories (22字)
で、それぞれ 7-4 の選択肢表の「内容」列 68mm、1-5 の本文(<br/> で行頭に落とす)、
7-5 の解釈表の「DBでの表現」列 90mm に収めている。
"""
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_確認事項_第9回_20260915.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) # 設問(Q2-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 を足しただけ。空セルは で高さを持たせる。"""
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 " ", 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):
"""コードを等幅フォントの枠に入れる。空白は にして桁を保つ。"""
esc = []
for ln in lines:
ln = ln.replace("&", "&").replace("<", "<").replace(">", ">")
esc.append(ln.replace(" ", " "))
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):
"""設問(Q2-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(" ", 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]
def numlist(items):
"""「1. 」形式の番号付き箇条書き。"""
return [Paragraph("%d. %s" % (i + 1, t), S_LIST)
for i, t in enumerate(items)]
# ---------------------------------------------------------------- 表データ
# はじめに: おことわり(2026-09-17 の2回目の改訂で新設)
DISCLAIM_ROWS = [
["種別", "例", "本書での扱い"],
["<b>構造から分かること</b>",
"DDL / SHOW CREATE TABLE / 索引の有無 / possible_keys = null / "
"カラムの有無・型 / サーバー変数 / 権限",
"<b>そのまま根拠として記載しております</b>"],
["<b>行の中身から<br/>読み取っていたこと</b>",
"行数・値の分布・「全件が同じ値」「0行である」等",
"<b>取り下げるか、「当方の検証用データでの観測です」と"
"明記しております</b>"],
]
# はじめに: 12部構成(原稿の表のまま)
SUMMARY_ROWS = [
["#", "内容", "種別", "優先度"],
["<b>1. 索引について</b>",
"<b>ご対応中と伺いました。当方の EXPLAIN 実測と、"
"当方が見ていた列の一覧です</b>",
"<b>参考情報</b>", "<b>—</b>"],
["<b>2. サマリの面積は期間内の合計でしょうか</b>",
"<b>仕様の確認のみに縮めました</b>", "質問", "<b>高</b>"],
["<b>3. 第8回 Q4 の残り<br/>(サマリの率・分子・分母)</b>",
"<b>再掲</b>", "質問", "高"],
["<b>4. サマリの error_count の集計範囲</b>",
"<b>当期と確定期間で定義が食い違う恐れ</b>", "質問", "高"],
["<b>5. 面積の単位</b>",
"<b>弊社で㎡と確定しました。設問は取り下げです</b>",
"<b>ご報告</b>", "<b>—</b>"],
["<b>6. グループ単位のサマリ</b>",
"<b>group_summaries が存在しません</b>", "質問", "<b>高</b>"],
["<b>7. グループ・顧客マスタのデータ投入</b>",
"<b>画面に分類を出すために必要です</b>", "<b>お願い</b>", "中"],
["<b>8. 制御盤の報告間隔</b>",
"<b>control_reports の行数を決めます。台数は当方で把握しました</b>",
"質問", "中"],
["<b>9. 軽い確認2件 + DDL未反映2件</b>",
"<b>DDLは4回目のお願いです</b>", "質問", "低"],
["<b>10. DB更新用アカウント</b>",
"<b>発行済みでした。ご依頼を取り下げます</b>",
"<b>ご報告</b>", "<b>—</b>"],
["<b>11. 清掃の開始時刻と<br/>リアルタイム性</b>",
"<b>Q11-1 は「実装の確認」に改めました。Q11-4 を新設(4問)</b>",
"質問", "中"],
["<b>12. DB接続経路についての<br/>お願い3件・確認6問</b>",
"<b>実測つき。期限は「ステージングに実データが入る前」です</b>",
"<b>お願い+質問</b>", "<b>高</b>"],
]
# 1-2. 影響の大きさ(★2026-09-17 にロボット30台の前提で置き直した★)
SCAN_ROWS = [
["表示期間", "塊の数",
"走査行数<br/>(clean_status_histories・<b>ロボット30台で外挿</b>)"],
["今日1日", "1", "約 4.4万行"],
["<b>2年(731日)</b>", "<b>44</b>", "<b>約 193万行</b>"],
]
# 1-6. ①〜⑦の一覧(突き合わせ用。2026-09-17 に新設)
# ★列幅★ control_error_histories(24字 = 8.8pt で 105.6pt = 37.3mm)が
# 語中で割れないよう「テーブル」列の内寸(列幅 - 4.2mm)でこれを上回るようにする。
INDEX_LIST_ROWS = [
["#", "テーブル", "当方が見ていた列<br/>(順序も含みます)",
"2026-09-13 時点の実測"],
["<b>①</b>", "clean_status_histories", "clean_timetable_id",
"<b>索引なし(possible_keys = null)</b>"],
["<b>②</b>", "robot_error_reports", "clean_timetable_id",
"<b>索引なし(possible_keys = null)</b>"],
["<b>③</b>", "clean_timetables", "(precheck, start_at)",
"単一列索引はあるが実行計画で未使用"],
["<b>④</b>", "clean_timetables", "(facility_id, start_at)", "同上"],
["<b>⑤</b>", "clean_status_histories", "started_at", "<b>索引なし</b>"],
["<b>⑥</b>", "control_reports", "(control_id, reported_at)",
"(control_id) のみ。<br/>MAX(reported_at) がフルスキャン"],
["<b>⑦</b>", "control_error_histories", "occurred_at", "<b>索引なし</b>"],
]
# 1-5. ⑥の根拠
MAXCOL_ROWS = [
["MAX() の対象", "索引に含まれるか", "type", "rows", "Extra"],
["reported_at(現行)", "<b>含まれない</b>",
"index(<b>フルスキャン</b>)", "14", "Using where"],
["id(参考の実験)", "<b>含まれる</b>", "<b>range</b>", "<b>3</b>",
"<b>Using index for group-by</b>"],
]
# 2-2. Q2-1
# ★2026-09-17(2回目)の改訂で、2-1 の実測表(6列4行)は削除した。
# テストDBの行は当方が入れたダミーだったため(原稿 2-1)。
# 選択肢の C(「まだバッチが動いていない」)も同時に消えて A / B の2行になった★
Q21_ROWS = [
["選択肢", "内容"],
["<b>A</b>",
"<b>期間内の合計を書く</b>"
"(当方はサマリをそのまま足し合わせて総面積を出します)"],
["B", "<b>合計ではない</b>(1回ぶんの代表値・平均など。その場合、定義をお教えください)"],
]
Q23_ROWS = [
["選択肢", "内容"],
["<b>A</b>", "<b>必ず1行</b>(清掃1回につき1行)"],
["B", "<b>複数行できる</b>(清掃前 → 清掃中 → 清掃終了 のように状態ごとに積む)"],
]
# 2-4. 年次サマリの桁あふれ
OVERFLOW_ROWS = [
["前提", "上限に達する条件"],
["<b>ロボット30台・年365日</b>",
"<b>1台1日あたり 約9,100㎡ を超えると、年次行があふれます</b>"],
]
# 4. error_count
Q41_ROWS = [
["選択肢", "内容"],
["A", "<b>除外している</b>(本清掃 precheck = 0 のみを数えている)"],
["B", "<b>除外していない</b>(事前チェックのエラーも数えている)"],
]
# 6-1
Q61_ROWS = [
["選択肢", "内容"],
["<b>A</b>",
"<b>当方が customer_summaries の複数行を合算する</b>(テーブル追加は不要)"],
["B", "<b>group_summaries を追加してバッチで集計する</b>"],
["C", "<b>グループ単位の集計は想定していない</b>"],
]
# 6-2
RATIO_ROWS = [
["率", "計算式(当方の定義)", "サマリの分子", "サマリの分母", "合算できるか"],
["清掃完了率<br/>clean_ratio", "Σ清掃面積 ÷ Σ対象面積", "clean_area",
"task_area", "<b>できる</b>"],
["出発成功率<br/>launch_ratio", "定刻出発回数 ÷ 予定回数", "launch_count",
"task_count ?", "<b>式が未確認</b>"],
["<b>稼働率<br/>operation_ratio</b>", "清掃<b>実施</b>回数 ÷ 予定回数",
"<b>該当カラムが無い</b>", "task_count ?", "<b>できない</b>"],
["<b>エラー発生率<br/>error_ratio</b>",
"エラー発生回数 ÷ 清掃<b>実施</b>回数", "error_count",
"<b>該当カラムが無い</b>", "<b>できない</b>"],
]
# 6-2. なぜ「清掃実施回数」が要るのか(2026-09-17 の2回目の改訂で新設)
TASKCOUNT_ROWS = [
["task_count が何であれば", "困ること"],
["<b>予定回数</b>(当方の理解)",
"<b>実施回数が無い</b> → "
"<b>稼働率の分子・エラー発生率の分母が出せません</b>"],
["<b>実施回数</b>",
"<b>予定回数が無い</b> → <b>稼働率の分母が出せません</b>"],
]
Q62_ROWS = [
["選択肢", "内容"],
["<b>A</b>",
"<b>追加できる</b>(customer_summaries / facility_summaries に1カラムずつ)"],
["B", "<b>すでに既存のカラムで表現されている</b>(どれか教えてください)"],
["C", "<b>追加は難しい</b>"],
]
# 7-2. Q7-1
Q71_ROWS = [
["選択肢", "例"],
["<b>A</b>",
"<b>屋号</b>(ドン・キホーテ / MEGAドン・キホーテ / アピタ / ピアゴ …)"],
["B", "<b>法人</b>(株式会社ドン・キホーテ / 株式会社ユニー)"],
]
# 7-2. Q7-5
LAYER_ROWS = [
["解釈", "空くのは", "例", "DBでの表現"],
["<b>①</b>", "<b>真ん中(屋号)</b>",
"店舗がホールディングスの直下にぶら下がる",
"<b>表現できません</b> — facilities.customer_id は NOT NULL で "
"customers を指すため、中間を飛ばせません"],
["<b>②</b>", "<b>上(ホールディングス)</b>",
"屋号はあるが、属する持株会社が無い",
"customers.group_id = 0(= Q7-4 の件)"],
]
# 7-2. Q7-4
Q74_ROWS = [
["選択肢", "内容", "備考"],
["<b>A</b>", "<b>groups に id = 0 の番人行を1件入れる</b>",
"<b>INSERT には NO_AUTO_VALUE_ON_ZERO が必要になります</b>"],
["B", "<b>外部キー制約 customers_group_id_foreign を外す</b>",
"センチネル方式と両立します"],
["C", "<b>group_id を nullable にする</b>",
"<b>索引効率を理由にセンチネル方式を採られた経緯があるため、"
"蒸し返しになります</b>"],
["D", "<b>グループなしの顧客は存在しない</b>(必ずどこかのグループに属する)",
"<b>この場合、当方は「グループなし」の表示を作りません</b>"],
]
# 8. 報告間隔
INTERVAL_ROWS = [
["報告間隔", "2年間の行数(<b>制御盤11台と置いた試算</b>)"],
["60秒(定義書の例)", "約 1,100万行"],
["<b>10秒</b>(第2回のご回答の例)", "<b>約 7,000万行</b>"],
]
# 8. Q8-3(2026-09-17 の2回目の改訂で「固定値」→「下限・上限・変更の頻度」)
Q83_ROWS = [
["#", "伺いたいこと", "例"],
["<b>a</b>", "<b>運用上の下限(最短の報告間隔)</b>",
"「10秒より短くはしない」など"],
["<b>b</b>", "<b>運用上の上限(最長の報告間隔)</b>",
"「長くても60秒」など"],
["<b>c</b>", "<b>変更の頻度</b>",
"「初期設定から基本的に変えない」/"
"「制御盤ごとに個別に決める」/「運用中に随時変える」など"],
]
# 8-1. 当方の把握(2026-09-17 に新設。旧 Q8-1 / Q8-2 の差し替え)
# ★2回目の改訂で「テスト環境の controls は11行」等の行数根拠を外した
# (テストDBの行は当方が入れたダミーだったため)★
SCALE_ROWS = [
["項目", "当方の把握", "出典"],
["<b>ロボット台数</b>", "<b>現状 30台未満</b>",
"<b>弊社(DFA)の把握。2026-09-17</b>"],
["D-S制御盤の台数",
"<b>未確定</b>(試算では <b>11台</b> と置いております)",
"<b>弊社で確定させます</b>"],
["施設数", "<b>未確定</b>", "<b>弊社で確定させます</b>"],
]
# 9. DDL 未反映
DDL_ROWS = [
["#", "内容"],
["1", "clean_status_histories.id の AUTO_INCREMENT"],
["2", "security_status_histories.reported_at の DEFAULT"],
]
# 11. Q11-1(2026-09-17 の2回目の改訂で作り直した)
# ★旧 Q11-1「実開始時刻はどこにありますか」(A / B / C の選択肢表)と、
# 11-2 の delay 集計表・SQL 枠は削除した。started_at を使う形で解決したため★
Q111_ROWS = [
["ご回答", "当方の動き"],
["<b>A. その理解で問題ない</b>",
"<b>このまま実装します</b>(<b>ご回答はこの一言で結構です</b>)"],
["<b>B. started_at には予定時刻が入る</b>",
"<b>実装を止め、実開始時刻の所在を改めてご相談します</b>"],
]
# 11. Q11-4(2026-09-17 の2回目の改訂で新設)
Q114_ROWS = [
["#", "伺いたいこと"],
["<b>a</b>", "<b>そのテーブル・カラム名</b>"],
["<b>b</b>",
"<b>おおよその更新間隔</b>"
"(「数秒ごと」「1分ごと」など、おおよそで構いません)"],
["<b>c</b>", "<b>履歴として積まれるのか、上書きされるのか</b>"],
]
# 12-1. 期限の対比
DEADLINE_ROWS = [
["時点", "状態", "作り直しのコスト"],
["<b>いま<br/>(検証用データのみ)</b>",
"<b>実データは入っておりません</b>",
"<b>ほぼゼロ</b>。アカウントを作り直しても、失うものがありません"],
["<b>ステージングに<br/>実データが入った後</b>",
"<b>平文の経路を通った資格情報を前提に、"
"運用が始まってしまいます</b>",
"<b>「作り直す」しか手がなくなります</b>"],
]
# 12-2. 実測のメタ情報(いつ・誰が・どう測ったか)
MEASURE_META_ROWS = [
["項目", "内容"],
["<b>対象</b>",
"<b>テスト環境 133.125.238.144:13306 / DB clean_watch</b>"
"(本書1-1に記載のものと同じサーバーです)"],
["<b>サーバーの<br/>バージョン</b>",
"<b>VERSION() = 10.11.19-MariaDB-ubu2204</b>"],
["<b>測った人</b>", "<b>DFA(本プロジェクトの作業者)</b>"],
["<b>測った日</b>",
"<b>2026-09-13</b>(<b>2026-09-15 に本書の初版へ転記しました</b>)"],
["<b>測り方</b>",
"<b>貴社より受領した参照用アカウント</b>(SELECT のみ)で"
"テスト環境に接続し、<b>サーバー変数を照会した</b>"],
]
# 12-2. 実測値。★列幅は require_secure_transport(24字 = 8.8pt で 105.6pt
# = 37.3mm)が語中で割れないよう、内寸(列幅 - 左右パディング 12pt = 4.2mm)で
# これを上回るように 46mm にしている★
TESTENV_ROWS = [
["変数/設定", "実測値", "意味"],
["have_ssl", "<b>DISABLED</b>",
"<b>サーバーが TLS を提供していません</b>"],
["require_secure_transport", "<b>OFF</b>",
"<b>暗号化されていない接続を、サーバーが受け付けます</b>"],
["参照用アカウントの<br/>接続元ホスト", "<b>参照用アカウント@%</b>",
"<b>% は「どこからでも」。接続元の制限がありません</b>"],
]
# 12-6. 当方の現在の理解と、その根拠
UNDERSTAND_ROWS = [
["#", "当方の理解", "根拠", "確認の有無"],
["1", "本番のDBは <b>MariaDB 10.11</b>",
"<b>貴社のご回答(2026-09-10)"
"「本番サーバはMariaDBです」</b>", "<b>確認済み</b>"],
["2", "DBは <b>さくらインターネットの<br/>データベースアプライアンス</b>",
"同ご回答「さくらインターネットの"
"データベースアプライアンスはPostgreSQLかMariaDBなので」",
"<b>確認済み</b>"],
["3", "アプリは <b>さくらのクラウド VM</b>"
"(Node.js 常駐が必要なため)",
"<b>当方側の想定</b>", "<b>未確認</b>"],
["4", "<b>「アプリ=VM」「DB=アプライアンス」という"
"別サービスの組み合わせである</b>",
"<b>上記2・3からの読み取り</b>", "<b>未確認</b>"],
["5", "<b>VM と DB が同一ネットワークで閉域接続される</b>",
"<b>当方側の想定</b>", "<b>未確認</b>"],
]
# 12-6. Q12-1 の選択肢
Q121_ROWS = [
["#", "対象", "選択肢"],
["<b>a</b>", "<b>アプリサーバー(Node.js を載せる VM)</b>",
"<b>貴社が用意 / 弊社が用意 / 未定</b>"],
["<b>b</b>", "<b>DBサーバー(データベースアプライアンス)</b>",
"<b>貴社が用意 / 弊社が用意 / 未定</b>"],
]
# ご回答欄(原稿の設問の採番と要約を転記したもの)
ANSWER_ROWS = [
["質問番号", "質問の要約", "ご回答"],
["<b>1章</b>",
"<b>索引(参考情報)— ご対応が完了しましたら、その旨を<br/>"
"お知らせください</b>", ""],
["<b>Q2-1</b>",
"<b>集計バッチは task_area / clean_area に期間内の合計を<br/>"
"書き込む仕様か(A / B)</b>", ""],
# ★Q2-2 は 2026-09-17(2回目)に取り下げた。番号は詰めず Q2-3 のまま★
["<b>Q2-3</b>",
"1つの clean_timetable_id に clean_status_histories は<br/>"
"何行できるか(A / B)", ""],
["<b>3章</b>",
"<b>第8回 Q4 の残り(率の計算式・分子・分母・件数列)</b>", ""],
["Q4-1", "error_count は事前チェックを除外しているか(A / B)", ""],
["Q4-2", "clean_timetable_id が null のエラー行は含まれるか", ""],
["Q4-3", "clean_error が 2 / 3 の清掃は含まれるか", ""],
# ★Q5-1 は 2026-09-17(2回目)に取り下げた(弊社で ㎡ と確定)★
["Q6-1", "グループ単位の集計の想定(A / B / C)", ""],
["Q6-2", "「清掃実施回数」のカラムを追加できるか(A / B / C)", ""],
["<b>7章</b>",
"<b>本番の groups / customers のデータ投入の時期の目安</b>", ""],
["Q7-1", "customers は法人と屋号のどちらの粒度か(A / B)", ""],
["Q7-2", "屋号を持たない顧客の customer_name に入る値", ""],
["Q7-3", "facilities.shop_id は何を表す列か", ""],
["Q7-5", "「屋号を持たない顧客」はどの段が空くか(① / ②)", ""],
["<b>Q7-4</b>",
"<b>customers.group_id = 0 が投入できない件<br/>(A / B / C / D)</b>", ""],
# ★Q8-1 / Q8-2 は 2026-09-17 に取り下げた(当方側で決めることのため)。
# 番号は詰めず Q8-3 のまま残す(原稿8章の断り書きどおり)★
["Q8-3",
"link_schedules.intervals の運用上の下限・上限・<br/>変更の頻度"
"(a / b / c)", ""],
["Q9-1", "robot_task_reports は clean_task_reports のことか", ""],
["Q9-2", "1回の清掃の中の、区域別の内訳は取得できるか", ""],
["9章", "DDL 未反映の2件(AUTO_INCREMENT / DEFAULT)", ""],
# ★10章は 2026-09-17(2回目)にご依頼を取り下げた(すでに発行済みでした)★
["<b>Q11-1</b>",
"<b>clean_status_histories.started_at を実開始時刻として<br/>"
"実装してよいか(A / B)</b>", ""],
["Q11-2", "clean_status_trans = 1 で止まったケースは履歴に残るか", ""],
["<b>Q11-3</b>",
"<b>launch_count / launch_ratio の計算式</b>", ""],
["Q11-4",
"started_at より短い間隔で更新されるテーブル・カラムは<br/>"
"あるか(a / b / c)", ""],
["<b>12章</b>",
"<b>お願い3件: 接続元IPの制限 / TLS の有効化 /<br/>"
"本番用DBアカウントの新規発行</b>", ""],
["<b>Q12-1</b>",
"<b>アプリVM(a)とDBサーバー(b)は、それぞれ<br/>"
"どちらが用意するか</b>", ""],
["Q12-2", "本番のDBはデータベースアプライアンスか<br/>(YES / NO)", ""],
["Q12-3", "本番のアプリはさくらのクラウドのVMか<br/>(YES / NO)", ""],
["<b>Q12-4</b>",
"<b>VM と DBアプライアンスは同一の閉域ネットワークで<br/>"
"結べるか(YES / NO)</b>", ""],
["Q12-5", "YES の場合、公開IPからの接続を完全に閉じられるか"
"<br/>(YES / NO / 条件つき)", ""],
["Q12-6", "NO の場合、TLS を有効にできるか"
"<br/>(YES / NO / 該当なし)", ""],
]
def build():
doc = BaseDocTemplate(
OUT, pagesize=A4,
leftMargin=18 * mm, rightMargin=18 * mm,
topMargin=18 * mm, bottomMargin=20 * mm,
title="D-Watch 確認事項(第9回)",
author="DFA Robotics",
subject="サマリの面積・実際の清掃開始時刻・DB接続経路ほかのご確認",
)
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("確認事項(第9回)", S_TITLE))
story.append(Paragraph(
# ★2026-09-17 の日付が増えて1行に収まらなくなったので、
# 「作成者:」の前で明示的に改行する(自動折り返しだと
# DFA / Robotics が割れる)★
"宛先: TeamWorks(DB開発側) 作成: 2026-09-17 改訂"
"(初版 2026-09-15 / 統合 2026-09-16)<br/>"
"作成者: DFA Robotics", S_SUB))
story.append(Spacer(1, 5))
story.append(quote(
"<b>2026-09-17(2回目)の改訂点</b>: "
"<b>テスト環境のDBに入っているデータは、当方(DFA)が入れた"
"検証用のダミーデータでありました</b>。"
"<b>つきましては「行の中身」を根拠にしていた記述を、"
"取り下げるか、当方の検証用データでの観測である旨を明記する"
"形に改めております</b> / "
"<b>2章は実データの話を削り、仕様の確認のみに縮めました"
"(Q2-2 は取り下げ)</b> / "
"<b>5章(面積の単位)は弊社で「㎡」と確定しましたので、"
"設問(Q5-1)を取り下げました</b> / "
"<b>8章 Q8-3 は「固定値」ではなく「運用上の下限・上限・変更の頻度」"
"を伺う形に改めました</b> / "
"<b>10章(DB更新用アカウント)はすでに権限が発行されておりました。"
"当方の確認漏れです。ご依頼を取り下げました</b> / "
"<b>11章 Q11-1(実際の清掃開始時刻)は お伺いする内容を改めました</b>。"
"「どこにありますか」ではなく "
"<b>「started_at を使って実装してよろしいでしょうか」</b> という"
"確認に変えております。あわせて <b>Q11-4</b> を新たに立てました",
CW))
story.append(Spacer(1, 5))
story.append(quote(
"<b>2026-09-17(1回目)の改訂点</b>: "
"<b>索引(1章)はご対応中と伺いましたので、ご依頼を取り下げ"
"参考情報に改めました</b> / "
"<b>規模の前提を「施設200件(仮置き)」から"
"「ロボット30台未満(弊社把握)」に置き直しました(1章・2-4)</b> / "
"<b>台数・施設数の設問(旧 Q8-1 / Q8-2)を取り下げました"
"(弊社側で決めるべきことのため)</b>", CW))
story.append(Spacer(1, 5))
story.append(quote(
"<b>本書は、第9回確認事項に、DB接続経路についての申し送り"
"(お願い3件・確認6問)を統合したものです。</b><br/>"
"<b>別便は出しません。第9回として1通にまとめてお送りいたします。"
"統合した内容は12章です。</b>", CW))
story.append(Spacer(1, 9))
story.append(Paragraph("はじめに", S_H2))
story.append(Paragraph(
"<b>第8回(2026-09-11)にはご回答をいただき、ありがとうございました。</b>"
"<b>いただいた回答をもとに、タイムゾーン・エラー率・"
"エリア別完了率の実装を直しております。</b>", S_BODY))
story.append(Paragraph(
"<b>第8回の Q4(サマリの率・分子・分母の関係)は"
"「多いので後で回答」との留保をいただいたままです。"
"取り下げておりませんので、引き続きお願いいたします"
"(本書の3章に再掲します)。</b>", S_BODY))
story.append(Paragraph(
"そのうえで、<b>テスト環境のDBに接続して実測を行ったところ、"
"確認事項が新たに発生しました。</b>", S_BODY))
# -------------------------------- おことわり(2026-09-17 の2回目の改訂で新設)
story.append(CondPageBreak(90 * mm))
story.append(Paragraph(
"おことわり — テスト環境のデータは当方が入れたものでした", S_H2))
story.append(Paragraph(
"<b>本書の初版(2026-09-15)では、テスト環境のDBの「行の中身」を"
"根拠にした記述がございました。</b>"
"<b>その後、当方で確認いたしましたところ、テスト環境に入っている"
"データは当方(DFA)が開発用に投入した検証用のダミーデータでした。"
"</b>", S_BODY))
story.append(Paragraph(
"<b>そのため、行数・値の分布・特定のテーブルが0行であること等を"
"根拠にした指摘は、貴社のバッチや収集処理の挙動を"
"示すものではありませんでした。</b>", S_BODY))
story.append(Paragraph(
"<b>本改訂では、これらを次のように仕分けております。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(DISCLAIM_ROWS, [40 * mm, 66 * mm, W - 106 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>紛らわしいことを申し上げ、失礼いたしました。</b>", S_BODY))
# -------------------------------- 本書の構成
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("本書の構成", S_H2))
story.append(Spacer(1, 2))
story.append(callout(
"<b>索引の件については、ご対応中と伺いました。</b><br/>"
"<b>つきましては1章はご依頼を取り下げ、参考情報に改めております"
"(詳細は1章の冒頭に記しました)。</b><br/>"
"<b>そのうえで本書の中心は、12章(DB接続経路)です。</b><br/>"
"<b>12章は期限が「ステージングに実データが入る前」という"
"性質のもので、過ぎますと作り直しになります。</b>", CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>あわせて、3章・4章・6章(サマリの計算式と分子・分母)が"
"当方の実装に直結いたします。</b>"
"<b>11章は、Q11-1(実際の清掃開始時刻)について、お伺いする内容を"
"「どこにありますか」から "
"<b>「この理解で実装してよろしいでしょうか」</b> に"
"改めております。</b>", S_BODY))
story.append(Spacer(1, 2))
# ★12行になって表の高さが 147mm。表紙の前文と合わせると1ページに収まらず、
# 最終行(12章)だけが次ページに落ちる。表は目次なので割らずに送り、
# 導入の一文(「次の12部構成です。」)も表と一緒に送る★
story.append(CondPageBreak(157 * mm))
story.append(Paragraph(
"本書は次の12部構成です"
"(<b>質問25問 / お願い5件 / 参考情報1章</b>)。", S_LEAD))
story.append(Spacer(1, 3))
# 種別の列は「お願い+質問」(6字 = 8.8pt で 52.8pt = 18.6mm)が
# 1行に収まるよう 26mm にしている(内寸 = 26mm - 4.2mm = 21.8mm)
story.append(table(SUMMARY_ROWS, [52 * mm, W - 102 * mm, 26 * mm, 24 * mm],
[S_CELL, S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>7章は貴社の担当領域(DBとバッチ)への変更を伴いますので、"
"「お願い」として書いております。</b>"
"<b>ご負担や副作用のご懸念があれば、その旨をお知らせください。"
"当方で別の手を検討します。</b>", S_BODY))
# ---------------------------------------------------------- 1. 索引
story.append(CondPageBreak(75 * mm))
story.append(Paragraph(
"1. 索引について(ご対応中と伺いました。参考情報です)", S_H2))
story.append(Paragraph(
"1-0. 本章の位置づけ — ご依頼は取り下げております", S_H3))
story.append(Paragraph(
"<b>索引についてはご対応中と伺いました。"
"つきましては本章は依頼ではなく参考情報です。</b>"
"<b>ご対応が完了した時点で、残っているものがあれば"
"改めてご相談します。</b>", S_BODY))
story.append(Paragraph(
"<b>本章に残しておりますのは、当方が EXPLAIN で実測した結果と、"
"当方が見ていた列の一覧です。</b>"
"<b>貴社が入れられた索引で足りているかどうかを、そちらで"
"突き合わせていただけるように残しております。</b>"
"<b>この一覧のとおりにしていただきたい、という意味では"
"ありません。</b>", S_BODY))
story.append(Spacer(1, 2))
story.append(quote(
"<b>初版(2026-09-15)では本章を「最優先のご依頼(7件)」として"
"書いておりました。本改訂(2026-09-17)で取り下げております。"
"行き違いがございましたら申し訳ございません。</b>", CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(70 * mm))
story.append(Paragraph("1-1. なぜこの列を見ていたか", S_H3))
story.append(Paragraph(
"当方は清掃詳細ログの表示期間を <b>それまでの上限31日から、"
"上限2年(731日)に広げました</b>。", S_BODY))
story.append(Paragraph(
"そのために EXPLAIN で実行計画を確認したところ、"
"<b>索引が無いために全表走査になっている経路が"
"複数見つかりました。</b> "
"以下の①〜⑦は、そのとき当方が見ていた列です。", S_BODY))
story.append(Paragraph(
"<b>現状(データが少ないうち)は問題が表に出ません。</b>"
"<b>問題になるのは、本番のデータが積み上がってからです。</b>",
S_BODY))
# ★「実測」と書くときは数字の出所を併記する(当方の作業規約)。
# 旧「以下、すべて実測です。推測ではありません。」は出所を書かずに
# 言い切っていたので差し替えた。★点検スクリプトの 12. が旧文言を見る★
story.append(Paragraph(
"以下の SHOW CREATE TABLE と EXPLAIN は、"
"<b>テスト環境で実際に実行したものです。推測ではありません。</b>",
S_BODY))
story.append(Paragraph(
"<b>ただし、そこに出てくる行数(rows / r_rows)は"
"当方が入れた検証用データの件数です。</b>"
"<b>申し上げたいのは type / possible_keys / key という"
"実行計画の形のほうで、行数ではございません。</b>"
"<b>詳しくは冒頭の「おことわり」をご覧ください。</b>", S_BODY))
story.append(Paragraph("実測環境:", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"mysql://…@133.125.238.144:13306/clean_watch",
"VERSION() = 10.11.19-MariaDB-ubu2204",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>なお ALTER TABLE の形で書いておりますのは、"
"列と順序を誤解なくお伝えするためです。</b>"
"<b>当方でこれを実行することはございません"
"(当方はメインDBのスキーマを変更しない方針です)。</b>", S_BODY))
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"1-2. ①② — 索引が1本も無い列", S_H3))
story.append(Spacer(1, 2))
story.append(code([
"ALTER TABLE `clean_status_histories`",
" ADD INDEX `clean_status_histories_clean_timetable_id_index` "
"(`clean_timetable_id`);",
"",
"ALTER TABLE `robot_error_reports`",
" ADD INDEX `robot_error_reports_clean_timetable_id_index` "
"(`clean_timetable_id`);",
], CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("根拠(EXPLAIN の出力そのまま)", S_H4))
story.append(quote(
"<b>行数の読み方について</b><br/>"
"以下の rows / r_rows は、<b>当方が入れた検証用データでの行数</b>です"
"(「おことわり」参照)。<b>本番の行数を示すものではありません。</b><br/>"
"一方、type / possible_keys / key は "
"<b>表の構造(索引の有無)から決まる</b>もので、"
"<b>行数によって変わりません。</b> "
"<b>本章で申し上げたいのは後者です。</b>", CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"clean_status_histories を clean_timetable_id IN (5件) で"
"引いたとき:", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"type=ALL | possible_keys=null | key=null | key_len=null",
"rows=538 | r_rows=538.00 | filtered=100 | r_filtered=0.93 | "
"Extra=Using where",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"robot_error_reports を同様に引いたとき:", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"type=ALL | possible_keys=null | key=null",
"rows=49 | r_rows=49.00 | filtered=100 | r_filtered=4.08 | "
"Extra=Using where",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>possible_keys = null = 使える索引が1本もありません。</b>"
"<b>表の全行を読んで、そのうち一致した行だけを残しています</b>"
"(上の 538行・0.93% は <b>当方の検証用データでの値</b>です)。",
S_BODY))
story.append(Paragraph(
"これは統計に基づくコスト判断ではなく、<b>構造的な全表走査</b>です。"
"行数が増えても索引を選ぶようにはなりません。", S_BODY))
story.append(CondPageBreak(75 * mm))
story.append(Paragraph("SHOW CREATE TABLE での確認", S_H4))
story.append(Spacer(1, 2))
story.append(code([
"-- clean_status_histories",
"UNIQUE KEY `clean_status_histories_id_unique` (`id`)",
"KEY `clean_status_histories_facility_id_foreign` (`facility_id`)",
"KEY `clean_status_histories_section_id_foreign` (`section_id`)",
"-- clean_timetable_id には索引も FK もありません",
"",
"-- robot_error_reports",
"UNIQUE KEY `robot_error_reports_report_id_unique` (`error_id`)",
"KEY `robot_error_reports_facility_id_foreign` (`facility_id`)",
"KEY `robot_error_reports_section_id_foreign` (`section_id`)",
"KEY `robot_error_reports_robot_id_foreign` (`robot_id`)",
"-- clean_timetable_id には索引がありません",
], CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(70 * mm))
story.append(Paragraph(
"影響の大きさ(2026-09-17 に前提を置き直しました)", S_H4))
story.append(Paragraph(
"当方のアプリは、この2つのテーブルを "
"<b>clean_timetable_id IN (…) の形でしか引きません。</b> "
"IN の要素数が1000を超えると1000件ずつの塊に分けて投げますが、"
"<b>塊ごとに全表走査が起きます。</b>", S_BODY))
story.append(Paragraph(
"<b>初版ではこの走査行数を「施設200件」という"
"当方の仮置きで外挿しておりました。</b>"
"<b>その後、弊社で現状のロボット台数を確認したところ "
"30台未満 でしたので、置き直しております。</b>", S_BODY))
story.append(Paragraph(
"<b>台数の根拠は、弊社(DFA)自身の把握「現状30台未満」"
"(2026-09-17)です。</b>"
"<b>貴社への質問ではございません。</b>", S_BODY))
story.append(CondPageBreak(95 * mm))
story.append(Paragraph("計算過程", S_H4))
story.append(quote(
"<b>下の「1台1日2回」は、貴社のデータから得た値ではありません。</b><br/>"
"当方が入れた検証用データの密度"
"(31日 / 12エリア / 538行 = 1.45 回/日/エリア)を切り上げた、"
"<b>当方の置き値</b>です。<br/>"
"<b>実運用の清掃回数が決まりましたら、"
"当方から置き直してお知らせいたします。</b>", CW))
story.append(Spacer(1, 6))
story.append(code([
"1) 1ロボット(=1清掃エリア)あたりの1日の清掃予定回数",
" 当方の置き値(当方の検証用データの密度 "
"1.45 回/日/エリア を切り上げたもの)",
" → 2 回/日/台 として計算する",
"",
"2) 2年ぶんの行数(当方の画面の期間上限は 731日です)",
" 30台 × 2回/日 × 731日 = 43,860行",
" = clean_timetables(precheck = 0)の行数。",
" clean_status_histories も同数とみなす",
" (当方の検証用データでは 1対1 でしたが、"
"本来の仕様は 2章 Q2-3 でお伺いしております)",
"",
"3) IN の塊の数",
" 43,860 ÷ 1,000 → 44塊",
"",
"4) 走査行数(1塊につき全表走査1回)",
" 44塊 × 43,860行 = 約 193万行",
], CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(45 * mm))
story.append(table(SCAN_ROWS, [26 * mm, 20 * mm, W - 46 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>初版(施設200件での外挿)では「2年で約6,430万行」と"
"しておりました。約 1/33 になります。</b>", S_BODY))
story.append(CondPageBreak(70 * mm))
story.append(Paragraph(
"数字が小さくなったことについて、事実だけ申し添えます", S_H4))
story.append(Paragraph(
"<b>「だから問題ない」と断定するつもりはございません。</b> "
"分かっていることは次の3点です。", S_LEAD))
story.append(Spacer(1, 2))
story.extend(bull([
"possible_keys = null である以上、"
"<b>全表走査は行数によらず構造的に起きます</b>。"
"<b>行数が増えても索引を選ぶようにはなりません</b>"
"(統計に基づくコスト判断ではないためです)",
"清掃詳細ログは、この全表走査を "
"<b>2つの経路(本体と連続日数の算出)で別々に撃ちます</b>。"
"2年の表示1回あたり、合計で 約386万行の走査になります",
"上の 43,860行 は「1台1日2回」への切り上げです。"
"<b>塊の数と1塊あたりの行数が両方とも台数・回数に比例するため、"
"走査行数はそれらに対して二乗で効きます</b>。"
"台数が倍になれば走査行数は4倍になります",
]))
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"1-3. ③④ — 複合索引(clean_timetables)", S_H3))
story.append(Spacer(1, 2))
story.append(code([
"ALTER TABLE `clean_timetables`",
" ADD INDEX `clean_timetables_precheck_start_at_index` "
"(`precheck`, `start_at`);",
"",
"ALTER TABLE `clean_timetables`",
" ADD INDEX `clean_timetables_facility_id_start_at_index` "
"(`facility_id`, `start_at`);",
], CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(75 * mm))
story.append(Paragraph("根拠", S_H4))
story.append(Paragraph(
"当方の主クエリは <b>常に precheck = 0 AND start_at >= ? "
"AND start_at < ? の AND</b> です。"
"start_at 単独の索引はありますが、実行計画では使われていません。",
S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"-- 2年範囲の主クエリ",
"type=ALL | possible_keys=clean_timetables_start_at_index | key=null | "
"key_len=null",
"rows=1044 | r_rows=538.00 | Extra=Using where; Using filesort",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph("施設で絞った場合も同様です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"-- 2年範囲 + facility_id IN (1,2,3)",
"type=ALL | possible_keys=clean_timetables_facility_id_foreign,"
"clean_timetables_start_at_index",
"key=null | rows=1044 | r_rows=175.00 | filtered=32.47 | "
"Extra=Using where; Using filesort",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>単一列の索引が2本とも候補に挙がりながら、"
"どちらも使われていません。</b>"
"これが複合索引を必要とする形そのものです。", S_BODY))
story.append(Spacer(1, 2))
story.append(quote(
"<b>ただし、ここは①②とは性質が異なります。</b><br/>"
"③④は possible_keys に索引が挙がっておりますので、"
"<b>使うかどうかはコスト判断</b>です。<br/>"
"<b>上の実行計画は当方の検証用データ(少量)で測ったものですので、"
"行数が増えれば選択が変わる可能性があります。</b><br/>"
"そのため③④は、①②ほど強くは申し上げられません。", CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(45 * mm))
story.append(Paragraph("2本を1本にまとめない理由", S_H4))
story.append(Paragraph(
"(facility_id, precheck, start_at) の1本に寄せる案もありえますが、"
"<b>施設で絞らない呼び出し(画面の既定)が主経路</b>であり、"
"その1本では precheck = 0 AND start_at が引けません。"
"<b>そのため当方は2本別々のものとして見ておりました。</b>", S_BODY))
story.append(Paragraph(
"なお clean_timetables の既存索引は6本すべて単一列で、"
"<b>2026-09-13 時点では複合索引は1本もありませんでした</b>。",
S_BODY))
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"1-4. ⑤ — 期間での集計(面積の累計表示に関係します)", S_H3))
story.append(Spacer(1, 2))
story.append(code([
"ALTER TABLE `clean_status_histories`",
" ADD INDEX `clean_status_histories_started_at_index` (`started_at`);",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph("根拠", S_H4))
story.append(Paragraph(
"当方は <b>清掃総面積を期間で絞って集計する画面</b> を"
"追加しております(本書2章と関連)。"
"その集計は clean_status_histories を期間で絞って SUM() を取る"
"形になりますが、<b>started_at に索引が無いため、"
"期間で絞っても全行を走査します。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"EXPLAIN SELECT facility_id, SUM(clean_area) FROM "
"clean_status_histories",
" WHERE started_at >= '2026-09-01' AND started_at < '2026-09-14' "
"GROUP BY facility_id;",
"→ type=index | key=clean_status_histories_facility_id_foreign | "
"rows=538 | Extra=Using where",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"(rows は当方の検証用データでの行数です。"
"<b>申し上げたいのは「started_at に索引が無い」という構造のほう</b>で、"
"こちらは SHOW CREATE TABLE で確認しております)", S_NOTE))
story.append(Paragraph(
"<b>ここでも規模の前提を置き直しております。</b>"
"当方の社内資料は 「1日約360行・年間約13万行・保持2年」 を"
"想定しておりましたが、"
"これは施設200件規模に相当する数字でした。", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"ロボット30台 × 2回/日 × 365日 = 約 2.2万行/年"
"(2年で 約4.4万行)",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>規模は当初想定の 1/6 ほどになりますが、"
"「期間で絞っても全行を読む」という形そのものは"
"行数によらず変わりません。</b>", S_BODY))
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"1-5. ⑥⑦ — D-Security 制御盤(第8回以降に判明)", S_H3))
story.append(Spacer(1, 2))
story.append(code([
"ALTER TABLE `control_reports`",
" ADD INDEX `control_reports_control_id_reported_at_index` "
"(`control_id`, `reported_at`);",
"",
"ALTER TABLE `control_error_histories`",
" ADD INDEX `control_error_histories_occurred_at_index` "
"(`occurred_at`);",
], CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(70 * mm))
story.append(Paragraph(
"⑥の根拠 — <b>同じテーブル・同じ索引で、列を変えただけで"
"実行計画が切り替わりました</b>", S_H4))
story.append(Paragraph(
"制御盤ごとの「最終通信時刻」を取るため MAX(reported_at) を"
"使っています。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(MAXCOL_ROWS,
[32 * mm, 24 * mm, 30 * mm, 14 * mm, W - 100 * mm],
[S_CELL, S_CELL, S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 4))
story.append(Paragraph(
"(rows は当方の検証用データでの行数です。"
"<b>ご覧いただきたいのは type と Extra が切り替わった点</b>です)",
S_NOTE))
story.append(Spacer(1, 2))
story.append(Paragraph(
"<b>InnoDB の二次索引には主キーが暗黙に付く</b>ため、既存の<br/>"
"control_reports_control_id_foreign は物理的に "
"(control_id, id) の複合索引として働いています。"
"<b>2列目が reported_at になれば、MAX(reported_at) にも"
"同じ効果が及びます。</b>", S_BODY))
story.append(Paragraph(
"<b>control_reports は心拍の報告が積み上がるテーブルで、"
"本番では最も行数が多くなる見込みです</b>"
"(<b>8章の報告間隔次第で1,100万行〜7,000万行。"
"制御盤11台という当方の置き値での試算です</b>)。"
"<b>ここが全表走査のままですと、D-Sログ画面が開けなくなります。</b>",
S_BODY))
story.append(Paragraph(
"<b>清掃側(①〜⑤)とは事情が異なります。</b> "
"清掃側の行数は台数と清掃回数で決まりますが、"
"control_reports は <b>時間あたりの報告間隔で決まる</b>ため、"
"台数が少なくても行数は積み上がります。"
"<b>そのため上の「ロボット30台」の置き直しは、⑥⑦には効きません。"
"</b>", S_BODY))
story.append(CondPageBreak(35 * mm))
story.append(Paragraph("⑦の根拠", S_H4))
story.append(Paragraph(
"D-Secアラートの絞り込みで occurred_at の範囲指定を使いますが、"
"索引がありません。", S_BODY))
story.append(CondPageBreak(95 * mm))
story.append(Paragraph("1-6. ①〜⑦の一覧(突き合わせ用)", S_H3))
story.append(Paragraph(
"<b>貴社のご対応内容と突き合わせていただくための一覧です。</b>",
S_LEAD))
story.append(Spacer(1, 3))
# 「テーブル」列は control_error_histories(24字 = 8.8pt で 37.3mm)が
# 語中で割れないよう 44mm(内寸 39.8mm)にしている
story.append(table(INDEX_LIST_ROWS,
[10 * mm, 44 * mm, 44 * mm, W - 98 * mm],
[S_CELL, S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph("申し添え", S_H4))
story.extend(bull([
"①〜⑦のテーブルは、いずれも "
"<b>貴社のデータ収集処理が書き込むテーブル</b>です。"
"<b>索引を足すぶん、書き込みがわずかに遅くなります。</b>"
"<b>その兼ね合いのご判断は貴社にお任せいたします。</b>",
"<b>当方の見立てでは ①② が突出しています</b>"
"(索引が1本も無い列のため)。"
"<b>③〜⑦は、①②の後で差し支えありません。</b>",
"<b>当方はメインDBのスキーマを変更しない方針です。</b> "
"<b>上記を当方で実行することはございません。</b>",
"<b>ご対応が完了しましたら、その旨をお知らせいただけますと、"
"当方で EXPLAIN を測り直し、残りがあるかどうかを"
"確認いたします。</b>",
]))
# ---------------------------------------------------------- 2. サマリの面積
story.append(CondPageBreak(85 * mm))
story.append(Paragraph(
"2. サマリの面積は、期間内の合計でしょうか", S_H2))
story.append(Paragraph(
"2-1. 本章は仕様の確認のみに縮めております", S_H3))
story.append(Paragraph(
"<b>初版では、テスト環境の facility_summaries / "
"customer_summaries の値を並べて「合計に見えません」と"
"申し上げておりました。</b>"
"<b>この値は当方が入れた検証用のダミーデータでしたので、"
"当該の指摘は取り下げます。</b>"
"<b>お騒がせいたしました。</b>", S_BODY))
story.append(Paragraph(
"<b>残しますのは、実装のために必要な仕様の確認だけです。</b>",
S_BODY))
story.append(Paragraph(
"当方は <b>「清掃総面積の累計」を画面に表示する機能</b> を"
"作っており、その数字をサマリから取るのか、"
"clean_status_histories から毎回その場集計するのかを"
"決める必要があります"
"(後者を選ぶ場合に効くのが、1章の⑤の列です)。", S_BODY))
story.append(CondPageBreak(85 * mm))
story.append(Paragraph("2-2. お伺いしたいこと(2問)", S_H3))
story.append(Spacer(1, 2))
story.append(qhead(
"Q2-1. 集計バッチは、task_area / clean_area に "
"期間内の合計 を書き込む仕様でしょうか", 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(Paragraph(
"<b>B の場合、当方は clean_status_histories から"
"毎回その場集計する形にいたします。</b>"
"<b>「サマリに合計を入れるほうが筋がよい」というご判断であれば、"
"そちらに合わせます。</b>", S_BODY))
story.append(CondPageBreak(90 * mm))
story.append(qhead(
"Q2-3. 1つの clean_timetable_id に対し、clean_status_histories は"
"何行できますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>面積の合計をSQLの SUM() で取るか、行を1本に畳んでから足すかが、"
"これで変わります。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q23_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>「清掃状態履歴」というテーブル名からは、"
"B の設計ではないかとも思われます。</b>"
"<b>一方で clean_status_histories にはカラムとしての状態"
"(clean_status_trans 相当)が無く、この点は A のようにも"
"読めます。</b>"
"<b>どちらとも決めかねましたので、お教えください。</b>", S_BODY))
story.append(Paragraph(
"<b>B の場合、SUM() で足すと同じ清掃の面積を"
"何度も数えてしまいます。</b>"
"<b>そして、それは例外もエラーも出さずに、"
"ただ数字が大きくなるだけです。</b>", S_BODY))
story.append(Paragraph(
"<b>当方は安全側(B の前提で、最新の1行に畳んでから足す)で"
"実装いたしますが、A と確定できるのであれば、"
"そのほうが速くなります。</b>", S_BODY))
story.append(Spacer(1, 2))
story.append(quote(
"<b>設問番号について</b>: "
"初版の Q2-2(「合計でない場合の正しい経路をお教えください」)は"
"取り下げました"
"(Q2-1 の B に当方の対応を書きましたので、"
"独立の設問にする必要がなくなったためです)。"
"<b>番号は詰めずに Q2-3 のままにしております。</b>", CW))
story.append(CondPageBreak(85 * mm))
story.append(Paragraph(
"2-4. 参考: 年次サマリの桁あふれについて(優先度は低いです)", S_H3))
story.append(Paragraph(
"<b>ご依頼でも質問でもなく、気づいた点のご報告です。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"facility_summaries.task_area / clean_area double(14,6) "
"→ 上限 約 1億㎡",
"customer_summaries.task_area / clean_area double(14,6) → 同上",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>もしサマリに期間内の合計を入れる場合(Q2-1 が A の場合)、"
"年次の行が上限に当たる可能性があります。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(OVERFLOW_ROWS, [44 * mm, W - 44 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(code([
"1億㎡ ÷ 30台 ÷ 365日 ≒ 9,132 ㎡/台/日",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>これは当方の計算であり、実測ではありません</b>"
"(当方の手元には実際の店舗面積の値がございませんので、"
"9,100㎡ が現実的な値かどうかは判断できておりません)。", S_BODY))
story.append(Paragraph(
"<b>初版では「施設200件」を前提に 約1,370㎡ としておりましたが、"
"弊社で現状のロボット台数を確認したところ30台未満でしたので、"
"置き直しております(1章と同じ前提です)。</b>"
"<b>桁あふれの可能性はそのぶん遠のきました。</b>", S_BODY))
story.append(Paragraph(
"<b>週次・月次では問題になりません。"
"P1 で当方が読むのも週次・月次のみです。</b>"
"<b>年次サマリを作られる際にご一考いただければ、"
"という程度のものです。</b>", S_BODY))
# ---------------------------------------------------------- 3. 第8回 Q4
story.append(CondPageBreak(70 * mm))
story.append(Paragraph(
"3. 第8回 Q4 の残り(サマリの率・分子・分母の関係)— 再掲", S_H2))
story.append(Paragraph(
"第8回で「多いので後で回答」との留保をいただいた件です。"
"<b>取り下げておりません。</b>", S_BODY))
story.append(Paragraph(
"お伺いしたい内容は第8回の4章と同じです。要点だけ再掲します。",
S_LEAD))
story.append(Spacer(1, 2))
story.extend(bull([
"サマリに入る各率(clean_ratio 等)の <b>計算式</b>",
"<b>分子・分母に対応するカラム名</b>",
"件数列(task_count 等)が <b>何を数えているか</b>"
"(予定の数か、実施の数か)",
]))
story.append(Spacer(1, 2))
story.append(Paragraph(
"<b>2章の面積の件と同じ枠の話ですので、"
"あわせてご回答いただけますと助かります。</b>", S_BODY))
# ---------------------------------------------------------- 4. error_count
story.append(CondPageBreak(80 * mm))
story.append(Paragraph("4. サマリの error_count の集計範囲", S_H2))
story.append(Paragraph(
"第8回 A7 でいただいたご回答が、"
"<b>「バッチが実際にそうしている」のか「当方への助言」なのか</b>"
"を読み取れませんでした。"
"<b>当期(進行中の週・月)は当方が自前で集計するため、"
"定義が食い違うと、確定期間と当期でグラフが段差になります。</b>",
S_BODY))
story.append(Paragraph("お伺いしたいこと(3問)", S_H3))
story.append(Spacer(1, 2))
story.append(qhead(
"Q4-1. サマリの error_count は、事前チェック"
"(precheck = 30 / 60)を除外して集計されていますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(table(Q41_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
story.append(Spacer(1, 8))
# Q4-2 は原稿に本文が無く帯だけなので、単独でページ末に取り残されないよう
# Q4-3 の帯まで入る高さを確保してから置く
story.append(CondPageBreak(62 * mm))
story.append(qhead(
"Q4-2. clean_timetable_id が null のエラー行は、サマリの "
"error_count に含まれますでしょうか", CW))
story.append(Spacer(1, 8))
story.append(CondPageBreak(55 * mm))
story.append(qhead(
"Q4-3. clean_error が 2(セキュリティ有効)/ "
"3(D-S制御盤異常)の清掃は、サマリの error_count に"
"含まれますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>第8回 A6-1 で「2 / 3 では robot_error_reports が作られない」"
"とのご回答をいただいております。</b>"
"<b>その場合、サマリ側でも数えていない、"
"という理解でよろしいでしょうか。</b>", S_BODY))
# ---------------------------------------------------------- 5. 面積の単位
# ★2026-09-17(2回目)の改訂で章ごと差し替えた。設問(Q5-1)も表も無い★
story.append(CondPageBreak(55 * mm))
story.append(Paragraph("5. 面積の単位(設問を取り下げます)", S_H2))
story.append(Paragraph(
"<b>初版では「task_area / clean_area の単位は平方メートル(㎡)"
"でしょうか」とお伺いしておりました(Q5-1)。</b>", S_BODY))
story.append(Spacer(1, 2))
story.append(callout(
"<b>本件は弊社(DFA)で「㎡」と確定いたしましたので、"
"設問を取り下げます。</b><br/>"
"<b>画面には ㎡ として表示いたします。</b>", CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>貴社にご対応いただくことはございません。</b>"
"<b>万一「㎡ではない」という事情がございましたら、"
"その場合のみお知らせください。</b>", S_BODY))
# ---------------------------------------------------------- 6. グループ
story.append(CondPageBreak(85 * mm))
story.append(Paragraph("6. グループ単位のサマリ", S_H2))
story.append(Paragraph(
"6-1. group_summaries に相当するテーブルが見当たりません", S_H3))
story.append(Paragraph(
"サマリ系のテーブルは次の4本と認識しております。", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"customer_summaries / customer_error_summaries "
"(customer_id を参照)",
"facility_summaries / facility_error_summaries "
"(facility_id を参照)",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>グループ(groups)単位のサマリがありません。</b>", S_BODY))
story.append(Paragraph(
"当方は <b>ホールディングス単位のKPIを画面に出す</b> 予定です"
"(本書7章と関連)。", S_BODY))
story.append(CondPageBreak(75 * mm))
story.append(Paragraph("お伺いしたいこと(1問)", S_H3))
story.append(Spacer(1, 2))
story.append(qhead(
"Q6-1. グループ単位の集計は、どちらの想定でしょうか", CW))
story.append(Spacer(1, 4))
story.append(table(Q61_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>A の場合、率は「率の平均」ではなく"
"「Σ分子 ÷ Σ分母」で再計算いたします。</b>"
"<b>分子・分母がサマリに入っている前提ですので、"
"3章のご回答と関係します。</b>", S_BODY))
story.append(CondPageBreak(95 * mm))
story.append(Paragraph(
"6-2. ところが、4つの率のうち2つは分子・分母が揃っておりません",
S_H3))
story.append(Paragraph(
"<b>本節は、テーブルのカラム構成(DDL)と当方の計算式を"
"突き合わせたもので、行の中身には依存しておりません。</b>",
S_BODY))
story.append(Paragraph(
"サマリのカラムを当方の計算式(要件定義)と突き合わせたところ、"
"次のようになりました。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(RATIO_ROWS,
[30 * mm, 48 * mm, 30 * mm, 28 * mm, W - 136 * mm],
[S_CELL, S_CELL, S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>「清掃実施回数」に当たるカラムが見当たりませんでした。</b>",
S_BODY))
# なぜ「清掃実施回数」が要るのか(2026-09-17 の2回目の改訂で新設)
story.append(CondPageBreak(95 * mm))
story.append(Paragraph(
"なぜ「清掃実施回数」が要るのか(Q6-2 の理由)", S_H4))
story.append(Paragraph(
"<b>当方の4つの率のうち、2つは「清掃実施回数」を必要とします。</b>",
S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"稼働率 = 清掃実施回数 ÷ 清掃予定回数 × 100",
"エラー発生率 = エラー発生回数 ÷ 清掃実施回数 × 100",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>つまり「実施回数」は、一方の率では分母、"
"もう一方の率でも分母に当たります。</b>"
"<b>そして task_count は、予定回数か実施回数かの"
"どちらか一方にしかなりません。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(TASKCOUNT_ROWS, [50 * mm, W - 50 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>いずれにせよ、現在のカラム構成では2つの率のうち"
"少なくとも片方が計算できません。</b>"
"(task_count が実際にどちらなのかは、"
"3章でお伺いしております)", S_BODY))
story.append(Paragraph(
"<b>これは合算のときだけの話ではありません。</b>", S_BODY))
story.append(Spacer(1, 2))
story.append(callout(
"<b>確定した期間(前週・前月)の稼働率・エラー発生率そのものが、"
"サマリからは出せません。</b>", CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>進行中の期間は当方が詳細テーブルからその場集計しますので"
"出せますが、確定期間はサマリを読む設計ですので、"
"そこで数字が欠けます。</b>", S_BODY))
story.append(Paragraph(
"<b>複数顧客を合算する場合は、さらに事情が重くなります。</b>"
"<b>率の平均を取るのは誤りで、分子と分母をそれぞれ足してから"
"割る必要がありますが、この2つはその材料がありません。</b>",
S_BODY))
story.append(CondPageBreak(75 * mm))
story.append(qhead(
"Q6-2. サマリに「清掃実施回数」のカラムを追加していただくことは"
"可能でしょうか", CW))
story.append(Spacer(1, 4))
story.append(table(Q62_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>C の場合、当方は確定期間(前週・前月)の稼働率・"
"エラー発生率を画面に出さず、"
"清掃完了率と総清掃面積だけを出す形にいたします。</b>"
"<b>進行中の期間だけ2つの率が出て、確定期間では消える、"
"という見え方になります。</b>", S_BODY))
story.append(Paragraph(
"<b>初版では「効いてくるのは2社目から」と書いておりましたが、"
"これは合算の話に限った見立てでした。</b>"
"<b>上のとおり、1社であっても確定期間の2つの率が出せませんので、"
"訂正いたします。</b>", S_BODY))
story.append(Paragraph(
"<b>お急ぎいただく必要はございませんが、設計を決めたいので"
"方針だけ伺えますと助かります。</b>", S_BODY))
# ---------------------------------------------------------- 7. マスタ投入
story.append(CondPageBreak(85 * mm))
story.append(Paragraph(
"7. グループ・顧客マスタのデータ投入のお願い", S_H2))
story.append(Paragraph("7-1. 背景", S_H3))
story.append(Paragraph(
"当方は画面に <b>「ホールディングス → グループ(ブランド)→ 店舗」"
"の3階層の絞り込み</b> を出す予定です。", S_BODY))
story.append(Paragraph(
"テーブルの構造"
"(groups → customers.group_id → facilities.customer_id)は "
"<b>すでに3階層になっており、"
"当方から追加をお願いする必要はございません。</b>", S_BODY))
# ★2026-09-17(2回目)の改訂でコード枠を削除した
# (テスト環境の中身は当方が入れたダミーだったため)★
story.append(Paragraph(
"<b>ただし、当方の手元には本番の中身がございません。</b>", S_BODY))
story.append(Paragraph(
"テスト環境に入っておりますのは <b>当方が入れた検証用のダミー</b>"
"(groups 1行 / customers 1行 / "
"facilities 12行・全行が同じ customer_id)だけです。"
"<b>そのため当方では、3階層の絞り込みが実データで"
"どう見えるかを確かめられておりません。</b>", S_BODY))
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("7-2. お願いと、確認したいこと", S_H3))
story.append(Spacer(1, 2))
story.append(qhead(
"お願い: 本番の groups / customers のデータ投入を"
"お願いいたします", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>時期の目安だけでも結構です。</b>"
"<b>当方はそれまで、検証用の複数グループ・複数顧客のデータを"
"自前で用意して進めます。</b>", S_BODY))
story.append(CondPageBreak(75 * mm))
story.append(qhead(
"Q7-1. customers は、法人と屋号のどちらの粒度で"
"入る想定でしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>当方は「屋号(ブランド)」の粒度で画面を作る方針です。</b>",
S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q71_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>A と B では、同じ法人の中で屋号を分けられるかどうかが"
"変わります。</b>"
"<b>貴社の収集処理がどちらの粒度で customers を作る想定かを"
"お教えください。</b>", S_BODY))
story.append(CondPageBreak(55 * mm))
story.append(qhead(
"Q7-2. 屋号を持たない顧客は、どう表現されますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>当方は「屋号が無い顧客もありうる」という前提で"
"画面を作ります。</b> "
"customers.group_id は <b>0 がセンチネル(グループなし)</b> と"
"認識しておりますが、"
"<b>屋号そのものが無い場合に customer_name に何が入るか</b>を"
"お教えください。", S_BODY))
story.append(CondPageBreak(50 * mm))
story.append(qhead(
"Q7-3. facilities.shop_id(varchar(20)、既定 '0')は"
"何を表す列でしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"テーブル定義書からは読み取れず、"
"<b>テスト環境の値も当方が入れたダミー(全行 \"0\")でしたので、"
"値から推し量ることもできませんでした。</b>", S_BODY))
story.append(Paragraph(
"<b>顧客側の店舗コードでしょうか。"
"分類の手がかりに使えるかを知りたく存じます。</b>", S_BODY))
# ★原稿の7章は Q7-5 が Q7-4 より前に置かれている。その断り書き
# (2026-09-17 の改訂で新設)。引用ブロックと Q7-5 の帯は必ず同じページに★
story.append(CondPageBreak(112 * mm))
story.append(Spacer(1, 2))
story.append(quote(
"<b>設問の並びについて</b><br/>"
"<b>本節は Q7-5 → Q7-4 の順に並んでおります</b>。"
"Q7-4(group_id = 0 が投入できない件)が Q7-5 のご回答を"
"前提とする内容のため、この順にしております。<br/>"
"<b>当方の管理表がこの番号で追っておりますので、"
"番号は振り直しておりません。</b>", CW))
story.append(Spacer(1, 8))
story.append(qhead(
"Q7-5. 「屋号を持たない顧客」は、3階層のどの段が空くという"
"意味でしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>当方の中でも解釈が割れましたので、"
"貴社のDBの想定を伺わせてください。</b>", S_BODY))
story.append(Paragraph(
"当方が作ろうとしている階層はこの3段です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"ホールディングス → 屋号(ブランド) → 店舗",
" groups customers facilities",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"「屋号を持たない顧客がありうる」と伺っておりますが、"
"<b>どの段が空くのかで設計が変わります。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(LAYER_ROWS, [12 * mm, 28 * mm, 44 * mm, W - 84 * mm],
[S_CELL, S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>② であれば、Q7-4 が解決すればそのまま作れます。</b>", S_BODY))
story.append(Paragraph(
"<b>① の場合、customers にダミーの行を1件作って、"
"そこに店舗をぶら下げることになります。</b>"
"<b>その場合、customer_name に何を入れるか"
"(空文字は避けたく存じます)をお決めいただく必要があります。</b>",
S_BODY))
story.append(Paragraph(
"<b>貴社の収集処理が customers をどう作る想定かを"
"お教えいただければ、当方はそれに合わせます。</b>", S_BODY))
story.append(CondPageBreak(95 * mm))
story.append(qhead(
"Q7-4. customers.group_id = 0(グループなし)は、"
"現状のDDLでは投入できないように見えます", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>これは質問というより、実DDLの矛盾のご報告です。</b>", S_BODY))
story.append(Paragraph(
"当方は customers.group_id について、"
"<b>「NOT NULL / 既定値 0 / 0 はグループなしを表すセンチネル」</b>"
"と理解しております"
"(貴社より、索引の効率を理由にこの方式を採る旨の"
"ご回答をいただいております)。", S_BODY))
story.append(Paragraph(
"<b>ところが、実際のDDLでは 0 を投入できません。</b> "
"当方が接続して確認した内容です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"-- SHOW CREATE TABLE customers (抜粋)",
"`group_id` bigint(20) unsigned NOT NULL DEFAULT 0,",
"CONSTRAINT `customers_group_id_foreign` FOREIGN KEY (`group_id`) "
"REFERENCES `groups` (`id`)",
], CW))
story.append(Spacer(1, 6))
# ★2026-09-17(2回目)の改訂で「SELECT id FROM groups … 1 のみ」の行を
# 削除した(テスト環境の groups の中身は当方が入れたダミーのため)★
story.append(code([
"SELECT @@foreign_key_checks; → 1"
"(外部キー制約は有効です)",
"SELECT @@sql_mode; → NO_AUTO_VALUE_ON_ZERO は"
"含まれておりません",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>外部キー制約が有効である以上、group_id = 0 の顧客を "
"INSERT するには、groups に id = 0 の行が"
"存在していなければなりません。</b>"
"<b>group_id を省略して INSERT した場合も、"
"既定値の 0 が入るため同じです。</b>", S_BODY))
story.append(Paragraph(
"そして groups.id は AUTO_INCREMENT ですので、"
"<b>id = 0 の行を入れること自体に NO_AUTO_VALUE_ON_ZERO が必要</b>"
"ですが、上のとおり sql_mode には含まれておりません。", S_BODY))
story.append(Spacer(1, 2))
story.append(quote(
"<b>申し添え</b>: テスト環境の groups には id = 0 の行が"
"ございませんでしたが、"
"<b>この表の中身は当方が入れた検証用のダミー</b>ですので、"
"貴社の想定を示すものではありません。<br/>"
"<b>そのため本件は「実データがこうなっている」ではなく、"
"「DDLと sql_mode の組み合わせ上こうなる」というご報告です。</b>",
CW))
story.append(CondPageBreak(85 * mm))
story.append(Paragraph("Q7-4 でお伺いしたいこと", S_H3))
story.append(Paragraph(
"<b>当方はメインDBのスキーマを変更しない方針のため、"
"当方では直せません。</b>"
"<b>どの形がよいか、貴社のご判断をお聞かせください。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q74_ROWS, [16 * mm, 68 * mm, W - 84 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>D であれば、対応は不要です。その旨だけお知らせください。</b>",
S_BODY))
# ---------------------------------------------------------- 8. 報告間隔
story.append(CondPageBreak(85 * mm))
story.append(Paragraph("8. 制御盤の報告間隔", S_H2))
story.append(Paragraph(
"8-1. 台数については、当方で把握いたしました(設問を取り下げます)",
S_H3))
story.append(Paragraph(
"初版では「P1 稼働時点の施設数」「ロボット・D-S制御盤の台数」の"
"2問を貴社にお伺いしておりました。"
"<b>この2問は取り下げます。</b>", S_BODY))
story.append(Paragraph(
"<b>理由: 本番の規模は弊社(DFA)側で決めるべきことであり、"
"貴社にお伺いする筋のものではありませんでした。"
"当方の記録にもその旨が残っておりました。</b>", S_BODY))
story.append(Paragraph(
"<b>現時点の当方の把握は次のとおりです。"
"参考までにお知らせいたします。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(SCALE_ROWS, [30 * mm, 74 * mm, W - 104 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>上の「11台」は、当方がテスト環境に入れた検証用データの台数を"
"そのまま置き値にしたものです。</b>"
"実運用の台数ではありません。", S_BODY))
story.append(Paragraph(
"<b>1章・2-4 の外挿は、この「ロボット30台」を前提に"
"置き直しております。</b>"
"<b>台数が大きく動いた場合は、当方から改めてお知らせいたします。</b>",
S_BODY))
story.append(CondPageBreak(85 * mm))
story.append(Paragraph("お伺いしたいこと(1問)", S_H3))
story.append(Spacer(1, 2))
story.append(qhead(
"Q8-3. link_schedules.intervals(制御盤の報告間隔・秒)は、"
"運用上どの範囲で動きますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>初版では「実運用値をお教えください」と、"
"固定の1つの値を伺う形で書いておりました。</b>"
"<b>その後、この値は運用上変わりうるものだと分かりましたので、"
"聞き方を改めます。</b>", S_BODY))
story.append(Paragraph(
"<b>お教えいただきたいのは、次の3点です。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q83_ROWS, [10 * mm, 62 * mm, W - 72 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>おおよそで構いません。</b>"
"<b>「だいたいこの範囲」が分かれば十分です。</b>", S_BODY))
story.append(CondPageBreak(75 * mm))
story.append(Paragraph("なぜ範囲と頻度を伺うのか", S_H4))
story.append(Paragraph(
"<b>ここが control_reports の行数を決めるためです。</b>"
"<b>そして、行数の見積もりが6倍変わります。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(INTERVAL_ROWS, [60 * mm, W - 60 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>1,100万行と7,000万行とでは、索引の要否(1-5 の⑥)も、"
"画面の作り方も変わります。</b>"
"<b>下限が分かれば「最大でどこまで積み上がるか」を"
"押さえられますし、変更の頻度が分かれば、"
"その前提がいつまで持つかを判断できます。</b>", S_BODY))
story.append(Paragraph(
"<b>なお、上の台数(ロボット30台未満)はこの試算には効きません。</b>"
"control_reports の行数は台数よりも報告間隔で決まるためです"
"(1-5 ⑥に同じことを書いております)。", S_BODY))
story.append(Spacer(1, 2))
story.append(quote(
"<b>設問番号について</b>: 本章の設問は Q8-3 のみとなりますが、"
"<b>番号は詰めずに Q8-3 のままにしております</b>"
"(当方側の管理表がこの番号で対応を追っているためです。"
"ご了承ください)。", CW))
# ---------------------------------------------------------- 9. 軽い確認
story.append(CondPageBreak(80 * mm))
story.append(Paragraph("9. 軽い確認2件 + DDL未反映2件", S_H2))
story.append(Spacer(1, 2))
story.append(qhead(
"Q9-1. 第8回 A9-2 の robot_task_reports は、DDL 上の "
"clean_task_reports のことでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>テーブル一覧に robot_task_reports が"
"見当たりませんでした。</b>"
"<b>なお、仮にそうであっても当方の実装は "
"clean_status_histories を読んでおりますので、"
"影響はございません。表記の確認のみです。</b>", S_BODY))
story.append(Paragraph(
"<b>あわせて、clean_task_reports に "
"今後データが入る予定があるか もお教えいただけますと"
"助かります。</b>", S_BODY))
story.append(Paragraph(
"テスト環境のこの表には行がございませんが、"
"<b>そこに入っているデータは当方が用意した検証用のものですので、"
"貴社の収集処理が書くかどうかは当方では分かりません</b>。",
S_BODY))
story.append(CondPageBreak(60 * mm))
story.append(qhead(
"Q9-2. 1回の清掃の中の、区域別の内訳は取得できますでしょうか"
"(聞き直し)", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>第8回でもお伺いした件です。</b> "
"当方が実装できたのは "
"<b>「1清掃エリア(=1ロボット)につき1つの完了率」</b> までで、"
"<b>売り場・通路・レジ前といった内訳</b>ではありません。", S_BODY))
story.append(Paragraph(
"<b>取得できない場合は「できない」とお答えいただければ、"
"当方はこの機能を諦めます。</b>", S_BODY))
story.append(CondPageBreak(60 * mm))
story.append(qhead("お願い: DDL 未反映の2件(4回目)", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>急ぎません。お手すきの折で結構です。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(DDL_ROWS, [10 * mm, W - 10 * mm], [S_CODE, S_CELL]))
story.append(Spacer(1, 6))
# ---------------------------------------------------------- 10. 更新用アカウント
# ★2026-09-17(2回目)の改訂で章ごと差し替えた。すでに発行済みだった★
# ★SHOW GRANTS の出力を載せるが、IDENTIFIED BY PASSWORD のハッシュは
# 絶対に載せない(原稿も伏せてある。点検スクリプトの 10. が見ている)★
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"10. DB更新用アカウント"
"(すでに発行済みでした。ご依頼を取り下げます)", S_H2))
story.append(Paragraph(
"<b>第5回(2026-09-07)・第6回(2026-09-08)・"
"第8回(2026-09-11)から継続してお願いしておりました件です。</b>",
S_BODY))
story.append(Spacer(1, 2))
story.append(callout(
"<b>当方が接続して確認いたしましたところ、必要な UPDATE 権限は"
"すでに付与されておりました。</b>", CW))
story.append(Spacer(1, 6))
story.append(code([
"SHOW GRANTS FOR CURRENT_USER(); (2026-09-17 に当方が実行)",
"→ GRANT UPDATE ON `clean_watch`.`robot_error_reports` …",
"→ GRANT UPDATE ON `clean_watch`.`control_error_histories` …",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>当方が必要としていたのは、この2つのテーブルの"
"status / comment / responded_at / resolved_at の"
"更新のみです。</b>"
"<b>すでに満たされております。</b>", S_BODY))
story.append(Paragraph(
"<b>ご対応済みであることに気づかず、3回にわたって重ねて"
"お願いしてしまいました。当方の確認漏れです。"
"失礼いたしました。</b>", S_BODY))
story.append(Paragraph(
"<b>本件は取り下げます。"
"貴社にご対応いただくことはございません。</b>", S_BODY))
# ---------------------------------------------------------- 11. 開始時刻
# ★2026-09-17(2回目)の改訂で作り直した。
# 旧 Q11-1(実開始時刻はどこか)・11-2 の delay 集計表・SQL 枠は削除。
# Q11-1 は「この理解で実装してよいか」という確認に置き換え、
# Q11-4(よりリアルタイムに近い更新先)を新設した★
story.append(CondPageBreak(85 * mm))
story.append(Paragraph(
"11. 清掃の開始時刻と、リアルタイム性について", S_H2))
story.append(Paragraph(
"11-1. Q11-1 は、お伺いする内容を改めました", S_H3))
story.append(Paragraph(
"<b>初版では「ロボットが実際に清掃を開始した時刻は、"
"どのテーブル・どのカラムに入りますでしょうか」と"
"お伺いしておりました(Q11-1)。</b>", S_BODY))
story.append(Paragraph(
"<b>そのように申し上げた根拠は2つございましたが、"
"いずれも当方側の事情でした。</b>", S_LEAD))
story.append(Spacer(1, 2))
story.extend(numlist([
"テーブル定義書の started_at の備考が"
"「清掃スケジュールの清掃開始日時を格納する」と読め、"
"<b>予定時刻の写しのように見えた</b>こと",
"テスト環境で予定時刻との差を取ったところ全件0分だったこと — "
"<b>ただしこのデータは当方が入れた検証用のダミーで、"
"予定時刻をそのまま started_at にも入れていたものでした</b>",
]))
story.append(Spacer(1, 4))
story.append(callout(
"<b>弊社内で確認いたしましたところ、"
"clean_status_histories.started_at を実際の清掃開始時刻として"
"扱ってよい、との結論になりました。</b>", CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>誤った前提でお手を煩わせるところでした。"
"失礼いたしました。</b>"
"<b>つきましては、お伺いする内容を「どこにありますか」から"
"「この理解で実装してしまってよろしいでしょうか」に"
"改めさせていただきます。</b>", S_BODY))
story.append(CondPageBreak(100 * mm))
story.append(qhead(
"Q11-1. clean_status_histories.started_at を実際の清掃開始時刻"
"として実装を進めますが、問題ございませんでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>当方が書こうとしている実装は次のとおりです。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"実開始時刻 = clean_status_histories.started_at",
"遅延 = started_at − clean_timetables.start_at",
"出発失敗 = 予定開始時刻 + 猶予30分 を過ぎても "
"started_at が入らない",
], CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>確認させていただきたい理由</b> — テーブル定義書の "
"clean_status_histories.started_at の備考が "
"<b>「清掃スケジュールの清掃開始日時を格納する」</b> と"
"書かれており、<b>予定時刻の写しとも読めてしまう</b>ためです。",
S_BODY))
story.append(Spacer(1, 2))
story.append(callout(
"<b>もし実際には予定時刻が入る列でしたら、上の「出発失敗」は"
"永久に0件になります。</b><br/>"
"<b>エラーにもならず、画面上は「全件が時間どおりに出発した」と"
"表示され続けます。</b>", CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>そのため、実装前に一言だけ確認させていただきたい次第です。</b>",
S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q111_ROWS, [56 * mm, W - 56 * mm], [S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>貴社の収集処理の都合で「いまは予定時刻を書いているが、"
"今後は実開始時刻を書く」といった事情がございましたら、"
"その旨もお教えいただけますと助かります。</b>", S_BODY))
story.append(CondPageBreak(90 * mm))
story.append(Paragraph("11-2. 背景(残る3問の前提)", S_H3))
story.append(Paragraph(
"当方は、清掃が <b>「予定時刻から遅れて始まった/"
"始まらなかった」</b> という状態を画面に出そうと"
"しております。", S_BODY))
story.append(Paragraph(
"当方の要件定義では、清掃失敗を次のように定めております。", S_LEAD))
story.append(Spacer(1, 3))
story.append(quote(
"予定開始時刻 + <b>猶予30分</b> を経過しても"
"タスク開始が確認できない", CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"started_at で遅延そのものは出せるようになりましたが、"
"<b>次の2点は started_at だけでは埋まりません</b>。", S_LEAD))
story.append(Spacer(1, 2))
story.extend(bull([
"started_at が入らなかったとき、"
"<b>「指示すら出ていない」のか「指示は出たが動き出さなかった」のか</b>"
" が区別できません(→ Q11-2)",
"貴社のサマリに入っている「出発成功」と、当方の日次判定とで "
"<b>定義がそろっているか</b> が分かりません(→ Q11-3)",
]))
story.append(Spacer(1, 4))
# ★見出しだけがページ末に取り残されないよう、Q11-2 の帯まで入る高さを
# 確保してから見出しを置く★
story.append(CondPageBreak(95 * mm))
story.append(Paragraph(
"お伺いしたいこと(Q11-1 のほかに3問)", S_H3))
story.append(Spacer(1, 2))
story.append(qhead(
"Q11-2. clean_statuses.clean_status_trans が 1(清掃指示)で"
"止まったケースは、履歴として残りますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"定義書によれば clean_status_trans は<br/>"
"0: 清掃開始前, 1: 清掃指示, 2: 清掃中, … と推移します。", S_BODY))
story.append(Paragraph(
"<b>「1(清掃指示)で止まった = 指示は出したが動き出さなかった」"
"と読めますので、これが取れれば出発失敗を判定できます。</b>",
S_BODY))
story.append(Paragraph(
"<b>ただし clean_status_histories にこのカラムが無く、"
"clean_statuses(現在の状態)は清掃ごとに上書きされるため、"
"過去を遡ることができません。</b>", S_BODY))
story.append(Paragraph(
"<b>履歴に残る仕組みがあるかをお教えください。</b>", S_BODY))
story.append(CondPageBreak(80 * mm))
story.append(qhead(
"Q11-3. facility_summaries.launch_count / launch_ratio"
"(出発成功数・出発成功率)の計算式をお教えください", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>サマリには、貴社が計算済みの「出発成功数・出発成功率」の"
"カラムが実在しております。</b>"
"<b>つまり貴社側では「出発に成功したか」を判定できている、"
"ということかと存じます。</b>", S_BODY))
story.append(Paragraph(
"定義書の備考は <b>「補足を参照.」</b> とあるのみで、"
"<b>補足の本文を受領しておりません。</b>", S_BODY))
story.append(Paragraph(
"<b>この判定式が分かれば、当方も同じ定義で"
"日次の判定ができるようになります。</b>", S_BODY))
# Q11-4(2026-09-17 の2回目の改訂で新設)
story.append(CondPageBreak(95 * mm))
story.append(qhead(
"Q11-4. clean_status_histories.started_at のほかに、"
"より短い間隔で更新されるテーブル・カラムは"
"ございますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>新しくお伺いする設問です。</b>", S_BODY))
story.append(Paragraph(
"当方は現在、清掃の進み具合を clean_status_histories から"
"読んでおります。"
"<b>これよりも短い間隔で更新される(=よりリアルタイムに近い)"
"テーブル・カラムがあるのであれば、お教えください。</b>", S_BODY))
story.append(Paragraph(
"<b>具体的な候補を挙げてお伺いすることは、あえていたしません。</b>"
"<b>当方で「このテーブルではないか」と当たりをつけますと、"
"推測が混じります。</b>"
"<b>貴社の収集処理が書き込んでいるもののうち、"
"更新間隔が短いものをお示しいただければ、当方でそれに合わせます。</b>",
S_BODY))
story.append(Paragraph(
"<b>あわせて、差し支えなければ次も添えていただけますと"
"助かります。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q114_ROWS, [10 * mm, W - 10 * mm], [S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>無い場合は「無い」とお答えいただければ、当方は "
"clean_status_histories のままで進めます。</b>", S_BODY))
story.append(CondPageBreak(55 * mm))
story.append(Paragraph(
"11-3. ご回答がいただけるまでの当方の対応", S_H3))
story.append(Paragraph(
"<b>Q11-2 / Q11-3 のいずれも取れない場合、当方は"
"「出発失敗」と「未実施」を同じものとして扱います</b>"
"(どちらも clean_error = 1 で判定します)。", S_BODY))
story.append(Paragraph(
"<b>Q11-4 にご回答がない場合は、clean_status_histories を読む"
"現在の形のまま進めます。</b>", S_BODY))
story.append(Paragraph(
"<b>いずれも機能を止めるものではありませんので、"
"ご回答は急ぎません。</b>"
"<b>ただ、取れるのであれば取りたい、"
"という性質のものです。</b>", S_BODY))
# ------------------------------------------------ 12. DB接続経路
# 初版(2026-09-15)の「参考: テスト環境について(申し送り)」は
# ★この章に吸収した★。同じ内容を2箇所に書かないこと。
story.append(CondPageBreak(85 * mm))
story.append(Paragraph(
"12. DB接続経路について — お願い3件・確認6問", S_H2))
story.append(Paragraph(
"<b>本章は、第9回の初版(2026-09-15)の末尾に"
"「参考: テスト環境について(申し送り)」として"
"情報共有のみで記載していた内容を、"
"正式なお願いとして書き直したものです。</b>"
"<b>別便にはせず、本書に統合しております。重複はありません。</b>",
S_BODY))
story.append(Paragraph(
"<b>お願い(12-3〜12-5)と質問(12-6)を分けて"
"書いております。</b>", S_BODY))
story.append(CondPageBreak(75 * mm))
story.append(Paragraph(
"12-1. 期限 — ステージングに実データが入る前", S_H3))
story.append(Paragraph(
"<b>日付ではなく「イベント」で切っております。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(DEADLINE_ROWS,
[42 * mm, 56 * mm, W - 98 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>過ぎるとどうなるか</b> — <b>実データ投入後に同じことを"
"行う場合、次が全部必要になります。</b>", S_LEAD))
story.append(Spacer(1, 2))
story.extend(numlist([
"<b>DBアカウントの再発行</b> → <b>接続している側"
"(当方のアプリ、貴社のデータ収集処理・集計バッチ)を"
"いったん全部止めて差し替える必要があります</b>",
"<b>TLS の有効化</b> → <b>サーバ再起動を伴う可能性があり、"
"夜間の清掃データ収集を止められません</b>"
"(D-Watch は<b>夜間に自動実行された清掃を翌朝確認する運用</b>です)",
"<b>接続元IPの制限</b> → <b>実データが流れている状態で"
"絞りますと、漏れた接続元からデータ収集が止まります</b>",
]))
story.append(Spacer(1, 4))
story.append(callout(
"<b>つまり「止める調整」が発生します。"
"いまであれば、その調整が要りません。</b><br/>"
"<b>だからこそ、実データが入る前にお願いしております。</b>", CW))
story.append(CondPageBreak(95 * mm))
story.append(Paragraph(
"12-2. 実測値(いつ・誰が・どう測ったか)", S_H3))
story.append(Paragraph(
"<b>推測ではありません。実測です。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(MEASURE_META_ROWS, [30 * mm, W - 30 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(table(TESTENV_ROWS, [46 * mm, 26 * mm, W - 72 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(quote(
"<b>本章の作成にあたり、テストDBへの再接続は行っておりません"
"(意図的です)</b>。"
"<b>参照専用のアカウントではありますが、接続条件が現在も"
"揃っているかが不明なため</b>、"
"<b>上記は「2026-09-13 に実測された値」です</b>。<br/>"
"<b>すでにご対応済みでしたら、12-3〜12-5 は"
"読み飛ばしてください。</b>", CW))
story.append(Spacer(1, 6))
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("<b>なぜ問題か</b>:", S_LEAD))
story.append(Spacer(1, 2))
story.extend(bull([
"<b>have_ssl = DISABLED と require_secure_transport = OFF が"
"重なりますと、接続は必ず平文になります</b>。"
"<b>クライアント側で「TLSを使う」と指定しても、"
"サーバーが提供していないため暗号化されません</b>",
"<b>接続元が %(どこからでも)のため、"
"経路は公開IP上にあります</b>",
"この2つが重なりますと、"
"<b>DB接続の資格情報が経路上で保護されていない状態</b>"
"になります",
"<b>現在は検証用データのみのため実害はありません。"
"問題になるのは実データが入った後です</b>",
]))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>なお、このサーバーは弊社(DFA)が契約し、"
"貴社に構築・保守をお願いしているものと認識しております。</b>"
"<b>そのため、実施のご依頼は貴社宛といたしました。"
"この認識に誤りがございましたら、その旨をお知らせください。</b>",
S_BODY))
story.append(CondPageBreak(70 * mm))
story.append(Paragraph(
"12-3. お願い1: 接続元の制限(本章の中で最優先です)", S_H3))
story.append(Paragraph(
"参照用アカウント@% の % を、<b>固定IP</b>"
"(または閉域ネットワーク内のみ)に"
"絞っていただけますでしょうか。", S_BODY))
story.append(Paragraph("なぜ TLS より先にお願いするのか", S_H4))
story.append(Paragraph(
"当方が利用しているDB接続用のライブラリに、"
"<b>「TLS を有効にしても、条件によっては接続時の資格情報が"
"経路上に平文で出てしまう」という既知の問題</b>が"
"報告されております"
"(<b>当方が 2026-09-16 に自動検査ツールで確認した実測です</b>)。",
S_BODY))
story.append(Paragraph(
"<b>これは弊社内製アプリケーション側の事情ですので、"
"どう解くかは弊社で判断いたします。"
"貴社にご対応をお願いするものではありません。</b>", S_BODY))
story.append(Paragraph(
"<b>ただし1点だけ、貴社へのお願いの優先順に関係いたします。</b>"
"この問題がある間は、"
"<b>TLS を有効にしていただいても資格情報が保護されない"
"可能性が残ります</b>。", S_BODY))
story.append(Spacer(1, 2))
story.append(callout(
"<b>そのため当方としては、TLS の有効化よりも先に"
"「経路そのものを閉じる」=接続元IPの制限のほうが効くと"
"考えております。</b><br/>"
"<b>恐れ入りますが、12-4(TLS)より 12-3(接続元の制限)を"
"先にご検討いただけますと幸いです。</b>", CW))
story.append(CondPageBreak(45 * mm))
story.append(Paragraph("12-4. お願い2: TLS の有効化", S_H3))
story.append(Paragraph(
"have_ssl を有効にし、可能であれば "
"require_secure_transport = ON に"
"していただけますでしょうか。", S_BODY))
story.append(Paragraph("<b>12-3 の後で構いません。</b>", S_BODY))
story.append(Paragraph(
"<b>なお 12-6 の Q12-4 が YES(VMとDBを閉域で結べる)と"
"なる場合、本項は不要になる可能性があります。</b>", S_BODY))
story.append(CondPageBreak(90 * mm))
story.append(Paragraph(
"12-5. お願い3: 本番用DBアカウントの新規発行", S_H3))
story.append(Spacer(1, 2))
story.append(callout(
"<b>本番接続に使うアカウントは、現在のテスト用とは別のものを"
"新規に発行していただけますでしょうか。</b><br/>"
"<b>ユーザー名・パスワードとも</b>お願いいたします。", CW))
story.append(Spacer(1, 6))
story.append(Paragraph("理由", S_H4))
story.extend(bull([
"<b>過去に、実在のDBユーザー名が当方リポジトリのコミットに"
"含まれたことがあります</b>"
"(<b>2026-09-10 のコミットで除去済みですが、"
"Git の履歴には残っております</b>)。"
"リポジトリは非公開ですが、"
"<b>アクセス権を持つ全員が履歴を読めます</b>",
"<b>パスワードは当該コミットには含まれておりません</b>"
"(架空の値のみでした)。"
"<b>問題はユーザー名が既知になっている点です</b>",
"<b>ユーザー名が既知である以上、12-2 の 参照用アカウント@%"
"(どこからでも接続可)と組み合わさりますと、"
"推測の手間が減ります</b>",
"<b>あわせて、このアカウントの認証情報は、受け渡しの過程で"
"平文のまま当方側の記録に残っております</b>。"
"この点からも、"
"<b>本番用は別の値を新規に発行していただくのが確実</b>と"
"考えております",
]))
story.append(Spacer(1, 4))
story.append(Paragraph("お願いしていないこと", S_H4))
story.extend(bull([
"<b>Git 履歴の書き換えはお願いしておりません</b>"
"(当方でも実施いたしません)。"
"<b>「発行し直す」ほうで解いていただきたい、"
"というお願いです</b>",
"アカウントの発行元は貴社ですので、"
"<b>再発行は貴社にしかお願いできません</b>",
]))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>タイミングは 12-1 のとおり、"
"ステージングに実データが入る前です。</b>", S_BODY))
story.append(CondPageBreak(90 * mm))
story.append(Paragraph(
"12-6. 確認: 本番構成について(6問)", S_H3))
story.append(Paragraph(
"<b>ここからは質問です。お願いではありません。</b>"
"<b>Q12-1 以外は YES / NO でお答えいただけます。</b>", S_BODY))
story.append(Paragraph("なぜお伺いするのか", S_H4))
story.append(Paragraph(
"<b>12-3〜12-5 の解き方(=「経路を閉じる」)が"
"成立するかどうかが、本番の構成にかかっております</b>。",
S_LEAD))
story.append(Spacer(1, 2))
story.extend(bull([
"VM と DB が同一の閉域ネットワークに入れるのであれば → "
"<b>12-3・12-4 は「初めから起きない」</b>",
"公開IP越しに接続する構成であれば → "
"<b>現在の状態がそのまま本番に持ち越されます</b>",
]))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>当方は「閉じた構成になる見込み」と考えておりますが、"
"これは当方の読み取りであり、"
"明示の確認をいただいた記録がございません。</b>"
"<b>推測のまま本番に進みますと、"
"12-1 の「作り直し」に直結いたします。</b>", S_BODY))
story.append(CondPageBreak(85 * mm))
story.append(Paragraph("当方の現在の理解と、その根拠", S_H4))
story.append(Spacer(1, 2))
story.append(table(UNDERSTAND_ROWS,
[8 * mm, 56 * mm, 62 * mm, W - 126 * mm],
[S_CELL, S_CELL, S_CELL_S, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>すなわち、貴社のご回答は「なぜ MariaDB なのか」の"
"ご説明であり、「本番の構成はこうなります」という"
"ご説明ではありませんでした。当方が読み取っただけです。</b>",
S_BODY))
story.append(Paragraph(
"<b>そこで、読み取りが合っているかどうかを"
"確認させてください。</b>", S_BODY))
story.append(CondPageBreak(75 * mm))
story.append(qhead(
"Q12-1. アプリサーバー(VM)とDBサーバーは、"
"それぞれどちらが用意する想定でしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>当方の中で理解が揃っておりません。"
"まずここを確定させていただきたく存じます。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q121_ROWS, [12 * mm, 70 * mm, W - 82 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>弊社の記録には「アプリ側は弊社」と読める記述と"
"「貴社に用意していただく」と読める記述が"
"混在しております。</b>"
"<b>どちらが正かは貴社のご認識に合わせますので、"
"お教えください。</b>", S_BODY))
story.append(CondPageBreak(60 * mm))
story.append(qhead(
"Q12-2. 本番のDBは、さくらインターネットの"
"データベースアプライアンス"
"(=VM上に自前で構築した MariaDB ではない)で"
"間違いないでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph("YES / NO", S_BODY))
story.append(CondPageBreak(50 * mm))
story.append(qhead(
"Q12-3. 本番のアプリ(Node.js)は、"
"さくらのクラウドの VM に載せる想定で"
"間違いないでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph("YES / NO", S_BODY))
story.append(CondPageBreak(50 * mm))
story.append(qhead(
"Q12-4. その VM と DBアプライアンスは、"
"同一の閉域ネットワーク(プライベート接続)で"
"結べますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph("YES / NO", S_BODY))
story.append(CondPageBreak(50 * mm))
story.append(qhead(
"Q12-5. Q12-4 が YES の場合、"
"DB側で公開IPからの接続を完全に閉じることは"
"可能でしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph("YES / NO / 条件つき", S_BODY))
story.append(CondPageBreak(60 * mm))
story.append(qhead(
"Q12-6. Q12-4 が NO の場合(公開IP越しの接続になる場合)、"
"TLS を有効にできますでしょうか", CW))
story.append(Spacer(1, 4))
story.append(Paragraph("YES / NO / 該当なし", S_BODY))
story.append(Spacer(1, 2))
story.append(Paragraph(
"<b>NO や「条件つき」の場合は、差し支えなければ理由を"
"1行だけ添えていただけますと助かります。</b>"
"<b>当方はそれを前提に、12-3〜12-5 のお願いの内容を"
"組み直します。</b>", S_BODY))
story.append(CondPageBreak(70 * mm))
story.append(Paragraph(
"12-7. 当方(DFA)側で済ませたこと", S_H3))
story.append(Paragraph(
"<b>丸投げではない、というご説明のために書いております。</b>",
S_BODY))
story.append(Paragraph(
"2026-09-13 に当方のアプリコードを棚卸しいたしました。"
"<b>危険な箇所は1件も見つかっておりません</b>"
"(生SQL 0件 / 並べ替えのカラム名は固定リテラル1箇所 / "
"where にユーザー入力なし / "
"エラー本文に内部情報を載せている箇所 0件 / "
"DBへの書き込み 0件 / リポジトリ内に実在の認証情報なし)。",
S_BODY))
story.append(Paragraph(
"あわせて、生SQL禁止の静的解析ルール化・"
"パスパラメータ検証の全体一致化・"
"CSVの数式インジェクション対策・"
"本番前セキュリティ確認チェックリストの整備・"
"DBユーザー名の既定値をコードから除去する対応を"
"済ませております。", S_BODY))
story.append(Spacer(1, 2))
story.append(callout(
"<b>問題があるのはコードではなく、DBへ至る「経路」のほうです。"
"そして経路は、当方のコードでは閉じられません。</b><br/>"
"<b>そのため本章の3件を、正式にお願いとしてお渡しいたします。</b>",
CW))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>ご負担や副作用のご懸念があれば、その旨をお知らせください。"
"当方で別の手を検討します。</b>", S_BODY))
# ---------------------------------------------------------- おわりに
story.append(CondPageBreak(60 * mm))
story.append(Paragraph(
"<b>以上です。ご確認のほど、よろしくお願いいたします。</b>",
S_BODY))
# ★Q11-1 は「取り下げ」ではない。取り下げた項目の列挙に混ぜないこと
# (点検スクリプトの 12. の FORBIDDEN が、混ざった形の残存を見る)★
story.append(Paragraph(
"<b>本改訂で取り下げた項目</b>: <b>5章 Q5-1</b>"
"(面積の単位。弊社で㎡と確定)/ <b>10章</b>"
"(DB更新用アカウント。すでに発行済みでした)/ "
"<b>2章 Q2-2</b>。", S_BODY))
story.append(Paragraph(
"<b>取り下げではなく、お伺いする内容を改めた項目</b>: "
"<b>11章 Q11-1</b> — 「実際の清掃開始時刻はどこにありますか」→ "
"<b>「started_at を使って実装してよろしいでしょうか」</b>。"
"<b>こちらはご回答をお願いいたします"
"(A / B の一言で結構です)。</b>", S_BODY))
# ★この文は Q11-1(本書で唯一「ご回答をお願いします」と書いた設問)の
# 段落の直後に来る。「いずれもご対応は不要です」と書くと、せっかくの
# 回答依頼を打ち消して読めてしまう。★対象を「取り下げた項目」に限定すること★
# (点検スクリプトの 12. の FORBIDDEN が旧文言の復活を見る)★
story.append(Paragraph(
"<b>上の「本改訂で取り下げた項目」については、"
"ご対応は不要です。</b>"
"<b>あわせて、テスト環境の「行の中身」を根拠にしていた指摘を"
"取り下げております(冒頭の「おことわり」をご覧ください)。</b>",
S_BODY))
story.append(Spacer(1, 2))
story.append(callout(
"<b>1章(索引)はご対応中と伺いましたので、依頼を取り下げ、"
"参考情報に改めております。</b><br/>"
"<b>ご対応が完了しましたら、その旨をお知らせください。"
"当方で EXPLAIN を測り直します。</b>", CW))
story.append(Spacer(1, 6))
story.append(callout(
"<b>12章は、期限が「ステージングに実データが入る前」という"
"性質のものです。</b><br/>"
"<b>まずは 12-6 の Q12-1〜Q12-6 だけでも先にお答えいただけますと、"
"12-3〜12-5 のお願いの内容を確定できます。</b>", CW))
# ---------------------------------------------------------- ご回答欄
# ご回答欄は印刷してご記入いただく想定なので、必ず新しいページから始める
story.append(PageBreak())
story.append(Paragraph("ご回答欄", S_TITLE))
story.append(Paragraph(
"<b>この表に一言だけご記入いただく形でも、"
"下の自由記入欄にまとめてご記入いただく形でも結構です。</b>"
"<b>選択肢のある設問は、記号(A / B / C …)に○を付けて"
"いただくだけで結構です。</b>", S_LEAD))
story.append(Spacer(1, 4))
story.append(form_table(ANSWER_ROWS, [18 * mm, 100 * mm, W - 118 * 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, 6))
story.append(Paragraph("引き続きよろしくお願いします。", S_NOTE))
doc.build(story)
print("生成:", OUT)
if __name__ == "__main__":
build()