diff --git a/build.gradle b/build.gradle index ea7d1d54..6c4ff5df 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,14 @@ dependencies { // https://mvnrepository.com/artifact/org.postgresql/postgresql implementation 'org.postgresql:postgresql:42.7.1' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // gson + implementation 'com.google.code.gson:gson:2.8.6' + // restdocs-swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' @@ -85,4 +93,4 @@ openapi3 { outputFileNamePrefix = 'open-api-3.0.1' format = 'json' outputDirectory = 'build/resources/main/static/docs' -} \ No newline at end of file +} diff --git a/src/main/java/com/soptie/server/auth/controller/AuthController.java b/src/main/java/com/soptie/server/auth/controller/AuthController.java new file mode 100644 index 00000000..7b643e68 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/controller/AuthController.java @@ -0,0 +1,26 @@ +package com.soptie.server.auth.controller; + +import com.soptie.server.auth.dto.SignInRequest; +import com.soptie.server.auth.message.ResponseMessage; +import com.soptie.server.auth.service.AuthService; +import com.soptie.server.common.dto.Response; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.soptie.server.common.dto.Response.success; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/v1/auth") +public class AuthController { + + private final AuthService authService; + + @PostMapping + public ResponseEntity signIn(@RequestHeader("Authorization") String socialAccessToken, @RequestBody SignInRequest request) { + val response = authService.signIn(socialAccessToken, request); + return ResponseEntity.ok(success(ResponseMessage.SUCCESS_SIGNIN.getMessage(), response)); + } +} diff --git a/src/main/java/com/soptie/server/auth/dto/SignInRequest.java b/src/main/java/com/soptie/server/auth/dto/SignInRequest.java new file mode 100644 index 00000000..eb4f3e3d --- /dev/null +++ b/src/main/java/com/soptie/server/auth/dto/SignInRequest.java @@ -0,0 +1,12 @@ +package com.soptie.server.auth.dto; + +import com.soptie.server.member.entity.SocialType; + +public record SignInRequest( + SocialType socialType +) { + + public static SignInRequest of(SocialType socialType) { + return new SignInRequest(socialType); + } +} diff --git a/src/main/java/com/soptie/server/auth/dto/SignInResponse.java b/src/main/java/com/soptie/server/auth/dto/SignInResponse.java new file mode 100644 index 00000000..ba1b1aeb --- /dev/null +++ b/src/main/java/com/soptie/server/auth/dto/SignInResponse.java @@ -0,0 +1,18 @@ +package com.soptie.server.auth.dto; + +import com.soptie.server.auth.vo.Token; +import lombok.Builder; + +@Builder +public record SignInResponse( + String accessToken, + String refreshToken +) { + + public static SignInResponse of(Token token) { + return SignInResponse.builder() + .accessToken(token.getAccessToken()) + .refreshToken(token.getRefreshToken()) + .build(); + } +} diff --git a/src/main/java/com/soptie/server/auth/jwt/CustomJwtAuthenticationEntryPoint.java b/src/main/java/com/soptie/server/auth/jwt/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..f43c6fb5 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/jwt/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,35 @@ +package com.soptie.server.auth.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.soptie.server.common.dto.Response; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static com.soptie.server.auth.message.ErrorMessage.INVALID_TOKEN; +import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Component +@RequiredArgsConstructor +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) throws IOException { + response.setCharacterEncoding("UTF-8"); + response.setContentType(APPLICATION_JSON_VALUE); + response.setStatus(SC_UNAUTHORIZED); + response.getWriter().println(objectMapper.writeValueAsString(Response.fail(INVALID_TOKEN.getMessage()))); + } +} diff --git a/src/main/java/com/soptie/server/auth/jwt/JwtAuthenticationFilter.java b/src/main/java/com/soptie/server/auth/jwt/JwtAuthenticationFilter.java new file mode 100644 index 00000000..3e458ec5 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,63 @@ +package com.soptie.server.auth.jwt; + +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 lombok.val; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.soptie.server.auth.jwt.JwtValidationType.VALID_JWT; +import static io.jsonwebtoken.lang.Strings.hasText; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String BEARER_HEADER = "Bearer "; + private static final String BLANK = ""; + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + val token = getAccessTokenFromRequest(request); + if (hasText(token) && jwtTokenProvider.validateToken(token) == VALID_JWT) { + val authentication = new UserAuthentication(getMemberId(token), null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception exception) { + log.error(exception.getMessage()); + } + + filterChain.doFilter(request, response); + } + + private Long getMemberId(String token) { + return jwtTokenProvider.getUserFromJwt(token); + } + + private String getAccessTokenFromRequest(HttpServletRequest request) { + return isContainsAccessToken(request) ? getAuthorizationAccessToken(request) : null; + } + + private boolean isContainsAccessToken(HttpServletRequest request) { + val authorization = request.getHeader(AUTHORIZATION); + return authorization != null && authorization.startsWith(BEARER_HEADER); + } + + private String getAuthorizationAccessToken(HttpServletRequest request) { + return request.getHeader(AUTHORIZATION).replaceFirst(BEARER_HEADER, BLANK); + } +} diff --git a/src/main/java/com/soptie/server/auth/jwt/JwtTokenProvider.java b/src/main/java/com/soptie/server/auth/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..29bbdcc5 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/jwt/JwtTokenProvider.java @@ -0,0 +1,78 @@ +package com.soptie.server.auth.jwt; + +import com.soptie.server.common.config.ValueConfig; +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +import static com.soptie.server.auth.jwt.JwtValidationType.*; +import static io.jsonwebtoken.Header.*; +import static io.jsonwebtoken.security.Keys.hmacShaKeyFor; +import static java.util.Base64.getEncoder; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final ValueConfig valueConfig; + + public String generateToken(Authentication authentication, Long expiration) { + return Jwts.builder() + .setHeaderParam(TYPE, JWT_TYPE) + .setClaims(generateClaims(authentication)) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + public JwtValidationType validateToken(String token) { + try { + getBody(token); + return VALID_JWT; + } catch (MalformedJwtException exception) { + log.error(exception.getMessage()); + return INVALID_JWT_TOKEN; + } catch (ExpiredJwtException exception) { + log.error(exception.getMessage()); + return EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException exception) { + log.error(exception.getMessage()); + return UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException exception) { + log.error(exception.getMessage()); + return EMPTY_JWT; + } + } + + private Claims generateClaims(Authentication authentication) { + val claims = Jwts.claims(); + claims.put("memberId", authentication.getPrincipal()); + return claims; + } + + public Long getUserFromJwt(String token) { + val claims = getBody(token); + return Long.parseLong(claims.get("memberId").toString()); + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private SecretKey getSigningKey() { + val encodedKey = getEncoder().encodeToString(valueConfig.getSecretKey().getBytes()); + return hmacShaKeyFor(encodedKey.getBytes()); + } +} diff --git a/src/main/java/com/soptie/server/auth/jwt/JwtValidationType.java b/src/main/java/com/soptie/server/auth/jwt/JwtValidationType.java new file mode 100644 index 00000000..d51ca861 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/jwt/JwtValidationType.java @@ -0,0 +1,10 @@ +package com.soptie.server.auth.jwt; + +public enum JwtValidationType { + VALID_JWT, + INVALID_JWT_SIGNATURE, + INVALID_JWT_TOKEN, + EXPIRED_JWT_TOKEN, + UNSUPPORTED_JWT_TOKEN, + EMPTY_JWT +} diff --git a/src/main/java/com/soptie/server/auth/jwt/UserAuthentication.java b/src/main/java/com/soptie/server/auth/jwt/UserAuthentication.java new file mode 100644 index 00000000..3e07e766 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/jwt/UserAuthentication.java @@ -0,0 +1,13 @@ +package com.soptie.server.auth.jwt; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } +} diff --git a/src/main/java/com/soptie/server/auth/message/ErrorMessage.java b/src/main/java/com/soptie/server/auth/message/ErrorMessage.java new file mode 100644 index 00000000..6b309d0c --- /dev/null +++ b/src/main/java/com/soptie/server/auth/message/ErrorMessage.java @@ -0,0 +1,17 @@ +package com.soptie.server.auth.message; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum ErrorMessage { + + EMPTY_ACCESS_TOKEN("액세스 토큰이 없습니다."), + EMPTY_REFRESH_TOKEN("리프레시 토큰이 없습니다"), + INVALID_TOKEN("유효하지 않은 토큰입니다"), + MESSAGE_UNAUTHORIZED("유효하지 않은 토큰"), + ; + + private final String message; +} diff --git a/src/main/java/com/soptie/server/auth/message/ResponseMessage.java b/src/main/java/com/soptie/server/auth/message/ResponseMessage.java new file mode 100644 index 00000000..43b63fd7 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/message/ResponseMessage.java @@ -0,0 +1,13 @@ +package com.soptie.server.auth.message; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum ResponseMessage { + + SUCCESS_SIGNIN("소셜로그인 성공"); + + private final String message; +} diff --git a/src/main/java/com/soptie/server/auth/service/AuthService.java b/src/main/java/com/soptie/server/auth/service/AuthService.java new file mode 100644 index 00000000..c9f3165a --- /dev/null +++ b/src/main/java/com/soptie/server/auth/service/AuthService.java @@ -0,0 +1,9 @@ +package com.soptie.server.auth.service; + +import com.soptie.server.auth.dto.SignInRequest; +import com.soptie.server.auth.dto.SignInResponse; + +public interface AuthService { + + SignInResponse signIn(String socialAccessToken, SignInRequest request); +} diff --git a/src/main/java/com/soptie/server/auth/service/AuthServiceImpl.java b/src/main/java/com/soptie/server/auth/service/AuthServiceImpl.java new file mode 100644 index 00000000..0810cfb7 --- /dev/null +++ b/src/main/java/com/soptie/server/auth/service/AuthServiceImpl.java @@ -0,0 +1,74 @@ +package com.soptie.server.auth.service; + +import com.soptie.server.auth.dto.SignInRequest; +import com.soptie.server.auth.dto.SignInResponse; +import com.soptie.server.auth.jwt.JwtTokenProvider; +import com.soptie.server.auth.jwt.UserAuthentication; +import com.soptie.server.auth.vo.Token; +import com.soptie.server.common.config.ValueConfig; +import com.soptie.server.member.entity.Member; +import com.soptie.server.member.entity.SocialType; +import com.soptie.server.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.soptie.server.auth.message.ErrorMessage.INVALID_TOKEN; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthServiceImpl implements AuthService { + + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + private final KakaoService kakaoService; + private final ValueConfig valueConfig; + + @Override + @Transactional + public SignInResponse signIn(String socialAccessToken, SignInRequest request) { + return SignInResponse.of(getToken(getMember(socialAccessToken, request))); + } + + private Member getMember(String socialAccessToken, SignInRequest request) { + val socialType = request.socialType(); + val socialId = getSocialId(socialAccessToken, socialType); + return signUp(socialType, socialId); + } + + private String getSocialId(String socialAccessToken, SocialType socialType) { + return switch (socialType) { + case KAKAO -> kakaoService.getKakaoData(socialAccessToken); + default -> throw new IllegalArgumentException(INVALID_TOKEN.getMessage()); + }; + } + + private Member signUp(SocialType socialType, String socialId) { + return memberRepository.findBySocialTypeAndSocialId(socialType, socialId) + .orElseGet(() -> saveMember(socialType, socialId)); + } + + private Member saveMember(SocialType socialType, String socialId) { + val member = Member.builder() + .socialType(socialType) + .socialId(socialId) + .build(); + return memberRepository.save(member); + } + + private Token getToken(Member member) { + val token = generateToken(new UserAuthentication(member.getId(), null, null)); + member.updateRefreshToken(token.getRefreshToken()); + return token; + } + + private Token generateToken(Authentication authentication) { + return Token.builder() + .accessToken(jwtTokenProvider.generateToken(authentication, valueConfig.getAccessTokenExpired())) + .refreshToken(jwtTokenProvider.generateToken(authentication, valueConfig.getRefreshTokenExpired())) + .build(); + } +} diff --git a/src/main/java/com/soptie/server/auth/service/KakaoService.java b/src/main/java/com/soptie/server/auth/service/KakaoService.java new file mode 100644 index 00000000..ee39320c --- /dev/null +++ b/src/main/java/com/soptie/server/auth/service/KakaoService.java @@ -0,0 +1,6 @@ +package com.soptie.server.auth.service; + +public interface KakaoService { + + String getKakaoData(String socialAccessToken); +} diff --git a/src/main/java/com/soptie/server/auth/service/KakaoServiceImpl.java b/src/main/java/com/soptie/server/auth/service/KakaoServiceImpl.java new file mode 100644 index 00000000..4d443c8c --- /dev/null +++ b/src/main/java/com/soptie/server/auth/service/KakaoServiceImpl.java @@ -0,0 +1,31 @@ +package com.soptie.server.auth.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.JsonArray; +import com.soptie.server.common.config.ValueConfig; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class KakaoServiceImpl implements KakaoService { + + private final ValueConfig valueConfig; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + @Override + public String getKakaoData(String socialAccessToken) { + val headers = new HttpHeaders(); + headers.add("Authorization", socialAccessToken); + val httpEntity = new HttpEntity(headers); + val responseData = restTemplate.postForEntity(valueConfig.getUserInfoUri(), httpEntity, Object.class); + return objectMapper.convertValue(responseData.getBody(), Map.class).get("id").toString(); + } +} diff --git a/src/main/java/com/soptie/server/auth/vo/Token.java b/src/main/java/com/soptie/server/auth/vo/Token.java new file mode 100644 index 00000000..76f17d0a --- /dev/null +++ b/src/main/java/com/soptie/server/auth/vo/Token.java @@ -0,0 +1,32 @@ +package com.soptie.server.auth.vo; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Objects; + +@Getter +public class Token { + + private final String accessToken; + private final String refreshToken; + + @Builder + public Token(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Token token = (Token) o; + return Objects.equals(accessToken, token.accessToken) && Objects.equals(refreshToken, token.refreshToken); + } + + @Override + public int hashCode() { + return Objects.hash(accessToken, refreshToken); + } +} diff --git a/src/main/java/com/soptie/server/common/config/RestTemplateConfig.java b/src/main/java/com/soptie/server/common/config/RestTemplateConfig.java new file mode 100644 index 00000000..b0102b7f --- /dev/null +++ b/src/main/java/com/soptie/server/common/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.soptie.server.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/soptie/server/common/config/SecurityConfig.java b/src/main/java/com/soptie/server/common/config/SecurityConfig.java new file mode 100644 index 00000000..65710df0 --- /dev/null +++ b/src/main/java/com/soptie/server/common/config/SecurityConfig.java @@ -0,0 +1,44 @@ +package com.soptie.server.common.config; + +import com.soptie.server.auth.jwt.CustomJwtAuthenticationEntryPoint; +import com.soptie.server.auth.jwt.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagement -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .exceptionHandling(exceptionHandling -> + exceptionHandling.authenticationEntryPoint(customJwtAuthenticationEntryPoint)) + .authorizeHttpRequests(authorizeHttpRequests -> + authorizeHttpRequests + .requestMatchers(new AntPathRequestMatcher("/api/v1/auth/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/v1/test")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() + .anyRequest().authenticated()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } +} diff --git a/src/main/java/com/soptie/server/common/config/ValueConfig.java b/src/main/java/com/soptie/server/common/config/ValueConfig.java new file mode 100644 index 00000000..e48ca9eb --- /dev/null +++ b/src/main/java/com/soptie/server/common/config/ValueConfig.java @@ -0,0 +1,43 @@ +package com.soptie.server.common.config; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@Configuration +@Getter +public class ValueConfig { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${spring.security.oauth2.client.registration.kakao.client-id") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri") + private String redirectUri; + + @Value("${spring.security.oauth2.client.registration.kakao.client-secret") + private String clientSecret; + + @Value("${spring.security.oauth2.client.provider.kakao.token-uri") + private String tokenUri; + + @Value("${jwt.KAKAO_URL}") + private String userInfoUri; + + @Value("${jwt.ACCESS_TOKEN_EXPIRED}") + private Long accessTokenExpired; + + @Value("${jwt.REFRESH_TOKEN_EXPIRED}") + private Long refreshTokenExpired; + + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/soptie/server/member/entity/Cotton.java b/src/main/java/com/soptie/server/member/entity/Cotton.java index 98c3b20f..0b3d21a2 100644 --- a/src/main/java/com/soptie/server/member/entity/Cotton.java +++ b/src/main/java/com/soptie/server/member/entity/Cotton.java @@ -1,8 +1,12 @@ package com.soptie.server.member.entity; import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; @Embeddable +@AllArgsConstructor +@NoArgsConstructor public class Cotton { private int dailyCottonCount; private int happinessCottonCount; diff --git a/src/main/java/com/soptie/server/member/entity/Member.java b/src/main/java/com/soptie/server/member/entity/Member.java index be1d2f97..66ff0a11 100644 --- a/src/main/java/com/soptie/server/member/entity/Member.java +++ b/src/main/java/com/soptie/server/member/entity/Member.java @@ -19,6 +19,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -52,6 +53,13 @@ public class Member extends BaseTime { @JoinColumn(name = "happiness_routine_id") private MemberHappinessRoutine happinessRoutine; + @Builder + public Member(SocialType socialType, String socialId) { + this.socialType = socialType; + this.socialId = socialId; + this.cottonInfo = new Cotton(0, 0); + } + public void initHappinessRoutine() { this.happinessRoutine = null; } @@ -59,4 +67,8 @@ public void initHappinessRoutine() { public void addHappinessRoutine(MemberHappinessRoutine happinessRoutine) { this.happinessRoutine = happinessRoutine; } + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } } diff --git a/src/main/java/com/soptie/server/member/message/ErrorMessage.java b/src/main/java/com/soptie/server/member/message/ErrorMessage.java new file mode 100644 index 00000000..44e5f515 --- /dev/null +++ b/src/main/java/com/soptie/server/member/message/ErrorMessage.java @@ -0,0 +1,16 @@ +package com.soptie.server.member.message; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum ErrorMessage { + + EMPTY_MEMBER("존재하지 않는 회원입니다."), + DUPLICATE_USERNAME("이미 존재하는 닉네임입니다."), + INVALID_MEMBER("유효하지 않은 회원입니다."), + INVALID_USERNAME("유효하지 않은 닉네임입니다."); + + private final String message; +} diff --git a/src/main/java/com/soptie/server/member/repository/MemberRepository.java b/src/main/java/com/soptie/server/member/repository/MemberRepository.java new file mode 100644 index 00000000..ba0e6b6d --- /dev/null +++ b/src/main/java/com/soptie/server/member/repository/MemberRepository.java @@ -0,0 +1,14 @@ +package com.soptie.server.member.repository; + +import com.soptie.server.member.entity.Member; +import com.soptie.server.member.entity.SocialType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + boolean existsBySocialTypeAndSocialId(SocialType socialType, String socialId); + + Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c313baf7..3d10096b 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -16,6 +16,24 @@ spring: format_sql: true default_batch_fetch_size: 1000 auto_quote_keyword: true + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO.CLIENT_ID} + redirect-uri: ${KAKAO.REDIRECT_URI} + authorization-grant-type: authorization_code + scope: profile_nickname + client-name: Kakao + client-secret: ${KAKAO.CLIENT_SECRET} + client-authentication-method: POST + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id logging: level: @@ -36,4 +54,4 @@ springdoc: default-produces-media-type: application/json;charset=UTF-8 swagger-ui: url: /docs/open-api-3.0.1.json - path: /swagger \ No newline at end of file + path: /swagger