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
-## 예약
+## 프로젝트 구조
-## 해시태그
+---
-## 채팅
+
-## 공지
+## 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