Spaces:
Sleeping
Sleeping
Download app.py from devorah-ai-2016/DisasterTraining: direct link, hf CLI and curl.
- Browser
- Download file 27.2 kB
-
https://huggingface.co/spaces/devorah-ai-2016/DisasterTraining/resolve/main/app.py
- Command line
-
hf download hf://spaces/devorah-ai-2016/DisasterTraining/app.py
-
curl -L -o app.py https://huggingface.co/spaces/devorah-ai-2016/DisasterTraining/resolve/main/app.py
27.2 kB
| import os | |
| import sqlite3 | |
| import random | |
| import tempfile | |
| from io import BytesIO | |
| from datetime import date, datetime | |
| import pandas as pd | |
| import streamlit as st | |
| from openpyxl import Workbook | |
| from openpyxl.styles import Font, Alignment, Border, Side, PatternFill | |
| from openpyxl.utils import get_column_letter | |
| from openpyxl.drawing.image import Image as XLImage | |
| st.set_page_config(page_title="μ¬λνλ ¨μΌμ§", page_icon="π¨", layout="wide") | |
| CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| DB_CANDIDATES = [ | |
| os.path.join(CURRENT_DIR, "data", "clients.db"), | |
| os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"), | |
| os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"), | |
| os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"), | |
| ] | |
| DB_PATH = next((p for p in DB_CANDIDATES if os.path.exists(p)), None) | |
| if DB_PATH is None: | |
| os.makedirs(os.path.join(CURRENT_DIR, "data"), exist_ok=True) | |
| DB_PATH = os.path.join(CURRENT_DIR, "data", "clients.db") | |
| st.markdown(""" | |
| <style> | |
| .block-container { padding-top: 2rem; } | |
| .hero {background: linear-gradient(135deg, #991B1B, #F97316); padding:30px; border-radius:22px; text-align:center; margin-bottom:22px;} | |
| .hero h1 { color:white; margin:0; font-size:36px; } | |
| .hero p { color:white; margin-top:10px; font-size:18px; } | |
| .notice {background:#fff8e1; border-left:7px solid #f59e0b; padding:15px; border-radius:12px; margin-bottom:18px;} | |
| .okbox {background:#e8f5e9; border-left:7px solid #43a047; padding:14px; border-radius:12px; margin-bottom:14px;} | |
| .warnbox {background:#fff3e0; border-left:7px solid #fb8c00; padding:14px; border-radius:12px; margin-bottom:14px;} | |
| .dangerbox {background:#ffebee; border-left:7px solid #e53935; padding:14px; border-radius:12px; margin-bottom:14px;} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="hero"> | |
| <h1>π¨ μ¬λνλ ¨μΌμ§ μμ±κΈ°</h1> | |
| <p>νμ¬Β·μ§μ§Β·κ°μΌλ³Β·μκΈμν© νλ ¨μ μλ리μ€, μν λΆλ΄, κ²°κ³Όνκ°, κ°μ μ‘°μΉκΉμ§ κΈ°λ‘ν©λλ€.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="notice"><strong>νμ ꡬ쑰:</strong> νλ ¨μΌμ, μ₯μ, νλ ¨μ’ λ₯, μ°Έμμ, μλ리μ€, μν λΆλ΄, νλ ¨κ³Όμ , κ²°κ³Όνκ°, κ°μ μ‘°μΉ, μ¬μ§μ²¨λΆλ₯Ό ν¬ν¨ν©λλ€.</div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="notice"> | |
| β» μμ±λ λ¬Έμλ κ·Έλλ‘ μ¬μ©νμ λ λλ©°, κΈ°κ΄μ μ΄μλ°©μΉ¨, μκΈμ μν, 보νΈμ μ견 λ° νκ°κΈ°μ€μ λ°λΌ μΆκ° 보μμ¬νμ μΌλ§λ μ§ μμ κ°λ₯ν©λλ€. κΈ°κ΄μ μν©μ λ§κ² νμ©νμκΈ° λ°λλλ€. | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def get_conn(): | |
| return sqlite3.connect(DB_PATH, check_same_thread=False) | |
| def init_db(): | |
| conn = get_conn(); cur = conn.cursor() | |
| cur.execute(""" | |
| CREATE TABLE IF NOT EXISTS disaster_training_records ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| institution_name TEXT, | |
| institution_type TEXT, | |
| training_title TEXT, | |
| training_type TEXT, | |
| training_date TEXT, | |
| training_time TEXT, | |
| training_place TEXT, | |
| supervisor TEXT, | |
| participants TEXT, | |
| scenario TEXT, | |
| role_division TEXT, | |
| training_process TEXT, | |
| evaluation_result TEXT, | |
| improvement_plan TEXT, | |
| emergency_contacts TEXT, | |
| evaluation_score INTEGER, | |
| missing_items TEXT, | |
| compliance_result TEXT, | |
| photo_count INTEGER, | |
| created_at TEXT | |
| ) | |
| """) | |
| conn.commit(); conn.close() | |
| def save_record(data): | |
| conn = get_conn(); cur = conn.cursor() | |
| cur.execute(""" | |
| INSERT INTO disaster_training_records ( | |
| institution_name, institution_type, training_title, training_type, | |
| training_date, training_time, training_place, supervisor, | |
| participants, scenario, role_division, training_process, | |
| evaluation_result, improvement_plan, emergency_contacts, | |
| evaluation_score, missing_items, compliance_result, photo_count, created_at | |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| """, ( | |
| data["institution_name"], data["institution_type"], data["training_title"], data["training_type"], | |
| data["training_date"], data["training_time"], data["training_place"], data["supervisor"], | |
| data["participants"], data["scenario"], data["role_division"], data["training_process"], | |
| data["evaluation_result"], data["improvement_plan"], data["emergency_contacts"], | |
| data["evaluation_score"], data["missing_items"], data["compliance_result"], data.get("photo_count", 0), | |
| datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| )) | |
| conn.commit(); conn.close() | |
| def load_records(): | |
| conn = get_conn() | |
| try: | |
| df = pd.read_sql_query("SELECT * FROM disaster_training_records ORDER BY id DESC", conn) | |
| except Exception: | |
| df = pd.DataFrame() | |
| conn.close(); return df | |
| init_db() | |
| TRAINING_TYPES = ["νμ¬ λνΌνλ ¨", "μ§μ§ λνΌνλ ¨", "μκΈμν© λμνλ ¨", "κ°μΌλ³ λμνλ ¨", "μ μ /λ¨μ λμνλ ¨", "μ€μ’ Β·λ°°ν λμνλ ¨", "μΌκ° μ¬λμν© λμνλ ¨", "κΈ°ν"] | |
| INSTITUTION_POINTS = { | |
| "λ°©λ¬Έμμ": "μ¬κ° λ°©λ¬Έ μ€ μκΈμν© λ°κ²¬ μ 119 μ κ³ , 보νΈμ μ°λ½, κΈ°κ΄ λ³΄κ³ , μμ ν보 μ μ°¨λ₯Ό μ€μ¬μΌλ‘ νλ ¨ν©λλ€.", | |
| "λ°©λ¬Έλͺ©μ": "λͺ©μ μ€ λμ, νΈν‘κ³€λ, νΌλΆμμ λ± μν© λ°μ μ μ¦μ μ€λ¨, μμ ν보, 보νΈμ μ°λ½, κΈ°κ΄ λ³΄κ³ λ₯Ό μ€μ¬μΌλ‘ νλ ¨ν©λλ€.", | |
| "λ°©λ¬Έκ°νΈ": "건κ°μν κΈλ³, κ°μΌ μμ¬, μκΈμ²μΉ, μλ£κΈ°κ΄ μ°κ³ μ μ°¨λ₯Ό μ€μ¬μΌλ‘ νλ ¨ν©λλ€.", | |
| "μ£Όκ°λ³΄νΈ": "μΌν° λ΄ νμ¬Β·μ§μ§Β·μ‘μ μ€ μ¬κ³ Β·κ°μΌλ³ λ°μ μ λνΌμ λμ λνΌμ·¨μ½μ μ§μμ μ€μ¬μΌλ‘ νλ ¨ν©λλ€.", | |
| "μμμ": "μ μμ λνΌ, μΌκ° μμ°°, μΈ΅λ³ μν λΆλ΄, μ°μΒ·μΉ¨μΒ·ν μ²΄μ΄ μ΄μ©μμ μ΄λμ§μμ μ€μ¬μΌλ‘ νλ ¨ν©λλ€.", | |
| "곡λμνκ°μ ": "μκ·λͺ¨ μν곡κ°μμ νμ¬Β·μΌκ°μκΈΒ·κ³΅λμν κ°λ± μν© λ°μ μ μ μν λ³΄κ³ μ λνΌλ₯Ό μ€μ¬μΌλ‘ νλ ¨ν©λλ€.", | |
| } | |
| TRAINING_RULE_POINTS = { | |
| "νμ¬ λνΌνλ ¨": ["μ΄κΈ° λ°κ²¬ λ° μ ν", "119 μ κ³ ", "μνκΈ° μμΉ νμΈ", "λνΌμ·¨μ½μ μ°μ μ΄λ", "λνΌ μλ£ ν μΈμμ κ²"], | |
| "μ§μ§ λνΌνλ ¨": ["λνλ¬Ό ννΌ", "μ± μΒ·λ²½λ©΄ 보νΈ", "μΆμ λ¬Έ κ°λ°©", "μμ ν μΈλΆ μ₯μ μ΄λ", "λΆμμ νμΈ"], | |
| "μκΈμν© λμνλ ¨": ["μμΒ·νΈν‘ νμΈ", "119 μ κ³ ", "μ¬νμμμ λλ μκΈμ²μΉ", "보νΈμ μ°λ½", "μκΈκΈ°λ‘ μμ±"], | |
| "κ°μΌλ³ λμνλ ¨": ["μ¦μμ λΆλ¦¬", "λ§μ€ν¬ λ° λ³΄νΈκ΅¬ μ°©μ©", "μμμ", "κ³΅κ° μλ ", "보건μ λ° λ³΄νΈμ λ³΄κ³ "], | |
| "μ μ /λ¨μ λμνλ ¨": ["λΉμμ‘°λͺ νμΈ", "μλ£μ₯λΉ μ¬μ© μκΈμ νμΈ", "κΈμΒ·κΈμ λ체방μ", "μμ μ΄λ", "볡ꡬμν© κ³΅μ "], | |
| "μ€μ’ Β·λ°°ν λμνλ ¨": ["λ§μ§λ§ λͺ©κ²©μ₯μ νμΈ", "μ€λ΄μΈ μμ", "보νΈμ μ°λ½", "112 μ κ³ κ²ν ", "μ¬λ°λ°©μ§ λμ μ κ²"], | |
| "μΌκ° μ¬λμν© λμνλ ¨": ["μΌκ° μμ°°", "μ΅μμΈλ ₯ μν λΆλ΄", "λνΌμ·¨μ½μ μ°μ μ§μ", "λΉμμ°λ½λ§ κ°λ", "μΈμμΈκ³ κΈ°λ‘"], | |
| "κΈ°ν": ["μν© μ ν", "μμ ν보", "μν λΆλ΄", "κ΄κ³κΈ°κ΄ μ°λ½", "κ²°κ³Όνκ° λ° κ°μ μ‘°μΉ"], | |
| } | |
| def build_rule_application(training_type, institution_type): | |
| points = TRAINING_RULE_POINTS.get(training_type, TRAINING_RULE_POINTS["κΈ°ν"]) | |
| institution_point = INSTITUTION_POINTS.get(institution_type, "κΈ°κ΄ μν©μ λ§λ μ¬λμν© λμμ μ°¨λ₯Ό μ€μ¬μΌλ‘ νλ ¨ν©λλ€.") | |
| lines = [ | |
| "[μ¬λνλ ¨κ·μΉ μ μ©μ¬ν]", | |
| f"- νλ ¨μ νλ³ μ€μ : {', '.join(points)}", | |
| f"- κΈ°κ΄μ νλ³ μ€μ : {institution_point}", | |
| "- μν λΆλ΄: μ΄κ΄, μ κ³ Β·μ°λ½, λνΌμ λ, λνΌμ·¨μ½μ μ§μ, μκΈμ²μΉ, κΈ°λ‘ λ΄λΉμ ꡬλΆν¨", | |
| "- κ²°κ³Όνκ°: μλμ , λ―Έν‘μ , κ°μ μ‘°μΉ, 보좩κ΅μ‘ νμ μ¬λΆλ₯Ό κΈ°λ‘ν¨", | |
| "- νκ°λμ: μ¬μ§μ²¨λΆ, λΉμμ°λ½λ§, μ°Έμμ λͺ λ¨, νλ ¨ μ§νκ³Όμ μ΄ μλ‘ μΌμΉνλλ‘ μ κ²ν¨", | |
| ] | |
| return "\n".join(lines) | |
| def save_uploaded_photos(files): | |
| paths = [] | |
| for file in (files or [])[:3]: | |
| suffix = os.path.splitext(file.name)[1].lower() or ".png" | |
| temp_path = os.path.join(tempfile.gettempdir(), f"disaster_photo_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{suffix}") | |
| with open(temp_path, "wb") as f: | |
| f.write(file.getbuffer()) | |
| paths.append(temp_path) | |
| return paths | |
| def default_scenario(training_type, institution_type): | |
| base = INSTITUTION_POINTS.get(institution_type, "κΈ°κ΄ μν©μ λ§λ μ¬λμν© λμμ μ°¨λ₯Ό μ€μ¬μΌλ‘ νλ ¨ν©λλ€.") | |
| if "νμ¬" in training_type: | |
| situation = "κΈ°κ΄ λ΄ νμ¬ κ²½λ³΄κ° λ°μνμ¬ μ΄κΈ° μ κ³ , μκΈμ μμ νμΈ, λνΌμ λ, λΉμμ°λ½λ§ κ°λμ΄ νμν μν©μ κ°μ ν¨." | |
| elif "μ§μ§" in training_type: | |
| situation = "μ§μ§ λ°μμΌλ‘ λνλ¬Όκ³Ό μ΄λ μνμ΄ νμΈλμ΄ μ± μΒ·λ²½λ©΄ 보νΈ, λνΌλ‘ νμΈ, μμ ν μ₯μ μ΄λμ΄ νμν μν©μ κ°μ ν¨." | |
| elif "μκΈ" in training_type: | |
| situation = "μκΈμ νΈν‘κ³€λ λλ μμμ νκ° λ°μνμ¬ 119 μ κ³ , 보νΈμ μ°λ½, μκΈμ²μΉ, κΈ°λ‘μ΄ νμν μν©μ κ°μ ν¨." | |
| elif "κ°μΌ" in training_type: | |
| situation = "λ°μ΄Β·νΈν‘κΈ° μ¦μμκ° λ°μνμ¬ κ²©λ¦¬, λ§μ€ν¬ μ°©μ©, 보νΈμ μλ΄, μλ λ° λ³΄κ³ κ° νμν μν©μ κ°μ ν¨." | |
| elif "μΌκ°" in training_type: | |
| situation = "μΌκ° μκ°λ νμ¬ λλ μκΈμν©μ΄ λ°μνμ¬ μ΅μ μΈλ ₯μΌλ‘ μννμΈ, λνΌμ·¨μ½μ μ§μ, λΉμμ°λ½λ§ κ°λμ΄ νμν μν©μ κ°μ ν¨." | |
| else: | |
| situation = "κΈ°κ΄ μ΄μ μ€ μκΈ°μΉ λͺ»ν μ¬λΒ·μμ μ¬κ³ κ° λ°μν μν©μ κ°μ ν¨." | |
| return f"{situation}\n{base}" | |
| def build_role_division(institution_type, custom_roles): | |
| if custom_roles.strip(): | |
| return custom_roles.strip() | |
| return """ | |
| μμ€μ₯: νλ ¨ μ΄κ΄, λΉμμ°λ½λ§ κ°λ, κ΄κ³κΈ°κ΄ μ κ³ λ° νλ ¨ κ²°κ³Ό νμΈμ λ΄λΉνλ€. | |
| μ¬ν볡μ§μ¬: μκΈμ λͺ λ¨ νμΈ, 보νΈμ μ°λ½, λνΌμλ£ μ¬λΆ νμΈ, νλ ¨κΈ°λ‘ μμ±μ λ΄λΉνλ€. | |
| μμ보νΈμ¬: μκΈμ μ΄λ보쑰, λνΌμ·¨μ½μ μ§μ, λμμλ°©κ³Ό μμ νμΈμ λ΄λΉνλ€. | |
| κ°νΈ(쑰무)μ¬: μκΈμν νμΈ, νλ ₯μ§ν νμΈ, μκΈμ²μΉ λ° μλ£κΈ°κ΄ μ°κ³ νμμ±μ νμΈνλ€. | |
| μ΄μ μ/κΈ°νμ§μ: μ°¨λΒ·μΆμ ꡬ·λνΌλ‘ ν보, μνκΈ° μμΉ νμΈ, μΈλΆ μ΄λ μμ μ μ§μνλ€. | |
| """.strip() | |
| def build_training_text(data): | |
| return f""" | |
| [νλ ¨ κ°μ] | |
| - νλ ¨λͺ : {data['training_title']} | |
| - νλ ¨μ’ λ₯: {data['training_type']} | |
| - νλ ¨μΌμ: {data['training_date']} {data['training_time']} | |
| - νλ ¨μ₯μ: {data['training_place']} | |
| - μ΄κ΄μ± μμ: {data['supervisor']} | |
| [νλ ¨ μλ리μ€] | |
| {data['scenario']} | |
| [μν λΆλ΄] | |
| {data['role_division']} | |
| [νλ ¨ μ§νκ³Όμ ] | |
| {data['training_process']} | |
| [λΉμμ°λ½λ§] | |
| {data['emergency_contacts']} | |
| {data.get('rule_application', build_rule_application(data['training_type'], data['institution_type']))} | |
| """.strip() | |
| def evaluate_record(data): | |
| required = { | |
| "νλ ¨λͺ ": bool(data["training_title"].strip()), | |
| "νλ ¨μΌμ": bool(data["training_date"]) and bool(data["training_time"].strip()), | |
| "νλ ¨μ₯μ": bool(data["training_place"].strip()), | |
| "μ°Έμμ": bool(data["participants"].strip()), | |
| "μλ리μ€": bool(data["scenario"].strip()), | |
| "μν λΆλ΄": bool(data["role_division"].strip()), | |
| "νλ ¨κ³Όμ ": bool(data["training_process"].strip()), | |
| "κ²°κ³Όνκ°": bool(data["evaluation_result"].strip()), | |
| "κ°μ μ‘°μΉ": bool(data["improvement_plan"].strip()), | |
| "λΉμμ°λ½λ§": bool(data["emergency_contacts"].strip()), | |
| "μ¬λνλ ¨κ·μΉ": bool(data.get("rule_application", "").strip()), | |
| } | |
| score = int(sum(required.values()) / len(required) * 100) | |
| missing = [k for k, v in required.items() if not v] | |
| if score >= 90: | |
| msg = "νκ° μ ν©λκ° λμ΅λλ€. νλ ¨ μλ리μ€, μν λΆλ΄, μ§νκ³Όμ , κ²°κ³Όνκ°, κ°μ μ‘°μΉκ° μ λ°μλμμ΅λλ€." | |
| elif score >= 75: | |
| msg = "λμ²΄λ‘ μ ν©νλ μΌλΆ 보μμ΄ νμν©λλ€. λΆμ‘± νλͺ©μ 보μνλ©΄ νκ° λμλ ₯μ΄ λμμ§λλ€." | |
| else: | |
| msg = "보μμ΄ νμν©λλ€. νλ ¨μΌμ§μλ μλ리μ€, μ°Έμμ, μν λΆλ΄, κ²°κ³Όνκ°, κ°μ μ‘°μΉκ° ꡬ체μ μΌλ‘ λ€μ΄κ°μΌ ν©λλ€." | |
| return score, missing, msg | |
| def _border(): | |
| s = Side(style="thin", color="000000") | |
| return Border(left=s, right=s, top=s, bottom=s) | |
| def style_range(ws, min_row, max_row, min_col=1, max_col=9, fill=None, font=None, alignment=None): | |
| for row in ws.iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col): | |
| for cell in row: | |
| cell.border = _border() | |
| if fill is not None: | |
| cell.fill = fill | |
| if font is not None: | |
| cell.font = font | |
| if alignment is not None: | |
| cell.alignment = alignment | |
| def setup_print(ws, max_row): | |
| ws.page_setup.paperSize = ws.PAPERSIZE_A4 | |
| ws.page_setup.orientation = "portrait" | |
| ws.page_setup.fitToWidth = 1 | |
| ws.page_setup.fitToHeight = 0 | |
| ws.sheet_properties.pageSetUpPr.fitToPage = True | |
| ws.page_margins.left = 0.25 | |
| ws.page_margins.right = 0.25 | |
| ws.page_margins.top = 0.35 | |
| ws.page_margins.bottom = 0.35 | |
| ws.sheet_view.showGridLines = False | |
| ws.print_area = f"A1:I{max_row}" | |
| for col in range(1, 10): | |
| ws.column_dimensions[get_column_letter(col)].width = 12 | |
| def write_title(ws, title, title_fill, header_fill): | |
| ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=7) | |
| ws.cell(1, 1).value = title | |
| ws.cell(1, 1).font = Font(name="Malgun Gothic", size=24, bold=True) | |
| ws.cell(1, 1).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) | |
| style_range(ws, 1, 2, 1, 7, fill=title_fill, font=Font(name="Malgun Gothic", size=24, bold=True), alignment=Alignment(horizontal="center", vertical="center", wrap_text=True)) | |
| ws.cell(1, 8).value = "λ΄λΉμ"; ws.cell(1, 9).value = "μμ€μ₯" | |
| ws.cell(2, 8).value = ""; ws.cell(2, 9).value = "" | |
| style_range(ws, 1, 1, 8, 9, fill=header_fill, font=Font(name="Malgun Gothic", size=11, bold=True), alignment=Alignment(horizontal="center", vertical="center")) | |
| style_range(ws, 2, 2, 8, 9, font=Font(name="Malgun Gothic", size=11), alignment=Alignment(horizontal="center", vertical="center")) | |
| ws.row_dimensions[1].height = 28; ws.row_dimensions[2].height = 42 | |
| def write_info(ws, r, a, b, c, d, e, f, header_fill): | |
| ws.cell(r, 1).value = a; ws.merge_cells(start_row=r, start_column=2, end_row=r, end_column=3); ws.cell(r, 2).value = b | |
| ws.cell(r, 4).value = c; ws.merge_cells(start_row=r, start_column=5, end_row=r, end_column=6); ws.cell(r, 5).value = d | |
| ws.cell(r, 7).value = e; ws.merge_cells(start_row=r, start_column=8, end_row=r, end_column=9); ws.cell(r, 8).value = f | |
| for col in [1, 4, 7]: | |
| ws.cell(r, col).fill = header_fill; ws.cell(r, col).font = Font(name="Malgun Gothic", size=10, bold=True); ws.cell(r, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) | |
| for col in [2, 5, 8]: | |
| ws.cell(r, col).font = Font(name="Malgun Gothic", size=10); ws.cell(r, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) | |
| style_range(ws, r, r, 1, 9); ws.row_dimensions[r].height = 28 | |
| def height_for_text(text, chars=58, max_height=420): | |
| lines = 0 | |
| for line in str(text or "").split("\n"): | |
| lines += max(1, len(line) // chars + 1) | |
| return min(max(70, lines * 18 + 18), max_height) | |
| def write_section(ws, r, title, content, header_fill, min_height=90): | |
| ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9) | |
| ws.cell(r, 1).value = title | |
| style_range(ws, r, r, 1, 9, fill=header_fill, font=Font(name="Malgun Gothic", size=13, bold=True), alignment=Alignment(horizontal="center", vertical="center", wrap_text=True)) | |
| ws.row_dimensions[r].height = 28 | |
| r += 1 | |
| ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9) | |
| ws.cell(r, 1).value = str(content or "") | |
| style_range(ws, r, r, 1, 9, font=Font(name="Malgun Gothic", size=10), alignment=Alignment(horizontal="left", vertical="top", wrap_text=True)) | |
| ws.row_dimensions[r].height = max(min_height, height_for_text(content)) | |
| return r + 1 | |
| def write_internal_sheet(wb, data, header_fill): | |
| ws = wb.create_sheet("λ΄λΆμ κ²_μΆλ ₯μ μΈ") | |
| ws.sheet_state = "hidden" | |
| rows = [("νκ° μ ν©λ", f"{data['evaluation_score']}μ "), ("λΆμ‘± νλͺ©", data["missing_items"]), ("μ κ² μ견", data["compliance_result"])] | |
| for i, (a, b) in enumerate(rows, 1): | |
| ws.cell(i, 1).value = a; ws.cell(i, 2).value = b | |
| ws.cell(i, 1).font = Font(name="Malgun Gothic", size=10, bold=True); ws.cell(i, 1).fill = header_fill | |
| ws.cell(i, 2).font = Font(name="Malgun Gothic", size=10); ws.cell(i, 2).alignment = Alignment(wrap_text=True, vertical="top") | |
| ws.column_dimensions["A"].width = 18; ws.column_dimensions["B"].width = 90 | |
| def generate_excel(data): | |
| wb = Workbook() | |
| title_fill = PatternFill("solid", fgColor="FEE2E2") | |
| header_fill = PatternFill("solid", fgColor="F3F4F6") | |
| ws = wb.active | |
| ws.title = "μ¬λνλ ¨μΌμ§" | |
| write_title(ws, "μ¬λνλ ¨μΌμ§", title_fill, header_fill) | |
| r = 3 | |
| write_info(ws, r, "κΈ°κ΄λͺ ", data["institution_name"], "κΈ°κ΄μ ν", data["institution_type"], "νλ ¨μΌμ", data["training_date"], header_fill); r += 1 | |
| write_info(ws, r, "νλ ¨λͺ ", data["training_title"], "νλ ¨μ’ λ₯", data["training_type"], "νλ ¨μκ°", data["training_time"], header_fill); r += 1 | |
| write_info(ws, r, "μ₯μ", data["training_place"], "μ΄κ΄", data["supervisor"], "μ¬μ§", f"{data.get('photo_count', 0)}μ₯", header_fill); r += 1 | |
| r = write_section(ws, r, "μ°Έμμ λͺ λ¨", data["participants"], header_fill, 70) | |
| r = write_section(ws, r, "νλ ¨ κΈ°λ‘λ΄μ©", build_training_text(data), header_fill, 260) | |
| r = write_section(ws, r, "νλ ¨ κ²°κ³Όνκ°", data["evaluation_result"], header_fill, 100) | |
| r = write_section(ws, r, "κ°μ μ‘°μΉ λ° νμκ³ν", data["improvement_plan"], header_fill, 100) | |
| r = write_section(ws, r, "μ¬λνλ ¨κ·μΉ μ μ©μ¬ν", data.get("rule_application", ""), header_fill, 90) | |
| if data.get("photo_paths"): | |
| ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9) | |
| ws.cell(r, 1).value = "νλ ¨μ¬μ§" | |
| style_range(ws, r, r, 1, 9, fill=header_fill, font=Font(name="Malgun Gothic", size=13, bold=True), alignment=Alignment(horizontal="center", vertical="center")) | |
| r += 1 | |
| anchors = ["A", "D", "G"] | |
| img_row = r | |
| for idx, photo_path in enumerate(data.get("photo_paths", [])[:3]): | |
| try: | |
| img = XLImage(photo_path); img.width = 210; img.height = 150; ws.add_image(img, f"{anchors[idx]}{img_row}") | |
| except Exception: | |
| pass | |
| style_range(ws, img_row, img_row + 7, 1, 9) | |
| for rr in range(img_row, img_row + 8): ws.row_dimensions[rr].height = 22 | |
| r = img_row + 8 | |
| ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9) | |
| ws.cell(r, 1).value = "β» μμ±λ λ¬Έμλ κ·Έλλ‘ μ¬μ©νμ λ λλ©°, κΈ°κ΄μ μ΄μλ°©μΉ¨, μκΈμ μν, 보νΈμ μ견 λ° νκ°κΈ°μ€μ λ°λΌ μΆκ° 보μμ¬νμ μΌλ§λ μ§ μμ κ°λ₯ν©λλ€. κΈ°κ΄μ μν©μ λ§κ² νμ©νμκΈ° λ°λλλ€." | |
| style_range(ws, r, r, 1, 9, | |
| font=Font(name="Malgun Gothic", size=8.5, color="595959", italic=True), | |
| alignment=Alignment(horizontal="left", vertical="center", wrap_text=True)) | |
| ws.row_dimensions[r].height = 36 | |
| r += 1 | |
| setup_print(ws, r - 1) | |
| write_internal_sheet(wb, data, header_fill) | |
| output = BytesIO(); wb.save(output); output.seek(0); return output | |
| menu = st.sidebar.radio("λ©λ΄", ["μ¬λνλ ¨μΌμ§ μμ±", "μ μ₯κΈ°λ‘ λ³΄κΈ°"]) | |
| st.sidebar.success("DB μ°κ²°"); st.sidebar.caption(DB_PATH) | |
| if menu == "μ μ₯κΈ°λ‘ λ³΄κΈ°": | |
| st.subheader("π μ μ₯λ μ¬λνλ ¨ κΈ°λ‘") | |
| records = load_records() | |
| if records.empty: st.info("μ μ₯λ μ¬λνλ ¨ κΈ°λ‘μ΄ μμ΅λλ€.") | |
| else: st.dataframe(records, use_container_width=True) | |
| else: | |
| st.subheader("π¨ μ¬λνλ ¨μΌμ§ μμ±") | |
| c1, c2, c3 = st.columns(3) | |
| institution_name = c1.text_input("κΈ°κ΄λͺ ", "μ₯κΈ°μμκΈ°κ΄") | |
| institution_type = c2.selectbox("κΈ°κ΄μ ν", ["λ°©λ¬Έμμ", "λ°©λ¬Έλͺ©μ", "λ°©λ¬Έκ°νΈ", "μ£Όκ°λ³΄νΈ", "μμμ", "곡λμνκ°μ "]) | |
| training_title = c3.text_input("νλ ¨λͺ ", "μ¬λμν© λμνλ ¨") | |
| c4, c5, c6 = st.columns(3) | |
| training_type = c4.selectbox("νλ ¨μ’ λ₯", TRAINING_TYPES) | |
| training_date = c5.date_input("νλ ¨μΌμ", value=date.today()) | |
| training_time = c6.text_input("νλ ¨μκ°", "14:00~14:40") | |
| c7, c8 = st.columns(2) | |
| training_place = c7.text_input("νλ ¨μ₯μ", "κΈ°κ΄ λ΄μΈλΆ λ° λνΌμ₯μ") | |
| supervisor = c8.text_input("μ΄κ΄μ± μμ", "μμ€μ₯") | |
| participants = st.text_area("μ°Έμμ λͺ λ¨", placeholder="μ: μμ€μ₯ νκΈΈλ, μ¬ν볡μ§μ¬ κΉλ³΅μ§, μμ보νΈμ¬ μ΄μμ μΈ 8λͺ ", height=80) | |
| scenario = st.text_area("νλ ¨ μλ리μ€", value=default_scenario(training_type, institution_type), height=120) | |
| role_division = st.text_area("μν λΆλ΄", value=build_role_division(institution_type, ""), height=130) | |
| default_process = """1. νλ ¨ μμ μ νλ ¨ λͺ©μ κ³Ό μμ μμΉμ μλ΄νμλ€. | |
| 2. μ¬λμν© λ°μμ κ°μ νκ³ μ΅μ΄ λ°κ²¬μ μ κ³ λ° κΈ°κ΄ λ΄ λ³΄κ³ λ₯Ό μ€μνμλ€. | |
| 3. μκΈμ μνμ λνΌμ·¨μ½μλ₯Ό νμΈνκ³ μν λΆλ΄μ λ°λΌ μμ ν μ₯μλ‘ μ΄λμ μ§μνμλ€. | |
| 4. 119 μ κ³ , 보νΈμ μ°λ½, λΉμμ°λ½λ§ κ°λ μ μ°¨λ₯Ό νμΈνμλ€. | |
| 5. λνΌ μλ£ ν μΈμ νμΈ, νΉμ΄μ¬ν μ κ², νλ ¨ κ²°κ³Όνκ°λ₯Ό μ€μνμλ€.""" | |
| training_process = st.text_area("νλ ¨ μ§νκ³Όμ ", value=default_process, height=150) | |
| emergency_contacts = st.text_area("λΉμμ°λ½λ§", value="119, 보νΈμ μ°λ½μ², μμ€μ₯, μ¬ν볡μ§μ¬, κ΄ν μ§μ체, νλ ₯μλ£κΈ°κ΄ μ°λ½λ§μ νμΈν¨.", height=80) | |
| rule_application = st.text_area("μ¬λνλ ¨κ·μΉ μ μ©μ¬ν", value=build_rule_application(training_type, institution_type), height=120) | |
| evaluation_result = st.text_area("νλ ¨ κ²°κ³Όνκ°", value="μ°Έμμλ€μ΄ μ¬λμν© λ°μ μ μ κ³ , λνΌμ λ, μΈμνμΈ, 보νΈμ μ°λ½ μ μ°¨λ₯Ό μμ§νμλ€. λνΌμ·¨μ½μ μ§μκ³Ό λΉμμ°λ½λ§ κ°λ μ μ°¨λ₯Ό μ¬νμΈνμλ€.", height=110) | |
| improvement_plan = st.text_area("κ°μ μ‘°μΉ λ° νμκ³ν", value="λνΌλ‘ μ₯μ λ¬Όμ μ κΈ° μ κ²νκ³ , μ κ·μ§μ λ° λ―Έμ°Έμμμκ² λ³΄μΆ©κ΅μ‘μ μ€μνλ€. λ€μ νλ ¨ μ μν λ³ μνμκ°κ³Ό λνΌμ·¨μ½μ μ§μ μ μ°¨λ₯Ό μΆκ° μ κ²νλ€.", height=110) | |
| photos = st.file_uploader("νλ ¨μ¬μ§ μ²¨λΆ (μ΅λ 3μ₯)", type=["png", "jpg", "jpeg"], accept_multiple_files=True) | |
| if st.button("π¨ μ¬λνλ ¨μΌμ§ μμ±", use_container_width=True): | |
| photo_paths = save_uploaded_photos(photos) | |
| data = { | |
| "institution_name": institution_name, | |
| "institution_type": institution_type, | |
| "training_title": training_title, | |
| "training_type": training_type, | |
| "training_date": str(training_date), | |
| "training_time": training_time, | |
| "training_place": training_place, | |
| "supervisor": supervisor, | |
| "participants": participants, | |
| "scenario": scenario, | |
| "role_division": role_division, | |
| "training_process": training_process, | |
| "evaluation_result": evaluation_result, | |
| "improvement_plan": improvement_plan, | |
| "emergency_contacts": emergency_contacts, | |
| "rule_application": rule_application, | |
| "photo_paths": photo_paths, | |
| "photo_count": len(photo_paths), | |
| } | |
| score, missing, compliance = evaluate_record(data) | |
| data["evaluation_score"] = score | |
| data["missing_items"] = ", ".join(missing) if missing else "λΆμ‘± νλͺ© μμ" | |
| data["compliance_result"] = compliance | |
| st.session_state["disaster_data"] = data | |
| if "disaster_data" in st.session_state: | |
| data = st.session_state["disaster_data"] | |
| st.markdown("---"); st.subheader("μμ± κ²°κ³Ό μμ ") | |
| data["scenario"] = st.text_area("μλλ¦¬μ€ μμ ", value=data["scenario"], height=120) | |
| data["role_division"] = st.text_area("μν λΆλ΄ μμ ", value=data["role_division"], height=130) | |
| data["training_process"] = st.text_area("νλ ¨ μ§νκ³Όμ μμ ", value=data["training_process"], height=150) | |
| data["evaluation_result"] = st.text_area("κ²°κ³Όνκ° μμ ", value=data["evaluation_result"], height=110) | |
| data["improvement_plan"] = st.text_area("κ°μ μ‘°μΉ μμ ", value=data["improvement_plan"], height=110) | |
| data["rule_application"] = st.text_area("μ¬λνλ ¨κ·μΉ μ μ©μ¬ν μμ ", value=data.get("rule_application", ""), height=120) | |
| score, missing, compliance = evaluate_record(data) | |
| data["evaluation_score"] = score | |
| data["missing_items"] = ", ".join(missing) if missing else "λΆμ‘± νλͺ© μμ" | |
| data["compliance_result"] = compliance | |
| e1, e2, e3 = st.columns(3) | |
| e1.metric("νκ° μ ν©λ", f"{score}μ "); e2.metric("λΆμ‘± νλͺ©", data["missing_items"]); e3.metric("μ¬μ§", f"{data.get('photo_count', 0)}μ₯") | |
| if score >= 90: st.markdown(f'<div class="okbox">{compliance}</div>', unsafe_allow_html=True) | |
| else: st.markdown(f'<div class="warnbox">{compliance}</div>', unsafe_allow_html=True) | |
| if st.button("πΎ μ¬λνλ ¨ κΈ°λ‘ DB μ μ₯", use_container_width=True): | |
| save_record(data); st.success("μ¬λνλ ¨ κΈ°λ‘μ΄ μ μ₯λμμ΅λλ€.") | |
| filename = f"μ¬λνλ ¨μΌμ§_{data['institution_type']}_{data['training_date']}.xlsx" | |
| st.download_button("π₯ μ¬λνλ ¨μΌμ§ μμ λ€μ΄λ‘λ", data=generate_excel(data), file_name=filename, mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", use_container_width=True) | |
| st.caption("μ¬λνλ ¨μΌμ§ μμ±κΈ°") | |