from datetime import date, datetime, timedelta
from io import BytesIO
import os
import random
import sqlite3
from openpyxl import Workbook
from openpyxl.drawing.image import Image as XLImage
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
import pandas as pd
from PIL import Image as PILImage
import streamlit as st
# =========================================================
# 통합연동 사례관리 생성기 (클라우드/허깅페이스 최적화 버전)
# =========================================================
st.set_page_config(page_title="통합 사례관리 생성기", page_icon="📋", layout="wide")
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
DB_DIR = os.path.join(CURRENT_DIR, "data")
os.makedirs(DB_DIR, exist_ok=True)
DB_PATH = os.path.join(DB_DIR, "clients.db")
# -----------------------------
# UI 스타일
# -----------------------------
st.markdown(
"""
""",
unsafe_allow_html=True,
)
st.markdown(
"""
📋 통합 사례관리 회의록 자동생성기
수급자 상태·상담일지·욕구사정 연동 1차 계획회의록 및 2차 결과회의록 완벽 자동화
""",
unsafe_allow_html=True,
)
# -----------------------------
# DB 초기화 및 관리
# -----------------------------
def get_conn():
return sqlite3.connect(DB_PATH, check_same_thread=False)
def init_db():
conn = get_conn()
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
masked_name TEXT,
gender TEXT,
birth_year TEXT,
care_grade TEXT,
institution_type TEXT,
institution_name TEXT,
admission_date TEXT,
main_condition TEXT,
cognitive_status TEXT,
mobility_status TEXT,
diet_status TEXT,
toileting_status TEXT,
guardian_name TEXT,
guardian_phone TEXT,
notes TEXT,
status TEXT DEFAULT '이용중',
recognition_number TEXT,
recognition_start_date TEXT,
recognition_end_date TEXT,
created_at TEXT
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS counseling_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER,
counseling_date TEXT,
counseling_type TEXT,
counseling_reason TEXT,
client_status_change TEXT,
guardian_opinion TEXT
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS status_change_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER,
record_date TEXT,
change_category TEXT,
body_status TEXT,
cognitive_status TEXT,
staff_observation TEXT,
guardian_opinion TEXT
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS needs_assessment_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER,
assessment_date TEXT,
total_opinion TEXT,
result_plan TEXT,
fall_risk TEXT,
bedsore_risk TEXT,
dementia_status TEXT,
diaper_use TEXT,
service_needs TEXT
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS case_management_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER,
client_masked_name TEXT,
institution_type TEXT,
case_type TEXT,
first_meeting_date TEXT,
second_meeting_date TEXT,
selection_reason TEXT,
first_plan TEXT,
implementation_process TEXT,
second_result TEXT,
evaluation_score INTEGER,
missing_items TEXT,
created_at TEXT
)
""")
conn.commit()
conn.close()
init_db()
def load_clients():
conn = get_conn()
try:
df = pd.read_sql_query(
"SELECT * FROM clients ORDER BY id DESC LIMIT 300", conn
)
except Exception:
df = pd.DataFrame()
conn.close()
return df
def save_case_record(data):
conn = get_conn()
cur = conn.cursor()
cur.execute(
"""
INSERT INTO case_management_records (
client_id, client_masked_name, institution_type, case_type,
first_meeting_date, second_meeting_date, selection_reason,
first_plan, implementation_process, second_result,
evaluation_score, missing_items, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
data["client_id"],
data["client_masked_name"],
data["institution_type"],
data["case_type"],
data["first_meeting_date"],
data["second_meeting_date"],
data["selection_reason"],
data["first_plan"],
data["implementation_process"],
data["second_result"],
data["evaluation_score"],
data["missing_items"],
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
),
)
conn.commit()
conn.close()
def safe(row, key, default=""):
try:
v = row.get(key, default)
return default if pd.isna(v) else str(v)
except Exception:
return default
def mask_name(name):
name = str(name or "").strip()
if len(name) >= 3:
return name[0] + "○" + name[2:]
if len(name) == 2:
return name[0] + "○"
return name or "수급자"
def days_until(date_text):
v = str(date_text or "").strip()
if not v or v.lower() == "nan":
return None
try:
target = pd.to_datetime(v).date()
return (target - date.today()).days
except Exception:
return None
def recognition_alert_text(end_date_text):
days = days_until(end_date_text)
if days is None:
return "인정기간 종료일 미입력"
if days < 0:
return f"인정기간 만료 {abs(days)}일 경과"
if days <= 30:
return f"만료 {days}일 전: 급여계획 재검토 필요"
if days <= 90:
return f"만료 {days}일 전: 갱신/계획 확인"
return f"만료 {days}일 전 (유효)"
# -----------------------------
# 사례관리 데이터베이스
# -----------------------------
CASE_TYPES = {
"낙상 고위험 관리": {
"reason": (
"이동 중 불안정, 화장실 이동 위험, 보행보조 필요 등 낙상 위험요인이"
" 확인되어 사례관리가 필요함."
),
"goal": "낙상 사고를 예방하고 안전한 이동과 생활환경을 유지한다.",
"keywords": [
"낙상예방",
"이동보조",
"환경정비",
"화장실 동행",
"보행상태 관찰",
],
},
"식사량 감소·영양관리": {
"reason": (
"식사량 감소, 수분섭취 부족, 체중저하 우려가 확인되어 영양상태"
" 관리를 위한 사례관리가 필요함."
),
"goal": (
"식사량과 수분섭취를 안정적으로 유지하고 영양 저하 위험을 예방한다."
),
"keywords": [
"식사보조",
"수분섭취",
"선호음식",
"연하상태",
"섭취량 기록",
],
},
"인지저하·정서불안": {
"reason": (
"반복질문, 의심표현, 불안감, 정서적 위축 등 인지·정서 변화가"
" 확인되어 지속 관찰과 개별 대응이 필요함."
),
"goal": (
"불안감을 완화하고 안정적인 일상생활과 서비스 이용이 이루어지도록"
" 지원한다."
),
"keywords": [
"정서지원",
"인지활동",
"반복설명",
"안정적 환경",
"보호자 상담",
],
},
"욕창·피부상태 관리": {
"reason": (
"기저귀 착용, 활동량 저하, 피부 발적 또는 욕창 위험이 확인되어"
" 피부상태 관찰과 예방관리가 필요함."
),
"goal": "피부 청결과 안위를 유지하고 욕창 발생을 예방한다.",
"keywords": [
"피부관찰",
"체위변경",
"기저귀케어",
"보습",
"간호관찰",
],
},
"프로그램 참여 저하": {
"reason": (
"프로그램 참여 감소, 무기력, 대화 회피 등 사회적·정서적 위축이"
" 관찰되어 사례관리가 필요함."
),
"goal": (
"선호 활동 중심으로 참여를 유도하고 사회적 관계와 활력을 높인다."
),
"keywords": [
"선호활동",
"참여격려",
"정서지원",
"프로그램 조정",
"참여도 기록",
],
},
"보호자 요청·서비스 조정": {
"reason": (
"보호자 요청 또는 서비스 조정 필요성이 확인되어 급여제공 방향"
" 재검토와 사례관리가 필요함."
),
"goal": (
"보호자 의견과 수급자 상태를 반영하여 적절한 서비스 제공 방향을"
" 조정한다."
),
"keywords": [
"보호자 상담",
"서비스 조정",
"급여계획 반영",
"종사자 공유",
"후속상담",
],
},
}
CASE_DETAILS = {
"낙상 고위험 관리": {
"subjective": (
"수급자 및 보호자는 최근 보행 시 다리에 힘이 풀리거나 어지럼증이"
" 자주 발생하여 이동 시 불안감이 매우 크다고 호소함."
),
"objective": (
"보행보조기를 사용하더라도 걸음걸이가 불안정하며 기립 시 어지럼증이"
" 관찰됨. 바닥 전선 및 문턱 등 낙상 위험요소가 확인됨."
),
"assessment": (
"하지 근력 저하와 균형 감각 감퇴로 인한 낙상 위험도가 높은"
" 상태임. 이동 동선 정비 및 동행 부축 지원이 시급함."
),
"plan": [
"실내외 보행 시 1:1 밀착 부축 지원",
"미끄럼 방지 매트 및 안전 손잡이 점검",
"야간 화장실 이동 시 안전 동행 및 조명 확보",
"보행보조기 바퀴 및 제동장치 정기 점검",
],
"resource": (
"보건소 낙상예방 운동 프로그램 안내 및 복지용구(안전손잡이 등)"
" 추가 연계 검토"
),
},
"식사량 감소·영양관리": {
"subjective": (
"수급자는 입맛이 전혀 없다고 식사를 거부하거나 평소 섭취량의 절반"
" 이하만 섭취한다고 호소함."
),
"objective": (
"최근 식사 섭취량이 기존 대비 50% 미만으로 감소하였으며 연하 곤란과"
" 구강 건조가 관찰됨."
),
"assessment": (
"지속적인 섭취 저하 시 영양결핍 및 면역력 저하가 우려되므로"
" 선호식단 반영과 부드러운 유동식 지원이 필요함."
),
"plan": [
"수급자 선호 기호식 파악 및 식단 반영",
"식사 중 사레들림 방지를 위한 연하보조 지원",
"식간 충분한 수분 섭취 독려 및 기록",
"주기적인 체중 및 영양상태 모니터링",
],
"resource": (
"지역 보건소 영양플러스 상담 및 필요 시 영양 수액 진료 연계"
),
},
"인지저하·정서불안": {
"subjective": (
"수급자는 본인 물건을 남이 가져갔다고 의심하며 반복 질문과 불안감을"
" 강하게 표현함."
),
"objective": (
"시간 및 장소에 대한 지남력 저하가 관찰되며 외부 자극에 방어적 태도"
" 및 배회 행동을 보임."
),
"assessment": (
"치매 인지저하에 따른 BPSD(행동심리증상)로 판단되며 정면 반박을"
" 지양하고 정서적 지지와 공감이 필요함."
),
"plan": [
"날짜와 장소 반복 안내 및 현실감각 훈련",
"의심·망상 표현 시 수용적 태도 및 화제 전환",
"익숙한 소지품 중심의 환경 유지",
"회상 대화 및 1:1 맞춤형 인지활동 제공",
],
"resource": "치매안심센터 가족교실 및 인지자극 쉼터 프로그램 연계",
},
"욕창·피부상태 관리": {
"subjective": (
"수급자는 둔부 및 꼬리뼈 부위가 가렵고 뻐근하다고 호소하며 기저귀"
" 착용 시 답답함을 표현함."
),
"objective": (
"와상 상태로 활동량이 매우 적으며 천골 부위에 경미한 피부 발적이"
" 확인됨."
),
"assessment": (
"혈액순환 저하 및 습윤 환경으로 인한 욕창 발생 위험이 높아 2시간"
" 단위 체위변경과 청결 관리가 필수적임."
),
"plan": [
"주기적인 체위변경(2시간 간격) 철저 시행",
"기저귀 오염 즉시 교체 및 피부 건조 유지",
"체압 분산용 에어매트리스 점검",
"피부 보습제 도포 및 매일 발적 여부 관찰",
],
"resource": "욕창 드레싱 및 전문 치료를 위한 방문간호 서비스 연계",
},
"프로그램 참여 저하": {
"subjective": (
"수급자는 사람들과 어울리기 싫고 프로그램 참여가 귀찮다며 생활실에"
" 혼자 머물기를 원함."
),
"objective": (
"최근 단체 프로그램 참여율이 급감하였고 타 수급자와의 대화를"
" 회피하며 무기력한 태도를 보임."
),
"assessment": (
"정서적 위축 및 우울감에 기인한 것으로 보이며 강요보다는 선호하는"
" 소규모 개별 활동부터 접근이 필요함."
),
"plan": [
"선호 취미 및 과거 활동 파악 후 1:1 맞춤 접근",
"프로그램 참여 시 칭찬과 성취감 격려",
"또래 어르신과의 자연스러운 대화 환경 주선",
"가벼운 산책 및 개별 말벗 서비스 제공",
],
"resource": "지역 복지관 전문 정서지원 및 심리상담 멘토링 연계",
},
"보호자 요청·서비스 조정": {
"subjective": (
"보호자는 수급자의 상태 변화로 가정 내 돌봄 부담이 가중되어 이용"
" 시간 확대를 간곡히 요청함."
),
"objective": (
"수급자의 일상생활 자립도 저하가 관찰되며 보호자의 만성 피로와"
" 스트레스가 누적된 상태임."
),
"assessment": (
"가족의 돌봄 부담 경감을 위해 급여제공 시간 조정 및 외부 복지"
" 서비스 연계가 시급한 상황임."
),
"plan": [
"수급자 급여제공계획서 변경 및 이용시간 확대 검토",
"보호자 정기 상담을 통한 심리적 지지",
"가정 내 안전 돌봄 요령 및 복지용구 안내",
"종사자 간 상태변화 정보 신속 공유",
],
"resource": "지자체 긴급돌봄 및 치매가족 휴식지원 프로그램 안내",
},
}
def recommend_case_type(counseling_text, needs_text, condition_text):
text = f"{counseling_text} {needs_text} {condition_text}".lower()
scores = {k: 0 for k in CASE_TYPES}
kw_map = {
"낙상 고위험 관리": [
"낙상",
"어지러",
"넘어짐",
"보행",
"휠체어",
"이동",
"부축",
],
"식사량 감소·영양관리": [
"식사",
"식사량",
"영양",
"체중",
"섭취",
"연하",
"사레",
],
"인지저하·정서불안": [
"인지",
"치매",
"의심",
"망상",
"반복",
"불안",
"배회",
"기억",
],
"욕창·피부상태 관리": [
"욕창",
"피부",
"발적",
"기저귀",
"체위",
"와상",
"천골",
],
"프로그램 참여 저하": [
"프로그램",
"참여",
"무기력",
"우울",
"회피",
"거부",
],
"보호자 요청·서비스 조정": [
"보호자",
"시간",
"조정",
"부담",
"가족",
"요청",
"갈등",
],
}
for c_type, words in kw_map.items():
for w in words:
if w in text:
scores[c_type] += 1
best = max(scores, key=scores.get)
return best if scores[best] > 0 else "낙상 고위험 관리"
def evaluate_case(
selection,
first_content,
plan,
role_division,
implementation,
second_result,
):
required = {
"선정사유": bool(selection.strip()),
"1차 계획 및 목표": bool(plan.strip()) and "목표" in plan,
"직종별 역할분담": all(
w in role_division for w in ["시설장", "사회복지사", "요양보호사"]
),
"시행과정": bool(implementation.strip()),
"2차 결과평가": bool(second_result.strip()),
}
score = int(sum(required.values()) / len(required) * 100)
missing = [k for k, v in required.items() if not v]
if score >= 90:
res = (
"평가 적합도가 매우 높습니다. 1차 계획, 역할분담, 시행과정, 2차 결과가"
" 유기적으로 연결되었습니다."
)
else:
res = (
"보완이 필요합니다. 필수 항목(선정사유, 역할분담, 시행과정,"
" 결과평가)을 보완해 주세요."
)
return score, missing, res
# -----------------------------
# 엑셀 생성 엔진 (인메모리 전용)
# -----------------------------
def style_range(
ws,
min_row,
max_row,
min_col=1,
max_col=9,
fill=None,
font=None,
alignment=None,
):
thin = Side(style="thin", color="000000")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
for row in ws.iter_rows(
min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col
):
for cell in row:
cell.border = border
if fill:
cell.fill = fill
if font:
cell.font = font
if alignment:
cell.alignment = alignment
def write_header_and_meta(ws, title, data, is_second=False):
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.sheet_view.showGridLines = False
for col in range(1, 10):
ws.column_dimensions[get_column_letter(col)].width = 11.5
title_fill = PatternFill("solid", fgColor="EDE9FE")
header_fill = PatternFill("solid", fgColor="F3F4F6")
ws.merge_cells("A1:G2")
ws["A1"] = title
style_range(
ws,
1,
2,
1,
7,
fill=title_fill,
font=Font(name="맑은 고딕", size=20, bold=True),
alignment=Alignment(horizontal="center", vertical="center"),
)
ws["H1"] = "담당자"
ws["I1"] = "시설장"
style_range(
ws,
1,
1,
8,
9,
fill=header_fill,
font=Font(name="맑은 고딕", size=10, bold=True),
alignment=Alignment(horizontal="center", vertical="center"),
)
style_range(ws, 2, 2, 8, 9)
ws.row_dimensions[1].height = 24
ws.row_dimensions[2].height = 36
m_date = (
data["second_meeting_date"] if is_second else data["first_meeting_date"]
)
sub_date_lbl = "1차회의일" if is_second else "2차예정일"
sub_date_val = (
data["first_meeting_date"] if is_second else data["second_meeting_date"]
)
meta_rows = [
("기관명", data["institution_name"], "기관유형", data["institution_type"], "회의일자", m_date),
("수급자", data["client_masked_name"], "장기요양등급", data["grade"], "사례유형", data["case_type"]),
("인정번호", data.get("recognition_number", "-"), "인정기간", f"{data.get('recognition_start_date','')} ~ {data.get('recognition_end_date','')}", "기한알림", data.get("recognition_alert", "-")),
("회의장소", data["meeting_place"], "작성자", data["writer"], sub_date_lbl, sub_date_val),
]
r = 3
for row_data in meta_rows:
ws.cell(r, 1, row_data[0])
ws.merge_cells(start_row=r, start_column=2, end_row=r, end_column=3)
ws.cell(r, 2, row_data[1])
ws.cell(r, 4, row_data[2])
ws.merge_cells(start_row=r, start_column=5, end_row=r, end_column=6)
ws.cell(r, 5, row_data[3])
ws.cell(r, 7, row_data[4])
ws.merge_cells(start_row=r, start_column=8, end_row=r, end_column=9)
ws.cell(r, 8, row_data[5])
for c in [1, 4, 7]:
ws.cell(r, c).fill = header_fill
ws.cell(r, c).font = Font(name="맑은 고딕", size=9.5, bold=True)
ws.cell(r, c).alignment = Alignment(
horizontal="center", vertical="center"
)
for c in [2, 5, 8]:
ws.cell(r, c).font = Font(name="맑은 고딕", size=9.5)
ws.cell(r, c).alignment = Alignment(
horizontal="center", vertical="center"
)
style_range(ws, r, r, 1, 9)
ws.row_dimensions[r].height = 24
r += 1
return r
def write_sheet_sections(ws, r, sections, photos=None):
header_fill = PatternFill("solid", fgColor="F3F4F6")
for sec_title, content, min_h in sections:
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
ws.cell(r, 1, sec_title)
style_range(
ws,
r,
r,
1,
9,
fill=header_fill,
font=Font(name="맑은 고딕", size=11, bold=True),
alignment=Alignment(horizontal="center", vertical="center"),
)
ws.row_dimensions[r].height = 24
r += 1
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
ws.cell(r, 1, content)
style_range(
ws,
r,
r,
1,
9,
font=Font(name="맑은 고딕", size=9.5),
alignment=Alignment(
horizontal="left", vertical="top", wrap_text=True
),
)
ws.row_dimensions[r].height = min_h
r += 1
if photos:
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
ws.cell(r, 1, "사례관리 사진 첨부")
style_range(
ws,
r,
r,
1,
9,
fill=header_fill,
font=Font(name="맑은 고딕", size=11, bold=True),
alignment=Alignment(horizontal="center", vertical="center"),
)
ws.row_dimensions[r].height = 24
r += 1
anchors = ["A", "D", "G"]
for idx, img_file in enumerate(photos[:3]):
try:
pil_img = PILImage.open(img_file)
img_buf = BytesIO()
pil_img.save(img_buf, format="PNG")
img_buf.seek(0)
xl_img = XLImage(img_buf)
xl_img.width = 200
xl_img.height = 140
ws.add_image(xl_img, f"{anchors[idx]}{r}")
except Exception:
pass
style_range(ws, r, r + 6, 1, 9)
for rr in range(r, r + 7):
ws.row_dimensions[rr].height = 20
r += 7
# 하단 알림
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
ws.cell(
r,
1,
"※ 본 문서는 기관 운영방침 및 공단 평가기준에 따라 보완하여 사용할 수"
" 있습니다.",
)
style_range(
ws,
r,
r,
1,
9,
font=Font(name="맑은 고딕", size=8.5, color="777777", italic=True),
alignment=Alignment(horizontal="center", vertical="center"),
)
ws.row_dimensions[r].height = 24
ws.print_area = f"A1:I{r}"
def generate_excel_bytes(data, photos):
wb = Workbook()
# Sheet 1: 1차 계획회의록
ws1 = wb.active
ws1.title = "1차계획회의록"
r1 = write_header_and_meta(
ws1, "사례관리 1차 계획회의록", data, is_second=False
)
s1_sections = [
("사례관리 대상자 선정사유", data["selection_reason"], 60),
("상담일지 및 욕구사정 연동 요약", data["needs_summary"], 70),
("1차 회의 상세 기록", data["first_content"], 130),
(
"1차 사례관리 목표 및 계획",
f"[사례관리 목표]\n{data['goal']}\n\n[구체적 지원계획]\n{data['first_plan']}",
110,
),
("직종별 역할분담", data["role_division"], 90),
("참석자 명단", data["attendees_text"], 50),
]
write_sheet_sections(ws1, r1, s1_sections, photos)
# Sheet 2: 2차 결과회의록
ws2 = wb.create_sheet("2차결과회의록")
r2 = write_header_and_meta(
ws2, "사례관리 2차 결과회의록", data, is_second=True
)
s2_sections = [
("사례관리 대상자 선정사유", data["selection_reason"], 60),
(
"1차 목표 및 계획 재확인",
f"[목표] {data['goal']}\n[주요계획]\n{data['first_plan']}",
90,
),
("직종별 시행과정 및 개입내용", data["implementation_process"], 120),
("2차 회의결과 및 목표달성도 평가", data["second_result"], 120),
("참석자 명단", data["attendees_text"], 50),
]
write_sheet_sections(ws2, r2, s2_sections)
out = BytesIO()
wb.save(out)
out.seek(0)
return out
# -----------------------------
# Streamlit 화면 입력 구성
# -----------------------------
clients_df = load_clients()
st.sidebar.markdown("### ⚙️ 메뉴 설정")
menu_mode = st.sidebar.radio("작업 모드 선택", ["사례관리 회의록 작성", "저장기록 열람"])
if menu_mode == "저장기록 열람":
st.subheader("📚 최근 저장된 사례관리 기록")
conn = get_conn()
try:
recs = pd.read_sql_query(
"SELECT * FROM case_management_records ORDER BY id DESC LIMIT 20", conn
)
if recs.empty:
st.info("현재 저장된 사례관리 기록이 없습니다.")
else:
st.dataframe(recs, use_container_width=True)
except Exception as e:
st.error(f"기록 조회 오류: {e}")
conn.close()
else:
# 기본 정보 입력
c1, c2, c3 = st.columns([1.5, 1.2, 1.3])
inst_name = c1.text_input("기관명", "행복재가노인복지센터")
inst_type = c2.selectbox(
"기관유형",
["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"],
index=0,
)
client_name = c3.text_input("수급자 성명", "홍길동")
c4, c5, c6 = st.columns([1.5, 1.2, 1.3])
care_grade = c4.selectbox(
"장기요양등급",
["1등급", "2등급", "3등급", "4등급", "5등급", "인지지원등급"],
index=2,
)
rec_num = c5.text_input("장기요양 인정번호", "L1234567890")
rec_end = c6.date_input(
"인정기간 만료일", value=date.today() + timedelta(days=240)
)
c7, c8 = st.columns(2)
main_cond = c7.text_area(
"주요 질환 및 신체/인지 상태",
"초기 치매 증상, 무릎 관절염으로 인한 보행 불안정, 야간 화장실 이동 시 낙상 위험 높음.",
height=70,
)
needs_input = c8.text_area(
"상담 및 욕구사정 주요 내용",
"보호자는 야간 낙상 사고를 가장 우려함. 수급자는 기립 시 어지럼증을 호소하며 안전한 이동 보조를 희망함.",
height=70,
)
rec_type = recommend_case_type(needs_input, needs_input, main_cond)
col_m1, col_m2, col_m3 = st.columns([1.5, 1.2, 1.3])
case_types_list = list(CASE_TYPES.keys())
case_type = col_m1.selectbox(
"사례관리 유형",
case_types_list,
index=(
case_types_list.index(rec_type)
if rec_type in case_types_list
else 0
),
)
first_date = col_m2.date_input("1차 계획 회의일자", value=date.today())
second_date = col_m3.date_input(
"2차 결과 회의일자 (30일 후 권장)",
value=date.today() + timedelta(days=30),
)
col_m4, col_m5 = st.columns(2)
place = col_m4.text_input("회의장소", "기관 회의실")
writer_name = col_m5.text_input("작성자", "사회복지사")
# 상세 내용 자동완성 로직
details = CASE_DETAILS.get(case_type, CASE_DETAILS["낙상 고위험 관리"])
default_sel = (
f"{mask_name(client_name)} 수급자는 {care_grade} 대상자로,"
f" {CASE_TYPES[case_type]['reason']} 일상생활 안전과 맞춤형 급여 제공을"
f" 위해 {case_type} 사례관리 대상으로 선정함."
)
default_content = f"""[SOAP 구조화 기록 분석]
- [S 주관적 호소] {details['subjective']}
- [O 객관적 관찰] {details['objective']}
- [A 전문적 판단] {details['assessment']}
- [P 외부자원연계] {details['resource']}
[회의 종합 의견]
수급자의 안전 확보 및 상태 악화 방지를 위해 직종별 역할을 분담하여 집중 지원을 개시하기로 결정함."""
default_plan = "\n".join([f"- {p}" for p in details["plan"]])
default_roles = """시설장: 사례관리 계획 검토, 급여제공계획 변경 승인 및 외부자원 연계 총괄
사회복지사: 1·2차 회의 주관, 보호자 정기 상담, 맞춤형 개별이용계획 수립 및 모니터링
요양보호사: 일상 돌봄 시 개별 지원지침 이행, 수급자 특이사항 관찰 및 급여제공기록지 작성
간호(조무)사: 혈압·혈당 등 활력징후 측정, 복약 상태 및 전반적인 건강 모니터링""".strip()
default_impl = f"""[직종별 실행 내용]
- 요양보호사: 매 급여제공 시 {details['plan'][0]}을(를) 중점 시행하고 일상 기록지에 상세 기록함.
- 간호(조무)사: 주 2회 활력징후 점검 및 피부·영양 상태를 지속 관찰함.
- 사회복지사: 보호자와 주 1회 상담을 통해 수급자 상태 변화를 공유하고 피드백을 수렴함.
- 30일간 집중 모니터링을 차질 없이 수행함."""
default_second = f"""[목표달성도 평가]
- 평가 결과: 부분 달성 및 안전 상태 지속 유지
- 주요 성과: 사례관리 개입 기간 동안 낙상이나 상태 악화 없이 안정적인 일상생활을 유지함.
- 향후 계획: 수급자의 자립 잔존기능 유지를 위해 수립된 개별 지원방법을 정규 급여제공계획서에 반영하여 지속 지원하기로 함."""
st.markdown("### 📝 회의록 상세 내용 검토 및 수정")
col_t1, col_t2 = st.columns(2)
sel_reason = col_t1.text_area(
"1. 사례선정 사유", value=default_sel, height=95
)
goal_text = col_t2.text_area(
"2. 사례관리 목표", value=details.get("goal", default_sel), height=95
)
col_t3, col_t4 = st.columns(2)
first_cont = col_t3.text_area(
"3. 1차 회의 내용 (SOAP)", value=default_content, height=135
)
plan_text = col_t4.text_area(
"4. 구체적 지원계획", value=default_plan, height=135
)
col_t5, col_t6 = st.columns(2)
roles_text = col_t5.text_area(
"5. 직종별 역할분담", value=default_roles, height=115
)
impl_text = col_t6.text_area(
"6. 시행과정 (30일간 실행기록)", value=default_impl, height=115
)
second_text = st.text_area(
"7. 2차 회의결과 및 평가", value=default_second, height=100
)
attendees = st.text_input(
"참석자 명단",
"시설장 홍길동, 사회복지사 김복지, 요양보호사 이순신, 간호조무사 강감찬",
)
photo_files = st.file_uploader(
"📷 훈련/사례관리 사진 첨부 (최대 3장 / PNG, JPG)",
type=["png", "jpg", "jpeg"],
accept_multiple_files=True,
)
# 데이터 통합
data_payload = {
"client_id": 0,
"client_masked_name": mask_name(client_name),
"institution_name": inst_name,
"institution_type": inst_type,
"grade": care_grade,
"recognition_number": rec_num,
"recognition_start_date": str(date.today() - timedelta(days=365)),
"recognition_end_date": str(rec_end),
"recognition_alert": recognition_alert_text(str(rec_end)),
"case_type": case_type,
"first_meeting_date": str(first_date),
"second_meeting_date": str(second_date),
"meeting_place": place,
"writer": writer_name,
"selection_reason": sel_reason,
"first_content": first_cont,
"goal": goal_text,
"first_plan": plan_text,
"role_division": roles_text,
"implementation_process": impl_text,
"second_result": second_text,
"needs_summary": f"{needs_input}\n[주요상태] {main_cond}",
"attendees_text": attendees,
}
score, missing, compliance = evaluate_case(
sel_reason, first_cont, plan_text, roles_text, impl_text, second_text
)
data_payload["evaluation_score"] = score
data_payload["missing_items"] = ", ".join(missing) if missing else "없음"
st.markdown("---")
ev1, ev2, ev3 = st.columns(3)
ev1.metric("공단 평가 적합도", f"{score}점")
ev2.metric("보완 필요 항목", data_payload["missing_items"])
ev3.metric("첨부 사진 수", f"{len(photo_files) if photo_files else 0}장")
if score >= 90:
st.markdown(f'{compliance}
', unsafe_allow_html=True)
else:
st.markdown(
f'{compliance}
', unsafe_allow_html=True
)
col_b1, col_b2 = st.columns([1, 1])
with col_b1:
if st.button("💾 사례관리 기록 DB 저장", use_container_width=True):
save_case_record(data_payload)
st.success("데이터베이스에 안전하게 저장되었습니다.")
excel_file = generate_excel_bytes(data_payload, photo_files or [])
file_name_out = (
f"사례관리회의록_{mask_name(client_name)}_{first_date}.xlsx"
)
with col_b2:
st.download_button(
label="📥 사례관리 회의록 엑셀 다운로드 (.xlsx)",
data=excel_file,
file_name=file_name_out,
mime=(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
),
use_container_width=True,
type="primary",
)