# -*- coding: utf-8 -*-
"""
TeamWorks 宛 確認事項(第7回)を PDF にする。
第1回〜第6回(make_ddl_review_pdf.py / make_teamworks_qa2_pdf.py 〜
make_teamworks_qa6_pdf.py)の体裁をそのまま踏襲した続編。
A4 縦 / reportlab / 日本語フォント埋め込み / ヘッダー罫線とページ番号。
フォントは BIZ UDGothic(日本語フォント)を埋め込む。
中国語向けフォントは漢字の字形が異なるため使わない。
フォールバックにも混ぜないこと。
BIZ UDGothic は等幅(半角は 0.5em 固定)なので、SQL の枠にもこのフォントを使う。
コードブロック内の空白は に置換して桁を保持する。
原稿は docs/handoff/TeamWorks確認事項_第7回_20260910.md。
文言・件数・表の内容は原稿から変更していない。
★2026-09-10 の改訂(送付直前)★
1. 旧第4部(ダンプ元サーバが MariaDB に変わっている件)を削除した。
送付前に「本番サーバは MariaDB です」とご回答をいただけたため。
謝意と当方の対応状況は第1部 1-4 に移した。旧5→4、旧6→5 に繰り上げ。
2. 第3部の依頼から B-1(clean_timetable_id の外部キー/索引)と
A-1(主キー)を落とし、A-2 / A-3b / B-2 / B-3b の4件だけにした。
取り下げの経緯は第1部 1-5(共有)に書いた。
理由: prismaSource.cleanLogs()(lib/db/index.ts:186-237)が
cleanTimetable / cleanStatusHistory / robotErrorReport を
WHERE 句なしの全件取得で読み、取得後に Map でメモリ結合しているため、
clean_timetable_id で「検索」しておらず索引が効く場所が無い。
3. 質問は3件(接続先 / DDL の未反映4件 / precheck の値域)。
第5部(面積)は質問を取り下げて共有のみにした。
★列幅の決め方★
wordWrap="CJK" は ASCII の語中でも改行する。そのため
`robot_task_versions.clean_schedule_id` /
`security_status_histories.reported_at` /
`clean_watch_20260909.sql` / `clean_status_histories.id` /
`facility_contacts.email` / `@prisma/adapter-mariadb` /
`5.5.5-10.11.13-MariaDB-log` / `clean_timetables.precheck`
のような長い識別子は
(1) 行頭に来るよう <br/> で明示的に改行してから置く
(2) 列の内寸(列幅 - 左右パディング 12pt)が識別子の幅
(BIZ UDGothic は半角 0.5em なので フォントサイズ × 文字数 ÷ 2)
を上回るように列幅を決める
の2点で語中改行を避けている。check_teamworks_qa7_pdf.py の
「4. 識別子の語中改行」がこれを機械的に検査する。
"""
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, 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_確認事項_第7回_20260910.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) # 目立たせたい一文を囲む枠の中身
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),
]
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 code(lines, width):
"""SQL などを等幅フォントの枠に入れる。空白は にして桁を保つ。"""
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 num(items, start=1):
"""「1. 」形式の番号付きリスト。"""
return [Paragraph("%d. %s" % (i, t), S_LIST)
for i, t in enumerate(items, start)]
def bull(items):
"""「・」形式の箇条書き。"""
return [Paragraph("・%s" % t, S_BULL) for t in items]
# ---------------------------------------------------------------- 表データ
# 前文: 5部構成(旧第4部の MariaDB は削除済み。改訂1)
SUMMARY_ROWS = [
["#", "内容", "優先度"],
["1. 照合結果のご共有",
"いただいたDDLを当日中に照合しました。<br/>"
"MariaDB の件と、<b>依頼2件の取り下げ</b>もこちらです",
"<b>回答不要</b>"],
["2. 接続先<br/>(ホスト名・ポート)",
"<b>第6回からの継続。あとはこれだけです</b>", "<b>最優先</b>"],
["3. DDL の未反映項目<br/>(4件)",
"採番・桁数・DEFAULT・索引の食い違いです", "<b>お手すきの折に</b>"],
["4. precheck に入る<br/>値の範囲", "反映時期と既存データの扱い", "中"],
["5. 面積の型変更について", "<b>共有のみ。質問はありません</b>",
"<b>回答不要</b>"],
]
# 1-4. 本番が MariaDB であることのご教示への謝意と、当方の対応状況
MARIA_ROWS = [
["#", "内容"],
["1", "<b>アプリの実装に生の SQL は1件もありません。</b>"
"すべて Prisma のクエリビルダ経由ですので、"
"<b>現時点で対応が必要な箇所はありません</b>"],
["2", "<b>DBクライアントのドライバも、すでに MariaDB 用のもの"
"(@prisma/adapter-mariadb)を使っております</b>"],
["3", "<b>「どうしても MariaDB 特有の DML を使いたい場合は"
"その箇所をお知らせください」とのご指示、承知しました。</b>"
"<b>その必要が生じた際にご連絡します。</b>"],
]
# 1-2. 率のカラムの型変更
RATE_TYPE_ROWS = [
["変更前", "変更後"],
["tinyint unsigned(整数)", "<b>double(4,1)</b>(小数第一位)"],
]
RATE_COL_ROWS = [
["テーブル", "列"],
["customer_summaries /<br/>facility_summaries",
"operation_ratio / launch_ratio /<br/>clean_ratio / error_ratio(各4列)"],
["clean_status_histories /<br/>clean_statuses /<br/>clean_task_reports /<br/>"
"robots /<br/>section_robot_statuses",
"clean_ratio(各1列)"],
]
# 2. 接続先: あわせて確認させてください(第6回と同じ3件)
CONN_ROWS = [
["#", "確認したいこと", "補足"],
["1", "<b>接続元IPの制限はありますか</b>",
"ある場合、<b>当方の接続元をお伝えします</b>のでお知らせください"],
["2", "<b>SSL/TLS 接続は必須でしょうか</b>",
"必須の場合、<b>証明書の受け渡し方法</b>もお教えください"],
["3", "<b>接続先はステージング環境でしょうか、本番環境でしょうか</b>",
"当方の扱い(参照のみ / 負荷のかけ方)を合わせたいためです"],
]
# 2. 当方の準備状況
PREP_ROWS = [
["#", "済んでいること"],
["1", "<b>いただいたDDLからローカルにDBを作成し、"
"実DDLと列レベルで完全一致させました</b>"
"(差分0件を機械的に確認済み)"],
["2", "<b>prisma db pull でスキーマを取り込み済みです</b>"
"(今回の23列の型変更も反映済み)"],
["3", "<b>施設マスタ・スケジュール・D-Secログ・D-Secアラート・"
"清掃詳細ログの5画面が、"
"そのローカルDB経由で動くところまで実装済みです</b>"],
]
# 3. DDL の未反映項目(4件)
# B-1(索引・外部キー)と A-1(主キー)は当方の実装方針で必要が無くなったため
# 依頼から外し、取り下げの経緯を第1部 1-5 に移した(改訂2)。
DDL_ROWS = [
["#", "お願いしていた内容", "<b>09-09版の実測</b>"],
["A-2", "履歴テーブルの id を<br/>AUTO_INCREMENT で採番",
"clean_status_histories.id /<br/>clean_timetable_histories.id とも<br/>"
"bigint(20) unsigned NOT NULL。<br/>AUTO_INCREMENT 無し"],
["A-3b", "facility_contacts.email を<br/>varchar(100) に",
"varchar(255) DEFAULT NULL のまま<br/>"
"(contact_type は tinyint(3) unsigned<br/>で反映済みです)"],
["B-2", "robot_task_versions.clean_schedule_id<br/>にインデックス",
"KEY は<br/>robot_task_versions_robot_id_foreign<br/>(robot_id)のみ。<br/>"
"clean_schedule_id のインデックスは0件"],
["B-3b", "security_status_histories.reported_at<br/>に DEFAULT",
"timestamp NOT NULL、<br/>DEFAULT なしのまま"],
]
# 3-1. 4件の性質(設計判断ではなく具体的なズレ)
GAP_ROWS = [
["#", "食い違いの中身", "補足"],
["A-2", "<b>採番の仕組み</b>",
"id に AUTO_INCREMENT が無く、<br/>"
"<b>誰が採番するのかが決まっていない状態</b>です"],
["A-3b", "<b>桁数</b>",
"「email は varchar(100) とします」と<br/>"
"ご回答いただいた桁数が、<br/>varchar(255) のままです"],
["B-2", "<b>索引の有無</b>",
"<b>定義書で fk と記載されている列</b>ですが、<br/>"
"実DDLには外部キーも索引もありません"],
["B-3b", "<b>DEFAULT の有無</b>",
"timestamp NOT NULL に<br/>DEFAULT 句がありません"],
]
# 4. precheck
PRECHECK_ASK_ROWS = [
["#", "内容"],
["1", "<b>この理解で合っていますでしょうか</b>"],
["2", "<b>いつ頃反映される見込みでしょうか</b>"],
["3", "<b>既存のデータはどうなりますでしょうか</b> — "
"1 / 2 のまま残るのか、60 / 30 に更新されるのか"],
["4", "<b>新旧の値が混在する期間はありますでしょうか</b>"],
["5", "<b>0 以外に予約値はありますでしょうか</b>"],
]
# 5. 面積の型変更
AREA_TYPE_ROWS = [
["変更前", "変更後"],
["decimal(12,6) unsigned",
"<b>double(12,6)</b>(customer_summaries /<br/>"
"facility_summaries は double(14,6))"],
]
AREA_COL_ROWS = [
["テーブル", "列"],
["clean_status_histories /<br/>clean_statuses /<br/>clean_task_reports",
"task_area / clean_area"],
["customer_summaries /<br/>facility_summaries", "task_area / clean_area"],
]
AREA_HARM_ROWS = [
["#", "理由"],
["1", "<b>viewer に面積を表示する箇所は1つもありません</b>"
"(画面に出るのは % だけです)"],
["2", "<b>面積は完了率の計算材料としてのみ使う想定でしたが、"
"率を小数でいただけるようになったため、"
"当方で計算し直す必要も無くなりました</b>"],
]
# おわりに: 優先順(3件)
PRIO_ROWS = [
["順", "項目", "理由"],
["<b>1</b>", "<b>接続先<br/>(ホスト名・ポート)</b>",
"<b>アカウントはいただいておりますので、これだけで接続できます</b>。"
"9月中のデモを実データで行えるかがここで決まります"],
["2", "precheck の値の範囲と<br/>反映時期",
"新旧混在の期間があるかどうかを特に知りたいです"],
["3", "DDL の未反映4件<br/>(A-2 / A-3b / B-2 / B-3b)",
"お手すきの折で結構です。<br/>当方の開発は止まっていません"],
]
def build():
doc = BaseDocTemplate(
OUT, pagesize=A4,
leftMargin=18 * mm, rightMargin=18 * mm,
topMargin=18 * mm, bottomMargin=20 * mm,
title="D-Watch 確認事項(第7回)",
author="DFA Robotics",
subject="09-09版DDLの照合結果のご共有と、接続先のご連絡のお願い",
)
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
CW = W - 6 * mm
story = []
# ---------------------------------------------------------- 表題と前文
story.append(Paragraph("確認事項(第7回)", S_TITLE))
story.append(Paragraph(
"宛先: TeamWorks(DB開発側) 作成日: 2026-09-10 "
"作成者: DFA Robotics", S_SUB))
story.append(Spacer(1, 9))
story.append(Paragraph(
"<b>最新のDDL(clean_watch_20260909.sql)をお送りいただき、"
"ありがとうございました。</b>"
"(2026-09-09 受領。第6回でお願いしていた項目です)", S_BODY))
story.append(Paragraph(
"また、<b>率のカラムを小数第一位まで持つ形(double(4,1))に"
"変更いただき、ありがとうございました。</b>"
"<b>当方からお願いしていた内容が、そのまま反映されておりました。</b>"
"これで当方の判定処理が正しく行えるようになります(詳細は第1部)。",
S_BODY))
story.append(Paragraph("本書は次の5部構成です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(SUMMARY_ROWS,
[46 * mm, W - 74 * mm, 28 * mm],
[S_CELL, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>今回は、お伺いすることを3件に絞りました。</b>"
"(<b>接続先 / DDL の未反映4件 / precheck の値域</b>)"
"<b>お伺いする件数が少ないほど、"
"早くお返事をいただけると考えたためです。</b>"
"<b>第1部と第5部は当方からの共有ですので、回答は不要です。</b>",
S_BODY))
# ---------------------------------------------------------- 1. 照合結果
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("1. 照合結果のご共有(回答不要です)", S_H2))
story.append(Paragraph(
"第6回で「<b>受領しましたら、その日のうちに照合してご報告します</b>」と"
"お約束していた件です。"
"<b>2026-09-09 に受領し、当日中に照合を完了しました。</b>", S_BODY))
story.append(Paragraph("1-1. 照合方法", S_H3))
story.append(Paragraph(
"<b>ローカルの MySQL に読み込み、09-07版・09-02版と "
"information_schema で機械的に差分を取りました。</b>"
"目視ではなく、列名・型の両方を双方向で突き合わせています。", S_BODY))
story.append(Paragraph(
"1-2. 率のカラムが小数第一位になっていました(13列)", S_H3))
story.append(Spacer(1, 2))
story.append(table(RATE_TYPE_ROWS, [56 * mm, W - 56 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 6))
# 5行のセルがあるので、導入文ごと表が割れないよう高さを確保してから置く
story.append(CondPageBreak(50 * mm))
story.append(Paragraph("対象は <b>13列</b> です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(RATE_COL_ROWS, [58 * mm, W - 58 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>これで当方の判定が正しく行えるようになりました。"
"ありがとうございます。</b>", S_BODY))
story.append(Paragraph(
"当方は清掃完了率から4段階(◎○△×)の判定を行っており、"
"<b>その境界値は 90.0 / 90.1 / 70.0 / 70.1 / 10.0 / 9.9 と、"
"すべて小数第一位までです。</b>", S_BODY))
story.append(Paragraph(
"整数で格納されていると、"
"<b>例えば 89.6% が 90 に丸められて格納され、"
"本来「○」であるべきものが「◎」になってしまう</b>"
"という問題がありました。"
"<b>今回の変更で、この問題は解消しました。</b>", S_BODY))
story.append(Paragraph("1-3. テーブル・列の増減はありませんでした", S_H3))
story.append(Paragraph(
"<b>CREATE TABLE は 39 で、09-07版・09-02版と同数です。</b>"
"テーブル名の集合も完全に一致しました。"
"<b>列の増減もありません。</b>"
"今回の変更は型(と一部の DEFAULT)だけでした。", S_BODY))
story.append(Paragraph(
"なお、面積のカラムも decimal から double に変わっておりましたので、"
"この点だけ第5部で申し添えます"
"(<b>当方に実害はありません</b>)。", S_BODY))
# 1-4. MariaDB の件(送付前にご回答をいただけたので、質問から共有に変えた)
story.append(CondPageBreak(70 * mm))
story.append(Paragraph(
"1-4. 本番が MariaDB であることをお教えいただき、"
"ありがとうございました", S_H3))
story.append(Paragraph(
"<b>本書をお送りする前に、本番サーバが MariaDB であることを"
"お教えいただきました。ありがとうございます。</b>", S_BODY))
story.append(Paragraph(
"(09-09版のダンプヘッダが 5.5.5-10.11.13-MariaDB-log に"
"なっており、09-07版までの MySQL 8.0.41 と異なっておりましたので、"
"お伺いしようとしていた件です。"
"<b>先にご回答いただけましたので、本書からは質問を取り下げました。</b>)",
S_BODY))
story.append(Paragraph("当方の対応状況を申し添えます。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(MARIA_ROWS, [10 * mm, W - 10 * mm],
[S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"なお、<b>当方のローカルの検証環境は MySQL です</b>ので、"
"<b>今後もし生の SQL を書く場合は、"
"MySQL と MariaDB の双方で通る書き方に注意いたします。</b>", S_BODY))
# 1-5. B-1 / A-1 の取り下げ(改訂2)
story.append(CondPageBreak(75 * mm))
story.append(Paragraph(
"1-5. これまでお願いしていた2件(外部キー/索引・主キー)を、"
"今回の依頼から外します", S_H3))
story.append(Paragraph(
"第5回・第6回でお願いしておりました "
"<b>B-1(clean_timetable_id の外部キー/索引)</b> と "
"<b>A-1(robot_error_reports の主キー)</b> について、"
"<b>当方の実装が固まり、いずれも当方側で支障が無いことが"
"はっきりしました。</b>"
"<b>今回の依頼から外させてください。</b>", S_BODY))
story.append(Paragraph(
"B-1(clean_timetable_id の外部キー・索引)", S_H4))
story.append(Paragraph(
"<b>当方は、これらのテーブルを全件取得し、"
"アプリ側で突き合わせる実装にしました。</b>", S_LEAD))
story.extend(bull([
"clean_timetables / clean_status_histories /<br/>"
"robot_error_reports を、それぞれ "
"<b>WHERE 句なしの全件取得</b>で読み出しています",
"読み出したあと、clean_timetable_id をキーにした対応表を"
"メモリ上に作り、そこで結合しています",
"<b>つまり、clean_timetable_id で「検索」をしておりません。</b>"
"<b>索引は WHERE 句で絞り込むときに効くものですので、"
"この実装では使われません。</b>",
]))
story.append(Spacer(1, 2))
story.append(Paragraph(
"<b>この実装はすでに完成しており、外部キーが無いまま"
"清掃ログの画面が動いております。</b>", S_BODY))
story.append(Paragraph(
"また、<b>どの索引が必要かは、実際のクエリの形と、"
"実データの量・分布で決まります。</b>"
"<b>当方はまだ実データを1行も見ておらず、"
"絞り込みのクエリも書いておりません。</b>"
"<b>この状態で特定の索引を名指しでお願いするのは、"
"当方の根拠が薄いと考え直しました。</b>", S_BODY))
story.append(Paragraph(
"<b>したがって、現時点で支障はありません。</b>"
"<b>将来クエリの形が変わって索引が必要になりましたら、"
"その時点で具体的なクエリを添えて、"
"あらためてご相談します。</b>", S_BODY))
story.append(Paragraph(
"過去の確認書では外部キーの追加をお願いしておりましたが、"
"<b>当方の実装方針が定まった結果、当方の見立てが変わりました。</b>"
"お手数をおかけしました。", S_BODY))
story.append(CondPageBreak(40 * mm))
story.append(Paragraph(
"A-1(robot_error_reports の主キー)", S_H4))
story.append(Paragraph(
"error_id を識別子として使う方針は第4回のご回答でいただいており、"
"当方はその前提で実装しております。", S_BODY))
story.append(Paragraph(
"<b>主キーが無くても当方の実装は動いております。"
"error_id に UNIQUE がありますので、一意性は担保されています。</b>"
"<b>当方の実装で支障が無い以上、"
"DDL の変更をお願いする根拠がありませんので、"
"こちらも依頼から外します。</b>", S_BODY))
# ---------------------------------------------------------- 2. 接続先
story.append(CondPageBreak(75 * mm))
story.append(Paragraph(
"2. 接続先(ホスト名・ポート)をお教えいただけますでしょうか — 最優先",
S_H2))
story.append(Paragraph(
"<b>第6回でもお願いした件で、恐れ入りますが繰り返しになります。</b>",
S_BODY))
story.append(Paragraph(
"<b>参照用アカウントは 2026-09-08 にいただいておりますので、"
"あとは接続先だけです。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(callout(
"接続先のホスト名(または IP アドレス)と、ポート番号", CW))
story.append(Spacer(1, 6))
story.append(Paragraph("をお教えいただければ接続できます。", S_BODY))
story.append(Paragraph("あわせて確認させてください", S_H3))
story.append(Spacer(1, 3))
story.append(table(CONN_ROWS, [10 * mm, 60 * mm, W - 70 * mm],
[S_CODE, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
# 見出しと2つの箇条書きが改ページで割れないように高さを確保する
story.append(CondPageBreak(28 * mm))
story.append(Paragraph("なぜ急いでお願いしているか", S_H3))
story.extend(bull([
"<b>9月中に PPIH 様向けにデモを実施する可能性があります。</b>",
"<b>実データでお見せできるかどうかが、この接続情報で決まります。</b>",
]))
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("当方の準備状況", S_H3))
story.append(Paragraph(
"<b>接続情報さえいただければ、すぐに使える状態になっております。</b>",
S_LEAD))
story.append(Spacer(1, 3))
story.append(table(PREP_ROWS, [10 * mm, W - 10 * mm],
[S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>接続先をいただければ、接続文字列を差し替えるだけです。</b>", S_BODY))
story.append(Paragraph(
"また第6回でお伝えしたとおり、接続でき次第、"
"error_type の値の分布・robots.status の実出現値・"
"週次サマリの week の値域など "
"<b>当方で数えられるものは当方で確認します</b> ので、"
"<b>その分だけ貴社への照会が減ります。</b>", S_BODY))
# ---------------------------------------------------------- 3. 未反映項目
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"3. DDL の未反映項目(4件)を、"
"お手すきの折にご対応いただけますでしょうか", S_H2))
story.append(Paragraph(
"<b>お忙しいところ恐縮です。3回目のお願いになります。</b>"
"第5回・第6回でお願いしていた項目のうち、"
"<b>4項目が 09-09版でも未反映でした。</b>", S_BODY))
story.append(Paragraph(
"<b>今回のDDLは率と面積の型変更が主眼だったかと存じますので、"
"これらは単に今回のスコープに入っていなかっただけかと思っております。</b>"
"状況だけ共有させてください。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(DDL_ROWS, [12 * mm, 68 * mm, W - 80 * mm],
[S_CODE, S_CODE, S_CELL], head_color=WN))
story.append(Spacer(1, 6))
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"3-1. この4件は、当方の設計上のお願いではなく、具体的なズレです",
S_H3))
story.append(Paragraph(
"1-5 で取り下げた2件(外部キー/索引・主キー)は、"
"<b>「DBの設計をどうするか」という判断を伴うお願い</b>でした。"
"<b>当方の実装方針が固まり、必要がなくなりましたので"
"取り下げました。</b>", S_BODY))
story.append(Paragraph(
"<b>一方、この4件は性質が違います。</b>"
"<b>いずれも、採番・桁数・DEFAULT・索引の有無という、"
"具体的で機械的な食い違いです。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(GAP_ROWS, [12 * mm, 30 * mm, W - 42 * mm],
[S_CODE, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>いずれも「どちらが正なのか」が当方には判断できない箇所です。</b>"
"<b>意図的なものでしたら、"
"その旨だけお知らせいただければ結構です。</b>", S_BODY))
story.append(Paragraph("3-2. お手すきの折で結構です", S_H3))
story.append(Paragraph(
"<b>いずれも当方の開発を止めておりません。</b>"
"<b>お手すきの折に、まとめてご対応いただければ十分です。</b>", S_BODY))
story.append(Spacer(1, 2))
story.append(quote(
"<b>急かす意図はありません。</b>状況が分かれば大丈夫です。<br/>"
"<b>次回まとまった段階でDDLをいただければ、"
"同じように当日中に照合してご報告します。</b>", CW))
# ---------------------------------------------------------- 4. precheck
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"4. clean_timetables.precheck に入る値の範囲を"
"お教えいただけますでしょうか", S_H2))
story.append(Paragraph("4-1. 現状の理解", S_H3))
story.extend(bull([
"clean_timetables.precheck は現行 "
"<b>tinyint unsigned / 既定値 0</b> です(09-09版でも変化なし)",
"<b>DFA 側で「0 = 本清掃 / 60 = 60分前 / 30 = 30分前」のように、"
"分前の数値をそのまま入れる方向でご検討いただいている</b> "
"と伺っております",
"設定値によっては <b>20 なら20分前</b> のように、"
"任意の分数が入りうるとも伺っております",
]))
story.append(Paragraph("4-2. お伺いしたいこと", S_H3))
story.append(Spacer(1, 3))
story.append(table(PRECHECK_ASK_ROWS, [10 * mm, W - 10 * mm],
[S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>特に4番をお伺いしたいです。</b>"
"<b>混在すると、当方は 1 を「1分前」と読み違えてしまうためです。</b>"
"<b>値だけを見て新旧を判別する方法がありませんので、"
"期間が分かれば当方で切り替え時期を合わせられます。</b>", S_BODY))
story.append(CondPageBreak(45 * mm))
story.append(Paragraph("4-3. なぜお伺いするか", S_H3))
story.append(Paragraph(
"<b>アラート画面で「60分前の異常」「30分前の異常」を出し分けるのに、"
"この値を使います。</b>", S_BODY))
story.append(Paragraph(
"<b>値の意味を取り違えると、"
"現場への連絡タイミングの判断を誤ります。</b>"
"「30分前のチェックで異常が出た」のか「60分前で出た」のかによって、"
"現場に間に合うかどうかが変わるためです。", S_BODY))
story.append(Paragraph(
"なお、<b>DDL からは値の意味が読めませんでした</b>"
"(カラムコメントが空のため)。"
"<b>型は tinyint unsigned(0〜255)ですので、"
"60 も 30 も 20 も、型の変更なしで入ります。</b>"
"つまり <b>値の意味の変更は DDL には現れません</b> ので、"
"お伺いする次第です。", S_BODY))
# ---------------------------------------------------------- 5. 面積
story.append(CondPageBreak(80 * mm))
story.append(Paragraph(
"5. 面積のカラムの型変更について(共有のみ。回答は不要です)", S_H2))
story.append(Paragraph(
"<b>これは要望でも質問でもなく、事実の共有だけです。</b>", S_BODY))
story.append(Paragraph("5-1. 実測", S_H3))
story.append(Paragraph(
"09-09版で、面積のカラムも型が変わっておりました(<b>10列</b>)。",
S_LEAD))
story.append(Spacer(1, 3))
story.append(table(AREA_TYPE_ROWS, [54 * mm, W - 54 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 5))
story.append(CondPageBreak(38 * mm))
story.append(table(AREA_COL_ROWS, [58 * mm, W - 58 * mm],
[S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph("5-2. 当方に実害はありません", S_H3))
story.append(Paragraph(
"<b>この変更で当方が困ることはありません。</b>"
"理由は2つです。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(AREA_HARM_ROWS, [10 * mm, W - 10 * mm],
[S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>したがって、お戻しいただく必要はありません。</b>", S_BODY))
story.append(Paragraph("5-3. 意図の確認は今回は見送ります", S_H3))
story.append(Paragraph(
"当初は「率の小数化に伴う変更でしょうか」と"
"お伺いしようと考えておりましたが、"
"<b>上記のとおり当方に実害がありませんので、"
"今回は質問といたしません。</b>"
"<b>お伺いする件数を絞り、"
"早くお返事をいただきたいためです。</b>", S_BODY))
story.append(Paragraph(
"もし何か意図がおありでしたら、"
"<b>次回ついでの折にお聞かせいただければ十分です。</b>", S_BODY))
# ---------------------------------------------------------- 末尾
story.append(CondPageBreak(75 * mm))
story.append(Paragraph("おわりに", S_H2))
story.append(Paragraph(
"<b>今回は、お伺いすることを3件に絞りました。</b>"
"<b>お伺いする件数が少ないほど、"
"早くお返事をいただけると考えたためです。</b>", S_BODY))
story.append(Paragraph(
"優先度をつけるとすれば次の順です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(PRIO_ROWS, [10 * mm, 50 * mm, W - 60 * mm],
[S_CODE, S_CELL, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"<b>1(接続先)が最優先です。</b>"
"<b>これだけ先にいただければ、当方で確認できることが一気に増え、"
"その分だけ貴社への照会が減ります。</b>", S_BODY))
story.append(Paragraph(
"なお、これまでお願いしておりました外部キー/索引(B-1)と"
"主キー(A-1)は、"
"<b>当方の実装方針が固まり必要がなくなりましたので、"
"今回の依頼から外しました</b>(1-5 に経緯を記しました)。", S_BODY))
story.append(Spacer(1, 4))
story.append(Paragraph(
"率のカラムの小数化、そして本番が MariaDB であることのご教示、"
"あらためてありがとうございました。", S_BODY))
story.append(Paragraph("引き続きよろしくお願いします。", S_NOTE))
doc.build(story)
print("生成:", OUT)
if __name__ == "__main__":
build()