RestFul API 개발 4

j0yy00n0·2025년 6월 9일

2025.05.09 ~ 05.14

RestFul API 개발

  • 백엔드

POSTMAN Collection 설정

  • 인증관련 API
  • 회원가입
    POST 방식에서 Body 안에 raw 방식을 사용하면 Body 형식으로 요청을 보낼 수 있다
  • 로그인
  • 회원관리 API
  • 회원정보조회
    인증권한을 주지 않고 조회를 하게 되면 에러가 나게 된다.
  • 인증 토큰 부여
    로그인 할 때 부여받은 bearer 토큰을 입력한다

회원 관련 API

POST : /auth/signup

  • 설명 : 회원가입 요청 , Request Body : 회원 관련 데이터
  • AuthController
@RestController
@RequestMapping("/auth")
public class AuthController {

    private final AuthService authService;

    @Autowired
    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    <!-- 
       	 @RequestBody를 통해 RequestBody로 넘어온 JSON 문자열을 파싱해 MemberDTO 속성으로 매핑해 객체로 받아낸다.
         (회원 아이디, 비밀번호)
       ========================================================================================================
         참고로 요청의 body에서 데이터를 뽑아내겠다는 것은 요청이 POST 요청이었다는 것을 알 수 있다.
         왜냐하면 GET 요청은 body가 아니라 header에 데이터가 담겨있기 때문이다.
     -->
    

    @Operation(summary = "회원 가입 요청", description = "회원 가입이 진행됩니다.", tags = {"AuthController"})
    @PostMapping("/signup")
    public ResponseEntity<ResponseDTO> signup(@RequestBody MemberDTO memberDTO) {	<!-- 회원 가입 정보를 받아 냄 -->
        return ResponseEntity
                .ok()
                .body(new ResponseDTO(HttpStatus.CREATED, "회원가입 성공", authService.signup(memberDTO)));
    }
}
  • AuthService
@Service
public class AuthService {

    private static final Logger log = LoggerFactory.getLogger(AuthService.class);
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenProvider tokenProvider;
    private final ModelMapper modelMapper;
    private final MemberRoleRepository memberRoleRepository;

    @Autowired
    public AuthService(MemberRepository memberRepository, PasswordEncoder passwordEncoder,
                       TokenProvider tokenProvider, ModelMapper modelMapper,
                       MemberRoleRepository memberRoleRepository) {
        this.memberRepository = memberRepository;
        this.passwordEncoder = passwordEncoder;
        this.tokenProvider = tokenProvider;
        this.modelMapper = modelMapper;
        this.memberRoleRepository = memberRoleRepository;
    }
    
    <!-- signup은 DML(INSERT) 작업이므로 @Transactional 어노테이션 추가 -->
    @Transactional
    public MemberDTO signup(MemberDTO memberDTO) {
        log.info("[AuthService] signup() Start.");
        log.info("[AuthService] memberDTO {}", memberDTO);

        <!-- 이메일 중복 유효성 검사(비즈니스 로직에 따라 선택적으로 구현하면 됨) -->
        if(memberRepository.findByMemberEmail(memberDTO.getMemberEmail()) != null) {
            log.info("[AuthService] 이메일이 중복됩니다.");
            throw new DuplicatedMemberEmailException("이메일이 중복됩니다.");
        }

        <!-- 우선 Repository로 쿼리를 작성하기 전에 DTO를 Entity로 매핑. -->
        Member registMember = modelMapper.map(memberDTO, Member.class);

        <!-- 목차. 1. tbl_member 테이블에 회원 INSERT -->
        <!-- 비밀번호 암호화 후 insert -->
        registMember.setMemberPassword(passwordEncoder.encode(registMember.getMemberPassword()));
        <!-- registMember.toBuilder().memberPassword(passwordEncoder.encode(registMember.getMemberPassword())).build(); -->
        Member result1 = memberRepository.save(registMember);		<!-- 반환형은 int값이 아닌 엔티티임. -->

        <!-- 목차. 2. tbl_member_role 테이블에 회원별 권한 INSERT (현재 엔티티에는 회원가입 후 pk값이 없다!)
                     JPA 에서 회원을 먼저 저장해야(save) pk(회원번호) 가 생기는데, 엔티티로 작업할 땐 저장 전까지는 pk값이 null 이거나 할딩이 안된다. -->
        <!-- 목차. 2-1. 우선 일반 권한(AuthorityCode값이 2번)의 회원을 추가(일종의 디폴트 권한을 지정해주면 됨) -->
        <!--
         	 목차. 2-2. 엔티티에는 추가 할 회원의 pk값이 아직 없으므로 기존 회원의 마지막 회원 번호를 조회
         		       (하지만 jpql에 의해 앞선 save와 jpql이 flush()로 쿼리와 함께 날아가고 회원이 이미 sequence객체 값
         		       증가와 함께 insert가 되 버린다. -> 결론은, maxMemberCode가 현재 가입하는 회원의 번호이다.)
         		       maxMemberCode() 는 가장 최근(=방금 가입한) 회원번호
         -->
        int maxMemberCode = memberRepository.maxMemberCode();	<!-- JPQL을 사용해 회원번호 max값 추출 -->

        MemberRole registMemberRole = new MemberRole(maxMemberCode, 2);

        MemberRole result2 = memberRoleRepository.save(registMemberRole);

        <!-- 위의 두 가지 save()가 모두 성공해야 해당 트랜잭션이 성공했다고 판단. -->
        log.info("[AuthService] Member Insert Result {}",
                (result1 != null && result2 != null) ? "회원 가입 성공" : "회원 가입 실패");

        log.info("[AuthService] signup() End.");

        return memberDTO;
    }
}
  • memberRepository
public interface MemberRepository extends JpaRepository<Member, Integer> {

    Member findByMemberId(String memberId);
    Member findByMemberEmail(String memberEmail);

    <!--  JPQL과 @Query를 활용한 구문 -->
    <!-- JPQL에서 엔티티 이름은 대소문자까지 완벽히 일치할 것! -->
    @Query("SELECT MAX(m.memberCode) FROM Member m")    
    int maxMemberCode();

    <!-- purchase 도메인 추가하면서 추가한 메소드 -->
    @Query("SELECT m.memberCode FROM Member m WHERE m.memberId = ?1")
    int findMemberCodeByMemberId(String orderMemberId);
}
  • memberRoleRepository
public interface MemberRoleRepository extends JpaRepository<MemberRole, MemberRolePk> {
}

POST : /auth/login

  • 설명 : 로그인 요청 , Request Body : 회원 관련 데이터
  • AuthController
@RestController
@RequestMapping("/auth")
public class AuthController {

    private final AuthService authService;

