devorah-ai-2016 commited on
Commit
4e73432
·
verified ·
1 Parent(s): 48e102e

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1184
app.py DELETED
@@ -1,1184 +0,0 @@
1
- import os
2
- import sqlite3
3
- import random
4
- from io import BytesIO
5
- from datetime import date, datetime, timedelta
6
-
7
- import pandas as pd
8
- import streamlit as st
9
- from openpyxl import Workbook
10
- from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
11
- from openpyxl.utils import get_column_letter
12
-
13
-
14
- # =========================================================
15
- # 통합연동 급여제공계획서 + 결과평가서 생성기
16
- # Client Center + 상담일지 + 욕구사정 + 사례관리 연동형
17
- # =========================================================
18
-
19
- st.set_page_config(
20
- page_title="급여계획/결과평가",
21
- page_icon="📄",
22
- layout="wide"
23
- )
24
-
25
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
26
-
27
- DB_CANDIDATES = [
28
- os.path.join(CURRENT_DIR, "data", "clients.db"),
29
- os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"),
30
- os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"),
31
- os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"),
32
- ]
33
-
34
- DB_PATH = None
35
- for p in DB_CANDIDATES:
36
- if os.path.exists(p):
37
- DB_PATH = p
38
- break
39
-
40
- if DB_PATH is None:
41
- os.makedirs(os.path.join(CURRENT_DIR, "data"), exist_ok=True)
42
- DB_PATH = os.path.join(CURRENT_DIR, "data", "clients.db")
43
-
44
-
45
- # -----------------------------
46
- # UI
47
- # -----------------------------
48
- st.markdown("""
49
- <style>
50
- .block-container { padding-top: 2rem; }
51
- .hero {
52
- background: linear-gradient(135deg, #0F766E, #14B8A6);
53
- padding: 30px;
54
- border-radius: 22px;
55
- text-align: center;
56
- margin-bottom: 22px;
57
- }
58
- .hero h1 { color: white; margin: 0; font-size: 36px; }
59
- .hero p { color: white; margin-top: 10px; font-size: 18px; }
60
- .notice {
61
- background: #fff8e1;
62
- border-left: 7px solid #f9a825;
63
- padding: 15px;
64
- border-radius: 12px;
65
- margin-bottom: 18px;
66
- }
67
- .okbox {
68
- background: #e8f5e9;
69
- border-left: 7px solid #43a047;
70
- padding: 14px;
71
- border-radius: 12px;
72
- margin-bottom: 14px;
73
- }
74
- .warnbox {
75
- background: #fff3e0;
76
- border-left: 7px solid #fb8c00;
77
- padding: 14px;
78
- border-radius: 12px;
79
- margin-bottom: 14px;
80
- }
81
- </style>
82
- """, unsafe_allow_html=True)
83
-
84
- st.markdown("""
85
- <div class="hero">
86
- <h1>📄 급여제공계획서 · 결과평가서 생성기</h1>
87
- <p>수급자 정보, 상담일지, 욕구사정, 사례관리를 통합 연동하여 계획서와 기관유형별 결과평가서를 생성합니다.</p>
88
- </div>
89
- """, unsafe_allow_html=True)
90
-
91
- st.markdown("""
92
- <div class="notice">
93
- <strong>통합연동:</strong> Client Center → 상담일지 → 욕구사정 → 사례관리 → 급여제공계획서 → 결과평가서
94
- </div>
95
- """, unsafe_allow_html=True)
96
-
97
- st.markdown("""
98
- <div class="notice">
99
- ※ 생성된 문서는 그대로 사용하셔도 되며, 기관의 운영방침, 수급자 상태, 보호자 의견 및 평가기준에 따라 추가 보완사항은 얼마든지 수정 가능합니다. 기관의 상황에 맞게 활용하시기 바랍니다.
100
- </div>
101
- """, unsafe_allow_html=True)
102
-
103
-
104
- # -----------------------------
105
- # DB
106
- # -----------------------------
107
- def get_conn():
108
- return sqlite3.connect(DB_PATH, check_same_thread=False)
109
-
110
-
111
- def init_db():
112
- conn = get_conn()
113
- cur = conn.cursor()
114
-
115
- cur.execute("""
116
- CREATE TABLE IF NOT EXISTS clients (
117
- id INTEGER PRIMARY KEY AUTOINCREMENT,
118
- name TEXT NOT NULL,
119
- masked_name TEXT,
120
- gender TEXT,
121
- birth_year TEXT,
122
- care_grade TEXT,
123
- institution_type TEXT,
124
- institution_name TEXT,
125
- admission_date TEXT,
126
- main_condition TEXT,
127
- cognitive_status TEXT,
128
- mobility_status TEXT,
129
- diet_status TEXT,
130
- toileting_status TEXT,
131
- guardian_name TEXT,
132
- guardian_phone TEXT,
133
- notes TEXT,
134
- status TEXT DEFAULT '이용중',
135
- created_at TEXT,
136
- updated_at TEXT
137
- )
138
- """)
139
-
140
- # Client Center 최종 양식과 맞춤: 인정번호/인정기간 컬럼이 없는 기존 DB도 자동 보완
141
- new_client_columns = {
142
- "recognition_no": "TEXT",
143
- "recognition_number": "TEXT",
144
- "recognition_start_date": "TEXT",
145
- "recognition_end_date": "TEXT",
146
- }
147
- cur.execute("PRAGMA table_info(clients)")
148
- existing_cols = [row[1] for row in cur.fetchall()]
149
- for col, col_type in new_client_columns.items():
150
- if col not in existing_cols:
151
- cur.execute(f"ALTER TABLE clients ADD COLUMN {col} {col_type}")
152
-
153
- cur.execute("""
154
- CREATE TABLE IF NOT EXISTS counseling_records (
155
- id INTEGER PRIMARY KEY AUTOINCREMENT,
156
- client_id INTEGER,
157
- client_masked_name TEXT,
158
- institution_type TEXT,
159
- counseling_type TEXT,
160
- counseling_date TEXT,
161
- counselor TEXT,
162
- counseling_reason TEXT,
163
- client_status_change TEXT,
164
- guardian_opinion TEXT,
165
- counseling_content TEXT,
166
- action_plan TEXT,
167
- evaluation_score INTEGER,
168
- missing_items TEXT,
169
- compliance_result TEXT,
170
- next_due_note TEXT,
171
- created_at TEXT
172
- )
173
- """)
174
-
175
- cur.execute("""
176
- CREATE TABLE IF NOT EXISTS needs_assessment_records (
177
- id INTEGER PRIMARY KEY AUTOINCREMENT,
178
- client_id INTEGER,
179
- client_masked_name TEXT,
180
- assessment_date TEXT,
181
- total_opinion TEXT,
182
- result_status TEXT,
183
- result_plan TEXT,
184
- fall_risk TEXT,
185
- bedsore_risk TEXT,
186
- dementia_status TEXT,
187
- diaper_use TEXT,
188
- service_needs TEXT,
189
- created_at TEXT
190
- )
191
- """)
192
-
193
- cur.execute("""
194
- CREATE TABLE IF NOT EXISTS case_management_records (
195
- id INTEGER PRIMARY KEY AUTOINCREMENT,
196
- client_id INTEGER,
197
- client_masked_name TEXT,
198
- institution_type TEXT,
199
- case_type TEXT,
200
- first_meeting_date TEXT,
201
- second_meeting_date TEXT,
202
- selection_reason TEXT,
203
- first_plan TEXT,
204
- implementation_process TEXT,
205
- second_result TEXT,
206
- evaluation_score INTEGER,
207
- missing_items TEXT,
208
- created_at TEXT
209
- )
210
- """)
211
-
212
- cur.execute("""
213
- CREATE TABLE IF NOT EXISTS care_plan_records (
214
- id INTEGER PRIMARY KEY AUTOINCREMENT,
215
- client_id INTEGER,
216
- client_masked_name TEXT,
217
- institution_type TEXT,
218
- plan_date TEXT,
219
- evaluation_date TEXT,
220
- care_plan TEXT,
221
- result_evaluation TEXT,
222
- evaluation_score INTEGER,
223
- missing_items TEXT,
224
- created_at TEXT
225
- )
226
- """)
227
-
228
- conn.commit()
229
- conn.close()
230
-
231
-
232
- def load_table(query, params=()):
233
- conn = get_conn()
234
- try:
235
- df = pd.read_sql_query(query, conn, params=params)
236
- except Exception:
237
- df = pd.DataFrame()
238
- conn.close()
239
- return df
240
-
241
-
242
- def load_clients():
243
- df = load_table("SELECT * FROM clients ORDER BY id DESC LIMIT 300")
244
- if not df.empty:
245
- if "recognition_no" not in df.columns:
246
- df["recognition_no"] = ""
247
- if "recognition_number" not in df.columns:
248
- df["recognition_number"] = ""
249
- df["recognition_no"] = df["recognition_no"].where(df["recognition_no"].astype(str).str.strip() != "", df["recognition_number"])
250
- return df
251
-
252
-
253
- def load_counseling(client_id):
254
- return load_table(
255
- "SELECT * FROM counseling_records WHERE client_id=? ORDER BY counseling_date DESC, id DESC",
256
- (int(client_id),)
257
- )
258
-
259
-
260
- def load_needs(client_id):
261
- return load_table(
262
- "SELECT * FROM needs_assessment_records WHERE client_id=? ORDER BY assessment_date DESC, id DESC",
263
- (int(client_id),)
264
- )
265
-
266
-
267
- def load_cases(client_id):
268
- return load_table(
269
- "SELECT * FROM case_management_records WHERE client_id=? ORDER BY id DESC",
270
- (int(client_id),)
271
- )
272
-
273
-
274
- def save_plan_record(data):
275
- conn = get_conn()
276
- cur = conn.cursor()
277
- cur.execute("""
278
- INSERT INTO care_plan_records (
279
- client_id, client_masked_name, institution_type, plan_date, evaluation_date,
280
- care_plan, result_evaluation, evaluation_score, missing_items, created_at
281
- )
282
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
283
- """, (
284
- data["client_id"],
285
- data["client_masked_name"],
286
- data["institution_type"],
287
- data["plan_date"],
288
- data["evaluation_date"],
289
- data["care_plan"],
290
- data["result_evaluation"],
291
- data["evaluation_score"],
292
- data["missing_items"],
293
- datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
294
- ))
295
- conn.commit()
296
- conn.close()
297
-
298
-
299
- def safe(row, key, default=""):
300
- try:
301
- v = row.get(key, default)
302
- if pd.isna(v):
303
- return default
304
- return str(v)
305
- except Exception:
306
- return default
307
-
308
-
309
- def mask_name(name):
310
- name = str(name or "").strip()
311
- if len(name) >= 3:
312
- return name[0] + "○" + name[2:]
313
- if len(name) == 2:
314
- return name[0] + "○"
315
- return name
316
-
317
-
318
- def parse_date_safe(value):
319
- """YYYY-MM-DD 형식 날짜를 안전하게 변환합니다."""
320
- try:
321
- if not value:
322
- return None
323
- return pd.to_datetime(str(value)).date()
324
- except Exception:
325
- return None
326
-
327
-
328
- def get_recognition_info(client):
329
- recognition_no = safe(client, "recognition_no") or safe(client, "recognition_number")
330
- recognition_start = safe(client, "recognition_start_date")
331
- recognition_end = safe(client, "recognition_end_date")
332
- recognition_period = " ~ ".join([v for v in [recognition_start, recognition_end] if v]) or "미입력"
333
- return recognition_no or "미입력", recognition_start, recognition_end, recognition_period
334
-
335
-
336
- def recognition_due_alert(recognition_end, base_date=None):
337
- base_date = base_date or date.today()
338
- end_date = parse_date_safe(recognition_end)
339
- if not end_date:
340
- return "인정기간 종료일 미입력"
341
- remain = (end_date - base_date).days
342
- if remain < 0:
343
- return f"인정기간 만료 {abs(remain)}일 경과"
344
- if remain <= 30:
345
- return f"인정기간 종료 {remain}일 전: 급여계획 재점검 필요"
346
- if remain <= 90:
347
- return f"인정기간 종료 {remain}일 전: 갱신 및 재작성 검토"
348
- return f"인정기간 종료까지 {remain}일 남음"
349
-
350
-
351
- init_db()
352
-
353
-
354
- # -----------------------------
355
- # 기관유형별 설계
356
- # -----------------------------
357
- INSTITUTION_RESULT_POINTS = {
358
- "방문요양": {
359
- "focus": ["신체활동지원", "가사지원", "인지활동", "복약확인", "낙상예방", "보호자 상담"],
360
- "result_view": "재가생활 유지, 서비스 시간·내용 적정성, 요양보호사 관찰내용, 보호자 의견 반영 여부를 중심으로 평가합니다.",
361
- },
362
- "방문목욕": {
363
- "focus": ["피부상태", "목욕협조도", "청결상태", "목욕 전후 건강상태", "이동안전", "보호자 협조"],
364
- "result_view": "목욕 전후 상태 변화, 피부 발적·상처 여부, 목욕 거부감 완화, 이동 안전성을 중심으로 평가합니다.",
365
- },
366
- "방문간호": {
367
- "focus": ["혈압/혈당", "상처관리", "투약관리", "건강상담", "의료기관 연계", "보호자 교육"],
368
- "result_view": "건강지표 변화, 간호처치 이행, 이상징후 관리, 의료연계 필요성 및 보호자 교육 반영을 중심으로 평가합니다.",
369
- },
370
- "주간보호": {
371
- "focus": ["프로그램 참여", "식사", "배변", "정서상태", "사회성", "송영안전", "낙상예방"],
372
- "result_view": "센터 이용 적응, 프로그램 참여도, 식사·배변 상태, 대인관계와 정서 변화를 중심으로 평가합니다.",
373
- },
374
- "요양원": {
375
- "focus": ["식사", "수면", "욕창", "낙상", "인지", "배변", "프로그램", "정서지원"],
376
- "result_view": "입소생활 전반의 안정성, 욕창·낙상 예방, 식사·수면·배변 변화, 다직종 관찰 내용을 중심으로 평가합니다.",
377
- },
378
- "공동생활가정": {
379
- "focus": ["생활적응", "정서안정", "사회관계", "프로그램 참여", "식사", "개별 욕구", "가족소통"],
380
- "result_view": "소규모 공동생활 적응, 정서 안정, 타 수급자와의 관계, 개별 욕구 반영 여부를 중심으로 평가합니다.",
381
- },
382
- }
383
-
384
- VARIATION_PHRASES = [
385
- "상담기록과 욕구사정에서 확인된 내용을 바탕으로",
386
- "최근 상태 변화와 보호자 의견을 함께 고려하여",
387
- "수급자의 현재 기능상태와 서비스 이용 흐름을 종합하여",
388
- "사례관리 과정에서 확인된 위험요인과 욕구를 반영하여",
389
- "기관유형별 급여제공 특성과 수급자 개별 상태를 연결하여",
390
- ]
391
-
392
- CARE_DOMAINS = [
393
- "신체활동지원",
394
- "인지·정서지원",
395
- "식사·영양관리",
396
- "배변·위생관리",
397
- "이동·낙상예방",
398
- "건강상태 관찰",
399
- "보호자 상담 및 정보공유",
400
- "사례관리 연계",
401
- ]
402
-
403
- RESTRICTED_REPLACEMENTS = {
404
- "상태 양호": "신체·인지·정서 상태가 비교적 안정적으로 유지되나 정기 관찰이 필요함",
405
- "특이사항 없음": "즉시 개입이 필요한 돌발 상황은 확인되지 않았으나 상태변화 관찰을 지속함",
406
- "문제 없음": "현재 확인된 위험요인은 제한적이나 급여제공 과정에서 재확인이 필요함",
407
- "이전과 동일": "기존 상태의 급격한 악화는 제한적이나 기능 유지 여부를 구체적으로 확인함",
408
- "잘 지냄": "일상생활 적응은 비교적 안정적이나 개별 욕구와 위험요인 관찰을 지속함",
409
- }
410
-
411
- FALL_KEYWORDS = ["낙상", "넘어", "미끄", "어지", "현기", "휘청", "보행", "부축", "워커", "지팡", "휠체어", "문턱", "화장실", "송영"]
412
- PRESSURE_KEYWORDS = ["욕창", "발적", "상처", "피부", "와상", "침상", "체위", "드레싱", "삼출", "감염", "영양", "탈수"]
413
- DEMENTIA_KEYWORDS = ["치매", "인지", "기억", "반복", "배회", "망상", "환각", "불안", "공격", "귀가", "집에", "혼동", "고집"]
414
- NUTRITION_KEYWORDS = ["식사량", "식욕", "체중", "영양", "사레", "연하", "탈수"]
415
- GUARDIAN_KEYWORDS = ["보호자", "가족", "걱정", "부담", "소진", "요청", "변경", "확대", "동행", "상담"]
416
-
417
-
418
- def normalize_document_text(text):
419
- value = str(text or "").strip()
420
- for bad, good in RESTRICTED_REPLACEMENTS.items():
421
- value = value.replace(bad, good)
422
- return value
423
-
424
-
425
- def has_keyword(text, keywords):
426
- source = str(text or "")
427
- return any(keyword in source for keyword in keywords)
428
-
429
-
430
- def build_risk_context(client, counseling_summary, needs_summary, case_summary, manual_goal, manual_guardian, observed_change=""):
431
- combined = " ".join([
432
- safe(client, "main_condition"),
433
- safe(client, "cognitive_status"),
434
- safe(client, "mobility_status"),
435
- safe(client, "diet_status"),
436
- safe(client, "toileting_status"),
437
- counseling_summary,
438
- needs_summary,
439
- case_summary,
440
- manual_goal,
441
- manual_guardian,
442
- observed_change,
443
- ])
444
-
445
- fall = has_keyword(combined, FALL_KEYWORDS)
446
- pressure = has_keyword(combined, PRESSURE_KEYWORDS)
447
- dementia = has_keyword(combined, DEMENTIA_KEYWORDS)
448
- nutrition = has_keyword(combined, NUTRITION_KEYWORDS)
449
- guardian = has_keyword(combined, GUARDIAN_KEYWORDS)
450
- case_needed = has_keyword(combined, ["사례관리", "위험", "악화", "반복", "입원", "응급", "갈등", "공격", "거부"])
451
-
452
- score = sum([fall, pressure, dementia, nutrition, guardian, case_needed])
453
- if score >= 4 or has_keyword(combined, ["응급", "입원", "고위험", "공격", "망상", "삼출", "감염"]):
454
- level = "고위험"
455
- elif score >= 2:
456
- level = "중위험"
457
- else:
458
- level = "저위험"
459
-
460
- flags = {
461
- "상태변화기록 작성 필요": fall or pressure or dementia or nutrition,
462
- "사례관리 검토 필요": case_needed or level == "고위험",
463
- "급여제공계획 변경 검토": score >= 2,
464
- "보호자 상담 필요": guardian,
465
- }
466
-
467
- plans = []
468
- if fall:
469
- plans.append("낙상예방을 위해 이동 동선, 화장실 이용, 보행보조기 사용, 직원 동행 여부를 급여제공기록과 함께 점검함")
470
- if pressure:
471
- plans.append("욕창예방을 위해 피부상태, 체위변경, 압박부위, 영양·수분섭취, 감염 징후를 정기 확인함")
472
- if dementia:
473
- plans.append("인지저하 및 BPSD 예방을 위해 반복 안내, 감정 공감, 정면 반박 금지, 익숙한 활동 전환을 적용함")
474
- if nutrition:
475
- plans.append("영양관리를 위해 식사량, 사레 여부, 수분섭취, 체중 변화 및 보호자 공유 필요성을 확인함")
476
- if guardian:
477
- plans.append("보호자 의견을 정기 상담에 반영하고 서비스 조정 가능 범위와 상태변화 공유 절차를 안내함")
478
- if not plans:
479
- plans.append("현재 급여제공 방향을 유지하되 기능 유지와 상태변화 여부를 정기적으로 확인함")
480
-
481
- return {
482
- "combined": combined,
483
- "level": level,
484
- "fall": fall,
485
- "pressure": pressure,
486
- "dementia": dementia,
487
- "nutrition": nutrition,
488
- "guardian": guardian,
489
- "flags": flags,
490
- "plans": plans,
491
- "flag_text": " / ".join([f"□ {name}" for name, enabled in flags.items() if enabled]) or "□ 정기 관찰 유지",
492
- }
493
-
494
-
495
- # -----------------------------
496
- # 연동 요약
497
- # -----------------------------
498
- def summarize_counseling(df):
499
- if df.empty:
500
- return "저장된 상담일지가 없어 수급자 기본정보와 직접 입력한 보호자 의견을 중심으로 반영함."
501
- lines = []
502
- for _, r in df.head(5).iterrows():
503
- lines.append(
504
- f"- {safe(r, 'counseling_date')}: {safe(r, 'counseling_type')} / "
505
- f"사유: {safe(r, 'counseling_reason')} / 상태변화: {safe(r, 'client_status_change') or '기록 없음'} / "
506
- f"보호자 의견: {safe(r, 'guardian_opinion') or '기록 없음'}"
507
- )
508
- return "\n".join(lines)
509
-
510
-
511
- def summarize_needs(df, client, manual_needs):
512
- if not df.empty:
513
- r = df.iloc[0]
514
- return (
515
- f"최근 욕구사정일: {safe(r, 'assessment_date')}\n"
516
- f"총평: {safe(r, 'total_opinion')}\n"
517
- f"위험요인: 낙상 {safe(r, 'fall_risk')}, 욕창 {safe(r, 'bedsore_risk')}, "
518
- f"인지 {safe(r, 'dementia_status')}, 기저귀 {safe(r, 'diaper_use')}\n"
519
- f"서비스 욕구: {safe(r, 'service_needs')}\n"
520
- f"결과/계획: {safe(r, 'result_plan')}"
521
- )
522
- base = (
523
- f"Client Center 등록정보 기준: 주요상태 {safe(client, 'main_condition') or '기록 없음'}, "
524
- f"인지상태 {safe(client, 'cognitive_status') or '기록 없음'}, "
525
- f"이동상태 {safe(client, 'mobility_status') or '기록 없음'}, "
526
- f"식사상태 {safe(client, 'diet_status') or '기록 없음'}, "
527
- f"배변상태 {safe(client, 'toileting_status') or '기록 없음'}"
528
- )
529
- if manual_needs.strip():
530
- base += "\n직접 입력 욕구사정 요약: " + manual_needs.strip()
531
- return base
532
-
533
-
534
- def summarize_case(df):
535
- if df.empty:
536
- return "저장된 사례관리 기록이 없어 상담일지와 욕구사정 내용을 중심으로 급여제공계획을 수립함."
537
- r = df.iloc[0]
538
- return (
539
- f"사례유형: {safe(r, 'case_type')}\n"
540
- f"선정사유: {safe(r, 'selection_reason')}\n"
541
- f"1차 계��: {safe(r, 'first_plan')}\n"
542
- f"시행과정: {safe(r, 'implementation_process')}\n"
543
- f"2차 결과: {safe(r, 'second_result')}"
544
- )
545
-
546
- from care_rules import get_concrete_services, format_domain_plan
547
-
548
-
549
- # -----------------------------
550
- # 생성 로직
551
- # -----------------------------
552
- def make_care_plan(client, institution_type, counseling_summary, needs_summary, case_summary, manual_goal, manual_guardian):
553
- phrase = random.choice(VARIATION_PHRASES)
554
- points = INSTITUTION_RESULT_POINTS[institution_type]["focus"]
555
- points_text = ", ".join(points)
556
- risk_context = build_risk_context(client, counseling_summary, needs_summary, case_summary, manual_goal, manual_guardian)
557
-
558
- goal_intro = manual_goal.strip() or (
559
- f"{safe(client, 'masked_name')} 수급자의 현재 상태를 유지·개선하고, "
560
- f"{institution_type} 서비스 특성에 맞추어 {points_text} 영역을 안정적으로 지원한다."
561
- )
562
-
563
- # 개인별이용계획규칙(care_rules) 적용하여 추상적인 표현 방지 및 구체적 지원방법 자동 매핑
564
- concrete_dict = get_concrete_services(
565
- client,
566
- fall_risk=safe(client, "fall_risk", "낮음") or "낮음",
567
- bedsore_risk=safe(client, "bedsore_risk", "낮음") or "낮음",
568
- diaper_use=safe(client, "diaper_use", "미사용") or "미사용",
569
- custom_notes=manual_guardian
570
- )
571
- domain_lines = format_domain_plan(concrete_dict)
572
-
573
- risk_lines = "\n".join([f"- {line}" for line in risk_context["plans"]])
574
-
575
- care_plan = f"""
576
- [급여제공 목표]
577
- {goal_intro}
578
-
579
- [연동 근거]
580
- {phrase} 급여제공계획을 수립함.
581
-
582
- 1) 상담일지 반영
583
- {counseling_summary}
584
-
585
- 2) 욕구사정 반영
586
- {needs_summary}
587
-
588
- 3) 사례관리 반영
589
- {case_summary}
590
-
591
- [기관유형별 중점 급여]
592
- {institution_type}의 중점 평가 영역은 {points_text}이며, 해당 항목이 서비스 제공과 기록에 반영되도록 한다.
593
-
594
- [위험요인 및 연계 판단]
595
- - 종합 위험도는 {risk_context['level']}으로 판단됨.
596
- - 연계 판단: {risk_context['flag_text']}
597
- {risk_lines}
598
-
599
- [세부 급여제공계획]
600
- {chr(10).join(domain_lines)}
601
-
602
- [급여제공 방법]
603
- - 담당자는 수급자의 상태변화와 보호자 의견을 정기적으로 확인한다.
604
- - 종사자는 제공한 서비스와 관찰사항을 급여제공기록지에 구체적으로 남긴다.
605
- - 위험요인 발생 시 상담일지, 욕구사정, 사례관리 회의와 연결하여 재점검한다.
606
- - 필요 시 급여제공계획서를 수정하고 변경 사유를 기록한다.
607
- """.strip()
608
-
609
- return normalize_document_text(care_plan)
610
-
611
-
612
- def make_result_evaluation(client, institution_type, care_plan, counseling_summary, needs_summary, case_summary, result_level, observed_change):
613
- point_info = INSTITUTION_RESULT_POINTS[institution_type]
614
- points = point_info["focus"]
615
- view = point_info["result_view"]
616
- risk_context = build_risk_context(client, counseling_summary, needs_summary, case_summary, "", "", observed_change)
617
-
618
- result_lines = []
619
- for p in points:
620
- if institution_type == "방문목욕":
621
- text = f"{p} 항목은 목욕 전후 상태와 안전 여부를 기준으로 확인하였으며, 특이사항은 보호자 및 담당자에게 공유함."
622
- elif institution_type == "방문간호":
623
- text = f"{p} 항목은 간호 관찰 및 건강상담 결과를 중심으로 확인하였고, 필요 시 의료연계 여부를 검토함."
624
- elif institution_type == "주간보호":
625
- text = f"{p} 항목은 센터 이용 중 관찰기록, 프로그램 참여, 식사·배변·정서 변화를 기준으로 평가함."
626
- elif institution_type == "요양원":
627
- text = f"{p} 항목은 입소생활 전반의 관찰, 다직종 공유, 급여제공기록 반영 여부를 기준으로 평가함."
628
- elif institution_type == "공동생활가정":
629
- text = f"{p} 항목은 공동생활 적응, 대인관계, 정서 안정, 가족소통 여부를 중심으로 평가함."
630
- else:
631
- text = f"{p} 항목은 방문서비스 제공 중 관찰사항과 보호자 의견을 기준으로 평가함."
632
- result_lines.append(f"- {p}: {text}")
633
-
634
- change_text = observed_change.strip() or "급여제공계획에 따른 서비스 제공 후 상태를 관찰한 결과, 급격한 악화는 확인되지 않았으며 지속적인 관찰과 개별지원이 필요함."
635
- achievement_text = {
636
- "목표달성": "계획한 급여제공 목표가 대체로 달성된 것으로 판단됨",
637
- "부분달성": "일부 목표는 달성되었으나 위험요인에 대한 지속 관리가 필요함",
638
- "진행중": "목표 달성 과정에 있으며 다음 평가 시 변화 여부 재확인이 필요함",
639
- "미달성": "목표 달성이 미흡하여 급여제공계획 변경과 사례관리 검토가 필요함",
640
- "재사정 필요": "상태변화가 확인되어 욕구사정 재점검 및 급여제���계획 재수립이 필요함",
641
- }.get(result_level, "급여제공 결과에 대한 재확인이 필요함")
642
-
643
- result = f"""
644
- [평가결과]
645
- {result_level}: {achievement_text}
646
-
647
- [기관유형별 평가 관점]
648
- {view}
649
-
650
- [급여제공계획 이행 평가]
651
- {care_plan[:900]}...
652
-
653
- [기관유형별 세부 결과평가]
654
- {chr(10).join(result_lines)}
655
-
656
- [상담일지 반영 확인]
657
- {counseling_summary}
658
-
659
- [욕구사정 반영 확인]
660
- {needs_summary}
661
-
662
- [사례관리 반영 확인]
663
- {case_summary}
664
-
665
- [상태변화 및 보호자 의견]
666
- {change_text}
667
-
668
- [자동 연계 판단]
669
- - 종합 위험도: {risk_context['level']}
670
- - 연계 판단: {risk_context['flag_text']}
671
-
672
- [향후 계획]
673
- - 현재 급여제공 방향을 유지하되, 상태 변화가 지속될 경우 욕구사정 재점검을 실시한다.
674
- - 보호자 의견과 종사자 관찰내용을 상담일지에 지속 기록한다.
675
- - 사례관리 대상에 해당하는 경우 2차 결과회의 또는 추가 사례회의에 반영한다.
676
- - 급여제공계획 변경이 필요한 경우 변경 사유와 반영 내용을 명확히 기록한다.
677
- """.strip()
678
-
679
- return normalize_document_text(result)
680
-
681
-
682
- def evaluate_document(care_plan, result_eval, counseling_summary, needs_summary, case_summary, guardian_input):
683
- required = {
684
- "수급자 정보 반영": "주요" in care_plan or "상태" in care_plan,
685
- "상담일지 반영": "상담" in care_plan and "상담" in result_eval,
686
- "욕구사정 반영": "욕구사정" in care_plan and "욕구사정" in result_eval,
687
- "사례관리 반영": "사례관리" in care_plan and "사례관리" in result_eval,
688
- "보호자 의견 반영": bool(guardian_input.strip()) or "보호자" in care_plan,
689
- "상태변화 반영": "상태변화" in care_plan or "상태변화" in result_eval or "변화" in result_eval,
690
- "급여제공 목표": "급여제공 목표" in care_plan,
691
- "위험요인 판단": "위험요인" in care_plan or "자동 연계 판단" in result_eval,
692
- "세부 급여제공계획": "세부 급여제공계획" in care_plan,
693
- "결과평가": "평가결과" in result_eval,
694
- "향후 계획": "향후 계획" in result_eval,
695
- }
696
- score = int(sum(required.values()) / len(required) * 100)
697
- missing = [k for k, v in required.items() if not v]
698
-
699
- if score >= 90:
700
- comment = "평가 적합도가 높습니다. 급여제공계획과 결과평가가 상담·욕구사정·사례관리와 잘 연결되어 있습니다."
701
- elif score >= 75:
702
- comment = "대체로 적합하나 일부 보완이 필요합니다. 부족 항목을 보완하면 평가 대응력이 높아집니다."
703
- else:
704
- comment = "보완이 필요합니다. 상담일지, 욕구사정, 사례관리, 보호자 의견, 상태변화가 더 구체적으로 연결되어야 합니다."
705
-
706
- return score, missing, comment
707
-
708
-
709
- # -----------------------------
710
- # 엑셀 출력
711
- # - 결재라인: 오른쪽 맨 위 H:I 고정
712
- # - 출력용 시트: 평가적합도/비고 내부자료는 출력 제외
713
- # - 선 잘림 방지: 병합범위 전체 테두리 적용 + 긴 문장은 여러 행으로 분리
714
- # -----------------------------
715
- def set_print_layout(ws):
716
- """A4 출력 시 오른쪽/아래 선이 잘리지 않도록 인쇄 설정"""
717
- ws.page_setup.paperSize = ws.PAPERSIZE_A4
718
- ws.page_setup.orientation = "portrait"
719
- ws.page_setup.fitToWidth = 1
720
- ws.page_setup.fitToHeight = 0
721
- ws.sheet_properties.pageSetUpPr.fitToPage = True
722
- ws.page_margins.left = 0.25
723
- ws.page_margins.right = 0.25
724
- ws.page_margins.top = 0.35
725
- ws.page_margins.bottom = 0.35
726
- ws.page_margins.header = 0.1
727
- ws.page_margins.footer = 0.1
728
- ws.sheet_view.showGridLines = False
729
- ws.print_options.horizontalCentered = True
730
-
731
-
732
- def apply_border(ws, start_row, end_row, start_col=1, end_col=9):
733
- """값이 없는 병합 셀까지 전체 테두리 적용하여 지저분한 끊김 방지"""
734
- thin = Side(style="thin", color="000000")
735
- border = Border(left=thin, right=thin, top=thin, bottom=thin)
736
- for row in ws.iter_rows(min_row=start_row, max_row=end_row, min_col=start_col, max_col=end_col):
737
- for cell in row:
738
- cell.border = border
739
- if cell.font is None or cell.font.name is None:
740
- cell.font = Font(name="Malgun Gothic", size=10)
741
- cell.alignment = Alignment(
742
- horizontal=cell.alignment.horizontal or "left",
743
- vertical=cell.alignment.vertical or "center",
744
- wrap_text=True
745
- )
746
-
747
-
748
- def style_ws(ws, max_row):
749
- set_print_layout(ws)
750
- apply_border(ws, 1, max_row, 1, 9)
751
- ws.print_area = f"A1:I{max_row}"
752
-
753
- # 가독성 좋은 폭: A~I 전체 사용, 결재라인 H:I는 너무 좁지 않게
754
- widths = [10.5, 12, 12, 10.5, 12, 12, 10.5, 12, 12]
755
- for i, width in enumerate(widths, start=1):
756
- ws.column_dimensions[get_column_letter(i)].width = width
757
-
758
-
759
- def write_title_with_approval(ws, title, title_fill, header_fill):
760
- # 제목은 A:G, 결재라인은 H:I 오른쪽 맨 위 고정
761
- for col in range(1, 10):
762
- ws.column_dimensions[get_column_letter(col)].width = 11.5
763
-
764
- ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=7)
765
- title_cell = ws.cell(1, 1)
766
- title_cell.value = title
767
- title_cell.font = Font(name="Malgun Gothic", size=24, bold=True)
768
- title_cell.alignment = Alignment(horizontal="center", vertical="center")
769
- title_cell.fill = title_fill
770
-
771
- ws.cell(1, 8).value = "담당자"
772
- ws.cell(1, 9).value = "시설장"
773
- ws.cell(2, 8).value = ""
774
- ws.cell(2, 9).value = ""
775
-
776
- for rr in [1, 2]:
777
- for cc in [8, 9]:
778
- cell = ws.cell(rr, cc)
779
- cell.font = Font(name="Malgun Gothic", size=10, bold=(rr == 1))
780
- cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
781
- if rr == 1:
782
- cell.fill = header_fill
783
-
784
- apply_border(ws, 1, 2, 1, 9)
785
- ws.row_dimensions[1].height = 26
786
- ws.row_dimensions[2].height = 40
787
-
788
-
789
- def write_info_row(ws, row, a, b, c, d, e, f, header_fill):
790
- ws.cell(row, 1).value = a
791
- ws.merge_cells(start_row=row, start_column=2, end_row=row, end_column=3)
792
- ws.cell(row, 2).value = b
793
-
794
- ws.cell(row, 4).value = c
795
- ws.merge_cells(start_row=row, start_column=5, end_row=row, end_column=6)
796
- ws.cell(row, 5).value = d
797
-
798
- ws.cell(row, 7).value = e
799
- ws.merge_cells(start_row=row, start_column=8, end_row=row, end_column=9)
800
- ws.cell(row, 8).value = f
801
-
802
- for col in [1, 4, 7]:
803
- ws.cell(row, col).fill = header_fill
804
- ws.cell(row, col).font = Font(name="Malgun Gothic", size=10, bold=True)
805
- ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
806
- for col in [2, 5, 8]:
807
- ws.cell(row, col).font = Font(name="Malgun Gothic", size=10)
808
- ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
809
-
810
- apply_border(ws, row, row, 1, 9)
811
- ws.row_dimensions[row].height = 28
812
-
813
-
814
- def split_text_for_rows(content, max_chars=850):
815
- """긴 내용을 한 행에 몰아넣지 않고 여러 행으로 나누어 선/글자 잘림 방지"""
816
- content = str(content or "").strip()
817
- if not content:
818
- return [""]
819
-
820
- chunks = []
821
- current = ""
822
- for paragraph in content.split("\n"):
823
- paragraph = paragraph.rstrip()
824
- if len(current) + len(paragraph) + 1 <= max_chars:
825
- current = (current + "\n" + paragraph).strip()
826
- else:
827
- if current:
828
- chunks.append(current)
829
- while len(paragraph) > max_chars:
830
- chunks.append(paragraph[:max_chars])
831
- paragraph = paragraph[max_chars:]
832
- current = paragraph
833
- if current:
834
- chunks.append(current)
835
- return chunks or [""]
836
-
837
-
838
- def calc_row_height(text, base=18, chars_per_line=58, max_height=360):
839
- lines = 0
840
- for line in str(text or "").split("\n"):
841
- lines += max(1, (len(line) // chars_per_line) + 1)
842
- return min(max(34, lines * base + 10), max_height)
843
-
844
-
845
- def write_section(ws, row, title, content, header_fill, min_height=70, font_size=10):
846
- ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=9)
847
- title_cell = ws.cell(row, 1)
848
- title_cell.value = title
849
- title_cell.font = Font(name="Malgun Gothic", size=13, bold=True)
850
- title_cell.alignment = Alignment(horizontal="center", vertical="center")
851
- title_cell.fill = header_fill
852
- apply_border(ws, row, row, 1, 9)
853
- ws.row_dimensions[row].height = 27
854
- row += 1
855
-
856
- for chunk in split_text_for_rows(content):
857
- ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=9)
858
- cell = ws.cell(row, 1)
859
- cell.value = chunk
860
- cell.font = Font(name="Malgun Gothic", size=font_size)
861
- cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
862
- apply_border(ws, row, row, 1, 9)
863
- ws.row_dimensions[row].height = max(min_height, calc_row_height(chunk))
864
- row += 1
865
- return row
866
-
867
-
868
- def write_internal_sheet(wb, data, header_fill):
869
- """평가적합도와 부족 항목은 내부 확인용으로만 보관하고 출력은 제외"""
870
- ws = wb.create_sheet("내부점검자료_출력제외")
871
- ws.sheet_state = "hidden"
872
- ws.cell(1, 1).value = "구분"
873
- ws.cell(1, 2).value = "내용"
874
- ws.cell(2, 1).value = "평가적합도"
875
- ws.cell(2, 2).value = f"{data['evaluation_score']}점"
876
- ws.cell(3, 1).value = "부족 항목"
877
- ws.cell(3, 2).value = data["missing_items"]
878
- ws.cell(4, 1).value = "점검 의견"
879
- ws.cell(4, 2).value = data["compliance_result"]
880
-
881
- ws.column_dimensions["A"].width = 18
882
- ws.column_dimensions["B"].width = 90
883
- for r in range(1, 5):
884
- ws.cell(r, 1).font = Font(name="Malgun Gothic", size=10, bold=True)
885
- ws.cell(r, 1).fill = header_fill
886
- ws.cell(r, 2).font = Font(name="Malgun Gothic", size=10)
887
- ws.cell(r, 2).alignment = Alignment(wrap_text=True, vertical="top")
888
- apply_border(ws, 1, 4, 1, 2)
889
-
890
-
891
- def generate_excel(data):
892
- wb = Workbook()
893
- title_fill = PatternFill("solid", fgColor="CCFBF1")
894
- header_fill = PatternFill("solid", fgColor="F3F4F6")
895
-
896
- # 급여제공계획서: 출력용에는 평가적합도/비고란 표시하지 않음
897
- ws1 = wb.active
898
- ws1.title = "급여제공계획서"
899
- write_title_with_approval(ws1, "급여제공계획서", title_fill, header_fill)
900
-
901
- r = 3
902
- write_info_row(ws1, r, "기관명", data["institution_name"], "기관유형", data["institution_type"], "작성일자", data["plan_date"], header_fill); r += 1
903
- write_info_row(ws1, r, "수급자", data["client_masked_name"], "장기요양등급", data["grade"], "계획기간", data["plan_period"], header_fill); r += 1
904
- write_info_row(ws1, r, "인정번호", data.get("recognition_no", "미입력"), "인정기간", data.get("recognition_period", "미입력"), "기한알림", data.get("recognition_alert", ""), header_fill); r += 1
905
- write_info_row(ws1, r, "작성자", data["writer"], "평가예정일", data["evaluation_date"], "", "", header_fill); r += 1
906
-
907
- r = write_section(ws1, r, "상담일지 연동 요약", data["counseling_summary"], header_fill, 60, 10)
908
- r = write_section(ws1, r, "욕구사정 연동 요약", data["needs_summary"], header_fill, 60, 10)
909
- r = write_section(ws1, r, "사례관리 연동 요약", data["case_summary"], header_fill, 60, 10)
910
- r = write_section(ws1, r, "급여제공계획", data["care_plan"], header_fill, 75, 10)
911
-
912
- # 공통 안내문구 추가
913
- ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
914
- ws1.cell(r, 1).value = "※ 생성된 문서는 그대로 사용하셔도 되며, 기관의 운영방침, 수급자 상태, 보호자 의견 및 평가기준에 따라 추가 보완사항은 얼마든지 수정 가능합니다. 기관의 상황에 맞게 활용하시기 바랍니다."
915
- ws1.cell(r, 1).font = Font(name="Malgun Gothic", size=8.5, color="595959", italic=True)
916
- ws1.cell(r, 1).alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
917
- apply_border(ws1, r, r, 1, 9)
918
- ws1.row_dimensions[r].height = 36
919
- r += 1
920
-
921
- style_ws(ws1, r - 1)
922
-
923
- # 결과평가서: 출력용에는 평가적합도/내부점검 자료 표시하지 않음
924
- ws2 = wb.create_sheet("결과평가서")
925
- write_title_with_approval(ws2, "급여제공 결과평가서", title_fill, header_fill)
926
-
927
- r = 3
928
- write_info_row(ws2, r, "기관명", data["institution_name"], "기관유형", data["institution_type"], "평가일자", data["evaluation_date"], header_fill); r += 1
929
- write_info_row(ws2, r, "수급자", data["client_masked_name"], "장기요양등급", data["grade"], "평가결과", data["result_level"], header_fill); r += 1
930
- write_info_row(ws2, r, "인정번호", data.get("recognition_no", "미입력"), "인정기간", data.get("recognition_period", "미입력"), "기한알림", data.get("recognition_alert", ""), header_fill); r += 1
931
- write_info_row(ws2, r, "작성자", data["writer"], "계획작성일", data["plan_date"], "", "", header_fill); r += 1
932
-
933
- r = write_section(ws2, r, "기관유형별 결과평가 관점", INSTITUTION_RESULT_POINTS[data["institution_type"]]["result_view"], header_fill, 60, 10)
934
- r = write_section(ws2, r, "결과평가 내용", data["result_evaluation"], header_fill, 75, 10)
935
-
936
- # 공통 안내문구 추가
937
- ws2.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
938
- ws2.cell(r, 1).value = "※ 생성된 문서는 그대로 사용하셔도 되며, 기관의 운영방침, 수급자 상태, 보호자 의견 및 평가기준에 따라 추가 보완사항은 얼마든지 수정 가능합니다. 기관의 상황에 맞게 활용하시기 바랍니다."
939
- ws2.cell(r, 1).font = Font(name="Malgun Gothic", size=8.5, color="595959", italic=True)
940
- ws2.cell(r, 1).alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
941
- apply_border(ws2, r, r, 1, 9)
942
- ws2.row_dimensions[r].height = 36
943
- r += 1
944
-
945
- style_ws(ws2, r - 1)
946
-
947
- write_internal_sheet(wb, data, header_fill)
948
-
949
- output = BytesIO()
950
- wb.save(output)
951
- output.seek(0)
952
- return output
953
-
954
-
955
- # -----------------------------
956
- # 화면
957
- # -----------------------------
958
- st.sidebar.success("DB 연결")
959
- st.sidebar.caption(DB_PATH)
960
-
961
- clients_df = load_clients()
962
-
963
- if clients_df.empty:
964
- st.warning("Client Center 수급자 DB가 비어 있습니다. 직접 입력 모드로 임시 생성합니다.")
965
- temp_name = st.text_input("수급자명", "홍길동")
966
- client = pd.Series({
967
- "id": 0,
968
- "name": temp_name,
969
- "masked_name": mask_name(temp_name),
970
- "care_grade": "3등급",
971
- "institution_type": "방문요양",
972
- "institution_name": "",
973
- "main_condition": "",
974
- "cognitive_status": "",
975
- "mobility_status": "",
976
- "diet_status": "",
977
- "toileting_status": "",
978
- "status": "",
979
- "recognition_no": "",
980
- "recognition_start_date": "",
981
- "recognition_end_date": "",
982
- })
983
- else:
984
- options = clients_df.apply(
985
- lambda x: f"{x['id']}번 - {x['masked_name']} / {x['care_grade']} / {x['institution_type']} / {x.get('status','')}",
986
- axis=1
987
- ).tolist()
988
- selected_option = st.selectbox("수급자 선택", options)
989
- selected_id = int(selected_option.split("번")[0])
990
- client = clients_df[clients_df["id"] == selected_id].iloc[0]
991
-
992
- client_id = int(safe(client, "id", 0) or 0)
993
- counseling_df = load_counseling(client_id)
994
- needs_df = load_needs(client_id)
995
- case_df = load_cases(client_id)
996
-
997
- recognition_no, recognition_start, recognition_end, recognition_period = get_recognition_info(client)
998
- recognition_alert = recognition_due_alert(recognition_end)
999
-
1000
- c1, c2, c3, c4, c5 = st.columns(5)
1001
- c1.metric("수급자", safe(client, "masked_name"))
1002
- c2.metric("등급", safe(client, "care_grade"))
1003
- c3.metric("기관유형", safe(client, "institution_type"))
1004
- c4.metric("인정번호", recognition_no)
1005
- c5.metric("연동자료", f"상담 {len(counseling_df)} / 욕구 {len(needs_df)} / 사례 {len(case_df)}")
1006
- st.info(f"인정기간: {recognition_period} / {recognition_alert}")
1007
-
1008
- with st.expander("상담일지 연동 내용", expanded=False):
1009
- if counseling_df.empty:
1010
- st.info("저장된 상담일지가 없습니다.")
1011
- else:
1012
- st.dataframe(counseling_df, use_container_width=True)
1013
-
1014
- with st.expander("욕구사정 연동 내용", expanded=False):
1015
- if needs_df.empty:
1016
- st.info("저장된 욕구사정 기록이 없습니다. 아래 직접 입력 요약을 사용할 수 있습니다.")
1017
- else:
1018
- st.dataframe(needs_df, use_container_width=True)
1019
-
1020
- with st.expander("사례관리 연동 내용", expanded=False):
1021
- if case_df.empty:
1022
- st.info("저장된 사례관리 기록이 없습니다.")
1023
- else:
1024
- st.dataframe(case_df, use_container_width=True)
1025
-
1026
- manual_needs_summary = st.text_area(
1027
- "욕구사정 요약 직접 입력",
1028
- placeholder="예: 낙상위험 높음, 인지저하, 식사량 감소, 배변관리 필요, 정서지원 필요 등",
1029
- height=80,
1030
- )
1031
-
1032
- manual_guardian = st.text_area(
1033
- "보호자 의견 / 상태변화 직접 입력",
1034
- placeholder="예: 보호자가 최근 식사량 감소와 보행 불안정 증가를 걱정함.",
1035
- height=80,
1036
- )
1037
-
1038
- manual_goal = st.text_area(
1039
- "급여제공 목표 직접 입력",
1040
- placeholder="비워두면 연동자료를 바탕으로 자동 작성됩니다.",
1041
- height=70,
1042
- )
1043
-
1044
- st.markdown("### 문서 기본정보")
1045
- col1, col2, col3 = st.columns(3)
1046
-
1047
- institution_options = ["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"]
1048
- default_inst = safe(client, "institution_type", "방문요양")
1049
- if default_inst not in institution_options:
1050
- default_inst = "방문요양"
1051
-
1052
- institution_name = col1.text_input("기관명", value=safe(client, "institution_name"))
1053
- institution_type = col2.selectbox(
1054
- "기관유형",
1055
- institution_options,
1056
- index=institution_options.index(default_inst)
1057
- )
1058
- writer = col3.text_input("작성자", "사회복지사")
1059
-
1060
- col4, col5, col6 = st.columns(3)
1061
- plan_date = col4.date_input("계획서 작성일", value=date.today())
1062
- evaluation_date = col5.date_input("결과평가일", value=date.today() + timedelta(days=30))
1063
- result_level = col6.selectbox("결과평가 등급", ["목표달성", "부분달성", "진행중", "미달성", "재사정 필요"])
1064
-
1065
- default_plan_period = recognition_period if recognition_period != "미입력" else f"{plan_date} ~ {evaluation_date}"
1066
- plan_period = st.text_input("계획기간", default_plan_period)
1067
- observed_change = st.text_area(
1068
- "결과평가용 상태변화 / 관찰결과",
1069
- placeholder="예: 30일간 낙상은 발생하지 않았으나 보행 시 불안정함은 지속되어 화장실 동행과 환경정리가 필요함.",
1070
- height=100,
1071
- )
1072
-
1073
- if st.button("📄 급여제공계획서 + 결과평가서 생성", use_container_width=True):
1074
- counseling_summary = summarize_counseling(counseling_df)
1075
- needs_summary = summarize_needs(needs_df, client, manual_needs_summary)
1076
- case_summary = summarize_case(case_df)
1077
-
1078
- care_plan = make_care_plan(
1079
- client,
1080
- institution_type,
1081
- counseling_summary,
1082
- needs_summary,
1083
- case_summary,
1084
- manual_goal,
1085
- manual_guardian,
1086
- )
1087
-
1088
- result_evaluation = make_result_evaluation(
1089
- client,
1090
- institution_type,
1091
- care_plan,
1092
- counseling_summary,
1093
- needs_summary,
1094
- case_summary,
1095
- result_level,
1096
- observed_change,
1097
- )
1098
-
1099
- score, missing, compliance = evaluate_document(
1100
- care_plan,
1101
- result_evaluation,
1102
- counseling_summary,
1103
- needs_summary,
1104
- case_summary,
1105
- manual_guardian,
1106
- )
1107
-
1108
- st.session_state["care_plan_data"] = {
1109
- "client_id": client_id,
1110
- "client_masked_name": safe(client, "masked_name"),
1111
- "institution_name": institution_name,
1112
- "institution_type": institution_type,
1113
- "grade": safe(client, "care_grade"),
1114
- "writer": writer,
1115
- "plan_date": str(plan_date),
1116
- "evaluation_date": str(evaluation_date),
1117
- "plan_period": plan_period,
1118
- "recognition_no": recognition_no,
1119
- "recognition_period": recognition_period,
1120
- "recognition_alert": recognition_alert,
1121
- "result_level": result_level,
1122
- "counseling_summary": counseling_summary,
1123
- "needs_summary": needs_summary,
1124
- "case_summary": case_summary,
1125
- "care_plan": care_plan,
1126
- "result_evaluation": result_evaluation,
1127
- "evaluation_score": score,
1128
- "missing_items": ", ".join(missing) if missing else "부족 항목 없음",
1129
- "compliance_result": compliance,
1130
- }
1131
-
1132
- if "care_plan_data" in st.session_state:
1133
- data = st.session_state["care_plan_data"]
1134
-
1135
- st.markdown("---")
1136
- st.subheader("생성 결과 수정")
1137
-
1138
- data["care_plan"] = st.text_area("급여제공계획서 내용", value=data["care_plan"], height=300)
1139
- data["result_evaluation"] = st.text_area("결과평가서 내용", value=data["result_evaluation"], height=300)
1140
-
1141
- score, missing, compliance = evaluate_document(
1142
- data["care_plan"],
1143
- data["result_evaluation"],
1144
- data["counseling_summary"],
1145
- data["needs_summary"],
1146
- data["case_summary"],
1147
- manual_guardian,
1148
- )
1149
-
1150
- data["evaluation_score"] = score
1151
- data["missing_items"] = ", ".join(missing) if missing else "부족 항목 없음"
1152
- data["compliance_result"] = compliance
1153
-
1154
- st.subheader("평가 적합도 점검")
1155
- a, b, c = st.columns(3)
1156
- a.metric("평가 적합도", f"{score}점")
1157
- b.metric("부족 항목", data["missing_items"])
1158
- c.metric("결재라인", "담당자/시설장 2칸")
1159
-
1160
- if score >= 90:
1161
- st.markdown(f'<div class="okbox">{compliance}</div>', unsafe_allow_html=True)
1162
- else:
1163
- st.markdown(f'<div class="warnbox">{compliance}</div>', unsafe_allow_html=True)
1164
-
1165
- if st.button("💾 급여계획/결과평가 DB 저장", use_container_width=True):
1166
- save_plan_record(data)
1167
- st.success("급여제공계획서와 결과평가서 기록이 저장되었습니다.")
1168
-
1169
- filename = f"급여제공계획서_결과평가서_{data['client_masked_name']}_{datetime.now().strftime('%Y%m%d')}.xlsx"
1170
- if st.button("📦 엑셀 파일 준비하기", use_container_width=True, key="prepare_careplan_excel_fast"):
1171
- st.session_state["careplan_excel_fast"] = generate_excel(data)
1172
- st.success("엑셀 파일 준비가 완료되었습니다. 아래 다운로드 버튼을 눌러주세요.")
1173
-
1174
- if "careplan_excel_fast" in st.session_state:
1175
- st.download_button(
1176
- "📥 엑셀 다운로드",
1177
- data=st.session_state["careplan_excel_fast"],
1178
- file_name=filename,
1179
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1180
- use_container_width=True,
1181
- key="download_careplan_excel_fast",
1182
- )
1183
-
1184
- st.caption("빠른 버전: 화면 생성 후, 엑셀은 필요할 때만 준비합니다.")