QIDNLF commited on
Commit
2755a97
ยท
verified ยท
1 Parent(s): 94459ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -206
app.py CHANGED
@@ -6,8 +6,6 @@ import re
6
  import base64
7
  import difflib
8
  import traceback
9
- from functools import lru_cache
10
- from threading import Lock
11
 
12
  # --------------------------
13
  # 1๏ธโƒฃ ์ €์žฅ์†Œ ์„ค์ •
@@ -19,184 +17,39 @@ if not os.path.exists(UPLOAD_DIR):
19
  db_registry = []
20
 
21
  # --------------------------
22
- # 2๏ธโƒฃ DB ์—ฐ๊ฒฐ ์บ์‹ฑ (ํ•ต์‹ฌ ๊ฐœ์„ )
23
  # --------------------------
24
- _conn_cache: dict[str, sqlite3.Connection] = {}
25
- _conn_lock = Lock()
26
-
27
- def get_connection(db_path: str) -> sqlite3.Connection:
28
- """
29
- ๊ฐ™์€ ๊ฒฝ๋กœ์˜ DB๋Š” ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉ.
30
- DB ํŒŒ์ผ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๊ฒฝ์šฐ(mtime ๋ณ€ํ™”)๋ฅผ ๊ฐ์ง€ํ•ด ์ž๋™์œผ๋กœ ์žฌ์—ฐ๊ฒฐ.
31
- """
32
- mtime = os.path.getmtime(db_path)
33
- cache_key = f"{db_path}::{mtime}"
34
-
35
- with _conn_lock:
36
- if cache_key not in _conn_cache:
37
- # ๊ธฐ์กด ์—ฐ๊ฒฐ ์ •๋ฆฌ (๊ฐ™์€ path์˜ ๊ตฌ๋ฒ„์ „ ์—ฐ๊ฒฐ ์ œ๊ฑฐ)
38
- stale = [k for k in _conn_cache if k.startswith(db_path + "::")]
39
- for k in stale:
40
- try:
41
- _conn_cache[k].close()
42
- except Exception:
43
- pass
44
- del _conn_cache[k]
45
-
46
- conn = sqlite3.connect(db_path, check_same_thread=False)
47
- conn.text_factory = safe_text_factory
48
-
49
- # ์ธ๋ฑ์Šค ์ž๋™ ์ƒ์„ฑ (chapter, category, section ์ปฌ๋Ÿผ ๋Œ€์ƒ)
50
- _ensure_indexes(conn, db_path)
51
-
52
- _conn_cache[cache_key] = conn
53
-
54
- return _conn_cache[cache_key]
55
-
56
-
57
- def _ensure_indexes(conn: sqlite3.Connection, db_path: str):
58
- """์ž์ฃผ ์กฐํšŒ๋˜๋Š” ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๊ฐ€ ์—†์œผ๋ฉด ์ž๋™ ์ƒ์„ฑ."""
59
  try:
60
- tables = pd.read_sql(
61
- "SELECT name FROM sqlite_master WHERE type='table';", conn
62
- )['name'].tolist()
63
-
64
- for table in tables:
65
- cols_df = pd.read_sql(f"PRAGMA table_info([{table}])", conn)
66
- cols = [c.lower() for c in cols_df['name'].tolist()]
67
-
68
- for target_col in ['chapter', 'category', 'section']:
69
- if target_col in cols:
70
- real_col = cols_df['name'].tolist()[cols.index(target_col)]
71
- idx_name = f"idx_{table}_{target_col}"
72
- conn.execute(
73
- f"CREATE INDEX IF NOT EXISTS [{idx_name}] ON [{table}] ([{real_col}])"
74
- )
75
- conn.commit()
76
- except Exception:
77
- pass # ์ธ๋ฑ์Šค ์ƒ์„ฑ ์‹คํŒจ๋Š” ๋ฌด์‹œ (read-only DB ๋“ฑ ์˜ˆ์™ธ ์ƒํ™ฉ ๋Œ€์‘)
78
-
79
-
80
- # --------------------------
81
- # 3๏ธโƒฃ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์บ์‹ฑ
82
- # --------------------------
83
- _query_cache: dict[str, pd.DataFrame] = {}
84
-
85
- def _cache_key(*args) -> str:
86
- return "::".join(str(a) for a in args)
87
-
88
- def get_cached_df(key: str):
89
- return _query_cache.get(key)
90
-
91
- def set_cached_df(key: str, df: pd.DataFrame):
92
- # ์บ์‹œ๊ฐ€ ๋„ˆ๋ฌด ์ปค์ง€์ง€ ์•Š๋„๋ก 50๊ฐœ ์ดˆ๊ณผ ์‹œ ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ํ•ญ๋ชฉ ์ œ๊ฑฐ
93
- if len(_query_cache) >= 50:
94
- oldest = next(iter(_query_cache))
95
- del _query_cache[oldest]
96
- _query_cache[key] = df
97
-
98
 
99
- # --------------------------
100
- # ์œ ํ‹ธ
101
- # --------------------------
102
  def safe_text_factory(x):
 
103
  try:
104
  return x.decode('utf-8')
105
  except UnicodeDecodeError:
106
  return x
107
 
