Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

경북대 BE_김동현_1주차 과제(3단계) #201

Open
wants to merge 45 commits into
base: donghyuun
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
902b824
chore: build setting modification
donghyuun Jun 26, 2024
4e8d336
docs: post README.md
donghyuun Jun 26, 2024
b8a1e9b
feat: create Product class
donghyuun Jun 26, 2024
1bb950d
feat: create Product DTO
donghyuun Jun 26, 2024
ddd17ee
refactor: Product
donghyuun Jun 26, 2024
5cdd9db
feat: add product
donghyuun Jun 26, 2024
9e860bf
feat: get product(s)
donghyuun Jun 26, 2024
d6fcb53
refactor: 상품의 id 정적 변수 -> 지역 변수 사용으로 변경
donghyuun Jun 26, 2024
5e3b595
feat: modify product
donghyuun Jun 26, 2024
22320ce
feat: delete product
donghyuun Jun 26, 2024
b260e4a
refactor: 모든 요청 방식 form-data -> raw-json 변경
donghyuun Jun 26, 2024
c937c17
refactor: controller -> controller + service
donghyuun Jun 26, 2024
ceb052a
feat: post, get 테스트코드 작성
donghyuun Jun 26, 2024
29d8beb
fix: @PathVariable 에 경로 변수 이름 명시
donghyuun Jun 26, 2024
7d6874f
feat: update, delete 테스트코드 작성
donghyuun Jun 26, 2024
5a6d0d3
fix: javadoc 의 설명 부분 한칸 띄움
donghyuun Jun 26, 2024
35718a5
chore: java 버전 변경 17 -> 21
donghyuun Jun 26, 2024
93d2b81
fix: API 수정 "/products" -> "/api/products"
donghyuun Jun 26, 2024
43d6adc
docs(README): add step2 README
donghyuun Jun 28, 2024
e104fac
feat: 상품 조회 기능(홈 페이지의 일부 기능)
donghyuun Jun 28, 2024
b99a57c
refactor: controller 내 상품 저장소 -> Repository 클래스 생성
donghyuun Jun 28, 2024
19f0229
feat: 상품 추가 기능
donghyuun Jun 28, 2024
0eafb22
feat: 상품 삭제, 수정 기능
donghyuun Jun 28, 2024
60171d0
docs(JavaDoc): javadoc 수정
donghyuun Jun 28, 2024
a446935
refactor: 파일명 대문자로 수정
donghyuun Jun 28, 2024
304c9db
feat: JdbcTemplate 설정 및 Product 테이블 생성
donghyuun Jun 28, 2024
0c195c7
feat: 상품 조회 기능
donghyuun Jun 28, 2024
e1710be
feat: 상품 추가 기능
donghyuun Jun 28, 2024
fd7f6a5
feat: 상품 수정 기능
donghyuun Jun 28, 2024
9f832a2
feat: 체크 박스로 선택된 상품들 제거
donghyuun Jun 28, 2024
f3c8fc5
refactor: 불필요한 Mapping 및 메서드 삭제
donghyuun Jun 28, 2024
0997aa1
refactor: 데이터베이스 초기화 설정
donghyuun Jun 28, 2024
7af869e
remove: 기존 클래스 내 저장소 파일 제거
donghyuun Jun 28, 2024
8012427
fix: 직접 ID 생성, 증가시키는 코드 제거
donghyuun Jun 28, 2024
37ff02d
refactor: 주석 정리
donghyuun Jun 28, 2024
adb5221
remove: 코드로 db 초기화하는 파일 제거(불필요)
donghyuun Jun 28, 2024
f1ba685
refactor: @Controller + @ResponseBody -> @RestController
donghyuun Jun 28, 2024
d67b2a9
chore: spring-boot-starter-validation 의존성 추가
donghyuun Jun 30, 2024
ca31aa2
fix: 응답 메시지 정상 출력되도록
donghyuun Jun 30, 2024
fb8bbb8
feat: 예외 처리 & 응답 body 에 상황 별 ResponseDTO 사용
donghyuun Jun 30, 2024
96a9416
refactor: 폴더 이름 소문자로 변경
donghyuun Jul 3, 2024
c633d3e
refactor: @RequestMapping value 속성명 생략, javadoc 설명 추상화
donghyuun Jul 3, 2024
5a61b96
refactor: 응답(에러포함) 생성 방법 간소화, Error/Result ENUM 클래스 사용 X
donghyuun Jul 3, 2024
682473e
refactor: 유효성 검사 방식 변경, @Valid 어노테이션 사용
donghyuun Jul 3, 2024
9e8fa45
remove: Result/Errorcode Enum 클래스 삭제
donghyuun Jul 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
# spring-gift-product
# spring-gift-product

