/** * Rougame.js * * 룰렛 게임 기능을 구현한 모듈 * 사용자가 룰렛을 돌려 다양한 보상을 획득할 수 있는 기능 제공 * * 주요 기능: * 1. 캔버스 기반 룰렛 UI 구현 * 2. 룰렛 회전 애니메이션 및 결과 처리 * 3. 서버와의 통신을 통한 결과 확인 * 4. 남은 게임 횟수 관리 */ document.addEventListener('DOMContentLoaded', function() { // 룰렛 세그먼트 데이터 가져오기 (각 칸의 정보) const segmentElements = document.querySelectorAll('.segment-data'); const segments = Array.from(segmentElements).map(el => ({ rank: parseInt(el.dataset.rank), // 순위 정보 code: el.dataset.code, // 코드 정보 label: el.dataset.label // 표시될 텍스트 })); // 화면 정보 및 설정값 가져오기 const screenInfo = { scrRwdLinkUrl: document.getElementById('scrRwdLinkUrl').value, // 보상 링크 URL bnfInnb: document.getElementById('bnfInnb').value // 혜택 고유 번호 }; /** * RouletteGame 객체 * 룰렛 게임의 모든 기능을 구현한 객체 */ const RouletteGame = { canvas: document.getElementById('rouletteCanvas'), // 룰렛을 그릴 캔버스 요소 ctx: null, // 캔버스 컨텍스트 colors: ['#FDBE01', '#ffffff', '#FDBE01', '#ffffff', '#FDBE01', '#ffffff', '#FDBE01', '#ffffff'], // 룰렛 색상 배열 angle: 0, // 현재 룰렛 각도 segmentAngle: 2 * Math.PI / segments.length, // 각 세그먼트의 각도 diameterPercentage: 0.76, // 캔버스 대비 룰렛 지름 비율 canvasCenterX: 0, // 캔버스 중심 X 좌표 canvasCenterY: 0, // 캔버스 중심 Y 좌표 radius: 0, // 룰렛 반지름 isSpinning: false, // 회전 중 여부 rewardResult: null, // 서버에서 받은 보상 결과 저장 /** * 초기화 함수 * 캔버스 설정 및 이벤트 리스너 등록 */ init() { this.ctx = this.canvas.getContext('2d'); this.canvasCenterX = this.canvas.width / 2; this.canvasCenterY = this.canvas.height / 2; this.radius = (Math.min(this.canvas.width, this.canvas.height) * this.diameterPercentage) / 2; this.addEventListeners(); // 이벤트 리스너 등록 this.drawRoulette(); // 초기 룰렛 그리기 this.checkRewardAvailability(); // 보상 가능 여부 체크 추가 }, /** * 이벤트 리스너 등록 함수 * 버튼 클릭 이벤트 처리 */ addEventListeners() { // 회전 버튼 이벤트 등록 const spinButton = document.getElementById('spinButton'); spinButton.addEventListener('click', this.spinRoulette.bind(this)); // 결과 팝업 닫기 버튼 이벤트 등록 const closeButton = document.querySelector('.microModal__btn'); closeButton.addEventListener('click', () => MicroModal.close('resultPopup')); // 품절 모달의 메인 이동 버튼 이벤트 설정 (행운복권과 동일한 방식) $('#btn-go-main').on('click', function() { window.location.href = '/sopmal/bnft/bnftMain'; }); }, /** * 룰렛 그리기 함수 * 캔버스에 룰렛과 세그먼트를 그림 */ drawRoulette() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 캔버스 초기화 let angle = this.angle; // 각 세그먼트 그리기 segments.forEach((seg, index) => { this.drawSegment(seg, index, angle); angle += this.segmentAngle; }); // 화살표 그리기 this.drawArrow(); }, /** * 세그먼트 그리기 함수 * @param {Object} seg - 세그먼트 정보 * @param {number} index - 세그먼트 인덱스 * @param {number} angle - 시작 각도 */ drawSegment(seg, index, angle) { // 세그먼트 배경 그리기 this.ctx.beginPath(); this.ctx.fillStyle = this.colors[index % this.colors.length]; this.ctx.moveTo(this.canvasCenterX, this.canvasCenterY); this.ctx.arc(this.canvasCenterX, this.canvasCenterY, this.radius, angle, angle + this.segmentAngle); this.ctx.lineTo(this.canvasCenterX, this.canvasCenterY); this.ctx.fill(); // 세그먼트 텍스트 그리기 this.ctx.save(); this.ctx.translate(this.canvasCenterX, this.canvasCenterY); this.ctx.rotate(angle + this.segmentAngle / 2); this.ctx.textAlign = 'right'; this.ctx.fillStyle = '#000000'; this.ctx.font = 'bold 20px "GmarketSans"'; this.ctx.fillText(seg.label, this.radius - 20, 10); this.ctx.restore(); }, /** * 화살표 그리기 함수 * 룰렛 결과를 가리키는 화살표 표시 */ drawArrow() { this.ctx.fillStyle = 'transparent'; this.ctx.beginPath(); this.ctx.moveTo(this.canvasCenterX, this.canvasCenterY - this.radius - 15); this.ctx.lineTo(this.canvasCenterX - 10, this.canvasCenterY - this.radius + 5); this.ctx.lineTo(this.canvasCenterX + 10, this.canvasCenterY - this.radius + 5); this.ctx.closePath(); this.ctx.fill(); }, /** * 화면 로딩시 보상 가능 여부 체크 */ checkRewardAvailability() { $.ajax({ url: '/sopmal/bnft/checkRewardAvailability', type: 'POST', contentType: 'application/json', data: JSON.stringify({ bnfInnb: screenInfo.bnfInnb }), success: (response) => { console.log('checkRewardAvailability response:', response); // 응답 결과 파싱 let result = response.resBody.result; // 보상이 없는 경우 처리 if (result === "N" || result === false) { // 게임 버튼 비활성화 this.disableGameButton(); // 품절 안내 모달 표시 this.showNoRewardModal(); return; } // 보상이 있는 경우 정상 진행 console.log('보상 가능 상태:', result); }, error: (xhr, status, error) => { console.error('보상 가능 여부 확인 실패:', error); // 에러 발생 시에도 게임 버튼 비활성화 this.disableGameButton(); this.showErrorModal(); } }); }, /** * 게임 버튼 비활성화 */ disableGameButton() { const spinButton = document.getElementById('spinButton'); if (spinButton) { spinButton.disabled = true; spinButton.style.opacity = '0.5'; spinButton.style.cursor = 'not-allowed'; spinButton.textContent = '품절'; } }, /** * 보상 품절 안내 모달 표시 */ showNoRewardModal() { // 품절 전용 모달 표시 MicroModal.show('modal-no-reward'); }, /** * 에러 안내 모달 표시 */ showErrorModal() { if (typeof showCustomAlert === 'function') { showCustomAlert('보상 상태 확인 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); } else { MicroModal.show('modal-alert'); document.querySelector('#modal-alert .microModal__content p').textContent = '보상 상태 확인 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; } }, /** * 룰렛 회전 시작 함수 (수정된 버전) */ spinRoulette() { // 이미 회전 중이면 중복 실행 방지 if (this.isSpinning) return; // 버튼이 비활성화되어 있으면 실행 방지 const spinButton = document.getElementById('spinButton'); if (spinButton && spinButton.disabled) { this.showNoRewardModal(); return; } // 남은 게임 횟수 확인 const remainingChancesElement = document.querySelector('.rouletteCnt b'); const remainingChances = parseInt(remainingChancesElement.textContent, 10); if (isNaN(remainingChances) || remainingChances <= 0) { // alert 대신 커스텀 모달 사용 if (typeof showCustomAlert === 'function') { showCustomAlert('남은 횟수가 없습니다.'); } else { // showCustomAlert 함수가 없는 경우 fallback MicroModal.show('modal-alert'); document.querySelector('#modal-alert .microModal__content p').textContent = '남은 횟수가 없습니다.'; } return; } this.isSpinning = true; // 서버에 게임 결과 요청 $.ajax({ url: '/sopmal/bnft/playRougame', method: 'POST', contentType: 'application/json', data: JSON.stringify({ "bnfInnb": screenInfo.bnfInnb }), success: (response) => { const result = JSON.parse(response.resBody.result); // 에러 응답 처리 if (result.success === false && result.error) { this.isSpinning = false; console.log('Server error:', result.error); // 모든 에러를 showCustomAlert로 처리 if (typeof showCustomAlert === 'function') { showCustomAlert(result.error); } else { // showCustomAlert 함수가 없는 경우 fallback MicroModal.show('modal-alert'); document.querySelector('#modal-alert .microModal__content p').textContent = result.error; } return; } // 정상 응답 처리 this.rewardResult = result; const targetRank = result.rank; const targetIndex = segments.findIndex(segment => segment.rank === targetRank); console.log('Reward Result:', result); console.log('Target Rank:', targetRank); console.log('Target Index:', targetIndex); console.log('Segments:', segments); if (targetIndex !== -1) { const adjustedIndex = targetIndex; // 12시 방향을 기준으로 인덱스 조정 this.startSpin(adjustedIndex); // 회전 애니메이션 시작 this.updateRemainingChances(); // 남은 횟수 업데이트 } }, error: (xhr, status, error) => { console.error("AJAX request failed:", error); this.isSpinning = false; // alert 대신 커스텀 모달 사용 if (typeof showCustomAlert === 'function') { showCustomAlert('룰렛 회전 중 오류가 발생했습니다. 다시 시도해주세요.'); } else { // showCustomAlert 함수가 없는 경우 fallback MicroModal.show('modal-alert'); document.querySelector('#modal-alert .microModal__content p').textContent = '룰렛 회전 중 오류가 발생했습니다. 다시 시도해주세요.'; } } }); }, /** * 회전 애니메이션 시작 함수 * @param {number} targetIndex - 목표 세그먼트 인덱스 */ startSpin(targetIndex) { const segmentsCount = segments.length; const segmentAngleDeg = 360 / segmentsCount; // 수정된 목표 각도 계산: 12시 방향이 0도가 되도록 조정 const targetAngleDeg = -(targetIndex * segmentAngleDeg) + 255; // 270도는 12시 방향 const initialAngle = this.angle % (2 * Math.PI); const finalAngle = (targetAngleDeg * Math.PI / 180) + (2 * Math.PI * 5); // 5바퀴 추가 회전 console.log('Segments Count:', segmentsCount); console.log('Segment Angle (deg):', segmentAngleDeg); console.log('Target Angle (deg):', targetAngleDeg); console.log('Initial Angle (rad):', initialAngle); console.log('Final Angle (rad):', finalAngle); // 회전 시간 및 속도 설정 const spinTimeTotal = Math.random() * 1000 + 4000; // 4~5초 사이 랜덤 시간 let spinTime = 0; // 회전 애니메이션 함수 const rotateWheel = () => { spinTime += 30; // 30ms마다 업데이트 if (spinTime >= spinTimeTotal) { // 회전 완료 this.angle = targetAngleDeg * Math.PI / 180; console.log('Final Position Angle (rad):', this.angle); this.drawRoulette(); this.stopRotateWheel(); return; } // easeOut 함수로 감속 효과 적용 const spinAngle = this.easeOut(spinTime, 0, finalAngle - initialAngle, spinTimeTotal); this.angle = initialAngle + spinAngle; this.drawRoulette(); requestAnimationFrame(rotateWheel); // 다음 프레임 요청 }; rotateWheel(); // 애니메이션 시작 }, /** * 회전 정지 및 결과 처리 함수 * 최종 위치에서 결과를 계산하고 팝업 표시 */ stopRotateWheel() { let degrees = (this.angle * 180 / Math.PI) % 360; console.log('Current Angle in Degrees:', degrees); if (degrees < 0) degrees += 360; console.log('Normalized Angle in Degrees:', degrees); const arcd = 360 / segments.length; console.log('Arc Degree per Segment:', arcd); // 수정된 인덱스 계산: 12시 방향을 기준으로 계산 const index = (Math.floor(((360 - degrees + 255) % 360) / arcd)) % segments.length; console.log('Calculated Segment Index:', index); // 보상 유형에 따라 결과 텍스트와 안내 문구 설정 let resultText = ''; let guideText = ''; if (this.rewardResult) { if (this.rewardResult.rewardType === 'MILEAGE') { // 마일리지인 경우 resultText = `마일리지 ${this.rewardResult.rewardAmount}원 획득하셨습니다.`; guideText = 'MY혜택 > 마일리지에서 확인하세요.'; } else if (this.rewardResult.rewardType === 'COUPON') { // 쿠폰인 경우 resultText = this.rewardResult.itemName; guideText = 'MY혜택 > 쿠폰에서 확인하세요.'; } else { // 기타 경우 resultText = segments[index] ? segments[index].label : '보상'; guideText = 'MY혜택에서 확인하세요.'; } } else { // rewardResult가 없는 경우 기본값 resultText = segments[index] ? segments[index].label : '보상'; guideText = 'MY혜택에서 확인하세요.'; } console.log('Final Result Text:', resultText); console.log('Guide Text:', guideText); // 결과 텍스트와 안내 문구 업데이트 document.getElementById('resultText').textContent = resultText; // 안내 문구도 동적으로 변경 const guideElement = document.querySelector('#resultPopup .microModal__content p:last-child'); if (guideElement) { guideElement.textContent = guideText; } // 결과 팝업 표시 MicroModal.show('resultPopup'); this.isSpinning = false; // 회전 상태 초기화 this.rewardResult = null; // 결과 초기화 }, /** * easeOut 애니메이션 함수 * 점점 느려지는 효과를 위한 계산 * @param {number} t - 현재 시간 * @param {number} b - 시작 값 * @param {number} c - 변화량 * @param {number} d - 지속 시간 * @returns {number} - 계산된 값 */ easeOut(t, b, c, d) { const ts = (t /= d) * t; const tc = ts * t; const result = b + c * (tc + -3 * ts + 3 * t); console.log('EaseOut Calculation:', { t, b, c, d, result }); return result; }, /** * 남은 게임 횟수 업데이트 함수 * 게임 후 남은 횟수를 감소시킴 */ updateRemainingChances() { const remainingChancesElement = document.querySelector('.rouletteCnt b'); if (remainingChancesElement) { let remainingChances = parseInt(remainingChancesElement.textContent, 10); console.log('Current Remaining Chances:', remainingChances); if (!isNaN(remainingChances) && remainingChances > 0) { remainingChancesElement.textContent = remainingChances - 1; console.log('Updated Remaining Chances:', remainingChances - 1); } } } }; // 룰렛 게임 초기화 RouletteGame.init(); });