devorah-ai-2016 commited on
Commit
9b60b3e
·
verified ·
1 Parent(s): 050d0c1

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1211
app.py DELETED
@@ -1,1211 +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_spacer(ws, r_num)
530
- r_num += 1
531
-
532
- write_section_header(ws, r_num, "3. 다직종별 역할 분담 계획")
533
- r_num += 1
534
-
535
- ws.row_dimensions[r_num].height = 24
536
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
537
- ws.cell(row=r_num, column=1, value="직종")
538
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
539
- ws.cell(row=r_num, column=3, value="역할 및 주요 업무 분담 내용")
540
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
541
- style_range(ws, f"C{r_num}:I{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
542
- r_num += 1
543
-
544
- roles = [
545
- ("시설장", "훈련 총괄, 지휘, 평가 대응, 유관기관 연락체계 확인"),
546
- ("사회복지사", "수급자 명단 확인, 대피취약자 분류, 보호자 연락, 훈련기록 작성"),
547
- ("요양보호사", "수급자 대피 보조, 이동 지원, 낙상위험자 보호, 실제 현장 대응"),
548
- ("간호조무사", "응급상황 확인, 활력징후 확인, 투약·건강상태 확인, 부상자 응급조치"),
549
- ("물리치료사", "보행불안정 수급자 이동지원, 휠체어·워커 사용 대상자 대피 지원, 낙상예방 확인")
550
- ]
551
- for role, desc in roles:
552
- ws.row_dimensions[r_num].height = 24
553
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
554
- ws.cell(row=r_num, column=1, value=role)
555
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
556
- ws.cell(row=r_num, column=3, value=desc)
557
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
558
- style_range(ws, f"C{r_num}:I{r_num}", font=value_font, alignment=align_left, border=thin_border)
559
- r_num += 1
560
-
561
- write_spacer(ws, r_num)
562
- r_num += 1
563
-
564
- write_section_header(ws, r_num, "4. 비상 연락 및 유관기관 협조 체계")
565
- r_num += 1
566
-
567
- ws.row_dimensions[r_num].height = 24
568
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
569
- ws.cell(row=r_num, column=1, value="유관기관")
570
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
571
- ws.cell(row=r_num, column=3, value="비상연락처")
572
- ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=9)
573
- ws.cell(row=r_num, column=5, value="주요 협조 및 대응 내용")
574
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
575
- style_range(ws, f"C{r_num}:D{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
576
- style_range(ws, f"E{r_num}:I{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
577
- r_num += 1
578
-
579
- contacts = [
580
- ("소방서 (119)", "119 / 관할소방서", f"화재 발생 신고, 인명 구조 및 화재 진압 지원 (소화기 시연: {info['fire_ext_demo']})"),
581
- ("경찰서 (112)", "112 / 관할지구대", "훈련 시 주변 교통 통제 및 안전 유지 협조"),
582
- ("협력 의료기관", "인근 종합병원", f"응급 환자 발생 시 긴급 이송 및 긴급 의료 지원체계 가동 (모의대응: {info['emergency_mock']})")
583
- ]
584
- for org, phone, task in contacts:
585
- ws.row_dimensions[r_num].height = 24
586
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
587
- ws.cell(row=r_num, column=1, value=org)
588
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
589
- ws.cell(row=r_num, column=3, value=phone)
590
- ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=9)
591
- ws.cell(row=r_num, column=5, value=task)
592
-
593
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
594
- style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
595
- style_range(ws, f"E{r_num}:I{r_num}", font=value_font, alignment=align_left, border=thin_border)
596
- r_num += 1
597
-
598
- return r_num
599
-
600
- def write_report_participants(ws, r_num, info):
601
- ws.row_dimensions[r_num].height = 24
602
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
603
- ws.cell(row=r_num, column=1, value="총 참여인원")
604
- ws.cell(row=r_num, column=3, value=f"{info['total_participants']} 명")
605
-
606
- ws.merge_cells(start_row=r_num, start_column=4, end_row=r_num, end_column=5)
607
- ws.cell(row=r_num, column=4, value="종사자 참여자 수")
608
- ws.cell(row=r_num, column=6, value=f"{info['staff_participants']} 명")
609
-
610
- ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=8)
611
- ws.cell(row=r_num, column=7, value="수급자 참여자 수")
612
- ws.cell(row=r_num, column=9, value=f"{info['recipient_participants']} 명")
613
-
614
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
615
- ws.cell(row=r_num, column=3).font = value_font
616
- ws.cell(row=r_num, column=3).alignment = align_center
617
- ws.cell(row=r_num, column=3).border = thin_border
618
-
619
- style_range(ws, f"D{r_num}:E{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
620
- ws.cell(row=r_num, column=6).font = value_font
621
- ws.cell(row=r_num, column=6).alignment = align_center
622
- ws.cell(row=r_num, column=6).border = thin_border
623
-
624
- style_range(ws, f"G{r_num}:H{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
625
- ws.cell(row=r_num, column=9).font = value_font
626
- ws.cell(row=r_num, column=9).alignment = align_center
627
- ws.cell(row=r_num, column=9).border = thin_border
628
-
629
- r_num += 1
630
-
631
- ws.row_dimensions[r_num].height = 24
632
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
633
- ws.cell(row=r_num, column=1, value="대피완료 시간")
634
- ws.cell(row=r_num, column=3, value=f"{info['evac_time']}")
635
-
636
- ws.merge_cells(start_row=r_num, start_column=4, end_row=r_num, end_column=5)
637
- ws.cell(row=r_num, column=4, value="대피취약자 수")
638
- ws.cell(row=r_num, column=6, value=f"{info['vulnerable_count']} 명")
639
-
640
- ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=8)
641
- ws.cell(row=r_num, column=7, value="휠체어 사용 대상자")
642
- ws.cell(row=r_num, column=9, value=f"{info['wheelchair_count']} 명")
643
-
644
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
645
- ws.cell(row=r_num, column=3).font = value_font
646
- ws.cell(row=r_num, column=3).alignment = align_center
647
- ws.cell(row=r_num, column=3).border = thin_border
648
-
649
- style_range(ws, f"D{r_num}:E{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
650
- ws.cell(row=r_num, column=6).font = value_font
651
- ws.cell(row=r_num, column=6).alignment = align_center
652
- ws.cell(row=r_num, column=6).border = thin_border
653
-
654
- style_range(ws, f"G{r_num}:H{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
655
- ws.cell(row=r_num, column=9).font = value_font
656
- ws.cell(row=r_num, column=9).alignment = align_center
657
- ws.cell(row=r_num, column=9).border = thin_border
658
-
659
- r_num += 1
660
-
661
- ws.row_dimensions[r_num].height = 24
662
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
663
- ws.cell(row=r_num, column=1, value="보행보조 필요자")
664
- ws.cell(row=r_num, column=3, value=f"{info['walk_aid_count']} 명")
665
-
666
- ws.merge_cells(start_row=r_num, start_column=4, end_row=r_num, end_column=5)
667
- ws.cell(row=r_num, column=4, value="응급상황 모의대응")
668
- ws.cell(row=r_num, column=6, value=f"{info['emergency_mock']}")
669
-
670
- ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=8)
671
- ws.cell(row=r_num, column=7, value="소화기 사용 시연")
672
- ws.cell(row=r_num, column=9, value=f"{info['fire_ext_demo']}")
673
-
674
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
675
- ws.cell(row=r_num, column=3).font = value_font
676
- ws.cell(row=r_num, column=3).alignment = align_center
677
- ws.cell(row=r_num, column=3).border = thin_border
678
-
679
- style_range(ws, f"D{r_num}:E{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
680
- ws.cell(row=r_num, column=6).font = value_font
681
- ws.cell(row=r_num, column=6).alignment = align_center
682
- ws.cell(row=r_num, column=6).border = thin_border
683
-
684
- style_range(ws, f"G{r_num}:H{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
685
- ws.cell(row=r_num, column=9).font = value_font
686
- ws.cell(row=r_num, column=9).alignment = align_center
687
- ws.cell(row=r_num, column=9).border = thin_border
688
-
689
- r_num += 1
690
-
691
- ws.row_dimensions[r_num].height = 24
692
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
693
- ws.cell(row=r_num, column=1, value="대피경로 확인")
694
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
695
- ws.cell(row=r_num, column=3, value=f"{info['route_checked']}")
696
-
697
- ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=6)
698
- ws.cell(row=r_num, column=5, value="비상연락망 확인")
699
- ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=9)
700
- ws.cell(row=r_num, column=7, value=f"{info['contact_checked']}")
701
-
702
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
703
- style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
704
- style_range(ws, f"E{r_num}:F{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
705
- style_range(ws, f"G{r_num}:I{r_num}", font=value_font, alignment=align_center, border=thin_border)
706
-
707
- return r_num
708
-
709
- # ---------------------------------------------------------
710
- # Audit Style Meeting Opinion & 2nd Result Writers
711
- # ---------------------------------------------------------
712
- def write_role_meeting_opinion(ws, r_num, role, detail1, detail2, role_desc):
713
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+2, end_column=2)
714
- ws.cell(row=r_num, column=1, value=role)
715
-
716
- # Row 1 (Detail 1)
717
- ws.row_dimensions[r_num].height = 28
718
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
719
- ws.cell(row=r_num, column=3, value=detail1)
720
-
721
- # Row 2 (Detail 2)
722
- ws.row_dimensions[r_num+1].height = 28
723
- ws.merge_cells(start_row=r_num+1, start_column=3, end_row=r_num+1, end_column=9)
724
- ws.cell(row=r_num+1, column=3, value=detail2)
725
-
726
- # Row 3 (Role Desc)
727
- ws.row_dimensions[r_num+2].height = 28
728
- ws.merge_cells(start_row=r_num+2, start_column=3, end_row=r_num+2, end_column=9)
729
- ws.cell(row=r_num+2, column=3, value=role_desc)
730
-
731
- style_range(ws, f"A{r_num}:B{r_num+2}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
732
- style_range(ws, f"C{r_num}:I{r_num}", font=value_font, alignment=align_left_wrap, border=thin_border)
733
- style_range(ws, f"C{r_num+1}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
734
- style_range(ws, f"C{r_num+2}:I{r_num+2}", font=value_font, alignment=align_left_wrap, border=thin_border)
735
-
736
- return r_num + 2
737
-
738
- def write_secondary_result_row(ws, r_num, label, content):
739
- ws.row_dimensions[r_num].height = 28
740
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
741
- ws.cell(row=r_num, column=1, value=label)
742
-
743
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=9)
744
- ws.cell(row=r_num, column=3, value=content)
745
-
746
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
747
- style_range(ws, f"C{r_num}:I{r_num}", font=value_font, alignment=align_left_wrap, border=thin_border)
748
-
749
- return r_num
750
-
751
- def write_guardian_feedback(ws, r_num, info):
752
- # Row 1: meta (Date, Reflected)
753
- ws.row_dimensions[r_num].height = 24
754
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
755
- ws.cell(row=r_num, column=1, value="통화일자")
756
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=4)
757
- ws.cell(row=r_num, column=3, value=f"{info['guardian_call_date']}")
758
-
759
- ws.merge_cells(start_row=r_num, start_column=5, end_row=r_num, end_column=6)
760
- ws.cell(row=r_num, column=5, value="반영 여부")
761
- ws.merge_cells(start_row=r_num, start_column=7, end_row=r_num, end_column=9)
762
- ws.cell(row=r_num, column=7, value=f"{info['guardian_reflected']}")
763
-
764
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
765
- style_range(ws, f"C{r_num}:D{r_num}", font=value_font, alignment=align_center, border=thin_border)
766
- style_range(ws, f"E{r_num}:F{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
767
- style_range(ws, f"G{r_num}:I{r_num}", font=value_font, alignment=align_center, border=thin_border)
768
-
769
- r_num += 1
770
-
771
- # Row 2 & 3: Guardian Opinion
772
- ws.row_dimensions[r_num].height = 28
773
- ws.row_dimensions[r_num+1].height = 28
774
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+1, end_column=2)
775
- ws.cell(row=r_num, column=1, value="보호자 의견")
776
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num+1, end_column=9)
777
- ws.cell(row=r_num, column=3, value=f"{info['guardian_opinion']}")
778
-
779
- style_range(ws, f"A{r_num}:B{r_num+1}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
780
- style_range(ws, f"C{r_num}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
781
-
782
- r_num += 2
783
-
784
- # Row 4 & 5: Facility Explanation
785
- ws.row_dimensions[r_num].height = 28
786
- ws.row_dimensions[r_num+1].height = 28
787
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+1, end_column=2)
788
- ws.cell(row=r_num, column=1, value="기관 설명내용")
789
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num+1, end_column=9)
790
- ws.cell(row=r_num, column=3, value=f"{info['guardian_response']}")
791
-
792
- style_range(ws, f"A{r_num}:B{r_num+1}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
793
- style_range(ws, f"C{r_num}:I{r_num+1}", font=value_font, alignment=align_left_wrap, border=thin_border)
794
-
795
- return r_num + 1
796
-
797
- def write_photo_section(ws, r_num):
798
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num+5, end_column=9)
799
- for i in range(6):
800
- ws.row_dimensions[r_num+i].height = 25
801
-
802
- photo_cell = ws.cell(row=r_num, column=1)
803
- photo_cell.value = (
804
- "[훈련 사진 첨부란]\n\n"
805
- "※ 여기에 실제 대피훈련, 소화기 시연, 응급모의대응 등 훈련 사진을 부착하십시오.\n"
806
- "(장기요양기관 평가 대응을 위해 2장 이상의 사진 부착이 필수적입니다.)"
807
- )
808
- style_range(ws, f"A{r_num}:I{r_num+5}",
809
- font=Font(name="맑은 고딕", size=9.5, italic=True, color="667085"),
810
- alignment=Alignment(vertical="center", horizontal="center", wrap_text=True),
811
- border=thin_border)
812
-
813
- return r_num + 5
814
-
815
- # ---------------------------------------------------------
816
- # Excel Creator: Write Checklist Sheet
817
- # ---------------------------------------------------------
818
- def write_checklist_tab(ws, info):
819
- col_widths = {
820
- 'A': 10, 'B': 10, 'C': 12, 'D': 12, 'E': 12, 'F': 12, 'G': 12, 'H': 14, 'I': 14
821
- }
822
- for col, width in col_widths.items():
823
- ws.column_dimensions[col].width = width
824
-
825
- setup_page_print(ws)
826
-
827
- # Title
828
- ws.row_dimensions[1].height = 22
829
- ws.row_dimensions[2].height = 22
830
- ws.merge_cells("A1:I2")
831
- title_cell = ws.cell(row=1, column=1)
832
- title_cell.value = "장기요양기관 평가 대응 - 훈련 및 교육 누락 체크리스트"
833
- style_range(ws, "A1:I2",
834
- font=Font(name="맑은 고딕", size=14, bold=True, color="111827"),
835
- alignment=align_center,
836
- border=thin_border)
837
-
838
- # Info Banner
839
- ws.row_dimensions[4].height = 30
840
- ws.merge_cells("A4:I4")
841
- banner_cell = ws.cell(row=4, column=1)
842
- banner_cell.value = "※ 본 체크리스트는 국민건강보험공단 장기요양기관 평가 고시 기준 및 매뉴얼에 의거하여 작성되었습니다. 각 항목의 반영 여부를 확인하십시오."
843
- style_range(ws, "A4:I4",
844
- fill=PatternFill(start_color="F9FAFB", end_color="F9FAFB", fill_type="solid"),
845
- font=Font(name="맑은 고딕", size=9, italic=True, color="475467"),
846
- alignment=align_center,
847
- border=thin_border)
848
-
849
- # Headers
850
- ws.row_dimensions[6].height = 26
851
- ws.merge_cells("A6:B6")
852
- ws.cell(row=6, column=1, value="평가 항목")
853
- ws.merge_cells("C6:E6")
854
- ws.cell(row=6, column=3, value="세부 확인사항 및 기준")
855
- ws.merge_cells("F6:G6")
856
- ws.cell(row=6, column=6, value="반영 상태")
857
- ws.merge_cells("H6:I6")
858
- ws.cell(row=6, column=8, value="평가 기준 충족 여부")
859
-
860
- style_range(ws, "A6:I6", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
861
-
862
- checklist_items = [
863
- ("훈련계획서 작성 여부", "훈련을 실시하기 전 구체적인 계획서가 수립되어 있는가?", "작성 완료", "충족 (Pass)"),
864
- ("결과보고서 작성 여부", "훈련 실시 후 결과보고서가 작성 및 보관되어 있는가?", "작성 완료", "충족 (Pass)"),
865
- ("훈련일자 기록", "계획서 및 결과보고서에 실제 훈련일자가 정확히 기록되었는가?", f"{info['training_date']} 기록됨", "충족 (Pass)"),
866
- ("훈련시간 기록", "훈련 시작 및 종료 시간(예: 1시간 이상)이 상세히 기록되었는가?", f"{info['training_time']} 기록됨", "충족 (Pass)"),
867
- ("훈련장소 기록", "훈련이 실시된 구체적인 장소가 명시되어 있는가?", f"{info['training_location']} 기록됨", "충족 (Pass)"),
868
- ("참여자 명단", "훈련에 참여한 수급자와 종사자의 명단이 첨부 또는 기록되었는가?", f"총 {info['total_participants']}명 명단 확인", "충족 (Pass)"),
869
- ("직종별 역할분담", "시설장, 복지사, 요양보호사 등 다직종 역할 분담이 계획서에 수립되었는가?", "5개 직종 역할 수립", "충족 (Pass)"),
870
- ("대피취약자 지원계획", "휠체어, 보행보조 필요 등 대피취약자 개별 이동지원 계획이 수립되었는가?", f"대피취약자 {info['vulnerable_count']}명 지원 계획 반영", "충족 (Pass)"),
871
- ("비상연락체계", "소방서, 경찰서, 협력병원 등 유관기관 비상연락체계가 구성되었는가?", f"{info['contact_checked']}", "충족 (Pass)"),
872
- ("대피경로", "피난동선 및 비상 대피경로가 지정되고 사전에 확인되었는가?", f"{info['route_checked']}", "충족 (Pass)"),
873
- ("소화기 사용 또는 화재대응", "훈련 내용 중 소화설비 시연 또는 화재 대피 실무가 포함되었는가?", f"{info['fire_ext_demo']}", "충족 (Pass)"),
874
- ("실제 대피훈련 실시 여부", "모의 상황에 따라 전원이 실제로 야외 대피 동선으로 대피를 실시했는가?", "실제 대피훈련 실시", "충족 (Pass)"),
875
- ("사진 첨부란", "훈련 장면을 증명할 수 있는 사진첩 또는 부착 공간이 마련되었는가?", "사진 첨부 공간 확보", "충족 (Pass)"),
876
- ("개선조치", "훈련 과정에서 노출된 문제점에 대해 명확한 개선조치가 수립되었는가?", "데이터 기반 개선조치 수립", "충족 (Pass)"),
877
- ("다음 훈련 반영사항", "금번 훈련 평가 결과가 다음번 훈련 계획에 구체적으로 반영되었는가?", "다음 훈련 반영사항 수립", "충족 (Pass)"),
878
- ("보호자 전화 의견 반영란", "훈련 결과에 대해 보호자의 유선 피드백을 기록하고 서비스계획에 반영하였는가?", "보호자 의견 수렴 완료", "충족 (Pass)")
879
- ]
880
-
881
- r_num = 7
882
- for item, desc, status, compliance in checklist_items:
883
- ws.row_dimensions[r_num].height = 24
884
- ws.merge_cells(start_row=r_num, start_column=1, end_row=r_num, end_column=2)
885
- ws.cell(row=r_num, column=1, value=item)
886
- ws.merge_cells(start_row=r_num, start_column=3, end_row=r_num, end_column=5)
887
- ws.cell(row=r_num, column=3, value=desc)
888
- ws.merge_cells(start_row=r_num, start_column=6, end_row=r_num, end_column=7)
889
- ws.cell(row=r_num, column=6, value=status)
890
- ws.merge_cells(start_row=r_num, start_column=8, end_row=r_num, end_column=9)
891
- ws.cell(row=r_num, column=8, value=compliance)
892
-
893
- style_range(ws, f"A{r_num}:B{r_num}", fill=header_fill, font=label_font, alignment=align_center, border=thin_border)
894
- style_range(ws, f"C{r_num}:E{r_num}", font=value_font, alignment=align_left, border=thin_border)
895
- style_range(ws, f"F{r_num}:G{r_num}", font=value_font, alignment=align_center, border=thin_border)
896
-
897
- pass_fill = PatternFill(start_color="E8F8F5", end_color="E8F8F5", fill_type="solid")
898
- pass_font = Font(name="맑은 고딕", size=9.5, bold=True, color="0E6251")
899
- style_range(ws, f"H{r_num}:I{r_num}", fill=pass_fill, font=pass_font, alignment=align_center, border=thin_border)
900
-
901
- r_num += 1
902
-
903
- # ---------------------------------------------------------
904
- # Main Excel Workbook Generator
905
- # ---------------------------------------------------------
906
- def create_excel(info, training_type):
907
- data = compile_document_data(info, training_type)
908
- wb = openpyxl.Workbook()
909
-
910
- # ---------------------------------------------------------
911
- # TAB 1: 훈련 계획서 (Plan)
912
- # ---------------------------------------------------------
913
- ws_plan = wb.active
914
- ws_plan.title = "훈련 계획서"
915
-
916
- col_widths = {
917
- 'A': 10, 'B': 10, 'C': 12, 'D': 12, 'E': 12, 'F': 12, 'G': 12, 'H': 14, 'I': 14
918
- }
919
- for col, width in col_widths.items():
920
- ws_plan.column_dimensions[col].width = width
921
-
922
- setup_page_print(ws_plan)
923
- write_title_and_approval(ws_plan, f"{training_type} 계획서", data)
924
- write_spacer(ws_plan, 3)
925
-
926
- write_general_info_row(ws_plan, 4, "기관명", data['facility_name'], "훈련일자", data['training_date'])
927
- write_general_info_row(ws_plan, 5, "훈련시간", data['training_time'], "훈련장소", data['training_location'])
928
- write_general_info_row(ws_plan, 6, "훈련유형", f"{training_type} (대피실무 포함)", "훈련주관자", data['host'])
929
- write_spacer(ws_plan, 7)
930
-
931
- write_section_header(ws_plan, 8, "1. 훈련 참여자 계획 현황")
932
- write_plan_participants(ws_plan, 9, data)
933
- write_spacer(ws_plan, 11)
934
-
935
- write_plan_details(ws_plan, 12, data)
936
-
937
- # ---------------------------------------------------------
938
- # TAB 2: 훈련 결과보고서 (Result Report)
939
- # ---------------------------------------------------------
940
- ws_report = wb.create_sheet(title="훈련 결과보고서")
941
- for col, width in col_widths.items():
942
- ws_report.column_dimensions[col].width = width
943
-
944
- setup_page_print(ws_report)
945
- write_title_and_approval(ws_report, f"{training_type} 결과보고서 & 감사 의견서", data)
946
- write_spacer(ws_report, 3)
947
-
948
- write_general_info_row(ws_report, 4, "기관명", data['facility_name'], "훈련일자", data['training_date'])
949
- write_general_info_row(ws_report, 5, "훈련시간", data['training_time'], "훈련장소", data['training_location'])
950
- write_general_info_row(ws_report, 6, "훈련유형", f"{training_type} (실제 훈련완료)", "훈련주관자", data['host'])
951
- write_spacer(ws_report, 7)
952
-
953
- # 1. 훈련 실제 참여 및 데이터 결과
954
- write_section_header(ws_report, 8, "1. 훈련 실제 참여 및 데이터 결과")
955
- curr_row = write_report_participants(ws_report, 9, data)
956
-
957
- curr_row += 1
958
- write_spacer(ws_report, curr_row)
959
- curr_row += 1
960
-
961
- # 2. 다직종별 훈련 평가 및 회의 의견
962
- write_section_header(ws_report, curr_row, "2. 다직종별 훈련 평가 및 회의 의견")
963
- curr_row += 1
964
-
965
- curr_row = write_role_meeting_opinion(
966
- ws_report, curr_row, "시설장",
967
- f"- 평가 및 운영상 필요한 조치: {data['opinion_시설장_1']}",
968
- f"- 기관 차원의 지원: {data['opinion_시설장_2']}",
969
- f"- 역할: {data['opinion_시설장_role']}"
970
- )
971
- curr_row += 1
972
- curr_row = write_role_meeting_opinion(
973
- ws_report, curr_row, "사회복지사",
974
- f"- 사례관리 관점: {data['opinion_사회복지사_1']}",
975
- f"- 서비스계획 반영: {data['opinion_사회복지사_2']}",
976
- f"- 역할: {data['opinion_사회복지사_role']}"
977
- )
978
- curr_row += 1
979
- curr_row = write_role_meeting_opinion(
980
- ws_report, curr_row, "요양보호사",
981
- f"- 일상생활 관찰: {data['opinion_요양보호사_1']}",
982
- f"- 돌봄 제공 시 유의사항: {data['opinion_요양보호사_2']}",
983
- f"- 역할: {data['opinion_요양보호사_role']}"
984
- )
985
- curr_row += 1
986
- curr_row = write_role_meeting_opinion(
987
- ws_report, curr_row, "간호조무사",
988
- f"- 건강상태 확인: {data['opinion_간호조무사_1']}",
989
- f"- 복약·피부·감염관리: {data['opinion_간호조무사_2']}",
990
- f"- 역할: {data['opinion_간호조무사_role']}"
991
- )
992
- curr_row += 1
993
- curr_row = write_role_meeting_opinion(
994
- ws_report, curr_row, "물리치료사",
995
- f"- 신체기능 평가: {data['opinion_물리치료사_1']}",
996
- f"- 낙상예방 및 운동계획: {data['opinion_물리치료사_2']}",
997
- f"- 역할: {data['opinion_물리치료사_role']}"
998
- )
999
-
1000
- curr_row += 1
1001
- write_spacer(ws_report, curr_row)
1002
- curr_row += 1
1003
-
1004
- # 3. 훈련 결과 분석 및 2차 반영 (2차 결과 반영)
1005
- write_section_header(ws_report, curr_row, "3. 훈련 결과 분석 및 2차 반영 (2차 결과 반영)")
1006
- curr_row += 1
1007
-
1008
- curr_row = write_secondary_result_row(ws_report, curr_row, "1차 문제상태", data['result_problem'])
1009
- curr_row += 1
1010
- curr_row = write_secondary_result_row(ws_report, curr_row, "적용한 조치", data['result_action'])
1011
- curr_row += 1
1012
- curr_row = write_secondary_result_row(ws_report, curr_row, "직종별 수행내용", data['result_role_actions'])
1013
- curr_row += 1
1014
- curr_row = write_secondary_result_row(ws_report, curr_row, "변화된 결과", data['result_outcome'])
1015
- curr_row += 1
1016
- curr_row = write_secondary_result_row(ws_report, curr_row, "구체적 데이터", data['result_data'])
1017
- curr_row += 1
1018
- curr_row = write_secondary_result_row(ws_report, curr_row, "남은 위험요인", data['result_risks'])
1019
- curr_row += 1
1020
- curr_row = write_secondary_result_row(ws_report, curr_row, "향후 계획", data['result_next'])
1021
- curr_row += 1
1022
- curr_row = write_secondary_result_row(ws_report, curr_row, "보호자 의견 반영내용", data['result_guardian'])
1023
-
1024
- curr_row += 1
1025
- write_spacer(ws_report, curr_row)
1026
- curr_row += 1
1027
-
1028
- # 4. 보호자 전화 의견 반영
1029
- write_section_header(ws_report, curr_row, "4. 보호자 전화 의견 반영")
1030
- curr_row += 1
1031
- curr_row = write_guardian_feedback(ws_report, curr_row, data)
1032
-
1033
- curr_row += 1
1034
- write_spacer(ws_report, curr_row)
1035
- curr_row += 1
1036
-
1037
- # 5. 훈련 사진 첨부란
1038
- write_section_header(ws_report, curr_row, "5. 훈련 사진 첨부란")
1039
- curr_row += 1
1040
- curr_row = write_photo_section(ws_report, curr_row)
1041
-
1042
- # ---------------------------------------------------------
1043
- # TAB 3: 평가누락 체크리스트 (Checklist)
1044
- # ---------------------------------------------------------
1045
- ws_checklist = wb.create_sheet(title="평가누락 체크리스트")
1046
- write_checklist_tab(ws_checklist, data)
1047
-
1048
- excel_stream = io.BytesIO()
1049
- wb.save(excel_stream)
1050
- excel_stream.seek(0)
1051
- return excel_stream
1052
-
1053
- # ---------------------------------------------------------
1054
- # Streamlit Application User Interface
1055
- # ---------------------------------------------------------
1056
- def main():
1057
- st.markdown('<div class="title-card"><h1>🚒 장기요양기관 평가 대응 훈련 문서 자동생성기</h1><p>다직종 감사 에이전트의 지침을 100% 반영하여, 실제 현장 및 평가 대응이 가능한 수준의 훈련 문서를 설계합니다.</p></div>', unsafe_allow_html=True)
1058
-
1059
- col_left, col_right = st.columns([1, 2.2])
1060
-
1061
- with col_left:
1062
- st.markdown('<div class="section-header">🏢 기관 기본 정보</div>', unsafe_allow_html=True)
1063
- facility_name = st.text_input("기관명", value="행복요양원")
1064
- facility_head = st.text_input("시설장명", value="홍길동")
1065
- writer = st.text_input("담당자 (작성자) 명", value="김복지")
1066
- training_date = st.date_input("훈련/교육 일자", value=datetime.today())
1067
- training_time = st.text_input("훈련/교육 시간", value="14:00 ~ 15:00")
1068
- training_location = st.text_input("훈련/교육 장소", value="요양원 전 층 및 야외 대피광장")
1069
-
1070
- st.markdown('<div class="section-header">⚙️ 훈련 및 교육 설정</div>', unsafe_allow_html=True)
1071
- training_type = st.selectbox("훈련/교육 유형", ["소방훈련", "재난훈련", "안전교육"])
1072
- host = st.text_input("훈련 주관자", value=f"시설장 {facility_head}")
1073
-
1074
- with col_right:
1075
- tab_inputs, tab_guardian, tab_preview = st.tabs(["📊 상세 정량적 데이터", "📞 보호자 의견 반영", "👁️ 전문성강화 감사 문장 실시간 프리뷰"])
1076
-
1077
- with tab_inputs:
1078
- st.markdown("##### 훈련 참여 정량 데이터")
1079
- col1, col2, col3 = st.columns(3)
1080
- with col1:
1081
- staff_participants = st.number_input("종사자 참여자 수 (명)", value=8, min_value=1)
1082
- vulnerable_count = st.number_input("대피취약자 수 (명)", value=5, min_value=0)
1083
- with col2:
1084
- recipient_participants = st.number_input("수급자 참여자 수 (명)", value=12, min_value=1)
1085
- wheelchair_count = st.number_input("휠체어 사용 대상자 수 (명)", value=3, min_value=0)
1086
- with col3:
1087
- total_participants = staff_participants + recipient_participants
1088
- st.metric("총 참여 인원 (종사자 + 수급자)", f"{total_participants} 명")
1089
- walk_aid_count = st.number_input("보행보조 필요자 수 (명)", value=4, min_value=0)
1090
-
1091
- st.markdown("##### 훈련 관련 확인/점검 지표")
1092
- col4, col5 = st.columns(2)
1093
- with col4:
1094
- evac_time = st.text_input("대피완료 시간", value="4분 30초")
1095
- route_checked = st.selectbox("대피경로 확인 여부", ["확인완료", "미확인"])
1096
- contact_checked = st.selectbox("비상연락망 작동 여부", ["작동확인", "미확인"])
1097
- with col5:
1098
- emergency_mock = st.selectbox("응급상황 모의대응 여부", ["실시 및 양호", "미실시"])
1099
- fire_ext_demo = st.selectbox("소화기 사용 시연 여부", ["실시 및 양호", "미실시"])
1100
-
1101
- with tab_guardian:
1102
- st.markdown("##### 훈련 후 보호자 유선 피드백 수렴 (사후 의견 반영)")
1103
- col_date, col_reflected = st.columns(2)
1104
- with col_date:
1105
- guardian_call_date = st.date_input("보호자 유선 연락 일자", value=datetime.today())
1106
- with col_reflected:
1107
- guardian_reflected = st.selectbox("안전관리/서비스계획 반영 여부", [
1108
- "반영완료 (서비스계획 및 안전관리계획 반영)",
1109
- "반영완료 (보호자 안내 및 연락망 최신화)",
1110
- "미반영"
1111
- ])
1112
-
1113
- guardian_opinion = st.text_area(
1114
- "보호자 통화 의견",
1115
- value="훈련 시 거동이 불편한 어르신들이 낙상하지 않도록 특별히 신경 써 주셨으면 좋겠고, 비상시 연락이 잘 닿을 수 있게 비상 연락망 관리를 철저히 해달라고 요청하셨습니다."
1116
- )
1117
- guardian_response = st.text_area(
1118
- "기관 설명 및 대책 내용",
1119
- value="훈련 전 대피취약자로 분류하여 요양보호사를 1:1 매칭하여 이동을 지원할 계획임을 설명하였으며, 보호자 비상연락처 최신화를 마쳤고 훈련 직후 결과를 안내할 예정임을 설명함."
1120
- )
1121
-
1122
- with tab_preview:
1123
- preview_info = {
1124
- 'facility_name': facility_name,
1125
- 'total_participants': total_participants,
1126
- 'staff_participants': staff_participants,
1127
- 'recipient_participants': recipient_participants,
1128
- 'evac_time': evac_time,
1129
- 'vulnerable_count': vulnerable_count,
1130
- 'wheelchair_count': wheelchair_count,
1131
- 'walk_aid_count': walk_aid_count,
1132
- 'emergency_mock': emergency_mock,
1133
- 'fire_ext_demo': fire_ext_demo,
1134
- 'route_checked': route_checked,
1135
- 'contact_checked': contact_checked,
1136
- 'guardian_call_date': str(guardian_call_date),
1137
- 'guardian_reflected': guardian_reflected
1138
- }
1139
-
1140
- st.markdown(f"### 📝 {training_type} 다직종 감사 의견서 및 2차 결과 프리뷰")
1141
-
1142
- st.markdown("#### [다직종 회의 의견]")
1143
- roles_list = ['시설장', '사회복지사', '요양보호사', '간호조무사', '물리치료사']
1144
- for r in roles_list:
1145
- st.markdown(f"**🔹 {r} 의견**")
1146
- if r == '시설장':
1147
- st.write(f"- 평가 및 운영상 필요한 조치: {generate_narrative_text(preview_info, 'opinion_시설장_1', training_type)}")
1148
- st.write(f"- 기관 차원의 지원: {generate_narrative_text(preview_info, 'opinion_시설장_2', training_type)}")
1149
- elif r == '사회복지사':
1150
- st.write(f"- 사례관리 관점: {generate_narrative_text(preview_info, 'opinion_사회복지사_1', training_type)}")
1151
- st.write(f"- 서비스계획 반영: {generate_narrative_text(preview_info, 'opinion_사회복지사_2', training_type)}")
1152
- elif r == '요양보호사':
1153
- st.write(f"- 일상생활 관찰: {generate_narrative_text(preview_info, 'opinion_요양보호사_1', training_type)}")
1154
- st.write(f"- 돌봄 제공 시 유의사항: {generate_narrative_text(preview_info, 'opinion_요양보호사_2', training_type)}")
1155
- elif r == '간호조무사':
1156
- st.write(f"- 건강상태 확인: {generate_narrative_text(preview_info, 'opinion_간호조무사_1', training_type)}")
1157
- st.write(f"- 복약·피부·감염관리: {generate_narrative_text(preview_info, 'opinion_간호조무사_2', training_type)}")
1158
- elif r == '물리치료사':
1159
- st.write(f"- 신체기능 평가: {generate_narrative_text(preview_info, 'opinion_물리치료사_1', training_type)}")
1160
- st.write(f"- 낙상예방 및 운동계획: {generate_narrative_text(preview_info, 'opinion_물리치료사_2', training_type)}")
1161
- st.write(f"- 역할: {generate_narrative_text(preview_info, f'opinion_{r}_role', training_type)}")
1162
-
1163
- st.markdown("#### [2차 결과 반영]")
1164
- st.write(f"**1차 문제상태**: {generate_narrative_text(preview_info, 'result_problem', training_type)}")
1165
- st.write(f"**적용한 조치**: {generate_narrative_text(preview_info, 'result_action', training_type)}")
1166
- st.write(f"**직종별 수행내용**: {generate_narrative_text(preview_info, 'result_role_actions', training_type)}")
1167
- st.write(f"**변화된 결과**: {generate_narrative_text(preview_info, 'result_outcome', training_type)}")
1168
- st.write(f"**구체적 데이터**: {generate_narrative_text(preview_info, 'result_data', training_type)}")
1169
- st.write(f"**남은 위험요인**: {generate_narrative_text(preview_info, 'result_risks', training_type)}")
1170
- st.write(f"**향후 계획**: {generate_narrative_text(preview_info, 'result_next', training_type)}")
1171
- st.write(f"**보호자 의견 반영내용**: {generate_narrative_text(preview_info, 'result_guardian', training_type)}")
1172
-
1173
- st.markdown('<div style="margin-top: 2rem;"></div>', unsafe_allow_html=True)
1174
-
1175
- info_payload = {
1176
- 'facility_name': facility_name,
1177
- 'facility_head': facility_head,
1178
- 'writer': writer,
1179
- 'training_date': training_date,
1180
- 'training_time': training_time,
1181
- 'training_location': training_location,
1182
- 'host': host,
1183
- 'total_participants': total_participants,
1184
- 'staff_participants': staff_participants,
1185
- 'recipient_participants': recipient_participants,
1186
- 'evac_time': evac_time,
1187
- 'vulnerable_count': vulnerable_count,
1188
- 'wheelchair_count': wheelchair_count,
1189
- 'walk_aid_count': walk_aid_count,
1190
- 'emergency_mock': emergency_mock,
1191
- 'fire_ext_demo': fire_ext_demo,
1192
- 'route_checked': route_checked,
1193
- 'contact_checked': contact_checked,
1194
- 'guardian_call_date': guardian_call_date,
1195
- 'guardian_opinion': guardian_opinion,
1196
- 'guardian_response': guardian_response,
1197
- 'guardian_reflected': guardian_reflected,
1198
- }
1199
-
1200
- excel_file = create_excel(info_payload, training_type)
1201
- filename = f"{facility_name}_{training_date.strftime('%Y%m%d')}_{training_type}_다직종평가대응_통합문서.xlsx"
1202
-
1203
- st.download_button(
1204
- label=f"📥 {training_type} 계획서/결과보고서 엑셀 다운로드 (감사 반영본)",
1205
- data=excel_file,
1206
- file_name=filename,
1207
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1208
- )
1209
-
1210
- if __name__ == "__main__":
1211
- main()