PopupModal.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. 'use client';
  2. import { useState, useEffect, useCallback } from 'react';
  3. import { Swiper, SwiperSlide } from 'swiper/react';
  4. import { Navigation, Pagination } from 'swiper/modules';
  5. import { PopupItem, PopupResponse } from '@/types/response/page/popup';
  6. import { fetchApi } from '@/lib/utils/client';
  7. import 'swiper/css';
  8. import 'swiper/css/navigation';
  9. import 'swiper/css/pagination';
  10. import './PopupModal.scss';
  11. interface PopupModalProps {
  12. position: string;
  13. }
  14. // localStorage 키 생성
  15. function getDismissKey(position: string): string {
  16. return `popup_dismiss_${position}`;
  17. }
  18. // 24시간 이내에 dismiss 했는지 확인
  19. function isDismissed(position: string): boolean {
  20. if (typeof window === 'undefined') {
  21. return false;
  22. }
  23. const dismissed = localStorage.getItem(getDismissKey(position));
  24. if (!dismissed) {
  25. return false;
  26. }
  27. const dismissedAt = parseInt(dismissed, 10);
  28. const now = Date.now();
  29. const hours24 = 24 * 60 * 60 * 1000;
  30. return (now - dismissedAt) < hours24;
  31. }
  32. // 유효한 팝업인지 확인 (isActive + 날짜 범위)
  33. function isValidPopup(item: PopupItem): boolean {
  34. if (!item.isActive) {
  35. return false;
  36. }
  37. const now = new Date();
  38. if (item.startAt && new Date(item.startAt) > now) {
  39. return false;
  40. }
  41. if (item.endAt && new Date(item.endAt) < now) {
  42. return false;
  43. }
  44. return true;
  45. }
  46. export default function PopupModal({ position }: PopupModalProps) {
  47. const [open, setOpen] = useState<boolean>(false);
  48. const [items, setItems] = useState<PopupItem[]>([]);
  49. const [dismissToday, setDismissToday] = useState<boolean>(false);
  50. useEffect(() => {
  51. // 이미 dismiss 되었으면 fetch하지 않음
  52. if (isDismissed(position)) {
  53. return;
  54. }
  55. fetchApi<PopupResponse>('/api/popup/popups', {
  56. method: 'POST',
  57. body: { Code: position }
  58. }).then((res) => {
  59. if (res.success && res.data) {
  60. const validItems = (res.data.list ?? []).filter(isValidPopup);
  61. if (validItems.length > 0) {
  62. setItems(validItems);
  63. setOpen(true);
  64. }
  65. }
  66. });
  67. }, [position]);
  68. const handleClose = useCallback(() => {
  69. if (dismissToday) {
  70. localStorage.setItem(getDismissKey(position), Date.now().toString());
  71. }
  72. setOpen(false);
  73. }, [dismissToday, position]);
  74. // 팝업이 없거나 닫혔으면 렌더링하지 않음
  75. if (!open || items.length === 0) {
  76. return null;
  77. }
  78. return (
  79. <div className='popup-modal-overlay' onClick={handleClose}>
  80. <div className='popup-modal-container' onClick={(e) => e.stopPropagation()}>
  81. {/* 팝업 본문 */}
  82. <div className='popup-modal-body'>
  83. {items.length === 1 ? (
  84. // 단일 팝업
  85. <PopupSlide item={items[0]} />
  86. ) : (
  87. // 다중 팝업 - Swiper 슬라이드
  88. <Swiper
  89. modules={[Navigation, Pagination]}
  90. navigation
  91. pagination={{ clickable: true }}
  92. loop={items.length > 1}
  93. spaceBetween={0}
  94. slidesPerView={1}
  95. >
  96. {items.map((item) => (
  97. <SwiperSlide key={item.id}>
  98. <PopupSlide item={item} />
  99. </SwiperSlide>
  100. ))}
  101. </Swiper>
  102. )}
  103. </div>
  104. {/* 하단 - 하루 동안 보지 않기 + 닫기 */}
  105. <div className='popup-modal-footer'>
  106. <label className='popup-modal-dismiss'>
  107. <input
  108. type='checkbox'
  109. checked={dismissToday}
  110. onChange={(e) => setDismissToday(e.target.checked)}
  111. />
  112. <span>하루 동안 보지 않기</span>
  113. </label>
  114. <button type='button' className='popup-modal-close' onClick={handleClose}>
  115. 닫기
  116. </button>
  117. </div>
  118. </div>
  119. </div>
  120. );
  121. }
  122. // 개별 팝업 슬라이드
  123. function PopupSlide({ item }: { item: PopupItem }) {
  124. const content = (
  125. <div className='popup-slide'>
  126. {item.subject && (
  127. <h3 className='popup-slide-subject'>{item.subject}</h3>
  128. )}
  129. {item.content && (
  130. <div
  131. className='popup-slide-content'
  132. dangerouslySetInnerHTML={{ __html: item.content }}
  133. />
  134. )}
  135. </div>
  136. );
  137. if (item.link) {
  138. return (
  139. <a href={item.link} target='_blank' rel='noopener noreferrer' className='popup-slide-link'>
  140. {content}
  141. </a>
  142. );
  143. }
  144. return content;
  145. }