OpenSource-Hub

securo

애플리케이션

securo-finance/securo

개인정보 보호를 중시하는 오픈 소스 셀프 호스팅 개인 재무 관리 도구입니다.

개요

Securo는 셀프 호스팅 개인 재무 관리 애플리케이션으로, 사용자가 자신의 재무 데이터를 완전히 제어할 수 있게 해줍니다. 다중 계정 관리, 거래 기록, 예산, 저축 목표, 자산 추적을 지원하며, Pluggy, Enable Banking, SimpleFIN 등의 제공업체를 통해 은행 동기화가 가능합니다. 이 앱은 개인정보 보호를 핵심으로 하며, 제3자가 사용자의 데이터에 액세스할 수 없도록 보장합니다.

README 미리보기

\n  \n\nSecuro\n\n  \n  \n  \n  \n  \n  \n  \n  \n  Website · Demo · Roadmap · Docs · Discord · Talk to the maintainer\n\n\nFinance apps want your data. This one doesn't.\n\n\nWe believe personal finance should actually be personal. No corporation should sit between you and your financial data. Securo is an open-source finance manager that runs on your own infrastructure, giving you full visibility into your accounts, spending, and habits, without surrendering a single byte to third parties. Take back control.\n\n\n## Quick Start\n\n**Linux & macOS** (uses Docker or Podman; installs Docker if neither is present):\n\n```bash\ncurl -fsSL https://usesecuro.com/install.sh | bash\n```\n\n**Windows:** Install [Docker Desktop](https://www.docker.com/products/docker-desktop/), then:\n\n```bash\ngit clone https://github.com/securo-finance/securo.git && cd securo\ndocker compose up --build\n```\n\nOpen [http://localhost:3000](http://localhost:3000) and create an account. That's it.\n\n\n  \n\n\n## Features\n\n- Multi-account management with running balances\n- Transaction management with search, filters, and CSV export\n- File import (OFX, QIF, CAMT, CSV)\n- Auto-categorization rules engine\n- Recurring transactions and budgets\n- Goals and savings targets with progress tracking\n- Asset management with valuation tracking and growth rules\n- Reports: Net Worth and Income vs Expenses with category sparklines\n- Bank sync via providers (Pluggy for Brazilian banks, Enable Banking for ~2500 European PSD2 banks, SimpleFIN for US and international banks, extensible)\n- Multi-currency support with automatic FX conversion\n- Multi-user support with admin panel and registration controls\n- Two-factor authentication (TOTP) with brute-force protection\n- OIDC login support for Authentik, Pocket ID, and other standard providers\n- AI Agents (optional): self-hosted LLM chat with tool-use over your data, plus a per-agent RAG knowledge base\n\n## Bank Sync (Optional)\n\nAdd credentials for 

FAQ (5)

문제 해결
SQLAlchemy가 SQLite에서 UUID를 읽을 때 왜 "'float' object has no attribute 'replace'" 오류를 발생시키나요?

SQLite는 UUID로 선언된 컬럼에 NUMERIC affinity를 부여하므로, 1e999...와 같은 일부 UUID hex 문자열이 경고 없이 REAL/float으로 변환됩니다. 그러면 SQLAlchemy의 UUID result processor가 문자열 대신 float을 받게 되고, 그 값에 대해 .replace()를 호출하게 됩니다.

TEXT affinity를 갖는 CHAR(32)로 PostgreSQL UUID를 컴파일하여 SQLite 테스트 스키마를 수정하세요:

from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.ext.compiler import compiles

@compiles(PG_UUID, "sqlite")
def _uuid_as_char32(element, compiler, **kw):
return "CHAR(32)"

Base.metadata.create_all() 또는 엔진/테이블 생성 전에 이를 등록하세요. 기존에 존재하는 파일 기반 SQLite 테스트 데이터베이스는 다시 생성하세요. in-memory 데이터베이스는 자동으로 다시 생성됩니다. PostgreSQL은 영향을 받지 않으며 네이티브 uuid 타입을 계속 사용합니다.

원본 Issue #820
문제 해결
podman-compose를 사용할 때 FRONTEND_URL에 '}'가 붙는 이유는 무엇인가요?

