/* ========================================================================== 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 = ` `; 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 = `

상단의 [다음 1개 그룹 추첨] 버튼을 눌러 그룹 편성을 시작하세요.

`; } 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 = ` ${group.name} ${group.members.length}명 `; 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 = `${idx + 1} ${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(); });