dohyune commited on
Commit
fbfae11
ยท
verified ยท
1 Parent(s): 715aec7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +223 -204
app.py CHANGED
@@ -1,221 +1,240 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import io, zipfile, re, html, json
4
-
5
- st.set_page_config(page_title="๐Ÿ“ฆ ๋ฐ•์Šค๋ผ๋ฒจ ์ž๋™ ์ƒ์„ฑ๊ธฐ (HWPX ํ•„๋“œ ํ‰๋ฌธํ™”)", layout="wide")
6
- st.title("๐Ÿ“ฆ ๋ฐ•์Šค๋ผ๋ฒจ ์ž๋™ ์ƒ์„ฑ๊ธฐ โ€” HWPX **ํ•„๋“œ ์ œ๊ฑฐ/ํ‰๋ฌธํ™” ๋ฐฉ์‹**")
7
-
8
- # ================= ๊ณตํ†ต ์œ ํ‹ธ =================
9
- def compute_year_range(series: pd.Series) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  s = series.astype(str).fillna("")
11
- valid = s[~s.isin(["", "0", "0000"])]
12
- if len(valid) == 0:
13
  return "0000-0000"
14
- valid_int = pd.to_numeric(valid, errors="coerce").dropna().astype(int)
15
- if len(valid_int) == 0:
16
  return "0000-0000"
17
- return f"{valid_int.min():04d}-{valid_int.max():04d}"
18
 
19
- def build_merged_df(df: pd.DataFrame) -> pd.DataFrame:
20
  df = df.copy()
21
  df["๋ฐ•์Šค๋ฒˆํ˜ธ"] = df["๋ฐ•์Šค๋ฒˆํ˜ธ"].astype(str).str.zfill(4)
22
- if "์ œ๋ชฉ" in df.columns:
23
- df["์ œ๋ชฉ"] = df["์ œ๋ชฉ"].astype(str)
24
-
25
- # ์ƒ์‚ฐ์—ฐ๋„(๋ฒ”์œ„) = ์ข…๋ฃŒ์—ฐ๋„ ๊ทธ๋ฃน ๋ฒ”์œ„
26
  if "์ข…๋ฃŒ์—ฐ๋„" in df.columns:
27
- prod_df = df.groupby("๋ฐ•์Šค๋ฒˆํ˜ธ")["์ข…๋ฃŒ์—ฐ๋„"].apply(compute_year_range).reset_index()
28
- prod_df.columns = ["๋ฐ•์Šค๋ฒˆํ˜ธ", "์ƒ์‚ฐ์—ฐ๋„"]
29
  else:
30
- prod_df = pd.DataFrame({"๋ฐ•์Šค๋ฒˆํ˜ธ": df["๋ฐ•์Šค๋ฒˆํ˜ธ"].unique(), "์ƒ์‚ฐ์—ฐ๋„": "0000-0000"})
31
-
32
- # ๋ชฉ๋ก(๊ด€๋ฆฌ๋ฒˆํ˜ธ + ์ œ๋ชฉ)
33
  has_mgmt = "๊ด€๋ฆฌ๋ฒˆํ˜ธ" in df.columns
34
  list_rows = []
35
  for box, g in df.groupby("๋ฐ•์Šค๋ฒˆํ˜ธ"):
36
- lines = [f"- {r['๊ด€๋ฆฌ๋ฒˆํ˜ธ']} {r['์ œ๋ชฉ']}" if has_mgmt else f"- {r['์ œ๋ชฉ']}"
37
  for _, r in g.iterrows()]
38
- list_rows.append({"๋ฐ•์Šค๋ฒˆํ˜ธ": box, "๋ชฉ๋ก": "\r\n".join(lines)})
39
  list_df = pd.DataFrame(list_rows)
