Spaces:
Sleeping
Sleeping
Delete app.py
Browse files
app.py
DELETED
|
@@ -1,1205 +0,0 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
import openpyxl
|
| 3 |
-
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
|
| 4 |
-
from openpyxl.utils import get_column_letter
|
| 5 |
-
from datetime import datetime, date
|
| 6 |
-
import io
|
| 7 |
-
|
| 8 |
-
# ---------------------------------------------------------
|
| 9 |
-
# Page Configurations and Premium Styling (CSS)
|
| 10 |
-
# ---------------------------------------------------------
|
| 11 |
-
st.set_page_config(
|
| 12 |
-
page_title="재난훈련기록지 자동생성기",
|
| 13 |
-
page_icon="🚒",
|
| 14 |
-
layout="wide",
|
| 15 |
-
initial_sidebar_state="expanded"
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
# Custom Glassmorphism & Modern Soft Palettes UI styling
|
| 19 |
-
st.markdown("""
|
| 20 |
-
<style>
|
| 21 |
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
|
| 22 |
-
|
| 23 |
-
html, body, [class*="css"] {
|
| 24 |
-
font-family: 'Noto Sans KR', 'Inter', sans-serif;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
/* Premium Title Card styling */
|
| 28 |
-
.title-card {
|
| 29 |
-
background: linear-gradient(135deg, #1A365D 0%, #2A4365 100%);
|
| 30 |
-
padding: 2.5rem;
|
| 31 |
-
border-radius: 16px;
|
| 32 |
-
color: white;
|
| 33 |
-
margin-bottom: 2rem;
|
| 34 |
-
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
| 35 |
-
position: relative;
|
| 36 |
-
overflow: hidden;
|
| 37 |
-
}
|
| 38 |
-
.title-card::after {
|
| 39 |
-
content: "";
|
| 40 |
-
position: absolute;
|
| 41 |
-
top: -50%;
|
| 42 |
-
right: -30%;
|
| 43 |
-
width: 300px;
|
| 44 |
-
height: 300px;
|
| 45 |
-
background: rgba(255, 255, 255, 0.05);
|
| 46 |
-
border-radius: 50%;
|
| 47 |
-
}
|
| 48 |
-
.title-card h1 {
|
| 49 |
-
font-size: 2.2rem;
|
| 50 |
-
font-weight: 900;
|
| 51 |
-
margin: 0 0 0.5rem 0;
|
| 52 |
-
color: #FFFFFF !important;
|
| 53 |
-
letter-spacing: -0.025em;
|
| 54 |
-
}
|
| 55 |
-
.title-card p {
|
| 56 |
-
font-size: 1.1rem;
|
| 57 |
-
margin: 0;
|
| 58 |
-
opacity: 0.9;
|
| 59 |
-
font-weight: 300;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
/* Section Headers */
|
| 63 |
-
.section-header {
|
| 64 |
-
font-size: 1.3rem;
|
| 65 |
-
font-weight: 700;
|
| 66 |
-
color: #1E3A8A;
|
| 67 |
-
border-left: 5px solid #3B82F6;
|
| 68 |
-
padding-left: 10px;
|
| 69 |
-
margin-top: 1.5rem;
|
| 70 |
-
margin-bottom: 1rem;
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
/* Custom button styling */
|
| 74 |
-
div.stButton > button {
|
| 75 |
-
background: linear-gradient(90deg, #2563EB 0%, #1D4ED8 100%) !important;
|
| 76 |
-
color: white !important;
|
| 77 |
-
font-weight: 600 !important;
|
| 78 |
-
padding: 0.75rem 2rem !important;
|
| 79 |
-
border-radius: 10px !important;
|
| 80 |
-
border: none !important;
|
| 81 |
-
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2) !important;
|
| 82 |
-
transition: all 0.2s ease-in-out !important;
|
| 83 |
-
width: 100% !important;
|
| 84 |
-
font-size: 1.1rem !important;
|
| 85 |
-
}
|
| 86 |
-
div.stButton > button:hover {
|
| 87 |
-
transform: translateY(-2px) !important;
|
| 88 |
-
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.35) !important;
|
| 89 |
-
}
|
| 90 |
-
</style>
|
| 91 |
-
""", unsafe_allow_html=True)
|
| 92 |
-
|
| 93 |
-
# ---------------------------------------------------------
|
| 94 |
-
# Dynamic Narrative Sentence Generation Engine
|
| 95 |
-
# ---------------------------------------------------------
|
| 96 |
-
def generate_narrative_text(info, section, training_type):
|
| 97 |
-
facility_name = info.get('facility_name', '행복요양원')
|
| 98 |
-
total_participants = info.get('total_participants', 18)
|
| 99 |
-
staff_count = info.get('staff_participants', 8)
|
| 100 |
-
recipient_count = info.get('recipient_participants', 12)
|
| 101 |
-
evac_time = info.get('evac_time', '4분 30초')
|
| 102 |
-
vulnerable_count = info.get('vulnerable_count', 5)
|
| 103 |
-
wheelchair_count = info.get('wheelchair_count', 3)
|
| 104 |
-
walk_aid_count = info.get('walk_aid_count', 4)
|
| 105 |
-
|
| 106 |
-
emergency_mock = info.get('emergency_mock', '실시 및 양호')
|
| 107 |
-
fire_ext_demo = info.get('fire_ext_demo', '실시 및 양호')
|
| 108 |
-
route_checked = info.get('route_checked', '확인완료')
|
| 109 |
-
contact_checked = info.get('contact_checked', '작동확인')
|
| 110 |
-
guardian_call_date = info.get('guardian_call_date', str(datetime.today().strftime('%Y-%m-%d')))
|
| 111 |
-
guardian_reflected = info.get('guardian_reflected', '반영완료')
|
| 112 |
-
|
| 113 |
-
if training_type == "소방훈련":
|
| 114 |
-
# Multi-occupational Opinions
|
| 115 |
-
if section == "opinion_시설장_1":
|
| 116 |
-
return "소방 훈련 시 전 층의 소화 및 경보설비 가동을 일일 점검하고, 인력 교대 근무 시 인수인계서에 '오늘의 비상 대책 역할' 표기를 의무화하는 행정 지침을 시달함."
|
| 117 |
-
elif section == "opinion_시설장_2":
|
| 118 |
-
return "원활한 피난을 보장하기 위해 노후 휠체어 정비 예산을 편성하고, 시끄러운 경보 속 통제를 위한 휴대식 메가폰 2대 및 반사 유도조끼 추가 공급을 지원함."
|
| 119 |
-
elif section == "opinion_시설장_role":
|
| 120 |
-
return "훈련 총괄 지휘, 소방 안전 대책 위원회 주관 및 예산 결재"
|
| 121 |
-
|
| 122 |
-
elif section == "opinion_사회복지사_1":
|
| 123 |
-
return f"수급자 {recipient_count}명 중 {vulnerable_count}명의 대피취약자에 대하여 요양보호사 및 물리치료사와 1:1 매칭 조를 편성하고, 대피 경로 동선에서 장애 요소를 사전에 스크리닝함."
|
| 124 |
-
elif section == "opinion_사회복지사_2":
|
| 125 |
-
return "보호자 비상연락망 최신화 데이터를 바탕으로 안전 계획을 갱신하였으며, 수급자별 욕구사정 기록지에 개인별 대피 시 특이사항을 명시하여 반영함."
|
| 126 |
-
elif section == "opinion_사회복지사_role":
|
| 127 |
-
return "대피취약자 분류, 비상연락 동선 점검, 훈련일지 기록 및 대외 보호자 안내"
|
| 128 |
-
|
| 129 |
-
elif section == "opinion_요양보호사_1":
|
| 130 |
-
return f"휠체어 대상자 {wheelchair_count}명과 보행보조 필요자 {walk_aid_count}명은 스스로 경보음을 인지하더라도 관절 경직 및 인지 저하로 자력 대피가 불가능하여 대피 시 낙상 위험이 매우 높은 상태로 관찰됨."
|
| 131 |
-
elif section == "opinion_요양보호사_2":
|
| 132 |
-
return "화재 시 연기 흡입을 막기 위해 젖은 수건으로 코와 입을 보호하며, 휠체어 이동 시에는 경사로 브레이크가 밀리지 않도록 고정하는 절차를 출발 전 필히 숙지하고 이행해야 함."
|
| 133 |
-
elif section == "opinion_요양보호사_role":
|
| 134 |
-
return "수급자 1:1 대피 동반 이동, 대피 중 낙상 위험 통제, 피난 물품 적시 배부"
|
| 135 |
-
|
| 136 |
-
elif section == "opinion_간호조무사_1":
|
| 137 |
-
return f"대피 완료 후 신속하게 임시 집결지에서 수급자 {recipient_count}명의 혈압, 맥박 등 활력징후를 기록하여 비상 진료 카드를 작성하고 쇼크 증상 및 호흡기 질환 여부를 확인하였음."
|
| 138 |
-
elif section == "opinion_간호조무사_2":
|
| 139 |
-
return "휴대용 구급 약품 가방 및 산소캔 준비 상태를 사전에 확인하였고, 대피 과정에서 발생 가능한 수급자의 가벼운 찰과상에 대한 소독 및 응급 처치 요령을 훈련하였음."
|
| 140 |
-
elif section == "opinion_간호조무사_role":
|
| 141 |
-
return "비상 약품 휴대, 활력징후 측정 및 응급구호 조치 가동"
|
| 142 |
-
|
| 143 |
-
elif section == "opinion_물리치료사_1":
|
| 144 |
-
return "보행이 불안정한 수급자들은 비상상황 시 평소보다 보행 보조기 조작 서두름으로 인해 무게중심이 무너져 넘어지기 쉬운 신체 특성을 지님."
|
| 145 |
-
elif section == "opinion_물리치료사_2":
|
| 146 |
-
return "대피 시 경사로 부근에서 낙상 방지 파지법을 실시하고, 휠체어 승강기 진입 시 방향 전환에 따른 충돌 방지 요령을 물리치료 관점 및 재활 보조 기구를 적용해 집중 지원함."
|
| 147 |
-
elif section == "opinion_물리치료사_role":
|
| 148 |
-
return "보행 및 휠체어 안전 이동 기법 교육, 미끄럼 방지 제동 보조"
|
| 149 |
-
|
| 150 |
-
# 2차 결과 반영
|
| 151 |
-
elif section == "result_problem":
|
| 152 |
-
return f"자력 대피가 불가능한 대피취약자 {vulnerable_count}명의 인솔 인력 분담이 불명확하였고, 일부 휠체어({wheelchair_count}대)의 브레이크 작동 결함 및 경보 작동 시 소음으로 인한 무선 지시 전달 불량 현상이 존재했음."
|
| 153 |
-
elif section == "result_action":
|
| 154 |
-
return f"화재 비상경보 전파 후 119 신고를 진행하고 지정 대피경로({route_checked})에 따라 전원 대피를 마쳤으며, 다직종이 밀착 협업하여 1:1 대피 및 소화기 작동 실무 시연({fire_ext_demo})을 완수함."
|
| 155 |
-
elif section == "result_role_actions":
|
| 156 |
-
return "시설장은 전체 훈련 지휘 및 연락망 작동을 점검했고, 복지사는 취약자 조 분담과 기록을, 요양보호사는 젖은 수건을 활용해 1:1 대피를, 간호조무사는 활력징후 점검을, 물리치료사는 이동 자세 보조를 전담함."
|
| 157 |
-
elif section == "result_outcome":
|
| 158 |
-
return f"전 직원이 비상 시 행동 지침을 숙지하였고, 요양보호사의 1:1 매칭 지원을 통해 최종 대피완료 시간이 {evac_time}로 크게 단축되어 실전 대응 능력이 입증됨."
|
| 159 |
-
elif section == "result_data":
|
| 160 |
-
return f"총 참여자 {total_participants}명 (종사자 {staff_count}명, 수급자 {recipient_count}명), 대피취약자 {vulnerable_count}명, 휠체어 {wheelchair_count}대, 보행보조 {walk_aid_count}명, 대피완료 {evac_time}, 소화기 실습 {fire_ext_demo}, 응급대응 {emergency_mock}."
|
| 161 |
-
elif section == "result_risks":
|
| 162 |
-
return f"일부 복도 경사로 구간에서 휠체어 {wheelchair_count}대가 교행할 때 문턱 장애물로 인해 속도가 지체되는 병목 현상이 남아있고, 시끄러운 사이렌 상황에서 유도자의 음성 전달이 부분적으로 방해받는 무선 수신 음영이 관찰됨."
|
| 163 |
-
elif section == "result_next":
|
| 164 |
-
return "경사로 주변 미끄럼 방지 매트를 시공하고 문턱을 완만하게 개조할 것이며, 비상 대피용 휴대식 메가폰을 추가 배치할 예정임. 다음 훈련 시에는 야간 화재 대비 최소 근무자 중심 대피 훈련을 추진하겠음."
|
| 165 |
-
elif section == "result_guardian":
|
| 166 |
-
return f"보호자 유선 상담({guardian_call_date}) 결과 거동불편 수급자의 대피 안전 우려가 있어 1:1 전담 인력 매칭 계획을 설명하였으며, 보호자 비상 연락처 최신화를 통해 {guardian_reflected} 상태를 완료함."
|
| 167 |
-
|
| 168 |
-
elif training_type == "재난훈련":
|
| 169 |
-
# Multi-occupational Opinions
|
| 170 |
-
if section == "opinion_시설장_1":
|
| 171 |
-
return "지진 상황 발생 시 건물 붕괴 위험에 대비한 실내 긴급 대피처와 실외 집결지의 안전 상태를 재정비하고, 재난 통제 매뉴얼을 전 직원에게 상시 공람 지시함."
|
| 172 |
-
elif section == "opinion_시설장_2":
|
| 173 |
-
return "정전에 대비해 비상 발전기 가동 상태를 총괄 확인하고 비상용 손전등, 재난 안전모 등 구호 물품의 예산 및 전 직원 지급을 승인함."
|
| 174 |
-
elif section == "opinion_시설장_role":
|
| 175 |
-
return "재난 대책 본부 총괄 지휘, 건물 안전성 관리 및 비상 용품 결재"
|
| 176 |
-
|
| 177 |
-
elif section == "opinion_사회복지사_1":
|
| 178 |
-
return f"지진 경보 시 패닉 상태에 빠질 수 있는 수급자 {recipient_count}명의 인지 상태를 감안해 특별 심리 지원 및 1:1 대피 매칭 표를 작성함."
|
| 179 |
-
elif section == "opinion_사회복지사_2":
|
| 180 |
-
return "재난 대비 수급자 통보체계를 검토하고, 유선 상담 데이터를 기반으로 보호자들에게 재난 대피 훈련 계획과 대피소 위치를 서비스 동의서에 명시함."
|
| 181 |
-
elif section == "opinion_사회복지사_role":
|
| 182 |
-
return "심리 불안 위험자 선별, 비상 대피 계획 서비스 계획 반영, 훈련 시나리오 작성"
|
| 183 |
-
|
| 184 |
-
elif section == "opinion_요양보호사_1":
|
| 185 |
-
return f"지진 진동 발생 시 휠체어 {wheelchair_count}대 및 보행보조 장비의 미끄러짐으로 인한 충돌 및 가구 낙하로 낙상과 신체 골절 위험이 크게 관찰됨."
|
| 186 |
-
elif section == "opinion_요양보호사_2":
|
| 187 |
-
return "진동 시 수급자를 테이블 밑으로 대피시키거나 베개 등으로 머리를 최우선적으로 보호하며, 휠체어의 바퀴 고정 레버를 반드시 제동 위치로 잡고 밀착 대기해야 함."
|
| 188 |
-
elif section == "opinion_요양보호사_role":
|
| 189 |
-
return "신체 낙하 위험 차단, 매칭 수급자 긴급 보호 및 안전 유도"
|
| 190 |
-
|
| 191 |
-
elif section == "opinion_간호조무사_1":
|
| 192 |
-
return f"지진으로 발생 가능한 타박상, 열상 등 골절 의심 환자에 대비하여 신속 진단 가이드를 확인하고 대피 완료 후 활력징후 및 심리 충격 상태를 관찰하였음."
|
| 193 |
-
elif section == "opinion_간호조무사_2":
|
| 194 |
-
return "비상 구급 키트 내 깁스 붕대 및 외상 약품 재고를 파악하고, 당뇨 환자 등 투약 시간이 중요한 수급자의 약품 휴대 보존 상태를 점검함."
|
| 195 |
-
elif section == "opinion_간호조무사_role":
|
| 196 |
-
return "외상 응급처치, 비상 구급품 관리, 대피 완료자 건강 검진"
|
| 197 |
-
|
| 198 |
-
elif section == "opinion_물리치료사_1":
|
| 199 |
-
return "지진 대피 시 야외로 나가는 울퉁불퉁한 보도블록이나 자갈길 위에서 보행 불안정 수급자들의 균형 저하로 발목 부상 및 전도 위험이 높은 것으로 확인됨."
|
| 200 |
-
elif section == "opinion_물리치료사_2":
|
| 201 |
-
return "대피경로 상의 경사면을 지날 때 뒤에서 받쳐주는 신체 지지 운동법을 활용하고 훈련 종료 후 피로가 쌓인 관절과 근육을 풀어주기 위한 이완 치료를 지도함."
|
| 202 |
-
elif section == "opinion_물리치료사_role":
|
| 203 |
-
return "이동 경로 노면 상태 안내, 이동 보조 기구 제동 자세 교정"
|
| 204 |
-
|
| 205 |
-
# 2차 결과 반영
|
| 206 |
-
elif section == "result_problem":
|
| 207 |
-
return f"진동 상황 시 휠체어 고정 조치 미흡으로 2차 충돌 위험이 감지되었고, 대피 경로 내 복도 코너 부분의 교행 정체와 함께 중증 치매 수급자 1명의 대피 거부 돌발 행동이 발생함."
|
| 208 |
-
elif section == "result_action":
|
| 209 |
-
return f"지진 발생에 따른 머리 보호(베개 및 방석 활용) 후, 진동이 멈춘 상태를 가정하고 대피경로({route_checked})를 따라 야외 대피하였으며, 정전 대비 플래시 활용 및 연락체계({contact_checked})를 검토함."
|
| 210 |
-
elif section == "result_role_actions":
|
| 211 |
-
return "시설장은 비상 발전 상태를 체크하고, 복지사는 치매 수급자에게 안심 지지 요령을 제공했으며, 요양보호사는 1:1 대피 이송을, 간호조무사는 활력징후 및 구호 약품을 지참하였고, 물리치료사는 지진 시 이동 지지를 담당함."
|
| 212 |
-
elif section == "result_outcome":
|
| 213 |
-
return f"초기 머리 보호 요령 및 질서 정연한 이동을 통해 대피완료 시간이 {evac_time}로 측정되었고, 재난 상황에 대응할 수 있는 요령이 성공적으로 습득되었음."
|
| 214 |
-
elif section == "result_data":
|
| 215 |
-
return f"총 참여자 {total_participants}명 (종사자 {staff_count}명, 수급자 {recipient_count}명), 대피취약자 {vulnerable_count}명, 휠체어 {wheelchair_count}대, 보행보조 {walk_aid_count}명, 대피완료 {evac_time}, 소화기 실습 {fire_ext_demo}, 응급대응 {emergency_mock}."
|
| 216 |
-
elif section == "result_risks":
|
| 217 |
-
return "야외 집결지인 주차장 노면에 돌출된 경계석이 보행자 이동 시 발 걸림 낙상 요소로 식별되었고, 재난 상황 긴장감으로 일부 고령 수급자의 단기 혈압 상승세가 유지됨."
|
| 218 |
-
elif section == "result_next":
|
| 219 |
-
return "집결지 바닥의 돌출물에 황색 안전 테이프를 부착하고 미끄럼 방지 처리를 추가 수립하며, 다음 훈련 시에는 인근 실내 지정 대피소(대강당)로의 실질 연계 동선 훈련을 실시하겠음."
|
| 220 |
-
elif section == "result_guardian":
|
| 221 |
-
return f"보호자 유선 상담({guardian_call_date}) 결과 거동불편 수급자의 대피 안전 우려가 있어 1:1 전담 인력 매칭 계획을 설명하였으며, 보호자 비상 연락처 최신화를 통해 {guardian_reflected} 상태를 완료함."
|
| 222 |
-
|
| 223 |
-
else: # 안전교육
|
| 224 |
-
# Multi-occupational Opinions
|
| 225 |
-
if section == "opinion_시설장_1":
|
| 226 |
-
return "기관 내 낙상, 질식, 소방 등 안전 관련 매뉴얼을 수급자 입소 규정에 의무 첨부하고, 직종 간의 매뉴얼 숙지 편차가 없도록 연간 안전교육 이수제를 운영 지시함."
|
| 227 |
-
elif section == "opinion_시설장_2":
|
| 228 |
-
return "응급상황 모의대응 능력 향상을 위한 고성능 CPR 마네킹 및 소방 살수 장비 구입을 지원하고 원내 대피 유도 유광 조끼 구입 예산을 전액 배정함."
|
| 229 |
-
elif section == "opinion_시설장_role":
|
| 230 |
-
return "안전 교육 체계 구축, 시설 내 안전 규정 준수 감독 및 교육 교구 승인"
|
| 231 |
-
|
| 232 |
-
elif section == "opinion_사회복지사_1":
|
| 233 |
-
return f"낙상 및 질식 고위험 수급자 {vulnerable_count}명의 사례 회의를 통해 개별 맞춤형 급여제공계획을 변경하고 안전교육에 따른 예방 수칙을 정비함."
|
| 234 |
-
elif section == "opinion_사회복지사_2":
|
| 235 |
-
return "보호자와의 상담 기록을 기반으로 기관 내 응급상황 대응 절차를 공유하고 동의 여부를 확인하여, 안전 교육 계획 및 CPR 동의서를 수집·보관함."
|
| 236 |
-
elif section == "opinion_사회복지사_role":
|
| 237 |
-
return "안전 동의서 관리, 수급자 낙상위험도 재평가 및 교육 실습 행정 기록"
|
| 238 |
-
|
| 239 |
-
elif section == "opinion_요양보호사_1":
|
| 240 |
-
return f"인지 장애가 있는 수급자들은 기침 반사가 저하되어 식사 시 목 넘김이 불완전해 질식 위험이 있으며, 휠체어 제동 장치를 수시로 잊어 침대 탑승 시 낙상 위험이 높음."
|
| 241 |
-
elif section == "opinion_요양보호사_2":
|
| 242 |
-
return f"식사 지원 시 상체를 바르게 세우는 식사 자세를 교정하고, 휠체어 승하차 시에는 브레이크 상태를 보호자가 전화 상담에서 언급한 낙상 방지 수칙에 맞춰 필히 이중 잠금하여 제공함."
|
| 243 |
-
elif section == "opinion_요양보호사_role":
|
| 244 |
-
return "안전 식사 동행 보조, 휠체어 안전 조작 및 낙상 예방 규칙 적용"
|
| 245 |
-
|
| 246 |
-
elif section == "opinion_간호조무사_1":
|
| 247 |
-
return "질식 상황 모의 시 하임리히법을 정확하게 시연하고, 심정지 수급자 발생에 따른 신속한 인공호흡 및 자동심장충격기(AED) 패드 부착 위치와 상황별 심전도 분석 절차를 교육함."
|
| 248 |
-
elif section == "opinion_간호조무사_2":
|
| 249 |
-
return "감염 관리를 위한 손 소독 절차 및 전염병 유행 시 격리 지침을 사전에 수립하고 비상 약품 캐비닛의 열쇠 및 배치 상태를 전 직원에 전파함."
|
| 250 |
-
elif section == "opinion_간호조무사_role":
|
| 251 |
-
return "하임리히법 및 CPR 시연 교육 주도, 감염병 예방 규칙 지도"
|
| 252 |
-
|
| 253 |
-
elif section == "opinion_물리치료사_1":
|
| 254 |
-
return "보행보조기(워커) 사용 어르신들이 방향 전환 시 다리 엉킴으로 인한 낙상 위험이 큼을 확인하고 하지 근력 강화를 위한 재활 운동이 중요함을 인지함."
|
| 255 |
-
elif section == "opinion_물리치료사_2":
|
| 256 |
-
return "휠체어와 워커 사이의 올바른 체중 이동 중심 훈련을 주 3회 정기 진행하고, 낙상 예방 3대 신체 보조 기법을 요양보호사에게 직무 교육으로 연계함."
|
| 257 |
-
elif section == "opinion_물리치료사_role":
|
| 258 |
-
return "낙상 예방 스트레칭 교육, 보조 보행 기구 교정 및 보행 재활 운동 제공"
|
| 259 |
-
|
| 260 |
-
# 2차 결과 반영
|
| 261 |
-
elif section == "result_problem":
|
| 262 |
-
return "수급자들의 근력 약화로 낙상 위험도가 높은 상태에서 응급 도구(AED) 및 약품함 보관 위치 인지 부족이 확인되었고, 역할 분담에 따른 신속한 응급 조치 연계가 일부 미흡하였음."
|
| 263 |
-
elif section == "result_action":
|
| 264 |
-
return f"다직종 합동 안전교육을 실시하고 대피취약자 {vulnerable_count}명에 대한 1:1 보행 지원법을 실습하였으며, 하임리히법 및 AED를 활용한 심폐소생술 모의실습({emergency_mock})을 진행함."
|
| 265 |
-
elif section == "result_role_actions":
|
| 266 |
-
return "시설장은 안전 감독을, 복지사는 안전 동의서 수집 및 기록을, 요양보호사는 휠체어 이중 제동 실습을, 간호조무사는 하임리히법과 CPR 교육을, 물리치료사는 낙상 방지 보행 중심 지도를 담당함."
|
| 267 |
-
elif section == "result_outcome":
|
| 268 |
-
return "전 직원이 응급상황 발생 시 본인의 담당 역할을 숙지하고 AED 조작 및 하임리히법 등 위급 상황에서의 초기 대응 처치 능력이 크게 향상되었음."
|
| 269 |
-
elif section == "result_data":
|
| 270 |
-
return f"총 참여자 {total_participants}명 (종사자 {staff_count}명, 수급자 {recipient_count}명), 대피취약자 {vulnerable_count}명, 휠체어 {wheelchair_count}대, 보행보조 {walk_aid_count}명, 대피완료 {evac_time}, 소화기 실습 {fire_ext_demo}, 응급대응 {emergency_mock}."
|
| 271 |
-
elif section == "result_risks":
|
| 272 |
-
return "종사자들의 근무 교대 시 비상 역할 인수인계 매뉴얼 숙지 편차가 미세하게 관찰되고 있으며, 원내 일부 복도 조명이 다소 어두워 야간 낙상 유발 요인으로 작용할 우려가 보임."
|
| 273 |
-
elif section == "result_next":
|
| 274 |
-
return "교대 대장 양식에 '비상대비반 역할'을 표기하는 조치를 규정 개정하고, 어두운 복도에 동작 감지식 야간 안전 유도등을 추가 시공하며, 분기별 1회 CPR 모의 테스트를 실시하겠음."
|
| 275 |
-
elif section == "result_guardian":
|
| 276 |
-
return f"보호자 유선 상담({guardian_call_date}) 결과 거동불편 수급자의 대피 안전 우려가 있어 1:1 전담 인력 매칭 계획을 설명하였으며, 보호자 비상 연락처 최신화를 통해 {guardian_reflected} 상태를 완료함."
|
| 277 |
-
|
| 278 |
-
return ""
|
| 279 |
-
|
| 280 |
-
# ---------------------------------------------------------
|
| 281 |
-
# Compile Data Structures for Excel Workbook
|
| 282 |
-
# ---------------------------------------------------------
|
| 283 |
-
def compile_document_data(info, training_type):
|
| 284 |
-
data = {
|
| 285 |
-
'facility_name': info['facility_name'],
|
| 286 |
-
'facility_head': info['facility_head'],
|
| 287 |
-
'writer': info['writer'],
|
| 288 |
-
'training_date': info['training_date'].strftime('%Y-%m-%d') if isinstance(info['training_date'], (date, datetime)) else str(info['training_date']),
|
| 289 |
-
'training_time': info['training_time'],
|
| 290 |
-
'training_location': info['training_location'],
|
| 291 |
-
'training_type': training_type,
|
| 292 |
-
'host': info['host'],
|
| 293 |
-
'total_participants': info['total_participants'],
|
| 294 |
-
'staff_participants': info['staff_participants'],
|
| 295 |
-
'recipient_participants': info['recipient_participants'],
|
| 296 |
-
'evac_time': info['evac_time'],
|
| 297 |
-
'vulnerable_count': info['vulnerable_count'],
|
| 298 |
-
'wheelchair_count': info['wheelchair_count'],
|
| 299 |
-
'walk_aid_count': info['walk_aid_count'],
|
| 300 |
-
'emergency_mock': info['emergency_mock'],
|
| 301 |
-
'fire_ext_demo': info['fire_ext_demo'],
|
| 302 |
-
'route_checked': info['route_checked'],
|
| 303 |
-
'contact_checked': info['contact_checked'],
|
| 304 |
-
'guardian_call_date': info['guardian_call_date'].strftime('%Y-%m-%d') if isinstance(info['guardian_call_date'], (date, datetime)) else str(info['guardian_call_date']),
|
| 305 |
-
'guardian_opinion': info['guardian_opinion'],
|
| 306 |
-
'guardian_response': info['guardian_response'],
|
| 307 |
-
'guardian_reflected': info['guardian_reflected'],
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
-
# 5 Job Roles Opinions
|
| 311 |
-
roles_list = ['시설장', '사회복지사', '요양보호사', '간호조무사', '물리치료사']
|
| 312 |
-
for r in roles_list:
|
| 313 |
-
data[f'opinion_{r}_1'] = generate_narrative_text(data, f'opinion_{r}_1', training_type)
|
| 314 |
-
data[f'opinion_{r}_2'] = generate_narrative_text(data, f'opinion_{r}_2', training_type)
|
| 315 |
-
data[f'opinion_{r}_role'] = generate_narrative_text(data, f'opinion_{r}_role', training_type)
|
| 316 |
-
|
| 317 |
-
# 2차 결과 반영
|
| 318 |
-
data['result_problem'] = generate_narrative_text(data, 'result_problem', training_type)
|
| 319 |
-
data['result_action'] = generate_narrative_text(data, 'result_action', training_type)
|
| 320 |
-
data['result_role_actions'] = generate_narrative_text(data, 'result_role_actions', training_type)
|
| 321 |
-
data['result_outcome'] = generate_narrative_text(data, 'result_outcome', training_type)
|
| 322 |
-
data['result_data'] = generate_narrative_text(data, 'result_data', training_type)
|
| 323 |
-
data['result_risks'] = generate_narrative_text(data, 'result_risks', training_type)
|
| 324 |
-
data['result_next'] = generate_narrative_text(data, 'result_next', training_type)
|
| 325 |
-
data['result_guardian'] = generate_narrative_text(data, 'result_guardian', training_type)
|
| 326 |
-
|
| 327 |
-
return data
|
| 328 |
-
|
| 329 |
-
# ---------------------------------------------------------
|
| 330 |
-
# openpyxl Styling Helpers
|
| 331 |
-
# ---------------------------------------------------------
|
| 332 |
-
header_fill = PatternFill(start_color="F2F4F7", end_color="F2F4F7", fill_type="solid") # soft gray
|
| 333 |
-
section_fill = PatternFill(start_color="E8EFF9", end_color="E8EFF9", fill_type="solid") # professional light blue
|
| 334 |
-
|
| 335 |
-
label_font = Font(name="맑은 고딕", size=9.5, bold=True, color="374151")
|
| 336 |
-
value_font = Font(name="맑은 고딕", size=9.5, color="111827")
|
| 337 |
-
section_font = Font(name="맑은 고딕", size=10.5, bold=True, color="1A365D")
|
| 338 |
-
|
| 339 |
-
align_center = Alignment(vertical="center", horizontal="center")
|
| 340 |
-
align_left = Alignment(vertical="center", horizontal="left", indent=1)
|
| 341 |
-
align_left_wrap = Alignment(vertical="center", horizontal="left", wrap_text=True)
|
| 342 |
-
|
| 343 |
-
thin_border = Border(
|
| 344 |
-
left=Side(style='thin', color='D0D5DD'),
|
| 345 |
-
right=Side(style='thin', color='D0D5DD'),
|
| 346 |
-
top=Side(style='thin', color='D0D5DD'),
|
| 347 |
-
bottom=Side(style='thin', color='D0D5DD')
|
| 348 |
-
)
|
| 349 |
-
|
| 350 |
-
def style_range(ws, cell_range, border=None, fill=None, font=None, alignment=None):
|
| 351 |
-
for row in ws[cell_range]:
|
| 352 |
-
for cell in row:
|
| 353 |
-
if border is not None:
|
| 354 |
-
cell.border = border
|
| 355 |
-
if fill is not None:
|
| 356 |
-
cell.fill = fill
|
| 357 |
-
if font is not None:
|
| 358 |
-
cell.font = font
|
| 359 |
-
if alignment is not None:
|
| 360 |
-
cell.alignment = alignment
|
| 361 |
-
|
| 362 |
-
def write_spacer(ws, r_num):
|
| 363 |
-
ws.row_dimensions[r_num].height = 10
|
| 364 |
-
|
| 365 |
-
def write_section_header(ws, r_num, title):
|
| 366 |
-
ws.row_dimensions[r_num].height = 25
|
| 367 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=9)
|
| 368 |
-
ws.cell(row=r_num, column=1, value=title)
|
| 369 |
-
style_range(ws, f"A{r_num}:I{r_num}", fill=section_fill, font=section_font, alignment=align_left, border=thin_border)
|
| 370 |
-
|
| 371 |
-
def setup_page_print(ws):
|
| 372 |
-
ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
|
| 373 |
-
ws.page_setup.paperSize = ws.PAPERSIZE_A4
|
| 374 |
-
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
| 375 |
-
ws.page_setup.fitToWidth = 1
|
| 376 |
-
ws.page_setup.fitToHeight = 1
|
| 377 |
-
|
| 378 |
-
ws.page_margins.left = 0.5
|
| 379 |
-
ws.page_margins.right = 0.5
|
| 380 |
-
ws.page_margins.top = 0.5
|
| 381 |
-
ws.page_margins.bottom = 0.5
|
| 382 |
-
|
| 383 |
-
ws.views.sheetView[0].showGridLines = True
|
| 384 |
-
|
| 385 |
-
def write_general_info_row(ws, r_num, key1, val1, key2, val2):
|
| 386 |
-
ws.row_dimensions[r_num].height = 24
|
| 387 |
-
|
| 388 |
-
# Left key-val
|
| 389 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 390 |
-
ws.cell(row=r_num, column=1, value=key1)
|
| 391 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
|
| 392 |
-
ws.cell(row=r_num, column=3, value=val1)
|
| 393 |
-
|
| 394 |
-
# Right key-val
|
| 395 |
-
ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=6)
|
| 396 |
-
ws.cell(row=r_num, column=5, value=key2)
|
| 397 |
-
ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=9)
|
| 398 |
-
ws.cell(row=r_num, column=7, value=val2)
|
| 399 |
-
|
| 400 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 401 |
-
style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 402 |
-
style_range(ws, f"E{r_num}:F{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 403 |
-
style_range(ws, f"G{r_num}:I{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 404 |
-
|
| 405 |
-
# ---------------------------------------------------------
|
| 406 |
-
# Excel Creator: Header and Content Helpers
|
| 407 |
-
# ---------------------------------------------------------
|
| 408 |
-
def write_title_and_approval(ws, title_text, info):
|
| 409 |
-
ws.row_dimensions[1].height = 22
|
| 410 |
-
ws.row_dimensions[2].height = 43
|
| 411 |
-
|
| 412 |
-
ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=7)
|
| 413 |
-
title_cell = ws.cell(row=1, column=1)
|
| 414 |
-
title_cell.value = title_text
|
| 415 |
-
style_range(ws, "A1:G2",
|
| 416 |
-
font=Font(name="맑은 고딕", size=15, bold=True, color="111827"),
|
| 417 |
-
alignment=align_center,
|
| 418 |
-
border=thin_border)
|
| 419 |
-
|
| 420 |
-
ws.cell(row=1, column=8, value="담당자")
|
| 421 |
-
ws.cell(row=1, column=9, value="시설장")
|
| 422 |
-
style_range(ws, "H1:I1",
|
| 423 |
-
fill=header_fill,
|
| 424 |
-
font=Font(name="맑은 고딕", size=9.5, bold=True, color="374151"),
|
| 425 |
-
alignment=align_center,
|
| 426 |
-
border=thin_border)
|
| 427 |
-
|
| 428 |
-
ws.cell(row=2, column=8, value="")
|
| 429 |
-
ws.cell(row=2, column=9, value="")
|
| 430 |
-
style_range(ws, "H2:I2", border=thin_border)
|
| 431 |
-
|
| 432 |
-
def write_plan_participants(ws, r_num, info):
|
| 433 |
-
ws.row_dimensions[r_num].height = 24
|
| 434 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 435 |
-
ws.cell(row=r_num, column=1, value="총 참여인원")
|
| 436 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
|
| 437 |
-
ws.cell(row=r_num, column=3, value=f"{info['total_participants']} 명")
|
| 438 |
-
|
| 439 |
-
ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=6)
|
| 440 |
-
ws.cell(row=r_num, column=5, value="종사자 수")
|
| 441 |
-
ws.cell(row=r_num, column=7, value=f"{info['staff_participants']} 명")
|
| 442 |
-
|
| 443 |
-
ws.cell(row=r_num, column=8, value="수급자 수")
|
| 444 |
-
ws.cell(row=r_num, column=9, value=f"{info['recipient_participants']} 명")
|
| 445 |
-
|
| 446 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 447 |
-
style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 448 |
-
style_range(ws, f"E{r_num}:F{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 449 |
-
|
| 450 |
-
ws.cell(row=r_num, column=7).font = value_font
|
| 451 |
-
ws.cell(row=r_num, column=7).alignment = align_center
|
| 452 |
-
ws.cell(row=r_num, column=7).border = thin_border
|
| 453 |
-
|
| 454 |
-
ws.cell(row=r_num, column=8).fill = header_fill
|
| 455 |
-
ws.cell(row=r_num, column=8).font = label_font
|
| 456 |
-
ws.cell(row=r_num, column=8).alignment = align_center
|
| 457 |
-
ws.cell(row=r_num, column=8).border = thin_border
|
| 458 |
-
|
| 459 |
-
ws.cell(row=r_num, column=9).font = value_font
|
| 460 |
-
ws.cell(row=r_num, column=9).alignment = align_center
|
| 461 |
-
ws.cell(row=r_num, column=9).border = thin_border
|
| 462 |
-
|
| 463 |
-
r_num += 1
|
| 464 |
-
|
| 465 |
-
ws.row_dimensions[r_num].height = 24
|
| 466 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 467 |
-
ws.cell(row=r_num, column=1, value="대피취약자 수")
|
| 468 |
-
ws.cell(row=r_num, column=3, value=f"{info['vulnerable_count']} 명")
|
| 469 |
-
|
| 470 |
-
ws.merge_cells(start_row=r_num, start_column=4, end_row=r_num, end_column=5)
|
| 471 |
-
ws.cell(row=r_num, column=4, value="휠체어 사용 대상자")
|
| 472 |
-
ws.cell(row=r_num, column=6, value=f"{info['wheelchair_count']} 명")
|
| 473 |
-
|
| 474 |
-
ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=8)
|
| 475 |
-
ws.cell(row=r_num, column=7, value="보행보조 필요자")
|
| 476 |
-
ws.cell(row=r_num, column=9, value=f"{info['walk_aid_count']} 명")
|
| 477 |
-
|
| 478 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 479 |
-
ws.cell(row=r_num, column=3).font = value_font
|
| 480 |
-
ws.cell(row=r_num, column=3).alignment = align_center
|
| 481 |
-
ws.cell(row=r_num, column=3).border = thin_border
|
| 482 |
-
|
| 483 |
-
style_range(ws, f"D{r_num}:E{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 484 |
-
ws.cell(row=r_num, column=6).font = value_font
|
| 485 |
-
ws.cell(row=r_num, column=6).alignment = align_center
|
| 486 |
-
ws.cell(row=r_num, column=6).border = thin_border
|
| 487 |
-
|
| 488 |
-
style_range(ws, f"G{r_num}:H{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 489 |
-
ws.cell(row=r_num, column=9).font = value_font
|
| 490 |
-
ws.cell(row=r_num, column=9).alignment = align_center
|
| 491 |
-
ws.cell(row=r_num, column=9).border = thin_border
|
| 492 |
-
|
| 493 |
-
return r_num
|
| 494 |
-
|
| 495 |
-
def write_plan_details(ws, r_num, info):
|
| 496 |
-
write_section_header(ws, r_num, "2. 훈련 목적 및 계획")
|
| 497 |
-
r_num += 1
|
| 498 |
-
|
| 499 |
-
ws.row_dimensions[r_num].height = 24
|
| 500 |
-
ws.row_dimensions[r_num+1].height = 24
|
| 501 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+1, end_column=2)
|
| 502 |
-
ws.cell(row=r_num, column=1, value="훈련 목적")
|
| 503 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num+1, end_column=9)
|
| 504 |
-
|
| 505 |
-
purpose_text = (
|
| 506 |
-
f"{info['training_type']} 상황 발생 시 신속하고 체계적인 대피 및 초기 대응 능력을 배양하여 수급자의 생명과 신체를 보호하고, "
|
| 507 |
-
"다직종 간의 유기적인 협력을 통해 장기요양기관 평가 기준에 적합한 안전 관리 체계를 구축하고자 함."
|
| 508 |
-
)
|
| 509 |
-
ws.cell(row=r_num, column=3, value=purpose_text)
|
| 510 |
-
style_range(ws, f"A{r_num}:B{r_num+1}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 511 |
-
style_range(ws, f"C{r_num}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 512 |
-
r_num += 2
|
| 513 |
-
|
| 514 |
-
ws.row_dimensions[r_num].height = 24
|
| 515 |
-
ws.row_dimensions[r_num+1].height = 24
|
| 516 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+1, end_column=2)
|
| 517 |
-
ws.cell(row=r_num, column=1, value="대피 경로")
|
| 518 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num+1, end_column=9)
|
| 519 |
-
|
| 520 |
-
route_text = (
|
| 521 |
-
"피난계단 및 지정 대피경로 사전 확인: 실내 비상구 표시등 및 대피 경로를 따라 지정된 외부 집결지까지의 이동 동선을 파악하고 장애물을 완전히 제거함.\n"
|
| 522 |
-
f"(대피경로 확인 상태: {info['route_checked']})"
|
| 523 |
-
)
|
| 524 |
-
ws.cell(row=r_num, column=3, value=route_text)
|
| 525 |
-
style_range(ws, f"A{r_num}:B{r_num+1}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 526 |
-
style_range(ws, f"C{r_num}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 527 |
-
r_num += 2
|
| 528 |
-
|
| 529 |
-
write_section_header(ws, r_num, "3. 다직종별 역할 분담 계획")
|
| 530 |
-
r_num += 1
|
| 531 |
-
|
| 532 |
-
ws.row_dimensions[r_num].height = 24
|
| 533 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 534 |
-
ws.cell(row=r_num, column=1, value="직종")
|
| 535 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
|
| 536 |
-
ws.cell(row=r_num, column=3, value="역할 및 주요 업무 분담 내용")
|
| 537 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 538 |
-
style_range(ws, f"C{r_num}:I{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 539 |
-
r_num += 1
|
| 540 |
-
|
| 541 |
-
roles = [
|
| 542 |
-
("시설장", "훈련 총괄, 지휘, 평가 대응, 유관기관 연락체계 확인"),
|
| 543 |
-
("사회복지사", "수급자 명단 확인, 대피취약자 분류, 보호자 연락, 훈련기록 작성"),
|
| 544 |
-
("요양보호사", "수급자 대피 보조, 이동 지원, 낙상위험자 보호, 실제 현장 대응"),
|
| 545 |
-
("간호조무사", "응급상황 확인, 활력징후 확인, 투약·건강상태 확인, 부상자 응급조치"),
|
| 546 |
-
("물리치료사", "보행불안정 수급자 이동지원, 휠체어·워커 사용 대상자 대피 지원, 낙상예방 확인")
|
| 547 |
-
]
|
| 548 |
-
for role, desc in roles:
|
| 549 |
-
ws.row_dimensions[r_num].height = 24
|
| 550 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 551 |
-
ws.cell(row=r_num, column=1, value=role)
|
| 552 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
|
| 553 |
-
ws.cell(row=r_num, column=3, value=desc)
|
| 554 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 555 |
-
style_range(ws, f"C{r_num}:I{r_num}", font=value_font, alignment=align_left, border=thin_border)
|
| 556 |
-
r_num += 1
|
| 557 |
-
|
| 558 |
-
write_section_header(ws, r_num, "4. 비상 연락 및 유관기관 협조 체계")
|
| 559 |
-
r_num += 1
|
| 560 |
-
|
| 561 |
-
ws.row_dimensions[r_num].height = 24
|
| 562 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 563 |
-
ws.cell(row=r_num, column=1, value="유관기관")
|
| 564 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
|
| 565 |
-
ws.cell(row=r_num, column=3, value="비상연락처")
|
| 566 |
-
ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=9)
|
| 567 |
-
ws.cell(row=r_num, column=5, value="주요 협조 및 대응 내용")
|
| 568 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 569 |
-
style_range(ws, f"C{r_num}:D{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 570 |
-
style_range(ws, f"E{r_num}:I{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 571 |
-
r_num += 1
|
| 572 |
-
|
| 573 |
-
contacts = [
|
| 574 |
-
("소방서 (119)", "119 / 관할소방서", f"화재 발생 신고, 인명 구조 및 화재 진압 지원 (소화기 시연: {info['fire_ext_demo']})"),
|
| 575 |
-
("경찰서 (112)", "112 / 관할지구대", "훈련 시 주변 교통 통제 및 안전 유지 협조"),
|
| 576 |
-
("협력 의료기관", "인근 종합병원", f"응급 환자 발생 시 긴급 이송 및 긴급 의료 지원체계 가동 (모의대응: {info['emergency_mock']})")
|
| 577 |
-
]
|
| 578 |
-
for org, phone, task in contacts:
|
| 579 |
-
ws.row_dimensions[r_num].height = 24
|
| 580 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 581 |
-
ws.cell(row=r_num, column=1, value=org)
|
| 582 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
|
| 583 |
-
ws.cell(row=r_num, column=3, value=phone)
|
| 584 |
-
ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=9)
|
| 585 |
-
ws.cell(row=r_num, column=5, value=task)
|
| 586 |
-
|
| 587 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 588 |
-
style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 589 |
-
style_range(ws, f"E{r_num}:I{r_num}", font=value_font, alignment=align_left, border=thin_border)
|
| 590 |
-
r_num += 1
|
| 591 |
-
|
| 592 |
-
return r_num
|
| 593 |
-
|
| 594 |
-
def write_report_participants(ws, r_num, info):
|
| 595 |
-
ws.row_dimensions[r_num].height = 24
|
| 596 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 597 |
-
ws.cell(row=r_num, column=1, value="총 참여인원")
|
| 598 |
-
ws.cell(row=r_num, column=3, value=f"{info['total_participants']} 명")
|
| 599 |
-
|
| 600 |
-
ws.merge_cells(start_row=r_num, start_column=4, end_row=r_num, end_column=5)
|
| 601 |
-
ws.cell(row=r_num, column=4, value="종사자 참여자 수")
|
| 602 |
-
ws.cell(row=r_num, column=6, value=f"{info['staff_participants']} 명")
|
| 603 |
-
|
| 604 |
-
ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=8)
|
| 605 |
-
ws.cell(row=r_num, column=7, value="수급자 참여자 수")
|
| 606 |
-
ws.cell(row=r_num, column=9, value=f"{info['recipient_participants']} 명")
|
| 607 |
-
|
| 608 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 609 |
-
ws.cell(row=r_num, column=3).font = value_font
|
| 610 |
-
ws.cell(row=r_num, column=3).alignment = align_center
|
| 611 |
-
ws.cell(row=r_num, column=3).border = thin_border
|
| 612 |
-
|
| 613 |
-
style_range(ws, f"D{r_num}:E{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 614 |
-
ws.cell(row=r_num, column=6).font = value_font
|
| 615 |
-
ws.cell(row=r_num, column=6).alignment = align_center
|
| 616 |
-
ws.cell(row=r_num, column=6).border = thin_border
|
| 617 |
-
|
| 618 |
-
style_range(ws, f"G{r_num}:H{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 619 |
-
ws.cell(row=r_num, column=9).font = value_font
|
| 620 |
-
ws.cell(row=r_num, column=9).alignment = align_center
|
| 621 |
-
ws.cell(row=r_num, column=9).border = thin_border
|
| 622 |
-
|
| 623 |
-
r_num += 1
|
| 624 |
-
|
| 625 |
-
ws.row_dimensions[r_num].height = 24
|
| 626 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 627 |
-
ws.cell(row=r_num, column=1, value="대피완료 시간")
|
| 628 |
-
ws.cell(row=r_num, column=3, value=f"{info['evac_time']}")
|
| 629 |
-
|
| 630 |
-
ws.merge_cells(start_row=r_num, start_column=4, end_row=r_num, end_column=5)
|
| 631 |
-
ws.cell(row=r_num, column=4, value="대피취약자 수")
|
| 632 |
-
ws.cell(row=r_num, column=6, value=f"{info['vulnerable_count']} 명")
|
| 633 |
-
|
| 634 |
-
ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=8)
|
| 635 |
-
ws.cell(row=r_num, column=7, value="휠체어 사용 대상자")
|
| 636 |
-
ws.cell(row=r_num, column=9, value=f"{info['wheelchair_count']} 명")
|
| 637 |
-
|
| 638 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 639 |
-
ws.cell(row=r_num, column=3).font = value_font
|
| 640 |
-
ws.cell(row=r_num, column=3).alignment = align_center
|
| 641 |
-
ws.cell(row=r_num, column=3).border = thin_border
|
| 642 |
-
|
| 643 |
-
style_range(ws, f"D{r_num}:E{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 644 |
-
ws.cell(row=r_num, column=6).font = value_font
|
| 645 |
-
ws.cell(row=r_num, column=6).alignment = align_center
|
| 646 |
-
ws.cell(row=r_num, column=6).border = thin_border
|
| 647 |
-
|
| 648 |
-
style_range(ws, f"G{r_num}:H{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 649 |
-
ws.cell(row=r_num, column=9).font = value_font
|
| 650 |
-
ws.cell(row=r_num, column=9).alignment = align_center
|
| 651 |
-
ws.cell(row=r_num, column=9).border = thin_border
|
| 652 |
-
|
| 653 |
-
r_num += 1
|
| 654 |
-
|
| 655 |
-
ws.row_dimensions[r_num].height = 24
|
| 656 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 657 |
-
ws.cell(row=r_num, column=1, value="보행보조 필요자")
|
| 658 |
-
ws.cell(row=r_num, column=3, value=f"{info['walk_aid_count']} 명")
|
| 659 |
-
|
| 660 |
-
ws.merge_cells(start_row=r_num, start_column=4, end_row=r_num, end_column=5)
|
| 661 |
-
ws.cell(row=r_num, column=4, value="응급상황 모의대응")
|
| 662 |
-
ws.cell(row=r_num, column=6, value=f"{info['emergency_mock']}")
|
| 663 |
-
|
| 664 |
-
ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=8)
|
| 665 |
-
ws.cell(row=r_num, column=7, value="소화기 사용 시연")
|
| 666 |
-
ws.cell(row=r_num, column=9, value=f"{info['fire_ext_demo']}")
|
| 667 |
-
|
| 668 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 669 |
-
ws.cell(row=r_num, column=3).font = value_font
|
| 670 |
-
ws.cell(row=r_num, column=3).alignment = align_center
|
| 671 |
-
ws.cell(row=r_num, column=3).border = thin_border
|
| 672 |
-
|
| 673 |
-
style_range(ws, f"D{r_num}:E{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 674 |
-
ws.cell(row=r_num, column=6).font = value_font
|
| 675 |
-
ws.cell(row=r_num, column=6).alignment = align_center
|
| 676 |
-
ws.cell(row=r_num, column=6).border = thin_border
|
| 677 |
-
|
| 678 |
-
style_range(ws, f"G{r_num}:H{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 679 |
-
ws.cell(row=r_num, column=9).font = value_font
|
| 680 |
-
ws.cell(row=r_num, column=9).alignment = align_center
|
| 681 |
-
ws.cell(row=r_num, column=9).border = thin_border
|
| 682 |
-
|
| 683 |
-
r_num += 1
|
| 684 |
-
|
| 685 |
-
ws.row_dimensions[r_num].height = 24
|
| 686 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 687 |
-
ws.cell(row=r_num, column=1, value="대피경로 확인")
|
| 688 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
|
| 689 |
-
ws.cell(row=r_num, column=3, value=f"{info['route_checked']}")
|
| 690 |
-
|
| 691 |
-
ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=6)
|
| 692 |
-
ws.cell(row=r_num, column=5, value="비상연락망 확인")
|
| 693 |
-
ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=9)
|
| 694 |
-
ws.cell(row=r_num, column=7, value=f"{info['contact_checked']}")
|
| 695 |
-
|
| 696 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 697 |
-
style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 698 |
-
style_range(ws, f"E{r_num}:F{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 699 |
-
style_range(ws, f"G{r_num}:I{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 700 |
-
|
| 701 |
-
return r_num
|
| 702 |
-
|
| 703 |
-
# ---------------------------------------------------------
|
| 704 |
-
# Audit Style Meeting Opinion & 2nd Result Writers
|
| 705 |
-
# ---------------------------------------------------------
|
| 706 |
-
def write_role_meeting_opinion(ws, r_num, role, detail1, detail2, role_desc):
|
| 707 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+2, end_column=2)
|
| 708 |
-
ws.cell(row=r_num, column=1, value=role)
|
| 709 |
-
|
| 710 |
-
# Row 1 (Detail 1)
|
| 711 |
-
ws.row_dimensions[r_num].height = 28
|
| 712 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
|
| 713 |
-
ws.cell(row=r_num, column=3, value=detail1)
|
| 714 |
-
|
| 715 |
-
# Row 2 (Detail 2)
|
| 716 |
-
ws.row_dimensions[r_num+1].height = 28
|
| 717 |
-
ws.merge_cells(start_row=r_num+1, start_column=3, end_row=r_num+1, end_column=9)
|
| 718 |
-
ws.cell(row=r_num+1, column=3, value=detail2)
|
| 719 |
-
|
| 720 |
-
# Row 3 (Role Desc)
|
| 721 |
-
ws.row_dimensions[r_num+2].height = 28
|
| 722 |
-
ws.merge_cells(start_row=r_num+2, start_column=3, end_row=r_num+2, end_column=9)
|
| 723 |
-
ws.cell(row=r_num+2, column=3, value=role_desc)
|
| 724 |
-
|
| 725 |
-
style_range(ws, f"A{r_num}:B{r_num+2}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 726 |
-
style_range(ws, f"C{r_num}:I{r_num}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 727 |
-
style_range(ws, f"C{r_num+1}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 728 |
-
style_range(ws, f"C{r_num+2}:I{r_num+2}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 729 |
-
|
| 730 |
-
return r_num + 2
|
| 731 |
-
|
| 732 |
-
def write_secondary_result_row(ws, r_num, label, content):
|
| 733 |
-
ws.row_dimensions[r_num].height = 28
|
| 734 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 735 |
-
ws.cell(row=r_num, column=1, value=label)
|
| 736 |
-
|
| 737 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
|
| 738 |
-
ws.cell(row=r_num, column=3, value=content)
|
| 739 |
-
|
| 740 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 741 |
-
style_range(ws, f"C{r_num}:I{r_num}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 742 |
-
|
| 743 |
-
return r_num
|
| 744 |
-
|
| 745 |
-
def write_guardian_feedback(ws, r_num, info):
|
| 746 |
-
# Row 1: meta (Date, Reflected)
|
| 747 |
-
ws.row_dimensions[r_num].height = 24
|
| 748 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 749 |
-
ws.cell(row=r_num, column=1, value="통화일자")
|
| 750 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
|
| 751 |
-
ws.cell(row=r_num, column=3, value=f"{info['guardian_call_date']}")
|
| 752 |
-
|
| 753 |
-
ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=6)
|
| 754 |
-
ws.cell(row=r_num, column=5, value="반영 여부")
|
| 755 |
-
ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=9)
|
| 756 |
-
ws.cell(row=r_num, column=7, value=f"{info['guardian_reflected']}")
|
| 757 |
-
|
| 758 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 759 |
-
style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 760 |
-
style_range(ws, f"E{r_num}:F{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 761 |
-
style_range(ws, f"G{r_num}:I{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 762 |
-
|
| 763 |
-
r_num += 1
|
| 764 |
-
|
| 765 |
-
# Row 2 & 3: Guardian Opinion
|
| 766 |
-
ws.row_dimensions[r_num].height = 28
|
| 767 |
-
ws.row_dimensions[r_num+1].height = 28
|
| 768 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+1, end_column=2)
|
| 769 |
-
ws.cell(row=r_num, column=1, value="보호자 의견")
|
| 770 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num+1, end_column=9)
|
| 771 |
-
ws.cell(row=r_num, column=3, value=f"{info['guardian_opinion']}")
|
| 772 |
-
|
| 773 |
-
style_range(ws, f"A{r_num}:B{r_num+1}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 774 |
-
style_range(ws, f"C{r_num}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 775 |
-
|
| 776 |
-
r_num += 2
|
| 777 |
-
|
| 778 |
-
# Row 4 & 5: Facility Explanation
|
| 779 |
-
ws.row_dimensions[r_num].height = 28
|
| 780 |
-
ws.row_dimensions[r_num+1].height = 28
|
| 781 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+1, end_column=2)
|
| 782 |
-
ws.cell(row=r_num, column=1, value="기관 설명내용")
|
| 783 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num+1, end_column=9)
|
| 784 |
-
ws.cell(row=r_num, column=3, value=f"{info['guardian_response']}")
|
| 785 |
-
|
| 786 |
-
style_range(ws, f"A{r_num}:B{r_num+1}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 787 |
-
style_range(ws, f"C{r_num}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
|
| 788 |
-
|
| 789 |
-
return r_num + 1
|
| 790 |
-
|
| 791 |
-
def write_photo_section(ws, r_num):
|
| 792 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+5, end_column=9)
|
| 793 |
-
for i in range(6):
|
| 794 |
-
ws.row_dimensions[r_num+i].height = 25
|
| 795 |
-
|
| 796 |
-
photo_cell = ws.cell(row=r_num, column=1)
|
| 797 |
-
photo_cell.value = (
|
| 798 |
-
"[훈련 사진 첨부란]\n\n"
|
| 799 |
-
"※ 여기에 실제 대피훈련, 소화기 시연, 응급모의대응 등 훈련 사진을 부착하십시오.\n"
|
| 800 |
-
"(장기요양기관 평가 대응을 위해 2장 이상의 사진 부착이 필수적입니다.)"
|
| 801 |
-
)
|
| 802 |
-
style_range(ws, f"A{r_num}:I{r_num+5}",
|
| 803 |
-
font=Font(name="맑은 고딕", size=9.5, italic=True, color="667085"),
|
| 804 |
-
alignment=Alignment(vertical="center", horizontal="center", wrap_text=True),
|
| 805 |
-
border=thin_border)
|
| 806 |
-
|
| 807 |
-
return r_num + 5
|
| 808 |
-
|
| 809 |
-
# ---------------------------------------------------------
|
| 810 |
-
# Excel Creator: Write Checklist Sheet
|
| 811 |
-
# ---------------------------------------------------------
|
| 812 |
-
def write_checklist_tab(ws, info):
|
| 813 |
-
col_widths = {
|
| 814 |
-
'A': 10, 'B': 10, 'C': 12, 'D': 12, 'E': 12, 'F': 12, 'G': 12, 'H': 14, 'I': 14
|
| 815 |
-
}
|
| 816 |
-
for col, width in col_widths.items():
|
| 817 |
-
ws.column_dimensions[col].width = width
|
| 818 |
-
|
| 819 |
-
setup_page_print(ws)
|
| 820 |
-
|
| 821 |
-
# Title
|
| 822 |
-
ws.row_dimensions[1].height = 22
|
| 823 |
-
ws.row_dimensions[2].height = 22
|
| 824 |
-
ws.merge_cells("A1:I2")
|
| 825 |
-
title_cell = ws.cell(row=1, column=1)
|
| 826 |
-
title_cell.value = f"{info['training_type']} 체크리스트"
|
| 827 |
-
style_range(ws, "A1:I2",
|
| 828 |
-
font=Font(name="맑은 고딕", size=14, bold=True, color="111827"),
|
| 829 |
-
alignment=align_center,
|
| 830 |
-
border=thin_border)
|
| 831 |
-
|
| 832 |
-
# Info Banner (Row 3)
|
| 833 |
-
ws.row_dimensions[3].height = 30
|
| 834 |
-
ws.merge_cells("A3:I3")
|
| 835 |
-
banner_cell = ws.cell(row=3, column=1)
|
| 836 |
-
banner_cell.value = "※ 본 체크리스트는 국민건강보험공단 장기요양기관 평가 고시 기준 및 매뉴얼에 의거하여 작성되었습니다. 각 항목의 반영 여부를 확인하십시오."
|
| 837 |
-
style_range(ws, "A3:I3",
|
| 838 |
-
fill=PatternFill(start_color="F9FAFB", end_color="F9FAFB", fill_type="solid"),
|
| 839 |
-
font=Font(name="맑은 고딕", size=9, italic=True, color="475467"),
|
| 840 |
-
alignment=align_center,
|
| 841 |
-
border=thin_border)
|
| 842 |
-
|
| 843 |
-
# Headers (Row 4)
|
| 844 |
-
ws.row_dimensions[4].height = 26
|
| 845 |
-
ws.merge_cells("A4:B4")
|
| 846 |
-
ws.cell(row=4, column=1, value="평가 항목")
|
| 847 |
-
ws.merge_cells("C4:E4")
|
| 848 |
-
ws.cell(row=4, column=3, value="세부 확인사항 및 기준")
|
| 849 |
-
ws.merge_cells("F4:G4")
|
| 850 |
-
ws.cell(row=4, column=6, value="반영 상태")
|
| 851 |
-
ws.merge_cells("H4:I4")
|
| 852 |
-
ws.cell(row=4, column=8, value="평가 기준 충족 여부")
|
| 853 |
-
|
| 854 |
-
style_range(ws, "A4:I4", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 855 |
-
|
| 856 |
-
checklist_items = [
|
| 857 |
-
("훈련계획서 작성 여부", "훈련을 실시하기 전 구체적인 계획서가 수립되어 있는가?", "작성 완료", "충족 (Pass)"),
|
| 858 |
-
("결과보고서 작성 여부", "훈련 실시 후 결과보고서가 작성 및 보관되어 있는가?", "작성 완료", "충족 (Pass)"),
|
| 859 |
-
("훈련일자 기록", "계획서 및 결과보고서에 실제 훈련일자가 정확히 기록되었는가?", f"{info['training_date']} 기록됨", "충족 (Pass)"),
|
| 860 |
-
("훈련시간 기록", "훈련 시작 및 종료 시간(예: 1시간 이상)이 상세히 기록되었는가?", f"{info['training_time']} 기록됨", "충족 (Pass)"),
|
| 861 |
-
("훈련장소 기록", "훈련이 실시된 구체적인 장소가 명시되어 있는가?", f"{info['training_location']} 기록됨", "충족 (Pass)"),
|
| 862 |
-
("참여자 명단", "훈련에 참여한 수급자와 종사자의 명단이 첨부 또는 기록되었는가?", f"총 {info['total_participants']}명 명단 확인", "충족 (Pass)"),
|
| 863 |
-
("직종별 역할분담", "시설장, 복지사, 요양보호사 등 다직종 역할 분담이 계획서에 수립되었는가?", "5개 직종 역할 수립", "충족 (Pass)"),
|
| 864 |
-
("대피취약자 지원계획", "휠체어, 보행보조 필요 등 대피취약자 개별 이동지원 계획이 수립되었는가?", f"대피취약자 {info['vulnerable_count']}명 지원 계획 반영", "충족 (Pass)"),
|
| 865 |
-
("비상연락체계", "소방서, 경찰서, 협력병원 등 유관기관 비상연락체계가 구성되었는가?", f"{info['contact_checked']}", "충족 (Pass)"),
|
| 866 |
-
("대피경로", "피난동선 및 비상 대피경로가 지정되고 사전에 확인되었는가?", f"{info['route_checked']}", "충족 (Pass)"),
|
| 867 |
-
("소화기 사용 또는 화재대응", "훈련 내용 중 소화설비 시연 또는 화재 대피 실무가 포함되었는가?", f"{info['fire_ext_demo']}", "충족 (Pass)"),
|
| 868 |
-
("실제 대피훈련 실시 여부", "모의 상황에 따라 전원이 실제로 야외 대피 동선으로 대피를 실시했는가?", "실제 대피훈련 실시", "충족 (Pass)"),
|
| 869 |
-
("사진 첨부란", "훈련 장면��� 증명할 수 있는 사진첩 또는 부착 공간이 마련되었는가?", "사진 첨부 공간 확보", "충족 (Pass)"),
|
| 870 |
-
("개선조치", "훈련 과정에서 노출된 문제점에 대해 명확한 개선조치가 수립되었는가?", "데이터 기반 개선조치 수립", "충족 (Pass)"),
|
| 871 |
-
("다음 훈련 반영사항", "금번 훈련 평가 결과가 다음번 훈련 계획에 구체적으로 반영되었는가?", "다음 훈련 반영사항 수립", "충족 (Pass)"),
|
| 872 |
-
("보호자 전화 의견 반영란", "훈련 결과에 대해 보호자의 유선 피드백을 기록하고 서비스계획에 반영하였는가?", "보호자 의견 수렴 완료", "충족 (Pass)")
|
| 873 |
-
]
|
| 874 |
-
|
| 875 |
-
r_num = 5
|
| 876 |
-
for item, desc, status, compliance in checklist_items:
|
| 877 |
-
ws.row_dimensions[r_num].height = 24
|
| 878 |
-
ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
|
| 879 |
-
ws.cell(row=r_num, column=1, value=item)
|
| 880 |
-
ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=5)
|
| 881 |
-
ws.cell(row=r_num, column=3, value=desc)
|
| 882 |
-
ws.merge_cells(start_row=r_num, start_column=6, end_row=r_num, end_column=7)
|
| 883 |
-
ws.cell(row=r_num, column=6, value=status)
|
| 884 |
-
ws.merge_cells(start_row=r_num, start_column=8, end_row=r_num, end_column=9)
|
| 885 |
-
ws.cell(row=r_num, column=8, value=compliance)
|
| 886 |
-
|
| 887 |
-
style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
|
| 888 |
-
style_range(ws, f"C{r_num}:E{r_num}", font=value_font, alignment=align_left, border=thin_border)
|
| 889 |
-
style_range(ws, f"F{r_num}:G{r_num}", font=value_font, alignment=align_center, border=thin_border)
|
| 890 |
-
|
| 891 |
-
pass_fill = PatternFill(start_color="E8F8F5", end_color="E8F8F5", fill_type="solid")
|
| 892 |
-
pass_font = Font(name="맑은 고딕", size=9.5, bold=True, color="0E6251")
|
| 893 |
-
style_range(ws, f"H{r_num}:I{r_num}", fill=pass_fill, font=pass_font, alignment=align_center, border=thin_border)
|
| 894 |
-
|
| 895 |
-
r_num += 1
|
| 896 |
-
|
| 897 |
-
# ---------------------------------------------------------
|
| 898 |
-
# Main Excel Workbook Generator
|
| 899 |
-
# ---------------------------------------------------------
|
| 900 |
-
def create_excel(info, training_type):
|
| 901 |
-
data = compile_document_data(info, training_type)
|
| 902 |
-
wb = openpyxl.Workbook()
|
| 903 |
-
|
| 904 |
-
# ---------------------------------------------------------
|
| 905 |
-
# TAB 1: 훈련 계획서 (Plan)
|
| 906 |
-
# ---------------------------------------------------------
|
| 907 |
-
ws_plan = wb.active
|
| 908 |
-
ws_plan.title = "훈련 계획서"
|
| 909 |
-
|
| 910 |
-
col_widths = {
|
| 911 |
-
'A': 10, 'B': 10, 'C': 12, 'D': 12, 'E': 12, 'F': 12, 'G': 12, 'H': 14, 'I': 14
|
| 912 |
-
}
|
| 913 |
-
for col, width in col_widths.items():
|
| 914 |
-
ws_plan.column_dimensions[col].width = width
|
| 915 |
-
|
| 916 |
-
setup_page_print(ws_plan)
|
| 917 |
-
write_title_and_approval(ws_plan, f"{training_type} 계획서", data)
|
| 918 |
-
|
| 919 |
-
curr_row = 3
|
| 920 |
-
write_general_info_row(ws_plan, curr_row, "기관명", data['facility_name'], "훈련일자", data['training_date'])
|
| 921 |
-
curr_row += 1
|
| 922 |
-
write_general_info_row(ws_plan, curr_row, "훈련시간", data['training_time'], "훈련장소", data['training_location'])
|
| 923 |
-
curr_row += 1
|
| 924 |
-
write_general_info_row(ws_plan, curr_row, "훈련유형", f"{training_type} (대피실무 포함)", "훈련주관자", data['host'])
|
| 925 |
-
curr_row += 1
|
| 926 |
-
|
| 927 |
-
write_section_header(ws_plan, curr_row, "1. 훈련 참여자 계획 현황")
|
| 928 |
-
curr_row += 1
|
| 929 |
-
curr_row = write_plan_participants(ws_plan, curr_row, data)
|
| 930 |
-
|
| 931 |
-
curr_row += 1
|
| 932 |
-
write_plan_details(ws_plan, curr_row, data)
|
| 933 |
-
|
| 934 |
-
# ---------------------------------------------------------
|
| 935 |
-
# TAB 2: 훈련 결과보고서 (Result Report)
|
| 936 |
-
# ---------------------------------------------------------
|
| 937 |
-
ws_report = wb.create_sheet(title="훈련 결과보고서")
|
| 938 |
-
for col, width in col_widths.items():
|
| 939 |
-
ws_report.column_dimensions[col].width = width
|
| 940 |
-
|
| 941 |
-
setup_page_print(ws_report)
|
| 942 |
-
write_title_and_approval(ws_report, f"{training_type} 결과보고서 & 감사 의견서", data)
|
| 943 |
-
|
| 944 |
-
curr_row = 3
|
| 945 |
-
write_general_info_row(ws_report, curr_row, "기관명", data['facility_name'], "훈련일자", data['training_date'])
|
| 946 |
-
curr_row += 1
|
| 947 |
-
write_general_info_row(ws_report, curr_row, "훈련시간", data['training_time'], "훈련장소", data['training_location'])
|
| 948 |
-
curr_row += 1
|
| 949 |
-
write_general_info_row(ws_report, curr_row, "훈련유형", f"{training_type} (실제 훈련완료)", "훈련주관자", data['host'])
|
| 950 |
-
curr_row += 1
|
| 951 |
-
|
| 952 |
-
# 1. 훈련 실제 참여 및 데이터 결과
|
| 953 |
-
write_section_header(ws_report, curr_row, "1. 훈련 실제 참여 및 데이터 결과")
|
| 954 |
-
curr_row += 1
|
| 955 |
-
curr_row = write_report_participants(ws_report, curr_row, data)
|
| 956 |
-
|
| 957 |
-
curr_row += 1
|
| 958 |
-
|
| 959 |
-
# 2. 다직종별 훈련 평가 및 회의 의견
|
| 960 |
-
write_section_header(ws_report, curr_row, "2. 다직종별 훈련 평가 및 회의 의견")
|
| 961 |
-
curr_row += 1
|
| 962 |
-
|
| 963 |
-
curr_row = write_role_meeting_opinion(
|
| 964 |
-
ws_report, curr_row, "시설장",
|
| 965 |
-
f"- 평가 및 운영상 필요한 조치: {data['opinion_시설장_1']}",
|
| 966 |
-
f"- 기관 차원의 지원: {data['opinion_시설장_2']}",
|
| 967 |
-
f"- 역할: {data['opinion_시설장_role']}"
|
| 968 |
-
)
|
| 969 |
-
curr_row += 1
|
| 970 |
-
curr_row = write_role_meeting_opinion(
|
| 971 |
-
ws_report, curr_row, "사회복지사",
|
| 972 |
-
f"- 사례관리 관점: {data['opinion_사회복지사_1']}",
|
| 973 |
-
f"- 서비스계획 반영: {data['opinion_사회복지사_2']}",
|
| 974 |
-
f"- 역할: {data['opinion_사회복지사_role']}"
|
| 975 |
-
)
|
| 976 |
-
curr_row += 1
|
| 977 |
-
curr_row = write_role_meeting_opinion(
|
| 978 |
-
ws_report, curr_row, "요양보호사",
|
| 979 |
-
f"- 일상생활 관찰: {data['opinion_요양보호사_1']}",
|
| 980 |
-
f"- 돌봄 제공 시 유의사항: {data['opinion_요양보호사_2']}",
|
| 981 |
-
f"- 역할: {data['opinion_요양보호사_role']}"
|
| 982 |
-
)
|
| 983 |
-
curr_row += 1
|
| 984 |
-
curr_row = write_role_meeting_opinion(
|
| 985 |
-
ws_report, curr_row, "간호조무사",
|
| 986 |
-
f"- 건강상태 확인: {data['opinion_간호조무사_1']}",
|
| 987 |
-
f"- 복약·피부·감염관리: {data['opinion_간호조무사_2']}",
|
| 988 |
-
f"- 역할: {data['opinion_간호조무사_role']}"
|
| 989 |
-
)
|
| 990 |
-
curr_row += 1
|
| 991 |
-
curr_row = write_role_meeting_opinion(
|
| 992 |
-
ws_report, curr_row, "물리치료사",
|
| 993 |
-
f"- 신체기능 평가: {data['opinion_물리치료사_1']}",
|
| 994 |
-
f"- 낙상예방 및 운동계획: {data['opinion_물리치료사_2']}",
|
| 995 |
-
f"- 역할: {data['opinion_물리치료사_role']}"
|
| 996 |
-
)
|
| 997 |
-
|
| 998 |
-
curr_row += 1
|
| 999 |
-
|
| 1000 |
-
# 3. 훈련 결과 분석 및 2차 반영 (2차 결과 반영)
|
| 1001 |
-
write_section_header(ws_report, curr_row, "3. 훈련 결과 분석 및 2차 반영 (2차 결과 반영)")
|
| 1002 |
-
curr_row += 1
|
| 1003 |
-
|
| 1004 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "1차 문제상태", data['result_problem'])
|
| 1005 |
-
curr_row += 1
|
| 1006 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "적용한 조치", data['result_action'])
|
| 1007 |
-
curr_row += 1
|
| 1008 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "직종별 수행내용", data['result_role_actions'])
|
| 1009 |
-
curr_row += 1
|
| 1010 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "변화된 결과", data['result_outcome'])
|
| 1011 |
-
curr_row += 1
|
| 1012 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "구체적 데이터", data['result_data'])
|
| 1013 |
-
curr_row += 1
|
| 1014 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "남은 위험요인", data['result_risks'])
|
| 1015 |
-
curr_row += 1
|
| 1016 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "향후 계획", data['result_next'])
|
| 1017 |
-
curr_row += 1
|
| 1018 |
-
curr_row = write_secondary_result_row(ws_report, curr_row, "보호자 의견 반영내용", data['result_guardian'])
|
| 1019 |
-
|
| 1020 |
-
curr_row += 1
|
| 1021 |
-
|
| 1022 |
-
# 4. 보호자 전화 의견 반영
|
| 1023 |
-
write_section_header(ws_report, curr_row, "4. 보호자 전화 의견 반영")
|
| 1024 |
-
curr_row += 1
|
| 1025 |
-
curr_row = write_guardian_feedback(ws_report, curr_row, data)
|
| 1026 |
-
|
| 1027 |
-
curr_row += 1
|
| 1028 |
-
|
| 1029 |
-
# 5. 훈련 사진 첨부란
|
| 1030 |
-
write_section_header(ws_report, curr_row, "5. 훈련 사진 첨부란")
|
| 1031 |
-
curr_row += 1
|
| 1032 |
-
curr_row = write_photo_section(ws_report, curr_row)
|
| 1033 |
-
|
| 1034 |
-
# ---------------------------------------------------------
|
| 1035 |
-
# TAB 3: 평가누락 체크리스트 (Checklist)
|
| 1036 |
-
# ---------------------------------------------------------
|
| 1037 |
-
ws_checklist = wb.create_sheet(title="평가누락 체크리스트")
|
| 1038 |
-
write_checklist_tab(ws_checklist, data)
|
| 1039 |
-
|
| 1040 |
-
excel_stream = io.BytesIO()
|
| 1041 |
-
wb.save(excel_stream)
|
| 1042 |
-
excel_stream.seek(0)
|
| 1043 |
-
return excel_stream
|
| 1044 |
-
|
| 1045 |
-
# ---------------------------------------------------------
|
| 1046 |
-
# Streamlit Application User Interface
|
| 1047 |
-
# ---------------------------------------------------------
|
| 1048 |
-
def main():
|
| 1049 |
-
st.markdown('<div class="title-card"><h1>🚒 재난훈련기록지 자동생성기</h1><p>다직종 감사 에이전트의 지침을 100% 반영하여, 실제 현장 및 평가 대응이 가능한 수준의 훈련 문서를 설계합니다.</p></div>', unsafe_allow_html=True)
|
| 1050 |
-
|
| 1051 |
-
col_left, col_right = st.columns([1, 2.2])
|
| 1052 |
-
|
| 1053 |
-
with col_left:
|
| 1054 |
-
st.markdown('<div class="section-header">🏢 기관 기본 정보</div>', unsafe_allow_html=True)
|
| 1055 |
-
facility_name = st.text_input("기관명", value="행복요양원")
|
| 1056 |
-
facility_head = st.text_input("시설장명", value="홍길동")
|
| 1057 |
-
writer = st.text_input("담당자 (작성자) 명", value="김복지")
|
| 1058 |
-
training_date = st.date_input("훈련/교육 일자", value=datetime.today())
|
| 1059 |
-
training_time = st.text_input("훈련/교육 시간", value="14:00 ~ 15:00")
|
| 1060 |
-
training_location = st.text_input("훈련/교육 장소", value="요양원 전 층 및 야외 대피광장")
|
| 1061 |
-
|
| 1062 |
-
st.markdown('<div class="section-header">⚙️ 훈련 및 교육 설정</div>', unsafe_allow_html=True)
|
| 1063 |
-
training_type = st.selectbox("훈련/교육 유형", ["소방��련", "재난훈련", "안전교육"])
|
| 1064 |
-
host = st.text_input("훈련 주관자", value=f"시설장 {facility_head}")
|
| 1065 |
-
|
| 1066 |
-
with col_right:
|
| 1067 |
-
tab_inputs, tab_guardian, tab_preview = st.tabs(["📊 상세 정량적 데이터", "📞 보호자 의견 반영", "👁️ 전문성강화 감사 문장 실시간 프리뷰"])
|
| 1068 |
-
|
| 1069 |
-
with tab_inputs:
|
| 1070 |
-
st.markdown("##### 훈련 참여 정량 데이터")
|
| 1071 |
-
col1, col2, col3 = st.columns(3)
|
| 1072 |
-
with col1:
|
| 1073 |
-
staff_participants = st.number_input("종사자 참여자 수 (명)", value=8, min_value=1)
|
| 1074 |
-
vulnerable_count = st.number_input("대피취약자 수 (명)", value=5, min_value=0)
|
| 1075 |
-
with col2:
|
| 1076 |
-
recipient_participants = st.number_input("수급자 참여자 수 (명)", value=12, min_value=1)
|
| 1077 |
-
wheelchair_count = st.number_input("휠체어 사용 대상자 수 (명)", value=3, min_value=0)
|
| 1078 |
-
with col3:
|
| 1079 |
-
total_participants = staff_participants + recipient_participants
|
| 1080 |
-
st.metric("총 참여 인원 (종사자 + 수급자)", f"{total_participants} 명")
|
| 1081 |
-
walk_aid_count = st.number_input("보행보조 필요자 수 (명)", value=4, min_value=0)
|
| 1082 |
-
|
| 1083 |
-
st.markdown("##### 훈련 관련 확인/점검 지표")
|
| 1084 |
-
col4, col5 = st.columns(2)
|
| 1085 |
-
with col4:
|
| 1086 |
-
evac_time = st.text_input("대피완료 시간", value="4분 30초")
|
| 1087 |
-
route_checked = st.selectbox("대피경로 확인 여부", ["확인완료", "미확인"])
|
| 1088 |
-
contact_checked = st.selectbox("비상연락망 작동 여부", ["작동확인", "미확인"])
|
| 1089 |
-
with col5:
|
| 1090 |
-
emergency_mock = st.selectbox("응급상황 모의대응 여부", ["실시 및 양호", "미실시"])
|
| 1091 |
-
fire_ext_demo = st.selectbox("소화기 사용 시연 여부", ["실시 및 양호", "미실시"])
|
| 1092 |
-
|
| 1093 |
-
with tab_guardian:
|
| 1094 |
-
st.markdown("##### 훈련 후 보호자 유선 피드백 수렴 (사후 의견 반영)")
|
| 1095 |
-
col_date, col_reflected = st.columns(2)
|
| 1096 |
-
with col_date:
|
| 1097 |
-
guardian_call_date = st.date_input("보호자 유선 연락 일자", value=datetime.today())
|
| 1098 |
-
with col_reflected:
|
| 1099 |
-
guardian_reflected = st.selectbox("안전관리/서비스계획 반영 여부", [
|
| 1100 |
-
"반영완료 (서비스계획 및 안전관리계획 반영)",
|
| 1101 |
-
"반영완료 (보호자 안내 및 연락망 최신화)",
|
| 1102 |
-
"미반영"
|
| 1103 |
-
])
|
| 1104 |
-
|
| 1105 |
-
guardian_opinion = st.text_area(
|
| 1106 |
-
"보호자 통화 의견",
|
| 1107 |
-
value="훈련 시 거동이 불편한 어르신들이 낙상하지 않도록 특별히 신경 써 주셨으면 좋겠고, 비상시 연락이 잘 닿을 수 있게 비상 연락망 관리를 철저히 해달라고 요청하셨습니다."
|
| 1108 |
-
)
|
| 1109 |
-
guardian_response = st.text_area(
|
| 1110 |
-
"기관 설명 및 대책 내용",
|
| 1111 |
-
value="훈련 전 대피취약자로 분류하여 요양보호사를 1:1 매칭하여 이동을 지원할 계획임을 설명하였으며, 보호자 비상연락처 최신화를 마쳤고 훈련 직후 결과를 안내할 예정임을 설명함."
|
| 1112 |
-
)
|
| 1113 |
-
|
| 1114 |
-
with tab_preview:
|
| 1115 |
-
preview_info = {
|
| 1116 |
-
'facility_name': facility_name,
|
| 1117 |
-
'total_participants': total_participants,
|
| 1118 |
-
'staff_participants': staff_participants,
|
| 1119 |
-
'recipient_participants': recipient_participants,
|
| 1120 |
-
'evac_time': evac_time,
|
| 1121 |
-
'vulnerable_count': vulnerable_count,
|
| 1122 |
-
'wheelchair_count': wheelchair_count,
|
| 1123 |
-
'walk_aid_count': walk_aid_count,
|
| 1124 |
-
'emergency_mock': emergency_mock,
|
| 1125 |
-
'fire_ext_demo': fire_ext_demo,
|
| 1126 |
-
'route_checked': route_checked,
|
| 1127 |
-
'contact_checked': contact_checked,
|
| 1128 |
-
'guardian_call_date': str(guardian_call_date),
|
| 1129 |
-
'guardian_reflected': guardian_reflected
|
| 1130 |
-
}
|
| 1131 |
-
|
| 1132 |
-
st.markdown(f"### 📝 {training_type} 다직종 감사 의견서 및 2차 결과 프리뷰")
|
| 1133 |
-
|
| 1134 |
-
# Meeting opinions review
|
| 1135 |
-
st.markdown("#### [다직종 회의 의견]")
|
| 1136 |
-
roles_list = ['시설장', '사회복지사', '요양보호사', '간호조무사', '물리치료사']
|
| 1137 |
-
for r in roles_list:
|
| 1138 |
-
st.markdown(f"**🔹 {r} 의견**")
|
| 1139 |
-
if r == '시설장':
|
| 1140 |
-
st.write(f"- 평가 및 운영상 필요한 조치: {generate_narrative_text(preview_info, 'opinion_시설장_1', training_type)}")
|
| 1141 |
-
st.write(f"- 기관 차원의 지원: {generate_narrative_text(preview_info, 'opinion_시설장_2', training_type)}")
|
| 1142 |
-
elif r == '사회복지사':
|
| 1143 |
-
st.write(f"- 사례관리 관점: {generate_narrative_text(preview_info, 'opinion_사회복지사_1', training_type)}")
|
| 1144 |
-
st.write(f"- 서비스계획 반영: {generate_narrative_text(preview_info, 'opinion_사회복지사_2', training_type)}")
|
| 1145 |
-
elif r == '요양보호사':
|
| 1146 |
-
st.write(f"- 일상생활 관찰: {generate_narrative_text(preview_info, 'opinion_요양보호사_1', training_type)}")
|
| 1147 |
-
st.write(f"- 돌봄 제공 시 유의사항: {generate_narrative_text(preview_info, 'opinion_요양보호사_2', training_type)}")
|
| 1148 |
-
elif r == '간호조무사':
|
| 1149 |
-
st.write(f"- 건강상태 확인: {generate_narrative_text(preview_info, 'opinion_간호조무사_1', training_type)}")
|
| 1150 |
-
st.write(f"- 복약·피부·감염관리: {generate_narrative_text(preview_info, 'opinion_간호조무사_2', training_type)}")
|
| 1151 |
-
elif r == '물리치료사':
|
| 1152 |
-
st.write(f"- 신체기능 평가: {generate_narrative_text(preview_info, 'opinion_물리치료사_1', training_type)}")
|
| 1153 |
-
st.write(f"- 낙상예방 및 운동계획: {generate_narrative_text(preview_info, 'opinion_물리치료사_2', training_type)}")
|
| 1154 |
-
st.write(f"- 역할: {generate_narrative_text(preview_info, f'opinion_{r}_role', training_type)}")
|
| 1155 |
-
|
| 1156 |
-
# Secondary results review
|
| 1157 |
-
st.markdown("#### [2차 결과 반영]")
|
| 1158 |
-
st.write(f"**1차 문제상태**: {generate_narrative_text(preview_info, 'result_problem', training_type)}")
|
| 1159 |
-
st.write(f"**적용한 조치**: {generate_narrative_text(preview_info, 'result_action', training_type)}")
|
| 1160 |
-
st.write(f"**직종별 수행내용**: {generate_narrative_text(preview_info, 'result_role_actions', training_type)}")
|
| 1161 |
-
st.write(f"**변화된 결과**: {generate_narrative_text(preview_info, 'result_outcome', training_type)}")
|
| 1162 |
-
st.write(f"**구체적 데이터**: {generate_narrative_text(preview_info, 'result_data', training_type)}")
|
| 1163 |
-
st.write(f"**남은 위험요인**: {generate_narrative_text(preview_info, 'result_risks', training_type)}")
|
| 1164 |
-
st.write(f"**향후 계획**: {generate_narrative_text(preview_info, 'result_next', training_type)}")
|
| 1165 |
-
st.write(f"**보호자 의견 반영내용**: {generate_narrative_text(preview_info, 'result_guardian', training_type)}")
|
| 1166 |
-
|
| 1167 |
-
st.markdown('<div style="margin-top: 2rem;"></div>', unsafe_allow_html=True)
|
| 1168 |
-
|
| 1169 |
-
info_payload = {
|
| 1170 |
-
'facility_name': facility_name,
|
| 1171 |
-
'facility_head': facility_head,
|
| 1172 |
-
'writer': writer,
|
| 1173 |
-
'training_date': training_date,
|
| 1174 |
-
'training_time': training_time,
|
| 1175 |
-
'training_location': training_location,
|
| 1176 |
-
'host': host,
|
| 1177 |
-
'total_participants': total_participants,
|
| 1178 |
-
'staff_participants': staff_participants,
|
| 1179 |
-
'recipient_participants': recipient_participants,
|
| 1180 |
-
'evac_time': evac_time,
|
| 1181 |
-
'vulnerable_count': vulnerable_count,
|
| 1182 |
-
'wheelchair_count': wheelchair_count,
|
| 1183 |
-
'walk_aid_count': walk_aid_count,
|
| 1184 |
-
'emergency_mock': emergency_mock,
|
| 1185 |
-
'fire_ext_demo': fire_ext_demo,
|
| 1186 |
-
'route_checked': route_checked,
|
| 1187 |
-
'contact_checked': contact_checked,
|
| 1188 |
-
'guardian_call_date': guardian_call_date,
|
| 1189 |
-
'guardian_opinion': guardian_opinion,
|
| 1190 |
-
'guardian_response': guardian_response,
|
| 1191 |
-
'guardian_reflected': guardian_reflected,
|
| 1192 |
-
}
|
| 1193 |
-
|
| 1194 |
-
excel_file = create_excel(info_payload, training_type)
|
| 1195 |
-
filename = f"{facility_name}_{training_date.strftime('%Y%m%d')}_{training_type}_다직종평가대응_통합문서.xlsx"
|
| 1196 |
-
|
| 1197 |
-
st.download_button(
|
| 1198 |
-
label=f"📥 {training_type} 계획서/결과보고서 엑셀 다운로드 (감사 반영본)",
|
| 1199 |
-
data=excel_file,
|
| 1200 |
-
file_name=filename,
|
| 1201 |
-
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
| 1202 |
-
)
|
| 1203 |
-
|
| 1204 |
-
if __name__ == "__main__":
|
| 1205 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|