Update app.py
Browse files
app.py
CHANGED
|
@@ -1,221 +1,240 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import pandas as pd
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
s = series.astype(str).fillna("")
|
| 11 |
-
|
| 12 |
-
if len(
|
| 13 |
return "0000-0000"
|
| 14 |
-
|
| 15 |
-
if len(
|
| 16 |
return "0000-0000"
|
| 17 |
-
return f"{
|
| 18 |
|
| 19 |
-
def
|
| 20 |
df = df.copy()
|
| 21 |
df["박스번호"] = df["박스번호"].astype(str).str.zfill(4)
|
| 22 |
-
|
| 23 |
-
df["제목"] = df["제목"].astype(str)
|
| 24 |
-
|
| 25 |
-
# 생산연도(범위) = 종료연도 그룹 범위
|
| 26 |
if "종료연도" in df.columns:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
else:
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# 목록(관리번호 + 제목)
|
| 33 |
has_mgmt = "관리번호" in df.columns
|
| 34 |
list_rows = []
|
| 35 |
for box, g in df.groupby("박스번호"):
|
| 36 |
-
lines = [f"- {r['관리번호']} {r
|
| 37 |
for _, r in g.iterrows()]
|
| 38 |
-
list_rows.append({"박스번호": box, "목록": "\
|
| 39 |
list_df = pd.DataFrame(list_rows)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
meta_exist = [c for c in
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
out = write_hwpx_like_src(zin, writer)
|
| 138 |
-
zin.close()
|
| 139 |
-
return (out, dbg) if collect_debug else (out, None)
|
| 140 |
-
|
| 141 |
-
# ================= UI =================
|
| 142 |
-
with st.expander("사용법", expanded=True):
|
| 143 |
-
st.markdown("""
|
| 144 |
-
- 템플릿은 **한글 필드컨트롤**이어야 합니다. (예: `name="박스번호1"`)
|
| 145 |
-
- 이 앱은 필드 구간을 **평문화(필드 제거)** 하여 값 run들로 바꿉니다. → 한글 뷰어에서 **항상 보임**.
|
| 146 |
-
- 라벨 한 페이지에 N개면, 필드명은 `박스번호1..N`, `종료연도1..N`, `보존기간1..N`, `단위업무1..N`, `기록물철1..N`, `목록1..N`.
|
| 147 |
-
""")
|
| 148 |
-
|
| 149 |
-
tpl_file = st.file_uploader("📄 HWPX 템플릿 업로드", type=["hwpx"])
|
| 150 |
-
batch_size = st.number_input("템플릿의 라벨 세트 개수 (한 페이지 N개)", min_value=1, max_value=12, value=3, step=1)
|
| 151 |
-
data_file = st.file_uploader("📊 데이터 업로드 (Excel/CSV)", type=["xlsx","xls","csv"])
|
| 152 |
-
|
| 153 |
-
if tpl_file and data_file:
|
| 154 |
-
tpl_bytes = tpl_file.read()
|
| 155 |
-
df = pd.read_csv(data_file) if data_file.name.lower().endswith(".csv") else pd.read_excel(data_file)
|
| 156 |
-
|
| 157 |
-
if "박스번호" not in df.columns:
|
| 158 |
-
st.error("❌ 필수 컬럼 '박스번호'가 없습니다.")
|
| 159 |
-
st.stop()
|
| 160 |
-
|
| 161 |
-
st.success("✅ 위치 매핑 완료 (엑셀 측)")
|
| 162 |
st.dataframe(df.head(10), use_container_width=True)
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
st.
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# 1페이지 프리뷰
|
| 176 |
-
st.subheader("🧪 1페이지 매핑 프리뷰")
|
| 177 |
-
keys = ["박스번호","종료연도","보존기간","단위업무","기록물철","목록"]
|
| 178 |
-
n = int(batch_size)
|
| 179 |
-
preview = {}
|
| 180 |
-
for i in range(n):
|
| 181 |
-
if i < len(rows):
|
| 182 |
-
r = rows[i]
|
| 183 |
-
for k in keys:
|
| 184 |
-
preview[f"{k}{i+1}"] = r.get("생산연도","") if k=="종료연도" else r.get(k,"")
|
| 185 |
-
else:
|
| 186 |
-
for k in keys:
|
| 187 |
-
preview[f"{k}{i+1}"] = ""
|
| 188 |
-
st.dataframe(
|
| 189 |
-
pd.DataFrame([{"필드명":k, "값 앞부분":str(v)[:120]} for k,v in sorted(preview.items())]),
|
| 190 |
-
use_container_width=True, height=320
|
| 191 |
-
)
|
| 192 |
-
|
| 193 |
-
if st.button("🚀 라벨 생성 (페이지별 HWPX ZIP)"):
|
| 194 |
-
mem_zip = io.BytesIO()
|
| 195 |
-
zout = zipfile.ZipFile(mem_zip, "w", zipfile.ZIP_DEFLATED)
|
| 196 |
-
pages = (len(rows) + n - 1) // n
|
| 197 |
-
all_dbg = []
|
| 198 |
-
|
| 199 |
-
for p in range(pages):
|
| 200 |
-
chunk = rows[p*n:(p+1)*n]
|
| 201 |
-
mapping = {}
|
| 202 |
-
for i in range(n):
|
| 203 |
-
if i < len(chunk):
|
| 204 |
-
r = chunk[i]
|
| 205 |
-
for k in keys:
|
| 206 |
-
mapping[f"{k}{i+1}"] = r.get("생산연도","") if k=="종료연도" else r.get(k,"")
|
| 207 |
-
else:
|
| 208 |
-
for k in keys:
|
| 209 |
-
mapping[f"{k}{i+1}"] = ""
|
| 210 |
-
|
| 211 |
-
out_hwpx, dbg = apply_field_flatten(tpl_bytes, mapping, collect_debug=True)
|
| 212 |
-
all_dbg.append({"page": p+1, "stats": dbg})
|
| 213 |
-
name = "_".join([r.get("박스번호","") for r in chunk]) if chunk else f"empty_{p+1}"
|
| 214 |
-
zout.writestr(f"label_{name}.hwpx", out_hwpx)
|
| 215 |
-
|
| 216 |
-
zout.close(); mem_zip.seek(0)
|
| 217 |
-
st.download_button("⬇️ ZIP 다운로드", data=mem_zip, file_name="labels_by_page.zip", mime="application/zip")
|
| 218 |
-
st.download_button("⬇️ 디버그(JSON)", data=json.dumps(all_dbg, ensure_ascii=False, indent=2),
|
| 219 |
-
file_name="debug.json", mime="application/json")
|
| 220 |
-
|
| 221 |
-
st.caption("필드 구간을 통째로 값 run들로 교체합니다. (필드 제거 → 값이 확실히 보입니다)")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import pandas as pd
|
| 3 |
+
from reportlab.pdfgen import canvas
|
| 4 |
+
from reportlab.pdfbase import pdfmetrics
|
| 5 |
+
from reportlab.pdfbase.ttfonts import TTFont
|
| 6 |
+
from reportlab.lib.pagesizes import A4
|
| 7 |
+
from reportlab.lib.units import mm
|
| 8 |
+
from io import BytesIO
|
| 9 |
+
import math
|
| 10 |
+
|
| 11 |
+
st.set_page_config(page_title="📦 박스라벨 PDF 출력기", layout="wide")
|
| 12 |
+
st.title("📦 박스라벨 PDF 출력기 (라벨 규격 커스텀 / 한국어 폰트 업로드)")
|
| 13 |
+
|
| 14 |
+
with st.expander("사용 방법", expanded=True):
|
| 15 |
+
st.markdown("""
|
| 16 |
+
1. **엑셀/CSV 업로드** → 필수 컬럼: `박스번호` / 권장: `종료연도`, `보존기간`, `단위업무`, `기록물철`, `제목`, `관리번호`
|
| 17 |
+
2. (선택) **TTF 폰트 업로드**(예: 나눔고딕, 본고딕, 맑은 고딕 등). 업로드 안 하면 기본 폰트 사용(영문 위주).
|
| 18 |
+
3. **라벨 규격**(페이지 여백, 라벨 가로/세로, 행/열, 라벨 간격)을 입력.
|
| 19 |
+
4. **텍스트 배치**(라벨 안쪽 패딩, 폰트 크기, 줄 간격 등) 조정.
|
| 20 |
+
5. **PDF 생성** → 라벨 용지(Formtec 등)에 인쇄.
|
| 21 |
+
""")
|
| 22 |
+
|
| 23 |
+
# -----------------
|
| 24 |
+
# 데이터 로드
|
| 25 |
+
# -----------------
|
| 26 |
+
file = st.file_uploader("📊 데이터 업로드 (Excel/CSV)", type=["xlsx","xls","csv"])
|
| 27 |
+
df = None
|
| 28 |
+
if file:
|
| 29 |
+
if file.name.lower().endswith(".csv"):
|
| 30 |
+
df = pd.read_csv(file)
|
| 31 |
+
else:
|
| 32 |
+
df = pd.read_excel(file)
|
| 33 |
+
|
| 34 |
+
# 필수 컬럼 검사
|
| 35 |
+
if df is not None and "박스번호" not in df.columns:
|
| 36 |
+
st.error("❌ 필수 컬럼 '박스번호'가 없습니다.")
|
| 37 |
+
st.stop()
|
| 38 |
+
|
| 39 |
+
# -----------------
|
| 40 |
+
# 폰트 설정
|
| 41 |
+
# -----------------
|
| 42 |
+
st.subheader("🔤 폰트 설정")
|
| 43 |
+
font_file = st.file_uploader("한국어 폰트(TTF) 업로드 (예: NanumGothic.ttf / MalgunGothic.ttf)", type=["ttf"])
|
| 44 |
+
font_name = "BaseFont"
|
| 45 |
+
if font_file:
|
| 46 |
+
try:
|
| 47 |
+
font_bytes = font_file.read()
|
| 48 |
+
# 메모리 등록: ReportLab은 파일 경로가 필요 → 임시 파일 만들기보다 메모리 레지스터 트릭
|
| 49 |
+
# 하지만 TTFont는 파일 경로 요구 → 임시파일 저장
|
| 50 |
+
import tempfile
|
| 51 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ttf")
|
| 52 |
+
tmp.write(font_bytes); tmp.flush()
|
| 53 |
+
pdfmetrics.registerFont(TTFont("UserKorean", tmp.name))
|
| 54 |
+
font_name = "UserKorean"
|
| 55 |
+
st.success("✅ 폰트 등록 완료: UserKorean")
|
| 56 |
+
except Exception as e:
|
| 57 |
+
st.warning(f"폰트 등록 실패. 기본 폰트 사용합니다. (사유: {e})")
|
| 58 |
+
else:
|
| 59 |
+
# 내장 기본 폰트 (영문 중심)
|
| 60 |
+
font_name = "Helvetica"
|
| 61 |
+
|
| 62 |
+
# -----------------
|
| 63 |
+
# 라벨/페이지 레이아웃
|
| 64 |
+
# -----------------
|
| 65 |
+
st.subheader("📐 라벨 규격 (mm 단위)")
|
| 66 |
+
colA, colB, colC = st.columns(3)
|
| 67 |
+
with colA:
|
| 68 |
+
page_size = st.selectbox("페이지 크기", ["A4"], index=0)
|
| 69 |
+
with colB:
|
| 70 |
+
margin_left = st.number_input("왼쪽 여백(mm)", 5.0, 50.0, 10.0, 0.5)
|
| 71 |
+
margin_top = st.number_input("상단 여백(mm)", 5.0, 50.0, 10.0, 0.5)
|
| 72 |
+
with colC:
|
| 73 |
+
rows = st.number_input("행 수", 1, 20, 10, 1)
|
| 74 |
+
cols = st.number_input("열 수", 1, 10, 3, 1)
|
| 75 |
+
|
| 76 |
+
colD, colE, colF = st.columns(3)
|
| 77 |
+
with colD:
|
| 78 |
+
label_w = st.number_input("라벨 가로(mm)", 20.0, 210.0, 70.0, 0.5)
|
| 79 |
+
with colE:
|
| 80 |
+
label_h = st.number_input("라벨 세로(mm)", 10.0, 297.0, 25.0, 0.5)
|
| 81 |
+
with colF:
|
| 82 |
+
gap_x = st.number_input("가로 간격(mm)", 0.0, 20.0, 3.0, 0.5)
|
| 83 |
+
gap_y = st.number_input("세로 간격(mm)", 0.0, 20.0, 3.0, 0.5)
|
| 84 |
+
|
| 85 |
+
# -----------------
|
| 86 |
+
# 라벨 내부 텍스트 배치
|
| 87 |
+
# -----------------
|
| 88 |
+
st.subheader("🧱 라벨 내부 레이아웃")
|
| 89 |
+
col1, col2, col3 = st.columns(3)
|
| 90 |
+
with col1:
|
| 91 |
+
pad_x = st.number_input("내부 패딩 X(mm)", 0.0, 20.0, 2.0, 0.5)
|
| 92 |
+
pad_y = st.number_input("내부 패딩 Y(mm)", 0.0, 20.0, 2.0, 0.5)
|
| 93 |
+
with col2:
|
| 94 |
+
fs_big = st.number_input("폰트 크기(큰 제목)", 6, 40, 16, 1)
|
| 95 |
+
fs_mid = st.number_input("폰트 크기(중간)", 6, 40, 11, 1)
|
| 96 |
+
with col3:
|
| 97 |
+
fs_small = st.number_input("폰트 크기(작게/목록)", 6, 20, 9, 1)
|
| 98 |
+
line_gap = st.number_input("줄 간격(배수)", 0.8, 2.0, 1.2, 0.1)
|
| 99 |
+
|
| 100 |
+
st.caption("💡 Formtec 3203 비슷한 설정 예시: 가로 70, 세로 25, 열 3, 행 10, 여백 10/10, 간격 3/3 (프린터마다 약간 조정)")
|
| 101 |
+
|
| 102 |
+
# -----------------
|
| 103 |
+
# 텍스트 생성 함수
|
| 104 |
+
# -----------------
|
| 105 |
+
def year_range(series):
|
| 106 |
s = series.astype(str).fillna("")
|
| 107 |
+
v = s[~s.isin(["", "0", "0000"])]
|
| 108 |
+
if len(v) == 0:
|
| 109 |
return "0000-0000"
|
| 110 |
+
nums = pd.to_numeric(v, errors="coerce").dropna().astype(int)
|
| 111 |
+
if len(nums) == 0:
|
| 112 |
return "0000-0000"
|
| 113 |
+
return f"{nums.min():04d}-{nums.max():04d}"
|
| 114 |
|
| 115 |
+
def build_records(df: pd.DataFrame):
|
| 116 |
df = df.copy()
|
| 117 |
df["박스번호"] = df["박스번호"].astype(str).str.zfill(4)
|
| 118 |
+
# 생산연도(범위)
|
|
|
|
|
|
|
|
|
|
| 119 |
if "종료연도" in df.columns:
|
| 120 |
+
yr = df.groupby("박스번호")["종료연도"].apply(year_range).reset_index()
|
| 121 |
+
yr.columns = ["박스번호", "생산연도"]
|
| 122 |
else:
|
| 123 |
+
yr = pd.DataFrame({"박스번호": df["박스번호"].unique(), "생산연도": "0000-0000"})
|
| 124 |
+
# 목록
|
|
|
|
| 125 |
has_mgmt = "관리번호" in df.columns
|
| 126 |
list_rows = []
|
| 127 |
for box, g in df.groupby("박스번호"):
|
| 128 |
+
lines = [f"- {r['관리번호']} {r.get('제목','')}" if has_mgmt else f"- {r.get('제목','')}"
|
| 129 |
for _, r in g.iterrows()]
|
| 130 |
+
list_rows.append({"박스번호": box, "목록": "\n".join(lines)})
|
| 131 |
list_df = pd.DataFrame(list_rows)
|
| 132 |
+
# 대표 메타
|
| 133 |
+
cols = ["박스번호","보존기간","단위업무","기록물철","제목"]
|
| 134 |
+
meta_exist = [c for c in cols if c in df.columns]
|
| 135 |
+
meta = df.groupby("박스번호", as_index=False).first()[meta_exist] if meta_exist else pd.DataFrame({"박스번호": df["박스번호"].unique()})
|
| 136 |
+
merged = meta.merge(list_df, on="박스번호", how="left").merge(yr, on="박스번호", how="left")
|
| 137 |
+
return merged.sort_values("박스번호").to_dict(orient="records")
|
| 138 |
+
|
| 139 |
+
def draw_label(c: canvas.Canvas, x, y, w, h, rec, font_name, fs_big, fs_mid, fs_small, line_gap):
|
| 140 |
+
"""
|
| 141 |
+
좌표계: reportlab은 좌하단이 원점.
|
| 142 |
+
x,y = 라벨 좌하단. w,h = 라벨 크기.
|
| 143 |
+
"""
|
| 144 |
+
# 여백
|
| 145 |
+
inner_x = x + pad_x * mm
|
| 146 |
+
inner_y = y + pad_y * mm
|
| 147 |
+
inner_w = w - 2 * pad_x * mm
|
| 148 |
+
inner_h = h - 2 * pad_y * mm
|
| 149 |
+
|
| 150 |
+
# 상단 굵은 줄: 박스번호
|
| 151 |
+
c.setFont(font_name, fs_big)
|
| 152 |
+
boxno = rec.get("박스번호", "")
|
| 153 |
+
c.drawString(inner_x, inner_y + inner_h - fs_big*1.1, f"{boxno}")
|
| 154 |
+
|
| 155 |
+
# 2행: (생산연도/보존기간)
|
| 156 |
+
c.setFont(font_name, fs_mid)
|
| 157 |
+
prod = rec.get("생산연도","")
|
| 158 |
+
keep = rec.get("보존기간","") or ""
|
| 159 |
+
line_y = inner_y + inner_h - fs_big*1.1 - fs_mid*1.5
|
| 160 |
+
c.drawString(inner_x, line_y, f"{prod} {keep}")
|
| 161 |
+
|
| 162 |
+
# 3행: 단위업무 / 기록물철 (있으면)
|
| 163 |
+
line_y -= fs_mid * 1.2
|
| 164 |
+
unit = rec.get("단위업무","") or ""
|
| 165 |
+
series = rec.get("기록물철","") or ""
|
| 166 |
+
if unit or series:
|
| 167 |
+
c.setFont(font_name, fs_mid)
|
| 168 |
+
c.drawString(inner_x, line_y, f"{unit} {series}")
|
| 169 |
+
line_y -= fs_mid * 1.0
|
| 170 |
+
|
| 171 |
+
# 목록(여러 줄, 작은 글씨)
|
| 172 |
+
c.setFont(font_name, fs_small)
|
| 173 |
+
list_text = rec.get("목록","") or ""
|
| 174 |
+
for ln in list_text.split("\n"):
|
| 175 |
+
if line_y < inner_y + fs_small * 1.2: # 라벨 하단 넘어가면 중단
|
| 176 |
+
break
|
| 177 |
+
c.drawString(inner_x, line_y, ln)
|
| 178 |
+
line_y -= fs_small * line_gap
|
| 179 |
+
|
| 180 |
+
def make_pdf(records):
|
| 181 |
+
buffer = BytesIO()
|
| 182 |
+
if page_size == "A4":
|
| 183 |
+
pw, ph = A4
|
| 184 |
+
else:
|
| 185 |
+
pw, ph = A4
|
| 186 |
+
|
| 187 |
+
c = canvas.Canvas(buffer, pagesize=(pw, ph))
|
| 188 |
+
c.setAuthor("BoxLabel")
|
| 189 |
+
c.setTitle("Box Labels")
|
| 190 |
+
|
| 191 |
+
pdfmetrics.getFont(font_name) # ensure registered
|
| 192 |
+
|
| 193 |
+
# 좌표/크기(mm → pt)
|
| 194 |
+
L = margin_left * mm
|
| 195 |
+
T = margin_top * mm
|
| 196 |
+
W = label_w * mm
|
| 197 |
+
H = label_h * mm
|
| 198 |
+
GX = gap_x * mm
|
| 199 |
+
GY = gap_y * mm
|
| 200 |
+
|
| 201 |
+
per_page = int(rows * cols)
|
| 202 |
+
total_pages = math.ceil(len(records) / per_page) if records else 1
|
| 203 |
+
|
| 204 |
+
idx = 0
|
| 205 |
+
for p in range(total_pages):
|
| 206 |
+
for r in range(int(rows)):
|
| 207 |
+
for ccol in range(int(cols)):
|
| 208 |
+
if idx >= len(records):
|
| 209 |
+
break
|
| 210 |
+
# 좌표 계산 (좌하단 원점이므로 상단에서 내려오게 Y를 조정)
|
| 211 |
+
x = L + ccol * (W + GX)
|
| 212 |
+
y_top = ph - T - r * (H + GY)
|
| 213 |
+
y = y_top - H
|
| 214 |
+
draw_label(c, x, y, W, H, records[idx], font_name, fs_big, fs_mid, fs_small, line_gap)
|
| 215 |
+
idx += 1
|
| 216 |
+
if idx >= len(records):
|
| 217 |
+
break
|
| 218 |
+
c.showPage()
|
| 219 |
+
c.save()
|
| 220 |
+
buffer.seek(0)
|
| 221 |
+
return buffer
|
| 222 |
+
|
| 223 |
+
# -----------------
|
| 224 |
+
# 메인 동작
|
| 225 |
+
# -----------------
|
| 226 |
+
if df is not None:
|
| 227 |
+
# 미리보기
|
| 228 |
+
st.subheader("📋 데이터 미리보기")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
st.dataframe(df.head(10), use_container_width=True)
|
| 230 |
|
| 231 |
+
records = build_records(df)
|
| 232 |
+
st.write(f"총 **{len(records)}**개 박스가 감지되었습니다.")
|
| 233 |
+
default_sel = [r["박스번호"] for r in records]
|
| 234 |
+
sel = st.multiselect("생성할 박스번호 선택 (비우면 전체)", options=default_sel)
|
| 235 |
+
if sel:
|
| 236 |
+
records = [r for r in records if r["박스번호"] in set(sel)]
|
| 237 |
+
|
| 238 |
+
if st.button("🚀 PDF 생성"):
|
| 239 |
+
pdf = make_pdf(records)
|
| 240 |
+
st.download_button("⬇️ PDF 다운로드", data=pdf.getvalue(), file_name="box_labels.pdf", mime="application/pdf")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|