TitleVisual.vue 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  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-html="title"></h2>
  16. <span class="sub--title" v-html="subtitle"></span>
  17. <span class="title--description" v-html="description"></span>
  18. <div v-if="$slots.default" class="title--additional">
  19. <slot></slot>
  20. </div>
  21. </div>
  22. </div>
  23. </section>
  24. </template>
  25. <script setup>
  26. // Props 정의
  27. const props = defineProps({
  28. title: {
  29. type: String,
  30. required: true,
  31. default: "",
  32. },
  33. subtitle: {
  34. type: String,
  35. default: "",
  36. },
  37. description: {
  38. type: String,
  39. required: true,
  40. default: "",
  41. },
  42. textAlign: {
  43. type: String,
  44. default: "center", // 'left', 'center', 'right'
  45. validator: (value) => ["left", "center", "right"].includes(value),
  46. },
  47. theme: {
  48. type: String,
  49. default: "light", // 'light', 'dark'
  50. validator: (value) => ["light", "dark"].includes(value),
  51. },
  52. type: {
  53. type: String,
  54. default: "", // 'light', 'dark'
  55. validator: (value) => ["", "middle", "small"].includes(value),
  56. },
  57. animation: {
  58. type: Boolean,
  59. default: true,
  60. },
  61. animationDelay: {
  62. type: Number,
  63. default: 300, // milliseconds
  64. },
  65. });
  66. // 애니메이션 로직
  67. import { onMounted, ref } from "vue";
  68. const isVisible = ref(false);
  69. const titleVisualRef = ref(null);
  70. onMounted(() => {
  71. if (props.animation) {
  72. // IntersectionObserver로 스크롤 애니메이션 구현
  73. const observer = new IntersectionObserver(
  74. (entries) => {
  75. entries.forEach((entry) => {
  76. if (entry.isIntersecting) {
  77. setTimeout(() => {
  78. isVisible.value = true;
  79. }, props.animationDelay);
  80. observer.unobserve(entry.target);
  81. }
  82. });
  83. },
  84. {
  85. threshold: 0.3, // 30% 보일 때 애니메이션 시작
  86. }
  87. );
  88. // ref를 통해 현재 컴포넌트 요소만 선택
  89. if (titleVisualRef.value) {
  90. observer.observe(titleVisualRef.value);
  91. }
  92. // cleanup
  93. return () => {
  94. if (titleVisualRef.value) {
  95. observer.unobserve(titleVisualRef.value);
  96. }
  97. };
  98. } else {
  99. isVisible.value = true;
  100. }
  101. });
  102. </script>