devorah-ai-2016 commited on
Commit
02187ce
ยท
verified ยท
1 Parent(s): 3fac630

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +706 -0
app.py ADDED
@@ -0,0 +1,706 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ write_info_row(ws, r, "๊ต์œก์ž๋ฃŒ", data["education_materials"], "๋ฏธ์ฐธ์„์ž", data["absent_staff"] or "์—†์Œ", "์ ํ•ฉ๋„", f"{data['evaluation_score']}์ ", header_fill); r += 1
522
+
523
+ # ์ฐธ์„์ž ๋ช…๋‹จ
524
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
525
+ ws.cell(r, 1).value = "์ง์ข…๋ณ„ ์ฐธ์„์ž ๋ช…๋‹จ"
526
+ ws.cell(r, 1).font = Font(name="Malgun Gothic", size=15, bold=True)
527
+ ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center")
528
+ ws.cell(r, 1).fill = header_fill
529
+ style_range(ws, f"A{r}:I{r}")
530
+ ws.row_dimensions[r].height = 30
531
+ r += 1
532
+
533
+ att = data.get("attendee_dict", {}) or {}
534
+ groups = [
535
+ ("์‹œ์„ค์žฅ", att.get("์‹œ์„ค์žฅ", "")),
536
+ ("์‚ฌํšŒ๋ณต์ง€์‚ฌ", att.get("์‚ฌํšŒ๋ณต์ง€์‚ฌ", "")),
537
+ ("๊ฐ„ํ˜ธ(์กฐ๋ฌด)์‚ฌ", att.get("๊ฐ„ํ˜ธ(์กฐ๋ฌด)์‚ฌ", "")),
538
+ ("์š”์–‘๋ณดํ˜ธ์‚ฌ", att.get("์š”์–‘๋ณดํ˜ธ์‚ฌ", "")),
539
+ ("๋ฌผ๋ฆฌ/์ž‘์—…์น˜๋ฃŒ์‚ฌ", att.get("๋ฌผ๋ฆฌ/์ž‘์—…์น˜๋ฃŒ์‚ฌ", "")),
540
+ ("์กฐ๋ฆฌ์›/๊ธฐํƒ€", att.get("์กฐ๋ฆฌ์›/๊ธฐํƒ€", "")),
541
+ ]
542
+ write_attendee_row(ws, r, groups[0][0], groups[0][1], groups[1][0], groups[1][1], groups[2][0], groups[2][1], header_fill); r += 1
543
+ write_attendee_row(ws, r, groups[3][0], groups[3][1], groups[4][0], groups[4][1], groups[5][0], groups[5][1], header_fill); r += 1
544
+
545
+ r = write_section(ws, r, "1. ๊ต์œก ๋ชฉ์ ", data["education_purpose"], header_fill, min_height=90)
546
+ r = write_section(ws, r, "2. ๊ต์œก ๋‚ด์šฉ", data["education_content"], header_fill, min_height=170)
547
+ r = write_section(ws, r, "3. ๊ต์œก ๊ฒฐ๊ณผ", data["education_result"], header_fill, min_height=130)
548
+ r = write_section(ws, r, "4. ํ›„์†์กฐ์น˜ ๋ฐ ๋ฏธ์ฐธ์„์ž ๋ณด์ถฉ๊ต์œก ๊ณ„ํš", data["followup_plan"], header_fill, min_height=90)
549
+ r = write_section(ws, r, "5. ํ‰๊ฐ€ ์ ํ•ฉ๋„ ์ ๊ฒ€", data["compliance_result"] + ("\n๋ณด์™„ ํ•„์š” ํ•ญ๋ชฉ: " + data["missing_items"] if data["missing_items"] else "\n๋ณด์™„ ํ•„์š” ํ•ญ๋ชฉ: ์—†์Œ"), header_fill, min_height=80)
550
+
551
+ # ๊ต์œก์ž๋ฃŒ ์ด๋ฏธ์ง€ ์ฒจ๋ถ€(๏ฟฝ๏ฟฝํƒ)
552
+ material_paths = data.get("material_paths", []) or []
553
+ if material_paths:
554
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=9)
555
+ ws.cell(r, 1).value = "6. ์ฒจ๋ถ€ ๊ต์œก์ž๋ฃŒ ๋ฏธ๋ฆฌ๋ณด๊ธฐ"
556
+ ws.cell(r, 1).font = Font(name="Malgun Gothic", size=15, bold=True)
557
+ ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center")
558
+ ws.cell(r, 1).fill = header_fill
559
+ style_range(ws, f"A{r}:I{r}")
560
+ r += 1
561
+ for img_path in material_paths:
562
+ try:
563
+ img = XLImage(img_path)
564
+ img.width = 520
565
+ img.height = 330
566
+ ws.add_image(img, f"A{r}")
567
+ ws.row_dimensions[r].height = 250
568
+ r += 14
569
+ except Exception:
570
+ pass
571
+
572
+ # ๋งˆ์ง€๋ง‰ ๋นˆ์นธ/์ง€์ €๋ถ„ํ•œ ์นธ ๋ฐฉ์ง€: ์‹ค์ œ ์‚ฌ์šฉ ์˜์—ญ๋งŒ ์ธ์‡„
573
+ ws.print_area = f"A1:I{max(r-1, 1)}"
574
+
575
+ bio = BytesIO()
576
+ wb.save(bio)
577
+ bio.seek(0)
578
+ return bio.getvalue()
579
+
580
+
581
+ # -----------------------------
582
+ # ํ™”๋ฉด ์ž…๋ ฅ UI
583
+ # -----------------------------
584
+ with st.sidebar:
585
+ st.subheader("๊ธฐ๊ด€ ๊ธฐ๋ณธ์ •๋ณด")
586
+ institution_name = st.text_input("๊ธฐ๊ด€๋ช…", value="")
587
+ institution_type = st.selectbox("๊ธฐ๊ด€์œ ํ˜•", ["๋ฐฉ๋ฌธ์š”์–‘", "๋ฐฉ๋ฌธ๋ชฉ์š•", "๋ฐฉ๋ฌธ๊ฐ„ํ˜ธ", "์ฃผ๊ฐ„๋ณดํ˜ธ", "์š”์–‘์›", "๊ณต๋™์ƒํ™œ๊ฐ€์ •"])
588
+ education_date = st.date_input("๊ต์œก์ผ์ž", value=date.today())
589
+ education_time = st.text_input("๊ต์œก์‹œ๊ฐ„", value="14:00~15:00")
590
+ education_place = st.text_input("๊ต์œก์žฅ์†Œ", value="์‚ฌ๋ฌด์‹ค")
591
+
592
+ st.subheader("1. ๊ต์œก ์ง€์นจ ์„ ํƒ")
593
+ selected_items = st.multiselect(
594
+ "๊ต์œกํ•  ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ์„ ์„ ํƒํ•˜์„ธ์š”.",
595
+ list(GUIDELINES.keys()),
596
+ default=["01. ์ข…์‚ฌ์ž ์œค๋ฆฌ์ง€์นจ"]
597
+ )
598
+
599
+ col1, col2, col3 = st.columns(3)
600
+ with col1:
601
+ education_title = st.text_input("๊ต์œก๋ช…", value=(" / ".join(selected_items[:2]) + " ๊ต์œก") if selected_items else "๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก")
602
+ with col2:
603
+ instructor_name = st.text_input("๊ฐ•์‚ฌ๋ช…", value="")
604
+ with col3:
605
+ instructor_position = st.text_input("๊ฐ•์‚ฌ์ง์ฑ…", value="์‹œ์„ค์žฅ")
606
+
607
+ education_method = st.selectbox("๊ต์œก๋ฐฉ๋ฒ•", METHODS)
608
+ custom_content = st.text_area("๊ธฐ๊ด€ ์ถ”๊ฐ€ ๊ต์œก๋‚ด์šฉ", height=90, placeholder="๊ธฐ๊ด€ ์‚ฌ๋ก€, ์ตœ๊ทผ ์‚ฌ๊ณ ์˜ˆ๋ฐฉ ๋‚ด์šฉ, ๋ณด์™„ํ•  ์ง€์นจ ๋“ฑ์„ ์ž…๋ ฅํ•˜์„ธ์š”.")
609
+
610
+ st.subheader("2. ์ง์ข…๋ณ„ ์ฐธ์„์ž ์ž…๋ ฅ")
611
+ col1, col2, col3 = st.columns(3)
612
+ with col1:
613
+ att_director = st.text_input("์‹œ์„ค์žฅ", value="")
614
+ att_care = st.text_input("์š”์–‘๋ณดํ˜ธ์‚ฌ", value="")
615
+ with col2:
616
+ att_social = st.text_input("์‚ฌํšŒ๋ณต์ง€์‚ฌ", value="")
617
+ att_therapy = st.text_input("๋ฌผ๋ฆฌ/์ž‘์—…์น˜๋ฃŒ์‚ฌ", value="")
618
+ with col3:
619
+ att_nurse = st.text_input("๊ฐ„ํ˜ธ(์กฐ๋ฌด)์‚ฌ", value="")
620
+ att_etc = st.text_input("์กฐ๋ฆฌ์›/๊ธฐํƒ€", value="")
621
+
622
+ absent_staff = st.text_input("๋ฏธ์ฐธ์„์ž ๋ฐ ๋ณด์ถฉ๊ต์œก ๋Œ€์ƒ", value="")
623
+
624
+ attendee_dict = {
625
+ "์‹œ์„ค์žฅ": att_director,
626
+ "์‚ฌํšŒ๋ณต์ง€์‚ฌ": att_social,
627
+ "๊ฐ„ํ˜ธ(์กฐ๋ฌด)์‚ฌ": att_nurse,
628
+ "์š”์–‘๋ณดํ˜ธ์‚ฌ": att_care,
629
+ "๋ฌผ๋ฆฌ/์ž‘์—…์น˜๋ฃŒ์‚ฌ": att_therapy,
630
+ "์กฐ๋ฆฌ์›/๊ธฐํƒ€": att_etc,
631
+ }
632
+ attendees_text = "\n".join([f"{k}: {v}" for k, v in attendee_dict.items() if v.strip()])
633
+
634
+ st.subheader("3. ๊ต์œก์ž๋ฃŒ ์ฒจ๋ถ€")
635
+ uploaded_files = st.file_uploader("๊ต์œก์ž๋ฃŒ ์ด๋ฏธ์ง€/PDF ๋“ฑ ์ฒจ๋ถ€(์ตœ๋Œ€ 3๊ฐœ)", accept_multiple_files=True)
636
+ material_paths, material_names = save_uploaded_materials(uploaded_files)
637
+ education_materials = ", ".join(material_names) if material_names else "๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์ž๋ฃŒ, ๊ธฐ๊ด€ ์šด์˜๊ทœ์ •, ์‚ฌ๋ก€ ์ค‘์‹ฌ ๊ต์œก์ž๋ฃŒ"
638
+
639
+ purpose, content = build_education_text(selected_items, institution_type, education_method, custom_content)
640
+ result_text = build_result_text(selected_items, attendee_dict, absent_staff, education_method) if selected_items else ""
641
+ followup_plan = st.text_area(
642
+ "ํ›„์†์กฐ์น˜",
643
+ value="๋ฏธ์ฐธ์„์ž๋Š” 7์ผ ์ด๋‚ด ๋ณด์ถฉ๊ต์œก์„ ์‹ค์‹œํ•˜๊ณ , ์‹ ๊ทœ ์ข…์‚ฌ์ž๋Š” ์ž…์‚ฌ ์‹œ ๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ ๊ต์œก์„ ๋ณ„๋„๋กœ ์ง„ํ–‰ํ•œ๋‹ค. ๊ต์œก์ž๋ฃŒ๋Š” ์ข…์‚ฌ์ž๊ฐ€ ์ˆ˜์‹œ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฒŒ์‹œยท๋ณด๊ด€ํ•œ๋‹ค.",
644
+ height=90
645
+ )
646
+
647
+ with st.expander("์ž๋™ ์ƒ์„ฑ๋œ ๊ต์œก๋ชฉ์ /๋‚ด์šฉ/๊ฒฐ๊ณผ ํ™•์ธ ๋ฐ ์ˆ˜์ •", expanded=True):
648
+ education_purpose = st.text_area("๊ต์œก๋ชฉ์ ", value=purpose, height=130)
649
+ education_content = st.text_area("๊ต์œก๋‚ด์šฉ", value=content, height=220)
650
+ education_result = st.text_area("๊ต์œก๊ฒฐ๊ณผ", value=result_text, height=170)
651
+
652
+ submit = st.button("๊ต์œก์ผ์ง€ ์ƒ์„ฑ", type="primary", use_container_width=True)
653
+
654
+ if submit:
655
+ data = {
656
+ "institution_name": institution_name,
657
+ "institution_type": institution_type,
658
+ "education_title": education_title,
659
+ "guideline_items": ", ".join(selected_items),
660
+ "education_date": str(education_date),
661
+ "education_time": education_time,
662
+ "education_place": education_place,
663
+ "instructor_name": instructor_name,
664
+ "instructor_position": instructor_position,
665
+ "attendees": attendees_text,
666
+ "attendee_dict": attendee_dict,
667
+ "absent_staff": absent_staff,
668
+ "education_method": education_method,
669
+ "education_purpose": education_purpose,
670
+ "education_content": education_content,
671
+ "education_materials": education_materials,
672
+ "education_result": education_result,
673
+ "followup_plan": followup_plan,
674
+ "material_paths": material_paths,
675
+ }
676
+ score, missing, compliance = evaluate_education(data)
677
+ data["evaluation_score"] = score
678
+ data["missing_items"] = ", ".join(missing)
679
+ data["compliance_result"] = compliance
680
+
681
+ save_data = dict(data)
682
+ save_data.pop("attendee_dict", None)
683
+ save_data.pop("material_paths", None)
684
+ save_education_record(save_data)
685
+
686
+ excel_bytes = generate_excel(data)
687
+ st.success(f"๊ต์œก์ผ์ง€๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ‰๊ฐ€ ์ ํ•ฉ๋„: {score}์ ")
688
+ if missing:
689
+ st.warning("๋ณด์™„ ํ•„์š” ํ•ญ๋ชฉ: " + ", ".join(missing))
690
+ else:
691
+ st.info("๋ณด์™„ ํ•„์š” ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค.")
692
+
693
+ st.download_button(
694
+ label="์—‘์…€ ๊ต์œก์ผ์ง€ ๋‹ค์šด๋กœ๋“œ",
695
+ data=excel_bytes,
696
+ file_name=f"๊ธ‰์—ฌ์ œ๊ณต์ง€์นจ_๊ต์œก์ผ์ง€_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx",
697
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
698
+ use_container_width=True,
699
+ )
700
+
701
+ st.subheader("์ตœ๊ทผ ์ƒ์„ฑ ๊ธฐ๋ก")
702
+ df = load_education_records()
703
+ if not df.empty:
704
+ st.dataframe(df[["created_at", "institution_name", "institution_type", "education_title", "education_date", "evaluation_score"]].head(20), use_container_width=True)
705
+ else:
706
+ st.caption("์•„์ง ์ €์žฅ๋œ ๊ต์œก์ผ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")