    @Autowired
    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    <!-- 
       	 @RequestBody를 통해 RequestBody로 넘어온 JSON 문자열을 파싱해 MemberDTO 속성으로 매핑해 객체로 받아낸다.
         (회원 아이디, 비밀번호)
       ========================================================================================================
         참고로 요청의 body에서 데이터를 뽑아내겠다는 것은 요청이 POST 요청이었다는 것을 알 수 있다.
         왜냐하면 GET 요청은 body가 아니라 header에 데이터가 담겨있기 때문이다.
     -->
    @Operation(summary = "로그인 요청", description = "로그인 및 인증이 진행됩니다.", tags = {"AuthController"})
    @PostMapping("/login")
    public ResponseEntity<ResponseDTO> login(@RequestBody MemberDTO memberDTO) {

        <!-- 
        	 ResponseEntity
             HTTP 응답 몸체와 헤더, 그리고 상태 코드를 제어할 수 있는 Spring Framework의 클래스다.
           	 응답으로 변환될 정보가 담긴 모든 요소들을 해당 객체로 만들어서 반환해 준다.(body + header + status)
             (ResponseBody와 차별점이 있다면, ResponseEntity는 HTTP 상태 코드나 헤더도 다룰 수 있다.)
             필요한 정보들만 담아서 전달할 수 있기 때문에 REST API를 만들 때 유용하게 사용하는 클래스다.
           	 또한 ResponseEntity를 사용할 때, 생성자 대신 Builder 사용을 권장한다.
             (숫자 타입인 상태 코드를 실수로 잘못 입력하지 않도록 메소드들이 제공 된다.)
         -->
        <!-- Builder 사용 -->
        return ResponseEntity
                .ok()
                .body(new ResponseDTO(HttpStatus.OK, "로그인 성공~", authService.login(memberDTO)));
        <!-- (React 및 Spring 연계 시, 가장 중요한 개념!!!)
         	  ResponseEntity의 body() 메소드를 사용하면 Response객체의 body에 담기는 ResponseDTO는 JSON문자열이 되고
         	  화면단이 React인 곳으로 가면 결국 Redux Store에 해당 리듀서가 관리하는 state 값이 된다.
         -->
    }
}
  • AuthService
@Service
public class AuthService {

    private static final Logger log = LoggerFactory.getLogger(AuthService.class);
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenProvider tokenProvider;
    private final ModelMapper modelMapper;
    private final MemberRoleRepository memberRoleRepository;

    @Autowired
    public AuthService(MemberRepository memberRepository, PasswordEncoder passwordEncoder,
                       TokenProvider tokenProvider, ModelMapper modelMapper,
                       MemberRoleRepository memberRoleRepository) {
        this.memberRepository = memberRepository;
        this.passwordEncoder = passwordEncoder;
        this.tokenProvider = tokenProvider;
        this.modelMapper = modelMapper;
        this.memberRoleRepository = memberRoleRepository;
    }

    public Object login(MemberDTO memberDTO) {

        log.info("[AuthService] login() START");
        log.info("[AuthService] {}", memberDTO);

        <!-- 목차. 1. 아이디 조회 -->
        Member member = memberRepository.findByMemberId(memberDTO.getMemberId());

        if(member == null) {
            log.info("[AuthService] login() Required User Not Found!");
            throw new LoginFailedException(memberDTO.getMemberId() + " 유저를 찾을 수 없습니다.");
        }

        <!-- 목차. 2. 비밀번호 매칭 -->
        if(!passwordEncoder.matches(memberDTO.getMemberPassword(), member.getMemberPassword())) {
            log.info("[AuthService] login() Password Match Failed!");
            throw new LoginFailedException("잘못된 비밀번호 입니다.");
        }

        <!-- 목차. 3. 토큰 발급 -->
        TokenDTO newToken = tokenProvider.generateTokenDTO(member);

        return newToken;
    }
}
  • MemberRepository
public interface MemberRepository extends JpaRepository<Member, Integer> {

    Member findByMemberId(String memberId);
    Member findByMemberEmail(String memberEmail);

    <!--  JPQL과 @Query를 활용한 구문 -->
    <!-- JPQL에서 엔티티 이름은 대소문자까지 완벽히 일치할 것! -->
    @Query("SELECT MAX(m.memberCode) FROM Member m")    
    int maxMemberCode();

    <!-- purchase 도메인 추가하면서 추가한 메소드 -->
    @Query("SELECT m.memberCode FROM Member m WHERE m.memberId = ?1")
    int findMemberCodeByMemberId(String orderMemberId);
}

GET : /api/v1/members/{memberId}

  • 설명 : 회원 상세 조회 , Request Body : 회원 PK
  • MemberController
@RestController
@RequestMapping("/api/v1")
public class MemberController {
	
	private final MemberService memberService;
	
	public MemberController(MemberService memberService) {
		this.memberService = memberService;
	}
	
	@Operation(summary = "회원 조회 요청", description = "회원 한명이 조회됩니다.", tags = { "MemberController" })
	@GetMapping("/members/{memberId}")
	public ResponseEntity<ResponseDTO> selectMyMemberInfo(@PathVariable String memberId) {
		return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK, "조회 성공", memberService.selectMyInfo(memberId)));
	}
}
  • MemberService
@Service
public class MemberService {

	private static final Logger log = LoggerFactory.getLogger(MemberService.class);
	private final MemberRepository memberRepository;
	private final ModelMapper modelMapper;
	
	@Autowired
	public MemberService(MemberRepository memberRepository, ModelMapper modelMapper) {
		this.memberRepository = memberRepository;
		this.modelMapper = modelMapper;
	}
	
	public MemberDTO selectMyInfo(String memberId) {
		log.info("[MemberService] getMyInfo Start =======================");
		
		Member member = memberRepository.findByMemberId(memberId);
		log.info("[MemberService] {}", member);
		log.info("[MemberService] getMyInfo End =========================");
		
		return modelMapper.map(member, MemberDTO.class);
	}
}
  • MemberRepository
public interface MemberRepository extends JpaRepository<Member, Integer> {

    Member findByMemberId(String memberId);
    Member findByMemberEmail(String memberEmail);

    <!--  JPQL과 @Query를 활용한 구문 -->
    <!-- JPQL에서 엔티티 이름은 대소문자까지 완벽히 일치할 것! -->
    @Query("SELECT MAX(m.memberCode) FROM Member m")    
    int maxMemberCode();

    <!-- purchase 도메인 추가하면서 추가한 메소드 -->
    @Query("SELECT m.memberCode FROM Member m WHERE m.memberId = ?1")
    int findMemberCodeByMemberId(String orderMemberId);
}

제품 관련 API

GET : /api/v1/products?offset=1

  • 설명 : 제품 조회 , Request Body : offset 관련 데이터
  • ProductController
