본문 바로가기
[Project] CafeHub

[CafeHub] 코드 분석 및 이슈 기록 - 카페 정보 (with. 카카오 지도)

by heosj 2024. 1. 8.

✔️ 주요 코드 중심 분석 ✔️


카카오 지 API을 통한 카페 정보 저장

Back

public void saveCafe() throws Exception {
    List<Cafe> cafes = new ArrayList<>();

    List<String> regions = new ArrayList<>();
// 1
    regions.add("서울");
    	// 지역 정보 추가하는 코드 생략
    int totalCount = 45;
    int countPerPage = 15; 
    int totalPages = (totalCount + countPerPage - 1) / countPerPage; 

// 2
    for (String region : regions) {
        for (int page = 1; page <= totalPages; page++) { 
            StringBuilder urlBuilder = new StringBuilder("https://dapi.kakao.com/v2/local/search/keyword.json");
            urlBuilder.append("?query=" + URLEncoder.encode(region + "테마카페", "UTF-8"));
            urlBuilder.append("&page=" + page);

        // 3
            URL url = new URL(urlBuilder.toString());
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("content-type", "application/json;charset=UTF-8");
            conn.setRequestProperty("Authorization", "KakaoAK "+ apiKey);

            StringBuilder resBuilder = new StringBuilder();
            try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
                String line;
                while ((line = br.readLine()) != null) {
                    resBuilder.append(line);
                }
            } finally {
                conn.disconnect();
            }

        // 4
            JSONParser parser = new JSONParser();
            JSONObject mobj = (JSONObject) parser.parse(resBuilder.toString());
            JSONArray data = (JSONArray) mobj.get("documents");

            for (int i = 0; i < data.size(); i++) {
                JSONObject cafeJson = (JSONObject) data.get(i);

                Cafe cafe = new Cafe();
                cafe.setCafeName((String) cafeJson.get("place_name"));
                cafe.setTel((String) cafeJson.get("phone"));
                cafe.setAddress((String) cafeJson.get("address_name"));
                cafe.setLat((String) cafeJson.get("y"));
                cafe.setLng((String) cafeJson.get("x"));

                cafes.add(cafe);
            }
        }
    }
    cafeRepository.saveAll(cafes);
}

 

  1. 전 지역의 카페 데이터를 수집하기 위해 지역 정보를 담는 리스트 생성
  2. API를 통해 1에서 저장한 지역의 '테마카페' 관련한 정보(주소, 위경도, 카페명)를 수집
  3. 외부 API에 HTTP GET 요청
  4. 응답 받은 데이터 중 필요한 정보를 파싱하여 데이터 베이스에 저장
관련 이슈
카카오맵 키워드 검색 API 결과값으로 최대 45개까지만 제공한다고 함
그러나 우리는 전국의 카페들을 지도에 노출해주어야 함

해결 방법
행정 구역을 리스트에 담아 리스트를 돌며 '서울 테마카페', '강원 테마카페' 와 같은 방식으로 각 행정구역별 45개의 데이터를 담도록 함 

모든 카페 위치 정보

Front

useEffect(() => {
    axios.get(`${url}/mapMarker`) // 1
    .then(response => {
      setCafes(response.data);
    })
    .catch(error => {
      console.error('에러:', error);
    });
}, []);
useEffect(() => {     
    var mapContainer = document.getElementById("mapView"),
      mapOption = {
        center: new kakao.maps.LatLng(37.4738645692092, 126.885434915952), 
        level: 4,
      };

    var map = new kakao.maps.Map(mapContainer, mapOption);

	// 2
    cafes.forEach((cafe) => {
      var imageSrc = cafe.existing
          ? "/img/map3.png" // 입점카페
          : "/img/marker_basic.png", // 기본카페
      imageSize = cafe.existing ? new kakao.maps.Size(60, 60) : new kakao.maps.Size(50, 50),
      imageOption = { offset: new kakao.maps.Point(27, 69) };

      var markerImage = new kakao.maps.MarkerImage(
        imageSrc, imageSize, imageOption
      );

      const markerPosition = new kakao.maps.LatLng(cafe.lat, cafe.lng);
      const marker = new kakao.maps.Marker({
        position: markerPosition,
        map: map,
        image: markerImage,
      });

	// 3
      kakao.maps.event.addListener(marker, "click", function () {
        setSelectCafe(cafe);
      });
    });

	// 4
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(function (position) {
        const lat = position.coords.latitude;
        const lon = position.coords.longitude;
        const locPosition = new kakao.maps.LatLng(lat, lon); 
        map.setCenter(locPosition); // 지도 중심 좌표 설정
      });
    }
}, [cafes]);
  1.  mapMarker 엔드포인트로 카페 정보를 불러오는 GET 요청 전달하고 응답으로 받은 데이터를 cafes[]에 저장
  2. 저장된 카페 데이터를 순회하며 서비스 입점 여부를 파악하여 마커를 달리 표시하도록 설정
  3. 마커를 클릭하면 선택한 카페의 정보를 selectCafe에 저장
  4. 사용자의 현재 위치를 기반으로 지도의 중심 좌표를 설정

서비스 입점하지 않은 카페는 초록색 원모양 마커, 입점한 카페는 눈에 잘 띄는 마커로 차별화

 

Back

