merge master into develop - CscListRForm.jsp FAQ 아코디언 버그 수정

This commit is contained in:
jilee4unity
2026-05-28 23:34:06 +09:00
7250 changed files with 570509 additions and 25 deletions

View File

@@ -0,0 +1,650 @@
/**
* 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 = $('<div class="toastPop" style="z-index: 1001; position: fixed;">' + message + '</div>');
$('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('<div class="dot" data-slide="' + i + '"></div>');
}
// 첫 번째 도트를 활성화 상태로 설정
$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 = `
<div class="reviewModal" id="review-modal" style="display:none;">
<div class="reviewModal__overlay" style="position:fixed; top:0; left:0; right:0; bottom:0; background-color:rgba(0,0,0,0.6); z-index:999;">
<div class="reviewModal__container" style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); width:90%; max-width:800px; max-height:80vh; background-color:#fff; border-radius:5px; overflow:hidden;">
<div class="reviewModal__header" style="display:flex; justify-content:space-between; align-items:center; padding:15px; border-bottom:1px solid #e0e0e0;">
<h2 class="reviewModal__title" style="margin:0; font-size:18px; font-weight:bold;">리뷰 보기</h2>
<button class="reviewModal__close" style="background:transparent; border:0; width:24px; height:24px; position:relative; cursor:pointer;">
<span style="position:absolute; width:2px; height:16px; background-color:#666; top:4px; left:11px; transform:rotate(45deg);"></span>
<span style="position:absolute; width:2px; height:16px; background-color:#666; top:4px; left:11px; transform:rotate(-45deg);"></span>
</button>
</div>
<div class="reviewModal__body" style="max-height:calc(80vh - 130px); overflow:hidden;">
<div class="reviewModal__content">
<div class="review-iframe-container" style="width:100%; height:calc(80vh - 170px); min-height:300px; overflow:hidden;">
<iframe id="reviewIframe" src="about:blank" frameborder="0" style="width:100%; height:100%; border:none;"></iframe>
</div>
<div class="review-timer" style="padding:10px 15px; text-align:center; background-color:#f8f8f8; border-top:1px solid #e0e0e0; font-weight:bold;">
남은 시간: <span id="popupCountdown">0</span>초
</div>
</div>
</div>
<div class="reviewModal__btnContainer" style="padding:15px; border-top:1px solid #e0e0e0; text-align:right;">
<button id="review-modal-close-btn" style="padding:8px 16px; border-radius:4px; cursor:pointer; border:none; font-weight:bold; background-color:#f1f1f1; color:#333;">닫기</button>
</div>
</div>
</div>
</div>
`;
// 모달을 페이지에 추가
$('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 = $('<button class="reviewText-toggle">더보기</button>');
$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();
});