108
-
109
- def blob_to_base64_html(blob_data):
110
- if blob_data is None or (isinstance(blob_data, float) and pd.isna(blob_data)):
111
- return ""
112
- if isinstance(blob_data, (bytes, bytearray)):
113
- encoded = base64.b64encode(blob_data).decode('utf-8')
114
- return (
115
- f'<img src="data:image/png;base64,{encoded}" '
116
- f'style="width:50%;max-height:400px;object-fit:contain;'
117
- f'cursor:zoom-in;border:1px solid #ccc;border-radius:4px;'
118
- f'padding:2px;background-color:white;margin:5px 0;display:block;" '
119
- f'onclick="window.open(this.src)">'
120
- )
121
- return str(blob_data)
122
-
123
-
124
- def convert_blob_columns(df: pd.DataFrame) -> pd.DataFrame:
125
- """BLOB์ด ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ ๋ณ€ํ™˜ (๋ถˆํ•„์š”ํ•œ ์ˆœํšŒ ์ œ๊ฑฐ)."""
126
- for col in df.columns:
127
- # ์ƒ˜ํ”Œ ์ฒซ ํ–‰๋งŒ ํ™•์ธํ•ด์„œ bytes ์—ฌ๋ถ€ ํŒ๋‹จ
128
- sample = df[col].dropna().head(1)
129
- if not sample.empty and isinstance(sample.iloc[0], (bytes, bytearray)):
130
- df[col] = df[col].apply(blob_to_base64_html)
131
- return df
132
-
133
-
134
- _sort_key_cache: dict[str, list] = {}
135
-
136
  def natural_sort_key(s):
137
- """๊ฒฐ๊ณผ๋ฅผ ์บ์‹ฑํ•ด ๋™์ผ ๊ฐ’ ์žฌ๊ณ„์‚ฐ ๋ฐฉ์ง€."""
138
  if s is None:
139
  return []
140
- s = str(s)
141
- if s not in _sort_key_cache:
142
- _sort_key_cache[s] = [
143
- int(t) if t.isdigit() else t.lower()
144
- for t in re.split(r'(\d+)', s)
145
- ]
146
- return _sort_key_cache[s]
147
-
148
-
149
- def natural_sort_key_list(series: pd.Series):
150
- return series.map(natural_sort_key)
151
-
152
-
153
- # --------------------------
154
- # diff (์ตœ์ ํ™”)
155
- # --------------------------
156
- def highlight_diff(base: str, comp: str):
157
- """SequenceMatcher ๊ธฐ๋ฐ˜์œผ๋กœ ๊ต์ฒด โ€” ndiff๋ณด๋‹ค ๋น ๋ฆ„."""
158
- b_words = base.split()
159
- c_words = comp.split()
160
-
161
- sm = difflib.SequenceMatcher(None, b_words, c_words, autojunk=False)
162
- b_res, c_res = [], []
163
-
164
- for tag, i1, i2, j1, j2 in sm.get_opcodes():
165
- if tag == 'equal':
166
- b_res.extend(b_words[i1:i2])
167
- c_res.extend(c_words[j1:j2])
168
- elif tag == 'replace':
169
- for w in b_words[i1:i2]:
170
- b_res.append(f"<span style='color:#ff4d4f;font-weight:600'>{w}</span>")
171
- for w in c_words[j1:j2]:
172
- c_res.append(f"<span style='color:#ff4d4f;font-weight:600'>{w}</span>")
173
- elif tag == 'delete':
174
- for w in b_words[i1:i2]:
175
- b_res.append(f"<span style='color:#ff4d4f;font-weight:600'>{w}</span>")
176
- elif tag == 'insert':
177
- for w in c_words[j1:j2]:
178
- c_res.append(f"<span style='color:#2ecc71;font-weight:600'>{w}</span>")
179
-
180
- return " ".join(b_res), " ".join(c_res)
181
-
182
-
183
- def apply_diff_row(row, base_col, comp_col):
184
- base_val = str(row.get(base_col, "") or "")
185
- comp_val = str(row.get(comp_col, "") or "")
186
-
187
- if "<img" in base_val or "<img" in comp_val:
188
- return pd.Series([base_val, comp_val])
189
- if base_val == comp_val:
190
- return pd.Series([base_val, comp_val])
191
- if base_val and comp_val:
192
- b, c = highlight_diff(base_val, comp_val)
193
- return pd.Series([b, c])
194
- return pd.Series([base_val, comp_val])
195
-
196
 
197
- # --------------------------
198
- # ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ
199
- # --------------------------
200
  def refresh_registry_data():
201
  global db_registry
202
  db_registry = []
@@ -214,7 +67,6 @@ def refresh_registry_data():
214
 
215
  return sorted(list(set([d["standard"] for d in db_registry])))
216
 
217
-
218
  # --------------------------
219
  # ๋“œ๋กญ๋‹ค์šด
220
  # --------------------------
@@ -224,7 +76,9 @@ def on_load():
224
  def update_version_dd(standard):
225
  if not standard:
226
  return gr.Dropdown(choices=[])
227
- versions = sorted(set(d["version"] for d in db_registry if d["standard"] == standard))
 
 
228
  return gr.Dropdown(choices=versions)
229
 
230
  def update_category_dd(standard, version):
@@ -235,7 +89,10 @@ def update_category_dd(standard, version):
235
 