@RestController
@RequestMapping("/api/v1")
<!-- Lombok이 제공하는 애너테이션 -->
<!-- final 또는 @NonNull이 붙은 필드들만 포함하는 생성자(Constructor)를 자동 생성 -->
@RequiredArgsConstructor
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    @Operation(summary = "상품 리스트 조회 요청", description = "상품 조회 및 페이징 처리가 진행됩니다.", tags = { "ProductController" })
    @GetMapping("/products")
    public ResponseEntity<ResponseDTO> selectProductListWithPaging(
            <!-- offset 은 보통 페이징 처리할 때 사용하는 변수,몇 번째 데이터부터 보여줄지 지정하는 숫자 -->
            @RequestParam(name = "offset" , defaultValue = "1") String offset
    ) {

        log.info("[ProductController] selectProductListWithPaging : " + offset);

        <!-- 제품 갯수 -->
        int total = productService.selectProductTotal();

        <!-- Criteria 검색조건 -->
        Criteria cri = new Criteria(Integer.valueOf(offset) , 10);
        PagingResponseDTO pagingResponseDTO = new PagingResponseDTO();

        <!-- 1. offset 의 번호에 맞는 페이지에 뿌릴 Product 들 -->
        pagingResponseDTO.setData(productService.selectProductListWithPaging(cri));

        <!-- 2. PageDTO(Criteria(보고싶은 페이지, 한페이지에 뿌릴 갯 수), 전체 상품 수) -->
        pagingResponseDTO.setPageInfo(new PageDTO(cri , total));

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK, "조회 성공함!" , pagingResponseDTO));
    }
}
  • ProductService
@Service
@RequiredArgsConstructor
public class ProductService {

    @Value("${image.image-dir}")
    private String IMAGE_DIR;

    @Value("${image.image-url}")
    private String IMAGE_URL;

    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    private final ProductRepository productRepository;
    <!-- Entity <-> DTO 객체 변환 관련 라이브러리 -->
    private final ModelMapper modelMapper;

    public int selectProductTotal() {

        log.info("[ProductService] selectProductTotal() start!!");

        List<Product> productList = productRepository.findByProductOrderable("Y");

        log.info("[ProductService] selectProductTotal() end!!");

        return productList.size();
    }

    public Object selectProductListWithPaging(Criteria cri) {

        log.info("[ProductService] selectProductListWithPaging() start!!");

        int index = cri.getPageNum() - 1;
        int count = cri.getAmount();

        Pageable paging = PageRequest.of(index , count , Sort.by("productCode").descending());

        Page<Product> result = productRepository.findByProductOrderable("Y" , paging);

        List<Product> productList = result.getContent();

        <!-- 이미지 관련 처리 -->
        for(int i = 0; i < productList.size(); i++) {
            productList.get(i).setProductImageUrl(IMAGE_URL + productList.get(i).getProductImageUrl());
        }

        log.info("[ProductService] selectProductListWithPaging() end!!");

        return productList.stream().map(product -> modelMapper.map(product , ProductDTO.class)).collect(Collectors.toList());
    }
}
  • ProductRepository
@Repository
<!-- JpaRepository 는 Spring Data JPA 에서 데이터 베이스와 연결해주는 인터페이스를 사용할 때 선언하는 방식 -->
<!-- DB에 있는 테이블과 자바 객체 사이의 CRUD 작업을 자동으로 처리해주는 역할 -->
public interface ProductRepository extends JpaRepository<Product , Integer> {

    <!-- 판매 가능한 메뉴 조회 -->
    List<Product> findByProductOrderable(String y);

    <!-- 페이징 처리가 된 메뉴 조회 -->
    Page<Product> findByProductOrderable(String y , Pageable pageable);
}

offset 설정 확인


GET : /api/v1/products/{productCode}

  • 설명 : 제품 상세 조회 , Request Body : 제품 PK
  • ProductController
@RestController
@RequestMapping("/api/v1")
<!-- Lombok이 제공하는 애너테이션
	 final 또는 @NonNull이 붙은 필드들만 포함하는 생성자 -->(Constructor)를 자동 생성
@RequiredArgsConstructor
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    @Operation(summary = "상품 상세 조회 요청", description = "상품의 상세 페이지 처리가 진행됩니다.", tags = { "ProductController" })
    @GetMapping("/products/{productCode}")
    public ResponseEntity<ResponseDTO> selectProductDetail(@PathVariable int productCode) {

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , productCode + "번 상품 상세조회 성공", productService.selectProduct(productCode)));
    }
}
  • ProductService
@Service
@RequiredArgsConstructor
public class ProductService {

    @Value("${image.image-dir}")
    private String IMAGE_DIR;

    @Value("${image.image-url}")
    private String IMAGE_URL;

    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    private final ProductRepository productRepository;
    <!-- Entity <-> DTO 객체 변환 관련 라이브러리 -->
    private final ModelMapper modelMapper;

    public ProductDTO selectProduct(int productCode) {
        log.info("[ProductService] selectProduct() Start");

        <!-- Product vs Optional<Product> 차이 때문 -->
        <!-- Optional<T> 는 값이 있을 수도 있고, 없을 수도 있다를 명확하게 코드로 표현하는 클래스 -->
        <!-- 일단 get() 으로 단순 데이터를 끌고 오기만 했다 -->
        Product product = productRepository.findById(productCode).get();
        <!-- 이미지 설정 -->
        product.setProductImageUrl(IMAGE_URL + product.getProductImageUrl());

        log.info("[ProductService] selectProduct() End");

        return modelMapper.map(product , ProductDTO.class);
    }
}


GET : /api/v1/products/search?s={menuName}

  • 설명 : 제품 검색 , Request Body : 메뉴 이름 관련 QueryString
  • ProductController
@RestController
@RequestMapping("/api/v1")
<!-- Lombok이 제공하는 애너테이션
	 final 또는 @NonNull이 붙은 필드들만 포함하는 
     생성자(Constructor)를 자동 생성 -->
@RequiredArgsConstructor
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    @Operation(summary = "검색 상품 리스트 조회 요청", description = "검색어에 해당되는 상품 리스트 조회가 진행됩니다.", tags = { "ProductController" })
    @GetMapping("/products/search") <!- 쿼리 스트링으로 오는 값을 search  넣어준다 -->
    public ResponseEntity<ResponseDTO> selectSearchProductList(@RequestParam(name = "s" , defaultValue = "all") String search) {

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , search + " 검색어 조회 성공 ", productService.selectSearchProductList(search)));
    }
}
  • ProductService
@Service
@RequiredArgsConstructor
public class ProductService {

    @Value("${image.image-dir}")
    private String IMAGE_DIR;

    @Value("${image.image-url}")
    private String IMAGE_URL;

    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    private final ProductRepository productRepository;
    <!-- Entity <-> DTO 객체 변환 관련 라이브러리 -->
    private final ModelMapper modelMapper;

