Light Blue Pointer
본문 바로가기
Developing/Journal

[내일배움캠프][팀프로젝트]햇살마켓 프로그램 개발일지

by Greedy 2023. 12. 5.

프로젝트 주소

 

GitHub - minisundev/SunlightMarket

Contribute to minisundev/SunlightMarket development by creating an account on GitHub.

github.com

이름에서 티가 나듯이 유사 당근마켓 프로그램이다!

채팅 기능은 없고 주문 후 승인, 배달 완료 처리를 하는건 배달의민족과도 비슷하다!

프로젝트 하고 나서 느낀점

매 프로젝트마다 내 실력이 늘어나는걸 느낀다!

사실 저번 프로젝트는 스프링을 할 줄 안다 상태였는데

이제 꽤 익숙해져서 자바만큼이나 편하게 느껴진다(자바 베이스긴 한데)

이제는 레고처럼 코드 조립하면서 혼자 놀 수 있는 수준이 된 거 같다 

이번 프로젝트 혼자 코드 짜고 이거저거 시도해보는게 정말 재미있었다!

저번 프로젝트에서 이상한 방식으로 해결했었던 Response에 메세지와 상태 코드 달기를 이번엔 개선된 방식으로 해보고

상수는 Enum을 사용해서 구현하는 것,

수많은 try catch문을 사용하지 않고도 에러를 잡아낼 수 있게 된게 가장 큰 발전이었던 거 같다!

이제 Controller- Service - Repository는 코드는 어디서 복사하지 않아도 자유자재로 작성할 수 있는 수준이 되었다!

 

그리고 Spring security와 인증/인가 처리도 아직은 살짝 복사가 필요하지만 하고 싶은 부분 바꾸면서 쓸 수 있는 수준이 되었다!

이후에 팀 프로젝트라 하지 못했던 부분 내가 개인 프로젝트로 토마토마켓 하면서 해보고 더 공부해보고 싶다!

 

\

2023-12-06

오늘 한 일

햇살마켓(유사 당근마켓 프로그램) 개발하면서 

게시글 CRUD를 하고 Authorization이 넘어오면 로그인된 user정보 이용해서 유저 확인 후 수행하기

 

오늘 알게된 것

⭐ResponseDto에 Getter를 달지 않으면 객체가 Json으로 변환이 안 되는 것 같다

⭐HttpStatus.CREATED → HttpStatusCode 임

⭐HttpStatus.CREATED.value() → int임

⭐db에는 created_at으로 저장되더라도 Entity에는 createdAt으로 써야한다 그렇지 않으면 찾지 못함

⭐제너릭 타입으로 리턴타입을 감싸서 항상 공통적으로 덧붙여지는 정보를 전달해줄 수 있다

 

오늘 개선한 것

💡개선 : return 타입으로 commonresponse돌려보내고 싶어서 responsedto마다 다 상속받던걸 개선해보기

원래 리턴 타입을 ResponseEntity로 하고있었는데 상태메시지도 돌려보내고 싶어짐

repository

public interface ItemRepository extends JpaRepository<Item, Long> {
    List<Item> findAllByOrderByCreatedAtDesc();
}

service

public List<ItemResponseDto> getAllItems() {
        return itemRepository.findAllByOrderByCreatedAtDesc().stream()
                .map(ItemResponseDto::new)
                .collect(Collectors.toList());
    }

controller

