2026. 5. 3. 19:57ใ์นดํ ๊ณ ๋ฆฌ ์์
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title></title>
<style>
body { font-family: sans-serif; padding: 10px; background-color: #f4f4f9; }
.controls { margin-bottom: 15px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.controls select, .btn { padding: 5px 12px; border-radius: 4px; border: 1px solid #ccc; cursor: pointer; font-size: 13px; }
.btn-save { background-color: #4CAF50; color: white; border: none; }
.btn-load { background-color: #2196F3; color: white; border: none; }
/* ์๋ฆผ ๋ฉ์์ง ์คํ์ผ */
#saveMessage { font-size: 12px; color: #4CAF50; font-weight: bold; opacity: 0; transition: opacity 0.3s; }
.table-container { overflow-x: auto; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 1px 5px rgba(0,0,0,0.1); }
table { border-collapse: collapse; width: 100%; min-width: 1400px; border: 1px solid #ccc}
th, td { border: 1px solid black; text-align: center; font-size: 11px; height: 35px; }
th { background-color: #eee; position: sticky; top: 0; z-index: 2; }
.group-cell { background-color: #f9f9f9; font-weight: bold; position: sticky; left: 0; z-index: 3; width: 80px; }
.schedule-select { width: 100%; height: 100%; border: none; outline: none; text-align-last: center; cursor: pointer; appearance: none; background: transparent; }
td { padding: 0; }
.s-day { background-color: #c7d32fb8 !important; font-weight: bold; }
.g-day { background-color: #6e9dcb !important; font-weight: bold; }
.d-day { background-color: #388e3c !important; font-weight: bold; }
.off-day { background-color: #d9195985 !important; }
.leave-day { background-color: #2196F3 !important; color: white !important; font-weight: bold; }
.unpaid-day { background-color: #795548 !important; color: white !important; font-weight: bold; }
.holiday-text { color: red !important; font-weight: bold; }
.holiday-header { color: red !important; background-color: #ffebee !important; }
.holiday-cell { background-color: #ffebee; }
</style>
</head>
<body>
<div class="controls">
<select id="yearSelect"></select>
<select id="monthSelect"></select>
<button class="btn btn-save" onclick="saveToFile()">๐พ </button>
<button class="btn btn-load" onclick="document.getElementById('fileInput').click()">๐ </button>
<span id="saveMessage">์ ์ฅ๋์์ต๋๋ค!</span>
<input type="file" id="fileInput" style="display:none" onchange="loadFromFile(event)">
</div>
<div class="table-container">
<table id="scheduleTable">
<thead>
<tr id="headerRow">
<th class="group-cell"> / ๋ ์ง</th>
</tr>
</thead>
<tbody id="scheduleBody"></tbody>
</table>
</div>
<script>
const holidays = ["2026-01-01", "2026-02-16", "2026-02-17", "2026-02-18", "2026-03-01", "2026-05-01", "2026-05-05", "2026-05-24", "2026-06-06", "2026-07-17", "2026-08-15", "2026-09-24", "2026-09-25", "2026-09-26", "2026-10-03", "2026-10-09", "2026-12-25"];
const members = [
{ name: '-1', group: 'A' }, { name: '-2', group: 'A' },
{ name: '-1', group: 'B' }, { name: '-2', group: 'B' },
{ name: '-1', group: 'C' }, { name: '-2', group: 'C' },
{ name: '-1', group: 'D' }, { name: '-2', group: 'D' }
];
const basePattern = ['a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a'];
const baseStartIndices = { 'A': 19, 'b': 4, 'C': 9, 'D': 14 };
const referenceDate = new Date(2026, 0, 1);
const workOptions = ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'];
let currentYear = new Date().getFullYear();
let currentMonth = new Date().getMonth();
function setupControls() {
const ySelect = document.getElementById('yearSelect');
const mSelect = document.getElementById('monthSelect');
for (let i = 2025; i <= 2030; i++) {
const opt = document.createElement('option');
opt.value = i; opt.innerText = i + "๋
";
if (i === currentYear) opt.selected = true;
ySelect.appendChild(opt);
}
for (let i = 0; i < 12; i++) {
const opt = document.createElement('option');
opt.value = i; opt.innerText = (i + 1) + "์";
if (i === currentMonth) opt.selected = true;
mSelect.appendChild(opt);
}
ySelect.onchange = () => { currentYear = parseInt(ySelect.value); initTable(); };
mSelect.onchange = () => { currentMonth = parseInt(mSelect.value); initTable(); };
}
function isHoliday(year, month, day) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const date = new Date(year, month, day);
return holidays.includes(dateStr) || date.getDay() === 0;
}
function updateCellColor(td, value, isRedDay) {
td.classList.remove('s-day', 'g-day', 'd-day', 'off-day', 'leave-day', 'unpaid-day', 'holiday-text');
if (value.includes('S')) td.classList.add('s-day');
else if (value.includes('G')) td.classList.add('g-day');
else if (value.includes('D')) td.classList.add('d-day');
else if (value === 'ํด') td.classList.add('off-day');
else if (value === '์ฐ') td.classList.add('leave-day');
else if (value === '๋ฌด') td.classList.add('unpaid-day');
if(isRedDay) td.classList.add('holiday-text');
}
function showSaveMessage() {
const msg = document.getElementById('saveMessage');
msg.style.opacity = '1';
setTimeout(() => { msg.style.opacity = '0'; }, 2000);
}
function saveToFile() {
const data = {};
const selects = document.querySelectorAll('.schedule-select');
selects.forEach((sel, idx) => { data[idx] = sel.value; });
const blob = new Blob([JSON.stringify({year: currentYear, month: currentMonth, data})], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `๋ฌ๋ ฅ_${currentYear}_${currentMonth}.json`;
a.click();
showSaveMessage();
}
window.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault(); // ๋ธ๋ผ์ฐ์ ๊ธฐ๋ณธ ์ ์ฅ์ฐฝ ๋ฐฉ์ง
saveToFile();
}
});
function loadFromFile(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const imported = JSON.parse(e.target.result);
currentYear = imported.year;
currentMonth = imported.month;
document.getElementById('yearSelect').value = currentYear;
document.getElementById('monthSelect').value = currentMonth;
initTable();
const selects = document.querySelectorAll('.schedule-select');
Object.keys(imported.data).forEach(idx => {
if(selects[idx]) {
selects[idx].value = imported.data[idx];
updateCellColor(selects[idx].parentElement, selects[idx].value, isHoliday(currentYear, currentMonth, parseInt(selects[idx].dataset.day)));
}
});
};
reader.readAsText(file);
}
function initTable() {
const headerRow = document.getElementById('headerRow');
const tbody = document.getElementById('scheduleBody');
headerRow.innerHTML = '<th class="group-cell"> / ๋ ์ง</th>';
tbody.innerHTML = '';
const lastDay = new Date(currentYear, currentMonth + 1, 0).getDate();
for (let i = 1; i <= lastDay; i++) {
const th = document.createElement('th'); th.innerText = i;
if (isHoliday(currentYear, currentMonth, i)) th.classList.add('holiday-header');
headerRow.appendChild(th);
}
members.forEach(member => {
const tr = document.createElement('tr');
const nameTd = document.createElement('td'); nameTd.className = 'group-cell'; nameTd.innerText = member.name;
tr.appendChild(nameTd);
for (let d = 1; d <= lastDay; d++) {
const targetDate = new Date(currentYear, currentMonth, d);
const diffDays = Math.floor((targetDate - referenceDate) / (1000 * 60 * 60 * 24));
let patternIdx = (baseStartIndices[member.group] + diffDays) % basePattern.length;
const currentWork = basePattern[patternIdx < 0 ? patternIdx + basePattern.length : patternIdx];
const td = document.createElement('td');
const redDay = isHoliday(currentYear, currentMonth, d);
if (redDay) td.classList.add('holiday-cell');
const select = document.createElement('select');
select.className = 'schedule-select';
select.dataset.day = d;
workOptions.forEach(opt => {
const option = document.createElement('option');
option.value = opt; option.innerText = opt;
if(opt === currentWork) option.selected = true;
select.appendChild(option);
});
select.onchange = () => updateCellColor(td, select.value, redDay);
updateCellColor(td, currentWork, redDay);
td.appendChild(select);
tr.appendChild(td);
}
tbody.appendChild(tr);
});
}
setupControls();
initTable();
</script>
</body>
</html>