diff --git a/place/build.gradle b/place/build.gradle index 984cb62..be629fd 100644 --- a/place/build.gradle +++ b/place/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/place/src/main/java/com/umc/place/common/BaseResponseStatus.java b/place/src/main/java/com/umc/place/common/BaseResponseStatus.java index fa94a70..7afcacb 100644 --- a/place/src/main/java/com/umc/place/common/BaseResponseStatus.java +++ b/place/src/main/java/com/umc/place/common/BaseResponseStatus.java @@ -13,12 +13,19 @@ public enum BaseResponseStatus { /** * 2000: Request 오류 */ - INVALID_STORY_IDX(false, 2100, "잘못된 스토리 Idx 입니다"), - // user(2000~2099) INVALID_USER_IDX(false, 2000, "잘못된 user Idx 입니다."), + NULL_TOKEN(false, 2001, "토큰 값을 입력해주세요."), + NULL_USER_IDX(false, 2002, "user Idx를 입력해주세요."), + NULL_PROVIDER(false, 2003, "소셜 이름을 입력해주세요."), + INVALID_PROVIDER(false, 2004, "잘못된 소셜 이름입니다."), + ALREADY_WITHDRAW_USER(false, 2005, "이미 탈퇴한 회원입니다."), + INVALID_TOKEN(false, 2006, "유효하지 않은 토큰 값입니다."), + UNSUPPORTED_TOKEN(false, 2007, "잘못된 형식의 토큰 값입니다."), + MALFORMED_TOKEN(false, 2008, "잘못된 구조의 토큰 값입니다."), // story(2100~2199) + INVALID_STORY_IDX(false, 2100, "잘못된 스토리 Idx 입니다"), // exhibition(2200~2299) INVALID_EXHIBITION_IDX(false, 2200, "잘못된 전시회 Idx 입니다."), @@ -31,6 +38,8 @@ public enum BaseResponseStatus { * 3000: Response 오류 */ // user(3000~3099) + EXPIRED_TOKEN(false, 3000, "만료된 토큰 값입니다."), + EXIST_NICKNAME(false, 3001, "이미 사용 중인 닉네임입니다."), // story(3100~3199) diff --git a/place/src/main/java/com/umc/place/common/Constant.java b/place/src/main/java/com/umc/place/common/Constant.java index 2ab9eb0..fd60b3b 100644 --- a/place/src/main/java/com/umc/place/common/Constant.java +++ b/place/src/main/java/com/umc/place/common/Constant.java @@ -3,4 +3,5 @@ public class Constant { public static final String ACTIVE = "active"; public static final String INACTIVE = "inactive"; + public static final String LOGOUT = "logout"; } diff --git a/place/src/main/java/com/umc/place/common/config/AWSConfig.java b/place/src/main/java/com/umc/place/common/config/AWSConfig.java new file mode 100644 index 0000000..856b210 --- /dev/null +++ b/place/src/main/java/com/umc/place/common/config/AWSConfig.java @@ -0,0 +1,32 @@ +package com.umc.place.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +@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 AmazonS3Client amazonS3Client() { + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) + .build(); + } +} diff --git a/place/src/main/java/com/umc/place/common/controller/FileUploadController.java b/place/src/main/java/com/umc/place/common/controller/FileUploadController.java new file mode 100644 index 0000000..f61723c --- /dev/null +++ b/place/src/main/java/com/umc/place/common/controller/FileUploadController.java @@ -0,0 +1,35 @@ +package com.umc.place.common.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.umc.place.common.service.S3Upload; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; + +@RequiredArgsConstructor +@RestController +public class FileUploadController { + + private final S3Upload s3Upload; + + @PostMapping("/upload") + public ResponseEntity uploadFile(MultipartFile[] multipartFileLis) throws IOException { + return ResponseEntity.ok( + s3Upload.upload(multipartFileLis) + ); + } +} \ No newline at end of file diff --git a/place/src/main/java/com/umc/place/common/service/S3Upload.java b/place/src/main/java/com/umc/place/common/service/S3Upload.java new file mode 100644 index 0000000..4c0586d --- /dev/null +++ b/place/src/main/java/com/umc/place/common/service/S3Upload.java @@ -0,0 +1,79 @@ +package com.umc.place.common.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +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.ArrayList; +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class S3Upload { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3Client amazonS3Client; + ; + + //https://jforj.tistory.com/261 + public List upload(MultipartFile[] multipartFileList) throws IOException { + List imagePathList = new ArrayList<>(); + + for (MultipartFile multipartFile : multipartFileList) { + String originalName = multipartFile.getOriginalFilename(); // 파일 이름 + long size = multipartFile.getSize(); // 파일 크기 + + ObjectMetadata objectMetaData = new ObjectMetadata(); + objectMetaData.setContentType(multipartFile.getContentType()); + objectMetaData.setContentLength(size); + + // S3에 업로드 + amazonS3Client.putObject( + new PutObjectRequest(bucket, originalName, multipartFile.getInputStream(), objectMetaData) + .withCannedAcl(CannedAccessControlList.PublicRead) + ); + + String imagePath = amazonS3Client.getUrl(bucket, originalName).toString(); // 접근가능한 URL 가져오기 + imagePathList.add(imagePath); + } + return imagePathList; + } +} + +// 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(); +// for(MultipartFile multipartFile: multipartFileList) { +// String originalName = multipartFile.getOriginalFilename(); // 파일 이름 +// long size = multipartFile.getSize(); // 파일 크기 +// +// ObjectMetadata objectMetaData = new ObjectMetadata(); +// objectMetaData.setContentType(multipartFile.getContentType()); +// objectMetaData.setContentLength(size); +// +// // S3에 업로드 +// amazonS3Client.putObject( +// new PutObjectRequest(S3Bucket, originalName, multipartFile.getInputStream(), objectMetaData) +// .withCannedAcl(CannedAccessControlList.PublicRead) +// ); +// +// String imagePath = amazonS3Client.getUrl(S3Bucket, originalName).toString(); // 접근가능한 URL 가져오기 +// imagePathList.add(imagePath); +// } +// +// return new ResponseEntity(imagePathList, HttpStatus.OK); diff --git a/place/src/main/java/com/umc/place/exhibition/dto/SearchExhibitionsByNameResDto.java b/place/src/main/java/com/umc/place/exhibition/dto/SearchExhibitionsByNameResDto.java new file mode 100644 index 0000000..9891824 --- /dev/null +++ b/place/src/main/java/com/umc/place/exhibition/dto/SearchExhibitionsByNameResDto.java @@ -0,0 +1,39 @@ +package com.umc.place.exhibition.dto; + +import com.umc.place.exhibition.entity.Exhibition; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@NoArgsConstructor +public class SearchExhibitionsByNameResDto { + + private List searchedExhibitions = new ArrayList(); + + @Builder + public SearchExhibitionsByNameResDto(Page exhibitions) { + this.searchedExhibitions + = exhibitions.stream() + .map(exhibition -> new SearchedExhibitionByName(exhibition)) + .collect(Collectors.toList()); + } + + @Data + @NoArgsConstructor + public static class SearchedExhibitionByName { + private Long exhibitionIdx; + private String exhibitionName; + + @Builder + public SearchedExhibitionByName(Exhibition exhibition) { + this.exhibitionIdx = exhibition.getExhibitionIdx(); + this.exhibitionName = exhibition.getExhibitionName(); + } + } +} diff --git a/place/src/main/java/com/umc/place/exhibition/repository/ExhibitionRepository.java b/place/src/main/java/com/umc/place/exhibition/repository/ExhibitionRepository.java index ebc4428..f87bd22 100644 --- a/place/src/main/java/com/umc/place/exhibition/repository/ExhibitionRepository.java +++ b/place/src/main/java/com/umc/place/exhibition/repository/ExhibitionRepository.java @@ -13,7 +13,9 @@ @Repository public interface ExhibitionRepository extends JpaRepository { Page findByCategory(Category category, Pageable pageable); // 카테고리 기반 전체 조회(페이징) + Page findAll(Pageable pageable); // 전체 조회(페이징) + boolean existsByCategory(Category category); @Query("select case when count(e) > 0 then true else false end from Exhibition e where Function('replace', e.location, ' ', '') like %:location%") @@ -25,4 +27,6 @@ public interface ExhibitionRepository extends JpaRepository { @Query("select e from Exhibition e where Function('replace', e.location, ' ', '') like %:location%") Page findByLocationLike(@Param("location") String location, Pageable pageable); + + Page findByExhibitionNameContainingOrderByExhibitionName(String searchKeyword, Pageable pageable); } diff --git a/place/src/main/java/com/umc/place/story/controller/StoryController.java b/place/src/main/java/com/umc/place/story/controller/StoryController.java index f338edd..f9fdfd2 100644 --- a/place/src/main/java/com/umc/place/story/controller/StoryController.java +++ b/place/src/main/java/com/umc/place/story/controller/StoryController.java @@ -5,11 +5,14 @@ import com.umc.place.comment.service.CommentService; import com.umc.place.common.BaseException; import com.umc.place.common.BaseResponse; +import com.umc.place.exhibition.dto.SearchExhibitionsByNameResDto; import com.umc.place.story.dto.StoryDetailResponseDto; import com.umc.place.story.dto.StoryUploadRequestDto; import com.umc.place.story.dto.StoryUploadResponseDto; import com.umc.place.story.service.StoryService; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.*; import static com.umc.place.common.BaseResponseStatus.NULL_STORY; @@ -42,6 +45,17 @@ public BaseResponse getStoryDetail(@PathVariable Long st } } + @GetMapping("/search") + public BaseResponse getExhibitionWhenUploadStory( + @PageableDefault(size = 5) Pageable pageable, + @RequestParam(required = false) String searchWord) { + try { + return new BaseResponse<>(storyService.searchExhibitionByName(searchWord, pageable)); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + @PostMapping("/{storyIdx}/comment") public BaseResponse uploadStoryComment(@PathVariable Long storyIdx, @RequestBody CommentUploadReqDto reqDto, diff --git a/place/src/main/java/com/umc/place/story/service/StoryService.java b/place/src/main/java/com/umc/place/story/service/StoryService.java index 57f6972..3809497 100644 --- a/place/src/main/java/com/umc/place/story/service/StoryService.java +++ b/place/src/main/java/com/umc/place/story/service/StoryService.java @@ -3,6 +3,7 @@ import com.umc.place.comment.dto.CommentResDto; import com.umc.place.comment.repository.CommentRepository; import com.umc.place.common.BaseException; +import com.umc.place.exhibition.dto.SearchExhibitionsByNameResDto; import com.umc.place.exhibition.entity.Exhibition; import com.umc.place.exhibition.repository.ExhibitionRepository; import com.umc.place.story.dto.StoryDetailResponseDto; @@ -17,6 +18,8 @@ import com.umc.place.user.entity.User; import com.umc.place.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -82,6 +85,19 @@ public StoryDetailResponseDto getStoryDetail(Long storyIdx, Long userId) throws } } + public SearchExhibitionsByNameResDto searchExhibitionByName(String searchWord, Pageable page) throws BaseException { + try { + searchWord = searchWord.trim(); + Page searchedExhibitions + = exhibitionRepository.findByExhibitionNameContainingOrderByExhibitionName(searchWord, page); + return SearchExhibitionsByNameResDto.builder() + .exhibitions(searchedExhibitions) + .build(); + } catch (Exception e) { + throw new BaseException(DATABASE_ERROR); + } + } + @Transactional public StoryUploadResponseDto uploadStory(StoryUploadRequestDto storyUploadRequestDto, Long userId) throws BaseException { try { diff --git a/place/src/main/java/com/umc/place/user/OAuth2/dto/KakaoTokenResponse.java b/place/src/main/java/com/umc/place/user/OAuth2/dto/KakaoTokenResponse.java new file mode 100644 index 0000000..b63ec19 --- /dev/null +++ b/place/src/main/java/com/umc/place/user/OAuth2/dto/KakaoTokenResponse.java @@ -0,0 +1,13 @@ +package com.umc.place.user.OAuth2.dto; + +import lombok.Data; + +@Data +public class KakaoTokenResponse { + private String token_type; + private String access_token; + private int expires_in; + private String refresh_token; + private int refresh_token_expires_in; + private String scope; +} diff --git a/place/src/main/java/com/umc/place/user/OAuth2/service/KakaoAuthService.java b/place/src/main/java/com/umc/place/user/OAuth2/service/KakaoAuthService.java new file mode 100644 index 0000000..dfa5931 --- /dev/null +++ b/place/src/main/java/com/umc/place/user/OAuth2/service/KakaoAuthService.java @@ -0,0 +1,80 @@ +package com.umc.place.user.OAuth2.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.place.user.OAuth2.dto.KakaoTokenResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Service +@Transactional +@Slf4j +public class KakaoAuthService { + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String kakaoClientId; + + //@Value("${spring.security.oauth2.client.registration.kakao.client-secret}") + //private String kakaoClientSecret; + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String kakaoRedirectUri; + + @Value("${spring.OAuth2.kakao.url.token}") + private String KAKAO_TOKEN_REQUEST_URL; + + @Value("${spring.OAuth2.kakao.url.profile}") + private String KAKAO_USERINFO_REQUEST_URL; + + //인가코드로 카카오 토큰 발급받기 + public KakaoTokenResponse getKakaoToken (String authorizationCode) throws JsonProcessingException { + Map params = new HashMap<>(); + params.put("code", authorizationCode); + params.put("client_id", kakaoClientId); + params.put("redirect_uri", kakaoRedirectUri); + params.put("grant_type", "authorization_code"); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.postForEntity(KAKAO_TOKEN_REQUEST_URL, + params, String.class); + + ObjectMapper objectMapper = new ObjectMapper(); + KakaoTokenResponse kakaoOAuthToken = objectMapper.readValue(response.getBody(), KakaoTokenResponse.class); + return kakaoOAuthToken; + } + + //카카오 토큰으로 사용자 정보(식별자) 가져오기 + public String getKakaoUserIdx (KakaoTokenResponse kakaoToken) throws JsonProcessingException { + + //header에 accessToken 담기 + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + kakaoToken.getAccess_token()); + + HttpEntity> request = new HttpEntity<>(headers); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange(KAKAO_USERINFO_REQUEST_URL, HttpMethod.GET, request, String.class); + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(response.getBody()); + + //user id 가져오기 + String identifier = jsonNode.get("id").asText(); + + return identifier; + } + +} + diff --git a/place/src/main/java/com/umc/place/user/controller/UserController.java b/place/src/main/java/com/umc/place/user/controller/UserController.java index ee44449..1878fdb 100644 --- a/place/src/main/java/com/umc/place/user/controller/UserController.java +++ b/place/src/main/java/com/umc/place/user/controller/UserController.java @@ -1,11 +1,80 @@ package com.umc.place.user.controller; +import com.umc.place.common.BaseException; +import com.umc.place.common.BaseResponse; +import com.umc.place.user.OAuth2.dto.KakaoTokenResponse; +import com.umc.place.user.OAuth2.service.KakaoAuthService; +import com.umc.place.user.dto.PostUserRes; +import com.umc.place.user.dto.PostNewUserReq; +import com.umc.place.user.dto.PostNicknameReq; +import com.umc.place.user.dto.LoginRequest; +import com.umc.place.user.service.AuthService; +import com.umc.place.user.service.UserService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; + +import static com.umc.place.common.BaseResponseStatus.*; @RestController @RequiredArgsConstructor -@RequestMapping("/users") +@RequestMapping ("/users") public class UserController { + private final UserService userService; + private final AuthService authService; + private final KakaoAuthService kakaoAuthService; + + + //카카오 소셜 로그인 + @ResponseBody + @PostMapping("/login/kakao") + public BaseResponse login(@RequestBody LoginRequest loginRequest) throws IOException, BaseException { + try{ + KakaoTokenResponse kakaoTokenResponse = kakaoAuthService.getKakaoToken(loginRequest.getCode()); + Long useridx = kakaoAuthService.getKakaoUserIdx(kakaoTokenResponse); + PostUserRes postUserRes = userService.login(useridx, "카카오"); + return new BaseResponse<>(postUserRes); + }catch (BaseException e){ + return new BaseResponse<>(e.getStatus()); + } + } + + //회원가입 후 정보 입력 + @ResponseBody + @PostMapping("/signup") + public BaseResponse signup(@RequestBody PostNewUserReq postNewUserReq) { + try{ + return new BaseResponse<>(userService.signup_UserInfo(authService.getUserIdx(), postNewUserReq)); + } catch(BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + //닉네임 중복 확인 + @ResponseBody + @PostMapping("/nickname") + public BaseResponse checkNickname(@RequestBody PostNicknameReq postNicknameReq) { + try{ + userService.checkNickname(postNicknameReq); + return new BaseResponse<>(SUCCESS); + }catch (BaseException e){ + return new BaseResponse<>(e.getStatus()); + } + } + + //마이페이지 조회 + + + //회원 프로필 수정 + + + //회원 탈퇴 + + + //회원 로그아웃 + + + //AccessToken 재발급 + } diff --git a/place/src/main/java/com/umc/place/user/dto/LoginRequest.java b/place/src/main/java/com/umc/place/user/dto/LoginRequest.java new file mode 100644 index 0000000..2b8c08d --- /dev/null +++ b/place/src/main/java/com/umc/place/user/dto/LoginRequest.java @@ -0,0 +1,11 @@ +package com.umc.place.user.dto; + +import lombok.Getter; +import org.antlr.v4.runtime.misc.NotNull; + +@Getter +public class LoginRequest { + @NotNull + private String code; + private String provider; +} diff --git a/place/src/main/java/com/umc/place/user/dto/PatchProfileReq.java b/place/src/main/java/com/umc/place/user/dto/PatchProfileReq.java new file mode 100644 index 0000000..7e45129 --- /dev/null +++ b/place/src/main/java/com/umc/place/user/dto/PatchProfileReq.java @@ -0,0 +1,12 @@ +package com.umc.place.user.dto; + +import lombok.Data; + + +@Data +public class PatchProfileReq { + private String nickname; + private String userImg; + private String email; + private String location; +} diff --git a/place/src/main/java/com/umc/place/user/dto/PostNewUserReq.java b/place/src/main/java/com/umc/place/user/dto/PostNewUserReq.java new file mode 100644 index 0000000..ef95755 --- /dev/null +++ b/place/src/main/java/com/umc/place/user/dto/PostNewUserReq.java @@ -0,0 +1,16 @@ +package com.umc.place.user.dto; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.util.Date; + +@Data +@RequiredArgsConstructor +public class PostNewUserReq { + private String nickname; + private String UserImg; + private String email; + private String location; + private Date birthday; +} diff --git a/place/src/main/java/com/umc/place/user/dto/PostNicknameReq.java b/place/src/main/java/com/umc/place/user/dto/PostNicknameReq.java new file mode 100644 index 0000000..c61bc66 --- /dev/null +++ b/place/src/main/java/com/umc/place/user/dto/PostNicknameReq.java @@ -0,0 +1,9 @@ +package com.umc.place.user.dto; + +import lombok.Getter; + +@Getter +public class PostNicknameReq { + //닉네임 중복 확인 + private String nickname; +} diff --git a/place/src/main/java/com/umc/place/user/dto/PostUserRes.java b/place/src/main/java/com/umc/place/user/dto/PostUserRes.java new file mode 100644 index 0000000..0b5f2c9 --- /dev/null +++ b/place/src/main/java/com/umc/place/user/dto/PostUserRes.java @@ -0,0 +1,16 @@ +package com.umc.place.user.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +public class PostUserRes { + private final String accessToken; + private final String refreshToken; + + @Builder + public PostUserRes(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/place/src/main/java/com/umc/place/user/entity/Provider.java b/place/src/main/java/com/umc/place/user/entity/Provider.java index 3d49674..2f6b807 100644 --- a/place/src/main/java/com/umc/place/user/entity/Provider.java +++ b/place/src/main/java/com/umc/place/user/entity/Provider.java @@ -1,5 +1,27 @@ package com.umc.place.user.entity; +import lombok.Getter; + +import java.util.Arrays; + +@Getter public enum Provider { - KAKAO, NAVER, GOOGLE + KAKAO(1, "카카오"), + NAVER(2, "네이버"), + GOOGLE(3, "구글"), + + ANONYMOUS(4,"비회원"); + + private int number; + private String name; + Provider(int number, String name){ + this.number = number; + this.name = name; + } + + public static Provider getProviderByName(String name){ + return Arrays.stream(Provider.values()) + .filter(r -> r.getName().equals(name)) + .findAny().orElse(null); + } } diff --git a/place/src/main/java/com/umc/place/user/entity/User.java b/place/src/main/java/com/umc/place/user/entity/User.java index 3be86ad..05e1c2f 100644 --- a/place/src/main/java/com/umc/place/user/entity/User.java +++ b/place/src/main/java/com/umc/place/user/entity/User.java @@ -2,11 +2,14 @@ import com.umc.place.common.BaseEntity; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.util.Date; + @Entity @Getter @NoArgsConstructor @@ -18,9 +21,15 @@ public class User extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userIdx; + @Column(nullable = false) + private String identifier; + @Column(nullable = false, length = 10) private String nickname; + @Column(nullable = false, length = 50) + private String email; + @Column private String userImg; @@ -31,9 +40,68 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private Provider provider; + @Column(nullable = false) + private Date birthday; + + //삭제하기 @Column(nullable = false) private String accessToken; @Column(nullable = false) private String refreshToken; + + @Builder + public User(String identifier, Provider provider) { + this.identifier = identifier; + this.provider = provider; + } + + public void signup(String nickname, String userImg, Date birthday, String location, String email){ + this.nickname = nickname; + this.userImg = userImg; + this.birthday = birthday; + this.location = location; + this.email = email; + } + + public void storeSignUp(String nickname, String userImg, Provider provider) { + this.nickname = nickname; + this.userImg = userImg; + this.provider = provider; + } + + //탈퇴하기 + public void signout() { + this.setNickname("알 수 없음"); + this.setProfileImg(null); + this.setProvider(Provider.ANONYMOUS); + this.setStatus("inactive"); + } + + public void setProfileImg(String userImg) { + this.userImg = userImg; + } + public void setNickname(String nickname) { + this.nickname = nickname; + } + public void setProvider(Provider provider){ + this.provider = provider; + } + + public void modifyNickname(String nickname) { + this.nickname = nickname; + } + + public void modifyUserImg(String userImg) { + this.userImg = userImg; + } + + public void logout() { + this.setStatus("logout"); + } + public void login() { + this.setStatus("active"); + } + + } diff --git a/place/src/main/java/com/umc/place/user/repository/UserRepository.java b/place/src/main/java/com/umc/place/user/repository/UserRepository.java index c758e3c..f2e84b2 100644 --- a/place/src/main/java/com/umc/place/user/repository/UserRepository.java +++ b/place/src/main/java/com/umc/place/user/repository/UserRepository.java @@ -1,9 +1,18 @@ package com.umc.place.user.repository; + +import com.umc.place.user.entity.Provider; import com.umc.place.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface UserRepository extends JpaRepository { + boolean existsByNickname(String nickname); + Optional findByUserIdxAndStatus(Long userIdx, String status); + User findByIdentifierAndProvider(String identifier, Provider provider); + Optional findByIdentifierAndProviderAndStatus(String identifier, Provider provider, String status); + } diff --git a/place/src/main/java/com/umc/place/user/service/AuthService.java b/place/src/main/java/com/umc/place/user/service/AuthService.java new file mode 100644 index 0000000..36656ab --- /dev/null +++ b/place/src/main/java/com/umc/place/user/service/AuthService.java @@ -0,0 +1,143 @@ +package com.umc.place.user.service; + +import com.umc.place.common.BaseException; +import com.umc.place.common.Constant; +import com.umc.place.user.dto.PostUserRes; +import com.umc.place.user.entity.User; +import com.umc.place.user.repository.UserRepository; +import io.jsonwebtoken.*; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.time.Duration; +import java.util.Date; + +import static com.umc.place.common.BaseResponseStatus.*; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final int accessTokenExpiryDate = 604800000; + private final int refreshTokenExpiryDate = 604800000; + + private final RedisTemplate redisTemplate; + + @Value("${auth.key}") + private String key; + String CLAIM_NAME = "userIdx"; + String REQUEST_HEADER_NAME = "Authorization"; + public static final String TOKEN_REGEX = "^Bearer( )*"; + public static final String TOKEN_REPLACEMENT = ""; + + private final UserRepository userRepository; + + /** + * userIdx 필수 + * @return 있으면 id, 없으면 EXCEPTION + */ + public Long getUserIdx() throws BaseException{ + String token = getToken(); + if(token==null) throw new BaseException(NULL_TOKEN); + return getClaims(token).getBody().get(CLAIM_NAME, Long.class); + } + + // 토큰 추출 + private String getToken() throws BaseException { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + String token = request.getHeader(REQUEST_HEADER_NAME); + if (token == null) return null; + if(redisTemplate.opsForValue().get(token)!=null) throw new BaseException(INVALID_TOKEN); + return token; + } + + public Jws getClaims(String token) throws BaseException{ + Jws claims = null; + token = token.replaceAll(TOKEN_REGEX, TOKEN_REPLACEMENT); + try { + claims = Jwts.parser() + .setSigningKey(key) + .parseClaimsJws(token); + } catch (ExpiredJwtException expiredJwtException) { + throw new BaseException(EXPIRED_TOKEN); + } catch (MalformedJwtException malformedJwtException) { + throw new BaseException(MALFORMED_TOKEN); + } catch (UnsupportedJwtException unsupportedJwtException) { + throw new BaseException(UNSUPPORTED_TOKEN); + } catch (Exception e) { + throw new BaseException(INVALID_TOKEN); + } + return claims; + } + + //user id로 JWT 토큰 생성 + public PostUserRes createToken(User user){ + String accessToken = createAccessToken(user.getUserIdx()); + String refreshToken = createRefreshToken(user.getUserIdx()); + return new PostUserRes(accessToken, refreshToken); + } + + public String createRefreshToken(Long userIdx){ + Date now = new Date(); + String refreshToken = Jwts.builder() + .setExpiration(new Date(now.getTime() + refreshTokenExpiryDate)) + .signWith(SignatureAlgorithm.HS256, key) + .compact(); + redisTemplate.opsForValue().set(String.valueOf(userIdx), refreshToken, Duration.ofMillis(refreshTokenExpiryDate)); + return refreshToken; + } + public String createAccessToken(Long userIdx){ + Date now = new Date(); + String accessToken = Jwts.builder() + .claim("userIdx", userIdx) + .setSubject(userIdx.toString()) + .setExpiration(new Date(now.getTime() + accessTokenExpiryDate)) + .signWith(SignatureAlgorithm.HS256, key) + .compact(); + return accessToken; + } + + //토큰 재발급 시 사용 + public String validateRefreshToken(Long userIdx, String refreshTokenReq) throws BaseException { + String refreshToken = (String) redisTemplate.opsForValue().get(String.valueOf(userIdx)); + if(!refreshToken.equals(refreshTokenReq)) throw new BaseException(INVALID_TOKEN); + return refreshToken; + } + + // 회원 로그아웃 + public void logout(Long userIdx) throws BaseException { + deleteToken(userIdx); + User user = userRepository.findByUserIdxAndStatus(userIdx, "active").orElseThrow(()->new BaseException(INVALID_USER_IDX)); + String token = user.getAccessToken(); + registerBlackList(token, Constant.LOGOUT); + } + + // 회원 탈퇴 + public void signout(Long userIdx) throws BaseException { + deleteToken(userIdx); + User user = userRepository.findByUserIdxAndStatus(userIdx, "active").orElseThrow(()->new BaseException(INVALID_USER_IDX)); + String token = user.getAccessToken(); + registerBlackList(token, Constant.INACTIVE); + } + + // refreshToken 삭제 + public void deleteToken(Long userIdx) { + String key = String.valueOf(userIdx); + if(redisTemplate.opsForValue().get(key)!=null) redisTemplate.delete(key); + } + + // 유효한 토큰(Bearer) blacklist로 등록 + private void registerBlackList(String token, String status) { + token = token.replaceAll(TOKEN_REGEX, TOKEN_REPLACEMENT); + Date AccessTokenExpiration = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody().getExpiration(); + long now = (new Date()).getTime(); + + Long expiration = AccessTokenExpiration.getTime() - now; + redisTemplate.opsForValue().set(token, status, Duration.ofMillis(expiration)); + } + +} diff --git a/place/src/main/java/com/umc/place/user/service/UserService.java b/place/src/main/java/com/umc/place/user/service/UserService.java index e0cf95c..3556cd9 100644 --- a/place/src/main/java/com/umc/place/user/service/UserService.java +++ b/place/src/main/java/com/umc/place/user/service/UserService.java @@ -1,9 +1,104 @@ package com.umc.place.user.service; +import com.umc.place.common.BaseException; +import com.umc.place.user.dto.*; +import com.umc.place.user.entity.Provider; +import com.umc.place.user.entity.User; +import com.umc.place.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.umc.place.common.BaseResponseStatus.*; @Service -@RequiredArgsConstructor +@RequiredArgsConstructor //생성자 자동 생성 public class UserService { + + private final UserRepository userRepository; + private final AuthService authService; + + private PostUserRes signUpOrLogin(String identifier, Provider provider) throws BaseException { + //Provider provider = Provider.KAKAO; + User user = userRepository.findByIdentifierAndProvider(identifier, provider); + //기존 회원이 아닐 경우 회원가입 + if (user == null) + user = signup(identifier,provider); //provider 카카오로 설정 + //탈퇴한 회원일 경우 + if (user.getStatus().equals("inactive")) + throw new BaseException(ALREADY_WITHDRAW_USER); + //기존 회원이면 로그인 처리 + user.login(); //user status active로 바꾸기 + userRepository.save(user); //userRepository에 user 저장 + return authService.createToken(user); //user의 access token, refresh token 반환 + } + + //로그인 + public PostUserRes login(String identifier, String provider) throws BaseException{ + try{ + if(Provider.getProviderByName(provider) == null) + throw new BaseException(INVALID_PROVIDER); + return signUpOrLogin(identifier, Provider.getProviderByName(provider)); //access token, refresh token 반환 + } catch (BaseException e){ + throw e; + } catch (Exception e){ + throw new BaseException(DATABASE_ERROR); + } + } + + //첫 로그인 시 user 생성 및 저장 (가입 연도 추가하기 createdDate) + public User signup(String identifier, Provider provider) { + User newuser = User.builder() + .identifier(identifier) + .provider(provider) + .build(); + return userRepository.save(newuser); + } + + //회원 가입 후 사용자 정보 입력 + @Transactional(rollbackFor = Exception.class) + public PostUserRes signup_UserInfo(Long userIdx, PostNewUserReq postNewUserReq) throws BaseException { + try{ + //userIdx로 해당 user 찾기 + User user = userRepository.findByUserIdxAndStatus(userIdx, "active").orElseThrow(()->new BaseException(INVALID_USER_IDX)); + String accessToken = authService.createAccessToken(userIdx); + String refreshToken = authService.createRefreshToken(userIdx); + + user.signup(postNewUserReq.getNickname(), postNewUserReq.getUserImg(), postNewUserReq.getBirthday(), postNewUserReq.getLocation(), postNewUserReq.getEmail()); + userRepository.save(user); //repository에 저장 + + return new PostUserRes(accessToken, refreshToken); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(DATABASE_ERROR); + } + } + + //닉네임 중복 확인하기 + public void checkNickname(PostNicknameReq postNicknameReq) throws BaseException { + boolean existence = userRepository.existsByNickname(postNicknameReq.getNickname()); + if(existence) throw new BaseException(EXIST_NICKNAME); + } + + //마이페이지 조회 + public GetProfileRes getProfile() throws BaseException { + Long userIdx = authService.getUserIdx(); + User user = userRepository.findByUserIdxAndStatus(userIdx, "active").orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + return new GetProfileRes(user.getUserImg(), user.getNickname(), "Hello, " + user.getNickname()); + } + + + //사용자 프로필 수정 + + + //회원 탈퇴 + + + //로그아웃 + + + // AccessToken 재발급 + + }