devorah-ai-2016 commited on
Commit
6ab9e83
·
verified ·
1 Parent(s): f5d8a86

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -395
app.py DELETED
@@ -1,395 +0,0 @@
1
- import os
2
- import sqlite3
3
- import random
4
- import tempfile
5
- from io import BytesIO
6
- from datetime import date, datetime
7
-
8
- import pandas as pd
9
- import streamlit as st
10
- from openpyxl import Workbook
11
- from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
12
- from openpyxl.utils import get_column_letter
13
- from openpyxl.drawing.image import Image as XLImage
14
-
15
-
16
- # =========================================================
17
- # 통합연동 재난/소방훈련일지 생성기
18
- # - Client Center DB 연동
19
- # - 직원 등록/참석자 명단 연동
20
- # - 훈련유형: 소방훈련 / 재난훈련 / 안전교육 / 감염대응훈련 / 실종대응훈련 / 응급상황훈련
21
- # - 계획서 + 결과보고서 + 평가누락 체크리스트 엑셀 생성
22
- # - 훈련사진 최대 4장 첨부
23
- # - 평가 적합도/부족 항목 점검
24
- # - 결재라인 담당자/시설장 2칸, 지저분한 빈칸 없이 출력
25
- # =========================================================
26
-
27
- st.set_page_config(
28
- page_title="재난훈련일지",
29
- page_icon="🚒",
30
- layout="wide"
31
- )
32
-
33
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
34
-
35
- DB_CANDIDATES = [
36
- os.path.join(CURRENT_DIR, "data", "clients.db"),
37
- os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"),
38
- os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"),
39
- os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"),
40
- ]
41
-
42
- DB_PATH = None
43
- for p in DB_CANDIDATES:
44
- if os.path.exists(p):
45
- DB_PATH = p
46
- break
47
-
48
- if DB_PATH is None:
49
- os.makedirs(os.path.join(CURRENT_DIR, "data"), exist_ok=True)
50
- DB_PATH = os.path.join(CURRENT_DIR, "data", "clients.db")
51
-
52
-
53
- # -----------------------------
54
- # UI 디자인
55
- # -----------------------------
56
- st.markdown("""
57
- <style>
58
- .block-container { padding-top: 2rem; }
59
- .hero {
60
- background: linear-gradient(135deg, #991B1B, #EF4444);
61
- padding: 30px;
62
- border-radius: 22px;
63
- text-align: center;
64
- margin-bottom: 22px;
65
- }
66
- .hero h1 { color: white; margin: 0; font-size: 36px; }
67
- .hero p { color: white; margin-top: 10px; font-size: 18px; }
68
- .notice {
69
- background: #fff8e1;
70
- border-left: 7px solid #f9a825;
71
- padding: 15px;
72
- border-radius: 12px;
73
- margin-bottom: 18px;
74
- }
75
- .okbox {
76
- background: #e8f5e9;
77
- border-left: 7px solid #43a047;
78
- padding: 14px;
79
- border-radius: 12px;
80
- margin-bottom: 14px;
81
- }
82
- .warnbox {
83
- background: #fff3e0;
84
- border-left: 7px solid #fb8c00;
85
- padding: 14px;
86
- border-radius: 12px;
87
- margin-bottom: 14px;
88
- }
89
- </style>
90
- """, unsafe_allow_html=True)
91
-
92
- st.markdown("""
93
- <div class="hero">
94
- <h1>🚒 재난·소방훈련일지 생성기</h1>
95
- <p>재난훈련, 소방훈련, 안전교육을 하나의 통합 생성기에서 관리하고 평가누락을 점검합니다.</p>
96
- </div>
97
- """, unsafe_allow_html=True)
98
-
99
- st.markdown("""
100
- <div class="notice">
101
- <strong>운영 권장:</strong> 소방훈련과 재난훈련을 완전히 따로 만들기보다, 하나의 통합 생성기 안에서 훈련유형을 선택하게 하는 방식이 관리에 좋습니다. 실제 기관에서는 소방훈련을 재난대응훈련의 한 유형으로 함께 관리하는 경우가 많기 때문입니다.
102
- </div>
103
- """, unsafe_allow_html=True)
104
-
105
-
106
- # -----------------------------
107
- # DB
108
- # -----------------------------
109
- def get_conn():
110
- return sqlite3.connect(DB_PATH, check_same_thread=False)
111
-
112
-
113
- def init_db():
114
- conn = get_conn()
115
- cur = conn.cursor()
116
-
117
- cur.execute("""
118
- CREATE TABLE IF NOT EXISTS clients (
119
- id INTEGER PRIMARY KEY AUTOINCREMENT,
120
- name TEXT NOT NULL,
121
- masked_name TEXT,
122
- gender TEXT,
123
- birth_year TEXT,
124
- care_grade TEXT,
125
- institution_type TEXT,
126
- institution_name TEXT,
127
- admission_date TEXT,
128
- main_condition TEXT,
129
- cognitive_status TEXT,
130
- mobility_status TEXT,
131
- diet_status TEXT,
132
- toileting_status TEXT,
133
- guardian_name TEXT,
134
- guardian_phone TEXT,
135
- notes TEXT,
136
- status TEXT DEFAULT '이용중',
137
- created_at TEXT,
138
- updated_at TEXT
139
- )
140
- """)
141
-
142
- cur.execute("""
143
- CREATE TABLE IF NOT EXISTS staff_members (
144
- id INTEGER PRIMARY KEY AUTOINCREMENT,
145
- name TEXT NOT NULL,
146
- position TEXT,
147
- department TEXT,
148
- phone TEXT,
149
- employment_status TEXT DEFAULT '재직',
150
- created_at TEXT
151
- )
152
- """)
153
-
154
- cur.execute("""
155
- CREATE TABLE IF NOT EXISTS disaster_training_records (
156
- id INTEGER PRIMARY KEY AUTOINCREMENT,
157
- institution_name TEXT,
158
- institution_type TEXT,
159
- training_type TEXT,
160
- training_title TEXT,
161
- training_date TEXT,
162
- training_time TEXT,
163
- training_place TEXT,
164
- trainer TEXT,
165
- host TEXT,
166
- staff_attendees TEXT,
167
- client_attendees TEXT,
168
- total_participants INTEGER,
169
- vulnerable_count INTEGER,
170
- wheelchair_count INTEGER,
171
- walk_aid_count INTEGER,
172
- scenario TEXT,
173
- role_division TEXT,
174
- training_process TEXT,
175
- result_evaluation TEXT,
176
- improvement_plan TEXT,
177
- guardian_feedback TEXT,
178
- photo_count INTEGER,
179
- evaluation_score INTEGER,
180
- missing_items TEXT,
181
- compliance_result TEXT,
182
- created_at TEXT
183
- )
184
- """)
185
-
186
- conn.commit()
187
- conn.close()
188
-
189
-
190
- def load_staff():
191
- conn = get_conn()
192
- try:
193
- df = pd.read_sql_query("SELECT * FROM staff_members ORDER BY id DESC", conn)
194
- except Exception:
195
- df = pd.DataFrame()
196
- conn.close()
197
- return df
198
-
199
-
200
- def load_clients():
201
- conn = get_conn()
202
- try:
203
- df = pd.read_sql_query("SELECT * FROM clients ORDER BY id DESC", conn)
204
- except Exception:
205
- df = pd.DataFrame()
206
- conn.close()
207
- return df
208
-
209
-
210
- def save_staff(name, position, department, phone):
211
- conn = get_conn()
212
- cur = conn.cursor()
213
- cur.execute("""
214
- INSERT INTO staff_members (name, position, department, phone, employment_status, created_at)
215
- VALUES (?, ?, ?, ?, ?, ?)
216
- """, (name, position, department, phone, "재직", datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
217
- conn.commit()
218
- conn.close()
219
-
220
-
221
- def save_training_record(data):
222
- conn = get_conn()
223
- cur = conn.cursor()
224
- cur.execute("""
225
- INSERT INTO disaster_training_records (
226
- institution_name, institution_type, training_type, training_title,
227
- training_date, training_time, training_place, trainer, host,
228
- staff_attendees, client_attendees, total_participants,
229
- vulnerable_count, wheelchair_count, walk_aid_count,
230
- scenario, role_division, training_process, result_evaluation,
231
- improvement_plan, guardian_feedback, photo_count,
232
- evaluation_score, missing_items, compliance_result, created_at
233
- )
234
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
235
- """, (
236
- data["institution_name"], data["institution_type"], data["training_type"], data["training_title"],
237
- data["training_date"], data["training_time"], data["training_place"], data["trainer"], data["host"],
238
- data["staff_attendees"], data["client_attendees"], data["total_participants"],
239
- data["vulnerable_count"], data["wheelchair_count"], data["walk_aid_count"],
240
- data["scenario"], data["role_division"], data["training_process"], data["result_evaluation"],
241
- data["improvement_plan"], data["guardian_feedback"], data["photo_count"],
242
- data["evaluation_score"], data["missing_items"], data["compliance_result"],
243
- datetime.now().strftime("%Y-%m-%d %H:%M:%S")
244
- ))
245
- conn.commit()
246
- conn.close()
247
-
248
-
249
- def load_training_records():
250
- conn = get_conn()
251
- try:
252
- df = pd.read_sql_query("SELECT * FROM disaster_training_records ORDER BY id DESC", conn)
253
- except Exception:
254
- df = pd.DataFrame()
255
- conn.close()
256
- return df
257
-
258
-
259
- init_db()
260
-
261
-
262
- # -----------------------------
263
- # 훈련 유형별 시나리오
264
- # -----------------------------
265
- TRAINING_TYPES = {
266
- "소방훈련": {
267
- "scenario": "시설 내 화재 발생을 가정하여 화재 발견, 비상벨 작동, 119 신고, 초기 소화, 수급자 대피, 집결지 인원확인 순으로 훈련을 실시한다.",
268
- "focus": ["화재 발견", "119 신고", "초기 소화", "대피유도", "대피취약자 지원", "인원확인"],
269
- "risk": "연기 흡입, 대피 지연, 휠체어 이동 지연, 야간 화재 시 인력 부족",
270
- },
271
- "재난훈련": {
272
- "scenario": "지진, 태풍, 정전 등 재난상황을 가정하여 실내 안전확보, 비상연락망 가동, 대피경로 확인, 대피취약자 이동지원, 집결지 확인을 실시한다.",
273
- "focus": ["재난상황 인지", "머리보호/초기안전", "비상연락망", "대피경로", "집결지 확인", "보호자 안내"],
274
- "risk": "정전, 낙하물, 이동 중 낙상, 통신장애, 대피공간 혼잡",
275
- },
276
- "안전교육": {
277
- "scenario": "낙상, 질식, 응급상황, 감염 등 기관 내 안전사고 예방을 위해 직원 역할, 초기대응, 보고체계, 수급자 보호 방법을 교육하고 모의실습한다.",
278
- "focus": ["낙상예방", "질식예방", "응급처치", "보고체계", "안전점검", "사고예방"],
279
- "risk": "안전수칙 미숙지, 응급도구 위치 미확인, 초기 대응 지연",
280
- },
281
- "감염대응훈련": {
282
- "scenario": "감염 의심 수급자 발생을 가정하여 발열 확인, 격리 안내, 보호구 착용, 환경소독, 보호자 안내, 보건소/의료기관 연계를 점검한다.",
283
- "focus": ["발열확인", "격리", "손위생", "보호구", "환경소독", "의료기관 연계"],
284
- "risk": "감염확산, 보호구 착용 미흡, 동선 분리 미흡, 보호자 안내 지연",
285
- },
286
- "실종대응훈련": {
287
- "scenario": "인지저하 수급자 실종 또는 이탈 상황을 가정하여 즉시 인원확인, 내부수색, CCTV/출입문 확인, 보호자 및 경찰 신고, 수색결과 기록을 실시한다.",
288
- "focus": ["인원확인", "내부수색", "출입문 확인", "CCTV 확인", "보호자 연락", "경찰 신고"],
289
- "risk": "초기 발견 지연, 출입문 관리 미흡, 수색 역할 혼선",
290
- },
291
- "응급상황훈련": {
292
- "scenario": "수급자의 의식저하, 호흡곤란, 낙상 후 통증, 질식 등 응급상황 발생을 가정하여 상태확인, 119 신고, 보호자 연락, 응급처치, 기록을 실시한다.",
293
- "focus": ["상태확인", "119 신고", "응급처치", "보호자 연락", "활력징후", "기록"],
294
- "risk": "초기 판단 지연, 신고 지연, 응급처치 미숙, 기록 누락",
295
- },
296
- }
297
-
298
- ROLE_BASE = {
299
- "시설장": "훈련 총괄, 비상대응 지휘, 유관기관 협조, 개선계획 승인",
300
- "사회복지사": "훈련계획 수립, 참석자 명단 확인, 수급자/보호자 안내, 훈련기록 작성",
301
- "요양보호사": "수급자 대피 보조, 이동지원, 낙상예방, 현장 안전확인",
302
- "간호조무사": "응급상황 확인, 활력징후 관찰, 구급함 준비, 부상자 응급조치",
303
- "물리치료사": "휠체어·워커 사용 대상자 이동지원, 보행불안정자 안전자문, 낙상위험 동선 점검",
304
- "조리원": "가스·전열기구 확인, 조리실 화재위험 차단, 대피지원",
305
- "사무원": "비상연락망 확인, 전화연락 보조, 문서·사진자료 정리",
306
- }
307
-
308
- VARIANTS = [
309
- "훈련 과정에서 각 직종은 담당 역할을 숙지하고 실제 상황에 준하는 방식으로 대응하였다.",
310
- "훈련은 단순 교육에 그치지 않고 대피동선, 비상연락, 대피취약자 지원을 실제로 확인하는 방식으로 진행하였다.",
311
- "참석자들은 훈련 시나리오에 따라 역할을 분담하고, 문제점과 개선사항을 현장에서 즉시 확인하였다.",
312
- ]
313
-
314
-
315
- # -----------------------------
316
- # 생성 로직
317
- # -----------------------------
318
- def save_uploaded_photos(uploaded_files):
319
- paths = []
320
- for file in (uploaded_files or [])[:4]:
321
- suffix = os.path.splitext(file.name)[1].lower() or ".png"
322
- temp_path = os.path.join(tempfile.gettempdir(), f"disaster_photo_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{suffix}")
323
- with open(temp_path, "wb") as f:
324
- f.write(file.getbuffer())
325
- paths.append(temp_path)
326
- return paths
327
-
328
-
329
- def build_attendees_text(selected_staff, selected_clients, extra_staff, extra_clients):
330
- staff_text = "\n".join(selected_staff) if selected_staff else ""
331
- client_text = "\n".join(selected_clients) if selected_clients else ""
332
-
333
- if extra_staff.strip():
334
- staff_text += ("\n" if staff_text else "") + extra_staff.strip()
335
- if extra_clients.strip():
336
- client_text += ("\n" if client_text else "") + extra_clients.strip()
337
-
338
- return staff_text.strip(), client_text.strip()
339
-
340
-
341
- def build_role_division(training_type, selected_roles):
342
- focus = ", ".join(TRAINING_TYPES[training_type]["focus"])
343
- lines = []
344
- for role in selected_roles:
345
- base = ROLE_BASE.get(role, "훈련 참여 및 현장 안전관리")
346
- lines.append(f"- {role}: {base}. {training_type} 중점항목({focus})을 확인하고 결과를 공유한다.")
347
- return "\n".join(lines)
348
-
349
-
350
- def build_training_process(training_type, route_checked, contact_checked, emergency_mock, extinguisher_demo):
351
- scenario = TRAINING_TYPES[training_type]["scenario"]
352
- focus = TRAINING_TYPES[training_type]["focus"]
353
- steps = []
354
- for idx, item in enumerate(focus, start=1):
355
- steps.append(f"{idx}. {item} 절차를 확인하고 담당자별 역할에 따라 수행함.")
356
-
357
- return f"""
358
- [훈련 시나리오]
359
- {scenario}
360
-
361
- [훈련 진행절차]
362
- {chr(10).join(steps)}
363
-
364
- [점검결과]
365
- - 대피경로 확인: {route_checked}
366
- - 비상연락망 확인: {contact_checked}
367
- - 응급상황 모의대응: {emergency_mock}
368
- - 소화기/장비 시연: {extinguisher_demo}
369
-
370
- {random.choice(VARIANTS)}
371
- """.strip()
372
-
373
-
374
- def build_result(training_type, total_participants, vulnerable_count, wheelchair_count, walk_aid_count,
375
- evac_time, guardian_opinion, custom_result):
376
- risk = TRAINING_TYPES[training_type]["risk"]
377
- focus = ", ".join(TRAINING_TYPES[training_type]["focus"])
378
-
379
- result = f"""
380
- [훈련결과]
381
- 총 {total_participants}명이 훈련에 참여하였으며, 훈련유형은 {training_type}입니다.
382
- 대피완료 또는 주요 대응 완료시간은 {evac_time}로 확인되었습니다.
383
- 대피취약자는 {vulnerable_count}명, 휠체어 사용 대상자는 {wheelchair_count}명, 보행보조 필요자는 {walk_aid_count}명으��� 확인되어 개별 이동지원이 필요한 대상자를 중심으로 대응체계를 점검하였습니다.
384
-
385
- [기관유형별 안전관리 반영]
386
- 이번 훈련에서는 {focus} 항목을 중심으로 실제 대응능력을 확인하였고, 남은 위험요인은 {risk}입니다.
387
-
388
- [보호자 의견 반영]
389
- {guardian_opinion or '보호자 의견은 특이사항 없으며, 비상연락망과 대피취약자 지원계획을 지속 관리하기로 함.'}
390
-
391
- [교육 및 훈련 효과]
392
- 참석자들은 비상상황 발생 시 본인의 역할, 보고체계, 대피지원 방법을 숙지하였으며, 훈련 내용을 실제 수급자 안전관리와 급여제공 과정에 반영하기로 하였습니다.
393
-
394
- [기관 추가 결과]
395
- {custom_