QIDNLF commited on
Commit
42e165e
ยท
verified ยท
1 Parent(s): 1b20bdc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -109
app.py CHANGED
@@ -42,7 +42,6 @@ def decode_sqlite_text(x):
42
  def fetch_available_standards():
43
  global db_registry
44
  db_registry = []
45
-
46
  for file_name in os.listdir(UPLOAD_DIR):
47
  if file_name.endswith(".db"):
48
  name = file_name.replace(".db", "")
@@ -53,19 +52,14 @@ def fetch_available_standards():
53
  "standard": parts[0],
54
  "version": parts[1]
55
  })
56
-
57
  return sorted(list(set([d["standard"] for d in db_registry])))
58
 
59
- # ==========================================
60
- # ๐Ÿ’ก [์Šค๋งˆํŠธ ์„ค์ • ํƒ์ƒ‰ ๊ธฐ๋Šฅ์ด ํƒ‘์žฌ๋œ ๋ฐ์ดํ„ฐ ๋กœ๋”]
61
- # ==========================================
62
  def fetch_database_records(std, ver, cat, table_type):
63
  try:
64
  db_path = os.path.join(UPLOAD_DIR, f"{std}_{ver}.db")
65
  conn = sqlite3.connect(db_path)
66
  conn.text_factory = decode_sqlite_text
67
 
68
- # 1. ์‹ค์ œ DB ํ…Œ์ด๋ธ”๊ณผ ์—ด(Column) ์ด๋ฆ„์„ ๋จผ์ € ํ›‘์–ด๋ด…๋‹ˆ๋‹ค.
69
  tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist()