    public List<ProductDTO> selectSearchProductList(String search) {
        log.info("[ProductService] selectSearchProductList() Start");
        log.info("[ProductService] searchValue : {}", search);

        <!-- Containing 는 포함하고 있는지를 확인하는 구문 -->
        List<Product> productListWithSearchValue = productRepository.findByProductNameContaining(search);
        log.info("[ProductService] productListWithSearchValue : {}", productListWithSearchValue);

        <!-- 이미지 관련 처리 -->
        for (int i = 0; i < productListWithSearchValue.size(); i++){
            productListWithSearchValue.get(i).setProductImageUrl(IMAGE_URL + productListWithSearchValue.get(i).getProductImageUrl());
        }

        log.info("[ProductService] selectSearchProductList() End");

        return productListWithSearchValue.stream()
                .map(product -> modelMapper.map(product , ProductDTO.class))
                .collect(Collectors.toList());
    }
}
  • ProductRepository
@Repository
<!-- JpaRepository 는 Spring Data JPA 에서 데이터 베이스와 
	 연결해주는 인터페이스를 사용할 때 선언하는 방식
 	 DB에 있는 테이블과 자바객체 사이의 CRUD 작업을 자동으로 처리해주는 역할 -->
public interface ProductRepository extends JpaRepository<Product , Integer> {

    <!-- 판매 가능한 메뉴 조회 -->
    List<Product> findByProductOrderable(String y);

    <!-- 페이징 처리가 된 메뉴 조회 -->
    Page<Product> findByProductOrderable(String y , Pageable pageable);

    <!-- 우리가 입력한 검색어를 포함하고 있는 제품 리스트 조회 메서드 -->
    List<Product> findByProductNameContaining(String search);
}


GET : /api/v1/products/meals

  • 설명 : 카테고리 별 제품 조회
  • ProductController
@RestController
@RequestMapping("/api/v1")
<!-- Lombok이 제공하는 애너테이션
	 final 또는 @NonNull이 붙은 필드들만 포함하는 
     생성자(Constructor)를 자동 생성 -->
@RequiredArgsConstructor
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    @Operation(summary = "식사 상품 리스트 조회 요청", description = "식사 카테고리에 해당하는 상품 리스트 조회가 진행됩니다.", tags = { "ProductController" })
    @GetMapping("/products/meals")
    public ResponseEntity<ResponseDTO> selectProductListAboutMeal() {

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , "카테고리 별 조회 성공!" , productService.selectProductListAboutMeal()));
    }
}
  • ProductService
Service
@RequiredArgsConstructor
public class ProductService {

    @Value("${image.image-dir}")
    private String IMAGE_DIR;

    @Value("${image.image-url}")
    private String IMAGE_URL;

    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    private final ProductRepository productRepository;
    <!-- Entity <-> DTO 객체 변환 관련 라이브러리 -->
    private final ModelMapper modelMapper;

    public List<ProductDTO> selectProductListAboutMeal() {
        log.info("[ProductService] selectProductListAboutMeal() Start");

        <!-- findByCategoryCode(categoryCode) 처럼 
        	 변수로 작성하게 되면 일일히 만들어 줄 필요가 없다 -->
        List<Product> productListAboutMeal = productRepository.findByCategoryCode(1);

        <!-- 이미지 관련 처리 -->
        for (int i = 0; i < productListAboutMeal.size(); i++){
            productListAboutMeal.get(i).setProductImageUrl(IMAGE_URL + productListAboutMeal.get(i).getProductImageUrl());
        }

        log.info("[ProductService] selectProductListAboutMeal() End");

        return productListAboutMeal.stream()
                .map(product -> modelMapper.map(product , ProductDTO.class))
                .collect(Collectors.toList());
    }
}
  • ProductRepository
@Repository
<!-- JpaRepository 는 Spring Data JPA 에서 데이터 베이스와 
	 연결해주는 인터페이스를 사용할 때 선언하는 방식
	 DB에 있는 테이블과 자바객체 사이의 CRUD 작업을 자동으로 처리해주는 역할 -->
public interface ProductRepository extends JpaRepository<Product , Integer> {

    <!-- 판매 가능한 메뉴 조회 -->
    List<Product> findByProductOrderable(String y);

    <!-- 페이징 처리가 된 메뉴 조회 -->
    Page<Product> findByProductOrderable(String y , Pageable pageable);

    <!-- 우리가 입력한 검색어를 포함하고 있는 제품 리스트 조회 메서드 -->
    List<Product> findByProductNameContaining(String search);

    <!-- 카테고리 코드 별 메뉴 리스트 조회 메서드 -->
    List<Product> findByCategoryCode(int i);
}


GET : /api/v1/products-management/{productCode}

  • 설명 : 관리자 제품 관리 페이지 상품 상세 페이지 조회
  • ProductController
@RestController
@RequestMapping("/api/v1")
<!-- Lombok이 제공하는 애너테이션
	 final 또는 @NonNull이 붙은 필드들만 
     포함하는 생성자 (Constructor)를 자동 생성-->
@RequiredArgsConstructor
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    @Operation(summary = "관리자 페이지 상품 상세 페이지 조회 요청", description = "관리자 페이지에서 상품 상세 페이지 조회가 진행됩니다.", tags = { "ProductController" })
    @GetMapping("/products-management/{productCode}")
    public ResponseEntity<ResponseDTO> selectProductDetailForAdmin(@PathVariable int productCode) {

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , "관리자 제품 상세조회 성공!" , productService.selectProductForAdmin(productCode)));
    }
}
  • ProductService
@Service
@RequiredArgsConstructor
public class ProductService {

    @Value("${image.image-dir}")
    private String IMAGE_DIR;

    @Value("${image.image-url}")
    private String IMAGE_URL;

    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    private final ProductRepository productRepository;
    <!-- Entity <-> DTO 객체 변환 관련 라이브러리 -->
    private final ModelMapper modelMapper;

    public ProductDTO selectProductForAdmin(int productCode) {
        log.info("[ProductService] selectProductForAdmin() Start");

        Product product = productRepository.findById(productCode).get();
        product.setProductImageUrl(IMAGE_URL + product.getProductImageUrl());

        log.info("[ProductService] selectProductForAdmin() End");
        
        return modelMapper.map(product , ProductDTO.class);
    }
  • 인증 없이 조회
  • 관리자 로그인

POST : /api/v1/products

  • 설명 : 제품 등록 Request Body : 메뉴 등록 관련 메뉴 데이터
  • ProductController
@RestController
@RequestMapping("/api/v1")
<!-- Lombok이 제공하는 애너테이션
	 final 또는 @NonNull이 붙은 필드들만 
     포함하는 생성자(Constructor)를 자동 생성 -->
@RequiredArgsConstructor
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    <!--
    @RequestBody 는 Json 형식의 요청 데이터를 객체로 매핑하는 데 사용이 되며
    파일 업로드 같은 멀티파트 데이터를 처리할 수 없다.
    반면, @ModelAttribute 는 폼 데이터와 파일 업로드 데이터를 함께 처리할 수 있도록
    설계가 되었기 때문에 Image or File 관련 처리는 @ModelAttribute 를 사용하는 것이
    더욱 적합하게 된다.
    -->

