feat: init premium random group selector with custom sizes and scroll prevention
This commit is contained in:
@@ -0,0 +1,808 @@
|
||||
/* ==========================================================================
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user