<맛찾사> 기술설명서

정윤서·2024년 1월 22일
0
post-custom-banner

✔️ 개요 및 목적

🔗 개요

  • 위치 기반 서비스를 사용하여 주변의 다양한 식당 및 맛집을 쉽게 찾을 수 있게 도와주는 웹 플랫폼

🔗 목적

  • 사용자들이 원하는 지역 또는 현재 위치 주변의 다양한 식당과 맛집을 쉽고 빠르게 탐색할 수 있도록 지원
  • 특정 지역에 대한 음식점 선택의 다양성과 편의성을 제공

✔️ Github


✔️ 맛찾사 주소


✔️ 사용된 기술

🔗 버전 관리

  • Github

🔗 배포

  • docker
  • CentOS
  • Ngnix
  • MariaDB
  • FileZila
  • Putty
  • NAVER CLOUD FLATFORM

🔗 개발환경

  • IntelliJ IDEA
  • MySQL
  • Windows11
  • Chrome

🔗 기술스택

  • Java
  • Spring Boot
  • HTML, CSS, JS
  • JQuery
  • Bootstrap
  • WebSocket

🔗 API

  • Kakao OAuth 2.0
  • Kakao 지도/로컬 API
  • Google OAuth 2.0
  • Naver OAuth 2.0
  • CoolSMS REST API
  • 기상청 동네예보정보 조회서비스

✔️ 요구사항

🙍 회원

- 회원가입
- 로그인
- 로그아웃
- 아이디/비밀번호 찾기
- 프로필 변경
- 손님/사장 권한 변경


🗺️ 지도

내 위치 주변

- 사용자의 현재 위치(lon, lat)를 중심으로 0.005 단위 반경 내의 식당 조회

주소 검색

- 검색한 주소를 중심으로 0.005 단위 반경 내의 식당 조회

마커

- 별점별 지도 마크 아이콘 분류


🏠 가게

👨‍💼 사장

- 가게 등록, 조회, 수정, 삭제

🙍‍♂️ 손님

- 가게 검색 및 조회
- 즐겨찾기
- 사용자 위치 주변 별점순, 리뷰순 TOP3 식당 조회


📅 예약

👨‍💼 사장

- 예약 조회, 수락, 거부

🙍‍♂️ 손님

- 예약, 예약 조회
- 휴대폰 인증


✏️ 리뷰

👨‍💼 사장

- 리뷰에 대한 댓글 등록, 조회, 수정, 삭제

🙍‍♂️ 손님

- 리뷰 등록, 조회, 수정, 삭제


🗨️ 채팅

- 사장님에게 1:1 문의
- 채팅 수신 시 알림 발생


☀️ 날씨

- 현재 위치의 날씨 정보 표시
- 날씨에 따른 메뉴 추천, 가게 추천
- 추천된 메뉴를 기반으로 한 랜덤 돌림판


📚 게시판

- 게시물 등록, 조회, 수정, 삭제
- 댓글 등록, 조회, 수정, 삭제


✔️ ERD


✔️ 주요 기능 설명

📌 위치 기반 서비스

  • 내 위치를 중심으로 주변 맛집 표시
  • 주소 검색을 통해 검색된 주소의 맛집 표시

// 주소 입력 값 가져오기
var inputAddress = document.querySelector('#searchAddress').value;
// 정보 창 초기화
var infowindow = new kakao.maps.InfoWindow({zIndex:1});
// 사용자의 위치 설정
var locPosition = new kakao.maps.LatLng(myY, myX);
// 지도 컨테이너 및 옵션 설정
var mapContainer = document.getElementById('map'),
    mapOption = {
      center: new kakao.maps.LatLng(33.450701, 126.570667),
      level: 3
    };
