LLDDWW commited on
Commit
262c670
Β·
1 Parent(s): f28f567

add swmirae4

Browse files
Files changed (1) hide show
  1. app.py +29 -55
app.py CHANGED
@@ -2,11 +2,7 @@
2
  # -----------------------------
3
  # HF Router Inference API (OpenAI-compatible)
4
  # Toss-like UI + "SW 미래 μ˜μž¬ν•™μ›" theme
5
- # Tabs:
6
- # 1) μ˜ˆμ•½Β·μΆ”μ²œ (슬둯 μ˜ˆμ•½ + 맞좀 μΆ”μ²œ)
7
- # 2) μ§ˆλ¬Έν•˜κΈ° (μ±„νŒ…)
8
- # 3) 학원 μ†Œκ°œ
9
- # 4) μƒνƒœ 점검
10
  # -----------------------------
11
 
12
  import os, json, time, requests, gradio as gr
@@ -17,7 +13,7 @@ from datetime import datetime, timedelta
17
  # Config
18
  # =========================
19
  API_BASE = os.getenv("HF_ENDPOINT_URL") or "https://router.huggingface.co/v1" # OpenAI-compatible Router
20
- MODEL_QA = os.getenv("HF_MODEL_ID") or "Qwen/Qwen2.5-7B-Instruct" # Provider-attached model
21
  HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
22
  TIMEOUT_S = 60
23
 
@@ -33,7 +29,6 @@ KWDAYS = ["μ›”", "ν™”", "수", "λͺ©", "금", "ν† ", "일"]
33
  # =========================
34
  def hf_chat(model: str, messages: list, temperature: float = 0.2, max_tokens: int = 256,
35
  timeout: int = TIMEOUT_S, retries: int = 3) -> str:
36
- """Call HF Router /v1/chat/completions with basic retry/backoff."""
37
  if not HF_TOKEN:
38
  return "⚠️ HF_TOKEN이 μ—†μŠ΅λ‹ˆλ‹€. ν™˜κ²½λ³€μˆ˜μ— HF_TOKEN을 μ„€μ •ν•˜μ„Έμš”."
39
  headers = {"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"}
@@ -79,7 +74,6 @@ def save_reservations(resv: List[Dict[str, Any]]) -> None:
79
  os.replace(tmp, RESV_FILE)
80
 
81
  def make_slots(start: datetime | None = None, days: int = SLOT_DAYS_AHEAD) -> List[str]:
82
- """Return list like 'YYYY-MM-DD HH:MM (μš”μΌ)' for open slots (Mon-Sat only)."""
83
  now = datetime.now()
84
  base = start or now
85
  out = []
@@ -95,12 +89,10 @@ def booked_slots() -> set:
95
  return {item["slot"] for item in load_reservations()}
96
 
97
  def available_slots() -> List[str]:
98
- # remove already booked
99
  bset = booked_slots()
100
  return [s for s in make_slots() if s not in bset]
101
 
102
- def resv_to_rows(resv: List[Dict[str, Any]]) -> List[List[str]]:
103
- # For Dataframe display
104
  rows = []
105
  for r in sorted(resv, key=lambda x: x.get("slot", "")):
