484 lines
25 KiB
HTML
484 lines
25 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">
|
||
<style>
|
||
.range-wrap{display:flex;align-items:center;gap:12px;}
|
||
.range-wrap input[type=range]{flex:1;accent-color:var(--accent);}
|
||
.range-val{min-width:48px;text-align:center;font-weight:700;color:var(--navy);font-size:14px;}
|
||
.toggle-row{display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--gray2);}
|
||
.toggle-row:last-child{border-bottom:none;}
|
||
.toggle-label h4{font-size:14px;font-weight:700;color:var(--navy);}
|
||
.toggle-label p{font-size:12px;color:var(--gray4);margin-top:2px;}
|
||
.toggle{position:relative;width:44px;height:24px;flex-shrink:0;}
|
||
.toggle input{opacity:0;width:0;height:0;}
|
||
.toggle-slider{position:absolute;inset:0;background:var(--gray3);border-radius:24px;cursor:pointer;transition:.2s;}
|
||
.toggle-slider::before{content:'';position:absolute;width:18px;height:18px;left:3px;bottom:3px;background:white;border-radius:50%;transition:.2s;}
|
||
.toggle input:checked + .toggle-slider{background:var(--accent);}
|
||
.toggle input:checked + .toggle-slider::before{transform:translateX(20px);}
|
||
.preset-btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;}
|
||
.preset-btn{padding:5px 14px;border-radius:6px;border:1px solid var(--gray3);background:white;font-size:12px;cursor:pointer;transition:all .15s;font-family:'Noto Sans KR',sans-serif;}
|
||
.preset-btn:hover{border-color:var(--accent);color:var(--accent);}
|
||
.preset-btn.active{border-color:var(--accent);background:#E3EDFF;color:var(--blue);font-weight:700;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav class="nav"><div style="display:flex;align-items:center;gap:2px;"><button class="nav-hamburger" onclick="toggleSidebar()">☰</button><span class="nav-brand">⚡ EV AS 관리 — 관리자</span></div><div id="navUser"></div></nav>
|
||
<div class="mobile-nav-overlay" id="navOverlay" onclick="toggleSidebar()"></div>
|
||
<div class="layout">
|
||
<div class="sidebar" id="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">🔧 개선항목</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" class="active">⚙️ 설정</a>
|
||
</div>
|
||
<div class="main">
|
||
<h2 style="font-size:18px;font-weight:700;color:var(--navy);margin-bottom:18px">시스템 설정</h2>
|
||
|
||
<!-- 신고 공개 정책 -->
|
||
<div class="card" style="max-width:560px">
|
||
<div class="card-title">📋 신고 공개 정책</div>
|
||
<div class="alert alert-info" style="margin-bottom:14px">
|
||
신고 접수 시 정비사에게 공개하는 방식을 선택합니다.
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="check-item" style="display:flex;gap:12px;padding:14px;margin-bottom:8px;cursor:pointer;border-radius:8px;border:2px solid var(--gray3)" id="lbl-immediate">
|
||
<input type="radio" name="policy" value="immediate" style="accent-color:var(--accent);width:auto;margin-top:2px;flex-shrink:0">
|
||
<div>
|
||
<div style="font-weight:700">⚡ 즉시 공개 (기본 권장)</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-top:3px">신고 접수 즉시 정비사 목록에 표시됩니다. 빠른 대응이 가능합니다.</div>
|
||
</div>
|
||
</label>
|
||
<label class="check-item" style="display:flex;gap:12px;padding:14px;cursor:pointer;border-radius:8px;border:2px solid var(--gray3)" id="lbl-approval">
|
||
<input type="radio" name="policy" value="admin_approval" style="accent-color:var(--accent);width:auto;margin-top:2px;flex-shrink:0">
|
||
<div>
|
||
<div style="font-weight:700">🔒 관리자 승인 후 공개</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-top:3px">관리자가 신고를 확인·승인한 후 정비사에게 공개됩니다. 중복·허위 신고 방지에 유리합니다.</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
<div id="saveOk" class="alert alert-success" style="display:none">설정이 저장되었습니다.</div>
|
||
<button class="btn btn-primary" onclick="saveAll()" style="margin-top:4px">전체 설정 저장</button>
|
||
</div>
|
||
|
||
<!-- 처리시간 지표 기준 -->
|
||
<div class="card" style="max-width:560px;margin-top:20px">
|
||
<div class="card-title">⏱ 처리시간 지표 기준</div>
|
||
<div class="alert alert-info" style="margin-bottom:14px">
|
||
대시보드의 <strong>처리시간 평균</strong> 및 <strong>대기 심각도</strong> 지표를 계산할 때<br>
|
||
시작 시점으로 사용할 기준을 선택합니다.
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="check-item" style="display:flex;gap:12px;padding:14px;margin-bottom:8px;cursor:pointer;border-radius:8px;border:2px solid var(--gray3)" id="lbl-occurred">
|
||
<input type="radio" name="timeBase" value="occurred" style="accent-color:var(--accent);width:auto;margin-top:2px;flex-shrink:0">
|
||
<div>
|
||
<div style="font-weight:700">📅 발생시각 기준 (권장)</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-top:3px">실제 고장이 발생한 시각부터 계산합니다. 발생시각이 없으면 등록시간으로 대체됩니다.</div>
|
||
</div>
|
||
</label>
|
||
<label class="check-item" style="display:flex;gap:12px;padding:14px;cursor:pointer;border-radius:8px;border:2px solid var(--gray3)" id="lbl-reported">
|
||
<input type="radio" name="timeBase" value="reported" style="accent-color:var(--accent);width:auto;margin-top:2px;flex-shrink:0">
|
||
<div>
|
||
<div style="font-weight:700">🕐 등록시간 기준</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-top:3px">신고가 시스템에 접수된 시각부터 계산합니다.</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 처리시간 집계 방식 -->
|
||
<div class="card" style="max-width:560px;margin-top:20px">
|
||
<div class="card-title">🏢 처리시간 집계 방식</div>
|
||
<div class="alert alert-info" style="margin-bottom:14px">
|
||
대기·처리시간 지표를 산출할 때 공휴일·주말을 처리하는 방식을 선택합니다.
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="check-item" style="display:flex;gap:12px;padding:14px;margin-bottom:8px;cursor:pointer;border-radius:8px;border:2px solid var(--gray3)" id="lbl-wt-off">
|
||
<input type="radio" name="worktimeMode" value="off" style="accent-color:var(--accent);width:auto;margin-top:2px;flex-shrink:0" onchange="updateWorktimeModeLabels()">
|
||
<div>
|
||
<div style="font-weight:700">📅 달력 기준 (기본)</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-top:3px">주말·공휴일 포함 모든 경과시간을 그대로 집계합니다.</div>
|
||
</div>
|
||
</label>
|
||
<label class="check-item" style="display:flex;gap:12px;padding:14px;margin-bottom:8px;cursor:pointer;border-radius:8px;border:2px solid var(--gray3)" id="lbl-wt-holiday24h">
|
||
<input type="radio" name="worktimeMode" value="holiday_24h" style="accent-color:var(--accent);width:auto;margin-top:2px;flex-shrink:0" onchange="updateWorktimeModeLabels()">
|
||
<div>
|
||
<div style="font-weight:700">🗓 공휴일 제외 24시간</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-top:3px">공휴일만 제외하고, 주말을 포함한 나머지 날은 하루 24시간 전체를 카운트합니다.</div>
|
||
</div>
|
||
</label>
|
||
<label class="check-item" style="display:flex;gap:12px;padding:14px;cursor:pointer;border-radius:8px;border:2px solid var(--gray3)" id="lbl-wt-worktime">
|
||
<input type="radio" name="worktimeMode" value="worktime" style="accent-color:var(--accent);width:auto;margin-top:2px;flex-shrink:0" onchange="updateWorktimeModeLabels()">
|
||
<div>
|
||
<div style="font-weight:700">💼 업무시간 기준 (09:00–18:00)</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-top:3px">주말·공휴일 제외 후, 평일 업무시간(09:00–18:00) 내 경과시간만 집계합니다.</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- 공휴일 관리 (공휴일 제외 모드일 때만 표시) -->
|
||
<div id="holidaySection" style="display:none;margin-top:18px;border-top:1px solid var(--gray2);padding-top:16px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px">
|
||
<div style="font-size:13px;font-weight:700;color:var(--navy)">
|
||
📅 공휴일 관리
|
||
<select id="holidayYear" onchange="loadHolidays()" style="margin-left:10px;width:auto;font-size:13px;padding:4px 8px">
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||
<button class="btn btn-sm btn-outline" onclick="addFixedHolidays()">📋 고정 공휴일 추가</button>
|
||
<button class="btn btn-sm btn-primary" onclick="openHolidayModal()">+ 공휴일 추가</button>
|
||
</div>
|
||
</div>
|
||
<div style="font-size:12px;color:var(--gray4);margin-bottom:10px;background:#FFFBEB;border:1px solid #FDE68A;border-radius:6px;padding:8px 12px">
|
||
⚠ <strong>설날·추석·부처님오신날</strong> 등 음력 공휴일과 <strong>대체공휴일</strong>은 매년 직접 추가해야 합니다.
|
||
</div>
|
||
<div id="holidayList" style="max-height:300px;overflow-y:auto">
|
||
<div style="color:var(--gray4);font-size:13px;text-align:center;padding:20px">불러오는 중...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 공휴일 추가 모달 -->
|
||
<div class="modal-bg hidden" id="holidayModal">
|
||
<div class="modal" style="max-width:380px">
|
||
<div class="modal-title">공휴일 추가</div>
|
||
<div class="form-group">
|
||
<label>날짜 <span class="req">*</span></label>
|
||
<input type="date" id="hDate">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>공휴일명 <span class="req">*</span></label>
|
||
<input type="text" id="hName" placeholder="예) 추석">
|
||
</div>
|
||
<div id="hErr" class="alert alert-danger" style="display:none"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-outline" onclick="closeHolidayModal()">취소</button>
|
||
<button class="btn btn-primary" onclick="saveHoliday()">추가</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 이미지 압축 설정 -->
|
||
<div class="card" style="max-width:560px;margin-top:20px">
|
||
<div class="card-title">🖼️ 사진 업로드 압축 설정</div>
|
||
<div class="alert alert-info" style="margin-bottom:16px">
|
||
신고·조치 사진 업로드 시 브라우저에서 자동으로 압축합니다.<br>
|
||
서버 저장 용량 절약 및 업로드 속도를 개선합니다.
|
||
</div>
|
||
|
||
<!-- 압축 ON/OFF -->
|
||
<div class="toggle-row">
|
||
<div class="toggle-label">
|
||
<h4>📸 자동 압축 사용</h4>
|
||
<p>업로드 전 이미지를 자동으로 리사이즈·압축합니다</p>
|
||
</div>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="compressEnabled">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- 최대 해상도 -->
|
||
<div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:12px;">
|
||
<div class="toggle-label">
|
||
<h4>📐 최대 해상도 (긴 변 기준)</h4>
|
||
<p>이 픽셀 수를 초과하면 비율 유지하며 축소합니다</p>
|
||
</div>
|
||
<div style="width:100%">
|
||
<div class="preset-btns" id="presetBtns">
|
||
<button class="preset-btn" onclick="setMaxPx(640)" data-px="640" >640px <small style="color:var(--gray4)">저화질</small></button>
|
||
<button class="preset-btn" onclick="setMaxPx(1024)" data-px="1024">1024px <small style="color:var(--gray4)">권장 ★</small></button>
|
||
<button class="preset-btn" onclick="setMaxPx(1920)" data-px="1920">1920px <small style="color:var(--gray4)">FHD</small></button>
|
||
<button class="preset-btn" onclick="setMaxPx(2560)" data-px="2560">2560px <small style="color:var(--gray4)">QHD</small></button>
|
||
<button class="preset-btn" onclick="setMaxPx(3840)" data-px="3840">3840px <small style="color:var(--gray4)">4K</small></button>
|
||
</div>
|
||
<div class="range-wrap" style="margin-top:10px;">
|
||
<input type="range" id="maxPx" min="320" max="3840" step="64"
|
||
value="1024" oninput="syncMaxPx(this.value)">
|
||
<span class="range-val" id="maxPxVal">1024px</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JPEG 품질 -->
|
||
<div class="toggle-row" style="flex-direction:column;align-items:flex-start;gap:10px;border-bottom:none;">
|
||
<div class="toggle-label">
|
||
<h4>🎨 JPEG 압축 품질</h4>
|
||
<p>높을수록 화질 좋고 용량 큼 / 낮을수록 용량 작고 화질 저하</p>
|
||
</div>
|
||
<div style="width:100%">
|
||
<div class="preset-btns" id="qualityPresets">
|
||
<button class="preset-btn" onclick="setQuality(60)" data-q="60" >60% <small style="color:var(--gray4)">압축 최대</small></button>
|
||
<button class="preset-btn" onclick="setQuality(75)" data-q="75" >75% <small style="color:var(--gray4)">균형</small></button>
|
||
<button class="preset-btn" onclick="setQuality(85)" data-q="85" >85% <small style="color:var(--gray4)">권장 ★</small></button>
|
||
<button class="preset-btn" onclick="setQuality(95)" data-q="95" >95% <small style="color:var(--gray4)">고화질</small></button>
|
||
</div>
|
||
<div class="range-wrap" style="margin-top:10px;">
|
||
<input type="range" id="quality" min="30" max="100" step="5"
|
||
value="85" oninput="syncQuality(this.value)">
|
||
<span class="range-val" id="qualityVal">85%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 현재 효과 미리보기 텍스트 -->
|
||
<div id="effectDesc" style="background:var(--gray1);border-radius:8px;padding:12px 14px;font-size:13px;color:var(--text);margin-top:4px;line-height:1.7;">
|
||
</div>
|
||
|
||
<div id="imgSaveOk" class="alert alert-success" style="display:none;margin-top:12px">이미지 설정이 저장되었습니다.</div>
|
||
<button class="btn btn-primary" onclick="saveAll()" style="margin-top:12px">전체 설정 저장</button>
|
||
</div>
|
||
|
||
<!-- 비밀번호 변경 -->
|
||
<div class="card" style="max-width:560px;margin-top:20px">
|
||
<div class="card-title">🔑 내 비밀번호 변경</div>
|
||
<div class="form-group"><label>현재 비밀번호</label><input type="password" id="curPw"></div>
|
||
<div class="form-group"><label>새 비밀번호</label><input type="password" id="newPw"></div>
|
||
<div class="form-group"><label>새 비밀번호 확인</label><input type="password" id="newPw2"></div>
|
||
<div id="pwErr" class="alert alert-danger" style="display:none"></div>
|
||
<div id="pwOk" class="alert alert-success" style="display:none">비밀번호가 변경되었습니다.</div>
|
||
<button class="btn btn-outline" onclick="changePw()">비밀번호 변경</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script src="/js/auth.js"></script>
|
||
<script>
|
||
Auth.require(['admin']); Auth.renderNav(document.getElementById('navUser'));
|
||
|
||
function syncMaxPx(v) {
|
||
document.getElementById('maxPxVal').textContent = v + 'px';
|
||
updatePresetBtns('presetBtns', 'data-px', v);
|
||
updateEffect();
|
||
}
|
||
function syncQuality(v) {
|
||
document.getElementById('qualityVal').textContent = v + '%';
|
||
updatePresetBtns('qualityPresets', 'data-q', v);
|
||
updateEffect();
|
||
}
|
||
function setMaxPx(v) {
|
||
document.getElementById('maxPx').value = v;
|
||
syncMaxPx(v);
|
||
}
|
||
function setQuality(v) {
|
||
document.getElementById('quality').value = v;
|
||
syncQuality(v);
|
||
}
|
||
function updatePresetBtns(containerId, attr, val) {
|
||
document.querySelectorAll(`#${containerId} .preset-btn`).forEach(b => {
|
||
b.classList.toggle('active', b.getAttribute(attr) == val);
|
||
});
|
||
}
|
||
function updateEffect() {
|
||
const enabled = document.getElementById('compressEnabled').checked;
|
||
const px = parseInt(document.getElementById('maxPx').value);
|
||
const q = parseInt(document.getElementById('quality').value);
|
||
const el = document.getElementById('effectDesc');
|
||
if (!enabled) {
|
||
el.innerHTML = '⚪ 압축 비활성 — 원본 파일 그대로 업로드됩니다.';
|
||
el.style.color = 'var(--gray4)';
|
||
return;
|
||
}
|
||
// 대략적 용량 절약 예측 (12MP 스마트폰 사진 기준 ~8MB)
|
||
const areaRatio = Math.min(1, (px * px) / (4032 * 3024));
|
||
const estMB = (8 * areaRatio * (q / 100) * 0.6).toFixed(1);
|
||
el.innerHTML = `✅ <strong>${px}px / JPEG ${q}%</strong> 로 압축<br>
|
||
스마트폰 고화질 사진(약 8MB) 기준 → 업로드 약 <strong>${estMB}MB</strong> 예상<br>
|
||
<span style="color:var(--green);font-size:12px">업로드 속도 향상 + 서버 용량 절약</span>`;
|
||
el.style.color = 'var(--text)';
|
||
}
|
||
|
||
async function load() {
|
||
const s = await API.get('/settings');
|
||
const policy = s.report_visibility_policy || 'immediate';
|
||
document.querySelector(`input[name="policy"][value="${policy}"]`).checked = true;
|
||
updateLabels();
|
||
|
||
const timeBase = s.time_metric_base || 'occurred';
|
||
document.querySelector(`input[name="timeBase"][value="${timeBase}"]`).checked = true;
|
||
updateTimeBaseLabels();
|
||
|
||
const wtMode = ['off','holiday_24h','worktime'].includes(s.time_metric_worktime)
|
||
? s.time_metric_worktime
|
||
: (s.time_metric_worktime === 'true' ? 'worktime' : 'off');
|
||
const wtRadio = document.querySelector(`input[name="worktimeMode"][value="${wtMode}"]`);
|
||
if (wtRadio) wtRadio.checked = true;
|
||
updateWorktimeModeLabels();
|
||
|
||
const enabled = s.image_compress_enabled !== 'false';
|
||
document.getElementById('compressEnabled').checked = enabled;
|
||
|
||
const px = parseInt(s.image_max_px || '1024');
|
||
document.getElementById('maxPx').value = px;
|
||
syncMaxPx(px);
|
||
|
||
const q = parseInt(s.image_quality || '85');
|
||
document.getElementById('quality').value = q;
|
||
syncQuality(q);
|
||
|
||
updateEffect();
|
||
}
|
||
|
||
function updateLabels() {
|
||
document.querySelectorAll('input[name="policy"]').forEach(r => {
|
||
const lbl = r.closest('label');
|
||
lbl.style.borderColor = r.checked ? 'var(--accent)' : 'var(--gray3)';
|
||
lbl.style.background = r.checked ? '#E3EDFF' : 'white';
|
||
});
|
||
}
|
||
function updateTimeBaseLabels() {
|
||
document.querySelectorAll('input[name="timeBase"]').forEach(r => {
|
||
const lbl = r.closest('label');
|
||
lbl.style.borderColor = r.checked ? 'var(--accent)' : 'var(--gray3)';
|
||
lbl.style.background = r.checked ? '#E3EDFF' : 'white';
|
||
});
|
||
}
|
||
document.querySelectorAll('input[name="policy"]').forEach(r => r.addEventListener('change', updateLabels));
|
||
document.querySelectorAll('input[name="timeBase"]').forEach(r => r.addEventListener('change', updateTimeBaseLabels));
|
||
document.getElementById('compressEnabled').addEventListener('change', updateEffect);
|
||
|
||
async function saveAll() {
|
||
const fd = new FormData();
|
||
fd.append('report_visibility_policy', document.querySelector('input[name="policy"]:checked').value);
|
||
fd.append('time_metric_base', document.querySelector('input[name="timeBase"]:checked').value);
|
||
fd.append('time_metric_worktime', document.querySelector('input[name="worktimeMode"]:checked').value);
|
||
fd.append('image_compress_enabled', document.getElementById('compressEnabled').checked ? 'true' : 'false');
|
||
fd.append('image_max_px', document.getElementById('maxPx').value);
|
||
fd.append('image_quality', document.getElementById('quality').value);
|
||
await API.put('/settings', fd);
|
||
|
||
const ok = document.getElementById('saveOk');
|
||
ok.style.display = 'block';
|
||
setTimeout(() => ok.style.display = 'none', 2500);
|
||
const ok2 = document.getElementById('imgSaveOk');
|
||
ok2.style.display = 'block';
|
||
setTimeout(() => ok2.style.display = 'none', 2500);
|
||
}
|
||
|
||
async function changePw() {
|
||
const cur = document.getElementById('curPw').value;
|
||
const nw = document.getElementById('newPw').value;
|
||
const nw2 = document.getElementById('newPw2').value;
|
||
const errEl = document.getElementById('pwErr');
|
||
errEl.style.display = 'none';
|
||
if (!cur || !nw) { errEl.textContent = '현재·새 비밀번호를 입력하세요.'; errEl.style.display = 'block'; return; }
|
||
if (nw !== nw2) { errEl.textContent = '새 비밀번호가 일치하지 않습니다.'; errEl.style.display = 'block'; return; }
|
||
const fd = new FormData(); fd.append('current_password', cur); fd.append('new_password', nw);
|
||
try {
|
||
await API.patch('/accounts/me/password', fd);
|
||
document.getElementById('pwOk').style.display = 'block';
|
||
['curPw','newPw','newPw2'].forEach(id => document.getElementById(id).value = '');
|
||
setTimeout(() => document.getElementById('pwOk').style.display = 'none', 2500);
|
||
} catch(e) { errEl.textContent = e.message; errEl.style.display = 'block'; }
|
||
}
|
||
|
||
// ── 처리시간 집계 방식 ──
|
||
function updateWorktimeModeLabels() {
|
||
document.querySelectorAll('input[name="worktimeMode"]').forEach(r => {
|
||
const lbl = r.closest('label');
|
||
lbl.style.borderColor = r.checked ? 'var(--accent)' : 'var(--gray3)';
|
||
lbl.style.background = r.checked ? '#E3EDFF' : 'white';
|
||
});
|
||
const mode = document.querySelector('input[name="worktimeMode"]:checked')?.value || 'off';
|
||
const showHoliday = mode === 'holiday_24h' || mode === 'worktime';
|
||
document.getElementById('holidaySection').style.display = showHoliday ? 'block' : 'none';
|
||
if (showHoliday && !document.getElementById('holidayYear').options.length) initHolidayYear();
|
||
}
|
||
|
||
function initHolidayYear() {
|
||
const sel = document.getElementById('holidayYear');
|
||
const cur = new Date().getFullYear();
|
||
for (let y = cur + 1; y >= cur - 2; y--) {
|
||
const opt = document.createElement('option');
|
||
opt.value = y; opt.textContent = y + '년';
|
||
if (y === cur) opt.selected = true;
|
||
sel.appendChild(opt);
|
||
}
|
||
loadHolidays();
|
||
}
|
||
|
||
async function loadHolidays() {
|
||
const year = document.getElementById('holidayYear').value;
|
||
const list = await API.get('/holidays?year=' + year);
|
||
const el = document.getElementById('holidayList');
|
||
if (!list.length) {
|
||
el.innerHTML = '<div style="color:var(--gray4);font-size:13px;text-align:center;padding:20px">등록된 공휴일이 없습니다.</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||
<thead><tr style="background:var(--gray2)">
|
||
<th style="padding:7px 10px;text-align:left">날짜</th>
|
||
<th style="padding:7px 10px;text-align:left">공휴일명</th>
|
||
<th style="padding:7px 10px;width:50px"></th>
|
||
</tr></thead>
|
||
<tbody>${list.map(h => `
|
||
<tr style="border-bottom:1px solid var(--gray2)">
|
||
<td style="padding:7px 10px">${h.date}</td>
|
||
<td style="padding:7px 10px">${h.name}</td>
|
||
<td style="padding:7px 10px;text-align:center">
|
||
<button onclick="deleteHoliday('${h.date}')" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:15px" title="삭제">✕</button>
|
||
</td>
|
||
</tr>`).join('')}
|
||
</tbody></table>`;
|
||
}
|
||
|
||
function openHolidayModal() {
|
||
document.getElementById('holidayModal').classList.remove('hidden');
|
||
document.getElementById('hErr').style.display = 'none';
|
||
document.getElementById('hDate').value = '';
|
||
document.getElementById('hName').value = '';
|
||
}
|
||
function closeHolidayModal() { document.getElementById('holidayModal').classList.add('hidden'); }
|
||
|
||
async function saveHoliday() {
|
||
const d = document.getElementById('hDate').value;
|
||
const n = document.getElementById('hName').value.trim();
|
||
const errEl = document.getElementById('hErr');
|
||
if (!d || !n) { errEl.textContent = '날짜와 공휴일명을 입력하세요.'; errEl.style.display = 'block'; return; }
|
||
try {
|
||
const fd = new FormData(); fd.append('holiday_date', d); fd.append('name', n);
|
||
await API.post('/holidays', fd);
|
||
closeHolidayModal(); loadHolidays();
|
||
} catch(e) { errEl.textContent = e.message; errEl.style.display = 'block'; }
|
||
}
|
||
|
||
async function deleteHoliday(date) {
|
||
if (!confirm(`${date} 공휴일을 삭제하시겠습니까?`)) return;
|
||
await API.delete('/holidays/' + date);
|
||
loadHolidays();
|
||
}
|
||
|
||
// 고정 공휴일 (양력) 일괄 추가
|
||
async function addFixedHolidays() {
|
||
const year = parseInt(document.getElementById('holidayYear').value);
|
||
const fixed = [
|
||
{ date: `${year}-01-01`, name: '신정' },
|
||
{ date: `${year}-03-01`, name: '삼일절' },
|
||
{ date: `${year}-05-05`, name: '어린이날' },
|
||
{ date: `${year}-06-06`, name: '현충일' },
|
||
{ date: `${year}-08-15`, name: '광복절' },
|
||
{ date: `${year}-10-03`, name: '개천절' },
|
||
{ date: `${year}-10-09`, name: '한글날' },
|
||
{ date: `${year}-12-25`, name: '성탄절' },
|
||
];
|
||
const res = await API.post('/holidays/bulk', fixed);
|
||
alert(`${res.added}개 고정 공휴일이 추가되었습니다.\n설날·추석·부처님오신날·대체공휴일은 직접 추가해 주세요.`);
|
||
loadHolidays();
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|