40
-
41
- meta_cols = ["๋ฐ•์Šค๋ฒˆํ˜ธ","์ข…๋ฃŒ์—ฐ๋„","๋ณด์กด๊ธฐ๊ฐ„","๋‹จ์œ„์—…๋ฌด","๊ธฐ๋ก๋ฌผ์ฒ ","์ œ๋ชฉ"]
42
- meta_exist = [c for c in meta_cols if c in df.columns]
43
- meta_df = df.groupby("๋ฐ•์Šค๋ฒˆํ˜ธ", as_index=False).first()[meta_exist] if meta_exist \
44
- else pd.DataFrame({"๋ฐ•์Šค๋ฒˆํ˜ธ": df["๋ฐ•์Šค๋ฒˆํ˜ธ"].unique()})
45
-
46
- return meta_df.merge(list_df, on="๋ฐ•์Šค๋ฒˆํ˜ธ", how="left").merge(prod_df, on="๋ฐ•์Šค๋ฒˆํ˜ธ", how="left")
47
-
48
- def _runs_plain(text: str) -> str:
49
- return f"<hp:run><hp:t>{html.escape('' if text is None else str(text))}</hp:t></hp:run>"
50
-
51
- def _runs_list(text: str) -> str:
52
- if text is None: return ""
53
- lines = str(text).replace("\r\n", "\n").split("\n")
54
- parts = []
55
- for i, ln in enumerate(lines):
56
- if i > 0:
57
- parts.append("<hp:lineBreak/>")
58
- parts.append(f"<hp:run><hp:t>{html.escape(ln)}</hp:t></hp:run>")
59
- return "".join(parts)
60
-
61
- # =============== HWPX ์“ฐ๊ธฐ (mimetype ๋งจ์•ž/๋ฌด์••์ถ•) ===============
62
- def write_hwpx_like_src(zin: zipfile.ZipFile, writer_fn) -> bytes:
63
- out = io.BytesIO()
64
- zout = zipfile.ZipFile(out, "w")
65
-
66
- if "mimetype" in zin.namelist():
67
- zi = zipfile.ZipInfo("mimetype")
68
- zi.compress_type = zipfile.ZIP_STORED
69
- zout.writestr(zi, zin.read("mimetype"))
70
-
71
- for e in zin.infolist():
72
- if e.filename == "mimetype":
73
- continue
74
- data = zin.read(e.filename)
75
- if e.filename.startswith("Contents/") and e.filename.endswith(".xml"):
76
- try:
77
- s = data.decode("utf-8", errors="ignore")
78
- s2 = writer_fn(e.filename, s)
79
- data = s2.encode("utf-8")
80
- except Exception:
81
- pass
82
- zi = zipfile.ZipInfo(e.filename)
83
- zi.compress_type = zipfile.ZIP_DEFLATED
84
- zout.writestr(zi, data)
85
-
86
- zout.close(); out.seek(0)
87
- return out.getvalue()
88
-
89
- # =============== ํ•„๋“œ ํ‰๋ฌธํ™”(์ œ๊ฑฐ) ์น˜ํ™˜ ===============
90
- # ํ•œ๊ธ€์€ ํ•„๋“œ๊ฐ€ ๋ณดํ†ต ์ด๋ ‡๊ฒŒ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค:
91
- # <hp:run> ... <hp:fieldBegin name="ํ‚ค" .../> ... </hp:run>
92
- # (์ค‘๊ฐ„์— ์—ฌ๋Ÿฌ run/ํ…์ŠคํŠธ)
93
- # <hp:run> ... <hp:fieldEnd/> ... </hp:run>
94
- # => ์•„๋ž˜ ์ •๊ทœ์‹์œผ๋กœ "fieldBegin run ~ fieldEnd run" ์ „์ฒด๋ฅผ ๊ฐ’ run๋“ค๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.
95
- FIELD_RANGE_RE_TMPL = (
96
- r'(<hp:run[^>]*>[^<]*'
97
- r'<hp:fieldBegin[^>]*name="{name}"[^>]*/>'
98
- r'.*?</hp:run>)'
99
- r'(.*?)'
100
- r'(<hp:run[^>]*>.*?<hp:fieldEnd[^>]*/>.*?</hp:run>)'
101
- )
102
-
103
- def apply_field_flatten(hwpx_bytes: bytes, mapping: dict, collect_debug=False):
104
- dbg = {"mode":"field-flatten","files_touched":[], "field_hits":{}} if collect_debug else None
105
- zin = zipfile.ZipFile(io.BytesIO(hwpx_bytes), "r")
106
-
107
- # ์‹ค์ œ ์กด์žฌํ•˜๋Š” name๋งŒ ์ถ”์ถœ
108
- present = set()
109
- for e in zin.infolist():
110
- if e.filename.startswith("Contents/") and e.filename.endswith(".xml"):
111
- try:
112
- s = zin.read(e.filename).decode("utf-8", errors="ignore")
113
- for k in mapping.keys():
114
- if f'name="{k}"' in s:
115
- present.add(k)
116
- except:
117
- pass
118
-
119
- def writer(fname: str, xml: str) -> str:
120
- changed = False
121
- for k in present:
122
- val = mapping.get(k, "")
123
- is_list = bool(re.match(r"^(๋ชฉ๋ก|list)\d+$", k, re.IGNORECASE))
124
- replacement_runs = _runs_list(val) if is_list else _runs_plain(val)
125
-
126
- pat = re.compile(FIELD_RANGE_RE_TMPL.format(name=re.escape(k)), re.DOTALL)
127
- xml2, n = pat.subn(replacement_runs, xml)
128
- if n:
129
- changed = True
130
- xml = xml2
131
- if dbg: dbg["field_hits"][k] = dbg["field_hits"].get(k, 0) + 1
132
-
133
- if changed and dbg and fname not in dbg["files_touched"]:
134
- dbg["files_touched"].append(fname)
135
- return xml
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
- merged = build_merged_df(df)
165
- box_list = merged["๋ฐ•์Šค๋ฒˆํ˜ธ"].astype(str).str.zfill(4).unique().tolist()
166
-
167
- st.subheader("๐Ÿ”Ž ์—…๋กœ๋“œ๋œ ๋ฐ•์Šค๋ฒˆํ˜ธ ๋ชฉ๋ก")
168
- st.write(f"์ด **{len(box_list)}**๊ฐœ")
169
- st.dataframe(pd.DataFrame({"๋ฐ•์Šค๋ฒˆํ˜ธ": box_list}), use_container_width=True, height=240)
170
-
171
- selected = st.multiselect("์ƒ์„ฑํ•  ๋ฐ•์Šค๋ฒˆํ˜ธ ์„ ํƒ (๋น„์šฐ๋ฉด ์ „์ฒด ์ƒ์„ฑ)", options=box_list)
172
- work = merged[merged["๋ฐ•์Šค๋ฒˆํ˜ธ"].isin(selected)] if selected else merged
173
- rows = work.sort_values("๋ฐ•์Šค๋ฒˆํ˜ธ").to_dict(orient="records")
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")