## 기능 목록
- 상품 추가 기능
- 상품 조회 기능
- 상품 수정 기능
- 상품 삭제 기능
- 상품 테스트 코드 작성
- 리팩토링
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ dependencies {
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

implementation 'org.springframework.boot:spring-boot-starter-validation'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 validation 기능을 활용하셨군요 좋습니당! 👍 저도 좋아합니다!


}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/gift/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}


}
37 changes: 37 additions & 0 deletions src/main/java/gift/Controller/BasicController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gift.Controller;

import gift.Service.ProductService;
import gift.Model.Product;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping(value = "/api")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별건 아니지만 줄일 수 있는 부분은 줄이는 것이 좋은 것 같아요ㅎㅎ

Suggested change
@RequestMapping(value = "/api")
@RequestMapping("/api")

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 반영하겠습니다!

public class BasicController {

private final ProductService productService;

@Autowired
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자가 1개인 경우에는 @Autowired 빼셔도 됩니다! 알고 계시겠지만 혹시나 하여 남깁니다! 🙏

Suggested change
@Autowired

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 알겠습니다!

public BasicController(ProductService productService) {
this.productService = productService;
}

/**
* 홈 화면으로 이동, 상품 목록 넘겨줌
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이러한 주석은 코드와 강결합된 주석이라서요ㅎㅎ 코드가 바뀜에 따라 변경되지 않을 확률이 매우 높습니다!
제 개인적으로 주석은 맥락 이해를 돕거나 의사 결정을 내린 부분 등 코드로 파악하기 어려운 부분만 남기는 것이 좋은 것 같다는 생각입니다ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멘토님 말씀 듣고 생각해보니 추상화 시키는 것이 좋은 것 같습니다!

*
* @param model
* @return 홈 화면 html 명
*/
@GetMapping
String homePage(Model model) {
// 현재 상품 목록 조회
List<Product> products = productService.getProducts();
model.addAttribute("products", products);
return "index";
}
}
106 changes: 106 additions & 0 deletions src/main/java/gift/Controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package gift.Controller;

import gift.Global.Response.ErrorCode;
import gift.Global.Response.ErrorResponseDto;
import gift.Global.Response.ResponseMaker;
import gift.Global.Response.ResultCode;
import gift.Global.Response.ResultResponseDto;
import gift.Global.Response.SimpleResultResponseDto;
import gift.Service.ProductService;
import gift.DTO.ProductDTO;
import gift.Model.Product;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import javax.swing.text.StyledEditorKit.BoldAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/api/products")
public class ProductController {

private final ProductService productService;

@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}

