2024-12-03 14:28:53

Home.vue

<script setup>
import { ref, computed } from "vue";
import properties from "../data/properties"; // 매물 데이터 가져오기
import { formatPrice } from "../utils/format"; // 가격 포맷 유틸리티

// 상태 관리
const minPrice = ref(""); // 최소 가격
const maxPrice = ref(""); // 최대 가격
const selectedCity = ref(""); // 선택된 도시
const filteredProperties = ref(properties); // 필터링된 매물 목록 초기값은 모든 매물
const currentPage = ref(1); // 현재 페이지
const itemsPerPage = 5; // 페이지당 매물 개수

// 즐겨찾기 상태
const favorites = ref(
  JSON.parse(localStorage.getItem("favorites")) || [] // localStorage에서 즐겨찾기 불러오기
);

// 도시 목록 정의
const cities = [
  "서울특별시",
  "부산광역시",
  "대구광역시",
  "인천광역시",
  "광주광역시",
  "대전광역시",
  "울산광역시",
  "세종특별자치시",
  "경기도",
  "강원도",
  "충청북도",
  "충청남도",
  "전라북도",
  "전라남도",
  "경상북도",
  "경상남도",
  "제주특별자치도",
];

// 검색 버튼 클릭 시 실행
const searchProperties = () => {
  const matchingProperties = properties.filter((property) => {
    const matchesPrice =
      (minPrice.value === "" ||
        property.price >= (parseInt(minPrice.value) || 0)) &&
      (maxPrice.value === "" ||
        property.price <= (parseInt(maxPrice.value) || Infinity));

    const matchesCity =
      selectedCity.value === "" ||
      property.location.includes(selectedCity.value);

    return matchesPrice && matchesCity;
  });

  filteredProperties.value = matchingProperties;
  currentPage.value = 1; // 페이지 초기화
};

// 초기화 버튼 클릭 시 실행
const resetFilters = () => {
  minPrice.value = "";
  maxPrice.value = "";
  selectedCity.value = "";
  filteredProperties.value = properties; // 초기 매물 리스트로 복원
  currentPage.value = 1; // 페이지 초기화
};

// 즐겨찾기 추가/제거 함수
const toggleFavorite = (propertyId) => {
  if (favorites.value.includes(propertyId)) {
    favorites.value = favorites.value.filter((id) => id !== propertyId); // 즐겨찾기 제거
  } else {
    favorites.value.push(propertyId); // 즐겨찾기 추가
  }
  localStorage.setItem("favorites", JSON.stringify(favorites.value)); // localStorage 업데이트
};

// 현재 페이지의 매물 계산
const paginatedProperties = computed(() => {
  const startIndex = (currentPage.value - 1) * itemsPerPage;
  const endIndex = startIndex + itemsPerPage;
  return filteredProperties.value.slice(startIndex, endIndex);
});

// 총 페이지 수 계산
const totalPages = computed(() =>
  Math.ceil(filteredProperties.value.length / itemsPerPage)
);

// 페이지 변경
const changePage = (page) => {
  if (page >= 1 && page <= totalPages.value) {
    currentPage.value = page;
  }
};
</script>