    @Operation(summary = "상품 등록 요청", description = "해당 상품 등록이 진행됩니다.", tags = { "ProductController" })
    @PostMapping(value = "/products")
    public ResponseEntity<ResponseDTO> insertProduct(@ModelAttribute ProductDTO productDTO , MultipartFile productImage) {

        log.info("[Controller] 프론트에서 전달 받은 productDTO : {}" , productDTO);
        log.info("[Controller] 프론트에서 전달 받은 productImage : {}" , productImage);

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.CREATED , "상품 등록 성공", productService.insertProduct(productDTO , productImage)));
    }
}
  • ProductService
@Service
@RequiredArgsConstructor
public class ProductService {

    @Value("${image.image-dir}")
    private String IMAGE_DIR;

    @Value("${image.image-url}")
    private String IMAGE_URL;

    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    private final ProductRepository productRepository;
    <!-- Entity <-> DTO 객체 변환 관련 라이브러리 -->
    private final ModelMapper modelMapper;

    @Transactional
    public String insertProduct(ProductDTO productDTO , MultipartFile productImage) {
        log.info("[ProductService] insertProduct() Start");
        log.info("[ProductService] productDTO : {}", productDTO);

        <!-- 이미지 파일 이름을 랜덤하게 지정해서 저장한다 -->
        String imageName = UUID.randomUUID().toString().replace("-" , "");
        String replaceFileName = null;
        int result = 0;

        try {
            replaceFileName = FileUploadUtils.saveFile(IMAGE_DIR , imageName , productImage);

            <!-- 변환 처리 된 파일 값으로 Set -->
            productDTO.setProductImageUrl(replaceFileName);

            <!-- 화면에서 전달 받은 DTO 객체를 Entity 로 변경 -->
            Product insertProduct = modelMapper.map(productDTO , Product.class);

            productRepository.save(insertProduct);

            <!-- 정상적으로 예외 없이 마무리 되면 result 를 1로 초기화 -->
            result = 1;

        } catch (IOException e) {
            <!-- 예외 발생 시 파일에 대한 정보 삭제 -->
            FileUploadUtils.deleteFile(IMAGE_DIR , replaceFileName);
            throw new RuntimeException(e);
        }

        log.info("[ProductService] insertProduct() End");
        return (result > 0) ? productDTO.getProductName() + " 상품 등록 성공!!" : " 상품 등록 실패";
    }
}
  • @ModelAttribute 로 받을 경우에는 raw 가 아닌 form-data로 확인할 수 있다
  • MultipartFile 은 Text 타입을 File로 두고 이미지를 삽입한다

관리자 기능 엔티티 연관관계 설정

  • 카테고리 자체로 수정, 삭제할 것이 아니기 때문에 product의 도메인형식으로 작성하는 것이 아닌 product entity 안에 부가적인 형태로 생성한다
  • 또한 특정 기능만 사용할 수 있도록 연관관계를 맺은 다른 클래스를 생성해서 사용한다
  • ProductAndCategory Entity
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
@Table(name = "tbl_product")
@Builder(toBuilder = true)
public class ProductAndCategory {

    @Id
    @Column(name = "product_code")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int productCode;

    @Column(name = "product_name")
    private String productName;

    @Column(name = "product_price")
    private String productPrice;

    @Column(name = "product_description")
    private String productDescription;

    @Column(name = "product_orderable")
    private String productOrderable;

<!--    @Column(name = "category_code")
	    private int categoryCode; -->
    <!-- Product 와 Category 의 연관관계를 형성 할 필드 설정 -->
    @ManyToOne
    @JoinColumn(name = "category_code")
    private Category category;

    @Column(name = "product_image_url")
    private String productImageUrl;

    @Column(name = "product_stock")
    private Long productStock;
}
  • ProductAndCategoryDTO
@Data
public class ProductAndCategoryDTO {

    private int productCode;
    private String productName;
    private String productPrice;
    private String productDescription;
    private String productOrderable;
    private CategoryDTO category;
    private String productImageUrl;
    private Long productStock;
}
  • ProductController
@RestController
@RequestMapping("/api/v1")
// Lombok이 제공하는 애너테이션
// final 또는 @NonNull이 붙은 필드들만 포함하는 생성자(Constructor)를 자동 생성
@RequiredArgsConstructor
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private final ProductService productService;

    @Operation(summary = "관리자 페이지 상품 리스트 조회 요청", description = "관리자 페이지에서 상품 리스트 조회가 진행됩니다.", tags = { "ProductController" })
    @GetMapping("/products-management")
    public ResponseEntity<ResponseDTO> selectProductListWithPagingForAdmin(
            @RequestParam(name = "offset" , defaultValue = "1") String offset) {

            int total = productService.selectProductTotal();

            // 1 페이지에 10 개의 데이터 라는 검색 조건을 담을 객체
            Criteria cri = new Criteria(Integer.valueOf(offset), 10);
            PagingResponseDTO pagingResponseDTO = new PagingResponseDTO();
            // DB 에서 조회해온 데이터를 담는 setData()
            pagingResponseDTO.setData(productService.selectProductListWithPagingForAdmin(cri));
            pagingResponseDTO.setPageInfo(new PageDTO(cri, total));

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , "조회 성공" , pagingResponseDTO));
    }
}
  • ProductService
@Service
@RequiredArgsConstructor
public class ProductService {

    @Value("${image.image-dir}")
    private String IMAGE_DIR;

    @Value("${image.image-url}")
    private String IMAGE_URL;

    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    private final ProductRepository productRepository;
    // Category <-> Product 연관관계 전용 레포지토리
    private final ProductAndCategoryRepository productAndCategoryRepository;
    /* Entity <-> DTO 객체 변환 관련 라이브러리 */
    private final ModelMapper modelMapper;

    public List<ProductAndCategoryDTO> selectProductListWithPagingForAdmin(Criteria cri) {
        log.info("[ProductService] selectProductListWithPagingForAdmin() Start");

        int index = cri.getPageNum()-1;
        int count = cri.getAmount();
        Pageable paging = PageRequest.of(index, count, Sort.by("productCode").descending());

        // 레파지토리 작성하면 쓸 부분 
        // ProductRepositoy 는 Product 엔티티에 대한 레파지토리이므로 ProductAndCategory 엔티티에 대해서는 사용할 수 없다
        // 엔티티를 분리해서 사용했으므로 레파지토리도 분리해서 사용해야 한다
        // findAll : 페이징 넘버를 담고 있는 전체 데이터 조회
        Page<ProductAndCategory> result = productAndCategoryRepository.findAll(paging);
        List<ProductAndCategory> productList = result.getContent();

        // 이미지 관련 처리 -> 여러분들이 신경쓰지 않아도 됩니다.
        for(int i = 0; i < productList.size(); i++) {
            productList.get(i).setProductImageUrl(IMAGE_URL + productList.get(i).getProductImageUrl());
        }

        log.info("[ProductService] selectProductListWithPagingForAdmin() End");

        return productList.stream()
                .map(product -> modelMapper.map(product , ProductAndCategoryDTO.class))
                .collect(Collectors.toList());
    }
}
  • ProductAndCategoryRepository