/**
* 상품 추가
*
* @param productDTO
* @return 결과 메시지
*/
@PostMapping
public ResponseEntity<SimpleResultResponseDto> postProduct(@Valid @ModelAttribute ProductDTO productDTO) {
productService.postProduct(productDTO);
return ResponseMaker.createSimpleResponse(ResultCode.CREATE_PRODUCT_SUCCESS);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서로 다른 클래스를 왔다갔다 하면서 ResultCode.CREATE_PRODUCT_SUCCESS까지 관리해줘서 얻을 이점이 있을지 모르겠네요! 단순하게 ResponseEntity.ok를 하면 훨씬 간단할 것 같아서요ㅎㅎ 작업 부담도 덜구요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 사항으로 ResultCode 등의 Enum 에러 코드 사용하지 않도록 하였습니다.

모든 상황의 성공에 ok 를 붙이는 건 디테일 하지 않다고 생각하여 서비스or에러 핸들러 부분에서 응답 코드, 메시지를 매개변수로 입력하면 그에 맞게 응답 객체를 생성하도록 했습니다. pr 올리겠습니다ㅎㅎ

}

/**
* 전체 상품 목록 조회
*
* @return products (상품 목록)
*/
@GetMapping
public ResponseEntity<ResultResponseDto<List<Product>>> getProducts() {
List<Product> products = productService.getProducts();
// 성공 시
return ResponseMaker.createResponse(ResultCode.GET_ALL_PRODUCTS_SUCCESS, products);
}


/**
* 상품 수정
*
* @param id
* @param productDTO
* @return 결과 메시지
*/
@PutMapping("/{id}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삭제와 함께 적절한 HttpMethod 사용 좋네요! 👍

public ResponseEntity<SimpleResultResponseDto> updateProduct(@PathVariable("id") Long id,
@RequestBody ProductDTO productDTO) {
productService.updateProduct(id, productDTO);
return ResponseMaker.createSimpleResponse(ResultCode.UPDATE_PRODUCT_SUCCESS);
}


/**
* 해당 ID 리스트에 속한 상품 삭제
*
* @param productIds
* @return 결과 메시지
*/
@DeleteMapping
public ResponseEntity<?> deleteSelectedProducts(@RequestBody List<Long> productIds) {
productService.deleteProductsByIds(productIds);
return ResponseMaker.createSimpleResponse(ResultCode.DELETE_PRODUCTS_SUCCESS);
}

/**
* 해당 ID 를 가진 상품 삭제
*
* @param id
* @return 결과 메시지
*/
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteProduct(@PathVariable("id") Long id) {
productService.deleteProduct(id);
return ResponseMaker.createSimpleResponse(ResultCode.DELETE_PRODUCT_SUCCESS);
}


}
41 changes: 41 additions & 0 deletions src/main/java/gift/DTO/ProductDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gift.DTO;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자바에선 최근에 요러한 케이스에 대해 dto 그리고 클래스 이름은 ProductDto로 하는 것이 일반적인 것 같네요ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 분들의 코드도 살펴보니 그런 것 같습니다 반영하겠습니다!


public class ProductDTO {

private String name;
private int price;
private String imageUrl;

public ProductDTO() {
}

public ProductDTO(String name, int price, String imageUrl) {
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}

public String getImageUrl() {
return imageUrl;
}

public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
}
22 changes: 22 additions & 0 deletions src/main/java/gift/Global/Exception/BusinessException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gift.Global.Exception;

import gift.Global.Response.ErrorCode;

/**
* RuntimeException 을 상속받는 커스텀 에러
* 개발자가 직접 날리는 에러
*/
public class BusinessException extends RuntimeException{

private final ErrorCode errorCode;

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public ErrorCode getErrorCode() {
return errorCode;
}

}
38 changes: 38 additions & 0 deletions src/main/java/gift/Global/Handler/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gift.Global.Handler;

import gift.Global.Exception.BusinessException;
import gift.Global.Response.ErrorCode;
import gift.Global.Response.ErrorResponseDto;
import gift.Global.Response.ResponseMaker;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 핸들링 처리하신 부분 좋네요! 👍


