diff --git a/README.md b/README.md index 4e73a72e..9e16f323 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,152 @@ # bw-table-back + ## 개요 -식당 테이블 예약 서비스 +- 프로젝트 명: 흑백테이블 +- 프로젝트 기간 : 2024.10.18 ~ 2024.11.28 (6주) +- 깃허브 주소 : https://github.com/bw-table +- 노션 + 팀링크 : [흑백테이블](https://www.notion.so/123e2b4a200680e5ac90daeffb97d8b8?pvs=21) + +## 팀원 + +| **오신웅** | **이솔** | **박준엽** | **차진아** | **지승연** | +|----------|----------|---------|---------|---------| +| 프론트엔드 팀장 | 팀원 | 백엔드 팀장 | 팀원 | 팀원 | +| Frontend | Frontend | Backend | Backend | Backend | + +### 백엔드 업무 담당 + +| **박준엽** | **차진아** | **지승연** | +|------------------------------------|----------|---------| +| - 회원
-예약
CI/CD 및 배포 환경 구성 | 알림
통계 | 식당 CRUD | + +## 📌 프로젝트 소개 + +### 기획 배경 + +--- + +현대의 외식 환경에서는 고객과 식당 간의 원활한 소통과 신뢰를 바탕으로 한 효율적인 예약 관리가 필수적입니다. +고객은 실시간으로 예약 현황을 확인하고, 맞춤형 서비스를 통해 자신에게 적합한 식당을 쉽게 탐색하기를 원합니다. +사장님들에게는 손쉬운 예약 상태 관리와 더불어 실시간으로 고객 요청을 처리하며 더 나은 서비스를 제공할 수 있는 시스템이 필요합니다. +이러한 요구를 충족하기 위해 흑백테이블 프로젝트를 기획했습니다. -## 회원 -### 공통 -- [ ] 소셜 로그인 및 인증 -### 손님 +### 해결 컨셉 + +--- + +- **실시간 예약 시스템** + - 실시간 예약 현황 확인 + - 방문 여부 및 노쇼 관리 +- **맞춤형 식당 탐색** + - 업종, 메뉴, 해시태그 기반 검색 + - 최근 예약 내역 기반 식당 추천 +- **신뢰할 수 있는 리뷰 시스템** + - 실제 방문 고객의 검증된 후기 +- **원활한 실시간 소통** + - 예약부터 방문까지 1:1 실시간 채팅 + - 특별 요청사항 전달 용이 +- **순차적인 예약 처리** + - 동시성을 제어하여 안전하고 순차적인 예약 처리 + +## 📌 주요 기능 요약 ### 사장님 -## 가게 +**스마트 예약 관리** + +- 실시간 예약 현황 확인 +- 방문 여부 및 노쇼 직접 관리 +- 세부적인 예약 슬롯 설정 및 관리 +- 최근 예약 내역 기반 통계 차트 확인 + +**효율적인 고객 소통** + +- 예약 고객과의 1:1 실시간 채팅 +- 특별 요청사항 확인 및 대응 + +**식당 정보 관리** + +- 프로모션 정보 업데이트 +- 식당을 잘 나타낼 수 있도록 직접 작성한 + 해시태그로 맞춤형 홍보 + +**간편한 예약 처리** + +- 예약과 동시에 이루어지는 예약금 결제 + +### 손님 + +**편리한 예약 시스템** + +- 실시간 예약 가능 시간 확인 +- 순차적인 예약 기회 + +**맞춤형 식당 탐색** + +- 가게이름, 해시태그, 업종, 메뉴로 검색해 맞춤 정보 탐색 +- 업데이트되는 프로모션 정보 확인 + +**신뢰할 수 있는 리뷰 시스템** + +- 실제 방문 고객의 검증된 후기 확인 +- 방문일 3일내에 작성, 작성일 3일 이내에만 수정할 수 있도록 하여 신뢰성 보장 + +**원활한 소통 채널** + +- 예약부터 방문까지 식당과 1:1 실시간 채팅 +- 알러지 정보나 특별 요청사항 쉽게 전달 + +**개인화된 서비스** + +- 과거 예약 및 방문 이력 관리 +- 최근 예약 내역을 기반으로 추천 식당 확인 +- 현재 위치 기반으로 추천 식당 확인 + +## 📌 기술 스택 + +### Frontend + +- Next.js 15 +- tailwindCSS +- zustand +- reat-query +- React-Hook-Form +- storybook +- sockJS +- stomp +- daisyUI +- react-icon +- github action + +### Backend + +- Language : Java 17 +- Framework : Spring Boot +- Build Tool : Gradle +- DB : MySQL , Redis +- Test : JUnit, Postman +- CI/CD: Docker, DockerHub, Github Action +- Auth : JWT, OAuth 2.0 +- JPA +- AWS EC2 +- AWS S3 +- WebSocket +- STOMP +- SSE +- Spring Security +- Spring Batch +- Spring Scheduler -## 예약 +## 프로젝트 구조 -## 해시태그 +--- -## 채팅 +![CleanShot 2024-11-27 at 23.09.22.png](https://prod-files-secure.s3.us-west-2.amazonaws.com/23988a35-eeeb-4174-b959-70dc0574a0cb/39be4891-affd-45d8-aba4-1062ca7f5abd/CleanShot_2024-11-27_at_23.09.22.png) -## 공지 +## ERD -## 알림 +## 개선할 점 -## 통계 +--- \ No newline at end of file diff --git a/src/main/java/com/zero/bwtableback/chat/controller/ChatController.java b/src/main/java/com/zero/bwtableback/chat/controller/ChatController.java index 7e63bd5c..18fbdf95 100644 --- a/src/main/java/com/zero/bwtableback/chat/controller/ChatController.java +++ b/src/main/java/com/zero/bwtableback/chat/controller/ChatController.java @@ -26,9 +26,7 @@ public class ChatController { private final ChatService chatService; - // 채팅방 생성 엔드포인트는 예약 확정 시 자동으로 생성 - - // FIXME 특정 채팅방 조회 (필요 여부 판단) + // 채팅방 조회 @GetMapping("/{chatRoomId}") public ResponseEntity getChatRoomById(@PathVariable Long chatRoomId) { ChatRoom chatRoom = chatService.getChatRoomById(chatRoomId); @@ -51,8 +49,6 @@ public ResponseEntity> getMessages(@PathVariable Long chatRo /** * 메시지 전송 * - * TODO Redis에 캐싱 및 배치 작업 고려 - * TODO 첫 연결 시 메시지 * 임시로 DB에 저장 */ @MessageMapping("/send/{chatRoomId}") diff --git a/src/main/java/com/zero/bwtableback/chat/entity/ChatRoom.java b/src/main/java/com/zero/bwtableback/chat/entity/ChatRoom.java index 6960dc47..94287f1b 100644 --- a/src/main/java/com/zero/bwtableback/chat/entity/ChatRoom.java +++ b/src/main/java/com/zero/bwtableback/chat/entity/ChatRoom.java @@ -4,14 +4,16 @@ import com.zero.bwtableback.reservation.entity.Reservation; import com.zero.bwtableback.restaurant.entity.Restaurant; import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; +import lombok.*; import java.util.List; @Entity @Getter @Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor @Table(name = "chat_room") public class ChatRoom { diff --git a/src/main/java/com/zero/bwtableback/chat/service/ChatService.java b/src/main/java/com/zero/bwtableback/chat/service/ChatService.java index b18dedb8..0e6b1be8 100644 --- a/src/main/java/com/zero/bwtableback/chat/service/ChatService.java +++ b/src/main/java/com/zero/bwtableback/chat/service/ChatService.java @@ -19,14 +19,15 @@ import com.zero.bwtableback.restaurant.entity.Restaurant; import com.zero.bwtableback.restaurant.repository.RestaurantRepository; import com.zero.bwtableback.restaurant.service.RestaurantService; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + @Service @RequiredArgsConstructor public class ChatService { @@ -44,28 +45,29 @@ public class ChatService { * @return 예약 정보, 가게 정보 */ public PaymentCompleteResDto createChatRoom(ReservationResDto reservationResDto) { - // 식당 및 예약 정보 조회 Restaurant restaurant = getRestaurant(reservationResDto.restaurantId()); Reservation reservation = getReservation(reservationResDto.reservationId()); Member member = getMember(reservationResDto.memberId()); - // 채팅방 이름 생성 String roomName = generateRoomName(restaurant.getName(), reservationResDto.reservationDate(), reservationResDto.reservationTime()); - ChatRoom chatRoom = new ChatRoom(); - chatRoom.setRoomName(roomName); - chatRoom.setStatus(ChatRoomStatus.ACTIVE); - chatRoom.setRestaurant(restaurant); - chatRoom.setReservation(reservation); - chatRoom.setMember(member); - + ChatRoom chatRoom = createChatRoomEntity(roomName, restaurant, reservation, member); chatRoomRepository.save(chatRoom); RestaurantDetailDto restaurantDetailDto = restaurantService.getRestaurantById(restaurant.getId()); return PaymentCompleteResDto.fromEntities(restaurantDetailDto, reservation); } - + // 채팅방 객체 생성 + private ChatRoom createChatRoomEntity(String roomName, Restaurant restaurant, Reservation reservation, Member member) { + return ChatRoom.builder() + .roomName(roomName) + .status(ChatRoomStatus.ACTIVE) + .restaurant(restaurant) + .reservation(reservation) + .member(member) + .build(); + } // 식당 조회 private Restaurant getRestaurant(Long restaurantId) { @@ -96,7 +98,7 @@ private String generateRoomName(String restaurantName, LocalDate reservationDate */ public ChatRoom getChatRoomById(Long chatRoomId) { return chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> new RuntimeException("채팅방을 찾을 수 없습니다.")); + .orElseThrow(() -> new CustomException(ErrorCode.CHAT_ROOM_NOT_FOUND)); } /** @@ -107,7 +109,6 @@ public void inactivateChatRoom(Long reservationId) { .orElseThrow(() -> new CustomException(ErrorCode.CHAT_ROOM_NOT_FOUND)); chatRoom.setStatus(ChatRoomStatus.INACTIVE); - chatRoomRepository.save(chatRoom); } @@ -115,9 +116,6 @@ public void inactivateChatRoom(Long reservationId) { * 특정 채팅방 전체 메시지 조회 */ public Page getMessages(Long chatRoomId, Pageable pageable) { - chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> new CustomException(ErrorCode.CHAT_ROOM_NOT_FOUND)); - return messageRepository.findByChatRoomIdOrderByTimestampDesc(chatRoomId, pageable) .map(MessageResDto::fromEntity); } @@ -126,25 +124,31 @@ public Page getMessages(Long chatRoomId, Pageable pageable) { * 특정 채팅방 메시지 전송 */ public MessageResDto saveMessage(Long chatRoomId, String email, MessageReqDto messageReqDto) { - ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> new CustomException(ErrorCode.CHAT_ROOM_NOT_FOUND)); + ChatRoom chatRoom = getChatRoomById(chatRoomId); Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); - Message message = Message.builder() + Message message = createMessageEntity(chatRoom, member, messageReqDto); + messageRepository.save(message); + + return new MessageResDto(member.getNickname(), messageReqDto.getContent(), messageReqDto.getTimestamp()); + } + + // 메시지 객체 생성 + private Message createMessageEntity(ChatRoom chatRoom, Member member, MessageReqDto messageReqDto) { + return Message.builder() .content(messageReqDto.getContent()) .sender(member) .chatRoom(chatRoom) .restaurant(chatRoom.getRestaurant()) .timestamp(messageReqDto.getTimestamp()) .build(); - - messageRepository.save(message); - - return new MessageResDto(member.getNickname(), messageReqDto.getContent(), messageReqDto.getTimestamp()); } + /** + * 채팅방 활성화 여부 확인 + */ public boolean isChatRoomActive(Long chatRoomId) { ChatRoom chatRoom = getChatRoomById(chatRoomId); return ChatRoomStatus.ACTIVE.equals(chatRoom.getStatus()); diff --git a/src/main/java/com/zero/bwtableback/common/exception/ErrorCode.java b/src/main/java/com/zero/bwtableback/common/exception/ErrorCode.java index beab124d..a9ab11d6 100644 --- a/src/main/java/com/zero/bwtableback/common/exception/ErrorCode.java +++ b/src/main/java/com/zero/bwtableback/common/exception/ErrorCode.java @@ -79,6 +79,8 @@ public enum ErrorCode { FILE_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "파일 업로드에 실패했습니다."), FILE_DELETE_FAILED(HttpStatus.BAD_REQUEST, "파일 삭제에 실패하였습니다."), + // REDIS 오류 + REDIS_CONNECTION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR,"Redis에 연결할 수 없습니다."), // 기타 오류 UNAUTHORIZED_ACCESS(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."); diff --git a/src/main/java/com/zero/bwtableback/member/controller/AuthController.java b/src/main/java/com/zero/bwtableback/member/controller/AuthController.java index d0b5ee22..aa1677ff 100644 --- a/src/main/java/com/zero/bwtableback/member/controller/AuthController.java +++ b/src/main/java/com/zero/bwtableback/member/controller/AuthController.java @@ -93,31 +93,27 @@ public ResponseEntity signUp(@Valid @RequestBody SignUpReqDto signU public ResponseEntity login(@RequestBody EmailLoginReqDto loginReqDto, HttpServletRequest request, HttpServletResponse response) { - MemberDto memberDto = authService.authenticateMember(loginReqDto); try { - String accessToken = getJwtFromRequest(request); - - // 액세스 토큰이 유효한 경우 - if (StringUtils.hasText(accessToken) && tokenProvider.validateAccessToken(accessToken)) { - // 기존의 액세스 토큰과 사용자 정보를 반환 - return ResponseEntity.ok(authService.handleExistingToken(accessToken)); + MemberDto authenticatedMember = authService.authenticateMember(loginReqDto); + String accessToken = tokenProvider.extractToken(request); + + if (StringUtils.hasText(accessToken)) { + if (tokenProvider.validateAccessToken(accessToken)) { + // 유효한 토큰이 있는 경우, 기존 토큰 정보 반환 + return ResponseEntity.ok(authService.handleExistingToken(accessToken)); + } else { + // 토큰이 만료된 경우, 401 에러 반환 + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("토큰이 만료되었습니다."); + } + } else { + // 토큰이 없는 경우, 새로운 로그인 프로세스 진행 + return ResponseEntity.ok(authService.login(authenticatedMember, request, response)); } - // 액세스 토큰이 없거나 유효하지 않은 경우, 새로운 로그인 처리 - LoginResDto loginResDto = authService.login(memberDto, request, response); - return ResponseEntity.ok(loginResDto); - } catch (CustomException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("Unauthorized: " + e.getMessage()); - } - } - - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); // "Bearer " 부분을 제거하고 토큰 반환 + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Forbidden: " + e.getMessage()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인 정보가 유효하지 않습니다."); } - return null; // 토큰이 없으면 null 반환 } /** @@ -152,9 +148,8 @@ private String getRefreshTokenFromCookies(HttpServletRequest request) { @PostMapping("/logout") public ResponseEntity logout(@AuthenticationPrincipal MemberDetails memberDetails, HttpServletResponse response) { - String email = memberDetails.getUsername(); - authService.logout(email, response); + authService.logout(memberDetails.getMemberId(), response); return ResponseEntity.ok("로그아웃이 완료되었습니다."); } @@ -168,7 +163,7 @@ public ResponseEntity withdrawMember(@AuthenticationPrincipal MemberDetails m HttpServletResponse response) { authService.withdraw(memberDetails.getMemberId(), response); - return ResponseEntity.ok().body("회원탈퇴가 완료되었습니다."); + return ResponseEntity.ok("회원탈퇴가 완료되었습니다."); } } diff --git a/src/main/java/com/zero/bwtableback/member/controller/MemberController.java b/src/main/java/com/zero/bwtableback/member/controller/MemberController.java index 3eca3e57..a02b2abd 100644 --- a/src/main/java/com/zero/bwtableback/member/controller/MemberController.java +++ b/src/main/java/com/zero/bwtableback/member/controller/MemberController.java @@ -42,7 +42,7 @@ public ResponseEntity> getMembers(Pageable pageable) { */ @GetMapping("/{memberId}") public MemberDto getMemberById(@PathVariable Long memberId) { - return memberService.getMemberById(memberId); + return memberService.getMemberInfo(memberId); } /** @@ -55,8 +55,7 @@ public ResponseEntity getMyInfo(@AuthenticationPrincipal MemberDetails member if (memberDetails == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - Long memberId = memberDetails.getMemberId(); - return ResponseEntity.ok(memberService.getMyInfo(memberId)); + return ResponseEntity.ok(memberService.getMyInfo(memberDetails.getMemberId())); } /** @@ -68,9 +67,7 @@ public ResponseEntity> getMyReservations(Pageable pageab if (memberDetails == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - String email = memberDetails.getUsername(); - - return ResponseEntity.ok(memberService.getMyReservations(pageable, email)); + return ResponseEntity.ok(memberService.getMyReservations(pageable, memberDetails.getMemberId())); } /** @@ -78,9 +75,8 @@ public ResponseEntity> getMyReservations(Pageable pageab */ @GetMapping("/me/reviews") public ResponseEntity> getMyReviews(Pageable pageable, - @AuthenticationPrincipal MemberDetails memberDetails) { - String email = memberDetails.getUsername(); - return ResponseEntity.ok(memberService.getMyReviews(pageable, email)); + @AuthenticationPrincipal MemberDetails memberDetails) {; + return ResponseEntity.ok(memberService.getMyReviews(pageable, memberDetails.getMemberId())); } /** @@ -92,9 +88,7 @@ public ResponseEntity> getMyChats(Pageable pageable, if (memberDetails == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - String email = memberDetails.getUsername(); - - Page rooms = memberService.getMyChatRooms(pageable, email); + Page rooms = memberService.getMyChatRooms(pageable, memberDetails.getMemberId()); return ResponseEntity.ok(rooms); } @@ -145,4 +139,4 @@ public ResponseEntity removeProfileImage(@AuthenticationPrincipal MemberDetai imageUploadService.deleteFileFromDB(memberDetails.getMemberId()); return ResponseEntity.noContent().build(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/zero/bwtableback/member/dto/DuplicateCheckReqDto.java b/src/main/java/com/zero/bwtableback/member/dto/DuplicateCheckReqDto.java index 0d8ef78f..ee8a0482 100644 --- a/src/main/java/com/zero/bwtableback/member/dto/DuplicateCheckReqDto.java +++ b/src/main/java/com/zero/bwtableback/member/dto/DuplicateCheckReqDto.java @@ -1,10 +1,14 @@ package com.zero.bwtableback.member.dto; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder +@NoArgsConstructor +@AllArgsConstructor public class DuplicateCheckReqDto { private String email; private String nickname; diff --git a/src/main/java/com/zero/bwtableback/member/dto/MemberPrivateDto.java b/src/main/java/com/zero/bwtableback/member/dto/MemberPrivateDto.java index 8b99f470..39a8c863 100644 --- a/src/main/java/com/zero/bwtableback/member/dto/MemberPrivateDto.java +++ b/src/main/java/com/zero/bwtableback/member/dto/MemberPrivateDto.java @@ -1,6 +1,7 @@ package com.zero.bwtableback.member.dto; import com.zero.bwtableback.member.entity.LoginType; +import com.zero.bwtableback.member.entity.Member; import com.zero.bwtableback.member.entity.Role; import lombok.AllArgsConstructor; import lombok.Getter; @@ -16,4 +17,18 @@ public MemberPrivateDto(Long id, String email, String name, String nickname, super(id, email, name, nickname, phone, role, profileImage, businessNubmer); this.loginType = loginType; } + + public static MemberPrivateDto from(Member member) { + return new MemberPrivateDto( + member.getId(), + member.getEmail(), + member.getName(), + member.getNickname(), + member.getPhone(), + member.getRole(), + member.getProfileImage(), + member.getBusinessNumber(), + member.getLoginType() + ); + } } diff --git a/src/main/java/com/zero/bwtableback/member/oauth2/controller/KakaoOAuth2Controller.java b/src/main/java/com/zero/bwtableback/member/oauth2/controller/KakaoOAuth2Controller.java index d73588ac..4f87496f 100644 --- a/src/main/java/com/zero/bwtableback/member/oauth2/controller/KakaoOAuth2Controller.java +++ b/src/main/java/com/zero/bwtableback/member/oauth2/controller/KakaoOAuth2Controller.java @@ -41,9 +41,6 @@ public class KakaoOAuth2Controller { public ResponseEntity kakaoLogin(@RequestParam(required = false) String code, HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException { - // 요청 헤더에서 액세스 토큰 추출 - String accessToken = getJwtFromRequest(request); - // 첫 번째 로그인: 카카오에서 정보를 추출하여 서버에 회원가입 if (code != null) { String kakaoToken = kakaoService.getAccessToken(code); @@ -52,24 +49,23 @@ public ResponseEntity kakaoLogin(@RequestParam(required = false) String code, return ResponseEntity.ok(loginResDto); } else { - // 액세스 토큰 존재하고 유효한 경우 - if (StringUtils.hasText(accessToken) && tokenProvider.validateAccessToken(accessToken)) { - // 기존의 액세스 토큰과 사용자 정보를 반환 - LoginResDto loginResDto = authService.handleExistingToken(accessToken); - - return ResponseEntity.ok(loginResDto); + // 카카오 인가 코드가 없는 경우 + String accessToken = tokenProvider.extractToken(request); + if (StringUtils.hasText(accessToken)) { + if (tokenProvider.validateAccessToken(accessToken)) { + // 유효한 액세스 토큰이 있는 경우 + LoginResDto loginResDto = authService.handleExistingToken(accessToken); + return ResponseEntity.ok(loginResDto); + } else { + // 액세스 토큰이 만료된 경우 + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body("토큰이 만료되었습니다."); + } + } else { + // 액세스 토큰이 없는 경우 + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body("토큰이 존재하지 않습니다."); } - // 토큰이 없거나 유효하지 않은 경우 401 응답을 던짐 - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("Unauthorized. 토큰이 유효하지 않습니다."); - } - } - - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); // "Bearer " 부분을 제거하고 토큰 반환 } - return null; // 토큰이 없으면 null 반환 } } \ No newline at end of file diff --git a/src/main/java/com/zero/bwtableback/member/oauth2/service/KakaoOAuth2Service.java b/src/main/java/com/zero/bwtableback/member/oauth2/service/KakaoOAuth2Service.java index 0d769f7c..bdeb286a 100644 --- a/src/main/java/com/zero/bwtableback/member/oauth2/service/KakaoOAuth2Service.java +++ b/src/main/java/com/zero/bwtableback/member/oauth2/service/KakaoOAuth2Service.java @@ -139,6 +139,7 @@ private MemberDto registerNewMember(KakaoUserInfoDto userInfo) { .nickname(userInfo.getNickName()) .phone(userInfo.getPhone()) .role(Role.GUEST) + .status(Status.ACTIVE) .provider(userInfo.getProvider()) .providerId(userInfo.getProviderId()) .profileImage(userInfo.getProfileImage()) diff --git a/src/main/java/com/zero/bwtableback/member/service/AuthService.java b/src/main/java/com/zero/bwtableback/member/service/AuthService.java index e103c8e8..817163de 100644 --- a/src/main/java/com/zero/bwtableback/member/service/AuthService.java +++ b/src/main/java/com/zero/bwtableback/member/service/AuthService.java @@ -123,26 +123,27 @@ public LoginResDto signUpLogin(MemberDto memberDto, HttpServletRequest request, * 로그인 */ public LoginResDto login(MemberDto memberDto, HttpServletRequest request, HttpServletResponse response) { + try { + String accessToken = tokenProvider.createAccessToken(memberDto.getEmail(), memberDto.getRole()); + String refreshToken = tokenProvider.createRefreshToken(memberDto.getId().toString()); - String accessToken = tokenProvider.createAccessToken(memberDto.getEmail(), memberDto.getRole()); - String refreshToken = tokenProvider.createRefreshToken(memberDto.getId().toString()); - - // 회원 상태 조회 - - // HttpOnly 쿠키에 리프레시 토큰 저장 - saveRefreshTokenToCookie(refreshToken, response); - - // Redis에 리프레시 토큰 저장 - saveRefreshTokenToRedis(memberDto.getId(), refreshToken); + // HttpOnly 쿠키에 리프레시 토큰 저장 + saveRefreshTokenToCookie(refreshToken, response); - // 레스토랑 ID 조회 (사장님일 경우) - Long restaurantId = getRestaurantIdIfOwner(memberDto); + // Redis에 리프레시 토큰 저장 + saveRefreshTokenToRedis(memberDto.getId(), refreshToken); + // 레스토랑 ID 조회 (사장님일 경우) + Long restaurantId = getRestaurantIdIfOwner(memberDto); - return new LoginResDto(accessToken, memberDto, restaurantId); + return new LoginResDto(accessToken, memberDto, restaurantId); + } catch (Exception e) { + log.error("로그인 실패: {}", memberDto.getEmail(), e); + throw new IllegalArgumentException("로그인 실패", e); + } } - // 회원 인증 + // 이메일과 비밀번호 검증 public MemberDto authenticateMember(EmailLoginReqDto loginReqDto) { Member member = memberRepository.findByEmail(loginReqDto.getEmail()) .orElseThrow(() -> new CustomException(ErrorCode.INVALID_CREDENTIALS)); @@ -162,7 +163,6 @@ public MemberDto authenticateMember(EmailLoginReqDto loginReqDto) { // 리프레시 토큰으로 액세스 토큰 갱신 public LoginResDto renewAccessTokenWithRefreshToken(String refreshToken) { String email = tokenProvider.getUsername(refreshToken); - Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); @@ -178,7 +178,6 @@ public LoginResDto renewAccessTokenWithRefreshToken(String refreshToken) { // 리프레시 토큰 검증 public void validateRefreshToken(String refreshToken, Long memberId) { String key = "refresh_token:" + memberId; - String storedRefreshToken = redisTemplate.opsForValue().get(key); if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) { @@ -202,10 +201,9 @@ public void saveRefreshTokenToRedis(Long memberId, String refreshToken) { try { redisTemplate.opsForValue().set(key, refreshToken); } catch (RedisConnectionFailureException e) { - System.err.println("Redis에 연결할 수 없습니다: " + e.getMessage()); + throw new CustomException(ErrorCode.REDIS_CONNECTION_FAILURE); } catch (Exception e) { - // 다른 예외 처리 - System.err.println("예기치 않은 오류 발생: " + e.getMessage()); + throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); } } @@ -234,9 +232,8 @@ private Long getRestaurantIdIfOwner(MemberDto member) { /** * 사용자 로그아웃 처리 */ - public void logout(String email, HttpServletResponse response) { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + public void logout(Long memberId, HttpServletResponse response) { + Member member = getMemberById(memberId); String key = "refresh_token:" + member.getId(); redisTemplate.delete(key); @@ -255,8 +252,7 @@ public void logout(String email, HttpServletResponse response) { * 로그아웃 처리 후 */ public void withdraw(Long memberId, HttpServletResponse response) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + Member member = getMemberById(memberId); String key = "refresh_token:" + member.getId(); redisTemplate.delete(key); @@ -274,4 +270,9 @@ public void withdraw(Long memberId, HttpServletResponse response) { member.setStatus(Status.INACTIVE); memberRepository.save(member); } + + private Member getMemberById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } } \ No newline at end of file diff --git a/src/main/java/com/zero/bwtableback/member/service/MemberService.java b/src/main/java/com/zero/bwtableback/member/service/MemberService.java index 6be7cde9..0e1e90d4 100644 --- a/src/main/java/com/zero/bwtableback/member/service/MemberService.java +++ b/src/main/java/com/zero/bwtableback/member/service/MemberService.java @@ -32,73 +32,54 @@ public class MemberService { */ public Page getMembers(Pageable pageable) { return memberRepository.findAll(pageable) - .map(this::convertToDto); + .map(MemberDto::from); } /** - * 단일 회원 정보 조회 + * 회원 정보 조회 */ - public MemberDto getMemberById(Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - return convertToDto(member); + public MemberDto getMemberInfo(Long memberId) { + Member member = getMemberById(memberId); + return MemberDto.from(member); } /** * 본인 정보 조회 */ public MemberPrivateDto getMyInfo(Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - return convertToPrivateDto(member); - } - - private MemberDto convertToDto(Member member) { - return new MemberDto( - member.getId(), - member.getEmail(), - member.getName(), - member.getNickname(), - member.getPhone(), - member.getRole(), - member.getProfileImage(), - member.getBusinessNumber()); - } - - private MemberPrivateDto convertToPrivateDto(Member member) { - return new MemberPrivateDto( - member.getId(), - member.getEmail(), - member.getName(), - member.getNickname(), - member.getPhone(), - member.getRole(), - member.getProfileImage(), - member.getBusinessNumber(), - member.getLoginType()); + Member member = getMemberById(memberId); + return MemberPrivateDto.from(member); } - public Page getMyReservations(Pageable pageable, String email) { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - + /** + * 나의 예약 목록 조회 + */ + public Page getMyReservations(Pageable pageable, Long memberId) { + Member member = getMemberById(memberId); return reservationRepository.findByMemberId(member.getId(), pageable) .map(ReservationResDto::fromEntity); } - public Page getMyReviews(Pageable pageable, String email) { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - + /** + * 나의 리뷰 목록 조회 + */ + public Page getMyReviews(Pageable pageable, Long memberId) { + Member member = getMemberById(memberId); return reviewRepository.findByMemberIdOrderByRestaurantId(member.getId(), pageable) .map(ReviewDetailDto::fromEntity); } - public Page getMyChatRooms(Pageable pageable, String email) { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - + /** + * 나의 채팅방 조회 + */ + public Page getMyChatRooms(Pageable pageable, Long memberId) { + Member member = getMemberById(memberId); return chatRoomRepository.findChatRoomsByMemberIdOrderByLastMessageTime(member.getId(), pageable) .map(ChatRoomCreateResDto::fromEntity); } + + private Member getMemberById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } } \ No newline at end of file diff --git a/src/main/java/com/zero/bwtableback/reservation/controller/ReservationController.java b/src/main/java/com/zero/bwtableback/reservation/controller/ReservationController.java index a2eebfc0..c4c82a4d 100644 --- a/src/main/java/com/zero/bwtableback/reservation/controller/ReservationController.java +++ b/src/main/java/com/zero/bwtableback/reservation/controller/ReservationController.java @@ -61,8 +61,7 @@ public ReservationResDto getReservationById(@PathVariable Long reservationId) { * 4. 생성된 예약 토큰을 클라이언트에 반환합니다. */ @PostMapping() - public ResponseEntity requestReservation(@RequestBody ReservationCreateReqDto request, - @AuthenticationPrincipal MemberDetails memberDetails) { + public ResponseEntity requestReservation(@RequestBody ReservationCreateReqDto request) { ReservationAvailabilityDto availability = reservationService.checkReservationAvailability(request); if (availability.isAvailable()) { String reservationToken = UUID.randomUUID().toString(); @@ -79,7 +78,7 @@ public ResponseEntity requestReservation(@RequestBody ReservationCreateReqDto * 1. 결제 정보와 함께 요청이 들어오면, 해당 예약 정보를 조회 * 2. 결제가 성공적으로 완료되면, 분산 락을 사용하여 동시성 제어 * 3. 현재 예약된 인원 수를 확인하고, 최대 결제 인원을 초과하지 않는 경우에만 예약을 확정하고 DB에 저장 - * 4. 채팅방을 생성하고 Redis에서 임시 예약 정보를 삭제 + * 4. 채팅방을 생성하고 결제 정보를 저장 * * @return 결제 완료 페이지에 보여질 정보 반환 */ diff --git a/src/main/java/com/zero/bwtableback/reservation/service/ReservationService.java b/src/main/java/com/zero/bwtableback/reservation/service/ReservationService.java index 9dbbdbb2..140110d2 100644 --- a/src/main/java/com/zero/bwtableback/reservation/service/ReservationService.java +++ b/src/main/java/com/zero/bwtableback/reservation/service/ReservationService.java @@ -88,17 +88,15 @@ public ReservationAvailabilityDto checkReservationAvailability(ReservationCreate restaurantRepository.findById(request.restaurantId()) .orElseThrow(() -> new CustomException(ErrorCode.RESTAURANT_NOT_FOUND)); - // 예약 기간 설정 확인 ReservationSetting reservationSetting = findReservationSetting(request); - // 요일 설정 확인 WeekdaySetting weekdaySetting = findWeekdaySetting(reservationSetting, request.reservationDate()); - // 시간대 설정 확인 TimeslotSetting timeslotSetting = findTimeslotSetting(weekdaySetting, request.reservationTime()); String currentCountKey = String.format("reservation:currentCount:%d:%s:%s", request.restaurantId(), request.reservationDate(), request.reservationTime()); + if (integerRedisTemplate.opsForValue().get(currentCountKey) == null) { integerRedisTemplate.opsForValue().set(currentCountKey, timeslotSetting.getMaxCapacity()); } diff --git a/src/main/java/com/zero/bwtableback/restaurant/controller/MainController.java b/src/main/java/com/zero/bwtableback/restaurant/controller/MainController.java index 3cfbdc7c..c743c1e5 100644 --- a/src/main/java/com/zero/bwtableback/restaurant/controller/MainController.java +++ b/src/main/java/com/zero/bwtableback/restaurant/controller/MainController.java @@ -97,7 +97,6 @@ public ResponseEntity> getRestaurantsNearby( @RequestParam double latitude, @RequestParam double longitude, @RequestParam double radius) { - List restaurants = mainService.getRestaurantsNearby(latitude, longitude, radius); return ResponseEntity.ok(restaurants); @@ -113,12 +112,13 @@ public ResponseEntity> getRestaurantsByRegion(@RequestPa // [놓치면 안되는 혜택 가득, 방문자 리얼리뷰 pick, 고객님이 좋아할 매장, 새로 오픈했어요!] 리스트 @GetMapping - public ResponseEntity>> getMainPageData( - Pageable pageable, @AuthenticationPrincipal MemberDetails memberDetails) { + public ResponseEntity>> getMainPageData(Pageable pageable, + @AuthenticationPrincipal MemberDetails memberDetails) { Long memberId = (memberDetails != null) ? memberDetails.getMemberId() : null; Map> mainPageData = mainService.getMainPageData(pageable, memberId); + return ResponseEntity.ok(mainPageData); } } diff --git a/src/main/java/com/zero/bwtableback/restaurant/entity/Restaurant.java b/src/main/java/com/zero/bwtableback/restaurant/entity/Restaurant.java index 4e7b4236..dc6637b1 100644 --- a/src/main/java/com/zero/bwtableback/restaurant/entity/Restaurant.java +++ b/src/main/java/com/zero/bwtableback/restaurant/entity/Restaurant.java @@ -86,7 +86,7 @@ public class Restaurant extends BaseEntity { ) private List hashtags; - @Column(nullable = false) // TODO: 평점 null 허용? + @Column(nullable = false) private double averageRating; // 평균 평점 @OneToOne diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 205e8be5..88ec86b8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,7 @@ spring: activate: on-profile: prod multipart: + enabled: true max-file-size: 5MB max-request-size: 10MB iamport: diff --git a/src/test/java/com/zero/bwtableback/member/servcie/AuthServiceTest.java b/src/test/java/com/zero/bwtableback/member/servcie/AuthServiceTest.java index 56f7efda..1914b7fa 100644 --- a/src/test/java/com/zero/bwtableback/member/servcie/AuthServiceTest.java +++ b/src/test/java/com/zero/bwtableback/member/servcie/AuthServiceTest.java @@ -361,10 +361,10 @@ void logoutTest() { HttpServletRequest request = mock(HttpServletRequest.class); HttpServletResponse response = mock(HttpServletResponse.class); - when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); // when - authService.logout(email, request, response); + authService.logout(memberId, response); // then ArgumentCaptor cookieCaptor = ArgumentCaptor.forClass(Cookie.class); // HttpServletResponse에 추가된 쿠키 캡처 diff --git a/src/test/java/com/zero/bwtableback/member/servcie/MemberServiceTest.java b/src/test/java/com/zero/bwtableback/member/servcie/MemberServiceTest.java index 0d460b9c..82c50a48 100644 --- a/src/test/java/com/zero/bwtableback/member/servcie/MemberServiceTest.java +++ b/src/test/java/com/zero/bwtableback/member/servcie/MemberServiceTest.java @@ -15,7 +15,7 @@ import com.zero.bwtableback.reservation.entity.Reservation; import com.zero.bwtableback.reservation.entity.ReservationStatus; import com.zero.bwtableback.reservation.repository.ReservationRepository; -import com.zero.bwtableback.restaurant.dto.ReviewInfoDto; +import com.zero.bwtableback.restaurant.dto.ReviewDetailDto; import com.zero.bwtableback.restaurant.entity.Restaurant; import com.zero.bwtableback.restaurant.entity.Review; import com.zero.bwtableback.restaurant.repository.ReviewRepository; @@ -99,7 +99,7 @@ void testGetMemberById_Success() { when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); // when - MemberDto result = memberService.getMemberById(1L); + MemberDto result = memberService.getMemberInfo(1L); System.out.println(result.getName()); // then @@ -115,7 +115,7 @@ public void testGetMemberById_UserNotFound() { // when & then CustomException exception = assertThrows(CustomException.class, () -> { - memberService.getMemberById(1L); + memberService.getMemberInfo(1L); }); assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); } @@ -168,7 +168,7 @@ void testGetMyReservations_Success() { when(reservationRepository.findByMemberId(member.getId(), pageable)).thenReturn(pageReservations); // when - Page result = memberService.getMyReservations(pageable, member.getEmail()); + Page result = memberService.getMyReservations(pageable, member.getId()); // then assertNotNull(result); @@ -187,7 +187,7 @@ void testGetMyReviews_Success() { when(reviewRepository.findByMemberIdOrderByRestaurantId(member.getId(), pageable)).thenReturn(new PageImpl<>(reviews)); // when - Page result = memberService.getMyReviews(pageable, member.getEmail()); + Page result = memberService.getMyReviews(pageable, member.getId()); // then assertNotNull(result); @@ -201,11 +201,11 @@ void testGetMyChatRooms_Success() { Pageable pageable = PageRequest.of(0, 10); List chatRooms = new ArrayList<>(); // Mock ChatRoomCreateResDto 객체 추가 - when(memberRepository.findByEmail(member.getEmail())).thenReturn(Optional.of(member)); + when(memberRepository.findById(member.getId())).thenReturn(Optional.of(member)); when(chatRoomRepository.findChatRoomsByMemberIdOrderByLastMessageTime(member.getId(), pageable)).thenReturn(new PageImpl<>(chatRooms)); // when - Page result = memberService.getMyChatRooms(pageable, member.getEmail()); + Page result = memberService.getMyChatRooms(pageable, member.getId()); // then assertNotNull(result);