@Repository
public interface ProductAndCategoryRepository extends JpaRepository<ProductAndCategory , Integer> {
}

관리자 계정으로 먼저 로그인 카테고리부분까지 나오는 것을 확인


주문관련 API

Post : /api/v1/purchase

  • 설명 : 상품 주문 등록 Request Body : 해당 상품 주문 진행
  • Order Entity
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
@Table(name = "tbl_order")
@Builder(toBuilder = true)
public class Order {

    @Id
    @Column(name = "order_code")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int orderCode;

    @Column(name = "product_code")
    private int productCode;

    @Column(name = "order_member")
    private int orderMember;

    @Column(name = "order_phone")
    private String orderPhone;

    @Column(name = "order_email")
    private String orderEmail;

    @Column(name = "order_receiver")
    private String orderReceiver;

    @Column(name = "order_address")
    private String orderAddress;

    @Column(name = "order_amount")
    private String orderAmount;

    @Column(name = "order_date")
    private String orderDate;
}
  • OrderDTO
@Data
public class OrderDTO {

    // 구매를 완료하고 하고 나서 내역 확인할 때 사용
    private int orderCode;
    private int productCode;
    private int orderMember;
    private String orderPhone;
    private String orderEmail;
    private String orderReceiver;
    private String orderAddress;
    private String orderAmount;
    private String orderDate;
}
  • PurchaseDTO
@Data
public class PurchaseDTO {

    // 구매 시 사용할 DTO 필드 구성
    private String memberId;
    private String orderAddress;
    private int orderAmount;
    private String orderEmail;
    private String orderPhone;
    private String orderReceiver;
    private int productCode;

}
  • OrderController
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class OrderController {

	private final OrderService orderService;

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

	/* 설명. @RequestBody로 넘어온 JSON 문자열을 모두 받아줄 DTO(커맨드객체)를 작성할 것(getter, setter 필수)*/
	@Operation(summary = "상품 주문 요청", description = "해당 상품 주문이 진행됩니다.", tags = { "OrderController" })
	@PostMapping("/purchase")
	public ResponseEntity<ResponseDTO> insertPurchase(@RequestBody PurchaseDTO purchaseDTO) {

		return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.CREATED, " 주문 성공" , orderService.insertProduct(purchaseDTO)));
	}
}
  • OrderService
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {


	private final MemberRepository memberRepository;
	private final OrderRepository orderRepository;
	private final ModelMapper modelMapper;
	private final ProductRepository productRepository;

	@Transactional
	public String insertProduct(PurchaseDTO purchaseDTO) {
		log.info("[OrderService] insertPurchase() Start");
		log.info("[OrderService] purchaseDTO : {}", purchaseDTO);

		int result = 0;

		try {

			/* 1. 해당 주문을 진행하고 있는 회원의 PK 값 조회 */
			int memberCode = memberRepository.findMemberCodeByMemberId(purchaseDTO.getMemberId());

			/* 2. 주문 INSERT */
			Date now = new Date();
			// 주문 date 값을 포멧팅
			SimpleDateFormat sdf = new SimpleDateFormat("yy/MM/dd HH:mm:ss");
			String orderDate = sdf.format(now);

			Order order = Order.builder()
					.productCode(purchaseDTO.getProductCode())
					.orderMember(memberCode)
					.orderPhone(purchaseDTO.getOrderPhone())
					.orderAddress(purchaseDTO.getOrderAddress())
					.orderDate(orderDate)
					.orderEmail(purchaseDTO.getOrderEmail())
					.orderReceiver(purchaseDTO.getOrderReceiver())
					.orderAmount(String.valueOf(purchaseDTO.getOrderAmount()))
					.build();

			// 위에 생성한 order 인스턴스 save
			orderRepository.save(order);

			/* 3. 상품(Product) 재고 Update */
			// 상품 한 행 식별
			Product product = productRepository.findById(Integer.valueOf(order.getProductCode())).get();

			// 재고 업데이트
			product = product.toBuilder()
					// 기존 재고 - 주문 시 양
					.productStock(product.getProductStock() - purchaseDTO.getOrderAmount())
					.build();

			// 업데이트 반영
			productRepository.save(product);

			result = 1;

		}catch (Exception e){
			log.error("[Order] Exception 발생!!" , e);
		}

		log.info("[OrderService] insertPurchase() End");
		return (result > 0) ? "주문 성공!! " : " 주문 실패 ㅠㅜ ";
	}
}
  • orderRepository
@Repository
public interface OrderRepository extends JpaRepository<Order , Integer> {
}
  • 변경 전 DB
  • 회원으로 로그인
  • 제품 등록
  • 재고 상태 변경 19->16

GET : /api/v1/purchase/{memberId}

  • 설명 : 회원 주문 리스트 조회 요청 Request Body : 해당 회원의 주문건에 대한 상품 리스트 조회
  • OrderAndProduct Entity
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
@Table(name = "tbl_order")
@Builder(toBuilder = true)
public class OrderAndProduct {

    @Id
    @Column(name = "order_code")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int orderCode;

    @ManyToOne
    @JoinColumn(name = "product_code")
    private Product product;

    @Column(name = "order_member")
    private int orderMember;

    @Column(name = "order_phone")
    private String orderPhone;

    @Column(name = "order_email")
    private String orderEmail;

    @Column(name = "order_receiver")
    private String orderReceiver;

    @Column(name = "order_address")
    private String orderAddress;

    @Column(name = "order_amount")
    private String orderAmount;

    @Column(name = "order_date")
    private String orderDate;
}
  • OrderAndProductDTO
@Data
public class OrderAndProductDTO {

