/** * Conrev.js * * 리뷰 슬라이더 및 리뷰 보기 보상 기능을 관리하는 모듈 * 사용자가 일정 시간 동안 리뷰를 보면 마일리지를 적립해주는 기능 구현 */ const Conrev = (function() { // 모듈 내부 변수 선언 let timer; // 리뷰 시간 카운트다운 타이머 let remainingTime; // 리뷰를 봐야 하는 남은 시간(초) let currentEventId; // 현재 선택된 리뷰의 고유 ID let startTime; // 리뷰 보기 시작 시간 let reviewCheckInterval; // 리뷰 팝업이 계속 열려있는지 확인하는 타이머 let currentSlideIndex = 0; // 현재 보고 있는 슬라이드 인덱스 let reviewData = []; // 서버에서 받아온 리뷰 데이터 저장 배열 let lastViewedSlideIndex = 0; // 마지막으로 본 슬라이드 인덱스 (중복 마일리지 적립 방지용) let reviewLayer; // 리뷰 팝업 레이어 요소 let autoSlideTimer; // 자동 슬라이드 타이머 let mileageAwarded = false; // 마일리지 적립 완료 플래그 추가 /** * 토스트 메시지 표시 함수 * @param {string} message - 표시할 메시지 */ function showToast(message) { // 기존에 토스트 요소가 있으면 제거 $('.toastPop').remove(); // 토스트 요소 생성 및 추가 (z-index 1001로 설정하여 모달 위에 표시) const $toast = $('
' + message + '
'); $('body').append($toast); // 토스트 표시 및 자동 제거 setTimeout(function() { $toast.addClass('show'); setTimeout(function() { $toast.removeClass('show'); // 애니메이션 완료 후 요소 제거 setTimeout(function() { $toast.remove(); }, 300); }, 2000); }, 100); } /** * 마일리지 보상 정보 가져오기 * 현재 슬라이드의 리뷰에 맞는 보상 정보를 가져옴 * @param {string|number} bnfInnb - 리뷰 혜택 고유 번호 (선택사항) */ function fetchRewardInfo(bnfInnb) { // 현재 슬라이드의 bnfInnb 사용하거나 파라미터로 받은 값 사용 const targetBnfInnb = bnfInnb || (reviewData.length > currentSlideIndex ? reviewData[currentSlideIndex].bnfInnb : ''); if (targetBnfInnb) { $.ajax({ url: '/sopmal/bnft/getRewardInfo', method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ bnfInnb: targetBnfInnb }), success: function(response) { if (response.success && response.mlgRwdAmt) { // JSP의 금액 부분을 동적으로 업데이트 $('.topupPoint b').text(response.mlgRwdAmt + '원'); console.log('마일리지 보상 금액:', response.mlgRwdAmt); } else { console.error('마일리지 보상 정보를 가져오는데 실패했습니다.'); } }, error: function(jqXHR, textStatus, errorThrown) { console.error('마일리지 보상 정보 요청 중 오류 발생:', textStatus, errorThrown); } }); } } /** * 슬라이더 초기화 함수 * 페이지 로드 시 호출되어 리뷰 슬라이더의 모든 기능을 설정 */ function initializeSlider() { // hidden input에서 JSON 형태의 리뷰 데이터 가져오기 try { let rawData = $('#reviewDataInput').val(); // JSON 파싱을 위해 백슬래시를 이중 백슬래시로 변환 (이스케이프 처리) rawData = rawData.replace(/\\(?!["])/g, "\\\\"); reviewData = JSON.parse(rawData) || []; } catch (e) { console.error('리뷰 데이터 파싱 실패:', e); reviewData = []; } // 슬라이더 요소 참조 가져오기 const $slider = $('.reviewSlider ul'); // 슬라이더 컨테이너 const $slides = $('.reviewSlider li'); // 각 슬라이드 항목 const slideCount = $slides.length; // 전체 슬라이드 개수 let currentSlide = 0; // 현재 슬라이드 인덱스 // 페이지네이션 도트 동적 생성 (슬라이드 개수만큼 도트 생성) const $pagination = $('.pagination'); for (let i = 0; i < slideCount; i++) { $pagination.append('
'); } // 첫 번째 도트를 활성화 상태로 설정 $pagination.find('.dot').first().addClass('active'); /** * 슬라이드 이동 함수 * 현재 슬라이드 인덱스에 따라 슬라이더 위치와 페이지네이션 상태 업데이트 */ function moveSlide() { // 슬라이더 위치 조정 (CSS margin-left 사용하여 수평 이동) $slider.css('margin-left', -currentSlide * 100 + '%'); // 현재 슬라이드에 해당하는 도트만 활성화 $pagination.find('.dot').removeClass('active').eq(currentSlide).addClass('active'); // 글로벌 변수 업데이트 currentSlideIndex = currentSlide; // 리뷰 버튼 업데이트 (현재 슬라이드에 맞는 리뷰 데이터 연결) updateReviewButton(); // 현재 슬라이드에 맞는 마일리지 보상 정보 업데이트 fetchRewardInfo(); } // 슬라이드 이미지 로드 함수 호출 - JSP에서 CDN으로 이미 설정되므로 주석 처리 // loadImages(); /** * 페이지네이션 도트 클릭 이벤트 핸들러 * 사용자가 특정 도트를 클릭하면 해당 슬라이드로 이동 */ $pagination.on('click', '.dot', function() { const newSlideIndex = $(this).data('slide'); currentSlide = newSlideIndex; moveSlide(); // 도트 클릭으로 슬라이드 변경시에는 마일리지 적립하지 않음 (리뷰 보기를 통해서만 적립) }); /** * 마우스/터치 드래그 이벤트 처리 * 슬라이더를 드래그하여 다음/이전 슬라이드로 이동할 수 있게 함 */ let isDragging = false; // 드래그 중인지 상태 플래그 let startPos = 0; // 드래그 시작 위치 let endPos = 0; // 드래그 종료 위치 // 드래그 시작 이벤트 (마우스다운 또는 터치스타트) // $slider 대신 더 넓은 영역인 .revieBox에 이벤트 적용 $('.revieBox').on('mousedown touchstart', function(e) { isDragging = true; // 마우스 또는 터치 이벤트의 시작 위치 저장 startPos = e.pageX || e.originalEvent.touches[0].pageX; }); // 드래그 종료 이벤트 (마우스업 또는 터치엔드) $(document).on('mouseup touchend', function(e) { if (isDragging) { // 마우스 또는 터치 이벤트의 종료 위치 저장 endPos = e.pageX || e.originalEvent.changedTouches[0].pageX; const diff = endPos - startPos; // 드래그 거리가 충분히 클 경우에만 슬라이드 전환 (50px 이상) if (Math.abs(diff) > 50) { // 오른쪽으로 드래그: 이전 슬라이드, 왼쪽으로 드래그: 다음 슬라이드 currentSlide += (diff > 0) ? -1 : 1; // 슬라이드 인덱스 범위 제한 (순환 슬라이더) if (currentSlide < 0) { currentSlide = slideCount - 1; // 첫 슬라이드에서 이전으로 가면 마지막으로 } else if (currentSlide >= slideCount) { currentSlide = 0; // 마지막 슬라이드에서 다음으로 가면 처음으로 } moveSlide(); } isDragging = false; } }); // 이미지 드래그 방지 (브라우저 기본 동작 차단) $('.reviewSlider img').on('dragstart', function(event) { event.preventDefault(); }); // 슬라이드 클릭 시 링크 이동 처리 - 드래그와 구분하여 처리 $slides.on('click', function(e) { // 더보기 버튼이나 그 하위 요소를 클릭한 경우 제외 if ($(e.target).closest('.reviewText-toggle').length > 0) { return; } // 드래그 중이 아닐 때만 링크 이동 if (!isDragging) { const link = $(this).data('link'); if (link) { window.location.href = link; } } }); // 초기 리뷰 버튼 설정 updateReviewButton(); // 첫 번째 슬라이드의 마일리지 보상 정보 가져오기 (페이지 로딩시) fetchRewardInfo(); /** * 자동 슬라이드 시작 함수 * 일정 시간마다 자동으로 다음 슬라이드로 이동 */ function startAutoSlide() { autoSlideTimer = setInterval(function() { // 다음 슬라이드로 순환 이동 (마지막 슬라이드에서는 처음으로) currentSlide = (currentSlide + 1) % slideCount; moveSlide(); }, 5000); // 5초마다 슬라이드 자동 전환 } /** * 자동 슬라이드 정지 함수 */ function stopAutoSlide() { clearInterval(autoSlideTimer); } // 슬라이더에 마우스 오버/아웃 시 자동 슬라이드 제어 // .reviewSlider 대신 .revieBox에 적용 $('.revieBox').hover( function() { stopAutoSlide(); }, // 마우스 오버 시 자동 슬라이드 정지 function() { startAutoSlide(); } // 마우스 아웃 시 자동 슬라이드 재시작 ); // 초기 자동 슬라이드 시작 startAutoSlide(); // 더보기 기능 초기화 initializeReadMore(); } /** * 리뷰 모달 생성 함수 * 리뷰 버튼 클릭 시 호출되어 모달 창 요소를 생성 */ function createReviewModal() { // 기존 모달이 있다면 제거 $('#review-modal').remove(); // 모달 HTML 구조 생성 const modalHTML = ` `; // 모달을 페이지에 추가 $('body').append(modalHTML); // 모달 닫기 버튼에 이벤트 핸들러 직접 연결 $('#review-modal-close-btn, .reviewModal__close').on('click', function() { closeReviewModal(); }); } /** * 리뷰 버튼 업데이트 함수 * 현재 슬라이드에 맞는 리뷰 정보로 리뷰 버튼의 이벤트 핸들러 업데이트 */ function updateReviewButton() { if (reviewData.length > currentSlideIndex) { const currentReview = reviewData[currentSlideIndex]; // 기존 이벤트 핸들러 제거 후 새로운 핸들러 등록 $('#reviewButton').off('click').on('click', function() { fetchReviewTime(currentReview.bnfInnb); }); // 현재 슬라이드의 더보기 상태 리셋 const $currentSlide = $('.reviewSlider li').eq(currentSlideIndex); const $reviewText = $currentSlide.find('.reviewText'); const $toggleBtn = $currentSlide.find('.reviewText-toggle'); if ($toggleBtn.length > 0) { $reviewText.removeClass('expanded').addClass('collapsed'); $toggleBtn.text('더보기'); } } } /** * 리뷰 시간 정보 가져오기 * 서버에 리뷰 보기 필요 시간을 요청 * @param {string|number} bnfInnb - 리뷰 혜택 고유 번호 */ function fetchReviewTime(bnfInnb) { $.ajax({ url: '/sopmal/bnft/getReviewTime', method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ bnfInnb: bnfInnb }), success: function(response) { // 서버에서 정상적으로 시간 정보를 받아왔을 경우 if (response.success && response.tmeSecnd) { // 모달 생성 먼저 createReviewModal(); // 그 다음 리뷰 보기 시작 (ID, 링크URL, 필요 시간(초)) start(bnfInnb, reviewData[currentSlideIndex].linkUrl, response.tmeSecnd); } else { console.error('리뷰 시간 정보를 가져오는데 실패했습니다.'); } }, error: function(jqXHR, textStatus, errorThrown) { console.error('리뷰 시간 정보 요청 중 오류 발생:', textStatus, errorThrown); } }); } /** * 리뷰 보기 시작 함수 * 리뷰 모달을 열고 타이머 시작 * @param {string|number} bnfInnb - 리뷰 혜택 고유 번호 * @param {string} linkUrl - 리뷰 페이지 URL * @param {number} seconds - 리뷰를 봐야 하는 시간(초) */ function start(bnfInnb, linkUrl, seconds) { // 현재 eventId를 명확히 설정하여 나중에 마일리지 적립시 올바른 ID 사용 currentEventId = bnfInnb; remainingTime = seconds; mileageAwarded = false; startTime = new Date().getTime(); const endTime = startTime + (seconds * 1000); $('#reviewIframe').attr('src', linkUrl); $('#review-modal').fadeIn(300); startTimer(); reviewCheckInterval = setInterval(function() { if ($('#review-modal').is(':hidden')) { clearInterval(reviewCheckInterval); stopTimer(); // 토스트 메시지 제거하고 서버 응답에서만 표시 if (!mileageAwarded && remainingTime <= 0) { awardMileage(); } else if (!mileageAwarded) { cancelMileageAward(); } } }, 1000); } /** * 리뷰 모달 닫기 함수 */ function closeReviewModal() { console.log('리뷰 모달 닫기 함수 호출'); $('#review-modal').fadeOut(300, function() { $('#reviewIframe').attr('src', 'about:blank'); }); stopTimer(); // 토스트 메시지 제거하고 서버 응답에서만 표시 if (!mileageAwarded && remainingTime <= 0) { awardMileage(); } } /** * 타이머 시작 함수 * 리뷰 보기 시간 카운트다운 시작 */ function startTimer() { updateTimerDisplay(); // 초기 타이머 표시 업데이트 // 기존 타이머 있으면 정지 clearInterval(timer); // 1초마다 카운트다운 실행 timer = setInterval(function() { remainingTime--; updateTimerDisplay(); // 시간이 모두 경과했을 경우 if (remainingTime <= 0) { clearInterval(timer); completeReviewViewing(); // 리뷰 보기 완료 처리 } }, 1000); } /** * 리뷰 보기 완료 처리 함수 */ function completeReviewViewing() { clearInterval(timer); // 마일리지가 아직 적립되지 않았다면 적립 (토스트 메시지 제거) if (!mileageAwarded && remainingTime <= 0) { awardMileage(); // 서버 응답에서만 토스트 표시하도록 함 } } /** * 마일리지 적립 함수 */ function awardMileage() { // 이미 적립되었다면 중복 호출 방지 if (mileageAwarded) { console.log('마일리지 이미 적립됨 - 중복 호출 방지'); return; } // 적립 시작 시 플래그 설정 mileageAwarded = true; // currentEventId (리뷰 보기 시작시 설정된 ID) 사용하여 정확한 마일리지 적립 $.ajax({ url: '/sopmal/bnft/awardConrevMileage', method: 'POST', contentType: 'application/json', data: JSON.stringify({ bnfInnb: currentEventId }), success: function(response) { let result; try { result = typeof response === 'string' ? JSON.parse(response) : response; } catch (e) { console.error('서버 응답 파싱 오류:', e); mileageAwarded = false; showToast('서버 응답 처리 중 오류가 발생했습니다.'); return; } // 서버 응답에 따른 정확한 메시지 표시 if (result.result === true) { const successMessage = result.message || '리뷰 보기 완료! 마일리지가 적립되었습니다!'; showToast(successMessage); } else { // 실패 시에도 사용자에게 알림 (이미 적립된 경우 등) const errorMessage = result.message || '마일리지 적립에 실패했습니다.'; showToast(errorMessage); console.log('마일리지 적립 실패 - 오류 코드:', result.errorCode); } }, error: function(jqXHR, textStatus, errorThrown) { console.error('마일리지 적립 요청 중 오류 발생:', textStatus, errorThrown); mileageAwarded = false; showToast('마일리지 적립 요청 중 오류가 발생했습니다.'); } }); } /** * 마일리지 적립 취소 함수 * 사용자가 리뷰 보기를 중단한 경우 */ function cancelMileageAward() { console.log('마일리지 적립이 취소되었습니다.'); // 필요한 경우 서버에 마일리지 적립 취소 요청을 보낼 수 있습니다. } /** * 타이머 정지 함수 * 모든 타이머 및 인터벌 중지 */ function stopTimer() { if (timer) { clearInterval(timer); } if (reviewCheckInterval) { clearInterval(reviewCheckInterval); } if (autoSlideTimer) { clearInterval(autoSlideTimer); } } /** * 이미지 로딩 함수 * 슬라이더의 모든 이미지를 로드 */ function loadImages() { // JSP에서 이미 CDN URL로 이미지 src가 설정되어 있으므로 // 추가적인 이미지 로딩 처리가 불필요함 console.log('이미지는 JSP에서 CDN URL로 이미 로드됨'); // 필요한 경우에만 이미지 로드 실패 처리 $('.reviewSlider .thum img').each(function(index) { const $img = $(this); // 이미지 로드 실패 시에만 처리 $img.on('error', function() { console.error(`이미지 로드 실패: ${this.src}`); this.src = '/mobile/images/image_placeholder.png'; // 모바일용 플레이스홀더로 변경 }); }); } /** * 앱에서 호출하는 콜백 함수 * 앱에서 리뷰 보기 완료 시 호출됨 * @param {string} url - 완료된 리뷰 URL */ function complete_open_blank_url(url) { console.log('Review completed for URL:', url); awardMileage(); // 마일리지 적립 } /** * 타이머 표시 업데이트 함수 * 남은 시간을 화면에 표시 */ function updateTimerDisplay() { // 카운트다운 타이머와 팝업 카운트다운에 남은 시간 표시 $('#countdown, #popupCountdown').text(remainingTime); } /** * 더보기 기능 초기화 */ function initializeReadMore() { $('.reviewSlider li').each(function() { const $reviewText = $(this).find('.reviewText'); const $slide = $(this); // 원본 텍스트 저장 const originalText = $reviewText.html(); $reviewText.data('original-text', originalText); // 높이 체크하여 더보기 버튼 필요 여부 판단 checkTextHeight($reviewText, $slide); }); } /** * 텍스트 높이 체크 및 더보기 버튼 생성 */ function checkTextHeight($reviewText, $slide) { // 임시로 전체 텍스트 표시하여 높이 측정 $reviewText.removeClass('collapsed expanded'); const fullHeight = $reviewText.height(); // 3줄 높이로 변경: 3줄 * line-height(1.6) * font-size(16px) = 약 76.8px const maxHeight = 3 * 1.6 * 16; if (fullHeight > maxHeight) { // 긴 텍스트인 경우 더보기 버튼 추가 $reviewText.addClass('collapsed'); // 더보기 버튼이 없으면 생성 if ($slide.find('.reviewText-toggle').length === 0) { const $toggleBtn = $(''); $reviewText.after($toggleBtn); // 클릭 이벤트 바인딩 - 강화된 이벤트 방지 $toggleBtn.on('click touchend', function(event) { // 모든 이벤트 전파 방지 event.stopPropagation(); event.stopImmediatePropagation(); event.preventDefault(); toggleReadMore($reviewText, $toggleBtn); // 추가적인 이벤트 방지를 위한 return false return false; }); } } else { // 짧은 텍스트인 경우 더보기 버튼 제거 $slide.find('.reviewText-toggle').remove(); } } /** * 더보기/접기 토글 */ function toggleReadMore($reviewText, $toggleBtn) { if ($reviewText.hasClass('collapsed')) { // 펼치기 $reviewText.removeClass('collapsed').addClass('expanded'); $toggleBtn.text('접기'); } else { // 접기 $reviewText.removeClass('expanded').addClass('collapsed'); $toggleBtn.text('더보기'); // 접을 때 해당 위치로 스크롤 $reviewText[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); } } // 모듈의 공개 인터페이스 return { init: initializeSlider, // 슬라이더 초기화 start: start, // 리뷰 보기 시작 stop: stopTimer, // 타이머 정지 loadImages: loadImages, // 이미지 로드 showToast: showToast, // 토스트 메시지 표시 함수 추가 fetchRewardInfo: fetchRewardInfo // 마일리지 보상 정보 가져오기 함수 추가 }; })(); /** * DOM이 로드된 후 슬라이더 초기화 * 페이지 로드 완료 시 Conrev 모듈 초기화 */ $(document).ready(function() { Conrev.init(); // 페이지 로드 시 기존 alert 메시지가 있다면 토스트로 변환 if (typeof alertMessage !== 'undefined' && alertMessage) { Conrev.showToast(alertMessage); } }); /** * 창이 닫힐 때 타이머 정지 * 페이지 이탈 시 리소스 정리 */ $(window).on('beforeunload', function() { Conrev.stop(); });