Files

809 lines
27 KiB
JavaScript

/* ==========================================================================
PREMIUM RANDOM GROUP SELECTOR - APPLICATION LOGIC
========================================================================== */
document.addEventListener('DOMContentLoaded', () => {
// ------------------------------------------------------------------------
// State Variables
// ------------------------------------------------------------------------
let allMembers = [];
let remainingMembers = [];
let completedGroups = [];
let isDrawing = false;
// Member Table Array State
let memberTableData = [];
// Custom Groups State
let customGroups = [
{ id: 1, name: '그룹 1', count: 5 },
{ id: 2, name: '그룹 2', count: 4 },
{ id: 3, name: '그룹 3', count: 3 }
];
let customIdCounter = 4;
// ------------------------------------------------------------------------
// DOM Elements
// ------------------------------------------------------------------------
const settingsModal = document.getElementById('settings-modal');
const btnOpenModal = document.getElementById('btn-open-modal');
const btnOpenModalSub = document.getElementById('btn-open-modal-sub');
const btnCloseModal = document.getElementById('btn-close-modal');
const btnSaveModal = document.getElementById('btn-save-modal');
const modalTabBtns = document.querySelectorAll('.modal-tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane');
const currentModeIndicator = document.getElementById('current-mode-indicator');
const btnModeTable = document.getElementById('btn-mode-table');
const btnModeText = document.getElementById('btn-mode-text');
const tableViewContainer = document.getElementById('table-view-container');
const textViewContainer = document.getElementById('text-view-container');
const memberTableBody = document.getElementById('member-table-body');
const tableCountBadge = document.getElementById('table-count-badge');
const btnAddRow = document.getElementById('btn-add-row');
const btnClearTable = document.getElementById('btn-clear-table');
const memberInput = document.getElementById('member-input');
const btnPreset49num = document.getElementById('btn-preset-49num');
const btnPreset49names = document.getElementById('btn-preset-49names');
const statTotal = document.getElementById('stat-total');
const statRemaining = document.getElementById('stat-remaining');
const statGroups = document.getElementById('stat-groups');
const modePerGroup = document.getElementById('mode-per-group');
const modeTotalGroups = document.getElementById('mode-total-groups');
const modeCustomGroups = document.getElementById('mode-custom-groups');
const uniformModeControls = document.getElementById('uniform-mode-controls');
const modeSizeLabel = document.getElementById('mode-size-label');
const groupSizeVal = document.getElementById('group-size-val');
const btnSizeMinus = document.getElementById('btn-size-minus');
const btnSizePlus = document.getElementById('btn-size-plus');
const customBuilderContainer = document.getElementById('custom-builder-container');
const customGroupsList = document.getElementById('custom-groups-list');
const btnAddCustomGroup = document.getElementById('btn-add-custom-group');
const customTotalBadge = document.getElementById('custom-total-badge');
const toggleSound = document.getElementById('toggle-sound');
const btnDrawGroup = document.getElementById('btn-draw-group');
const btnDrawAll = document.getElementById('btn-draw-all');
const drawBtnText = document.getElementById('draw-btn-text');
const drawingGroupTitle = document.getElementById('drawing-group-title');
const drawingStatusText = document.getElementById('drawing-status-text');
const drawingCardsContainer = document.getElementById('drawing-cards-container');
const groupsContainer = document.getElementById('groups-container');
const emptyResults = document.getElementById('empty-results');
const btnCopyResults = document.getElementById('btn-copy-results');
const btnResetAll = document.getElementById('btn-reset-all');
// ------------------------------------------------------------------------
// Sample Data (49 Names)
// ------------------------------------------------------------------------
const SAMPLE_49_NAMES = [
'김민준', '이서준', '박도윤', '최예준', '정시우', '강하준', '조주원', '윤지호',
'장지후', '임준서', '한건우', '오우진', '서선우', '신연우', '권유준', '황은우',
'안민재', '송현우', '류유찬', '홍지훈', '고지석', '문이준', '양정우', '손유진',
'배승현', '백승민', '허시안', '유지원', '남지원', '심현준', '노이도', '하도현',
'곽태경', '성하진', '차윤성', '주원준', '우재원', '구지한', '신유현', '임서진',
'민서안', '진율', '지성현', '엄태윤', '채하민', '원도윤', '천서우', '방지호', '공유준'
];
// ------------------------------------------------------------------------
// Member Table Logic & Synchronization
// ------------------------------------------------------------------------
// Toggle Table vs Text View Mode
btnModeTable.addEventListener('click', () => {
syncTextToTableData();
btnModeTable.classList.add('active');
btnModeText.classList.remove('active');
tableViewContainer.classList.remove('hidden');
textViewContainer.classList.add('hidden');
renderMemberTable();
});
btnModeText.addEventListener('click', () => {
syncTableDataToText();
btnModeText.classList.add('active');
btnModeTable.classList.remove('active');
textViewContainer.classList.remove('hidden');
tableViewContainer.classList.add('hidden');
});
function renderMemberTable() {
memberTableBody.innerHTML = '';
tableCountBadge.textContent = `총 ${memberTableData.length}명`;
memberTableData.forEach((name, idx) => {
const tr = document.createElement('tr');
const tdIdx = document.createElement('td');
tdIdx.className = 'table-idx';
tdIdx.textContent = idx + 1;
const tdName = document.createElement('td');
const input = document.createElement('input');
input.type = 'text';
input.className = 'table-row-input';
input.value = name;
input.addEventListener('change', (e) => {
memberTableData[idx] = e.target.value.trim();
syncTableDataToText();
});
tdName.appendChild(input);
const tdAction = document.createElement('td');
tdAction.style.textAlign = 'center';
const btnRemove = document.createElement('button');
btnRemove.type = 'button';
btnRemove.className = 'btn-remove-row';
btnRemove.textContent = '✕';
btnRemove.title = '삭제';
btnRemove.addEventListener('click', () => {
memberTableData.splice(idx, 1);
renderMemberTable();
syncTableDataToText();
});
tdAction.appendChild(btnRemove);
tr.appendChild(tdIdx);
tr.appendChild(tdName);
tr.appendChild(tdAction);
memberTableBody.appendChild(tr);
});
}
function syncTextToTableData() {
const rawText = memberInput.value;
const list = rawText
.split(/[\n,]/)
.map(name => name.trim())
.filter(name => name.length > 0);
memberTableData = [...list];
}
function syncTableDataToText() {
const cleaned = memberTableData.filter(n => n && n.trim().length > 0);
memberInput.value = cleaned.join('\n');
}
// Add Row in Table
btnAddRow.addEventListener('click', () => {
memberTableData.push(`참가자 ${memberTableData.length + 1}`);
renderMemberTable();
syncTableDataToText();
const wrapper = document.querySelector('.member-table-wrapper');
if (wrapper) wrapper.scrollTop = wrapper.scrollHeight;
});
// Clear Table
btnClearTable.addEventListener('click', () => {
memberTableData = [];
memberInput.value = '';
renderMemberTable();
});
// Presets in Modal
btnPreset49num.addEventListener('click', () => {
const numbers = Array.from({ length: 49 }, (_, i) => `${i + 1}번`);
memberTableData = [...numbers];
syncTableDataToText();
renderMemberTable();
});
btnPreset49names.addEventListener('click', () => {
memberTableData = [...SAMPLE_49_NAMES];
syncTableDataToText();
renderMemberTable();
});
// ------------------------------------------------------------------------
// Modal Control Logic
// ------------------------------------------------------------------------
function openModal() {
settingsModal.classList.remove('hidden');
}
// Save/Apply
btnSaveModal.addEventListener('click', () => {
if (!textViewContainer.classList.contains('hidden')) {
syncTextToTableData();
} else {
syncTableDataToText();
}
loadMembersFromTableOrInput();
updateModeUI();
closeModal();
});
function closeModal() {
settingsModal.classList.add('hidden');
}
btnOpenModal.addEventListener('click', openModal);
if (btnOpenModalSub) btnOpenModalSub.addEventListener('click', openModal);
btnCloseModal.addEventListener('click', closeModal);
settingsModal.addEventListener('click', (e) => {
if (e.target === settingsModal) {
closeModal();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !settingsModal.classList.contains('hidden')) {
closeModal();
}
});
// Modal Tabs
modalTabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.getAttribute('data-tab');
modalTabBtns.forEach(b => b.classList.remove('active'));
tabPanes.forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(targetTab).classList.add('active');
});
});
// ------------------------------------------------------------------------
// Mode Toggle & Settings
// ------------------------------------------------------------------------
modePerGroup.addEventListener('change', updateModeUI);
modeTotalGroups.addEventListener('change', updateModeUI);
modeCustomGroups.addEventListener('change', updateModeUI);
function updateModeUI() {
let modeText = '';
if (modeCustomGroups.checked) {
uniformModeControls.classList.add('hidden');
customBuilderContainer.classList.remove('hidden');
modeText = '추첨 방식: 🛠️ 그룹별 명수 개별 지정 (커스텀)';
} else {
uniformModeControls.classList.remove('hidden');
customBuilderContainer.classList.add('hidden');
if (modePerGroup.checked) {
modeSizeLabel.textContent = '한 그룹당 인원수';
modeText = `추첨 방식: 그룹 당 ${groupSizeVal.value}명`;
} else {
modeSizeLabel.textContent = '생성할 총 그룹 수';
modeText = `추첨 방식: 총 ${groupSizeVal.value}개 그룹 균등`;
}
}
if (currentModeIndicator) {
currentModeIndicator.textContent = modeText;
}
}
// Stepper Buttons
btnSizeMinus.addEventListener('click', () => {
let val = parseInt(groupSizeVal.value) || 1;
if (val > 1) {
groupSizeVal.value = val - 1;
updateModeUI();
}
});
btnSizePlus.addEventListener('click', () => {
let val = parseInt(groupSizeVal.value) || 1;
if (val < 100) {
groupSizeVal.value = val + 1;
updateModeUI();
}
});
groupSizeVal.addEventListener('change', updateModeUI);
// Custom Groups Builder UI
btnAddCustomGroup.addEventListener('click', () => {
customGroups.push({
id: customIdCounter++,
name: `그룹 ${customGroups.length + 1}`,
count: 3
});
renderCustomGroupsBuilder();
});
function renderCustomGroupsBuilder() {
customGroupsList.innerHTML = '';
customGroups.forEach((cg, idx) => {
const row = document.createElement('div');
row.className = 'custom-group-row';
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'custom-group-name';
nameInput.value = cg.name;
nameInput.addEventListener('change', (e) => {
cg.name = e.target.value || `그룹 ${idx + 1}`;
});
const stepper = document.createElement('div');
stepper.className = 'number-stepper sm-stepper';
stepper.innerHTML = `
<button type="button" class="btn-cg-minus">-</button>
<input type="number" value="${cg.count}" min="1" max="50" class="input-cg-val">
<button type="button" class="btn-cg-plus">+</button>
`;
const inputVal = stepper.querySelector('.input-cg-val');
stepper.querySelector('.btn-cg-minus').addEventListener('click', () => {
if (cg.count > 1) {
cg.count--;
inputVal.value = cg.count;
updateCustomTotalBadge();
}
});
stepper.querySelector('.btn-cg-plus').addEventListener('click', () => {
cg.count++;
inputVal.value = cg.count;
updateCustomTotalBadge();
});
inputVal.addEventListener('change', (e) => {
let v = parseInt(e.target.value) || 1;
cg.count = Math.max(1, v);
inputVal.value = cg.count;
updateCustomTotalBadge();
});
const btnRemove = document.createElement('button');
btnRemove.type = 'button';
btnRemove.className = 'btn-remove-custom-group';
btnRemove.textContent = '✕';
btnRemove.title = '삭제';
btnRemove.addEventListener('click', () => {
if (customGroups.length <= 1) {
alert('최소 1개 이상의 그룹 설정이 필요합니다.');
return;
}
customGroups = customGroups.filter(item => item.id !== cg.id);
renderCustomGroupsBuilder();
});
row.appendChild(nameInput);
row.appendChild(stepper);
row.appendChild(btnRemove);
customGroupsList.appendChild(row);
});
updateCustomTotalBadge();
}
function updateCustomTotalBadge() {
const totalCount = customGroups.reduce((sum, item) => sum + item.count, 0);
customTotalBadge.textContent = `총 설정 인원: ${totalCount}명`;
}
// Sound Toggle
toggleSound.addEventListener('change', (e) => {
window.soundEffects.enabled = e.target.checked;
});
// Action Buttons
btnDrawGroup.addEventListener('click', () => drawNextGroup(false));
btnDrawAll.addEventListener('click', () => drawNextGroup(true));
btnResetAll.addEventListener('click', resetAllData);
btnCopyResults.addEventListener('click', copyResultsToClipboard);
// ------------------------------------------------------------------------
// Core Functions
// ------------------------------------------------------------------------
function loadMembersFromTableOrInput() {
const list = memberTableData.filter(name => name && name.trim().length > 0);
if (list.length === 0) {
const numbers = Array.from({ length: 49 }, (_, i) => `${i + 1}번`);
memberTableData = [...numbers];
syncTableDataToText();
renderMemberTable();
allMembers = [...numbers];
remainingMembers = [...numbers];
} else {
allMembers = [...list];
remainingMembers = [...list];
}
completedGroups = [];
updateUIStats();
renderGroups();
}
function resetAllData() {
allMembers = [];
remainingMembers = [];
completedGroups = [];
isDrawing = false;
updateUIStats();
renderGroups();
// Reset Stage to Initial Placeholder
drawingGroupTitle.textContent = 'READY';
if (drawingStatusText) drawingStatusText.textContent = '추첨 대기 중';
drawingCardsContainer.className = 'cards-grid grid-size-lg';
drawingCardsContainer.innerHTML = `
<div class="initial-placeholder">
<span class="placeholder-icon">✨</span>
<p>상단의 <strong>[다음 1개 그룹 추첨]</strong> 버튼을 눌러 그룹 편성을 시작하세요.</p>
</div>
`;
}
function updateUIStats() {
statTotal.textContent = allMembers.length;
statRemaining.textContent = remainingMembers.length;
statGroups.textContent = completedGroups.length;
if (remainingMembers.length === 0 && allMembers.length > 0) {
drawBtnText.textContent = '모든 추첨 완료';
btnDrawGroup.disabled = true;
btnDrawAll.disabled = true;
} else {
drawBtnText.textContent = '다음 1개 그룹 추첨';
btnDrawGroup.disabled = false;
btnDrawAll.disabled = false;
}
}
function shuffleArray(array) {
const arr = [...array];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function getNextGroupConfig() {
const totalRemaining = remainingMembers.length;
if (modeCustomGroups.checked) {
const nextCustomIndex = completedGroups.length;
if (nextCustomIndex < customGroups.length) {
const cg = customGroups[nextCustomIndex];
const targetCount = Math.min(totalRemaining, cg.count);
return { name: cg.name, count: targetCount };
} else {
const groupNum = completedGroups.length + 1;
const targetCount = Math.min(totalRemaining, 4);
return { name: `그룹 ${groupNum}`, count: targetCount };
}
} else {
const groupNum = completedGroups.length + 1;
const groupName = `GROUP ${groupNum}`;
const inputValue = parseInt(groupSizeVal.value) || 1;
if (modePerGroup.checked) {
const targetCount = Math.min(totalRemaining, Math.max(1, inputValue));
return { name: groupName, count: targetCount };
} else {
const desiredTotalGroups = Math.max(1, inputValue);
const remainingGroupCount = Math.max(1, desiredTotalGroups - completedGroups.length);
const memberCountPerGroup = Math.ceil(totalRemaining / remainingGroupCount);
const targetCount = Math.min(totalRemaining, memberCountPerGroup);
return { name: groupName, count: targetCount };
}
}
}
// ------------------------------------------------------------------------
// Group Draw Execution
// ------------------------------------------------------------------------
async function drawNextGroup(drawAll = false) {
if (isDrawing) return;
if (remainingMembers.length === 0) {
if (allMembers.length === 0) {
openModal();
alert('먼저 ⚙️ 설정 & 명단 모달에서 참가자 명단을 입력하세요.');
} else {
alert('모든 인원의 그룹 배치가 완료되었습니다.');
}
return;
}
isDrawing = true;
btnDrawGroup.disabled = true;
btnDrawAll.disabled = true;
const speedMode = document.querySelector('input[name="anim-speed"]:checked').value;
if (drawAll) {
while (remainingMembers.length > 0) {
const config = getNextGroupConfig();
await performSingleGroupDraw(config.name, config.count, speedMode);
}
} else {
const config = getNextGroupConfig();
await performSingleGroupDraw(config.name, config.count, speedMode);
}
isDrawing = false;
updateUIStats();
}
function performSingleGroupDraw(groupName, count, speedMode) {
return new Promise((resolve) => {
const shuffled = shuffleArray(remainingMembers);
const pickedMembers = shuffled.slice(0, count);
remainingMembers = remainingMembers.filter(m => !pickedMembers.includes(m));
// Header status text update
drawingGroupTitle.textContent = groupName;
if (drawingStatusText) drawingStatusText.textContent = '무작위 추출 중...';
// Reset cards container and assign grid scaling class
drawingCardsContainer.innerHTML = '';
drawingCardsContainer.className = 'cards-grid';
if (count <= 4) {
drawingCardsContainer.classList.add('grid-size-xl');
} else if (count <= 12) {
drawingCardsContainer.classList.add('grid-size-lg');
} else if (count <= 24) {
drawingCardsContainer.classList.add('grid-size-md');
} else {
drawingCardsContainer.classList.add('grid-size-sm');
}
if (speedMode === 'instant') {
pickedMembers.forEach((member, i) => {
createCardElement(i + 1, member, true);
});
if (drawingStatusText) drawingStatusText.textContent = '추첨 완료!';
finishGroupDraw(groupName, pickedMembers);
setTimeout(resolve, 200);
return;
}
const cardElements = pickedMembers.map((member, i) => {
return createCardElement(i + 1, member, false);
});
let spinInterval = null;
let tickCount = 0;
const spinDuration = speedMode === 'fast' ? 500 : 1200;
spinInterval = setInterval(() => {
tickCount++;
if (tickCount % 2 === 0) {
window.soundEffects.playTick();
}
cardElements.forEach(cardObj => {
if (!cardObj.revealed) {
const randIdx = Math.floor(Math.random() * allMembers.length);
cardObj.reelElement.textContent = allMembers[randIdx];
}
});
}, 50);
setTimeout(() => {
clearInterval(spinInterval);
const flipStagger = speedMode === 'fast' ? 80 : 180;
pickedMembers.forEach((member, idx) => {
setTimeout(() => {
const cardObj = cardElements[idx];
cardObj.revealed = true;
cardObj.element.classList.add('revealed');
window.soundEffects.playReveal();
if (idx === pickedMembers.length - 1) {
setTimeout(() => {
window.soundEffects.playFanfare();
triggerConfetti();
if (drawingStatusText) drawingStatusText.textContent = '추첨 완료!';
finishGroupDraw(groupName, pickedMembers);
resolve();
}, 300);
}
}, idx * flipStagger);
});
}, spinDuration);
});
}
function createCardElement(index, finalName, instantRevealed) {
const card = document.createElement('div');
card.className = `draw-card ${instantRevealed ? 'revealed' : ''}`;
const inner = document.createElement('div');
inner.className = 'card-inner';
const front = document.createElement('div');
front.className = 'card-front';
const reel = document.createElement('span');
reel.className = 'slot-reel spinning';
reel.textContent = '???';
front.appendChild(reel);
const back = document.createElement('div');
back.className = 'card-back';
const numSpan = document.createElement('span');
numSpan.className = 'member-num';
numSpan.textContent = `MEMBER ${index}`;
const nameSpan = document.createElement('span');
nameSpan.className = 'member-name';
nameSpan.textContent = finalName;
back.appendChild(numSpan);
back.appendChild(nameSpan);
inner.appendChild(front);
inner.appendChild(back);
card.appendChild(inner);
drawingCardsContainer.appendChild(card);
return {
element: card,
reelElement: reel,
revealed: instantRevealed
};
}
function finishGroupDraw(groupName, members) {
completedGroups.push({
name: groupName,
members: members
});
renderGroups();
updateUIStats();
}
function renderGroups() {
groupsContainer.innerHTML = '';
if (completedGroups.length === 0) {
groupsContainer.appendChild(emptyResults);
emptyResults.style.display = 'block';
return;
}
emptyResults.style.display = 'none';
completedGroups.forEach(group => {
const card = document.createElement('div');
card.className = 'group-card';
const header = document.createElement('div');
header.className = 'group-card-header';
header.innerHTML = `
<span class="title">${group.name}</span>
<span class="count">${group.members.length}명</span>
`;
const membersList = document.createElement('div');
membersList.className = 'group-members-list';
group.members.forEach((m, idx) => {
const badge = document.createElement('span');
badge.className = 'member-badge';
badge.innerHTML = `<span class="num">${idx + 1}</span> ${m}`;
membersList.appendChild(badge);
});
card.appendChild(header);
card.appendChild(membersList);
groupsContainer.appendChild(card);
});
}
function copyResultsToClipboard() {
if (completedGroups.length === 0) {
alert('복사할 그룹 추첨 결과가 없습니다.');
return;
}
let text = `[ 무작위 그룹 추첨 결과 ]\n\n`;
completedGroups.forEach(g => {
text += `■ ${g.name} (${g.members.length}명)\n`;
text += `${g.members.join(', ')}\n\n`;
});
navigator.clipboard.writeText(text).then(() => {
alert('추첨 결과가 클립보드에 복사되었습니다!');
}).catch(() => {
alert('복사에 실패했습니다. 직접 선택하여 복사해주세요.');
});
}
// Confetti Animation (Canvas)
function triggerConfetti() {
const canvas = document.getElementById('confetti-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const colors = ['#06b6d4', '#8b5cf6', '#10b981', '#f43f5e', '#f59e0b', '#ffffff'];
for (let i = 0; i < 70; i++) {
particles.push({
x: canvas.width / 2,
y: canvas.height / 2 + 50,
vx: (Math.random() - 0.5) * 18,
vy: (Math.random() - 0.7) * 18,
size: Math.random() * 8 + 4,
color: colors[Math.floor(Math.random() * colors.length)],
rotation: Math.random() * 360,
rSpeed: (Math.random() - 0.5) * 10,
opacity: 1
});
}
let startTime = Date.now();
function animate() {
const elapsed = Date.now() - startTime;
if (elapsed > 1600) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.4;
p.rotation += p.rSpeed;
p.opacity -= 0.015;
if (p.opacity > 0) {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate((p.rotation * Math.PI) / 180);
ctx.globalAlpha = Math.max(0, p.opacity);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
}
});
requestAnimationFrame(animate);
}
animate();
}
// Window Resize handle
window.addEventListener('resize', () => {
const canvas = document.getElementById('confetti-canvas');
if (canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
});
// Initialize Default State
renderCustomGroupsBuilder();
updateModeUI();
// Load default 49 numbers
const defaultNumbers = Array.from({ length: 49 }, (_, i) => `${i + 1}번`);
memberTableData = [...defaultNumbers];
syncTableDataToText();
renderMemberTable();
loadMembersFromTableOrInput();
});