<template>
  <div>
    <h1>부동산 매물 검색</h1>

    <!-- 검색 필터 -->
    <div class="filters">
      <label>최소 가격 (만원):</label>
      <input type="number" v-model="minPrice" placeholder="최소 가격 입력" />

      <label>최대 가격 (만원):</label>
      <input type="number" v-model="maxPrice" placeholder="최대 가격 입력" />

      <label>도시:</label>
      <select v-model="selectedCity">
        <option value="">전체</option>
        <option v-for="city in cities" :key="city" :value="city">
          {{ city }}
        </option>
      </select>

      <button @click="searchProperties">검색</button>
      <button @click="resetFilters" class="reset-button">초기화</button>
    </div>

    <!-- 검색 결과 -->
    <h2>검색 결과</h2>
    <div v-if="paginatedProperties.length > 0" class="property-list">
      <div
        v-for="property in paginatedProperties"
        :key="property.id"
        class="property-card"
      >
        <img :src="property.images[0]" alt="대표 매물 이미지" />
        <h3>{{ property.title }}</h3>
        <p>가격: {{ formatPrice(property.price) }} 만원</p>
        <p>위치: {{ property.location }}</p>
        <router-link :to="`/property/${property.id}`">상세보기</router-link>

        <!-- 즐겨찾기 버튼 -->
        <button @click="toggleFavorite(property.id)">
          {{ favorites.includes(property.id) ? "즐겨찾기 해제" : "즐겨찾기" }}
        </button>
      </div>
    </div>
    <div v-else>
      <p>검색 조건에 맞는 매물이 없습니다.</p>
    </div>

    <!-- 즐겨찾기 목록 -->
    <h2>즐겨찾기 목록</h2>
    <div v-if="favorites.length > 0" class="property-list">
      <div
        v-for="property in properties.filter((p) => favorites.includes(p.id))"
        :key="property.id"
        class="property-card"
      >
        <img :src="property.images[0]" alt="대표 매물 이미지" />
        <h3>{{ property.title }}</h3>
        <p>가격: {{ formatPrice(property.price) }} 만원</p>
        <p>위치: {{ property.location }}</p>
        <router-link :to="`/property/${property.id}`">상세보기</router-link>

        <!-- 즐겨찾기 해제 버튼 -->
        <button @click="toggleFavorite(property.id)">즐겨찾기 해제</button>
      </div>
    </div>
    <div v-else>
      <p>즐겨찾기한 매물이 없습니다.</p>
    </div>

    <!-- 페이징 -->
    <div v-if="totalPages > 1" class="pagination">
      <button
        @click="changePage(currentPage - 1)"
        :disabled="currentPage === 1"
      >
        이전
      </button>
      <span v-for="page in totalPages" :key="page" @click="changePage(page)">
        <button :class="{ active: page === currentPage }">{{ page }}</button>
      </span>
      <button
        @click="changePage(currentPage + 1)"
        :disabled="currentPage === totalPages"
      >
        다음
      </button>
    </div>
  </div>
</template>

<style scoped>
.filters {
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}

.reset-button {
  background-color: #f44336;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
}

.reset-button:hover {
  background-color: #d32f2f;
}

.property-list {
  display: grid;
  grid-template-columns: repeat(5, 1fr); /* 5열 */
  gap: 1rem;
}

.property-card {
  border: 1px solid #ccc;
  padding: 1rem;
  border-radius: 8px;
  text-align: center;
  background-color: #f9f9f9;
}

.property-card img {
  width: 100%;
  height: auto;
  border-radius: 4px;
}

.property-card a {
  display: block;
  margin-top: 0.5rem;
  color: #007bff;
  text-decoration: none;
}

.property-card a:hover {
  text-decoration: underline;
}

