Skip to content

Commit

Permalink
충남대 BE_윤정훈 6주차 과제 (2단계 & 3단계) (#216)
Browse files Browse the repository at this point in the history
* add: 주문하기 코드를 옮겨온다.

옮겨온다.

* refactor: gitignore 변경

* remove: 동시성 테스트 진행으로 인해 발생하는 build 과부하 방지를 위해 동시성테스트 제거

동시성 테스트 제거

* add: 기존 동시성테스트 추가

동시성 테스트 추가

* add: ci-cd 파일 추가

ci-cd 파일 추가

* rename: workflows -> workflow

* rename: ci-cd to gradle

* Create gradle.yml

* remove: workflow remove

* add: CI with java

* refactor: 자동으로 application.yml 파일 생성하도록 추가

* add: make application.yml 설정파일 생성

* rename: application 설정파일 이름 재설정

* remove: 동시성 테스트 삭제

* change: setup jdk

* remove: deploy 코드 제거

* add: make application.yml 방식 추가

* refactor: application.yml 만드는 코드 수정

* add: deploy gradle 추가

* rename: secret deploy server로 ip 보호

* add: EC2 서버에 키를 알려줌

* add: 호스트키 신뢰하도록 설정

* rename: 호스트키 이름 변경

* add: HOST 추가

* add: 호스트키 검증 무시

* add: redirect-token-uri를 따로 관리해서 배포환경과 다른값 설정할 수 있도록 함

* refactor: 이름 명시

* refactor: 배포 코드 수정

* refactor: 절대 경로 설정

* add: 배포시 중간에 sleep 10 추가

* remove: 그냥 실행되는 부분 제거

* add: 기존 실행중인 8080포트 죽이기

* refactor: 절대경로로 설정

* refactor: application.yml이 아닌 application.properties에 추가작성함으로써 하나로 통일관리

* refactor: 설정값 변경

* add: 동시성테스트의 크기를 줄여 부하를 적게하여 업로드하기 쉽도록 변경

테스트코드 사이즈 변경

* refactor: test부하 일부 증가

test부하 일부 증가

* refactor: API 명세 팀원들과 통일

API 명세-URL을 팀원들과 통일

* test: 수정된 API 명세에 알맞게 controller URL 변경

* docs: Swagger 문서 작성을 위한 ApiResponse(Auth, Category) 작성 완료

AuthApi, CategoryApi ApiResponse 정의 완료

* docs: API 명세 통일에 따른 응답 반환

응답 반환하기

* remove: 중복되는 @hidden 삭제

* infra: 스프링 배포 환경시 nohup 스프링 실행부분을 세션과 분리하여 종료

* infra: 백그라운드에서 실행 및 exit 으로 자동으로 배포까지하도록 설정

* infra: 백그라운드에서 스프링이 정상적으로 돌아가도록 gradle.yml 수정

* add: 스프링실행시 세션을 정상적으로 종료할 수 있도록 한다

* remove: Member-Role을 삭제

* add: 예외 Response를 한번 더 감싸서 status와 함께 반환

예외 Response 를 status, message 함께 반환

* add: API 명세서 중 응답이 포함되지 않는 부분은 응답에서 제외한다.

Schema-hidden=true 설정을 통해 응답에서 제외했다.

* refactor: 팀원들간 API 명세 통일 작업 수행

API 명세 통일 작업 수행

* docs: README.md 진행한 부분 체크

진행한 부분 체크작업 수행

* add: ADD 요청시 created 에 URL 을 담고, body를 반환하도록 함

body를 반환하도록 함

* remove: 사용하지 않는 예외 코드 삭제

사용하지 않는 예외코드 삭제

* refactor: List로 반환한 정보를 Page로 반환하도록 변경

List to Page 변경

* refactor: Page -> PageResponse 라는 DTO를 만들어 반환하도록 변경

DTO로 응답 타입 변환 과정 거침

* docs: API 명세서 PageResponse 로 변경

API 명세서 변경

* refactor: API 수정사항 반영

API 수정 사항 반영

* add: CORS 설정, react포트넘버 3000에 대해서 localhost에서 접속시 확인할 수 있도록 열어줌

addCorsMapping 적용

* docs: 2단계 README.md 작성

* test: cors 테스트코드 작성

* test: CORS테스트만을 위한 email, pw 작성

* add: cors 허용 주소 추가(localhost:3000)

* add: 쿠키저장 허용

* rewrite: cors 설정시 localhost:8080 추가

* rewrite: cors 설정 재설정

* add: Member의 필드에 point 추가

Member 필드에 point 추가

* test: 포인트 관련 테스트 추가

포인트 관련 테스트 추가

* docs: swagger 포인트API 명세 추가

포인트 API 명세 추가

* docs: README.md 작성
  • Loading branch information
yunjunghun0116 authored Aug 2, 2024
1 parent b72cae4 commit 30eb525
Show file tree
Hide file tree
Showing 18 changed files with 325 additions and 12 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@
- [X] 반환하는 응답을 일정하게 상태코드를 통일시킨다.
- [X] 예외에 해당하는 응답을 Exception Handler 에서는 ExceptionResponse 으로 감싸서 반환하도록 한다.

#### 2단계

- [X] 배포 스크립트를 작성한다.
- [X] 클라이언트와 API 연동시 발생하는 CORS 문제에 대응한다.
- [X] 배포하여 클라이언트와 테스트를 진행한다.

#### 3단계

- [X] 포인트는 사용자별로 보유한다.
- [X] 포인트 차감 방법 : 주문 요청시 사용할 포인트도 함께 전달받아 차감한다.
- [X] 포인트를 충전할 수 있다.

### 나만의 HTTP RULE

| HTTP Method | 사용상황 | 반환(상태코드) |
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/gift/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import gift.controller.auth.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

Expand All @@ -22,4 +23,14 @@ public void addInterceptors(InterceptorRegistry registry) {
.excludePathPatterns("/api/members/login/**")
.excludePathPatterns("/api/members/register");
}

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000", "localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
37 changes: 37 additions & 0 deletions src/main/java/gift/controller/PointController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gift.controller;

import gift.controller.api.PointApi;
import gift.dto.point.PointRequest;
import gift.dto.point.PointResponse;
import gift.service.PointService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/points")
public class PointController implements PointApi {

private final PointService pointService;

public PointController(PointService pointService) {
this.pointService = pointService;
}

@PostMapping
public ResponseEntity<PointResponse> addPoint(@RequestAttribute("memberId") Long memberId, @Valid @RequestBody PointRequest pointRequest) {
var point = pointService.addPoint(memberId, pointRequest.point());
return ResponseEntity.ok(point);
}

@GetMapping
public ResponseEntity<PointResponse> getPoint(@RequestAttribute("memberId") Long memberId) {
var point = pointService.getPoint(memberId);
return ResponseEntity.ok(point);
}
}
1 change: 1 addition & 0 deletions src/main/java/gift/controller/api/GiftOrderApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public interface GiftOrderApi {
@Operation(summary = "회원의 새 주문을 생성한다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "주문 생성 성공", content = @Content(schema = @Schema(implementation = GiftOrderResponse.class))),
@ApiResponse(responseCode = "400", description = "주문 생성 실패(사유 : 사용할 수 있는 포인트보다 더 많은 포인트가 입력되었거나 주문 정보가 잘못되었습니다.)", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "401", description = "주문 생성 실패(사유 : 카카오 토큰이 만료되었거나, 허용되지 않은 요청입니다.)", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "500", description = "내부 서버의 오류", content = @Content(schema = @Schema(hidden = true)))
})
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/gift/controller/api/PointApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package gift.controller.api;

import gift.dto.point.PointRequest;
import gift.dto.point.PointResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

@Tag(name = "포인트 API")
public interface PointApi {

@Operation(summary = "회원의 포인트를 추가한다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "포인트 추가 성공", content = @Content(schema = @Schema(implementation = PointResponse.class))),
@ApiResponse(responseCode = "500", description = "내부 서버의 오류", content = @Content(schema = @Schema(hidden = true)))
})
ResponseEntity<PointResponse> addPoint(Long memberId, PointRequest pointRequest);

@Operation(summary = "회원의 사용 가능한 포인트를 조회한다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "포인트 조회 성공", content = @Content(schema = @Schema(implementation = PointResponse.class))),
@ApiResponse(responseCode = "500", description = "내부 서버의 오류", content = @Content(schema = @Schema(hidden = true)))
})
ResponseEntity<PointResponse> getPoint(Long memberId);
}
5 changes: 4 additions & 1 deletion src/main/java/gift/dto/giftorder/GiftOrderRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;

