0b04a05b26
- CategoryStore: 분류 항목 JSON 저장소(DATA_DIR/expense_categories.json) - 관리자 설정 페이지(/expense/settings) + 분류 CRUD API - _normalize 분류 고정목록 검증 제거 — 추가 분류 즉시 등록 폼/집계 반영 - index/reports 분류를 동적 목록으로 교체 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
3.7 KiB
HTML
120 lines
3.7 KiB
HTML
{% extends "erp_base.html" %}
|
|
|
|
{% block content %}
|
|
<section class="erp-ex-settings">
|
|
|
|
<div class="erp-page-actions">
|
|
<a class="erp-btn erp-btn-ghost" href="/expense/">← 개인경비</a>
|
|
</div>
|
|
|
|
<div class="erp-card-block" style="max-width: 640px;">
|
|
<div class="erp-card-block-head">
|
|
<h2>분류 항목 관리</h2>
|
|
<span class="erp-muted">추가/삭제 즉시 사용자 등록 폼에 반영됩니다.</span>
|
|
</div>
|
|
|
|
<form id="cat-form" class="erp-form-grid" style="grid-template-columns: 1fr auto; align-items: end; gap: var(--sp-12);">
|
|
<label class="erp-field"><span>새 분류명</span>
|
|
<input type="text" name="name" maxlength="30" placeholder="예) 마케팅" required />
|
|
</label>
|
|
<div class="erp-form-actions" style="margin: 0;">
|
|
<button type="submit" class="erp-btn erp-btn-primary">추가</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="erp-table-wrap" style="margin-top: var(--sp-16);">
|
|
<table class="erp-table" id="cat-table">
|
|
<thead>
|
|
<tr>
|
|
<th>분류명</th>
|
|
<th style="width: 100px; text-align: right;">동작</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="cat-tbody">
|
|
{% for c in categories %}
|
|
<tr data-name="{{ c }}">
|
|
<td>{{ c }}</td>
|
|
<td style="text-align: right;">
|
|
<button class="erp-btn erp-btn-outline erp-btn-sm js-del">삭제</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p class="erp-muted" style="margin-top: var(--sp-12);">
|
|
삭제해도 이미 등록된 경비 항목의 분류는 그대로 유지됩니다. 분류는 최소 1개 이상이어야 합니다.
|
|
</p>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<style>
|
|
.erp-page-actions { display: flex; gap: var(--sp-8); margin-bottom: var(--sp-16); }
|
|
#cat-tbody td { vertical-align: middle; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
(function () {
|
|
const form = document.getElementById("cat-form");
|
|
const tbody = document.getElementById("cat-tbody");
|
|
|
|
function rowFor(name) {
|
|
const tr = document.createElement("tr");
|
|
tr.dataset.name = name;
|
|
tr.innerHTML =
|
|
`<td></td>` +
|
|
`<td style="text-align: right;">` +
|
|
`<button class="erp-btn erp-btn-outline erp-btn-sm js-del">삭제</button></td>`;
|
|
tr.children[0].textContent = name;
|
|
return tr;
|
|
}
|
|
|
|
function render(categories) {
|
|
tbody.innerHTML = "";
|
|
categories.forEach((c) => tbody.appendChild(rowFor(c)));
|
|
}
|
|
|
|
form.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const name = form.name.value.trim();
|
|
if (!name) return;
|
|
try {
|
|
const res = await fetch("/expense/api/categories", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || res.status);
|
|
render(data.categories);
|
|
form.reset();
|
|
form.name.focus();
|
|
} catch (err) {
|
|
alert(`추가 실패: ${err.message || err}`);
|
|
}
|
|
});
|
|
|
|
tbody.addEventListener("click", async (e) => {
|
|
const btn = e.target.closest("button.js-del");
|
|
if (!btn) return;
|
|
const tr = btn.closest("tr[data-name]");
|
|
const name = tr.dataset.name;
|
|
if (!confirm(`분류 "${name}" 을(를) 삭제할까요?`)) return;
|
|
try {
|
|
const res = await fetch(`/expense/api/categories/${encodeURIComponent(name)}`, {
|
|
method: "DELETE",
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.detail || res.status);
|
|
render(data.categories);
|
|
} catch (err) {
|
|
alert(`삭제 실패: ${err.message || err}`);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|