Update app.py
Browse files
app.py
CHANGED
|
@@ -6,745 +6,1487 @@ import re
|
|
| 6 |
import base64
|
| 7 |
import difflib
|
| 8 |
|
|
|
|
| 9 |
# ==========================================
|
| 10 |
# 1. Environment Setup & Data Helpers
|
| 11 |
# ==========================================
|
|
|
|
| 12 |
UPLOAD_DIR = "uploaded_dbs"
|
|
|
|
| 13 |
if not os.path.exists(UPLOAD_DIR):
|
|
|
|
| 14 |
os.makedirs(UPLOAD_DIR)
|
| 15 |
|
|
|
|
|
|
|
| 16 |
db_registry = []
|
| 17 |
|
|
|
|
|
|
|
| 18 |
def convert_blob_to_html_img(blob_data):
|
|
|
|
| 19 |
if blob_data is None or pd.isna(blob_data):
|
|
|
|
| 20 |
return ""
|
|
|
|
| 21 |
try:
|
|
|
|
| 22 |
if isinstance(blob_data, (bytes, bytearray)):
|
|
|
|
| 23 |
encoded = base64.b64encode(blob_data).decode('utf-8')
|
|
|
|
| 24 |
return f'''
|
|
|
|
| 25 |
<img src="data:image/png;base64,{encoded}"
|
|
|
|
| 26 |
style="width: 40%;
|
|
|
|
| 27 |
max-height: 300px;
|
|
|
|
| 28 |
object-fit: contain;
|
|
|
|
| 29 |
display: block;
|
|
|
|
| 30 |
margin: 10px 0;">
|
|
|
|
| 31 |
'''
|
|
|
|
| 32 |
return str(blob_data)
|
|
|
|
| 33 |
except Exception:
|
|
|
|
| 34 |
return str(blob_data)
|
| 35 |
|
|
|
|
|
|
|
| 36 |
def decode_sqlite_text(x):
|
|
|
|
| 37 |
try:
|
|
|
|
| 38 |
return x.decode('utf-8')
|
|
|
|
| 39 |
except UnicodeDecodeError:
|
|
|
|
| 40 |
return x
|
| 41 |
|
|
|
|
|
|
|
| 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", "")
|
|
|
|
| 48 |
parts = name.split("_")
|
|
|
|
| 49 |
if len(parts) >= 2:
|
|
|
|
| 50 |
db_registry.append({
|
|
|
|
| 51 |
"path": os.path.join(UPLOAD_DIR, file_name),
|
|
|
|
| 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 |
|
|
|
|
| 66 |
main_table = None
|
|
|
|
| 67 |
if table_type and table_type.upper() != "MAIN":
|
|
|
|
| 68 |
expected_name = f"{std}_{ver}_{table_type}"
|
|
|
|
| 69 |
for t in valid_tables:
|
|
|
|
| 70 |
if t.lower() == expected_name.lower() or t.lower() == table_type.lower():
|
|
|
|
| 71 |
main_table = t
|
|
|
|
| 72 |
break
|
|
|
|
| 73 |
|
|
|
|
| 74 |
if not main_table:
|
|
|
|
| 75 |
main_table = f"{std}_{ver}"
|
|
|
|
| 76 |
if main_table not in valid_tables:
|
|
|
|
| 77 |
main_table = valid_tables[0] if valid_tables else None
|
|
|
|
| 78 |
|
|
|
|
| 79 |
if not main_table:
|
|
|
|
| 80 |
conn.close()
|
|
|
|
| 81 |
return pd.DataFrame({"Error": ["데이터 테이블을 찾을 수 없습니다."]}), []
|
|
|
|
| 82 |
|
|
|
|
| 83 |
cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
|
|
|
|
| 84 |
lower_cols = [c.lower() for c in cols]
|
|
|
|
| 85 |
|
|
|
|
| 86 |
conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
|
|
|
| 87 |
config_df = pd.read_sql("SELECT * FROM Table_Config WHERE TRIM(Table_Type)=?", conn_map, params=[table_type.strip()])
|
|
|
|
| 88 |
conn_map.close()
|
|
|
|
| 89 |
|
|
|
|
| 90 |
if config_df.empty:
|
|
|
|
| 91 |
return pd.DataFrame({"Error": [f"Table_Config에서 '{table_type}' 설정을 찾을 수 없습니다."]}), []
|
|
|
|
| 92 |
|
|
|
|
| 93 |
matched_config = None
|
|
|
|
| 94 |
for _, row in config_df.iterrows():
|
|
|
|
| 95 |
anchors_test = [x.strip().lower() for x in str(row['Anchor_Column']).split(',')]
|
|
|
|
| 96 |
if any(a in lower_cols for a in anchors_test):
|
|
|
|
| 97 |
matched_config = row
|
|
|
|
| 98 |
break
|
|
|
|
| 99 |
|
|
|
|
| 100 |
if matched_config is None:
|
|
|
|
| 101 |
matched_config = config_df.iloc[0]
|
|
|
|
| 102 |
|
|
|
|
| 103 |
anchors = [x.strip() for x in matched_config['Anchor_Column'].split(',')]
|
|
|
|
| 104 |
displays = [x.strip() for x in matched_config['Display_Columns'].split(',')] if pd.notna(matched_config['Display_Columns']) else anchors
|
|
|
|
| 105 |
|
|
|
|
| 106 |
query = f"SELECT * FROM [{main_table}]"
|
|
|
|
| 107 |
conditions = []
|
|
|
|
| 108 |
|
|
|
|
| 109 |
if cat and cat != "ALL":
|
|
|
|
| 110 |
if "." in cat and 'chapter' in lower_cols and 'category' in lower_cols:
|
|
|
|
| 111 |
ch, ca = cat.split(".", 1)
|
|
|
|
| 112 |
ch_col = cols[lower_cols.index('chapter')]
|
|
|
|
| 113 |
ca_col = cols[lower_cols.index('category')]
|
|
|
|
| 114 |
conditions.append(f"[{ch_col}] = '{ch}' AND [{ca_col}] = '{ca}'")
|
|
|
|
| 115 |
elif 'category' in lower_cols:
|
|
|
|
| 116 |
ca_col = cols[lower_cols.index('category')]
|
|
|
|
| 117 |
conditions.append(f"[{ca_col}] = '{cat}'")
|
|
|
|
| 118 |
|
|
|
|
| 119 |
if conditions:
|
|
|
|
| 120 |
query += " WHERE " + " AND ".join(conditions)
|
|
|
|
| 121 |
|
|
|
|
| 122 |
df = pd.read_sql(query, conn)
|
|
|
|
| 123 |
conn.close()
|
|
|
|
| 124 |
|
|
|
|
| 125 |
real_anchors = [c for c in df.columns if any(a.lower() == c.lower() for a in anchors)]
|
|
|
|
| 126 |
|
|
|
|
| 127 |
final_cols = []
|
|
|
|
| 128 |
for d in displays:
|
|
|
|
| 129 |
for c in df.columns:
|
|
|
|
| 130 |
if d.lower() == c.lower():
|
|
|
|
| 131 |
if c not in final_cols:
|
|
|
|
| 132 |
final_cols.append(c)
|
|
|
|
| 133 |
break
|
|
|
|
| 134 |
|
|
|
|
| 135 |
for ra in real_anchors:
|
|
|
|
| 136 |
if ra not in final_cols:
|
|
|
|
| 137 |
final_cols.append(ra)
|
|
|
|
| 138 |
|
|
|
|
| 139 |
if not final_cols:
|
|
|
|
| 140 |
return df, real_anchors
|
|
|
|
| 141 |
|
|
|
|
| 142 |
for c in final_cols:
|
|
|
|
| 143 |
df[c] = df[c].apply(convert_blob_to_html_img)
|
|
|
|
| 144 |
|
|
|
|
| 145 |
return df[final_cols], real_anchors
|
|
|
|
| 146 |
|
|
|
|
| 147 |
except Exception as e:
|
|
|
|
| 148 |
import traceback
|
|
|
|
| 149 |
traceback.print_exc()
|
|
|
|
| 150 |
return pd.DataFrame({"Error": [f"데이터 로드 오류: {str(e)}"]}), []
|
| 151 |
|
|
|
|
|
|
|
| 152 |
def generate_html_diff(text1, text2):
|
|
|
|
| 153 |
try:
|
|
|
|
| 154 |
s1, s2 = str(text1), str(text2)
|
|
|
|
| 155 |
|
|
|
|
| 156 |
if len(s1) > 1000 or len(s2) > 1000:
|
|
|
|
| 157 |
return s1, s2
|
|
|
|
| 158 |
|
|
|
|
| 159 |
words1, words2 = s1.split(), s2.split()
|
|
|
|
| 160 |
if not words1 or not words2:
|
|
|
|
| 161 |
return s1, s2
|
|
|
|
| 162 |
|
|
|
|
| 163 |
common_words = set(words1) & set(words2)
|
|
|
|
| 164 |
if len(common_words) / min(len(words1), len(words2)) < 0.05:
|
|
|
|
| 165 |
return s1, s2
|
| 166 |
|
|
|
|
|
|
|
| 167 |
matcher = difflib.SequenceMatcher(None, words1, words2)
|
|
|
|
| 168 |
res1, res2 = [], []
|
|
|
|
| 169 |
|
|
|
|
| 170 |
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
|
|
| 171 |
if tag == 'replace':
|
|
|
|
| 172 |
res1.append(f"<span style='color:#ff4d4f; font-weight:bold;'>{' '.join(words1[i1:i2])}</span>")
|
|
|
|
| 173 |
res2.append(f"<span style='color:#2ecc71; font-weight:bold;'>{' '.join(words2[j1:j2])}</span>")
|
|
|
|
| 174 |
elif tag == 'delete':
|
|
|
|
| 175 |
res1.append(f"<span style='color:#ff4d4f; font-weight:bold;'>{' '.join(words1[i1:i2])}</span>")
|
|
|
|
| 176 |
elif tag == 'insert':
|
|
|
|
| 177 |
res2.append(f"<span style='color:#2ecc71; font-weight:bold;'>{' '.join(words2[j1:j2])}</span>")
|
|
|
|
| 178 |
elif tag == 'equal':
|
|
|
|
| 179 |
res1.append(' '.join(words1[i1:i2]))
|
|
|
|
| 180 |
res2.append(' '.join(words2[j1:j2]))
|
|
|
|
| 181 |
|
|
|
|
| 182 |
return " ".join(res1), " ".join(res2)
|
|
|
|
| 183 |
except Exception:
|
|
|
|
| 184 |
return text1, text2
|
| 185 |
|
|
|
|
|
|
|
| 186 |
# ==========================================
|
|
|
|
| 187 |
# 2. UI Component Handlers
|
|
|
|
| 188 |
# ==========================================
|
|
|
|
| 189 |
def load_initial_standards():
|
|
|
|
| 190 |
return gr.Dropdown(choices=fetch_available_standards())
|
| 191 |
|
|
|
|
|
|
|
| 192 |
def update_version_dropdown(standard):
|
|
|
|
| 193 |
if not standard:
|
|
|
|
| 194 |
return gr.Dropdown(choices=[])
|
|
|
|
| 195 |
versions = []
|
|
|
|
| 196 |
for file_name in os.listdir(UPLOAD_DIR):
|
|
|
|
| 197 |
if file_name.startswith(standard + "_") and file_name.endswith(".db"):
|
|
|
|
| 198 |
versions.append(file_name.replace(standard + "_", "").replace(".db", ""))
|
|
|
|
| 199 |
return gr.Dropdown(choices=sorted(list(set(versions))))
|
| 200 |
|
|
|
|
|
|
|
| 201 |
def update_base_category_dropdown(standard, version):
|
|
|
|
| 202 |
if not standard or not version:
|
|
|
|
| 203 |
return gr.update(choices=[], value=None, interactive=False), gr.update(value="")
|
| 204 |
|
|
|
|
|
|
|
| 205 |
choices = ["ALL"]
|
|
|
|
| 206 |
status_value = ""
|
|
|
|
| 207 |
db_path = os.path.join(UPLOAD_DIR, f"{standard}_{version}.db")
|
|
|
|
| 208 |
|
|
|
|
| 209 |
if not os.path.exists(db_path):
|
|
|
|
| 210 |
return gr.update(choices=choices), gr.update(value="")
|
| 211 |
|
|
|
|
|
|
|
| 212 |
try:
|
|
|
|
| 213 |
conn = sqlite3.connect(db_path)
|
|
|
|
| 214 |
tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist()
|
|
|
|
| 215 |
valid_tables = [t for t in tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']]
|
|
|
|
| 216 |
main_table = f"{standard}_{version}"
|
|
|
|
| 217 |
if main_table not in valid_tables:
|
|
|
|
| 218 |
main_table = valid_tables[0] if valid_tables else None
|
| 219 |
|
|
|
|
|
|
|
| 220 |
if main_table:
|
|
|
|
| 221 |
try:
|
|
|
|
| 222 |
cols_check = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
|
|
|
|
| 223 |
if "Status" in cols_check or "status" in cols_check:
|
|
|
|
| 224 |
status_df = pd.read_sql(f"SELECT Status FROM [{main_table}] WHERE Status IS NOT NULL AND Status != '' LIMIT 1", conn)
|
|
|
|
| 225 |
if not status_df.empty:
|
|
|
|
| 226 |
status_value = str(status_df.iloc[0]['Status'])
|
|
|
|
| 227 |
except Exception:
|
|
|
|
| 228 |
pass
|
| 229 |
|
|
|
|
|
|
|
| 230 |
cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
|
|
|
|
| 231 |
lower_cols = [c.lower() for c in cols]
|
|
|
|
| 232 |
if 'chapter' in lower_cols and 'category' in lower_cols:
|
|
|
|
| 233 |
ch_col = cols[lower_cols.index('chapter')]
|
|
|
|
| 234 |
ca_col = cols[lower_cols.index('category')]
|
|
|
|
| 235 |
|
|
|
|
| 236 |
df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{main_table}]", conn)
|
|
|
|
| 237 |
for _, row in df.iterrows():
|
|
|
|
| 238 |
ch = str(row[ch_col]).strip()
|
|
|
|
| 239 |
ca = str(row[ca_col]).strip()
|
|
|
|
| 240 |
if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']:
|
|
|
|
| 241 |
choices.append(f"{ch}.{ca}")
|
| 242 |
|
|
|
|
|
|
|
| 243 |
pattern = re.compile(f"^{standard}[_\\s-]*{version}[_\\s-]*", re.IGNORECASE)
|
|
|
|
| 244 |
for t in valid_tables:
|
|
|
|
| 245 |
if t == main_table: continue
|
|
|
|
| 246 |
short_name = pattern.sub("", t).strip(" _")
|
|
|
|
| 247 |
if short_name and short_name not in choices:
|
|
|
|
| 248 |
choices.append(short_name)
|
|
|
|
| 249 |
elif t not in choices:
|
|
|
|
| 250 |
choices.append(t)
|
| 251 |
|
|
|
|
|
|
|
| 252 |
conn.close()
|
|
|
|
| 253 |
except Exception:
|
|
|
|
| 254 |
pass
|
|
|
|
| 255 |
|
|
|
|
| 256 |
return gr.update(choices=choices, value=None, interactive=True), gr.update(value=status_value)
|
| 257 |
|
|
|
|
|
|
|
| 258 |
def update_comp_standard_dropdown(base_std, base_ver):
|
|
|
|
| 259 |
if not base_std or not base_ver:
|
|
|
|
| 260 |
return gr.Dropdown(choices=[], value=None, interactive=False)
|
| 261 |
|
|
|
|
|
|
|
| 262 |
try:
|
|
|
|
| 263 |
conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
|
|
|
| 264 |
query = "SELECT DISTINCT TRIM(Comp_std) AS Comp_std FROM Mapping_registry WHERE TRIM(Base_std)=TRIM(?) AND TRIM(Base_ver)=TRIM(?)"
|
|
|
|
| 265 |
df = pd.read_sql(query, conn, params=[base_std, base_ver])
|
|
|
|
| 266 |
conn.close()
|
| 267 |
|
|
|
|
|
|
|
| 268 |
mapped_stds = sorted(df['Comp_std'].dropna().unique().tolist()) if not df.empty else []
|
|
|
|
| 269 |
return gr.update(choices=mapped_stds, value=None, interactive=bool(mapped_stds))
|
|
|
|
| 270 |
except Exception:
|
|
|
|
| 271 |
return gr.update(choices=[], value=None, interactive=False)
|
| 272 |
|
|
|
|
|
|
|
| 273 |
def update_comp_version_dropdown(base_std, base_ver, comp_std):
|
|
|
|
| 274 |
if not all([base_std, base_ver, comp_std]):
|
|
|
|
| 275 |
return gr.update(choices=[], value=None, interactive=False)
|
| 276 |
|
|
|
|
|
|
|
| 277 |
try:
|
|
|
|
| 278 |
conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
|
|
|
| 279 |
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(?)"
|
|
|
|
| 280 |
df = pd.read_sql(query, conn, params=[base_std, base_ver, comp_std])
|
|
|
|
| 281 |
conn.close()
|
| 282 |
|
|
|
|
|
|
|
| 283 |
mapped_vers = sorted(df['Comp_ver'].dropna().unique().tolist()) if not df.empty else []
|
|
|
|
| 284 |
return gr.update(choices=mapped_vers, value=None, interactive=bool(mapped_vers))
|
|
|
|
| 285 |
except Exception:
|
|
|
|
| 286 |
return gr.update(choices=[], value=None, interactive=False)
|
| 287 |
|
|
|
|
|
|
|
| 288 |
def update_comp_category_dropdown(base_std, base_ver, base_cat, comp_std, comp_ver):
|
|
|
|
| 289 |
if not all([base_std, base_ver, base_cat, comp_std, comp_ver]):
|
|
|
|
| 290 |
return gr.update(choices=[], value=None, interactive=False), gr.update(value="")
|
| 291 |
|
|
|
|
|
|
|
| 292 |
base_type = "Main" if not base_cat or base_cat == "ALL" or "." in base_cat else base_cat
|
|
|
|
| 293 |
status_value = ""
|
|
|
|
| 294 |
comp_db_path = os.path.join(UPLOAD_DIR, f"{comp_std}_{comp_ver}.db")
|
|
|
|
| 295 |
c_main_table = None
|
| 296 |
|
|
|
|
|
|
|
| 297 |
try:
|
|
|
|
| 298 |
if os.path.exists(comp_db_path):
|
|
|
|
| 299 |
c_conn = sqlite3.connect(comp_db_path)
|
|
|
|
| 300 |
c_tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", c_conn)['name'].tolist()
|
|
|
|
| 301 |
c_valid_tables = [t for t in c_tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']]
|
|
|
|
| 302 |
c_main_table = f"{comp_std}_{comp_ver}"
|
|
|
|
| 303 |
if c_main_table not in c_valid_tables:
|
|
|
|
| 304 |
c_main_table = c_valid_tables[0] if c_valid_tables else None
|
|
|
|
| 305 |
|
|
|
|
| 306 |
if c_main_table:
|
|
|
|
| 307 |
try:
|
|
|
|
| 308 |
cols_check = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist()
|
|
|
|
| 309 |
if "Status" in cols_check or "status" in cols_check:
|
|
|
|
| 310 |
status_df = pd.read_sql(f"SELECT Status FROM [{c_main_table}] WHERE Status IS NOT NULL AND Status != '' LIMIT 1", c_conn)
|
|
|
|
| 311 |
if not status_df.empty:
|
|
|
|
| 312 |
status_value = str(status_df.iloc[0]['Status'])
|
|
|
|
| 313 |
except Exception:
|
|
|
|
| 314 |
pass
|
|
|
|
| 315 |
c_conn.close()
|
| 316 |
|
|
|
|
|
|
|
| 317 |
conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
|
|
|
| 318 |
query = """
|
|
|
|
| 319 |
SELECT DISTINCT TRIM(Comp_Type) AS Comp_Type
|
|
|
|
| 320 |
FROM Mapping_registry
|
|
|
|
| 321 |
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(?)
|
|
|
|
| 322 |
"""
|
|
|
|
| 323 |
df = pd.read_sql(query, conn, params=[base_std, base_ver, base_type, comp_std, comp_ver])
|
|
|
|
| 324 |
conn.close()
|
| 325 |
|
|
|
|
|
|
|
| 326 |
allowed_types = df['Comp_Type'].dropna().tolist()
|
|
|
|
| 327 |
if not allowed_types:
|
|
|
|
| 328 |
return gr.update(choices=[], value=None), gr.update(value=status_value)
|
| 329 |
|
|
|
|
|
|
|
| 330 |
final_choices = []
|
|
|
|
| 331 |
if "Main" in allowed_types:
|
|
|
|
| 332 |
final_choices.append("ALL")
|
|
|
|
| 333 |
if os.path.exists(comp_db_path) and c_main_table:
|
|
|
|
| 334 |
c_conn = sqlite3.connect(comp_db_path)
|
|
|
|
| 335 |
cols = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist()
|
|
|
|
| 336 |
lower_cols = [c.lower() for c in cols]
|
|
|
|
| 337 |
if 'chapter' in lower_cols and 'category' in lower_cols:
|
|
|
|
| 338 |
ch_col = cols[lower_cols.index('chapter')]
|
|
|
|
| 339 |
ca_col = cols[lower_cols.index('category')]
|
|
|
|
| 340 |
c_df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{c_main_table}]", c_conn)
|
|
|
|
| 341 |
for _, row in c_df.iterrows():
|
|
|
|
| 342 |
ch = str(row[ch_col]).strip()
|
|
|
|
| 343 |
ca = str(row[ca_col]).strip()
|
|
|
|
| 344 |
if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']:
|
|
|
|
| 345 |
final_choices.append(f"{ch}.{ca}")
|
|
|
|
| 346 |
c_conn.close()
|
|
|
|
| 347 |
|
|
|
|
| 348 |
for t in allowed_types:
|
|
|
|
| 349 |
if t != "Main": final_choices.append(t)
|
| 350 |
|
|
|
|
|
|
|
| 351 |
return gr.update(choices=final_choices, value=final_choices[0] if final_choices else None, interactive=True), gr.update(value=status_value)
|
|
|
|
| 352 |
|
|
|
|
| 353 |
except Exception:
|
|
|
|
| 354 |
return gr.update(choices=[], value=None), gr.update(value="")
|
| 355 |
|
|
|
|
|
|
|
| 356 |
def reset_base_selections():
|
|
|
|
| 357 |
return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="")
|
| 358 |
|
|
|
|
|
|
|
| 359 |
def reset_comp_selections():
|
|
|
|
| 360 |
return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="")
|
| 361 |
|
|
|
|
|
|
|
| 362 |
# ==========================================
|
|
|
|
| 363 |
# 3. Core Search Logic
|
|
|
|
| 364 |
# ==========================================
|
|
|
|
| 365 |
def execute_unified_search(base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat, mapped_only, diff_only):
|
|
|
|
| 366 |
try:
|
|
|
|
| 367 |
def get_type_by_cat(cat):
|
|
|
|
| 368 |
if not cat or cat == "ALL": return "Main"
|
|
|
|
| 369 |
if "." in cat: return "Main"
|
|
|
|
| 370 |
return cat
|
| 371 |
|
|
|
|
|
|
|
| 372 |
type_b = get_type_by_cat(base_cat)
|
|
|
|
| 373 |
type_c = get_type_by_cat(comp_cat)
|
|
|
|
| 374 |
|
|
|
|
| 375 |
def apply_visual_merge(df, cols):
|
|
|
|
| 376 |
if not df.empty and len(cols) > 1:
|
|
|
|
| 377 |
is_dup = pd.Series([True] * len(df), index=df.index)
|
|
|
|
| 378 |
for col in cols:
|
|
|
|
| 379 |
if col in df.columns:
|
|
|
|
| 380 |
curr = df[col].astype(str).str.strip()
|
|
|
|
| 381 |
match = (curr == curr.shift(1)) & (~curr.isin(["", "nan", "None", " "]))
|
|
|
|
| 382 |
is_dup = is_dup & match
|
|
|
|
| 383 |
df.loc[is_dup, col] = " "
|
|
|
|
| 384 |
return df
|
| 385 |
|
|
|
|
|
|
|
| 386 |
def combine_code_desc(df):
|
|
|
|
| 387 |
cols = list(df.columns)
|
|
|
|
| 388 |
new_cols = []
|
|
|
|
| 389 |
processed = set()
|
|
|
|
| 390 |
for col in cols:
|
|
|
|
| 391 |
if col in processed: continue
|
|
|
|
| 392 |
if "_Code" in col:
|
|
|
|
| 393 |
desc_col = col.replace("_Code", "_Description")
|
|
|
|
| 394 |
if desc_col in cols:
|
|
|
|
| 395 |
new_col_name = col.replace("_Code", "")
|
|
|
|
| 396 |
def combine_cells(row):
|
|
|
|
| 397 |
c, d = str(row[col]).strip(), str(row[desc_col]).strip()
|
|
|
|
| 398 |
if c in ["nan", "None", "", " "]: return d
|
|
|
|
| 399 |
if d in ["nan", "None", "", " "]: return f"<span style='font-weight:bold; color:#1a73e8;'>{c}</span>"
|
|
|
|
| 400 |
return f"<span style='font-weight:bold; color:#1a73e8; display:block; margin-bottom:4px;'>{c}</span>{d}"
|
|
|
|
| 401 |
df[new_col_name] = df.apply(combine_cells, axis=1)
|
|
|
|
| 402 |
new_cols.append(new_col_name)
|
|
|
|
| 403 |
processed.update([col, desc_col])
|
|
|
|
| 404 |
else: new_cols.append(col)
|
|
|
|
| 405 |
elif "_Description" in col:
|
|
|
|
| 406 |
if col.replace("_Description", "_Code") not in cols: new_cols.append(col)
|
|
|
|
| 407 |
else: new_cols.append(col)
|
|
|
|
| 408 |
return df[new_cols]
|
| 409 |
|
|
|
|
|
|
|
| 410 |
if base_std and base_ver and base_cat and (not comp_std or not comp_ver or not comp_cat):
|
|
|
|
| 411 |
df, _ = fetch_database_records(base_std, base_ver, base_cat, type_b)
|
|
|
|
| 412 |
if "Error" in df.columns: return df
|
| 413 |
-
|
| 414 |
-
# 💡 [요청 반영] 단일 조회 시 화면 하단 표에 Status 열이 나오지 않도록 처리합니다.
|
| 415 |
-
status_cols = [c for c in df.columns if c.lower() == 'status']
|
| 416 |
-
if status_cols:
|
| 417 |
-
df = df.drop(columns=status_cols)
|
| 418 |
-
|
| 419 |
return apply_visual_merge(df, df.columns)
|
| 420 |
|
|
|
|
|
|
|
| 421 |
if all([base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat]):
|
|
|
|
| 422 |
df_base, real_anchors_b = fetch_database_records(base_std, base_ver, base_cat, type_b)
|
|
|
|
| 423 |
df_comp, real_anchors_c = fetch_database_records(comp_std, comp_ver, comp_cat, type_c)
|
| 424 |
|
|
|
|
|
|
|
| 425 |
if "Error" in df_base.columns: return df_base
|
|
|
|
| 426 |
if "Error" in df_comp.columns: return df_comp
|
| 427 |
|
|
|
|
|
|
|
| 428 |
for ra in real_anchors_b:
|
|
|
|
| 429 |
if ra not in df_base.columns:
|
|
|
|
| 430 |
return pd.DataFrame({"Error": [f"기준 열(Anchor) '{ra}'이(가) 기준 데이터에 존재하지 않습니다. Table_Config를 확인하세요."]})
|
|
|
|
| 431 |
for ra in real_anchors_c:
|
|
|
|
| 432 |
if ra not in df_comp.columns:
|
|
|
|
| 433 |
return pd.DataFrame({"Error": [f"비교 열(Anchor) '{ra}'이(가) 비교 데이터에 존재하지 않습니다. Table_Config를 확인하세요."]})
|
| 434 |
|
|
|
|
|
|
|
| 435 |
def clean_key_val(v):
|
|
|
|
| 436 |
s = str(v).strip()
|
|
|
|
| 437 |
if s.endswith('.0') and s[:-2].isdigit():
|
|
|
|
| 438 |
s = s[:-2]
|
|
|
|
| 439 |
return s.replace(" ", "")
|
| 440 |
|
| 441 |
-
df_base['merge_key'] = df_base[real_anchors_b].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
|
| 442 |
-
df_comp['merge_key'] = df_comp[real_anchors_c].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
|
| 443 |
|
| 444 |
-
conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
| 445 |
-
registry_query = """
|
| 446 |
-
SELECT Target_Table FROM Mapping_registry
|
| 447 |
-
WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?
|
| 448 |
-
LIMIT 1
|
| 449 |
-
"""
|
| 450 |
-
reg_df = pd.read_sql(registry_query, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
|
| 451 |
-
|
| 452 |
-
target_table_name = "Mapping_table"
|
| 453 |
-
if not reg_df.empty and pd.notna(reg_df.iloc[0]['Target_Table']):
|
| 454 |
-
val = str(reg_df.iloc[0]['Target_Table']).strip()
|
| 455 |
-
if val and val.lower() not in ["none", "nan"]:
|
| 456 |
-
target_table_name = val
|
| 457 |
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
|
| 469 |
-
cols_fw_lower = {c.lower(): c for c in df_fw.columns}
|
| 470 |
-
|
| 471 |
-
if 'base_type' in cols_fw_lower and 'comp_type' in cols_fw_lower:
|
| 472 |
-
b_col = cols_fw_lower['base_type']
|
| 473 |
-
c_col = cols_fw_lower['comp_type']
|
| 474 |
-
|
| 475 |
-
df_fw[b_col] = df_fw[b_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 476 |
-
df_fw[c_col] = df_fw[c_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 477 |
-
df_fw = df_fw[(df_fw[b_col] == type_b.strip().upper()) & (df_fw[c_col] == type_c.strip().upper())]
|
| 478 |
-
|
| 479 |
-
if not df_rv.empty:
|
| 480 |
-
df_rv[b_col] = df_rv[b_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 481 |
-
df_rv[c_col] = df_rv[c_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 482 |
-
df_rv = df_rv[(df_rv[c_col] == type_b.strip().upper()) & (df_rv[b_col] == type_c.strip().upper())]
|
| 483 |
|
| 484 |
-
b_sec = cols_fw_lower.get('base_section', 'Base_section')
|
| 485 |
-
c_sec = cols_fw_lower.get('comp_section', 'Comp_section')
|
| 486 |
-
|
| 487 |
-
if b_sec not in df_fw.columns or c_sec not in df_fw.columns:
|
| 488 |
-
return pd.DataFrame({"Error": [f"'{target_table_name}' 장부에 '{b_sec}' 또는 '{c_sec}' 열이 없습니다. 대소문자를 확인하세요."]})
|
| 489 |
-
|
| 490 |
-
df_fw = df_fw[[b_sec, c_sec]].rename(columns={b_sec: 'Base_section', c_sec: 'Comp_section'})
|
| 491 |
-
if not df_rv.empty:
|
| 492 |
-
df_rv = df_rv[[b_sec, c_sec]].rename(columns={b_sec: 'Comp_section', c_sec: 'Base_section'})
|
| 493 |
-
else:
|
| 494 |
-
df_rv = pd.DataFrame(columns=['Base_section', 'Comp_section'])
|
| 495 |
|
| 496 |
-
|
| 497 |
|
| 498 |
-
|
| 499 |
-
df_mapping['Base_section'] = df_mapping['Base_section'].astype(str).str.replace('\n', ',').str.split(',')
|
| 500 |
-
df_mapping['Comp_section'] = df_mapping['Comp_section'].astype(str).str.replace('\n', ',').str.split(',')
|
| 501 |
-
df_mapping = df_mapping.explode('Base_section').explode('Comp_section')
|
| 502 |
-
|
| 503 |
-
df_mapping['Base_section'] = df_mapping['Base_section'].apply(clean_key_val)
|
| 504 |
-
df_mapping['Comp_section'] = df_mapping['Comp_section'].apply(clean_key_val)
|
| 505 |
-
|
| 506 |
-
df_mapping = df_mapping[(df_mapping['Base_section'] != "") & (df_mapping['Comp_section'] != "")]
|
| 507 |
-
bridge = df_mapping.dropna().drop_duplicates()
|
| 508 |
-
else:
|
| 509 |
-
bridge = pd.DataFrame(columns=['Base_section', 'Comp_section'])
|
| 510 |
|
| 511 |
-
# 💡 [요청하신 딱 그 부분 수정!]
|
| 512 |
-
# 1순위: 장부에 적힌 매핑(bridge)을 그대로 씁니다.
|
| 513 |
-
# 2순위: 장부가 비어있고 양쪽 법규/카테고리가 완벽히 같을 때만 양쪽 조항 전체(합집합)를 1:1로 묶습니다.
|
| 514 |
-
if bridge.empty and base_std.strip().upper() == comp_std.strip().upper() and type_b.strip().upper() == type_c.strip().upper():
|
| 515 |
-
all_keys = list(set(df_base['merge_key']).union(set(df_comp['merge_key'])))
|
| 516 |
-
bridge = pd.DataFrame({'Base_section': all_keys, 'Comp_section': all_keys})
|
| 517 |
|
| 518 |
-
rename_b = {c: f"{c}_{base_ver}" for c in df_base.columns if c != 'merge_key'}
|
| 519 |
-
df_base = df_base.rename(columns=rename_b)
|
| 520 |
-
|
| 521 |
-
rename_c = {c: f"{c}_{comp_ver}" for c in df_comp.columns if c != 'merge_key'}
|
| 522 |
-
df_comp = df_comp.rename(columns=rename_c)
|
| 523 |
|
| 524 |
df_base['base_idx'], df_comp['comp_idx'] = range(len(df_base)), range(len(df_comp))
|
|
|
|
| 525 |
merged = pd.merge(bridge, df_base, left_on='Base_section', right_on='merge_key', how='outer')
|
|
|
|
| 526 |
merged = pd.merge(merged, df_comp, left_on='Comp_section', right_on='merge_key', how='outer')
|
|
|
|
| 527 |
|
|
|
|
| 528 |
merged['base_idx'] = merged['base_idx'].fillna(float('inf'))
|
|
|
|
| 529 |
merged['comp_idx'] = merged['comp_idx'].fillna(float('inf'))
|
|
|
|
| 530 |
merged = merged.sort_values(['base_idx', 'comp_idx'])
|
| 531 |
|
|
|
|
|
|
|
| 532 |
result_rows = []
|
|
|
|
| 533 |
for _, row in merged.iterrows():
|
|
|
|
| 534 |
row_dict = {}
|
|
|
|
| 535 |
has_b = not pd.isna(row.get('base_idx')) and row.get('base_idx') != float('inf')
|
|
|
|
| 536 |
has_c = not pd.isna(row.get('comp_idx')) and row.get('comp_idx') != float('inf')
|
|
|
|
| 537 |
|
| 538 |
-
|
| 539 |
-
for c in
|
|
|
|
|
|
|
|
|
|
| 540 |
|
|
|
|
| 541 |
if mapped_only:
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
|
|
|
|
|
|
| 545 |
if not (b_has_val and c_has_val):
|
|
|
|
| 546 |
continue
|
|
|
|
| 547 |
|
|
|
|
| 548 |
if has_b and has_c:
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
if b_v and c_v and "<img" not in b_v and "<img" not in c_v and b_v != c_v:
|
| 553 |
-
|
|
|
|
|
|
|
| 554 |
result_rows.append(row_dict)
|
| 555 |
|
|
|
|
|
|
|
| 556 |
final_df = combine_code_desc(pd.DataFrame(result_rows))
|
| 557 |
|
|
|
|
|
|
|
| 558 |
if final_df.empty:
|
|
|
|
| 559 |
return pd.DataFrame({"Info": ["💡 조건에 맞는 데이터가 없습니다."]})
|
| 560 |
|
|
|
|
|
|
|
| 561 |
if diff_only:
|
|
|
|
| 562 |
mask = final_df.astype(str).apply(lambda col: col.str.contains('color:#ff4d4f|color:#2ecc71', case=False, regex=True)).any(axis=1)
|
|
|
|
| 563 |
final_df = final_df[mask]
|
|
|
|
| 564 |
if final_df.empty:
|
|
|
|
| 565 |
return pd.DataFrame({"Info": ["💡 선택하신 조건 간에 변경된 내용이 없습니다. (100% 동일)"]})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
b_cols_final = [c for c in final_df.columns if c.endswith(f"_{base_ver}")]
|
|
|
|
| 568 |
c_cols_final = [c for c in final_df.columns if c.endswith(f"_{comp_ver}")]
|
| 569 |
|
|
|
|
|
|
|
| 570 |
final_df = apply_visual_merge(final_df, b_cols_final)
|
|
|
|
| 571 |
final_df = apply_visual_merge(final_df, c_cols_final)
|
| 572 |
|
|
|
|
|
|
|
| 573 |
return final_df
|
| 574 |
|
|
|
|
|
|
|
| 575 |
return pd.DataFrame({"Info": ["조건을 선택하세요."]})
|
|
|
|
| 576 |
except Exception as e:
|
|
|
|
| 577 |
error_msg = str(e)
|
|
|
|
| 578 |
if "database is locked" in error_msg.lower():
|
|
|
|
| 579 |
return pd.DataFrame({"Error": ["🚨 DB가 잠겨있습니다! 켜놓으신 'DB Browser' 프로그램을 완전히 종료한 뒤 다시 조회해 주세요."]})
|
|
|
|
| 580 |
return pd.DataFrame({"Error": [f"시스템 오류 발생: {error_msg}"]})
|
| 581 |
|
|
|
|
|
|
|
| 582 |
# ==========================================
|
|
|
|
| 583 |
# 4. UI Layout & Event Binding
|
|
|
|
| 584 |
# ==========================================
|
|
|
|
| 585 |
with gr.Blocks() as demo:
|
|
|
|
| 586 |
gr.Markdown("# 📜 Regulation Viewer")
|
| 587 |
|
|
|
|
|
|
|
| 588 |
with gr.Row():
|
|
|
|
| 589 |
with gr.Accordion("📌 기준 법규", open=True):
|
|
|
|
| 590 |
with gr.Column():
|
|
|
|
| 591 |
base_standard = gr.Dropdown(label="Standard")
|
|
|
|
| 592 |
base_version = gr.Dropdown(label="Version")
|
|
|
|
| 593 |
base_status = gr.Textbox(label="Status", interactive=False, lines=1)
|
|
|
|
| 594 |
|
|
|
|
| 595 |
with gr.Row(elem_classes="reset-row"):
|
|
|
|
| 596 |
base_category = gr.Dropdown(label="Category", scale=4)
|
|
|
|
| 597 |
base_reset_btn = gr.Button("↺ 초기화", scale=1, elem_classes="reset-btn")
|
|
|
|
| 598 |
|
|
|
|
| 599 |
with gr.Accordion("🔄 비교 법규", open=False):
|
|
|
|
| 600 |
with gr.Column():
|
|
|
|
| 601 |
comp_standard = gr.Dropdown(label="Standard", choices=[])
|
|
|
|
| 602 |
comp_version = gr.Dropdown(label="Version", choices=[])
|
|
|
|
| 603 |
comp_status = gr.Textbox(label="Status", interactive=False, lines=1)
|
|
|
|
| 604 |
|
|
|
|
| 605 |
with gr.Row(elem_classes="reset-row"):
|
|
|
|
| 606 |
comp_category = gr.Dropdown(label="Category", choices=[], scale=4)
|
|
|
|
| 607 |
comp_reset_btn = gr.Button("↺ 초기화", scale=1, elem_classes="reset-btn")
|
| 608 |
|
|
|
|
|
|
|
| 609 |
with gr.Row(elem_id="search_row"):
|
|
|
|
| 610 |
search_btn = gr.Button("🔍 조회", variant="primary", scale=10)
|
|
|
|
| 611 |
mapped_only_cb = gr.Checkbox(label="🔗 매핑된 항목만 보기", value=False, elem_id="mapped_cb_item", container=False, scale=1)
|
|
|
|
| 612 |
diff_filter_cb = gr.Checkbox(label="💡 변경된 내용만 보기", value=False, elem_id="diff_cb_item", container=False, scale=1)
|
| 613 |
|
|
|
|
|
|
|
| 614 |
output_df = gr.Dataframe(wrap=True, interactive=False, datatype="html", max_height=800)
|
| 615 |
|
|
|
|
|
|
|
| 616 |
demo.load(fn=load_initial_standards, inputs=None, outputs=base_standard)
|
| 617 |
|
|
|
|
|
|
|
| 618 |
base_standard.change(fn=update_version_dropdown, inputs=[base_standard], outputs=[base_version])
|
|
|
|
| 619 |
base_version.change(fn=update_base_category_dropdown, inputs=[base_standard, base_version], outputs=[base_category, base_status])
|
| 620 |
|
|
|
|
|
|
|
| 621 |
base_version.change(fn=update_comp_standard_dropdown, inputs=[base_standard, base_version], outputs=[comp_standard])
|
|
|
|
| 622 |
comp_standard.change(fn=update_comp_version_dropdown, inputs=[base_standard, base_version, comp_standard], outputs=[comp_version])
|
| 623 |
|
|
|
|
|
|
|
| 624 |
comp_change_triggers = [base_standard, base_version, base_category, comp_standard, comp_version]
|
|
|
|
| 625 |
|
|
|
|
| 626 |
base_category.change(fn=update_comp_category_dropdown, inputs=comp_change_triggers, outputs=[comp_category, comp_status])
|
|
|
|
| 627 |
comp_version.change(fn=update_comp_category_dropdown, inputs=comp_change_triggers, outputs=[comp_category, comp_status])
|
| 628 |
|
|
|
|
|
|
|
| 629 |
search_btn.click(
|
|
|
|
| 630 |
fn=execute_unified_search,
|
|
|
|
| 631 |
inputs=[base_standard, base_version, base_category, comp_standard, comp_version, comp_category, mapped_only_cb, diff_filter_cb],
|
|
|
|
| 632 |
outputs=[output_df]
|
|
|
|
| 633 |
)
|
|
|
|
| 634 |
|
|
|
|
| 635 |
mapped_only_cb.change(
|
|
|
|
| 636 |
fn=execute_unified_search,
|
|
|
|
| 637 |
inputs=[base_standard, base_version, base_category, comp_standard, comp_version, comp_category, mapped_only_cb, diff_filter_cb],
|
|
|
|
| 638 |
outputs=[output_df]
|
|
|
|
| 639 |
)
|
| 640 |
|
|
|
|
|
|
|
| 641 |
diff_filter_cb.change(
|
|
|
|
| 642 |
fn=execute_unified_search,
|
|
|
|
| 643 |
inputs=[base_standard, base_version, base_category, comp_standard, comp_version, comp_category, mapped_only_cb, diff_filter_cb],
|
|
|
|
| 644 |
outputs=[output_df]
|
|
|
|
| 645 |
)
|
| 646 |
|
|
|
|
|
|
|
| 647 |
base_reset_btn.click(fn=reset_base_selections, inputs=None, outputs=[base_standard, base_version, base_category, base_status])
|
|
|
|
| 648 |
comp_reset_btn.click(fn=reset_comp_selections, inputs=None, outputs=[comp_standard, comp_version, comp_category, comp_status])
|
| 649 |
|
|
|
|
|
|
|
| 650 |
# ==========================================
|
|
|
|
| 651 |
# 5. Application Styling (CSS)
|
|
|
|
| 652 |
# ==========================================
|
|
|
|
| 653 |
css = """
|
|
|
|
| 654 |
.reset-row {
|
|
|
|
| 655 |
align-items: flex-end !important;
|
|
|
|
| 656 |
margin-bottom: 5px !important;
|
|
|
|
| 657 |
}
|
|
|
|
| 658 |
.reset-btn {
|
|
|
|
| 659 |
margin-bottom: 10px !important;
|
|
|
|
| 660 |
}
|
|
|
|
| 661 |
table {
|
|
|
|
| 662 |
table-layout: auto !important;
|
|
|
|
| 663 |
width: max-content !important;
|
|
|
|
| 664 |
min-width: 100% !important;
|
|
|
|
| 665 |
}
|
|
|
|
| 666 |
th, td {
|
|
|
|
| 667 |
min-width: 150px;
|
|
|
|
| 668 |
}
|
|
|
|
| 669 |
table:has(th:nth-last-child(2):first-child),
|
|
|
|
| 670 |
table:has(th:nth-last-child(3):first-child),
|
|
|
|
| 671 |
table:has(th:nth-last-child(4):first-child),
|
|
|
|
| 672 |
table:has(th:nth-last-child(5):first-child),
|
|
|
|
| 673 |
table:has(th:nth-last-child(6):first-child) {
|
|
|
|
| 674 |
table-layout: fixed !important;
|
|
|
|
| 675 |
width: 100% !important;
|
|
|
|
| 676 |
}
|
|
|
|
| 677 |
table th:nth-last-child(2):first-child, table td:nth-last-child(2):first-child { width: 15% !important; min-width: 0 !important; }
|
|
|
|
| 678 |
table th:nth-last-child(1), table td:nth-last-child(1) { width: 85% !important; min-width: 0 !important; }
|
| 679 |
|
|
|
|
|
|
|
| 680 |
table th:nth-child(1):nth-last-child(3), table td:nth-child(1):nth-last-child(3) { width: 20% !important; min-width: 0 !important; }
|
|
|
|
| 681 |
table th:nth-child(2):nth-last-child(2), table td:nth-child(2):nth-last-child(2) { width: 30% !important; min-width: 0 !important; }
|
|
|
|
| 682 |
table th:nth-child(3):nth-last-child(1), table td:nth-child(3):nth-last-child(1) { width: 50% !important; min-width: 0 !important; }
|
| 683 |
|
|
|
|
|
|
|
| 684 |
table th:nth-child(1):nth-last-child(4), table td:nth-child(1):nth-last-child(4) { width: 10% !important; min-width: 0 !important; }
|
|
|
|
| 685 |
table th:nth-child(2):nth-last-child(3), table td:nth-child(2):nth-last-child(3) { width: 40% !important; min-width: 0 !important; }
|
|
|
|
| 686 |
table th:nth-child(3):nth-last-child(2), table td:nth-child(3):nth-last-child(2) { width: 10% !important; min-width: 0 !important; }
|
|
|
|
| 687 |
table th:nth-child(4):nth-last-child(1), table td:nth-child(4):nth-last-child(1) { width: 40% !important; min-width: 0 !important; }
|
| 688 |
|
|
|
|
|
|
|
| 689 |
table th:nth-child(1):nth-last-child(5), table td:nth-child(1):nth-last-child(5) { width: 8% !important; min-width: 0 !important; }
|
|
|
|
| 690 |
table th:nth-child(2):nth-last-child(4), table td:nth-child(2):nth-last-child(4) { width: 8% !important; min-width: 0 !important; }
|
|
|
|
| 691 |
table th:nth-child(3):nth-last-child(3), table td:nth-child(3):nth-last-child(3) { width: 8% !important; min-width: 0 !important; }
|
|
|
|
| 692 |
table th:nth-child(4):nth-last-child(2), table td:nth-child(4):nth-last-child(2) { width: 8% !important; min-width: 0 !important; }
|
|
|
|
| 693 |
table th:nth-child(5):nth-last-child(1), table td:nth-child(5):nth-last-child(1) { width: 68% !important; min-width: 0 !important; }
|
| 694 |
|
|
|
|
|
|
|
| 695 |
table th:nth-child(1):nth-last-child(6), table td:nth-child(1):nth-last-child(6) { width: 8% !important; min-width: 0 !important; }
|
|
|
|
| 696 |
table th:nth-child(2):nth-last-child(5), table td:nth-child(2):nth-last-child(5) { width: 15% !important; min-width: 0 !important; }
|
|
|
|
| 697 |
table th:nth-child(3):nth-last-child(4), table td:nth-child(3):nth-last-child(4) { width: 27% !important; min-width: 0 !important; }
|
|
|
|
| 698 |
table th:nth-child(4):nth-last-child(3), table td:nth-child(4):nth-last-child(3) { width: 8% !important; min-width: 0 !important; }
|
|
|
|
| 699 |
table th:nth-child(5):nth-last-child(2), table td:nth-child(5):nth-last-child(2) { width: 15% !important; min-width: 0 !important; }
|
|
|
|
| 700 |
table th:nth-child(6):nth-last-child(1), table td:nth-child(6):nth-last-child(1) { width: 27% !important; min-width: 0 !important; }
|
| 701 |
|
|
|
|
|
|
|
| 702 |
thead th {
|
|
|
|
| 703 |
font-size: 18px !important;
|
|
|
|
| 704 |
position: sticky;
|
|
|
|
| 705 |
top: 0;
|
|
|
|
| 706 |
background: white;
|
|
|
|
| 707 |
z-index: 10;
|
|
|
|
| 708 |
}
|
|
|
|
| 709 |
.dataframe {
|
|
|
|
| 710 |
max-height: none !important;
|
|
|
|
| 711 |
overflow-y: visible !important;
|
|
|
|
| 712 |
overflow-x: auto !important;
|
|
|
|
| 713 |
display: block;
|
|
|
|
| 714 |
}
|
|
|
|
| 715 |
.dataframe > div {
|
|
|
|
| 716 |
max-height: none !important;
|
|
|
|
| 717 |
overflow: visible !important;
|
|
|
|
| 718 |
}
|
|
|
|
| 719 |
td {
|
|
|
|
| 720 |
font-size: 18px !important;
|
|
|
|
| 721 |
white-space: pre-wrap !important;
|
|
|
|
| 722 |
word-break: keep-all !important;
|
|
|
|
| 723 |
line-height: 1.6;
|
|
|
|
| 724 |
padding: 10px;
|
|
|
|
| 725 |
vertical-align: top !important;
|
|
|
|
| 726 |
text-align: left !important;
|
|
|
|
| 727 |
}
|
|
|
|
| 728 |
td img {
|
|
|
|
| 729 |
display: block;
|
|
|
|
| 730 |
max-width: none !important;
|
|
|
|
| 731 |
}
|
|
|
|
| 732 |
#search_row {
|
|
|
|
| 733 |
align-items: center !important;
|
|
|
|
| 734 |
margin-bottom: 5px !important;
|
|
|
|
| 735 |
}
|
|
|
|
| 736 |
#diff_cb_item, #mapped_cb_item {
|
|
|
|
| 737 |
margin-top: 0 !important;
|
|
|
|
| 738 |
padding-left: 15px !important;
|
|
|
|
| 739 |
width: max-content !important;
|
|
|
|
| 740 |
min-width: max-content !important;
|
|
|
|
| 741 |
flex-grow: 0 !important;
|
|
|
|
| 742 |
}
|
|
|
|
| 743 |
"""
|
| 744 |
|
|
|
|
|
|
|
| 745 |
if __name__ == "__main__":
|
|
|
|
| 746 |
demo.launch(
|
|
|
|
| 747 |
theme=gr.themes.Soft(),
|
|
|
|
| 748 |
share=True,
|
|
|
|
| 749 |
css=css
|
|
|
|
| 750 |
)
|
|
|
|
| 6 |
import base64
|
| 7 |
import difflib
|
| 8 |
|
| 9 |
+
|
| 10 |
# ==========================================
|
| 11 |
# 1. Environment Setup & Data Helpers
|
| 12 |
# ==========================================
|
| 13 |
+
|
| 14 |
UPLOAD_DIR = "uploaded_dbs"
|
| 15 |
+
|
| 16 |
if not os.path.exists(UPLOAD_DIR):
|
| 17 |
+
|
| 18 |
os.makedirs(UPLOAD_DIR)
|
| 19 |
|
| 20 |
+
|
| 21 |
+
|
| 22 |
db_registry = []
|
| 23 |
|
| 24 |
+
|
| 25 |
+
|
| 26 |
def convert_blob_to_html_img(blob_data):
|
| 27 |
+
|
| 28 |
if blob_data is None or pd.isna(blob_data):
|
| 29 |
+
|
| 30 |
return ""
|
| 31 |
+
|
| 32 |
try:
|
| 33 |
+
|
| 34 |
if isinstance(blob_data, (bytes, bytearray)):
|
| 35 |
+
|
| 36 |
encoded = base64.b64encode(blob_data).decode('utf-8')
|
| 37 |
+
|
| 38 |
return f'''
|
| 39 |
+
|
| 40 |
<img src="data:image/png;base64,{encoded}"
|
| 41 |
+
|
| 42 |
style="width: 40%;
|
| 43 |
+
|
| 44 |
max-height: 300px;
|
| 45 |
+
|
| 46 |
object-fit: contain;
|
| 47 |
+
|
| 48 |
display: block;
|
| 49 |
+
|
| 50 |
margin: 10px 0;">
|
| 51 |
+
|
| 52 |
'''
|
| 53 |
+
|
| 54 |
return str(blob_data)
|
| 55 |
+
|
| 56 |
except Exception:
|
| 57 |
+
|
| 58 |
return str(blob_data)
|
| 59 |
|
| 60 |
+
|
| 61 |
+
|
| 62 |
def decode_sqlite_text(x):
|
| 63 |
+
|
| 64 |
try:
|
| 65 |
+
|
| 66 |
return x.decode('utf-8')
|
| 67 |
+
|
| 68 |
except UnicodeDecodeError:
|
| 69 |
+
|
| 70 |
return x
|
| 71 |
|
| 72 |
+
|
| 73 |
+
|
| 74 |
def fetch_available_standards():
|
| 75 |
+
|
| 76 |
global db_registry
|
| 77 |
+
|
| 78 |
db_registry = []
|
| 79 |
+
|
| 80 |
for file_name in os.listdir(UPLOAD_DIR):
|
| 81 |
+
|
| 82 |
if file_name.endswith(".db"):
|
| 83 |
+
|
| 84 |
name = file_name.replace(".db", "")
|
| 85 |
+
|
| 86 |
parts = name.split("_")
|
| 87 |
+
|
| 88 |
if len(parts) >= 2:
|
| 89 |
+
|
| 90 |
db_registry.append({
|
| 91 |
+
|
| 92 |
"path": os.path.join(UPLOAD_DIR, file_name),
|
| 93 |
+
|
| 94 |
"standard": parts[0],
|
| 95 |
+
|
| 96 |
"version": parts[1]
|
| 97 |
+
|
| 98 |
})
|
| 99 |
+
|
| 100 |
return sorted(list(set([d["standard"] for d in db_registry])))
|
| 101 |
|
| 102 |
+
|
| 103 |
+
|
| 104 |
def fetch_database_records(std, ver, cat, table_type):
|
| 105 |
+
|
| 106 |
try:
|
| 107 |
+
|
| 108 |
db_path = os.path.join(UPLOAD_DIR, f"{std}_{ver}.db")
|
| 109 |
+
|
| 110 |
conn = sqlite3.connect(db_path)
|
| 111 |
+
|
| 112 |
conn.text_factory = decode_sqlite_text
|
| 113 |
+
|
| 114 |
|
| 115 |
+
|
| 116 |
tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist()
|
| 117 |
+
|
| 118 |
valid_tables = [t for t in tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']]
|
| 119 |
+
|
| 120 |
|
| 121 |
+
|
| 122 |
main_table = None
|
| 123 |
+
|
| 124 |
if table_type and table_type.upper() != "MAIN":
|
| 125 |
+
|
| 126 |
expected_name = f"{std}_{ver}_{table_type}"
|
| 127 |
+
|
| 128 |
for t in valid_tables:
|
| 129 |
+
|
| 130 |
if t.lower() == expected_name.lower() or t.lower() == table_type.lower():
|
| 131 |
+
|
| 132 |
main_table = t
|
| 133 |
+
|
| 134 |
break
|
| 135 |
+
|
| 136 |
|
| 137 |
+
|
| 138 |
if not main_table:
|
| 139 |
+
|
| 140 |
main_table = f"{std}_{ver}"
|
| 141 |
+
|
| 142 |
if main_table not in valid_tables:
|
| 143 |
+
|
| 144 |
main_table = valid_tables[0] if valid_tables else None
|
| 145 |
+
|
| 146 |
|
| 147 |
+
|
| 148 |
if not main_table:
|
| 149 |
+
|
| 150 |
conn.close()
|
| 151 |
+
|
| 152 |
return pd.DataFrame({"Error": ["데이터 테이블을 찾을 수 없습니다."]}), []
|
| 153 |
+
|
| 154 |
|
| 155 |
+
|
| 156 |
cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
|
| 157 |
+
|
| 158 |
lower_cols = [c.lower() for c in cols]
|
| 159 |
+
|
| 160 |
|
| 161 |
+
|
| 162 |
conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
| 163 |
+
|
| 164 |
config_df = pd.read_sql("SELECT * FROM Table_Config WHERE TRIM(Table_Type)=?", conn_map, params=[table_type.strip()])
|
| 165 |
+
|
| 166 |
conn_map.close()
|
| 167 |
+
|
| 168 |
|
| 169 |
+
|
| 170 |
if config_df.empty:
|
| 171 |
+
|
| 172 |
return pd.DataFrame({"Error": [f"Table_Config에서 '{table_type}' 설정을 찾을 수 없습니다."]}), []
|
| 173 |
+
|
| 174 |
|
| 175 |
+
|
| 176 |
matched_config = None
|
| 177 |
+
|
| 178 |
for _, row in config_df.iterrows():
|
| 179 |
+
|
| 180 |
anchors_test = [x.strip().lower() for x in str(row['Anchor_Column']).split(',')]
|
| 181 |
+
|
| 182 |
if any(a in lower_cols for a in anchors_test):
|
| 183 |
+
|
| 184 |
matched_config = row
|
| 185 |
+
|
| 186 |
break
|
| 187 |
+
|
| 188 |
|
| 189 |
+
|
| 190 |
if matched_config is None:
|
| 191 |
+
|
| 192 |
matched_config = config_df.iloc[0]
|
| 193 |
+
|
| 194 |
|
| 195 |
+
|
| 196 |
anchors = [x.strip() for x in matched_config['Anchor_Column'].split(',')]
|
| 197 |
+
|
| 198 |
displays = [x.strip() for x in matched_config['Display_Columns'].split(',')] if pd.notna(matched_config['Display_Columns']) else anchors
|
| 199 |
+
|
| 200 |
|
| 201 |
+
|
| 202 |
query = f"SELECT * FROM [{main_table}]"
|
| 203 |
+
|
| 204 |
conditions = []
|
| 205 |
+
|
| 206 |
|
| 207 |
+
|
| 208 |
if cat and cat != "ALL":
|
| 209 |
+
|
| 210 |
if "." in cat and 'chapter' in lower_cols and 'category' in lower_cols:
|
| 211 |
+
|
| 212 |
ch, ca = cat.split(".", 1)
|
| 213 |
+
|
| 214 |
ch_col = cols[lower_cols.index('chapter')]
|
| 215 |
+
|
| 216 |
ca_col = cols[lower_cols.index('category')]
|
| 217 |
+
|
| 218 |
conditions.append(f"[{ch_col}] = '{ch}' AND [{ca_col}] = '{ca}'")
|
| 219 |
+
|
| 220 |
elif 'category' in lower_cols:
|
| 221 |
+
|
| 222 |
ca_col = cols[lower_cols.index('category')]
|
| 223 |
+
|
| 224 |
conditions.append(f"[{ca_col}] = '{cat}'")
|
| 225 |
+
|
| 226 |
|
| 227 |
+
|
| 228 |
if conditions:
|
| 229 |
+
|
| 230 |
query += " WHERE " + " AND ".join(conditions)
|
| 231 |
+
|
| 232 |
|
| 233 |
+
|
| 234 |
df = pd.read_sql(query, conn)
|
| 235 |
+
|
| 236 |
conn.close()
|
| 237 |
+
|
| 238 |
|
| 239 |
+
|
| 240 |
real_anchors = [c for c in df.columns if any(a.lower() == c.lower() for a in anchors)]
|
| 241 |
+
|
| 242 |
|
| 243 |
+
|
| 244 |
final_cols = []
|
| 245 |
+
|
| 246 |
for d in displays:
|
| 247 |
+
|
| 248 |
for c in df.columns:
|
| 249 |
+
|
| 250 |
if d.lower() == c.lower():
|
| 251 |
+
|
| 252 |
if c not in final_cols:
|
| 253 |
+
|
| 254 |
final_cols.append(c)
|
| 255 |
+
|
| 256 |
break
|
| 257 |
+
|
| 258 |
|
| 259 |
+
|
| 260 |
for ra in real_anchors:
|
| 261 |
+
|
| 262 |
if ra not in final_cols:
|
| 263 |
+
|
| 264 |
final_cols.append(ra)
|
| 265 |
+
|
| 266 |
|
| 267 |
+
|
| 268 |
if not final_cols:
|
| 269 |
+
|
| 270 |
return df, real_anchors
|
| 271 |
+
|
| 272 |
|
| 273 |
+
|
| 274 |
for c in final_cols:
|
| 275 |
+
|
| 276 |
df[c] = df[c].apply(convert_blob_to_html_img)
|
| 277 |
+
|
| 278 |
|
| 279 |
+
|
| 280 |
return df[final_cols], real_anchors
|
| 281 |
+
|
| 282 |
|
| 283 |
+
|
| 284 |
except Exception as e:
|
| 285 |
+
|
| 286 |
import traceback
|
| 287 |
+
|
| 288 |
traceback.print_exc()
|
| 289 |
+
|
| 290 |
return pd.DataFrame({"Error": [f"데이터 로드 오류: {str(e)}"]}), []
|
| 291 |
|
| 292 |
+
|
| 293 |
+
|
| 294 |
def generate_html_diff(text1, text2):
|
| 295 |
+
|
| 296 |
try:
|
| 297 |
+
|
| 298 |
s1, s2 = str(text1), str(text2)
|
| 299 |
+
|
| 300 |
|
| 301 |
+
|
| 302 |
if len(s1) > 1000 or len(s2) > 1000:
|
| 303 |
+
|
| 304 |
return s1, s2
|
| 305 |
+
|
| 306 |
|
| 307 |
+
|
| 308 |
words1, words2 = s1.split(), s2.split()
|
| 309 |
+
|
| 310 |
if not words1 or not words2:
|
| 311 |
+
|
| 312 |
return s1, s2
|
| 313 |
+
|
| 314 |
|
| 315 |
+
|
| 316 |
common_words = set(words1) & set(words2)
|
| 317 |
+
|
| 318 |
if len(common_words) / min(len(words1), len(words2)) < 0.05:
|
| 319 |
+
|
| 320 |
return s1, s2
|
| 321 |
|
| 322 |
+
|
| 323 |
+
|
| 324 |
matcher = difflib.SequenceMatcher(None, words1, words2)
|
| 325 |
+
|
| 326 |
res1, res2 = [], []
|
| 327 |
+
|
| 328 |
|
| 329 |
+
|
| 330 |
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
| 331 |
+
|
| 332 |
if tag == 'replace':
|
| 333 |
+
|
| 334 |
res1.append(f"<span style='color:#ff4d4f; font-weight:bold;'>{' '.join(words1[i1:i2])}</span>")
|
| 335 |
+
|
| 336 |
res2.append(f"<span style='color:#2ecc71; font-weight:bold;'>{' '.join(words2[j1:j2])}</span>")
|
| 337 |
+
|
| 338 |
elif tag == 'delete':
|
| 339 |
+
|
| 340 |
res1.append(f"<span style='color:#ff4d4f; font-weight:bold;'>{' '.join(words1[i1:i2])}</span>")
|
| 341 |
+
|
| 342 |
elif tag == 'insert':
|
| 343 |
+
|
| 344 |
res2.append(f"<span style='color:#2ecc71; font-weight:bold;'>{' '.join(words2[j1:j2])}</span>")
|
| 345 |
+
|
| 346 |
elif tag == 'equal':
|
| 347 |
+
|
| 348 |
res1.append(' '.join(words1[i1:i2]))
|
| 349 |
+
|
| 350 |
res2.append(' '.join(words2[j1:j2]))
|
| 351 |
+
|
| 352 |
|
| 353 |
+
|
| 354 |
return " ".join(res1), " ".join(res2)
|
| 355 |
+
|
| 356 |
except Exception:
|
| 357 |
+
|
| 358 |
return text1, text2
|
| 359 |
|
| 360 |
+
|
| 361 |
+
|
| 362 |
# ==========================================
|
| 363 |
+
|
| 364 |
# 2. UI Component Handlers
|
| 365 |
+
|
| 366 |
# ==========================================
|
| 367 |
+
|
| 368 |
def load_initial_standards():
|
| 369 |
+
|
| 370 |
return gr.Dropdown(choices=fetch_available_standards())
|
| 371 |
|
| 372 |
+
|
| 373 |
+
|
| 374 |
def update_version_dropdown(standard):
|
| 375 |
+
|
| 376 |
if not standard:
|
| 377 |
+
|
| 378 |
return gr.Dropdown(choices=[])
|
| 379 |
+
|
| 380 |
versions = []
|
| 381 |
+
|
| 382 |
for file_name in os.listdir(UPLOAD_DIR):
|
| 383 |
+
|
| 384 |
if file_name.startswith(standard + "_") and file_name.endswith(".db"):
|
| 385 |
+
|
| 386 |
versions.append(file_name.replace(standard + "_", "").replace(".db", ""))
|
| 387 |
+
|
| 388 |
return gr.Dropdown(choices=sorted(list(set(versions))))
|
| 389 |
|
| 390 |
+
|
| 391 |
+
|
| 392 |
def update_base_category_dropdown(standard, version):
|
| 393 |
+
|
| 394 |
if not standard or not version:
|
| 395 |
+
|
| 396 |
return gr.update(choices=[], value=None, interactive=False), gr.update(value="")
|
| 397 |
|
| 398 |
+
|
| 399 |
+
|
| 400 |
choices = ["ALL"]
|
| 401 |
+
|
| 402 |
status_value = ""
|
| 403 |
+
|
| 404 |
db_path = os.path.join(UPLOAD_DIR, f"{standard}_{version}.db")
|
| 405 |
+
|
| 406 |
|
| 407 |
+
|
| 408 |
if not os.path.exists(db_path):
|
| 409 |
+
|
| 410 |
return gr.update(choices=choices), gr.update(value="")
|
| 411 |
|
| 412 |
+
|
| 413 |
+
|
| 414 |
try:
|
| 415 |
+
|
| 416 |
conn = sqlite3.connect(db_path)
|
| 417 |
+
|
| 418 |
tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)['name'].tolist()
|
| 419 |
+
|
| 420 |
valid_tables = [t for t in tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']]
|
| 421 |
+
|
| 422 |
main_table = f"{standard}_{version}"
|
| 423 |
+
|
| 424 |
if main_table not in valid_tables:
|
| 425 |
+
|
| 426 |
main_table = valid_tables[0] if valid_tables else None
|
| 427 |
|
| 428 |
+
|
| 429 |
+
|
| 430 |
if main_table:
|
| 431 |
+
|
| 432 |
try:
|
| 433 |
+
|
| 434 |
cols_check = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
|
| 435 |
+
|
| 436 |
if "Status" in cols_check or "status" in cols_check:
|
| 437 |
+
|
| 438 |
status_df = pd.read_sql(f"SELECT Status FROM [{main_table}] WHERE Status IS NOT NULL AND Status != '' LIMIT 1", conn)
|
| 439 |
+
|
| 440 |
if not status_df.empty:
|
| 441 |
+
|
| 442 |
status_value = str(status_df.iloc[0]['Status'])
|
| 443 |
+
|
| 444 |
except Exception:
|
| 445 |
+
|
| 446 |
pass
|
| 447 |
|
| 448 |
+
|
| 449 |
+
|
| 450 |
cols = pd.read_sql(f"PRAGMA table_info([{main_table}])", conn)['name'].tolist()
|
| 451 |
+
|
| 452 |
lower_cols = [c.lower() for c in cols]
|
| 453 |
+
|
| 454 |
if 'chapter' in lower_cols and 'category' in lower_cols:
|
| 455 |
+
|
| 456 |
ch_col = cols[lower_cols.index('chapter')]
|
| 457 |
+
|
| 458 |
ca_col = cols[lower_cols.index('category')]
|
| 459 |
+
|
| 460 |
|
| 461 |
+
|
| 462 |
df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{main_table}]", conn)
|
| 463 |
+
|
| 464 |
for _, row in df.iterrows():
|
| 465 |
+
|
| 466 |
ch = str(row[ch_col]).strip()
|
| 467 |
+
|
| 468 |
ca = str(row[ca_col]).strip()
|
| 469 |
+
|
| 470 |
if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']:
|
| 471 |
+
|
| 472 |
choices.append(f"{ch}.{ca}")
|
| 473 |
|
| 474 |
+
|
| 475 |
+
|
| 476 |
pattern = re.compile(f"^{standard}[_\\s-]*{version}[_\\s-]*", re.IGNORECASE)
|
| 477 |
+
|
| 478 |
for t in valid_tables:
|
| 479 |
+
|
| 480 |
if t == main_table: continue
|
| 481 |
+
|
| 482 |
short_name = pattern.sub("", t).strip(" _")
|
| 483 |
+
|
| 484 |
if short_name and short_name not in choices:
|
| 485 |
+
|
| 486 |
choices.append(short_name)
|
| 487 |
+
|
| 488 |
elif t not in choices:
|
| 489 |
+
|
| 490 |
choices.append(t)
|
| 491 |
|
| 492 |
+
|
| 493 |
+
|
| 494 |
conn.close()
|
| 495 |
+
|
| 496 |
except Exception:
|
| 497 |
+
|
| 498 |
pass
|
| 499 |
+
|
| 500 |
|
| 501 |
+
|
| 502 |
return gr.update(choices=choices, value=None, interactive=True), gr.update(value=status_value)
|
| 503 |
|
| 504 |
+
|
| 505 |
+
|
| 506 |
def update_comp_standard_dropdown(base_std, base_ver):
|
| 507 |
+
|
| 508 |
if not base_std or not base_ver:
|
| 509 |
+
|
| 510 |
return gr.Dropdown(choices=[], value=None, interactive=False)
|
| 511 |
|
| 512 |
+
|
| 513 |
+
|
| 514 |
try:
|
| 515 |
+
|
| 516 |
conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
| 517 |
+
|
| 518 |
query = "SELECT DISTINCT TRIM(Comp_std) AS Comp_std FROM Mapping_registry WHERE TRIM(Base_std)=TRIM(?) AND TRIM(Base_ver)=TRIM(?)"
|
| 519 |
+
|
| 520 |
df = pd.read_sql(query, conn, params=[base_std, base_ver])
|
| 521 |
+
|
| 522 |
conn.close()
|
| 523 |
|
| 524 |
+
|
| 525 |
+
|
| 526 |
mapped_stds = sorted(df['Comp_std'].dropna().unique().tolist()) if not df.empty else []
|
| 527 |
+
|
| 528 |
return gr.update(choices=mapped_stds, value=None, interactive=bool(mapped_stds))
|
| 529 |
+
|
| 530 |
except Exception:
|
| 531 |
+
|
| 532 |
return gr.update(choices=[], value=None, interactive=False)
|
| 533 |
|
| 534 |
+
|
| 535 |
+
|
| 536 |
def update_comp_version_dropdown(base_std, base_ver, comp_std):
|
| 537 |
+
|
| 538 |
if not all([base_std, base_ver, comp_std]):
|
| 539 |
+
|
| 540 |
return gr.update(choices=[], value=None, interactive=False)
|
| 541 |
|
| 542 |
+
|
| 543 |
+
|
| 544 |
try:
|
| 545 |
+
|
| 546 |
conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
| 547 |
+
|
| 548 |
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(?)"
|
| 549 |
+
|
| 550 |
df = pd.read_sql(query, conn, params=[base_std, base_ver, comp_std])
|
| 551 |
+
|
| 552 |
conn.close()
|
| 553 |
|
| 554 |
+
|
| 555 |
+
|
| 556 |
mapped_vers = sorted(df['Comp_ver'].dropna().unique().tolist()) if not df.empty else []
|
| 557 |
+
|
| 558 |
return gr.update(choices=mapped_vers, value=None, interactive=bool(mapped_vers))
|
| 559 |
+
|
| 560 |
except Exception:
|
| 561 |
+
|
| 562 |
return gr.update(choices=[], value=None, interactive=False)
|
| 563 |
|
| 564 |
+
|
| 565 |
+
|
| 566 |
def update_comp_category_dropdown(base_std, base_ver, base_cat, comp_std, comp_ver):
|
| 567 |
+
|
| 568 |
if not all([base_std, base_ver, base_cat, comp_std, comp_ver]):
|
| 569 |
+
|
| 570 |
return gr.update(choices=[], value=None, interactive=False), gr.update(value="")
|
| 571 |
|
| 572 |
+
|
| 573 |
+
|
| 574 |
base_type = "Main" if not base_cat or base_cat == "ALL" or "." in base_cat else base_cat
|
| 575 |
+
|
| 576 |
status_value = ""
|
| 577 |
+
|
| 578 |
comp_db_path = os.path.join(UPLOAD_DIR, f"{comp_std}_{comp_ver}.db")
|
| 579 |
+
|
| 580 |
c_main_table = None
|
| 581 |
|
| 582 |
+
|
| 583 |
+
|
| 584 |
try:
|
| 585 |
+
|
| 586 |
if os.path.exists(comp_db_path):
|
| 587 |
+
|
| 588 |
c_conn = sqlite3.connect(comp_db_path)
|
| 589 |
+
|
| 590 |
c_tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", c_conn)['name'].tolist()
|
| 591 |
+
|
| 592 |
c_valid_tables = [t for t in c_tables if t.lower() not in ['mapping_registry', 'mapping_table', 'table_config', 'sqlite_sequence']]
|
| 593 |
+
|
| 594 |
c_main_table = f"{comp_std}_{comp_ver}"
|
| 595 |
+
|
| 596 |
if c_main_table not in c_valid_tables:
|
| 597 |
+
|
| 598 |
c_main_table = c_valid_tables[0] if c_valid_tables else None
|
| 599 |
+
|
| 600 |
|
| 601 |
+
|
| 602 |
if c_main_table:
|
| 603 |
+
|
| 604 |
try:
|
| 605 |
+
|
| 606 |
cols_check = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist()
|
| 607 |
+
|
| 608 |
if "Status" in cols_check or "status" in cols_check:
|
| 609 |
+
|
| 610 |
status_df = pd.read_sql(f"SELECT Status FROM [{c_main_table}] WHERE Status IS NOT NULL AND Status != '' LIMIT 1", c_conn)
|
| 611 |
+
|
| 612 |
if not status_df.empty:
|
| 613 |
+
|
| 614 |
status_value = str(status_df.iloc[0]['Status'])
|
| 615 |
+
|
| 616 |
except Exception:
|
| 617 |
+
|
| 618 |
pass
|
| 619 |
+
|
| 620 |
c_conn.close()
|
| 621 |
|
| 622 |
+
|
| 623 |
+
|
| 624 |
conn = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
| 625 |
+
|
| 626 |
query = """
|
| 627 |
+
|
| 628 |
SELECT DISTINCT TRIM(Comp_Type) AS Comp_Type
|
| 629 |
+
|
| 630 |
FROM Mapping_registry
|
| 631 |
+
|
| 632 |
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(?)
|
| 633 |
+
|
| 634 |
"""
|
| 635 |
+
|
| 636 |
df = pd.read_sql(query, conn, params=[base_std, base_ver, base_type, comp_std, comp_ver])
|
| 637 |
+
|
| 638 |
conn.close()
|
| 639 |
|
| 640 |
+
|
| 641 |
+
|
| 642 |
allowed_types = df['Comp_Type'].dropna().tolist()
|
| 643 |
+
|
| 644 |
if not allowed_types:
|
| 645 |
+
|
| 646 |
return gr.update(choices=[], value=None), gr.update(value=status_value)
|
| 647 |
|
| 648 |
+
|
| 649 |
+
|
| 650 |
final_choices = []
|
| 651 |
+
|
| 652 |
if "Main" in allowed_types:
|
| 653 |
+
|
| 654 |
final_choices.append("ALL")
|
| 655 |
+
|
| 656 |
if os.path.exists(comp_db_path) and c_main_table:
|
| 657 |
+
|
| 658 |
c_conn = sqlite3.connect(comp_db_path)
|
| 659 |
+
|
| 660 |
cols = pd.read_sql(f"PRAGMA table_info([{c_main_table}])", c_conn)['name'].tolist()
|
| 661 |
+
|
| 662 |
lower_cols = [c.lower() for c in cols]
|
| 663 |
+
|
| 664 |
if 'chapter' in lower_cols and 'category' in lower_cols:
|
| 665 |
+
|
| 666 |
ch_col = cols[lower_cols.index('chapter')]
|
| 667 |
+
|
| 668 |
ca_col = cols[lower_cols.index('category')]
|
| 669 |
+
|
| 670 |
c_df = pd.read_sql(f"SELECT DISTINCT [{ch_col}], [{ca_col}] FROM [{c_main_table}]", c_conn)
|
| 671 |
+
|
| 672 |
for _, row in c_df.iterrows():
|
| 673 |
+
|
| 674 |
ch = str(row[ch_col]).strip()
|
| 675 |
+
|
| 676 |
ca = str(row[ca_col]).strip()
|
| 677 |
+
|
| 678 |
if ch and ca and ch.lower() not in ['none', 'nan'] and ca.lower() not in ['none', 'nan']:
|
| 679 |
+
|
| 680 |
final_choices.append(f"{ch}.{ca}")
|
| 681 |
+
|
| 682 |
c_conn.close()
|
| 683 |
+
|
| 684 |
|
| 685 |
+
|
| 686 |
for t in allowed_types:
|
| 687 |
+
|
| 688 |
if t != "Main": final_choices.append(t)
|
| 689 |
|
| 690 |
+
|
| 691 |
+
|
| 692 |
return gr.update(choices=final_choices, value=final_choices[0] if final_choices else None, interactive=True), gr.update(value=status_value)
|
| 693 |
+
|
| 694 |
|
| 695 |
+
|
| 696 |
except Exception:
|
| 697 |
+
|
| 698 |
return gr.update(choices=[], value=None), gr.update(value="")
|
| 699 |
|
| 700 |
+
|
| 701 |
+
|
| 702 |
def reset_base_selections():
|
| 703 |
+
|
| 704 |
return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="")
|
| 705 |
|
| 706 |
+
|
| 707 |
+
|
| 708 |
def reset_comp_selections():
|
| 709 |
+
|
| 710 |
return gr.update(value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(value="")
|
| 711 |
|
| 712 |
+
|
| 713 |
+
|
| 714 |
# ==========================================
|
| 715 |
+
|
| 716 |
# 3. Core Search Logic
|
| 717 |
+
|
| 718 |
# ==========================================
|
| 719 |
+
|
| 720 |
def execute_unified_search(base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat, mapped_only, diff_only):
|
| 721 |
+
|
| 722 |
try:
|
| 723 |
+
|
| 724 |
def get_type_by_cat(cat):
|
| 725 |
+
|
| 726 |
if not cat or cat == "ALL": return "Main"
|
| 727 |
+
|
| 728 |
if "." in cat: return "Main"
|
| 729 |
+
|
| 730 |
return cat
|
| 731 |
|
| 732 |
+
|
| 733 |
+
|
| 734 |
type_b = get_type_by_cat(base_cat)
|
| 735 |
+
|
| 736 |
type_c = get_type_by_cat(comp_cat)
|
| 737 |
+
|
| 738 |
|
| 739 |
+
|
| 740 |
def apply_visual_merge(df, cols):
|
| 741 |
+
|
| 742 |
if not df.empty and len(cols) > 1:
|
| 743 |
+
|
| 744 |
is_dup = pd.Series([True] * len(df), index=df.index)
|
| 745 |
+
|
| 746 |
for col in cols:
|
| 747 |
+
|
| 748 |
if col in df.columns:
|
| 749 |
+
|
| 750 |
curr = df[col].astype(str).str.strip()
|
| 751 |
+
|
| 752 |
match = (curr == curr.shift(1)) & (~curr.isin(["", "nan", "None", " "]))
|
| 753 |
+
|
| 754 |
is_dup = is_dup & match
|
| 755 |
+
|
| 756 |
df.loc[is_dup, col] = " "
|
| 757 |
+
|
| 758 |
return df
|
| 759 |
|
| 760 |
+
|
| 761 |
+
|
| 762 |
def combine_code_desc(df):
|
| 763 |
+
|
| 764 |
cols = list(df.columns)
|
| 765 |
+
|
| 766 |
new_cols = []
|
| 767 |
+
|
| 768 |
processed = set()
|
| 769 |
+
|
| 770 |
for col in cols:
|
| 771 |
+
|
| 772 |
if col in processed: continue
|
| 773 |
+
|
| 774 |
if "_Code" in col:
|
| 775 |
+
|
| 776 |
desc_col = col.replace("_Code", "_Description")
|
| 777 |
+
|
| 778 |
if desc_col in cols:
|
| 779 |
+
|
| 780 |
new_col_name = col.replace("_Code", "")
|
| 781 |
+
|
| 782 |
def combine_cells(row):
|
| 783 |
+
|
| 784 |
c, d = str(row[col]).strip(), str(row[desc_col]).strip()
|
| 785 |
+
|
| 786 |
if c in ["nan", "None", "", " "]: return d
|
| 787 |
+
|
| 788 |
if d in ["nan", "None", "", " "]: return f"<span style='font-weight:bold; color:#1a73e8;'>{c}</span>"
|
| 789 |
+
|
| 790 |
return f"<span style='font-weight:bold; color:#1a73e8; display:block; margin-bottom:4px;'>{c}</span>{d}"
|
| 791 |
+
|
| 792 |
df[new_col_name] = df.apply(combine_cells, axis=1)
|
| 793 |
+
|
| 794 |
new_cols.append(new_col_name)
|
| 795 |
+
|
| 796 |
processed.update([col, desc_col])
|
| 797 |
+
|
| 798 |
else: new_cols.append(col)
|
| 799 |
+
|
| 800 |
elif "_Description" in col:
|
| 801 |
+
|
| 802 |
if col.replace("_Description", "_Code") not in cols: new_cols.append(col)
|
| 803 |
+
|
| 804 |
else: new_cols.append(col)
|
| 805 |
+
|
| 806 |
return df[new_cols]
|
| 807 |
|
| 808 |
+
|
| 809 |
+
|
| 810 |
if base_std and base_ver and base_cat and (not comp_std or not comp_ver or not comp_cat):
|
| 811 |
+
|
| 812 |
df, _ = fetch_database_records(base_std, base_ver, base_cat, type_b)
|
| 813 |
+
|
| 814 |
if "Error" in df.columns: return df
|
| 815 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 816 |
return apply_visual_merge(df, df.columns)
|
| 817 |
|
| 818 |
+
|
| 819 |
+
|
| 820 |
if all([base_std, base_ver, base_cat, comp_std, comp_ver, comp_cat]):
|
| 821 |
+
|
| 822 |
df_base, real_anchors_b = fetch_database_records(base_std, base_ver, base_cat, type_b)
|
| 823 |
+
|
| 824 |
df_comp, real_anchors_c = fetch_database_records(comp_std, comp_ver, comp_cat, type_c)
|
| 825 |
|
| 826 |
+
|
| 827 |
+
|
| 828 |
if "Error" in df_base.columns: return df_base
|
| 829 |
+
|
| 830 |
if "Error" in df_comp.columns: return df_comp
|
| 831 |
|
| 832 |
+
|
| 833 |
+
|
| 834 |
for ra in real_anchors_b:
|
| 835 |
+
|
| 836 |
if ra not in df_base.columns:
|
| 837 |
+
|
| 838 |
return pd.DataFrame({"Error": [f"기준 열(Anchor) '{ra}'이(가) 기준 데이터에 존재하지 않습니다. Table_Config를 확인하세요."]})
|
| 839 |
+
|
| 840 |
for ra in real_anchors_c:
|
| 841 |
+
|
| 842 |
if ra not in df_comp.columns:
|
| 843 |
+
|
| 844 |
return pd.DataFrame({"Error": [f"비교 열(Anchor) '{ra}'이(가) 비교 데이터에 존재하지 않습니다. Table_Config를 확인하세요."]})
|
| 845 |
|
| 846 |
+
|
| 847 |
+
|
| 848 |
def clean_key_val(v):
|
| 849 |
+
|
| 850 |
s = str(v).strip()
|
| 851 |
+
|
| 852 |
if s.endswith('.0') and s[:-2].isdigit():
|
| 853 |
+
|
| 854 |
s = s[:-2]
|
| 855 |
+
|
| 856 |
return s.replace(" ", "")
|
| 857 |
|
|
|
|
|
|
|
| 858 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 859 |
|
| 860 |
+
internal_rename_b = {c: f"{c}_INTERNAL_BASE" for c in df_base.columns if c != 'merge_key'}
|
| 861 |
+
|
| 862 |
+
internal_rename_c = {c: f"{c}_INTERNAL_COMP" for c in df_comp.columns if c != 'merge_key'}
|
| 863 |
+
|
| 864 |
+
|
| 865 |
+
|
| 866 |
+
df_base['merge_key'] = df_base[real_anchors_b].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
|
| 867 |
+
|
| 868 |
+
df_comp['merge_key'] = df_comp[real_anchors_c].apply(lambda row: '-'.join([clean_key_val(x) for x in row]), axis=1)
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
is_same_std = (base_std.strip().upper() == comp_std.strip().upper() and type_b.strip().upper() == type_c.strip().upper())
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
if is_same_std:
|
| 877 |
+
|
| 878 |
+
all_keys = list(set(df_base['merge_key']).union(set(df_comp['merge_key'])))
|
| 879 |
+
|
| 880 |
+
bridge = pd.DataFrame({'Base_section': all_keys, 'Comp_section': all_keys})
|
| 881 |
+
|
| 882 |
+
else:
|
| 883 |
+
|
| 884 |
+
conn_map = sqlite3.connect(os.path.join(UPLOAD_DIR, "mapping.db"))
|
| 885 |
+
|
| 886 |
+
registry_query = """
|
| 887 |
+
|
| 888 |
+
SELECT Target_Table FROM Mapping_registry
|
| 889 |
+
|
| 890 |
+
WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?
|
| 891 |
+
|
| 892 |
+
LIMIT 1
|
| 893 |
+
|
| 894 |
+
"""
|
| 895 |
+
|
| 896 |
+
reg_df = pd.read_sql(registry_query, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
|
| 900 |
+
target_table_name = "Mapping_table"
|
| 901 |
+
|
| 902 |
+
if not reg_df.empty and pd.notna(reg_df.iloc[0]['Target_Table']):
|
| 903 |
+
|
| 904 |
+
val = str(reg_df.iloc[0]['Target_Table']).strip()
|
| 905 |
+
|
| 906 |
+
if val and val.lower() not in ["none", "nan"]:
|
| 907 |
+
|
| 908 |
+
target_table_name = val
|
| 909 |
+
|
| 910 |
+
|
| 911 |
+
|
| 912 |
+
try:
|
| 913 |
+
|
| 914 |
+
q_fw = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Base_std)=? AND TRIM(Base_ver)=? AND TRIM(Comp_std)=? AND TRIM(Comp_ver)=?"
|
| 915 |
+
|
| 916 |
+
df_fw = pd.read_sql(q_fw, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
|
| 917 |
+
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
q_rv = f"SELECT * FROM [{target_table_name}] WHERE TRIM(Comp_std)=? AND TRIM(Comp_ver)=? AND TRIM(Base_std)=? AND TRIM(Base_ver)=?"
|
| 921 |
+
|
| 922 |
+
df_rv = pd.read_sql(q_rv, conn_map, params=[base_std.strip(), base_ver.strip(), comp_std.strip(), comp_ver.strip()])
|
| 923 |
+
|
| 924 |
+
except Exception as sql_e:
|
| 925 |
+
|
| 926 |
+
conn_map.close()
|
| 927 |
+
|
| 928 |
+
return pd.DataFrame({"Error": [f"매핑 '{target_table_name}'을 여는 데 실패했습니다. 테이블 이름을 확인하세요: {str(sql_e)}"]})
|
| 929 |
+
|
| 930 |
+
conn_map.close()
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
|
| 934 |
+
cols_fw_lower = {c.lower(): c for c in df_fw.columns}
|
| 935 |
+
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
if 'base_type' in cols_fw_lower and 'comp_type' in cols_fw_lower:
|
| 939 |
+
|
| 940 |
+
b_col = cols_fw_lower['base_type']
|
| 941 |
+
|
| 942 |
+
c_col = cols_fw_lower['comp_type']
|
| 943 |
+
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
df_fw[b_col] = df_fw[b_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 947 |
+
|
| 948 |
+
df_fw[c_col] = df_fw[c_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 949 |
+
|
| 950 |
+
df_fw = df_fw[(df_fw[b_col] == type_b.strip().upper()) & (df_fw[c_col] == type_c.strip().upper())]
|
| 951 |
+
|
| 952 |
+
|
| 953 |
+
|
| 954 |
+
if not df_rv.empty:
|
| 955 |
+
|
| 956 |
+
df_rv[b_col] = df_rv[b_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 957 |
+
|
| 958 |
+
df_rv[c_col] = df_rv[c_col].fillna('Main').astype(str).str.strip().str.upper()
|
| 959 |
+
|
| 960 |
+
df_rv = df_rv[(df_rv[c_col] == type_b.strip().upper()) & (df_rv[b_col] == type_c.strip().upper())]
|
| 961 |
+
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
b_sec = cols_fw_lower.get('base_section', 'Base_section')
|
| 965 |
+
|
| 966 |
+
c_sec = cols_fw_lower.get('comp_section', 'Comp_section')
|
| 967 |
+
|
| 968 |
+
|
| 969 |
+
|
| 970 |
+
if b_sec not in df_fw.columns or c_sec not in df_fw.columns:
|
| 971 |
+
|
| 972 |
+
return pd.DataFrame({"Error": [f"'{target_table_name}' 에 '{b_sec}' 또는 '{c_sec}' 열이 없습니다. 대소문자를 확인하세요."]})
|
| 973 |
+
|
| 974 |
+
|
| 975 |
+
|
| 976 |
+
df_fw = df_fw[[b_sec, c_sec]].rename(columns={b_sec: 'Base_section', c_sec: 'Comp_section'})
|
| 977 |
+
|
| 978 |
+
if not df_rv.empty:
|
| 979 |
+
|
| 980 |
+
df_rv = df_rv[[b_sec, c_sec]].rename(columns={b_sec: 'Comp_section', c_sec: 'Base_section'})
|
| 981 |
+
|
| 982 |
+
else:
|
| 983 |
+
|
| 984 |
+
df_rv = pd.DataFrame(columns=['Base_section', 'Comp_section'])
|
| 985 |
+
|
| 986 |
+
|
| 987 |
+
|
| 988 |
+
df_mapping = pd.concat([df_fw, df_rv], ignore_index=True)
|
| 989 |
+
|
| 990 |
+
|
| 991 |
+
|
| 992 |
+
if not df_mapping.empty:
|
| 993 |
+
|
| 994 |
+
df_mapping['Base_section'] = df_mapping['Base_section'].astype(str).str.replace('\n', ',').str.split(',')
|
| 995 |
+
|
| 996 |
+
df_mapping['Comp_section'] = df_mapping['Comp_section'].astype(str).str.replace('\n', ',').str.split(',')
|
| 997 |
+
|
| 998 |
+
df_mapping = df_mapping.explode('Base_section').explode('Comp_section')
|
| 999 |
+
|
| 1000 |
+
|
| 1001 |
+
|
| 1002 |
+
df_mapping['Base_section'] = df_mapping['Base_section'].apply(clean_key_val)
|
| 1003 |
+
|
| 1004 |
+
df_mapping['Comp_section'] = df_mapping['Comp_section'].apply(clean_key_val)
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
|
| 1008 |
+
df_mapping = df_mapping[(df_mapping['Base_section'] != "") & (df_mapping['Comp_section'] != "")]
|
| 1009 |
+
|
| 1010 |
+
bridge = df_mapping.dropna().drop_duplicates()
|
| 1011 |
+
|
| 1012 |
+
else:
|
| 1013 |
+
|
| 1014 |
+
bridge = pd.DataFrame(columns=['Base_section', 'Comp_section'])
|
| 1015 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
|
| 1018 |
+
df_base = df_base.rename(columns=internal_rename_b)
|
| 1019 |
|
| 1020 |
+
df_comp = df_comp.rename(columns=internal_rename_c)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
|
| 1024 |
df_base['base_idx'], df_comp['comp_idx'] = range(len(df_base)), range(len(df_comp))
|
| 1025 |
+
|
| 1026 |
merged = pd.merge(bridge, df_base, left_on='Base_section', right_on='merge_key', how='outer')
|
| 1027 |
+
|
| 1028 |
merged = pd.merge(merged, df_comp, left_on='Comp_section', right_on='merge_key', how='outer')
|
| 1029 |
+
|
| 1030 |
|
| 1031 |
+
|
| 1032 |
merged['base_idx'] = merged['base_idx'].fillna(float('inf'))
|
| 1033 |
+
|
| 1034 |
merged['comp_idx'] = merged['comp_idx'].fillna(float('inf'))
|
| 1035 |
+
|
| 1036 |
merged = merged.sort_values(['base_idx', 'comp_idx'])
|
| 1037 |
|
| 1038 |
+
|
| 1039 |
+
|
| 1040 |
result_rows = []
|
| 1041 |
+
|
| 1042 |
for _, row in merged.iterrows():
|
| 1043 |
+
|
| 1044 |
row_dict = {}
|
| 1045 |
+
|
| 1046 |
has_b = not pd.isna(row.get('base_idx')) and row.get('base_idx') != float('inf')
|
| 1047 |
+
|
| 1048 |
has_c = not pd.isna(row.get('comp_idx')) and row.get('comp_idx') != float('inf')
|
| 1049 |
+
|
| 1050 |
|
| 1051 |
+
|
| 1052 |
+
for c in internal_rename_b.values(): row_dict[c] = str(row[c]) if has_b and not pd.isna(row[c]) else ""
|
| 1053 |
+
|
| 1054 |
+
for c in internal_rename_c.values(): row_dict[c] = str(row[c]) if has_c and not pd.isna(row[c]) else ""
|
| 1055 |
+
|
| 1056 |
|
| 1057 |
+
|
| 1058 |
if mapped_only:
|
| 1059 |
+
|
| 1060 |
+
b_has_val = any(str(row_dict[c]).strip() not in ["", "nan", "None", " "] for c in internal_rename_b.values())
|
| 1061 |
+
|
| 1062 |
+
c_has_val = any(str(row_dict[c]).strip() not in ["", "nan", "None", " "] for c in internal_rename_c.values())
|
| 1063 |
+
|
| 1064 |
if not (b_has_val and c_has_val):
|
| 1065 |
+
|
| 1066 |
continue
|
| 1067 |
+
|
| 1068 |
|
| 1069 |
+
|
| 1070 |
if has_b and has_c:
|
| 1071 |
+
|
| 1072 |
+
for orig_col in internal_rename_b.keys():
|
| 1073 |
+
|
| 1074 |
+
if ('description' in orig_col.lower() or '내용' in orig_col) and internal_rename_c.get(orig_col) in row_dict:
|
| 1075 |
+
|
| 1076 |
+
b_v, c_v = row_dict[internal_rename_b[orig_col]], row_dict[internal_rename_c[orig_col]]
|
| 1077 |
+
|
| 1078 |
if b_v and c_v and "<img" not in b_v and "<img" not in c_v and b_v != c_v:
|
| 1079 |
+
|
| 1080 |
+
row_dict[internal_rename_b[orig_col]], row_dict[internal_rename_c[orig_col]] = generate_html_diff(b_v, c_v)
|
| 1081 |
+
|
| 1082 |
result_rows.append(row_dict)
|
| 1083 |
|
| 1084 |
+
|
| 1085 |
+
|
| 1086 |
final_df = combine_code_desc(pd.DataFrame(result_rows))
|
| 1087 |
|
| 1088 |
+
|
| 1089 |
+
|
| 1090 |
if final_df.empty:
|
| 1091 |
+
|
| 1092 |
return pd.DataFrame({"Info": ["💡 조건에 맞는 데이터가 없습니다."]})
|
| 1093 |
|
| 1094 |
+
|
| 1095 |
+
|
| 1096 |
if diff_only:
|
| 1097 |
+
|
| 1098 |
mask = final_df.astype(str).apply(lambda col: col.str.contains('color:#ff4d4f|color:#2ecc71', case=False, regex=True)).any(axis=1)
|
| 1099 |
+
|
| 1100 |
final_df = final_df[mask]
|
| 1101 |
+
|
| 1102 |
if final_df.empty:
|
| 1103 |
+
|
| 1104 |
return pd.DataFrame({"Info": ["💡 선택하신 조건 간에 변경된 내용이 없습니다. (100% 동일)"]})
|
| 1105 |
+
|
| 1106 |
+
|
| 1107 |
+
|
| 1108 |
+
final_rename_map = {}
|
| 1109 |
+
|
| 1110 |
+
for col in final_df.columns:
|
| 1111 |
+
|
| 1112 |
+
if col.endswith("_INTERNAL_BASE"):
|
| 1113 |
+
|
| 1114 |
+
final_rename_map[col] = f"{col.replace('_INTERNAL_BASE', '')}_{base_ver}"
|
| 1115 |
+
|
| 1116 |
+
elif col.endswith("_INTERNAL_COMP"):
|
| 1117 |
+
|
| 1118 |
+
final_rename_map[col] = f"{col.replace('_INTERNAL_COMP', '')}_{comp_ver}"
|
| 1119 |
+
|
| 1120 |
|
| 1121 |
+
|
| 1122 |
+
final_df = final_df.rename(columns=final_rename_map)
|
| 1123 |
+
|
| 1124 |
+
|
| 1125 |
+
|
| 1126 |
b_cols_final = [c for c in final_df.columns if c.endswith(f"_{base_ver}")]
|
| 1127 |
+
|
| 1128 |
c_cols_final = [c for c in final_df.columns if c.endswith(f"_{comp_ver}")]
|
| 1129 |
|
| 1130 |
+
|
| 1131 |
+
|
| 1132 |
final_df = apply_visual_merge(final_df, b_cols_final)
|
| 1133 |
+
|
| 1134 |
final_df = apply_visual_merge(final_df, c_cols_final)
|
| 1135 |
|
| 1136 |
+
|
| 1137 |
+
|
| 1138 |
return final_df
|
| 1139 |
|
| 1140 |
+
|
| 1141 |
+
|
| 1142 |
return pd.DataFrame({"Info": ["조건을 선택하세요."]})
|
| 1143 |
+
|
| 1144 |
except Exception as e:
|
| 1145 |
+
|
| 1146 |
error_msg = str(e)
|
| 1147 |
+
|
| 1148 |
if "database is locked" in error_msg.lower():
|
| 1149 |
+
|
| 1150 |
return pd.DataFrame({"Error": ["🚨 DB가 잠겨있습니다! 켜놓으신 'DB Browser' 프로그램을 완전히 종료한 뒤 다시 조회해 주세요."]})
|
| 1151 |
+
|
| 1152 |
return pd.DataFrame({"Error": [f"시스템 오류 발생: {error_msg}"]})
|
| 1153 |
|
| 1154 |
+
|
| 1155 |
+
|
| 1156 |
# ==========================================
|
| 1157 |
+
|
| 1158 |
# 4. UI Layout & Event Binding
|
| 1159 |
+
|
| 1160 |
# ==========================================
|
| 1161 |
+
|
| 1162 |
with gr.Blocks() as demo:
|
| 1163 |
+
|
| 1164 |
gr.Markdown("# 📜 Regulation Viewer")
|
| 1165 |
|
| 1166 |
+
|
| 1167 |
+
|
| 1168 |
with gr.Row():
|
| 1169 |
+
|
| 1170 |
with gr.Accordion("📌 기준 법규", open=True):
|
| 1171 |
+
|
| 1172 |
with gr.Column():
|
| 1173 |
+
|
| 1174 |
base_standard = gr.Dropdown(label="Standard")
|
| 1175 |
+
|
| 1176 |
base_version = gr.Dropdown(label="Version")
|
| 1177 |
+
|
| 1178 |
base_status = gr.Textbox(label="Status", interactive=False, lines=1)
|
| 1179 |
+
|
| 1180 |
|
| 1181 |
+
|
| 1182 |
with gr.Row(elem_classes="reset-row"):
|
| 1183 |
+
|
| 1184 |
base_category = gr.Dropdown(label="Category", scale=4)
|
| 1185 |
+
|
| 1186 |
base_reset_btn = gr.Button("↺ 초기화", scale=1, elem_classes="reset-btn")
|
| 1187 |
+
|
| 1188 |
|
| 1189 |
+
|
| 1190 |
with gr.Accordion("🔄 비교 법규", open=False):
|
| 1191 |
+
|
| 1192 |
with gr.Column():
|
| 1193 |
+
|
| 1194 |
comp_standard = gr.Dropdown(label="Standard", choices=[])
|
| 1195 |
+
|
| 1196 |
comp_version = gr.Dropdown(label="Version", choices=[])
|
| 1197 |
+
|
| 1198 |
comp_status = gr.Textbox(label="Status", interactive=False, lines=1)
|
| 1199 |
+
|
| 1200 |
|
| 1201 |
+
|
| 1202 |
with gr.Row(elem_classes="reset-row"):
|
| 1203 |
+
|
| 1204 |
comp_category = gr.Dropdown(label="Category", choices=[], scale=4)
|
| 1205 |
+
|
| 1206 |
comp_reset_btn = gr.Button("↺ 초기화", scale=1, elem_classes="reset-btn")
|
| 1207 |
|
| 1208 |
+
|
| 1209 |
+
|
| 1210 |
with gr.Row(elem_id="search_row"):
|
| 1211 |
+
|
| 1212 |
search_btn = gr.Button("🔍 조회", variant="primary", scale=10)
|
| 1213 |
+
|
| 1214 |
mapped_only_cb = gr.Checkbox(label="🔗 매핑된 항목만 보기", value=False, elem_id="mapped_cb_item", container=False, scale=1)
|
| 1215 |
+
|
| 1216 |
diff_filter_cb = gr.Checkbox(label="💡 변경된 내용만 보기", value=False, elem_id="diff_cb_item", container=False, scale=1)
|
| 1217 |
|
| 1218 |
+
|
| 1219 |
+
|
| 1220 |
output_df = gr.Dataframe(wrap=True, interactive=False, datatype="html", max_height=800)
|
| 1221 |
|
| 1222 |
+
|
| 1223 |
+
|
| 1224 |
demo.load(fn=load_initial_standards, inputs=None, outputs=base_standard)
|
| 1225 |
|
| 1226 |
+
|
| 1227 |
+
|
| 1228 |
base_standard.change(fn=update_version_dropdown, inputs=[base_standard], outputs=[base_version])
|
| 1229 |
+
|
| 1230 |
base_version.change(fn=update_base_category_dropdown, inputs=[base_standard, base_version], outputs=[base_category, base_status])
|
| 1231 |
|
| 1232 |
+
|
| 1233 |
+
|
| 1234 |
base_version.change(fn=update_comp_standard_dropdown, inputs=[base_standard, base_version], outputs=[comp_standard])
|
| 1235 |
+
|
| 1236 |
comp_standard.change(fn=update_comp_version_dropdown, inputs=[base_standard, base_version, comp_standard], outputs=[comp_version])
|
| 1237 |
|
| 1238 |
+
|
| 1239 |
+
|
| 1240 |
comp_change_triggers = [base_standard, base_version, base_category, comp_standard, comp_version]
|
| 1241 |
+
|
| 1242 |
|
| 1243 |
+
|
| 1244 |
base_category.change(fn=update_comp_category_dropdown, inputs=comp_change_triggers, outputs=[comp_category, comp_status])
|
| 1245 |
+
|
| 1246 |
comp_version.change(fn=update_comp_category_dropdown, inputs=comp_change_triggers, outputs=[comp_category, comp_status])
|
| 1247 |
|
| 1248 |
+
|
| 1249 |
+
|
| 1250 |
search_btn.click(
|
| 1251 |
+
|
| 1252 |
fn=execute_unified_search,
|
| 1253 |
+
|
| 1254 |
inputs=[base_standard, base_version, base_category, comp_standard, comp_version, comp_category, mapped_only_cb, diff_filter_cb],
|
| 1255 |
+
|
| 1256 |
outputs=[output_df]
|
| 1257 |
+
|
| 1258 |
)
|
| 1259 |
+
|
| 1260 |
|
| 1261 |
+
|
| 1262 |
mapped_only_cb.change(
|
| 1263 |
+
|
| 1264 |
fn=execute_unified_search,
|
| 1265 |
+
|
| 1266 |
inputs=[base_standard, base_version, base_category, comp_standard, comp_version, comp_category, mapped_only_cb, diff_filter_cb],
|
| 1267 |
+
|
| 1268 |
outputs=[output_df]
|
| 1269 |
+
|
| 1270 |
)
|
| 1271 |
|
| 1272 |
+
|
| 1273 |
+
|
| 1274 |
diff_filter_cb.change(
|
| 1275 |
+
|
| 1276 |
fn=execute_unified_search,
|
| 1277 |
+
|
| 1278 |
inputs=[base_standard, base_version, base_category, comp_standard, comp_version, comp_category, mapped_only_cb, diff_filter_cb],
|
| 1279 |
+
|
| 1280 |
outputs=[output_df]
|
| 1281 |
+
|
| 1282 |
)
|
| 1283 |
|
| 1284 |
+
|
| 1285 |
+
|
| 1286 |
base_reset_btn.click(fn=reset_base_selections, inputs=None, outputs=[base_standard, base_version, base_category, base_status])
|
| 1287 |
+
|
| 1288 |
comp_reset_btn.click(fn=reset_comp_selections, inputs=None, outputs=[comp_standard, comp_version, comp_category, comp_status])
|
| 1289 |
|
| 1290 |
+
|
| 1291 |
+
|
| 1292 |
# ==========================================
|
| 1293 |
+
|
| 1294 |
# 5. Application Styling (CSS)
|
| 1295 |
+
|
| 1296 |
# ==========================================
|
| 1297 |
+
|
| 1298 |
css = """
|
| 1299 |
+
|
| 1300 |
.reset-row {
|
| 1301 |
+
|
| 1302 |
align-items: flex-end !important;
|
| 1303 |
+
|
| 1304 |
margin-bottom: 5px !important;
|
| 1305 |
+
|
| 1306 |
}
|
| 1307 |
+
|
| 1308 |
.reset-btn {
|
| 1309 |
+
|
| 1310 |
margin-bottom: 10px !important;
|
| 1311 |
+
|
| 1312 |
}
|
| 1313 |
+
|
| 1314 |
table {
|
| 1315 |
+
|
| 1316 |
table-layout: auto !important;
|
| 1317 |
+
|
| 1318 |
width: max-content !important;
|
| 1319 |
+
|
| 1320 |
min-width: 100% !important;
|
| 1321 |
+
|
| 1322 |
}
|
| 1323 |
+
|
| 1324 |
th, td {
|
| 1325 |
+
|
| 1326 |
min-width: 150px;
|
| 1327 |
+
|
| 1328 |
}
|
| 1329 |
+
|
| 1330 |
table:has(th:nth-last-child(2):first-child),
|
| 1331 |
+
|
| 1332 |
table:has(th:nth-last-child(3):first-child),
|
| 1333 |
+
|
| 1334 |
table:has(th:nth-last-child(4):first-child),
|
| 1335 |
+
|
| 1336 |
table:has(th:nth-last-child(5):first-child),
|
| 1337 |
+
|
| 1338 |
table:has(th:nth-last-child(6):first-child) {
|
| 1339 |
+
|
| 1340 |
table-layout: fixed !important;
|
| 1341 |
+
|
| 1342 |
width: 100% !important;
|
| 1343 |
+
|
| 1344 |
}
|
| 1345 |
+
|
| 1346 |
table th:nth-last-child(2):first-child, table td:nth-last-child(2):first-child { width: 15% !important; min-width: 0 !important; }
|
| 1347 |
+
|
| 1348 |
table th:nth-last-child(1), table td:nth-last-child(1) { width: 85% !important; min-width: 0 !important; }
|
| 1349 |
|
| 1350 |
+
|
| 1351 |
+
|
| 1352 |
table th:nth-child(1):nth-last-child(3), table td:nth-child(1):nth-last-child(3) { width: 20% !important; min-width: 0 !important; }
|
| 1353 |
+
|
| 1354 |
table th:nth-child(2):nth-last-child(2), table td:nth-child(2):nth-last-child(2) { width: 30% !important; min-width: 0 !important; }
|
| 1355 |
+
|
| 1356 |
table th:nth-child(3):nth-last-child(1), table td:nth-child(3):nth-last-child(1) { width: 50% !important; min-width: 0 !important; }
|
| 1357 |
|
| 1358 |
+
|
| 1359 |
+
|
| 1360 |
table th:nth-child(1):nth-last-child(4), table td:nth-child(1):nth-last-child(4) { width: 10% !important; min-width: 0 !important; }
|
| 1361 |
+
|
| 1362 |
table th:nth-child(2):nth-last-child(3), table td:nth-child(2):nth-last-child(3) { width: 40% !important; min-width: 0 !important; }
|
| 1363 |
+
|
| 1364 |
table th:nth-child(3):nth-last-child(2), table td:nth-child(3):nth-last-child(2) { width: 10% !important; min-width: 0 !important; }
|
| 1365 |
+
|
| 1366 |
table th:nth-child(4):nth-last-child(1), table td:nth-child(4):nth-last-child(1) { width: 40% !important; min-width: 0 !important; }
|
| 1367 |
|
| 1368 |
+
|
| 1369 |
+
|
| 1370 |
table th:nth-child(1):nth-last-child(5), table td:nth-child(1):nth-last-child(5) { width: 8% !important; min-width: 0 !important; }
|
| 1371 |
+
|
| 1372 |
table th:nth-child(2):nth-last-child(4), table td:nth-child(2):nth-last-child(4) { width: 8% !important; min-width: 0 !important; }
|
| 1373 |
+
|
| 1374 |
table th:nth-child(3):nth-last-child(3), table td:nth-child(3):nth-last-child(3) { width: 8% !important; min-width: 0 !important; }
|
| 1375 |
+
|
| 1376 |
table th:nth-child(4):nth-last-child(2), table td:nth-child(4):nth-last-child(2) { width: 8% !important; min-width: 0 !important; }
|
| 1377 |
+
|
| 1378 |
table th:nth-child(5):nth-last-child(1), table td:nth-child(5):nth-last-child(1) { width: 68% !important; min-width: 0 !important; }
|
| 1379 |
|
| 1380 |
+
|
| 1381 |
+
|
| 1382 |
table th:nth-child(1):nth-last-child(6), table td:nth-child(1):nth-last-child(6) { width: 8% !important; min-width: 0 !important; }
|
| 1383 |
+
|
| 1384 |
table th:nth-child(2):nth-last-child(5), table td:nth-child(2):nth-last-child(5) { width: 15% !important; min-width: 0 !important; }
|
| 1385 |
+
|
| 1386 |
table th:nth-child(3):nth-last-child(4), table td:nth-child(3):nth-last-child(4) { width: 27% !important; min-width: 0 !important; }
|
| 1387 |
+
|
| 1388 |
table th:nth-child(4):nth-last-child(3), table td:nth-child(4):nth-last-child(3) { width: 8% !important; min-width: 0 !important; }
|
| 1389 |
+
|
| 1390 |
table th:nth-child(5):nth-last-child(2), table td:nth-child(5):nth-last-child(2) { width: 15% !important; min-width: 0 !important; }
|
| 1391 |
+
|
| 1392 |
table th:nth-child(6):nth-last-child(1), table td:nth-child(6):nth-last-child(1) { width: 27% !important; min-width: 0 !important; }
|
| 1393 |
|
| 1394 |
+
|
| 1395 |
+
|
| 1396 |
thead th {
|
| 1397 |
+
|
| 1398 |
font-size: 18px !important;
|
| 1399 |
+
|
| 1400 |
position: sticky;
|
| 1401 |
+
|
| 1402 |
top: 0;
|
| 1403 |
+
|
| 1404 |
background: white;
|
| 1405 |
+
|
| 1406 |
z-index: 10;
|
| 1407 |
+
|
| 1408 |
}
|
| 1409 |
+
|
| 1410 |
.dataframe {
|
| 1411 |
+
|
| 1412 |
max-height: none !important;
|
| 1413 |
+
|
| 1414 |
overflow-y: visible !important;
|
| 1415 |
+
|
| 1416 |
overflow-x: auto !important;
|
| 1417 |
+
|
| 1418 |
display: block;
|
| 1419 |
+
|
| 1420 |
}
|
| 1421 |
+
|
| 1422 |
.dataframe > div {
|
| 1423 |
+
|
| 1424 |
max-height: none !important;
|
| 1425 |
+
|
| 1426 |
overflow: visible !important;
|
| 1427 |
+
|
| 1428 |
}
|
| 1429 |
+
|
| 1430 |
td {
|
| 1431 |
+
|
| 1432 |
font-size: 18px !important;
|
| 1433 |
+
|
| 1434 |
white-space: pre-wrap !important;
|
| 1435 |
+
|
| 1436 |
word-break: keep-all !important;
|
| 1437 |
+
|
| 1438 |
line-height: 1.6;
|
| 1439 |
+
|
| 1440 |
padding: 10px;
|
| 1441 |
+
|
| 1442 |
vertical-align: top !important;
|
| 1443 |
+
|
| 1444 |
text-align: left !important;
|
| 1445 |
+
|
| 1446 |
}
|
| 1447 |
+
|
| 1448 |
td img {
|
| 1449 |
+
|
| 1450 |
display: block;
|
| 1451 |
+
|
| 1452 |
max-width: none !important;
|
| 1453 |
+
|
| 1454 |
}
|
| 1455 |
+
|
| 1456 |
#search_row {
|
| 1457 |
+
|
| 1458 |
align-items: center !important;
|
| 1459 |
+
|
| 1460 |
margin-bottom: 5px !important;
|
| 1461 |
+
|
| 1462 |
}
|
| 1463 |
+
|
| 1464 |
#diff_cb_item, #mapped_cb_item {
|
| 1465 |
+
|
| 1466 |
margin-top: 0 !important;
|
| 1467 |
+
|
| 1468 |
padding-left: 15px !important;
|
| 1469 |
+
|
| 1470 |
width: max-content !important;
|
| 1471 |
+
|
| 1472 |
min-width: max-content !important;
|
| 1473 |
+
|
| 1474 |
flex-grow: 0 !important;
|
| 1475 |
+
|
| 1476 |
}
|
| 1477 |
+
|
| 1478 |
"""
|
| 1479 |
|
| 1480 |
+
|
| 1481 |
+
|
| 1482 |
if __name__ == "__main__":
|
| 1483 |
+
|
| 1484 |
demo.launch(
|
| 1485 |
+
|
| 1486 |
theme=gr.themes.Soft(),
|
| 1487 |
+
|
| 1488 |
share=True,
|
| 1489 |
+
|
| 1490 |
css=css
|
| 1491 |
+
|
| 1492 |
)
|