금융 거래 기록 흐름 공통화

This commit is contained in:
seo
2026-07-20 18:27:41 +09:00
parent 8a64d993c3
commit c86e5a1481
7 changed files with 383 additions and 413 deletions
+4
View File
@@ -32,6 +32,7 @@ Financial은 개인 금융 데이터를 서버 렌더링 PHP 화면에서 관리
- `app/lib/auth.php`: 인증, remember token, CSRF, no-store header
- `app/lib/db.php`: PDO 연결
- `app/lib/account_service.php`: 계좌 서비스
- `app/lib/transaction_record_service.php`: 거래 fingerprint, 입력 검증, 공통 거래 row 기록
- `app/lib/transaction_service.php`: 거래 저장과 조회
- `app/lib/installment_service.php`: 할부 청구와 선납 처리
- `app/lib/loan_service.php`: 대출과 상환 처리
@@ -58,9 +59,12 @@ Financial은 개인 금융 데이터를 서버 렌더링 PHP 화면에서 관리
4. 저장 후 redirect와 flash 메시지로 응답합니다.
5. 카테고리 추천 API는 merchant rule 우선순위와 confidence를 반환합니다.
거래 테이블에 실제 row를 추가하는 공통 경로는 `transaction_record_service.php`입니다. 일반 거래 등록/수정은 같은 입력 검증 함수를 쓰고, 대출 실행/상환/중도상환과 할부 선결제도 공통 기록 함수를 통해 `transactions`에 남깁니다. 카드/계좌/할부/대출 기능을 바꿀 때는 먼저 이 공통 기록 규칙과 잔액 재계산 흐름을 확인해야 합니다.
## 주요 함수/모듈
- `auth.php`: 로그인, 세션, CSRF
- `transaction_record_service.php`: 거래 공통 검증과 row 기록
- `transaction_service.php`: 거래 저장과 조회
- `installment_*`: 할부 청구와 선납 처리
- `merchant_rules.php`: 가맹점 자동분류 규칙 관리
+26 -57
View File
@@ -1,8 +1,9 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/card_billing_service.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/card_billing_service.php';
require_once __DIR__ . '/transaction_record_service.php';
function split_amount_evenly(float $amount, int $months): array
{
@@ -547,43 +548,10 @@ function prepay_installment(
recalculate_installment_status($installmentId);
$stmt = $pdo->prepare("
INSERT INTO transactions
(
user_id,
account_id,
category_id,
transaction_type,
amount,
is_installment,
installment_months,
installment_interest_rate,
installment_interest_total,
installment_total_billed,
installment_prepay_amount,
transaction_date,
merchant_name,
description,
related_account_id,
fingerprint
)
VALUES (?, ?, ?, 'expense', ?, 0, NULL, 0, 0, NULL, ?, ?, ?, ?, NULL, ?)
");
$desc = $description ?: '할부 선결제/중도상환';
$fingerprint = hash('sha256', implode('|', [
$userId,
$paymentAccountId,
'installment_prepay',
$installmentId,
$prepayDate,
number_format($totalAmount, 2, '.', ''),
$desc
]));
$stmtCat = $pdo->prepare("
SELECT id
$desc = $description ?: '할부 선결제/중도상환';
$stmtCat = $pdo->prepare("
SELECT id
FROM categories
WHERE user_id = ?
AND category_type = 'expense'
@@ -594,22 +562,23 @@ function prepay_installment(
$category = $stmtCat->fetch();
if (!$category) {
throw new RuntimeException('선결제 기록용 expense 카테고리(기타지출)를 찾을 수 없습니다.');
}
$stmt->execute([
$userId,
$paymentAccountId,
(int)$category['id'],
$totalAmount,
$totalAmount,
$prepayDate,
$installment['merchant_name'],
'[할부 선결제] ' . $desc,
$fingerprint
]);
$pdo->commit();
throw new RuntimeException('선결제 기록용 expense 카테고리(기타지출)를 찾을 수 없습니다.');
}
insert_transaction_record([
'user_id' => $userId,
'account_id' => $paymentAccountId,
'category_id' => (int)$category['id'],
'transaction_type' => 'expense',
'amount' => $totalAmount,
'transaction_date' => $prepayDate,
'merchant_name' => $installment['merchant_name'],
'description' => '[할부 선결제] ' . $installmentId . ' / ' . $desc,
'related_account_id' => null,
'installment_prepay_amount' => $totalAmount,
], true);
$pdo->commit();
recalculate_account_balance($paymentAccountId);
} catch (Throwable $e) {
@@ -701,4 +670,4 @@ function rebuild_all_installments_for_user(int $userId): int
throw $e;
}
}
}
+41 -116
View File
@@ -1,7 +1,8 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/transaction_record_service.php';
function add_months_keep_day(string $date, int $months, int $paymentDay): string
{
@@ -176,110 +177,10 @@ function get_category_id_by_name(int $userId, string $categoryType, string $name
return $row ? (int)$row['id'] : null;
}
function build_tx_fingerprint(
int $userId,
int $accountId,
?int $relatedAccountId,
int $categoryId,
string $transactionType,
float $amount,
string $transactionDate,
?string $merchantName,
?string $description,
?int $sourceLoanId = null
): string {
$raw = implode('|', [
$userId,
$accountId,
$relatedAccountId ?? 0,
$categoryId,
$transactionType,
number_format($amount, 2, '.', ''),
$transactionDate,
trim((string)$merchantName),
trim((string)$description),
$sourceLoanId ?? 0,
]);
return hash('sha256', $raw);
}
function insert_transaction_row(array $data): int
{
$pdo = db();
$sourceLoanId = !empty($data['source_loan_id']) ? (int)$data['source_loan_id'] : null;
$fingerprint = build_tx_fingerprint(
(int)$data['user_id'],
(int)$data['account_id'],
!empty($data['related_account_id']) ? (int)$data['related_account_id'] : null,
(int)$data['category_id'],
(string)$data['transaction_type'],
(float)$data['amount'],
(string)$data['transaction_date'],
$data['merchant_name'] ?? null,
$data['description'] ?? null,
$sourceLoanId
);
$stmt = $pdo->prepare("
SELECT id
FROM transactions
WHERE user_id = ?
AND fingerprint = ?
LIMIT 1
");
$stmt->execute([
$data['user_id'],
$fingerprint
]);
$exists = $stmt->fetch();
if ($exists) {
return (int)$exists['id'];
}
$stmt = $pdo->prepare("
INSERT INTO transactions
(
user_id,
account_id,
category_id,
transaction_type,
amount,
is_installment,
installment_months,
installment_interest_rate,
installment_interest_total,
installment_total_billed,
installment_prepay_amount,
transaction_date,
merchant_name,
description,
related_account_id,
source_loan_id,
fingerprint
)
VALUES (?, ?, ?, ?, ?, 0, NULL, 0, 0, NULL, 0, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['user_id'],
$data['account_id'],
$data['category_id'],
$data['transaction_type'],
$data['amount'],
$data['transaction_date'],
$data['merchant_name'],
$data['description'],
$data['related_account_id'],
$sourceLoanId,
$fingerprint,
]);
return (int)$pdo->lastInsertId();
}
function insert_transaction_row(array $data): int
{
return insert_transaction_record($data, true);
}
function create_loan_schedule_only(
int $loanId,
@@ -928,20 +829,44 @@ function prepay_loan(
)
VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, 'prepayment', 0, ?)
");
$stmt->execute([
$userId,
$loanId,
$accountId ?: null,
$stmt->execute([
$userId,
$loanId,
$accountId ?: null,
$paymentDate,
$principalAmount,
$interestAmount,
$feeAmount,
$totalAmount,
$description,
]);
if ($principalAmount > 0) {
$stmt = $pdo->prepare("
$description,
]);
if (!empty($accountId) && $totalAmount > 0) {
$expenseCategoryId = get_category_id_by_name($userId, 'expense', '대출상환');
if ($expenseCategoryId) {
insert_transaction_row([
'user_id' => $userId,
'account_id' => $accountId,
'category_id' => $expenseCategoryId,
'transaction_type' => 'expense',
'amount' => $totalAmount,
'transaction_date' => $paymentDate,
'merchant_name' => $loan['loan_name'],
'description' => '[대출중도상환] 원금 ' .
number_format($principalAmount, 0) .
' / 이자 ' .
number_format($interestAmount, 0) .
' / 수수료 ' .
number_format($feeAmount, 0),
'related_account_id' => null,
'source_loan_id' => $loanId,
]);
}
}
if ($principalAmount > 0) {
$stmt = $pdo->prepare("
UPDATE loans
SET current_principal_balance = GREATEST(current_principal_balance - ?, 0)
WHERE id = ?
@@ -1151,4 +1076,4 @@ function update_loan_and_rebuild_full_history(int $userId, int $loanId, array $d
}
throw $e;
}
}
}
+274
View File
@@ -0,0 +1,274 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/card_billing_service.php';
function build_transaction_fingerprint(
int $userId,
int $accountId,
?int $relatedAccountId,
int $categoryId,
string $transactionType,
float $amount,
string $transactionDate,
?string $merchantName,
?string $description,
?int $sourceLoanId = null
): string {
$parts = [
$userId,
$accountId,
$relatedAccountId ?? 0,
$categoryId,
$transactionType,
number_format($amount, 2, '.', ''),
$transactionDate,
trim((string)$merchantName),
trim((string)$description),
];
if ($sourceLoanId !== null) {
$parts[] = $sourceLoanId;
}
return hash('sha256', implode('|', $parts));
}
function get_account_for_transaction(int $userId, int $accountId): ?array
{
$pdo = db();
$stmt = $pdo->prepare("
SELECT *
FROM accounts
WHERE id = ?
AND user_id = ?
LIMIT 1
");
$stmt->execute([$accountId, $userId]);
$row = $stmt->fetch();
return $row ?: null;
}
function get_transaction_billing_year_month(int $userId, int $accountId, string $transactionDate): ?string
{
$account = get_account_for_transaction($userId, $accountId);
return $account ? get_card_billing_year_month($account, $transactionDate) : null;
}
function transaction_category_type(string $transactionType): string
{
if ($transactionType === 'income') {
return 'income';
}
if ($transactionType === 'expense') {
return 'expense';
}
return 'transfer';
}
function parse_money_input($value): float
{
return (float)str_replace(',', '', trim((string)$value));
}
function build_transaction_payload_from_input(array $input, int $userId): array
{
$pdo = db();
$transactionType = (string)($input['transaction_type'] ?? 'expense');
$accountId = (int)($input['account_id'] ?? 0);
$categoryId = (int)($input['category_id'] ?? 0);
$amount = parse_money_input($input['amount'] ?? 0);
$transactionDate = (string)($input['transaction_date'] ?? date('Y-m-d'));
$merchantName = trim((string)($input['merchant_name'] ?? ''));
$description = trim((string)($input['description'] ?? ''));
$relatedAccountId = !empty($input['related_account_id']) ? (int)$input['related_account_id'] : null;
$isInstallment = (int)($input['is_installment'] ?? 0);
$installmentMonths = trim((string)($input['installment_months'] ?? '')) !== ''
? (int)$input['installment_months']
: null;
$installmentInterestRate = trim((string)($input['installment_interest_rate'] ?? '0')) !== ''
? (float)$input['installment_interest_rate']
: 0.0;
$installmentInterestTotal = trim((string)($input['installment_interest_total'] ?? '')) !== ''
? parse_money_input($input['installment_interest_total'])
: null;
$installmentTotalBilled = trim((string)($input['installment_total_billed'] ?? '')) !== ''
? parse_money_input($input['installment_total_billed'])
: null;
if ($amount <= 0) {
throw new RuntimeException('금액은 0보다 커야 합니다.');
}
if (!in_array($transactionType, ['income', 'expense', 'transfer', 'card_payment'], true)) {
throw new RuntimeException('거래 유형이 올바르지 않습니다.');
}
if ($accountId <= 0) {
throw new RuntimeException('주 계좌/카드를 선택하세요.');
}
$stmt = $pdo->prepare("SELECT id FROM accounts WHERE id = ? AND user_id = ? AND is_active = 1 LIMIT 1");
$stmt->execute([$accountId, $userId]);
if (!$stmt->fetch()) {
throw new RuntimeException('선택한 주 계좌/카드를 사용할 수 없습니다.');
}
if ($categoryId <= 0) {
throw new RuntimeException('카테고리를 선택하세요.');
}
$stmt = $pdo->prepare("
SELECT category_type
FROM categories
WHERE id = ?
AND user_id = ?
AND is_active = 1
LIMIT 1
");
$stmt->execute([$categoryId, $userId]);
$category = $stmt->fetch();
if (!$category) {
throw new RuntimeException('선택한 카테고리를 사용할 수 없습니다.');
}
if ((string)$category['category_type'] !== transaction_category_type($transactionType)) {
throw new RuntimeException('거래 유형과 카테고리 유형이 일치하지 않습니다.');
}
if (in_array($transactionType, ['transfer', 'card_payment'], true) && !$relatedAccountId) {
throw new RuntimeException('관련 계좌를 선택해야 합니다.');
}
if ($relatedAccountId) {
if ($relatedAccountId === $accountId) {
throw new RuntimeException('주 계좌와 관련 계좌는 같을 수 없습니다.');
}
$stmt = $pdo->prepare("SELECT id FROM accounts WHERE id = ? AND user_id = ? AND is_active = 1 LIMIT 1");
$stmt->execute([$relatedAccountId, $userId]);
if (!$stmt->fetch()) {
throw new RuntimeException('선택한 관련 계좌를 사용할 수 없습니다.');
}
}
if ($transactionType !== 'expense') {
$isInstallment = 0;
$installmentMonths = null;
$installmentInterestRate = 0.0;
$installmentInterestTotal = null;
$installmentTotalBilled = null;
}
return [
'user_id' => $userId,
'account_id' => $accountId,
'category_id' => $categoryId,
'transaction_type' => $transactionType,
'amount' => $amount,
'transaction_date' => $transactionDate,
'merchant_name' => $merchantName !== '' ? $merchantName : null,
'description' => $description !== '' ? $description : null,
'related_account_id' => $relatedAccountId,
'is_installment' => $isInstallment,
'installment_months' => $installmentMonths,
'installment_interest_rate' => $installmentInterestRate,
'installment_interest_total' => $installmentInterestTotal,
'installment_total_billed' => $installmentTotalBilled,
];
}
function insert_transaction_record(array $data, bool $skipIfDuplicate = true): int
{
$pdo = db();
$sourceLoanId = !empty($data['source_loan_id']) ? (int)$data['source_loan_id'] : null;
$fingerprint = build_transaction_fingerprint(
(int)$data['user_id'],
(int)$data['account_id'],
!empty($data['related_account_id']) ? (int)$data['related_account_id'] : null,
(int)$data['category_id'],
(string)$data['transaction_type'],
(float)$data['amount'],
(string)$data['transaction_date'],
$data['merchant_name'] ?? null,
$data['description'] ?? null,
$sourceLoanId
);
if ($skipIfDuplicate) {
$stmt = $pdo->prepare("
SELECT id
FROM transactions
WHERE user_id = ?
AND fingerprint = ?
LIMIT 1
");
$stmt->execute([$data['user_id'], $fingerprint]);
$exists = $stmt->fetch();
if ($exists) {
return (int)$exists['id'];
}
}
$billingYearMonth = get_transaction_billing_year_month(
(int)$data['user_id'],
(int)$data['account_id'],
(string)$data['transaction_date']
);
$stmt = $pdo->prepare("
INSERT INTO transactions
(
user_id,
account_id,
category_id,
transaction_type,
amount,
is_installment,
installment_months,
installment_interest_rate,
installment_interest_total,
installment_total_billed,
installment_prepay_amount,
transaction_date,
billing_year_month,
merchant_name,
description,
related_account_id,
source_loan_id,
fingerprint
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['user_id'],
$data['account_id'],
$data['category_id'],
$data['transaction_type'],
$data['amount'],
!empty($data['is_installment']) ? 1 : 0,
$data['installment_months'] ?? null,
$data['installment_interest_rate'] ?? 0,
$data['installment_interest_total'] ?? 0,
$data['installment_total_billed'] ?? null,
$data['installment_prepay_amount'] ?? 0,
$data['transaction_date'],
$billingYearMonth,
$data['merchant_name'] ?? null,
$data['description'] ?? null,
$data['related_account_id'] ?? null,
$sourceLoanId,
$fingerprint,
]);
return (int)$pdo->lastInsertId();
}
+24 -123
View File
@@ -1,56 +1,14 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/installment_service.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/card_billing_service.php';
function build_transaction_fingerprint(
int $userId,
int $accountId,
?int $relatedAccountId,
int $categoryId,
string $transactionType,
float $amount,
string $transactionDate,
?string $merchantName,
?string $description
): string {
$raw = implode('|', [
$userId,
$accountId,
$relatedAccountId ?? 0,
$categoryId,
$transactionType,
number_format($amount, 2, '.', ''),
$transactionDate,
trim((string)$merchantName),
trim((string)$description),
]);
return hash('sha256', $raw);
}
function get_account_for_transaction(int $userId, int $accountId): ?array
{
$pdo = db();
$stmt = $pdo->prepare("
SELECT *
FROM accounts
WHERE id = ?
AND user_id = ?
LIMIT 1
");
$stmt->execute([$accountId, $userId]);
$row = $stmt->fetch();
return $row ?: null;
}
function create_transaction(array $data, bool $skipIfDuplicate = false): bool
{
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/installment_service.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/card_billing_service.php';
require_once __DIR__ . '/transaction_record_service.php';
function create_transaction(array $data, bool $skipIfDuplicate = false): bool
{
$pdo = db();
$pdo->beginTransaction();
@@ -83,20 +41,7 @@ function create_transaction(array $data, bool $skipIfDuplicate = false): bool
}
}
$account = get_account_for_transaction(
(int)$data['user_id'],
(int)$data['account_id']
);
$billingYearMonth = null;
if ($account) {
$billingYearMonth = get_card_billing_year_month(
$account,
(string)$data['transaction_date']
);
}
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
$installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0;
@@ -146,51 +91,14 @@ function create_transaction(array $data, bool $skipIfDuplicate = false): bool
$installmentTotalBilled = null;
}
$stmt = $pdo->prepare("
INSERT INTO transactions
(
user_id,
account_id,
category_id,
transaction_type,
amount,
is_installment,
installment_months,
installment_interest_rate,
installment_interest_total,
installment_total_billed,
installment_prepay_amount,
transaction_date,
billing_year_month,
merchant_name,
description,
related_account_id,
fingerprint
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['user_id'],
$data['account_id'],
$data['category_id'],
$data['transaction_type'],
$data['amount'],
$isInstallment,
$installmentMonths,
$installmentInterestRate,
$installmentInterestTotal,
$installmentTotalBilled,
$data['transaction_date'],
$billingYearMonth,
$data['merchant_name'],
$data['description'],
$data['related_account_id'],
$fingerprint,
]);
$transactionId = (int)$pdo->lastInsertId();
$pdo->commit();
$transactionId = insert_transaction_record(array_merge($data, [
'is_installment' => $isInstallment,
'installment_months' => $installmentMonths,
'installment_interest_rate' => $installmentInterestRate,
'installment_interest_total' => $installmentInterestTotal,
'installment_total_billed' => $installmentTotalBilled,
]), false);
$pdo->commit();
if (
$data['transaction_type'] === 'expense' &&
@@ -281,18 +189,11 @@ function update_transaction(int $transactionId, int $userId, array $data): void
$data['description'] ?? null
);
$account = get_account_for_transaction(
$userId,
(int)$data['account_id']
);
$billingYearMonth = null;
if ($account) {
$billingYearMonth = get_card_billing_year_month(
$account,
(string)$data['transaction_date']
);
}
$billingYearMonth = get_transaction_billing_year_month(
$userId,
(int)$data['account_id'],
(string)$data['transaction_date']
);
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
@@ -438,4 +339,4 @@ function delete_transaction(int $transactionId, int $userId): void
}
throw $e;
}
}
}
+12 -68
View File
@@ -118,73 +118,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'save_as_defaults' => !empty($_POST['save_as_defaults']) ? 1 : 0,
];
$transactionType = $form['transaction_type'];
$accountId = (int)$form['account_id'];
$categoryId = (int)$form['category_id'];
$amount = (float)str_replace(',', '', $form['amount']);
$transactionDate = $form['transaction_date'];
$merchantName = $form['merchant_name'];
$description = $form['description'];
$relatedAccountId = $form['related_account_id'] > 0 ? (int)$form['related_account_id'] : null;
$isInstallment = (int)$form['is_installment'];
$installmentMonths = $form['installment_months'] !== '' ? (int)$form['installment_months'] : null;
$installmentInterestRate = $form['installment_interest_rate'] !== '' ? (float)$form['installment_interest_rate'] : 0.0;
$installmentInterestTotal = $form['installment_interest_total'] !== ''
? (float)str_replace(',', '', $form['installment_interest_total'])
: null;
$installmentTotalBilled = $form['installment_total_billed'] !== ''
? (float)str_replace(',', '', $form['installment_total_billed'])
: null;
if ($amount <= 0) {
throw new RuntimeException('금액은 0보다 커야 합니다.');
}
if (!in_array($transactionType, ['income', 'expense', 'transfer', 'card_payment'], true)) {
throw new RuntimeException('거래 유형이 올바르지 않습니다.');
}
if ($accountId <= 0) {
throw new RuntimeException('주 계좌/카드를 선택하세요.');
}
if ($categoryId <= 0) {
throw new RuntimeException('카테고리를 선택하세요.');
}
if (in_array($transactionType, ['transfer', 'card_payment'], true) && !$relatedAccountId) {
throw new RuntimeException('관련 계좌를 선택해야 합니다.');
}
if ($relatedAccountId && $relatedAccountId === $accountId) {
throw new RuntimeException('주 계좌와 관련 계좌는 같을 수 없습니다.');
}
if ($transactionType !== 'expense') {
$isInstallment = 0;
$installmentMonths = null;
$installmentInterestRate = 0.0;
$installmentInterestTotal = null;
$installmentTotalBilled = null;
}
create_transaction([
'user_id' => $uid,
'account_id' => $accountId,
'category_id' => $categoryId,
'transaction_type' => $transactionType,
'amount' => $amount,
'transaction_date' => $transactionDate,
'merchant_name' => $merchantName !== '' ? $merchantName : null,
'description' => $description !== '' ? $description : null,
'related_account_id' => $relatedAccountId,
'is_installment' => $isInstallment,
'installment_months' => $installmentMonths,
'installment_interest_rate' => $installmentInterestRate,
'installment_interest_total' => $installmentInterestTotal,
'installment_total_billed' => $installmentTotalBilled,
]);
$payload = build_transaction_payload_from_input($form, $uid);
$transactionType = $payload['transaction_type'];
$accountId = (int)$payload['account_id'];
$categoryId = (int)$payload['category_id'];
$merchantName = (string)($payload['merchant_name'] ?? '');
$relatedAccountId = $payload['related_account_id'] ? (int)$payload['related_account_id'] : null;
$isInstallment = (int)$payload['is_installment'];
$installmentMonths = $payload['installment_months'];
$installmentInterestRate = (float)$payload['installment_interest_rate'];
create_transaction($payload);
if ($form['save_as_defaults']) {
$save = [
@@ -877,4 +821,4 @@ require __DIR__ . '/../app/views/header.php';
})();
</script>
<?php require __DIR__ . '/../app/views/footer.php'; ?>
<?php require __DIR__ . '/../app/views/footer.php'; ?>
+2 -49
View File
@@ -41,55 +41,8 @@ $categories = $stmt->fetchAll();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$transactionType = $_POST['transaction_type'] ?? '';
$accountId = (int)($_POST['account_id'] ?? 0);
$categoryId = (int)($_POST['category_id'] ?? 0);
$amount = (float)str_replace(',', '', (string)($_POST['amount'] ?? 0));
$transactionDate = $_POST['transaction_date'] ?? date('Y-m-d');
$merchantName = trim($_POST['merchant_name'] ?? '');
$description = trim($_POST['description'] ?? '');
$relatedAccountId = !empty($_POST['related_account_id']) ? (int)$_POST['related_account_id'] : null;
$isInstallment = (int)($_POST['is_installment'] ?? 0);
$installmentMonths = !empty($_POST['installment_months']) ? (int)$_POST['installment_months'] : null;
$installmentInterestRate = !empty($_POST['installment_interest_rate']) ? (float)$_POST['installment_interest_rate'] : 0.0;
$installmentInterestTotal = isset($_POST['installment_interest_total']) && $_POST['installment_interest_total'] !== ''
? (float)str_replace(',', '', (string)$_POST['installment_interest_total'])
: null;
$installmentTotalBilled = isset($_POST['installment_total_billed']) && $_POST['installment_total_billed'] !== ''
? (float)str_replace(',', '', (string)$_POST['installment_total_billed'])
: null;
if ($amount <= 0) throw new RuntimeException('금액은 0보다 커야 합니다.');
if (!in_array($transactionType, ['income', 'expense', 'transfer', 'card_payment'], true)) throw new RuntimeException('거래 유형이 올바르지 않습니다.');
if ($accountId <= 0) throw new RuntimeException('주 계좌/카드를 선택하세요.');
if ($categoryId <= 0) throw new RuntimeException('카테고리를 선택하세요.');
if (in_array($transactionType, ['transfer', 'card_payment'], true) && !$relatedAccountId) throw new RuntimeException('관련 계좌를 선택해야 합니다.');
if ($relatedAccountId && $relatedAccountId === $accountId) throw new RuntimeException('주 계좌와 관련 계좌는 같을 수 없습니다.');
if ($transactionType !== 'expense') {
$isInstallment = 0;
$installmentMonths = null;
$installmentInterestRate = 0.0;
$installmentInterestTotal = null;
$installmentTotalBilled = null;
}
update_transaction($id, $uid, [
'account_id' => $accountId,
'category_id' => $categoryId,
'transaction_type' => $transactionType,
'amount' => $amount,
'transaction_date' => $transactionDate,
'merchant_name' => $merchantName !== '' ? $merchantName : null,
'description' => $description !== '' ? $description : null,
'related_account_id' => $relatedAccountId,
'is_installment' => $isInstallment,
'installment_months' => $installmentMonths,
'installment_interest_rate' => $installmentInterestRate,
'installment_interest_total' => $installmentInterestTotal,
'installment_total_billed' => $installmentTotalBilled,
]);
$payload = build_transaction_payload_from_input($_POST, $uid);
update_transaction($id, $uid, $payload);
redirect('/transactions.php');
} catch (Throwable $e) {