    private int orderCode;
    // 제품 관련 정보 객체
    private ProductDTO product;
    private int orderMember;
    private String orderPhone;
    private String orderEmail;
    private String orderReceiver;
    private String orderAddress;
    private String orderAmount;
    private String orderDate;
}
  • OrderController
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class OrderController {

	private final OrderService orderService;

	@Autowired
	public OrderController(OrderService orderService) {
		this.orderService = orderService;
	}
    
	@Operation(summary = "회원 주문 리스트 조회 요청", description = "해당 회원의 주문건에 대한 상품 리스트 조회가 진행됩니다.", tags = { "OrderController" })
	@GetMapping("/purchase/{memberId}")
    public ResponseEntity<ResponseDTO> getPurchaseList(@PathVariable String memberId) {
	
        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , "조회 성공" , orderService.selectPurchaseList(memberId)));
    }
}
  • OrderService
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {


	private final MemberRepository memberRepository;
	private final OrderRepository orderRepository;
	private final ModelMapper modelMapper;
	private final ProductRepository productRepository;
	private final OrderAndProductRepository orderAndProductRepository;

	public List<OrderAndProductDTO> selectPurchaseList(String memberId) {
		log.info("[OrderService] selectPurchaseList() Start");

		// 우리에게 주어진 힌트가 memberId 이기 때문에 Id 로 Code(식별자) 조회
		int memberCode = memberRepository.findMemberCodeByMemberId(memberId);

		// 제품과 주문 엔티티 연관관계 형성
		List<OrderAndProduct> orderList = orderAndProductRepository.findByOrderMember(memberCode);
        log.info("[OrderService] purchaseList {}", orderList);

        log.info("[OrderService] selectPurchaseList() End");
        
        return orderList.stream().map(
				order -> modelMapper.map(order , OrderAndProductDTO.class)
		).collect(Collectors.toList());
	}
}
  • OrderAndProductRepository
public interface OrderAndProductRepository extends JpaRepository<OrderAndProduct , Integer> {

    // 회원 별 주문 목록 조회
    List<OrderAndProduct> findByOrderMember(int memberCode);
}


리뷰관련 API

POST : /api/v1/reviews

  • 설명 : 상품 리뷰 등록 Request Body : 해당 상품 리뷰 등록 진행
  • Review Entity
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
@Builder(toBuilder = true)
@Table(name = "tbl_review")
public class Review {

    @Id
    @Column(name = "review_code")
    private int reviewCode;

    @Column(name = "product_code")
    private int productCode;

    @Column(name = "member_code")
    private int memberCode;

    @Column(name = "review_title")
    private String reviewTitle;

    @Column(name = "review_content")
    private String reviewContent;

    @Column(name = "review_create_date")
    private String reviewCreateDate;

}
  • ReviewDTO
@Data
public class ReviewDTO {

    private int reviewCode;
    private int productCode;
    private int memberCode;
    private String reviewTitle;
    private String reviewContent;
    private String reviewCreateDate;
}
  • ReviewController
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class ReviewController {

	private final ReviewService reviewService;
	
	@Autowired
	public ReviewController(ReviewService reviewService) {
		this.reviewService = reviewService;
	}
	
	@Operation(summary = "상품 리뷰 등록 요청", description = "해당 상품 리뷰 등록이 진행됩니다.", tags = { "ReviewController" })
	@PostMapping("/reviews")
    /* 리뷰를 달 제품 코드 , 리뷰 입력 회원번호 , 리뷰 제목 , 리뷰 내용 */
    public ResponseEntity<ResponseDTO> insertProductReview(@RequestBody ReviewDTO reviewDTO) {

        log.info("[ReviewController] 전달 받은 reviewDTO : {} " , reviewDTO);
        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.CREATED , "리뷰 입력 성공!!" , reviewService.insertProductReview(reviewDTO)));
    }
}
  • ReviewService
@Slf4j
@Service
@RequiredArgsConstructor
public class ReviewService {

	private final ModelMapper modelMapper;
	private final ReviewRepository reviewRepository;


	@Transactional
	public Object insertProductReview(ReviewDTO reviewDTO) {
		log.info("[ReviewService] insertProductReview() Start");

		int result = 0;

		Date now = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yy/MM/dd HH:mm:ss");
		String reviewDate = sdf.format(now);

		reviewDTO.setReviewCreateDate(reviewDate);

		try{
			Review review = modelMapper.map(reviewDTO , Review.class);

			reviewRepository.save(review);

			result = 1;
		} catch (Exception e){

			log.error("[Review] 등록 중 에러 발생 : {}" , e);
		}

		log.info("[ReviewService] insertProductReview() End");
		
		return (result > 0) ? "리뷰 등록 성공" : "리뷰 등록 실패" ;
	}
}
  • ReviewRepository
@Repository
public interface ReviewRepository extends JpaRepository<Review , Integer> {
}
  • BeanConfiguration
@Configuration
public class BeanConfiguration {

    @Bean
    public ModelMapper modelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                // private 필드에 접근하기 위한 설정
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                // DTO , Entity 필드 접근 가능 설정
                .setFieldMatchingEnabled(true);
        return modelMapper;
    }
}


GET : /api/v1/reviews/{productCode}

  • 설명 : 상품 리뷰 리스트 조회 Request Body : 해당 상품 리뷰 리스트 조회 진행
  • ReviewAndMember Entity
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
@Builder(toBuilder = true)
@Table(name = "tbl_review")
public class ReviewAndMember {

    @Id
    @Column(name = "review_code")
    private int reviewCode;

    // 리뷰 와 멤버 간의 연간관계 형성
    @ManyToOne
    @JoinColumn(name = "member_code")
    private Member member;

    @Column(name = "member_code")
    private int memberCode;

    @Column(name = "review_title")
    private String reviewTitle;

    @Column(name = "review_content")
    private String reviewContent;

    @Column(name = "review_create_date")
    private String reviewCreateDate;

}
  • ReviewController
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class ReviewController {

	private final ReviewService reviewService;
	
	@Autowired
	public ReviewController(ReviewService reviewService) {
		this.reviewService = reviewService;
	}
	
	@Operation(summary = "상품 리뷰 리스트 조회 요청", description = "해당 상품에 등록된 리뷰 리스트 조회가 진행됩니다.", tags = { "ReviewController" })
    @GetMapping("/reviews/{productCode}")
    public ResponseEntity<ResponseDTO> selectReviewListWithPaging(
            @PathVariable String productCode ,
            @RequestParam(name = "offset" , defaultValue = "1")
            String offset) {
        log.info("[ReviewController] selectReviewListWithPaging : " + offset);
        log.info("[ReviewController] productCode : " + productCode);

        Criteria cri = new Criteria(Integer.valueOf(offset) , 10);
        cri.setSearchValue(productCode); // 상품을 리뷰에 대한 검색 조건으로 설정

        int total = (int) reviewService.selectReviewTotal(Integer.valueOf(cri.getSearchValue()));

        // Criteria 검색조건
        PagingResponseDTO pagingResponseDTO = new PagingResponseDTO();

        /* 1. offset 의 번호에 맞는 페이지에 뿌릴 Review 들 */
        pagingResponseDTO.setData(reviewService.selectReviewListWithPaging(cri));

        /* 2. PageDTO(Criteria(보고싶은 페이지, 한페이지에 뿌릴 갯 수), 전체 상품 수) */
        pagingResponseDTO.setPageInfo(new PageDTO(cri , total));

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK, "조회 성공함!" , pagingResponseDTO));

    }
}
  • ReviewService
@Slf4j
@Service
@RequiredArgsConstructor
public class ReviewService {

