Files
financial/public/installment_prepay.php
2026-06-07 00:33:58 +09:00

554 lines
24 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/installment_service.php';
check_auth();
$pdo = db();
$uid = user_id();
$error = '';
$msg = '';
$installmentId = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("
SELECT
i.*,
a.account_name,
a.institution_name,
t.transaction_date,
t.merchant_name,
t.description,
COALESCE(SUM(CASE WHEN s.is_billed = 0 THEN s.principal_amount ELSE 0 END), 0) AS remaining_principal,
COALESCE(SUM(CASE WHEN s.is_billed = 0 THEN s.interest_amount ELSE 0 END), 0) AS remaining_interest,
COALESCE(SUM(CASE WHEN s.is_billed = 0 THEN s.total_amount ELSE 0 END), 0) AS remaining_total,
COALESCE(SUM(CASE WHEN s.is_billed = 1 THEN s.total_amount ELSE 0 END), 0) AS billed_total,
COALESCE(MIN(CASE WHEN s.is_billed = 0 THEN s.cycle_no ELSE NULL END), 0) AS next_cycle
FROM installments i
JOIN accounts a ON a.id = i.account_id
JOIN transactions t ON t.id = i.transaction_id
LEFT JOIN installment_schedules s ON s.installment_id = i.id
WHERE i.id = ?
AND i.user_id = ?
GROUP BY
i.id, i.user_id, i.transaction_id, i.account_id,
i.principal_amount, i.interest_total, i.total_billed_amount,
i.installment_months, i.annual_interest_rate, i.start_year_month,
i.interest_type, i.current_cycle, i.is_completed,
i.prepaid_principal_amount, i.prepaid_interest_amount,
i.created_at, i.updated_at,
a.account_name, a.institution_name,
t.transaction_date, t.merchant_name, t.description
");
$stmt->execute([$installmentId, $uid]);
$installment = $stmt->fetch();
if (!$installment) {
require __DIR__ . '/../app/views/header.php';
?>
<div class="page-head">
<h2>할부 선결제 / 중도상환</h2>
<a href="/installments.php" class="btn btn-outline-secondary">할부내역</a>
</div>
<div class="card finance-card">
<div class="card-body text-center py-5">
<div class="card-title-sm mb-3">할부 정보를 찾을 수 없습니다.</div>
<div class="text-secondary mb-4">
이미 초기화되었거나 삭제된 할부 내역일 수 있습니다.
</div>
<div class="d-flex justify-content-center gap-2 flex-wrap">
<a href="/installments.php" class="btn btn-primary">할부내역으로 이동</a>
<button type="button" class="btn btn-outline-secondary" onclick="history.back();">뒤로가기</button>
</div>
</div>
</div>
<?php
require __DIR__ . '/../app/views/footer.php';
exit;
}
$stmt = $pdo->prepare("
SELECT *
FROM accounts
WHERE user_id = ?
AND is_active = 1
AND account_type IN ('bank','cash','other')
ORDER BY FIELD(account_type, 'bank', 'cash', 'other'), id ASC
");
$stmt->execute([$uid]);
$paymentAccounts = $stmt->fetchAll();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$paymentAccountId = (int)($_POST['payment_account_id'] ?? 0);
$prepayDate = $_POST['prepay_date'] ?? date('Y-m-d');
$prepayPrincipal = (float)str_replace(',', '', (string)($_POST['prepay_principal_amount'] ?? 0));
$prepayInterest = (float)str_replace(',', '', (string)($_POST['prepay_interest_amount'] ?? 0));
$description = trim($_POST['description'] ?? '') ?: null;
$targetScheduleId = !empty($_POST['target_schedule_id'])
? (int)$_POST['target_schedule_id']
: null;
if ($paymentAccountId <= 0) {
throw new RuntimeException('결제 계좌를 선택하세요.');
}
prepay_installment(
$uid,
$installmentId,
$paymentAccountId,
$prepayDate,
$prepayPrincipal,
$prepayInterest,
$description,
$targetScheduleId
);
$msg = '선결제/중도상환이 처리되었습니다.';
$stmt = $pdo->prepare("
SELECT
i.*,
a.account_name,
a.institution_name,
t.transaction_date,
t.merchant_name,
t.description,
COALESCE(SUM(CASE WHEN s.is_billed = 0 THEN s.principal_amount ELSE 0 END), 0) AS remaining_principal,
COALESCE(SUM(CASE WHEN s.is_billed = 0 THEN s.interest_amount ELSE 0 END), 0) AS remaining_interest,
COALESCE(SUM(CASE WHEN s.is_billed = 0 THEN s.total_amount ELSE 0 END), 0) AS remaining_total,
COALESCE(SUM(CASE WHEN s.is_billed = 1 THEN s.total_amount ELSE 0 END), 0) AS billed_total,
COALESCE(MIN(CASE WHEN s.is_billed = 0 THEN s.cycle_no ELSE NULL END), 0) AS next_cycle
FROM installments i
JOIN accounts a ON a.id = i.account_id
JOIN transactions t ON t.id = i.transaction_id
LEFT JOIN installment_schedules s ON s.installment_id = i.id
WHERE i.id = ?
AND i.user_id = ?
GROUP BY
i.id, i.user_id, i.transaction_id, i.account_id,
i.principal_amount, i.interest_total, i.total_billed_amount,
i.installment_months, i.annual_interest_rate, i.start_year_month,
i.interest_type, i.current_cycle, i.is_completed,
i.prepaid_principal_amount, i.prepaid_interest_amount,
i.created_at, i.updated_at,
a.account_name, a.institution_name,
t.transaction_date, t.merchant_name, t.description
");
$stmt->execute([$installmentId, $uid]);
$installment = $stmt->fetch();
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
$stmt = $pdo->prepare("
SELECT *
FROM installment_schedules
WHERE installment_id = ?
ORDER BY cycle_no ASC
");
$stmt->execute([$installmentId]);
$schedules = $stmt->fetchAll();
$stmt = $pdo->prepare("
SELECT *
FROM installment_prepayments
WHERE installment_id = ?
AND user_id = ?
ORDER BY prepay_date DESC, id DESC
");
$stmt->execute([$installmentId, $uid]);
$prepayments = $stmt->fetchAll();
$remainingPrincipal = (float)($installment['remaining_principal'] ?? 0);
$remainingInterest = (float)($installment['remaining_interest'] ?? 0);
$remainingTotal = (float)($installment['remaining_total'] ?? 0);
$totalBilled = (float)($installment['total_billed_amount'] ?? 0);
$billedTotal = (float)($installment['billed_total'] ?? 0);
$progress = $totalBilled > 0 ? (($totalBilled - $remainingTotal) / $totalBilled) * 100 : 0;
$progress = max(0, min(100, $progress));
$barWidth = $progress > 0 ? max($progress, 2) : 0;
require __DIR__ . '/../app/views/header.php';
?>
<div class="page-head">
<h2>할부 선결제 / 중도상환</h2>
<div class="d-flex flex-wrap gap-2">
<a href="/installments.php" class="btn btn-outline-secondary">할부내역</a>
<a href="/installment_billing.php?ym=<?= h($installment['start_year_month']) ?>&account_id=<?= h((string)$installment['account_id']) ?>" class="btn btn-outline-primary">청구관리</a>
</div>
</div>
<?php if ($error): ?>
<div class="alert alert-danger"><?= h($error) ?></div>
<?php endif; ?>
<?php if ($msg): ?>
<div class="alert alert-success"><?= h($msg) ?></div>
<?php endif; ?>
<div class="row g-4">
<div class="col-12 col-xl-5">
<div class="card finance-card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-2">
<div>
<div class="eyebrow"><?= h($installment['institution_name']) ?> / <?= h($installment['account_name']) ?></div>
<div class="card-title-lg"><?= h($installment['merchant_name'] ?: '사용처 없음') ?></div>
</div>
<span class="badge <?= $remainingTotal > 0 ? 'text-bg-warning' : 'text-bg-success' ?>">
<?= $remainingTotal > 0 ? '진행중' : '완료' ?>
</span>
</div>
<div class="small text-secondary mb-3">
결제일 <?= ymd($installment['transaction_date']) ?>
· 시작 청구월 <?= h($installment['start_year_month']) ?>
· <?= intvalf($installment['installment_months']) ?>개월
<?php if (!empty($installment['description'])): ?>
<br><?= h($installment['description']) ?>
<?php endif; ?>
</div>
<div class="row g-3">
<div class="col-6">
<div class="stat-label">남은 원금</div>
<div class="stat-value"><?= won($remainingPrincipal) ?></div>
</div>
<div class="col-6">
<div class="stat-label">남은 이자</div>
<div class="stat-value text-danger"><?= won($remainingInterest) ?></div>
</div>
<div class="col-6">
<div class="stat-label">남은 총액</div>
<div class="stat-value"><?= won($remainingTotal) ?></div>
</div>
<div class="col-6">
<div class="stat-label">연이자율</div>
<div class="stat-value"><?= numf($installment['annual_interest_rate'], 2) ?>%</div>
</div>
<div class="col-6">
<div class="stat-label">총 청구금액</div>
<div class="stat-value"><?= won($totalBilled) ?></div>
</div>
<div class="col-6">
<div class="stat-label">청구완료 금액</div>
<div class="stat-value text-primary"><?= won($billedTotal) ?></div>
</div>
</div>
<div class="mt-3">
<div class="finance-progress">
<div class="progress-bar" role="progressbar" style="width: <?= number_format($barWidth, 1) ?>%"></div>
</div>
<div class="small text-secondary mt-2">
진행률 <?= percentf($progress) ?>
<?php if ((int)$installment['next_cycle'] > 0): ?>
· 다음 <?= intvalf($installment['next_cycle']) ?>회차
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="card finance-card">
<div class="card-body">
<div class="card-title-sm mb-3">선결제 입력</div>
<?php if ($remainingTotal <= 0): ?>
<div class="alert alert-success mb-0">
남아있는 할부 금액이 없습니다.
</div>
<?php else: ?>
<form method="post" class="row g-3" id="prepayForm">
<input type="hidden" name="target_schedule_id" id="target_schedule_id" value="">
<div class="col-12">
<label class="form-label">선결제 출금 계좌</label>
<select name="payment_account_id" class="form-select" required>
<option value="">선택하세요</option>
<?php foreach ($paymentAccounts as $acc): ?>
<option value="<?= $acc['id'] ?>">
<?= h($acc['account_name']) ?> (<?= h($acc['account_type']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<label class="form-label">선결제일</label>
<input type="date" name="prepay_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-12 col-md-6">
<label class="form-label">선결제 원금</label>
<input
type="text"
name="prepay_principal_amount"
id="prepay_principal_amount"
class="form-control"
inputmode="numeric"
value="0"
required
>
<div class="mt-2 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary fill-principal">남은 원금</button>
<button type="button" class="btn btn-sm btn-outline-secondary clear-principal">초기화</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">선결제 이자</label>
<input
type="text"
name="prepay_interest_amount"
id="prepay_interest_amount"
class="form-control"
inputmode="numeric"
value="0"
>
<div class="mt-2 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary fill-interest">남은 이자</button>
<button type="button" class="btn btn-sm btn-outline-secondary clear-interest">초기화</button>
</div>
</div>
<div class="col-12">
<div class="loan-create-highlight">
<div class="title">빠른 입력</div>
<div class="desc">남은 금액 전체를 선결제하려면 전체상환 버튼을 사용하세요.</div>
<div class="mt-3 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary" id="fill_all_btn">전체상환 입력</button>
<button type="button" class="btn btn-outline-secondary" id="clear_all_btn">전체 초기화</button>
</div>
</div>
</div>
<div class="col-12">
<label class="form-label">메모</label>
<input type="text" name="description" id="description" class="form-control" placeholder="예: 카드 선결제">
</div>
<div class="col-12 d-flex flex-wrap gap-2">
<button class="btn btn-primary" onclick="return confirm('선결제/중도상환 처리하시겠습니까?');">
선결제 실행
</button>
<a href="/installments.php" class="btn btn-outline-secondary">취소</a>
</div>
</form>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card finance-card mb-4">
<div class="card-body">
<div class="card-title-sm mb-3">남은 회차 상세</div>
<div class="mobile-scroll">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>회차</th>
<th>청구월</th>
<th class="text-end">원금</th>
<th class="text-end">이자</th>
<th class="text-end">합계</th>
<th>상태</th>
<th>선납</th>
</tr>
</thead>
<tbody>
<?php foreach ($schedules as $s): ?>
<?php $isBilled = (int)$s['is_billed'] === 1; ?>
<tr class="<?= $isBilled ? 'text-secondary' : '' ?>">
<td><?= intvalf($s['cycle_no']) ?>회차</td>
<td><?= h($s['bill_year_month']) ?></td>
<td class="text-end"><?= won($s['principal_amount']) ?></td>
<td class="text-end text-danger"><?= won($s['interest_amount']) ?></td>
<td class="text-end fw-bold"><?= won($s['total_amount']) ?></td>
<td>
<?php if ($isBilled): ?>
<span class="badge text-bg-success">청구완료</span>
<?php else: ?>
<span class="badge text-bg-secondary">미청구</span>
<?php endif; ?>
</td>
<td>
<?php if (!$isBilled && (float)$s['total_amount'] > 0): ?>
<button
type="button"
class="btn btn-sm btn-outline-primary cycle-prepay-btn"
data-schedule-id="<?= (int)$s['id'] ?>"
data-cycle="<?= (int)$s['cycle_no'] ?>"
data-principal="<?= (float)$s['principal_amount'] ?>"
data-interest="<?= (float)$s['interest_amount'] ?>"
>
선납
</button>
<?php else: ?>
-
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$schedules): ?>
<tr>
<td colspan="7" class="text-center text-secondary py-4">회차 정보가 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="card finance-card">
<div class="card-body">
<div class="card-title-sm mb-3">선결제 이력</div>
<div class="mobile-scroll">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>일자</th>
<th class="text-end">원금</th>
<th class="text-end">이자</th>
<th class="text-end">합계</th>
<th>메모</th>
</tr>
</thead>
<tbody>
<?php foreach ($prepayments as $p): ?>
<tr>
<td><?= ymd($p['prepay_date']) ?></td>
<td class="text-end"><?= won($p['principal_amount']) ?></td>
<td class="text-end text-danger"><?= won($p['interest_amount']) ?></td>
<td class="text-end fw-bold"><?= won($p['total_amount']) ?></td>
<td><?= h($p['description'] ?: '-') ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$prepayments): ?>
<tr>
<td colspan="5" class="text-center text-secondary py-4">선결제 이력이 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const remainPrincipal = <?= json_encode($remainingPrincipal) ?>;
const remainInterest = <?= json_encode($remainingInterest) ?>;
const principalEl = document.getElementById('prepay_principal_amount');
const interestEl = document.getElementById('prepay_interest_amount');
if (!principalEl || !interestEl) return;
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 setMoney(el, value) {
el.value = formatWithComma(Math.round(Number(value || 0)));
}
principalEl.addEventListener('input', function () {
this.value = formatWithComma(this.value);
});
interestEl.addEventListener('input', function () {
this.value = formatWithComma(this.value);
});
document.querySelector('.fill-principal')?.addEventListener('click', function () {
setMoney(principalEl, remainPrincipal);
});
document.querySelector('.clear-principal')?.addEventListener('click', function () {
principalEl.value = '0';
});
document.querySelector('.fill-interest')?.addEventListener('click', function () {
setMoney(interestEl, remainInterest);
});
document.querySelector('.clear-interest')?.addEventListener('click', function () {
interestEl.value = '0';
});
document.getElementById('fill_all_btn')?.addEventListener('click', function () {
setMoney(principalEl, remainPrincipal);
setMoney(interestEl, remainInterest);
const targetScheduleEl = document.getElementById('target_schedule_id');
if (targetScheduleEl) targetScheduleEl.value = '';
});
document.getElementById('clear_all_btn')?.addEventListener('click', function () {
principalEl.value = '0';
interestEl.value = '0';
const targetScheduleEl = document.getElementById('target_schedule_id');
if (targetScheduleEl) targetScheduleEl.value = '';
});
document.getElementById('prepayForm')?.addEventListener('submit', function () {
principalEl.value = principalEl.value.replace(/,/g, '');
interestEl.value = interestEl.value.replace(/,/g, '');
});
const descEl = document.getElementById('description');
const targetScheduleEl = document.getElementById('target_schedule_id');
document.querySelectorAll('.cycle-prepay-btn').forEach(btn => {
btn.addEventListener('click', function () {
const cycle = this.dataset.cycle;
const principal = Number(this.dataset.principal || 0);
const interest = Number(this.dataset.interest || 0);
setMoney(principalEl, principal);
setMoney(interestEl, interest);
if (targetScheduleEl) {
targetScheduleEl.value = this.dataset.scheduleId || '';
}
if (descEl) {
descEl.value = cycle + '회차 선납';
}
principalEl.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
});
});
})();
</script>
<?php require __DIR__ . '/../app/views/footer.php'; ?>