From 4e3a3ea04ae898daafbf3b77742db039b3adaac4 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sat, 24 Aug 2024 01:22:14 +0900 Subject: [PATCH 01/69] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20-=20accessTo?= =?UTF-8?q?ken=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EB=A8=BC=EC=A0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20UserDetail=EA=B3=BC=20UserDetailService=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20-=20UserRepository=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?-=20=ED=86=A0=ED=81=B0=20=EC=83=9D=EC=84=B1,=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D,=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20TokenProvide?= =?UTF-8?q?r=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20-=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=EB=B0=9B=EC=9D=80=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=84=20=EA=B2=80=EC=A6=9D=ED=95=98=EA=B3=A0=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=8C=20=ED=95=84=ED=84=B0=EB=A1=9C=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20TokenAuthenticationFilter=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20-=20Spring=20Security?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=9C=84=ED=95=9C=20SecurityC?= =?UTF-8?q?onfig=20=EC=9E=91=EC=84=B1=20-=20UserController=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../media/user/config/SecurityConfig.java | 35 +++++++++ .../config/TokenAuthenticationFilter.java | 29 +++++++ .../media/user/config/TokenProvider.java | 76 +++++++++++++++++++ .../media/user/controller/UserController.java | 11 +++ .../wanted/media/user/domain/UserDetail.java | 61 +++++++++++++++ .../media/user/dto/UserLoginRequestDto.java | 15 ++++ .../media/user/dto/UserLoginResponseDto.java | 16 ++++ .../media/user/repository/UserRepository.java | 2 + .../media/user/service/UserDetailService.java | 36 +++++++++ .../media/user/service/UserService.java | 13 ++++ 11 files changed, 295 insertions(+) create mode 100644 src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java create mode 100644 src/main/java/wanted/media/user/config/TokenProvider.java create mode 100644 src/main/java/wanted/media/user/domain/UserDetail.java create mode 100644 src/main/java/wanted/media/user/dto/UserLoginRequestDto.java create mode 100644 src/main/java/wanted/media/user/dto/UserLoginResponseDto.java create mode 100644 src/main/java/wanted/media/user/service/UserDetailService.java diff --git a/build.gradle b/build.gradle index cd13b6d..99b5fd2 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 0d84f89..2970150 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -1,7 +1,42 @@ package wanted.media.user.config; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +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; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .httpBasic(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests((auth) -> auth + .requestMatchers("/user/**").permitAll() // 어떤 사용자든 접근 가능 + .anyRequest().authenticated()) + .exceptionHandling(e -> e + .authenticationEntryPoint((request, response, exception) -> { + response.sendError(HttpStatus.UNAUTHORIZED.value(), "인증이 필요합니다."); + }) + .accessDeniedHandler((request, response, exception) -> { + response.sendError(HttpStatus.FORBIDDEN.value(), "접근권한이 없습니다."); + })) + .addFilterBefore(new TokenAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } } diff --git a/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java new file mode 100644 index 0000000..4aa72e7 --- /dev/null +++ b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java @@ -0,0 +1,29 @@ +package wanted.media.user.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + private final TokenProvider tokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = tokenProvider.resolveToken(request); + // 토큰 유효성 검증 + if (token != null && tokenProvider.validToken(token)) { + Authentication authentication = tokenProvider.getAuthentication(token); // 인증 정보 + SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 보안 컨텍스트에 설정 + } + filterChain.doFilter(request, response); // 응답과 요청을 다음 필터로 전달 + } +} diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java new file mode 100644 index 0000000..542933d --- /dev/null +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -0,0 +1,76 @@ +package wanted.media.user.config; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import wanted.media.user.domain.UserDetail; +import wanted.media.user.service.UserDetailService; + +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class TokenProvider { + + @Value("&{jwt.secret_key}") + private String key; + + private long tokenValidTime = 1000L * 60 * 60; // 1시간 + + private final UserDetailService userDetailService; + + public String makeToken(String account) { + Date now = new Date(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 + .setIssuedAt(now) // 발급시간 + .setExpiration(new Date(now.getTime() + tokenValidTime)) // 만료시간 + .setClaims(Jwts.claims().setSubject(account)) // 회원 계정 (사용자 식별값) + .signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화 + .compact(); + } + + // 토큰 유효성 검증 + public boolean validToken(String token) { + try { + Jwts.parser().setSigningKey(key) + .parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + // 토큰으로 인증 정보 담은 Authentication 반환 + public Authentication getAuthentication(String token) { + UserDetail userDetail = (UserDetail) userDetailService.loadUserByUsername(getUserAccount(token)); + return new UsernamePasswordAuthenticationToken(userDetail, "", userDetail.getAuthorities()); + /* principal : 인증된 사용자 정보 + credentials : 사용자의 인증 자격 증명 (인증 완료된 상태이므로 빈 문자열 사용) + authorities : 사용자의 권한목록*/ + } + + public String getUserAccount(String token) { + try { // JWT를 파싱해서 JWT 서명 검증 후 클레임을 반환하여 payload에서 subject 클레임 추출 + return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody().getSubject(); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); + } + } + + // 토큰 Header에서 꺼내오기 + public String resolveToken(HttpServletRequest request) { + String header = request.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) + return header.substring(7); + return null; + } +} diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 867ccff..91027ec 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -1,8 +1,13 @@ package wanted.media.user.controller; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wanted.media.user.dto.UserLoginRequestDto; +import wanted.media.user.dto.UserLoginResponseDto; import wanted.media.user.service.UserService; @RestController @@ -11,4 +16,10 @@ public class UserController { private final UserService userService; + + @PostMapping("/login") + public ResponseEntity loginUser(@RequestBody UserLoginRequestDto requestDto) { + UserLoginResponseDto responseDto = userService.loginUser(requestDto); + return ResponseEntity.ok().body(responseDto); + } } diff --git a/src/main/java/wanted/media/user/domain/UserDetail.java b/src/main/java/wanted/media/user/domain/UserDetail.java new file mode 100644 index 0000000..55177f5 --- /dev/null +++ b/src/main/java/wanted/media/user/domain/UserDetail.java @@ -0,0 +1,61 @@ +package wanted.media.user.domain; + +import lombok.Builder; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +public class UserDetail implements UserDetails { + + private String account; + private String password; + private List authorities; + + @Builder + public UserDetail(String account, String password, List authorities) { + this.account = account; + this.password = password; + this.authorities = authorities; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return account; + } + + // 계정 만료 여부 + @Override + public boolean isAccountNonExpired() { + return true; + } + + // 계정 잠금 여부 + @Override + public boolean isAccountNonLocked() { + return true; + } + + // 비밀번호 만료 여부 + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + // 계정 사용 가능 여부 + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java new file mode 100644 index 0000000..cc46ffd --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java @@ -0,0 +1,15 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserLoginRequestDto { + + private String account; + private String password; +} diff --git a/src/main/java/wanted/media/user/dto/UserLoginResponseDto.java b/src/main/java/wanted/media/user/dto/UserLoginResponseDto.java new file mode 100644 index 0000000..9ba4e93 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserLoginResponseDto.java @@ -0,0 +1,16 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserLoginResponseDto { + private UUID userId; + private String token; +} diff --git a/src/main/java/wanted/media/user/repository/UserRepository.java b/src/main/java/wanted/media/user/repository/UserRepository.java index 51fa606..2be54a2 100644 --- a/src/main/java/wanted/media/user/repository/UserRepository.java +++ b/src/main/java/wanted/media/user/repository/UserRepository.java @@ -3,7 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import wanted.media.user.domain.User; +import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { + Optional findByAccount(String account); } diff --git a/src/main/java/wanted/media/user/service/UserDetailService.java b/src/main/java/wanted/media/user/service/UserDetailService.java new file mode 100644 index 0000000..c282738 --- /dev/null +++ b/src/main/java/wanted/media/user/service/UserDetailService.java @@ -0,0 +1,36 @@ +package wanted.media.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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; +import wanted.media.user.domain.User; +import wanted.media.user.domain.UserDetail; +import wanted.media.user.repository.UserRepository; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserDetailService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException { + User user = userRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("계정이 존재하지 않습니다.")); + List roles = new ArrayList<>(); + roles.add(new SimpleGrantedAuthority(user.getGrade().toString())); + + return UserDetail.builder() + .account(user.getAccount()) + .password(user.getPassword()) + .authorities(roles) + .build(); + } +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index e642526..218e2d5 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -2,6 +2,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import wanted.media.user.config.TokenProvider; +import wanted.media.user.domain.User; +import wanted.media.user.dto.UserLoginRequestDto; +import wanted.media.user.dto.UserLoginResponseDto; import wanted.media.user.repository.UserRepository; @Service @@ -9,4 +13,13 @@ public class UserService { private final UserRepository userRepository; + private final TokenProvider tokenProvider; + + public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { + User user = userRepository.findByAccount(requestDto.getAccount()) + .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); + if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 + throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); + return new UserLoginResponseDto(user.getUserId(), tokenProvider.makeToken(requestDto.getAccount())); + } } From 5d7326ed4b9e609d351deecc916489f15ec585b0 Mon Sep 17 00:00:00 2001 From: Jinhui Date: Sat, 24 Aug 2024 22:27:41 +0900 Subject: [PATCH 02/69] =?UTF-8?q?refactor=20:=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=AA=85=EA=B3=BC=20=EB=B3=80=EC=88=98=EB=AA=85=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=EC=9D=84=20=EB=9C=BB?= =?UTF-8?q?=ED=95=98=EB=8A=94=20content=EB=A5=BC=20post=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/content/repository/ContentRepository.java | 7 ------- .../java/wanted/media/content/service/ContentService.java | 7 ------- .../controller/PostController.java} | 6 +++--- .../domain/Content.java => post/domain/Post.java} | 8 ++++---- .../java/wanted/media/{content => post}/domain/Type.java | 2 +- .../java/wanted/media/post/repository/PostRepository.java | 7 +++++++ src/main/java/wanted/media/post/service/PostService.java | 7 +++++++ 7 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 src/main/java/wanted/media/content/repository/ContentRepository.java delete mode 100644 src/main/java/wanted/media/content/service/ContentService.java rename src/main/java/wanted/media/{content/controller/ContentController.java => post/controller/PostController.java} (58%) rename src/main/java/wanted/media/{content/domain/Content.java => post/domain/Post.java} (89%) rename src/main/java/wanted/media/{content => post}/domain/Type.java (63%) create mode 100644 src/main/java/wanted/media/post/repository/PostRepository.java create mode 100644 src/main/java/wanted/media/post/service/PostService.java diff --git a/src/main/java/wanted/media/content/repository/ContentRepository.java b/src/main/java/wanted/media/content/repository/ContentRepository.java deleted file mode 100644 index 510a593..0000000 --- a/src/main/java/wanted/media/content/repository/ContentRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package wanted.media.content.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import wanted.media.content.domain.Content; - -public interface ContentRepository extends JpaRepository { -} diff --git a/src/main/java/wanted/media/content/service/ContentService.java b/src/main/java/wanted/media/content/service/ContentService.java deleted file mode 100644 index b505adc..0000000 --- a/src/main/java/wanted/media/content/service/ContentService.java +++ /dev/null @@ -1,7 +0,0 @@ -package wanted.media.content.service; - -import org.springframework.stereotype.Service; - -@Service -public class ContentService { -} diff --git a/src/main/java/wanted/media/content/controller/ContentController.java b/src/main/java/wanted/media/post/controller/PostController.java similarity index 58% rename from src/main/java/wanted/media/content/controller/ContentController.java rename to src/main/java/wanted/media/post/controller/PostController.java index 00583de..97212e6 100644 --- a/src/main/java/wanted/media/content/controller/ContentController.java +++ b/src/main/java/wanted/media/post/controller/PostController.java @@ -1,9 +1,9 @@ -package wanted.media.content.controller; +package wanted.media.post.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/contents") -public class ContentController { +@RequestMapping("/posts") +public class PostController { } diff --git a/src/main/java/wanted/media/content/domain/Content.java b/src/main/java/wanted/media/post/domain/Post.java similarity index 89% rename from src/main/java/wanted/media/content/domain/Content.java rename to src/main/java/wanted/media/post/domain/Post.java index fed9efd..5f937db 100644 --- a/src/main/java/wanted/media/content/domain/Content.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -1,4 +1,4 @@ -package wanted.media.content.domain; +package wanted.media.post.domain; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; @@ -13,15 +13,15 @@ @Entity @Getter -@Table(name = "contents") +@Table(name = "posts") @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder @EntityListeners(AuditingEntityListener.class) -public class Content { +public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "content_id", nullable = false) + @Column(name = "post_id", nullable = false) private Long id; @Column(name = "like_count") diff --git a/src/main/java/wanted/media/content/domain/Type.java b/src/main/java/wanted/media/post/domain/Type.java similarity index 63% rename from src/main/java/wanted/media/content/domain/Type.java rename to src/main/java/wanted/media/post/domain/Type.java index 539f7fa..55a18e9 100644 --- a/src/main/java/wanted/media/content/domain/Type.java +++ b/src/main/java/wanted/media/post/domain/Type.java @@ -1,4 +1,4 @@ -package wanted.media.content.domain; +package wanted.media.post.domain; public enum Type { FACEBOOK, TWITTER, INSTAGRAM, THREADS; diff --git a/src/main/java/wanted/media/post/repository/PostRepository.java b/src/main/java/wanted/media/post/repository/PostRepository.java new file mode 100644 index 0000000..fb74f17 --- /dev/null +++ b/src/main/java/wanted/media/post/repository/PostRepository.java @@ -0,0 +1,7 @@ +package wanted.media.post.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wanted.media.post.domain.Post; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/wanted/media/post/service/PostService.java b/src/main/java/wanted/media/post/service/PostService.java new file mode 100644 index 0000000..cb52ee0 --- /dev/null +++ b/src/main/java/wanted/media/post/service/PostService.java @@ -0,0 +1,7 @@ +package wanted.media.post.service; + +import org.springframework.stereotype.Service; + +@Service +public class PostService { +} From 82c10bc2355407a016213b60ae7b31c889644ecc Mon Sep 17 00:00:00 2001 From: Jinhui Date: Sat, 24 Aug 2024 22:29:21 +0900 Subject: [PATCH 03/69] =?UTF-8?q?refactor=20:=20Long=EC=97=90=EC=84=9C=20S?= =?UTF-8?q?tring=EC=9C=BC=EB=A1=9C=20=EC=9E=90=EB=A3=8C=ED=98=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/post/domain/Post.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/post/domain/Post.java b/src/main/java/wanted/media/post/domain/Post.java index 5f937db..858f054 100644 --- a/src/main/java/wanted/media/post/domain/Post.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -22,7 +22,7 @@ public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "post_id", nullable = false) - private Long id; + private String id; @Column(name = "like_count") private Long likeCount; From 99ebf13d020bd813b33913b26ae315321d3a8e03 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 02:53:23 +0900 Subject: [PATCH 04/69] =?UTF-8?q?feat=20:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=8B=9C=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=86=A0=ED=81=B0=EA=B3=BC=20=EC=95=A1?= =?UTF-8?q?=EC=84=B8=EC=8A=A4=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20-=20=EA=B8=B0=EC=A1=B4=EC=9D=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=8B=9C=20=EC=95=A1=EC=84=B8=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EB=A7=8C=20=EB=B0=9C=EA=B8=89=EB=90=98?= =?UTF-8?q?=EB=8D=98=20=EC=BD=94=EB=93=9C=EC=97=90=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=86=A0=ED=81=B0=EB=8F=84=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=EB=90=98=EC=96=B4=20=EC=A0=80=EC=9E=A5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20UserService=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=ED=86=A0=ED=81=B0=EC=9D=B4=20=EB=B0=94=EB=80=8C?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20TokenProv?= =?UTF-8?q?ider=EC=97=90=EC=84=9C=20=EC=88=98=EC=A0=95=20--=20claim?= =?UTF-8?q?=EC=97=90=20=EB=B0=9C=EA=B8=89=20=EC=8B=9C=EA=B0=84=EA=B3=BC=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20TokenService=EC=97=90=EC=84=9C=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=EC=9D=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=B4=20=EC=95=A1=EC=84=B8=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EA=B3=BC=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/TokenAuthenticationFilter.java | 2 +- .../media/user/config/TokenProvider.java | 10 ++-- .../user/controller/TokenController.java | 26 +++++++++ .../java/wanted/media/user/domain/Token.java | 10 ++++ .../media/user/dto/TokenRequestDto.java | 14 +++++ .../media/user/dto/TokenResponseDto.java | 14 +++++ .../user/repository/TokenRepository.java | 14 +++++ .../media/user/service/TokenService.java | 54 +++++++++++++++++++ .../media/user/service/UserService.java | 16 +++++- 9 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 src/main/java/wanted/media/user/controller/TokenController.java create mode 100644 src/main/java/wanted/media/user/dto/TokenRequestDto.java create mode 100644 src/main/java/wanted/media/user/dto/TokenResponseDto.java create mode 100644 src/main/java/wanted/media/user/repository/TokenRepository.java create mode 100644 src/main/java/wanted/media/user/service/TokenService.java diff --git a/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java index 4aa72e7..8f18dc1 100644 --- a/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java +++ b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java @@ -18,7 +18,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String token = tokenProvider.resolveToken(request); + String token = tokenProvider.resolveToken(request); // 헤더에서 가져온 액세스토큰 // 토큰 유효성 검증 if (token != null && tokenProvider.validToken(token)) { Authentication authentication = tokenProvider.getAuthentication(token); // 인증 정보 diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index 542933d..839c6bd 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -19,21 +19,21 @@ @RequiredArgsConstructor public class TokenProvider { - @Value("&{jwt.secret_key}") + @Value("${jwt.secret_key}") private String key; private long tokenValidTime = 1000L * 60 * 60; // 1시간 + private long RefreshTokenValidTime = 1000L * 60 * 60 * 24 * 7; // 7일 private final UserDetailService userDetailService; - public String makeToken(String account) { + public String makeToken(String account, String type) { Date now = new Date(); + long time = type.equals("access") ? tokenValidTime : RefreshTokenValidTime; return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 - .setIssuedAt(now) // 발급시간 - .setExpiration(new Date(now.getTime() + tokenValidTime)) // 만료시간 - .setClaims(Jwts.claims().setSubject(account)) // 회원 계정 (사용자 식별값) + .setClaims(Jwts.claims().setSubject(account).setAudience(type).setIssuedAt(now).setExpiration(new Date(now.getTime() + time))) .signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화 .compact(); } diff --git a/src/main/java/wanted/media/user/controller/TokenController.java b/src/main/java/wanted/media/user/controller/TokenController.java new file mode 100644 index 0000000..85da174 --- /dev/null +++ b/src/main/java/wanted/media/user/controller/TokenController.java @@ -0,0 +1,26 @@ +package wanted.media.user.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wanted.media.user.dto.TokenRequestDto; +import wanted.media.user.dto.TokenResponseDto; +import wanted.media.user.service.TokenService; + +@RestController +@RequestMapping("/api/token") +@RequiredArgsConstructor +public class TokenController { + + private final TokenService tokenService; + + @PostMapping("/reissue") + public ResponseEntity reIssueToken(@RequestBody TokenRequestDto requestDto) { + TokenResponseDto responseDto = tokenService.reIssueToken(requestDto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } +} diff --git a/src/main/java/wanted/media/user/domain/Token.java b/src/main/java/wanted/media/user/domain/Token.java index 81fc2f5..d4e9793 100644 --- a/src/main/java/wanted/media/user/domain/Token.java +++ b/src/main/java/wanted/media/user/domain/Token.java @@ -23,4 +23,14 @@ public class Token { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; + + public Token(String refreshToken, User user) { + this.refreshToken = refreshToken; + this.user = user; + } + + public Token updateToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } } diff --git a/src/main/java/wanted/media/user/dto/TokenRequestDto.java b/src/main/java/wanted/media/user/dto/TokenRequestDto.java new file mode 100644 index 0000000..f3c22bb --- /dev/null +++ b/src/main/java/wanted/media/user/dto/TokenRequestDto.java @@ -0,0 +1,14 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TokenRequestDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/wanted/media/user/dto/TokenResponseDto.java b/src/main/java/wanted/media/user/dto/TokenResponseDto.java new file mode 100644 index 0000000..789e867 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/TokenResponseDto.java @@ -0,0 +1,14 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TokenResponseDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/wanted/media/user/repository/TokenRepository.java b/src/main/java/wanted/media/user/repository/TokenRepository.java new file mode 100644 index 0000000..efd7f78 --- /dev/null +++ b/src/main/java/wanted/media/user/repository/TokenRepository.java @@ -0,0 +1,14 @@ +package wanted.media.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import wanted.media.user.domain.Token; + +import java.util.Optional; +import java.util.UUID; + +public interface TokenRepository extends JpaRepository { + @Query("SELECT t FROM Token t WHERE t.user.userId = :userId") + Optional findByUserId(@Param("userId") UUID userID); +} diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java new file mode 100644 index 0000000..0c00375 --- /dev/null +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -0,0 +1,54 @@ +package wanted.media.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import wanted.media.user.config.TokenProvider; +import wanted.media.user.domain.Token; +import wanted.media.user.domain.User; +import wanted.media.user.domain.UserDetail; +import wanted.media.user.dto.TokenRequestDto; +import wanted.media.user.dto.TokenResponseDto; +import wanted.media.user.repository.TokenRepository; +import wanted.media.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class TokenService { + + private final TokenProvider tokenProvider; + private final TokenRepository tokenRepository; + private final UserRepository userRepository; + + // 액세스 토큰 재발행 + public TokenResponseDto reIssueToken(TokenRequestDto requestDto) { + if (!tokenProvider.validToken(requestDto.getRefreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 + throw new IllegalArgumentException("다시 로그인해주세요."); + } + + User user = findUserByToken(requestDto); + + Token storedToken = tokenRepository.findByUserId(user.getUserId()) + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 토큰입니다.")); + + if (!storedToken.getRefreshToken().equals(requestDto.getRefreshToken())) { + throw new IllegalArgumentException("유효하지 않은 토큰입니다."); + } + + String accessToken = tokenProvider.makeToken(user.getAccount(), "access"); + String refreshToken = tokenProvider.makeToken(user.getAccount(), "refresh"); + + storedToken.updateToken(refreshToken); + tokenRepository.save(storedToken); + + return new TokenResponseDto(accessToken, refreshToken); + } + + public User findUserByToken(TokenRequestDto requestDto) { + Authentication authentication = tokenProvider.getAuthentication(requestDto.getAccessToken()); + UserDetail userDetail = (UserDetail) authentication.getPrincipal(); + String account = userDetail.getUsername(); + return userRepository.findByAccount(account).orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다.")); + } + +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 218e2d5..5b1e8bf 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -3,16 +3,21 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import wanted.media.user.config.TokenProvider; +import wanted.media.user.domain.Token; import wanted.media.user.domain.User; import wanted.media.user.dto.UserLoginRequestDto; import wanted.media.user.dto.UserLoginResponseDto; +import wanted.media.user.repository.TokenRepository; import wanted.media.user.repository.UserRepository; +import java.util.Optional; + @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { @@ -20,6 +25,15 @@ public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); - return new UserLoginResponseDto(user.getUserId(), tokenProvider.makeToken(requestDto.getAccount())); + + Optional refreshToken = tokenRepository.findByUserId(user.getUserId()); // 리프레시 토큰 있는지 확인 + String newRefreshToken = tokenProvider.makeToken(requestDto.getAccount(), "refresh"); // 새 리프레시 토큰 + if (refreshToken.isPresent()) { // 리프레시 토큰 있을 경우 + tokenRepository.save(refreshToken.get().updateToken(newRefreshToken)); // 새 토큰으로 업데이트 + } else { // 리프레시 토큰 없을 경우 + tokenRepository.save(new Token(newRefreshToken, user)); // 새 토큰 저장 + } + + return new UserLoginResponseDto(user.getUserId(), tokenProvider.makeToken(requestDto.getAccount(), "access")); } } From 97059e41e86725354e013f6ac448b6c037959de6 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Sun, 25 Aug 2024 16:23:46 +0900 Subject: [PATCH 05/69] =?UTF-8?q?feat=20:=20users=20SecurityConfig=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=95=94=ED=98=B8?= =?UTF-8?q?=ED=99=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/user/config/SecurityConfig.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 0d84f89..320ed5b 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -1,7 +1,16 @@ package wanted.media.user.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @EnableWebSecurity +@Configuration public class SecurityConfig { + // 비밀번호 암호화 기능 + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } From 41cd12ec4dab8a80509cd2c93853e1c2c5ee0a87 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 16:28:23 +0900 Subject: [PATCH 06/69] =?UTF-8?q?refactor=20:=20Post=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Content -> Post로 클래스 네임 변경 - PK : Long -> String 으로 타입 변경 - ContentController -> PostController 클래스 네임 변경 --- ...entController.java => PostController.java} | 4 +- .../wanted/media/content/domain/Content.java | 57 ---------------- .../wanted/media/content/domain/Post.java | 67 +++++++++++++++++++ 3 files changed, 69 insertions(+), 59 deletions(-) rename src/main/java/wanted/media/content/controller/{ContentController.java => PostController.java} (75%) delete mode 100644 src/main/java/wanted/media/content/domain/Content.java create mode 100644 src/main/java/wanted/media/content/domain/Post.java diff --git a/src/main/java/wanted/media/content/controller/ContentController.java b/src/main/java/wanted/media/content/controller/PostController.java similarity index 75% rename from src/main/java/wanted/media/content/controller/ContentController.java rename to src/main/java/wanted/media/content/controller/PostController.java index 00583de..6d07cac 100644 --- a/src/main/java/wanted/media/content/controller/ContentController.java +++ b/src/main/java/wanted/media/content/controller/PostController.java @@ -4,6 +4,6 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/contents") -public class ContentController { +@RequestMapping("/posts") +public class PostController { } diff --git a/src/main/java/wanted/media/content/domain/Content.java b/src/main/java/wanted/media/content/domain/Content.java deleted file mode 100644 index fed9efd..0000000 --- a/src/main/java/wanted/media/content/domain/Content.java +++ /dev/null @@ -1,57 +0,0 @@ -package wanted.media.content.domain; - -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.*; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import wanted.media.user.domain.User; - -import java.time.LocalDateTime; - -@Entity -@Getter -@Table(name = "contents") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -@EntityListeners(AuditingEntityListener.class) -public class Content { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "content_id", nullable = false) - private Long id; - - @Column(name = "like_count") - private Long likeCount; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private Type type; - - @Size(max = 150) - @Column(nullable = false) - private String title; - - private String content; - - private String hashtags; - - private Long viewCount; - - private Long shareCount; - - @LastModifiedDate - private LocalDateTime updatedAt; - - @CreatedDate - private LocalDateTime createdAt; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - @NotNull - private User user; - -} diff --git a/src/main/java/wanted/media/content/domain/Post.java b/src/main/java/wanted/media/content/domain/Post.java new file mode 100644 index 0000000..ceff407 --- /dev/null +++ b/src/main/java/wanted/media/content/domain/Post.java @@ -0,0 +1,67 @@ +package wanted.media.content.domain; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.ColumnDefault; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +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 jakarta.persistence.Table; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wanted.media.user.domain.User; + +@Entity +@Getter +@Table(name = "post") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public class Post { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_id", nullable = false) + private String id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Type type; + + @Size(max = 150) + @Column(nullable = false) + private String title; + private String content; + private String hashtags; + + @ColumnDefault("0") + private Long likeCount; + + @ColumnDefault("0") + private Long viewCount; + + @ColumnDefault("0") + private Long shareCount; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} From 4715204eb00e72ea2dc80e5346a7e26f1796bd4a Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 16:38:14 +0900 Subject: [PATCH 07/69] =?UTF-8?q?refactor=20:=20ContentRepository=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/content/repository/ContentRepository.java | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/main/java/wanted/media/content/repository/ContentRepository.java diff --git a/src/main/java/wanted/media/content/repository/ContentRepository.java b/src/main/java/wanted/media/content/repository/ContentRepository.java deleted file mode 100644 index 510a593..0000000 --- a/src/main/java/wanted/media/content/repository/ContentRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package wanted.media.content.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import wanted.media.content.domain.Content; - -public interface ContentRepository extends JpaRepository { -} From a44386f9438021f6fd79b9c7688d78d1c73e69d4 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 17:01:18 +0900 Subject: [PATCH 08/69] =?UTF-8?q?build=20:=20QueryDsl=20dependency=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Q클래스 폴더 ignore 처리 --- .gitignore | 1 + build.gradle | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/.gitignore b/.gitignore index cf4d44f..02485a7 100644 --- a/.gitignore +++ b/.gitignore @@ -267,3 +267,4 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/macos,intellij,windows,java,gradle src/main/resources/application-secret.yml +src/main/generated diff --git a/build.gradle b/build.gradle index cd13b6d..860eaf4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,9 @@ +buildscript { + ext { + queryDslVersion = "5.0.0" + } +} + plugins { id 'java' id 'org.springframework.boot' version '3.2.8' @@ -28,11 +34,21 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'io.jsonwebtoken:jjwt:0.9.1' + + // QueryDSL + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } @@ -40,3 +56,25 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +/** QueryDSL start **/ + +// Querydsl 설정부 +def generated = 'src/main/generated' + +// querydsl QClass 파일 생성 위치를 지정 +tasks.withType(JavaCompile) { + options.getGeneratedSourceOutputDirectory().set(file(generated)) +} + +// java source set 에 querydsl QClass 위치 추가 +sourceSets { + main.java.srcDirs += [generated] +} + +// gradle clean 시에 QClass 디렉토리 삭제 +clean { + delete file(generated) +} + +/** QueryDSL end **/ From d1e4d20690ad3f054e69cd34d30d544a05932bd8 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 17:02:36 +0900 Subject: [PATCH 09/69] =?UTF-8?q?chore=20:=20QueryDslConfig=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/global/config/QueryDslConfig.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/wanted/media/global/config/QueryDslConfig.java diff --git a/src/main/java/wanted/media/global/config/QueryDslConfig.java b/src/main/java/wanted/media/global/config/QueryDslConfig.java new file mode 100644 index 0000000..eb4ba75 --- /dev/null +++ b/src/main/java/wanted/media/global/config/QueryDslConfig.java @@ -0,0 +1,17 @@ +package wanted.media.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; + +@Configuration +public class QueryDslConfig { + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } +} From 2c28a09002d6a52c8479fe3662b8c3b44b3398c0 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 17:03:58 +0900 Subject: [PATCH 10/69] =?UTF-8?q?feat=20:=20String=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A8=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20LocalDateTime=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BF=94=EC=A3=BC=EB=8A=94=20Converter=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/global/config/WebConfig.java | 16 ++++++++++++ .../StringToLocalDateTimeConverter.java | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/main/java/wanted/media/global/config/WebConfig.java create mode 100644 src/main/java/wanted/media/global/converter/StringToLocalDateTimeConverter.java diff --git a/src/main/java/wanted/media/global/config/WebConfig.java b/src/main/java/wanted/media/global/config/WebConfig.java new file mode 100644 index 0000000..e698180 --- /dev/null +++ b/src/main/java/wanted/media/global/config/WebConfig.java @@ -0,0 +1,16 @@ +package wanted.media.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import wanted.media.global.converter.StringToLocalDateTimeConverter; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new StringToLocalDateTimeConverter()); + } +} diff --git a/src/main/java/wanted/media/global/converter/StringToLocalDateTimeConverter.java b/src/main/java/wanted/media/global/converter/StringToLocalDateTimeConverter.java new file mode 100644 index 0000000..08005b1 --- /dev/null +++ b/src/main/java/wanted/media/global/converter/StringToLocalDateTimeConverter.java @@ -0,0 +1,25 @@ +package wanted.media.global.converter; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +@Component +public class StringToLocalDateTimeConverter implements Converter { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Override + public LocalDateTime convert(String source) { + try { + return LocalDateTime.parse(source, DATE_TIME_FORMATTER); + } catch (DateTimeParseException e) { + return LocalDate.parse(source, DATE_FORMATTER).atStartOfDay(); + } + } +} From a80f8a25d0c3ec1c8b9341b63b831844c7026c2d Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 17:04:56 +0900 Subject: [PATCH 11/69] =?UTF-8?q?feat=20:=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/controller/StatController.java | 25 ++++++++ .../media/content/domain/CountValueType.java | 5 ++ .../media/content/domain/StatDateType.java | 5 ++ .../media/content/domain/dto/StatParam.java | 60 +++++++++++++++++++ .../content/domain/dto/StatResponse.java | 8 +++ .../content/repository/StatRepository.java | 43 +++++++++++++ .../media/content/service/StatService.java | 19 ++++++ 7 files changed, 165 insertions(+) create mode 100644 src/main/java/wanted/media/content/controller/StatController.java create mode 100644 src/main/java/wanted/media/content/domain/CountValueType.java create mode 100644 src/main/java/wanted/media/content/domain/StatDateType.java create mode 100644 src/main/java/wanted/media/content/domain/dto/StatParam.java create mode 100644 src/main/java/wanted/media/content/domain/dto/StatResponse.java create mode 100644 src/main/java/wanted/media/content/repository/StatRepository.java create mode 100644 src/main/java/wanted/media/content/service/StatService.java diff --git a/src/main/java/wanted/media/content/controller/StatController.java b/src/main/java/wanted/media/content/controller/StatController.java new file mode 100644 index 0000000..48da145 --- /dev/null +++ b/src/main/java/wanted/media/content/controller/StatController.java @@ -0,0 +1,25 @@ +package wanted.media.content.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import wanted.media.content.domain.dto.StatParam; +import wanted.media.content.domain.dto.StatResponse; +import wanted.media.content.service.StatService; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class StatController { + private final StatService statService; + + @GetMapping("/statistics") + public ResponseEntity statistics(@ModelAttribute StatParam param) { + StatResponse response = StatResponse.from(statService.statistics(param)); + return ResponseEntity.ok().body(response); + } +} diff --git a/src/main/java/wanted/media/content/domain/CountValueType.java b/src/main/java/wanted/media/content/domain/CountValueType.java new file mode 100644 index 0000000..1fbf344 --- /dev/null +++ b/src/main/java/wanted/media/content/domain/CountValueType.java @@ -0,0 +1,5 @@ +package wanted.media.content.domain; + +public enum CountValueType { + COUNT, LIKE_COUNT, VIEW_COUNT, SHARE_COUNT; +} diff --git a/src/main/java/wanted/media/content/domain/StatDateType.java b/src/main/java/wanted/media/content/domain/StatDateType.java new file mode 100644 index 0000000..c1e071f --- /dev/null +++ b/src/main/java/wanted/media/content/domain/StatDateType.java @@ -0,0 +1,5 @@ +package wanted.media.content.domain; + +public enum StatDateType { + DATE, HOUR; +} diff --git a/src/main/java/wanted/media/content/domain/dto/StatParam.java b/src/main/java/wanted/media/content/domain/dto/StatParam.java new file mode 100644 index 0000000..4a49c40 --- /dev/null +++ b/src/main/java/wanted/media/content/domain/dto/StatParam.java @@ -0,0 +1,60 @@ +package wanted.media.content.domain.dto; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Optional; + +import wanted.media.content.domain.CountValueType; +import wanted.media.content.domain.StatDateType; +import wanted.media.exception.BadRequestException; +import wanted.media.exception.ErrorCode; +import wanted.media.exception.InvalidParamException; + +/** + * + * @param hashtag 계정, defaultValue = 본인 계정 + * @param type DATE or HOUR, 필수 값 + * @param start 검색 시작 기간, defaultValue = 오늘로 부터 7일 전 + * @param end 검색 끝 기간, defaultValue = 오늘 + * @param value COUNT or VIEW_COUNT, LIKE_COUNT, SHARE_COUNT, defaultValue = count + */ +public record StatParam( + String hashtag, + StatDateType type, + LocalDateTime start, + LocalDateTime end, + CountValueType value) { + + public StatParam(String hashtag, StatDateType type, LocalDateTime start, LocalDateTime end, CountValueType value) { + // JWT 구현시 default 값 변경 예정 + this.hashtag = (hashtag == null) ? "me" : hashtag; + + // Type(DATE, HOUR)에 맞춰 기본 값 설정 + this.start = switch (type) { + case DATE -> (start == null) ? LocalDateTime.of(LocalDate.now().minusDays(7), LocalTime.MIN) : start; + case HOUR -> (start == null) ? LocalDateTime.now().minusDays(7) : start; + }; + this.end = switch (type) { + case DATE -> (end == null) ? LocalDateTime.of(LocalDate.now(), LocalTime.MAX) : end; + case HOUR -> (end == null) ? LocalDateTime.now() : end; + }; + validateDateRange(start, end); + + // Default 값 설정 + this.value = (value == null) ? CountValueType.COUNT : value; + + // Default 값 설정 + this.type = Optional.ofNullable(type) + .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_PARAMETER)); + } + + /** + * start 일자가 end 일자보다 앞인지 검증 + */ + private void validateDateRange(LocalDateTime start, LocalDateTime end) { + if (start.isAfter(end)) { + throw new BadRequestException(ErrorCode.ENTITY_NOT_FOUND); + } + } +} diff --git a/src/main/java/wanted/media/content/domain/dto/StatResponse.java b/src/main/java/wanted/media/content/domain/dto/StatResponse.java new file mode 100644 index 0000000..0880cc3 --- /dev/null +++ b/src/main/java/wanted/media/content/domain/dto/StatResponse.java @@ -0,0 +1,8 @@ +package wanted.media.content.domain.dto; + +public record StatResponse(Long count) { + + public static StatResponse from(Long count) { + return new StatResponse(count); + } +} diff --git a/src/main/java/wanted/media/content/repository/StatRepository.java b/src/main/java/wanted/media/content/repository/StatRepository.java new file mode 100644 index 0000000..8dffe1c --- /dev/null +++ b/src/main/java/wanted/media/content/repository/StatRepository.java @@ -0,0 +1,43 @@ +package wanted.media.content.repository; + +import static com.querydsl.core.types.ExpressionUtils.*; +import static wanted.media.content.domain.QPost.*; +import static wanted.media.user.domain.QUser.*; + +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; +import wanted.media.content.domain.dto.StatParam; + +@Repository +@RequiredArgsConstructor +public class StatRepository { + private final JPAQueryFactory queryFactory; + + /** + * ex. + * SELECT SUM(view_count) + * FROM post p + * LEFT JOIN members m ON p.user_id = m.user_id + * where p.created_at between '2024-08-18 00:00:00' and '2024-08-25 23:59:59' + * and m.account = 'user1'; + */ + public Long statistics(StatParam param) { + var selectQuery = switch (param.value()) { + case COUNT -> queryFactory.select(count(post.id)); + case LIKE_COUNT -> queryFactory.select(post.likeCount.sum()); + case VIEW_COUNT -> queryFactory.select(post.viewCount.sum()); + case SHARE_COUNT -> queryFactory.select(post.shareCount.sum()); + }; + + return selectQuery.from(post) + .leftJoin(user).on(post.user.userId.eq(user.userId)) + .where( + post.createdAt.between(param.start(), param.end()), + post.user.account.eq(param.hashtag()) + ) + .fetchFirst(); + } +} diff --git a/src/main/java/wanted/media/content/service/StatService.java b/src/main/java/wanted/media/content/service/StatService.java new file mode 100644 index 0000000..b54b91c --- /dev/null +++ b/src/main/java/wanted/media/content/service/StatService.java @@ -0,0 +1,19 @@ +package wanted.media.content.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import wanted.media.content.domain.dto.StatParam; +import wanted.media.content.repository.StatRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StatService { + private final StatRepository statRepository; + + public Long statistics(StatParam param) { + return statRepository.statistics(param); + } +} From c82cce5aea2c7f9575fde11046a88ec52771190a Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 17:07:55 +0900 Subject: [PATCH 12/69] =?UTF-8?q?refactor=20:=20ErrorResponse=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EB=A5=BC=20record=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/exception/ErrorResponse.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/wanted/media/exception/ErrorResponse.java b/src/main/java/wanted/media/exception/ErrorResponse.java index 5ce1086..bb5cc5e 100644 --- a/src/main/java/wanted/media/exception/ErrorResponse.java +++ b/src/main/java/wanted/media/exception/ErrorResponse.java @@ -1,11 +1,4 @@ package wanted.media.exception; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -@Getter -@RequiredArgsConstructor -public class ErrorResponse { - - private final int statusCode; - private final String message; +public record ErrorResponse(int statusCode, String message) { } From 32ec4f721e0cb203eed614a66104a3227e392ce1 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 17:11:07 +0900 Subject: [PATCH 13/69] =?UTF-8?q?feat=20:=20400=EC=97=90=EB=9F=AC=EB=A5=BC?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Exception=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StatParam : 사용하지 않는 Exception 제거 - Handler : 잘못된 import 변경 --- .../media/content/domain/dto/StatParam.java | 75 +++++++++---------- .../media/exception/BadRequestException.java | 8 ++ .../wanted/media/exception/BaseException.java | 10 +++ .../wanted/media/exception/ErrorCode.java | 3 +- .../handler/GlobalExceptionHandler.java | 12 +-- 5 files changed, 63 insertions(+), 45 deletions(-) create mode 100644 src/main/java/wanted/media/exception/BadRequestException.java create mode 100644 src/main/java/wanted/media/exception/BaseException.java diff --git a/src/main/java/wanted/media/content/domain/dto/StatParam.java b/src/main/java/wanted/media/content/domain/dto/StatParam.java index 4a49c40..5fcebf0 100644 --- a/src/main/java/wanted/media/content/domain/dto/StatParam.java +++ b/src/main/java/wanted/media/content/domain/dto/StatParam.java @@ -9,7 +9,6 @@ import wanted.media.content.domain.StatDateType; import wanted.media.exception.BadRequestException; import wanted.media.exception.ErrorCode; -import wanted.media.exception.InvalidParamException; /** * @@ -20,41 +19,41 @@ * @param value COUNT or VIEW_COUNT, LIKE_COUNT, SHARE_COUNT, defaultValue = count */ public record StatParam( - String hashtag, - StatDateType type, - LocalDateTime start, - LocalDateTime end, - CountValueType value) { - - public StatParam(String hashtag, StatDateType type, LocalDateTime start, LocalDateTime end, CountValueType value) { - // JWT 구현시 default 값 변경 예정 - this.hashtag = (hashtag == null) ? "me" : hashtag; - - // Type(DATE, HOUR)에 맞춰 기본 값 설정 - this.start = switch (type) { - case DATE -> (start == null) ? LocalDateTime.of(LocalDate.now().minusDays(7), LocalTime.MIN) : start; - case HOUR -> (start == null) ? LocalDateTime.now().minusDays(7) : start; - }; - this.end = switch (type) { - case DATE -> (end == null) ? LocalDateTime.of(LocalDate.now(), LocalTime.MAX) : end; - case HOUR -> (end == null) ? LocalDateTime.now() : end; - }; - validateDateRange(start, end); - - // Default 값 설정 - this.value = (value == null) ? CountValueType.COUNT : value; - - // Default 값 설정 - this.type = Optional.ofNullable(type) - .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_PARAMETER)); - } - - /** - * start 일자가 end 일자보다 앞인지 검증 - */ - private void validateDateRange(LocalDateTime start, LocalDateTime end) { - if (start.isAfter(end)) { - throw new BadRequestException(ErrorCode.ENTITY_NOT_FOUND); - } - } + String hashtag, + StatDateType type, + LocalDateTime start, + LocalDateTime end, + CountValueType value) { + + public StatParam(String hashtag, StatDateType type, LocalDateTime start, LocalDateTime end, CountValueType value) { + // JWT 구현시 default 값 변경 예정 + this.hashtag = (hashtag == null) ? "me" : hashtag; + + // Type(DATE, HOUR)에 맞춰 기본 값 설정 + this.start = switch (type) { + case DATE -> (start == null) ? LocalDateTime.of(LocalDate.now().minusDays(7), LocalTime.MIN) : start; + case HOUR -> (start == null) ? LocalDateTime.now().minusDays(7) : start; + }; + this.end = switch (type) { + case DATE -> (end == null) ? LocalDateTime.of(LocalDate.now(), LocalTime.MAX) : end; + case HOUR -> (end == null) ? LocalDateTime.now() : end; + }; + validateDateRange(start, end); + + // Default 값 설정 + this.value = (value == null) ? CountValueType.COUNT : value; + + // Default 값 설정 + this.type = Optional.ofNullable(type) + .orElseThrow(() -> new BadRequestException(ErrorCode.INVALID_PARAMETER)); + } + + /** + * start 일자가 end 일자보다 앞인지 검증 + */ + private void validateDateRange(LocalDateTime start, LocalDateTime end) { + if (start.isAfter(end)) { + throw new BadRequestException(ErrorCode.INVALID_PARAMETER); + } + } } diff --git a/src/main/java/wanted/media/exception/BadRequestException.java b/src/main/java/wanted/media/exception/BadRequestException.java new file mode 100644 index 0000000..e2a8444 --- /dev/null +++ b/src/main/java/wanted/media/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package wanted.media.exception; + +public class BadRequestException extends BaseException { + + public BadRequestException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/wanted/media/exception/BaseException.java b/src/main/java/wanted/media/exception/BaseException.java new file mode 100644 index 0000000..7b14527 --- /dev/null +++ b/src/main/java/wanted/media/exception/BaseException.java @@ -0,0 +1,10 @@ +package wanted.media.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class BaseException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/src/main/java/wanted/media/exception/ErrorCode.java b/src/main/java/wanted/media/exception/ErrorCode.java index dade125..9336a90 100644 --- a/src/main/java/wanted/media/exception/ErrorCode.java +++ b/src/main/java/wanted/media/exception/ErrorCode.java @@ -8,7 +8,8 @@ @Getter @RequiredArgsConstructor public enum ErrorCode { - ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."); + ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."), + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index c96a472..9bc4019 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -1,18 +1,18 @@ package wanted.media.exception.handler; -import org.apache.coyote.BadRequestException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import wanted.media.exception.BadRequestException; import wanted.media.exception.ErrorResponse; @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(400, e.getMessage())); - } + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException e) { + return ResponseEntity.badRequest() + .body(new ErrorResponse(400, e.getErrorCode().getMessage())); + } } From 2d8e04e8d765f4f5828123201849f87d3a8e910d Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 17:11:42 +0900 Subject: [PATCH 14/69] =?UTF-8?q?feat=20:=20api=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20security=20=EC=9E=84?= =?UTF-8?q?=EC=9D=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/config/SecurityConfig.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 0d84f89..4613053 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -1,7 +1,27 @@ package wanted.media.user.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +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.web.SecurityFilterChain; @EnableWebSecurity +@Configuration public class SecurityConfig { + // 해당 파일은 api 테스트하느라 임의로 추가해놓은 메서드입니다! + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeRequests -> + authorizeRequests + .requestMatchers("/api/statistics/**").permitAll() + .anyRequest().authenticated() // 그 외의 경로는 인증 필요 + ) + .anonymous(Customizer.withDefaults()) + .build(); + } } From 7851d342d25b73408d4a12a21f753a80e6533ca1 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 20:25:04 +0900 Subject: [PATCH 15/69] =?UTF-8?q?refactor=20:=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit post 테이블 명 변경 post -> posts --- .../wanted/media/content/domain/Post.java | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/java/wanted/media/content/domain/Post.java b/src/main/java/wanted/media/content/domain/Post.java index ceff407..8938183 100644 --- a/src/main/java/wanted/media/content/domain/Post.java +++ b/src/main/java/wanted/media/content/domain/Post.java @@ -27,41 +27,41 @@ @Entity @Getter -@Table(name = "post") +@Table(name = "posts") @NoArgsConstructor(access = AccessLevel.PROTECTED) @EntityListeners(AuditingEntityListener.class) public class Post { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "post_id", nullable = false) - private String id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_id", nullable = false) + private String id; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private Type type; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Type type; - @Size(max = 150) - @Column(nullable = false) - private String title; - private String content; - private String hashtags; + @Size(max = 150) + @Column(nullable = false) + private String title; + private String content; + private String hashtags; - @ColumnDefault("0") - private Long likeCount; + @ColumnDefault("0") + private Long likeCount; - @ColumnDefault("0") - private Long viewCount; + @ColumnDefault("0") + private Long viewCount; - @ColumnDefault("0") - private Long shareCount; + @ColumnDefault("0") + private Long shareCount; - @CreatedDate - private LocalDateTime createdAt; + @CreatedDate + private LocalDateTime createdAt; - @LastModifiedDate - private LocalDateTime updatedAt; + @LastModifiedDate + private LocalDateTime updatedAt; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; } From 24fbc9294eaf1f9c88c36c26c701761b3122ed05 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 25 Aug 2024 20:27:37 +0900 Subject: [PATCH 16/69] =?UTF-8?q?refactor=20:=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit post 테이블의 id 타입을 String으로 변경하면서 GeneratedValue 어노테이션 제거 --- src/main/java/wanted/media/content/domain/Post.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/wanted/media/content/domain/Post.java b/src/main/java/wanted/media/content/domain/Post.java index 8938183..fca737d 100644 --- a/src/main/java/wanted/media/content/domain/Post.java +++ b/src/main/java/wanted/media/content/domain/Post.java @@ -13,8 +13,6 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; @@ -32,7 +30,6 @@ @EntityListeners(AuditingEntityListener.class) public class Post { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "post_id", nullable = false) private String id; From 5c6d9b842a13d9edcb30d491c710d54c037ce002 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Sun, 25 Aug 2024 22:19:19 +0900 Subject: [PATCH 17/69] =?UTF-8?q?feat=20:=20api=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20security=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/config/SecurityConfig.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 320ed5b..49c98e5 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -2,8 +2,10 @@ 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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; @EnableWebSecurity @Configuration @@ -13,4 +15,19 @@ public class SecurityConfig { public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + // 임시 설정 + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().permitAll() // 모든 요청 허용 + ) + .csrf().disable() // CSRF 보호 비활성화 (테스트 목적으로만 사용) + .formLogin().disable() // 로그인 폼 비활성화 + .httpBasic().disable(); // HTTP Basic 인증 비활성화 + + return http.build(); + } } From 41909aad126733a3eb7e5d6acc90b526c858bde6 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Sun, 25 Aug 2024 22:22:57 +0900 Subject: [PATCH 18/69] =?UTF-8?q?feat=20:=20Code=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20@Builder=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/domain/Code.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/wanted/media/user/domain/Code.java b/src/main/java/wanted/media/user/domain/Code.java index 75bd557..742920d 100644 --- a/src/main/java/wanted/media/user/domain/Code.java +++ b/src/main/java/wanted/media/user/domain/Code.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.CreatedDate; @@ -12,6 +13,7 @@ @NoArgsConstructor @AllArgsConstructor @Getter +@Builder @Entity @Table(name = "codes") public class Code { From 2ee34cb9a240c950966df1b28091f3bdf1bfc4fe Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Sun, 25 Aug 2024 22:24:24 +0900 Subject: [PATCH 19/69] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/controller/UserController.java | 15 ++- .../wanted/media/user/dto/SignUpRequest.java | 22 +++++ .../wanted/media/user/dto/SignUpResponse.java | 14 +++ .../wanted/media/user/dto/UserCreateDto.java | 13 +++ .../media/user/repository/CodeRepository.java | 10 ++ .../media/user/repository/UserRepository.java | 6 ++ .../media/user/service/GenerateCode.java | 24 +++++ .../media/user/service/UserService.java | 49 ++++++++++ .../media/user/service/UserValidator.java | 98 +++++++++++++++++++ 9 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/main/java/wanted/media/user/dto/SignUpRequest.java create mode 100644 src/main/java/wanted/media/user/dto/SignUpResponse.java create mode 100644 src/main/java/wanted/media/user/dto/UserCreateDto.java create mode 100644 src/main/java/wanted/media/user/repository/CodeRepository.java create mode 100644 src/main/java/wanted/media/user/service/GenerateCode.java create mode 100644 src/main/java/wanted/media/user/service/UserValidator.java diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 867ccff..6e82349 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -1,14 +1,27 @@ package wanted.media.user.controller; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wanted.media.user.dto.SignUpRequest; +import wanted.media.user.dto.SignUpResponse; import wanted.media.user.service.UserService; @RestController -@RequestMapping("/user") +@RequestMapping("/api/user") @RequiredArgsConstructor public class UserController { private final UserService userService; + + @PostMapping("/sign-up") + public ResponseEntity signUp(@Validated @RequestBody SignUpRequest request) { + SignUpResponse response = userService.signUp(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } } diff --git a/src/main/java/wanted/media/user/dto/SignUpRequest.java b/src/main/java/wanted/media/user/dto/SignUpRequest.java new file mode 100644 index 0000000..3b2cb4f --- /dev/null +++ b/src/main/java/wanted/media/user/dto/SignUpRequest.java @@ -0,0 +1,22 @@ +package wanted.media.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class SignUpRequest { + @NotBlank + @Size(max = 50) + private String account; + + @NotBlank + @Email + @Size(max = 50) + private String email; + + @NotBlank + @Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") + private String password; +} diff --git a/src/main/java/wanted/media/user/dto/SignUpResponse.java b/src/main/java/wanted/media/user/dto/SignUpResponse.java new file mode 100644 index 0000000..b16ae2f --- /dev/null +++ b/src/main/java/wanted/media/user/dto/SignUpResponse.java @@ -0,0 +1,14 @@ +package wanted.media.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class SignUpResponse { + private String message; + private UserCreateDto userCreateDto; // 사용자 정보 DTO + private String authCode; // 사용자 인증코드 +} diff --git a/src/main/java/wanted/media/user/dto/UserCreateDto.java b/src/main/java/wanted/media/user/dto/UserCreateDto.java new file mode 100644 index 0000000..fc7e4b5 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserCreateDto.java @@ -0,0 +1,13 @@ +package wanted.media.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class UserCreateDto { + private String account; + private String email; +} diff --git a/src/main/java/wanted/media/user/repository/CodeRepository.java b/src/main/java/wanted/media/user/repository/CodeRepository.java new file mode 100644 index 0000000..d24c229 --- /dev/null +++ b/src/main/java/wanted/media/user/repository/CodeRepository.java @@ -0,0 +1,10 @@ +package wanted.media.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wanted.media.user.domain.Code; +import wanted.media.user.domain.User; + +public interface CodeRepository extends JpaRepository { + // 사용자별 인증코드 중복확인 + boolean existsByUserAndAuthCode(User user, String newAuthCode); +} diff --git a/src/main/java/wanted/media/user/repository/UserRepository.java b/src/main/java/wanted/media/user/repository/UserRepository.java index 51fa606..f48d108 100644 --- a/src/main/java/wanted/media/user/repository/UserRepository.java +++ b/src/main/java/wanted/media/user/repository/UserRepository.java @@ -3,7 +3,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import wanted.media.user.domain.User; +import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { + // 사용자 계정으로 회원 조회 + Optional findByAccount(String account); + + // 사용자 이메일로 회원 조회 + Optional findByEmail(String email); } diff --git a/src/main/java/wanted/media/user/service/GenerateCode.java b/src/main/java/wanted/media/user/service/GenerateCode.java new file mode 100644 index 0000000..c12c858 --- /dev/null +++ b/src/main/java/wanted/media/user/service/GenerateCode.java @@ -0,0 +1,24 @@ +package wanted.media.user.service; + +import org.springframework.stereotype.Component; + +import java.util.Random; + +@Component +public class GenerateCode { + private int codeLength = 6; //6자리 코드 + private final char[] characterTable = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}; + + public String codeGenerate() { + Random random = new Random(System.currentTimeMillis()); + int tableLength = characterTable.length; + StringBuilder code = new StringBuilder(); + + for (int i = 0; i < codeLength; i++) { + code.append(characterTable[random.nextInt(tableLength)]); + } + return code.toString(); + } +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index e642526..10dd14b 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -1,12 +1,61 @@ package wanted.media.user.service; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import wanted.media.user.domain.Code; +import wanted.media.user.domain.Grade; +import wanted.media.user.domain.User; +import wanted.media.user.dto.SignUpRequest; +import wanted.media.user.dto.SignUpResponse; +import wanted.media.user.dto.UserCreateDto; +import wanted.media.user.repository.CodeRepository; import wanted.media.user.repository.UserRepository; +import java.time.LocalDateTime; + @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final CodeRepository codeRepository; + private final BCryptPasswordEncoder passwordEncoder; + private final UserValidator userValidator; + private final GenerateCode generateCode; + + + //회원가입 + public SignUpResponse signUp(SignUpRequest request) { + // 1. 사용자 입력내용 검증 + userValidator.validateRequest(request); + // 2. 비밀번호 암호화 + String encodedPassword = passwordEncoder.encode(request.getPassword()); + // 3. 인증코드 생성 + String verificationCode = generateCode.codeGenerate(); + // 4. User 객체 생성 + User user = User.builder() + .account(request.getAccount()) + .email(request.getEmail()) + .password(encodedPassword) + .grade(Grade.NORMAL_USER) + .build(); + // 5. 사용자 db 저장 + userRepository.save(user); + // 6. Code 객체 생성 + Code code = Code.builder() + .user(user) + .authCode(verificationCode) + .createdTime(LocalDateTime.now()) + .build(); + // 7. 인증코드 db 저장 + codeRepository.save(code); + // 8. UserCreateDto 생성 + UserCreateDto userCreateDto = new UserCreateDto(user.getAccount(), user.getEmail()); + // 9. SignUpResponse 생성 + SignUpResponse signUpResponse = new SignUpResponse("회원가입이 성공적으로 완료됐습니다.", userCreateDto, verificationCode); + + return signUpResponse; + } + } diff --git a/src/main/java/wanted/media/user/service/UserValidator.java b/src/main/java/wanted/media/user/service/UserValidator.java new file mode 100644 index 0000000..69f2cca --- /dev/null +++ b/src/main/java/wanted/media/user/service/UserValidator.java @@ -0,0 +1,98 @@ +package wanted.media.user.service; + +import org.springframework.stereotype.Component; +import wanted.media.user.dto.SignUpRequest; +import wanted.media.user.repository.UserRepository; + +@Component +public class UserValidator { + private final UserRepository userRepository; + + public UserValidator(UserRepository userRepository) { + this.userRepository = userRepository; + } + + /* + * 회원가입 시 사용자 정보 중복 확인 + * */ + public void validateRequest(SignUpRequest signUpRequest) { + //account 중복 확인 + if (userRepository.findByAccount(signUpRequest.getAccount()).isPresent()) { + throw new IllegalArgumentException("이미 존재하는 계정입니다."); + } + //email 중복 확인 + if (userRepository.findByEmail(signUpRequest.getEmail()).isPresent()) { + throw new IllegalArgumentException("이미 존재하는 이메일입니다."); + } + //비밀번호 유효성 검사 + validatePassword(signUpRequest.getPassword(), signUpRequest.getAccount(), signUpRequest.getEmail()); + } + + //비밀번호 유효성 검사 + private void validatePassword(String password, String account, String email) { + // 1. 10자리 미만일 경우 + if (password.length() < 10) { + throw new IllegalArgumentException("비밀번호는 최소 10자리 이상으로 설정해주세요."); + } + // 2. 숫자로만 설정된 경우 + if (password.chars().allMatch(Character::isDigit)) { + throw new IllegalArgumentException("비밀번호가 숫자로만 이루어졌습니다."); + } + // 3. 숫자, 문자, 특수문자 중 최소 2가지 이상 포함하지 않은 경우 + if (!password.matches(".*[a-zA-Z].*") || !password.matches(".*[0-9!@#$%^&*].*")) { + throw new IllegalArgumentException("비밀번호는 최소 숫자, 문자, 특수문자 중 최소 2가지를 포함해야 합니다."); + } + // 4. 통상적으로 자주 사용되는 비밀번호인 경우 + if (containsCommonPasswords(password)) { + throw new IllegalArgumentException("통상적으로 자주 사용되는 비밀번호는 사용할 수 없습니다."); + } + // 5. 연속된 문자열을 사용한 경우 + if (hasSequentialCharacters(password)) { + throw new IllegalArgumentException("3회 이상 연속되는 문자 사용은 불가능합니다."); + } + // 6. 사용자 개인정보를 포함한 경우 + if (isSimilarToPersonalInfo(password, account, email)) { + throw new IllegalArgumentException("사용자 개인정보가 포함된 비밀번호입니다."); + } + } + + // 통상적으로 자주 사용되는 비밀번호 검사 + private boolean containsCommonPasswords(String password) { + String[] commonPasswords = {"123456", "password", "123456789", "qwerty", "abc123"}; + for (String commonPassword : commonPasswords) { + if (password.equalsIgnoreCase(commonPassword)) { + return true; + } + } + return false; + } + + // 3회 이상 연속된 문자열을 사용한 비밀번호 검사 + private boolean hasSequentialCharacters(String password) { + if (password == null || password.length() < 3) { + return false; + } + + int seqCount = 1; + for (int i = 1; i < password.length(); i++) { + // 현재 문자와 이전 문자의 차이를 비교 + if (password.charAt(i) == password.charAt(i - 1) + 1) { + seqCount++; + if (seqCount >= 3) { + return true; + } + } else { + seqCount = 1; + } + } + return false; + } + + + // 사용자 개인정보를 포함한 경우 비밀번호 검사 + private boolean isSimilarToPersonalInfo(String password, String account, String email) { + String emailLocalPart = email.split("@")[0]; // email 사용자명 부분 + return password.toLowerCase().contains(emailLocalPart.toLowerCase()) || + password.toLowerCase().contains(account.toLowerCase()); + } +} From 376565708a67cfcd60ab305ef1742f23f863ddab Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:28:59 +0900 Subject: [PATCH 20/69] =?UTF-8?q?refactor=20:=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=20=EC=9C=84=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A7=81=20=EC=B6=94=EA=B0=80=20-=20TokenProvider=EC=9D=98=20m?= =?UTF-8?q?akeToken=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20=ED=9A=8C=EC=9B=90=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=9D=98=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/exception/NotFoundException.java | 7 +++++++ .../handler/GlobalExceptionHandler.java | 18 ++++++++++++------ .../media/user/config/TokenProvider.java | 7 ++++++- .../media/user/service/TokenService.java | 4 +++- .../media/user/service/UserDetailService.java | 3 ++- 5 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 src/main/java/wanted/media/exception/NotFoundException.java diff --git a/src/main/java/wanted/media/exception/NotFoundException.java b/src/main/java/wanted/media/exception/NotFoundException.java new file mode 100644 index 0000000..b86cf33 --- /dev/null +++ b/src/main/java/wanted/media/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index c96a472..c86c3c0 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -1,18 +1,24 @@ package wanted.media.exception.handler; import org.apache.coyote.BadRequestException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; - import wanted.media.exception.ErrorResponse; +import wanted.media.exception.NotFoundException; @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(400, e.getMessage())); - } + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException e) { + return ResponseEntity.badRequest() + .body(new ErrorResponse(400, e.getMessage())); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } } diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index 839c6bd..2700f52 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -33,7 +33,12 @@ public String makeToken(String account, String type) { return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 - .setClaims(Jwts.claims().setSubject(account).setAudience(type).setIssuedAt(now).setExpiration(new Date(now.getTime() + time))) + .setClaims(Jwts.claims(). + setSubject(account). + setAudience(type). + setIssuedAt(now). + setExpiration(new Date(now.getTime() + time)) + ) .signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화 .compact(); } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 0c00375..1801ab4 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import wanted.media.exception.NotFoundException; import wanted.media.user.config.TokenProvider; import wanted.media.user.domain.Token; import wanted.media.user.domain.User; @@ -48,7 +49,8 @@ public User findUserByToken(TokenRequestDto requestDto) { Authentication authentication = tokenProvider.getAuthentication(requestDto.getAccessToken()); UserDetail userDetail = (UserDetail) authentication.getPrincipal(); String account = userDetail.getUsername(); - return userRepository.findByAccount(account).orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다.")); + return userRepository.findByAccount(account) + .orElseThrow(() -> new NotFoundException("회원을 찾을 수 없습니다.")); } } diff --git a/src/main/java/wanted/media/user/service/UserDetailService.java b/src/main/java/wanted/media/user/service/UserDetailService.java index c282738..c5d1a6d 100644 --- a/src/main/java/wanted/media/user/service/UserDetailService.java +++ b/src/main/java/wanted/media/user/service/UserDetailService.java @@ -7,6 +7,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import wanted.media.exception.NotFoundException; import wanted.media.user.domain.User; import wanted.media.user.domain.UserDetail; import wanted.media.user.repository.UserRepository; @@ -23,7 +24,7 @@ public class UserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException { User user = userRepository.findByAccount(account) - .orElseThrow(() -> new IllegalArgumentException("계정이 존재하지 않습니다.")); + .orElseThrow(() -> new NotFoundException("계정이 존재하지 않습니다.")); List roles = new ArrayList<>(); roles.add(new SimpleGrantedAuthority(user.getGrade().toString())); From d888f838f77904851006167eefeec9481163a04f Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:35:30 +0900 Subject: [PATCH 21/69] =?UTF-8?q?refactor=20:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/TokenProvider.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index 2700f52..c034492 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -21,9 +21,10 @@ public class TokenProvider { @Value("${jwt.secret_key}") private String key; - - private long tokenValidTime = 1000L * 60 * 60; // 1시간 - private long RefreshTokenValidTime = 1000L * 60 * 60 * 24 * 7; // 7일 + @Value("${jwt.access_token_expiration}") + private long tokenValidTime; + @Value("${jwt.refresh_token_expiration}") + private long RefreshTokenValidTime; private final UserDetailService userDetailService; From 848ce307fde40ffc508e2785ed6d66e776827c05 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:48:56 +0900 Subject: [PATCH 22/69] =?UTF-8?q?refactor=20:=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/SecurityConfig.java | 1 - src/main/java/wanted/media/user/domain/UserDetail.java | 1 - src/main/java/wanted/media/user/dto/UserLoginRequestDto.java | 1 - src/main/java/wanted/media/user/service/TokenService.java | 1 - src/main/java/wanted/media/user/service/UserDetailService.java | 1 - src/main/java/wanted/media/user/service/UserService.java | 1 - 6 files changed, 6 deletions(-) diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 2970150..0957987 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -15,7 +15,6 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - private final TokenProvider tokenProvider; @Bean diff --git a/src/main/java/wanted/media/user/domain/UserDetail.java b/src/main/java/wanted/media/user/domain/UserDetail.java index 55177f5..899d9e9 100644 --- a/src/main/java/wanted/media/user/domain/UserDetail.java +++ b/src/main/java/wanted/media/user/domain/UserDetail.java @@ -8,7 +8,6 @@ import java.util.List; public class UserDetail implements UserDetails { - private String account; private String password; private List authorities; diff --git a/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java index cc46ffd..820b751 100644 --- a/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java +++ b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java @@ -9,7 +9,6 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class UserLoginRequestDto { - private String account; private String password; } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 1801ab4..568a048 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -16,7 +16,6 @@ @Service @RequiredArgsConstructor public class TokenService { - private final TokenProvider tokenProvider; private final TokenRepository tokenRepository; private final UserRepository userRepository; diff --git a/src/main/java/wanted/media/user/service/UserDetailService.java b/src/main/java/wanted/media/user/service/UserDetailService.java index c5d1a6d..4795e4d 100644 --- a/src/main/java/wanted/media/user/service/UserDetailService.java +++ b/src/main/java/wanted/media/user/service/UserDetailService.java @@ -18,7 +18,6 @@ @Service @RequiredArgsConstructor public class UserDetailService implements UserDetailsService { - private final UserRepository userRepository; @Override diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 5b1e8bf..779a35a 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -15,7 +15,6 @@ @Service @RequiredArgsConstructor public class UserService { - private final UserRepository userRepository; private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; From 979198ca75a4fec16238c4ce390de211e3f7caa3 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:55:05 +0900 Subject: [PATCH 23/69] =?UTF-8?q?refactor=20:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=AA=85=EC=B9=AD=20=EC=88=98=EC=A0=95=20-=20TokenController?= =?UTF-8?q?=EA=B3=BC=20TokenService=EC=9D=98=20=EC=9E=AC=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=AA=85=EC=B9=AD=20getToken?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20-=20=EC=A7=81?= =?UTF-8?q?=EA=B4=80=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=B4=20URL=EB=8F=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/controller/TokenController.java | 8 ++++---- src/main/java/wanted/media/user/service/TokenService.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/wanted/media/user/controller/TokenController.java b/src/main/java/wanted/media/user/controller/TokenController.java index 85da174..f4f5d37 100644 --- a/src/main/java/wanted/media/user/controller/TokenController.java +++ b/src/main/java/wanted/media/user/controller/TokenController.java @@ -12,15 +12,15 @@ import wanted.media.user.service.TokenService; @RestController -@RequestMapping("/api/token") +@RequestMapping("/api") @RequiredArgsConstructor public class TokenController { private final TokenService tokenService; - @PostMapping("/reissue") - public ResponseEntity reIssueToken(@RequestBody TokenRequestDto requestDto) { - TokenResponseDto responseDto = tokenService.reIssueToken(requestDto); + @PostMapping("/token") + public ResponseEntity getToken(@RequestBody TokenRequestDto requestDto) { + TokenResponseDto responseDto = tokenService.getToken(requestDto); return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); } } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 568a048..e07cc30 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -20,8 +20,8 @@ public class TokenService { private final TokenRepository tokenRepository; private final UserRepository userRepository; - // 액세스 토큰 재발행 - public TokenResponseDto reIssueToken(TokenRequestDto requestDto) { + // 액세스 토큰, 리프레시 토큰 재발행 + public TokenResponseDto getToken(TokenRequestDto requestDto) { if (!tokenProvider.validToken(requestDto.getRefreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 throw new IllegalArgumentException("다시 로그인해주세요."); } From e2ff106856fcc0de9cadc572612500eefb4ba600 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:59:29 +0900 Subject: [PATCH 24/69] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=AA=85=EC=B9=AD=20?= =?UTF-8?q?=E3=85=85=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/controller/UserController.java | 2 +- src/main/java/wanted/media/user/service/UserService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 91027ec..9191334 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -19,7 +19,7 @@ public class UserController { @PostMapping("/login") public ResponseEntity loginUser(@RequestBody UserLoginRequestDto requestDto) { - UserLoginResponseDto responseDto = userService.loginUser(requestDto); + UserLoginResponseDto responseDto = userService.login(requestDto); return ResponseEntity.ok().body(responseDto); } } diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 779a35a..4e0fecf 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -19,7 +19,7 @@ public class UserService { private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; - public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { + public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 From 9455bed04006953e0bd94c3ac4d4085f30266656 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:19:58 +0900 Subject: [PATCH 25/69] =?UTF-8?q?refactor=20:=20TokenRequestDto=20record?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/user/dto/TokenRequestDto.java | 12 +----------- .../java/wanted/media/user/service/TokenService.java | 6 +++--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/main/java/wanted/media/user/dto/TokenRequestDto.java b/src/main/java/wanted/media/user/dto/TokenRequestDto.java index f3c22bb..d734138 100644 --- a/src/main/java/wanted/media/user/dto/TokenRequestDto.java +++ b/src/main/java/wanted/media/user/dto/TokenRequestDto.java @@ -1,14 +1,4 @@ package wanted.media.user.dto; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class TokenRequestDto { - private String accessToken; - private String refreshToken; +public record TokenRequestDto(String accessToken, String refreshToken) { } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index e07cc30..6b44669 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -22,7 +22,7 @@ public class TokenService { // 액세스 토큰, 리프레시 토큰 재발행 public TokenResponseDto getToken(TokenRequestDto requestDto) { - if (!tokenProvider.validToken(requestDto.getRefreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 + if (!tokenProvider.validToken(requestDto.refreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 throw new IllegalArgumentException("다시 로그인해주세요."); } @@ -31,7 +31,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { Token storedToken = tokenRepository.findByUserId(user.getUserId()) .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 토큰입니다.")); - if (!storedToken.getRefreshToken().equals(requestDto.getRefreshToken())) { + if (!storedToken.getRefreshToken().equals(requestDto.refreshToken())) { throw new IllegalArgumentException("유효하지 않은 토큰입니다."); } @@ -45,7 +45,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { } public User findUserByToken(TokenRequestDto requestDto) { - Authentication authentication = tokenProvider.getAuthentication(requestDto.getAccessToken()); + Authentication authentication = tokenProvider.getAuthentication(requestDto.accessToken()); UserDetail userDetail = (UserDetail) authentication.getPrincipal(); String account = userDetail.getUsername(); return userRepository.findByAccount(account) From e0ac706d4513782d3c3cfb9326238175d950a974 Mon Sep 17 00:00:00 2001 From: Jinhui Date: Sun, 25 Aug 2024 23:34:52 +0900 Subject: [PATCH 26/69] =?UTF-8?q?fix=20:=20String=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=9D=80=20IDENTITY=20=EC=A0=81=EC=9A=A9=ED=95=A0=20=EC=88=98?= =?UTF-8?q?=20=EC=97=86=EC=96=B4=EC=84=9C=20=EC=A0=9C=EA=B1=B0=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/post/domain/Post.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/wanted/media/post/domain/Post.java b/src/main/java/wanted/media/post/domain/Post.java index 858f054..6a24fdb 100644 --- a/src/main/java/wanted/media/post/domain/Post.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -20,7 +20,6 @@ @EntityListeners(AuditingEntityListener.class) public class Post { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "post_id", nullable = false) private String id; From e21ab02ae4e45a514c69d82db82561d484182891 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:35:28 +0900 Subject: [PATCH 27/69] =?UTF-8?q?refactor=20:=20JPA=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EA=B0=90=EC=A7=80=20=EA=B8=B0=EB=8A=A5=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20save()=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/service/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 4e0fecf..686e7a8 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -28,7 +28,7 @@ public UserLoginResponseDto login(UserLoginRequestDto requestDto) { Optional refreshToken = tokenRepository.findByUserId(user.getUserId()); // 리프레시 토큰 있는지 확인 String newRefreshToken = tokenProvider.makeToken(requestDto.getAccount(), "refresh"); // 새 리프레시 토큰 if (refreshToken.isPresent()) { // 리프레시 토큰 있을 경우 - tokenRepository.save(refreshToken.get().updateToken(newRefreshToken)); // 새 토큰으로 업데이트 + refreshToken.get().updateToken(newRefreshToken); // 새 토큰으로 업데이트 } else { // 리프레시 토큰 없을 경우 tokenRepository.save(new Token(newRefreshToken, user)); // 새 토큰 저장 } From 382d0322ecae008533b9e3d0194187f56a6bdce8 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:50:04 +0900 Subject: [PATCH 28/69] =?UTF-8?q?refactor=20:=20Service=EC=97=90=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/service/TokenService.java | 3 +++ src/main/java/wanted/media/user/service/UserService.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 6b44669..defd287 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import wanted.media.exception.NotFoundException; import wanted.media.user.config.TokenProvider; import wanted.media.user.domain.Token; @@ -21,6 +22,7 @@ public class TokenService { private final UserRepository userRepository; // 액세스 토큰, 리프레시 토큰 재발행 + @Transactional public TokenResponseDto getToken(TokenRequestDto requestDto) { if (!tokenProvider.validToken(requestDto.refreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 throw new IllegalArgumentException("다시 로그인해주세요."); @@ -44,6 +46,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { return new TokenResponseDto(accessToken, refreshToken); } + @Transactional(readOnly = true) public User findUserByToken(TokenRequestDto requestDto) { Authentication authentication = tokenProvider.getAuthentication(requestDto.accessToken()); UserDetail userDetail = (UserDetail) authentication.getPrincipal(); diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 686e7a8..0800bd8 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import wanted.media.user.config.TokenProvider; import wanted.media.user.domain.Token; import wanted.media.user.domain.User; @@ -19,6 +20,7 @@ public class UserService { private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; + @Transactional public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); From 22cfabc2b3f08acec88482be958bae187444639b Mon Sep 17 00:00:00 2001 From: Jinhui Date: Sun, 25 Aug 2024 23:54:54 +0900 Subject: [PATCH 29/69] =?UTF-8?q?feat=20:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/controller/PostLikeController.java | 37 ++++++++++++++ .../post/repository/PostLikeRepository.java | 16 ++++++ .../media/post/service/PostLikeService.java | 49 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/main/java/wanted/media/post/controller/PostLikeController.java create mode 100644 src/main/java/wanted/media/post/repository/PostLikeRepository.java create mode 100644 src/main/java/wanted/media/post/service/PostLikeService.java diff --git a/src/main/java/wanted/media/post/controller/PostLikeController.java b/src/main/java/wanted/media/post/controller/PostLikeController.java new file mode 100644 index 0000000..af10b57 --- /dev/null +++ b/src/main/java/wanted/media/post/controller/PostLikeController.java @@ -0,0 +1,37 @@ +package wanted.media.post.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wanted.media.post.service.PostLikeService; + +@RestController +@RequestMapping("/api/posts") +@RequiredArgsConstructor +public class PostLikeController { + + private final PostLikeService postLikeService; + + @PostMapping("/likes/{postId}") + public ResponseEntity getLikes(@PathVariable(name = "postId") String postId) { + if (postId == null) { + return ResponseEntity.badRequest().body("잘못된 요청입니다."); + } + + // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) + try { + String endpoint = postLikeService.makeEndpoint(postId); + // 요구사항 시나리오에 따라 필요하지만 실제 동작하지 않기에 주석 처리함 +// RestTemplate restTemplate = new RestTemplate(); +// String response = restTemplate.postForObject(endpoint, null, String.class); + // 외부 데이터와 동기 시키는게 정상적인 동작이나, 본 과제에서는 내부에서 좋아요 수를 증가 시키도록 처리함 + postLikeService.increaseLike(postId); + return ResponseEntity.ok().body("좋아요 수 증가 완료"); + } catch (Exception e) { + return ResponseEntity.internalServerError().body("SNS API 호출 실패: " + e.getMessage()); + } + } +} diff --git a/src/main/java/wanted/media/post/repository/PostLikeRepository.java b/src/main/java/wanted/media/post/repository/PostLikeRepository.java new file mode 100644 index 0000000..cd71f07 --- /dev/null +++ b/src/main/java/wanted/media/post/repository/PostLikeRepository.java @@ -0,0 +1,16 @@ +package wanted.media.post.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import wanted.media.post.domain.Post; + +public interface PostLikeRepository extends JpaRepository { + + @Transactional + @Modifying(clearAutomatically = true) + @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 WHERE p.id = :postId") + void incrementLikeCount(@Param("postId") String postId); +} diff --git a/src/main/java/wanted/media/post/service/PostLikeService.java b/src/main/java/wanted/media/post/service/PostLikeService.java new file mode 100644 index 0000000..81c529e --- /dev/null +++ b/src/main/java/wanted/media/post/service/PostLikeService.java @@ -0,0 +1,49 @@ +package wanted.media.post.service; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; +import wanted.media.exception.ErrorCode; +import wanted.media.post.domain.Post; +import wanted.media.post.repository.PostLikeRepository; + +@Service +@RequiredArgsConstructor +public class PostLikeService { + + private final PostLikeRepository postLikeRepository; + + @Transactional(readOnly = true) + public String makeEndpoint(String postId) { + // contentId를 통해 게시물의 SNS 유형 조회 + Post post = postLikeRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException(ErrorCode.ENTITY_NOT_FOUND.getMessage())); + String snsType = post.getType().name(); + + String endpoint = ""; + // SNS 유형에 따른 외부 엔드포인트 구성 + switch (snsType.toLowerCase()) { + case "facebook": + endpoint = "https://www.facebook.com/likes/" + postId; + break; + case "twitter": + endpoint = "https://www.twitter.com/likes/" + postId; + break; + case "instagram": + endpoint = "https://www.instagram.com/likes/" + postId; + break; + case "threads": + endpoint = "https://www.threads.net/likes/" + postId; + break; + default: + endpoint = null; + } + return endpoint; + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public void increaseLike(String postId) { + postLikeRepository.incrementLikeCount(postId); + } +} From 720d726ca48b4df1b8ccd019a516b6eb8dc56f80 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:59:07 +0900 Subject: [PATCH 30/69] =?UTF-8?q?refactor=20:TokenProvider=EC=97=90=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/TokenProvider.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index c034492..070efef 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -25,6 +25,7 @@ public class TokenProvider { private long tokenValidTime; @Value("${jwt.refresh_token_expiration}") private long RefreshTokenValidTime; + private final String BEARER_PREFIX = "Bearer "; private final UserDetailService userDetailService; @@ -75,8 +76,8 @@ public String getUserAccount(String token) { // 토큰 Header에서 꺼내오기 public String resolveToken(HttpServletRequest request) { String header = request.getHeader("Authorization"); - if (header != null && header.startsWith("Bearer ")) - return header.substring(7); + if (header != null && header.startsWith(BEARER_PREFIX)) + return header.substring(BEARER_PREFIX.length()); return null; } } From 679147710d30e546f11473ec02d8830b2ce943a2 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 00:25:37 +0900 Subject: [PATCH 31/69] =?UTF-8?q?refactor=20:=20TokenRepository=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EB=AC=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/user/repository/TokenRepository.java | 4 +--- src/main/java/wanted/media/user/service/TokenService.java | 2 +- src/main/java/wanted/media/user/service/UserService.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/wanted/media/user/repository/TokenRepository.java b/src/main/java/wanted/media/user/repository/TokenRepository.java index efd7f78..09fe379 100644 --- a/src/main/java/wanted/media/user/repository/TokenRepository.java +++ b/src/main/java/wanted/media/user/repository/TokenRepository.java @@ -1,7 +1,6 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import wanted.media.user.domain.Token; @@ -9,6 +8,5 @@ import java.util.UUID; public interface TokenRepository extends JpaRepository { - @Query("SELECT t FROM Token t WHERE t.user.userId = :userId") - Optional findByUserId(@Param("userId") UUID userID); + Optional findByUser_UserId(@Param("userId") UUID userId); } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index defd287..0fb2b7f 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -30,7 +30,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { User user = findUserByToken(requestDto); - Token storedToken = tokenRepository.findByUserId(user.getUserId()) + Token storedToken = tokenRepository.findByUser_UserId(user.getUserId()) .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 토큰입니다.")); if (!storedToken.getRefreshToken().equals(requestDto.refreshToken())) { diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 0800bd8..c391357 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -27,7 +27,7 @@ public UserLoginResponseDto login(UserLoginRequestDto requestDto) { if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); - Optional refreshToken = tokenRepository.findByUserId(user.getUserId()); // 리프레시 토큰 있는지 확인 + Optional refreshToken = tokenRepository.findByUser_UserId(user.getUserId()); // 리프레시 토큰 있는지 확인 String newRefreshToken = tokenProvider.makeToken(requestDto.getAccount(), "refresh"); // 새 리프레시 토큰 if (refreshToken.isPresent()) { // 리프레시 토큰 있을 경우 refreshToken.get().updateToken(newRefreshToken); // 새 토큰으로 업데이트 From a5a25d742764e8a4f04516b04db9b769ee61e9c6 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 02:53:29 +0900 Subject: [PATCH 32/69] =?UTF-8?q?refactor=20:=20=20UserCreateDto=20?= =?UTF-8?q?=E2=86=92=20UserInfoDto=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/user/dto/SignUpResponse.java | 2 +- .../java/wanted/media/user/dto/UserInfoDto.java | 13 +++++++++++++ .../java/wanted/media/user/service/UserService.java | 6 +++--- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 src/main/java/wanted/media/user/dto/UserInfoDto.java diff --git a/src/main/java/wanted/media/user/dto/SignUpResponse.java b/src/main/java/wanted/media/user/dto/SignUpResponse.java index b16ae2f..877d276 100644 --- a/src/main/java/wanted/media/user/dto/SignUpResponse.java +++ b/src/main/java/wanted/media/user/dto/SignUpResponse.java @@ -9,6 +9,6 @@ @Getter public class SignUpResponse { private String message; - private UserCreateDto userCreateDto; // 사용자 정보 DTO + private UserInfoDto userInfoDto; // 사용자 정보 DTO private String authCode; // 사용자 인증코드 } diff --git a/src/main/java/wanted/media/user/dto/UserInfoDto.java b/src/main/java/wanted/media/user/dto/UserInfoDto.java new file mode 100644 index 0000000..5dcf80a --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserInfoDto.java @@ -0,0 +1,13 @@ +package wanted.media.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class UserInfoDto { + private String account; + private String email; +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 10dd14b..170b2a9 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -8,7 +8,7 @@ import wanted.media.user.domain.User; import wanted.media.user.dto.SignUpRequest; import wanted.media.user.dto.SignUpResponse; -import wanted.media.user.dto.UserCreateDto; +import wanted.media.user.dto.UserInfoDto; import wanted.media.user.repository.CodeRepository; import wanted.media.user.repository.UserRepository; @@ -51,9 +51,9 @@ public SignUpResponse signUp(SignUpRequest request) { // 7. 인증코드 db 저장 codeRepository.save(code); // 8. UserCreateDto 생성 - UserCreateDto userCreateDto = new UserCreateDto(user.getAccount(), user.getEmail()); + UserInfoDto userInfoDto = new UserInfoDto(user.getAccount(), user.getEmail()); // 9. SignUpResponse 생성 - SignUpResponse signUpResponse = new SignUpResponse("회원가입이 성공적으로 완료됐습니다.", userCreateDto, verificationCode); + SignUpResponse signUpResponse = new SignUpResponse("회원가입이 성공적으로 완료됐습니다.", userInfoDto, verificationCode); return signUpResponse; } From 49858f2518fbcce253232322dfff7bc1db62dc03 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 02:54:33 +0900 Subject: [PATCH 33/69] =?UTF-8?q?refactor=20:=20UserCreateDto=20=E2=86=92?= =?UTF-8?q?=20UserInfoDto=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/user/dto/UserCreateDto.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/main/java/wanted/media/user/dto/UserCreateDto.java diff --git a/src/main/java/wanted/media/user/dto/UserCreateDto.java b/src/main/java/wanted/media/user/dto/UserCreateDto.java deleted file mode 100644 index fc7e4b5..0000000 --- a/src/main/java/wanted/media/user/dto/UserCreateDto.java +++ /dev/null @@ -1,13 +0,0 @@ -package wanted.media.user.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class UserCreateDto { - private String account; - private String email; -} From c0c760d84acd34ed37d658c45bae6f403cd86ca7 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 02:57:17 +0900 Subject: [PATCH 34/69] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EB=93=B1?= =?UTF-8?q?=EA=B8=89=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/dto/UserInfoDto.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/wanted/media/user/dto/UserInfoDto.java b/src/main/java/wanted/media/user/dto/UserInfoDto.java index 5dcf80a..6557be3 100644 --- a/src/main/java/wanted/media/user/dto/UserInfoDto.java +++ b/src/main/java/wanted/media/user/dto/UserInfoDto.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import wanted.media.user.domain.Grade; @NoArgsConstructor @AllArgsConstructor @@ -10,4 +11,5 @@ public class UserInfoDto { private String account; private String email; + private Grade grade; // 현재 회원등급 } From 59b7b02a38ae8a9e4248435a7782249572e44a6d Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 02:59:32 +0900 Subject: [PATCH 35/69] =?UTF-8?q?feat=20:=20UserInfoDto=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EB=93=B1=EA=B8=89=20=EC=86=8D=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/service/UserService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 170b2a9..db7543e 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -50,8 +50,8 @@ public SignUpResponse signUp(SignUpRequest request) { .build(); // 7. 인증코드 db 저장 codeRepository.save(code); - // 8. UserCreateDto 생성 - UserInfoDto userInfoDto = new UserInfoDto(user.getAccount(), user.getEmail()); + // 8. UserInfoDto 생성 + UserInfoDto userInfoDto = new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade()); // 9. SignUpResponse 생성 SignUpResponse signUpResponse = new SignUpResponse("회원가입이 성공적으로 완료됐습니다.", userInfoDto, verificationCode); From 01e65fab7baee390b0d14014c7042d865d446a6a Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 03:49:04 +0900 Subject: [PATCH 36/69] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=8A=B9=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/controller/UserController.java | 14 ++++++++ .../java/wanted/media/user/domain/Code.java | 2 +- .../wanted/media/user/dto/VerifyRequest.java | 20 +++++++++++ .../wanted/media/user/dto/VerifyResponse.java | 13 +++++++ .../media/user/repository/CodeRepository.java | 9 +++-- .../media/user/repository/UserRepository.java | 9 +++++ .../media/user/service/UserService.java | 34 +++++++++++++++++-- 7 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 src/main/java/wanted/media/user/dto/VerifyRequest.java create mode 100644 src/main/java/wanted/media/user/dto/VerifyResponse.java diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 6e82349..570bcd3 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.RestController; import wanted.media.user.dto.SignUpRequest; import wanted.media.user.dto.SignUpResponse; +import wanted.media.user.dto.VerifyRequest; +import wanted.media.user.dto.VerifyResponse; import wanted.media.user.service.UserService; @RestController @@ -19,9 +21,21 @@ public class UserController { private final UserService userService; + // 회원가입 API @PostMapping("/sign-up") public ResponseEntity signUp(@Validated @RequestBody SignUpRequest request) { SignUpResponse response = userService.signUp(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } + + /* + * 가입승인 API + * 회원등급 (normal -> premium) + * */ + @PostMapping("/approve") + public ResponseEntity approveSignUp(@RequestBody VerifyRequest request) { + VerifyResponse response = userService.approveSignUp(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + } diff --git a/src/main/java/wanted/media/user/domain/Code.java b/src/main/java/wanted/media/user/domain/Code.java index 742920d..7b2d813 100644 --- a/src/main/java/wanted/media/user/domain/Code.java +++ b/src/main/java/wanted/media/user/domain/Code.java @@ -23,7 +23,7 @@ public class Code { @Column(nullable = false) private Long codeId; - @OneToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; diff --git a/src/main/java/wanted/media/user/dto/VerifyRequest.java b/src/main/java/wanted/media/user/dto/VerifyRequest.java new file mode 100644 index 0000000..a6fb53e --- /dev/null +++ b/src/main/java/wanted/media/user/dto/VerifyRequest.java @@ -0,0 +1,20 @@ +package wanted.media.user.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class VerifyRequest { + @NotBlank + @Size(max = 50) + private String account; + + @NotBlank + @Size(min = 10, max = 200) + private String password; + + @NotBlank + @Size(max = 10) + private String inputCode; //사용자 입력 인증코드 +} diff --git a/src/main/java/wanted/media/user/dto/VerifyResponse.java b/src/main/java/wanted/media/user/dto/VerifyResponse.java new file mode 100644 index 0000000..8ef1740 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/VerifyResponse.java @@ -0,0 +1,13 @@ +package wanted.media.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class VerifyResponse { + private String message; + private UserInfoDto userInfo; //사용자 정보 +} diff --git a/src/main/java/wanted/media/user/repository/CodeRepository.java b/src/main/java/wanted/media/user/repository/CodeRepository.java index d24c229..4e7fb6d 100644 --- a/src/main/java/wanted/media/user/repository/CodeRepository.java +++ b/src/main/java/wanted/media/user/repository/CodeRepository.java @@ -4,7 +4,12 @@ import wanted.media.user.domain.Code; import wanted.media.user.domain.User; +import java.util.Optional; + public interface CodeRepository extends JpaRepository { - // 사용자별 인증코드 중복확인 - boolean existsByUserAndAuthCode(User user, String newAuthCode); + //인증코드 검증 + Optional findByUserAndAuthCode(User user, String authCode); + + //사용자가 발급받은 인증코드 삭제 + void deleteByUser(User user); } diff --git a/src/main/java/wanted/media/user/repository/UserRepository.java b/src/main/java/wanted/media/user/repository/UserRepository.java index f48d108..3cff36c 100644 --- a/src/main/java/wanted/media/user/repository/UserRepository.java +++ b/src/main/java/wanted/media/user/repository/UserRepository.java @@ -1,6 +1,10 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import wanted.media.user.domain.Grade; import wanted.media.user.domain.User; import java.util.Optional; @@ -12,4 +16,9 @@ public interface UserRepository extends JpaRepository { // 사용자 이메일로 회원 조회 Optional findByEmail(String email); + + // 가입인증 회원 등급 변경 + @Modifying + @Query("UPDATE User u SET u.grade = :grade WHERE u.account = :account") + void updateUserGrade(@Param("account") String account, @Param("grade") Grade grade); } diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index db7543e..8de6478 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -1,14 +1,13 @@ package wanted.media.user.service; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import wanted.media.user.domain.Code; import wanted.media.user.domain.Grade; import wanted.media.user.domain.User; -import wanted.media.user.dto.SignUpRequest; -import wanted.media.user.dto.SignUpResponse; -import wanted.media.user.dto.UserInfoDto; +import wanted.media.user.dto.*; import wanted.media.user.repository.CodeRepository; import wanted.media.user.repository.UserRepository; @@ -16,6 +15,7 @@ @Service @RequiredArgsConstructor +@Transactional public class UserService { private final UserRepository userRepository; @@ -58,4 +58,32 @@ public SignUpResponse signUp(SignUpRequest request) { return signUpResponse; } + //가입승인 + public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { + // 1. account로 사용자 조회 + User user = userRepository.findByAccount(verifyRequest.getAccount()) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(verifyRequest.getPassword(), user.getPassword())) { + throw new RuntimeException("비밀번호가 일치하지 않습니다."); + } + // 3. 사용자 인증코드 검증 + Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.getInputCode()) + .orElseThrow(() -> new RuntimeException("인증코드가 일치하지 않습니다.")); + // 4. 인증코드 유효성 검증 (유효시간 15분) + if (code.getCreatedTime().plusMinutes(15).isBefore(LocalDateTime.now())) { + throw new RuntimeException("만료된 인증코드입니다."); + } + // 5. 인증 완료 -> 회원 등급 변경 (normal -> premium) + userRepository.updateUserGrade(user.getAccount(), Grade.PREMIUM_USER); + // 6. 인증 완료 회원 인증코드 삭제 + codeRepository.deleteByUser(user); + // 7. 변경된 사용자정보 다시 조회 + User updateUserInfo = userRepository.findByAccount(user.getAccount()) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + + return new VerifyResponse("인증이 성공적으로 완료되었습니다!", + new UserInfoDto(updateUserInfo.getAccount(), updateUserInfo.getEmail(), updateUserInfo.getGrade())); + } + } From e97334b92214e07c534210142988427ffd13b5f6 Mon Sep 17 00:00:00 2001 From: pie Date: Mon, 26 Aug 2024 10:03:46 +0900 Subject: [PATCH 37/69] =?UTF-8?q?style=20:=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/content/service/ContentService.java | 7 ------- .../media/{content => post}/controller/PostController.java | 2 +- .../java/wanted/media/{content => post}/domain/Post.java | 2 +- .../java/wanted/media/{content => post}/domain/Type.java | 2 +- src/main/java/wanted/media/post/service/PostService.java | 7 +++++++ 5 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 src/main/java/wanted/media/content/service/ContentService.java rename src/main/java/wanted/media/{content => post}/controller/PostController.java (83%) rename src/main/java/wanted/media/{content => post}/domain/Post.java (97%) rename src/main/java/wanted/media/{content => post}/domain/Type.java (63%) create mode 100644 src/main/java/wanted/media/post/service/PostService.java diff --git a/src/main/java/wanted/media/content/service/ContentService.java b/src/main/java/wanted/media/content/service/ContentService.java deleted file mode 100644 index b505adc..0000000 --- a/src/main/java/wanted/media/content/service/ContentService.java +++ /dev/null @@ -1,7 +0,0 @@ -package wanted.media.content.service; - -import org.springframework.stereotype.Service; - -@Service -public class ContentService { -} diff --git a/src/main/java/wanted/media/content/controller/PostController.java b/src/main/java/wanted/media/post/controller/PostController.java similarity index 83% rename from src/main/java/wanted/media/content/controller/PostController.java rename to src/main/java/wanted/media/post/controller/PostController.java index 6d07cac..97212e6 100644 --- a/src/main/java/wanted/media/content/controller/PostController.java +++ b/src/main/java/wanted/media/post/controller/PostController.java @@ -1,4 +1,4 @@ -package wanted.media.content.controller; +package wanted.media.post.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/wanted/media/content/domain/Post.java b/src/main/java/wanted/media/post/domain/Post.java similarity index 97% rename from src/main/java/wanted/media/content/domain/Post.java rename to src/main/java/wanted/media/post/domain/Post.java index fca737d..0e1d996 100644 --- a/src/main/java/wanted/media/content/domain/Post.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -1,4 +1,4 @@ -package wanted.media.content.domain; +package wanted.media.post.domain; import java.time.LocalDateTime; diff --git a/src/main/java/wanted/media/content/domain/Type.java b/src/main/java/wanted/media/post/domain/Type.java similarity index 63% rename from src/main/java/wanted/media/content/domain/Type.java rename to src/main/java/wanted/media/post/domain/Type.java index 539f7fa..55a18e9 100644 --- a/src/main/java/wanted/media/content/domain/Type.java +++ b/src/main/java/wanted/media/post/domain/Type.java @@ -1,4 +1,4 @@ -package wanted.media.content.domain; +package wanted.media.post.domain; public enum Type { FACEBOOK, TWITTER, INSTAGRAM, THREADS; diff --git a/src/main/java/wanted/media/post/service/PostService.java b/src/main/java/wanted/media/post/service/PostService.java new file mode 100644 index 0000000..cb52ee0 --- /dev/null +++ b/src/main/java/wanted/media/post/service/PostService.java @@ -0,0 +1,7 @@ +package wanted.media.post.service; + +import org.springframework.stereotype.Service; + +@Service +public class PostService { +} From e330442ee7f7ccee4484dcc1f1a6799baee4d58c Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 11:53:52 +0900 Subject: [PATCH 38/69] =?UTF-8?q?refactor=20:=20yml=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/resources/application-dev.yml diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..afc3723 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,8 @@ +spring: + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: update \ No newline at end of file From 44cf1980cffb28172227ea7853f51698b64eeb90 Mon Sep 17 00:00:00 2001 From: Jinhui Date: Mon, 26 Aug 2024 12:08:01 +0900 Subject: [PATCH 39/69] =?UTF-8?q?fix=20:=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20-=20@Query=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20addLikeCount()=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20JPA=20=EB=B3=80=EA=B2=BD=20=EA=B0=90?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/controller/PostLikeController.java | 16 ++++++------- .../java/wanted/media/post/domain/Post.java | 3 +++ .../post/repository/PostLikeRepository.java | 9 ------- .../media/post/service/PostLikeService.java | 24 ++++++++++++------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/main/java/wanted/media/post/controller/PostLikeController.java b/src/main/java/wanted/media/post/controller/PostLikeController.java index af10b57..e82fda5 100644 --- a/src/main/java/wanted/media/post/controller/PostLikeController.java +++ b/src/main/java/wanted/media/post/controller/PostLikeController.java @@ -8,6 +8,9 @@ import org.springframework.web.bind.annotation.RestController; import wanted.media.post.service.PostLikeService; +import java.util.HashMap; +import java.util.Map; + @RestController @RequestMapping("/api/posts") @RequiredArgsConstructor @@ -21,15 +24,12 @@ public ResponseEntity getLikes(@PathVariable(name = "postId") String postId) { return ResponseEntity.badRequest().body("잘못된 요청입니다."); } - // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) try { - String endpoint = postLikeService.makeEndpoint(postId); - // 요구사항 시나리오에 따라 필요하지만 실제 동작하지 않기에 주석 처리함 -// RestTemplate restTemplate = new RestTemplate(); -// String response = restTemplate.postForObject(endpoint, null, String.class); - // 외부 데이터와 동기 시키는게 정상적인 동작이나, 본 과제에서는 내부에서 좋아요 수를 증가 시키도록 처리함 - postLikeService.increaseLike(postId); - return ResponseEntity.ok().body("좋아요 수 증가 완료"); + String id = postLikeService.makeEndpoint(postId); + Map response = new HashMap<>(); + response.put("postId", id); + response.put("message", "좋아요 수 증가 완료"); + return ResponseEntity.ok().body(response); } catch (Exception e) { return ResponseEntity.internalServerError().body("SNS API 호출 실패: " + e.getMessage()); } diff --git a/src/main/java/wanted/media/post/domain/Post.java b/src/main/java/wanted/media/post/domain/Post.java index 6a24fdb..a59ff76 100644 --- a/src/main/java/wanted/media/post/domain/Post.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -53,4 +53,7 @@ public class Post { @NotNull private User user; + public void addLikeCount(Long count) { + this.likeCount = count + 1; + } } diff --git a/src/main/java/wanted/media/post/repository/PostLikeRepository.java b/src/main/java/wanted/media/post/repository/PostLikeRepository.java index cd71f07..aab6cdb 100644 --- a/src/main/java/wanted/media/post/repository/PostLikeRepository.java +++ b/src/main/java/wanted/media/post/repository/PostLikeRepository.java @@ -1,16 +1,7 @@ package wanted.media.post.repository; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.transaction.annotation.Transactional; import wanted.media.post.domain.Post; public interface PostLikeRepository extends JpaRepository { - - @Transactional - @Modifying(clearAutomatically = true) - @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 WHERE p.id = :postId") - void incrementLikeCount(@Param("postId") String postId); } diff --git a/src/main/java/wanted/media/post/service/PostLikeService.java b/src/main/java/wanted/media/post/service/PostLikeService.java index 81c529e..f40293f 100644 --- a/src/main/java/wanted/media/post/service/PostLikeService.java +++ b/src/main/java/wanted/media/post/service/PostLikeService.java @@ -3,7 +3,6 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import wanted.media.exception.ErrorCode; import wanted.media.post.domain.Post; @@ -12,16 +11,27 @@ @Service @RequiredArgsConstructor public class PostLikeService { - private final PostLikeRepository postLikeRepository; - @Transactional(readOnly = true) + @Transactional public String makeEndpoint(String postId) { // contentId를 통해 게시물의 SNS 유형 조회 Post post = postLikeRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException(ErrorCode.ENTITY_NOT_FOUND.getMessage())); + String snsType = post.getType().name(); + // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) + snsApi(snsType, postId); + + // 좋아요 수 증가 + Long like = post.getLikeCount(); + post.addLikeCount(like); + return postId; + } + // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) + public void snsApi(String snsType, String postId) { String endpoint = ""; + // SNS 유형에 따른 외부 엔드포인트 구성 switch (snsType.toLowerCase()) { case "facebook": @@ -39,11 +49,9 @@ public String makeEndpoint(String postId) { default: endpoint = null; } - return endpoint; - } - @Transactional(isolation = Isolation.READ_COMMITTED) - public void increaseLike(String postId) { - postLikeRepository.incrementLikeCount(postId); + // 요구사항 시나리오에 따라 필요하지만 실제 동작하지 않기에 주석 처리함 +// RestTemplate restTemplate = new RestTemplate(); +// String response = restTemplate.postForObject(endpoint, null, String.class); } } From 1f12dd4659e29e5f86f47db6b889a2d8c809f181 Mon Sep 17 00:00:00 2001 From: Jinhui Date: Mon, 26 Aug 2024 12:29:00 +0900 Subject: [PATCH 40/69] =?UTF-8?q?refactor=20:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EB=AC=BC=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=8C=8C=EC=9D=BC=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?PostLike~=20=E2=86=92=20Post~?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/post/controller/PostController.java | 30 +++++++++- .../post/controller/PostLikeController.java | 37 ------------ .../post/repository/PostLikeRepository.java | 7 --- .../media/post/service/PostLikeService.java | 57 ------------------- .../media/post/service/PostService.java | 50 ++++++++++++++++ 5 files changed, 79 insertions(+), 102 deletions(-) delete mode 100644 src/main/java/wanted/media/post/controller/PostLikeController.java delete mode 100644 src/main/java/wanted/media/post/repository/PostLikeRepository.java delete mode 100644 src/main/java/wanted/media/post/service/PostLikeService.java diff --git a/src/main/java/wanted/media/post/controller/PostController.java b/src/main/java/wanted/media/post/controller/PostController.java index 97212e6..e12ac29 100644 --- a/src/main/java/wanted/media/post/controller/PostController.java +++ b/src/main/java/wanted/media/post/controller/PostController.java @@ -1,9 +1,37 @@ package wanted.media.post.controller; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wanted.media.post.service.PostService; + +import java.util.HashMap; +import java.util.Map; @RestController -@RequestMapping("/posts") +@RequestMapping("/api/posts") +@RequiredArgsConstructor public class PostController { + + private final PostService postService; + + @PostMapping("/likes/{postId}") + public ResponseEntity getLikes(@PathVariable(name = "postId") String postId) { + if (postId == null) { + return ResponseEntity.badRequest().body("잘못된 요청입니다."); + } + + try { + String id = postService.increaseLike(postId); + Map response = new HashMap<>(); + response.put("postId", id); + response.put("message", "좋아요 수 증가 완료"); + return ResponseEntity.ok().body(response); + } catch (Exception e) { + return ResponseEntity.internalServerError().body("좋아요 수 증가 실패 : " + e.getMessage()); + } + } } diff --git a/src/main/java/wanted/media/post/controller/PostLikeController.java b/src/main/java/wanted/media/post/controller/PostLikeController.java deleted file mode 100644 index e82fda5..0000000 --- a/src/main/java/wanted/media/post/controller/PostLikeController.java +++ /dev/null @@ -1,37 +0,0 @@ -package wanted.media.post.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import wanted.media.post.service.PostLikeService; - -import java.util.HashMap; -import java.util.Map; - -@RestController -@RequestMapping("/api/posts") -@RequiredArgsConstructor -public class PostLikeController { - - private final PostLikeService postLikeService; - - @PostMapping("/likes/{postId}") - public ResponseEntity getLikes(@PathVariable(name = "postId") String postId) { - if (postId == null) { - return ResponseEntity.badRequest().body("잘못된 요청입니다."); - } - - try { - String id = postLikeService.makeEndpoint(postId); - Map response = new HashMap<>(); - response.put("postId", id); - response.put("message", "좋아요 수 증가 완료"); - return ResponseEntity.ok().body(response); - } catch (Exception e) { - return ResponseEntity.internalServerError().body("SNS API 호출 실패: " + e.getMessage()); - } - } -} diff --git a/src/main/java/wanted/media/post/repository/PostLikeRepository.java b/src/main/java/wanted/media/post/repository/PostLikeRepository.java deleted file mode 100644 index aab6cdb..0000000 --- a/src/main/java/wanted/media/post/repository/PostLikeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package wanted.media.post.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import wanted.media.post.domain.Post; - -public interface PostLikeRepository extends JpaRepository { -} diff --git a/src/main/java/wanted/media/post/service/PostLikeService.java b/src/main/java/wanted/media/post/service/PostLikeService.java deleted file mode 100644 index f40293f..0000000 --- a/src/main/java/wanted/media/post/service/PostLikeService.java +++ /dev/null @@ -1,57 +0,0 @@ -package wanted.media.post.service; - -import jakarta.persistence.EntityNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import wanted.media.exception.ErrorCode; -import wanted.media.post.domain.Post; -import wanted.media.post.repository.PostLikeRepository; - -@Service -@RequiredArgsConstructor -public class PostLikeService { - private final PostLikeRepository postLikeRepository; - - @Transactional - public String makeEndpoint(String postId) { - // contentId를 통해 게시물의 SNS 유형 조회 - Post post = postLikeRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException(ErrorCode.ENTITY_NOT_FOUND.getMessage())); - - String snsType = post.getType().name(); - // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) - snsApi(snsType, postId); - - // 좋아요 수 증가 - Long like = post.getLikeCount(); - post.addLikeCount(like); - return postId; - } - - // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) - public void snsApi(String snsType, String postId) { - String endpoint = ""; - - // SNS 유형에 따른 외부 엔드포인트 구성 - switch (snsType.toLowerCase()) { - case "facebook": - endpoint = "https://www.facebook.com/likes/" + postId; - break; - case "twitter": - endpoint = "https://www.twitter.com/likes/" + postId; - break; - case "instagram": - endpoint = "https://www.instagram.com/likes/" + postId; - break; - case "threads": - endpoint = "https://www.threads.net/likes/" + postId; - break; - default: - endpoint = null; - } - - // 요구사항 시나리오에 따라 필요하지만 실제 동작하지 않기에 주석 처리함 -// RestTemplate restTemplate = new RestTemplate(); -// String response = restTemplate.postForObject(endpoint, null, String.class); - } -} diff --git a/src/main/java/wanted/media/post/service/PostService.java b/src/main/java/wanted/media/post/service/PostService.java index cb52ee0..31aeb32 100644 --- a/src/main/java/wanted/media/post/service/PostService.java +++ b/src/main/java/wanted/media/post/service/PostService.java @@ -1,7 +1,57 @@ package wanted.media.post.service; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wanted.media.exception.ErrorCode; +import wanted.media.post.domain.Post; +import wanted.media.post.repository.PostRepository; @Service +@RequiredArgsConstructor public class PostService { + private final PostRepository postRepository; + + @Transactional + public String increaseLike(String postId) { + // contentId를 통해 게시물의 SNS 유형 조회 + Post post = postRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException(ErrorCode.ENTITY_NOT_FOUND.getMessage())); + + String snsType = post.getType().name(); + // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) + snsApi(snsType, postId); + + // 좋아요 수 증가 + Long like = post.getLikeCount(); + post.addLikeCount(like); + return postId; + } + + // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) + public void snsApi(String snsType, String postId) { + String endpoint = ""; + + // SNS 유형에 따른 외부 엔드포인트 구성 + switch (snsType.toLowerCase()) { + case "facebook": + endpoint = "https://www.facebook.com/likes/" + postId; + break; + case "twitter": + endpoint = "https://www.twitter.com/likes/" + postId; + break; + case "instagram": + endpoint = "https://www.instagram.com/likes/" + postId; + break; + case "threads": + endpoint = "https://www.threads.net/likes/" + postId; + break; + default: + endpoint = null; + } + + // 요구사항 시나리오에 따라 필요하지만 실제 동작하지 않기에 주석 처리함 +// RestTemplate restTemplate = new RestTemplate(); +// String response = restTemplate.postForObject(endpoint, null, String.class); + } } From cee297593403c54f743bf24cfbea07eb30550431 Mon Sep 17 00:00:00 2001 From: Jinhui Date: Mon, 26 Aug 2024 12:44:03 +0900 Subject: [PATCH 41/69] =?UTF-8?q?feat=20:=20PostIdResponse=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/post/controller/PostController.java | 10 ++-------- .../java/wanted/media/post/dto/PostIdResponse.java | 11 +++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 src/main/java/wanted/media/post/dto/PostIdResponse.java diff --git a/src/main/java/wanted/media/post/controller/PostController.java b/src/main/java/wanted/media/post/controller/PostController.java index e12ac29..0cc2ade 100644 --- a/src/main/java/wanted/media/post/controller/PostController.java +++ b/src/main/java/wanted/media/post/controller/PostController.java @@ -6,16 +6,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wanted.media.post.dto.PostIdResponse; import wanted.media.post.service.PostService; -import java.util.HashMap; -import java.util.Map; - @RestController @RequestMapping("/api/posts") @RequiredArgsConstructor public class PostController { - private final PostService postService; @PostMapping("/likes/{postId}") @@ -26,10 +23,7 @@ public ResponseEntity getLikes(@PathVariable(name = "postId") String postId) { try { String id = postService.increaseLike(postId); - Map response = new HashMap<>(); - response.put("postId", id); - response.put("message", "좋아요 수 증가 완료"); - return ResponseEntity.ok().body(response); + return ResponseEntity.ok().body(new PostIdResponse(id, "좋아요 수 증가 완료")); } catch (Exception e) { return ResponseEntity.internalServerError().body("좋아요 수 증가 실패 : " + e.getMessage()); } diff --git a/src/main/java/wanted/media/post/dto/PostIdResponse.java b/src/main/java/wanted/media/post/dto/PostIdResponse.java new file mode 100644 index 0000000..8c00f46 --- /dev/null +++ b/src/main/java/wanted/media/post/dto/PostIdResponse.java @@ -0,0 +1,11 @@ +package wanted.media.post.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PostIdResponse { + private final String postId; + private final String messgae; +} From ca15823376f9def4b553f07c0109b5559fdc7984 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 14:11:33 +0900 Subject: [PATCH 42/69] =?UTF-8?q?refactor=20:=20dev=20yml=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index afc3723..e5f26d3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,4 +1,8 @@ spring: + datasource: + url: ${db.dev.datasource.url} + username: ${db.dev.datasource.username} + password: ${db.dev.datasource.password} jpa: show-sql: true properties: From c7c8031b5150254aba264c2c119b4275518a246f Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 14:35:15 +0900 Subject: [PATCH 43/69] =?UTF-8?q?refactor=20:=20=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/controller/UserController.java | 1 - src/main/java/wanted/media/user/service/UserService.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 6e82349..d48ce0d 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -16,7 +16,6 @@ @RequestMapping("/api/user") @RequiredArgsConstructor public class UserController { - private final UserService userService; @PostMapping("/sign-up") diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 10dd14b..c32e5a0 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -24,7 +24,6 @@ public class UserService { private final UserValidator userValidator; private final GenerateCode generateCode; - //회원가입 public SignUpResponse signUp(SignUpRequest request) { // 1. 사용자 입력내용 검증 From fb58e7389c9ad67fd53b426bf9a60acd99b34f1c Mon Sep 17 00:00:00 2001 From: Jinhui Date: Mon, 26 Aug 2024 14:39:53 +0900 Subject: [PATCH 44/69] =?UTF-8?q?refactor=20:=20addLikeCount()=EC=9D=98=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=84=EA=B2=B0=ED=95=98=EA=B2=8C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/post/domain/Post.java | 4 ++-- src/main/java/wanted/media/post/service/PostService.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/wanted/media/post/domain/Post.java b/src/main/java/wanted/media/post/domain/Post.java index a59ff76..d07fc7c 100644 --- a/src/main/java/wanted/media/post/domain/Post.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -53,7 +53,7 @@ public class Post { @NotNull private User user; - public void addLikeCount(Long count) { - this.likeCount = count + 1; + public void addLikeCount() { + this.likeCount++; } } diff --git a/src/main/java/wanted/media/post/service/PostService.java b/src/main/java/wanted/media/post/service/PostService.java index 31aeb32..eef9606 100644 --- a/src/main/java/wanted/media/post/service/PostService.java +++ b/src/main/java/wanted/media/post/service/PostService.java @@ -23,8 +23,7 @@ public String increaseLike(String postId) { snsApi(snsType, postId); // 좋아요 수 증가 - Long like = post.getLikeCount(); - post.addLikeCount(like); + post.addLikeCount(); return postId; } From b93636e7512a75b04bb59ff2882222729272b593 Mon Sep 17 00:00:00 2001 From: Jinhui Date: Mon, 26 Aug 2024 15:30:37 +0900 Subject: [PATCH 45/69] no message --- .../media/exception/CustomException.java | 15 +++++++++++++++ .../wanted/media/exception/ErrorCode.java | 11 ++++++----- .../handler/GlobalExceptionHandler.java | 19 +++++++++++++------ .../media/post/service/PostService.java | 8 ++++++-- 4 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 src/main/java/wanted/media/exception/CustomException.java diff --git a/src/main/java/wanted/media/exception/CustomException.java b/src/main/java/wanted/media/exception/CustomException.java new file mode 100644 index 0000000..bcd7af3 --- /dev/null +++ b/src/main/java/wanted/media/exception/CustomException.java @@ -0,0 +1,15 @@ +package wanted.media.exception; + +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException { + private final ErrorCode errorCode; + private String customMessage; + + public CustomException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.customMessage = errorCode.getMessage(); + } +} diff --git a/src/main/java/wanted/media/exception/ErrorCode.java b/src/main/java/wanted/media/exception/ErrorCode.java index dade125..adaec97 100644 --- a/src/main/java/wanted/media/exception/ErrorCode.java +++ b/src/main/java/wanted/media/exception/ErrorCode.java @@ -1,15 +1,16 @@ package wanted.media.exception; -import org.springframework.http.HttpStatus; - import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor public enum ErrorCode { - ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."); + ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."), + // 클라이언트의 입력 값에 대한 일반적인 오류 (@PathVariable, @RequestParam가 잘못되었을 때) + INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "클라이언트의 입력 값을 확인해주세요."); - private final HttpStatus status; - private final String message; + private final HttpStatus status; + private final String message; } diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index c96a472..db9f567 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -4,15 +4,22 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; - +import wanted.media.exception.CustomException; import wanted.media.exception.ErrorResponse; @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(400, e.getMessage())); - } + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException e) { + return ResponseEntity.badRequest() + .body(new ErrorResponse(400, e.getMessage())); + } + + @ExceptionHandler(CustomException.class) + protected ResponseEntity handleCustomException(final CustomException e) { + return ResponseEntity + .status(e.getErrorCode().getStatus().value()) + .body(new ErrorResponse(e.getErrorCode().getStatus().value(), e.getCustomMessage())); + } } diff --git a/src/main/java/wanted/media/post/service/PostService.java b/src/main/java/wanted/media/post/service/PostService.java index eef9606..99f796b 100644 --- a/src/main/java/wanted/media/post/service/PostService.java +++ b/src/main/java/wanted/media/post/service/PostService.java @@ -1,9 +1,9 @@ package wanted.media.post.service; -import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import wanted.media.exception.CustomException; import wanted.media.exception.ErrorCode; import wanted.media.post.domain.Post; import wanted.media.post.repository.PostRepository; @@ -15,8 +15,12 @@ public class PostService { @Transactional public String increaseLike(String postId) { + if (postId == null) { + new CustomException(ErrorCode.INVALID_INPUT_VALUE); + } + // contentId를 통해 게시물의 SNS 유형 조회 - Post post = postRepository.findById(postId).orElseThrow(() -> new EntityNotFoundException(ErrorCode.ENTITY_NOT_FOUND.getMessage())); + Post post = postRepository.findById(postId).orElseThrow(() -> new CustomException(ErrorCode.ENTITY_NOT_FOUND)); String snsType = post.getType().name(); // 외부 SNS API 호출 부분 (기능 개발을 위한 요소로, 실제 동작하지 않음) From f8863e61feebd2237cb83aab6ab9a476f149daa8 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 15:35:09 +0900 Subject: [PATCH 46/69] =?UTF-8?q?refactor=20:=20SignUpRequset,=20SignUpRes?= =?UTF-8?q?ponse,=20UserCreateDto=20record=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/dto/SignUpRequest.java | 20 +++++-------------- .../wanted/media/user/dto/SignUpResponse.java | 11 +--------- .../wanted/media/user/dto/UserCreateDto.java | 11 +--------- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/src/main/java/wanted/media/user/dto/SignUpRequest.java b/src/main/java/wanted/media/user/dto/SignUpRequest.java index 3b2cb4f..907fea4 100644 --- a/src/main/java/wanted/media/user/dto/SignUpRequest.java +++ b/src/main/java/wanted/media/user/dto/SignUpRequest.java @@ -3,20 +3,10 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lombok.Data; -@Data -public class SignUpRequest { - @NotBlank - @Size(max = 50) - private String account; - - @NotBlank - @Email - @Size(max = 50) - private String email; - - @NotBlank - @Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") - private String password; +public record SignUpRequest( + @NotBlank @Size(max = 50) String account, + @NotBlank @Email @Size(max = 50) String email, + @NotBlank @Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") String password +) { } diff --git a/src/main/java/wanted/media/user/dto/SignUpResponse.java b/src/main/java/wanted/media/user/dto/SignUpResponse.java index b16ae2f..834709b 100644 --- a/src/main/java/wanted/media/user/dto/SignUpResponse.java +++ b/src/main/java/wanted/media/user/dto/SignUpResponse.java @@ -1,14 +1,5 @@ package wanted.media.user.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +public record SignUpResponse(String message, UserCreateDto dto, String authCode) { -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class SignUpResponse { - private String message; - private UserCreateDto userCreateDto; // 사용자 정보 DTO - private String authCode; // 사용자 인증코드 } diff --git a/src/main/java/wanted/media/user/dto/UserCreateDto.java b/src/main/java/wanted/media/user/dto/UserCreateDto.java index fc7e4b5..e7ac9b6 100644 --- a/src/main/java/wanted/media/user/dto/UserCreateDto.java +++ b/src/main/java/wanted/media/user/dto/UserCreateDto.java @@ -1,13 +1,4 @@ package wanted.media.user.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class UserCreateDto { - private String account; - private String email; +public record UserCreateDto(String account, String email) { } From db0a386ff19a84773501d412d4b33c28c6bf49e9 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 15:35:41 +0900 Subject: [PATCH 47/69] =?UTF-8?q?refactor=20:=20access=20level=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/domain/Code.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/wanted/media/user/domain/Code.java b/src/main/java/wanted/media/user/domain/Code.java index 742920d..93240bc 100644 --- a/src/main/java/wanted/media/user/domain/Code.java +++ b/src/main/java/wanted/media/user/domain/Code.java @@ -2,15 +2,12 @@ import jakarta.persistence.*; import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.annotation.CreatedDate; import java.time.LocalDateTime; -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Getter @Builder From 7b88754a6a691c2e35e0f4285aaef23bb8d23252 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 15:36:36 +0900 Subject: [PATCH 48/69] =?UTF-8?q?refactor=20:=20=ED=86=B5=EC=83=81?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=9E=90=EC=A3=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=82=AC=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/service/UserValidator.java | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/main/java/wanted/media/user/service/UserValidator.java b/src/main/java/wanted/media/user/service/UserValidator.java index 69f2cca..0d9dcf9 100644 --- a/src/main/java/wanted/media/user/service/UserValidator.java +++ b/src/main/java/wanted/media/user/service/UserValidator.java @@ -1,31 +1,29 @@ package wanted.media.user.service; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import wanted.media.user.dto.SignUpRequest; import wanted.media.user.repository.UserRepository; @Component +@RequiredArgsConstructor public class UserValidator { private final UserRepository userRepository; - - public UserValidator(UserRepository userRepository) { - this.userRepository = userRepository; - } - + /* * 회원가입 시 사용자 정보 중복 확인 * */ public void validateRequest(SignUpRequest signUpRequest) { //account 중복 확인 - if (userRepository.findByAccount(signUpRequest.getAccount()).isPresent()) { + if (userRepository.findByAccount(signUpRequest.account()).isPresent()) { throw new IllegalArgumentException("이미 존재하는 계정입니다."); } //email 중복 확인 - if (userRepository.findByEmail(signUpRequest.getEmail()).isPresent()) { + if (userRepository.findByEmail(signUpRequest.email()).isPresent()) { throw new IllegalArgumentException("이미 존재하는 이메일입니다."); } //비밀번호 유효성 검사 - validatePassword(signUpRequest.getPassword(), signUpRequest.getAccount(), signUpRequest.getEmail()); + validatePassword(signUpRequest.password(), signUpRequest.account(), signUpRequest.email()); } //비밀번호 유효성 검사 @@ -42,9 +40,13 @@ private void validatePassword(String password, String account, String email) { if (!password.matches(".*[a-zA-Z].*") || !password.matches(".*[0-9!@#$%^&*].*")) { throw new IllegalArgumentException("비밀번호는 최소 숫자, 문자, 특수문자 중 최소 2가지를 포함해야 합니다."); } - // 4. 통상적으로 자주 사용되는 비밀번호인 경우 - if (containsCommonPasswords(password)) { - throw new IllegalArgumentException("통상적으로 자주 사용되는 비밀번호는 사용할 수 없습니다."); + // 4. 비밀번호가 통상적으로 자주 사용되는 경우 + String[] commonPasswords = {"123456", "password", "123456789", "qwerty", "abc123"}; + String lowerCasePassword = password.toLowerCase(); // 비밀번호를 소문자로 변환 후 비교 + for (String commonPassword : commonPasswords) { + if (lowerCasePassword.equals(commonPassword)) { + throw new IllegalArgumentException("통상적으로 자주 사용되는 비밀번호는 사용할 수 없습니다."); + } } // 5. 연속된 문자열을 사용한 경우 if (hasSequentialCharacters(password)) { @@ -56,23 +58,11 @@ private void validatePassword(String password, String account, String email) { } } - // 통상적으로 자주 사용되는 비밀번호 검사 - private boolean containsCommonPasswords(String password) { - String[] commonPasswords = {"123456", "password", "123456789", "qwerty", "abc123"}; - for (String commonPassword : commonPasswords) { - if (password.equalsIgnoreCase(commonPassword)) { - return true; - } - } - return false; - } - // 3회 이상 연속된 문자열을 사용한 비밀번호 검사 private boolean hasSequentialCharacters(String password) { if (password == null || password.length() < 3) { return false; } - int seqCount = 1; for (int i = 1; i < password.length(); i++) { // 현재 문자와 이전 문자의 차이를 비교 @@ -88,7 +78,6 @@ private boolean hasSequentialCharacters(String password) { return false; } - // 사용자 개인정보를 포함한 경우 비밀번호 검사 private boolean isSimilarToPersonalInfo(String password, String account, String email) { String emailLocalPart = email.split("@")[0]; // email 사용자명 부분 From c107f787846dc18db076b4adea6aef2000e31bf0 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 15:38:32 +0900 Subject: [PATCH 49/69] =?UTF-8?q?feat=20:=20=EC=9D=B8=EC=A6=9D=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=9C=EA=B8=89=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20?= =?UTF-8?q?uuid=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/service/GenerateCode.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/java/wanted/media/user/service/GenerateCode.java b/src/main/java/wanted/media/user/service/GenerateCode.java index c12c858..f7ec600 100644 --- a/src/main/java/wanted/media/user/service/GenerateCode.java +++ b/src/main/java/wanted/media/user/service/GenerateCode.java @@ -2,23 +2,16 @@ import org.springframework.stereotype.Component; -import java.util.Random; +import java.util.UUID; @Component public class GenerateCode { private int codeLength = 6; //6자리 코드 - private final char[] characterTable = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', - 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}; public String codeGenerate() { - Random random = new Random(System.currentTimeMillis()); - int tableLength = characterTable.length; - StringBuilder code = new StringBuilder(); - - for (int i = 0; i < codeLength; i++) { - code.append(characterTable[random.nextInt(tableLength)]); - } - return code.toString(); + // 1. UUID 생성 후, "-" 제거, 대문자 변환 + String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase(); + // 2. UUID에서 필요한 길이만큼 자르기 + return uuid.substring(0, codeLength); } } From 18a237c9eac08441dfba710716fa790c16afb65a Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 15:39:26 +0900 Subject: [PATCH 50/69] =?UTF-8?q?refactor=20:=20record=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20api=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/controller/UserController.java | 1 + .../wanted/media/user/service/UserService.java | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index d48ce0d..ac317cb 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -18,6 +18,7 @@ public class UserController { private final UserService userService; + //회원가입 @PostMapping("/sign-up") public ResponseEntity signUp(@Validated @RequestBody SignUpRequest request) { SignUpResponse response = userService.signUp(request); diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index c32e5a0..7b11d32 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -1,5 +1,6 @@ package wanted.media.user.service; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -16,8 +17,8 @@ @Service @RequiredArgsConstructor +@Transactional public class UserService { - private final UserRepository userRepository; private final CodeRepository codeRepository; private final BCryptPasswordEncoder passwordEncoder; @@ -29,13 +30,13 @@ public SignUpResponse signUp(SignUpRequest request) { // 1. 사용자 입력내용 검증 userValidator.validateRequest(request); // 2. 비밀번호 암호화 - String encodedPassword = passwordEncoder.encode(request.getPassword()); + String encodedPassword = passwordEncoder.encode(request.password()); // 3. 인증코드 생성 String verificationCode = generateCode.codeGenerate(); // 4. User 객체 생성 User user = User.builder() - .account(request.getAccount()) - .email(request.getEmail()) + .account(request.account()) + .email(request.email()) .password(encodedPassword) .grade(Grade.NORMAL_USER) .build(); @@ -51,10 +52,7 @@ public SignUpResponse signUp(SignUpRequest request) { codeRepository.save(code); // 8. UserCreateDto 생성 UserCreateDto userCreateDto = new UserCreateDto(user.getAccount(), user.getEmail()); - // 9. SignUpResponse 생성 - SignUpResponse signUpResponse = new SignUpResponse("회원가입이 성공적으로 완료됐습니다.", userCreateDto, verificationCode); - - return signUpResponse; + // 9. SignUpResponse 생성 및 반환 + return new SignUpResponse("회원가입이 완료되었습니다.", userCreateDto, verificationCode); } - } From a5649ef4a4c511e0a94788c1dcf5c16c9cc00aa5 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 15:41:35 +0900 Subject: [PATCH 51/69] =?UTF-8?q?refactor=20:=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/service/UserValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/user/service/UserValidator.java b/src/main/java/wanted/media/user/service/UserValidator.java index 0d9dcf9..c000c06 100644 --- a/src/main/java/wanted/media/user/service/UserValidator.java +++ b/src/main/java/wanted/media/user/service/UserValidator.java @@ -9,7 +9,7 @@ @RequiredArgsConstructor public class UserValidator { private final UserRepository userRepository; - + /* * 회원가입 시 사용자 정보 중복 확인 * */ From be4f76767fc1d7d526ccc62be60bad8c1a23a7e4 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 15:43:42 +0900 Subject: [PATCH 52/69] =?UTF-8?q?refactor=20:=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/dto/SignUpResponse.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/wanted/media/user/dto/SignUpResponse.java b/src/main/java/wanted/media/user/dto/SignUpResponse.java index 834709b..14be31e 100644 --- a/src/main/java/wanted/media/user/dto/SignUpResponse.java +++ b/src/main/java/wanted/media/user/dto/SignUpResponse.java @@ -1,5 +1,4 @@ package wanted.media.user.dto; public record SignUpResponse(String message, UserCreateDto dto, String authCode) { - } From cbdc2bce3b7d071fa1d98ca4b500961c3500cfb9 Mon Sep 17 00:00:00 2001 From: Jinhui Date: Mon, 26 Aug 2024 16:05:01 +0900 Subject: [PATCH 53/69] =?UTF-8?q?refactor=20:=20PostIdResponse=EB=A5=BC=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=98=ED=99=98=EC=97=90=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=9D=91=EB=8B=B5=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/post/controller/PostController.java | 16 +++++++++------- .../wanted/media/post/dto/PostIdResponse.java | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/wanted/media/post/controller/PostController.java b/src/main/java/wanted/media/post/controller/PostController.java index 0cc2ade..3074064 100644 --- a/src/main/java/wanted/media/post/controller/PostController.java +++ b/src/main/java/wanted/media/post/controller/PostController.java @@ -9,6 +9,9 @@ import wanted.media.post.dto.PostIdResponse; import wanted.media.post.service.PostService; +import java.util.HashMap; +import java.util.Map; + @RestController @RequestMapping("/api/posts") @RequiredArgsConstructor @@ -16,16 +19,15 @@ public class PostController { private final PostService postService; @PostMapping("/likes/{postId}") - public ResponseEntity getLikes(@PathVariable(name = "postId") String postId) { - if (postId == null) { - return ResponseEntity.badRequest().body("잘못된 요청입니다."); - } - + public ResponseEntity getLikes(@PathVariable(name = "postId") String postId) { try { String id = postService.increaseLike(postId); - return ResponseEntity.ok().body(new PostIdResponse(id, "좋아요 수 증가 완료")); + return ResponseEntity.ok().body(new PostIdResponse<>(id, "좋아요 수 증가 완료")); } catch (Exception e) { - return ResponseEntity.internalServerError().body("좋아요 수 증가 실패 : " + e.getMessage()); + Map errorMessage = new HashMap<>(); + errorMessage.put("좋아요 수 증가 실패", e.getMessage()); + return ResponseEntity.internalServerError().body(new PostIdResponse<>(postId, errorMessage)); } } + } diff --git a/src/main/java/wanted/media/post/dto/PostIdResponse.java b/src/main/java/wanted/media/post/dto/PostIdResponse.java index 8c00f46..b2a57b6 100644 --- a/src/main/java/wanted/media/post/dto/PostIdResponse.java +++ b/src/main/java/wanted/media/post/dto/PostIdResponse.java @@ -5,7 +5,7 @@ @Getter @AllArgsConstructor -public class PostIdResponse { +public class PostIdResponse { private final String postId; - private final String messgae; + private final T messgae; } From 9e1b67c0befaa58100251cf00a6140bd50bae2c1 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 16:09:43 +0900 Subject: [PATCH 54/69] =?UTF-8?q?refactor=20:=20=ED=86=B5=EC=83=81?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/service/UserValidator.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/wanted/media/user/service/UserValidator.java b/src/main/java/wanted/media/user/service/UserValidator.java index c000c06..4a65067 100644 --- a/src/main/java/wanted/media/user/service/UserValidator.java +++ b/src/main/java/wanted/media/user/service/UserValidator.java @@ -40,19 +40,11 @@ private void validatePassword(String password, String account, String email) { if (!password.matches(".*[a-zA-Z].*") || !password.matches(".*[0-9!@#$%^&*].*")) { throw new IllegalArgumentException("비밀번호는 최소 숫자, 문자, 특수문자 중 최소 2가지를 포함해야 합니다."); } - // 4. 비밀번호가 통상적으로 자주 사용되는 경우 - String[] commonPasswords = {"123456", "password", "123456789", "qwerty", "abc123"}; - String lowerCasePassword = password.toLowerCase(); // 비밀번호를 소문자로 변환 후 비교 - for (String commonPassword : commonPasswords) { - if (lowerCasePassword.equals(commonPassword)) { - throw new IllegalArgumentException("통상적으로 자주 사용되는 비밀번호는 사용할 수 없습니다."); - } - } - // 5. 연속된 문자열을 사용한 경우 + // 4. 연속된 문자열을 사용한 경우 if (hasSequentialCharacters(password)) { throw new IllegalArgumentException("3회 이상 연속되는 문자 사용은 불가능합니다."); } - // 6. 사용자 개인정보를 포함한 경우 + // 5. 사용자 개인정보를 포함한 경우 if (isSimilarToPersonalInfo(password, account, email)) { throw new IllegalArgumentException("사용자 개인정보가 포함된 비밀번호입니다."); } From 7f57a62d6caa932c27bef7118e8b42920c309c60 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 16:54:15 +0900 Subject: [PATCH 55/69] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EC=8B=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=95=94=ED=98=B8=ED=99=94=20=EC=A0=81=EC=9A=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/SecurityConfig.java | 6 ++++++ src/main/java/wanted/media/user/service/UserService.java | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 0957987..ee7440c 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -8,6 +8,7 @@ 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -17,6 +18,11 @@ public class SecurityConfig { private final TokenProvider tokenProvider; + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index c391357..e4747c9 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -1,6 +1,7 @@ package wanted.media.user.service; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import wanted.media.user.config.TokenProvider; @@ -17,6 +18,7 @@ @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final BCryptPasswordEncoder passwordEncoder; private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; @@ -24,7 +26,7 @@ public class UserService { public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); - if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 + if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); Optional refreshToken = tokenRepository.findByUser_UserId(user.getUserId()); // 리프레시 토큰 있는지 확인 From 5e163496d833b406a0f52589fb54e519efc6b18b Mon Sep 17 00:00:00 2001 From: ssunnykku <108388578+ssunnykku@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:00:28 +0900 Subject: [PATCH 56/69] =?UTF-8?q?[feat]=20post=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20api=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: post 상세보기 api * refactor: 테스트용 security 설정 * refactor: review 반영 - 공백 제거 - dto record로 변경 - DetailResponse 반환 코드 PostService -> PostController 수정 작성 - .gitignore 수정 - PostNotFoundException -> NotFoundException * refactor: repository test code 삭제 * refactor: Entity, DTO Colum post -> content 변경 * fix: conflict 해결 --- .gitignore | 2 + .../media/exception/NotFoundException.java | 10 +++ .../handler/GlobalExceptionHandler.java | 18 +++++- .../media/post/controller/PostController.java | 37 ++++++++++- .../java/wanted/media/post/domain/Post.java | 38 ++++++----- .../media/post/dto/PostDetailResponse.java | 25 ++++++++ .../media/post/repository/PostRepository.java | 7 +++ .../media/post/service/PostService.java | 17 +++++ .../media/user/config/SecurityConfig.java | 2 - .../java/wanted/media/user/domain/Code.java | 1 - .../java/wanted/media/user/domain/Token.java | 1 - src/main/resources/application-dev.yml | 21 +++++++ .../media/post/service/PostServiceTest.java | 63 +++++++++++++++++++ 13 files changed, 214 insertions(+), 28 deletions(-) create mode 100644 src/main/java/wanted/media/exception/NotFoundException.java create mode 100644 src/main/java/wanted/media/post/dto/PostDetailResponse.java create mode 100644 src/main/java/wanted/media/post/repository/PostRepository.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/test/java/wanted/media/post/service/PostServiceTest.java diff --git a/.gitignore b/.gitignore index 02485a7..d9327e5 100644 --- a/.gitignore +++ b/.gitignore @@ -267,4 +267,6 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/macos,intellij,windows,java,gradle src/main/resources/application-secret.yml +src/main/resources/application-test.yml src/main/generated + diff --git a/src/main/java/wanted/media/exception/NotFoundException.java b/src/main/java/wanted/media/exception/NotFoundException.java new file mode 100644 index 0000000..1d1fd12 --- /dev/null +++ b/src/main/java/wanted/media/exception/NotFoundException.java @@ -0,0 +1,10 @@ +package wanted.media.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class NotFoundException extends RuntimeException { + private final ErrorCode errorCode; +} \ No newline at end of file diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index 9bc4019..f341fdd 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -1,11 +1,13 @@ package wanted.media.exception.handler; +import org.apache.coyote.BadRequestException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; - -import wanted.media.exception.BadRequestException; +import wanted.media.exception.ErrorCode; import wanted.media.exception.ErrorResponse; +import wanted.media.exception.NotFoundException; @RestControllerAdvice public class GlobalExceptionHandler { @@ -13,6 +15,16 @@ public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(BadRequestException e) { return ResponseEntity.badRequest() - .body(new ErrorResponse(400, e.getErrorCode().getMessage())); + .body(new ErrorResponse(400, e.getMessage())); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handlePostNotFound(NotFoundException e) { + ErrorCode errorCode = e.getErrorCode(); + ErrorResponse errorResponse = new ErrorResponse( + errorCode.getStatus().value(), + errorCode.getMessage() + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } } diff --git a/src/main/java/wanted/media/post/controller/PostController.java b/src/main/java/wanted/media/post/controller/PostController.java index 97212e6..66913de 100644 --- a/src/main/java/wanted/media/post/controller/PostController.java +++ b/src/main/java/wanted/media/post/controller/PostController.java @@ -1,9 +1,44 @@ package wanted.media.post.controller; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wanted.media.post.domain.Post; +import wanted.media.post.dto.PostDetailResponse; +import wanted.media.post.service.PostService; @RestController -@RequestMapping("/posts") +@RequestMapping("/api/posts") +@RequiredArgsConstructor public class PostController { + + private final PostService posetService; + + /** + * @param postId + * @return PostDetailResponse + */ + @GetMapping("/{postId}") + public ResponseEntity getPost(@PathVariable String postId) { + Post post = posetService.getPost(postId); + PostDetailResponse result = PostDetailResponse.builder() + .postId(post.getId()) + .likeCount(post.getLikeCount()) + .type(post.getType()) + .title(post.getTitle()) + .content(post.getContent()) + .hashtags(post.getHashtags()) + .viewCount(post.getViewCount()) + .shareCount(post.getShareCount()) + .updatedAt(post.getUpdatedAt()) + .createdAt(post.getCreatedAt()) + .userId(post.getUser().getUserId()) + .account(post.getUser().getAccount()) + .email(post.getUser().getEmail()) + .build(); + return ResponseEntity.ok(result); + } } diff --git a/src/main/java/wanted/media/post/domain/Post.java b/src/main/java/wanted/media/post/domain/Post.java index 0e1d996..617544b 100644 --- a/src/main/java/wanted/media/post/domain/Post.java +++ b/src/main/java/wanted/media/post/domain/Post.java @@ -1,32 +1,22 @@ package wanted.media.post.domain; -import java.time.LocalDateTime; - +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; +import lombok.*; import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import jakarta.validation.constraints.Size; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; import wanted.media.user.domain.User; +import java.time.LocalDateTime; + @Entity @Getter @Table(name = "posts") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder @EntityListeners(AuditingEntityListener.class) public class Post { @Id @@ -40,6 +30,7 @@ public class Post { @Size(max = 150) @Column(nullable = false) private String title; + private String content; private String hashtags; @@ -52,13 +43,20 @@ public class Post { @ColumnDefault("0") private Long shareCount; - @CreatedDate - private LocalDateTime createdAt; - @LastModifiedDate private LocalDateTime updatedAt; + @CreatedDate + private LocalDateTime createdAt; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; + + public void incrementViewCount() { + if (this.viewCount == null) { + this.viewCount = 0L; + } + this.viewCount += 1; + } } diff --git a/src/main/java/wanted/media/post/dto/PostDetailResponse.java b/src/main/java/wanted/media/post/dto/PostDetailResponse.java new file mode 100644 index 0000000..69b9217 --- /dev/null +++ b/src/main/java/wanted/media/post/dto/PostDetailResponse.java @@ -0,0 +1,25 @@ +package wanted.media.post.dto; + +import lombok.Builder; +import wanted.media.post.domain.Type; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Builder +public record PostDetailResponse( + String postId, + Type type, + String title, + String content, + String hashtags, + Long likeCount, + Long viewCount, + Long shareCount, + LocalDateTime updatedAt, + LocalDateTime createdAt, + UUID userId, + String account, + String email +) { +} diff --git a/src/main/java/wanted/media/post/repository/PostRepository.java b/src/main/java/wanted/media/post/repository/PostRepository.java new file mode 100644 index 0000000..fb74f17 --- /dev/null +++ b/src/main/java/wanted/media/post/repository/PostRepository.java @@ -0,0 +1,7 @@ +package wanted.media.post.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wanted.media.post.domain.Post; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/wanted/media/post/service/PostService.java b/src/main/java/wanted/media/post/service/PostService.java index cb52ee0..aa1ecb5 100644 --- a/src/main/java/wanted/media/post/service/PostService.java +++ b/src/main/java/wanted/media/post/service/PostService.java @@ -1,7 +1,24 @@ package wanted.media.post.service; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wanted.media.exception.ErrorCode; +import wanted.media.exception.NotFoundException; +import wanted.media.post.domain.Post; +import wanted.media.post.repository.PostRepository; @Service +@RequiredArgsConstructor public class PostService { + private final PostRepository postRepository; + + @Transactional + public Post getPost(String postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.ENTITY_NOT_FOUND)); + + post.incrementViewCount(); + return post; + } } diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index afbbcf7..320ed5b 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -2,10 +2,8 @@ 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.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; @EnableWebSecurity @Configuration diff --git a/src/main/java/wanted/media/user/domain/Code.java b/src/main/java/wanted/media/user/domain/Code.java index 93240bc..c6167c8 100644 --- a/src/main/java/wanted/media/user/domain/Code.java +++ b/src/main/java/wanted/media/user/domain/Code.java @@ -14,7 +14,6 @@ @Entity @Table(name = "codes") public class Code { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false) diff --git a/src/main/java/wanted/media/user/domain/Token.java b/src/main/java/wanted/media/user/domain/Token.java index 81fc2f5..e14e5ed 100644 --- a/src/main/java/wanted/media/user/domain/Token.java +++ b/src/main/java/wanted/media/user/domain/Token.java @@ -11,7 +11,6 @@ @Entity @Table(name = "tokens") public class Token { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..9dc08f0 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,21 @@ +# application-test +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:db;MODE=MYSQL + username: sa + password: + h2: + console: + enabled: true + path: /h2-console + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: update + +jwt: + secret_key: key diff --git a/src/test/java/wanted/media/post/service/PostServiceTest.java b/src/test/java/wanted/media/post/service/PostServiceTest.java new file mode 100644 index 0000000..ea8295a --- /dev/null +++ b/src/test/java/wanted/media/post/service/PostServiceTest.java @@ -0,0 +1,63 @@ +package wanted.media.post.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import wanted.media.post.domain.Post; +import wanted.media.post.domain.Type; +import wanted.media.post.repository.PostRepository; +import wanted.media.user.domain.Grade; +import wanted.media.user.domain.User; +import wanted.media.user.repository.UserRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +class PostServiceTest { + @Autowired + private PostService postService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PostRepository postRepository; + + @Test + @Transactional + void getPostTest() { + // given + User user = User.builder() + .account("sun") + .email("sun@gmail.com") + .password("1234") + .grade(Grade.NORMAL_USER) + .build(); + + userRepository.save(user); + + Post post = Post.builder() + .id("qwer") + .type(Type.TWITTER) + .title("제목 입력") + .content("내용 입력") + .user(user) + .viewCount(100L) + .build(); + + postRepository.save(post); + + // when + Post getData = postService.getPost(post.getId()); + + // then + assertThat(getData.getTitle()).isEqualTo("제목 입력"); + assertThat(getData.getContent()).isEqualTo("내용 입력"); + assertThat(getData.getViewCount()).isEqualTo(101); + assertThat(getData.getUser().getAccount()).isEqualTo("sun"); + assertThat(getData.getUser().getEmail()).isEqualTo("sun@gmail.com"); + } +} \ No newline at end of file From a47f35bc6cdb5ee5064372b6d9c08c145037b0ef Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 18:35:41 +0900 Subject: [PATCH 57/69] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20Exc?= =?UTF-8?q?eption=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/exception/BaseException.java | 10 ++++++++++ .../wanted/media/exception/ErrorCode.java | 14 ++++++++----- .../exception/InvalidPasswordException.java | 8 ++++++++ .../exception/UserNotFoundException.java | 7 +++++++ .../VerificationCodeExpiredException.java | 7 +++++++ .../VerificationCodeMismatchException.java | 7 +++++++ .../handler/GlobalExceptionHandler.java | 20 +++++++++++++------ 7 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 src/main/java/wanted/media/exception/BaseException.java create mode 100644 src/main/java/wanted/media/exception/InvalidPasswordException.java create mode 100644 src/main/java/wanted/media/exception/UserNotFoundException.java create mode 100644 src/main/java/wanted/media/exception/VerificationCodeExpiredException.java create mode 100644 src/main/java/wanted/media/exception/VerificationCodeMismatchException.java diff --git a/src/main/java/wanted/media/exception/BaseException.java b/src/main/java/wanted/media/exception/BaseException.java new file mode 100644 index 0000000..7b14527 --- /dev/null +++ b/src/main/java/wanted/media/exception/BaseException.java @@ -0,0 +1,10 @@ +package wanted.media.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class BaseException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/src/main/java/wanted/media/exception/ErrorCode.java b/src/main/java/wanted/media/exception/ErrorCode.java index dade125..7e58fa1 100644 --- a/src/main/java/wanted/media/exception/ErrorCode.java +++ b/src/main/java/wanted/media/exception/ErrorCode.java @@ -1,15 +1,19 @@ package wanted.media.exception; -import org.springframework.http.HttpStatus; - import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor public enum ErrorCode { - ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."); + ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."), + + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), + VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "인증코드가 일치하지 않습니다."), + VERIFICATION_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "만료된 인증코드입니다."); - private final HttpStatus status; - private final String message; + private final HttpStatus status; + private final String message; } diff --git a/src/main/java/wanted/media/exception/InvalidPasswordException.java b/src/main/java/wanted/media/exception/InvalidPasswordException.java new file mode 100644 index 0000000..3ff8264 --- /dev/null +++ b/src/main/java/wanted/media/exception/InvalidPasswordException.java @@ -0,0 +1,8 @@ +package wanted.media.exception; + +public class InvalidPasswordException extends BaseException { + public InvalidPasswordException() { + + super(ErrorCode.INVALID_PASSWORD); + } +} diff --git a/src/main/java/wanted/media/exception/UserNotFoundException.java b/src/main/java/wanted/media/exception/UserNotFoundException.java new file mode 100644 index 0000000..5e8535d --- /dev/null +++ b/src/main/java/wanted/media/exception/UserNotFoundException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class UserNotFoundException extends BaseException { + public UserNotFoundException() { + super(ErrorCode.USER_NOT_FOUND); + } +} diff --git a/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java b/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java new file mode 100644 index 0000000..6fd18c3 --- /dev/null +++ b/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class VerificationCodeExpiredException extends BaseException { + public VerificationCodeExpiredException() { + super(ErrorCode.VERIFICATION_CODE_EXPIRED); + } +} diff --git a/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java b/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java new file mode 100644 index 0000000..d5f538d --- /dev/null +++ b/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class VerificationCodeMismatchException extends BaseException { + public VerificationCodeMismatchException() { + super(ErrorCode.VERIFICATION_CODE_MISMATCH); + } +} diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index c96a472..5ab1f6b 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -4,15 +4,23 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; - +import wanted.media.exception.BaseException; +import wanted.media.exception.ErrorCode; import wanted.media.exception.ErrorResponse; @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(400, e.getMessage())); - } + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException e) { + return ResponseEntity.badRequest() + .body(new ErrorResponse(400, e.getMessage())); + } + + @ExceptionHandler(BaseException.class) + public ResponseEntity handleBaseException(BaseException e) { + ErrorCode errorCode = e.getErrorCode(); + return ResponseEntity.status(errorCode.getStatus()) + .body(new ErrorResponse(errorCode.getStatus().value(), errorCode.getMessage())); + } } From 45b8c68f686141bfafc722d65760444a25636fae Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 18:37:03 +0900 Subject: [PATCH 58/69] =?UTF-8?q?refactor=20:=20VerifyRequest,=20VerifyRes?= =?UTF-8?q?ponse,=20UserInfoDto=20radio=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/dto/UserInfoDto.java | 13 ++---------- .../wanted/media/user/dto/VerifyRequest.java | 21 ++++++------------- .../wanted/media/user/dto/VerifyResponse.java | 13 ++---------- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/src/main/java/wanted/media/user/dto/UserInfoDto.java b/src/main/java/wanted/media/user/dto/UserInfoDto.java index 6557be3..c2899a4 100644 --- a/src/main/java/wanted/media/user/dto/UserInfoDto.java +++ b/src/main/java/wanted/media/user/dto/UserInfoDto.java @@ -1,15 +1,6 @@ package wanted.media.user.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; import wanted.media.user.domain.Grade; -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class UserInfoDto { - private String account; - private String email; - private Grade grade; // 현재 회원등급 -} +public record UserInfoDto(String account, String email, Grade grade) { +} \ No newline at end of file diff --git a/src/main/java/wanted/media/user/dto/VerifyRequest.java b/src/main/java/wanted/media/user/dto/VerifyRequest.java index a6fb53e..3d0b086 100644 --- a/src/main/java/wanted/media/user/dto/VerifyRequest.java +++ b/src/main/java/wanted/media/user/dto/VerifyRequest.java @@ -2,19 +2,10 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lombok.Data; -@Data -public class VerifyRequest { - @NotBlank - @Size(max = 50) - private String account; - - @NotBlank - @Size(min = 10, max = 200) - private String password; - - @NotBlank - @Size(max = 10) - private String inputCode; //사용자 입력 인증코드 -} +public record VerifyRequest( + @NotBlank @Size(max = 50) String account, + @NotBlank @Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") String password, + @NotBlank @Size(max = 10) String inputCode +) { +} \ No newline at end of file diff --git a/src/main/java/wanted/media/user/dto/VerifyResponse.java b/src/main/java/wanted/media/user/dto/VerifyResponse.java index 8ef1740..57164b5 100644 --- a/src/main/java/wanted/media/user/dto/VerifyResponse.java +++ b/src/main/java/wanted/media/user/dto/VerifyResponse.java @@ -1,13 +1,4 @@ package wanted.media.user.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class VerifyResponse { - private String message; - private UserInfoDto userInfo; //사용자 정보 -} +public record VerifyResponse(String message, UserInfoDto dto) { +} \ No newline at end of file From 89b64f1ff6288214d1f7589caf9fb18d7b5be379 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 18:38:12 +0900 Subject: [PATCH 59/69] =?UTF-8?q?refactor=20:=20Exception=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/service/UserService.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 8de6478..c52a5a6 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -4,6 +4,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import wanted.media.exception.InvalidPasswordException; +import wanted.media.exception.UserNotFoundException; +import wanted.media.exception.VerificationCodeExpiredException; +import wanted.media.exception.VerificationCodeMismatchException; import wanted.media.user.domain.Code; import wanted.media.user.domain.Grade; import wanted.media.user.domain.User; @@ -12,19 +16,18 @@ import wanted.media.user.repository.UserRepository; import java.time.LocalDateTime; +import java.util.List; @Service @RequiredArgsConstructor @Transactional public class UserService { - private final UserRepository userRepository; private final CodeRepository codeRepository; private final BCryptPasswordEncoder passwordEncoder; private final UserValidator userValidator; private final GenerateCode generateCode; - //회원가입 public SignUpResponse signUp(SignUpRequest request) { // 1. 사용자 입력내용 검증 @@ -61,29 +64,31 @@ public SignUpResponse signUp(SignUpRequest request) { //가입승인 public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { // 1. account로 사용자 조회 - User user = userRepository.findByAccount(verifyRequest.getAccount()) - .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + User user = userRepository.findByAccount(verifyRequest.account()) + .orElseThrow(UserNotFoundException::new); // 2. 비밀번호 검증 - if (!passwordEncoder.matches(verifyRequest.getPassword(), user.getPassword())) { - throw new RuntimeException("비밀번호가 일치하지 않습니다."); + if (!passwordEncoder.matches(verifyRequest.password(), user.getPassword())) { + throw new InvalidPasswordException(); } // 3. 사용자 인증코드 검증 - Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.getInputCode()) - .orElseThrow(() -> new RuntimeException("인증코드가 일치하지 않습니다.")); - // 4. 인증코드 유효성 검증 (유효시간 15분) + Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.inputCode()) + .orElseThrow(VerificationCodeMismatchException::new); + // 4. 해당 사용자의 모든 인증코드 조회 (최신순 정렬) + List userCodes = codeRepository.findAllByUserOrderByCreatedTimeDesc(user); + // 5. 해당 사용자에게 발급된 모든 인증코드와 입력된 인증코드 일치 조회 + if (!userCodes.isEmpty() && !userCodes.get(0).equals(code)) { + throw new VerificationCodeMismatchException(); + } + // 6. 인증코드 유효성 검증 (유효시간 15분) if (code.getCreatedTime().plusMinutes(15).isBefore(LocalDateTime.now())) { - throw new RuntimeException("만료된 인증코드입니다."); + throw new VerificationCodeExpiredException(); } - // 5. 인증 완료 -> 회원 등급 변경 (normal -> premium) + // 7. 인증 완료 -> 회원 등급 변경 (normal -> premium) userRepository.updateUserGrade(user.getAccount(), Grade.PREMIUM_USER); - // 6. 인증 완료 회원 인증코드 삭제 + // 8. 인증 완료 회원 인증코드 삭제 codeRepository.deleteByUser(user); - // 7. 변경된 사용자정보 다시 조회 - User updateUserInfo = userRepository.findByAccount(user.getAccount()) - .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); - return new VerifyResponse("인증이 성공적으로 완료되었습니다!", - new UserInfoDto(updateUserInfo.getAccount(), updateUserInfo.getEmail(), updateUserInfo.getGrade())); + new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade())); } } From 49843142191a2a02d2bd2ea592648232d9d4345f Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 18:51:43 +0900 Subject: [PATCH 60/69] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=BD=94=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/repository/CodeRepository.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/user/repository/CodeRepository.java b/src/main/java/wanted/media/user/repository/CodeRepository.java index 4e7fb6d..cede332 100644 --- a/src/main/java/wanted/media/user/repository/CodeRepository.java +++ b/src/main/java/wanted/media/user/repository/CodeRepository.java @@ -1,15 +1,23 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import wanted.media.user.domain.Code; import wanted.media.user.domain.User; +import java.util.List; import java.util.Optional; public interface CodeRepository extends JpaRepository { - //인증코드 검증 + // 특정 사용자 인증코드로 조회 Optional findByUserAndAuthCode(User user, String authCode); //사용자가 발급받은 인증코드 삭제 void deleteByUser(User user); + + // 사용자에 대해 모든 인증 코드 조회 + @Query("SELECT c FROM Code c WHERE c.user = :user ORDER BY c.createdTime DESC") + List findAllByUserOrderByCreatedTimeDesc(@Param("user") User user); + } From 8abeddedc58a33b8df3cd6f47ec4ed78ccb572b7 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 18:57:03 +0900 Subject: [PATCH 61/69] =?UTF-8?q?feat=20:=20=EC=9D=B8=EC=A6=9D=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/user/controller/UserController.java | 11 ++++++---- .../media/user/dto/ReissueCodeRequest.java | 4 ++++ .../media/user/dto/ReissueCodeResponse.java | 4 ++++ .../media/user/service/UserService.java | 22 +++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/main/java/wanted/media/user/dto/ReissueCodeRequest.java create mode 100644 src/main/java/wanted/media/user/dto/ReissueCodeResponse.java diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 570bcd3..58492c5 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -8,10 +8,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import wanted.media.user.dto.SignUpRequest; -import wanted.media.user.dto.SignUpResponse; -import wanted.media.user.dto.VerifyRequest; -import wanted.media.user.dto.VerifyResponse; +import wanted.media.user.dto.*; import wanted.media.user.service.UserService; @RestController @@ -38,4 +35,10 @@ public ResponseEntity approveSignUp(@RequestBody VerifyRequest r return ResponseEntity.status(HttpStatus.CREATED).body(response); } + // 인증코드 재발급 요청 API + @PostMapping("/reissue-code") + public ResponseEntity reissueCode(@RequestBody ReissueCodeRequest request) { + ReissueCodeResponse response = userService.reissueCode(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } } diff --git a/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java b/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java new file mode 100644 index 0000000..00b9911 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java @@ -0,0 +1,4 @@ +package wanted.media.user.dto; + +public record ReissueCodeRequest(String account, String password) { +} diff --git a/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java b/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java new file mode 100644 index 0000000..ace75df --- /dev/null +++ b/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java @@ -0,0 +1,4 @@ +package wanted.media.user.dto; + +public record ReissueCodeResponse(String message, String newAuthCode) { +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index c52a5a6..5bc0844 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -91,4 +91,26 @@ public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade())); } + // 인증코드 재발급 + public ReissueCodeResponse reissueCode(ReissueCodeRequest reissueCodeRequest) { + // 1. account로 사용자 조회 + User user = userRepository.findByAccount(reissueCodeRequest.account()) + .orElseThrow(UserNotFoundException::new); + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(reissueCodeRequest.password(), user.getPassword())) { + throw new InvalidPasswordException(); + } + // 3. 새로운 인증코드 발급 + String newAuthCode = generateCode.codeGenerate(); + // 4. 코드 객체 생성 + Code newCode = Code.builder() + .user(user) + .authCode(newAuthCode) + .createdTime(LocalDateTime.now()) + .build(); + // 5. 코드 db 저장 + codeRepository.save(newCode); + + return new ReissueCodeResponse("인증코드가 성공적으로 재발급되었습니다.", newAuthCode); + } } From f2ff33ac46c10d4490347717c68efaf80ce5b504 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 21:47:01 +0900 Subject: [PATCH 62/69] Changes --- .../java/wanted/media/user/dto/UserInfoDto.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/wanted/media/user/dto/UserInfoDto.java diff --git a/src/main/java/wanted/media/user/dto/UserInfoDto.java b/src/main/java/wanted/media/user/dto/UserInfoDto.java new file mode 100644 index 0000000..5dcf80a --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserInfoDto.java @@ -0,0 +1,13 @@ +package wanted.media.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class UserInfoDto { + private String account; + private String email; +} From a8dbf4d598cf8c1d9ddb6dd7679ed96102fbd0e1 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 02:57:17 +0900 Subject: [PATCH 63/69] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EB=93=B1?= =?UTF-8?q?=EA=B8=89=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/dto/UserInfoDto.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/wanted/media/user/dto/UserInfoDto.java b/src/main/java/wanted/media/user/dto/UserInfoDto.java index 5dcf80a..6557be3 100644 --- a/src/main/java/wanted/media/user/dto/UserInfoDto.java +++ b/src/main/java/wanted/media/user/dto/UserInfoDto.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import wanted.media.user.domain.Grade; @NoArgsConstructor @AllArgsConstructor @@ -10,4 +11,5 @@ public class UserInfoDto { private String account; private String email; + private Grade grade; // 현재 회원등급 } From 8dc959db9d2f65fc289f1d342f0f9e115c7651f2 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 21:50:33 +0900 Subject: [PATCH 64/69] Changes --- .../media/user/controller/UserController.java | 13 ++++++++ .../java/wanted/media/user/domain/Code.java | 2 +- .../wanted/media/user/dto/VerifyRequest.java | 20 +++++++++++ .../wanted/media/user/dto/VerifyResponse.java | 13 ++++++++ .../media/user/repository/CodeRepository.java | 9 +++-- .../media/user/repository/UserRepository.java | 9 +++++ .../media/user/service/UserService.java | 33 +++++++++++++++++-- 7 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/main/java/wanted/media/user/dto/VerifyRequest.java create mode 100644 src/main/java/wanted/media/user/dto/VerifyResponse.java diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 1d649c4..1235a03 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.RestController; import wanted.media.user.dto.SignUpRequest; import wanted.media.user.dto.SignUpResponse; +import wanted.media.user.dto.VerifyRequest; +import wanted.media.user.dto.VerifyResponse; import wanted.media.user.dto.UserLoginRequestDto; import wanted.media.user.dto.UserLoginResponseDto; import wanted.media.user.service.UserService; @@ -32,4 +34,15 @@ public ResponseEntity signUp(@Validated @RequestBody SignUpReque SignUpResponse response = userService.signUp(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } + + /* + * 가입승인 API + * 회원등급 (normal -> premium) + * */ + @PostMapping("/approve") + public ResponseEntity approveSignUp(@RequestBody VerifyRequest request) { + VerifyResponse response = userService.approveSignUp(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + } diff --git a/src/main/java/wanted/media/user/domain/Code.java b/src/main/java/wanted/media/user/domain/Code.java index c6167c8..2811fe4 100644 --- a/src/main/java/wanted/media/user/domain/Code.java +++ b/src/main/java/wanted/media/user/domain/Code.java @@ -19,7 +19,7 @@ public class Code { @Column(nullable = false) private Long codeId; - @OneToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; diff --git a/src/main/java/wanted/media/user/dto/VerifyRequest.java b/src/main/java/wanted/media/user/dto/VerifyRequest.java new file mode 100644 index 0000000..a6fb53e --- /dev/null +++ b/src/main/java/wanted/media/user/dto/VerifyRequest.java @@ -0,0 +1,20 @@ +package wanted.media.user.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class VerifyRequest { + @NotBlank + @Size(max = 50) + private String account; + + @NotBlank + @Size(min = 10, max = 200) + private String password; + + @NotBlank + @Size(max = 10) + private String inputCode; //사용자 입력 인증코드 +} diff --git a/src/main/java/wanted/media/user/dto/VerifyResponse.java b/src/main/java/wanted/media/user/dto/VerifyResponse.java new file mode 100644 index 0000000..8ef1740 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/VerifyResponse.java @@ -0,0 +1,13 @@ +package wanted.media.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class VerifyResponse { + private String message; + private UserInfoDto userInfo; //사용자 정보 +} diff --git a/src/main/java/wanted/media/user/repository/CodeRepository.java b/src/main/java/wanted/media/user/repository/CodeRepository.java index d24c229..4e7fb6d 100644 --- a/src/main/java/wanted/media/user/repository/CodeRepository.java +++ b/src/main/java/wanted/media/user/repository/CodeRepository.java @@ -4,7 +4,12 @@ import wanted.media.user.domain.Code; import wanted.media.user.domain.User; +import java.util.Optional; + public interface CodeRepository extends JpaRepository { - // 사용자별 인증코드 중복확인 - boolean existsByUserAndAuthCode(User user, String newAuthCode); + //인증코드 검증 + Optional findByUserAndAuthCode(User user, String authCode); + + //사용자가 발급받은 인증코드 삭제 + void deleteByUser(User user); } diff --git a/src/main/java/wanted/media/user/repository/UserRepository.java b/src/main/java/wanted/media/user/repository/UserRepository.java index f48d108..3cff36c 100644 --- a/src/main/java/wanted/media/user/repository/UserRepository.java +++ b/src/main/java/wanted/media/user/repository/UserRepository.java @@ -1,6 +1,10 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import wanted.media.user.domain.Grade; import wanted.media.user.domain.User; import java.util.Optional; @@ -12,4 +16,9 @@ public interface UserRepository extends JpaRepository { // 사용자 이메일로 회원 조회 Optional findByEmail(String email); + + // 가입인증 회원 등급 변경 + @Modifying + @Query("UPDATE User u SET u.grade = :grade WHERE u.account = :account") + void updateUserGrade(@Param("account") String account, @Param("grade") Grade grade); } diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 49da7b8..3b22f88 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -1,9 +1,9 @@ package wanted.media.user.service; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import wanted.media.user.config.TokenProvider; import wanted.media.user.domain.Code; import wanted.media.user.domain.Grade; @@ -29,7 +29,7 @@ public class UserService { private final UserValidator userValidator; private final GenerateCode generateCode; - @Transactional + public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); @@ -77,4 +77,33 @@ public SignUpResponse signUp(SignUpRequest request) { // 9. SignUpResponse 생성 및 반환 return new SignUpResponse("회원가입이 완료되었습니다.", userCreateDto, verificationCode); } + + //가입승인 + public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { + // 1. account로 사용자 조회 + User user = userRepository.findByAccount(verifyRequest.getAccount()) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(verifyRequest.getPassword(), user.getPassword())) { + throw new RuntimeException("비밀번호가 일치하지 않습니다."); + } + // 3. 사용자 인증코드 검증 + Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.getInputCode()) + .orElseThrow(() -> new RuntimeException("인증코드가 일치하지 않습니다.")); + // 4. 인증코드 유효성 검증 (유효시간 15분) + if (code.getCreatedTime().plusMinutes(15).isBefore(LocalDateTime.now())) { + throw new RuntimeException("만료된 인증코드입니다."); + } + // 5. 인증 완료 -> 회원 등급 변경 (normal -> premium) + userRepository.updateUserGrade(user.getAccount(), Grade.PREMIUM_USER); + // 6. 인증 완료 회원 인증코드 삭제 + codeRepository.deleteByUser(user); + // 7. 변경된 사용자정보 다시 조회 + User updateUserInfo = userRepository.findByAccount(user.getAccount()) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + + return new VerifyResponse("인증이 성공적으로 완료되었습니다!", + new UserInfoDto(updateUserInfo.getAccount(), updateUserInfo.getEmail(), updateUserInfo.getGrade())); + } + } From 5d6de40fba4e2adde36af2c2cb972a6066dd9e35 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 21:52:56 +0900 Subject: [PATCH 65/69] Changes --- src/main/java/wanted/media/exception/ErrorCode.java | 7 ++++++- .../media/exception/InvalidPasswordException.java | 8 ++++++++ .../wanted/media/exception/UserNotFoundException.java | 7 +++++++ .../exception/VerificationCodeExpiredException.java | 7 +++++++ .../exception/VerificationCodeMismatchException.java | 7 +++++++ .../exception/handler/GlobalExceptionHandler.java | 10 ++++++++-- 6 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/main/java/wanted/media/exception/InvalidPasswordException.java create mode 100644 src/main/java/wanted/media/exception/UserNotFoundException.java create mode 100644 src/main/java/wanted/media/exception/VerificationCodeExpiredException.java create mode 100644 src/main/java/wanted/media/exception/VerificationCodeMismatchException.java diff --git a/src/main/java/wanted/media/exception/ErrorCode.java b/src/main/java/wanted/media/exception/ErrorCode.java index dd34485..b709326 100644 --- a/src/main/java/wanted/media/exception/ErrorCode.java +++ b/src/main/java/wanted/media/exception/ErrorCode.java @@ -10,7 +10,12 @@ public enum ErrorCode { ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."), // 클라이언트의 입력 값에 대한 일반적인 오류 (@PathVariable, @RequestParam가 잘못되었을 때) INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "클라이언트의 입력 값을 확인해주세요."), - INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."); + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), + VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "인증코드가 일치하지 않습니다."), + VERIFICATION_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "만료된 인증코드입니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/wanted/media/exception/InvalidPasswordException.java b/src/main/java/wanted/media/exception/InvalidPasswordException.java new file mode 100644 index 0000000..3ff8264 --- /dev/null +++ b/src/main/java/wanted/media/exception/InvalidPasswordException.java @@ -0,0 +1,8 @@ +package wanted.media.exception; + +public class InvalidPasswordException extends BaseException { + public InvalidPasswordException() { + + super(ErrorCode.INVALID_PASSWORD); + } +} diff --git a/src/main/java/wanted/media/exception/UserNotFoundException.java b/src/main/java/wanted/media/exception/UserNotFoundException.java new file mode 100644 index 0000000..5e8535d --- /dev/null +++ b/src/main/java/wanted/media/exception/UserNotFoundException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class UserNotFoundException extends BaseException { + public UserNotFoundException() { + super(ErrorCode.USER_NOT_FOUND); + } +} diff --git a/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java b/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java new file mode 100644 index 0000000..6fd18c3 --- /dev/null +++ b/src/main/java/wanted/media/exception/VerificationCodeExpiredException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class VerificationCodeExpiredException extends BaseException { + public VerificationCodeExpiredException() { + super(ErrorCode.VERIFICATION_CODE_EXPIRED); + } +} diff --git a/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java b/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java new file mode 100644 index 0000000..d5f538d --- /dev/null +++ b/src/main/java/wanted/media/exception/VerificationCodeMismatchException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class VerificationCodeMismatchException extends BaseException { + public VerificationCodeMismatchException() { + super(ErrorCode.VERIFICATION_CODE_MISMATCH); + } +} diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index 0ee7a8f..15f413b 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -5,6 +5,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import wanted.media.exception.BaseException; +import wanted.media.exception.ErrorCode; import wanted.media.exception.CustomException; import wanted.media.exception.ErrorCode; import wanted.media.exception.ErrorResponse; @@ -18,7 +20,6 @@ public ResponseEntity handleBadRequestException(BadRequestExcepti return ResponseEntity.badRequest() .body(new ErrorResponse(400, e.getMessage())); } - @ExceptionHandler(NotFoundException.class) public ResponseEntity handlePostNotFound(NotFoundException e) { ErrorCode errorCode = e.getErrorCode(); @@ -28,11 +29,16 @@ public ResponseEntity handlePostNotFound(NotFoundException e) { ); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } - @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(final CustomException e) { return ResponseEntity .status(e.getErrorCode().getStatus().value()) .body(new ErrorResponse(e.getErrorCode().getStatus().value(), e.getCustomMessage())); } + @ExceptionHandler(BaseException.class) + public ResponseEntity handleBaseException(BaseException e) { + ErrorCode errorCode = e.getErrorCode(); + return ResponseEntity.status(errorCode.getStatus()) + .body(new ErrorResponse(errorCode.getStatus().value(), errorCode.getMessage())); + } } From 98de95f4cb756a8ecb87690262504c5f87299f26 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 18:37:03 +0900 Subject: [PATCH 66/69] =?UTF-8?q?refactor=20:=20VerifyRequest,=20VerifyRes?= =?UTF-8?q?ponse,=20UserInfoDto=20radio=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/dto/UserInfoDto.java | 13 ++---------- .../wanted/media/user/dto/VerifyRequest.java | 21 ++++++------------- .../wanted/media/user/dto/VerifyResponse.java | 13 ++---------- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/src/main/java/wanted/media/user/dto/UserInfoDto.java b/src/main/java/wanted/media/user/dto/UserInfoDto.java index 6557be3..c2899a4 100644 --- a/src/main/java/wanted/media/user/dto/UserInfoDto.java +++ b/src/main/java/wanted/media/user/dto/UserInfoDto.java @@ -1,15 +1,6 @@ package wanted.media.user.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; import wanted.media.user.domain.Grade; -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class UserInfoDto { - private String account; - private String email; - private Grade grade; // 현재 회원등급 -} +public record UserInfoDto(String account, String email, Grade grade) { +} \ No newline at end of file diff --git a/src/main/java/wanted/media/user/dto/VerifyRequest.java b/src/main/java/wanted/media/user/dto/VerifyRequest.java index a6fb53e..3d0b086 100644 --- a/src/main/java/wanted/media/user/dto/VerifyRequest.java +++ b/src/main/java/wanted/media/user/dto/VerifyRequest.java @@ -2,19 +2,10 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lombok.Data; -@Data -public class VerifyRequest { - @NotBlank - @Size(max = 50) - private String account; - - @NotBlank - @Size(min = 10, max = 200) - private String password; - - @NotBlank - @Size(max = 10) - private String inputCode; //사용자 입력 인증코드 -} +public record VerifyRequest( + @NotBlank @Size(max = 50) String account, + @NotBlank @Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") String password, + @NotBlank @Size(max = 10) String inputCode +) { +} \ No newline at end of file diff --git a/src/main/java/wanted/media/user/dto/VerifyResponse.java b/src/main/java/wanted/media/user/dto/VerifyResponse.java index 8ef1740..57164b5 100644 --- a/src/main/java/wanted/media/user/dto/VerifyResponse.java +++ b/src/main/java/wanted/media/user/dto/VerifyResponse.java @@ -1,13 +1,4 @@ package wanted.media.user.dto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class VerifyResponse { - private String message; - private UserInfoDto userInfo; //사용자 정보 -} +public record VerifyResponse(String message, UserInfoDto dto) { +} \ No newline at end of file From 6873fb3c6877546c8beb2975eb7942478fd8179d Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 21:54:36 +0900 Subject: [PATCH 67/69] Changes --- .../media/user/service/UserService.java | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 3b22f88..4c93f5e 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -5,6 +5,10 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import wanted.media.user.config.TokenProvider; +import wanted.media.exception.InvalidPasswordException; +import wanted.media.exception.UserNotFoundException; +import wanted.media.exception.VerificationCodeExpiredException; +import wanted.media.exception.VerificationCodeMismatchException; import wanted.media.user.domain.Code; import wanted.media.user.domain.Grade; import wanted.media.user.domain.Token; @@ -16,6 +20,7 @@ import java.time.LocalDateTime; import java.util.Optional; +import java.util.List; @Service @RequiredArgsConstructor @@ -81,29 +86,31 @@ public SignUpResponse signUp(SignUpRequest request) { //가입승인 public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { // 1. account로 사용자 조회 - User user = userRepository.findByAccount(verifyRequest.getAccount()) - .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + User user = userRepository.findByAccount(verifyRequest.account()) + .orElseThrow(UserNotFoundException::new); // 2. 비밀번호 검증 - if (!passwordEncoder.matches(verifyRequest.getPassword(), user.getPassword())) { - throw new RuntimeException("비밀번호가 일치하지 않습니다."); + if (!passwordEncoder.matches(verifyRequest.password(), user.getPassword())) { + throw new InvalidPasswordException(); } // 3. 사용자 인증코드 검증 - Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.getInputCode()) - .orElseThrow(() -> new RuntimeException("인증코드가 일치하지 않습니다.")); - // 4. 인증코드 유효성 검증 (유효시간 15분) + Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.inputCode()) + .orElseThrow(VerificationCodeMismatchException::new); + // 4. 해당 사용자의 모든 인증코드 조회 (최신순 정렬) + List userCodes = codeRepository.findAllByUserOrderByCreatedTimeDesc(user); + // 5. 해당 사용자에게 발급된 모든 인증코드와 입력된 인증코드 일치 조회 + if (!userCodes.isEmpty() && !userCodes.get(0).equals(code)) { + throw new VerificationCodeMismatchException(); + } + // 6. 인증코드 유효성 검증 (유효시간 15분) if (code.getCreatedTime().plusMinutes(15).isBefore(LocalDateTime.now())) { - throw new RuntimeException("만료된 인증코드입니다."); + throw new VerificationCodeExpiredException(); } - // 5. 인증 완료 -> 회원 등급 변경 (normal -> premium) + // 7. 인증 완료 -> 회원 등급 변경 (normal -> premium) userRepository.updateUserGrade(user.getAccount(), Grade.PREMIUM_USER); - // 6. 인증 완료 회원 인증코드 삭제 + // 8. 인증 완료 회원 인증코드 삭제 codeRepository.deleteByUser(user); - // 7. 변경된 사용자정보 다시 조회 - User updateUserInfo = userRepository.findByAccount(user.getAccount()) - .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); - return new VerifyResponse("인증이 성공적으로 완료되었습니다!", - new UserInfoDto(updateUserInfo.getAccount(), updateUserInfo.getEmail(), updateUserInfo.getGrade())); + new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade())); } } From 6756816b6e59ab7d26b0c173ec38e906a4d4dc61 Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 18:51:43 +0900 Subject: [PATCH 68/69] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=BD=94=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/repository/CodeRepository.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/user/repository/CodeRepository.java b/src/main/java/wanted/media/user/repository/CodeRepository.java index 4e7fb6d..cede332 100644 --- a/src/main/java/wanted/media/user/repository/CodeRepository.java +++ b/src/main/java/wanted/media/user/repository/CodeRepository.java @@ -1,15 +1,23 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import wanted.media.user.domain.Code; import wanted.media.user.domain.User; +import java.util.List; import java.util.Optional; public interface CodeRepository extends JpaRepository { - //인증코드 검증 + // 특정 사용자 인증코드로 조회 Optional findByUserAndAuthCode(User user, String authCode); //사용자가 발급받은 인증코드 삭제 void deleteByUser(User user); + + // 사용자에 대해 모든 인증 코드 조회 + @Query("SELECT c FROM Code c WHERE c.user = :user ORDER BY c.createdTime DESC") + List findAllByUserOrderByCreatedTimeDesc(@Param("user") User user); + } From 905d212616eccec77d8ad5020fab607ef4c815ac Mon Sep 17 00:00:00 2001 From: jeongeungyeong Date: Mon, 26 Aug 2024 21:55:11 +0900 Subject: [PATCH 69/69] Changes --- .../media/user/controller/UserController.java | 7 ++++++ .../media/user/dto/ReissueCodeRequest.java | 4 ++++ .../media/user/dto/ReissueCodeResponse.java | 4 ++++ .../media/user/service/UserService.java | 22 +++++++++++++++++++ 4 files changed, 37 insertions(+) create mode 100644 src/main/java/wanted/media/user/dto/ReissueCodeRequest.java create mode 100644 src/main/java/wanted/media/user/dto/ReissueCodeResponse.java diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 1235a03..621cadb 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -14,6 +14,7 @@ import wanted.media.user.dto.VerifyResponse; import wanted.media.user.dto.UserLoginRequestDto; import wanted.media.user.dto.UserLoginResponseDto; +import wanted.media.user.dto.*; import wanted.media.user.service.UserService; @RestController @@ -45,4 +46,10 @@ public ResponseEntity approveSignUp(@RequestBody VerifyRequest r return ResponseEntity.status(HttpStatus.CREATED).body(response); } + // 인증코드 재발급 요청 API + @PostMapping("/reissue-code") + public ResponseEntity reissueCode(@RequestBody ReissueCodeRequest request) { + ReissueCodeResponse response = userService.reissueCode(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } } diff --git a/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java b/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java new file mode 100644 index 0000000..00b9911 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/ReissueCodeRequest.java @@ -0,0 +1,4 @@ +package wanted.media.user.dto; + +public record ReissueCodeRequest(String account, String password) { +} diff --git a/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java b/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java new file mode 100644 index 0000000..ace75df --- /dev/null +++ b/src/main/java/wanted/media/user/dto/ReissueCodeResponse.java @@ -0,0 +1,4 @@ +package wanted.media.user.dto; + +public record ReissueCodeResponse(String message, String newAuthCode) { +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 4c93f5e..3ab0392 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -113,4 +113,26 @@ public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade())); } + // 인증코드 재발급 + public ReissueCodeResponse reissueCode(ReissueCodeRequest reissueCodeRequest) { + // 1. account로 사용자 조회 + User user = userRepository.findByAccount(reissueCodeRequest.account()) + .orElseThrow(UserNotFoundException::new); + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(reissueCodeRequest.password(), user.getPassword())) { + throw new InvalidPasswordException(); + } + // 3. 새로운 인증코드 발급 + String newAuthCode = generateCode.codeGenerate(); + // 4. 코드 객체 생성 + Code newCode = Code.builder() + .user(user) + .authCode(newAuthCode) + .createdTime(LocalDateTime.now()) + .build(); + // 5. 코드 db 저장 + codeRepository.save(newCode); + + return new ReissueCodeResponse("인증코드가 성공적으로 재발급되었습니다.", newAuthCode); + } }