Files
ev-charger-as/frontend/static/pages/admin/improvement-detail.html
byun 2e8751ea6c 기능 추가 및 버그 수정 — 처리시간 지표, 대시보드 차트, UI 개선
## 처리시간 지표
- 업무시간 기준(09-18 평일) / 공휴일 제외 24h / 달력 기준 3가지 모드 선택
- 공휴일 DB 관리 (holidays 테이블, 수동 등록·삭제·일괄 추가)
- 2026년 공휴일 등록 지원
- 설정 페이지에서 라디오 버튼으로 모드 선택

## 대시보드 차트
- 월별 평균 처리시간 막대 차트 추가
- 월별 신고 접수 건수 누적 막대 차트 추가
- 월별 → 일별 드릴다운 (막대 클릭 시 해당 월의 일별 차트로 전환)
- 일별 막대 클릭 시 처리 완료/신고 접수 상세 내역 모달
- 충전기별 누적 고장 건수 Top 10 수평 막대 차트 추가

## 신고 목록
- # 컬럼을 DB PK 대신 현재 목록 순서(1, 2, 3…)로 표시
- 엑셀 export 접수번호도 순차번호로 변경

## 모바일 네비게이션 버그 수정
- 모바일에서 가로 오버플로우 시 nav가 body 넓이로 늘어나
  햄버거 버튼이 화면 밖으로 밀리는 문제 수정
- nav를 position:fixed + body padding-top:54px 로 변경 (전체 페이지 적용)
- 충전기 관리·신고 목록 페이지 지도 컨테이너에 isolation:isolate 적용

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 06:52:56 +09:00

119 lines
6.9 KiB
HTML

