DealerPopup2.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <template>
  2. <Teleport to="body">
  3. <Transition name="dealer--popup--fade">
  4. <div
  5. v-if="isOpen"
  6. class="dealer--popup--overlay type2"
  7. @click.self="handleClose"
  8. role="dialog"
  9. aria-modal="true"
  10. :aria-labelledby="dealerData ? 'dealer-name' : undefined"
  11. >
  12. <div class="dealer--popup--container" ref="popupRef">
  13. <!-- 닫기 버튼 -->
  14. <button
  15. class="dealer--popup--close"
  16. @click="handleClose"
  17. aria-label="팝업 닫기"
  18. >
  19. <span class="close--icon"></span>
  20. </button>
  21. <!-- 팝업 내용 -->
  22. <div v-if="dealerData" class="dealer--popup--content">
  23. <div class="dealer--thumb--wrap">
  24. <!-- imgsrc2가 있으면 스와이퍼 사용 -->
  25. <template v-if="dealerData.imgsrc2">
  26. <Swiper
  27. :modules="[SwiperNavigation, SwiperPagination]"
  28. :navigation="true"
  29. :loop="true"
  30. :spaceBetween="0"
  31. :slidesPerView="1"
  32. class="dealer--swiper"
  33. >
  34. <SwiperSlide v-for="(image, index) in dealerImages" :key="index">
  35. <img :src="image" :alt="`딜러 이미지 ${index + 1}`" />
  36. </SwiperSlide>
  37. </Swiper>
  38. </template>
  39. <!-- imgsrc2가 없으면 단일 이미지 -->
  40. <template v-else>
  41. <img :src="dealerData.imgsrc" alt="딜러 이미지" />
  42. </template>
  43. </div>
  44. <div class="dealer--infos--wrap">
  45. <!-- 헤더 -->
  46. <div class="dealer--popup--header">
  47. <h2 id="dealer-name" class="dealer--name">
  48. {{ dealerData.fullName }}
  49. </h2>
  50. </div>
  51. <!-- 정보 섹션 -->
  52. <div class="dealer--popup--body">
  53. <!-- 소개 -->
  54. <div v-if="dealerData.desc" class="dealer--info--section">
  55. <div class="info--label desc--type">{{ dealerData.desc }}
  56. </div>
  57. </div>
  58. <!-- 강조 문구 -->
  59. <div v-if="dealerData.strongDesc"><b>{{ dealerData.strongDesc }}</b></div>
  60. <!-- 주소 -->
  61. <div v-if="dealerData.address" class="dealer--info--section">
  62. <div class="info--label">주소: {{ dealerData.address }}</div>
  63. </div>
  64. <!-- 전화번호 -->
  65. <div v-if="dealerData.phone" class="dealer--info--section">
  66. <div class="info--label">Tel: {{ dealerData.phone }}</div>
  67. </div>
  68. <!-- 팩스 -->
  69. <div v-if="dealerData.fax" class="dealer--info--section">
  70. <div class="info--label">Fax: {{ dealerData.fax }}</div>
  71. </div>
  72. <!-- A/S -->
  73. <div v-if="dealerData.as" class="dealer--info--section">
  74. <div class="info--label">A/S: {{ dealerData.as }}</div>
  75. </div>
  76. <!-- 추가내용 -->
  77. <div v-if="dealerData.add1" class="dealer--info--section ">
  78. <div class="info--label">
  79. {{ dealerData.add1 }}: {{ dealerData.adddesc1 }}
  80. </div>
  81. </div>
  82. <!-- 추가내용 -->
  83. <div v-if="dealerData.add2" class="dealer--info--section">
  84. <div class="info--label">
  85. {{ dealerData.add2 }}: {{ dealerData.adddesc2 }}
  86. </div>
  87. </div>
  88. <!-- 추가내용 -->
  89. <div v-if="dealerData.add3" class="dealer--info--section">
  90. <div class="info--label">
  91. {{ dealerData.add3 }}: {{ dealerData.adddesc3 }}
  92. </div>
  93. </div>
  94. <!-- 추가내용 -->
  95. <div v-if="dealerData.addtext" class="dealer--info--section">
  96. <div class="info--label">
  97. {{ dealerData.addtext }}
  98. </div>
  99. </div>
  100. <!-- 웹사이트 -->
  101. <div v-if="dealerData.website" class="dealer--info--section">
  102. <div class="info--label">
  103. <NuxtLink
  104. :to="dealerData.website"
  105. target="_blank"
  106. class="light--gray--btn mt--20 ft--14"
  107. >
  108. {{ dealerData.websitetitle }}
  109. </NuxtLink>
  110. </div>
  111. </div>
  112. </div>
  113. </div>
  114. </div>
  115. <!-- 데이터가 없을 때 -->
  116. <div v-else class="dealer--popup--empty">
  117. <p>딜러 정보를 불러올 수 없습니다.</p>
  118. </div>
  119. </div>
  120. </div>
  121. </Transition>
  122. </Teleport>
  123. </template>
  124. <script setup>
  125. import { ref, watch, onMounted, onUnmounted, computed } from "vue";
  126. import { Navigation as SwiperNavigation, Pagination as SwiperPagination } from "swiper/modules";
  127. import { Swiper, SwiperSlide } from "swiper/vue";
  128. import "swiper/css";
  129. import "swiper/css/navigation";
  130. import "swiper/css/pagination";
  131. const props = defineProps({
  132. isOpen: {
  133. type: Boolean,
  134. required: true,
  135. default: false,
  136. },
  137. dealerData: {
  138. type: Object,
  139. default: null,
  140. },
  141. });
  142. const emit = defineEmits(["close"]);
  143. const popupRef = ref(null);
  144. // 딜러 이미지 배열 생성 (imgsrc, imgsrc2, imgsrc3, ... 동적 처리)
  145. const dealerImages = computed(() => {
  146. if (!props.dealerData) return [];
  147. const images = [];
  148. let index = 1;
  149. // imgsrc부터 시작
  150. if (props.dealerData.imgsrc) {
  151. images.push(props.dealerData.imgsrc);
  152. }
  153. // imgsrc2, imgsrc3, ... 숫자가 있는 이미지들 추가
  154. while (props.dealerData[`imgsrc${index + 1}`]) {
  155. images.push(props.dealerData[`imgsrc${index + 1}`]);
  156. index++;
  157. }
  158. return images;
  159. });
  160. const handleClose = () => {
  161. emit("close");
  162. };
  163. // ESC 키로 팝업 닫기
  164. const handleEscKey = (event) => {
  165. if (event.key === "Escape" && props.isOpen) {
  166. handleClose();
  167. }
  168. };
  169. // body 스크롤 제어
  170. const toggleBodyScroll = (disable) => {
  171. if (disable) {
  172. document.body.style.overflow = "hidden";
  173. } else {
  174. document.body.style.overflow = "";
  175. }
  176. };
  177. // 포커스 트랩
  178. const handleFocusTrap = (event) => {
  179. if (!props.isOpen || !popupRef.value) return;
  180. const focusableElements = popupRef.value.querySelectorAll(
  181. 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  182. );
  183. const firstElement = focusableElements[0];
  184. const lastElement = focusableElements[focusableElements.length - 1];
  185. if (event.key === "Tab") {
  186. if (event.shiftKey && document.activeElement === firstElement) {
  187. event.preventDefault();
  188. lastElement.focus();
  189. } else if (!event.shiftKey && document.activeElement === lastElement) {
  190. event.preventDefault();
  191. firstElement.focus();
  192. }
  193. }
  194. };
  195. // isOpen 상태 감지
  196. watch(
  197. () => props.isOpen,
  198. (newValue) => {
  199. toggleBodyScroll(newValue);
  200. if (newValue) {
  201. // 팝업이 열릴 때 첫 번째 포커스 가능한 요소에 포커스
  202. setTimeout(() => {
  203. if (popupRef.value) {
  204. const firstFocusable = popupRef.value.querySelector(
  205. 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  206. );
  207. if (firstFocusable) {
  208. firstFocusable.focus();
  209. }
  210. }
  211. }, 100);
  212. }
  213. }
  214. );
  215. onMounted(() => {
  216. document.addEventListener("keydown", handleEscKey);
  217. document.addEventListener("keydown", handleFocusTrap);
  218. });
  219. onUnmounted(() => {
  220. document.removeEventListener("keydown", handleEscKey);
  221. document.removeEventListener("keydown", handleFocusTrap);
  222. toggleBodyScroll(false);
  223. });
  224. </script>