public record GiftOrderRequest(
@NotNull(message = "상품 옵션은 반드시 선택되어야 합니다.")
Expand All @@ -12,6 +13,8 @@ public record GiftOrderRequest(
@Max(value = 100_000_000, message = "수량은 최소 1개 이상, 1억개 미만입니다.")
Integer quantity,
@NotBlank(message = "메시지의 길이는 최소 1자 이상이어야 합니다.")
String message
String message,
@PositiveOrZero(message = "포인트는 0보다 크거나 같아야 합니다.")
Integer point
) {
}
9 changes: 9 additions & 0 deletions src/main/java/gift/dto/point/PointRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gift.dto.point;

import jakarta.validation.constraints.Positive;

public record PointRequest(
@Positive(message = "포인트는 최소 1원 이상이어야 추가할 수 있습니다.")
Integer point
) {
}
9 changes: 9 additions & 0 deletions src/main/java/gift/dto/point/PointResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gift.dto.point;

public record PointResponse(
Integer point
) {
public static PointResponse of(Integer point) {
return new PointResponse(point);
}
}
7 changes: 7 additions & 0 deletions src/main/java/gift/exception/GiftOrderException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gift.exception;

public class GiftOrderException extends RuntimeException {
public GiftOrderException(String message) {
super(message);
}
}
5 changes: 5 additions & 0 deletions src/main/java/gift/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public ResponseEntity<ExceptionResponse> invalidLoginInfoExceptionHandling() {
return getExceptionResponse(INVALID_LOGIN_INFO_MESSAGE, HttpStatus.UNAUTHORIZED);
}

@ExceptionHandler(value = GiftOrderException.class)
public ResponseEntity<ExceptionResponse> giftOrderExceptionHandling(GiftOrderException exception) {
return getExceptionResponse(exception.getMessage(), HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(value = UnauthorizedAccessException.class)
public ResponseEntity<ExceptionResponse> unauthorizedAccessExceptionHandling(UnauthorizedAccessException exception) {
return getExceptionResponse(exception.getMessage(), HttpStatus.UNAUTHORIZED);
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/gift/model/Member.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gift.model;

import gift.exception.GiftOrderException;
import gift.exception.InvalidLoginInfoException;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -20,6 +21,9 @@ public class Member extends BaseEntity {
@Column(name = "password")
private String password;
@NotNull
@Column(name = "point")
private Integer point = 0;
@NotNull
@Column(name = "deleted")
private Boolean deleted = Boolean.FALSE;

Expand All @@ -40,9 +44,24 @@ public String getPassword() {
return password;
}

public Integer getPoint() {
return point;
}

public void passwordCheck(String inputPassword) {
if (!password.equals(inputPassword)) {
throw new InvalidLoginInfoException("로그인 정보가 유효하지 않습니다.");
}
}

public void addPoint(Integer newPoint) {
this.point = point + newPoint;
}

public void subtractPoint(Integer usedPoint) {
if (point < usedPoint) {
throw new GiftOrderException("사용가능한 포인트보다 더 많은 포인트를 사용할 수 없습니다.");
}
this.point = point - usedPoint;
}
}
5 changes: 4 additions & 1 deletion src/main/java/gift/service/GiftOrderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ public class GiftOrderService {
private final GiftOrderRepository giftOrderRepository;
private final MemberRepository memberRepository;
private final WishProductService wishProductService;
private final PointService pointService;

public GiftOrderService(GiftOrderRepository giftOrderRepository, MemberRepository memberRepository, WishProductService wishProductService) {
public GiftOrderService(GiftOrderRepository giftOrderRepository, MemberRepository memberRepository, WishProductService wishProductService, PointService pointService) {
this.giftOrderRepository = giftOrderRepository;
this.memberRepository = memberRepository;
this.wishProductService = wishProductService;
this.pointService = pointService;
}

public GiftOrderResponse addGiftOrder(Long memberId, Option option, GiftOrderRequest giftOrderRequest) {
pointService.subtractPoint(memberId, giftOrderRequest.point());
var order = saveGiftOrderWithGiftOrderRequest(memberId, option, giftOrderRequest);
wishProductService.deleteAllByMemberIdAndProductId(memberId, option.getProduct().getId());
return getGiftOrderResponseFromGiftOrder(order);
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/gift/service/PointService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gift.service;

import gift.dto.point.PointResponse;
import gift.exception.NotFoundElementException;
import gift.repository.MemberRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class PointService {

private final MemberRepository memberRepository;

public PointService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

public PointResponse addPoint(Long memberId, Integer point) {
var member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundElementException(memberId + "를 가진 이용자가 존재하지 않습니다."));
member.addPoint(point);
memberRepository.save(member);
return PointResponse.of(member.getPoint());
}

public void subtractPoint(Long memberId, Integer point) {
var member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundElementException(memberId + "를 가진 이용자가 존재하지 않습니다."));
member.subtractPoint(point);
memberRepository.save(member);
}

public PointResponse getPoint(Long memberId) {
var member = memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundElementException(memberId + "를 가진 이용자가 존재하지 않습니다."));
return PointResponse.of(member.getPoint());
}
}
22 changes: 14 additions & 8 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
insert into member(email, password, deleted)
values ('[email protected]', 'password', 0);
insert into member(email, password, deleted)
values ('[email protected]', 'password', 0);
insert into member(email, password, point, deleted)
values ('[email protected]', 'password', 0, 0);
insert into member(email, password, point, deleted)
values ('[email protected]', 'password', 0, 0);

insert into category(name, description, color, image_url, deleted)
values ('디지털/가전', '가전설명', '#888888', '가전이미지', 0);
values ('디지털/가전', '가전설명', '#888888',
'https://prs.ohou.se/apne2/any/uploads/productions/v1-262152097570816.jpg?w=256&h=256&c=c&q=50', 0);
insert into category(name, description, color, image_url, deleted)
values ('상품권', '상품권설명', '#123456', '상품권이미지', 0);
values ('상품권', '상품권설명', '#123456', 'https://sitem.ssgcdn.com/01/88/00/item/1000010008801_i1_750.jpg', 0);
insert into category(name, description, color, image_url, deleted)
values ('뷰티', '뷰티설명', '#777777', '뷰티이미지', 0);
values ('뷰티', '뷰티설명', '#777777',
'https://i0.wp.com/blog.opensurvey.co.kr/wp-content/uploads/2020/01/2020_beauty_blog.jpg?resize=700%2C350&ssl=1',
0);
insert into category(name, description, color, image_url, deleted)
values ('식품', '식품설명', '#222222', '식품이미지', 0);
values ('식품', '식품설명', '#222222',
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS97vAEobij-ygYW3Zk4c_N5GJz9CxBb7fLNA&s', 0);

insert into product(name, price, image_url, category_id, deleted)
values ('Apple 정품 아이폰 15', 1700000,
Expand All @@ -27,3 +31,5 @@ values ('50000원 상품권', 50000,

insert into option(product_id, name, quantity, deleted)
values (3, '상품옵션', 10000, 0);
insert into option(product_id, name, quantity, deleted)
values (2, '상품옵션2', 100000, 0);
52 changes: 52 additions & 0 deletions src/test/java/gift/controller/CorsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package gift.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import gift.dto.auth.RegisterRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class CorsTest {

@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;

@Test
void successPostRequestWithCors() throws Exception {
var postRequest = post("/api/members/register")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.ORIGIN, "http://localhost:3000")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.content(objectMapper.writeValueAsString(new RegisterRequest("[email protected]", "corsTestPassword")));
//when
var result = mockMvc.perform(postRequest);
//then
result.andExpect(status().isOk());
}

@Test
void failPostRequestWithCors() throws Exception {
var postRequest = post("/api/members/register")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.ORIGIN, "http://localhost:3001")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.content(objectMapper.writeValueAsString(new RegisterRequest("[email protected]", "corsTestPassword")));
//when
var result = mockMvc.perform(postRequest);
//then
result.andExpect(status().isForbidden());
}
}
Loading

0 comments on commit 30eb525

Please sign in to comment.