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"> <table class="erp-table" id="ex-table">
<thead> <thead>
<tr> <tr>
<th style="width: 28px;"></th>
<th style="width: 110px;">사용일</th> <th style="width: 110px;">사용일</th>
<th style="width: 90px;">분류</th> <th style="width: 90px;">분류</th>
<th style="width: 100px;">수단</th> <th style="width: 100px;">수단</th>
<th>가맹점</th> <th>가맹점</th>
<th style="width: 120px; text-align: right;">금액</th> <th style="width: 120px; text-align: right;">금액</th>
<th style="width: 100px;">상태</th> <th style="width: 90px;">상태</th>
<th style="width: 220px; text-align: right;">동작</th> <th style="width: 280px; text-align: right;">동작</th>
</tr> </tr>
</thead> </thead>
<tbody id="ex-tbody"> <tbody id="ex-tbody">
{% for it in items %} {% for it in items %}
<tr data-id="{{ it.id }}" data-status="{{ it.status }}"> <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.spent_at }}</td>
<td>{{ it.category }}</td> <td>{{ it.category }}</td>
<td>{{ it.method }}</td> <td>{{ it.method }}</td>
@@ -127,6 +121,14 @@
{% endif %} {% endif %}
</td> </td>
<td style="text-align: right;" class="js-actions"> <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 ('작성중', '반려') %} {% 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-edit">수정</button>
<button class="erp-btn erp-btn-ghost erp-btn-sm js-delete">삭제</button> <button class="erp-btn erp-btn-ghost erp-btn-sm js-delete">삭제</button>
@@ -139,24 +141,9 @@
</td> </td>
</tr> </tr>
{% if supports_workflow %} {% if supports_workflow %}
<tr class="ex-detail" data-detail-for="{{ it.id }}" hidden> <tr class="ex-detail" data-detail-for="{{ it.id }}">
<td colspan="8"> <td colspan="7">
<div class="ex-detail-box"> <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 }}"> <ul class="ex-attach-list" data-list="{{ it.id }}">
<li class="erp-muted">불러오는 중…</li> <li class="erp-muted">불러오는 중…</li>
</ul> </ul>
@@ -170,7 +157,7 @@
</tr> </tr>
{% endif %} {% endif %}
{% else %} {% 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 %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -183,16 +170,10 @@
.erp-page-actions { .erp-page-actions {
display: flex; gap: var(--sp-8); margin-bottom: var(--sp-16); flex-wrap: wrap; display: flex; gap: var(--sp-8); margin-bottom: var(--sp-16); flex-wrap: wrap;
} }
.ex-detail-box { /* detail 행은 메인 행 바로 아래 — 위쪽 보더만 살짝, 좌측 들여쓰기 */
background: var(--color-ghost-gray); tr.ex-detail > td { padding: 6px var(--sp-16) var(--sp-12) 110px; background: var(--color-canvas-white); }
padding: var(--sp-12) var(--sp-16); tr.ex-detail + tr[data-id] > td { border-top: 1px solid var(--color-subtle-ash); }
border-radius: 8px; .ex-detail-box { background: transparent; padding: 0; }
}
.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; }
.ex-attach-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; } .ex-attach-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.ex-attach-list li { .ex-attach-list li {
display: flex; align-items: center; gap: 8px; padding: 6px 8px; display: flex; align-items: center; gap: 8px; padding: 6px 8px;
@@ -310,23 +291,13 @@
tbody.addEventListener("click", async (e) => { tbody.addEventListener("click", async (e) => {
const btn = e.target.closest("button"); const btn = e.target.closest("button");
if (!btn) return; if (!btn) return;
const tr = btn.closest("tr[data-id]"); // 메인 행 또는 detail 행 모두 처리
if (!tr) return; const mainTr = btn.closest("tr[data-id]");
const id = tr.dataset.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")) { if (btn.classList.contains("js-edit")) {
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")) {
const res = await fetch(`/expense/api/items`); const res = await fetch(`/expense/api/items`);
if (!res.ok) return; if (!res.ok) return;
const { items } = await res.json(); const { items } = await res.json();
@@ -353,13 +324,13 @@
} }
}); });
// 첨부 업로드 // 첨부 업로드 (메인 행 동작셀 input)
tbody.addEventListener("change", async (e) => { tbody.addEventListener("change", async (e) => {
const input = e.target.closest("input.js-upload"); const input = e.target.closest("input.js-upload");
if (!input || !input.files.length) return; if (!input || !input.files.length) return;
const tr = input.closest("tr.ex-detail"); const mainTr = input.closest("tr[data-id]");
if (!tr) return; if (!mainTr) return;
const itemId = tr.dataset.detailFor; const itemId = mainTr.dataset.id;
const fd = new FormData(); const fd = new FormData();
fd.append("file", input.files[0]); fd.append("file", input.files[0]);
fd.append("kind", input.dataset.kind || "other"); 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) { if (!form.spent_at.value) {
const d = new Date(); const d = new Date();
form.spent_at.value = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; form.spent_at.value = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;