DealerPopup.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <template>
  2. <Teleport to="body">
  3. <Transition name="dealer--popup--fade">
  4. <div
  5. v-if="isOpen"
  6. class="dealer--popup--overlay"
  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. <img :src="dealerData.imgsrc" />
  25. </div>
  26. <div class="dealer--infos--wrap">
  27. <!-- 헤더 -->
  28. <div class="dealer--popup--header">
  29. <h2 id="dealer-name" class="dealer--name">
  30. {{ dealerData.fullName }}
  31. </h2>
  32. </div>
  33. <!-- 정보 섹션 -->
  34. <div class="dealer--popup--body">
  35. <!-- 주소 -->
  36. <div v-if="dealerData.address" class="dealer--info--section">
  37. <div class="info--label">주소: {{ dealerData.address }}</div>
  38. </div>
  39. <!-- 전화번호 -->
  40. <div v-if="dealerData.phone" class="dealer--info--section">
  41. <div class="info--label">Tel: {{ dealerData.phone }}</div>
  42. </div>
  43. <!-- 이메일 -->
  44. <div v-if="dealerData.email" class="dealer--info--section">
  45. <div class="info--label">
  46. E-mail:
  47. <a :href="`mailto:${dealerData.email}`" class="email--link">
  48. {{ dealerData.email }}
  49. </a>
  50. </div>
  51. </div>
  52. <!-- 추가내용 -->
  53. <div v-if="dealerData.add1" class="dealer--info--section mt--20">
  54. <div class="info--label">
  55. {{ dealerData.add1 }}: {{ dealerData.adddesc1 }}
  56. </div>
  57. </div>
  58. <!-- 추가내용 -->
  59. <div v-if="dealerData.add2" class="dealer--info--section">
  60. <div class="info--label">
  61. {{ dealerData.add2 }}: {{ dealerData.adddesc2 }}
  62. </div>
  63. </div>
  64. <!-- 추가내용 -->
  65. <div v-if="dealerData.add3" class="dealer--info--section">
  66. <div class="info--label">
  67. {{ dealerData.add3 }}: {{ dealerData.adddesc3 }}
  68. </div>
  69. </div>
  70. <!-- 추가내용 -->
  71. <div v-if="dealerData.addtext" class="dealer--info--section">
  72. <div class="info--label">
  73. {{ dealerData.addtext }}
  74. </div>
  75. </div>
  76. <!-- 웹사이트 -->
  77. <div v-if="dealerData.website" class="dealer--info--section">
  78. <div class="info--label">
  79. <NuxtLink
  80. :to="dealerData.website"
  81. target="_blank"
  82. class="light--gray--btn mt--20 ft--14"
  83. >
  84. {{ dealerData.websitetitle }}
  85. </NuxtLink>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. <!-- 데이터가 없을 때 -->
  92. <div v-else class="dealer--popup--empty">
  93. <p>딜러 정보를 불러올 수 없습니다.</p>
  94. </div>
  95. </div>
  96. </div>
  97. </Transition>
  98. </Teleport>
  99. </template>
  100. <script setup>
  101. import { ref, watch, onMounted, onUnmounted } from "vue";
  102. const props = defineProps({
  103. isOpen: {
  104. type: Boolean,
  105. required: true,
  106. default: false,
  107. },
  108. dealerData: {
  109. type: Object,
  110. default: null,
  111. },
  112. });
  113. const emit = defineEmits(["close"]);
  114. const popupRef = ref(null);
  115. const handleClose = () => {
  116. emit("close");
  117. };
  118. // ESC 키로 팝업 닫기
  119. const handleEscKey = (event) => {
  120. if (event.key === "Escape" && props.isOpen) {
  121. handleClose();
  122. }
  123. };
  124. // body 스크롤 제어
  125. const toggleBodyScroll = (disable) => {
  126. if (disable) {
  127. document.body.style.overflow = "hidden";
  128. } else {
  129. document.body.style.overflow = "";
  130. }
  131. };
  132. // 포커스 트랩
  133. const handleFocusTrap = (event) => {
  134. if (!props.isOpen || !popupRef.value) return;
  135. const focusableElements = popupRef.value.querySelectorAll(
  136. 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  137. );
  138. const firstElement = focusableElements[0];
  139. const lastElement = focusableElements[focusableElements.length - 1];
  140. if (event.key === "Tab") {
  141. if (event.shiftKey && document.activeElement === firstElement) {
  142. event.preventDefault();
  143. lastElement.focus();
  144. } else if (!event.shiftKey && document.activeElement === lastElement) {
  145. event.preventDefault();
  146. firstElement.focus();
  147. }
  148. }
  149. };
  150. // isOpen 상태 감지
  151. watch(
  152. () => props.isOpen,
  153. (newValue) => {
  154. toggleBodyScroll(newValue);
  155. if (newValue) {
  156. // 팝업이 열릴 때 첫 번째 포커스 가능한 요소에 포커스
  157. setTimeout(() => {
  158. if (popupRef.value) {
  159. const firstFocusable = popupRef.value.querySelector(
  160. 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  161. );
  162. if (firstFocusable) {
  163. firstFocusable.focus();
  164. }
  165. }
  166. }, 100);
  167. }
  168. }
  169. );
  170. onMounted(() => {
  171. document.addEventListener("keydown", handleEscKey);
  172. document.addEventListener("keydown", handleFocusTrap);
  173. });
  174. onUnmounted(() => {
  175. document.removeEventListener("keydown", handleEscKey);
  176. document.removeEventListener("keydown", handleFocusTrap);
  177. toggleBodyScroll(false);
  178. });
  179. </script>