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

장박2바스단구터계니 #12

Open
wants to merge 60 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f9e0307
[1단계 - 상품 관리 기능] 박스터(한우석) 미션 제출합니다. (#171)
drunkenhw Apr 26, 2023
e648bf4
refactor(ProductRequest): 요청에 대한 검증 추가
drunkenhw Apr 29, 2023
553f0b4
refactor(ProductController): http 상태코드 변경
drunkenhw Apr 29, 2023
8562789
refactor(ProductDao): Optional로 수정
drunkenhw Apr 29, 2023
9bdc819
refactor(ProductResponse): wrapper 타입으로 변경
drunkenhw Apr 29, 2023
d6d5791
fix(DbProductDao): 없는 자원에 접근하면 예외 발생하는 오류 수정
drunkenhw Apr 29, 2023
bfbe857
refactor(ProductIntegrationTest): 순서에 상관없이 테스트가 돌아가도록 수정
drunkenhw Apr 29, 2023
7136b50
feat(ProductControllerAdvice): logger 추가
drunkenhw Apr 29, 2023
ef488cf
refactor(ProductDao): update 반환 타입 void로 변경
drunkenhw Apr 29, 2023
4417703
refactor(ProductController): 컨트롤러 분리
drunkenhw Apr 29, 2023
735a52c
feat(Product): 도메인 검증 로직 추가
drunkenhw Apr 29, 2023
d6c477a
feat(ProductControllerAdvice): 클라이언트에서 에러를 확인할 수 있는 기능 추가
drunkenhw Apr 30, 2023
45db0d5
refactor(ProductController): 수정 요청의 http 메서드 변경
drunkenhw Apr 30, 2023
e0ebacc
test(ProductController): controller 단위 테스트 추가
drunkenhw Apr 30, 2023
4c293d3
docs(README): 2단계 기능 구현 목록 추가
drunkenhw Apr 30, 2023
7d238dd
refactor(DbInit): 더미 데이터 넣는 방식 변경
drunkenhw Apr 30, 2023
bc28771
refactor(H2ProductDao): 클래스명 변경
drunkenhw Apr 30, 2023
cc9f732
feat(Member): 회원을 저장하는 기능 추가
drunkenhw Apr 30, 2023
88a844e
feat(MemberDao): 회원을 전체 조회하는 기능 추가
drunkenhw Apr 30, 2023
23204b6
refactor(H2ProductDao): 상수 접근 제어자 변경
drunkenhw Apr 30, 2023
38f6d6e
feat(MemberService): 전체 회원을 조회하는 기능 추가
drunkenhw Apr 30, 2023
4f73250
feat(PageController): /settings 로 접근할 경우 전체 사용자 조회
drunkenhw Apr 30, 2023
2d5de0c
refactor(PageController): 사용자 조회 시 dto가 반환되도록 변경
drunkenhw Apr 30, 2023
79f6030
refactor(Member): equals and hashcode 구현
drunkenhw Apr 30, 2023
b21565c
docs(README): 기능 구현 목록 수정
drunkenhw Apr 30, 2023
dd3558c
refactor(controller): dto 패키지 이동
drunkenhw Apr 30, 2023
c19b047
feat(ProductCart): 장바구니 저장 기능 추가
drunkenhw May 1, 2023
1cf3d23
feat(ProductCartDao): member를 기준으로 전체를 찾는 기능 추가
drunkenhw May 1, 2023
68a62e1
feat(ProductCartService): 내 장바구니를 조회하는 기능 추가
drunkenhw May 1, 2023
4d9df77
feat(ArgumentResolver): argument resolver 생성
drunkenhw May 2, 2023
9e311e1
style(MemberService): static import
drunkenhw May 2, 2023
b26bb07
fix(ControllerTest): 깨지는 테스트 수정
drunkenhw May 2, 2023
8b1ca97
refactor(ProductController): 중복된 url 제거
drunkenhw May 2, 2023
4d897d2
refactor(ProductCartController): 내 장바구니 조회 기능 추가
drunkenhw May 2, 2023
bebac97
feat(ProductCartService): 장바구니 추가하는 기능 추가
drunkenhw May 2, 2023
c8641e8
feat(ProductCartDao): 장바구니 삭제 기능 추가
drunkenhw May 2, 2023
783da48
refactor(dto): dto 패키지 이동
drunkenhw May 2, 2023
b1e8ccc
refactor(service): service에서 dto를 반환하도록 변경
drunkenhw May 3, 2023
16fea55
feat(ProductCartDao): cart id와 member로 존재하는지 검증하는 기능 추가
drunkenhw May 3, 2023
590a390
feat(DbInit): dummy 데이터 추가
drunkenhw May 3, 2023
b4e4d55
feat(cart.html): html, js 수정
drunkenhw May 3, 2023
9ad8cb0
feat(Authentication): 인증 관련 예외 추가
drunkenhw May 3, 2023
32d3221
feat(ProductCartService): 예외 처리 추가
drunkenhw May 3, 2023
f30f6b0
fix(ProductCartController): 오타 수정
drunkenhw May 3, 2023
764e7a3
test(ProductIntegrationTest): 통합 테스트 추가
drunkenhw May 3, 2023
b73a42a
chore(ProductControllerTest): display name 추가
drunkenhw May 3, 2023
226bb5b
refactor(AuthenticationArgumentResolver): 회원 조회와 인증 분리
drunkenhw May 3, 2023
f717690
feat: 검증 기능 추가
drunkenhw May 3, 2023
77ca508
refactor: 상수 추출
drunkenhw May 6, 2023
36f2ae9
refactor: argument resolver에서 dto를 반환하도록 변경
drunkenhw May 6, 2023
0bc7551
refactor: 다른 dao에서 entity 참조 제거
drunkenhw May 6, 2023
b00f4a5
feat: 예외처리 기능 추가
drunkenhw May 6, 2023
44b593a
refactor: 불필요한 생성자 제거
drunkenhw May 6, 2023
6a3db19
feat: 인증 정보를 추출하는 객체 생성
drunkenhw May 6, 2023
4c80a76
refactor: 검증 메서지 구체화
drunkenhw May 6, 2023
660f85a
refactor: authentication 예외 처리 추가
drunkenhw May 7, 2023
6642bf4
feat: product에 soft delete 적용
drunkenhw May 7, 2023
887acd7
refactor: 불필요한 어노테이션 제거
drunkenhw May 8, 2023
73a8d5a
fix: 오타 수정
drunkenhw May 8, 2023
aee5a62
refactor: authentication extractor 생성 방식 변경
drunkenhw May 8, 2023
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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
# jwp-shopping-cart

### 기능 목록

- [x] 상품 목록 페이지 연동
- [x] 상품 객체 생성 (ID, 이름, 이미지, 가격)
- [x] `/`로 접근할 경우 상품 목록 페이지 조회

- [x] 상품 관리 CRUD API 작성
- [x] 상품 생성
- [x] 상품 목록 조회
- [x] 상품 수정
- [x] 상품 삭제

- [x] 관리자 도구 페이지 연동
- [x] `/admin`로 접근할 경우 전체 상품 조회
- [x] `상품 추가` 클릭 시 상품 생성 API 호출
- [x] `수정` 클릭 시 상품 수정 API 호출
- [x] `삭제` 클릭 시 상품 삭제 API 호출

- [ ] 사용자 기능 구현
- [x] 사용자 저장
- [x] `/settings`로 접근할 경우 전체 사용자 조회
- [ ] `select` 클릭 시 이후 요청에 사용자 인증 정보 포함

- [ ] 장바구니 기능 구현

Choose a reason for hiding this comment

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

뭐죠? x해줘요

- [ ] 사용자 인증 정보는 요청 header에 포함
- [ ] basic 인증 사용
- [ ] `담기` 클릭 시 장바구니에 추가
- [ ] `/cart`로 접근할 경우 전체 장바구니 목록 조회
- [ ] `delete`클릭 시 장바구니에서 제거

### 리팩터링 목록

- [x] 예외 처리
- [x] 전체 test 코드 작성
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:4.4.0'
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/cart/DbInit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cart;

import cart.dao.MemberDao;
import cart.dao.ProductDao;
import cart.entity.Member;
import cart.entity.Product;
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class DbInit {

private final ProductDao productDao;
private final MemberDao memberDao;

public DbInit(ProductDao productDao, MemberDao memberDao) {
this.productDao = productDao;
this.memberDao = memberDao;
}

@PostConstruct
private void saveDummyData() {
productDao.save(new Product(
"피자",
"https://cdn.dominos.co.kr/admin/upload/goods/20200311_x8StB1t3.jpg",
13000));
productDao.save(new Product(
"샐러드",
"https://m.subway.co.kr/upload/menu/K-%EB%B0%94%EB%B9%84%ED%81%90-%EC%83%90%EB%9F%AC%EB%93%9C-%EB%8B%A8%ED%92%88_20220413025007802.png",
20000));
productDao.save(new Product(
"치킨",
"https://cdn.thescoop.co.kr/news/photo/202010/41306_58347_1055.jpg",
10000));
Comment on lines +10 to +34
Copy link
Member

Choose a reason for hiding this comment

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

data.sql 파일을 이용해서 초기값을 설정해보는건 어떨까? 🤔

Choose a reason for hiding this comment

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

더미데이터 넣을 때 data.sql 안쓰고 @PostConstruct한 이유가 뭐야?


memberDao.save(new Member(
"[email protected]",
"boxster"
)
);
memberDao.save(new Member(
"[email protected]",
"member"
)
);
}
Comment on lines +22 to +46

Choose a reason for hiding this comment

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

@PostConstruct 굳 👍

한편으로는 sql 작성에 비해 dao 구현에 의존적이라 dao 변경에 영향을 받을 수 있을 것 같은데 어떻게 생각해?

Copy link
Member Author

Choose a reason for hiding this comment

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

sql 은 sql에 의존적이라고 생각해서 이렇게 했어요~
만약 nosql 로 바뀐다면?
쿼리 문법이 바뀐다면?
이라는 생각에요~

Choose a reason for hiding this comment

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

다즐 의견에 동의합니다

}
20 changes: 20 additions & 0 deletions src/main/java/cart/auth/AuthMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cart.auth;

public class AuthMember {

private final String email;
private final String password;

public AuthMember(String email, String password) {
this.email = email;
this.password = password;
}

public String getEmail() {
return email;
}

public String getPassword() {
return password;
}
}
11 changes: 11 additions & 0 deletions src/main/java/cart/auth/AuthPrincipal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package cart.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthPrincipal {
Copy link
Member

Choose a reason for hiding this comment

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

Custom Annotation 👍

}
31 changes: 31 additions & 0 deletions src/main/java/cart/auth/AuthenticationArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cart.auth;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

Choose a reason for hiding this comment

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

argumentResolver를 bean으로 등록하는건 어떻게 생각해?


private static final String AUTHORIZATION = "Authorization";
Copy link
Member

Choose a reason for hiding this comment

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

어떤 리뷰에서 봤는데 Spring이 제공하는 HttpHeaders.AUTHORIZATION 있다고 하네 나도 그걸 쓸까 했는데 머지댐


private final AuthenticationExtractor authenticationExtractor;

public AuthenticationArgumentResolver(AuthenticationExtractor authenticationExtractor) {
this.authenticationExtractor = authenticationExtractor;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.withContainingClass(AuthMember.class)
.hasParameterAnnotation(AuthPrincipal.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
String authentication = webRequest.getHeader(AUTHORIZATION);
return authenticationExtractor.extractAuthInfo(authentication);
Comment on lines +28 to +29

Choose a reason for hiding this comment

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

깔끔 👍

}
}
7 changes: 7 additions & 0 deletions src/main/java/cart/auth/AuthenticationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cart.auth;

public class AuthenticationException extends RuntimeException {
public AuthenticationException() {
super("인증에 실패했습니다.");
}
}
61 changes: 61 additions & 0 deletions src/main/java/cart/auth/AuthenticationExtractor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cart.auth;

import java.util.Base64;
import java.util.regex.Pattern;

public class AuthenticationExtractor {

private static final String BASIC_PREFIX = "Basic ";
private static final String BASIC_DELIMITER = ":";
private static final String EMPTY = "";
private static final Pattern BASIC_CREDENTIAL_PATTERN = Pattern.compile("^Basic [A-Za-z0-9+/]+=*$");
private static final int EMAIL = 0;
private static final int PASSWORD = 1;
Comment on lines +12 to +13

Choose a reason for hiding this comment

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

사소하지만 INDEX를 명시해주는건 어떻게 생각하시나용?


public AuthMember extractAuthInfo(String authentication) {
validateCredentials(authentication);
String[] emailAndPassword = extractBasicAuthInfo(authentication);
String email = emailAndPassword[EMAIL];
String password = emailAndPassword[PASSWORD];
return new AuthMember(email, password);
}

private void validateCredentials(String authorization) {
validateNull(authorization);
validateBasicAuth(authorization);
}

Choose a reason for hiding this comment

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

깔끔하고 멋지네여 👍


private void validateNull(String authorization) {
if (authorization == null || authorization.isBlank()) {
throw new AuthenticationException();
}
}

private void validateBasicAuth(String authorization) {
if (!BASIC_CREDENTIAL_PATTERN.matcher(authorization).matches()) {

Choose a reason for hiding this comment

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

개인적으로 정규 표현식 너무 가독성 떨어진다고 생각하는데 어떻게 생각하시죠 박박박스터씨?

throw new AuthenticationException();
}
}

private String[] extractBasicAuthInfo(String authorization) {
String credentials = authorization.replace(BASIC_PREFIX, EMPTY);
String decodedString = decodeCredentials(credentials);
String[] emailAndPassword = decodedString.split(BASIC_DELIMITER);
validateLength(emailAndPassword);
return emailAndPassword;
}

private String decodeCredentials(String credentials) {
try {
return new String(Base64.getDecoder().decode(credentials));
} catch (IllegalArgumentException e) {
throw new AuthenticationException();
}
}

private void validateLength(String[] emailAndPassword) {
if (emailAndPassword.length != 2) {
throw new AuthenticationException();
}
}
}
25 changes: 25 additions & 0 deletions src/main/java/cart/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cart.config;

import cart.auth.AuthenticationArgumentResolver;
import cart.auth.AuthenticationExtractor;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@Scope
Copy link
Member

Choose a reason for hiding this comment

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

Scope는 어떤 이유로 사용했어? 🤔

Choose a reason for hiding this comment

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

붙어 있는 이유가 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

아 개망했다 뭐 테스트해본다고 붙인건데 이거 붙여놓고 리뷰요청 보냈네 ;;;;;;

public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticationArgumentResolver(authenticationExtractor()));
}

@Bean
public AuthenticationExtractor authenticationExtractor() {
return new AuthenticationExtractor();
}
}
45 changes: 45 additions & 0 deletions src/main/java/cart/controller/ControllerAdvice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cart.controller;

import cart.auth.AuthenticationException;
import cart.dto.ErrorResponse;
import java.util.NoSuchElementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
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

Choose a reason for hiding this comment

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

각각 로그 남기는건 어떨까용?

public class ControllerAdvice {

private final Logger logger = LoggerFactory.getLogger(getClass());

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleException(NoSuchElementException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleException(MethodArgumentNotValidException e) {
String defaultMessage = e.getBindingResult().getFieldError().getDefaultMessage();
return ResponseEntity.badRequest().body(new ErrorResponse(defaultMessage));
}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleException(IllegalArgumentException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleException(AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(e.getMessage()));
Copy link
Member

Choose a reason for hiding this comment

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

인증을 위해 들어오는 값에 대한 로깅을 조금 더 자세히 남겨보는건 어떨까?

}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handleException(Exception e) {
logger.error(e.getMessage());
return ResponseEntity.internalServerError().body(new ErrorResponse("알 수 없는 에러가 발생했습니다."));
}
}
47 changes: 47 additions & 0 deletions src/main/java/cart/controller/PageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cart.controller;

import cart.dto.MemberResponse;
import cart.dto.ProductResponse;
import cart.service.MemberService;
import cart.service.ProductService;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {
private final ProductService productService;
private final MemberService memberService;

public PageController(ProductService productService, MemberService memberService) {
this.productService = productService;
this.memberService = memberService;
}

@GetMapping("/")
public String index(Model model) {
List<ProductResponse> products = productService.findProducts();
model.addAttribute("products", products);
return "index.html";
}

@GetMapping("/admin")
public String admin(Model model) {
List<ProductResponse> products = productService.findProducts();
model.addAttribute("products", products);
return "admin.html";
}

@GetMapping("/settings")
public String setting(Model model) {
List<MemberResponse> members = memberService.findMembers();
model.addAttribute("members", members);
return "settings.html";
}

@GetMapping("/cart")
public String cart() {
return "cart.html";

Choose a reason for hiding this comment

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

확장자 명시 굿

}
}
Loading