devorah-ai-2016's picture
Upload app.py
2477ef1 verified
Raw History Blame Contribute Delete
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("μž¬λ‚œν›ˆλ ¨μΌμ§€ 생성기")