# -*- coding: utf-8 -*-
"""
TeamWorks 宛 確認事項(第2回)を PDF にする。
第1回「実DDL照合結果と確認事項」(make_ddl_review_pdf.py) の体裁を
そのまま踏襲した続編。A4 縦 / reportlab / 日本語フォント埋め込み /
ヘッダー罫線とページ番号。
フォントは BIZ UDGothic(日本語フォント)を埋め込む。
中国語向けフォントは漢字の字形が異なるため使わない。
フォールバックにも混ぜないこと。
BIZ UDGothic は等幅(半角は 0.5em 固定)なので、SQL の枠にもこのフォントを使う。
コードブロック内の空白は に置換して桁を保持する。
原稿は docs/handoff/TeamWorks確認事項_第2回_20260903.md。
文言・件数・表の内容は原稿から変更していない。
"""
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_確認事項_第2回_20260903.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_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)
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 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]
# ---------------------------------------------------------------- 表データ
SUMMARY_ROWS = [
["区分", "件数", "性質"],
["A. 実装が止まる", "2件", "取得元が特定できず、該当画面を実装できない"],
["B. 定義の確認", "2件", "取り違えると誤った値を表示する"],
["C. ご回答を受けた追加確認", "3件", "第1回のご回答の反映にあたり確認したい点"],
["D. 継続", "1件", "第1回でお願いしたもののうち、まだ動きがないもの"],
]
# Q1: D-Security の候補テーブル
Q1_CAND_ROWS = [
["テーブル", "主なカラム"],
["controls",
"serial_number / facility_id / section_id / status(tinyint unsigned, 既定 0) / "
"inactive / verified_at / deleted_at"],
["control_reports",
"control_id / facility_id / section_id / serial_number / reported_at"],
["control_error_histories",
"control_id / facility_id / section_id / status(tinyint unsigned) / comment / "
"occurred_at / responded_at / resolved_at"],
["security_statuses",
"facility_id / section_id / status(tinyint unsigned, 既定 1) / reported_at"],
["security_status_histories",
"facility_id / section_id / status(tinyint unsigned) / reported_at"],
]
# Q1-1: D-Secログ画面
Q1_LOG_ROWS = [
["画面の項目", "想定している取得元", "確認したいこと"],
["デバイスのシリアル", "controls.serial_number", "これで合っていますか"],
["施設名", "controls.facility_id → facilities.facility_name", ""],
["<b>通信状態</b>(正常 / 通信断 / 異常)", "<b>不明</b>", "Q1-2 でお伺いします"],
["<b>最終通信日時</b>", "control_reports.reported_at",
"この最新値でよいですか。control_reports は定期的な生存報告という理解で"
"合っていますか。報告の間隔はどれくらいですか"],
["備考", "<b>不明</b>",
"デバイスごとの備考を持つカラムはありますか(無ければ画面から落とします)"],
]
# Q1-3: D-Secアラート画面
Q1_ALERT_ROWS = [
["画面の項目", "想定している取得元", "確認したいこと"],
["検知日時", "control_error_histories.occurred_at", ""],
["施設名", "facility_id → facilities.facility_name", ""],
["清掃エリア名", "section_id → sections.section_name", ""],
["デバイスのシリアル", "control_id → controls.serial_number", ""],
["対応状況(未対応 / 対応中 / 完了)", "control_error_histories.status",
"<b>コード値定義をご教示ください</b>"],
["メモ", "control_error_histories.comment", ""],
["対応日時 / 解決日時", "responded_at / resolved_at", ""],
["<b>アラート種別</b>(セキュリティ解除 / 通信断)", "<b>不明</b>", "下記でお伺いします"],
["<b>詳細</b>(アラートの本文)", "<b>不明</b>", "下記でお伺いします"],
]
# Q2: 事前チェック時点の状態を探した範囲
Q2_ROWS = [
["テーブル・カラム", "確認した状況"],
["robots.status<br/>(smallint, 既定 0)",
"ロボットの<b>現在</b>の状態と思われます。<b>過去の値は残りません</b>"],
["section_robot_statuses.robot_status<br/>(smallint, 既定 0)",
"同上。定義書の備考も「現時点では未確定」で、robots.status からの算出ルールが不明です"],
["clean_timetables.precheck",
"0: 本清掃 / 1: 1回目 / 2: 2回目(第1回 B-1 でご回答いただいた内容)。"
"事前チェックの<b>タイミングは分かりますが、そのときの状態がどこにもありません</b>"],
["robot_instruct_histories",
"指示の履歴であり、状態のスナップショットではないと理解しています"],
]
# Q4: コード値定義が不明なカラム
Q4_ROWS = [
["テーブル.カラム", "型", "既定値"],
["robots.status", "smallint", "0"],
["section_robot_statuses.robot_status", "smallint", "0"],
["controls.status", "tinyint unsigned", "0"],
["control_error_histories.status", "tinyint unsigned", "0"],
["security_statuses.status", "tinyint unsigned", "1"],
["security_status_histories.status", "tinyint unsigned", "なし"],
["robot_error_reports.status", "tinyint unsigned", "0"],
]
# C-1: A-3(contact 系)のご回答と現在の DDL
C1_ROWS = [
["カラム", "ご回答", "現在の DDL"],
["customer_contacts.contact_type", "tinyint", "<b>bigint unsigned</b>"],
["facility_contacts.contact_type", "tinyint", "tinyint unsigned(一致)"],
["customer_contacts.email", "varchar(100)", "varchar(100)(一致)"],
["facility_contacts.email", "varchar(100)", "<b>varchar(255)</b>"],
]
# D: 継続
CONT_ROWS = [
["内容", "状態"],
["DB更新用アカウントの発行。<br/>robot_error_reports / control_error_histories の<br/>"
"status / comment / responded_at / resolved_at のみ UPDATE 可",
"<b>継続</b>"],
]
# 参考: 第1回のご回答で解決した事項
ANS_ROWS = [
["#", "ご回答", "こちらの対応"],
["A-1",
"error_id を主キーと位置付けて対応する予定。id も error_id も英字を含むため"
"数値型にはできない",
"PrecheckItem.id を <b>number → string</b> に変更し、値は error_id にします。"
"PATCH /prechecks/{id}/status の設計に着手できます(C-3 で最終確認)"],
["A-2",
"採番はデータベース。auto_increment で採番する。元テーブルと履歴テーブルは<b>1対1</b>",
"ビューアーはこの2テーブルを参照しないため、実装への影響はありません。"
"DDL への AUTO_INCREMENT 追加を C-2 で確認しています"],
["A-3",
"contact_type は両テーブルとも tinyint、email は両テーブルとも varchar(100)",
"DDL への反映を C-1 で確認しています。反映後の DDL を db pull で取り込みます"],
["B-1", "制約(外部キー3本)は後から追加する",
"<b>孤児行を想定した実装はやめます。</b>追加後の DDL に合わせます"],
["B-2", "インデックスも後から追加する。現状は20施設弱",
"承知しました。<b>当面の性能懸念は小さい</b>と理解しました"],
["B-3",
"プログラム側で明示的に対応しているが、指摘どおり DEFAULT を追加する"
"(created_at: 新規作成時、updated_at: 新規作成時/更新時)",
"追加後の DDL に合わせます"],
]
# 参考: 受領DDL の確認により解決していた事項
DONE_ROWS = [
["内容", "確認結果"],
["responded_at / resolved_at の<br/>カラム追加",
"<b>両テーブルに既に存在します</b>(robot_error_reports / control_error_histories、"
"いずれも datetime NULL可)。綴りも responded_at で正しくなっています"],
["D-Secアラートへの書き込み可否",
"実体が control_error_histories であれば、<b>既に更新可4カラムに含まれています</b>。"
"追加の権限は不要という理解です(Q1-3 で実体の確認をお願いしています)"],
]
def build():
doc = BaseDocTemplate(
OUT, pagesize=A4,
leftMargin=18 * mm, rightMargin=18 * mm,
topMargin=18 * mm, bottomMargin=20 * mm,
title="D-Watch 確認事項(第2回)",
author="DFA Robotics",
subject="実DDL(clean_watch_20260902.sql)を正とした実装で取得元が特定できなかった項目",
)
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("確認事項(第2回)", S_TITLE))
story.append(Paragraph(
"宛先: TeamWorks(DB開発側) 対象: clean_watch_20260902.sql "
"作成日: 2026-09-03 作成者: DFA Robotics", S_SUB))
story.append(Spacer(1, 9))
story.append(Paragraph(
"第1回「実DDL照合結果と確認事項」に続く確認事項です。"
"<b>第1回の A-1〜A-3 / B-1〜B-3 についてはご回答をいただきました。"
"ありがとうございました。</b>"
"受け止めと対応は末尾の「参考: 第1回のご回答で解決した事項」に記載しています。", S_BODY))
story.append(Paragraph(
"今回の本題は、<b>DDLを正としてビューアーのデータ取得層を実装しようとした結果、"
"取得元が特定できなかった項目</b>(Q1・Q2)です。", S_BODY))
story.append(Paragraph(
"<b>Q1 と Q2 は、解決しないと該当画面を実装できません。</b>"
"いずれも「DDL に該当カラムが見当たらない」という種類の問い合わせです。"
"<b>すでに存在するのに当方が見落としている可能性</b>もありますので、"
"その場合はテーブル名・カラム名をご指摘いただければ十分です。", S_BODY))
story.append(Spacer(1, 4))
# ---------------------------------------------------------- サマリ表
story.append(table(SUMMARY_ROWS, [44 * mm, 14 * mm, W - 58 * mm],
[S_CELL, S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"末尾の「参考」2節(第1回のご回答で解決した事項 / 受領DDLの確認により解決していた事項)は"
"<b>回答不要</b>です。こちらの受け止めを記載しているだけですので、"
"認識に相違があればご指摘ください。", S_NOTE))
# ---------------------------------------------------------- Q1
story.append(Paragraph("A. 実装が止まる(2件)", S_H2))
story.append(Paragraph(
"Q1 D-Security 画面のデータ取得元", S_H3))
story.append(Paragraph(
"ビューアーには D-Security 用の画面が2つあります。"
"F-06 D-Secログ(D-S制御盤の一覧と通信状態)と、"
"F-07 D-Secアラート(セキュリティ解除・通信断のアラート一覧)です。", S_BODY))
story.append(Paragraph(
"受領したDDL(39テーブル)を確認したところ、"
"<b>これらに直接対応する名前のテーブルが見当たりませんでした</b>"
"(ds_devices / ds_alerts のような名前のテーブルはありません)。"
"候補として次の5テーブルがあると考えています。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q1_CAND_ROWS, [46 * mm, W - 46 * mm], [S_CODE, S_CODE]))
story.append(Spacer(1, 6))
story.append(Paragraph("Q1-1 D-Secログ画面(デバイス一覧)", S_H3))
story.append(Paragraph(
"画面に出す項目と当方の想定を並べます。<b>想定が合っているかご確認ください。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q1_LOG_ROWS, [38 * mm, 56 * mm, W - 94 * mm],
[S_CELL, S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(CondPageBreak(40 * mm))
story.append(Paragraph("Q1-2 controls.status のコード値定義をご教示ください", S_H3))
story.append(Paragraph(
"controls.status は tinyint unsigned / 既定値 0 ですが、"
"<b>取りうる値と意味が分かりません。</b>"
"0 / 1 / 2 … がそれぞれ何を表すかをご教示ください。", S_BODY))
story.append(Paragraph(
"あわせて、画面に出したい<b>3状態(正常 / 通信断 / 異常)</b>の"
"判定方法を確認させてください。", S_LEAD))
story.extend(num([
"「異常」は controls.status から判定できますか",
"「通信断」は control_reports.reported_at からの経過時間で判定する想定でよいですか。"
"その場合、<b>何分(何時間)で通信断とみなすか</b>の運用上の基準はありますか",
]))
story.append(Spacer(1, 3))
story.append(Paragraph(
"現在のサンプルデータでは「6時間以上通信なし」としていますが、"
"根拠のない仮の値です。", S_NOTE))
story.append(CondPageBreak(50 * mm))
story.append(Paragraph("Q1-3 D-Secアラート画面", S_H3))
story.append(Paragraph(
"control_error_histories が実体だと考えています。"
"<b>カラムの対応が下記で合っているかご確認ください。</b>", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q1_ALERT_ROWS, [36 * mm, 62 * mm, W - 98 * mm],
[S_CELL, S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph("伺いたいこと:", S_LEAD))
story.extend(num([
"<b>アラート種別</b>(「セキュリティ解除されたまま」なのか「通信断」なのか)を"
"区別するカラムがありますか。control_error_histories には種別を表すカラムが"
"見当たりません。status が種別を兼ねているのでしょうか、それとも別テーブルでしょうか",
"<b>アラートの詳細文</b>(例:「セキュリティ復帰失敗」)はどこに入りますか。"
"comment は運用担当が書き込む欄という理解なので、"
"<b>検知側が書く詳細</b>とは別だと考えています",
]))
story.append(CondPageBreak(40 * mm))
story.append(Paragraph(
"Q1-4 security_statuses / security_status_histories の位置づけ", S_H3))
story.append(Paragraph(
"security_statuses.status は tinyint unsigned / <b>既定値 1</b> です。", S_LEAD))
story.extend(num([
"<b>コード値定義をご教示ください</b>(0 / 1 / 2 … が何を表すか。"
"既定値が 1 である理由も)",
"このテーブルは<b>セキュリティの現在状態</b>を持つもので、"
"「解除されたまま戻っていない」という状態は"
"<b>ここから判定する</b>という理解で合っていますか",
"security_status_histories は security_statuses の変更履歴という理解で合っていますか",
]))
# ---------------------------------------------------------- Q2
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("Q2 事前チェック(60分前 / 30分前)時点のロボット状態", S_H3))
story.append(Paragraph(
"ダッシュボードと清掃詳細ログに、<b>清掃開始の60分前と30分前のロボットの状態</b>を"
"並べて表示する欄があります。表示する値は次の6種類です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(code([
"待機中 / 充電中 / ハンドルアップ / 緊急停止 / オフライン / セキュリティ有効",
], CW))
story.append(Spacer(1, 5))
story.append(Paragraph(
"<b>この「60分前時点の状態」「30分前時点の状態」を、"
"後から参照できるテーブルが見当たりません。</b>"
"当方が確認した範囲では次のとおりです。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q2_ROWS, [56 * mm, W - 56 * mm], [S_CODE, S_CELL]))
story.append(Spacer(1, 6))
story.append(Paragraph("伺いたいこと:", S_LEAD))
story.extend(num([
"<b>事前チェック時点のロボット状態は、どのテーブルのどのカラムに記録されますか。</b>"
"記録されない場合は、<b>現状では画面に出せない</b>ことになるため、その旨をご教示ください"
"(その場合は当該欄を P2 以降に送るか、別の表現に変える判断をします)",
"<b>robots.status(smallint)のコード値定義をご教示ください。</b>"
"上記6種類との対応が分かる形でお願いします。なお robots.clean_status については"
"第1回 A-2 で「0: 清掃前 / 1: 清掃中 / 2: 清掃終了(セキュリティ有効) / "
"-1: 清掃完了(セキュリティ無効)」とご回答いただいていますが、"
"<b>status のほうは未回答</b>です",
"section_robot_statuses.robot_status を配下の robots.status から算出するルールは"
"<b>決まりましたか</b>(定義書の備考は「現時点では未確定」のままです)",
]))
# ---------------------------------------------------------- B
story.append(CondPageBreak(50 * mm))
story.append(Paragraph("B. 定義の確認(2件)", S_H2))
story.append(Paragraph("Q3 clean_schedules.weeks の曜日の並び順", S_H3))
story.append(Paragraph(
"clean_schedules.weeks は varchar(7) で、"
"曜日ごとの実施有無を7桁で持つ理解です。", S_BODY))
story.append(Paragraph(
"<b>左から順に「日・月・火・水・木・金・土」で合っていますか。</b>"
"それとも「月・火・水・木・金・土・日」でしょうか。", S_BODY))
story.append(Paragraph(
"画面には「毎日」「月水金」のようなラベルで表示するため、"
"<b>並び順を取り違えると別の曜日として表示されます</b>"
"(例: 1000000 を日曜と読むか月曜と読むか)。"
"あわせて、1 が実施・0 が非実施で合っていますか。", S_BODY))
story.append(Paragraph("Q4 ステータス系カラムのコード値定義(まとめ)", S_H3))
story.append(Paragraph(
"Q1 / Q2 と重複しますが、<b>コード値の定義が不明なカラムを一覧にします。</b>"
"第1回で clean_statuses.clean_status と clean_timetables.precheck は"
"ご回答いただきました。残りは次の7つです。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(Q4_ROWS, [72 * mm, 34 * mm, W - 106 * mm],
[S_CODE, S_CODE, S_CODE]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"robot_error_reports.status については、"
"画面では「未対応 / 対応中 / 完了」の3状態で扱う想定です。"
"control_error_histories.status と同じ体系でしょうか。", S_BODY))
# ---------------------------------------------------------- C
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("C. 第1回のご回答を受けた追加確認(3件)", S_H2))
story.append(Paragraph(
"第1回について<b>ご回答をいただきありがとうございました。すべて反映しています。</b>"
"そのうえで、<b>確認させていただきたい点が3件</b>あります。", S_BODY))
story.append(Paragraph("C-1 A-3(contact 系の型)は DDL 側を修正いただけますか", S_H3))
story.append(Paragraph("いただいたご回答:", S_LEAD))
story.append(Spacer(1, 2))
story.append(quote(
"contact_type は両テーブルとも tiny int. "
"email は両テーブルとも varchar(100) で定義してください.", CW))
story.append(Spacer(1, 5))
story.append(Paragraph(
"現在いただいている DDL(clean_watch_20260902.sql)は"
"<b>まだ食い違ったまま</b>です。", S_LEAD))
story.append(Spacer(1, 3))
story.append(table(C1_ROWS, [62 * mm, 34 * mm, W - 96 * mm],
[S_CODE, S_CODE, S_CODE]))
story.append(Spacer(1, 6))
story.append(Paragraph(
"こちらは<b>実DDL を正としてスキーマを機械的に取り込む運用</b>にしているため、"
"<b>DDL 側を直していただく前提</b>で認識が合っているかを確認させてください。", S_LEAD))
story.extend(num([
"<b>DDL 側を修正いただけるという理解で合っていますか</b>"
"(こちらは修正後の DDL を prisma db pull で取り込みます)",
"反映の目安時期をお知らせください",
]))
story.append(CondPageBreak(45 * mm))
story.append(Paragraph("C-2 B-1 / B-2 / B-3 の修正を含む DDL をいつ頂けますか", S_H3))
story.append(Paragraph(
"ご回答で、<b>外部キー3本(B-1)・インデックス1本(B-2)・"
"timestamp の DEFAULT(B-3)</b>を後から追加いただけると伺いました。"
"こちらは受領した DDL を prisma db pull で<b>機械的に取り込む運用</b>にしているため、"
"修正が入るたびに DDL を頂き直す必要があります。", S_LEAD))
story.extend(num([
"上記の修正(<b>C-1 の contact 系も含めて</b>)がまとまった段階で、"
"<b>DDL を再度いただけますか</b>",
"目安の時期をお知らせください",
"<b>A-2 の auto_increment も同じ修正に含まれる</b>という理解で合っていますか。"
"現在の DDL では clean_status_histories.id / clean_timetable_histories.id に "
"AUTO_INCREMENT が付いていません",
]))
story.append(CondPageBreak(45 * mm))
story.append(Paragraph("C-3 API が画面に返す ID は error_id で確定してよいですか", S_H3))
story.append(Paragraph("いただいたご回答:", S_LEAD))
story.append(Spacer(1, 2))
story.append(quote(
"とりあえず error_id を主キーと位置付け対応する予定です. "
"ちなみに id も error_id もアルファベットを含んでいるので, "
"数値型にすることはできません.", CW))
story.append(Spacer(1, 5))
story.append(Paragraph(
"このご回答を受けて、こちらは <b>API の PrecheckItem.id を number から string へ変更し、"
"値には error_id を入れる</b>方針で進めます。", S_LEAD))
story.extend(num([
"<b>この方針で問題ないですか</b>",
"「運用を以て最終的な回答を求める」とのことでしたが、"
"<b>後で id 側に一意制約が付く可能性はありますか。</b>"
"付く可能性があるのであれば、切り替えられる形で実装しておきます",
]))
# ---------------------------------------------------------- D
story.append(CondPageBreak(40 * mm))
story.append(Paragraph("D. 継続(1件)", S_H2))
story.append(Paragraph(
"第1回でお願いしたもののうち、<b>まだ動きがないもの</b>です。", S_NOTE))
story.append(Spacer(1, 3))
story.append(table(CONT_ROWS, [W - 30 * mm, 30 * mm], [S_CELL, S_CELL]))
# ---------------------------------------------------------- 参考
story.append(CondPageBreak(60 * mm))
story.append(Paragraph("参考: 第1回のご回答で解決した事項", S_H2))
story.append(Paragraph(
"<b>回答不要です。</b>こちらの受け止めと対応を記載しますので、"
"認識に相違があればご指摘ください。", S_NOTE))
story.append(Spacer(1, 3))
story.append(table(ANS_ROWS, [12 * mm, 68 * mm, W - 80 * mm],
[S_CODE, S_CELL, S_CELL], head_color=SUB))
story.append(CondPageBreak(45 * mm))
story.append(Paragraph("参考: 受領DDL の確認により解決していた事項", S_H2))
story.append(Paragraph(
"第1回でお願いしていた次の2件は、受領したDDLを確認したところ"
"既に満たされていました。<b>対応不要です。</b>", S_NOTE))
story.append(Spacer(1, 3))
story.append(table(DONE_ROWS, [52 * mm, W - 52 * mm], [S_CELL, S_CELL]))
# ---------------------------------------------------------- 当方判断
story.append(Paragraph("参考: 当方で判断して進める項目(回答不要)", S_H2))
story.append(Paragraph(
"ご回答を待たずに当方で決めて進めるものです。"
"認識に相違があればご指摘ください。", S_NOTE))
story.append(Spacer(1, 2))
story.extend(bull([
"<b>clean_schedules に serial_number が無い</b>件 → sections 経由で robots を引きます。"
"P1 は「1清掃エリア = 1ロボット」の前提のため一意に定まります",
"<b>clean_schedules.end_time が 24〜29時を取る</b>件(第1回 B-5 で回答済み)"
"→ 画面表示は「翌04:30」の形にします",
]))
# ---------------------------------------------------------- 末尾
story.append(Spacer(1, 10))
story.append(Paragraph(
"本書の Q1・Q2 についてご回答をいただければ、"
"D-Security 画面と清掃詳細ログのデータ取得層に着手できます。"
"Q3・Q4 は実装を止めるものではありませんが、"
"表示が誤る箇所のため早めにご確認いただけると助かります。"
"C は第1回のご回答の反映にあたっての確認で、"
"とくに C-2(修正版 DDL の受領時期)の目安をいただけると助かります。", S_NOTE))
doc.build(story)
print("生成:", OUT)
if __name__ == "__main__":
build()