diff --git a/build.gradle b/build.gradle index 8e9da50..74f51d2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,43 +1,56 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '3.3.5' + id 'io.spring.dependency-management' version '1.1.6' } group = 'com.hackathon' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-common:2.1.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/java/com/hackathon/momento/auth/api/AuthController.java b/src/main/java/com/hackathon/momento/auth/api/AuthController.java new file mode 100644 index 0000000..33f32d6 --- /dev/null +++ b/src/main/java/com/hackathon/momento/auth/api/AuthController.java @@ -0,0 +1,39 @@ +package com.hackathon.momento.auth.api; + +import com.hackathon.momento.auth.api.dto.AuthResDto; +import com.hackathon.momento.global.oauth.KakaoOauthService; +import com.hackathon.momento.global.template.RspTemplate; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +@Tag(name = "회원가입/로그인", description = "회원가입/로그인을 담당하는 api 그룹") +@RequestMapping("/api/v1/auth") +public class AuthController { + + private final KakaoOauthService kakaoOAuthService; + + @GetMapping("/callback") + @Operation( + summary = "카카오 회원가입/로그인 콜백", + description = "카카오 로그인 후 리다이렉션된 URI입니다. 인가 코드를 받아서 accessToken을 요청하고, 회원가입 또는 로그인을 처리합니다.", + responses = { + @ApiResponse(responseCode = "200", description = "회원가입/로그인 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "500", description = "관리자 문의") + } + ) + public RspTemplate kakaoCallback(@RequestParam(name = "code") String code) { + AuthResDto token = kakaoOAuthService.signUpOrLogin(code); + return new RspTemplate<>(HttpStatus.OK, "토큰 발급", token); + } +} diff --git a/src/main/java/com/hackathon/momento/auth/api/dto/AuthResDto.java b/src/main/java/com/hackathon/momento/auth/api/dto/AuthResDto.java new file mode 100644 index 0000000..7d68752 --- /dev/null +++ b/src/main/java/com/hackathon/momento/auth/api/dto/AuthResDto.java @@ -0,0 +1,16 @@ +package com.hackathon.momento.auth.api.dto; + +import lombok.Builder; + +@Builder +public record AuthResDto( + String accessToken, + String refreshToken +) { + public static AuthResDto of(String accessToken, String refreshToken) { + return AuthResDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/hackathon/momento/auth/application/TokenRenewService.java b/src/main/java/com/hackathon/momento/auth/application/TokenRenewService.java new file mode 100644 index 0000000..ba8c5b0 --- /dev/null +++ b/src/main/java/com/hackathon/momento/auth/application/TokenRenewService.java @@ -0,0 +1,30 @@ +package com.hackathon.momento.auth.application; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class TokenRenewService { + + private static final String REFRESH_TOKEN_PREFIX = "refreshToken:"; + + private final RedisTemplate redisTemplate; + + @Transactional + public void saveRefreshToken(String refreshToken, Long memberId) { + deleteExistingToken(memberId); + + String key = REFRESH_TOKEN_PREFIX + memberId; + redisTemplate.opsForValue().set(key, refreshToken); + } + + private void deleteExistingToken(Long memberId) { + String key = REFRESH_TOKEN_PREFIX + memberId; + redisTemplate.delete(key); + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathon/momento/global/config/RedisConfig.java b/src/main/java/com/hackathon/momento/global/config/RedisConfig.java new file mode 100644 index 0000000..9b13f4c --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/config/RedisConfig.java @@ -0,0 +1,33 @@ +package com.hackathon.momento.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.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } +} diff --git a/src/main/java/com/hackathon/momento/global/config/SecurityConfig.java b/src/main/java/com/hackathon/momento/global/config/SecurityConfig.java new file mode 100644 index 0000000..43fd9f2 --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/config/SecurityConfig.java @@ -0,0 +1,44 @@ +package com.hackathon.momento.global.config; + +import com.hackathon.momento.global.jwt.JwtFilter; +import com.hackathon.momento.global.jwt.TokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final TokenProvider tokenprovider; + private final String[] PERMIT_ALL_URLS = { + "swagger-ui/**", + "v3/api-docs/**", + "api/v1/auth/**" + }; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + return httpSecurity + .httpBasic(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers(PERMIT_ALL_URLS).permitAll() + .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") // 관리자만 해당 URL에 접근 가능 + .anyRequest().authenticated() + ) + .addFilterBefore(new JwtFilter(tokenprovider), UsernamePasswordAuthenticationFilter.class) + .build(); + } +} diff --git a/src/main/java/com/hackathon/momento/global/config/SwaggerConfig.java b/src/main/java/com/hackathon/momento/global/config/SwaggerConfig.java new file mode 100644 index 0000000..b32cb9f --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/config/SwaggerConfig.java @@ -0,0 +1,35 @@ +package com.hackathon.momento.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + private Info apiInfo() { + return new Info() + .version("v1.0.0") + .title("Momento API") + .description("Momento API 명세서"); + } + + @Bean + public OpenAPI openAPI() { + String authHeader = "Authorization"; + + return new OpenAPI() + .info(apiInfo()) + .addSecurityItem(new SecurityRequirement().addList(authHeader)) + .components(new Components() + .addSecuritySchemes(authHeader, new SecurityScheme() + .name(authHeader) + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT"))); + } +} diff --git a/src/main/java/com/hackathon/momento/global/entity/BaseEntity.java b/src/main/java/com/hackathon/momento/global/entity/BaseEntity.java new file mode 100644 index 0000000..2942ea3 --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/entity/BaseEntity.java @@ -0,0 +1,24 @@ +package com.hackathon.momento.global.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseEntity { + + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/hackathon/momento/global/jwt/JwtFilter.java b/src/main/java/com/hackathon/momento/global/jwt/JwtFilter.java new file mode 100644 index 0000000..e5a7cb7 --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/jwt/JwtFilter.java @@ -0,0 +1,32 @@ +package com.hackathon.momento.global.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +@RequiredArgsConstructor +public class JwtFilter extends GenericFilterBean { + + private final TokenProvider tokenProvider; + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + + String token = tokenProvider.resolveToken((HttpServletRequest) request); + + if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) { + Authentication authentication = tokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/hackathon/momento/global/jwt/TokenProvider.java b/src/main/java/com/hackathon/momento/global/jwt/TokenProvider.java new file mode 100644 index 0000000..59b5933 --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/jwt/TokenProvider.java @@ -0,0 +1,112 @@ +package com.hackathon.momento.global.jwt; + +import com.hackathon.momento.member.domain.Member; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import jakarta.servlet.http.HttpServletRequest; +import java.security.Key; +import java.util.Collections; +import java.util.Date; +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.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Slf4j +@Component +public class TokenProvider { + + private final Key key; + private final long accessTokenValidityTime; + private final long refreshTokenValidityTime; + + public TokenProvider(@Value("${jwt.secret}") String secretKey, + @Value("${jwt.access-token-validity-in-milliseconds}") long accessTokenValidityTime, + @Value("${jwt.refresh-token-validity-in-milliseconds}") long refreshTokenValidityTime) { + this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); + this.accessTokenValidityTime = accessTokenValidityTime; + this.refreshTokenValidityTime = refreshTokenValidityTime; + } + + public String createAccessToken(Member member) { + return createToken(member, accessTokenValidityTime); + } + + public String createRefreshToken(Member member) { + return createToken(member, refreshTokenValidityTime); + } + + private String createToken(Member member, long validityTime) { + Date now = new Date(); + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("Role", member.getRoleType().name()) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + validityTime)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + private Claims parseClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Authentication getAuthentication(String accessToken) { + Claims claims = parseClaims(accessToken); + String roleName = claims.get("Role", String.class); + + if (roleName == null) { + log.error("Role extraction failed."); + } + + GrantedAuthority authority = new SimpleGrantedAuthority(roleName); + return new UsernamePasswordAuthenticationToken(claims.getSubject(), "", Collections.singletonList(authority)); + } + + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + + return null; + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + return true; + } catch (ExpiredJwtException e) { + log.error("JWT expired."); + } catch (SignatureException e) { + log.error("Invalid JWT signature."); + } catch (UnsupportedJwtException | MalformedJwtException e) { + log.error("Invalid JWT format."); + } catch (IllegalArgumentException e) { + log.error("JWT is empty or blank."); + } catch (Exception e) { + log.error("JWT validation failed.", e); + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/hackathon/momento/global/oauth/KakaoOauthService.java b/src/main/java/com/hackathon/momento/global/oauth/KakaoOauthService.java new file mode 100644 index 0000000..e468d4c --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/oauth/KakaoOauthService.java @@ -0,0 +1,117 @@ +package com.hackathon.momento.global.oauth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hackathon.momento.auth.api.dto.AuthResDto; +import com.hackathon.momento.auth.application.TokenRenewService; +import com.hackathon.momento.global.jwt.TokenProvider; +import com.hackathon.momento.global.oauth.exception.OauthException; +import com.hackathon.momento.member.domain.Member; +import com.hackathon.momento.member.domain.RoleType; +import com.hackathon.momento.member.domain.repository.MemberRepository; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class KakaoOauthService { + + @Value("${oauth.kakao.client-id}") + private String KAKAO_CLIENT_ID; + + @Value("${oauth.kakao.redirect-uri}") + private String KAKAO_REDIRECT_URI; + + @Value("${oauth.kakao.token-url}") + private String KAKAO_TOKEN_URL; + + private final MemberRepository memberRepository; + private final TokenProvider tokenProvider; + private final TokenRenewService tokenRenewService; + private final ObjectMapper objectMapper; + + @Transactional + public AuthResDto signUpOrLogin(String code) { + try { + String kakaoAccessToken = getKakaoAccessToken(code); + String[] memberInfo = getMemberInfo(kakaoAccessToken); + + Member member = memberRepository.findByEmail(memberInfo[0]) + .orElseGet(() -> memberRepository.save(Member.builder() + .email(memberInfo[0]) + .name(memberInfo[1]) + .roleType(RoleType.ROLE_USER) + .build()) + ); + + String accessToken = tokenProvider.createAccessToken(member); + String refreshToken = tokenProvider.createRefreshToken(member); + tokenRenewService.saveRefreshToken(refreshToken, member.getId()); + + return AuthResDto.of(accessToken, refreshToken); + } catch (JsonProcessingException e) { + throw new OauthException(); + } + } + + private String getKakaoAccessToken(String code) throws JsonProcessingException { + RestTemplate restTemplate = new RestTemplate(); + + String url = UriComponentsBuilder.fromHttpUrl(KAKAO_TOKEN_URL) + .queryParam("grant_type", "authorization_code") + .queryParam("client_id", KAKAO_CLIENT_ID) + .queryParam("redirect_uri", KAKAO_REDIRECT_URI) + .queryParam("code", code) + .toUriString(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity reqEntity = new HttpEntity<>(headers); + ResponseEntity resEntity = restTemplate.exchange(url, HttpMethod.POST, reqEntity, String.class); + + if (!resEntity.getStatusCode().is2xxSuccessful()) { + throw new OauthException(); + } + + String json = resEntity.getBody(); + JsonNode jsonNode = objectMapper.readTree(json); + + return jsonNode.get("access_token").asText(); + } + + private String[] getMemberInfo(String kakaoAccessToken) throws JsonProcessingException { + RestTemplate restTemplate = new RestTemplate(); + String url = "https://kapi.kakao.com/v2/user/me?access_token=" + kakaoAccessToken; + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + kakaoAccessToken); + headers.setContentType(MediaType.APPLICATION_JSON); + + RequestEntity reqEntity = new RequestEntity<>(headers, HttpMethod.GET, URI.create(url)); + ResponseEntity resEntity = restTemplate.exchange(reqEntity, String.class); + + if (!resEntity.getStatusCode().is2xxSuccessful()) { + throw new OauthException(); + } + + JsonNode jsonNode = objectMapper.readTree(resEntity.getBody()); + String email = jsonNode.path("kakao_account").path("email").asText(); + String name = jsonNode.path("kakao_account").path("profile").path("nickname").asText(); + + return new String[]{email, name}; + } +} diff --git a/src/main/java/com/hackathon/momento/global/oauth/exception/OauthException.java b/src/main/java/com/hackathon/momento/global/oauth/exception/OauthException.java new file mode 100644 index 0000000..f58bc68 --- /dev/null +++ b/src/main/java/com/hackathon/momento/global/oauth/exception/OauthException.java @@ -0,0 +1,13 @@ +package com.hackathon.momento.global.oauth.exception; + +import com.hackathon.momento.global.error.exception.AuthGroupException; + +public class OauthException extends AuthGroupException { + public OauthException(String message) { + super(message); + } + + public OauthException() { + this("카카오 서버와의 통신 과정에서 문제가 발생했습니다."); + } +} diff --git a/src/main/java/com/hackathon/momento/member/Member.java b/src/main/java/com/hackathon/momento/member/domain/Member.java similarity index 89% rename from src/main/java/com/hackathon/momento/member/Member.java rename to src/main/java/com/hackathon/momento/member/domain/Member.java index a1e022c..9f869a6 100644 --- a/src/main/java/com/hackathon/momento/member/Member.java +++ b/src/main/java/com/hackathon/momento/member/domain/Member.java @@ -1,5 +1,6 @@ -package com.hackathon.momento.member; +package com.hackathon.momento.member.domain; +import com.hackathon.momento.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -15,7 +16,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -37,7 +38,6 @@ public class Member { private String ability; @Enumerated(EnumType.STRING) - @Column(nullable = false) private RoleType roleType; @Builder diff --git a/src/main/java/com/hackathon/momento/member/RoleType.java b/src/main/java/com/hackathon/momento/member/domain/RoleType.java similarity index 53% rename from src/main/java/com/hackathon/momento/member/RoleType.java rename to src/main/java/com/hackathon/momento/member/domain/RoleType.java index 3f24419..85883b1 100644 --- a/src/main/java/com/hackathon/momento/member/RoleType.java +++ b/src/main/java/com/hackathon/momento/member/domain/RoleType.java @@ -1,4 +1,4 @@ -package com.hackathon.momento.member; +package com.hackathon.momento.member.domain; public enum RoleType { ROLE_USER, ROLE_ADMIN diff --git a/src/main/java/com/hackathon/momento/member/domain/repository/MemberRepository.java b/src/main/java/com/hackathon/momento/member/domain/repository/MemberRepository.java new file mode 100644 index 0000000..4412139 --- /dev/null +++ b/src/main/java/com/hackathon/momento/member/domain/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package com.hackathon.momento.member.domain.repository; + +import com.hackathon.momento.member.domain.Member; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); +} diff --git a/src/main/java/com/hackathon/momento/member/exception/MemberNotFoundException.java b/src/main/java/com/hackathon/momento/member/exception/MemberNotFoundException.java new file mode 100644 index 0000000..b22cc2b --- /dev/null +++ b/src/main/java/com/hackathon/momento/member/exception/MemberNotFoundException.java @@ -0,0 +1,13 @@ +package com.hackathon.momento.member.exception; + +import com.hackathon.momento.global.error.exception.NotFoundGroupException; + +public class MemberNotFoundException extends NotFoundGroupException { + public MemberNotFoundException(String message) { + super(message); + } + + public MemberNotFoundException() { + this("존재하지 않는 사용자 입니다."); + } +} diff --git a/src/main/java/com/hackathon/momento/notification/Notification.java b/src/main/java/com/hackathon/momento/notification/domain/Notification.java similarity index 68% rename from src/main/java/com/hackathon/momento/notification/Notification.java rename to src/main/java/com/hackathon/momento/notification/domain/Notification.java index e45120c..738162d 100644 --- a/src/main/java/com/hackathon/momento/notification/Notification.java +++ b/src/main/java/com/hackathon/momento/notification/domain/Notification.java @@ -1,28 +1,24 @@ -package com.hackathon.momento.notification; +package com.hackathon.momento.notification.domain; -import com.hackathon.momento.member.Member; +import com.hackathon.momento.global.entity.BaseEntity; +import com.hackathon.momento.member.domain.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EntityListeners(AuditingEntityListener.class) -public class Notification { +public class Notification extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -35,10 +31,6 @@ public class Notification { @Column(columnDefinition = "TEXT") private String message; - @CreatedDate - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; diff --git a/src/main/java/com/hackathon/momento/team/domain/Status.java b/src/main/java/com/hackathon/momento/team/domain/Status.java new file mode 100644 index 0000000..4c9e52b --- /dev/null +++ b/src/main/java/com/hackathon/momento/team/domain/Status.java @@ -0,0 +1,5 @@ +package com.hackathon.momento.team.domain; + +public enum Status { + PENDING, COMPLETED +} diff --git a/src/main/java/com/hackathon/momento/team/TeamBuilding.java b/src/main/java/com/hackathon/momento/team/domain/TeamBuilding.java similarity index 67% rename from src/main/java/com/hackathon/momento/team/TeamBuilding.java rename to src/main/java/com/hackathon/momento/team/domain/TeamBuilding.java index 6c81093..796d35c 100644 --- a/src/main/java/com/hackathon/momento/team/TeamBuilding.java +++ b/src/main/java/com/hackathon/momento/team/domain/TeamBuilding.java @@ -1,25 +1,23 @@ -package com.hackathon.momento.team; +package com.hackathon.momento.team.domain; +import com.hackathon.momento.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import java.time.LocalDate; -import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EntityListeners(AuditingEntityListener.class) -public class TeamBuilding { +public class TeamBuilding extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -38,15 +36,12 @@ public class TeamBuilding { @Column(nullable = false) private String position; + @Enumerated(EnumType.STRING) @Column(nullable = false) - private String status; - - @CreatedDate - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt; + private Status status; @Builder - private TeamBuilding(LocalDate startDate, LocalDate endDate, int teamSize, String position, String status) { + private TeamBuilding(LocalDate startDate, LocalDate endDate, int teamSize, String position, Status status) { this.startDate = startDate; this.endDate = endDate; this.teamSize = teamSize; diff --git a/src/main/java/com/hackathon/momento/team/TeamBuildingMember.java b/src/main/java/com/hackathon/momento/team/domain/TeamBuildingMember.java similarity index 91% rename from src/main/java/com/hackathon/momento/team/TeamBuildingMember.java rename to src/main/java/com/hackathon/momento/team/domain/TeamBuildingMember.java index e9df21a..c154278 100644 --- a/src/main/java/com/hackathon/momento/team/TeamBuildingMember.java +++ b/src/main/java/com/hackathon/momento/team/domain/TeamBuildingMember.java @@ -1,6 +1,6 @@ -package com.hackathon.momento.team; +package com.hackathon.momento.team.domain; -import com.hackathon.momento.member.Member; +import com.hackathon.momento.member.domain.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/src/main/java/com/hackathon/momento/team/TeamInfo.java b/src/main/java/com/hackathon/momento/team/domain/TeamInfo.java similarity index 69% rename from src/main/java/com/hackathon/momento/team/TeamInfo.java rename to src/main/java/com/hackathon/momento/team/domain/TeamInfo.java index 8153353..02543f3 100644 --- a/src/main/java/com/hackathon/momento/team/TeamInfo.java +++ b/src/main/java/com/hackathon/momento/team/domain/TeamInfo.java @@ -1,27 +1,23 @@ -package com.hackathon.momento.team; +package com.hackathon.momento.team.domain; +import com.hackathon.momento.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EntityListeners(AuditingEntityListener.class) -public class TeamInfo { +public class TeamInfo extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -31,10 +27,6 @@ public class TeamInfo { @Column(nullable = false) private String teamName; - @CreatedDate - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_building_id") private TeamBuilding teamBuilding; diff --git a/src/main/java/com/hackathon/momento/team/TeamInfoMember.java b/src/main/java/com/hackathon/momento/team/domain/TeamInfoMember.java similarity index 91% rename from src/main/java/com/hackathon/momento/team/TeamInfoMember.java rename to src/main/java/com/hackathon/momento/team/domain/TeamInfoMember.java index 3b2bd4e..0ab1b65 100644 --- a/src/main/java/com/hackathon/momento/team/TeamInfoMember.java +++ b/src/main/java/com/hackathon/momento/team/domain/TeamInfoMember.java @@ -1,6 +1,6 @@ -package com.hackathon.momento.team; +package com.hackathon.momento.team.domain; -import com.hackathon.momento.member.Member; +import com.hackathon.momento.member.domain.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 01c0fa4..affe64a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,24 @@ spring: format_sql: true open-in-view: false + data: + redis: + port: ${REDIS_PORT} + host: ${REDIS_SERVER} + logging: level: org.hibernate.sql: debug - org.hibernate.type: trace \ No newline at end of file + org.hibernate.type: trace + +jwt: + secret: ${JWT_SECRET} + access-token-validity-in-milliseconds: ${ACCESS_TOKEN_VALIDITY_IN_MILLISECONDS} + refresh-token-validity-in-milliseconds: ${REFRESH_TOKEN_VALIDITY_IN_MILLISECONDS} + +oauth: + kakao: + client-id: ${KAKAO_CLIENT_ID} + redirect-uri: ${KAKAO_REDIRECT_URI} + admin-key: ${KAKAO_ADMIN_KEY} + token-url: ${KAKAO_TOKEN_URL} \ No newline at end of file