@GetMapping("/mapMarker")
public ResponseEntity<Object> getAllCafes() {
    try {
        List<CafeDto> cafes = service.getCafes(); // 1
        return new ResponseEntity<>(cafes, HttpStatus.OK); 
    } catch (Exception e) {
        e.printStackTrace();
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}
public List<CafeDto> getCafes(){
    List<Cafe> cafeList = cafeRepository.findAll(); // 2
    List<CafeDto> cafeDTOList = new ArrayList<>(); 
    for (Cafe cafe : cafeList) { // 3
        CafeDto cafeDTO = cafe.toDTO(); 
        cafeDTOList.add(cafeDTO); 
    }
    return cafeDTOList;
}
  1. 클라이언트에서 전달 받은 데이터를 통해 서비스 레이어로 모든 카페 정보 요청
  2. 데이터베이스에서 모든 카페 정보를 불러옴
  3. Cafe 객체를 DTO로 변환하여 클라이언트에 반환 

특정 카페 정보

Front

// 1
const MapCafeInfo = ({ selectCafe, setSelectCafe, wish, setWish, wishModal, wishCafeNo }) => {

useEffect(() => { 
if(selectCafe !== null) {
    axios.get(`${url}/review/storeList/${cafeNo}?page=${currentPage}&size=5`) // 2
    .then((res) => {
        setReviewList(res.data.data);
        setTotalPages(res.data.pageInfo.totalPages);
    })
    .catch((error) => {
    	console.error("에러:" + error);
    }); 
}
  1. 앞서 모든 카페 정보 노출시 마커를 클릭하여 저장한 selectCafe 정보를 props로 전달
  2. review/storeList/{cafeNo} 엔드포인트로 특정 카페에 작성된 리뷰 리스트 GET 요청 전달
  3. 특정 카페에 작성된 리뷰 리스트는 5개를 1페이지로 지정하므로 페이지 정보도 함께 저장 

Back

@GetMapping("/review/storeList/{cafeNo}")
public ResponseEntity<Object> getStoreList(@RequestParam("page") Integer page,
					@RequestParam("size") Integer size,
                                        @PathVariable("cafeNo") Integer cafeNo){
    try{
        Page<Review> reviewPage = reviewService.storeReviewPage(page-1, size, cafeNo);
        List<Review> responseList = reviewPage.getContent();
        List<ReviewListResDto> responseLists = new ArrayList<>();
        for(Review review : responseList){
            responseLists.add(ReviewListResDto.reviewToReviewListRes(review));
        }
        return new ResponseEntity<>(new MultiResponseDto<>(responseLists, reviewPage), HttpStatus.OK);
    }catch (Exception e){
        e.printStackTrace();
        return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
    }
}

 

선택한 카페에 대한 정보와 해당 카페에 작성된 리뷰 리스트


카페 찜하기

useEffect(() => { 
    if (memNo != null) { 
      axios.get(`${url}/member/cafeIsWish/${memNo}/${cafeNo}`, { // 1
          headers : {
              Authorization :accessToken,
              Refresh : getCookie("refreshToken")
          }
      })
      .then((res) => {
        setWish(res.data);
        })
      .catch((error) => {
        console.log(error);
      })
    }
}, [selectCafe, currentPage])

const toggleWish = () => {    
if (memNo !== undefined) {
  axios.post(`${url}/member/cafeWish/${memNo}/${selectCafe.cafeNo}`, null, { // 2
      headers : {
        Authorization :accessToken,
        Refresh : getCookie("refreshToken")
    }
    .then(()=>{
        setWish(res.data);
    })
} else {
    Toast('error', '로그인이 필요합니다')
  };
}
  1. member/cafeIsWish/{memNo}/{cafeNo} 엔드포인트로 회원의 찜 여부를 확인하는 GET 요청 전달
  2. member/cafeWish/{memNo}/{cafeNo} 엔드포인트로 회원의 찜 선택 / 취소 상태를 관리하는 POST 요청 전달

Back

@GetMapping("member/cafeIsWish/{memNo}/{cafeNo}") // 특정 카페의 찜 여부
public ResponseEntity<Boolean> isWish(@PathVariable Integer memNo, @PathVariable Integer cafeNo) {
    try {
        Boolean isWish = service.isWishCafe(memNo, cafeNo);
        return new ResponseEntity<>(isWish, HttpStatus.OK);
    } catch (Exception e) {
        e.printStackTrace();
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}

@PostMapping("member/cafeWish/{memNo}/{cafeNo}") // 특정 카페 찜하기
public ResponseEntity<Boolean> isWishCafe(@PathVariable Integer memNo, @PathVariable Integer cafeNo) {
    try {
        Boolean toggleWish = service.toggleWishCafe(memNo, cafeNo);
        return new ResponseEntity<>(toggleWish, HttpStatus.OK);
    } catch (Exception e) {
        e.printStackTrace();
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}
public boolean isWishCafe(Integer memNo, Integer cafeNo){
    return wishRepository.existsByMember_memNoAndCafe_cafeNo(memNo, cafeNo);
}

@Transactional
public boolean toggleWishCafe(Integer memNo, Integer cafeNo){
    Cafe cafe = cafeRepository.findByCafeNo(cafeNo);
    Member member = memberRepository.findByMemNo(memNo);
    boolean isWish = wishRepository.existsByMember_memNoAndCafe_cafeNo(memNo, cafeNo);
    if(isWish) {
        wishRepository.deleteByMember_memNoAndCafe_cafeNo(memNo, cafeNo);
        return false;
    } else {
        wishRepository.save(WishCafe.builder().member(member).cafe(cafe).build());
        return true;
    }
}