//선택 상품 조회
    @GetMapping("/items")
    public ResponseEntity<ItemAllResponseDto> getAllItems(
            @RequestParam String type
    ) {
        try {
            ItemAllResponseDto responseDto = new ItemAllResponseDto();
            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

 

저번 팀플에서는 리턴 타입을 두개로 할 수가 없으니 에러메시지/메시지와 HTTP상태코드번호를 가지는 CommonResponse객체를 만들어서 모든 Response객체에 상속시키고 모든 Response객체에 super(message,statuscode) 생성자를 만들어서 해결했었다

그런데 이번 팀플은 저번 팀플보다 볼륨이 커지고 Response객체도 많아지다 보니까 그런 식으로 하면 일이 엄청 늘어나고 ResponseDto들의 List를 중간에 전달할 일도 많았는데 모든 ResponseDto에 CommonResponse 필드가 다 달려다닌다고 생각하니까 너무 이상했다...

 

⛳해결 : 제너릭 리턴 객체 만들기

https://velog.io/@qotndus43/스프링-API-공통-응답-포맷-개발하기

이 블로그 참고했다!

에러 부분은 내일 좀 더 공부하고 Exception 커스터마이징도 하고싶다

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiResponse<T> {
    
    private String status;
    private String message;
    private T data;
    
    public ApiResponse(String stts, String msg, T data){
        this.status = stts;
        this.message = msg;
        this.data = data;  
    }
}

나는 원래 status code랑 message붙은 commonResponse 돌려보대고 싶은 거여서 저렇게 만들었다

EntityResponse여태까지 <>왜 괄호 안에다 타입 넣어서 쓰는지 궁금했는데 제너릭이었군!!

public class ResponseEntity<T> extends HttpEntity<T> {

	private final HttpStatusCode status;

	/**
	 * Create a {@code ResponseEntity} with a status code only.
	 * @param status the status code
	 */
	public ResponseEntity(HttpStatusCode status) {
		this(null, null, status);
	}

	/**
	 * Create a {@code ResponseEntity} with a body and status code.
	 * @param body the entity body
	 * @param status the status code
	 */
	public ResponseEntity(@Nullable T body, HttpStatusCode status) {
		this(body, null, status);
	}

	/**
	 * Create a {@code ResponseEntity} with headers and a status code.
	 * @param headers the entity headers
	 * @param status the status code
	 */
	public ResponseEntity(MultiValueMap<String, String> headers, HttpStatusCode status) {
		this(null, headers, status);
	}

	/**
	 * Create a {@code ResponseEntity} with a body, headers, and a raw status code.
	 * @param body the entity body
	 * @param headers the entity headers
	 * @param rawStatus the status code value
	 * @since 5.3.2
	 */
	public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, int rawStatus) {
		this(body, headers, HttpStatusCode.valueOf(rawStatus));
	}

가서 보니까 엄청나게 많은 생성자와 빌더가 있었다

 

원래 ResponseEntity.status(HttpStatus.CREATED).body(responseDto); 이렇게 하고 있었는데

⭐HttpStatus.CREATED → HttpStatusCode 임

⭐HttpStatus.CREATED.value() → int임

 

@PostMapping("/add")
    public ApiResponse<ItemResponseDto> addItem(
            @RequestBody ItemRequestDto requestDto
    ) {
        try {
            ItemResponseDto responseDto = itemService.addItem(requestDto);
             ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
            return new ApiResponse<ItemResponseDto>(HttpStatus.CREATED.value(),"아이템 추가 성공했습니다",responseDto);
        }

이렇게 바꾸고

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiResponse<T> {

    private int status;
    private String message;
    private T data;

    public ApiResponse(int status, String message, T data){
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

이렇게 만들었다

 

catch부분도 이렇게 바꾸고

catch (RejectedExecutionException | IllegalArgumentException ex){
            return new ApiResponse<ItemResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }

ApiResponse에다가 메시지랑 int value(status code)만 있는 생성자 추가했다

선택상품 조회 기능도 이렇게 바꿈

//선택 상품 조회
    @GetMapping("/{itemId}")
    public ResponseEntity<ItemResponseDto> getItem(
            @PathVariable Long itemId
    ) {
        try {
            ItemResponseDto responseDto = itemService.getItem(itemId);
            return ResponseEntity.status(HttpStatus.OK).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));

        }
    }

@GetMapping("/{itemId}")
    public ApiResponse<ItemResponseDto> getItem(
            @PathVariable Long itemId
    ) {
        try {
            ItemResponseDto responseDto = itemService.getItem(itemId);
            return new ApiResponse<ItemResponseDto>(HttpStatus.OK.value(),"아이템 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return new ApiResponse<ItemResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

 

 

 

에러 해결

🚩문제 : 

 

2023-12-06T11:34:08.057+09:00 ERROR 17584 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :


APPLICATION FAILED TO START


Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

Action:

Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

⛳해결  : application.properties가 비어있었던거 채워줌

spring.datasource.url=jdbc:mysql://localhost:3306/orderapp
spring.datasource.username=root
spring.datasource.password=58155815
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

저거 넣고나니 빌드 성공했다

//선택 상품 조회
    @GetMapping("/items")
    public ResponseEntity<ItemAllResponseDto> getItems(
            @RequestParam String type
    ) {
        if(type.equals("All")){ return getAllItems();}
        else if(type.equals("Myselect")){return ResponseEntity.badRequest().body(new ItemAllResponseDto("myselect", HttpStatus.BAD_REQUEST.value()));}
        else{return ResponseEntity.badRequest().body(new ItemAllResponseDto("올바르지 않은 요청입니다", HttpStatus.BAD_REQUEST.value()));}
    }

    public ResponseEntity<ItemAllResponseDto> getAllItems()
    {
        try {
            ItemAllResponseDto responseDto = new ItemAllResponseDto();
            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

프론트단 이렇게 바꿔봄

 

🚩문제 : 빈 객체가 옴

localhost:8080/api/items/get?type=All

보내니까

{}

이게 옴

빈 객체가 온거같음

Hibernate: 
    /* <criteria> */ select
        i1_0.id,
        i1_0.address,
        i1_0.completed,
        i1_0.content,
        i1_0.created_at,
        i1_0.delivered,
        i1_0.image,
        i1_0.modified_at,
        i1_0.price,
        i1_0.seller_id,
        i1_0.title 
    from
        items i1_0 
    order by
        i1_0.created_at desc

responseDto.setItemResponseDtos(itemService.getAllItems());

이부분이 안 되고 있는 거 같음

@Setter
@RequiredArgsConstructor
public class ItemAllResponseDto extends CommonResponseDto {
    List<ItemResponseDto> itemResponseDtos;

Setter가 안 되는건가 싶어서 Setter를 만들어봄

public void setItemResponseDtos(List<ItemResponseDto> itemResponseDtos){
        this.itemResponseDtos = itemResponseDtos;
    }
{}

또 저게 옴

읽지 못하는게 Getter가 없어서일 수도 있다고 생각해서

⛳해결 : Getter를 달아봄

⭐Getter를 달지 않으면 객체가 Json으로 변환이 안 되는 것 같다

@Getter
@Setter
@RequiredArgsConstructor
public class ItemAllResponseDto extends CommonResponseDto {
    List<ItemResponseDto> itemResponseDtos;

    public ItemAllResponseDto(String msg, Integer statuscode){
        super(msg,statuscode);
    }
}
{
    "itemResponseDtos": []
}

Getter를 달았더니 이건 나옴 비어있지만

그래서 ItemResponseDto에도 Getter달아봄

{
    "itemResponseDtos": []
}

여전히 이렇게 뜬다

findAll()

Repository findAll해봄

여전히 그렇게 뜸

return itemRepository.findAllByOrderByCreatedAtDesc().stream()
                .map(ItemResponseDto::new)
                .collect(Collectors.toList());

이부분이 뭔가 잘못됐나?

생성자로 responseDto만들수 있어야 하나 근데 있는데

public ResponseEntity<ItemAllResponseDto> getAllItems()
    {
        try {
            ItemAllResponseDto responseDto = new ItemAllResponseDto();
            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

일단 이걸 바꿔봄

제대로 오나 보게

@GetMapping("/get")
    public ResponseEntity<List<ItemResponseDto>> getItems(
            @RequestParam String type
    ) {
        if(type.equals("All")){ return getAllItems();}
        //else if(type.equals("Myselect")){return ResponseEntity.badRequest().body(new ItemAllResponseDto("myselect", HttpStatus.BAD_REQUEST.value()));}
        //else{return ResponseEntity.badRequest().body(new ItemAllResponseDto("올바르지 않은 요청입니다", HttpStatus.BAD_REQUEST.value()));}
        return null;
    }

    public ResponseEntity<List<ItemResponseDto>> getAllItems()
    {
//        try {
//            ItemAllResponseDto responseDto = new ItemAllResponseDto();
//            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(itemService.getAllItems());
//        }catch (RejectedExecutionException | IllegalArgumentException ex){
//            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
//        }
    }
[]

이렇게 옴 안 찾아지는게 맞는거같음

 

⛳ 해결 : application.properties에 db이름 잘못 쓴 거 고침

spring.datasource.url=jdbc:mysql://localhost:3306/orderapp

 

ㅎ혹시나 해서 db연결 확인해보니 이걸 잘못써놨음

spring.datasource.url=jdbc:mysql://localhost:3306/sunlightmarket

이렇게 수정함 

{
    "itemResponseDtos": [
        {
            "seller_id": 1,
            "title": "제목제목",
            "image": "src",
            "price": "2,000",
            "address": "내머릿속",
            "content": "내용내용",
            "created_at": "2023-12-05T22:23:35.837991",
            "modified_at": "2023-12-05T22:23:35.837991"
        },
        {
            "seller_id": 1,
            "title": "제목제목",
            "created_at": "2023-12-05T22:19:30.19611",
            "modified_at": "2023-12-05T22:19:30.19611"
        }
    ]
}

잘 뜸

 

Get item 기능 개발

Controller

@GetMapping("/{itemId}")
    public ResponseEntity<ItemResponseDto> getItem(
            @PathVariable Long itemId
    ) {
        try {
            ItemResponseDto responseDto = itemService.getItem(itemId);
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));

        }
    }

Service

public ItemResponseDto getItem(Long itemId){
        Item item = itemRepository.findById(itemId).orElse(null);
        return new ItemResponseDto(item);
    }

Repository

Optional<Item> findById(Long itemId);

처음에는 repository에서 그냥 Item findByID(Long itemId);

하고싶었는데 빨간줄이 뜨면서 Optional로 만들라고 함

Q. 왜? →

근데 저번에는 그냥 Item처럼 만들었었는데 …

Creating an Object from Optional<Object>

findFirst() gives you an Optional and you then have to decide what to do if it's not present. So findFirst().orElse(null) should give you the object or null if it's not present

<Optional>Item으로 오는거 처리하기

Item item = itemRepository.findById(itemId).orElse(null);

잘 뜸

나중에 저 코드는

Item item = itemRepository.findById(itemId).orElseThrow(NullPointerException::new);

이렇게 수정함

 

{
    "seller_id": 1,
    "title": "제목제목",
    "image": "src",
    "price": "2,000",
    "address": "내머릿속",
    "content": "내용내용",
    "created_at": "2023-12-05T22:23:35.837991",
    "modified_at": "2023-12-05T22:23:35.837991"
}

 

 

 

 

🚩문제 : 그러고 실행시켰는데 자꾸 autogenerated password뜨고 401 unauthorized라고 응답이 옴

⛳해결 : build.gradle에 security 주석처리한거 빌드 다시했더니 해결됨

 

🚩문제  : 팀원한테 kakao_id 필드를 찾을수 없다는 에러가 자꾸 떴음

⛳해결  : kakao_id → kakaoId

로 했더니 됨

private LocalDateTime createdAt;

⭐db에는 created_at이지만 entity에는 _넣으면 안되고 createdAt을 넣어야 함

 

 

🚩문제 : Update가 안 됨

localhost:8080/api/items/4

update를 하면 403forbidden이 뜸…

@PutMapping("/items/{itemsId}")

put을 patch로 바꿔봄

그래도 또 뜸 403 forbidden이

private Item getUserItem(Long itemId, User user){
        Item item = itemRepository.findById(itemId).orElseThrow(NullPointerException::new);
//        if(!item.getUser().getId().equals(user.getId())){
//            throw new RejectedExecutionException("작성자만 수정할 수 있습니다.");
//        }
        return item;
    }

이부분 주석처리하고 테스트해봄

그래도 403으로 안됨

Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PUT' is not supported]

이는 해당 HttpMethod를 mapping하는 Controller가 존재하지 않아서 발생한 에러였다. 컨트롤러에 PUT으로 받는 메소드를 추가해 문제를 해결할 수 있었다.

근데 나는 버젓이 있는데요 ??

@PutMapping("/items/{itemsId}")

아 주소가 틀려도 저렇게 나오는 거였음

items를 또 하면 안됨

@PutMapping("/{itemsId}")

이렇게 수정함

 

Resolved [org.springframework.web.bind.MissingPathVariableException: Required URI template variable 'itemId' for method parameter type Long is not present]

ㅋㅋ

localhost:8080/api/items/1 으로 get 하는것도 안 되기 시작함 ㅋㅋㅋㅋㅋㅋ

조회는 왜 되고 update는 왜 path를 못 찾지?

Resolved [org.springframework.web.bind.MissingPathVariableException: Required URI template variable 'itemId' for method parameter type Long is not present]

Spring MVC Missing URI template variable

@PathVariable is used to tell Spring that part of the URI path is a value you want passed to your method. Is this what you want, or are the variables supposed to be form data posted to the URI?

If you want form data, use @RequestParam instead of @PathVariable.

If you want @PathVariable, you need to specify placeholders in the @RequestMapping entry to tell Spring where the path variables fit in the URI. For example, if you want to extract a path variable called contentId, you would use:

@RequestMapping(value = "/whatever/{contentId}", method = RequestMethod.POST)

 

⛳해결 : @RequestParam으로 바꾸니까 됨

Q.대체 왜???

localhost:8080/api/items?id=1

 

 

 

🚩문제 : 게시글 조회시 로그인을 안 하면 조회가 안 됨

로그인 정보가 없어도 게시글 조회가 되게 하고싶음

⛳해결 : WebSecurityConfig에서 인증 안 거쳐도 갈 수 있도록 path 걸러줌

httpSecurity.authorizeHttpRequests(
            (authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll()
                .requestMatchers("/api/users/**").permitAll()
                    **.requestMatchers("/api/items/read/**").permitAll()**
                .anyRequest().authenticated());

items/read로 시작하는 parh로 오는 모든 요청은 인증처리 안 하게 함

 

WebSecurityConfig

package com.raincloud.sunlightmarket.global.config;

import com.raincloud.sunlightmarket.global.jwt.JwtUtil;
import com.raincloud.sunlightmarket.global.security.JwtAuthenticationFilter;
import com.raincloud.sunlightmarket.global.security.JwtAuthorizationFilter;
import com.raincloud.sunlightmarket.global.security.UserDetailsServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration configuration;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
        throws Exception {

        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(configuration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthentizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf((csrf) -> csrf.disable());

        httpSecurity.sessionManagement(
            (sessionManagemnet) -> sessionManagemnet
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        httpSecurity.authorizeHttpRequests(
            (authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll()
                .requestMatchers("/api/users/**").permitAll()
                    .requestMatchers("/api/items/read/**").permitAll()
                .anyRequest().authenticated());

        httpSecurity
            .addFilterBefore(jwtAuthentizationFilter(), JwtAuthenticationFilter.class);
        httpSecurity
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

}

 

2023-12-07

오늘도 유사 당근마켓(햇살마켓) 개발을 계속 했다

1. User → Seller로 변경

UserId를 Item에 넣어두고 있었는데 판매자 프로필과 구매자 프로필을 따로따로 하기로 해서 Item에는 User대신 Seller를 넣었다

2. 주문(구매 요청) 기능 만들기 주문 CRUD는 오늘 다 했다!

 

내일 할 일

주문 승인처리, 배송 완료처리

 

1. User → Seller로 변경

@ManyToOne
    @JoinColumn(name = "seller_id")
    private Seller seller;

전에 User였던 것을 Seller로 바꾸니까 그냥 됐음

 

2. 주문(구매 요청) 기능 만들기

2-1. Order Entity를 생성함

@Getter
@Entity
@Table(name = "orders")
public class Order {
    private final int ORDER_PENDING = 1;
    private final int ORDER_ACCEPTED = 2;
    private final int ORDER_REJECTED = 3;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JoinColumn(name = "item_id")
    private Item item;

    @JoinColumn(name = "buyer_id")
    private Buyer buyer;

    private int orderStatus;

}

Order의 진행 상황을 어떻게 표시할지 생각하다가 상태 코드를 만들고 그걸 상수로 만들어서 상태 업데이트 함수로 orderStatus에 값 넣어주기로 함

아 근데 나는 Column 안 붙이면 db에 안 들어가는줄 알고

이래놨는데 db에 order_accepted, order_pending, order_rejected가 다 들어가있음ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

public class Order extends Timestamped {
//    private final String  ORDER_PENDING = "Pending";
//    private final String ORDER_ACCEPTED = "Accepted";
//    private final String ORDER_REJECTED = "Rejected";

그래서 그냥 삭제하고 String값으로 함 내일 Enum을 공부하고 다시 해보기로 했음

 

⛳문제 : 그렇게 하고 나니까 Item에 오류가 뜸

basic attribute type should not be ‘attribute entity’

🚩해결 : @ManyToOne 달아주니까 사라졌다

item이 삭제될때 그거에 딸린 orders도 삭제되어야 하니까 cascade를 Item에다 달아줌

@OneToMany
    @JoinColumn(name = "item_id")
    private List<Order> orders;
@OneToMany(mappedBy = "item",cascade = CascadeType.PERSIST,orphanRemoval = true)
    @JoinColumn(name = "item_id")
    private List<Order> orders;

이렇게 만들어줌!

item이 삭제되면 거기에 딸린 order들도 삭제되도록

 

🚩문제 : 실행이 안 됨

with name 'webSecurityConfig' defined in file [E:\Workspace\Temp\SunlightMarket\build\classes\java\main\com\raincloud\sunlightmarket\global\config\WebSecurityConfig.class]: Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'userDetailsServiceImpl' defined in file [E:\Workspace\Temp\SunlightMarket\build\classes\java\main\com\raincloud\sunlightmarket\global\security\UserDetailsServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'userRepository' defined in com.raincloud.sunlightmarket.user.repository.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager’

Cannot resolve reference to bean 'jpaSharedEM_entityManagerFactory' while setting bean property 'entityManager’

구글링함

Exception says that there is no bean with name entityManagerFactory, Place a debug point in

public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder factoryBuilder)

method org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration.class and check why the bean is not getting initiated.

Oh, From discussion I came to the conclusion that mentioned issue is due to hibernate older dependencies. Also, the @Id which was imported needs to be from javax.persistence.id for all your entities.

Association 'com.raincloud.sunlightmarket.item.entity.Item.orders' is 'mappedBy' another entity and may not specify the '@JoinColumn' at org.hibernate.boot.model.internal.CollectionBinder.detectMappedByProblem(CollectionBinder.java:1239) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.CollectionBinder.bind(CollectionBinder.java:1154) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.CollectionBinder.bindCollection(CollectionBinder.java:353) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.PropertyBinder.bindProperty(PropertyBinder.java:873) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.PropertyBinder.buildProperty(PropertyBinder.java:787) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations(PropertyBinder.java:708) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.EntityBinder.processIdPropertiesIfNotAlready(EntityBinder.java:967) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.EntityBinder.handleIdentifier(EntityBinder.java:302) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.EntityBinder.bindEntityClass(EntityBinder.java:228) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.internal.AnnotationBinder.bindClass(AnnotationBinder.java:417) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.source.internal.annotations.AnnotationMetadataSourceProcessorImpl.processEntityHierarchies(AnnotationMetadataSourceProcessorImpl.java:255) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.process.spi.MetadataBuildingProcess$1.processEntityHierarchies(MetadataBuildingProcess.java:278) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:321) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1383) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1454) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final]

 

⛳해결 : 

@OneToMany(mappedBy = "item",cascade = CascadeType.PERSIST,orphanRemoval = true)
@JoinColumn(name = "item_id")

@OneToMany(mappedBy = "item",cascade = CascadeType.PERSIST,orphanRemoval = true)
joincolumn 삭제

 

 

🚩문제 : references items (item_id)" via JDBC [Failed to add the foreign key constraint. Missing column 'item_id' for constraint 'FK247nnxschdfm8lre0ssvy3k1r' in the referenced table 'items']

alter table orders add constraint FK247nnxschdfm8lre0ssvy3k1r foreign key (item_id) references items (item_id)" via JDBC [Failed to add the foreign key constraint. Missing column 'item_id' for constraint 'FK247nnxschdfm8lre0ssvy3k1r' in the referenced table 'items']

@Table(name = "items")
@NoArgsConstructor
public class Item extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_id")
    private Long id;

⛳해결 : name = item_id도 삭제해봄

그러니까 됨

Q. 왜?

 

🚩문제 : 

localhost:8080/api/orders/add?itemId=1

403 인증문제 아니라고 함

ERROR 31240 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.NullPointerException: Cannot invoke "com.raincloud.sunlightmarket.order.service.OrderService.addOrder(com.raincloud.sunlightmarket.order.dto.OrderRequestDto, java.lang.Long, com.raincloud.sunlightmarket.user.entity.User)" because "this.orderService" is null] with root cause

public class OrderController { private OrderService orderService;

이게 회색으로 뜨고 autowired가 안 됨 왜지…

public OrderController(OrderService orderService){
        this.orderService = orderService;
    }

이렇게 넣어주니까 회색이 보라색 됨

ERROR 22552 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.NullPointerException: Cannot invoke "com.raincloud.sunlightmarket.order.service.OrderService.addOrder(com.raincloud.sunlightmarket.order.dto.OrderRequestDto, java.lang.Long, com.raincloud.sunlightmarket.user.entity.User)" because "this.orderService" is null] with root cause

java.lang.NullPointerException: Cannot invoke "com.raincloud.sunlightmarket.order.service.OrderService.addOrder(com.raincloud.sunlightmarket.order.dto.OrderRequestDto, java.lang.Long, com.raincloud.sunlightmarket.user.entity.User)" because "this.orderService" is null

근데 또 안들어가네 뭐지

 

⛳해결 : final 추가했더니 보라색 되고 에러 사라지고 잘 동작함

private OrderService orderService → private final OrderService orderService;

 

Q. 왜죠?

{
    "status": 201,
    "message": "구매 요청 성공했습니다",
    "data": {
        "buyerName": null,
        "address": "주소주소",
        "orderStatus": "Pending"
    }
}

 

🚩문제 : 2023-12-07T17:13:03.025+09:00 WARN 30216 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'text/plain;charset=UTF-8' is not supported]

와 근데 희한한게 item 1에는 order가 들어가지고 item 2에는 order이 안 들어가짐

localhost:8080/api/orders/add?itemId=1

{
    "status": 201,
    "message": "구매 요청 성공했습니다",
    "data": {
        "buyerName": "닉",
        "address": "주소주소",
        "orderStatus": "PENDING"
    }
}
localhost:8080/api/orders/add?itemId=2

403forbidden

Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'text/plain;charset=UTF-8' is not supported]

이 창에서 1로 보내면 또 안 들어감

아까 되던 창에서 2로 보내면 그거는 또 들어감

⛳해결 :  포스트맨에서 JSON이 아니라 text로 보내고 있었음, JSON 으로 보내니까 잘 됨

 

 

근데 본인이 본인의 글에다가 댓글을 달 수 있으면 안될 거 같음

private Item getNotUserItemById(Long itemId, User user){
        Item item = itemRepository.findById(itemId).orElseThrow(NullPointerException::new);
        Seller seller = item.getSeller();
        User user = getUserBySeller(seller);
    }

 

💡개선 : 바보같이 Join한 Column을 findBy ... 만들어서 가져오던거 그냥 가져오게 수정함

private User getUserBySeller(Seller seller){
        User user = userRepository.findBySellerId(seller.getId()).orElseThrow(NullPointerException::new);
        return user;
    }

이렇게 하고 있다가 깨달음 그냥 가져올 수 있는 거 같은데…?

item.getSeller().getUser();

itemService에서

public ItemResponseDto addItem(ItemRequestDto requestDto, User user) {
        Seller seller = getSellerByUser(user);

...

private Seller getSellerByUser(User user){
        Seller seller = sellerRepository.findByUserId(user.getId()).orElseThrow(NullPointerException::new);
        return seller;
    }

이러고 있었던거 그냥

public ItemResponseDto addItem(ItemRequestDto requestDto, User user) {
        Seller seller = user.getSeller();

이렇게 다 수정함

 

 

💡발견 !! : ApiResponse로 감싸고 에러 날렸더니

{
    "status": 400,
    "message": "작성자는 구매 요청을 할 수 없습니다.",
    "data": null
}

이렇게 리턴값이 옴

→ 위에서 RejectedError에 상태메시지 넣어서 날렸더니

controller단에서 RejectedError를 catch로 받아서 에러메시지가 출력되는듯 !!

어떻게 할지 고민하고 있었는데 좋았다

NullPointerException도 상태메시지 넣어서 Catch해 버리면 될듯

 

🚩문제 : .orElseThrow(NullPointerException::new); 저 안에 상태 메시지를 넣은 NullPointerException을 생성할 줄 모름

⛳해결 : 저 :: 이 부분이 람다식이라는 것을 알고 나서 .orElseThrow(()-> new NullPointerException("해당 id로 아이템을 찾을 수 없습니다."))); 이렇게 함

 

Controller--------
@PostMapping("/add")
    public ApiResponse<OrderResponseDto> addOrder(
            @RequestBody OrderRequestDto requestDto,
            @RequestParam Long itemId,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try {
            OrderResponseDto responseDto = orderService.addOrder(requestDto,itemId, userDetails.getUser());
            return new ApiResponse<OrderResponseDto>(HttpStatus.CREATED.value(),"구매 요청 성공했습니다",responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return new ApiResponse<OrderResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }
Service----------
public OrderResponseDto addOrder(OrderRequestDto requestDto, Long itemId , User user)
    {
        Item item  = getNotUserItemById(itemId,user);
        Buyer buyer = user.getBuyer();
        Order order = new Order(requestDto,item,buyer);
        orderRepository.save(order);
        return new OrderResponseDto(order);
    }
private Item getNotUserItemById(Long itemId, User user){
        Item item = itemRepository.findById(itemId).orElseThrow(Item item = itemRepository.findById(itemId).orElseThrow(()-> new NullPointerException("해당 id로 아이템을 찾을 수 없습니다.")););
        User userFound = item.getSeller().getUser();
        if(userFound.getId().equals(user.getId())){
            throw new RejectedExecutionException("작성자는 구매 요청을 할 수 없습니다.");
        }
        return item;
    }
Result-------------
{
    "status": 400,
    "message": "작성자는 구매 요청을 할 수 없습니다.",
    "data": null
}

 

저렇게 하니까 NullPointerException도 받아서 ApiResponse로 상태메시지 출력되어짐!!

 

 

💡개선 : 아이템(판매글)작성자가 댓글(구매요청)을 달지 못하게 하기 위해서

public OrderResponseDto addOrder(OrderRequestDto requestDto, Long itemId , User user)
    {
        Item item  = getNotUserItemById(itemId,user);
        Buyer buyer = getBuyerByUser(user);
        Order order = new Order(requestDto,item,buyer);
        orderRepository.save(order);
        return new OrderResponseDto(order);
    }
private Item getItemById(Long itemId){
         Item item = itemRepository.findById(itemId).orElseThrow(NullPointerException::new);
        return item;
    }

private Item getNotUserItemById(Long itemId, User user){
        Item item = itemRepository.findById(itemId).orElseThrow(NullPointerException::new);
        User userFound = item.getSeller().getUser();
        if(userFound.getId().equals(user.getId())){
            throw new RejectedExecutionException("작성자는 구매 요청을 할 수 없습니다.");
        }
        return item;
    }

이렇게 수정함

 

그러고 나니까 자기 Item 조회할때는 그 Item에 들어온 요청목록을 보이게 하고 싶어졌음

남의 Item을 볼 때나 비로그인시에는 안 보이고

나는 Filter에서 조회하는 부분을 걸러버렸는데 저 부분을 어떻게 구현할지 생각해 보다가

혹시 method주소랑 path가 같아도 parameter가 다르면 오버라이딩 되나 싶어짐

근데 controller로 요청이 들어오기 전에 filter에서 걸러버린다는데 로그인을 안 했는데 Authentication이 되나..?

아니다 남들도 요청을 볼 수 있으면 되나..?

try {
            ItemAllResponseDto responseDto = new ItemAllResponseDto();
            responseDto.setItemResponseDtos(itemService.getAllItems());
            return new ApiResponse<ItemAllResponseDto>(HttpStatus.OK.value(),"아이템 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException | NullPointerException ex){
            return new ApiResponse<ItemAllResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }

Controller

@GetMapping("/read")
    public ApiResponse<List<OrderResponseDto>> getOrdersForAll(
            @RequestParam Long itemId
    ) {
        try {
            List<OrderResponseDto> responseDto = orderService.getOrders(itemId);
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.OK.value(),"구매 요청 성공했습니다",responseDto);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

Service

public List<OrderResponseDto> getOrders(Long itemId){
        return orderRepository.findAllByItemId(itemId).stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }

 

⛳문제 : 웃긴게 1개만 들어있는 Order는 반환이 되는데

한 Item에 Order이 2개 이상 들어있으면 안 됨

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.IncorrectResultSizeDataAccessException: query did not return a unique result: 2] with root cause

Item은 어케한거야

jakarta.persistence.NonUniqueResultException: query did not return a unique result: 2 at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:128) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final] at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:482) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final]

The error message says it all. findByDescription() is supposed to return 0 or 1 DocumentType, but it returns several ones. So either your Java design, or your data is corrupted. If you're really supposed to have a single document type with a given description, there should be a unique constraint in the database, that would prevent you from breaking that rule.

List<Item> findAllByOrderByModifiedAtDesc();

Item은 이렇게 함

앗 바본가 orderRepository에 이렇게 해놨음

당연히 하나밖에 리턴이 안되겠죠..

Optional <Order> findAllByItemId(Long itemId);

⛳해결 : Optional<Order> -> List<Order>로 수정함

 

💡개선 : List<Order>도 Optional로 감싸서 null일때 에러 throw하고 싶어짐

public List<OrderResponseDto> getOrders(Long itemId){
        return orderRepository.findAllByItemId(itemId)
                .stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }

근데 이것도 못찾거나 null일때 에러 리턴하고 싶은데…

Optional로 감싸봄

Optional<List<Order>> findAllByItemId(Long itemId);

여기는 이렇게 해봄

public List<OrderResponseDto> getOrders(Long itemId){
        return orderRepository.findAllByItemId(itemId).orElseThrow(()-> new NullPointerException("해당 id로 주문요청을 찾을 수 없습니다."))
                .stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }

잘 됨

 

 

🚩문제 : Order update기능 구현하고 수정할 request를 보내니까 백지가 뜸

Error도 안 잡힘 

⛳해결 : 헤더에 Authorization안 넣은 거였어서 저거 넣어줌

 

🚩문제 : 이제 response는 오는데 db가 안 바뀜

"message": "구매 요청 업데이트 성공했습니다",
    "data": {
        "orderId": 5,
        "buyerName": "닉2",
        "address": "수정수정",
        "price": "가격가격",
        "orderStatus": "PENDING"
    }
}

아 뭔가

Transaction안 달아놓은 거 같음

Service단의 Update함수에는 @Transactional

이걸 꼭 달아줘야 함

Q. 저걸 왜 달아줘야 하더라…

 해결 : Service의 update method에 @Transaction 달아주니까 잘 됐음

저거 달아주니까 이제 됐다

 

🎪하고싶은 것 : 로그인했을때/ 로그인 안 했을때 path는 같고 method명이랑 variable은 다르게 메서드들 오버로딩 하는 것!

Controller

@GetMapping("/read")
    public ApiResponse<List<OrderResponseDto>> getOrdersForAll(
            @RequestParam Long itemId
    ) {
        try {
            List<OrderResponseDto> responseDto = orderService.getOrders(itemId);
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.OK.value(),"구매 요청 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

    @GetMapping("/read")
    public ApiResponse<List<OrderResponseDto>> getOrdersForItemAuthor(
            @RequestParam Long itemId,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try {
            List<OrderResponseDto> responseDto = orderService.getOrdersForUsers(itemId, userDetails.getUser());
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.OK.value(),"구매 요청 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

Service에서 아이템의 작성자면 order에 id랑 address출력해주고

아니면 저거 빼고 출력하는게 하고싶음

public List<OrderResponseDto> getOrdersForUsers(Long itemId, User user){
        List<Order> orders = orderRepository.findAllByItemId(itemId).orElseThrow(()-> new NullPointerException("해당 id로 구매요청을 찾을 수 없습니다."));
        if(orders.get(0).getItem().getSeller().equals(user.getSeller())){
            return orders.stream()
                    .map(OrderResponseDto::new)
                    .collect(Collectors.toList());
        }else {

        }
    }

 

🚩문제 : 근데 상황에 따라서 리턴 타입이 달라야됨

OrderResponseDto에는 orderid랑 address가 표시되고

저게 약간 바깥에 보여주기 싫은 정보나 개인정보일 수 있으니

PublicOrderResponseDto에는 orderid랑 address를 뺀 price랑 nickname이런것만 출력됨

상황에 따라서 리턴 타입이 다른걸 대체 어떻게 하냐 싶었음

 

⛳해결 : 그래서 데이터타입이 두개 있는 리턴 타입 클래스를 제너릭으로 만들어봄

package com.raincloud.sunlightmarket.global.dto;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DoubleResponse<T,U> {
    
    private T data1;
    private U data2;

    public DoubleResponse(T data1, U data2){
        this.data1 = data1;
        this.data2 = data2;
    }
}
public DoubleResponse<List<OrderResponseDto>,List<PublicOrderResponseDto>> getOrdersForUsers(Long itemId, User user){
        List<Order> orders = orderRepository.findAllByItemId(itemId).orElseThrow(()-> new NullPointerException("해당 id로 구매요청을 찾을 수 없습니다."));
        List<OrderResponseDto> orderDtos = null;
        List<PublicOrderResponseDto> publicOrderDtos = null;
        if(orders.get(0).getItem().getSeller().equals(user.getSeller())){//Item의 작성자가 볼때는 orderId와 address가 포함된 response리턴
            orderDtos = orders.stream()
                    .map(OrderResponseDto::new)
                    .collect(Collectors.toList());
        }else {//Item의 작성자가 아닌 유저가 볼때는 orderId와 address가 제외된 response리턴
            publicOrderDtos = orders.stream()
                    .map(PublicOrderResponseDto::new)
                    .collect(Collectors.toList());
        }
        return new DoubleResponse(orderDtos,publicOrderDtos);
    }

Controller

@GetMapping("/read")
    public ApiResponse<DoubleResponse<List<OrderResponseDto>,List<PublicOrderResponseDto>>> getOrdersForItemAuthor(
            @RequestParam Long itemId,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try {
            DoubleResponse<List<OrderResponseDto>,List<PublicOrderResponseDto>> responseDto = orderService.getOrdersForUsers(itemId, userDetails.getUser());
            return new ApiResponse<DoubleResponse<List<OrderResponseDto>,List<PublicOrderResponseDto>>>(HttpStatus.OK.value(),"구매 요청 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<DoubleResponse<List<OrderResponseDto>,List<PublicOrderResponseDto>>>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

기적의 3단 제너릭 ㅋㅋㅋㅋㅋ 넘 웃기다

 

🚩문제 : path 오버로딩이 안 됨

com.raincloud.sunlightmarket.order.controller.OrderController#getOrdersForItemAuthor(Long, UserDetailsImpl) to {GET [/api/orders/read]}: There is already 'orderController' bean method com.raincloud.sunlightmarket.order.controller.OrderController#getOrdersForAll(Long) mapped.

orderController에서 오류남 따흑

com.raincloud.sunlightmarket.order.controller.OrderController#getOrdersForAll(Long) to {GET [/api/orders/read]}: There is already 'orderController' bean method com.raincloud.sunlightmarket.order.controller.OrderController#getOrdersForUsers(Long, UserDetailsImpl) mapped.

path 오버로딩이 안되나봄…

method parameter가 Long으로 똑같아서 그런가…

다른 방식 한번 써봄

⛳해결 : 같은 path를 공유하는 다른 메소드의 parameter를 PathVariable로 바꿔봄

@GetMapping("/read")
    public ApiResponse<List<OrderResponseDto>> getOrdersForAll(
            @RequestParam Long itemId
    ) {
        try {
            List<OrderResponseDto> responseDto = orderService.getOrders(itemId);
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.OK.value(),"구매 요청 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

@GetMapping("/read/{itenId}")
    public ApiResponse<List<OrderResponseDto>> getOrdersForAll(
            @PathVariable Long itemId
    ) {
        try {
            List<OrderResponseDto> responseDto = orderService.getOrders(itemId);
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.OK.value(),"구매 요청 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

오… 이렇게는 오버로딩이 되나봄 문제없이 동작됨

Authentication은 parameter로 안 쳐주나봄…

근데 생각해보니 저러면 path가 걍 달라지잖아.. 오버로딩이라고 볼 수 없으니 되는듯

같은 path에다가는 두개의 함수를 달 수 없는걸까?

 

 

🚩문제 : Item글쓴이어도 orderId랑 address가 보이는 response가 리턴되지 않음

아무튼 유저 1로 로그인해서 아이템의 Order들을 읽어봄

"status": 200,
    "message": "구매 요청 조회에 성공했습니다",
    "data": {
        "data1": null,
        "data2": [
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            },
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            }
        ]
    }
}

유저 2로 로그인해서 읽어봄

{
    "status": 200,
    "message": "구매 요청 조회에 성공했습니다",
    "data": {
        "data1": null,
        "data2": [
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            },
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            }
        ]
    }
}

똑같으면 안되는데…

아 read밑에다 놔두면 filter를 거쳐서 @AuthenticationPrincipal UserDetailsImpl userDetails 로 User 정보가 안 들어오기 때문에

그냥 public만 리턴되는 걸수도 있겠다…

 

WebSecurityConfig에서 /orders/read/**는 인증없이 다 지나가게 필터에 처리해뒀으니 path를 /read말고 다른걸로 바꿔봄

유저 1로 로그인해봄

{
    "status": 200,
    "message": "구매 요청 조회에 성공했습니다",
    "data": {
        "data1": null,
        "data2": [
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            },
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            }
        ]
    }
}

유저 2로 로그인해봄 안 됨

{
    "status": 200,
    "message": "구매 요청 조회에 성공했습니다",
    "data": {
        "data1": null,
        "data2": [
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            },
            {
                "buyerName": "닉",
                "price": null,
                "orderStatus": "PENDING"
            }
        ]
    }
}

 

⛳해결 : 해당 유저인지 판별할때 Object끼리 equals하지 않고 String끼리 equals하게 함

if(orders.get(0).getItem().getSeller().equals(user.getSeller()))

if(orders.get(0).getItem().getSeller().getId().equals(user.getSeller().getId()))

지금 보니 id가 아니라 seller로 비교하고 있었음

{
    "status": 200,
    "message": "구매 요청 조회에 성공했습니다",
    "data": {
        "data1": [
            {
                "orderId": 2,
                "buyerName": "닉",
                "address": "주소주소2",
                "price": null,
                "orderStatus": "PENDING"
            },
            {
                "orderId": 3,
                "buyerName": "닉",
                "address": "주소주소2",
                "price": null,
                "orderStatus": "PENDING"
            }
        ],
        "data2": null
    }
}

오 Seller대신 seller_id로 비교하니까 됨

filter로 거르지 않아도 로그인 정보 없어도 메소드 안에서 돌아가게 하는 법은 없는지 내일 알아보기로 함

 

2023-12-12

오늘 한 일

주문 승인처리, 배달 완료처리

Response타입 개선, 에러 처리, Price 형식 지정,

내가 찜한 아이템 보기 기능 구현, 로그아웃 기능 구현

 

오늘 개발하는데 레고 조립하는거같고 뭔가 엄청 재밌었다 ㅋㅋㅋㅋㅋㅋ

어떤건 글쓴이가 가능하면 안되고 어떤건 댓글 단 사람(주문 신청한 사람)만 가능해야 하고

어떤건 글쓴이만 가능해야 하고

구분하면서 코드짜는게 너무 재밌었음 ㅋㅋㅋ

로직 개발이 제일 재밌는 파트인 거 같다

 

 

요청 승인할때

이미 rejected/confirmed 된 거는 이제 변하면 안 되니까 boolean도 주고싶음

-> boolean으로 했다가 나중에 Status Enum으로 또 바꿨다

public void reject(){
        if(this.completed == true){
            throw new RejectedExecutionException("이미 처리된 요청입니다");
        }
        this.orderStatus = "REJECTED";
        this.completed = true;
    }

이렇게 해봄

유저 2로 거절해봄

{
    "status": 400,
    "message": "아이템 작성자만 구매요청을 승인/거절할 수 있습니다.",
    "data": null
}

유저 1로 거절해봄

한번 거절해봄

{
    "status": 200,
    "message": "구매 요청 거절 성공했습니다",
    "data": {
        "orderId": 1,
        "buyerName": "닉2",
        "address": "주소주소",
        "price": "가격가격",
        "orderStatus": "REJECTED"
    }
}

두번 거절해봄

{
    "status": 400,
    "message": "이미 처리된 요청입니다",
    "data": null
}

승인할때

item.complete();
order.confirm();

이 순서로 달아서

public void complete(){
        if(this.completed == true){
            throw new RejectedExecutionException("이미 주문이 완료된 아이템입니다");
        }
        this.completed = true;
    }

item이 이미 completed된 아이템이면 승인 못 하게 함!

{
    "status": 400,
    "message": "이미 주문이 완료된 아이템입니다",
    "data": null
}

잘 뜸

 

 

myorders신나게 만들었는데 에러뜸

@GetMapping("/myorders")
    public ApiResponse<List<OrderResponseDto>> getMyOrders(
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try {
            List<OrderResponseDto> responseDtos = orderService.getMyOrders(userDetails.getUser());
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.OK.value(),"구매 요청 조회에 성공했습니다",responseDtos);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<List<OrderResponseDto>>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }
public List<OrderResponseDto> getMyOrders(User user){
        return orderRepository.findAllByUserId(user.getId()).orElseThrow(()-> new NullPointerException("주문 요청이 존재하지 않습니다"))
                .stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }

java.util.Optional com.raincloud.sunlightmarket.order.repository.OrderRepository.findAllByUserId(java.lang.Long); Reason: Failed to create query for method public abstract java.util.Optional com.raincloud.sunlightmarket.order.repository.OrderRepository.findAllByUserId(java.lang.Long); No property 'userId' found for type 'Order’

쿼리가 잘못되었나봄

private Buyer buyer;

지금 보니 Order에는 User가 아니라 Buyer가 들어있었음

public List<OrderResponseDto> getMyOrders(User user){
        return orderRepository.findAllByBuyerId(user.getBuyer().getId()).orElseThrow(()-> new NullPointerException("주문 요청이 존재하지 않습니다"))
                .stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }

User → Buyer로 수정해줌

됐음

 

 

요청이 수락된 Order들만 가져와서 보여주고 싶은데

List를 가져와서 orderStatus가 confirmed인 것들만 출력하고 싶어짐

public List<OrderResponseDto> getMyConfirmedOrders(User user){
        List<Order> orders = orderRepository.findAllByBuyerId(user.getBuyer().getId()).orElseThrow(()-> new NullPointerException("주문 요청이 존재하지 않습니다"));
        
        return orders.stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }

Service단에서 orders List 순회하면서 confirmed가 아니면 다 지워버리고 리턴하려고 했었으나

그냥 db에서 쿼리로 할 수 있지 않을까 싶어짐

Optional<List<Order>> findAllByBuyerIdWhereOrderStatus(Long buyerId, String orderStatus);

이렇게 바꿔봄

public List<OrderResponseDto> getMyConfirmedOrders(User user){
        List<Order> orders = orderRepository.findAllByBuyerIdWhereOrderStatus(user.getBuyer().getId(),"CONFIRMED").orElseThrow(()-> new NullPointerException("주문 요청이 존재하지 않습니다"));
        return orders.stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }

Caused by: org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.util.Optional com.raincloud.sunlightmarket.order.repository.OrderRepository.findAllByBuyerIdWhereOrderStatus(java.lang.Long,java.lang.String); Reason: Failed to create query for method public abstract java.util.Optional com.raincloud.sunlightmarket.order.re

Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.Optional com.raincloud.sunlightmarket.order.repository.OrderRepository.findAllByBuyerIdWhereOrderStatus(java.lang.Long,java.lang.String); No property 'whereOrderStatus' found for type 'Long'; Traversed path: Order.buyer.id

쿼리가 잘못된 거 같음

어떻게 쓰면 되는지 찾아봄

https://www.baeldung.com/spring-data-derived-queries

where이 아니라 equals를 써야하나봄

List<User> findByNameEquals(String name);

근데 조건을 두개 하고 싶어

findAllByBuyerIdAndOrderStatusEquals

이렇게 해봄

오 Confirmed만 잘 넘어옴

{
    "status": 200,
    "message": "구매 요청 조회에 성공했습니다",
    "data": [
        {
            "orderId": 3,
            "buyerName": "닉2",
            "address": "주소주소2",
            "price": "가격가격2",
            "orderStatus": "CONFIRMED"
        }
    ]
}

이건 전체를 조회한 것

{
    "status": 200,
    "message": "구매 요청 조회에 성공했습니다",
    "data": [
        {
            "orderId": 1,
            "buyerName": "닉2",
            "address": "주소주소",
            "price": "가격가격",
            "orderStatus": "REJECTED"
        },
        {
            "orderId": 2,
            "buyerName": "닉2",
            "address": "주소주소2",
            "price": "가격가격2",
            "orderStatus": "REJECTED"
        },
        {
            "orderId": 3,
            "buyerName": "닉2",
            "address": "주소주소2",
            "price": "가격가격2",
            "orderStatus": "CONFIRMED"
        }
    ]
}
@Transactional
    public OrderResponseDto confirmDelivery(Long orderId,User user){
        Order order = getUserOrderById(orderId,user);
        Item item = order.getItem();
        item.confirmDelivery();
        order.confirmDelivery();
        return new OrderResponseDto(order);
    }

이렇게 하고 생각해보니

Confirmed인것만 승인해줘야 함

public void confirmDelivery(){
        if(this.orderStatus.equals("CONFIRMED")){
            this.orderStatus = "DELIVERED";
        }else{
            throw new RejectedExecutionException("유효하지 않은 요청입니다.");
        }
    }

이렇게 수정해봄

 

💡개선 : status String으로 쓰던거 Enum화 하기

https://www.w3schools.com/java/java_enums.asp

https://www.geeksforgeeks.org/enum-in-java/

A Java enumeration is a class type. Although we don’t need to instantiate an enum using new, it has the same capabilities as other classes. This fact makes Java enumeration a very powerful tool. Just like classes, you can give them constructors, add instance variables and methods, and even implement interfaces.

public enum OrderStatusEnum {
    PENDING,
    CONFIRMED,
    REJECTED,
    DELIVERED
}

내가 저거 보고 필요한거 짜면 이렇게 됨

package com.raincloud.sunlightmarket.global.entity;

public enum UserRoleEnum {
    USER(Authority.USER),  // 사용자 권한
    ADMIN(Authority.ADMIN);  // 관리자 권한

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() {
        return this.authority;
    }

    public static class Authority {

        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
}

팀원이 짠건 이럼 static으로 저걸 하면 좋은점이 있나?

if(this.orderStatus.equals("CONFIRMED")||this.orderStatus.equals("REJECTED")){

근데 나 이거 해야해서 Enum 끼리 비교해야 하는데 그거 가능?

db에 Enum이 안 들어가서 String으로 넣으려고 저랬나?

Comparing Java enum members: == or equals()?

another argument to use == instead of equals ist compile-time checking of types. myEnum.MY_CONSTANT.equals("Some String") will compile and myEnum.MY_CONSTANT == "Some String" will not, as "Some String" is not of the same type and the compiler can determin it upfront

오 팀원이 한건 String으로 들어가 지는데 나는 int로 됨

db에 0,1,2,3이 들어가 있다

pending 0

rejected 2

confirmed 1

delivered 3

{
    "status": 200,
    "message": "나의 구매 요청 조회에 성공했습니다",
    "data": [
        {
            "orderId": 1,
            "buyerName": "닉2",
            "address": "주소주소3",
            "price": "가격가격4",
            "orderStatus": "REJECTED"
        },
        {
            "orderId": 2,
            "buyerName": "닉2",
            "address": "주소주소3",
            "price": "가격가격4",
            "orderStatus": "CONFIRMED"
        },
        {
            "orderId": 3,
            "buyerName": "닉2",
            "address": "주소주소3",
            "price": "가격가격4",
            "orderStatus": "PENDING"
        }
    ]
}

all은 잘 불러서 읽어지는데

confirm만 불러오라 하면 에러남

String을 주면 안되나봄

List<Order> orders = orderRepository.findAllByBuyerIdAndOrderStatusEquals(buyerId,"CONFIRMED").orElseThrow(()-> new NullPointerException("주문 요청이 존재하지 않습니다"));

->
List<Order> orders = orderRepository.findAllByBuyerIdAndOrderStatusEquals(buyerId, OrderStatus.CONFIRMED).orElseThrow(()-> new NullPointerException("주문 요청이 존재하지 않습니다"));

public List<OrderResponseDto> getMyConfirmedOrders(User user){
        Long buyerId = user.getBuyer().getId();
        List<Order> orders = orderRepository.findAllByBuyerIdAndOrderStatusEquals(buyerId, OrderStatus.CONFIRMED).orElseThrow(()-> new NullPointerException("주문 요청이 존재하지 않습니다"));
        return orders.stream()
                .map(OrderResponseDto::new)
                .collect(Collectors.toList());
    }
{
    "status": 200,
    "message": "나의 구매 요청 조회에 성공했습니다",
    "data": [
        {
            "orderId": 2,
            "buyerName": "닉2",
            "address": "주소주소3",
            "price": "가격가격4",
            "orderStatus": "CONFIRMED"
        }
    ]
}

오호 저 부분 바꿔주니까 잘 동작함

Item의 completed, delivered도 하나의 Status Enum으로 만들어봄

completed전 후

ON_SALE

SOLD

DELIVERED

price의 값 지정해보기

gradle에 이거 추가하기

// Validation
    implementation 'org.springframework.boot:spring-boot-starter-validation'
@Getter
@RequiredArgsConstructor
public class ItemRequestDto {
    private String title;
    private String image;
    @Pattern(regexp = "^[0-9]*$", message = "가격은 숫자로만 입력가능합니다.")
    private String price;
    private String address;
    private String content;
}
}

log 빨간줄 →

import lombok.extern.slf4j.Slf4j;

@Slf4j

이걸 가져와줌

//상품 등록
    @PostMapping("/add")
    public ApiResponse<ItemResponseDto> addItem(
            @Valid @RequestBody ItemRequestDto requestDto,
            BindingResult bindingResult,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try {
            // Validation 예외 처리
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            if (fieldErrors.size() > 0) {
                for (FieldError fieldError : bindingResult.getFieldErrors()) {
                    log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
                }
                throw new IllegalArgumentException("입력 형식이 정확하지 않습니다");
            }
            ItemResponseDto responseDto = itemService.addItem(requestDto, userDetails.getUser());
            return new ApiResponse<ItemResponseDto>(HttpStatus.CREATED.value(),"아이템 추가 성공했습니다",responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException | NullPointerException ex){
            return new ApiResponse<ItemResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }
{
    "status": 400,
    "message": "입력 형식이 정확하지 않습니다",
    "data": null
}

이렇게 출력됨

 

Response 타입 개선해보기, global하게 에러 처리

ApiResponse개선해보기!

package com.raincloud.sunlightmarket.global.dto;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatusCode;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiResponse<T> {

    private int status;
    private String message;
    private T data;

    public ApiResponse(int status, String message, T data){
        this.status = status;
        this.message = message;
        this.data = data;
    }

    public ApiResponse(int status, String message){
        this.status = status;
        this.message = message;
        this.data = null;
    }

    public ApiResponse(T data){
        this.data = data;
    }
}

지금 활용법

@PutMapping("")
    public ApiResponse<OrderResponseDto> updateOrder(
            @RequestBody OrderRequestDto requestDto,
            @RequestParam Long orderId,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try {
            OrderResponseDto responseDto = orderService.updateOrder(requestDto,orderId,userDetails.getUser());
            return new ApiResponse<OrderResponseDto>(HttpStatus.OK.value(),"구매 요청 업데이트 성공했습니다",responseDto);
        }catch (RejectedExecutionException | NullPointerException ex){
            return new ApiResponse<OrderResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

메시지를 넣지 말고 그냥 에러를 넣어서 처리하게 해봄!

 

Q. 여기에 저 blindingresult 는 validation pattern에도 나오는데 저게 뭐지?

 

@ExceptionHandler({IllegalArgumentException.class})
    public ApiResponse<?> handleException(IllegalArgumentException ex){
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
    }

<?> 를 쓰면 제너릭일때 타입에 상관없이 쓸수있단걸 알았다!

package com.raincloud.sunlightmarket.global.exception;

import com.raincloud.sunlightmarket.global.dto.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.concurrent.RejectedExecutionException;

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler({IllegalArgumentException.class})
    public ApiResponse<?> handleIllegalArgumentException(IllegalArgumentException ex){
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
    }
    @ExceptionHandler({NullPointerException.class})
    public ApiResponse<?> handleNullPointerException(NullPointerException ex){
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
    }

    @ExceptionHandler({RejectedExecutionException.class})
    public ApiResponse<?> handleRejectedExecutionException(RejectedExecutionException ex){
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
    }

}

이거 만들고 try catch문 다 빼봄!

//상품 등록
    @PostMapping("/add")
    public ApiResponse<ItemResponseDto> addItem(
            @Valid @RequestBody ItemRequestDto requestDto,
            BindingResult bindingResult,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        try {
            // Validation 예외 처리
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            if (fieldErrors.size() > 0) {
                for (FieldError fieldError : bindingResult.getFieldErrors()) {
                    log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
                }
                throw new IllegalArgumentException("입력 형식이 정확하지 않습니다");
            }
            ItemResponseDto responseDto = itemService.addItem(requestDto, userDetails.getUser());
            return new ApiResponse<ItemResponseDto>(HttpStatus.CREATED.value(),"아이템 추가 성공했습니다",responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException | NullPointerException ex){
            return new ApiResponse<ItemResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

//상품 등록
    @PostMapping("/add")
    public ApiResponse<ItemResponseDto> addItem(
            @Valid @RequestBody ItemRequestDto requestDto,
            BindingResult bindingResult,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        
        // Validation 예외 처리
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        if (fieldErrors.size() > 0) {
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
            }
            throw new IllegalArgumentException("입력 형식이 정확하지 않습니다");
        }
        ItemResponseDto responseDto = itemService.addItem(requestDto, userDetails.getUser());
        return new ApiResponse<ItemResponseDto>(HttpStatus.CREATED.value(),"아이템 추가 성공했습니다",responseDto);
    }
{
    "status": 400,
    "message": "입력 형식이 정확하지 않습니다",
    "data": null
}

오 catch문 뺐는데 에러 잘 받아서 처리해줌!

 

 

내가 찜한 아이템 보는 기능

//전체 상품 조회, 로그인
    @GetMapping("")
    public ApiResponse<List<ItemResponseDto>> getItems(
            @RequestParam String type,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        if(type.equals("All")){
            return getAllItems();
        }
        else if(type.equals("Myselect")){
            return getAllLikedItems(userDetails.getUser());
        }
        else{return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),"올바르지 않은 요청입니다");}
    }

    public ApiResponse<List<ItemResponseDto>> getAllItems()
    {
        List<ItemResponseDto> responseDto = itemService.getAllItems();
        return new ApiResponse<>(HttpStatus.OK.value(),"모든 아이템 조회에 성공했습니다",responseDto);
    }

    public ApiResponse<List<ItemResponseDto>> getAllLikedItems(User user)
    {
        List<ItemResponseDto> responseDto = itemService.getAllLikedItems(user);
        return new ApiResponse<>(HttpStatus.OK.value(),"좋아요한 아이템 조회에 성공했습니다",responseDto);
    }

 

public List<ItemResponseDto> getAllLikedItems(User user) {
    List<Item> items =  itemRepository.findAllByOrderByModifiedAtDesc();
    List<ItemResponseDto> responseDtos = new ArrayList<>();
    for(Item item : items){
        if(likeRepository.existsByUserAndItem(user,item)){
            responseDtos.add(new ItemResponseDto(item));
        }
    }
    return  responseDtos;
}

간단하게 끝났다!

 

로그아웃 기능 구현

팀원이 로그아웃을 해서 올린 순간부터 로그아웃이 안 되길래

내가 다른 방식으로 개발해봄

  1. 쿠키방식으로 바꾸기
  2. 쿠키를 delete하기

이렇게 하는게 좋겠음

예전

// HttpServletRequest 에서 Cookie Value : JWT 가져오기
    public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    try {//FU//
                        return URLDecoder.decode(cookie.getValue().replaceAll("Bearer%20", ""), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
        }
        return null;
    }

예전

public String createToken(String userId) {
        Date date = new Date();

        // 토큰 만료시간 60분
        long TOKEN_TIME = 60 * 60 * 1000;
        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(userId)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key, signatureAlgorithm)
                        .compact();
    }

지금

public String createToken(String username, UserRoleEnum roleEnum) {
        Date date = new Date();

        return BEARER_PREFIX +
            Jwts.builder()
                .setSubject(username)
                .claim(AUTHORIZATION_KEY, roleEnum)
                .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                .setIssuedAt(date)
                .signWith(key, signatureAlgorithm)
                .compact();
    }

UserController

@PostMapping("/login")
    public ResponseEntity<CommonResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
        try {
            userService.login(loginRequestDto);
            jwtUtil.addJwtToCookie(jwtUtil.createToken(loginRequestDto.getEmail(), UserRoleEnum.USER), response);
            return ResponseEntity.ok().body(new CommonResponseDto("로그인 성공", HttpStatus.OK.value()));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(new CommonResponseDto(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

UserService

public void login(LoginRequestDto loginRequestDto) {
        String email = loginRequestDto.getEmail();
        String password = loginRequestDto.getPassword();
        // userId 검색
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException("등록된 유저가 없습니다."));
        // 비밀번호 확인
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }
    }

authorization 쿠키까지 만드는데는 성공함

근데 이제 Post가 안됨 ㅎㅎ

@AuthenticationPrincipal UserDetailsImpl userDetails으로 넘겨주는 부분은 어디일까?

거긴 문제없었음

Filterchain에서 이 부분으로 아직 하고있었어서 안됐나봄

String tokenValue = jwtUtil.getJwtFromHeader(request);

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain chain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getJwtFromHeader(request);
        if (StringUtils.hasText(tokenValue)) {
            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("token 에러");
                return;
            }
            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }
        chain.doFilter(request, response);
    }

오 쿠키로 로그인 바꾸는데 성공함

이제 쿠키를 삭제하기만 하면 됨

 

@RequestMapping("/logout")
    public ResponseEntity<CommonResponseDto> logout(HttpServletRequest req) {
        try {
            userService.logout(req);
            return ResponseEntity.ok().body(new CommonResponseDto("로그아웃 성공", HttpStatus.OK.value()));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(new CommonResponseDto(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

로그아웃 이렇게 해봄

Service단에 가져가는게 웃기긴 한데…

public void logout(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("Authorization")) {
                    cookie.setMaxAge(0);
                }
            }
        }
    }

근데 로그아웃이 안됨 ㅋㅋㅋㅋ

아 쿠키cookie.setMaxAge(0)

해서 response에다가 다시 돌려보내야 되나봄

public void logout(HttpServletRequest req, HttpServletResponse res) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("Authorization")) {
                    cookie.setMaxAge(0);
                    res.addCookie(cookie);
                }
            }
        }
    }

이렇게 해도 쿠키가 그냥 그대로 남아있음

그럼 내용을 빈칸으로 해보는건 어때

cookie.setValue("");

빈칸으로 날려보내봄

흠 아닌가봄 저 함수를 controller단 아니고

jwtUtil같은데다 잘 위치시키면 될거같기도

@RequestMapping("/logout")
    public ResponseEntity<CommonResponseDto> logout(HttpServletRequest req, HttpServletResponse res) {
        try {
            userService.logout(req,res);
            return ResponseEntity.ok().body(new CommonResponseDto("로그아웃 성공", HttpStatus.OK.value()));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(new CommonResponseDto(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

public void logout(HttpServletRequest req, HttpServletResponse res) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("Authorization")) {
                    cookie.setMaxAge(0);
                    cookie.setValue("");
                    res.addCookie(cookie);
                }
            }
        }
    }

근데 지금보니 login도 jwtUtil을 부르고있음

@PostMapping("/login2")
    public ResponseEntity<CommonResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
        try {
            userService.login(loginRequestDto);
            jwtUtil.addJwtToCookie(jwtUtil.createToken(loginRequestDto.getEmail(), UserRoleEnum.USER), response);
            return ResponseEntity.ok().body(new CommonResponseDto("로그인 성공", HttpStatus.OK.value()));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(new CommonResponseDto(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

이렇게 바꿔봄, 여전히 안 됨

HttpServletResponse는 어디로 가는거지??

cookie를 넣었는데 cookie가 cookies에 들어가지긴 하는데

로그인 에러가 뜸

콘솔 확인해보니 이러고 있다

2023-12-09T21:29:55.751+09:00 ERROR 11144 --- [nio-8080-exec-2] c.r.sunlightmarket.global.jwt.JwtUtil    : 만료된 JWT token 입니다.
2023-12-09T21:29:55.752+09:00 ERROR 11144 --- [nio-8080-exec-2] JWT 검증 및 인가                              : token 에러
2023-12-09T21:30:08.657+09:00 ERROR 11144 --- [nio-8080-exec-3] c.r.sunlightmarket.global.jwt.JwtUtil    : 만료된 JWT token 입니다.
2023-12-09T21:30:08.657+09:00 ERROR 11144 --- [nio-8080-exec-3] JWT 검증 및 인가                              : token 에러
2023-12-09T21:30:44.619+09:00 ERROR 11144 --- [nio-8080-exec-8] c.r.sunlightmarket.global.jwt.JwtUtil    : 만료된 JWT token 입니다.
2023-12-09T21:30:44.619+09:00 ERROR 11144 --- [nio-8080-exec-8] JWT 검증 및 인가                              : token 에러
@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain chain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getTokenFromRequest(request);
        if (StringUtils.hasText(tokenValue)) {
            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("token 에러");
                return;
            }
            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }
        chain.doFilter(request, response);
    }

저부분에서 validateToken이 잘 안 되고 있음

public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            log.error("유효하지 않는 JWT token 입니다.");
        } catch (ExpiredJwtException e) {
            log.error("만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.error("지원되지 않는 JWT token 입니다.");
        } catch (IllegalArgumentException e) {
            log.error("잘못된 JWT token 입니다.");
        }
        return false;
    }

이부분은 전이랑 똑같은데?

public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }
public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

헤더에 있는 Cookies의 cookie지우고 하니까 되긴 함

근데 로그인을 유저 2로 새로 할때도 Cookie를 삭제해줘야 될 거 같은데 그게 안되는듯 지금?

어 근데 login을 새로 하면 Authorization 쿠키가 덮어씌워지는 거 같음!!

그렇다면 login이랑 똑같은 방법으로

Authorization 쿠키를 만들어서 넣어주면 로그아웃이 되지 않을까..?

jwtUtil.logout(res,req);

jwtUtil.logout(res);
public void logout(HttpServletResponse res) {

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, null);
        cookie.setPath("/");

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
    }

오 로그아웃 기능 구현 성공함!!