""", unsafe_allow_html=True)
result_data = st.session_state.get("sim_result", DEFAULT_RESULT)
src = st.session_state.get("topcard_source", "")
if src == "filter": st.caption("카드 값: **필터 내 최적** (THINNING 최소 / MAX FAILURE 최소)")
elif src == "overall": st.caption("카드 값: **전체 탐색 최적** (THINNING 최소 / MAX FAILURE 최소)")
elif src == "single": st.caption("카드 값: **단일 입력 결과**")
col1, col2 = st.columns(2, gap="small")
with col1:
st.session_state.setdefault("material", "590")
cur_mat = st.session_state["material"]
thin_cap = MATERIAL_THICKNESS_CAP.get(cur_mat, 0.16)
st.session_state["thinning_min"] = 0.0
st.session_state["thinning_max"] = thin_cap
metric_card("THINNING (두께 감소율)",
result_data.get("THINNING", 0.0),
float(st.session_state["thinning_min"]),
float(st.session_state["thinning_max"]))
with col2:
st.session_state.setdefault("max_failure_min", 0.00)
st.session_state.setdefault("max_failure_max", 0.97)
metric_card("MAX FAILURE",
result_data.get("MAX_FAILURE", 0.0),
float(st.session_state["max_failure_min"]),
float(st.session_state["max_failure_max"]))
cL, cR = st.columns(2, gap="small")
def _summary_block(title: str, row_thin: pd.Series, row_mf: pd.Series):
st.subheader(title)
st.markdown("**THINNING 최소**")
if row_thin is not None and len(row_thin):
lb, rb = int(row_thin.get('LB',0)), int(row_thin.get('RB',0))
bead_label = ("No Bead" if (lb==0 and rb==0) else ("Left Bead" if (lb==1 and rb==0)
else ("Right Bead" if (lb==0 and rb==1) else "Double Bead")))
st.markdown(
f"- 재질: **{row_thin.get('material','-')}** (비드: **{bead_label}**)
"
f"- 두께: **{row_thin.get('thickness','-')} mm**, 직경: **{row_thin.get('diameter','-')} mm**, 각도: **{row_thin.get('degree','-')}°**
"
f"- 상단 R: **{row_thin.get('upper_radius','-')}**, 하단 R: **{row_thin.get('lower_radius','-')}**
"
f"- **THINNING: {row_thin.get('THINNING',0):.3f}**, MAX_FAILURE: {row_thin.get('MAX_FAILURE',0):.3f}",
unsafe_allow_html=True)
else:
st.caption("해당 없음")
st.markdown("---")
st.markdown("**MAX_FAILURE 최소**")
if row_mf is not None and len(row_mf):
lb, rb = int(row_mf.get('LB',0)), int(row_mf.get('RB',0))
bead_label = ("No Bead" if (lb==0 and rb==0) else ("Left Bead" if (lb==1 and rb==0)
else ("Right Bead" if (lb==0 and rb==1) else "Double Bead")))
st.markdown(
f"- 재질: **{row_mf.get('material','-')}** (비드: **{bead_label}**)
"
f"- 두께: **{row_mf.get('thickness','-')} mm**, 직경: **{row_mf.get('diameter','-')} mm**, 각도: **{row_mf.get('degree','-')}°**
"
f"- 상단 R: **{row_mf.get('upper_radius','-')}**, 하단 R: **{row_mf.get('lower_radius','-')}**
"
f"- THINNING: {row_mf.get('THINNING',0):.3f}, **MAX_FAILURE: {row_mf.get('MAX_FAILURE',0):.3f}**",
unsafe_allow_html=True)
else:
st.caption("해당 없음")
with cL:
_summary_block(
"필터 내 최적",
None if st.session_state.get("best_filter_thin") is None else pd.Series(st.session_state["best_filter_thin"]),
None if st.session_state.get("best_filter_mf") is None else pd.Series(st.session_state["best_filter_mf"]),
)
with cR:
_summary_block(
"전체 탐색 최적",
pd.Series(st.session_state.get("best_all_thin", {})) if st.session_state.get("best_all_thin") else None,
pd.Series(st.session_state.get("best_all_mf", {})) if st.session_state.get("best_all_mf") else None,
)
# ✅ 기준 재질 & 상한표 — 같은 위치(한 열)에 세로로 정렬
with st.container():
material_list = DISPLAY_LABELS
cur = st.session_state.get("material", "590")
default_idx = material_list.index(cur) if cur in material_list else 1
sel = st.selectbox("기준 재질", material_list, index=default_idx, key="material_for_result")
st.session_state["material"] = sel
cap = MATERIAL_THICKNESS_CAP[sel]
st.session_state["thinning_min"] = 0.0
st.session_state["thinning_max"] = cap
st.caption(f"현재 기준 재질: {sel} (두께 감소율 상한 {cap:.2f})")
# 바로 아래에 상한 기준표를 붙여서 표시
st.markdown('
', unsafe_allow_html=True)
st.caption("재질별 두께 감소율 상한")
render_cap_table()
st.markdown('
', unsafe_allow_html=True)
# (선택) 위에서 열었으면 닫기
# st.markdown("
", unsafe_allow_html=True)
# -----------------------------------------
# 3) 기록 조회 ✅ 전체 교체
# -----------------------------------------
with tabs[2]:
st.header("기록 조회")
col1, col2, col3 = st.columns([1, 1, 1])
with col1:
save_btn = st.button("현재 결과 저장", type="primary", use_container_width=True)
with col2:
select_all_btn = st.button("전체 선택", use_container_width=True)
with col3:
delete_btn = st.button("선택 항목 삭제", use_container_width=True)
# ===== 현재 결과 저장 처리 =====
if save_btn:
if "sim_result" not in st.session_state:
st.warning("먼저 시뮬레이션을 실행하세요.")
else:
# 다음 인덱스
try:
next_idx = max([r.get("Index", 0) for r in st.session_state.history]) + 1 if st.session_state.history else 1
except Exception:
next_idx = len(st.session_state.history) + 1
# 입력 모드/라벨
input_mode = st.session_state.get("input_mode", "직접 입력")
if input_mode == "직접 입력":
bead_label = st.session_state.get("beadType", "-")
material_label = st.session_state.get("material", "-")
else:
bead_label = ", ".join(st.session_state.get("beadTypes_multi", [])) or "-"
material_label = ", ".join(st.session_state.get("materials_multi", [])) or "-"
# 단일 값도 있으면 함께 저장(표에서 필터/정렬 편하게)
diameter_val = st.session_state.get("diameter")
degree_val = st.session_state.get("degree")
new_row = {
"선택": False,
"Index": next_idx,
"저장시각": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"비드 타입": bead_label,
"입력 방식": input_mode,
"소재 두께 (mm)": val_or_range("thickness", "thicknessRange", " mm"),
"재질": material_label,
"직경": val_or_range("diameter", "diameterRange", " mm"),
"각도": val_or_range("degree", "degreeRange", "°"),
"상단 R": val_or_range("upperR", "upperRRange"),
"하단 R": val_or_range("lowerR", "lowerRRange"),
"THINNING": float(st.session_state.sim_result.get("THINNING")),
"MAX_FAILURE": float(st.session_state.sim_result.get("MAX_FAILURE")),
# 참고용 원시 숫자(없으면 None)
"diameter": diameter_val,
"degree": degree_val,
}
st.session_state.history.append(new_row)
# 즉시 반영
st.success("현재 결과가 기록에 저장되었습니다.")
st.rerun()
# ===== 테이블/버튼 동작 =====
if st.session_state.history:
df = pd.DataFrame(st.session_state.history)
cols = ["선택"] + [c for c in df.columns if c != "선택"]
df = df[cols]
if select_all_btn:
for r in st.session_state.history:
r["선택"] = True
st.rerun()
st.subheader(f"기록 테이블 (총 {len(df)}건, 체크 후 삭제 가능)")
edited_df = st.data_editor(
df, hide_index=True, use_container_width=True, key="history_editor"
)
if delete_btn:
selected_index = edited_df[edited_df["선택"] == True]["Index"].tolist()
st.session_state.history = [
rec for rec in st.session_state.history if rec.get("Index") not in selected_index
]
st.success(f"{len(selected_index)}개 항목 삭제 완료!")
st.rerun()
# CSV 다운로드(전체/선택)
sel_df = edited_df[edited_df["선택"] == True].copy()
c1, c2 = st.columns(2)
with c1:
csv_all = edited_df.to_csv(index=False).encode("utf-8-sig")
st.download_button("CSV (전체 다운로드)", csv_all, "simulation_history_all.csv", "text/csv", use_container_width=True)
with c2:
if len(sel_df):
csv_sel = sel_df.to_csv(index=False).encode("utf-8-sig")
st.download_button("CSV (선택만 다운로드)", csv_sel, "simulation_history_selected.csv", "text/csv", use_container_width=True)
else:
st.caption("선택된 행이 없습니다. 표에서 체크 후 다운로드하세요.")
else:
st.info("아직 저장된 기록이 없습니다. 범위 입력으로 실행하면 조건을 만족한 모든 조합이 자동 저장됩니다.")