// 지도 생성
var map = new kakao.maps.Map(mapContainer, mapOption);
// 사용자 정의 오버레이 초기화
var overlay = new kakao.maps.CustomOverlay({
  yAnchor: 1.2,
  zIndex:2
});
// 식당 리스트를 순회하며 마커와 오버레이 생성
resArr.forEach(function(element) {
  var imageSrc,
      imageSize = new kakao.maps.Size(45, 50),
      imageOption = {offset: new kakao.maps.Point(21, 47)};
  // 평점에 따른 마커 이미지 결정
  if (element.averageStar > 4.5) imageSrc = '/image/mark/red.png';
  else if (element.averageStar > 4) imageSrc = '/image/mark/yellow.png';
  else if (element.averageStar > 3) imageSrc = '/image/mark/green.png';
  else imageSrc = '/image/mark/blue.png';
  // 마커 이미지 설정 및 마커 생성
  var markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imageOption)
  var marker = new kakao.maps.Marker({
    map: map,
    position: new kakao.maps.LatLng(element.y, element.x),
    clickable: true,
    image: markerImage
  });
  // 각 식당의 오버레이 정보 및 스타일 설정
  var tmpOverlay = new kakao.maps.CustomOverlay({
    content: '<div class="badge text-dark" style="background:white; border:1px solid tan;">'+element.name+'</div>',
    position: marker.getPosition(),
    map: map,
    yAnchor: 2.6
  });
  // 오버레이 내용과 스타일 설정
  // 마커 클릭 이벤트 리스너 추가
  kakao.maps.event.addListener(marker, 'click', function() {
    overlay.setContent(content);
    overlay.setPosition(marker.getPosition());
    overlay.setMap(map);
    tmpOverlay.setMap(null);
  });
});
// 사용자의 위치 주변 맛집 표시
if (inputAddress == 'aroundMe') {
  displayMarker_myLocation(locPosition);
} else if (inputAddress !== 'searchKeyword') {
  // 사용자가 입력한 주소에 마커 및 정보창 표시
  var marker = new kakao.maps.Marker({
    map: map,
    position: locPosition
  });
  // 정보창 내용 설정 및 표시
  var infowindow = new kakao.maps.InfoWindow({
    content: '<div style="width:150px;text-align:center;padding:6px 0;">' + inputAddress + '</div>'
  });

  infowindow.open(map, marker);
}
// 지도 중심을 사용자의 위치로 설정
map.setCenter(locPosition);
// 사용자의 현재 위치에 마커 표시 함수
function displayMarker_myLocation(locPosition) {
  // 사용자 위치 마커 이미지 설정 및 생성
  var markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imageOption)
  var marker = new kakao.maps.Marker({
    map: map,
    position: locPosition,
    image: markerImage
  });
}

📌 예약 서비스

  • 가게 상세 페이지에서 예약 가능(등록된 영업시간 내에서만 가능)
  • 노쇼 방지를 위한 전화번호 인증
  • 예약 관리 페이지에서 예약 목록 확인 가능(가게별, 날짜별, 전체 목록)

1. 예약 VIEW

<!-- 예약 날짜 선택 -->
<div class="my-3">
    <label for="datepicker" class="form-label border-bottom">예약 날짜</label>
    <div class="input-group">
        <button type="button" id="datepicker" class="버튼 btn">날짜 선택</button>
        <input type="text" id="selectedDate" class="form-control" readonly>
    </div>
</div>
<!-- 예약 시간 선택 -->
<div class="mb-3">
    <label for="timepicker" class="form-label border-bottom">예약시간</label>
    <div class="input-group">
        <button type="button" id="timepicker" class="버튼 btn">시간 선택</button>
        <input type="text" id="selectedTime" class="form-control" readonly>
    </div>
</div>
<!-- 예약 인원 수 조절 -->
<div class="mb-3">
    <label for="inputCount" class="form-label border-bottom">인원 수</label>
    <div class="btn-group border" style="width:50%;">
        <button type="button" class="버튼 btn" onclick="decreaseCount()">-</button>
        <input type="button" class="btn" id="inputCount" value="1" style="border:1px solid black;">
        <button type="button" class="버튼 btn" onclick="increaseCount()">+</button>
    </div>
</div>
<!-- 전화번호 인증 -->
<div class="mb-3">
    <button type="button" class="버튼 btn btn-sm me-2" data-bs-toggle="modal" data-bs-target="#exampleModal" id="verBtn">
        전화번호 인증
    </button>
    <div class="d-flex justify-content-between align-items-start" style="width:70%;">
        <input type="text" class="form-control" id="verKey" placeholder="인증번호 입력">
        <input type="button" class="버튼 btn" value="확인" onclick="checkVerificationKey()">
    </div>
</div>
<!-- 예약 버튼 -->
<div class="d-flex justify-content-end align-items-center">
    <input id="reserveBtn" type="button" class="버튼 btn" value="예약">
</div>

2. 클릭 이벤트 처리

// 예약 버튼 클릭 이벤트 처리
const reserve = document.getElementById("reserveBtn");
reserve.addEventListener('click', function() {
    if (document.getElementById('selectedDate').value == "")
        alert("예약 날짜를 선택해주세요.");
    else if (document.getElementById('selectedTime').value == "")
        alert("예약 시간을 선택해주세요.");
    else if (document.getElementById('inputCount').value == "")
        alert("인원 수를 선택해주세요.");
    else if (!document.getElementById('verResult').classList.contains('valid-feedback'))
        alert("인증이 완료되지 않았습니다.");
    else {
        // 여기에 예약 처리 로직 추가
    }
});

// 예약 날짜 및 시간 선택기 초기화
document.addEventListener("DOMContentLoaded", function () {
    // 날짜 및 시간 선택기 설정 코드
});

// 인원 수 조절 함수
function increaseCount() { /*...*/ }
function decreaseCount() { /*...*/ }

// 전화번호 인증 처리 함수
function checkVerificationKey() { /*...*/ }

3. 서버

