From e910696fbef46e148cde13e3d1107795b38b0a58 Mon Sep 17 00:00:00 2001 From: JooWangi Date: Wed, 22 Jul 2026 12:27:24 +0900 Subject: [PATCH] feat: init premium random group selector with custom sizes and scroll prevention --- app.js | 808 +++++++++++++++++++++++++++++ audio.js | 105 ++++ index.html | 261 ++++++++++ style.css | 1199 +++++++++++++++++++++++++++++++++++++++++++ 총괄기획실 인원.txt | 47 ++ 5 files changed, 2420 insertions(+) create mode 100644 app.js create mode 100644 audio.js create mode 100644 index.html create mode 100644 style.css create mode 100644 총괄기획실 인원.txt diff --git a/app.js b/app.js new file mode 100644 index 0000000..a10b9d8 --- /dev/null +++ b/app.js @@ -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 = ` + + + + `; + + 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(); +}); diff --git a/audio.js b/audio.js new file mode 100644 index 0000000..94529dc --- /dev/null +++ b/audio.js @@ -0,0 +1,105 @@ +/* ========================================================================== + ELEGANT AUDIO SYNTHESIZER (WEB AUDIO API) + ========================================================================== */ + +class SoundEffects { + constructor() { + this.ctx = null; + this.enabled = true; + } + + init() { + if (!this.ctx) { + const AudioCtx = window.AudioContext || window.webkitAudioContext; + if (AudioCtx) { + this.ctx = new AudioCtx(); + } + } + if (this.ctx && this.ctx.state === 'suspended') { + this.ctx.resume(); + } + } + + playTick() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + + try { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sine'; + // Low subtle tick + osc.frequency.setValueAtTime(600, this.ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(1200, this.ctx.currentTime + 0.03); + + gain.gain.setValueAtTime(0.04, this.ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.03); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(); + osc.stop(this.ctx.currentTime + 0.035); + } catch (e) { + // Audio context edge cases + } + } + + playReveal() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + + try { + const now = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'triangle'; + osc.frequency.setValueAtTime(440, now); + osc.frequency.exponentialRampToValueAtTime(880, now + 0.12); + + gain.gain.setValueAtTime(0.08, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.13); + } catch (e) {} + } + + playFanfare() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + + try { + const now = this.ctx.currentTime; + // Elegant Glass Chimes chord: C5, E5, G5, C6 + const freqs = [523.25, 659.25, 783.99, 1046.50]; + + freqs.forEach((f, idx) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(f, now + idx * 0.04); + + gain.gain.setValueAtTime(0.06, now + idx * 0.04); + gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.04 + 0.5); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now + idx * 0.04); + osc.stop(now + idx * 0.04 + 0.55); + }); + } catch (e) {} + } +} + +window.soundEffects = new SoundEffects(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..59a364e --- /dev/null +++ b/index.html @@ -0,0 +1,261 @@ + + + + + + Premium Random Group Selector | 무작위 인원 그룹 추첨기 + + + + + + + +
+ +
+
+
+ + + + + + +
+
+

RANDOM SELECTOR

+ PRO GROUPING +
+
+ +
+
+
+ 전체 인원 + 0 +
+
+ 남은 대기 + 0 +
+
+ 완료 그룹 + 0 +
+
+ + + +
+
+ + +
+ +
+
+ 추첨 방식: 그룹 당 4명 +
+
+ + + +
+
+ + +
+
+ +
+
+ READY + 추첨 대기 중 +
+ +
+
+ +

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

+
+
+
+
+ + +
+
+

🏆 생성된 그룹 목록

+
+ + +
+
+ +
+
+ 아직 추첨된 그룹이 없습니다. +
+
+
+
+
+ + + + + + +
+ + + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..bab153d --- /dev/null +++ b/style.css @@ -0,0 +1,1199 @@ +/* ========================================================================== + PREMIUM RANDOM GROUP SELECTOR - MODERN DARK GLASSMORPHISM STYLESHEET + ========================================================================== */ + +:root { + --bg-dark: #0b0f17; + --bg-card: rgba(18, 24, 38, 0.85); + --bg-card-hover: rgba(30, 41, 59, 0.9); + --border-glass: rgba(255, 255, 255, 0.08); + --border-glass-strong: rgba(255, 255, 255, 0.16); + + --primary-cyan: #06b6d4; + --primary-cyan-glow: rgba(6, 182, 212, 0.35); + --accent-purple: #8b5cf6; + --accent-purple-glow: rgba(139, 92, 246, 0.35); + --accent-emerald: #10b981; + --accent-rose: #f43f5e; + --accent-amber: #f59e0b; + + --text-main: #f3f4f6; + --text-muted: #9ca3af; + --text-dim: #6b7280; + + --font-family: 'Outfit', 'Inter', 'Noto Sans KR', -apple-system, sans-serif; + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 20px; + --radius-full: 9999px; + + --transition-fast: 0.18s cubic-bezier(0.16, 1, 0.3, 1); + --transition-normal: 0.3s cubic-bezier(0.16, 1, 0.3, 1); + + /* Dynamic Card Height Variable */ + --card-height: 140px; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: var(--font-family); + -webkit-font-smoothing: antialiased; +} + +body { + background: var(--bg-dark); + background-image: + radial-gradient(at 0% 0%, rgba(139, 92, 246, 0.12) 0px, transparent 50%), + radial-gradient(at 100% 0%, rgba(6, 182, 212, 0.12) 0px, transparent 50%), + radial-gradient(at 50% 100%, rgba(16, 185, 129, 0.08) 0px, transparent 50%); + color: var(--text-main); + height: 100vh; + min-height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; /* Prevent body scroll entirely */ +} + +.app-container { + max-width: 1320px; + margin: 0 auto; + padding: 1rem 1.5rem; + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + gap: 1rem; +} + +/* ========================================================================== + Header + ========================================================================== */ +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1.25rem; + background: var(--bg-card); + backdrop-filter: blur(16px); + border: 1px solid var(--border-glass); + border-radius: var(--radius-md); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + flex-shrink: 0; /* Keep header size fixed */ +} + +.logo-area { + display: flex; + align-items: center; + gap: 1rem; +} + +.logo-icon { + width: 38px; + height: 38px; + border-radius: var(--radius-sm); + background: linear-gradient(135deg, var(--primary-cyan), var(--accent-purple)); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + box-shadow: 0 0 15px var(--primary-cyan-glow); +} + +.logo-text h1 { + font-size: 1.2rem; + font-weight: 800; + letter-spacing: 0.5px; + background: linear-gradient(90deg, #ffffff, #cbd5e1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.logo-text .badge { + font-size: 0.6rem; + font-weight: 700; + padding: 1px 6px; + border-radius: var(--radius-full); + background: rgba(6, 182, 212, 0.15); + color: var(--primary-cyan); + border: 1px solid rgba(6, 182, 212, 0.3); + letter-spacing: 1px; +} + +.header-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.status-summary { + display: flex; + gap: 0.5rem; +} + +.stat-pill { + display: flex; + flex-direction: column; + align-items: flex-end; + padding: 0.3rem 0.8rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-glass); + border-radius: var(--radius-sm); + min-width: 80px; +} + +.stat-pill.highlight-pill { + background: rgba(6, 182, 212, 0.08); + border-color: rgba(6, 182, 212, 0.25); +} + +.stat-pill .label { + font-size: 0.65rem; + color: var(--text-muted); +} + +.stat-pill .value { + font-size: 1.1rem; + font-weight: 700; + color: var(--text-main); +} + +.stat-pill.highlight-pill .value { + color: var(--primary-cyan); +} + +.btn-icon-setting { + padding: 0.45rem 0.95rem; + font-size: 0.8rem; + border-radius: var(--radius-sm); + background: linear-gradient(135deg, rgba(6, 182, 212, 0.15), rgba(139, 92, 246, 0.15)); + border: 1px solid rgba(6, 182, 212, 0.3); + color: #fff; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + transition: var(--transition-fast); +} + +.btn-icon-setting:hover { + background: linear-gradient(135deg, rgba(6, 182, 212, 0.3), rgba(139, 92, 246, 0.3)); + border-color: var(--primary-cyan); + transform: translateY(-2px); + box-shadow: 0 0 20px var(--primary-cyan-glow); +} + +/* ========================================================================== + Main Single Column Layout + ========================================================================== */ +.main-single-layout { + display: flex; + flex-direction: column; + gap: 1rem; + flex: 1; + overflow: hidden; /* Prevent outer scroll */ +} + +.main-action-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1.25rem; + background: var(--bg-card); + backdrop-filter: blur(16px); + border: 1px solid var(--border-glass); + border-radius: var(--radius-md); + flex-shrink: 0; /* Keep action bar fixed */ +} + +.mode-indicator { + font-size: 0.85rem; + font-weight: 600; + color: var(--primary-cyan); + background: rgba(6, 182, 212, 0.1); + padding: 0.35rem 0.85rem; + border-radius: var(--radius-full); + border: 1px solid rgba(6, 182, 212, 0.2); +} + +.action-buttons-group { + display: flex; + gap: 0.5rem; +} + +/* ========================================================================== + Center Stage Section (No Scroll Stage) + ========================================================================== */ +.stage-section { + display: flex; + flex-direction: column; + gap: 1rem; + flex: 1; + overflow: hidden; /* Prevent stage scroll */ +} + +.draw-stage { + position: relative; + height: 250px; /* Fixed height for stage to fit in 100vh */ + flex-shrink: 0; /* Prevent resizing */ + background: var(--bg-card); + backdrop-filter: blur(16px); + border: 1px solid var(--border-glass); + border-radius: var(--radius-lg); + padding: 1rem 1.5rem; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +/* Initial placeholder */ +.initial-placeholder { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + min-height: 150px; + color: var(--text-dim); +} + +.placeholder-icon { + font-size: 2.5rem; + margin-bottom: 0.4rem; + opacity: 0.8; + animation: pulse 1.5s infinite alternate; +} + +.initial-placeholder p { + font-size: 0.9rem; + font-weight: 500; + color: var(--text-muted); +} + +/* Active Draw Display */ +.active-draw-display { + width: 100%; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.drawing-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.group-title-badge { + font-size: 0.95rem; + font-weight: 800; + letter-spacing: 1px; + padding: 0.35rem 1.1rem; + background: linear-gradient(90deg, var(--primary-cyan), var(--accent-purple)); + color: #fff; + border-radius: var(--radius-full); + box-shadow: 0 0 15px var(--primary-cyan-glow); +} + +.drawing-status-text { + font-size: 0.82rem; + color: var(--primary-cyan); + font-weight: 600; + animation: pulse 1s infinite alternate; +} + +@keyframes pulse { + 0% { opacity: 0.4; } + 100% { opacity: 1; } +} + +/* -------------------------------------------------------------------------- + Cards Grid - Dynamic Scaling Classes + -------------------------------------------------------------------------- */ +.cards-grid { + display: grid; + width: 100%; + gap: 0.6rem; + justify-content: center; + align-content: center; + align-items: center; +} + +/* 1. XL Size: 1 ~ 4 Members (Very large) */ +.cards-grid.grid-size-xl { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + max-width: 900px; + margin: 0 auto; + --card-height: 150px; +} +.cards-grid.grid-size-xl .member-name { font-size: 1.35rem; } +.cards-grid.grid-size-xl .slot-reel { font-size: 1.4rem; } + +/* 2. LG Size: 5 ~ 12 Members (Standard) */ +.cards-grid.grid-size-lg { + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + --card-height: 110px; +} +.cards-grid.grid-size-lg .member-name { font-size: 1.05rem; } +.cards-grid.grid-size-lg .slot-reel { font-size: 1.15rem; } + +/* 3. MD Size: 13 ~ 24 Members (Compact) */ +.cards-grid.grid-size-md { + grid-template-columns: repeat(auto-fit, minmax(95px, 1fr)); + gap: 0.45rem; + --card-height: 75px; +} +.cards-grid.grid-size-md .member-name { font-size: 0.9rem; } +.cards-grid.grid-size-md .slot-reel { font-size: 0.98rem; } +.cards-grid.grid-size-md .member-num { display: none; } +.cards-grid.grid-size-md .card-front, .cards-grid.grid-size-md .card-back { padding: 0.35rem; } + +/* 4. SM Size: 25 ~ 49 Members (Micro cards) */ +.cards-grid.grid-size-sm { + grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); + gap: 0.3rem; + --card-height: 50px; +} +.cards-grid.grid-size-sm .member-name { font-size: 0.75rem; font-weight: 600; } +.cards-grid.grid-size-sm .slot-reel { font-size: 0.8rem; } +.cards-grid.grid-size-sm .member-num { display: none; } +.cards-grid.grid-size-sm .card-front, .cards-grid.grid-size-sm .card-back { padding: 0.2rem; } + +/* Base Card Styles */ +.draw-card { + perspective: 1000px; + height: var(--card-height); + width: 100%; + transition: height var(--transition-fast); +} + +.card-inner { + position: relative; + width: 100%; + height: 100%; + text-align: center; + transition: transform 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); + transform-style: preserve-3d; + border-radius: var(--radius-md); +} + +.draw-card.revealed .card-inner { + transform: rotateY(180deg); +} + +.card-front, .card-back { + position: absolute; + width: 100%; + height: 100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0.75rem; +} + +.card-front { + background: linear-gradient(145deg, rgba(30, 41, 59, 0.9), rgba(15, 23, 42, 0.9)); + border: 1px solid var(--border-glass-strong); + color: var(--primary-cyan); + overflow: hidden; +} + +.slot-reel { + font-weight: 700; + text-shadow: 0 0 10px var(--primary-cyan-glow); + white-space: nowrap; +} + +.slot-reel.spinning { + animation: reelBlur 0.1s linear infinite; +} + +@keyframes reelBlur { + 0% { transform: translateY(-5px); opacity: 0.7; filter: blur(1px); } + 50% { transform: translateY(5px); opacity: 0.9; filter: blur(2px); } + 100% { transform: translateY(0); opacity: 0.7; filter: blur(1px); } +} + +.card-back { + background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(139, 92, 246, 0.25)); + border: 1.5px solid var(--primary-cyan); + transform: rotateY(180deg); + box-shadow: 0 0 20px rgba(6, 182, 212, 0.3); +} + +.card-back .member-num { + font-size: 0.65rem; + font-weight: 600; + color: var(--primary-cyan); + margin-bottom: 0.2rem; + text-transform: uppercase; +} + +.card-back .member-name { + font-weight: 700; + color: #ffffff; + word-break: break-all; +} + +/* ========================================================================== + Results Area (Dashboard scroll isolation) + ========================================================================== */ +.results-area { + background: var(--bg-card); + backdrop-filter: blur(16px); + border: 1px solid var(--border-glass); + border-radius: var(--radius-lg); + padding: 1rem 1.25rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + flex: 1; /* Stretch to occupy remaining space */ + overflow: hidden; /* Hide outer bounds */ +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +} + +.results-header h3 { + font-size: 1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Scrollable Groups Grid container */ +.groups-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + flex: 1; + overflow-y: auto; /* Internal scrolling */ + padding-right: 6px; + align-content: start; +} + +/* Sleek Webkit Scrollbar for groups-grid */ +.groups-grid::-webkit-scrollbar { + width: 6px; +} +.groups-grid::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.02); + border-radius: var(--radius-full); +} +.groups-grid::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.12); + border-radius: var(--radius-full); + transition: var(--transition-fast); +} +.groups-grid::-webkit-scrollbar-thumb:hover { + background: rgba(6, 182, 212, 0.4); +} + +.empty-results { + grid-column: 1 / -1; + text-align: center; + padding: 3rem; + color: var(--text-dim); + font-size: 0.88rem; + border: 1px dashed var(--border-glass); + border-radius: var(--radius-md); +} + +.group-card { + background: rgba(15, 23, 42, 0.6); + border: 1px solid var(--border-glass-strong); + border-radius: var(--radius-md); + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.6rem; + transition: var(--transition-normal); + animation: fadeIn 0.4s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.group-card:hover { + border-color: rgba(6, 182, 212, 0.4); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4); +} + +.group-card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.4rem; + border-bottom: 1px solid var(--border-glass); +} + +.group-card-header .title { + font-size: 0.92rem; + font-weight: 700; + color: var(--primary-cyan); +} + +.group-card-header .count { + font-size: 0.7rem; + padding: 1px 7px; + background: rgba(255, 255, 255, 0.06); + border-radius: var(--radius-full); + color: var(--text-muted); +} + +.group-members-list { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; /* Tighter gap */ +} + +/* Compact member badge */ +.member-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.55rem; /* Much smaller padding */ + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border-glass); + border-radius: var(--radius-sm); + font-size: 0.78rem; /* Smaller font size */ + color: var(--text-main); +} + +.member-badge .num { + font-size: 0.68rem; + color: var(--accent-purple); + font-weight: 700; +} + +/* ========================================================================== + SETTINGS MODAL POPUP STYLES + ========================================================================== */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(5, 8, 15, 0.82); + backdrop-filter: blur(12px); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + opacity: 1; + transition: opacity 0.25s ease; +} + +.modal-overlay.hidden { + opacity: 0; + pointer-events: none; +} + +.modal-content { + background: rgba(18, 24, 38, 0.95); + border: 1px solid var(--border-glass-strong); + border-radius: var(--radius-lg); + width: 100%; + max-width: 580px; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 25px 60px rgba(0, 0, 0, 0.6); + transform: scale(1); + transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); + overflow: hidden; +} + +.modal-overlay.hidden .modal-content { + transform: scale(0.92); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-glass); + background: rgba(10, 15, 26, 0.5); +} + +.modal-tabs { + display: flex; + gap: 0.5rem; +} + +.modal-tab-btn { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 0.9rem; + font-weight: 600; + padding: 0.5rem 0.8rem; + cursor: pointer; + border-radius: var(--radius-sm); + transition: var(--transition-fast); +} + +.modal-tab-btn:hover { + color: #fff; + background: rgba(255, 255, 255, 0.05); +} + +.modal-tab-btn.active { + color: var(--primary-cyan); + background: rgba(6, 182, 212, 0.12); + border: 1px solid rgba(6, 182, 212, 0.3); +} + +.modal-close-btn { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 1.2rem; + cursor: pointer; + padding: 0.3rem 0.6rem; + border-radius: var(--radius-sm); + transition: var(--transition-fast); +} + +.modal-close-btn:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + +.modal-body { + padding: 1.5rem; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +.tab-pane { + display: none; + flex-direction: column; + gap: 1rem; +} + +.tab-pane.active { + display: flex; +} + +.panel-header-sub { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.preset-group { + display: flex; + align-items: center; + gap: 0.4rem; +} + +.sub-title { + font-size: 0.82rem; + font-weight: 600; + color: var(--text-muted); +} + +.input-mode-toggle { + display: flex; + background: rgba(10, 15, 26, 0.6); + padding: 2px; + border-radius: 6px; + border: 1px solid var(--border-glass); +} + +.input-mode-toggle .btn.active { + background: var(--primary-cyan); + color: #000; + font-weight: 700; +} + +/* Table View Container */ +.member-view-box { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.member-view-box.hidden { display: none; } + +.table-actions-bar { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.table-count-badge { + margin-left: auto; + font-size: 0.75rem; + font-weight: 600; + padding: 2px 10px; + border-radius: var(--radius-full); + background: rgba(6, 182, 212, 0.15); + color: var(--primary-cyan); + border: 1px solid rgba(6, 182, 212, 0.3); +} + +.member-table-wrapper { + max-height: 250px; + overflow-y: auto; + border: 1px solid var(--border-glass-strong); + border-radius: var(--radius-sm); + background: rgba(10, 15, 26, 0.6); +} + +.member-table { + width: 100%; + border-collapse: collapse; + font-size: 0.88rem; + text-align: left; +} + +.member-table th { + position: sticky; + top: 0; + background: rgba(20, 28, 45, 0.95); + backdrop-filter: blur(8px); + color: var(--text-muted); + font-weight: 600; + font-size: 0.78rem; + padding: 0.5rem 0.8rem; + border-bottom: 1px solid var(--border-glass-strong); + z-index: 2; +} + +.member-table td { + padding: 0.35rem 0.8rem; + border-bottom: 1px solid var(--border-glass); +} + +.member-table tr:hover { + background: rgba(255, 255, 255, 0.03); +} + +.table-idx { + font-size: 0.75rem; + font-weight: 700; + color: var(--primary-cyan); +} + +.table-row-input { + width: 100%; + background: transparent; + border: none; + border-bottom: 1px solid transparent; + color: #fff; + font-size: 0.88rem; + padding: 3px 6px; + outline: none; + transition: var(--transition-fast); +} + +.table-row-input:focus { + border-bottom-color: var(--primary-cyan); + background: rgba(6, 182, 212, 0.08); +} + +.btn-remove-row { + background: transparent; + border: none; + color: var(--accent-rose); + font-size: 0.95rem; + cursor: pointer; + opacity: 0.7; + padding: 2px 6px; + transition: var(--transition-fast); +} + +.btn-remove-row:hover { + opacity: 1; + transform: scale(1.15); +} + +.setting-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.setting-label { + font-size: 0.85rem; + color: var(--text-muted); + font-weight: 600; +} + +.modal-divider { + border: 0; + height: 1px; + background: var(--border-glass); +} + +.modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-glass); + background: rgba(10, 15, 26, 0.5); +} + +/* Form controls in modal */ +.quick-preset-btns { + display: flex; + gap: 0.35rem; +} + +.input-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +textarea { + width: 100%; + height: 200px; + background: rgba(10, 15, 26, 0.8); + border: 1px solid var(--border-glass-strong); + border-radius: var(--radius-sm); + color: #fff; + padding: 0.8rem; + font-size: 0.9rem; + line-height: 1.4; + resize: vertical; + outline: none; + transition: var(--transition-fast); +} + +textarea:focus { + border-color: var(--primary-cyan); + box-shadow: 0 0 0 3px var(--primary-cyan-glow); +} + +.setting-row { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.setting-row.inline-input { + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.radio-toggle { + display: flex; + background: rgba(10, 15, 26, 0.6); + padding: 3px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-glass); +} + +.radio-toggle.stacked-toggle { + flex-direction: column; + gap: 2px; +} + +.radio-toggle input[type="radio"] { + display: none; +} + +.radio-toggle label { + flex: 1; + text-align: center; + padding: 0.45rem 0.6rem; + font-size: 0.8rem; + color: var(--text-muted); + cursor: pointer; + border-radius: 6px; + transition: var(--transition-fast); +} + +.radio-toggle.stacked-toggle label { + text-align: left; + padding-left: 0.8rem; +} + +.radio-toggle input[type="radio"]:checked + label { + background: var(--primary-cyan); + color: #000; + font-weight: 600; + box-shadow: 0 2px 8px var(--primary-cyan-glow); +} + +/* Custom Builder Container */ +.custom-builder-container { + display: flex; + flex-direction: column; + gap: 0.6rem; + background: rgba(10, 15, 26, 0.5); + border: 1px solid var(--border-glass-strong); + border-radius: var(--radius-sm); + padding: 0.8rem; +} + +.custom-builder-container.hidden { display: none; } + +.custom-builder-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.8rem; + color: var(--text-muted); + font-weight: 600; +} + +.total-custom-badge { + font-size: 0.72rem; + padding: 2px 8px; + border-radius: var(--radius-full); + background: rgba(139, 92, 246, 0.2); + color: var(--accent-purple); + border: 1px solid rgba(139, 92, 246, 0.3); +} + +.custom-groups-list { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-height: 180px; + overflow-y: auto; + padding-right: 2px; +} + +.custom-group-row { + display: flex; + align-items: center; + gap: 0.4rem; + background: rgba(255, 255, 255, 0.03); + padding: 0.3rem 0.5rem; + border-radius: 6px; + border: 1px solid var(--border-glass); +} + +.custom-group-name { + flex: 1; + background: transparent; + border: none; + color: var(--text-main); + font-size: 0.82rem; + font-weight: 600; + outline: none; + width: 80px; +} + +.custom-group-name:focus { + border-bottom: 1px solid var(--primary-cyan); +} + +.btn-remove-custom-group { + background: transparent; + border: none; + color: var(--accent-rose); + font-size: 0.9rem; + cursor: pointer; + opacity: 0.7; + padding: 0 4px; +} +.btn-remove-custom-group:hover { opacity: 1; } + +.btn-full { width: 100%; } + +.number-stepper { + display: flex; + align-items: center; + background: rgba(10, 15, 26, 0.8); + border: 1px solid var(--border-glass-strong); + border-radius: var(--radius-sm); + overflow: hidden; +} + +.number-stepper.sm-stepper button { + width: 26px; + height: 26px; + font-size: 0.8rem; +} +.number-stepper.sm-stepper input { + width: 36px; + font-size: 0.85rem; +} + +.number-stepper button { + background: transparent; + border: none; + color: var(--text-main); + width: 32px; + height: 32px; + font-weight: 700; + cursor: pointer; + transition: var(--transition-fast); +} + +.number-stepper button:hover { + background: rgba(255, 255, 255, 0.1); +} + +.number-stepper input { + width: 48px; + background: transparent; + border: none; + color: #fff; + text-align: center; + font-weight: 700; + font-size: 0.95rem; +} + +.toggle-row { + flex-direction: row; + justify-content: space-between; + align-items: center; + font-size: 0.85rem; + color: var(--text-muted); +} + +/* Switch UI */ +.switch { + position: relative; + display: inline-block; + width: 42px; + height: 24px; +} +.switch input { opacity: 0; width: 0; height: 0; } +.slider { + position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(255,255,255,0.15); transition: .3s; border-radius: 24px; +} +.slider:before { + position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; + background-color: white; transition: .3s; border-radius: 50%; +} +input:checked + .slider { background-color: var(--primary-cyan); } +input:checked + .slider:before { transform: translateX(18px); } + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0.5rem 1rem; + border-radius: var(--radius-sm); + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + border: none; + transition: var(--transition-fast); + outline: none; +} + +.btn-xs { padding: 0.25rem 0.5rem; font-size: 0.72rem; } +.btn-sm { padding: 0.4rem 0.8rem; font-size: 0.8rem; } +.btn-md { padding: 0.65rem 1.2rem; font-size: 0.9rem; } +.btn-xl { padding: 0.85rem 1.4rem; font-size: 1rem; border-radius: var(--radius-md); } + +.btn-primary { + background: linear-gradient(135deg, var(--primary-cyan), var(--accent-purple)); + color: #ffffff; + box-shadow: 0 4px 15px var(--primary-cyan-glow); +} +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 22px var(--primary-cyan-glow); +} +.btn-primary:active { transform: translateY(0); } + +.btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: var(--text-main); + border: 1px solid var(--border-glass-strong); +} +.btn-secondary:hover { background: rgba(255, 255, 255, 0.18); } + +.btn-accent { + background: rgba(139, 92, 246, 0.15); + color: var(--accent-purple); + border: 1px solid rgba(139, 92, 246, 0.3); +} +.btn-accent:hover { + background: rgba(139, 92, 246, 0.28); + box-shadow: 0 0 15px var(--accent-purple-glow); +} + +.btn-outline { + background: transparent; + color: var(--text-muted); + border: 1px solid var(--border-glass); +} +.btn-outline:hover { color: #fff; border-color: rgba(255,255,255,0.3); } + +.btn-ghost { + background: transparent; + color: var(--text-muted); +} +.btn-ghost:hover { color: #fff; background: rgba(255,255,255,0.06); } + +/* Confetti Canvas */ +#confetti-canvas { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 99999; +} + +/* Responsive */ +@media (max-width: 768px) { + .app-container { + padding: 1rem; + height: auto; + overflow: auto; + } + body { + height: auto; + overflow: auto; + } + .app-header { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + .header-right { + width: 100%; + justify-content: space-between; + } + .main-action-bar { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + .action-buttons-group { + flex-direction: column; + } + .draw-stage { + height: auto; + min-height: 250px; + } + .groups-grid { + overflow-y: visible; + } +} diff --git a/총괄기획실 인원.txt b/총괄기획실 인원.txt new file mode 100644 index 0000000..dbcda39 --- /dev/null +++ b/총괄기획실 인원.txt @@ -0,0 +1,47 @@ +김우진 +임민경 +최선아 +이미영 +국혜림 +조태희 +최근혜 +류원준 +주완기 +정성호 +김원기 +홍아름 +이경민 +김윤재 +한승민 +김신지 +김용연 +성형일 +박주한 +황동환 +최현호 +조찬영 +한치영 +문형석 +김혜인 +송대일 +이태훈 +신혜영 +정은혜 +채선영 +허유나 +김수현 +김정석 +김태식 +최영환 +박지영 +권순호 +정두휘 +양숙영 +최혜은 +윤봄이 +마희연 +정지윤 +권혁진 +염승호 +윤준수 +김지영 \ No newline at end of file