236
  try:
237
  target = next(d for d in db_registry if d["standard"] == standard and d["version"] == version)
238
- conn = get_connection(target["path"])
 
 
 
239
 
240
  tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist()
241
  main_t = f"{standard}_{version}"
@@ -245,26 +102,35 @@ def update_category_dd(standard, version):
245
  cols = pd.read_sql(f"PRAGMA table_info([{main_t}])", conn)['name'].tolist()
246
  lower = [c.lower() for c in cols]
247
 
 
248
  if 'chapter' in lower and 'category' in lower:
249
  ch = cols[lower.index('chapter')]
250
  ca = cols[lower.index('category')]
 
251
  df = pd.read_sql(f"SELECT DISTINCT [{ch}], [{ca}] FROM [{main_t}]", conn)
 
252
  for _, r in df.iterrows():
253
  choices.append(f"{str(r[ch]).strip()}.{str(r[ca]).strip()}")
254
 
 
255
  pattern = re.compile(f"^{standard}[_\\s-]*{version}[_\\s-]*", re.IGNORECASE)
 
256
  for t in tables:
257
  if t == main_t:
258
  continue
 
 
259
  short_name = pattern.sub("", t).strip(" _")
260
  choices.append(short_name)
261
 
262
- except Exception:
 
 
 
263
  traceback.print_exc()
264
 
265
  return gr.Dropdown(choices=choices)
266
 
267
-
268
  # --------------------------
269
  # ์ดˆ๊ธฐํ™”
270
  # --------------------------
@@ -274,35 +140,74 @@ def reset_base():
274
  def reset_comp():
275
  return None, None, None
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
  # --------------------------
279
- # ๋ฐ์ดํ„ฐ ์กฐํšŒ (์บ์‹ฑ ์ ์šฉ)
280
  # --------------------------
281
  def display_data(standard, version, selection):
282
  if not all([standard, version, selection]):
283
  return pd.DataFrame({"Info": ["์„ ํƒ ํ•„์š”"]})
284
 
285
- # ์บ์‹œ ํ™•์ธ
286
- ck = _cache_key(standard, version, selection)
287
- cached = get_cached_df(ck)
288
- if cached is not None:
289
- return cached.copy()
290
-
291
  try:
292
  target = next(d for d in db_registry if d["standard"] == standard and d["version"] == version)
293
- conn = get_connection(target["path"])
 
 
 
294
 
295
  tables = pd.read_sql(
296
- "SELECT name FROM sqlite_master WHERE type='table';", conn
 
297
  )['name'].tolist()
298
 
299
  main_t = f"{standard}_{version}"
300
  if main_t not in tables:
301
  main_t = tables[0]
302
 
303
- # ํ…Œ์ด๋ธ” reverse lookup
 
 
304
  full_table_name = None
305
  pattern = re.compile(f"^{standard}[_\\s-]*{version}[_\\s-]*", re.IGNORECASE)
 
306
  for t in tables:
307
  if t == selection:
308
  full_table_name = t
@@ -312,23 +217,31 @@ def display_data(standard, version, selection):
312
  full_table_name = t
313
  break
314
 
315
- # TableA/B ์„ ํƒ
 
 
316
  if full_table_name:
317
  cursor = conn.cursor()
318
  cursor.execute(f"SELECT * FROM [{full_table_name}]")
 
319
  rows = cursor.fetchall()
320
  cols = [desc[0] for desc in cursor.description]
321
 
322
  df = pd.DataFrame(rows, columns=cols)
 
323
  df.columns = [c.strip() for c in df.columns]
324
  df = df.drop(columns=[c for c in df.columns if c.lower() == "version"], errors="ignore")
325
  df.columns = [c.replace("_", " ").title() for c in df.columns]
326
- df = convert_blob_columns(df)
327
 
328
- set_cached_df(ck, df)
329
- return df.copy()
330
 
331
- # ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ํŒŒ์•…
 
 
 
 
 
332
  cursor = conn.cursor()
333
  cursor.execute(f"PRAGMA table_info([{main_t}])")
334
  cols_info = cursor.fetchall()
@@ -342,48 +255,85 @@ def display_data(standard, version, selection):
342
  elif lc == "category":
343
  real_cat = c
344
 
 
 
 
345
  if selection == "ALL":
 
346
  cursor.execute(f"SELECT * FROM [{main_t}]")
347
 
 
 
 
348
  elif "." in selection and real_ch and real_cat:
349
  ch, ca = selection.split(".", 1)
 
350
  query = f"""
351
  SELECT * FROM [{main_t}]
352
  WHERE REPLACE(TRIM(CAST([{real_ch}] AS TEXT)), ' ', '') = ?
353
  AND REPLACE(TRIM(CAST([{real_cat}] AS TEXT)), ' ', '') = ?
354
  """
 
355
  cursor.execute(query, (ch.replace(" ", ""), ca.replace(" ", "")))
356
 
 
 
 
357
  else:
358
  cursor.execute(f"SELECT * FROM [{main_t}]")
359
 
360
  rows = cursor.fetchall()
361
  df = pd.DataFrame(rows, columns=cols)
 
 
 
 
362
  df.columns = [c.lower().strip() for c in df.columns]
363
 
364
  if 'section' in df.columns and 'description' in df.columns:
