devorah-ai-2016 commited on
Commit
88b52e6
·
verified ·
1 Parent(s): b1b6f49

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +450 -0
app.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sqlite3
3
+ import pandas as pd
4
+ from datetime import datetime
5
+ import os
6
+
7
+ DB_DIR = "data"
8
+ DB_PATH = os.path.join(DB_DIR, "clients.db")
9
+
10
+ os.makedirs(DB_DIR, exist_ok=True)
11
+
12
+ st.set_page_config(
13
+ page_title="Client Center",
14
+ page_icon="👥",
15
+ layout="wide"
16
+ )
17
+
18
+ # -----------------------------
19
+ # DB 연결 및 테이블 생성
20
+ # -----------------------------
21
+ def get_conn():
22
+ return sqlite3.connect(DB_PATH, check_same_thread=False)
23
+
24
+ def init_db():
25
+ conn = get_conn()
26
+ cur = conn.cursor()
27
+ cur.execute("""
28
+ CREATE TABLE IF NOT EXISTS clients (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ name TEXT NOT NULL,
31
+ masked_name TEXT,
32
+ gender TEXT,
33
+ birth_year TEXT,
34
+ care_grade TEXT,
35
+ institution_type TEXT,
36
+ institution_name TEXT,
37
+ admission_date TEXT,
38
+ main_condition TEXT,
39
+ cognitive_status TEXT,
40
+ mobility_status TEXT,
41
+ diet_status TEXT,
42
+ toileting_status TEXT,
43
+ guardian_name TEXT,
44
+ guardian_phone TEXT,
45
+ notes TEXT,
46
+ status TEXT DEFAULT '이용중',
47
+ created_at TEXT,
48
+ updated_at TEXT
49
+ )
50
+ """)
51
+ conn.commit()
52
+ conn.close()
53
+
54
+ def mask_name(name):
55
+ if not name:
56
+ return ""
57
+ name = name.strip()
58
+ if len(name) >= 3:
59
+ return name[0] + "○" + name[2:]
60
+ elif len(name) == 2:
61
+ return name[0] + "○"
62
+ return name
63
+
64
+ init_db()
65
+
66
+ # -----------------------------
67
+ # CSS 디자인
68
+ # -----------------------------
69
+ st.markdown("""
70
+ <style>
71
+ .main {
72
+ background-color: #f8fafc;
73
+ }
74
+ .block-container {
75
+ padding-top: 2rem;
76
+ }
77
+ .title-box {
78
+ background: linear-gradient(135deg, #2E7D32, #66BB6A);
79
+ padding: 28px;
80
+ border-radius: 18px;
81
+ color: white;
82
+ text-align: center;
83
+ margin-bottom: 25px;
84
+ }
85
+ .title-box h1 {
86
+ margin: 0;
87
+ font-size: 34px;
88
+ }
89
+ .title-box p {
90
+ font-size: 17px;
91
+ margin-top: 8px;
92
+ }
93
+ .card {
94
+ background: white;
95
+ padding: 22px;
96
+ border-radius: 16px;
97
+ border: 1px solid #e5e7eb;
98
+ box-shadow: 0 4px 10px rgba(0,0,0,0.04);
99
+ margin-bottom: 16px;
100
+ }
101
+ .notice {
102
+ background: #fff8e1;
103
+ padding: 15px;
104
+ border-left: 6px solid #f9a825;
105
+ border-radius: 10px;
106
+ margin-bottom: 20px;
107
+ }
108
+ </style>
109
+ """, unsafe_allow_html=True)
110
+
111
+ st.markdown("""
112
+ <div class="title-box">
113
+ <h1>👥 Client Center</h1>
114
+ <p>수급자·입소자 등록 / 검색 / 수정 / 엑셀 업로드 관리센터</p>
115
+ </div>
116
+ """, unsafe_allow_html=True)
117
+
118
+ st.markdown("""
119
+ <div class="notice">
120
+ <strong>안내:</strong> 생년월일 전체 대신 출생연도 중심으로 입력하고, 이름은 자동으로 홍○동 형식으로 가명 처리됩니다.
121
+ </div>
122
+ """, unsafe_allow_html=True)
123
+
124
+ menu = st.sidebar.radio(
125
+ "메뉴 선택",
126
+ ["수급자 등록", "수급자 목록", "수급자 검색", "엑셀 업로드", "수정 / 삭제"]
127
+ )
128
+
129
+ # -----------------------------
130
+ # 등록
131
+ # -----------------------------
132
+ if menu == "수급자 등록":
133
+ st.subheader("📝 수급자 직접 등록")
134
+
135
+ with st.form("client_form"):
136
+ col1, col2, col3 = st.columns(3)
137
+
138
+ with col1:
139
+ name = st.text_input("수급자 이름")
140
+ gender = st.selectbox("성별", ["여", "남", "기타"])
141
+ birth_year = st.text_input("출생연도", placeholder="예: 1942")
142
+
143
+ with col2:
144
+ care_grade = st.selectbox(
145
+ "장기요양등급",
146
+ ["1등급", "2등급", "3등급", "4등급", "5등급", "인지지원등급", "등급외", "미입력"]
147
+ )
148
+ institution_type = st.selectbox(
149
+ "기관 유형",
150
+ ["방문요양", "방문목욕", "방문간호", "주간보호", "요양원", "공동생활가정"]
151
+ )
152
+ institution_name = st.text_input("기관명")
153
+
154
+ with col3:
155
+ admission_date = st.date_input("이용 시작일 / 입소일")
156
+ status = st.selectbox("상태", ["이용중", "입소중", "퇴소", "중지", "사망"])
157
+
158
+ st.markdown("### 건강 및 생활 상태")
159
+
160
+ col4, col5 = st.columns(2)
161
+
162
+ with col4:
163
+ main_condition = st.text_area("주요 질환 / 상태", placeholder="예: 치매, 고혈압, 당뇨, 낙상위험 등")
164
+ cognitive_status = st.text_area("인지 상태", placeholder="예: 단기기억 저하, 지남력 저하 등")
165
+ mobility_status = st.text_area("이동 상태", placeholder="예: 워커 사용, 부축 필요, 휠체어 사용 등")
166
+
167
+ with col5:
168
+ diet_status = st.text_area("식사 상태", placeholder="예: 일반식, 죽식, 연하곤란, 식사보조 필요 등")
169
+ toileting_status = st.text_area("배변 / 배뇨 상태", placeholder="예: 기저귀 착용, 화장실 유도 필요 등")
170
+ notes = st.text_area("특이사항 / 메모")
171
+
172
+ st.markdown("### 보호자 정보")
173
+
174
+ col6, col7 = st.columns(2)
175
+ with col6:
176
+ guardian_name = st.text_input("보호자 이름")
177
+ with col7:
178
+ guardian_phone = st.text_input("보호자 연락처")
179
+
180
+ submitted = st.form_submit_button("등록하기")
181
+
182
+ if submitted:
183
+ if not name:
184
+ st.error("수급자 이름은 필수입니다.")
185
+ else:
186
+ conn = get_conn()
187
+ cur = conn.cursor()
188
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
189
+
190
+ cur.execute("""
191
+ INSERT INTO clients (
192
+ name, masked_name, gender, birth_year, care_grade,
193
+ institution_type, institution_name, admission_date,
194
+ main_condition, cognitive_status, mobility_status,
195
+ diet_status, toileting_status, guardian_name,
196
+ guardian_phone, notes, status, created_at, updated_at
197
+ )
198
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
199
+ """, (
200
+ name,
201
+ mask_name(name),
202
+ gender,
203
+ birth_year,
204
+ care_grade,
205
+ institution_type,
206
+ institution_name,
207
+ str(admission_date),
208
+ main_condition,
209
+ cognitive_status,
210
+ mobility_status,
211
+ diet_status,
212
+ toileting_status,
213
+ guardian_name,
214
+ guardian_phone,
215
+ notes,
216
+ status,
217
+ now,
218
+ now
219
+ ))
220
+
221
+ conn.commit()
222
+ conn.close()
223
+ st.success(f"{mask_name(name)} 수급자가 등록되었습니다.")
224
+
225
+ # -----------------------------
226
+ # 목록
227
+ # -----------------------------
228
+ elif menu == "수급자 목록":
229
+ st.subheader("📋 수급자 목록")
230
+
231
+ conn = get_conn()
232
+ df = pd.read_sql_query("SELECT * FROM clients ORDER BY id DESC", conn)
233
+ conn.close()
234
+
235
+ if df.empty:
236
+ st.info("등록된 수급자가 없습니다.")
237
+ else:
238
+ show_cols = [
239
+ "id", "masked_name", "gender", "birth_year", "care_grade",
240
+ "institution_type", "institution_name", "status", "admission_date"
241
+ ]
242
+ st.dataframe(df[show_cols], use_container_width=True)
243
+
244
+ # -----------------------------
245
+ # 검색
246
+ # -----------------------------
247
+ elif menu == "수급자 검색":
248
+ st.subheader("🔍 수급자 검색")
249
+
250
+ keyword = st.text_input("이름, 기관명, 등급, 상태로 검색")
251
+
252
+ if keyword:
253
+ conn = get_conn()
254
+ query = """
255
+ SELECT * FROM clients
256
+ WHERE name LIKE ?
257
+ OR masked_name LIKE ?
258
+ OR institution_name LIKE ?
259
+ OR care_grade LIKE ?
260
+ OR status LIKE ?
261
+ ORDER BY id DESC
262
+ """
263
+ like_keyword = f"%{keyword}%"
264
+ df = pd.read_sql_query(
265
+ query,
266
+ conn,
267
+ params=(like_keyword, like_keyword, like_keyword, like_keyword, like_keyword)
268
+ )
269
+ conn.close()
270
+
271
+ if df.empty:
272
+ st.warning("검색 결과가 없습니다.")
273
+ else:
274
+ st.dataframe(df, use_container_width=True)
275
+
276
+ # -----------------------------
277
+ # 엑셀 업로드
278
+ # -----------------------------
279
+ elif menu == "엑셀 업로드":
280
+ st.subheader("📤 엑셀 업로드")
281
+
282
+ st.info("""
283
+ 엑셀 권장 컬럼:
284
+ 이름, 성별, 출생연도, 장기요양등급, 기관유형, 기관명, 이용시작일, 주요상태, 보호자명, 보호자연락처
285
+ """)
286
+
287
+ uploaded_file = st.file_uploader("엑셀 파일 업로드", type=["xlsx", "xls"])
288
+
289
+ if uploaded_file:
290
+ try:
291
+ df = pd.read_excel(uploaded_file)
292
+ st.write("업로드된 엑셀 미리보기")
293
+ st.dataframe(df, use_container_width=True)
294
+
295
+ if st.button("DB에 저장하기"):
296
+ conn = get_conn()
297
+ cur = conn.cursor()
298
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
299
+
300
+ for _, row in df.iterrows():
301
+ name = str(row.get("이름", "")).strip()
302
+ if not name:
303
+ continue
304
+
305
+ cur.execute("""
306
+ INSERT INTO clients (
307
+ name, masked_name, gender, birth_year, care_grade,
308
+ institution_type, institution_name, admission_date,
309
+ main_condition, guardian_name, guardian_phone,
310
+ status, created_at, updated_at
311
+ )
312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
313
+ """, (
314
+ name,
315
+ mask_name(name),
316
+ str(row.get("성별", "")),
317
+ str(row.get("출생연도", "")),
318
+ str(row.get("장기요양등급", "")),
319
+ str(row.get("기관유형", "")),
320
+ str(row.get("기관명", "")),
321
+ str(row.get("이용시작일", "")),
322
+ str(row.get("주요상태", "")),
323
+ str(row.get("보호자명", "")),
324
+ str(row.get("보호자연락처", "")),
325
+ "이용중",
326
+ now,
327
+ now
328
+ ))
329
+
330
+ conn.commit()
331
+ conn.close()
332
+ st.success("엑셀 자료가 DB에 저장되었습니다.")
333
+
334
+ except Exception as e:
335
+ st.error(f"엑셀 업로드 중 오류가 발생했습니다: {e}")
336
+
337
+ # -----------------------------
338
+ # 수정 / 삭제
339
+ # -----------------------------
340
+ elif menu == "수정 / 삭제":
341
+ st.subheader("✏️ 수급자 정보 수정 / 삭제")
342
+
343
+ conn = get_conn()
344
+ df = pd.read_sql_query("SELECT id, name, masked_name, care_grade, institution_name FROM clients ORDER BY id DESC", conn)
345
+ conn.close()
346
+
347
+ if df.empty:
348
+ st.info("수정할 수급자가 없습니다.")
349
+ else:
350
+ options = df.apply(
351
+ lambda x: f"{x['id']}번 - {x['masked_name']} / {x['care_grade']} / {x['institution_name']}",
352
+ axis=1
353
+ ).tolist()
354
+
355
+ selected = st.selectbox("수정할 수급자 선택", options)
356
+ selected_id = int(selected.split("번")[0])
357
+
358
+ conn = get_conn()
359
+ client = pd.read_sql_query("SELECT * FROM clients WHERE id = ?", conn, params=(selected_id,))
360
+ conn.close()
361
+
362
+ if not client.empty:
363
+ c = client.iloc[0]
364
+
365
+ with st.form("edit_form"):
366
+ col1, col2, col3 = st.columns(3)
367
+
368
+ with col1:
369
+ name = st.text_input("수급자 이름", value=c["name"])
370
+ gender = st.selectbox("성별", ["여", "남", "기타"], index=["여", "남", "기타"].index(c["gender"]) if c["gender"] in ["여", "남", "기타"] else 0)
371
+ birth_year = st.text_input("출생연도", value=c["birth_year"])
372
+
373
+ with col2:
374
+ grade_options = ["1등급", "2등급", "3등급", "4등급", "5등급", "인지지원등급", "등급외", "미입력"]
375
+ care_grade = st.selectbox(
376
+ "장기요양등급",
377
+ grade_options,
378
+ index=grade_options.index(c["care_grade"]) if c["care_grade"] in grade_options else 7
379
+ )
380
+ institution_type = st.text_input("기관 유형", value=c["institution_type"])
381
+ institution_name = st.text_input("기관명", value=c["institution_name"])
382
+
383
+ with col3:
384
+ admission_date = st.text_input("이용 시작일 / 입소일", value=c["admission_date"])
385
+ status = st.selectbox(
386
+ "상태",
387
+ ["이용중", "입소중", "퇴소", "중지", "사망"],
388
+ index=["이용중", "입소중", "퇴소", "중지", "사망"].index(c["status"]) if c["status"] in ["이용중", "입소중", "퇴소", "중지", "사망"] else 0
389
+ )
390
+
391
+ main_condition = st.text_area("주요 질환 / 상태", value=c["main_condition"])
392
+ cognitive_status = st.text_area("인지 상태", value=c["cognitive_status"])
393
+ mobility_status = st.text_area("이동 상태", value=c["mobility_status"])
394
+ diet_status = st.text_area("식사 상태", value=c["diet_status"])
395
+ toileting_status = st.text_area("배변 / 배뇨 상태", value=c["toileting_status"])
396
+
397
+ guardian_name = st.text_input("보호자 이름", value=c["guardian_name"])
398
+ guardian_phone = st.text_input("보호자 연락처", value=c["guardian_phone"])
399
+ notes = st.text_area("특이사항 / 메모", value=c["notes"])
400
+
401
+ save_btn = st.form_submit_button("수정 저장")
402
+
403
+ if save_btn:
404
+ conn = get_conn()
405
+ cur = conn.cursor()
406
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
407
+
408
+ cur.execute("""
409
+ UPDATE clients
410
+ SET name=?, masked_name=?, gender=?, birth_year=?, care_grade=?,
411
+ institution_type=?, institution_name=?, admission_date=?,
412
+ main_condition=?, cognitive_status=?, mobility_status=?,
413
+ diet_status=?, toileting_status=?, guardian_name=?,
414
+ guardian_phone=?, notes=?, status=?, updated_at=?
415
+ WHERE id=?
416
+ """, (
417
+ name,
418
+ mask_name(name),
419
+ gender,
420
+ birth_year,
421
+ care_grade,
422
+ institution_type,
423
+ institution_name,
424
+ admission_date,
425
+ main_condition,
426
+ cognitive_status,
427
+ mobility_status,
428
+ diet_status,
429
+ toileting_status,
430
+ guardian_name,
431
+ guardian_phone,
432
+ notes,
433
+ status,
434
+ now,
435
+ selected_id
436
+ ))
437
+
438
+ conn.commit()
439
+ conn.close()
440
+ st.success("수급자 정보가 수정되었습니다.")
441
+
442
+ st.divider()
443
+
444
+ if st.button("⚠️ 이 수급자 삭제하기"):
445
+ conn = get_conn()
446
+ cur = conn.cursor()
447
+ cur.execute("DELETE FROM clients WHERE id = ?", (selected_id,))
448
+ conn.commit()
449
+ conn.close()
450
+ st.warning("수급자 정보가 삭제되었습니다.")