기능 개선 — 사진 업로드, HEIC 지원, 재조치 흐름, 신고 순번, 모바일 UI

- 이미지 압축: 삼성/네이버 브라우저 호환, URL.createObjectURL 방식으로 메모리 절감,
  대용량 PNG/HEIC 처리, blob 유효성 검증, 순차 압축으로 모바일 OOM 방지
- HEIC/HEIF 지원: pillow-heif 서버사이드 변환, Pillow 12.2.0 업그레이드
- 조치 페이지: '조치 완료 저장' 단일 버튼으로 단순화
- 재조치 흐름: 관리자 재조치 요청 시 이전 조치 이력을 번호 카드로 순차 표시
- 신고 순번: 전체 기준 ROW_NUMBER(oldest=1) 순번 표시, 삭제 gap 제거
- 모바일 탭바: position:fixed 적용으로 nav 하단 흰 여백 제거

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
byun
2026-06-02 05:38:33 +09:00
parent 5ebd0a6ae7
commit 9f0f4326fe
18 changed files with 436 additions and 255 deletions

View File

@@ -147,7 +147,7 @@
<script src="/js/api.js"></script>
<script src="/js/auth.js"></script>
<script src="/js/imageCompress.js"></script>
<script src="/js/imageCompress.js?v=20260603"></script>
<script>
Auth.require(['admin']);
Auth.renderNav(document.getElementById('navUser'));
@@ -208,7 +208,7 @@ async function load() {
const cost = repair?.cost;
const prevRepairs = r.prev_repairs || [];
document.getElementById('pageTitle').textContent = `신고 #${r.id} 상세`;
document.getElementById('pageTitle').textContent = `신고 #${r.seq} 상세`;
// ── 출장비 요약 HTML 생성 ──
let costHtml = '';
@@ -522,10 +522,35 @@ async function load() {
</div>
</div>
<!-- 조치 정보 -->
<div class="card">
<!-- 조치 정보 — 이전 조치(순번카드) + 최신 조치 순서로 표시 -->
${prevRepairs.length ? (() => {
// prev_repairs는 최신순(내림차순)이므로 뒤집어서 오래된 것부터 번호 부여
const orderedPrev = prevRepairs.slice().reverse();
return orderedPrev.map((pr, idx) => `
<div class="card" style="border-left:4px solid #F59E0B;">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<span>🔧 조치 #${idx + 1}${orderedPrev.length + 1 > 2 ? '' : ' (최초)'}</span>
<span style="font-size:12px;background:#FEF3C7;color:#92400E;padding:3px 12px;border-radius:10px;font-weight:700;">🔁 재조치 요청됨 · ${Auth.fmtDt(pr.re_dispatch_requested_at)}</span>
</div>
<table class="no-hover" style="font-size:13px;">
<tr><td style="color:var(--gray4);width:100px">정비사</td><td>${pr.mechanic_name||'-'}</td></tr>
<tr><td style="color:var(--gray4)">조치유형</td><td>${(pr.repair_types||[]).join(', ')||'-'}</td></tr>
<tr><td style="color:var(--gray4)">조치내용</td><td>${pr.description||'-'}</td></tr>
<tr><td style="color:var(--gray4)">완료시각</td><td>${Auth.fmtDt(pr.completed_at)}</td></tr>
</table>
${(pr.photos_before||[]).length || (pr.photos_after||[]).length ? `
<div style="margin-top:10px;">
${(pr.photos_before||[]).length ? `<label style="font-size:12px;font-weight:700;color:var(--navy2)">조치 전 사진</label>
<div class="photo-preview">${(pr.photos_before||[]).map(p=>`<img src="${p}" onclick="window.open('${p}')" style="cursor:zoom-in">`).join('')}</div>` : ''}
${(pr.photos_after||[]).length ? `<label style="font-size:12px;font-weight:700;color:var(--navy2);margin-top:8px;display:block">조치 후 사진</label>
<div class="photo-preview">${(pr.photos_after||[]).map(p=>`<img src="${p}" onclick="window.open('${p}')" style="cursor:zoom-in">`).join('')}</div>` : ''}
</div>` : ''}
</div>`).join('');
})() : ''}
<div class="card" style="${prevRepairs.length ? 'border-left:4px solid var(--accent);' : ''}">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<span>🔧 조치 정보</span>
<span>🔧 조치 ${prevRepairs.length ? '#' + (prevRepairs.length + 1) + ' (최신)' : '정보'}</span>
${repair ? `
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
${repair.approved_at
@@ -669,34 +694,6 @@ async function load() {
</div>
</div>
${prevRepairs.length ? `
<div class="card">
<div class="card-title">📋 이전 조치 이력 (재조치 전 기록 ${prevRepairs.length}건)</div>
${prevRepairs.map((pr, idx) => `
<details style="margin-bottom:10px;border:1px solid var(--gray2);border-radius:8px;overflow:hidden;">
<summary style="padding:10px 14px;cursor:pointer;font-size:13px;font-weight:700;color:var(--navy2);background:var(--gray1);list-style:none;display:flex;justify-content:space-between;align-items:center;">
<span>#${pr.id} · ${pr.mechanic_name||'?'} · ${Auth.fmtDt(pr.completed_at)}</span>
<span style="font-size:11px;background:#FEF3C7;color:#92400E;padding:2px 8px;border-radius:8px;">🔁 재조치 요청됨</span>
</summary>
<div style="padding:12px 14px;font-size:13px;">
<table class="no-hover" style="font-size:13px;">
<tr><td style="color:var(--gray4);width:100px">정비사</td><td>${pr.mechanic_name||'-'} (${pr.mechanic_company||'-'})</td></tr>
<tr><td style="color:var(--gray4)">조치유형</td><td>${(pr.repair_types||[]).join(', ')||'-'}</td></tr>
<tr><td style="color:var(--gray4)">조치내용</td><td>${pr.description||'-'}</td></tr>
<tr><td style="color:var(--gray4)">완료시각</td><td>${Auth.fmtDt(pr.completed_at)}</td></tr>
<tr><td style="color:var(--gray4)">처리결과</td><td>${Auth.statusBadge(pr.result_status)}</td></tr>
<tr><td style="color:var(--gray4)">재조치요청</td><td style="color:#92400E;font-weight:700;">${Auth.fmtDt(pr.re_dispatch_requested_at)}</td></tr>
</table>
${(pr.photos_before||[]).length || (pr.photos_after||[]).length ? `
<div style="margin-top:10px;">
${(pr.photos_before||[]).length ? `<div style="font-size:11px;font-weight:700;color:var(--gray4);margin-bottom:4px;">조치 전</div>
<div class="photo-preview">${(pr.photos_before||[]).map(p=>`<img src="${p}" onclick="window.open('${p}')" style="cursor:zoom-in">`).join('')}</div>` : ''}
${(pr.photos_after||[]).length ? `<div style="font-size:11px;font-weight:700;color:var(--gray4);margin:6px 0 4px;">조치 후</div>
<div class="photo-preview">${(pr.photos_after||[]).map(p=>`<img src="${p}" onclick="window.open('${p}')" style="cursor:zoom-in">`).join('')}</div>` : ''}
</div>` : ''}
</div>
</details>`).join('')}
</div>` : ''}
${costHtml}
`;