From 05735acf79427f74a100989c4ec1fa9354a1f014 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Tue, 1 Jul 2025 23:16:52 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20swagger.config=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application-config/build.gradle | 2 ++ .../nowait/config/config/SwaggerConfig.java | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java diff --git a/application-config/build.gradle b/application-config/build.gradle index a6b89a70..788a7e3f 100644 --- a/application-config/build.gradle +++ b/application-config/build.gradle @@ -11,4 +11,6 @@ repositories { dependencies { api 'org.springframework.boot:spring-boot-starter-web' + // SWAGGER + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' } diff --git a/application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java b/application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java new file mode 100644 index 00000000..7c6454c0 --- /dev/null +++ b/application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java @@ -0,0 +1,30 @@ +package com.nowait.config.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .addSecurityItem(new SecurityRequirement().addList("JWT")) + .components(new Components() + .addSecuritySchemes("JWT", new SecurityScheme() + .name("Authorization") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER))) + .info(new Info().title("NOWAIT API") + .description("NOWAIT API Specification") + .version("v0.0.1")); + } +} From c4efe359a6980c20e11b3eae40672d5841323541 Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Wed, 2 Jul 2025 20:47:43 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat(Order):=20order=20=ED=8B=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicationadmin/order/controller/OrderController.java | 5 +++++ .../applicationuser/order/controller/OrderController.java | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java index 2adb243b..65c815c6 100644 --- a/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java +++ b/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java @@ -1,5 +1,6 @@ package com.nowait.applicationadmin.order.controller; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -11,5 +12,9 @@ @RequestMapping("admin//orders") @RequiredArgsConstructor public class OrderController { + @GetMapping + public void getOrders() { + //TODO + } } diff --git a/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java b/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java index 87cb3fe0..1c5f6df1 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java +++ b/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java @@ -57,7 +57,6 @@ public ResponseEntity getOrderItems( @PathVariable Long tableId, HttpSession session ) { - // 세션ID 추출 (Spring이 세션 자동 관리) String sessionId = session.getId(); List orderItems = orderService.getOrderItems(storeId, tableId, sessionId); From a186e7835eb795ecd5cb2b0d0af6f23296a1755d Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Wed, 2 Jul 2025 21:37:49 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat(Order):=20order=20=ED=8B=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicationadmin/order/controller/OrderController.java | 2 ++ .../applicationuser/order/controller/OrderController.java | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java index 65c815c6..c1a25862 100644 --- a/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java +++ b/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java @@ -15,6 +15,8 @@ public class OrderController { @GetMapping public void getOrders() { //TODO + } + } diff --git a/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java b/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java index 1c5f6df1..43be357d 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java +++ b/application-user/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java @@ -58,7 +58,6 @@ public ResponseEntity getOrderItems( HttpSession session ) { String sessionId = session.getId(); - List orderItems = orderService.getOrderItems(storeId, tableId, sessionId); return ResponseEntity. status(HttpStatus.OK) From 2399095e7e5ffd0589fa7111a28b35aee43fac91 Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Wed, 2 Jul 2025 22:10:22 +0900 Subject: [PATCH 04/12] =?UTF-8?q?refactor(Oauth):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=9B=84=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20=EC=A3=BC=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/OAuth2LoginSuccessHandler.java | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/external-oauth/src/main/java/com/nowait/externaloauth/oauth2/OAuth2LoginSuccessHandler.java b/external-oauth/src/main/java/com/nowait/externaloauth/oauth2/OAuth2LoginSuccessHandler.java index ea397a72..68206c61 100644 --- a/external-oauth/src/main/java/com/nowait/externaloauth/oauth2/OAuth2LoginSuccessHandler.java +++ b/external-oauth/src/main/java/com/nowait/externaloauth/oauth2/OAuth2LoginSuccessHandler.java @@ -19,6 +19,7 @@ import com.nowait.externaloauth.jwt.JwtUtil; import com.nowait.user.entity.User; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -40,42 +41,32 @@ public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHan public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - // 1. CustomOAuth2UserService에서 설정한 OAuth2User 정보 가져오기 - CustomOAuth2User customUserDetails = (CustomOAuth2User)authentication.getPrincipal(); - + CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal(); User user = customUserDetails.getUser(); Long userId = customUserDetails.getUserId(); - String email = customUserDetails.getName(); - - Collection authorities = authentication.getAuthorities(); - Iterator iterator = authorities.iterator(); - GrantedAuthority auth = iterator.next(); - - String role = auth.getAuthority(); + String role = authentication.getAuthorities().iterator().next().getAuthority(); - log.info("user, userId, email, role :: {} {} {} {}", user, userId, email, role); + // JWT 발급 + String accessToken = jwtUtil.createAccessToken("accessToken", userId, role, 30 * 60 * 1000L); // 30분 + String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, 30L * 24 * 60 * 60 * 1000L); // 30일 - // 2. 1)의 사용자 정보를 담아, accessToken과 refreshToken 발행 - String accessToken = jwtUtil.createAccessToken("accessToken", userId, role, 30 * 60 * 1000L); // 유효기간 30분 - String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, - 30 * 24 * 60 * 60 * 1000L); // 유효기간 30일 - - // 3. refreshToken을 DB에 저장 + // 1. refreshToken을 DB에 저장 Token refreshTokenEntity = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30)); tokenRepository.save(refreshTokenEntity); - // 4. JSON 응답으로, accessToken과 refreshToken 을 반환해준다. - response.setContentType("application/json"); - response.setCharacterEncoding("utf-8"); + // 2. refreshToken을 HttpOnly 쿠키로 설정 + Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken); + refreshTokenCookie.setHttpOnly(true); // JS 접근 불가 + refreshTokenCookie.setSecure(false); // 운영환경 https라면 true로 변경 필요 + refreshTokenCookie.setPath("/"); + refreshTokenCookie.setMaxAge(30 * 24 * 60 * 60); // 30일 + response.addCookie(refreshTokenCookie); + response.addHeader("Set-Cookie", response.getHeader("Set-Cookie") + "; SameSite=Lax"); + - ObjectMapper objectMapper = new ObjectMapper(); // 객체 -> json 문자열로 변환 - String body = objectMapper.writeValueAsString( - Map.of( - "accessToken", accessToken, - "refreshToken", refreshToken - ) - ); - response.getWriter().write(body); + // 3. 프론트엔드로 리다이렉트 (accessToken만 쿼리로 전달) + String targetUrl = "http://localhost:5173/login/success?accessToken=" + accessToken; + response.sendRedirect(targetUrl); } } From 6e01b5903dfd26acb8ffb00d6dbe9ba84807b9c7 Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Thu, 3 Jul 2025 17:31:41 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat(user):=20=EC=8A=88=ED=8D=BC=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SUPER_ADMIN 추가 - git 추적에 빠졌던 파일 git 추적하도록 추가 --- ...4\353\223\234-\354\235\264\354\212\210.md" | 14 ++ .../example/apiuser/ApiUserApplication.java | 39 ++++ .../controller/BookmarkController.java | 67 +++++++ .../bookmark/dto/BookmarkCreateResponse.java | 24 +++ .../bookmark/dto/BookmarkGetResponse.java | 24 +++ .../bookmark/service/BookmarkService.java | 78 ++++++++ .../exception/GlobalExceptionHandler.java | 170 ++++++++++++++++++ .../menu/controller/MenuController.java | 32 ++++ .../menu/dto/MenuImageUploadResponse.java | 20 +++ .../example/apiuser/menu/dto/MenuReadDto.java | 32 ++++ .../apiuser/menu/dto/MenuReadResponse.java | 21 +++ .../apiuser/menu/service/MenuService.java | 42 +++++ .../order/controller/OrderController.java | 70 ++++++++ .../apiuser/order/dto/CartItemDto.java | 11 ++ .../order/dto/OrderCreateRequestDto.java | 15 ++ .../order/dto/OrderCreateResponseDto.java | 28 +++ .../dto/OrderItemListGetResponseDto.java | 29 +++ .../order/dto/OrderItemResponseDTO.java | 16 ++ .../apiuser/order/service/OrderService.java | 128 +++++++++++++ .../controller/ReservationController.java | 47 +++++ .../dto/ReservationCreateRequestDto.java | 10 ++ .../dto/ReservationCreateResponseDto.java | 17 ++ .../ReservationNotFoundException.java | 9 + .../service/ReservationService.java | 58 ++++++ .../store/controller/StoreController.java | 56 ++++++ .../store/dto/StoreImageUploadResponse.java | 22 +++ .../apiuser/store/dto/StoreReadDto.java | 39 ++++ .../apiuser/store/dto/StoreReadResponse.java | 23 +++ .../apiuser/store/service/StoreService.java | 16 ++ .../store/service/StoreServiceImpl.java | 73 ++++++++ .../token/controller/TokenController.java | 59 ++++++ .../token/dto/AuthenticationResponse.java | 18 ++ .../token/dto/RefreshTokenRequest.java | 12 ++ .../apiuser/token/service/TokenService.java | 66 +++++++ .../user/serivce/UserService.java | 8 +- .../applicationconfig/config/AsyncConfig.java | 21 +++ .../applicationconfig/config/CorsConfig.java | 28 +++ .../token/controller/TokenController.java | 7 +- .../java/com/nowait/common/enums/Role.java | 3 +- .../com/example/domaintoken/entity/Token.java | 54 ++++++ .../exception/BusinessException.java | 16 ++ .../repository/TokenRepository.java | 11 ++ .../infrastorage/config/AwsS3Config.java | 33 ++++ .../example/infrastorage/s3/S3Service.java | 57 ++++++ .../com/nowait/auth/config/CorsConfig.java | 28 +++ .../nowait/auth/config/SecurityConfig.java | 93 ++++++++++ .../com/nowait/auth/dto/CustomOAuth2User.java | 60 +++++++ .../com/nowait/auth/dto/KaKaoResponse.java | 40 +++++ .../com/nowait/auth/dto/OAuth2Response.java | 19 ++ .../auth/jwt/JwtAuthorizationFilter.java | 93 ++++++++++ .../java/com/nowait/auth/jwt/JwtUtil.java | 76 ++++++++ .../auth/oauth2/CustomOAuth2UserService.java | 70 ++++++++ .../oauth2/OAuth2LoginSuccessHandler.java | 80 +++++++++ .../auth/service/CustomUserDetailService.java | 30 ++++ .../nowait/exception/BusinessException.java | 14 ++ .../com/nowait/exception/ErrorMessage.java | 22 +++ .../com/nowait/exception/ErrorResponse.java | 26 +++ .../exception/GlobalExceptionHandler.java | 108 +++++++++++ .../RefreshTokenNotFoundException.java | 9 + .../exception/ResourceNotFoundException.java | 14 ++ .../exception/TokenBadRequestException.java | 8 + .../exception/UnauthorizedException.java | 19 ++ 62 files changed, 2429 insertions(+), 3 deletions(-) create mode 100644 ".github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" create mode 100644 api-user/src/main/java/com/example/apiuser/ApiUserApplication.java create mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java create mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java create mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java create mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java create mode 100644 api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java create mode 100644 api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java create mode 100644 api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java create mode 100644 api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java create mode 100644 api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java create mode 100644 api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java create mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java create mode 100644 api-user/src/main/java/com/example/apiuser/order/service/OrderService.java create mode 100644 api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java create mode 100644 api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java create mode 100644 api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java create mode 100644 api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java create mode 100644 api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java create mode 100644 api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java create mode 100644 api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java create mode 100644 api-user/src/main/java/com/example/apiuser/store/service/StoreService.java create mode 100644 api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java create mode 100644 api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java create mode 100644 api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java create mode 100644 api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java create mode 100644 api-user/src/main/java/com/example/apiuser/token/service/TokenService.java create mode 100644 application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java create mode 100644 application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java create mode 100644 domain-token/src/main/java/com/example/domaintoken/entity/Token.java create mode 100644 domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java create mode 100644 domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java create mode 100644 infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java create mode 100644 infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java create mode 100644 security-front/src/main/java/com/nowait/auth/config/CorsConfig.java create mode 100644 security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java create mode 100644 security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java create mode 100644 security-front/src/main/java/com/nowait/auth/dto/KaKaoResponse.java create mode 100644 security-front/src/main/java/com/nowait/auth/dto/OAuth2Response.java create mode 100644 security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java create mode 100644 security-front/src/main/java/com/nowait/auth/jwt/JwtUtil.java create mode 100644 security-front/src/main/java/com/nowait/auth/oauth2/CustomOAuth2UserService.java create mode 100644 security-front/src/main/java/com/nowait/auth/oauth2/OAuth2LoginSuccessHandler.java create mode 100644 security-front/src/main/java/com/nowait/auth/service/CustomUserDetailService.java create mode 100644 security-front/src/main/java/com/nowait/exception/BusinessException.java create mode 100644 security-front/src/main/java/com/nowait/exception/ErrorMessage.java create mode 100644 security-front/src/main/java/com/nowait/exception/ErrorResponse.java create mode 100644 security-front/src/main/java/com/nowait/exception/GlobalExceptionHandler.java create mode 100644 security-front/src/main/java/com/nowait/exception/RefreshTokenNotFoundException.java create mode 100644 security-front/src/main/java/com/nowait/exception/ResourceNotFoundException.java create mode 100644 security-front/src/main/java/com/nowait/exception/TokenBadRequestException.java create mode 100644 security-front/src/main/java/com/nowait/exception/UnauthorizedException.java diff --git "a/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" "b/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" new file mode 100644 index 00000000..f728915f --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" @@ -0,0 +1,14 @@ +--- +name: 백엔드 이슈 +about: 백엔드와 관련된 이슈 +title: "[백엔드] " +labels: Backend +assignees: '' + +--- + +# 이슈 내용 + + +# 작업 목록 +- [ ] diff --git a/api-user/src/main/java/com/example/apiuser/ApiUserApplication.java b/api-user/src/main/java/com/example/apiuser/ApiUserApplication.java new file mode 100644 index 00000000..fcaf441e --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/ApiUserApplication.java @@ -0,0 +1,39 @@ +package com.example.apiuser; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@EnableJpaAuditing +@SpringBootApplication(scanBasePackages = { + "com.example.apiuser", + "com.nowait.auth" +}) +@EntityScan(basePackages = { + "com.example.menu.entity", // domain-menu + "com.example.domainstore.entity", // domain-store + "com.example.domaintoken.entity", + "com.nowaiting.user.entity", + "com.nowait.domainbookmark.entity", + "com.nowait.domainreservation.entity", + "com.nowait.domainorder.entity", + "com.nowait.domainorder.entity" +}) +@EnableJpaRepositories(basePackages = { + "com.example.menu.repository", + "com.nowaiting.user.repository", + "com.example.domainstore.repository", + "com.example.domaintoken.repository", + "com.nowait.domainbookmark.repository", + "com.nowait.domainorder.repository", + "com.nowait.domainreservation.repository" +}) +public class ApiUserApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiUserApplication.class, args); + } + +} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java b/api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java new file mode 100644 index 00000000..32c4132a --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java @@ -0,0 +1,67 @@ +package com.example.apiuser.bookmark.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.apiuser.bookmark.dto.BookmarkCreateResponse; +import com.example.apiuser.bookmark.service.BookmarkService; +import com.nowait.auth.dto.CustomOAuth2User; +import com.nowaiting.common.api.ApiUtils; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +@Tag(name = "Bookmark API", description = "북마크 API") +@RestController +@RequestMapping("/bookmarks") +@RequiredArgsConstructor +public class BookmarkController { + private final BookmarkService bookmarkService; + + @PostMapping("/{storeId}") + @Operation(summary = "북마크 생성", description = "특정 주점에 대한 북마크 생성") + @ApiResponse(responseCode = "201", description = "북마크 생성") + public ResponseEntity createBookmark(@PathVariable Long storeId,@AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + BookmarkCreateResponse response = bookmarkService.createBookmark(storeId,customOAuth2User); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body( + ApiUtils.success( + response + ) + ); + } + @GetMapping + @Operation(summary = "북마크 조회", description = "내가 북마크한 주점 조회") + @ApiResponse(responseCode = "200", description = "북마크 조회") + public ResponseEntity getAllBookmarks(@AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + return ResponseEntity + .ok() + .body( + ApiUtils.success( + bookmarkService.getBookmarks(customOAuth2User) + ) + ); + } + @DeleteMapping("/{bookmarkId}") + @Operation(summary = "북마크 삭제", description = "특정 주점에 대한 북마크 삭제") + @ApiResponse(responseCode = "200", description = "북마크 삭제") + public ResponseEntity deleteBookmark(@PathVariable Long bookmarkId, @AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + return ResponseEntity + .ok() + .body( + ApiUtils.success( + bookmarkService.deleteBookmark(bookmarkId,customOAuth2User) + ) + ); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java b/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java new file mode 100644 index 00000000..5e4a8f65 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java @@ -0,0 +1,24 @@ +package com.example.apiuser.bookmark.dto; + +import com.nowait.domainbookmark.entity.Bookmark; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class BookmarkCreateResponse { + private Long bookmarkId; + private Long userId; + private Long storeId; + + public static BookmarkCreateResponse fromEntity(Bookmark bookmark) { + return BookmarkCreateResponse.builder() + .bookmarkId(bookmark.getId()) + .userId(bookmark.getUser().getId()) + .storeId(bookmark.getStore().getStoreId()) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java b/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java new file mode 100644 index 00000000..6f05db8e --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java @@ -0,0 +1,24 @@ +package com.example.apiuser.bookmark.dto; + +import com.nowait.domainbookmark.entity.Bookmark; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class BookmarkGetResponse { + private Long bookmarkId; + private Long userId; + private Long storeId; + + public static BookmarkGetResponse fromEntity(Bookmark bookmark) { + return BookmarkGetResponse.builder() + .bookmarkId(bookmark.getId()) + .userId(bookmark.getUser().getId()) + .storeId(bookmark.getStore().getStoreId()) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java b/api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java new file mode 100644 index 00000000..91ce0ff8 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java @@ -0,0 +1,78 @@ +package com.example.apiuser.bookmark.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.apiuser.bookmark.dto.BookmarkCreateResponse; +import com.example.apiuser.bookmark.dto.BookmarkGetResponse; +import com.example.domainstore.entity.Store; +import com.example.domainstore.repository.StoreRepository; +import com.nowait.auth.dto.CustomOAuth2User; +import com.nowait.domainbookmark.entity.Bookmark; +import com.nowait.domainbookmark.repository.BookmarkRepository; +import com.nowaiting.user.entity.User; +import com.nowaiting.user.repository.UserRepository; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class BookmarkService { + private final BookmarkRepository bookmarkRepository; + private final StoreRepository storeRepository; + private final UserRepository userRepository; + @Transactional + public BookmarkCreateResponse createBookmark(Long storeId, CustomOAuth2User customOAuth2User) { + parameterValidation(storeId, customOAuth2User); + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new EntityNotFoundException(storeId + " store not found.")); + User user = userRepository.findById(customOAuth2User.getUserId()) + .orElseThrow(() -> new EntityNotFoundException("User not found")); + + if (bookmarkRepository.existsByUserAndStore(user, store)) { + throw new IllegalArgumentException("already bookmarked"); + } + + Bookmark bookmark = Bookmark.builder() + .store(store) + .user(user) + .build(); + + return BookmarkCreateResponse.fromEntity(bookmarkRepository.save(bookmark)); + } + + @Transactional(readOnly = true) + public List getBookmarks(CustomOAuth2User customOAuth2User) { + User user = userRepository.findById(customOAuth2User.getUserId()) + .orElseThrow(() -> new EntityNotFoundException("User not found")); + return bookmarkRepository.findAllByUser(user) + .stream() + .map(BookmarkGetResponse::fromEntity) + .toList(); + } + + @Transactional + public String deleteBookmark(Long bookmarkId, CustomOAuth2User customOAuth2User) { + parameterValidation(bookmarkId, customOAuth2User); + Bookmark bookmark = bookmarkRepository.findById(bookmarkId) + .orElseThrow(() -> new EntityNotFoundException(bookmarkId + " bookmark not found.")); + if (bookmark.getUser().getId() != customOAuth2User.getUserId()) { + throw new IllegalArgumentException("you can only delete your own bookmark"); + } + bookmarkRepository.delete(bookmark); + return "Bookmark ID " + bookmarkId + " deleted."; + } + + private static void parameterValidation(Long storeId, CustomOAuth2User customOAuth2User) { + // 파라미터 유효성 검사 + if (storeId == null || storeId < 0) { + throw new IllegalArgumentException("storeId must be a positive number"); + } + if (customOAuth2User == null || customOAuth2User.getUserId() == null) { + throw new IllegalArgumentException("UserInfo is required"); + } + } +} diff --git a/api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java b/api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..fedb029f --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java @@ -0,0 +1,170 @@ +package com.example.apiuser.exception; + +import static com.nowaiting.common.exception.ErrorMessage.*; +import static org.springframework.http.HttpStatus.*; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestValueException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MultipartException; + +import com.example.apiuser.reservation.exception.ReservationNotFoundException; +import com.nowait.domainbookmark.exception.BookmarkOwnerMismatchException; +import com.nowait.domainbookmark.exception.DuplicateBookmarkException; +import com.nowait.domainorder.exception.DuplicateOrderException; +import com.nowait.domainorder.exception.OrderItemsEmptyException; +import com.nowait.domainorder.exception.OrderParameterEmptyException; +import com.nowait.exception.BusinessException; +import com.nowait.exception.ResourceNotFoundException; +import com.nowait.exception.UnauthorizedException; +import com.nowaiting.common.exception.ErrorMessage; +import com.nowaiting.common.exception.ErrorResponse; +import com.nowaiting.user.exception.UserNotFoundException; + +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Hidden +@RestControllerAdvice +public class GlobalExceptionHandler { + + // OAUTH 인증 실패 에러처리 메서드 + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(OAuth2AuthenticationException.class) + public ErrorResponse handlerOAuth2AuthenticationException(OAuth2AuthenticationException e) { + log.error("handleOAuth2AuthenticationException", e); + + return new ErrorResponse("OAuth 인증 실패 : " + e.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(BusinessException.class) + public ErrorResponse handleBusinessException(BusinessException e) { + log.error("handleBusinessException", e); + return new ErrorResponse(e.getMessage(), e.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("handleMethodArgumentNotValidException", e); + Map errors = getErrors(e); + return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode(), errors); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(HttpMessageNotReadableException.class) + public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.error("handleHttpMessageNotReadableException", e); + return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) { + log.error("handleIllegalArgumentException", e); + return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(MissingRequestValueException.class) + public ErrorResponse handleMissingRequestValueException(MissingRequestValueException e) { + log.error("handleMissingRequestValueExceptionException", e); + return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = UNAUTHORIZED) + @ExceptionHandler(UnauthorizedException.class) + public ErrorResponse handleUnauthorizedException(UnauthorizedException e) { + log.error("handleUnauthorizedExceptionException", e); + return new ErrorResponse(e.getMessage(), e.getCode()); + } + + @ResponseStatus(value = NOT_FOUND) + @ExceptionHandler(ResourceNotFoundException.class) + public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException e) { + log.error("handleResourceNotFoundExceptionException", e); + return new ErrorResponse(e.getMessage(), e.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(MultipartException.class) + public ErrorResponse handleMultipartException(MultipartException e) { + log.error("handleMultipartException", e); + return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(DuplicateBookmarkException.class) + public ErrorResponse handleDuplicateBookmarkException(DuplicateBookmarkException e) { + log.error("handleDuplicateBookmarkException", e); + return new ErrorResponse(e.getMessage(), ErrorMessage.DUPLICATE_BOOKMARK.getCode()); + } + + @ResponseStatus(value = FORBIDDEN) + @ExceptionHandler(BookmarkOwnerMismatchException.class) + public ErrorResponse bookmarkOwnerMismatchException(BookmarkOwnerMismatchException e) { + log.error("bookmarkOwnerMismatchException", e); + return new ErrorResponse(e.getMessage(), NOT_OWN_BOOKMARK.getCode()); + } + + @ResponseStatus(value = NOT_FOUND) + @ExceptionHandler(UserNotFoundException.class) + public ErrorResponse userNotFoundException(UserNotFoundException e) { + log.error("userNotFoundException", e); + return new ErrorResponse(e.getMessage(), NOTFOUND_USER.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(OrderParameterEmptyException.class) + public ErrorResponse orderParameterEmptyException(OrderParameterEmptyException e) { + log.error("orderParameterEmptyException", e); + return new ErrorResponse(e.getMessage(), ORDER_PARAMETER_EMPTY.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(OrderItemsEmptyException.class) + public ErrorResponse orderItemsEmptyException(OrderItemsEmptyException e) { + log.error("orderItemsEmptyException", e); + return new ErrorResponse(e.getMessage(), ORDER_ITEMS_EMPTY.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(DuplicateOrderException.class) + public ErrorResponse duplicateOrderException(DuplicateOrderException e) { + log.error("duplicateOrderException", e); + return new ErrorResponse(e.getMessage(), ErrorMessage.DUPLICATE_ORDER.getCode()); + } + + @ResponseStatus(value = NOT_FOUND) + @ExceptionHandler(ReservationNotFoundException.class) + public ErrorResponse reservationNotFoundException(ReservationNotFoundException e) { + log.error("reservationNotFoundException", e); + return new ErrorResponse(e.getMessage(), NOTFOUND_RESERVATION.getCode()); + } + + + private static Map getErrors(MethodArgumentNotValidException e) { + return e.getBindingResult() + .getAllErrors() + .stream() + .filter(ObjectError.class::isInstance) + .collect(Collectors.toMap( + error -> error instanceof FieldError ? ((FieldError)error).getField() : error.getObjectName(), + ObjectError::getDefaultMessage, + (msg1, msg2) -> msg1 + ";" + msg2 + )); + } + +} diff --git a/api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java b/api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java new file mode 100644 index 00000000..d3fb6d02 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java @@ -0,0 +1,32 @@ +package com.example.apiuser.menu.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.apiuser.menu.service.MenuService; +import com.nowaiting.common.api.ApiUtils; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/v1/menus") +@RequiredArgsConstructor +public class MenuController { + + private final MenuService menuService; + + @GetMapping("/all-menus/stores/{storeId}") + public ResponseEntity getMenusByStoreId(@PathVariable Long storeId) { + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiUtils.success( + menuService.getMenusByStoreId(storeId) + ) + ); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java new file mode 100644 index 00000000..c32be356 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java @@ -0,0 +1,20 @@ +package com.example.apiuser.menu.dto; + +import com.example.menu.entity.MenuImage; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MenuImageUploadResponse { + private final Long id; + private final String imageUrl; + + public static MenuImageUploadResponse fromEntity(MenuImage menuImage) { + return MenuImageUploadResponse.builder() + .id(menuImage.getId()) + .imageUrl(menuImage.getImageUrl()) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java new file mode 100644 index 00000000..98dda1f2 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java @@ -0,0 +1,32 @@ +package com.example.apiuser.menu.dto; + +import java.util.List; + +import com.example.menu.entity.Menu; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class MenuReadDto { + private Long menuId; + private Long storeId; + private String name; + private String description; + private Integer price; + private List images; + + public static MenuReadDto fromEntity(Menu menu, List images) { + return MenuReadDto.builder() + .menuId(menu.getId()) + .storeId(menu.getStoreId()) + .name(menu.getName()) + .description(menu.getDescription()) + .price(menu.getPrice()) + .images(images) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java new file mode 100644 index 00000000..755b243f --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java @@ -0,0 +1,21 @@ +package com.example.apiuser.menu.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class MenuReadResponse { + + private List menuReadDto; + + public static MenuReadResponse of(List menuReadDto) { + return MenuReadResponse.builder() + .menuReadDto(menuReadDto) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java b/api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java new file mode 100644 index 00000000..64b9e0b9 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java @@ -0,0 +1,42 @@ +package com.example.apiuser.menu.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.apiuser.menu.dto.MenuImageUploadResponse; +import com.example.apiuser.menu.dto.MenuReadDto; +import com.example.apiuser.menu.dto.MenuReadResponse; +import com.example.menu.entity.Menu; +import com.example.menu.entity.MenuImage; +import com.example.menu.repository.MenuImageRepository; +import com.example.menu.repository.MenuRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class MenuService { + + private final MenuRepository menuRepository; + private final MenuImageRepository menuImageRepository; + + + @Transactional(readOnly = true) + public MenuReadResponse getMenusByStoreId(Long storeId) { + List menus = menuRepository.findAllByStoreId(storeId); + + List menuReadResponse = menus.stream() + .map(menu -> { + List images = menuImageRepository.findByMenu(menu); + List imageDto = images.stream() + .map(MenuImageUploadResponse::fromEntity) + .toList(); + return MenuReadDto.fromEntity(menu, imageDto); + }) + .toList(); + + return MenuReadResponse.of(menuReadResponse); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java b/api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java new file mode 100644 index 00000000..65238120 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java @@ -0,0 +1,70 @@ +package com.example.apiuser.order.controller; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.apiuser.order.dto.OrderCreateRequestDto; +import com.example.apiuser.order.dto.OrderCreateResponseDto; +import com.example.apiuser.order.dto.OrderItemListGetResponseDto; +import com.example.apiuser.order.service.OrderService; +import com.nowaiting.common.api.ApiUtils; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@Tag(name = "Order API", description = "주문 API") +@RestController +@RequestMapping("/orders") +@RequiredArgsConstructor +public class OrderController { + private final OrderService orderService; + + @PostMapping("/create/{storeId}/{tableId}") + @Operation(summary = "주문 생성", description = "특정 주점 - 특정 테이블에 대한 주문 생성") + @ApiResponse(responseCode = "201", description = "주문 생성") + public ResponseEntity createOrder( + @PathVariable Long storeId, + @PathVariable Long tableId, + @RequestBody @Valid OrderCreateRequestDto orderCreateRequestDto, + HttpSession session + ) { + String sessionId = session.getId(); + OrderCreateResponseDto response = orderService.createOrder(storeId,tableId,orderCreateRequestDto,sessionId); + return ResponseEntity + .status(HttpStatus.CREATED) + .body( + ApiUtils.success(response) + ); + } + + @GetMapping("/items/{storeId}/{tableId}") + @Operation(summary = "테이블별 주문 아이템 조회", description = "비로그인(세션) 기준으로 테이블의 내 주문 목록만 조회") + @ApiResponse(responseCode = "200", description = "주문 조회") + public ResponseEntity getOrderItems( + @PathVariable Long storeId, + @PathVariable Long tableId, + HttpSession session + ) { + // 세션ID 추출 (Spring이 세션 자동 관리) + String sessionId = session.getId(); + + List orderItems = orderService.getOrderItems(storeId, tableId, sessionId); + return ResponseEntity. + status(HttpStatus.OK) + .body( + ApiUtils.success(orderItems) + ); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java new file mode 100644 index 00000000..1b8fc52b --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java @@ -0,0 +1,11 @@ +package com.example.apiuser.order.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CartItemDto { + private final Long menuId; // 메뉴 ID + private final int quantity; // 수량 +} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java new file mode 100644 index 00000000..056d44a6 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java @@ -0,0 +1,15 @@ +package com.example.apiuser.order.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class OrderCreateRequestDto { + private final List items; // 장바구니 항목 리스트 + +} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java new file mode 100644 index 00000000..1293ac0f --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java @@ -0,0 +1,28 @@ +package com.example.apiuser.order.dto; + +import java.util.List; + +import com.nowait.domainorder.entity.UserOrder; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class OrderCreateResponseDto { + private Long orderId; // 주문 ID // 주문 상태 (예: "주문완료", "배송중" 등) + private Long storeId; // 상점 ID + private String storeName; // 상점 이름 + private List orderItems; // 주문 항목 목록 + + public static OrderCreateResponseDto fromEntity(UserOrder order) { + return OrderCreateResponseDto.builder() + .orderId(order.getId()) + .storeId(order.getStore().getStoreId()) + .storeName(order.getStore().getName()) + .orderItems(List.of()) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java new file mode 100644 index 00000000..02e24e3f --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java @@ -0,0 +1,29 @@ +package com.example.apiuser.order.dto; + +import com.nowait.domainorder.entity.OrderItem; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderItemListGetResponseDto { + private Long orderId; + private String menuName; + private Integer quantity; + private Integer price; + + public static OrderItemListGetResponseDto fromEntity(OrderItem orderItem) { + return OrderItemListGetResponseDto.builder() + .orderId(orderItem.getUserOrder().getId()) + .menuName(orderItem.getMenu().getName()) + .quantity(orderItem.getQuantity()) + .price(orderItem.getMenu().getPrice()) + .build(); + + } +} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java new file mode 100644 index 00000000..12f04514 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java @@ -0,0 +1,16 @@ +package com.example.apiuser.order.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrderItemResponseDTO { + private Long menuId; // 메뉴 ID + private String menuName; // 메뉴 이름 + private int quantity; // 수량 +} diff --git a/api-user/src/main/java/com/example/apiuser/order/service/OrderService.java b/api-user/src/main/java/com/example/apiuser/order/service/OrderService.java new file mode 100644 index 00000000..1644ba16 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/order/service/OrderService.java @@ -0,0 +1,128 @@ +package com.example.apiuser.order.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.DigestUtils; + +import com.example.apiuser.order.dto.CartItemDto; +import com.example.apiuser.order.dto.OrderCreateRequestDto; +import com.example.apiuser.order.dto.OrderCreateResponseDto; +import com.example.apiuser.order.dto.OrderItemListGetResponseDto; +import com.example.domainstore.entity.Store; +import com.example.domainstore.repository.StoreRepository; +import com.example.menu.entity.Menu; +import com.example.menu.repository.MenuRepository; +import com.nowait.domainorder.entity.OrderItem; +import com.nowait.domainorder.entity.UserOrder; +import com.nowait.domainorder.exception.DuplicateOrderException; +import com.nowait.domainorder.exception.OrderItemsEmptyException; +import com.nowait.domainorder.exception.OrderParameterEmptyException; +import com.nowait.domainorder.repository.OrderItemRepository; +import com.nowait.domainorder.repository.OrderRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class OrderService { + private final OrderRepository orderRepository; + private final StoreRepository storeRepository; + private final MenuRepository menuRepository; + private final OrderItemRepository orderItemRepository; + @Transactional + public OrderCreateResponseDto createOrder(Long storeId, Long tableId, + OrderCreateRequestDto orderCreateRequestDto, String sessionId) { + parameterValidation(storeId, tableId, orderCreateRequestDto); + + // 💡 [중복 주문 방지] signature 생성 및 체크 + String signature = generateOrderSignature(storeId, tableId, orderCreateRequestDto.getItems()); + checkDuplicateOrderSignature(signature); + + // 1. Store 조회 + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new IllegalArgumentException("store not found")); + + // 2. UserOrder 생성 및 signature 저장 + UserOrder order = UserOrder.builder() + .tableId(tableId) + .store(store) + .signature(signature) // signature 저장 + .sessionId(sessionId) // sessionId 저장 + .build(); + UserOrder savedOrder = orderRepository.save(order); + + // 3. 메뉴 ID 리스트 수집 -> Map으로 캐싱 + List menuIds = orderCreateRequestDto.getItems().stream() + .map(CartItemDto::getMenuId) + .toList(); + + List menus = menuRepository.findAllById(menuIds); + Map menuMap = menus.stream() + .collect(Collectors.toMap(Menu::getId, Function.identity())); + + // 4. 각 장바구니 항목에 대해 OrderItem 생성 및 저장 + List orderItems = orderCreateRequestDto.getItems().stream() + .map(item -> { + Menu menu = Optional.ofNullable(menuMap.get(item.getMenuId())) + .orElseThrow(() -> new IllegalArgumentException("menu not found: " + item.getMenuId())); + return OrderItem.builder() + .userOrder(savedOrder) + .menu(menu) + .quantity(item.getQuantity()) + .build(); + }) + .collect(Collectors.toList()); + + + orderItemRepository.saveAll(orderItems); + + // 5. 응답 반환 + return OrderCreateResponseDto.fromEntity(savedOrder); + } + + @Transactional(readOnly = true) + public List getOrderItems(Long storeId, Long tableId, String sessionId) { + // 1. UserOrder 목록 조회 (storeId, tableId, sessionId 기준) + List userOrders = orderRepository.findByStore_StoreIdAndTableIdAndSessionId(storeId, tableId, sessionId); + + // 2. OrderItem으로 변환 + return userOrders.stream() + .flatMap(order -> order.getOrderItems().stream()) + .map(OrderItemListGetResponseDto::fromEntity) + .toList(); + } + + + private static void parameterValidation(Long storeId, Long tableId, OrderCreateRequestDto orderCreateRequestDto) { + if (storeId == null || tableId == null || orderCreateRequestDto == null) { + throw new OrderParameterEmptyException(); + } + if (orderCreateRequestDto.getItems() == null || orderCreateRequestDto.getItems().isEmpty()) { + throw new OrderItemsEmptyException(); + } + } + private String generateOrderSignature(Long storeId, Long tableId, List items) { + String cartString = items.stream() + .sorted((a, b) -> a.getMenuId().compareTo(b.getMenuId())) // 메뉴 ID 기준 정렬 + .map(item -> item.getMenuId() + ":" + item.getQuantity()) + .collect(Collectors.joining(",")); + String raw = storeId + "-" + tableId + "-" + cartString; + return DigestUtils.md5DigestAsHex(raw.getBytes()); + } + + private void checkDuplicateOrderSignature(String signature) { + // 최근 2초 이내 동일 signature 주문이 있는지 검사 + LocalDateTime threshold = LocalDateTime.now().minusSeconds(2); + boolean exists = orderRepository.existsBySignatureAndCreatedAtAfter(signature, threshold); + if (exists) { + throw new DuplicateOrderException(); + } + } +} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java b/api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java new file mode 100644 index 00000000..75416513 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java @@ -0,0 +1,47 @@ +package com.example.apiuser.reservation.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.apiuser.reservation.dto.ReservationCreateRequestDto; +import com.example.apiuser.reservation.dto.ReservationCreateResponseDto; +import com.example.apiuser.reservation.service.ReservationService; +import com.nowait.auth.dto.CustomOAuth2User; +import com.nowaiting.common.api.ApiUtils; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "Reservation API", description = "예약 API") +@RestController +@RequestMapping("/reservations") +@RequiredArgsConstructor +public class ReservationController { + + private final ReservationService reservationService; + + @PostMapping("/create/{storeId}") + @Operation(summary = "예약 생성", description = "특정 주점에 대한 예약하기 생성") + @ApiResponse(responseCode = "201", description = "예약 생성") + public ResponseEntity create( + @PathVariable Long storeId, + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @RequestBody ReservationCreateRequestDto requestDto) { + ReservationCreateResponseDto response = reservationService.create(storeId, customOAuth2User, requestDto); + return ResponseEntity + .status(HttpStatus.CREATED) + .body( + ApiUtils.success( + response + ) + ); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java b/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java new file mode 100644 index 00000000..03fbfb4f --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java @@ -0,0 +1,10 @@ +package com.example.apiuser.reservation.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ReservationCreateRequestDto { + private Integer partySize; +} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java b/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java new file mode 100644 index 00000000..fa581589 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java @@ -0,0 +1,17 @@ +package com.example.apiuser.reservation.dto; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ReservationCreateResponseDto { + private Long id; + private Long storeId; + private Long userId; + private LocalDateTime requestedAt; + private String status; + private Integer partySize; +} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java b/api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java new file mode 100644 index 00000000..37dc86eb --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java @@ -0,0 +1,9 @@ +package com.example.apiuser.reservation.exception; + +import com.nowaiting.common.exception.ErrorMessage; + +public class ReservationNotFoundException extends RuntimeException { + public ReservationNotFoundException() { + super(ErrorMessage.NOTFOUND_RESERVATION.getMessage()); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java b/api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java new file mode 100644 index 00000000..a5394ec8 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java @@ -0,0 +1,58 @@ +package com.example.apiuser.reservation.service; + +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.apiuser.reservation.dto.ReservationCreateRequestDto; +import com.example.apiuser.reservation.dto.ReservationCreateResponseDto; +import com.example.domainstore.entity.Store; +import com.example.domainstore.repository.StoreRepository; +import com.nowait.auth.dto.CustomOAuth2User; +import com.nowait.domainreservation.entity.Reservation; +import com.nowait.domainreservation.repository.ReservationRepository; +import com.nowaiting.common.enums.ReservationStatus; +import com.nowaiting.user.entity.User; +import com.nowaiting.user.exception.UserNotFoundException; +import com.nowaiting.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final ReservationRepository reservationRepository; + private final StoreRepository storeRepository; + private final UserRepository userRepository; + + @Transactional + public ReservationCreateResponseDto create(Long storeId, CustomOAuth2User customOAuth2User, + ReservationCreateRequestDto requestDto) { + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 store")); + User user = userRepository.findById(customOAuth2User.getUserId()) + .orElseThrow(UserNotFoundException::new); + + Reservation reservation = Reservation.builder() + .store(store) + .user(user) + .requestedAt(LocalDateTime.now()) + .status(ReservationStatus.WAITING) + .partySize(requestDto.getPartySize()) + .build(); + + Reservation saved = reservationRepository.save(reservation); + + return ReservationCreateResponseDto.builder() + .id(saved.getId()) + .storeId(saved.getStore().getStoreId()) + .userId(saved.getUser().getId()) + .requestedAt(saved.getRequestedAt()) + .status(saved.getStatus().name()) + .partySize(saved.getPartySize()) + .build(); + } +} + diff --git a/api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java b/api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java new file mode 100644 index 00000000..510b19d7 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java @@ -0,0 +1,56 @@ +package com.example.apiuser.store.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.apiuser.store.service.StoreService; +import com.nowaiting.common.api.ApiUtils; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("v1/stores") +@RequiredArgsConstructor +public class StoreController { + + private final StoreService storeService; + + + @GetMapping("/all-stores") + public ResponseEntity getAllStores() { + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiUtils.success( + storeService.getAllStores() + ) + ); + } + + @GetMapping("/{storeId}") + public ResponseEntity getStoreById(@PathVariable Long storeId) { + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiUtils.success( + storeService.getStoreByStoreId(storeId) + ) + ); + } + + @GetMapping("/search") + public ResponseEntity searchStores(@RequestParam("name") String name) { + return ResponseEntity + .ok() + .body( + ApiUtils.success( + storeService.searchStoresByName(name) + ) + ); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java b/api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java new file mode 100644 index 00000000..40cb984d --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java @@ -0,0 +1,22 @@ +package com.example.apiuser.store.dto; + +import com.example.domainstore.entity.StoreImage; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class StoreImageUploadResponse { + private final Long id; + private final String imageUrl; + private final String type; + + public static StoreImageUploadResponse fromEntity(StoreImage storeImage) { + return StoreImageUploadResponse.builder() + .id(storeImage.getId()) + .imageUrl(storeImage.getImageUrl()) + .type(storeImage.getType()) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java b/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java new file mode 100644 index 00000000..d6cfab6e --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java @@ -0,0 +1,39 @@ +package com.example.apiuser.store.dto; + +import java.time.LocalDateTime; +import java.util.List; + +import com.example.domainstore.entity.Store; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class StoreReadDto { + private Long storeId; + private Long departmentId; + private String name; + private String location; + private String description; + private List images; + private Boolean isActive; + private Boolean deleted; + private LocalDateTime createdAt; + + public static StoreReadDto fromEntity(Store store, List images) { + return StoreReadDto.builder() + .createdAt(store.getCreatedAt()) + .storeId(store.getStoreId()) + .departmentId(store.getDepartmentId()) + .name(store.getName()) + .location(store.getLocation()) + .description(store.getDescription()) + .isActive(store.getIsActive()) + .deleted(store.getDeleted()) + .images(images) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java b/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java new file mode 100644 index 00000000..a580e562 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java @@ -0,0 +1,23 @@ +package com.example.apiuser.store.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class StoreReadResponse { + + private List storeReadDtos; + private boolean hasNext; + + public static StoreReadResponse of(List storeReadDtos, boolean hasNext) { + return StoreReadResponse.builder() + .storeReadDtos(storeReadDtos) + .hasNext(hasNext) + .build(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/store/service/StoreService.java b/api-user/src/main/java/com/example/apiuser/store/service/StoreService.java new file mode 100644 index 00000000..fd90de2a --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/store/service/StoreService.java @@ -0,0 +1,16 @@ +package com.example.apiuser.store.service; + +import java.util.List; + +import com.example.apiuser.store.dto.StoreReadDto; +import com.example.apiuser.store.dto.StoreReadResponse; + +public interface StoreService { + + StoreReadResponse getAllStores(); + + StoreReadDto getStoreByStoreId(Long storeId); + + List searchStoresByName(String name); + +} diff --git a/api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java b/api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java new file mode 100644 index 00000000..d641f03e --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java @@ -0,0 +1,73 @@ +package com.example.apiuser.store.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.example.apiuser.store.dto.StoreImageUploadResponse; +import com.example.apiuser.store.dto.StoreReadDto; +import com.example.apiuser.store.dto.StoreReadResponse; +import com.example.domainstore.entity.Store; +import com.example.domainstore.entity.StoreImage; +import com.example.domainstore.repository.StoreImageRepository; +import com.example.domainstore.repository.StoreRepository; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class StoreServiceImpl implements StoreService { + + private final StoreRepository storeRepository; + private final StoreImageRepository storeImageRepository; + + + @Override + @Transactional(readOnly = true) + public StoreReadResponse getAllStores() { + List stores = storeRepository.findAllByDeletedFalse(); + + List storeRead = stores.stream() + .map(store -> { + List images = storeImageRepository.findByStore(store); + List imageDto = images.stream() + .map(StoreImageUploadResponse::fromEntity) + .toList(); + return StoreReadDto.fromEntity(store, imageDto); + }) + .toList(); + + boolean hasNext = false; + + return StoreReadResponse.of(storeRead, hasNext); + } + + @Override + @Transactional(readOnly = true) + public StoreReadDto getStoreByStoreId(Long storeId) { + Store store = storeRepository.findByStoreIdAndDeletedFalse(storeId) + .orElseThrow(() -> new EntityNotFoundException(storeId + " store not found.")); + + List images = storeImageRepository.findByStore(store); + List imageDto = images.stream() + .map(StoreImageUploadResponse::fromEntity) + .toList(); + + return StoreReadDto.fromEntity(store, imageDto); + } + + @Override + public List searchStoresByName(String name) { + List stores = storeRepository.findByNameContainingIgnoreCaseAndDeletedFalse(name); + return stores.stream() + .map(store -> { + List images = storeImageRepository.findByStore(store); + List imageDto = images.stream() + .map(StoreImageUploadResponse::fromEntity) + .toList(); + return StoreReadDto.fromEntity(store, imageDto); + }) + .toList(); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java b/api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java new file mode 100644 index 00000000..67bafe53 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java @@ -0,0 +1,59 @@ +package com.example.apiuser.token.controller; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.apiuser.token.dto.AuthenticationResponse; +import com.example.apiuser.token.dto.RefreshTokenRequest; +import com.example.apiuser.token.service.TokenService; +import com.nowait.auth.jwt.JwtUtil; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/refresh-token") +@Slf4j +public class TokenController { + private final JwtUtil jwtUtil; + private final TokenService tokenService; + @Value("${jwt.access-token-expiration-ms}") + private long accessTokenExpiration; + @Value("${jwt.refresh-token-expiration-ms}") + private long refreshTokenExpiration; + @PostMapping + public ResponseEntity refreshToken(@RequestBody RefreshTokenRequest request){ + String refreshToken = request.getRefreshToken(); + + // 리프레시 토큰 검증 + Long userId = jwtUtil.getUserId(refreshToken); + String role = jwtUtil.getRole(refreshToken); + + long currentAccessTokenExpiration = accessTokenExpiration; + if (role.equals("SUPER_ADMIN")) { + currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일 + } + + // 리프레시 토큰 유효성 검증 + if (tokenService.validateToken(refreshToken, userId)){ + // 유효한 토큰이라면, 새로운 accessToken, refreshToken 생성 + String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, currentAccessTokenExpiration); + String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration); + + // DB에 새로운 refreshToken으로 교체 + tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken); + + AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, refreshToken); + return ResponseEntity.ok().body(authenticationResponse); + + } + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token"); + } +} diff --git a/api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java b/api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java new file mode 100644 index 00000000..71db8914 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java @@ -0,0 +1,18 @@ +package com.example.apiuser.token.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@RequiredArgsConstructor +@Getter +@ToString(exclude = {"accessToken", "refreshToken"}) // 로깅 시 토큰 노출 방지 +public class AuthenticationResponse { + @JsonProperty("access_token") + private final String accessToken; + + @JsonProperty("refresh_token") + private final String refreshToken; +} diff --git a/api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java b/api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java new file mode 100644 index 00000000..4ab8ae75 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java @@ -0,0 +1,12 @@ +package com.example.apiuser.token.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RefreshTokenRequest { + @NotBlank(message = "Refresh token은 필수입니다.") + private String refreshToken; +} diff --git a/api-user/src/main/java/com/example/apiuser/token/service/TokenService.java b/api-user/src/main/java/com/example/apiuser/token/service/TokenService.java new file mode 100644 index 00000000..56845238 --- /dev/null +++ b/api-user/src/main/java/com/example/apiuser/token/service/TokenService.java @@ -0,0 +1,66 @@ +package com.example.apiuser.token.service; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.domaintoken.entity.Token; +import com.example.domaintoken.repository.TokenRepository; +import com.nowait.auth.jwt.JwtUtil; +import com.nowait.exception.RefreshTokenNotFoundException; +import com.nowait.exception.TokenBadRequestException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TokenService { + private final TokenRepository tokenRepository; + private final JwtUtil jwtUtil; + + @Transactional + public Boolean validateToken(String token, Long userId){ + // DB에서 해당 userId와 일치하는 리프레시토큰을 찾는다. + Optional savedToken = tokenRepository.findByUserId(userId); + + // DB에서 userId에 대응되는 리프레시토큰 없으면, 유효하지 않음 + if (savedToken.isEmpty()){ + log.info("여기에 걸렸니 ? -- 1 "); + return false; + } + + // 리프레시 토큰이 DB에 저장된 토큰과 일치하는지 확인 + if (!savedToken.get().getRefreshToken().equals(token)){ + log.info("여기에 걸렸니 ? -- 2 "); + return false; + } + + // 리프레시 토큰의 만료여부 확인 + if(jwtUtil.isExpired(token)){ + log.info("여기에 걸렸니 ? -- 3 "); + return false; // 만료된 토큰은 유효하지 않음 + } + + log.info("여기에 걸렸니 ? -- 4 "); + return true; // 모든 조건 만족 시, 유효한 토큰 + } + + @Transactional + public void updateRefreshToken(Long userId, String oldRefreshToken, String newRefreshToken){ + Token token = tokenRepository.findByUserId(userId) + .orElseThrow(RefreshTokenNotFoundException::new); // 404 + + if (!token.getRefreshToken().equals(oldRefreshToken)){ + throw new TokenBadRequestException(); // 400 + } + + // 기존 토큰 삭제 및 새 토큰 저장 + tokenRepository.delete(token); + Token newToken = Token.toEntity(token.getUser(), newRefreshToken, LocalDateTime.now().plusDays(30)); + tokenRepository.save(newToken); + } +} diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java b/application-admin/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java index 9a11c497..7a797a9b 100644 --- a/application-admin/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java +++ b/application-admin/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java @@ -60,7 +60,13 @@ public ManagerLoginResponseDto login(ManagerLoginRequestDto managerLoginRequestD ); MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); User user = userRepository.getReferenceById(memberDetails.getId()); - String accessToken = jwtUtil.createAccessToken("accessToken", user.getId(), String.valueOf(user.getRole()), accessTokenExpiration); + + long currentAccessTokenExpiration = accessTokenExpiration; + if (user.getRole() == com.nowait.common.enums.Role.SUPER_ADMIN) { + currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일 + } + + String accessToken = jwtUtil.createAccessToken("accessToken", user.getId(), String.valueOf(user.getRole()), currentAccessTokenExpiration); return ManagerLoginResponseDto.fromEntity(user,accessToken); } } diff --git a/application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java b/application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java new file mode 100644 index 00000000..9c839fbb --- /dev/null +++ b/application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java @@ -0,0 +1,21 @@ +package com.example.applicationconfig.config; + +import java.util.concurrent.Executor; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class AsyncConfig { + @Bean(name = "s3UploadExecutor") + public Executor s3UploadExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("S3Upload-"); + executor.initialize(); + return executor; + } +} diff --git a/application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java b/application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java new file mode 100644 index 00000000..d8615194 --- /dev/null +++ b/application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java @@ -0,0 +1,28 @@ +package com.example.applicationconfig.config; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용 + config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정 + config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용 + config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더 + config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정 + // config.setAllowCredentials(true); // 쿠키 포함 허용 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); //** 뜻은 모든 URL 경로에 적용한다는 의미 + return source; + } +} diff --git a/application-user/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java b/application-user/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java index c9298d04..009fafe4 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java +++ b/application-user/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java @@ -40,10 +40,15 @@ public ResponseEntity refreshToken(@RequestBody RefreshTokenRequest request){ Long userId = jwtUtil.getUserId(refreshToken); String role = jwtUtil.getRole(refreshToken); + long currentAccessTokenExpiration = accessTokenExpiration; + if (role.equals("SUPER_ADMIN")) { + currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일 + } + // 리프레시 토큰 유효성 검증 if (tokenService.validateToken(refreshToken, userId)){ // 유효한 토큰이라면, 새로운 accessToken, refreshToken 생성 - String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, accessTokenExpiration); + String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, currentAccessTokenExpiration); String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration); // DB에 새로운 refreshToken으로 교체 diff --git a/common/src/main/java/com/nowait/common/enums/Role.java b/common/src/main/java/com/nowait/common/enums/Role.java index 5aed5a91..ddbd6911 100644 --- a/common/src/main/java/com/nowait/common/enums/Role.java +++ b/common/src/main/java/com/nowait/common/enums/Role.java @@ -2,7 +2,8 @@ public enum Role { USER("USER"), - MANAGER("MANAGER"); + MANAGER("MANAGER"), + SUPER_ADMIN("SUPER_ADMIN"); private final String name; diff --git a/domain-token/src/main/java/com/example/domaintoken/entity/Token.java b/domain-token/src/main/java/com/example/domaintoken/entity/Token.java new file mode 100644 index 00000000..3bd47167 --- /dev/null +++ b/domain-token/src/main/java/com/example/domaintoken/entity/Token.java @@ -0,0 +1,54 @@ +package com.example.domaintoken.entity; + +import java.time.LocalDateTime; + +import com.nowait.user.entity.User; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "token") +@Getter +public class Token { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long tokenId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column + private String refreshToken; + + private LocalDateTime expiredDate; + + @Builder + public Token(User user, String refreshToken, LocalDateTime expiredDate) { + this.user = user; + this.refreshToken = refreshToken; + this.expiredDate = expiredDate; + } + + // static method로 객체를 생성 - 생성 의도 파악 쉬웁 + public static Token toEntity(User user, String refreshToken, LocalDateTime expiredDate){ + return Token.builder() + .user(user) + .refreshToken(refreshToken) + .expiredDate(expiredDate) + .build(); + } + +} diff --git a/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java b/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java new file mode 100644 index 00000000..05dc5c67 --- /dev/null +++ b/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java @@ -0,0 +1,16 @@ +package com.example.domaintoken.exception; + +import com.nowaiting.common.exception.ErrorMessage; + +public abstract class BusinessException extends RuntimeException { + private final ErrorMessage errorMessage; + + protected BusinessException(ErrorMessage errorMessage) { + super(errorMessage.getMessage()); + this.errorMessage = errorMessage; + } + + public String getCode() { + return errorMessage.getCode(); + } +} diff --git a/domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java b/domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java new file mode 100644 index 00000000..d813fd09 --- /dev/null +++ b/domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java @@ -0,0 +1,11 @@ +package com.example.domaintoken.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.example.domaintoken.entity.Token; + +public interface TokenRepository extends JpaRepository { + Optional findByUserId(Long userId); +} diff --git a/infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java b/infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java new file mode 100644 index 00000000..4eda87c2 --- /dev/null +++ b/infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java @@ -0,0 +1,33 @@ +package com.example.infrastorage.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +@Configuration +public class AwsS3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client)AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + +} diff --git a/infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java b/infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java new file mode 100644 index 00000000..20d9abf0 --- /dev/null +++ b/infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java @@ -0,0 +1,57 @@ +package com.example.infrastorage.s3; + +import java.io.InputStream; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; + +import io.github.resilience4j.bulkhead.annotation.Bulkhead; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class S3Service { + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public record S3UploadResult(String key, String url) { + } + + @Bulkhead(name = "s3UploadBulkhead", type = Bulkhead.Type.THREADPOOL) + @Async("s3UploadExecutor") + public CompletableFuture upload(String type, Long refId, MultipartFile file) { // TODO MultipartFile 분리 필요 (Spring에 의존하면 안 됨) + try (InputStream inputStream = file.getInputStream()) { + String key = createFileKey(type, refId, file.getOriginalFilename()); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + + amazonS3Client.putObject(bucket, key, inputStream, metadata); + String url = amazonS3Client.getUrl(bucket, key).toString(); + + return CompletableFuture.completedFuture(new S3UploadResult(key, url)); + } catch (Exception e) { + throw new RuntimeException("S3 업로드 실패", e); + } + } + + public void delete(String filename) { + try { + amazonS3Client.deleteObject(bucket, filename); + } catch (Exception e) { + throw new RuntimeException("S3 파일 삭제 실패", e); + } + } + + private String createFileKey(String type, Long refId, String filename) { + return type + "/" + refId + "/" + UUID.randomUUID() + "-" + filename; + } +} diff --git a/security-front/src/main/java/com/nowait/auth/config/CorsConfig.java b/security-front/src/main/java/com/nowait/auth/config/CorsConfig.java new file mode 100644 index 00000000..34b293b5 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/config/CorsConfig.java @@ -0,0 +1,28 @@ +package com.nowait.auth.config; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용 + config.setAllowedOrigins(List.of("http://localhost:3000", "http://localhost:8083")); // 허용할 출처 설정 + config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용 + config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더 + config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정 + // config.setAllowCredentials(true); // 쿠키 포함 허용 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); //** 뜻은 모든 URL 경로에 적용한다는 의미 + return source; + } +} diff --git a/security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java b/security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java new file mode 100644 index 00000000..aa535f8c --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java @@ -0,0 +1,93 @@ +package com.nowait.auth.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfigurationSource; + +import com.nowait.auth.jwt.JwtAuthorizationFilter; +import com.nowait.auth.jwt.JwtUtil; +import com.nowait.auth.oauth2.CustomOAuth2UserService; +import com.nowait.auth.oauth2.OAuth2LoginSuccessHandler; +import com.nowait.auth.service.CustomUserDetailService; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSecurity // security 활성화 어노테이션 +@RequiredArgsConstructor +public class SecurityConfig { + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2LoginSuccessHandler OAuth2LoginSuccessHandler; + private final JwtUtil jwtUtil; + private final CustomUserDetailService userDetailsService; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // CSRF 방어 기능 비활성화 (jwt 토큰을 사용할 것이기에 필요없음) + .csrf(AbstractHttpConfigurer::disable) + // 시큐리티 폼 로그인 비활성화 + .formLogin(AbstractHttpConfigurer::disable) + // HTTP Basic 인증 비활성화 + .httpBasic(AbstractHttpConfigurer::disable) + // oauth2 로그인 + // - userInfoEndPoint에서 사용자 정보 불러오고, + // - successHandler에서 로그인 성공 시 JWT 생성 및 반환로직 + .oauth2Login(oauth2 -> + oauth2.userInfoEndpoint(userInfoEndpoint -> + userInfoEndpoint.userService(customOAuth2UserService) + ).successHandler(OAuth2LoginSuccessHandler) + ) + // 세션 사용하지 않음 + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/oauth2/authorization/kakao", // 카카오 로그인 요청 + "/login/oauth2/code/**", // 카카오 인증 콜백 + "/api/refresh-token", // refresh token (토큰 갱신) + "orders/**", // 주문 관련 API + "/v1/menus/**", + "/v1/stores/**", + "/api/users/signup", + "/api/users/login", + "/swagger-ui/**", + "/v3/api-docs/**", + "/v3/api-docs.json", + "/api-docs/**", + "/swagger-resources/**", + "/webjars/**", + "/demo-ui.html" + ) + .permitAll() + .anyRequest().authenticated() // 그외 요청은 허가된 사람만 인가 + ) + // JWTFiler + .addFilterBefore(new JwtAuthorizationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(userDetailsService); + authenticationProvider.setPasswordEncoder(passwordEncoder()); + return authenticationProvider; + } + +} diff --git a/security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java b/security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java new file mode 100644 index 00000000..af969966 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java @@ -0,0 +1,60 @@ +package com.nowait.auth.dto; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.nowait.user.entity.User; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CustomOAuth2User implements OAuth2User { + private User user; + + // User 객체를 받는 생성자 + public CustomOAuth2User(User user) { + this.user = user; + } + + @Override + public Map getAttributes() { + return null; + } + + // 사용자가 가지는 권한 설정 + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + + authorities.add(new GrantedAuthority() { + @Override + public String getAuthority() { + return user.getRole().getName(); // 유저의 권한 리턴 + } + }); + + return authorities; + } + + @Override + public String getName() { + return user.getEmail(); + } + + public User getUser() { + return user; + } + + public Long getUserId() { + return user.getId(); + } + + public String getNickname() { + return user.getNickname(); + } + +} diff --git a/security-front/src/main/java/com/nowait/auth/dto/KaKaoResponse.java b/security-front/src/main/java/com/nowait/auth/dto/KaKaoResponse.java new file mode 100644 index 00000000..e745bd26 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/dto/KaKaoResponse.java @@ -0,0 +1,40 @@ +package com.nowait.auth.dto; + +import java.util.Map; + +import lombok.RequiredArgsConstructor; + +// 카카오 OAuth2 응답에서 필요한 정보를 추출하는 역할 +@RequiredArgsConstructor +public class KaKaoResponse implements OAuth2Response { + private final Map attributes; + + @Override + public String getProvider() { + return "kakao"; + } + + @Override + public String getProviderId() { + return attributes.get("id").toString(); + } + + @Override + public String getEmail() { + Map kakaoAccount = (Map)attributes.get("kakao_account"); + return kakaoAccount.get("email").toString(); + } + + @Override + public String getNickName() { + Map properties = (Map)attributes.get("properties"); + return properties.get("nickname").toString(); + } + + @Override + public String getProfileImage() { + Map properties = (Map)attributes.get("properties"); + return properties.get("profile_image").toString(); + } + +} diff --git a/security-front/src/main/java/com/nowait/auth/dto/OAuth2Response.java b/security-front/src/main/java/com/nowait/auth/dto/OAuth2Response.java new file mode 100644 index 00000000..de987480 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/dto/OAuth2Response.java @@ -0,0 +1,19 @@ +package com.nowait.auth.dto; + +public interface OAuth2Response { + // 제공자 (ex. naver, kakao) + String getProvider(); + + // 제공자에서 발급해주는 아이디 (번호) + String getProviderId(); + + // 아래 이메일, 닉네임, 프로필이미지는 내가 카카오 developers에서 발급받겠다고 신청한 정보들이다 + // 이메일 + String getEmail(); + + // 닉네임 + String getNickName(); + + // 프로필이미지 + String getProfileImage(); +} diff --git a/security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java b/security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java new file mode 100644 index 00000000..97a341d3 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java @@ -0,0 +1,93 @@ +package com.nowait.auth.jwt; + +import java.io.IOException; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.nowait.auth.dto.CustomOAuth2User; +import com.nowait.common.enums.Role; +import com.nowait.common.enums.SocialType; +import com.nowait.user.entity.User; + +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +// JWT 검증 필터 +// 1. 헤더에서 accessToken 추출, 2. 토큰 검증, 3. 유효하면 사용자정보를 SecurityContextHolder에 세팅 +// 그러면, 이후 컨트롤러에서 @AuthenticationPrincipal에서 저장했던 사용자 정보를 꺼내쓸 수 있음 +@RequiredArgsConstructor +@Slf4j +public class JwtAuthorizationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String header = request.getHeader("Authorization"); + + // 인증헤더 Bearer가 없다면, 다음 필터로 넘김 + if (header == null || !header.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + + log.info("JwtAuthorizationFilter 1 "); + return; + } + + log.info("header :: {}, header.substring(7) :: {}", header, header.substring(7)); + String accessToken = header.substring(7); + + // 토큰 만료 여부 확인, 만료 시 다음 필터로 넘기지 않음 + try { + jwtUtil.isExpired(accessToken); + } catch (ExpiredJwtException e) { + + // response status code + msg + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().print("access token expired"); + + log.info("JwtAuthorizationFilter 2 "); + return; + } + + // 토큰이 accessToken 종류인지 확인 + String tokenCategory = jwtUtil.getTokenCategory(accessToken); + + if (!tokenCategory.equals("accessToken")) { + //response status code + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().print("invalid access token"); + + log.info("JwtAuthorizationFilter 3 "); + return; + } + + // userId와 role 값 추출 + Long userId = jwtUtil.getUserId(accessToken); + String roleString = jwtUtil.getRole(accessToken); + + User user = User.createUserWithId(userId, "sampleEmail", "sampleNickname", "sampleProfileImg" + , SocialType.KAKAO, Role.fromString(roleString)); + + CustomOAuth2User customOAuth2User = new CustomOAuth2User(user); + + // 스프링 시큐리티 인증 토큰 생성 + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( + customOAuth2User, null, customOAuth2User.getAuthorities()); + + // 생성한 인증 정보를 SecurityContext에 설정 + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + + log.info("JwtAuthorizationFilter 4 "); + + filterChain.doFilter(request, response); + + } + +} diff --git a/security-front/src/main/java/com/nowait/auth/jwt/JwtUtil.java b/security-front/src/main/java/com/nowait/auth/jwt/JwtUtil.java new file mode 100644 index 00000000..fd0620e9 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/jwt/JwtUtil.java @@ -0,0 +1,76 @@ +package com.nowait.auth.jwt; + +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; + +@Component +public class JwtUtil { + private final SecretKey secretKey; + + // 시크릿 키를 암호화하여, 키 생성 + public JwtUtil(@Value("${jwt.secret}") String secret) { + this.secretKey = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS256.key().build().getAlgorithm() + ); + } + + public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) { + return Jwts.builder() + .claim("tokenCategory", tokenCategory) // accessToken + .claim("userId", userId) + .claim("role", role) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(secretKey) + .compact(); + } + + public String createRefreshToken(String tokenCategory, Long userId, Long expiredMs) { + return Jwts.builder() + .claim("tokenCategory", tokenCategory) // refreshToken + .claim("userId", userId) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(secretKey) + .compact(); + } + + public String getTokenCategory(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseClaimsJws(token) + .getBody() + .get("tokenCategory", String.class); + } + + public String getRole(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseClaimsJws(token) + .getBody() + .get("role", String.class); + } + + public Long getUserId(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseClaimsJws(token) + .getBody() + .get("userId", Long.class); + } + + public Boolean isExpired(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseSignedClaims(token) + .getPayload() + .getExpiration() + .before(new Date()); + } + +} diff --git a/security-front/src/main/java/com/nowait/auth/oauth2/CustomOAuth2UserService.java b/security-front/src/main/java/com/nowait/auth/oauth2/CustomOAuth2UserService.java new file mode 100644 index 00000000..b51577f7 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,70 @@ +package com.nowait.auth.oauth2; + +import java.util.Optional; + +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import com.nowait.auth.dto.CustomOAuth2User; +import com.nowait.auth.dto.KaKaoResponse; +import com.nowait.auth.dto.OAuth2Response; +import com.nowait.common.enums.Role; +import com.nowait.common.enums.SocialType; +import com.nowait.user.entity.User; +import com.nowait.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +// OAuth2 제공자(카카오)로부터 제공받은 사용자 정보를, 우리 서비스에 맞게 가공, 변환 +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + private final UserRepository userRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + + log.info("CustomOAuth2UserService :: {}", oAuth2User); + log.info("oAuthUser.getAttributes :: {}", oAuth2User.getAttributes()); + + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + OAuth2Response oAuth2Response = null; + + if (registrationId.equals("kakao")) { + oAuth2Response = new KaKaoResponse(oAuth2User.getAttributes()); + } else { + throw new OAuth2AuthenticationException("지원하지 않는 OAuth2 Provider 입니다."); + } + + // DB에 유저가 있는지 판단 + Optional foundUser = userRepository.findByEmail(oAuth2Response.getEmail()); + + // DB에 유저 없으면 - 회원가입 + if (foundUser.isEmpty()) { + + User user = User.builder() + .email(oAuth2Response.getEmail()) + .nickname(oAuth2Response.getNickName()) + .profileImage(oAuth2Response.getProfileImage()) + .socialType(SocialType.KAKAO) + .role(Role.USER) // 일반 유저 설정 + .build(); + + userRepository.save(user); + + return new CustomOAuth2User(user); + } else { + // DB에 유저 존재하면 - 로그인 진행 (이때 로그인 처리는 안하고, OAuth2LoginSuccessHandler에서 담당함) + User user = foundUser.get(); + + return new CustomOAuth2User(user); + } + } + +} diff --git a/security-front/src/main/java/com/nowait/auth/oauth2/OAuth2LoginSuccessHandler.java b/security-front/src/main/java/com/nowait/auth/oauth2/OAuth2LoginSuccessHandler.java new file mode 100644 index 00000000..9364e1cc --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/oauth2/OAuth2LoginSuccessHandler.java @@ -0,0 +1,80 @@ +package com.nowait.auth.oauth2; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import com.example.domaintoken.entity.Token; +import com.example.domaintoken.repository.TokenRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nowait.auth.dto.CustomOAuth2User; +import com.nowait.auth.jwt.JwtUtil; +import com.nowait.user.entity.User; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +// 카카오 로그인 성공 시, 콜백 핸들러 +// 1. JWT 토큰 발급 +// - 이때, JWT payload는 보안상 최소한의 정보(userId, role)만 담겠다 +// 2. refreshToken만 DB에 저장 +// 3. JSON 응답으로, accessToken과 refreshToken 을 반환해준다. +@Component +@RequiredArgsConstructor +@Slf4j +public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private final JwtUtil jwtUtil; + private final TokenRepository tokenRepository; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + + // 1. CustomOAuth2UserService에서 설정한 OAuth2User 정보 가져오기 + CustomOAuth2User customUserDetails = (CustomOAuth2User)authentication.getPrincipal(); + + User user = customUserDetails.getUser(); + Long userId = customUserDetails.getUserId(); + String email = customUserDetails.getName(); + + Collection authorities = authentication.getAuthorities(); + Iterator iterator = authorities.iterator(); + GrantedAuthority auth = iterator.next(); + + String role = auth.getAuthority(); + + log.info("user, userId, email, role :: {} {} {} {}", user, userId, email, role); + + // 2. 1)의 사용자 정보를 담아, accessToken과 refreshToken 발행 + String accessToken = jwtUtil.createAccessToken("accessToken", userId, role, 60 * 60 * 1000L); // 유효기간 1시간 + String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, + 30 * 24 * 60 * 60 * 1000L); // 유효기간 30일 + + // 3. refreshToken을 DB에 저장 + Token refreshTokenEntity = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30)); + tokenRepository.save(refreshTokenEntity); + + // 4. JSON 응답으로, accessToken과 refreshToken 을 반환해준다. + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + + ObjectMapper objectMapper = new ObjectMapper(); // 객체 -> json 문자열로 변환 + String body = objectMapper.writeValueAsString( + Map.of( + "accessToken", accessToken, + "refreshToken", refreshToken + ) + ); + response.getWriter().write(body); + } + +} diff --git a/security-front/src/main/java/com/nowait/auth/service/CustomUserDetailService.java b/security-front/src/main/java/com/nowait/auth/service/CustomUserDetailService.java new file mode 100644 index 00000000..4476c9c4 --- /dev/null +++ b/security-front/src/main/java/com/nowait/auth/service/CustomUserDetailService.java @@ -0,0 +1,30 @@ +package com.nowait.auth.service; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import com.nowait.user.entity.MemberDetails; +import com.nowait.user.entity.User; +import com.nowait.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CustomUserDetailService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email).orElseThrow(); + return MemberDetails.create(user); + } + + public UserDetails loadUserById(Long id) throws UsernameNotFoundException { + User member = userRepository.findById(id).orElseThrow(); + return MemberDetails.create(member); + } +} diff --git a/security-front/src/main/java/com/nowait/exception/BusinessException.java b/security-front/src/main/java/com/nowait/exception/BusinessException.java new file mode 100644 index 00000000..60eec165 --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/BusinessException.java @@ -0,0 +1,14 @@ +package com.nowait.exception; + +public abstract class BusinessException extends RuntimeException { + private final ErrorMessage errorMessage; + + protected BusinessException(ErrorMessage errorMessage) { + super(errorMessage.getMessage()); + this.errorMessage = errorMessage; + } + + public String getCode() { + return errorMessage.getCode(); + } +} diff --git a/security-front/src/main/java/com/nowait/exception/ErrorMessage.java b/security-front/src/main/java/com/nowait/exception/ErrorMessage.java new file mode 100644 index 00000000..c365a92e --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/ErrorMessage.java @@ -0,0 +1,22 @@ +package com.nowait.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ErrorMessage { + // global + INVALID_INPUT_VALUE("입력값이 올바르지 않습니다.", "global001"), + + // auth + UNAUTHORIZED("권한이 없습니다", "auth001"), + + // token + REFRESH_TOKEN_NOT_FOUND("기존 리프레시 토큰을 찾을 수 없습니다.", "token001"), + DOES_NOT_MATCH_REFRESH_TOKEN("기존 리프레시 토큰이 일치하지 않습니다.", "token002"); + + private final String message; + private final String code; + +} diff --git a/security-front/src/main/java/com/nowait/exception/ErrorResponse.java b/security-front/src/main/java/com/nowait/exception/ErrorResponse.java new file mode 100644 index 00000000..a15935e8 --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/ErrorResponse.java @@ -0,0 +1,26 @@ +package com.nowait.exception; + +import java.util.HashMap; +import java.util.Map; + +import lombok.Getter; + +@Getter +public class ErrorResponse { + private final String message; + private final String code; + private final Map errors; + + public ErrorResponse(String message, String code) { + this.message = message; + this.code = code; + errors = new HashMap<>(); + } + + public ErrorResponse(String message, String code, Map errors) { + this.message = message; + this.code = code; + this.errors = errors; + } + +} diff --git a/security-front/src/main/java/com/nowait/exception/GlobalExceptionHandler.java b/security-front/src/main/java/com/nowait/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..b9f7c1e2 --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/GlobalExceptionHandler.java @@ -0,0 +1,108 @@ +package com.nowait.exception; + +import static com.nowait.exception.ErrorMessage.*; +import static org.springframework.http.HttpStatus.*; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestValueException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MultipartException; + +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Hidden +@RestControllerAdvice +public class GlobalExceptionHandler { + + // OAUTH 인증 실패 에러처리 메서드 + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(OAuth2AuthenticationException.class) + public ErrorResponse handlerOAuth2AuthenticationException(OAuth2AuthenticationException e) { + log.error("handleOAuth2AuthenticationException", e); + + return new ErrorResponse("OAuth 인증 실패 : " + e.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(BusinessException.class) + public ErrorResponse handleBusinessException(BusinessException e) { + log.error("handleBusinessException", e); + return new ErrorResponse(e.getMessage(), e.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("handleMethodArgumentNotValidException", e); + Map errors = getErrors(e); + return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode(), errors); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(HttpMessageNotReadableException.class) + public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.error("handleHttpMessageNotReadableException", e); + return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) { + log.error("handleIllegalArgumentException", e); + return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(MissingRequestValueException.class) + public ErrorResponse handleMissingRequestValueException(MissingRequestValueException e) { + log.error("handleMissingRequestValueExceptionException", e); + return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + @ResponseStatus(value = UNAUTHORIZED) + @ExceptionHandler(UnauthorizedException.class) + public ErrorResponse handleUnauthorizedException(UnauthorizedException e) { + log.error("handleUnauthorizedExceptionException", e); + return new ErrorResponse(e.getMessage(), e.getCode()); + } + + @ResponseStatus(value = NOT_FOUND) + @ExceptionHandler(ResourceNotFoundException.class) + public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException e) { + log.error("handleResourceNotFoundExceptionException", e); + return new ErrorResponse(e.getMessage(), e.getCode()); + } + + @ResponseStatus(value = BAD_REQUEST) + @ExceptionHandler(MultipartException.class) + public ErrorResponse handleMultipartException(MultipartException e) { + log.error("handleMultipartException", e); + return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode()); + } + + + private static Map getErrors(MethodArgumentNotValidException e) { + return e.getBindingResult() + .getAllErrors() + .stream() + .filter(ObjectError.class::isInstance) + .collect(Collectors.toMap( + error -> error instanceof FieldError ? ((FieldError)error).getField() : error.getObjectName(), + ObjectError::getDefaultMessage, + (msg1, msg2) -> msg1 + ";" + msg2 + )); + } + +} diff --git a/security-front/src/main/java/com/nowait/exception/RefreshTokenNotFoundException.java b/security-front/src/main/java/com/nowait/exception/RefreshTokenNotFoundException.java new file mode 100644 index 00000000..5365ef52 --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/RefreshTokenNotFoundException.java @@ -0,0 +1,9 @@ +package com.nowait.exception; + +public class RefreshTokenNotFoundException extends ResourceNotFoundException { + + public RefreshTokenNotFoundException() { + super(ErrorMessage.REFRESH_TOKEN_NOT_FOUND); + } + +} diff --git a/security-front/src/main/java/com/nowait/exception/ResourceNotFoundException.java b/security-front/src/main/java/com/nowait/exception/ResourceNotFoundException.java new file mode 100644 index 00000000..ae4e4b7b --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/ResourceNotFoundException.java @@ -0,0 +1,14 @@ +package com.nowait.exception; + +public abstract class ResourceNotFoundException extends RuntimeException { + private final ErrorMessage errorMessage; + + protected ResourceNotFoundException(ErrorMessage errorMessage) { + super(errorMessage.getMessage()); + this.errorMessage = errorMessage; + } + + public String getCode() { + return errorMessage.getCode(); + } +} diff --git a/security-front/src/main/java/com/nowait/exception/TokenBadRequestException.java b/security-front/src/main/java/com/nowait/exception/TokenBadRequestException.java new file mode 100644 index 00000000..504852ee --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/TokenBadRequestException.java @@ -0,0 +1,8 @@ +package com.nowait.exception; + +public class TokenBadRequestException extends BusinessException { + public TokenBadRequestException() { + super(ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN); + } + +} diff --git a/security-front/src/main/java/com/nowait/exception/UnauthorizedException.java b/security-front/src/main/java/com/nowait/exception/UnauthorizedException.java new file mode 100644 index 00000000..65a8fd0a --- /dev/null +++ b/security-front/src/main/java/com/nowait/exception/UnauthorizedException.java @@ -0,0 +1,19 @@ +package com.nowait.exception; + +public class UnauthorizedException extends RuntimeException { + private final ErrorMessage errorMessage; + + public UnauthorizedException() { + super(ErrorMessage.UNAUTHORIZED.getMessage()); + this.errorMessage = ErrorMessage.UNAUTHORIZED; + } + + public UnauthorizedException(ErrorMessage errorMessage) { + super(errorMessage.getMessage()); + this.errorMessage = errorMessage; + } + + public String getCode() { + return errorMessage.getCode(); + } +} From e8351c08db10ac9e4f6703e47679b79e6664014f Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Thu, 3 Jul 2025 18:07:01 +0900 Subject: [PATCH 06/12] =?UTF-8?q?fix(Bookmark,user):=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B5=AC=EC=B2=B4=EC=A0=81=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwt 토큰 에러 로그 구체화 - bookmark 참조비교에서 값비교로 변경 --- .../token/service/TokenService.java | 10 +++++----- .../bookmark/service/BookmarkService.java | 3 ++- .../com/nowait/auth/config/SecurityConfig.java | 2 +- .../com/nowait/auth/dto/CustomOAuth2User.java | 9 ++------- .../nowait/auth/jwt/JwtAuthorizationFilter.java | 15 ++++++++++----- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/token/service/TokenService.java b/application-admin/src/main/java/com/nowait/applicationadmin/token/service/TokenService.java index 3ba54b63..746b86cb 100644 --- a/application-admin/src/main/java/com/nowait/applicationadmin/token/service/TokenService.java +++ b/application-admin/src/main/java/com/nowait/applicationadmin/token/service/TokenService.java @@ -23,29 +23,29 @@ public class TokenService { private final JwtUtil jwtUtil; @Transactional - public Boolean validateToken(String token, Long userId){ + public boolean validateToken(String token, Long userId){ // DB에서 해당 userId와 일치하는 리프레시토큰을 찾는다. Optional savedToken = tokenRepository.findByUserId(userId); // DB에서 userId에 대응되는 리프레시토큰 없으면, 유효하지 않음 if (savedToken.isEmpty()){ - log.info("여기에 걸렸니 ? -- 1 "); + log.debug("DB에 현재 userId에 대응되는 리프레시 토큰이 없습니다"); return false; } // 리프레시 토큰이 DB에 저장된 토큰과 일치하는지 확인 if (!savedToken.get().getRefreshToken().equals(token)){ - log.info("여기에 걸렸니 ? -- 2 "); + log.debug("DB에 저장된 리프레시 토큰와 현재 전달받은 리프레시 토큰 일치하지 않습니다"); return false; } // 리프레시 토큰의 만료여부 확인 if(jwtUtil.isExpired(token)){ - log.info("여기에 걸렸니 ? -- 3 "); + log.debug("리프레시 토큰이 만료된 토큰입니다"); return false; // 만료된 토큰은 유효하지 않음 } - log.info("여기에 걸렸니 ? -- 4 "); + log.info("리프레시 토큰이 유효한 토큰입니다"); return true; // 모든 조건 만족 시, 유효한 토큰 } diff --git a/application-user/src/main/java/com/nowait/applicationuser/bookmark/service/BookmarkService.java b/application-user/src/main/java/com/nowait/applicationuser/bookmark/service/BookmarkService.java index 5aae6553..31fdc89e 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/bookmark/service/BookmarkService.java +++ b/application-user/src/main/java/com/nowait/applicationuser/bookmark/service/BookmarkService.java @@ -1,6 +1,7 @@ package com.nowait.applicationuser.bookmark.service; import java.util.List; +import java.util.Objects; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -59,7 +60,7 @@ public String deleteBookmark(Long bookmarkId, CustomOAuth2User customOAuth2User) parameterValidation(bookmarkId, customOAuth2User); Bookmark bookmark = bookmarkRepository.findById(bookmarkId) .orElseThrow(() -> new EntityNotFoundException(bookmarkId + " bookmark not found.")); - if (bookmark.getUser().getId() != customOAuth2User.getUserId()) { + if (!Objects.equals(bookmark.getUser().getId(), customOAuth2User.getUserId())) { throw new IllegalArgumentException("you can only delete your own bookmark"); } bookmarkRepository.delete(bookmark); diff --git a/security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java b/security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java index aa535f8c..c4ebb970 100644 --- a/security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java +++ b/security-front/src/main/java/com/nowait/auth/config/SecurityConfig.java @@ -57,7 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/oauth2/authorization/kakao", // 카카오 로그인 요청 "/login/oauth2/code/**", // 카카오 인증 콜백 "/api/refresh-token", // refresh token (토큰 갱신) - "orders/**", // 주문 관련 API + "/orders/**", // 주문 관련 API "/v1/menus/**", "/v1/stores/**", "/api/users/signup", diff --git a/security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java b/security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java index af969966..61432393 100644 --- a/security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java +++ b/security-front/src/main/java/com/nowait/auth/dto/CustomOAuth2User.java @@ -13,16 +13,11 @@ @RequiredArgsConstructor public class CustomOAuth2User implements OAuth2User { - private User user; - - // User 객체를 받는 생성자 - public CustomOAuth2User(User user) { - this.user = user; - } + private final User user; @Override public Map getAttributes() { - return null; + return Map.of("email", user.getEmail(), "nickname", user.getNickname()); } // 사용자가 가지는 권한 설정 diff --git a/security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java b/security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java index 97a341d3..dcbbd985 100644 --- a/security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java +++ b/security-front/src/main/java/com/nowait/auth/jwt/JwtAuthorizationFilter.java @@ -36,7 +36,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (header == null || !header.startsWith("Bearer ")) { filterChain.doFilter(request, response); - log.info("JwtAuthorizationFilter 1 "); + log.debug("JwtAuthorizationFilter: Authorization 헤더가 없거나 Bearer 토큰 형식이 아님. JWT 인증 필터를 건너뜁니다. [header={}] ", header); return; } @@ -52,7 +52,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().print("access token expired"); - log.info("JwtAuthorizationFilter 2 "); + log.warn("JwtAuthorizationFilter: 만료된 AccessToken입니다. 토큰 인증 거부, URI: {}", request.getRequestURI()); return; } @@ -64,14 +64,19 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().print("invalid access token"); - log.info("JwtAuthorizationFilter 3 "); + log.warn("JwtAuthorizationFilter: 잘못된 토큰 유형(accessToken 아님)으로 인증 요청. URI: {}, tokenCategory: {}", request.getRequestURI(), tokenCategory); return; } // userId와 role 값 추출 Long userId = jwtUtil.getUserId(accessToken); String roleString = jwtUtil.getRole(accessToken); - + if (userId == null || roleString == null) { + log.warn("JwtAuthorizationFilter: JWT에서 userId 또는 role 추출 실패. 토큰: {}", accessToken); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().print("invalid token"); + return; + } User user = User.createUserWithId(userId, "sampleEmail", "sampleNickname", "sampleProfileImg" , SocialType.KAKAO, Role.fromString(roleString)); @@ -84,7 +89,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 생성한 인증 정보를 SecurityContext에 설정 SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); - log.info("JwtAuthorizationFilter 4 "); + log.info("JwtAuthorizationFilter: 인증 성공. userId={}, role={}, URI={}", userId, roleString, request.getRequestURI()); filterChain.doFilter(request, response); From 4d432d3692cab67121240acdbcd080c619e4024f Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Thu, 3 Jul 2025 19:58:10 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat(order):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=A3=BC=EB=AC=B8=EC=A1=B0=ED=9A=8C/=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - entity에 swagger 의존성 추가 - order 주문,수정 로직 구현 --- .../order/controller/OrderController.java | 49 +++++++++++++++++-- .../order/dto/OrderResponseDto.java | 33 +++++++++++++ .../dto/OrderStatusUpdateRequestDto.java | 16 ++++++ .../dto/OrderStatusUpdateResponseDto.java | 17 +++++++ .../order/service/OrderService.java | 37 ++++++++++++++ .../order/dto/OrderCreateRequestDto.java | 3 ++ .../order/dto/OrderCreateResponseDto.java | 5 ++ .../order/service/OrderService.java | 4 ++ domain-order/build.gradle | 3 ++ .../com/nowait/order/entity/OrderStatus.java | 22 +++++++++ .../com/nowait/order/entity/UserOrder.java | 13 +++++ .../order/repository/OrderRepository.java | 1 + .../exception/BusinessException.java | 2 +- 13 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java create mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java create mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java create mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java create mode 100644 domain-order/src/main/java/com/nowait/order/entity/OrderStatus.java diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java index c1a25862..775306e8 100644 --- a/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java +++ b/application-admin/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java @@ -1,22 +1,61 @@ package com.nowait.applicationadmin.order.controller; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.nowait.applicationadmin.order.dto.OrderResponseDto; +import com.nowait.applicationadmin.order.dto.OrderStatusUpdateRequestDto; +import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; +import com.nowait.applicationadmin.order.service.OrderService; +import com.nowait.common.api.ApiUtils; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @Tag(name = "Order API", description = "주문 API") @RestController -@RequestMapping("admin//orders") +@RequestMapping("admin/orders") @RequiredArgsConstructor public class OrderController { - @GetMapping - public void getOrders() { - //TODO - } + private final OrderService orderService; + @GetMapping("/{storeId}") + @Operation(summary = "주점별 주문리스트 조회", description = "특정 주점에 대한 예약리스트 조회") + @ApiResponse(responseCode = "200", description = "주리스트 조회") + public ResponseEntity getOrderListByStoreId(@PathVariable Long storeId) { + List response = orderService.findAllOrders(storeId); + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiUtils.success( + response + ) + ); + } + @PatchMapping("/status/{orderId}") + @Operation(summary = "주문 상태 변경", description = "특정 주문의 상태를 변경.") + @ApiResponse(responseCode = "200", description = "주문 상태 변경 성공") + @ApiResponse(responseCode = "400", description = "주문을 찾을 수 없음") + public ResponseEntity updateOrderStatus( + @PathVariable Long orderId, + @RequestBody@Valid OrderStatusUpdateRequestDto requestDto + ) { + OrderStatusUpdateResponseDto response = orderService.updateOrderStatus(orderId, requestDto.getOrderStatus()); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiUtils.success(response)); + } } diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java new file mode 100644 index 00000000..afb2a079 --- /dev/null +++ b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java @@ -0,0 +1,33 @@ +package com.nowait.applicationadmin.order.dto; + +import java.time.LocalDateTime; + +import com.nowait.applicationadmin.reservation.dto.ReservationGetResponseDto; +import com.nowait.order.entity.UserOrder; +import com.nowait.order.entity.OrderStatus; +import com.nowait.reservation.entity.Reservation; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class OrderResponseDto { + private Long id; + private Long tableId; + private String depositorName; + private Integer totalPrice; + private OrderStatus status; + private LocalDateTime createdAt; + + public static OrderResponseDto fromEntity(UserOrder userOrder) { + return OrderResponseDto.builder() + .id(userOrder.getId()) + .tableId(userOrder.getTableId()) + .depositorName(userOrder.getDepositorName()) + .totalPrice(userOrder.getTotalPrice()) + .status(userOrder.getStatus()) + .createdAt(userOrder.getCreatedAt()) + .build(); + } +} diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java new file mode 100644 index 00000000..a8a78442 --- /dev/null +++ b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java @@ -0,0 +1,16 @@ +package com.nowait.applicationadmin.order.dto; + +import com.nowait.order.entity.OrderStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OrderStatusUpdateRequestDto { + @Schema(description = "주문 상태", example = "WAITING_FOR_PAYMENT", allowableValues = {"WAITING_FOR_PAYMENT", "COOKING", "COOKED"}) + @NotNull(message = "주문상태는 필수입니다") + private final OrderStatus orderStatus; +} diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java new file mode 100644 index 00000000..63de5b53 --- /dev/null +++ b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java @@ -0,0 +1,17 @@ +package com.nowait.applicationadmin.order.dto; + +import com.nowait.order.entity.OrderStatus; +import com.nowait.order.entity.UserOrder; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OrderStatusUpdateResponseDto { + private final OrderStatus orderStatus; + + public static OrderStatusUpdateResponseDto fromEntity(UserOrder userOrder) { + return new OrderStatusUpdateResponseDto(userOrder.getStatus()); + } +} diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java new file mode 100644 index 00000000..c06986cf --- /dev/null +++ b/application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java @@ -0,0 +1,37 @@ +package com.nowait.applicationadmin.order.service; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.nowait.applicationadmin.order.dto.OrderResponseDto; +import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; +import com.nowait.order.entity.OrderStatus; +import com.nowait.order.entity.UserOrder; +import com.nowait.order.repository.OrderRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class OrderService { + + private final OrderRepository orderRepository; + + @Transactional(readOnly = true) + public List findAllOrders(Long storeId) { + return orderRepository.findAllByStore_StoreId(storeId).stream() + .map(OrderResponseDto::fromEntity) + .collect(Collectors.toList()); + } + + @Transactional + public OrderStatusUpdateResponseDto updateOrderStatus(Long orderId, OrderStatus newStatus) { + UserOrder userOrder = orderRepository.findById(orderId) + .orElseThrow(() -> new IllegalArgumentException("Order not found with id: " + orderId)); + userOrder.updateStatus(newStatus); + return OrderStatusUpdateResponseDto.fromEntity(userOrder); + } +} diff --git a/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java b/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java index 8823d0e4..50c6795f 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java +++ b/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java @@ -15,6 +15,9 @@ public class OrderCreateRequestDto { @NotBlank(message = "주문자 이름은 필수입니다") @Size(max = 10, message = "주문자 이름은 10자 이하여야 합니다") private final String depositorName; // 예약자 이름 + @NotBlank(message = "주문 내역은 필수입니다") private final List items; // 장바구니 항목 리스트 + @NotBlank(message = "주문금액은 필수 입니다.") + private final int totalPrice; } diff --git a/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java b/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java index 89a0e5e5..6044303e 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java +++ b/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java @@ -2,6 +2,7 @@ import java.util.List; +import com.nowait.order.entity.OrderStatus; import com.nowait.order.entity.UserOrder; import lombok.AllArgsConstructor; @@ -18,6 +19,8 @@ public class OrderCreateResponseDto { private String sessionId; private String depositorName; private List orderItems; // 주문 항목 목록 + private OrderStatus status; + private Integer totalPrice; public static OrderCreateResponseDto fromEntity(UserOrder order) { return OrderCreateResponseDto.builder() @@ -27,6 +30,8 @@ public static OrderCreateResponseDto fromEntity(UserOrder order) { .sessionId(order.getSessionId()) .depositorName(order.getDepositorName()) .orderItems(List.of()) + .status(order.getStatus()) + .totalPrice(order.getTotalPrice()) .build(); } } diff --git a/application-user/src/main/java/com/nowait/applicationuser/order/service/OrderService.java b/application-user/src/main/java/com/nowait/applicationuser/order/service/OrderService.java index 35ffdde4..a4835e61 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/order/service/OrderService.java +++ b/application-user/src/main/java/com/nowait/applicationuser/order/service/OrderService.java @@ -15,6 +15,7 @@ import com.nowait.applicationuser.order.dto.OrderCreateRequestDto; import com.nowait.applicationuser.order.dto.OrderCreateResponseDto; import com.nowait.applicationuser.order.dto.OrderItemListGetResponseDto; +import com.nowait.order.entity.OrderStatus; import com.nowait.store.entity.Store; import com.nowait.store.repository.StoreRepository; import com.nowait.menu.entity.Menu; @@ -56,6 +57,9 @@ public OrderCreateResponseDto createOrder(Long storeId, Long tableId, .signature(signature) // signature 저장 .sessionId(sessionId) // sessionId 저장 .depositorName(orderCreateRequestDto.getDepositorName()) + .status(OrderStatus.WAITING_FOR_PAYMENT) + .totalPrice(orderCreateRequestDto.getTotalPrice()) + .build(); UserOrder savedOrder = orderRepository.save(order); diff --git a/domain-order/build.gradle b/domain-order/build.gradle index 263db1df..7c7124e7 100644 --- a/domain-order/build.gradle +++ b/domain-order/build.gradle @@ -29,4 +29,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + + // SWAGGER + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' } diff --git a/domain-order/src/main/java/com/nowait/order/entity/OrderStatus.java b/domain-order/src/main/java/com/nowait/order/entity/OrderStatus.java new file mode 100644 index 00000000..7a111111 --- /dev/null +++ b/domain-order/src/main/java/com/nowait/order/entity/OrderStatus.java @@ -0,0 +1,22 @@ +package com.nowait.order.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(description = "주문 상태 Enum") +public enum OrderStatus { + @Schema(description = "입금대기") + WAITING_FOR_PAYMENT("입금대기"), + + @Schema(description = "조리중") + COOKING("조리중"), + + @Schema(description = "조리완료") + COOKED("조리완료"); + + private final String description; +} + diff --git a/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java b/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java index 06909f2c..a1c27498 100644 --- a/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java +++ b/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java @@ -9,6 +9,8 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -43,10 +45,21 @@ public class UserOrder extends BaseTimeEntity { private Store store; @OneToMany(mappedBy = "userOrder", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default private List orderItems = new ArrayList<>(); private String sessionId; @Column(length = 10) // 예약자 이름 길이 제한 private String depositorName; + @Builder.Default + @Enumerated(value = EnumType.STRING) + private OrderStatus status = OrderStatus.WAITING_FOR_PAYMENT; + + private Integer totalPrice; + + public void updateStatus(OrderStatus newStatus) { + this.status = newStatus; + } + } diff --git a/domain-order/src/main/java/com/nowait/order/repository/OrderRepository.java b/domain-order/src/main/java/com/nowait/order/repository/OrderRepository.java index e41b80de..48ca22d4 100644 --- a/domain-order/src/main/java/com/nowait/order/repository/OrderRepository.java +++ b/domain-order/src/main/java/com/nowait/order/repository/OrderRepository.java @@ -13,6 +13,7 @@ public interface OrderRepository extends JpaRepository { boolean existsBySignatureAndCreatedAtAfter(String signature, LocalDateTime createdAt); List findByStore_StoreIdAndTableIdAndSessionId(Long storeId, Long tableId, String sessionId); + List findAllByStore_StoreId(Long storeId); } diff --git a/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java b/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java index 05dc5c67..7fe9c748 100644 --- a/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java +++ b/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java @@ -1,6 +1,6 @@ package com.example.domaintoken.exception; -import com.nowaiting.common.exception.ErrorMessage; +import com.nowait.common.exception.ErrorMessage; public abstract class BusinessException extends RuntimeException { private final ErrorMessage errorMessage; From 9828345cb5544dcc2469b065438251a37e85b4d3 Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Thu, 3 Jul 2025 20:18:19 +0900 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 삭제된 디렉토리가 중복 등장 / 삭제처리 --- .../example/apiuser/ApiUserApplication.java | 39 ---- .../controller/BookmarkController.java | 67 ------- .../bookmark/dto/BookmarkCreateResponse.java | 24 --- .../bookmark/dto/BookmarkGetResponse.java | 24 --- .../bookmark/service/BookmarkService.java | 78 -------- .../exception/GlobalExceptionHandler.java | 170 ------------------ .../menu/controller/MenuController.java | 32 ---- .../menu/dto/MenuImageUploadResponse.java | 20 --- .../example/apiuser/menu/dto/MenuReadDto.java | 32 ---- .../apiuser/menu/dto/MenuReadResponse.java | 21 --- .../apiuser/menu/service/MenuService.java | 42 ----- .../order/controller/OrderController.java | 70 -------- .../apiuser/order/dto/CartItemDto.java | 11 -- .../order/dto/OrderCreateRequestDto.java | 15 -- .../order/dto/OrderCreateResponseDto.java | 28 --- .../dto/OrderItemListGetResponseDto.java | 29 --- .../order/dto/OrderItemResponseDTO.java | 16 -- .../apiuser/order/service/OrderService.java | 128 ------------- .../controller/ReservationController.java | 47 ----- .../dto/ReservationCreateRequestDto.java | 10 -- .../dto/ReservationCreateResponseDto.java | 17 -- .../ReservationNotFoundException.java | 9 - .../service/ReservationService.java | 58 ------ .../store/controller/StoreController.java | 56 ------ .../store/dto/StoreImageUploadResponse.java | 22 --- .../apiuser/store/dto/StoreReadDto.java | 39 ---- .../apiuser/store/dto/StoreReadResponse.java | 23 --- .../apiuser/store/service/StoreService.java | 16 -- .../store/service/StoreServiceImpl.java | 73 -------- .../token/controller/TokenController.java | 59 ------ .../token/dto/AuthenticationResponse.java | 18 -- .../token/dto/RefreshTokenRequest.java | 12 -- .../apiuser/token/service/TokenService.java | 66 ------- .../order/dto/OrderCreateRequestDto.java | 8 +- .../com/nowait/order/entity/UserOrder.java | 1 + 35 files changed, 7 insertions(+), 1373 deletions(-) delete mode 100644 api-user/src/main/java/com/example/apiuser/ApiUserApplication.java delete mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java delete mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java delete mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java delete mode 100644 api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java delete mode 100644 api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java delete mode 100644 api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java delete mode 100644 api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java delete mode 100644 api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java delete mode 100644 api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java delete mode 100644 api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java delete mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java delete mode 100644 api-user/src/main/java/com/example/apiuser/order/service/OrderService.java delete mode 100644 api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java delete mode 100644 api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java delete mode 100644 api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java delete mode 100644 api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java delete mode 100644 api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java delete mode 100644 api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java delete mode 100644 api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java delete mode 100644 api-user/src/main/java/com/example/apiuser/store/service/StoreService.java delete mode 100644 api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java delete mode 100644 api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java delete mode 100644 api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java delete mode 100644 api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java delete mode 100644 api-user/src/main/java/com/example/apiuser/token/service/TokenService.java diff --git a/api-user/src/main/java/com/example/apiuser/ApiUserApplication.java b/api-user/src/main/java/com/example/apiuser/ApiUserApplication.java deleted file mode 100644 index fcaf441e..00000000 --- a/api-user/src/main/java/com/example/apiuser/ApiUserApplication.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.apiuser; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; - -@EnableJpaAuditing -@SpringBootApplication(scanBasePackages = { - "com.example.apiuser", - "com.nowait.auth" -}) -@EntityScan(basePackages = { - "com.example.menu.entity", // domain-menu - "com.example.domainstore.entity", // domain-store - "com.example.domaintoken.entity", - "com.nowaiting.user.entity", - "com.nowait.domainbookmark.entity", - "com.nowait.domainreservation.entity", - "com.nowait.domainorder.entity", - "com.nowait.domainorder.entity" -}) -@EnableJpaRepositories(basePackages = { - "com.example.menu.repository", - "com.nowaiting.user.repository", - "com.example.domainstore.repository", - "com.example.domaintoken.repository", - "com.nowait.domainbookmark.repository", - "com.nowait.domainorder.repository", - "com.nowait.domainreservation.repository" -}) -public class ApiUserApplication { - - public static void main(String[] args) { - SpringApplication.run(ApiUserApplication.class, args); - } - -} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java b/api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java deleted file mode 100644 index 32c4132a..00000000 --- a/api-user/src/main/java/com/example/apiuser/bookmark/controller/BookmarkController.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.example.apiuser.bookmark.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.example.apiuser.bookmark.dto.BookmarkCreateResponse; -import com.example.apiuser.bookmark.service.BookmarkService; -import com.nowait.auth.dto.CustomOAuth2User; -import com.nowaiting.common.api.ApiUtils; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -@Tag(name = "Bookmark API", description = "북마크 API") -@RestController -@RequestMapping("/bookmarks") -@RequiredArgsConstructor -public class BookmarkController { - private final BookmarkService bookmarkService; - - @PostMapping("/{storeId}") - @Operation(summary = "북마크 생성", description = "특정 주점에 대한 북마크 생성") - @ApiResponse(responseCode = "201", description = "북마크 생성") - public ResponseEntity createBookmark(@PathVariable Long storeId,@AuthenticationPrincipal CustomOAuth2User customOAuth2User) { - BookmarkCreateResponse response = bookmarkService.createBookmark(storeId,customOAuth2User); - - return ResponseEntity - .status(HttpStatus.CREATED) - .body( - ApiUtils.success( - response - ) - ); - } - @GetMapping - @Operation(summary = "북마크 조회", description = "내가 북마크한 주점 조회") - @ApiResponse(responseCode = "200", description = "북마크 조회") - public ResponseEntity getAllBookmarks(@AuthenticationPrincipal CustomOAuth2User customOAuth2User) { - return ResponseEntity - .ok() - .body( - ApiUtils.success( - bookmarkService.getBookmarks(customOAuth2User) - ) - ); - } - @DeleteMapping("/{bookmarkId}") - @Operation(summary = "북마크 삭제", description = "특정 주점에 대한 북마크 삭제") - @ApiResponse(responseCode = "200", description = "북마크 삭제") - public ResponseEntity deleteBookmark(@PathVariable Long bookmarkId, @AuthenticationPrincipal CustomOAuth2User customOAuth2User) { - return ResponseEntity - .ok() - .body( - ApiUtils.success( - bookmarkService.deleteBookmark(bookmarkId,customOAuth2User) - ) - ); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java b/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java deleted file mode 100644 index 5e4a8f65..00000000 --- a/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkCreateResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.apiuser.bookmark.dto; - -import com.nowait.domainbookmark.entity.Bookmark; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class BookmarkCreateResponse { - private Long bookmarkId; - private Long userId; - private Long storeId; - - public static BookmarkCreateResponse fromEntity(Bookmark bookmark) { - return BookmarkCreateResponse.builder() - .bookmarkId(bookmark.getId()) - .userId(bookmark.getUser().getId()) - .storeId(bookmark.getStore().getStoreId()) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java b/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java deleted file mode 100644 index 6f05db8e..00000000 --- a/api-user/src/main/java/com/example/apiuser/bookmark/dto/BookmarkGetResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.apiuser.bookmark.dto; - -import com.nowait.domainbookmark.entity.Bookmark; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class BookmarkGetResponse { - private Long bookmarkId; - private Long userId; - private Long storeId; - - public static BookmarkGetResponse fromEntity(Bookmark bookmark) { - return BookmarkGetResponse.builder() - .bookmarkId(bookmark.getId()) - .userId(bookmark.getUser().getId()) - .storeId(bookmark.getStore().getStoreId()) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java b/api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java deleted file mode 100644 index 91ce0ff8..00000000 --- a/api-user/src/main/java/com/example/apiuser/bookmark/service/BookmarkService.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.example.apiuser.bookmark.service; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.example.apiuser.bookmark.dto.BookmarkCreateResponse; -import com.example.apiuser.bookmark.dto.BookmarkGetResponse; -import com.example.domainstore.entity.Store; -import com.example.domainstore.repository.StoreRepository; -import com.nowait.auth.dto.CustomOAuth2User; -import com.nowait.domainbookmark.entity.Bookmark; -import com.nowait.domainbookmark.repository.BookmarkRepository; -import com.nowaiting.user.entity.User; -import com.nowaiting.user.repository.UserRepository; - -import jakarta.persistence.EntityNotFoundException; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class BookmarkService { - private final BookmarkRepository bookmarkRepository; - private final StoreRepository storeRepository; - private final UserRepository userRepository; - @Transactional - public BookmarkCreateResponse createBookmark(Long storeId, CustomOAuth2User customOAuth2User) { - parameterValidation(storeId, customOAuth2User); - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new EntityNotFoundException(storeId + " store not found.")); - User user = userRepository.findById(customOAuth2User.getUserId()) - .orElseThrow(() -> new EntityNotFoundException("User not found")); - - if (bookmarkRepository.existsByUserAndStore(user, store)) { - throw new IllegalArgumentException("already bookmarked"); - } - - Bookmark bookmark = Bookmark.builder() - .store(store) - .user(user) - .build(); - - return BookmarkCreateResponse.fromEntity(bookmarkRepository.save(bookmark)); - } - - @Transactional(readOnly = true) - public List getBookmarks(CustomOAuth2User customOAuth2User) { - User user = userRepository.findById(customOAuth2User.getUserId()) - .orElseThrow(() -> new EntityNotFoundException("User not found")); - return bookmarkRepository.findAllByUser(user) - .stream() - .map(BookmarkGetResponse::fromEntity) - .toList(); - } - - @Transactional - public String deleteBookmark(Long bookmarkId, CustomOAuth2User customOAuth2User) { - parameterValidation(bookmarkId, customOAuth2User); - Bookmark bookmark = bookmarkRepository.findById(bookmarkId) - .orElseThrow(() -> new EntityNotFoundException(bookmarkId + " bookmark not found.")); - if (bookmark.getUser().getId() != customOAuth2User.getUserId()) { - throw new IllegalArgumentException("you can only delete your own bookmark"); - } - bookmarkRepository.delete(bookmark); - return "Bookmark ID " + bookmarkId + " deleted."; - } - - private static void parameterValidation(Long storeId, CustomOAuth2User customOAuth2User) { - // 파라미터 유효성 검사 - if (storeId == null || storeId < 0) { - throw new IllegalArgumentException("storeId must be a positive number"); - } - if (customOAuth2User == null || customOAuth2User.getUserId() == null) { - throw new IllegalArgumentException("UserInfo is required"); - } - } -} diff --git a/api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java b/api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java deleted file mode 100644 index fedb029f..00000000 --- a/api-user/src/main/java/com/example/apiuser/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.example.apiuser.exception; - -import static com.nowaiting.common.exception.ErrorMessage.*; -import static org.springframework.http.HttpStatus.*; -import static org.springframework.http.HttpStatus.UNAUTHORIZED; - -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingRequestValueException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.multipart.MultipartException; - -import com.example.apiuser.reservation.exception.ReservationNotFoundException; -import com.nowait.domainbookmark.exception.BookmarkOwnerMismatchException; -import com.nowait.domainbookmark.exception.DuplicateBookmarkException; -import com.nowait.domainorder.exception.DuplicateOrderException; -import com.nowait.domainorder.exception.OrderItemsEmptyException; -import com.nowait.domainorder.exception.OrderParameterEmptyException; -import com.nowait.exception.BusinessException; -import com.nowait.exception.ResourceNotFoundException; -import com.nowait.exception.UnauthorizedException; -import com.nowaiting.common.exception.ErrorMessage; -import com.nowaiting.common.exception.ErrorResponse; -import com.nowaiting.user.exception.UserNotFoundException; - -import io.swagger.v3.oas.annotations.Hidden; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Hidden -@RestControllerAdvice -public class GlobalExceptionHandler { - - // OAUTH 인증 실패 에러처리 메서드 - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(OAuth2AuthenticationException.class) - public ErrorResponse handlerOAuth2AuthenticationException(OAuth2AuthenticationException e) { - log.error("handleOAuth2AuthenticationException", e); - - return new ErrorResponse("OAuth 인증 실패 : " + e.getMessage(), INVALID_INPUT_VALUE.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(BusinessException.class) - public ErrorResponse handleBusinessException(BusinessException e) { - log.error("handleBusinessException", e); - return new ErrorResponse(e.getMessage(), e.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(MethodArgumentNotValidException.class) - public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - log.error("handleMethodArgumentNotValidException", e); - Map errors = getErrors(e); - return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode(), errors); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(HttpMessageNotReadableException.class) - public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { - log.error("handleHttpMessageNotReadableException", e); - return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(IllegalArgumentException.class) - public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) { - log.error("handleIllegalArgumentException", e); - return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(MissingRequestValueException.class) - public ErrorResponse handleMissingRequestValueException(MissingRequestValueException e) { - log.error("handleMissingRequestValueExceptionException", e); - return new ErrorResponse(INVALID_INPUT_VALUE.getMessage(), INVALID_INPUT_VALUE.getCode()); - } - - @ResponseStatus(value = UNAUTHORIZED) - @ExceptionHandler(UnauthorizedException.class) - public ErrorResponse handleUnauthorizedException(UnauthorizedException e) { - log.error("handleUnauthorizedExceptionException", e); - return new ErrorResponse(e.getMessage(), e.getCode()); - } - - @ResponseStatus(value = NOT_FOUND) - @ExceptionHandler(ResourceNotFoundException.class) - public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException e) { - log.error("handleResourceNotFoundExceptionException", e); - return new ErrorResponse(e.getMessage(), e.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(MultipartException.class) - public ErrorResponse handleMultipartException(MultipartException e) { - log.error("handleMultipartException", e); - return new ErrorResponse(e.getMessage(), INVALID_INPUT_VALUE.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(DuplicateBookmarkException.class) - public ErrorResponse handleDuplicateBookmarkException(DuplicateBookmarkException e) { - log.error("handleDuplicateBookmarkException", e); - return new ErrorResponse(e.getMessage(), ErrorMessage.DUPLICATE_BOOKMARK.getCode()); - } - - @ResponseStatus(value = FORBIDDEN) - @ExceptionHandler(BookmarkOwnerMismatchException.class) - public ErrorResponse bookmarkOwnerMismatchException(BookmarkOwnerMismatchException e) { - log.error("bookmarkOwnerMismatchException", e); - return new ErrorResponse(e.getMessage(), NOT_OWN_BOOKMARK.getCode()); - } - - @ResponseStatus(value = NOT_FOUND) - @ExceptionHandler(UserNotFoundException.class) - public ErrorResponse userNotFoundException(UserNotFoundException e) { - log.error("userNotFoundException", e); - return new ErrorResponse(e.getMessage(), NOTFOUND_USER.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(OrderParameterEmptyException.class) - public ErrorResponse orderParameterEmptyException(OrderParameterEmptyException e) { - log.error("orderParameterEmptyException", e); - return new ErrorResponse(e.getMessage(), ORDER_PARAMETER_EMPTY.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(OrderItemsEmptyException.class) - public ErrorResponse orderItemsEmptyException(OrderItemsEmptyException e) { - log.error("orderItemsEmptyException", e); - return new ErrorResponse(e.getMessage(), ORDER_ITEMS_EMPTY.getCode()); - } - - @ResponseStatus(value = BAD_REQUEST) - @ExceptionHandler(DuplicateOrderException.class) - public ErrorResponse duplicateOrderException(DuplicateOrderException e) { - log.error("duplicateOrderException", e); - return new ErrorResponse(e.getMessage(), ErrorMessage.DUPLICATE_ORDER.getCode()); - } - - @ResponseStatus(value = NOT_FOUND) - @ExceptionHandler(ReservationNotFoundException.class) - public ErrorResponse reservationNotFoundException(ReservationNotFoundException e) { - log.error("reservationNotFoundException", e); - return new ErrorResponse(e.getMessage(), NOTFOUND_RESERVATION.getCode()); - } - - - private static Map getErrors(MethodArgumentNotValidException e) { - return e.getBindingResult() - .getAllErrors() - .stream() - .filter(ObjectError.class::isInstance) - .collect(Collectors.toMap( - error -> error instanceof FieldError ? ((FieldError)error).getField() : error.getObjectName(), - ObjectError::getDefaultMessage, - (msg1, msg2) -> msg1 + ";" + msg2 - )); - } - -} diff --git a/api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java b/api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java deleted file mode 100644 index d3fb6d02..00000000 --- a/api-user/src/main/java/com/example/apiuser/menu/controller/MenuController.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.apiuser.menu.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.example.apiuser.menu.service.MenuService; -import com.nowaiting.common.api.ApiUtils; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequestMapping("/v1/menus") -@RequiredArgsConstructor -public class MenuController { - - private final MenuService menuService; - - @GetMapping("/all-menus/stores/{storeId}") - public ResponseEntity getMenusByStoreId(@PathVariable Long storeId) { - return ResponseEntity - .status(HttpStatus.OK) - .body( - ApiUtils.success( - menuService.getMenusByStoreId(storeId) - ) - ); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java deleted file mode 100644 index c32be356..00000000 --- a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuImageUploadResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.apiuser.menu.dto; - -import com.example.menu.entity.MenuImage; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class MenuImageUploadResponse { - private final Long id; - private final String imageUrl; - - public static MenuImageUploadResponse fromEntity(MenuImage menuImage) { - return MenuImageUploadResponse.builder() - .id(menuImage.getId()) - .imageUrl(menuImage.getImageUrl()) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java deleted file mode 100644 index 98dda1f2..00000000 --- a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadDto.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.apiuser.menu.dto; - -import java.util.List; - -import com.example.menu.entity.Menu; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class MenuReadDto { - private Long menuId; - private Long storeId; - private String name; - private String description; - private Integer price; - private List images; - - public static MenuReadDto fromEntity(Menu menu, List images) { - return MenuReadDto.builder() - .menuId(menu.getId()) - .storeId(menu.getStoreId()) - .name(menu.getName()) - .description(menu.getDescription()) - .price(menu.getPrice()) - .images(images) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java b/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java deleted file mode 100644 index 755b243f..00000000 --- a/api-user/src/main/java/com/example/apiuser/menu/dto/MenuReadResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.apiuser.menu.dto; - -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class MenuReadResponse { - - private List menuReadDto; - - public static MenuReadResponse of(List menuReadDto) { - return MenuReadResponse.builder() - .menuReadDto(menuReadDto) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java b/api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java deleted file mode 100644 index 64b9e0b9..00000000 --- a/api-user/src/main/java/com/example/apiuser/menu/service/MenuService.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.apiuser.menu.service; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.example.apiuser.menu.dto.MenuImageUploadResponse; -import com.example.apiuser.menu.dto.MenuReadDto; -import com.example.apiuser.menu.dto.MenuReadResponse; -import com.example.menu.entity.Menu; -import com.example.menu.entity.MenuImage; -import com.example.menu.repository.MenuImageRepository; -import com.example.menu.repository.MenuRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class MenuService { - - private final MenuRepository menuRepository; - private final MenuImageRepository menuImageRepository; - - - @Transactional(readOnly = true) - public MenuReadResponse getMenusByStoreId(Long storeId) { - List menus = menuRepository.findAllByStoreId(storeId); - - List menuReadResponse = menus.stream() - .map(menu -> { - List images = menuImageRepository.findByMenu(menu); - List imageDto = images.stream() - .map(MenuImageUploadResponse::fromEntity) - .toList(); - return MenuReadDto.fromEntity(menu, imageDto); - }) - .toList(); - - return MenuReadResponse.of(menuReadResponse); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java b/api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java deleted file mode 100644 index 65238120..00000000 --- a/api-user/src/main/java/com/example/apiuser/order/controller/OrderController.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.apiuser.order.controller; - -import java.util.List; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.example.apiuser.order.dto.OrderCreateRequestDto; -import com.example.apiuser.order.dto.OrderCreateResponseDto; -import com.example.apiuser.order.dto.OrderItemListGetResponseDto; -import com.example.apiuser.order.service.OrderService; -import com.nowaiting.common.api.ApiUtils; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpSession; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -@Tag(name = "Order API", description = "주문 API") -@RestController -@RequestMapping("/orders") -@RequiredArgsConstructor -public class OrderController { - private final OrderService orderService; - - @PostMapping("/create/{storeId}/{tableId}") - @Operation(summary = "주문 생성", description = "특정 주점 - 특정 테이블에 대한 주문 생성") - @ApiResponse(responseCode = "201", description = "주문 생성") - public ResponseEntity createOrder( - @PathVariable Long storeId, - @PathVariable Long tableId, - @RequestBody @Valid OrderCreateRequestDto orderCreateRequestDto, - HttpSession session - ) { - String sessionId = session.getId(); - OrderCreateResponseDto response = orderService.createOrder(storeId,tableId,orderCreateRequestDto,sessionId); - return ResponseEntity - .status(HttpStatus.CREATED) - .body( - ApiUtils.success(response) - ); - } - - @GetMapping("/items/{storeId}/{tableId}") - @Operation(summary = "테이블별 주문 아이템 조회", description = "비로그인(세션) 기준으로 테이블의 내 주문 목록만 조회") - @ApiResponse(responseCode = "200", description = "주문 조회") - public ResponseEntity getOrderItems( - @PathVariable Long storeId, - @PathVariable Long tableId, - HttpSession session - ) { - // 세션ID 추출 (Spring이 세션 자동 관리) - String sessionId = session.getId(); - - List orderItems = orderService.getOrderItems(storeId, tableId, sessionId); - return ResponseEntity. - status(HttpStatus.OK) - .body( - ApiUtils.success(orderItems) - ); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java deleted file mode 100644 index 1b8fc52b..00000000 --- a/api-user/src/main/java/com/example/apiuser/order/dto/CartItemDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.apiuser.order.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class CartItemDto { - private final Long menuId; // 메뉴 ID - private final int quantity; // 수량 -} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java deleted file mode 100644 index 056d44a6..00000000 --- a/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateRequestDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.apiuser.order.dto; - -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class OrderCreateRequestDto { - private final List items; // 장바구니 항목 리스트 - -} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java deleted file mode 100644 index 1293ac0f..00000000 --- a/api-user/src/main/java/com/example/apiuser/order/dto/OrderCreateResponseDto.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.apiuser.order.dto; - -import java.util.List; - -import com.nowait.domainorder.entity.UserOrder; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class OrderCreateResponseDto { - private Long orderId; // 주문 ID // 주문 상태 (예: "주문완료", "배송중" 등) - private Long storeId; // 상점 ID - private String storeName; // 상점 이름 - private List orderItems; // 주문 항목 목록 - - public static OrderCreateResponseDto fromEntity(UserOrder order) { - return OrderCreateResponseDto.builder() - .orderId(order.getId()) - .storeId(order.getStore().getStoreId()) - .storeName(order.getStore().getName()) - .orderItems(List.of()) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java deleted file mode 100644 index 02e24e3f..00000000 --- a/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemListGetResponseDto.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.apiuser.order.dto; - -import com.nowait.domainorder.entity.OrderItem; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class OrderItemListGetResponseDto { - private Long orderId; - private String menuName; - private Integer quantity; - private Integer price; - - public static OrderItemListGetResponseDto fromEntity(OrderItem orderItem) { - return OrderItemListGetResponseDto.builder() - .orderId(orderItem.getUserOrder().getId()) - .menuName(orderItem.getMenu().getName()) - .quantity(orderItem.getQuantity()) - .price(orderItem.getMenu().getPrice()) - .build(); - - } -} diff --git a/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java b/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java deleted file mode 100644 index 12f04514..00000000 --- a/api-user/src/main/java/com/example/apiuser/order/dto/OrderItemResponseDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.apiuser.order.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class OrderItemResponseDTO { - private Long menuId; // 메뉴 ID - private String menuName; // 메뉴 이름 - private int quantity; // 수량 -} diff --git a/api-user/src/main/java/com/example/apiuser/order/service/OrderService.java b/api-user/src/main/java/com/example/apiuser/order/service/OrderService.java deleted file mode 100644 index 1644ba16..00000000 --- a/api-user/src/main/java/com/example/apiuser/order/service/OrderService.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.example.apiuser.order.service; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.DigestUtils; - -import com.example.apiuser.order.dto.CartItemDto; -import com.example.apiuser.order.dto.OrderCreateRequestDto; -import com.example.apiuser.order.dto.OrderCreateResponseDto; -import com.example.apiuser.order.dto.OrderItemListGetResponseDto; -import com.example.domainstore.entity.Store; -import com.example.domainstore.repository.StoreRepository; -import com.example.menu.entity.Menu; -import com.example.menu.repository.MenuRepository; -import com.nowait.domainorder.entity.OrderItem; -import com.nowait.domainorder.entity.UserOrder; -import com.nowait.domainorder.exception.DuplicateOrderException; -import com.nowait.domainorder.exception.OrderItemsEmptyException; -import com.nowait.domainorder.exception.OrderParameterEmptyException; -import com.nowait.domainorder.repository.OrderItemRepository; -import com.nowait.domainorder.repository.OrderRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class OrderService { - private final OrderRepository orderRepository; - private final StoreRepository storeRepository; - private final MenuRepository menuRepository; - private final OrderItemRepository orderItemRepository; - @Transactional - public OrderCreateResponseDto createOrder(Long storeId, Long tableId, - OrderCreateRequestDto orderCreateRequestDto, String sessionId) { - parameterValidation(storeId, tableId, orderCreateRequestDto); - - // 💡 [중복 주문 방지] signature 생성 및 체크 - String signature = generateOrderSignature(storeId, tableId, orderCreateRequestDto.getItems()); - checkDuplicateOrderSignature(signature); - - // 1. Store 조회 - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new IllegalArgumentException("store not found")); - - // 2. UserOrder 생성 및 signature 저장 - UserOrder order = UserOrder.builder() - .tableId(tableId) - .store(store) - .signature(signature) // signature 저장 - .sessionId(sessionId) // sessionId 저장 - .build(); - UserOrder savedOrder = orderRepository.save(order); - - // 3. 메뉴 ID 리스트 수집 -> Map으로 캐싱 - List menuIds = orderCreateRequestDto.getItems().stream() - .map(CartItemDto::getMenuId) - .toList(); - - List menus = menuRepository.findAllById(menuIds); - Map menuMap = menus.stream() - .collect(Collectors.toMap(Menu::getId, Function.identity())); - - // 4. 각 장바구니 항목에 대해 OrderItem 생성 및 저장 - List orderItems = orderCreateRequestDto.getItems().stream() - .map(item -> { - Menu menu = Optional.ofNullable(menuMap.get(item.getMenuId())) - .orElseThrow(() -> new IllegalArgumentException("menu not found: " + item.getMenuId())); - return OrderItem.builder() - .userOrder(savedOrder) - .menu(menu) - .quantity(item.getQuantity()) - .build(); - }) - .collect(Collectors.toList()); - - - orderItemRepository.saveAll(orderItems); - - // 5. 응답 반환 - return OrderCreateResponseDto.fromEntity(savedOrder); - } - - @Transactional(readOnly = true) - public List getOrderItems(Long storeId, Long tableId, String sessionId) { - // 1. UserOrder 목록 조회 (storeId, tableId, sessionId 기준) - List userOrders = orderRepository.findByStore_StoreIdAndTableIdAndSessionId(storeId, tableId, sessionId); - - // 2. OrderItem으로 변환 - return userOrders.stream() - .flatMap(order -> order.getOrderItems().stream()) - .map(OrderItemListGetResponseDto::fromEntity) - .toList(); - } - - - private static void parameterValidation(Long storeId, Long tableId, OrderCreateRequestDto orderCreateRequestDto) { - if (storeId == null || tableId == null || orderCreateRequestDto == null) { - throw new OrderParameterEmptyException(); - } - if (orderCreateRequestDto.getItems() == null || orderCreateRequestDto.getItems().isEmpty()) { - throw new OrderItemsEmptyException(); - } - } - private String generateOrderSignature(Long storeId, Long tableId, List items) { - String cartString = items.stream() - .sorted((a, b) -> a.getMenuId().compareTo(b.getMenuId())) // 메뉴 ID 기준 정렬 - .map(item -> item.getMenuId() + ":" + item.getQuantity()) - .collect(Collectors.joining(",")); - String raw = storeId + "-" + tableId + "-" + cartString; - return DigestUtils.md5DigestAsHex(raw.getBytes()); - } - - private void checkDuplicateOrderSignature(String signature) { - // 최근 2초 이내 동일 signature 주문이 있는지 검사 - LocalDateTime threshold = LocalDateTime.now().minusSeconds(2); - boolean exists = orderRepository.existsBySignatureAndCreatedAtAfter(signature, threshold); - if (exists) { - throw new DuplicateOrderException(); - } - } -} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java b/api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java deleted file mode 100644 index 75416513..00000000 --- a/api-user/src/main/java/com/example/apiuser/reservation/controller/ReservationController.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.apiuser.reservation.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.example.apiuser.reservation.dto.ReservationCreateRequestDto; -import com.example.apiuser.reservation.dto.ReservationCreateResponseDto; -import com.example.apiuser.reservation.service.ReservationService; -import com.nowait.auth.dto.CustomOAuth2User; -import com.nowaiting.common.api.ApiUtils; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; - -@Tag(name = "Reservation API", description = "예약 API") -@RestController -@RequestMapping("/reservations") -@RequiredArgsConstructor -public class ReservationController { - - private final ReservationService reservationService; - - @PostMapping("/create/{storeId}") - @Operation(summary = "예약 생성", description = "특정 주점에 대한 예약하기 생성") - @ApiResponse(responseCode = "201", description = "예약 생성") - public ResponseEntity create( - @PathVariable Long storeId, - @AuthenticationPrincipal CustomOAuth2User customOAuth2User, - @RequestBody ReservationCreateRequestDto requestDto) { - ReservationCreateResponseDto response = reservationService.create(storeId, customOAuth2User, requestDto); - return ResponseEntity - .status(HttpStatus.CREATED) - .body( - ApiUtils.success( - response - ) - ); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java b/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java deleted file mode 100644 index 03fbfb4f..00000000 --- a/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateRequestDto.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.apiuser.reservation.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class ReservationCreateRequestDto { - private Integer partySize; -} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java b/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java deleted file mode 100644 index fa581589..00000000 --- a/api-user/src/main/java/com/example/apiuser/reservation/dto/ReservationCreateResponseDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.apiuser.reservation.dto; - -import java.time.LocalDateTime; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class ReservationCreateResponseDto { - private Long id; - private Long storeId; - private Long userId; - private LocalDateTime requestedAt; - private String status; - private Integer partySize; -} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java b/api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java deleted file mode 100644 index 37dc86eb..00000000 --- a/api-user/src/main/java/com/example/apiuser/reservation/exception/ReservationNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.apiuser.reservation.exception; - -import com.nowaiting.common.exception.ErrorMessage; - -public class ReservationNotFoundException extends RuntimeException { - public ReservationNotFoundException() { - super(ErrorMessage.NOTFOUND_RESERVATION.getMessage()); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java b/api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java deleted file mode 100644 index a5394ec8..00000000 --- a/api-user/src/main/java/com/example/apiuser/reservation/service/ReservationService.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.apiuser.reservation.service; - -import java.time.LocalDateTime; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.example.apiuser.reservation.dto.ReservationCreateRequestDto; -import com.example.apiuser.reservation.dto.ReservationCreateResponseDto; -import com.example.domainstore.entity.Store; -import com.example.domainstore.repository.StoreRepository; -import com.nowait.auth.dto.CustomOAuth2User; -import com.nowait.domainreservation.entity.Reservation; -import com.nowait.domainreservation.repository.ReservationRepository; -import com.nowaiting.common.enums.ReservationStatus; -import com.nowaiting.user.entity.User; -import com.nowaiting.user.exception.UserNotFoundException; -import com.nowaiting.user.repository.UserRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class ReservationService { - - private final ReservationRepository reservationRepository; - private final StoreRepository storeRepository; - private final UserRepository userRepository; - - @Transactional - public ReservationCreateResponseDto create(Long storeId, CustomOAuth2User customOAuth2User, - ReservationCreateRequestDto requestDto) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 store")); - User user = userRepository.findById(customOAuth2User.getUserId()) - .orElseThrow(UserNotFoundException::new); - - Reservation reservation = Reservation.builder() - .store(store) - .user(user) - .requestedAt(LocalDateTime.now()) - .status(ReservationStatus.WAITING) - .partySize(requestDto.getPartySize()) - .build(); - - Reservation saved = reservationRepository.save(reservation); - - return ReservationCreateResponseDto.builder() - .id(saved.getId()) - .storeId(saved.getStore().getStoreId()) - .userId(saved.getUser().getId()) - .requestedAt(saved.getRequestedAt()) - .status(saved.getStatus().name()) - .partySize(saved.getPartySize()) - .build(); - } -} - diff --git a/api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java b/api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java deleted file mode 100644 index 510b19d7..00000000 --- a/api-user/src/main/java/com/example/apiuser/store/controller/StoreController.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.apiuser.store.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.example.apiuser.store.service.StoreService; -import com.nowaiting.common.api.ApiUtils; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequestMapping("v1/stores") -@RequiredArgsConstructor -public class StoreController { - - private final StoreService storeService; - - - @GetMapping("/all-stores") - public ResponseEntity getAllStores() { - return ResponseEntity - .status(HttpStatus.OK) - .body( - ApiUtils.success( - storeService.getAllStores() - ) - ); - } - - @GetMapping("/{storeId}") - public ResponseEntity getStoreById(@PathVariable Long storeId) { - return ResponseEntity - .status(HttpStatus.OK) - .body( - ApiUtils.success( - storeService.getStoreByStoreId(storeId) - ) - ); - } - - @GetMapping("/search") - public ResponseEntity searchStores(@RequestParam("name") String name) { - return ResponseEntity - .ok() - .body( - ApiUtils.success( - storeService.searchStoresByName(name) - ) - ); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java b/api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java deleted file mode 100644 index 40cb984d..00000000 --- a/api-user/src/main/java/com/example/apiuser/store/dto/StoreImageUploadResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.apiuser.store.dto; - -import com.example.domainstore.entity.StoreImage; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class StoreImageUploadResponse { - private final Long id; - private final String imageUrl; - private final String type; - - public static StoreImageUploadResponse fromEntity(StoreImage storeImage) { - return StoreImageUploadResponse.builder() - .id(storeImage.getId()) - .imageUrl(storeImage.getImageUrl()) - .type(storeImage.getType()) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java b/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java deleted file mode 100644 index d6cfab6e..00000000 --- a/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadDto.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.apiuser.store.dto; - -import java.time.LocalDateTime; -import java.util.List; - -import com.example.domainstore.entity.Store; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class StoreReadDto { - private Long storeId; - private Long departmentId; - private String name; - private String location; - private String description; - private List images; - private Boolean isActive; - private Boolean deleted; - private LocalDateTime createdAt; - - public static StoreReadDto fromEntity(Store store, List images) { - return StoreReadDto.builder() - .createdAt(store.getCreatedAt()) - .storeId(store.getStoreId()) - .departmentId(store.getDepartmentId()) - .name(store.getName()) - .location(store.getLocation()) - .description(store.getDescription()) - .isActive(store.getIsActive()) - .deleted(store.getDeleted()) - .images(images) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java b/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java deleted file mode 100644 index a580e562..00000000 --- a/api-user/src/main/java/com/example/apiuser/store/dto/StoreReadResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.apiuser.store.dto; - -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@Getter -@AllArgsConstructor -@Builder -public class StoreReadResponse { - - private List storeReadDtos; - private boolean hasNext; - - public static StoreReadResponse of(List storeReadDtos, boolean hasNext) { - return StoreReadResponse.builder() - .storeReadDtos(storeReadDtos) - .hasNext(hasNext) - .build(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/store/service/StoreService.java b/api-user/src/main/java/com/example/apiuser/store/service/StoreService.java deleted file mode 100644 index fd90de2a..00000000 --- a/api-user/src/main/java/com/example/apiuser/store/service/StoreService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.apiuser.store.service; - -import java.util.List; - -import com.example.apiuser.store.dto.StoreReadDto; -import com.example.apiuser.store.dto.StoreReadResponse; - -public interface StoreService { - - StoreReadResponse getAllStores(); - - StoreReadDto getStoreByStoreId(Long storeId); - - List searchStoresByName(String name); - -} diff --git a/api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java b/api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java deleted file mode 100644 index d641f03e..00000000 --- a/api-user/src/main/java/com/example/apiuser/store/service/StoreServiceImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.example.apiuser.store.service; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import com.example.apiuser.store.dto.StoreImageUploadResponse; -import com.example.apiuser.store.dto.StoreReadDto; -import com.example.apiuser.store.dto.StoreReadResponse; -import com.example.domainstore.entity.Store; -import com.example.domainstore.entity.StoreImage; -import com.example.domainstore.repository.StoreImageRepository; -import com.example.domainstore.repository.StoreRepository; - -import jakarta.persistence.EntityNotFoundException; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class StoreServiceImpl implements StoreService { - - private final StoreRepository storeRepository; - private final StoreImageRepository storeImageRepository; - - - @Override - @Transactional(readOnly = true) - public StoreReadResponse getAllStores() { - List stores = storeRepository.findAllByDeletedFalse(); - - List storeRead = stores.stream() - .map(store -> { - List images = storeImageRepository.findByStore(store); - List imageDto = images.stream() - .map(StoreImageUploadResponse::fromEntity) - .toList(); - return StoreReadDto.fromEntity(store, imageDto); - }) - .toList(); - - boolean hasNext = false; - - return StoreReadResponse.of(storeRead, hasNext); - } - - @Override - @Transactional(readOnly = true) - public StoreReadDto getStoreByStoreId(Long storeId) { - Store store = storeRepository.findByStoreIdAndDeletedFalse(storeId) - .orElseThrow(() -> new EntityNotFoundException(storeId + " store not found.")); - - List images = storeImageRepository.findByStore(store); - List imageDto = images.stream() - .map(StoreImageUploadResponse::fromEntity) - .toList(); - - return StoreReadDto.fromEntity(store, imageDto); - } - - @Override - public List searchStoresByName(String name) { - List stores = storeRepository.findByNameContainingIgnoreCaseAndDeletedFalse(name); - return stores.stream() - .map(store -> { - List images = storeImageRepository.findByStore(store); - List imageDto = images.stream() - .map(StoreImageUploadResponse::fromEntity) - .toList(); - return StoreReadDto.fromEntity(store, imageDto); - }) - .toList(); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java b/api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java deleted file mode 100644 index 67bafe53..00000000 --- a/api-user/src/main/java/com/example/apiuser/token/controller/TokenController.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.example.apiuser.token.controller; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.example.apiuser.token.dto.AuthenticationResponse; -import com.example.apiuser.token.dto.RefreshTokenRequest; -import com.example.apiuser.token.service.TokenService; -import com.nowait.auth.jwt.JwtUtil; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/refresh-token") -@Slf4j -public class TokenController { - private final JwtUtil jwtUtil; - private final TokenService tokenService; - @Value("${jwt.access-token-expiration-ms}") - private long accessTokenExpiration; - @Value("${jwt.refresh-token-expiration-ms}") - private long refreshTokenExpiration; - @PostMapping - public ResponseEntity refreshToken(@RequestBody RefreshTokenRequest request){ - String refreshToken = request.getRefreshToken(); - - // 리프레시 토큰 검증 - Long userId = jwtUtil.getUserId(refreshToken); - String role = jwtUtil.getRole(refreshToken); - - long currentAccessTokenExpiration = accessTokenExpiration; - if (role.equals("SUPER_ADMIN")) { - currentAccessTokenExpiration = 7L * 24 * 60 * 60 * 1000L; // 7일 - } - - // 리프레시 토큰 유효성 검증 - if (tokenService.validateToken(refreshToken, userId)){ - // 유효한 토큰이라면, 새로운 accessToken, refreshToken 생성 - String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, currentAccessTokenExpiration); - String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration); - - // DB에 새로운 refreshToken으로 교체 - tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken); - - AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, refreshToken); - return ResponseEntity.ok().body(authenticationResponse); - - } - - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token"); - } -} diff --git a/api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java b/api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java deleted file mode 100644 index 71db8914..00000000 --- a/api-user/src/main/java/com/example/apiuser/token/dto/AuthenticationResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.apiuser.token.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.ToString; - -@RequiredArgsConstructor -@Getter -@ToString(exclude = {"accessToken", "refreshToken"}) // 로깅 시 토큰 노출 방지 -public class AuthenticationResponse { - @JsonProperty("access_token") - private final String accessToken; - - @JsonProperty("refresh_token") - private final String refreshToken; -} diff --git a/api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java b/api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java deleted file mode 100644 index 4ab8ae75..00000000 --- a/api-user/src/main/java/com/example/apiuser/token/dto/RefreshTokenRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.apiuser.token.dto; - -import jakarta.validation.constraints.NotBlank; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public class RefreshTokenRequest { - @NotBlank(message = "Refresh token은 필수입니다.") - private String refreshToken; -} diff --git a/api-user/src/main/java/com/example/apiuser/token/service/TokenService.java b/api-user/src/main/java/com/example/apiuser/token/service/TokenService.java deleted file mode 100644 index 56845238..00000000 --- a/api-user/src/main/java/com/example/apiuser/token/service/TokenService.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.example.apiuser.token.service; - -import java.time.LocalDateTime; -import java.util.Optional; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.example.domaintoken.entity.Token; -import com.example.domaintoken.repository.TokenRepository; -import com.nowait.auth.jwt.JwtUtil; -import com.nowait.exception.RefreshTokenNotFoundException; -import com.nowait.exception.TokenBadRequestException; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Service -@RequiredArgsConstructor -@Slf4j -public class TokenService { - private final TokenRepository tokenRepository; - private final JwtUtil jwtUtil; - - @Transactional - public Boolean validateToken(String token, Long userId){ - // DB에서 해당 userId와 일치하는 리프레시토큰을 찾는다. - Optional savedToken = tokenRepository.findByUserId(userId); - - // DB에서 userId에 대응되는 리프레시토큰 없으면, 유효하지 않음 - if (savedToken.isEmpty()){ - log.info("여기에 걸렸니 ? -- 1 "); - return false; - } - - // 리프레시 토큰이 DB에 저장된 토큰과 일치하는지 확인 - if (!savedToken.get().getRefreshToken().equals(token)){ - log.info("여기에 걸렸니 ? -- 2 "); - return false; - } - - // 리프레시 토큰의 만료여부 확인 - if(jwtUtil.isExpired(token)){ - log.info("여기에 걸렸니 ? -- 3 "); - return false; // 만료된 토큰은 유효하지 않음 - } - - log.info("여기에 걸렸니 ? -- 4 "); - return true; // 모든 조건 만족 시, 유효한 토큰 - } - - @Transactional - public void updateRefreshToken(Long userId, String oldRefreshToken, String newRefreshToken){ - Token token = tokenRepository.findByUserId(userId) - .orElseThrow(RefreshTokenNotFoundException::new); // 404 - - if (!token.getRefreshToken().equals(oldRefreshToken)){ - throw new TokenBadRequestException(); // 400 - } - - // 기존 토큰 삭제 및 새 토큰 저장 - tokenRepository.delete(token); - Token newToken = Token.toEntity(token.getUser(), newRefreshToken, LocalDateTime.now().plusDays(30)); - tokenRepository.save(newToken); - } -} diff --git a/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java b/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java index 50c6795f..89de6631 100644 --- a/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java +++ b/application-user/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateRequestDto.java @@ -3,6 +3,9 @@ import java.util.List; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; @@ -15,9 +18,10 @@ public class OrderCreateRequestDto { @NotBlank(message = "주문자 이름은 필수입니다") @Size(max = 10, message = "주문자 이름은 10자 이하여야 합니다") private final String depositorName; // 예약자 이름 - @NotBlank(message = "주문 내역은 필수입니다") + @NotEmpty(message = "주문 내역은 필수입니다") private final List items; // 장바구니 항목 리스트 - @NotBlank(message = "주문금액은 필수 입니다.") + @NotNull(message = "주문금액은 필수입니다") + @Positive(message = "주문금액은 양수여야 합니다") private final int totalPrice; } diff --git a/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java b/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java index a1c27498..c5e4639a 100644 --- a/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java +++ b/domain-order/src/main/java/com/nowait/order/entity/UserOrder.java @@ -56,6 +56,7 @@ public class UserOrder extends BaseTimeEntity { @Enumerated(value = EnumType.STRING) private OrderStatus status = OrderStatus.WAITING_FOR_PAYMENT; + @Column(nullable = false) private Integer totalPrice; public void updateStatus(OrderStatus newStatus) { From b9bb8cb6ffa3f5bae99e5afb2ecf958b9f7cc7aa Mon Sep 17 00:00:00 2001 From: Harry <114489245+HyemIin@users.noreply.github.com> Date: Thu, 3 Jul 2025 20:24:48 +0900 Subject: [PATCH 09/12] =?UTF-8?q?Update=20and=20rename=20=EB=B0=B1?= =?UTF-8?q?=EC=97=94=EB=93=9C-=EC=9D=B4=EC=8A=88.md=20to=20needdelete.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/needdelete.md | 1 + ...27\224\353\223\234-\354\235\264\354\212\210.md" | 14 -------------- 2 files changed, 1 insertion(+), 14 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/needdelete.md delete mode 100644 ".github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" diff --git a/.github/ISSUE_TEMPLATE/needdelete.md b/.github/ISSUE_TEMPLATE/needdelete.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/needdelete.md @@ -0,0 +1 @@ + diff --git "a/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" "b/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" deleted file mode 100644 index f728915f..00000000 --- "a/.github/ISSUE_TEMPLATE/\353\260\261\354\227\224\353\223\234-\354\235\264\354\212\210.md" +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: 백엔드 이슈 -about: 백엔드와 관련된 이슈 -title: "[백엔드] " -labels: Backend -assignees: '' - ---- - -# 이슈 내용 - - -# 작업 목록 -- [ ] From 48cc4fcbcdffec91c1b5c8e253bba7f2896188f0 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Fri, 4 Jul 2025 22:37:44 +0900 Subject: [PATCH 10/12] =?UTF-8?q?refactor:=20=EB=A9=80=ED=8B=B0=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-admin.yml | 16 ++++++++-------- .github/workflows/deploy-user.yml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deploy-admin.yml b/.github/workflows/deploy-admin.yml index 39de4768..5c9889c2 100644 --- a/.github/workflows/deploy-admin.yml +++ b/.github/workflows/deploy-admin.yml @@ -5,14 +5,14 @@ on: branches: - develop paths: - - 'nowait-app-admin-api/**' - - 'nowait-common/**' - - 'nowait-domain/domain-core-rdb/**' - - 'nowait-domain/domain-admin-rdb/**' - - 'nowait-infra/**' - - 'build.gradle' - - 'settings.gradle' - - 'gradle/**' + - 'nowait-app-admin-api/**' + - 'nowait-common/**' + - 'nowait-domain/domain-core-rdb/**' + - 'nowait-domain/domain-admin-rdb/**' + - 'nowait-infra/**' + - 'build.gradle' + - 'settings.gradle' + - 'gradle/**' env: PROJECT_NAME: NoWait diff --git a/.github/workflows/deploy-user.yml b/.github/workflows/deploy-user.yml index 70f31909..67b2f28f 100644 --- a/.github/workflows/deploy-user.yml +++ b/.github/workflows/deploy-user.yml @@ -5,14 +5,14 @@ on: branches: - develop paths: - - 'nowait-app-user-api/**' - - 'nowait-common/**' - - 'nowait-domain/domain-core-rdb/**' - - 'nowait-domain/domain-user-rdb/**' - - 'nowait-infra/**' - - 'build.gradle' - - 'settings.gradle' - - 'gradle/**' + - 'nowait-app-user-api/**' + - 'nowait-common/**' + - 'nowait-domain/domain-core-rdb/**' + - 'nowait-domain/domain-user-rdb/**' + - 'nowait-infra/**' + - 'build.gradle' + - 'settings.gradle' + - 'gradle/**' env: PROJECT_NAME: NoWait From 28ba7b8fb7f576b77f690f9bc0f5f585d8ea0eb6 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Fri, 4 Jul 2025 22:38:12 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat(order):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20Order=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/controller/OrderController.java | 2 +- .../order/dto/OrderResponseDto.java | 31 ++++++++++++++++ .../dto/OrderStatusUpdateRequestDto.java | 16 +++++++++ .../dto/OrderStatusUpdateResponseDto.java | 17 +++++++++ .../order/service/OrderService.java | 36 +++++++++++++++++++ .../order/service/OrderService.java | 1 + .../order/entity/OrderStatus.java | 2 +- 7 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java index 775306e8..dfc03619 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java @@ -25,7 +25,7 @@ @Tag(name = "Order API", description = "주문 API") @RestController -@RequestMapping("admin/orders") +@RequestMapping("/admin/orders") @RequiredArgsConstructor public class OrderController { diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java new file mode 100644 index 00000000..f5e93610 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java @@ -0,0 +1,31 @@ +package com.nowait.applicationadmin.order.dto; + +import java.time.LocalDateTime; + +import com.nowait.domaincorerdb.order.entity.OrderStatus; +import com.nowait.domaincorerdb.order.entity.UserOrder; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class OrderResponseDto { + private Long id; + private Long tableId; + private String depositorName; + private Integer totalPrice; + private OrderStatus status; + private LocalDateTime createdAt; + + public static OrderResponseDto fromEntity(UserOrder userOrder) { + return OrderResponseDto.builder() + .id(userOrder.getId()) + .tableId(userOrder.getTableId()) + .depositorName(userOrder.getDepositorName()) + .totalPrice(userOrder.getTotalPrice()) + .status(userOrder.getStatus()) + .createdAt(userOrder.getCreatedAt()) + .build(); + } +} diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java new file mode 100644 index 00000000..a28f0434 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java @@ -0,0 +1,16 @@ +package com.nowait.applicationadmin.order.dto; + +import com.nowait.domaincorerdb.order.entity.OrderStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OrderStatusUpdateRequestDto { + @Schema(description = "주문 상태", example = "WAITING_FOR_PAYMENT", allowableValues = {"WAITING_FOR_PAYMENT", "COOKING", "COOKED"}) + @NotNull(message = "주문상태는 필수입니다") + private final OrderStatus orderStatus; +} diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java new file mode 100644 index 00000000..dfcec919 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java @@ -0,0 +1,17 @@ +package com.nowait.applicationadmin.order.dto; + +import com.nowait.domaincorerdb.order.entity.OrderStatus; +import com.nowait.domaincorerdb.order.entity.UserOrder; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OrderStatusUpdateResponseDto { + private final OrderStatus orderStatus; + + public static OrderStatusUpdateResponseDto fromEntity(UserOrder userOrder) { + return new OrderStatusUpdateResponseDto(userOrder.getStatus()); + } +} diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java new file mode 100644 index 00000000..41418531 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java @@ -0,0 +1,36 @@ +package com.nowait.applicationadmin.order.service; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.nowait.applicationadmin.order.dto.OrderResponseDto; +import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; +import com.nowait.domaincorerdb.order.entity.OrderStatus; +import com.nowait.domaincorerdb.order.entity.UserOrder; +import com.nowait.domaincorerdb.order.repository.OrderRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class OrderService { + private final OrderRepository orderRepository; + + @Transactional(readOnly = true) + public List findAllOrders(Long storeId) { + return orderRepository.findAllByStore_StoreId(storeId).stream() + .map(OrderResponseDto::fromEntity) + .collect(Collectors.toList()); + } + + @Transactional + public OrderStatusUpdateResponseDto updateOrderStatus(Long orderId, OrderStatus newStatus) { + UserOrder userOrder = orderRepository.findById(orderId) + .orElseThrow(() -> new IllegalArgumentException("Order not found with id: " + orderId)); + userOrder.updateStatus(newStatus); + return OrderStatusUpdateResponseDto.fromEntity(userOrder); + } +} diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java index a3f59ebf..1f0ed53c 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java @@ -18,6 +18,7 @@ import com.nowait.domaincorerdb.menu.entity.Menu; import com.nowait.domaincorerdb.menu.repository.MenuRepository; import com.nowait.domaincorerdb.order.entity.OrderItem; +import com.nowait.domaincorerdb.order.entity.OrderStatus; import com.nowait.domaincorerdb.order.entity.UserOrder; import com.nowait.domaincorerdb.order.exception.DuplicateOrderException; import com.nowait.domaincorerdb.order.exception.OrderItemsEmptyException; diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java index 7a111111..98b50744 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java @@ -1,4 +1,4 @@ -package com.nowait.order.entity; +package com.nowait.domaincorerdb.order.entity; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; From 012c9f699b4b669ff1afbecd53c7d02bcd35d42b Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Fri, 4 Jul 2025 22:38:33 +0900 Subject: [PATCH 12/12] =?UTF-8?q?refactor:=20=EB=A9=80=ED=8B=B0=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/dto/OrderResponseDto.java | 33 ----------- .../dto/OrderStatusUpdateRequestDto.java | 16 ------ .../dto/OrderStatusUpdateResponseDto.java | 17 ------ .../order/service/OrderService.java | 37 ------------ application-config/build.gradle | 0 .../applicationconfig/config/AsyncConfig.java | 21 ------- .../applicationconfig/config/CorsConfig.java | 28 --------- .../nowait/config/config/SwaggerConfig.java | 30 ---------- build.gradle | 4 +- .../com/example/domaintoken/entity/Token.java | 54 ------------------ .../exception/BusinessException.java | 16 ------ .../repository/TokenRepository.java | 11 ---- .../infrastorage/config/AwsS3Config.java | 33 ----------- .../example/infrastorage/s3/S3Service.java | 57 ------------------- nowait-app-admin-api/build.gradle | 9 ++- nowait-app-user-api/build.gradle | 7 +-- nowait-common/build.gradle | 3 - nowait-domain/build.gradle | 6 +- nowait-domain/domain-admin-rdb/build.gradle | 3 - nowait-domain/domain-core-rdb/build.gradle | 7 +-- nowait-domain/domain-user-rdb/build.gradle | 6 +- nowait-infra/build.gradle | 3 - 22 files changed, 18 insertions(+), 383 deletions(-) delete mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java delete mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java delete mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java delete mode 100644 application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java delete mode 100644 application-config/build.gradle delete mode 100644 application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java delete mode 100644 application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java delete mode 100644 application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java delete mode 100644 domain-token/src/main/java/com/example/domaintoken/entity/Token.java delete mode 100644 domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java delete mode 100644 domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java delete mode 100644 infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java delete mode 100644 infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java deleted file mode 100644 index afb2a079..00000000 --- a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderResponseDto.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.nowait.applicationadmin.order.dto; - -import java.time.LocalDateTime; - -import com.nowait.applicationadmin.reservation.dto.ReservationGetResponseDto; -import com.nowait.order.entity.UserOrder; -import com.nowait.order.entity.OrderStatus; -import com.nowait.reservation.entity.Reservation; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class OrderResponseDto { - private Long id; - private Long tableId; - private String depositorName; - private Integer totalPrice; - private OrderStatus status; - private LocalDateTime createdAt; - - public static OrderResponseDto fromEntity(UserOrder userOrder) { - return OrderResponseDto.builder() - .id(userOrder.getId()) - .tableId(userOrder.getTableId()) - .depositorName(userOrder.getDepositorName()) - .totalPrice(userOrder.getTotalPrice()) - .status(userOrder.getStatus()) - .createdAt(userOrder.getCreatedAt()) - .build(); - } -} diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java deleted file mode 100644 index a8a78442..00000000 --- a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateRequestDto.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.nowait.applicationadmin.order.dto; - -import com.nowait.order.entity.OrderStatus; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class OrderStatusUpdateRequestDto { - @Schema(description = "주문 상태", example = "WAITING_FOR_PAYMENT", allowableValues = {"WAITING_FOR_PAYMENT", "COOKING", "COOKED"}) - @NotNull(message = "주문상태는 필수입니다") - private final OrderStatus orderStatus; -} diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java deleted file mode 100644 index 63de5b53..00000000 --- a/application-admin/src/main/java/com/nowait/applicationadmin/order/dto/OrderStatusUpdateResponseDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.nowait.applicationadmin.order.dto; - -import com.nowait.order.entity.OrderStatus; -import com.nowait.order.entity.UserOrder; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class OrderStatusUpdateResponseDto { - private final OrderStatus orderStatus; - - public static OrderStatusUpdateResponseDto fromEntity(UserOrder userOrder) { - return new OrderStatusUpdateResponseDto(userOrder.getStatus()); - } -} diff --git a/application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java b/application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java deleted file mode 100644 index c06986cf..00000000 --- a/application-admin/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.nowait.applicationadmin.order.service; - -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.nowait.applicationadmin.order.dto.OrderResponseDto; -import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; -import com.nowait.order.entity.OrderStatus; -import com.nowait.order.entity.UserOrder; -import com.nowait.order.repository.OrderRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class OrderService { - - private final OrderRepository orderRepository; - - @Transactional(readOnly = true) - public List findAllOrders(Long storeId) { - return orderRepository.findAllByStore_StoreId(storeId).stream() - .map(OrderResponseDto::fromEntity) - .collect(Collectors.toList()); - } - - @Transactional - public OrderStatusUpdateResponseDto updateOrderStatus(Long orderId, OrderStatus newStatus) { - UserOrder userOrder = orderRepository.findById(orderId) - .orElseThrow(() -> new IllegalArgumentException("Order not found with id: " + orderId)); - userOrder.updateStatus(newStatus); - return OrderStatusUpdateResponseDto.fromEntity(userOrder); - } -} diff --git a/application-config/build.gradle b/application-config/build.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java b/application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java deleted file mode 100644 index 9c839fbb..00000000 --- a/application-config/src/main/java/com/example/applicationconfig/config/AsyncConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.applicationconfig.config; - -import java.util.concurrent.Executor; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -@Configuration -public class AsyncConfig { - @Bean(name = "s3UploadExecutor") - public Executor s3UploadExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("S3Upload-"); - executor.initialize(); - return executor; - } -} diff --git a/application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java b/application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java deleted file mode 100644 index d8615194..00000000 --- a/application-config/src/main/java/com/example/applicationconfig/config/CorsConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.applicationconfig.config; - -import java.util.List; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -@Configuration -public class CorsConfig { - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - - config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용 - config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정 - config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용 - config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더 - config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정 - // config.setAllowCredentials(true); // 쿠키 포함 허용 - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); //** 뜻은 모든 URL 경로에 적용한다는 의미 - return source; - } -} diff --git a/application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java b/application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java deleted file mode 100644 index 7c6454c0..00000000 --- a/application-config/src/main/java/com/nowait/config/config/SwaggerConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.nowait.config.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; - -@Configuration -public class SwaggerConfig { - - @Bean - public OpenAPI openAPI() { - return new OpenAPI() - .addSecurityItem(new SecurityRequirement().addList("JWT")) - .components(new Components() - .addSecuritySchemes("JWT", new SecurityScheme() - .name("Authorization") - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .in(SecurityScheme.In.HEADER))) - .info(new Info().title("NOWAIT API") - .description("NOWAIT API Specification") - .version("v0.0.1")); - } -} diff --git a/build.gradle b/build.gradle index 03ccdb50..94b2c6d6 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,9 @@ subprojects { apply plugin: "java" apply plugin: 'java-library' apply plugin: "io.spring.dependency-management" - apply plugin: "org.springframework.boot" + if (project.name.startsWith('nowait-app-')) { + apply plugin: "org.springframework.boot" + } repositories { mavenCentral() diff --git a/domain-token/src/main/java/com/example/domaintoken/entity/Token.java b/domain-token/src/main/java/com/example/domaintoken/entity/Token.java deleted file mode 100644 index 3bd47167..00000000 --- a/domain-token/src/main/java/com/example/domaintoken/entity/Token.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.example.domaintoken.entity; - -import java.time.LocalDateTime; - -import com.nowait.user.entity.User; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "token") -@Getter -public class Token { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long tokenId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @Column - private String refreshToken; - - private LocalDateTime expiredDate; - - @Builder - public Token(User user, String refreshToken, LocalDateTime expiredDate) { - this.user = user; - this.refreshToken = refreshToken; - this.expiredDate = expiredDate; - } - - // static method로 객체를 생성 - 생성 의도 파악 쉬웁 - public static Token toEntity(User user, String refreshToken, LocalDateTime expiredDate){ - return Token.builder() - .user(user) - .refreshToken(refreshToken) - .expiredDate(expiredDate) - .build(); - } - -} diff --git a/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java b/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java deleted file mode 100644 index 7fe9c748..00000000 --- a/domain-token/src/main/java/com/example/domaintoken/exception/BusinessException.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.domaintoken.exception; - -import com.nowait.common.exception.ErrorMessage; - -public abstract class BusinessException extends RuntimeException { - private final ErrorMessage errorMessage; - - protected BusinessException(ErrorMessage errorMessage) { - super(errorMessage.getMessage()); - this.errorMessage = errorMessage; - } - - public String getCode() { - return errorMessage.getCode(); - } -} diff --git a/domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java b/domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java deleted file mode 100644 index d813fd09..00000000 --- a/domain-token/src/main/java/com/example/domaintoken/repository/TokenRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.domaintoken.repository; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.example.domaintoken.entity.Token; - -public interface TokenRepository extends JpaRepository { - Optional findByUserId(Long userId); -} diff --git a/infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java b/infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java deleted file mode 100644 index 4eda87c2..00000000 --- a/infra-aws/src/main/java/com/example/infrastorage/config/AwsS3Config.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.infrastorage.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; - -@Configuration -public class AwsS3Config { - - @Value("${cloud.aws.credentials.access-key}") - private String accessKey; - - @Value("${cloud.aws.credentials.secret-key}") - private String secretKey; - - @Value("${cloud.aws.region.static}") - private String region; - - @Bean - public AmazonS3Client amazonS3Client() { - BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); - return (AmazonS3Client)AmazonS3ClientBuilder.standard() - .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .build(); - } - -} diff --git a/infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java b/infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java deleted file mode 100644 index 20d9abf0..00000000 --- a/infra-aws/src/main/java/com/example/infrastorage/s3/S3Service.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.infrastorage.s3; - -import java.io.InputStream; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.ObjectMetadata; - -import io.github.resilience4j.bulkhead.annotation.Bulkhead; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class S3Service { - private final AmazonS3Client amazonS3Client; - - @Value("${cloud.aws.s3.bucket}") - private String bucket; - - public record S3UploadResult(String key, String url) { - } - - @Bulkhead(name = "s3UploadBulkhead", type = Bulkhead.Type.THREADPOOL) - @Async("s3UploadExecutor") - public CompletableFuture upload(String type, Long refId, MultipartFile file) { // TODO MultipartFile 분리 필요 (Spring에 의존하면 안 됨) - try (InputStream inputStream = file.getInputStream()) { - String key = createFileKey(type, refId, file.getOriginalFilename()); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(file.getSize()); - - amazonS3Client.putObject(bucket, key, inputStream, metadata); - String url = amazonS3Client.getUrl(bucket, key).toString(); - - return CompletableFuture.completedFuture(new S3UploadResult(key, url)); - } catch (Exception e) { - throw new RuntimeException("S3 업로드 실패", e); - } - } - - public void delete(String filename) { - try { - amazonS3Client.deleteObject(bucket, filename); - } catch (Exception e) { - throw new RuntimeException("S3 파일 삭제 실패", e); - } - } - - private String createFileKey(String type, Long refId, String filename) { - return type + "/" + refId + "/" + UUID.randomUUID() + "-" + filename; - } -} diff --git a/nowait-app-admin-api/build.gradle b/nowait-app-admin-api/build.gradle index 11db36d5..f6f6a509 100644 --- a/nowait-app-admin-api/build.gradle +++ b/nowait-app-admin-api/build.gradle @@ -2,12 +2,12 @@ plugins { id 'java' } -bootJar { - enabled = true -} jar { enabled = false } +bootJar { + enabled = true +} group = 'com.nowait' version = '0.0.1-SNAPSHOT' @@ -16,10 +16,9 @@ repositories { mavenCentral() } -// api-admin에서 사용하는 도메인, 인프라 모듈만 추가 dependencies { implementation project(':nowait-common') - implementation project(':nowait-infra') // aws 관련 도메인 + implementation project(':nowait-infra') implementation project(':nowait-domain:domain-admin-rdb') implementation project(':nowait-domain:domain-core-rdb') diff --git a/nowait-app-user-api/build.gradle b/nowait-app-user-api/build.gradle index 47adf831..6e3a1302 100644 --- a/nowait-app-user-api/build.gradle +++ b/nowait-app-user-api/build.gradle @@ -2,12 +2,12 @@ plugins { id 'java' } -bootJar { - enabled = true -} jar { enabled = false } +bootJar { + enabled = true +} group = 'com.nowait' version = '0.0.1-SNAPSHOT' @@ -30,7 +30,6 @@ dependencies { // SPRING SECURITY implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.boot:spring-boot-starter-test' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.3' diff --git a/nowait-common/build.gradle b/nowait-common/build.gradle index 41106356..c2f76e1b 100644 --- a/nowait-common/build.gradle +++ b/nowait-common/build.gradle @@ -1,9 +1,6 @@ jar { enabled = true } -bootJar { - enabled = false -} java { toolchain { diff --git a/nowait-domain/build.gradle b/nowait-domain/build.gradle index ebc5b4ae..9ce9a13f 100644 --- a/nowait-domain/build.gradle +++ b/nowait-domain/build.gradle @@ -5,9 +5,6 @@ plugins { jar { enabled = true } -bootJar { - enabled = false -} group = 'com.nowaiting' @@ -32,10 +29,11 @@ dependencies { api 'jakarta.persistence:jakarta.persistence-api:3.1.0' // SPRING SECURITY api 'org.springframework.boot:spring-boot-starter-security' - api 'org.springframework.boot:spring-boot-starter-test' // Lombok (optional) compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok:1.18.26' + // SWAGGER + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/nowait-domain/domain-admin-rdb/build.gradle b/nowait-domain/domain-admin-rdb/build.gradle index 48aa299a..6b1b69c7 100644 --- a/nowait-domain/domain-admin-rdb/build.gradle +++ b/nowait-domain/domain-admin-rdb/build.gradle @@ -5,9 +5,6 @@ plugins { jar { enabled = true } -bootJar { - enabled = false -} group = 'com.nowaiting' diff --git a/nowait-domain/domain-core-rdb/build.gradle b/nowait-domain/domain-core-rdb/build.gradle index 7ea55f16..01bc023b 100644 --- a/nowait-domain/domain-core-rdb/build.gradle +++ b/nowait-domain/domain-core-rdb/build.gradle @@ -5,9 +5,6 @@ plugins { jar { enabled = true } -bootJar { - enabled = false -} group = 'com.nowait' @@ -32,9 +29,11 @@ dependencies { // SPRING SECURITY implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.boot:spring-boot-starter-test' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-validation' + // SWAGGER + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok:1.18.26' diff --git a/nowait-domain/domain-user-rdb/build.gradle b/nowait-domain/domain-user-rdb/build.gradle index 09aa35f3..5a14ae53 100644 --- a/nowait-domain/domain-user-rdb/build.gradle +++ b/nowait-domain/domain-user-rdb/build.gradle @@ -5,9 +5,6 @@ plugins { jar { enabled = true } -bootJar { - enabled = false -} group = 'com.nowaiting' @@ -35,6 +32,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0' + // SWAGGER + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok:1.18.26' diff --git a/nowait-infra/build.gradle b/nowait-infra/build.gradle index aa478c84..5cbc1f17 100644 --- a/nowait-infra/build.gradle +++ b/nowait-infra/build.gradle @@ -5,9 +5,6 @@ plugins { jar { enabled = true } -bootJar { - enabled = false -} group = 'com.nowaiting'