import random 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 openpyxl.utils import get_column_letter from openpyxl.worksheet.pagebreak import Break from openai import OpenAI st.set_page_config(page_title="상담일지", page_icon="πŸ“", layout="wide") # ----------------------------- # DB 경둜 μ„€μ • # ----------------------------- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) DB_CANDIDATES = [ "/app/data/clients.db", 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"), os.path.join(CURRENT_DIR, "data", "clients.db"), ] DB_PATH = None for path in DB_CANDIDATES: if os.path.exists(path): DB_PATH = path break if DB_PATH is None: os.makedirs(os.path.join(CURRENT_DIR, "data"), exist_ok=True) DB_PATH = os.path.join(CURRENT_DIR, "data", "clients.db") # ----------------------------- # DB μ΄ˆκΈ°ν™” ν•¨μˆ˜ # ----------------------------- def init_db(): try: conn = sqlite3.connect(DB_PATH) cur = conn.cursor() # clients ν…Œμ΄λΈ” (κΈ°μ‘΄ ν…Œμ΄λΈ” 쑴재 μ‹œ λ¬΄μ‹œ) cur.execute(""" CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, masked_name TEXT, gender TEXT, birth_year TEXT, care_grade TEXT, institution_type TEXT, institution_name TEXT, recognition_number TEXT, recognition_start_date TEXT, recognition_end_date TEXT, status TEXT ) """) # counseling_records ν…Œμ΄λΈ” cur.execute(""" CREATE TABLE IF NOT EXISTS counseling_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER, client_masked_name TEXT, institution_type TEXT, counseling_type TEXT, counseling_date TEXT, counselor TEXT, counseling_reason TEXT, client_status_change TEXT, guardian_opinion TEXT, counseling_content TEXT, action_plan TEXT, evaluation_score INTEGER, missing_items TEXT, compliance_result TEXT, next_due_note TEXT, created_at TEXT ) """) # 동적 컬럼 μΆ”κ°€ (ALTER TABLE) cur.execute("PRAGMA table_info(counseling_records)") existing_cols = [r[1] for r in cur.fetchall()] new_cols = { "counseling_purpose": "TEXT", "selected_items": "TEXT", "keywords": "TEXT", "additional_needs": "TEXT", "case_needed": "INTEGER DEFAULT 0", "needs_assessment_needed": "INTEGER DEFAULT 0", "careplan_change_needed": "INTEGER DEFAULT 0", "care_grade_review_needed": "INTEGER DEFAULT 0", "followup_counseling_needed": "INTEGER DEFAULT 0", } for col, col_type in new_cols.items(): if col not in existing_cols: cur.execute(f"ALTER TABLE counseling_records ADD COLUMN {col} {col_type}") conn.commit() conn.close() except Exception as e: st.sidebar.error(f"DB μ΄ˆκΈ°ν™” μ‹€νŒ¨: {e}") init_db() # ----------------------------- # μˆ˜κΈ‰μž 정보 λ‘œλ“œ ν•¨μˆ˜ # ----------------------------- def load_clients() -> pd.DataFrame: # 1. DBμ—μ„œ λ‘œλ“œ μ‹œλ„ if DB_PATH and os.path.exists(DB_PATH): try: conn = sqlite3.connect(DB_PATH) 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", "status" ] select_cols = [c for c in wanted_cols if c in available_cols] query = "SELECT " + ", ".join(select_cols) + " FROM clients ORDER BY id DESC" df = pd.read_sql_query(query, conn) conn.close() # DB 컬럼 ν•œκΈ€ 바인딩 db_to_excel_map = { "name": "μˆ˜κΈ‰μžμ΄λ¦„", "gender": "성별", "birth_year": "생년월일", "care_grade": "μž₯κΈ°μš”μ–‘λ“±κΈ‰", "recognition_number": "μΈμ •λ²ˆν˜Έ", "recognition_start_date": "인정기간 μ‹œμž‘μΌ", "recognition_end_date": "인정기간 μ’…λ£ŒμΌ", "institution_type": "κΈ°κ΄€μœ ν˜•", "institution_name": "κΈ°κ΄€λͺ…" } for db_col, kor_col in db_to_excel_map.items(): if db_col in df.columns: df[kor_col] = df[db_col] if "μˆ˜κΈ‰μžμ΄λ¦„" not in df.columns and "name" in df.columns: df["μˆ˜κΈ‰μžμ΄λ¦„"] = df["name"] return df except Exception as e: st.sidebar.warning(f"DB λ‘œλ“œ μ‹€νŒ¨: {e}. Excel μ‹œλ„ 쀑...") # 2. Excelμ—μ„œ λ‘œλ“œ μ‹œλ„ excel_path = os.path.join(CURRENT_DIR, "data", "clients.xlsx") if os.path.exists(excel_path): try: df = pd.read_excel(excel_path) if "μˆ˜κΈ‰μžλͺ…" in df.columns and "μˆ˜κΈ‰μžμ΄λ¦„" not in df.columns: df["μˆ˜κΈ‰μžμ΄λ¦„"] = df["μˆ˜κΈ‰μžλͺ…"] expected = ["μˆ˜κΈ‰μžμ΄λ¦„", "성별", "생년월일", "μž₯κΈ°μš”μ–‘λ“±κΈ‰", "μΈμ •λ²ˆν˜Έ", "인정기간 μ‹œμž‘μΌ", "인정기간 μ’…λ£ŒμΌ", "κΈ°κ΄€μœ ν˜•", "κΈ°κ΄€λͺ…"] for col in expected: if col not in df.columns: df[col] = "" return df except Exception as e: st.sidebar.error(f"Excel λ‘œλ“œ μ‹€νŒ¨: {e}") return pd.DataFrame() # ----------------------------- # AI ν”„λ‘¬ν”„νŠΈ κ·œμΉ™ μ •μ˜ # ----------------------------- COUNSELING_GENERATION_RULE = """ λ„ˆλŠ” μž₯κΈ°μš”μ–‘κΈ°κ΄€ 상담일지 μž‘μ„± 보쑰 AI이닀. λ°˜λ“œμ‹œ μ‚¬μš©μžκ°€ μž…λ ₯ν•œ λ‚΄μš©λ§Œ μ‚¬μš©ν•œλ‹€. μ„ νƒλœ ν•­λͺ©, 핡심단어, μΆ”κ°€μš•κ΅¬/μš”μ²­μ‚¬ν•­μ— μ—†λŠ” λ‚΄μš©μ€ μž‘μ„±ν•˜μ§€ μ•ŠλŠ”λ‹€. μš•μ°½, 배회, 곡격성, κ·€κ°€μš•κ΅¬, 낙상, μΉ˜λ§€ν–‰λ™ 등은 μž…λ ₯된 κ²½μš°μ—λ§Œ μž‘μ„±ν•œλ‹€. AIλŠ” μƒˆλ‘œμš΄ 사싀을 λ§Œλ“€μ§€ μ•Šκ³  λ¬Έμž₯을 μžμ—°μŠ€λŸ½κ²Œ μ •λ¦¬λ§Œ ν•œλ‹€. 상담 λͺ©μ μ΄ 사둀관리 κ²€ν†  상담이면 사둀관리 ν•„μš” μ‚¬μœ  μ€‘μ‹¬μœΌλ‘œ μž‘μ„±ν•œλ‹€. 상담 λͺ©μ μ΄ μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  상담이면 μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  μ‚¬μœ  μ€‘μ‹¬μœΌλ‘œ μž‘μ„±ν•œλ‹€. 상담 λͺ©μ μ΄ κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ 상담이면 κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ μ‚¬μœ  μ€‘μ‹¬μœΌλ‘œ μž‘μ„±ν•œλ‹€. 볡합 상담이면 체크된 ν•­λͺ©λ³„λ‘œ κ΅¬λΆ„ν•˜μ—¬ μž‘μ„±ν•œλ‹€. μž‘μ„± ν˜•μ‹: 1. 상담 μ‚¬μœ  2. 상담 λ‚΄μš© 3. 전문적 사정 4. μ‘°μΉ˜κ³„νš 5. 연동 ν•„μš” ν•­λͺ© """ # ----------------------------- # 상담일지 λ¬Έμž₯ νŒŒμ‹± ν•¨μˆ˜ # ----------------------------- def parse_sections(text): sections = { "상담 μ‚¬μœ ": "", "상담 λ‚΄μš©": "", "전문적 사정": "", "μ‘°μΉ˜κ³„νš": "", "연동 ν•„μš” ν•­λͺ©": "" } import re headers = [ ("상담 μ‚¬μœ ", ["1. 상담 μ‚¬μœ ", "상담 μ‚¬μœ :", "상담 μ‚¬μœ "]), ("상담 λ‚΄μš©", ["2. 상담 λ‚΄μš©", "상담 λ‚΄μš©:", "상담 λ‚΄μš©"]), ("전문적 사정", ["3. 전문적 사정", "전문적 사정:", "전문적 사정"]), ("μ‘°μΉ˜κ³„νš", ["4. μ‘°μΉ˜κ³„νš", "μ‘°μΉ˜κ³„νš:", "μ‘°μΉ˜κ³„νš"]), ("연동 ν•„μš” ν•­λͺ©", ["5. 연동 ν•„μš” ν•­λͺ©", "연동 ν•„μš” ν•­λͺ©:", "연동 ν•„μš” ν•­λͺ©"]) ] current_text = text for i in range(len(headers)): header_name, header_aliases = headers[i] next_header_aliases = headers[i+1][1] if i+1 < len(headers) else [] start_idx = -1 matched_alias = "" for alias in header_aliases: idx = current_text.find(alias) if idx != -1: start_idx = idx matched_alias = alias break if start_idx != -1: end_idx = len(current_text) for next_alias in next_header_aliases: n_idx = current_text.find(next_alias, start_idx + len(matched_alias)) if n_idx != -1: end_idx = n_idx break section_content = current_text[start_idx + len(matched_alias):end_idx].strip() section_content = re.sub(r'^[\s\:\-\=]*', '', section_content).strip() sections[header_name] = section_content # νŒŒμ‹± μ‹€νŒ¨ μ‹œ λŒ€μ²΄ λΆ„ν•  if not sections["상담 μ‚¬μœ "] and not sections["상담 λ‚΄μš©"]: lines = text.split("\n\n") if len(lines) >= 5: sections["상담 μ‚¬μœ "] = lines[0].strip() sections["상담 λ‚΄μš©"] = lines[1].strip() sections["전문적 사정"] = lines[2].strip() sections["μ‘°μΉ˜κ³„νš"] = lines[3].strip() sections["연동 ν•„μš” ν•­λͺ©"] = lines[4].strip() return sections # ----------------------------- # κ·œμΉ™ 기반 λ¬Έμž₯ 컴파일러 (AI μ˜€ν”„λΌμΈ λŒ€λΉ„) # ----------------------------- def generate_rule_based_counseling_text(data): name = data["masked_name"] grade = data["care_grade"] inst_type = data["institution_type"] reason = data["counseling_reason"] purpose = data["counseling_purpose"] selected_items_str = ", ".join(data.get("selected_items_list", [])) keywords = data.get("keywords", "").strip() additional_needs = data.get("additional_needs", "").strip() # μž…λ ₯ 정보 기반 μœ„ν—˜ ν‚€μ›Œλ“œ 탐지 combined_inputs = keywords + " " + additional_needs has_fall = any(k in combined_inputs for k in ["낙상", "λ„˜μ–΄", "미끄", "μ–΄μ§€", "보행", "λΆ€μΆ•", "μ§€νŒ‘", "μ›Œμ»€", "νœ μ²΄μ–΄"]) has_bedsore = any(k in combined_inputs for k in ["μš•μ°½", "발적", "μƒμ²˜", "ν”ΌλΆ€", "짓무", "μ²΄μœ„", "침상"]) has_dementia = any(k in combined_inputs for k in ["치맀", "인지", "κΈ°μ–΅", "배회", "망상", "λΆˆμ•ˆ", "κ·€κ°€", "집에", "ν˜Όλ™"]) has_guardian = any(k in combined_inputs for k in ["νž˜λ“€", "μ†Œμ§„", "λΆ€λ‹΄", "μš”μ²­", "κ°€μ‘±", "보호자"]) # 1. 상담 μ‚¬μœ  reason_lines = [] reason_lines.append(f"{name} μˆ˜κΈ‰μžλŠ” μž₯κΈ°μš”μ–‘ {grade} λ“±κΈ‰μœΌλ‘œμ„œ {inst_type} μ„œλΉ„μŠ€ 이용 쀑, [{reason}] μ‚¬μœ λ‘œ 상담을 μ‹€μ‹œν•˜μ˜€μŠ΅λ‹ˆλ‹€.") if purpose != "일반 상담": reason_lines.append(f"이번 상담은 μˆ˜κΈ‰μžμ˜ 상황 변화에 λ”°λ₯Έ [{purpose}] 진행을 λͺ©μ μœΌλ‘œ ν•©λ‹ˆλ‹€.") counseling_reason_text = " ".join(reason_lines) # 2. 상담 λ‚΄μš© content_lines = [] if selected_items_str: content_lines.append(f"상담 λͺ©μ μ— 따라 μ„ νƒν•œ κ²€ν†  ν•­λͺ©({selected_items_str})을 μ€‘μ‹¬μœΌλ‘œ ν˜„μž¬ μƒνƒœμ™€ μ£Όμš” μ• λ‘œμ‚¬ν•­μ„ μ²­μ·¨ν•˜κ³  ν™•μΈν•˜μ˜€μŠ΅λ‹ˆλ‹€.") if keywords: content_lines.append(f"μˆ˜κΈ‰μž 및 κ΄€λ ¨μΈμ˜ 핡심 μš”μ²­ μ‚¬ν•­μœΌλ‘œ '{keywords}' κ΄€λ ¨ λ‚΄μš©μ΄ μ§μ ‘μ μœΌλ‘œ μ œμ‹œλ˜μ—ˆμœΌλ©° 이에 λŒ€ν•΄ λ©΄λ°€νžˆ 이야기λ₯Ό λ‚˜λˆ„μ—ˆμŠ΅λ‹ˆλ‹€.") if additional_needs: content_lines.append(f"μΆ”κ°€μ μœΌλ‘œ μ œμ‹œλœ μš•κ΅¬ 및 ꡬ체적 μƒν™©μœΌλ‘œ '{additional_needs}'κ°€ νŒŒμ•…λ˜μ—ˆμŠ΅λ‹ˆλ‹€.") risk_mentions = [] if has_fall: risk_mentions.append("보행 λΆˆμ•ˆμ •μ„± 및 낙상 μœ„ν—˜ 예방 ν•„μš”μ„±") if has_bedsore: risk_mentions.append("와상 μƒνƒœ 및 μž₯μ‹œκ°„ 침상 μƒν™œλ‘œ μΈν•œ ν”ΌλΆ€ 발적/μš•μ°½ 예방 관리") if has_dementia: risk_mentions.append("인지 κΈ°λŠ₯ 변화에 λ”°λ₯Έ 행동 변화와 μ •μ„œ μ•ˆμ •μ  μ‘λŒ€ ν•„μš”μ„±") if has_guardian: risk_mentions.append("주수 λŒλ΄„ 보호자의 λΆ€μ–‘ λΆ€λ‹΄ 및 μ„œλΉ„μŠ€ μ‘°μ • 의견") if risk_mentions: content_lines.append("상담 쀑 μž…λ ₯된 ꡬ체적 μƒνƒœ 및 κ΄€μ°° μ‚¬ν•­μœΌλ‘œ " + ", ".join(risk_mentions) + "에 κ΄€ν•œ μ„ΈλΆ€ 정황을 ν™•μΈν•˜μ˜€μŠ΅λ‹ˆλ‹€.") if not content_lines: content_lines.append("μˆ˜κΈ‰μž 및 보호자의 일상적인 μƒν™œ μƒνƒœμ™€ μ„œλΉ„μŠ€ λ§Œμ‘±λ„λ₯Ό ν™•μΈν•˜κ³  격렀의 말씀을 μ „ν•˜μ˜€μŠ΅λ‹ˆλ‹€.") counseling_content_text = " ".join(content_lines) # 3. 전문적 사정 assessment_lines = [] if selected_items_str: assessment_lines.append(f"μ„ νƒλœ κ²€ν†  ν•­λͺ©({selected_items_str})κ³Ό κ΄€λ ¨ν•˜μ—¬ κΈ°μ‘΄ κΈ‰μ—¬μ œκ³΅κ³„νšμ˜ μ μ ˆμ„± μž¬κ²€ν†  및 μ‹€μ§ˆμ μΈ λŒλ΄„ 방법 쑰정이 μš”κ΅¬λ˜λŠ” μƒνƒœλ‘œ νŒλ‹¨λ©λ‹ˆλ‹€.") if keywords: assessment_lines.append(f"μž…λ ₯된 ν‚€μ›Œλ“œ '{keywords}'λ₯Ό λ°”νƒ•μœΌλ‘œ μ‚¬μ •ν•œ κ²°κ³Ό, κΈ°λŠ₯ μ €ν•˜λ‚˜ 심리적 λΆˆνŽΈκ°μ„ μ™„ν™”ν•˜κΈ° μœ„ν•œ μš”μ–‘λ³΄ν˜Έμ‚¬μ˜ 적극적인 지원과 관찰이 μ€‘μš”ν•©λ‹ˆλ‹€.") if additional_needs: assessment_lines.append(f"μΆ”κ°€ μš”μ²­μΈ '{additional_needs}'에 κ·Όκ±°ν•  λ•Œ, κ°€μ • λ‚΄ ν™˜κ²½ κ°œμ„ μ΄λ‚˜ μœ κ΄€ μ„œλΉ„μŠ€ λ³€κ²½ κ²€ν† κ°€ 즉각 반영될 ν•„μš”κ°€ μžˆλŠ” 사정 κ²°κ³Όλ₯Ό μ–»μ—ˆμŠ΅λ‹ˆλ‹€.") risk_assess = [] if has_fall: risk_assess.append("낙상 λ°©μ§€λ₯Ό μœ„ν•œ 이동 μ‹œ 전적인 λΆ€μΆ• 및 ν™”μž₯μ‹€ 동행 μ•ˆμ „ 확보") if has_bedsore: risk_assess.append("μ²΄μœ„ λ³€κ²½ λΉˆλ„ μ€€μˆ˜ 및 꼬리뼈 λΆ€μœ„ λ“± 발적 집쀑 κ΄€μ°°") if has_dementia: risk_assess.append("배회 및 치맀 인지 혼돈 μ˜ˆλ°©μ„ μœ„ν•œ 일관적 ν™˜κ²½ μ‘°μ„± 및 곡감적 μ˜μ‚¬μ†Œν†΅ 적용") if risk_assess: assessment_lines.append("μ „λ¬Έ 사정상 " + ", ".join(risk_assess) + " κ΄€λ ¨ κ°œλ³„ μΌ€μ–΄ μ „λž΅ 수립이 ν•΅μ‹¬μ μž„μ΄ λ„μΆœλ˜μ—ˆμŠ΅λ‹ˆλ‹€.") if not assessment_lines: assessment_lines.append("μ „λ°˜μ μΈ μˆ˜κΈ‰μžμ˜ κΈ°λŠ₯ μƒνƒœλŠ” μ•ˆμ •μ μœΌλ‘œ μœ μ§€λ˜κ³  있으며, κΈ‰κ²©ν•œ μƒνƒœ μ €ν•˜ μ§•ν›„λ‚˜ μœ„ν—˜ μš”μΈμ€ μ‚¬μ •λ˜μ§€ μ•Šμ•„ ν˜„ν–‰ 관리λ₯Ό μœ μ§€ν•©λ‹ˆλ‹€.") professional_assessment_text = " ".join(assessment_lines) # 4. μ‘°μΉ˜κ³„νš plan_lines = [] plan_lines.append("상담 κ³Όμ •μ—μ„œ μˆ˜μ§‘λœ λ‚΄μš©κ³Ό 사정 μ˜κ²¬μ„ μ„œλΉ„μŠ€ 제곡 인λ ₯(μš”μ–‘λ³΄ν˜Έμ‚¬ λ“±)κ³Ό κ³΅μœ ν•˜κ³ , ν•„μš”ν•œ 쑰치λ₯Ό μ΄ν–‰ν•˜κΈ°λ‘œ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.") interlock_actions = [] if data["case_needed"]: interlock_actions.append("사둀관리 회의 μ•ˆκ±΄μœΌλ‘œ μƒμ •ν•˜μ—¬ 닀각적 지원 λ°©μ•ˆ(보쑰기ꡬ ꡐ체, μ™ΈλΆ€μžμ› λ“±)을 λ…Όμ˜ν•¨") if data["needs_assessment_needed"]: interlock_actions.append("μš•κ΅¬μ‚¬μ • μž¬ν‰κ°€λ₯Ό μ‹ μ†νžˆ μ‹œν–‰ν•˜μ—¬ μˆ˜κΈ‰μž ADL 및 ν™˜κ²½ λ³€ν™”λ₯Ό κ³΅μ‹μ μœΌλ‘œ 기둝함") if data["careplan_change_needed"]: interlock_actions.append("κΈ‰μ—¬μ œκ³΅κ³„νšμ˜ λͺ©ν‘œλ₯Ό μˆ˜μ •ν•˜κ³  μ œκ³΅λ˜λŠ” μ„œλΉ„μŠ€ μ‹œκ°„/λ‚΄μš© λ³€κ²½ 계약을 쀀비함") if data["care_grade_review_needed"]: interlock_actions.append("κ΅­λ―Όκ±΄κ°•λ³΄ν—˜κ³΅λ‹¨ λ“±κΈ‰ μ‹ μ²­ 절차 및 μ„œλ₯˜ ꡬ비 μš”λ Ήμ— λŒ€ν•΄ λ³΄ν˜Έμžμ—κ²Œ μ•ˆλ‚΄ν•¨") if data["followup_counseling_needed"]: interlock_actions.append("μΆ”ν›„ λ³΄ν˜Έμžμ™€μ˜ μΆ”κ°€ λŒ€λ©΄ 및 μ „ν™” 상담 일정을 μ‘°μœ¨ν•˜μ—¬ 쑰치 결과의 λ°˜μ‘μ„ 확인함") if interlock_actions: plan_lines.append("μ„ΈλΆ€ 쑰치 κ³„νšμœΌλ‘œλŠ”: " + ", ".join(interlock_actions) + "λ₯Ό μ‹œν–‰ν•  μ˜ˆμ •μž…λ‹ˆλ‹€.") else: plan_lines.append("μˆ˜κΈ‰μžμ˜ μš•κ΅¬μ— κΈ°λ°˜ν•˜μ—¬ ν˜„ μ„œλΉ„μŠ€ μƒνƒœλ₯Ό λ©΄λ°€νžˆ λͺ¨λ‹ˆν„°λ§ν•˜κ³  정기적인 μœ μ„  확인을 μ§€μ†ν•˜κΈ°λ‘œ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.") action_plan_text = " ".join(plan_lines) # 5. 연동 ν•„μš” ν•­λͺ© interlock_names = [] if data["case_needed"]: interlock_names.append("사둀관리 ν•„μš”") if data["needs_assessment_needed"]: interlock_names.append("μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  ν•„μš”") if data["careplan_change_needed"]: interlock_names.append("κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ ν•„μš”") if data["care_grade_review_needed"]: interlock_names.append("λ“±κΈ‰λ³€κ²½ μ‹ μ²­ κ²€ν†  ν•„μš”") if data["followup_counseling_needed"]: interlock_names.append("보호자 μΆ”κ°€ 상담 ν•„μš”") interlock_text = ", ".join(interlock_names) if interlock_names else "ν•΄λ‹Ή 사항 μ—†μŒ" full_text = f""" 1. 상담 μ‚¬μœ  {counseling_reason_text} 2. 상담 λ‚΄μš© {counseling_content_text} 3. 전문적 사정 {professional_assessment_text} 4. μ‘°μΉ˜κ³„νš {action_plan_text} 5. 연동 ν•„μš” ν•­λͺ© {interlock_text} """.strip() return full_text # ----------------------------- # DB μ €μž₯ ν•¨μˆ˜ # ----------------------------- def save_counseling_record(data) -> bool: try: conn = sqlite3.connect(DB_PATH) cur = conn.cursor() insert_query = """ INSERT INTO counseling_records ( client_id, client_masked_name, institution_type, counseling_type, counseling_date, counselor, counseling_reason, client_status_change, guardian_opinion, counseling_content, action_plan, evaluation_score, missing_items, compliance_result, next_due_note, created_at, counseling_purpose, selected_items, keywords, additional_needs, case_needed, needs_assessment_needed, careplan_change_needed, care_grade_review_needed, followup_counseling_needed ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ cur.execute( insert_query, ( data.get("client_id", 0), data["masked_name"], data["institution_type"], data["counseling_type"], data["counseling_date"], data["counselor"], data["counseling_reason"], data.get("client_status_change", ""), data.get("guardian_opinion", ""), data["counseling_content"], data["action_plan"], data["evaluation_score"], data["missing_items"], data["compliance_result"], data.get("next_due_note", ""), datetime.now().strftime("%Y-%m-%d %H:%M:%S"), data["counseling_purpose"], ", ".join(data.get("selected_items_list", [])), data["keywords"], data["additional_needs"], 1 if data["case_needed"] else 0, 1 if data["needs_assessment_needed"] else 0, 1 if data["careplan_change_needed"] else 0, 1 if data["care_grade_review_needed"] else 0, 1 if data["followup_counseling_needed"] else 0, ) ) conn.commit() conn.close() return True except Exception as e: st.error(f"DB μ €μž₯ 쀑 였λ₯˜ λ°œμƒ: {e}") return False st.markdown(""" """, unsafe_allow_html=True) st.markdown("""