70
  valid_tables = [t for t in tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']]
71
  main_table = f"{std}_{ver}"
@@ -79,7 +73,6 @@ def fetch_database_records(std, ver, cat, table_type):
79
  cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
80
  lower_cols = [c.lower() for c in cols]
81
 
82
- # 2. Table_Config๋ฅผ ์ฝ์–ด์˜ต๋‹ˆ๋‹ค.
83
  conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
84
  config_df = pd.read_sql("SELECT * FROM Table_Config WHERE TRIM(Table_Type)=?", conn_map, params=[table_type.strip()])
85
  conn_map.close()
@@ -87,7 +80,6 @@ def fetch_database_records(std, ver, cat, table_type):
87
  if config_df.empty:
88
  return pd.DataFrame({"Error": [f"Table_Config์—์„œ '{table_type}' ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."]}), []
89
 
90
- # ๐Ÿ’ก [ํ•ต์‹ฌ ํ•ด๊ฒฐ] Table_Config์— 'Main'์ด ์—ฌ๋Ÿฌ ๊ฐœ์ผ ๊ฒฝ์šฐ, ์‹ค์ œ DB ์—ด ์ด๋ฆ„๊ณผ ์ผ์น˜ํ•˜๋Š” ๊ฒƒ์„ ๋˜‘๋˜‘ํ•˜๊ฒŒ ์ฐพ์•„๋ƒ…๋‹ˆ๋‹ค!
91
  matched_config = None
92
  for _, row in config_df.iterrows():
93
  anchors_test = [x.strip().lower() for x in str(row['Anchor_Column']).split(',')]
@@ -96,15 +88,14 @@ def fetch_database_records(std, ver, cat, table_type):
96
  break
97
 
98
  if matched_config is None:
99
- matched_config = config_df.iloc[0] # ๋ชป ์ฐพ์œผ๋ฉด ์–ด์ฉ” ์ˆ˜ ์—†์ด ์ฒซ ๋ฒˆ์งธ ๊ฐ•์ œ ์‚ฌ์šฉ
100
 
101
  anchors = [x.strip() for x in matched_config['Anchor_Column'].split(',')]
102
  displays = [x.strip() for x in matched_config['Display_Columns'].split(',')] if pd.notna(matched_config['Display_Columns']) else anchors
103
 
104
- # 3. ์ฟผ๋ฆฌ ์‹คํ–‰ ๋ฐ ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ๋ง
105
  query = f"SELECT * FROM [{main_table}]"
106
-
107
  conditions = []
 
108
  if cat and cat != "ALL":
109
  if "." in cat and 'chapter' in lower_cols and 'category' in lower_cols:
110
  ch, ca = cat.split(".", 1)
@@ -121,7 +112,6 @@ def fetch_database_records(std, ver, cat, table_type):
121
  df = pd.read_sql(query, conn)
122
  conn.close()
123
 
124
- # 4. ํ™”๋ฉด์— ๋ณด์—ฌ์ค„ ์—ด ์ •๋ฆฌ
125
  final_cols = []
126
  for d in displays:
127
  for c in df.columns:
@@ -179,7 +169,7 @@ def generate_html_diff(text1, text2):
179
  return text1, text2
180
 
181
  # ==========================================
182
- # 2. UI Component Handlers
183
  # ==========================================
184
  def load_initial_standards():
185
  return gr.Dropdown(choices=fetch_available_standards())
@@ -187,8 +177,11 @@ def load_initial_standards():
187
  def update_version_dropdown(standard):
188
  if not standard:
189
  return gr.Dropdown(choices=[])
190
- versions = sorted(set(d["version"] for d in db_registry if d["standard"] == standard))
191
- return gr.Dropdown(choices=versions)
 
 
 
192
 
193
  def update_base_category_dropdown(standard, version):
194
  if not standard or not version:
@@ -196,8 +189,8 @@ def update_base_category_dropdown(standard, version):
196
 
197
  choices = ["ALL"]
198
  status_value = ""
199
-
200
  db_path = os.path.join(UPLOAD_DIR, f"{standard}_{version}.db")
 
201
  if not os.path.exists(db_path):
202
  return gr.update(choices=choices), gr.update(value="")
203
 
@@ -258,15 +251,29 @@ def update_comp_standard_dropdown(base_std, base_ver):
258
  conn.close()
259
 
260
  mapped_stds = sorted(df['Comp_std'].dropna().unique().tolist()) if not df.empty else []
 
 
 
 
 
 
261
  return gr.update(choices=mapped_stds, value=None, interactive=bool(mapped_stds))
262
  except Exception:
263
- return gr.update(choices=[], value=None, interactive=False)
264
 
265
  def update_comp_version_dropdown(base_std, base_ver, comp_std):
266
  if not all([base_std, base_ver, comp_std]):
267
  return gr.update(choices=[], value=None, interactive=False)
268
 
269
  try:
 
 
 
 
 
 
 
 
270
  conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
271
  query = "SELECT DISTINCT TRIM(Comp_ver) AS Comp_ver FROM Mapping_registry WHERE TRIM(Base_std)=TRIM(?) AND TRIM(Base_ver)=TRIM(?) AND TRIM(Comp_std)=TRIM(?)"
272
  df = pd.read_sql(query, conn, params=[base_std, base_ver, comp_std])
@@ -283,9 +290,10 @@ def update_comp_category_dropdown(base_std, base_ver, base_cat, comp_std, comp_v
283
 
284
  base_type = "Main" if not base_cat or base_cat == "ALL" or "." in base_cat else base_cat
285
  status_value = ""
 
 
286
 
287
  try:
288
- comp_db_path = os.path.join(UPLOAD_DIR, f"{comp_std}_{comp_ver}.db")
289
  if os.path.exists(comp_db_path):
290
  c_conn = sqlite3.connect(comp_db_path)
291
  c_tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", c_conn)['name'].tolist()
@@ -303,16 +311,42 @@ def update_comp_category_dropdown(base_std, base_ver, base_cat, comp_std, comp_v
303
  status_value = str(status_df.iloc[0]['Status'])
304
  except Exception:
305
  pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
 
307
  conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
308
  query = """
309
  SELECT DISTINCT TRIM(Comp_Type) AS Comp_Type
310
  FROM Mapping_registry
311
- WHERE TRIM(Base_std)=TRIM(?)
312
- AND TRIM(Base_ver)=TRIM(?)
313
- AND TRIM(Base_Type)=TRIM(?)
314
- AND TRIM(Comp_std)=TRIM(?)
315
- AND TRIM(Comp_ver)=TRIM(?)
316
  """
317
  df = pd.read_sql(query, conn, params=[base_std, base_ver, base_type, comp_std, comp_ver])
318
  conn.close()
@@ -324,21 +358,19 @@ def update_comp_category_dropdown(base_std, base_ver, base_cat, comp_std, comp_v
324
  final_choices = []
325
  if "Main" in allowed_types:
326
  final_choices.append("ALL")
327
- if os.path.exists(comp_db_path):
328
  c_conn = sqlite3.connect(comp_db_path)
329
- if c_main_table:
330
- cols = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist()
331
- lower_cols = [c.lower() for c in cols]
332
- if 'chapter' in lower_cols and 'category' in lower_cols:
333
- ch_col = cols[lower_cols.index('chapter')]
334
- ca_col = cols[lower_cols.index('category')]
335
-
336
- c_df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{c_main_table}]", c_conn)
337
- for _, row in c_df.iterrows():
338
- ch = str(row[ch_col]).strip()
339
- ca = str(row[ca_col]).strip()
340
- if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']:
341
- final_choices.append(f"{ch}.{ca}")
342
  c_conn.close()
343
 
344
  for t in allowed_types:
@@ -356,7 +388,7 @@ def reset_comp_selections():
356
  return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="")
357
 
358
  # ==========================================
359
- # 3. Core Search Logic
360
  # ==========================================
361
  def execute_unified_search(base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat, mapped_only, diff_only):
362
  try:
@@ -431,79 +463,79 @@ def execute_unified_search(base_std, base_ver, base_cat, comp_std, comp_ver, com
431
  df_base['merge_key'] = df_base[real_anchors_b].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
432
  df_comp['merge_key'] = df_comp[real_anchors_c].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
433
 
434
- conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
435
- registry_query = """
436
- SELECT Target_Table FROM Mapping_registry
437
- WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?
438
- LIMIT 1
439
- """
440
- reg_df = pd.read_sql(registry_query, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
441
-
442
- target_table_name = "Mapping_table"
443
- if not reg_df.empty and pd.notna(reg_df.iloc[0]['Target_Table']):
444
- val = str(reg_df.iloc[0]['Target_Table']).strip()
445
- if val and val.lower() not in ["none", "nan"]:
446
- target_table_name = val
447
 
448
- try:
449
- q_fw = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?"
450
- df_fw = pd.read_sql(q_fw, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
 
 
 
 
 
 
 
 
451
 
452
- q_rv = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Comp_std)=? AND TRIM(Comp_ver)=? AND TRIM(Base_std)=? AND TRIM(Base_ver)=?"
453
- df_rv = pd.read_sql(q_rv, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
454
- except Exception as sql_e:
 
 
 
 
 
 
 
 
 
 
 
 
455
  conn_map.close()
456
- return pd.DataFrame({"Error": [f"๋งคํ•‘ '{target_table_name}'์„ ์—ฌ๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ ํ™•์ธํ•˜์„ธ์š”: {str(sql_e)}"]})
457
- conn_map.close()
458
 
459
- cols_fw_lower = {c.lower(): c for c in df_fw.columns}
460
-
461
- if 'base_type' in cols_fw_lower and 'comp_type' in cols_fw_lower:
462
- b_col = cols_fw_lower['base_type']
463
- c_col = cols_fw_lower['comp_type']
464
-
465
- df_fw[b_col] = df_fw[b_col].fillna('Main').astype(str).str.strip().str.upper()
466
- df_fw[c_col] = df_fw[c_col].fillna('Main').astype(str).str.strip().str.upper()
467
- df_fw = df_fw[(df_fw[b_col] == type_b.strip().upper()) & (df_fw[c_col] == type_c.strip().upper())]
468
 
469
- if not df_rv.empty:
470
- df_rv[b_col] = df_rv[b_col].fillna('Main').astype(str).str.strip().str.upper()
471
- df_rv[c_col] = df_rv[c_col].fillna('Main').astype(str).str.strip().str.upper()
472
- df_rv = df_rv[(df_rv[c_col] == type_b.strip().upper()) & (df_rv[b_col] == type_c.strip().upper())]
473
-
474
- b_sec = cols_fw_lower.get('base_section', 'Base_section')
475
- c_sec = cols_fw_lower.get('comp_section', 'Comp_section')
476
-
477
- if b_sec not in df_fw.columns or c_sec not in df_fw.columns:
478
- return pd.DataFrame({"Error": [f"'{target_table_name}' ์— '{b_sec}' ๋˜๋Š” '{c_sec}' ์—ด์ด ์—†์Šต๋‹ˆ๋‹ค. ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ํ™•์ธํ•˜์„ธ์š”."]})
479
-
480
- df_fw = df_fw[[b_sec, c_sec]].rename(columns={b_sec: 'Base_section', c_sec: 'Comp_section'})
481
- if not df_rv.empty:
482
- df_rv = df_rv[[b_sec, c_sec]].rename(columns={b_sec: 'Comp_section', c_sec: 'Base_section'})
483
- else:
484
- df_rv = pd.DataFrame(columns=['Base_section', 'Comp_section'])
485
-
486
- df_mapping = pd.concat([df_fw, df_rv], ignore_index=True)
487
 
488
- if not df_mapping.empty:
489
- df_mapping['Base_section'] = df_mapping['Base_section'].astype(str).str.replace('\n', ',').str.split(',')
490
- df_mapping['Comp_section'] = df_mapping['Comp_section'].astype(str).str.replace('\n', ',').str.split(',')
491
- df_mapping = df_mapping.explode('Base_section').explode('Comp_section')
492
 
493
- df_mapping['Base_section'] = df_mapping['Base_section'].apply(clean_key_val)
494
- df_mapping['Comp_section'] = df_mapping['Comp_section'].apply(clean_key_val)
495
 
496
- df_mapping = df_mapping[(df_mapping['Base_section'] != "") & (df_mapping['Comp_section'] != "")]
497
- df_mapping = df_mapping.dropna().drop_duplicates()
498
- else:
499
- df_mapping = pd.DataFrame(columns=['Base_section', 'Comp_section'])
 
500
 
501
- if base_std.strip().upper() == comp_std.strip().upper() and type_b.strip().upper() == type_c.strip().upper():
502
- implicit = pd.DataFrame({'Base_section': list(set(df_base['merge_key']) & set(df_comp['merge_key'])),
503
- 'Comp_section': list(set(df_base['merge_key']) & set(df_comp['merge_key']))})
504
- bridge = pd.concat([df_mapping, implicit], ignore_index=True).drop_duplicates()
505
- else:
506
- bridge = df_mapping.copy()
 
 
 
 
 
 
 
 
507
 
508
  rename_b = {c: f"{c}_{base_ver}" for c in df_base.columns if c != 'merge_key'}
509
  df_base = df_base.rename(columns=rename_b)
@@ -637,7 +669,6 @@ with gr.Blocks() as demo:
637
  base_reset_btn.click(fn=reset_base_selections, inputs=None, outputs=[base_standard, base_version, base_category, base_status])
638
  comp_reset_btn.click(fn=reset_comp_selections, inputs=None, outputs=[comp_standard, comp_version, comp_category, comp_status])
639
 
640
-
641
  # ==========================================
642
  # 5. Application Styling (CSS)
643
  # ==========================================
@@ -649,17 +680,14 @@ css = """
649
  .reset-btn {
650
  margin-bottom: 10px !important;
651
  }
652
-
653
  table {
654
  table-layout: auto !important;
655
  width: max-content !important;
656
  min-width: 100% !important;
657
  }
658
-
659
  th, td {
660
  min-width: 150px;
661
  }
662
-
663
  table:has(th:nth-last-child(2):first-child),
664
  table:has(th:nth-last-child(3):first-child),
665
  table:has(th:nth-last-child(4):first-child),
@@ -668,7 +696,6 @@ table:has(th:nth-last-child(6):first-child) {
668
  table-layout: fixed !important;
669
  width: 100% !important;
670
  }
671
-
672
  table th:nth-last-child(2):first-child, table td:nth-last-child(2):first-child { width: 15% !important; min-width: 0 !important; }
673
  table th:nth-last-child(1), table td:nth-last-child(1) { width: 85% !important; min-width: 0 !important; }
674
 
@@ -724,12 +751,10 @@ td img {
724
  display: block;
725
  max-width: none !important;
726
  }
727
-
728
  #search_row {
729
  align-items: center !important;
730
  margin-bottom: 5px !important;
731
  }
732
-
733
  #diff_cb_item, #mapped_cb_item {
734
  margin-top: 0 !important;
735
  padding-left: 15px !important;
 
42
  def fetch_available_standards():
43
  global db_registry
44
  db_registry = []
 
45
  for file_name in os.listdir(UPLOAD_DIR):
46
  if file_name.endswith(".db"):
47
  name = file_name.replace(".db", "")
 
52
  "standard": parts[0],
53
  "version": parts[1]
54
  })
 
55
  return sorted(list(set([d["standard"] for d in db_registry])))
56
 
 
 
 
57
  def fetch_database_records(std, ver, cat, table_type):
58
  try:
59
  db_path = os.path.join(UPLOAD_DIR, f"{std}_{ver}.db")
60
  conn = sqlite3.connect(db_path)
61
  conn.text_factory = decode_sqlite_text
62
 
 
63
  tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist()
64
  valid_tables = [t for t in tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']]
65
  main_table = f"{std}_{ver}"
 
73
  cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
74
  lower_cols = [c.lower() for c in cols]
75
 
 
76
  conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
77
  config_df = pd.read_sql("SELECT * FROM Table_Config WHERE TRIM(Table_Type)=?", conn_map, params=[table_type.strip()])
78
  conn_map.close()
 
80
  if config_df.empty:
81
  return pd.DataFrame({"Error": [f"Table_Config์—์„œ '{table_type}' ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."]}), []
82
 
 
83
  matched_config = None
84
  for _, row in config_df.iterrows():
85
  anchors_test = [x.strip().lower() for x in str(row['Anchor_Column']).split(',')]
 
88
  break
89
 
90
  if matched_config is None:
91
+ matched_config = config_df.iloc[0]
92
 
93
  anchors = [x.strip() for x in matched_config['Anchor_Column'].split(',')]
94
  displays = [x.strip() for x in matched_config['Display_Columns'].split(',')] if pd.notna(matched_config['Display_Columns']) else anchors
95
 
 
96
  query = f"SELECT * FROM [{main_table}]"
 
97
  conditions = []
98
+
99
  if cat and cat != "ALL":
100
  if "." in cat and 'chapter' in lower_cols and 'category' in lower_cols:
101
  ch, ca = cat.split(".", 1)
 
112
  df = pd.read_sql(query, conn)
113
  conn.close()
114
 
 
115
  final_cols = []
116
  for d in displays:
117
  for c in df.columns:
 
169
  return text1, text2
170
 
171
  # ==========================================
172
+ # 2. UI Component Handlers (์Šค๋งˆํŠธ ๋“œ๋กญ๋‹ค์šด ์ ์šฉ)
173
  # ==========================================
174
  def load_initial_standards():
175
  return gr.Dropdown(choices=fetch_available_standards())
 
177
  def update_version_dropdown(standard):
178
  if not standard:
179
  return gr.Dropdown(choices=[])
180
+ versions = []
181
+ for file_name in os.listdir(UPLOAD_DIR):
182
+ if file_name.startswith(standard + "_") and file_name.endswith(".db"):
183
+ versions.append(file_name.replace(standard + "_", "").replace(".db", ""))
184
+ return gr.Dropdown(choices=sorted(list(set(versions))))
185
 
186
  def update_base_category_dropdown(standard, version):
187
  if not standard or not version:
 
189
 
190
  choices = ["ALL"]
191
  status_value = ""
 
192
  db_path = os.path.join(UPLOAD_DIR, f"{standard}_{version}.db")
193
+
194
  if not os.path.exists(db_path):
195
  return gr.update(choices=choices), gr.update(value="")
196
 
 
251
  conn.close()
252
 
253
  mapped_stds = sorted(df['Comp_std'].dropna().unique().tolist()) if not df.empty else []
254
+
255
+ # ๐Ÿ’ก [ํ•ต์‹ฌ] ์ž๊ธฐ ์ž์‹ (๊ฐ™์€ ๋ฒ•๊ทœ)์€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ์—†์–ด๋„ ํ•ญ์ƒ ๊ณ ๋ฅผ ์ˆ˜ ์žˆ๊ฒŒ ๋ฌด์กฐ๊ฑด ์ถ”๊ฐ€!
256
+ if base_std not in mapped_stds:
257
+ mapped_stds.append(base_std)
258
+ mapped_stds.sort()
259
+
260
  return gr.update(choices=mapped_stds, value=None, interactive=bool(mapped_stds))
261
  except Exception:
262
+ return gr.update(choices=[base_std], value=None, interactive=True)
263
 
264
  def update_comp_version_dropdown(base_std, base_ver, comp_std):
265
  if not all([base_std, base_ver, comp_std]):
266
  return gr.update(choices=[], value=None, interactive=False)
267
 
268
  try:
269
+ # ๐Ÿ’ก [ํ•ต์‹ฌ] ๊ฐ™์€ ๋ฒ•๊ทœ๋ฉด ๋กœ์ปฌ ํŒŒ์ผ ๋ชฉ๋ก์—์„œ ์ „์ฒด ๋ฒ„์ „์„ ์ง์ ‘ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
270
+ if base_std.strip() == comp_std.strip():
271
+ versions = []
272
+ for file_name in os.listdir(UPLOAD_DIR):
273
+ if file_name.startswith(comp_std + "_") and file_name.endswith(".db"):
274
+ versions.append(file_name.replace(comp_std + "_", "").replace(".db", ""))
275
+ return gr.update(choices=sorted(list(set(versions))), value=None, interactive=True)
276
+
277
  conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
278
  query = "SELECT DISTINCT TRIM(Comp_ver) AS Comp_ver FROM Mapping_registry WHERE TRIM(Base_std)=TRIM(?) AND TRIM(Base_ver)=TRIM(?) AND TRIM(Comp_std)=TRIM(?)"
279
  df = pd.read_sql(query, conn, params=[base_std, base_ver, comp_std])
 
290
 
291
  base_type = "Main" if not base_cat or base_cat == "ALL" or "." in base_cat else base_cat
292
  status_value = ""
293
+ comp_db_path = os.path.join(UPLOAD_DIR, f"{comp_std}_{comp_ver}.db")
294
+ c_main_table = None
295
 
296
  try:
 
297
  if os.path.exists(comp_db_path):
298
  c_conn = sqlite3.connect(comp_db_path)
299
  c_tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", c_conn)['name'].tolist()
 
311
  status_value = str(status_df.iloc[0]['Status'])
312
  except Exception:
313
  pass
314
+ c_conn.close()
315
+
316
+ # ๐Ÿ’ก [ํ•ต์‹ฌ] ๊ฐ™์€ ๋ฒ•๊ทœ์ธ ๊ฒฝ์šฐ ์นดํ…Œ๊ณ ๋ฆฌ๋„ ์Šค์Šค๋กœ ์Šค์บ”ํ•ด์„œ ๋„์›Œ์ค๋‹ˆ๋‹ค.
317
+ if base_std.strip() == comp_std.strip():
318
+ final_choices = ["ALL"]
319
+ if os.path.exists(comp_db_path) and c_main_table:
320
+ c_conn = sqlite3.connect(comp_db_path)
321
+ cols = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist()
322
+ lower_cols = [c.lower() for c in cols]
323
+ if 'chapter' in lower_cols and 'category' in lower_cols:
324
+ ch_col = cols[lower_cols.index('chapter')]
325
+ ca_col = cols[lower_cols.index('category')]
326
+ c_df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{c_main_table}]", c_conn)
327
+ for _, row in c_df.iterrows():
328
+ ch = str(row[ch_col]).strip()
329
+ ca = str(row[ca_col]).strip()
330
+ if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']:
331
+ final_choices.append(f"{ch}.{ca}")
332
+
333
+ pattern = re.compile(f"^{comp_std}[_\\s-]*{comp_ver}[_\\s-]*", re.IGNORECASE)
334
+ for t in c_valid_tables:
335
+ if t == c_main_table: continue
336
+ short_name = pattern.sub("", t).strip(" _")
337
+ if short_name and short_name not in final_choices:
338
+ final_choices.append(short_name)
339
+ elif t not in final_choices:
340
+ final_choices.append(t)
341
+ c_conn.close()
342
+ return gr.update(choices=final_choices, value=final_choices[0] if final_choices else None, interactive=True), gr.update(value=status_value)
343
 
344
+ # ๊ธฐ์กด ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋กœ์ง
345
  conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
346
  query = """
347
  SELECT DISTINCT TRIM(Comp_Type) AS Comp_Type
348
  FROM Mapping_registry
349
+ WHERE TRIM(Base_std)=TRIM(?) AND TRIM(Base_ver)=TRIM(?) AND TRIM(Base_Type)=TRIM(?) AND TRIM(Comp_std)=TRIM(?) AND TRIM(Comp_ver)=TRIM(?)
 
 
 
 
350
  """
351
  df = pd.read_sql(query, conn, params=[base_std, base_ver, base_type, comp_std, comp_ver])
352
  conn.close()
 
358
  final_choices = []
359
  if "Main" in allowed_types:
360
  final_choices.append("ALL")
361
+ if os.path.exists(comp_db_path) and c_main_table:
362
  c_conn = sqlite3.connect(comp_db_path)
363
+ cols = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist()
364
+ lower_cols = [c.lower() for c in cols]
365
+ if 'chapter' in lower_cols and 'category' in lower_cols:
366
+ ch_col = cols[lower_cols.index('chapter')]
367
+ ca_col = cols[lower_cols.index('category')]
368
+ c_df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{c_main_table}]", c_conn)
369
+ for _, row in c_df.iterrows():
370
+ ch = str(row[ch_col]).strip()
371
+ ca = str(row[ca_col]).strip()
372
+ if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']:
373
+ final_choices.append(f"{ch}.{ca}")
 
 
374
  c_conn.close()
375
 
376
  for t in allowed_types:
 
388
  return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="")
389
 
390
  # ==========================================
391
+ # 3. Core Search Logic (ํ•ฉ์ง‘ํ•ฉ ๋น„๊ต ์ ์šฉ ์™„๋ฃŒ)
392
  # ==========================================
393
  def execute_unified_search(base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat, mapped_only, diff_only):
394
  try:
 
463
  df_base['merge_key'] = df_base[real_anchors_b].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
464
  df_comp['merge_key'] = df_comp[real_anchors_c].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
465
 
466
+ # ๐Ÿ’ก [ํ•ต์‹ฌ] ๊ฐ™์€ ๋ฒ•๊ทœ์ธ ๊ฒฝ์šฐ ์žฅ๋ถ€๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ์–‘์ชฝ ์กฐํ•ญ์˜ 'ํ•ฉ์ง‘ํ•ฉ'์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
467
+ is_same_std = (base_std.strip().upper() == comp_std.strip().upper() and type_b.strip().upper() == type_c.strip().upper())
 
 
 
 
 
 
 
 
 
 
 
468
 
469
+ if is_same_std:
470
+ all_keys = list(set(df_base['merge_key']).union(set(df_comp['merge_key'])))
471
+ bridge = pd.DataFrame({'Base_section': all_keys, 'Comp_section': all_keys})
472
+ else:
473
+ conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
474
+ registry_query = """
475
+ SELECT Target_Table FROM Mapping_registry
476
+ WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?
477
+ LIMIT 1
478
+ """
479
+ reg_df = pd.read_sql(registry_query, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
480
 
481
+ target_table_name = "Mapping_table"
482
+ if not reg_df.empty and pd.notna(reg_df.iloc[0]['Target_Table']):
483
+ val = str(reg_df.iloc[0]['Target_Table']).strip()
484
+ if val and val.lower() not in ["none", "nan"]:
485
+ target_table_name = val
486
+
487
+ try:
488
+ q_fw = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?"
489
+ df_fw = pd.read_sql(q_fw, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
490
+
491
+ q_rv = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Comp_std)=? AND TRIM(Comp_ver)=? AND TRIM(Base_std)=? AND TRIM(Base_ver)=?"
492
+ df_rv = pd.read_sql(q_rv, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
493
+ except Exception as sql_e:
494
+ conn_map.close()
495
+ return pd.DataFrame({"Error": [f"๋งคํ•‘ ์žฅ๋ถ€ '{target_table_name}'์„ ์—ฌ๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ ํ™•์ธํ•˜์„ธ์š”: {str(sql_e)}"]})
496
  conn_map.close()
 
 
497
 
498
+ cols_fw_lower = {c.lower(): c for c in df_fw.columns}
 
 
 
 
 
 
 
 
499
 
500
+ if 'base_type' in cols_fw_lower and 'comp_type' in cols_fw_lower:
501
+ b_col = cols_fw_lower['base_type']
502
+ c_col = cols_fw_lower['comp_type']
503
+
504
+ df_fw[b_col] = df_fw[b_col].fillna('Main').astype(str).str.strip().str.upper()
505
+ df_fw[c_col] = df_fw[c_col].fillna('Main').astype(str).str.strip().str.upper()
506
+ df_fw = df_fw[(df_fw[b_col] == type_b.strip().upper()) & (df_fw[c_col] == type_c.strip().upper())]
507
+
508
+ if not df_rv.empty:
509
+ df_rv[b_col] = df_rv[b_col].fillna('Main').astype(str).str.strip().str.upper()
510
+ df_rv[c_col] = df_rv[c_col].fillna('Main').astype(str).str.strip().str.upper()
511
+ df_rv = df_rv[(df_rv[c_col] == type_b.strip().upper()) & (df_rv[b_col] == type_c.strip().upper())]
 
 
 
 
 
 
512
 
513
+ b_sec = cols_fw_lower.get('base_section', 'Base_section')
514
+ c_sec = cols_fw_lower.get('comp_section', 'Comp_section')
 
 
515
 
516
+ if b_sec not in df_fw.columns or c_sec not in df_fw.columns:
517
+ return pd.DataFrame({"Error": [f"'{target_table_name}' ์žฅ๋ถ€์— '{b_sec}' ๋˜๋Š” '{c_sec}' ์—ด์ด ์—†์Šต๋‹ˆ๋‹ค. ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ํ™•์ธํ•˜์„ธ์š”."]})
518
 
519
+ df_fw = df_fw[[b_sec, c_sec]].rename(columns={b_sec: 'Base_section', c_sec: 'Comp_section'})
520
+ if not df_rv.empty:
521
+ df_rv = df_rv[[b_sec, c_sec]].rename(columns={b_sec: 'Comp_section', c_sec: 'Base_section'})
522
+ else:
523
+ df_rv = pd.DataFrame(columns=['Base_section', 'Comp_section'])
524
 
525
+ df_mapping = pd.concat([df_fw, df_rv], ignore_index=True)
526
+
527
+ if not df_mapping.empty:
528
+ df_mapping['Base_section'] = df_mapping['Base_section'].astype(str).str.replace('\n', ',').str.split(',')
529
+ df_mapping['Comp_section'] = df_mapping['Comp_section'].astype(str).str.replace('\n', ',').str.split(',')
530
+ df_mapping = df_mapping.explode('Base_section').explode('Comp_section')
531
+
532
+ df_mapping['Base_section'] = df_mapping['Base_section'].apply(clean_key_val)
533
+ df_mapping['Comp_section'] = df_mapping['Comp_section'].apply(clean_key_val)
534
+
535
+ df_mapping = df_mapping[(df_mapping['Base_section'] != "") & (df_mapping['Comp_section'] != "")]
536
+ bridge = df_mapping.dropna().drop_duplicates()
537
+ else:
538
+ bridge = pd.DataFrame(columns=['Base_section', 'Comp_section'])
539
 
540
  rename_b = {c: f"{c}_{base_ver}" for c in df_base.columns if c != 'merge_key'}
541
  df_base = df_base.rename(columns=rename_b)
 
669
  base_reset_btn.click(fn=reset_base_selections, inputs=None, outputs=[base_standard, base_version, base_category, base_status])
670
  comp_reset_btn.click(fn=reset_comp_selections, inputs=None, outputs=[comp_standard, comp_version, comp_category, comp_status])
671
 
 
672
  # ==========================================
673
  # 5. Application Styling (CSS)
674
  # ==========================================
 
680
  .reset-btn {
681
  margin-bottom: 10px !important;
682
  }
 
683
  table {
684
  table-layout: auto !important;
685
  width: max-content !important;
686
  min-width: 100% !important;
687
  }
 
688
  th, td {
689
  min-width: 150px;
690
  }
 
691
  table:has(th:nth-last-child(2):first-child),
692
  table:has(th:nth-last-child(3):first-child),
693
  table:has(th:nth-last-child(4):first-child),
 
696
  table-layout: fixed !important;
697
  width: 100% !important;
698
  }
 
699
  table th:nth-last-child(2):first-child, table td:nth-last-child(2):first-child { width: 15% !important; min-width: 0 !important; }
700
  table th:nth-last-child(1), table td:nth-last-child(1) { width: 85% !important; min-width: 0 !important; }
701
 
 
751
  display: block;
752
  max-width: none !important;
753
  }
 
754
  #search_row {
755
  align-items: center !important;
756
  margin-bottom: 5px !important;
757
  }
 
758
  #diff_cb_item, #mapped_cb_item {
759
  margin-top: 0 !important;
760
  padding-left: 15px !important;