ui(expense): 첨부 버튼을 동작 셀에 직접 노출 (화살표 토글 제거)
기존: 행마다 ▸ 화살표 클릭 → 상세 영역 펼침 → 영수증/기타 추가 변경: 동작 셀에 📎영수증 / 📎기타 버튼 항상 노출. 첨부 목록은 메인 행 바로 아래 인라인으로 항상 표시. 페이지 로드 시 모든 항목의 첨부 자동 fetch. 업로드 input 위치 변경에 맞춰 JS 핸들러 보정.
This commit is contained in:
@@ -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")}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user