@PreAuthorize("isAuthenticated()")
@PostMapping("/{id}")
public String reserve(@PathVariable("id") Integer id, Principal principal, @RequestParam("date") LocalDate date,
  @RequestParam("time") LocalTime time, @RequestParam("count") int count) {
  SiteUser user = this.siteUserService.getUser(principal.getName());
  Restaurant restaurant = this.restaurantService.getRestaurant(id);
  this.reservationService.reserveRestaurant(user, restaurant, date, time, count);
  return String.format("redirect:/restaurant/detail/%s", id);
}

📌 실시간 문의 서비스

  • 가게 상세 페이지에서 실시간 채팅 선택
  • 채팅방으로 이동
  • 문의한 가게 사장에게 채팅 알림 발생, 채팅방 이동 시 답장 가능

1. 채팅 VIEW

<!-- 채팅 목록 -->
<div class="chatList">
    <h3 class="border-bottom pb-2">채팅 목록</h3>
    <div th:if="${roomList.isEmpty()}" class="p-5 text-center">
        <h3 style="color:gray;">채팅 내역 없음</h3>
    </div>
    <ul class="list-group" th:if="${!roomList.isEmpty()}">
        <li class="list-group-item d-flex justify-content-between align-items-center"
            th:each="chat : ${roomList}" th:classappend="${chat.room == room} ? 'activeRoom'">
            <!-- 채팅방 링크와 삭제 버튼 -->
        </li>
    </ul>
</div>

<!-- 채팅 내용 영역 -->
<div class="chatContent">
    <div class="chatRoom card my-3">
        <!-- 채팅 상대방 헤더 -->
        <div class="card-header p-3 d-flex justify-content-between" style="background:#9FB3CD">
            <h4 th:text="|${restaurant.name} >|" th:if="${user.loginId == customer.loginId}"></h4>
            <h4 th:text="|${target.name} >|" th:if="${user.loginId == owner.loginId}"></h4>
            <!-- 아이콘 버튼들 -->
        </div>
        <!-- 실제 채팅 메시지가 표시되는 부분 -->
        <div class="chatContainer card-body">
            <div th:each="chat : ${chatList}">
                <!-- 각 채팅 메시지 내용 -->
            </div>
            <div id="chatMessages" class="my-2"></div>
        </div>
        <!-- 채팅 메시지 입력 폼 -->
        <form id="messageForm">
            <!-- 입력 필드와 전송 버튼 -->
        </form>
    </div>
</div>

2. 실시간 메시지 송수신 및 사용자 인터페이스 동적 업데이트

$(function () {
    // 새로운 채팅 메시지가 도착했을 때 알림 표시를 업데이트하는 로직
    $('.confirm').each(function(index, element) {
        if ($(element).attr('data-value') == 'false') {
            $(element).show();
        }
    });

    // WebSocket 연결을 설정하는 함수
    var stompClient = null;
    function connect() {
        var socket = new SockJS('/ws'); // SockJS를 사용한 WebSocket 연결
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            // 특정 채팅방 구독
            stompClient.subscribe('/topic/' + $("#room").val(), function (chatMessage) {
                showMessage(JSON.parse(chatMessage.body)); // 메시지 수신 시 처리
            });

            // 알림 구독
            stompClient.subscribe('/topic/room', function (chatMessage) {
                showAlarm(JSON.parse(chatMessage.body)); // 알림 수신 시 처리
            });
        });
    }

    // 새로운 알림을 표시하는 함수
    function showAlarm(data) {
        if (data.room != $('#room').val()) {
            var elementId = '#alarm_' + data.room;
            $(elementId).show();
        }
    }

    // WebSocket 연결을 해제하는 함수
    function disconnect() {
        if (stompClient !== null) {
            stompClient.disconnect();
        }
    }

    // 채팅 메시지를 전송하는 함수
    function sendMessage() {
        var target = $("#target").val();
        var writer = $("#user").val();
        var messageInput = $("#messageInput").val();
        var room = $("#room").val();
        var restaurantId = $("#restaurantId").val();

        // 서버에 메시지 전송
        stompClient.send("/app/chat", {}, JSON.stringify({
            content: messageInput,
            writer: writer,
            target: target,
            room: room,
            restaurant: restaurantId,
            type: 'chat'
        }));
        $("#messageInput").val("");
    }

    // 새로운 채팅 메시지를 화면에 표시하는 함수
    function showMessage(message) {
        var userImage = [[${user.image}]]; // 사용자 이미지
        var targetImage = [[${target.image}]]; // 대상 사용자 이미지
        // 메시지를 화면에 추가하는 로직
    }

    // 메시지 전송 폼 이벤트 핸들러
    $("#messageForm").submit(function (e) {
        e.preventDefault();
        sendMessage();
    });

    // 페이지 로드 시 WebSocket 연결
    connect();

    // 페이지를 벗어날 때 알림 확인 처리
    $(window).on('beforeunload', function(e) {
        // 알림 확인 요청을 서버로 전송
    });
});
post-custom-banner

0개의 댓글