fix(b09-print): 인쇄 미리보기 좌측 하단 두 줄(가로 넘침 없음) · 엑셀 시트 인쇄 설정 = PDF 방향 · A4 · 폭 1쪽 · 머리 줄 반복 (302-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -50,3 +50,31 @@
|
||||
font-size: var(--text-body-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 좌측 하단 두 줄(302-5 · B09 Dock 꼴) — 단추는 줄 폭을 나눠 가짐 · 가로 넘침 없음 */
|
||||
.b09-print__actions.ui-sidebar-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.b09-print__actions.ui-sidebar-actions > * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.b09-print__actions .b09-print__row {
|
||||
display: flex;
|
||||
gap: var(--btn-group-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-sidebar-actions.b09-print__actions .b09-print__row > .ui-btn:not(.ui-help-btn) {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding-inline: var(--spacing-4);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b09-print__actions .b09-print__row > .ui-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -268,9 +268,17 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
);
|
||||
};
|
||||
|
||||
// 두 줄(302-5) — 위 = 내려받기 · 아래 = [?](도움말 줄) · 돌아가기 — 패널 폭 넘침 없음
|
||||
const files = document.createElement("div");
|
||||
files.className = "b09-print__row";
|
||||
files.append(pdfBtn, xlsxBtn);
|
||||
const nav = document.createElement("div");
|
||||
nav.className = "b09-print__row";
|
||||
nav.dataset.helpRow = "";
|
||||
nav.append(back);
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "ui-sidebar-actions";
|
||||
actions.append(pdfBtn, xlsxBtn, back);
|
||||
actions.className = "ui-sidebar-actions b09-print__actions";
|
||||
actions.append(files, nav);
|
||||
const left = document.createElement("div");
|
||||
left.className = "ui-sidebar-panel b09-print__panel";
|
||||
left.append(areaCard, sheetCard, actions);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
POST /api/projects/{id}/print/table-xlsx {title, sheets:[{name, grid}]}
|
||||
grid(`B09_Draft_Print_Table.sheetGrid`) → 엑셀 — 시트 = 차례대로 · 이름 31자 ·
|
||||
금지 글자 바꿈 · 머리 굵게 · 병합 · 테두리 · 숫자 = 숫자 칸(열 소수 서식) ·
|
||||
레벨 = 들여쓰기 · 열 폭
|
||||
레벨 = 들여쓰기 · 열 폭 · 인쇄 = 시트 방향 · A4 · 폭 1쪽 · 머리 줄 반복
|
||||
|
||||
`B09_Estimation_Router` 가 draft/*_Router.py 의 router 를 끼워 넣음
|
||||
(같은 머리 · 같은 권한 = 회사 · 프로젝트).
|
||||
@@ -199,6 +199,22 @@ def number_format(decimals: int | None) -> str:
|
||||
return "#,##0" + ("." + "0" * decimals if decimals else "")
|
||||
|
||||
|
||||
def print_setup(ws: Any, orientation: str, head_rows: int) -> None:
|
||||
"""인쇄 설정(302-5) — PDF 와 같은 방향 · A4 · 폭 1쪽 맞춤 · 머리 줄 쪽마다 · 여백 ≈ PDF."""
|
||||
from openpyxl.worksheet.page import PageMargins
|
||||
|
||||
ws.page_setup.orientation = orientation
|
||||
ws.page_setup.paperSize = ws.PAPERSIZE_A4
|
||||
ws.page_setup.fitToWidth = 1
|
||||
ws.page_setup.fitToHeight = 0
|
||||
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
||||
ws.page_margins = PageMargins(left=0.4, right=0.4, top=0.4, bottom=0.55, header=0.2, footer=0.2)
|
||||
if head_rows:
|
||||
ws.print_title_rows = f"1:{head_rows}"
|
||||
ws.oddHeader.left.text = "&A" # 시트 이름
|
||||
ws.oddFooter.center.text = "&P / &N" # 쪽 번호
|
||||
|
||||
|
||||
def build_xlsx(sheets: list[dict[str, Any]]) -> bytes:
|
||||
"""grid 시트들 → 엑셀 바이트(차례 그대로)."""
|
||||
from openpyxl import Workbook
|
||||
@@ -255,6 +271,7 @@ def build_xlsx(sheets: list[dict[str, Any]]) -> bytes:
|
||||
ws.cell(top + 1 + m["r"], m["c"] + 1).alignment = Alignment(vertical="center")
|
||||
if top:
|
||||
ws.freeze_panes = ws.cell(top + 1, (g.get("frozen") or 0) + 1)
|
||||
print_setup(ws, sheet.get("orientation") or "portrait", top)
|
||||
out = io.BytesIO()
|
||||
wb.save(out)
|
||||
return out.getvalue()
|
||||
@@ -292,6 +309,7 @@ class PdfSheet(BaseModel):
|
||||
|
||||
class XlsxSheet(BaseModel):
|
||||
name: str = Field(max_length=200)
|
||||
orientation: Literal["portrait", "landscape"] = "portrait"
|
||||
grid: dict[str, Any]
|
||||
|
||||
|
||||
|
||||
@@ -134,7 +134,11 @@ export const pdfBody = (title: string, sheets: PrintSheet[]) => ({
|
||||
|
||||
export const xlsxBody = (title: string, sheets: PrintSheet[]) => ({
|
||||
title,
|
||||
sheets: sheets.map((s) => ({ name: s.name, grid: sheetGrid(s.table) })),
|
||||
sheets: sheets.map((s) => ({
|
||||
name: s.name,
|
||||
orientation: s.orientation ?? "portrait",
|
||||
grid: sheetGrid(s.table),
|
||||
})),
|
||||
});
|
||||
|
||||
/** 그림 주소 → data: 주소(서버 헤드리스는 바깥 요청을 막음 · 로그인 쿠키로 브라우저가 받아 둠) */
|
||||
|
||||
@@ -214,15 +214,30 @@ def test_table_pdf_바깥_요청_막음():
|
||||
pytest.skip(str(error))
|
||||
|
||||
|
||||
def _assert_print(wb, orientations: list[str], heads: list[int]) -> None:
|
||||
"""엑셀 인쇄 설정(302-5) — 시트 방향 = PDF · A4 · 폭 1쪽 · 높이 자유 · 머리 줄 반복."""
|
||||
assert len(wb.worksheets) == len(orientations)
|
||||
for ws, orient, head in zip(wb.worksheets, orientations, heads):
|
||||
ps = ws.page_setup
|
||||
assert ps.orientation == orient, ws.title
|
||||
assert int(ps.paperSize) == 9 # A4
|
||||
assert (int(ps.fitToWidth), int(ps.fitToHeight)) == (1, 0), ws.title
|
||||
assert ws.sheet_properties.pageSetUpPr.fitToPage is True
|
||||
assert ws.print_title_rows.replace("$", "") == f"1:{head}", ws.title
|
||||
assert ws.page_margins.left < 0.5 and ws.page_margins.bottom < 0.6
|
||||
|
||||
|
||||
def test_table_xlsx_시트_차례_이름_머리_병합_숫자_칸():
|
||||
from openpyxl import load_workbook
|
||||
|
||||
sheets = _sheets()
|
||||
body = {"title": "수량", "sheets": [{"name": s["name"], "grid": s["grid"]} for s in sheets]}
|
||||
keys = ("name", "orientation", "grid")
|
||||
body = {"title": "수량", "sheets": [{k: s[k] for k in keys} for s in sheets]}
|
||||
body["sheets"].append({"name": "갑 _ 수량_1_", "grid": sheets[0]["grid"]}) # 같은 이름
|
||||
res = _client().post(f"/api/projects/{PID}/print/table-xlsx", json=body)
|
||||
assert res.status_code == 200, res.text
|
||||
wb = load_workbook(io.BytesIO(res.content))
|
||||
_assert_print(wb, [*[s["orientation"] for s in sheets], "portrait"], [2, 2, 2])
|
||||
names = wb.sheetnames
|
||||
assert names[0] == "갑 _ 수량_1_" # 금지 글자 = _
|
||||
assert (
|
||||
@@ -313,6 +328,11 @@ def test_원가_영역_엑셀_18장_차례_그대로(cost_bodies):
|
||||
first = wb[wb.sheetnames[0]]
|
||||
assert first.max_row > body["sheets"][0]["grid"]["head_rows"] # 값 줄이 들어감
|
||||
assert any(c.data_type == "n" for row in first.iter_rows() for c in row if c.value is not None)
|
||||
_assert_print( # 엑셀 인쇄 방향 = 그 시트 PDF 방향(302-5)
|
||||
wb,
|
||||
[s["orientation"] for s in cost_bodies["pdf"]["sheets"]],
|
||||
[s["grid"]["head_rows"] for s in body["sheets"]],
|
||||
)
|
||||
|
||||
|
||||
def test_원가_영역_PDF_시트_이름_가로(cost_bodies):
|
||||
@@ -375,6 +395,11 @@ def test_수량_영역_엑셀_차례_그대로(quantity_bodies):
|
||||
assert all(
|
||||
wb[n].max_row >= s["grid"]["head_rows"] for n, s in zip(wb.sheetnames, body["sheets"])
|
||||
)
|
||||
_assert_print(
|
||||
wb,
|
||||
[s["orientation"] for s in quantity_bodies["pdf"]["sheets"]],
|
||||
[s["grid"]["head_rows"] for s in body["sheets"]],
|
||||
)
|
||||
|
||||
|
||||
def test_수량_영역_PDF_시트_이름_지도_그림(quantity_bodies):
|
||||
@@ -401,3 +426,17 @@ def test_B09_좌측_하단_단추는_초안_입구에서만():
|
||||
assert 'import.meta.glob("./draft/*_UI.ts", { eager: true })' in shell
|
||||
dock = _text(ROOT / "B09_Estimation/B09_Estimation_UI_Dock.ts")
|
||||
assert "DRAFT.actions.map((make) => make())" in dock and "Print" not in dock
|
||||
|
||||
|
||||
def test_좌측_하단_두_줄_가로_넘침_없음():
|
||||
"""302-5 — 위 = [PDF][엑셀] · 아래 = [?](도움말 줄) [B09 로 돌아가기] · 줄 폭을 나눠 가짐."""
|
||||
page = _text(DRAFT / "B09_Draft_Print_Page.ts")
|
||||
assert "files.append(pdfBtn, xlsxBtn)" in page
|
||||
assert "nav.dataset.helpRow" in page and "nav.append(back)" in page
|
||||
assert "actions.append(files, nav)" in page
|
||||
css = re.sub(r"\s+", " ", _text(DRAFT / "B09_Draft_Print_Page.css"))
|
||||
assert ".b09-print__actions.ui-sidebar-actions { flex-direction: column; }" in css
|
||||
btn = css[css.index(".ui-sidebar-actions.b09-print__actions .b09-print__row > .ui-btn") :]
|
||||
btn = btn[: btn.index("}")]
|
||||
# 공용 단추 줄(min-width: auto = 글보다 안 좁아짐)보다 앞서 이기는 규칙 · 글 줄임표
|
||||
assert "flex: 1 1 0" in btn and "min-width: 0" in btn and "text-overflow: ellipsis" in btn
|
||||
|
||||
Reference in New Issue
Block a user