880 lines
38 KiB
PHP
880 lines
38 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../app/lib/auth.php';
|
|
require_once __DIR__ . '/../app/lib/db.php';
|
|
require_once __DIR__ . '/../app/lib/helpers.php';
|
|
require_once __DIR__ . '/../app/lib/transaction_service.php';
|
|
require_once __DIR__ . '/../app/lib/transaction_form_service.php';
|
|
|
|
check_auth();
|
|
|
|
$pdo = db();
|
|
$uid = user_id();
|
|
$error = '';
|
|
$success = '';
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT *
|
|
FROM accounts
|
|
WHERE user_id = ?
|
|
AND is_active = 1
|
|
ORDER BY
|
|
FIELD(account_type, 'bank', 'card', 'cash', 'other'),
|
|
id ASC
|
|
");
|
|
$stmt->execute([$uid]);
|
|
$accounts = $stmt->fetchAll();
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT *
|
|
FROM categories
|
|
WHERE user_id = ?
|
|
AND is_active = 1
|
|
ORDER BY category_type, sort_order, id
|
|
");
|
|
$stmt->execute([$uid]);
|
|
$categories = $stmt->fetchAll();
|
|
|
|
$defaults = get_transaction_form_defaults($uid);
|
|
$lastForm = $_SESSION['tx_create_last_form'] ?? [];
|
|
|
|
$form = [
|
|
'transaction_type' => 'expense',
|
|
'account_id' => (int)($defaults['default_account_id'] ?? 0),
|
|
'related_account_id' => 0,
|
|
'category_id' => (int)($defaults['default_expense_category_id'] ?? 0),
|
|
'amount' => '',
|
|
'transaction_date' => date('Y-m-d'),
|
|
'merchant_name' => '',
|
|
'description' => '',
|
|
'is_installment' => 0,
|
|
'installment_months' => '',
|
|
'installment_interest_rate' => '0',
|
|
'installment_interest_total' => '',
|
|
'installment_total_billed' => '',
|
|
'continue_after_save' => !empty($defaults['continue_after_save']) ? 1 : 0,
|
|
'keep_last_values' => !empty($defaults['keep_last_values']) ? 1 : 0,
|
|
'save_as_defaults' => 0,
|
|
];
|
|
|
|
if (!empty($defaults['keep_last_values']) && !empty($lastForm)) {
|
|
$form = array_merge($form, $lastForm);
|
|
}
|
|
|
|
$copyId = (int)($_GET['copy_id'] ?? 0);
|
|
|
|
if ($copyId > 0 && $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$stmt = $pdo->prepare("
|
|
SELECT *
|
|
FROM transactions
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([$copyId, $uid]);
|
|
$copy = $stmt->fetch();
|
|
|
|
if ($copy) {
|
|
$form = array_merge($form, [
|
|
'transaction_type' => $copy['transaction_type'],
|
|
'account_id' => (int)$copy['account_id'],
|
|
'related_account_id' => (int)($copy['related_account_id'] ?? 0),
|
|
'category_id' => (int)$copy['category_id'],
|
|
'amount' => number_format((float)$copy['amount'], 0),
|
|
'transaction_date' => date('Y-m-d'),
|
|
'merchant_name' => (string)($copy['merchant_name'] ?? ''),
|
|
'description' => (string)($copy['description'] ?? ''),
|
|
'is_installment' => (int)($copy['is_installment'] ?? 0),
|
|
'installment_months' => $copy['installment_months'] ?? '',
|
|
'installment_interest_rate' => (string)($copy['installment_interest_rate'] ?? '0'),
|
|
'installment_interest_total' => !empty($copy['installment_interest_total']) ? number_format((float)$copy['installment_interest_total'], 0) : '',
|
|
'installment_total_billed' => !empty($copy['installment_total_billed']) ? number_format((float)$copy['installment_total_billed'], 0) : '',
|
|
'continue_after_save' => 0,
|
|
'keep_last_values' => 0,
|
|
'save_as_defaults' => 0,
|
|
]);
|
|
|
|
$success = '기존 거래를 복사했습니다. 날짜/메모 수정 후 저장하세요.';
|
|
}
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
try {
|
|
$form = [
|
|
'transaction_type' => $_POST['transaction_type'] ?? 'expense',
|
|
'account_id' => (int)($_POST['account_id'] ?? 0),
|
|
'related_account_id' => !empty($_POST['related_account_id']) ? (int)$_POST['related_account_id'] : 0,
|
|
'category_id' => (int)($_POST['category_id'] ?? 0),
|
|
'amount' => trim((string)($_POST['amount'] ?? '')),
|
|
'transaction_date' => $_POST['transaction_date'] ?? date('Y-m-d'),
|
|
'merchant_name' => trim((string)($_POST['merchant_name'] ?? '')),
|
|
'description' => trim((string)($_POST['description'] ?? '')),
|
|
'is_installment' => (int)($_POST['is_installment'] ?? 0),
|
|
'installment_months' => trim((string)($_POST['installment_months'] ?? '')),
|
|
'installment_interest_rate' => trim((string)($_POST['installment_interest_rate'] ?? '0')),
|
|
'installment_interest_total' => trim((string)($_POST['installment_interest_total'] ?? '')),
|
|
'installment_total_billed' => trim((string)($_POST['installment_total_billed'] ?? '')),
|
|
'continue_after_save' => !empty($_POST['continue_after_save']) ? 1 : 0,
|
|
'keep_last_values' => !empty($_POST['keep_last_values']) ? 1 : 0,
|
|
'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,
|
|
]);
|
|
|
|
if ($form['save_as_defaults']) {
|
|
$save = [
|
|
'default_account_id' => 0,
|
|
'default_card_account_id' => 0,
|
|
'default_income_category_id' => 0,
|
|
'default_expense_category_id' => 0,
|
|
'default_transfer_category_id' => 0,
|
|
'default_card_payment_category_id' => 0,
|
|
'keep_last_values' => $form['keep_last_values'],
|
|
'continue_after_save' => $form['continue_after_save'],
|
|
];
|
|
|
|
if ($transactionType === 'income') {
|
|
$save['default_account_id'] = $accountId;
|
|
$save['default_income_category_id'] = $categoryId;
|
|
} elseif ($transactionType === 'expense') {
|
|
$save['default_account_id'] = $accountId;
|
|
$save['default_expense_category_id'] = $categoryId;
|
|
} elseif ($transactionType === 'transfer') {
|
|
$save['default_account_id'] = $accountId;
|
|
$save['default_transfer_category_id'] = $categoryId;
|
|
} elseif ($transactionType === 'card_payment') {
|
|
$save['default_account_id'] = $accountId;
|
|
$save['default_card_payment_category_id'] = $categoryId;
|
|
}
|
|
|
|
foreach ($accounts as $acc) {
|
|
if ((int)$acc['id'] === $accountId && $acc['account_type'] === 'card') {
|
|
$save['default_card_account_id'] = $accountId;
|
|
break;
|
|
}
|
|
}
|
|
|
|
save_transaction_form_defaults($uid, array_merge($defaults, $save));
|
|
}
|
|
|
|
if ($form['keep_last_values']) {
|
|
$_SESSION['tx_create_last_form'] = [
|
|
'transaction_type' => $transactionType,
|
|
'account_id' => $accountId,
|
|
'related_account_id' => $relatedAccountId ?: 0,
|
|
'category_id' => $categoryId,
|
|
'amount' => '',
|
|
'transaction_date' => date('Y-m-d'),
|
|
'merchant_name' => $merchantName,
|
|
'description' => '',
|
|
'is_installment' => $isInstallment,
|
|
'installment_months' => $installmentMonths ?? '',
|
|
'installment_interest_rate' => (string)$installmentInterestRate,
|
|
'installment_interest_total' => '',
|
|
'installment_total_billed' => '',
|
|
'continue_after_save' => $form['continue_after_save'],
|
|
'keep_last_values' => $form['keep_last_values'],
|
|
'save_as_defaults' => 0,
|
|
];
|
|
} else {
|
|
unset($_SESSION['tx_create_last_form']);
|
|
}
|
|
|
|
if ($form['continue_after_save']) {
|
|
$success = '저장되었습니다. 계속 등록할 수 있습니다.';
|
|
$form['amount'] = '';
|
|
$form['description'] = '';
|
|
$form['transaction_date'] = date('Y-m-d');
|
|
$form['installment_interest_total'] = '';
|
|
$form['installment_total_billed'] = '';
|
|
} else {
|
|
redirect('/transactions.php');
|
|
}
|
|
} catch (Throwable $e) {
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
$accountsJson = [];
|
|
foreach ($accounts as $acc) {
|
|
$accountsJson[] = [
|
|
'id' => (int)$acc['id'],
|
|
'name' => $acc['account_name'],
|
|
'type' => $acc['account_type'],
|
|
'card_kind' => $acc['card_kind'] ?? null,
|
|
'billing_day' => $acc['billing_day'] ?? null,
|
|
'payment_day' => $acc['payment_day'] ?? null,
|
|
'use_credit_grace_period' => !empty($acc['use_credit_grace_period']) ? 1 : 0,
|
|
];
|
|
}
|
|
|
|
$categoriesJson = [];
|
|
foreach ($categories as $cat) {
|
|
$categoriesJson[] = [
|
|
'id' => (int)$cat['id'],
|
|
'type' => $cat['category_type'],
|
|
'name' => $cat['name'],
|
|
];
|
|
}
|
|
|
|
require __DIR__ . '/../app/views/header.php';
|
|
?>
|
|
|
|
<div class="page-head">
|
|
<h2>거래 등록</h2>
|
|
<a href="/transactions.php" class="btn btn-outline-secondary">목록</a>
|
|
</div>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger"><?= h($error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($success): ?>
|
|
<div class="alert alert-success"><?= h($success) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card finance-card">
|
|
<div class="card-body">
|
|
<form method="post" class="row g-3" id="transactionForm" autocomplete="off">
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">거래 유형</label>
|
|
<select name="transaction_type" id="transaction_type" class="form-select" required>
|
|
<option value="expense" <?= $form['transaction_type'] === 'expense' ? 'selected' : '' ?>>지출</option>
|
|
<option value="income" <?= $form['transaction_type'] === 'income' ? 'selected' : '' ?>>수입</option>
|
|
<option value="transfer" <?= $form['transaction_type'] === 'transfer' ? 'selected' : '' ?>>계좌이체</option>
|
|
<option value="card_payment" <?= $form['transaction_type'] === 'card_payment' ? 'selected' : '' ?>>카드대금납부</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">주 계좌/카드</label>
|
|
<select name="account_id" id="account_id" class="form-select" required>
|
|
<option value="0">선택하세요</option>
|
|
<?php foreach ($accounts as $a): ?>
|
|
<option
|
|
value="<?= $a['id'] ?>"
|
|
data-account-type="<?= h($a['account_type']) ?>"
|
|
<?= (int)$form['account_id'] === (int)$a['id'] ? 'selected' : '' ?>
|
|
>
|
|
<?= h($a['account_name']) ?> (<?= h($a['account_type']) ?>)
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<div class="form-text" id="account_help_text"></div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">관련 계좌</label>
|
|
<select name="related_account_id" id="related_account_id" class="form-select">
|
|
<option value="">선택안함</option>
|
|
<?php foreach ($accounts as $a): ?>
|
|
<option
|
|
value="<?= $a['id'] ?>"
|
|
data-account-type="<?= h($a['account_type']) ?>"
|
|
<?= (int)$form['related_account_id'] === (int)$a['id'] ? 'selected' : '' ?>
|
|
>
|
|
<?= h($a['account_name']) ?> (<?= h($a['account_type']) ?>)
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<div class="form-text" id="related_account_help">계좌이체/카드대금납부 시 선택</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">카테고리</label>
|
|
<select name="category_id" id="category_id" class="form-select" required>
|
|
<option value="0">선택하세요</option>
|
|
<?php foreach ($categories as $c): ?>
|
|
<option
|
|
value="<?= $c['id'] ?>"
|
|
data-type="<?= h($c['category_type']) ?>"
|
|
<?= (int)$form['category_id'] === (int)$c['id'] ? 'selected' : '' ?>
|
|
>
|
|
[<?= h($c['category_type']) ?>] <?= h($c['name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">금액</label>
|
|
<input
|
|
type="text"
|
|
name="amount"
|
|
id="amount"
|
|
class="form-control"
|
|
value="<?= h($form['amount']) ?>"
|
|
inputmode="numeric"
|
|
required
|
|
>
|
|
<div class="mt-2 d-flex flex-wrap gap-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary quick-amt" data-add="10000">+1만</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary quick-amt" data-add="50000">+5만</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary quick-amt" data-add="100000">+10만</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="amount_clear_btn">초기화</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">거래일</label>
|
|
<input type="date" name="transaction_date" class="form-control" value="<?= h($form['transaction_date']) ?>" required>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3">
|
|
<label class="form-label">결제 방식</label>
|
|
<select name="is_installment" id="is_installment" class="form-select">
|
|
<option value="0" <?= (int)$form['is_installment'] === 0 ? 'selected' : '' ?>>일시불</option>
|
|
<option value="1" <?= (int)$form['is_installment'] === 1 ? 'selected' : '' ?>>할부</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3" id="installment_months_wrap" style="display:none;">
|
|
<label class="form-label">할부 개월</label>
|
|
<select name="installment_months" id="installment_months" class="form-select">
|
|
<option value="">선택하세요</option>
|
|
<?php foreach ([2,3,4,5,6,10,12,18,24] as $m): ?>
|
|
<option value="<?= $m ?>" <?= (string)$form['installment_months'] === (string)$m ? 'selected' : '' ?>>
|
|
<?= intvalf($m) ?>개월
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3" id="installment_rate_wrap" style="display:none;">
|
|
<label class="form-label">연이자율(%)</label>
|
|
<input
|
|
type="number"
|
|
name="installment_interest_rate"
|
|
id="installment_interest_rate"
|
|
class="form-control"
|
|
step="0.01"
|
|
min="0"
|
|
value="<?= h($form['installment_interest_rate']) ?>"
|
|
>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3" id="installment_interest_wrap" style="display:none;">
|
|
<label class="form-label">총 할부이자</label>
|
|
<input
|
|
type="text"
|
|
name="installment_interest_total"
|
|
id="installment_interest_total"
|
|
class="form-control"
|
|
inputmode="numeric"
|
|
value="<?= h($form['installment_interest_total']) ?>"
|
|
>
|
|
<div class="form-text">비우면 연이자율로 자동 계산</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3" id="installment_total_wrap" style="display:none;">
|
|
<label class="form-label">총 청구금액</label>
|
|
<input
|
|
type="text"
|
|
name="installment_total_billed"
|
|
id="installment_total_billed"
|
|
class="form-control"
|
|
inputmode="numeric"
|
|
value="<?= h($form['installment_total_billed']) ?>"
|
|
>
|
|
<div class="form-text">비우면 원금+이자로 자동 계산</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label">사용처 / 거래처</label>
|
|
<input
|
|
type="text"
|
|
name="merchant_name"
|
|
id="merchant_name"
|
|
class="form-control"
|
|
placeholder="예: 스타벅스, 쿠팡, 급여"
|
|
autocomplete="off"
|
|
value="<?= h($form['merchant_name']) ?>"
|
|
>
|
|
<div class="form-text">상호명 입력 시 카테고리를 자동 추천합니다.</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label">메모</label>
|
|
<input
|
|
type="text"
|
|
name="description"
|
|
class="form-control"
|
|
placeholder="간단한 설명"
|
|
value="<?= h($form['description']) ?>"
|
|
>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div id="category_suggest_box" class="alert alert-info py-2 px-3 d-none mb-0"></div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="loan-create-highlight">
|
|
<div class="title">등록 편의 설정</div>
|
|
<div class="desc">자주 쓰는 카드/계좌와 입력값을 계속 재사용할 수 있습니다.</div>
|
|
|
|
<div class="row g-3 mt-1">
|
|
<div class="col-12 col-md-4">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="continue_after_save" id="continue_after_save" value="1" <?= !empty($form['continue_after_save']) ? 'checked' : '' ?>>
|
|
<label class="form-check-label" for="continue_after_save">저장 후 계속 등록</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-4">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="keep_last_values" id="keep_last_values" value="1" <?= !empty($form['keep_last_values']) ? 'checked' : '' ?>>
|
|
<label class="form-check-label" for="keep_last_values">마지막 입력값 유지</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-4">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="save_as_defaults" id="save_as_defaults" value="1">
|
|
<label class="form-check-label" for="save_as_defaults">현재 선택을 기본값으로 저장</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 d-flex flex-wrap gap-2">
|
|
<button class="btn btn-primary">저장</button>
|
|
<button type="submit" name="continue_after_save" value="1" class="btn btn-outline-primary">저장 후 계속 등록</button>
|
|
<a href="/transactions.php" class="btn btn-outline-secondary">목록</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const ACCOUNTS = <?= json_encode($accountsJson, JSON_UNESCAPED_UNICODE) ?>;
|
|
const CATEGORIES = <?= json_encode($categoriesJson, JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
const transactionTypeEl = document.getElementById('transaction_type');
|
|
const accountEl = document.getElementById('account_id');
|
|
const accountHelpTextEl = document.getElementById('account_help_text');
|
|
const relatedAccountEl = document.getElementById('related_account_id');
|
|
const relatedHelpEl = document.getElementById('related_account_help');
|
|
const categoryEl = document.getElementById('category_id');
|
|
const merchantEl = document.getElementById('merchant_name');
|
|
const suggestBoxEl = document.getElementById('category_suggest_box');
|
|
|
|
const installmentEl = document.getElementById('is_installment');
|
|
const installmentWrapEl = document.getElementById('installment_months_wrap');
|
|
const installmentMonthsEl = document.getElementById('installment_months');
|
|
const installmentRateWrapEl = document.getElementById('installment_rate_wrap');
|
|
const installmentRateEl = document.getElementById('installment_interest_rate');
|
|
const installmentInterestWrapEl = document.getElementById('installment_interest_wrap');
|
|
const installmentTotalWrapEl = document.getElementById('installment_total_wrap');
|
|
const installmentInterestEl = document.getElementById('installment_interest_total');
|
|
const installmentTotalEl = document.getElementById('installment_total_billed');
|
|
const amountEl = document.getElementById('amount');
|
|
|
|
const quickAmtButtons = document.querySelectorAll('.quick-amt');
|
|
const amountClearBtn = document.getElementById('amount_clear_btn');
|
|
|
|
const allCategoryOptions = Array.from(categoryEl.querySelectorAll('option')).map(option => ({
|
|
value: option.value,
|
|
text: option.textContent,
|
|
type: option.dataset.type
|
|
}));
|
|
|
|
let suggestTimer = null;
|
|
let lastSuggestedCategoryId = null;
|
|
|
|
function parseNumber(value) {
|
|
const normalized = String(value || '').replace(/,/g, '').trim();
|
|
const n = parseFloat(normalized);
|
|
return isNaN(n) ? 0 : n;
|
|
}
|
|
|
|
function formatWithComma(value) {
|
|
const normalized = String(value || '').replace(/,/g, '').replace(/[^\d.]/g, '');
|
|
if (normalized === '') return '';
|
|
const parts = normalized.split('.');
|
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
return parts.join('.');
|
|
}
|
|
|
|
function selectedAccount() {
|
|
const id = parseInt(accountEl.value || '0', 10);
|
|
return ACCOUNTS.find(a => a.id === id) || null;
|
|
}
|
|
|
|
function mapTransactionTypeToCategoryType(transactionType) {
|
|
if (transactionType === 'income') return 'income';
|
|
if (transactionType === 'expense') return 'expense';
|
|
return 'transfer';
|
|
}
|
|
|
|
function rebuildCategoryOptions() {
|
|
const transactionType = transactionTypeEl.value;
|
|
const categoryType = mapTransactionTypeToCategoryType(transactionType);
|
|
const currentValue = categoryEl.value;
|
|
const filtered = allCategoryOptions.filter(item => item.type === categoryType);
|
|
|
|
categoryEl.innerHTML = '<option value="0">선택하세요</option>';
|
|
|
|
filtered.forEach((item) => {
|
|
const option = document.createElement('option');
|
|
option.value = item.value;
|
|
option.textContent = item.text;
|
|
if (item.value === currentValue) option.selected = true;
|
|
categoryEl.appendChild(option);
|
|
});
|
|
|
|
if (![...categoryEl.options].some(opt => opt.value === currentValue)) {
|
|
categoryEl.value = '0';
|
|
}
|
|
}
|
|
|
|
function filterRelatedAccountOptions() {
|
|
const transactionType = transactionTypeEl.value;
|
|
const mainAccountType = accountEl.selectedOptions[0]?.dataset.accountType || '';
|
|
const mainAccountId = accountEl.value;
|
|
|
|
Array.from(relatedAccountEl.options).forEach(option => {
|
|
if (option.value === '') {
|
|
option.hidden = false;
|
|
return;
|
|
}
|
|
|
|
const optionType = option.dataset.accountType || '';
|
|
let visible = true;
|
|
|
|
if (option.value === mainAccountId) {
|
|
visible = false;
|
|
} else if (transactionType === 'transfer') {
|
|
visible =
|
|
['bank', 'cash', 'other'].includes(mainAccountType) &&
|
|
['bank', 'cash', 'other'].includes(optionType);
|
|
} else if (transactionType === 'card_payment') {
|
|
visible =
|
|
['bank', 'cash', 'other'].includes(mainAccountType) &&
|
|
optionType === 'card';
|
|
}
|
|
|
|
option.hidden = !visible;
|
|
|
|
if (!visible && option.selected) {
|
|
relatedAccountEl.value = '';
|
|
}
|
|
});
|
|
|
|
if (transactionType === 'transfer') {
|
|
relatedAccountEl.disabled = false;
|
|
relatedAccountEl.required = true;
|
|
relatedHelpEl.textContent = '같은 주 계좌는 제외되고, 입금 계좌만 선택 가능합니다.';
|
|
} else if (transactionType === 'card_payment') {
|
|
relatedAccountEl.disabled = false;
|
|
relatedAccountEl.required = true;
|
|
relatedHelpEl.textContent = '관련 계좌에는 카드 계정만 선택 가능합니다.';
|
|
} else {
|
|
relatedAccountEl.disabled = true;
|
|
relatedAccountEl.required = false;
|
|
relatedAccountEl.value = '';
|
|
relatedHelpEl.textContent = '계좌이체/카드대금납부 시 선택';
|
|
}
|
|
}
|
|
|
|
function updateAccountHelpText() {
|
|
const acc = selectedAccount();
|
|
if (!acc) {
|
|
accountHelpTextEl.textContent = '';
|
|
return;
|
|
}
|
|
|
|
if (acc.type === 'card' && acc.card_kind === 'check') {
|
|
accountHelpTextEl.textContent = '체크카드 · 즉시 출금 기준';
|
|
return;
|
|
}
|
|
|
|
if (acc.type === 'card' && acc.card_kind === 'credit' && acc.use_credit_grace_period) {
|
|
const billingDay = acc.billing_day ? `${acc.billing_day}일` : '-';
|
|
const paymentDay = acc.payment_day ? `${acc.payment_day}일` : '-';
|
|
accountHelpTextEl.textContent = `신용카드 · 결제기준일 ${billingDay} / 납부일 ${paymentDay}`;
|
|
return;
|
|
}
|
|
|
|
accountHelpTextEl.textContent = '';
|
|
}
|
|
|
|
function calculateInterest() {
|
|
const principal = parseNumber(amountEl.value);
|
|
const months = parseInt(installmentMonthsEl.value || '0', 10) || 0;
|
|
const annualRate = parseFloat(installmentRateEl.value || '0') || 0;
|
|
|
|
if (principal <= 0 || months <= 1 || annualRate <= 0) {
|
|
if (!installmentInterestEl.dataset.userEdited) {
|
|
installmentInterestEl.value = '';
|
|
}
|
|
syncInstallmentTotal();
|
|
return;
|
|
}
|
|
|
|
const monthlyRate = (annualRate / 100) / 12;
|
|
const averageOutstanding = principal / 2;
|
|
const interestTotal = Math.round((averageOutstanding * monthlyRate * months));
|
|
|
|
if (!installmentInterestEl.dataset.userEdited || installmentInterestEl.value === '') {
|
|
installmentInterestEl.value = formatWithComma(interestTotal);
|
|
}
|
|
|
|
syncInstallmentTotal();
|
|
}
|
|
|
|
function syncInstallmentTotal() {
|
|
const principal = parseNumber(amountEl.value);
|
|
const interest = parseNumber(installmentInterestEl.value);
|
|
const calculated = principal + interest;
|
|
|
|
if (!installmentTotalEl.dataset.userEdited || installmentTotalEl.value === '') {
|
|
installmentTotalEl.value = calculated > 0 ? formatWithComma(calculated) : '';
|
|
}
|
|
}
|
|
|
|
function toggleInstallmentFields() {
|
|
const transactionType = transactionTypeEl.value;
|
|
const acc = selectedAccount();
|
|
const mainAccountType = acc ? acc.type : '';
|
|
const isExpenseCard = (transactionType === 'expense' && mainAccountType === 'card');
|
|
|
|
if (isExpenseCard) {
|
|
installmentEl.disabled = false;
|
|
|
|
if (String(installmentEl.value) === '1') {
|
|
installmentWrapEl.style.display = '';
|
|
installmentRateWrapEl.style.display = '';
|
|
installmentInterestWrapEl.style.display = '';
|
|
installmentTotalWrapEl.style.display = '';
|
|
installmentMonthsEl.required = true;
|
|
calculateInterest();
|
|
} else {
|
|
installmentWrapEl.style.display = 'none';
|
|
installmentRateWrapEl.style.display = 'none';
|
|
installmentInterestWrapEl.style.display = 'none';
|
|
installmentTotalWrapEl.style.display = 'none';
|
|
installmentMonthsEl.required = false;
|
|
installmentMonthsEl.value = '';
|
|
installmentRateEl.value = '0';
|
|
installmentInterestEl.value = '';
|
|
installmentTotalEl.value = '';
|
|
installmentInterestEl.dataset.userEdited = '';
|
|
installmentTotalEl.dataset.userEdited = '';
|
|
}
|
|
} else {
|
|
installmentEl.value = '0';
|
|
installmentEl.disabled = true;
|
|
installmentWrapEl.style.display = 'none';
|
|
installmentRateWrapEl.style.display = 'none';
|
|
installmentInterestWrapEl.style.display = 'none';
|
|
installmentTotalWrapEl.style.display = 'none';
|
|
installmentMonthsEl.required = false;
|
|
installmentMonthsEl.value = '';
|
|
installmentRateEl.value = '0';
|
|
installmentInterestEl.value = '';
|
|
installmentTotalEl.value = '';
|
|
installmentInterestEl.dataset.userEdited = '';
|
|
installmentTotalEl.dataset.userEdited = '';
|
|
}
|
|
}
|
|
|
|
function hideSuggestBox() {
|
|
suggestBoxEl.classList.add('d-none');
|
|
suggestBoxEl.textContent = '';
|
|
lastSuggestedCategoryId = null;
|
|
}
|
|
|
|
function showSuggestBox(message) {
|
|
suggestBoxEl.textContent = message;
|
|
suggestBoxEl.classList.remove('d-none');
|
|
}
|
|
|
|
async function fetchCategorySuggestion() {
|
|
const merchantName = merchantEl.value.trim();
|
|
const transactionType = transactionTypeEl.value;
|
|
const categoryType = mapTransactionTypeToCategoryType(transactionType);
|
|
|
|
if (merchantName.length < 2) {
|
|
hideSuggestBox();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const url = `/api/category_suggest.php?merchant_name=${encodeURIComponent(merchantName)}&transaction_type=${encodeURIComponent(categoryType)}`;
|
|
const response = await fetch(url, {
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
hideSuggestBox();
|
|
return;
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.ok || !result.found) {
|
|
hideSuggestBox();
|
|
return;
|
|
}
|
|
|
|
const targetOption = Array.from(categoryEl.options).find(option => option.value === String(result.category_id));
|
|
if (!targetOption) {
|
|
hideSuggestBox();
|
|
return;
|
|
}
|
|
|
|
categoryEl.value = String(result.category_id);
|
|
lastSuggestedCategoryId = String(result.category_id);
|
|
showSuggestBox(`자동 추천: ${result.category_name} (규칙: ${result.keyword ?? result.pattern_text ?? ''})`);
|
|
} catch (error) {
|
|
hideSuggestBox();
|
|
}
|
|
}
|
|
|
|
function scheduleSuggestion() {
|
|
clearTimeout(suggestTimer);
|
|
suggestTimer = setTimeout(fetchCategorySuggestion, 250);
|
|
}
|
|
|
|
quickAmtButtons.forEach(btn => {
|
|
btn.addEventListener('click', function () {
|
|
const current = parseNumber(amountEl.value);
|
|
const add = parseInt(btn.dataset.add || '0', 10) || 0;
|
|
amountEl.value = formatWithComma(current + add);
|
|
calculateInterest();
|
|
});
|
|
});
|
|
|
|
amountClearBtn.addEventListener('click', function () {
|
|
amountEl.value = '';
|
|
calculateInterest();
|
|
amountEl.focus();
|
|
});
|
|
|
|
amountEl.addEventListener('input', function () {
|
|
const cursorAtEnd = this.selectionStart === this.value.length;
|
|
this.value = formatWithComma(this.value);
|
|
if (cursorAtEnd) {
|
|
this.setSelectionRange(this.value.length, this.value.length);
|
|
}
|
|
calculateInterest();
|
|
});
|
|
|
|
installmentInterestEl.addEventListener('input', function () {
|
|
installmentInterestEl.dataset.userEdited = '1';
|
|
installmentInterestEl.value = formatWithComma(installmentInterestEl.value);
|
|
syncInstallmentTotal();
|
|
});
|
|
|
|
installmentTotalEl.addEventListener('input', function () {
|
|
installmentTotalEl.dataset.userEdited = '1';
|
|
installmentTotalEl.value = formatWithComma(installmentTotalEl.value);
|
|
});
|
|
|
|
transactionTypeEl.addEventListener('change', function () {
|
|
rebuildCategoryOptions();
|
|
filterRelatedAccountOptions();
|
|
toggleInstallmentFields();
|
|
scheduleSuggestion();
|
|
});
|
|
|
|
accountEl.addEventListener('change', function () {
|
|
updateAccountHelpText();
|
|
filterRelatedAccountOptions();
|
|
toggleInstallmentFields();
|
|
});
|
|
|
|
installmentEl.addEventListener('change', toggleInstallmentFields);
|
|
installmentMonthsEl.addEventListener('change', calculateInterest);
|
|
installmentRateEl.addEventListener('input', calculateInterest);
|
|
|
|
merchantEl.addEventListener('input', scheduleSuggestion);
|
|
|
|
categoryEl.addEventListener('change', function () {
|
|
if (lastSuggestedCategoryId && categoryEl.value !== lastSuggestedCategoryId) {
|
|
hideSuggestBox();
|
|
}
|
|
});
|
|
|
|
document.getElementById('transactionForm').addEventListener('submit', function () {
|
|
amountEl.value = String(parseNumber(amountEl.value));
|
|
installmentInterestEl.value = installmentInterestEl.value === '' ? '' : String(parseNumber(installmentInterestEl.value));
|
|
installmentTotalEl.value = installmentTotalEl.value === '' ? '' : String(parseNumber(installmentTotalEl.value));
|
|
});
|
|
|
|
rebuildCategoryOptions();
|
|
updateAccountHelpText();
|
|
filterRelatedAccountOptions();
|
|
toggleInstallmentFields();
|
|
})();
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/../app/views/footer.php'; ?>
|