diff --git a/README.md b/README.md new file mode 100644 index 0000000..f651ce5 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Spring-JPA-study diff --git a/build.gradle b/build.gradle index 15b77ef..c124dac 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.0.1' + id 'org.springframework.boot' version '2.7.6' id 'io.spring.dependency-management' version '1.1.0' } group = 'com.dku' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +sourceCompatibility = '11' configurations { compileOnly { @@ -22,6 +22,20 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE' // redis + + // s3 storage + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.281' + + // swagger + implementation 'io.springfox:springfox-boot-starter:3.0.0' + implementation 'io.springfox:springfox-swagger-ui:3.0.0' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/dku/springstudy/SpringConfig.java b/src/main/java/com/dku/springstudy/SpringConfig.java new file mode 100644 index 0000000..ac99f57 --- /dev/null +++ b/src/main/java/com/dku/springstudy/SpringConfig.java @@ -0,0 +1,12 @@ +package com.dku.springstudy; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import static com.dku.springstudy.SpringConfig.BASE_PACKAGE; + +@Configuration +@EnableJpaRepositories(basePackages = {BASE_PACKAGE}) +public class SpringConfig { + static final String BASE_PACKAGE = "com.dku.springstudy.repository.jpa"; +} diff --git a/src/main/java/com/dku/springstudy/SpringStudyApplication.java b/src/main/java/com/dku/springstudy/SpringStudyApplication.java index ef164c9..5002d38 100644 --- a/src/main/java/com/dku/springstudy/SpringStudyApplication.java +++ b/src/main/java/com/dku/springstudy/SpringStudyApplication.java @@ -2,7 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import springfox.documentation.swagger2.annotations.EnableSwagger2; +@EnableJpaAuditing @SpringBootApplication public class SpringStudyApplication { diff --git a/src/main/java/com/dku/springstudy/config/AwsConfig.java b/src/main/java/com/dku/springstudy/config/AwsConfig.java new file mode 100644 index 0000000..14800e4 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/AwsConfig.java @@ -0,0 +1,31 @@ +package com.dku.springstudy.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsConfig { + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/com/dku/springstudy/config/SecurityConfig.java b/src/main/java/com/dku/springstudy/config/SecurityConfig.java new file mode 100644 index 0000000..e1b706e --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/SecurityConfig.java @@ -0,0 +1,47 @@ +package com.dku.springstudy.config; + +import com.dku.springstudy.security.JwtAuthenticationFilter; +import com.dku.springstudy.security.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.SecurityConfigurer; +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.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .csrf().disable() + .httpBasic().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/member/login", "/member/join", "/member/reissue", "/swagger-ui/**", "/swagger-ui.html", "/api/usage").permitAll() + .antMatchers("/swagger-resources/**", "/swagger-ui.html", "/webjars/**", "/v2/**").permitAll() +// .antMatchers("/item/**").hasRole("USER") + .anyRequest().authenticated() + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) + .build(); + } + + + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + +} diff --git a/src/main/java/com/dku/springstudy/config/SwaggerConfig.java b/src/main/java/com/dku/springstudy/config/SwaggerConfig.java new file mode 100644 index 0000000..32db9b7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/SwaggerConfig.java @@ -0,0 +1,58 @@ +package com.dku.springstudy.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.view.InternalResourceViewResolver; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.HashSet; +import java.util.Set; + +@Configuration +@EnableWebMvc +public class SwaggerConfig { + private ApiInfo swaggerInfo() { + return new ApiInfoBuilder() + .title("Karrot API") + .description("Karrot API Docs") + .build(); + } + + @Bean + public Docket swaggerApi() { + return new Docket(DocumentationType.SWAGGER_2) + .consumes(getConsumeContentTypes()) + .produces(getProduceContentTypes()) + .apiInfo(swaggerInfo()).select() + .apis(RequestHandlerSelectors.basePackage("com.dku.springstudy.controller")) + .paths(PathSelectors.any()) + .build() + .useDefaultResponseMessages(false); + } + + @Bean + public InternalResourceViewResolver defaultViewResolver() { + return new InternalResourceViewResolver(); + } + + private Set getConsumeContentTypes() { + Set consumes = new HashSet<>(); + consumes.add("application/json;charset=UTF-8"); + consumes.add("application/x-www-form-urlencoded"); + return consumes; + } + + private Set getProduceContentTypes() { + Set produces = new HashSet<>(); + produces.add("application/json;charset=UTF-8"); + return produces; + } + +} diff --git a/src/main/java/com/dku/springstudy/config/WebMvcConfig.java b/src/main/java/com/dku/springstudy/config/WebMvcConfig.java new file mode 100644 index 0000000..2bf82bf --- /dev/null +++ b/src/main/java/com/dku/springstudy/config/WebMvcConfig.java @@ -0,0 +1,30 @@ +package com.dku.springstudy.config; + +import com.dku.springstudy.security.ResponseInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final ResponseInterceptor responseInterceptor; + @Override + public void addCorsMappings(CorsRegistry registry) { + registry + .addMapping("/**") + .allowedOrigins("*"); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry + .addInterceptor(responseInterceptor) + .addPathPatterns("/**"); + } + + +} diff --git a/src/main/java/com/dku/springstudy/controller/HomeController.java b/src/main/java/com/dku/springstudy/controller/HomeController.java new file mode 100644 index 0000000..568cefe --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/HomeController.java @@ -0,0 +1,45 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.domain.ImageFile; +import com.dku.springstudy.domain.Item; +import com.dku.springstudy.dto.ItemDto; +import com.dku.springstudy.service.ItemService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequiredArgsConstructor +public class HomeController { + + private final ItemService itemService; + + @ApiOperation(value = "홈", notes = "등록된 전체 상품 리스트 반환") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @GetMapping("/home") + public List home(){ + List allItems = itemService.findAll(); + return allItems.stream() + .map(i -> new ItemDto( + i.getId(), + i.getImages().stream().map(ImageFile::getImageUrl).collect(Collectors.toList()), + i.getTitle(), + i.getContent(), + i.getPrice(), + i.getLikes().size(), + i.getStatus()) + ) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/dku/springstudy/controller/ItemController.java b/src/main/java/com/dku/springstudy/controller/ItemController.java new file mode 100644 index 0000000..be78730 --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/ItemController.java @@ -0,0 +1,154 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.domain.ImageFile; +import com.dku.springstudy.domain.Item; +import com.dku.springstudy.domain.Member; +import com.dku.springstudy.dto.AddItemDto; +import com.dku.springstudy.dto.ItemDetailsDto; +import com.dku.springstudy.dto.ItemDto; +import com.dku.springstudy.enums.Category; +import com.dku.springstudy.repository.jpa.MemberRepository; +import com.dku.springstudy.service.ImageFileService; +import com.dku.springstudy.service.ItemService; +import com.dku.springstudy.service.MemberService; +import com.dku.springstudy.service.S3Upload; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/item") +public class ItemController { + private final ItemService itemService; + private final MemberService memberService; + private final ImageFileService imageFileService; + private final S3Upload s3Upload; + + + @ApiOperation(value = "상품 추가 API", notes = "상품 title, 상품 content, 상품 category, 상품 price, 상품 이미지들을 form-data형식으로 받아서 상품 추가") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @PostMapping("/add") + private AddItemDto addItem(@RequestPart("data") AddItemDto itemDto, @RequestPart("images")MultipartFile[] multipartFiles) throws IOException { + Member currentLoginMember = getCurrentLoginMember(); + log.info("currentLoginMember={}", currentLoginMember); + Item item = Item.createItem(currentLoginMember, itemDto.getTitle(), itemDto.getContent(), itemDto.getPrice(), itemDto.getCategory()); + itemService.addItem(item); + + if(multipartFiles.length != 0){ + for(MultipartFile multipartFile : multipartFiles){ + String imagePath = s3Upload.upload(multipartFile); + ImageFile imageFile = ImageFile.createImageFile(imagePath); + imageFile.updateItem(item); + imageFileService.save(imageFile); + } + } + + return itemDto; + } + + + @ApiOperation(value = "상품 상세 페이지 API", notes = "상품 판매자 닉네임, 상품 카테고리, 상품 등록일(최근 수정날짜), 상품 이미지 링크들, 상품 제목, 상품 내용, 상품 가격을 반환") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + + @GetMapping("/details/{itemId}") + public ItemDetailsDto details(@PathVariable("itemId") Long itemId){ + Item findItem = itemService.findById(itemId); + Member seller = findItem.getMember(); + List result = itemService.findByMember(seller); + + return new ItemDetailsDto( + findItem.getMember().getNickname(), + findItem.getCategory().getCategory(), + findItem.getLastModifiedDate(), + findItem.getImages().stream().map(ImageFile::getImageUrl).collect(Collectors.toList()), + findItem.getTitle(), + findItem.getContent(), + findItem.getPrice(), + result + ); + } + + + @ApiOperation(value = "해당 판매자의 모든 판매중인 상품 조회", notes = "판매자의 모든 판매상품들 조회. PathVariable로 판매자 id 필요") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + + @GetMapping("/details/all/{sellerId}") + public List sellerItems(@PathVariable("sellerId") Long sellerId){ + Member seller = memberService.findById(sellerId); + return itemService.findByMember(seller); + } + + + @ApiOperation(value = "상품 수정 페이지 조회", notes = "상품 수정 페이지 이동. PathVariable로 상품 id 필요. 기존의 상품 정보가 입력되어있어야 하기에 기존의 상품 정보 반환") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @GetMapping("/update/{itemId}") + public ItemDto transferPreviousItemInfo(@PathVariable Long itemId){ + return itemService.transferPreviousItemInfo(itemId); + } + + + @ApiOperation(value = "상품 수정 API", notes = "실제 상품 수정 API. PathVariable로 상품 id, form-data로 수정한 상품 정보들, 상품 이미지들 받음") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @PostMapping("/update/{itemId}") + public void updateItem( @PathVariable("itemId") Long itemId, + @RequestPart("data") AddItemDto updateItemDto, + @RequestPart("images")MultipartFile[] updateMultipartFiles) throws IOException { + + itemService.updateItem( + itemId, + updateItemDto.getTitle(), + updateItemDto.getContent(), + updateItemDto.getCategory(), + updateItemDto.getPrice(), + updateMultipartFiles + ); + } + + + @ApiOperation(value = "상품 삭제 API", notes = "상품 삭제 API. PathVariable로 상품 id 필요") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @PostMapping("/delete/{itemId}") + public void deleteItem(@PathVariable Long itemId){ + itemService.deleteItem(itemId); + } + + private Member getCurrentLoginMember() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String loginEmail = authentication.getName(); + return memberService.getCurrentLoginMember(loginEmail); + } + + +} diff --git a/src/main/java/com/dku/springstudy/controller/ItemLikeController.java b/src/main/java/com/dku/springstudy/controller/ItemLikeController.java new file mode 100644 index 0000000..4e87e63 --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/ItemLikeController.java @@ -0,0 +1,40 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.service.ItemLikeService; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/item-like") +public class ItemLikeController { + + private final ItemLikeService itemLikeService; + + @ApiOperation(value = "상품 좋아요 추가 API", notes = "상품 좋아요 기능") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @PostMapping("/add/{itemId}") + public void addItemLike(@PathVariable Long itemId){ + itemLikeService.addItemLike(itemId); + } + + + @ApiOperation(value = "상품 좋아요 삭제 API", notes = "상품 좋아요 취소 기능") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @PostMapping("/delete/{itemId}") + public void deleteItemLike(@PathVariable Long itemId){ + itemLikeService.deleteItemLike(itemId); + } +} diff --git a/src/main/java/com/dku/springstudy/controller/MemberController.java b/src/main/java/com/dku/springstudy/controller/MemberController.java new file mode 100644 index 0000000..f3484ef --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/MemberController.java @@ -0,0 +1,137 @@ +package com.dku.springstudy.controller; + +import com.dku.springstudy.domain.ImageFile; +import com.dku.springstudy.domain.Member; +import com.dku.springstudy.dto.*; +import com.dku.springstudy.enums.Role; +import com.dku.springstudy.repository.jpa.MemberRepository; +import com.dku.springstudy.security.AuthenticationProvider; +import com.dku.springstudy.service.ItemService; +import com.dku.springstudy.service.MemberService; +import com.dku.springstudy.service.S3Upload; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/member") +public class MemberController { + private final MemberService memberService; + private final ItemService itemService; + private final S3Upload s3Upload; + + @ApiOperation(value = "회원가입", notes = "회원가입한 회원 리턴") + @ApiResponse(code = 200, message = "API 정상 작동") + @PostMapping("/join") + public JoinDto join(@RequestBody JoinDto joinDto){ + log.info("Join Request={}", joinDto); + memberService.join( + joinDto.getEmail(), + joinDto.getPassword(), + joinDto.getName(), + joinDto.getPhone(), + joinDto.getNickname(), + Role.USER); + return joinDto; + } + + + @ApiOperation(value = "로그인", notes = "회원 로그인 후, Token 정보 리턴") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동"), + @ApiResponse(code = 401, message = "토큰 정보 오류"), + @ApiResponse(code = 404, message = "토큰 만료 오류") + } + ) + @PostMapping("/login") + public TokenDto login(@RequestBody LoginDto loginDto){ + log.info("login request={}", loginDto); + return memberService.login(loginDto.getEmail(), loginDto.getPassword()); + } + + @ApiOperation(value = "토큰 재발행", notes = "accessToken 유효기간 만료되었는데 refreshToken은 아직 유효기간 만료되지 않았으면 토큰 재발행") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동"), + @ApiResponse(code = 401, message = "토큰 정보 오류"), + @ApiResponse(code = 404, message = "토큰 만료 오류, 회원 존재하지 않음") + } + ) + @PostMapping("/reissue") + public TokenDto reissue(@RequestBody TokenDto tokenDto){ + log.info("Reissue={}", tokenDto); + return memberService.reissue(tokenDto); + } + + + @ApiOperation(value = "마이페이지", notes = "마이페이지 홈에서는 프로필 이미지 링크와 회원 닉네임 반환") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @GetMapping("/mypage") + public MyPageDto myPageHome(){ + return memberService.myPageHome(); + } + + @ApiOperation(value = "마이페이지의 프로필 수정", notes = "마이페이지 내의 프로필 수정 기능. 프로필 사진과 닉네임 변경 가능") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @PostMapping("/mypage/update-profiles") + public void updateProfiles(@RequestPart("data") UpdateProfilesDto updateProfilesDto, + @RequestPart("images") MultipartFile multipartFile) throws IOException { + + + String uploadImageUri = s3Upload.upload(multipartFile); + ImageFile profileImageFile = ImageFile.createImageFile(uploadImageUri); + + memberService.updateProfiles(updateProfilesDto.getNickname(), profileImageFile); + } + + @ApiOperation(value = "내 판매내역 조회", notes = "마이페이지에서 나의 판매내역 조회") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @GetMapping("/mypage/sales-details") + public List salesDetails(){ + Member currentMember = AuthenticationProvider.getCurrentMember(); + return itemService.findByMember(currentMember); + } + + @ApiOperation(value = "나의 판매 상품 중에서 판매상태 변경", notes = "판맵중, 예약중, 거래완료로 상태변경 가능") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + @PostMapping("/mypage/update-sales-status") + public void updateSalesStatus(@RequestBody UpdateItemStatusDto updateItemStatusDto){ + itemService.updateItemStatus(updateItemStatusDto.getId(), updateItemStatusDto.getItemStatus()); + } + + + @ApiOperation(value = "좋아요 누른 상품 리스트 확인", notes = "좋아요 누른 상품 리스트 리턴") + @ApiResponses({ + @ApiResponse(code = 200, message = "API 정상 작동") + } + ) + + @GetMapping("/mypage/favorite") + public List favoriteItemList(){ + return itemService.findFavoriteItems(); + } + + + + +} diff --git a/src/main/java/com/dku/springstudy/controller/SwaggerController.java b/src/main/java/com/dku/springstudy/controller/SwaggerController.java new file mode 100644 index 0000000..d3d6455 --- /dev/null +++ b/src/main/java/com/dku/springstudy/controller/SwaggerController.java @@ -0,0 +1,14 @@ +package com.dku.springstudy.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class SwaggerController { + + @GetMapping("/api/usage") + public String swaggerUi(){ + return "redirect:/swagger-ui/index.html"; + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/BaseEntity.java b/src/main/java/com/dku/springstudy/domain/BaseEntity.java new file mode 100644 index 0000000..27af012 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/BaseEntity.java @@ -0,0 +1,27 @@ +package com.dku.springstudy.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +@Getter +public class BaseEntity { + + @CreatedDate + @Column(name = "created_date", nullable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + @Column(name = "last_modified_date", nullable = false) + private LocalDateTime lastModifiedDate; +} diff --git a/src/main/java/com/dku/springstudy/domain/ImageFile.java b/src/main/java/com/dku/springstudy/domain/ImageFile.java new file mode 100644 index 0000000..967078a --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/ImageFile.java @@ -0,0 +1,33 @@ +package com.dku.springstudy.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ImageFile extends BaseEntity{ + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String imageUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + + public static ImageFile createImageFile(String imageUrl){ + ImageFile imageFile = new ImageFile(); + imageFile.imageUrl = imageUrl; + return imageFile; + } + + public void updateItem(Item item){ + this.item = item; + item.getImages().add(this); + } +} diff --git a/src/main/java/com/dku/springstudy/domain/Item.java b/src/main/java/com/dku/springstudy/domain/Item.java new file mode 100644 index 0000000..4c9c9d8 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/Item.java @@ -0,0 +1,69 @@ +package com.dku.springstudy.domain; + +import com.dku.springstudy.enums.Category; +import com.dku.springstudy.enums.ItemStatus; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Item extends BaseEntity{ + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "item_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + private String title; + private String content; + private int price; + + @OneToMany(mappedBy = "item", cascade = CascadeType.REMOVE) + private List images = new ArrayList<>(); + + @Enumerated(value = EnumType.STRING) + private ItemStatus status; + + @Enumerated(EnumType.STRING) + private Category category; + + @OneToMany(mappedBy = "item", cascade = CascadeType.REMOVE) + private List likes = new ArrayList<>(); + + + public static Item createItem(Member member, String title, String content, int price, Category category){ + Item item = new Item(); + item.member = member; + item.title = title; + item.content = content; + item.price = price; + item.category = category; + item.status = ItemStatus.SELLING; // 상품 처음 등록시에는 SELLING(판매중) 상태 + + return item; + } + + public void updateItem(String title, String content, Category category, int price, List updateMultipartFiles){ + this.title = title; + this.content = content; + this.category = category; + this.price = price; + this.images.clear(); + this.images.addAll(updateMultipartFiles); + } + + public void changeStatus(ItemStatus itemStatus){ + this.status = itemStatus; + } + +} diff --git a/src/main/java/com/dku/springstudy/domain/ItemLike.java b/src/main/java/com/dku/springstudy/domain/ItemLike.java new file mode 100644 index 0000000..f9d0baf --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/ItemLike.java @@ -0,0 +1,33 @@ +package com.dku.springstudy.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "ITEM_LIKE") +public class ItemLike extends BaseEntity{ + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + public static ItemLike createItemLike(Item item, Member member){ + ItemLike itemLike = new ItemLike(); + itemLike.item = item; + item.getLikes().add(itemLike); + itemLike.member = member; + return itemLike; + } +} diff --git a/src/main/java/com/dku/springstudy/domain/Member.java b/src/main/java/com/dku/springstudy/domain/Member.java new file mode 100644 index 0000000..7b48d58 --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/Member.java @@ -0,0 +1,85 @@ +package com.dku.springstudy.domain; + +import com.dku.springstudy.enums.Role; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member extends BaseEntity implements UserDetails{ + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long id; + + private String email; + private String password; + private String name; + private String phone; + private String nickname; + + @OneToOne(fetch = FetchType.LAZY) + private ImageFile profileImage; + + // 권한 정보(Role) + @Enumerated(EnumType.STRING) + private Role role; + + public static Member createMember(String email, String password, String name, String phone, String nickname, Role role){ + Member member = new Member(); + member.email = email; + member.password = password; + member.name = name; + member.phone = phone; + member.nickname = nickname; + member.role = role; + return member; + } + + public void updateMemberProfiles(String nickname, ImageFile profileImage){ + this.nickname = nickname; + } + + @Override + public Collection getAuthorities() { + String s = this.getRole().toString(); + SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(s); + return List.of(simpleGrantedAuthority); + } + + @Override + public String getUsername() { + return this.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/dku/springstudy/domain/token/RefreshToken.java b/src/main/java/com/dku/springstudy/domain/token/RefreshToken.java new file mode 100644 index 0000000..42681cd --- /dev/null +++ b/src/main/java/com/dku/springstudy/domain/token/RefreshToken.java @@ -0,0 +1,22 @@ +package com.dku.springstudy.domain.token; + +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@RedisHash(value = "refreshToken", timeToLive = 3600) +@Getter +public class RefreshToken { + + @Id + private String memberEmail; + private String refreshToken; + + public RefreshToken(String memberEmail, String refreshToken){ + this.memberEmail = memberEmail; + this.refreshToken = refreshToken; + } + + + +} diff --git a/src/main/java/com/dku/springstudy/dto/AddItemDto.java b/src/main/java/com/dku/springstudy/dto/AddItemDto.java new file mode 100644 index 0000000..ec78c2b --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/AddItemDto.java @@ -0,0 +1,25 @@ +package com.dku.springstudy.dto; + +import com.dku.springstudy.domain.ImageFile; +import com.dku.springstudy.enums.Category; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AddItemDto { + + @ApiModelProperty(value = "상품 제목", dataType = "string") + private String title; + @ApiModelProperty(value = "상품 카테고리", dataType = "string") + private Category category; + @ApiModelProperty(value = "상품 가격", dataType = "int") + private int price; + @ApiModelProperty(value = "상품 내용", dataType = "string") + private String content; +} diff --git a/src/main/java/com/dku/springstudy/dto/ItemDetailsDto.java b/src/main/java/com/dku/springstudy/dto/ItemDetailsDto.java new file mode 100644 index 0000000..8fc4141 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/ItemDetailsDto.java @@ -0,0 +1,37 @@ +package com.dku.springstudy.dto; + +import com.dku.springstudy.domain.Item; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@AllArgsConstructor +public class ItemDetailsDto { + @ApiModelProperty(value = "판매자 닉네임", dataType = "string") + private String sellerNickname; + + @ApiModelProperty(value = "상품 카테고리", dataType = "string") + private String category; + + @ApiModelProperty(value = "상품 등록일(최근 수정일)", dataType = "LocalDateTime") + private LocalDateTime lastModifiedDate; + + @ApiModelProperty(value = "상품 이미지 경로들", dataType = "List") + private List imagePath; + + @ApiModelProperty(value = "상품 제목", dataType = "string") + private String title; + + @ApiModelProperty(value = "상품 내용", dataType = "string") + private String content; + + @ApiModelProperty(value = "상품 가격", dataType = "int") + private int price; + + @ApiModelProperty(value = "판매자의 모든 상품 내역", dataType = "List") + private List sellerItems; +} diff --git a/src/main/java/com/dku/springstudy/dto/ItemDto.java b/src/main/java/com/dku/springstudy/dto/ItemDto.java new file mode 100644 index 0000000..1c28fc9 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/ItemDto.java @@ -0,0 +1,35 @@ +package com.dku.springstudy.dto; + +import com.dku.springstudy.enums.ItemStatus; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ItemDto { + @ApiModelProperty(value = "상품 id", dataType = "Long") + private Long id; + + @ApiModelProperty(value = "상품 이미지 경로들", dataType = "List") + private List imagePath; + + @ApiModelProperty(value = "상품 제목", dataType = "string") + private String title; + + @ApiModelProperty(value = "상품 내용", dataType = "string") + private String content; + + @ApiModelProperty(value = "상품 가격", dataType = "price") + private int price; + + @ApiModelProperty(value = "상품 좋아요 개수", dataType = "int") + private int likeCount; + + @ApiModelProperty(value = "상품 상태", dataType = "string") + private ItemStatus itemStatus; +} diff --git a/src/main/java/com/dku/springstudy/dto/JoinDto.java b/src/main/java/com/dku/springstudy/dto/JoinDto.java new file mode 100644 index 0000000..a748c50 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/JoinDto.java @@ -0,0 +1,22 @@ +package com.dku.springstudy.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class JoinDto { + @ApiModelProperty(value = "이메일", dataType = "string") + private String email; + @ApiModelProperty(value = "비밀번호", dataType = "string") + private String password; + @ApiModelProperty(value = "이름", dataType = "string") + private String name; + @ApiModelProperty(value = "전화번호", dataType = "string") + private String phone; + @ApiModelProperty(value = "닉네임", dataType = "string") + private String nickname; + +} diff --git a/src/main/java/com/dku/springstudy/dto/LoginDto.java b/src/main/java/com/dku/springstudy/dto/LoginDto.java new file mode 100644 index 0000000..10c4b50 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/LoginDto.java @@ -0,0 +1,19 @@ +package com.dku.springstudy.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class LoginDto { + @ApiModelProperty(value = "이메일", dataType = "string") + private String email; + @ApiModelProperty(value = "비밀번호", dataType = "string") + private String password; + + + +} diff --git a/src/main/java/com/dku/springstudy/dto/MyPageDto.java b/src/main/java/com/dku/springstudy/dto/MyPageDto.java new file mode 100644 index 0000000..e3092f8 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/MyPageDto.java @@ -0,0 +1,15 @@ +package com.dku.springstudy.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MyPageDto { + @ApiModelProperty(value = "프로필 이미지 경로", dataType = "string") + private String profileImagePath; + @ApiModelProperty(value = "닉네임", dataType = "string") + private String nickname; + +} diff --git a/src/main/java/com/dku/springstudy/dto/ResponseEntity.java b/src/main/java/com/dku/springstudy/dto/ResponseEntity.java new file mode 100644 index 0000000..a387463 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/ResponseEntity.java @@ -0,0 +1,20 @@ +package com.dku.springstudy.dto; + +import lombok.Data; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +@Data +public class ResponseEntity extends HttpEntity { + private HttpStatus status; + private T body; + + public ResponseEntity(@Nullable T body, HttpStatus status) { +// super(body, headers); + this.body = body; + this.status = status; + } +} diff --git a/src/main/java/com/dku/springstudy/dto/TokenDto.java b/src/main/java/com/dku/springstudy/dto/TokenDto.java new file mode 100644 index 0000000..57ac467 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/TokenDto.java @@ -0,0 +1,25 @@ +package com.dku.springstudy.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +@AllArgsConstructor +public class TokenDto { + + @ApiModelProperty(value = "토큰 타입(Bearer사용)", dataType = "string") + private String grantType; // Bearer + + @ApiModelProperty(value = "Access Token", dataType = "string") + private String accessToken; + + @ApiModelProperty(value = "Refresh Token", dataType = "string") + private String refreshToken; + + public TokenDto(){ + } + +} diff --git a/src/main/java/com/dku/springstudy/dto/UpdateItemStatusDto.java b/src/main/java/com/dku/springstudy/dto/UpdateItemStatusDto.java new file mode 100644 index 0000000..d830d2f --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/UpdateItemStatusDto.java @@ -0,0 +1,13 @@ +package com.dku.springstudy.dto; + +import com.dku.springstudy.enums.ItemStatus; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; + +@Getter +public class UpdateItemStatusDto { + @ApiModelProperty(value = "수정할 상품 id", dataType = "Long") + private Long id; + @ApiModelProperty(value = "수정할 상품 상태", dataType = "string") + private ItemStatus itemStatus; +} diff --git a/src/main/java/com/dku/springstudy/dto/UpdateProfilesDto.java b/src/main/java/com/dku/springstudy/dto/UpdateProfilesDto.java new file mode 100644 index 0000000..a3b7130 --- /dev/null +++ b/src/main/java/com/dku/springstudy/dto/UpdateProfilesDto.java @@ -0,0 +1,12 @@ +package com.dku.springstudy.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UpdateProfilesDto { + @ApiModelProperty(value = "수정할 회원 닉네임", dataType = "string") + private String nickname; +} diff --git a/src/main/java/com/dku/springstudy/enums/Category.java b/src/main/java/com/dku/springstudy/enums/Category.java new file mode 100644 index 0000000..acb67af --- /dev/null +++ b/src/main/java/com/dku/springstudy/enums/Category.java @@ -0,0 +1,17 @@ +package com.dku.springstudy.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Category { + + DIGITAL("DIGITAL"), ELECTRONIC("ELECTRONIC"), FURNITURE_INTERIOR("FURNITURE_INTERIOR"), KIDS("KIDS"), LIFE_FOOD("LIFE_FOOD"), BOOK_KIDS("BOOK_KIDS"), SPORTS("SPORTS"), + WOMAN_MERCHANDISE("WOMAN_MERCHANDISE"), WOMAN_CLOTHES("WOMAN_CLOTHES"), MAN_MERCHANDISE_CLOTHES("MAN_MERCHANDISE_CLOTHES"), + GAME_HOBBY("GAME_HOBBY"), BEAUTY_HAIR("BEAUTY_HAIR"), PET("PET"), BOOK_TICKET_MUSIC("BOOK_TICKET_MUSIC"), PLANTS("PLANTS"), USED_THINGS("USED_THINGS"), USED_CAR("USED_CAR"); + + @JsonValue + private final String category; +} diff --git a/src/main/java/com/dku/springstudy/enums/ItemStatus.java b/src/main/java/com/dku/springstudy/enums/ItemStatus.java new file mode 100644 index 0000000..855ae44 --- /dev/null +++ b/src/main/java/com/dku/springstudy/enums/ItemStatus.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.enums; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ItemStatus { + SELLING, RESERVATION, COMPLETE +} diff --git a/src/main/java/com/dku/springstudy/enums/Role.java b/src/main/java/com/dku/springstudy/enums/Role.java new file mode 100644 index 0000000..2c7cbd8 --- /dev/null +++ b/src/main/java/com/dku/springstudy/enums/Role.java @@ -0,0 +1,10 @@ +package com.dku.springstudy.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Role { + USER, ADMIN +} diff --git a/src/main/java/com/dku/springstudy/exception/ControllerAdvisor.java b/src/main/java/com/dku/springstudy/exception/ControllerAdvisor.java new file mode 100644 index 0000000..64dd955 --- /dev/null +++ b/src/main/java/com/dku/springstudy/exception/ControllerAdvisor.java @@ -0,0 +1,17 @@ +package com.dku.springstudy.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +@Slf4j + +public class ControllerAdvisor { + @ExceptionHandler(value = KarrotException.class) + protected ResponseEntity exceptionHandler(KarrotException e){ + log.error("KarrotException : ", e); + return ResponseEntity.status(e.getHttpStatus()).body(new KarrotException(e.getHttpStatus(), e.getCode(), e.getMessage())); + } +} diff --git a/src/main/java/com/dku/springstudy/exception/KarrotException.java b/src/main/java/com/dku/springstudy/exception/KarrotException.java new file mode 100644 index 0000000..96272d2 --- /dev/null +++ b/src/main/java/com/dku/springstudy/exception/KarrotException.java @@ -0,0 +1,19 @@ +package com.dku.springstudy.exception; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Data +public class KarrotException extends RuntimeException{ + private final HttpStatus httpStatus; + private final int code; + private final String message; + + public KarrotException(HttpStatus httpStatus, int code, String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/com/dku/springstudy/repository/jpa/ImageFileRepository.java b/src/main/java/com/dku/springstudy/repository/jpa/ImageFileRepository.java new file mode 100644 index 0000000..2fe06cb --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/jpa/ImageFileRepository.java @@ -0,0 +1,7 @@ +package com.dku.springstudy.repository.jpa; + +import com.dku.springstudy.domain.ImageFile; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ImageFileRepository extends JpaRepository { +} diff --git a/src/main/java/com/dku/springstudy/repository/jpa/ItemLikeRepository.java b/src/main/java/com/dku/springstudy/repository/jpa/ItemLikeRepository.java new file mode 100644 index 0000000..222587b --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/jpa/ItemLikeRepository.java @@ -0,0 +1,18 @@ +package com.dku.springstudy.repository.jpa; + + +import com.dku.springstudy.domain.Item; +import com.dku.springstudy.domain.ItemLike; +import com.dku.springstudy.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ItemLikeRepository extends JpaRepository { + List findByItem(Item item); + + List findByMember(Member member); + + Optional findByMemberAndItem(Member member, Item item); +} diff --git a/src/main/java/com/dku/springstudy/repository/jpa/ItemRepository.java b/src/main/java/com/dku/springstudy/repository/jpa/ItemRepository.java new file mode 100644 index 0000000..f027e61 --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/jpa/ItemRepository.java @@ -0,0 +1,11 @@ +package com.dku.springstudy.repository.jpa; + +import com.dku.springstudy.domain.Item; +import com.dku.springstudy.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ItemRepository extends JpaRepository { + List findByMember(Member member); +} diff --git a/src/main/java/com/dku/springstudy/repository/jpa/MemberRepository.java b/src/main/java/com/dku/springstudy/repository/jpa/MemberRepository.java new file mode 100644 index 0000000..a0c381b --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/jpa/MemberRepository.java @@ -0,0 +1,13 @@ +package com.dku.springstudy.repository.jpa; + +import com.dku.springstudy.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByName(String name); + + Optional findByEmail(String email); + +} diff --git a/src/main/java/com/dku/springstudy/repository/redis/RefreshTokenRepository.java b/src/main/java/com/dku/springstudy/repository/redis/RefreshTokenRepository.java new file mode 100644 index 0000000..5ef4915 --- /dev/null +++ b/src/main/java/com/dku/springstudy/repository/redis/RefreshTokenRepository.java @@ -0,0 +1,7 @@ +package com.dku.springstudy.repository.redis; + +import com.dku.springstudy.domain.token.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { +} diff --git a/src/main/java/com/dku/springstudy/security/AuthenticationProvider.java b/src/main/java/com/dku/springstudy/security/AuthenticationProvider.java new file mode 100644 index 0000000..ad522ca --- /dev/null +++ b/src/main/java/com/dku/springstudy/security/AuthenticationProvider.java @@ -0,0 +1,17 @@ +package com.dku.springstudy.security; + +import com.dku.springstudy.domain.Member; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class AuthenticationProvider { + public static Member getCurrentMember() { + UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return userDetails.getMember(); + } + + public static Long getCurrentMemberId() { + return getCurrentMember().getId(); + } +} diff --git a/src/main/java/com/dku/springstudy/security/CustomUserDetailsService.java b/src/main/java/com/dku/springstudy/security/CustomUserDetailsService.java new file mode 100644 index 0000000..0a0e705 --- /dev/null +++ b/src/main/java/com/dku/springstudy/security/CustomUserDetailsService.java @@ -0,0 +1,26 @@ +package com.dku.springstudy.security; + +import com.dku.springstudy.domain.Member; +import com.dku.springstudy.repository.jpa.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + + @Override + public UserDetailsImpl loadUserByUsername(String username) throws UsernameNotFoundException { + return memberRepository.findByEmail(username) // username = email + .map(UserDetailsImpl::new) + .orElseThrow(() -> new UsernameNotFoundException("해당 회원이 존재하지 않습니다.")); + } +} diff --git a/src/main/java/com/dku/springstudy/security/JwtAuthenticationFilter.java b/src/main/java/com/dku/springstudy/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4e3892e --- /dev/null +++ b/src/main/java/com/dku/springstudy/security/JwtAuthenticationFilter.java @@ -0,0 +1,76 @@ +package com.dku.springstudy.security; + +import com.dku.springstudy.exception.KarrotException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + + try{ + String token = getTokenFromHeader(requestWrapper); + String path = requestWrapper.getServletPath(); + if(path.startsWith("/member/reissue")){ + filterChain.doFilter(requestWrapper,responseWrapper); + } else { + if(token != null && jwtTokenProvider.validateToken(token)){ + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(requestWrapper, responseWrapper); + responseWrapper.copyBodyToResponse(); + } + } catch (ExpiredJwtException e){ + log.info("Expired JWT token", e); + throw new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "토큰 유효기간 만료"); + } catch (KarrotException e){ + response.setStatus(e.getHttpStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + ResponseEntity exception = ResponseEntity.status(e.getHttpStatus()).body(new KarrotException(e.getHttpStatus(), e.getCode(), e.getMessage())); + ObjectMapper objectMapper = new ObjectMapper(); + String exceptionMessage = objectMapper.writeValueAsString(exception); + response.getWriter().write(exceptionMessage); + } + } + + private String getTokenFromHeader(ContentCachingRequestWrapper request) { +// HttpServletRequest req = request; + String bearerToken = request.getHeader("Authorization"); + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")){ + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/dku/springstudy/security/JwtTokenProvider.java b/src/main/java/com/dku/springstudy/security/JwtTokenProvider.java new file mode 100644 index 0000000..74f6fe7 --- /dev/null +++ b/src/main/java/com/dku/springstudy/security/JwtTokenProvider.java @@ -0,0 +1,130 @@ +package com.dku.springstudy.security; + +import com.dku.springstudy.domain.token.RefreshToken; +import com.dku.springstudy.dto.TokenDto; +import com.dku.springstudy.exception.KarrotException; +import com.dku.springstudy.repository.jpa.MemberRepository; +import com.dku.springstudy.repository.redis.RefreshTokenRepository; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class JwtTokenProvider { + private final Key key; + + private final RefreshTokenRepository refreshTokenRepository; + private final MemberRepository memberRepository; + + @Autowired + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, RefreshTokenRepository refreshTokenRepository, MemberRepository memberRepository){ + byte[] bytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(bytes); + this.refreshTokenRepository = refreshTokenRepository; + this.memberRepository = memberRepository; + } + + // authentication 객체를 기반으로 access token, refresh token 생성 + public TokenDto generateToken(Authentication authentication){ + // 권한 정보 + String authorities = authentication.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = new Date().getTime(); + + // access token 생성 + Date accessTokenExpDate = new Date(now + 3600000); // 발급시간으로부터 1h + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .claim("auth", authorities) + .setExpiration(accessTokenExpDate) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + // refresh token 생성 + Date refreshTokenExpDate = new Date(now + 86400000); + String refreshToken = Jwts.builder() + .setExpiration(refreshTokenExpDate) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + // refresh token 저장 + RefreshToken refToken = new RefreshToken(authentication.getName(), refreshToken); + refreshTokenRepository.save(refToken); + + + return TokenDto.builder() + .grantType("Bearer") + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + // token을 복호화하여 토큰 정보 꺼내는 메서드 + // client로부터 받은 token 정보를 통해 Authentication 객체 생성 + public Authentication getAuthentication(String accessToken){ + Claims claims = getClaims(accessToken); + + if(claims.get("auth") == null){ + throw new RuntimeException("권한이 없는 토큰입니다."); + } + + // claims에서 권한정보 가져오기 + List authorities = Arrays.stream(claims.get("auth").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + // UserDetails 객체 만들어서 Authentication return + UserDetailsImpl principal = memberRepository.findByEmail(claims.getSubject()) + .map(UserDetailsImpl::new) + .orElseThrow(() -> new UsernameNotFoundException("Can't find User")); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + // 토큰 유효성 검증 + public boolean validateToken(String token){ + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + throw new KarrotException(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(),"토큰 인증 오류 발생"); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + throw new KarrotException(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(),"토큰 인증 오류 발생"); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + throw new KarrotException(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(),"토큰 인증 오류 발생"); + } + } + + private Claims getClaims(String accessToken) { + try{ + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch(ExpiredJwtException e){ + return e.getClaims(); + } + } + + +} diff --git a/src/main/java/com/dku/springstudy/security/ResponseInterceptor.java b/src/main/java/com/dku/springstudy/security/ResponseInterceptor.java new file mode 100644 index 0000000..fc9fa72 --- /dev/null +++ b/src/main/java/com/dku/springstudy/security/ResponseInterceptor.java @@ -0,0 +1,43 @@ +package com.dku.springstudy.security; + +import com.dku.springstudy.dto.ResponseEntity; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ResponseInterceptor implements HandlerInterceptor { + private final ObjectMapper objectMapper; + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + ContentCachingResponseWrapper res = (ContentCachingResponseWrapper) response; + + if(!request.getRequestURI().contains("swagger")){ + if(!request.getRequestURI().contains("v2")){ + String contentString = new String(res.getContentAsByteArray()); + Object readValue = objectMapper.readValue(contentString, Object.class); + + ResponseEntity objectResponseEntity = new ResponseEntity<>(readValue, HttpStatus.OK); + String wrappedBody = objectMapper.writeValueAsString(objectResponseEntity); + res.resetBuffer(); + res.getOutputStream().write(wrappedBody.getBytes(), 0, wrappedBody.getBytes().length); + } + } + + res.copyBodyToResponse(); + + } +} diff --git a/src/main/java/com/dku/springstudy/security/UserDetailsImpl.java b/src/main/java/com/dku/springstudy/security/UserDetailsImpl.java new file mode 100644 index 0000000..826c5fe --- /dev/null +++ b/src/main/java/com/dku/springstudy/security/UserDetailsImpl.java @@ -0,0 +1,16 @@ +package com.dku.springstudy.security; + +import com.dku.springstudy.domain.Member; +import lombok.Getter; +import org.springframework.security.core.userdetails.User; + +@Getter +public class UserDetailsImpl extends User { + + private final Member member; + + public UserDetailsImpl(Member member) { + super(member.getEmail(), member.getPassword(), member.getAuthorities()); + this.member = member; + } +} diff --git a/src/main/java/com/dku/springstudy/service/ImageFileService.java b/src/main/java/com/dku/springstudy/service/ImageFileService.java new file mode 100644 index 0000000..51f4b41 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/ImageFileService.java @@ -0,0 +1,29 @@ +package com.dku.springstudy.service; + +import com.dku.springstudy.domain.ImageFile; +import com.dku.springstudy.repository.jpa.ImageFileRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ImageFileService { + + private final ImageFileRepository imageFileRepository; + + @Transactional + public void save(ImageFile imageFile){ + imageFileRepository.save(imageFile); + } + + @Transactional + public void deleteAllImages(List imageFiles){ + for (ImageFile imageFile : imageFiles) { + imageFileRepository.delete(imageFile); + } + } +} diff --git a/src/main/java/com/dku/springstudy/service/ItemLikeService.java b/src/main/java/com/dku/springstudy/service/ItemLikeService.java new file mode 100644 index 0000000..aa2ca87 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/ItemLikeService.java @@ -0,0 +1,51 @@ +package com.dku.springstudy.service; + +import com.dku.springstudy.domain.Item; +import com.dku.springstudy.domain.ItemLike; +import com.dku.springstudy.domain.Member; +import com.dku.springstudy.exception.KarrotException; +import com.dku.springstudy.repository.jpa.ItemLikeRepository; +import com.dku.springstudy.repository.jpa.ItemRepository; +import com.dku.springstudy.security.AuthenticationProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ItemLikeService { + + private final ItemLikeRepository itemLikeRepository; + private final ItemRepository itemRepository; + + public int getLikeCountByItemId(Item item){ + List itemLikes = itemLikeRepository.findByItem(item); + return itemLikes.size(); + } + + @Transactional + public void addItemLike(Long itemId) { + Member currentMember = AuthenticationProvider.getCurrentMember(); + Item findItem = itemRepository.findById(itemId) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "해당 상품이 존재하지 않습니다.")); + + ItemLike itemLike = ItemLike.createItemLike(findItem, currentMember); + itemLikeRepository.save(itemLike); + } + + @Transactional + public void deleteItemLike(Long itemId) { + Member currentMember = AuthenticationProvider.getCurrentMember(); + Item findItem = itemRepository.findById(itemId) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "해당 상품이 존재하지 않습니다.")); + + ItemLike deleteItemLike = itemLikeRepository.findByMemberAndItem(currentMember, findItem) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "해당 좋아요 정보가 없습니다.")); + + itemLikeRepository.delete(deleteItemLike); + + } +} diff --git a/src/main/java/com/dku/springstudy/service/ItemService.java b/src/main/java/com/dku/springstudy/service/ItemService.java new file mode 100644 index 0000000..f27ce26 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/ItemService.java @@ -0,0 +1,138 @@ +package com.dku.springstudy.service; + +import com.dku.springstudy.domain.ImageFile; +import com.dku.springstudy.domain.Item; +import com.dku.springstudy.domain.ItemLike; +import com.dku.springstudy.domain.Member; +import com.dku.springstudy.dto.ItemDetailsDto; +import com.dku.springstudy.dto.ItemDto; +import com.dku.springstudy.enums.Category; +import com.dku.springstudy.enums.ItemStatus; +import com.dku.springstudy.exception.KarrotException; +import com.dku.springstudy.repository.jpa.ItemLikeRepository; +import com.dku.springstudy.repository.jpa.ItemRepository; +import com.dku.springstudy.security.AuthenticationProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ItemService { + private final ItemRepository itemRepository; + private final ImageFileService imageFileService; + private final ItemLikeRepository itemLikeRepository; + private final S3Upload s3Upload; + + @Transactional + public Long addItem(Item item){ + itemRepository.save(item); + return item.getId(); + } + + public List findAll() { + return itemRepository.findAll(); + } + + public Item findById(Long id) { + return itemRepository.findById(id) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "해당 상품이 존재하지 않습니다.")); + } + + public List findByMember(Member member) { + List sellerItems = itemRepository.findByMember(member); + return sellerItems.stream() + .map(i -> new ItemDto( + i.getId(), + i.getImages().stream().map(ImageFile::getImageUrl).collect(Collectors.toList()), + i.getTitle(), + i.getContent(), + i.getPrice(), + i.getLikes().size(), + i.getStatus()) + ) + .collect(Collectors.toList()); + } + + @Transactional + public void updateItemStatus(Long id, ItemStatus itemStatus) { + Item item = itemRepository.findById(id) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "존재하지 않는 상품입니다.")); + item.changeStatus(itemStatus); + } + + + public ItemDto transferPreviousItemInfo(Long itemId) { + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "존재하지 않는 상품입니다.")); + return new ItemDto( + item.getId(), + item.getImages().stream().map(ImageFile::getImageUrl).collect(Collectors.toList()), + item.getTitle(), + item.getContent(), + item.getPrice(), + item.getLikes().size(), + item.getStatus()); + } + + @Transactional + public void updateItem(Long itemId, String title, String content, Category category, int price, MultipartFile[] updateMultipartFiles) throws IOException { + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "존재하지 않는 상품입니다.")); + + imageFileService.deleteAllImages(item.getImages()); + + List imageFiles = new ArrayList<>(); + + if(updateMultipartFiles.length != 0){ + for(MultipartFile multipartFile : updateMultipartFiles){ + String imagePath = s3Upload.upload(multipartFile); + ImageFile imageFile = ImageFile.createImageFile(imagePath); + imageFiles.add(imageFile); + imageFile.updateItem(item); + imageFileService.save(imageFile); + } + } + + item.updateItem(title, content, category, price, imageFiles); + + + } + + @Transactional + public void deleteItem(Long itemId) { + itemRepository.deleteById(itemId); + } + + public List findFavoriteItems() { + Member currentMember = AuthenticationProvider.getCurrentMember(); + + List itemLikesByMember = itemLikeRepository.findByMember(currentMember); + + List result = itemLikesByMember.stream() + .map(ItemLike::getItem) + .collect(Collectors.toList()); + + return result.stream() + .map(i -> new ItemDto( + i.getId(), + i.getImages().stream().map(ImageFile::getImageUrl).collect(Collectors.toList()), + i.getTitle(), + i.getContent(), + i.getPrice(), + i.getLikes().size(), + i.getStatus()) + ) + .collect(Collectors.toList()); + + } +} diff --git a/src/main/java/com/dku/springstudy/service/MemberService.java b/src/main/java/com/dku/springstudy/service/MemberService.java new file mode 100644 index 0000000..79d4818 --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/MemberService.java @@ -0,0 +1,112 @@ +package com.dku.springstudy.service; + +import com.dku.springstudy.domain.ImageFile; +import com.dku.springstudy.domain.Member; +import com.dku.springstudy.domain.token.RefreshToken; +import com.dku.springstudy.dto.MyPageDto; +import com.dku.springstudy.enums.Role; +import com.dku.springstudy.exception.KarrotException; +import com.dku.springstudy.repository.jpa.MemberRepository; +import com.dku.springstudy.repository.redis.RefreshTokenRepository; +import com.dku.springstudy.security.AuthenticationProvider; +import com.dku.springstudy.security.JwtTokenProvider; +import com.dku.springstudy.dto.TokenDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberService { + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + private final AuthenticationManagerBuilder authenticationManagerBuilder; + private final RefreshTokenRepository refreshTokenRepository; + + private final PasswordEncoder passwordEncoder; + + @Transactional + public Long join(String email, String password, String name, String phone, String nickname, Role role){ + String encodedPassword = passwordEncoder.encode(password); + + Member joinMember = Member.createMember( + email, + encodedPassword, + name, + phone, + nickname, + role + ); + memberRepository.save(joinMember); + return joinMember.getId(); + } + + @Transactional + public TokenDto login(String email, String password){ + // email/password + + // email, password기반으로 Authentication 객체 생성 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password); + + // 실제 검증 + // authenticate() 메서드 실행 시, CustomUserDetailService의 loadByUsername() 메서드 실행 + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + + // 인증 정보를 기반으로 Token 생성 + return jwtTokenProvider.generateToken(authentication); + } + + public TokenDto reissue(TokenDto token){ +// if(!jwtTokenProvider.validateToken(token.getRefreshToken())){ +// return ResponseEntity.badRequest().body("유효하지 않은 Refresh Token 입니다."); +// } + log.info("authentication"); + Authentication authentication = jwtTokenProvider.getAuthentication(token.getAccessToken()); + + + // redis에서 refresh Token 가져오기 + RefreshToken refreshToken = refreshTokenRepository.findById(authentication.getName()) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "Refresh Token이 존재하지 않습니다.")); + if(!token.getRefreshToken().equals(refreshToken.getRefreshToken())){ + throw new KarrotException(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), "토큰 정보 유효성 검증 실패"); + } + + TokenDto reissuedTokenDto = jwtTokenProvider.generateToken(authentication); + RefreshToken reissuedRefreshToken = new RefreshToken(authentication.getName(), reissuedTokenDto.getRefreshToken()); + refreshTokenRepository.save(reissuedRefreshToken); + + return reissuedTokenDto; + } + + public Member getCurrentLoginMember(String email){ + return memberRepository.findByEmail(email) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "존재하지 않은 사용자 입니다.")); + } + + public Member findById(Long id){ + return memberRepository.findById(id) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.value(), "존재하지 않은 사용자 입니다.")); + } + + public MyPageDto myPageHome() { + Member currentMember = AuthenticationProvider.getCurrentMember(); + return new MyPageDto(currentMember.getProfileImage().getImageUrl(), currentMember.getNickname()); + } + + @Transactional + public void updateProfiles(String nickname, ImageFile profileImage) { + Long currentMemberId = AuthenticationProvider.getCurrentMemberId(); + Member updateMember = memberRepository.findById(currentMemberId) + .orElseThrow(() -> new KarrotException(HttpStatus.NOT_FOUND, HttpStatus.NO_CONTENT.value(), "존재하지 않은 사용자 입니다.")); + + updateMember.updateMemberProfiles(nickname, profileImage); + } +} diff --git a/src/main/java/com/dku/springstudy/service/S3Upload.java b/src/main/java/com/dku/springstudy/service/S3Upload.java new file mode 100644 index 0000000..4ba48ec --- /dev/null +++ b/src/main/java/com/dku/springstudy/service/S3Upload.java @@ -0,0 +1,33 @@ +package com.dku.springstudy.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class S3Upload { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String upload(MultipartFile multipartFile) throws IOException { + String s3FileName = UUID.randomUUID() + "-" + multipartFile.getOriginalFilename(); + + ObjectMetadata objMeta = new ObjectMetadata(); + objMeta.setContentLength(multipartFile.getInputStream().available()); + + amazonS3.putObject(bucket, s3FileName, multipartFile.getInputStream(), objMeta); + + return amazonS3.getUrl(bucket, s3FileName).toString(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..261d4c3 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,44 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/karrot?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: password!23 + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 5 + + redis: + host: localhost + port: 6379 + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + show_sql: true + format_sql: true + +jwt: + secret: caf4bd8949582e4f85f8750c5e44bfba217e4a346c79c204cd2fb2a37071694bde9e55edab2f81b4fa6d2cd7a7d54e7e313c1a869f43cd30f5f37f462ce051b3 + +server: + error: + include-message: always + include-binding-errors: always + include-stacktrace: always + include-exception: false + + +cloud: + aws: + credentials: + accessKey: AKIAXW4DDD5LZRII4EWE # AWS IAM AccessKey 적기 + secretKey: fKI02S0pNFwTq/e0rLRNTXy0z+GVElu9ujlUiDyJ # AWS IAM SecretKey 적기 + s3: + bucket: karrotbucket # ex) marryting-gyunny + #dir: /karrotbucket # ex) /gyunny + region: + static: ap-northeast-2 + stack: + auto: false \ No newline at end of file diff --git a/src/test/java/com/dku/springstudy/domain/MemberTest.java b/src/test/java/com/dku/springstudy/domain/MemberTest.java new file mode 100644 index 0000000..4791b95 --- /dev/null +++ b/src/test/java/com/dku/springstudy/domain/MemberTest.java @@ -0,0 +1,86 @@ +package com.dku.springstudy.domain; + +import com.dku.springstudy.enums.Category; +import com.dku.springstudy.enums.Role; +import com.dku.springstudy.repository.jpa.ItemLikeRepository; +import com.dku.springstudy.repository.jpa.ItemRepository; +import com.dku.springstudy.repository.jpa.MemberRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@Transactional +class EntityTest { + @PersistenceContext + EntityManager em; + @Autowired MemberRepository memberRepository; + @Autowired + ItemRepository itemRepository; + + @Autowired + ItemLikeRepository itemLikeRepository; + + @Test + public void createMember(){ + //given + Member member = Member.createMember("test@naver.com", "1234", "동현", "010-9358-4027", "홍홍홍", Role.USER); + memberRepository.save(member); + + //when + Member findMember = memberRepository.findByName("동현").get(); + + //then + assertThat(findMember).isEqualTo(member); + } + + @Test + public void createItem(){ + //given + Member member = Member.createMember("test@naver.com", "1234", "동현", "010-9358-4027", "홍홍홍", Role.USER); + memberRepository.save(member); + + //when + Item item = new Item(); + item.createItem(member, "title", "content", 10000, Category.ELECTRONIC); + itemRepository.save(item); + + //then + assertThat(item.getMember()).isEqualTo(member); + + } + + @Test + public void createLike(){ + //given + Member member1 = Member.createMember("test@naver.com", "1234", "동현", "010-9358-4027", "홍홍홍", Role.USER); + memberRepository.save(member1); + Member member2 = Member.createMember("qwe123@gmail.com", "qwe123", "dong", "010-1234-5678", "공공공", Role.USER); + memberRepository.save(member2); + + Item item = new Item(); + item.createItem(member1, "title", "content", 10000, Category.ELECTRONIC); + itemRepository.save(item); + + //when + ItemLike itemLike1 = new ItemLike(); + itemLike1.createItemLike(item, member1); + ItemLike itemLike2 = new ItemLike(); + itemLike2.createItemLike(item, member2); + itemLikeRepository.save(itemLike1); + itemLikeRepository.save(itemLike2); + + //then + assertThat(itemLike1.getMember().getUsername()).isEqualTo("동현"); + assertThat(itemLike2.getMember().getUsername()).isEqualTo("dong"); + assertThat(itemLike1.getItem().getTitle()).isEqualTo("title"); + assertThat(itemLike2.getItem().getContent()).isEqualTo("content"); + } + +} \ No newline at end of file