carouselModels.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <template>
  2. <main class="models--page">
  3. <div class="type--carousel">
  4. <!-- 필터 -->
  5. <div class="models--filter--section">
  6. <div class="filter--group">
  7. <!-- 연료 타입 필터 -->
  8. <div class="filter--dropdown--list">
  9. <div class="filter--dropdown">
  10. <button
  11. v-for="fuel in filters.fuelType"
  12. :key="fuel.id"
  13. :class="['filter--btn', { active: activeFuelTypes.includes(fuel.id) }]"
  14. @click="toggleFuelType(fuel.id)"
  15. >
  16. <svg
  17. v-if="fuel.id == 'etron'"
  18. fill="none"
  19. xmlns="http://www.w3.org/2000/svg"
  20. aria-hidden="true"
  21. width="16px"
  22. height="16px"
  23. class=""
  24. >
  25. <use
  26. v-if="activeFuelTypes.includes(fuel.id)"
  27. href="/img/select-xs.svg#main"
  28. ></use>
  29. <use v-else href="/img/charging-xs.svg#main"></use>
  30. </svg>
  31. <svg
  32. v-else-if="activeFuelTypes.includes(fuel.id)"
  33. fill="none"
  34. xmlns="http://www.w3.org/2000/svg"
  35. aria-hidden="true"
  36. width="16px"
  37. height="16px"
  38. class=""
  39. >
  40. <use href="/img/select-xs.svg#main"></use>
  41. </svg>
  42. {{ fuel.name }}
  43. </button>
  44. <button
  45. v-for="body in filters.bodyType"
  46. :key="body.id"
  47. :class="['filter--btn', { active: activeBodyTypes.includes(body.id) }]"
  48. @click="toggleBodyType(body.id)"
  49. >
  50. <svg
  51. v-if="activeBodyTypes.includes(body.id)"
  52. fill="none"
  53. xmlns="http://www.w3.org/2000/svg"
  54. aria-hidden="true"
  55. width="16px"
  56. height="16px"
  57. class="sc-AxhUy kMyssS sc-fzoant LAaNs"
  58. >
  59. <use href="/img/select-xs.svg#main"></use>
  60. </svg>
  61. {{ body.name }}
  62. </button>
  63. </div>
  64. </div>
  65. </div>
  66. </div>
  67. <!-- 차량 그리드 (항상 표시) -->
  68. <div class="models--list--wrap">
  69. <div class="all--models--wrap">
  70. <div class="all--models">Model 보기 ({{ filteredModels.length }})</div>
  71. <div class="nav--btn--wrap">
  72. <button class="nav--prev--btn">
  73. <svg
  74. fill="none"
  75. xmlns="http://www.w3.org/2000/svg"
  76. aria-hidden="true"
  77. width="24px"
  78. height="24px"
  79. class=""
  80. >
  81. <use href="/img/forward-s.svg#main"></use>
  82. </svg>
  83. </button>
  84. <button class="nav--next--btn">
  85. <svg
  86. fill="none"
  87. xmlns="http://www.w3.org/2000/svg"
  88. aria-hidden="true"
  89. width="24px"
  90. height="24px"
  91. class=""
  92. >
  93. <use href="/img/forward-s.svg#main"></use>
  94. </svg>
  95. </button>
  96. </div>
  97. </div>
  98. <div class="models--list">
  99. <Swiper
  100. :slides-per-view="'auto'"
  101. :space-between="8"
  102. :navigation="{
  103. nextEl: '.nav--next--btn',
  104. prevEl: '.nav--prev--btn',
  105. }"
  106. :centered-slides="false"
  107. :modules="modules"
  108. class="models--swiper"
  109. >
  110. <SwiperSlide
  111. v-for="model in filteredModels"
  112. :key="model.id"
  113. :class="['model--item', { active: selectedGroupName === model.groupName }]"
  114. >
  115. <div @click="handleModelClick(model)">
  116. <!-- 이미지 -->
  117. <h2 class="model--name">
  118. {{ model.groupName }}
  119. <div class="etron--wrap" v-if="model.fuelType === 'etron'">
  120. <div class="etron">
  121. <svg
  122. fill="none"
  123. xmlns="http://www.w3.org/2000/svg"
  124. aria-hidden="true"
  125. width="16px"
  126. height="16px"
  127. class="sc-AxhUy kMyssS sc-fzoant LAaNs"
  128. >
  129. <use href="/img/charging-xs.svg#main"></use>
  130. </svg>
  131. </div>
  132. </div>
  133. </h2>
  134. <div class="model--image--wrap">
  135. <img :src="model.image" :alt="model.name" />
  136. </div>
  137. <!-- 정보 -->
  138. <div class="model--info--wrap">
  139. Model 보기 {{ getGroupModelCount(model.groupName) }}
  140. <i class="ico"></i>
  141. </div>
  142. </div>
  143. </SwiperSlide>
  144. </Swiper>
  145. </div>
  146. <!-- 모든 차량 리스트 (Swiper 차량 클릭 시 드롭다운 표시) -->
  147. <div class="models--list--car" :class="{ active: selectedGroupName }">
  148. <div class="car--list">
  149. <div v-for="car in filteredCars" :key="car.id" class="car--item">
  150. <div class="bedge--wrap">
  151. <div class="etron--wrap" v-if="car.fuelType === 'etron'">
  152. <div class="etron">
  153. <svg
  154. fill="none"
  155. xmlns="http://www.w3.org/2000/svg"
  156. aria-hidden="true"
  157. width="16px"
  158. height="16px"
  159. class="sc-AxhUy kMyssS sc-fzoant LAaNs"
  160. >
  161. <use href="/img/charging-xs.svg#main"></use>
  162. </svg>
  163. Electrified
  164. </div>
  165. </div>
  166. </div>
  167. <div class="car--image">
  168. <NuxtLink :to="car.link">
  169. <img :src="car.image" :alt="car.name" />
  170. </NuxtLink>
  171. </div>
  172. <div class="car--info">
  173. <h3 class="car--name">{{ car.fullName }}</h3>
  174. <NuxtLink :to="car.link" class="car--link">자세히 보기</NuxtLink>
  175. </div>
  176. </div>
  177. </div>
  178. </div>
  179. </div>
  180. </div>
  181. </main>
  182. </template>
  183. <script setup>
  184. import { Swiper, SwiperSlide } from "swiper/vue";
  185. import { Navigation } from "swiper/modules";
  186. import "swiper/css";
  187. import "swiper/css/navigation";
  188. const modules = [Navigation];
  189. const modelsData = ref({
  190. models: [],
  191. filters: { fuelType: [], bodyType: [] },
  192. });
  193. const activeFuelTypes = ref([]);
  194. const activeBodyTypes = ref([]);
  195. const selectedGroupName = ref(null); // Swiper에서 클릭한 그룹
  196. // 필터 옵션
  197. const filters = computed(() => modelsData.value.filters);
  198. // 필터 토글 함수
  199. const toggleFuelType = (value) => {
  200. const index = activeFuelTypes.value.indexOf(value);
  201. if (index > -1) {
  202. activeFuelTypes.value.splice(index, 1);
  203. } else {
  204. activeFuelTypes.value.push(value);
  205. }
  206. // 필터 버튼 클릭 시 선택된 그룹 초기화
  207. selectedGroupName.value = null;
  208. };
  209. const toggleBodyType = (value) => {
  210. const index = activeBodyTypes.value.indexOf(value);
  211. if (index > -1) {
  212. activeBodyTypes.value.splice(index, 1);
  213. } else {
  214. activeBodyTypes.value.push(value);
  215. }
  216. // 필터 버튼 클릭 시 선택된 그룹 초기화
  217. selectedGroupName.value = null;
  218. };
  219. // groupName별 모델 개수를 계산하는 함수
  220. const getGroupModelCount = (groupName) => {
  221. return modelsData.value.models.filter((model) => model.groupName === groupName)
  222. .length;
  223. };
  224. // 연료 타입 이름 가져오기
  225. const getFuelTypeName = (fuelType) => {
  226. const fuelTypes = {
  227. etron: "전기차",
  228. hybrid: "하이브리드",
  229. "gas-diesel": "가솔린/디젤",
  230. };
  231. return fuelTypes[fuelType] || fuelType;
  232. };
  233. // 모델 그룹 클릭 핸들러 (Swiper에서 차량 클릭 - 토글)
  234. const handleModelClick = (model) => {
  235. // 같은 그룹을 다시 클릭하면 토글 (숨김)
  236. if (selectedGroupName.value === model.groupName) {
  237. selectedGroupName.value = null;
  238. } else {
  239. // 다른 그룹을 클릭하면 변경
  240. selectedGroupName.value = model.groupName;
  241. }
  242. };
  243. // 필터링된 차량 리스트 (모든 차량)
  244. const filteredCars = computed(() => {
  245. // Swiper에서 차량을 클릭한 경우 - groupName으로 필터링
  246. if (selectedGroupName.value) {
  247. return modelsData.value.models.filter(
  248. (model) => model.groupName === selectedGroupName.value
  249. );
  250. }
  251. return [];
  252. });
  253. // 필터링된 모델 계산 (그룹화된 모델)
  254. const filteredModels = computed(() => {
  255. let filtered = modelsData.value.models;
  256. // 연료 타입 필터 (OR 조건 - 선택된 연료 타입 중 하나라도 일치하면 포함)
  257. if (activeFuelTypes.value.length > 0) {
  258. filtered = filtered.filter((model) =>
  259. activeFuelTypes.value.includes(model.fuelType)
  260. );
  261. }
  262. // 바디 타입 필터 (OR 조건 - 선택된 바디 타입 중 하나라도 일치하면 포함)
  263. if (activeBodyTypes.value.length > 0) {
  264. filtered = filtered.filter((model) =>
  265. model.bodyType.some((type) => activeBodyTypes.value.includes(type))
  266. );
  267. }
  268. // groupName별로 그룹화하고 각 그룹의 첫 번째 모델만 추출
  269. const groupedByName = {};
  270. filtered.forEach((model) => {
  271. // groupName을 키로 사용
  272. const groupName = model.groupName;
  273. // 해당 groupName 그룹이 없으면 생성하고 첫 번째 모델 저장
  274. if (!groupedByName[groupName]) {
  275. groupedByName[groupName] = model;
  276. }
  277. });
  278. // 그룹화된 객체에서 값(첫 번째 모델들)만 배열로 반환
  279. return Object.values(groupedByName);
  280. });
  281. onMounted(async () => {
  282. try {
  283. const response = await fetch("/models.json");
  284. const data = await response.json();
  285. modelsData.value = data;
  286. console.log("Models data loaded:", data);
  287. } catch (error) {
  288. console.error("Failed to load models data:", error);
  289. }
  290. });
  291. </script>
  292. <style scoped lang="scss">
  293. .type--carousel {
  294. .models--list {
  295. position: relative;
  296. }
  297. .models--list--car {
  298. transition: all 0.3s ease-in-out;
  299. }
  300. }
  301. </style>