이 문제는 1.6.0보다 오래된 podman-compose 버전에서 발생하며, 이 버전들은 ${FRONTEND_URL:-http://localhost:${FRONTEND_PORT:-3000}}와 같은 compose 파일의 중첩된 기본값을 파싱할 수 없습니다. 파서가 첫 번째 }에서 멈추고 뒤따르는 중괄호가 값에 붙은 채로 남기 때문에, printenv FRONTEND_URL을 실행하면 https://example.mydomain.com}가 표시됩니다. 해결 방법: podman-compose를 1.6.0 이상으로 업그레이드한 후(예: pip install -U podman-compose), podman-compose up -d로 컨테이너를 다시 생성하세요. 또는, 중첩 변수 구문을 제거하여 모든 podman-compose 버전에서 작동하도록 수정된 이 프로젝트의 버전으로 업데이트하세요.

원본 Issue #1162
문제 해결
왜 특정 날짜 이후부터 테스트가 'Due date cannot precede the issue date' 오류로 실패하나요?

테스트 스위트는 하드코딩된 고정 날짜(예: TODAY = date(2026, 8, 26))를 사용하는 반면, 서비스는 datetime.now(timezone.utc).date()를 통해 실제 UTC 시계로부터 issue_date를 생성합니다. 실제 시간이 고정된 due_date(TODAY + 10일)를 지나면, due_date < issue_date 검증이 인보이스를 거부하고 이에 의존하는 모든 테스트가 영구적으로 실패합니다. 해결 방법: 테스트를 서비스가 사용하는 것과 동일한 시계에 맞추세요. 즉, TODAY = date(2026, 8, 26)를 TODAY = datetime.now(timezone.utc).date()로 교체하세요. 이 문제는 인보이스 테스트 스위트를 자체 시계에 고정하는 PR #852를 통해 업스트림에서 수정되었으며, #852가 포함된 버전으로 업데이트해도 해결됩니다. 리터럴 값을 단순히 뒤로 미루는 방식(예: date(2027, 1, 1))은 피하세요. 나중에 동일한 실패가 다시 발생합니다. 참고: 테스트 상수는 임포트 시점에 평가되는 반면 서비스는 호출 시점에 시계를 읽으므로, UTC 날짜 경계에서 간헐적인 실패가 발생하지 않도록 발행일과 마감일 사이에 며칠의 여유를 두세요.

원본 Issue #851
문제 해결
Securo에서 크로스 워크스페이스 그룹 멤버가 split을 저장할 때 발생하는 "One or more split members not found" 오류를 어떻게 해결할 수 있나요?

이 회귀는 Securo v0.16.0에 영향을 미칩니다. PR #375에서 split_service._validate_members에 엄격한 Group.workspace_id == workspace_id 검사가 추가되면서, UI에 그룹이 표시됨에도 불구하고 다른 워크스페이스의 공유 그룹에 연결된 사용자가 거부됩니다.

PR #972가 포함된 릴리스로 업그레이드하여 수정하세요. v0.16.0을 직접 패치해야 하는 경우, split_service._validate_members를 수정하여 group_service._visible_predicate와 동일한 가시성 규칙을 사용하도록 하세요. 즉, 현재 워크스페이스에 속한 그룹이거나 사용자가 소유자가 아닌 멤버로 연결된 그룹을 허용해야 합니다:

member_groups = (
    select(GroupMember.group_id)
    .where(
        GroupMember.linked_user_id == user_id,
        GroupMember.is_self.is_(False),
    )
    .distinct()
)

result = await session.execute(
    select(GroupMember, Group)
    .join(Group, GroupMember.group_id == Group.id)
    .where(
        GroupMember.id.in_(member_ids),
        or_(
            Group.workspace_id == workspace_id,
            Group.id.in_(member_groups),
        ),
    )
)

가능하다면 규칙을 중복으로 작성하는 대신 group_service._visible_predicate를 재사용하거나 임포트하여, 읽기와 쓰기 경로가 다시 서로 달라지지 않도록 하세요. 패치를 적용한 후에는 셀프 호스팅 컨테이너를 다시 빌드하고 재시작하세요. 이 변경은 다른 워크스페이스의 임의 그룹이 참조되는 것을 여전히 차단하며, 요청하는 사용자가 명시적으로 연결된 그룹만 허용됩니다.

원본 Issue #972
문제 해결
Enable Banking 거래에서 채권자 또는 채무자 이름 대신 "Transaction"이 표시되는 이유는 무엇인가요?

이 문제는 remittance_information과 additional_information이 비어 있을 때 발생합니다. Securo가 거래 상대방 이름을 사용하는 대신 바로 "Transaction"으로 대체하기 때문입니다. Enable Banking 가져오기 매핑을 수정하여, remittance 정보와 추가 정보를 확인한 후, 차변(DBIT) 거래에서는 creditor.name을, 대변(CRDT) 거래에서는 debtor.name을 사용하고, 그 다음 반대쪽 거래 상대방을 사용하며, 최종적으로만 "Transaction"을 사용하도록 하세요:

description = _join_remittance(raw.get("remittance_information")) or (
    raw.get("additional_information") or ""
)

if not description:
    creditor = (raw.get("creditor") or {}).get("name") or raw.get("creditor_name")
    debtor = (raw.get("debtor") or {}).get("name") or raw.get("debtor_name")

    if indicator == "DBIT":
        description = creditor or debtor or ""
    else:
        description = debtor or creditor or ""

description = description.strip()[:500] or "Transaction"

Enable Banking 거래 상대방 폴백 패치를 적용하거나, PR fix(enable-banking): fall back to counterparty name when description is missing이 포함된 릴리스로 업그레이드하세요. 수정 후에는 기존 거래를 다시 가져오거나 수동으로 수정해야 할 수 있습니다.

원본 Issue #734