	private final ModelMapper modelMapper;
	private final ReviewRepository reviewRepository;
	private final ReviewAndMemberRepository reviewAndMemberRepository;

	public long selectReviewTotal(int productCode) {
		log.info("[ReviewService] selectReviewTotal() Start");

		// Product 코드를 통해 행의 수 반환
		long result = reviewRepository.countByProductCode(productCode);

        log.info("[ReviewService] selectReviewTotal() End");
        
        return result;
	}

	public List<ReviewAndMemberDTO> selectReviewListWithPaging(Criteria cri) {
		log.info("[ReviewService] selectReviewListWithPaging() Start");

		int index = cri.getPageNum() - 1;
		int count = cri.getAmount();

		// 리뷰는 최신순 부터 확인
		Pageable pageable = PageRequest.of(index , count , Sort.by("reviewCode"));

		// 리뷰를 조회할 때 작성자에 대한 정보를 가져와야 하기 때문에
		// Member Entity 와 연관관계를 형성
		Page<ReviewAndMember> result = reviewAndMemberRepository.findByProductCode(Integer.valueOf(cri.getSearchValue()) , pageable);
        List<ReviewAndMember> reviewList = result.getContent();

		log.info("[ReviewService] selectReviewListWithPaging() End");
        
		return reviewList.stream()
				.map(review -> modelMapper.map(review , ReviewAndMemberDTO.class))
				.collect(Collectors.toList());
	}
}
  • ReviewRepository
@Repository
public interface ReviewRepository extends JpaRepository<Review , Integer> {

    // count 는 반환형 long
    long countByProductCode(int productCode);
}
  • ReviewAndMemberRepository
@Repository
public interface ReviewAndMemberRepository extends JpaRepository<ReviewAndMember , Integer> {

    // 제품 기준 리뷰를 페이징 처리까지 한 조회 메서드
    Page<ReviewAndMember> findByProductCode(Integer integer, Pageable pageable);
}



GET : /api/v1/reviews/product/{reviewCode}

  • 설명 : 상품 리뷰 상세 페이지 조회 Request Body : 해당 상품 리뷰의 상세 페이지 조회 진행
  • ReviewController
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class ReviewController {

	private final ReviewService reviewService;
	
	@Autowired
	public ReviewController(ReviewService reviewService) {
		this.reviewService = reviewService;
	}
    
	@Operation(summary = "리뷰 상세 페이지 조회 요청", description = "해당 리뷰의 상세 페이지 조회가 진행됩니다.", tags = { "ReviewController" })
    @GetMapping("/reviews/product/{reviewCode}")
    public ResponseEntity<ResponseDTO> selectReviewDetail(@PathVariable int reviewCode) {

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , reviewCode + "번 리뷰 상세 조회 성공", reviewService.selectReviewDetail(reviewCode)));
    }
}
  • ReviewService
@Slf4j
@Service
@RequiredArgsConstructor
public class ReviewService {

	private final ModelMapper modelMapper;
	private final ReviewRepository reviewRepository;
	private final ReviewAndMemberRepository reviewAndMemberRepository;

	public ReviewAndMemberDTO selectReviewDetail(int reviewCode) {
		log.info("[ReviewService] getReviewDetail() Start");

		ReviewAndMember review = reviewAndMemberRepository.findById(reviewCode).get();
		
        log.info("[ReviewService] getReviewDetail() End");
        
        return modelMapper.map(review , ReviewAndMemberDTO.class);
	}
}


PUT : /api/v1/reviews

  • 설명 : 상품 리뷰 수정 Request Body : 해당 상품 리뷰 작성자의 리뷰 수정 진행
  • ReviewController
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class ReviewController {

	private final ReviewService reviewService;
	
	@Autowired
	public ReviewController(ReviewService reviewService) {
		this.reviewService = reviewService;
	}
	
	@Operation(summary = "리뷰 수정 요청", description = "리뷰 작성자의 리뷰 수정이 진행됩니다.", tags = { "ReviewController" })
    @PutMapping("/reviews")
    /* 전달 받을 데이터 : 리뷰를 식별할 수 있는 reviewCode , reviewTitle , reviewContent */
    public ResponseEntity<ResponseDTO> updateProductReview(@RequestBody ReviewDTO reviewDTO) {

        return ResponseEntity.ok().body(new ResponseDTO(HttpStatus.OK , "리뷰 수정 성공" , reviewService.updateProductReview(reviewDTO)));
    }
}
  • ReviewService
@Slf4j
@Service
@RequiredArgsConstructor
public class ReviewService {

	private final ModelMapper modelMapper;
	private final ReviewRepository reviewRepository;
	private final ReviewAndMemberRepository reviewAndMemberRepository;

	@Transactional
	public String updateProductReview(ReviewDTO reviewDTO) {
		log.info("[ReviewService] updateProductReview() Start");

		int result = 0;

		try {
			// 리뷰 수정하기 위한 수정할 엔티티 인스턴스 추출
			Review review = reviewRepository.findById(reviewDTO.getReviewCode()).get();
			review = review.toBuilder()
					.reviewTitle(reviewDTO.getReviewTitle())
					.reviewContent(reviewDTO.getReviewContent())
					.build();
			reviewRepository.save(review);

			result = 1;

		} catch (Exception e){
			log.error("[Review] 수정 중 오류 발생 : {}" , e);
		}

		log.info("[ReviewService] updateProductReview() End");
		
		return (result > 0) ? "수정 성공" : "수정 실패" ;
	}

}


참고

DB 서버와 IntelliJ 연결 및 사용

권한설정에 환경넣기

  • variable 값을 토큰에 {{}} 형식으로 입력

ModelMapper를 스프링 Bean으로 등록할 때 커스터마이즈 하는 이유

  • DTO/Entity에 getter/setter가 없는 경우
  • Lombok을 쓰지만 일부 필드에만 getter/setter가 있고, 필드명만 일치하는 경우
  • 불필요하게 public 메서드 추가하기 싫을 때

Spring 에서 요청 파라미터(값)을 받을 때 방법

@RequestParam

  • url 쿼리 파라미터 (?name=value 형태)
  • GET / POST 일 때 자주 사용
  • 정보를 어떻게 표현할지 결정
  • 필터링, 정렬 옵션 같은 부가정보 페이징
  • 선택적으로 바꿀 수 있는 값

@PathVariable

  • url 경로에 값을 직접 포함시키는 방식
  • GET / DELECT / PUT 일 때 자주 사용
  • url 맨 뒤에 /products/{id}
  • 어떤 정보인지 리소스를 고유하게 식별하는 식별자
  • 무조건 필수로 사용해야 하는 값

@RequestBody

  • JSON, XML 등 요청 본문(body) 전체를 자바 객체로 자동 변환해서 매핑할 때 사용
  • POST / PUT 일 때 자주 사용
profile
잔디 속 새싹 하나

0개의 댓글