devorah-ai-2016 commited on
Commit
a92b700
·
verified ·
1 Parent(s): 599b298

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -652
app.py DELETED
@@ -1,652 +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 openai import OpenAI
11
-
12
-
13
- # =========================================================
14
- # BeeCare Client Center 연동형 욕구사정 + 결과평가 생성기
15
- # =========================================================
16
-
17
- st.set_page_config(
18
- page_title="욕구사정기록지",
19
- page_icon="🏥",
20
- layout="wide"
21
- )
22
-
23
- # -----------------------------
24
- # DB 경로 설정
25
- # -----------------------------
26
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
27
-
28
- DB_CANDIDATES = [
29
- os.path.join(CURRENT_DIR, "data", "clients.db"),
30
- os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"),
31
- os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"),
32
- os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"),
33
- ]
34
-
35
- DB_PATH = None
36
- for path in DB_CANDIDATES:
37
- if os.path.exists(path):
38
- DB_PATH = path
39
- break
40
-
41
-
42
- # -----------------------------
43
- # AI 연결 설정
44
- # -----------------------------
45
- USE_LM_STUDIO = st.sidebar.toggle("LM Studio / Qwen 사용", value=False)
46
-
47
- client = None
48
- if USE_LM_STUDIO:
49
- try:
50
- client = OpenAI(base_url="http://127.0.0.1:1234/v1", api_key="lm-studio")
51
- except Exception:
52
- client = None
53
-
54
-
55
- def get_ai_content(system_prompt: str, user_prompt: str) -> str:
56
- """LM Studio 연결이 안 되면 기본 문장을 생성합니다."""
57
- if not USE_LM_STUDIO or client is None:
58
- return generate_rule_based_text(user_prompt)
59
-
60
- try:
61
- completion = client.chat.completions.create(
62
- model="qwen2.5-7b-instruct",
63
- messages=[
64
- {"role": "system", "content": system_prompt},
65
- {"role": "user", "content": user_prompt},
66
- ],
67
- temperature=0.7,
68
- )
69
- return completion.choices[0].message.content
70
- except Exception as e:
71
- return f"로컬 서버 연결 오류: {e}\n\n" + generate_rule_based_text(user_prompt)
72
-
73
-
74
- def generate_rule_based_text(prompt: str) -> str:
75
- """AI 연결 없이도 기본 문서가 나오도록 하는 안전 장치"""
76
- if "결과평가" in prompt or "30일" in prompt:
77
- return (
78
- "1. 30일간 급여제공 후 일상생활 지원과 정서적 지지 영역에서 일부 긍정적인 변화가 관찰되었습니다.\n"
79
- "2. 신체기능 유지를 위해 이동 보조, 자세변경, 안전관찰을 지속적으로 제공할 필요가 있습니다.\n"
80
- "3. 낙상예방과 인지기능 저하 예방을 위해 환경정비와 반복적인 안내가 필요합니다.\n"
81
- "4. 향후 급여제공계획에 따라 개인별 상태에 맞춘 지원을 유지하고, 변화 발생 시 재사정을 실시하겠습니다."
82
- )
83
-
84
- return (
85
- "1. 신체기능과 일상생활 수행능력 전반에서 지속적인 관찰과 지원이 필요합니다.\n"
86
- "2. 안전한 이동 지원, 식사 및 배변 관리, 정서적 지지, 인지기능 유지 활동을 제공할 필요가 있습니다.\n"
87
- "3. 낙상예방과 관절구축 예방을 위해 자세변경, 가벼운 관절운동, 환경정비가 필요합니다.\n"
88
- "4. 수급자의 상태 변화가 확인될 경우 급여제공계획에 반영하여 재사정합니다."
89
- )
90
-
91
-
92
- # -----------------------------
93
- # DB 함수
94
- # -----------------------------
95
- def load_clients() -> pd.DataFrame:
96
- if DB_PATH is None:
97
- return pd.DataFrame()
98
-
99
- try:
100
- conn = sqlite3.connect(DB_PATH)
101
- df = pd.read_sql_query(
102
- """
103
- SELECT
104
- id,
105
- name,
106
- masked_name,
107
- gender,
108
- birth_year,
109
- care_grade,
110
- institution_type,
111
- institution_name,
112
- admission_date,
113
- main_condition,
114
- cognitive_status,
115
- mobility_status,
116
- diet_status,
117
- toileting_status,
118
- guardian_name,
119
- guardian_phone,
120
- notes,
121
- status
122
- FROM clients
123
- ORDER BY id DESC
124
- """,
125
- conn,
126
- )
127
- conn.close()
128
- return df
129
- except Exception as e:
130
- st.error(f"Client Center DB를 불러오는 중 오류가 발생했습니다: {e}")
131
- return pd.DataFrame()
132
-
133
-
134
- def safe_value(row, key, default=""):
135
- try:
136
- value = row.get(key, default)
137
- if pd.isna(value):
138
- return default
139
- return str(value)
140
- except Exception:
141
- return default
142
-
143
-
144
- def format_numbered_text(text: str) -> str:
145
- """총평과 결과평가를 번호형으로 정돈합니다."""
146
- text = (text or "").strip()
147
- if not text:
148
- return ""
149
-
150
- lines = [line.strip() for line in text.splitlines() if line.strip()]
151
- if len(lines) >= 2 and all(line[:2].replace('.', '').isdigit() or line[0].isdigit() for line in lines[:2]):
152
- return "\n".join(lines)
153
-
154
- # 문장 단위로 나누어 1, 2, 3번 형태로 정리
155
- import re
156
- sentences = re.split(r'(?<=[.!?。]|다\.)\s+', text.replace("\n", " "))
157
- sentences = [s.strip() for s in sentences if s.strip()]
158
- cleaned = []
159
- for s in sentences:
160
- s = re.sub(r'^\d+[.)]\s*', '', s).strip()
161
- if s:
162
- cleaned.append(s)
163
- return "\n".join([f"{i}. {sentence}" for i, sentence in enumerate(cleaned[:6], start=1)])
164
-
165
-
166
- # -----------------------------
167
- # 디자인
168
- # -----------------------------
169
- st.markdown(
170
- """
171
- <style>
172
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700;800&display=swap');
173
- html, body, [class*="css"], .stApp {
174
- font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
175
- }
176
- .block-container {
177
- padding-top: 1.5rem;
178
- max-width: 1180px;
179
- }
180
- .hero {
181
- background: linear-gradient(135deg, #1B7F5A, #5DBB70);
182
- padding: 34px 36px;
183
- border-radius: 26px;
184
- text-align: center;
185
- margin: 8px 0 24px 0;
186
- box-shadow: 0 8px 20px rgba(27, 127, 90, 0.15);
187
- }
188
- .hero .eyebrow {
189
- color: #EAF7EE;
190
- font-size: 17px;
191
- font-weight: 700;
192
- letter-spacing: 0.02em;
193
- margin-bottom: 8px;
194
- }
195
- .hero h1 {
196
- color: white;
197
- margin: 0;
198
- font-size: clamp(34px, 4.2vw, 54px);
199
- line-height: 1.18;
200
- font-weight: 800;
201
- word-break: keep-all;
202
- }
203
- .hero p {
204
- color: #F4FFF6;
205
- font-size: 20px;
206
- line-height: 1.55;
207
- margin-top: 20px;
208
- word-break: keep-all;
209
- }
210
- .notice {
211
- background: #FFF9E8;
212
- border-left: 7px solid #F6A623;
213
- border-radius: 16px;
214
- padding: 17px 20px;
215
- margin-bottom: 22px;
216
- font-size: 17px;
217
- line-height: 1.65;
218
- }
219
- .section-card {
220
- background: #ffffff;
221
- border: 1px solid #E5E7EB;
222
- border-radius: 18px;
223
- padding: 18px 20px;
224
- margin-bottom: 18px;
225
- }
226
- .stTextInput input, .stTextArea textarea, .stSelectbox div, .stMultiSelect div {
227
- font-size: 16px;
228
- }
229
- .stButton button, .stDownloadButton button {
230
- border-radius: 12px;
231
- font-weight: 700;
232
- }
233
- </style>
234
- """,
235
- unsafe_allow_html=True,
236
- )
237
-
238
- st.markdown(
239
- """
240
- <div class="hero">
241
- <div class="eyebrow">BeeCare Client Center 연동</div>
242
- <h1>욕구사정기록지</h1>
243
- <p>등록된 수급자 정보를 불러와 욕구사정 총평과 30일 후 결과평가를 깔끔한 문서로 생성합니다.</p>
244
- </div>
245
- """,
246
- unsafe_allow_html=True,
247
- )
248
-
249
- st.markdown(
250
- """
251
- <div class="notice">
252
- <strong>중요:</strong> 이 앱은 Client Center의 SQLite DB(data/clients.db)를 불러오는 통합연동형 코드입니다.
253
- DB가 없으면 직접 입력 모드로 사용할 수 있습니다.
254
- </div>
255
- """,
256
- unsafe_allow_html=True,
257
- )
258
-
259
-
260
- # -----------------------------
261
- # 수급자 선택 / 직접 입력
262
- # -----------------------------
263
- clients_df = load_clients()
264
-
265
- if DB_PATH:
266
- st.sidebar.success(f"Client Center DB 연결됨")
267
- st.sidebar.caption(DB_PATH)
268
- else:
269
- st.sidebar.warning("Client Center DB를 찾지 못했습니다. 직접 입력 모드로 실행됩니다.")
270
-
271
- mode = st.radio(
272
- "입력 방식 선택",
273
- ["Client Center에서 수급자 선택", "직접 입력"],
274
- horizontal=True,
275
- )
276
-
277
- selected_client = None
278
-
279
- if mode == "Client Center에서 수급자 선택" and not clients_df.empty:
280
- options = clients_df.apply(
281
- lambda x: f"{x['id']}번 - {x['masked_name']} / {x['care_grade']} / {x['institution_type']} / {x['status']}",
282
- axis=1,
283
- ).tolist()
284
-
285
- selected_option = st.selectbox("수급자 선택", options)
286
- selected_id = int(selected_option.split("번")[0])
287
- selected_client = clients_df[clients_df["id"] == selected_id].iloc[0]
288
-
289
- st.markdown("### 👤 선택된 수급자 기본정보")
290
- c1, c2, c3, c4 = st.columns(4)
291
- c1.metric("성명", safe_value(selected_client, "masked_name"))
292
- c2.metric("성별", safe_value(selected_client, "gender"))
293
- c3.metric("출생연도", safe_value(selected_client, "birth_year"))
294
- c4.metric("등급", safe_value(selected_client, "care_grade"))
295
-
296
- c5, c6, c7 = st.columns(3)
297
- c5.metric("기관유형", safe_value(selected_client, "institution_type"))
298
- c6.metric("기관명", safe_value(selected_client, "institution_name"))
299
- c7.metric("상태", safe_value(selected_client, "status"))
300
-
301
- elif mode == "Client Center에서 수급자 선택" and clients_df.empty:
302
- st.warning("등록된 수급자 정보가 없거나 DB를 찾지 못했습니다. 직접 입력 모드로 전환하세요.")
303
-
304
-
305
- # -----------------------------
306
- # 입력값 구성
307
- # -----------------------------
308
- st.markdown("### 📝 욕구사정 기본 입력")
309
-
310
- if selected_client is not None:
311
- default_name = safe_value(selected_client, "name")
312
- default_masked_name = safe_value(selected_client, "masked_name")
313
- default_gender = safe_value(selected_client, "gender", "여")
314
- default_birth_year = safe_value(selected_client, "birth_year")
315
- default_grade = safe_value(selected_client, "care_grade", "3등급")
316
- default_institution_type = safe_value(selected_client, "institution_type", "방문요양")
317
- default_institution_name = safe_value(selected_client, "institution_name")
318
- default_main_condition = safe_value(selected_client, "main_condition")
319
- default_cognitive_status = safe_value(selected_client, "cognitive_status")
320
- default_mobility_status = safe_value(selected_client, "mobility_status")
321
- default_diet_status = safe_value(selected_client, "diet_status")
322
- default_toileting_status = safe_value(selected_client, "toileting_status")
323
- default_notes = safe_value(selected_client, "notes")
324
- else:
325
- default_name = "홍길동"
326
- default_masked_name = "홍○동"
327
- default_gender = "여"
328
- default_birth_year = "1945"
329
- default_grade = "3등급"
330
- default_institution_type = "방문요양"
331
- default_institution_name = ""
332
- default_main_condition = ""
333
- default_cognitive_status = ""
334
- default_mobility_status = ""
335
- default_diet_status = ""
336
- default_toileting_status = ""
337
- default_notes = ""
338
-
339
- grade_options = ["1등급", "2등급", "3등급", "4등급", "5등급", "인지지원등급", "등급외", "미입력"]
340
- gender_options = ["여", "남", "기타"]
341
- institution_options = ["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"]
342
-
343
- col1, col2, col3, col4 = st.columns(4)
344
- name = col1.text_input("수급자 성명", value=default_name)
345
- masked_name = col2.text_input("가명 표기", value=default_masked_name)
346
- birth_year = col3.text_input("출생연도", value=default_birth_year)
347
- gender = col4.selectbox(
348
- "성별",
349
- gender_options,
350
- index=gender_options.index(default_gender) if default_gender in gender_options else 0,
351
- )
352
-
353
- col5, col6, col7 = st.columns(3)
354
- grade = col5.selectbox(
355
- "장기요양 등급",
356
- grade_options,
357
- index=grade_options.index(default_grade) if default_grade in grade_options else 2,
358
- )
359
- institution_type = col6.selectbox(
360
- "기관 유형",
361
- institution_options,
362
- index=institution_options.index(default_institution_type) if default_institution_type in institution_options else 0,
363
- )
364
- institution_name = col7.text_input("기관명", value=default_institution_name)
365
-
366
- st.markdown("### 🧩 상태 정보")
367
- c1, c2 = st.columns(2)
368
- main_condition = c1.text_area("주요 질환 / 상태", value=default_main_condition, height=90)
369
- cognitive_status = c2.text_area("인지 상태", value=default_cognitive_status, height=90)
370
-
371
- c3, c4 = st.columns(2)
372
- mobility_status = c3.text_area("이동 상태", value=default_mobility_status, height=90)
373
- diet_status = c4.text_area("식사 상태", value=default_diet_status, height=90)
374
-
375
- c5, c6 = st.columns(2)
376
- toileting_status = c5.text_area("배변 / 배뇨 상태", value=default_toileting_status, height=90)
377
- notes = c6.text_area("특이사항 / 메모", value=default_notes, height=90)
378
-
379
- st.markdown("### 📌 욕구사정 세부 체크")
380
- cc1, cc2, cc3, cc4 = st.columns(4)
381
- fall_risk = cc1.selectbox("낙상 위험", ["낮음", "보통", "높음"])
382
- bedsore_risk = cc2.selectbox("욕창 위험", ["낮음", "보통", "높음"])
383
- dementia_status = cc3.selectbox("인지저하 / 치매", ["없음", "의심", "진단 있음", "중증"])
384
- diaper_use = cc4.selectbox("기저귀 사용", ["미사용", "일부 사용", "상시 사용"])
385
-
386
- service_needs = st.multiselect(
387
- "주요 서비스 욕구",
388
- [
389
- "신체활동 지원",
390
- "인지활동 지원",
391
- "정서지원",
392
- "식사 지원",
393
- "배변/배뇨 지원",
394
- "이동 지원",
395
- "낙상예방",
396
- "욕창예방",
397
- "복약확인",
398
- "가족상담",
399
- ],
400
- default=["신체활동 지원", "정서지원", "낙상예방"],
401
- )
402
-
403
-
404
- # -----------------------------
405
- # AI 전문 소견 생성
406
- # -----------------------------
407
- st.markdown("---")
408
- st.subheader("🤖 전문 소견 생성")
409
-
410
- system_prompt = (
411
- "당신은 장기요양기관 평가와 문서작성에 익숙한 20년 경력의 사회복지사입니다. "
412
- "평가기준에 맞는 전문적이고 따뜻한 문체로 작성하세요. "
413
- "개인정보는 과도하게 노출하지 말고, 수급자 상태와 급여 반영 방향이 드러나게 작성하세요."
414
- )
415
-
416
- user_summary = f"""
417
- 수급자: {masked_name}
418
- 성별: {gender}
419
- 출생연도: {birth_year}
420
- 장기요양등급: {grade}
421
- 기관유형: {institution_type}
422
- 주요질환/상태: {main_condition}
423
- 인지상태: {cognitive_status}
424
- 이동상태: {mobility_status}
425
- 식사상태: {diet_status}
426
- 배변/배뇨상태: {toileting_status}
427
- 낙상위험: {fall_risk}
428
- 욕창위험: {bedsore_risk}
429
- 인지저하/치매: {dementia_status}
430
- 기저귀사용: {diaper_use}
431
- 주요 서비스 욕구: {', '.join(service_needs)}
432
- 특이사항: {notes}
433
- """
434
-
435
- col_ai1, col_ai2 = st.columns(2)
436
-
437
- with col_ai1:
438
- if st.button("욕구사정 ��평 생성"):
439
- prompt = user_summary + "\n위 정보를 바탕으로 욕구사정 종합소견을 1번부터 5번까지 번호 형식으로 작성해줘. 각 번호는 한 문장으로 간결하게 작성해줘."
440
- st.session_state["총평"] = format_numbered_text(get_ai_content(system_prompt, prompt))
441
-
442
- with col_ai2:
443
- if st.button("30일 후 결과평가 생성"):
444
- prompt = user_summary + "\n위 욕구사정 내용을 바탕으로 30일 후 결과평가와 향후 급여 반영 방향을 1번부터 5번까지 번호 형식으로 작성해줘. 각 번호는 한 문장으로 간결하게 작성해줘."
445
- st.session_state["결과평가"] = format_numbered_text(get_ai_content(system_prompt, prompt))
446
-
447
- v_total = st.text_area(
448
- "11. 총평 / 종합소견",
449
- value=st.session_state.get("총평", ""),
450
- height=170,
451
- help="생성 버튼을 누르면 번호순으로 정돈되어 작성됩니다. 직접 입력한 문장도 다운로드 시 번호형으로 정리됩니다.",
452
- )
453
- v_total_clean = format_numbered_text(v_total)
454
-
455
- st.markdown("---")
456
- st.subheader("📊 30일 후 결과평가")
457
-
458
- v_result = st.selectbox("해결 여부", ["해결됨", "부분 해결", "진행 중", "재사정 필요"])
459
- v_next_plan = st.text_area(
460
- "미해결 시 조치 계획 및 향후 급여 반영 계획",
461
- value=st.session_state.get("결과평가", ""),
462
- height=170,
463
- help="다운로드 시 번호순 문장으로 정돈됩니다.",
464
- )
465
- v_next_plan_clean = format_numbered_text(v_next_plan)
466
-
467
-
468
- # -----------------------------
469
- # 엑셀 생성
470
- # -----------------------------
471
- def set_cell(ws, cell, value, bold=False, size=11, fill=None):
472
- ws[cell] = value
473
- ws[cell].font = Font(name="Malgun Gothic", size=size, bold=bold)
474
- ws[cell].alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
475
- if fill:
476
- ws[cell].fill = fill
477
-
478
-
479
- def generate_excel():
480
- wb = Workbook()
481
-
482
- thin = Side(style="thin", color="888888")
483
- border = Border(left=thin, right=thin, top=thin, bottom=thin)
484
- title_fill = PatternFill("solid", fgColor="E2F0D9")
485
- header_fill = PatternFill("solid", fgColor="F2F2F2")
486
-
487
- def style_range(ws, cell_range, border_on=True, fill=None, font=None, alignment=None):
488
- """병합 셀 포함 범위 전체에 스타일 적용"""
489
- for row in ws[cell_range]:
490
- for cell in row:
491
- if border_on:
492
- cell.border = border
493
- if fill:
494
- cell.fill = fill
495
- if font:
496
- cell.font = font
497
- if alignment:
498
- cell.alignment = alignment
499
-
500
- def write_cell(ws, cell, value, bold=False, size=11, fill=None, align="center"):
501
- ws[cell] = value
502
- ws[cell].font = Font(name="Malgun Gothic", size=size, bold=bold)
503
- ws[cell].alignment = Alignment(horizontal=align, vertical="center", wrap_text=True)
504
- ws[cell].border = border
505
- if fill:
506
- ws[cell].fill = fill
507
-
508
- def approval_box(ws, start_col, end_col):
509
- """큰제목 우측 상단 결재란: 3번째 이미지처럼 제목과 같은 높이에 배치"""
510
- headers = ["담당자", "시설장"]
511
- for idx, col in enumerate(range(start_col, end_col + 1)):
512
- c1 = ws.cell(1, col)
513
- c2 = ws.cell(2, col)
514
- c1.value = headers[idx] if idx < len(headers) else ""
515
- c2.value = ""
516
- c1.font = Font(name="Malgun Gothic", size=12, bold=True)
517
- c2.font = Font(name="Malgun Gothic", size=11)
518
- c1.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
519
- c2.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
520
- c1.fill = header_fill
521
- c1.border = border
522
- c2.border = border
523
-
524
- # =====================================================
525
- # 시트 1: 욕구사정평가서
526
- # =====================================================
527
- ws1 = wb.active
528
- ws1.title = "욕구사정기록지"
529
-
530
- # 엑셀 기본 눈금선 제거 (깔끔한 표시)
531
- ws1.sheet_view.showGridLines = False
532
-
533
- widths = {"A": 16, "B": 16, "C": 16, "D": 16, "E": 16, "F": 16, "G": 16, "H": 14, "I": 14}
534
- for col, width in widths.items():
535
- ws1.column_dimensions[col].width = width
536
-
537
- ws1.merge_cells("A1:G2")
538
- write_cell(ws1, "A1", "욕 구 사 정 기 록 지", True, 26, title_fill)
539
- style_range(ws1, "A1:G2", fill=title_fill, font=Font(name="Malgun Gothic", size=26, bold=True), alignment=Alignment(horizontal="center", vertical="center", wrap_text=True))
540
- approval_box(ws1, 8, 9)
541
- ws1.row_dimensions[1].height = 42
542
- ws1.row_dimensions[2].height = 52
543
-
544
- basic_rows = [
545
- ("수급자명", masked_name, "성별", gender, "출생연도", birth_year),
546
- ("장기요양등급", grade, "기관유형", institution_type, "기관명", institution_name),
547
- ("작성일자", str(date.today()), "낙상위험", fall_risk, "욕창위험", bedsore_risk),
548
- ("인지저하/치매", dementia_status, "기저귀 사용", diaper_use, "해결여부", v_result),
549
- ]
550
-
551
- row_num = 3
552
- for row in basic_rows:
553
- ws1.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=3)
554
- ws1.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6)
555
- ws1.merge_cells(start_row=row_num, start_column=8, end_row=row_num, end_column=9)
556
- label_value_pairs = [(1, row[0]), (2, row[1]), (4, row[2]), (5, row[3]), (7, row[4]), (8, row[5])]
557
- for col, value in label_value_pairs:
558
- is_label = col in [1, 4, 7]
559
- write_cell(ws1, ws1.cell(row_num, col).coordinate, value, bold=is_label, size=11, fill=header_fill if is_label else None)
560
- style_range(ws1, f"A{row_num}:I{row_num}")
561
- ws1.row_dimensions[row_num].height = 28
562
- row_num += 1
563
-
564
- detail_rows = [
565
- ("주요 질환 / 상태", main_condition),
566
- ("인지 상태", cognitive_status),
567
- ("이동 상태", mobility_status),
568
- ("식사 상태", diet_status),
569
- ("배변 / 배뇨 상태", toileting_status),
570
- ("주요 서비스 욕구", ", ".join(service_needs)),
571
- ("특이사항", notes),
572
- ("총평 / 종합소견", v_total_clean),
573
- ]
574
-
575
- for label, value in detail_rows:
576
- ws1.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=9)
577
- write_cell(ws1, f"A{row_num}", label, True, 11, header_fill)
578
- write_cell(ws1, f"B{row_num}", value, False, 11, None, align="left")
579
- ws1[f"B{row_num}"].alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
580
- style_range(ws1, f"A{row_num}:I{row_num}")
581
- ws1.row_dimensions[row_num].height = 50 if label != "총평 / 종합소견" else 82
582
- row_num += 1
583
-
584
- ws1.freeze_panes = "A3"
585
- ws1.page_setup.orientation = "landscape"
586
- ws1.page_setup.fitToWidth = 1
587
- ws1.page_setup.fitToHeight = 0
588
- ws1.sheet_properties.pageSetUpPr.fitToPage = True
589
- ws1.print_area = f"A1:I{row_num - 1}"
590
-
591
- # =====================================================
592
- # 시트 2: 30일 후 결과평가 기록지
593
- # =====================================================
594
- ws2 = wb.create_sheet("결과평가서")
595
-
596
- # 엑셀 기본 눈금선 제거 (지저분한 여백 제거)
597
- ws2.sheet_view.showGridLines = False
598
-
599
- for col in "ABCDE":
600
- ws2.column_dimensions[col].width = 24
601
-
602
- ws2.merge_cells("A1:C2")
603
- write_cell(ws2, "A1", "30일 후 결과평가 기록지", True, 24, title_fill)
604
- style_range(ws2, "A1:C2", fill=title_fill, font=Font(name="Malgun Gothic", size=24, bold=True), alignment=Alignment(horizontal="center", vertical="center", wrap_text=True))
605
- approval_box(ws2, 4, 5)
606
- ws2.row_dimensions[1].height = 42
607
- ws2.row_dimensions[2].height = 52
608
-
609
- result_rows = [
610
- ("수급자 성명", masked_name),
611
- ("장기요양등급", grade),
612
- ("기관유형", institution_type),
613
- ("평가일자", str(date.today())),
614
- ("해결 여부", v_result),
615
- ("결과평가 및 조치계획", v_next_plan_clean),
616
- ]
617
-
618
- r = 3
619
- for label, value in result_rows:
620
- ws2.merge_cells(start_row=r, start_column=2, end_row=r, end_column=5)
621
- write_cell(ws2, f"A{r}", label, True, 11, header_fill)
622
- write_cell(ws2, f"B{r}", value, False, 11, None, align="left")
623
- ws2[f"B{r}"].alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
624
- style_range(ws2, f"A{r}:E{r}")
625
- ws2.row_dimensions[r].height = 48 if label != "결과평가 및 조치계획" else 90
626
- r += 1
627
-
628
- ws2.freeze_panes = "A3"
629
- ws2.page_setup.orientation = "landscape"
630
- ws2.page_setup.fitToWidth = 1
631
- ws2.page_setup.fitToHeight = 0
632
- ws2.sheet_properties.pageSetUpPr.fitToPage = True
633
- ws2.print_area = f"A1:E{r - 1}"
634
-
635
- # 시트 3(2차사례관리결과평가서) 생성 코드 제거됨
636
-
637
- buffer = BytesIO()
638
- wb.save(buffer)
639
- buffer.seek(0)
640
- return buffer
641
-
642
-
643
- file_name = f"욕구사정_결과평가_{masked_name}_{datetime.now().strftime('%Y%m%d')}.xlsx"
644
-
645
- st.download_button(
646
- "📥 통합 엑셀 다운로드",
647
- data=generate_excel(),
648
- file_name=file_name,
649
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
650
- )
651
-
652
- st.caption("BeeCare Client 연동형 욕구사정기록지 생성기")