From c2dd01db3946e5025d4035bb840bdfe49e649355 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Fri, 21 Aug 2026 13:03:23 +0900 Subject: [PATCH] feat: use list actions for uncategorized groups --- dashboard/page_views/uncategorized.py | 86 ++++++++++++++++----------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/dashboard/page_views/uncategorized.py b/dashboard/page_views/uncategorized.py index 04d04df..7bd50c3 100644 --- a/dashboard/page_views/uncategorized.py +++ b/dashboard/page_views/uncategorized.py @@ -6,6 +6,7 @@ from __future__ import annotations import streamlit as st from jarca_ledger_mcp.adapters.streamlit import categorization_gateway +from dashboard.components import JarcaList from dashboard.common import * # noqa: F403, F405 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{group.get('raw_summary') or '-'} · {group.get('type') or '-'}", 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: - visible = [g for g in groups if not repeated_only or int(g.get("count") or 0) >= 2] - visible = visible[:10] - if not visible: - st.info("반복 후보가 없습니다.") - return - for idx, group in enumerate(visible): - rec = group.get("recommendation") or {} - 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{group.get('raw_summary') or '-'} · {group.get('type') or '-'}", 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() + visible = [g for g in groups if not repeated_only or int(g.get("count") or 0) >= 2][:10] + JarcaList( + render_header=render_uncategorized_groups_header, + render_item=lambda group: render_uncategorized_group(group, db_path), + items=visible, + empty_message="반복 후보가 없습니다.", + ).render() def render_uncategorized_work_items(rows: list[dict[str, Any]], db_path: str, recommended_only: bool = False) -> None: