import gradio as gr import sqlite3 import pandas as pd import os import re import base64 import difflib # ========================================== # 1. Environment Setup & Data Helpers # ========================================== UPLOAD_DIR = "uploaded_dbs" if not os.path.exists(UPLOAD_DIR): os.makedirs(UPLOAD_DIR) db_registry = [] def convert_blob_to_html_img(blob_data): if blob_data is None or pd.isna(blob_data): return "" try: if isinstance(blob_data, (bytes, bytearray)): encoded = base64.b64encode(blob_data).decode('utf-8') return f''' ''' return str(blob_data) except Exception: return str(blob_data) def decode_sqlite_text(x): try: return x.decode('utf-8') except UnicodeDecodeError: return x def fetch_available_standards(): global db_registry db_registry = [] for file_name in os.listdir(UPLOAD_DIR): if file_name.endswith(".db"): name = file_name.replace(".db", "") parts = name.split("_") if len(parts) >= 2: db_registry.append({ "path": os.path.join(UPLOAD_DIR, file_name), "standard": parts[0], "version": parts[1] }) return sorted(list(set([d["standard"] for d in db_registry]))) def fetch_database_records(std, ver, cat, table_type): try: db_path = os.path.join(UPLOAD_DIR, f"{std}_{ver}.db") conn = sqlite3.connect(db_path) conn.text_factory = decode_sqlite_text tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist() valid_tables = [t for t in tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']] main_table = None if table_type and table_type.upper() != "MAIN": expected_name = f"{std}_{ver}_{table_type}" for t in valid_tables: if t.lower() == expected_name.lower() or t.lower() == table_type.lower(): main_table = t break if not main_table: main_table = f"{std}_{ver}" if main_table not in valid_tables: main_table = valid_tables[0] if valid_tables else None if not main_table: conn.close() return pd.DataFrame({"Error": ["데이터 테이블을 찾을 수 없습니다."]}), [] cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist() lower_cols = [c.lower() for c in cols] conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db")) config_df = pd.read_sql("SELECT * FROM Table_Config WHERE TRIM(Table_Type)=?", conn_map, params=[table_type.strip()]) conn_map.close() if config_df.empty: return pd.DataFrame({"Error": [f"Table_Config에서 '{table_type}' 설정을 찾을 수 없습니다."]}), [] matched_config = None for _, row in config_df.iterrows(): anchors_test = [x.strip().lower() for x in str(row['Anchor_Column']).split(',')] if any(a in lower_cols for a in anchors_test): matched_config = row break if matched_config is None: matched_config = config_df.iloc[0] anchors = [x.strip() for x in matched_config['Anchor_Column'].split(',')] displays = [x.strip() for x in matched_config['Display_Columns'].split(',')] if pd.notna(matched_config['Display_Columns']) else anchors query = f"SELECT * FROM [{main_table}]" conditions = [] if cat and cat != "ALL": if "." in cat and 'chapter' in lower_cols and 'category' in lower_cols: ch, ca = cat.split(".", 1) ch_col = cols[lower_cols.index('chapter')] ca_col = cols[lower_cols.index('category')] conditions.append(f"[{ch_col}] = '{ch}' AND [{ca_col}] = '{ca}'") elif 'category' in lower_cols: ca_col = cols[lower_cols.index('category')] conditions.append(f"[{ca_col}] = '{cat}'") if conditions: query += " WHERE " + " AND ".join(conditions) df = pd.read_sql(query, conn) conn.close() real_anchors = [c for c in df.columns if any(a.lower() == c.lower() for a in anchors)] final_cols = [] for d in displays: for c in df.columns: if d.lower() == c.lower(): if c not in final_cols: final_cols.append(c) break for ra in real_anchors: if ra not in final_cols: final_cols.append(ra) if not final_cols: return df, real_anchors for c in final_cols: df[c] = df[c].apply(convert_blob_to_html_img) return df[final_cols], real_anchors except Exception as e: import traceback traceback.print_exc() return pd.DataFrame({"Error": [f"데이터 로드 오류: {str(e)}"]}), [] def generate_html_diff(text1, text2): try: s1, s2 = str(text1), str(text2) if len(s1) > 1000 or len(s2) > 1000: return s1, s2 words1, words2 = s1.split(), s2.split() if not words1 or not words2: return s1, s2 common_words = set(words1) & set(words2) if len(common_words) / min(len(words1), len(words2)) < 0.05: return s1, s2 matcher = difflib.SequenceMatcher(None, words1, words2) res1, res2 = [], [] for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == 'replace': res1.append(f"{' '.join(words1[i1:i2])}") res2.append(f"{' '.join(words2[j1:j2])}") elif tag == 'delete': res1.append(f"{' '.join(words1[i1:i2])}") elif tag == 'insert': res2.append(f"{' '.join(words2[j1:j2])}") elif tag == 'equal': res1.append(' '.join(words1[i1:i2])) res2.append(' '.join(words2[j1:j2])) return " ".join(res1), " ".join(res2) except Exception: return text1, text2 # ========================================== # 2. UI Component Handlers # ========================================== def load_initial_standards(): return gr.Dropdown(choices=fetch_available_standards()) def update_version_dropdown(standard): if not standard: return gr.Dropdown(choices=[]) versions = [] for file_name in os.listdir(UPLOAD_DIR): if file_name.startswith(standard + "_") and file_name.endswith(".db"): versions.append(file_name.replace(standard + "_", "").replace(".db", "")) return gr.Dropdown(choices=sorted(list(set(versions)))) def update_base_category_dropdown(standard, version): if not standard or not version: return gr.update(choices=[], value=None, interactive=False), gr.update(value="") choices = ["ALL"] status_value = "" db_path = os.path.join(UPLOAD_DIR, f"{standard}_{version}.db") if not os.path.exists(db_path): return gr.update(choices=choices), gr.update(value="") try: conn = sqlite3.connect(db_path) tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist() valid_tables = [t for t in tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']] main_table = f"{standard}_{version}" if main_table not in valid_tables: main_table = valid_tables[0] if valid_tables else None if main_table: try: cols_check = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist() if "Status" in cols_check or "status" in cols_check: status_df = pd.read_sql(f"SELECT Status FROM [{main_table}] WHERE Status IS NOT NULL AND Status != '' LIMIT 1", conn) if not status_df.empty: status_value = str(status_df.iloc[0]['Status']) except Exception: pass cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist() lower_cols = [c.lower() for c in cols] if 'chapter' in lower_cols and 'category' in lower_cols: ch_col = cols[lower_cols.index('chapter')] ca_col = cols[lower_cols.index('category')] df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{main_table}]", conn) for _, row in df.iterrows(): ch = str(row[ch_col]).strip() ca = str(row[ca_col]).strip() if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']: choices.append(f"{ch}.{ca}") pattern = re.compile(f"^{standard}[_\\s-]*{version}[_\\s-]*", re.IGNORECASE) for t in valid_tables: if t == main_table: continue short_name = pattern.sub("", t).strip(" _") if short_name and short_name not in choices: choices.append(short_name) elif t not in choices: choices.append(t) conn.close() except Exception: pass return gr.update(choices=choices, value=None, interactive=True), gr.update(value=status_value) def update_comp_standard_dropdown(base_std, base_ver): if not base_std or not base_ver: return gr.Dropdown(choices=[], value=None, interactive=False) try: conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db")) query = "SELECT DISTINCT TRIM(Comp_std) AS Comp_std FROM Mapping_registry WHERE TRIM(Base_std)=TRIM(?) AND TRIM(Base_ver)=TRIM(?)" df = pd.read_sql(query, conn, params=[base_std, base_ver]) conn.close() mapped_stds = sorted(df['Comp_std'].dropna().unique().tolist()) if not df.empty else [] return gr.update(choices=mapped_stds, value=None, interactive=bool(mapped_stds)) except Exception: return gr.update(choices=[], value=None, interactive=False) def update_comp_version_dropdown(base_std, base_ver, comp_std): if not all([base_std, base_ver, comp_std]): return gr.update(choices=[], value=None, interactive=False) try: conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db")) 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(?)" df = pd.read_sql(query, conn, params=[base_std, base_ver, comp_std]) conn.close() mapped_vers = sorted(df['Comp_ver'].dropna().unique().tolist()) if not df.empty else [] return gr.update(choices=mapped_vers, value=None, interactive=bool(mapped_vers)) except Exception: return gr.update(choices=[], value=None, interactive=False) def update_comp_category_dropdown(base_std, base_ver, base_cat, comp_std, comp_ver): if not all([base_std, base_ver, base_cat, comp_std, comp_ver]): return gr.update(choices=[], value=None, interactive=False), gr.update(value="") base_type = "Main" if not base_cat or base_cat == "ALL" or "." in base_cat else base_cat status_value = "" comp_db_path = os.path.join(UPLOAD_DIR, f"{comp_std}_{comp_ver}.db") c_main_table = None try: if os.path.exists(comp_db_path): c_conn = sqlite3.connect(comp_db_path) c_tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", c_conn)['name'].tolist() c_valid_tables = [t for t in c_tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']] c_main_table = f"{comp_std}_{comp_ver}" if c_main_table not in c_valid_tables: c_main_table = c_valid_tables[0] if c_valid_tables else None if c_main_table: try: cols_check = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist() if "Status" in cols_check or "status" in cols_check: status_df = pd.read_sql(f"SELECT Status FROM [{c_main_table}] WHERE Status IS NOT NULL AND Status != '' LIMIT 1", c_conn) if not status_df.empty: status_value = str(status_df.iloc[0]['Status']) except Exception: pass c_conn.close() conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db")) query = """ SELECT DISTINCT TRIM(Comp_Type) AS Comp_Type FROM Mapping_registry 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(?) """ df = pd.read_sql(query, conn, params=[base_std, base_ver, base_type, comp_std, comp_ver]) conn.close() allowed_types = df['Comp_Type'].dropna().tolist() if not allowed_types: return gr.update(choices=[], value=None), gr.update(value=status_value) final_choices = [] if "Main" in allowed_types: final_choices.append("ALL") if os.path.exists(comp_db_path) and c_main_table: c_conn = sqlite3.connect(comp_db_path) cols = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist() lower_cols = [c.lower() for c in cols] if 'chapter' in lower_cols and 'category' in lower_cols: ch_col = cols[lower_cols.index('chapter')] ca_col = cols[lower_cols.index('category')] c_df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{c_main_table}]", c_conn) for _, row in c_df.iterrows(): ch = str(row[ch_col]).strip() ca = str(row[ca_col]).strip() if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']: final_choices.append(f"{ch}.{ca}") c_conn.close() for t in allowed_types: if t != "Main": final_choices.append(t) return gr.update(choices=final_choices, value=final_choices[0] if final_choices else None, interactive=True), gr.update(value=status_value) except Exception: return gr.update(choices=[], value=None), gr.update(value="") def reset_base_selections(): return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="") def reset_comp_selections(): return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="") # ========================================== # 3. Core Search Logic # ========================================== def execute_unified_search(base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat, mapped_only, diff_only): try: def get_type_by_cat(cat): if not cat or cat == "ALL": return "Main" if "." in cat: return "Main" return cat type_b = get_type_by_cat(base_cat) type_c = get_type_by_cat(comp_cat) def apply_visual_merge(df, cols): if not df.empty and len(cols) > 1: is_dup = pd.Series([True] * len(df), index=df.index) for col in cols: if col in df.columns: curr = df[col].astype(str).str.strip() match = (curr == curr.shift(1)) & (~curr.isin(["", "nan", "None", " "])) is_dup = is_dup & match df.loc[is_dup, col] = " " return df def combine_code_desc(df): cols = list(df.columns) new_cols = [] processed = set() for col in cols: if col in processed: continue if "_Code" in col: desc_col = col.replace("_Code", "_Description") if desc_col in cols: new_col_name = col.replace("_Code", "") def combine_cells(row): c, d = str(row[col]).strip(), str(row[desc_col]).strip() if c in ["nan", "None", "", " "]: return d if d in ["nan", "None", "", " "]: return f"{c}" return f"{c}{d}" df[new_col_name] = df.apply(combine_cells, axis=1) new_cols.append(new_col_name) processed.update([col, desc_col]) else: new_cols.append(col) elif "_Description" in col: if col.replace("_Description", "_Code") not in cols: new_cols.append(col) else: new_cols.append(col) return df[new_cols] if base_std and base_ver and base_cat and (not comp_std or not comp_ver or not comp_cat): df, _ = fetch_database_records(base_std, base_ver, base_cat, type_b) if "Error" in df.columns: return df return apply_visual_merge(df, df.columns) if all([base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat]): df_base, real_anchors_b = fetch_database_records(base_std, base_ver, base_cat, type_b) df_comp, real_anchors_c = fetch_database_records(comp_std, comp_ver, comp_cat, type_c) if "Error" in df_base.columns: return df_base if "Error" in df_comp.columns: return df_comp for ra in real_anchors_b: if ra not in df_base.columns: return pd.DataFrame({"Error": [f"기준 열(Anchor) '{ra}'이(가) 기준 데이터에 존재하지 않습니다. Table_Config를 확인하세요."]}) for ra in real_anchors_c: if ra not in df_comp.columns: return pd.DataFrame({"Error": [f"비교 열(Anchor) '{ra}'이(가) 비교 데이터에 존재하지 않습니다. Table_Config를 확인하세요."]}) def clean_key_val(v): s = str(v).strip() if s.endswith('.0') and s[:-2].isdigit(): s = s[:-2] return s.replace(" ", "") internal_rename_b = {c: f"{c}_INTERNAL_BASE" for c in df_base.columns if c != 'merge_key'} internal_rename_c = {c: f"{c}_INTERNAL_COMP" for c in df_comp.columns if c != 'merge_key'} df_base['merge_key'] = df_base[real_anchors_b].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1) df_comp['merge_key'] = df_comp[real_anchors_c].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1) conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db")) registry_query = """ SELECT Target_Table FROM Mapping_registry WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=? LIMIT 1 """ reg_df = pd.read_sql(registry_query, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()]) target_table_name = None if not reg_df.empty and pd.notna(reg_df.iloc[0]['Target_Table']): val = str(reg_df.iloc[0]['Target_Table']).strip() if val and val.lower() not in ["none", "nan"]: target_table_name = val # 💡 [핵심] 사용자님 시나리오 완벽 적용 is_same_std = (base_std.strip().upper() == comp_std.strip().upper() and type_b.strip().upper() == type_c.strip().upper()) df_mapping = pd.DataFrame() if target_table_name: try: q_fw = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?" df_fw = pd.read_sql(q_fw, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()]) q_rv = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Comp_std)=? AND TRIM(Comp_ver)=? AND TRIM(Base_std)=? AND TRIM(Base_ver)=?" df_rv = pd.read_sql(q_rv, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()]) cols_fw_lower = {c.lower(): c for c in df_fw.columns} if 'base_type' in cols_fw_lower and 'comp_type' in cols_fw_lower: b_col = cols_fw_lower['base_type'] c_col = cols_fw_lower['comp_type'] df_fw[b_col] = df_fw[b_col].fillna('Main').astype(str).str.strip().str.upper() df_fw[c_col] = df_fw[c_col].fillna('Main').astype(str).str.strip().str.upper() df_fw = df_fw[(df_fw[b_col] == type_b.strip().upper()) & (df_fw[c_col] == type_c.strip().upper())] if not df_rv.empty: df_rv[b_col] = df_rv[b_col].fillna('Main').astype(str).str.strip().str.upper() df_rv[c_col] = df_rv[c_col].fillna('Main').astype(str).str.strip().str.upper() df_rv = df_rv[(df_rv[c_col] == type_b.strip().upper()) & (df_rv[b_col] == type_c.strip().upper())] b_sec = cols_fw_lower.get('base_section', 'Base_section') c_sec = cols_fw_lower.get('comp_section', 'Comp_section') if b_sec not in df_fw.columns or c_sec not in df_fw.columns: conn_map.close() return pd.DataFrame({"Error": [f"'{target_table_name}' 에 '{b_sec}' 또는 '{c_sec}' 열이 없습니다. 대소문자를 확인하세요."]}) df_fw = df_fw[[b_sec, c_sec]].rename(columns={b_sec: 'Base_section', c_sec: 'Comp_section'}) if not df_rv.empty: df_rv = df_rv[[b_sec, c_sec]].rename(columns={b_sec: 'Comp_section', c_sec: 'Base_section'}) else: df_rv = pd.DataFrame(columns=['Base_section', 'Comp_section']) df_mapping = pd.concat([df_fw, df_rv], ignore_index=True) if not df_mapping.empty: df_mapping['Base_section'] = df_mapping['Base_section'].astype(str).str.replace('\n', ',').str.split(',') df_mapping['Comp_section'] = df_mapping['Comp_section'].astype(str).str.replace('\n', ',').str.split(',') df_mapping = df_mapping.explode('Base_section').explode('Comp_section') df_mapping['Base_section'] = df_mapping['Base_section'].apply(clean_key_val) df_mapping['Comp_section'] = df_mapping['Comp_section'].apply(clean_key_val) df_mapping = df_mapping[(df_mapping['Base_section'] != "") & (df_mapping['Comp_section'] != "")] bridge = df_mapping.dropna().drop_duplicates() else: bridge = pd.DataFrame(columns=['Base_section', 'Comp_section']) except Exception as sql_e: conn_map.close() return pd.DataFrame({"Error": [f"매핑 테이블 '{target_table_name}'을 여는 데 실패했습니다. 테이블 이름을 확인하세요: {str(sql_e)}"]}) else: if is_same_std: all_keys = list(set(df_base['merge_key']).union(set(df_comp['merge_key']))) bridge = pd.DataFrame({'Base_section': all_keys, 'Comp_section': all_keys}) else: conn_map.close() return pd.DataFrame({"Error": ["매핑 테이블이 존재하지 않습니다."]}) conn_map.close() df_base = df_base.rename(columns=internal_rename_b) df_comp = df_comp.rename(columns=internal_rename_c) df_base['base_idx'], df_comp['comp_idx'] = range(len(df_base)), range(len(df_comp)) merged = pd.merge(bridge, df_base, left_on='Base_section', right_on='merge_key', how='outer') merged = pd.merge(merged, df_comp, left_on='Comp_section', right_on='merge_key', how='outer') merged['base_idx'] = merged['base_idx'].fillna(float('inf')) merged['comp_idx'] = merged['comp_idx'].fillna(float('inf')) merged = merged.sort_values(['base_idx', 'comp_idx']) result_rows = [] for _, row in merged.iterrows(): row_dict = {} has_b = not pd.isna(row.get('base_idx')) and row.get('base_idx') != float('inf') has_c = not pd.isna(row.get('comp_idx')) and row.get('comp_idx') != float('inf') for c in internal_rename_b.values(): row_dict[c] = str(row[c]) if has_b and not pd.isna(row[c]) else "" for c in internal_rename_c.values(): row_dict[c] = str(row[c]) if has_c and not pd.isna(row[c]) else "" if mapped_only: b_has_val = any(str(row_dict[c]).strip() not in ["", "nan", "None", " "] for c in internal_rename_b.values()) c_has_val = any(str(row_dict[c]).strip() not in ["", "nan", "None", " "] for c in internal_rename_c.values()) if not (b_has_val and c_has_val): continue if has_b and has_c: for orig_col in internal_rename_b.keys(): if ('description' in orig_col.lower() or '내용' in orig_col) and internal_rename_c.get(orig_col) in row_dict: b_v, c_v = row_dict[internal_rename_b[orig_col]], row_dict[internal_rename_c[orig_col]] if b_v and c_v and " div { max-height: none !important; overflow: visible !important; } td { font-size: 18px !important; white-space: pre-wrap !important; word-break: keep-all !important; line-height: 1.6; padding: 10px; vertical-align: top !important; text-align: left !important; } td img { display: block; max-width: none !important; } #search_row { align-items: center !important; margin-bottom: 5px !important; } #diff_cb_item, #mapped_cb_item { margin-top: 0 !important; padding-left: 15px !important; width: max-content !important; min-width: max-content !important; flex-grow: 0 !important; } """ if __name__ == "__main__": demo.launch( theme=gr.themes.Soft(), share=True, css=css )