365
  df = df[['section', 'description']]
366
 
367
- df = convert_blob_columns(df)
 
 
368
 
369
- set_cached_df(ck, df)
370
- return df.copy()
371
 
372
  except Exception as e:
 
373
  traceback.print_exc()
374
  return pd.DataFrame({"Error": [str(e)]})
375
 
376
-
377
  # --------------------------
378
- # ํ†ตํ•ฉ ์กฐํšŒ
379
  # --------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  def unified_search(bs, bv, bc, cs, cv, cc):
381
- # ๋‹จ์ผ ์กฐํšŒ
382
  if bs and bv and bc and not (cs and cv and cc):
383
  df = display_data(bs, bv, bc)
384
  return df.rename(columns={"section": "Section", "description": "Description"})
385
 
386
- # ๋น„๊ต ์กฐํšŒ
387
  if bs and bv and bc and cs and cv and cc:
388
  df_base = display_data(bs, bv, bc)
389
  df_comp = display_data(cs, cv, cc)
@@ -391,32 +341,52 @@ def unified_search(bs, bv, bc, cs, cv, cc):
391
  if 'section' not in df_base.columns or 'section' not in df_comp.columns:
392
  return pd.DataFrame({"Error": ["section ์—†์Œ"]})
393
 
394
- df_base = df_base.rename(columns={"section": f"Section_{bv}", "description": f"Description_{bv}"})
395
- df_comp = df_comp.rename(columns={"section": f"Section_{cv}", "description": f"Description_{cv}"})
 
 
 
 
 
 
 
396
 
397
  base_sec = f"Section_{bv}"
398
  comp_sec = f"Section_{cv}"
399
  base_col = f"Description_{bv}"
400
  comp_col = f"Description_{cv}"
401
 
402
- merged = pd.merge(df_base, df_comp, left_on=base_sec, right_on=comp_sec, how="outer")
 
 
 
 
 
 
 
 
403
  merged = merged.fillna("")
404
 
 
405
  merged[[base_col, comp_col]] = merged.apply(
406
- lambda row: apply_diff_row(row, base_col, comp_col), axis=1
 
 
 
 
 
 
407
  )
408
 
409
- merged["__sort_key"] = merged[base_sec].where(merged[base_sec] != "", merged[comp_sec])
410
  merged = merged.sort_values(
411
  by="__sort_key",
412
- key=natural_sort_key_list
413
  ).drop(columns="__sort_key")
414
 
415
  return merged
416
 
417
  return pd.DataFrame({"Info": ["์„ ํƒ ํ•„์š”"]})
418
 
419
-
420
  # --------------------------
421
  # UI
422
  # --------------------------
@@ -460,7 +430,6 @@ with gr.Blocks() as demo:
460
  base_reset_btn.click(reset_base, None, [base_standard, base_version, base_category])
461
  comp_reset_btn.click(reset_comp, None, [comp_standard, comp_version, comp_category])
462
 
463
-
464
  # --------------------------
465
  # CSS
466
  # --------------------------
@@ -469,27 +438,63 @@ table {
469
  table-layout: fixed !important;
470
  width: 100% !important;
471
  }
 
 
 
 
472
  table th:nth-last-child(2):first-child, table td:nth-last-child(2):first-child { width: 15% !important; }
473
  table th:nth-last-child(1), table td:nth-last-child(1) { width: 85% !important; }
 
 
 
 
474
  table th:nth-child(1):nth-last-child(4), table td:nth-child(1):nth-last-child(4) { width: 7% !important; }
475
  table th:nth-child(2):nth-last-child(3), table td:nth-child(2):nth-last-child(3) { width: 43% !important; }
476
  table th:nth-child(3):nth-last-child(2), table td:nth-child(3):nth-last-child(2) { width: 7% !important; }
477
  table th:nth-child(4):nth-last-child(1), table td:nth-child(4):nth-last-child(1) { width: 43% !important; }
 
 
 
 
478
  table th:nth-child(1):nth-last-child(5), table td:nth-child(1):nth-last-child(5) { width: 15% !important; }
479
  table th:nth-child(2):nth-last-child(4), table td:nth-child(2):nth-last-child(4) { width: 10% !important; }
480
  table th:nth-child(3):nth-last-child(3), table td:nth-child(3):nth-last-child(3) { width: 30% !important; }
481
  table th:nth-child(4):nth-last-child(2), table td:nth-child(4):nth-last-child(2) { width: 10% !important; }
482
  table th:nth-child(5):nth-last-child(1), table td:nth-child(5):nth-last-child(1) { width: 35% !important; }
 
 
 
 
483
  table th:nth-child(1):nth-last-child(6), table td:nth-child(1):nth-last-child(6) { width: 8% !important; }
484
  table th:nth-child(2):nth-last-child(5), table td:nth-child(2):nth-last-child(5) { width: 25% !important; }
485
  table th:nth-child(3):nth-last-child(4), table td:nth-child(3):nth-last-child(4) { width: 8% !important; }
486
  table th:nth-child(4):nth-last-child(3), table td:nth-child(4):nth-last-child(3) { width: 25% !important; }
487
  table th:nth-child(5):nth-last-child(2), table td:nth-child(5):nth-last-child(2) { width: 8% !important; }
