ui(expense): 첨부 버튼을 동작 셀에 직접 노출 (화살표 토글 제거)

기존: 행마다 ▸ 화살표 클릭 → 상세 영역 펼침 → 영수증/기타 추가
변경: 동작 셀에 📎영수증 / 📎기타 버튼 항상 노출.
      첨부 목록은 메인 행 바로 아래 인라인으로 항상 표시.

페이지 로드 시 모든 항목의 첨부 자동 fetch.
업로드 input 위치 변경에 맞춰 JS 핸들러 보정.
This commit is contained in:
2026-05-29 03:33:41 +09:00
parent d2c1dfa2a0
commit d4af2dc064
@@ -84,24 +84,18 @@
<table class="erp-table" id="ex-table">
<thead>
<tr>
<th style="width: 28px;"></th>
<th style="width: 110px;">사용일</th>
<th style="width: 90px;">분류</th>
<th style="width: 100px;">수단</th>
<th>가맹점</th>
<th style="width: 120px; text-align: right;">금액</th>
<th style="width: 100px;">상태</th>
<th style="width: 220px; text-align: right;">동작</th>
<th style="width: 90px;">상태</th>
<th style="width: 280px; text-align: right;">동작</th>
</tr>
</thead>
<tbody id="ex-tbody">
{% for it in items %}
<tr data-id="{{ it.id }}" data-status="{{ it.status }}">
<td>
{% if supports_workflow %}
<button class="erp-btn erp-btn-ghost erp-btn-sm js-toggle" title="첨부/상세"></button>
{% endif %}
</td>
<td>{{ it.spent_at }}</td>
<td>{{ it.category }}</td>
<td>{{ it.method }}</td>
@@ -127,6 +121,14 @@
{% endif %}
</td>
<td style="text-align: right;" class="js-actions">
{% if it.status in ('작성중', '반려') and supports_workflow %}
<label class="erp-btn erp-btn-outline erp-btn-sm" title="영수증 첨부">
📎영수증<input type="file" class="js-upload" data-kind="receipt" hidden />
</label>
<label class="erp-btn erp-btn-outline erp-btn-sm" title="기타파일 첨부">
📎기타<input type="file" class="js-upload" data-kind="other" hidden />
</label>
{% endif %}
{% if it.status in ('작성중', '반려') %}
<button class="erp-btn erp-btn-ghost erp-btn-sm js-edit">수정</button>
<button class="erp-btn erp-btn-ghost erp-btn-sm js-delete">삭제</button>
@@ -139,24 +141,9 @@
</td>
</tr>
{% if supports_workflow %}
<tr class="ex-detail" data-detail-for="{{ it.id }}" hidden>
<td colspan="8">
<tr class="ex-detail" data-detail-for="{{ it.id }}">
<td colspan="7">
<div class="ex-detail-box">
<div class="ex-attach-head">
<strong>첨부파일</strong>
{% if it.status in ('작성중', '반려') %}
<div class="ex-upload">
<label class="erp-btn erp-btn-outline erp-btn-sm">
영수증 추가
<input type="file" class="js-upload" data-kind="receipt" hidden />
</label>
<label class="erp-btn erp-btn-outline erp-btn-sm">
기타파일 추가
<input type="file" class="js-upload" data-kind="other" hidden />
</label>
</div>
{% endif %}
</div>
<ul class="ex-attach-list" data-list="{{ it.id }}">
<li class="erp-muted">불러오는 중…</li>
</ul>
@@ -170,7 +157,7 @@
</tr>
{% endif %}
{% else %}
<tr id="ex-empty"><td colspan="8" class="erp-empty">등록된 경비가 없습니다.</td></tr>
<tr id="ex-empty"><td colspan="7" class="erp-empty">등록된 경비가 없습니다.</td></tr>
{% endfor %}
</tbody>
</table>
@@ -183,16 +170,10 @@
.erp-page-actions {
display: flex; gap: var(--sp-8); margin-bottom: var(--sp-16); flex-wrap: wrap;
}
.ex-detail-box {
background: var(--color-ghost-gray);
padding: var(--sp-12) var(--sp-16);
border-radius: 8px;
}
.ex-attach-head {
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-8);
margin-bottom: 8px;
}
.ex-upload { display: flex; gap: 6px; }
/* detail 행은 메인 행 바로 아래 — 위쪽 보더만 살짝, 좌측 들여쓰기 */
tr.ex-detail > td { padding: 6px var(--sp-16) var(--sp-12) 110px; background: var(--color-canvas-white); }
tr.ex-detail + tr[data-id] > td { border-top: 1px solid var(--color-subtle-ash); }
.ex-detail-box { background: transparent; padding: 0; }
.ex-attach-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.ex-attach-list li {
display: flex; align-items: center; gap: 8px; padding: 6px 8px;
@@ -310,23 +291,13 @@
tbody.addEventListener("click", async (e) => {
const btn = e.target.closest("button");
if (!btn) return;
const tr = btn.closest("tr[data-id]");
if (!tr) return;
const id = tr.dataset.id;
// 메인 행 또는 detail 행 모두 처리
const mainTr = btn.closest("tr[data-id]");
const detailTr = btn.closest("tr.ex-detail");
const id = mainTr ? mainTr.dataset.id : (detailTr ? detailTr.dataset.detailFor : null);
if (!id) return;
if (btn.classList.contains("js-toggle")) {
const detail = tbody.querySelector(`tr.ex-detail[data-detail-for="${id}"]`);
if (!detail) return;
const opening = detail.hasAttribute("hidden");
if (opening) {
detail.removeAttribute("hidden");
btn.textContent = "▾";
loadAttachments(id);
} else {
detail.setAttribute("hidden", "");
btn.textContent = "▸";
}
} else if (btn.classList.contains("js-edit")) {
if (btn.classList.contains("js-edit")) {
const res = await fetch(`/expense/api/items`);
if (!res.ok) return;
const { items } = await res.json();
@@ -353,13 +324,13 @@
}
});
// 첨부 업로드
// 첨부 업로드 (메인 행 동작셀 input)
tbody.addEventListener("change", async (e) => {
const input = e.target.closest("input.js-upload");
if (!input || !input.files.length) return;
const tr = input.closest("tr.ex-detail");
if (!tr) return;
const itemId = tr.dataset.detailFor;
const mainTr = input.closest("tr[data-id]");
if (!mainTr) return;
const itemId = mainTr.dataset.id;
const fd = new FormData();
fd.append("file", input.files[0]);
fd.append("kind", input.dataset.kind || "other");
@@ -375,6 +346,13 @@
}
});
// 페이지 로드 시 모든 detail 행의 첨부 자동 로드
if (supportsWorkflow) {
tbody.querySelectorAll("tr.ex-detail[data-detail-for]").forEach((tr) => {
loadAttachments(tr.dataset.detailFor);
});
}
if (!form.spent_at.value) {
const d = new Date();
form.spent_at.value = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;