106
  rows.append([
@@ -116,12 +108,11 @@ def resv_to_rows(resv: List[Dict[str, Any]]) -> List[List[str]]:
116
  return rows
117
 
118
  # =========================
119
- # Recommendation (LLM + fallback)
120
  # =========================
121
  def recommend_plan(grade: str, focuses: List[str], level: str, goals: str,
122
  weeks: int, hours_per_week: int, constraints: str,
123
  temperature: float, max_tokens: int) -> str:
124
- """Create a tailored plan via LLM. Falls back to rule-based if LLM fails."""
125
  focus_str = ", ".join(focuses) if focuses else "일반"
126
  user_profile = (
127
  f"- ν•™λ…„: {grade}\n- 관심 λΆ„μ•Ό: {focus_str}\n- μˆ˜μ€€: {level}\n"
@@ -139,7 +130,7 @@ def recommend_plan(grade: str, focuses: List[str], level: str, goals: str,
139
  "\n\nν˜•μ‹:\n"
140
  "1) ν•œ 쀄 μš”μ•½\n"
141
  "2) 주차별 κ³„νš(μ£Όμ°¨/ν•™μŠ΅λͺ©ν‘œ/ν™œλ™/과제)\n"
142
- "3) μΆ”μ²œ ꡐ재/툴(간단 링크λͺ…λ§Œ, μ‹€μ œ URL은 μƒλž΅)\n"
143
  "4) μ˜ˆμƒ κ²°κ³Όλ¬Ό(포트폴리였/λŒ€νšŒ/자격증)\n"
144
  "5) λ‹€μŒ μ˜ˆμ•½ μ œμ•ˆ(μ²΄ν—˜ μˆ˜μ—… 1회 + μ •κ·œ κ³Όμ •)\n"
145
  "λ¬Έμž₯ 짧게. λΆˆν•„μš”ν•œ μˆ˜μ‹μ–΄ κΈˆμ§€."
@@ -150,7 +141,6 @@ def recommend_plan(grade: str, focuses: List[str], level: str, goals: str,
150
  ]
151
  out = hf_chat(MODEL_QA, msgs, temperature=temperature, max_tokens=max_tokens)
152
  if out.startswith("❌") or out.startswith("⚠️"):
153
- # Fallback (간단 κ·œμΉ™)
154
  rec = f"""[간단 μΆ”μ²œμ•ˆ β€” Fallback]
155
  ν•œ 쀄 μš”μ•½: {grade} λŒ€μƒ {focus_str} {level} κ³Όμ •, {weeks}μ£Ό, μ£Ό {hours_per_week}μ‹œκ°„.
156
 
@@ -200,27 +190,26 @@ def reset_chat():
200
  return [], []
201
 
202
  # =========================
203
- # Booking handlers (Gradio)
204
  # =========================
205
- def refresh_slots() -> List[str]:
206
- return available_slots()
207
 
208
  def submit_booking(name: str, grade: str, focuses: List[str], level: str,
209
- contact: str, slot: str, notes: str) -> str:
210
  name = (name or "").strip()
211
  contact = (contact or "").strip()
212
  slot = (slot or "").strip()
213
  focus_str = ", ".join(focuses) if focuses else "일반"
214
 
215
  if not name:
216
- return "⚠️ 이름을 μž…λ ₯ν•˜μ„Έμš”."
217
  if not contact:
218
- return "⚠️ μ—°λ½μ²˜(μ „ν™”/카카였/이메일) μž…λ ₯ν•˜μ„Έμš”."
219
  if not slot:
220
- return "⚠️ μ˜ˆμ•½ μŠ¬λ‘―μ„ μ„ νƒν•˜μ„Έμš”."
221
- # re-check conflict
222
  if slot in booked_slots():
223
- return "❌ 이미 μ˜ˆμ•½λœ μŠ¬λ‘―μž…λ‹ˆλ‹€. λ‹€λ₯Έ μ‹œκ°„μ„ μ„ νƒν•˜μ„Έμš”."
224
 
225
  resv = load_reservations()
226
  item = {
@@ -235,13 +224,9 @@ def submit_booking(name: str, grade: str, focuses: List[str], level: str,
235
  }
236
  resv.append(item)
237
  save_reservations(resv)
238
- return f"βœ… μ˜ˆμ•½ μ™„λ£Œ: {slot} Β· {name} ({grade}, {focus_str}/{level}) β€” λ‹΄λ‹Ήμžκ°€ κ³§ μ—°λ½λ“œλ¦½λ‹ˆλ‹€."
239
-
240
- def get_reservation_table():
241
- resv = load_reservations()
242
- header = ["슬둯", "이름", "ν•™λ…„", "관심 λΆ„μ•Ό", "레벨", "μ—°λ½μ²˜", "λΉ„κ³ ", "μ˜ˆμ•½μ‹œκ°"]
243
- rows = resv_to_rows(resv)
244
- return [header] + rows if rows else [header]
245
 
246
  # =========================
247
  # Health check
@@ -306,12 +291,10 @@ with gr.Blocks(title="μœ¨ν•˜ SWλ―Έλž˜μ˜μž¬μ»΄ν“¨ν„°ν•™μ› β€” μ˜ˆμ•½Β·μΆ”μ²œΒ·Q
306
  """)
307
 
308
  with gr.Tabs():
309
- # =========================
310
  # 1) μ˜ˆμ•½Β·μΆ”μ²œ
311
- # =========================
312
  with gr.Tab("μ˜ˆμ•½ Β· μΆ”μ²œ"):
313
  with gr.Row():
314
- # ----- Left: Recommendation -----
315
  with gr.Column(scale=5):
316
  with gr.Group(elem_classes=["toss-card", "toss-input"]):
317
  gr.Markdown("### 맞좀 μΆ”μ²œ")
@@ -322,7 +305,7 @@ with gr.Blocks(title="μœ¨ν•˜ SWλ―Έλž˜μ˜μž¬μ»΄ν“¨ν„°ν•™μ› β€” μ˜ˆμ•½Β·μΆ”μ²œΒ·Q
322
  label="관심 λΆ„μ•Ό", value=["파이썬"]
323
  )
324
  level = gr.Radio(["μž…λ¬Έ", "기초", "쀑급", "심화"], value="기초", label="μˆ˜μ€€")
325
- goals = gr.Textbox(label="λͺ©ν‘œ(예: λŒ€νšŒ μž…μƒ/자격증/포트폴리였/학ꡐ μˆ˜ν–‰ν‰κ°€ λ“±)", lines=2)
326
  with gr.Row():
327
  weeks = gr.Slider(2, 16, value=8, step=1, label="κΈ°κ°„(μ£Ό)")
328
  hpw = gr.Slider(1, 6, value=2, step=1, label="μ£Όλ‹Ή μ‹œκ°„(μ‹œκ°„)")
@@ -334,7 +317,7 @@ with gr.Blocks(title="μœ¨ν•˜ SWλ―Έλž˜μ˜μž¬μ»΄ν“¨ν„°ν•™μ› β€” μ˜ˆμ•½Β·μΆ”μ²œΒ·Q
334
  with gr.Group(elem_classes=["toss-card"]):
335
  rec_out = gr.Textbox(label="μΆ”μ²œ κ²°κ³Ό", lines=18)
336
 
337
- # ----- Right: Booking -----
338
  with gr.Column(scale=4):
339
  with gr.Group(elem_classes=["toss-card", "toss-input"]):
340
  gr.Markdown("### μ˜ˆμ•½")
@@ -354,34 +337,29 @@ with gr.Blocks(title="μœ¨ν•˜ SWλ―Έλž˜μ˜μž¬μ»΄ν“¨ν„°ν•™μ› β€” μ˜ˆμ•½Β·μΆ”μ²œΒ·Q
354
  btn_book = gr.Button("μ˜ˆμ•½ ν™•μ •", elem_classes=["toss-primary"])
355
  with gr.Group(elem_classes=["toss-card"]):
356
  gr.Markdown("### ν˜„μž¬ μ˜ˆμ•½ ν˜„ν™©")
357
- table = gr.Dataframe(headers=["슬둯", "이름", "ν•™λ…„", "관심 λΆ„μ•Ό", "레벨", "μ—°λ½μ²˜", "λΉ„κ³ ", "μ˜ˆμ•½μ‹œκ°"],
358
- value=get_reservation_table(), interactive=False, wrap=True, height=260)
 
 
 
359
  res_msg = gr.Markdown("", elem_classes=["toss-note"])
360
 
361
- # Bind: recommend
362
  btn_rec.click(
363
  fn=recommend_plan,
364
  inputs=[grade, focus, level, goals, weeks, hpw, constraints, r_temp, r_max],
365
  outputs=[rec_out],
366
  )
367
-
368
- # Bind: refresh slots
369
- btn_refresh.click(fn=refresh_slots, outputs=[slot])
370
-
371
- # Bind: booking
372
- def _book_and_refresh(name_v, grade_v, focus_v, level_v, contact_v, slot_v, notes_v):
373
- msg = submit_booking(name_v, grade_v, focus_v, level_v, contact_v, slot_v, notes_v)
374
- # refresh dropdown + table
375
- return msg, get_reservation_table(), available_slots()
376
  btn_book.click(
377
- fn=_book_and_refresh,
378
  inputs=[name, b_grade, b_focus, b_level, contact, slot, notes],
379
  outputs=[res_msg, table, slot],
380
  )
381
 
382
- # =========================
383
  # 2) μ§ˆλ¬Έν•˜κΈ° (μ±„νŒ…)
384
- # =========================
385
  with gr.Tab("μ§ˆλ¬Έν•˜κΈ° (μ±„νŒ…)"):
386
  chat_history = gr.State([]) # list of (user, assistant)
387
  with gr.Row():
@@ -420,9 +398,7 @@ with gr.Blocks(title="μœ¨ν•˜ SWλ―Έλž˜μ˜μž¬μ»΄ν“¨ν„°ν•™μ› β€” μ˜ˆμ•½Β·μΆ”μ²œΒ·Q
420
  gr.Markdown('<div class="toss-note">β€’ λŒ€ν™” 기둝은 μœ„ μ±„νŒ… μ˜μ—­μ— μˆœμ„œλŒ€λ‘œ λˆ„μ λ©λ‹ˆλ‹€.'
421
  '<br>β€’ 과금 주의: Router μ‚¬μš©λŸ‰/토큰에 따라 λΉ„μš©μ΄ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.</div>')
422
 
423
- # =========================
424
  # 3) 학원 μ†Œκ°œ
425
- # =========================
426
  with gr.Tab("학원 μ†Œκ°œ"):
427
  with gr.Group(elem_classes=["toss-card"]):
428
  gr.Markdown("""
@@ -440,9 +416,7 @@ with gr.Blocks(title="μœ¨ν•˜ SWλ―Έλž˜μ˜μž¬μ»΄ν“¨ν„°ν•™μ› β€” μ˜ˆμ•½Β·μΆ”μ²œΒ·Q
440
  > 상담/μ²΄ν—˜ μˆ˜μ—… λ¬Έμ˜λŠ” 'μ˜ˆμ•½ Β· μΆ”μ²œ' νƒ­μ—μ„œ μ§„ν–‰ν•˜μ„Έμš”.
441
  """)
442
 
443
- # =========================
444
  # 4) μƒνƒœ 점검
445
- # =========================
446
  with gr.Tab("μƒνƒœ 점검"):
447
  with gr.Group(elem_classes=["toss-card"]):
448
  diag_btn = gr.Button("μ—”λ“œν¬μΈνŠΈ 점검", elem_classes=["toss-primary"])
 
2
  # -----------------------------
3
  # HF Router Inference API (OpenAI-compatible)
4
  # Toss-like UI + "SW 미래 μ˜μž¬ν•™μ›" theme
5
+ # Tabs: μ˜ˆμ•½Β·μΆ”μ²œ / 질문(μ±„νŒ…) / 학원 μ†Œκ°œ / μƒνƒœ 점검
 
 
 
 
6
  # -----------------------------
7
 
8
  import os, json, time, requests, gradio as gr
 
13
  # Config
14
  # =========================
15
  API_BASE = os.getenv("HF_ENDPOINT_URL") or "https://router.huggingface.co/v1" # OpenAI-compatible Router
16
+ MODEL_QA = os.getenv("HF_MODEL_ID") or "Qwen/Qwen2.5-7B-Instruct" # Provider-attached model
17
  HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
18
  TIMEOUT_S = 60
19
 
 
29
  # =========================
30
  def hf_chat(model: str, messages: list, temperature: float = 0.2, max_tokens: int = 256,
31
  timeout: int = TIMEOUT_S, retries: int = 3) -> str:
 
32
  if not HF_TOKEN:
33
  return "⚠️ HF_TOKEN이 μ—†μŠ΅λ‹ˆλ‹€. ν™˜κ²½λ³€μˆ˜μ— HF_TOKEN을 μ„€μ •ν•˜μ„Έμš”."
34
  headers = {"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"}
 
74
  os.replace(tmp, RESV_FILE)
75
 
76
  def make_slots(start: datetime | None = None, days: int = SLOT_DAYS_AHEAD) -> List[str]:
 
77
  now = datetime.now()
78
  base = start or now
79
  out = []
 
89
  return {item["slot"] for item in load_reservations()}
90
 
91
  def available_slots() -> List[str]:
 
92
  bset = booked_slots()
93
  return [s for s in make_slots() if s not in bset]
94
 
95
+ def reservation_rows(resv: List[Dict[str, Any]]) -> List[List[str]]:
 
96
  rows = []
97
  for r in sorted(resv, key=lambda x: x.get("slot", "")):
98
  rows.append([
 
108
  return rows
109
 
110
  # =========================
111
+ # Recommendation
112
  # =========================
113
  def recommend_plan(grade: str, focuses: List[str], level: str, goals: str,
114
  weeks: int, hours_per_week: int, constraints: str,
115
  temperature: float, max_tokens: int) -> str:
 
116
  focus_str = ", ".join(focuses) if focuses else "일반"
117
  user_profile = (
118
  f"- ν•™λ…„: {grade}\n- 관심 λΆ„μ•Ό: {focus_str}\n- μˆ˜μ€€: {level}\n"
 
130
  "\n\nν˜•μ‹:\n"
131
  "1) ν•œ 쀄 μš”μ•½\n"
132
  "2) 주차별 κ³„νš(μ£Όμ°¨/ν•™μŠ΅λͺ©ν‘œ/ν™œλ™/과제)\n"
133
+ "3) μΆ”μ²œ ꡐ재/툴(간단 링크λͺ…λ§Œ, μ‹€μ œ URL μƒλž΅)\n"
134
  "4) μ˜ˆμƒ κ²°κ³Όλ¬Ό(포트폴리였/λŒ€νšŒ/자격증)\n"
135
  "5) λ‹€μŒ μ˜ˆμ•½ μ œμ•ˆ(μ²΄ν—˜ μˆ˜μ—… 1회 + μ •κ·œ κ³Όμ •)\n"
136
  "λ¬Έμž₯ 짧게. λΆˆν•„μš”ν•œ μˆ˜μ‹μ–΄ κΈˆμ§€."
 
141
  ]
142
  out = hf_chat(MODEL_QA, msgs, temperature=temperature, max_tokens=max_tokens)
143
  if out.startswith("❌") or out.startswith("⚠️"):
 
144
  rec = f"""[간단 μΆ”μ²œμ•ˆ β€” Fallback]
145
  ν•œ 쀄 μš”μ•½: {grade} λŒ€μƒ {focus_str} {level} κ³Όμ •, {weeks}μ£Ό, μ£Ό {hours_per_week}μ‹œκ°„.
146
 
 
190
  return [], []
191
 
192
  # =========================
193
+ # Booking handlers
194
  # =========================
195
+ def refresh_slots_update():
196
+ return gr.update(choices=available_slots(), value=None)
197
 
198
  def submit_booking(name: str, grade: str, focuses: List[str], level: str,
199
+ contact: str, slot: str, notes: str):
200
  name = (name or "").strip()
201
  contact = (contact or "").strip()
202
  slot = (slot or "").strip()
203
  focus_str = ", ".join(focuses) if focuses else "일반"
204
 
205
  if not name:
206
+ return "⚠️ 이름을 μž…λ ₯ν•˜μ„Έμš”.", reservation_rows(load_reservations()), refresh_slots_update()
207
  if not contact:
208
+ return "⚠️ μ—°λ½μ²˜(μ „ν™”/카카였/이메일) μž…λ ₯ν•˜μ„Έμš”.", reservation_rows(load_reservations()), refresh_slots_update()
209
  if not slot:
210
+ return "⚠️ μ˜ˆμ•½ μŠ¬λ‘―μ„ μ„ νƒν•˜μ„Έμš”.", reservation_rows(load_reservations()), refresh_slots_update()
 
211
  if slot in booked_slots():
212
+ return "❌ 이미 μ˜ˆμ•½λœ μŠ¬λ‘―μž…λ‹ˆλ‹€. λ‹€λ₯Έ μ‹œκ°„μ„ μ„ νƒν•˜μ„Έμš”.", reservation_rows(load_reservations()), refresh_slots_update()
213
 
214
  resv = load_reservations()
215
  item = {
 
224
  }
225
  resv.append(item)
226
  save_reservations(resv)
227
+ return (f"βœ… μ˜ˆμ•½ μ™„λ£Œ: {slot} Β· {name} ({grade}, {focus_str}/{level}) β€” λ‹΄λ‹Ήμžκ°€ κ³§ μ—°λ½λ“œλ¦½λ‹ˆλ‹€.",
228
+ reservation_rows(resv),
229
+ refresh_slots_update())
 
 
 
 
230
 
231
  # =========================
232
  # Health check
 
291
  """)
292
 
293
  with gr.Tabs():
 
294
  # 1) μ˜ˆμ•½Β·μΆ”μ²œ
 
295
  with gr.Tab("μ˜ˆμ•½ Β· μΆ”μ²œ"):
296
  with gr.Row():
297
+ # Left: Recommendation
298
  with gr.Column(scale=5):
299
  with gr.Group(elem_classes=["toss-card", "toss-input"]):
300
  gr.Markdown("### 맞좀 μΆ”μ²œ")
 
305
  label="관심 λΆ„μ•Ό", value=["파이썬"]
306
  )
307
  level = gr.Radio(["μž…λ¬Έ", "기초", "쀑급", "심화"], value="기초", label="μˆ˜μ€€")
308
+ goals = gr.Textbox(label="λͺ©ν‘œ(예: λŒ€νšŒ μž…μƒ/자격증/포트폴리였/μˆ˜ν–‰ν‰κ°€ λ“±)", lines=2)
309
  with gr.Row():
310
  weeks = gr.Slider(2, 16, value=8, step=1, label="κΈ°κ°„(μ£Ό)")
311
  hpw = gr.Slider(1, 6, value=2, step=1, label="μ£Όλ‹Ή μ‹œκ°„(μ‹œκ°„)")
 
317
  with gr.Group(elem_classes=["toss-card"]):
318
  rec_out = gr.Textbox(label="μΆ”μ²œ κ²°κ³Ό", lines=18)
319
 
320
+ # Right: Booking
321
  with gr.Column(scale=4):
322
  with gr.Group(elem_classes=["toss-card", "toss-input"]):
323
  gr.Markdown("### μ˜ˆμ•½")
 
337
  btn_book = gr.Button("μ˜ˆμ•½ ν™•μ •", elem_classes=["toss-primary"])
338
  with gr.Group(elem_classes=["toss-card"]):
339
  gr.Markdown("### ν˜„μž¬ μ˜ˆμ•½ ν˜„ν™©")
340
+ table = gr.Dataframe(
341
+ headers=["슬둯", "이름", "ν•™λ…„", "관심 λΆ„μ•Ό", "레벨", "μ—°λ½μ²˜", "λΉ„κ³ ", "μ˜ˆμ•½μ‹œκ°"],
342
+ value=reservation_rows(load_reservations()),
343
+ interactive=False,
344
+ )
345
  res_msg = gr.Markdown("", elem_classes=["toss-note"])
346
 
347
+ # Recommend
348
  btn_rec.click(
349
  fn=recommend_plan,
350
  inputs=[grade, focus, level, goals, weeks, hpw, constraints, r_temp, r_max],
351
  outputs=[rec_out],
352
  )
353
+ # Refresh slots
354
+ btn_refresh.click(fn=refresh_slots_update, outputs=[slot])
355
+ # Booking
 
 
 
 
 
 
356
  btn_book.click(
357
+ fn=submit_booking,
358
  inputs=[name, b_grade, b_focus, b_level, contact, slot, notes],
359
  outputs=[res_msg, table, slot],
360
  )
361
 
 
362
  # 2) μ§ˆλ¬Έν•˜κΈ° (μ±„νŒ…)
 
363
  with gr.Tab("μ§ˆλ¬Έν•˜κΈ° (μ±„νŒ…)"):
364
  chat_history = gr.State([]) # list of (user, assistant)
365
  with gr.Row():
 
398
  gr.Markdown('<div class="toss-note">β€’ λŒ€ν™” 기둝은 μœ„ μ±„νŒ… μ˜μ—­μ— μˆœμ„œλŒ€λ‘œ λˆ„μ λ©λ‹ˆλ‹€.'
399
  '<br>β€’ 과금 주의: Router μ‚¬μš©λŸ‰/토큰에 따라 λΉ„μš©μ΄ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.</div>')
400
 
 
401
  # 3) 학원 μ†Œκ°œ
 
402
  with gr.Tab("학원 μ†Œκ°œ"):
403
  with gr.Group(elem_classes=["toss-card"]):
404
  gr.Markdown("""
 
416
  > 상담/μ²΄ν—˜ μˆ˜μ—… λ¬Έμ˜λŠ” 'μ˜ˆμ•½ Β· μΆ”μ²œ' νƒ­μ—μ„œ μ§„ν–‰ν•˜μ„Έμš”.
417
  """)
418
 
 
419
  # 4) μƒνƒœ 점검
 
420
  with gr.Tab("μƒνƒœ 점검"):
421
  with gr.Group(elem_classes=["toss-card"]):
422
  diag_btn = gr.Button("μ—”λ“œν¬μΈνŠΈ 점검", elem_classes=["toss-primary"])