Refactor import and workflow services plus adapter seams
This commit is contained in:
+65
-58
@@ -15,7 +15,14 @@ import plotly.express as px
|
||||
import plotly.io as pio
|
||||
import streamlit as st
|
||||
|
||||
from jarca_ledger_mcp.adapters.streamlit import service_gateway as service
|
||||
from jarca_ledger_mcp.adapters.streamlit import accounts_gateway as accounts_gateway
|
||||
from jarca_ledger_mcp.adapters.streamlit import assets_gateway as assets_gateway
|
||||
from jarca_ledger_mcp.adapters.streamlit import categorization_gateway as categorization_gateway
|
||||
from jarca_ledger_mcp.adapters.streamlit import imports_gateway as imports_gateway
|
||||
from jarca_ledger_mcp.adapters.streamlit import overview_gateway as overview_gateway
|
||||
from jarca_ledger_mcp.adapters.streamlit import reconciliation_gateway as reconciliation_gateway
|
||||
from jarca_ledger_mcp.adapters.streamlit import transactions_gateway as transactions_gateway
|
||||
from jarca_ledger_mcp.adapters.streamlit import transfers_gateway as transfers_gateway
|
||||
from jarca_ledger_mcp.db import resolve_db_path
|
||||
|
||||
st.set_page_config(page_title="Jarca Ledger", page_icon="💰", layout="wide")
|
||||
@@ -146,7 +153,7 @@ def get_db_path() -> str:
|
||||
return str(resolve_db_path())
|
||||
|
||||
|
||||
def get_service_callable(name: str, required_params: list[str] | None = None):
|
||||
def get_service_callable(module, name: str, required_params: list[str] | None = None):
|
||||
"""Return a service function, reloading once for Streamlit hot-reload edge cases.
|
||||
|
||||
Streamlit can keep an already-imported service module in memory while the
|
||||
@@ -155,7 +162,7 @@ def get_service_callable(name: str, required_params: list[str] | None = None):
|
||||
signature. In that case reload the service module before returning it.
|
||||
"""
|
||||
required_params = required_params or []
|
||||
fn = getattr(service, name, None)
|
||||
fn = getattr(module, name, None)
|
||||
if callable(fn):
|
||||
try:
|
||||
params = inspect.signature(fn).parameters
|
||||
@@ -164,8 +171,8 @@ def get_service_callable(name: str, required_params: list[str] | None = None):
|
||||
except (TypeError, ValueError):
|
||||
if not required_params:
|
||||
return fn
|
||||
importlib.reload(service)
|
||||
return getattr(service, name, None)
|
||||
importlib.reload(module)
|
||||
return getattr(module, name, None)
|
||||
|
||||
|
||||
def inject_app_css() -> None:
|
||||
@@ -431,10 +438,10 @@ def parse_optional_int(value: str, field_name: str) -> int | None:
|
||||
|
||||
|
||||
def get_category_options(db_path: str) -> tuple[list[str], dict[str, list[str]]]:
|
||||
result = service.list_ledger_categories(active_only=True, include_usage=False, db_path=db_path)
|
||||
result = categorization_gateway.list_ledger_categories(active_only=True, include_usage=False, db_path=db_path)
|
||||
if result.get("success") and result.get("count", 0) == 0:
|
||||
service.seed_default_ledger_categories(db_path=db_path)
|
||||
result = service.list_ledger_categories(active_only=True, include_usage=False, db_path=db_path)
|
||||
categorization_gateway.seed_default_ledger_categories(db_path=db_path)
|
||||
result = categorization_gateway.list_ledger_categories(active_only=True, include_usage=False, db_path=db_path)
|
||||
grouped = result.get("grouped", {}) if result.get("success") else {}
|
||||
categories = [""] + sorted(grouped.keys())
|
||||
subcategories = {cat: [""] + sorted([s for s in subs if s]) for cat, subs in grouped.items()}
|
||||
@@ -524,7 +531,7 @@ def update_transaction_form(tx: dict[str, Any], key_prefix: str, db_path: str |
|
||||
submitted = st.button("저장", type="primary", key=f"{key_prefix}_submit")
|
||||
|
||||
if submitted:
|
||||
result = service.update_ledger_transaction(
|
||||
result = transactions_gateway.update_ledger_transaction(
|
||||
tx["id"], category=category, subcategory=subcategory, person=person,
|
||||
is_fixed=is_fixed, memo=memo, db_path=db_path,
|
||||
)
|
||||
@@ -532,7 +539,7 @@ def update_transaction_form(tx: dict[str, Any], key_prefix: str, db_path: str |
|
||||
st.error(result.get("error", result))
|
||||
return
|
||||
if save_rule and tx.get("merchant") and category:
|
||||
rule = service.create_category_rule(
|
||||
rule = categorization_gateway.create_category_rule(
|
||||
match_field="merchant",
|
||||
pattern=tx["merchant"],
|
||||
category=category,
|
||||
@@ -560,7 +567,7 @@ def transaction_edit_dialog(tx: dict[str, Any], key_prefix: str, db_path: str) -
|
||||
def linked_transfer_candidates_dialog(tx: dict[str, Any], db_path: str) -> None:
|
||||
st.caption(f"기준 거래: `{tx['id']}`")
|
||||
st.write(f"**{tx.get('transaction_datetime') or tx.get('transaction_date')}** · {tx.get('merchant')} · {format_krw(tx.get('amount'))}")
|
||||
result = service.find_self_transfer_candidates(tx["id"], limit=20, db_path=db_path)
|
||||
result = transfers_gateway.find_self_transfer_candidates(tx["id"], limit=20, db_path=db_path)
|
||||
if not result.get("success"):
|
||||
st.error(result.get("error", result))
|
||||
return
|
||||
@@ -578,7 +585,7 @@ def linked_transfer_candidates_dialog(tx: dict[str, Any], db_path: str) -> None:
|
||||
cols[3].write(format_krw(candidate.get("amount")))
|
||||
cols[4].write(candidate.get("merchant") or candidate.get("counterparty") or "")
|
||||
if cols[5].button("이 거래와 연결", key=f"link_pair_{tx['id']}_{candidate['id']}_{idx}", use_container_width=True, type="primary"):
|
||||
link_res = service.link_self_transfer_pair(tx["id"], candidate["id"], db_path=db_path)
|
||||
link_res = transfers_gateway.link_self_transfer_pair(tx["id"], candidate["id"], db_path=db_path)
|
||||
if not link_res.get("success"):
|
||||
st.error(link_res.get("error", link_res))
|
||||
else:
|
||||
@@ -603,7 +610,7 @@ def linked_transfer_candidates_dialog(tx: dict[str, Any], db_path: str) -> None:
|
||||
def cancel_original_candidates_dialog(tx: dict[str, Any], db_path: str) -> None:
|
||||
st.caption(f"취소/환급 거래: `{tx['id']}`")
|
||||
st.write(f"**{tx.get('transaction_datetime') or tx.get('transaction_date')}** · {tx.get('merchant')} · {format_krw(tx.get('amount'))}")
|
||||
result = service.find_cancel_original_candidates(tx["id"], limit=20, db_path=db_path)
|
||||
result = transfers_gateway.find_cancel_original_candidates(tx["id"], limit=20, db_path=db_path)
|
||||
if not result.get("success"):
|
||||
st.error(result.get("error", result))
|
||||
return
|
||||
@@ -621,7 +628,7 @@ def cancel_original_candidates_dialog(tx: dict[str, Any], db_path: str) -> None:
|
||||
cols[3].write(format_krw(candidate.get("amount")))
|
||||
cols[4].write(candidate.get("merchant") or candidate.get("counterparty") or "")
|
||||
if cols[5].button("이 원거래와 연결", key=f"cancel_pair_{tx['id']}_{candidate['id']}_{idx}", use_container_width=True, type="primary"):
|
||||
link_res = service.link_cancel_to_original(tx["id"], candidate["id"], db_path=db_path)
|
||||
link_res = transfers_gateway.link_cancel_to_original(tx["id"], candidate["id"], db_path=db_path)
|
||||
if not link_res.get("success"):
|
||||
st.error(link_res.get("error", link_res))
|
||||
else:
|
||||
@@ -683,7 +690,7 @@ def category_rule_create_dialog(db_path: str) -> None:
|
||||
st.error(str(exc))
|
||||
else:
|
||||
fixed_value = None if is_fixed == "변경안함" else is_fixed == "예"
|
||||
res = service.create_category_rule(match_field, pattern, category, subcategory, tx_type, match_type, int(priority), person, fixed_value, 1.0, memo, amount_min, amount_max, day_of_month, db_path=db_path)
|
||||
res = categorization_gateway.create_category_rule(match_field, pattern, category, subcategory, tx_type, match_type, int(priority), person, fixed_value, 1.0, memo, amount_min, amount_max, day_of_month, db_path=db_path)
|
||||
clear_cache()
|
||||
if res.get("success"):
|
||||
st.success("규칙을 저장했습니다.")
|
||||
@@ -735,7 +742,7 @@ def category_rule_edit_dialog(rule: dict[str, Any], db_path: str) -> None:
|
||||
st.error(str(exc))
|
||||
else:
|
||||
edit_fixed_value = None if edit_is_fixed == "변경안함" else edit_is_fixed == "예"
|
||||
update_rule = getattr(service, "update_category_rule", None)
|
||||
update_rule = getattr(categorization_gateway, "update_category_rule", None)
|
||||
if update_rule is None:
|
||||
st.error("현재 실행 중인 서비스에 update_category_rule 함수가 없습니다. 앱을 재시작해 최신 코드를 다시 불러오세요.")
|
||||
else:
|
||||
@@ -787,7 +794,7 @@ def save_merchant_rule_from_recommendation(merchant: str, tx_type: str, rec: dic
|
||||
return {"success": False, "error": "merchant가 비어 있어 규칙을 만들 수 없습니다."}
|
||||
if not rec.get("category"):
|
||||
return {"success": False, "error": "추천 분류가 없어 규칙을 만들 수 없습니다."}
|
||||
return service.create_category_rule(
|
||||
return categorization_gateway.create_category_rule(
|
||||
match_field="merchant",
|
||||
match_type="contains",
|
||||
pattern=merchant,
|
||||
@@ -817,7 +824,7 @@ def render_uncategorized_groups(groups: list[dict[str, Any]], db_path: str, repe
|
||||
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 = service.apply_uncategorized_group_recommendation(
|
||||
res = categorization_gateway.apply_uncategorized_group_recommendation(
|
||||
group.get("merchant", ""), group.get("raw_summary", ""), group.get("type", ""), db_path=db_path
|
||||
)
|
||||
clear_cache()
|
||||
@@ -860,7 +867,7 @@ def render_uncategorized_work_items(rows: list[dict[str, Any]], db_path: str, re
|
||||
action_cols = st.columns([1.0, 1.0, 1.0, 1.1, 1.1, 2.5], vertical_alignment="center")
|
||||
apply_disabled = not bool(rec.get("category"))
|
||||
if action_cols[0].button("추천 적용", key=f"uncat_apply_rec_{row['id']}_{idx}", disabled=apply_disabled, use_container_width=True, type="primary"):
|
||||
res = service.apply_uncategorized_recommendation(row["id"], db_path=db_path)
|
||||
res = categorization_gateway.apply_uncategorized_recommendation(row["id"], db_path=db_path)
|
||||
clear_cache()
|
||||
if res.get("success"):
|
||||
st.success(f"{recommendation_text(res.get('recommendation'))}로 분류했습니다.")
|
||||
@@ -892,7 +899,7 @@ def category_rule_preview_dialog(rule: dict[str, Any], db_path: str) -> None:
|
||||
st.caption(f"규칙 ID: `{rule_id}`")
|
||||
st.write(f"**{pattern}** → **{result_text}**")
|
||||
|
||||
preview_fn = get_service_callable("preview_category_rule_matches")
|
||||
preview_fn = get_service_callable(categorization_gateway, "preview_category_rule_matches")
|
||||
if not callable(preview_fn):
|
||||
st.error("현재 실행 중인 서비스에서 단일 규칙 미리보기 함수를 찾지 못했습니다. Streamlit 서버를 완전히 종료한 뒤 다시 시작해 주세요.")
|
||||
return
|
||||
@@ -922,7 +929,7 @@ def category_rule_preview_dialog(rule: dict[str, Any], db_path: str) -> None:
|
||||
st.info("이 규칙에 부합하는 거래가 없습니다.")
|
||||
|
||||
def page_overview(db_path: str) -> None:
|
||||
health = service.health_check(db_path)
|
||||
health = overview_gateway.health_check(db_path)
|
||||
render_page_header(
|
||||
"가계부 운영 대시보드",
|
||||
"이번 달 실제 현금흐름, 자산 유동성, 운영 경고를 우선순위대로 확인합니다.",
|
||||
@@ -931,14 +938,14 @@ def page_overview(db_path: str) -> None:
|
||||
)
|
||||
st.caption(f"DB: `{health.get('db_path')}`")
|
||||
month = current_kst_month()
|
||||
cashflow = service.get_monthly_cashflow_report(month, db_path=db_path)
|
||||
asset_report = service.get_asset_liquidity_report(db_path=db_path)
|
||||
budget_report = service.get_budget_vs_actual_report(month, db_path=db_path)
|
||||
uncategorized_res = service.list_uncategorized_transactions(limit=1000, db_path=db_path)
|
||||
transfer_queue_res = service.list_cross_transfer_review_queue(limit=300, db_path=db_path)
|
||||
reconciliation_res = service.get_balance_reconciliation_report(db_path=db_path)
|
||||
backups_res = service.list_ledger_backups(limit=1)
|
||||
imports_res = service.list_ledger_import_batches(limit=1, db_path=db_path)
|
||||
cashflow = overview_gateway.get_monthly_cashflow_report(month, db_path=db_path)
|
||||
asset_report = overview_gateway.get_asset_liquidity_report(db_path=db_path)
|
||||
budget_report = overview_gateway.get_budget_vs_actual_report(month, db_path=db_path)
|
||||
uncategorized_res = overview_gateway.list_uncategorized_transactions(limit=1000, db_path=db_path)
|
||||
transfer_queue_res = overview_gateway.list_cross_transfer_review_queue(limit=300, db_path=db_path)
|
||||
reconciliation_res = reconciliation_gateway.get_balance_reconciliation_report(db_path=db_path)
|
||||
backups_res = overview_gateway.list_ledger_backups(limit=1)
|
||||
imports_res = overview_gateway.list_ledger_import_batches(limit=1, db_path=db_path)
|
||||
|
||||
render_section_label("💎", "이번 달 수입/지출 현황")
|
||||
st.caption(f"기준: KST 현재 월 {month}, transactions.transaction_date, ledger_categories.cashflow_kind 실제 현금흐름")
|
||||
@@ -1037,7 +1044,7 @@ def page_assets(db_path: str) -> None:
|
||||
memo = st.text_input("운영 메모", value="")
|
||||
submitted = st.form_submit_button("수기 자산 추가", type="primary")
|
||||
if submitted:
|
||||
res = service.create_manual_asset(name, int(amount), valuation_date.isoformat(), bucket, source_note=source_note, memo=memo, db_path=db_path)
|
||||
res = assets_gateway.create_manual_asset(name, int(amount), valuation_date.isoformat(), bucket, source_note=source_note, memo=memo, db_path=db_path)
|
||||
if res.get("success"):
|
||||
clear_cache()
|
||||
st.success("수기 자산을 추가했습니다.")
|
||||
@@ -1045,7 +1052,7 @@ def page_assets(db_path: str) -> None:
|
||||
else:
|
||||
st.error(res.get("error", res))
|
||||
|
||||
assets_res = service.list_manual_assets(active_only=False, db_path=db_path)
|
||||
assets_res = assets_gateway.list_manual_assets(active_only=False, db_path=db_path)
|
||||
assets = assets_res.get("manual_assets", []) if assets_res.get("success") else []
|
||||
render_section_label("📋", "수기 자산/부채 목록")
|
||||
if assets:
|
||||
@@ -1071,13 +1078,13 @@ def page_assets(db_path: str) -> None:
|
||||
do_update = col_update.form_submit_button("선택 항목 수정", type="primary")
|
||||
do_deactivate = col_deactivate.form_submit_button("선택 항목 비활성", type="secondary")
|
||||
if do_update:
|
||||
res = service.update_manual_asset(selected["id"], name=new_name, valuation_amount=int(new_amount), valuation_date=new_date, liquidity_bucket=new_bucket, source_note=new_source, memo=new_memo, active=make_active, db_path=db_path)
|
||||
res = assets_gateway.update_manual_asset(selected["id"], name=new_name, valuation_amount=int(new_amount), valuation_date=new_date, liquidity_bucket=new_bucket, source_note=new_source, memo=new_memo, active=make_active, db_path=db_path)
|
||||
if res.get("success"):
|
||||
clear_cache(); st.success("수정했습니다."); st.rerun()
|
||||
else:
|
||||
st.error(res.get("error", res))
|
||||
if do_deactivate:
|
||||
res = service.deactivate_manual_asset(selected["id"], db_path=db_path)
|
||||
res = assets_gateway.deactivate_manual_asset(selected["id"], db_path=db_path)
|
||||
if res.get("success"):
|
||||
clear_cache(); st.warning("비활성 처리했습니다."); st.rerun()
|
||||
else:
|
||||
@@ -1094,9 +1101,9 @@ def page_assets(db_path: str) -> None:
|
||||
correction_reason = c3.text_input("정정 사유(재마감 시 필수)", value="")
|
||||
p1, p2 = st.columns(2)
|
||||
if p1.button("스냅샷 Preview", type="secondary"):
|
||||
st.session_state["asset_snapshot_preview"] = service.preview_asset_snapshot_close(basis_date, db_path=db_path)
|
||||
st.session_state["asset_snapshot_preview"] = assets_gateway.preview_asset_snapshot_close(basis_date, db_path=db_path)
|
||||
if p2.button("스냅샷 Close 실행", type="primary"):
|
||||
st.session_state["asset_snapshot_close"] = service.close_asset_snapshot(basis_date, source_note=close_note, correction_reason=correction_reason, db_path=db_path)
|
||||
st.session_state["asset_snapshot_close"] = assets_gateway.close_asset_snapshot(basis_date, source_note=close_note, correction_reason=correction_reason, db_path=db_path)
|
||||
clear_cache()
|
||||
preview = st.session_state.get("asset_snapshot_preview")
|
||||
close_res = st.session_state.get("asset_snapshot_close")
|
||||
@@ -1170,10 +1177,10 @@ def page_cross_transfer_review(db_path: str) -> None:
|
||||
c1, c2 = st.columns(2)
|
||||
with c1:
|
||||
if st.button("미리보기(드라이런)"):
|
||||
st.session_state["cross_transfer_preview"] = service.preview_cross_account_transfers(db_path=db_path)
|
||||
st.session_state["cross_transfer_preview"] = transfers_gateway.preview_cross_account_transfers(db_path=db_path)
|
||||
with c2:
|
||||
if st.button("자동 매칭 실행"):
|
||||
res = service.run_cross_account_transfer_match(db_path=db_path)
|
||||
res = transfers_gateway.run_cross_account_transfer_match(db_path=db_path)
|
||||
clear_cache()
|
||||
st.session_state["cross_transfer_last_run"] = res
|
||||
st.success(f"매칭 {res.get('matched_count', 0)}건 적용, 확인필요 {res.get('review_count', 0)}건")
|
||||
@@ -1190,7 +1197,7 @@ def page_cross_transfer_review(db_path: str) -> None:
|
||||
|
||||
st.divider()
|
||||
render_section_label("📌", "확인필요 큐")
|
||||
list_queue_fn = getattr(service, "list_cross_transfer_review_queue", None)
|
||||
list_queue_fn = getattr(transfers_gateway, "list_cross_transfer_review_queue", None)
|
||||
if callable(list_queue_fn):
|
||||
queue = list_queue_fn(db_path=db_path)
|
||||
rows = queue.get("transactions", [])
|
||||
@@ -1242,7 +1249,7 @@ def page_uncategorized(db_path: str) -> None:
|
||||
sort_label = filter_cols[4].selectbox("작업목록 정렬", ["금액순", "날짜순(최신순)"], index=0)
|
||||
sort_by = "date_desc" if sort_label == "날짜순(최신순)" else "amount_desc"
|
||||
|
||||
workbench_fn = get_service_callable("get_uncategorized_workbench", required_params=["sort_by"])
|
||||
workbench_fn = get_service_callable(categorization_gateway, "get_uncategorized_workbench", required_params=["sort_by"])
|
||||
if not callable(workbench_fn):
|
||||
st.error("현재 실행 중인 서비스에서 미분류 작업대 함수를 찾지 못했습니다. Streamlit 서버를 완전히 재시작해 주세요.")
|
||||
return
|
||||
@@ -1449,7 +1456,7 @@ def render_rule_list_items(rules: list[dict[str, Any]], db_path: str) -> None:
|
||||
if action_cols[0].button("🔎 적용(미리보기)", key=f"rule_expander_preview_{rule_id}_{idx}", use_container_width=True):
|
||||
category_rule_preview_dialog(rule, db_path)
|
||||
if action_cols[1].button("✅ 적용", key=f"rule_expander_apply_{rule_id}_{idx}", use_container_width=True):
|
||||
apply_fn = get_service_callable("apply_category_rule")
|
||||
apply_fn = get_service_callable(categorization_gateway, "apply_category_rule")
|
||||
if not callable(apply_fn):
|
||||
st.error("현재 실행 중인 서비스에서 단일 규칙 적용 함수를 찾지 못했습니다. Streamlit 서버를 완전히 종료한 뒤 다시 시작해 주세요.")
|
||||
else:
|
||||
@@ -1465,7 +1472,7 @@ def render_rule_list_items(rules: list[dict[str, Any]], db_path: str) -> None:
|
||||
with action_cols[3]:
|
||||
st.markdown('<div class="rule-danger-note">삭제보다 비활성화를 권장합니다.</div>', unsafe_allow_html=True)
|
||||
if action_cols[4].button("🗑️ 삭제", key=f"rule_expander_delete_{rule_id}_{idx}"):
|
||||
delete_rule = getattr(service, "delete_category_rule", None)
|
||||
delete_rule = getattr(categorization_gateway, "delete_category_rule", None)
|
||||
if delete_rule is None:
|
||||
st.error("현재 실행 중인 서비스에 delete_category_rule 함수가 없습니다. 앱을 재시작해 최신 코드를 다시 불러오세요.")
|
||||
else:
|
||||
@@ -1491,7 +1498,7 @@ def page_rules(db_path: str) -> None:
|
||||
with action_cols[1]:
|
||||
if st.button("추가", key="rule_add_dialog_open", use_container_width=True, type="primary"):
|
||||
category_rule_create_dialog(db_path)
|
||||
rules = service.list_category_rules(active_only=False, db_path=db_path).get("rules", [])
|
||||
rules = categorization_gateway.list_category_rules(active_only=False, db_path=db_path).get("rules", [])
|
||||
render_section_label("📚", "현재 분류 규칙")
|
||||
render_rule_list_items(rules, db_path)
|
||||
|
||||
@@ -1499,12 +1506,12 @@ def page_rules(db_path: str) -> None:
|
||||
|
||||
c1, c2, c3 = st.columns(3)
|
||||
if c1.button("기본 규칙 seed/update"):
|
||||
res = service.seed_default_category_rules(db_path=db_path)
|
||||
res = categorization_gateway.seed_default_category_rules(db_path=db_path)
|
||||
clear_cache(); st.success(res)
|
||||
if c2.button("규칙 preview"):
|
||||
st.session_state["rule_preview"] = service.preview_categorization(limit=5000, db_path=db_path)
|
||||
st.session_state["rule_preview"] = categorization_gateway.preview_categorization(limit=5000, db_path=db_path)
|
||||
if c3.button("규칙 적용"):
|
||||
res = service.apply_categorization_rules(limit=5000, db_path=db_path)
|
||||
res = categorization_gateway.apply_categorization_rules(limit=5000, db_path=db_path)
|
||||
clear_cache(); st.success(f"{res.get('applied_count', 0)}건 적용")
|
||||
if "rule_preview" in st.session_state:
|
||||
prev = st.session_state["rule_preview"]
|
||||
@@ -1528,7 +1535,7 @@ def page_categories(db_path: str) -> None:
|
||||
|
||||
c1, c2 = st.columns(2)
|
||||
if c1.button("기본 카테고리 seed/update"):
|
||||
res = service.seed_default_ledger_categories(db_path=db_path)
|
||||
res = categorization_gateway.seed_default_ledger_categories(db_path=db_path)
|
||||
clear_cache()
|
||||
st.success(f"{res.get('created_or_updated', 0)}개 카테고리를 생성/갱신했습니다.")
|
||||
st.rerun()
|
||||
@@ -1545,7 +1552,7 @@ def page_categories(db_path: str) -> None:
|
||||
submitted = st.form_submit_button("추가/재활성화", type="primary")
|
||||
if submitted:
|
||||
cashflow_kind = next(k for k, v in CASHFLOW_KIND_LABELS.items() if v == cashflow_kind_label)
|
||||
res = service.create_ledger_category(category, subcategory, int(sort_order), memo, cashflow_kind, db_path=db_path)
|
||||
res = categorization_gateway.create_ledger_category(category, subcategory, int(sort_order), memo, cashflow_kind, db_path=db_path)
|
||||
clear_cache()
|
||||
if res.get("success"):
|
||||
st.success("카테고리를 저장했습니다.")
|
||||
@@ -1553,7 +1560,7 @@ def page_categories(db_path: str) -> None:
|
||||
else:
|
||||
st.error(res.get("error", res))
|
||||
|
||||
result = service.list_ledger_categories(active_only=not show_inactive, include_usage=True, db_path=db_path)
|
||||
result = categorization_gateway.list_ledger_categories(active_only=not show_inactive, include_usage=True, db_path=db_path)
|
||||
rows = result.get("categories", [])
|
||||
render_section_label("🗂️", "카테고리 목록")
|
||||
if rows:
|
||||
@@ -1577,7 +1584,7 @@ def page_categories(db_path: str) -> None:
|
||||
hard_delete = st.checkbox("DB에서 완전 삭제", value=False, help="보통은 끄고 비활성화만 하세요. 거래 기록은 별도 보존됩니다.")
|
||||
force = st.checkbox("사용 중이어도 강제", value=False, help="거래/예산/규칙에서 사용 중인 카테고리도 삭제/비활성화합니다. 거래 category 값은 변경하지 않습니다.")
|
||||
if st.button("삭제/비활성화 실행", type="secondary"):
|
||||
res = service.delete_ledger_category(target["category"], target.get("subcategory", ""), hard_delete=hard_delete, force=force, db_path=db_path)
|
||||
res = categorization_gateway.delete_ledger_category(target["category"], target.get("subcategory", ""), hard_delete=hard_delete, force=force, db_path=db_path)
|
||||
clear_cache()
|
||||
if res.get("success"):
|
||||
st.warning(f"처리했습니다. 사용 거래 {res.get('transaction_count', 0)}건")
|
||||
@@ -1593,7 +1600,7 @@ def page_accounts(db_path: str) -> None:
|
||||
kicker="🏦 Accounts",
|
||||
pills=["잔액", "기관", "statement_status", "수동 잔액대조"],
|
||||
)
|
||||
accounts_res = service.list_ledger_accounts(active_only=False, db_path=db_path)
|
||||
accounts_res = accounts_gateway.list_ledger_accounts(active_only=False, db_path=db_path)
|
||||
accounts_data = accounts_res.get("accounts", []) if accounts_res.get("success") else []
|
||||
accounts = pd.DataFrame(accounts_data)
|
||||
if not accounts.empty:
|
||||
@@ -1638,7 +1645,7 @@ def page_accounts(db_path: str) -> None:
|
||||
except ValueError:
|
||||
st.error("기준 잔액은 정수로 입력하세요.")
|
||||
return
|
||||
res = service.update_ledger_account(selected["id"], db_path=db_path, **update_fields)
|
||||
res = accounts_gateway.update_ledger_account(selected["id"], db_path=db_path, **update_fields)
|
||||
clear_cache()
|
||||
if res.get("success"):
|
||||
st.success("계좌 상태를 저장했습니다.")
|
||||
@@ -1654,7 +1661,7 @@ def page_accounts(db_path: str) -> None:
|
||||
tolerance = c2.number_input("허용 오차(원)", min_value=0, value=1000, step=100)
|
||||
source_note = c3.text_input("출처 메모", value="", placeholder="예: 월말 앱 잔액")
|
||||
if st.button("잔액대조 실행", type="secondary"):
|
||||
st.session_state["balance_reconciliation_report"] = service.get_balance_reconciliation_report(
|
||||
st.session_state["balance_reconciliation_report"] = reconciliation_gateway.get_balance_reconciliation_report(
|
||||
baseline_at=baseline_at,
|
||||
source_note=source_note,
|
||||
tolerance=int(tolerance),
|
||||
@@ -1696,10 +1703,10 @@ def page_imports(db_path: str) -> None:
|
||||
upload_path.write_bytes(uploaded.getbuffer())
|
||||
c1, c2 = st.columns(2)
|
||||
if c1.button("토스증권 Preview"):
|
||||
res = service.preview_toss_securities_pdf_import(str(upload_path), db_path=db_path)
|
||||
res = imports_gateway.preview_toss_securities_pdf_import(str(upload_path), db_path=db_path)
|
||||
st.session_state["toss_preview"] = res
|
||||
if c2.button("토스증권 Import", type="primary"):
|
||||
res = service.import_toss_securities_pdf(str(upload_path), db_path=db_path)
|
||||
res = imports_gateway.import_toss_securities_pdf(str(upload_path), db_path=db_path)
|
||||
clear_cache()
|
||||
st.session_state["toss_import_result"] = res
|
||||
st.rerun()
|
||||
@@ -1730,10 +1737,10 @@ def page_imports(db_path: str) -> None:
|
||||
upload_path.write_bytes(toss_bank_uploaded.getbuffer())
|
||||
c1, c2 = st.columns(2)
|
||||
if c1.button("토스뱅크 Preview"):
|
||||
res = service.preview_toss_bank_pdf_import(str(upload_path), db_path=db_path)
|
||||
res = imports_gateway.preview_toss_bank_pdf_import(str(upload_path), db_path=db_path)
|
||||
st.session_state["toss_bank_preview"] = res
|
||||
if c2.button("토스뱅크 Import", type="primary"):
|
||||
res = service.import_toss_bank_pdf(str(upload_path), db_path=db_path)
|
||||
res = imports_gateway.import_toss_bank_pdf(str(upload_path), db_path=db_path)
|
||||
clear_cache()
|
||||
st.session_state["toss_bank_import_result"] = res
|
||||
st.rerun()
|
||||
@@ -1755,7 +1762,7 @@ def page_imports(db_path: str) -> None:
|
||||
|
||||
st.divider()
|
||||
render_section_label("🧾", "Import 이력")
|
||||
batches = service.list_ledger_import_batches(limit=100, db_path=db_path).get("import_batches", [])
|
||||
batches = overview_gateway.list_ledger_import_batches(limit=100, db_path=db_path).get("import_batches", [])
|
||||
st.dataframe(pd.DataFrame(batches), width="stretch", height=350)
|
||||
if batches:
|
||||
with st.form("rollback"):
|
||||
@@ -1763,7 +1770,7 @@ def page_imports(db_path: str) -> None:
|
||||
hard = st.checkbox("Hard delete", value=False)
|
||||
submitted = st.form_submit_button("Rollback 실행", type="secondary")
|
||||
if submitted:
|
||||
res = service.rollback_ledger_import_batch(batch_id, hard_delete=hard, db_path=db_path)
|
||||
res = imports_gateway.rollback_ledger_import_batch(batch_id, hard_delete=hard, db_path=db_path)
|
||||
clear_cache()
|
||||
st.warning(res)
|
||||
st.rerun()
|
||||
|
||||
Reference in New Issue
Block a user