Skip to content

Commit

Permalink
Merge pull request #6 from Central-MakeUs/dev
Browse files Browse the repository at this point in the history
feat: 구매 인증 기능 구현
  • Loading branch information
KarmaPol authored Jan 24, 2024
2 parents 5b7cc32 + 991b01a commit 7e61f5c
Show file tree
Hide file tree
Showing 82 changed files with 1,644 additions and 323 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ build/
### yml
**/application-infra-rdb.yml
**/application-security.yml
**/application-s3.yml

### STS ###
.apt_generated
Expand Down
15 changes: 14 additions & 1 deletion api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ jar { enabled = false }

dependencies {
implementation project(':core:core-domain');
implementation project(':core:core-infra:core-infra-qdsl');
implementation project(':core:core-infra-rdb');
implementation project(':core:core-infra-qdsl');
implementation project(':core:core-infra-redis');
implementation project(':core:core-infra-s3');
implementation project(':core:core-security');

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand All @@ -21,6 +25,15 @@ dependencies {
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// security
implementation 'org.springframework.boot:spring-boot-starter-security'

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

// sentry
implementation 'io.sentry:sentry-spring-boot-starter-jakarta:7.1.0'
}
7 changes: 7 additions & 0 deletions api/http/test.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### 토큰 재발급 요청
POST http://localhost:8080/api/v1/auth/refresh-access-token
Content-Type: application/json

{
"refreshToken": "eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3MDYwMDc0MzYsImV4cCI6MTcwNjI2NjYzNn0.7Cnv74WOMVBEKR6K33-EtTyB84DNMuA_FpD1OnduO399LA_tVhCK34V80z1f2dOS9U-CM9D3OqPGLYOmUWKjFQ"
}
15 changes: 15 additions & 0 deletions api/src/main/java/com/mm/api/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mm.api.config;

import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("/**")
.allowedHeaders("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowCredentials(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.mm.api.domain.auth.controller;

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.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.mm.api.domain.auth.dto.request.RefreshTokenRequest;
import com.mm.api.domain.auth.dto.response.TokenResponse;
import com.mm.api.domain.auth.service.AuthService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@Tag(name = "회원 인증", description = "회원 인증 관련 API 입니다.")
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;

@Operation(summary = "oAuth 로그인을 합니다. 현재 provider는 kakao만 제공됩니다.")
@GetMapping("/oauth2/authorization/{oauth2-provider}")
public void login() {
// oauth2 로그인
}

@Operation(summary = "access token을 갱신합니다.")
@PostMapping("/api/v1/auth/refresh-access-token")
public ResponseEntity<TokenResponse> refreshAccessToken(@RequestBody RefreshTokenRequest request) {
TokenResponse tokenResponse = authService.refreshAccessToken(request);
return ResponseEntity.ok(tokenResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.mm.api.domain.auth.dto.request;

public record RefreshTokenRequest(String refreshToken) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.mm.api.domain.auth.dto.response;

public record TokenResponse(String accessToken, String refreshToken) {
}
57 changes: 57 additions & 0 deletions api/src/main/java/com/mm/api/domain/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.mm.api.domain.auth.service;

import java.util.List;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import com.mm.api.domain.auth.dto.request.RefreshTokenRequest;
import com.mm.api.domain.auth.dto.response.TokenResponse;
import com.mm.api.exception.CustomException;
import com.mm.api.exception.ErrorCode;
import com.mm.coredomain.domain.Member;
import com.mm.coredomain.repository.MemberRepository;
import com.mm.coreinfraredis.repository.RedisRefreshTokenRepository;
import com.mm.coresecurity.jwt.JwtTokenProvider;
import com.mm.coresecurity.oauth.OAuth2UserDetails;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService {
private final RedisRefreshTokenRepository redisRefreshTokenRepository;
private final JwtTokenProvider jwtTokenProvider;
private final MemberRepository memberRepository;

public TokenResponse refreshAccessToken(RefreshTokenRequest request) {
Long memberId = redisRefreshTokenRepository.findByRefreshToken(request.refreshToken())
.orElseThrow(() -> new CustomException(ErrorCode.REFRESH_TOKEN_EXPIRED));

Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));

OAuth2UserDetails oauth2UserDetails = createOauth2UserDetails(member);

String accessToken = jwtTokenProvider.generateAccessToken(oauth2UserDetails);
String refreshToken = jwtTokenProvider.generateRefreshToken();

redisRefreshTokenRepository.save(refreshToken, memberId);

return new TokenResponse(accessToken, refreshToken);
}

private OAuth2UserDetails createOauth2UserDetails(Member member) {
List<SimpleGrantedAuthority> authorities = member.getGroups()
.getGroupPermissions()
.stream()
.map(groupPermission -> new SimpleGrantedAuthority(groupPermission.getPermission().getName()))
.toList();

return OAuth2UserDetails.builder()
.id(member.getId())
.provider(member.getProvider())
.authorities(authorities)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.mm.api.domain.buy.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.mm.api.domain.buy.dto.response.BuyResponse;
import com.mm.api.domain.buy.service.BuyService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@Tag(name = "구매 인증", description = "구매 인증 관련 API 입니다.")
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class BuyController {
private final BuyService buyService;

// 관리자만
@Operation(summary = "구매 인증을 페이지 단위로 가져옵니다.")
@GetMapping("/buys")
public ResponseEntity<?> getBuys(@RequestParam(required = false, defaultValue = "1") Integer page) {
List<BuyResponse> responses = buyService.getBuys(page);
return ResponseEntity.ok(responses);
}

@Operation(summary = "구매 인증 상태를 변경합니다.")
@PatchMapping("/buys/{buyId}/refund-status")
public ResponseEntity<?> updateBuyRefundStatus(@PathVariable Long buyId,
@RequestParam String refundStatus) {
BuyResponse response = buyService.updateBuyRefundStatus(buyId, refundStatus);
return ResponseEntity.ok(response);
}

// 관리자 + 회원(자신만)
@Operation(summary = "구매 인증을 삭제합니다.")
@DeleteMapping("/buys/{buyId}")
public ResponseEntity<?> deleteBuy(@PathVariable Long buyId) {
buyService.deleteBuy(buyId);
return ResponseEntity.noContent().build();
}

// 회원만
@Operation(summary = "구매 인증을 작성합니다.")
@PostMapping("/buys/{memberId}/{itemId}")
public ResponseEntity<?> postBuy(@PathVariable Long memberId, @PathVariable Long itemId,
@RequestPart(value = "file", required = true) MultipartFile file) {
BuyResponse buyResponse = buyService.postBuy(memberId, itemId, file);
return ResponseEntity.ok(buyResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.mm.api.domain.buy.dto.response;

import java.time.LocalDateTime;

import com.mm.coredomain.domain.Buy;
import com.mm.coredomain.domain.RefundStatus;

import lombok.Builder;

@Builder
public record BuyResponse(Long id,
String redirectUrl,
LocalDateTime uploadTime,
Integer refund,
RefundStatus refundStatus,
String certImageUrl) {
public static BuyResponse of(Buy buy) {
return BuyResponse.builder()
.id(buy.getId())
.redirectUrl(buy.getRedirectUrl())
.uploadTime(buy.getUploadTime())
.refund(buy.getRefund())
.refundStatus(buy.getRefundStatus())
.certImageUrl(buy.getCertImageUrl())
.build();
}
}
91 changes: 91 additions & 0 deletions api/src/main/java/com/mm/api/domain/buy/service/BuyService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.mm.api.domain.buy.service;

import static com.mm.api.exception.ErrorCode.*;

import java.time.LocalDateTime;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.mm.api.domain.buy.dto.response.BuyResponse;
import com.mm.api.exception.CustomException;
import com.mm.api.exception.ErrorCode;
import com.mm.coredomain.domain.Buy;
import com.mm.coredomain.domain.Item;
import com.mm.coredomain.domain.Member;
import com.mm.coredomain.domain.RefundStatus;
import com.mm.coredomain.repository.BuyRepository;
import com.mm.coredomain.repository.ItemRepository;
import com.mm.coredomain.repository.MemberRepository;
import com.mm.coreinfraqdsl.repository.BuyCustomRepository;
import com.mm.coreinfras3.util.S3Service;

import lombok.RequiredArgsConstructor;

@Service
@Transactional
@RequiredArgsConstructor
public class BuyService {
private final BuyRepository buyRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
private final S3Service s3Service;
private final BuyCustomRepository buyCustomRepository;

public BuyResponse postBuy(Long memberId, Long itemId, MultipartFile file) {
Member member = getMember(memberId);
Item item = getItem(itemId);

String url = s3Service.uploadFileToS3(file, memberId, itemId);

Buy buy = Buy.builder()
.member(member)
.item(item)
.redirectUrl(item.getRedirectUrl())
.refund(item.getRefund())
.refundStatus(RefundStatus.IN_PROGRESS)
.uploadTime(LocalDateTime.now())
.certImageUrl(url)
.build();

return BuyResponse.of(buyRepository.save(buy));
}

public BuyResponse updateBuyRefundStatus(Long buyId, String refundStatus) {
RefundStatus convertedRefundStatus = RefundStatus.of(refundStatus);
Buy buy = getBuy(buyId);
buy.updateRefundStatus(convertedRefundStatus);

return BuyResponse.of(buy);
}

public void deleteBuy(Long buyId) {
Buy buy = getBuy(buyId);
buyRepository.delete(buy);
}

@Transactional(readOnly = true)
public List<BuyResponse> getBuys(Integer page) {
List<Buy> buys = buyCustomRepository.getBuysByPage(page);
return buys.stream()
.map(BuyResponse::of)
.toList();
}

private Buy getBuy(Long buyId) {
return buyRepository.findById(buyId)
.orElseThrow(() -> new CustomException(BUY_NOT_FOUND));
}

private Item getItem(Long id) {
return itemRepository.findById(id)
.orElseThrow(() -> new CustomException(ITEM_NOT_FOUND));
}

private Member getMember(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
}
}
Loading

0 comments on commit 7e61f5c

Please sign in to comment.