You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1335 lines
59 KiB
1335 lines
59 KiB
|
2 weeks ago
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Iterable, Sequence
|
||
|
|
|
||
|
|
from docx import Document
|
||
|
|
from docx.enum.section import WD_SECTION
|
||
|
|
from docx.enum.table import (
|
||
|
|
WD_CELL_VERTICAL_ALIGNMENT,
|
||
|
|
WD_ROW_HEIGHT_RULE,
|
||
|
|
WD_TABLE_ALIGNMENT,
|
||
|
|
)
|
||
|
|
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
|
||
|
|
from docx.oxml import OxmlElement
|
||
|
|
from docx.oxml.ns import qn
|
||
|
|
from docx.shared import Cm, Inches, Pt, RGBColor
|
||
|
|
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
OUT_DIR = ROOT / "Housing_UserManual"
|
||
|
|
OUT_FILE = OUT_DIR / "Housing_User_Manual_CodeAligned_v1.0.docx"
|
||
|
|
LOGO = ROOT / "Resources" / "mobidigm-logo.png"
|
||
|
|
SCREEN_DIR = ROOT / "tools" / "manual-screens"
|
||
|
|
LOGIN_IMAGE = SCREEN_DIR / "01-login.png"
|
||
|
|
WAITING_IMAGE = SCREEN_DIR / "02-waiting-barcode.png"
|
||
|
|
START_IMAGE = SCREEN_DIR / "03-wait-start-signal.png"
|
||
|
|
BOARD_IMAGE = SCREEN_DIR / "04-board-test.png"
|
||
|
|
OK_IMAGE = SCREEN_DIR / "05-result-ok.png"
|
||
|
|
NG_IMAGE = SCREEN_DIR / "06-result-ng.png"
|
||
|
|
DB_ERROR_IMAGE = SCREEN_DIR / "07-db-save-error.png"
|
||
|
|
|
||
|
|
NAVY = "102738"
|
||
|
|
DARK_NAVY = "07111C"
|
||
|
|
CYAN = "16C7E8"
|
||
|
|
BLUE = "2F86FF"
|
||
|
|
GREEN = "27D17F"
|
||
|
|
RED = "E65252"
|
||
|
|
ORANGE = "F39C3D"
|
||
|
|
LIGHT_BLUE = "EAF7FA"
|
||
|
|
LIGHT_GRAY = "F2F5F7"
|
||
|
|
MID_GRAY = "D7E0E5"
|
||
|
|
TEXT = "17212B"
|
||
|
|
MUTED = "586A75"
|
||
|
|
WHITE = "FFFFFF"
|
||
|
|
|
||
|
|
|
||
|
|
def set_cell_shading(cell, fill: str) -> None:
|
||
|
|
tc_pr = cell._tc.get_or_add_tcPr()
|
||
|
|
shd = tc_pr.find(qn("w:shd"))
|
||
|
|
if shd is None:
|
||
|
|
shd = OxmlElement("w:shd")
|
||
|
|
tc_pr.append(shd)
|
||
|
|
shd.set(qn("w:fill"), fill)
|
||
|
|
|
||
|
|
|
||
|
|
def set_cell_margins(cell, top=90, start=100, bottom=90, end=100) -> None:
|
||
|
|
tc = cell._tc
|
||
|
|
tc_pr = tc.get_or_add_tcPr()
|
||
|
|
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||
|
|
if tc_mar is None:
|
||
|
|
tc_mar = OxmlElement("w:tcMar")
|
||
|
|
tc_pr.append(tc_mar)
|
||
|
|
for margin, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
||
|
|
node = tc_mar.find(qn(f"w:{margin}"))
|
||
|
|
if node is None:
|
||
|
|
node = OxmlElement(f"w:{margin}")
|
||
|
|
tc_mar.append(node)
|
||
|
|
node.set(qn("w:w"), str(value))
|
||
|
|
node.set(qn("w:type"), "dxa")
|
||
|
|
|
||
|
|
|
||
|
|
def set_repeat_table_header(row) -> None:
|
||
|
|
tr_pr = row._tr.get_or_add_trPr()
|
||
|
|
tbl_header = OxmlElement("w:tblHeader")
|
||
|
|
tbl_header.set(qn("w:val"), "true")
|
||
|
|
tr_pr.append(tbl_header)
|
||
|
|
|
||
|
|
|
||
|
|
def set_cell_text_color(cell, color: str) -> None:
|
||
|
|
for paragraph in cell.paragraphs:
|
||
|
|
for run in paragraph.runs:
|
||
|
|
run.font.color.rgb = RGBColor.from_string(color)
|
||
|
|
|
||
|
|
|
||
|
|
def set_cell_width(cell, width_cm: float) -> None:
|
||
|
|
tc_pr = cell._tc.get_or_add_tcPr()
|
||
|
|
tc_w = tc_pr.find(qn("w:tcW"))
|
||
|
|
if tc_w is None:
|
||
|
|
tc_w = OxmlElement("w:tcW")
|
||
|
|
tc_pr.append(tc_w)
|
||
|
|
tc_w.set(qn("w:w"), str(Cm(width_cm).twips))
|
||
|
|
tc_w.set(qn("w:type"), "dxa")
|
||
|
|
|
||
|
|
|
||
|
|
def set_fixed_table_widths(table, widths: Sequence[float]) -> None:
|
||
|
|
tbl_pr = table._tbl.tblPr
|
||
|
|
layout = tbl_pr.first_child_found_in("w:tblLayout")
|
||
|
|
if layout is None:
|
||
|
|
layout = OxmlElement("w:tblLayout")
|
||
|
|
tbl_pr.append(layout)
|
||
|
|
layout.set(qn("w:type"), "fixed")
|
||
|
|
|
||
|
|
for index, width_cm in enumerate(widths):
|
||
|
|
table.columns[index].width = Cm(width_cm)
|
||
|
|
for cell in table.columns[index].cells:
|
||
|
|
cell.width = Cm(width_cm)
|
||
|
|
set_cell_width(cell, width_cm)
|
||
|
|
|
||
|
|
|
||
|
|
def set_run_font(run, name: str = "Malgun Gothic", size: float | None = None, bold: bool | None = None) -> None:
|
||
|
|
run.font.name = name
|
||
|
|
run._element.rPr.rFonts.set(qn("w:eastAsia"), name)
|
||
|
|
if size is not None:
|
||
|
|
run.font.size = Pt(size)
|
||
|
|
if bold is not None:
|
||
|
|
run.bold = bold
|
||
|
|
|
||
|
|
|
||
|
|
def add_page_number(paragraph) -> None:
|
||
|
|
paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||
|
|
run = paragraph.add_run("Page ")
|
||
|
|
set_run_font(run, size=8)
|
||
|
|
fld_char1 = OxmlElement("w:fldChar")
|
||
|
|
fld_char1.set(qn("w:fldCharType"), "begin")
|
||
|
|
instr_text = OxmlElement("w:instrText")
|
||
|
|
instr_text.set(qn("xml:space"), "preserve")
|
||
|
|
instr_text.text = " PAGE "
|
||
|
|
fld_char2 = OxmlElement("w:fldChar")
|
||
|
|
fld_char2.set(qn("w:fldCharType"), "end")
|
||
|
|
run._r.append(fld_char1)
|
||
|
|
run._r.append(instr_text)
|
||
|
|
run._r.append(fld_char2)
|
||
|
|
|
||
|
|
|
||
|
|
def set_keep_with_next(paragraph) -> None:
|
||
|
|
p_pr = paragraph._p.get_or_add_pPr()
|
||
|
|
keep_next = OxmlElement("w:keepNext")
|
||
|
|
p_pr.append(keep_next)
|
||
|
|
|
||
|
|
|
||
|
|
def set_table_borders(table, color=MID_GRAY, size="6") -> None:
|
||
|
|
tbl = table._tbl
|
||
|
|
tbl_pr = tbl.tblPr
|
||
|
|
borders = tbl_pr.first_child_found_in("w:tblBorders")
|
||
|
|
if borders is None:
|
||
|
|
borders = OxmlElement("w:tblBorders")
|
||
|
|
tbl_pr.append(borders)
|
||
|
|
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||
|
|
tag = f"w:{edge}"
|
||
|
|
element = borders.find(qn(tag))
|
||
|
|
if element is None:
|
||
|
|
element = OxmlElement(tag)
|
||
|
|
borders.append(element)
|
||
|
|
element.set(qn("w:val"), "single")
|
||
|
|
element.set(qn("w:sz"), size)
|
||
|
|
element.set(qn("w:space"), "0")
|
||
|
|
element.set(qn("w:color"), color)
|
||
|
|
|
||
|
|
|
||
|
|
def set_paragraph_spacing(paragraph, before=0, after=5, line=1.25) -> None:
|
||
|
|
fmt = paragraph.paragraph_format
|
||
|
|
fmt.space_before = Pt(before)
|
||
|
|
fmt.space_after = Pt(after)
|
||
|
|
fmt.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
|
||
|
|
fmt.line_spacing = line
|
||
|
|
|
||
|
|
|
||
|
|
def add_table(
|
||
|
|
doc: Document,
|
||
|
|
headers: Sequence[str],
|
||
|
|
rows: Iterable[Sequence[str]],
|
||
|
|
widths: Sequence[float] | None = None,
|
||
|
|
header_fill: str = NAVY,
|
||
|
|
) -> object:
|
||
|
|
rows = list(rows)
|
||
|
|
table = doc.add_table(rows=1, cols=len(headers))
|
||
|
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||
|
|
table.autofit = False
|
||
|
|
set_table_borders(table)
|
||
|
|
if widths:
|
||
|
|
set_fixed_table_widths(table, widths)
|
||
|
|
header = table.rows[0]
|
||
|
|
set_repeat_table_header(header)
|
||
|
|
for index, text in enumerate(headers):
|
||
|
|
cell = header.cells[index]
|
||
|
|
cell.text = text
|
||
|
|
set_cell_shading(cell, header_fill)
|
||
|
|
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||
|
|
set_cell_margins(cell)
|
||
|
|
if widths:
|
||
|
|
cell.width = Cm(widths[index])
|
||
|
|
paragraph = cell.paragraphs[0]
|
||
|
|
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
|
for run in paragraph.runs:
|
||
|
|
set_run_font(run, size=8.5, bold=True)
|
||
|
|
run.font.color.rgb = RGBColor(255, 255, 255)
|
||
|
|
for row_index, values in enumerate(rows):
|
||
|
|
cells = table.add_row().cells
|
||
|
|
for index, value in enumerate(values):
|
||
|
|
cell = cells[index]
|
||
|
|
cell.text = str(value)
|
||
|
|
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||
|
|
set_cell_margins(cell)
|
||
|
|
if widths:
|
||
|
|
cell.width = Cm(widths[index])
|
||
|
|
if row_index % 2 == 1:
|
||
|
|
set_cell_shading(cell, "F7F9FA")
|
||
|
|
for paragraph in cell.paragraphs:
|
||
|
|
set_paragraph_spacing(paragraph, after=1, line=1.12)
|
||
|
|
for run in paragraph.runs:
|
||
|
|
set_run_font(run, size=8.4)
|
||
|
|
run.font.color.rgb = RGBColor.from_string(TEXT)
|
||
|
|
doc.add_paragraph()
|
||
|
|
return table
|
||
|
|
|
||
|
|
|
||
|
|
def add_bullet(doc: Document, text: str, level: int = 0) -> None:
|
||
|
|
paragraph = doc.add_paragraph(style="List Bullet" if level == 0 else "List Bullet 2")
|
||
|
|
paragraph.paragraph_format.left_indent = Cm(0.55 + 0.45 * level)
|
||
|
|
paragraph.paragraph_format.first_line_indent = Cm(-0.25)
|
||
|
|
set_paragraph_spacing(paragraph, after=3, line=1.2)
|
||
|
|
run = paragraph.add_run(text)
|
||
|
|
set_run_font(run, size=9.5)
|
||
|
|
|
||
|
|
|
||
|
|
def add_number(doc: Document, text: str) -> None:
|
||
|
|
paragraph = doc.add_paragraph(style="List Number")
|
||
|
|
paragraph.paragraph_format.left_indent = Cm(0.7)
|
||
|
|
paragraph.paragraph_format.first_line_indent = Cm(-0.35)
|
||
|
|
set_paragraph_spacing(paragraph, after=3, line=1.2)
|
||
|
|
run = paragraph.add_run(text)
|
||
|
|
set_run_font(run, size=9.5)
|
||
|
|
|
||
|
|
|
||
|
|
def add_body(doc: Document, text: str, bold_prefix: str | None = None) -> None:
|
||
|
|
paragraph = doc.add_paragraph()
|
||
|
|
set_paragraph_spacing(paragraph, after=6, line=1.3)
|
||
|
|
if bold_prefix and text.startswith(bold_prefix):
|
||
|
|
first = paragraph.add_run(bold_prefix)
|
||
|
|
set_run_font(first, size=9.5, bold=True)
|
||
|
|
rest = paragraph.add_run(text[len(bold_prefix):])
|
||
|
|
set_run_font(rest, size=9.5)
|
||
|
|
else:
|
||
|
|
run = paragraph.add_run(text)
|
||
|
|
set_run_font(run, size=9.5)
|
||
|
|
|
||
|
|
|
||
|
|
def add_note(doc: Document, title: str, text: str, kind: str = "info") -> None:
|
||
|
|
palette = {
|
||
|
|
"info": (LIGHT_BLUE, CYAN),
|
||
|
|
"warning": ("FFF3E5", ORANGE),
|
||
|
|
"danger": ("FDEBEC", RED),
|
||
|
|
"success": ("E9F8F1", GREEN),
|
||
|
|
}
|
||
|
|
fill, accent = palette[kind]
|
||
|
|
table = doc.add_table(rows=1, cols=2)
|
||
|
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||
|
|
table.autofit = False
|
||
|
|
set_table_borders(table, color=fill, size="0")
|
||
|
|
set_fixed_table_widths(table, [1.4, 14.6])
|
||
|
|
table.rows[0].height = Cm(1.4)
|
||
|
|
table.rows[0].height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
|
||
|
|
accent_cell, text_cell = table.rows[0].cells
|
||
|
|
set_cell_shading(accent_cell, accent)
|
||
|
|
set_cell_shading(text_cell, fill)
|
||
|
|
set_cell_margins(accent_cell, top=100, bottom=100, start=0, end=0)
|
||
|
|
set_cell_margins(text_cell, top=110, bottom=110, start=150, end=150)
|
||
|
|
p = text_cell.paragraphs[0]
|
||
|
|
set_paragraph_spacing(p, after=1, line=1.18)
|
||
|
|
r1 = p.add_run(f"{title} ")
|
||
|
|
set_run_font(r1, size=8.8, bold=True)
|
||
|
|
r1.font.color.rgb = RGBColor.from_string(accent)
|
||
|
|
r2 = p.add_run(text)
|
||
|
|
set_run_font(r2, size=8.8)
|
||
|
|
r2.font.color.rgb = RGBColor.from_string(TEXT)
|
||
|
|
doc.add_paragraph().paragraph_format.space_after = Pt(1)
|
||
|
|
|
||
|
|
|
||
|
|
def add_code_block(doc: Document, text: str) -> None:
|
||
|
|
table = doc.add_table(rows=1, cols=1)
|
||
|
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||
|
|
set_table_borders(table, color="CAD4DA")
|
||
|
|
cell = table.cell(0, 0)
|
||
|
|
set_cell_shading(cell, "F5F7F8")
|
||
|
|
set_cell_margins(cell, top=120, bottom=120, start=160, end=160)
|
||
|
|
p = cell.paragraphs[0]
|
||
|
|
p.paragraph_format.space_after = Pt(0)
|
||
|
|
for line_index, line in enumerate(text.splitlines()):
|
||
|
|
if line_index:
|
||
|
|
p.add_run().add_break()
|
||
|
|
run = p.add_run(line)
|
||
|
|
set_run_font(run, name="Consolas", size=8)
|
||
|
|
run.font.color.rgb = RGBColor.from_string("25323B")
|
||
|
|
doc.add_paragraph()
|
||
|
|
|
||
|
|
|
||
|
|
def add_image(doc: Document, path: Path, caption: str) -> None:
|
||
|
|
if not path.exists():
|
||
|
|
add_note(doc, "이미지 누락", f"{path.name} 파일을 찾을 수 없어 본문에 삽입하지 못했습니다.", "warning")
|
||
|
|
return
|
||
|
|
paragraph = doc.add_paragraph()
|
||
|
|
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
|
paragraph.paragraph_format.space_after = Pt(3)
|
||
|
|
run = paragraph.add_run()
|
||
|
|
run.add_picture(str(path), width=Cm(16.6))
|
||
|
|
caption_p = doc.add_paragraph()
|
||
|
|
caption_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
|
caption_p.paragraph_format.space_after = Pt(8)
|
||
|
|
caption_run = caption_p.add_run(caption)
|
||
|
|
set_run_font(caption_run, size=8)
|
||
|
|
caption_run.italic = True
|
||
|
|
caption_run.font.color.rgb = RGBColor.from_string(MUTED)
|
||
|
|
|
||
|
|
|
||
|
|
def add_flow(doc: Document, steps: Sequence[str]) -> None:
|
||
|
|
table = doc.add_table(rows=1, cols=len(steps))
|
||
|
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||
|
|
table.autofit = False
|
||
|
|
set_table_borders(table, color="7BC9D7", size="8")
|
||
|
|
for index, step in enumerate(steps):
|
||
|
|
cell = table.cell(0, index)
|
||
|
|
set_cell_shading(cell, LIGHT_BLUE if index % 2 == 0 else "F5FBFC")
|
||
|
|
set_cell_margins(cell, top=160, bottom=160, start=60, end=60)
|
||
|
|
p = cell.paragraphs[0]
|
||
|
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
|
r = p.add_run(step)
|
||
|
|
set_run_font(r, size=7.8, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(NAVY)
|
||
|
|
doc.add_paragraph()
|
||
|
|
|
||
|
|
|
||
|
|
def add_chapter(doc: Document, number: int, title: str) -> None:
|
||
|
|
if number > 1:
|
||
|
|
doc.add_page_break()
|
||
|
|
p = doc.add_paragraph(style="Heading 1")
|
||
|
|
p.paragraph_format.space_before = Pt(0)
|
||
|
|
p.paragraph_format.space_after = Pt(12)
|
||
|
|
set_keep_with_next(p)
|
||
|
|
r = p.add_run(f"{number}. {title}")
|
||
|
|
set_run_font(r, size=18, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(NAVY)
|
||
|
|
border = OxmlElement("w:pBdr")
|
||
|
|
bottom = OxmlElement("w:bottom")
|
||
|
|
bottom.set(qn("w:val"), "single")
|
||
|
|
bottom.set(qn("w:sz"), "18")
|
||
|
|
bottom.set(qn("w:space"), "5")
|
||
|
|
bottom.set(qn("w:color"), CYAN)
|
||
|
|
border.append(bottom)
|
||
|
|
p._p.get_or_add_pPr().append(border)
|
||
|
|
|
||
|
|
|
||
|
|
def add_section(doc: Document, number: str, title: str) -> None:
|
||
|
|
p = doc.add_paragraph(style="Heading 2")
|
||
|
|
p.paragraph_format.space_before = Pt(10)
|
||
|
|
p.paragraph_format.space_after = Pt(6)
|
||
|
|
set_keep_with_next(p)
|
||
|
|
r = p.add_run(f"{number} {title}")
|
||
|
|
set_run_font(r, size=12, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(BLUE)
|
||
|
|
|
||
|
|
|
||
|
|
def configure_document(doc: Document) -> None:
|
||
|
|
section = doc.sections[0]
|
||
|
|
section.page_width = Inches(8.5)
|
||
|
|
section.page_height = Inches(11)
|
||
|
|
section.top_margin = Cm(1.78)
|
||
|
|
section.bottom_margin = Cm(1.78)
|
||
|
|
section.left_margin = Cm(1.91)
|
||
|
|
section.right_margin = Cm(1.91)
|
||
|
|
section.header_distance = Cm(1.0)
|
||
|
|
section.footer_distance = Cm(1.0)
|
||
|
|
|
||
|
|
styles = doc.styles
|
||
|
|
normal = styles["Normal"]
|
||
|
|
normal.font.name = "Malgun Gothic"
|
||
|
|
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Malgun Gothic")
|
||
|
|
normal.font.size = Pt(9.5)
|
||
|
|
normal.font.color.rgb = RGBColor.from_string(TEXT)
|
||
|
|
|
||
|
|
for style_name in ("Heading 1", "Heading 2", "Heading 3"):
|
||
|
|
style = styles[style_name]
|
||
|
|
style.font.name = "Malgun Gothic"
|
||
|
|
style._element.rPr.rFonts.set(qn("w:eastAsia"), "Malgun Gothic")
|
||
|
|
|
||
|
|
for style_name in ("List Bullet", "List Bullet 2", "List Number"):
|
||
|
|
style = styles[style_name]
|
||
|
|
style.font.name = "Malgun Gothic"
|
||
|
|
style._element.rPr.rFonts.set(qn("w:eastAsia"), "Malgun Gothic")
|
||
|
|
|
||
|
|
header = section.header
|
||
|
|
header_p = header.paragraphs[0]
|
||
|
|
header_p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||
|
|
r = header_p.add_run("HOUSING ASSEMBLY HMI | USER MANUAL")
|
||
|
|
set_run_font(r, size=7.5, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(MUTED)
|
||
|
|
|
||
|
|
footer = section.footer
|
||
|
|
footer_table = footer.add_table(1, 2, width=Inches(6.6))
|
||
|
|
footer_table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||
|
|
footer_table.autofit = False
|
||
|
|
left, right = footer_table.rows[0].cells
|
||
|
|
left.width = Cm(8)
|
||
|
|
right.width = Cm(8)
|
||
|
|
p_left = left.paragraphs[0]
|
||
|
|
r_left = p_left.add_run("MOBIDIGM · Internal Production Document")
|
||
|
|
set_run_font(r_left, size=7.5)
|
||
|
|
r_left.font.color.rgb = RGBColor.from_string(MUTED)
|
||
|
|
add_page_number(right.paragraphs[0])
|
||
|
|
|
||
|
|
settings = doc.settings._element
|
||
|
|
update_fields = OxmlElement("w:updateFields")
|
||
|
|
update_fields.set(qn("w:val"), "true")
|
||
|
|
settings.append(update_fields)
|
||
|
|
|
||
|
|
|
||
|
|
def add_cover(doc: Document) -> None:
|
||
|
|
accent = doc.add_table(rows=1, cols=1)
|
||
|
|
accent.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||
|
|
accent.autofit = False
|
||
|
|
cell = accent.cell(0, 0)
|
||
|
|
set_cell_shading(cell, CYAN)
|
||
|
|
cell.height = Cm(0.25)
|
||
|
|
set_cell_margins(cell, top=0, bottom=0, start=0, end=0)
|
||
|
|
|
||
|
|
for _ in range(3):
|
||
|
|
doc.add_paragraph()
|
||
|
|
|
||
|
|
if LOGO.exists():
|
||
|
|
p_logo = doc.add_paragraph()
|
||
|
|
p_logo.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||
|
|
p_logo.add_run().add_picture(str(LOGO), width=Cm(4.8))
|
||
|
|
|
||
|
|
doc.add_paragraph()
|
||
|
|
title = doc.add_paragraph()
|
||
|
|
title.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||
|
|
r = title.add_run("HOUSING\nASSEMBLY HMI")
|
||
|
|
set_run_font(r, size=30, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(NAVY)
|
||
|
|
title.paragraph_format.space_after = Pt(4)
|
||
|
|
|
||
|
|
subtitle = doc.add_paragraph()
|
||
|
|
r = subtitle.add_run("사용자 매뉴얼")
|
||
|
|
set_run_font(r, size=21, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(CYAN)
|
||
|
|
subtitle.paragraph_format.space_after = Pt(3)
|
||
|
|
|
||
|
|
desc = doc.add_paragraph()
|
||
|
|
r = desc.add_run("Inspection Control and Sensor Monitoring Console")
|
||
|
|
set_run_font(r, size=10.5)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(MUTED)
|
||
|
|
|
||
|
|
for _ in range(4):
|
||
|
|
doc.add_paragraph()
|
||
|
|
|
||
|
|
meta = doc.add_table(rows=4, cols=2)
|
||
|
|
meta.alignment = WD_TABLE_ALIGNMENT.LEFT
|
||
|
|
meta.autofit = False
|
||
|
|
set_table_borders(meta, color=MID_GRAY)
|
||
|
|
values = [
|
||
|
|
("문서 버전", "1.0"),
|
||
|
|
("작성 기준일", "2026-07-09"),
|
||
|
|
("대상 프로그램", "Housing (.NET 9 WPF)"),
|
||
|
|
("작성 기준", "소스 코드 및 배포 구성 파일 정합"),
|
||
|
|
]
|
||
|
|
for i, (label, value) in enumerate(values):
|
||
|
|
left, right = meta.rows[i].cells
|
||
|
|
left.width = Cm(4.0)
|
||
|
|
right.width = Cm(9.0)
|
||
|
|
left.text = label
|
||
|
|
right.text = value
|
||
|
|
set_cell_shading(left, LIGHT_GRAY)
|
||
|
|
set_cell_margins(left, top=90, bottom=90, start=120, end=120)
|
||
|
|
set_cell_margins(right, top=90, bottom=90, start=120, end=120)
|
||
|
|
for run in left.paragraphs[0].runs:
|
||
|
|
set_run_font(run, size=9, bold=True)
|
||
|
|
run.font.color.rgb = RGBColor.from_string(NAVY)
|
||
|
|
for run in right.paragraphs[0].runs:
|
||
|
|
set_run_font(run, size=9)
|
||
|
|
|
||
|
|
doc.add_paragraph()
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"문서 주의",
|
||
|
|
"본 매뉴얼은 2026-07-09 현재 코드 동작을 기준으로 작성했습니다. 현장 배선, 장비 주소, 판정 기준은 승인된 생산 조건을 우선하며, 문서의 예시 값은 그대로 적용하지 마십시오.",
|
||
|
|
"warning",
|
||
|
|
)
|
||
|
|
doc.add_page_break()
|
||
|
|
|
||
|
|
|
||
|
|
def add_front_matter(doc: Document) -> None:
|
||
|
|
p = doc.add_paragraph()
|
||
|
|
r = p.add_run("문서 관리")
|
||
|
|
set_run_font(r, size=19, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(NAVY)
|
||
|
|
p.paragraph_format.space_after = Pt(10)
|
||
|
|
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["버전", "일자", "변경 내용", "비고"],
|
||
|
|
[("1.0", "2026-07-09", "Housing 코드 정합형 사용자 매뉴얼 최초 작성", "Initial")],
|
||
|
|
[2.0, 3.0, 8.5, 2.5],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"확인 범위",
|
||
|
|
"프로그램 빌드는 성공했으나 실제 보드·계측기·NI-6501·SQL Server를 연결한 생산 사이클은 수행하지 않았습니다. 설비별 명령 및 네트워크 값은 현장 승인본과 대조해야 합니다.",
|
||
|
|
"info",
|
||
|
|
)
|
||
|
|
doc.add_page_break()
|
||
|
|
p = doc.add_paragraph()
|
||
|
|
r = p.add_run("목차")
|
||
|
|
set_run_font(r, size=19, bold=True)
|
||
|
|
r.font.color.rgb = RGBColor.from_string(NAVY)
|
||
|
|
p.paragraph_format.space_after = Pt(10)
|
||
|
|
|
||
|
|
contents = [
|
||
|
|
("1", "개요"),
|
||
|
|
("2", "시스템 구성 및 사전 준비"),
|
||
|
|
("3", "설치 및 프로그램 실행"),
|
||
|
|
("4", "로그인"),
|
||
|
|
("5", "메인 화면 구성"),
|
||
|
|
("6", "환경 및 판정 기준 설정"),
|
||
|
|
("7", "표준 작업 절차"),
|
||
|
|
("8", "검사 공정 상세"),
|
||
|
|
("9", "판정 및 결과 확인"),
|
||
|
|
("10", "데이터베이스 저장"),
|
||
|
|
("11", "로그아웃 및 종료"),
|
||
|
|
("12", "오류 처리 및 문제 해결"),
|
||
|
|
("13", "운영 주의사항 및 유지관리"),
|
||
|
|
("14", "부록"),
|
||
|
|
]
|
||
|
|
toc = doc.add_table(rows=0, cols=2)
|
||
|
|
toc.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||
|
|
toc.autofit = False
|
||
|
|
set_fixed_table_widths(toc, [1.0, 15.0])
|
||
|
|
for number, title in contents:
|
||
|
|
row = toc.add_row()
|
||
|
|
row.height = Cm(1.0)
|
||
|
|
row.height_rule = WD_ROW_HEIGHT_RULE.EXACTLY
|
||
|
|
set_cell_width(row.cells[0], 1.0)
|
||
|
|
set_cell_width(row.cells[1], 15.0)
|
||
|
|
row.cells[0].text = number
|
||
|
|
row.cells[1].text = title
|
||
|
|
set_cell_shading(row.cells[0], NAVY)
|
||
|
|
set_cell_shading(row.cells[1], LIGHT_GRAY if int(number) % 2 else "F9FAFB")
|
||
|
|
for run in row.cells[0].paragraphs[0].runs:
|
||
|
|
set_run_font(run, size=9.5, bold=True)
|
||
|
|
run.font.color.rgb = RGBColor(255, 255, 255)
|
||
|
|
for run in row.cells[1].paragraphs[0].runs:
|
||
|
|
set_run_font(run, size=9.5, bold=True)
|
||
|
|
run.font.color.rgb = RGBColor.from_string(TEXT)
|
||
|
|
set_cell_margins(row.cells[0], top=100, bottom=100)
|
||
|
|
set_cell_margins(row.cells[1], top=100, bottom=100, start=150)
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_1(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 1, "개요")
|
||
|
|
add_section(doc, "1.1", "프로그램 목적")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"Housing Assembly HMI는 PCB 바코드와 보드 IC 일련번호(IC_SN)를 연결하고, 전압 및 전류를 측정하여 설정 범위에 따라 OK/NG를 판정한 뒤 SQL Server에 결과를 저장하는 WPF 기반 생산 검사 프로그램이다.",
|
||
|
|
)
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"프로그램은 작업자 로그인 정보(Maker, Model, Variant, Operator, Line, Lot, Jig)와 측정 결과를 하나의 Housing 조립 이력으로 관리한다.",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "1.2", "주요 공정 흐름")
|
||
|
|
add_flow(doc, ["LOGIN", "BARCODE", "START", "BOARD", "IC_SN", "MEASURE", "DB SAVE", "RESULT"])
|
||
|
|
for text in [
|
||
|
|
"작업 정보와 작업자 ID/PW를 입력하여 로그인한다.",
|
||
|
|
"PCB 바코드를 스캐너로 읽는다.",
|
||
|
|
"설비 시작 신호가 활성화된 경우 NI-6501 또는 LAN 신호를 기다린다.",
|
||
|
|
"보드 연결, IC_SN 읽기, CAL DEFAULT 명령을 수행한다.",
|
||
|
|
"계측기에서 전압과 전류를 읽는다.",
|
||
|
|
"V/mA 범위로 OK 또는 NG를 판정한다.",
|
||
|
|
"dbo.Housing_Assembly 테이블에 IC_SN 기준으로 Insert 또는 Update한다.",
|
||
|
|
"결과를 표시하고 다음 스캔을 위해 바코드와 IC_SN을 비운다.",
|
||
|
|
]:
|
||
|
|
add_number(doc, text)
|
||
|
|
|
||
|
|
add_section(doc, "1.3", "대상 사용자")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["사용자", "주요 업무"],
|
||
|
|
[
|
||
|
|
("생산 작업자", "로그인, 바코드 스캔, 결과 확인, NG 및 오류 보고"),
|
||
|
|
("라인 관리자", "작업 옵션·판정 범위 확인, 설비 상태 점검, 재작업 판단"),
|
||
|
|
("설비/IT 관리자", "Hardware.ini, Database.ini, LoginOptions.json, DB 및 드라이버 유지관리"),
|
||
|
|
],
|
||
|
|
[3.2, 12.8],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "1.4", "문서 적용 한계")
|
||
|
|
add_bullet(doc, "정확한 Windows 지원 버전은 프로젝트 코드에 명시되어 있지 않다. 배포본은 Windows x64 대상으로 구성되어 있다.")
|
||
|
|
add_bullet(doc, "계측기 모델, IP, COM 포트, DI 채널은 현장 구성에 따라 달라질 수 있다.")
|
||
|
|
add_bullet(doc, "예시 화면의 수치와 색상은 화면 구조 설명용이며 생산 판정 기준이 아니다.")
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_2(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 2, "시스템 구성 및 사전 준비")
|
||
|
|
add_section(doc, "2.1", "소프트웨어 구성")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["항목", "코드/배포 기준", "설명"],
|
||
|
|
[
|
||
|
|
("Application", ".NET 9 / WPF / Windows", "Housing.exe, 최대화된 HMI 화면"),
|
||
|
|
("Architecture", "win-x64 배포 폴더", "현재 publish 폴더 기준 x64"),
|
||
|
|
("Database", "Microsoft SQL Server", "Microsoft.Data.SqlClient 사용"),
|
||
|
|
("Board", "Serial COM", "PCB 보드 명령 및 IC_SN 읽기"),
|
||
|
|
("Equipment", "VISA / Serial / TCP", "SCPI 전압·전류 측정"),
|
||
|
|
("Start Signal", "NI-6501 또는 TCP", "설비 시작 입력 감시"),
|
||
|
|
("Barcode", "Serial COM", "고정 포트 또는 Auto 탐색"),
|
||
|
|
],
|
||
|
|
[2.6, 4.8, 8.6],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "2.2", "필요 장비 및 연결")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["장비", "용도", "대표 설정"],
|
||
|
|
[
|
||
|
|
("PCB Board Interface", "연결 확인, IC_SN 읽기, CAL DEFAULT", "COM, Baud, 명령, <end> 토큰"),
|
||
|
|
("Barcode Scanner", "PCB 바코드 입력", "COM 또는 Auto, Baud 9600"),
|
||
|
|
("Keysight 34465A 계열 DMM", "DC 전압 측정", "LAN/VISA/Serial, MEAS:VOLT:DC?"),
|
||
|
|
("Keysight E36233A 계열", "전류 측정 및 출력 제어", "LAN/VISA/Serial, MEAS:CURR?"),
|
||
|
|
("NI USB-6501", "설비 시작 DI 신호", "Dev*/port*/line*, Active High/Low"),
|
||
|
|
("SQL Server", "로그인 및 검사 이력 저장", "서버, DB, 계정, 프로시저/테이블"),
|
||
|
|
],
|
||
|
|
[3.7, 5.0, 7.3],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "2.3", "실행 전 점검")
|
||
|
|
for text in [
|
||
|
|
"보드, 바코드 스캐너, DMM, 전원 공급 장치 및 NI-6501 전원이 정상인지 확인한다.",
|
||
|
|
"보드 COM 포트와 바코드 스캐너 COM 포트가 중복되지 않는지 확인한다.",
|
||
|
|
"LAN 계측기와 DB 서버에 네트워크로 접근 가능한지 확인한다.",
|
||
|
|
"NI-DAQmx Runtime과 NI-6501 인식 상태를 확인한다(StartSignal=Ni6501 사용 시).",
|
||
|
|
"Hardware.ini, Database.ini, LoginOptions.json이 사용자가 Housing을 설치한 폴더에 있는지 확인한다.",
|
||
|
|
"승인된 V/mA 판정 범위를 확인한다.",
|
||
|
|
"DB의 dbo.Housing_Assembly 테이블과 로그인 구성이 준비되었는지 확인한다.",
|
||
|
|
]:
|
||
|
|
add_bullet(doc, text)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"보안",
|
||
|
|
"Database.ini와 LoginOptions.json에는 평문 비밀번호가 포함될 수 있다. 생산 PC의 파일 접근 권한을 제한하고 매뉴얼·메일·메신저에 실제 값을 복사하지 않는다.",
|
||
|
|
"danger",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_3(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 3, "설치 및 프로그램 실행")
|
||
|
|
add_section(doc, "3.1", "설치")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"승인된 배포 패키지의 setup.exe 또는 Setup.msi를 사용한다. 저장소에는 여러 Setup 산출물이 있으므로 파일명만으로 최신본을 판단하지 말고 배포 담당자가 지정한 버전을 사용한다.",
|
||
|
|
)
|
||
|
|
add_number(doc, "이전 버전이 실행 중이면 정상 로그아웃 후 종료한다.")
|
||
|
|
add_number(doc, "승인된 설치 패키지를 실행한다.")
|
||
|
|
add_number(doc, "설치 후 사용자가 Housing을 설치한 폴더에 필수 설정 파일이 배치되었는지 확인한다.")
|
||
|
|
add_number(doc, "현장 승인본과 설정을 대조한 뒤 프로그램을 실행한다.")
|
||
|
|
|
||
|
|
add_section(doc, "3.2", "실행 방법")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"바탕화면 바로가기 또는 사용자가 Housing을 설치한 폴더의 Housing.exe를 실행한다. 메인 창이 생성된 뒤 로그인 창이 모달로 표시된다. 로그인 창을 취소하거나 닫으면 메인 프로그램도 종료된다.",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "3.3", "실행 및 구성 파일")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["파일", "기본 위치", "용도"],
|
||
|
|
[
|
||
|
|
("Housing.exe", "사용자가 Housing을 설치한 폴더의 Housing.exe", "프로그램 실행"),
|
||
|
|
("Hardware.ini", "사용자가 Housing을 설치한 폴더의 Hardware.ini", "Board, Scanner, StartSignal, Equipment"),
|
||
|
|
("Database.ini", "사용자가 Housing을 설치한 폴더의 Database.ini", "DB 및 로그인 프로시저 설정"),
|
||
|
|
("LoginOptions.json", "사용자가 Housing을 설치한 폴더의 LoginOptions.json", "로그인 선택지 및 선택적 로컬 계정"),
|
||
|
|
("init.sql", "사용자가 Housing을 설치한 폴더의 init.sql(선택)", "DB 최초 구축 또는 승인된 스키마 변경 시에만 DBA가 실행"),
|
||
|
|
],
|
||
|
|
[3.0, 8.0, 5.0],
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"init.sql은 런타임 필수 파일이 아님",
|
||
|
|
"프로그램 코드는 init.sql을 읽거나 실행하지 않는다. 운영 DB에 필요한 로그인 프로시저와 테이블/인덱스가 이미 준비되어 있다면 사용자가 Housing을 설치한 폴더에 init.sql이 없어도 검사 동작에는 영향이 없다.",
|
||
|
|
"info",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "3.4", "초기 실행 확인")
|
||
|
|
add_bullet(doc, "로그인 화면 제목이 Housing HMI Login으로 표시되는지 확인한다.")
|
||
|
|
add_bullet(doc, "Maker/Model/Line/Jig 선택 목록이 표시되는지 확인한다.")
|
||
|
|
add_bullet(doc, "로그인 후 메인 화면이 최대화되어 표시되는지 확인한다.")
|
||
|
|
add_bullet(doc, "바코드 입력 영역에 포커스가 자동으로 유지되는지 확인한다.")
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_4(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 4, "로그인")
|
||
|
|
add_section(doc, "4.1", "입력 항목")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["화면 항목", "필수", "설명"],
|
||
|
|
[
|
||
|
|
("Maker", "필수", "LoginOptions.json의 makers 목록"),
|
||
|
|
("Model", "필수", "LoginOptions.json의 models 목록"),
|
||
|
|
("Variant 1", "선택", "빈 값 (None) 허용"),
|
||
|
|
("Variant 2", "선택", "빈 값 (None) 허용"),
|
||
|
|
("ID", "필수", "작업자 ID, DB 필드 최대 10자"),
|
||
|
|
("PW", "필수", "작업자 비밀번호"),
|
||
|
|
("Line", "필수", "생산 라인"),
|
||
|
|
("Lot No", "필수", "LOT 번호, DB 필드 최대 8자"),
|
||
|
|
("Jig No", "필수", "치구 번호"),
|
||
|
|
],
|
||
|
|
[3.2, 2.0, 10.8],
|
||
|
|
)
|
||
|
|
add_image(doc, LOGIN_IMAGE, "그림 4-1. 로그인 화면 예시 (작업 정보와 계정 값은 설명용)")
|
||
|
|
|
||
|
|
add_section(doc, "4.2", "로그인 절차")
|
||
|
|
for text in [
|
||
|
|
"Maker와 Model을 선택한다.",
|
||
|
|
"필요한 경우 Variant 1/2를 선택한다. 미사용 시 (None)을 선택한다.",
|
||
|
|
"ID와 PW를 입력한다.",
|
||
|
|
"Line, Lot No, Jig No를 입력 또는 선택한다.",
|
||
|
|
"ACCESS / LOG IN 버튼을 누른다.",
|
||
|
|
"메인 화면 우측 상단의 LOGIN ID가 입력한 작업자 ID로 표시되는지 확인한다.",
|
||
|
|
]:
|
||
|
|
add_number(doc, text)
|
||
|
|
|
||
|
|
add_section(doc, "4.3", "인증 방식 우선순위")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["우선", "조건", "인증 동작"],
|
||
|
|
[
|
||
|
|
("1", "LoginOptions.json의 loginAccounts가 1개 이상", "JSON의 ID/PW와 일치하는지 확인; DB 인증은 수행하지 않음"),
|
||
|
|
("2", "로컬 계정 없음 + OfflinePreview=True", "입력값이 비어 있지 않으면 인증 허용"),
|
||
|
|
("3", "로컬 계정 없음 + OfflinePreview=False", "Database.ini의 저장 프로시저로 ID/PW/ProcessName 확인"),
|
||
|
|
],
|
||
|
|
[1.3, 6.0, 8.7],
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"운영 권고",
|
||
|
|
"양산 환경에서는 테스트용 로컬 계정과 OfflinePreview 설정을 제거/비활성화하고 DB 인증 정책을 사용한다. 실제 비밀번호는 본 문서에 기록하지 않는다.",
|
||
|
|
"danger",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "4.4", "입력 이력")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"Maker, Model, Variant, Operator, Line, Lot, Jig 선택값은 프로그램이 자동으로 저장하여 다음 로그인 때 제안한다. Operator 값 중 특정 관리자용 ID는 코드에서 이력 저장 대상에서 제외된다.",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_5(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 5, "메인 화면 구성")
|
||
|
|
add_section(doc, "5.1", "전체 화면")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"메인 창은 기본 1200×800, 최소 840×560으로 정의되며 실행 시 최대화된다. 상단 헤더, 공정 추적 바, 스캔/추적성 영역, 측정 모니터링 영역, 하단 상태 표시로 구성된다.",
|
||
|
|
)
|
||
|
|
add_image(doc, WAITING_IMAGE, "그림 5-1. 로그인 직후 바코드 대기 화면")
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"화면 예시",
|
||
|
|
"프로젝트에 포함된 화면 이미지는 UI 구조와 OK/NG 색상 설명용이다. 이미지의 수치·LOGIN ID·공정 단계가 실제 검사 한 사이클의 정합된 기록을 의미하지는 않는다.",
|
||
|
|
"warning",
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"RESULT 영역의 의미",
|
||
|
|
"RESULT는 최종 판정만 표시하는 칸이 아니라 공정 안내를 함께 표시하는 공용 영역이다. 바코드 인식 순간에는 “스캔 완료. 시료 투입 후 Housing 장비 시작 버튼을 눌러주세요.”가 잠깐 표시될 수 있으나, StartSignal이 활성화된 현재 구성에서는 곧 “장비 시작 신호 대기 / NI-6501 Dev1/port0/line0” 안내로 전환된다. 측정 판정이 끝난 뒤에만 OK 또는 NG로 바뀐다.",
|
||
|
|
"info",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "5.2", "화면 영역")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["영역", "표시/입력", "사용 목적"],
|
||
|
|
[
|
||
|
|
("Header", "프로그램명, LOGIN ID, LOGOUT", "작업자 확인 및 로그아웃"),
|
||
|
|
("PROCESS", "BARCODE > START > BOARD > IC_SN > MEASURE > DB SAVE > RESULT", "현재 공정 단계 강조"),
|
||
|
|
("PCB_BARCODE INPUT", "스캔된 PCB 바코드", "추적성 입력"),
|
||
|
|
("IC_SN OUTPUT", "보드에서 읽은 Hex ID", "제품 식별"),
|
||
|
|
("V OUTPUT", "최소/최대 기준, 측정 전압", "전압 판정"),
|
||
|
|
("mA OUTPUT", "최소/최대 기준, 측정 전류", "전류 판정"),
|
||
|
|
("RESULT", "상태 메시지 또는 OK/NG", "최종 결과 확인"),
|
||
|
|
],
|
||
|
|
[3.4, 6.2, 6.6],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "5.3", "공정 상태 표시")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["상태", "의미", "기본 색상"],
|
||
|
|
[
|
||
|
|
("WAITING BARCODE", "다음 PCB 바코드 대기", "Cyan"),
|
||
|
|
("BARCODE SCANNED", "유효 바코드 수신", "Green"),
|
||
|
|
("WAIT START SIGNAL / START DELAY", "설비 시작 입력 또는 지연 대기", "Orange"),
|
||
|
|
("BOARD TEST", "보드 통신 및 명령 수행", "Orange"),
|
||
|
|
("IC_SN READ", "IC_SN 읽기 완료", "Cyan"),
|
||
|
|
("MEASUREMENT DONE", "전압/전류 읽기 완료", "Cyan"),
|
||
|
|
("DB SAVING", "검사 결과 저장 중", "Orange"),
|
||
|
|
("COMPLETE OK / COMPLETE NG", "저장 완료 및 최종 상태", "Green / Red"),
|
||
|
|
("ERROR / DB SAVE ERROR / SCANNER ERROR", "오류 발생", "Red"),
|
||
|
|
],
|
||
|
|
[5.3, 8.0, 2.9],
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_6(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 6, "환경 및 판정 기준 설정")
|
||
|
|
add_section(doc, "6.1", "V/mA 판정 범위")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"메인 화면의 V OUTPUT 및 mA OUTPUT 상단 범위 입력란에서 최소값과 최대값을 설정한다. Enter를 누르면 포커스가 바코드 입력란으로 돌아간다. 실제 저장은 다음 검사가 시작될 때 입력값 검증 후 수행된다.",
|
||
|
|
)
|
||
|
|
add_bullet(doc, "VMin/VMax 단위: V")
|
||
|
|
add_bullet(doc, "AMin/AMax 화면 단위: mA")
|
||
|
|
add_bullet(doc, "내부 저장 단위: A (화면 mA 값을 1000으로 나누어 저장)")
|
||
|
|
add_bullet(doc, "최소값은 최대값보다 클 수 없다.")
|
||
|
|
add_bullet(doc, "범위 경계값은 PASS에 포함된다.")
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"중요",
|
||
|
|
"판정 범위는 품질 승인값이다. 권한 없는 작업자가 임의 변경하지 않도록 운영 절차와 계정 권한으로 통제한다.",
|
||
|
|
"danger",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "6.2", "Hardware.ini")
|
||
|
|
add_body(doc, "Hardware.ini는 사용자가 Housing을 설치한 폴더에 있다. 이 파일은 설비/IT 관리자가 장비 연결 구성을 변경할 때 사용한다.")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["Section", "주요 Key", "주의사항"],
|
||
|
|
[
|
||
|
|
("[Board]", "PortName, BaudRate, ReadTimeout, Commands, EndToken", "보드 펌웨어 명령과 일치"),
|
||
|
|
("[BarcodeScanner]", "Enabled, PortName, BaudRate, IdleCommitMilliseconds", "Auto 사용 시 다른 Serial 포트 제외"),
|
||
|
|
("[StartSignal]", "Enabled, Connection, Channel/Host, ActiveState, Timeout", "Ni6501 또는 Tcp"),
|
||
|
|
("[Equipment]", "Timeout, Settle, PreBoard/Logout, Voltage*, Current*", "VISA/Serial/TCP와 SCPI 명령"),
|
||
|
|
],
|
||
|
|
[3.5, 7.2, 6.5],
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"보호 동작",
|
||
|
|
"Equipment 명령 목록은 세미콜론(;)으로 분리된다. 코드상 전원 공급 장치 CH2의 OUTP OFF 명령은 보호 목적으로 건너뛴다.",
|
||
|
|
"info",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "6.3", "Database.ini")
|
||
|
|
add_body(doc, "Database.ini는 사용자가 Housing을 설치한 폴더에 있다. 이 파일은 설비/IT 관리자가 DB 연결 및 로그인 설정을 변경할 때 사용한다.")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["Section", "Key", "설명"],
|
||
|
|
[
|
||
|
|
("[Database]", "IP, Database, DbId, DbPw", "SQL Server 접속 정보"),
|
||
|
|
("[Database]", "Encrypt, TrustServerCertificate, Timeout", "연결 보안 및 시간 제한"),
|
||
|
|
("[Login]", "Procedure", "기본 dbo.CheckOperator"),
|
||
|
|
("[Login]", "ProcessName", "기본 Housing"),
|
||
|
|
("[Login]", "OfflinePreview", "True이면 DB 없이 로그인 허용 가능"),
|
||
|
|
],
|
||
|
|
[3.0, 5.2, 9.0],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "6.4", "LoginOptions.json")
|
||
|
|
add_body(doc, "LoginOptions.json은 사용자가 Housing을 설치한 폴더에 있다. 이 파일은 설비/IT 관리자가 로그인 선택 항목을 변경할 때 사용한다.")
|
||
|
|
add_code_block(
|
||
|
|
doc,
|
||
|
|
"""{
|
||
|
|
"loginAccounts": [],
|
||
|
|
"makers": ["<MAKER>"],
|
||
|
|
"models": ["<MODEL>"],
|
||
|
|
"variant1Values": [],
|
||
|
|
"variant2Values": [],
|
||
|
|
"lineValues": ["<LINE>"],
|
||
|
|
"jigNoValues": ["<JIG>"]
|
||
|
|
}""",
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"예시 값",
|
||
|
|
"위 예시는 구조만 보여준다. loginAccounts를 비워야 DB/OfflinePreview 분기로 이동한다. 생산 값은 현장 관리자가 승인한 목록을 사용한다.",
|
||
|
|
"warning",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_7(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 7, "표준 작업 절차")
|
||
|
|
add_section(doc, "7.1", "작업 시작 전")
|
||
|
|
for text in [
|
||
|
|
"설비 전원, 케이블, 치구 및 안전 상태를 확인한다.",
|
||
|
|
"보드/스캐너 COM 포트와 계측기 LAN 또는 VISA 연결을 확인한다.",
|
||
|
|
"작업 지시서의 Maker, Model, Variant, Line, Lot, Jig를 확인한다.",
|
||
|
|
"승인된 전압/전류 판정 범위를 확인한다.",
|
||
|
|
"DB와 시작 신호 장치가 준비되었는지 확인한다.",
|
||
|
|
]:
|
||
|
|
add_bullet(doc, text)
|
||
|
|
|
||
|
|
add_section(doc, "7.2", "검사 시작")
|
||
|
|
for text in [
|
||
|
|
"Housing을 실행하고 작업 정보를 입력하여 로그인한다.",
|
||
|
|
"메인 화면 LOGIN ID와 V/mA 판정 범위를 확인한다.",
|
||
|
|
"제품을 치구에 올바르게 장착한다.",
|
||
|
|
"PCB 바코드를 스캔한다.",
|
||
|
|
"화면에 BARCODE SCANNED가 표시되는지 확인한다.",
|
||
|
|
"시작 신호가 활성화된 구성에서는 설비 Start 버튼을 눌러 입력 신호를 발생시킨다.",
|
||
|
|
"BOARD → IC_SN → MEASURE → DB SAVE → RESULT 진행을 관찰한다.",
|
||
|
|
"OK이면 제품을 정상 흐름으로 이동하고, NG 또는 오류이면 분리 후 표준 조치를 수행한다.",
|
||
|
|
]:
|
||
|
|
add_number(doc, text)
|
||
|
|
add_image(
|
||
|
|
doc,
|
||
|
|
START_IMAGE,
|
||
|
|
"그림 7-1. 바코드 입력 후 NI-6501 Dev1/port0/line0 시작 신호 대기 예시",
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"바코드 입력 후 화면 전환",
|
||
|
|
"BarcodeTextBox의 확인 문구는 일시적이다. 프로그램이 검사 시퀀스에 진입하고 StartSignal.Enabled=True이면 RESULT에 “장비 시작 신호 대기”, “NI-6501 Dev1/port0/line0”, “시료를 넣고 Housing 장비 시작 버튼을 눌러주세요.”가 표시된다. PhysicalChannel은 Hardware.ini에서 읽으며, 최종 OK/NG는 보드 통신과 전압·전류 측정이 완료된 뒤 표시된다.",
|
||
|
|
"info",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "7.3", "허용 바코드 형식")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["형식", "예시", "판정 규칙"],
|
||
|
|
[
|
||
|
|
("표기형", "PCBA S/N: A251001AM46110901T00001-01", "PCBA S/N 뒤 4~50자의 영문/숫자/-"),
|
||
|
|
("단일 Serial", "A251001AM46110901T00001-01", "영문/숫자/- 4~50자, 첫 글자 영숫자, 숫자 1개 이상"),
|
||
|
|
("구조형", "MODEL:M1;SERIAL:A251001AM46110901T00001-01;OPTION:O1", "MODEL, SERIAL, OPTION 세 필드 필수"),
|
||
|
|
],
|
||
|
|
[2.5, 6.0, 8.7],
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"스캐너 입력",
|
||
|
|
"붙여넣기·Backspace·Delete·Esc·Ctrl/Alt 조합은 바코드 입력에서 차단된다. 스캐너 또는 승인된 키보드 웨지 입력을 사용한다.",
|
||
|
|
"info",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "7.4", "검사 후")
|
||
|
|
add_bullet(doc, "OK/NG 표시와 색상을 확인한다.")
|
||
|
|
add_bullet(doc, "DB SAVE ERROR가 표시되면 결과가 화면에 OK여도 저장 완료로 간주하지 않는다.")
|
||
|
|
add_bullet(doc, "검사 종료 후 바코드와 IC_SN이 자동으로 비워지고 다음 스캔을 기다리는지 확인한다.")
|
||
|
|
add_bullet(doc, "작업 종료 또는 작업자 교대 시 LOGOUT을 사용한다.")
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_8(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 8, "검사 공정 상세")
|
||
|
|
add_section(doc, "8.1", "전체 시퀀스")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["단계", "프로그램 동작", "실패 시 대표 상태"],
|
||
|
|
[
|
||
|
|
("1. Barcode", "형식 검증 후 PCB_BARCODE INPUT 반영", "SCAN ERROR"),
|
||
|
|
("2. Range", "V/mA 숫자 및 Min ≤ Max 검증, 설정 저장", "RANGE SETTING ERROR"),
|
||
|
|
("3. Start", "NI-6501 또는 TCP 신호 대기, 지연 시각 계산", "ERROR / Timeout"),
|
||
|
|
("4. Pre-Board", "전류 채널 연결, 사전 SCPI 명령, 지연", "ERROR"),
|
||
|
|
("5. Board Connect", "PreConnect → Connect, Success 응답 확인", "ERROR"),
|
||
|
|
("6. IC_SN", "ReadId 응답에서 8~32자리 Hex 추출", "ERROR"),
|
||
|
|
("7. Board Setup", "PostReadId 및 CalDefault 명령", "ERROR"),
|
||
|
|
("8. Measure", "전압 후 전류 측정, 숫자 파싱", "ERROR"),
|
||
|
|
("9. Judge", "전압 AND 전류가 범위 내이면 OK", "NG"),
|
||
|
|
("10. DB", "IC_SN 기준 Upsert", "DB SAVE ERROR"),
|
||
|
|
("11. Reset", "PCB Barcode/IC_SN 비우기, 다음 스캔", "WAITING BARCODE"),
|
||
|
|
],
|
||
|
|
[2.7, 9.0, 5.5],
|
||
|
|
)
|
||
|
|
add_image(
|
||
|
|
doc,
|
||
|
|
BOARD_IMAGE,
|
||
|
|
"그림 8-1. 보드 검사 진행 및 IC_SN 025949A8492CFEAC 표시 예시",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "8.2", "시작 신호")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"StartSignal.Enabled=False이면 바코드 수신 후 시작 신호를 기다리지 않는다. Enabled=True이면 설정된 입력이 활성 상태가 될 때까지 기다린다.",
|
||
|
|
)
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["Connection", "동작"],
|
||
|
|
[
|
||
|
|
("Ni6501", "지정 DI PhysicalChannel을 PollInterval마다 읽고 ActiveState와 비교"),
|
||
|
|
("Tcp + ReadCommand 없음", "LAN 장치의 이벤트 메시지를 기다리고 ActiveResponse 일치 확인"),
|
||
|
|
("Tcp + ReadCommand 있음", "명령을 주기적으로 전송하고 응답을 ActiveResponse와 비교"),
|
||
|
|
],
|
||
|
|
[4.2, 11.8],
|
||
|
|
)
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"RequireInactiveBeforeStart",
|
||
|
|
"True이면 프로그램은 기존 Active 신호가 먼저 해제된 뒤 새로운 Active 전이를 기다린다. 버튼이 눌린 채로 시작하면 해제 대기에서 Timeout이 발생할 수 있다.",
|
||
|
|
"warning",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "8.3", "보드 통신")
|
||
|
|
add_bullet(doc, "보드 통신은 Hardware.ini [Board]의 Serial COM 설정을 사용한다.")
|
||
|
|
add_bullet(doc, "CONNECT 응답에 독립된 Success 행이 있어야 정상 연결로 판단한다.")
|
||
|
|
add_bullet(doc, "IC_SN은 응답 중 8~32자리 16진수 행을 찾아 대문자로 표시한다.")
|
||
|
|
add_bullet(doc, "IC_SN 읽기 전에 실패하면 finally에서 보드 OFF 명령을 시도한다.")
|
||
|
|
|
||
|
|
add_section(doc, "8.4", "계측기 통신")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"전압과 전류 채널은 각각 VISA, Serial(COM), TCP(LAN/Ethernet) 중 하나를 사용할 수 있다. TCP 사용 시 IdnMatch가 설정되어 있으면 *IDN? 응답을 먼저 검증한다.",
|
||
|
|
)
|
||
|
|
add_bullet(doc, "전류 채널 SetupCommand를 먼저 실행하고 SettleMilliseconds 동안 대기한다.")
|
||
|
|
add_bullet(doc, "전압을 먼저 읽고 전류를 읽는다.")
|
||
|
|
add_bullet(doc, "응답에서 첫 번째 숫자(지수 표기 포함)를 측정값으로 파싱한다.")
|
||
|
|
add_bullet(doc, "전류 채널 CleanupCommand는 성공/실패와 관계없이 가능한 범위에서 실행한다.")
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_9(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 9, "판정 및 결과 확인")
|
||
|
|
add_section(doc, "9.1", "판정식")
|
||
|
|
add_code_block(
|
||
|
|
doc,
|
||
|
|
"""VoltagePass = VMin ≤ MeasuredVoltage ≤ VMax
|
||
|
|
CurrentPass = AMin ≤ MeasuredCurrent(A) ≤ AMax
|
||
|
|
InspectionPass = VoltagePass AND CurrentPass""",
|
||
|
|
)
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"화면의 전류 기준은 mA이지만 내부 판정은 A 단위로 변환하여 비교한다. 측정 결과는 화면에서 소수점 둘째 자리까지 표시되며, 비교 자체는 decimal 원값으로 수행된다.",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "9.2", "OK 결과")
|
||
|
|
add_bullet(doc, "전압과 전류가 모두 범위 내이면 RESULT에 큰 초록색 OK가 표시된다.")
|
||
|
|
add_bullet(doc, "V OUTPUT과 mA OUTPUT 테두리 및 글자도 각각 초록색으로 표시된다.")
|
||
|
|
add_bullet(doc, "DB 저장이 성공하면 상태가 COMPLETE OK로 표시된다.")
|
||
|
|
add_image(doc, OK_IMAGE, "그림 9-1. OK 표시 예시 (표시 수치는 설명용)")
|
||
|
|
|
||
|
|
add_section(doc, "9.3", "NG 결과")
|
||
|
|
add_bullet(doc, "전압 또는 전류 중 하나라도 범위를 벗어나면 RESULT에 큰 빨간색 NG가 표시된다.")
|
||
|
|
add_bullet(doc, "각 항목은 개별 PASS/FAIL 색상으로 표시된다.")
|
||
|
|
add_bullet(doc, "NG 팝업에 실패 항목의 측정값과 기준 범위가 표시된다.")
|
||
|
|
add_bullet(doc, "DB에는 Result=NG로 저장을 시도한다.")
|
||
|
|
add_image(doc, NG_IMAGE, "그림 9-2. NG 표시 예시 (표시 수치는 설명용)")
|
||
|
|
|
||
|
|
add_section(doc, "9.4", "DB 저장 오류와 판정의 구분")
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"DB SAVE ERROR",
|
||
|
|
"측정 판정과 데이터 저장은 별개다. 화면에 OK/NG가 먼저 표시된 뒤 DB 저장이 실패할 수 있다. DB SAVE ERROR가 발생한 제품은 저장 여부를 DB에서 확인하기 전 정상 완료로 처리하지 않는다.",
|
||
|
|
"danger",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_10(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 10, "데이터베이스 저장")
|
||
|
|
add_section(doc, "10.1", "저장 방식")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"검사 완료 후 dbo.Housing_Assembly를 IC_SN으로 조회하여 기존 행이 있으면 Update, 없으면 Insert한다. 트랜잭션과 UPDLOCK/HOLDLOCK을 사용하여 동시 Upsert 충돌을 줄인다.",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "10.2", "저장 필드")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["구분", "DB Field", "출처"],
|
||
|
|
[
|
||
|
|
("식별", "IC_SN", "보드 READ_ID 응답"),
|
||
|
|
("식별", "PCB_Barcode", "바코드 스캔"),
|
||
|
|
("작업", "Maker, Model, Variant_1, Variant_2", "로그인 화면"),
|
||
|
|
("작업", "Operator, Line, Lot_No, Jig_No", "로그인 화면"),
|
||
|
|
("시간", "Production_Date", "저장 시점의 PC 로컬 시간(초 단위)"),
|
||
|
|
("측정", "PT_Vol_1", "전압 측정값"),
|
||
|
|
("측정", "PT_Current_1", "전류 측정값을 mA로 변환"),
|
||
|
|
("판정", "Result", "OK 또는 NG"),
|
||
|
|
],
|
||
|
|
[2.6, 6.4, 9.6],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "10.3", "DB 사전 조건")
|
||
|
|
add_bullet(doc, "dbo.Housing_Assembly 테이블은 운영 DB에 미리 존재해야 한다.")
|
||
|
|
add_bullet(doc, "init.sql은 선택적 DB 관리 스크립트다. 로그인 테이블/프로시저를 준비하고, Housing_Assembly가 이미 있는 경우 IC_SN Unique Index를 추가한다.")
|
||
|
|
add_bullet(doc, "DB 서버에 필요한 프로시저와 테이블/인덱스가 이미 구성되어 있다면 init.sql을 다시 실행할 필요가 없다.")
|
||
|
|
add_bullet(doc, "init.sql은 기본 계정 데이터를 생성 또는 갱신할 수 있으므로 운영 DB에서는 DBA 검토와 승인 없이 실행하지 않는다.")
|
||
|
|
add_bullet(doc, "중복 IC_SN이 존재하면 Unique Index 생성이 중단된다.")
|
||
|
|
add_bullet(doc, "Database.ini의 IP, Database, DbId가 비어 있으면 저장할 수 없다.")
|
||
|
|
add_bullet(doc, "현재 코드에는 DB 실패 시 CSV 등 로컬 대체 저장 기능이 없다.")
|
||
|
|
|
||
|
|
add_section(doc, "10.4", "DB 오류 확인")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"DB 저장 실패 팝업에는 예외 메시지와 대상 서버/DB/설정 파일 경로가 표시된다. 비밀번호는 표시되지 않지만 화면 캡처를 외부로 공유하기 전에 서버 정보가 포함되어 있는지 확인한다.",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_11(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 11, "로그아웃 및 종료")
|
||
|
|
add_section(doc, "11.1", "정상 로그아웃")
|
||
|
|
for text in [
|
||
|
|
"진행 중인 검사가 없는지 확인한다.",
|
||
|
|
"상단 LOGOUT 버튼을 누른다.",
|
||
|
|
"프로그램이 Current 채널의 LogoutCommand를 실행하여 전원 출력 OFF를 시도한다.",
|
||
|
|
"화면이 Reset되고 로그인 창이 다시 표시되는지 확인한다.",
|
||
|
|
"작업자 교대 시 새 작업 정보로 로그인한다.",
|
||
|
|
]:
|
||
|
|
add_number(doc, text)
|
||
|
|
|
||
|
|
add_section(doc, "11.2", "로그아웃 명령 실패")
|
||
|
|
add_body(
|
||
|
|
doc,
|
||
|
|
"전원 OFF 명령이 실패하면 경고 팝업이 표시되지만 프로그램은 화면을 Reset하고 로그인 창을 연다. 이 경우 실제 전원 공급 장치 출력 상태를 직접 확인하고 설비 담당자에게 보고한다.",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "11.3", "프로그램 종료")
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"종료 순서",
|
||
|
|
"창 닫기는 바코드 스캐너와 타이머를 정리하지만 LogoutCommand를 호출하지 않는다. 작업 종료 시 먼저 LOGOUT으로 전원 OFF 동작을 수행하고, 실제 장비 출력이 안전한지 확인한 다음 창을 닫는다.",
|
||
|
|
"danger",
|
||
|
|
)
|
||
|
|
add_bullet(doc, "검사 중 강제 종료하지 않는다.")
|
||
|
|
add_bullet(doc, "작업 관리자에서 Housing 프로세스를 강제 종료하는 것은 비상 상황에서만 수행한다.")
|
||
|
|
add_bullet(doc, "비정상 종료 후에는 보드·전원 공급 장치·치구 상태를 확인한 뒤 재실행한다.")
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_12(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 12, "오류 처리 및 문제 해결")
|
||
|
|
add_section(doc, "12.1", "표준 조치")
|
||
|
|
for text in [
|
||
|
|
"제품을 정상 제품 흐름에서 분리한다.",
|
||
|
|
"현재 PROCESS 상태와 팝업 메시지를 기록한다.",
|
||
|
|
"장비 전원, 케이블, COM 포트, LAN 상태를 확인한다.",
|
||
|
|
"관련 설정 파일을 승인본과 비교한다.",
|
||
|
|
"DB SAVE ERROR이면 IC_SN/PCB Barcode로 DB 저장 여부를 확인한다.",
|
||
|
|
"원인을 제거한 뒤 제품 재검사/재작업 기준에 따라 진행한다.",
|
||
|
|
"동일 오류가 반복되면 설비/IT 관리자에게 전달한다.",
|
||
|
|
]:
|
||
|
|
add_number(doc, text)
|
||
|
|
|
||
|
|
add_section(doc, "12.2", "주요 오류와 조치")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["상태/오류", "대표 원인", "조치"],
|
||
|
|
[
|
||
|
|
("Login 실패", "ID/PW 불일치, 로컬/DB 인증 설정", "LoginOptions, OfflinePreview, DB 프로시저 확인"),
|
||
|
|
("SCANNER ERROR", "COM 포트 없음/점유/설정 오류", "스캐너 전원, 포트, Baud, Board 포트 중복 확인"),
|
||
|
|
("SCAN ERROR", "바코드 형식 불일치", "라벨 품질과 허용 형식 확인 후 재스캔"),
|
||
|
|
("RANGE SETTING ERROR", "숫자 아님 또는 Min > Max", "승인된 V/mA 기준 재입력"),
|
||
|
|
("Start Timeout", "DI/LAN 신호 미수신 또는 Active 고정", "NI 채널/배선/ActiveState/RequireInactive 확인"),
|
||
|
|
("NI-DAQmx 오류", "Runtime 누락, 장치 미인식, 비트 불일치", "NI-DAQmx x64 Runtime 및 NI MAX 확인"),
|
||
|
|
("Board 연결 실패", "COM/케이블/명령/응답 오류", "Board Port, Baud, Success 응답 확인"),
|
||
|
|
("IC_SN 읽기 실패", "응답에 유효 Hex ID 없음", "제품 접촉, 보드 응답, ReadIdCommand 확인"),
|
||
|
|
("LAN IDN mismatch", "잘못된 계측기 IP 또는 IdnMatch", "*IDN? 응답과 설정 대조"),
|
||
|
|
("측정값 파싱 실패", "SCPI 응답에 숫자 없음", "ReadCommand, Terminator, 계측기 모드 확인"),
|
||
|
|
("DB SAVE ERROR", "DB 접속/권한/스키마/데이터 길이", "서버, 계정, 테이블, IC_SN 중복 및 필드 길이 확인"),
|
||
|
|
("Logout OFF 실패", "전원 공급 장치 연결/명령 오류", "출력 상태 수동 확인 후 설정/통신 점검"),
|
||
|
|
],
|
||
|
|
[4.0, 6.0, 8.2],
|
||
|
|
)
|
||
|
|
add_image(
|
||
|
|
doc,
|
||
|
|
DB_ERROR_IMAGE,
|
||
|
|
"그림 12-1. 측정 판정은 OK이나 DB 저장에 실패한 상태 예시",
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "12.3", "관리자에게 전달할 정보")
|
||
|
|
add_bullet(doc, "발생 시각, 작업자 ID, Line/Lot/Jig")
|
||
|
|
add_bullet(doc, "PCB Barcode와 IC_SN(표시된 경우)")
|
||
|
|
add_bullet(doc, "PROCESS 상태와 팝업 전문")
|
||
|
|
add_bullet(doc, "사용한 설정 파일 버전(비밀번호 제외)")
|
||
|
|
add_bullet(doc, "장비 전원/케이블/포트/네트워크 확인 결과")
|
||
|
|
add_bullet(doc, "재현 빈도와 재시도 결과")
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_13(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 13, "운영 주의사항 및 유지관리")
|
||
|
|
add_section(doc, "13.1", "생산 운영 주의")
|
||
|
|
add_bullet(doc, "검사 중 판정 범위를 변경하지 않는다.")
|
||
|
|
add_bullet(doc, "검사 중 보드, 스캐너, 계측기, NI-6501 케이블을 분리하지 않는다.")
|
||
|
|
add_bullet(doc, "NG 제품은 팝업의 실패 항목을 확인하고 정상 제품과 즉시 분리한다.")
|
||
|
|
add_bullet(doc, "DB 저장 실패 제품은 저장 여부 확인 전 출하 가능 상태로 처리하지 않는다.")
|
||
|
|
add_bullet(doc, "같은 PCB Barcode/IC_SN 재검사는 기존 DB 행을 덮어쓸 수 있으므로 재작업 절차를 따른다.")
|
||
|
|
|
||
|
|
add_section(doc, "13.2", "설정 변경 관리")
|
||
|
|
add_number(doc, "현재 설정 파일을 날짜/버전이 포함된 폴더에 백업한다.")
|
||
|
|
add_number(doc, "변경 사유, 변경자, 승인자, 적용 일시를 기록한다.")
|
||
|
|
add_number(doc, "비밀번호를 제외한 변경 항목을 동료 검토한다.")
|
||
|
|
add_number(doc, "비생산 제품으로 로그인·스캔·시작 신호·측정·DB 저장을 검증한다.")
|
||
|
|
add_number(doc, "검증 결과와 배포 파일 해시/버전을 보관한다.")
|
||
|
|
|
||
|
|
add_section(doc, "13.3", "정기 점검")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["주기", "점검 항목"],
|
||
|
|
[
|
||
|
|
("매 작업 시작", "장비 연결, 판정 범위, 로그인 정보, DB/시작 신호"),
|
||
|
|
("일일", "NG/오류 현황, DB 누락, 스캐너/치구 상태"),
|
||
|
|
("변경 시", "설정 백업, 비생산 검증, 승인 기록"),
|
||
|
|
("정기", "계측기 교정 유효기간, NI/통신 드라이버, DB 백업 및 계정 권한"),
|
||
|
|
],
|
||
|
|
[3.0, 13.0],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "13.4", "정보 보안")
|
||
|
|
add_bullet(doc, "Database.ini와 LoginOptions.json의 평문 자격 증명을 최소화한다.")
|
||
|
|
add_bullet(doc, "생산 PC 폴더 ACL을 필요한 관리자와 실행 계정으로 제한한다.")
|
||
|
|
add_bullet(doc, "기본/샘플 계정은 양산 전 변경 또는 제거한다.")
|
||
|
|
add_bullet(doc, "오류 화면 공유 시 서버 주소, 작업자 ID, 제품 식별자를 마스킹한다.")
|
||
|
|
|
||
|
|
|
||
|
|
def chapter_14(doc: Document) -> None:
|
||
|
|
add_chapter(doc, 14, "부록")
|
||
|
|
add_section(doc, "14.1", "설정 파일 경로 요약")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["파일", "실제 사용 위치", "비고"],
|
||
|
|
[
|
||
|
|
("Hardware.ini", "사용자가 Housing을 설치한 폴더의 Hardware.ini", "필수"),
|
||
|
|
("Database.ini", "사용자가 Housing을 설치한 폴더의 Database.ini", "필수"),
|
||
|
|
("LoginOptions.json", "사용자가 Housing을 설치한 폴더의 LoginOptions.json", "필수. 없으면 관리자에게 문의"),
|
||
|
|
("init.sql", "사용자가 Housing을 설치한 폴더의 init.sql", "선택 파일. 최초 DB 구성/변경 시 DBA 승인 후 실행"),
|
||
|
|
],
|
||
|
|
[4.0, 8.0, 4.0],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "14.2", "Hardware.ini 키 참조")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["영역", "대표 키", "허용/의미"],
|
||
|
|
[
|
||
|
|
("Board", "PortName / BaudRate / ReadTimeout", "COM 포트 / bps / ms"),
|
||
|
|
("BarcodeScanner", "Enabled / PortName", "True|False / COMx|Auto"),
|
||
|
|
("StartSignal", "Connection", "Ni6501 또는 Tcp/Lan/Ethernet"),
|
||
|
|
("StartSignal", "ActiveState", "High 또는 Low"),
|
||
|
|
("StartSignal", "TimeoutMilliseconds", "0 이하면 무제한, 그 외 ms"),
|
||
|
|
("Equipment", "*Connection", "Visa|Usb|Serial|Com|Tcp|Lan|Ethernet|None"),
|
||
|
|
("Equipment", "*IdnMatch", "쉼표로 구분된 허용 IDN 문자열"),
|
||
|
|
("Equipment", "*Setup/Cleanup/ReadCommand", "SCPI, 복수 명령은 ; 구분"),
|
||
|
|
],
|
||
|
|
[3.5, 6.0, 6.5],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "14.3", "용어")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["용어", "의미"],
|
||
|
|
[
|
||
|
|
("PCB Barcode", "PCB/PCBA 라벨에서 읽은 생산 추적 식별자"),
|
||
|
|
("IC_SN", "보드에서 읽은 8~32자리 16진수 IC 일련번호"),
|
||
|
|
("SCPI", "계측기 제어에 사용되는 표준 명령 체계"),
|
||
|
|
("VISA", "USB/LAN/계측기 통신 리소스 접근 계층"),
|
||
|
|
("NI-6501", "시작 신호에 사용하는 USB 디지털 I/O 장치"),
|
||
|
|
("DI", "Digital Input"),
|
||
|
|
("IDN", "SCPI *IDN? 장비 식별 응답"),
|
||
|
|
("Upsert", "같은 IC_SN이 있으면 Update, 없으면 Insert"),
|
||
|
|
("OK/NG", "판정 합격/불합격"),
|
||
|
|
],
|
||
|
|
[3.3, 12.7],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "14.4", "코드 기준 기능 매핑")
|
||
|
|
add_table(
|
||
|
|
doc,
|
||
|
|
["기능", "코드 기준"],
|
||
|
|
[
|
||
|
|
("프로그램 시작", "App.xaml.cs → MainWindow"),
|
||
|
|
("로그인", "Login/StartupLoginWindow.xaml(.cs)"),
|
||
|
|
("바코드 입력", "MainWindow.ProcessScannerTextAsync / SerialBarcodeScanner"),
|
||
|
|
("검사 전체 흐름", "MainWindow.RunBoardSequenceAsync"),
|
||
|
|
("시작 신호", "StartSignalWatcher / Ni6501StartSignalWatcher / TcpStartSignalWatcher"),
|
||
|
|
("보드 통신", "BoardTestService / SerialBoardClient"),
|
||
|
|
("전압·전류 측정", "EquipmentMeasurementService / IScpiClient 구현"),
|
||
|
|
("판정", "MainWindow.IsVoltagePass / IsCurrentPass"),
|
||
|
|
("설정 저장", "InspectionSettingsStore"),
|
||
|
|
("DB 저장", "HousingAssemblyRepository.UpsertInspectionAsync"),
|
||
|
|
("로그아웃 전원 OFF", "EquipmentMeasurementService.RunLogoutCommandAsync"),
|
||
|
|
],
|
||
|
|
[5.2, 10.8],
|
||
|
|
)
|
||
|
|
|
||
|
|
add_section(doc, "14.5", "빠른 작업 체크리스트")
|
||
|
|
checklist = [
|
||
|
|
"□ 설비·보드·계측기·스캐너·NI-6501 전원/연결 확인",
|
||
|
|
"□ 작업 정보(Maker/Model/Line/Lot/Jig) 확인",
|
||
|
|
"□ LOGIN ID 확인",
|
||
|
|
"□ V/mA 판정 범위 승인값 확인",
|
||
|
|
"□ 제품 장착 및 PCB 바코드 스캔",
|
||
|
|
"□ 시작 신호 입력",
|
||
|
|
"□ IC_SN 및 측정 진행 확인",
|
||
|
|
"□ OK/NG 및 DB SAVE 상태 확인",
|
||
|
|
"□ NG/오류 제품 분리 및 보고",
|
||
|
|
"□ 작업 종료 시 LOGOUT 후 장비 출력 확인",
|
||
|
|
]
|
||
|
|
for item in checklist:
|
||
|
|
add_body(doc, item)
|
||
|
|
|
||
|
|
add_note(
|
||
|
|
doc,
|
||
|
|
"문의 시",
|
||
|
|
"프로그램 버전, 발생 시각, 공정 상태, 제품 식별자, 오류 메시지, 설정 버전과 장비 연결 상태를 준비하십시오. 비밀번호 및 민감한 서버 정보는 전달하지 마십시오.",
|
||
|
|
"info",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def build_document() -> Path:
|
||
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
doc = Document()
|
||
|
|
configure_document(doc)
|
||
|
|
add_cover(doc)
|
||
|
|
add_front_matter(doc)
|
||
|
|
chapter_1(doc)
|
||
|
|
chapter_2(doc)
|
||
|
|
chapter_3(doc)
|
||
|
|
chapter_4(doc)
|
||
|
|
chapter_5(doc)
|
||
|
|
chapter_6(doc)
|
||
|
|
chapter_7(doc)
|
||
|
|
chapter_8(doc)
|
||
|
|
chapter_9(doc)
|
||
|
|
chapter_10(doc)
|
||
|
|
chapter_11(doc)
|
||
|
|
chapter_12(doc)
|
||
|
|
chapter_13(doc)
|
||
|
|
chapter_14(doc)
|
||
|
|
|
||
|
|
props = doc.core_properties
|
||
|
|
props.title = "Housing Assembly HMI 사용자 매뉴얼"
|
||
|
|
props.subject = "Housing 코드 정합형 사용자 매뉴얼"
|
||
|
|
props.author = "MOBIDIGM"
|
||
|
|
props.keywords = "Housing, HMI, User Manual, Barcode, Voltage, Current, SQL Server"
|
||
|
|
props.comments = "Generated from the Housing source tree and reference manual structure."
|
||
|
|
|
||
|
|
doc.save(OUT_FILE)
|
||
|
|
return OUT_FILE
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
result = build_document()
|
||
|
|
print(result)
|