devorah-ai-2016 commited on
Commit
e0f3a9a
ยท
verified ยท
1 Parent(s): 265933c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +935 -0
app.py ADDED
@@ -0,0 +1,935 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ import random
4
+ from io import BytesIO
5
+ from datetime import date, datetime, timedelta
6
+ import tempfile
7
+
8
+ import pandas as pd
9
+ import streamlit as st
10
+ from openpyxl import Workbook
11
+ from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
12
+ from openpyxl.utils import get_column_letter
13
+ from openpyxl.drawing.image import Image as XLImage
14
+
15
+
16
+ # =========================================================
17
+ # BeeCare ํ†ตํ•ฉ์—ฐ๋™ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์ƒ์„ฑ๊ธฐ
18
+ # - Client Center ์ˆ˜๊ธ‰์ž DB ์—ฐ๋™
19
+ # - ๋ชจ๋“  ๊ธฐ๊ด€์œ ํ˜• ๊ธฐ๋ณธ ์ฃผ 1ํšŒ ์ž‘์„ฑ
20
+ # - ํ•„์š” ์‹œ ์ถ”๊ฐ€์ž‘์„ฑ ๊ฐ€๋Šฅ
21
+ # - ์ƒํƒœ๋ณ€ํ™” ๋ฐœ์ƒ ์‹œ ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ์žฌ์ž‘์„ฑ ํ•„์š” ์—ฌ๋ถ€ ์ž๋™ ํŒ๋‹จ
22
+ # - ์ƒ๋‹ด์ผ์ง€/์š•๊ตฌ์‚ฌ์ •/์‚ฌ๋ก€๊ด€๋ฆฌ/๊ธ‰์—ฌ๊ณ„ํš๊ณผ ์—ฐ๊ฒฐ ๊ฐ€๋Šฅํ•œ DB ์ €์žฅ
23
+ # - ์—‘์…€ ๋‹ค์šด๋กœ๋“œ, ์‚ฌ์ง„์ฒจ๋ถ€, ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€, ๊ฒฐ์žฌ๋ผ์ธ 2์นธ
24
+ # =========================================================
25
+
26
+ st.set_page_config(
27
+ page_title="์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€",
28
+ page_icon="๐Ÿฉบ",
29
+ layout="wide"
30
+ )
31
+
32
+ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
33
+
34
+ DB_CANDIDATES = [
35
+ os.path.join(CURRENT_DIR, "data", "clients.db"),
36
+ os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"),
37
+ os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"),
38
+ os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"),
39
+ ]
40
+
41
+ DB_PATH = None
42
+ for p in DB_CANDIDATES:
43
+ if os.path.exists(p):
44
+ DB_PATH = p
45
+ break
46
+
47
+ if DB_PATH is None:
48
+ os.makedirs(os.path.join(CURRENT_DIR, "data"), exist_ok=True)
49
+ DB_PATH = os.path.join(CURRENT_DIR, "data", "clients.db")
50
+
51
+
52
+ # -----------------------------
53
+ # UI ๋””์ž์ธ
54
+ # -----------------------------
55
+ st.markdown("""
56
+ <style>
57
+ .block-container { padding-top: 2rem; }
58
+ .hero {
59
+ background: linear-gradient(135deg, #1D4ED8, #60A5FA);
60
+ padding: 30px;
61
+ border-radius: 22px;
62
+ text-align: center;
63
+ margin-bottom: 22px;
64
+ }
65
+ .hero h1 { color: white; margin: 0; font-size: 36px; }
66
+ .hero p { color: white; margin-top: 10px; font-size: 18px; }
67
+ .notice {
68
+ background: #fff8e1;
69
+ border-left: 7px solid #f9a825;
70
+ padding: 15px;
71
+ border-radius: 12px;
72
+ margin-bottom: 18px;
73
+ }
74
+ .okbox {
75
+ background: #e8f5e9;
76
+ border-left: 7px solid #43a047;
77
+ padding: 14px;
78
+ border-radius: 12px;
79
+ margin-bottom: 14px;
80
+ }
81
+ .warnbox {
82
+ background: #fff3e0;
83
+ border-left: 7px solid #fb8c00;
84
+ padding: 14px;
85
+ border-radius: 12px;
86
+ margin-bottom: 14px;
87
+ }
88
+ .dangerbox {
89
+ background: #ffebee;
90
+ border-left: 7px solid #e53935;
91
+ padding: 14px;
92
+ border-radius: 12px;
93
+ margin-bottom: 14px;
94
+ }
95
+ </style>
96
+ """, unsafe_allow_html=True)
97
+
98
+ st.markdown("""
99
+ <div class="hero">
100
+ <h1>๐Ÿฉบ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์ƒ์„ฑ๊ธฐ</h1>
101
+ <p>Client Center ์ˆ˜๊ธ‰์ž ์ •๋ณด์™€ ์—ฐ๋™ํ•˜์—ฌ ์ฃผ 1ํšŒ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก ๋ฐ ์ถ”๊ฐ€๊ธฐ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.</p>
102
+ </div>
103
+ """, unsafe_allow_html=True)
104
+
105
+ st.markdown("""
106
+ <div class="notice">
107
+ <strong>์ž‘์„ฑ ์›์น™:</strong> ๋ชจ๋“  ๊ธฐ๊ด€์œ ํ˜•์€ ๊ธฐ๋ณธ ์ฃผ 1ํšŒ ์ž‘์„ฑ์œผ๋กœ ์„ค๊ณ„ํ•˜๊ณ , ๋‚™์ƒยท์ž…์›ยทํ‡ด์›ยท์š•์ฐฝยท์‹์‚ฌ๋Ÿ‰ ๊ฐ์†Œยท๋ณดํ˜ธ์ž ์š”์ฒญ ๋“ฑ ํŠน์ด์‚ฌํ•ญ ๋ฐœ์ƒ ์‹œ ์ถ”๊ฐ€์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
108
+ </div>
109
+ """, unsafe_allow_html=True)
110
+
111
+
112
+ # -----------------------------
113
+ # DB
114
+ # -----------------------------
115
+ def get_conn():
116
+ return sqlite3.connect(DB_PATH, check_same_thread=False)
117
+
118
+
119
+ def init_db():
120
+ conn = get_conn()
121
+ cur = conn.cursor()
122
+
123
+ cur.execute("""
124
+ CREATE TABLE IF NOT EXISTS clients (
125
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
126
+ name TEXT NOT NULL,
127
+ masked_name TEXT,
128
+ gender TEXT,
129
+ birth_year TEXT,
130
+ care_grade TEXT,
131
+ institution_type TEXT,
132
+ institution_name TEXT,
133
+ admission_date TEXT,
134
+ main_condition TEXT,
135
+ cognitive_status TEXT,
136
+ mobility_status TEXT,
137
+ diet_status TEXT,
138
+ toileting_status TEXT,
139
+ guardian_name TEXT,
140
+ guardian_phone TEXT,
141
+ notes TEXT,
142
+ status TEXT DEFAULT '์ด์šฉ์ค‘',
143
+ created_at TEXT,
144
+ updated_at TEXT
145
+ )
146
+ """)
147
+
148
+ # Client Center v2 ํ•„๋“œ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ์ถ”๊ฐ€
149
+ new_columns = {
150
+ "previous_care_grade": "TEXT",
151
+ "plan_change_required": "TEXT DEFAULT '์•„๋‹ˆ์˜ค'",
152
+ "plan_change_type": "TEXT",
153
+ "plan_change_reason": "TEXT",
154
+ "plan_change_date": "TEXT",
155
+ "plan_rewrite_status": "TEXT DEFAULT 'ํ•ด๋‹น์—†์Œ'",
156
+ "service_end_reason": "TEXT",
157
+ "service_end_date": "TEXT",
158
+ "linked_document": "TEXT",
159
+ }
160
+ cur.execute("PRAGMA table_info(clients)")
161
+ existing_cols = [row[1] for row in cur.fetchall()]
162
+ for col, col_type in new_columns.items():
163
+ if col not in existing_cols:
164
+ cur.execute(f"ALTER TABLE clients ADD COLUMN {col} {col_type}")
165
+
166
+ cur.execute("""
167
+ CREATE TABLE IF NOT EXISTS status_change_records (
168
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
169
+ client_id INTEGER,
170
+ client_masked_name TEXT,
171
+ institution_type TEXT,
172
+ record_type TEXT,
173
+ record_date TEXT,
174
+ week_period TEXT,
175
+ writer TEXT,
176
+ change_category TEXT,
177
+ body_status TEXT,
178
+ cognitive_status TEXT,
179
+ emotion_status TEXT,
180
+ diet_status TEXT,
181
+ toileting_status TEXT,
182
+ mobility_status TEXT,
183
+ skin_status TEXT,
184
+ sleep_status TEXT,
185
+ program_status TEXT,
186
+ guardian_opinion TEXT,
187
+ staff_observation TEXT,
188
+ action_taken TEXT,
189
+ followup_plan TEXT,
190
+ plan_change_required TEXT,
191
+ plan_change_reason TEXT,
192
+ evaluation_score INTEGER,
193
+ missing_items TEXT,
194
+ compliance_result TEXT,
195
+ photo_count INTEGER,
196
+ created_at TEXT
197
+ )
198
+ """)
199
+
200
+ cur.execute("""
201
+ CREATE TABLE IF NOT EXISTS plan_change_logs (
202
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
203
+ client_id INTEGER,
204
+ client_masked_name TEXT,
205
+ institution_type TEXT,
206
+ change_type TEXT,
207
+ change_reason TEXT,
208
+ change_date TEXT,
209
+ linked_document TEXT,
210
+ plan_change_required TEXT,
211
+ plan_rewrite_status TEXT,
212
+ memo TEXT,
213
+ created_at TEXT
214
+ )
215
+ """)
216
+
217
+ conn.commit()
218
+ conn.close()
219
+
220
+
221
+ def load_clients():
222
+ conn = get_conn()
223
+ df = pd.read_sql_query("SELECT * FROM clients ORDER BY id DESC", conn)
224
+ conn.close()
225
+ return df
226
+
227
+
228
+ def load_status_records(client_id=None):
229
+ conn = get_conn()
230
+ try:
231
+ if client_id:
232
+ df = pd.read_sql_query(
233
+ "SELECT * FROM status_change_records WHERE client_id=? ORDER BY record_date DESC, id DESC",
234
+ conn,
235
+ params=(int(client_id),)
236
+ )
237
+ else:
238
+ df = pd.read_sql_query(
239
+ "SELECT * FROM status_change_records ORDER BY record_date DESC, id DESC",
240
+ conn
241
+ )
242
+ except Exception:
243
+ df = pd.DataFrame()
244
+ conn.close()
245
+ return df
246
+
247
+
248
+ def safe(row, key, default=""):
249
+ try:
250
+ v = row.get(key, default)
251
+ if pd.isna(v):
252
+ return default
253
+ return str(v)
254
+ except Exception:
255
+ return default
256
+
257
+
258
+ def mask_name(name):
259
+ name = str(name or "").strip()
260
+ if len(name) >= 3:
261
+ return name[0] + "โ—‹" + name[2:]
262
+ if len(name) == 2:
263
+ return name[0] + "โ—‹"
264
+ return name
265
+
266
+
267
+ def save_uploaded_photos(uploaded_files):
268
+ paths = []
269
+ for file in (uploaded_files or [])[:3]:
270
+ suffix = os.path.splitext(file.name)[1].lower() or ".png"
271
+ temp_path = os.path.join(tempfile.gettempdir(), f"status_photo_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{suffix}")
272
+ with open(temp_path, "wb") as f:
273
+ f.write(file.getbuffer())
274
+ paths.append(temp_path)
275
+ return paths
276
+
277
+
278
+ def save_plan_change_log(client_id, client_masked_name, institution_type, change_reason, record_date, required, memo):
279
+ conn = get_conn()
280
+ cur = conn.cursor()
281
+
282
+ rewrite_status = "์žฌ์ž‘์„ฑ ํ•„์š”" if required == "์˜ˆ" else "ํ˜„์žฌ๊ณ„ํš ์œ ์ง€"
283
+
284
+ cur.execute("""
285
+ INSERT INTO plan_change_logs (
286
+ client_id, client_masked_name, institution_type, change_type,
287
+ change_reason, change_date, linked_document, plan_change_required,
288
+ plan_rewrite_status, memo, created_at
289
+ )
290
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
291
+ """, (
292
+ client_id,
293
+ client_masked_name,
294
+ institution_type,
295
+ "์ƒํƒœ๋ณ€ํ™”",
296
+ change_reason,
297
+ record_date,
298
+ "์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€",
299
+ required,
300
+ rewrite_status,
301
+ memo,
302
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S")
303
+ ))
304
+
305
+ cur.execute("""
306
+ UPDATE clients
307
+ SET plan_change_required=?,
308
+ plan_change_type=?,
309
+ plan_change_reason=?,
310
+ plan_change_date=?,
311
+ plan_rewrite_status=?,
312
+ linked_document=?,
313
+ updated_at=?
314
+ WHERE id=?
315
+ """, (
316
+ required,
317
+ "์ƒํƒœ๋ณ€ํ™”",
318
+ change_reason,
319
+ record_date,
320
+ rewrite_status,
321
+ "์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€",
322
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
323
+ client_id
324
+ ))
325
+
326
+ conn.commit()
327
+ conn.close()
328
+
329
+
330
+ def save_status_record(data):
331
+ conn = get_conn()
332
+ cur = conn.cursor()
333
+ cur.execute("""
334
+ INSERT INTO status_change_records (
335
+ client_id, client_masked_name, institution_type, record_type,
336
+ record_date, week_period, writer, change_category,
337
+ body_status, cognitive_status, emotion_status, diet_status,
338
+ toileting_status, mobility_status, skin_status, sleep_status,
339
+ program_status, guardian_opinion, staff_observation,
340
+ action_taken, followup_plan, plan_change_required,
341
+ plan_change_reason, evaluation_score, missing_items,
342
+ compliance_result, photo_count, created_at
343
+ )
344
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
345
+ """, (
346
+ data["client_id"],
347
+ data["client_masked_name"],
348
+ data["institution_type"],
349
+ data["record_type"],
350
+ data["record_date"],
351
+ data["week_period"],
352
+ data["writer"],
353
+ data["change_category"],
354
+ data["body_status"],
355
+ data["cognitive_status"],
356
+ data["emotion_status"],
357
+ data["diet_status"],
358
+ data["toileting_status"],
359
+ data["mobility_status"],
360
+ data["skin_status"],
361
+ data["sleep_status"],
362
+ data["program_status"],
363
+ data["guardian_opinion"],
364
+ data["staff_observation"],
365
+ data["action_taken"],
366
+ data["followup_plan"],
367
+ data["plan_change_required"],
368
+ data["plan_change_reason"],
369
+ data["evaluation_score"],
370
+ data["missing_items"],
371
+ data["compliance_result"],
372
+ data["photo_count"],
373
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S")
374
+ ))
375
+ conn.commit()
376
+ conn.close()
377
+
378
+
379
+ init_db()
380
+
381
+
382
+ # -----------------------------
383
+ # ๊ธฐ๊ด€์œ ํ˜•๋ณ„ ์ถ”๊ฐ€ ์ ๊ฒ€ ํ•ญ๋ชฉ
384
+ # -----------------------------
385
+ INSTITUTION_GUIDES = {
386
+ "๋ฐฉ๋ฌธ์š”์–‘": {
387
+ "focus": ["์žฌ๊ฐ€์ƒํ™œ ๋ณ€ํ™”", "๋ณดํ–‰/์ด๋™", "์‹์‚ฌยท๋ณต์•ฝ", "์ธ์ง€ํ™œ๋™", "๊ฐ€์‚ฌ์ง€์› ๋ฒ”์œ„", "๋ณดํ˜ธ์ž ์š”์ฒญ"],
388
+ "note": "๋ฐฉ๋ฌธ์„œ๋น„์Šค ์ œ๊ณต ์ค‘ ํ™•์ธ๋œ ์ƒํƒœ๋ณ€ํ™”์™€ ๋ณดํ˜ธ์ž ์˜๊ฒฌ, ์š”์–‘๋ณดํ˜ธ์‚ฌ ๊ด€์ฐฐ๋‚ด์šฉ์„ ์ค‘์‹ฌ์œผ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.",
389
+ },
390
+ "๋ฐฉ๋ฌธ๋ชฉ์š•": {
391
+ "focus": ["ํ”ผ๋ถ€์ƒํƒœ", "๋ชฉ์š•ํ˜‘์กฐ๋„", "๋ชฉ์š• ์ „ํ›„ ํ™œ๋ ฅ", "์ด๋™์•ˆ์ „", "์ฒญ๊ฒฐ์ƒํƒœ", "๋ณดํ˜ธ์ž ํ˜‘์กฐ"],
392
+ "note": "๋ชฉ์š• ์ „ํ›„ ๊ฑด๊ฐ•์ƒํƒœ, ํ”ผ๋ถ€ ๋ฐœ์ ยท์ƒ์ฒ˜, ์ด๋™์•ˆ์ „๊ณผ ๋ชฉ์š• ๊ฑฐ๋ถ€ ์—ฌ๋ถ€๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.",
393
+ },
394
+ "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ": {
395
+ "focus": ["ํ˜ˆ์••/ํ˜ˆ๋‹น", "์ƒ์ฒ˜๊ด€๋ฆฌ", "ํˆฌ์•ฝ๋ณ€๊ฒฝ", "๊ฑด๊ฐ•์ƒ๋‹ด", "์˜๋ฃŒ์—ฐ๊ณ„", "๋ณดํ˜ธ์ž ๊ต์œก"],
396
+ "note": "๊ฑด๊ฐ•์ง€ํ‘œ ๋ณ€ํ™”, ์ฒ˜์น˜ ๋‚ด์šฉ, ํˆฌ์•ฝยท์ƒ์ฒ˜ยท์˜๋ฃŒ์—ฐ๊ณ„ ํ•„์š”์„ฑ์„ ์ค‘์‹ฌ์œผ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.",
397
+ },
398
+ "์ฃผ๊ฐ„๋ณดํ˜ธ": {
399
+ "focus": ["ํ”„๋กœ๊ทธ๋žจ ์ฐธ์—ฌ", "์‹์‚ฌ", "๋ฐฐ๋ณ€", "์ •์„œ", "์‚ฌํšŒ์„ฑ", "์†ก์˜์•ˆ์ „", "๋‚™์ƒ์œ„ํ—˜"],
400
+ "note": "์„ผํ„ฐ ์ด์šฉ ์ค‘ ํ•œ ์ฃผ๊ฐ„์˜ ์‹ ์ฒดยท์ธ์ง€ยท์ •์„œยท์‹์‚ฌยท๋ฐฐ๋ณ€ยทํ”„๋กœ๊ทธ๋žจ ์ฐธ์—ฌ ๋ณ€ํ™”๋ฅผ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค.",
401
+ },
402
+ "์š”์–‘์›": {
403
+ "focus": ["์‹์‚ฌ", "์ˆ˜๋ฉด", "์š•์ฐฝ", "๋‚™์ƒ", "์ธ์ง€", "๋ฐฐ๋ณ€", "ํ”„๋กœ๊ทธ๋žจ", "์ •์„œ์ง€์›"],
404
+ "note": "๋งค์ผ ๊ด€์ฐฐ๊ธฐ๋ก๊ณผ ๋ณ„๋„๋กœ ์ฃผ 1ํšŒ ์ด์ƒ ์ƒํƒœ๋ณ€ํ™”๋ฅผ ์š”์•ฝํ•˜๊ณ , ๊ธ‰๊ฒฉํ•œ ๋ณ€ํ™”๋Š” ์ถ”๊ฐ€์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.",
405
+ },
406
+ "๊ณต๋™์ƒํ™œ๊ฐ€์ •": {
407
+ "focus": ["์ƒํ™œ์ ์‘", "์ •์„œ์•ˆ์ •", "์‚ฌํšŒ๊ด€๊ณ„", "ํ”„๋กœ๊ทธ๋žจ ์ฐธ์—ฌ", "์‹์‚ฌ", "๊ฐœ๋ณ„ ์š•๊ตฌ", "๊ฐ€์กฑ์†Œํ†ต"],
408
+ "note": "์†Œ๊ทœ๋ชจ ๊ณต๋™์ƒํ™œ ์ ์‘, ์ •์„œยท์‚ฌํšŒ๊ด€๊ณ„ ๋ณ€ํ™”, ๊ฐœ๋ณ„์š•๊ตฌ์™€ ๊ฐ€์กฑ์†Œํ†ต ๋ณ€ํ™”๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.",
409
+ },
410
+ }
411
+
412
+ CHANGE_CATEGORIES = [
413
+ "ํŠน์ด๋ณ€ํ™” ์—†์Œ",
414
+ "๋‚™์ƒ ๋ฐœ์ƒ/์œ„ํ—˜ ์ฆ๊ฐ€",
415
+ "์š•์ฐฝ ๋ฐœ์ƒ/์œ„ํ—˜ ์ฆ๊ฐ€",
416
+ "์‹์‚ฌ๋Ÿ‰ ๊ฐ์†Œ",
417
+ "์ˆ˜๋ถ„์„ญ์ทจ ์ €ํ•˜",
418
+ "์ฒด์ค‘๊ฐ์†Œ ์šฐ๋ ค",
419
+ "๋ณดํ–‰๋Šฅ๋ ฅ ์ €ํ•˜",
420
+ "์ธ์ง€๊ธฐ๋Šฅ ์ €ํ•˜",
421
+ "์ •์„œ๋ถˆ์•ˆ/์šฐ์šธ๊ฐ",
422
+ "๋ฐฐ๋ณ€/๋ฐฐ๋‡จ ๋ณ€ํ™”",
423
+ "๊ธฐ์ €๊ท€ ์ฐฉ์šฉ ์‹œ์ž‘/์ฆ๊ฐ€",
424
+ "ํ”ผ๋ถ€์ƒํƒœ ๋ณ€ํ™”",
425
+ "์ˆ˜๋ฉด์ƒํƒœ ๋ณ€ํ™”",
426
+ "๋ณต์•ฝ/๊ฑด๊ฐ•์ƒํƒœ ๋ณ€ํ™”",
427
+ "๋ณ‘์›์ง„๋ฃŒ/์ž…์›",
428
+ "ํ‡ด์› ํ›„ ์žฌ์ด์šฉ",
429
+ "๋“ฑ๊ธ‰๋ณ€๊ฒฝ",
430
+ "๋ณดํ˜ธ์ž ์š”์ฒญ",
431
+ "์„œ๋น„์Šค ๋ณ€๊ฒฝ ํ•„์š”",
432
+ "์‚ฌ๋ง/์ „์›/์„œ๋น„์Šค ์ข…๋ฃŒ",
433
+ "๊ธฐํƒ€",
434
+ ]
435
+
436
+ PLAN_CHANGE_TRIGGER_WORDS = [
437
+ "๋‚™์ƒ", "์š•์ฐฝ", "์‹์‚ฌ๋Ÿ‰ ๊ฐ์†Œ", "์ˆ˜๋ถ„์„ญ์ทจ", "์ฒด์ค‘", "๋ณดํ–‰๋Šฅ๋ ฅ ์ €ํ•˜",
438
+ "์ธ์ง€๊ธฐ๋Šฅ ์ €ํ•˜", "์ •์„œ๋ถˆ์•ˆ", "๋ฐฐ๋ณ€", "๊ธฐ์ €๊ท€", "ํ”ผ๋ถ€์ƒํƒœ ๋ณ€ํ™”",
439
+ "์ˆ˜๋ฉด์ƒํƒœ ๋ณ€ํ™”", "๋ณต์•ฝ", "์ž…์›", "ํ‡ด์›", "๋“ฑ๊ธ‰๋ณ€๊ฒฝ", "๋ณดํ˜ธ์ž ์š”์ฒญ", "์„œ๋น„์Šค ๋ณ€๊ฒฝ"
440
+ ]
441
+
442
+ TERMINATION_WORDS = ["์‚ฌ๋ง", "์ „์›", "์„œ๋น„์Šค ์ข…๋ฃŒ"]
443
+
444
+
445
+ def determine_plan_change(change_category, body_status, cognitive_status, diet_status, mobility_status, skin_status, guardian_opinion, action_taken):
446
+ combined = " ".join([
447
+ change_category, body_status, cognitive_status, diet_status,
448
+ mobility_status, skin_status, guardian_opinion, action_taken
449
+ ])
450
+
451
+ if any(w in combined for w in TERMINATION_WORDS):
452
+ return "์•„๋‹ˆ์˜ค", "์‚ฌ๋งยท์ „์›ยท์„œ๋น„์Šค ์ข…๋ฃŒ ์‚ฌ์œ ๋Š” ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ์žฌ์ž‘์„ฑ๋ณด๋‹ค ์ข…๊ฒฐ/์ค‘์ง€ ๊ธฐ๋ก ์ •๋ฆฌ๊ฐ€ ์šฐ์„ ์ž…๋‹ˆ๋‹ค."
453
+
454
+ if change_category == "ํŠน์ด๋ณ€ํ™” ์—†์Œ":
455
+ return "์•„๋‹ˆ์˜ค", "ํŠน์ด ์ƒํƒœ๋ณ€ํ™”๊ฐ€ ์—†์–ด ํ˜„์žฌ ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค."
456
+
457
+ if any(w in combined for w in PLAN_CHANGE_TRIGGER_WORDS):
458
+ return "์˜ˆ", "์ƒํƒœ๋ณ€ํ™”๊ฐ€ ํ™•์ธ๋˜์–ด ์ƒ๋‹ด์ผ์ง€ยท์š•๊ตฌ์‚ฌ์ •ยท์‚ฌ๋ก€๊ด€๋ฆฌ์™€ ์—ฐ๋™ํ•˜์—ฌ ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ์žฌ์ž‘์„ฑ ์—ฌ๋ถ€๋ฅผ ๊ฒ€ํ† ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
459
+
460
+ return "๊ฒ€ํ† ", "์ƒํƒœ๋ณ€ํ™” ๋‚ด์šฉ์ด ์žˆ์œผ๋ฏ€๋กœ ๋‹ด๋‹น์ž ํ™•์ธ ํ›„ ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ๋ฐ˜์˜ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค."
461
+
462
+
463
+ def build_status_text(client, institution_type, record_type, change_category, body_status, cognitive_status, emotion_status,
464
+ diet_status, toileting_status, mobility_status, skin_status, sleep_status, program_status,
465
+ guardian_opinion, staff_observation, action_taken, followup_plan):
466
+ focus = ", ".join(INSTITUTION_GUIDES[institution_type]["focus"])
467
+ opener = random.choice([
468
+ "์ด๋ฒˆ ์ฃผ ์ˆ˜๊ธ‰์ž์˜ ์ „๋ฐ˜์ ์ธ ์ƒํƒœ๋ฅผ ๊ด€์ฐฐํ•œ ๊ฒฐ๊ณผ",
469
+ "์„œ๋น„์Šค ์ œ๊ณต ๊ณผ์ •์—์„œ ํ™•์ธ๋œ ์ƒํƒœ๋ณ€ํ™”๋ฅผ ์ข…ํ•ฉํ•˜๋ฉด",
470
+ "์ฃผ๊ฐ„ ๊ด€์ฐฐ ๋ฐ ์ข…์‚ฌ์ž ์˜๊ฒฌ์„ ์ข…ํ•ฉํ•œ ๊ฒฐ๊ณผ",
471
+ "์ˆ˜๊ธ‰์ž์˜ ์‹ ์ฒดยท์ธ์ง€ยท์ •์„œ ์ƒํƒœ๋ฅผ ํ•จ๊ป˜ ํ™•์ธํ•œ ๊ฒฐ๊ณผ",
472
+ ])
473
+
474
+ if record_type == "์ถ”๊ฐ€์ž‘์„ฑ":
475
+ opener = random.choice([
476
+ "ํŠน์ด์‚ฌํ•ญ ๋ฐœ์ƒ์œผ๋กœ ์ถ”๊ฐ€ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์„ ์ž‘์„ฑํ•จ.",
477
+ "์ •๊ธฐ ์ฃผ๊ฐ„๊ธฐ๋ก ์™ธ์— ์ฆ‰์‹œ ํ™•์ธ์ด ํ•„์š”ํ•œ ๋ณ€ํ™”๊ฐ€ ์žˆ์–ด ์ถ”๊ฐ€ ๊ธฐ๋กํ•จ.",
478
+ "์ƒํƒœ๋ณ€ํ™” ๋ฐœ์ƒ์— ๋”ฐ๋ผ ๊ธ‰์—ฌ์ œ๊ณต ๋ฐฉํ–ฅ ๊ฒ€ํ† ๋ฅผ ์œ„ํ•ด ์ถ”๊ฐ€ ๊ธฐ๋กํ•จ.",
479
+ ])
480
+
481
+ text = f"""
482
+ {opener}
483
+
484
+ [๊ธฐ๋ก๊ตฌ๋ถ„]
485
+ {record_type}
486
+
487
+ [๊ธฐ๊ด€์œ ํ˜•๋ณ„ ์ค‘์  ํ™•์ธ]
488
+ {institution_type}์—์„œ๋Š” {focus} ํ•ญ๋ชฉ์„ ์ค‘์‹ฌ์œผ๋กœ ์ƒํƒœ๋ณ€ํ™”๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
489
+ {INSTITUTION_GUIDES[institution_type]["note"]}
490
+
491
+ [์ƒํƒœ๋ณ€ํ™” ๋ถ„๋ฅ˜]
492
+ {change_category}
493
+
494
+ [์„ธ๋ถ€ ๊ด€์ฐฐ๋‚ด์šฉ]
495
+ - ์‹ ์ฒด์ƒํƒœ: {body_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
496
+ - ์ธ์ง€์ƒํƒœ: {cognitive_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
497
+ - ์ •์„œ์ƒํƒœ: {emotion_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
498
+ - ์‹์‚ฌ์ƒํƒœ: {diet_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
499
+ - ๋ฐฐ๋ณ€/๋ฐฐ๋‡จ์ƒํƒœ: {toileting_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
500
+ - ์ด๋™์ƒํƒœ: {mobility_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
501
+ - ํ”ผ๋ถ€์ƒํƒœ: {skin_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
502
+ - ์ˆ˜๋ฉด์ƒํƒœ: {sleep_status or 'ํŠน์ด์‚ฌํ•ญ ์—†์Œ'}
503
+ - ํ”„๋กœ๊ทธ๋žจ/ํ™œ๋™์ƒํƒœ: {program_status or 'ํ•ด๋‹น ์—†์Œ'}
504
+
505
+ [๋ณดํ˜ธ์ž ์˜๊ฒฌ]
506
+ {guardian_opinion or '๋ณดํ˜ธ์ž ์˜๊ฒฌ์€ ํŠน์ด์‚ฌํ•ญ ์—†์œผ๋ฉฐ, ํ•„์š” ์‹œ ์ƒ๋‹ด์ผ์ง€๋กœ ์ถ”๊ฐ€ ํ™•์ธ ์˜ˆ์ •'}
507
+
508
+ [์ข…์‚ฌ์ž ๊ด€์ฐฐ์‚ฌํ•ญ]
509
+ {staff_observation or '๊ธ‰์—ฌ์ œ๊ณต ์ค‘ ํŠน์ด์‚ฌํ•ญ์€ ์—†์œผ๋‚˜ ์ง€์† ๊ด€์ฐฐ ํ•„์š”'}
510
+
511
+ [์กฐ์น˜๋‚ด์šฉ]
512
+ {action_taken or '์ƒํƒœ๋ฅผ ์ง€์† ๊ด€์ฐฐํ•˜๊ณ  ํ•„์š” ์‹œ ๋‹ด๋‹น์ž ๋ฐ ๋ณดํ˜ธ์ž์—๊ฒŒ ๊ณต์œ '}
513
+
514
+ [ํ›„์†๊ณ„ํš]
515
+ {followup_plan or '๋‹ค์Œ ์ฃผ ์ƒํƒœ๋ณ€ํ™”๋ฅผ ์žฌํ™•์ธํ•˜๊ณ , ๋ณ€ํ™”๊ฐ€ ์ง€์†๋  ๊ฒฝ์šฐ ์ƒ๋‹ด์ผ์ง€ยท์š•๊ตฌ์‚ฌ์ •ยท๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš์— ๋ฐ˜์˜ ์—ฌ๋ถ€๋ฅผ ๊ฒ€ํ† '}
516
+ """.strip()
517
+
518
+ return text
519
+
520
+
521
+ def evaluate_record(record_type, change_category, status_text, guardian_opinion, staff_observation, action_taken, followup_plan, plan_change_required):
522
+ required = {
523
+ "๊ธฐ๋ก๊ตฌ๋ถ„": bool(record_type),
524
+ "์ƒํƒœ๋ณ€ํ™” ๋ถ„๋ฅ˜": bool(change_category),
525
+ "์‹ ์ฒด/์ธ์ง€/์ •์„œ ๋“ฑ ๊ด€์ฐฐ": "์„ธ๋ถ€ ๊ด€์ฐฐ๋‚ด์šฉ" in status_text,
526
+ "๋ณดํ˜ธ์ž ์˜๊ฒฌ ํ™•์ธ": bool(guardian_opinion.strip()) or "๋ณดํ˜ธ์ž ์˜๊ฒฌ" in status_text,
527
+ "์ข…์‚ฌ์ž ๊ด€์ฐฐ ๋ฐ˜์˜": bool(staff_observation.strip()) or "์ข…์‚ฌ์ž ๊ด€์ฐฐ" in status_text,
528
+ "์กฐ์น˜๋‚ด์šฉ": bool(action_taken.strip()) or "์กฐ์น˜๋‚ด์šฉ" in status_text,
529
+ "ํ›„์†๊ณ„ํš": bool(followup_plan.strip()) or "ํ›„์†๊ณ„ํš" in status_text,
530
+ "๊ธ‰์—ฌ๊ณ„ํš ์—ฐ๊ณ„ ํŒ๋‹จ": plan_change_required in ["์˜ˆ", "์•„๋‹ˆ์˜ค", "๊ฒ€ํ† "],
531
+ }
532
+
533
+ score = int(sum(required.values()) / len(required) * 100)
534
+ missing = [k for k, v in required.items() if not v]
535
+
536
+ if score >= 90:
537
+ result = "ํ‰๊ฐ€ ์ ํ•ฉ๋„๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค. ์ƒํƒœ๋ณ€ํ™”, ์กฐ์น˜, ํ›„์†๊ณ„ํš, ๊ธ‰์—ฌ๊ณ„ํš ์—ฐ๊ณ„ ํŒ๋‹จ์ด ์ž˜ ๋ฐ˜์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
538
+ elif score >= 75:
539
+ result = "๋Œ€์ฒด๋กœ ์ ํ•ฉํ•˜๋‚˜ ์ผ๋ถ€ ๋ณด์™„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ถ€์กฑ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ธฐ๋ก ์™„์„ฑ๋„๊ฐ€ ๋†’์•„์ง‘๋‹ˆ๋‹ค."
540
+ else:
541
+ result = "๋ณด์™„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ๋ณ€ํ™”, ๋ณดํ˜ธ์ž ์˜๊ฒฌ, ์กฐ์น˜๋‚ด์šฉ, ํ›„์†๊ณ„ํš์ด ๊ตฌ์ฒด์ ์œผ๋กœ ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค."
542
+
543
+ return score, missing, result
544
+
545
+
546
+ # -----------------------------
547
+ # ์—‘์…€
548
+ # -----------------------------
549
+ def style_ws(ws, max_row):
550
+ """A4 ์ถœ๋ ฅ ์‹œ ์„ ์ด ์ž˜๋ฆฌ์ง€ ์•Š๋„๋ก ์ „์ฒด ์ถœ๋ ฅ๋ฒ”์œ„์— ํ…Œ๋‘๋ฆฌ/์ •๋ ฌ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค."""
551
+ thin = Side(style="thin", color="000000")
552
+ border = Border(left=thin, right=thin, top=thin, bottom=thin)
553
+
554
+ for row in ws.iter_rows(min_row=1, max_row=max_row, min_col=1, max_col=9):
555
+ for cell in row:
556
+ cell.border = border
557
+ if cell.value is not None:
558
+ current_size = cell.font.sz or 10
559
+ cell.font = Font(
560
+ name="Malgun Gothic",
561
+ size=current_size,
562
+ bold=cell.font.bold
563
+ )
564
+ else:
565
+ cell.font = Font(name="Malgun Gothic", size=10)
566
+ cell.alignment = Alignment(
567
+ horizontal=cell.alignment.horizontal or "left",
568
+ vertical=cell.alignment.vertical or "center",
569
+ wrap_text=True
570
+ )
571
+
572
+ # ๋ณด๊ธฐ ์ข‹๊ฒŒ ์ถœ๋ ฅ๋˜๋„๋ก ์—ด ๋„ˆ๋น„ ์กฐ์ •
573
+ widths = {1: 11, 2: 13, 3: 13, 4: 11, 5: 13, 6: 13, 7: 11, 8: 13, 9: 13}
574
+ for col, width in widths.items():
575
+ ws.column_dimensions[get_column_letter(col)].width = width
576
+
577
+ ws.page_setup.paperSize = ws.PAPERSIZE_A4
578
+ ws.page_setup.orientation = "portrait"
579
+ ws.page_setup.fitToWidth = 1
580
+ ws.page_setup.fitToHeight = 0
581
+ ws.sheet_properties.pageSetUpPr.fitToPage = True
582
+ ws.page_margins.left = 0.25
583
+ ws.page_margins.right = 0.25
584
+ ws.page_margins.top = 0.30
585
+ ws.page_margins.bottom = 0.30
586
+ ws.sheet_view.showGridLines = False
587
+ ws.print_area = f"A1:I{max_row}"
588
+ ws.print_options.horizontalCentered = True
589
+
590
+
591
+ def write_title_with_approval(ws, title, title_fill, header_fill):
592
+ """ํฐ ์ œ๋ชฉ์€ ์™ผ์ชฝ A:G, ๊ฒฐ์žฌ๋ผ์ธ์€ ์˜ค๋ฅธ์ชฝ ๋งจ ์œ„ H:I์— ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค."""
593
+ for col in range(1, 10):
594
+ ws.column_dimensions[get_column_letter(col)].width = 12
595
+
596
+ thin = Side(style="thin", color="000000")
597
+ border = Border(left=thin, right=thin, top=thin, bottom=thin)
598
+
599
+ ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=7)
600
+ title_cell = ws.cell(1, 1)
601
+ title_cell.value = title
602
+ title_cell.font = Font(name="Malgun Gothic", size=24, bold=True)
603
+ title_cell.alignment = Alignment(horizontal="center", vertical="center")
604
+ title_cell.fill = title_fill
605
+
606
+ # ์ œ๋ชฉ ๋ณ‘ํ•ฉ ์˜์—ญ ์ „์ฒด ํ…Œ๋‘๋ฆฌ ์ ์šฉ
607
+ for rr in [1, 2]:
608
+ for cc in range(1, 8):
609
+ cell = ws.cell(rr, cc)
610
+ cell.border = border
611
+ cell.fill = title_fill
612
+
613
+ # ๊ฒฐ์žฌ๋ผ์ธ ์˜ค๋ฅธ์ชฝ ๋งจ ์œ„ ๊ณ ์ •: H:I๋งŒ ์‚ฌ์šฉ
614
+ approval = {
615
+ (1, 8): "๋‹ด๋‹น์ž",
616
+ (1, 9): "์‹œ์„ค์žฅ",
617
+ (2, 8): "",
618
+ (2, 9): "",
619
+ }
620
+ for (rr, cc), value in approval.items():
621
+ cell = ws.cell(rr, cc)
622
+ cell.value = value
623
+ cell.border = border
624
+ cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
625
+ cell.font = Font(name="Malgun Gothic", size=11, bold=(rr == 1))
626
+ if rr == 1:
627
+ cell.fill = header_fill
628
+
629
+ ws.row_dimensions[1].height = 28
630
+ ws.row_dimensions[2].height = 42
631
+
632
+
633
+ def write_info_row(ws, row, a, b, c, d, e, f, header_fill):
634
+ ws.cell(row, 1).value = a
635
+ ws.merge_cells(start_row=row, start_column=2, end_row=row, end_column=3)
636
+ ws.cell(row, 2).value = b
637
+ ws.cell(row, 4).value = c
638
+ ws.merge_cells(start_row=row, start_column=5, end_row=row, end_column=6)
639
+ ws.cell(row, 5).value = d
640
+ ws.cell(row, 7).value = e
641
+ ws.merge_cells(start_row=row, start_column=8, end_row=row, end_column=9)
642
+ ws.cell(row, 8).value = f
643
+
644
+ for col in [1, 4, 7]:
645
+ ws.cell(row, col).fill = header_fill
646
+ ws.cell(row, col).font = Font(name="Malgun Gothic", size=11, bold=True)
647
+ ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
648
+
649
+ for col in [2, 5, 8]:
650
+ ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
651
+
652
+ ws.row_dimensions[row].height = 30
653
+
654
+
655
+ def write_section(ws, row, title, content, header_fill, height=100):
656
+ ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=9)
657
+ ws.cell(row, 1).value = title
658
+ ws.cell(row, 1).font = Font(name="Malgun Gothic", size=15, bold=True)
659
+ ws.cell(row, 1).alignment = Alignment(horizontal="center", vertical="center")
660
+ ws.cell(row, 1).fill = header_fill
661
+ ws.row_dimensions[row].height = 30
662
+ row += 1
663
+
664
+ ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=9)
665
+ ws.cell(row, 1).value = content
666
+ ws.cell(row, 1).font = Font(name="Malgun Gothic", size=11)
667
+ ws.cell(row, 1).alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
668
+ lines = len(str(content)) // 60 + 1 + str(content).count("\n")
669
+ ws.row_dimensions[row].height = max(height, lines * 18)
670
+ return row + 1
671
+
672
+
673
+ def generate_excel(data):
674
+ wb = Workbook()
675
+ title_fill = PatternFill("solid", fgColor="DBEAFE")
676
+ header_fill = PatternFill("solid", fgColor="F3F4F6")
677
+
678
+ ws = wb.active
679
+ ws.title = "์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€"
680
+ write_title_with_approval(ws, "์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€", title_fill, header_fill)
681
+
682
+ r = 3
683
+ write_info_row(ws, r, "๊ธฐ๊ด€๋ช…", data["institution_name"], "๊ธฐ๊ด€์œ ํ˜•", data["institution_type"], "๊ธฐ๋ก์ผ์ž", data["record_date"], header_fill); r += 1
684
+ write_info_row(ws, r, "์ˆ˜๊ธ‰์ž", data["client_masked_name"], "์žฅ๊ธฐ์š”์–‘๋“ฑ๊ธ‰", data["grade"], "๊ธฐ๋ก๊ตฌ๋ถ„", data["record_type"], header_fill); r += 1
685
+ write_info_row(ws, r, "์ž‘์„ฑ์ž", data["writer"], "์ฃผ๊ฐ„๊ธฐ๊ฐ„", data["week_period"], "์žฌ์ž‘์„ฑํ•„์š”", data["plan_change_required"], header_fill); r += 1
686
+
687
+ r = write_section(ws, r, "์ƒํƒœ๋ณ€ํ™” ๊ธฐ๋ก๋‚ด์šฉ", data["status_text"], header_fill, 240)
688
+ r = write_section(ws, r, "๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ์—ฐ๊ณ„ ํŒ๋‹จ", data["plan_change_reason"], header_fill, 70)
689
+
690
+ # ํ‰๊ฐ€ ์ ํ•ฉ๋„๋Š” ์ถœ๋ ฅ๋ฌผ์—๋Š” ๋ณด์ด์ง€ ์•Š๋„๋ก ๋‚ด๋ถ€์ ๊ฒ€ ์‹œํŠธ๋กœ๋งŒ ๊ด€๋ฆฌ
691
+ internal_ws = wb.create_sheet("๋‚ด๋ถ€์ ๊ฒ€์ž๋ฃŒ_์ถœ๋ ฅ์ œ์™ธ")
692
+ internal_ws.sheet_state = "hidden"
693
+ internal_ws.cell(1, 1).value = "ํ‰๊ฐ€ ์ ํ•ฉ๋„"
694
+ internal_ws.cell(1, 2).value = f"{data['evaluation_score']}์ "
695
+ internal_ws.cell(2, 1).value = "๋ถ€์กฑ ํ•ญ๋ชฉ"
696
+ internal_ws.cell(2, 2).value = data["missing_items"]
697
+ internal_ws.cell(3, 1).value = "์ ๊ฒ€ ๊ฒฐ๊ณผ"
698
+ internal_ws.cell(3, 2).value = data["compliance_result"]
699
+ for row in internal_ws.iter_rows(min_row=1, max_row=3, min_col=1, max_col=2):
700
+ for cell in row:
701
+ cell.font = Font(name="Malgun Gothic", size=10)
702
+ cell.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
703
+
704
+ # ์‚ฌ์ง„์ฒจ๋ถ€
705
+ if data.get("photo_paths"):
706
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
707
+ ws.cell(r, 1).value = "์‚ฌ์ง„์ฒจ๋ถ€"
708
+ ws.cell(r, 1).font = Font(name="Malgun Gothic", size=15, bold=True)
709
+ ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center")
710
+ ws.cell(r, 1).fill = header_fill
711
+ r += 1
712
+
713
+ anchors = ["A", "D", "G"]
714
+ img_row = r
715
+ for idx, photo_path in enumerate(data.get("photo_paths", [])[:3]):
716
+ try:
717
+ img = XLImage(photo_path)
718
+ img.width = 210
719
+ img.height = 150
720
+ ws.add_image(img, f"{anchors[idx]}{img_row}")
721
+ except Exception:
722
+ pass
723
+
724
+ for rr in range(img_row, img_row + 8):
725
+ ws.row_dimensions[rr].height = 22
726
+ r = img_row + 8
727
+
728
+ style_ws(ws, r)
729
+
730
+ output = BytesIO()
731
+ wb.save(output)
732
+ output.seek(0)
733
+ return output
734
+
735
+
736
+ # -----------------------------
737
+ # ํ™”๋ฉด
738
+ # -----------------------------
739
+ menu = st.sidebar.radio("๋ฉ”๋‰ด", ["์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก ์ƒ์„ฑ", "์ €์žฅ๊ธฐ๋ก ๋ณด๊ธฐ"])
740
+
741
+ st.sidebar.success("DB ์—ฐ๊ฒฐ")
742
+ st.sidebar.caption(DB_PATH)
743
+
744
+ clients_df = load_clients()
745
+
746
+ if menu == "์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก ์ƒ์„ฑ":
747
+ if clients_df.empty:
748
+ st.warning("Client Center ์ˆ˜๊ธ‰์ž DB๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋จผ์ € ์ˆ˜๊ธ‰์ž๋ฅผ ๋“ฑ๋กํ•˜์„ธ์š”.")
749
+ st.stop()
750
+
751
+ options = clients_df.apply(
752
+ lambda x: f"{x['id']}๋ฒˆ - {x['masked_name']} / {x['care_grade']} / {x['institution_type']} / {x.get('status','')}",
753
+ axis=1
754
+ ).tolist()
755
+
756
+ selected_option = st.selectbox("์ˆ˜๊ธ‰์ž ์„ ํƒ", options)
757
+ selected_id = int(selected_option.split("๋ฒˆ")[0])
758
+ client = clients_df[clients_df["id"] == selected_id].iloc[0]
759
+
760
+ client_id = int(safe(client, "id", 0) or 0)
761
+ institution_type = safe(client, "institution_type", "๋ฐฉ๋ฌธ์š”์–‘")
762
+ if institution_type not in INSTITUTION_GUIDES:
763
+ institution_type = "๋ฐฉ๋ฌธ์š”์–‘"
764
+
765
+ m1, m2, m3, m4 = st.columns(4)
766
+ m1.metric("์ˆ˜๊ธ‰์ž", safe(client, "masked_name"))
767
+ m2.metric("๋“ฑ๊ธ‰", safe(client, "care_grade"))
768
+ m3.metric("๊ธฐ๊ด€์œ ํ˜•", institution_type)
769
+ m4.metric("์ž‘์„ฑ์ฃผ๊ธฐ", "์ฃผ 1ํšŒ ๊ธฐ๋ณธ")
770
+
771
+ st.info(INSTITUTION_GUIDES[institution_type]["note"])
772
+
773
+ st.markdown("### ๊ธฐ๋ก ๊ธฐ๋ณธ์ •๋ณด")
774
+ col1, col2, col3 = st.columns(3)
775
+
776
+ record_type = col1.radio("๊ธฐ๋ก๊ตฌ๋ถ„", ["์ฃผ๊ฐ„ ์ •๊ธฐ๊ธฐ๋ก", "์ถ”๊ฐ€์ž‘์„ฑ"], horizontal=True)
777
+ record_date = col2.date_input("๊ธฐ๋ก์ผ์ž", value=date.today())
778
+ writer = col3.text_input("์ž‘์„ฑ์ž", "์‚ฌํšŒ๋ณต์ง€์‚ฌ")
779
+
780
+ week_start = record_date - timedelta(days=record_date.weekday())
781
+ week_end = week_start + timedelta(days=6)
782
+ week_period = st.text_input("์ฃผ๊ฐ„๊ธฐ๊ฐ„", value=f"{week_start} ~ {week_end}")
783
+
784
+ st.markdown("### ์ƒํƒœ๋ณ€ํ™” ๋ถ„๋ฅ˜")
785
+ change_category = st.selectbox("์ƒํƒœ๋ณ€ํ™” ๋ถ„๋ฅ˜", CHANGE_CATEGORIES)
786
+
787
+ st.markdown("### ์„ธ๋ถ€ ์ƒํƒœ ์ž…๋ ฅ")
788
+ c1, c2 = st.columns(2)
789
+
790
+ with c1:
791
+ body_status = st.text_area("์‹ ์ฒด์ƒํƒœ ๋ณ€ํ™”", placeholder="์˜ˆ: ๋ณดํ–‰ ์‹œ ํœ˜์ฒญ๊ฑฐ๋ฆผ ์ฆ๊ฐ€, ํ†ต์ฆ ํ˜ธ์†Œ ๋“ฑ", height=90)
792
+ cognitive_status = st.text_area("์ธ์ง€์ƒํƒœ ๋ณ€ํ™”", placeholder="์˜ˆ: ๋ฐ˜๋ณต์งˆ๋ฌธ ์ฆ๊ฐ€, ์‹œ๊ฐ„/์žฅ์†Œ ํ˜ผ๋™ ๋“ฑ", height=90)
793
+ emotion_status = st.text_area("์ •์„œ์ƒํƒœ ๋ณ€ํ™”", placeholder="์˜ˆ: ๋ถˆ์•ˆ, ์šฐ์šธ๊ฐ, ์งœ์ฆ ์ฆ๊ฐ€ ๋“ฑ", height=90)
794
+ diet_status = st.text_area("์‹์‚ฌ์ƒํƒœ ๋ณ€ํ™”", placeholder="์˜ˆ: ์‹์‚ฌ๋Ÿ‰ ๊ฐ์†Œ, ์‚ฌ๋ ˆ, ์—ฐํ•˜๊ณค๋ž€ ๋“ฑ", height=90)
795
+ toileting_status = st.text_area("๋ฐฐ๋ณ€/๋ฐฐ๋‡จ ๋ณ€ํ™”", placeholder="์˜ˆ: ๋ณ€๋น„, ์„ค์‚ฌ, ์‹ค๊ธˆ, ๊ธฐ์ €๊ท€ ์‚ฌ์šฉ ์ฆ๊ฐ€ ๋“ฑ", height=90)
796
+
797
+ with c2:
798
+ mobility_status = st.text_area("์ด๋™์ƒํƒœ ๋ณ€ํ™”", placeholder="์˜ˆ: ๋ถ€์ถ• ํ•„์š” ์ฆ๊ฐ€, ํœ ์ฒด์–ด ์ด๋™์œผ๋กœ ๋ณ€๊ฒฝ ๋“ฑ", height=90)
799
+ skin_status = st.text_area("ํ”ผ๋ถ€์ƒํƒœ ๋ณ€ํ™”", placeholder="์˜ˆ: ๋ฐœ์ , ๏ฟฝ๏ฟฝ์ฒ˜, ์š•์ฐฝ ์œ„ํ—˜ ๋“ฑ", height=90)
800
+ sleep_status = st.text_area("์ˆ˜๋ฉด์ƒํƒœ ๋ณ€ํ™”", placeholder="์˜ˆ: ์•ผ๊ฐ„๊ฐ์„ฑ, ๋‚ฎ์ž  ์ฆ๊ฐ€ ๋“ฑ", height=90)
801
+ program_status = st.text_area("ํ”„๋กœ๊ทธ๋žจ/ํ™œ๋™ ๋ณ€ํ™”", placeholder="์˜ˆ: ์ฐธ์—ฌ ์ €์กฐ, ํฅ๋ฏธ ์ €ํ•˜ ๋“ฑ", height=90)
802
+ guardian_opinion = st.text_area("๋ณดํ˜ธ์ž ์˜๊ฒฌ", placeholder="์˜ˆ: ๋ณดํ˜ธ์ž๊ฐ€ ์‹์‚ฌ๋Ÿ‰ ๊ฐ์†Œ๋ฅผ ๊ฑฑ์ •ํ•จ ๋“ฑ", height=90)
803
+
804
+ staff_observation = st.text_area("์ข…์‚ฌ์ž ๊ด€์ฐฐ์‚ฌํ•ญ", placeholder="์˜ˆ: ์š”์–‘๋ณดํ˜ธ์‚ฌ/๊ฐ„ํ˜ธ์กฐ๋ฌด์‚ฌ๊ฐ€ ๊ด€์ฐฐํ•œ ๋ณ€ํ™”", height=90)
805
+ action_taken = st.text_area("์กฐ์น˜๋‚ด์šฉ", placeholder="์˜ˆ: ๋ณดํ˜ธ์ž์—๊ฒŒ ๊ณต์œ , ๋ณ‘์›์ง„๋ฃŒ ๊ถŒ์œ , ํ™˜๊ฒฝ์ •๋ฆฌ, ์‹์‚ฌ๋ณด์กฐ ๊ฐ•ํ™” ๋“ฑ", height=90)
806
+ followup_plan = st.text_area("ํ›„์†๊ณ„ํš", placeholder="์˜ˆ: ๋‹ค์Œ ์ฃผ ์žฌํ™•์ธ, ์ƒ๋‹ด์ผ์ง€ ์ž‘์„ฑ, ์š•๊ตฌ์‚ฌ์ • ์žฌ์ ๊ฒ€, ๊ธ‰์—ฌ๊ณ„ํš ๋ณ€๊ฒฝ ๊ฒ€ํ†  ๋“ฑ", height=90)
807
+
808
+ photos = st.file_uploader("์‚ฌ์ง„์ฒจ๋ถ€ (์ตœ๋Œ€ 3์žฅ, ์„ ํƒ์‚ฌํ•ญ)", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
809
+
810
+ required, plan_reason = determine_plan_change(
811
+ change_category, body_status, cognitive_status, diet_status,
812
+ mobility_status, skin_status, guardian_opinion, action_taken
813
+ )
814
+
815
+ st.markdown("### ๊ธ‰์—ฌ์ œ๊ณต๊ณ„ํš ์—ฐ๊ณ„ ํŒ๋‹จ")
816
+ p1, p2 = st.columns(2)
817
+ p1.metric("๊ธ‰์—ฌ๊ณ„ํš ์žฌ์ž‘์„ฑ ํ•„์š”", required)
818
+ p2.metric("์—ฐ๋™๋ฌธ์„œ", "์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€")
819
+
820
+ if required == "์˜ˆ":
821
+ st.markdown(f'<div class="dangerbox">{plan_reason}</div>', unsafe_allow_html=True)
822
+ elif required == "๊ฒ€ํ† ":
823
+ st.markdown(f'<div class="warnbox">{plan_reason}</div>', unsafe_allow_html=True)
824
+ else:
825
+ st.markdown(f'<div class="okbox">{plan_reason}</div>', unsafe_allow_html=True)
826
+
827
+ if st.button("๐Ÿฉบ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก ์ƒ์„ฑ", use_container_width=True):
828
+ status_text = build_status_text(
829
+ client, institution_type, record_type, change_category,
830
+ body_status, cognitive_status, emotion_status, diet_status,
831
+ toileting_status, mobility_status, skin_status, sleep_status,
832
+ program_status, guardian_opinion, staff_observation, action_taken, followup_plan
833
+ )
834
+
835
+ score, missing, compliance = evaluate_record(
836
+ record_type, change_category, status_text, guardian_opinion,
837
+ staff_observation, action_taken, followup_plan, required
838
+ )
839
+
840
+ photo_paths = save_uploaded_photos(photos)
841
+
842
+ st.session_state["status_data"] = {
843
+ "client_id": client_id,
844
+ "client_masked_name": safe(client, "masked_name"),
845
+ "institution_name": safe(client, "institution_name"),
846
+ "institution_type": institution_type,
847
+ "grade": safe(client, "care_grade"),
848
+ "record_type": record_type,
849
+ "record_date": str(record_date),
850
+ "week_period": week_period,
851
+ "writer": writer,
852
+ "change_category": change_category,
853
+ "body_status": body_status,
854
+ "cognitive_status": cognitive_status,
855
+ "emotion_status": emotion_status,
856
+ "diet_status": diet_status,
857
+ "toileting_status": toileting_status,
858
+ "mobility_status": mobility_status,
859
+ "skin_status": skin_status,
860
+ "sleep_status": sleep_status,
861
+ "program_status": program_status,
862
+ "guardian_opinion": guardian_opinion,
863
+ "staff_observation": staff_observation,
864
+ "action_taken": action_taken,
865
+ "followup_plan": followup_plan,
866
+ "status_text": status_text,
867
+ "plan_change_required": required,
868
+ "plan_change_reason": plan_reason,
869
+ "evaluation_score": score,
870
+ "missing_items": ", ".join(missing) if missing else "๋ถ€์กฑ ํ•ญ๋ชฉ ์—†์Œ",
871
+ "compliance_result": compliance,
872
+ "photo_paths": photo_paths,
873
+ "photo_count": len(photo_paths),
874
+ }
875
+
876
+ if "status_data" in st.session_state:
877
+ data = st.session_state["status_data"]
878
+
879
+ st.markdown("---")
880
+ st.subheader("์ƒ์„ฑ ๊ฒฐ๊ณผ ์ˆ˜์ •")
881
+ data["status_text"] = st.text_area("์ƒํƒœ๋ณ€ํ™” ๊ธฐ๋ก๋‚ด์šฉ", value=data["status_text"], height=320)
882
+ data["plan_change_reason"] = st.text_area("๊ธ‰์—ฌ๊ณ„ํš ์—ฐ๊ณ„ ํŒ๋‹จ ๋ฌธ๊ตฌ", value=data["plan_change_reason"], height=90)
883
+
884
+ score, missing, compliance = evaluate_record(
885
+ data["record_type"], data["change_category"], data["status_text"],
886
+ data["guardian_opinion"], data["staff_observation"], data["action_taken"],
887
+ data["followup_plan"], data["plan_change_required"]
888
+ )
889
+
890
+ data["evaluation_score"] = score
891
+ data["missing_items"] = ", ".join(missing) if missing else "๋ถ€์กฑ ํ•ญ๋ชฉ ์—†์Œ"
892
+ data["compliance_result"] = compliance
893
+
894
+ st.subheader("ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€")
895
+ e1, e2, e3 = st.columns(3)
896
+ e1.metric("ํ‰๊ฐ€ ์ ํ•ฉ๋„", f"{score}์ ")
897
+ e2.metric("๋ถ€์กฑ ํ•ญ๋ชฉ", data["missing_items"])
898
+ e3.metric("์žฌ์ž‘์„ฑ ํ•„์š”", data["plan_change_required"])
899
+
900
+ if score >= 90:
901
+ st.markdown(f'<div class="okbox">{compliance}</div>', unsafe_allow_html=True)
902
+ else:
903
+ st.markdown(f'<div class="warnbox">{compliance}</div>', unsafe_allow_html=True)
904
+
905
+ if st.button("๐Ÿ’พ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก DB ์ €์žฅ ๋ฐ Client Center ๋ฐ˜์˜", use_container_width=True):
906
+ save_status_record(data)
907
+ save_plan_change_log(
908
+ data["client_id"],
909
+ data["client_masked_name"],
910
+ data["institution_type"],
911
+ data["plan_change_reason"],
912
+ data["record_date"],
913
+ data["plan_change_required"],
914
+ data["plan_change_reason"]
915
+ )
916
+ st.success("์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ด ์ €์žฅ๋˜์—ˆ๊ณ  Client Center ๊ธ‰์—ฌ๊ณ„ํš ๋ณ€๊ฒฝ๊ด€๋ฆฌ์—๋„ ๋ฐ˜์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
917
+
918
+ filename = f"์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€_{data['client_masked_name']}_{data['record_date']}.xlsx"
919
+ st.download_button(
920
+ "๐Ÿ“ฅ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์—‘์…€ ๋‹ค์šด๋กœ๋“œ",
921
+ data=generate_excel(data),
922
+ file_name=filename,
923
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
924
+ use_container_width=True,
925
+ )
926
+
927
+ elif menu == "์ €์žฅ๊ธฐ๋ก ๋ณด๊ธฐ":
928
+ st.subheader("๐Ÿ“š ์ €์žฅ๋œ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก")
929
+ df = load_status_records()
930
+ if df.empty:
931
+ st.info("์ €์žฅ๋œ ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค.")
932
+ else:
933
+ st.dataframe(df, use_container_width=True)
934
+
935
+ st.caption("Client Center ์—ฐ๋™ํ˜• ์ƒํƒœ๋ณ€ํ™”๊ธฐ๋ก์ง€ ์ƒ์„ฑ๊ธฐ")