657 lines
29 KiB
PHP
657 lines
29 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';
|
|
|
|
check_auth();
|
|
|
|
$pdo = db();
|
|
$uid = user_id();
|
|
$id = (int)($_GET['id'] ?? 0);
|
|
$error = '';
|
|
|
|
$stmt = $pdo->prepare("SELECT * FROM transactions WHERE id = ? AND user_id = ?");
|
|
$stmt->execute([$id, $uid]);
|
|
$item = $stmt->fetch();
|
|
|
|
if (!$item) {
|
|
exit('거래를 찾을 수 없습니다.');
|
|
}
|
|
|
|
$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();
|
|
|
|
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,
|
|
]);
|
|
|
|
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,
|
|
];
|
|
}
|
|
|
|
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; ?>
|
|
|
|
<div class="card finance-card">
|
|
<div class="card-body">
|
|
<form method="post" class="row g-3" id="transactionEditForm" 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" <?= $item['transaction_type'] === 'expense' ? 'selected' : '' ?>>지출</option>
|
|
<option value="income" <?= $item['transaction_type'] === 'income' ? 'selected' : '' ?>>수입</option>
|
|
<option value="transfer" <?= $item['transaction_type'] === 'transfer' ? 'selected' : '' ?>>계좌이체</option>
|
|
<option value="card_payment" <?= $item['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>
|
|
<?php foreach ($accounts as $a): ?>
|
|
<option
|
|
value="<?= $a['id'] ?>"
|
|
data-account-type="<?= h($a['account_type']) ?>"
|
|
<?= (int)$item['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)($item['related_account_id'] ?? 0) === (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>
|
|
<?php foreach ($categories as $c): ?>
|
|
<option
|
|
value="<?= $c['id'] ?>"
|
|
data-type="<?= h($c['category_type']) ?>"
|
|
<?= (int)$item['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(money_plain($item['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($item['transaction_date']) ?>" required>
|
|
<?php if (!empty($item['billing_year_month'])): ?>
|
|
<div class="form-text">현재 청구월: <?= h($item['billing_year_month']) ?></div>
|
|
<?php endif; ?>
|
|
</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" <?= empty($item['is_installment']) ? 'selected' : '' ?>>일시불</option>
|
|
<option value="1" <?= !empty($item['is_installment']) ? '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 $month): ?>
|
|
<option value="<?= $month ?>" <?= (int)($item['installment_months'] ?? 0) === $month ? 'selected' : '' ?>>
|
|
<?= intvalf($month) ?>개월
|
|
</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((string)($item['installment_interest_rate'] ?? 0)) ?>">
|
|
</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(money_plain($item['installment_interest_total'] ?? 0)) ?>">
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3"
|
|
id="installment_total_wrap"
|
|
style="display:<?= !empty($item['is_installment']) ? 'block' : 'none' ?>;">
|
|
|
|
<label class="form-label">총 청구금액</label>
|
|
|
|
<input type="text"
|
|
name="installment_total_billed"
|
|
id="installment_total_billed"
|
|
class="form-control"
|
|
inputmode="numeric"
|
|
value="<?= h(!empty($item['installment_total_billed']) ? money_plain($item['installment_total_billed']) : '') ?>">
|
|
|
|
<?php if (!empty($item['is_installment'])): ?>
|
|
<div class="form-text text-primary">
|
|
현재 <?= intvalf($item['installment_months']) ?>개월 /
|
|
<?= ((float)$item['installment_interest_rate'] > 0)
|
|
? '연 ' . numf($item['installment_interest_rate'],2) . '%'
|
|
: '무이자' ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<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" value="<?= h($item['merchant_name'] ?? '') ?>" autocomplete="off">
|
|
<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" value="<?= h($item['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 d-flex flex-wrap gap-2">
|
|
<button class="btn btn-primary">수정 저장</button>
|
|
|
|
<a href="/transactions.php" class="btn btn-outline-secondary">목록</a>
|
|
|
|
<button type="submit"
|
|
formaction="/transaction_delete.php"
|
|
formmethod="post"
|
|
name="id"
|
|
value="<?= (int)$id ?>"
|
|
class="btn btn-outline-danger"
|
|
onclick="return confirm('거래를 삭제하시겠습니까?');">
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const ACCOUNTS = <?= json_encode($accountsJson, 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,
|
|
selected: option.selected
|
|
}));
|
|
|
|
const initialCategoryValue = categoryEl.value;
|
|
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 || initialCategoryValue;
|
|
const filtered = allCategoryOptions.filter(item => item.type === categoryType);
|
|
|
|
categoryEl.innerHTML = '';
|
|
|
|
filtered.forEach((item, index) => {
|
|
const option = document.createElement('option');
|
|
option.value = item.value;
|
|
option.textContent = item.text;
|
|
|
|
if (item.value === currentValue) {
|
|
option.selected = true;
|
|
} else if (!filtered.some(x => x.value === currentValue) && index === 0) {
|
|
option.selected = true;
|
|
}
|
|
|
|
categoryEl.appendChild(option);
|
|
});
|
|
}
|
|
|
|
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 isExpenseCard = transactionType === 'expense' && acc && acc.type === '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 categoryType = mapTransactionTypeToCategoryType(transactionTypeEl.value);
|
|
|
|
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 () {
|
|
amountEl.value = formatWithComma(parseNumber(amountEl.value) + parseInt(btn.dataset.add || '0', 10));
|
|
calculateInterest();
|
|
});
|
|
});
|
|
|
|
amountClearBtn.addEventListener('click', function () {
|
|
amountEl.value = '';
|
|
calculateInterest();
|
|
amountEl.focus();
|
|
});
|
|
|
|
amountEl.addEventListener('input', function () {
|
|
this.value = formatWithComma(this.value);
|
|
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('transactionEditForm').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();
|
|
|
|
if (merchantEl.value.trim().length >= 2) {
|
|
scheduleSuggestion();
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/../app/views/footer.php'; ?>
|