devorah-ai-2016 commited on
Commit
57ea634
ยท
verified ยท
1 Parent(s): 8ffd02d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +758 -0
app.py ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ import random
4
+ import tempfile
5
+ from io import BytesIO
6
+ from datetime import date, datetime
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
+ # ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€ ํ†ตํ•ฉ์—ฐ๋™ ์ƒ์„ฑ๊ธฐ
18
+ # - Client Center DB์™€ ๊ฐ™์€ clients.db ์‚ฌ์šฉ
19
+ # - ๊ธฐ๊ด€์ •๋ณด/์ฐธ์„์ž(์ง์ข…๋ณ„ ์ž…๋ ฅ)/๊ฐ•์‚ฌ/๊ต์œก์ž๋ฃŒ ์ฒจ๋ถ€/๊ต์œก๊ฒฐ๊ณผ/ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€
20
+ # - ๊ฒฐ์žฌ๋ผ์ธ ๋‹ด๋‹น์ž/์‹œ์„ค์žฅ 2์นธ, ์ง€์ €๋ถ„ํ•œ ๋นˆ์นธ ์—†์ด ์ถœ๋ ฅ
21
+ # =========================================================
22
+
23
+ st.set_page_config(
24
+ page_title="๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€",
25
+ page_icon="๐Ÿ“˜",
26
+ layout="wide"
27
+ )
28
+
29
+ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
30
+
31
+ DB_CANDIDATES = [
32
+ os.path.join(CURRENT_DIR, "data", "clients.db"),
33
+ os.path.join(CURRENT_DIR, "..", "Client Center", "data", "clients.db"),
34
+ os.path.join(CURRENT_DIR, "..", "ClientCenter", "data", "clients.db"),
35
+ os.path.join(CURRENT_DIR, "..", "Client_Center", "data", "clients.db"),
36
+ ]
37
+
38
+ DB_PATH = None
39
+ for p in DB_CANDIDATES:
40
+ if os.path.exists(p):
41
+ DB_PATH = p
42
+ break
43
+
44
+ if DB_PATH is None:
45
+ os.makedirs(os.path.join(CURRENT_DIR, "data"), exist_ok=True)
46
+ DB_PATH = os.path.join(CURRENT_DIR, "data", "clients.db")
47
+
48
+
49
+ # -----------------------------
50
+ # ๋””์ž์ธ
51
+ # -----------------------------
52
+ st.markdown("""
53
+ <style>
54
+ .block-container { padding-top: 2rem; }
55
+ .hero {
56
+ background: linear-gradient(135deg, #7C2D12, #EA580C);
57
+ padding: 30px;
58
+ border-radius: 22px;
59
+ text-align: center;
60
+ margin-bottom: 22px;
61
+ }
62
+ .hero h1 { color: white; margin: 0; font-size: 36px; }
63
+ .hero p { color: white; margin-top: 10px; font-size: 18px; }
64
+ .notice {
65
+ background: #fff8e1;
66
+ border-left: 7px solid #f9a825;
67
+ padding: 15px;
68
+ border-radius: 12px;
69
+ margin-bottom: 18px;
70
+ }
71
+ .okbox {
72
+ background: #e8f5e9;
73
+ border-left: 7px solid #43a047;
74
+ padding: 14px;
75
+ border-radius: 12px;
76
+ margin-bottom: 14px;
77
+ }
78
+ .warnbox {
79
+ background: #fff3e0;
80
+ border-left: 7px solid #fb8c00;
81
+ padding: 14px;
82
+ border-radius: 12px;
83
+ margin-bottom: 14px;
84
+ }
85
+ </style>
86
+ """, unsafe_allow_html=True)
87
+
88
+ st.markdown("""
89
+ <div class="hero">
90
+ <h1>๐Ÿ“˜ ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€ ์ƒ์„ฑ๊ธฐ</h1>
91
+ <p>์ข…์‚ฌ์ž ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ง์ข…๋ณ„ ์ฐธ์„ ๋ช…๋‹จ๊ณผ ์ด์ˆ˜๊ธฐ๋ก์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.</p>
92
+ </div>
93
+ """, unsafe_allow_html=True)
94
+
95
+ st.markdown("""
96
+ <div class="notice">
97
+ <strong>ํ•„์ˆ˜ ๊ตฌ์กฐ:</strong> ๊ต์œก๋ช…, ๊ต์œก์ผ์‹œ, ์žฅ์†Œ, ๊ฐ•์‚ฌ๋ช…, ์ง์ข…๋ณ„ ์ฐธ์„์ž ๋ช…๋‹จ, ๊ต์œก๋ชฉ์ , ๊ต์œก๋‚ด์šฉ, ๊ต์œก์ž๋ฃŒ ์ฒจ๋ถ€, ๊ต์œก๊ฒฐ๊ณผ, ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€, ๊ฒฐ์žฌ๋ผ์ธ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
98
+ </div>
99
+ """, unsafe_allow_html=True)
100
+
101
+
102
+ # -----------------------------
103
+ # DB
104
+ # -----------------------------
105
+ def get_conn():
106
+ return sqlite3.connect(DB_PATH, check_same_thread=False)
107
+
108
+ def init_db():
109
+ conn = get_conn()
110
+ cur = conn.cursor()
111
+
112
+ cur.execute("""
113
+ CREATE TABLE IF NOT EXISTS education_records (
114
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
115
+ institution_name TEXT,
116
+ institution_type TEXT,
117
+ education_title TEXT,
118
+ guideline_items TEXT,
119
+ education_date TEXT,
120
+ education_time TEXT,
121
+ education_place TEXT,
122
+ instructor_name TEXT,
123
+ instructor_position TEXT,
124
+ attendees TEXT,
125
+ absent_staff TEXT,
126
+ education_method TEXT,
127
+ education_purpose TEXT,
128
+ education_content TEXT,
129
+ education_materials TEXT,
130
+ education_result TEXT,
131
+ followup_plan TEXT,
132
+ evaluation_score INTEGER,
133
+ missing_items TEXT,
134
+ compliance_result TEXT,
135
+ created_at TEXT
136
+ )
137
+ """)
138
+
139
+ conn.commit()
140
+ conn.close()
141
+
142
+ def save_education_record(data):
143
+ conn = get_conn()
144
+ cur = conn.cursor()
145
+ cur.execute("""
146
+ INSERT INTO education_records (
147
+ institution_name, institution_type, education_title, guideline_items,
148
+ education_date, education_time, education_place,
149
+ instructor_name, instructor_position, attendees, absent_staff,
150
+ education_method, education_purpose, education_content,
151
+ education_materials, education_result, followup_plan,
152
+ evaluation_score, missing_items, compliance_result, created_at
153
+ )
154
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
155
+ """, (
156
+ data["institution_name"], data["institution_type"], data["education_title"],
157
+ data["guideline_items"], data["education_date"], data["education_time"],
158
+ data["education_place"], data["instructor_name"], data["instructor_position"],
159
+ data["attendees"], data["absent_staff"], data["education_method"],
160
+ data["education_purpose"], data["education_content"], data["education_materials"],
161
+ data["education_result"], data["followup_plan"], data["evaluation_score"],
162
+ data["missing_items"], data["compliance_result"], datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
163
+ ))
164
+ conn.commit()
165
+ conn.close()
166
+
167
+ def load_education_records():
168
+ conn = get_conn()
169
+ try:
170
+ df = pd.read_sql_query("SELECT * FROM education_records ORDER BY id DESC", conn)
171
+ except Exception:
172
+ df = pd.DataFrame()
173
+ conn.close()
174
+ return df
175
+
176
+ init_db()
177
+
178
+
179
+ # -----------------------------
180
+ # ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ
181
+ # -----------------------------
182
+ GUIDELINES = {
183
+ "01. ์ข…์‚ฌ์ž ์œค๋ฆฌ์ง€์นจ": {
184
+ "purpose": "์ข…์‚ฌ์ž์˜ ์ง์—…์œค๋ฆฌ์™€ ์ˆ˜๊ธ‰์ž ์กด์ค‘ ํƒœ๋„๋ฅผ ๊ฐ•ํ™”ํ•˜์—ฌ ์ „๋ฌธ์ ์ธ ๊ธ‰์—ฌ์ œ๊ณต์ด ์ด๋ฃจ์–ด์ง€๋„๋ก ํ•œ๋‹ค.",
185
+ "content": ["์ˆ˜๊ธ‰์ž ์ธ๊ฒฉ ์กด์ค‘๊ณผ ์‚ฌ์ƒํ™œ ๋ณดํ˜ธ", "๊ธ‰์—ฌ์ œ๊ณต ๋ฒ”์œ„ ์ค€์ˆ˜์™€ ๋ถ€๋‹น์š”๊ตฌ ๋Œ€์‘", "์ „๋ฌธ์ง ์ข…์‚ฌ์ž๋กœ์„œ์˜ ์ฑ…์ž„๊ณผ ๋ณด๊ณ ์ฒด๊ณ„", "์„œ๋น„์Šค ์ œ๊ณต ์ค‘ ๋น„๋ฐ€์œ ์ง€์™€ ๊ณต์ •ํ•œ ํƒœ๋„"],
186
+ },
187
+ "02. ๋…ธ์ธ์ธ๊ถŒ๋ณดํ˜ธ์ง€์นจ": {
188
+ "purpose": "์ˆ˜๊ธ‰์ž์˜ ๊ถŒ๋ฆฌ์™€ ์ž๊ธฐ๊ฒฐ์ •๊ถŒ์„ ์กด์ค‘ํ•˜๊ณ  ์ธ๊ถŒ์นจํ•ด๋ฅผ ์˜ˆ๋ฐฉํ•œ๋‹ค.",
189
+ "content": ["๋…ธ์ธ ์ธ๊ถŒ์˜ ๊ธฐ๋ณธ ์›์น™", "์‹ ์ฒด์ ยท์ •์„œ์ ยท์–ธ์–ด์  ์ธ๊ถŒ์นจํ•ด ์˜ˆ๋ฐฉ", "์ˆ˜๊ธ‰์ž์˜ ์˜์‚ฌํ‘œํ˜„๊ณผ ์„ ํƒ๊ถŒ ์กด์ค‘", "์ธ๊ถŒ์นจํ•ด ๋ฐœ์ƒ ์‹œ ๋ณด๊ณ  ๋ฐ ์กฐ์น˜ ์ ˆ์ฐจ"],
190
+ },
191
+ "03. ๋…ธ์ธํ•™๋Œ€์˜ˆ๋ฐฉ์ง€์นจ": {
192
+ "purpose": "๋…ธ์ธํ•™๋Œ€ ์œ ํ˜•์„ ์ดํ•ดํ•˜๊ณ  ์˜์‹ฌ์‚ฌ๋ก€ ๋ฐœ๊ฒฌ ์‹œ ์ฆ‰์‹œ ๋ณด๊ณ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.",
193
+ "content": ["์‹ ์ฒด์ ยท์ •์„œ์ ยท๊ฒฝ์ œ์  ํ•™๋Œ€ ๋ฐ ๋ฐฉ์ž„์˜ ์œ ํ˜•", "ํ•™๋Œ€ ์˜์‹ฌ ์ง•ํ›„ ํ™•์ธ", "์‹ ๊ณ ์˜๋ฌด์™€ ์‹ ๊ณ ์ ˆ์ฐจ", "ํ•™๋Œ€ ์˜ˆ๋ฐฉ์„ ์œ„ํ•œ ์ข…์‚ฌ์ž ํ–‰๋™์ˆ˜์น™"],
194
+ },
195
+ "04. ๊ฐœ์ธ์ •๋ณด๋ณดํ˜ธ์ง€์นจ": {
196
+ "purpose": "์ˆ˜๊ธ‰์ž์™€ ๋ณดํ˜ธ์ž์˜ ๊ฐœ์ธ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ์œ ์ถœ์„ ์˜ˆ๋ฐฉํ•œ๋‹ค.",
197
+ "content": ["๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ยท์ด์šฉยท๋ณด๊ด€ ์›์น™", "๋ฌธ์„œ ๋ฐ ์ „์‚ฐ์ž๋ฃŒ ์ ‘๊ทผ๊ด€๋ฆฌ", "์‚ฌ์ง„ยท์˜์ƒ ์‚ฌ์šฉ ์‹œ ๋™์˜์™€ ๋ณดํ˜ธ์กฐ์น˜", "๊ฐœ์ธ์ •๋ณด ์œ ์ถœ ์‹œ ๋ณด๊ณ  ์ ˆ์ฐจ"],
198
+ },
199
+ "05. ์„ฑํญ๋ ฅ์˜ˆ๋ฐฉ ๋ฐ ๋Œ€์‘์ง€์นจ": {
200
+ "purpose": "์ˆ˜๊ธ‰์ž์™€ ์ข…์‚ฌ์ž์˜ ์„ฑ์  ์ž๊ธฐ๊ฒฐ์ •๊ถŒ์„ ๋ณดํ˜ธํ•˜๊ณ  ์„ฑํฌ๋กฑยท์„ฑํญ๋ ฅ์„ ์˜ˆ๋ฐฉํ•œ๋‹ค.",
201
+ "content": ["์„ฑํฌ๋กฑยท์„ฑํญ๋ ฅ์˜ ๊ฐœ๋…๊ณผ ์‚ฌ๋ก€", "๋ชฉ์š•ยท๋ฐฐ๋ณ€ยท์‹ ์ฒด์ง€์› ์‹œ ์œ ์˜์‚ฌํ•ญ", "๋ถˆ์พŒ๊ฐ ํ‘œํ˜„ ์‹œ ์ฆ‰์‹œ ์ค‘๋‹จ ๋ฐ ๋ณด๊ณ ", "ํ”ผํ•ด ๋ฐœ์ƒ ์‹œ ๋ณดํ˜ธ์กฐ์น˜์™€ ๋ณด๊ณ ์ฒด๊ณ„"],
202
+ },
203
+ "06. ์‘๊ธ‰์ƒํ™ฉ๋Œ€์‘์ง€์นจ": {
204
+ "purpose": "๋‚™์ƒ, ํ˜ธํก๊ณค๋ž€, ์˜์‹์ €ํ•˜ ๋“ฑ ์‘๊ธ‰์ƒํ™ฉ ๋ฐœ์ƒ ์‹œ ์‹ ์†ํ•˜๊ณ  ์ ์ ˆํ•˜๊ฒŒ ๋Œ€์‘ํ•œ๋‹ค.",
205
+ "content": ["์‘๊ธ‰์ƒํ™ฉ ์œ ํ˜•๊ณผ ์ดˆ๊ธฐ ํ™•์ธ", "119 ์‹ ๊ณ  ๋ฐ ๋ณดํ˜ธ์ž ์—ฐ๋ฝ ์ ˆ์ฐจ", "๊ธฐ๊ด€ ๋ณด๊ณ ์ฒด๊ณ„์™€ ๊ธฐ๋ก ๋ฐฉ๋ฒ•", "์‹ฌํ์†Œ์ƒ์ˆ  ๋ฐ ๊ธฐ๋ณธ ์‘๊ธ‰์ฒ˜์น˜ ์œ ์˜์‚ฌํ•ญ"],
206
+ },
207
+ "07. ๊ฐ์—ผ์˜ˆ๋ฐฉ ๋ฐ ๊ด€๋ฆฌ์ง€์นจ": {
208
+ "purpose": "๊ฐ์—ผ๋ณ‘ ์ „ํŒŒ๋ฅผ ์˜ˆ๋ฐฉํ•˜๊ณ  ์ˆ˜๊ธ‰์ž์™€ ์ข…์‚ฌ์ž์˜ ๊ฑด๊ฐ•์„ ๋ณดํ˜ธํ•œ๋‹ค.",
209
+ "content": ["์†์œ„์ƒ๊ณผ ๊ฐœ์ธ๋ณดํ˜ธ๊ตฌ ์‚ฌ์šฉ", "๋ฐœ์—ดยทํ˜ธํก๊ธฐ ์ฆ์ƒ ํ™•์ธ๊ณผ ๊ฒฉ๋ฆฌ ์•ˆ๋‚ด", "ํ™˜๊ฒฝ์†Œ๋… ๋ฐ ํ™˜๊ธฐ๊ด€๋ฆฌ", "๊ฐ์—ผ ์˜์‹ฌ ์‹œ ๋ณด๊ณ  ๋ฐ ์กฐ์น˜"],
210
+ },
211
+ "08. ์š•์ฐฝ์˜ˆ๋ฐฉ์ง€์นจ": {
212
+ "purpose": "์š•์ฐฝ ์œ„ํ—˜์š”์ธ์„ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•˜๊ณ  ์ฒด์œ„๋ณ€๊ฒฝ๊ณผ ํ”ผ๋ถ€๊ด€์ฐฐ์„ ํ†ตํ•ด ์š•์ฐฝ์„ ์˜ˆ๋ฐฉํ•œ๋‹ค.",
213
+ "content": ["์š•์ฐฝ ์œ„ํ—˜์š”์ธ๊ณผ ํ˜ธ๋ฐœ๋ถ€์œ„", "์ฒด์œ„๋ณ€๊ฒฝ๊ณผ ์••๋ฐ• ์™„ํ™”", "ํ”ผ๋ถ€ ๋ฐœ์ ยท์ƒ์ฒ˜ ๊ด€์ฐฐ ๋ฐ ๋ณด๊ณ ", "์˜์–‘ยท์ˆ˜๋ถ„์„ญ์ทจ์™€ ์ฒญ๊ฒฐ๊ด€๋ฆฌ"],
214
+ },
215
+ "09. ๋‚™์ƒ์˜ˆ๋ฐฉ์ง€์นจ": {
216
+ "purpose": "์ˆ˜๊ธ‰์ž์˜ ๋‚™์ƒ ์œ„ํ—˜์„ ์ค„์ด๊ณ  ์•ˆ์ „ํ•œ ์ด๋™๊ณผ ์ƒํ™œํ™˜๊ฒฝ์„ ์œ ์ง€ํ•œ๋‹ค.",
217
+ "content": ["๋‚™์ƒ ์œ„ํ—˜์š”์ธ ํ™•์ธ", "๋ณดํ–‰๋ณด์กฐ์™€ ํ™”์žฅ์‹ค ์ด๋™ ์ง€์›", "ํ™˜๊ฒฝ์ •๋ฆฌ์™€ ๋ฏธ๋„๋Ÿผ ์˜ˆ๋ฐฉ", "๋‚™์ƒ ๋ฐœ์ƒ ์‹œ ์กฐ์น˜์™€ ๊ธฐ๋ก"],
218
+ },
219
+ "10. ์น˜๋งค์˜ˆ๋ฐฉ ๋ฐ ๊ด€๋ฆฌ์ง€์นจ": {
220
+ "purpose": "์ธ์ง€๊ธฐ๋Šฅ ์ €ํ•˜ ์ˆ˜๊ธ‰์ž์˜ ํŠน์„ฑ์„ ์ดํ•ดํ•˜๊ณ  ์•ˆ์ •์ ์ธ ๋Œ๋ด„์„ ์ œ๊ณตํ•œ๋‹ค.",
221
+ "content": ["์น˜๋งค ์ฆ์ƒ๊ณผ ํ–‰๋™์‹ฌ๋ฆฌ์ฆ์ƒ ์ดํ•ด", "๋ฐ˜๋ณต์„ค๋ช…๊ณผ ์•ˆ์ •์  ํ™˜๊ฒฝ ์ œ๊ณต", "์ธ์ง€ํ™œ๋™๊ณผ ์ผ์ƒ์ƒํ™œ ์ง€์›", "๋ฐฐํšŒยท๋ถˆ์•ˆยท๊ณต๊ฒฉ์„ฑ ๋Œ€์‘ ๋ฐฉ๋ฒ•"],
222
+ },
223
+ "11. ๊ทผ๊ณจ๊ฒฉ๊ณ„์งˆํ™˜ ์˜ˆ๋ฐฉ์ง€์นจ": {
224
+ "purpose": "์ข…์‚ฌ์ž์˜ ํ—ˆ๋ฆฌยท์–ด๊นจยท์†๋ชฉ ์†์ƒ์„ ์˜ˆ๋ฐฉํ•˜๊ณ  ์•ˆ์ „ํ•œ ์—…๋ฌด์ˆ˜ํ–‰ ๋ฐฉ๋ฒ•์„ ์ตํžŒ๋‹ค.",
225
+ "content": ["๊ทผ๊ณจ๊ฒฉ๊ณ„์งˆํ™˜ ์œ„ํ—˜์š”์ธ", "์ด๋™ยท์ฒด์œ„๋ณ€๊ฒฝ ์‹œ ์˜ฌ๋ฐ”๋ฅธ ์ž์„ธ", "๋ณด์กฐ๋„๊ตฌ ํ™œ์šฉ๊ณผ 2์ธ ํ˜‘์กฐ", "ํ†ต์ฆ ๋ฐœ์ƒ ์‹œ ๋ณด๊ณ ์™€ ์—…๋ฌด์กฐ์ •"],
226
+ },
227
+ "12. ์ข…์‚ฌ์ž ์ธ๊ถŒ์นจํ•ด ์˜ˆ๋ฐฉ์ง€์นจ": {
228
+ "purpose": "์ข…์‚ฌ์ž์— ๋Œ€ํ•œ ํญ์–ธยทํญํ–‰ยท์„ฑํฌ๋กฑ ๋“ฑ ์ธ๊ถŒ์นจํ•ด๋ฅผ ์˜ˆ๋ฐฉํ•˜๊ณ  ๋ณดํ˜ธ์ฒด๊ณ„๋ฅผ ์ดํ•ดํ•œ๋‹ค.",
229
+ "content": ["์ข…์‚ฌ์ž ์ธ๊ถŒ์นจํ•ด ์œ ํ˜•", "๋ถ€๋‹น์š”๊ตฌ์™€ ํญ์–ธยทํญํ–‰ ๋Œ€์‘", "๊ณ ์ถฉ ์ ‘์ˆ˜์™€ ๊ธฐ๊ด€ ๋ณด๏ฟฝ๏ฟฝ๏ฟฝ์กฐ์น˜", "์žฌ๋ฐœ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๊ธฐ๋ก๊ณผ ๋ณด๊ณ "],
230
+ },
231
+ "13. ๊ณ ์ถฉ์ฒ˜๋ฆฌ ๋ฐ ์•ผ๊ฐ„๊ทผ๋ฌด์ง€์นจ": {
232
+ "purpose": "์ˆ˜๊ธ‰์žยท๋ณดํ˜ธ์žยท์ข…์‚ฌ์ž ๊ณ ์ถฉ์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์•ผ๊ฐ„๊ทผ๋ฌด ์‹œ ์•ˆ์ „๊ด€๋ฆฌ ์›์น™์„ ์ค€์ˆ˜ํ•œ๋‹ค.",
233
+ "content": ["๊ณ ์ถฉ ์ ‘์ˆ˜์™€ ์ฒ˜๋ฆฌ ์ ˆ์ฐจ", "๋ฏผ์› ์˜ˆ๋ฐฉ๊ณผ ์ƒ๋‹ด๊ธฐ๋ก ๊ด€๋ฆฌ", "์•ผ๊ฐ„๊ทผ๋ฌด ์‹œ ์ˆœ์ฐฐยท์‘๊ธ‰์ƒํ™ฉ ๋Œ€์‘", "์•ผ๊ฐ„ ํŠน์ด์‚ฌํ•ญ ๋ณด๊ณ  ๋ฐ ์ธ์ˆ˜์ธ๊ณ„"],
234
+ },
235
+ }
236
+
237
+ METHODS = ["์ง‘ํ•ฉ๊ต์œก", "๊ฐœ๋ณ„๊ต์œก", "์˜จ๋ผ์ธ๊ต์œก", "ํšŒ์˜๊ต์œก", "์ž๋ฃŒ๋ฐฐํฌ ํ›„ ํ™•์ธ", "ํ˜„์žฅ์‹ค์Šต ๋ณ‘ํ–‰"]
238
+
239
+
240
+ # -----------------------------
241
+ # ์ƒ์„ฑ ๋กœ์ง
242
+ # -----------------------------
243
+ def build_education_text(selected_items, institution_type, method, custom_content):
244
+ if not selected_items:
245
+ return "", ""
246
+
247
+ purpose_lines = []
248
+ content_lines = []
249
+ for item in selected_items:
250
+ g = GUIDELINES[item]
251
+ purpose_lines.append(f"- {item}: {g['purpose']}")
252
+ content_lines.append(f"[{item}]")
253
+ for c in g["content"]:
254
+ content_lines.append(f"- {c}")
255
+ content_lines.append("")
256
+
257
+ institution_note = {
258
+ "๋ฐฉ๋ฌธ์š”์–‘": "๋ฐฉ๋ฌธ์„œ๋น„์Šค ํ˜„์žฅ์—์„œ ๋‹จ๋… ๊ทผ๋ฌด ์ค‘ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์‘๊ธ‰์ƒํ™ฉ, ๊ฐœ์ธ์ •๋ณด๋ณดํ˜ธ, ๋ณดํ˜ธ์ž ์š”์ฒญ ๋Œ€์‘์„ ์ค‘์‹ฌ์œผ๋กœ ๊ต์œกํ•จ.",
259
+ "๋ฐฉ๋ฌธ๋ชฉ์š•": "๋ชฉ์š•์ง€์› ๊ณผ์ •์—์„œ ์‹ ์ฒด๋…ธ์ถœ, ๋‚™์ƒ, ํ”ผ๋ถ€์ƒํƒœ ํ™•์ธ, ๊ฐ์—ผ์˜ˆ๋ฐฉ, ์‘๊ธ‰์ƒํ™ฉ ๋Œ€์‘์„ ์ค‘์‹ฌ์œผ๋กœ ๊ต์œกํ•จ.",
260
+ "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ": "๊ฑด๊ฐ•์ƒํƒœ ๊ด€์ฐฐ, ๊ฐ์—ผ๊ด€๋ฆฌ, ๊ฐœ์ธ์ •๋ณด๋ณดํ˜ธ, ์˜๋ฃŒ์—ฐ๊ณ„ ๋ฐ ์‘๊ธ‰์ƒํ™ฉ ๋Œ€์‘์„ ์ค‘์‹ฌ์œผ๋กœ ๊ต์œกํ•จ.",
261
+ "์ฃผ๊ฐ„๋ณดํ˜ธ": "์„ผํ„ฐ ๋‚ด ๋‹จ์ฒด์ƒํ™œ, ํ”„๋กœ๊ทธ๋žจ ์ฐธ์—ฌ, ์†ก์˜์•ˆ์ „, ๋‚™์ƒยท๊ฐ์—ผ์˜ˆ๋ฐฉ, ์ˆ˜๊ธ‰์ž ๊ฐ„ ๊ฐˆ๋“ฑ ์˜ˆ๋ฐฉ์„ ์ค‘์‹ฌ์œผ๋กœ ๊ต์œกํ•จ.",
262
+ "์š”์–‘์›": "์ž…์†Œ์ƒํ™œ ์ „๋ฐ˜์˜ ์ธ๊ถŒ๋ณดํ˜ธ, ์š•์ฐฝยท๋‚™์ƒยท๊ฐ์—ผ์˜ˆ๋ฐฉ, ์•ผ๊ฐ„๊ทผ๋ฌด, ์‘๊ธ‰์ƒํ™ฉ ๋Œ€์‘์„ ์ค‘์‹ฌ์œผ๋กœ ๊ต์œกํ•จ.",
263
+ "๊ณต๋™์ƒํ™œ๊ฐ€์ •": "์†Œ๊ทœ๋ชจ ๊ณต๋™์ƒํ™œ ์ค‘ ์ธ๊ถŒ๋ณดํ˜ธ, ์ •์„œ์ง€์›, ์•ผ๊ฐ„์•ˆ์ „, ๊ฐ์—ผ์˜ˆ๋ฐฉ, ๊ณ ์ถฉ์ฒ˜๋ฆฌ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๊ต์œกํ•จ.",
264
+ }.get(institution_type, "")
265
+
266
+ purpose = "๊ต์œก ๋ชฉ์ \n" + "\n".join(purpose_lines)
267
+
268
+ content = f"""
269
+ [๊ต์œก๋ฐฉ๋ฒ•]
270
+ {method}
271
+
272
+ [๊ธฐ๊ด€์œ ํ˜•๋ณ„ ๊ต์œก ์ค‘์ ]
273
+ {institution_note}
274
+
275
+ [๊ต์œก๋‚ด์šฉ]
276
+ {chr(10).join(content_lines)}
277
+
278
+ [๊ธฐ๊ด€ ์ถ”๊ฐ€ ๊ต์œก๋‚ด์šฉ]
279
+ {custom_content or '๊ธฐ๊ด€ ์šด์˜๊ทœ์ •๊ณผ ์ข…์‚ฌ์ž ์—…๋ฌด์ˆ˜์น™์— ๋”ฐ๋ผ ์‹ค์ œ ์‚ฌ๋ก€ ์ค‘์‹ฌ์œผ๋กœ ๊ต์œก์„ ์ง„ํ–‰ํ•จ.'}
280
+ """.strip()
281
+
282
+ return purpose, content
283
+
284
+
285
+ def build_result_text(selected_items, att_dict, absent_staff, method):
286
+ item_text = ", ".join(selected_items)
287
+
288
+ # ์ด๋ฆ„ ๊ฐฏ์ˆ˜ ๋Œ€๋žต์  ์นด์šดํŠธ (์‰ผํ‘œ๋‚˜ ๋„์–ด์“ฐ๊ธฐ ๊ธฐ์ค€)
289
+ total_names = 0
290
+ for names in att_dict.values():
291
+ total_names += len([n for n in names.replace(',', ' ').split() if n.strip()])
292
+
293
+ absent_text = absent_staff.strip() or "๋ฏธ์ฐธ์„์ž ์—†์Œ"
294
+
295
+ variants = [
296
+ f"{item_text} ๊ต์œก์„ ํ†ตํ•ด ์ข…์‚ฌ์ž์˜ ์ง€์นจ ์ดํ•ด๋„๋ฅผ ๋†’์ด๊ณ  ์‹ค์ œ ๊ธ‰์—ฌ์ œ๊ณต ๊ณผ์ •์—์„œ ์ค€์ˆ˜ํ•ด์•ผ ํ•  ๋ณด๊ณ ์ฒด๊ณ„์™€ ํ–‰๋™์ˆ˜์น™์„ ์žฌํ™•์ธํ•˜์˜€๋‹ค.",
297
+ f"์ฐธ์„์ž๋“ค์€ {item_text}์˜ ์ฃผ์š” ๋‚ด์šฉ์„ ํ™•์ธํ•˜์˜€์œผ๋ฉฐ, ์ˆ˜๊ธ‰์ž ์•ˆ์ „๊ณผ ๊ถŒ๋ฆฌ๋ณดํ˜ธ๋ฅผ ์œ„ํ•œ ์‹ค์ฒœ๋ฐฉ๋ฒ•์„ ๊ณต์œ ํ•˜์˜€๋‹ค.",
298
+ f"๊ต์œก ํ›„ ์งˆ์˜์‘๋‹ต์„ ํ†ตํ•ด ํ˜„์žฅ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ๋ก€๋ฅผ ์ ๊ฒ€ํ•˜์˜€๊ณ , ์ง€์นจ ๋ฏธ์ค€์ˆ˜ ์˜ˆ๋ฐฉ๊ณผ ๊ธฐ๋ก๊ด€๋ฆฌ์˜ ์ค‘์š”์„ฑ์„ ํ™•์ธํ•˜์˜€๋‹ค.",
299
+ ]
300
+
301
+ return f"""
302
+ ๊ต์œก๊ฒฐ๊ณผ, ์ด {total_names}๋ช…์˜ ์ข…์‚ฌ์ž๊ฐ€ {method} ๋ฐฉ์‹์œผ๋กœ ๊ต์œก์— ์ฐธ์—ฌํ•˜์˜€๋‹ค.
303
+ {random.choice(variants)}
304
+
305
+ ์ฐธ์„์ž ์ „์›์ด ๊ต์œก๋‚ด์šฉ์„ ์ž˜ ์ˆ™์ง€ํ•˜์˜€์œผ๋ฉฐ, ๊ต์œก์ž๋ฃŒ์— ์ œ์‹œ๋œ ์ง€์นจ ๋‚ด์šฉ์„ ์‹ค์ œ ๊ธ‰์—ฌ์ œ๊ณต ๊ณผ์ •์—์„œ ์ค€์ˆ˜ํ•˜๊ธฐ๋กœ ํ•˜์˜€๋‹ค.
306
+ ๊ธฐ๊ด€์€ ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ž๋ฃŒ๋ฅผ ์ข…์‚ฌ์ž๊ฐ€ ์‰ฝ๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์†Œ์— ๊ณต๊ฐœ ๊ฒŒ์‹œํ•˜๊ณ , ์ข…์‚ฌ์ž๊ฐ€ ์ˆ˜์‹œ๋กœ ์ง€์นจ ๋‚ด์šฉ์„ ํ™•์ธํ•˜์—ฌ ๊ธ‰์—ฌ์ œ๊ณต์— ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ด€๋ฆฌํ•œ๋‹ค.
307
+ ๋˜ํ•œ ์‹ ๊ทœ์ง์›, ๋ฏธ์ฐธ์„์ž, ์ง€์นจ ์ˆ™์ง€๊ฐ€ ๋ถ€์กฑํ•œ ์ข…์‚ฌ์ž์—๊ฒŒ๋Š” ๋ณด์ถฉ๊ต์œก์„ ์‹ค์‹œํ•˜๊ณ , ๊ต์œก๋‚ด์šฉ์ด ํ˜„์žฅ์—์„œ ์ดํ–‰๋˜๋Š”์ง€ ์ง€์†์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•œ๋‹ค.
308
+
309
+ ๋ฏธ์ฐธ์„์ž ๊ด€๋ฆฌ: {absent_text}
310
+ """.strip()
311
+
312
+
313
+ def evaluate_education(data):
314
+ required = {
315
+ "๊ต์œก๋ช…": bool(data["education_title"].strip()),
316
+ "๊ต์œก์ผ์‹œ": bool(data["education_date"]) and bool(data["education_time"].strip()),
317
+ "๊ต์œก์žฅ์†Œ": bool(data["education_place"].strip()),
318
+ "๊ฐ•์‚ฌ๋ช…": bool(data["instructor_name"].strip()),
319
+ "๊ต์œก๋Œ€์ƒ/์ฐธ์„์ž": bool(data["attendees"].strip()),
320
+ "๊ต์œก์ง€์นจ ์„ ํƒ": bool(data["guideline_items"].strip()),
321
+ "๊ต์œก๋ชฉ์ ": bool(data["education_purpose"].strip()),
322
+ "๊ต์œก๋‚ด์šฉ": bool(data["education_content"].strip()),
323
+ "๊ต์œก์ž๋ฃŒ ์ฒจ๋ถ€/๋ช…์‹œ": bool(data["education_materials"].strip()),
324
+ "๊ต์œก๊ฒฐ๊ณผ": bool(data["education_result"].strip()),
325
+ "๋ฏธ์ฐธ์„์ž ๊ด€๋ฆฌ": "๏ฟฝ๏ฟฝ๏ฟฝ์ฐธ์„" in data["education_result"] or bool(data["absent_staff"].strip()),
326
+ "ํ›„์†์กฐ์น˜": bool(data["followup_plan"].strip()),
327
+ }
328
+
329
+ score = int(sum(required.values()) / len(required) * 100)
330
+ missing = [k for k, v in required.items() if not v]
331
+
332
+ if score >= 90:
333
+ result = "ํ‰๊ฐ€ ์ ํ•ฉ๋„๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค. ๊ต์œก์ผ์‹œ, ๊ฐ•์‚ฌ, ์ฐธ์„์ž, ๊ต์œก๋‚ด์šฉ, ๊ฒฐ๊ณผ ๋ฐ ํ›„์†์กฐ์น˜๊ฐ€ ์ž˜ ๋ฐ˜์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
334
+ elif score >= 75:
335
+ result = "๋Œ€์ฒด๋กœ ์ ํ•ฉํ•˜๋‚˜ ์ผ๋ถ€ ๋ณด์™„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ถ€์กฑ ํ•ญ๋ชฉ์„ ๋ณด์™„ํ•˜๋ฉด ํ‰๊ฐ€ ๋Œ€์‘๋ ฅ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค."
336
+ else:
337
+ result = "๋ณด์™„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ฐธ์„์ž ๋ช…๋‹จ, ๊ต์œก์ž๋ฃŒ, ๊ต์œก๊ฒฐ๊ณผ, ๋ฏธ์ฐธ์„์ž ๊ด€๋ฆฌ๊ฐ€ ๊ตฌ์ฒด์ ์œผ๋กœ ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค."
338
+
339
+ return score, missing, result
340
+
341
+
342
+ def save_uploaded_materials(uploaded_files):
343
+ paths = []
344
+ names = []
345
+ for file in (uploaded_files or [])[:3]:
346
+ suffix = os.path.splitext(file.name)[1].lower() or ".png"
347
+ temp_path = os.path.join(tempfile.gettempdir(), f"edu_material_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{suffix}")
348
+ with open(temp_path, "wb") as f:
349
+ f.write(file.getbuffer())
350
+ paths.append(temp_path)
351
+ names.append(file.name)
352
+ return paths, names
353
+
354
+
355
+ # -----------------------------
356
+ # ์—‘์…€ ์ถœ๋ ฅ (๋””์ž์ธ ์„ ๋ช…ํ™” ๋ฐ ์…€ ์ž˜๋ฆผ ๋ฐฉ์ง€)
357
+ # -----------------------------
358
+ def style_range(ws, cell_range, border_on=True, fill=None, font=None, alignment=None):
359
+ """๋ณ‘ํ•ฉ ์…€ ํฌํ•จ ๋ฒ”์œ„ ์ „์ฒด์— ์Šคํƒ€์ผ ์ ์šฉ (๊ฒ€์ •์ƒ‰ ํ…Œ๋‘๋ฆฌ๋กœ ์„ ๋ช…ํ•˜๊ฒŒ)"""
360
+ thin = Side(style="thin", color="000000")
361
+ border = Border(left=thin, right=thin, top=thin, bottom=thin)
362
+
363
+ cells = ws[cell_range]
364
+ if not isinstance(cells, tuple):
365
+ cells = ((cells,),)
366
+
367
+ for row in cells:
368
+ for cell in row:
369
+ if border_on:
370
+ cell.border = border
371
+ if fill:
372
+ cell.fill = fill
373
+ if font:
374
+ cell.font = font
375
+ if alignment:
376
+ cell.alignment = alignment
377
+
378
+ def write_title_with_approval(ws, title, title_fill, header_fill):
379
+ for col in range(1, 10):
380
+ ws.column_dimensions[get_column_letter(col)].width = 11.5
381
+
382
+ ws.merge_cells("A1:G2")
383
+ c = ws.cell(1, 1)
384
+ c.value = title
385
+ c.font = Font(name="Malgun Gothic", size=24, bold=True)
386
+ c.alignment = Alignment(horizontal="center", vertical="center")
387
+ style_range(ws, "A1:G2", fill=title_fill)
388
+
389
+ # ๊ฒฐ์žฌ๋ผ์ธ (์šฐ์ธก ์ƒ๋‹จ 2์นธ)
390
+ ws.cell(1, 8).value = "๋‹ด๋‹น์ž"
391
+ ws.cell(1, 9).value = "์‹œ์„ค์žฅ"
392
+ ws.cell(2, 8).value = ""
393
+ ws.cell(2, 9).value = ""
394
+
395
+ for rr in [1, 2]:
396
+ for cc in [8, 9]:
397
+ cell = ws.cell(rr, cc)
398
+ cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
399
+ if rr == 1:
400
+ cell.font = Font(name="Malgun Gothic", size=11, bold=True)
401
+ cell.fill = header_fill
402
+ else:
403
+ cell.font = Font(name="Malgun Gothic", size=11)
404
+ style_range(ws, f"{get_column_letter(cc)}{rr}:{get_column_letter(cc)}{rr}")
405
+
406
+ ws.row_dimensions[1].height = 28
407
+ ws.row_dimensions[2].height = 42
408
+
409
+ def write_info_row(ws, row, a, b, c, d, e, f, header_fill):
410
+ ws.cell(row, 1).value = a
411
+ ws.merge_cells(start_row=row, start_column=2, end_row=row, end_column=3)
412
+ ws.cell(row, 2).value = b
413
+
414
+ ws.cell(row, 4).value = c
415
+ ws.merge_cells(start_row=row, start_column=5, end_row=row, end_column=6)
416
+ ws.cell(row, 5).value = d
417
+
418
+ ws.cell(row, 7).value = e
419
+ ws.merge_cells(start_row=row, start_column=8, end_row=row, end_column=9)
420
+ ws.cell(row, 8).value = f
421
+
422
+ for col in [1, 4, 7]:
423
+ ws.cell(row, col).fill = header_fill
424
+ ws.cell(row, col).font = Font(name="Malgun Gothic", size=11, bold=True)
425
+ ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
426
+
427
+ for col in [2, 5, 8]:
428
+ ws.cell(row, col).font = Font(name="Malgun Gothic", size=11)
429
+ ws.cell(row, col).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
430
+
431
+ style_range(ws, f"A{row}:I{row}")
432
+ ws.row_dimensions[row].height = 30
433
+
434
+ def write_attendee_row(ws, row, k1, v1, k2, v2, k3, v3, header_fill):
435
+ """์ฐธ์„์ž ๋ช…๋‹จ ์ „์šฉ 3์—ด ๋ถ„ํ•  ๊ตฌ์กฐ (์ง์ข… | ์ด๋ฆ„ | ์ง์ข… | ์ด๋ฆ„ | ์ง์ข… | ์ด๋ฆ„)"""
436
+ ws.cell(row, 1).value = k1
437
+ ws.merge_cells(start_row=row, start_column=2, end_row=row, end_column=3)
438
+ ws.cell(row, 2).value = v1
439
+
440
+ ws.cell(row, 4).value = k2
441
+ ws.merge_cells(start_row=row, start_column=5, end_row=row, end_column=6)
442
+ ws.cell(row, 5).value = v2
443
+
444
+ ws.cell(row, 7).value = k3
445
+ ws.merge_cells(start_row=row, start_column=8, end_row=row, end_column=9)
446
+ ws.cell(row, 8).value = v3
447
+
448
+ for c in [1, 4, 7]:
449
+ ws.cell(row, c).fill = header_fill
450
+ ws.cell(row, c).font = Font(name="Malgun Gothic", size=11, bold=True)
451
+ ws.cell(row, c).alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
452
+
453
+ for c in [2, 5, 8]:
454
+ ws.cell(row, c).font = Font(name="Malgun Gothic", size=11)
455
+ ws.cell(row, c).alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
456
+
457
+ style_range(ws, f"A{row}:I{row}")
458
+
459
+ # ๋‚ด์šฉ์ด ๊ธธ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด ํ–‰ ๋†’์ด ์ž๋™ ๊ณ„์‚ฐ (๊ธ€์ž์ˆ˜ ๊ธฐ๋ฐ˜)
460
+ max_len = max(len(str(v1)), len(str(v2)), len(str(v3)))
461
+ lines = (max_len // 18) + 1
462
+ ws.row_dimensions[row].height = max(30, lines * 20 + 10)
463
+
464
+
465
+ def write_section(ws, row, title, content, header_fill, min_height=100):
466
+ # ์ œ๋ชฉ ํ–‰
467
+ ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=9)
468
+ t_cell = ws.cell(row, 1)
469
+ t_cell.value = title
470
+ t_cell.font = Font(name="Malgun Gothic", size=15, bold=True)
471
+ t_cell.alignment = Alignment(horizontal="center", vertical="center")
472
+ t_cell.fill = header_fill
473
+ style_range(ws, f"A{row}:I{row}")
474
+ ws.row_dimensions[row].height = 30
475
+ row += 1
476
+
477
+ # ๋‚ด์šฉ ํ–‰
478
+ ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=9)
479
+ c_cell = ws.cell(row, 1)
480
+ c_cell.value = content
481
+ c_cell.font = Font(name="Malgun Gothic", size=11)
482
+ c_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
483
+ style_range(ws, f"A{row}:I{row}")
484
+
485
+ # ์ค„๋ฐ”๊ฟˆ๊ณผ ํ…์ŠคํŠธ ๊ธธ์ด์— ๋น„๋ก€ํ•ด ์…€ ๋†’์ด ์ •๋ฐ€ ๊ณ„์‚ฐ (์•„๋ž˜ ํ…Œ๋‘๋ฆฌ ๋ฎ์ž„ ๋ฐฉ์ง€)
486
+ content_str = str(content)
487
+ lines = 0
488
+ for line in content_str.split("\n"):
489
+ lines += (len(line) // 50) + 1
490
+ calculated_height = lines * 22 + 15
491
+ ws.row_dimensions[row].height = max(min_height, calculated_height)
492
+
493
+ return row + 1
494
+
495
+
496
+ def generate_excel(data):
497
+ wb = Workbook()
498
+ title_fill = PatternFill("solid", fgColor="FFEDD5")
499
+ header_fill = PatternFill("solid", fgColor="F3F4F6")
500
+
501
+ ws = wb.active
502
+ ws.title = "๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ๊ต์œก์ผ์ง€"
503
+
504
+ ws.page_setup.paperSize = ws.PAPERSIZE_A4
505
+ ws.page_setup.orientation = "portrait"
506
+ ws.page_setup.fitToWidth = 1
507
+ ws.page_setup.fitToHeight = 0
508
+ ws.sheet_properties.pageSetUpPr.fitToPage = True
509
+ ws.page_margins.left = 0.3
510
+ ws.page_margins.right = 0.3
511
+ ws.page_margins.top = 0.4
512
+ ws.page_margins.bottom = 0.4
513
+ ws.sheet_view.showGridLines = False
514
+
515
+ write_title_with_approval(ws, "๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€", title_fill, header_fill)
516
+
517
+ r = 3
518
+ write_info_row(ws, r, "๊ธฐ๊ด€๋ช…", data["institution_name"], "๊ธฐ๊ด€์œ ํ˜•", data["institution_type"], "๊ต์œก์ผ์ž", data["education_date"], header_fill); r += 1
519
+ write_info_row(ws, r, "๊ต์œก๋ช…", data["education_title"], "๊ต์œก์‹œ๊ฐ„", data["education_time"], "๊ต์œก์žฅ์†Œ", data["education_place"], header_fill); r += 1
520
+ write_info_row(ws, r, "๊ฐ•์‚ฌ๋ช…", data["instructor_name"], "๊ฐ•์‚ฌ์ง์ฑ…", data["instructor_position"], "๊ต์œก๋ฐฉ๋ฒ•", data["education_method"], header_fill); r += 1
521
+
522
+ r = write_section(ws, r, "๊ต์œก์ง€์นจ", data["guideline_items"], header_fill, 80)
523
+
524
+ # ==== ์ฐธ์„์ž ๋ช…๋‹จ (์ง์ข…๋ณ„ ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ) ====
525
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
526
+ ws.cell(r, 1).value = "์ฐธ์„์ž ๋ช…๋‹จ"
527
+ ws.cell(r, 1).font = Font(name="Malgun Gothic", size=15, bold=True)
528
+ ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center")
529
+ ws.cell(r, 1).fill = header_fill
530
+ style_range(ws, f"A{r}:I{r}")
531
+ ws.row_dimensions[r].height = 30
532
+ r += 1
533
+
534
+ att = data.get("attendees_dict", {})
535
+ write_attendee_row(ws, r, "์‹œ์„ค์žฅ", att.get("์‹œ์„ค์žฅ", ""), "์‚ฌํšŒ๋ณต์ง€์‚ฌ", att.get("์‚ฌํšŒ๋ณต์ง€์‚ฌ", ""), "๋ฌผ๋ฆฌ์น˜๋ฃŒ์‚ฌ", att.get("๋ฌผ๋ฆฌ์น˜๋ฃŒ์‚ฌ", ""), header_fill); r += 1
536
+ write_attendee_row(ws, r, "๊ฐ„ํ˜ธ์กฐ๋ฌด์‚ฌ", att.get("๊ฐ„ํ˜ธ์กฐ๋ฌด์‚ฌ", ""), "์š”์–‘๋ณดํ˜ธ์‚ฌ", att.get("์š”์–‘๋ณดํ˜ธ์‚ฌ", ""), "๊ธฐํƒ€์ง์ข…", att.get("๊ธฐํƒ€", ""), header_fill); r += 1
537
+
538
+ # =========================================
539
+
540
+ r = write_section(ws, r, "๋ฏธ์ฐธ์„์ž ๋ฐ ๋ณด์ถฉ๊ต์œก ๊ณ„ํš", data["absent_staff"] or "๋ฏธ์ฐธ์„์ž ์—†์Œ", header_fill, 70)
541
+ r = write_section(ws, r, "๊ต์œก๋ชฉ์ ", data["education_purpose"], header_fill, 110)
542
+ r = write_section(ws, r, "๊ต์œก๋‚ด์šฉ", data["education_content"], header_fill, 220)
543
+ r = write_section(ws, r, "๊ต์œก์ž๋ฃŒ ์ฒจ๋ถ€/์ž๋ฃŒ๋ช…", data["education_materials"], header_fill, 80)
544
+ r = write_section(ws, r, "๊ต์œก๊ฒฐ๊ณผ", data["education_result"], header_fill, 120)
545
+ r = write_section(ws, r, "ํ›„์†์กฐ์น˜ ๋ฐ ํ˜„์žฅ ์ ์šฉ๊ณ„ํš", data["followup_plan"], header_fill, 90)
546
+ r = write_section(ws, r, "ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€", f"{data['evaluation_score']}์ \n๋ถ€์กฑ ํ•ญ๋ชฉ: {data['missing_items']}\n{data['compliance_result']}", header_fill, 80)
547
+
548
+ # ๊ต์œก์ž๋ฃŒ ์ด๋ฏธ์ง€ ์ฒจ๋ถ€: ์ด๋ฏธ์ง€ ํŒŒ์ผ์ผ ๋•Œ๋งŒ ์‚ฝ์ž…
549
+ image_paths = [p for p in data.get("material_paths", []) if os.path.splitext(p)[1].lower() in [".png", ".jpg", ".jpeg"]]
550
+ if image_paths:
551
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
552
+ ws.cell(r, 1).value = "๊ต์œก์ž๋ฃŒ ์ด๋ฏธ์ง€"
553
+ ws.cell(r, 1).font = Font(name="Malgun Gothic", size=15, bold=True)
554
+ ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center")
555
+ ws.cell(r, 1).fill = header_fill
556
+ style_range(ws, f"A{r}:I{r}")
557
+ r += 1
558
+
559
+ anchors = ["A", "D", "G"]
560
+ img_row = r
561
+ for idx, img_path in enumerate(image_paths[:3]):
562
+ try:
563
+ img = XLImage(img_path)
564
+ img.width = 210
565
+ img.height = 150
566
+ ws.add_image(img, f"{anchors[idx]}{img_row}")
567
+ except Exception:
568
+ pass
569
+
570
+ # ์ด๋ฏธ์ง€ ์˜์—ญ ๋ณ‘ํ•ฉ ๋ฐ ์Šคํƒ€์ผ ์ ์šฉ (ํ…Œ๋‘๋ฆฌ ๋‹ซํž˜ ๋ณด์žฅ)
571
+ ws.merge_cells(start_row=img_row, start_column=1, end_row=img_row+7, end_column=9)
572
+ style_range(ws, f"A{img_row}:I{img_row+7}")
573
+ for rr in range(img_row, img_row + 8):
574
+ ws.row_dimensions[rr].height = 22
575
+ r = img_row + 8
576
+
577
+ output = BytesIO()
578
+ wb.save(output)
579
+ output.seek(0)
580
+ return output
581
+
582
+
583
+ # -----------------------------
584
+ # ํ™”๋ฉด
585
+ # -----------------------------
586
+ menu = st.sidebar.radio("๋ฉ”๋‰ด", ["๊ต์œก์ผ์ง€ ์ƒ์„ฑ", "๊ต์œก๊ธฐ๋ก ๋ณด๊ธฐ"])
587
+
588
+ st.sidebar.success("DB ์—ฐ๊ฒฐ")
589
+ st.sidebar.caption(DB_PATH)
590
+
591
+ if menu == "๊ต์œก๊ธฐ๋ก ๋ณด๊ธฐ":
592
+ st.subheader("๐Ÿ“š ์ €์žฅ๋œ ๊ต์œก๊ธฐ๋ก")
593
+ records = load_education_records()
594
+ if records.empty:
595
+ st.info("์ €์žฅ๋œ ๊ต์œก๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค.")
596
+ else:
597
+ st.dataframe(records, use_container_width=True)
598
+
599
+ elif menu == "๊ต์œก์ผ์ง€ ์ƒ์„ฑ":
600
+ st.subheader("๐Ÿ“˜ ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€ ์ƒ์„ฑ")
601
+
602
+ col1, col2, col3 = st.columns(3)
603
+ institution_name = col1.text_input("๊ธฐ๊ด€๋ช…", "BeeCare ์žฅ๊ธฐ์š”์–‘๊ธฐ๊ด€")
604
+ institution_type = col2.selectbox("๊ธฐ๊ด€์œ ํ˜•", ["๋ฐฉ๋ฌธ์š”์–‘", "๋ฐฉ๋ฌธ๋ชฉ์š•", "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ", "์ฃผ๊ฐ„๋ณดํ˜ธ", "์š”์–‘์›", "๊ณต๋™์ƒํ™œ๊ฐ€์ •"])
605
+ education_title = col3.text_input("๊ต์œก๋ช…", "๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ์ข…์‚ฌ์ž ๊ต์œก")
606
+
607
+ col4, col5, col6 = st.columns(3)
608
+ education_date = col4.date_input("๊ต์œก์ผ์ž", value=date.today())
609
+ education_time = col5.text_input("๊ต์œก์‹œ๊ฐ„", "14:00~15:00")
610
+ education_place = col6.text_input("๊ต์œก์žฅ์†Œ", "๊ธฐ๊ด€ ๋‚ด ํ”„๋กœ๊ทธ๋žจ์‹ค")
611
+
612
+ col7, col8, col9 = st.columns(3)
613
+ instructor_name = col7.text_input("๊ฐ•์‚ฌ๋ช…", "์‹œ์„ค์žฅ")
614
+ instructor_position = col8.text_input("๊ฐ•์‚ฌ์ง์ฑ…", "์‹œ์„ค์žฅ")
615
+ education_method = col9.selectbox("๊ต์œก๋ฐฉ๋ฒ•", METHODS)
616
+
617
+ selected_guidelines = st.multiselect(
618
+ "๊ต์œก ์ง€์นจ ์„ ํƒ",
619
+ list(GUIDELINES.keys()),
620
+ default=["02. ๋…ธ์ธ์ธ๊ถŒ๋ณดํ˜ธ์ง€์นจ", "03. ๋…ธ์ธํ•™๋Œ€์˜ˆ๋ฐฉ์ง€์นจ", "07. ๊ฐ์—ผ์˜ˆ๋ฐฉ ๋ฐ ๊ด€๋ฆฌ์ง€์นจ", "09. ๋‚™์ƒ์˜ˆ๋ฐฉ์ง€์นจ"]
621
+ )
622
+
623
+ st.markdown("### ์ง์ข…๋ณ„ ์ฐธ์„์ž ๋ช…๋‹จ ์ž…๋ ฅ")
624
+ st.caption("๊ฐ ์นธ์— ์ฐธ์„์ž์˜ ์ด๋ฆ„์„ ์ง์ ‘ ์‰ผํ‘œ๋‚˜ ๋„์–ด์“ฐ๊ธฐ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
625
+
626
+ a1, a2, a3 = st.columns(3)
627
+ att_director = a1.text_input("์‹œ์„ค์žฅ", placeholder="์˜ˆ: ํ™๊ธธ๋™")
628
+ att_social = a2.text_input("์‚ฌํšŒ๋ณต์ง€์‚ฌ", placeholder="์˜ˆ: ๊น€๋ณต์ง€")
629
+ att_therapist = a3.text_input("๋ฌผ๋ฆฌ์น˜๋ฃŒ์‚ฌ", placeholder="์˜ˆ: ์ด์น˜๋ฃŒ")
630
+
631
+ a4, a5, a6 = st.columns(3)
632
+ att_nurse = a4.text_input("๊ฐ„ํ˜ธ์กฐ๋ฌด์‚ฌ", placeholder="์˜ˆ: ๋ฐ•๊ฐ„ํ˜ธ")
633
+ att_caregiver = a5.text_input("์š”์–‘๋ณดํ˜ธ์‚ฌ", placeholder="์˜ˆ: ์ตœ์š”์–‘, ์ •์š”์–‘")
634
+ att_others = a6.text_input("๊ธฐํƒ€ ์ง์ข…", placeholder="์˜ˆ: ์กฐ๋ฆฌ์› ๊น€์กฐ๋ฆฌ")
635
+
636
+ # ์ž…๋ ฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋”•์…”๋„ˆ๋ฆฌ์™€ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
637
+ attendees_dict = {
638
+ "์‹œ์„ค์žฅ": att_director,
639
+ "์‚ฌํšŒ๋ณต์ง€์‚ฌ": att_social,
640
+ "๋ฌผ๋ฆฌ์น˜๋ฃŒ์‚ฌ": att_therapist,
641
+ "๊ฐ„ํ˜ธ์กฐ๋ฌด์‚ฌ": att_nurse,
642
+ "์š”์–‘๋ณดํ˜ธ์‚ฌ": att_caregiver,
643
+ "๊ธฐํƒ€": att_others
644
+ }
645
+ attendees_summary = "\n".join([f"[{k}] {v}" for k, v in attendees_dict.items() if v.strip()])
646
+
647
+ absent_staff = st.text_area(
648
+ "๋ฏธ์ฐธ์„์ž ๋ฐ ๋ณด์ถฉ๊ต์œก ๊ณ„ํš",
649
+ placeholder="์˜ˆ: ๋ฐ•์š”์–‘๋ณดํ˜ธ์‚ฌ ์—ฐ์ฐจ๋กœ ๋ฏธ์ฐธ์„, 6์›” 20์ผ ๊ฐœ๋ณ„ ๋ณด์ถฉ๊ต์œก ์˜ˆ์ •",
650
+ height=80
651
+ )
652
+
653
+ custom_content = st.text_area(
654
+ "๊ธฐ๊ด€ ์ถ”๊ฐ€ ๊ต์œก๋‚ด์šฉ",
655
+ placeholder="๊ธฐ๊ด€์—์„œ ์‹ค์ œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ๋ก€, ์ž์ฒด ์šด์˜๊ทœ์ •, ์œ ์˜์‚ฌํ•ญ ๋“ฑ์„ ์ž…๋ ฅํ•˜์„ธ์š”.",
656
+ height=90
657
+ )
658
+
659
+ education_material_files = st.file_uploader(
660
+ "๊ต์œก์ž๋ฃŒ ์ฒจ๋ถ€ (์ด๋ฏธ์ง€/PDF/๋ฌธ์„œ ๊ฐ€๋Šฅ, ์ตœ๋Œ€ 3๊ฐœ)",
661
+ type=["png", "jpg", "jpeg", "pdf", "docx", "xlsx"],
662
+ accept_multiple_files=True
663
+ )
664
+
665
+ material_paths, material_names = save_uploaded_materials(education_material_files)
666
+ material_text = ", ".join(material_names) if material_names else "๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ž๋ฃŒ ๋น„์น˜ ๋ฐ ๊ณต๊ฐœ๊ฒŒ์‹œ ์ž๋ฃŒ ์‚ฌ์šฉ"
667
+
668
+ purpose, content = build_education_text(
669
+ selected_guidelines,
670
+ institution_type,
671
+ education_method,
672
+ custom_content
673
+ )
674
+
675
+ result_text = build_result_text(
676
+ selected_guidelines,
677
+ attendees_dict,
678
+ absent_staff,
679
+ education_method
680
+ ) if selected_guidelines else ""
681
+
682
+ followup_plan = st.text_area(
683
+ "ํ›„์†์กฐ์น˜ ๋ฐ ํ˜„์žฅ ์ ์šฉ๊ณ„ํš",
684
+ value="๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ž๋ฃŒ๋ฅผ ๊ธฐ๊ด€ ๋‚ด ์ข…์‚ฌ์ž๊ฐ€ ํ™•์ธํ•˜๊ธฐ ์‰ฌ์šด ์žฅ์†Œ์— ๊ณต๊ฐœ ๊ฒŒ์‹œํ•˜๊ณ , ์ข…์‚ฌ์ž๊ฐ€ ์ˆ˜์‹œ๋กœ ์ง€์นจ ๋‚ด์šฉ์„ ํ™•์ธํ•˜์—ฌ ์‹ค์ œ ๊ธ‰์—ฌ์ œ๊ณต์— ๋ฐ˜์˜ํ•˜๋„๋ก ํ•œ๋‹ค. ๊ต์œก ํ›„ ์ข…์‚ฌ์ž๊ฐ€ ํ˜„์žฅ์—์„œ ์ง€์นจ์„ ์ค€์ˆ˜ํ•˜๋Š”์ง€ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ , ๋ฏธ์ฐธ์„์ž ๋˜๋Š” ์‹ ๊ทœ์ง์›์— ๋Œ€ํ•ด์„œ๋Š” ๋ณด์ถฉ๊ต์œก์„ ์‹ค์‹œํ•œ๋‹ค.",
685
+ height=100
686
+ )
687
+
688
+ if st.button("๐Ÿ“˜ ๊ต์œก์ผ์ง€ ์ƒ์„ฑ", use_container_width=True):
689
+ data = {
690
+ "institution_name": institution_name,
691
+ "institution_type": institution_type,
692
+ "education_title": education_title,
693
+ "guideline_items": "\n".join(selected_guidelines),
694
+ "education_date": str(education_date),
695
+ "education_time": education_time,
696
+ "education_place": education_place,
697
+ "instructor_name": instructor_name,
698
+ "instructor_position": instructor_position,
699
+ "attendees_dict": attendees_dict,
700
+ "attendees": attendees_summary, # ํ‰๊ฐ€ ๋กœ์ง ๋ฐ DB ์ €์žฅ์šฉ
701
+ "absent_staff": absent_staff,
702
+ "education_method": education_method,
703
+ "education_purpose": purpose,
704
+ "education_content": content,
705
+ "education_materials": material_text,
706
+ "education_result": result_text,
707
+ "followup_plan": followup_plan,
708
+ "material_paths": material_paths,
709
+ }
710
+
711
+ score, missing, compliance = evaluate_education(data)
712
+ data["evaluation_score"] = score
713
+ data["missing_items"] = ", ".join(missing) if missing else "๋ถ€์กฑ ํ•ญ๋ชฉ ์—†์Œ"
714
+ data["compliance_result"] = compliance
715
+
716
+ st.session_state["edu_data"] = data
717
+
718
+ if "edu_data" in st.session_state:
719
+ data = st.session_state["edu_data"]
720
+
721
+ st.markdown("---")
722
+ st.subheader("์ƒ์„ฑ ๊ฒฐ๊ณผ ์ˆ˜์ •")
723
+
724
+ data["education_purpose"] = st.text_area("๊ต์œก๋ชฉ์ ", value=data["education_purpose"], height=140)
725
+ data["education_content"] = st.text_area("๊ต์œก๋‚ด์šฉ", value=data["education_content"], height=260)
726
+ data["education_result"] = st.text_area("๊ต์œก๊ฒฐ๊ณผ", value=data["education_result"], height=140)
727
+ data["followup_plan"] = st.text_area("ํ›„์†์กฐ์น˜", value=data["followup_plan"], height=100)
728
+
729
+ score, missing, compliance = evaluate_education(data)
730
+ data["evaluation_score"] = score
731
+ data["missing_items"] = ", ".join(missing) if missing else "๋ถ€์กฑ ํ•ญ๋ชฉ ์—†์Œ"
732
+ data["compliance_result"] = compliance
733
+
734
+ st.subheader("ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€")
735
+ e1, e2, e3 = st.columns(3)
736
+ e1.metric("ํ‰๊ฐ€ ์ ํ•ฉ๋„", f"{score}์ ")
737
+ e2.metric("๋ถ€์กฑ ํ•ญ๋ชฉ", data["missing_items"])
738
+ e3.metric("๊ฒฐ์žฌ๋ผ์ธ", "๋‹ด๋‹น์ž/์‹œ์„ค์žฅ 2์นธ")
739
+
740
+ if score >= 90:
741
+ st.markdown(f'<div class="okbox">{compliance}</div>', unsafe_allow_html=True)
742
+ else:
743
+ st.markdown(f'<div class="warnbox">{compliance}</div>', unsafe_allow_html=True)
744
+
745
+ if st.button("๐Ÿ’พ ๊ต์œก๊ธฐ๋ก DB ์ €์žฅ", use_container_width=True):
746
+ save_education_record(data)
747
+ st.success("๊ต์œก๊ธฐ๋ก์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
748
+
749
+ filename = f"๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ๊ต์œก์ผ์ง€_{data['education_date']}.xlsx"
750
+ st.download_button(
751
+ "๐Ÿ“ฅ ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€ ์—‘์…€ ๋‹ค์šด๋กœ๋“œ",
752
+ data=generate_excel(data),
753
+ file_name=filename,
754
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
755
+ use_container_width=True,
756
+ )
757
+
758
+ st.caption("BeeCare ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ผ์ง€ ์ƒ์„ฑ๊ธฐ")