Spaces:
Sleeping
Sleeping
Download app.py from devorah-ai-2016/NeedsAssessment: direct link, hf CLI and curl.
- Browser
- Download file 50.6 kB
-
https://huggingface.co/spaces/devorah-ai-2016/NeedsAssessment/resolve/main/app.py
- Command line
-
hf download hf://spaces/devorah-ai-2016/NeedsAssessment/app.py
-
curl -L -o app.py https://huggingface.co/spaces/devorah-ai-2016/NeedsAssessment/resolve/main/app.py
50.6 kB
| import os | |
| import sqlite3 | |
| from io import BytesIO | |
| from datetime import date, datetime | |
| import pandas as pd | |
| import streamlit as st | |
| from openpyxl import Workbook | |
| from openpyxl.styles import Font, Alignment, Border, Side, PatternFill | |
| from openai import OpenAI | |
| # ========================================================= | |
| # BeeCare Client Center 연동형 욕구사정 + 결과평가 생성기 | |
| # ========================================================= | |
| st.set_page_config( | |
| page_title="욕구사정기록지", | |
| page_icon="🏥", | |
| layout="wide" | |
| ) | |
| # ----------------------------- | |
| # DB 경로 설정 | |
| # ----------------------------- | |
| CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| DB_CANDIDATES = [ | |
| os.path.join(CURRENT_DIR, "data", "clients.db"), | |
| os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"), | |
| os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"), | |
| os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"), | |
| ] | |
| DB_PATH = None | |
| for path in DB_CANDIDATES: | |
| if os.path.exists(path): | |
| DB_PATH = path | |
| break | |
| # ----------------------------- | |
| # AI 연결 설정 | |
| # ----------------------------- | |
| USE_LM_STUDIO = st.sidebar.toggle("LM Studio / Qwen 사용", value=False) | |
| client = None | |
| if USE_LM_STUDIO: | |
| try: | |
| client = OpenAI(base_url="http://127.0.0.1:1234/v1", api_key="lm-studio") | |
| except Exception: | |
| client = None | |
| def get_ai_content(system_prompt: str, user_prompt: str) -> str: | |
| """LM Studio 연결이 안 되면 기본 문장을 생성합니다.""" | |
| if not USE_LM_STUDIO or client is None: | |
| return generate_rule_based_text(user_prompt) | |
| try: | |
| completion = client.chat.completions.create( | |
| model="qwen2.5-7b-instruct", | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=0.7, | |
| ) | |
| return completion.choices[0].message.content | |
| except Exception: | |
| return generate_rule_based_text(user_prompt) | |
| def generate_rule_based_text(prompt: str) -> str: | |
| """AI 연결 없이도 기본 문서가 나오도록 하는 안전 장치""" | |
| if "결과평가" in prompt or "30일" in prompt: | |
| return ( | |
| "1. 30일간 급여제공 후 일상생활 지원과 정서적 지지 영역에서 일부 긍정적인 변화가 관찰되었습니다.\n" | |
| "2. 신체기능 유지를 위해 이동 보조, 자세변경, 안전관찰을 지속적으로 제공할 필요가 있습니다.\n" | |
| "3. 낙상예방과 인지기능 저하 예방을 위해 환경정비와 반복적인 안내가 필요합니다.\n" | |
| "4. 향후 급여제공계획에 따라 개인별 상태에 맞춘 지원을 유지하고, 변화 발생 시 재사정을 실시하겠습니다." | |
| ) | |
| return ( | |
| "1. 신체기능과 일상생활 수행능력 전반에서 지속적인 관찰과 지원이 필요합니다.\n" | |
| "2. 안전한 이동 지원, 식사 및 배변 관리, 정서적 지지, 인지기능 유지 활동을 제공할 필요가 있습니다.\n" | |
| "3. 낙상예방과 관절구축 예방을 위해 자세변경, 가벼운 관절운동, 환경정비가 필요합니다.\n" | |
| "4. 수급자의 상태 변화가 확인될 경우 급여제공계획에 반영하여 재사정합니다." | |
| ) | |
| # ----------------------------- | |
| # DB 함수 | |
| # ----------------------------- | |
| def load_clients() -> pd.DataFrame: | |
| if DB_PATH is None: | |
| return pd.DataFrame() | |
| try: | |
| conn = sqlite3.connect(DB_PATH) | |
| # Client Center 버전이 달라도 오류가 나지 않도록 실제 존재하는 컬럼만 읽습니다. | |
| available_cols = pd.read_sql_query("PRAGMA table_info(clients)", conn)["name"].tolist() | |
| wanted_cols = [ | |
| "id", "name", "masked_name", "gender", "birth_year", "care_grade", | |
| "recognition_number", "recognition_start_date", "recognition_end_date", | |
| "institution_type", "institution_name", "admission_date", | |
| "main_condition", "cognitive_status", "mobility_status", "diet_status", | |
| "toileting_status", "guardian_name", "guardian_phone", "notes", "status" | |
| ] | |
| select_cols = [c for c in wanted_cols if c in available_cols] | |
| if not select_cols: | |
| conn.close() | |
| return pd.DataFrame() | |
| query = "SELECT " + ", ".join(select_cols) + " FROM clients ORDER BY id DESC LIMIT 300" | |
| df = pd.read_sql_query(query, conn) | |
| conn.close() | |
| # 없는 컬럼은 빈칸으로 채워 다른 입력칸과 매칭되게 합니다. | |
| for col in wanted_cols: | |
| if col not in df.columns: | |
| df[col] = "" | |
| return df | |
| except Exception as e: | |
| st.error(f"Client Center DB를 불러오는 중 오류가 발생했습니다: {e}") | |
| return pd.DataFrame() | |
| def safe_value(row, key, default=""): | |
| try: | |
| value = row.get(key, default) | |
| if pd.isna(value): | |
| return default | |
| return str(value) | |
| except Exception: | |
| return default | |
| def format_numbered_text(text: str) -> str: | |
| """총평과 결과평가를 번호형으로 정돈합니다.""" | |
| text = (text or "").strip() | |
| if not text: | |
| return "" | |
| lines = [line.strip() for line in text.splitlines() if line.strip()] | |
| if len(lines) >= 2 and all(line[:2].replace('.', '').isdigit() or line[0].isdigit() for line in lines[:2]): | |
| return "\n".join(lines) | |
| # 문장 단위로 나누어 1, 2, 3번 형태로 정리 | |
| import re | |
| sentences = re.split(r'(?<=[.!?。]|다\.)\s+', text.replace("\n", " ")) | |
| sentences = [s.strip() for s in sentences if s.strip()] | |
| cleaned = [] | |
| for s in sentences: | |
| s = re.sub(r'^\d+[.)]\s*', '', s).strip() | |
| if s: | |
| cleaned.append(s) | |
| return "\n".join([f"{i}. {sentence}" for i, sentence in enumerate(cleaned[:6], start=1)]) | |
| # ----------------------------- | |
| # 욕구사정 품질 고도화 엔진 | |
| # ----------------------------- | |
| RESTRICTED_REPLACEMENTS = { | |
| "상태 양호": "현재 신체·인지·정서 상태가 비교적 안정적으로 유지되나 정기 관찰이 요구됨", | |
| "특이사항 없음": "즉시 개입이 필요한 돌발 상황은 확인되지 않았으나 서비스 제공 중 상태변화 관찰이 요구됨", | |
| "문제 없음": "현재 확인된 위험요인은 제한적이나 욕구와 기능상태 변화 여부를 지속 확인할 필요가 있음", | |
| "이전과 동일": "기존 관찰 항목의 급격한 악화는 제한적이나 기능 유지 여부를 구체적으로 재확인함", | |
| "치매 있으심": "인지기능 저하 및 치매 관련 행동 변화 여부에 대한 관찰이 요구됨", | |
| "욕창 있음": "욕창 또는 피부손상 위험이 확인되어 피부상태와 압박부위 관리가 요구됨", | |
| } | |
| INSTITUTION_NEEDS_GUIDES = { | |
| "방문요양": "가정 내 잔존기능 유지, 주거환경 안전, 보호자 부재시간, 신체활동 및 정서지원의 적정성을 중심으로 사정함", | |
| "방문목욕": "목욕 전후 건강상태, 피부상태, 미끄럼·화상·낙상예방, 목욕 거부감 및 보호자 협조를 중심으로 사정함", | |
| "방문간호": "만성질환 관리, 복약 이행, 간호처치 상태, 욕창·상처관리, 의료기관 연계 필요성을 중심으로 사정함", | |
| "주간보호": "송영 안전, 프로그램 참여, 식사·위생, 센터 내 관계, 보호자 부양 부담 경감을 중심으로 사정함", | |
| "요양원": "입소생활 적응, 식사·수면·배변, 낙상·욕창예방, 공동생활 적응 및 보호자 상담 반영을 중심으로 사정함", | |
| "공동생활가정": "소규모 공동생활 적응, 개별 생활습관 존중, 관계 갈등 조정, 실내 이동 동선 안전을 중심으로 사정함", | |
| } | |
| FALL_KEYWORDS = ["낙상", "넘어", "미끄", "어지", "현기", "휘청", "보행", "부축", "워커", "지팡", "휠체어", "문턱", "야간", "화장실", "송영"] | |
| PRESSURE_KEYWORDS = ["욕창", "발적", "상처", "피부", "와상", "침상", "체위", "드레싱", "삼출", "감염", "영양불량", "탈수", "통증"] | |
| DEMENTIA_KEYWORDS = ["치매", "인지", "기억", "반복", "배회", "망상", "환각", "불안", "공격", "귀가", "집에", "야간배회", "혼동", "고집"] | |
| MOOD_KEYWORDS = ["우울", "불안", "무기력", "외로", "눈물", "거부", "짜증", "화"] | |
| NUTRITION_KEYWORDS = ["식사량", "식욕", "체중", "영양", "사레", "연하", "탈수"] | |
| GUARDIAN_KEYWORDS = ["힘들", "소진", "부담", "걱정", "확대", "변경", "동행", "상담", "요청", "불안"] | |
| SUPPORT_METHOD_RULES = { | |
| "이동지원": { | |
| "keywords": ["이동", "보행", "부축", "휠체어", "지팡", "워커", "화장실", "낙상", "휘청", "어지"], | |
| "items": [ | |
| "침상과 휠체어 이동 시 부축을 제공함", | |
| "보행기 및 지팡이 사용을 지원함", | |
| "실내 이동 시 안전을 확인함", | |
| "이동 동선을 정리하여 낙상을 예방함", | |
| "화장실 이동 시 동행 및 안전관찰을 실시함", | |
| ], | |
| }, | |
| "일상생활지원": { | |
| "keywords": ["신체활동", "위생", "세면", "구강", "의복", "착탈", "손발톱", "목욕", "피부"], | |
| "items": [ | |
| "세면 및 구강관리를 지원함", | |
| "의복 착탈의를 지원함", | |
| "개인위생 상태를 점검함", | |
| "손발톱 상태를 확인하고 위생관리를 지원함", | |
| "목욕 전후 건강상태와 피부상태를 확인함", | |
| ], | |
| }, | |
| "낙상예방 지원": { | |
| "keywords": FALL_KEYWORDS + ["낙상예방"], | |
| "items": [ | |
| "보행 시 부축을 제공함", | |
| "미끄럼 방지 환경을 점검함", | |
| "침상 난간 상태를 확인함", | |
| "야간 이동 시 안전관찰을 실시함", | |
| "이동 전 어지럼 여부를 확인함", | |
| "보행보조기 사용을 안내함", | |
| ], | |
| }, | |
| "욕창예방 지원": { | |
| "keywords": PRESSURE_KEYWORDS + ["욕창예방"], | |
| "items": [ | |
| "정기적 체위변경을 실시함", | |
| "피부상태를 확인함", | |
| "욕창 발생 여부를 관찰함", | |
| "영양상태를 확인함", | |
| "장시간 동일 자세를 예방함", | |
| "피부 청결을 유지함", | |
| ], | |
| }, | |
| "인지지원": { | |
| "keywords": DEMENTIA_KEYWORDS + ["인지활동", "인지저하", "기억저하", "일정표", "안전수칙"], | |
| "items": [ | |
| "날짜와 장소를 반복 안내함", | |
| "회상대화를 실시함", | |
| "익숙한 환경을 유지하도록 지원함", | |
| "인지자극 활동을 제공함", | |
| "일정표를 활용하여 안내함", | |
| "안전수칙을 반복 교육함", | |
| ], | |
| }, | |
| "BPSD 지원": { | |
| "keywords": ["반복질문", "망상", "배회", "공격", "폭언", "귀가", "환각", "거부", "불안", "초조"], | |
| "items": [ | |
| "동일 질문에 안정적으로 응대함", | |
| "시각자료를 활용하여 설명함", | |
| "정면 반박을 지양함", | |
| "감정을 공감함", | |
| "화제를 전환함", | |
| "안전한 이동을 지원함", | |
| "자극요인을 제거하고 차분한 환경을 제공함", | |
| ], | |
| }, | |
| "정서지원": { | |
| "keywords": MOOD_KEYWORDS + ["정서지원", "외로움", "소외", "가족"], | |
| "items": [ | |
| "말벗 서비스를 제공함", | |
| "공감적 경청을 실시함", | |
| "긍정적 피드백을 제공함", | |
| "취미활동 참여를 지원함", | |
| "성취감을 경험하도록 지원함", | |
| "안정적인 환경을 제공함", | |
| "가족 연락을 지원함", | |
| ], | |
| }, | |
| "사회참여지원": { | |
| "keywords": ["고립", "프로그램", "관계", "대인", "참여", "활동", "말다툼", "갈등"], | |
| "items": [ | |
| "집단 프로그램 참여를 지원함", | |
| "또래 관계 형성을 지원함", | |
| "지역사회 활동 참여를 연계함", | |
| "대화 기회를 제공함", | |
| "공동활동 참여를 유도함", | |
| ], | |
| }, | |
| "영양지원": { | |
| "keywords": NUTRITION_KEYWORDS + ["식사 지원", "기호식", "선호 음식"], | |
| "items": [ | |
| "선호 음식을 확인함", | |
| "기호식을 반영함", | |
| "수분섭취를 독려함", | |
| "영양상태를 관찰함", | |
| "체중변화를 확인함", | |
| "영양보충을 지원함", | |
| ], | |
| }, | |
| "배변배뇨지원": { | |
| "keywords": ["배변", "배뇨", "변비", "요실금", "실금", "기저귀", "화장실"], | |
| "items": [ | |
| "수분섭취를 독려함", | |
| "규칙적 배변습관을 지원함", | |
| "활동량 증가를 유도함", | |
| "배변배뇨 패턴을 확인함", | |
| "정기 변기유도를 실시함", | |
| "위생관리를 지원함", | |
| ], | |
| }, | |
| "복약지원": { | |
| "keywords": ["복약", "약", "투약", "약물", "복용", "혈압", "혈당", "복약확인"], | |
| "items": [ | |
| "복약 여부를 확인함", | |
| "약물 보관상태를 점검함", | |
| "보호자와 복약 정보를 공유함", | |
| "복약일지를 확인함", | |
| "투약시간을 안내함", | |
| ], | |
| }, | |
| "보호자지원": { | |
| "keywords": GUARDIAN_KEYWORDS + ["보호자", "가족", "가족상담", "가족갈등"], | |
| "items": [ | |
| "보호자 상담을 실시함", | |
| "돌봄 부담을 경청함", | |
| "지역자원 연계를 안내함", | |
| "가족 의견을 조정함", | |
| "의사소통을 지원함", | |
| ], | |
| }, | |
| "지역자원 연계": { | |
| "keywords": ["치매", "건강관리", "경제", "고립", "병원", "보건소", "복지관", "행정복지센터", "지역자원", "방문간호"], | |
| "items": [ | |
| "치매안심센터 연계를 검토함", | |
| "방문간호 서비스를 연계함", | |
| "보건소 프로그램을 안내함", | |
| "행정복지센터 연계를 검토함", | |
| "복지관 프로그램 참여를 연계함", | |
| ], | |
| }, | |
| } | |
| def normalize_assessment_text(text: str) -> str: | |
| value = str(text or "").strip() | |
| for bad, good in RESTRICTED_REPLACEMENTS.items(): | |
| value = value.replace(bad, good) | |
| return value | |
| def has_keyword(text: str, keywords) -> bool: | |
| source = str(text or "") | |
| return any(keyword in source for keyword in keywords) | |
| def formal_sentence(text: str) -> str: | |
| value = normalize_assessment_text(text).strip() | |
| if not value: | |
| return "" | |
| if value.endswith(("확인됨", "관찰됨", "사정됨", "판단됨", "요구됨", "계획함", "실시함", "필요함", "있음", "없음", "됨", "함", "하심", "말씀하심")): | |
| return value | |
| last = value[-1] | |
| if "가" <= last <= "힣" and (ord(last) - ord("가")) % 28: | |
| return f"{value}으로 확인됨" | |
| return f"{value}로 확인됨" | |
| def build_individual_support_methods(data: dict, ctx: dict | None = None) -> str: | |
| combined = " ".join([ | |
| data.get("institution_type", ""), | |
| data.get("grade", ""), | |
| data.get("main_condition", ""), | |
| data.get("cognitive_status", ""), | |
| data.get("mobility_status", ""), | |
| data.get("diet_status", ""), | |
| data.get("toileting_status", ""), | |
| data.get("notes", ""), | |
| data.get("guardian_opinion", ""), | |
| " ".join(data.get("service_needs", [])), | |
| ]) | |
| selected = {} | |
| for category, rule in SUPPORT_METHOD_RULES.items(): | |
| if has_keyword(combined, rule["keywords"]): | |
| selected[category] = rule["items"][:3] | |
| if ctx: | |
| if ctx.get("fall_detected"): | |
| selected.setdefault("낙상예방 지원", SUPPORT_METHOD_RULES["낙상예방 지원"]["items"][:4]) | |
| selected.setdefault("이동지원", SUPPORT_METHOD_RULES["이동지원"]["items"][:3]) | |
| if ctx.get("pressure_detected"): | |
| selected.setdefault("욕창예방 지원", SUPPORT_METHOD_RULES["욕창예방 지원"]["items"][:4]) | |
| if ctx.get("dementia_detected"): | |
| selected.setdefault("인지지원", SUPPORT_METHOD_RULES["인지지원"]["items"][:4]) | |
| selected.setdefault("BPSD 지원", SUPPORT_METHOD_RULES["BPSD 지원"]["items"][2:5]) | |
| if ctx.get("guardian_detected"): | |
| selected.setdefault("보호자지원", SUPPORT_METHOD_RULES["보호자지원"]["items"][:3]) | |
| if not selected: | |
| selected = { | |
| "이동지원": SUPPORT_METHOD_RULES["이동지원"]["items"][:2], | |
| "일상생활지원": SUPPORT_METHOD_RULES["일상생활지원"]["items"][:2], | |
| "정서지원": SUPPORT_METHOD_RULES["정서지원"]["items"][:2], | |
| } | |
| lines = ["[개인별 이용계획 지원방법]"] | |
| for category, items in list(selected.items())[:5]: | |
| lines.append(category) | |
| for item in items[:5]: | |
| lines.append(f"- {item}") | |
| return "\n".join(lines) | |
| def build_needs_context(data: dict) -> dict: | |
| combined = " ".join([ | |
| data.get("main_condition", ""), | |
| data.get("cognitive_status", ""), | |
| data.get("mobility_status", ""), | |
| data.get("diet_status", ""), | |
| data.get("toileting_status", ""), | |
| data.get("notes", ""), | |
| data.get("guardian_opinion", ""), | |
| " ".join(data.get("service_needs", [])), | |
| ]) | |
| fall_detected = data.get("fall_risk") == "높음" or has_keyword(combined, FALL_KEYWORDS) | |
| pressure_detected = data.get("bedsore_risk") == "높음" or has_keyword(combined, PRESSURE_KEYWORDS) | |
| dementia_detected = data.get("dementia_status") in ["의심", "진단 있음", "중증"] or has_keyword(combined, DEMENTIA_KEYWORDS) | |
| mood_detected = has_keyword(combined, MOOD_KEYWORDS) or "정서지원" in data.get("service_needs", []) | |
| nutrition_detected = has_keyword(combined, NUTRITION_KEYWORDS) or "식사 지원" in data.get("service_needs", []) | |
| guardian_detected = has_keyword(data.get("guardian_opinion", ""), GUARDIAN_KEYWORDS) or "가족상담" in data.get("service_needs", []) | |
| risk_score = 0 | |
| risk_score += {"낮음": 0, "보통": 1, "높음": 2}.get(data.get("fall_risk"), 0) | |
| risk_score += {"낮음": 0, "보통": 1, "높음": 2}.get(data.get("bedsore_risk"), 0) | |
| risk_score += {"없음": 0, "의심": 1, "진단 있음": 2, "중증": 3}.get(data.get("dementia_status"), 0) | |
| risk_score += 1 if guardian_detected else 0 | |
| fall_level = "고위험" if data.get("fall_risk") == "높음" or has_keyword(combined, ["낙상", "넘어", "휘청"]) else "중위험" if data.get("fall_risk") == "보통" or fall_detected else "저위험" | |
| if data.get("dementia_status") == "중증" or has_keyword(combined, ["배회", "공격", "환각", "망상", "야간배회"]): | |
| bpsd_level = "고위험" | |
| elif dementia_detected or has_keyword(combined, ["반복질문", "귀가", "불안"]): | |
| bpsd_level = "중위험" | |
| else: | |
| bpsd_level = "저위험" | |
| overall_level = "고위험" if risk_score >= 5 else "중위험" if risk_score >= 2 else "저위험" | |
| goals = [] | |
| if fall_detected: | |
| goals.append("낙상예방을 위해 이동 동선 정비, 보행보조, 보호자 교육을 통해 안전한 이동을 유지함") | |
| if pressure_detected: | |
| goals.append("욕창예방을 위해 피부관찰, 체위변경, 영양·수분관리, 감염예방을 지속함") | |
| if dementia_detected: | |
| goals.append("인지기능 유지를 위해 반복 안내, 정서 안정, BPSD 예방 중심의 개별화 지원을 실시함") | |
| if mood_detected: | |
| goals.append("우울·불안 완화를 위해 정서지원과 사회적 상호작용 기회를 제공함") | |
| if nutrition_detected: | |
| goals.append("영양관리 강화를 위해 식사량, 연하상태, 수분섭취를 정기 확인함") | |
| if guardian_detected: | |
| goals.append("보호자 소진 예방을 위해 상담, 정보공유, 서비스 조정 가능 범위를 안내함") | |
| if not goals: | |
| goals.append("현재 기능 유지와 일상생활 안정성 확보를 목표로 정기적인 상태 관찰을 실시함") | |
| flags = { | |
| "사례관리 검토 필요": overall_level == "고위험" or bpsd_level == "고위험", | |
| "급여제공계획 변경 필요": any([fall_detected, pressure_detected, dementia_detected, nutrition_detected, guardian_detected]), | |
| "상태변화기록 작성 필요": any([fall_detected, pressure_detected, dementia_detected]), | |
| "보호자 상담 필요": guardian_detected or bool(str(data.get("guardian_opinion", "")).strip()), | |
| } | |
| return { | |
| "fall_detected": fall_detected, | |
| "pressure_detected": pressure_detected, | |
| "dementia_detected": dementia_detected, | |
| "guardian_detected": guardian_detected, | |
| "fall_level": fall_level, | |
| "bpsd_level": bpsd_level, | |
| "overall_level": overall_level, | |
| "goals": goals, | |
| "flags": flags, | |
| "institution_guide": INSTITUTION_NEEDS_GUIDES.get(data.get("institution_type"), INSTITUTION_NEEDS_GUIDES["방문요양"]), | |
| } | |
| def build_enhanced_needs_summary(data: dict) -> str: | |
| ctx = build_needs_context(data) | |
| support_methods = build_individual_support_methods(data, ctx) | |
| guardian_text = formal_sentence(data.get("guardian_opinion")) or "보호자 의견은 추가 상담 시 구체적으로 확인할 계획임" | |
| client_need = ", ".join(data.get("service_needs", [])) or "일상생활 지원 및 상태관찰" | |
| problem_parts = [] | |
| if ctx["fall_detected"]: | |
| problem_parts.append(f"낙상위험 {ctx['fall_level']} 단계") | |
| if ctx["pressure_detected"]: | |
| problem_parts.append("피부손상 및 욕창예방 관리 필요") | |
| if ctx["dementia_detected"]: | |
| problem_parts.append(f"인지저하 및 BPSD {ctx['bpsd_level']} 단계") | |
| if not problem_parts: | |
| problem_parts.append("급격한 위험 단서는 제한적이나 기능 유지와 정기 관찰 필요") | |
| flag_text = " / ".join([f"□ {name}" for name, enabled in ctx["flags"].items() if enabled]) or "□ 정기 관찰 유지" | |
| plan_parts = [ctx["institution_guide"], " / ".join(ctx["goals"][:4])] | |
| if ctx["fall_detected"]: | |
| plan_parts.append("낙상 위험요인 분석, 환경개선, 보호자 교육, 이동지원 계획을 급여제공 과정에 반영함") | |
| if ctx["pressure_detected"]: | |
| plan_parts.append("피부상태 분석, 체위변경, 영양관리, 감염예방 계획을 급여제공 과정에 반영함") | |
| if ctx["dementia_detected"]: | |
| plan_parts.append("기억력, 지남력, BPSD, 보호자 부담을 함께 사정하고 공감과 전환 중심으로 지원함") | |
| summary = f""" | |
| 1. 현재상태: {data['masked_name']} 수급자는 {data['grade']} 대상자로서 {data['institution_type']} 이용 특성에 따라 {ctx['institution_guide']} 주요 질환 및 상태는 {formal_sentence(data.get('main_condition')) or '지속 관찰이 요구됨'}. | |
| 2. 핵심문제: {', '.join(problem_parts)}로 사정되며, 낙상위험은 {ctx['fall_level']}, BPSD 위험도는 {ctx['bpsd_level']}, 종합 위험도는 {ctx['overall_level']}으로 판단됨. | |
| 3. 수급자 및 보호자 욕구: 수급자는 {client_need} 욕구가 확인되며, 보호자 의견은 {guardian_text} | |
| 4. 장단기 목표: 단기적으로 안전사고 예방과 일상생활 안정화를 목표로 하고, 장기적으로 {' / '.join(ctx['goals'][:4])} | |
| 5. 급여제공 방향: {' '.join(plan_parts)} 사례관리 연계 판단은 {flag_text}로 정리됨. | |
| 6. 개인별 이용계획 지원방법: 아래 지원방법은 단순한 신체지원, 인지지원, 정서지원 표현을 대체하여 실제 제공 가능한 서비스로 적용함. | |
| {support_methods} | |
| """.strip() | |
| return format_numbered_text(normalize_assessment_text(summary)) | |
| def build_enhanced_result_evaluation(data: dict) -> str: | |
| ctx = build_needs_context(data) | |
| support_methods = build_individual_support_methods(data, ctx) | |
| achievement = "부분달성 또는 미달성" if ctx["overall_level"] == "고위험" else "부분달성" if ctx["overall_level"] == "중위험" else "달성" | |
| eval_items = [ | |
| f"욕구사정 총평에서 확인된 {ctx['overall_level']} 수준의 위험요인에 대해 30일간 급여제공 후 목표 달성도는 {achievement}으로 평가됨", | |
| f"낙상예방 목표는 {ctx['fall_level']} 수준에 따라 이동지원, 환경정비, 보호자 교육의 지속 여부를 확인함", | |
| "욕창예방 목표는 피부발적, 체위변경, 영양·수분섭취, 감염징후 관찰 결과를 기준으로 평가함" if ctx["pressure_detected"] else "피부상태는 정기 관찰을 유지하며 압박부위 이상 여부를 다음 평가 시 재확인함", | |
| "인지기능 유지 목표는 기억력, 지남력, BPSD 변화와 보호자 부담 정도를 함께 확인함" if ctx["dementia_detected"] else "인지기능은 현재 관찰 수준을 유지하되 변화 발생 시 재사정을 실시함", | |
| "급여제공계획은 확인된 욕구와 위험요인에 따라 변경 필요성을 검토하고, 상태변화기록·보호자 상담·사례관리 연계 여부를 함께 판단함", | |
| "개인별 이용계획 지원방법은 다음 급여제공 시 실제 서비스 제공 항목으로 적용하고 이행 여부를 확인함\n" + support_methods, | |
| ] | |
| return format_numbered_text(normalize_assessment_text("\n".join([f"{i}. {item}" for i, item in enumerate(eval_items, 1)]))) | |
| # ----------------------------- | |
| # 디자인 | |
| # ----------------------------- | |
| st.markdown( | |
| """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700;800&display=swap'); | |
| html, body, [class*="css"], .stApp { | |
| font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif; | |
| } | |
| .block-container { | |
| padding-top: 1.5rem; | |
| max-width: 1180px; | |
| } | |
| .hero { | |
| background: linear-gradient(135deg, #1B7F5A, #5DBB70); | |
| padding: 34px 36px; | |
| border-radius: 26px; | |
| text-align: center; | |
| margin: 8px 0 24px 0; | |
| box-shadow: 0 8px 20px rgba(27, 127, 90, 0.15); | |
| } | |
| .hero .eyebrow { | |
| color: #EAF7EE; | |
| font-size: 17px; | |
| font-weight: 700; | |
| letter-spacing: 0.02em; | |
| margin-bottom: 8px; | |
| } | |
| .hero h1 { | |
| color: white; | |
| margin: 0; | |
| font-size: clamp(34px, 4.2vw, 54px); | |
| line-height: 1.18; | |
| font-weight: 800; | |
| word-break: keep-all; | |
| } | |
| .hero p { | |
| color: #F4FFF6; | |
| font-size: 20px; | |
| line-height: 1.55; | |
| margin-top: 20px; | |
| word-break: keep-all; | |
| } | |
| .notice { | |
| background: #FFF9E8; | |
| border-left: 7px solid #F6A623; | |
| border-radius: 16px; | |
| padding: 17px 20px; | |
| margin-bottom: 22px; | |
| font-size: 17px; | |
| line-height: 1.65; | |
| } | |
| .section-card { | |
| background: #ffffff; | |
| border: 1px solid #E5E7EB; | |
| border-radius: 18px; | |
| padding: 18px 20px; | |
| margin-bottom: 18px; | |
| } | |
| .stTextInput input, .stTextArea textarea, .stSelectbox div, .stMultiSelect div { | |
| font-size: 16px; | |
| } | |
| .stButton button, .stDownloadButton button { | |
| border-radius: 12px; | |
| font-weight: 700; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| """ | |
| <div class="hero"> | |
| <div class="eyebrow">BeeCare Client Center 연동</div> | |
| <h1>욕구사정기록지</h1> | |
| <p>등록된 수급자 정보를 불러와 욕구사정 총평과 30일 후 결과평가를 깔끔한 문서로 생성합니다.</p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| """ | |
| <div class="notice"> | |
| <strong>중요:</strong> 이 앱은 Client Center의 SQLite DB(data/clients.db)를 불러오는 통합연동형 코드입니다. | |
| DB가 없으면 직접 입력 모드로 사용할 수 있습니다. | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # ----------------------------- | |
| # 수급자 선택 / 직접 입력 | |
| # ----------------------------- | |
| clients_df = load_clients() | |
| if DB_PATH: | |
| st.sidebar.success(f"Client Center DB 연결됨") | |
| st.sidebar.caption(DB_PATH) | |
| else: | |
| st.sidebar.warning("Client Center DB를 찾지 못했습니다. 직접 입력 모드로 실행됩니다.") | |
| mode = st.radio( | |
| "입력 방식 선택", | |
| ["Client Center에서 수급자 선택", "직접 입력"], | |
| horizontal=True, | |
| ) | |
| selected_client = None | |
| if mode == "Client Center에서 수급자 선택" and not clients_df.empty: | |
| options = clients_df.apply( | |
| lambda x: f"{x['id']}번 - {x['masked_name']} / {x['care_grade']} / {x['institution_type']} / {x['status']}", | |
| axis=1, | |
| ).tolist() | |
| selected_option = st.selectbox("수급자 선택", options) | |
| selected_id = int(selected_option.split("번")[0]) | |
| selected_client = clients_df[clients_df["id"] == selected_id].iloc[0] | |
| st.markdown("### 👤 선택된 수급자 기본정보") | |
| c1, c2, c3, c4 = st.columns(4) | |
| c1.metric("성명", safe_value(selected_client, "masked_name")) | |
| c2.metric("성별", safe_value(selected_client, "gender")) | |
| c3.metric("출생연도", safe_value(selected_client, "birth_year")) | |
| c4.metric("등급", safe_value(selected_client, "care_grade")) | |
| c5, c6, c7 = st.columns(3) | |
| c5.metric("기관유형", safe_value(selected_client, "institution_type")) | |
| c6.metric("기관명", safe_value(selected_client, "institution_name")) | |
| c7.metric("상태", safe_value(selected_client, "status")) | |
| c8, c9, c10 = st.columns(3) | |
| c8.metric("인정번호", safe_value(selected_client, "recognition_number", "미입력")) | |
| c9.metric("인정기간 시작일", safe_value(selected_client, "recognition_start_date", "미입력")) | |
| c10.metric("인정기간 종료일", safe_value(selected_client, "recognition_end_date", "미입력")) | |
| elif mode == "Client Center에서 수급자 선택" and clients_df.empty: | |
| st.warning("등록된 수급자 정보가 없거나 DB를 찾지 못했습니다. 직접 입력 모드로 전환하세요.") | |
| # ----------------------------- | |
| # 입력값 구성 | |
| # ----------------------------- | |
| st.markdown("### 📝 욕구사정 기본 입력") | |
| if selected_client is not None: | |
| default_name = safe_value(selected_client, "name") | |
| default_masked_name = safe_value(selected_client, "masked_name") | |
| default_gender = safe_value(selected_client, "gender", "여") | |
| default_birth_year = safe_value(selected_client, "birth_year") | |
| default_grade = safe_value(selected_client, "care_grade", "3등급") | |
| default_institution_type = safe_value(selected_client, "institution_type", "방문요양") | |
| default_institution_name = safe_value(selected_client, "institution_name") | |
| default_recognition_number = safe_value(selected_client, "recognition_number") | |
| default_recognition_start_date = safe_value(selected_client, "recognition_start_date") | |
| default_recognition_end_date = safe_value(selected_client, "recognition_end_date") | |
| default_main_condition = safe_value(selected_client, "main_condition") | |
| default_cognitive_status = safe_value(selected_client, "cognitive_status") | |
| default_mobility_status = safe_value(selected_client, "mobility_status") | |
| default_diet_status = safe_value(selected_client, "diet_status") | |
| default_toileting_status = safe_value(selected_client, "toileting_status") | |
| default_notes = safe_value(selected_client, "notes") | |
| default_guardian_opinion = "" | |
| else: | |
| default_name = "홍길동" | |
| default_masked_name = "홍○동" | |
| default_gender = "여" | |
| default_birth_year = "1945" | |
| default_grade = "3등급" | |
| default_institution_type = "방문요양" | |
| default_institution_name = "" | |
| default_recognition_number = "" | |
| default_recognition_start_date = "" | |
| default_recognition_end_date = "" | |
| default_main_condition = "" | |
| default_cognitive_status = "" | |
| default_mobility_status = "" | |
| default_diet_status = "" | |
| default_toileting_status = "" | |
| default_notes = "" | |
| default_guardian_opinion = "" | |
| grade_options = ["1등급", "2등급", "3등급", "4등급", "5등급", "인지지원등급", "등급외", "미입력"] | |
| gender_options = ["여", "남", "기타"] | |
| institution_options = ["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"] | |
| col1, col2, col3, col4 = st.columns(4) | |
| name = col1.text_input("수급자 성명", value=default_name) | |
| masked_name = col2.text_input("가명 표기", value=default_masked_name) | |
| birth_year = col3.text_input("출생연도", value=default_birth_year) | |
| gender = col4.selectbox( | |
| "성별", | |
| gender_options, | |
| index=gender_options.index(default_gender) if default_gender in gender_options else 0, | |
| ) | |
| col5, col6, col7 = st.columns(3) | |
| grade = col5.selectbox( | |
| "장기요양 등급", | |
| grade_options, | |
| index=grade_options.index(default_grade) if default_grade in grade_options else 2, | |
| ) | |
| institution_type = col6.selectbox( | |
| "기관 유형", | |
| institution_options, | |
| index=institution_options.index(default_institution_type) if default_institution_type in institution_options else 0, | |
| ) | |
| institution_name = col7.text_input("기관명", value=default_institution_name) | |
| col8, col9, col10 = st.columns(3) | |
| recognition_number = col8.text_input("인정번호", value=default_recognition_number, help="Client Center에서 숫자로 입력해도 L이 자동으로 붙어 저장됩니다.") | |
| recognition_start_date = col9.text_input("인정기간 시작일", value=default_recognition_start_date, placeholder="예: 2025-03-03") | |
| recognition_end_date = col10.text_input("인정기간 종료일", value=default_recognition_end_date, placeholder="예: 2027-03-02") | |
| st.markdown("### 🧩 상태 정보") | |
| c1, c2 = st.columns(2) | |
| main_condition = c1.text_area("주요 질환 / 상태", value=default_main_condition, height=90) | |
| cognitive_status = c2.text_area("인지 상태", value=default_cognitive_status, height=90) | |
| c3, c4 = st.columns(2) | |
| mobility_status = c3.text_area("이동 상태", value=default_mobility_status, height=90) | |
| diet_status = c4.text_area("식사 상태", value=default_diet_status, height=90) | |
| c5, c6 = st.columns(2) | |
| toileting_status = c5.text_area("배변 / 배뇨 상태", value=default_toileting_status, height=90) | |
| notes = c6.text_area("특이사항 / 메모", value=default_notes, height=90) | |
| guardian_opinion = st.text_area( | |
| "보호자 의견", | |
| value=default_guardian_opinion, | |
| height=90, | |
| placeholder="예: 최근 어지럼이 잦아 낙상이 걱정됨, 돌봄 부담이 커 서비스 조정 상담을 희망함", | |
| ) | |
| st.markdown("### 📌 욕구사정 세부 체크") | |
| cc1, cc2, cc3, cc4 = st.columns(4) | |
| fall_risk = cc1.selectbox("낙상 위험", ["낮음", "보통", "높음"]) | |
| bedsore_risk = cc2.selectbox("욕창 위험", ["낮음", "보통", "높음"]) | |
| dementia_status = cc3.selectbox("인지저하 / 치매", ["없음", "의심", "진단 있음", "중증"]) | |
| diaper_use = cc4.selectbox("기저귀 사용", ["미사용", "일부 사용", "상시 사용"]) | |
| service_needs = st.multiselect( | |
| "주요 서비스 욕구", | |
| [ | |
| "신체활동 지원", | |
| "인지활동 지원", | |
| "정서지원", | |
| "식사 지원", | |
| "배변/배뇨 지원", | |
| "이동 지원", | |
| "낙상예방", | |
| "욕창예방", | |
| "복약확인", | |
| "가족상담", | |
| ], | |
| default=["신체활동 지원", "정서지원", "낙상예방"], | |
| ) | |
| # ----------------------------- | |
| # AI 전문 소견 생성 | |
| # ----------------------------- | |
| st.markdown("---") | |
| st.subheader("🤖 전문 소견 생성") | |
| system_prompt = ( | |
| "당신은 장기요양기관 평가와 문서작성에 익숙한 20년 경력의 사회복지사입니다. " | |
| "평가기준에 맞는 전문적이고 따뜻한 문체로 작성하세요. " | |
| "개인정보는 과도하게 노출하지 말고, 수급자 상태와 급여 반영 방향이 드러나게 작성하세요." | |
| ) | |
| user_summary = f""" | |
| 수급자: {masked_name} | |
| 성별: {gender} | |
| 출생연도: {birth_year} | |
| 장기요양등급: {grade} | |
| 기관유형: {institution_type} | |
| 인정번호: {recognition_number} | |
| 인정기간: {recognition_start_date} ~ {recognition_end_date} | |
| 주요질환/상태: {main_condition} | |
| 인지상태: {cognitive_status} | |
| 이동상태: {mobility_status} | |
| 식사상태: {diet_status} | |
| 배변/배뇨상태: {toileting_status} | |
| 낙상위험: {fall_risk} | |
| 욕창위험: {bedsore_risk} | |
| 인지저하/치매: {dementia_status} | |
| 기저귀사용: {diaper_use} | |
| 주요 서비스 욕구: {', '.join(service_needs)} | |
| 특이사항: {notes} | |
| 보호자 의견: {guardian_opinion} | |
| """ | |
| needs_data = { | |
| "masked_name": masked_name, | |
| "gender": gender, | |
| "birth_year": birth_year, | |
| "grade": grade, | |
| "institution_type": institution_type, | |
| "institution_name": institution_name, | |
| "recognition_number": recognition_number, | |
| "recognition_start_date": recognition_start_date, | |
| "recognition_end_date": recognition_end_date, | |
| "main_condition": main_condition, | |
| "cognitive_status": cognitive_status, | |
| "mobility_status": mobility_status, | |
| "diet_status": diet_status, | |
| "toileting_status": toileting_status, | |
| "fall_risk": fall_risk, | |
| "bedsore_risk": bedsore_risk, | |
| "dementia_status": dementia_status, | |
| "diaper_use": diaper_use, | |
| "service_needs": service_needs, | |
| "notes": notes, | |
| "guardian_opinion": guardian_opinion, | |
| } | |
| needs_context = build_needs_context(needs_data) | |
| auto_support_methods = build_individual_support_methods(needs_data, needs_context) | |
| st.markdown("### 🧭 자동 연계 판단") | |
| flag_cols = st.columns(4) | |
| for idx, (flag_name, enabled) in enumerate(needs_context["flags"].items()): | |
| flag_cols[idx].metric(flag_name, "필요" if enabled else "관찰") | |
| st.caption(f"낙상 위험도: {needs_context['fall_level']} / BPSD 위험도: {needs_context['bpsd_level']} / 종합 위험도: {needs_context['overall_level']}") | |
| col_ai1, col_ai2 = st.columns(2) | |
| with col_ai1: | |
| if st.button("욕구사정 총평 생성"): | |
| if USE_LM_STUDIO: | |
| prompt = user_summary + "\n현재상태, 핵심문제, 수급자 욕구, 보호자 욕구, 장단기 목표, 급여제공 방향이 모두 드러나도록 평가 적합 문체로 욕구사정 종합소견을 1번부터 5번까지 작성해줘. 개인별 이용계획 지원방법은 아래 실제 제공 항목을 반영해줘.\n" + auto_support_methods + "\n금지문구는 사용하지 마." | |
| generated = get_ai_content(system_prompt, prompt) | |
| st.session_state["총평"] = format_numbered_text(normalize_assessment_text(generated + "\n\n" + auto_support_methods)) | |
| else: | |
| st.session_state["총평"] = build_enhanced_needs_summary(needs_data) | |
| with col_ai2: | |
| if st.button("30일 후 결과평가 생성"): | |
| if USE_LM_STUDIO: | |
| prompt = user_summary + "\n욕구사정 총평과 연결되도록 목표별 달성/부분달성/미달성 평가, 급여제공계획 반영 방향, 상태변화기록·사례관리·보호자상담 필요 여부를 포함하여 1번부터 5번까지 작성해줘. 개인별 이용계획 지원방법 이행 여부도 포함해줘.\n" + auto_support_methods + "\n금지문구는 사용하지 마." | |
| generated = get_ai_content(system_prompt, prompt) | |
| st.session_state["결과평가"] = format_numbered_text(normalize_assessment_text(generated + "\n\n" + auto_support_methods)) | |
| else: | |
| st.session_state["결과평가"] = build_enhanced_result_evaluation(needs_data) | |
| v_total = st.text_area( | |
| "11. 총평 / 종합소견", | |
| value=st.session_state.get("총평", ""), | |
| height=170, | |
| help="생성 버튼을 누르면 번호순으로 정돈되어 작성됩니다. 직접 입력한 문장도 다운로드 시 번호형으로 정리됩니다.", | |
| ) | |
| v_total_clean = format_numbered_text(v_total) | |
| v_support_methods = st.text_area( | |
| "개인별 이용계획 지원방법", | |
| value=st.session_state.get("지원방법", auto_support_methods), | |
| height=190, | |
| help="규칙에 따라 신체지원, 인지지원, 정서지원 같은 추상어 대신 실제 제공 가능한 지원방법이 자동 선택됩니다.", | |
| ) | |
| v_support_methods_clean = normalize_assessment_text(v_support_methods) | |
| st.markdown("---") | |
| st.subheader("📊 30일 후 결과평가") | |
| v_result = st.selectbox("해결 여부", ["해결됨", "부분 해결", "진행 중", "재사정 필요"]) | |
| v_next_plan = st.text_area( | |
| "미해결 시 조치 계획 및 향후 급여 반영 계획", | |
| value=st.session_state.get("결과평가", ""), | |
| height=170, | |
| help="다운로드 시 번호순 문장으로 정돈됩니다.", | |
| ) | |
| v_next_plan_clean = format_numbered_text(v_next_plan) | |
| auto_link_judgement = " / ".join([f"□ {name}" for name, enabled in needs_context["flags"].items() if enabled]) or "□ 정기 관찰 유지" | |
| auto_risk_summary = f"낙상 위험도: {needs_context['fall_level']} / BPSD 위험도: {needs_context['bpsd_level']} / 종합 위험도: {needs_context['overall_level']}" | |
| # ----------------------------- | |
| # 엑셀 생성 | |
| # ----------------------------- | |
| def set_cell(ws, cell, value, bold=False, size=11, fill=None): | |
| ws[cell] = value | |
| ws[cell].font = Font(name="Malgun Gothic", size=size, bold=bold) | |
| ws[cell].alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) | |
| if fill: | |
| ws[cell].fill = fill | |
| def generate_excel(): | |
| wb = Workbook() | |
| thin = Side(style="thin", color="888888") | |
| border = Border(left=thin, right=thin, top=thin, bottom=thin) | |
| title_fill = PatternFill("solid", fgColor="E2F0D9") | |
| header_fill = PatternFill("solid", fgColor="F2F2F2") | |
| def style_range(ws, cell_range, border_on=True, fill=None, font=None, alignment=None): | |
| """병합 셀 포함 범위 전체에 스타일 적용""" | |
| for row in ws[cell_range]: | |
| for cell in row: | |
| if border_on: | |
| cell.border = border | |
| if fill: | |
| cell.fill = fill | |
| if font: | |
| cell.font = font | |
| if alignment: | |
| cell.alignment = alignment | |
| def write_cell(ws, cell, value, bold=False, size=11, fill=None, align="center"): | |
| ws[cell] = value | |
| ws[cell].font = Font(name="Malgun Gothic", size=size, bold=bold) | |
| ws[cell].alignment = Alignment(horizontal=align, vertical="center", wrap_text=True) | |
| ws[cell].border = border | |
| if fill: | |
| ws[cell].fill = fill | |
| def approval_box(ws, start_col, end_col): | |
| """큰제목 우측 상단 결재란: 3번째 이미지처럼 제목과 같은 높이에 배치""" | |
| headers = ["담당자", "시설장"] | |
| for idx, col in enumerate(range(start_col, end_col + 1)): | |
| c1 = ws.cell(1, col) | |
| c2 = ws.cell(2, col) | |
| c1.value = headers[idx] if idx < len(headers) else "" | |
| c2.value = "" | |
| c1.font = Font(name="Malgun Gothic", size=12, bold=True) | |
| c2.font = Font(name="Malgun Gothic", size=11) | |
| c1.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) | |
| c2.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) | |
| c1.fill = header_fill | |
| c1.border = border | |
| c2.border = border | |
| # ===================================================== | |
| # 시트 1: 욕구사정평가서 | |
| # ===================================================== | |
| ws1 = wb.active | |
| ws1.title = "욕구사정기록지" | |
| # 엑셀 기본 눈금선 제거 (깔끔한 표시) | |
| ws1.sheet_view.showGridLines = False | |
| widths = {"A": 16, "B": 16, "C": 16, "D": 16, "E": 16, "F": 16, "G": 16, "H": 14, "I": 14} | |
| for col, width in widths.items(): | |
| ws1.column_dimensions[col].width = width | |
| ws1.merge_cells("A1:G2") | |
| write_cell(ws1, "A1", "욕 구 사 정 기 록 지", True, 26, title_fill) | |
| 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)) | |
| approval_box(ws1, 8, 9) | |
| ws1.row_dimensions[1].height = 42 | |
| ws1.row_dimensions[2].height = 52 | |
| basic_rows = [ | |
| ("수급자명", masked_name, "성별", gender, "출생연도", birth_year), | |
| ("장기요양등급", grade, "기관유형", institution_type, "기관명", institution_name), | |
| ("인정번호", recognition_number, "인정기간 시작일", recognition_start_date, "인정기간 종료일", recognition_end_date), | |
| ("작성일자", str(date.today()), "낙상위험", fall_risk, "욕창위험", bedsore_risk), | |
| ("인지저하/치매", dementia_status, "기저귀 사용", diaper_use, "해결여부", v_result), | |
| ] | |
| row_num = 3 | |
| for row in basic_rows: | |
| ws1.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=3) | |
| ws1.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) | |
| ws1.merge_cells(start_row=row_num, start_column=8, end_row=row_num, end_column=9) | |
| label_value_pairs = [(1, row[0]), (2, row[1]), (4, row[2]), (5, row[3]), (7, row[4]), (8, row[5])] | |
| for col, value in label_value_pairs: | |
| is_label = col in [1, 4, 7] | |
| write_cell(ws1, ws1.cell(row_num, col).coordinate, value, bold=is_label, size=11, fill=header_fill if is_label else None) | |
| style_range(ws1, f"A{row_num}:I{row_num}") | |
| ws1.row_dimensions[row_num].height = 28 | |
| row_num += 1 | |
| detail_rows = [ | |
| ("주요 질환 / 상태", main_condition), | |
| ("인지 상태", cognitive_status), | |
| ("이동 상태", mobility_status), | |
| ("식사 상태", diet_status), | |
| ("배변 / 배뇨 상태", toileting_status), | |
| ("보호자 의견", guardian_opinion), | |
| ("주요 서비스 욕구", ", ".join(service_needs)), | |
| ("자동 연계 판단", auto_link_judgement), | |
| ("자동 위험도", auto_risk_summary), | |
| ("개인별 이용계획 지원방법", v_support_methods_clean), | |
| ("특이사항", notes), | |
| ("총평 / 종합소견", v_total_clean), | |
| ] | |
| for label, value in detail_rows: | |
| ws1.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=9) | |
| write_cell(ws1, f"A{row_num}", label, True, 11, header_fill) | |
| write_cell(ws1, f"B{row_num}", value, False, 11, None, align="left") | |
| ws1[f"B{row_num}"].alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) | |
| style_range(ws1, f"A{row_num}:I{row_num}") | |
| if label in ["총평 / 종합소견", "개인별 이용계획 지원방법"]: | |
| ws1.row_dimensions[row_num].height = 110 | |
| else: | |
| ws1.row_dimensions[row_num].height = 50 | |
| row_num += 1 | |
| ws1.freeze_panes = "A3" | |
| ws1.page_setup.orientation = "landscape" | |
| ws1.page_setup.fitToWidth = 1 | |
| ws1.page_setup.fitToHeight = 0 | |
| ws1.sheet_properties.pageSetUpPr.fitToPage = True | |
| ws1.print_area = f"A1:I{row_num - 1}" | |
| # ===================================================== | |
| # 시트 2: 30일 후 결과평가 기록지 | |
| # ===================================================== | |
| ws2 = wb.create_sheet("결과평가서") | |
| # 엑셀 기본 눈금선 제거 (지저분한 여백 제거) | |
| ws2.sheet_view.showGridLines = False | |
| for col in "ABCDE": | |
| ws2.column_dimensions[col].width = 24 | |
| ws2.merge_cells("A1:C2") | |
| write_cell(ws2, "A1", "30일 후 결과평가 기록지", True, 24, title_fill) | |
| 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)) | |
| approval_box(ws2, 4, 5) | |
| ws2.row_dimensions[1].height = 42 | |
| ws2.row_dimensions[2].height = 52 | |
| result_rows = [ | |
| ("수급자 성명", masked_name), | |
| ("장기요양등급", grade), | |
| ("기관유형", institution_type), | |
| ("인정번호", recognition_number), | |
| ("인정기간", f"{recognition_start_date} ~ {recognition_end_date}"), | |
| ("평가일자", str(date.today())), | |
| ("해결 여부", v_result), | |
| ("자동 연계 판단", auto_link_judgement), | |
| ("자동 위험도", auto_risk_summary), | |
| ("개인별 이용계획 지원방법", v_support_methods_clean), | |
| ("결과평가 및 조치계획", v_next_plan_clean), | |
| ] | |
| r = 3 | |
| for label, value in result_rows: | |
| ws2.merge_cells(start_row=r, start_column=2, end_row=r, end_column=5) | |
| write_cell(ws2, f"A{r}", label, True, 11, header_fill) | |
| write_cell(ws2, f"B{r}", value, False, 11, None, align="left") | |
| ws2[f"B{r}"].alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) | |
| style_range(ws2, f"A{r}:E{r}") | |
| if label in ["개인별 이용계획 지원방법", "결과평가 및 조치계획"]: | |
| ws2.row_dimensions[r].height = 110 | |
| else: | |
| ws2.row_dimensions[r].height = 48 | |
| r += 1 | |
| ws2.freeze_panes = "A3" | |
| ws2.page_setup.orientation = "landscape" | |
| ws2.page_setup.fitToWidth = 1 | |
| ws2.page_setup.fitToHeight = 0 | |
| ws2.sheet_properties.pageSetUpPr.fitToPage = True | |
| ws2.print_area = f"A1:E{r - 1}" | |
| # 시트 3(2차사례관리결과평가서) 생성 코드 제거됨 | |
| buffer = BytesIO() | |
| wb.save(buffer) | |
| buffer.seek(0) | |
| return buffer | |
| file_name = f"욕구사정_결과평가_{masked_name}_{datetime.now().strftime('%Y%m%d')}.xlsx" | |
| if st.button("📦 엑셀 파일 준비하기", use_container_width=True, key="prepare_needs_excel_fast"): | |
| st.session_state["needs_excel_fast"] = generate_excel() | |
| st.success("엑셀 파일 준비가 완료되었습니다. 아래 다운로드 버튼을 눌러주세요.") | |
| if "needs_excel_fast" in st.session_state: | |
| st.download_button( | |
| "📥 통합 엑셀 다운로드", | |
| data=st.session_state["needs_excel_fast"], | |
| file_name=file_name, | |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| use_container_width=True, | |
| key="download_needs_excel_fast", | |
| ) | |
| st.caption("빠른 버전: 화면 생성 후, 엑셀은 필요할 때만 준비합니다.") | |