devorah-ai-2016 commited on
Commit
d13cc47
ยท
verified ยท
1 Parent(s): c9f66fd

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +571 -0
app.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ from io import BytesIO
4
+ from datetime import date, datetime
5
+
6
+ import pandas as pd
7
+ import streamlit as st
8
+ from openpyxl import Workbook
9
+ from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
10
+ from openpyxl.utils import get_column_letter
11
+
12
+
13
+ st.set_page_config(page_title="์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€", page_icon="๐Ÿ“ˆ", layout="wide")
14
+
15
+ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
16
+ DB_CANDIDATES = [
17
+ os.path.join(CURRENT_DIR, "data", "clients.db"),
18
+ os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"),
19
+ os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"),
20
+ os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"),
21
+ ]
22
+
23
+ DB_PATH = None
24
+ for candidate in DB_CANDIDATES:
25
+ if os.path.exists(candidate):
26
+ DB_PATH = candidate
27
+ break
28
+
29
+
30
+ st.markdown(
31
+ """
32
+ <style>
33
+ .block-container { padding-top: 1.6rem; max-width: 1160px; }
34
+ .hero { background: linear-gradient(135deg,#2563EB,#60A5FA); padding:32px; border-radius:24px; text-align:center; margin-bottom:20px; }
35
+ .hero h1 { color:white; margin:0; font-size:42px; font-weight:800; }
36
+ .hero p { color:white; margin:14px 0 0 0; font-size:19px; line-height:1.55; }
37
+ .notice { background:#FFF8E1; border-left:7px solid #F59E0B; padding:15px 18px; border-radius:14px; margin:16px 0; line-height:1.7; }
38
+ .okbox { background:#E8F5E9; border-left:7px solid #43A047; padding:14px; border-radius:12px; margin:10px 0; }
39
+ .warnbox { background:#FFF3E0; border-left:7px solid #FB8C00; padding:14px; border-radius:12px; margin:10px 0; }
40
+ </style>
41
+ """,
42
+ unsafe_allow_html=True,
43
+ )
44
+
45
+ st.markdown(
46
+ """
47
+ <div class="hero">
48
+ <h1>๐Ÿ“ˆ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์ƒ์„ฑ๊ธฐ</h1>
49
+ <p>Client Center ์ •๋ณด์™€ ์ง์ ‘ ์ž…๋ ฅ ๋ฉ”๋ชจ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ 5๋Œ€ ์˜์—ญ, SOAP, ์œ„ํ—˜ํŒ์ •, ์—ฐ๊ณ„๊ณ„ํš์„ ์ž๋™ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.</p>
50
+ </div>
51
+ """,
52
+ unsafe_allow_html=True,
53
+ )
54
+
55
+ st.markdown(
56
+ """
57
+ <div class="notice">
58
+ <strong>์ž‘์„ฑ ์›์น™:</strong> ์ƒํƒœ๋ณ€ํ™”๊ฐ€ ์—†๋‹ค๋Š” ๋ฐ˜๋ณต ๋ฌธ๊ตฌ๋ฅผ ํ”ผํ•˜๊ณ , ์‹ ์ฒดยท์ธ์ง€ยท๊ฐ์ •ยท๊ด€๊ณ„ยท์ง€์—ญ์ž์›์—ฐ๊ณ„ ์˜์—ญ์„ ๊ธฐ์ค€์œผ๋กœ ํ‰๊ฐ€ ์ ํ•ฉ ๋ฌธ์ฒด๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
59
+ </div>
60
+ """,
61
+ unsafe_allow_html=True,
62
+ )
63
+
64
+ st.markdown(
65
+ """
66
+ <div class="notice">
67
+ โ€ป ์ƒ์„ฑ๋œ ๋ฌธ์„œ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์…”๋„ ๋˜๋ฉฐ, ๊ธฐ๊ด€์˜ ์šด์˜๋ฐฉ์นจ, ์ˆ˜๊ธ‰์ž ์ƒํƒœ, ๋ณดํ˜ธ์ž ์˜๊ฒฌ ๋ฐ ํ‰๊ฐ€๊ธฐ์ค€์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ๋ณด์™„์‚ฌํ•ญ์€ ์–ผ๋งˆ๋“ ์ง€ ์ˆ˜์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๊ด€์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ํ™œ์šฉํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.
68
+ </div>
69
+ """,
70
+ unsafe_allow_html=True,
71
+ )
72
+
73
+
74
+ RESTRICTED_REPLACEMENTS = {
75
+ "์ƒํƒœ ์–‘ํ˜ธ": "์‹ ์ฒดยท์ธ์ง€ยท์ •์„œ ์ƒํƒœ๊ฐ€ ๋น„๊ต์  ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜๋‚˜ ์ •๊ธฐ ๊ด€์ฐฐ์ด ์š”๊ตฌ๋จ",
76
+ "ํŠน์ด์‚ฌํ•ญ ์—†์Œ": "์ฆ‰์‹œ ๊ฐœ์ž…์ด ํ•„์š”ํ•œ ๋Œ๋ฐœ ์ƒํ™ฉ์€ ํ™•์ธ๋˜์ง€ ์•Š์•˜์œผ๋‚˜ ์ƒํƒœ๋ณ€ํ™” ๊ด€์ฐฐ์„ ์ง€์†ํ•จ",
77
+ "์ด์ „๊ณผ ๋™์ผ": "๊ธฐ์กด ์ƒํƒœ์˜ ๊ธ‰๊ฒฉํ•œ ์•…ํ™”๋Š” ์ œํ•œ์ ์ด๋‚˜ ๊ธฐ๋Šฅ ์œ ์ง€ ์—ฌ๋ถ€๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ํ™•์ธํ•จ",
78
+ "๋ฌธ์ œ์—†์Œ": "ํ˜„์žฌ ํ™•์ธ๋œ ์œ„ํ—˜์š”์ธ์€ ์ œํ•œ์ ์ด๋‚˜ ๊ธ‰์—ฌ์ œ๊ณต ๊ณผ์ •์—์„œ ์žฌํ™•์ธ์ด ํ•„์š”ํ•จ",
79
+ "๋ฌธ์ œ ์—†์Œ": "ํ˜„์žฌ ํ™•์ธ๋œ ์œ„ํ—˜์š”์ธ์€ ์ œํ•œ์ ์ด๋‚˜ ๊ธ‰์—ฌ์ œ๊ณต ๊ณผ์ •์—์„œ ์žฌํ™•์ธ์ด ํ•„์š”ํ•จ",
80
+ }
81
+
82
+ DOMAIN_KEYWORDS = {
83
+ "์‹ ์ฒด": ["ํ†ต์ฆ", "๋‚™์ƒ", "์–ด์ง€", "ํœ˜์ฒญ", "๋ณดํ–‰", "์‹์‚ฌ", "์‹์š•", "์ฒด์ค‘", "์‚ฌ๋ ˆ", "์—ฐํ•˜", "๋ฐฐ๋ณ€", "๋ฐฐ๋‡จ", "์‹ค๊ธˆ", "๊ธฐ์ €๊ท€", "์š•์ฐฝ", "๋ฐœ์ ", "์ƒ์ฒ˜", "ํ”ผ๋ถ€", "์ˆ˜๋ฉด", "์ž…์›", "ํ‡ด์›"],
84
+ "์ธ์ง€": ["์น˜๋งค", "์ธ์ง€", "๊ธฐ์–ต", "๋ฐ˜๋ณต", "ํ˜ผ๋™", "์„ฌ๋ง", "์ง€๋‚จ๋ ฅ", "๋ฐฐํšŒ", "๋ง์ƒ", "๋„๋‘‘", "ํ™˜๊ฐ", "๊ท€๊ฐ€", "์ง‘์—"],
85
+ "๊ฐ์ •": ["์šฐ์šธ", "๋ˆˆ๋ฌผ", "๋ฌด๊ธฐ๋ ฅ", "๋ถˆ์•ˆ", "์ดˆ์กฐ", "์งœ์ฆ", "ํ™”", "๊ฑฐ๋ถ€", "์‚ด๊ธฐ ์‹ซ", "๊ท€์ฐฎ"],
86
+ "๊ด€๊ณ„": ["๋‹คํˆผ", "๊ฐˆ๋“ฑ", "ํญ์–ธ", "๊ณต๊ฒฉ", "๋ณดํ˜ธ์ž", "๊ฐ€์กฑ", "์ข…์‚ฌ์ž", "ํƒ€ ์ˆ˜๊ธ‰์ž", "๋ง๋‹คํˆผ", "๊ฑฐ๋ถ€"],
87
+ "์ง€์—ญ์ž์›์—ฐ๊ณ„": ["๋…๊ฑฐ", "๊ฒฐ์‹", "๊ฒฝ์ œ", "์ฃผ๊ฑฐ", "๋ณต์•ฝ", "๋ณ‘์›", "์ง„๋ฃŒ", "๋ณด๊ฑด์†Œ", "๋ณต์ง€๊ด€", "ํ–‰์ •๋ณต์ง€์„ผํ„ฐ", "์น˜๋งค์•ˆ์‹ฌ์„ผํ„ฐ", "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ"],
88
+ }
89
+
90
+ BPSD_KEYWORDS = ["๋„๋‘‘", "๋ง์ƒ", "์„์–‘", "๋ฐฐํšŒ", "๊ณต๊ฒฉ", "ํญ์–ธ", "์•ผ๊ฐ„์„ฌ๋ง", "ํ™˜๊ฐ", "๊ท€๊ฐ€", "์ง‘์—", "๊ฑฐ๋ถ€"]
91
+ DEPRESSION_KEYWORDS = ["์šฐ์šธ", "๋ˆˆ๋ฌผ", "์‹์‚ฌ๊ฑฐ๋ถ€", "๋ฌด๊ธฐ๋ ฅ", "์‚ด๊ธฐ ์‹ซ", "๊ท€์ฐฎ", "์™ธ๋กœ"]
92
+ COMMUNITY_KEYWORDS = ["๋ณต์•ฝ", "๊ฒฐ์‹", "๋…๊ฑฐ", "์ฃผ๊ฑฐ", "๊ฒฝ์ œ", "๋ณ‘์›", "์น˜๋งค", "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ"]
93
+ HIGH_RISK_KEYWORDS = ["์‘๊ธ‰", "์ž…์›", "๋‚™์ƒ", "์š•์ฐฝ", "์ƒ์ฒ˜", "๊ฐ์—ผ", "๊ณต๊ฒฉ", "๋ฐฐํšŒ", "๋ง์ƒ", "์‹์‚ฌ๊ฑฐ๋ถ€", "์žํ•ด", "ํ•™๋Œ€", "๋ฐฉ์ž„"]
94
+ WARNING_KEYWORDS = ["์–ด์ง€", "ํ†ต์ฆ", "์‹์‚ฌ๋Ÿ‰", "์‹ค๊ธˆ", "๋ฐ˜๋ณต", "๋ถˆ์•ˆ", "์šฐ์šธ", "๊ฐˆ๋“ฑ", "๊ฑฐ๋ถ€", "๋ฐœ์ ", "๋ณดํ–‰"]
95
+
96
+
97
+ def normalize_text(text):
98
+ value = str(text or "").strip()
99
+ for bad, good in RESTRICTED_REPLACEMENTS.items():
100
+ value = value.replace(bad, good)
101
+ return value
102
+
103
+
104
+ def has_keyword(text, keywords):
105
+ source = str(text or "")
106
+ return any(keyword in source for keyword in keywords)
107
+
108
+
109
+ def pick(data, *keys, default=""):
110
+ for key in keys:
111
+ value = data.get(key, "")
112
+ if value is None:
113
+ continue
114
+ value = str(value).strip()
115
+ if value:
116
+ return value
117
+ return default
118
+
119
+
120
+ def formal_sentence(text, fallback="์ง€์† ๊ด€์ฐฐ์ด ์š”๊ตฌ๋จ"):
121
+ value = normalize_text(text)
122
+ if not value:
123
+ value = fallback
124
+ if value.endswith(("ํ™•์ธ๋จ", "๊ด€์ฐฐ๋จ", "์‚ฌ์ •๋จ", "ํŒ๋‹จ๋จ", "๊ณ„ํšํ•จ", "์‹ค์‹œํ•จ", "์š”๊ตฌ๋จ", "ํ•„์š”ํ•จ", "์žˆ์Œ", "์—†์Œ", "๋จ", "ํ•จ")):
125
+ return value
126
+ last = value[-1]
127
+ if "๊ฐ€" <= last <= "ํžฃ" and (ord(last) - ord("๊ฐ€")) % 28:
128
+ return f"{value}์œผ๋กœ ํ™•์ธ๋จ"
129
+ return f"{value}๋กœ ํ™•์ธ๋จ"
130
+
131
+
132
+ def load_clients():
133
+ if not DB_PATH:
134
+ return pd.DataFrame()
135
+ try:
136
+ conn = sqlite3.connect(DB_PATH)
137
+ cols = pd.read_sql_query("PRAGMA table_info(clients)", conn)["name"].tolist()
138
+ wanted = [
139
+ "id", "name", "masked_name", "gender", "birth_year", "care_grade",
140
+ "recognition_no", "recognition_number", "recognition_start_date", "recognition_end_date",
141
+ "institution_type", "institution_name", "main_condition", "cognitive_status",
142
+ "mobility_status", "diet_status", "toileting_status", "guardian_name", "guardian_phone",
143
+ "notes", "status",
144
+ ]
145
+ select_cols = [c for c in wanted if c in cols]
146
+ if not select_cols:
147
+ conn.close()
148
+ return pd.DataFrame()
149
+ df = pd.read_sql_query("SELECT " + ", ".join(select_cols) + " FROM clients ORDER BY id DESC LIMIT 300", conn)
150
+ conn.close()
151
+ for col in wanted:
152
+ if col not in df.columns:
153
+ df[col] = ""
154
+ df["recognition_no"] = df["recognition_no"].where(df["recognition_no"].astype(str).str.strip() != "", df["recognition_number"])
155
+ return df
156
+ except Exception:
157
+ return pd.DataFrame()
158
+
159
+
160
+ def safe(row, key, default=""):
161
+ try:
162
+ value = row.get(key, default)
163
+ if pd.isna(value):
164
+ return default
165
+ return str(value)
166
+ except Exception:
167
+ return default
168
+
169
+
170
+ def mask_name(name):
171
+ name = str(name or "").strip()
172
+ if len(name) >= 3:
173
+ return name[0] + "โ—‹" + name[2:]
174
+ if len(name) == 2:
175
+ return name[0] + "โ—‹"
176
+ return name or "๋ฏธ์ž…๋ ฅ"
177
+
178
+
179
+ def determine_plan_change(analysis):
180
+ if analysis["risk_level"] == "๊ณ ์œ„ํ—˜(์•…ํ™”)":
181
+ return "๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ๋ณ€๊ฒฝ ๊ฒ€ํ†  ํ•„์š”ํ•จ"
182
+ if analysis["risk_level"] == "๊ฒฝ๊ณ (์ฃผ์˜)" and (analysis["bpsd"] or analysis["community_needed"]):
183
+ return "๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ๋ถ€๋ถ„ ์กฐ์ • ๊ฒ€ํ†  ํ•„์š”ํ•จ"
184
+ return "ํ˜„ ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ์œ ์ง€ ๊ฐ€๋Šฅํ•˜๋‚˜ ๋‹ค์Œ ์ œ๊ณต ์‹œ ์žฌํ™•์ธ ํ•„์š”ํ•จ"
185
+
186
+
187
+ def analyze_status_change(data):
188
+ combined = " ".join([
189
+ pick(data, "main_condition", "์ฃผ์š”์ƒํƒœ", "์ฃผ์š” ์งˆํ™˜ / ๊ธฐ์กด ์ƒํƒœ"),
190
+ pick(data, "physical_note", "์‹ ์ฒด์ƒํƒœ", "์‹ ์ฒด์˜์—ญ ๋ณ€ํ™”"),
191
+ pick(data, "cognitive_note", "์ธ์ง€์ƒํƒœ", "์ธ์ง€์˜์—ญ ๋ณ€ํ™”"),
192
+ pick(data, "emotion_note", "๊ฐ์ •์ƒํƒœ", "๊ฐ์ •์˜์—ญ ๋ณ€ํ™”"),
193
+ pick(data, "relation_note", "๊ด€๊ณ„์ƒํƒœ", "๊ด€๊ณ„์˜์—ญ ๋ณ€ํ™”"),
194
+ pick(data, "resource_note", "์ง€์—ญ์ž์›", "์ง€์—ญ์ž์›์—ฐ๊ณ„", "์ง€์—ญ์ž์›์—ฐ๊ณ„ ํ•„์š”"),
195
+ pick(data, "field_memo", "ํ˜„์žฅ๋ฉ”๋ชจ", "ํ˜„์žฅ ๋‹จ์ˆœ ๋ฉ”๋ชจ"),
196
+ pick(data, "guardian_opinion", "๋ณดํ˜ธ์ž์˜๊ฒฌ", "๋ณดํ˜ธ์ž ์˜๊ฒฌ"),
197
+ ])
198
+
199
+ domain_results = {}
200
+ for domain, keywords in DOMAIN_KEYWORDS.items():
201
+ if has_keyword(combined, keywords):
202
+ domain_results[domain] = True
203
+
204
+ bpsd = has_keyword(combined, BPSD_KEYWORDS)
205
+ depression = has_keyword(combined, DEPRESSION_KEYWORDS)
206
+ community_needed = has_keyword(combined, COMMUNITY_KEYWORDS)
207
+
208
+ high = has_keyword(combined, HIGH_RISK_KEYWORDS) and has_keyword(combined, ["์•…ํ™”", "๋ฐ˜๋ณต", "์ง€์†", "์‘๊ธ‰", "์ž…์›", "๊ฐ์—ผ", "๊ณต๊ฒฉ", "๋ฐฐํšŒ", "์‹์‚ฌ๊ฑฐ๋ถ€"])
209
+ if high or (bpsd and has_keyword(combined, ["๊ณต๊ฒฉ", "๋ฐฐํšŒ", "๋ง์ƒ", "์•ผ๊ฐ„"])):
210
+ risk_level = "๊ณ ์œ„ํ—˜(์•…ํ™”)"
211
+ elif has_keyword(combined, WARNING_KEYWORDS) or bpsd or depression or community_needed:
212
+ risk_level = "๊ฒฝ๊ณ (์ฃผ์˜)"
213
+ else:
214
+ risk_level = "์•ˆ์ •(์œ ์ง€)"
215
+
216
+ flags = {
217
+ "์š•๊ตฌ์‚ฌ์ • ์žฌ์‹ค์‹œ ํ•„์š”": risk_level == "๊ณ ์œ„ํ—˜(์•…ํ™”)" or depression,
218
+ "์‚ฌ๋ก€๊ด€๋ฆฌ ์—ฐ๊ณ„ ํ•„์š”": risk_level == "๊ณ ์œ„ํ—˜(์•…ํ™”)" or bpsd,
219
+ "๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ๋ณ€๊ฒฝ ํ•„์š”": risk_level != "์•ˆ์ •(์œ ์ง€)",
220
+ "๋ณดํ˜ธ์ž ์ƒ๋‹ด ํ•„์š”": bool(pick(data, "guardian_opinion", "๋ณดํ˜ธ์ž์˜๊ฒฌ", "๋ณดํ˜ธ์ž ์˜๊ฒฌ")) or risk_level != "์•ˆ์ •(์œ ์ง€)",
221
+ }
222
+ return {
223
+ "combined": combined,
224
+ "domains": domain_results,
225
+ "bpsd": bpsd,
226
+ "depression": depression,
227
+ "community_needed": community_needed,
228
+ "risk_level": risk_level,
229
+ "flags": flags,
230
+ "plan_change": "",
231
+ }
232
+
233
+
234
+ def build_community_plan(analysis):
235
+ if not analysis["community_needed"]:
236
+ return "์ง€์—ญ์ž์› ์—ฐ๊ณ„๋Š” ์ฆ‰์‹œ ํ•„์š”์„ฑ์ด ๋‚ฎ์œผ๋‚˜ ๋…๊ฑฐ, ๊ฒฐ์‹, ๋ณต์•ฝ ๋ถˆ์ดํ–‰, ์ฃผ๊ฑฐํ™˜๊ฒฝ ์œ„ํ—˜ ๋ฐœ์ƒ ์‹œ ์žฌ๊ฒ€ํ†  ๊ณ„ํšํ•จ"
237
+ return "๋ณด๊ฑด์†Œ, ์น˜๋งค์•ˆ์‹ฌ์„ผํ„ฐ, ํ–‰์ •๋ณต์ง€์„ผํ„ฐ, ๋ณต์ง€๊ด€, ๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ ๋“ฑ ์ง€์—ญ์ž์› ์—ฐ๊ณ„๋ฅผ ๊ฒ€ํ† ํ•˜๊ณ  ๋ณดํ˜ธ์ž ๋ฐ ๋‹ด๋‹น์ž์™€ ๊ณต์œ  ๊ณ„ํšํ•จ"
238
+
239
+
240
+ def build_status_text(data):
241
+ analysis = analyze_status_change(data)
242
+ analysis["plan_change"] = determine_plan_change(analysis)
243
+
244
+ domain_lines = []
245
+ labels = {
246
+ "์‹ ์ฒด": pick(data, "physical_note", "์‹ ์ฒด์ƒํƒœ", "์‹ ์ฒด์˜์—ญ ๋ณ€ํ™”"),
247
+ "์ธ์ง€": pick(data, "cognitive_note", "์ธ์ง€์ƒํƒœ", "์ธ์ง€์˜์—ญ ๋ณ€ํ™”"),
248
+ "๊ฐ์ •": pick(data, "emotion_note", "๊ฐ์ •์ƒํƒœ", "๊ฐ์ •์˜์—ญ ๋ณ€ํ™”"),
249
+ "๊ด€๊ณ„": pick(data, "relation_note", "๊ด€๊ณ„์ƒํƒœ", "๊ด€๊ณ„์˜์—ญ ๋ณ€ํ™”"),
250
+ "์ง€์—ญ์ž์›์—ฐ๊ณ„": pick(data, "resource_note", "์ง€์—ญ์ž์›", "์ง€์—ญ์ž์›์—ฐ๊ณ„", "์ง€์—ญ์ž์›์—ฐ๊ณ„ ํ•„์š”"),
251
+ }
252
+ for domain, note in labels.items():
253
+ if analysis["domains"].get(domain) or note:
254
+ domain_lines.append(f"- {domain}์˜์—ญ: {formal_sentence(note, '๊ด€๋ จ ๋ณ€ํ™” ๊ฐ€๋Šฅ์„ฑ์ด ํ™•์ธ๋˜์–ด ๊ด€์ฐฐ์ด ์š”๊ตฌ๋จ')}")
255
+ if not domain_lines:
256
+ domain_lines.append("- ์‹ ์ฒดยท์ธ์ง€ยท์ •์„œ ์ „๋ฐ˜: ๊ธ‰๊ฒฉํ•œ ์•…ํ™” ๋‹จ์„œ๋Š” ์ œํ•œ์ ์ด๋‚˜ ์ •๊ธฐ ๊ด€์ฐฐ์ด ์š”๊ตฌ๋จ")
257
+
258
+ bpsd_text = "BPSD ๊ด€๋ จ ๋‹จ์„œ๊ฐ€ ํ™•์ธ๋˜์–ด ์ •๋ฉด ๋ฐ˜๋ฐ•์„ ์ง€์–‘ํ•˜๊ณ  ๊ฐ์ • ๊ณต๊ฐ, ์งง์€ ์„ค๋ช…, ํ™”์ œ ์ „ํ™˜, ์ •์„œ์•ˆ์ • ์ง€์›์„ ์‹ค์‹œํ•จ" if analysis["bpsd"] else "BPSD ๊ณ ์œ„ํ—˜ ๋‹จ์„œ๋Š” ์ œํ•œ์ ์ด๋‚˜ ์ธ์ง€ยท์ •์„œ ๋ณ€ํ™” ์—ฌ๋ถ€๋ฅผ ์ง€์† ๊ด€์ฐฐํ•จ"
259
+ depression_text = "์šฐ์šธ ์œ„ํ—˜ ๋‹จ์„œ๊ฐ€ ํ™•์ธ๋˜์–ด ์‚ฌํšŒ์  ๊ณ ๋ฆฝ๊ฐ, ์‹์‚ฌ๊ฑฐ๋ถ€, ๋ฌด๊ธฐ๋ ฅ ์ง€์† ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ  ์ •์„œ์ง€์› ๊ฐ•ํ™”๊ฐ€ ์š”๊ตฌ๋จ" if analysis["depression"] else "์šฐ์šธ ๊ณ ์œ„ํ—˜ ๋‹จ์„œ๋Š” ์ œํ•œ์ ์ด๋‚˜ ์ •์„œํ‘œํ˜„ ๋ณ€ํ™” ์—ฌ๋ถ€๋ฅผ ๊ด€์ฐฐํ•จ"
260
+ community_text = build_community_plan(analysis)
261
+ flag_text = " / ".join([f"โ–ก {name}" for name, enabled in analysis["flags"].items() if enabled]) or "โ–ก ์ •๊ธฐ ๊ด€์ฐฐ ์œ ์ง€"
262
+
263
+ soap = f"""
264
+ [SOAP ๊ตฌ์กฐ ๊ธฐ๋ก]
265
+ S(์ฃผ๊ด€์  ์ •๋ณด): {formal_sentence(pick(data, 'guardian_opinion', '๋ณดํ˜ธ์ž์˜๊ฒฌ', '๋ณดํ˜ธ์ž ์˜๊ฒฌ') or pick(data, 'field_memo', 'ํ˜„์žฅ๋ฉ”๋ชจ', 'ํ˜„์žฅ ๋‹จ์ˆœ ๋ฉ”๋ชจ'), '์ˆ˜๊ธ‰์ž ๋ฐ ๋ณดํ˜ธ์ž ํ˜ธ์†Œ๋Š” ์ถ”๊ฐ€ ํ™•์ธ ์˜ˆ์ •์ž„')}
266
+ O(๊ฐ๊ด€์  ๊ด€์ฐฐ): {formal_sentence(pick(data, 'field_memo', 'ํ˜„์žฅ๋ฉ”๋ชจ', 'ํ˜„์žฅ ๋‹จ์ˆœ ๋ฉ”๋ชจ'), '์„œ๋น„์Šค ์ œ๊ณต ์ค‘ ํ–‰๋™, ํ‘œ์ •, ์ด๋™, ์‹์‚ฌ, ๋ฐฐ๋ณ€ ๋“ฑ ์ƒํƒœ ๊ด€์ฐฐ์ด ์š”๊ตฌ๋จ')}
267
+ A(์ „๋ฌธ์  ์ƒํƒœ๋ถ„์„): ์ž๋™ํŒ์ • ๊ฒฐ๊ณผ {analysis['risk_level']} ๋‹จ๊ณ„๋กœ ์‚ฌ์ •๋จ. {bpsd_text} {depression_text}
268
+ P(์กฐ์น˜ ๋ฐ ๊ณ„ํš): {analysis['plan_change']} {community_text}
269
+ """.strip()
270
+
271
+ content = f"""
272
+ [์ƒํƒœ๋ณ€ํ™” ์ž๋™ํŒ์ •]
273
+ ํŒ์ •: {analysis['risk_level']}
274
+ ๊ทผ๊ฑฐ: ์ž…๋ ฅ๋œ ํ˜„์žฅ ๋ฉ”๋ชจ, ๋ณดํ˜ธ์ž ์˜๊ฒฌ, ์ˆ˜๊ธ‰์ž ๊ธฐ๋ณธ์ƒํƒœ๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ์‹ ์ฒดยท์ธ์ง€ยท๊ฐ์ •ยท๊ด€๊ณ„ยท์ง€์—ญ์ž์›์—ฐ๊ณ„ ์˜์—ญ์œผ๋กœ ๋ถ„์„ํ•จ.
275
+
276
+ [5๋Œ€ ์˜์—ญ ๋ถ„์„]
277
+ {chr(10).join(domain_lines)}
278
+
279
+ [BPSD ๋ฐ ์šฐ์šธ ์œ„ํ—˜ ๋ถ„์„]
280
+ - ์น˜๋งคํ–‰๋™์‹ฌ๋ฆฌ์ฆ์ƒ: {bpsd_text}
281
+ - ์šฐ์šธ ์œ„ํ—˜: {depression_text}
282
+
283
+ [์ง€์—ญ์ž์› ์—ฐ๊ณ„๊ณ„ํš]
284
+ {community_text}
285
+
286
+ {soap}
287
+
288
+ [ํ‰๊ฐ€ ์—ฐ๊ณ„]
289
+ {flag_text}
290
+ ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ํŒ๋‹จ: {analysis['plan_change']}
291
+ """.strip()
292
+ return normalize_text(content), analysis
293
+
294
+
295
+ def evaluate_record(content, analysis, photo_count):
296
+ required = {
297
+ "์ž๋™ํŒ์ •": "ํŒ์ •:" in content,
298
+ "5๋Œ€ ์˜์—ญ": "5๋Œ€ ์˜์—ญ ๋ถ„์„" in content,
299
+ "SOAP": "SOAP ๊ตฌ์กฐ ๊ธฐ๋ก" in content,
300
+ "BPSD": "BPSD" in content,
301
+ "์ง€์—ญ์ž์›": "์ง€์—ญ์ž์›" in content,
302
+ "ํ‰๊ฐ€ ์—ฐ๊ณ„": "ํ‰๊ฐ€ ์—ฐ๊ณ„" in content,
303
+ "์‚ฌ์ง„์ฒจ๋ถ€ ํ™•์ธ": photo_count >= 0,
304
+ }
305
+ score = int(sum(required.values()) / len(required) * 100)
306
+ missing = [k for k, v in required.items() if not v]
307
+ if score >= 90:
308
+ comment = "ํ‰๊ฐ€ ์ ํ•ฉ๋„๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค. ์ƒํƒœํŒ์ •, ์˜์—ญ๋ถ„์„, SOAP, ํ›„์†์กฐ์น˜๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค."
309
+ elif score >= 75:
310
+ comment = "๋Œ€์ฒด๋กœ ์ ํ•ฉํ•˜๋‚˜ ์ผ๋ถ€ ๋ณด์™„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."
311
+ else:
312
+ comment = "๋ณด์™„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ๋ณ€ํ™” ๊ทผ๊ฑฐ์™€ ํ›„์†์กฐ์น˜๋ฅผ ๋” ๊ตฌ์ฒดํ™”ํ•ด ์ฃผ์„ธ์š”."
313
+ return score, missing, comment
314
+
315
+
316
+ def style_range(ws, cell_range, fill=None, font=None, alignment=None):
317
+ thin = Side(style="thin", color="000000")
318
+ border = Border(left=thin, right=thin, top=thin, bottom=thin)
319
+ for row in ws[cell_range]:
320
+ for cell in row:
321
+ cell.border = border
322
+ if fill:
323
+ cell.fill = fill
324
+ if font:
325
+ cell.font = font
326
+ if alignment:
327
+ cell.alignment = alignment
328
+
329
+
330
+ def generate_excel(data):
331
+ wb = Workbook()
332
+ ws = wb.active
333
+ ws.title = "์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€"
334
+ ws.sheet_view.showGridLines = False
335
+ ws.page_setup.orientation = "portrait"
336
+ ws.page_setup.fitToWidth = 1
337
+ ws.page_setup.fitToHeight = 0
338
+ ws.sheet_properties.pageSetUpPr.fitToPage = True
339
+ ws.page_margins.left = 0.3
340
+ ws.page_margins.right = 0.3
341
+ ws.page_margins.top = 0.4
342
+ ws.page_margins.bottom = 0.4
343
+
344
+ title_fill = PatternFill("solid", fgColor="DBEAFE")
345
+ header_fill = PatternFill("solid", fgColor="F3F4F6")
346
+ for col in range(1, 10):
347
+ ws.column_dimensions[get_column_letter(col)].width = 12
348
+
349
+ ws.merge_cells("A1:G2")
350
+ ws["A1"] = "์ƒ ํƒœ ๋ณ€ ํ™” ๊ธฐ ๋ก ์ง€"
351
+ ws["A1"].font = Font(name="Malgun Gothic", size=24, bold=True)
352
+ ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
353
+ ws["A1"].fill = title_fill
354
+ ws["H1"] = "๋‹ด๋‹น์ž"
355
+ ws["I1"] = "์‹œ์„ค์žฅ"
356
+ ws["H2"] = ""
357
+ ws["I2"] = ""
358
+ for cell in ["H1", "I1"]:
359
+ ws[cell].fill = header_fill
360
+ ws[cell].font = Font(name="Malgun Gothic", size=11, bold=True)
361
+ ws[cell].alignment = Alignment(horizontal="center", vertical="center")
362
+ style_range(ws, "A1:I2")
363
+ ws.row_dimensions[1].height = 28
364
+ ws.row_dimensions[2].height = 45
365
+
366
+ def info_row(row, a, b, c, d, e, f):
367
+ ws.cell(row, 1).value = a
368
+ ws.merge_cells(start_row=row, start_column=2, end_row=row, end_column=3)
369
+ ws.cell(row, 2).value = b
370
+ ws.cell(row, 4).value = c
371
+ ws.merge_cells(start_row=row, start_column=5, end_row=row, end_column=6)
372
+ ws.cell(row, 5).value = d
373
+ ws.cell(row, 7).value = e
374
+ ws.merge_cells(start_row=row, start_column=8, end_row=row, end_column=9)
375
+ ws.cell(row, 8).value = f
376
+ for col in [1, 4, 7]:
377
+ ws.cell(row, col).fill = header_fill
378
+ ws.cell(row, col).font = Font(name="Malgun Gothic", size=10, bold=True)
379
+ ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
380
+ for col in [2, 5, 8]:
381
+ ws.cell(row, col).font = Font(name="Malgun Gothic", size=10)
382
+ ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
383
+ style_range(ws, f"A{row}:I{row}")
384
+ ws.row_dimensions[row].height = 30
385
+
386
+ r = 3
387
+ info_row(r, "๊ธฐ๊ด€๋ช…", data["institution_name"], "๊ธฐ๊ด€์œ ํ˜•", data["institution_type"], "์ž‘์„ฑ์ผ์ž", data["record_date"]); r += 1
388
+ info_row(r, "์ˆ˜๊ธ‰์ž", data["masked_name"], "์žฅ๊ธฐ์š”์–‘๋“ฑ๊ธ‰", data["care_grade"], "ํŒ์ •", data["risk_level"]); r += 1
389
+ info_row(r, "์ธ์ •๋ฒˆํ˜ธ", data["recognition_no"], "์ธ์ •๊ธฐ๊ฐ„", data["recognition_period"], "์‚ฌ์ง„์ฒจ๋ถ€", data["photo_names"]); r += 1
390
+
391
+ sections = [
392
+ ("์ƒํƒœ๋ณ€ํ™” ๊ธฐ๋ก๋‚ด์šฉ", data["content"], 180),
393
+ ("ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€", data["compliance_result"], 55),
394
+ ("๋ถ€์กฑํ•œ ํ•ญ๋ชฉ", data["missing_items"], 45),
395
+ ]
396
+ for title, body, min_height in sections:
397
+ ws.cell(r, 1).value = title
398
+ ws.cell(r, 1).fill = header_fill
399
+ ws.cell(r, 1).font = Font(name="Malgun Gothic", size=11, bold=True)
400
+ ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
401
+ ws.merge_cells(start_row=r, start_column=2, end_row=r, end_column=9)
402
+ ws.cell(r, 2).value = body
403
+ ws.cell(r, 2).font = Font(name="Malgun Gothic", size=10)
404
+ ws.cell(r, 2).alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
405
+ lines = sum(max(1, len(line) // 55 + 1) for line in str(body).splitlines())
406
+ ws.row_dimensions[r].height = max(min_height, min(360, lines * 19 + 20))
407
+ style_range(ws, f"A{r}:I{r}")
408
+ r += 1
409
+
410
+ # ๊ณตํ†ต ์•ˆ๋‚ด๋ฌธ๊ตฌ ์ถ”๊ฐ€
411
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
412
+ ws.cell(r, 1).value = "โ€ป ์ƒ์„ฑ๋œ ๋ฌธ์„œ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์…”๋„ ๋˜๋ฉฐ, ๊ธฐ๊ด€์˜ ์šด์˜๋ฐฉ์นจ, ์ˆ˜๊ธ‰์ž ์ƒํƒœ, ๋ณดํ˜ธ์ž ์˜๊ฒฌ ๋ฐ ํ‰๊ฐ€๊ธฐ์ค€์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ๋ณด์™„์‚ฌํ•ญ์€ ์–ผ๋งˆ๋“ ์ง€ ์ˆ˜์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๊ด€์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ํ™œ์šฉํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค."
413
+ style_range(ws, f"A{r}:I{r}",
414
+ font=Font(name="Malgun Gothic", size=8.5, color="595959", italic=True),
415
+ alignment=Alignment(horizontal="left", vertical="center", wrap_text=True))
416
+ ws.row_dimensions[r].height = 36
417
+ r += 1
418
+
419
+ hidden = wb.create_sheet("๋‚ด๋ถ€์ ๊ฒ€์ž๋ฃŒ_์ถœ๋ ฅ์ œ์™ธ")
420
+ hidden.sheet_state = "hidden"
421
+ hidden["A1"] = "๊ตฌ๋ถ„"
422
+ hidden["B1"] = "๋‚ด์šฉ"
423
+ rows = [
424
+ ("ํ‰๊ฐ€์ ํ•ฉ๋„", f"{data['evaluation_score']}์ "),
425
+ ("๋ถ€์กฑํ•ญ๋ชฉ", data["missing_items"]),
426
+ ("์ž๋™ํŒ์ •", data["risk_level"]),
427
+ ("์‚ฌ์ง„์ฒจ๋ถ€", data["photo_names"]),
428
+ ]
429
+ for idx, (a, b) in enumerate(rows, 2):
430
+ hidden.cell(idx, 1).value = a
431
+ hidden.cell(idx, 2).value = b
432
+ hidden.column_dimensions["A"].width = 20
433
+ hidden.column_dimensions["B"].width = 90
434
+
435
+ output = BytesIO()
436
+ wb.save(output)
437
+ output.seek(0)
438
+ return output
439
+
440
+
441
+ clients_df = load_clients()
442
+ if DB_PATH:
443
+ st.sidebar.success("Client Center DB ์—ฐ๊ฒฐ๋จ")
444
+ st.sidebar.caption(DB_PATH)
445
+ else:
446
+ st.sidebar.warning("Client Center DB๋ฅผ ์ฐพ์ง€ ๋ชปํ•ด ์ง์ ‘์ž…๋ ฅ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.")
447
+
448
+ mode = st.radio("์ž…๋ ฅ ๋ฐฉ์‹", ["Client Center์—์„œ ์ˆ˜๊ธ‰์ž ์„ ํƒ", "์ง์ ‘ ์ž…๋ ฅ"], horizontal=True)
449
+ selected_client = None
450
+ if mode == "Client Center์—์„œ ์ˆ˜๊ธ‰์ž ์„ ํƒ" and not clients_df.empty:
451
+ options = clients_df.apply(lambda x: f"{x['id']}๋ฒˆ - {x['masked_name']} / {x['care_grade']} / {x['institution_type']} / {x['status']}", axis=1).tolist()
452
+ option = st.selectbox("์ˆ˜๊ธ‰์ž ์„ ํƒ", options)
453
+ selected_id = int(option.split("๋ฒˆ")[0])
454
+ selected_client = clients_df[clients_df["id"] == selected_id].iloc[0]
455
+ elif mode == "Client Center์—์„œ ์ˆ˜๊ธ‰์ž ์„ ํƒ":
456
+ st.warning("๋“ฑ๋ก๋œ ์ˆ˜๊ธ‰์ž ์ •๋ณด๊ฐ€ ์—†๊ฑฐ๋‚˜ DB๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ง์ ‘ ์ž…๋ ฅ์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
457
+
458
+ if selected_client is not None:
459
+ default_name = safe(selected_client, "name")
460
+ default_masked = safe(selected_client, "masked_name") or mask_name(default_name)
461
+ default_grade = safe(selected_client, "care_grade", "3๋“ฑ๊ธ‰")
462
+ default_inst = safe(selected_client, "institution_type", "๋ฐฉ๋ฌธ์š”์–‘")
463
+ default_inst_name = safe(selected_client, "institution_name")
464
+ default_main = safe(selected_client, "main_condition")
465
+ default_cog = safe(selected_client, "cognitive_status")
466
+ default_mob = safe(selected_client, "mobility_status")
467
+ default_diet = safe(selected_client, "diet_status")
468
+ default_toilet = safe(selected_client, "toileting_status")
469
+ recognition_no = safe(selected_client, "recognition_no") or safe(selected_client, "recognition_number") or "๋ฏธ์ž…๋ ฅ"
470
+ recognition_period = " ~ ".join([x for x in [safe(selected_client, "recognition_start_date"), safe(selected_client, "recognition_end_date")] if x]) or "๋ฏธ์ž…๋ ฅ"
471
+ else:
472
+ default_name = "ํ™๊ธธ๋™"
473
+ default_masked = "ํ™โ—‹๋™"
474
+ default_grade = "3๋“ฑ๊ธ‰"
475
+ default_inst = "๋ฐฉ๋ฌธ์š”์–‘"
476
+ default_inst_name = ""
477
+ default_main = ""
478
+ default_cog = ""
479
+ default_mob = ""
480
+ default_diet = ""
481
+ default_toilet = ""
482
+ recognition_no = "๋ฏธ์ž…๋ ฅ"
483
+ recognition_period = "๋ฏธ์ž…๋ ฅ"
484
+
485
+ st.subheader("1๋‹จ๊ณ„. ๊ธฐ๋ณธ์ •๋ณด")
486
+ col1, col2, col3, col4 = st.columns(4)
487
+ name = col1.text_input("์ˆ˜๊ธ‰์ž ์„ฑ๋ช…", default_name)
488
+ masked_name = col2.text_input("๊ฐ€๋ช… ํ‘œ๊ธฐ", default_masked or mask_name(name))
489
+ care_grade = col3.selectbox("์žฅ๊ธฐ์š”์–‘๋“ฑ๊ธ‰", ["1๋“ฑ๊ธ‰", "2๋“ฑ๊ธ‰", "3๋“ฑ๊ธ‰", "4๋“ฑ๊ธ‰", "5๋“ฑ๊ธ‰", "์ธ์ง€์ง€์›๋“ฑ๊ธ‰", "๋ฏธ์ž…๋ ฅ"], index=2 if default_grade not in ["1๋“ฑ๊ธ‰", "2๋“ฑ๊ธ‰", "4๋“ฑ๊ธ‰", "5๋“ฑ๊ธ‰", "์ธ์ง€์ง€์›๋“ฑ๊ธ‰", "๋ฏธ์ž…๋ ฅ"] else ["1๋“ฑ๊ธ‰", "2๋“ฑ๊ธ‰", "3๋“ฑ๊ธ‰", "4๋“ฑ๊ธ‰", "5๋“ฑ๊ธ‰", "์ธ์ง€์ง€์›๋“ฑ๊ธ‰", "๋ฏธ์ž…๋ ฅ"].index(default_grade))
490
+ record_date = col4.date_input("์ž‘์„ฑ์ผ์ž", value=date.today())
491
+
492
+ col5, col6 = st.columns(2)
493
+ institution_type = col5.selectbox("๊ธฐ๊ด€์œ ํ˜•", ["๋ฐฉ๋ฌธ์š”์–‘", "๋ฐฉ๋ฌธ๋ชฉ์š•", "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ", "์ฃผ๊ฐ„๋ณดํ˜ธ", "์š”์–‘์›", "๊ณต๋™์ƒํ™œ๊ฐ€์ •"], index=["๋ฐฉ๋ฌธ์š”์–‘", "๋ฐฉ๋ฌธ๋ชฉ์š•", "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ", "์ฃผ๊ฐ„๋ณดํ˜ธ", "์š”์–‘์›", "๊ณต๋™์ƒํ™œ๊ฐ€์ •"].index(default_inst) if default_inst in ["๋ฐฉ๋ฌธ์š”์–‘", "๋ฐฉ๋ฌธ๋ชฉ์š•", "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ", "์ฃผ๊ฐ„๋ณดํ˜ธ", "์š”์–‘์›", "๊ณต๋™์ƒํ™œ๊ฐ€์ •"] else 0)
494
+ institution_name = col6.text_input("๊ธฐ๊ด€๋ช…", default_inst_name)
495
+
496
+ st.subheader("2๋‹จ๊ณ„. ์ƒํƒœ๋ณ€ํ™” ์ž…๋ ฅ")
497
+ c1, c2 = st.columns(2)
498
+ main_condition = c1.text_area("์ฃผ์š” ์งˆํ™˜ / ๊ธฐ์กด ์ƒํƒœ", value=default_main, height=85)
499
+ physical_note = c2.text_area("์‹ ์ฒด์˜์—ญ ๋ณ€ํ™”", value=f"{default_mob} {default_diet} {default_toilet}".strip(), height=85, placeholder="์˜ˆ: ๋ณดํ–‰ ์‹œ ํœ˜์ฒญ๊ฑฐ๋ฆผ, ์‹์‚ฌ๋Ÿ‰ ๊ฐ์†Œ, ์š”์‹ค๊ธˆ ์ฆ๊ฐ€")
500
+ c3, c4 = st.columns(2)
501
+ cognitive_note = c3.text_area("์ธ์ง€์˜์—ญ ๋ณ€ํ™”", value=default_cog, height=85, placeholder="์˜ˆ: ๋ฐ˜๋ณต์งˆ๋ฌธ, ๊ท€๊ฐ€์š•๊ตฌ, ๋„๋‘‘๋ง์ƒ, ๋ฐฐํšŒ")
502
+ emotion_note = c4.text_area("๊ฐ์ •์˜์—ญ ๋ณ€ํ™”", height=85, placeholder="์˜ˆ: ์šฐ์šธ, ๋ˆˆ๋ฌผ, ๋ฌด๊ธฐ๋ ฅ, ๋ถˆ์•ˆ")
503
+ c5, c6 = st.columns(2)
504
+ relation_note = c5.text_area("๊ด€๊ณ„์˜์—ญ ๋ณ€ํ™”", height=85, placeholder="์˜ˆ: ํƒ€ ์ˆ˜๊ธ‰์ž์™€ ๋ง๋‹คํˆผ, ๋ณดํ˜ธ์ž ๊ฐˆ๋“ฑ")
505
+ resource_note = c6.text_area("์ง€์—ญ์ž์›์—ฐ๊ณ„ ํ•„์š”", height=85, placeholder="์˜ˆ: ๋…๊ฑฐ, ๊ฒฐ์‹, ๋ณต์•ฝ ๋ถˆ์ดํ–‰, ๊ฒฝ์ œ์  ์–ด๋ ค์›€")
506
+ field_memo = st.text_area("ํ˜„์žฅ ๋‹จ์ˆœ ๋ฉ”๋ชจ", height=100, placeholder="์˜ˆ: ์˜คํ›„์— ๋„๋‘‘๋…„์ด๋ผ๊ณ  ์š•ํ•˜๊ณ  ํ™”๋ƒ„. ์‹์‚ฌ ์ ˆ๋ฐ˜๋งŒ ๋“œ์‹ฌ.")
507
+ guardian_opinion = st.text_area("๋ณดํ˜ธ์ž ์˜๊ฒฌ", height=85, placeholder="์˜ˆ: ์ตœ๊ทผ ๋ฐค์— ์ž ์„ ๋ชป ์ž๊ณ  ๋ฐฐํšŒ๊ฐ€ ๋Š˜์–ด ๊ฑฑ์ •๋œ๋‹ค๊ณ  ํ•จ")
508
+
509
+ photo_files = st.file_uploader("์‚ฌ์ง„์ฒจ๋ถ€", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
510
+ photo_names = ", ".join([f.name for f in photo_files]) if photo_files else "์ฒจ๋ถ€ ์—†์Œ"
511
+
512
+ if st.button("๐Ÿ“ˆ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์ƒ์„ฑ", use_container_width=True):
513
+ input_data = {
514
+ "main_condition": main_condition,
515
+ "physical_note": physical_note,
516
+ "cognitive_note": cognitive_note,
517
+ "emotion_note": emotion_note,
518
+ "relation_note": relation_note,
519
+ "resource_note": resource_note,
520
+ "field_memo": field_memo,
521
+ "guardian_opinion": guardian_opinion,
522
+ }
523
+ content, analysis = build_status_text(input_data)
524
+ score, missing, result = evaluate_record(content, analysis, len(photo_files or []))
525
+ st.session_state["status_change_data"] = {
526
+ "name": name,
527
+ "masked_name": masked_name,
528
+ "care_grade": care_grade,
529
+ "institution_type": institution_type,
530
+ "institution_name": institution_name,
531
+ "record_date": str(record_date),
532
+ "recognition_no": recognition_no,
533
+ "recognition_period": recognition_period,
534
+ "risk_level": analysis["risk_level"],
535
+ "content": content,
536
+ "evaluation_score": score,
537
+ "missing_items": ", ".join(missing) if missing else "๋ถ€์กฑ ํ•ญ๋ชฉ ์—†์Œ",
538
+ "compliance_result": result,
539
+ "photo_names": photo_names,
540
+ }
541
+ st.session_state.pop("status_change_excel", None)
542
+
543
+ if "status_change_data" in st.session_state:
544
+ data = st.session_state["status_change_data"]
545
+ st.markdown("---")
546
+ st.subheader("3๋‹จ๊ณ„. ์ƒ์„ฑ ๊ฒฐ๊ณผ ํ™•์ธ/์ˆ˜์ •")
547
+ data["content"] = st.text_area("์ƒํƒœ๋ณ€ํ™” ๊ธฐ๋ก๋‚ด์šฉ", value=data["content"], height=340)
548
+ a, b, c = st.columns(3)
549
+ a.metric("์ž๋™ํŒ์ •", data["risk_level"])
550
+ b.metric("ํ‰๊ฐ€ ์ ํ•ฉ๋„", f"{data['evaluation_score']}์ ")
551
+ c.metric("๊ฒฐ์žฌ๋ผ์ธ", "๋‹ด๋‹น์ž/์‹œ์„ค์žฅ 2์นธ")
552
+ if data["evaluation_score"] >= 90:
553
+ st.markdown(f'<div class="okbox">{data["compliance_result"]}</div>', unsafe_allow_html=True)
554
+ else:
555
+ st.markdown(f'<div class="warnbox">{data["compliance_result"]}</div>', unsafe_allow_html=True)
556
+
557
+ if st.button("๐Ÿ“ฆ ์—‘์…€ ํŒŒ์ผ ์ค€๋น„ํ•˜๊ธฐ", use_container_width=True):
558
+ st.session_state["status_change_excel"] = generate_excel(data)
559
+ st.success("์—‘์…€ ํŒŒ์ผ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
560
+
561
+ if "status_change_excel" in st.session_state:
562
+ filename = f"์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€_{data['masked_name']}_{datetime.now().strftime('%Y%m%d')}.xlsx"
563
+ st.download_button(
564
+ "๐Ÿ“ฅ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์—‘์…€ ๋‹ค์šด๋กœ๋“œ",
565
+ data=st.session_state["status_change_excel"],
566
+ file_name=filename,
567
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
568
+ use_container_width=True,
569
+ )
570
+
571
+ st.caption("์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์ตœ์ข…๋ณธ: Client Center ์—ฐ๋™, ์ง์ ‘์ž…๋ ฅ, ์‚ฌ์ง„์ฒจ๋ถ€, ์—‘์…€ ๋‹ค์šด๋กœ๋“œ, ๊ฒฐ์žฌ๋ผ์ธ 2์นธ, ๋‚ด๋ถ€์ ๊ฒ€ ์ˆจ๊น€์‹œํŠธ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.")