diff --git a/.gitignore b/.gitignore index c2065bc..0ece36a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ HELP.md .gradle +.yml build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f0ee35 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# 0. 기초 세팅 +### 0.1. 폴더 구조 생성 + - entity 폴더 + - repository 폴더 + - service 폴더 + - controller 폴더 +### 0.2. h2 db 연결 +# 1. 기프티콘 구현 +- [x] 엔티티 구현 +- [x] 레포지토리 구현 +- [x] 서비스 구현 +- [x] 컨트롤러 구현 +- [ ] 테스트 코드 작성 +- [x] API 문서 작성 +- [ ] API 테스트 +- [x] 예외 처리 +- [ ] 예외 처리 테스트 +# 2. 회원가입 및 로그인 구현 +- [x] 클라이언트- 인가코드 받기 +- [x] 클라이언트- 인가 코드를 서버에 넘기기 +- [x] 서버- 카카오 엑세스 토큰 받기 +- [x] 서버- 카카오 리소스 서버에서 유저 정보 받기 +- [x] 서버- 유저 정보를 DB에 저장하기 +- [x] 서버- 로그인 구현 +- [x] 서버- 회원가입 구현 +- [ ] 테스트 코드 작성 +- [ ] API 문서 작성 +- [ ] API 테스트 diff --git a/build.gradle b/build.gradle index 041fe6e..8adba0b 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,13 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //security + implementation 'org.springframework.boot:spring-boot-starter-security' + + //test + testImplementation 'org.springframework.security:spring-security-test' + } tasks.named('test') { diff --git a/src/main/java/team/haedal/gifticionfunding/auth/api/OAuthController.java b/src/main/java/team/haedal/gifticionfunding/auth/api/OAuthController.java new file mode 100644 index 0000000..de3c6af --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/auth/api/OAuthController.java @@ -0,0 +1,46 @@ +package team.haedal.gifticionfunding.auth.api; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import team.haedal.gifticionfunding.auth.dto.TokenDto; +import team.haedal.gifticionfunding.auth.service.OAuthService; +import team.haedal.gifticionfunding.auth.service.SecurityService; +import team.haedal.gifticionfunding.dto.user.request.UserCreate; +import team.haedal.gifticionfunding.entity.user.User; +import team.haedal.gifticionfunding.service.user.UserService; + +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class OAuthController { + private final OAuthService oAuthService; + private final SecurityService securityService; + private final UserService userService; + + @GetMapping("/oauth2") + public ResponseEntity socialLogin(@RequestParam("code") String code){ + UserCreate userInfo=oAuthService.getUserInfo(code); + log.info("userInfo: {}",userInfo); + //null일때 exception 처리 + Optional user= Optional.ofNullable(userService.SignIn(userInfo).orElseThrow(() -> + new IllegalArgumentException("로그인 실패"))); + log.info("user: {}",user.get().getId()); + TokenDto tokenDto=securityService.generateTokenDto(user.get().getId()); + HttpHeaders headers=securityService.setTokenHeaders(tokenDto); + log.info("로그인 성공"); + + return ResponseEntity + .status(HttpStatus.OK) + .headers(headers) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/auth/config/JwtAuthorizationFilter.java b/src/main/java/team/haedal/gifticionfunding/auth/config/JwtAuthorizationFilter.java new file mode 100644 index 0000000..e5b8e38 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/auth/config/JwtAuthorizationFilter.java @@ -0,0 +1,60 @@ +package team.haedal.gifticionfunding.auth.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import team.haedal.gifticionfunding.auth.jwt.JwtProvider; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthorizationFilter extends OncePerRequestFilter { + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + log.info("dofilterinternal 실행"); + + if (request.getRequestURI().startsWith("/oauth2") || request.getRequestURI().startsWith("/refresh") ||request.getRequestURI().startsWith("/swagger-ui")||request.getRequestURI().startsWith("/api-docs")||request.getRequestURI().startsWith("/v3")) { + log.info("다음필터 실행"); + + filterChain.doFilter(request, response); + return; + } + + //authorization header에서 access token을 추출 + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + String accessToken = jwtProvider.parseAccessToken(authHeader); + + if (StringUtils.hasText(accessToken) && jwtProvider.validateToken(accessToken)) { + // 일단 USER 권한만 부여 + List authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + Authentication authentication = new UsernamePasswordAuthenticationToken(jwtProvider.getUserIdFromToken(accessToken), null, authorities); + SecurityContextHolder.getContext().setAuthentication(authentication); + } else { + throw new IllegalArgumentException("예상치 못한 토큰 오류"); + } + + log.info("다음필터 실행"); + // 다음 Filter를 실행하기 위한 코드. 마지막 필터라면 필터 실행 후 리소스를 반환한다. + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/auth/config/SecurityConfig.java b/src/main/java/team/haedal/gifticionfunding/auth/config/SecurityConfig.java new file mode 100644 index 0000000..e1f8991 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/auth/config/SecurityConfig.java @@ -0,0 +1,74 @@ +package team.haedal.gifticionfunding.auth.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import team.haedal.gifticionfunding.global.error.auth.CustomAccessDeniedHandler; +import team.haedal.gifticionfunding.global.error.auth.CustomAuthenticationEntryPoint; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@Slf4j +public class SecurityConfig { + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final JwtAuthorizationFilter jwtAuthorizationFilter; + + private static final String[] WHITE_LIST = { + "/auth2", + "/api-docs/**", + "/login/**", + }; + private static final String[] AUTHENTICATION_LIST = { + "/api/gifticon" + }; + + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + log.info("필터체인 실행"); + http + .csrf(AbstractHttpConfigurer::disable) + .cors(c -> c.configurationSource(corsConfigSource())) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authReq -> authReq + .requestMatchers(HttpMethod.OPTIONS).permitAll() + .requestMatchers("/", "/login", "/oauth2/**", "/refresh","/swagger-ui/**","/api-docs","/v3/api-docs/**").permitAll() + .anyRequest().authenticated()) + .exceptionHandling(e -> e + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler)) + .addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + @Bean + public CorsConfigurationSource corsConfigSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(Arrays.asList("*")); // 모든 요청 허용 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); // 모든 HTTP 메서드 허용 + configuration.setAllowedHeaders(Arrays.asList("*")); // 모든 헤더 허용 + configuration.setExposedHeaders(Arrays.asList("Authorization", "Set-cookie")); + configuration.setAllowCredentials(true); // 쿠키와 같은 자격 증명을 허용 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/auth/dto/TokenDto.java b/src/main/java/team/haedal/gifticionfunding/auth/dto/TokenDto.java new file mode 100644 index 0000000..f119f71 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/auth/dto/TokenDto.java @@ -0,0 +1,15 @@ +package team.haedal.gifticionfunding.auth.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class TokenDto { + private final String grantType; + private final String accessToken; + private final String refreshToken; + private final Long accessTokenExpiresIn; +} diff --git a/src/main/java/team/haedal/gifticionfunding/auth/jwt/JwtProvider.java b/src/main/java/team/haedal/gifticionfunding/auth/jwt/JwtProvider.java new file mode 100644 index 0000000..be348ed --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/auth/jwt/JwtProvider.java @@ -0,0 +1,111 @@ +package team.haedal.gifticionfunding.auth.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.util.Date; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import team.haedal.gifticionfunding.auth.dto.TokenDto; +import team.haedal.gifticionfunding.global.error.auth.InvalidTokenException; + +@Component +public class JwtProvider { + + private final Key key; + private static final String GRANT_TYPE = "Bearer"; + private static final long ACCESS_TOKEN_EXPIRES_IN = 1000L * 60 * 30; //access 30분 + private static final long REFRESH_TOKEN_EXPIRES_IN = 1000L * 60 * 60 * 24 * 14; //refresh 14일 + + public JwtProvider(@Value("${jwt.secret}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public TokenDto generateTokenDto(Long userId) { + + long now = (new Date()).getTime(); + + String accessToken = generateAccessToken(now, userId); + String refreshToken = generateRefreshToken(now); + + return TokenDto.builder() + .grantType(GRANT_TYPE) + .accessToken(accessToken) + .accessTokenExpiresIn(new Date(now + ACCESS_TOKEN_EXPIRES_IN).getTime()) + .refreshToken(refreshToken) + .build(); + } + + public String generateAccessToken(long now, Long userId) { + String accessToken = Jwts.builder() + .setSubject(String.valueOf(userId)) + .setExpiration(new Date(now + ACCESS_TOKEN_EXPIRES_IN)) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + + return accessToken; + } + + public String generateRefreshToken(long now) { + String refreshToken = Jwts.builder() + .setExpiration(new Date(now + REFRESH_TOKEN_EXPIRES_IN)) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + + return refreshToken; + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (SecurityException | MalformedJwtException | ExpiredJwtException | + UnsupportedJwtException | + IllegalArgumentException e) { + throw e; + } + } + + public String parseAccessToken(String authHeader) { + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring("Bearer ".length()); + return token; + } else { + throw new InvalidTokenException("AccessToken이 없거나 Bearer type이 아닙니다."); + } + } + + public long getUserIdFromToken(String accessToken) { + String userId = parseClaims(accessToken).getSubject(); + + return Long.parseLong(userId); + } + + protected Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (ExpiredJwtException e) { + throw e; + } + } + + + public String generateAccessTokenForTest() { + return Jwts.builder() + .setSubject("1") + .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRES_IN)) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/auth/service/OAuthService.java b/src/main/java/team/haedal/gifticionfunding/auth/service/OAuthService.java new file mode 100644 index 0000000..118f88c --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/auth/service/OAuthService.java @@ -0,0 +1,73 @@ +package team.haedal.gifticionfunding.auth.service; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.PropertySource; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import team.haedal.gifticionfunding.dto.user.request.UserCreate; + +@Service +@Slf4j +@RequiredArgsConstructor +@PropertySource("classpath:application.yml") +public class OAuthService { + @Value("${kakao.client-id}") + private String CLIENT_ID; + @Value("${kakao.redirect-uri}") + private String REDIRECTION_URI; + @Value("${kakao.token-uri}") + private String TOKEN_URI; + + @Value("${kakao.user-info-uri}") + private String USER_INFO_URI; + + private final RestTemplate restTemplate = new RestTemplate(); + + + public UserCreate getUserInfo(String code) { + //리소스 서버의 access token 받아오기 + String accessToken = getAccessToken(code); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + HttpEntity entity = new HttpEntity(headers); + + //restTemplate.exchange(USER_INFO_URI,HttpMethod.GET, entity, JsonNode.class).getBody(); + ResponseEntity responseNode = restTemplate.exchange(USER_INFO_URI, HttpMethod.GET, entity, JsonNode.class); + JsonNode userInfoNode = responseNode.getBody(); + log.info("userInfoNode: {}", userInfoNode); + return UserCreate.from(userInfoNode); + } + + public String getAccessToken(String code) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", CLIENT_ID); + params.add("redirect_uri", REDIRECTION_URI); + params.add("code", code); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity entity=new HttpEntity(params, headers); + + ResponseEntity responseNode=restTemplate.exchange(TOKEN_URI, HttpMethod.POST,entity,JsonNode.class); + //responseNode의 exception 처리 + if (responseNode.getStatusCode() != HttpStatus.OK) { + throw new IllegalArgumentException("카카오 인증 정보가 올바르지 않습니다."); + } + log.info("responseNode: {}",responseNode); + JsonNode accessTokenNode=responseNode.getBody(); + log.info("accessTokenNode: {}",accessTokenNode); + return accessTokenNode.get("access_token").asText(); + } + + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/auth/service/SecurityService.java b/src/main/java/team/haedal/gifticionfunding/auth/service/SecurityService.java new file mode 100644 index 0000000..3ef0306 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/auth/service/SecurityService.java @@ -0,0 +1,42 @@ +package team.haedal.gifticionfunding.auth.service; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Service; +import team.haedal.gifticionfunding.auth.dto.TokenDto; +import team.haedal.gifticionfunding.auth.jwt.JwtProvider; + +@Service +@Slf4j +@RequiredArgsConstructor +public class SecurityService { + private final JwtProvider jwtProvider; + private final String prefix = "Bearer "; + + public TokenDto generateTokenDto(Long userId) { + + TokenDto tokenDto = jwtProvider.generateTokenDto(userId); + + return tokenDto; + } + + public HttpHeaders setTokenHeaders(TokenDto tokenDto) { + HttpHeaders headers = new HttpHeaders(); + ResponseCookie cookie = ResponseCookie.from("RefreshToken", tokenDto.getRefreshToken()) + .path("/") + .maxAge(60*60*24*7) // 쿠키 유효기간: 7일 + .secure(false) + .sameSite("Lax") + .httpOnly(true) + .build(); + headers.add("Set-cookie", cookie.toString()); + headers.add("Authorization", prefix + tokenDto.getAccessToken()); + + return headers; + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/controller/funding/FundingController.java b/src/main/java/team/haedal/gifticionfunding/controller/funding/FundingController.java new file mode 100644 index 0000000..a84d141 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/controller/funding/FundingController.java @@ -0,0 +1,75 @@ +package team.haedal.gifticionfunding.controller.funding; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.security.Principal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import team.haedal.gifticionfunding.dto.common.PagingRequest; +import team.haedal.gifticionfunding.dto.common.PagingResponse; +import team.haedal.gifticionfunding.dto.funding.request.FundingCreateRequest; +import team.haedal.gifticionfunding.dto.funding.response.FundingArticleResponse; +import team.haedal.gifticionfunding.service.funding.FundingService; + +@Tag(name = "펀딩", description = "펀딩 API") +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/funding") +public class FundingController { + + private final FundingService fundingService; + + @Operation(summary = "펀딩 게시글 생성", description = "펀딩 게시글 생성") + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Long createFunding(@Valid @RequestBody FundingCreateRequest fundingCreateRequest, + Principal principal) { + log.info("펀딩 게시글 생성"); + return fundingService.createFunding(fundingCreateRequest.toDomain(), + fundingCreateRequest.gifticonIds(), + Long.parseLong(principal.getName())); + } + + @Operation(summary = "펀딩 게시글 페이징 조회", description = "펀딩 게시글 페이징 조회") + @GetMapping + @ResponseStatus(HttpStatus.OK) + public PagingResponse getFundings(@Valid PagingRequest pagingRequest + ) { + log.info("펀딩 게시글 페이징 조회"); + return fundingService.getFundingArticlePaging(pagingRequest); + } + + @Operation(summary = "펀딩 게시글 상세 조회", description = "펀딩 게시글 상세 조회") + @GetMapping("/{fundingId}") + @ResponseStatus(HttpStatus.OK) + public void getFunding() { + log.info("펀딩 게시글 상세 조회"); + } + + @Operation(summary = "펀딩 게시글 수정", description = "펀딩 게시글 수정") + @PutMapping + @ResponseStatus(HttpStatus.OK) + public void updateFunding() { + log.info("펀딩 게시글 수정"); + } + + @Operation(summary = "펀딩 게시글 삭제", description = "펀딩 게시글 삭제") + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteFunding() { + log.info("펀딩 게시글 삭제"); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/controller/gifticon/GifticonController.java b/src/main/java/team/haedal/gifticionfunding/controller/gifticon/GifticonController.java new file mode 100644 index 0000000..e71d9b6 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/controller/gifticon/GifticonController.java @@ -0,0 +1,106 @@ +package team.haedal.gifticionfunding.controller.gifticon; + +import io.swagger.v3.oas.annotations.Operation; +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 jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import team.haedal.gifticionfunding.dto.gifticon.request.GifticonCreateRequest; +import team.haedal.gifticionfunding.dto.gifticon.request.GifticonUpdateRequest; +import team.haedal.gifticionfunding.dto.gifticon.response.GifticonDetailResponse; +import team.haedal.gifticionfunding.dto.gifticon.response.GifticonResponse; +import team.haedal.gifticionfunding.entity.gifticon.Gifticon; +import team.haedal.gifticionfunding.entity.gifticon.GifticonCreate; +import team.haedal.gifticionfunding.service.gifticon.GifticonService; + +import java.net.URI; +import java.util.List; + +@Tag(name = "기프티콘", description = "기프티콘 API") +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/gifticons") +public class GifticonController { + private final GifticonService gifticonService; + + @Operation(summary = "기프티콘 전체 조회", description = "기프티콘 정보를 모두 조회합니다") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "기프티콘 전체 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> getGifticons() { + log.info("기프티콘 전체조회"); + List gifticons = gifticonService.findGifticonAll(); + return ResponseEntity.ok(GifticonResponse.fromList(gifticons)); + } + + @Operation(summary = "기프티콘 조회", description = "기프티콘 정보를 조회합니다") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "기프티콘 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "404", description = "기프티콘을 찾을 수 없음"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @GetMapping("/{gifticonId}") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity getGifticon(@PathVariable("gifticonId") Long gifticonId) { + log.info("gifticonId : {} 기프티콘조회", gifticonId); + Gifticon gifticon = gifticonService.findGifticon(gifticonId); + return ResponseEntity.ok(GifticonDetailResponse.from(gifticon)); + } + + @Operation(summary = "기프티콘 생성", description = "기프티콘을 생성합니다") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "기프티콘 생성 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createGifticon(@RequestBody @Valid GifticonCreateRequest request) { + log.info("기프티콘 생성 name: {},price:{}",request.getName(), request.getPrice()); + final Long id = gifticonService.registerGifticon(request.toCommand()); + return ResponseEntity.created(URI.create("/api/gifticons/"+id)).build(); + } + + @Operation(summary = "기프티콘 수정", description = "기프티콘을 수정합니다") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "기프티콘 수정 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "404", description = "기프티콘을 찾을 수 없음"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @PutMapping("/{gifticonId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity putGifticon(@PathVariable("gifticonId") Long gifticonId, @RequestBody GifticonUpdateRequest request) { + log.info("gifticonId : {} 기프티콘 수정", gifticonId); + Gifticon gifticon=gifticonService.updateGifticon(gifticonId, request.toCommand()); + return ResponseEntity.ok(GifticonDetailResponse.from(gifticon)); + } + + @Operation(summary = "기프티콘 삭제", description = "기프티콘을 삭제합니다") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "기프티콘 삭제 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "404", description = "기프티콘을 찾을 수 없음"), + @ApiResponse(responseCode = "500", description = "서버 오류") + }) + @DeleteMapping("/{gifticonId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity deleteGifticon(@PathVariable("gifticonId") Long gifticonId) { + log.info("gifticonId : {} 기프티콘 삭제", gifticonId); + gifticonService.deleteGifticon(gifticonId); + return ResponseEntity.noContent().build(); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/controller/user/FriendController.java b/src/main/java/team/haedal/gifticionfunding/controller/user/FriendController.java new file mode 100644 index 0000000..985e2d2 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/controller/user/FriendController.java @@ -0,0 +1,103 @@ +package team.haedal.gifticionfunding.controller.user; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.security.Principal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import team.haedal.gifticionfunding.dto.common.PagingRequest; +import team.haedal.gifticionfunding.dto.common.PagingResponse; +import team.haedal.gifticionfunding.dto.user.request.FriendRequestCreateRequest; +import team.haedal.gifticionfunding.dto.user.response.FriendInfoDto; +import team.haedal.gifticionfunding.dto.user.response.FriendRequestReceiverDto; +import team.haedal.gifticionfunding.dto.user.response.FriendRequestSenderDto; +import team.haedal.gifticionfunding.service.user.FriendService; + +@Tag(name = "친구", description = "친구 API") +@Slf4j +@RequiredArgsConstructor +@RestController +public class FriendController { + + private final FriendService friendService; + + @Operation(summary = "친구 추가 요청", description = "친구 추가 요청 id 반환") + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("api/user/friend") + public void requestFriend(@RequestBody FriendRequestCreateRequest friendRequestCreateRequest) { + log.info("친구 추가 요청"); + friendService.requestFriend(friendRequestCreateRequest.getRequesterId(), + friendRequestCreateRequest.getReceiverId()); + } + + @Operation(summary = "친구 목록 조회", description = "목록 페이징 조회") + @GetMapping("api/user/friend") + public PagingResponse getFriendList(@Valid PagingRequest pagingRequest, + Principal principal) { + log.info("친구 목록 조회"); + return friendService.getFriendPaging(pagingRequest, Long.parseLong(principal.getName())); + } + + @Operation(summary = "친구 추가 요청 보낸 목록 조회", description = "친구 추가 요청 목록 페이징 조회") + @GetMapping("api/user/friend/request") + public PagingResponse getFriendRequestList( + @Valid PagingRequest pagingRequest, Principal principal) { + log.info("친구 추가 요청 보낸 목록 조회"); + return friendService.getFriendRequestSenderPaging(pagingRequest, + Long.parseLong(principal.getName())); + + } + + @Operation(summary = "친구 추가 요청 받은 목록 조회", description = "친구 추가 요청 받은 목록 페이징 조회") + @GetMapping("api/user/friend/receive") + public PagingResponse getFriendRequestReceiveList( + @Valid PagingRequest pagingRequest, Principal principal) { + log.info("친구 추가 요청 받은 목록 조회"); + return friendService.getFriendRequestReceiverPaging(pagingRequest, + Long.parseLong(principal.getName())); + } + + @Operation(summary = "친구 추가 요청 수락", description = "친구 추가 요청 수락") + @ResponseStatus(HttpStatus.NO_CONTENT) + @PostMapping("api/user/friend/accept/{friendRequestId}") + public void acceptFriend(Principal principal, @PathVariable Long friendRequestId) { + log.info("친구 추가 요청 수락"); + friendService.acceptFriendRequest(friendRequestId); + } + + + @Operation(summary = "친구 추가 요청 거절", description = "친구 추가 요청 거절") + @ResponseStatus(HttpStatus.NO_CONTENT) + @PostMapping("api/user/friend/reject/{friendRequestId}") + public void rejectFriend(Principal principal, @PathVariable Long friendRequestId) { + log.info("친구 추가 요청 거절"); + friendService.rejectFriendRequest(friendRequestId); + } + + @Operation(summary = "친구 삭제", description = "친구 삭제") + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("api/user/friend/{friendId}") + public void deleteFriend(@PathVariable Long friendId, Principal principal) { + friendService.deleteFriend(Long.parseLong(principal.getName()), friendId); + } + + @Operation(summary = "친구의 친구 목록 조회", description = "친구의 친구 목록 조회") + @GetMapping("api/user/friend/related") + public PagingResponse getRelatedFriendList(@Valid PagingRequest pagingRequest, + Principal principal) { + log.info("친구의 친구 목록 조회"); + return friendService.getRelatedFriendPaging(pagingRequest, + Long.parseLong(principal.getName())); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/common/PagingRequest.java b/src/main/java/team/haedal/gifticionfunding/dto/common/PagingRequest.java new file mode 100644 index 0000000..49b1628 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/common/PagingRequest.java @@ -0,0 +1,24 @@ +package team.haedal.gifticionfunding.dto.common; + +import jakarta.validation.constraints.Min; +import lombok.Builder; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@Builder +public record PagingRequest(@Min(0) int page, Integer size) { + + public PagingRequest { + if (size == null) { + size = 20; + } + } + + public Pageable toPageable() { + return Pageable.ofSize(size).withPage(page); + } + + public PageRequest toPageRequest() { + return PageRequest.of(page, size); + } +} \ No newline at end of file diff --git a/src/main/java/team/haedal/gifticionfunding/dto/common/PagingResponse.java b/src/main/java/team/haedal/gifticionfunding/dto/common/PagingResponse.java new file mode 100644 index 0000000..7aa270c --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/common/PagingResponse.java @@ -0,0 +1,22 @@ +package team.haedal.gifticionfunding.dto.common; + +import java.util.List; +import java.util.function.Function; +import lombok.Builder; +import org.springframework.data.domain.Page; + +@Builder +public record PagingResponse(boolean hasNext, List data) { + /** + * [Entity]를 [Model]로 변환하여 [PagingResponse]를 생성합니다.
+ * 해당 메서드를 [Entity]와 [Model]이 1:1 매핑되는 경우에 사용하는 것을 권장합니다.
+ * [Template Method Pattern]을 사용하여 변환 로직을 외부에서 주입받습니다. + */ + public static PagingResponse from(Page page, Function converter) { + return PagingResponse.builder() + .hasNext(page.hasNext()) + .data(page.getContent().stream().map(converter).toList()) + .build(); + + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/funding/domain/FundingArticleCreate.java b/src/main/java/team/haedal/gifticionfunding/dto/funding/domain/FundingArticleCreate.java new file mode 100644 index 0000000..63d95c8 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/funding/domain/FundingArticleCreate.java @@ -0,0 +1,15 @@ +package team.haedal.gifticionfunding.dto.funding.domain; + +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FundingArticleCreate { + + private final String title; + private final String content; + private final int duration; + private final LocalDateTime startAt; +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/funding/request/FundingCreateRequest.java b/src/main/java/team/haedal/gifticionfunding/dto/funding/request/FundingCreateRequest.java new file mode 100644 index 0000000..ae40720 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/funding/request/FundingCreateRequest.java @@ -0,0 +1,39 @@ +package team.haedal.gifticionfunding.dto.funding.request; + + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; +import team.haedal.gifticionfunding.dto.funding.domain.FundingArticleCreate; + + +@Builder +public record FundingCreateRequest( + @NotBlank(message = "제목은 필수 입력 값입니다.") + String title, + @NotBlank(message = "내용은 필수 입력 값입니다.") + String content, + @NotNull(message = "펀딩 기간은 필수 입력 값입니다.") + @Min(value = 1, message = "펀딩 기간은 1일 이상이어야 합니다.") + int duration, + @NotNull(message = "펀딩 시작일은 필수 입력 값입니다.") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime startAt, + @NotNull(message = "기프티콘 리스트는 필수 입력 값입니다.") + List gifticonIds +) { + + public FundingArticleCreate toDomain() { + return FundingArticleCreate.builder() + .title(title) + .content(content) + .duration(duration) + .startAt(startAt) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/funding/response/FundingArticleResponse.java b/src/main/java/team/haedal/gifticionfunding/dto/funding/response/FundingArticleResponse.java new file mode 100644 index 0000000..91fa0cb --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/funding/response/FundingArticleResponse.java @@ -0,0 +1,28 @@ +package team.haedal.gifticionfunding.dto.funding.response; + +import java.time.LocalDateTime; +import lombok.Builder; +import team.haedal.gifticionfunding.entity.funding.FundingArticle; + +@Builder +public record FundingArticleResponse( + Long id, + LocalDateTime startAt, + LocalDateTime endAt, + String title, + String content + +) { + + public static FundingArticleResponse from(FundingArticle fundingArticle) { + return FundingArticleResponse.builder() + .id(fundingArticle.getId()) + .startAt(fundingArticle.getCreatedAt()) + .endAt(fundingArticle.getEndAt()) + .title(fundingArticle.getTitle()) + .content(fundingArticle.getContent()) + .build(); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/funding/response/FundingInfoResponse.java b/src/main/java/team/haedal/gifticionfunding/dto/funding/response/FundingInfoResponse.java new file mode 100644 index 0000000..8a4ebc8 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/funding/response/FundingInfoResponse.java @@ -0,0 +1,10 @@ +package team.haedal.gifticionfunding.dto.funding.response; + +import lombok.Builder; + +@Builder +public record FundingInfoResponse( + +) { + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/gifticon/request/GifticonCreateRequest.java b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/request/GifticonCreateRequest.java new file mode 100644 index 0000000..af682f0 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/request/GifticonCreateRequest.java @@ -0,0 +1,49 @@ +package team.haedal.gifticionfunding.dto.gifticon.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import team.haedal.gifticionfunding.entity.gifticon.Category; +import team.haedal.gifticionfunding.entity.gifticon.GifticonCreate; +import team.haedal.gifticionfunding.entity.gifticon.GifticonPurchase; + +import java.time.LocalDate; + +@Getter +public class GifticonCreateRequest{ + @Min(value = 1,message = "가격은 1원 이상이어야 합니다.") + private Long price; + + @NotBlank(message = "상품명은 필수 입력 값입니다.") + private String name; + + @NotBlank(message = "카테고리는 필수 입력 값입니다.") + private String category; + + @Min(value=1,message = "재고는 1 이상이어야 합니다.") + private Long stock; + + private String imageUrl; + + @NotBlank(message = "설명은 필수 입력 값입니다.") + private String description; + + @NotBlank(message = "브랜드는 필수 입력 값입니다.") + private String brand; + + @NotBlank(message = "유효기간은 필수 입력 값입니다.") + private String expirationPeriod; + + public GifticonCreate toCommand(){ + return GifticonCreate.builder() + .price(price) + .name(name) + .category(Category.valueOf(category)) + .stock(stock) + .imageUrl(imageUrl) + .description(description) + .brand(brand) + .expirationPeriod(LocalDate.parse(expirationPeriod)) + .build(); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/gifticon/request/GifticonUpdateRequest.java b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/request/GifticonUpdateRequest.java new file mode 100644 index 0000000..6f850dd --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/request/GifticonUpdateRequest.java @@ -0,0 +1,53 @@ +package team.haedal.gifticionfunding.dto.gifticon.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import team.haedal.gifticionfunding.entity.gifticon.Category; +import team.haedal.gifticionfunding.entity.gifticon.GifticonCreate; +import team.haedal.gifticionfunding.entity.gifticon.GifticonUpdate; + +import java.time.LocalDate; + +@Getter +public class GifticonUpdateRequest { + private Long price; + + private String name; + + private String category; + + private Long stock; + + private String imageUrl; + + private String description; + + private String brand; + + private String expirationPeriod; + + public GifticonUpdate toCommand(){ + if(this.category == null){ + return GifticonUpdate.builder() + .price(price) + .name(name) + .stock(stock) + .imageUrl(imageUrl) + .description(description) + .brand(brand) + .expirationPeriod(LocalDate.parse(expirationPeriod)) + .build(); + } + return GifticonUpdate.builder() + .price(price) + .name(name) + .category(Category.valueOf(category)) + .stock(stock) + .imageUrl(imageUrl) + .description(description) + .brand(brand) + .expirationPeriod(LocalDate.parse(expirationPeriod)) + .build(); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/gifticon/response/GifticonDetailResponse.java b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/response/GifticonDetailResponse.java new file mode 100644 index 0000000..680bbc3 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/response/GifticonDetailResponse.java @@ -0,0 +1,52 @@ +package team.haedal.gifticionfunding.dto.gifticon.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.haedal.gifticionfunding.entity.gifticon.Category; +import team.haedal.gifticionfunding.entity.gifticon.Gifticon; + +import java.time.LocalDate; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class GifticonDetailResponse { + private Long id; + private Long price; + private String name; + private Category category; + private Long stock; + private String imageUrl; + private String description; + private String brand; + private LocalDate expirationPeriod; + + @Builder + private GifticonDetailResponse(final Long id, final Long price, final String name, final Category category, final Long stock, final String imageUrl, final String description, final String brand, final LocalDate expirationPeriod) { + this.id = id; + this.price = price; + this.name = name; + this.category = category; + this.stock = stock; + this.imageUrl = imageUrl; + this.description = description; + this.brand = brand; + this.expirationPeriod = expirationPeriod; + } + + public static GifticonDetailResponse from(final Gifticon gifticon){ + return GifticonDetailResponse.builder() + .id(gifticon.getId()) + .price(gifticon.getPrice()) + .name(gifticon.getName()) + .category(gifticon.getCategory()) + .stock(gifticon.getStock()) + .imageUrl(gifticon.getImageUrl()) + .description(gifticon.getDescription()) + .brand(gifticon.getBrand()) + .expirationPeriod(gifticon.getExpirationPeriod()) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/gifticon/response/GifticonResponse.java b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/response/GifticonResponse.java new file mode 100644 index 0000000..69028fd --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/gifticon/response/GifticonResponse.java @@ -0,0 +1,61 @@ +package team.haedal.gifticionfunding.dto.gifticon.response; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.cglib.core.Local; +import team.haedal.gifticionfunding.entity.gifticon.Category; +import team.haedal.gifticionfunding.entity.gifticon.Gifticon; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GifticonResponse { + private Long id; + private Long price; + private String name; + private Category category; + private Long stock; + private String imageUrl; + private String description; + private String brand; + private LocalDate expirationPeriod; + + + @Builder + private GifticonResponse(final Long id, final Long price, final String name, final Category category, final Long stock, final String imageUrl, final String description, final String brand, final LocalDate expirationPeriod) { + this.id = id; + this.price = price; + this.name = name; + this.category = category; + this.stock = stock; + this.imageUrl = imageUrl; + this.description = description; + this.brand = brand; + this.expirationPeriod = expirationPeriod; + } + + public static GifticonResponse from(final Gifticon gifticon){ + return GifticonResponse.builder() + .id(gifticon.getId()) + .price(gifticon.getPrice()) + .name(gifticon.getName()) + .category(gifticon.getCategory()) + .stock(gifticon.getStock()) + .imageUrl(gifticon.getImageUrl()) + .description(gifticon.getDescription()) + .brand(gifticon.getBrand()) + .expirationPeriod(gifticon.getExpirationPeriod()) + .build(); + } + + public static List fromList(final List gifticons){ + return gifticons.stream() + .map(GifticonResponse::from) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/user/request/FriendRequestCreateRequest.java b/src/main/java/team/haedal/gifticionfunding/dto/user/request/FriendRequestCreateRequest.java new file mode 100644 index 0000000..3605d4d --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/user/request/FriendRequestCreateRequest.java @@ -0,0 +1,27 @@ +package team.haedal.gifticionfunding.dto.user.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class FriendRequestCreateRequest { + @NotBlank(message = "요청자 아이디는 필수 입력 값입니다.") + private final Long requesterId; + @NotBlank(message = "수신자 아이디는 필수 입력 값입니다.") + private final Long receiverId; + + @Builder + private FriendRequestCreateRequest(Long requesterId, Long receiverId) { + this.requesterId = requesterId; + this.receiverId = receiverId; + } + + public static FriendRequestCreateRequest from(Long requesterId, Long receiverId){ + return FriendRequestCreateRequest.builder() + .requesterId(requesterId) + .receiverId(receiverId) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/user/request/UserCreate.java b/src/main/java/team/haedal/gifticionfunding/dto/user/request/UserCreate.java new file mode 100644 index 0000000..55cba61 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/user/request/UserCreate.java @@ -0,0 +1,41 @@ +package team.haedal.gifticionfunding.dto.user.request; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Getter; +import team.haedal.gifticionfunding.entity.user.Role; + +import java.time.LocalDate; + +@Getter +public class UserCreate { + + private final String email; + private final String nickname; + private final Long point; + private final LocalDate birthdate; + private final String profileImageUrl; + private final Role role; + + @Builder + private UserCreate(String email, String nickname, Long point, LocalDate birthdate, String profileImageUrl, Role role) { + this.email = email; + this.nickname = nickname; + this.point = point; + this.birthdate = birthdate; + this.profileImageUrl = profileImageUrl; + this.role = role; + } + + public static UserCreate from(JsonNode jsonNode){ + return UserCreate.builder() + .email(jsonNode.get("kakao_account").get("email").asText()) + .nickname(jsonNode.get("properties").get("nickname").asText()) + .point(0L) + .birthdate(LocalDate.parse("1999-01-01")) + //.birthdate(LocalDate.parse(jsonNode.get("kakao_account").get("birthday").asText())) + .role(Role.USER) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendInfoDto.java b/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendInfoDto.java new file mode 100644 index 0000000..8a0c61a --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendInfoDto.java @@ -0,0 +1,27 @@ +package team.haedal.gifticionfunding.dto.user.response; + +import java.time.LocalDate; +import lombok.Builder; +import team.haedal.gifticionfunding.entity.user.Friendship; + +@Builder +public record FriendInfoDto( + Long friendshipId, + Long friendId, + String nickname, + String profileImageUrl, + LocalDate birthDate) { + + public static FriendInfoDto from(Friendship friendship) { + var friend = friendship.getUser2(); + return FriendInfoDto.builder() + .friendshipId(friendship.getId()) + .friendId(friend.getId()) + .nickname(friend.getNickname()) + .profileImageUrl(friend.getProfileImageUrl()) + .birthDate(friend.getBirthdate()) + .build(); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendRequestReceiverDto.java b/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendRequestReceiverDto.java new file mode 100644 index 0000000..a18c270 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendRequestReceiverDto.java @@ -0,0 +1,23 @@ +package team.haedal.gifticionfunding.dto.user.response; + +import java.time.LocalDate; +import lombok.Builder; +import team.haedal.gifticionfunding.entity.user.FriendRequest; + +@Builder +public record FriendRequestReceiverDto ( + Long id, + UserInfoDto requestee, + LocalDate createdAt +){ + public static FriendRequestReceiverDto from(FriendRequest action) { + + var user = action.getRequestee(); + return FriendRequestReceiverDto.builder() + .id(action.getId()) + .requestee(UserInfoDto.from(user)) + .createdAt(action.getCreatedAt().toLocalDate()) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendRequestSenderDto.java b/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendRequestSenderDto.java new file mode 100644 index 0000000..dac4faf --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/user/response/FriendRequestSenderDto.java @@ -0,0 +1,23 @@ +package team.haedal.gifticionfunding.dto.user.response; + +import java.time.LocalDate; +import lombok.Builder; +import team.haedal.gifticionfunding.entity.user.FriendRequest; + +@Builder +public record FriendRequestSenderDto ( + Long id, + UserInfoDto requester, + LocalDate createdAt +){ + public static FriendRequestSenderDto from(FriendRequest action) { + + var user = action.getRequester(); + return FriendRequestSenderDto.builder() + .id(action.getId()) + .requester(UserInfoDto.from(user)) + .createdAt(action.getCreatedAt().toLocalDate()) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/dto/user/response/UserInfoDto.java b/src/main/java/team/haedal/gifticionfunding/dto/user/response/UserInfoDto.java new file mode 100644 index 0000000..db9e9aa --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/dto/user/response/UserInfoDto.java @@ -0,0 +1,24 @@ +package team.haedal.gifticionfunding.dto.user.response; + +import java.time.LocalDate; +import lombok.Builder; +import org.springframework.cglib.core.Local; +import team.haedal.gifticionfunding.entity.user.User; + +@Builder +public record UserInfoDto( + Long id, + String nickname, + String profileImageUrl, + LocalDate birthDate +) { + public static UserInfoDto from(User user) { + return UserInfoDto.builder() + .id(user.getId()) + .nickname(user.getNickname()) + .profileImageUrl(user.getProfileImageUrl()) + .birthDate(user.getBirthdate()) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingArticle.java b/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingArticle.java new file mode 100644 index 0000000..e8b2cdb --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingArticle.java @@ -0,0 +1,66 @@ +package team.haedal.gifticionfunding.entity.funding; + +import static lombok.AccessLevel.PROTECTED; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.haedal.gifticionfunding.dto.funding.domain.FundingArticleCreate; +import team.haedal.gifticionfunding.entity.user.User; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class FundingArticle { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @NotNull + private LocalDateTime createdAt; + + @NotNull + private LocalDateTime endAt; + + @NotNull + private String title; + + @NotNull + private String content; + + @Builder + private FundingArticle(User user, LocalDateTime createdAt, LocalDateTime endAt, String title, + String content) { + this.user = user; + this.createdAt = createdAt; + this.endAt = endAt; + this.title = title; + this.content = content; + } + + public static FundingArticle create(FundingArticleCreate fundingArticleCreate, User user) { + return FundingArticle.builder() + .user(user) + .createdAt(LocalDateTime.now()) + .endAt(LocalDateTime.now().plusDays(fundingArticleCreate.getDuration())) + .title(fundingArticleCreate.getTitle()) + .content(fundingArticleCreate.getContent()) + .build(); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingArticleGifticon.java b/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingArticleGifticon.java new file mode 100644 index 0000000..bf0abe7 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingArticleGifticon.java @@ -0,0 +1,42 @@ +package team.haedal.gifticionfunding.entity.funding; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.haedal.gifticionfunding.entity.gifticon.Gifticon; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FundingArticleGifticon { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private FundingArticle fundingArticle; + + @ManyToOne(fetch = FetchType.LAZY) + private Gifticon gifticon; + + @Builder + private FundingArticleGifticon(FundingArticle fundingArticle, Gifticon gifticon) { + this.fundingArticle = fundingArticle; + this.gifticon = gifticon; + } + + public static FundingArticleGifticon create(FundingArticle fundingArticle, Gifticon gifticon) { + return FundingArticleGifticon.builder() + .fundingArticle(fundingArticle) + .gifticon(gifticon) + .build(); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingContribute.java b/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingContribute.java new file mode 100644 index 0000000..1b2f9b6 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/funding/FundingContribute.java @@ -0,0 +1,4 @@ +package team.haedal.gifticionfunding.entity.funding; + +public class FundingContribute { +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/gifticon/Category.java b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/Category.java new file mode 100644 index 0000000..eb3bbfd --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/Category.java @@ -0,0 +1,7 @@ +package team.haedal.gifticionfunding.entity.gifticon; + +public enum Category { + FASHION, BEAUTY, FOOD, SPORTS, LIFE, TRAVEL, CULTURE, HOBBY, + CONVENIENCE, HEALTH, EDUCATION, FASTFOOD, CAFE, DESSERT,SUPERMARKET, + ETC +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/gifticon/Gifticon.java b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/Gifticon.java new file mode 100644 index 0000000..f0f3fce --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/Gifticon.java @@ -0,0 +1,111 @@ +package team.haedal.gifticionfunding.entity.gifticon; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import team.haedal.gifticionfunding.global.error.NotEnoughStockException; + +import java.time.LocalDate; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Slf4j +public class Gifticon { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private Long price; + + @NotNull + private String name; + + @NotNull + @Enumerated(EnumType.STRING) + private Category category; + + @NotNull + private Long stock; + + private String imageUrl; + + @NotNull + private String description; + + @NotNull + private String brand; + + @NotNull + private LocalDate expirationPeriod; + + /** 생성 메서드 */ + public static Gifticon createGifticon(GifticonCreate gifticonCreate) { + return Gifticon.builder() + .price(gifticonCreate.getPrice()) + .name(gifticonCreate.getName()) + .category(gifticonCreate.getCategory()) + .stock(gifticonCreate.getStock()) + .imageUrl(gifticonCreate.getImageUrl()) + .description(gifticonCreate.getDescription()) + .brand(gifticonCreate.getBrand()) + .expirationPeriod(gifticonCreate.getExpirationPeriod()) + .build(); + } + + /** 빌더 패턴 */ + @Builder + private Gifticon(Long price, String name, Category category, Long stock, String imageUrl, String description, String brand, LocalDate expirationPeriod) { + this.price = price; + this.name = name; + this.category = category; + this.stock = stock; + this.imageUrl = imageUrl; + this.description = description; + this.brand = brand; + this.expirationPeriod = expirationPeriod; + } + + //==비즈니스 로직==// + + /** + * 재고 감소 + */ + public void removeStock(Long quantity) { + Long restStock = this.stock - quantity; + if (this.stock < 0) { + throw new NotEnoughStockException("재고가 부족합니다."); + } + this.stock = restStock; + } + + /** + * 재고 증가 + */ + public void addStock(Long quantity) { + this.stock += quantity; + } + + /** + * 상품 수정 + */ + public Gifticon updateGifticon(final GifticonUpdate gifticonUpdate) { + this.price = gifticonUpdate.getPrice() != null ? gifticonUpdate.getPrice() : this.price; + this.name = gifticonUpdate.getName() != null ? gifticonUpdate.getName() : this.name; + this.category = gifticonUpdate.getCategory() != null ? gifticonUpdate.getCategory() : this.category; + this.stock = gifticonUpdate.getStock() != null ? gifticonUpdate.getStock() : this.stock; + this.imageUrl = gifticonUpdate.getImageUrl() != null ? gifticonUpdate.getImageUrl() : this.imageUrl; + this.description = gifticonUpdate.getDescription() != null ? gifticonUpdate.getDescription() : this.description; + this.brand = gifticonUpdate.getBrand() != null ? gifticonUpdate.getBrand() : this.brand; + this.expirationPeriod = gifticonUpdate.getExpirationPeriod() != null ? gifticonUpdate.getExpirationPeriod() : this.expirationPeriod; + + return this; + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonCreate.java b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonCreate.java new file mode 100644 index 0000000..e790597 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonCreate.java @@ -0,0 +1,19 @@ +package team.haedal.gifticionfunding.entity.gifticon; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +@Builder +public class GifticonCreate { + private final Long price; + private final String name; + private final Category category; + private final Long stock; + private final String imageUrl; + private final String description; + private final String brand; + private final LocalDate expirationPeriod; +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonPurchase.java b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonPurchase.java new file mode 100644 index 0000000..b4563ce --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonPurchase.java @@ -0,0 +1,17 @@ +package team.haedal.gifticionfunding.entity.gifticon; + +import lombok.Builder; +import lombok.Getter; +import team.haedal.gifticionfunding.entity.user.User; + +import java.time.LocalDate; + +@Builder +@Getter +public class GifticonPurchase { + private final User buyer; + private final User owner; + private final Gifticon gifticon; + private final LocalDate purchaseDate; + private final LocalDate expirationDate; +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonUpdate.java b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonUpdate.java new file mode 100644 index 0000000..b29e712 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/GifticonUpdate.java @@ -0,0 +1,22 @@ +package team.haedal.gifticionfunding.entity.gifticon; + + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +@Builder +@Getter +public class GifticonUpdate { + private final Long price; + private final String name; + private final Category category; + private final Long stock; + private final String imageUrl; + private final String description; + private final String brand; + private final LocalDate expirationPeriod; + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/gifticon/UserGifticon.java b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/UserGifticon.java new file mode 100644 index 0000000..37c8f69 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/gifticon/UserGifticon.java @@ -0,0 +1,105 @@ +package team.haedal.gifticionfunding.entity.gifticon; + +import jakarta.persistence.*; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.haedal.gifticionfunding.entity.user.User; + +import java.time.LocalDate; +import java.util.UUID; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserGifticon { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "buyer_id") + private User buyer; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + @JoinColumn(name = "owner_id") + private User owner; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + @JoinColumn(name = "gifticon_id") + private Gifticon gifticon; + + @NotNull + private LocalDate purchaseDate; + + @NotNull + private LocalDate expirationDate; + + + private LocalDate usedDate; + + @Column(length = 12) + private String giftCode; + + //--비즈니스 로직--// + /** 기프티콘 구매 */ + public static UserGifticon purchaseGifticon(GifticonPurchase gifticonPurchase){ + UserGifticon userGifticon = new UserGifticon(); + userGifticon.buyer = gifticonPurchase.getBuyer(); + userGifticon.owner = gifticonPurchase.getOwner(); + userGifticon.gifticon = gifticonPurchase.getGifticon(); + userGifticon.purchaseDate = gifticonPurchase.getPurchaseDate(); + userGifticon.expirationDate = gifticonPurchase.getExpirationDate(); + // 구매한 기프티콘의 재고를 감소시킨다. + gifticonPurchase.getGifticon().removeStock(gifticonPurchase.getGifticon().getId()); + // 구매한 기프티콘의 금액만큼 구매자의 포인트를 차감한다. + gifticonPurchase.getBuyer().usePoint(gifticonPurchase.getGifticon().getPrice()); + return userGifticon; + } + + /** 기프티콘 사용 */ + public String useGifticon(){ + if(this.usedDate != null || this.giftCode != null){ + throw new IllegalStateException("이미 사용한 기프티콘입니다."); + } + if(LocalDate.now().isAfter(this.expirationDate)){ + throw new IllegalStateException("기프티콘 유효기간이 만료되었습니다."); + } + this.usedDate = LocalDate.now(); + this.giftCode = UUID.randomUUID().toString().substring(0, 12); + return giftCode; + } + + /** 기프티콘 소유자 변경 */ + public void changeOwner(User owner){ + if(this.usedDate != null || this.giftCode != null){ + throw new IllegalStateException("이미 사용한 기프티콘은 소유자를 변경할 수 없습니다."); + } + if(LocalDate.now().isAfter(this.expirationDate)){ + throw new IllegalStateException("기프티콘 유효기간이 만료되어 소유자를 변경할 수 없습니다."); + } + if(this.owner.equals(owner)){ + throw new IllegalStateException("이미 소유한 기프티콘입니다."); + } + + this.owner = owner; + } + + + //환불 관련해서 status 필요할듯 + /** 기프티콘 환불 */ + public void refundGifticon(){ + if(this.usedDate != null || this.giftCode != null){ + throw new IllegalStateException("이미 사용한 기프티콘은 환불할 수 없습니다."); + } + if(LocalDate.now().isAfter(this.expirationDate)){ + throw new IllegalStateException("기프티콘 유효기간이 만료되어 환불할 수 없습니다."); + } + // 환불한 기프티콘의 재고를 증가시킨다. + this.gifticon.addStock(this.gifticon.getId()); + // 소유자에게 기프티콘 금액을 환불한다. + this.owner.chargePoint(this.gifticon.getPrice()); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/user/FriendRequest.java b/src/main/java/team/haedal/gifticionfunding/entity/user/FriendRequest.java new file mode 100644 index 0000000..c16f9cf --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/user/FriendRequest.java @@ -0,0 +1,57 @@ +package team.haedal.gifticionfunding.entity.user; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.springframework.data.util.Pair; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FriendRequest { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "from_user_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private User requester; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "to_user_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private User requestee; + + @NotNull + private LocalDateTime createdAt; + + @Builder + private FriendRequest(User requester, User requestee, LocalDateTime createdAt) { + this.requester = requester; + this.requestee = requestee; + this.createdAt = createdAt; + } + + public static FriendRequest create(User requester, User requestee) { + return FriendRequest.builder() + .requester(requester) + .requestee(requestee) + .createdAt(LocalDateTime.now()) + .build(); + } + + public Pair makeFriendship() { + Friendship friendship1 = Friendship.create(requester, requestee); + Friendship friendship2 = Friendship.create(requestee, requester); + return Pair.of(friendship1, friendship2); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/user/Friendship.java b/src/main/java/team/haedal/gifticionfunding/entity/user/Friendship.java new file mode 100644 index 0000000..22c7901 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/user/Friendship.java @@ -0,0 +1,41 @@ +package team.haedal.gifticionfunding.entity.user; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Friendship { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user1_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private User user1; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user2_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private User user2; + + @Builder + private Friendship(User user1, User user2) { + this.user1 = user1; + this.user2 = user2; + } + + public static Friendship create(User user1, User user2) { + return Friendship.builder() + .user1(user1) + .user2(user2) + .build(); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/user/Role.java b/src/main/java/team/haedal/gifticionfunding/entity/user/Role.java new file mode 100644 index 0000000..4a25512 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/user/Role.java @@ -0,0 +1,5 @@ +package team.haedal.gifticionfunding.entity.user; + +public enum Role { + ADMIN, USER +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/user/User.java b/src/main/java/team/haedal/gifticionfunding/entity/user/User.java new file mode 100644 index 0000000..6e0368f --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/user/User.java @@ -0,0 +1,89 @@ +package team.haedal.gifticionfunding.entity.user; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.haedal.gifticionfunding.dto.user.request.UserCreate; + +import java.time.LocalDate; + +@Entity +@Table(name = "users") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + + // @NotNull 어노테이션을 쓰면, 데이터베이스에 SQL 쿼리를 보내기 전에 예외가 발생한다. + // nullable=false 로 @Column 어노테이션에 속성을 붙이면, + // null을 넣은 엔티티를 생성하면 생성이 된 뒤 Repository에 전달되고, + // 이 값이 DB에 넘어간 뒤에 예외가 발생해 위험한 오류를 맞을 수 있다. + @Column(unique = true) + @NotNull + private String email; + + @Column(unique = true) + @NotNull + private String nickname; + + @NotNull + private Long point; + + @NotNull + private LocalDate birthdate; + private String profileImageUrl; + + @NotNull + @Enumerated(EnumType.STRING) + private Role role; + + /** + * 생성 메서드 + */ + public static User createUser(UserCreate userCreate) { + return User.builder() + .email(userCreate.getEmail()) + .nickname(userCreate.getNickname()) + .point(userCreate.getPoint()) + .birthdate(userCreate.getBirthdate()) + .profileImageUrl(userCreate.getProfileImageUrl()) + .role(userCreate.getRole()) + .build(); + } + + /** + * 빌더 패턴 + */ + @Builder + private User(String email, String nickname, Long point, LocalDate birthdate, String profileImageUrl, Role role) { + this.email = email; + this.nickname = nickname; + this.point = point; + this.birthdate = birthdate; + this.profileImageUrl = profileImageUrl; + this.role = role; + } + //== 비즈니스 로직==// + + /** + * 포인트 충전 + */ + public void chargePoint(Long point) { + this.point += point; + } + + /** + * 포인트 사용 + */ + public void usePoint(Long price) { + if (this.point < price) { + throw new IllegalStateException("포인트가 부족합니다."); + } + this.point -= price; + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/entity/user/UserRefreshToken.java b/src/main/java/team/haedal/gifticionfunding/entity/user/UserRefreshToken.java new file mode 100644 index 0000000..8c6cb23 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/entity/user/UserRefreshToken.java @@ -0,0 +1,4 @@ +package team.haedal.gifticionfunding.entity.user; + +public class UserRefreshToken { +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/config/SwaggerConfig.java b/src/main/java/team/haedal/gifticionfunding/global/config/SwaggerConfig.java new file mode 100644 index 0000000..26a1815 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package team.haedal.gifticionfunding.global.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.OpenAPI; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@OpenAPIDefinition( + info = @io.swagger.v3.oas.annotations.info.Info( + title = "Gifticion Funding API", + description = "기프티콘 펀딩 서비스 API 명세서", + version = "v1" + ) +) +@RequiredArgsConstructor +@Configuration +public class SwaggerConfig { + @Bean + public OpenAPI gifticonFundingAPI(){ + return new OpenAPI(); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/CommonExceptionHandler.java b/src/main/java/team/haedal/gifticionfunding/global/error/CommonExceptionHandler.java new file mode 100644 index 0000000..9ff1cf6 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/CommonExceptionHandler.java @@ -0,0 +1,48 @@ +package team.haedal.gifticionfunding.global.error; + +import io.jsonwebtoken.ExpiredJwtException; +import lombok.extern.slf4j.Slf4j; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import team.haedal.gifticionfunding.global.error.auth.InvalidTokenException; + +import java.time.format.DateTimeParseException; + +@RestControllerAdvice(basePackages = "team.haedal.gifticionfunding") +@Slf4j +public class CommonExceptionHandler { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({IllegalArgumentException.class, MethodArgumentNotValidException.class, NotEnoughStockException.class}) + public ResponseEntity badRequestExceptionHandler(IllegalArgumentException e){ + log.error("IllegalArgumentException : {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage())); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(DateTimeParseException.class) + public ResponseEntity badDateFormatExceptionHandler(DateTimeParseException e){ + log.error("DateTimeParseException : {}","날짜 형식이 맞지 않습니다 YYYY-MM-DD 의 형식을 지원합니다 " ); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "날짜 형식이 맞지 않습니다")); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler + public ResponseEntity exHandle(Exception e) { + log.error("[exceptionHandle] ex", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage())); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler + public ResponseEntity notFoundGifticonExceptionHandler(NotFoundGifticonException e){ + log.error("NotFoundGifticonException : {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND.value(), e.getMessage())); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/ErrorResponse.java b/src/main/java/team/haedal/gifticionfunding/global/error/ErrorResponse.java new file mode 100644 index 0000000..80944af --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/ErrorResponse.java @@ -0,0 +1,11 @@ +package team.haedal.gifticionfunding.global.error; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ErrorResponse { + private int code; + private String message; +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/NotEnoughStockException.java b/src/main/java/team/haedal/gifticionfunding/global/error/NotEnoughStockException.java new file mode 100644 index 0000000..08a90fa --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/NotEnoughStockException.java @@ -0,0 +1,19 @@ +package team.haedal.gifticionfunding.global.error; + +public class NotEnoughStockException extends RuntimeException{ + public NotEnoughStockException() { + super(); + } + + public NotEnoughStockException(String message) { + super(message); + } + + public NotEnoughStockException(String message, Throwable cause) { + super(message, cause); + } + + public NotEnoughStockException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundFriendRequestException.java b/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundFriendRequestException.java new file mode 100644 index 0000000..ba72d61 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundFriendRequestException.java @@ -0,0 +1,21 @@ +package team.haedal.gifticionfunding.global.error; + +public class NotFoundFriendRequestException extends IllegalArgumentException { + + public NotFoundFriendRequestException() { + super(); + } + + public NotFoundFriendRequestException(String message) { + super(message); + } + + public NotFoundFriendRequestException(String message, Throwable cause) { + super(message, cause); + } + + public NotFoundFriendRequestException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundGifticonException.java b/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundGifticonException.java new file mode 100644 index 0000000..3a91229 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundGifticonException.java @@ -0,0 +1,19 @@ +package team.haedal.gifticionfunding.global.error; + +public class NotFoundGifticonException extends IllegalArgumentException{ + public NotFoundGifticonException() { + super(); + } + + public NotFoundGifticonException(String message) { + super(message); + } + + public NotFoundGifticonException(String message, Throwable cause) { + super(message, cause); + } + + public NotFoundGifticonException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundUserException.java b/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundUserException.java new file mode 100644 index 0000000..d35fb0e --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/NotFoundUserException.java @@ -0,0 +1,21 @@ +package team.haedal.gifticionfunding.global.error; + +public class NotFoundUserException extends IllegalArgumentException { + + public NotFoundUserException() { + super(); + } + + public NotFoundUserException(String message) { + super(message); + } + + public NotFoundUserException(String message, Throwable cause) { + super(message, cause); + } + + public NotFoundUserException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/auth/CustomAccessDeniedHandler.java b/src/main/java/team/haedal/gifticionfunding/global/error/auth/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..5d0910d --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/auth/CustomAccessDeniedHandler.java @@ -0,0 +1,22 @@ +package team.haedal.gifticionfunding.global.error.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + + response.sendError(HttpServletResponse.SC_FORBIDDEN, "권한이 없는 사용자입니다."); // 403 Forbidden + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/auth/CustomAuthenticationEntryPoint.java b/src/main/java/team/haedal/gifticionfunding/global/error/auth/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..240152c --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/auth/CustomAuthenticationEntryPoint.java @@ -0,0 +1,22 @@ +package team.haedal.gifticionfunding.global.error.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "인증되지 않은 클라이언트입니다."); // 401 Unauthorized + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/error/auth/InvalidTokenException.java b/src/main/java/team/haedal/gifticionfunding/global/error/auth/InvalidTokenException.java new file mode 100644 index 0000000..46136b1 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/error/auth/InvalidTokenException.java @@ -0,0 +1,11 @@ +package team.haedal.gifticionfunding.global.error.auth; + +import io.jsonwebtoken.ExpiredJwtException; + +public class InvalidTokenException extends ExpiredJwtException { + + public InvalidTokenException(String message) { + super(null, null, message); + } + +} diff --git a/src/main/java/team/haedal/gifticionfunding/global/validation/GifticonUpdateValidator.java b/src/main/java/team/haedal/gifticionfunding/global/validation/GifticonUpdateValidator.java new file mode 100644 index 0000000..d66d683 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/global/validation/GifticonUpdateValidator.java @@ -0,0 +1,35 @@ +package team.haedal.gifticionfunding.global.validation; + +import team.haedal.gifticionfunding.entity.gifticon.GifticonUpdate; + +public class GifticonUpdateValidator { + public static boolean validate(GifticonUpdate gifticonUpdate){ + if(gifticonUpdate.getBrand() != null && gifticonUpdate.getBrand().isBlank()){ + throw new IllegalArgumentException("브랜드는 필수 입력 값입니다."); + } + if(gifticonUpdate.getDescription() != null && gifticonUpdate.getDescription().isBlank()){ + throw new IllegalArgumentException("설명은 필수 입력 값입니다."); + } + if(gifticonUpdate.getImageUrl() != null && gifticonUpdate.getImageUrl().isBlank()){ + throw new IllegalArgumentException("이미지 URL은 필수 입력 값입니다."); + } + if(gifticonUpdate.getName() != null && gifticonUpdate.getName().isBlank()){ + throw new IllegalArgumentException("상품명은 필수 입력 값입니다."); + } + if(gifticonUpdate.getPrice() != null && gifticonUpdate.getPrice() < 1){ + throw new IllegalArgumentException("가격은 1원 이상이어야 합니다."); + } + if(gifticonUpdate.getStock() != null && gifticonUpdate.getStock() < 1){ + throw new IllegalArgumentException("재고는 1 이상이어야 합니다."); + } + + return true; + } + + + + + + +} + diff --git a/src/main/java/team/haedal/gifticionfunding/repository/funding/FundingArticleGifticonJpaRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/funding/FundingArticleGifticonJpaRepository.java new file mode 100644 index 0000000..6130f99 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/funding/FundingArticleGifticonJpaRepository.java @@ -0,0 +1,9 @@ +package team.haedal.gifticionfunding.repository.funding; + +import org.springframework.data.jpa.repository.JpaRepository; +import team.haedal.gifticionfunding.entity.funding.FundingArticleGifticon; + +public interface FundingArticleGifticonJpaRepository extends + JpaRepository { + +} diff --git a/src/main/java/team/haedal/gifticionfunding/repository/funding/FundingArticleJpaRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/funding/FundingArticleJpaRepository.java new file mode 100644 index 0000000..92c3877 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/funding/FundingArticleJpaRepository.java @@ -0,0 +1,8 @@ +package team.haedal.gifticionfunding.repository.funding; + +import org.springframework.data.jpa.repository.JpaRepository; +import team.haedal.gifticionfunding.entity.funding.FundingArticle; + +public interface FundingArticleJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/team/haedal/gifticionfunding/repository/gifticon/GifticonJpaRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/gifticon/GifticonJpaRepository.java new file mode 100644 index 0000000..f504653 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/gifticon/GifticonJpaRepository.java @@ -0,0 +1,7 @@ +package team.haedal.gifticionfunding.repository.gifticon; + +import org.springframework.data.jpa.repository.JpaRepository; +import team.haedal.gifticionfunding.entity.gifticon.Gifticon; + +public interface GifticonJpaRepository extends JpaRepository { +} diff --git a/src/main/java/team/haedal/gifticionfunding/repository/gifticon/UserGifticonJpaRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/gifticon/UserGifticonJpaRepository.java new file mode 100644 index 0000000..4484aa3 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/gifticon/UserGifticonJpaRepository.java @@ -0,0 +1,14 @@ +package team.haedal.gifticionfunding.repository.gifticon; + +import org.springframework.data.jpa.repository.JpaRepository; +import team.haedal.gifticionfunding.entity.gifticon.UserGifticon; +import team.haedal.gifticionfunding.entity.user.User; + +import java.util.List; + +public interface UserGifticonJpaRepository extends JpaRepository { + List findByOwner(User owner); + + List findByBuyer(User buyer); + +} diff --git a/src/main/java/team/haedal/gifticionfunding/repository/user/FriendRequestJpaRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/user/FriendRequestJpaRepository.java new file mode 100644 index 0000000..d4b76fa --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/user/FriendRequestJpaRepository.java @@ -0,0 +1,11 @@ +package team.haedal.gifticionfunding.repository.user; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import team.haedal.gifticionfunding.entity.user.FriendRequest; + +public interface FriendRequestJpaRepository extends JpaRepository { + Page findAllByRequesterId(Long requesteeId, Pageable pageable); + Page findAllByRequesteeId(Long requesterId, Pageable pageable); +} diff --git a/src/main/java/team/haedal/gifticionfunding/repository/user/FriendshipJpaRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/user/FriendshipJpaRepository.java new file mode 100644 index 0000000..071529c --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/user/FriendshipJpaRepository.java @@ -0,0 +1,40 @@ +package team.haedal.gifticionfunding.repository.user; + +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import team.haedal.gifticionfunding.entity.user.Friendship; +import team.haedal.gifticionfunding.entity.user.User; + +public interface FriendshipJpaRepository extends JpaRepository { + + Page findAllByUser1_Id(Long userId, Pageable pageable); + + boolean existsByUser1AndUser2(User requester, User requestee); + + Optional findByUser1_IdAndUser2_Id(Long user1Id, Long user2Id); + + /** + * 1. 유저의 친구 조회 2. 그 친구의 친구 조회 + * + * @param userId + * @param pageable + * @return + */ + @Query(value = "SELECT DISTINCT user_id " + + "FROM (" + + " SELECT user2_id AS user_id FROM friendship WHERE user1_id = :userId " + + " UNION " + + " SELECT user1_id AS user_id FROM friendship WHERE user2_id = :userId " + + " UNION " + + " SELECT f2.user2_id AS user_id FROM friendship f1 " + + " JOIN friendship f2 ON f1.user2_id = f2.user1_id WHERE f1.user1_id = :userId " + + " UNION " + + " SELECT f2.user1_id AS user_id FROM friendship f1 " + + " JOIN friendship f2 ON f1.user2_id = f2.user1_id WHERE f1.user1_id = :userId" + + ") AS users " + + "WHERE user_id <> :userId", nativeQuery = true) + Page getFriendshipRelatedByUser1_id(Long userId, Pageable pageable); +} diff --git a/src/main/java/team/haedal/gifticionfunding/repository/user/UserJpaRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/user/UserJpaRepository.java new file mode 100644 index 0000000..e2b17b1 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/user/UserJpaRepository.java @@ -0,0 +1,10 @@ +package team.haedal.gifticionfunding.repository.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import team.haedal.gifticionfunding.entity.user.User; + +import java.util.Optional; + +public interface UserJpaRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/team/haedal/gifticionfunding/repository/user/UserRefreshTokenRepository.java b/src/main/java/team/haedal/gifticionfunding/repository/user/UserRefreshTokenRepository.java new file mode 100644 index 0000000..09a422a --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/repository/user/UserRefreshTokenRepository.java @@ -0,0 +1,4 @@ +package team.haedal.gifticionfunding.repository.user; + +public class UserRefreshTokenRepository { +} diff --git a/src/main/java/team/haedal/gifticionfunding/service/funding/FundingService.java b/src/main/java/team/haedal/gifticionfunding/service/funding/FundingService.java new file mode 100644 index 0000000..996f40d --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/service/funding/FundingService.java @@ -0,0 +1,73 @@ +package team.haedal.gifticionfunding.service.funding; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import team.haedal.gifticionfunding.dto.common.PagingRequest; +import team.haedal.gifticionfunding.dto.common.PagingResponse; +import team.haedal.gifticionfunding.dto.funding.domain.FundingArticleCreate; +import team.haedal.gifticionfunding.dto.funding.response.FundingArticleResponse; +import team.haedal.gifticionfunding.entity.funding.FundingArticle; +import team.haedal.gifticionfunding.entity.funding.FundingArticleGifticon; +import team.haedal.gifticionfunding.entity.gifticon.Gifticon; +import team.haedal.gifticionfunding.entity.user.User; +import team.haedal.gifticionfunding.repository.funding.FundingArticleGifticonJpaRepository; +import team.haedal.gifticionfunding.repository.funding.FundingArticleJpaRepository; +import team.haedal.gifticionfunding.repository.gifticon.GifticonJpaRepository; +import team.haedal.gifticionfunding.repository.user.UserJpaRepository; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class FundingService { + + private final UserJpaRepository userJpaRepository; + private final GifticonJpaRepository gifticonJpaRepository; + private final FundingArticleJpaRepository fundingArticleJpaRepository; + private final FundingArticleGifticonJpaRepository fundingArticleGifticonJpaRepository; + + /** + * FundingArticle 및 FundingArticleGifticon 생성 + * + * @return Funding id + */ + public Long createFunding(FundingArticleCreate fundingArticleCreate, List gifticonIds, + Long userId) { + User user = userJpaRepository.findById(userId).orElseThrow(); + FundingArticle fundingArticle = FundingArticle.create(fundingArticleCreate, user); + createFundingArticleGifticon(fundingArticle, gifticonIds); + + return fundingArticleJpaRepository.save(fundingArticle).getId(); + } + + /** + * FundingArticleGifticon 생성 + * + * @param fundingArticle + * @param gifticonIds + */ + private void createFundingArticleGifticon(FundingArticle fundingArticle, + List gifticonIds) { + List gifticons = gifticonIds.stream() + .map(gifticonId -> gifticonJpaRepository.findById(gifticonId).orElseThrow()) + .toList(); + + List fundingArticleGifticons = gifticons.stream() + .map(gifticon -> FundingArticleGifticon.create(fundingArticle, gifticon)) + .toList(); + + fundingArticleGifticonJpaRepository.saveAll(fundingArticleGifticons); + } + + /** + * 펀딩 게시글 페이징 조회 + */ + public PagingResponse getFundingArticlePaging( + PagingRequest pagingRequest) { + return PagingResponse.from(fundingArticleJpaRepository.findAll(pagingRequest.toPageable()), + FundingArticleResponse::from); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/service/gifticon/GifticonService.java b/src/main/java/team/haedal/gifticionfunding/service/gifticon/GifticonService.java new file mode 100644 index 0000000..58570cb --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/service/gifticon/GifticonService.java @@ -0,0 +1,95 @@ +package team.haedal.gifticionfunding.service.gifticon; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import team.haedal.gifticionfunding.entity.gifticon.Gifticon; +import team.haedal.gifticionfunding.entity.gifticon.GifticonCreate; +import team.haedal.gifticionfunding.entity.gifticon.GifticonUpdate; +import team.haedal.gifticionfunding.global.error.NotFoundGifticonException; +import team.haedal.gifticionfunding.global.validation.GifticonUpdateValidator; +import team.haedal.gifticionfunding.repository.gifticon.GifticonJpaRepository; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GifticonService { + + private final GifticonJpaRepository gifticonJpaRepository; + + //상품등록 + @Transactional + public Long registerGifticon(GifticonCreate gifticonCreate){ + Gifticon gifticon = Gifticon.createGifticon(gifticonCreate); + gifticonJpaRepository.save(gifticon); + return gifticon.getId(); + } + + //상품 전체 조회 + public List findGifticonAll(){ + return gifticonJpaRepository.findAll(); + } + + //상품 상세 조회 + public Gifticon findGifticon(Long gifticonId){ + Gifticon findGifticon= gifticonJpaRepository.findById(gifticonId).orElse(null); + if(findGifticon == null){ + throw new NotFoundGifticonException("해당 상품이 존재하지 않습니다."); + } + return findGifticon; + } + + //상품 수정 + @Transactional + public Gifticon updateGifticon(Long gifticonId, GifticonUpdate gifticonUpadate){ + Gifticon findGifticon = gifticonJpaRepository.findById(gifticonId).orElse(null); + if(findGifticon == null){ + throw new NotFoundGifticonException("해당 상품이 존재하지 않습니다."); + } + //gifticonUpdate 검증 + GifticonUpdateValidator.validate(gifticonUpadate); + + return findGifticon.updateGifticon(gifticonUpadate); + } + + //상품 삭제 + @Transactional + public void deleteGifticon(Long gifticonId){ + //상품이 있는지 확인 + Gifticon findGifticon = gifticonJpaRepository.findById(gifticonId).orElse(null); + if(findGifticon == null){ + throw new NotFoundGifticonException("해당 상품이 존재하지 않습니다."); + } + gifticonJpaRepository.deleteById(gifticonId); + } + + /** + * 재고 추가 + */ + @Transactional + public void addStock(Long gifticonId, Long stock){ + Gifticon findGifticon = gifticonJpaRepository.findById(gifticonId).orElse(null); + if(findGifticon == null){ + throw new NotFoundGifticonException("해당 상품이 존재하지 않습니다."); + } + findGifticon.addStock(stock); + } + + /** + * 재고 감소 + */ + @Transactional + public void removeStock(Long gifticonId, Long stock){ + Gifticon findGifticon = gifticonJpaRepository.findById(gifticonId).orElse(null); + if(findGifticon == null){ + throw new NotFoundGifticonException("해당 상품이 존재하지 않습니다."); + } + findGifticon.removeStock(stock); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/service/gifticon/UserGifticonService.java b/src/main/java/team/haedal/gifticionfunding/service/gifticon/UserGifticonService.java new file mode 100644 index 0000000..9d3c2ac --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/service/gifticon/UserGifticonService.java @@ -0,0 +1,117 @@ +package team.haedal.gifticionfunding.service.gifticon; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import team.haedal.gifticionfunding.entity.gifticon.GifticonPurchase; +import team.haedal.gifticionfunding.entity.gifticon.UserGifticon; +import team.haedal.gifticionfunding.entity.user.User; +import team.haedal.gifticionfunding.repository.gifticon.UserGifticonJpaRepository; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class UserGifticonService { + private final UserGifticonJpaRepository userGifticonJpaRepository; + + /** + * 유저기프티콘 전체 조회 + */ + public List findUserGifticonAll() + { + return userGifticonJpaRepository.findAll(); + } + + /** + * 유저기프티콘 상세 조회 + */ + public UserGifticon findUserGifticon(Long userGifticonId) + { + return userGifticonJpaRepository.findById(userGifticonId).orElse(null); + } + + /** + * 유저기프티콘 소유자별 조회 + */ + public List findUserGifticonByOwner(User owner) + { + return userGifticonJpaRepository.findByOwner(owner); + } + + /** + * 유저기프티콘 구매자별 조회 + */ + public List findUserGifticonByBuyer(User buyer) + { + return userGifticonJpaRepository.findByBuyer(buyer); + } + + /** + * 유저기프티콘 삭제 + */ + @Transactional + public void deleteUserGifticon(Long userGifticonId) + { + userGifticonJpaRepository.deleteById(userGifticonId); + } + + + + + /** + * 기프티콘 구매 + */ + @Transactional + public Long purchaseGifticon(GifticonPurchase gifticonPurchase) + { + UserGifticon userGifticon = UserGifticon.purchaseGifticon(gifticonPurchase); + userGifticonJpaRepository.save(userGifticon); + return userGifticon.getId(); + } + + /** + * 기프티콘 사용 + */ + @Transactional + public void useGifticon(Long userGifticonId) + { + UserGifticon userGifticon = userGifticonJpaRepository.findById(userGifticonId).orElse(null); + if(userGifticon == null) + { + throw new IllegalArgumentException("해당 기프티콘이 존재하지 않습니다."); + } + userGifticon.useGifticon(); + } + + /** + * 기프티콘 환불 + */ + @Transactional + public void refundGifticon(Long userGifticonId) + { + UserGifticon userGifticon = userGifticonJpaRepository.findById(userGifticonId).orElse(null); + if(userGifticon == null) + { + throw new IllegalArgumentException("해당 기프티콘이 존재하지 않습니다."); + } + userGifticon.refundGifticon(); + } + + /** + * 기프티콘 소유자 변경 + */ + @Transactional + public void changeOwner(Long userGifticonId, User owner) + { + UserGifticon userGifticon = userGifticonJpaRepository.findById(userGifticonId).orElse(null); + if(userGifticon == null) + { + throw new IllegalArgumentException("해당 기프티콘이 존재하지 않습니다."); + } + userGifticon.changeOwner(owner); + } + + +} diff --git a/src/main/java/team/haedal/gifticionfunding/service/user/FriendService.java b/src/main/java/team/haedal/gifticionfunding/service/user/FriendService.java new file mode 100644 index 0000000..acda1bb --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/service/user/FriendService.java @@ -0,0 +1,135 @@ +package team.haedal.gifticionfunding.service.user; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import team.haedal.gifticionfunding.dto.common.PagingRequest; +import team.haedal.gifticionfunding.dto.common.PagingResponse; +import team.haedal.gifticionfunding.dto.user.response.FriendInfoDto; +import team.haedal.gifticionfunding.dto.user.response.FriendRequestReceiverDto; +import team.haedal.gifticionfunding.dto.user.response.FriendRequestSenderDto; +import team.haedal.gifticionfunding.entity.user.FriendRequest; +import team.haedal.gifticionfunding.entity.user.Friendship; +import team.haedal.gifticionfunding.entity.user.User; +import team.haedal.gifticionfunding.global.error.NotFoundFriendRequestException; +import team.haedal.gifticionfunding.global.error.NotFoundUserException; +import team.haedal.gifticionfunding.repository.user.FriendRequestJpaRepository; +import team.haedal.gifticionfunding.repository.user.FriendshipJpaRepository; +import team.haedal.gifticionfunding.repository.user.UserJpaRepository; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class FriendService { + + private final UserJpaRepository userJpaRepository; + private final FriendRequestJpaRepository friendRequestJpaRepository; + private final FriendshipJpaRepository friendshipJpaRepository; + + /** + * 친구 추가 요청 + * + * @return FriendRequest id + */ + public Long requestFriend(Long userId, Long friendId) { + User requester = userJpaRepository.findById(userId) + .orElseThrow(() -> new NotFoundUserException("해당 유저가 존재하지 않습니다.")); + User requestee = userJpaRepository.findById(friendId) + .orElseThrow(() -> new NotFoundUserException("해당 유저가 존재하지 않습니다.")); + + /** + * 이미 친구인지 확인 + * user1과 user2가 친구인지 확인 + * user2와 user1이 친구인지 확인 + */ + if (friendshipJpaRepository.existsByUser1AndUser2(requester, requestee) || + friendshipJpaRepository.existsByUser1AndUser2(requestee, requester)) { + throw new IllegalArgumentException("이미 친구인 상대방에게는 친구 초대 요청을 보낼 수 없습니다."); + } + + FriendRequest friendRequest = FriendRequest.create(requester, requestee); + return friendRequestJpaRepository.save(friendRequest).getId(); + } + + /** + * 친구 추가 요청 보낸 목록 조회 + */ + @Transactional(readOnly = true) + public PagingResponse getFriendRequestSenderPaging( + PagingRequest pagingRequest, Long requesterUserId) { + Page friendRequestPage = friendRequestJpaRepository.findAllByRequesterId( + requesterUserId, pagingRequest.toPageable()); + return PagingResponse.from(friendRequestPage, FriendRequestReceiverDto::from); + } + + /** + * 친구 추가 요청 받은 목록 조회 + */ + @Transactional(readOnly = true) + public PagingResponse getFriendRequestReceiverPaging( + PagingRequest pagingRequest, Long requesteeUserId) { + Page friendRequestPage = friendRequestJpaRepository.findAllByRequesteeId( + requesteeUserId, pagingRequest.toPageable()); + return PagingResponse.from(friendRequestPage, FriendRequestSenderDto::from); + } + + /** + * 친구 추가 요청 수락 + */ + public void acceptFriendRequest(Long friendRequestId) { + log.info(friendRequestId.toString()); + FriendRequest friendRequest = friendRequestJpaRepository.findById(friendRequestId) + .orElseThrow(() -> new NotFoundFriendRequestException("해당 친구 요청이 존재하지 않습니다.")); + Pair friendShips = friendRequest.makeFriendship(); + friendshipJpaRepository.save(friendShips.getFirst()); + friendshipJpaRepository.save(friendShips.getSecond()); + friendRequestJpaRepository.delete(friendRequest); + } + + /** + * 친구 추가 요청 거절 + */ + public void rejectFriendRequest(Long friendRequestId) { + FriendRequest friendRequest = friendRequestJpaRepository.findById(friendRequestId) + .orElseThrow(() -> new NotFoundFriendRequestException("해당 친구 요청이 존재하지 않습니다.")); + friendRequestJpaRepository.delete(friendRequest); + } + + /** + * 친구 목록 페이징 조회 + */ + @Transactional(readOnly = true) + public PagingResponse getFriendPaging(PagingRequest pagingRequest, Long userId) { + Page friendshipPage = friendshipJpaRepository.findAllByUser1_Id(userId, + pagingRequest.toPageable()); + return PagingResponse.from(friendshipPage, friendship -> FriendInfoDto.from(friendship)); + } + + /** + * 친구 삭제 + */ + public void deleteFriend(Long user1Id, Long user2Id) { + /** + * Pair를 사용하여 (user1과 user2), ( user2와 user1)가 친구인지 확인 + * A->B, B->A 친구 관계를 삭제 // A->B 는 exception 발생, B->A는 exception 발생하지 않음 + */ + Pair friendships = friendshipJpaRepository + .findByUser1_IdAndUser2_Id(user1Id, user2Id) + .map(friendship -> Pair.of(friendship, friendshipJpaRepository + .findByUser1_IdAndUser2_Id(user2Id, user1Id).orElse(null))) + .orElseThrow(() -> new IllegalArgumentException("해당 친구가 존재하지 않습니다.")); + friendshipJpaRepository.delete((Friendship) friendships.getFirst()); + friendshipJpaRepository.delete((Friendship) friendships.getSecond()); + } + + public PagingResponse getRelatedFriendPaging(PagingRequest pagingRequest, + Long userId) { + Page friendshipPage = friendshipJpaRepository.getFriendshipRelatedByUser1_id( + userId, pagingRequest.toPageable()); + return PagingResponse.from(friendshipPage, friendship -> FriendInfoDto.from(friendship)); + } +} diff --git a/src/main/java/team/haedal/gifticionfunding/service/user/UserService.java b/src/main/java/team/haedal/gifticionfunding/service/user/UserService.java new file mode 100644 index 0000000..3b60767 --- /dev/null +++ b/src/main/java/team/haedal/gifticionfunding/service/user/UserService.java @@ -0,0 +1,35 @@ +package team.haedal.gifticionfunding.service.user; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import team.haedal.gifticionfunding.dto.user.request.UserCreate; +import team.haedal.gifticionfunding.entity.user.User; +import team.haedal.gifticionfunding.repository.user.UserJpaRepository; + +import java.util.Optional; + + +@Service +@Slf4j +@RequiredArgsConstructor +public class UserService { + + private final UserJpaRepository userJpaRepository; + private final RestTemplate restTemplate = new RestTemplate(); + + @Transactional + public Optional SignIn(UserCreate userInfo) { + //email로 회원가입이 되어있는지 확인 + Optional user = userJpaRepository.findByEmail(userInfo.getEmail()); + + //회원가입이 되어있지 않다면 회원가입 진행 + if (!user.isPresent()) { + userJpaRepository.save(User.createUser(userInfo)); + return userJpaRepository.findByEmail(userInfo.getEmail()); + } + return user; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a760115..4d89dc3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,12 +1,12 @@ spring: # default test profile datasource: - url: "jdbc:h2:mem:birthdayFunding;" + url: "jdbc:h2:tcp://localhost/~/test;" username: "sa" password: "" driver-class-name: org.h2.Driver jpa: hibernate: - ddl-auto: create + ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy properties: @@ -23,5 +23,22 @@ logging.level: springdoc: default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + path: /swagger-ui.html + groups-order: DESC + operationsSorter: method + disable-swagger-default-url: true + display-request-duration: true + api-docs: + path: /api-docs + show-actuator: true + default-consumes-media-type: application/json + default-produces-media-type: application/json + paths-to-match: jwt: - secret: 4099a46b-39db-4860-a61b-2ae76ea24c43 + secret: 4099a46b39db4860a61b2ae76ea24c434099a46b39db4860a61b2ae76ea24c434099a46b39db4860a61b2ae76ea24c434099a46b39db4860a61b2ae76ea24c434099a46b39db4860a61b2ae76ea24c434099a46b39db4860a61b2ae76ea24c43 +kakao: + client-id: ab472e238df9e16d1e7662ee32c4f9cf + redirect-uri: http://localhost:5173/redirection + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me \ No newline at end of file diff --git a/src/main/resources/db/data.sql b/src/main/resources/db/data.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/team/haedal/gifticionfunding/controller/friend/FriendControllerTest.java b/src/test/java/team/haedal/gifticionfunding/controller/friend/FriendControllerTest.java new file mode 100644 index 0000000..11982f1 --- /dev/null +++ b/src/test/java/team/haedal/gifticionfunding/controller/friend/FriendControllerTest.java @@ -0,0 +1,45 @@ +package team.haedal.gifticionfunding.controller.friend; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +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.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import team.haedal.gifticionfunding.auth.jwt.JwtProvider; +import team.haedal.gifticionfunding.service.user.FriendService; + +@SpringBootTest +@AutoConfigureMockMvc +public class FriendControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private FriendService friendService; + + @Autowired + private JwtProvider jwtProvider; + + @Test + @DisplayName("친구 목록 조회 페이징") + @WithMockUser(username = "test", roles = "USER") + void getFriendRequestTest() throws Exception { + // given + String token = jwtProvider.generateAccessTokenForTest(); + + // when + + // then + mockMvc.perform(get("/api/user/friend") + .header("Authorization", "Bearer " + token) + .param("page", "1") + .param("size", "10")) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/team/haedal/gifticionfunding/controller/gifticon/GifticonControllerTest.java b/src/test/java/team/haedal/gifticionfunding/controller/gifticon/GifticonControllerTest.java new file mode 100644 index 0000000..17b2164 --- /dev/null +++ b/src/test/java/team/haedal/gifticionfunding/controller/gifticon/GifticonControllerTest.java @@ -0,0 +1,60 @@ +package team.haedal.gifticionfunding.controller.gifticon; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import team.haedal.gifticionfunding.auth.jwt.JwtProvider; +import team.haedal.gifticionfunding.dto.gifticon.request.GifticonCreateRequest; +import team.haedal.gifticionfunding.entity.gifticon.Category; +import team.haedal.gifticionfunding.entity.gifticon.GifticonCreate; +import team.haedal.gifticionfunding.service.gifticon.GifticonService; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(GifticonController.class) +@AutoConfigureRestDocs(uriHost = "api.test.com", uriPort = 80) +public class GifticonControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private GifticonService gifticonService; + + @Test + @DisplayName("[GET] [/api/gifticons] 기프티콘 조회 테스트") + public void getGifticons() throws Exception { + // given + List gifticonList = new ArrayList<>(); + gifticonList.add(GifticonCreate.builder() + .price(1000L) + .name("테스트") + .category(Category.CULTURE) + .stock(10L) + .imageUrl("test") + .description("test") + .brand("test") + .expirationPeriod(LocalDate.now()) + .build()); + + // when, then + mockMvc.perform(MockMvcRequestBuilders.get("/api/gifticons") + // .with(oauth2Login()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(gifticonList))) + .andExpect(status().isOk()) + .andReturn(); + } +} diff --git a/src/test/java/team/haedal/gifticionfunding/service/friend/friendServiceTest.java b/src/test/java/team/haedal/gifticionfunding/service/friend/friendServiceTest.java new file mode 100644 index 0000000..dd5003c --- /dev/null +++ b/src/test/java/team/haedal/gifticionfunding/service/friend/friendServiceTest.java @@ -0,0 +1,161 @@ +package team.haedal.gifticionfunding.service.friend; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.time.LocalDate; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import team.haedal.gifticionfunding.dto.common.PagingRequest; +import team.haedal.gifticionfunding.dto.common.PagingResponse; +import team.haedal.gifticionfunding.dto.user.request.UserCreate; +import team.haedal.gifticionfunding.dto.user.response.FriendRequestReceiverDto; +import team.haedal.gifticionfunding.entity.user.Friendship; +import team.haedal.gifticionfunding.entity.user.Role; +import team.haedal.gifticionfunding.entity.user.User; +import team.haedal.gifticionfunding.global.error.NotFoundUserException; +import team.haedal.gifticionfunding.repository.user.FriendshipJpaRepository; +import team.haedal.gifticionfunding.repository.user.UserJpaRepository; +import team.haedal.gifticionfunding.service.user.FriendService; + +@DataJpaTest +@DisplayName("FriendService 테스트") +@Import({FriendService.class}) +public class friendServiceTest { + + @Autowired + private FriendService friendService; + + @Autowired + private FriendshipJpaRepository friendshipJpaRepository; + @Autowired + private UserJpaRepository userJpaRepository; + + @AfterEach + void tearDown() { + userJpaRepository.deleteAllInBatch(); + friendshipJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("존재하지 않은 유저에게 친구 추가 요청시 NotFoundUserException 발생") + void requestFriend_NotFoundFriendRequestException() { + // given + /** + * 유저 생성 + */ + Long userId = 1L; + Long friendId = 2L; + + userJpaRepository.save(User.createUser(UserCreate.builder() + .email("test@123"). + nickname("test"). + point(0L). + birthdate(LocalDate.parse("1999-01-01")). + profileImageUrl("test.com"). + role(Role.USER). + build())); + + // when, then + assertThatCode(() -> friendService.requestFriend(userId, friendId)) + .isInstanceOf(NotFoundUserException.class) + .hasMessage("해당 유저가 존재하지 않습니다."); + + } + + @Test + @DisplayName("이미 친구인 상대방에게 친구 추가 요청시 IllegalArgumentException 발생") + void requestFriend_IllegalArgumentException() { + // given + /** + * 유저1, 유저2 생성 + */ + User user1 = userJpaRepository.save(User.createUser(UserCreate.builder() + .email("test@123"). + nickname("test"). + point(0L). + birthdate(LocalDate.parse("1999-01-01")). + profileImageUrl("test.com"). + role(Role.USER). + build())); + + User user2 = userJpaRepository.save(User.createUser(UserCreate.builder() + .email("test2@123"). + nickname("test2"). + point(0L). + birthdate(LocalDate.parse("1999-01-01")). + profileImageUrl("test.com"). + role(Role.USER). + build())); + + /** + * 친구 관계 생성 + */ + Friendship friendship = Friendship.create(user1, user2); + friendshipJpaRepository.save(friendship); + + // when, then + assertThatCode(() -> friendService.requestFriend(user1.getId(), user2.getId())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 친구인 상대방에게는 친구 초대 요청을 보낼 수 없습니다."); + } + + @Test + @DisplayName("친구 추가 요청 보낸 목록 페이징 조회") + void getFriendRequestSenderPaging() { + // given + /** + * 유저1, 유저2, 유저3 생성 + */ + User user1 = userJpaRepository.save(User.createUser(UserCreate.builder() + .email("test@123"). + nickname("test"). + point(0L). + birthdate(LocalDate.parse("1999-01-01")). + profileImageUrl("test.com"). + role(Role.USER). + build())); + + User user2 = userJpaRepository.save(User.createUser(UserCreate.builder() + .email("test2@123"). + nickname("test2"). + point(0L). + birthdate(LocalDate.parse("1999-01-01")). + profileImageUrl("test.com"). + role(Role.USER). + build())); + + User user3 = userJpaRepository.save(User.createUser(UserCreate.builder() + .email("test3@123"). + nickname("test3"). + point(0L). + birthdate(LocalDate.parse("1999-01-01")). + profileImageUrl("test.com"). + role(Role.USER). + build())); + + // 친구 요청 생성 + friendService.requestFriend(user1.getId(), user2.getId()); + friendService.requestFriend(user1.getId(), user3.getId()); + + PageRequest pageRequest = PageRequest.of(0, 10); + PagingRequest pagingRequest = PagingRequest.builder() + .page(0) + .size(10) + .build(); + + // when + PagingResponse friendRequestLists = friendService.getFriendRequestSenderPaging( + pagingRequest, user1.getId()); + + //then + assertThat(friendRequestLists.data().size()).isEqualTo(2); + } + + +} diff --git a/src/test/java/team/haedal/gifticionfunding/service/gifticon/GifticonServiceTest.java b/src/test/java/team/haedal/gifticionfunding/service/gifticon/GifticonServiceTest.java new file mode 100644 index 0000000..bc9db60 --- /dev/null +++ b/src/test/java/team/haedal/gifticionfunding/service/gifticon/GifticonServiceTest.java @@ -0,0 +1,59 @@ +package team.haedal.gifticionfunding.service.gifticon; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import team.haedal.gifticionfunding.entity.gifticon.Category; +import team.haedal.gifticionfunding.entity.gifticon.GifticonUpdate; +import team.haedal.gifticionfunding.global.error.NotFoundGifticonException; + +@DataJpaTest +@DisplayName("GifticonService 테스트") +@Import({GifticonService.class}) +public class GifticonServiceTest { + + @Autowired + private GifticonService gifticonService; + + @Test + @DisplayName("존재하지 않은 id로 Gifticon 조회 시 NotFoundGifticonException 발생") + void findGifticon_NotFoundGifticonException() { + // given + Long gifticonId = 1L; // 존재하지 않는 id + + // when, then + assertThatCode(() -> gifticonService.findGifticon(gifticonId)) + .isInstanceOf(NotFoundGifticonException.class) + .hasMessage("해당 상품이 존재하지 않습니다."); + } + + @Test + @DisplayName("존재하지 않은 id로 Gifticon 수정 시 NotFoundGifticonException 발생") + void updateGifticon_NotFoundGifticonException() { + // given + Long gifticonId = 1L; // 존재하지 않는 id + // gifticonUpdate 객체 생성 + GifticonUpdate gifticonUpdate = GifticonUpdate.builder() + .price(10000L) + .name("테스트 상품") + .category(Category.BEAUTY) + .stock(100L) + .imageUrl("test.com") + .description("테스트 상품입니다.") + .brand("테스트 브랜드") + .expirationPeriod(LocalDate.now().plusDays(30)) + .build(); + + // when, then + assertThatCode(() -> gifticonService.updateGifticon(gifticonId, gifticonUpdate)) + .isInstanceOf(NotFoundGifticonException.class) + .hasMessage("해당 상품이 존재하지 않습니다."); + } + + +}