diff --git a/member-service/.gitignore b/member-service/.gitignore index c2065bc..af565c3 100644 --- a/member-service/.gitignore +++ b/member-service/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +src/main/resources/application-prod.yml \ No newline at end of file diff --git a/member-service/build.gradle b/member-service/build.gradle index 08bf3b3..008d07e 100644 --- a/member-service/build.gradle +++ b/member-service/build.gradle @@ -47,19 +47,21 @@ dependencies { // model-mapper implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.3.8' + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.5' implementation 'com.fasterxml.jackson.core:jackson-core:2.15.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' - //google + // google implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' - //jwt + // jwt runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/constant/MemberProvider.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/constant/MemberProvider.java new file mode 100644 index 0000000..d0f732e --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/constant/MemberProvider.java @@ -0,0 +1,5 @@ +package com.chuca.memberservice.domain.member.constant; + +public enum MemberProvider { + APP, KAKAO, GOOGLE, NAVER +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/MemberController.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/MemberController.java new file mode 100644 index 0000000..3e65c7a --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/MemberController.java @@ -0,0 +1,41 @@ +package com.chuca.memberservice.domain.member.controller; + +import com.chuca.memberservice.domain.member.dto.CheckDto; +import com.chuca.memberservice.domain.member.dto.LoginDto; +import com.chuca.memberservice.domain.member.dto.OAuthLoginDto; +import com.chuca.memberservice.domain.member.dto.SignUpDto; +import com.chuca.memberservice.domain.member.service.MemberService; +import com.chuca.memberservice.global.response.BaseResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/member") +public class MemberController { + + private final MemberService memberService; + + // 휴대전화 인증 + + + // 아이디 중복 확인 + @GetMapping("/check-id") + public ResponseEntity> checkId(@RequestBody @Validated CheckDto.idRequest request) { + return ResponseEntity.ok(BaseResponse.create(memberService.checkId(request.getGeneralId()))); + } + + // 일반 회원가입 + @PostMapping("/signup") + public ResponseEntity> signup(@RequestBody @Validated SignUpDto.Request request) { // 추후에 이미지도 처리 해야함 + return ResponseEntity.ok(BaseResponse.create(memberService.signup(request))); + } + + // 일반 로그인 + @PostMapping("/login") + public ResponseEntity> login(@RequestBody @Validated LoginDto.Request request) { + return ResponseEntity.ok(BaseResponse.create(memberService.login(request))); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/OAuthController.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/OAuthController.java new file mode 100644 index 0000000..6d3ed8e --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/OAuthController.java @@ -0,0 +1,42 @@ +//package com.chuca.memberservice.domain.member.controller; +// +//import com.chuca.memberservice.domain.member.dto.OAuthLoginDto; +//import com.chuca.memberservice.domain.member.service.oauth.KakaoOauthService; +//import com.chuca.memberservice.domain.member.service.oauth.NaverOauthService; +//import com.chuca.memberservice.global.response.BaseResponse; +//import com.fasterxml.jackson.core.JsonProcessingException; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +// +//import java.io.IOException; +// +//@RestController +//@RequestMapping("/oauth") +//@RequiredArgsConstructor +//public class OAuthController { +// +// private final NaverOauthService naverOauthService; +// private final KakaoOauthService kakaoOauthService; +// +// // 1. 네이버 로그인 (사용자 로그인 페이지 제공 단계) +// @GetMapping("/{socialType}") +// public void naverLogin(@PathVariable("socialType") String provider) throws IOException { +// naverOauthService.request(); +// } +// +// // 2. 네이버 로그인 (사용자 정보 받기) +// @GetMapping("/{socialType}/callback") +// public ResponseEntity> callback( +// @PathVariable("socialType") String provider, +// @RequestParam String code, +// @RequestParam String state) throws JsonProcessingException { +// return ResponseEntity.ok(BaseResponse.create(naverOauthService.login(code, state))); +// } +// +// // 3. 카카오 로그인 및 회원가입 (기획 로직 수정 중이라 추후에 약관 관련한 처리해야하고, 사업자 정보 발급하면 전화번호도 받아오기) +// @PostMapping("/{socialType}") +// public ResponseEntity> kakaoLogin(@PathVariable("socialType") String provider, @RequestBody OAuthLoginDto.Request request) { +// return ResponseEntity.ok(BaseResponse.create(kakaoOauthService.kakaoLogin(request))); +// } +//} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/TestController.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/TestController.java new file mode 100644 index 0000000..a3f1c84 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/controller/TestController.java @@ -0,0 +1,17 @@ +package com.chuca.memberservice.domain.member.controller; + +import com.chuca.memberservice.domain.member.entity.Member; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +public class TestController { + + @GetMapping("/test") + public String test() { + return "Hello, World!"; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/CheckDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/CheckDto.java new file mode 100644 index 0000000..7c2960a --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/CheckDto.java @@ -0,0 +1,35 @@ +package com.chuca.memberservice.domain.member.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +public class CheckDto { + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class idRequest { + @NotEmpty + private String generalId; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Response { + private boolean check; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class nicknameRequest { + @NotEmpty + private String nickname; + } + +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/LoginDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/LoginDto.java new file mode 100644 index 0000000..bccdcd5 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/LoginDto.java @@ -0,0 +1,29 @@ +package com.chuca.memberservice.domain.member.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +public class LoginDto { + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Request { + @NotEmpty + private String generalId; + + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,20}$", message = "비밀번호는 8자 이상 대문자, 소문자, 숫자, 특수문자 등 3가지 이상을 사용해 주세요.") + private String password; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Response { + private String accessToken; + private String refreshToken; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/OAuthLoginDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/OAuthLoginDto.java new file mode 100644 index 0000000..6968f96 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/OAuthLoginDto.java @@ -0,0 +1,24 @@ +package com.chuca.memberservice.domain.member.dto; + +import lombok.*; + +public class OAuthLoginDto { + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Request { + private String email; + private String nickname; + private String profileImage; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Response { + private String accessToken; + private String refreshToken; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/PhoneDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/PhoneDto.java new file mode 100644 index 0000000..87e8caa --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/PhoneDto.java @@ -0,0 +1,4 @@ +package com.chuca.memberservice.domain.member.dto; + +public class PhoneDto { +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/SignUpDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/SignUpDto.java new file mode 100644 index 0000000..a22866b --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/dto/SignUpDto.java @@ -0,0 +1,46 @@ +package com.chuca.memberservice.domain.member.dto; + +import jakarta.persistence.Column; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +public class SignUpDto { + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Request { + @NotEmpty + private String generalId; + + @NotEmpty + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,20}$", message = "비밀번호는 8자 이상 대문자, 소문자, 숫자, 특수문자 등 3가지 이상을 사용해 주세요.") + private String password; + + @NotEmpty + private String phone; + + @NotNull + private boolean locTos; // 위치 기반 서비스 이용약관 동의 여부 + + @NotNull + private boolean adTos; // 광고성 정보 수신 동의 여부 + + @NotEmpty + private String nickname; + + private String profileImage; // 추후에 이미지 관련 처리해야함 + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Response { + private String accessToken; + private String refreshToken; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/entity/Member.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/entity/Member.java new file mode 100644 index 0000000..a260db5 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/entity/Member.java @@ -0,0 +1,115 @@ +package com.chuca.memberservice.domain.member.entity; + +import com.chuca.memberservice.domain.member.constant.MemberProvider; +import com.chuca.memberservice.global.entity.BaseTime; +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Type; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +@DynamicUpdate +@Entity +@Table(name = "member") +public class Member extends BaseTime implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "memberId") + private Long id; + + // 일반 로그인 시 필요한 아이디 (일반 로그인에만 사용) + @Column(nullable = true, unique = true) + private String generalId; + + @Column(unique = true) + private String email; // 이메일 (소셜 로그인에만 사용) + + @Column(unique = true) + private String password; + + @Enumerated(EnumType.STRING) + @ColumnDefault("'APP'") + private MemberProvider provider; + + private String phone; + + @Column(nullable = false) + private boolean locTos; // 위치 기반 서비스 이용약관 동의 여부 + + @Column(nullable = false) + private boolean adTos; // 광고성 정보 수신 동의 여부 + + @Column(nullable = false, unique = true) + private String nickname; + + private String profileImage; // 프로필 이미지 + + @Column(nullable = false) + @ColumnDefault("'active'") + private String status; + + @Type(JsonType.class) + @Column(name = "alarms", columnDefinition = "longtext") + private Map alarms = new LinkedHashMap<>(); // 알림 설정 (관심 키워드, 관심 카페, 연락, 야간 푸시 알림)\ + + // 소셜 로그인 시 활용 + @Builder + public Member(String email, MemberProvider provider, String phone, String nickname, String profileImage) { + Map settings = new LinkedHashMap<>(); + settings.put("option1", true); // 관심 키워드 + settings.put("option2", true); // 관심 카페 + settings.put("option3", true); // 연락 + settings.put("option4", true); // 야간 푸시 알림 + + this.email = email; + this.provider = provider; + this.phone = phone; + this.nickname = nickname; + this.profileImage = profileImage != null ? profileImage : ""; + this.locTos = false; + this.adTos = false; + this.alarms = settings; + } + + // 일반 로그인 시 활용 + public Member(String gerneralId, String password, String phone, boolean locTos, boolean adTos, String nickname, String profileImage) { + Map settings = new LinkedHashMap<>(); + settings.put("option1", true); // 관심 키워드 + settings.put("option2", true); // 관심 카페 + settings.put("option3", true); // 연락 + settings.put("option4", true); // 야간 푸시 알림 + + this.generalId = gerneralId; + this.password = password; + this.phone = phone; + this.locTos = locTos; + this.adTos = adTos; + this.nickname = nickname; + this.profileImage = profileImage != null ? profileImage : ""; + this.alarms = settings; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getUsername() { + return null; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/repository/MemberRepository.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..a9a5105 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/repository/MemberRepository.java @@ -0,0 +1,14 @@ +package com.chuca.memberservice.domain.member.repository; + +import com.chuca.memberservice.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + Member findByEmail(String email); + + Optional findByGeneralId(String generalId); + Optional findByNickname(String nickname); +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/service/MemberDetailServiceImpl.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/MemberDetailServiceImpl.java new file mode 100644 index 0000000..c95b0da --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/MemberDetailServiceImpl.java @@ -0,0 +1,28 @@ +package com.chuca.memberservice.domain.member.service; + +import com.chuca.memberservice.domain.member.repository.MemberRepository; +import com.chuca.memberservice.global.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class MemberDetailServiceImpl implements UserDetailsService { + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException { + System.out.println("로그인한 memberId : " + memberId); + UserDetails result = (UserDetails) memberRepository.findById(Long.parseLong(memberId)) + .orElseThrow(() -> new BadRequestException("해당하는 사용자를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST)); + log.info("UserDetails: " + result.toString()); + + return result; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/service/MemberService.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/MemberService.java new file mode 100644 index 0000000..382c43e --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/MemberService.java @@ -0,0 +1,93 @@ +package com.chuca.memberservice.domain.member.service; + +import com.chuca.memberservice.domain.member.dto.CheckDto; +import com.chuca.memberservice.domain.member.dto.LoginDto; +import com.chuca.memberservice.domain.member.dto.SignUpDto; +import com.chuca.memberservice.domain.member.entity.Member; +import com.chuca.memberservice.domain.member.repository.MemberRepository; +import com.chuca.memberservice.global.exception.BadRequestException; +import com.chuca.memberservice.global.security.JwtProvider; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class MemberService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; + + // 일반 회원가입 + public SignUpDto.Response signup(SignUpDto.Request request) { + if(!checkId(request.getGeneralId()).isCheck()) { + throw new BadRequestException("이미 가입한 아이디입니다.", HttpStatus.BAD_REQUEST); + } + + if(!checkNickname(request.getNickname()).isCheck()) { + throw new BadRequestException("이미 존재하는 닉네임입니다.", HttpStatus.BAD_REQUEST); + } + + // 추후에 이미지 처리 해야함 + String encodedPassword = passwordEncoder.encode(request.getPassword()); + Member member = memberRepository.save( + new Member( + request.getGeneralId(), + encodedPassword, + request.getPhone(), + request.isLocTos(), + request.isAdTos(), + request.getNickname(), + request.getProfileImage() + ) + ); + + String accessToken = jwtProvider.encodeJwtToken(member.getId()); // 액세스 토큰 발급 + String refreshToken = jwtProvider.encodeJwtRefreshToken(member.getId()); // 리프레시 토큰 발급 + jwtProvider.storeJwtRefreshToken(member.getId(), refreshToken); // 리프레시 redis에 저장 + + return new SignUpDto.Response(accessToken, refreshToken); + } + + // 아이디 중복 확인 + public CheckDto.Response checkId(String generalId) { + Optional member = memberRepository.findByGeneralId(generalId); + if(member.isPresent()) + return new CheckDto.Response(false); // 이미 존재하기에 사용 불가능 + return new CheckDto.Response(true); + } + + // 닉네임 중복 확인 + public CheckDto.Response checkNickname(String nickname) { + Optional member = memberRepository.findByNickname(nickname); + if(member.isPresent()) + return new CheckDto.Response(false); // 이미 존재하기에 사용 불가능 + return new CheckDto.Response(true); + } + + // 로그인 + public LoginDto.Response login(LoginDto.Request request) { + Optional getMember = memberRepository.findByGeneralId(request.getGeneralId()); + + if(getMember.isEmpty()) + throw new BadRequestException("잘못된 아이디 입니다.", HttpStatus.BAD_REQUEST); + + Member member = getMember.get(); + if(!passwordEncoder.matches(request.getPassword(), member.getPassword())) { + throw new BadRequestException("비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST); + } + + String accessToken = jwtProvider.encodeJwtToken(member.getId()); // 액세스 토큰 발급 + String refreshToken = jwtProvider.encodeJwtRefreshToken(member.getId()); // 리프레시 토큰 발급 + jwtProvider.storeJwtRefreshToken(member.getId(), refreshToken); // 리프레시 redis에 저장 + + return new LoginDto.Response(accessToken, refreshToken); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/service/oauth/KakaoOauthService.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/oauth/KakaoOauthService.java new file mode 100644 index 0000000..24c2e64 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/oauth/KakaoOauthService.java @@ -0,0 +1,109 @@ +package com.chuca.memberservice.domain.member.service.oauth; + +import com.chuca.memberservice.domain.member.constant.MemberProvider; +import com.chuca.memberservice.domain.member.dto.OAuthLoginDto; +import com.chuca.memberservice.domain.member.entity.Member; +import com.chuca.memberservice.domain.member.repository.MemberRepository; +import com.chuca.memberservice.global.dto.JwtTokenDto; +import com.chuca.memberservice.global.exception.BadRequestException; +import com.chuca.memberservice.global.security.JwtProvider; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.Optional; + + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class KakaoOauthService { + + private final MemberRepository memberRepository; + private final JwtProvider jwtProvider; + + // 카카오 + // 로그인 및 회원가입 + public OAuthLoginDto.Response kakaoLogin(OAuthLoginDto.Request kakaoReqDto) { + String email = kakaoReqDto.getEmail(); + log.info(kakaoReqDto.getEmail()); + + if(email == null) + throw new BadRequestException("이메일 정보가 비어있습니다.", HttpStatus.BAD_REQUEST); + + Member getMember = memberRepository.findByEmail(email); + Member member; + if(getMember != null){ // 이미 회원가입한 회원인 경우 + member = getMember; + if(!member.getProvider().equals(MemberProvider.KAKAO)) // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 + throw new BadRequestException("이미 다른 경로로 가입한 회원입니다.", HttpStatus.UNAUTHORIZED); + } else { // 아직 회원가입 하지 않은 회원인 경우 + member = memberRepository.save( + Member.builder() + .email(email) + .nickname(kakaoReqDto.getNickname()) + .profileImage(kakaoReqDto.getProfileImage()) + .provider(MemberProvider.KAKAO) + .build() + ); + System.out.println("member id : " + member.getId()); + System.out.println("member email : " + member.getEmail()); + System.out.println("member nickname : " + member.getNickname()); + } + + // accessToken, refreshToken 발급 후 반환 + return createToken(member); // 임시 + } + + // accessToken, refreshToken 발급 + @Transactional + public OAuthLoginDto.Response createToken(Member member) { + String newAccessToken = jwtProvider.encodeJwtToken(member.getId()); + String newRefreshToken = jwtProvider.encodeJwtRefreshToken(member.getId()); + + System.out.println("newAccessToken : " + newAccessToken); + System.out.println("newRefreshToken : " + newRefreshToken); + + // DB에 refreshToken 저장 +// member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + System.out.println("member nickname : " + member.getNickname()); + + return new OAuthLoginDto.Response(newAccessToken, newRefreshToken); + } + + // refreshToken으로 accessToken 발급하기 + @Transactional + public OAuthLoginDto.Response regenerateAccessToken(String accessToken, String refreshToken) { + if(jwtProvider.validateToken(accessToken)) // access token 유효성 검사 + throw new BadRequestException("유효한 Access Token입니다.", HttpStatus.BAD_REQUEST); + + if(!jwtProvider.validateToken(refreshToken)) // refresh token 유효성 검사 + throw new BadRequestException("유효하지 않은 Refresh Token입니다. 다시 로그인하세요.", HttpStatus.UNAUTHORIZED); + + Long memberId = jwtProvider.getMemberIdFromJwtToken(refreshToken); + log.info("memberId : " + memberId); + + Optional getMember = memberRepository.findById(memberId); + if(getMember.isEmpty()) + throw new BadRequestException("해당하는 사용자를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST); + + Member member = getMember.get(); +// if(!refreshToken.equals(member.getRefreshToken())) +// throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); + + String newRefreshToken = jwtProvider.encodeJwtRefreshToken(memberId); + String newAccessToken = jwtProvider.encodeJwtToken(memberId); + +// member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + System.out.println("member nickname : " + member.getNickname()); + + return new OAuthLoginDto.Response(newAccessToken, newRefreshToken); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/member/service/oauth/NaverOauthService.java b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/oauth/NaverOauthService.java new file mode 100644 index 0000000..73470bc --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/member/service/oauth/NaverOauthService.java @@ -0,0 +1,147 @@ +//package com.chuca.memberservice.domain.member.service.oauth; +// +//import com.chuca.memberservice.domain.member.constant.MemberProvider; +//import com.chuca.memberservice.domain.member.dto.OAuthLoginDto; +//import com.chuca.memberservice.domain.member.entity.Member; +//import com.chuca.memberservice.domain.member.repository.MemberRepository; +//import com.chuca.memberservice.global.dto.NaverTokenDto; +//import com.chuca.memberservice.global.dto.NaverUserDto; +//import com.chuca.memberservice.global.exception.UnauthorizedException; +//import com.fasterxml.jackson.core.JsonProcessingException; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import jakarta.servlet.http.HttpServletResponse; +//import jakarta.transaction.Transactional; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.http.*; +//import org.springframework.stereotype.Service; +//import org.springframework.util.LinkedMultiValueMap; +//import org.springframework.util.MultiValueMap; +//import org.springframework.web.client.RestTemplate; +// +//import java.io.IOException; +//import java.util.UUID; +// +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class NaverOauthService { +// @Value("${security.oauth2.client.provider.naver.authorization-uri}") +// private String NAVER_AUTHORIZATINO_URI; +// +// @Value("${security.oauth2.client.provider.naver.token-uri}") +// private String NAVER_TOKEN_URI; +// +// @Value("${security.oauth2.client.provider.naver.user-info-uri}") +// private String NAVER_USER_INFO_URI; +// +// @Value("${security.oauth2.client.registration.naver.redirect-uri}") +// private String NAVER_REDIRECT_URI; +// +// @Value("${security.oauth2.client.registration.naver.client-id}") +// private String NAVER_CLIENT_ID; +// +// @Value("${security.oauth2.client.registration.naver.client-secret}") +// private String NAVER_CLIENT_SECRET; +// +// private final ObjectMapper objectMapper; +// private final HttpServletResponse response; +// private final MemberRepository memberRepository; +// +// // CSS 공격 방지를 위한 상태 토큰값 +// public String generateRandomString() { +// return UUID.randomUUID().toString(); +// } +// +// public String getAuthorizatioUri() { +// StringBuilder sb = new StringBuilder(); +// sb.append(NAVER_AUTHORIZATINO_URI); +// sb.append("?response_type=" + "code"); +// sb.append("&client_id=" + NAVER_CLIENT_ID); +// sb.append("&redirect_uri=" + NAVER_REDIRECT_URI); +// sb.append("&state=" + generateRandomString()); +// +// return sb.toString(); +// } +// +// // 1. 네이버 로그인 (사용자 로그인 페이지 제공 단계) +// public void request() throws IOException { +// String redirectURL = getAuthorizatioUri(); +// response.sendRedirect(redirectURL); +// } +// +// // 2. 네이버 로그인 (사용자 정보 조회) +// @Transactional +// public OAuthLoginDto.Response login(String code, String state) throws JsonProcessingException { +// // 1단계 : 네이버 액세스 토큰 발급 +// ResponseEntity tokenResult = requestAccessToken(code, state); +// String accessToken = getAccessToken(tokenResult); +// +// // 2단계 : 액세스 토큰으로 유저 정보 조회 +// ResponseEntity userInfoResult = requestUserInfo(accessToken); +// NaverUserDto naverUserDto = getUserInfo(userInfoResult); +// +// // 3단계 : DB에 없는 회원이면 회원가입 + 액세스/리프레시 토큰 발급 +// String email = naverUserDto.getResponse().getEmail(); +// Member getMember = memberRepository.findByEmail(email); +// if(getMember == null){ +// Member member = Member.builder() +// .email(email) +// .phone(naverUserDto.getResponse().getMobile()) +// .nickname(naverUserDto.getResponse().getNickname()) +// .profileImage(naverUserDto.getResponse().getProfile_image()) +// .provider(MemberProvider.NAVER) +// .build(); +// getMember = memberRepository.save(member); +// } +// +// return new OAuthLoginDto.Response("accessToken", "refreshToken"); // 임시 +// } +// +// // 네이버 액세스 토큰 발급 +// public ResponseEntity requestAccessToken(String code, String state) { +// RestTemplate restTemplate = new RestTemplate(); +// +// HttpHeaders headers = new HttpHeaders(); +// MultiValueMap params = new LinkedMultiValueMap<>(); +// params.add("grant_type", "authorization_code"); +// params.add("client_id", NAVER_CLIENT_ID); +// params.add("client_secret", NAVER_CLIENT_SECRET); +// params.add("code", code); +// params.add("state", state); +// +// HttpEntity> request = new HttpEntity<>(params, headers); +// ResponseEntity response = restTemplate.exchange(NAVER_TOKEN_URI, HttpMethod.POST, request, String.class); +// if (response.getStatusCode() == HttpStatus.OK) { +// return response; +// } +// throw new UnauthorizedException("네이버 액세스 토큰 발급에 실패했습니다.", HttpStatus.UNAUTHORIZED); +// } +// +// // 네이버 액세스 토큰 추출 +// public String getAccessToken(ResponseEntity response) throws JsonProcessingException { +// NaverTokenDto naverTokenDto = objectMapper.readValue(response.getBody(), NaverTokenDto.class); +// return naverTokenDto.getAccess_token(); +// } +// +// // 네이버 유저 정보 조회 +// public ResponseEntity requestUserInfo(String accessToken) { +// RestTemplate restTemplate = new RestTemplate(); +// +// // 요청 헤더에 accessToken 담기 +// HttpHeaders headers = new HttpHeaders(); +// headers.add("Authorization", "Bearer " + accessToken); +// +// // HttpEntity를 생성해, 헤더를 담아 restTemplate으로 네이버와 통신 +// HttpEntity> request = new HttpEntity(headers); +// ResponseEntity response = restTemplate.exchange(NAVER_USER_INFO_URI, HttpMethod.POST, request, String.class); +// return response; +// } +// +// // 네이버 유저 정보 추출 +// public NaverUserDto getUserInfo(ResponseEntity response) throws JsonProcessingException { +// NaverUserDto naverUserDto = objectMapper.readValue(response.getBody(), NaverUserDto.class); +// return naverUserDto; +// } +//} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/constant/Bank.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/constant/Bank.java new file mode 100644 index 0000000..38b09da --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/constant/Bank.java @@ -0,0 +1,33 @@ +package com.chuca.memberservice.domain.owner.constant; + +public enum Bank { + /* + KB국민은행, SC제일은행, 경남은행, 광주은행, + 기업은행, 농협, 대구은행, 부산은행, + 산업은행, 수협, 신한은행, 신협, + 외환은행, 우리은행, 우체국, 전북은행, + 제주은행, 축협, 하나은행, 한국씨티은행, + K뱅크, 카카오뱅크, 한국투자증권, 삼성증권 + + */ + KOOKMIN("004"), SCB("023"), BNK("039"), KJB("034"), + IBK("003"), NH("011"), DGB("031"), BSB("032"), + KDB("002"), SH("007"), SHINHAN("088"), CU("048"), + KEB("005"), WOORI("020"), POST("071"), JBB("037"), + JEJU("035"), CH("012"), HANA("081"), CITY("027"), + KBANK("089"), KAKAO("090"), KI("243"), SAMSUNG("240"); + + private String value; + + Bank(String value) { + this.value = value; + } + + public String getKey() { + return name(); + } + + public String getValue() { + return value; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/controller/OwnerController.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/controller/OwnerController.java new file mode 100644 index 0000000..d859412 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/controller/OwnerController.java @@ -0,0 +1,44 @@ +//package com.chuca.memberservice.domain.owner.controller; +// +//import com.chuca.memberservice.domain.owner.dto.BankDto; +//import com.chuca.memberservice.domain.owner.dto.CafeDto; +//import com.chuca.memberservice.domain.owner.dto.LoginDto; +//import com.chuca.memberservice.domain.owner.dto.OwnerDto; +//import com.chuca.memberservice.domain.owner.service.OwnerService; +//import com.chuca.memberservice.global.dto.BusinessDto; +//import com.chuca.memberservice.global.response.BaseResponse; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.ResponseEntity; +//import org.springframework.validation.annotation.Validated; +//import org.springframework.web.bind.annotation.*; +// +//@RestController +//@RequiredArgsConstructor +//@RequestMapping("/owner") +//public class OwnerController { +// private final OwnerService ownerService; +// +// // 1. 회원가입 및 입점신청 +// @PostMapping("/signup") +// public ResponseEntity> signup(@RequestBody @Validated OwnerDto.Request request) { +// return ResponseEntity.ok(BaseResponse.create(ownerService.signup(request))); +// } +// +// // 2. 사업자 진위 여부 확인 +// @PostMapping("/check") +// public ResponseEntity> checkBusinessNum(@RequestBody @Validated BusinessDto.Request request) { +// return ResponseEntity.ok(BaseResponse.create(ownerService.checkBusinessNum(request))); +// } +// +// // 3. 계좌 실명 조회 +// @PostMapping("/bank") +// public ResponseEntity> checkAccountRealName(@RequestBody @Validated BankDto request) { +// return ResponseEntity.ok(BaseResponse.create(ownerService.checkAccountRealName(request))); +// } +// +// // 4. 사장님 로그인 +// @PostMapping("/login") +// public ResponseEntity> login(@RequestBody @Validated LoginDto.Request request) { +// return ResponseEntity.ok(BaseResponse.create(ownerService.login(request))); +// } +//} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/BankDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/BankDto.java new file mode 100644 index 0000000..15f8b8d --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/BankDto.java @@ -0,0 +1,13 @@ +package com.chuca.memberservice.domain.owner.dto; + +import com.chuca.memberservice.domain.owner.constant.Bank; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class BankDto { + @NotBlank + private Bank bank_code; // 은행코드 + @NotBlank + private String bank_num; // 계좌번호 +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/CafeDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/CafeDto.java new file mode 100644 index 0000000..33434e5 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/CafeDto.java @@ -0,0 +1,47 @@ +package com.chuca.memberservice.domain.owner.dto; + +import com.chuca.memberservice.domain.owner.entity.Cafe; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +public class CafeDto { + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Request { + @NotBlank(message = "카페명을 입력해주세요.") + private String name; + + @NotBlank(message = "카페 전화번호를 입력해주세요.") + private String phone; + + @NotBlank(message = "주소를 입력해주세요.") + private String address; + + @NotNull + private Double latitude; + + @NotNull + private Double longitude; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Response { + private Long cafeId; + + private String name; + + private String status; + + public Response(Cafe cafe) { + this.cafeId = cafe.getId(); + this.name = cafe.getName(); + this.status = "입점 신청이 완료되었습니다."; + } + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/LoginDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/LoginDto.java new file mode 100644 index 0000000..edfe7b4 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/LoginDto.java @@ -0,0 +1,27 @@ +package com.chuca.memberservice.domain.owner.dto; + + +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +public class LoginDto { + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Request { + @NotBlank + private String businessNum; + @NotBlank + private String password; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Response { + private String accessToken; + private String refreshToken; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/OwnerDto.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/OwnerDto.java new file mode 100644 index 0000000..b184b36 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/dto/OwnerDto.java @@ -0,0 +1,58 @@ +package com.chuca.memberservice.domain.owner.dto; + +import com.chuca.memberservice.domain.owner.constant.Bank; +import com.chuca.memberservice.domain.owner.entity.Cafe; +import com.chuca.memberservice.global.annotation.Enum; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.*; + +import java.time.LocalDate; + +public class OwnerDto { + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + public static class Request { + @Email + private String email; // 이메일 + + @NotBlank(message = "사업자 등록 번호를 입력해주세요.") + private String businessNum; // 사업자 등록 번호 + + @NotBlank(message = "사업자 등록번호 사본를 첨부해주세요") + private String businessImage; // 사업자 등록번호 사본 + + @NotBlank(message = "개업일자를 입력해주세요.") + private LocalDate openingDate; // 개업일자 + + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}", message = "비밀번호는 영문자, 숫자, 특수문자 조합을 입력해야합니다.") + private String password; // 비밀번호 + + @NotBlank(message = "사업주명을 입력해주세요.") + private String name; // 사업주명 + + @NotBlank(message = "휴대폰 번호를 입력해주세요.") + private String phone; // 사업자 휴대폰 번호 + + @NotBlank(message = "계좌번호를 입력해주세요.") + private String account; // 계좌번호 + + @Enum(enumClass = Bank.class) + private Bank bank; // 은행 + + @NotNull + private boolean agreeOption; // 선택 약관 동의 + + @NotBlank(message = "닉네임을 입력해주세요.") + private String nickname; + + private String profileImage; + + @NotNull + private CafeDto.Request cafeDto; // 카페 정보 + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/entity/Cafe.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/entity/Cafe.java new file mode 100644 index 0000000..0b83147 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/entity/Cafe.java @@ -0,0 +1,55 @@ +package com.chuca.memberservice.domain.owner.entity; + +import com.chuca.memberservice.domain.owner.dto.CafeDto; +import com.chuca.memberservice.global.entity.BaseTime; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +@DynamicUpdate +@Entity +@Table(name = "cafe") +public class Cafe extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "cafeId") + private Long id; + + @Column(nullable = false) + private String name; // 카페명 + + @Column(nullable = false) + private String phone; // 카페 전화번호 + + private String description; // 카페 소개 + + @Column(nullable = false) + private String address; // 주소 + + @Column(nullable = false) + private Double latitude; // 위도 + + @Column(nullable = false) + private Double longitude; // 경도 + + @ManyToOne + @JoinColumn(name="ownerId") + private Owner owner; + + @Builder + public Cafe(CafeDto.Request request, Owner owner) { + this.name = request.getName(); + this.phone = request.getPhone(); + this.address = request.getAddress(); + this.latitude = request.getLatitude(); + this.longitude = request.getLongitude(); + this.owner = owner; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/entity/Owner.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/entity/Owner.java new file mode 100644 index 0000000..3080c19 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/entity/Owner.java @@ -0,0 +1,91 @@ +package com.chuca.memberservice.domain.owner.entity; + +import com.chuca.memberservice.domain.owner.constant.Bank; +import com.chuca.memberservice.domain.owner.dto.OwnerDto; +import com.chuca.memberservice.global.entity.BaseTime; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +@DynamicUpdate +@Entity +@Table(name = "owner") +public class Owner extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ownerId") + private Long id; + + @Column(nullable = false, unique = true) + private String email; // 이메일 + + @Column(nullable = false, unique = true) + private String businessNum; // 사업자 등록 번호 + + @Column(nullable = false) + private String businessImage; // 사업자 등록번호 사본 + + @Column(nullable = false) + private LocalDate openingDate; // 개업일자 + + @Column(nullable = false) + private String password; // 비밀번호 + + @Column(nullable = false) + private String name; // 사업주명 + + @Column(nullable = false) + private String phone; // 사업자 휴대폰 번호 + + @Column(nullable = false, unique = true) + private String account; // 계좌번호 + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Bank bank; // 은행 + + @Column(nullable = false) + @ColumnDefault("false") + private boolean agreeOption; // 선택 약관 동의 + + @Column(nullable = false) + private String nickname; + + private String profileImage; // 프로필 이미지 + + @Column(nullable = false) + @ColumnDefault("'active'") + private String status; // 상태 + + @OneToMany(mappedBy="owner", cascade=CascadeType.ALL) + private List cafes; + + + @Builder + public Owner(String email, String businessNum, String businessImage, LocalDate openingDate, String password, + String name, String phone, String account, Bank bank, boolean agreeOption, String nickname, String profileImage) { + this.email = email; + this.businessNum = businessNum; + this.businessImage = businessImage; + this.openingDate = openingDate; + this.password = password; + this.name = name; + this.phone = phone; + this.account = account; + this.bank = bank; + this.agreeOption = agreeOption; + this.nickname = nickname; + this.profileImage = profileImage; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/repository/CafeRepository.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/repository/CafeRepository.java new file mode 100644 index 0000000..d91e234 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/repository/CafeRepository.java @@ -0,0 +1,9 @@ +package com.chuca.memberservice.domain.owner.repository; + +import com.chuca.memberservice.domain.owner.entity.Cafe; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CafeRepository extends JpaRepository { +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/repository/OwnerRepository.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/repository/OwnerRepository.java new file mode 100644 index 0000000..bf75919 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/repository/OwnerRepository.java @@ -0,0 +1,12 @@ +package com.chuca.memberservice.domain.owner.repository; + +import com.chuca.memberservice.domain.owner.entity.Owner; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface OwnerRepository extends JpaRepository { + Optional findByBusinessNum(String businessNum); +} diff --git a/member-service/src/main/java/com/chuca/memberservice/domain/owner/service/OwnerService.java b/member-service/src/main/java/com/chuca/memberservice/domain/owner/service/OwnerService.java new file mode 100644 index 0000000..bc8a09d --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/domain/owner/service/OwnerService.java @@ -0,0 +1,110 @@ +//package com.chuca.memberservice.domain.owner.service; +// +//import com.chuca.memberservice.domain.owner.constant.Bank; +//import com.chuca.memberservice.domain.owner.dto.BankDto; +//import com.chuca.memberservice.domain.owner.dto.CafeDto; +//import com.chuca.memberservice.domain.owner.dto.LoginDto; +//import com.chuca.memberservice.domain.owner.dto.OwnerDto; +//import com.chuca.memberservice.domain.owner.entity.Cafe; +//import com.chuca.memberservice.domain.owner.entity.Owner; +//import com.chuca.memberservice.domain.owner.repository.CafeRepository; +//import com.chuca.memberservice.domain.owner.repository.OwnerRepository; +//import com.chuca.memberservice.global.dto.BusinessDto; +//import com.chuca.memberservice.global.dto.PortOneDto; +//import com.chuca.memberservice.global.dto.TokenDto; +//import com.chuca.memberservice.global.exception.BadRequestException; +//import com.chuca.memberservice.global.exception.UnauthorizedException; +//import com.chuca.memberservice.global.feign.BusinessManClient; +//import com.chuca.memberservice.global.feign.PortOneClient; +//import feign.FeignException; +//import jakarta.transaction.Transactional; +//import lombok.RequiredArgsConstructor; +// +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.http.HttpStatus; +//import org.springframework.security.crypto.password.PasswordEncoder; +//import org.springframework.stereotype.Service; +// +// +//@Service +//@RequiredArgsConstructor +//@Transactional +//public class OwnerService { +// @Value("${business.validate.key}") +// private String serviceKey; +// @Value("${imp.key}") +// private String impKey; +// @Value("${imp.secret}") +// private String impSecret; +// +// private final PasswordEncoder passwordEncoder; +// private final OwnerRepository ownerRepository; +// private final CafeRepository cafeRepository; +// private final BusinessManClient businessManClient; +// private final PortOneClient portOneClient; +// +// // 회원가입 및 입점 신청 +// public CafeDto.Response signup(OwnerDto.Request request) { +// String encodedPassword = passwordEncoder.encode(request.getPassword()); +// Owner newOwner = Owner.builder() +// .email(request.getEmail()) +// .businessNum(request.getBusinessNum()) +// .businessImage(request.getBusinessImage()) +// .openingDate(request.getOpeningDate()) +// .password(encodedPassword) +// .name(request.getName()) +// .phone(request.getPhone()) +// .account(request.getAccount()) +// .bank(request.getBank()) +// .agreeOption(request.isAgreeOption()) +// .nickname(request.getNickname()) +// .profileImage(request.getProfileImage()) +// .build(); +// Owner owner = ownerRepository.save(newOwner); +// +// Cafe newCafe = Cafe.builder() +// .request(request.getCafeDto()) +// .owner(owner) +// .build(); +// return new CafeDto.Response(cafeRepository.save(newCafe)); +// } +// +// // 사업자 진위 여부 확인 +// public Boolean checkBusinessNum(BusinessDto.Request request) { +// BusinessDto.Response response = businessManClient.checkValidate(serviceKey, request); +// BusinessDto.ValidData data = response.getData().get(0); // 요청을 List로 보내지만 실질적 데이터는 1개 +// String valid = data.getValid(); // 01 : valid, 02 : invalid +// return valid.equals("01"); +// } +// +// // 계좌 실명 확인 +// public String checkAccountRealName(BankDto request) { +// TokenDto.Request tokenRequest = TokenDto.Request.builder() +// .imp_key(impKey) +// .imp_secret(impSecret) +// .build(); +// +// TokenDto.Response tokenResponse = portOneClient.getToken(tokenRequest); +// if(tokenResponse.getCode() != 0) +// throw new UnauthorizedException("Access token 발급에 실패했습니다.", HttpStatus.UNAUTHORIZED); +// +// Bank bank = request.getBank_code(); +// String accessToken = "Bearer " + tokenResponse.getResponse().getAccess_token(); +// PortOneDto portOneResponse = portOneClient.getBankHolder(accessToken, bank.getValue(), request.getBank_num()); +// if(portOneResponse.getCode() != 0) +// throw new BadRequestException("계좌 정보 조회에 실패했습니다.", HttpStatus.UNAUTHORIZED); +// return portOneResponse.getResponse().getBank_holder(); // 계좌 예금주명 +// } +// +// // 사장님 로그인 +// public LoginDto.Response login(LoginDto.Request request) { +// String businessNum = request.getBusinessNum(); +// Owner owner = ownerRepository.findByBusinessNum(businessNum) +// .orElseThrow(() -> new IllegalArgumentException("잘못된 사업자 등록 번호 입니다.")); +// +// String encodedPassword = passwordEncoder.encode(request.getPassword()); +// if(owner.getPassword().equals(encodedPassword)) +// return new LoginDto.Response("accessToken", "refreshToken"); // 임시 +// throw new BadRequestException("비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST); +// } +//} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/annotation/Enum.java b/member-service/src/main/java/com/chuca/memberservice/global/annotation/Enum.java new file mode 100644 index 0000000..70a33b0 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/annotation/Enum.java @@ -0,0 +1,20 @@ +package com.chuca.memberservice.global.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = {EnumValidator.class}) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Enum { + String message() default "enum 형식에 맞지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; + Class> enumClass(); + boolean ignoreCase() default false; +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/annotation/EnumValidator.java b/member-service/src/main/java/com/chuca/memberservice/global/annotation/EnumValidator.java new file mode 100644 index 0000000..bb7dad3 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/annotation/EnumValidator.java @@ -0,0 +1,31 @@ +package com.chuca.memberservice.global.annotation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class EnumValidator implements ConstraintValidator { + + private Enum annotation; + + @Override + public void initialize(Enum constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + boolean result = false; + Object[] enumValues = this.annotation.enumClass().getEnumConstants(); + if (enumValues != null) { + for (Object enumValue : enumValues) { + if (value.equals(enumValue.toString()) + || (this.annotation.ignoreCase() && value.equalsIgnoreCase(enumValue.toString()))) { + result = true; + break; + } + } + } + return result; + } + +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/config/FeignClientConfig.java b/member-service/src/main/java/com/chuca/memberservice/global/config/FeignClientConfig.java new file mode 100644 index 0000000..bf44e50 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/config/FeignClientConfig.java @@ -0,0 +1,10 @@ +package com.chuca.memberservice.global.config; + +import com.chuca.memberservice.MemberServiceApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients(basePackageClasses = MemberServiceApplication.class) +public class FeignClientConfig { +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/config/JpaConfig.java b/member-service/src/main/java/com/chuca/memberservice/global/config/JpaConfig.java new file mode 100644 index 0000000..90eded1 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/config/JpaConfig.java @@ -0,0 +1,12 @@ +package com.chuca.memberservice.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +@RequiredArgsConstructor +public class JpaConfig { + +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/config/MapperConfig.java b/member-service/src/main/java/com/chuca/memberservice/global/config/MapperConfig.java new file mode 100644 index 0000000..8389667 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/config/MapperConfig.java @@ -0,0 +1,13 @@ +package com.chuca.memberservice.global.config; + +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MapperConfig { + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/config/RedisConfig.java b/member-service/src/main/java/com/chuca/memberservice/global/config/RedisConfig.java new file mode 100644 index 0000000..6328cd0 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/config/RedisConfig.java @@ -0,0 +1,35 @@ +package com.chuca.memberservice.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.host}") + private String host; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + // redisTemplate를 받아와서 set, get, delete를 사용 + RedisTemplate redisTemplate = new RedisTemplate<>(); + // setKeySerializer, setValueSerializer 설정 + // redis-cli을 통해 직접 데이터를 조회 시 알아볼 수 없는 형태로 출력되는 것을 방지 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/config/SwaggerConfig.java b/member-service/src/main/java/com/chuca/memberservice/global/config/SwaggerConfig.java new file mode 100644 index 0000000..a837e43 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/config/SwaggerConfig.java @@ -0,0 +1,52 @@ +package com.chuca.memberservice.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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 java.util.Arrays; + +@Configuration +public class SwaggerConfig { + private static final String API_NAME = "CHUCA API"; + private static final String API_VERSION = "0.0.1"; + private static final String API_DESCRIPTION = "CHUCA API v.1.0"; + + @Bean + public Docket api() { + return new Docket(DocumentationType.OAS_30) +// .securityContexts(Arrays.asList(securityContext())) +// .securitySchemes(Arrays.asList(apiKey())) + .select() + .apis(RequestHandlerSelectors. + basePackage("com.chuca.memberservice")) + .paths(PathSelectors.any()).build().apiInfo(apiInfo()); + } + + public ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title(API_NAME) + .version(API_VERSION) + .description(API_DESCRIPTION) + .build(); + } +// +// // JWT SecurityContext 구성 +// private SecurityContext securityContext() { +// return SecurityContext.builder() +// .securityReferences(defaultAuth()) +// .build(); +// } +// +// private List defaultAuth() { +// AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); +// AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; +// authorizationScopes[0] = authorizationScope; +// return List.of(new SecurityReference("Authorization", authorizationScopes)); +// } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/dto/BusinessDto.java b/member-service/src/main/java/com/chuca/memberservice/global/dto/BusinessDto.java new file mode 100644 index 0000000..860f1fc --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/dto/BusinessDto.java @@ -0,0 +1,42 @@ +package com.chuca.memberservice.global.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BusinessDto { + @Getter + public static class Request { + List businesses; + } + + @Getter + public static class Business { + @NotBlank + private String b_no; // 사업자 등록 번호 + @NotBlank + private String start_dt; // 개업일자 + @NotBlank + private String p_nm; // 사업자명 + } + + @Getter + public static class Response { + private String status_code; // API 상태 코드 + private Integer request_cnt; // 조회 요청 수 + private Integer valid_cnt; // 검증 valid 수 + private List data; // 사업자 등록 정보 진위확인 결과 + } + + @Getter + public static class ValidData { + private String b_no; // 사업자 등록 번호 + private String valid; // 진위여부 (01 : valid, 02 : invalid) + private Object request_param; // 사업자등록정보 진위확인 + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/dto/JwtTokenDto.java b/member-service/src/main/java/com/chuca/memberservice/global/dto/JwtTokenDto.java new file mode 100644 index 0000000..50525fd --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/dto/JwtTokenDto.java @@ -0,0 +1,13 @@ +package com.chuca.memberservice.global.dto; + +import lombok.Getter; + +@Getter +public class JwtTokenDto { + + private final Long memberId; + + public JwtTokenDto(Long memberId) { + this.memberId = memberId; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/dto/NaverTokenDto.java b/member-service/src/main/java/com/chuca/memberservice/global/dto/NaverTokenDto.java new file mode 100644 index 0000000..95094c5 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/dto/NaverTokenDto.java @@ -0,0 +1,14 @@ +package com.chuca.memberservice.global.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public class NaverTokenDto { + private String access_token; // + private String refresh_token; + private String token_type; // 토큰 타입 (Bearer / MAC) + private Integer expires_in; // 유효기간 (초단위) + private String error; // 에러 코드 + private String error_description; // 에러 메시지 +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/dto/NaverUserDto.java b/member-service/src/main/java/com/chuca/memberservice/global/dto/NaverUserDto.java new file mode 100644 index 0000000..c09a68b --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/dto/NaverUserDto.java @@ -0,0 +1,19 @@ +package com.chuca.memberservice.global.dto; + +import lombok.Getter; + +@Getter +public class NaverUserDto { + private String resultcode; + private String message; + private UserInfo response; + + @Getter + public static class UserInfo { + private String id; // 고유 식별값 (동일인 식별 정보) + private String nickname; // 닉네임 + private String email; // 이메일 + private String profile_image; // 프로핊 이미지 + private String mobile; // 휴대폰 번호 + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/dto/PortOneDto.java b/member-service/src/main/java/com/chuca/memberservice/global/dto/PortOneDto.java new file mode 100644 index 0000000..6c933d5 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/dto/PortOneDto.java @@ -0,0 +1,18 @@ +package com.chuca.memberservice.global.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PortOneDto { + private Integer code; // 응답코드 + private String message; // 응답메시지 + private VbankHolderAnnotation response; + + @Getter + public static class VbankHolderAnnotation { + private String bank_holder; // 계좌의 예금주명 + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/dto/TokenDto.java b/member-service/src/main/java/com/chuca/memberservice/global/dto/TokenDto.java new file mode 100644 index 0000000..d9c4bfe --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/dto/TokenDto.java @@ -0,0 +1,31 @@ +package com.chuca.memberservice.global.dto; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TokenDto { + @Getter + @Builder + public static class Request { + private String imp_key; + private String imp_secret; + } + + @Getter + public static class Response { + private Integer code; // 응답 코드 + private String message; // 응답 메시지 + private AuthAnnotation response; + + @Getter + public static class AuthAnnotation { + private String access_token; // access_token + private Integer expired_at; // token 만료 시각 + private Integer now; // 현재 시각 + } + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/entity/BaseTime.java b/member-service/src/main/java/com/chuca/memberservice/global/entity/BaseTime.java new file mode 100644 index 0000000..7b9377b --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/entity/BaseTime.java @@ -0,0 +1,24 @@ +package com.chuca.memberservice.global.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTime { + @CreatedDate + @Column(name = "createdAt", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updatedAt", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/exception/BadRequestException.java b/member-service/src/main/java/com/chuca/memberservice/global/exception/BadRequestException.java new file mode 100644 index 0000000..87f3979 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/exception/BadRequestException.java @@ -0,0 +1,20 @@ +package com.chuca.memberservice.global.exception; + + +import com.chuca.memberservice.global.response.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class BadRequestException extends BaseException { + private String message; + + protected BadRequestException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } + + public BadRequestException(String message, HttpStatus httpStatus) { + super(ErrorCode.BAD_REQUEST, message, httpStatus); + this.message = message; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/exception/BaseException.java b/member-service/src/main/java/com/chuca/memberservice/global/exception/BaseException.java new file mode 100644 index 0000000..8dd0989 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/exception/BaseException.java @@ -0,0 +1,27 @@ +package com.chuca.memberservice.global.exception; + +import com.chuca.memberservice.global.response.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public abstract class BaseException extends RuntimeException { + + private final ErrorCode errorCode; + private final String message; + private final HttpStatus httpStatus; + + protected BaseException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.message = errorCode.getMessage(); + this.httpStatus = httpStatus; + } + + public BaseException(ErrorCode errorCode, String message, HttpStatus httpstatus) { + super(); + this.errorCode = errorCode; + this.message = message; + this.httpStatus = httpstatus; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/exception/ExceptionAdvice.java b/member-service/src/main/java/com/chuca/memberservice/global/exception/ExceptionAdvice.java new file mode 100644 index 0000000..3882226 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/exception/ExceptionAdvice.java @@ -0,0 +1,62 @@ +package com.chuca.memberservice.global.exception; + +import com.chuca.memberservice.global.response.ErrorResponse; +import com.chuca.memberservice.global.response.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.context.support.DefaultMessageSourceResolvable; + +import java.util.function.Consumer; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class ExceptionAdvice { + private static final String LOG_FORMAT = "Class : {}, Code : {}, Message : {}"; + + @ExceptionHandler(BaseException.class) + public ResponseEntity handleBaseException(BaseException ex) { + return handleException(ex, ex.getErrorCode(), ex.getMessage(), ex.getHttpStatus(), log::warn); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity inputMethodArgumentInvalidExceptionHandler (MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + return handleException(ex, ErrorCode.BAD_REQUEST, message, HttpStatus.BAD_REQUEST, log::warn); + } + + @ExceptionHandler(PatternSyntaxException.class) + public ResponseEntity inputPatternSyntaxExceptionHandler(PatternSyntaxException ex) { + return handleException(ex, ErrorCode.BAD_REQUEST, ErrorCode.BAD_REQUEST.getMessage(), HttpStatus.BAD_REQUEST, log::warn); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity jsonParseExceptionHandler(HttpMessageNotReadableException ex) { + return handleException(ex, ErrorCode.BAD_REQUEST, ErrorCode.BAD_REQUEST.getMessage(), HttpStatus.BAD_REQUEST, log::warn); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity httpRequestNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + return handleException(ex, ErrorCode.METHOD_NOT_ALLOWED, ErrorCode.METHOD_NOT_ALLOWED.getMessage(), HttpStatus.METHOD_NOT_ALLOWED, log::warn); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity internalServerErrorHandler(Exception ex) { + return handleException(ex, ErrorCode.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_SERVER_ERROR.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, log::error); + } + + private ResponseEntity handleException(Exception ex, ErrorCode errorCode, String message, HttpStatus httpStatus, Consumer logger) { + log.error(LOG_FORMAT, ex.getClass().getSimpleName(), errorCode.getErrorCode(), ex.getMessage()); + ErrorResponse errorResponse = new ErrorResponse(errorCode, message); + return ResponseEntity.status(httpStatus.value()).body(errorResponse); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/exception/UnauthorizedException.java b/member-service/src/main/java/com/chuca/memberservice/global/exception/UnauthorizedException.java new file mode 100644 index 0000000..0bece0a --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/exception/UnauthorizedException.java @@ -0,0 +1,19 @@ +package com.chuca.memberservice.global.exception; + +import com.chuca.memberservice.global.response.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class UnauthorizedException extends BaseException { + private String message; + + protected UnauthorizedException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } + + public UnauthorizedException(String message, HttpStatus httpStatus) { + super(ErrorCode.BAD_REQUEST, message, httpStatus); + this.message = message; + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/feign/BusinessManClient.java b/member-service/src/main/java/com/chuca/memberservice/global/feign/BusinessManClient.java new file mode 100644 index 0000000..1a761a3 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/feign/BusinessManClient.java @@ -0,0 +1,18 @@ +package com.chuca.memberservice.global.feign; + +import com.chuca.memberservice.global.config.FeignClientConfig; +import com.chuca.memberservice.global.dto.BusinessDto; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient( + name = "businessman-check-validate-client", + url = "https://api.odcloud.kr/api/nts-businessman/v1", + configuration = FeignClientConfig.class +) +public interface BusinessManClient { + @PostMapping("/validate") + BusinessDto.Response checkValidate(@RequestParam String serviceKey, @RequestBody BusinessDto.Request businesses); +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/feign/PortOneClient.java b/member-service/src/main/java/com/chuca/memberservice/global/feign/PortOneClient.java new file mode 100644 index 0000000..6d14b06 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/feign/PortOneClient.java @@ -0,0 +1,20 @@ +package com.chuca.memberservice.global.feign; + +import com.chuca.memberservice.global.config.FeignClientConfig; +import com.chuca.memberservice.global.dto.PortOneDto; +import com.chuca.memberservice.global.dto.TokenDto; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +@FeignClient( + name = "portone-client", + url = "https://api.iamport.kr", + configuration = FeignClientConfig.class +) +public interface PortOneClient { + @PostMapping("/users/getToken") + TokenDto.Response getToken(@RequestBody TokenDto.Request request); // access token 발급 + + @GetMapping("/vbanks/holder") + PortOneDto getBankHolder(@RequestHeader("Authorization") String accessToken, @RequestParam("bank_code") String code, @RequestParam("bank_num") String bank); // 계좌 실명 조회 +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/response/BaseResponse.java b/member-service/src/main/java/com/chuca/memberservice/global/response/BaseResponse.java new file mode 100644 index 0000000..350cfab --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/response/BaseResponse.java @@ -0,0 +1,26 @@ +package com.chuca.memberservice.global.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class BaseResponse { + private Boolean isSuccess; + private String message; + @JsonInclude(JsonInclude.Include.NON_NULL) + private T data; + + /* 요청이 성공한 경우 1 */ + public static BaseResponse create(T data) { + return new BaseResponse<>(true, "요청에 성공하였습니다.", data); + } + + /* 요청이 성공한 경우 2 */ + public static BaseResponse create(String message, T data) { + return new BaseResponse<>(true, message, data); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/response/ErrorCode.java b/member-service/src/main/java/com/chuca/memberservice/global/response/ErrorCode.java new file mode 100644 index 0000000..18a6e0f --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/response/ErrorCode.java @@ -0,0 +1,16 @@ +package com.chuca.memberservice.global.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + BAD_REQUEST("400", "입력값이 유효하지 않습니다."), + UNAUTHORIZED("401", "권한이 없습니다."), + METHOD_NOT_ALLOWED("405", "클라이언트가 사용한 HTTP 메서드가 리소스에서 허용되지 않습니다."), + INTERNAL_SERVER_ERROR("500", "서버에서 요청을 처리하는 동안 오류가 발생했습니다."); + + private String errorCode; + private String message; +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/response/ErrorResponse.java b/member-service/src/main/java/com/chuca/memberservice/global/response/ErrorResponse.java new file mode 100644 index 0000000..0197aed --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/response/ErrorResponse.java @@ -0,0 +1,39 @@ +package com.chuca.memberservice.global.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor +public class ErrorResponse { + private Boolean isSuccess; + private LocalDateTime timeStamp; + private String errorCode; + private String message; + + /* 요청에 실패한 경우 1 */ + public ErrorResponse(String errorCode, String message) { + this.isSuccess=false; + this.timeStamp = LocalDateTime.now().withNano(0); + this.errorCode = errorCode; + this.message = message; + } + + /* 요청에 실패한 경우 2 */ + public ErrorResponse(ErrorCode errorCode, String message) { + this.isSuccess=false; + this.timeStamp = LocalDateTime.now().withNano(0); + this.errorCode=errorCode.getErrorCode(); + this.message=message; + } + + /* 요청에 실패한 경우 3 */ + public ErrorResponse(ErrorCode errorCode) { + this.isSuccess=false; + this.timeStamp = LocalDateTime.now().withNano(0); + this.errorCode=errorCode.getErrorCode(); + this.message=errorCode.getMessage(); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/security/JwtAuthenticationFilter.java b/member-service/src/main/java/com/chuca/memberservice/global/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..90a96c5 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/security/JwtAuthenticationFilter.java @@ -0,0 +1,58 @@ +package com.chuca.memberservice.global.security; + +import io.jsonwebtoken.ExpiredJwtException; +import io.micrometer.common.util.StringUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +import java.io.IOException; + +// Jwt 토큰으로 인증하는 필터 +@Slf4j +public class JwtAuthenticationFilter extends BasicAuthenticationFilter { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + private JwtProvider jwtProvider; + + public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtProvider jwtProvider) { + super(authenticationManager); + this.jwtProvider =jwtProvider; + + } + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + // 헤더에서 토큰 가져오기 + String token = jwtProvider.resolveToken(request); + String requestURI = request.getRequestURI(); + + // 토큰이 존재 여부 + 토큰 검증 + if (StringUtils.isNotEmpty(token) && jwtProvider.validateToken(token)) { + log.info("토큰 검증"); + Authentication authentication = jwtProvider.getAuthentication(token); // 권한 + + // security 세션에 등록 + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI); + } + else if (StringUtils.isEmpty(token)){ // 널일때 + throw new NullPointerException(); + } + else if(!jwtProvider.validateToken(token)){ + log.debug("유효한 JWT 토큰이 없습니다, uri: {} ", requestURI); + throw new ExpiredJwtException(null, null, "유효한 JWT 토큰이 없습니다"); + } + + chain.doFilter(request, response); + + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/security/JwtExceptionFilter.java b/member-service/src/main/java/com/chuca/memberservice/global/security/JwtExceptionFilter.java new file mode 100644 index 0000000..e4a8a59 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/security/JwtExceptionFilter.java @@ -0,0 +1,65 @@ +package com.chuca.memberservice.global.security; + +import com.chuca.memberservice.global.exception.BadRequestException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtExceptionFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try{ + log.info("exception filter"); + doFilter(request,response,filterChain); + log.info("jwt success"); + } catch (NullPointerException e) { + final Map body = new HashMap<>(); + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 예외에 맞는 HTTP 상태 코드 설정 + response.setContentType("application/json"); + body.put("timestamp", LocalDateTime.now()); + body.put("error", "Bad Request"); + body.put("message", e.getMessage()); // 예외에 맞는 메시지 설정 + body.put("path", request.getRequestURI()); + + mapper.writeValue(response.getOutputStream(), body); + logger.info("jwt exception nullpointer"); + throw new BadRequestException("토큰값이 존재하지 않습니다.", HttpStatus.BAD_REQUEST); + } + catch (ExpiredJwtException e) { + final Map body = new HashMap<>(); + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + body.put("timestamp", LocalDateTime.now()); + body.put("error", "Unauthorized"); + body.put("message", e.getMessage()); + body.put("path", request.getRequestURI()); + + mapper.writeValue(response.getOutputStream(), body); + } + } +} \ No newline at end of file diff --git a/member-service/src/main/java/com/chuca/memberservice/global/security/JwtProvider.java b/member-service/src/main/java/com/chuca/memberservice/global/security/JwtProvider.java new file mode 100644 index 0000000..f0c8ffa --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/security/JwtProvider.java @@ -0,0 +1,163 @@ +package com.chuca.memberservice.global.security; + +import com.chuca.memberservice.domain.member.repository.MemberRepository; +import com.chuca.memberservice.domain.member.service.MemberDetailServiceImpl; +import com.chuca.memberservice.domain.owner.repository.OwnerRepository; +import com.chuca.memberservice.global.dto.JwtTokenDto; +import com.chuca.memberservice.global.security.JwtAuthenticationFilter; +import com.chuca.memberservice.global.util.RedisService; +import io.jsonwebtoken.*; +import io.jsonwebtoken.JwtException; +import io.micrometer.common.util.StringUtils; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Date; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JwtProvider { + + @Value("${jwt.secret}") + private String JWT_SECRET; + + // 일반 유저 + private final MemberDetailServiceImpl memberDetailService; + private final MemberRepository memberRepository; + private final OwnerRepository ownerRepository; + + private final RedisService redisService; + private final Long tokenValidTime = 1000L * 60 * 60; // 1h + private final Long refreshTokenValidTime = 1000L * 60 * 60 * 24 * 7; // 7d + + + // access token 생성 + public String encodeJwtToken(Long memberId) { + Date now = new Date(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer("chuca") + .setIssuedAt(now) + .setSubject(memberId.toString()) + .setExpiration(new Date(now.getTime() + tokenValidTime)) + .claim("memberId", memberId) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET) + .compact(); + } + + // refresh token 생성 + public String encodeJwtRefreshToken(Long memberId) { + Date now = new Date(); + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuedAt(now) + .setSubject(memberId.toString()) + .setExpiration(new Date(now.getTime() + refreshTokenValidTime)) + .claim("memberId", memberId) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET) + .compact(); + } + + // JWT 토큰으로부터 memberId 추출 + public Long getMemberIdFromJwtToken(String token) { + try { + Claims claims = Jwts.parser() + .setSigningKey(JWT_SECRET) + .parseClaimsJws(token) + .getBody(); + return Long.parseLong(claims.getSubject()); + } catch(Exception e) { + throw new io.jsonwebtoken.JwtException(e.getMessage()); + } + } + + + // Autorization : Bearer에서 token 추출 (refreshToken, accessToken 포함) + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(JwtAuthenticationFilter.AUTHORIZATION_HEADER); + if(bearerToken == null) + throw new NullPointerException(); + else if(StringUtils.isNotEmpty(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + // 토큰 유효성 + 만료일자 확인 (만료 여부만 확인. 에러 발생 x) + public boolean validateToken(String token) { + Date now = new Date(); + + try{ + // 주어진 토큰을 파싱하고 검증. + Jws claims = Jwts.parser() + .setSigningKey(JWT_SECRET) + .parseClaimsJws(token); + + Long memberId = claims.getBody().get("memberId", Long.class); + log.info("validateToken ------- memberId : " + memberId); + + /* + * 이미 로그아웃 or 하이재킹 당한 토큰으로 로그인 시도중인지 체크 + * 1. 이미 로그아웃한 경우 를 redis에 저장 + * 2. redis에 저장된 value가 memberId와 동일 -> 토큰 탈취 + */ + String expiredAt = redisService.getValues(token); + if(expiredAt != null && expiredAt.equals(String.valueOf(memberId))) { + log.info("Hijacking!!!!!!!"); + return false; + } + + return !claims.getBody().getExpiration().before(new Date(now.getTime())); + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("잘못된 JWT 서명입니다."); + } catch (ExpiredJwtException e) { + log.info("만료된 JWT 토큰입니다."); + } catch (UnsupportedJwtException e) { + log.info("지원되지 않는 JWT 토큰입니다."); + } catch (IllegalArgumentException e) { + log.info("JWT 토큰이 잘못되었습니다."); + } + return false; + } + + // JWT 토큰 인증 정보 조회 (토큰 복호화) + public Authentication getAuthentication(String token) { + UserDetails userDetails = memberDetailService.loadUserByUsername(this.getMemberIdFromJwtToken(token).toString()); + return new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities()); + } + + // refresh token redis에 저장 + public void storeJwtRefreshToken(Long memberId, String token) { + redisService.setValues(String.valueOf(memberId), token, Duration.ofSeconds(refreshTokenValidTime)); + } + + // 로그아웃을 위한 토큰 만료 + public void expireToken(Long memberId, String token) { + long expiredAccessTokenTime = getExpiredTime(token).getTime() - new Date().getTime(); + // 1. Redis에 액세스 토큰값을 key로 가지는 memberId 값 저장 (블랙리스트 처리) + redisService.setValues(token,String.valueOf(memberId), Duration.ofSeconds(expiredAccessTokenTime)); + // 2. Redis에 저장된 refreshToken 삭제 + redisService.deleteValues(String.valueOf(memberId)); + } + + // 토큰 유효 시간 계산 + public Date getExpiredTime(String token){ + return Jwts + .parser() + .setSigningKey(JWT_SECRET) + .parseClaimsJws(token) + .getBody() + .getExpiration(); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/security/SecurityConfig.java b/member-service/src/main/java/com/chuca/memberservice/global/security/SecurityConfig.java new file mode 100644 index 0000000..2569ccf --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/security/SecurityConfig.java @@ -0,0 +1,90 @@ +package com.chuca.memberservice.global.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +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.util.matcher.AntPathRequestMatcher; + + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final AuthenticationConfiguration authenticationConfiguration; + private final JwtProvider jwtProvider; + private final JwtExceptionFilter jwtExceptionFilter; + + // 비밀번호 암호화 + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + @Bean + @Order(0) + public WebSecurityCustomizer webSecurityCustomizer(){ + return web -> web.ignoring() + .requestMatchers("/swagger-ui/**", "/swagger/**", "/swagger-resources/**", "/swagger-ui.html", + + "/configuration/ui", "/v3/api-docs/**", "/h2-console/**", "/oauth/**", "/member/**"); + } + + //선언 방식이 3.x에서 바뀜 + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception { + return authConfiguration.getAuthenticationManager(); + } + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(Customizer.withDefaults()) + .sessionManagement((sessionManagement) -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) +// 세션을 사용하지 않는다고 설정함 + ) + .addFilter(new JwtAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtProvider)) +// JwtAuthenticationFilter를 필터에 넣음 + .authorizeHttpRequests((authorizeRequests) -> + authorizeRequests + .requestMatchers( + AntPathRequestMatcher.antMatcher("/oauth/**") + ).authenticated() + .requestMatchers( + AntPathRequestMatcher.antMatcher("/member/**") + ).authenticated() + .requestMatchers( + AntPathRequestMatcher.antMatcher("/h2-console/**") + ).permitAll() + + .anyRequest().authenticated() + ) + .headers( + headersConfigurer -> + headersConfigurer + .frameOptions( + HeadersConfigurer.FrameOptionsConfig::sameOrigin + ) + ) + .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/util/RedisService.java b/member-service/src/main/java/com/chuca/memberservice/global/util/RedisService.java new file mode 100644 index 0000000..57bef0d --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/util/RedisService.java @@ -0,0 +1,15 @@ +package com.chuca.memberservice.global.util; + +import java.time.Duration; + +public interface RedisService { + void setValues(String key, String data) ; + + void setValues(String key, String data, Duration duration) ; + + String getValues(String key) ; + + void deleteValues(String key) ; + + void saveValues(String key, String value, long time); +} diff --git a/member-service/src/main/java/com/chuca/memberservice/global/util/RedisServiceImpl.java b/member-service/src/main/java/com/chuca/memberservice/global/util/RedisServiceImpl.java new file mode 100644 index 0000000..87b5584 --- /dev/null +++ b/member-service/src/main/java/com/chuca/memberservice/global/util/RedisServiceImpl.java @@ -0,0 +1,39 @@ +package com.chuca.memberservice.global.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class RedisServiceImpl implements RedisService { + private final RedisTemplate redisTemplate; + + public void setValues(String key, String data) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data); + } + + @Override + public void setValues(String key, String data, Duration duration) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data, duration); + } + + public String getValues(String key) { + ValueOperations values = redisTemplate.opsForValue(); + return values.get(key); + } + + public void deleteValues(String key) { + redisTemplate.delete(key); + } + + public void saveValues(String key, String value, long time){ + redisTemplate.opsForValue().set(key, value, time, TimeUnit.MILLISECONDS); + } +} diff --git a/member-service/src/main/resources/application-local.yml b/member-service/src/main/resources/application-local.yml index b944a92..7237876 100644 --- a/member-service/src/main/resources/application-local.yml +++ b/member-service/src/main/resources/application-local.yml @@ -26,13 +26,19 @@ spring: default_batch_fetch_size: 1000 show-sql: true - # redis: - # host: localhost - # port: 6379 + data: + redis: + host: localhost + port: 6379 config: activate: on-profile: local + +jwt: + secret: ${JWT_SECRET_KEY} + + # import: # application-secret.yml @@ -87,7 +93,4 @@ security: redirect-uri: http://localhost:8081/google/callback scope: - email - - profile - -jwt: - secret: ${JWT_SECRET_KEY} \ No newline at end of file + - profile \ No newline at end of file