devorah-ai-2016 commited on
Commit
27ca2de
·
verified ·
1 Parent(s): 6300b77

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1205
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()