Files
ev-charger-as/frontend/static/pages/admin/settings.html
2026-06-02 19:34:36 +09:00

484 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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:0018:00)</div>
<div style="font-size:12px;color:var(--gray4);margin-top:3px">주말·공휴일 제외 후, 평일 업무시간(09:0018: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>