488
  table th:nth-child(6):nth-last-child(1), table td:nth-child(6):nth-last-child(1) { width: 26% !important; }
489
- thead th { position: sticky; top: 0; background: white; z-index: 10; }
490
- .dataframe { max-height: none !important; overflow-y: visible !important; }
491
- .dataframe > div { max-height: none !important; overflow: visible !important; }
492
- td { white-space: pre-wrap !important; word-break: break-word !important; line-height: 1.6; padding: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  """
494
 
495
  if __name__ == "__main__":
 
6
  import base64
7
  import difflib
8
  import traceback
 
 
9
 
10
  # --------------------------
11
  # 1๏ธโƒฃ ์ €์žฅ์†Œ ์„ค์ •
 
17
  db_registry = []
18
 
19
  # --------------------------
20
+ # ์œ ํ‹ธ
21
  # --------------------------
22
+ def blob_to_base64_html(blob_data):
23
+ if blob_data is None or pd.isna(blob_data):
24
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  try:
26
+ if isinstance(blob_data, (bytes, bytearray)):
27
+ encoded = base64.b64encode(blob_data).decode('utf-8')
28
+ return f'''
29
+ <img src="data:image/png;base64,{encoded}"
30
+ style="width: 50%; max-height: 400px; object-fit: contain;
31
+ cursor: zoom-in; border: 1px solid #ccc; border-radius: 4px;
32
+ padding: 2px; background-color: white; margin: 5px 0; display: block;"
33
+ onclick="window.open(this.src)">
34
+ '''
35
+ return str(blob_data)
36
+ except Exception as e:
37
+ return str(blob_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ # ๐Ÿš€ [ํ•ต์‹ฌ ์ถ”๊ฐ€] ํ…์ŠคํŠธ ๋””์ฝ”๋”ฉ ์—๋Ÿฌ ๋ฐฉ์ง€์šฉ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜
 
 
40
  def safe_text_factory(x):
41
+ """ํ…์ŠคํŠธ ๋””์ฝ”๋”ฉ์„ ์‹œ๋„ํ•˜๊ณ , ์‹คํŒจํ•˜๋ฉด(PNG ๋“ฑ ์ด๋ฏธ์ง€์ผ ๊ฒฝ์šฐ) ์›๋ณธ ๋ฐ”์ดํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜"""
42
  try:
43
  return x.decode('utf-8')
44
  except UnicodeDecodeError:
45
  return x
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def natural_sort_key(s):
 
48
  if s is None:
49
  return []
50
+ return [int(t) if t.isdigit() else t.lower()
51
+ for t in re.split(r'(\d+)', str(s))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
 
 
 
53
  def refresh_registry_data():
54
  global db_registry
55
  db_registry = []
 
67
 
68
  return sorted(list(set([d["standard"] for d in db_registry])))
69
 
 
70
  # --------------------------
71
  # ๋“œ๋กญ๋‹ค์šด
72
  # --------------------------
 
76
  def update_version_dd(standard):
77
  if not standard:
78
  return gr.Dropdown(choices=[])
79
+ versions = sorted(set(
80
+ d["version"] for d in db_registry if d["standard"] == standard
81
+ ))
82
  return gr.Dropdown(choices=versions)
83
 
84
  def update_category_dd(standard, version):
 
89
 
90
  try:
91
  target = next(d for d in db_registry if d["standard"] == standard and d["version"] == version)
92
+ conn = sqlite3.connect(target["path"])
93
+
94
+ # ๐Ÿš€ [ํ•ต์‹ฌ ์ถ”๊ฐ€] ์นดํ…Œ๊ณ ๋ฆฌ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ๋„ ์ด๋ฏธ์ง€ ์—๋Ÿฌ ์ฐจ๋‹จ
95
+ conn.text_factory = safe_text_factory
96
 
97
  tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist()
98
  main_t = f"{standard}_{version}"
 
102
  cols = pd.read_sql(f"PRAGMA table_info([{main_t}])", conn)['name'].tolist()
103
  lower = [c.lower() for c in cols]
104
 
105
+ # 1. ์ฑ•ํ„ฐ/์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ์ค€ ๋กœ์ง (๊ธฐ์กด ์œ ์ง€)
106
  if 'chapter' in lower and 'category' in lower:
107
  ch = cols[lower.index('chapter')]
108
  ca = cols[lower.index('category')]
109
+
110
  df = pd.read_sql(f"SELECT DISTINCT [{ch}], [{ca}] FROM [{main_t}]", conn)
111
+
112
  for _, r in df.iterrows():
113
  choices.append(f"{str(r[ch]).strip()}.{str(r[ca]).strip()}")
114
 
115
+ # 2. ํ…Œ์ด๋ธ” ์ด๋ฆ„ ๊น”๋”ํ•˜๊ฒŒ ์ž๋ฅด๊ธฐ (๊ฐ•๋ ฅํ•œ ์ •๊ทœ์‹ ์ ์šฉ)
116
  pattern = re.compile(f"^{standard}[_\\s-]*{version}[_\\s-]*", re.IGNORECASE)
117
+
118
  for t in tables:
119
  if t == main_t:
120
  continue
121
+
122
+ # ๋Œ€์†Œ๋ฌธ์ž, ์–ธ๋”๋ฐ”, ๊ณต๋ฐฑ์„ ์ฐฐ๋–ก๊ฐ™์ด ๋ฌด์‹œํ•˜๊ณ  ๊ธฐ์ค€ ์ ‘๋‘์‚ฌ๋ฅผ ๋‚ ๋ ค๋ฒ„๋ฆผ
123
  short_name = pattern.sub("", t).strip(" _")
124
  choices.append(short_name)
125
 
126
+ conn.close()
127
+
128
+ except Exception as e:
129
+ print(f"[Error in update_category_dd] standard: {standard}, version: {version}")
130
  traceback.print_exc()
131
 
132
  return gr.Dropdown(choices=choices)
133
 
 
134
  # --------------------------
135
  # ์ดˆ๊ธฐํ™”
136
  # --------------------------
 
140
  def reset_comp():
141
  return None, None, None
142
 
143
+ # --------------------------
144
+ # diff (ํ•˜์ด๋ผ์ดํŒ… ๋กœ์ง)
145
+ # --------------------------
146
+ def highlight_diff(base, comp):
147
+ base = "" if pd.isna(base) else str(base)
148
+ comp = "" if pd.isna(comp) else str(comp)
149
+
150
+ b_words = base.split()
151
+ c_words = comp.split()
152
+
153
+ d = list(difflib.ndiff(b_words, c_words))
154
+
155
+ b_res, c_res = []
156
+ i = 0
157
+
158
+ while i < len(d):
159
+ code = d[i][0]
160
+ word = d[i][2:]
161
+
162
+ if code == ' ':
163
+ b_res.append(word)
164
+ c_res.append(word)
165
+
166
+ elif code == '-' and i+1 < len(d) and d[i+1][0] == '+':
167
+ new_word = d[i+1][2:]
168
+ b_res.append(f"<span style='color:#ff4d4f;font-weight:600'>{word}</span>")
169
+ c_res.append(f"<span style='color:#ff4d4f;font-weight:600'>{new_word}</span>")
170
+ i += 1
171
+
172
+ elif code == '-':
173
+ b_res.append(f"<span style='color:#ff4d4f;font-weight:600'>{word}</span>")
174
+
175
+ elif code == '+':
176
+ c_res.append(f"<span style='color:#2ecc71;font-weight:600'>{word}</span>")
177
+
178
+ i += 1
179
+
180
+ return " ".join(b_res), " ".join(c_res)
181
 
182
  # --------------------------
183
+ # ๋ฐ์ดํ„ฐ ์กฐํšŒ
184
  # --------------------------
185
  def display_data(standard, version, selection):
186
  if not all([standard, version, selection]):
187
  return pd.DataFrame({"Info": ["์„ ํƒ ํ•„์š”"]})
188
 
 
 
 
 
 
 
189
  try:
190
  target = next(d for d in db_registry if d["standard"] == standard and d["version"] == version)
191
+ conn = sqlite3.connect(target["path"])
192
+
193
+ # ๐Ÿš€ [ํ•ต์‹ฌ ์ถ”๊ฐ€] ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ ์ด๋ฏธ์ง€ ์—๋Ÿฌ ์™„๋ฒฝ ์ฐจ๋‹จ!
194
+ conn.text_factory = safe_text_factory
195
 
196
  tables = pd.read_sql(
197
+ "SELECT name FROM sqlite_master WHERE type='table';",
198
+ conn
199
  )['name'].tolist()
200
 
201
  main_t = f"{standard}_{version}"
202
  if main_t not in tables:
203
  main_t = tables[0]
204
 
205
+ # --------------------------
206
+ # ๐Ÿ”น ํ…Œ์ด๋ธ” reverse lookup
207
+ # --------------------------
208
  full_table_name = None
209
  pattern = re.compile(f"^{standard}[_\\s-]*{version}[_\\s-]*", re.IGNORECASE)
210
+
211
  for t in tables:
212
  if t == selection:
213
  full_table_name = t
 
217
  full_table_name = t
218
  break
219
 
220
+ # --------------------------
221
+ # ๐Ÿ”น TableA/B ์„ ํƒ
222
+ # --------------------------
223
  if full_table_name:
224
  cursor = conn.cursor()
225
  cursor.execute(f"SELECT * FROM [{full_table_name}]")
226
+
227
  rows = cursor.fetchall()
228
  cols = [desc[0] for desc in cursor.description]
229
 
230
  df = pd.DataFrame(rows, columns=cols)
231
+
232
  df.columns = [c.strip() for c in df.columns]
233
  df = df.drop(columns=[c for c in df.columns if c.lower() == "version"], errors="ignore")
234
  df.columns = [c.replace("_", " ").title() for c in df.columns]
 
235
 
236
+ for col in df.columns:
237
+ df[col] = df[col].apply(blob_to_base64_html)
238
 
239
+ conn.close()
240
+ return df
241
+
242
+ # --------------------------
243
+ # ๐Ÿ”น ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ
244
+ # --------------------------
245
  cursor = conn.cursor()
246
  cursor.execute(f"PRAGMA table_info([{main_t}])")
247
  cols_info = cursor.fetchall()
 
255
  elif lc == "category":
256
  real_cat = c
257
 
258
+ # --------------------------
259
+ # ๐Ÿ”น ALL ์„ ํƒ
260
+ # --------------------------
261
  if selection == "ALL":
262
+ # ๐Ÿš€ [์ˆ˜์ •] ํ…์ŠคํŠธ ํŒฉํ† ๋ฆฌ๋กœ ์—๋Ÿฌ๋ฅผ ์žก์•˜์œผ๋ฏ€๋กœ, ๋ถˆํ•„์š”ํ•œ ํ•„ํ„ฐ ์กฐ๊ฑด ์‚ญ์ œ
263
  cursor.execute(f"SELECT * FROM [{main_t}]")
264
 
265
+ # --------------------------
266
+ # ๐Ÿ”น chapter.category
267
+ # --------------------------
268
  elif "." in selection and real_ch and real_cat:
269
  ch, ca = selection.split(".", 1)
270
+
271
  query = f"""
272
  SELECT * FROM [{main_t}]
273
  WHERE REPLACE(TRIM(CAST([{real_ch}] AS TEXT)), ' ', '') = ?
274
  AND REPLACE(TRIM(CAST([{real_cat}] AS TEXT)), ' ', '') = ?
275
  """
276
+
277
  cursor.execute(query, (ch.replace(" ", ""), ca.replace(" ", "")))
278
 
279
+ # --------------------------
280
+ # ๐Ÿ”น fallback
281
+ # --------------------------
282
  else:
283
  cursor.execute(f"SELECT * FROM [{main_t}]")
284
 
285
  rows = cursor.fetchall()
286
  df = pd.DataFrame(rows, columns=cols)
287
+
288
+ # --------------------------
289
+ # ๐Ÿ”น ํ›„์ฒ˜๋ฆฌ
290
+ # --------------------------
291
  df.columns = [c.lower().strip() for c in df.columns]
292
 
293
  if 'section' in df.columns and 'description' in df.columns:
294
  df = df[['section', 'description']]
295
 
296
+ # ๐Ÿ”ฅ ํ•ต์‹ฌ: blob โ†’ ์ด๋ฏธ์ง€ ๋ณ€ํ™˜ (์‚ญ์ œ X)
297
+ for col in df.columns:
298
+ df[col] = df[col].apply(blob_to_base64_html)
299
 
300
+ conn.close()
301
+ return df
302
 
303
  except Exception as e:
304
+ print(f"[Error in display_data]")
305
  traceback.print_exc()
306
  return pd.DataFrame({"Error": [str(e)]})
307
 
 
308
  # --------------------------
309
+ # ํ†ตํ•ฉ ์กฐํšŒ (apply๋กœ ์†๋„ ์ตœ์ ํ™”)
310
  # --------------------------
311
+ def apply_diff_row(row, base_col, comp_col):
312
+ """Pandas apply๋ฅผ ์œ„ํ•œ Diff ์—ฐ์‚ฐ ๋ž˜ํผ ํ•จ์ˆ˜"""
313
+ base_val = str(row.get(base_col, ""))
314
+ comp_val = str(row.get(comp_col, ""))
315
+
316
+ # 1. ์ด๋ฏธ์ง€๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ diff ์ƒ๋žต
317
+ if "<img" in base_val or "<img" in comp_val:
318
+ return pd.Series([base_val, comp_val])
319
+
320
+ # 2. [ํ•ต์‹ฌ] ๋‘ ํ…์ŠคํŠธ๊ฐ€ ์™„์ „ํžˆ ๋˜‘๊ฐ™์œผ๋ฉด ๋ฌด๊ฑฐ์šด diff ์—ฐ์‚ฐ ์ƒ๋žต (Fast-path)
321
+ if base_val == comp_val:
322
+ return pd.Series([base_val, comp_val])
323
+
324
+ # 3. ๋‚ด์šฉ์ด ์„œ๋กœ ๋‹ค๋ฅผ ๋•Œ๋งŒ ๋‹จ์–ด ๋‹จ์œ„ diff ์—ฐ์‚ฐ ์ˆ˜ํ–‰
325
+ if base_val and comp_val:
326
+ b, c = highlight_diff(base_val, comp_val)
327
+ return pd.Series([b, c])
328
+
329
+ return pd.Series([base_val, comp_val])
330
+
331
+
332
  def unified_search(bs, bv, bc, cs, cv, cc):
 
333
  if bs and bv and bc and not (cs and cv and cc):
334
  df = display_data(bs, bv, bc)
335
  return df.rename(columns={"section": "Section", "description": "Description"})
336
 
 
337
  if bs and bv and bc and cs and cv and cc:
338
  df_base = display_data(bs, bv, bc)
339
  df_comp = display_data(cs, cv, cc)
 
341
  if 'section' not in df_base.columns or 'section' not in df_comp.columns:
342
  return pd.DataFrame({"Error": ["section ์—†์Œ"]})
343
 
344
+ df_base = df_base.rename(columns={
345
+ "section": f"Section_{bv}",
346
+ "description": f"Description_{bv}"
347
+ })
348
+
349
+ df_comp = df_comp.rename(columns={
350
+ "section": f"Section_{cv}",
351
+ "description": f"Description_{cv}"
352
+ })
353
 
354
  base_sec = f"Section_{bv}"
355
  comp_sec = f"Section_{cv}"
356
  base_col = f"Description_{bv}"
357
  comp_col = f"Description_{cv}"
358
 
359
+ merged = pd.merge(
360
+ df_base,
361
+ df_comp,
362
+ left_on=base_sec,
363
+ right_on=comp_sec,
364
+ how="outer"
365
+ )
366
+
367
+ # NaN ๊ฐ’์„ ๋นˆ ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ
368
  merged = merged.fillna("")
369
 
370
+ # ์ตœ์ ํ™” ํฌ์ธํŠธ: iterrows() ๋Œ€์‹  apply() ์‚ฌ์šฉ
371
  merged[[base_col, comp_col]] = merged.apply(
372
+ lambda row: apply_diff_row(row, base_col, comp_col),
373
+ axis=1
374
+ )
375
+
376
+ # ์ •๋ ฌ ๊ฐœ์„ 
377
+ merged["__sort_key"] = merged[base_sec].where(
378
+ merged[base_sec] != "", merged[comp_sec]
379
  )
380
 
 
381
  merged = merged.sort_values(
382
  by="__sort_key",
383
+ key=lambda col: col.map(natural_sort_key)
384
  ).drop(columns="__sort_key")
385
 
386
  return merged
387
 
388
  return pd.DataFrame({"Info": ["์„ ํƒ ํ•„์š”"]})
389
 
 
390
  # --------------------------
391
  # UI
392
  # --------------------------
 
430
  base_reset_btn.click(reset_base, None, [base_standard, base_version, base_category])
431
  comp_reset_btn.click(reset_comp, None, [comp_standard, comp_version, comp_category])
432
 
 
433
  # --------------------------
434
  # CSS
435
  # --------------------------
 
438
  table-layout: fixed !important;
439
  width: 100% !important;
440
  }
441
+
442
+ /* -------------------------- */
443
+ /* 2์ปฌ๋Ÿผ */
444
+ /* -------------------------- */
445
  table th:nth-last-child(2):first-child, table td:nth-last-child(2):first-child { width: 15% !important; }
446
  table th:nth-last-child(1), table td:nth-last-child(1) { width: 85% !important; }
447
+
448
+ /* -------------------------- */
449
+ /* 4์ปฌ๋Ÿผ */
450
+ /* -------------------------- */
451
  table th:nth-child(1):nth-last-child(4), table td:nth-child(1):nth-last-child(4) { width: 7% !important; }
452
  table th:nth-child(2):nth-last-child(3), table td:nth-child(2):nth-last-child(3) { width: 43% !important; }
453
  table th:nth-child(3):nth-last-child(2), table td:nth-child(3):nth-last-child(2) { width: 7% !important; }
454
  table th:nth-child(4):nth-last-child(1), table td:nth-child(4):nth-last-child(1) { width: 43% !important; }
455
+
456
+ /* -------------------------- */
457
+ /* 5์ปฌ๋Ÿผ */
458
+ /* -------------------------- */
459
  table th:nth-child(1):nth-last-child(5), table td:nth-child(1):nth-last-child(5) { width: 15% !important; }
460
  table th:nth-child(2):nth-last-child(4), table td:nth-child(2):nth-last-child(4) { width: 10% !important; }
461
  table th:nth-child(3):nth-last-child(3), table td:nth-child(3):nth-last-child(3) { width: 30% !important; }
462
  table th:nth-child(4):nth-last-child(2), table td:nth-child(4):nth-last-child(2) { width: 10% !important; }
463
  table th:nth-child(5):nth-last-child(1), table td:nth-child(5):nth-last-child(1) { width: 35% !important; }
464
+
465
+ /* -------------------------- */
466
+ /* 6์ปฌ๋Ÿผ */
467
+ /* -------------------------- */
468
  table th:nth-child(1):nth-last-child(6), table td:nth-child(1):nth-last-child(6) { width: 8% !important; }
469
  table th:nth-child(2):nth-last-child(5), table td:nth-child(2):nth-last-child(5) { width: 25% !important; }
470
  table th:nth-child(3):nth-last-child(4), table td:nth-child(3):nth-last-child(4) { width: 8% !important; }
471
  table th:nth-child(4):nth-last-child(3), table td:nth-child(4):nth-last-child(3) { width: 25% !important; }
472
  table th:nth-child(5):nth-last-child(2), table td:nth-child(5):nth-last-child(2) { width: 8% !important; }
473
  table th:nth-child(6):nth-last-child(1), table td:nth-child(6):nth-last-child(1) { width: 26% !important; }
474
+
475
+ /* -------------------------- */
476
+ /* ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” & ์Šคํฌ๋กค ์„ค์ • */
477
+ /* -------------------------- */
478
+ thead th {
479
+ position: sticky;
480
+ top: 0;
481
+ background: white;
482
+ z-index: 10;
483
+ }
484
+ .dataframe {
485
+ max-height: none !important;
486
+ overflow-y: visible !important;
487
+ }
488
+ .dataframe > div {
489
+ max-height: none !important;
490
+ overflow: visible !important;
491
+ }
492
+ td {
493
+ white-space: pre-wrap !important;
494
+ word-break: break-word !important;
495
+ line-height: 1.6;
496
+ padding: 10px;
497
+ }
498
  """
499
 
500
  if __name__ == "__main__":