feat: use list actions for uncategorized groups

This commit is contained in:
2026-08-21 13:03:23 +09:00
parent 1712060386
commit c2dd01db39
+51 -35
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import streamlit as st import streamlit as st
from jarca_ledger_mcp.adapters.streamlit import categorization_gateway from jarca_ledger_mcp.adapters.streamlit import categorization_gateway
from dashboard.components import JarcaList
from dashboard.common import * # noqa: F403, F405 from dashboard.common import * # noqa: F403, F405
def render_uncategorized_action_table(rows: list[dict[str, Any]], db_path: str) -> None: def render_uncategorized_action_table(rows: list[dict[str, Any]], db_path: str) -> None:
@@ -56,42 +57,57 @@ def save_merchant_rule_from_recommendation(merchant: str, tx_type: str, rec: dic
) )
def render_uncategorized_groups_header(groups: list[dict[str, Any]]) -> None:
st.markdown(f"{len(groups)}개 사용처 그룹")
def render_uncategorized_group(group: dict[str, Any], db_path: str) -> None:
rec = group.get("recommendation") or {}
cols = st.columns([2.0, 0.7, 1.0, 1.55, 1.0], vertical_alignment="center")
merchant = group.get("merchant") or group.get("raw_summary") or "-"
group_key = group.get("sample_transaction_id") or merchant
cols[0].markdown(f"**{merchant}** \n<span style='color:#64748b;font-size:0.82rem'>{group.get('raw_summary') or '-'} · {group.get('type') or '-'}</span>", unsafe_allow_html=True)
cols[1].write(f"{int(group.get('count') or 0):,}건")
cols[2].write(format_krw(group.get("total_amount")))
cols[3].write(recommendation_text(rec))
apply_disabled = not bool(rec.get("category"))
action = cols[4].menu_button(
"작업",
options=["그룹 적용", "규칙 만들기"],
key=f"uncat_group_action_{group_key}",
disabled=apply_disabled,
width="stretch",
)
if action == "그룹 적용":
res = categorization_gateway.apply_uncategorized_group_recommendation(
group.get("merchant", ""), group.get("raw_summary", ""), group.get("type", ""), db_path=db_path
)
clear_cache()
if res.get("success"):
st.success(f"그룹 {res.get('applied_count', 0):,}건을 {recommendation_text(res.get('recommendation'))}로 분류했습니다.")
st.rerun()
else:
st.error(res.get("error", res))
elif action == "규칙 만들기":
res = save_merchant_rule_from_recommendation(group.get("merchant", ""), group.get("type", ""), rec, db_path)
clear_cache()
if res.get("success"):
st.success("사용처 규칙을 저장했습니다.")
st.rerun()
else:
st.warning(res.get("error", res))
st.caption(f"최근: {group.get('latest_date') or '-'} · 최대금액: {format_krw(group.get('max_amount'))} · 근거: {rec.get('reason') or '추천 없음'}")
def render_uncategorized_groups(groups: list[dict[str, Any]], db_path: str, repeated_only: bool = False) -> None: def render_uncategorized_groups(groups: list[dict[str, Any]], db_path: str, repeated_only: bool = False) -> None:
visible = [g for g in groups if not repeated_only or int(g.get("count") or 0) >= 2] visible = [g for g in groups if not repeated_only or int(g.get("count") or 0) >= 2][:10]
visible = visible[:10] JarcaList(
if not visible: render_header=render_uncategorized_groups_header,
st.info("반복 후보가 없습니다.") render_item=lambda group: render_uncategorized_group(group, db_path),
return items=visible,
for idx, group in enumerate(visible): empty_message="반복 후보가 없습니다.",
rec = group.get("recommendation") or {} ).render()
cols = st.columns([2.0, 0.7, 1.0, 1.55, 1.0, 1.0], vertical_alignment="center")
merchant = group.get("merchant") or group.get("raw_summary") or "-"
cols[0].markdown(f"**{merchant}** \n<span style='color:#64748b;font-size:0.82rem'>{group.get('raw_summary') or '-'} · {group.get('type') or '-'}</span>", unsafe_allow_html=True)
cols[1].write(f"{int(group.get('count') or 0):,}건")
cols[2].write(format_krw(group.get("total_amount")))
cols[3].write(recommendation_text(rec))
apply_disabled = not bool(rec.get("category"))
if cols[4].button("그룹 적용", key=f"uncat_group_apply_{idx}_{merchant}", disabled=apply_disabled, use_container_width=True):
res = categorization_gateway.apply_uncategorized_group_recommendation(
group.get("merchant", ""), group.get("raw_summary", ""), group.get("type", ""), db_path=db_path
)
clear_cache()
if res.get("success"):
st.success(f"그룹 {res.get('applied_count', 0):,}건을 {recommendation_text(res.get('recommendation'))}로 분류했습니다.")
st.rerun()
else:
st.error(res.get("error", res))
if cols[5].button("규칙 만들기", key=f"uncat_group_rule_{idx}_{merchant}", disabled=apply_disabled, use_container_width=True):
res = save_merchant_rule_from_recommendation(group.get("merchant", ""), group.get("type", ""), rec, db_path)
clear_cache()
if res.get("success"):
st.success("사용처 규칙을 저장했습니다.")
st.rerun()
else:
st.warning(res.get("error", res))
st.caption(f"최근: {group.get('latest_date') or '-'} · 최대금액: {format_krw(group.get('max_amount'))} · 근거: {rec.get('reason') or '추천 없음'}")
if idx < len(visible) - 1:
st.divider()
def render_uncategorized_work_items(rows: list[dict[str, Any]], db_path: str, recommended_only: bool = False) -> None: def render_uncategorized_work_items(rows: list[dict[str, Any]], db_path: str, recommended_only: bool = False) -> None: