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_이영준_2주차 과제 step2,step3 #426

Open
wants to merge 54 commits into
base: 20jcode
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
bb85df0
docs : 0단계 준비 문서 작성
20jcode Jul 1, 2024
9ccee7e
init : 이전 작업 코드 복사
20jcode Jul 1, 2024
2edaa1f
docs : step1 milestone 추가
20jcode Jul 3, 2024
963dd37
build : spring validation 추가
20jcode Jul 3, 2024
d79909e
build : spring webflux 추가
20jcode Jul 3, 2024
4dc8e45
feat : NotNull, namesize 추가
20jcode Jul 3, 2024
1ce8f4b
fix : add 에서 @RequestBody 가 없어서 생기는 500오류 해결
20jcode Jul 3, 2024
a98e47d
docs : 작업 일지 추가
20jcode Jul 4, 2024
6bfa3e5
refact : api request를 위한 공통 경로 정리
20jcode Jul 4, 2024
8c1cf2d
test : api 200 응답 테스트 추가
20jcode Jul 4, 2024
1ef4e42
refact : 모호한 변수 명 수정
20jcode Jul 4, 2024
59b5741
refact : 모호한 변수 명 수정, 공통 경로 정리+rest 하게 경로 설정
20jcode Jul 4, 2024
c1e4cee
feat : product 생성 시 name 길이 Valid 추가
20jcode Jul 4, 2024
1c68863
refact : NotNull과 size 어노테이션 중복 수정
20jcode Jul 4, 2024
9bed6ab
feat : Product 이름 테스트 코드 작성
20jcode Jul 4, 2024
dd205dc
refact : 문자열이 비어있는 경우를 위해 어노테이션 변경
20jcode Jul 4, 2024
f9637f0
test : 가격과 이미지가 비어있는 경우 테스트 추가
20jcode Jul 4, 2024
8f3fa03
feat : 상품이름 특수문자 탐색 Valid 추가
20jcode Jul 4, 2024
b11dde5
feat : 카카오 사용 Valid 추가
20jcode Jul 4, 2024
0a578c4
docs : 작업일지 업데이트
20jcode Jul 4, 2024
8fb7997
chore : 가독성을 위한 정렬
20jcode Jul 4, 2024
1f1844a
docs : milestone 추가
20jcode Jul 5, 2024
a9c327b
feat : 회원 모델 + 회원 등급 추가
20jcode Jul 5, 2024
9bdaa3d
build : JWT 의존성 추가
20jcode Jul 5, 2024
7ec1633
feat : memberRepository 추가
20jcode Jul 6, 2024
9925735
feat : memberService 추가
20jcode Jul 6, 2024
5f4d556
feat : memberController 추가
20jcode Jul 6, 2024
45ce4a6
refact : productexception HTTP status 추가
20jcode Jul 6, 2024
0caa2b3
test : memberController 테스트
20jcode Jul 6, 2024
771d69a
docs : 문서 업데이트, 수정사항 등에 대해
20jcode Jul 6, 2024
997d53c
test : 로그인 성공 테스트
20jcode Jul 6, 2024
9382b54
refact : JWT 보안 문제 수정
20jcode Jul 6, 2024
3c1c770
docs : step3 문서 생성
20jcode Jul 6, 2024
953b831
feat : 위시리스트 모델 만들기
20jcode Jul 6, 2024
3b73a9c
feat : 위시리스트 Repository 만들기
20jcode Jul 6, 2024
ab8035d
feat : 위시리스트 서비스 추가
20jcode Jul 6, 2024
ac1b086
refact : LoginToken 인증을 위해 일부 수정
20jcode Jul 6, 2024
2c3d350
refact : member Role 수정
20jcode Jul 7, 2024
dd1d339
refact : 변수명 수정
20jcode Jul 7, 2024
2b72758
fix : 버그 수정
20jcode Jul 7, 2024
98a90c2
feat : wishlist controller 추가
20jcode Jul 7, 2024
098ac44
refact : repository 규격 통일
20jcode Jul 7, 2024
c9af84d
fix : Product 테스트 통과
20jcode Jul 7, 2024
75ccd13
fix : Member 테스트 통과
20jcode Jul 7, 2024
b8f6454
refact : 클래스 이름 변경
20jcode Jul 7, 2024
f5787dd
refact : PR 반영 리펙토링
20jcode Jul 8, 2024
9f5fefb
refact : 클래스 이름 변경, 간단 주석 추가
20jcode Jul 8, 2024
d09d4ff
feat : 이메일 유효성 검사 추가, 불필요 주석 삭제
20jcode Jul 8, 2024
309efa5
test : 없는 이메일로 로그인 시도 테스트
20jcode Jul 8, 2024
59eaa34
feat : controller test helper 생성
20jcode Jul 8, 2024
3b827c2
refact : token 기반 사용자 인증 기능 수정
20jcode Jul 8, 2024
8e77209
refact : wishlist 버그 수정
20jcode Jul 8, 2024
7bb04e2
test : wishlist add 테스트 추가
20jcode Jul 8, 2024
c2b54e8
Merge branch '20jcode' into step2
20jcode Jul 16, 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
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,71 @@

# 2주차 위시 리스트 - 요청과 응답 심화

### step 0
# step 0


1주차 코드 가져오기

1주차 PR에 대해서 잘못된 저장소에서 fork하는 바람에 리뷰를 받지 못하였습니다.


# step 1

목표

- 상품 추가, 수정 시 잘못된 값에 대한 처리 + 응답 설정
- 상품 이름 제한 : 공백 포함 최대 15글자
- 일부 특수 문자만 허용
- "카카오" 포함 문구 입력 시 따로 confirm 이후 진행 가능하도록

## milestone

-[X] 스프링 validation 의존성 추가
-[X] feat : DTO valid 추가
-[ ] refact : service - 상품 update 로직 변경 (하나로 통합)
-[X] feat : @ControllerAdvice 클래스 추가
-[X] feat : "카카오" 검사를 위한 예외클래스 추가

## milestone 2

회원 가입, 로그인, 추후 회원별 기능 이용을 위해

회원은 이메일과 비밀번호를 입력하여 가입한다.
토큰을 받으려면 이메일과 비밀번호를 보내야 하며, 가입한 이메일과 비밀번호가 일치하면 토큰이 발급된다.
토큰은 JWT 를 사용하도록 한다.
관리자는 회원을 조회 추가 수정 삭제 할 수 있다.

- [X] feat : 회원 모델 만들기
- [X] feat : 회원 Repository 만들기
- [X] feat : 회원 가입 서비스 만들기
- [X] feat : 회원 가입 컨트롤러 만들기
- [X] feat : 로그인 서비스 만들기
- [X] feat : 인증 서비스 만들기
- [X] feat : 인증 컨트롤러 만들기

### 회원 모델 만들기

Member.class : 회원모델
MemberRole.class : 회원 등급 enum class

### 응답 코드

헤더나 토큰이 유효하지 않은 경우 -> 401
잘못된 로그인, 비밀번호 찾기, 비밀번호 변경 요청 -> 403

## milestone 3

위시 리스트

위시 리스트는 일종의 장바구니로 생각할 수 있다.
위시 리스트에 등록된 상품 목록을 조회할 수 있다.
위시 리스트에 상품을 추가할 수 있다.
위시 리스트에 담긴 상품을 삭제할 수 있다.
사용자 정보는 요청 헤더의 Authorization 필드를 사용한다.

- [X] feat : 위시 리스트 모델 만들기
- [X] feat : 위시리스트 Repository 만들기
- [X] feat : 위시리스트 서비스 만들기
- [ ] feat : 위시리스트 컨트롤러 만들기
- [ ] feat : 인증을 위한 ArgumentResolver 만들기
- [ ] test : 테스트 코드로 동작 확인
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'io.projectreactor:reactor-test'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
compileOnly 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
}

tasks.named('test') {
Expand Down
103 changes: 103 additions & 0 deletions history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 작업 일지

스스로 기록용도로 작성

## 7월 4일 (목)

### PR 코멘트 반영하기

1. ~~GetMapping 등에서 url 이 동일한 것에 대해서 통합표기 (@controller)~~
2. rest 도메인과 관련해서 엔드포인트 네이밍의 해석의 모호함
3. 문법 띄워쓰기 관련 google java style guide 참고하기 (내가 못하면 cmd + option + L) 을 매번 눌러주자
4. ~~pm 변수명 삭제 → productService로 변경 (처음에는 productManager 정도로 생각하고 임시로 작성하였다가 바꾸지 않았던 것이 문제가 됨)~~
5. 불필요한 주석 삭제 → 사용하지 않는 것들에 대해서 (가독성 떨어뜨림)

에 대해서 진행

### valid 적용 테스트 코드 작성하기

이름 길이가 15글자를 넘는 경우와 0글자인 경우에 대해 로직과 테스트 추가

### TODO

예외에 대한 메세지가 너무 흩어져있어서 관리가 안되고 있다고 생각되어짐.
한 군데로 모아서 예외에 대한 관리를 진행할 필요가 있다고 생각.
예외 테스트코드에서 문자열 비교를 통해서 assert 하는 것은 너무 안좋은 방법이라고 생각되어짐.

## 7월 5일 (금)

1. 회원가입 관련 구현
2. 로그인 관련 구현

궁금증 : 모델에서 null 허용이 되는 wrapper class를 사용하는 것이 좋은 방법일까? 나쁜 방법일까?

-[ ] Product 에 setter가 필요한가?

readme 와 history와 notion 3개의 중복된 문서 작성 문제... 굳이 필요없는 내용을 많이 남긴다는 생각 자체가 잘못된 것 같기도?
주말에 특정한 규칙을 정하기 필요

-[ ] 문서정리규칙 정하기

### 수정해야할 것

1. 규격화된 테스트로 전체적인 동작에 문제가 없는지 확인하기
2. 컨트롤러 - 서비스 - 모델 사이에서 명확하게 책임 정해서 작업하기
3. JWT 공부해서 제대로 적용시키기 (비밀키 숨기기)
4. 인증토큰에 대해서 서버에서 어떻게 인증토큰을 통해서 권한을 설정하는지 제대로 알고 적용시키기

- [ ] 테스트코드를 위한 보일러플레이트 클래스를 만들어서 사용해도 되는가? - 좋은가? 나쁜가?

- [ ] Repository는 정확히 어떤 책임을 지고 있는 것인지? 새로운 멤버를 등록하였다면 이걸 다시 Model로 반환해줘야하는가?

- [ ] Repository는 무엇을 얼마만큼 제어해야할까?
만약 Model의 내부 값이 변하면, 이것은 service에서 하나하나 분리해서 repository를 업데이트 해줘야하는가?

## 로그인 인증 과정에서의 어려움

상품과 멤버에 대해 컨트롤러와 서비스에서 규칙 없이 작성하였고, 위시 리스트 구현에서 어디서 무엇을 가져와서 어썬 식으로 사용해야할 지
생각하기에 어려움이 너무 커졌다.

# 7월 7일 refact 계획

1. dto, 컨트롤, 서비스, repository에 대해 통일성 부여하기

dto -> 데이터 transfer를 위해 존재
dto -> 모델로 변환하는 로직은 있으면 좋겠다는 생각 (model은 dto를 몰라도 되도록)
클라이언트 -> 컨트롤러 (dto사용) -> 서비스 (같은 dto 사용??)

근데 서비스에서는 특정 필드의 값만 필요한 경우가 있다.
서비스 내부에서는 항상 model로 변환해서? 서비스는 여러 모델들의 협업?

### repository 들 먼저 구조 변경해주기

실제 사용되는 String, int 등의 value를 직접 넘겨주도록 한다.
repository는 결국 DB와 매우 큰 밀착관계이고, Model을 몰라도 된다?

반환은 Model을 해준다?

- [X] Repository 통일성 부여 : 기본 검색은 id를 기반으로 한다. Model을 params로 받지 않는다. 반환은 Model로 해준다.

# 7월 8일 (월)

1. 피드백에 따라서 수정 진행
2. wishlist 완벽하게 동작하도록 수정하기

## 피드백 반영

1. 공통적인 포멧이 있는 것은 좋지만, DTO는 용도에 맞게 사용하는 것이 좋다. 불필요한 데이터까지 내줄 필요 없음

2. 문서는 코드 자체로 잘 이해할 수 있도록 해두는 것이 좋다. -> 클린코드

3. 불필요한 축약어 사용 X, stream 적극적인 활용 -> ok

4. 파일 전반적인 설명 주석은 상단에, 그리고 주석이 필요하다면 자세하게 -> ok

5. rest 규격에 맞게 경로 설계하기 -> {id} 등으로 받기 -> ok

6. 변수명 camelCase 로 -> ok

7. 인증 토큰은 저장할 필요가 있는가?

8. 중복된 이메일 -> 데이터베이스에서도 막을 수 있도록 유니크 설정시켜주기 -> ok

9. addNew 와 같이 중복된 네이밍 X
12 changes: 12 additions & 0 deletions src/main/java/gift/ArgumentResolver/LoginMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package gift.ArgumentResolver;

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 LoginMember {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gift.ArgumentResolver;

import gift.service.MemberService;
import jakarta.servlet.http.HttpServletRequest;
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 LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final MemberService memberService;

public LoginMemberArgumentResolver(MemberService memberService) {
this.memberService = memberService;
}

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

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String token = request.getHeader("Authorization");
if (token == null) {
return null;
}
return memberService.getLoginUser(token);
}
}
55 changes: 30 additions & 25 deletions src/main/java/gift/controller/AdminPageController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import gift.dto.ProductDTO;
import gift.service.ProductService;
import jakarta.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -10,53 +12,56 @@
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.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/admin/products")
@Controller
public class AdminPageController {

private final ProductService pm;
private final ProductService productService;

public AdminPageController(ProductService pm) {
this.pm = pm;
public AdminPageController(ProductService productService) {
this.productService = productService;
}

@GetMapping("/admin")
@GetMapping
public String adminPage(Model model) {
model.addAttribute("products",pm.readAll());
model.addAttribute("productDTO",new ProductDTO());
return "admin/index";//렌더링하는 html 이름
model.addAttribute("products", productService.readAll());
model.addAttribute("productDTO", new ProductDTO());
return "admin/index";
}

@PostMapping("/admin") //admin으로 오는 post에 대해서 submit
public String adminPageSubmit(@ModelAttribute("productDTO") ProductDTO productDTO) {
pm.create(productDTO); //서비스에 접근해서 해당 부분을 추가해주도록 한다.
return "redirect:/admin";
@PostMapping
public String adminPageSubmit(@ModelAttribute("productDTO") @Valid ProductDTO productDTO) {
productService.create(productDTO);
return "redirect:/admin/products";
}

@PutMapping("/admin/{id}")
public String adminPageUpdate(@PathVariable Long id,@ModelAttribute("productDTO") ProductDTO productDTO) {
changeCheckAndUpdate(id,productDTO);
return "redirect:/admin";
@PutMapping("/{id}")
public String adminPageUpdate(@PathVariable Long id,
@ModelAttribute("productDTO") @Valid ProductDTO productDTO) {
changeCheckAndUpdate(id, productDTO);
return "redirect:/admin/products";
}

@DeleteMapping("/admin/{id}")
@DeleteMapping("/{id}")
public String adminPageDelete(@PathVariable Long id) {
pm.delete(id);
return "redirect:/admin";
productService.delete(id);
return "redirect:/admin/products";

}

private void changeCheckAndUpdate(Long id, ProductDTO dto) {

if (dto.getName().length()>0){
pm.updateName(id, dto.getName());

if (dto.getName().length() > 0) {
productService.updateName(id, dto.getName());
}
if (dto.getPrice()!=null){
pm.updatePrice(id, dto.getPrice());
if (dto.getPrice() != null) {
productService.updatePrice(id, dto.getPrice());
}
if (dto.getImageUrl().length()>0){
pm.updateImageUrl(id, dto.getImageUrl());
if (dto.getImageUrl().length() > 0) {
productService.updateImageUrl(id, dto.getImageUrl());
}
}
}
39 changes: 39 additions & 0 deletions src/main/java/gift/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gift.controller;

import gift.dto.LoginMemberToken;
import gift.dto.MemberDTO;
import gift.model.MemberRole;
import gift.service.MemberService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api/member")
@RestController
public class MemberController {

private final MemberService memberService;

public MemberController(MemberService memberService) {
this.memberService = memberService;
}

@PutMapping
public void register(@RequestBody @Valid MemberDTO memberDTO) {
if (memberDTO.getRole() == null) {
memberDTO.setRole(MemberRole.COMMON_MEMBER);
}
memberService.register(memberDTO);
}

@GetMapping("/login")
public LoginMemberToken login(@RequestParam("email") @NotBlank String email,
@RequestParam("password") @NotBlank String password) {
return memberService.login(new MemberDTO(email, password, null));
}
}
Loading