<!DOCTYPE html><html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>개선항목 상세</title><link rel="stylesheet" href="/css/style.css"></head>
<body>
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
<div class="layout">
<div class="sidebar">
<div class="sidebar-section">AS 관리</div>
<a href="/pages/admin/dashboard.html">📊 대시보드</a>
<a href="/pages/admin/reports.html">📋 신고 목록</a>
<a href="/pages/admin/costs.html">💰 출장비 관리</a>
<div class="sidebar-section">시스템</div>
<a href="/pages/admin/improvements.html" class="active">🔧 개선항목</a>
<a href="/pages/admin/chargers.html">⚡ 충전기 관리</a>
<a href="/pages/admin/charger-types.html">🏷 충전기 종류</a>
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
<a href="/pages/admin/qr.html">📷 QR 생성</a>
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
<a href="/pages/admin/settings.html">⚙️ 설정</a>
</div>
<div class="main">
<div style="margin-bottom:14px;display:flex;gap:10px;align-items:center;">
<a href="/pages/admin/improvements.html" class="btn btn-outline btn-sm">← 목록</a>
<h2 id="pageTitle" style="font-size:17px;font-weight:700;color:var(--navy)">개선항목 상세</h2>
</div>
<div id="content"></div>
</div>
</div>
<script src="/js/api.js"></script><script src="/js/auth.js"></script>
<script>
Auth.require(['admin']); Auth.renderNav(document.getElementById('navUser'));
const id = new URLSearchParams(location.search).get('id');
const CAT={sw:'SW개선',hw:'HW개선',ui:'UI개선',firmware:'펌웨어',other:'기타'};
const STATUS_OPTIONS = ['registered','reviewing','developing','deployed','done'];
const STATUS_LABEL = {registered:'등록',reviewing:'검토중',developing:'개발중',deployed:'배포완료',done:'완료'};
async function load() {
const imp = await API.get('/improvements/'+id);
document.getElementById('pageTitle').textContent = `개선항목 #${imp.id}`;
document.getElementById('content').innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
<div class="card">
<div class="card-title">📋 기본 정보</div>
<table class="no-hover" style="font-size:13px">
<tr><td style="color:var(--gray4);width:90px">제목</td><td><strong>${imp.title}</strong></td></tr>
<tr><td style="color:var(--gray4)">분류</td><td>${CAT[imp.category]||imp.category}</td></tr>
<tr><td style="color:var(--gray4)">우선순위</td><td>${imp.priority}</td></tr>
<tr><td style="color:var(--gray4)">관련 부품</td><td>${imp.part_name||'-'}</td></tr>
<tr><td style="color:var(--gray4)">담당 제조사</td><td><strong>${imp.manufacturer_company||'-'}</strong><br>${imp.manufacturer_name||''}</td></tr>
<tr><td style="color:var(--gray4)">등록자</td><td>${imp.created_by_name||'-'}</td></tr>
<tr><td style="color:var(--gray4)">등록일시</td><td>${Auth.fmtDt(imp.created_at)}</td></tr>
<tr><td style="color:var(--gray4)">배포 목표일</td><td>${imp.sw_deploy_target||'-'}</td></tr>
<tr><td style="color:var(--gray4)">실제 배포일</td><td>${imp.sw_deployed_at||'-'}</td></tr>
<tr><td style="color:var(--gray4)">현재 상태</td><td>${Auth.statusBadge(imp.status)}</td></tr>
</table>
<div style="margin-top:12px">
<div style="font-size:12px;font-weight:700;color:var(--navy2);margin-bottom:6px">개선 내용</div>
<div style="background:var(--gray1);padding:12px;border-radius:6px;font-size:13px;white-space:pre-wrap">${imp.description}</div>
</div>
${imp.manufacturer_memo?`<div style="margin-top:12px"><div style="font-size:12px;font-weight:700;color:var(--orange);margin-bottom:6px">제조사 메모</div><div style="background:#FFF5E6;padding:12px;border-radius:6px;font-size:13px">${imp.manufacturer_memo}</div></div>`:''}
</div>
<div class="card">
<div class="card-title">📎 연결된 AS 신고</div>
${imp.report_ids.length ? imp.report_ids.map(rid=>`
<div onclick="location.href='/pages/admin/report-detail.html?id=${rid}'"
style="padding:8px;border:1px solid var(--gray2);border-radius:6px;margin-bottom:6px;cursor:pointer;font-size:13px">
신고 #${rid}
</div>`).join('') : '<div class="alert alert-info">연결된 신고 없음</div>'}
<div class="card-title" style="margin-top:16px">📁 첨부 파일</div>
${imp.attachments.length ? imp.attachments.map(a=>`
<a href="${a.path}" target="_blank" class="btn btn-outline btn-sm" style="margin-bottom:6px;display:block">
📄 ${a.name||a.path.split('/').pop()}
</a>`).join('') : '<div style="font-size:13px;color:var(--gray4)">첨부 파일 없음</div>'}
</div>
</div>
<!-- 상태 변경 -->
<div class="card" style="margin-top:0">
<div class="card-title">🔄 상태 변경</div>
<div class="form-row">
<div class="form-group">
<label>상태 변경</label>
<select id="newStatus">
${STATUS_OPTIONS.map(s=>`<option value="${s}" ${imp.status===s?'selected':''}>${STATUS_LABEL[s]}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label>SW 실제 배포일 (배포완료 시)</label>
<input type="date" id="deployedAt" value="${imp.sw_deployed_at||''}">
</div>
</div>
<div class="form-group"><label>변경 메모</label><input type="text" id="changeMemo" placeholder="상태 변경 사유 또는 메모"></div>
<button class="btn btn-primary" onclick="changeStatus()">상태 저장</button>
</div>
<!-- 이력 로그 -->
<div class="card" style="margin-top:0">
<div class="card-title">📜 변경 이력</div>
${imp.logs.length ? `<div class="timeline">${imp.logs.map(l=>`
<div class="tl-item">
<div class="tl-time">${Auth.fmtDt(l.changed_at)}${l.by||'시스템'}</div>
<div class="tl-text">${l.old_status?`${STATUS_LABEL[l.old_status]||l.old_status}`:''}${STATUS_LABEL[l.new_status]||l.new_status}${l.memo?` / ${l.memo}`:''}</div>
</div>`).join('')}</div>` : '<div style="color:var(--gray4);font-size:13px">이력 없음</div>'}
</div>
`;
}
async function changeStatus() {
const status = document.getElementById('newStatus').value;
const memo = document.getElementById('changeMemo').value;
const date = document.getElementById('deployedAt').value;
const fd = new FormData();
fd.append('status', status); fd.append('memo', memo);
if (date) fd.append('sw_deployed_at', date);
await API.patch('/improvements/'+id+'/status', fd);
load();
}
load();
</script></div></div></body></html>