devorah-ai-2016 commited on
Commit
56d34ff
·
verified ·
1 Parent(s): d13cc47

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -571
app.py DELETED
@@ -1,571 +0,0 @@
1
- import os
2
- import sqlite3
3
- from io import BytesIO
4
- from datetime import date, datetime
5
-
6
- import pandas as pd
7
- import streamlit as st
8
- from openpyxl import Workbook
9
- from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
10
- from openpyxl.utils import get_column_letter
11
-
12
-
13
- st.set_page_config(page_title="상태변화기록지", page_icon="📈", layout="wide")
14
-
15
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
16
- DB_CANDIDATES = [
17
- os.path.join(CURRENT_DIR, "data", "clients.db"),
18
- os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"),
19
- os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"),
20
- os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"),
21
- ]
22
-
23
- DB_PATH = None
24
- for candidate in DB_CANDIDATES:
25
- if os.path.exists(candidate):
26
- DB_PATH = candidate
27
- break
28
-
29
-
30
- st.markdown(
31
- """
32
- <style>
33
- .block-container { padding-top: 1.6rem; max-width: 1160px; }
34
- .hero { background: linear-gradient(135deg,#2563EB,#60A5FA); padding:32px; border-radius:24px; text-align:center; margin-bottom:20px; }
35
- .hero h1 { color:white; margin:0; font-size:42px; font-weight:800; }
36
- .hero p { color:white; margin:14px 0 0 0; font-size:19px; line-height:1.55; }
37
- .notice { background:#FFF8E1; border-left:7px solid #F59E0B; padding:15px 18px; border-radius:14px; margin:16px 0; line-height:1.7; }
38
- .okbox { background:#E8F5E9; border-left:7px solid #43A047; padding:14px; border-radius:12px; margin:10px 0; }
39
- .warnbox { background:#FFF3E0; border-left:7px solid #FB8C00; padding:14px; border-radius:12px; margin:10px 0; }
40
- </style>
41
- """,
42
- unsafe_allow_html=True,
43
- )
44
-
45
- st.markdown(
46
- """
47
- <div class="hero">
48
- <h1>📈 상태변화기록지 생성기</h1>
49
- <p>Client Center 정보와 직접 입력 메모를 바탕으로 5대 영역, SOAP, 위험판정, 연계계획을 자동 생성합니다.</p>
50
- </div>
51
- """,
52
- unsafe_allow_html=True,
53
- )
54
-
55
- st.markdown(
56
- """
57
- <div class="notice">
58
- <strong>작성 원칙:</strong> 상태변화가 없다는 반복 문구를 피하고, 신체·인지·감정·관계·지역자원연계 영역을 기준으로 평가 적합 문체로 기록합니다.
59
- </div>
60
- """,
61
- unsafe_allow_html=True,
62
- )
63
-
64
- st.markdown(
65
- """
66
- <div class="notice">
67
- ※ 생성된 문서는 그대로 사용하셔도 되며, 기관의 운영방침, 수급자 상태, 보호자 의견 및 평가기준에 따라 추가 보완사항은 얼마든지 수정 가능합니다. 기관의 상황에 맞게 활용하시기 바랍니다.
68
- </div>
69
- """,
70
- unsafe_allow_html=True,
71
- )
72
-
73
-
74
- RESTRICTED_REPLACEMENTS = {
75
- "상태 양호": "신체·인지·정서 상태가 비교적 안정적으로 유지되나 정기 관찰이 요구됨",
76
- "특이사항 없음": "즉시 개입이 필요한 돌발 상황은 확인되지 않았으나 상태변화 관찰을 지속함",
77
- "이전과 동일": "기존 상태의 급격한 악화는 제한적이나 기능 유지 여부를 구체적으로 확인함",
78
- "문제없음": "현재 확인된 위험요인은 제한적이나 급여제공 과정에서 재확인이 필요함",
79
- "문제 없음": "현재 확인된 위험요인은 제한적이나 급여제공 과정에서 재확인이 필요함",
80
- }
81
-
82
- DOMAIN_KEYWORDS = {
83
- "신체": ["통증", "낙상", "어지", "휘청", "보행", "식사", "식욕", "체중", "사레", "연하", "배변", "배뇨", "실금", "기저귀", "욕창", "발적", "상처", "피부", "수면", "입원", "퇴원"],
84
- "인지": ["치매", "인지", "기억", "반복", "혼동", "섬망", "지남력", "배회", "망상", "도둑", "환각", "귀가", "집에"],
85
- "감정": ["우울", "눈물", "무기력", "불안", "초조", "짜증", "화", "거부", "살기 싫", "귀찮"],
86
- "관계": ["다툼", "갈등", "폭언", "공격", "보호자", "가족", "종사자", "타 수급자", "말다툼", "거부"],
87
- "지역자원연계": ["독거", "결식", "경제", "주거", "복약", "병원", "진료", "보건소", "복지관", "행정복지센터", "치매안심센터", "방문간호"],
88
- }
89
-
90
- BPSD_KEYWORDS = ["도둑", "망상", "석양", "배회", "공격", "폭언", "야간섬망", "환각", "귀가", "집에", "거부"]
91
- DEPRESSION_KEYWORDS = ["우울", "눈물", "식사거부", "무기력", "살기 싫", "귀찮", "외로"]
92
- COMMUNITY_KEYWORDS = ["복약", "결식", "독거", "주거", "경제", "병원", "치매", "방문간호"]
93
- HIGH_RISK_KEYWORDS = ["응급", "입원", "낙상", "욕창", "상처", "감염", "공격", "배회", "망상", "식사거부", "자해", "학대", "방임"]
94
- WARNING_KEYWORDS = ["어지", "통증", "식사량", "실금", "반복", "불안", "우울", "갈등", "거부", "발적", "보행"]
95
-
96
-
97
- def normalize_text(text):
98
- value = str(text or "").strip()
99
- for bad, good in RESTRICTED_REPLACEMENTS.items():
100
- value = value.replace(bad, good)
101
- return value
102
-
103
-
104
- def has_keyword(text, keywords):
105
- source = str(text or "")
106
- return any(keyword in source for keyword in keywords)
107
-
108
-
109
- def pick(data, *keys, default=""):
110
- for key in keys:
111
- value = data.get(key, "")
112
- if value is None:
113
- continue
114
- value = str(value).strip()
115
- if value:
116
- return value
117
- return default
118
-
119
-
120
- def formal_sentence(text, fallback="지속 관찰이 요구됨"):
121
- value = normalize_text(text)
122
- if not value:
123
- value = fallback
124
- if value.endswith(("확인됨", "관찰됨", "사정됨", "판단됨", "계획함", "실시함", "요구됨", "필요함", "있음", "없음", "됨", "함")):
125
- return value
126
- last = value[-1]
127
- if "가" <= last <= "힣" and (ord(last) - ord("가")) % 28:
128
- return f"{value}으로 확인됨"
129
- return f"{value}로 확인됨"
130
-
131
-
132
- def load_clients():
133
- if not DB_PATH:
134
- return pd.DataFrame()
135
- try:
136
- conn = sqlite3.connect(DB_PATH)
137
- cols = pd.read_sql_query("PRAGMA table_info(clients)", conn)["name"].tolist()
138
- wanted = [
139
- "id", "name", "masked_name", "gender", "birth_year", "care_grade",
140
- "recognition_no", "recognition_number", "recognition_start_date", "recognition_end_date",
141
- "institution_type", "institution_name", "main_condition", "cognitive_status",
142
- "mobility_status", "diet_status", "toileting_status", "guardian_name", "guardian_phone",
143
- "notes", "status",
144
- ]
145
- select_cols = [c for c in wanted if c in cols]
146
- if not select_cols:
147
- conn.close()
148
- return pd.DataFrame()
149
- df = pd.read_sql_query("SELECT " + ", ".join(select_cols) + " FROM clients ORDER BY id DESC LIMIT 300", conn)
150
- conn.close()
151
- for col in wanted:
152
- if col not in df.columns:
153
- df[col] = ""
154
- df["recognition_no"] = df["recognition_no"].where(df["recognition_no"].astype(str).str.strip() != "", df["recognition_number"])
155
- return df
156
- except Exception:
157
- return pd.DataFrame()
158
-
159
-
160
- def safe(row, key, default=""):
161
- try:
162
- value = row.get(key, default)
163
- if pd.isna(value):
164
- return default
165
- return str(value)
166
- except Exception:
167
- return default
168
-
169
-
170
- def mask_name(name):
171
- name = str(name or "").strip()
172
- if len(name) >= 3:
173
- return name[0] + "○" + name[2:]
174
- if len(name) == 2:
175
- return name[0] + "○"
176
- return name or "미입력"
177
-
178
-
179
- def determine_plan_change(analysis):
180
- if analysis["risk_level"] == "고위험(악화)":
181
- return "급여제공계획 변경 검토 필요함"
182
- if analysis["risk_level"] == "경고(주의)" and (analysis["bpsd"] or analysis["community_needed"]):
183
- return "급여제공계획 부분 조정 검토 필요함"
184
- return "현 급여제공계획 유지 가능하나 다음 제공 시 재확인 필요함"
185
-
186
-
187
- def analyze_status_change(data):
188
- combined = " ".join([
189
- pick(data, "main_condition", "주요상태", "주요 질환 / 기존 상태"),
190
- pick(data, "physical_note", "신체상태", "신체영역 변화"),
191
- pick(data, "cognitive_note", "인지상태", "인지영역 변화"),
192
- pick(data, "emotion_note", "감정상태", "감정영역 변화"),
193
- pick(data, "relation_note", "관계상태", "관계영역 변화"),
194
- pick(data, "resource_note", "지역자원", "지역자원연계", "지역자원연계 필요"),
195
- pick(data, "field_memo", "현장메모", "현장 단순 메모"),
196
- pick(data, "guardian_opinion", "보호자의견", "보호자 의견"),
197
- ])
198
-
199
- domain_results = {}
200
- for domain, keywords in DOMAIN_KEYWORDS.items():
201
- if has_keyword(combined, keywords):
202
- domain_results[domain] = True
203
-
204
- bpsd = has_keyword(combined, BPSD_KEYWORDS)
205
- depression = has_keyword(combined, DEPRESSION_KEYWORDS)
206
- community_needed = has_keyword(combined, COMMUNITY_KEYWORDS)
207
-
208
- high = has_keyword(combined, HIGH_RISK_KEYWORDS) and has_keyword(combined, ["악화", "반복", "지속", "응급", "입원", "감염", "공격", "배회", "식사거부"])
209
- if high or (bpsd and has_keyword(combined, ["공격", "배회", "망상", "야간"])):
210
- risk_level = "고위험(악화)"
211
- elif has_keyword(combined, WARNING_KEYWORDS) or bpsd or depression or community_needed:
212
- risk_level = "경고(주의)"
213
- else:
214
- risk_level = "안정(유지)"
215
-
216
- flags = {
217
- "욕구사정 재실시 필요": risk_level == "고위험(악화)" or depression,
218
- "사례관리 연계 필요": risk_level == "고위험(악화)" or bpsd,
219
- "급여제공계획 변경 필요": risk_level != "안정(유지)",
220
- "보호자 상담 필요": bool(pick(data, "guardian_opinion", "보호자의견", "보호자 의견")) or risk_level != "안정(유지)",
221
- }
222
- return {
223
- "combined": combined,
224
- "domains": domain_results,
225
- "bpsd": bpsd,
226
- "depression": depression,
227
- "community_needed": community_needed,
228
- "risk_level": risk_level,
229
- "flags": flags,
230
- "plan_change": "",
231
- }
232
-
233
-
234
- def build_community_plan(analysis):
235
- if not analysis["community_needed"]:
236
- return "지역자원 연계는 즉시 필요성이 낮으나 독거, 결식, 복약 불이행, 주거환경 위험 발생 시 재검토 계획함"
237
- return "보건소, 치매안심센터, 행정복지센터, 복지관, 방문간호 등 지역자원 연계를 검토하고 보호자 및 담당자와 공유 계획함"
238
-
239
-
240
- def build_status_text(data):
241
- analysis = analyze_status_change(data)
242
- analysis["plan_change"] = determine_plan_change(analysis)
243
-
244
- domain_lines = []
245
- labels = {
246
- "신체": pick(data, "physical_note", "신체상태", "신체영역 변화"),
247
- "인지": pick(data, "cognitive_note", "인지상태", "인지영역 변화"),
248
- "감정": pick(data, "emotion_note", "감정상태", "감정영역 변화"),
249
- "관계": pick(data, "relation_note", "관계상태", "관계영역 변화"),
250
- "지역자원연계": pick(data, "resource_note", "지역자원", "지역자원연계", "지역자원연계 필요"),
251
- }
252
- for domain, note in labels.items():
253
- if analysis["domains"].get(domain) or note:
254
- domain_lines.append(f"- {domain}영역: {formal_sentence(note, '관련 변화 가능성이 확인되어 관찰이 요구됨')}")
255
- if not domain_lines:
256
- domain_lines.append("- 신체·인지·정서 전반: 급격한 악화 단서는 제한적이나 정기 관찰이 요구됨")
257
-
258
- bpsd_text = "BPSD 관련 단서가 확인되어 정면 반박을 지양하고 감정 공감, 짧은 설명, 화제 전환, 정서안정 지원을 실시함" if analysis["bpsd"] else "BPSD 고위험 단서는 제한적이나 인지·정서 변화 여부를 지속 관찰함"
259
- depression_text = "우울 위험 단서가 확인되어 사회적 고립감, 식사거부, 무기력 지속 여부를 확인하고 정서지원 강화가 요구됨" if analysis["depression"] else "우울 고위험 단서는 제한적이나 정서표현 변화 여부를 관찰함"
260
- community_text = build_community_plan(analysis)
261
- flag_text = " / ".join([f"□ {name}" for name, enabled in analysis["flags"].items() if enabled]) or "□ 정기 관찰 유지"
262
-
263
- soap = f"""
264
- [SOAP 구조 기록]
265
- S(주관적 정보): {formal_sentence(pick(data, 'guardian_opinion', '보호자의견', '보호자 의견') or pick(data, 'field_memo', '현장메모', '현장 단순 메모'), '수급자 및 보호자 호소는 추가 확인 예정임')}
266
- O(객관적 관찰): {formal_sentence(pick(data, 'field_memo', '현장메모', '현장 단순 메모'), '서비스 제공 중 행동, 표정, 이동, 식사, 배변 등 상태 관찰이 요구됨')}
267
- A(전문적 상태분석): 자동판정 결과 {analysis['risk_level']} 단계로 사정됨. {bpsd_text} {depression_text}
268
- P(조치 및 계획): {analysis['plan_change']} {community_text}
269
- """.strip()
270
-
271
- content = f"""
272
- [상태변화 자동판정]
273
- 판정: {analysis['risk_level']}
274
- 근거: 입력된 현장 메모, 보호자 의견, 수급자 기본상태를 종합하여 신체·인지·감정·관계·지역자원연계 영역으로 분석함.
275
-
276
- [5대 영역 분석]
277
- {chr(10).join(domain_lines)}
278
-
279
- [BPSD 및 우울 위험 분석]
280
- - 치매행동심리증상: {bpsd_text}
281
- - 우울 위험: {depression_text}
282
-
283
- [지역자원 연계계획]
284
- {community_text}
285
-
286
- {soap}
287
-
288
- [평가 연계]
289
- {flag_text}
290
- 급여제공계획 판단: {analysis['plan_change']}
291
- """.strip()
292
- return normalize_text(content), analysis
293
-
294
-
295
- def evaluate_record(content, analysis, photo_count):
296
- required = {
297
- "자동판정": "판정:" in content,
298
- "5대 영역": "5대 영역 분석" in content,
299
- "SOAP": "SOAP 구조 기록" in content,
300
- "BPSD": "BPSD" in content,
301
- "지역자원": "지역자원" in content,
302
- "평가 연계": "평가 연계" in content,
303
- "사진첨부 확인": photo_count >= 0,
304
- }
305
- score = int(sum(required.values()) / len(required) * 100)
306
- missing = [k for k, v in required.items() if not v]
307
- if score >= 90:
308
- comment = "평가 적합도가 높습니다. 상태판정, 영역분석, SOAP, 후속조치가 연결되어 있습니다."
309
- elif score >= 75:
310
- comment = "대체로 적합하나 일부 보완이 필요합니다."
311
- else:
312
- comment = "보완이 필요합니다. 상태변화 근거와 후속조치를 더 구체화해 주세요."
313
- return score, missing, comment
314
-
315
-
316
- def style_range(ws, cell_range, fill=None, font=None, alignment=None):
317
- thin = Side(style="thin", color="000000")
318
- border = Border(left=thin, right=thin, top=thin, bottom=thin)
319
- for row in ws[cell_range]:
320
- for cell in row:
321
- cell.border = border
322
- if fill:
323
- cell.fill = fill
324
- if font:
325
- cell.font = font
326
- if alignment:
327
- cell.alignment = alignment
328
-
329
-
330
- def generate_excel(data):
331
- wb = Workbook()
332
- ws = wb.active
333
- ws.title = "상태변화기록지"
334
- ws.sheet_view.showGridLines = False
335
- ws.page_setup.orientation = "portrait"
336
- ws.page_setup.fitToWidth = 1
337
- ws.page_setup.fitToHeight = 0
338
- ws.sheet_properties.pageSetUpPr.fitToPage = True
339
- ws.page_margins.left = 0.3
340
- ws.page_margins.right = 0.3
341
- ws.page_margins.top = 0.4
342
- ws.page_margins.bottom = 0.4
343
-
344
- title_fill = PatternFill("solid", fgColor="DBEAFE")
345
- header_fill = PatternFill("solid", fgColor="F3F4F6")
346
- for col in range(1, 10):
347
- ws.column_dimensions[get_column_letter(col)].width = 12
348
-
349
- ws.merge_cells("A1:G2")
350
- ws["A1"] = "상 태 변 화 기 록 지"
351
- ws["A1"].font = Font(name="Malgun Gothic", size=24, bold=True)
352
- ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
353
- ws["A1"].fill = title_fill
354
- ws["H1"] = "담당자"
355
- ws["I1"] = "시설장"
356
- ws["H2"] = ""
357
- ws["I2"] = ""
358
- for cell in ["H1", "I1"]:
359
- ws[cell].fill = header_fill
360
- ws[cell].font = Font(name="Malgun Gothic", size=11, bold=True)
361
- ws[cell].alignment = Alignment(horizontal="center", vertical="center")
362
- style_range(ws, "A1:I2")
363
- ws.row_dimensions[1].height = 28
364
- ws.row_dimensions[2].height = 45
365
-
366
- def info_row(row, a, b, c, d, e, f):
367
- ws.cell(row, 1).value = a
368
- ws.merge_cells(start_row=row, start_column=2, end_row=row, end_column=3)
369
- ws.cell(row, 2).value = b
370
- ws.cell(row, 4).value = c
371
- ws.merge_cells(start_row=row, start_column=5, end_row=row, end_column=6)
372
- ws.cell(row, 5).value = d
373
- ws.cell(row, 7).value = e
374
- ws.merge_cells(start_row=row, start_column=8, end_row=row, end_column=9)
375
- ws.cell(row, 8).value = f
376
- for col in [1, 4, 7]:
377
- ws.cell(row, col).fill = header_fill
378
- ws.cell(row, col).font = Font(name="Malgun Gothic", size=10, bold=True)
379
- ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
380
- for col in [2, 5, 8]:
381
- ws.cell(row, col).font = Font(name="Malgun Gothic", size=10)
382
- ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
383
- style_range(ws, f"A{row}:I{row}")
384
- ws.row_dimensions[row].height = 30
385
-
386
- r = 3
387
- info_row(r, "기관명", data["institution_name"], "기관유형", data["institution_type"], "작성일자", data["record_date"]); r += 1
388
- info_row(r, "수급자", data["masked_name"], "장기요양등급", data["care_grade"], "판정", data["risk_level"]); r += 1
389
- info_row(r, "인정번호", data["recognition_no"], "인정기간", data["recognition_period"], "사진첨부", data["photo_names"]); r += 1
390
-
391
- sections = [
392
- ("상태변화 기록내용", data["content"], 180),
393
- ("평가 적합도 점검", data["compliance_result"], 55),
394
- ("부족한 항목", data["missing_items"], 45),
395
- ]
396
- for title, body, min_height in sections:
397
- ws.cell(r, 1).value = title
398
- ws.cell(r, 1).fill = header_fill
399
- ws.cell(r, 1).font = Font(name="Malgun Gothic", size=11, bold=True)
400
- ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
401
- ws.merge_cells(start_row=r, start_column=2, end_row=r, end_column=9)
402
- ws.cell(r, 2).value = body
403
- ws.cell(r, 2).font = Font(name="Malgun Gothic", size=10)
404
- ws.cell(r, 2).alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
405
- lines = sum(max(1, len(line) // 55 + 1) for line in str(body).splitlines())
406
- ws.row_dimensions[r].height = max(min_height, min(360, lines * 19 + 20))
407
- style_range(ws, f"A{r}:I{r}")
408
- r += 1
409
-
410
- # 공통 안내문구 추가
411
- ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
412
- ws.cell(r, 1).value = "※ 생성된 문서는 그대로 사용하셔도 되며, 기관의 운영방침, 수급자 상태, 보호자 의견 및 평가기준에 따라 추가 보완사항은 얼마든지 수정 가능합니다. 기관의 상황에 맞게 활용하시기 바랍니다."
413
- style_range(ws, f"A{r}:I{r}",
414
- font=Font(name="Malgun Gothic", size=8.5, color="595959", italic=True),
415
- alignment=Alignment(horizontal="left", vertical="center", wrap_text=True))
416
- ws.row_dimensions[r].height = 36
417
- r += 1
418
-
419
- hidden = wb.create_sheet("내부점검자료_출력제외")
420
- hidden.sheet_state = "hidden"
421
- hidden["A1"] = "구분"
422
- hidden["B1"] = "내용"
423
- rows = [
424
- ("평가적합도", f"{data['evaluation_score']}점"),
425
- ("부족항목", data["missing_items"]),
426
- ("자동판정", data["risk_level"]),
427
- ("사진첨부", data["photo_names"]),
428
- ]
429
- for idx, (a, b) in enumerate(rows, 2):
430
- hidden.cell(idx, 1).value = a
431
- hidden.cell(idx, 2).value = b
432
- hidden.column_dimensions["A"].width = 20
433
- hidden.column_dimensions["B"].width = 90
434
-
435
- output = BytesIO()
436
- wb.save(output)
437
- output.seek(0)
438
- return output
439
-
440
-
441
- clients_df = load_clients()
442
- if DB_PATH:
443
- st.sidebar.success("Client Center DB 연결됨")
444
- st.sidebar.caption(DB_PATH)
445
- else:
446
- st.sidebar.warning("Client Center DB를 찾지 못해 직접입력으로 실행됩니다.")
447
-
448
- mode = st.radio("입력 방식", ["Client Center에서 수급자 선택", "직접 입력"], horizontal=True)
449
- selected_client = None
450
- if mode == "Client Center에서 수급자 선택" and not clients_df.empty:
451
- options = clients_df.apply(lambda x: f"{x['id']}번 - {x['masked_name']} / {x['care_grade']} / {x['institution_type']} / {x['status']}", axis=1).tolist()
452
- option = st.selectbox("수급자 선택", options)
453
- selected_id = int(option.split("번")[0])
454
- selected_client = clients_df[clients_df["id"] == selected_id].iloc[0]
455
- elif mode == "Client Center에서 수급자 선택":
456
- st.warning("등록된 수급자 정보가 없거나 DB를 찾지 못했습니다. 직접 입력으로 작성할 수 있습니다.")
457
-
458
- if selected_client is not None:
459
- default_name = safe(selected_client, "name")
460
- default_masked = safe(selected_client, "masked_name") or mask_name(default_name)
461
- default_grade = safe(selected_client, "care_grade", "3등급")
462
- default_inst = safe(selected_client, "institution_type", "방문요양")
463
- default_inst_name = safe(selected_client, "institution_name")
464
- default_main = safe(selected_client, "main_condition")
465
- default_cog = safe(selected_client, "cognitive_status")
466
- default_mob = safe(selected_client, "mobility_status")
467
- default_diet = safe(selected_client, "diet_status")
468
- default_toilet = safe(selected_client, "toileting_status")
469
- recognition_no = safe(selected_client, "recognition_no") or safe(selected_client, "recognition_number") or "미입력"
470
- recognition_period = " ~ ".join([x for x in [safe(selected_client, "recognition_start_date"), safe(selected_client, "recognition_end_date")] if x]) or "미입력"
471
- else:
472
- default_name = "홍길동"
473
- default_masked = "홍○동"
474
- default_grade = "3등급"
475
- default_inst = "방문요양"
476
- default_inst_name = ""
477
- default_main = ""
478
- default_cog = ""
479
- default_mob = ""
480
- default_diet = ""
481
- default_toilet = ""
482
- recognition_no = "미입력"
483
- recognition_period = "미입력"
484
-
485
- st.subheader("1단계. 기본정보")
486
- col1, col2, col3, col4 = st.columns(4)
487
- name = col1.text_input("수급자 성명", default_name)
488
- masked_name = col2.text_input("가명 표기", default_masked or mask_name(name))
489
- care_grade = col3.selectbox("장기요양등급", ["1등급", "2등급", "3등급", "4등급", "5등급", "인지지원등급", "미입력"], index=2 if default_grade not in ["1등급", "2등급", "4등급", "5등급", "인지지원등급", "미입력"] else ["1등급", "2등급", "3등급", "4등급", "5등급", "인지지원등급", "미입력"].index(default_grade))
490
- record_date = col4.date_input("작성일자", value=date.today())
491
-
492
- col5, col6 = st.columns(2)
493
- institution_type = col5.selectbox("기관유형", ["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"], index=["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"].index(default_inst) if default_inst in ["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"] else 0)
494
- institution_name = col6.text_input("기관명", default_inst_name)
495
-
496
- st.subheader("2단계. 상태변화 입력")
497
- c1, c2 = st.columns(2)
498
- main_condition = c1.text_area("주요 질환 / 기존 상태", value=default_main, height=85)
499
- physical_note = c2.text_area("신체영역 변화", value=f"{default_mob} {default_diet} {default_toilet}".strip(), height=85, placeholder="예: 보행 시 휘청거림, 식사량 감소, 요실금 증가")
500
- c3, c4 = st.columns(2)
501
- cognitive_note = c3.text_area("인지영역 변화", value=default_cog, height=85, placeholder="예: 반복질문, 귀가욕구, 도둑망상, 배회")
502
- emotion_note = c4.text_area("감정영역 변화", height=85, placeholder="예: 우울, 눈물, 무기력, 불안")
503
- c5, c6 = st.columns(2)
504
- relation_note = c5.text_area("관계영역 변화", height=85, placeholder="예: 타 수급자와 말다툼, 보호자 갈등")
505
- resource_note = c6.text_area("지역자원연계 필요", height=85, placeholder="예: 독거, 결식, 복약 불이행, 경제적 어려움")
506
- field_memo = st.text_area("현장 단순 메모", height=100, placeholder="예: 오후에 도둑년이라고 욕하고 화냄. 식사 절반만 드심.")
507
- guardian_opinion = st.text_area("보호자 의견", height=85, placeholder="예: 최근 밤에 잠을 못 자고 배회가 늘어 걱정된다고 함")
508
-
509
- photo_files = st.file_uploader("사진첨부", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
510
- photo_names = ", ".join([f.name for f in photo_files]) if photo_files else "첨부 없음"
511
-
512
- if st.button("📈 상태변화기록지 생성", use_container_width=True):
513
- input_data = {
514
- "main_condition": main_condition,
515
- "physical_note": physical_note,
516
- "cognitive_note": cognitive_note,
517
- "emotion_note": emotion_note,
518
- "relation_note": relation_note,
519
- "resource_note": resource_note,
520
- "field_memo": field_memo,
521
- "guardian_opinion": guardian_opinion,
522
- }
523
- content, analysis = build_status_text(input_data)
524
- score, missing, result = evaluate_record(content, analysis, len(photo_files or []))
525
- st.session_state["status_change_data"] = {
526
- "name": name,
527
- "masked_name": masked_name,
528
- "care_grade": care_grade,
529
- "institution_type": institution_type,
530
- "institution_name": institution_name,
531
- "record_date": str(record_date),
532
- "recognition_no": recognition_no,
533
- "recognition_period": recognition_period,
534
- "risk_level": analysis["risk_level"],
535
- "content": content,
536
- "evaluation_score": score,
537
- "missing_items": ", ".join(missing) if missing else "부족 항목 없음",
538
- "compliance_result": result,
539
- "photo_names": photo_names,
540
- }
541
- st.session_state.pop("status_change_excel", None)
542
-
543
- if "status_change_data" in st.session_state:
544
- data = st.session_state["status_change_data"]
545
- st.markdown("---")
546
- st.subheader("3단계. 생성 결과 확인/수정")
547
- data["content"] = st.text_area("상태변화 기록내용", value=data["content"], height=340)
548
- a, b, c = st.columns(3)
549
- a.metric("자동판정", data["risk_level"])
550
- b.metric("평가 적합도", f"{data['evaluation_score']}점")
551
- c.metric("결재라인", "담당자/시설장 2칸")
552
- if data["evaluation_score"] >= 90:
553
- st.markdown(f'<div class="okbox">{data["compliance_result"]}</div>', unsafe_allow_html=True)
554
- else:
555
- st.markdown(f'<div class="warnbox">{data["compliance_result"]}</div>', unsafe_allow_html=True)
556
-
557
- if st.button("📦 엑셀 파일 준비하기", use_container_width=True):
558
- st.session_state["status_change_excel"] = generate_excel(data)
559
- st.success("엑셀 파일이 준비되었습니다.")
560
-
561
- if "status_change_excel" in st.session_state:
562
- filename = f"상태변화기록지_{data['masked_name']}_{datetime.now().strftime('%Y%m%d')}.xlsx"
563
- st.download_button(
564
- "📥 상태변화기록지 엑셀 다운로드",
565
- data=st.session_state["status_change_excel"],
566
- file_name=filename,
567
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
568
- use_container_width=True,
569
- )
570
-
571
- st.caption("상태변화기록지 최종본: Client Center 연동, 직접입력, 사진첨부, 엑셀 다운로드, 결재라인 2칸, 내부점검 숨김시트를 포함합니다.")