πŸ“ 상담일지 생성기

λΉ λ₯Έ μ§μ ‘μž…λ ₯ λ²„μ „μž…λ‹ˆλ‹€. 상담 κ³Όμ •κ³Ό 확인 μ ˆμ°¨κ°€ ν¬ν•¨λ˜λ„λ‘ λ¬Έμ„œλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

""", unsafe_allow_html=True) st.markdown("""
μΆœμ‹œ ν…ŒμŠ€νŠΈμš©: 속도와 λ¬Έμ„œ ν’ˆμ§ˆμ„ λ¨Όμ € ν™•μΈν•˜κΈ° μœ„ν•΄ μˆ˜κΈ‰μž DB μžλ™μ‘°νšŒ κΈ°λŠ₯을 μ œμ™Έν–ˆμŠ΅λ‹ˆλ‹€. 직접 μž…λ ₯ ν›„ 상담 과정·확인 λ‚΄μš©Β·μ‘°μΉ˜κ³„νšμ΄ ν¬ν•¨λœ 상담일지λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
""", unsafe_allow_html=True) st.markdown("""
β€» μƒμ„±λœ λ¬Έμ„œλŠ” κ·ΈλŒ€λ‘œ μ‚¬μš©ν•˜μ…”λ„ 되며, κΈ°κ΄€μ˜ 운영방침, μˆ˜κΈ‰μž μƒνƒœ, 보호자 의견 및 평가기쀀에 따라 μΆ”κ°€ 보완사항은 μ–Όλ§ˆλ“ μ§€ μˆ˜μ • κ°€λŠ₯ν•©λ‹ˆλ‹€. κΈ°κ΄€μ˜ 상황에 맞게 ν™œμš©ν•˜μ‹œκΈ° λ°”λžλ‹ˆλ‹€.
""", unsafe_allow_html=True) INSTITUTION_GUIDES = { "λ°©λ¬Έμš”μ–‘": ["μ„œλΉ„μŠ€ μ‹œκ°„", "μš”μ–‘λ³΄ν˜Έμ‚¬ λ³€κ²½", "가사지원 λ²”μœ„", "μΈμ§€ν™œλ™", "볡약확인", "λ‚™μƒμ˜ˆλ°©"], "λ°©λ¬Έλͺ©μš•": ["λͺ©μš• μ „ν›„ κ±΄κ°•μƒνƒœ", "ν”ΌλΆ€μƒνƒœ", "λ‚™μƒμ˜ˆλ°©", "이동 보쑰", "λͺ©μš• 거뢀감", "보호자 ν˜‘μ‘°"], "λ°©λ¬Έκ°„ν˜Έ": ["ν˜ˆμ••/ν˜ˆλ‹Ή κ΄€μ°°", "μƒμ²˜κ΄€λ¦¬", "νˆ¬μ•½κ΄€λ¦¬", "건강상담", "μ˜λ£ŒκΈ°κ΄€ 연계", "κ°„ν˜Έμ²˜μΉ˜"], "μ£Όκ°„λ³΄ν˜Έ": ["μ†‘μ˜", "ν”„λ‘œκ·Έλž¨ μ°Έμ—¬", "식사", "λ°°λ³€", "λ‚™μƒμ˜ˆλ°©", "μ •μ„œμ§€μ›", "κ°€μ‘±μ†Œν†΅"], "μš”μ–‘μ›": ["μž…μ†Œμƒν™œ 적응", "식사/수면/λ°°λ³€", "μš•μ°½μ˜ˆλ°©", "λ‚™μƒμ˜ˆλ°©", "ν”„λ‘œκ·Έλž¨ μ°Έμ—¬", "κ°€μ‘±λ©΄νšŒ"], "κ³΅λ™μƒν™œκ°€μ •": ["κ³΅λ™μƒν™œ 적응", "μ •μ„œμ§€μ›", "κ°œλ³„ μš•κ΅¬", "κ°€μ‘±μ†Œν†΅", "ν”„λ‘œκ·Έλž¨ μ°Έμ—¬", "μ•ˆμ „κ΄€λ¦¬"], } TYPE_GUIDES = { "μˆ˜κΈ‰μžμƒλ‹΄": { "focus": "μˆ˜κΈ‰μžμ˜ κ±΄κ°•μƒνƒœ, μ •μ„œμƒνƒœ, μΌμƒμƒν™œ 뢈편감, μ„œλΉ„μŠ€ λ§Œμ‘±λ„, μƒνƒœλ³€ν™”λ₯Ό μ€‘μ‹¬μœΌλ‘œ 상담", "action": "μˆ˜κΈ‰μž μš•κ΅¬λ₯Ό κΈ‰μ—¬μ œκ³΅κ³„νšκ³Ό 일일 μ„œλΉ„μŠ€ 제곡 λ‚΄μš©μ— 반영", }, "λ³΄ν˜Έμžμƒλ‹΄": { "focus": "보호자의 의견, κ°€μ‘± μš”μ²­μ‚¬ν•­, μƒνƒœλ³€ν™” 전달, μ„œλΉ„μŠ€ μ‘°μ • ν•„μš”μ„±μ„ μ€‘μ‹¬μœΌλ‘œ 상담", "action": "보호자 μ˜κ²¬μ„ κΈ°λ‘ν•˜κ³  ν•„μš”ν•œ 경우 κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ λ˜λŠ” 직원 곡유", }, "μ’…μ‚¬μžμƒλ‹΄": { "focus": "μ„œλΉ„μŠ€ 제곡 쀑 관찰사항, 업무 κ³ μΆ©, μˆ˜κΈ‰μž μƒνƒœ λ³€ν™”, μ•ˆμ „κ΄€λ¦¬ ν•„μš”μ„±μ„ μ€‘μ‹¬μœΌλ‘œ 상담", "action": "μ’…μ‚¬μž κ΄€μ°° λ‚΄μš©μ„ λ°˜μ˜ν•˜μ—¬ μ„œλΉ„μŠ€ 방법을 μ‘°μ •ν•˜κ³  ν•„μš” μ‹œ ꡐ윑·회의 μ•ˆκ±΄μœΌλ‘œ μ—°κ²°", }, "기타상담": { "focus": "κΈ°κ΄€ μš΄μ˜μƒ ν•„μš”ν•œ 상담, μ§€μ—­μžμ› 연계, 민원 예방, νŠΉμ΄μ‚¬ν•­ 확인을 μ€‘μ‹¬μœΌλ‘œ 상담", "action": "상담 λ‚΄μš©μ„ κΈ°κ΄€ 기둝으둜 남기고 ν›„μ†μ‘°μΉ˜ λ‹΄λ‹Ήμžμ™€ κΈ°ν•œμ„ μ„€μ •", }, } QA_RULES = { "μˆ˜κΈ‰μžμƒλ‹΄": [ "ν˜„μž¬ κ°€μž₯ λΆˆνŽΈν•˜κ±°λ‚˜ νž˜λ“  점은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "졜근 κ±΄κ°•μƒνƒœ, 톡증, μ–΄μ§€λŸΌ, μ‹μ‚¬λŸ‰, 수면, 배변·배뇨에 λ³€ν™”κ°€ μžˆμ—ˆμŠ΅λ‹ˆκΉŒ?", "μ„œλΉ„μŠ€ 제곡 κ³Όμ •μ—μ„œ λ§Œμ‘±ν•˜λŠ” 점과 λΆˆνŽΈν•œ 점은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "μš”μ–‘λ³΄ν˜Έμ‚¬ λ˜λŠ” 기관에 μΆ”κ°€λ‘œ μš”μ²­ν•˜κ³  싢은 도움이 μžˆμŠ΅λ‹ˆκΉŒ?", "낙상, μš•μ°½, μΈμ§€ν˜Όλ™, μ •μ„œλΆˆμ•ˆ λ“± μ•ˆμ „μƒ κ±±μ •λ˜λŠ” 일이 μžˆμ—ˆμŠ΅λ‹ˆκΉŒ?", ], "λ³΄ν˜Έμžμƒλ‹΄": [ "λ³΄ν˜Έμžκ°€ 보기에 μˆ˜κΈ‰μžμ˜ 졜근 κ±΄κ°•μƒνƒœλ‚˜ μƒν™œμƒνƒœ λ³€ν™”λŠ” λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "가쑱이 κ±±μ •ν•˜λŠ” μœ„ν—˜μš”μΈμ΄λ‚˜ λ³‘μ›μ§„λ£Œ ν•„μš”μ„±μ΄ μžˆμŠ΅λ‹ˆκΉŒ?", "ν˜„μž¬ μ„œλΉ„μŠ€ μ‹œκ°„, μ œκ³΅λ‚΄μš©, μ’…μ‚¬μž λ°°μΉ˜μ— 쑰정이 ν•„μš”ν•©λ‹ˆκΉŒ?", "λ³΄ν˜Έμžκ°€ 기관에 μš”μ²­ν•˜κ±°λ‚˜ κ³΅μœ ν•˜κ³  싢은 사항은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "κΈ‰μ—¬μ œκ³΅κ³„νšμ— λ°˜μ˜ν•΄μ•Ό ν•  κ°€μ‘± 의견이 μžˆμŠ΅λ‹ˆκΉŒ?", ], "μ’…μ‚¬μžμƒλ‹΄": [ "μ„œλΉ„μŠ€ 제곡 쀑 κ΄€μ°°ν•œ μˆ˜κΈ‰μžμ˜ μ£Όμš” λ³€ν™”λŠ” λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "낙상, μš•μ°½, μ‹μ‚¬λŸ‰ κ°μ†Œ, μΈμ§€ν˜Όλ™, 폭언·거뢀 λ“± μœ„ν—˜ μ§•ν›„κ°€ μžˆμ—ˆμŠ΅λ‹ˆκΉŒ?", "ν˜„μž¬ 제곡 방법 쀑 쑰정이 ν•„μš”ν•œ 뢀뢄은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "업무 μˆ˜ν–‰ 쀑 μ–΄λ €μ›€μ΄λ‚˜ μΆ”κ°€ κ΅μœ‘Β·νšŒμ˜κ°€ ν•„μš”ν•œ λ‚΄μš©μ€ λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "보호자 λ˜λŠ” λ‹΄λ‹Ήμžμ—κ²Œ μ¦‰μ‹œ κ³΅μœ ν•΄μ•Ό ν•  사항이 μžˆμŠ΅λ‹ˆκΉŒ?", ], "기타상담": [ "상담이 ν•„μš”ν•œ μ£Όμš” 상황은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "μˆ˜κΈ‰μž, 보호자, μ’…μ‚¬μž 쀑 λˆ„κ΅¬μ™€ κ΄€λ ¨λœ λ‚΄μš©μž…λ‹ˆκΉŒ?", "κΈ°κ΄€ μ°¨μ›μ˜ μ‘°μΉ˜λ‚˜ μ™ΈλΆ€ 연계가 ν•„μš”ν•œ 뢀뢄은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?", "후속 확인이 ν•„μš”ν•œ μ‹œμ κ³Ό λ‹΄λ‹ΉμžλŠ” λˆ„κ΅¬μž…λ‹ˆκΉŒ?", ], } STATUS_CHANGE_KEYWORDS = ["낙상", "μš•μ°½", "발적", "μƒμ²˜", "μ–΄μ§€", "톡증", "μ‹μ‚¬λŸ‰", "μ‚¬λ ˆ", "μ—°ν•˜", "수면", "λ°°λ³€", "배뇨", "μ‹€κΈˆ", "κΈ°μ €κ·€", "보행", "휘청", "인지", "ν˜Όλ™", "λΆˆμ•ˆ", "우울", "μž…μ›", "퇴원"] CASE_RISK_KEYWORDS = ["반볡", "지속", "μ•…ν™”", "곡격", "폭언", "κ±°λΆ€", "망상", "배회", "κ°ˆλ“±", "λ‹€νˆΌ", "ν•™λŒ€", "λ°©μž„", "μžν•΄", "μœ„ν—˜", "μž…μ›", "응급", "보호자 λΆ€μž¬"] CARE_PLAN_KEYWORDS = ["μ„œλΉ„μŠ€", "μ‹œκ°„", "λ³€κ²½", "μ‘°μ •", "μΆ”κ°€", "μš”μ²­", "병원", "동행", "식사", "이동", "λͺ©μš•", "볡약", "ν”„λ‘œκ·Έλž¨", "κΈ‰μ—¬", "κ³„νš"] FALL_KEYWORDS = ["낙상", "λ„˜μ–΄", "미끄", "μ–΄μ§€", "ν˜„κΈ°", "휘청", "보행", "λΆ€μΆ•", "μ§€νŒ‘", "μ›Œμ»€", "νœ μ²΄μ–΄", "λ¬Έν„±", "μ‘°λͺ…", "μ•Όκ°„", "ν™”μž₯μ‹€", "μ†‘μ˜"] PRESSURE_INJURY_KEYWORDS = ["μš•μ°½", "발적", "μƒμ²˜", "ν”ΌλΆ€", "짓무", "μ‚ΌμΆœ", "λ“œλ ˆμ‹±", "μ²΄μœ„", "와상", "침상", "νœ μ²΄μ–΄", "톡증", "열감", "μ˜μ–‘", "νƒˆμˆ˜"] DEMENTIA_KEYWORDS = ["치맀", "인지", "κΈ°μ–΅", "반볡", "배회", "망상", "ν™˜κ°", "λΆˆμ•ˆ", "초쑰", "곡격", "κ±°λΆ€", "κ·€κ°€", "집에", "κ³ μ§‘", "ν˜Όλ™", "섬망"] GUARDIAN_BURDEN_KEYWORDS = ["νž˜λ“€", "μ†Œμ§„", "λΆ€λ‹΄", "μ§€μΉ¨", "혼자", "κ±±μ •", "λΆˆμ•ˆ", "μš”μ²­", "ν™•λŒ€", "λ³€κ²½", "동행", "상담"] GENERATION_RULE = """ 상담일지 μž‘μ„± κ·œμΉ™: 1. μ‚¬μš©μžκ°€ μž…λ ₯ν•œ μƒλ‹΄λ‚΄μš©μ— μ—†λŠ” λ¬Έμ œλŠ” μ ˆλŒ€ μƒμ„±ν•˜μ§€ μ•ŠλŠ”λ‹€. 2. μš•μ°½, 배회, 곡격성, κ·€κ°€μš•κ΅¬, μΉ˜λ§€ν–‰λ™, 낙상 λ“± μœ„ν—˜μš”μΈμ€ μž…λ ₯된 κ²½μš°μ—λ§Œ μž‘μ„±ν•œλ‹€. 3. μž…λ ₯λ˜μ§€ μ•Šμ€ μœ„ν—˜μš”μΈμ„ '확인 ν•„μš”', 'κ²€ν†  ν•„μš”'λΌλŠ” ν‘œν˜„μœΌλ‘œλ„ λ„£μ§€ μ•ŠλŠ”λ‹€. 4. 전문적 사정(A)은 μ‹€μ œ μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ„ λ°”νƒ•μœΌλ‘œλ§Œ μž‘μ„±ν•œλ‹€. 5. μ‘°μΉ˜κ³„νš(P)은 ν™•μΈλœ λ¬Έμ œμ— λŒ€ν•œ κ³„νšλ§Œ μž‘μ„±ν•œλ‹€. 6. μƒλ‹΄λ‚΄μš©μ΄ λ‹¨μˆœ μΌλ°˜μƒλ‹΄μ΄λ©΄ μΌλ°˜μƒλ‹΄ λ‚΄μš©μœΌλ‘œλ§Œ μž‘μ„±ν•œλ‹€. """.strip() RESTRICTED_REPLACEMENTS = { "이전과 동일": "상담 μ‹œμ μ˜ μƒνƒœλŠ” 이전 기둝과 큰 차이 없이 μœ μ§€λ˜λŠ” κ²ƒμœΌλ‘œ μž…λ ₯됨", "μƒνƒœ μ–‘ν˜Έ": "식사, 이동, μ •μ„œ ν‘œν˜„ λ“± 일상 κΈ°λŠ₯이 비ꡐ적 μ•ˆμ •μ μœΌλ‘œ μœ μ§€λ˜λŠ” κ²ƒμœΌλ‘œ 확인함", "νŠΉμ΄μ‚¬ν•­ μ—†μŒ": "상담 μ‹œμ μ— μΆ”κ°€λ‘œ μž…λ ₯된 νŠΉμ΄μ‚¬ν•­μ€ μ—†μŒ", "별일 μ—†μŒ": "상담 μ‹œμ μ— μΆ”κ°€λ‘œ μž…λ ₯된 별도 사항은 μ—†μŒ", "문제 μ—†μŒ": "상담 μ‹œμ μ— 별도 문제둜 μž…λ ₯된 λ‚΄μš©μ€ μ—†μŒ", } INSTITUTION_SOAP_GUIDES = { "λ°©λ¬Έμš”μ–‘": { "focus": "κ°€μ • λ‚΄ μž”μ‘΄ κΈ°λŠ₯ μœ μ§€, μ£Όκ±°ν™˜κ²½ 낙상 μœ„ν—˜, 보호자 λΆ€μž¬μ‹œκ°„, λΉ„κΈ‰μ—¬μ„± μš”κ΅¬μ™€ κΈ‰μ—¬μ œκ³΅ λ²”μœ„ 쑰율", "plan": "κ°€μ • λ‚΄ 이동 동선과 ν™”μž₯μ‹€ 이용 ν™˜κ²½μ„ μ κ²€ν•˜κ³  κΈ‰μ—¬μ œκ³΅κ³„νš λ²”μœ„ μ•ˆμ—μ„œ μ‹ μ²΄ν™œλ™ 및 μ •μ„œμ§€μ› 방법을 쑰정함", }, "λ°©λ¬Έλͺ©μš•": { "focus": "λͺ©μš• μ „ν›„ κ±΄κ°•μƒνƒœ, ν”ΌλΆ€ 발적, 감염성 ν”ΌλΆ€μ§ˆν™˜, μ΄λ™Β·λ―Έλ„λŸΌΒ·ν™”μƒ 예방, λͺ©μš• 거뢀감", "plan": "λͺ©μš• μ „ν›„ μ»¨λ””μ…˜κ³Ό ν”ΌλΆ€μƒνƒœλ₯Ό ν™•μΈν•˜κ³  λ―Έλ„λŸΌ λ°©μ§€, 체온 λ³€ν™”, ν”ΌλΆ€ 마찰 μ΅œμ†Œν™” 쑰치λ₯Ό μ μš©ν•¨", }, "λ°©λ¬Έκ°„ν˜Έ": { "focus": "λ§Œμ„±μ§ˆν™˜ 관리, 볡약 이행, κ°„ν˜Έμ²˜μΉ˜ μƒνƒœ, λ°©λ¬Έκ°„ν˜Έμ§€μ‹œμ„œ μœ νš¨μ„±, 보호자의 처치 이해도", "plan": "ν™œλ ₯징후와 처치 μƒνƒœλ₯Ό ν™•μΈν•˜κ³  λ³΅μ•½Β·μƒμ²˜Β·κ΄€ μ‚½μž… 관리 ν•„μš” μ‹œ μ˜λ£ŒκΈ°κ΄€ λ˜λŠ” λ³΄ν˜Έμžμ™€ 연계함", }, "μ£Όκ°„λ³΄ν˜Έ": { "focus": "μ†‘μ˜ μ•ˆμ „, 등원 κ±°λΆ€, ν”„λ‘œκ·Έλž¨ 참여도, μ‹μ‚¬Β·μœ„μƒ μ°Έμ—¬, 보호자 λΆ€μ–‘ λΆ€λ‹΄ 경감", "plan": "μ†‘μ˜ 및 μ„Όν„° λ‚΄ 이동 동선을 μ κ²€ν•˜κ³  ν”„λ‘œκ·Έλž¨ μ°Έμ—¬ μˆ˜μ€€κ³Ό 보호자 λΆ€λ‹΄ λ³€ν™”λ₯Ό ν•¨κ»˜ 확인함", }, "μš”μ–‘μ›": { "focus": "μž…μ†Œμƒν™œ 적응, κ³΅λ™μƒν™œ κ°ˆλ“±, μ‹μ‚¬Β·μˆ˜λ©΄Β·λ°°λ³€, μš•μ°½Β·λ‚™μƒ 예방, 보호자 상담 반영", "plan": "κ³΅λ™μƒν™œ 적응과 μ‹ μ²΄Β·μ •μ„œ μƒνƒœλ₯Ό κ΄€μ°°ν•˜κ³  보호자 λ©΄νšŒΒ·μƒλ‹΄ λ‚΄μš©μ„ μ‹œμ„€ μΌ€μ–΄κ³„νšμ— λ°˜μ˜ν•¨", }, "κ³΅λ™μƒν™œκ°€μ •": { "focus": "μ†Œκ·œλͺ¨ 곡동체 관계, μžκΈ°κ²°μ •κΆŒ, κ°œλ³„ μƒν™œμŠ΅κ΄€, κ°€μ •ν˜• ν™˜κ²½ μœ μ§€, μ‹€λ‚΄ 이동 동선 μ•ˆμ „", "plan": "κ°œλ³„ μƒν™œμŠ΅κ΄€κ³Ό κ³΅λ™μƒν™œ κ°ˆλ“±μš”μΈμ„ μ‘°μœ¨ν•˜κ³  μ†Œκ·œλͺ¨ ν™˜κ²½μ—μ„œμ˜ μ •μ„œ μ•ˆμ • 및 μ•ˆμ „κ΄€λ¦¬ κ³„νšμ„ μˆ˜λ¦½ν•¨", }, } SUPPORT_METHOD_RULES = { "이동지원": { "keywords": ["이동", "보행", "λΆ€μΆ•", "νœ μ²΄μ–΄", "μ§€νŒ‘", "μ›Œμ»€", "ν™”μž₯μ‹€", "낙상", "휘청", "μ–΄μ§€"], "items": [ "침상과 νœ μ²΄μ–΄ 이동 μ‹œ 뢀좕을 μ œκ³΅ν•¨", "보행기 및 μ§€νŒ‘μ΄ μ‚¬μš©μ„ 지원함", "μ‹€λ‚΄ 이동 μ‹œ μ•ˆμ „μ„ 확인함", "이동 동선을 μ •λ¦¬ν•˜μ—¬ 낙상을 μ˜ˆλ°©ν•¨", "ν™”μž₯μ‹€ 이동 μ‹œ 동행 및 μ•ˆμ „κ΄€μ°°μ„ μ‹€μ‹œν•¨", ], }, "μΌμƒμƒν™œμ§€μ›": { "keywords": ["μœ„μƒ", "μ„Έλ©΄", "ꡬ강", "의볡", "μ°©νƒˆ", "μ†λ°œν†±", "λͺ©μš•", "ν”ΌλΆ€"], "items": [ "μ„Έλ©΄ 및 ꡬ강관리λ₯Ό 지원함", "의볡 μ°©νƒˆμ˜λ₯Ό 지원함", "κ°œμΈμœ„μƒ μƒνƒœλ₯Ό 점검함", "μ†λ°œν†± μƒνƒœλ₯Ό ν™•μΈν•˜κ³  μœ„μƒκ΄€λ¦¬λ₯Ό 지원함", "λͺ©μš• μ „ν›„ κ±΄κ°•μƒνƒœμ™€ ν”ΌλΆ€μƒνƒœλ₯Ό 확인함", ], }, "λ‚™μƒμ˜ˆλ°© 지원": { "keywords": FALL_KEYWORDS, "items": [ "보행 μ‹œ 뢀좕을 μ œκ³΅ν•¨", "λ―Έλ„λŸΌ λ°©μ§€ ν™˜κ²½μ„ 점검함", "침상 λ‚œκ°„ μƒνƒœλ₯Ό 확인함", "μ•Όκ°„ 이동 μ‹œ μ•ˆμ „κ΄€μ°°μ„ μ‹€μ‹œν•¨", "이동 μ „ μ–΄μ§€λŸΌ μ—¬λΆ€λ₯Ό 확인함", "보행보쑰기 μ‚¬μš©μ„ μ•ˆλ‚΄ν•¨", ], }, "μš•μ°½μ˜ˆλ°© 지원": { "keywords": PRESSURE_INJURY_KEYWORDS, "items": [ "정기적 μ²΄μœ„λ³€κ²½μ„ μ‹€μ‹œν•¨", "ν”ΌλΆ€μƒνƒœλ₯Ό 확인함", "μš•μ°½ λ°œμƒ μ—¬λΆ€λ₯Ό 관찰함", "μ˜μ–‘μƒνƒœλ₯Ό 확인함", "μž₯μ‹œκ°„ 동일 μžμ„Έλ₯Ό μ˜ˆλ°©ν•¨", "ν”ΌλΆ€ 청결을 μœ μ§€ν•¨", ], }, "인지지원": { "keywords": DEMENTIA_KEYWORDS + ["μΈμ§€μ €ν•˜", "κΈ°μ–΅μ €ν•˜", "μΌμ •ν‘œ", "μ•ˆμ „μˆ˜μΉ™"], "items": [ "λ‚ μ§œμ™€ μž₯μ†Œλ₯Ό 반볡 μ•ˆλ‚΄ν•¨", "νšŒμƒλŒ€ν™”λ₯Ό μ‹€μ‹œν•¨", "μ΅μˆ™ν•œ ν™˜κ²½μ„ μœ μ§€ν•˜λ„λ‘ 지원함", "μΈμ§€μžκ·Ή ν™œλ™μ„ μ œκ³΅ν•¨", "μΌμ •ν‘œλ₯Ό ν™œμš©ν•˜μ—¬ μ•ˆλ‚΄ν•¨", "μ•ˆμ „μˆ˜μΉ™μ„ 반볡 κ΅μœ‘ν•¨", ], }, "BPSD 지원": { "keywords": ["반볡질문", "망상", "배회", "곡격", "폭언", "κ·€κ°€", "ν™˜κ°", "κ±°λΆ€", "λΆˆμ•ˆ", "초쑰"], "items": [ "동일 μ§ˆλ¬Έμ— μ•ˆμ •μ μœΌλ‘œ μ‘λŒ€ν•¨", "μ‹œκ°μžλ£Œλ₯Ό ν™œμš©ν•˜μ—¬ μ„€λͺ…함", "μ •λ©΄ λ°˜λ°•μ„ 지양함", "감정을 곡감함", "ν™”μ œλ₯Ό μ „ν™˜ν•¨", "μ•ˆμ „ν•œ 이동을 지원함", "μžκ·Ήμš”μΈμ„ μ œκ±°ν•˜κ³  μ°¨λΆ„ν•œ ν™˜κ²½μ„ μ œκ³΅ν•¨", ], }, "μ •μ„œμ§€μ›": { "keywords": ["μ™Έλ‘œμ›€", "우울", "λΆˆμ•ˆ", "무기λ ₯", "μ†Œμ™Έ", "눈물", "짜증", "μ •μ„œ", "κ°€μ‘±", "κ±±μ •"], "items": [ "말벗 μ„œλΉ„μŠ€λ₯Ό μ œκ³΅ν•¨", "곡감적 경청을 μ‹€μ‹œν•¨", "긍정적 ν”Όλ“œλ°±μ„ μ œκ³΅ν•¨", "μ·¨λ―Έν™œλ™ μ°Έμ—¬λ₯Ό 지원함", "성취감을 κ²½ν—˜ν•˜λ„λ‘ 지원함", "μ•ˆμ •μ μΈ ν™˜κ²½μ„ μ œκ³΅ν•¨", "κ°€μ‘± 연락을 지원함", ], }, "μ‚¬νšŒμ°Έμ—¬μ§€μ›": { "keywords": ["고립", "ν”„λ‘œκ·Έλž¨", "관계", "λŒ€μΈ", "μ°Έμ—¬", "ν™œλ™", "λ§λ‹€νˆΌ", "κ°ˆλ“±"], "items": [ "집단 ν”„λ‘œκ·Έλž¨ μ°Έμ—¬λ₯Ό 지원함", "또래 관계 ν˜•μ„±μ„ 지원함", "μ§€μ—­μ‚¬νšŒ ν™œλ™ μ°Έμ—¬λ₯Ό 연계함", "λŒ€ν™” 기회λ₯Ό μ œκ³΅ν•¨", "κ³΅λ™ν™œλ™ μ°Έμ—¬λ₯Ό μœ λ„ν•¨", ], }, "μ˜μ–‘μ§€μ›": { "keywords": ["식사", "μ‹μš•", "체쀑", "μ˜μ–‘", "μˆ˜λΆ„", "νƒˆμˆ˜", "κΈ°ν˜Έμ‹", "μ„ ν˜Έ μŒμ‹"], "items": [ "μ„ ν˜Έ μŒμ‹μ„ 확인함", "κΈ°ν˜Έμ‹μ„ λ°˜μ˜ν•¨", "μˆ˜λΆ„μ„­μ·¨λ₯Ό 독렀함", "μ˜μ–‘μƒνƒœλ₯Ό 관찰함", "체쀑변화λ₯Ό 확인함", "μ˜μ–‘λ³΄μΆ©μ„ 지원함", ], }, "배변배뇨지원": { "keywords": ["λ°°λ³€", "배뇨", "λ³€λΉ„", "μš”μ‹€κΈˆ", "μ‹€κΈˆ", "κΈ°μ €κ·€", "ν™”μž₯μ‹€"], "items": [ "μˆ˜λΆ„μ„­μ·¨λ₯Ό 독렀함", "κ·œμΉ™μ  λ°°λ³€μŠ΅κ΄€μ„ 지원함", "ν™œλ™λŸ‰ 증가λ₯Ό μœ λ„ν•¨", "배변배뇨 νŒ¨ν„΄μ„ 확인함", "μ •κΈ° λ³€κΈ°μœ λ„λ₯Ό μ‹€μ‹œν•¨", "μœ„μƒκ΄€λ¦¬λ₯Ό 지원함", ], }, "볡약지원": { "keywords": ["볡약", "μ•½", "νˆ¬μ•½", "μ•½λ¬Ό", "볡용", "ν˜ˆμ••", "ν˜ˆλ‹Ή"], "items": [ "볡약 μ—¬λΆ€λ₯Ό 확인함", "μ•½λ¬Ό λ³΄κ΄€μƒνƒœλ₯Ό 점검함", "λ³΄ν˜Έμžμ™€ 볡약 정보λ₯Ό κ³΅μœ ν•¨", "볡약일지λ₯Ό 확인함", "νˆ¬μ•½μ‹œκ°„μ„ μ•ˆλ‚΄ν•¨", ], }, "λ³΄ν˜Έμžμ§€μ›": { "keywords": GUARDIAN_BURDEN_KEYWORDS + ["보호자", "κ°€μ‘±", "κ°€μ‘±κ°ˆλ“±"], "items": [ "보호자 상담을 μ‹€μ‹œν•¨", "λŒλ΄„ 뢀담을 경청함", "μ§€μ—­μžμ› 연계λ₯Ό μ•ˆλ‚΄ν•¨", "κ°€μ‘± μ˜κ²¬μ„ 쑰정함", "μ˜μ‚¬μ†Œν†΅μ„ 지원함", ], }, "μ§€μ—­μžμ› 연계": { "keywords": ["치맀", "건강관리", "경제", "고립", "병원", "λ³΄κ±΄μ†Œ", "볡지관", "행정볡지센터", "μ§€μ—­μžμ›", "λ°©λ¬Έκ°„ν˜Έ"], "items": [ "μž…λ ₯된 λ‚΄μš©μ— 따라 κ΄€λ ¨ μ§€μ—­μžμ›μ„ μ•ˆλ‚΄ν•¨", "λ°©λ¬Έκ°„ν˜Έ μ„œλΉ„μŠ€λ₯Ό 연계함", "λ³΄κ±΄μ†Œ ν”„λ‘œκ·Έλž¨μ„ μ•ˆλ‚΄ν•¨", "행정볡지센터 이용 정보λ₯Ό μ•ˆλ‚΄ν•¨", "볡지관 ν”„λ‘œκ·Έλž¨ μ°Έμ—¬λ₯Ό 연계함", ], }, } def _join_qna_answer_values(qna_answers): if not qna_answers: return "" answers = [] for answer in qna_answers.values(): answer = _clean_text(answer) if answer: answers.append(answer) return " ".join(answers) def _contains_keywords(text, keywords): source = str(text or "") return any(keyword in source for keyword in keywords) def _replace_restricted_phrases(text): result = str(text or "") for bad, good in RESTRICTED_REPLACEMENTS.items(): result = result.replace(bad, good) return result def _formalize_sentence(text): cleaned = _replace_restricted_phrases(_clean_text(text)) if not cleaned: return "" if cleaned.endswith(("함", "μž„", "됨", "확인됨", "νŒλ‹¨λ¨", "사정됨", "있음", "μ—†μŒ", "ν•˜μ‹¬", "λ§μ”€ν•˜μ‹¬", ".")): return cleaned.rstrip(".") return f"{cleaned}둜 확인됨" def _risk_flag(text, keywords): return _contains_keywords(text, keywords) def build_individual_support_methods(data, specialized=None): qna_text = _join_qna_answer_values(data.get("qna_answers", {})) combined = " ".join([ data.get("institution_type", ""), data.get("care_grade", ""), data.get("main_condition", ""), data.get("cognitive_status", ""), data.get("mobility_status", ""), data.get("diet_status", ""), data.get("toileting_status", ""), data.get("client_status_change", ""), data.get("guardian_opinion", ""), data.get("staff_observation", ""), data.get("custom_need", ""), qna_text, ]) selected = {} for category, rule in SUPPORT_METHOD_RULES.items(): if _contains_keywords(combined, rule["keywords"]): selected[category] = rule["items"][:3] if specialized: if specialized.get("fall"): selected.setdefault("λ‚™μƒμ˜ˆλ°© 지원", SUPPORT_METHOD_RULES["λ‚™μƒμ˜ˆλ°© 지원"]["items"][:3]) if specialized.get("pressure"): selected.setdefault("μš•μ°½μ˜ˆλ°© 지원", SUPPORT_METHOD_RULES["μš•μ°½μ˜ˆλ°© 지원"]["items"][:3]) if specialized.get("dementia"): selected.setdefault("인지지원", SUPPORT_METHOD_RULES["인지지원"]["items"][:3]) selected.setdefault("BPSD 지원", SUPPORT_METHOD_RULES["BPSD 지원"]["items"][2:5]) if specialized.get("guardian_burden"): selected.setdefault("λ³΄ν˜Έμžμ§€μ›", SUPPORT_METHOD_RULES["λ³΄ν˜Έμžμ§€μ›"]["items"][:3]) if not selected: return "[κ°œμΈλ³„ μ΄μš©κ³„νš 지원방법]\n- μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ— 별도 지원방법 λ³€κ²½ 사항이 μ—†μ–΄ ν˜„μž¬ μ„œλΉ„μŠ€ 제곡 λ‚΄μš©μ„ μœ μ§€ν•¨" limited = list(selected.items())[:5] lines = ["[κ°œμΈλ³„ μ΄μš©κ³„νš 지원방법]"] for category, items in limited: lines.append(f"{category}") for item in items[:3]: lines.append(f"- {item}") return "\n".join(lines) def analyze_specialized_risks(data): qna_text = _join_qna_answer_values(data.get("qna_answers", {})) combined = " ".join([ data.get("main_condition", ""), data.get("cognitive_status", ""), data.get("mobility_status", ""), data.get("diet_status", ""), data.get("toileting_status", ""), data.get("client_status_change", ""), data.get("guardian_opinion", ""), data.get("staff_observation", ""), data.get("custom_need", ""), qna_text, ]) fall = _risk_flag(combined, FALL_KEYWORDS) pressure = _risk_flag(combined, PRESSURE_INJURY_KEYWORDS) dementia = _risk_flag(combined, DEMENTIA_KEYWORDS) guardian_burden = _risk_flag(data.get("guardian_opinion", "") + " " + qna_text, GUARDIAN_BURDEN_KEYWORDS) count = sum([fall, pressure, dementia]) if count >= 2 or _contains_keywords(combined, ["응급", "μž…μ›", "μ•…ν™”", "곡격", "μƒμ²˜", "μ‚ΌμΆœ", "감염", "반볡 낙상"]): level = "μœ„ν—˜" elif count == 1 or guardian_burden: level = "주의" else: level = "μ•ˆμ •" details = [] plans = [] if fall: details.append("μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ— 낙상 κ΄€λ ¨ λ‹¨μ„œκ°€ 포함됨") plans.append("λ‚™μƒμ˜ˆλ°©μ„ μœ„ν•΄ 이동 동선, ν™”μž₯μ‹€ 이용, λ―Έλ„λŸΌ λ°©μ§€, 보행보쑰기 μ‚¬μš© μ—¬λΆ€λ₯Ό μ κ²€ν•˜κ³  이동 μ‹œ λΆ€μΆ• 및 직원 동행을 강화함") if pressure: details.append("μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ— μš•μ°½ λ˜λŠ” 피뢀손상 κ΄€λ ¨ λ‹¨μ„œκ°€ 포함됨") plans.append("μž…λ ₯된 ν”ΌλΆ€ κ΄€λ ¨ λ‚΄μš©μ— 따라 μ²΄μœ„λ³€κ²½, μ••λ°•λΆ€μœ„ 관리, μ˜μ–‘Β·μˆ˜λΆ„ μ„­μ·¨ 지원을 μ„œλΉ„μŠ€ 제곡 과정에 λ°˜μ˜ν•¨") if dementia: details.append("μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ— 치맀 λ˜λŠ” 인지·행동 λ³€ν™” κ΄€λ ¨ λ‹¨μ„œκ°€ 포함됨") plans.append("μ •λ©΄ λ°˜λ°•μ„ ν”Όν•˜κ³  감정 곡감, 짧은 μ„€λͺ…, μ΅μˆ™ν•œ ν™”μ œ μ „ν™˜, κ°œλ³„ ν”„λ‘œκ·Έλž¨ λ˜λŠ” μ•ˆμ • ν™˜κ²½ μ œκ³΅μ„ μ μš©ν•¨") if guardian_burden: details.append("μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ— 보호자의 λŒλ΄„ λΆ€λ‹΄ λ˜λŠ” μΆ”κ°€ μš”μ²­μ΄ 포함됨") plans.append("보호자 μ˜κ²¬μ„ 상담기둝에 λ°˜μ˜ν•˜κ³  μ„œλΉ„μŠ€ μ‘°μ • κ°€λŠ₯ λ²”μœ„, λ³‘μ›μ§„λ£Œ, 상담, μ§€μ—­μžμ› 연계 ν•„μš”μ„±μ„ μ•ˆλ‚΄ν•¨") if not details: details.append("μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ—μ„œ 별도 μœ„ν—˜μš”μΈμ€ μž‘μ„±λ˜μ§€ μ•ŠμŒ") if not plans: plans.append("μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ„ μƒλ‹΄κΈ°λ‘μœΌλ‘œ λ³΄κ΄€ν•˜κ³  ν˜„μž¬ μ„œλΉ„μŠ€ 제곡 λ‚΄μš©μ„ μœ μ§€ν•¨") return { "combined": combined, "fall": fall, "pressure": pressure, "dementia": dementia, "guardian_burden": guardian_burden, "level": level, "details": details, "plans": plans, } def build_soap_sections(data, analysis, specialized): subject_sources = [ data.get("guardian_opinion", ""), data.get("custom_need", ""), _join_qna_answer_values(data.get("qna_answers", {})), data.get("client_status_change", ""), ] subjective = _clean_text(" / ".join([_formalize_sentence(x) for x in subject_sources if _clean_text(x)])) if not subjective: subjective = f"{data['counseling_type']} κ³Όμ •μ—μ„œ {data['counseling_reason']} λ‚΄μš©μœΌλ‘œ μΌλ°˜μƒλ‹΄μ„ 진행함" objective_items = [] objective_fields = [ ("μ£Όμš” μ§ˆν™˜ 및 μƒνƒœ", "main_condition"), ("μΈμ§€μƒνƒœ", "cognitive_status"), ("μ΄λ™μƒνƒœ", "mobility_status"), ("μ‹μ‚¬μƒνƒœ", "diet_status"), ("λ°°λ³€Β·λ°°λ‡¨μƒνƒœ", "toileting_status"), ("μƒνƒœλ³€ν™”", "client_status_change"), ("μ’…μ‚¬μž 관찰사항", "staff_observation"), ] for label, key in objective_fields: value = _formalize_sentence(data.get(key)) if value: objective_items.append(f"{label}: {value}") if not objective_items: objective_items.append("μΆ”κ°€ μœ„ν—˜μš”μΈ μž…λ ₯ 없이 일반 μƒλ‹΄μœΌλ‘œ 기둝함") objective = "\n".join([f"- {item}" for item in objective_items]) risk_summary = " / ".join(specialized["details"]) assessment_parts = [ f"μƒλ‹΄λŒ€μƒμ€ {data['counseling_type']}이며, μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ„ κΈ°μ€€μœΌλ‘œ 사정함.", risk_summary, ] if analysis["status_needed"]: assessment_parts.append("μž…λ ₯ λ‚΄μš©μ— μƒνƒœλ³€ν™” λ‹¨μ„œκ°€ μžˆμ–΄ μƒνƒœλ³€ν™”κΈ°λ‘μ§€ μž‘μ„± λŒ€μƒμœΌλ‘œ νŒλ‹¨ν•¨.") if analysis["case_needed"] or specialized["level"] == "μœ„ν—˜": assessment_parts.append("μž…λ ₯ λ‚΄μš©μ— 사둀관리 연계가 ν•„μš”ν•œ μœ„ν—˜ λ‹¨μ„œκ°€ 포함됨.") assessment = " ".join([part for part in assessment_parts if _clean_text(part)]) plan_items = list(specialized["plans"]) if analysis["care_plan_needed"]: plan_items.append(analysis["care_plan_reason"]) if analysis["status_needed"]: plan_items.append(analysis["status_reason"]) if analysis["case_needed"] or specialized["level"] == "μœ„ν—˜": plan_items.append(analysis["case_reason"]) unique_plans = [] for item in plan_items: item = _formalize_sentence(item) if item and item not in unique_plans: unique_plans.append(item) plan = "\n".join([f"- {item}" for item in unique_plans[:8]]) return f""" [SOAP 상담기둝] S(주관적 μ§„μˆ ): {subjective} O(객관적 κ΄€μ°°): {objective} A(전문적 사정): {assessment} P(μ‘°μΉ˜κ³„νš): {plan} """.strip() def analyze_counseling_answers(data): qna_text = _join_qna_answer_values(data.get("qna_answers", {})) combined = " ".join([ data.get("main_condition", ""), data.get("cognitive_status", ""), data.get("mobility_status", ""), data.get("diet_status", ""), data.get("toileting_status", ""), data.get("client_status_change", ""), data.get("guardian_opinion", ""), data.get("staff_observation", ""), data.get("custom_need", ""), qna_text, ]) status_needed = _contains_keywords(combined, STATUS_CHANGE_KEYWORDS) case_needed = _contains_keywords(combined, CASE_RISK_KEYWORDS) care_plan_needed = _contains_keywords(combined, CARE_PLAN_KEYWORDS) or status_needed or case_needed if case_needed: risk_level = "μœ„ν—˜" elif status_needed: risk_level = "주의" else: risk_level = "μ•ˆμ •" return { "qna_text": qna_text, "combined": combined, "risk_level": risk_level, "status_needed": status_needed, "case_needed": case_needed, "care_plan_needed": care_plan_needed, "status_reason": "상담 λ‹΅λ³€μ—μ„œ μƒνƒœλ³€ν™” λ‹¨μ„œκ°€ ν™•μΈλ˜μ–΄ μƒνƒœλ³€ν™”κΈ°λ‘μ§€ μž‘μ„± λŒ€μƒμœΌλ‘œ 기둝함" if status_needed else "ν•΄λ‹Ή μ—†μŒ", "case_reason": "상담 λ‹΅λ³€μ—μ„œ λ°˜λ³΅Β·μ•…ν™”Β·μœ„ν—˜ μˆ˜μ€€μ˜ λ¬Έμ œκ°€ ν™•μΈλ˜μ–΄ 사둀관리 연계 λŒ€μƒμœΌλ‘œ 기둝함" if case_needed else "ν•΄λ‹Ή μ—†μŒ", "care_plan_reason": "상담 κ²°κ³Όλ₯Ό κΈ‰μ—¬μ œκ³΅κ³„νš λ˜λŠ” μ„œλΉ„μŠ€ 제곡 방법에 λ°˜μ˜ν•  ν•„μš”κ°€ 있음" if care_plan_needed else "μƒλ‹΄λ‚΄μš©λ§Œ 기둝함", } def build_qna_generated_sections(data): analysis = analyze_counseling_answers(data) qna_text = analysis["qna_text"] or "Q&A μž…λ ₯ λ‚΄μš© μ—†μŒ" status_text = _clean_text(data.get("client_status_change")) or "μƒνƒœλ³€ν™” μž…λ ₯ λ‚΄μš© μ—†μŒ" guardian_text = _clean_text(data.get("guardian_opinion")) or "보호자 의견 μž…λ ₯ λ‚΄μš© μ—†μŒ" staff_text = _clean_text(data.get("staff_observation")) or "μ’…μ‚¬μž 관찰사항 μž…λ ₯ λ‚΄μš© μ—†μŒ" need_text = _clean_text(data.get("custom_need")) or "μΆ”κ°€ μš•κ΅¬ 및 μš”μ²­μ‚¬ν•­ μž…λ ₯ λ‚΄μš© μ—†μŒ" follow_items = [] if analysis["status_needed"]: follow_items.append(analysis["status_reason"]) if analysis["case_needed"]: follow_items.append(analysis["case_reason"]) if analysis["care_plan_needed"]: follow_items.append(analysis["care_plan_reason"]) follow_text = "\n".join([f"- {item}" for item in follow_items]) or "- λ‹¨μˆœ μΌλ°˜μƒλ‹΄μœΌλ‘œ 기둝함" if analysis["status_needed"] or analysis["case_needed"]: status_analysis_text = f"μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ„ κΈ°μ€€μœΌλ‘œ μœ„ν—˜μˆ˜μ€€μ€ {analysis['risk_level']} λ‹¨κ³„λ‘œ 기둝함. μž…λ ₯λ˜μ§€ μ•Šμ€ μœ„ν—˜μš”μΈμ€ μƒμ„±ν•˜μ§€ μ•ŠμŒ." else: status_analysis_text = "μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ„ κΈ°μ€€μœΌλ‘œ μΌλ°˜μƒλ‹΄μœΌλ‘œ 기둝함." optional_sections = [] if analysis["care_plan_needed"]: optional_sections.append(f"[κΈ‰μ—¬μ œκ³΅κ³„νš 반영 μ—¬λΆ€]\n{analysis['care_plan_reason']}") if analysis["status_needed"]: optional_sections.append(f"[μƒνƒœλ³€ν™”κΈ°λ‘μ§€ μž‘μ„± ν•„μš” μ—¬λΆ€]\n{analysis['status_reason']}") if analysis["case_needed"]: optional_sections.append(f"[사둀관리 연계 ν•„μš” μ—¬λΆ€]\n{analysis['case_reason']}") optional_text = "\n\n".join(optional_sections) return f""" [Q&A 확인 λ‚΄μš©] μž…λ ₯ λ‹΅λ³€: {qna_text} [μƒλ‹΄λ‚΄μš©] 상담뢄λ₯˜λŠ” {data['counseling_type']}이며, μƒλ‹΄μ‚¬μœ λŠ” {data['counseling_reason']}μž„. Q&A 및 κΈ°λ³Έ μž…λ ₯을 μ’…ν•©ν•œ κ²°κ³Ό, μˆ˜κΈ‰μž μƒνƒœλ³€ν™”λŠ” "{status_text}"둜 확인됨. 보호자 μ˜κ²¬μ€ "{guardian_text}"이며, μ’…μ‚¬μž 관찰사항은 "{staff_text}"둜 정리됨. [μƒνƒœλΆ„μ„] {status_analysis_text} [μ‘°μΉ˜κ³„νš] ν™•μΈλœ μš•κ΅¬ 및 μš”μ²­μ‚¬ν•­μ€ "{need_text}"μž„. μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ„ λ‹΄λ‹Ήμžμ™€ κ³΅μœ ν•˜κ³  ν•„μš”ν•œ λ²”μœ„μ—μ„œλ§Œ μ‘°μΉ˜ν•¨. [후속관리] {follow_text} {chr(10) + chr(10) + optional_text if optional_text else ""} """.strip(), analysis OPENERS = [ "상담 κ³Όμ •μ—μ„œ ν™•μΈν•œ κ²°κ³Ό", "금일 상담을 톡해 νŒŒμ•…ν•œ λ‚΄μš©μ€", "졜근 μ„œλΉ„μŠ€ 제곡 상황을 μ’…ν•©ν•΄ 보면", "μˆ˜κΈ‰μž 및 κ΄€λ ¨μžμ˜ μ˜κ²¬μ„ ν™•μΈν•œ κ²°κ³Ό", ] TONES = [ "ν˜„μž¬ μƒνƒœλ₯Ό μœ μ§€ν•˜λ©΄μ„œλ„ μ„Έμ‹¬ν•œ 관찰이 μš”κ΅¬λ©λ‹ˆλ‹€.", "μΌμƒμƒν™œμ˜ μ•ˆμ •μ„±μ„ 높이기 μœ„ν•œ 지속적인 지원이 ν•„μš”ν•©λ‹ˆλ‹€.", "κ°œλ³„ μš•κ΅¬κ°€ κΈ‰μ—¬μ œκ³΅ 과정에 μΆ©λΆ„νžˆ λ°˜μ˜λ˜λ„λ‘ 쑰정이 ν•„μš”ν•©λ‹ˆλ‹€.", "μƒνƒœ λ³€ν™” κ°€λŠ₯성을 κ³ λ €ν•˜μ—¬ 정기적인 확인과 기둝이 ν•„μš”ν•©λ‹ˆλ‹€.", ] def mask_name(name: str) -> str: name = str(name or "").strip() if len(name) >= 3: return name[0] + "β—‹" + name[2:] if len(name) == 2: return name[0] + "β—‹" return name def _clean_text(value: str) -> str: text = str(value or "").strip() while ",," in text: text = text.replace(",,", ",") return text.strip(" ,") def _symptom_process(data): """μž…λ ₯된 μƒνƒœλ³€ν™”μ— 따라 상담 과정이 λ‹¨μˆœ 볡뢙이 λ˜μ§€ μ•Šλ„λ‘ 확인 절차λ₯Ό μžλ™ κ΅¬μ„±ν•©λ‹ˆλ‹€.""" combined = " ".join([ data.get("main_condition", ""), data.get("client_status_change", ""), data.get("guardian_opinion", ""), data.get("staff_observation", ""), data.get("custom_need", ""), ]) steps = [] if any(k in combined for k in ["μ–΄μ§€", "ν˜„κΈ°", "휘청", "기립", "λ©€λ―Έ"]): steps.extend([ "μž…λ ₯된 μ–΄μ§€λŸΌ κ΄€λ ¨ λ‚΄μš©μ„ 상담기둝에 λ°˜μ˜ν•˜μ˜€λ‹€.", "μ–΄μ§€λŸΌκ³Ό ν•¨κ»˜ μž…λ ₯된 μƒν™œμƒ 뢈편 사항을 λ‹΄λ‹Ήμžμ™€ κ³΅μœ ν•˜κΈ°λ‘œ ν•˜μ˜€λ‹€.", ]) if any(k in combined for k in ["식사", "μ‹μš•", "μ˜μ–‘", "체쀑", "μ‚¬λ ˆ", "μ—°ν•˜"]): steps.extend([ "μž…λ ₯된 식사 κ΄€λ ¨ λ‚΄μš©μ„ 상담기둝에 λ°˜μ˜ν•˜μ˜€λ‹€.", "식사와 κ΄€λ ¨ν•˜μ—¬ μž…λ ₯된 μš”μ²­μ‚¬ν•­μ„ μ„œλΉ„μŠ€ 제곡 과정에 μ°Έκ³ ν•˜κΈ°λ‘œ ν•˜μ˜€λ‹€.", ]) if any(k in combined for k in ["낙상", "보행", "μ§€νŒ‘", "νœ μ²΄μ–΄", "λΆ€μΆ•", "이동"]): steps.extend([ "보행 보쑰도ꡬ μ‚¬μš©μƒνƒœ, 이동 동선, ν™”μž₯μ‹€ 이동 μ‹œ μœ„ν—˜μš”μΈ, λ°”λ‹₯ λ―Έλ„λŸΌ μ—¬λΆ€λ₯Ό ν™•μΈν•˜μ˜€λ‹€.", "λ‚™μƒμ˜ˆλ°©μ„ μœ„ν•΄ 이동 μ „ν›„ 직원 확인, 자리 이동 μ‹œ λΆ€μΆ•, ν”„λ‘œκ·Έλž¨ μ°Έμ—¬ μ‹œ μ•ˆμ „ν•œ μ’Œμ„ 배치λ₯Ό μ κ²€ν•˜κΈ°λ‘œ ν•˜μ˜€λ‹€.", ]) if any(k in combined for k in ["치맀", "인지", "κΈ°μ–΅", "λΆˆμ•ˆ", "초쑰", "배회"]): steps.extend([ "μž…λ ₯된 인지 및 μ •μ„œ κ΄€λ ¨ λ‚΄μš©μ„ 상담기둝에 λ°˜μ˜ν•˜μ˜€λ‹€.", "μ„€λͺ…은 μ§§κ³  반볡적으둜 μ œκ³΅ν•˜κ³ , μ΅μˆ™ν•œ ν™˜κ²½μ—μ„œ μ•ˆμ •κ°μ„ λŠλΌλ„λ‘ μ •μ„œμ§€μ§€λ₯Ό λ³‘ν–‰ν•˜κΈ°λ‘œ ν•˜μ˜€λ‹€.", ]) if any(k in combined for k in ["λ°°λ³€", "배뇨", "κΈ°μ €κ·€", "μ‹€κΈˆ", "λ³€λΉ„"]): steps.extend([ "배변·배뇨 양상, ν™”μž₯μ‹€ 이동 κ°€λŠ₯ μ—¬λΆ€, μ‹€κΈˆ λ˜λŠ” λ³€λΉ„ μ—¬λΆ€λ₯Ό ν™•μΈν•˜κ³  μœ„μƒκ΄€λ¦¬ ν•„μš”μ„±μ„ μ κ²€ν•˜μ˜€λ‹€.", ]) if not steps: steps = [ "μž…λ ₯된 μƒλ‹΄λ‚΄μš©μ„ μ€‘μ‹¬μœΌλ‘œ 상담 μ‚¬μœ μ™€ μš”μ²­μ‚¬ν•­μ„ κΈ°λ‘ν•˜μ˜€λ‹€.", "상담 쀑 λ³„λ„λ‘œ μž…λ ₯된 μœ„ν—˜μš”μΈμ΄ μ—†μ–΄ 일반 상담 λ‚΄μš©μœΌλ‘œ μ •λ¦¬ν•˜μ˜€λ‹€.", "μž…λ ₯된 λ‚΄μš©μ€ λ‹΄λ‹Ήμžμ™€ κ³΅μœ ν•˜κ³  ν˜„μž¬ μ„œλΉ„μŠ€ 제곡 과정에 μ°Έκ³ ν•˜κΈ°λ‘œ ν•˜μ˜€λ‹€.", ] # 쀑볡 제거 unique = [] for step in steps: if step not in unique: unique.append(step) return "\n".join([f"{i}. {step}" for i, step in enumerate(unique[:7], 1)]) def build_counseling_text(data): # Prepare inputs selected_items_str = ", ".join(data.get("selected_items_list", [])) keywords = data.get("keywords", "").strip() additional_needs = data.get("additional_needs", "").strip() # Check if AI should be used use_ai = st.session_state.get("use_lm_studio", False) ai_generated = False ai_content = "" if use_ai: system_prompt = COUNSELING_GENERATION_RULE user_prompt = f""" μˆ˜κΈ‰μžλͺ…: {data['masked_name']} μž₯κΈ°μš”μ–‘λ“±κΈ‰: {data['care_grade']} κΈ°κ΄€μœ ν˜•: {data['institution_type']} 상담 μ‚¬μœ : {data['counseling_reason']} 상담 λͺ©μ : {data['counseling_purpose']} μ„ νƒλœ κ²€ν†  ν•­λͺ©: {selected_items_str} 핡심 단어: {keywords} μΆ”κ°€ μš•κ΅¬/μš”μ²­ 사항: {additional_needs} 연동 μ—¬λΆ€: 사둀관리 ν•„μš”({data['case_needed']}), μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  ν•„μš”({data['needs_assessment_needed']}), κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ ν•„μš”({data['careplan_change_needed']}), λ“±κΈ‰λ³€κ²½ μ‹ μ²­ κ²€ν†  ν•„μš”({data['care_grade_review_needed']}), 보호자 μΆ”κ°€ 상담 ν•„μš”({data['followup_counseling_needed']}) """ basic_status = [] if data.get("cognitive_status", "").strip(): basic_status.append(f"- μΈμ§€μƒνƒœ: {data['cognitive_status']}") if data.get("mobility_status", "").strip(): basic_status.append(f"- μ΄λ™μƒνƒœ: {data['mobility_status']}") if data.get("diet_status", "").strip(): basic_status.append(f"- μ‹μ‚¬μƒνƒœ: {data['diet_status']}") if data.get("toileting_status", "").strip(): basic_status.append(f"- λ°°λ³€/λ°°λ‡¨μƒνƒœ: {data['toileting_status']}") if data.get("client_status_change", "").strip(): basic_status.append(f"- μƒνƒœλ³€ν™”: {data['client_status_change']}") if data.get("guardian_opinion", "").strip(): basic_status.append(f"- 보호자 의견: {data['guardian_opinion']}") if data.get("staff_observation", "").strip(): basic_status.append(f"- μ’…μ‚¬μž 관찰사항: {data['staff_observation']}") basic_status_str = "\n".join(basic_status) if basic_status_str: user_prompt += f"\nμˆ˜κΈ‰μž μƒνƒœ μ°Έκ³  정보:\n{basic_status_str}" try: client = OpenAI(base_url="http://127.0.0.1:1234/v1", api_key="lm-studio") completion = client.chat.completions.create( model="qwen2.5-7b-instruct", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=0.7, ) ai_content = completion.choices[0].message.content ai_generated = True except Exception as e: st.error(f"LM Studio AI 생성 μ‹€νŒ¨ ({e}). κ·œμΉ™ 기반 μƒμ„±κΈ°λ‘œ λŒ€μ²΄ν•©λ‹ˆλ‹€.") if not ai_generated: ai_content = generate_rule_based_counseling_text(data) # Parse sections parsed = parse_sections(ai_content) # Build content by combining sections 1, 2, 3, 5 content_parts = [] if parsed["상담 μ‚¬μœ "]: content_parts.append(f"[상담 μ‚¬μœ ]\n{parsed['상담 μ‚¬μœ ']}") if parsed["상담 λ‚΄μš©"]: content_parts.append(f"[상담 λ‚΄μš©]\n{parsed['상담 λ‚΄μš©']}") if parsed["전문적 사정"]: content_parts.append(f"[전문적 사정]\n{parsed['전문적 사정']}") if parsed["연동 ν•„μš” ν•­λͺ©"]: content_parts.append(f"[연동 ν•„μš” ν•­λͺ©]\n{parsed['연동 ν•„μš” ν•­λͺ©']}") content = "\n\n".join(content_parts) if not content: content = ai_content # fallback action = parsed["μ‘°μΉ˜κ³„νš"] or "상담 κ²°κ³Όλ₯Ό λ‹΄λ‹Ή 직원듀과 κ³΅μœ ν•˜κ³  μ„œλΉ„μŠ€ μ œκ³΅μ— λ°˜μ˜ν•¨." # Generate support methods support_methods = build_individual_support_methods(data) return content, action, support_methods def evaluate_record(data): # μƒˆ ν•„λ“œ(핡심단어, μΆ”κ°€μš•κ΅¬) λ°˜μ˜ν•˜μ—¬ 평가 κΈ°μ€€ μ™„ν™” 및 검증 has_keywords_or_needs = bool(data.get("keywords", "").strip()) or bool(data.get("additional_needs", "").strip()) required = { "μƒλ‹΄μΌμž": bool(data["counseling_date"]), "상담뢄λ₯˜": bool(data["counseling_type"]), "μƒλ‹΄μ‚¬μœ ": bool(data["counseling_reason"]), "μƒνƒœλ³€ν™” 반영": bool(data.get("client_status_change", "").strip()) or has_keywords_or_needs or "μƒνƒœλ³€ν™”" in data.get("counseling_content", ""), "보호자 의견 반영": bool(data.get("guardian_opinion", "").strip()) or has_keywords_or_needs or data["counseling_type"] == "λ³΄ν˜Έμžμƒλ‹΄", "μ’…μ‚¬μž κ΄€μ°° 반영": bool(data.get("staff_observation", "").strip()) or has_keywords_or_needs or data["counseling_type"] == "μ’…μ‚¬μžμƒλ‹΄", "ν›„μ†μ‘°μΉ˜": bool(data.get("action_plan", "").strip()), "κΈ‰μ—¬μ œκ³΅ 반영": "κΈ‰μ—¬" in data.get("counseling_content", "") or "κ³„νš" in data.get("action_plan", ""), "지원방법 ꡬ체화": bool(data.get("support_methods", "").strip()) or "κ°œμΈλ³„ μ΄μš©κ³„νš 지원방법" in data.get("counseling_content", ""), } score = int(sum(required.values()) / len(required) * 100) missing = [k for k, v in required.items() if not v] if score >= 90: result = "평가 적합도가 λ†’μŠ΅λ‹ˆλ‹€. μƒλ‹΄μ‚¬μœ , μƒνƒœλ³€ν™”, 보호자 의견, ν›„μ†μ‘°μΉ˜κ°€ 잘 λ°˜μ˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€." elif score >= 75: result = "λŒ€μ²΄λ‘œ μ ν•©ν•˜λ‚˜ 일뢀 보완이 ν•„μš”ν•©λ‹ˆλ‹€. λΆ€μ‘± ν•­λͺ©μ„ μΆ”κ°€ν•˜λ©΄ 평가 λŒ€μ‘λ ₯이 λ†’μ•„μ§‘λ‹ˆλ‹€." else: result = "보완이 ν•„μš”ν•©λ‹ˆλ‹€. μƒλ‹΄μ‚¬μœ , μƒνƒœλ³€ν™”, 보호자 의견, ν›„μ†μ‘°μΉ˜λ₯Ό 더 ꡬ체적으둜 μž…λ ₯ν•΄ μ£Όμ„Έμš”." return score, missing, result def style_range(ws, cell_range, fill=None, font=None, alignment=None): thin = Side(style="thin", color="000000") border = Border(left=thin, right=thin, top=thin, bottom=thin) cells = ws[cell_range] if not isinstance(cells, tuple): cells = ((cells,),) for row in cells: for cell in row: cell.border = border if fill: cell.fill = fill if font: cell.font = font if alignment: cell.alignment = alignment def split_text_by_length(text, max_len=650): text = str(text or "").strip() if not text: return [""] chunks = [] while len(text) > max_len: cut = text.rfind("\n", 0, max_len) if cut == -1: cut = text.rfind(". ", 0, max_len) if cut == -1: cut = text.rfind("λ‹€.", 0, max_len) if cut != -1: cut += 2 if cut == -1 or cut < int(max_len * 0.45): cut = max_len chunks.append(text[:cut].strip()) text = text[cut:].strip() if text: chunks.append(text) return chunks or [""] def generate_excel(data): wb = Workbook() ws = wb.active ws.title = "상담일지" ws.page_setup.paperSize = ws.PAPERSIZE_A4 ws.page_setup.orientation = "portrait" ws.page_setup.fitToWidth = 1 ws.page_setup.fitToHeight = 0 ws.sheet_properties.pageSetUpPr.fitToPage = True ws.page_margins.left = 0.3 ws.page_margins.right = 0.3 ws.page_margins.top = 0.4 ws.page_margins.bottom = 0.4 ws.sheet_view.showGridLines = False title_fill = PatternFill("solid", fgColor="E2F0D9") header_fill = PatternFill("solid", fgColor="F2F2F2") for col in range(1, 10): ws.column_dimensions[get_column_letter(col)].width = 11.5 ws.merge_cells("A1:G2") ws.cell(1, 1).value = "상 λ‹΄ 일 μ§€" ws.cell(1, 1).font = Font(name="Malgun Gothic", size=24, bold=True) ws.cell(1, 1).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) style_range(ws, "A1:G2", fill=title_fill) ws.cell(1, 8).value = "λ‹΄λ‹Ήμž" ws.cell(1, 9).value = "μ‹œμ„€μž₯" ws.cell(2, 8).value = "" ws.cell(2, 9).value = "" for r in [1, 2]: for c in [8, 9]: cell = ws.cell(r, c) cell.alignment = Alignment(horizontal="center", vertical="center") cell.font = Font(name="Malgun Gothic", size=11, bold=(r == 1)) if r == 1: cell.fill = header_fill style_range(ws, f"{get_column_letter(c)}{r}:{get_column_letter(c)}{r}") ws.row_dimensions[1].height = 28 ws.row_dimensions[2].height = 45 def write_info_row(row, a, b, c_val, d, e, f): ws.cell(row, 1).value = a ws.merge_cells(start_row=row, start_column=2, end_row=row, end_column=3) ws.cell(row, 2).value = b ws.cell(row, 4).value = c_val ws.merge_cells(start_row=row, start_column=5, end_row=row, end_column=6) ws.cell(row, 5).value = d ws.cell(row, 7).value = e ws.merge_cells(start_row=row, start_column=8, end_row=row, end_column=9) ws.cell(row, 8).value = f for col in [1, 4, 7]: ws.cell(row, col).fill = header_fill ws.cell(row, col).font = Font(name="Malgun Gothic", size=11, bold=True) ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) for col in [2, 5, 8]: ws.cell(row, col).font = Font(name="Malgun Gothic", size=11) ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) style_range(ws, f"A{row}:I{row}") ws.row_dimensions[row].height = 30 r = 3 write_info_row(r, "κΈ°κ΄€λͺ…", data["institution_name"], "κΈ°κ΄€μœ ν˜•", data["institution_type"], "상담뢄λ₯˜", data["counseling_type"]); r += 1 write_info_row(r, "μˆ˜κΈ‰μž", data["masked_name"], "μž₯κΈ°μš”μ–‘λ“±κΈ‰", data["care_grade"], "μƒλ‹΄μΌμž", data["counseling_date"]); r += 1 write_info_row(r, "μƒλ‹΄μž", data["counselor"], "μƒλ‹΄μ‚¬μœ ", data["counseling_reason"], "평가적합도", f"{data['evaluation_score']}점"); r += 1 def estimate_body_rows(text, min_rows=4): text = str(text or "") visual_lines = 0 for line in text.split("\n") or [""]: visual_lines += max(1, (len(line) // 62) + 1) return max(min_rows, min(24, visual_lines + 2)) def write_text_section(row, label, content_val, min_rows=4): pages = split_text_by_length(content_val, 650) for page_idx, page_text in enumerate(pages, start=1): display_label = f"{label} ({page_idx}/{len(pages)}νŽ˜μ΄μ§€)" if len(pages) > 1 else label ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=9) ws.cell(row, 1).value = display_label ws.cell(row, 1).font = Font(name="Malgun Gothic", size=11, bold=True) ws.cell(row, 1).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) ws.cell(row, 1).fill = header_fill style_range(ws, f"A{row}:I{row}", fill=header_fill) ws.row_dimensions[row].height = 24 body_start = row + 1 body_rows = estimate_body_rows(page_text, min_rows) body_end = body_start + body_rows - 1 ws.merge_cells(start_row=body_start, start_column=1, end_row=body_end, end_column=9) ws.cell(body_start, 1).value = page_text ws.cell(body_start, 1).font = Font(name="Malgun Gothic", size=10.5) ws.cell(body_start, 1).alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) for body_row in range(body_start, body_end + 1): ws.row_dimensions[body_row].height = 19 style_range(ws, f"A{body_start}:I{body_end}") row = body_end + 1 if page_idx < len(pages): ws.row_breaks.append(Break(id=row)) return row detail_rows = [ ("μƒλ‹΄λ‚΄μš©", data["counseling_content"], 8), ("κ°œμΈλ³„ μ΄μš©κ³„νš 지원방법", data.get("support_methods", ""), 6), ("μ‘°μΉ˜κ³„νš", data["action_plan"], 5), ("평가 적합도 점검", data["compliance_result"], 3), ("λΆ€μ‘±ν•œ ν•­λͺ©", data["missing_items"], 3), ] for label, content_val, min_rows in detail_rows: r = write_text_section(r, label, content_val, min_rows) # 곡톡 μ•ˆλ‚΄λ¬Έκ΅¬ μΆ”κ°€ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9) ws.cell(r, 1).value = "β€» μƒμ„±λœ λ¬Έμ„œλŠ” κ·ΈλŒ€λ‘œ μ‚¬μš©ν•˜μ…”λ„ 되며, κΈ°κ΄€μ˜ 운영방침, μˆ˜κΈ‰μž μƒνƒœ, 보호자 의견 및 평가기쀀에 따라 μΆ”κ°€ 보완사항은 μ–Όλ§ˆλ“ μ§€ μˆ˜μ • κ°€λŠ₯ν•©λ‹ˆλ‹€. κΈ°κ΄€μ˜ 상황에 맞게 ν™œμš©ν•˜μ‹œκΈ° λ°”λžλ‹ˆλ‹€." ws.cell(r, 1).font = Font(name="Malgun Gothic", size=8.5, color="595959", italic=True) ws.cell(r, 1).alignment = Alignment(horizontal="left", vertical="center", wrap_text=True) style_range(ws, f"A{r}:I{r}") ws.row_dimensions[r].height = 36 r += 1 ws.print_area = f"A1:I{r - 1}" output = BytesIO() wb.save(output) output.seek(0) return output clients_df = load_clients() # Sidebar config st.sidebar.subheader("AI μ„€μ •") use_lm_studio = st.sidebar.toggle("LM Studio / Qwen μ‚¬μš©", value=False) st.session_state["use_lm_studio"] = use_lm_studio if DB_PATH and os.path.exists(DB_PATH): st.sidebar.success("Client Center DB 연결됨") st.sidebar.caption(DB_PATH) else: st.sidebar.warning("Client Center DBλ₯Ό μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. 직접 μž…λ ₯ λͺ¨λ“œλ‘œ μž‘λ™ν•©λ‹ˆλ‹€.") st.subheader("1단계. μˆ˜κΈ‰μž 선택 및 기본정보") 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['μž₯κΈ°μš”μ–‘λ“±κΈ‰']} / {x['κΈ°κ΄€μœ ν˜•']} / {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] elif mode == "Client Centerμ—μ„œ μˆ˜κΈ‰μž 선택" and clients_df.empty: st.warning("λ“±λ‘λœ μˆ˜κΈ‰μž 정보가 μ—†κ±°λ‚˜ DBλ₯Ό μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. 직접 μž…λ ₯ λͺ¨λ“œλ‘œ μ§„ν–‰ν•˜μ„Έμš”.") # Prefill variables if selected_client is not None: default_name = selected_client.get("name", "홍길동") default_masked_name = selected_client.get("masked_name", "홍○동") default_grade = str(selected_client.get("μž₯κΈ°μš”μ–‘λ“±κΈ‰", "3λ“±κΈ‰")).strip() if default_grade in ["1", "2", "3", "4", "5"]: default_grade = default_grade + "λ“±κΈ‰" default_inst_type = selected_client.get("κΈ°κ΄€μœ ν˜•", "λ°©λ¬Έμš”μ–‘") default_inst_name = selected_client.get("κΈ°κ΄€λͺ…", "") default_recognition_number = selected_client.get("μΈμ •λ²ˆν˜Έ", "") default_recognition_start = selected_client.get("인정기간 μ‹œμž‘μΌ", "") default_recognition_end = selected_client.get("인정기간 μ’…λ£ŒμΌ", "") default_main_condition = selected_client.get("main_condition", "") # Load status inputs if available default_cognitive = selected_client.get("cognitive_status", "") default_mobility = selected_client.get("mobility_status", "") default_diet = selected_client.get("diet_status", "") default_toileting = selected_client.get("toileting_status", "") default_notes = selected_client.get("notes", "") else: default_name = "홍길동" default_masked_name = "홍○동" default_grade = "3λ“±κΈ‰" default_inst_type = "λ°©λ¬Έμš”μ–‘" default_inst_name = "μž₯κΈ°μš”μ–‘κΈ°κ΄€" default_recognition_number = "" default_recognition_start = "" default_recognition_end = "" default_main_condition = "" default_cognitive = "" default_mobility = "" default_diet = "" default_toileting = "" default_notes = "" # Show input columns col1, col2, col3 = st.columns(3) institution_name = col1.text_input("κΈ°κ΄€λͺ…", default_inst_name) institution_type = col2.selectbox( "κΈ°κ΄€μœ ν˜•", list(INSTITUTION_GUIDES.keys()), index=list(INSTITUTION_GUIDES.keys()).index(default_inst_type) if default_inst_type in INSTITUTION_GUIDES else 0 ) counseling_type = col3.radio("상담뢄λ₯˜", ["μˆ˜κΈ‰μžμƒλ‹΄", "λ³΄ν˜Έμžμƒλ‹΄", "μ’…μ‚¬μžμƒλ‹΄", "기타상담"], horizontal=True) col4, col5, col6, col7 = st.columns(4) name = col4.text_input("μˆ˜κΈ‰μž 이름", default_name) masked_name = col5.text_input("κ°€λͺ… ν‘œκΈ°", default_masked_name) care_grade = col6.selectbox( "μž₯κΈ°μš”μ–‘λ“±κΈ‰", ["1λ“±κΈ‰", "2λ“±κΈ‰", "3λ“±κΈ‰", "4λ“±κΈ‰", "5λ“±κΈ‰", "인지지원등급", "λ―Έμž…λ ₯"], index=["1λ“±κΈ‰", "2λ“±κΈ‰", "3λ“±κΈ‰", "4λ“±κΈ‰", "5λ“±κΈ‰", "인지지원등급", "λ―Έμž…λ ₯"].index(default_grade) if default_grade in ["1λ“±κΈ‰", "2λ“±κΈ‰", "3λ“±κΈ‰", "4λ“±κΈ‰", "5λ“±κΈ‰", "인지지원등급", "λ―Έμž…λ ₯"] else 2 ) counseling_date = col7.date_input("μƒλ‹΄μΌμž", value=date.today()) col8, col9, col10 = st.columns(3) counselor = col8.text_input("μƒλ‹΄μž", "μ‚¬νšŒλ³΅μ§€μ‚¬") counseling_reason = col9.selectbox( "상담 μ‚¬μœ  (trigger)", [ "μƒνƒœλ³€ν™”κΈ°λ‘μ§€ μƒλ‹΄ν•„μš” 기둝", "μˆ˜κΈ‰μž μš”μ²­", "보호자 μš”μ²­", "μš”μ–‘λ³΄ν˜Έμ‚¬/μ’…μ‚¬μž μš”μ²­", "μ‚¬νšŒλ³΅μ§€μ‚¬ νŒλ‹¨", "μ‹œμ„€μž₯/κ΄€λ¦¬μ±…μž„μž νŒλ‹¨", "사둀관리 κ²€ν†  ν•„μš”", "μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  ν•„μš”", "κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ ν•„μš”", "기타 μ§μ ‘μž…λ ₯", ] ) main_condition = col10.text_input("μ£Όμš” μ§ˆν™˜/μƒνƒœ", default_main_condition) # Recognition numbers and dates (Section 3-1) col_rec1, col_rec2, col_rec3 = st.columns(3) recognition_number = col_rec1.text_input("μΈμ •λ²ˆν˜Έ", default_recognition_number) recognition_start_date = col_rec2.text_input("인정기간 μ‹œμž‘μΌ", default_recognition_start, placeholder="예: 2025-03-03") recognition_end_date = col_rec3.text_input("인정기간 μ’…λ£ŒμΌ", default_recognition_end, placeholder="예: 2027-03-02") st.subheader("2단계. 상담 λͺ©μ  및 κ΄€λ ¨ ν•­λͺ© 선택") counseling_purpose = st.radio( "이번 μƒλ‹΄μ˜ λͺ©μ ", [ "일반 상담", "사둀관리 κ²€ν†  상담", "μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  상담", "κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ 상담", "볡합 상담", ], horizontal=True ) selected_items_list = [] # Dynamic multiselect elements based on purpose if counseling_purpose in ["사둀관리 κ²€ν†  상담", "볡합 상담"]: case_items = st.multiselect( "사둀관리 κ²€ν†  ν•­λͺ©", [ "이동λŠ₯λ ₯ μ €ν•˜", "μ›Œμ»€ μ‚¬μš©μ—μ„œ νœ μ²΄μ–΄ μ‚¬μš©μœΌλ‘œ λ³€κ²½", "반볡 낙상 λ˜λŠ” λ‚™μƒμœ„ν—˜ 증가", "ν•˜μ§€κ·Όλ ₯ μ•½ν™”", "μž…μ› λ˜λŠ” 퇴원 ν›„ 관리 ν•„μš”", "μž₯κΈ°μš”μ–‘λ“±κΈ‰ λ³€κ²½ μ‹ μ²­ κ²€ν† ", "보호자 λŒλ΄„ λΆ€λ‹΄ 증가", "μ„œλΉ„μŠ€ μ‹œκ°„ λ˜λŠ” 횟수 μ‘°μ • ν•„μš”", "보쑰기ꡬ λ³€κ²½ ν•„μš”", "μΉ˜λ§€ν–‰λ™ λ˜λŠ” 인지기λŠ₯ λ³€ν™”", "μ‹μ‚¬λŸ‰ κ°μ†Œ λ˜λŠ” μ˜μ–‘λΆˆκ· ν˜•", "배변·배뇨 문제 증가", "μ •μ„œλΆˆμ•ˆ, 우울, 수면문제", "κ°€μ‘±Β·μ£Όκ±°Β·κ²½μ œμ  문제", "μ§€μ—­μ‚¬νšŒ μžμ› 연계 ν•„μš”", "기타 μ§μ ‘μž…λ ₯", ] ) selected_items_list.extend(case_items) if counseling_purpose in ["μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  상담", "볡합 상담"]: needs_items = st.multiselect( "μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  ν•­λͺ©", [ "ADL λ³€ν™”", "IADL λ³€ν™”", "보행 및 이동λŠ₯λ ₯ λ³€ν™”", "보쑰기ꡬ μ‚¬μš© λ³€ν™”", "인지기λŠ₯ λ³€ν™”", "μ˜μ‚¬μ†Œν†΅ λ³€ν™”", "식사 및 μ˜μ–‘μƒνƒœ λ³€ν™”", "배변·배뇨 μƒνƒœ λ³€ν™”", "λ‚™μƒμœ„ν—˜ 증가", "μš•μ°½μœ„ν—˜ 증가", "톡증 증가", "μˆ˜λ©΄μƒνƒœ λ³€ν™”", "μ •μ„œμƒνƒœ λ³€ν™”", "보호자 λ˜λŠ” κ°€μ‘±ν™˜κ²½ λ³€ν™”", "μ£Όκ±°ν™˜κ²½ λ³€ν™”", "μž…μ›Β·ν‡΄μ›Β·μ§ˆλ³‘μƒνƒœ λ³€ν™”", "기타 μ§μ ‘μž…λ ₯", ] ) selected_items_list.extend(needs_items) if counseling_purpose in ["κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ 상담", "볡합 상담"]: careplan_items = st.multiselect( "κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ ν•­λͺ©", [ "μ„œλΉ„μŠ€ μ‹œκ°„ λ³€κ²½ ν•„μš”", "μ„œλΉ„μŠ€ 횟수 λ³€κ²½ ν•„μš”", "μ„œλΉ„μŠ€ λ‚΄μš© λ³€κ²½ ν•„μš”", "μ‹ μ²΄ν™œλ™μ§€μ› λ³€κ²½", "μΈμ§€ν™œλ™μ§€μ› λ³€κ²½", "μ •μ„œμ§€μ› λ³€κ²½", "μΌμƒμƒν™œμ§€μ› λ³€κ²½", "가쑱상담 λ˜λŠ” 보호자 지원 ν•„μš”", "λ‚™μƒμ˜ˆλ°© κ³„νš 반영", "μš•μ°½μ˜ˆλ°© κ³„νš 반영", "μ‹μ‚¬Β·μ˜μ–‘κ΄€λ¦¬ κ³„νš 반영", "배변·배뇨관리 κ³„νš 반영", "보쑰기ꡬ λ³€κ²½ 반영", "λ“±κΈ‰λ³€κ²½ μ‹ μ²­ κ²€ν† ", "λͺ©ν‘œ μˆ˜μ • ν•„μš”", "곡단 μž…λ ₯ 참고자료 ν•„μš”", "기타 μ§μ ‘μž…λ ₯", ] ) selected_items_list.extend(careplan_items) st.subheader("3단계. 상세 정보 μž…λ ₯") col_text1, col_text2 = st.columns(2) with col_text1: keywords = st.text_area( "핡심 단어 λ˜λŠ” 짧은 λ¬Έμž₯ μž…λ ₯", placeholder="예: μ›Œμ»€β†’νœ μ²΄μ–΄, λ“±κΈ‰λ³€κ²½, 보호자 λΆ€λ‹΄", help="μž…λ ₯ν•œ 단어와 μ„ νƒν•œ ν•­λͺ©λ§Œ λ¬Έμ„œμ— λ°˜μ˜λ©λ‹ˆλ‹€. μž…λ ₯ν•˜μ§€ μ•Šμ€ λ‚΄μš©μ€ μž„μ˜ μƒμ„±λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€." ) st.caption("β€» 사둀관리, μš•κ΅¬μ‚¬μ • μž¬κ²€ν† , κΈ‰μ—¬μ œκ³΅κ³„νš 변경이 ν•„μš”ν•œ κ²½μš°μ—λŠ” κ΄€λ ¨ ν•­λͺ©μ„ μ„ νƒν•˜κ³  μœ„μ— 핡심 단어λ₯Ό 2~3개 μž…λ ₯ν•΄ μ£Όμ„Έμš”.") with col_text2: additional_needs = st.text_area( "μΆ”κ°€μš•κ΅¬ / μš”μ²­μ‚¬ν•­", placeholder="예: 사둀관리 ν•„μš” - μ›Œμ»€ μ‚¬μš©μ—μ„œ νœ μ²΄μ–΄ μ‚¬μš©μœΌλ‘œ λ³€κ²½λ˜μ–΄ 이동지원 μž¬κ²€ν†  ν•„μš”", help="μž…λ ₯된 λ‚΄μš©μ€ ν•΄λ‹Ή λ¬Έμ„œ μž‘μ„± μ‹œ 참고자료둜 μ—°λ™λ©λ‹ˆλ‹€." ) st.caption("β€» 사둀관리, μš•κ΅¬μ‚¬μ • μž¬κ²€ν† , κΈ‰μ—¬μ œκ³΅κ³„νš 변경이 ν•„μš”ν•œ κ²½μš°μ—λŠ” 이곳에 ꡬ체적인 μ‚¬μœ λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”.") # Optional/Basic input fields just in case they want to add them with st.expander("πŸ” μΆ”κ°€ μƒνƒœ κ΄€μ°° 정보 μž…λ ₯ (κΈ°λ³Έ μƒνƒœ/Q&A μž…λ ₯ 펼치기)"): input_mode = st.radio("μž…λ ₯ 방식", ["κΈ°λ³Έ μž…λ ₯", "Q&A μž…λ ₯", "κΈ°λ³Έ+Q&A μž…λ ₯"], horizontal=True) use_basic_input = input_mode in ["κΈ°λ³Έ μž…λ ₯", "κΈ°λ³Έ+Q&A μž…λ ₯"] use_qna_input = input_mode in ["Q&A μž…λ ₯", "κΈ°λ³Έ+Q&A μž…λ ₯"] cognitive_status = "" mobility_status = "" diet_status = "" toileting_status = "" client_status_change = "" guardian_opinion = "" staff_observation = "" custom_need = "" qna_answers = {} if use_basic_input: c1, c2 = st.columns(2) cognitive_status = c1.text_area("μΈμ§€μƒνƒœ", value=default_cognitive, placeholder="예: 반볡질문 증가, μ‹œκ°„/μž₯μ†Œ ν˜Όλ™ λ“±", height=80) mobility_status = c2.text_area("μ΄λ™μƒνƒœ", value=default_mobility, placeholder="예: 보행 λΆˆμ•ˆμ •, λΆ€μΆ• ν•„μš” λ“±", height=80) c3, c4 = st.columns(2) diet_status = c3.text_area("μ‹μ‚¬μƒνƒœ", value=default_diet, placeholder="예: μ‹μ‚¬λŸ‰ κ°μ†Œ, μ‚¬λ ˆ, μ—°ν•˜κ³€λž€ λ“±", height=80) toileting_status = c4.text_area("λ°°λ³€/λ°°λ‡¨μƒνƒœ", value=default_toileting, placeholder="예: λ³€λΉ„, μ‹€κΈˆ, κΈ°μ €κ·€ μ‚¬μš© 증가 λ“±", height=80) c5, c6 = st.columns(2) client_status_change = c5.text_area("μˆ˜κΈ‰μž μƒνƒœλ³€ν™”", placeholder="예: 졜근 μ–΄μ§€λŸΌμ¦μ΄ μžˆμ–΄ 보행을 κΊΌλ¦Ό, μ‹μ‚¬λŸ‰ κ°μ†Œ, 수면 λ³€ν™” λ“±", height=100) guardian_opinion = c6.text_area("보호자 의견", placeholder="예: μ–΄μ§€λŸΌμ΄ 심해지면 λ³‘μ›μ§„λ£Œλ₯Ό μœ„ν•΄ 병원동행 μš”μ²­", height=100) c7, c8 = st.columns(2) staff_observation = c7.text_area("μ’…μ‚¬μž 관찰사항", placeholder="예: 이동 μ‹œ λΆ€μΆ• 제곡, μ•ˆμƒ‰/λ³΄ν–‰μƒνƒœ κ΄€μ°°, μ‹μ‚¬λŸ‰ 확인 λ“±", height=100) custom_need = c8.text_area("μΆ”κ°€ μš•κ΅¬ / μš”μ²­μ‚¬ν•­", value=default_notes, placeholder="예: 이동 도움 κ°•ν™”, ν˜ˆμ•• μž¬ν™•μΈ, λ³΄ν˜Έμžμ—κ²Œ 증상변화 μ•ˆλ‚΄ λ“±", height=100) if use_qna_input: st.info(f"{counseling_type}에 λ§žλŠ” 질문이 μžλ™ ν‘œμ‹œλ©λ‹ˆλ‹€. λ‹΅λ³€ λ‚΄μš©μ€ μƒλ‹΄λ‚΄μš© μ •λˆμ— ν™œμš©λ©λ‹ˆλ‹€.") questions = QA_RULES.get(counseling_type, QA_RULES["기타상담"]) for idx, question in enumerate(questions, 1): qna_answers[question] = st.text_area(f"Q{idx}. {question}", key=f"qna_{counseling_type}_{idx}", height=80) st.subheader("4단계. 연동 μ„€μ • 및 생성") col_chk1, col_chk2, col_chk3, col_chk4, col_chk5 = st.columns(5) case_needed = col_chk1.checkbox("사둀관리 ν•„μš”", value=(counseling_purpose == "사둀관리 κ²€ν†  상담")) needs_assessment_needed = col_chk2.checkbox("μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  ν•„μš”", value=(counseling_purpose == "μš•κ΅¬μ‚¬μ • μž¬κ²€ν†  상담")) careplan_change_needed = col_chk3.checkbox("κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ ν•„μš”", value=(counseling_purpose == "κΈ‰μ—¬μ œκ³΅κ³„νš λ³€κ²½ 상담")) care_grade_review_needed = col_chk4.checkbox("λ“±κΈ‰λ³€κ²½ μ‹ μ²­ κ²€ν†  ν•„μš”") followup_counseling_needed = col_chk5.checkbox("보호자 μΆ”κ°€ 상담 ν•„μš”") if st.button("πŸ“ 상담일지 λΉ λ₯΄κ²Œ 생성", use_container_width=True): data = { "client_id": int(selected_client["id"]) if selected_client is not None and "id" in selected_client else 0, "institution_name": institution_name, "institution_type": institution_type, "counseling_type": counseling_type, "name": name, "masked_name": masked_name, "care_grade": care_grade, "counseling_date": str(counseling_date), "counselor": counselor, "counseling_reason": counseling_reason, # trigger "counseling_purpose": counseling_purpose, "selected_items_list": selected_items_list, "keywords": keywords, "additional_needs": additional_needs, "case_needed": case_needed, "needs_assessment_needed": needs_assessment_needed, "careplan_change_needed": careplan_change_needed, "care_grade_review_needed": care_grade_review_needed, "followup_counseling_needed": followup_counseling_needed, "main_condition": main_condition, # Optional basic status fields "cognitive_status": cognitive_status, "mobility_status": mobility_status, "diet_status": diet_status, "toileting_status": toileting_status, "client_status_change": client_status_change, "guardian_opinion": guardian_opinion, "staff_observation": staff_observation, "custom_need": custom_need, "qna_answers": qna_answers, } content, action, support_methods = build_counseling_text(data) data["counseling_content"] = content data["action_plan"] = action data["support_methods"] = support_methods score, missing, result = evaluate_record(data) data["evaluation_score"] = score data["missing_items"] = ", ".join(missing) if missing else "λΆ€μ‘± ν•­λͺ© μ—†μŒ" data["compliance_result"] = result st.session_state["counseling_data"] = data st.session_state.pop("counseling_excel", None) if "counseling_data" in st.session_state: data = st.session_state["counseling_data"] st.markdown("---") st.subheader("5단계. 생성 κ²°κ³Ό 확인/μˆ˜μ •") data["counseling_content"] = st.text_area("상담 μ‚¬μœ  및 λ‚΄μš©", value=data["counseling_content"], height=280) data["action_plan"] = st.text_area("μ‘°μΉ˜κ³„νš", value=data["action_plan"], height=130) data["support_methods"] = st.text_area("κ°œμΈλ³„ μ΄μš©κ³„νš 지원방법", value=data.get("support_methods", ""), height=150) # Recalculate score on modification score, missing, result = evaluate_record(data) data["evaluation_score"] = score data["missing_items"] = ", ".join(missing) if missing else "λΆ€μ‘± ν•­λͺ© μ—†μŒ" data["compliance_result"] = result analysis = analyze_counseling_answers(data) m1, m2, m3 = st.columns(3) m1.metric("평가 적합도", f"{score}점") m2.metric("λΆ€μ‘± ν•­λͺ©", data["missing_items"]) m3.metric("좜λ ₯", "μ—‘μ…€ 선택 생성") link1, link2, link3 = st.columns(3) link1.metric("사둀관리 연계", "ν•„μš”" if data["case_needed"] else "ν•΄λ‹Ή μ—†μŒ") link2.metric("μš•κ΅¬μ‚¬μ • μž¬κ²€ν† ", "ν•„μš”" if data["needs_assessment_needed"] else "ν•΄λ‹Ή μ—†μŒ") link3.metric("κΈ‰μ—¬κ³„νš 반영", "ν•„μš”" if data["careplan_change_needed"] else "μœ μ§€") if score >= 90: st.markdown(f'
{result}
', unsafe_allow_html=True) else: st.markdown(f'
{result}
', unsafe_allow_html=True) col_save1, col_save2 = st.columns(2) with col_save1: if st.button("πŸ’Ύ DB에 상담기둝 μ €μž₯ν•˜κΈ°", use_container_width=True): success = save_counseling_record(data) if success: st.success("λŒ€ν‘œλ‹˜! 상담일지 기둝이 μ„±κ³΅μ μœΌλ‘œ DB에 μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€! 😎") else: st.error("DB μ €μž₯ μ‹€νŒ¨. 둜그λ₯Ό ν™•μΈν•˜μ„Έμš”.") with col_save2: if st.button("πŸ“¦ μ—‘μ…€ 파일 μ€€λΉ„ν•˜κΈ°", use_container_width=True): st.session_state["counseling_excel"] = generate_excel(data) st.success("μ—‘μ…€ 파일이 μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.") if "counseling_excel" in st.session_state: filename = f"상담일지_{data['masked_name']}_{data['counseling_type']}_{datetime.now().strftime('%Y%m%d')}.xlsx" st.download_button("πŸ“₯ 상담일지 μ—‘μ…€ λ‹€μš΄λ‘œλ“œ", data=st.session_state["counseling_excel"], file_name=filename, mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", use_container_width=True) st.caption("톡합 연동 슀마트 버전: Client Center DB 및 Excel 데이터와 μƒν˜Έμž‘μš©ν•˜μ—¬ 상담일지λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.")