Files
ev-charger-as/frontend/static/pages/login.html
byun 124ad0d165 기능 추가 — 옵저버 계정 및 현황 조회 포털
읽기 전용 옵저버 역할 추가. 신고 현황 확인만 가능하며 모든 쓰기 동작 차단.

- auth.py: require_viewer(admin+observer) 의존성 추가
- auth_router.py: register 엔드포인트에 role 파라미터 추가 (mechanic/observer)
- login.html: 회원가입 시 정비사/옵저버 역할 카드 선택 UI, 역할별 안내문구
- 로그인 후 observer → /pages/observer/dashboard.html 라우팅
- observer/dashboard.html: 통계 카드(상태별 건수) + 신고 현황 테이블(읽기전용)
- observer/reports.html: 상태·충전기ID·충전소명 필터 신고 목록
- accounts.html: 옵저버 필터·생성·승인 대기 역할 표시 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 15:25:47 +09:00

235 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>로그인 — EV AS 관리</title>
<link rel="stylesheet" href="/css/style.css">
<style>
body { display:flex; align-items:center; justify-content:center; min-height:100vh; background:var(--navy); }
.login-box {
background:white; border-radius:14px; padding:40px 36px;
width:100%; max-width:380px; box-shadow:0 8px 32px rgba(0,0,0,.3);
}
.login-logo { text-align:center; margin-bottom:28px; }
.login-logo h1 { font-size:22px; font-weight:900; color:var(--navy); }
.login-logo p { font-size:12px; color:var(--gray4); margin-top:4px; }
.login-box .form-group { margin-bottom:14px; }
#err, #regErr { font-size:13px; text-align:center; min-height:18px; margin-bottom:8px; }
#err { color:var(--red); }
#regErr { color:var(--red); }
.tab-row {
display:flex; gap:0; border-bottom:2px solid var(--gray2); margin-bottom:24px;
}
.tab-row button {
flex:1; background:none; border:none; padding:9px 0; font-size:14px; font-weight:600;
color:var(--gray4); border-bottom:3px solid transparent; margin-bottom:-2px; cursor:pointer;
transition:color .15s, border-color .15s;
}
.tab-row button.active { color:var(--navy); border-bottom-color:var(--accent); }
.pane { display:none; }
.pane.active { display:block; }
.reg-notice {
background:#EFF6FF; border:1px solid #BFDBFE; border-radius:8px;
padding:10px 14px; font-size:12px; color:#1E40AF; margin-bottom:16px; line-height:1.6;
}
</style>
</head>
<body>
<div class="login-box">
<div class="login-logo">
<h1>⚡ EV AS 관리</h1>
<p>cs.byunc.com</p>
</div>
<div class="tab-row">
<button id="tabLogin" class="active" onclick="switchTab('login')">로그인</button>
<button id="tabRegister" onclick="switchTab('register')">회원가입</button>
</div>
<!-- 로그인 -->
<div class="pane active" id="paneLogin">
<div class="form-group">
<label>아이디</label>
<input type="text" id="username" placeholder="아이디 입력" autofocus>
</div>
<div class="form-group">
<label>비밀번호</label>
<input type="password" id="password" placeholder="비밀번호 입력">
</div>
<div id="err"></div>
<button class="btn btn-primary btn-lg" id="loginBtn">로그인</button>
</div>
<!-- 회원가입 -->
<div class="pane" id="paneRegister">
<div class="form-group" style="margin-bottom:12px">
<label>계정 유형 <span style="color:var(--red)">*</span></label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:4px">
<label id="roleCardMechanic" onclick="selectRole('mechanic')" style="border:2px solid var(--accent);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;background:#E3EDFF;">
<div style="font-size:18px;margin-bottom:2px">🔧</div>
<div style="font-size:13px;font-weight:700;color:var(--navy)">정비사</div>
<div style="font-size:11px;color:var(--gray4)">조치 입력·처리</div>
</label>
<label id="roleCardObserver" onclick="selectRole('observer')" style="border:2px solid var(--gray3);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;">
<div style="font-size:18px;margin-bottom:2px">👁</div>
<div style="font-size:13px;font-weight:700;color:var(--navy)">옵저버</div>
<div style="font-size:11px;color:var(--gray4)">현황 조회만 가능</div>
</label>
</div>
<input type="hidden" id="regRole" value="mechanic">
</div>
<div id="regNotice" class="reg-notice">
📌 정비사 계정으로 가입됩니다.<br>
가입 후 <strong>관리자 승인</strong>이 완료되어야 로그인 가능합니다.
</div>
<div class="form-group">
<label>이름 <span style="color:var(--red)">*</span></label>
<input type="text" id="regName" placeholder="실명 입력">
</div>
<div class="form-group">
<label>아이디 <span style="color:var(--red)">*</span></label>
<input type="text" id="regUsername" placeholder="영문·숫자 조합">
</div>
<div class="form-group">
<label>비밀번호 <span style="color:var(--red)">*</span></label>
<input type="password" id="regPassword" placeholder="8자 이상 권장">
</div>
<div class="form-group">
<label>비밀번호 확인 <span style="color:var(--red)">*</span></label>
<input type="password" id="regPassword2" placeholder="비밀번호 재입력">
</div>
<div class="form-group">
<label>전화번호 <span style="font-size:11px;color:var(--gray4);font-weight:400">(선택)</span></label>
<input type="tel" id="regPhone" placeholder="예) 010-1234-5678">
</div>
<div class="form-group">
<label>회사명 <span style="font-size:11px;color:var(--gray4);font-weight:400">(선택)</span></label>
<select id="regCompany">
<option value="">-- 소속 제조사 없음 --</option>
</select>
</div>
<div id="regErr"></div>
<button class="btn btn-primary btn-lg" id="regBtn">가입 신청</button>
<div id="regOk" class="alert alert-success" style="display:none;margin-top:14px;text-align:center">
✅ 가입 신청이 완료되었습니다.<br>
<span style="font-size:12px">관리자 승인 후 로그인 가능합니다.</span>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/auth.js"></script>
<script>
function switchTab(name) {
document.getElementById('tabLogin').classList.toggle('active', name==='login');
document.getElementById('tabRegister').classList.toggle('active', name==='register');
document.getElementById('paneLogin').classList.toggle('active', name==='login');
document.getElementById('paneRegister').classList.toggle('active', name==='register');
document.getElementById('err').textContent = '';
document.getElementById('regErr').textContent = '';
}
// ── 로그인 ──
async function doLogin() {
const u = document.getElementById('username').value.trim();
const p = document.getElementById('password').value;
if (!u || !p) { document.getElementById('err').textContent = '아이디와 비밀번호를 입력하세요.'; return; }
document.getElementById('loginBtn').disabled = true;
document.getElementById('err').textContent = '';
try {
const fd = new FormData();
fd.append('username', u); fd.append('password', p);
const res = await fetch('/api/auth/login', { method:'POST', body: fd });
if (!res.ok) { const e = await res.json(); throw new Error(e.detail); }
const data = await res.json();
Auth.save(data.access_token, data.role, data.name, data.user_id);
if (data.role === 'admin') location.href = '/pages/admin/dashboard.html';
else if (data.role === 'mechanic') location.href = '/pages/mechanic/dashboard.html';
else if (data.role === 'observer') location.href = '/pages/observer/dashboard.html';
else location.href = '/pages/manufacturer/dashboard.html';
} catch(e) {
document.getElementById('err').textContent = e.message;
document.getElementById('loginBtn').disabled = false;
}
}
// ── 계정 유형 선택 ──
function selectRole(role) {
document.getElementById('regRole').value = role;
const mc = document.getElementById('roleCardMechanic');
const oc = document.getElementById('roleCardObserver');
if (role === 'mechanic') {
mc.style.cssText = 'border:2px solid var(--accent);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;background:#E3EDFF;';
oc.style.cssText = 'border:2px solid var(--gray3);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;';
document.getElementById('regNotice').innerHTML = '📌 정비사 계정으로 가입됩니다.<br>가입 후 <strong>관리자 승인</strong>이 완료되어야 로그인 가능합니다.';
} else {
oc.style.cssText = 'border:2px solid var(--accent);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;background:#E3EDFF;';
mc.style.cssText = 'border:2px solid var(--gray3);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;';
document.getElementById('regNotice').innerHTML = '👁 현황 조회 전용 계정입니다.<br>신고 등록·조치 등 쓰기 기능은 사용할 수 없습니다.<br>가입 후 <strong>관리자 승인</strong>이 완료되어야 로그인 가능합니다.';
}
}
// ── 제조사 목록 로드 (비인증) ──
async function loadCompanies() {
try {
const list = await fetch('/api/manufacturers/public').then(r => r.json());
const sel = document.getElementById('regCompany');
list.forEach(m => {
const opt = document.createElement('option');
opt.value = m.name; opt.textContent = m.name;
sel.appendChild(opt);
});
} catch {}
}
loadCompanies();
// ── 회원가입 ──
async function doRegister() {
const name = document.getElementById('regName').value.trim();
const uname = document.getElementById('regUsername').value.trim();
const pw = document.getElementById('regPassword').value;
const pw2 = document.getElementById('regPassword2').value;
const phone = document.getElementById('regPhone').value.trim();
const company = document.getElementById('regCompany').value;
const errEl = document.getElementById('regErr');
errEl.textContent = '';
if (!name) { errEl.textContent = '이름을 입력하세요.'; return; }
if (!uname) { errEl.textContent = '아이디를 입력하세요.'; return; }
if (!pw) { errEl.textContent = '비밀번호를 입력하세요.'; return; }
if (pw !== pw2) { errEl.textContent = '비밀번호가 일치하지 않습니다.'; return; }
document.getElementById('regBtn').disabled = true;
try {
const fd = new FormData();
fd.append('username', uname);
fd.append('password', pw);
fd.append('name', name);
fd.append('phone', phone);
fd.append('company', company);
fd.append('role', document.getElementById('regRole').value);
const res = await fetch('/api/auth/register', { method:'POST', body: fd });
if (!res.ok) { const e = await res.json(); throw new Error(e.detail); }
document.getElementById('regOk').style.display = 'block';
document.getElementById('regBtn').style.display = 'none';
['regName','regUsername','regPassword','regPassword2','regPhone'].forEach(id =>
document.getElementById(id).value = '');
document.getElementById('regCompany').value = '';
} catch(e) {
errEl.textContent = e.message;
document.getElementById('regBtn').disabled = false;
}
}
document.getElementById('loginBtn').addEventListener('click', doLogin);
document.getElementById('password').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
document.getElementById('regBtn').addEventListener('click', doRegister);
document.getElementById('regPassword2').addEventListener('keydown', e => { if(e.key==='Enter') doRegister(); });
</script>
</body>
</html>