TitleVisual.vue 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. <template>
  2. <section
  3. class="title--visual"
  4. :data-type="type"
  5. :data-theme="theme"
  6. ref="titleVisualRef"
  7. >
  8. <div class="title--visual--wrapper">
  9. <div
  10. class="title--visual--content"
  11. :data-align="textAlign"
  12. :data-animated="animation"
  13. :class="{ visible: isVisible }"
  14. >
  15. <h2 class="title--main" v-if="title" v-html="title"></h2>
  16. <span class="sub--title" v-if="subtitle" v-html="subtitle"></span>
  17. <span class="title--description" v-if="description" v-html="description"></span>
  18. <div v-if="btnTitle" class="btn--wrap mt--24">
  19. <NuxtLink
  20. :to="btnLink"
  21. :target="btnTarget"
  22. class="btn more--btn"
  23. >
  24. {{ btnTitle }}
  25. <i class="ico"></i>
  26. </NuxtLink>
  27. </div>
  28. <div v-if="$slots.default" class="title--additional">
  29. <slot></slot>
  30. </div>
  31. </div>
  32. </div>
  33. </section>
  34. </template>
  35. <script setup>
  36. // Props 정의
  37. const props = defineProps({
  38. title: {
  39. type: String,
  40. required: true,
  41. default: "",
  42. },
  43. subtitle: {
  44. type: String,
  45. default: "",
  46. },
  47. description: {
  48. type: String,
  49. required: true,
  50. default: "",
  51. },
  52. textAlign: {
  53. type: String,
  54. default: "center", // 'left', 'center', 'right'
  55. validator: (value) => ["left", "center", "right"].includes(value),
  56. },
  57. theme: {
  58. type: String,
  59. default: "light", // 'light', 'dark'
  60. validator: (value) => ["light", "dark"].includes(value),
  61. },
  62. type: {
  63. type: String,
  64. default: "", // 'light', 'dark'
  65. validator: (value) => ["", "middle", "small"].includes(value),
  66. },
  67. animation: {
  68. type: Boolean,
  69. default: true,
  70. },
  71. animationDelay: {
  72. type: Number,
  73. default: 300, // milliseconds
  74. },
  75. btnTitle: {
  76. type: String,
  77. default: '',
  78. },
  79. btnLink: {
  80. type: String,
  81. default: '#',
  82. },
  83. btnTarget: {
  84. type: String,
  85. default: '_self',
  86. validator: (value) => ['_self', '_blank'].includes(value),
  87. },
  88. });
  89. // 애니메이션 로직
  90. import { onMounted, ref } from "vue";
  91. const isVisible = ref(false);
  92. const titleVisualRef = ref(null);
  93. onMounted(() => {
  94. if (props.animation) {
  95. // IntersectionObserver로 스크롤 애니메이션 구현
  96. const observer = new IntersectionObserver(
  97. (entries) => {
  98. entries.forEach((entry) => {
  99. if (entry.isIntersecting) {
  100. setTimeout(() => {
  101. isVisible.value = true;
  102. }, props.animationDelay);
  103. observer.unobserve(entry.target);
  104. }
  105. });
  106. },
  107. {
  108. threshold: 0.3, // 30% 보일 때 애니메이션 시작
  109. }
  110. );
  111. // ref를 통해 현재 컴포넌트 요소만 선택
  112. if (titleVisualRef.value) {
  113. observer.observe(titleVisualRef.value);
  114. }
  115. // cleanup
  116. return () => {
  117. if (titleVisualRef.value) {
  118. observer.unobserve(titleVisualRef.value);
  119. }
  120. };
  121. } else {
  122. isVisible.value = true;
  123. }
  124. });
  125. </script>