.property-card button {
  margin-top: 0.5rem;
  padding: 0.5rem;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.property-card button:hover {
  background-color: #0056b3;
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 1rem;
  gap: 0.5rem;
}

.pagination button {
  padding: 0.5rem 1rem;
  border: 1px solid #ccc;
  background-color: #007bff;
  color: white;
  border-radius: 4px;
  cursor: pointer;
}

.pagination button.active {
  background-color: #0056b3;
}

.pagination button:disabled {
  background-color: #ddd;
  cursor: not-allowed;
}
</style>

<script setup> 섹션

import { ref, computed } from "vue"

ref 
Vue에서 반응형 상태를 선언하기 위해 사용하는 함수, 일반 변수처럼 값을 저장하지만, 값이 변경될 때 Vue가 이를 감지하고 필요한 DOM 업데이트를 자동으로 수행함

  • 값을 value 프로퍼티를 통해 접근해야 함
  • Vue가 값의 변화를 감지해 관련 컴포넌트를 업데이트

언제 사용하는가

  • 상태를 저장하고 반응형으로 관리해야 할 때
  • 변수의 값이 변경될 때 DOM 업데이트가 필요할 때

computed

계산된 속성을 정의하기 위해 사용하는 함수, ref나 reactive 데이터에 의존해 새로운 값을 계산하며, 의존 데이터가 변경될 때만 다시 계산함

  • 캐싱 : 의존 데이터가 변경되지 않으면 계산을 반복하지 않음
  • 읽기 전용 속성으로 사용하거나, 읽기/쓰기 모두 가능하게 설정이 가능

* 번외

reactive를 사용하지 않는 이유

1. 타입제한 : ref에서는 어떤 타입도 사용할 수 있는 반면, reactive는 Object, array, Map, Set과 같은 타입에서만 사용할 수 있음

reactive의 장점 : 자바스크립트와 템플릿 사이에 일관성, 반응형 변수를 많이 선언할 때 간단하게 사용 가능, Vue2의 data()와 비슷함

 

<template> 섹션

UI를 정의하는 부분

 

1. 검색필터

<div class="filters">
  <label>최소 가격 (만원):</label>
  <input type="number" v-model="minPrice" placeholder="최소 가격 입력" />
  <label>최대 가격 (만원):</label>
  <input type="number" v-model="maxPrice" placeholder="최대 가격 입력" />
  <label>도시:</label>
  <select v-model="selectedCity">
    <option value="">전체</option>
    <option v-for="city in cities" :key="city" :value="city">{{ city }}</option>
  </select>
  <button @click="searchProperties">검색</button>
  <button @click="resetFilters" class="reset-button">초기화</button>
</div>
  • 필터 UI 구성
  • v-model을 통해 상태와 입력 필드가 양방향으로 바인딩 됨
  • 검색 버튼 클릭 시 searchProperties() 호출, 초기화 버튼 클릭 시 resetFilters() 호출

2. 검색결과

<div v-if="paginatedProperties.length > 0" class="property-list">
  <div v-for="property in paginatedProperties" :key="property.id" class="property-card">
    <img :src="property.images[0]" alt="대표 매물 이미지" />
    <h3>{{ property.title }}</h3>
    <p>가격: {{ formatPrice(property.price) }} 만원</p>
    <p>위치: {{ property.location }}</p>
    <router-link :to="`/property/${property.id}`">상세보기</router-link>
    <button @click="toggleFavorite(property.id)">
      {{ favorites.includes(property.id) ? "즐겨찾기 해제" : "즐겨찾기" }}
    </button>
  </div>
</div>
<div v-else>
  <p>검색 조건에 맞는 매물이 없습니다.</p>
</div>
  • 검색된 매물을 표시함
  • v-if를 사용하여 조건에 맞는 매물이 없으면 "검색 조건에 매맞는 매물이 없음을 표시"

3. 즐겨찾기 목록

<h2>즐겨찾기 목록</h2>
<div v-if="favorites.length > 0" class="property-list">
  <div v-for="property in properties.filter((p) => favorites.includes(p.id))" :key="property.id" class="property-card">
    ...
  </div>
</div>
<div v-else>
  <p>즐겨찾기한 매물이 없습니다.</p>
</div>
  • 즐겨찾기에 추가된 매물을 표시
  • 매울이 없으면 "즐겨찾기한 매물이 없습니다"를 표시

4. 페이징 처리

<div v-if="totalPages > 1" class="pagination">
  <button @click="changePage(currentPage - 1)" :disabled="currentPage === 1">이전</button>
  <span v-for="page in totalPages" :key="page" @click="changePage(page)">
    <button :class="{ active: page === currentPage }">{{ page }}</button>
  </span>
  <button @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages">다음</button>
</div>

4.1 <div v-if="totalPages > 1" class="pagination">

'VUE 공부' 카테고리의 다른 글

소규모 Project 제작 -1  (1) 2024.11.26
VUE 공부 5일차  (1) 2024.11.19
VUE 공부 4일차  (1) 2024.11.18
VUE 공부 3일차  (0) 2024.11.18
VUE 공부 2일차  (2) 2024.11.15