/**
* RuntimeException 을 상속받는 커스텀 에러 핸들러
* 개발자가 직접 날리는 에러
* @param e
* @return
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponseDto> handleBusinessException(BusinessException e) {
return ResponseMaker.createErrorResponse(e.getErrorCode());
}


/**
* MethodArgumentNotValidException 에러 핸들러
* 매개변수 인자가 유효하지 않은 경우 발생
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponseDto> MethodArgumentNotValidException(
MethodArgumentNotValidException e) {
return ResponseMaker.createErrorResponse(ErrorCode.INVALID_PRODUCT_ARGUMENT);
}
}
38 changes: 38 additions & 0 deletions src/main/java/gift/Global/Response/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gift.Global.Response;

public enum ErrorCode {

// Product CRUD 동작
GET_ALL_PRODUCTS_FAILED(400, "PE001", "전체 상품 목록 조회에 실패했습니다."),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상품 외에 다른 도메인이 추가된다면 서로 다른 도메인 간의 결합이 해당 클래스 안에서 일어날 것 같네요ㅎㅎ
ex) member

추후에 분리를 염두해두면 좋을 것 같습니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 PE004 요러한 코드는 최근에 사용하지 않는 추세입니다:)
이러한 코드는 식별성이 떨어지는 반면, HTTP Status와 Message로 구분하기에 충분하여 그렇습니다ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분의 수정 사항에 대해 pr 요청 시 설명 기입해놓겠습니다!

CREATE_PRODUCT_FAILED(400, "PE002", "상품 추가에 실패했습니다."),
UPDATE_PRODUCT_FAILED(400, "PE003", "상품 수정에 실패했습니다."),
DELETE_PRODUCT_FAILED(400, "PE004", "상품 삭제에 실패했습니다."),
DELETE_PRODUCTS_FAILED(400, "PE005", "상품들을 삭제에 실패했습니다."),

// 값 유효성 검사
INVALID_PRODUCT_ARGUMENT(400, "VE001", "상품 입력 정보가 유효하지 않습니다."),

;
;
private final int status;
private final String code;
private final String message;

ErrorCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}

public int getStatus() {
return status;
}

public String getCode() {
return code;
}

public String getMessage() {
return message;
}
}
7 changes: 7 additions & 0 deletions src/main/java/gift/Global/Response/ErrorResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gift.Global.Response;

public record ErrorResponseDto(String code, String message) {
public ErrorResponseDto(ErrorCode errorCode) {
this(errorCode.getCode(), errorCode.getMessage());
}
}
44 changes: 44 additions & 0 deletions src/main/java/gift/Global/Response/ResponseMaker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package gift.Global.Response;

import org.springframework.http.ResponseEntity;

/**
* 응답 객체를 생성하는 클래스
*/
public class ResponseMaker {

/**
* BODY 에 성공 메시지와 데이터를 보냄
* @param resultCode
* @param data
* @return
* @param <T>
*/
public static <T> ResponseEntity<ResultResponseDto<T>> createResponse(ResultCode resultCode, T data) {
ResultResponseDto<T> resultResponseDto = new ResultResponseDto<>(resultCode, data);
return ResponseEntity.status(resultCode.getStatus())
.body(resultResponseDto);
}

/**
* BODY 에 성공 메시지만 보냄 (데이터 X)
* @param resultCode
* @return
*/
public static ResponseEntity<SimpleResultResponseDto> createSimpleResponse(ResultCode resultCode) {
SimpleResultResponseDto simpleResultResponseDto = new SimpleResultResponseDto(resultCode);
return ResponseEntity.status(resultCode.getStatus())
.body(simpleResultResponseDto);
}

/**
* BODY 에 에러 메시지만 보냄 (데이터 X)
* @param errorCode
* @return
*/
public static ResponseEntity<ErrorResponseDto> createErrorResponse(ErrorCode errorCode) {
ErrorResponseDto errorResponseDto = new ErrorResponseDto(errorCode);
return ResponseEntity.status(errorCode.getStatus())
.body(errorResponseDto);
}
}
Loading