809 lines
27 KiB
JavaScript
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();
|
|
});
|