From 7582e63b2d74e18e37485577a9aa52fb54a08922 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 31 Jan 2025 10:20:43 +0900 Subject: [PATCH 001/258] =?UTF-8?q?chore:=20security,=20jwt=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=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 --- build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle b/build.gradle index 5bf64ca9..49fe15b7 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,14 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' From 0b873fdc8fe1ec3a8399837948d9a9ac862d94e4 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Feb 2025 21:13:03 +0900 Subject: [PATCH 002/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20ignore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index c2065bc2..653e7c70 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ out/ ### VS Code ### .vscode/ + +.DS_Store + +application*.yml From febf67c98ad1e5bb937e24bcc17216fbae967cc5 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Feb 2025 21:15:03 +0900 Subject: [PATCH 003/258] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20entity=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 --- .../user/application/UserService.java | 20 ++++++++++++ .../java/com/swyp8team2/user/domain/User.java | 31 +++++++++++++++++++ .../user/domain/UserRepository.java | 8 +++++ 3 files changed, 59 insertions(+) create mode 100644 src/main/java/com/swyp8team2/user/application/UserService.java create mode 100644 src/main/java/com/swyp8team2/user/domain/User.java create mode 100644 src/main/java/com/swyp8team2/user/domain/UserRepository.java diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java new file mode 100644 index 00000000..ea809311 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -0,0 +1,20 @@ +package com.swyp8team2.user.application; + +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public Long createUser() { + User user = userRepository.save(User.create("user_" + System.currentTimeMillis())); + return user.getId(); + } +} diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java new file mode 100644 index 00000000..25ec4e04 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -0,0 +1,31 @@ +package com.swyp8team2.user.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "users") +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String nickname; + + public User(Long id, String nickname) { + this.id = id; + this.nickname = nickname; + } + + public static User create(String nickname) { + return new User(null, nickname); + } +} diff --git a/src/main/java/com/swyp8team2/user/domain/UserRepository.java b/src/main/java/com/swyp8team2/user/domain/UserRepository.java new file mode 100644 index 00000000..a92bbdb2 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/domain/UserRepository.java @@ -0,0 +1,8 @@ +package com.swyp8team2.user.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { +} From 18fa032f40906c1e13c26e0ec7e4493a23fc8e19 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Feb 2025 21:16:05 +0900 Subject: [PATCH 004/258] =?UTF-8?q?feat:=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=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 --- .../swyp8team2/auth/application/JwtClaim.java | 24 +++++ .../auth/application/JwtProvider.java | 90 +++++++++++++++++++ .../auth/application/TokenPair.java | 7 ++ .../auth/presentation/dto/TokenResponse.java | 4 + .../filter/HeaderTokenExtractor.java | 20 +++++ .../presentation/filter/JwtAuthFilter.java | 54 +++++++++++ .../filter/JwtExceptionFilter.java | 38 ++++++++ .../common/config/CommonConfig.java | 16 ++++ 8 files changed, 253 insertions(+) create mode 100644 src/main/java/com/swyp8team2/auth/application/JwtClaim.java create mode 100644 src/main/java/com/swyp8team2/auth/application/JwtProvider.java create mode 100644 src/main/java/com/swyp8team2/auth/application/TokenPair.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java create mode 100644 src/main/java/com/swyp8team2/common/config/CommonConfig.java diff --git a/src/main/java/com/swyp8team2/auth/application/JwtClaim.java b/src/main/java/com/swyp8team2/auth/application/JwtClaim.java new file mode 100644 index 00000000..a52f3b37 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/JwtClaim.java @@ -0,0 +1,24 @@ +package com.swyp8team2.auth.application; + +public class JwtClaim { + + public static final String ID = "id"; + + private final String id; + + public JwtClaim(Long id) { + this.id = String.valueOf(id); + } + + public static JwtClaim from(Long id) { + return new JwtClaim(id); + } + + public Long idAsLong() { + return Long.parseLong(id); + } + + public String id() { + return id; + } +} \ No newline at end of file diff --git a/src/main/java/com/swyp8team2/auth/application/JwtProvider.java b/src/main/java/com/swyp8team2/auth/application/JwtProvider.java new file mode 100644 index 00000000..c61d31d7 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/JwtProvider.java @@ -0,0 +1,90 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import com.swyp8team2.common.exception.UnauthorizedException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +@Slf4j +@Component +public class JwtProvider { + + private static final long ACCESS_TOKEN_EXPIRATION_HOUR = 2; + private static final long REFRESH_TOKEN_EXPIRATION_HOUR = 24 * 14; + + private final Key key; + private final Clock clock; + private final String issuer; + + public JwtProvider( + @Value("${jwt.token.secret-key}") String key, + @Value("${jwt.token.issuer}") String issuer, + Clock clock) { + this.key = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)); + this.clock = clock; + this.issuer = issuer; + } + + public TokenPair createToken(JwtClaim claim) { + return new TokenPair(createAccessToken(claim), createRefreshToken(claim)); + } + + public String createAccessToken(JwtClaim claim) { + return createToken(claim, ACCESS_TOKEN_EXPIRATION_HOUR); + } + + public String createRefreshToken(JwtClaim claim) { + return createToken(claim, REFRESH_TOKEN_EXPIRATION_HOUR); + } + + private String createToken(JwtClaim claim, long expiration) { + Instant now = clock.instant(); + Instant expiredAt = now.plus(expiration, ChronoUnit.HOURS); + + return Jwts.builder() + .claim(JwtClaim.ID, claim.id()) + .setIssuedAt(Date.from(now)) + .setIssuer(issuer) + .setExpiration(Date.from(expiredAt)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public JwtClaim parseToken(String token) { + try { + JwtParser parser = Jwts.parserBuilder() + .requireIssuer(issuer) + .setSigningKey(key) + .build(); + + Claims claims = parser.parseClaimsJws(token) + .getBody(); + String userId = (String) claims.get(JwtClaim.ID); + return new JwtClaim(Long.parseLong(userId)); + } catch (ExpiredJwtException e) { + log.trace("Expired Jwt Token: {}", e.getMessage()); + throw new UnauthorizedException(ErrorCode.EXPIRED_TOKEN); + } catch (JwtException e) { + log.trace("Invalid Jwt Token: {}", e.getMessage()); + throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); + } catch (Exception e) { + throw new InternalServerException(); + } + } +} diff --git a/src/main/java/com/swyp8team2/auth/application/TokenPair.java b/src/main/java/com/swyp8team2/auth/application/TokenPair.java new file mode 100644 index 00000000..22c8db34 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/TokenPair.java @@ -0,0 +1,7 @@ +package com.swyp8team2.auth.application; + +public record TokenPair( + String accessToken, + String refreshToken +) { +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java new file mode 100644 index 00000000..0997e640 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java @@ -0,0 +1,4 @@ +package com.swyp8team2.auth.presentation.dto; + +public record TokenResponse(String accessToken, String refreshToken) { +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java b/src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java new file mode 100644 index 00000000..cc4aeca8 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java @@ -0,0 +1,20 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.UnauthorizedException; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +public class HeaderTokenExtractor { + + public static final String BEARER = "Bearer "; + + public String extractToken(String authorization) { + if (Objects.isNull(authorization) || !authorization.startsWith(BEARER)) { + throw new UnauthorizedException(ErrorCode.INVALID_AUTH_HEADER); + } + return authorization.substring(BEARER.length()); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java new file mode 100644 index 00000000..82512246 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -0,0 +1,54 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.swyp8team2.auth.application.JwtClaim; +import com.swyp8team2.auth.application.JwtProvider; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtProvider jwtProvider; + private final HeaderTokenExtractor headerTokenExtractor; + private final UserRepository userRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + + JwtClaim claim = jwtProvider.parseToken(headerTokenExtractor.extractToken(authorization)); + + User user = userRepository.findById(claim.idAsLong()) + .orElseThrow(() -> new IllegalArgumentException("ErrorCode.INVALID_USER")); + + List authorities = Collections.emptyList(); + UserDetails userDetails = org.springframework.security.core.userdetails.User.builder() + .username(user.getNickname()) + .password("") + .authorities(authorities) + .build(); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + doFilter(request, response, filterChain); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java new file mode 100644 index 00000000..4d354f27 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java @@ -0,0 +1,38 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ErrorResponse; +import com.swyp8team2.common.exception.UnauthorizedException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtExceptionFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (UnauthorizedException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(e.getErrorCode()))); + } catch (Exception e) { + log.error("JwtExceptionFilter error", e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR))); + } + } +} diff --git a/src/main/java/com/swyp8team2/common/config/CommonConfig.java b/src/main/java/com/swyp8team2/common/config/CommonConfig.java new file mode 100644 index 00000000..3ee5458b --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/CommonConfig.java @@ -0,0 +1,16 @@ +package com.swyp8team2.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Clock; + +@Configuration +public class CommonConfig { + + + @Bean + public Clock clock() { + return Clock.systemDefaultZone(); + } +} From b9924d01392e6ae3fc48b367be438f59e92c0450 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Feb 2025 20:29:17 +0900 Subject: [PATCH 005/258] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicationControllerAdvice.java | 32 +++++++++++++++++++ .../exception/ApplicationException.java | 14 ++++++++ .../common/exception/BadRequestException.java | 8 +++++ .../common/exception/ErrorCode.java | 19 +++++++++++ .../common/exception/ErrorResponse.java | 4 +++ .../exception/InternalServerException.java | 8 +++++ .../exception/UnauthorizedException.java | 8 +++++ 7 files changed, 93 insertions(+) create mode 100644 src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java create mode 100644 src/main/java/com/swyp8team2/common/exception/ApplicationException.java create mode 100644 src/main/java/com/swyp8team2/common/exception/BadRequestException.java create mode 100644 src/main/java/com/swyp8team2/common/exception/ErrorCode.java create mode 100644 src/main/java/com/swyp8team2/common/exception/ErrorResponse.java create mode 100644 src/main/java/com/swyp8team2/common/exception/InternalServerException.java create mode 100644 src/main/java/com/swyp8team2/common/exception/UnauthorizedException.java diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java new file mode 100644 index 00000000..5a466e1b --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -0,0 +1,32 @@ +package com.swyp8team2.common.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ApplicationControllerAdvice { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handle(MethodArgumentNotValidException e) { + log.info("MethodArgumentNotValidException", e); + return ResponseEntity.badRequest() + .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handle(HttpRequestMethodNotSupportedException e) { + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error("Exception", e); + return ResponseEntity.internalServerError() + .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR)); + } +} diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationException.java b/src/main/java/com/swyp8team2/common/exception/ApplicationException.java new file mode 100644 index 00000000..e17c4712 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationException.java @@ -0,0 +1,14 @@ +package com.swyp8team2.common.exception; + +import lombok.Getter; + +@Getter +public class ApplicationException extends RuntimeException { + + private final ErrorCode errorCode; + + public ApplicationException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/swyp8team2/common/exception/BadRequestException.java b/src/main/java/com/swyp8team2/common/exception/BadRequestException.java new file mode 100644 index 00000000..c428ca56 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package com.swyp8team2.common.exception; + +public class BadRequestException extends ApplicationException { + + public BadRequestException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java new file mode 100644 index 00000000..35cf4fc1 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -0,0 +1,19 @@ +package com.swyp8team2.common.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + //common + INVALID_ARGUMENT("잘못된 파라미터 요청"), + INTERNAL_SERVER_ERROR("서버 내부 오류"), + + //auth + EXPIRED_TOKEN("토큰 만료"), + INVALID_TOKEN("유효하지 않은 토큰"), + INVALID_AUTH_HEADER("잘못된 인증 헤더"); + + private final String message; +} diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorResponse.java b/src/main/java/com/swyp8team2/common/exception/ErrorResponse.java new file mode 100644 index 00000000..d4d166c7 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/ErrorResponse.java @@ -0,0 +1,4 @@ +package com.swyp8team2.common.exception; + +public record ErrorResponse(ErrorCode errorCode) { +} diff --git a/src/main/java/com/swyp8team2/common/exception/InternalServerException.java b/src/main/java/com/swyp8team2/common/exception/InternalServerException.java new file mode 100644 index 00000000..631645f9 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/InternalServerException.java @@ -0,0 +1,8 @@ +package com.swyp8team2.common.exception; + +public class InternalServerException extends ApplicationException { + + public InternalServerException() { + super(ErrorCode.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/swyp8team2/common/exception/UnauthorizedException.java b/src/main/java/com/swyp8team2/common/exception/UnauthorizedException.java new file mode 100644 index 00000000..6c12129d --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/UnauthorizedException.java @@ -0,0 +1,8 @@ +package com.swyp8team2.common.exception; + +public class UnauthorizedException extends ApplicationException { + + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } +} From 37ca9eebca23135e7ec6ad91aee1f0304b80a060 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Feb 2025 20:30:31 +0900 Subject: [PATCH 006/258] =?UTF-8?q?test:=20jwt=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- .../auth/application/JwtProviderTest.java | 85 +++++++++++++++++++ .../filter/HeaderTokenExtractorTest.java | 59 +++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java create mode 100644 src/test/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractorTest.java diff --git a/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java new file mode 100644 index 00000000..c51a3d32 --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java @@ -0,0 +1,85 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Clock; +import java.time.temporal.ChronoUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; + +class JwtProviderTest { + + @Test + @DisplayName("올바른 access token, refresh token 생성") + void createToken() throws Exception { + //given + JwtProvider jwtProvider = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "issuer", Clock.systemDefaultZone()); + JwtClaim givenClaim = new JwtClaim(1L); + + //when + TokenPair tokenPair = jwtProvider.createToken(givenClaim); + JwtClaim jwtClaim = jwtProvider.parseToken(tokenPair.accessToken()); + + //then + assertAll( + () -> assertThat(tokenPair.accessToken()).isNotNull(), + () -> assertThat(tokenPair.refreshToken()).isNotNull(), + () -> assertThat(jwtClaim.idAsLong()).isEqualTo(givenClaim.idAsLong()) + ); + } + + @Test + @DisplayName("토큰이 만료된 경우 예외가 발생해야 함") + void parseClaim_expiredToken() throws Exception { + //given + Clock clock = Clock.systemDefaultZone(); + Clock mockedClocked = spy(clock); + JwtProvider jwtProvider = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "issuer", mockedClocked); + given(mockedClocked.instant()) + .willReturn(clock.instant().minus(24, ChronoUnit.HOURS)); + + //when then + TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(1L)); + assertThatThrownBy(() -> jwtProvider.parseToken(tokenPair.accessToken())) + .isInstanceOf(UnauthorizedException.class) + .hasMessage(ErrorCode.EXPIRED_TOKEN.getMessage()); + } + + @Test + @DisplayName("토큰 key가 다른 경우 예외가 발생해야 함") + void parseClaim_differentKey() throws Exception { + //given + Clock clock = Clock.systemDefaultZone(); + JwtProvider jwtProvider = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "issuer", clock); + JwtProvider jwtProviderWithDifferentKey = new JwtProvider("1211qwerqwerqwer1111qwerqwerqwer", "issuer", clock); + + //when then + TokenPair tokenPair = jwtProviderWithDifferentKey.createToken(new JwtClaim(1L)); + assertThatThrownBy(() -> jwtProvider.parseToken(tokenPair.accessToken())) + .isInstanceOf(UnauthorizedException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + + @Test + @DisplayName("토큰 issuer가 다른 경우 예외가 발생해야 함") + void parseClaim_differentIssuer() throws Exception { + //given + Clock clock = Clock.systemDefaultZone(); + JwtProvider jwtProvider = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "issuer", clock); + JwtProvider jwtProviderWithDifferentIssuer = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "asdf", clock); + + //when then + TokenPair tokenPair = jwtProviderWithDifferentIssuer.createToken(new JwtClaim(1L)); + assertThatThrownBy(() -> jwtProvider.parseToken(tokenPair.accessToken())) + .isInstanceOf(UnauthorizedException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } +} diff --git a/src/test/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractorTest.java b/src/test/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractorTest.java new file mode 100644 index 00000000..5165e34f --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractorTest.java @@ -0,0 +1,59 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class HeaderTokenExtractorTest { + + HeaderTokenExtractor tokenExtractor; + + @BeforeEach + void setUp() { + tokenExtractor = new HeaderTokenExtractor(); + } + + @Test + @DisplayName("토큰을 추출해야 함") + void extractToken() throws Exception { + //given + String authorization = "Bearer token"; + + //when + String token = tokenExtractor.extractToken(authorization); + + //then + assertThat(token).isEqualTo("token"); + } + + @Test + @DisplayName("헤더가 null일 경우 예외가 발생해야 함") + void extractToken_null() throws Exception { + //given + String authorization = null; + + //when then + assertThatThrownBy(() -> tokenExtractor.extractToken(authorization)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage(ErrorCode.INVALID_AUTH_HEADER.getMessage()); + } + + @Test + @DisplayName("헤더 형식이 잘못되었을 경우 예외가 발생해야 함") + void extractToken_notBearer() throws Exception { + //given + String authorization = "bearer token"; + + //when then + assertThatThrownBy(() -> tokenExtractor.extractToken(authorization)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage(ErrorCode.INVALID_AUTH_HEADER.getMessage()); + } + +} From 51b21e7e608e30a26b7dae136ae3b73922d789ab Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Feb 2025 20:31:54 +0900 Subject: [PATCH 007/258] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20entity=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/auth/domain/SocialAccount.java | 41 +++++++++++++++++++ .../auth/domain/SocialAccountRepository.java | 12 ++++++ 2 files changed, 53 insertions(+) create mode 100644 src/main/java/com/swyp8team2/auth/domain/SocialAccount.java create mode 100644 src/main/java/com/swyp8team2/auth/domain/SocialAccountRepository.java diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java new file mode 100644 index 00000000..65e12665 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java @@ -0,0 +1,41 @@ +package com.swyp8team2.auth.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class SocialAccount { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + +// private String email; + + private String socialId; + + @Enumerated(EnumType.STRING) + private Provider provider; + + public SocialAccount(Long id, Long userId, String socialId, Provider provider) { + this.id = id; + this.userId = userId; +// this.email = email; + this.socialId = socialId; + this.provider = provider; + } + + public static SocialAccount create(Long userId, String socialId, Provider provider) { + return new SocialAccount(null, userId, socialId, provider); + } +} diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccountRepository.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccountRepository.java new file mode 100644 index 00000000..4de3f26d --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccountRepository.java @@ -0,0 +1,12 @@ +package com.swyp8team2.auth.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SocialAccountRepository extends JpaRepository { + + Optional findBySocialIdAndProvider(String socialId, Provider provider); +} From 18f41bf8c57b15fc606314934f3f8b5179e8be9f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 3 Feb 2025 20:32:18 +0900 Subject: [PATCH 008/258] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20security=20=EC=84=A4=EC=A0=95?= =?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 --- .../auth/application/OAuthService.java | 46 +++++++++++ .../auth/application/OAuthUserInfo.java | 28 +++++++ .../com/swyp8team2/auth/domain/OAuthUser.java | 22 ++++++ .../com/swyp8team2/auth/domain/Provider.java | 23 ++++++ .../filter/OAuthLoginFailureHandler.java | 27 +++++++ .../filter/OAuthLoginSuccessHandler.java | 41 ++++++++++ .../common/config/SecurityConfig.java | 78 +++++++++++++++++++ .../filter/CustomAccessDeniedHandler.java | 23 ++++++ 8 files changed, 288 insertions(+) create mode 100644 src/main/java/com/swyp8team2/auth/application/OAuthService.java create mode 100644 src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java create mode 100644 src/main/java/com/swyp8team2/auth/domain/OAuthUser.java create mode 100644 src/main/java/com/swyp8team2/auth/domain/Provider.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java create mode 100644 src/main/java/com/swyp8team2/common/config/SecurityConfig.java create mode 100644 src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/OAuthService.java new file mode 100644 index 00000000..0e673187 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/OAuthService.java @@ -0,0 +1,46 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.auth.domain.OAuthUser; +import com.swyp8team2.auth.domain.SocialAccount; +import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.auth.domain.Provider; +import com.swyp8team2.user.application.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class OAuthService extends DefaultOAuth2UserService { + + private final SocialAccountRepository socialAccountRepository; + private final UserService userService; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + Provider provider = Provider.of(registrationId); + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails() + .getUserInfoEndpoint() + .getUserNameAttributeName(); + Map attributes = oAuth2User.getAttributes(); + + OAuthUserInfo oAuthUserInfo = OAuthUserInfo.of(provider, attributes); + + SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider(oAuthUserInfo.getSocialId(), provider) + .orElseGet(() -> { + Long userId = userService.createUser(); + return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo.getSocialId(), provider)); + }); + + return new OAuthUser(oAuth2User.getAuthorities(), attributes, userNameAttributeName, socialAccount.getUserId()); + } +} diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java b/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java new file mode 100644 index 00000000..031c30f8 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java @@ -0,0 +1,28 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.auth.domain.Provider; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public class OAuthUserInfo { + + private final String socialId; + + public static OAuthUserInfo of(Provider provider, Map attributes) { + switch (provider) { + case KAKAO: + return ofKakao(attributes); + default: + throw new IllegalArgumentException(provider + "에 해당하는 OAuthUserInfo가 없습니다."); + } + } + + private static OAuthUserInfo ofKakao(Map attributes) { + String socialId = String.valueOf(attributes.get("id")); + return new OAuthUserInfo(socialId); + } +} diff --git a/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java b/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java new file mode 100644 index 00000000..f7f0b49c --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java @@ -0,0 +1,22 @@ +package com.swyp8team2.auth.domain; + +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +@Getter +public class OAuthUser extends DefaultOAuth2User { + + private final Long userId; + + public OAuthUser(Collection authorities, Map attributes, String nameAttributeKey, Long userId) { + super(authorities, attributes, nameAttributeKey); + this.userId = userId; + } +} diff --git a/src/main/java/com/swyp8team2/auth/domain/Provider.java b/src/main/java/com/swyp8team2/auth/domain/Provider.java new file mode 100644 index 00000000..f8ed5417 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/domain/Provider.java @@ -0,0 +1,23 @@ +package com.swyp8team2.auth.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +@Getter +@RequiredArgsConstructor +public enum Provider { + + KAKAO("kakao"); +// NAVER("naver"),; + + private final String registrationId; + + public static Provider of(String registrationId) { + return Arrays.stream(Provider.values()) + .filter(provider -> provider.registrationId.equals(registrationId)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(registrationId + "에 해당하는 SocialType이 없습니다.")); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java new file mode 100644 index 00000000..48f4e5c1 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java @@ -0,0 +1,27 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuthLoginFailureHandler implements AuthenticationFailureHandler { + + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write("OAuth login failed"); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java new file mode 100644 index 00000000..667860c0 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java @@ -0,0 +1,41 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.auth.application.JwtClaim; +import com.swyp8team2.auth.application.JwtProvider; +import com.swyp8team2.auth.application.TokenPair; +import com.swyp8team2.auth.domain.OAuthUser; +import com.swyp8team2.auth.presentation.dto.TokenResponse; +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.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Component +@RequiredArgsConstructor +public class OAuthLoginSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtProvider jwtProvider; + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + OAuthUser oAuthUser = (OAuthUser) authentication.getPrincipal(); + + TokenPair tokenPair = jwtProvider.createToken(JwtClaim.from(oAuthUser.getUserId())); + + response.setContentType(APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_OK); + + TokenResponse tokenResponse = new TokenResponse(tokenPair.accessToken(), tokenPair.refreshToken()); + response.getWriter().write(objectMapper.writeValueAsString(tokenResponse)); + response.getWriter().flush(); + } +} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java new file mode 100644 index 00000000..d7cc8d0b --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -0,0 +1,78 @@ +package com.swyp8team2.common.config; + +import com.swyp8team2.auth.application.OAuthService; +import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; +import com.swyp8team2.auth.presentation.filter.JwtExceptionFilter; +import com.swyp8team2.auth.presentation.filter.OAuthLoginFailureHandler; +import com.swyp8team2.auth.presentation.filter.OAuthLoginSuccessHandler; +import com.swyp8team2.common.filter.CustomAccessDeniedHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthFilter jwtAuthFilter; + private final JwtExceptionFilter jwtExceptionFilter; + private final OAuthService oAuthService; + private final OAuthLoginSuccessHandler oAuthLoginSuccessHandler; + private final OAuthLoginFailureHandler oAuthLoginFailureHandler; + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring() + .requestMatchers("/", "/error", "/favicon.ico"); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspect) throws Exception { + MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); + + MvcRequestMatcher[] permitWhiteList = { + mvc.pattern("/auth/sign-in"), + mvc.pattern("/auth/sign-up"), + mvc.pattern("/auth/refresh"), + }; + http + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .headers(headers -> + headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) + + .authorizeHttpRequests(authorize -> + authorize + .requestMatchers(permitWhiteList).permitAll() + .requestMatchers("/","/index.html", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**").permitAll() + .anyRequest().authenticated() + ) + + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtExceptionFilter, JwtAuthFilter.class) + .exceptionHandling(e -> e.accessDeniedHandler(new CustomAccessDeniedHandler())) + + .oauth2Login(oauth -> + oauth.userInfoEndpoint(userInfo -> userInfo.userService(oAuthService)) + .successHandler(oAuthLoginSuccessHandler) + .failureHandler(oAuthLoginFailureHandler)); + + return http.build(); + } +} diff --git a/src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java b/src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..80a7e27b --- /dev/null +++ b/src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java @@ -0,0 +1,23 @@ +package com.swyp8team2.common.filter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + log.error("Access Denied Handler", accessDeniedException); + response.getWriter().write(accessDeniedException.getMessage()); + + } +} From 71acf67881fb5ad529ac755a780143f241e3ec41 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Feb 2025 11:52:39 +0900 Subject: [PATCH 009/258] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EC=98=88=EC=99=B8=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 --- .../swyp8team2/auth/application/JwtClaim.java | 4 +-- .../auth/application/OAuthService.java | 24 +++++++++----- .../auth/application/OAuthUserInfo.java | 4 ++- .../com/swyp8team2/auth/domain/OAuthUser.java | 9 +++-- .../com/swyp8team2/auth/domain/Provider.java | 4 ++- .../filter/CustomAccessDeniedHandler.java | 33 +++++++++++++++++++ .../presentation/filter/JwtAuthFilter.java | 21 +++++++----- .../filter/JwtExceptionFilter.java | 9 +++-- .../filter/OAuthLoginFailureHandler.java | 16 +++++++-- .../filter/OAuthLoginSuccessHandler.java | 9 +++-- .../common/config/SecurityConfig.java | 30 ++++++++++++----- .../common/exception/ErrorCode.java | 8 ++++- .../exception/InternalServerException.java | 4 +++ .../filter/CustomAccessDeniedHandler.java | 23 ------------- 14 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java delete mode 100644 src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java diff --git a/src/main/java/com/swyp8team2/auth/application/JwtClaim.java b/src/main/java/com/swyp8team2/auth/application/JwtClaim.java index a52f3b37..c15d1d90 100644 --- a/src/main/java/com/swyp8team2/auth/application/JwtClaim.java +++ b/src/main/java/com/swyp8team2/auth/application/JwtClaim.java @@ -6,11 +6,11 @@ public class JwtClaim { private final String id; - public JwtClaim(Long id) { + public JwtClaim(long id) { this.id = String.valueOf(id); } - public static JwtClaim from(Long id) { + public static JwtClaim from(long id) { return new JwtClaim(id); } diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/OAuthService.java index 0e673187..acf793aa 100644 --- a/src/main/java/com/swyp8team2/auth/application/OAuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/OAuthService.java @@ -25,22 +25,30 @@ public class OAuthService extends DefaultOAuth2UserService { public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User oAuth2User = super.loadUser(userRequest); - String registrationId = userRequest.getClientRegistration().getRegistrationId(); - Provider provider = Provider.of(registrationId); + Provider provider = getProvider(userRequest); + String userNameAttributeName = userRequest.getClientRegistration() .getProviderDetails() .getUserInfoEndpoint() .getUserNameAttributeName(); - Map attributes = oAuth2User.getAttributes(); + Map attributes = oAuth2User.getAttributes(); OAuthUserInfo oAuthUserInfo = OAuthUserInfo.of(provider, attributes); - SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider(oAuthUserInfo.getSocialId(), provider) - .orElseGet(() -> { - Long userId = userService.createUser(); - return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo.getSocialId(), provider)); - }); + SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( + oAuthUserInfo.getSocialId(), provider) + .orElseGet(() -> createUser(oAuthUserInfo, provider)); return new OAuthUser(oAuth2User.getAuthorities(), attributes, userNameAttributeName, socialAccount.getUserId()); } + + private Provider getProvider(OAuth2UserRequest userRequest) { + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + return Provider.of(registrationId); + } + + private SocialAccount createUser(OAuthUserInfo oAuthUserInfo, Provider provider) { + Long userId = userService.createUser(); + return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo.getSocialId(), provider)); + } } diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java b/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java index 031c30f8..b9a60e79 100644 --- a/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java +++ b/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java @@ -1,6 +1,8 @@ package com.swyp8team2.auth.application; import com.swyp8team2.auth.domain.Provider; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -17,7 +19,7 @@ public static OAuthUserInfo of(Provider provider, Map attributes case KAKAO: return ofKakao(attributes); default: - throw new IllegalArgumentException(provider + "에 해당하는 OAuthUserInfo가 없습니다."); + throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); } } diff --git a/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java b/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java index f7f0b49c..d2af14c5 100644 --- a/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java +++ b/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java @@ -13,9 +13,14 @@ @Getter public class OAuthUser extends DefaultOAuth2User { - private final Long userId; + private final long userId; - public OAuthUser(Collection authorities, Map attributes, String nameAttributeKey, Long userId) { + public OAuthUser( + Collection authorities, + Map attributes, + String nameAttributeKey, + long userId + ) { super(authorities, attributes, nameAttributeKey); this.userId = userId; } diff --git a/src/main/java/com/swyp8team2/auth/domain/Provider.java b/src/main/java/com/swyp8team2/auth/domain/Provider.java index f8ed5417..f0451fa6 100644 --- a/src/main/java/com/swyp8team2/auth/domain/Provider.java +++ b/src/main/java/com/swyp8team2/auth/domain/Provider.java @@ -1,5 +1,7 @@ package com.swyp8team2.auth.domain; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -18,6 +20,6 @@ public static Provider of(String registrationId) { return Arrays.stream(Provider.values()) .filter(provider -> provider.registrationId.equals(registrationId)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException(registrationId + "에 해당하는 SocialType이 없습니다.")); + .orElseThrow(() -> new InternalServerException(ErrorCode.INVALID_INPUT_VALUE)); } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..a7ecf1b1 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java @@ -0,0 +1,33 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ErrorResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException, ServletException { + response.getWriter() + .write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.ACCESS_DENIED))); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index 82512246..79159b46 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -2,6 +2,8 @@ import com.swyp8team2.auth.application.JwtClaim; import com.swyp8team2.auth.application.JwtProvider; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import jakarta.servlet.FilterChain; @@ -31,24 +33,27 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final UserRepository userRepository; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); - JwtClaim claim = jwtProvider.parseToken(headerTokenExtractor.extractToken(authorization)); User user = userRepository.findById(claim.idAsLong()) - .orElseThrow(() -> new IllegalArgumentException("ErrorCode.INVALID_USER")); + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + + Authentication authentication = getAuthentication(user); + SecurityContextHolder.getContext().setAuthentication(authentication); + + doFilter(request, response, filterChain); + } + private Authentication getAuthentication(User user) { List authorities = Collections.emptyList(); UserDetails userDetails = org.springframework.security.core.userdetails.User.builder() .username(user.getNickname()) .password("") .authorities(authorities) .build(); - Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities); - - SecurityContextHolder.getContext().setAuthentication(authentication); - - doFilter(request, response, filterChain); + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java index 4d354f27..0d032587 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java @@ -23,16 +23,19 @@ public class JwtExceptionFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { try { filterChain.doFilter(request, response); } catch (UnauthorizedException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(e.getErrorCode()))); + response.getWriter() + .write(objectMapper.writeValueAsString(new ErrorResponse(e.getErrorCode()))); } catch (Exception e) { log.error("JwtExceptionFilter error", e); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR))); + response.getWriter() + .write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR))); } } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java index 48f4e5c1..80ee8c71 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java @@ -1,10 +1,13 @@ package com.swyp8team2.auth.presentation.filter; import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ErrorResponse; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -13,6 +16,7 @@ import java.io.IOException; +@Slf4j @Component @RequiredArgsConstructor public class OAuthLoginFailureHandler implements AuthenticationFailureHandler { @@ -20,8 +24,14 @@ public class OAuthLoginFailureHandler implements AuthenticationFailureHandler { private final ObjectMapper objectMapper; @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - response.getWriter().write("OAuth login failed"); + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException, ServletException { + log.error("OAuth login failed", exception); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter() + .write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.OAUTH_LOGIN_FAILED))); } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java index 667860c0..38bb6706 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java @@ -26,15 +26,18 @@ public class OAuthLoginSuccessHandler implements AuthenticationSuccessHandler { private final ObjectMapper objectMapper; @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { OAuthUser oAuthUser = (OAuthUser) authentication.getPrincipal(); TokenPair tokenPair = jwtProvider.createToken(JwtClaim.from(oAuthUser.getUserId())); + TokenResponse tokenResponse = new TokenResponse(tokenPair.accessToken(), tokenPair.refreshToken()); response.setContentType(APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_OK); - - TokenResponse tokenResponse = new TokenResponse(tokenPair.accessToken(), tokenPair.refreshToken()); response.getWriter().write(objectMapper.writeValueAsString(tokenResponse)); response.getWriter().flush(); } diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index d7cc8d0b..8f7f396a 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -5,7 +5,7 @@ import com.swyp8team2.auth.presentation.filter.JwtExceptionFilter; import com.swyp8team2.auth.presentation.filter.OAuthLoginFailureHandler; import com.swyp8team2.auth.presentation.filter.OAuthLoginSuccessHandler; -import com.swyp8team2.common.filter.CustomAccessDeniedHandler; +import com.swyp8team2.auth.presentation.filter.CustomAccessDeniedHandler; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,6 +30,7 @@ public class SecurityConfig { private final OAuthService oAuthService; private final OAuthLoginSuccessHandler oAuthLoginSuccessHandler; private final OAuthLoginFailureHandler oAuthLoginFailureHandler; + private final CustomAccessDeniedHandler customAccessDeniedHandler; @Bean public WebSecurityCustomizer webSecurityCustomizer() { @@ -39,13 +40,8 @@ public WebSecurityCustomizer webSecurityCustomizer() { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspect) throws Exception { - MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); + MvcRequestMatcher[] permitWhiteList = getWhiteList(introspect); - MvcRequestMatcher[] permitWhiteList = { - mvc.pattern("/auth/sign-in"), - mvc.pattern("/auth/sign-up"), - mvc.pattern("/auth/refresh"), - }; http .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) @@ -57,7 +53,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMapping .authorizeHttpRequests(authorize -> authorize .requestMatchers(permitWhiteList).permitAll() - .requestMatchers("/","/index.html", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**").permitAll() .anyRequest().authenticated() ) @@ -66,7 +61,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMapping .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtExceptionFilter, JwtAuthFilter.class) - .exceptionHandling(e -> e.accessDeniedHandler(new CustomAccessDeniedHandler())) + .exceptionHandling(e -> e.accessDeniedHandler(customAccessDeniedHandler)) .oauth2Login(oauth -> oauth.userInfoEndpoint(userInfo -> userInfo.userService(oAuthService)) @@ -75,4 +70,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMapping return http.build(); } + + private MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { + MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); + + return new MvcRequestMatcher[]{ + mvc.pattern("/auth/sign-in"), + mvc.pattern("/auth/sign-up"), + mvc.pattern("/auth/refresh"), + mvc.pattern("/"), + mvc.pattern("/index.html"), + mvc.pattern("/css/**"), + mvc.pattern("/images/**"), + mvc.pattern("/js/**"), + mvc.pattern("/favicon.ico"), + mvc.pattern("/h2-console/**") + }; + } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 35cf4fc1..18efa199 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -9,11 +9,17 @@ public enum ErrorCode { //common INVALID_ARGUMENT("잘못된 파라미터 요청"), INTERNAL_SERVER_ERROR("서버 내부 오류"), + INVALID_INPUT_VALUE("잘못된 입력 값"), //auth EXPIRED_TOKEN("토큰 만료"), INVALID_TOKEN("유효하지 않은 토큰"), - INVALID_AUTH_HEADER("잘못된 인증 헤더"); + INVALID_AUTH_HEADER("잘못된 인증 헤더"), + OAUTH_LOGIN_FAILED("소셜 로그인 실패"), + ACCESS_DENIED("접근 권한 없음"), + + //user + USER_NOT_FOUND("존재하지 않는 유저"), ; private final String message; } diff --git a/src/main/java/com/swyp8team2/common/exception/InternalServerException.java b/src/main/java/com/swyp8team2/common/exception/InternalServerException.java index 631645f9..bff8c30f 100644 --- a/src/main/java/com/swyp8team2/common/exception/InternalServerException.java +++ b/src/main/java/com/swyp8team2/common/exception/InternalServerException.java @@ -5,4 +5,8 @@ public class InternalServerException extends ApplicationException { public InternalServerException() { super(ErrorCode.INTERNAL_SERVER_ERROR); } + + public InternalServerException(ErrorCode errorCode) { + super(errorCode); + } } diff --git a/src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java b/src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java deleted file mode 100644 index 80a7e27b..00000000 --- a/src/main/java/com/swyp8team2/common/filter/CustomAccessDeniedHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.swyp8team2.common.filter; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Slf4j -@Component -public class CustomAccessDeniedHandler implements AccessDeniedHandler { - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { - log.error("Access Denied Handler", accessDeniedException); - response.getWriter().write(accessDeniedException.getMessage()); - - } -} From c398c8e448f1f7216857e6e4fec860b0511f4c05 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Feb 2025 11:53:00 +0900 Subject: [PATCH 010/258] =?UTF-8?q?feat:=20null=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=93=B1=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=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 --- .../auth/application/JwtProvider.java | 4 +++ .../auth/application/TokenPair.java | 11 ++++++++ .../swyp8team2/auth/domain/SocialAccount.java | 5 ++++ .../com/swyp8team2/common/util/Validator.java | 26 +++++++++++++++++++ .../java/com/swyp8team2/user/domain/User.java | 5 ++++ 5 files changed, 51 insertions(+) create mode 100644 src/main/java/com/swyp8team2/common/util/Validator.java diff --git a/src/main/java/com/swyp8team2/auth/application/JwtProvider.java b/src/main/java/com/swyp8team2/auth/application/JwtProvider.java index c61d31d7..e9dc5292 100644 --- a/src/main/java/com/swyp8team2/auth/application/JwtProvider.java +++ b/src/main/java/com/swyp8team2/auth/application/JwtProvider.java @@ -20,6 +20,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; +import java.util.Objects; @Slf4j @Component @@ -54,6 +55,9 @@ public String createRefreshToken(JwtClaim claim) { } private String createToken(JwtClaim claim, long expiration) { + if (Objects.isNull(claim)) { + throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); + } Instant now = clock.instant(); Instant expiredAt = now.plus(expiration, ChronoUnit.HOURS); diff --git a/src/main/java/com/swyp8team2/auth/application/TokenPair.java b/src/main/java/com/swyp8team2/auth/application/TokenPair.java index 22c8db34..1fb6b3b9 100644 --- a/src/main/java/com/swyp8team2/auth/application/TokenPair.java +++ b/src/main/java/com/swyp8team2/auth/application/TokenPair.java @@ -1,7 +1,18 @@ package com.swyp8team2.auth.application; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; + +import java.util.Objects; + public record TokenPair( String accessToken, String refreshToken ) { + + public TokenPair { + if (Objects.isNull(accessToken) || Objects.isNull(refreshToken)) { + throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); + } + } } diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java index 65e12665..1c2b7a99 100644 --- a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java @@ -9,6 +9,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import static com.swyp8team2.common.util.Validator.validateEmptyString; +import static com.swyp8team2.common.util.Validator.validateNull; + @Getter @Entity @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @@ -28,6 +31,8 @@ public class SocialAccount { private Provider provider; public SocialAccount(Long id, Long userId, String socialId, Provider provider) { + validateNull(userId, socialId, provider); + validateEmptyString(socialId); this.id = id; this.userId = userId; // this.email = email; diff --git a/src/main/java/com/swyp8team2/common/util/Validator.java b/src/main/java/com/swyp8team2/common/util/Validator.java new file mode 100644 index 00000000..3ab5c462 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/util/Validator.java @@ -0,0 +1,26 @@ +package com.swyp8team2.common.util; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; + +import java.util.Arrays; +import java.util.Objects; + +public class Validator { + + public static void validateNull(Object... object) { + Arrays.stream(object) + .filter(Objects::isNull) + .forEach(o -> { + throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); + }); + } + + public static void validateEmptyString(String... strings) { + Arrays.stream(strings) + .filter(String::isEmpty) + .forEach(s -> { + throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); + }); + } +} diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 25ec4e04..2958010f 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -8,6 +8,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import static com.swyp8team2.common.util.Validator.validateEmptyString; +import static com.swyp8team2.common.util.Validator.validateNull; + @Getter @Entity @Table(name = "users") @@ -21,6 +24,8 @@ public class User { private String nickname; public User(Long id, String nickname) { + validateNull(nickname); + validateEmptyString(nickname); this.id = id; this.nickname = nickname; } From 516ac48c51749161c43f4f554314a1eb8a5d4843 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 4 Feb 2025 11:53:28 +0900 Subject: [PATCH 011/258] =?UTF-8?q?test:=20null=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/JwtClaimTest.java | 23 +++++++ .../auth/application/JwtProviderTest.java | 8 +-- .../auth/domain/SocialAccountTest.java | 60 +++++++++++++++++++ .../com/swyp8team2/user/domain/UserTest.java | 50 ++++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java create mode 100644 src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java create mode 100644 src/test/java/com/swyp8team2/user/domain/UserTest.java diff --git a/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java b/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java new file mode 100644 index 00000000..524187b1 --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java @@ -0,0 +1,23 @@ +package com.swyp8team2.auth.application; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class JwtClaimTest { + + @Test + @DisplayName("JwtClaim 생성") + void idAsLong() { + // given + long givenId = 1; + + // when + JwtClaim jwtClaim = JwtClaim.from(givenId); + + // then + Assertions.assertThat(jwtClaim.idAsLong()).isEqualTo(givenId); + } +} diff --git a/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java index c51a3d32..666e24b5 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java @@ -18,7 +18,7 @@ class JwtProviderTest { @Test - @DisplayName("올바른 access token, refresh token 생성") + @DisplayName("jwt 토큰 생성") void createToken() throws Exception { //given JwtProvider jwtProvider = new JwtProvider("2345asdfasdfsadfsdf243dfdsfsfssasdf", "issuer", Clock.systemDefaultZone()); @@ -37,7 +37,7 @@ void createToken() throws Exception { } @Test - @DisplayName("토큰이 만료된 경우 예외가 발생해야 함") + @DisplayName("jwt 토큰 생성 - 만료 토큰") void parseClaim_expiredToken() throws Exception { //given Clock clock = Clock.systemDefaultZone(); @@ -54,7 +54,7 @@ void parseClaim_expiredToken() throws Exception { } @Test - @DisplayName("토큰 key가 다른 경우 예외가 발생해야 함") + @DisplayName("jwt 토큰 생성 - key가 다른 경우") void parseClaim_differentKey() throws Exception { //given Clock clock = Clock.systemDefaultZone(); @@ -69,7 +69,7 @@ void parseClaim_differentKey() throws Exception { } @Test - @DisplayName("토큰 issuer가 다른 경우 예외가 발생해야 함") + @DisplayName("jwt 토큰 생성 - issuer가 다른 경우") void parseClaim_differentIssuer() throws Exception { //given Clock clock = Clock.systemDefaultZone(); diff --git a/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java b/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java new file mode 100644 index 00000000..b4056f6c --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java @@ -0,0 +1,60 @@ +package com.swyp8team2.auth.domain; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class SocialAccountTest { + + + @Test + @DisplayName("SocialAccount Entity 생성") + void create() throws Exception { + //given + long givenUserId = 1L; + String givenSocialId = "socialId"; + Provider givenProvider = Provider.KAKAO; + + //when + SocialAccount socialAccount = SocialAccount.create(givenUserId, givenSocialId, givenProvider); + + //then + assertAll( + () -> assertThat(socialAccount.getUserId()).isEqualTo(givenUserId), + () -> assertThat(socialAccount.getSocialId()).isEqualTo(givenSocialId), + () -> assertThat(socialAccount.getProvider()).isEqualTo(givenProvider) + ); + } + + @Test + @DisplayName("SocialAccount Entity 생성 - 파라미터가 null인 경우") + void create_null() throws Exception { + //given + + //when then + assertAll( + () -> assertThatThrownBy(() -> SocialAccount.create(1L, null, Provider.KAKAO)) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), + () -> assertThatThrownBy(() -> SocialAccount.create(1L, "socialId", null)) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()) + ); + } + + @Test + @DisplayName("SocialAccount Entity 생성 - socialId가 빈 문자인 경우") + void create_emptyString() throws Exception { + //given + + //when then + assertThatThrownBy(() -> SocialAccount.create(1L, "", Provider.KAKAO)) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); + } +} diff --git a/src/test/java/com/swyp8team2/user/domain/UserTest.java b/src/test/java/com/swyp8team2/user/domain/UserTest.java new file mode 100644 index 00000000..eaaa11f1 --- /dev/null +++ b/src/test/java/com/swyp8team2/user/domain/UserTest.java @@ -0,0 +1,50 @@ +package com.swyp8team2.user.domain; + +import com.swyp8team2.auth.domain.Provider; +import com.swyp8team2.auth.domain.SocialAccount; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class UserTest { + + @Test + @DisplayName("user Entity 생성") + void create() throws Exception { + //given + String nickname = "nickname"; + + //when + User user = User.create(nickname); + + //then + assertThat(user.getNickname()).isEqualTo(nickname); + } + + @Test + @DisplayName("user Entity 생성 - 파라미터가 null인 경우") + void create_null() throws Exception { + //given + + //when then + assertThatThrownBy(() -> User.create(null)) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); + } + + @Test + @DisplayName("user Entity 생성 - nickname이 빈 문자인 경우") + void create_emptyString() throws Exception { + //given + + //when then + assertThatThrownBy(() -> User.create("")) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); + } +} From c5f62829619ca9cb3492ab84e616741c61fe6833 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 5 Feb 2025 18:06:32 +0900 Subject: [PATCH 012/258] =?UTF-8?q?fix:=20security=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B4=80=EB=A0=A8=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/CustomAccessDeniedHandler.java | 33 ------- .../filter/HeaderTokenExtractor.java | 2 - .../presentation/filter/JwtAuthFilter.java | 25 +++-- .../filter/JwtAuthenticationEntryPoint.java | 40 ++++++++ .../filter/JwtExceptionFilter.java | 41 --------- .../filter/OAuthLoginFailureHandler.java | 3 + .../common/config/SecurityConfig.java | 91 +++++++++++++------ .../ApplicationControllerAdvice.java | 38 +++++++- 8 files changed, 158 insertions(+), 115 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java delete mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java deleted file mode 100644 index a7ecf1b1..00000000 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/CustomAccessDeniedHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.swyp8team2.auth.presentation.filter; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.ErrorResponse; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -@Slf4j -@Component -@RequiredArgsConstructor -public class CustomAccessDeniedHandler implements AccessDeniedHandler { - - private final ObjectMapper objectMapper; - - @Override - public void handle( - HttpServletRequest request, - HttpServletResponse response, - AccessDeniedException accessDeniedException - ) throws IOException, ServletException { - response.getWriter() - .write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.ACCESS_DENIED))); - } -} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java b/src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java index cc4aeca8..4bd616c7 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/HeaderTokenExtractor.java @@ -2,11 +2,9 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.UnauthorizedException; -import org.springframework.stereotype.Component; import java.util.Objects; -@Component public class HeaderTokenExtractor { public static final String BEARER = "Bearer "; diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index 79159b46..40ac11c5 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.application.JwtClaim; import com.swyp8team2.auth.application.JwtProvider; +import com.swyp8team2.common.exception.ApplicationException; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.user.domain.User; @@ -11,20 +12,20 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.Collections; import java.util.List; -@Component +@Slf4j @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { @@ -35,16 +36,20 @@ public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); - JwtClaim claim = jwtProvider.parseToken(headerTokenExtractor.extractToken(authorization)); + try { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + JwtClaim claim = jwtProvider.parseToken(headerTokenExtractor.extractToken(authorization)); - User user = userRepository.findById(claim.idAsLong()) - .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + User user = userRepository.findById(claim.idAsLong()) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - Authentication authentication = getAuthentication(user); - SecurityContextHolder.getContext().setAuthentication(authentication); - - doFilter(request, response, filterChain); + Authentication authentication = getAuthentication(user); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (ApplicationException e) { + request.setAttribute("exception", e); + } finally { + doFilter(request, response, filterChain); + } } private Authentication getAuthentication(User user) { diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..f137b5fd --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java @@ -0,0 +1,40 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.common.exception.ApplicationException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ErrorResponse; +import com.swyp8team2.common.exception.UnauthorizedException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.io.IOException; +import java.util.Objects; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Slf4j +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final HandlerExceptionResolver exceptionResolver; + + public JwtAuthenticationEntryPoint( + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver exceptionResolver + ) { + this.exceptionResolver = exceptionResolver; + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + Exception e = (Exception) request.getAttribute("exception"); + + if (Objects.nonNull(e)) { + exceptionResolver.resolveException(request, response, null, e); + } + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java deleted file mode 100644 index 0d032587..00000000 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtExceptionFilter.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.swyp8team2.auth.presentation.filter; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.ErrorResponse; -import com.swyp8team2.common.exception.UnauthorizedException; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Slf4j -@Component -@RequiredArgsConstructor -public class JwtExceptionFilter extends OncePerRequestFilter { - - private final ObjectMapper objectMapper; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - try { - filterChain.doFilter(request, response); - } catch (UnauthorizedException e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter() - .write(objectMapper.writeValueAsString(new ErrorResponse(e.getErrorCode()))); - } catch (Exception e) { - log.error("JwtExceptionFilter error", e); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.getWriter() - .write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR))); - } - } -} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java index 80ee8c71..ac2ad59f 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java @@ -16,6 +16,8 @@ import java.io.IOException; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + @Slf4j @Component @RequiredArgsConstructor @@ -30,6 +32,7 @@ public void onAuthenticationFailure( AuthenticationException exception ) throws IOException, ServletException { log.error("OAuth login failed", exception); + response.setContentType(APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.getWriter() .write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.OAUTH_LOGIN_FAILED))); diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 8f7f396a..98e38cbc 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -1,14 +1,19 @@ package com.swyp8team2.common.config; +import com.swyp8team2.auth.application.JwtProvider; import com.swyp8team2.auth.application.OAuthService; +import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; -import com.swyp8team2.auth.presentation.filter.JwtExceptionFilter; +import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; import com.swyp8team2.auth.presentation.filter.OAuthLoginFailureHandler; import com.swyp8team2.auth.presentation.filter.OAuthLoginSuccessHandler; -import com.swyp8team2.auth.presentation.filter.CustomAccessDeniedHandler; -import lombok.RequiredArgsConstructor; +import com.swyp8team2.user.domain.UserRepository; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; @@ -18,50 +23,89 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @Configuration @EnableWebSecurity -@RequiredArgsConstructor public class SecurityConfig { - private final JwtAuthFilter jwtAuthFilter; - private final JwtExceptionFilter jwtExceptionFilter; private final OAuthService oAuthService; private final OAuthLoginSuccessHandler oAuthLoginSuccessHandler; private final OAuthLoginFailureHandler oAuthLoginFailureHandler; - private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final HandlerExceptionResolver handlerExceptionResolver; + + public SecurityConfig( + OAuthService oAuthService, + OAuthLoginSuccessHandler oAuthLoginSuccessHandler, + OAuthLoginFailureHandler oAuthLoginFailureHandler, + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver + ) { + this.oAuthService = oAuthService; + this.oAuthLoginSuccessHandler = oAuthLoginSuccessHandler; + this.oAuthLoginFailureHandler = oAuthLoginFailureHandler; + this.handlerExceptionResolver = handlerExceptionResolver; + } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring() - .requestMatchers("/", "/error", "/favicon.ico"); + .requestMatchers( + "/", + "/error", + "/favicon.ico", + "/index.html", + "/css/**", + "/images/**", + "/js/**", + "/favicon.ico" + ); } @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspect) throws Exception { - MvcRequestMatcher[] permitWhiteList = getWhiteList(introspect); + @Profile({"dev", "local"}) + @ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true") + public WebSecurityCustomizer configureH2ConsoleEnable() { + return web -> web.ignoring() + .requestMatchers(PathRequest.toH2Console()); + } + @Bean + public SecurityFilterChain securityFilterChain( + HttpSecurity http, + HandlerMappingIntrospector introspect, + JwtProvider jwtProvider, + UserRepository userRepository + ) throws Exception { + MvcRequestMatcher[] matchers = getWhiteList(introspect); http .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) - .headers(headers -> - headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .authorizeHttpRequests(authorize -> authorize - .requestMatchers(permitWhiteList).permitAll() + .requestMatchers( + matchers + ).permitAll() + .requestMatchers(PathRequest.toH2Console()).permitAll() .anyRequest().authenticated() ) + .headers(headers -> + headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(jwtExceptionFilter, JwtAuthFilter.class) - .exceptionHandling(e -> e.accessDeniedHandler(customAccessDeniedHandler)) + .addFilterBefore( + new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor(), userRepository), + UsernamePasswordAuthenticationFilter.class + ) + .exceptionHandling(exception -> + exception.authenticationEntryPoint( + new JwtAuthenticationEntryPoint(handlerExceptionResolver)) + ) .oauth2Login(oauth -> oauth.userInfoEndpoint(userInfo -> userInfo.userService(oAuthService)) @@ -71,20 +115,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMapping return http.build(); } - private MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { + private static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); - return new MvcRequestMatcher[]{ - mvc.pattern("/auth/sign-in"), - mvc.pattern("/auth/sign-up"), - mvc.pattern("/auth/refresh"), - mvc.pattern("/"), - mvc.pattern("/index.html"), - mvc.pattern("/css/**"), - mvc.pattern("/images/**"), - mvc.pattern("/js/**"), - mvc.pattern("/favicon.ico"), - mvc.pattern("/h2-console/**") + mvc.pattern("/auth/reissue"), + mvc.pattern("/guest") }; } } diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 5a466e1b..028c0577 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -1,16 +1,35 @@ package com.swyp8team2.common.exception; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import javax.naming.AuthenticationException; +import java.nio.file.AccessDeniedException; @Slf4j @RestControllerAdvice public class ApplicationControllerAdvice { + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handle(BadRequestException e) { + ErrorResponse response = new ErrorResponse(e.getErrorCode()); + return ResponseEntity.badRequest() + .body(response); + } + + @ExceptionHandler(UnauthorizedException.class) + public ResponseEntity handle(UnauthorizedException e) { + ErrorResponse response = new ErrorResponse(e.getErrorCode()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(response); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handle(MethodArgumentNotValidException e) { log.info("MethodArgumentNotValidException", e); @@ -23,8 +42,25 @@ public ResponseEntity handle(HttpRequestMethodNotSupportedException e) { return ResponseEntity.notFound().build(); } + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity handle(NoResourceFoundException e) { + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler + public ResponseEntity handle(AuthenticationException e) { + log.info(e.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); + } + + @ExceptionHandler + public ResponseEntity handle(AccessDeniedException e) { + log.info(e.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); + } + @ExceptionHandler(Exception.class) - public ResponseEntity handleException(Exception e) { + public ResponseEntity handle(Exception e) { log.error("Exception", e); return ResponseEntity.internalServerError() .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR)); From 85a628102c259eeb775efaab377e232944ac52b9 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 5 Feb 2025 20:01:08 +0900 Subject: [PATCH 013/258] =?UTF-8?q?refactor:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=84=98?= =?UTF-8?q?=EA=B8=B0=EB=8A=94=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/auth/domain/UserInfo.java | 31 +++++++++++++++++++ .../presentation/filter/JwtAuthFilter.java | 25 ++++----------- .../common/config/SecurityConfig.java | 6 ++-- 3 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/swyp8team2/auth/domain/UserInfo.java diff --git a/src/main/java/com/swyp8team2/auth/domain/UserInfo.java b/src/main/java/com/swyp8team2/auth/domain/UserInfo.java new file mode 100644 index 00000000..bd836070 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/domain/UserInfo.java @@ -0,0 +1,31 @@ +package com.swyp8team2.auth.domain; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +import static com.swyp8team2.common.util.Validator.validateNull; + +public record UserInfo(long userId) implements UserDetails { + + public UserInfo { + validateNull(userId); + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return ""; + } + + @Override + public String getUsername() { + return String.valueOf(userId); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index 40ac11c5..e3375f5a 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -5,8 +5,6 @@ import com.swyp8team2.common.exception.ApplicationException; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.user.domain.User; -import com.swyp8team2.user.domain.UserRepository; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -16,14 +14,12 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.Collections; -import java.util.List; + +import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; @Slf4j @RequiredArgsConstructor @@ -31,7 +27,6 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; private final HeaderTokenExtractor headerTokenExtractor; - private final UserRepository userRepository; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -40,10 +35,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); JwtClaim claim = jwtProvider.parseToken(headerTokenExtractor.extractToken(authorization)); - User user = userRepository.findById(claim.idAsLong()) - .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - - Authentication authentication = getAuthentication(user); + Authentication authentication = getAuthentication(claim.idAsLong()); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (ApplicationException e) { request.setAttribute("exception", e); @@ -52,13 +44,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } - private Authentication getAuthentication(User user) { - List authorities = Collections.emptyList(); - UserDetails userDetails = org.springframework.security.core.userdetails.User.builder() - .username(user.getNickname()) - .password("") - .authorities(authorities) - .build(); - return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + private Authentication getAuthentication(long userId) { + UserInfo userInfo = new UserInfo(userId); + return new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); } } diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 98e38cbc..fe245928 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -7,7 +7,6 @@ import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; import com.swyp8team2.auth.presentation.filter.OAuthLoginFailureHandler; import com.swyp8team2.auth.presentation.filter.OAuthLoginSuccessHandler; -import com.swyp8team2.user.domain.UserRepository; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; @@ -74,8 +73,7 @@ public WebSecurityCustomizer configureH2ConsoleEnable() { public SecurityFilterChain securityFilterChain( HttpSecurity http, HandlerMappingIntrospector introspect, - JwtProvider jwtProvider, - UserRepository userRepository + JwtProvider jwtProvider ) throws Exception { MvcRequestMatcher[] matchers = getWhiteList(introspect); http @@ -99,7 +97,7 @@ public SecurityFilterChain securityFilterChain( session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore( - new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor(), userRepository), + new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor()), UsernamePasswordAuthenticationFilter.class ) .exceptionHandling(exception -> From 629bc6207fe49e33dbe9f4962725e05ae89e4c0a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 5 Feb 2025 20:01:34 +0900 Subject: [PATCH 014/258] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/filter/JwtAuthFilter.java | 5 ++--- .../filter/JwtAuthenticationEntryPoint.java | 17 +++++++-------- .../common/config/SecurityConfig.java | 21 ++++++++----------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index e3375f5a..28b9dd50 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -2,9 +2,8 @@ import com.swyp8team2.auth.application.JwtClaim; import com.swyp8team2.auth.application.JwtProvider; +import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.exception.ApplicationException; -import com.swyp8team2.common.exception.BadRequestException; -import com.swyp8team2.common.exception.ErrorCode; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -38,7 +37,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse Authentication authentication = getAuthentication(claim.idAsLong()); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (ApplicationException e) { - request.setAttribute("exception", e); + request.setAttribute(EXCEPTION_KEY, e); } finally { doFilter(request, response, filterChain); } diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java index f137b5fd..e39ca988 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthenticationEntryPoint.java @@ -1,10 +1,5 @@ package com.swyp8team2.auth.presentation.filter; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.swyp8team2.common.exception.ApplicationException; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.ErrorResponse; -import com.swyp8team2.common.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -16,11 +11,11 @@ import java.io.IOException; import java.util.Objects; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - @Slf4j public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + public static final String EXCEPTION_KEY = "exception"; + private final HandlerExceptionResolver exceptionResolver; public JwtAuthenticationEntryPoint( @@ -30,8 +25,12 @@ public JwtAuthenticationEntryPoint( } @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - Exception e = (Exception) request.getAttribute("exception"); + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + Exception e = (Exception) request.getAttribute(EXCEPTION_KEY); if (Objects.nonNull(e)) { exceptionResolver.resolveException(request, response, null, e); diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index fe245928..dac72703 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -75,26 +75,22 @@ public SecurityFilterChain securityFilterChain( HandlerMappingIntrospector introspect, JwtProvider jwtProvider ) throws Exception { - MvcRequestMatcher[] matchers = getWhiteList(introspect); http .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) + .headers(headers -> + headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize - .requestMatchers( - matchers - ).permitAll() - .requestMatchers(PathRequest.toH2Console()).permitAll() + .requestMatchers(getWhiteList(introspect)).permitAll() +// .requestMatchers(PathRequest.toH2Console()).permitAll() .anyRequest().authenticated() ) - .headers(headers -> - headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) - - .sessionManagement(session -> - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore( new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor()), @@ -108,12 +104,13 @@ public SecurityFilterChain securityFilterChain( .oauth2Login(oauth -> oauth.userInfoEndpoint(userInfo -> userInfo.userService(oAuthService)) .successHandler(oAuthLoginSuccessHandler) - .failureHandler(oAuthLoginFailureHandler)); + .failureHandler(oAuthLoginFailureHandler) + ); return http.build(); } - private static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { + private MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), From 019fbb7e319e2478a5216e34701642c2b27376d1 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 5 Feb 2025 21:01:59 +0900 Subject: [PATCH 015/258] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC=20=EB=B0=A9=EC=8B=9D=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 --- .../com/swyp8team2/common/exception/ErrorCode.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 18efa199..71874a69 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -6,20 +6,19 @@ @Getter @RequiredArgsConstructor public enum ErrorCode { - //common + //400 + USER_NOT_FOUND("존재하지 않는 유저"), INVALID_ARGUMENT("잘못된 파라미터 요청"), - INTERNAL_SERVER_ERROR("서버 내부 오류"), - INVALID_INPUT_VALUE("잘못된 입력 값"), - //auth + //401 EXPIRED_TOKEN("토큰 만료"), INVALID_TOKEN("유효하지 않은 토큰"), INVALID_AUTH_HEADER("잘못된 인증 헤더"), OAUTH_LOGIN_FAILED("소셜 로그인 실패"), - ACCESS_DENIED("접근 권한 없음"), - //user - USER_NOT_FOUND("존재하지 않는 유저"), ; + //500 + INTERNAL_SERVER_ERROR("서버 내부 오류"), + INVALID_INPUT_VALUE("잘못된 입력 값"), ; private final String message; } From 69932a9c0947f52171acb9e8c54dd532c1edadad Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 5 Feb 2025 22:51:42 +0900 Subject: [PATCH 016/258] =?UTF-8?q?feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/auth/application/OAuthService.java | 7 ++++--- .../swyp8team2/auth/application/OAuthUserInfo.java | 12 ++++-------- .../com/swyp8team2/auth/domain/SocialAccount.java | 14 +++++++------- .../exception/ApplicationControllerAdvice.java | 5 ++--- .../swyp8team2/user/application/UserService.java | 4 ++-- src/main/java/com/swyp8team2/user/domain/User.java | 13 ++++++++----- .../java/com/swyp8team2/user/domain/UserTest.java | 9 +++------ 7 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/OAuthService.java index acf793aa..39918466 100644 --- a/src/main/java/com/swyp8team2/auth/application/OAuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/OAuthService.java @@ -36,7 +36,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic OAuthUserInfo oAuthUserInfo = OAuthUserInfo.of(provider, attributes); SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( - oAuthUserInfo.getSocialId(), provider) + oAuthUserInfo.socialId(), provider) .orElseGet(() -> createUser(oAuthUserInfo, provider)); return new OAuthUser(oAuth2User.getAuthorities(), attributes, userNameAttributeName, socialAccount.getUserId()); @@ -48,7 +48,8 @@ private Provider getProvider(OAuth2UserRequest userRequest) { } private SocialAccount createUser(OAuthUserInfo oAuthUserInfo, Provider provider) { - Long userId = userService.createUser(); - return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo.getSocialId(), provider)); + String email = oAuthUserInfo.email(); + Long userId = userService.createUser(email); + return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo.socialId(), provider, email)); } } diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java b/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java index b9a60e79..ec9abed8 100644 --- a/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java +++ b/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java @@ -3,16 +3,10 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; -import lombok.Getter; -import lombok.RequiredArgsConstructor; import java.util.Map; -@Getter -@RequiredArgsConstructor -public class OAuthUserInfo { - - private final String socialId; +public record OAuthUserInfo(String socialId, String email) { public static OAuthUserInfo of(Provider provider, Map attributes) { switch (provider) { @@ -25,6 +19,8 @@ public static OAuthUserInfo of(Provider provider, Map attributes private static OAuthUserInfo ofKakao(Map attributes) { String socialId = String.valueOf(attributes.get("id")); - return new OAuthUserInfo(socialId); + Map kakaoAccount = (Map) attributes.get("kakao_account"); + String email = String.valueOf(kakaoAccount.get("email")); + return new OAuthUserInfo(socialId, email); } } diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java index 1c2b7a99..b58aafb2 100644 --- a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java @@ -23,24 +23,24 @@ public class SocialAccount { private Long userId; -// private String email; + private String email; private String socialId; @Enumerated(EnumType.STRING) private Provider provider; - public SocialAccount(Long id, Long userId, String socialId, Provider provider) { - validateNull(userId, socialId, provider); - validateEmptyString(socialId); + public SocialAccount(Long id, Long userId, String socialId, Provider provider, String email) { + validateNull(userId, socialId, provider, email); + validateEmptyString(socialId, email); this.id = id; this.userId = userId; -// this.email = email; + this.email = email; this.socialId = socialId; this.provider = provider; } - public static SocialAccount create(Long userId, String socialId, Provider provider) { - return new SocialAccount(null, userId, socialId, provider); + public static SocialAccount create(Long userId, String socialId, Provider provider, String email) { + return new SocialAccount(null, userId, socialId, provider, email); } } diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 028c0577..986a4f39 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -32,7 +32,6 @@ public ResponseEntity handle(UnauthorizedException e) { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handle(MethodArgumentNotValidException e) { - log.info("MethodArgumentNotValidException", e); return ResponseEntity.badRequest() .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); } @@ -47,13 +46,13 @@ public ResponseEntity handle(NoResourceFoundException e) { return ResponseEntity.notFound().build(); } - @ExceptionHandler + @ExceptionHandler(AuthenticationException.class) public ResponseEntity handle(AuthenticationException e) { log.info(e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); } - @ExceptionHandler + @ExceptionHandler(AccessDeniedException.class) public ResponseEntity handle(AccessDeniedException e) { log.info(e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index ea809311..3b6b184e 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -13,8 +13,8 @@ public class UserService { private final UserRepository userRepository; @Transactional - public Long createUser() { - User user = userRepository.save(User.create("user_" + System.currentTimeMillis())); + public Long createUser(String email) { + User user = userRepository.save(User.create("user_" + System.currentTimeMillis(), email)); return user.getId(); } } diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 2958010f..18099677 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -23,14 +23,17 @@ public class User { private String nickname; - public User(Long id, String nickname) { - validateNull(nickname); - validateEmptyString(nickname); + private String email; + + public User(Long id, String nickname, String email) { + validateNull(nickname, email); + validateEmptyString(nickname, email); this.id = id; this.nickname = nickname; + this.email = email; } - public static User create(String nickname) { - return new User(null, nickname); + public static User create(String nickname, String email) { + return new User(null, nickname, email); } } diff --git a/src/test/java/com/swyp8team2/user/domain/UserTest.java b/src/test/java/com/swyp8team2/user/domain/UserTest.java index eaaa11f1..1c0edca0 100644 --- a/src/test/java/com/swyp8team2/user/domain/UserTest.java +++ b/src/test/java/com/swyp8team2/user/domain/UserTest.java @@ -1,7 +1,5 @@ package com.swyp8team2.user.domain; -import com.swyp8team2.auth.domain.Provider; -import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; import org.junit.jupiter.api.DisplayName; @@ -9,7 +7,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; class UserTest { @@ -20,7 +17,7 @@ void create() throws Exception { String nickname = "nickname"; //when - User user = User.create(nickname); + User user = User.create(nickname, "email"); //then assertThat(user.getNickname()).isEqualTo(nickname); @@ -32,7 +29,7 @@ void create_null() throws Exception { //given //when then - assertThatThrownBy(() -> User.create(null)) + assertThatThrownBy(() -> User.create(null, "email")) .isInstanceOf(InternalServerException.class) .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); } @@ -43,7 +40,7 @@ void create_emptyString() throws Exception { //given //when then - assertThatThrownBy(() -> User.create("")) + assertThatThrownBy(() -> User.create("", "email")) .isInstanceOf(InternalServerException.class) .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); } From a4b8c497fc35eea13405fd04c54c0abeb7183b35 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 6 Feb 2025 09:52:30 +0900 Subject: [PATCH 017/258] =?UTF-8?q?fix:=20jwt=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=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 --- .../com/swyp8team2/auth/application/JwtProvider.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/JwtProvider.java b/src/main/java/com/swyp8team2/auth/application/JwtProvider.java index e9dc5292..ad6a5a39 100644 --- a/src/main/java/com/swyp8team2/auth/application/JwtProvider.java +++ b/src/main/java/com/swyp8team2/auth/application/JwtProvider.java @@ -26,8 +26,8 @@ @Component public class JwtProvider { - private static final long ACCESS_TOKEN_EXPIRATION_HOUR = 2; - private static final long REFRESH_TOKEN_EXPIRATION_HOUR = 24 * 14; + private static final long ACCESS_TOKEN_EXPIRATION_MINUTES = 30; + private static final long REFRESH_TOKEN_EXPIRATION_HOUR_MINUTES = 60 * 24 * 14; private final Key key; private final Clock clock; @@ -47,11 +47,11 @@ public TokenPair createToken(JwtClaim claim) { } public String createAccessToken(JwtClaim claim) { - return createToken(claim, ACCESS_TOKEN_EXPIRATION_HOUR); + return createToken(claim, ACCESS_TOKEN_EXPIRATION_MINUTES); } public String createRefreshToken(JwtClaim claim) { - return createToken(claim, REFRESH_TOKEN_EXPIRATION_HOUR); + return createToken(claim, REFRESH_TOKEN_EXPIRATION_HOUR_MINUTES); } private String createToken(JwtClaim claim, long expiration) { @@ -59,7 +59,7 @@ private String createToken(JwtClaim claim, long expiration) { throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); } Instant now = clock.instant(); - Instant expiredAt = now.plus(expiration, ChronoUnit.HOURS); + Instant expiredAt = now.plus(expiration, ChronoUnit.MINUTES); return Jwts.builder() .claim(JwtClaim.ID, claim.id()) @@ -88,7 +88,7 @@ public JwtClaim parseToken(String token) { log.trace("Invalid Jwt Token: {}", e.getMessage()); throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); } catch (Exception e) { - throw new InternalServerException(); + throw new InternalServerException(e); } } } From 358a85216a9417e57423051e9f714da41fadad43 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 6 Feb 2025 09:52:51 +0900 Subject: [PATCH 018/258] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/common/exception/ApplicationException.java | 5 +++++ .../common/exception/InternalServerException.java | 4 ++++ .../com/swyp8team2/auth/domain/SocialAccountTest.java | 8 ++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationException.java b/src/main/java/com/swyp8team2/common/exception/ApplicationException.java index e17c4712..bad6539c 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationException.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationException.java @@ -7,6 +7,11 @@ public class ApplicationException extends RuntimeException { private final ErrorCode errorCode; + public ApplicationException(Throwable cause, ErrorCode errorCode) { + super(cause); + this.errorCode = errorCode; + } + public ApplicationException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; diff --git a/src/main/java/com/swyp8team2/common/exception/InternalServerException.java b/src/main/java/com/swyp8team2/common/exception/InternalServerException.java index bff8c30f..b29c8626 100644 --- a/src/main/java/com/swyp8team2/common/exception/InternalServerException.java +++ b/src/main/java/com/swyp8team2/common/exception/InternalServerException.java @@ -6,6 +6,10 @@ public InternalServerException() { super(ErrorCode.INTERNAL_SERVER_ERROR); } + public InternalServerException(Exception e) { + super(e, ErrorCode.INTERNAL_SERVER_ERROR); + } + public InternalServerException(ErrorCode errorCode) { super(errorCode); } diff --git a/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java b/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java index b4056f6c..da0072eb 100644 --- a/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java +++ b/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java @@ -21,7 +21,7 @@ void create() throws Exception { Provider givenProvider = Provider.KAKAO; //when - SocialAccount socialAccount = SocialAccount.create(givenUserId, givenSocialId, givenProvider); + SocialAccount socialAccount = SocialAccount.create(givenUserId, givenSocialId, givenProvider, "email"); //then assertAll( @@ -38,10 +38,10 @@ void create_null() throws Exception { //when then assertAll( - () -> assertThatThrownBy(() -> SocialAccount.create(1L, null, Provider.KAKAO)) + () -> assertThatThrownBy(() -> SocialAccount.create(1L, null, Provider.KAKAO, "email")) .isInstanceOf(InternalServerException.class) .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), - () -> assertThatThrownBy(() -> SocialAccount.create(1L, "socialId", null)) + () -> assertThatThrownBy(() -> SocialAccount.create(1L, "socialId", null, "email")) .isInstanceOf(InternalServerException.class) .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()) ); @@ -53,7 +53,7 @@ void create_emptyString() throws Exception { //given //when then - assertThatThrownBy(() -> SocialAccount.create(1L, "", Provider.KAKAO)) + assertThatThrownBy(() -> SocialAccount.create(1L, "", Provider.KAKAO, "email")) .isInstanceOf(InternalServerException.class) .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); } From 13cf1f1c7943ea2ce9ecfe2cd4edf1750d13f1ca Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Feb 2025 19:15:50 +0900 Subject: [PATCH 019/258] =?UTF-8?q?chore:=20=ED=8F=AC=ED=8A=B8=203000=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e69de29b..4f149e37 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -0,0 +1,2 @@ +server: + port: 3000 From 1987875bb95c78d533a2bcffc726565386d9f86c Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Feb 2025 19:18:03 +0900 Subject: [PATCH 020/258] =?UTF-8?q?chore:=20CI=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=ED=99=94=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - develop, main에 PR 요청 시 실행 - 빌드/테스트 이후 리포트 생성 --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3fb53752 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: ci + +on: + pull_request: + branches: ['develop', 'main'] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Cache Gradle + id: cache-gradle + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build with Gradle + run: ./gradlew build + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: always() + with: + report_paths: "**/build/test-results/test/TEST-*.xml" From 6465fabf37994539c1b7db394afdd99da54d2c7a Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 6 Feb 2025 19:22:05 +0900 Subject: [PATCH 021/258] =?UTF-8?q?chore:=20CD(develop)=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=ED=99=94=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - develop 브랜치 merge 이후 실행 - 빌드 파일을 photopic-dev.jar로 생성 후 SCP로 파일 전달 - EC2에 접속하여 새로운 배포 파일로 서버 재부팅 --- .github/workflows/cd-dev.yml | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/cd-dev.yml diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml new file mode 100644 index 00000000..7b397c80 --- /dev/null +++ b/.github/workflows/cd-dev.yml @@ -0,0 +1,61 @@ +name: cd dev + +on: + pull_request: + branches: [ "develop" ] + types: [closed] + +jobs: + deploy: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Cache Gradle + id: cache-gradle + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build with Gradle + run: ./gradlew bootJar + + - name: Copy jar file + run: mv ./build/libs/*SNAPSHOT.jar ./photopic-dev.jar + + - name: (SCP) Transfer build file + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.AWS_EC2_URL_DEV }} + username: ${{ secrets.AWS_EC2_USER }} + key: ${{ secrets.AWS_EC2_KEY }} + source: photopic-dev.jar + target: /home/${{ secrets.AWS_EC2_USER }} + + - name: (SSH) Connect EC2 and rerun jar + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.AWS_EC2_URL_DEV }} + username: ${{ secrets.AWS_EC2_USER }} + key: ${{ secrets.AWS_EC2_KEY }} + script_stop: true + script: | + sudo fuser -k -n tcp 3000 || true + nohup java -jar photopic-dev.jar > ./output.log 2>&1 & From a36c0323096d16fe9a24b1e5ff5c48dd3348e677 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 7 Feb 2025 01:22:23 +0900 Subject: [PATCH 022/258] =?UTF-8?q?=08chore:=20=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - workflow에서 초기화하기 위해 기존 내용 삭제 --- src/main/resources/application.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4f149e37..d3f5a12f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,2 +1 @@ -server: - port: 3000 + From eb6bfe2b00108a9131534de89549e17496ed7996 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 7 Feb 2025 04:25:22 +0900 Subject: [PATCH 023/258] =?UTF-8?q?chore:=20copyYML=20=ED=83=9C=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - submodule에 등록된 application.yml 복사 --- build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle b/build.gradle index 5bf64ca9..c94f98bf 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,14 @@ repositories { mavenCentral() } +task copyYML(type: Copy){ + copy { + from './server-config' + include "*.yml" + into './src/main/resources' + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' From f661cd89ff3ef1f84074a9d21b36be09ad2b9d07 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 7 Feb 2025 04:27:11 +0900 Subject: [PATCH 024/258] =?UTF-8?q?chore:=20CI=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - submodule 사용하기 위해 토큰 적용 및 세팅 - copyYML로 설정값 복사 후 test만 진행 --- .github/workflows/ci.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fb53752..8d24f104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: ci on: pull_request: - branches: ['develop', 'main'] + branches: [ "develop" , "main" ] jobs: build: @@ -10,6 +10,9 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + with: + token: ${{ secrets.SUBMODULE_TOKEN }} + submodules: true - name: Setup JDK 21 uses: actions/setup-java@v4 @@ -29,8 +32,11 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Build with Gradle - run: ./gradlew build + - name: Set application.yml + run: ./gradlew copyYML + + - name: Test with Gradle + run: ./gradlew test - name: Publish Test Report uses: mikepenz/action-junit-report@v5 From 6b694b21186ad94f89aa9720ea0e2e4f1e625be5 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 7 Feb 2025 04:28:11 +0900 Subject: [PATCH 025/258] =?UTF-8?q?=08chore:=20CD=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - submodule 사용하기 위한 토큰 적용 및 세팅 - copyYML 이후 build 진행 --- .github/workflows/cd-dev.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 7b397c80..393b68e6 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -2,7 +2,7 @@ name: cd dev on: pull_request: - branches: [ "develop" ] + branches: [ "develop", "main" ] types: [closed] jobs: @@ -15,6 +15,9 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + with: + token: ${{ secrets.SUBMODULE_TOKEN }} + submodules: true - name: Setup JDK 21 uses: actions/setup-java@v4 @@ -33,14 +36,17 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: | ${{ runner.os }}-gradle- - + + - name: Set application.yml + run: ./gradlew copyYML + - name: Build with Gradle - run: ./gradlew bootJar + run: ./gradlew build - name: Copy jar file run: mv ./build/libs/*SNAPSHOT.jar ./photopic-dev.jar - - name: (SCP) Transfer build file + - name: (SCP) transfer build file uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.AWS_EC2_URL_DEV }} @@ -49,7 +55,7 @@ jobs: source: photopic-dev.jar target: /home/${{ secrets.AWS_EC2_USER }} - - name: (SSH) Connect EC2 and rerun jar + - name: (SSH) connect EC2 uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.AWS_EC2_URL_DEV }} From f6dbaf7584893240dca2bc2f6d5b933af01a9a4f Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 7 Feb 2025 04:30:38 +0900 Subject: [PATCH 026/258] =?UTF-8?q?=08chore:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=8B=9C=20dev=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 393b68e6..be5590ff 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -64,4 +64,4 @@ jobs: script_stop: true script: | sudo fuser -k -n tcp 3000 || true - nohup java -jar photopic-dev.jar > ./output.log 2>&1 & + nohup java -jar -Dspring.profiles.active=dev photopic-dev.jar > ./output.log 2>&1 & From b51b78d31ddb6199478b5759406b260b20657e0a Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 7 Feb 2025 04:41:26 +0900 Subject: [PATCH 027/258] =?UTF-8?q?chore:=20submodule=20=EC=84=B8=ED=8C=85?= =?UTF-8?q?=20=EB=B0=8F=20gitignore=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++++ .gitmodules | 3 +++ server-config | 1 + 3 files changed, 8 insertions(+) create mode 100644 .gitmodules create mode 160000 server-config diff --git a/.gitignore b/.gitignore index c2065bc2..653e7c70 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ out/ ### VS Code ### .vscode/ + +.DS_Store + +application*.yml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..12262f4c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "server-config"] + path = server-config + url = https://github.com/SWYP-team-2th/server-config.git diff --git a/server-config b/server-config new file mode 160000 index 00000000..d3016420 --- /dev/null +++ b/server-config @@ -0,0 +1 @@ +Subproject commit d3016420e08d9b1e530db5534e85712cec4236c4 From c201137e42868e19373231eefa7b6695471880fd Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 7 Feb 2025 18:01:05 +0900 Subject: [PATCH 028/258] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=BD=94=EB=93=9C=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/com/swyp8team2/auth/domain/Provider.java | 1 - src/main/java/com/swyp8team2/common/config/SecurityConfig.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/domain/Provider.java b/src/main/java/com/swyp8team2/auth/domain/Provider.java index f0451fa6..6469c1c4 100644 --- a/src/main/java/com/swyp8team2/auth/domain/Provider.java +++ b/src/main/java/com/swyp8team2/auth/domain/Provider.java @@ -12,7 +12,6 @@ public enum Provider { KAKAO("kakao"); -// NAVER("naver"),; private final String registrationId; diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index dac72703..f0bd430f 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -52,7 +52,6 @@ public WebSecurityCustomizer webSecurityCustomizer() { .requestMatchers( "/", "/error", - "/favicon.ico", "/index.html", "/css/**", "/images/**", From 448fa93e6a976aa01f3fc16d8a062df1751e2f53 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sun, 9 Feb 2025 02:45:21 +0900 Subject: [PATCH 029/258] =?UTF-8?q?chore:=20CI/CD=20dev=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20copy=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 8 ++++---- .github/workflows/ci.yml | 4 ++-- build.gradle | 8 -------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index be5590ff..7134e3fd 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -2,7 +2,7 @@ name: cd dev on: pull_request: - branches: [ "develop", "main" ] + branches: [ "develop" ] types: [closed] jobs: @@ -37,8 +37,8 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Set application.yml - run: ./gradlew copyYML + - name: Copy application.yml + run: cp ./server-config/*.yml ./src/main/resources/ - name: Build with Gradle run: ./gradlew build @@ -63,5 +63,5 @@ jobs: key: ${{ secrets.AWS_EC2_KEY }} script_stop: true script: | - sudo fuser -k -n tcp 3000 || true + sudo fuser -k -n tcp 8080 || true nohup java -jar -Dspring.profiles.active=dev photopic-dev.jar > ./output.log 2>&1 & diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d24f104..3f3829f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,8 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Set application.yml - run: ./gradlew copyYML + - name: Copy application.yml + run: cp ./server-config/*.yml ./src/main/resources/ - name: Test with Gradle run: ./gradlew test diff --git a/build.gradle b/build.gradle index 772c7494..49fe15b7 100644 --- a/build.gradle +++ b/build.gradle @@ -23,14 +23,6 @@ repositories { mavenCentral() } -task copyYML(type: Copy){ - copy { - from './server-config' - include "*.yml" - into './src/main/resources' - } -} - dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' From 758f0cced8aacab4a88ec254115f78bd5f965895 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Feb 2025 15:20:51 +0900 Subject: [PATCH 030/258] =?UTF-8?q?chore:=20CI/CD=20dev=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20application-test.yml=20c?= =?UTF-8?q?opy=20=EB=AA=85=EB=A0=B9=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f3829f5..96e893c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: ${{ runner.os }}-gradle- - name: Copy application.yml - run: cp ./server-config/*.yml ./src/main/resources/ + run: cp ./server-config/application-test.yml ./src/test/resources/application.yml - name: Test with Gradle run: ./gradlew test From 95a36ef23cb6ff509e4f10dc37a55aeaab2d8639 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 10 Feb 2025 12:21:30 +0900 Subject: [PATCH 031/258] =?UTF-8?q?chore:=20rest=20docs=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=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 --- build.gradle | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/build.gradle b/build.gradle index 772c7494..b0671c14 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.2' id 'io.spring.dependency-management' version '1.1.7' + id 'org.asciidoctor.jvm.convert' version '3.3.2' } group = 'com.swyp8team2' @@ -17,6 +18,7 @@ configurations { compileOnly { extendsFrom annotationProcessor } + asciidoctorExt } repositories { @@ -46,10 +48,42 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } +ext { + snippetsDir = file('build/generated-snippets') +} + +asciidoctor { + inputs.dir snippetsDir + configurations 'asciidoctorExt' + baseDirFollowsSourceFile() + dependsOn test +} + +task copyDocument(type: Copy) { + dependsOn asciidoctor + doFirst { + delete file('src/main/resources/static/docs') + } + from file("build/docs/asciidoc") + into file("src/main/resources/static/docs") +} + +bootJar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}") { + into 'static/docs' + } +} + tasks.named('test') { + outputs.dir snippetsDir useJUnitPlatform() } From 291693164c9a1ed155fa867ae1e03d5eb71b4e19 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 10 Feb 2025 12:22:12 +0900 Subject: [PATCH 032/258] =?UTF-8?q?docs:=20rest=20docs=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/support/RestDocsTest.java | 29 +++++++++++++++ .../support/config/RestDocsConfiguration.java | 37 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/test/java/com/swyp8team2/support/RestDocsTest.java create mode 100644 src/test/java/com/swyp8team2/support/config/RestDocsConfiguration.java diff --git a/src/test/java/com/swyp8team2/support/RestDocsTest.java b/src/test/java/com/swyp8team2/support/RestDocsTest.java new file mode 100644 index 00000000..8532cb31 --- /dev/null +++ b/src/test/java/com/swyp8team2/support/RestDocsTest.java @@ -0,0 +1,29 @@ +package com.swyp8team2.support; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.support.config.RestDocsConfiguration; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest +@AutoConfigureRestDocs +@Import(RestDocsConfiguration.class) +@ExtendWith(RestDocumentationExtension.class) +public abstract class RestDocsTest { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + protected RestDocumentationResultHandler restDocs; + +} diff --git a/src/test/java/com/swyp8team2/support/config/RestDocsConfiguration.java b/src/test/java/com/swyp8team2/support/config/RestDocsConfiguration.java new file mode 100644 index 00000000..8848b5a8 --- /dev/null +++ b/src/test/java/com/swyp8team2/support/config/RestDocsConfiguration.java @@ -0,0 +1,37 @@ +package com.swyp8team2.support.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@TestConfiguration +public class RestDocsConfiguration { + + @Bean + public RestDocumentationResultHandler restDocumentationResultHandler() { + return MockMvcRestDocumentation.document( + "{class-name}/{method-name}", + preprocessRequest( + modifyHeaders() + .remove("Content-Length") + .remove("Host"), + prettyPrint()), + preprocessResponse( + modifyHeaders() + .remove("Content-Length") + .remove("X-Content-Type-Options") + .remove("X-XSS-Protection") + .remove("Cache-Control") + .remove("Pragma") + .remove("Expires") + .remove("X-Frame-Options"), + prettyPrint()) + ); + } +} From 6a093b122d60b16af122488a70e816d8a15c76f2 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 10 Feb 2025 15:27:50 +0900 Subject: [PATCH 033/258] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20mock=20api=20=EB=B0=8F=20docs?= =?UTF-8?q?=20=EC=9A=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?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 --- .../user/presentation/UserController.java | 19 +++++++++ .../presentation/dto/UserInfoResponse.java | 9 ++++ .../user/presentation/UserControllerTest.java | 42 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/main/java/com/swyp8team2/user/presentation/UserController.java create mode 100644 src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java create mode 100644 src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java diff --git a/src/main/java/com/swyp8team2/user/presentation/UserController.java b/src/main/java/com/swyp8team2/user/presentation/UserController.java new file mode 100644 index 00000000..a86483ed --- /dev/null +++ b/src/main/java/com/swyp8team2/user/presentation/UserController.java @@ -0,0 +1,19 @@ +package com.swyp8team2.user.presentation; + +import com.swyp8team2.user.presentation.dto.UserInfoResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/users") +public class UserController { + + @GetMapping("/me") + public ResponseEntity findUserInfo() { + return ResponseEntity.ok(new UserInfoResponse(1L, "nickname", "profileUrl", "email@email.email")); + } +} diff --git a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java new file mode 100644 index 00000000..99603f43 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java @@ -0,0 +1,9 @@ +package com.swyp8team2.user.presentation.dto; + +public record UserInfoResponse( + Long userId, + String nickname, + String profileUrl, + String email +) { +} diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java new file mode 100644 index 00000000..85c99ff9 --- /dev/null +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -0,0 +1,42 @@ +package com.swyp8team2.user.presentation; + +import com.swyp8team2.support.RestDocsTest; +import com.swyp8team2.user.presentation.dto.UserInfoResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.security.test.context.support.WithMockUser; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class UserControllerTest extends RestDocsTest { + + @Test + @WithMockUser + @DisplayName("유저 정보 조회") + void findUserInfo() throws Exception { + //given + UserInfoResponse response = new UserInfoResponse(1L, "nickname", "profileUrl", "email@email.email"); + + //when then + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + responseFields( + fieldWithPath("userId").description("유저 아이디").type(NUMBER), + fieldWithPath("nickname").description("닉네임").type(STRING), + fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING), + fieldWithPath("email").description("이메일").type(STRING) + ) + )); + } +} From b5b12dbdbb2b21b72c4993ede922a073cd5d116e Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 10 Feb 2025 15:28:04 +0900 Subject: [PATCH 034/258] =?UTF-8?q?docs:=20user=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/index.adoc | 10 ++++++++++ src/docs/asciidoc/users.adoc | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 src/docs/asciidoc/index.adoc create mode 100644 src/docs/asciidoc/users.adoc diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 00000000..188a6fb2 --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,10 @@ += API Docs +api문서 +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + +include::users.adoc[] diff --git a/src/docs/asciidoc/users.adoc b/src/docs/asciidoc/users.adoc new file mode 100644 index 00000000..45f8421c --- /dev/null +++ b/src/docs/asciidoc/users.adoc @@ -0,0 +1,7 @@ +[[유저-API]] +== 유저 API + +[[유저-정보-조회]] +=== 유저 정보 조회 + +operation::user-controller-test/find-user-info[snippets='http-request,curl-request,http-response,response-fields'] \ No newline at end of file From 852ab66e85ad70913fdb9dfeeacc476c1fe35b22 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 10 Feb 2025 15:29:18 +0900 Subject: [PATCH 035/258] =?UTF-8?q?feat:=20docs=20security=20ignore=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/com/swyp8team2/common/config/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index f0bd430f..dc7d98e2 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -56,7 +56,8 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/css/**", "/images/**", "/js/**", - "/favicon.ico" + "/favicon.ico", + "/docs/**" ); } From a59744d35ad06eda14a7f268f96d110b6977348a Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Feb 2025 15:29:23 +0900 Subject: [PATCH 036/258] =?UTF-8?q?chore:=20CI/CD=20dev=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96e893c2..e73f96b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,10 @@ on: pull_request: branches: [ "develop" , "main" ] +permissions: + contents: read + checks: write + jobs: build: runs-on: ubuntu-latest From ca6fbe56bb51e4777dfb6dc84fc612ce0229ebb9 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Feb 2025 15:45:11 +0900 Subject: [PATCH 037/258] =?UTF-8?q?chore:=20CI/CD=20dev=20workflow=20mkdir?= =?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 --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e73f96b5..d86d097c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- + - name: Create directory resources + run: mkdir -p ./src/test/resources + - name: Copy application.yml run: cp ./server-config/application-test.yml ./src/test/resources/application.yml From 93a22e9c0ccfc3cc81884e77f81e8b3bc6f09155 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Feb 2025 15:58:10 +0900 Subject: [PATCH 038/258] chore: server-config update --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index d3016420..c9e13d0b 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit d3016420e08d9b1e530db5534e85712cec4236c4 +Subproject commit c9e13d0b2d47d9b2014ee13590718d4e5134f4af From c3b141352400a58d51a11368f26c8f3e6fbb5e00 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Feb 2025 16:38:51 +0900 Subject: [PATCH 039/258] chore: server-config update --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index c9e13d0b..0680a2bd 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit c9e13d0b2d47d9b2014ee13590718d4e5134f4af +Subproject commit 0680a2bd13726ef1d76af6b8b4aeab11a13f8d25 From 992a4d94d383631a0d607e3e64e0b069ac2413da Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Feb 2025 16:50:50 +0900 Subject: [PATCH 040/258] =?UTF-8?q?chore:=20CD=20workflow=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EB=AA=85=EB=A0=B9=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 7134e3fd..bc4192cd 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -41,7 +41,7 @@ jobs: run: cp ./server-config/*.yml ./src/main/resources/ - name: Build with Gradle - run: ./gradlew build + run: ./gradlew bootJar - name: Copy jar file run: mv ./build/libs/*SNAPSHOT.jar ./photopic-dev.jar From 1be6a91c4cc748d243b48da627908af384738789 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 10 Feb 2025 16:52:15 +0900 Subject: [PATCH 041/258] chore: server-config update --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 0680a2bd..a0c8ba22 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 0680a2bd13726ef1d76af6b8b4aeab11a13f8d25 +Subproject commit a0c8ba227cccd05889d694098a7476a00890fd6c From 9995842bf7f6908056e6673bec6c85cfc796384b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 14 Feb 2025 20:21:27 +0900 Subject: [PATCH 042/258] =?UTF-8?q?chore:=20spring=20validation=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 --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index b0671c14..30dc2044 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-validation' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.5' From 59d347be1f4b03860a4a912e26912d0a4fe781c5 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 14 Feb 2025 20:22:05 +0900 Subject: [PATCH 043/258] =?UTF-8?q?chore:=20rest=20docs=20snippet=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../restdocs/templates/query-parameters.snippet | 13 +++++++++++++ .../restdocs/templates/request-fields.snippet | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet create mode 100644 src/test/resources/org/springframework/restdocs/templates/request-fields.snippet diff --git a/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet b/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet new file mode 100644 index 00000000..df8b2f28 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet @@ -0,0 +1,13 @@ +|=== + |파라미터|필수값|기본값|제약조건|설명 + +{{#parameters}} + |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{#defaultValue}}{{.}}{{/defaultValue}}{{/tableCellContent}} + |{{#tableCellContent}}{{#constraint}}{{.}}{{/constraint}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/parameters}} + +|=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet new file mode 100644 index 00000000..b8334609 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet @@ -0,0 +1,13 @@ +===== Request Fields +|=== +|필드명|타입|설명|필수값|Constraints + +{{#fields}} + |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} + |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{#constraints}}{{.}}+{{/constraints}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/fields}} + +|=== \ No newline at end of file From 2f657a12f89fa77f04eedf9c0060b9ad3ed9d806 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 14 Feb 2025 20:23:38 +0900 Subject: [PATCH 044/258] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20docs=20=EC=84=A4=EC=A0=95=20=ED=99=98=EA=B2=BD=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 --- .../com/swyp8team2/support/RestDocsTest.java | 33 ++++++++++++++- .../swyp8team2/support/WithMockUserInfo.java | 13 ++++++ .../support/config/TestSecurityConfig.java | 40 +++++++++++++++++++ .../security/TestSecurityContextFactory.java | 29 ++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/swyp8team2/support/WithMockUserInfo.java create mode 100644 src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java create mode 100644 src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java diff --git a/src/test/java/com/swyp8team2/support/RestDocsTest.java b/src/test/java/com/swyp8team2/support/RestDocsTest.java index 8532cb31..a032da37 100644 --- a/src/test/java/com/swyp8team2/support/RestDocsTest.java +++ b/src/test/java/com/swyp8team2/support/RestDocsTest.java @@ -1,19 +1,28 @@ package com.swyp8team2.support; import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.config.RestDocsConfiguration; +import com.swyp8team2.support.config.TestSecurityConfig; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.headers.HeaderDescriptor; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.request.ParameterDescriptor; +import org.springframework.restdocs.snippet.Attributes; import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + @WebMvcTest @AutoConfigureRestDocs -@Import(RestDocsConfiguration.class) +@Import({RestDocsConfiguration.class, TestSecurityConfig.class}) @ExtendWith(RestDocumentationExtension.class) public abstract class RestDocsTest { @@ -26,4 +35,26 @@ public abstract class RestDocsTest { @Autowired protected RestDocumentationResultHandler restDocs; + protected static Attributes.Attribute constraints(String value) { + return new Attributes.Attribute("constraints", value); + } + + protected static HeaderDescriptor authorizationHeader() { + return headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token"); + } + + protected static HeaderDescriptor guestHeader() { + return headerWithName(CustomHeader.GUEST_ID).description("게스트 Id"); + } + + protected static ParameterDescriptor[] cursorQueryParams() { + return new ParameterDescriptor[]{ + parameterWithName("cursor").optional().description("페이지 조회 커서 값"), + parameterWithName("size").optional().attributes(defaultValue("10")).description("페이지 크기 (기본 값 10)") + }; + } + + protected static Attributes.Attribute defaultValue(String value) { + return new Attributes.Attribute("defaultValue", value); + } } diff --git a/src/test/java/com/swyp8team2/support/WithMockUserInfo.java b/src/test/java/com/swyp8team2/support/WithMockUserInfo.java new file mode 100644 index 00000000..bb769e56 --- /dev/null +++ b/src/test/java/com/swyp8team2/support/WithMockUserInfo.java @@ -0,0 +1,13 @@ +package com.swyp8team2.support; + +import com.swyp8team2.support.security.TestSecurityContextFactory; +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = TestSecurityContextFactory.class) +public @interface WithMockUserInfo { + long userId() default 1L; +} diff --git a/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java new file mode 100644 index 00000000..163d7423 --- /dev/null +++ b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java @@ -0,0 +1,40 @@ +package com.swyp8team2.support.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +import static com.swyp8team2.common.config.SecurityConfig.getWhiteList; + +@TestConfiguration +public class TestSecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain( + HttpSecurity http, + HandlerMappingIntrospector introspect + ) throws Exception { + http + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .headers(headers -> + headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) + .httpStrictTransportSecurity(HeadersConfigurer.HstsConfig::disable)) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + .authorizeHttpRequests(authorize -> + authorize + .requestMatchers(getWhiteList(introspect)).permitAll() + .anyRequest().authenticated() + ); + return http.build(); + } +} diff --git a/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java b/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java new file mode 100644 index 00000000..822963d0 --- /dev/null +++ b/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java @@ -0,0 +1,29 @@ +package com.swyp8team2.support.security; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.support.WithMockUserInfo; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +import java.util.Collections; + +public class TestSecurityContextFactory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithMockUserInfo annotation) { + long userId = annotation.userId(); + UserInfo userInfo = new UserInfo(userId); + + Authentication auth = new UsernamePasswordAuthenticationToken( + userInfo, + null, + Collections.emptyList() + ); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(auth); + return context; + } +} From 49eff32b5826cb9144f4d533c334017f2f1ec428 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 14 Feb 2025 20:29:26 +0900 Subject: [PATCH 045/258] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EB=94=94=20=EC=9D=B4=EB=A6=84=20id=EB=A1=9C=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 --- .../com/swyp8team2/user/presentation/dto/UserInfoResponse.java | 2 +- .../com/swyp8team2/user/presentation/UserControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java index 99603f43..cc32433a 100644 --- a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java +++ b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java @@ -1,7 +1,7 @@ package com.swyp8team2.user.presentation.dto; public record UserInfoResponse( - Long userId, + Long id, String nickname, String profileUrl, String email diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 85c99ff9..a033bd37 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -32,7 +32,7 @@ void findUserInfo() throws Exception { .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( responseFields( - fieldWithPath("userId").description("유저 아이디").type(NUMBER), + fieldWithPath("id").description("유저 아이디").type(NUMBER), fieldWithPath("nickname").description("닉네임").type(STRING), fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING), fieldWithPath("email").description("이메일").type(STRING) From f329072323e4cc458ae120badc4ff935be9237dd Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 14 Feb 2025 20:36:36 +0900 Subject: [PATCH 046/258] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=20api=20uri=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20email=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/presentation/UserController.java | 7 ++++--- .../user/presentation/dto/UserInfoResponse.java | 3 +-- .../user/presentation/UserControllerTest.java | 13 +++++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/swyp8team2/user/presentation/UserController.java b/src/main/java/com/swyp8team2/user/presentation/UserController.java index a86483ed..c05705bb 100644 --- a/src/main/java/com/swyp8team2/user/presentation/UserController.java +++ b/src/main/java/com/swyp8team2/user/presentation/UserController.java @@ -4,6 +4,7 @@ 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; @@ -12,8 +13,8 @@ @RequestMapping("/api/users") public class UserController { - @GetMapping("/me") - public ResponseEntity findUserInfo() { - return ResponseEntity.ok(new UserInfoResponse(1L, "nickname", "profileUrl", "email@email.email")); + @GetMapping("/{userId}") + public ResponseEntity findUserInfo(@PathVariable("userId") Long userId) { + return ResponseEntity.ok(new UserInfoResponse(1L, "nickname", "https://image.com/profile-image")); } } diff --git a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java index cc32433a..19c204fb 100644 --- a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java +++ b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java @@ -3,7 +3,6 @@ public record UserInfoResponse( Long id, String nickname, - String profileUrl, - String email + String profileUrl ) { } diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index a033bd37..09a5cb1c 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -4,6 +4,7 @@ import com.swyp8team2.user.presentation.dto.UserInfoResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.security.test.context.support.WithMockUser; import static org.junit.jupiter.api.Assertions.*; @@ -14,6 +15,8 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -24,18 +27,20 @@ class UserControllerTest extends RestDocsTest { @DisplayName("유저 정보 조회") void findUserInfo() throws Exception { //given - UserInfoResponse response = new UserInfoResponse(1L, "nickname", "profileUrl", "email@email.email"); + UserInfoResponse response = new UserInfoResponse(1L, "nickname", "https://image.com/profile-image"); //when then - mockMvc.perform(get("/api/users/me")) + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/users/{userId}", "1")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( + pathParameters( + parameterWithName("userId").description("유저 아이디") + ), responseFields( fieldWithPath("id").description("유저 아이디").type(NUMBER), fieldWithPath("nickname").description("닉네임").type(STRING), - fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING), - fieldWithPath("email").description("이메일").type(STRING) + fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING) ) )); } From 4c1281908a48fcbaa544bb2961b2f710e69e85d1 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 09:34:10 +0900 Subject: [PATCH 047/258] =?UTF-8?q?refactor:=20uri=EC=97=90=20api=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/common/config/SecurityConfig.java | 12 +++++++----- .../swyp8team2/user/presentation/UserController.java | 2 +- .../user/presentation/UserControllerTest.java | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index dc7d98e2..5d2a8a05 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -13,6 +13,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; @@ -21,6 +22,7 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @@ -62,7 +64,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { } @Bean - @Profile({"dev", "local"}) + @Profile("local") @ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true") public WebSecurityCustomizer configureH2ConsoleEnable() { return web -> web.ignoring() @@ -88,7 +90,6 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize .requestMatchers(getWhiteList(introspect)).permitAll() -// .requestMatchers(PathRequest.toH2Console()).permitAll() .anyRequest().authenticated() ) @@ -106,15 +107,16 @@ public SecurityFilterChain securityFilterChain( .successHandler(oAuthLoginSuccessHandler) .failureHandler(oAuthLoginFailureHandler) ); - return http.build(); } - private MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { + public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), - mvc.pattern("/guest") + mvc.pattern("/guest"), + mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), + mvc.pattern("/votes/guest/**"), }; } } diff --git a/src/main/java/com/swyp8team2/user/presentation/UserController.java b/src/main/java/com/swyp8team2/user/presentation/UserController.java index c05705bb..d232886b 100644 --- a/src/main/java/com/swyp8team2/user/presentation/UserController.java +++ b/src/main/java/com/swyp8team2/user/presentation/UserController.java @@ -10,7 +10,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/users") +@RequestMapping("/users") public class UserController { @GetMapping("/{userId}") diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 09a5cb1c..056622e8 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -30,7 +30,7 @@ void findUserInfo() throws Exception { UserInfoResponse response = new UserInfoResponse(1L, "nickname", "https://image.com/profile-image"); //when then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/users/{userId}", "1")) + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/{userId}", "1")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( From 7b50498dbd4e044d46caa3b7ad68e4975a836dc9 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 09:45:37 +0900 Subject: [PATCH 048/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20a?= =?UTF-8?q?pi=20=EB=B0=8F=20mock=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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 --- .../dto/CursorBasePaginatedResponse.java | 20 ++++ .../com/swyp8team2/common/dto/CursorDto.java | 6 + .../post/presentation/PostController.java | 105 ++++++++++++++++++ .../post/presentation/dto/AuthorDto.java | 8 ++ .../presentation/dto/CreatePostRequest.java | 16 +++ .../post/presentation/dto/PostResponse.java | 14 +++ .../presentation/dto/SimplePostResponse.java | 18 +++ .../post/presentation/dto/VoteRequestDto.java | 9 ++ .../presentation/dto/VoteResponseDto.java | 9 ++ 9 files changed, 205 insertions(+) create mode 100644 src/main/java/com/swyp8team2/common/dto/CursorBasePaginatedResponse.java create mode 100644 src/main/java/com/swyp8team2/common/dto/CursorDto.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/PostController.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java diff --git a/src/main/java/com/swyp8team2/common/dto/CursorBasePaginatedResponse.java b/src/main/java/com/swyp8team2/common/dto/CursorBasePaginatedResponse.java new file mode 100644 index 00000000..379f2f23 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/dto/CursorBasePaginatedResponse.java @@ -0,0 +1,20 @@ +package com.swyp8team2.common.dto; + +import org.springframework.data.domain.Slice; + +import java.util.List; + +public record CursorBasePaginatedResponse( + Long nextCursor, + boolean hasNext, + List data +) { + + public static CursorBasePaginatedResponse of(Slice slice) { + return new CursorBasePaginatedResponse<>( + slice.getContent().isEmpty() ? null : slice.getContent().getLast().getId(), + slice.hasNext(), + slice.getContent() + ); + } +} diff --git a/src/main/java/com/swyp8team2/common/dto/CursorDto.java b/src/main/java/com/swyp8team2/common/dto/CursorDto.java new file mode 100644 index 00000000..bb4ee23f --- /dev/null +++ b/src/main/java/com/swyp8team2/common/dto/CursorDto.java @@ -0,0 +1,6 @@ +package com.swyp8team2.common.dto; + +public interface CursorDto { + + long getId(); +} diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java new file mode 100644 index 00000000..1d7cb7cb --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -0,0 +1,105 @@ +package com.swyp8team2.post.presentation; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.post.presentation.dto.AuthorDto; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.SimplePostResponse; +import com.swyp8team2.post.presentation.dto.VoteResponseDto; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/posts") +public class PostController { + + @PostMapping("") + public ResponseEntity createPost( + @Valid @RequestBody CreatePostRequest request, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } + + @GetMapping("/{shareUrl}") + public ResponseEntity findPost(@PathVariable("shareUrl") String shareUrl) { + return ResponseEntity.ok(new PostResponse( + 1L, + new AuthorDto( + 1L, + "author", + "https://image.com/profile-image" + ), + "description", + List.of( + new VoteResponseDto(1L, "https://image.com/1", 62.75, true), + new VoteResponseDto(2L, "https://image.com/2", 37.25, false) + ), + "https://photopic.site/shareurl", + LocalDateTime.of(2025, 2, 13, 12, 0) + )); + } + + @DeleteMapping("/{shareUrl}") + public ResponseEntity deletePost( + @PathVariable("shareUrl") String shareUrl, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } + + @GetMapping("/me") + public ResponseEntity> findMyPosts( + @RequestParam(name = "cursor", required = false) Long cursor, + @RequestParam(name = "size", required = false, defaultValue = "10") int size, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok(new CursorBasePaginatedResponse<>( + 1L, + false, + List.of( + new SimplePostResponse( + 1L, + "https://image.com/1", + "https://photopic.site/shareurl", + LocalDateTime.of(2025, 2, 13, 12, 0) + ) + ) + )); + } + + @GetMapping("/voted") + public ResponseEntity> findVotedPosts( + @RequestParam(name = "cursor", required = false) Long cursor, + @RequestParam(name = "size", required = false, defaultValue = "10") int size, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok(new CursorBasePaginatedResponse<>( + 1L, + false, + List.of( + new SimplePostResponse( + 1L, + "https://image.com/1", + "https://photopic.site/shareurl", + LocalDateTime.of(2025, 2, 13, 12, 0) + ) + ) + )); + } +} diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java new file mode 100644 index 00000000..494fa7f1 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java @@ -0,0 +1,8 @@ +package com.swyp8team2.post.presentation.dto; + +public record AuthorDto( + Long id, + String nickname, + String profileUrl +) { +} diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java new file mode 100644 index 00000000..3766fb83 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java @@ -0,0 +1,16 @@ +package com.swyp8team2.post.presentation.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.util.List; + +public record CreatePostRequest( + @Size(min = 1, max = 200) + String description, + + @Valid @NotNull @Size(min = 2) + List votes +) { +} diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java new file mode 100644 index 00000000..655611bd --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java @@ -0,0 +1,14 @@ +package com.swyp8team2.post.presentation.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record PostResponse( + Long id, + AuthorDto author, + String description, + List votes, + String shareUrl, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java new file mode 100644 index 00000000..9c32d9bb --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java @@ -0,0 +1,18 @@ +package com.swyp8team2.post.presentation.dto; + +import com.swyp8team2.common.dto.CursorDto; + +import java.time.LocalDateTime; + +public record SimplePostResponse( + long id, + String bestPickedImageUrl, + String shareUrl, + LocalDateTime createdAt +) implements CursorDto { + + @Override + public long getId() { + return id; + } +} diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java new file mode 100644 index 00000000..434726e6 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java @@ -0,0 +1,9 @@ +package com.swyp8team2.post.presentation.dto; + +import jakarta.validation.constraints.NotEmpty; + +public record VoteRequestDto( + @NotEmpty + String imageUrl +) { +} diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java new file mode 100644 index 00000000..ffc000a5 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java @@ -0,0 +1,9 @@ +package com.swyp8team2.post.presentation.dto; + +public record VoteResponseDto( + Long id, + String imageUrl, + double voteRatio, + boolean voted +) { +} From 721d0e007e62e6a67006de63a86601c79dc08624 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 09:47:21 +0900 Subject: [PATCH 049/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20a?= =?UTF-8?q?pi=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20rest=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/presentation/PostControllerTest.java | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java new file mode 100644 index 00000000..1abc8907 --- /dev/null +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -0,0 +1,241 @@ +package com.swyp8team2.post.presentation; + +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.post.presentation.dto.AuthorDto; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.SimplePostResponse; +import com.swyp8team2.post.presentation.dto.VoteRequestDto; +import com.swyp8team2.post.presentation.dto.VoteResponseDto; +import com.swyp8team2.support.RestDocsTest; +import com.swyp8team2.support.WithMockUserInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithAnonymousUser; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class PostControllerTest extends RestDocsTest { + + @Test + @WithMockUserInfo + @DisplayName("게시글 생성") + void createPost() throws Exception { + //given + CreatePostRequest request = new CreatePostRequest( + "제목", + List.of(new VoteRequestDto("https://image.com/1"), new VoteRequestDto("https://image.com/2")) + ); + + //when then + mockMvc.perform(post("/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + requestFields( + fieldWithPath("description") + .type(JsonFieldType.STRING) + .description("설명") + .attributes(constraints("1~200자 사이")), + fieldWithPath("votes") + .type(JsonFieldType.ARRAY) + .description("투표 후보 이미지 URL 목록") + .attributes(constraints("최소 2개")), + fieldWithPath("votes.imageUrl") + .type(JsonFieldType.ARRAY) + .description("투표 후보 이미지 URL 목록") + .attributes(constraints("최소 2개")) + ))); + } + + @Test + @WithAnonymousUser + @DisplayName("게시글 상세 조회") + void findPost() throws Exception { + //given + PostResponse response = new PostResponse( + 1L, + new AuthorDto( + 1L, + "author", + "https://image.com/profile-image" + ), + "description", + List.of( + new VoteResponseDto(1L, "https://image.com/1", 62.75, true), + new VoteResponseDto(2L, "https://image.com/2", 37.25, false) + ), + "https://photopic.site/shareurl", + LocalDateTime.of(2025, 2, 13, 12, 0) + ); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{shareUrl}", "shareUrl")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + pathParameters( + parameterWithName("shareUrl").description("게시글 공유 URL") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 Id"), + fieldWithPath("author").type(JsonFieldType.OBJECT).description("게시글 작성자 정보"), + fieldWithPath("author.id").type(JsonFieldType.NUMBER).description("게시글 작성자 유저 Id"), + fieldWithPath("author.nickname").type(JsonFieldType.STRING).description("게시글 작성자 닉네임"), + fieldWithPath("author.profileUrl").type(JsonFieldType.STRING).description("게시글 작성자 프로필 이미지"), + fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("votes[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("votes[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), + fieldWithPath("votes[].imageUrl").type(JsonFieldType.STRING).description("투표 이미지"), + fieldWithPath("votes[].voteRatio").type(JsonFieldType.NUMBER).description("득표 비율"), + fieldWithPath("votes[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), + fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간") + ) + )); + } + + @Test + @WithMockUserInfo + @DisplayName("게시글 삭제") + void deletePost() throws Exception { + //given + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/posts/{shareUrl}", "shareUrl") + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("shareUrl").description("게시글 공유 URL") + ) + )); + } + + @Test + @WithMockUserInfo + @DisplayName("내가 작성한 게시글 조회") + void findMyPost() throws Exception { + //given + CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( + 1L, + false, + List.of( + new SimplePostResponse( + 1L, + "https://image.com/1", + "https://photopic.site/shareurl", + LocalDateTime.of(2025, 2, 13, 12, 0) + ) + ) + ); + + //when then + mockMvc.perform(get("/posts/me") + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + queryParameters(cursorQueryParams()), + responseFields( + fieldWithPath("nextCursor") + .type(JsonFieldType.NUMBER) + .optional() + .description("다음 조회 커서 값"), + fieldWithPath("hasNext") + .type(JsonFieldType.BOOLEAN) + .description("다음 페이지 존재 여부 (기본 값 10)"), + fieldWithPath("data[]") + .type(JsonFieldType.ARRAY) + .description("게시글 데이터"), + fieldWithPath("data[].id") + .type(JsonFieldType.NUMBER) + .description("게시글 Id"), + fieldWithPath("data[].bestPickedImageUrl") + .type(JsonFieldType.STRING) + .description("가장 많은 득표를 받은 이미지 URL"), + fieldWithPath("data[].shareUrl") + .type(JsonFieldType.STRING) + .description("게시글 공유 URL"), + fieldWithPath("data[].createdAt") + .type(JsonFieldType.STRING) + .description("게시글 생성 시간") + ) + )); + } + + @Test + @WithMockUserInfo + @DisplayName("내가 참여한 게시글 조회") + void findVotedPost() throws Exception { + //given + CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( + 1L, + false, + List.of( + new SimplePostResponse( + 1L, + "https://image.com/1", + "https://photopic.site/shareurl", + LocalDateTime.of(2025, 2, 13, 12, 0) + ) + ) + ); + + //when then + mockMvc.perform(get("/posts/voted") + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + queryParameters(cursorQueryParams()), + responseFields( + fieldWithPath("nextCursor") + .type(JsonFieldType.NUMBER) + .optional() + .description("다음 조회 커서 값"), + fieldWithPath("hasNext") + .type(JsonFieldType.BOOLEAN) + .description("다음 페이지 존재 여부 (기본 값 10)"), + fieldWithPath("data[]") + .type(JsonFieldType.ARRAY) + .description("게시글 데이터"), + fieldWithPath("data[].id") + .type(JsonFieldType.NUMBER) + .description("게시글 Id"), + fieldWithPath("data[].bestPickedImageUrl") + .type(JsonFieldType.STRING) + .description("가장 많은 득표를 받은 이미지 URL"), + fieldWithPath("data[].shareUrl") + .type(JsonFieldType.STRING) + .description("게시글 공유 URL"), + fieldWithPath("data[].createdAt") + .type(JsonFieldType.STRING) + .description("게시글 생성 시간") + ) + )); + } +} From b464ddcd00c21ef0c9f2eecbde85d9c3d80f9e8a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 09:48:19 +0900 Subject: [PATCH 050/258] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20api=20?= =?UTF-8?q?=EB=B0=8F=20mock=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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 --- .../common/presentation/CustomHeader.java | 6 ++ .../vote/presentation/VoteController.java | 60 +++++++++++++++++++ .../presentation/dto/ChangeVoteRequest.java | 12 ++++ .../presentation/dto/GuestVoteRequest.java | 12 ++++ .../vote/presentation/dto/VoteRequest.java | 12 ++++ 5 files changed, 102 insertions(+) create mode 100644 src/main/java/com/swyp8team2/common/presentation/CustomHeader.java create mode 100644 src/main/java/com/swyp8team2/vote/presentation/VoteController.java create mode 100644 src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java create mode 100644 src/main/java/com/swyp8team2/vote/presentation/dto/GuestVoteRequest.java create mode 100644 src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java diff --git a/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java new file mode 100644 index 00000000..d9cf2279 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java @@ -0,0 +1,6 @@ +package com.swyp8team2.common.presentation; + +public abstract class CustomHeader { + + public static final String GUEST_ID = "X-Guest-Id"; +} diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java new file mode 100644 index 00000000..25117a7e --- /dev/null +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -0,0 +1,60 @@ +package com.swyp8team2.vote.presentation; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; +import com.swyp8team2.vote.presentation.dto.GuestVoteRequest; +import com.swyp8team2.vote.presentation.dto.VoteRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/votes") +public class VoteController { + + @PostMapping("") + public ResponseEntity vote( + @Valid @RequestBody VoteRequest request, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } + + @PostMapping("/guest") + public ResponseEntity guestVote( + @RequestHeader(CustomHeader.GUEST_ID) String guestId, + @Valid @RequestBody VoteRequest request + ) { + return ResponseEntity.ok().build(); + } + + @PatchMapping("/{voteId}") + public ResponseEntity changeVote( + @PathVariable("voteId") Long voteId, + @Valid @RequestBody ChangeVoteRequest request, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } + + @PatchMapping("/guest/{voteId}") + public ResponseEntity changeGuestVote( + @PathVariable("voteId") Long voteId, + @RequestHeader(CustomHeader.GUEST_ID) String guestId, + @Valid @RequestBody ChangeVoteRequest request + ) { + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java new file mode 100644 index 00000000..c29423dd --- /dev/null +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java @@ -0,0 +1,12 @@ +package com.swyp8team2.vote.presentation.dto; + +import jakarta.validation.constraints.NotNull; + +public record ChangeVoteRequest( + @NotNull + Long postId, + + @NotNull + Long voteId +) { +} diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/GuestVoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/GuestVoteRequest.java new file mode 100644 index 00000000..60492cc1 --- /dev/null +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/GuestVoteRequest.java @@ -0,0 +1,12 @@ +package com.swyp8team2.vote.presentation.dto; + +import jakarta.validation.constraints.NotNull; + +public record GuestVoteRequest( + @NotNull + Long postId, + + @NotNull + Long voteId +) { +} diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java new file mode 100644 index 00000000..d623cc72 --- /dev/null +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java @@ -0,0 +1,12 @@ +package com.swyp8team2.vote.presentation.dto; + +import jakarta.validation.constraints.NotNull; + +public record VoteRequest( + @NotNull + Long postId, + + @NotNull + Long voteId +) { +} From ee369d5657d4df6e69c2db02fc1abd0042281ad5 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 09:48:37 +0900 Subject: [PATCH 051/258] =?UTF-8?q?test:=20=ED=88=AC=ED=91=9C=20api=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20?= =?UTF-8?q?rest=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/presentation/VoteControllerTest.java | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java new file mode 100644 index 00000000..612e83b9 --- /dev/null +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -0,0 +1,129 @@ +package com.swyp8team2.vote.presentation; + +import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.support.RestDocsTest; +import com.swyp8team2.support.WithMockUserInfo; +import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; +import com.swyp8team2.vote.presentation.dto.VoteRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithAnonymousUser; + +import java.util.UUID; + +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class VoteControllerTest extends RestDocsTest { + + @Test + @WithMockUserInfo + @DisplayName("투표") + void vote() throws Exception { + //given + VoteRequest request = new VoteRequest(1L, 1L); + + //when test + mockMvc.perform(post("/votes") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + requestFields( + fieldWithPath("postId") + .type(JsonFieldType.NUMBER) + .description("투표 게시글 Id"), + fieldWithPath("voteId") + .type(JsonFieldType.NUMBER) + .description("투표 후보 Id") + ) + )); + } + + @Test + @WithAnonymousUser + @DisplayName("게스트 투표") + void guestVote() throws Exception { + //given + VoteRequest request = new VoteRequest(1L, 1L); + + //when test + mockMvc.perform(post("/votes/guest") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(guestHeader()), + requestFields( + fieldWithPath("postId") + .type(JsonFieldType.NUMBER) + .description("투표 게시글 Id"), + fieldWithPath("voteId") + .type(JsonFieldType.NUMBER) + .description("투표 후보 Id") + ) + )); + } + + @Test + @WithMockUserInfo + @DisplayName("투표 변경") + void changeVote() throws Exception { + //given + ChangeVoteRequest request = new ChangeVoteRequest(1L, 1L); + + //when + mockMvc.perform(patch("/votes/{voteId}", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + requestFields( + fieldWithPath("postId") + .type(JsonFieldType.NUMBER) + .description("변경할 투표 게시글 Id"), + fieldWithPath("voteId") + .type(JsonFieldType.NUMBER) + .description("변경할 투표 후보 Id") + ) + )); + } + + @Test + @WithAnonymousUser + @DisplayName("게스트 투표 변경") + void guestChangeVote() throws Exception { + //given + ChangeVoteRequest request = new ChangeVoteRequest(1L, 1L); + + //when + mockMvc.perform(patch("/votes/guest/{voteId}", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(guestHeader()), + requestFields( + fieldWithPath("postId") + .type(JsonFieldType.NUMBER) + .description("변경할 투표 게시글 Id"), + fieldWithPath("voteId") + .type(JsonFieldType.NUMBER) + .description("변경할 투표 후보 Id") + ) + )); + } +} From 861a9c842152d55a97d73e0cfc4b27861f27a60c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 09:49:34 +0900 Subject: [PATCH 052/258] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20api=20?= =?UTF-8?q?=EB=B0=8F=20mock=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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 --- .../presentation/CommentController.java | 59 +++++++++++++++++++ .../comment/presentation/dto/AuthorDto.java | 8 +++ .../presentation/dto/CommentResponse.java | 12 ++++ .../dto/CreateCommentRequest.java | 9 +++ 4 files changed, 88 insertions(+) create mode 100644 src/main/java/com/swyp8team2/comment/presentation/CommentController.java create mode 100644 src/main/java/com/swyp8team2/comment/presentation/dto/AuthorDto.java create mode 100644 src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java create mode 100644 src/main/java/com/swyp8team2/comment/presentation/dto/CreateCommentRequest.java diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java new file mode 100644 index 00000000..f4a16def --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -0,0 +1,59 @@ +package com.swyp8team2.comment.presentation; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.comment.presentation.dto.AuthorDto; +import com.swyp8team2.comment.presentation.dto.CommentResponse; +import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/posts/{postId}/comments") +public class CommentController { + + @PostMapping("") + public ResponseEntity createComment( + @PathVariable("postId") Long postId, + @Valid @RequestBody CreateCommentRequest request, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } + + @GetMapping("") + public ResponseEntity> findComments( + @PathVariable("postId") Long postId, + @RequestParam(value = "cursor", required = false) Long cursor, + @RequestParam(value = "size", required = false, defaultValue = "10") int size, + @AuthenticationPrincipal UserInfo userInfo + ) { + CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( + 1L, + false, + List.of( + new CommentResponse( + 1L, + "content", + new AuthorDto(1L, "author", "https://image.com/profile-image"), + 1L, + LocalDateTime.of(2025, 2, 13, 12, 0) + ) + ) + ); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/AuthorDto.java b/src/main/java/com/swyp8team2/comment/presentation/dto/AuthorDto.java new file mode 100644 index 00000000..69cff2d9 --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/AuthorDto.java @@ -0,0 +1,8 @@ +package com.swyp8team2.comment.presentation.dto; + +public record AuthorDto( + Long userId, + String nickname, + String profileUrl +) { +} diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java new file mode 100644 index 00000000..b7fe2f3b --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -0,0 +1,12 @@ +package com.swyp8team2.comment.presentation.dto; + +import java.time.LocalDateTime; + +public record CommentResponse( + Long commentId, + String content, + AuthorDto author, + Long voteId, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CreateCommentRequest.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CreateCommentRequest.java new file mode 100644 index 00000000..624a0fed --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CreateCommentRequest.java @@ -0,0 +1,9 @@ +package com.swyp8team2.comment.presentation.dto; + +import jakarta.validation.constraints.NotEmpty; + +public record CreateCommentRequest( + @NotEmpty + String content +) { +} From c4ebefbf9afd8011d6badcd95a3de2981427c840 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 10:20:57 +0900 Subject: [PATCH 053/258] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20api=20?= =?UTF-8?q?=EB=B0=8F=20mock=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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 --- .../comment/presentation/CommentController.java | 12 +++++++++++- .../com/swyp8team2/common/config/SecurityConfig.java | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index f4a16def..2fc9cb9a 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -22,7 +23,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/posts/{postId}/comments") +@RequestMapping("/posts/{postId}/comments") public class CommentController { @PostMapping("") @@ -56,4 +57,13 @@ public ResponseEntity> findComments ); return ResponseEntity.ok(response); } + + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment( + @PathVariable("postId") Long postId, + @PathVariable("commentId") Long commentId, + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 5d2a8a05..6ef057b1 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -116,6 +116,7 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros mvc.pattern("/auth/reissue"), mvc.pattern("/guest"), mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), + mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), mvc.pattern("/votes/guest/**"), }; } From 5edf916607b87fd1f0aa7a8e413fe4d22f473a4b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 10:21:14 +0900 Subject: [PATCH 054/258] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20api=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20?= =?UTF-8?q?rest=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/CommentControllerTest.java | 146 ++++++++++++++++++ .../com/swyp8team2/support/RestDocsTest.java | 2 +- 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java new file mode 100644 index 00000000..412f32b2 --- /dev/null +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -0,0 +1,146 @@ +package com.swyp8team2.comment.presentation; + +import com.swyp8team2.comment.presentation.dto.AuthorDto; +import com.swyp8team2.comment.presentation.dto.CommentResponse; +import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.support.RestDocsTest; +import com.swyp8team2.support.WithMockUserInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithAnonymousUser; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class CommentControllerTest extends RestDocsTest { + + + @Test + @WithMockUserInfo + @DisplayName("댓글 생성") + void createComment() throws Exception { + //given + CreateCommentRequest request = new CreateCommentRequest("content"); + + //when then + mockMvc.perform(post("/posts/{postId}/comments", "1") + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ), + requestFields( + fieldWithPath("content").type(JsonFieldType.STRING).description("댓글 내용").attributes(constraints("최대 ?글자")) + ) + )); + } + + @Test + @WithAnonymousUser + @DisplayName("댓글 조회") + void findComments() throws Exception { + //given + CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( + 1L, + false, + List.of( + new CommentResponse( + 1L, + "content", + new AuthorDto(1L, "author", "https://image.com/profile-image"), + 1L, + LocalDateTime.of(2025, 2, 13, 12, 0) + ) + ) + ); + + //when + mockMvc.perform(get("/posts/{postId}/comments", "1")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + pathParameters( + parameterWithName("postId").description("게시글 Id") + ), + queryParameters(cursorQueryParams()), + responseFields( + fieldWithPath("nextCursor") + .type(JsonFieldType.NUMBER) + .optional() + .description("다음 조회 커서 값"), + fieldWithPath("hasNext") + .type(JsonFieldType.BOOLEAN) + .description("다음 페이지 존재 여부 (기본 값 10)"), + fieldWithPath("data[]") + .type(JsonFieldType.ARRAY) + .description("댓글 데이터"), + fieldWithPath("data[].commentId") + .type(JsonFieldType.NUMBER) + .description("댓글 Id"), + fieldWithPath("data[].content") + .type(JsonFieldType.STRING) + .description("댓글 내용"), + fieldWithPath("data[].author") + .type(JsonFieldType.OBJECT) + .description("작성자"), + fieldWithPath("data[].author.userId") + .type(JsonFieldType.NUMBER) + .description("작성자 id"), + fieldWithPath("data[].author.nickname") + .type(JsonFieldType.STRING) + .description("작성자 닉네임"), + fieldWithPath("data[].author.profileUrl") + .type(JsonFieldType.STRING) + .description("작성자 프로필 이미지 url"), + fieldWithPath("data[].voteId") + .type(JsonFieldType.NUMBER) + .optional() + .description("작성자 투표 Id (투표 없을 시 null)"), + fieldWithPath("data[].createdAt") + .type(JsonFieldType.STRING) + .description("댓글 작성일") + ) + )); + } + + @Test + @WithMockUserInfo + @DisplayName("댓글 삭제") + void deleteComment() throws Exception { + //given + + //when then + mockMvc.perform(delete("/posts/{postId}/comments/{commentId}", "1", "1") + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id"), + parameterWithName("commentId").description("댯글 Id") + ) + )); + } +} diff --git a/src/test/java/com/swyp8team2/support/RestDocsTest.java b/src/test/java/com/swyp8team2/support/RestDocsTest.java index a032da37..1bce6011 100644 --- a/src/test/java/com/swyp8team2/support/RestDocsTest.java +++ b/src/test/java/com/swyp8team2/support/RestDocsTest.java @@ -44,7 +44,7 @@ protected static HeaderDescriptor authorizationHeader() { } protected static HeaderDescriptor guestHeader() { - return headerWithName(CustomHeader.GUEST_ID).description("게스트 Id"); + return headerWithName(CustomHeader.GUEST_ID).description("게스트 Id (UUID 형식)"); } protected static ParameterDescriptor[] cursorQueryParams() { From 9358cbade288375db9d3370451b0a90523811d33 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 10:27:18 +0900 Subject: [PATCH 055/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../swyp8team2/post/presentation/PostControllerTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 1abc8907..18678863 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -59,12 +59,11 @@ void createPost() throws Exception { .attributes(constraints("1~200자 사이")), fieldWithPath("votes") .type(JsonFieldType.ARRAY) - .description("투표 후보 이미지 URL 목록") + .description("투표 후보") .attributes(constraints("최소 2개")), - fieldWithPath("votes.imageUrl") - .type(JsonFieldType.ARRAY) - .description("투표 후보 이미지 URL 목록") - .attributes(constraints("최소 2개")) + fieldWithPath("votes[].imageUrl") + .type(JsonFieldType.STRING) + .description("투표 후보 이미지 URL") ))); } From a288ba3b79cee052b1bf151c409ce0b53f0e7473 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 10:42:55 +0900 Subject: [PATCH 056/258] =?UTF-8?q?docs:=20rest=20docs=20asciidoc=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/docs/asciidoc/comments.adoc | 17 ++++++++++++ src/docs/asciidoc/index.adoc | 6 +++++ src/docs/asciidoc/posts.adoc | 27 +++++++++++++++++++ src/docs/asciidoc/votes.adoc | 22 +++++++++++++++ .../presentation/CommentControllerTest.java | 2 +- .../templates/query-parameters.snippet | 11 ++++---- .../restdocs/templates/request-fields.snippet | 12 ++++----- .../templates/response-fields.snippet | 12 +++++++++ 8 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 src/docs/asciidoc/comments.adoc create mode 100644 src/docs/asciidoc/posts.adoc create mode 100644 src/docs/asciidoc/votes.adoc create mode 100644 src/test/resources/org/springframework/restdocs/templates/response-fields.snippet diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc new file mode 100644 index 00000000..bd48bd44 --- /dev/null +++ b/src/docs/asciidoc/comments.adoc @@ -0,0 +1,17 @@ +[[댓글-API]] +== 댓글 API + +[[댓글-생성]] +=== 댓글 생성 + +operation::comment-controller-test/create-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] + +[[댓글-조회]] +=== 댓글 조회 + +operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] + +[[댓글-삭제]] +=== 댓글 삭제 + +operation::comment-controller-test/delete-comment[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 188a6fb2..d01546f9 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -8,3 +8,9 @@ api문서 :sectlinks: include::users.adoc[] + +include::posts.adoc[] + +include::votes.adoc[] + +include::comments.adoc \ No newline at end of file diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc new file mode 100644 index 00000000..e7262787 --- /dev/null +++ b/src/docs/asciidoc/posts.adoc @@ -0,0 +1,27 @@ +[[게시글-API]] +== 게시글 API + +[[게시글-작성]] +=== 게시글 작성 + +operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-fields,http-response'] + +[[게시글-상세-조회]] +=== 게시글 상세 조회 + +operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] + +[[내가-작성한-게시글-조회]] +=== 내가 작성한 게시글 조회 + +operation::post-controller-test/find-my-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] + +[[내가-참여한-게시글-조회]] +=== 내가 참여한 게시글 조회 + +operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] + +[[게시글-삭제]] +=== 게시글 삭제 + +operation::post-controller-test/delete-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc new file mode 100644 index 00000000..c9a894a4 --- /dev/null +++ b/src/docs/asciidoc/votes.adoc @@ -0,0 +1,22 @@ +[[투표-API]] +== 투표 API + +[[투표]] +=== 투표 + +operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] + +[[게스트-투표]] +=== 게스트 투표 + +operation::vote-controller-test/guest-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] + +[[투표-변경]] +=== 투표 변경 + +operation::vote-controller-test/change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] + +[[게스트-투표-변경]] +=== 게스트 투표 변경 + +operation::vote-controller-test/guest-change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index 412f32b2..a9b95b19 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -139,7 +139,7 @@ void deleteComment() throws Exception { requestHeaders(authorizationHeader()), pathParameters( parameterWithName("postId").description("게시글 Id"), - parameterWithName("commentId").description("댯글 Id") + parameterWithName("commentId").description("댓글 Id") ) )); } diff --git a/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet b/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet index df8b2f28..3de99178 100644 --- a/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet +++ b/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet @@ -2,12 +2,11 @@ |파라미터|필수값|기본값|제약조건|설명 {{#parameters}} - |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} - |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} - |{{#tableCellContent}}{{#defaultValue}}{{.}}{{/defaultValue}}{{/tableCellContent}} - |{{#tableCellContent}}{{#constraint}}{{.}}{{/constraint}}{{/tableCellContent}} - |{{#tableCellContent}}{{description}}{{/tableCellContent}} - + |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{#defaultValue}}{{.}}{{/defaultValue}}{{/tableCellContent}} + |{{#tableCellContent}}{{#constraint}}{{.}}{{/constraint}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} |=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet index b8334609..f0ac5c55 100644 --- a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet +++ b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet @@ -1,13 +1,13 @@ ===== Request Fields |=== -|필드명|타입|설명|필수값|Constraints +|필드명|타입|필수값|조건|설명 {{#fields}} - |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} - |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} - |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} - |{{#tableCellContent}}{{#constraints}}{{.}}+{{/constraints}}{{/tableCellContent}} - |{{#tableCellContent}}{{description}}{{/tableCellContent}} + |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} + |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{#constraints}}{{/constraints}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet new file mode 100644 index 00000000..182a1a12 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet @@ -0,0 +1,12 @@ +===== Response Fields +|=== +|필드명|타입|필수값|설명 + +{{#fields}} + |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} + |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/fields}} + +|=== \ No newline at end of file From 38a8d6ec9d1db4a6be96221a4eaf10efc1d5928c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 11:01:05 +0900 Subject: [PATCH 057/258] =?UTF-8?q?refactor:=20=ED=88=AC=ED=91=9C=20reques?= =?UTF-8?q?t=20body=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/SecurityConfig.java | 2 +- .../vote/presentation/VoteController.java | 12 +++-- .../presentation/dto/ChangeVoteRequest.java | 3 -- .../vote/presentation/dto/VoteRequest.java | 3 -- .../vote/presentation/VoteControllerTest.java | 46 ++++++++++--------- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 6ef057b1..0b85d30a 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -117,7 +117,7 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros mvc.pattern("/guest"), mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), - mvc.pattern("/votes/guest/**"), + mvc.pattern("/posts/{postId}/votes/guest/**"), }; } } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index 25117a7e..50f24370 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -21,11 +21,12 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/votes") +@RequestMapping("/posts/{postId}/votes") public class VoteController { @PostMapping("") public ResponseEntity vote( + @PathVariable("postId") Long postId, @Valid @RequestBody VoteRequest request, @AuthenticationPrincipal UserInfo userInfo ) { @@ -34,24 +35,25 @@ public ResponseEntity vote( @PostMapping("/guest") public ResponseEntity guestVote( + @PathVariable("postId") Long postId, @RequestHeader(CustomHeader.GUEST_ID) String guestId, @Valid @RequestBody VoteRequest request ) { return ResponseEntity.ok().build(); } - @PatchMapping("/{voteId}") + @PatchMapping("") public ResponseEntity changeVote( - @PathVariable("voteId") Long voteId, + @PathVariable("postId") Long postId, @Valid @RequestBody ChangeVoteRequest request, @AuthenticationPrincipal UserInfo userInfo ) { return ResponseEntity.ok().build(); } - @PatchMapping("/guest/{voteId}") + @PatchMapping("/guest") public ResponseEntity changeGuestVote( - @PathVariable("voteId") Long voteId, + @PathVariable("postId") Long postId, @RequestHeader(CustomHeader.GUEST_ID) String guestId, @Valid @RequestBody ChangeVoteRequest request ) { diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java index c29423dd..816e9a7d 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java @@ -3,9 +3,6 @@ import jakarta.validation.constraints.NotNull; public record ChangeVoteRequest( - @NotNull - Long postId, - @NotNull Long voteId ) { diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java index d623cc72..202c8157 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java @@ -3,9 +3,6 @@ import jakarta.validation.constraints.NotNull; public record VoteRequest( - @NotNull - Long postId, - @NotNull Long voteId ) { diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index 612e83b9..e0e8263d 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -15,10 +15,12 @@ import java.util.UUID; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class VoteControllerTest extends RestDocsTest { @@ -28,20 +30,20 @@ class VoteControllerTest extends RestDocsTest { @DisplayName("투표") void vote() throws Exception { //given - VoteRequest request = new VoteRequest(1L, 1L); + VoteRequest request = new VoteRequest(1L); //when test - mockMvc.perform(post("/votes") + mockMvc.perform(post("/posts/{postId}/votes", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ), requestFields( - fieldWithPath("postId") - .type(JsonFieldType.NUMBER) - .description("투표 게시글 Id"), fieldWithPath("voteId") .type(JsonFieldType.NUMBER) .description("투표 후보 Id") @@ -54,20 +56,20 @@ void vote() throws Exception { @DisplayName("게스트 투표") void guestVote() throws Exception { //given - VoteRequest request = new VoteRequest(1L, 1L); + VoteRequest request = new VoteRequest(1L); //when test - mockMvc.perform(post("/votes/guest") + mockMvc.perform(post("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ), requestFields( - fieldWithPath("postId") - .type(JsonFieldType.NUMBER) - .description("투표 게시글 Id"), fieldWithPath("voteId") .type(JsonFieldType.NUMBER) .description("투표 후보 Id") @@ -80,20 +82,20 @@ void guestVote() throws Exception { @DisplayName("투표 변경") void changeVote() throws Exception { //given - ChangeVoteRequest request = new ChangeVoteRequest(1L, 1L); + ChangeVoteRequest request = new ChangeVoteRequest(1L); //when - mockMvc.perform(patch("/votes/{voteId}", "1") + mockMvc.perform(patch("/posts/{postId}/votes", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("변경할 게시글 Id") + ), requestFields( - fieldWithPath("postId") - .type(JsonFieldType.NUMBER) - .description("변경할 투표 게시글 Id"), fieldWithPath("voteId") .type(JsonFieldType.NUMBER) .description("변경할 투표 후보 Id") @@ -106,20 +108,20 @@ void changeVote() throws Exception { @DisplayName("게스트 투표 변경") void guestChangeVote() throws Exception { //given - ChangeVoteRequest request = new ChangeVoteRequest(1L, 1L); + ChangeVoteRequest request = new ChangeVoteRequest(1L); //when - mockMvc.perform(patch("/votes/guest/{voteId}", "1") + mockMvc.perform(patch("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()), + pathParameters( + parameterWithName("postId").description("변경활 게시글 Id") + ), requestFields( - fieldWithPath("postId") - .type(JsonFieldType.NUMBER) - .description("변경할 투표 게시글 Id"), fieldWithPath("voteId") .type(JsonFieldType.NUMBER) .description("변경할 투표 후보 Id") From 5e465509683207b554d33353169d1a6c7167348c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 15:39:59 +0900 Subject: [PATCH 058/258] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20api=20=EB=B0=8F=20mock=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/AuthController.java | 24 +++++++++++++++++++ .../common/presentation/CustomHeader.java | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/swyp8team2/auth/presentation/AuthController.java diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java new file mode 100644 index 00000000..142f0576 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -0,0 +1,24 @@ +package com.swyp8team2.auth.presentation; + + +import com.swyp8team2.auth.presentation.dto.TokenResponse; +import com.swyp8team2.common.presentation.CustomHeader; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/auth") +public class AuthController { + + @PostMapping("/reissue") + public ResponseEntity reissue( + @RequestHeader(CustomHeader.AUTHORIZATION_REFRESH) String refreshToken + ) { + return ResponseEntity.ok(new TokenResponse("accessToken", "refreshToken")); + } +} diff --git a/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java index d9cf2279..846ed7e3 100644 --- a/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java +++ b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java @@ -2,5 +2,6 @@ public abstract class CustomHeader { - public static final String GUEST_ID = "X-Guest-Id"; + public static final String GUEST_ID = "Guest-Id"; + public static final String AUTHORIZATION_REFRESH = "Authorization-Refresh"; } From be4c47533aa115fb1a35a810ffcffcf5e62b0fbd Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 15:40:24 +0900 Subject: [PATCH 059/258] =?UTF-8?q?test:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20rest=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/AuthControllerTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java new file mode 100644 index 00000000..df7b8d34 --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -0,0 +1,47 @@ +package com.swyp8team2.auth.presentation; + +import com.swyp8team2.auth.presentation.dto.TokenResponse; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ErrorResponse; +import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.support.RestDocsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.security.test.context.support.WithAnonymousUser; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AuthControllerTest extends RestDocsTest { + + @Test + @WithAnonymousUser + @DisplayName("토큰 재발급") + void reissue() throws Exception { + //given + TokenResponse response = new TokenResponse("accessToken", "refreshToken"); + + //when then + mockMvc.perform(post("/auth/reissue") + .header(CustomHeader.AUTHORIZATION_REFRESH, "refreshToken")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders( + headerWithName(CustomHeader.AUTHORIZATION_REFRESH).description("리프레시 토큰") + ), + responseFields( + fieldWithPath("accessToken").description("액세스 토큰"), + fieldWithPath("refreshToken").description("리프레시 토큰") + ) + )); + } +} From 920b9416a2c15dbafaaa38032a84abc8354f9c38 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 15:40:40 +0900 Subject: [PATCH 060/258] =?UTF-8?q?docs:=20rest=20docs=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 9 ++ src/docs/asciidoc/index.adoc | 84 ++++++++++++++++++- src/docs/asciidoc/users.adoc | 2 +- .../templates/query-parameters.snippet | 2 +- .../restdocs/templates/request-fields.snippet | 3 +- .../templates/response-fields.snippet | 1 - 6 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 src/docs/asciidoc/auth.adoc diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc new file mode 100644 index 00000000..45ebaba0 --- /dev/null +++ b/src/docs/asciidoc/auth.adoc @@ -0,0 +1,9 @@ +[[인증-API]] +== 인증 API + +[[토큰-재발급]] +=== 토큰 재발급 + +이 부분 프런트와 다시 고민해봐야 함 + +operation::auth-controller-test/reissue[snippets='http-request,curl-request,request-headers,http-response,response-fields'] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index d01546f9..06694e10 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -7,10 +7,92 @@ api문서 :toclevels: 2 :sectlinks: +[[overview]] +== 개요 + +``` +뽀토픽 API 문서입니다. +잘못 됐거나 추가되어야 할 내용이 있으면 언제든지 연락주세요. +``` + +[[상태코드]] +=== 상태 코드 + +``` +HTTP 상태 코드 본 REST API에서 사용하는 HTTP 상태 코드는 가능한한 표준 HTTP와 REST 규약을 따릅니다. +``` + +|=== + +| 상태 코드 | 용례 +| `200 OK`| 요청을 성공적으로 처리함 +| `400 Bad Request`| 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 +| `401 Unauthorization`| 인증에 실패한 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 +| `404 Not Found`| 요청한 리소스가 없음. +| `500 Internal Server Error`| 서버 내부 요류가 발생한 경우. + +|=== + +[[에러]] +=== 에러 + +``` +오류 에러 응답이 발생했을 때 (상태 코드 >= 400), 본문에 해당 문제를 기술한 JSON 객체가 담겨있음 +``` + +```HTTP +HTTP/1.1 200 OK +content-type: application/json + +{ + "errorCode": "에러 코드" +} +``` + +[[인증]] +=== 인증 + +``` +인증 토큰은 다음과 같은 형식으로 전달됨 +``` + +```HTTP +POST /posts HTTP/1.1 +Content-Type: application/json;charset=UTF-8 +Authorization: Bearer accessToken +``` + +[[인증-예외]] +=== 인증 예외 + +``` +인증 토큰관련 예외가 발생하면 다음과 같은 에러 코드와 함께 401 상태 코드를 응답함 +``` + +|=== + +| 에러 코드 | 용례 +|`EXPIRED_TOKEN`| 토큰이 만료되었을 경우 +|`INVALID_TOKEN`| 잘못된 형식의 토큰인 경우 +|`INVALID_AUTH_HEADER`| Authorization 헤더가 존재하지 않거나 Bearer 형식이 아닌 경우 +|=== + +예시 + +```HTTP +HTTP/1.1 401 Unauthorized + +{ + "errorCode": "EXPIRED_TOKEN" +} +``` + +include::auth.adoc[] + include::users.adoc[] include::posts.adoc[] include::votes.adoc[] -include::comments.adoc \ No newline at end of file +include::comments.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/users.adoc b/src/docs/asciidoc/users.adoc index 45f8421c..e2f16130 100644 --- a/src/docs/asciidoc/users.adoc +++ b/src/docs/asciidoc/users.adoc @@ -4,4 +4,4 @@ [[유저-정보-조회]] === 유저 정보 조회 -operation::user-controller-test/find-user-info[snippets='http-request,curl-request,http-response,response-fields'] \ No newline at end of file +operation::user-controller-test/find-user-info[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet b/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet index 3de99178..f8d07c43 100644 --- a/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet +++ b/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet @@ -1,5 +1,5 @@ |=== - |파라미터|필수값|기본값|제약조건|설명 + |파라미터|필수값|기본값|제약조건|설명 {{#parameters}} |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} diff --git a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet index f0ac5c55..58729b5d 100644 --- a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet +++ b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet @@ -1,4 +1,3 @@ -===== Request Fields |=== |필드명|타입|필수값|조건|설명 @@ -6,7 +5,7 @@ |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} - |{{#tableCellContent}}{{#constraints}}{{/constraints}}{{/tableCellContent}} + |{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} diff --git a/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet index 182a1a12..f63b0ed6 100644 --- a/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet +++ b/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet @@ -1,4 +1,3 @@ -===== Response Fields |=== |필드명|타입|필수값|설명 From 30bd99705f5de32e55391e994b873e88aaf40d7a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 15:46:39 +0900 Subject: [PATCH 061/258] =?UTF-8?q?docs:=20index.html=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/index.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 06694e10..43b1cd11 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -11,8 +11,8 @@ api문서 == 개요 ``` -뽀토픽 API 문서입니다. -잘못 됐거나 추가되어야 할 내용이 있으면 언제든지 연락주세요. +뽀또픽 API 문서입니다. +잘못되었거나 추가 및 수정되어야 할 내용이 있으면 언제든지 연락주세요. ``` [[상태코드]] From 89aba8e7c1803b0b3d23b5fa9d513dd3134192c8 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 15:55:10 +0900 Subject: [PATCH 062/258] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=9C=ED=8D=BC?= =?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 --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index a0c8ba22..6e226734 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit a0c8ba227cccd05889d694098a7476a00890fd6c +Subproject commit 6e226734850b8eb35cbfec5a36cd81b4321fd114 From 45ed0535803d50bc10ed488f2d5dc543d58df43f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 17 Feb 2025 16:14:39 +0900 Subject: [PATCH 063/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=83=AD=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 6e226734..f16b236c 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 6e226734850b8eb35cbfec5a36cd81b4321fd114 +Subproject commit f16b236c75930077b8aa7c5254f159b036282e68 From a6c513699654a1b99ad43238783f7d68c90d7f46 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 17 Feb 2025 21:58:06 +0900 Subject: [PATCH 064/258] fix: test application.yml --- .github/workflows/cd-dev.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index bc4192cd..29812158 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -37,8 +37,13 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- + - name: Create directory resources + run: mkdir -p ./src/test/resources + - name: Copy application.yml - run: cp ./server-config/*.yml ./src/main/resources/ + run: | + cp ./server-config/*.yml ./src/main/resources/ + cp ./server-config/application-test.yml ./src/test/resources/application.yml - name: Build with Gradle run: ./gradlew bootJar From 757ff784ecbf67b783a69c879873e3d750017d75 Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 19 Feb 2025 02:12:01 +0900 Subject: [PATCH 065/258] =?UTF-8?q?docs:=20=EC=98=A4=ED=83=80=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 --- src/docs/asciidoc/index.adoc | 2 +- .../restdocs/templates/request-parts.snippet | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/org/springframework/restdocs/templates/request-parts.snippet diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 43b1cd11..96cdfa39 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -29,7 +29,7 @@ HTTP 상태 코드 본 REST API에서 사용하는 HTTP 상태 코드는 가능 | `400 Bad Request`| 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 | `401 Unauthorization`| 인증에 실패한 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 | `404 Not Found`| 요청한 리소스가 없음. -| `500 Internal Server Error`| 서버 내부 요류가 발생한 경우. +| `500 Internal Server Error`| 서버 내부 오류가 발생한 경우. |=== diff --git a/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet b/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet new file mode 100644 index 00000000..0a0dd970 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet @@ -0,0 +1,21 @@ +|=== +|필드명|타입|필수값|조건|설명 + +{{#requestParts}} + |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} + |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} + |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} + |{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}} + |{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/requestParts}} +{{#requestPartFields}} + |{{#fields}} + |{{path}} + |{{description}} + |{{type}} + |{{optional}} + |{{#constraints}}{{.}}{{/constraints}} + {{/fields}} +{{/requestPartFields}} + +|=== \ No newline at end of file From 0fa10c524068e9598d75a67b9ae560fcbe5a4153 Mon Sep 17 00:00:00 2001 From: Nakji Date: Wed, 19 Feb 2025 02:21:41 +0900 Subject: [PATCH 066/258] =?UTF-8?q?docs:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20U?= =?UTF-8?q?RL=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EB=AC=BC=20=EC=83=9D=EC=84=B1=20=EB=AC=B8=EC=84=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 --- src/docs/asciidoc/posts.adoc | 2 +- .../post/presentation/PostController.java | 27 +++---- .../post/presentation/PostControllerTest.java | 70 +++++++++++-------- .../restdocs/templates/request-parts.snippet | 9 --- 4 files changed, 53 insertions(+), 55 deletions(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index e7262787..5493208f 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -4,7 +4,7 @@ [[게시글-작성]] === 게시글 작성 -operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-fields,http-response'] +operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-parts,http-response'] [[게시글-상세-조회]] === 게시글 상세 조회 diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 1d7cb7cb..b2034973 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -3,22 +3,16 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; -import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.VoteResponseDto; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -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.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; import java.util.List; @@ -28,9 +22,10 @@ @RequestMapping("/posts") public class PostController { - @PostMapping("") + @PostMapping(value ="", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity createPost( - @Valid @RequestBody CreatePostRequest request, + @Valid @RequestPart("description") String description, + @Valid @RequestPart("files") List files, @AuthenticationPrincipal UserInfo userInfo ) { return ResponseEntity.ok().build(); @@ -43,12 +38,12 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh new AuthorDto( 1L, "author", - "https://image.com/profile-image" + "https://image.photopic.site/imagePath/profile-image" ), "description", List.of( - new VoteResponseDto(1L, "https://image.com/1", 62.75, true), - new VoteResponseDto(2L, "https://image.com/2", 37.25, false) + new VoteResponseDto(1L, "https://image.photopic.site/imagePath/1", 62.75, true), + new VoteResponseDto(2L, "https://image.photopic.site/imagePath/2", 37.25, false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) @@ -75,7 +70,7 @@ public ResponseEntity> findMyPos List.of( new SimplePostResponse( 1L, - "https://image.com/1", + "https://image.photopic.site/imagePath/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) @@ -95,7 +90,7 @@ public ResponseEntity> findVoted List.of( new SimplePostResponse( 1L, - "https://image.com/1", + "https://image.photopic.site/imagePath/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 18678863..c5f2ae96 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -2,10 +2,8 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; -import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; -import com.swyp8team2.post.presentation.dto.VoteRequestDto; import com.swyp8team2.post.presentation.dto.VoteResponseDto; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; @@ -13,22 +11,22 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -39,32 +37,46 @@ class PostControllerTest extends RestDocsTest { @DisplayName("게시글 생성") void createPost() throws Exception { //given - CreatePostRequest request = new CreatePostRequest( - "제목", - List.of(new VoteRequestDto("https://image.com/1"), new VoteRequestDto("https://image.com/2")) + MockMultipartFile description = new MockMultipartFile( + "description", + null, + null, + "게시물 설명".getBytes(StandardCharsets.UTF_8) + ); + MockMultipartFile file1 = new MockMultipartFile( + "files", + "image.jpg", + MediaType.IMAGE_JPEG_VALUE, + "".getBytes(StandardCharsets.UTF_8) + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", + "image.png", + MediaType.IMAGE_PNG_VALUE, + "".getBytes(StandardCharsets.UTF_8) ); //when then - mockMvc.perform(post("/posts") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + mockMvc.perform(MockMvcRequestBuilders.multipart("/posts") + .file(description) + .file(file1) + .file(file2) + .contentType(MediaType.MULTIPART_FORM_DATA) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(authorizationHeader()), - requestFields( - fieldWithPath("description") - .type(JsonFieldType.STRING) + requestParts( + partWithName("description") .description("설명") + .attributes(key("type").value("String")) .attributes(constraints("1~200자 사이")), - fieldWithPath("votes") - .type(JsonFieldType.ARRAY) - .description("투표 후보") - .attributes(constraints("최소 2개")), - fieldWithPath("votes[].imageUrl") - .type(JsonFieldType.STRING) - .description("투표 후보 이미지 URL") - ))); + partWithName("files") + .description("투표 후보 이미지 파일") + .attributes(key("type").value("Array[File]")) + .attributes(constraints("최소 2개")) + ) + )); } @Test @@ -77,12 +89,12 @@ void findPost() throws Exception { new AuthorDto( 1L, "author", - "https://image.com/profile-image" + "https://image.photopic.site/imagePath/profile-image" ), "description", List.of( - new VoteResponseDto(1L, "https://image.com/1", 62.75, true), - new VoteResponseDto(2L, "https://image.com/2", 37.25, false) + new VoteResponseDto(1L, "https://image.photopic.site/imagePath/1", 62.75, true), + new VoteResponseDto(2L, "https://image.photopic.site/imagePath/2", 37.25, false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) @@ -143,7 +155,7 @@ void findMyPost() throws Exception { List.of( new SimplePostResponse( 1L, - "https://image.com/1", + "https://image.photopic.site/imagePath/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) @@ -196,7 +208,7 @@ void findVotedPost() throws Exception { List.of( new SimplePostResponse( 1L, - "https://image.com/1", + "https://image.photopic.site/imagePath/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) diff --git a/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet b/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet index 0a0dd970..46f44bdf 100644 --- a/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet +++ b/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet @@ -8,14 +8,5 @@ |{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/requestParts}} -{{#requestPartFields}} - |{{#fields}} - |{{path}} - |{{description}} - |{{type}} - |{{optional}} - |{{#constraints}}{{.}}{{/constraints}} - {{/fields}} -{{/requestPartFields}} |=== \ No newline at end of file From 12aabf9ca5dac40080e10919c748a3114f46f02d Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 19 Feb 2025 12:13:39 +0900 Subject: [PATCH 067/258] =?UTF-8?q?chore:=20spring=20actuator=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 --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 9eee25e5..c1de2014 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.5' From 58a7cbd8a9f89a863eb103e5b248b98a0b0c81da Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 19 Feb 2025 12:14:42 +0900 Subject: [PATCH 068/258] =?UTF-8?q?feat:=20refresh=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=BF=A0=ED=82=A4=20=EC=A0=80=EC=9E=A5=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=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 --- .../auth/application/JwtService.java | 38 +++++++ .../swyp8team2/auth/domain/RefreshToken.java | 41 ++++++++ .../auth/domain/RefreshTokenRepository.java | 9 ++ .../auth/presentation/AuthController.java | 23 ++++- .../RefreshTokenCookieGenerator.java | 18 ++++ .../auth/presentation/dto/TokenResponse.java | 2 +- .../filter/OAuthLoginSuccessHandler.java | 17 +++- .../common/config/SecurityConfig.java | 21 ++-- .../common/exception/ErrorCode.java | 4 +- .../common/presentation/CustomHeader.java | 4 + .../auth/application/JwtServiceTest.java | 99 +++++++++++++++++++ .../auth/domain/RefreshTokenTest.java | 4 + .../swyp8team2/support/IntegrationTest.java | 5 + 13 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/swyp8team2/auth/application/JwtService.java create mode 100644 src/main/java/com/swyp8team2/auth/domain/RefreshToken.java create mode 100644 src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java create mode 100644 src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java create mode 100644 src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java create mode 100644 src/test/java/com/swyp8team2/support/IntegrationTest.java diff --git a/src/main/java/com/swyp8team2/auth/application/JwtService.java b/src/main/java/com/swyp8team2/auth/application/JwtService.java new file mode 100644 index 00000000..ec5c4486 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/JwtService.java @@ -0,0 +1,38 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.auth.domain.RefreshToken; +import com.swyp8team2.auth.domain.RefreshTokenRepository; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.UnauthorizedException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class JwtService { + + private final JwtProvider jwtProvider; + private final RefreshTokenRepository refreshTokenRepository; + + @Transactional + public TokenPair createToken(long userId) { + TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(userId)); + RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId) + .orElseGet(() -> new RefreshToken(userId, tokenPair.refreshToken())); + refreshTokenRepository.save(refreshToken); + return tokenPair; + } + + @Transactional + public TokenPair reissue(String refreshToken) { + JwtClaim claim = jwtProvider.parseToken(refreshToken); + RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(claim.idAsLong()) + .orElseThrow(() -> new BadRequestException(ErrorCode.REFRESH_TOKEN_NOT_FOUND)); + + TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(claim.idAsLong())); + findRefreshToken.rotate(refreshToken, tokenPair.refreshToken()); + return tokenPair; + } +} diff --git a/src/main/java/com/swyp8team2/auth/domain/RefreshToken.java b/src/main/java/com/swyp8team2/auth/domain/RefreshToken.java new file mode 100644 index 00000000..883bdac3 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/domain/RefreshToken.java @@ -0,0 +1,41 @@ +package com.swyp8team2.auth.domain; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.util.Validator; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + + private String token; + + public RefreshToken(Long userId, String token) { + Validator.validateNull(userId); + Validator.validateEmptyString(token); + this.userId = userId; + this.token = token; + } + + public void rotate(String currentToken, String newToken) { + if (!this.token.equals(currentToken)) { + throw new BadRequestException(ErrorCode.REFRESH_TOKEN_MISMATCHED); + } + Validator.validateEmptyString(newToken); + this.token = newToken; + } +} diff --git a/src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java b/src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java new file mode 100644 index 00000000..b406ecfa --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java @@ -0,0 +1,9 @@ +package com.swyp8team2.auth.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RefreshTokenRepository extends JpaRepository { + Optional findByUserId(Long userId); +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 142f0576..5206f7b7 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -1,12 +1,19 @@ package com.swyp8team2.auth.presentation; +import com.swyp8team2.auth.application.JwtService; +import com.swyp8team2.auth.application.TokenPair; import com.swyp8team2.auth.presentation.dto.TokenResponse; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.common.presentation.CustomHeader; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -15,10 +22,20 @@ @RequestMapping("/auth") public class AuthController { + private final JwtService jwtService; + private final RefreshTokenCookieGenerator refreshTokenCookieGenerator; + @PostMapping("/reissue") public ResponseEntity reissue( - @RequestHeader(CustomHeader.AUTHORIZATION_REFRESH) String refreshToken + @CookieValue(value = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, + HttpServletResponse response ) { - return ResponseEntity.ok(new TokenResponse("accessToken", "refreshToken")); + if (!StringUtils.hasText(refreshToken)) { + throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); + } + TokenPair tokenPair = jwtService.reissue(refreshToken); + Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); + response.addCookie(cookie); + return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java new file mode 100644 index 00000000..d8f83041 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java @@ -0,0 +1,18 @@ +package com.swyp8team2.auth.presentation; + +import com.swyp8team2.common.presentation.CustomHeader; +import jakarta.servlet.http.Cookie; +import org.springframework.stereotype.Component; + +@Component +public class RefreshTokenCookieGenerator { + + public Cookie createCookie(String refreshToken) { + Cookie cookie = new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, refreshToken); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(60 * 60 * 24 * 14); + return cookie; + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java index 0997e640..bb0cf374 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java @@ -1,4 +1,4 @@ package com.swyp8team2.auth.presentation.dto; -public record TokenResponse(String accessToken, String refreshToken) { +public record TokenResponse(String accessToken) { } diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java index 38bb6706..4cfe9c8d 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java @@ -3,13 +3,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.swyp8team2.auth.application.JwtClaim; import com.swyp8team2.auth.application.JwtProvider; +import com.swyp8team2.auth.application.JwtService; import com.swyp8team2.auth.application.TokenPair; import com.swyp8team2.auth.domain.OAuthUser; +import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; import com.swyp8team2.auth.presentation.dto.TokenResponse; +import com.swyp8team2.common.presentation.CustomHeader; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; @@ -22,23 +27,25 @@ @RequiredArgsConstructor public class OAuthLoginSuccessHandler implements AuthenticationSuccessHandler { - private final JwtProvider jwtProvider; private final ObjectMapper objectMapper; + private final JwtService jwtService; + private final RefreshTokenCookieGenerator refreshTokenCookieGenerator; @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication - ) throws IOException, ServletException { + ) throws IOException { OAuthUser oAuthUser = (OAuthUser) authentication.getPrincipal(); + TokenPair tokenPair = jwtService.createToken(oAuthUser.getUserId()); - TokenPair tokenPair = jwtProvider.createToken(JwtClaim.from(oAuthUser.getUserId())); - TokenResponse tokenResponse = new TokenResponse(tokenPair.accessToken(), tokenPair.refreshToken()); + Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); + response.addCookie(cookie); response.setContentType(APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write(objectMapper.writeValueAsString(tokenResponse)); + response.getWriter().write(objectMapper.writeValueAsString(new TokenResponse(tokenPair.accessToken()))); response.getWriter().flush(); } } diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 0b85d30a..228a6f90 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -22,7 +22,6 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @@ -59,7 +58,8 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/images/**", "/js/**", "/favicon.ico", - "/docs/**" + "/docs/**", + "/actuator/health" ); } @@ -90,23 +90,21 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize .requestMatchers(getWhiteList(introspect)).permitAll() - .anyRequest().authenticated() - ) + .anyRequest().authenticated()) .addFilterBefore( new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor()), - UsernamePasswordAuthenticationFilter.class - ) + UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exception -> exception.authenticationEntryPoint( - new JwtAuthenticationEntryPoint(handlerExceptionResolver)) - ) + new JwtAuthenticationEntryPoint(handlerExceptionResolver))) .oauth2Login(oauth -> - oauth.userInfoEndpoint(userInfo -> userInfo.userService(oAuthService)) + oauth.authorizationEndpoint(authorizationEndpointConfig -> + authorizationEndpointConfig.baseUri("/auth/oauth2")) + .userInfoEndpoint(userInfo -> userInfo.userService(oAuthService)) .successHandler(oAuthLoginSuccessHandler) - .failureHandler(oAuthLoginFailureHandler) - ); + .failureHandler(oAuthLoginFailureHandler)); return http.build(); } @@ -118,6 +116,7 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), mvc.pattern("/posts/{postId}/votes/guest/**"), + mvc.pattern("/auth/oauth2") }; } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 71874a69..3465e9a5 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -9,6 +9,8 @@ public enum ErrorCode { //400 USER_NOT_FOUND("존재하지 않는 유저"), INVALID_ARGUMENT("잘못된 파라미터 요청"), + REFRESH_TOKEN_MISMATCHED("리프레시 토큰 불일치"), + REFRESH_TOKEN_NOT_FOUND("리프레시 토큰을 찾을 수 없음"), //401 EXPIRED_TOKEN("토큰 만료"), @@ -18,7 +20,7 @@ public enum ErrorCode { //500 INTERNAL_SERVER_ERROR("서버 내부 오류"), - INVALID_INPUT_VALUE("잘못된 입력 값"), ; + INVALID_INPUT_VALUE("잘못된 입력 값"),; private final String message; } diff --git a/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java index 846ed7e3..903322ea 100644 --- a/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java +++ b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java @@ -4,4 +4,8 @@ public abstract class CustomHeader { public static final String GUEST_ID = "Guest-Id"; public static final String AUTHORIZATION_REFRESH = "Authorization-Refresh"; + + public static class CustomCookie{ + public static final String REFRESH_TOKEN = "refreshToken"; + } } diff --git a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java new file mode 100644 index 00000000..0c54f433 --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java @@ -0,0 +1,99 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.auth.domain.RefreshToken; +import com.swyp8team2.auth.domain.RefreshTokenRepository; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.support.IntegrationTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +class JwtServiceTest extends IntegrationTest { + + @Autowired + JwtService jwtService; + + @Autowired + RefreshTokenRepository refreshTokenRepository; + + @MockitoBean + JwtProvider jwtProvider; + + @Test + @DisplayName("새 토큰 발급하고 refresh token을 db에 저장해야 함") + void createToken() throws Exception { + //given + long givenUserId = 1L; + TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); + given(jwtProvider.createToken(any(JwtClaim.class))) + .willReturn(expectedTokenPair); + + //when + TokenPair tokenPair = jwtService.createToken(givenUserId); + + //then + RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(givenUserId).get(); + assertThat(tokenPair).isEqualTo(expectedTokenPair); + assertThat(findRefreshToken.getToken()).isEqualTo(expectedTokenPair.refreshToken()); + } + + @Test + @DisplayName("토큰 재발급하고 refresh token을 db에 갱신해야 함") + void reissue() throws Exception { + //given + long givenUserId = 1L; + String givenRefreshToken = "refreshToken"; + String newRefreshToken = "newRefreshToken"; + TokenPair expectedTokenPair = new TokenPair("newAccessToken", newRefreshToken); + given(jwtProvider.parseToken(any(String.class))) + .willReturn(new JwtClaim(givenUserId)); + given(jwtProvider.createToken(any(JwtClaim.class))) + .willReturn(expectedTokenPair); + refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); + + //when + TokenPair tokenPair = jwtService.reissue(givenRefreshToken); + + //then + RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(givenUserId).get(); + assertThat(tokenPair).isEqualTo(expectedTokenPair); + assertThat(findRefreshToken.getToken()).isEqualTo(newRefreshToken); + } + + @Test + @DisplayName("토큰 재발급 - refresh token이 존재하지 않는 경우") + void reissue_refreshTokenNotFound() throws Exception { + //given + given(jwtProvider.parseToken(any(String.class))) + .willReturn(new JwtClaim(1L)); + + //when + assertThatThrownBy(() -> jwtService.reissue("refreshToken")) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.REFRESH_TOKEN_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("토큰 재발급 - refresh token이 일치하지 않은 경우") + void reissue_refreshTokenMismatched() throws Exception { + //given + long givenUserId = 1L; + String givenRefreshToken = "refreshToken"; + given(jwtProvider.parseToken(any(String.class))) + .willReturn(new JwtClaim(givenUserId)); + given(jwtProvider.createToken(any(JwtClaim.class))) + .willReturn(new TokenPair("accessToken", "newRefreshToken")); + refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); + + //when + assertThatThrownBy(() -> jwtService.reissue("mismatchToken")) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.REFRESH_TOKEN_MISMATCHED.getMessage()); + } +} diff --git a/src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java b/src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java new file mode 100644 index 00000000..dfac5d6a --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class RefreshTokenTest { + +} diff --git a/src/test/java/com/swyp8team2/support/IntegrationTest.java b/src/test/java/com/swyp8team2/support/IntegrationTest.java new file mode 100644 index 00000000..ac2c0f6f --- /dev/null +++ b/src/test/java/com/swyp8team2/support/IntegrationTest.java @@ -0,0 +1,5 @@ +package com.swyp8team2.support; + + +public class InteggrationTest { +} From ec011beac13009dfc899c4ae82b6f96e15af9e86 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 20 Feb 2025 10:14:20 +0900 Subject: [PATCH 069/258] =?UTF-8?q?test:=20refresh=20token=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/domain/RefreshTokenTest.java | 39 ++++++++- .../auth/presentation/AuthControllerTest.java | 79 ++++++++++++++++--- .../swyp8team2/support/IntegrationTest.java | 6 +- .../com/swyp8team2/support/RestDocsTest.java | 8 +- .../com/swyp8team2/support/WebUnitTest.java | 25 ++++++ 5 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 src/test/java/com/swyp8team2/support/WebUnitTest.java diff --git a/src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java b/src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java index dfac5d6a..29c985c8 100644 --- a/src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java +++ b/src/test/java/com/swyp8team2/auth/domain/RefreshTokenTest.java @@ -1,4 +1,39 @@ -import static org.junit.jupiter.api.Assertions.*; +package com.swyp8team2.auth.domain; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + class RefreshTokenTest { - + + @Test + @DisplayName("Refresh Token 교체") + void rotate() throws Exception { + //given + String token = "refreshToken"; + String newToken = "newRefreshToken"; + RefreshToken refreshToken = new RefreshToken(1L, token); + + //when + refreshToken.rotate(token, newToken); + + //then + assertThat(refreshToken.getToken()).isEqualTo(newToken); + } + + @Test + @DisplayName("Refresh Token 교체 - 현재 토큰과 다른 경우") + void rotate_() throws Exception { + //given + String newToken = "newRefreshToken"; + RefreshToken refreshToken = new RefreshToken(1L, "refreshToken"); + + //when then + assertThatThrownBy(() -> refreshToken.rotate("mismatchToken", newToken)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.REFRESH_TOKEN_MISMATCHED.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index df7b8d34..20db8312 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -1,23 +1,27 @@ package com.swyp8team2.auth.presentation; +import com.swyp8team2.auth.application.TokenPair; import com.swyp8team2.auth.presentation.dto.TokenResponse; +import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.ErrorResponse; import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.RestDocsTest; +import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; import org.springframework.security.test.context.support.WithAnonymousUser; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class AuthControllerTest extends RestDocsTest { @@ -27,21 +31,74 @@ class AuthControllerTest extends RestDocsTest { @DisplayName("토큰 재발급") void reissue() throws Exception { //given - TokenResponse response = new TokenResponse("accessToken", "refreshToken"); + String newRefreshToken = "newRefreshToken"; + given(jwtService.reissue(anyString())) + .willReturn(new TokenPair("accessToken", newRefreshToken)); + TokenResponse response = new TokenResponse("accessToken"); //when then mockMvc.perform(post("/auth/reissue") - .header(CustomHeader.AUTHORIZATION_REFRESH, "refreshToken")) + .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken"))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andExpect(cookie().value(CustomHeader.CustomCookie.REFRESH_TOKEN, newRefreshToken)) + .andExpect(cookie().httpOnly(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().path(CustomHeader.CustomCookie.REFRESH_TOKEN, "/auth/reissue")) + .andExpect(cookie().secure(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().attribute(CustomHeader.CustomCookie.REFRESH_TOKEN, "SameSite", "None")) + .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 60 * 60 * 24 * 14)) .andDo(restDocs.document( - requestHeaders( - headerWithName(CustomHeader.AUTHORIZATION_REFRESH).description("리프레시 토큰") + requestCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") + ), + responseCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("새 리프레시 토큰") ), responseFields( - fieldWithPath("accessToken").description("액세스 토큰"), - fieldWithPath("refreshToken").description("리프레시 토큰") + fieldWithPath("accessToken").description("새 액세스 토큰") ) )); } + + @Test + @DisplayName("토큰 재발급 - 리프레시 토큰 헤더 없는 경우") + void reissue_invalidRefreshTokenHeader() throws Exception { + //given + ErrorResponse response = new ErrorResponse(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); + + //when then + mockMvc.perform(post("/auth/reissue")) + .andExpect(status().isBadRequest()) + .andExpect(content().json(objectMapper.writeValueAsString(response))); + } + + @Test + @DisplayName("토큰 재발급 - 리프레시 토큰 헤더가 db에 없는 경우") + void reissue_refreshTokenNotFound() throws Exception { + //given + ErrorResponse response = new ErrorResponse(ErrorCode.REFRESH_TOKEN_NOT_FOUND); + given(jwtService.reissue(anyString())) + .willThrow(new BadRequestException(ErrorCode.REFRESH_TOKEN_NOT_FOUND)); + + //when then + mockMvc.perform(post("/auth/reissue") + .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken"))) + .andExpect(status().isBadRequest()) + .andExpect(content().json(objectMapper.writeValueAsString(response))); + } + + @Test + @DisplayName("토큰 재발급 - 리프레시 토큰 헤더가 db에 있는 값과 일치하지 않은 경우") + void reissue_refreshTokenMismatched() throws Exception { + //given + ErrorResponse response = new ErrorResponse(ErrorCode.REFRESH_TOKEN_MISMATCHED); + given(jwtService.reissue(anyString())) + .willThrow(new BadRequestException(ErrorCode.REFRESH_TOKEN_MISMATCHED)); + + //when then + mockMvc.perform(post("/auth/reissue") + .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken"))) + .andExpect(status().isBadRequest()) + .andExpect(content().json(objectMapper.writeValueAsString(response))); + } } diff --git a/src/test/java/com/swyp8team2/support/IntegrationTest.java b/src/test/java/com/swyp8team2/support/IntegrationTest.java index ac2c0f6f..357ab8e4 100644 --- a/src/test/java/com/swyp8team2/support/IntegrationTest.java +++ b/src/test/java/com/swyp8team2/support/IntegrationTest.java @@ -1,5 +1,9 @@ package com.swyp8team2.support; +import jakarta.transaction.Transactional; +import org.springframework.boot.test.context.SpringBootTest; -public class InteggrationTest { +@Transactional +@SpringBootTest +public abstract class IntegrationTest { } diff --git a/src/test/java/com/swyp8team2/support/RestDocsTest.java b/src/test/java/com/swyp8team2/support/RestDocsTest.java index 1bce6011..b6ce6487 100644 --- a/src/test/java/com/swyp8team2/support/RestDocsTest.java +++ b/src/test/java/com/swyp8team2/support/RestDocsTest.java @@ -24,13 +24,7 @@ @AutoConfigureRestDocs @Import({RestDocsConfiguration.class, TestSecurityConfig.class}) @ExtendWith(RestDocumentationExtension.class) -public abstract class RestDocsTest { - - @Autowired - protected MockMvc mockMvc; - - @Autowired - protected ObjectMapper objectMapper; +public abstract class RestDocsTest extends WebUnitTest { @Autowired protected RestDocumentationResultHandler restDocs; diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java new file mode 100644 index 00000000..c372df20 --- /dev/null +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -0,0 +1,25 @@ +package com.swyp8team2.support; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.swyp8team2.auth.application.JwtService; +import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@Import(RefreshTokenCookieGenerator.class) +public abstract class WebUnitTest { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @MockitoBean + protected JwtService jwtService; + + @Autowired + protected RefreshTokenCookieGenerator refreshTokenCookieGenerator; +} From 4c4f2b49a1661a4b42c4ac8694909a7283629997 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 20 Feb 2025 10:36:39 +0900 Subject: [PATCH 070/258] =?UTF-8?q?feat:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/auth/presentation/AuthController.java | 7 +++++-- .../auth/presentation/RefreshTokenCookieGenerator.java | 3 ++- .../java/com/swyp8team2/common/exception/ErrorCode.java | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 5206f7b7..78685995 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -4,6 +4,7 @@ import com.swyp8team2.auth.application.JwtService; import com.swyp8team2.auth.application.TokenPair; import com.swyp8team2.auth.presentation.dto.TokenResponse; +import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.common.presentation.CustomHeader; @@ -17,6 +18,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Objects; + @RestController @RequiredArgsConstructor @RequestMapping("/auth") @@ -30,8 +33,8 @@ public ResponseEntity reissue( @CookieValue(value = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, HttpServletResponse response ) { - if (!StringUtils.hasText(refreshToken)) { - throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); + if (Objects.isNull(refreshToken)) { + throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); } TokenPair tokenPair = jwtService.reissue(refreshToken); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); diff --git a/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java index d8f83041..5668a80d 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java +++ b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java @@ -11,7 +11,8 @@ public Cookie createCookie(String refreshToken) { Cookie cookie = new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, refreshToken); cookie.setHttpOnly(true); cookie.setSecure(true); - cookie.setPath("/"); + cookie.setAttribute("SameSite", "None"); + cookie.setPath("/auth/reissue"); cookie.setMaxAge(60 * 60 * 24 * 14); return cookie; } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 3465e9a5..7377757e 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -11,6 +11,7 @@ public enum ErrorCode { INVALID_ARGUMENT("잘못된 파라미터 요청"), REFRESH_TOKEN_MISMATCHED("리프레시 토큰 불일치"), REFRESH_TOKEN_NOT_FOUND("리프레시 토큰을 찾을 수 없음"), + INVALID_REFRESH_TOKEN_HEADER("잘못된 리프레시 토큰 헤더"), //401 EXPIRED_TOKEN("토큰 만료"), From 88c1ff2e7b02b111ecdd56f470deceb019bf1106 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 20 Feb 2025 10:37:27 +0900 Subject: [PATCH 071/258] =?UTF-8?q?docs:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 48 ++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 45ebaba0..67de2a82 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -1,9 +1,51 @@ [[인증-API]] == 인증 API +[[카카오-로그인]] +=== 카카오 로그인 + +==== HTTP request +---- +GET /auth/oauth2/kakao HTTP/1.1 +---- + +==== Curl request +---- +$ curl 'http://localhost:8080/auth/oauth2/kakao' -i -X GET +---- + +==== HTTP response +---- +HTTP/1.1 200 OK +Set-Cookie: refreshToken=refreshToken; Path=/auth/reissue; Max-Age=1209600; Expires=Thu, 06 Mar 2025 01:33:19 GMT; Secure; HttpOnly +Content-Type: application/json + +{ + "accessToken" : "accessToken" +} +---- + +==== Response cookies +|=== +|Name|Description + +|`+refreshToken+` +|리프레시 토큰 + +|=== + +==== Response fields +|=== +|필드명|타입|필수값|설명 + + |`+accessToken+` + |`+String+` + |true + |액세스 토큰 + +|=== + [[토큰-재발급]] === 토큰 재발급 -이 부분 프런트와 다시 고민해봐야 함 - -operation::auth-controller-test/reissue[snippets='http-request,curl-request,request-headers,http-response,response-fields'] +operation::auth-controller-test/reissue[snippets='http-request,curl-request,request-cookies,http-response,response-cookies,response-fields'] From faac27116f5d37c3974ad36d7474774d1f95ce19 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 20 Feb 2025 20:40:52 +0900 Subject: [PATCH 072/258] =?UTF-8?q?docs:=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20API=20=EC=9B=90=EB=B3=B5=20=EB=B0=8F=20vot?= =?UTF-8?q?es=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 2 +- .../post/presentation/PostController.java | 27 ++++--- .../post/presentation/dto/VoteRequestDto.java | 6 +- .../post/presentation/PostControllerTest.java | 70 ++++++++----------- 4 files changed, 49 insertions(+), 56 deletions(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 5493208f..e7262787 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -4,7 +4,7 @@ [[게시글-작성]] === 게시글 작성 -operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-parts,http-response'] +operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게시글-상세-조회]] === 게시글 상세 조회 diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index b2034973..01945f1b 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -3,16 +3,22 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.VoteResponseDto; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.util.List; @@ -22,10 +28,9 @@ @RequestMapping("/posts") public class PostController { - @PostMapping(value ="", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping("") public ResponseEntity createPost( - @Valid @RequestPart("description") String description, - @Valid @RequestPart("files") List files, + @Valid @RequestBody CreatePostRequest request, @AuthenticationPrincipal UserInfo userInfo ) { return ResponseEntity.ok().build(); @@ -38,12 +43,12 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh new AuthorDto( 1L, "author", - "https://image.photopic.site/imagePath/profile-image" + "https://image.photopic.site/profile-image" ), "description", List.of( - new VoteResponseDto(1L, "https://image.photopic.site/imagePath/1", 62.75, true), - new VoteResponseDto(2L, "https://image.photopic.site/imagePath/2", 37.25, false) + new VoteResponseDto(1L, "https://image.photopic.site/1", 62.75, true), + new VoteResponseDto(2L, "https://image.photopic.site/2", 37.25, false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) @@ -70,7 +75,7 @@ public ResponseEntity> findMyPos List.of( new SimplePostResponse( 1L, - "https://image.photopic.site/imagePath/1", + "https://image.photopic.site/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) @@ -90,7 +95,7 @@ public ResponseEntity> findVoted List.of( new SimplePostResponse( 1L, - "https://image.photopic.site/imagePath/1", + "https://image.photopic.site/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java index 434726e6..d532cdda 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java @@ -1,9 +1,9 @@ package com.swyp8team2.post.presentation.dto; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; public record VoteRequestDto( - @NotEmpty - String imageUrl + @NotNull + Long imageFileId ) { } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index c5f2ae96..03258d41 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -2,8 +2,10 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; +import com.swyp8team2.post.presentation.dto.VoteRequestDto; import com.swyp8team2.post.presentation.dto.VoteResponseDto; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; @@ -11,22 +13,22 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.security.test.context.support.WithAnonymousUser; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.*; -import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -37,46 +39,32 @@ class PostControllerTest extends RestDocsTest { @DisplayName("게시글 생성") void createPost() throws Exception { //given - MockMultipartFile description = new MockMultipartFile( - "description", - null, - null, - "게시물 설명".getBytes(StandardCharsets.UTF_8) - ); - MockMultipartFile file1 = new MockMultipartFile( - "files", - "image.jpg", - MediaType.IMAGE_JPEG_VALUE, - "".getBytes(StandardCharsets.UTF_8) - ); - MockMultipartFile file2 = new MockMultipartFile( - "files", - "image.png", - MediaType.IMAGE_PNG_VALUE, - "".getBytes(StandardCharsets.UTF_8) + CreatePostRequest request = new CreatePostRequest( + "제목", + List.of(new VoteRequestDto(1L), new VoteRequestDto(2L)) ); //when then - mockMvc.perform(MockMvcRequestBuilders.multipart("/posts") - .file(description) - .file(file1) - .file(file2) - .contentType(MediaType.MULTIPART_FORM_DATA) + mockMvc.perform(post("/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(authorizationHeader()), - requestParts( - partWithName("description") + requestFields( + fieldWithPath("description") + .type(JsonFieldType.STRING) .description("설명") - .attributes(key("type").value("String")) .attributes(constraints("1~200자 사이")), - partWithName("files") - .description("투표 후보 이미지 파일") - .attributes(key("type").value("Array[File]")) - .attributes(constraints("최소 2개")) - ) - )); + fieldWithPath("votes") + .type(JsonFieldType.ARRAY) + .description("투표 후보") + .attributes(constraints("최소 2개")), + fieldWithPath("votes[].imageFileId") + .type(JsonFieldType.NUMBER) + .description("투표 후보 이미지 ID") + ))); } @Test @@ -89,12 +77,12 @@ void findPost() throws Exception { new AuthorDto( 1L, "author", - "https://image.photopic.site/imagePath/profile-image" + "https://image.photopic.site/profile-image" ), "description", List.of( - new VoteResponseDto(1L, "https://image.photopic.site/imagePath/1", 62.75, true), - new VoteResponseDto(2L, "https://image.photopic.site/imagePath/2", 37.25, false) + new VoteResponseDto(1L, "https://image.photopic.site/1", 62.75, true), + new VoteResponseDto(2L, "https://image.photopic.site/2", 37.25, false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) @@ -155,7 +143,7 @@ void findMyPost() throws Exception { List.of( new SimplePostResponse( 1L, - "https://image.photopic.site/imagePath/1", + "https://image.photopic.site/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) @@ -208,7 +196,7 @@ void findVotedPost() throws Exception { List.of( new SimplePostResponse( 1L, - "https://image.photopic.site/imagePath/1", + "https://image.photopic.site/1", "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ) From ce9a71683dcc71f7739f6d07c08815ea4b719024 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 21 Feb 2025 02:31:19 +0900 Subject: [PATCH 073/258] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 + src/docs/asciidoc/images.adoc | 0 .../swyp8team2/common/domain/BaseEntity.java | 27 ++++ .../common/exception/ErrorCode.java | 8 +- .../ServiceUnavailableException.java | 8 + .../com/swyp8team2/common/util/DateTime.java | 13 ++ .../image/application/ImageService.java | 38 +++++ .../image/application/R2Storage.java | 151 ++++++++++++++++++ .../com/swyp8team2/image/config/S3Config.java | 38 +++++ .../swyp8team2/image/domain/ImageFile.java | 37 +++++ .../image/domain/ImageFileRepository.java | 8 + .../image/presentation/ImageController.java | 27 ++++ .../image/presentation/dto/ImageFileDto.java | 7 + .../presentation/dto/ImageFileResponse.java | 6 + .../swyp8team2/image/util/FileValidator.java | 49 ++++++ 15 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 src/docs/asciidoc/images.adoc create mode 100644 src/main/java/com/swyp8team2/common/domain/BaseEntity.java create mode 100644 src/main/java/com/swyp8team2/common/exception/ServiceUnavailableException.java create mode 100644 src/main/java/com/swyp8team2/common/util/DateTime.java create mode 100644 src/main/java/com/swyp8team2/image/application/ImageService.java create mode 100644 src/main/java/com/swyp8team2/image/application/R2Storage.java create mode 100644 src/main/java/com/swyp8team2/image/config/S3Config.java create mode 100644 src/main/java/com/swyp8team2/image/domain/ImageFile.java create mode 100644 src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java create mode 100644 src/main/java/com/swyp8team2/image/presentation/ImageController.java create mode 100644 src/main/java/com/swyp8team2/image/presentation/dto/ImageFileDto.java create mode 100644 src/main/java/com/swyp8team2/image/presentation/dto/ImageFileResponse.java create mode 100644 src/main/java/com/swyp8team2/image/util/FileValidator.java diff --git a/build.gradle b/build.gradle index 9eee25e5..b40441a3 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,10 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // image + implementation 'software.amazon.awssdk:s3:2.30.18' + implementation 'org.imgscalr:imgscalr-lib:4.2' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/docs/asciidoc/images.adoc b/src/docs/asciidoc/images.adoc new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/swyp8team2/common/domain/BaseEntity.java b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java new file mode 100644 index 00000000..79ec435e --- /dev/null +++ b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java @@ -0,0 +1,27 @@ +package com.swyp8team2.common.domain; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public abstract class BaseEntity { + + private String createdBy; + + @CreatedDate + private LocalDateTime createdAt; + + private String updatedBy; + + @LastModifiedDate + private LocalDateTime updatedAt; + + private boolean deleted = false; + private LocalDateTime deletedAt; +} diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 71874a69..43a0977d 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -9,6 +9,9 @@ public enum ErrorCode { //400 USER_NOT_FOUND("존재하지 않는 유저"), INVALID_ARGUMENT("잘못된 파라미터 요청"), + MISSING_FILE_EXTENSION("확장자가 누락됨"), + UNSUPPORTED_FILE_EXTENSION("지원하지 않는 확장자"), + EXCEED_MAX_FILE_SIZE("파일 크기 초과"), //401 EXPIRED_TOKEN("토큰 만료"), @@ -18,7 +21,10 @@ public enum ErrorCode { //500 INTERNAL_SERVER_ERROR("서버 내부 오류"), - INVALID_INPUT_VALUE("잘못된 입력 값"), ; + INVALID_INPUT_VALUE("잘못된 입력 값"), + + //503 + SERVICE_UNAVAILABLE("서비스 이용 불가"); private final String message; } diff --git a/src/main/java/com/swyp8team2/common/exception/ServiceUnavailableException.java b/src/main/java/com/swyp8team2/common/exception/ServiceUnavailableException.java new file mode 100644 index 00000000..3b15df8c --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/ServiceUnavailableException.java @@ -0,0 +1,8 @@ +package com.swyp8team2.common.exception; + +public class ServiceUnavailableException extends ApplicationException { + + public ServiceUnavailableException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/swyp8team2/common/util/DateTime.java b/src/main/java/com/swyp8team2/common/util/DateTime.java new file mode 100644 index 00000000..bc6b1f36 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/util/DateTime.java @@ -0,0 +1,13 @@ +package com.swyp8team2.common.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class DateTime { + + public static String getCurrentTimestamp() { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + return now.format(formatter); + } +} diff --git a/src/main/java/com/swyp8team2/image/application/ImageService.java b/src/main/java/com/swyp8team2/image/application/ImageService.java new file mode 100644 index 00000000..b5468f29 --- /dev/null +++ b/src/main/java/com/swyp8team2/image/application/ImageService.java @@ -0,0 +1,38 @@ +package com.swyp8team2.image.application; + +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; +import com.swyp8team2.image.presentation.dto.ImageFileDto; +import com.swyp8team2.image.presentation.dto.ImageFileResponse; +import com.swyp8team2.image.util.FileValidator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ImageService { + + private final R2Storage r2Storage; + private final FileValidator fileValidator; + private final ImageFileRepository imageFileRepository; + + public ImageFileResponse uploadImageFile(MultipartFile... files) { + fileValidator.validate(files); + List imageFiles = r2Storage.uploadImageFile(files); + List imageFileIds = imageFiles.stream() + .map(this::createImageFile) + .collect(Collectors.toList()); + return new ImageFileResponse(imageFileIds); + } + + public Long createImageFile(ImageFileDto imageFiledto) { + ImageFile imageFile = imageFileRepository.save(ImageFile.create(imageFiledto)); + return imageFile.getId(); + } +} diff --git a/src/main/java/com/swyp8team2/image/application/R2Storage.java b/src/main/java/com/swyp8team2/image/application/R2Storage.java new file mode 100644 index 00000000..40f926cf --- /dev/null +++ b/src/main/java/com/swyp8team2/image/application/R2Storage.java @@ -0,0 +1,151 @@ +package com.swyp8team2.image.application; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.ServiceUnavailableException; +import com.swyp8team2.common.util.DateTime; +import com.swyp8team2.image.presentation.dto.ImageFileDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.imgscalr.Scalr; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +@RequiredArgsConstructor +public class R2Storage { + + @Value("${file.endpoint}") + private String imageDomainUrl; + + @Value("${r2.bucket.name}") + private String bucketName; + + @Value("${r2.bucket.path}") + private String filePath; + + @Value("${r2.bucket.resize-path}") + private String resizedFilePath; + + @Value("${r2.bucket.resize-height}") + private int resizeHeight; + + private final S3Client s3Client; + + public List uploadImageFile(MultipartFile... files) { + List imageFiles = new ArrayList<>(); + List tempFiles = new ArrayList<>(); + try { + for (int i = 0; i < files.length; i++) { + MultipartFile file = files[i]; + String originFileName = file.getOriginalFilename(); + String realFileName = getRealFileName(originFileName, filePath, i); + File tempFile = File.createTempFile("upload_", "_" + originFileName); + file.transferTo(tempFile); + + int splitIndex = originFileName.lastIndexOf("/"); + String imageType = originFileName.substring(splitIndex + 1).toLowerCase(); + + File originFile = s3PutObject(tempFile, realFileName, imageType); + String imageUrl = imageDomainUrl + realFileName; + String resizeImageUrl = resizeImage(tempFile, realFileName, resizeHeight); + + imageFiles.add(new ImageFileDto(originFileName, imageUrl, resizeImageUrl)); + tempFiles.add(originFile); + } + + return imageFiles; + } catch (IOException e) { + log.error("Failed to create temp file", e); + throw new ServiceUnavailableException(ErrorCode.SERVICE_UNAVAILABLE); + } finally { + tempFiles.forEach(this::deleteTempFile); + } + } + + private String resizeImage(File file, String realFileName, int targetHeight) { + try { + BufferedImage srcImage = ImageIO.read(file); + BufferedImage resizedImage = highQualityResize(srcImage, targetHeight); + + int splitIndex = realFileName.lastIndexOf("/") + 1; + realFileName = realFileName.substring(splitIndex); + String dstKey = resizedFilePath + realFileName; + String imageType = realFileName.substring(realFileName.lastIndexOf(".") + 1).toLowerCase(); + + File tempFile = File.createTempFile("resized_", "." + imageType); + ImageIO.write(resizedImage, imageType, tempFile); + + s3PutObject(tempFile, dstKey, imageType); + deleteTempFile(tempFile); + + return imageDomainUrl + dstKey; + } catch (IOException e) { + log.error("Failed to create temp file", e); + throw new ServiceUnavailableException(ErrorCode.SERVICE_UNAVAILABLE); + } + + } + + private BufferedImage highQualityResize(BufferedImage originalImage, int targetHeight) { + int originalWidth = originalImage.getWidth(); + int originalHeight = originalImage.getHeight(); + + double scale = (double) targetHeight / originalHeight; + int targetWidth = (int) (originalWidth * scale); + + return Scalr.resize(originalImage, Scalr.Method.QUALITY, Scalr.Mode.FIT_EXACT, targetWidth, targetHeight); + } + + private File s3PutObject(File file, String realFileName, String imageType) { + Map metadata = getMetadataMap(imageType); + PutObjectRequest objectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .metadata(metadata) + .key(realFileName) + .build(); + + s3Client.putObject(objectRequest, RequestBody.fromFile(file)); + return file; + } + + private Map getMetadataMap(String imageType) { + Map metadata = new HashMap<>(); + switch (imageType) { + case "png" -> { + metadata.put("Content-Type", "image/png"); + } case "gif" -> { + metadata.put("Content-Type", "image/gif"); + } case "webp" -> { + metadata.put("Content-Type", "image/webp"); + } default -> { + metadata.put("Content-Type", "image/jpeg"); + } + } + return metadata; + } + + private String getRealFileName(String originFileName, String filePath, int sequence) { + String objectType = originFileName.substring(originFileName.lastIndexOf(".")).toLowerCase(); + return filePath + DateTime.getCurrentTimestamp() + sequence + objectType; + } + + private void deleteTempFile(File tempFile) { + if (!tempFile.delete()) { + log.error("Failed to delete temp file: {}", tempFile.getName()); + } + } +} diff --git a/src/main/java/com/swyp8team2/image/config/S3Config.java b/src/main/java/com/swyp8team2/image/config/S3Config.java new file mode 100644 index 00000000..4238db0c --- /dev/null +++ b/src/main/java/com/swyp8team2/image/config/S3Config.java @@ -0,0 +1,38 @@ +package com.swyp8team2.image.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; + +import java.net.URI; + +@Configuration +public class S3Config { + + @Value("${r2.access-key}") + private String r2AccessKey; + + @Value("${r2.secret-key}") + private String r2SecretKey; + + @Value("${r2.endpoint}") + private String endpoint; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.of("auto")) + .endpointOverride(URI.create(endpoint)) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(r2AccessKey, r2SecretKey))) + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(true) + .build()) + .build(); + } +} diff --git a/src/main/java/com/swyp8team2/image/domain/ImageFile.java b/src/main/java/com/swyp8team2/image/domain/ImageFile.java new file mode 100644 index 00000000..12ce861c --- /dev/null +++ b/src/main/java/com/swyp8team2/image/domain/ImageFile.java @@ -0,0 +1,37 @@ +package com.swyp8team2.image.domain; + +import com.swyp8team2.common.domain.BaseEntity; +import com.swyp8team2.image.presentation.dto.ImageFileDto; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "image_files") +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class ImageFile extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String originImageName; + + @Column(nullable = false, length = 200) + private String imageUrl; + + @Column(nullable = false, length = 200) + private String thumbnailUrl; + + private ImageFile(String originImageName, String imageUrl, String thumbnailUrl) { + this.originImageName = originImageName; + this.imageUrl = imageUrl; + this.thumbnailUrl = thumbnailUrl; + } + + public static ImageFile create(ImageFileDto dto) { + return new ImageFile(dto.originFileName(), dto.imageUrl(), dto.thumbnailUrl()); + } +} diff --git a/src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java b/src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java new file mode 100644 index 00000000..c0ae2259 --- /dev/null +++ b/src/main/java/com/swyp8team2/image/domain/ImageFileRepository.java @@ -0,0 +1,8 @@ +package com.swyp8team2.image.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ImageFileRepository extends JpaRepository { +} diff --git a/src/main/java/com/swyp8team2/image/presentation/ImageController.java b/src/main/java/com/swyp8team2/image/presentation/ImageController.java new file mode 100644 index 00000000..6c1912d0 --- /dev/null +++ b/src/main/java/com/swyp8team2/image/presentation/ImageController.java @@ -0,0 +1,27 @@ +package com.swyp8team2.image.presentation; + +import com.swyp8team2.image.application.ImageService; +import com.swyp8team2.image.presentation.dto.ImageFileResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/image") +public class ImageController { + + private final ImageService r2Service; + + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity createImageFile(@RequestPart("files") MultipartFile... files) { + ImageFileResponse response = r2Service.uploadImageFile(files); + return ResponseEntity.ok(response); + } + +} diff --git a/src/main/java/com/swyp8team2/image/presentation/dto/ImageFileDto.java b/src/main/java/com/swyp8team2/image/presentation/dto/ImageFileDto.java new file mode 100644 index 00000000..58ef06e5 --- /dev/null +++ b/src/main/java/com/swyp8team2/image/presentation/dto/ImageFileDto.java @@ -0,0 +1,7 @@ +package com.swyp8team2.image.presentation.dto; + +public record ImageFileDto(String originFileName, + String imageUrl, + String thumbnailUrl) { + +} diff --git a/src/main/java/com/swyp8team2/image/presentation/dto/ImageFileResponse.java b/src/main/java/com/swyp8team2/image/presentation/dto/ImageFileResponse.java new file mode 100644 index 00000000..1cae47a5 --- /dev/null +++ b/src/main/java/com/swyp8team2/image/presentation/dto/ImageFileResponse.java @@ -0,0 +1,6 @@ +package com.swyp8team2.image.presentation.dto; + +import java.util.List; + +public record ImageFileResponse(List imageFileId) { +} diff --git a/src/main/java/com/swyp8team2/image/util/FileValidator.java b/src/main/java/com/swyp8team2/image/util/FileValidator.java new file mode 100644 index 00000000..32a00c82 --- /dev/null +++ b/src/main/java/com/swyp8team2/image/util/FileValidator.java @@ -0,0 +1,49 @@ +package com.swyp8team2.image.util; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class FileValidator { + + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + + private final Set allowedExtensions; + + public FileValidator(@Value("${file.allowed-extensions}") String allowedExtensionsConfig) { + this.allowedExtensions = Arrays.stream(allowedExtensionsConfig.split(",")) + .map(String::trim) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + } + + public void validate(MultipartFile... files) { + Arrays.stream(files) + .forEach(this::validate); + } + + private void validate(MultipartFile file) { + if (file.getSize() > MAX_FILE_SIZE) { + throw new BadRequestException(ErrorCode.EXCEED_MAX_FILE_SIZE); + } + + String originalFilename = file.getOriginalFilename(); + String ext = Optional.of(originalFilename) + .filter(name -> name.contains(".")) + .map(name -> name.substring(name.lastIndexOf('.') + 1)) + .orElseThrow(() -> new BadRequestException(ErrorCode.MISSING_FILE_EXTENSION)) + .toLowerCase(); + + if (!allowedExtensions.contains(ext)) { + throw new BadRequestException(ErrorCode.UNSUPPORTED_FILE_EXTENSION); + } + } +} \ No newline at end of file From fe173b24cd83099382ce94a81767cc1d2baac80d Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 21 Feb 2025 02:32:14 +0900 Subject: [PATCH 074/258] =?UTF-8?q?test:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image/application/ImageServiceTest.java | 169 ++++++++++++++++++ .../presentation/ImageControllerTest.java | 96 ++++++++++ .../image/util/FileValidatorTest.java | 102 +++++++++++ 3 files changed, 367 insertions(+) create mode 100644 src/test/java/com/swyp8team2/image/application/ImageServiceTest.java create mode 100644 src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java create mode 100644 src/test/java/com/swyp8team2/image/util/FileValidatorTest.java diff --git a/src/test/java/com/swyp8team2/image/application/ImageServiceTest.java b/src/test/java/com/swyp8team2/image/application/ImageServiceTest.java new file mode 100644 index 00000000..b45bed7e --- /dev/null +++ b/src/test/java/com/swyp8team2/image/application/ImageServiceTest.java @@ -0,0 +1,169 @@ +package com.swyp8team2.image.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; +import com.swyp8team2.image.presentation.dto.ImageFileDto; +import com.swyp8team2.image.presentation.dto.ImageFileResponse; +import com.swyp8team2.image.util.FileValidator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ImageServiceTest { + + @Mock + private R2Storage r2Storage; + + @Mock + private FileValidator fileValidator; + + @Mock + private ImageFileRepository imageFileRepository; + + @InjectMocks + private ImageService imageService; + + @Test + @DisplayName("ImageFile Entity 생성") + void createImageFile() { + // given + ImageFileDto dto = new ImageFileDto("test.jpg", "https://image.photopic.site/test.jpg", "https://image.photopic.site/thumb.jpg"); + ImageFile imageFile = ImageFile.create(dto); + + // when: createImageFile 호출 + ReflectionTestUtils.setField(imageFile, "id", 100L); + when(imageFileRepository.save(any(ImageFile.class))).thenReturn(imageFile); + Long id = imageService.createImageFile(dto); + + // then: id가 100L인지 확인 + assertEquals(100L, id); + } + + @Test + @DisplayName("ImageFile Entity 생성 - 파라미터가 null인 경우") + void createImageFile_null() { + // given + ImageFileDto dto = new ImageFileDto("test.jpg", null, null); + + // when + when(imageFileRepository.save(any(ImageFile.class))) + .thenThrow(new BadRequestException(ErrorCode.INVALID_ARGUMENT)); + + // then + assertThatThrownBy(() -> imageService.createImageFile(dto)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_ARGUMENT.getMessage()); + + } + + @Test + @DisplayName("ImageFile Entity 생성 - 파라미터가 빈 값인 경우") + void createImageFile_emptyString() { + // given + ImageFileDto dto = new ImageFileDto("test.jpg", "", ""); + + // when + when(imageFileRepository.save(any(ImageFile.class))) + .thenThrow(new BadRequestException(ErrorCode.INVALID_ARGUMENT)); + + // then + assertThatThrownBy(() -> imageService.createImageFile(dto)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_ARGUMENT.getMessage()); + + } + + @Test + @DisplayName("파일 업로드") + void uploadImageFile() { + // given + MockMultipartFile file1 = new MockMultipartFile( + "files", + "test1.jpg", + MediaType.IMAGE_JPEG_VALUE, + "dummy content".getBytes(StandardCharsets.UTF_8) + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", + "test2.png", + MediaType.IMAGE_PNG_VALUE, + "dummy content".getBytes(StandardCharsets.UTF_8) + ); + + List imageFiles = List.of( + new ImageFileDto("test1.jpg", "https://image.photopic.site/test1.jpg", "https://image.photopic.site/thumb1.jpg"), + new ImageFileDto("test2.png", "https://image.photopic.site/test2.png", "https://image.photopic.site/thumb2.png") + ); + + doNothing().when(fileValidator).validate(file1, file2); + when(r2Storage.uploadImageFile(file1, file2)).thenReturn(imageFiles); + + AtomicLong idGenerator = new AtomicLong(1L); + when(imageFileRepository.save(any(ImageFile.class))).thenAnswer(invocation -> new ImageFile() { + @Override + public Long getId() { + return idGenerator.getAndIncrement(); + } + }); + + // when + ImageFileResponse response = imageService.uploadImageFile(file1, file2); + + // then + assertAll( + () -> assertNotNull(response), + () -> assertEquals(2, response.imageFileId().size()), + () -> assertThat(response.imageFileId().get(0)).isEqualTo(1L), + () -> assertThat(response.imageFileId().get(1)).isEqualTo(2L) + ); + } + + @Test + @DisplayName("파일 업로드 - IOException 발생") + void uploadImageFile_IOException() { + // given: 두 개의 파일 생성 + MockMultipartFile file1 = new MockMultipartFile( + "files", + "test1.jpg", + MediaType.IMAGE_JPEG_VALUE, + "dummy content".getBytes(StandardCharsets.UTF_8) + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", + "test2.png", + MediaType.IMAGE_PNG_VALUE, + "dummy content".getBytes(StandardCharsets.UTF_8) + ); + + doNothing().when(fileValidator).validate(file1, file2); + when(r2Storage.uploadImageFile(file1, file2)) + .thenThrow(new UncheckedIOException(new IOException(ErrorCode.SERVICE_UNAVAILABLE.getMessage()))); + + // when then + assertThatThrownBy(() -> imageService.uploadImageFile(file1, file2)) + .isInstanceOf(UncheckedIOException.class) + .hasMessageContaining(ErrorCode.SERVICE_UNAVAILABLE.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java b/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java new file mode 100644 index 00000000..f13b90a5 --- /dev/null +++ b/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java @@ -0,0 +1,96 @@ +package com.swyp8team2.image.presentation; + +import com.swyp8team2.image.application.ImageService; +import com.swyp8team2.image.presentation.dto.ImageFileResponse; +import com.swyp8team2.support.RestDocsTest; +import com.swyp8team2.support.WithMockUserInfo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ImageController.class) +@Import(ImageControllerTest.TestConfig.class) +class ImageControllerTest extends RestDocsTest { + + @Autowired + private ImageService imageService; + + @TestConfiguration + static class TestConfig { + @Bean + public ImageService imageService() { + return Mockito.mock(ImageService.class); + } + } + + @Test + @WithMockUserInfo + @DisplayName("이미지 업로드") + void createImageFile() throws Exception { + //given + MockMultipartFile file1 = new MockMultipartFile( + "files", + "image.jpg", + MediaType.IMAGE_JPEG_VALUE, + "".getBytes(StandardCharsets.UTF_8) + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", + "image.png", + MediaType.IMAGE_PNG_VALUE, + "".getBytes(StandardCharsets.UTF_8) + ); + ImageFileResponse response = new ImageFileResponse(List.of(1L, 2L)); + + // stub + when(imageService.uploadImageFile(file1, file2)).thenReturn(response); + + //when then + mockMvc.perform(MockMvcRequestBuilders.multipart("/image/upload") + .file(file1) + .file(file2) + .contentType(MediaType.MULTIPART_FORM_DATA) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + requestParts( + partWithName("files") + .description("투표 후보 이미지 파일") + .attributes(key("type").value("Array[File]")) + .attributes(constraints("최소 2개")) + ), + responseFields( + fieldWithPath("imageFileId") + .description("업로드된 이미지 파일") + .type(JsonFieldType.ARRAY) + ) + )); + + } +} \ No newline at end of file diff --git a/src/test/java/com/swyp8team2/image/util/FileValidatorTest.java b/src/test/java/com/swyp8team2/image/util/FileValidatorTest.java new file mode 100644 index 00000000..14fb3a6f --- /dev/null +++ b/src/test/java/com/swyp8team2/image/util/FileValidatorTest.java @@ -0,0 +1,102 @@ +package com.swyp8team2.image.util; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FileValidatorTest { + + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + + private FileValidator fileValidator; + + @BeforeEach + void setUp() { + String allowedExtensions = "gif,jpg,jpeg,png"; + fileValidator = new FileValidator(allowedExtensions); + } + + @Test + @DisplayName("파일 유효성 체크 - 파일 크기 초과") + void validate_validFile_shouldPass() { + // given + byte[] largeContent = new byte[(int) (MAX_FILE_SIZE + 1)]; + MockMultipartFile file = new MockMultipartFile( + "file", + "test.jpg", + MediaType.IMAGE_JPEG_VALUE, + largeContent + ); + + // when then + assertThatThrownBy(() -> fileValidator.validate(file)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.EXCEED_MAX_FILE_SIZE.getMessage()); + } + + @Test + @DisplayName("파일 유효성 체크 - 지원하지 않는 확장자") + void validate_unsupportedExtension_shouldThrowException() { + // given + MockMultipartFile file = new MockMultipartFile( + "file", + "test.txt", + "text/plain", + "dummy content".getBytes(StandardCharsets.UTF_8) + ); + + // when then + assertThatThrownBy(() -> fileValidator.validate(file)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.UNSUPPORTED_FILE_EXTENSION.getMessage()); + } + + @Test + @DisplayName("파일 유효성 체크 - 확장자 누락") + void validate_missingFileExtension_shouldThrowException() { + // given + MockMultipartFile file = new MockMultipartFile( + "file", + "test", + "", + "dummy content".getBytes(StandardCharsets.UTF_8) + ); + + // when then + assertThatThrownBy(() -> fileValidator.validate(file)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.MISSING_FILE_EXTENSION.getMessage()); + } + + @Test + @DisplayName("파일 유효성 체크 - 여러 파일 중 하나가 유효성 실패") + void validate_multipleFiles_oneInvalid_shouldThrowException() { + // given + MockMultipartFile file1 = new MockMultipartFile( + "file", + "test.jpg", + "image/jpeg", + "dummy".getBytes(StandardCharsets.UTF_8) + ); + byte[] largeContent = new byte[(int) (MAX_FILE_SIZE + 1)]; + MockMultipartFile file2 = new MockMultipartFile( + "file", + "large.jpg", + "image/jpeg", + largeContent + ); + + // when then + assertThatThrownBy(() -> fileValidator.validate(file1, file2)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.EXCEED_MAX_FILE_SIZE.getMessage()); + } +} \ No newline at end of file From cc6f7d4162afa7b32fb6580a97797a09a0ccfdc3 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 21 Feb 2025 02:33:34 +0900 Subject: [PATCH 075/258] =?UTF-8?q?docs:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/AuthControllerTest.java | 7 ++----- .../presentation/CommentControllerTest.java | 15 +++++---------- .../post/presentation/PostControllerTest.java | 2 ++ .../user/presentation/UserControllerTest.java | 6 ++---- .../vote/presentation/VoteControllerTest.java | 2 ++ 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index df7b8d34..c9fea805 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -1,25 +1,22 @@ package com.swyp8team2.auth.presentation; import com.swyp8team2.auth.presentation.dto.TokenResponse; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.ErrorResponse; import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.RestDocsTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithAnonymousUser; -import static org.junit.jupiter.api.Assertions.*; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@WebMvcTest(AuthController.class) class AuthControllerTest extends RestDocsTest { @Test diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index a9b95b19..bd15019f 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -8,6 +8,7 @@ import com.swyp8team2.support.WithMockUserInfo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; @@ -16,20 +17,14 @@ import java.time.LocalDateTime; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@WebMvcTest(CommentController.class) class CommentControllerTest extends RestDocsTest { diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 03258d41..e4bd8e6a 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -11,6 +11,7 @@ import com.swyp8team2.support.WithMockUserInfo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; @@ -32,6 +33,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@WebMvcTest(PostController.class) class PostControllerTest extends RestDocsTest { @Test diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 056622e8..67d93f56 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -4,22 +4,20 @@ import com.swyp8team2.user.presentation.dto.UserInfoResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.security.test.context.support.WithMockUser; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@WebMvcTest(UserController.class) class UserControllerTest extends RestDocsTest { @Test diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index e0e8263d..9c7a63c4 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -7,6 +7,7 @@ import com.swyp8team2.vote.presentation.dto.VoteRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; @@ -23,6 +24,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@WebMvcTest(VoteController.class) class VoteControllerTest extends RestDocsTest { @Test From a6564da688ea6c5d6894341624d7418dcb809d1e Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 21 Feb 2025 02:34:24 +0900 Subject: [PATCH 076/258] =?UTF-8?q?docs:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20A?= =?UTF-8?q?PI=20=EB=AC=B8=EC=84=9C=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/images.adoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/docs/asciidoc/images.adoc b/src/docs/asciidoc/images.adoc index e69de29b..c4d9f0ec 100644 --- a/src/docs/asciidoc/images.adoc +++ b/src/docs/asciidoc/images.adoc @@ -0,0 +1,7 @@ +[[이미지-API]] +== 이미지 API + +[[이미지-업로드]] +=== 이미지 업로드 + +operation::image-controller-test/create-image-file[snippets='http-request,curl-request,request-headers,request-parts,http-response,response-fields'] \ No newline at end of file From e6b1286ac03ae0987f7455da4bba646379a57cbc Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 21 Feb 2025 02:48:19 +0900 Subject: [PATCH 077/258] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=86=8C=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/image/application/ImageServiceTest.java | 6 +++--- .../swyp8team2/image/presentation/ImageControllerTest.java | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/swyp8team2/image/application/ImageServiceTest.java b/src/test/java/com/swyp8team2/image/application/ImageServiceTest.java index b45bed7e..c38daacf 100644 --- a/src/test/java/com/swyp8team2/image/application/ImageServiceTest.java +++ b/src/test/java/com/swyp8team2/image/application/ImageServiceTest.java @@ -52,12 +52,12 @@ void createImageFile() { ImageFileDto dto = new ImageFileDto("test.jpg", "https://image.photopic.site/test.jpg", "https://image.photopic.site/thumb.jpg"); ImageFile imageFile = ImageFile.create(dto); - // when: createImageFile 호출 + // when ReflectionTestUtils.setField(imageFile, "id", 100L); when(imageFileRepository.save(any(ImageFile.class))).thenReturn(imageFile); Long id = imageService.createImageFile(dto); - // then: id가 100L인지 확인 + // then assertEquals(100L, id); } @@ -143,7 +143,7 @@ public Long getId() { @Test @DisplayName("파일 업로드 - IOException 발생") void uploadImageFile_IOException() { - // given: 두 개의 파일 생성 + // given MockMultipartFile file1 = new MockMultipartFile( "files", "test1.jpg", diff --git a/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java b/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java index f13b90a5..97e520a4 100644 --- a/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java +++ b/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java @@ -21,7 +21,6 @@ import java.nio.charset.StandardCharsets; import java.util.List; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; From e66c760abaa8c2428042a788e0a8f6c3e8bb7ed3 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 21 Feb 2025 03:06:03 +0900 Subject: [PATCH 078/258] =?UTF-8?q?test:=20=ED=94=84=EB=A1=9C=ED=8D=BC?= =?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 --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index f16b236c..0deb20d3 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit f16b236c75930077b8aa7c5254f159b036282e68 +Subproject commit 0deb20d36de9bc994063cc2baae643eb227f8716 From f906ce5d0a974c2f65d3f7882cadc1c9025fdd66 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 17:41:32 +0900 Subject: [PATCH 079/258] =?UTF-8?q?refactor:=20jwt=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/auth/application/{ => jwt}/JwtClaim.java | 2 +- .../swyp8team2/auth/application/{ => jwt}/JwtProvider.java | 2 +- .../com/swyp8team2/auth/application/{ => jwt}/JwtService.java | 4 ++-- .../com/swyp8team2/auth/application/{ => jwt}/TokenPair.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/com/swyp8team2/auth/application/{ => jwt}/JwtClaim.java (89%) rename src/main/java/com/swyp8team2/auth/application/{ => jwt}/JwtProvider.java (98%) rename src/main/java/com/swyp8team2/auth/application/{ => jwt}/JwtService.java (92%) rename src/main/java/com/swyp8team2/auth/application/{ => jwt}/TokenPair.java (90%) diff --git a/src/main/java/com/swyp8team2/auth/application/JwtClaim.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtClaim.java similarity index 89% rename from src/main/java/com/swyp8team2/auth/application/JwtClaim.java rename to src/main/java/com/swyp8team2/auth/application/jwt/JwtClaim.java index c15d1d90..149aa64d 100644 --- a/src/main/java/com/swyp8team2/auth/application/JwtClaim.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtClaim.java @@ -1,4 +1,4 @@ -package com.swyp8team2.auth.application; +package com.swyp8team2.auth.application.jwt; public class JwtClaim { diff --git a/src/main/java/com/swyp8team2/auth/application/JwtProvider.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java similarity index 98% rename from src/main/java/com/swyp8team2/auth/application/JwtProvider.java rename to src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java index ad6a5a39..aca72623 100644 --- a/src/main/java/com/swyp8team2/auth/application/JwtProvider.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java @@ -1,4 +1,4 @@ -package com.swyp8team2.auth.application; +package com.swyp8team2.auth.application.jwt; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; diff --git a/src/main/java/com/swyp8team2/auth/application/JwtService.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java similarity index 92% rename from src/main/java/com/swyp8team2/auth/application/JwtService.java rename to src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java index ec5c4486..b077b422 100644 --- a/src/main/java/com/swyp8team2/auth/application/JwtService.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java @@ -1,10 +1,9 @@ -package com.swyp8team2.auth.application; +package com.swyp8team2.auth.application.jwt; import com.swyp8team2.auth.domain.RefreshToken; import com.swyp8team2.auth.domain.RefreshTokenRepository; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.UnauthorizedException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -21,6 +20,7 @@ public TokenPair createToken(long userId) { TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(userId)); RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId) .orElseGet(() -> new RefreshToken(userId, tokenPair.refreshToken())); + refreshToken.setRefreshToken(tokenPair.refreshToken()); refreshTokenRepository.save(refreshToken); return tokenPair; } diff --git a/src/main/java/com/swyp8team2/auth/application/TokenPair.java b/src/main/java/com/swyp8team2/auth/application/jwt/TokenPair.java similarity index 90% rename from src/main/java/com/swyp8team2/auth/application/TokenPair.java rename to src/main/java/com/swyp8team2/auth/application/jwt/TokenPair.java index 1fb6b3b9..e7a714ab 100644 --- a/src/main/java/com/swyp8team2/auth/application/TokenPair.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/TokenPair.java @@ -1,4 +1,4 @@ -package com.swyp8team2.auth.application; +package com.swyp8team2.auth.application.jwt; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; From 9eb39945d8e8ab8d2e1ab5ba86debc394ac19fcb Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 17:42:19 +0900 Subject: [PATCH 080/258] =?UTF-8?q?refactor:=20oauth=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AuthService.java | 40 +++++++++++++ .../auth/application/OAuthService.java | 55 ------------------ .../auth/application/OAuthUserInfo.java | 26 --------- .../application/oauth/KakaoOAuthClient.java | 21 +++++++ .../auth/application/oauth/OAuthService.java | 56 +++++++++++++++++++ .../oauth/dto/KakaoAuthResponse.java | 9 +++ .../oauth/dto/KakaoUserInfoResponse.java | 32 +++++++++++ .../application/oauth/dto/OAuthUserInfo.java | 11 ++++ .../com/swyp8team2/auth/domain/OAuthUser.java | 27 --------- .../swyp8team2/auth/domain/RefreshToken.java | 8 ++- .../swyp8team2/auth/domain/SocialAccount.java | 15 +++-- .../auth/presentation/AuthController.java | 36 ++++++++++-- .../presentation/dto/OAuthSignInRequest.java | 9 +++ .../presentation/filter/JwtAuthFilter.java | 4 +- .../filter/OAuthLoginFailureHandler.java | 40 ------------- .../filter/OAuthLoginSuccessHandler.java | 51 ----------------- .../common/config/CommonConfig.java | 2 + .../common/config/HttpInterfaceConfig.java | 20 +++++++ .../common/config/KakaoOAuthConfig.java | 14 +++++ .../common/config/SecurityConfig.java | 54 ++++++++++-------- .../common/exception/ErrorCode.java | 4 +- .../com/swyp8team2/common/util/Validator.java | 2 +- .../user/application/UserService.java | 16 +++++- .../java/com/swyp8team2/user/domain/User.java | 14 ++--- 24 files changed, 317 insertions(+), 249 deletions(-) create mode 100644 src/main/java/com/swyp8team2/auth/application/AuthService.java delete mode 100644 src/main/java/com/swyp8team2/auth/application/OAuthService.java delete mode 100644 src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java create mode 100644 src/main/java/com/swyp8team2/auth/application/oauth/KakaoOAuthClient.java create mode 100644 src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java create mode 100644 src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoAuthResponse.java create mode 100644 src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java create mode 100644 src/main/java/com/swyp8team2/auth/application/oauth/dto/OAuthUserInfo.java delete mode 100644 src/main/java/com/swyp8team2/auth/domain/OAuthUser.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java delete mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java delete mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java create mode 100644 src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java create mode 100644 src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java new file mode 100644 index 00000000..dba040ce --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -0,0 +1,40 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.auth.application.jwt.JwtService; +import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.application.oauth.OAuthService; +import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; +import com.swyp8team2.auth.domain.Provider; +import com.swyp8team2.auth.domain.SocialAccount; +import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.user.application.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final JwtService jwtService; + private final OAuthService oAuthService; + private final SocialAccountRepository socialAccountRepository; + private final UserService userService; + + public String getOAuthAuthorizationUrl() { + return oAuthService.getOAuthAuthorizationUrl(); + } + + public TokenPair oauthSignIn(String code) { + OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code); + SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( + oAuthUserInfo.socialId(), + Provider.KAKAO + ).orElseGet(() -> createUser(oAuthUserInfo)); + return jwtService.createToken(socialAccount.getUserId()); + } + + private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { + Long userId = userService.createUser(oAuthUserInfo.nickname(), oAuthUserInfo.profileImageUrl()); + return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo)); + } +} diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/OAuthService.java deleted file mode 100644 index 39918466..00000000 --- a/src/main/java/com/swyp8team2/auth/application/OAuthService.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.swyp8team2.auth.application; - -import com.swyp8team2.auth.domain.OAuthUser; -import com.swyp8team2.auth.domain.SocialAccount; -import com.swyp8team2.auth.domain.SocialAccountRepository; -import com.swyp8team2.auth.domain.Provider; -import com.swyp8team2.user.application.UserService; -import lombok.RequiredArgsConstructor; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; - -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class OAuthService extends DefaultOAuth2UserService { - - private final SocialAccountRepository socialAccountRepository; - private final UserService userService; - - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - OAuth2User oAuth2User = super.loadUser(userRequest); - - Provider provider = getProvider(userRequest); - - String userNameAttributeName = userRequest.getClientRegistration() - .getProviderDetails() - .getUserInfoEndpoint() - .getUserNameAttributeName(); - - Map attributes = oAuth2User.getAttributes(); - OAuthUserInfo oAuthUserInfo = OAuthUserInfo.of(provider, attributes); - - SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( - oAuthUserInfo.socialId(), provider) - .orElseGet(() -> createUser(oAuthUserInfo, provider)); - - return new OAuthUser(oAuth2User.getAuthorities(), attributes, userNameAttributeName, socialAccount.getUserId()); - } - - private Provider getProvider(OAuth2UserRequest userRequest) { - String registrationId = userRequest.getClientRegistration().getRegistrationId(); - return Provider.of(registrationId); - } - - private SocialAccount createUser(OAuthUserInfo oAuthUserInfo, Provider provider) { - String email = oAuthUserInfo.email(); - Long userId = userService.createUser(email); - return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo.socialId(), provider, email)); - } -} diff --git a/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java b/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java deleted file mode 100644 index ec9abed8..00000000 --- a/src/main/java/com/swyp8team2/auth/application/OAuthUserInfo.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.swyp8team2.auth.application; - -import com.swyp8team2.auth.domain.Provider; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.InternalServerException; - -import java.util.Map; - -public record OAuthUserInfo(String socialId, String email) { - - public static OAuthUserInfo of(Provider provider, Map attributes) { - switch (provider) { - case KAKAO: - return ofKakao(attributes); - default: - throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); - } - } - - private static OAuthUserInfo ofKakao(Map attributes) { - String socialId = String.valueOf(attributes.get("id")); - Map kakaoAccount = (Map) attributes.get("kakao_account"); - String email = String.valueOf(kakaoAccount.get("email")); - return new OAuthUserInfo(socialId, email); - } -} diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/KakaoOAuthClient.java b/src/main/java/com/swyp8team2/auth/application/oauth/KakaoOAuthClient.java new file mode 100644 index 00000000..17540f1a --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/oauth/KakaoOAuthClient.java @@ -0,0 +1,21 @@ +package com.swyp8team2.auth.application.oauth; + +import com.swyp8team2.auth.application.oauth.dto.KakaoAuthResponse; +import com.swyp8team2.auth.application.oauth.dto.KakaoUserInfoResponse; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.PostExchange; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; + +public interface KakaoOAuthClient { + + @PostExchange(url = "https://kauth.kakao.com/oauth/token", contentType = APPLICATION_FORM_URLENCODED_VALUE) + KakaoAuthResponse fetchToken(@RequestParam("params") MultiValueMap params); + + @GetExchange("https://kapi.kakao.com/v2/user/me") + KakaoUserInfoResponse fetchUserInfo(@RequestHeader(name = AUTHORIZATION) String bearerToken); +} diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java new file mode 100644 index 00000000..42d2eb1a --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java @@ -0,0 +1,56 @@ +package com.swyp8team2.auth.application.oauth; + +import com.swyp8team2.auth.application.oauth.dto.KakaoAuthResponse; +import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; +import com.swyp8team2.common.config.KakaoOAuthConfig; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OAuthService { + + private static final String BEARER = "Bearer "; + + private final KakaoOAuthConfig kakaoOAuthConfig; + private final KakaoOAuthClient kakaoOAuthClient; + + public String getOAuthAuthorizationUrl() { + return UriComponentsBuilder + .fromUriString(kakaoOAuthConfig.authorizationUri()) + .queryParam("client_id", kakaoOAuthConfig.clientId()) + .queryParam("redirect_uri", kakaoOAuthConfig.redirectUri()) + .queryParam("response_type", "code") + .queryParam("scope", String.join(",", kakaoOAuthConfig.scope())) + .toUriString(); + } + + public OAuthUserInfo getUserInfo(String code) { + try { + KakaoAuthResponse kakaoAuthResponse = kakaoOAuthClient.fetchToken(tokenRequestParams(code)); + return kakaoOAuthClient + .fetchUserInfo(BEARER + kakaoAuthResponse.accessToken()) + .toOAuthUserInfo(); + } catch (Exception e) { + log.error("소셜 로그인 실패", e); + throw new InternalServerException(ErrorCode.SOCIAL_AUTHENTICATION_FAILED); + } + } + + private MultiValueMap tokenRequestParams(String authCode) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", kakaoOAuthConfig.clientId()); + params.add("redirect_uri", kakaoOAuthConfig.redirectUri()); + params.add("code", authCode); + params.add("client_secret", kakaoOAuthConfig.clientSecret()); + return params; + } +} diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoAuthResponse.java b/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoAuthResponse.java new file mode 100644 index 00000000..dacc574c --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoAuthResponse.java @@ -0,0 +1,9 @@ +package com.swyp8team2.auth.application.oauth.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoAuthResponse( + String accessToken +) { } diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java b/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java new file mode 100644 index 00000000..52ee8c90 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java @@ -0,0 +1,32 @@ +package com.swyp8team2.auth.application.oauth.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.swyp8team2.auth.domain.Provider; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserInfoResponse( + String id, + KakaoAccount kakaoAccount +) { + + public OAuthUserInfo toOAuthUserInfo() { + return new OAuthUserInfo( + id, + kakaoAccount.profile().profileImageUrl(), + kakaoAccount.profile().nickname(), + Provider.KAKAO + ); + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record KakaoAccount( + Profile profile + ) {} + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record Profile( + String nickname, + String profileImageUrl + ) {} +} diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/dto/OAuthUserInfo.java b/src/main/java/com/swyp8team2/auth/application/oauth/dto/OAuthUserInfo.java new file mode 100644 index 00000000..8d987ab1 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/application/oauth/dto/OAuthUserInfo.java @@ -0,0 +1,11 @@ +package com.swyp8team2.auth.application.oauth.dto; + +import com.swyp8team2.auth.domain.Provider; + +public record OAuthUserInfo( + String socialId, + String profileImageUrl, + String nickname, + Provider provider +) { +} diff --git a/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java b/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java deleted file mode 100644 index d2af14c5..00000000 --- a/src/main/java/com/swyp8team2/auth/domain/OAuthUser.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.swyp8team2.auth.domain; - -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -@Getter -public class OAuthUser extends DefaultOAuth2User { - - private final long userId; - - public OAuthUser( - Collection authorities, - Map attributes, - String nameAttributeKey, - long userId - ) { - super(authorities, attributes, nameAttributeKey); - this.userId = userId; - } -} diff --git a/src/main/java/com/swyp8team2/auth/domain/RefreshToken.java b/src/main/java/com/swyp8team2/auth/domain/RefreshToken.java index 883bdac3..3f005c67 100644 --- a/src/main/java/com/swyp8team2/auth/domain/RefreshToken.java +++ b/src/main/java/com/swyp8team2/auth/domain/RefreshToken.java @@ -35,7 +35,11 @@ public void rotate(String currentToken, String newToken) { if (!this.token.equals(currentToken)) { throw new BadRequestException(ErrorCode.REFRESH_TOKEN_MISMATCHED); } - Validator.validateEmptyString(newToken); - this.token = newToken; + setRefreshToken(newToken); + } + + public void setRefreshToken(String token) { + Validator.validateEmptyString(token); + this.token = token; } } diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java index b58aafb2..ff12cb20 100644 --- a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java @@ -1,5 +1,6 @@ package com.swyp8team2.auth.domain; +import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -23,24 +24,22 @@ public class SocialAccount { private Long userId; - private String email; - private String socialId; @Enumerated(EnumType.STRING) private Provider provider; - public SocialAccount(Long id, Long userId, String socialId, Provider provider, String email) { - validateNull(userId, socialId, provider, email); - validateEmptyString(socialId, email); + public SocialAccount(Long id, Long userId, String socialId, Provider provider) { + validateNull(userId, provider); + validateEmptyString(socialId); this.id = id; this.userId = userId; - this.email = email; this.socialId = socialId; this.provider = provider; } - public static SocialAccount create(Long userId, String socialId, Provider provider, String email) { - return new SocialAccount(null, userId, socialId, provider, email); + public static SocialAccount create(Long userId, OAuthUserInfo oAuthUserInfo) { + validateNull(oAuthUserInfo); + return new SocialAccount(null, userId, oAuthUserInfo.socialId(), oAuthUserInfo.provider()); } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 78685995..ec600d41 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -1,21 +1,27 @@ package com.swyp8team2.auth.presentation; -import com.swyp8team2.auth.application.JwtService; -import com.swyp8team2.auth.application.TokenPair; +import com.swyp8team2.auth.application.AuthService; +import com.swyp8team2.auth.application.jwt.JwtService; +import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.UnauthorizedException; import com.swyp8team2.common.presentation.CustomHeader; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Objects; @@ -27,10 +33,30 @@ public class AuthController { private final JwtService jwtService; private final RefreshTokenCookieGenerator refreshTokenCookieGenerator; + private final AuthService authService; + + @GetMapping("/oauth2/kakao") + public ResponseEntity kakaoOAuth() { + String requestUrl = authService.getOAuthAuthorizationUrl(); + return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY) + .header(HttpHeaders.LOCATION, requestUrl) + .build(); + } + + @PostMapping("/oauth2/code/kakao") + public ResponseEntity kakaoOAuthSignIn( + @Valid @RequestBody OAuthSignInRequest request, + HttpServletResponse response + ) { + TokenPair tokenPair = authService.oauthSignIn(request.code()); + Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); + response.addCookie(cookie); + return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); + } @PostMapping("/reissue") public ResponseEntity reissue( - @CookieValue(value = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, + @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, HttpServletResponse response ) { if (Objects.isNull(refreshToken)) { diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java b/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java new file mode 100644 index 00000000..e8967052 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java @@ -0,0 +1,9 @@ +package com.swyp8team2.auth.presentation.dto; + +import jakarta.validation.constraints.NotNull; + +public record OAuthSignInRequest( + @NotNull + String code +) { +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index 28b9dd50..d77b476f 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -1,7 +1,7 @@ package com.swyp8team2.auth.presentation.filter; -import com.swyp8team2.auth.application.JwtClaim; -import com.swyp8team2.auth.application.JwtProvider; +import com.swyp8team2.auth.application.jwt.JwtClaim; +import com.swyp8team2.auth.application.jwt.JwtProvider; import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.exception.ApplicationException; import jakarta.servlet.FilterChain; diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java deleted file mode 100644 index ac2ad59f..00000000 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginFailureHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.swyp8team2.auth.presentation.filter; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.ErrorResponse; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - -@Slf4j -@Component -@RequiredArgsConstructor -public class OAuthLoginFailureHandler implements AuthenticationFailureHandler { - - private final ObjectMapper objectMapper; - - @Override - public void onAuthenticationFailure( - HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception - ) throws IOException, ServletException { - log.error("OAuth login failed", exception); - response.setContentType(APPLICATION_JSON_VALUE); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.getWriter() - .write(objectMapper.writeValueAsString(new ErrorResponse(ErrorCode.OAUTH_LOGIN_FAILED))); - } -} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java b/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java deleted file mode 100644 index 4cfe9c8d..00000000 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/OAuthLoginSuccessHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.swyp8team2.auth.presentation.filter; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.swyp8team2.auth.application.JwtClaim; -import com.swyp8team2.auth.application.JwtProvider; -import com.swyp8team2.auth.application.JwtService; -import com.swyp8team2.auth.application.TokenPair; -import com.swyp8team2.auth.domain.OAuthUser; -import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; -import com.swyp8team2.auth.presentation.dto.TokenResponse; -import com.swyp8team2.common.presentation.CustomHeader; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import java.io.IOException; - -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - -@Component -@RequiredArgsConstructor -public class OAuthLoginSuccessHandler implements AuthenticationSuccessHandler { - - private final ObjectMapper objectMapper; - private final JwtService jwtService; - private final RefreshTokenCookieGenerator refreshTokenCookieGenerator; - - @Override - public void onAuthenticationSuccess( - HttpServletRequest request, - HttpServletResponse response, - Authentication authentication - ) throws IOException { - OAuthUser oAuthUser = (OAuthUser) authentication.getPrincipal(); - TokenPair tokenPair = jwtService.createToken(oAuthUser.getUserId()); - - Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); - - response.addCookie(cookie); - response.setContentType(APPLICATION_JSON_VALUE); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write(objectMapper.writeValueAsString(new TokenResponse(tokenPair.accessToken()))); - response.getWriter().flush(); - } -} diff --git a/src/main/java/com/swyp8team2/common/config/CommonConfig.java b/src/main/java/com/swyp8team2/common/config/CommonConfig.java index 3ee5458b..efd9c6a0 100644 --- a/src/main/java/com/swyp8team2/common/config/CommonConfig.java +++ b/src/main/java/com/swyp8team2/common/config/CommonConfig.java @@ -1,11 +1,13 @@ package com.swyp8team2.common.config; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.Clock; @Configuration +@ConfigurationPropertiesScan public class CommonConfig { diff --git a/src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java b/src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java new file mode 100644 index 00000000..b95414e9 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java @@ -0,0 +1,20 @@ +package com.swyp8team2.common.config; + +import com.swyp8team2.auth.application.oauth.KakaoOAuthClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +@Configuration +public class HttpInterfaceConfig { + + @Bean + public KakaoOAuthClient kakaoAuthClient() { + RestClientAdapter adapter = RestClientAdapter.create(RestClient.create()); + HttpServiceProxyFactory build = HttpServiceProxyFactory + .builderFor(adapter).build(); + return build.createClient(KakaoOAuthClient.class); + } +} diff --git a/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java b/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java new file mode 100644 index 00000000..5feab34f --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java @@ -0,0 +1,14 @@ +package com.swyp8team2.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth2.kakao") +public record KakaoOAuthConfig( + String authorizationUri, + String clientId, + String clientSecret, + String redirectUri, + String[] scope, + String userInfoUri +) { +} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 228a6f90..1d0a47f9 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -1,12 +1,9 @@ package com.swyp8team2.common.config; -import com.swyp8team2.auth.application.JwtProvider; -import com.swyp8team2.auth.application.OAuthService; +import com.swyp8team2.auth.application.jwt.JwtProvider; import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; -import com.swyp8team2.auth.presentation.filter.OAuthLoginFailureHandler; -import com.swyp8team2.auth.presentation.filter.OAuthLoginSuccessHandler; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; @@ -23,27 +20,22 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import java.util.List; + @Configuration @EnableWebSecurity public class SecurityConfig { - private final OAuthService oAuthService; - private final OAuthLoginSuccessHandler oAuthLoginSuccessHandler; - private final OAuthLoginFailureHandler oAuthLoginFailureHandler; private final HandlerExceptionResolver handlerExceptionResolver; public SecurityConfig( - OAuthService oAuthService, - OAuthLoginSuccessHandler oAuthLoginSuccessHandler, - OAuthLoginFailureHandler oAuthLoginFailureHandler, @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver ) { - this.oAuthService = oAuthService; - this.oAuthLoginSuccessHandler = oAuthLoginSuccessHandler; - this.oAuthLoginFailureHandler = oAuthLoginFailureHandler; this.handlerExceptionResolver = handlerExceptionResolver; } @@ -82,6 +74,7 @@ public SecurityFilterChain securityFilterChain( .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSourceLocal())) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .sessionManagement(session -> @@ -97,17 +90,34 @@ public SecurityFilterChain securityFilterChain( UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exception -> exception.authenticationEntryPoint( - new JwtAuthenticationEntryPoint(handlerExceptionResolver))) - - .oauth2Login(oauth -> - oauth.authorizationEndpoint(authorizationEndpointConfig -> - authorizationEndpointConfig.baseUri("/auth/oauth2")) - .userInfoEndpoint(userInfo -> userInfo.userService(oAuthService)) - .successHandler(oAuthLoginSuccessHandler) - .failureHandler(oAuthLoginFailureHandler)); + new JwtAuthenticationEntryPoint(handlerExceptionResolver))); return http.build(); } + @Profile({"prod", "dev"}) + UrlBasedCorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("https://photopic.site")); + configuration.setAllowedMethods(List.of("GET","POST", "PUT", "DELETE", "PATCH")); + configuration.setAllowCredentials(true); + configuration.addAllowedHeader("*"); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Profile("local") + UrlBasedCorsConfigurationSource corsConfigurationSourceLocal() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:5173")); + configuration.setAllowedMethods(List.of("GET","POST", "PUT", "DELETE", "PATCH")); + configuration.setAllowCredentials(true); + configuration.addAllowedHeader("*"); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ @@ -116,7 +126,7 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), mvc.pattern("/posts/{postId}/votes/guest/**"), - mvc.pattern("/auth/oauth2") + mvc.pattern("/auth/oauth2/**") }; } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 7377757e..38016202 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -21,7 +21,9 @@ public enum ErrorCode { //500 INTERNAL_SERVER_ERROR("서버 내부 오류"), - INVALID_INPUT_VALUE("잘못된 입력 값"),; + INVALID_INPUT_VALUE("잘못된 입력 값"), + SOCIAL_AUTHENTICATION_FAILED("소셜 로그인 실패"), + ; private final String message; } diff --git a/src/main/java/com/swyp8team2/common/util/Validator.java b/src/main/java/com/swyp8team2/common/util/Validator.java index 3ab5c462..5e736223 100644 --- a/src/main/java/com/swyp8team2/common/util/Validator.java +++ b/src/main/java/com/swyp8team2/common/util/Validator.java @@ -18,7 +18,7 @@ public static void validateNull(Object... object) { public static void validateEmptyString(String... strings) { Arrays.stream(strings) - .filter(String::isEmpty) + .filter(s -> Objects.isNull(s) || s.isEmpty()) .forEach(s -> { throw new InternalServerException(ErrorCode.INVALID_INPUT_VALUE); }); diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index 3b6b184e..1d09f4bc 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -6,6 +6,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @RequiredArgsConstructor public class UserService { @@ -13,8 +15,18 @@ public class UserService { private final UserRepository userRepository; @Transactional - public Long createUser(String email) { - User user = userRepository.save(User.create("user_" + System.currentTimeMillis(), email)); + public Long createUser(String nickname, String profileImageUrl) { + User user = userRepository.save(User.create(getNickname(nickname), getProfileImage(profileImageUrl))); return user.getId(); } + + private String getProfileImage(String profileImageUrl) { + return Optional.ofNullable(profileImageUrl) + .orElse("defailt_profile_image"); + } + + private String getNickname(String email) { + return Optional.ofNullable(email) + .orElseGet(() -> "user_" + System.currentTimeMillis()); + } } diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 18099677..3581f3e3 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -23,17 +23,17 @@ public class User { private String nickname; - private String email; + private String profileUrl; - public User(Long id, String nickname, String email) { - validateNull(nickname, email); - validateEmptyString(nickname, email); + public User(Long id, String nickname, String profileUrl) { + validateNull(nickname, profileUrl); + validateEmptyString(nickname, profileUrl); this.id = id; this.nickname = nickname; - this.email = email; + this.profileUrl = profileUrl; } - public static User create(String nickname, String email) { - return new User(null, nickname, email); + public static User create(String nickname, String profileUrl) { + return new User(null, nickname, profileUrl); } } From 210a6e267d08752b07dd452c98b0e4967f099c31 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 17:52:53 +0900 Subject: [PATCH 081/258] =?UTF-8?q?fix:=20refreshToken=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95=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 --- .../presentation/RefreshTokenCookieGenerator.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java index 5668a80d..8e0c50f8 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java +++ b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java @@ -2,17 +2,25 @@ import com.swyp8team2.common.presentation.CustomHeader; import jakarta.servlet.http.Cookie; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class RefreshTokenCookieGenerator { + @Value("${spring.profiles.active:default}") + private String activeProfile; + public Cookie createCookie(String refreshToken) { Cookie cookie = new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, refreshToken); cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setAttribute("SameSite", "None"); - cookie.setPath("/auth/reissue"); + if ("local".equals(activeProfile)) { + cookie.setSecure(false); + } else { + cookie.setSecure(true); + cookie.setAttribute("SameSite", "None"); + } + cookie.setPath("/"); cookie.setMaxAge(60 * 60 * 24 * 14); return cookie; } From e625c5172d4b18bf6de04ec3a492d488c89e3900 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 17:53:02 +0900 Subject: [PATCH 082/258] =?UTF-8?q?fix:=20cors=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/common/config/CorsConfig.java | 41 +++++++++++++++++++ .../common/config/SecurityConfig.java | 29 ++----------- 2 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/swyp8team2/common/config/CorsConfig.java diff --git a/src/main/java/com/swyp8team2/common/config/CorsConfig.java b/src/main/java/com/swyp8team2/common/config/CorsConfig.java new file mode 100644 index 00000000..969bfe20 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/CorsConfig.java @@ -0,0 +1,41 @@ +package com.swyp8team2.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +public class CorsConfig { + + @Bean + @Profile("prod") + public UrlBasedCorsConfigurationSource corsConfigurationSourceProd() { + CorsConfiguration configuration = getCorsConfiguration(); + configuration.setAllowedOrigins(List.of("https://photopic.site")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + @Profile({"local", "dev", "default", "test"}) + public UrlBasedCorsConfigurationSource corsConfigurationSourceLocal() { + CorsConfiguration configuration = getCorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:5173", "https://dev.photopic.site")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + private static CorsConfiguration getCorsConfiguration() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedMethods(List.of("GET","POST", "PUT", "DELETE", "PATCH")); + configuration.setAllowCredentials(true); + configuration.addAllowedHeader("*"); + return configuration; + } +} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 1d0a47f9..4e94ba55 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -67,14 +67,15 @@ public WebSecurityCustomizer configureH2ConsoleEnable() { public SecurityFilterChain securityFilterChain( HttpSecurity http, HandlerMappingIntrospector introspect, - JwtProvider jwtProvider + JwtProvider jwtProvider, + UrlBasedCorsConfigurationSource corsConfigurationSource ) throws Exception { http .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSourceLocal())) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .sessionManagement(session -> @@ -94,30 +95,6 @@ public SecurityFilterChain securityFilterChain( return http.build(); } - @Profile({"prod", "dev"}) - UrlBasedCorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of("https://photopic.site")); - configuration.setAllowedMethods(List.of("GET","POST", "PUT", "DELETE", "PATCH")); - configuration.setAllowCredentials(true); - configuration.addAllowedHeader("*"); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } - - @Profile("local") - UrlBasedCorsConfigurationSource corsConfigurationSourceLocal() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of("http://localhost:5173")); - configuration.setAllowedMethods(List.of("GET","POST", "PUT", "DELETE", "PATCH")); - configuration.setAllowCredentials(true); - configuration.addAllowedHeader("*"); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } - public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector introspect) { MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ From 52101274588e835445105e2f0916b6c5cd3b1bb0 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 17:53:19 +0900 Subject: [PATCH 083/258] =?UTF-8?q?test:=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AuthServiceTest.java | 69 +++++++++++++++++++ .../auth/application/JwtClaimTest.java | 3 +- .../auth/application/JwtProviderTest.java | 4 +- .../auth/application/JwtServiceTest.java | 4 ++ .../auth/domain/SocialAccountTest.java | 39 ++++++----- .../auth/presentation/AuthControllerTest.java | 69 ++++++++++++++++++- .../com/swyp8team2/support/WebUnitTest.java | 6 +- 7 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java diff --git a/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java b/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java new file mode 100644 index 00000000..c5a62a40 --- /dev/null +++ b/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java @@ -0,0 +1,69 @@ +package com.swyp8team2.auth.application; + +import com.swyp8team2.auth.application.jwt.JwtProvider; +import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.application.oauth.OAuthService; +import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; +import com.swyp8team2.auth.domain.Provider; +import com.swyp8team2.auth.domain.SocialAccount; +import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.support.IntegrationTest; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +class AuthServiceTest extends IntegrationTest { + + @Autowired + AuthService authService; + + @MockitoBean + OAuthService oAuthService; + + @MockitoBean + JwtProvider jwtProvider; + + @Autowired + SocialAccountRepository socialAccountRepository; + + @Autowired + UserRepository userRepository; + + @Test + @DisplayName("OAuth 로그인하면 토큰 발급해야 하고 유저 정보 없는 경우 유저 생성") + void oAuthSignIn() throws Exception { + //given + OAuthUserInfo oAuthUserInfo = new OAuthUserInfo("socialId", "profileImageUrl", "nickname", Provider.KAKAO); + given(oAuthService.getUserInfo(anyString())) + .willReturn(oAuthUserInfo); + TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); + given(jwtProvider.createToken(any())) + .willReturn(expectedTokenPair); + + //when + TokenPair tokenPair = authService.oauthSignIn("code"); + + //then + SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider(oAuthUserInfo.socialId(), Provider.KAKAO).get(); + User user = userRepository.findById(socialAccount.getId()).get(); + assertAll( + () -> assertThat(tokenPair).isEqualTo(expectedTokenPair), + () -> assertThat(socialAccount.getUserId()).isNotNull(), + () -> assertThat(socialAccount.getSocialId()).isEqualTo(oAuthUserInfo.socialId()), + () -> assertThat(socialAccount.getProvider()).isEqualTo(oAuthUserInfo.provider()), + () -> assertThat(user.getNickname()).isEqualTo(oAuthUserInfo.nickname()), + () -> assertThat(user.getProfileUrl()).isEqualTo(oAuthUserInfo.profileImageUrl()) + ); + } +} diff --git a/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java b/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java index 524187b1..ef7a31e5 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtClaimTest.java @@ -1,11 +1,10 @@ package com.swyp8team2.auth.application; +import com.swyp8team2.auth.application.jwt.JwtClaim; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class JwtClaimTest { @Test diff --git a/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java index 666e24b5..d3a5fcae 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtProviderTest.java @@ -1,8 +1,10 @@ package com.swyp8team2.auth.application; +import com.swyp8team2.auth.application.jwt.JwtClaim; +import com.swyp8team2.auth.application.jwt.JwtProvider; +import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.UnauthorizedException; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java index 0c54f433..a21e7164 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java @@ -1,5 +1,9 @@ package com.swyp8team2.auth.application; +import com.swyp8team2.auth.application.jwt.JwtClaim; +import com.swyp8team2.auth.application.jwt.JwtProvider; +import com.swyp8team2.auth.application.jwt.JwtService; +import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.domain.RefreshToken; import com.swyp8team2.auth.domain.RefreshTokenRepository; import com.swyp8team2.common.exception.BadRequestException; diff --git a/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java b/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java index da0072eb..f73cd9ad 100644 --- a/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java +++ b/src/test/java/com/swyp8team2/auth/domain/SocialAccountTest.java @@ -1,5 +1,6 @@ package com.swyp8team2.auth.domain; +import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; import org.junit.jupiter.api.DisplayName; @@ -17,17 +18,21 @@ class SocialAccountTest { void create() throws Exception { //given long givenUserId = 1L; - String givenSocialId = "socialId"; - Provider givenProvider = Provider.KAKAO; + OAuthUserInfo oAuthUserInfo = new OAuthUserInfo( + "socialId", + "profileImageUrl", + "nickname", + Provider.KAKAO + ); //when - SocialAccount socialAccount = SocialAccount.create(givenUserId, givenSocialId, givenProvider, "email"); + SocialAccount socialAccount = SocialAccount.create(givenUserId, oAuthUserInfo); //then assertAll( () -> assertThat(socialAccount.getUserId()).isEqualTo(givenUserId), - () -> assertThat(socialAccount.getSocialId()).isEqualTo(givenSocialId), - () -> assertThat(socialAccount.getProvider()).isEqualTo(givenProvider) + () -> assertThat(socialAccount.getSocialId()).isEqualTo(oAuthUserInfo.socialId()), + () -> assertThat(socialAccount.getProvider()).isEqualTo(oAuthUserInfo.provider()) ); } @@ -38,23 +43,19 @@ void create_null() throws Exception { //when then assertAll( - () -> assertThatThrownBy(() -> SocialAccount.create(1L, null, Provider.KAKAO, "email")) + () -> assertThatThrownBy(() -> SocialAccount.create(1L, null)) .isInstanceOf(InternalServerException.class) .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), - () -> assertThatThrownBy(() -> SocialAccount.create(1L, "socialId", null, "email")) - .isInstanceOf(InternalServerException.class) + () -> assertThatThrownBy(() -> SocialAccount.create( + 1L, + new OAuthUserInfo(null, "profileImageUrl", "nickname", Provider.KAKAO) + )).isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), + () -> assertThatThrownBy(() -> SocialAccount.create( + 1L, + new OAuthUserInfo("socialId", "profileImageUrl", "nickname", null) + )).isInstanceOf(InternalServerException.class) .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()) ); } - - @Test - @DisplayName("SocialAccount Entity 생성 - socialId가 빈 문자인 경우") - void create_emptyString() throws Exception { - //given - - //when then - assertThatThrownBy(() -> SocialAccount.create(1L, "", Provider.KAKAO, "email")) - .isInstanceOf(InternalServerException.class) - .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); - } } diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 20db8312..d6d51d57 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -1,6 +1,7 @@ package com.swyp8team2.auth.presentation; -import com.swyp8team2.auth.application.TokenPair; +import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; @@ -10,6 +11,8 @@ import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; import static org.mockito.ArgumentMatchers.anyString; @@ -17,15 +20,75 @@ import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class AuthControllerTest extends RestDocsTest { + @Test + @DisplayName("카카오 로그인 리다이렉트") + void kakaoOAuth() throws Exception { + //given + String redirectUri = "https://kakao.com/oauth2/authorize"; + given(authService.getOAuthAuthorizationUrl()).willReturn(redirectUri); + + //when then + mockMvc.perform(get("/auth/oauth2/kakao")) + .andExpect(status().isMovedPermanently()) + .andExpect(header().string(HttpHeaders.LOCATION, redirectUri)) + .andDo(restDocs.document( + responseHeaders( + headerWithName(HttpHeaders.LOCATION).description("카카오 로그인 페이지 주소") + ) + )); + } + + @Test + @DisplayName("카카오 로그인 코드로 토큰 발급") + void kakaoOAuthSignIn() throws Exception { + //given + TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); + TokenResponse response = new TokenResponse(expectedTokenPair.accessToken()); + given(authService.oauthSignIn(anyString())) + .willReturn(expectedTokenPair); + OAuthSignInRequest request = new OAuthSignInRequest("code"); + + //when then + mockMvc.perform(post("/auth/oauth2/code/kakao") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andExpect(cookie().value(CustomHeader.CustomCookie.REFRESH_TOKEN, expectedTokenPair.refreshToken())) + .andExpect(cookie().httpOnly(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().path(CustomHeader.CustomCookie.REFRESH_TOKEN, "/")) + .andExpect(cookie().secure(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().attribute(CustomHeader.CustomCookie.REFRESH_TOKEN, "SameSite", "None")) + .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 60 * 60 * 24 * 14)) + .andDo(restDocs.document( + requestFields( + fieldWithPath("code").description("카카오 인증 코드") + ), + responseFields( + fieldWithPath("accessToken").description("액세스 토큰") + ), + responseCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") + ) + )); + } + @Test @WithAnonymousUser @DisplayName("토큰 재발급") @@ -43,7 +106,7 @@ void reissue() throws Exception { .andExpect(content().json(objectMapper.writeValueAsString(response))) .andExpect(cookie().value(CustomHeader.CustomCookie.REFRESH_TOKEN, newRefreshToken)) .andExpect(cookie().httpOnly(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) - .andExpect(cookie().path(CustomHeader.CustomCookie.REFRESH_TOKEN, "/auth/reissue")) + .andExpect(cookie().path(CustomHeader.CustomCookie.REFRESH_TOKEN, "/")) .andExpect(cookie().secure(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) .andExpect(cookie().attribute(CustomHeader.CustomCookie.REFRESH_TOKEN, "SameSite", "None")) .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 60 * 60 * 24 * 14)) diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index c372df20..55034f41 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -1,7 +1,8 @@ package com.swyp8team2.support; import com.fasterxml.jackson.databind.ObjectMapper; -import com.swyp8team2.auth.application.JwtService; +import com.swyp8team2.auth.application.AuthService; +import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; @@ -22,4 +23,7 @@ public abstract class WebUnitTest { @Autowired protected RefreshTokenCookieGenerator refreshTokenCookieGenerator; + + @MockitoBean + protected AuthService authService; } From cb42d03cdeabd4c8956574d10dae5161ba7d5ccd Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 17:55:44 +0900 Subject: [PATCH 084/258] =?UTF-8?q?feat:=20=EA=B0=9C=EB=B0=9C=EC=9A=A9=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=84=B8=ED=8C=85=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/dev/DataInitializer.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/com/swyp8team2/common/dev/DataInitializer.java diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java new file mode 100644 index 00000000..af71af9e --- /dev/null +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -0,0 +1,27 @@ +package com.swyp8team2.common.dev; + +import com.swyp8team2.auth.application.jwt.JwtService; +import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile({"dev", "local"}) +@Component +@RequiredArgsConstructor +public class DataInitializer { + + private final UserRepository userRepository; + private final JwtService jwtService; + + @PostConstruct + public void init() { + User save = userRepository.save(User.create("nickname", "defailt_profile_image")); + TokenPair tokenPair = jwtService.createToken(save.getId()); + System.out.println("accessToken = " + tokenPair.accessToken()); + System.out.println("refreshToken = " + tokenPair.refreshToken()); + } +} From a52d7207aa341e535002af68472de0201fd82b72 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 17:56:17 +0900 Subject: [PATCH 085/258] =?UTF-8?q?docs:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=AC=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 46 ++++++------------------------------- 1 file changed, 7 insertions(+), 39 deletions(-) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 67de2a82..50d45e6c 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -1,49 +1,17 @@ [[인증-API]] == 인증 API -[[카카오-로그인]] -=== 카카오 로그인 - -==== HTTP request ----- -GET /auth/oauth2/kakao HTTP/1.1 ----- - -==== Curl request ----- -$ curl 'http://localhost:8080/auth/oauth2/kakao' -i -X GET ----- - -==== HTTP response ----- -HTTP/1.1 200 OK -Set-Cookie: refreshToken=refreshToken; Path=/auth/reissue; Max-Age=1209600; Expires=Thu, 06 Mar 2025 01:33:19 GMT; Secure; HttpOnly -Content-Type: application/json +[[카카오-로그인-리다이렉트]] +=== 카카오 로그인 리다이렉트 -{ - "accessToken" : "accessToken" -} ----- +operation::auth-controller-test/kakao-o-auth[snippets='http-request,curl-request,http-response'] -==== Response cookies -|=== -|Name|Description - -|`+refreshToken+` -|리프레시 토큰 - -|=== - -==== Response fields -|=== -|필드명|타입|필수값|설명 +[[카카오-로그인]] +=== 카카오 로그인 - |`+accessToken+` - |`+String+` - |true - |액세스 토큰 +operation::auth-controller-test/kakao-o-auth-sign-in[snippets='http-request,curl-request,request-fields,http-response,response-cookies,response-fields'] -|=== +[[네이버-로그인]] [[토큰-재발급]] === 토큰 재발급 From f28a870e149e327018b15b69e92062b5bccd513a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 21 Feb 2025 18:01:32 +0900 Subject: [PATCH 086/258] =?UTF-8?q?chore:=20oauth=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index f16b236c..efab0d7d 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit f16b236c75930077b8aa7c5254f159b036282e68 +Subproject commit efab0d7daa33bb58b83650d634b0843eeb28c18a From 0e22afd0e89d89212c6058fbcdf479295e0e2a97 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sun, 23 Feb 2025 10:40:38 +0900 Subject: [PATCH 087/258] =?UTF-8?q?docs:=20images=20API=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/index.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 96cdfa39..79d0a7a2 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -91,6 +91,8 @@ include::auth.adoc[] include::users.adoc[] +include::images.adoc[] + include::posts.adoc[] include::votes.adoc[] From fa115bb6730fd020b546b18797cf10dd1102e6fb Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 10:42:44 +0900 Subject: [PATCH 088/258] =?UTF-8?q?test:=20webmvctest=20=EC=9C=84=EC=B9=98?= =?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 --- src/test/java/com/swyp8team2/support/RestDocsTest.java | 4 ---- src/test/java/com/swyp8team2/support/WebUnitTest.java | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/swyp8team2/support/RestDocsTest.java b/src/test/java/com/swyp8team2/support/RestDocsTest.java index b6ce6487..f8bd6e39 100644 --- a/src/test/java/com/swyp8team2/support/RestDocsTest.java +++ b/src/test/java/com/swyp8team2/support/RestDocsTest.java @@ -1,13 +1,11 @@ package com.swyp8team2.support; -import com.fasterxml.jackson.databind.ObjectMapper; import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.config.RestDocsConfiguration; import com.swyp8team2.support.config.TestSecurityConfig; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.RestDocumentationExtension; @@ -15,12 +13,10 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.request.ParameterDescriptor; import org.springframework.restdocs.snippet.Attributes; -import org.springframework.test.web.servlet.MockMvc; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -@WebMvcTest @AutoConfigureRestDocs @Import({RestDocsConfiguration.class, TestSecurityConfig.class}) @ExtendWith(RestDocumentationExtension.class) diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index 55034f41..ce660c2c 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -5,10 +5,12 @@ import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +@WebMvcTest @Import(RefreshTokenCookieGenerator.class) public abstract class WebUnitTest { From 8e919ce61eb2e99fb360e8abae09bd8e2480d537 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sun, 23 Feb 2025 10:43:30 +0900 Subject: [PATCH 089/258] =?UTF-8?q?docs:=20503=20error=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/docs/asciidoc/index.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 79d0a7a2..cada3319 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -30,6 +30,7 @@ HTTP 상태 코드 본 REST API에서 사용하는 HTTP 상태 코드는 가능 | `401 Unauthorization`| 인증에 실패한 경우. 응답 본문에 더 오류에 대한 정보가 담겨있음 | `404 Not Found`| 요청한 리소스가 없음. | `500 Internal Server Error`| 서버 내부 오류가 발생한 경우. +| `503 Service Unavailable`| 서버가 요청을 처리할 준비가 되지 않은 경우. |=== From 59dbb245a676f2e273cc1e259fb91b6e8fadb419 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 11:29:26 +0900 Subject: [PATCH 090/258] =?UTF-8?q?refactor:=20jwt=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20authService=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 --- .../java/com/swyp8team2/auth/application/AuthService.java | 5 +++++ .../com/swyp8team2/auth/presentation/AuthController.java | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index dba040ce..13568535 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -37,4 +37,9 @@ private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { Long userId = userService.createUser(oAuthUserInfo.nickname(), oAuthUserInfo.profileImageUrl()); return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo)); } + + public TokenPair reissue(String refreshToken) { + System.out.println("refreshToken = " + refreshToken); + return jwtService.reissue(refreshToken); + } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index ec600d41..b39e0d2b 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -31,7 +31,6 @@ @RequestMapping("/auth") public class AuthController { - private final JwtService jwtService; private final RefreshTokenCookieGenerator refreshTokenCookieGenerator; private final AuthService authService; @@ -62,7 +61,7 @@ public ResponseEntity reissue( if (Objects.isNull(refreshToken)) { throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); } - TokenPair tokenPair = jwtService.reissue(refreshToken); + TokenPair tokenPair = authService.reissue(refreshToken); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); response.addCookie(cookie); return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); From e7db15e29dc7acaddd06c675e5baf9b23543323f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 11:30:08 +0900 Subject: [PATCH 091/258] =?UTF-8?q?test:=20@WebMvcTest=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=20context=EC=97=90=EC=84=9C=20=EC=A7=84=ED=96=89?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/AuthControllerTest.java | 13 +++++++----- .../presentation/CommentControllerTest.java | 2 -- .../presentation/ImageControllerTest.java | 20 ------------------- .../post/presentation/PostControllerTest.java | 2 -- .../com/swyp8team2/support/WebUnitTest.java | 8 ++++---- .../support/config/WebDependencyConfig.java | 4 ---- .../user/presentation/UserControllerTest.java | 2 -- .../vote/presentation/VoteControllerTest.java | 2 -- 8 files changed, 12 insertions(+), 41 deletions(-) delete mode 100644 src/test/java/com/swyp8team2/support/config/WebDependencyConfig.java diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 6f172b2b..f801ff9a 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -1,5 +1,6 @@ package com.swyp8team2.auth.presentation; +import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; @@ -11,6 +12,7 @@ import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; @@ -20,7 +22,6 @@ import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; @@ -34,9 +35,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(AuthController.class) class AuthControllerTest extends RestDocsTest { + @Autowired + AuthService authService; + @Test @DisplayName("카카오 로그인 리다이렉트") void kakaoOAuth() throws Exception { @@ -96,7 +99,7 @@ void kakaoOAuthSignIn() throws Exception { void reissue() throws Exception { //given String newRefreshToken = "newRefreshToken"; - given(jwtService.reissue(anyString())) + given(authService.reissue(anyString())) .willReturn(new TokenPair("accessToken", newRefreshToken)); TokenResponse response = new TokenResponse("accessToken"); @@ -141,7 +144,7 @@ void reissue_invalidRefreshTokenHeader() throws Exception { void reissue_refreshTokenNotFound() throws Exception { //given ErrorResponse response = new ErrorResponse(ErrorCode.REFRESH_TOKEN_NOT_FOUND); - given(jwtService.reissue(anyString())) + given(authService.reissue(anyString())) .willThrow(new BadRequestException(ErrorCode.REFRESH_TOKEN_NOT_FOUND)); //when then @@ -156,7 +159,7 @@ void reissue_refreshTokenNotFound() throws Exception { void reissue_refreshTokenMismatched() throws Exception { //given ErrorResponse response = new ErrorResponse(ErrorCode.REFRESH_TOKEN_MISMATCHED); - given(jwtService.reissue(anyString())) + given(authService.reissue(anyString())) .willThrow(new BadRequestException(ErrorCode.REFRESH_TOKEN_MISMATCHED)); //when then diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index bd15019f..e2a23936 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -8,7 +8,6 @@ import com.swyp8team2.support.WithMockUserInfo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; @@ -24,7 +23,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(CommentController.class) class CommentControllerTest extends RestDocsTest { diff --git a/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java b/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java index 97e520a4..37c8b636 100644 --- a/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java +++ b/src/test/java/com/swyp8team2/image/presentation/ImageControllerTest.java @@ -1,17 +1,10 @@ package com.swyp8team2.image.presentation; -import com.swyp8team2.image.application.ImageService; import com.swyp8team2.image.presentation.dto.ImageFileResponse; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; @@ -31,21 +24,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(ImageController.class) -@Import(ImageControllerTest.TestConfig.class) class ImageControllerTest extends RestDocsTest { - @Autowired - private ImageService imageService; - - @TestConfiguration - static class TestConfig { - @Bean - public ImageService imageService() { - return Mockito.mock(ImageService.class); - } - } - @Test @WithMockUserInfo @DisplayName("이미지 업로드") diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index e4bd8e6a..03258d41 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -11,7 +11,6 @@ import com.swyp8team2.support.WithMockUserInfo; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; @@ -33,7 +32,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(PostController.class) class PostControllerTest extends RestDocsTest { @Test diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index ce660c2c..1d1f9a20 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -2,8 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.swyp8team2.auth.application.AuthService; -import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; +import com.swyp8team2.image.application.ImageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; @@ -20,12 +20,12 @@ public abstract class WebUnitTest { @Autowired protected ObjectMapper objectMapper; - @MockitoBean - protected JwtService jwtService; - @Autowired protected RefreshTokenCookieGenerator refreshTokenCookieGenerator; @MockitoBean protected AuthService authService; + + @MockitoBean + protected ImageService imageService; } diff --git a/src/test/java/com/swyp8team2/support/config/WebDependencyConfig.java b/src/test/java/com/swyp8team2/support/config/WebDependencyConfig.java deleted file mode 100644 index 2380be60..00000000 --- a/src/test/java/com/swyp8team2/support/config/WebDependencyConfig.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.swyp8team2.support.config; - -public class WebDependencyConfig { -} diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 67d93f56..63a47bd9 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -4,7 +4,6 @@ import com.swyp8team2.user.presentation.dto.UserInfoResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.security.test.context.support.WithMockUser; @@ -17,7 +16,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(UserController.class) class UserControllerTest extends RestDocsTest { @Test diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index 9c7a63c4..e0e8263d 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -7,7 +7,6 @@ import com.swyp8team2.vote.presentation.dto.VoteRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; @@ -24,7 +23,6 @@ import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(VoteController.class) class VoteControllerTest extends RestDocsTest { @Test From e1be7989fa30655147205127a5107aa6a78e1a38 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 12:37:30 +0900 Subject: [PATCH 092/258] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=BD=94=EB=93=9C=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/com/swyp8team2/auth/application/AuthService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 13568535..b589ea0f 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -39,7 +39,6 @@ private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { } public TokenPair reissue(String refreshToken) { - System.out.println("refreshToken = " + refreshToken); return jwtService.reissue(refreshToken); } } From c176411f827b6a1a909a87bf220ab80e25996c3a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 12:37:56 +0900 Subject: [PATCH 093/258] =?UTF-8?q?fix:=20profile=20null=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth/dto/KakaoUserInfoResponse.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java b/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java index 52ee8c90..bae00bdb 100644 --- a/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java +++ b/src/main/java/com/swyp8team2/auth/application/oauth/dto/KakaoUserInfoResponse.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.swyp8team2.auth.domain.Provider; +import java.util.Objects; + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record KakaoUserInfoResponse( String id, @@ -11,10 +13,19 @@ public record KakaoUserInfoResponse( ) { public OAuthUserInfo toOAuthUserInfo() { + String profileImageUrl; + String nickname; + if (Objects.isNull(kakaoAccount.profile())) { + profileImageUrl = null; + nickname = null; + } else { + profileImageUrl = kakaoAccount.profile().profileImageUrl(); + nickname = kakaoAccount.profile().nickname(); + } return new OAuthUserInfo( id, - kakaoAccount.profile().profileImageUrl(), - kakaoAccount.profile().nickname(), + profileImageUrl, + nickname, Provider.KAKAO ); } From 56de851a83b3800496a2326e137fbb8ed7b64491 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 14:13:35 +0900 Subject: [PATCH 094/258] =?UTF-8?q?fix:=20=EC=9C=A0=EC=A0=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20transaction=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/com/swyp8team2/auth/application/AuthService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index b589ea0f..965ad96b 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -10,6 +10,7 @@ import com.swyp8team2.user.application.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -24,6 +25,7 @@ public String getOAuthAuthorizationUrl() { return oAuthService.getOAuthAuthorizationUrl(); } + @Transactional public TokenPair oauthSignIn(String code) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code); SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( From 5c1c9ff4690173927b8841a08e132b39545ef295 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 14:14:22 +0900 Subject: [PATCH 095/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20e?= =?UTF-8?q?ntity=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ErrorCode.java | 7 +- .../java/com/swyp8team2/post/domain/Post.java | 73 +++++++++++++++++++ .../com/swyp8team2/post/domain/PostImage.java | 60 +++++++++++++++ .../post/domain/PostRepository.java | 8 ++ .../com/swyp8team2/post/domain/State.java | 5 ++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/swyp8team2/post/domain/Post.java create mode 100644 src/main/java/com/swyp8team2/post/domain/PostImage.java create mode 100644 src/main/java/com/swyp8team2/post/domain/PostRepository.java create mode 100644 src/main/java/com/swyp8team2/post/domain/State.java diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 2078a0de..aa0220ec 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -15,6 +15,9 @@ public enum ErrorCode { MISSING_FILE_EXTENSION("확장자가 누락됨"), UNSUPPORTED_FILE_EXTENSION("지원하지 않는 확장자"), EXCEED_MAX_FILE_SIZE("파일 크기 초과"), + POST_NOT_FOUND("존재하지 않는 게시글"), + DESCRIPTION_LENGTH_EXCEEDED("게시글 설명 길이 초과"), + INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"), //401 EXPIRED_TOKEN("토큰 만료"), @@ -26,9 +29,11 @@ public enum ErrorCode { INTERNAL_SERVER_ERROR("서버 내부 오류"), INVALID_INPUT_VALUE("잘못된 입력 값"), SOCIAL_AUTHENTICATION_FAILED("소셜 로그인 실패"), + POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), //503 - SERVICE_UNAVAILABLE("서비스 이용 불가"); + SERVICE_UNAVAILABLE("서비스 이용 불가"), + ; private final String message; } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java new file mode 100644 index 00000000..eebfa979 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -0,0 +1,73 @@ +package com.swyp8team2.post.domain; + +import com.swyp8team2.common.domain.BaseEntity; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static com.swyp8team2.common.util.Validator.*; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Post extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String description; + + private Long userId; + + @Enumerated(EnumType.STRING) + private State state; + + @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.ALL) + private List images = new ArrayList<>(); + + private String shareUrl; + + public Post(Long id, Long userId, String description, State state, List images, String shareUrl) { + validateNull(userId, state, description, images); + validateEmptyString(shareUrl); + validateDescription(description); + validatePostImages(images); + this.id = id; + this.description = description; + this.userId = userId; + this.state = state; + this.images = images; + images.forEach(image -> image.setPost(this)); + this.shareUrl = shareUrl; + } + + private void validatePostImages(List images) { + if (images.size() < 2) { + throw new BadRequestException(ErrorCode.INVALID_POST_IMAGE_COUNT); + } + } + + private void validateDescription(String description) { + if (description.length() > 100) { + throw new BadRequestException(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED); + } + } + + public static Post create(Long userId, String description, List images, String shareUrl) { + return new Post(null, userId, description, State.PROGRESS, images, shareUrl); + } +} diff --git a/src/main/java/com/swyp8team2/post/domain/PostImage.java b/src/main/java/com/swyp8team2/post/domain/PostImage.java new file mode 100644 index 00000000..c1295cec --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/PostImage.java @@ -0,0 +1,60 @@ +package com.swyp8team2.post.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static com.swyp8team2.common.util.Validator.validateEmptyString; +import static com.swyp8team2.common.util.Validator.validateNull; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + private String name; + + private Long imageFileId; + + private int voteCount; + + public PostImage(Long id, Post post, String name, Long imageFileId, int voteCount) { + validateNull(post, imageFileId); + validateEmptyString(name); + this.id = id; + this.post = post; + this.name = name; + this.imageFileId = imageFileId; + this.voteCount = voteCount; + } + + public PostImage(String name, Long imageFileId, int voteCount) { + validateNull(imageFileId); + validateEmptyString(name); + this.name = name; + this.imageFileId = imageFileId; + this.voteCount = voteCount; + } + + public static PostImage create(String name, Long imageFileId) { + return new PostImage(name, imageFileId, 0); + } + + public void setPost(Post post) { + validateNull(post); + this.post = post; + } +} diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java new file mode 100644 index 00000000..70bbb231 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -0,0 +1,8 @@ +package com.swyp8team2.post.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/com/swyp8team2/post/domain/State.java b/src/main/java/com/swyp8team2/post/domain/State.java new file mode 100644 index 00000000..cfdafbdd --- /dev/null +++ b/src/main/java/com/swyp8team2/post/domain/State.java @@ -0,0 +1,5 @@ +package com.swyp8team2.post.domain; + +public enum State { + PROGRESS, CLOSED +} From 808f33fa85a278a34a7dadcd64084ee509dae567 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 14:14:38 +0900 Subject: [PATCH 096/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=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 --- .../application/PostImageNameGenerator.java | 21 ++++++++++ .../post/application/PostService.java | 38 +++++++++++++++++++ .../post/presentation/PostController.java | 4 ++ .../presentation/dto/CreatePostRequest.java | 4 +- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java create mode 100644 src/main/java/com/swyp8team2/post/application/PostService.java diff --git a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java new file mode 100644 index 00000000..ab1edd99 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java @@ -0,0 +1,21 @@ +package com.swyp8team2.post.application; + +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; + +public class PostImageNameGenerator { + + private int index = 0; + private final String[] alphabets = new String[]{ + "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" + }; + + public String generate() { + if (index >= alphabets.length) { + throw new InternalServerException(ErrorCode.POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND); + } + return "뽀또" + alphabets[index++]; + } +} diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java new file mode 100644 index 00000000..7bc9b54c --- /dev/null +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -0,0 +1,38 @@ +package com.swyp8team2.post.application; + +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.user.domain.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + private final UserRepository userRepository; + + @Transactional + public Long create(Long userId, CreatePostRequest request) { + List postImages = createPostImages(request); + Post post = Post.create(userId, request.description(), postImages, "TODO: location"); + Post save = postRepository.save(post); + return save.getId(); + } + + private List createPostImages(CreatePostRequest request) { + PostImageNameGenerator nameGenerator = new PostImageNameGenerator(); + return request.votes().stream() + .map(voteRequestDto -> PostImage.create( + nameGenerator.generate(), + voteRequestDto.imageFileId() + )).toList(); + } +} diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 01945f1b..aa374101 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; @@ -28,11 +29,14 @@ @RequestMapping("/posts") public class PostController { + private final PostService postService; + @PostMapping("") public ResponseEntity createPost( @Valid @RequestBody CreatePostRequest request, @AuthenticationPrincipal UserInfo userInfo ) { + postService.create(userInfo.userId(), request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java index 3766fb83..6d3f326c 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java @@ -7,10 +7,10 @@ import java.util.List; public record CreatePostRequest( - @Size(min = 1, max = 200) + @NotNull String description, - @Valid @NotNull @Size(min = 2) + @Valid @NotNull List votes ) { } From e3a24e91111aa3da55aa592b313fddbf8e96f372 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 14:14:48 +0900 Subject: [PATCH 097/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostImageNameGeneratorTest.java | 32 ++++++ .../post/application/PostServiceTest.java | 88 +++++++++++++++ .../swyp8team2/post/domain/PostImageTest.java | 44 ++++++++ .../com/swyp8team2/post/domain/PostTest.java | 100 ++++++++++++++++++ .../post/presentation/PostControllerTest.java | 2 +- .../com/swyp8team2/support/WebUnitTest.java | 4 + 6 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java create mode 100644 src/test/java/com/swyp8team2/post/application/PostServiceTest.java create mode 100644 src/test/java/com/swyp8team2/post/domain/PostImageTest.java create mode 100644 src/test/java/com/swyp8team2/post/domain/PostTest.java diff --git a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java new file mode 100644 index 00000000..626bb21f --- /dev/null +++ b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java @@ -0,0 +1,32 @@ +package com.swyp8team2.post.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class PostImageNameGeneratorTest { + + PostImageNameGenerator postImageNameGenerator; + + @BeforeEach + void setUp() { + postImageNameGenerator = new PostImageNameGenerator(); + } + + @Test + @DisplayName("이미지 이름 생성") + void generate() throws Exception { + //given + + //when + String generate1 = postImageNameGenerator.generate(); + String generate2 = postImageNameGenerator.generate(); + + //then + assertThat(generate1).isEqualTo("뽀또A"); + assertThat(generate2).isEqualTo("뽀또B"); + } +} diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java new file mode 100644 index 00000000..b0f37a89 --- /dev/null +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -0,0 +1,88 @@ +package com.swyp8team2.post.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.VoteRequestDto; +import com.swyp8team2.support.IntegrationTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class PostServiceTest extends IntegrationTest { + + @Autowired + PostService postService; + + @Autowired + PostRepository postRepository; + + @Test + @DisplayName("게시글 작성") + void create() throws Exception { + //given + long userId = 1L; + CreatePostRequest request = new CreatePostRequest("description", List.of( + new VoteRequestDto(1L), + new VoteRequestDto(2L) + )); + + //when + Long postId = postService.create(userId, request); + + //then + Post post = postRepository.findById(postId).get(); + List images = post.getImages(); + assertAll( + () -> assertThat(post.getDescription()).isEqualTo("description"), + () -> assertThat(post.getUserId()).isEqualTo(userId), + () -> assertThat(images).hasSize(2), + () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), + () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), + () -> assertThat(images.get(0).getVoteCount()).isEqualTo(0), + () -> assertThat(images.get(1).getImageFileId()).isEqualTo(2L), + () -> assertThat(images.get(1).getName()).isEqualTo("뽀또B"), + () -> assertThat(images.get(1).getVoteCount()).isEqualTo(0) + ); + } + + @Test + @DisplayName("게시글 작성 - 이미지가 2개 미만인 경우") + void create_invalidPostImageCount() throws Exception { + //given + long userId = 1L; + CreatePostRequest request = new CreatePostRequest("description", List.of( + new VoteRequestDto(1L) + )); + + //when then + assertThatThrownBy(() -> postService.create(userId, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); + } + + @Test + @DisplayName("게시글 작성 - 설명이 100자 넘어가는 경우") + void create_descriptionCountExceeded() throws Exception { + //given + long userId = 1L; + CreatePostRequest request = new CreatePostRequest("a".repeat(101), List.of( + new VoteRequestDto(1L), + new VoteRequestDto(2L) + )); + + //when then + assertThatThrownBy(() -> postService.create(userId, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); + } +} diff --git a/src/test/java/com/swyp8team2/post/domain/PostImageTest.java b/src/test/java/com/swyp8team2/post/domain/PostImageTest.java new file mode 100644 index 00000000..433caaa2 --- /dev/null +++ b/src/test/java/com/swyp8team2/post/domain/PostImageTest.java @@ -0,0 +1,44 @@ +package com.swyp8team2.post.domain; + +import com.swyp8team2.common.exception.InternalServerException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class PostImageTest { + + @Test + @DisplayName("게시글 이미지 생성") + void create() throws Exception { + //given + String name = "뽀또A"; + long imageFileId = 1L; + + //when + PostImage postImage = PostImage.create(name, imageFileId); + + //then + assertAll( + () -> assertThat(postImage.getName()).isEqualTo(name), + () -> assertThat(postImage.getImageFileId()).isEqualTo(imageFileId), + () -> assertThat(postImage.getVoteCount()).isEqualTo(0) + ); + } + + @Test + @DisplayName("게시글 이미지 생성 - null 값이 들어온 경우") + void create_null() throws Exception { + //given + + //when then + assertAll( + () -> assertThatThrownBy(() -> PostImage.create(null, 1L)) + .isInstanceOf(InternalServerException.class), + () -> assertThatThrownBy(() -> PostImage.create("뽀또A", null)) + .isInstanceOf(InternalServerException.class) + ); + } +} diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java new file mode 100644 index 00000000..c6011385 --- /dev/null +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -0,0 +1,100 @@ +package com.swyp8team2.post.domain; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class PostTest { + + @Test + @DisplayName("게시글 생성") + void create() throws Exception { + //given + long userId = 1L; + String description = "description"; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + String shareUrl = "shareUrl"; + + //when + Post post = Post.create(userId, description, postImages, shareUrl); + + //then + List images = post.getImages(); + assertAll( + () -> assertThat(post.getUserId()).isEqualTo(userId), + () -> assertThat(post.getDescription()).isEqualTo(description), + () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), + () -> assertThat(post.getState()).isEqualTo(State.PROGRESS), + () -> assertThat(images).hasSize(2), + () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), + () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), + () -> assertThat(images.get(0).getVoteCount()).isEqualTo(0), + () -> assertThat(images.get(1).getName()).isEqualTo("뽀또B"), + () -> assertThat(images.get(1).getImageFileId()).isEqualTo(2L), + () -> assertThat(images.get(1).getVoteCount()).isEqualTo(0) + ); + } + + @Test + @DisplayName("게시글 생성 - 이미지가 2개 미만인 경우") + void create_invalidPostImageCount() throws Exception { + //given + List postImages = List.of( + PostImage.create("뽀또A", 1L) + ); + + //when then + assertThatThrownBy(() -> Post.create(1L, "description", postImages, "shareUrl")) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); + } + + @Test + @DisplayName("게시글 생성 - 설명이 100자 넘어가는 경우") + void create_descriptionCountExceeded() throws Exception { + //given + String description = "a".repeat(101); + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + + //when then + assertThatThrownBy(() -> Post.create(1L, description, postImages, "shareUrl")) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); + } + + @Test + @DisplayName("게시글 생성 - null 값이 들어오는 경우") + void create_null() throws Exception { + //given + + //when then + assertAll( + () -> assertThatThrownBy(() -> Post.create(null, "description", List.of(), "shareUrl")) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), + () -> assertThatThrownBy(() -> Post.create(1L, null, List.of(), "shareUrl")) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), + () -> assertThatThrownBy(() -> Post.create(1L, "description", List.of(), null)) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), + () -> assertThatThrownBy(() -> Post.create(1L, "description", null, "shareUrl")) + .isInstanceOf(InternalServerException.class) + .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()) + ); + } +} diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 03258d41..55cc8d81 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -56,7 +56,7 @@ void createPost() throws Exception { fieldWithPath("description") .type(JsonFieldType.STRING) .description("설명") - .attributes(constraints("1~200자 사이")), + .attributes(constraints("0~100자 사이")), fieldWithPath("votes") .type(JsonFieldType.ARRAY) .description("투표 후보") diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index 1d1f9a20..7fa26b75 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -4,6 +4,7 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; import com.swyp8team2.image.application.ImageService; +import com.swyp8team2.post.application.PostService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; @@ -28,4 +29,7 @@ public abstract class WebUnitTest { @MockitoBean protected ImageService imageService; + + @MockitoBean + protected PostService postService; } From 277b04eb7dd20a2b8bd40440ddc240467894b19b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 14:16:26 +0900 Subject: [PATCH 098/258] =?UTF-8?q?docs:=20docs=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/comments.adoc | 6 +++--- src/docs/asciidoc/posts.adoc | 8 ++++---- src/docs/asciidoc/users.adoc | 2 +- src/docs/asciidoc/votes.adoc | 8 ++++---- src/main/resources/application.yml | 17 +++++++++++++++++ 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index bd48bd44..26c4a2d3 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -2,16 +2,16 @@ == 댓글 API [[댓글-생성]] -=== 댓글 생성 +=== 댓글 생성 (미구현) operation::comment-controller-test/create-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] [[댓글-조회]] -=== 댓글 조회 +=== 댓글 조회 (미구현) operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] [[댓글-삭제]] -=== 댓글 삭제 +=== 댓글 삭제 (미구현) operation::comment-controller-test/delete-comment[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index e7262787..e52b9bc2 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -7,21 +7,21 @@ operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게시글-상세-조회]] -=== 게시글 상세 조회 +=== 게시글 상세 조회 (미구현) operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] [[내가-작성한-게시글-조회]] -=== 내가 작성한 게시글 조회 +=== 내가 작성한 게시글 조회 (미구현) operation::post-controller-test/find-my-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] [[내가-참여한-게시글-조회]] -=== 내가 참여한 게시글 조회 +=== 내가 참여한 게시글 조회 (미구현) operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] [[게시글-삭제]] -=== 게시글 삭제 +=== 게시글 삭제 (미구현) operation::post-controller-test/delete-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/docs/asciidoc/users.adoc b/src/docs/asciidoc/users.adoc index e2f16130..a342582e 100644 --- a/src/docs/asciidoc/users.adoc +++ b/src/docs/asciidoc/users.adoc @@ -2,6 +2,6 @@ == 유저 API [[유저-정보-조회]] -=== 유저 정보 조회 +=== 유저 정보 조회 (미구현) operation::user-controller-test/find-user-info[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] \ No newline at end of file diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc index c9a894a4..124e9540 100644 --- a/src/docs/asciidoc/votes.adoc +++ b/src/docs/asciidoc/votes.adoc @@ -2,21 +2,21 @@ == 투표 API [[투표]] -=== 투표 +=== 투표 (미구현) operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게스트-투표]] -=== 게스트 투표 +=== 게스트 투표 (미구현) operation::vote-controller-test/guest-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[투표-변경]] -=== 투표 변경 +=== 투표 변경 (미구현) operation::vote-controller-test/change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게스트-투표-변경]] -=== 게스트 투표 변경 +=== 게스트 투표 변경 (미구현) operation::vote-controller-test/guest-change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d3f5a12f..916e6a83 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1 +1,18 @@ +server: + port: 8080 +--- + +spring: + config: + import: classpath:application-dev.yml + activate: + on-profile: dev + +--- + +spring: + config: + import: classpath:application-prod.yml + activate: + on-profile: prod From e2236fe09c39d47ddd3593160c0ffa6d3c869435 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 15:54:51 +0900 Subject: [PATCH 099/258] =?UTF-8?q?feat:=20=EB=93=9D=ED=91=9C=EC=9C=A8=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=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 --- .../post/application/PostService.java | 24 +++++++++++++++++++ .../post/application/RatioCalculator.java | 18 ++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/main/java/com/swyp8team2/post/application/RatioCalculator.java diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 7bc9b54c..a0c96079 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,14 +1,20 @@ package com.swyp8team2.post.application; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.List; @Service @@ -35,4 +41,22 @@ private List createPostImages(CreatePostRequest request) { voteRequestDto.imageFileId() )).toList(); } + + public PostResponse find(Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + User user = userRepository.findById(post.getUserId()) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + List images = post.getImages(); + int totalVoteCount = 0; + for (PostImage image : images) { + totalVoteCount += image.getVoteCount(); + } + BigDecimal totalCount = new BigDecimal(totalVoteCount); + for (PostImage image : images) { + BigDecimal voteCount = new BigDecimal(image.getVoteCount()); + String voteRatio = voteCount.divide(totalCount, 2, RoundingMode.HALF_UP).toString(); + } + return null; + } } diff --git a/src/main/java/com/swyp8team2/post/application/RatioCalculator.java b/src/main/java/com/swyp8team2/post/application/RatioCalculator.java new file mode 100644 index 00000000..fdb2463a --- /dev/null +++ b/src/main/java/com/swyp8team2/post/application/RatioCalculator.java @@ -0,0 +1,18 @@ +package com.swyp8team2.post.application; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class RatioCalculator { + + public String calculateRatio(int totalVoteCount, int voteCount) { + if (totalVoteCount == 0) { + return "0.0"; + } + BigDecimal totalCount = new BigDecimal(totalVoteCount); + BigDecimal count = new BigDecimal(voteCount); + BigDecimal bigDecimal = count.divide(totalCount, 3, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)); + return String.format("%.1f", bigDecimal); + } +} From 697047ca65a799db49b3c9f18d27e6c128b8e19a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 15:55:02 +0900 Subject: [PATCH 100/258] =?UTF-8?q?test:=20=EB=93=9D=ED=91=9C=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- .../post/application/RatioCalculatorTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java diff --git a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java new file mode 100644 index 00000000..e0cf8b8d --- /dev/null +++ b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java @@ -0,0 +1,33 @@ +package com.swyp8team2.post.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class RatioCalculatorTest { + + RatioCalculator ratioCalculator; + + @BeforeEach + void setUp() { + ratioCalculator = new RatioCalculator(); + } + + @ParameterizedTest(name = "{index}: totalVoteCount={0}, voteCount={1} => result={2}") + @CsvSource({"3, 2, 66.7", "3, 1, 33.3", "4, 2, 50.0", "4, 3, 75.0", "0, 0, 0.0", "1, 0, 0.0", "1, 1, 100.0"}) + @DisplayName("비율 계산") + void calculate(int totalVoteCount, int voteCount, String result) throws Exception { + //given + + //when + String ratio = ratioCalculator.calculateRatio(totalVoteCount, voteCount); + + //then + assertThat(ratio).isEqualTo(result); + } +} From 84f1d05dfe68424b465d96d7c5014534c606385e Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 16:07:16 +0900 Subject: [PATCH 101/258] =?UTF-8?q?refactor:=20refresh=20token=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=EC=9D=B4=EB=A6=84=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 --- .../java/com/swyp8team2/auth/application/jwt/JwtProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java index aca72623..343f285b 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtProvider.java @@ -27,7 +27,7 @@ public class JwtProvider { private static final long ACCESS_TOKEN_EXPIRATION_MINUTES = 30; - private static final long REFRESH_TOKEN_EXPIRATION_HOUR_MINUTES = 60 * 24 * 14; + private static final long REFRESH_TOKEN_EXPIRATION_MINUTES = 60 * 24 * 14; private final Key key; private final Clock clock; @@ -51,7 +51,7 @@ public String createAccessToken(JwtClaim claim) { } public String createRefreshToken(JwtClaim claim) { - return createToken(claim, REFRESH_TOKEN_EXPIRATION_HOUR_MINUTES); + return createToken(claim, REFRESH_TOKEN_EXPIRATION_MINUTES); } private String createToken(JwtClaim claim, long expiration) { From dcf8b8f9242d1a56efc2e6b5957e9208430ef173 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 16:09:07 +0900 Subject: [PATCH 102/258] =?UTF-8?q?refactor:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=B8=EA=B0=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=9C=EA=B8=89=20=EB=B0=A9=EC=8B=9D=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 --- .../auth/application/AuthService.java | 8 ++--- .../auth/application/oauth/OAuthService.java | 19 +++--------- .../auth/presentation/AuthController.java | 15 +--------- .../presentation/dto/OAuthSignInRequest.java | 5 +++- .../common/config/KakaoOAuthConfig.java | 1 - .../auth/presentation/AuthControllerTest.java | 30 +++---------------- 6 files changed, 15 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 965ad96b..9b3898f9 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -21,13 +21,9 @@ public class AuthService { private final SocialAccountRepository socialAccountRepository; private final UserService userService; - public String getOAuthAuthorizationUrl() { - return oAuthService.getOAuthAuthorizationUrl(); - } - @Transactional - public TokenPair oauthSignIn(String code) { - OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code); + public TokenPair oauthSignIn(String code, String redirectUri) { + OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( oAuthUserInfo.socialId(), Provider.KAKAO diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java index 42d2eb1a..78c059d9 100644 --- a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.util.UriComponentsBuilder; @Slf4j @Service @@ -22,19 +21,9 @@ public class OAuthService { private final KakaoOAuthConfig kakaoOAuthConfig; private final KakaoOAuthClient kakaoOAuthClient; - public String getOAuthAuthorizationUrl() { - return UriComponentsBuilder - .fromUriString(kakaoOAuthConfig.authorizationUri()) - .queryParam("client_id", kakaoOAuthConfig.clientId()) - .queryParam("redirect_uri", kakaoOAuthConfig.redirectUri()) - .queryParam("response_type", "code") - .queryParam("scope", String.join(",", kakaoOAuthConfig.scope())) - .toUriString(); - } - - public OAuthUserInfo getUserInfo(String code) { + public OAuthUserInfo getUserInfo(String code, String redirectUri) { try { - KakaoAuthResponse kakaoAuthResponse = kakaoOAuthClient.fetchToken(tokenRequestParams(code)); + KakaoAuthResponse kakaoAuthResponse = kakaoOAuthClient.fetchToken(tokenRequestParams(code, redirectUri)); return kakaoOAuthClient .fetchUserInfo(BEARER + kakaoAuthResponse.accessToken()) .toOAuthUserInfo(); @@ -44,11 +33,11 @@ public OAuthUserInfo getUserInfo(String code) { } } - private MultiValueMap tokenRequestParams(String authCode) { + private MultiValueMap tokenRequestParams(String authCode, String redirectUri) { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); params.add("client_id", kakaoOAuthConfig.clientId()); - params.add("redirect_uri", kakaoOAuthConfig.redirectUri()); + params.add("redirect_uri", redirectUri); params.add("code", authCode); params.add("client_secret", kakaoOAuthConfig.clientSecret()); return params; diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index b39e0d2b..e7e2e908 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -2,7 +2,6 @@ import com.swyp8team2.auth.application.AuthService; -import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; @@ -13,15 +12,11 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; -import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Objects; @@ -34,20 +29,12 @@ public class AuthController { private final RefreshTokenCookieGenerator refreshTokenCookieGenerator; private final AuthService authService; - @GetMapping("/oauth2/kakao") - public ResponseEntity kakaoOAuth() { - String requestUrl = authService.getOAuthAuthorizationUrl(); - return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY) - .header(HttpHeaders.LOCATION, requestUrl) - .build(); - } - @PostMapping("/oauth2/code/kakao") public ResponseEntity kakaoOAuthSignIn( @Valid @RequestBody OAuthSignInRequest request, HttpServletResponse response ) { - TokenPair tokenPair = authService.oauthSignIn(request.code()); + TokenPair tokenPair = authService.oauthSignIn(request.code(), request.redirectUri()); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); response.addCookie(cookie); return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java b/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java index e8967052..7fa26ca1 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java @@ -4,6 +4,9 @@ public record OAuthSignInRequest( @NotNull - String code + String code, + + @NotNull + String redirectUri ) { } diff --git a/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java b/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java index 5feab34f..7fcf0fe9 100644 --- a/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java +++ b/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java @@ -7,7 +7,6 @@ public record KakaoOAuthConfig( String authorizationUri, String clientId, String clientSecret, - String redirectUri, String[] scope, String userInfoUri ) { diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index f801ff9a..fc93bafb 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; @@ -23,16 +22,12 @@ import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class AuthControllerTest extends RestDocsTest { @@ -40,33 +35,15 @@ class AuthControllerTest extends RestDocsTest { @Autowired AuthService authService; - @Test - @DisplayName("카카오 로그인 리다이렉트") - void kakaoOAuth() throws Exception { - //given - String redirectUri = "https://kakao.com/oauth2/authorize"; - given(authService.getOAuthAuthorizationUrl()).willReturn(redirectUri); - - //when then - mockMvc.perform(get("/auth/oauth2/kakao")) - .andExpect(status().isMovedPermanently()) - .andExpect(header().string(HttpHeaders.LOCATION, redirectUri)) - .andDo(restDocs.document( - responseHeaders( - headerWithName(HttpHeaders.LOCATION).description("카카오 로그인 페이지 주소") - ) - )); - } - @Test @DisplayName("카카오 로그인 코드로 토큰 발급") void kakaoOAuthSignIn() throws Exception { //given TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); TokenResponse response = new TokenResponse(expectedTokenPair.accessToken()); - given(authService.oauthSignIn(anyString())) + given(authService.oauthSignIn(anyString(), anyString())) .willReturn(expectedTokenPair); - OAuthSignInRequest request = new OAuthSignInRequest("code"); + OAuthSignInRequest request = new OAuthSignInRequest("code", "https://dev.photopic.site"); //when then mockMvc.perform(post("/auth/oauth2/code/kakao") @@ -82,7 +59,8 @@ void kakaoOAuthSignIn() throws Exception { .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 60 * 60 * 24 * 14)) .andDo(restDocs.document( requestFields( - fieldWithPath("code").description("카카오 인증 코드") + fieldWithPath("code").description("카카오 인증 코드"), + fieldWithPath("redirectUri").description("카카오 인증 redirect uri") ), responseFields( fieldWithPath("accessToken").description("액세스 토큰") From 989572b32bba79e53f5cd386727480070622c67a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 16:09:29 +0900 Subject: [PATCH 103/258] =?UTF-8?q?docs:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9D=B8=EA=B0=80=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20api=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 50d45e6c..5a2ef9b5 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -1,11 +1,6 @@ [[인증-API]] == 인증 API -[[카카오-로그인-리다이렉트]] -=== 카카오 로그인 리다이렉트 - -operation::auth-controller-test/kakao-o-auth[snippets='http-request,curl-request,http-response'] - [[카카오-로그인]] === 카카오 로그인 From b123f0f7aa8de5c61a0a175c3fa688366ce34124 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 16:09:07 +0900 Subject: [PATCH 104/258] =?UTF-8?q?refactor:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=B8=EA=B0=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=9C=EA=B8=89=20=EB=B0=A9=EC=8B=9D=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 --- .../auth/application/AuthService.java | 8 ++--- .../auth/application/oauth/OAuthService.java | 19 +++--------- .../auth/presentation/AuthController.java | 15 +--------- .../presentation/dto/OAuthSignInRequest.java | 5 +++- .../common/config/KakaoOAuthConfig.java | 1 - .../auth/application/AuthServiceTest.java | 4 +-- .../auth/presentation/AuthControllerTest.java | 30 +++---------------- 7 files changed, 17 insertions(+), 65 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 965ad96b..9b3898f9 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -21,13 +21,9 @@ public class AuthService { private final SocialAccountRepository socialAccountRepository; private final UserService userService; - public String getOAuthAuthorizationUrl() { - return oAuthService.getOAuthAuthorizationUrl(); - } - @Transactional - public TokenPair oauthSignIn(String code) { - OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code); + public TokenPair oauthSignIn(String code, String redirectUri) { + OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( oAuthUserInfo.socialId(), Provider.KAKAO diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java index 42d2eb1a..78c059d9 100644 --- a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.util.UriComponentsBuilder; @Slf4j @Service @@ -22,19 +21,9 @@ public class OAuthService { private final KakaoOAuthConfig kakaoOAuthConfig; private final KakaoOAuthClient kakaoOAuthClient; - public String getOAuthAuthorizationUrl() { - return UriComponentsBuilder - .fromUriString(kakaoOAuthConfig.authorizationUri()) - .queryParam("client_id", kakaoOAuthConfig.clientId()) - .queryParam("redirect_uri", kakaoOAuthConfig.redirectUri()) - .queryParam("response_type", "code") - .queryParam("scope", String.join(",", kakaoOAuthConfig.scope())) - .toUriString(); - } - - public OAuthUserInfo getUserInfo(String code) { + public OAuthUserInfo getUserInfo(String code, String redirectUri) { try { - KakaoAuthResponse kakaoAuthResponse = kakaoOAuthClient.fetchToken(tokenRequestParams(code)); + KakaoAuthResponse kakaoAuthResponse = kakaoOAuthClient.fetchToken(tokenRequestParams(code, redirectUri)); return kakaoOAuthClient .fetchUserInfo(BEARER + kakaoAuthResponse.accessToken()) .toOAuthUserInfo(); @@ -44,11 +33,11 @@ public OAuthUserInfo getUserInfo(String code) { } } - private MultiValueMap tokenRequestParams(String authCode) { + private MultiValueMap tokenRequestParams(String authCode, String redirectUri) { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); params.add("client_id", kakaoOAuthConfig.clientId()); - params.add("redirect_uri", kakaoOAuthConfig.redirectUri()); + params.add("redirect_uri", redirectUri); params.add("code", authCode); params.add("client_secret", kakaoOAuthConfig.clientSecret()); return params; diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index b39e0d2b..e7e2e908 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -2,7 +2,6 @@ import com.swyp8team2.auth.application.AuthService; -import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; @@ -13,15 +12,11 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; -import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Objects; @@ -34,20 +29,12 @@ public class AuthController { private final RefreshTokenCookieGenerator refreshTokenCookieGenerator; private final AuthService authService; - @GetMapping("/oauth2/kakao") - public ResponseEntity kakaoOAuth() { - String requestUrl = authService.getOAuthAuthorizationUrl(); - return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY) - .header(HttpHeaders.LOCATION, requestUrl) - .build(); - } - @PostMapping("/oauth2/code/kakao") public ResponseEntity kakaoOAuthSignIn( @Valid @RequestBody OAuthSignInRequest request, HttpServletResponse response ) { - TokenPair tokenPair = authService.oauthSignIn(request.code()); + TokenPair tokenPair = authService.oauthSignIn(request.code(), request.redirectUri()); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); response.addCookie(cookie); return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java b/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java index e8967052..7fa26ca1 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/OAuthSignInRequest.java @@ -4,6 +4,9 @@ public record OAuthSignInRequest( @NotNull - String code + String code, + + @NotNull + String redirectUri ) { } diff --git a/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java b/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java index 5feab34f..7fcf0fe9 100644 --- a/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java +++ b/src/main/java/com/swyp8team2/common/config/KakaoOAuthConfig.java @@ -7,7 +7,6 @@ public record KakaoOAuthConfig( String authorizationUri, String clientId, String clientSecret, - String redirectUri, String[] scope, String userInfoUri ) { diff --git a/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java b/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java index c5a62a40..6932bd27 100644 --- a/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java +++ b/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java @@ -45,14 +45,14 @@ class AuthServiceTest extends IntegrationTest { void oAuthSignIn() throws Exception { //given OAuthUserInfo oAuthUserInfo = new OAuthUserInfo("socialId", "profileImageUrl", "nickname", Provider.KAKAO); - given(oAuthService.getUserInfo(anyString())) + given(oAuthService.getUserInfo(anyString(), anyString())) .willReturn(oAuthUserInfo); TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); given(jwtProvider.createToken(any())) .willReturn(expectedTokenPair); //when - TokenPair tokenPair = authService.oauthSignIn("code"); + TokenPair tokenPair = authService.oauthSignIn("code", "https://dev.photopic.site"); //then SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider(oAuthUserInfo.socialId(), Provider.KAKAO).get(); diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index f801ff9a..fc93bafb 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; @@ -23,16 +22,12 @@ import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class AuthControllerTest extends RestDocsTest { @@ -40,33 +35,15 @@ class AuthControllerTest extends RestDocsTest { @Autowired AuthService authService; - @Test - @DisplayName("카카오 로그인 리다이렉트") - void kakaoOAuth() throws Exception { - //given - String redirectUri = "https://kakao.com/oauth2/authorize"; - given(authService.getOAuthAuthorizationUrl()).willReturn(redirectUri); - - //when then - mockMvc.perform(get("/auth/oauth2/kakao")) - .andExpect(status().isMovedPermanently()) - .andExpect(header().string(HttpHeaders.LOCATION, redirectUri)) - .andDo(restDocs.document( - responseHeaders( - headerWithName(HttpHeaders.LOCATION).description("카카오 로그인 페이지 주소") - ) - )); - } - @Test @DisplayName("카카오 로그인 코드로 토큰 발급") void kakaoOAuthSignIn() throws Exception { //given TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); TokenResponse response = new TokenResponse(expectedTokenPair.accessToken()); - given(authService.oauthSignIn(anyString())) + given(authService.oauthSignIn(anyString(), anyString())) .willReturn(expectedTokenPair); - OAuthSignInRequest request = new OAuthSignInRequest("code"); + OAuthSignInRequest request = new OAuthSignInRequest("code", "https://dev.photopic.site"); //when then mockMvc.perform(post("/auth/oauth2/code/kakao") @@ -82,7 +59,8 @@ void kakaoOAuthSignIn() throws Exception { .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 60 * 60 * 24 * 14)) .andDo(restDocs.document( requestFields( - fieldWithPath("code").description("카카오 인증 코드") + fieldWithPath("code").description("카카오 인증 코드"), + fieldWithPath("redirectUri").description("카카오 인증 redirect uri") ), responseFields( fieldWithPath("accessToken").description("액세스 토큰") From b4b5c50e78fe683c614974d0ae2538c849e38c86 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 16:09:29 +0900 Subject: [PATCH 105/258] =?UTF-8?q?docs:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9D=B8=EA=B0=80=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20api=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 50d45e6c..5a2ef9b5 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -1,11 +1,6 @@ [[인증-API]] == 인증 API -[[카카오-로그인-리다이렉트]] -=== 카카오 로그인 리다이렉트 - -operation::auth-controller-test/kakao-o-auth[snippets='http-request,curl-request,http-response'] - [[카카오-로그인]] === 카카오 로그인 From 9fc686808deba86fac11ace1d789d58d4cdcf381 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 20:34:35 +0900 Subject: [PATCH 106/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20i?= =?UTF-8?q?d=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=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 --- .../swyp8team2/common/domain/BaseEntity.java | 2 ++ .../common/exception/ErrorCode.java | 1 + .../post/application/PostService.java | 32 +++++++++++++++---- .../post/application/RatioCalculator.java | 3 ++ .../post/presentation/PostController.java | 4 +-- .../post/presentation/dto/AuthorDto.java | 9 ++++++ .../post/presentation/dto/PostResponse.java | 13 ++++++++ .../presentation/dto/VoteResponseDto.java | 3 +- 8 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/domain/BaseEntity.java b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java index 79ec435e..e6cff3e6 100644 --- a/src/main/java/com/swyp8team2/common/domain/BaseEntity.java +++ b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java @@ -2,12 +2,14 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +@Getter @EntityListeners(AuditingEntityListener.class) @MappedSuperclass public abstract class BaseEntity { diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index aa0220ec..b692b3d1 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -30,6 +30,7 @@ public enum ErrorCode { INVALID_INPUT_VALUE("잘못된 입력 값"), SOCIAL_AUTHENTICATION_FAILED("소셜 로그인 실패"), POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), + IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"), //503 SERVICE_UNAVAILABLE("서비스 이용 불가"), diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index a0c96079..6887f40d 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -2,11 +2,15 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.VoteResponseDto; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import lombok.RequiredArgsConstructor; @@ -24,6 +28,8 @@ public class PostService { private final PostRepository postRepository; private final UserRepository userRepository; + private final RatioCalculator ratioCalculator; + private final ImageFileRepository imageFileRepository; @Transactional public Long create(Long userId, CreatePostRequest request) { @@ -48,15 +54,29 @@ public PostResponse find(Long postId) { User user = userRepository.findById(post.getUserId()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); List images = post.getImages(); + List votes = images.stream() + .map(image -> createVoteResponseDto(image, images)) + .toList(); + return PostResponse.of(post, user, votes); + } + + private VoteResponseDto createVoteResponseDto(PostImage image, List images) { + ImageFile imageFile = imageFileRepository.findById(image.getImageFileId()) + .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); + return new VoteResponseDto( + image.getId(), + imageFile.getImageUrl(), + image.getVoteCount(), + ratioCalculator.calculateRatio(getTotalVoteCount(images), image.getVoteCount()), + false //TODO: implement + ); + } + + private int getTotalVoteCount(List images) { int totalVoteCount = 0; for (PostImage image : images) { totalVoteCount += image.getVoteCount(); } - BigDecimal totalCount = new BigDecimal(totalVoteCount); - for (PostImage image : images) { - BigDecimal voteCount = new BigDecimal(image.getVoteCount()); - String voteRatio = voteCount.divide(totalCount, 2, RoundingMode.HALF_UP).toString(); - } - return null; + return totalVoteCount; } } diff --git a/src/main/java/com/swyp8team2/post/application/RatioCalculator.java b/src/main/java/com/swyp8team2/post/application/RatioCalculator.java index fdb2463a..39e94ef9 100644 --- a/src/main/java/com/swyp8team2/post/application/RatioCalculator.java +++ b/src/main/java/com/swyp8team2/post/application/RatioCalculator.java @@ -1,8 +1,11 @@ package com.swyp8team2.post.application; +import org.springframework.stereotype.Component; + import java.math.BigDecimal; import java.math.RoundingMode; +@Component public class RatioCalculator { public String calculateRatio(int totalVoteCount, int voteCount) { diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index aa374101..3f4235d2 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -51,8 +51,8 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh ), "description", List.of( - new VoteResponseDto(1L, "https://image.photopic.site/1", 62.75, true), - new VoteResponseDto(2L, "https://image.photopic.site/2", 37.25, false) + new VoteResponseDto(1L, "https://image.photopic.site/1", 3, "60.0", true), + new VoteResponseDto(2L, "https://image.photopic.site/2", 2, "40.0", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java index 494fa7f1..f2cc0907 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/AuthorDto.java @@ -1,8 +1,17 @@ package com.swyp8team2.post.presentation.dto; +import com.swyp8team2.user.domain.User; + public record AuthorDto( Long id, String nickname, String profileUrl ) { + public static AuthorDto of(User user) { + return new AuthorDto( + user.getId(), + user.getNickname(), + user.getProfileUrl() + ); + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java index 655611bd..cd0849ae 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java @@ -1,5 +1,8 @@ package com.swyp8team2.post.presentation.dto; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.user.domain.User; + import java.time.LocalDateTime; import java.util.List; @@ -11,4 +14,14 @@ public record PostResponse( String shareUrl, LocalDateTime createdAt ) { + public static PostResponse of(Post post, User user, List votes) { + return new PostResponse( + post.getId(), + AuthorDto.of(user), + post.getDescription(), + votes, + post.getShareUrl(), + post.getCreatedAt() + ); + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java index ffc000a5..9c3bffcf 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java @@ -3,7 +3,8 @@ public record VoteResponseDto( Long id, String imageUrl, - double voteRatio, + int voteCount, + String voteRatio, boolean voted ) { } From 9d2bb4d2cae4d50d84e10fc1f211928611976c89 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 23 Feb 2025 20:34:45 +0900 Subject: [PATCH 107/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20i?= =?UTF-8?q?d=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostServiceTest.java | 57 +++++++++++++++++++ .../post/presentation/PostControllerTest.java | 7 ++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index b0f37a89..9c433f7c 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -2,12 +2,19 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; +import com.swyp8team2.image.presentation.dto.ImageFileDto; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.VoteRequestDto; +import com.swyp8team2.post.presentation.dto.VoteResponseDto; import com.swyp8team2.support.IntegrationTest; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +33,12 @@ class PostServiceTest extends IntegrationTest { @Autowired PostRepository postRepository; + @Autowired + UserRepository userRepository; + + @Autowired + ImageFileRepository imageFileRepository; + @Test @DisplayName("게시글 작성") void create() throws Exception { @@ -85,4 +98,48 @@ void create_descriptionCountExceeded() throws Exception { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } + + @Test + @DisplayName("게시글 조회") + void find() throws Exception { + //given + User user = userRepository.save(User.create("nickname", "profileUrl")); + ImageFile imageFile1 = imageFileRepository.save(ImageFile.create( + new ImageFileDto("originalFileName1", "imageUrl1", "thumbnailUrl1")) + ); + ImageFile imageFile2 = imageFileRepository.save(ImageFile.create( + new ImageFileDto("originalFileName2", "imageUrl2", "thumbnailUrl2")) + ); + Post post = postRepository.save(Post.create( + user.getId(), + "description", + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl" + )); + + //when + PostResponse response = postService.find(post.getId()); + + //then + List votes = response.votes(); + assertAll( + () -> assertThat(response.description()).isEqualTo("description"), + () -> assertThat(response.id()).isEqualTo(post.getId()), + () -> assertThat(response.author().nickname()).isEqualTo(user.getNickname()), + () -> assertThat(response.author().profileUrl()).isEqualTo(user.getProfileUrl()), + () -> assertThat(response.shareUrl()).isEqualTo("shareUrl"), + () -> assertThat(votes).hasSize(2), + () -> assertThat(votes.get(0).imageUrl()).isEqualTo(imageFile1.getImageUrl()), + () -> assertThat(votes.get(0).voteCount()).isEqualTo(0), + () -> assertThat(votes.get(0).voteRatio()).isEqualTo("0.0"), + () -> assertThat(votes.get(0).voted()).isFalse(), + () -> assertThat(votes.get(1).imageUrl()).isEqualTo(imageFile2.getImageUrl()), + () -> assertThat(votes.get(1).voteCount()).isEqualTo(0), + () -> assertThat(votes.get(1).voteRatio()).isEqualTo("0.0"), + () -> assertThat(votes.get(1).voted()).isFalse() + ); + } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 55cc8d81..86226a9d 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -81,8 +81,8 @@ void findPost() throws Exception { ), "description", List.of( - new VoteResponseDto(1L, "https://image.photopic.site/1", 62.75, true), - new VoteResponseDto(2L, "https://image.photopic.site/2", 37.25, false) + new VoteResponseDto(1L, "https://image.photopic.site/1", 3, "60.0", true), + new VoteResponseDto(2L, "https://image.photopic.site/2", 2, "40.0", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) @@ -106,7 +106,8 @@ void findPost() throws Exception { fieldWithPath("votes[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), fieldWithPath("votes[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), fieldWithPath("votes[].imageUrl").type(JsonFieldType.STRING).description("투표 이미지"), - fieldWithPath("votes[].voteRatio").type(JsonFieldType.NUMBER).description("득표 비율"), + fieldWithPath("votes[].voteRatio").type(JsonFieldType.STRING).description("득표 비율"), + fieldWithPath("votes[].voteCount").type(JsonFieldType.NUMBER).description("득표 수"), fieldWithPath("votes[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간") From 0ace5bd1fe3540fccca0f2a8e777df33b07d4483 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 08:00:00 +0900 Subject: [PATCH 108/258] =?UTF-8?q?chore:=20BaseEntity=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9D=BC=EC=9E=90=20=EC=88=98=EC=A0=95=EC=9D=BC?= =?UTF-8?q?=EC=9E=90=20=EC=9E=90=EB=8F=99=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/comment/application/CommentService.java | 2 ++ src/main/java/com/swyp8team2/comment/domain/Comment.java | 2 ++ .../com/swyp8team2/comment/domain/CommentRepository.java | 2 ++ .../java/com/swyp8team2/common/config/JpaConfig.java | 9 +++++++++ .../com/swyp8team2/common/domain/AuditorAwareImpl.java | 2 ++ .../comment/application/CommentServiceTest.java | 2 ++ .../swyp8team2/comment/domain/CommentRepositoryTest.java | 4 ++++ 7 files changed, 23 insertions(+) create mode 100644 src/main/java/com/swyp8team2/comment/application/CommentService.java create mode 100644 src/main/java/com/swyp8team2/comment/domain/Comment.java create mode 100644 src/main/java/com/swyp8team2/comment/domain/CommentRepository.java create mode 100644 src/main/java/com/swyp8team2/common/config/JpaConfig.java create mode 100644 src/main/java/com/swyp8team2/common/domain/AuditorAwareImpl.java create mode 100644 src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java create mode 100644 src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java new file mode 100644 index 00000000..673d562c --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -0,0 +1,2 @@ +package com.swyp8team2.comment.application;public class CommentService { +} diff --git a/src/main/java/com/swyp8team2/comment/domain/Comment.java b/src/main/java/com/swyp8team2/comment/domain/Comment.java new file mode 100644 index 00000000..1f266af2 --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/domain/Comment.java @@ -0,0 +1,2 @@ +package com.swyp8team2.comment.domain;public class Comment { +} diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java new file mode 100644 index 00000000..ccd7b8c9 --- /dev/null +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -0,0 +1,2 @@ +package com.swyp8team2.comment.domain;public interface CommentRepository { +} diff --git a/src/main/java/com/swyp8team2/common/config/JpaConfig.java b/src/main/java/com/swyp8team2/common/config/JpaConfig.java new file mode 100644 index 00000000..9109b4a3 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/JpaConfig.java @@ -0,0 +1,9 @@ +package com.swyp8team2.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/src/main/java/com/swyp8team2/common/domain/AuditorAwareImpl.java b/src/main/java/com/swyp8team2/common/domain/AuditorAwareImpl.java new file mode 100644 index 00000000..33a8add0 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/domain/AuditorAwareImpl.java @@ -0,0 +1,2 @@ +package com.swyp8team2.common.domain;public class AuditorAwareImpl { +} diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java new file mode 100644 index 00000000..a9a8a1c7 --- /dev/null +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -0,0 +1,2 @@ +package com.swyp8team2.comment.application;public class CommentServiceTest { +} diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java new file mode 100644 index 00000000..9a7197c9 --- /dev/null +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class CommentRepositoryTest { + +} \ No newline at end of file From 6a990372a35b8a793d692358b5f7a54bc1e283cf Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 10:33:24 +0900 Subject: [PATCH 109/258] =?UTF-8?q?fix:=20jpa=20auditing=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/com/swyp8team2/common/config/CommonConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/swyp8team2/common/config/CommonConfig.java b/src/main/java/com/swyp8team2/common/config/CommonConfig.java index efd9c6a0..329ab834 100644 --- a/src/main/java/com/swyp8team2/common/config/CommonConfig.java +++ b/src/main/java/com/swyp8team2/common/config/CommonConfig.java @@ -3,10 +3,12 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import java.time.Clock; @Configuration +@EnableJpaAuditing @ConfigurationPropertiesScan public class CommonConfig { From 09f53b98adc9d2c9efad0b2208ac2885b91b74d3 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 10:33:52 +0900 Subject: [PATCH 110/258] =?UTF-8?q?feat:=20=EB=82=B4=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=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 --- .../post/application/PostService.java | 21 ++++++++++++++++--- .../java/com/swyp8team2/post/domain/Post.java | 8 +++++++ .../post/domain/PostRepository.java | 16 ++++++++++++++ .../post/presentation/PostController.java | 13 +----------- .../presentation/dto/SimplePostResponse.java | 10 +++++++++ 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 6887f40d..e9e0e740 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,5 +1,6 @@ package com.swyp8team2.post.application; +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; @@ -10,15 +11,17 @@ import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.VoteResponseDto; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.List; @Service @@ -48,7 +51,7 @@ private List createPostImages(CreatePostRequest request) { )).toList(); } - public PostResponse find(Long postId) { + public PostResponse findById(Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); User user = userRepository.findById(post.getUserId()) @@ -79,4 +82,16 @@ private int getTotalVoteCount(List images) { } return totalVoteCount; } + + public CursorBasePaginatedResponse findMyPosts(Long userId, Long cursor, int size) { + Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.of(0, size)); + return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse) + ); + } + + private SimplePostResponse createSimplePostResponse(Post post) { + ImageFile bestPickedImage = imageFileRepository.findById(post.getBestPickedImage().getImageFileId()) + .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); + return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index eebfa979..567639d5 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.domain.BaseEntity; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -16,6 +17,7 @@ import lombok.NoArgsConstructor; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import static com.swyp8team2.common.util.Validator.*; @@ -70,4 +72,10 @@ private void validateDescription(String description) { public static Post create(Long userId, String description, List images, String shareUrl) { return new Post(null, userId, description, State.PROGRESS, images, shareUrl); } + + public PostImage getBestPickedImage() { + return images.stream() + .max(Comparator.comparing(PostImage::getVoteCount)) + .orElseThrow(() -> new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR)); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java index 70bbb231..66781a86 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -1,8 +1,24 @@ package com.swyp8team2.post.domain; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface PostRepository extends JpaRepository { + + @Query(""" + SELECT p + FROM Post p + WHERE p.userId = :userId + AND (:postId IS NULL OR p.id < :postId) + ORDER BY p.createdAt DESC + """ + ) + Slice findByUserId(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 3f4235d2..0c072f91 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -73,18 +73,7 @@ public ResponseEntity> findMyPos @RequestParam(name = "size", required = false, defaultValue = "10") int size, @AuthenticationPrincipal UserInfo userInfo ) { - return ResponseEntity.ok(new CursorBasePaginatedResponse<>( - 1L, - false, - List.of( - new SimplePostResponse( - 1L, - "https://image.photopic.site/1", - "https://photopic.site/shareurl", - LocalDateTime.of(2025, 2, 13, 12, 0) - ) - ) - )); + return ResponseEntity.ok(postService.findMyPosts(userInfo.userId(), cursor, size)); } @GetMapping("/voted") diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java index 9c32d9bb..2613a8e3 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/SimplePostResponse.java @@ -1,6 +1,7 @@ package com.swyp8team2.post.presentation.dto; import com.swyp8team2.common.dto.CursorDto; +import com.swyp8team2.post.domain.Post; import java.time.LocalDateTime; @@ -11,6 +12,15 @@ public record SimplePostResponse( LocalDateTime createdAt ) implements CursorDto { + public static SimplePostResponse of(Post post, String bestPickedImageUrl) { + return new SimplePostResponse( + post.getId(), + bestPickedImageUrl, + post.getShareUrl(), + post.getCreatedAt() + ); + } + @Override public long getId() { return id; From c0cbe3937fdef2ceaa0188648bab7b069b008f64 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 10:34:11 +0900 Subject: [PATCH 111/258] =?UTF-8?q?test:=20=EB=82=B4=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- .../post/application/PostServiceTest.java | 105 ++++++++++++++---- .../post/presentation/PostControllerTest.java | 5 +- 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 9c433f7c..08a5577e 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -10,6 +10,7 @@ import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.VoteRequestDto; import com.swyp8team2.post.presentation.dto.VoteResponseDto; import com.swyp8team2.support.IntegrationTest; @@ -19,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -101,36 +103,24 @@ void create_descriptionCountExceeded() throws Exception { @Test @DisplayName("게시글 조회") - void find() throws Exception { + void findById() throws Exception { //given - User user = userRepository.save(User.create("nickname", "profileUrl")); - ImageFile imageFile1 = imageFileRepository.save(ImageFile.create( - new ImageFileDto("originalFileName1", "imageUrl1", "thumbnailUrl1")) - ); - ImageFile imageFile2 = imageFileRepository.save(ImageFile.create( - new ImageFileDto("originalFileName2", "imageUrl2", "thumbnailUrl2")) - ); - Post post = postRepository.save(Post.create( - user.getId(), - "description", - List.of( - PostImage.create("뽀또A", imageFile1.getId()), - PostImage.create("뽀또B", imageFile2.getId()) - ), - "shareUrl" - )); + User user = createUser(1); + ImageFile imageFile1 = createImageFile(1); + ImageFile imageFile2 = createImageFile(2); + Post post = createPost(user, imageFile1, imageFile2, 1); //when - PostResponse response = postService.find(post.getId()); + PostResponse response = postService.findById(post.getId()); //then List votes = response.votes(); assertAll( - () -> assertThat(response.description()).isEqualTo("description"), + () -> assertThat(response.description()).isEqualTo(post.getDescription()), () -> assertThat(response.id()).isEqualTo(post.getId()), () -> assertThat(response.author().nickname()).isEqualTo(user.getNickname()), () -> assertThat(response.author().profileUrl()).isEqualTo(user.getProfileUrl()), - () -> assertThat(response.shareUrl()).isEqualTo("shareUrl"), + () -> assertThat(response.shareUrl()).isEqualTo(post.getShareUrl()), () -> assertThat(votes).hasSize(2), () -> assertThat(votes.get(0).imageUrl()).isEqualTo(imageFile1.getImageUrl()), () -> assertThat(votes.get(0).voteCount()).isEqualTo(0), @@ -142,4 +132,79 @@ void find() throws Exception { () -> assertThat(votes.get(1).voted()).isFalse() ); } + + @Test + @DisplayName("내가 작성한 게시글 조회 - 커서 null인 경우") + void findMyPosts() throws Exception { + //given + User user = createUser(1); + List posts = new ArrayList<>(); + for (int i = 0; i < 30; i += 2) { + ImageFile imageFile1 = createImageFile(i); + ImageFile imageFile2 = createImageFile(i + 1); + posts.add(createPost(user, imageFile1, imageFile2, i)); + } + int size = 10; + + //when + var response = postService.findMyPosts(user.getId(), null, size); + + //then + assertAll( + () -> assertThat(response.data()).hasSize(size), + () -> assertThat(response.hasNext()).isTrue(), + () -> assertThat(response.nextCursor()).isEqualTo(posts.get(posts.size() - size).getId()) + ); + } + + @Test + @DisplayName("내가 작성한 게시글 조회 - 커서 있는 경우") + void findMyPosts2() throws Exception { + //given + User user = createUser(1); + List posts = new ArrayList<>(); + for (int i = 0; i < 30; i += 2) { + ImageFile imageFile1 = createImageFile(i); + ImageFile imageFile2 = createImageFile(i + 1); + posts.add(createPost(user, imageFile1, imageFile2, i)); + } + int size = 10; + + //when + var response = postService.findMyPosts(user.getId(), posts.get(3).getId(), size); + + //then + assertAll( + () -> assertThat(response.data()).hasSize(3), + () -> assertThat(response.hasNext()).isFalse(), + () -> assertThat(response.nextCursor()).isEqualTo(posts.get(0).getId()) + ); + } + + private Post createPost(User user, ImageFile imageFile1, ImageFile imageFile2, int index) { + Post post = postRepository.save(Post.create( + user.getId(), + "description" + index, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl" + index + )); + return post; + } + + private User createUser(int index) { + return userRepository.save(User.create("nickname" + index, "profileUrl" + index)); + } + + private ImageFile createImageFile(int index) { + return imageFileRepository.save(ImageFile.create( + new ImageFileDto( + "originalFileName" + index, + "imageUrl" + index, + "thumbnailUrl" + index + ) + )); + } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 86226a9d..394604ea 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -20,6 +20,7 @@ import java.time.LocalDateTime; import java.util.List; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -138,7 +139,7 @@ void deletePost() throws Exception { @DisplayName("내가 작성한 게시글 조회") void findMyPost() throws Exception { //given - CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( + var response = new CursorBasePaginatedResponse<>( 1L, false, List.of( @@ -150,6 +151,8 @@ void findMyPost() throws Exception { ) ) ); + given(postService.findMyPosts(1L, null, 10)) + .willReturn(response); //when then mockMvc.perform(get("/posts/me") From daa4420c6619a7063ef398afd54950ddc8047e4f Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 12:15:37 +0900 Subject: [PATCH 112/258] =?UTF-8?q?chore:=20BaseEntity=20Getter=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 --- .../java/com/swyp8team2/common/domain/BaseEntity.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/domain/BaseEntity.java b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java index 79ec435e..f02fdd28 100644 --- a/src/main/java/com/swyp8team2/common/domain/BaseEntity.java +++ b/src/main/java/com/swyp8team2/common/domain/BaseEntity.java @@ -2,7 +2,10 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -10,14 +13,17 @@ @EntityListeners(AuditingEntityListener.class) @MappedSuperclass +@Getter public abstract class BaseEntity { - private String createdBy; + @CreatedBy + private Long createdBy; @CreatedDate private LocalDateTime createdAt; - private String updatedBy; + @LastModifiedBy + private Long updatedBy; @LastModifiedDate private LocalDateTime updatedAt; From 73d4f5d44d9f0c913b9e33e8ddaf4faa6948486c Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 12:18:04 +0900 Subject: [PATCH 113/258] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 44 ++++++++++++++++- .../swyp8team2/comment/domain/Comment.java | 48 ++++++++++++++++++- .../comment/domain/CommentRepository.java | 24 +++++++++- .../presentation/CommentController.java | 20 +++----- .../presentation/dto/CommentResponse.java | 22 ++++++++- 5 files changed, 139 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index 673d562c..f5f9df09 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -1,2 +1,44 @@ -package com.swyp8team2.comment.application;public class CommentService { +package com.swyp8team2.comment.application; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.comment.domain.Comment; +import com.swyp8team2.comment.domain.CommentRepository; +import com.swyp8team2.comment.presentation.dto.CommentResponse; +import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Slf4j +public class CommentService { + + private final UserRepository userRepository; + private final CommentRepository commentRepository; + + @Transactional + public void createComment(Long postId, CreateCommentRequest request, UserInfo userInfo) { + Comment comment = new Comment(postId, userInfo.userId(), request.content()); + commentRepository.save(comment); + } + + public CursorBasePaginatedResponse selectComments(Long postId, Long cursor, int size) { + Slice commentSlice = commentRepository.findByPostId(postId, cursor, PageRequest.of(0, size)); + return CursorBasePaginatedResponse.of(commentSlice.map(this::createCommentResponse)); + } + + private CommentResponse createCommentResponse(Comment comment) { + Optional user = userRepository.findById(comment.getUserNo()); + return CommentResponse.of(comment, user.get()); + } } diff --git a/src/main/java/com/swyp8team2/comment/domain/Comment.java b/src/main/java/com/swyp8team2/comment/domain/Comment.java index 1f266af2..68a56d49 100644 --- a/src/main/java/com/swyp8team2/comment/domain/Comment.java +++ b/src/main/java/com/swyp8team2/comment/domain/Comment.java @@ -1,2 +1,48 @@ -package com.swyp8team2.comment.domain;public class Comment { +package com.swyp8team2.comment.domain; + +import com.swyp8team2.common.domain.BaseEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static com.swyp8team2.common.util.Validator.validateEmptyString; +import static com.swyp8team2.common.util.Validator.validateNull; + +@Entity +@Getter +@Table(name = "comments") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Comment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private Long postId; + + @NotNull + private Long userNo; + + @NotNull + private String content; + + public Comment(Long id, Long postId, Long userNo, String content) { + validateNull(postId, userNo); + validateEmptyString(content); + this.id = id; + this.postId = postId; + this.userNo = userNo; + this.content = content; + } + + public Comment(Long postId, Long userNo, String content) { + validateNull(postId, userNo); + validateEmptyString(content); + this.postId = postId; + this.userNo = userNo; + this.content = content; + } } diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java index ccd7b8c9..8960a024 100644 --- a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -1,2 +1,24 @@ -package com.swyp8team2.comment.domain;public interface CommentRepository { +package com.swyp8team2.comment.domain; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommentRepository extends JpaRepository { + + @Query(""" + SELECT c + FROM Comment c + WHERE c.postId = :postId + AND (:cursor is null or c.id > :cursor) + ORDER BY c.createdAt DESC + """) + Slice findByPostId( + Long postId, + Long cursor, + Pageable pageable); + } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index 2fc9cb9a..5aa30415 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -1,6 +1,7 @@ package com.swyp8team2.comment.presentation; import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.comment.application.CommentService; import com.swyp8team2.comment.presentation.dto.AuthorDto; import com.swyp8team2.comment.presentation.dto.CommentResponse; import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; @@ -26,35 +27,26 @@ @RequestMapping("/posts/{postId}/comments") public class CommentController { + private final CommentService commentService; + @PostMapping("") public ResponseEntity createComment( @PathVariable("postId") Long postId, @Valid @RequestBody CreateCommentRequest request, @AuthenticationPrincipal UserInfo userInfo ) { + commentService.createComment(postId, request, userInfo); return ResponseEntity.ok().build(); } @GetMapping("") - public ResponseEntity> findComments( + public ResponseEntity> selectComments( @PathVariable("postId") Long postId, @RequestParam(value = "cursor", required = false) Long cursor, @RequestParam(value = "size", required = false, defaultValue = "10") int size, @AuthenticationPrincipal UserInfo userInfo ) { - CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( - 1L, - false, - List.of( - new CommentResponse( - 1L, - "content", - new AuthorDto(1L, "author", "https://image.com/profile-image"), - 1L, - LocalDateTime.of(2025, 2, 13, 12, 0) - ) - ) - ); + CursorBasePaginatedResponse response = commentService.selectComments(postId, cursor, size); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index b7fe2f3b..b7ddeeb1 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -1,12 +1,30 @@ package com.swyp8team2.comment.presentation.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.swyp8team2.comment.domain.Comment; +import com.swyp8team2.common.dto.CursorDto; +import com.swyp8team2.user.domain.User; + import java.time.LocalDateTime; public record CommentResponse( Long commentId, String content, AuthorDto author, - Long voteId, LocalDateTime createdAt -) { +) implements CursorDto { + + @Override + @JsonIgnore + public long getId() { + return commentId; + } + + public static CommentResponse of(Comment comment, User user) { + return new CommentResponse(comment.getId(), + comment.getContent(), + new AuthorDto(user.getId(), user.getNickname(), user.getProfileUrl()), + comment.getCreatedAt() + ); + } } From 61ce5be9fd35f37c3fbdd71293a49d01fdfed8f8 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 12:19:38 +0900 Subject: [PATCH 114/258] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/comments.adoc | 4 +- .../application/CommentServiceTest.java | 92 ++++++++++++++++++- .../comment/domain/CommentRepositoryTest.java | 43 ++++++++- .../presentation/CommentControllerTest.java | 43 +++++---- .../com/swyp8team2/support/WebUnitTest.java | 4 + 5 files changed, 164 insertions(+), 22 deletions(-) diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index 26c4a2d3..2321a088 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -2,12 +2,12 @@ == 댓글 API [[댓글-생성]] -=== 댓글 생성 (미구현) +=== 댓글 생성 operation::comment-controller-test/create-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] [[댓글-조회]] -=== 댓글 조회 (미구현) +=== 댓글 조회 operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index a9a8a1c7..7389c9ad 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -1,2 +1,92 @@ -package com.swyp8team2.comment.application;public class CommentServiceTest { +package com.swyp8team2.comment.application; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.comment.domain.Comment; +import com.swyp8team2.comment.domain.CommentRepository; +import com.swyp8team2.comment.presentation.dto.CommentResponse; +import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; +import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.SliceImpl; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CommentServiceTest { + + @Mock + private CommentRepository commentRepository; + + @Mock + private UserRepository userRepository; + + @InjectMocks + private CommentService commentService; + + @Test + @DisplayName("댓글 생성") + void createComment() { + // given + Long postId = 1L; + CreateCommentRequest request = new CreateCommentRequest("테스트 댓글"); + UserInfo userInfo = new UserInfo(100L); + Comment comment = new Comment(postId, userInfo.userId(), request.content()); + + // when + when(commentRepository.save(any(Comment.class))).thenReturn(comment); + + // then + assertDoesNotThrow(() -> commentService.createComment(postId, request, userInfo)); + } + + @Test + @DisplayName("댓글 조회") + void selectComments() { + // given + Long postId = 1L; + Long cursor = null; + int size = 2; + + Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); + Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); + SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); + User user = new User(100L, "닉네임","http://example.com/profile.png"); + + // Mock 설정 + given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); + // 각 댓글마다 user_no=100L 이므로, findById(100L)만 호출됨 + given(userRepository.findById(100L)).willReturn(Optional.of(user)); + + // when + CursorBasePaginatedResponse response = commentService.selectComments(postId, cursor, size); + + // then + assertThat(response.data()).hasSize(2); + + CommentResponse cr1 = response.data().get(0); + assertThat(cr1.commentId()).isEqualTo(1L); + assertThat(cr1.content()).isEqualTo("첫 번째 댓글"); + assertThat(cr1.author().nickname()).isEqualTo("닉네임"); + + CommentResponse cr2 = response.data().get(1); + assertThat(cr2.commentId()).isEqualTo(2L); + assertThat(cr2.content()).isEqualTo("두 번째 댓글"); + } + } diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index 9a7197c9..af2d8077 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -1,4 +1,43 @@ -import static org.junit.jupiter.api.Assertions.*; +package com.swyp8team2.comment.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest class CommentRepositoryTest { - + + @Autowired + private CommentRepository commentRepository; + + @Test + @DisplayName("댓글 조회") + void select_CommentUser() { + // given + Comment comment1 = new Comment(1L, 100L, "content1"); + Comment comment2 = new Comment(1L, 101L, "content2"); + Comment comment3 = new Comment(2L, 102L, "content3"); + Comment comment4 = new Comment(1L, 103L, "content4"); + commentRepository.saveAll(List.of(comment1, comment2, comment3, comment4)); + + // when + Slice result1 = commentRepository.findByPostId(1L, null, PageRequest.of(0, 10)); + + // then + assertThat(result1.getContent()).hasSize(3); + + // when2 + Slice result2 = commentRepository.findByPostId(1L, 1L, PageRequest.of(0, 10)); + + // then2 + assertThat(result2.getContent()).hasSize(2); + assertThat(result2.getContent().getFirst().getUserNo()).isEqualTo(101L); + } } \ No newline at end of file diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index e2a23936..fd32a824 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -1,5 +1,6 @@ package com.swyp8team2.comment.presentation; +import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.comment.presentation.dto.AuthorDto; import com.swyp8team2.comment.presentation.dto.CommentResponse; import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; @@ -14,8 +15,12 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; @@ -25,14 +30,16 @@ class CommentControllerTest extends RestDocsTest { - @Test @WithMockUserInfo @DisplayName("댓글 생성") void createComment() throws Exception { //given + Long postId = 1L; CreateCommentRequest request = new CreateCommentRequest("content"); + doNothing().when(commentService).createComment(eq(postId), any(CreateCommentRequest.class), any(UserInfo.class)); + //when then mockMvc.perform(post("/posts/{postId}/comments", "1") .header(HttpHeaders.AUTHORIZATION, "Bearer token") @@ -48,26 +55,30 @@ void createComment() throws Exception { fieldWithPath("content").type(JsonFieldType.STRING).description("댓글 내용").attributes(constraints("최대 ?글자")) ) )); + + verify(commentService, times(1)).createComment(eq(postId), any(CreateCommentRequest.class), any(UserInfo.class)); } @Test @WithAnonymousUser @DisplayName("댓글 조회") - void findComments() throws Exception { + void selectComments() throws Exception { //given - CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( + Long postId = 1L; + Long cursor = null; + int size = 10; + CommentResponse commentResponse = new CommentResponse( 1L, - false, - List.of( - new CommentResponse( - 1L, - "content", - new AuthorDto(1L, "author", "https://image.com/profile-image"), - 1L, - LocalDateTime.of(2025, 2, 13, 12, 0) - ) - ) + "댓글 내용", + new AuthorDto(100L, "닉네임", "http://example.com/profile.png"), + LocalDateTime.now() ); + List commentList = Collections.singletonList(commentResponse); + + CursorBasePaginatedResponse response = + new CursorBasePaginatedResponse<>(null, false, commentList); + + when(commentService.selectComments(eq(postId), eq(cursor), eq(size))).thenReturn(response); //when mockMvc.perform(get("/posts/{postId}/comments", "1")) @@ -107,15 +118,13 @@ void findComments() throws Exception { fieldWithPath("data[].author.profileUrl") .type(JsonFieldType.STRING) .description("작성자 프로필 이미지 url"), - fieldWithPath("data[].voteId") - .type(JsonFieldType.NUMBER) - .optional() - .description("작성자 투표 Id (투표 없을 시 null)"), fieldWithPath("data[].createdAt") .type(JsonFieldType.STRING) .description("댓글 작성일") ) )); + + verify(commentService, times(1)).selectComments(eq(postId), eq(cursor), eq(size)); } @Test diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index 7fa26b75..dc6291a6 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; +import com.swyp8team2.comment.application.CommentService; import com.swyp8team2.image.application.ImageService; import com.swyp8team2.post.application.PostService; import org.springframework.beans.factory.annotation.Autowired; @@ -32,4 +33,7 @@ public abstract class WebUnitTest { @MockitoBean protected PostService postService; + + @MockitoBean + protected CommentService commentService; } From 6bb8db57739f50af447d09836db3420e33907572 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 12:33:21 +0900 Subject: [PATCH 115/258] =?UTF-8?q?test:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../swyp8team2/comment/domain/CommentRepositoryTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index af2d8077..236ca023 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -23,15 +23,15 @@ void select_CommentUser() { // given Comment comment1 = new Comment(1L, 100L, "content1"); Comment comment2 = new Comment(1L, 101L, "content2"); - Comment comment3 = new Comment(2L, 102L, "content3"); - Comment comment4 = new Comment(1L, 103L, "content4"); - commentRepository.saveAll(List.of(comment1, comment2, comment3, comment4)); + Comment comment3 = new Comment(1L, 102L, "content3"); + commentRepository.saveAll(List.of(comment1, comment2, comment3)); // when Slice result1 = commentRepository.findByPostId(1L, null, PageRequest.of(0, 10)); // then assertThat(result1.getContent()).hasSize(3); + assertThat(result1.getContent().getFirst().getUserNo()).isEqualTo(100L); // when2 Slice result2 = commentRepository.findByPostId(1L, 1L, PageRequest.of(0, 10)); From df0eb28aeac57c71f3293ef151d7fbdb3d3f1208 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 12:38:14 +0900 Subject: [PATCH 116/258] =?UTF-8?q?test:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=EA=B3=BC=20CI=EC=97=90=EC=84=9C=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EA=B0=80=20=EB=8B=A4=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/comment/domain/CommentRepositoryTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index 236ca023..0e1bb068 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -31,13 +31,11 @@ void select_CommentUser() { // then assertThat(result1.getContent()).hasSize(3); - assertThat(result1.getContent().getFirst().getUserNo()).isEqualTo(100L); // when2 Slice result2 = commentRepository.findByPostId(1L, 1L, PageRequest.of(0, 10)); // then2 assertThat(result2.getContent()).hasSize(2); - assertThat(result2.getContent().getFirst().getUserNo()).isEqualTo(101L); } } \ No newline at end of file From e79050360d98e739813793ac798e90e869698355 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 12:48:39 +0900 Subject: [PATCH 117/258] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20DB=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index efab0d7d..841a21bd 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit efab0d7daa33bb58b83650d634b0843eeb28c18a +Subproject commit 841a21bd9ddc5018a1813fbbc72672b9ce825f64 From ccf3cbdda071ae4623255389aa6328dac1731031 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 12:52:25 +0900 Subject: [PATCH 118/258] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20DB=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index efab0d7d..841a21bd 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit efab0d7daa33bb58b83650d634b0843eeb28c18a +Subproject commit 841a21bd9ddc5018a1813fbbc72672b9ce825f64 From b71d8abd9e38b0d98eecd8e6f0524d867b8798fe Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 20:49:53 +0900 Subject: [PATCH 119/258] =?UTF-8?q?chore:=20import=20=EC=99=80=EC=9D=BC?= =?UTF-8?q?=EB=93=9C=EC=B9=B4=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 --- src/main/java/com/swyp8team2/comment/domain/Comment.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/comment/domain/Comment.java b/src/main/java/com/swyp8team2/comment/domain/Comment.java index 68a56d49..686c4d38 100644 --- a/src/main/java/com/swyp8team2/comment/domain/Comment.java +++ b/src/main/java/com/swyp8team2/comment/domain/Comment.java @@ -1,7 +1,11 @@ package com.swyp8team2.comment.domain; import com.swyp8team2.common.domain.BaseEntity; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.GenerationType; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Getter; From d7945735606192fc7bd3270c008f7432f5b6d13e Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 20:50:12 +0900 Subject: [PATCH 120/258] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20=EA=B8=B0?= =?UTF-8?q?=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/com/swyp8team2/user/domain/User.java | 9 +++- .../vote/application/VoteService.java | 47 +++++++++++++++++++ .../java/com/swyp8team2/vote/domain/Vote.java | 38 +++++++++++++++ .../vote/domain/VoteRepository.java | 14 ++++++ 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/swyp8team2/vote/application/VoteService.java create mode 100644 src/main/java/com/swyp8team2/vote/domain/Vote.java create mode 100644 src/main/java/com/swyp8team2/vote/domain/VoteRepository.java diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 3581f3e3..30d35a0b 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -8,6 +8,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.UUID; + import static com.swyp8team2.common.util.Validator.validateEmptyString; import static com.swyp8team2.common.util.Validator.validateNull; @@ -25,15 +27,18 @@ public class User { private String profileUrl; - public User(Long id, String nickname, String profileUrl) { + private String seq; + + public User(Long id, String nickname, String profileUrl, String seq) { validateNull(nickname, profileUrl); validateEmptyString(nickname, profileUrl); this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; + this.seq = seq; } public static User create(String nickname, String profileUrl) { - return new User(null, nickname, profileUrl); + return new User(null, nickname, profileUrl, UUID.randomUUID().toString()); } } diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java new file mode 100644 index 00000000..1c7b2bf0 --- /dev/null +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -0,0 +1,47 @@ +package com.swyp8team2.vote.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; +import com.swyp8team2.vote.domain.VoteRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class VoteService { + + private final VoteRepository voteRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + + @Transactional + public Long vote(Long userId, Long postId, Long imageId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + voteRepository.findByUserSeqAndPostId(user.getSeq(), postId) + .ifPresent(vote -> deleteExistingVote(postId, vote)); + Vote vote = createVote(postId, imageId, user); + return vote.getId(); + } + + private Vote createVote(Long postId, Long imageId, User user) { + Vote vote = voteRepository.save(Vote.of(postId, imageId, user.getSeq())); + postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) + .vote(imageId); + return vote; + } + + private void deleteExistingVote(Long postId, Vote vote) { + voteRepository.delete(vote); + postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) + .cancelVote(vote.getPostImageId()); + } +} diff --git a/src/main/java/com/swyp8team2/vote/domain/Vote.java b/src/main/java/com/swyp8team2/vote/domain/Vote.java new file mode 100644 index 00000000..dde80117 --- /dev/null +++ b/src/main/java/com/swyp8team2/vote/domain/Vote.java @@ -0,0 +1,38 @@ +package com.swyp8team2.vote.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "user_votes") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Vote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long postId; + + private Long postImageId; + + private String userSeq; + + public Vote(Long id, Long postId, Long postImageId, String userSeq) { + this.id = id; + this.postId = postId; + this.postImageId = postImageId; + this.userSeq = userSeq; + } + + public static Vote of(Long postId, Long postImageId, String userSeq) { + return new Vote(null, postId, postImageId, userSeq); + } +} diff --git a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java new file mode 100644 index 00000000..786e845f --- /dev/null +++ b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java @@ -0,0 +1,14 @@ +package com.swyp8team2.vote.domain; + +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface VoteRepository extends JpaRepository { + Optional findByUserSeqAndPostId(String userSeq, Long postId); + + Slice findByUserSeq(String seq); +} From 769fe47350e585f2ee7c43b7ccb8f1deb8526630 Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 20:50:40 +0900 Subject: [PATCH 121/258] =?UTF-8?q?chore:=20voteId=20=EC=9B=90=EB=B3=B5=20?= =?UTF-8?q?=ED=9B=84=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 11 ++++++----- .../comment/presentation/CommentController.java | 2 +- .../comment/presentation/dto/CommentResponse.java | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index f5f9df09..ad46dd31 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -6,6 +6,8 @@ import com.swyp8team2.comment.presentation.dto.CommentResponse; import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import lombok.RequiredArgsConstructor; @@ -15,8 +17,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -32,13 +32,14 @@ public void createComment(Long postId, CreateCommentRequest request, UserInfo us commentRepository.save(comment); } - public CursorBasePaginatedResponse selectComments(Long postId, Long cursor, int size) { + public CursorBasePaginatedResponse findComments(Long postId, Long cursor, int size) { Slice commentSlice = commentRepository.findByPostId(postId, cursor, PageRequest.of(0, size)); return CursorBasePaginatedResponse.of(commentSlice.map(this::createCommentResponse)); } private CommentResponse createCommentResponse(Comment comment) { - Optional user = userRepository.findById(comment.getUserNo()); - return CommentResponse.of(comment, user.get()); + User user = userRepository.findById(comment.getUserNo()) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + return CommentResponse.of(comment, user); } } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index 5aa30415..df7fc808 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -46,7 +46,7 @@ public ResponseEntity> selectCommen @RequestParam(value = "size", required = false, defaultValue = "10") int size, @AuthenticationPrincipal UserInfo userInfo ) { - CursorBasePaginatedResponse response = commentService.selectComments(postId, cursor, size); + CursorBasePaginatedResponse response = commentService.findComments(postId, cursor, size); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index b7ddeeb1..702158f2 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -11,6 +11,7 @@ public record CommentResponse( Long commentId, String content, AuthorDto author, + Long voteId, LocalDateTime createdAt ) implements CursorDto { @@ -24,6 +25,7 @@ public static CommentResponse of(Comment comment, User user) { return new CommentResponse(comment.getId(), comment.getContent(), new AuthorDto(user.getId(), user.getNickname(), user.getProfileUrl()), + null, comment.getCreatedAt() ); } From bf23dec728c1270076cee551d79f6b880e484453 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 20:50:56 +0900 Subject: [PATCH 122/258] =?UTF-8?q?test:=20=ED=88=AC=ED=91=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/support/RepositoryTest.java | 10 +++ .../support/fixture/FixtureGenerator.java | 38 ++++++++ .../vote/application/VoteServiceTest.java | 86 +++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 src/test/java/com/swyp8team2/support/RepositoryTest.java create mode 100644 src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java create mode 100644 src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java diff --git a/src/test/java/com/swyp8team2/support/RepositoryTest.java b/src/test/java/com/swyp8team2/support/RepositoryTest.java new file mode 100644 index 00000000..b12ed43f --- /dev/null +++ b/src/test/java/com/swyp8team2/support/RepositoryTest.java @@ -0,0 +1,10 @@ +package com.swyp8team2.support; + +import com.swyp8team2.common.config.CommonConfig; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(CommonConfig.class) +public abstract class RepositoryTest { +} diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java new file mode 100644 index 00000000..feafc88d --- /dev/null +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -0,0 +1,38 @@ +package com.swyp8team2.support.fixture; + +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.presentation.dto.ImageFileDto; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.user.domain.User; + +import java.util.List; + +public abstract class FixtureGenerator { + + public static Post createPost(Long userId, ImageFile imageFile1, ImageFile imageFile2, int key) { + return Post.create( + userId, + "description" + key, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl" + key + ); + } + + public static User createUser(int key) { + return User.create("nickname" + key, "profileUrl" + key); + } + + public static ImageFile createImageFile(int key) { + return ImageFile.create( + new ImageFileDto( + "originalFileName" + key, + "imageUrl" + key, + "thumbnailUrl" + key + ) + ); + } +} diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java new file mode 100644 index 00000000..e17a96db --- /dev/null +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -0,0 +1,86 @@ +package com.swyp8team2.vote.application; + +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.support.IntegrationTest; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; +import com.swyp8team2.vote.domain.VoteRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; +import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; +import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class VoteServiceTest extends IntegrationTest { + + @Autowired + VoteService voteService; + + @Autowired + UserRepository userRepository; + + @Autowired + VoteRepository voteRepository; + + @Autowired + PostRepository postRepository; + + @Autowired + ImageFileRepository imageFileRepository; + + @Test + @DisplayName("투표하기") + void vote() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + // when + Long voteId = voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + + // then + Vote vote = voteRepository.findById(voteId).get(); + Post findPost = postRepository.findById(post.getId()).get(); + assertAll( + () -> assertThat(vote.getUserSeq()).isEqualTo(user.getSeq()), + () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) + ); + } + + @Test + @DisplayName("투표하기 - 다른 이미지로 투표 변경한 경우") + void vote_change() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + + // when + Long voteId = voteService.vote(user.getId(), post.getId(), post.getImages().get(1).getId()); + + // then + Vote vote = voteRepository.findById(voteId).get(); + Post findPost = postRepository.findById(post.getId()).get(); + assertAll( + () -> assertThat(vote.getUserSeq()).isEqualTo(user.getSeq()), + () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), + () -> assertThat(findPost.getImages().get(1).getVoteCount()).isEqualTo(1) + ); + } +} From 4cdb56a44cf261fb29e619dfd6f1b1b6280b89cd Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 20:50:57 +0900 Subject: [PATCH 123/258] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=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 --- .../application/CommentServiceTest.java | 31 +++++++++++++++++-- .../presentation/CommentControllerTest.java | 9 ++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 7389c9ad..9864de8f 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -6,6 +6,8 @@ import com.swyp8team2.comment.presentation.dto.CommentResponse; import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import org.junit.jupiter.api.DisplayName; @@ -21,6 +23,7 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; @@ -57,7 +60,7 @@ void createComment() { @Test @DisplayName("댓글 조회") - void selectComments() { + void findComments() { // given Long postId = 1L; Long cursor = null; @@ -74,7 +77,7 @@ void selectComments() { given(userRepository.findById(100L)).willReturn(Optional.of(user)); // when - CursorBasePaginatedResponse response = commentService.selectComments(postId, cursor, size); + CursorBasePaginatedResponse response = commentService.findComments(postId, cursor, size); // then assertThat(response.data()).hasSize(2); @@ -89,4 +92,28 @@ void selectComments() { assertThat(cr2.content()).isEqualTo("두 번째 댓글"); } + @Test + @DisplayName("댓글 조회 - 유저 정보 없는 경우") + void findComments_userNotFound() { + // given + Long postId = 1L; + Long cursor = null; + int size = 2; + + Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); + Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); + SliceImpl commentSlice = new SliceImpl<>( + List.of(comment1, comment2), + PageRequest.of(0, size), + false + ); + + given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); + given(userRepository.findById(100L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.findComments(postId, cursor, size)) + .isInstanceOf(BadRequestException.class) + .hasMessage((ErrorCode.USER_NOT_FOUND.getMessage())); + } } diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index fd32a824..65e930b1 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -71,6 +71,7 @@ void selectComments() throws Exception { 1L, "댓글 내용", new AuthorDto(100L, "닉네임", "http://example.com/profile.png"), + null, LocalDateTime.now() ); List commentList = Collections.singletonList(commentResponse); @@ -78,7 +79,7 @@ void selectComments() throws Exception { CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>(null, false, commentList); - when(commentService.selectComments(eq(postId), eq(cursor), eq(size))).thenReturn(response); + when(commentService.findComments(eq(postId), eq(cursor), eq(size))).thenReturn(response); //when mockMvc.perform(get("/posts/{postId}/comments", "1")) @@ -118,13 +119,17 @@ void selectComments() throws Exception { fieldWithPath("data[].author.profileUrl") .type(JsonFieldType.STRING) .description("작성자 프로필 이미지 url"), + fieldWithPath("data[].voteId") + .type(JsonFieldType.NUMBER) + .optional() + .description("작성자 투표 Id (투표 없을 시 null)"), fieldWithPath("data[].createdAt") .type(JsonFieldType.STRING) .description("댓글 작성일") ) )); - verify(commentService, times(1)).selectComments(eq(postId), eq(cursor), eq(size)); + verify(commentService, times(1)).findComments(eq(postId), eq(cursor), eq(size)); } @Test From 16adb754f785a192689f95ac1304a0f4c09b2df3 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 20:52:06 +0900 Subject: [PATCH 124/258] =?UTF-8?q?feat:=20=EB=82=B4=EA=B0=80=20=ED=88=AC?= =?UTF-8?q?=ED=91=9C=ED=95=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=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 --- .../common/exception/ErrorCode.java | 1 + .../post/application/PostService.java | 15 ++++++++++++- .../java/com/swyp8team2/post/domain/Post.java | 22 ++++++++++++++++--- .../com/swyp8team2/post/domain/PostImage.java | 12 ++++++---- .../post/domain/PostRepository.java | 12 +++++++++- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index b692b3d1..0cbbdf1a 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -31,6 +31,7 @@ public enum ErrorCode { SOCIAL_AUTHENTICATION_FAILED("소셜 로그인 실패"), POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"), + POST_IMAGE_NOT_FOUND("게시글 이미지 없음"), //503 SERVICE_UNAVAILABLE("서비스 이용 불가"), diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index e9e0e740..79a19f8e 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -15,6 +15,8 @@ import com.swyp8team2.post.presentation.dto.VoteResponseDto; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; +import com.swyp8team2.vote.domain.VoteRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -33,6 +35,7 @@ public class PostService { private final UserRepository userRepository; private final RatioCalculator ratioCalculator; private final ImageFileRepository imageFileRepository; + private final VoteRepository voteRepository; @Transactional public Long create(Long userId, CreatePostRequest request) { @@ -84,7 +87,7 @@ private int getTotalVoteCount(List images) { } public CursorBasePaginatedResponse findMyPosts(Long userId, Long cursor, int size) { - Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.of(0, size)); + Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse) ); } @@ -94,4 +97,14 @@ private SimplePostResponse createSimplePostResponse(Post post) { .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); return SimplePostResponse.of(post, bestPickedImage.getThumbnailUrl()); } + + public CursorBasePaginatedResponse findVotedPosts(Long userId, Long cursor, int size) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + List postIds = voteRepository.findByUserSeq(user.getSeq()) + .map(Vote::getPostId) + .toList(); + Slice postSlice = postRepository.findByIdIn(postIds, cursor, PageRequest.ofSize(size)); + return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse)); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 567639d5..f799e21a 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -15,6 +15,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.ArrayList; import java.util.Comparator; @@ -24,6 +25,7 @@ @Getter @Entity +@ToString(exclude = "images") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Post extends BaseEntity { @@ -44,8 +46,6 @@ public class Post extends BaseEntity { private String shareUrl; public Post(Long id, Long userId, String description, State state, List images, String shareUrl) { - validateNull(userId, state, description, images); - validateEmptyString(shareUrl); validateDescription(description); validatePostImages(images); this.id = id; @@ -76,6 +76,22 @@ public static Post create(Long userId, String description, List image public PostImage getBestPickedImage() { return images.stream() .max(Comparator.comparing(PostImage::getVoteCount)) - .orElseThrow(() -> new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR)); + .orElseThrow(() -> new InternalServerException(ErrorCode.POST_IMAGE_NOT_FOUND)); + } + + public void vote(Long imageId) { + PostImage image = images.stream() + .filter(postImage -> postImage.getId().equals(imageId)) + .findFirst() + .orElseThrow(() -> new InternalServerException(ErrorCode.POST_IMAGE_NOT_FOUND)); + image.increaseVoteCount(); + } + + public void cancelVote(Long imageId) { + PostImage image = images.stream() + .filter(postImage -> postImage.getId().equals(imageId)) + .findFirst() + .orElseThrow(() -> new InternalServerException(ErrorCode.POST_IMAGE_NOT_FOUND)); + image.decreaseVoteCount(); } } diff --git a/src/main/java/com/swyp8team2/post/domain/PostImage.java b/src/main/java/com/swyp8team2/post/domain/PostImage.java index c1295cec..e24a760c 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostImage.java +++ b/src/main/java/com/swyp8team2/post/domain/PostImage.java @@ -32,8 +32,6 @@ public class PostImage { private int voteCount; public PostImage(Long id, Post post, String name, Long imageFileId, int voteCount) { - validateNull(post, imageFileId); - validateEmptyString(name); this.id = id; this.post = post; this.name = name; @@ -42,8 +40,6 @@ public PostImage(Long id, Post post, String name, Long imageFileId, int voteCoun } public PostImage(String name, Long imageFileId, int voteCount) { - validateNull(imageFileId); - validateEmptyString(name); this.name = name; this.imageFileId = imageFileId; this.voteCount = voteCount; @@ -57,4 +53,12 @@ public void setPost(Post post) { validateNull(post); this.post = post; } + + public void increaseVoteCount() { + this.voteCount++; + } + + public void decreaseVoteCount() { + this.voteCount = this.voteCount == 0 ? 0 : this.voteCount - 1; + } } diff --git a/src/main/java/com/swyp8team2/post/domain/PostRepository.java b/src/main/java/com/swyp8team2/post/domain/PostRepository.java index 66781a86..0653564c 100644 --- a/src/main/java/com/swyp8team2/post/domain/PostRepository.java +++ b/src/main/java/com/swyp8team2/post/domain/PostRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.Optional; +import java.util.List; @Repository public interface PostRepository extends JpaRepository { @@ -21,4 +21,14 @@ public interface PostRepository extends JpaRepository { """ ) Slice findByUserId(@Param("userId") Long userId, @Param("postId") Long postId, Pageable pageable); + + @Query(""" + SELECT p + FROM Post p + WHERE p.id IN :postIds + AND (:postId IS NULL OR p.id < :postId) + ORDER BY p.createdAt DESC + """ + ) + Slice findByIdIn(@Param("postIds") List postIds, @Param("postId") Long postId, Pageable pageable); } From dfe2c9486536b3509ed46c7d125314b36d9df57e Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 20:53:07 +0900 Subject: [PATCH 125/258] =?UTF-8?q?chore:=20dev=20H2=20DB=20=EC=9B=90?= =?UTF-8?q?=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 841a21bd..eac6fd57 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 841a21bd9ddc5018a1813fbbc72672b9ce825f64 +Subproject commit eac6fd571c28d4eca7401ca18d44f7802634c934 From 2b9ee31238fa1c2a33d46df0d93fa73fafe2c0cf Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 21:06:26 +0900 Subject: [PATCH 126/258] =?UTF-8?q?test:=20=EB=82=B4=EA=B0=80=20=ED=88=AC?= =?UTF-8?q?=ED=91=9C=ED=95=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- .../post/application/PostServiceTest.java | 85 ++++++++-------- .../post/domain/PostRepositoryTest.java | 96 +++++++++++++++++++ 2 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 08a5577e..85f831f1 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -16,6 +16,8 @@ import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; +import com.swyp8team2.vote.domain.VoteRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -23,6 +25,9 @@ import java.util.ArrayList; import java.util.List; +import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; +import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; +import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; @@ -41,6 +46,9 @@ class PostServiceTest extends IntegrationTest { @Autowired ImageFileRepository imageFileRepository; + @Autowired + VoteRepository voteRepository; + @Test @DisplayName("게시글 작성") void create() throws Exception { @@ -105,10 +113,10 @@ void create_descriptionCountExceeded() throws Exception { @DisplayName("게시글 조회") void findById() throws Exception { //given - User user = createUser(1); - ImageFile imageFile1 = createImageFile(1); - ImageFile imageFile2 = createImageFile(2); - Post post = createPost(user, imageFile1, imageFile2, 1); + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); //when PostResponse response = postService.findById(post.getId()); @@ -137,13 +145,8 @@ void findById() throws Exception { @DisplayName("내가 작성한 게시글 조회 - 커서 null인 경우") void findMyPosts() throws Exception { //given - User user = createUser(1); - List posts = new ArrayList<>(); - for (int i = 0; i < 30; i += 2) { - ImageFile imageFile1 = createImageFile(i); - ImageFile imageFile2 = createImageFile(i + 1); - posts.add(createPost(user, imageFile1, imageFile2, i)); - } + User user = userRepository.save(createUser(1)); + List posts = createPosts(user); int size = 10; //when @@ -161,13 +164,8 @@ void findMyPosts() throws Exception { @DisplayName("내가 작성한 게시글 조회 - 커서 있는 경우") void findMyPosts2() throws Exception { //given - User user = createUser(1); - List posts = new ArrayList<>(); - for (int i = 0; i < 30; i += 2) { - ImageFile imageFile1 = createImageFile(i); - ImageFile imageFile2 = createImageFile(i + 1); - posts.add(createPost(user, imageFile1, imageFile2, i)); - } + User user = userRepository.save(createUser(1)); + List posts = createPosts(user); int size = 10; //when @@ -181,30 +179,37 @@ void findMyPosts2() throws Exception { ); } - private Post createPost(User user, ImageFile imageFile1, ImageFile imageFile2, int index) { - Post post = postRepository.save(Post.create( - user.getId(), - "description" + index, - List.of( - PostImage.create("뽀또A", imageFile1.getId()), - PostImage.create("뽀또B", imageFile2.getId()) - ), - "shareUrl" + index - )); - return post; + private List createPosts(User user) { + List posts = new ArrayList<>(); + for (int i = 0; i < 30; i += 2) { + ImageFile imageFile1 = imageFileRepository.save(createImageFile(i)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(i + 1)); + posts.add(postRepository.save(createPost(user.getId(), imageFile1, imageFile2, i))); + } + return posts; } - private User createUser(int index) { - return userRepository.save(User.create("nickname" + index, "profileUrl" + index)); - } + @Test + @DisplayName("내가 투표한 게시글 조회 - 커서 null인 경우") + void findVotedPosts() throws Exception { + //given + User user = userRepository.save(createUser(1)); + List posts = createPosts(user); + for (int i = 0; i < 15; i++) { + Post post = posts.get(i); + voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getSeq())); + } + int size = 10; - private ImageFile createImageFile(int index) { - return imageFileRepository.save(ImageFile.create( - new ImageFileDto( - "originalFileName" + index, - "imageUrl" + index, - "thumbnailUrl" + index - ) - )); + //when + var response = postService.findVotedPosts(user.getId(), null, size); + + //then + int 전체_15개에서_맨_마지막_데이터_인덱스 = posts.size() - size; + assertAll( + () -> assertThat(response.data()).hasSize(size), + () -> assertThat(response.hasNext()).isTrue(), + () -> assertThat(response.nextCursor()).isEqualTo(posts.get(전체_15개에서_맨_마지막_데이터_인덱스).getId()) + ); } } diff --git a/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java new file mode 100644 index 00000000..e5b5157b --- /dev/null +++ b/src/test/java/com/swyp8team2/post/domain/PostRepositoryTest.java @@ -0,0 +1,96 @@ +package com.swyp8team2.post.domain; + +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.support.RepositoryTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +import java.util.ArrayList; +import java.util.List; + +import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; +import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class PostRepositoryTest extends RepositoryTest { + + @Autowired + PostRepository postRepository; + + @Test + @DisplayName("유저가 작성한 게시글 조회 - 게시글이 15개일 경우 15번쨰부터 10개 조회해야 함") + void select_post_findByUserId1() throws Exception { + //given + long userId = 1L; + List posts = createPosts(userId); + int size = 10; + + //when + Slice res = postRepository.findByUserId(userId, null, PageRequest.ofSize(size)); + + //then + assertAll( + () -> assertThat(res.getContent().size()).isEqualTo(size), + () -> assertThat(res.getContent().get(0).getId()).isEqualTo(posts.get(posts.size() - 1).getId()), + () -> assertThat(res.getContent().get(1).getId()).isEqualTo(posts.get(posts.size() - 2).getId()), + () -> assertThat(res.getContent().get(2).getId()).isEqualTo(posts.get(posts.size() - 3).getId()), + () -> assertThat(res.hasNext()).isTrue() + ); + } + + @Test + @DisplayName("유저가 작성한 게시글 조회 - 15개 중에 커서가 5번째 게시글의 id면 4번째부터 0번째까지 조회해야 함") + void select_post_findByUserId2() throws Exception { + //given + long userId = 1L; + List posts = createPosts(userId); + int size = 10; + int cursorIndex = 5; + + //when + Slice res = postRepository.findByUserId(userId, posts.get(cursorIndex).getId(), PageRequest.ofSize(size)); + + //then + assertAll( + () -> assertThat(res.getContent().size()).isEqualTo(5), + () -> assertThat(res.getContent().get(0).getId()).isEqualTo(posts.get(cursorIndex - 1).getId()), + () -> assertThat(res.getContent().get(1).getId()).isEqualTo(posts.get(cursorIndex - 2).getId()), + () -> assertThat(res.getContent().get(2).getId()).isEqualTo(posts.get(cursorIndex - 3).getId()), + () -> assertThat(res.hasNext()).isFalse() + ); + } + + @Test + @DisplayName("id 리스트에 포함되는 게시글 조회") + void select_post_findByIdIn() throws Exception { + //given + List posts = createPosts(1L); + List postIds = List.of(posts.get(0).getId(), posts.get(1).getId(), posts.get(2).getId()); + + //when + Slice postSlice = postRepository.findByIdIn(postIds, null, PageRequest.ofSize(10)); + + //then + assertAll( + () -> assertThat(postSlice.getContent().size()).isEqualTo(postIds.size()), + () -> assertThat(postSlice.getContent().get(0).getId()).isEqualTo(postIds.get(2)), + () -> assertThat(postSlice.getContent().get(1).getId()).isEqualTo(postIds.get(1)), + () -> assertThat(postSlice.getContent().get(2).getId()).isEqualTo(postIds.get(0)), + () -> assertThat(postSlice.hasNext()).isFalse() + ); + } + + private List createPosts(long userId) { + List posts = new ArrayList<>(); + for (int i = 0; i < 30; i += 2) { + ImageFile imageFile1 = createImageFile(i); + ImageFile imageFile2 = createImageFile(i + 1); + posts.add(postRepository.save(createPost(userId, imageFile1, imageFile2, i))); + } + return posts; + } +} From 5d0c52a107a9a24657f455e1a2520e913687e3b2 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Mon, 24 Feb 2025 21:06:45 +0900 Subject: [PATCH 127/258] =?UTF-8?q?feat:=20=EA=B5=AC=ED=98=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20controller=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/presentation/PostController.java | 13 +---------- .../vote/presentation/VoteController.java | 4 ++++ .../com/swyp8team2/post/domain/PostTest.java | 22 ------------------- .../post/presentation/PostControllerTest.java | 4 +++- .../com/swyp8team2/support/WebUnitTest.java | 4 ++++ 5 files changed, 12 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 0c072f91..c4b69fd9 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -82,17 +82,6 @@ public ResponseEntity> findVoted @RequestParam(name = "size", required = false, defaultValue = "10") int size, @AuthenticationPrincipal UserInfo userInfo ) { - return ResponseEntity.ok(new CursorBasePaginatedResponse<>( - 1L, - false, - List.of( - new SimplePostResponse( - 1L, - "https://image.photopic.site/1", - "https://photopic.site/shareurl", - LocalDateTime.of(2025, 2, 13, 12, 0) - ) - ) - )); + return ResponseEntity.ok(postService.findVotedPosts(userInfo.userId(), cursor, size)); } } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index 50f24370..df3e6711 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.vote.application.VoteService; import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; import com.swyp8team2.vote.presentation.dto.GuestVoteRequest; import com.swyp8team2.vote.presentation.dto.VoteRequest; @@ -24,12 +25,15 @@ @RequestMapping("/posts/{postId}/votes") public class VoteController { + private final VoteService voteService; + @PostMapping("") public ResponseEntity vote( @PathVariable("postId") Long postId, @Valid @RequestBody VoteRequest request, @AuthenticationPrincipal UserInfo userInfo ) { + voteService.vote(userInfo.userId(), postId, request.voteId()); return ResponseEntity.ok().build(); } diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index c6011385..def54c57 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -75,26 +75,4 @@ void create_descriptionCountExceeded() throws Exception { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } - - @Test - @DisplayName("게시글 생성 - null 값이 들어오는 경우") - void create_null() throws Exception { - //given - - //when then - assertAll( - () -> assertThatThrownBy(() -> Post.create(null, "description", List.of(), "shareUrl")) - .isInstanceOf(InternalServerException.class) - .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), - () -> assertThatThrownBy(() -> Post.create(1L, null, List.of(), "shareUrl")) - .isInstanceOf(InternalServerException.class) - .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), - () -> assertThatThrownBy(() -> Post.create(1L, "description", List.of(), null)) - .isInstanceOf(InternalServerException.class) - .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()), - () -> assertThatThrownBy(() -> Post.create(1L, "description", null, "shareUrl")) - .isInstanceOf(InternalServerException.class) - .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()) - ); - } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 394604ea..cca74096 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -194,7 +194,7 @@ void findMyPost() throws Exception { @DisplayName("내가 참여한 게시글 조회") void findVotedPost() throws Exception { //given - CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>( + var response = new CursorBasePaginatedResponse<>( 1L, false, List.of( @@ -206,6 +206,8 @@ void findVotedPost() throws Exception { ) ) ); + given(postService.findVotedPosts(1L, null, 10)) + .willReturn(response); //when then mockMvc.perform(get("/posts/voted") diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index 7fa26b75..1b361c8d 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -5,6 +5,7 @@ import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; import com.swyp8team2.image.application.ImageService; import com.swyp8team2.post.application.PostService; +import com.swyp8team2.vote.application.VoteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; @@ -32,4 +33,7 @@ public abstract class WebUnitTest { @MockitoBean protected PostService postService; + + @MockitoBean + protected VoteService voteService; } From f04dda2d83aee57fc78f46df985b67ad014e39bc Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 21:31:24 +0900 Subject: [PATCH 128/258] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 841a21bd..eac6fd57 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 841a21bd9ddc5018a1813fbbc72672b9ce825f64 +Subproject commit eac6fd571c28d4eca7401ca18d44f7802634c934 From 74b14d5fa7b49c1596deecb21262e0ece563d96e Mon Sep 17 00:00:00 2001 From: Nakji Date: Mon, 24 Feb 2025 21:42:56 +0900 Subject: [PATCH 129/258] =?UTF-8?q?docs:=20=EA=B0=9C=EB=B0=9C=20API=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B9=A8=EC=A7=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/comment/presentation/CommentControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index 65e930b1..3c55561e 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -62,7 +62,7 @@ void createComment() throws Exception { @Test @WithAnonymousUser @DisplayName("댓글 조회") - void selectComments() throws Exception { + void findComments() throws Exception { //given Long postId = 1L; Long cursor = null; From b242706f265a27d00e9b835693cf81cb2ce20ca6 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 09:32:24 +0900 Subject: [PATCH 130/258] =?UTF-8?q?feat:=20=EA=B0=9C=EB=B0=9C=EC=9A=A9=20?= =?UTF-8?q?=EB=8D=94=EB=AF=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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 --- .../swyp8team2/common/dev/DataInitConfig.java | 19 +++++++++ .../common/dev/DataInitializer.java | 39 +++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/swyp8team2/common/dev/DataInitConfig.java diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java b/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java new file mode 100644 index 00000000..561a4c99 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java @@ -0,0 +1,19 @@ +package com.swyp8team2.common.dev; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile({"dev", "local"}) +@Component +@RequiredArgsConstructor +public class DataInitConfig { + + private final DataInitializer dataInitializer; + + @PostConstruct + public void init() { + dataInitializer.init(); + } +} diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index af71af9e..0e7dc485 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -2,12 +2,24 @@ import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.image.domain.ImageFile; +import com.swyp8team2.image.domain.ImageFileRepository; +import com.swyp8team2.image.presentation.dto.ImageFileDto; +import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; +import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.application.VoteService; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; @Profile({"dev", "local"}) @Component @@ -15,13 +27,34 @@ public class DataInitializer { private final UserRepository userRepository; + private final ImageFileRepository imageFileRepository; + private final PostRepository postRepository; private final JwtService jwtService; + private final VoteService voteService; - @PostConstruct + @Transactional public void init() { - User save = userRepository.save(User.create("nickname", "defailt_profile_image")); - TokenPair tokenPair = jwtService.createToken(save.getId()); + User testUser = userRepository.save(User.create("nickname", "defailt_profile_image")); + TokenPair tokenPair = jwtService.createToken(testUser.getId()); System.out.println("accessToken = " + tokenPair.accessToken()); System.out.println("refreshToken = " + tokenPair.refreshToken()); + List users = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + User user = userRepository.save(User.create("nickname" + i, "defailt_profile_image")); + users.add(user); + for (int j = 0; j < 30; j += 2) { + ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); + ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); + postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl")); + } + } + List posts = postRepository.findAll(); + for (User user : users) { + for (Post post : posts) { + Random random = new Random(); + int num = random.nextInt(2); + voteService.vote(user.getId(), post.getId(), post.getImages().get(num).getId()); + } + } } } From 9a078b251e271fb6e658463aa53b7e08f5b4b1c7 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 09:36:55 +0900 Subject: [PATCH 131/258] =?UTF-8?q?docs:=20docs=20=ED=98=84=ED=99=A9=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20method=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 6 ++---- src/docs/asciidoc/images.adoc | 2 +- src/docs/asciidoc/posts.adoc | 6 +++--- src/docs/asciidoc/votes.adoc | 12 ++++++------ 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 50d45e6c..abcb28b4 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -7,13 +7,11 @@ operation::auth-controller-test/kakao-o-auth[snippets='http-request,curl-request,http-response'] [[카카오-로그인]] -=== 카카오 로그인 +=== `POST` 카카오 로그인 operation::auth-controller-test/kakao-o-auth-sign-in[snippets='http-request,curl-request,request-fields,http-response,response-cookies,response-fields'] -[[네이버-로그인]] - [[토큰-재발급]] -=== 토큰 재발급 +=== `POST` 토큰 재발급 operation::auth-controller-test/reissue[snippets='http-request,curl-request,request-cookies,http-response,response-cookies,response-fields'] diff --git a/src/docs/asciidoc/images.adoc b/src/docs/asciidoc/images.adoc index c4d9f0ec..2b08f951 100644 --- a/src/docs/asciidoc/images.adoc +++ b/src/docs/asciidoc/images.adoc @@ -2,6 +2,6 @@ == 이미지 API [[이미지-업로드]] -=== 이미지 업로드 +=== `POST` 이미지 업로드 operation::image-controller-test/create-image-file[snippets='http-request,curl-request,request-headers,request-parts,http-response,response-fields'] \ No newline at end of file diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index e52b9bc2..79fb7951 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -2,7 +2,7 @@ == 게시글 API [[게시글-작성]] -=== 게시글 작성 +=== `POST` 게시글 작성 operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-fields,http-response'] @@ -12,12 +12,12 @@ operation::post-controller-test/create-post[snippets='http-request,curl-request, operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] [[내가-작성한-게시글-조회]] -=== 내가 작성한 게시글 조회 (미구현) +=== `GET` 내가 작성한 게시글 조회 operation::post-controller-test/find-my-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] [[내가-참여한-게시글-조회]] -=== 내가 참여한 게시글 조회 (미구현) +=== `GET` 내가 참여한 게시글 조회 operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc index 124e9540..e89d4323 100644 --- a/src/docs/asciidoc/votes.adoc +++ b/src/docs/asciidoc/votes.adoc @@ -2,21 +2,21 @@ == 투표 API [[투표]] -=== 투표 (미구현) +=== `POST` 투표 operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게스트-투표]] -=== 게스트 투표 (미구현) +=== `POST` 게스트 투표 (미구현) operation::vote-controller-test/guest-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[투표-변경]] -=== 투표 변경 (미구현) +=== 투표 변경 (투표 API로 통일) -operation::vote-controller-test/change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] +// operation::vote-controller-test/change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게스트-투표-변경]] -=== 게스트 투표 변경 (미구현) +=== 게스트 투표 변경 (미구현, 게스트 투표 API로 통일) -operation::vote-controller-test/guest-change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] +// operation::vote-controller-test/guest-change-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] From 758ec9e105f525841637cbe64a11fb81f6b21c9b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 09:52:39 +0900 Subject: [PATCH 132/258] =?UTF-8?q?docs:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EA=B0=9C=EB=B0=9C=20=ED=86=A0=ED=81=B0=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/docs/asciidoc/index.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cada3319..be2279b8 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -63,6 +63,16 @@ Content-Type: application/json;charset=UTF-8 Authorization: Bearer accessToken ``` +#### 테스트용 개발 토큰 + +``` +user1 +eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE3NDAyOTQyMzEsImlzcyI6InN3eXA4dGVhbTIiLCJleHAiOjMzMjc2Mjk0MjMxfQ.gqA245tRiBQB9owKRWIpX1we1T362R-xDTt4YT9AhRY + +user2 +eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIiLCJpYXQiOjE3NDA0NDM0ODIsImlzcyI6InN3eXA4dGVhbTIiLCJleHAiOjMzMjc2NDQzNDgyfQ.2sTlCtSHb4eGzhlL6WlRT6xvJLtvipnHp6EAmC4j1UQ +``` + [[인증-예외]] === 인증 예외 From 94b14af3a5004d4027c1b058ad6a841a46097715 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 10:15:21 +0900 Subject: [PATCH 133/258] =?UTF-8?q?fix:=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/comment/domain/CommentRepository.java | 10 ++++++---- .../comment/presentation/CommentController.java | 5 +++-- .../common/exception/ApplicationControllerAdvice.java | 7 +++++++ .../swyp8team2/post/presentation/PostController.java | 9 +++++---- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java index 8960a024..dc36a80b 100644 --- a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -14,11 +15,12 @@ public interface CommentRepository extends JpaRepository { FROM Comment c WHERE c.postId = :postId AND (:cursor is null or c.id > :cursor) - ORDER BY c.createdAt DESC + ORDER BY c.createdAt DESC """) Slice findByPostId( - Long postId, - Long cursor, - Pageable pageable); + @Param("postId") Long postId, + @Param("cursor") Long cursor, + Pageable pageable + ); } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index df7fc808..9f21cba5 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -7,6 +7,7 @@ import com.swyp8team2.comment.presentation.dto.CreateCommentRequest; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -42,8 +43,8 @@ public ResponseEntity createComment( @GetMapping("") public ResponseEntity> selectComments( @PathVariable("postId") Long postId, - @RequestParam(value = "cursor", required = false) Long cursor, - @RequestParam(value = "size", required = false, defaultValue = "10") int size, + @RequestParam(value = "cursor", required = false) @Min(0) Long cursor, + @RequestParam(value = "size", required = false, defaultValue = "10") @Min(1) int size, @AuthenticationPrincipal UserInfo userInfo ) { CursorBasePaginatedResponse response = commentService.findComments(postId, cursor, size); diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 986a4f39..53ddd135 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.servlet.resource.NoResourceFoundException; import javax.naming.AuthenticationException; @@ -46,6 +47,12 @@ public ResponseEntity handle(NoResourceFoundException e) { return ResponseEntity.notFound().build(); } + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity handle(HandlerMethodValidationException e) { + return ResponseEntity.badRequest() + .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); + } + @ExceptionHandler(AuthenticationException.class) public ResponseEntity handle(AuthenticationException e) { log.info(e.getMessage()); diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index c4b69fd9..62489ca2 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -9,6 +9,7 @@ import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.VoteResponseDto; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -69,8 +70,8 @@ public ResponseEntity deletePost( @GetMapping("/me") public ResponseEntity> findMyPosts( - @RequestParam(name = "cursor", required = false) Long cursor, - @RequestParam(name = "size", required = false, defaultValue = "10") int size, + @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, + @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, @AuthenticationPrincipal UserInfo userInfo ) { return ResponseEntity.ok(postService.findMyPosts(userInfo.userId(), cursor, size)); @@ -78,8 +79,8 @@ public ResponseEntity> findMyPos @GetMapping("/voted") public ResponseEntity> findVotedPosts( - @RequestParam(name = "cursor", required = false) Long cursor, - @RequestParam(name = "size", required = false, defaultValue = "10") int size, + @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, + @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, @AuthenticationPrincipal UserInfo userInfo ) { return ResponseEntity.ok(postService.findVotedPosts(userInfo.userId(), cursor, size)); From 1c9859c3a4432a0358cb8fbf800676531aab3571 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 10:15:59 +0900 Subject: [PATCH 134/258] =?UTF-8?q?test:=20test=20=EA=B9=A8=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/common/config/CommonConfig.java | 2 -- .../comment/application/CommentServiceTest.java | 2 +- .../comment/domain/CommentRepositoryTest.java | 5 ++--- .../swyp8team2/post/domain/PostImageTest.java | 16 ---------------- .../com/swyp8team2/support/RepositoryTest.java | 4 ++-- 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/config/CommonConfig.java b/src/main/java/com/swyp8team2/common/config/CommonConfig.java index 329ab834..efd9c6a0 100644 --- a/src/main/java/com/swyp8team2/common/config/CommonConfig.java +++ b/src/main/java/com/swyp8team2/common/config/CommonConfig.java @@ -3,12 +3,10 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import java.time.Clock; @Configuration -@EnableJpaAuditing @ConfigurationPropertiesScan public class CommonConfig { diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 9864de8f..29b7f6f0 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -69,7 +69,7 @@ void findComments() { Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); - User user = new User(100L, "닉네임","http://example.com/profile.png"); + User user = new User(100L, "닉네임","http://example.com/profile.png", "seq"); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index 0e1bb068..aaa6f9ca 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -1,9 +1,9 @@ package com.swyp8team2.comment.domain; +import com.swyp8team2.support.RepositoryTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; @@ -11,8 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@DataJpaTest -class CommentRepositoryTest { +class CommentRepositoryTest extends RepositoryTest { @Autowired private CommentRepository commentRepository; diff --git a/src/test/java/com/swyp8team2/post/domain/PostImageTest.java b/src/test/java/com/swyp8team2/post/domain/PostImageTest.java index 433caaa2..2d22b4bd 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostImageTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostImageTest.java @@ -1,11 +1,9 @@ package com.swyp8team2.post.domain; -import com.swyp8team2.common.exception.InternalServerException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; class PostImageTest { @@ -27,18 +25,4 @@ void create() throws Exception { () -> assertThat(postImage.getVoteCount()).isEqualTo(0) ); } - - @Test - @DisplayName("게시글 이미지 생성 - null 값이 들어온 경우") - void create_null() throws Exception { - //given - - //when then - assertAll( - () -> assertThatThrownBy(() -> PostImage.create(null, 1L)) - .isInstanceOf(InternalServerException.class), - () -> assertThatThrownBy(() -> PostImage.create("뽀또A", null)) - .isInstanceOf(InternalServerException.class) - ); - } } diff --git a/src/test/java/com/swyp8team2/support/RepositoryTest.java b/src/test/java/com/swyp8team2/support/RepositoryTest.java index b12ed43f..9b3ded7e 100644 --- a/src/test/java/com/swyp8team2/support/RepositoryTest.java +++ b/src/test/java/com/swyp8team2/support/RepositoryTest.java @@ -1,10 +1,10 @@ package com.swyp8team2.support; -import com.swyp8team2.common.config.CommonConfig; +import com.swyp8team2.common.config.JpaConfig; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; @DataJpaTest -@Import(CommonConfig.class) +@Import(JpaConfig.class) public abstract class RepositoryTest { } From aee9244692f70a06ede5253629764e5166240755 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 11:14:04 +0900 Subject: [PATCH 135/258] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index efab0d7d..43bba82b 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit efab0d7daa33bb58b83650d634b0843eeb28c18a +Subproject commit 43bba82b898c66fca4cc4548a455805bb21e4ef1 From b6ea1ea6fa95f541eb7afba31e91aa81f3a1788a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 11:17:20 +0900 Subject: [PATCH 136/258] =?UTF-8?q?chore:=20gson=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../com/swyp8team2/common/presentation/HttpLoggingFilter.java | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java diff --git a/build.gradle b/build.gradle index eefb7630..8005ef46 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,9 @@ dependencies { implementation 'software.amazon.awssdk:s3:2.30.18' implementation 'org.imgscalr:imgscalr-lib:4.2' + // gson + implementation 'com.google.code.gson:gson:2.8.6' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java new file mode 100644 index 00000000..7f012ac8 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java @@ -0,0 +1,4 @@ +package com.swyp8team2.common.presentation; + +public class HttpLoggingFilter { +} From 1dd7ba22559c14578d4fe797060c49b7adf34169 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 11:17:41 +0900 Subject: [PATCH 137/258] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B9=85=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 --- .../com/swyp8team2/auth/application/jwt/JwtService.java | 8 ++++++++ .../swyp8team2/auth/application/oauth/OAuthService.java | 1 + .../common/exception/ApplicationControllerAdvice.java | 3 +++ .../java/com/swyp8team2/image/application/R2Storage.java | 4 +++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java index b077b422..389e0461 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java @@ -6,8 +6,10 @@ import com.swyp8team2.common.exception.ErrorCode; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public class JwtService { @@ -22,6 +24,9 @@ public TokenPair createToken(long userId) { .orElseGet(() -> new RefreshToken(userId, tokenPair.refreshToken())); refreshToken.setRefreshToken(tokenPair.refreshToken()); refreshTokenRepository.save(refreshToken); + + log.debug("createToken userId: {} accessToken: {} refreshToken: {}", + userId, tokenPair.accessToken(), tokenPair.refreshToken()); return tokenPair; } @@ -33,6 +38,9 @@ public TokenPair reissue(String refreshToken) { TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(claim.idAsLong())); findRefreshToken.rotate(refreshToken, tokenPair.refreshToken()); + + log.debug("reissue userId: {} accessToken: {} refreshToken: {}", + claim.id(), tokenPair.accessToken(), tokenPair.refreshToken()); return tokenPair; } } diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java index 78c059d9..826e77bc 100644 --- a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java @@ -24,6 +24,7 @@ public class OAuthService { public OAuthUserInfo getUserInfo(String code, String redirectUri) { try { KakaoAuthResponse kakaoAuthResponse = kakaoOAuthClient.fetchToken(tokenRequestParams(code, redirectUri)); + log.info("getUserInfo kakaoAuthResponse: {}", kakaoAuthResponse); return kakaoOAuthClient .fetchUserInfo(BEARER + kakaoAuthResponse.accessToken()) .toOAuthUserInfo(); diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 986a4f39..0128cb47 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -32,17 +32,20 @@ public ResponseEntity handle(UnauthorizedException e) { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handle(MethodArgumentNotValidException e) { + log.debug("MethodArgumentNotValidException {}", e.getMessage()); return ResponseEntity.badRequest() .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity handle(HttpRequestMethodNotSupportedException e) { + log.debug("HttpRequestMethodNotSupportedException {}", e.getMessage()); return ResponseEntity.notFound().build(); } @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity handle(NoResourceFoundException e) { + log.debug("NoResourceFoundException {}", e.getMessage()); return ResponseEntity.notFound().build(); } diff --git a/src/main/java/com/swyp8team2/image/application/R2Storage.java b/src/main/java/com/swyp8team2/image/application/R2Storage.java index 40f926cf..f735799e 100644 --- a/src/main/java/com/swyp8team2/image/application/R2Storage.java +++ b/src/main/java/com/swyp8team2/image/application/R2Storage.java @@ -63,6 +63,9 @@ public List uploadImageFile(MultipartFile... files) { String imageUrl = imageDomainUrl + realFileName; String resizeImageUrl = resizeImage(tempFile, realFileName, resizeHeight); + log.debug("uploadImageFile originFileName: {}, imageUrl: {}, resizeImageUrl: {}", + originFileName, imageUrl, resizeImageUrl); + imageFiles.add(new ImageFileDto(originFileName, imageUrl, resizeImageUrl)); tempFiles.add(originFile); } @@ -97,7 +100,6 @@ private String resizeImage(File file, String realFileName, int targetHeight) { log.error("Failed to create temp file", e); throw new ServiceUnavailableException(ErrorCode.SERVICE_UNAVAILABLE); } - } private BufferedImage highQualityResize(BufferedImage originalImage, int targetHeight) { From e3408e882bdcc09f0f25249b154cbcccbd2a32cf Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 11:17:49 +0900 Subject: [PATCH 138/258] =?UTF-8?q?feat:=20request=20response=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/HttpLoggingFilter.java | 150 +++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java index 7f012ac8..3652c75c 100644 --- a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java +++ b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java @@ -1,4 +1,152 @@ package com.swyp8team2.common.presentation; -public class HttpLoggingFilter { +import com.nimbusds.jose.shaded.gson.Gson; +import com.nimbusds.jose.shaded.gson.GsonBuilder; +import com.nimbusds.jose.shaded.gson.JsonElement; +import com.nimbusds.jose.shaded.gson.JsonParser; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.WebUtils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@Component +public class HttpLoggingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + if (request.getRequestURI().startsWith("/docs")) { + chain.doFilter(request, response); + return; + } + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + + long start = System.currentTimeMillis(); + chain.doFilter(requestWrapper, responseWrapper); + long end = System.currentTimeMillis(); + + String requestId = UUID.randomUUID().toString().substring(0, 8); + + // logback 설정 해야됨 + MDC.put("requestId", requestId); + + try { + log.info(""" + | + | [REQUEST] {} {} {} ({}s) + | Headers : {} + | RequestBody : {} + | RequestParams : {} + | Response : {} + """.trim(), + request.getMethod(), + request.getRequestURI(), + HttpStatus.valueOf(responseWrapper.getStatus()), + (end - start) / 1000.0, + getHeaders(request), + getRequestBody(requestWrapper), + getRequestParameters(request), + getResponseBody(responseWrapper) + ); + responseWrapper.copyBodyToResponse(); + } catch (Exception e) { + log.error("Logging Error", e); + } finally { + MDC.clear(); + } + } + + private String getHeaders(HttpServletRequest request) { + StringBuilder sb = new StringBuilder("{\n"); + + List loggingHeaders = List.of( + HttpHeaders.USER_AGENT, + HttpHeaders.AUTHORIZATION, + HttpHeaders.COOKIE, + HttpHeaders.CONTENT_TYPE, + HttpHeaders.HOST, + HttpHeaders.REFERER, + HttpHeaders.ORIGIN + ); + Enumeration headerArray = request.getHeaderNames(); + while (headerArray.hasMoreElements()) { + String headerName = headerArray.nextElement(); + if (loggingHeaders.contains(headerName)) { + sb.append("\t%s=%s,\n".formatted(headerName, request.getHeader(headerName))); + } + } + sb.append("}"); + return sb.toString(); + } + + private String getRequestParameters(HttpServletRequest request) { + Map paramMap = request.getParameterMap(); + StringBuilder params = new StringBuilder(); + + for (Map.Entry entry : paramMap.entrySet()) { + String key = entry.getKey(); + String[] values = entry.getValue(); + for (String value : values) { + params.append(key).append("=").append(value).append("&"); + } + } + + if (!params.isEmpty()) { + params.deleteCharAt(params.length() - 1); + } + + return params.toString(); + } + + private String getRequestBody(ContentCachingRequestWrapper request) { + ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); + assert wrapper != null; + + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + try { + return new String(buf, wrapper.getCharacterEncoding()); + } catch (UnsupportedEncodingException e) { + return ""; + } + } + return ""; + } + + private String getResponseBody(final HttpServletResponse response) throws IOException { + String payload = null; + ContentCachingResponseWrapper wrapper = + WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); + assert wrapper != null; + + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String responseBody = new String(buf, StandardCharsets.UTF_8); + JsonElement jsonElement = JsonParser.parseString(responseBody); + payload = gson.toJson(jsonElement); + } + wrapper.copyBodyToResponse(); + return Objects.isNull(payload) ? "" : payload; + } } From efff504eccd0b038376dba7b60ae4cba685e98db Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 11:20:57 +0900 Subject: [PATCH 139/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index eac6fd57..43bba82b 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit eac6fd571c28d4eca7401ca18d44f7802634c934 +Subproject commit 43bba82b898c66fca4cc4548a455805bb21e4ef1 From b230137e3fbd67965138ecc6db7b10041514d82a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 11:23:18 +0900 Subject: [PATCH 140/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 43bba82b..940191f7 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 43bba82b898c66fca4cc4548a455805bb21e4ef1 +Subproject commit 940191f742a9f5a0b0bf53e378c6728700264fce From ff78147c1160259256bbc0cb3459f46702166a5a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 11:25:42 +0900 Subject: [PATCH 141/258] =?UTF-8?q?refactor:=20http=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EB=A0=88=EB=B2=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/common/presentation/HttpLoggingFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java index 3652c75c..45d48741 100644 --- a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java +++ b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java @@ -51,7 +51,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse MDC.put("requestId", requestId); try { - log.info(""" + log.debug(""" | | [REQUEST] {} {} {} ({}s) | Headers : {} @@ -70,7 +70,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse ); responseWrapper.copyBodyToResponse(); } catch (Exception e) { - log.error("Logging Error", e); + log.debug("Logging Error", e); } finally { MDC.clear(); } From b1430a03cac0cc71e5c87823b7f7100dd5dbd2ac Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 11:34:51 +0900 Subject: [PATCH 142/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 940191f7..fdcf1abd 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 940191f742a9f5a0b0bf53e378c6728700264fce +Subproject commit fdcf1abd0df6d64328a4b99dd6d92a1ab24c7673 From e7fde7cc36ca4cc24736a9a470f254d0637559b5 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 12:22:01 +0900 Subject: [PATCH 143/258] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=ED=88=AC=ED=91=9C=20=ED=98=84=ED=99=A9?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostService.java | 35 ++++++++++++------- .../post/presentation/PostController.java | 18 ++++++++-- ...esponseDto.java => PostImageResponse.java} | 4 +-- .../post/presentation/dto/PostResponse.java | 4 +-- 4 files changed, 41 insertions(+), 20 deletions(-) rename src/main/java/com/swyp8team2/post/presentation/dto/{VoteResponseDto.java => PostImageResponse.java} (59%) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 79a19f8e..e4d8c826 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -12,19 +12,19 @@ import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; -import com.swyp8team2.post.presentation.dto.VoteResponseDto; +import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; @Service @Transactional(readOnly = true) @@ -54,30 +54,41 @@ private List createPostImages(CreatePostRequest request) { )).toList(); } - public PostResponse findById(Long postId) { + public PostResponse findById(Long userId, Long postId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - User user = userRepository.findById(post.getUserId()) + User author = userRepository.findById(post.getUserId()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + List votes = createPostImageResponse(userId, postId, post); + return PostResponse.of(post, author, votes); + } + + private List createPostImageResponse(Long userId, Long postId, Post post) { List images = post.getImages(); - List votes = images.stream() - .map(image -> createVoteResponseDto(image, images)) + return images.stream() + .map(image -> createVoteResponseDto(image, userId, postId)) .toList(); - return PostResponse.of(post, user, votes); } - private VoteResponseDto createVoteResponseDto(PostImage image, List images) { + private PostImageResponse createVoteResponseDto(PostImage image, Long userId, Long postId) { ImageFile imageFile = imageFileRepository.findById(image.getImageFileId()) .orElseThrow(() -> new InternalServerException(ErrorCode.IMAGE_FILE_NOT_FOUND)); - return new VoteResponseDto( + boolean voted = Objects.nonNull(userId) && getVoted(image, userId, postId); + return new PostImageResponse( image.getId(), imageFile.getImageUrl(), - image.getVoteCount(), - ratioCalculator.calculateRatio(getTotalVoteCount(images), image.getVoteCount()), - false //TODO: implement + voted ); } + private Boolean getVoted(PostImage image, Long userId, Long postId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + return voteRepository.findByUserSeqAndPostId(user.getSeq(), postId) + .map(vote -> vote.getPostImageId().equals(image.getId())) + .orElse(false); + } + private int getTotalVoteCount(List images) { int totalVoteCount = 0; for (PostImage image : images) { diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 62489ca2..c29b6287 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -7,7 +7,7 @@ import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; -import com.swyp8team2.post.presentation.dto.VoteResponseDto; +import com.swyp8team2.post.presentation.dto.PostImageResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; @@ -24,6 +24,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @RestController @RequiredArgsConstructor @@ -41,6 +42,17 @@ public ResponseEntity createPost( return ResponseEntity.ok().build(); } + @GetMapping("/{postId}") + public ResponseEntity findPost( + @PathVariable("shareUrl") Long postId, + @AuthenticationPrincipal UserInfo userInfo + ) { + Long userId = Optional.ofNullable(userInfo) + .map(UserInfo::userId) + .orElse(null); + return ResponseEntity.ok(postService.findById(userId, postId)); + } + @GetMapping("/{shareUrl}") public ResponseEntity findPost(@PathVariable("shareUrl") String shareUrl) { return ResponseEntity.ok(new PostResponse( @@ -52,8 +64,8 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh ), "description", List.of( - new VoteResponseDto(1L, "https://image.photopic.site/1", 3, "60.0", true), - new VoteResponseDto(2L, "https://image.photopic.site/2", 2, "40.0", false) + new PostImageResponse(1L, "https://image.photopic.site/1", true), + new PostImageResponse(2L, "https://image.photopic.site/2", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java similarity index 59% rename from src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java rename to src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java index 9c3bffcf..daed7194 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/VoteResponseDto.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java @@ -1,10 +1,8 @@ package com.swyp8team2.post.presentation.dto; -public record VoteResponseDto( +public record PostImageResponse( Long id, String imageUrl, - int voteCount, - String voteRatio, boolean voted ) { } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java index cd0849ae..78b9a1b2 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java @@ -10,11 +10,11 @@ public record PostResponse( Long id, AuthorDto author, String description, - List votes, + List votes, String shareUrl, LocalDateTime createdAt ) { - public static PostResponse of(Post post, User user, List votes) { + public static PostResponse of(Post post, User user, List votes) { return new PostResponse( post.getId(), AuthorDto.of(user), From 2bd4246b01789fd20b842d3a3b183000ca58d411 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 12:22:09 +0900 Subject: [PATCH 144/258] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/post/application/PostServiceTest.java | 12 +++--------- .../post/presentation/PostControllerTest.java | 6 +++--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 85f831f1..646318b0 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -4,15 +4,13 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; -import com.swyp8team2.image.presentation.dto.ImageFileDto; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; -import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.VoteRequestDto; -import com.swyp8team2.post.presentation.dto.VoteResponseDto; +import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -119,10 +117,10 @@ void findById() throws Exception { Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); //when - PostResponse response = postService.findById(post.getId()); + PostResponse response = postService.findById(user.getId(), post.getId()); //then - List votes = response.votes(); + List votes = response.votes(); assertAll( () -> assertThat(response.description()).isEqualTo(post.getDescription()), () -> assertThat(response.id()).isEqualTo(post.getId()), @@ -131,12 +129,8 @@ void findById() throws Exception { () -> assertThat(response.shareUrl()).isEqualTo(post.getShareUrl()), () -> assertThat(votes).hasSize(2), () -> assertThat(votes.get(0).imageUrl()).isEqualTo(imageFile1.getImageUrl()), - () -> assertThat(votes.get(0).voteCount()).isEqualTo(0), - () -> assertThat(votes.get(0).voteRatio()).isEqualTo("0.0"), () -> assertThat(votes.get(0).voted()).isFalse(), () -> assertThat(votes.get(1).imageUrl()).isEqualTo(imageFile2.getImageUrl()), - () -> assertThat(votes.get(1).voteCount()).isEqualTo(0), - () -> assertThat(votes.get(1).voteRatio()).isEqualTo("0.0"), () -> assertThat(votes.get(1).voted()).isFalse() ); } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index cca74096..9f0bd4d0 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -6,7 +6,7 @@ import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.VoteRequestDto; -import com.swyp8team2.post.presentation.dto.VoteResponseDto; +import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; import org.junit.jupiter.api.DisplayName; @@ -82,8 +82,8 @@ void findPost() throws Exception { ), "description", List.of( - new VoteResponseDto(1L, "https://image.photopic.site/1", 3, "60.0", true), - new VoteResponseDto(2L, "https://image.photopic.site/2", 2, "40.0", false) + new PostImageResponse(1L, "https://image.photopic.site/1", true), + new PostImageResponse(2L, "https://image.photopic.site/2", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) From da35ae3a7d1a79d6435b0bcce47092a02342604a Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 13:18:17 +0900 Subject: [PATCH 145/258] =?UTF-8?q?chore:=20=EC=9E=84=EA=B3=84=EC=B9=98=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=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 --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index fdcf1abd..5d637c79 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit fdcf1abd0df6d64328a4b99dd6d92a1ab24c7673 +Subproject commit 5d637c791045a025ca7543f14a83e4e5aaf9fcdf From d2fd4f23f1257ca39e2256928a12a3b44eefaafb Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 13:27:36 +0900 Subject: [PATCH 146/258] =?UTF-8?q?chore:=20dev=20CD=20=EC=88=98=EB=8F=99?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 29812158..a43de187 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -1,6 +1,7 @@ name: cd dev on: + workflow_dispatch: pull_request: branches: [ "develop" ] types: [closed] From 21a022c99074cbe594289c1e25654235d4b931a4 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 13:30:34 +0900 Subject: [PATCH 147/258] =?UTF-8?q?chore:=20workflow=5Fdispatch=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index a43de187..27324d2a 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -1,10 +1,10 @@ name: cd dev on: - workflow_dispatch: pull_request: branches: [ "develop" ] types: [closed] + workflow_dispatch: jobs: deploy: From 8829d7f2286e3534b95d48bcefafdaf5a67a7b29 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 13:31:29 +0900 Subject: [PATCH 148/258] =?UTF-8?q?chore:=20workflow=5Fdispatch=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 27324d2a..29812158 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -4,7 +4,6 @@ on: pull_request: branches: [ "develop" ] types: [closed] - workflow_dispatch: jobs: deploy: From e9f55b3a8cc7b9b75bee93750c03901d7673bcda Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 13:33:20 +0900 Subject: [PATCH 149/258] =?UTF-8?q?test:=20dev-cd=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=EC=8B=9C=ED=82=A4=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 916e6a83..00c1e2db 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,3 +16,4 @@ spring: import: classpath:application-prod.yml activate: on-profile: prod + From a020c613076626085f9410994e4d6bc65ebfcda5 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 13:43:27 +0900 Subject: [PATCH 150/258] =?UTF-8?q?Revert=20"test:=20dev-cd=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=EC=8B=9C=ED=82=A4=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 00c1e2db..916e6a83 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,3 @@ spring: import: classpath:application-prod.yml activate: on-profile: prod - From 5f5419efc0e3645471ae9f89fe3a034fb573f941 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 14:49:57 +0900 Subject: [PATCH 151/258] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B9=85=20h2=20c?= =?UTF-8?q?onsole=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/common/presentation/HttpLoggingFilter.java | 2 +- .../dto/{VoteRequestDto.java => PostImageRequestDto.java} | 0 .../post/presentation/dto/PostImageVoteStatusResponse.java | 4 ++++ 3 files changed, 5 insertions(+), 1 deletion(-) rename src/main/java/com/swyp8team2/post/presentation/dto/{VoteRequestDto.java => PostImageRequestDto.java} (100%) create mode 100644 src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java diff --git a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java index 45d48741..8d235e3a 100644 --- a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java +++ b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java @@ -34,7 +34,7 @@ public class HttpLoggingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - if (request.getRequestURI().startsWith("/docs")) { + if (request.getRequestURI().startsWith("/docs") || request.getRequestURI().startsWith("/h2-console")) { chain.doFilter(request, response); return; } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageRequestDto.java similarity index 100% rename from src/main/java/com/swyp8team2/post/presentation/dto/VoteRequestDto.java rename to src/main/java/com/swyp8team2/post/presentation/dto/PostImageRequestDto.java diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java new file mode 100644 index 00000000..d8d2acee --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java @@ -0,0 +1,4 @@ +package com.swyp8team2.post.presentation.dto; + +public record PostStatusResponse() { +} From 8d9d05bf5f08786c2ae923520c0c95f2835f3026 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 14:50:37 +0900 Subject: [PATCH 152/258] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=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/application/PostService.java | 31 +++++++++++++------ .../post/application/RatioCalculator.java | 2 +- .../java/com/swyp8team2/post/domain/Post.java | 2 +- .../post/presentation/PostController.java | 16 +++++++--- .../presentation/dto/CreatePostRequest.java | 3 +- .../presentation/dto/PostImageRequestDto.java | 2 +- .../presentation/dto/PostImageResponse.java | 1 + .../dto/PostImageVoteStatusResponse.java | 6 +++- .../post/presentation/dto/PostResponse.java | 6 ++-- 9 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index e4d8c826..1eb11b6f 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -11,6 +11,7 @@ import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; +import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.user.domain.User; @@ -47,7 +48,7 @@ public Long create(Long userId, CreatePostRequest request) { private List createPostImages(CreatePostRequest request) { PostImageNameGenerator nameGenerator = new PostImageNameGenerator(); - return request.votes().stream() + return request.images().stream() .map(voteRequestDto -> PostImage.create( nameGenerator.generate(), voteRequestDto.imageFileId() @@ -76,6 +77,7 @@ private PostImageResponse createVoteResponseDto(PostImage image, Long userId, Lo boolean voted = Objects.nonNull(userId) && getVoted(image, userId, postId); return new PostImageResponse( image.getId(), + image.getName(), imageFile.getImageUrl(), voted ); @@ -89,14 +91,6 @@ private Boolean getVoted(PostImage image, Long userId, Long postId) { .orElse(false); } - private int getTotalVoteCount(List images) { - int totalVoteCount = 0; - for (PostImage image : images) { - totalVoteCount += image.getVoteCount(); - } - return totalVoteCount; - } - public CursorBasePaginatedResponse findMyPosts(Long userId, Long cursor, int size) { Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse) @@ -118,4 +112,23 @@ public CursorBasePaginatedResponse findVotedPosts(Long userI Slice postSlice = postRepository.findByIdIn(postIds, cursor, PageRequest.ofSize(size)); return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse)); } + + public List findPostStatus(Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + int totalVoteCount = getTotalVoteCount(post.getImages()); + return post.getImages().stream() + .map(image -> { + String ratio = ratioCalculator.calculate(image.getVoteCount(), totalVoteCount); + return new PostImageVoteStatusResponse(image.getName(), image.getVoteCount(), ratio); + }).toList(); + } + + private int getTotalVoteCount(List images) { + int totalVoteCount = 0; + for (PostImage image : images) { + totalVoteCount += image.getVoteCount(); + } + return totalVoteCount; + } } diff --git a/src/main/java/com/swyp8team2/post/application/RatioCalculator.java b/src/main/java/com/swyp8team2/post/application/RatioCalculator.java index 39e94ef9..1f4c8ec1 100644 --- a/src/main/java/com/swyp8team2/post/application/RatioCalculator.java +++ b/src/main/java/com/swyp8team2/post/application/RatioCalculator.java @@ -8,7 +8,7 @@ @Component public class RatioCalculator { - public String calculateRatio(int totalVoteCount, int voteCount) { + public String calculate(int totalVoteCount, int voteCount) { if (totalVoteCount == 0) { return "0.0"; } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index f799e21a..b036b0e8 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -83,7 +83,7 @@ public void vote(Long imageId) { PostImage image = images.stream() .filter(postImage -> postImage.getId().equals(imageId)) .findFirst() - .orElseThrow(() -> new InternalServerException(ErrorCode.POST_IMAGE_NOT_FOUND)); + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_IMAGE_NOT_FOUND)); image.increaseVoteCount(); } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index c29b6287..1d67011e 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -5,6 +5,7 @@ import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; import com.swyp8team2.post.presentation.dto.PostImageResponse; @@ -44,7 +45,7 @@ public ResponseEntity createPost( @GetMapping("/{postId}") public ResponseEntity findPost( - @PathVariable("shareUrl") Long postId, + @PathVariable("postId") Long postId, @AuthenticationPrincipal UserInfo userInfo ) { Long userId = Optional.ofNullable(userInfo) @@ -53,7 +54,14 @@ public ResponseEntity findPost( return ResponseEntity.ok(postService.findById(userId, postId)); } - @GetMapping("/{shareUrl}") + @GetMapping("/{postId}/status") + public ResponseEntity> findVoteStatus( + @PathVariable("postId") Long postId + ) { + return ResponseEntity.ok(postService.findPostStatus(postId)); + } + +// @GetMapping("/{shareUrl}") public ResponseEntity findPost(@PathVariable("shareUrl") String shareUrl) { return ResponseEntity.ok(new PostResponse( 1L, @@ -64,8 +72,8 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh ), "description", List.of( - new PostImageResponse(1L, "https://image.photopic.site/1", true), - new PostImageResponse(2L, "https://image.photopic.site/2", false) + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/1", true), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/2", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java index 6d3f326c..1efb3b7a 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostRequest.java @@ -2,7 +2,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import java.util.List; @@ -11,6 +10,6 @@ public record CreatePostRequest( String description, @Valid @NotNull - List votes + List images ) { } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageRequestDto.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageRequestDto.java index d532cdda..15aeeeef 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageRequestDto.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageRequestDto.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotNull; -public record VoteRequestDto( +public record PostImageRequestDto( @NotNull Long imageFileId ) { diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java index daed7194..28b3280c 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java @@ -2,6 +2,7 @@ public record PostImageResponse( Long id, + String imageName, String imageUrl, boolean voted ) { diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java index d8d2acee..652f0899 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java @@ -1,4 +1,8 @@ package com.swyp8team2.post.presentation.dto; -public record PostStatusResponse() { +public record PostImageVoteStatusResponse( + String imageName, + int voteCount, + String voteRatio +) { } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java index 78b9a1b2..cb32afc3 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java @@ -10,16 +10,16 @@ public record PostResponse( Long id, AuthorDto author, String description, - List votes, + List images, String shareUrl, LocalDateTime createdAt ) { - public static PostResponse of(Post post, User user, List votes) { + public static PostResponse of(Post post, User user, List images) { return new PostResponse( post.getId(), AuthorDto.of(user), post.getDescription(), - votes, + images, post.getShareUrl(), post.getCreatedAt() ); From 62ee4613776aadc6e5383516b8823ee10e3dfdeb Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 14:50:47 +0900 Subject: [PATCH 153/258] =?UTF-8?q?test:=20=ED=88=AC=ED=91=9C=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostServiceTest.java | 43 ++++++++++--- .../post/application/RatioCalculatorTest.java | 4 +- .../post/presentation/PostControllerTest.java | 62 ++++++++++++++----- 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 646318b0..6921d19a 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -9,11 +9,12 @@ import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; -import com.swyp8team2.post.presentation.dto.VoteRequestDto; +import com.swyp8team2.post.presentation.dto.PostImageRequestDto; import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.application.VoteService; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; import org.junit.jupiter.api.DisplayName; @@ -47,14 +48,17 @@ class PostServiceTest extends IntegrationTest { @Autowired VoteRepository voteRepository; + @Autowired + VoteService voteService; + @Test @DisplayName("게시글 작성") void create() throws Exception { //given long userId = 1L; CreatePostRequest request = new CreatePostRequest("description", List.of( - new VoteRequestDto(1L), - new VoteRequestDto(2L) + new PostImageRequestDto(1L), + new PostImageRequestDto(2L) )); //when @@ -82,7 +86,7 @@ void create_invalidPostImageCount() throws Exception { //given long userId = 1L; CreatePostRequest request = new CreatePostRequest("description", List.of( - new VoteRequestDto(1L) + new PostImageRequestDto(1L) )); //when then @@ -97,8 +101,8 @@ void create_descriptionCountExceeded() throws Exception { //given long userId = 1L; CreatePostRequest request = new CreatePostRequest("a".repeat(101), List.of( - new VoteRequestDto(1L), - new VoteRequestDto(2L) + new PostImageRequestDto(1L), + new PostImageRequestDto(2L) )); //when then @@ -120,7 +124,7 @@ void findById() throws Exception { PostResponse response = postService.findById(user.getId(), post.getId()); //then - List votes = response.votes(); + List votes = response.images(); assertAll( () -> assertThat(response.description()).isEqualTo(post.getDescription()), () -> assertThat(response.id()).isEqualTo(post.getId()), @@ -206,4 +210,29 @@ void findVotedPosts() throws Exception { () -> assertThat(response.nextCursor()).isEqualTo(posts.get(전체_15개에서_맨_마지막_데이터_인덱스).getId()) ); } + + @Test + @DisplayName("투표 현황 조회") + void findPostStatus() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId()); + + //when + var response = postService.findPostStatus(post.getId()); + + //then + assertAll( + () -> assertThat(response).hasSize(2), + () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), + () -> assertThat(response.get(0).voteCount()).isEqualTo(1), + () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), + () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), + () -> assertThat(response.get(1).voteCount()).isEqualTo(0), + () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") + ); + } } diff --git a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java index e0cf8b8d..c9262704 100644 --- a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java +++ b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java @@ -2,12 +2,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; class RatioCalculatorTest { @@ -25,7 +23,7 @@ void calculate(int totalVoteCount, int voteCount, String result) throws Exceptio //given //when - String ratio = ratioCalculator.calculateRatio(totalVoteCount, voteCount); + String ratio = ratioCalculator.calculate(totalVoteCount, voteCount); //then assertThat(ratio).isEqualTo(result); diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 9f0bd4d0..809dc400 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -3,9 +3,10 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; -import com.swyp8team2.post.presentation.dto.VoteRequestDto; +import com.swyp8team2.post.presentation.dto.PostImageRequestDto; import com.swyp8team2.post.presentation.dto.PostImageResponse; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; @@ -20,6 +21,7 @@ import java.time.LocalDateTime; import java.util.List; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -42,7 +44,7 @@ void createPost() throws Exception { //given CreatePostRequest request = new CreatePostRequest( "제목", - List.of(new VoteRequestDto(1L), new VoteRequestDto(2L)) + List.of(new PostImageRequestDto(1L), new PostImageRequestDto(2L)) ); //when then @@ -58,11 +60,11 @@ void createPost() throws Exception { .type(JsonFieldType.STRING) .description("설명") .attributes(constraints("0~100자 사이")), - fieldWithPath("votes") + fieldWithPath("images") .type(JsonFieldType.ARRAY) .description("투표 후보") .attributes(constraints("최소 2개")), - fieldWithPath("votes[].imageFileId") + fieldWithPath("images[].imageFileId") .type(JsonFieldType.NUMBER) .description("투표 후보 이미지 ID") ))); @@ -82,20 +84,22 @@ void findPost() throws Exception { ), "description", List.of( - new PostImageResponse(1L, "https://image.photopic.site/1", true), - new PostImageResponse(2L, "https://image.photopic.site/2", false) + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/1", true), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/2", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) ); + given(postService.findById(any(), any())) + .willReturn(response); //when then - mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{shareUrl}", "shareUrl")) + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{postId}", 1)) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( pathParameters( - parameterWithName("shareUrl").description("게시글 공유 URL") + parameterWithName("postId").description("게시글 Id") ), responseFields( fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 Id"), @@ -104,18 +108,48 @@ void findPost() throws Exception { fieldWithPath("author.nickname").type(JsonFieldType.STRING).description("게시글 작성자 닉네임"), fieldWithPath("author.profileUrl").type(JsonFieldType.STRING).description("게시글 작성자 프로필 이미지"), fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), - fieldWithPath("votes[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), - fieldWithPath("votes[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), - fieldWithPath("votes[].imageUrl").type(JsonFieldType.STRING).description("투표 이미지"), - fieldWithPath("votes[].voteRatio").type(JsonFieldType.STRING).description("득표 비율"), - fieldWithPath("votes[].voteCount").type(JsonFieldType.NUMBER).description("득표 수"), - fieldWithPath("votes[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), + fieldWithPath("images[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), + fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), + fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), + fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간") ) )); } + @Test + @WithMockUserInfo + @DisplayName("게시글 투표 상태 조회") + void findVoteStatus() throws Exception { + //given + var response = List.of( + new PostImageVoteStatusResponse("뽀또A", 2, "66.7"), + new PostImageVoteStatusResponse("뽀또B", 1, "33.3") + ); + given(postService.findPostStatus(1L)) + .willReturn(response); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{postId}/status", 1) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ), + responseFields( + fieldWithPath("[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("[].imageName").type(JsonFieldType.STRING).description("사진 이름"), + fieldWithPath("[].voteCount").type(JsonFieldType.NUMBER).description("투표 수"), + fieldWithPath("[].voteRatio").type(JsonFieldType.STRING).description("투표 비율") + ) + )); + } + @Test @WithMockUserInfo @DisplayName("게시글 삭제") From e63c95eb4bc18c446dce910bcf78ec2af37d68cd Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 14:51:01 +0900 Subject: [PATCH 154/258] =?UTF-8?q?docs:=20=ED=88=AC=ED=91=9C=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 79fb7951..55a433b8 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -7,10 +7,17 @@ operation::post-controller-test/create-post[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게시글-상세-조회]] -=== 게시글 상세 조회 (미구현) +=== `GET` 게시글 상세 조회 operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] +[[사진-투표-현황-조회]] +=== `GET` 사진 투표 현황 조회 + +operation::post-controller-test/find-vote-status[snippets='http-request,curl-request,request-headers,path-parameters,http-response,response-fields'] + +[[게시글-목록-조회]] + [[내가-작성한-게시글-조회]] === `GET` 내가 작성한 게시글 조회 From aa24789357a80910abc5dcb228ccdba9dbef33d6 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 15:13:53 +0900 Subject: [PATCH 155/258] =?UTF-8?q?refactor:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=ED=98=84=ED=99=A9=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20id=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=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/com/swyp8team2/post/application/PostService.java | 2 +- .../post/presentation/dto/PostImageVoteStatusResponse.java | 1 + .../java/com/swyp8team2/post/application/PostServiceTest.java | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 1eb11b6f..ae02e2e9 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -120,7 +120,7 @@ public List findPostStatus(Long postId) { return post.getImages().stream() .map(image -> { String ratio = ratioCalculator.calculate(image.getVoteCount(), totalVoteCount); - return new PostImageVoteStatusResponse(image.getName(), image.getVoteCount(), ratio); + return new PostImageVoteStatusResponse(image.getId(), image.getName(), image.getVoteCount(), ratio); }).toList(); } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java index 652f0899..56c0f57f 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java @@ -1,6 +1,7 @@ package com.swyp8team2.post.presentation.dto; public record PostImageVoteStatusResponse( + Long id, String imageName, int voteCount, String voteRatio diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 6921d19a..b86dd717 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -227,9 +227,11 @@ void findPostStatus() throws Exception { //then assertAll( () -> assertThat(response).hasSize(2), + () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), () -> assertThat(response.get(0).voteCount()).isEqualTo(1), () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), + () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), () -> assertThat(response.get(1).voteCount()).isEqualTo(0), () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") From 08fbd84d746ca2c230ef7f8c17c03f8c028c69ff Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 15:14:17 +0900 Subject: [PATCH 156/258] =?UTF-8?q?docs:=20=ED=95=84=EC=9A=94=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 55a433b8..6206b90b 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -16,7 +16,6 @@ operation::post-controller-test/find-post[snippets='http-request,curl-request,pa operation::post-controller-test/find-vote-status[snippets='http-request,curl-request,request-headers,path-parameters,http-response,response-fields'] -[[게시글-목록-조회]] [[내가-작성한-게시글-조회]] === `GET` 내가 작성한 게시글 조회 From b4bb9fa9c376bac9a992ab255b0eaae0cf877049 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 15:14:17 +0900 Subject: [PATCH 157/258] =?UTF-8?q?docs:=20=ED=95=84=EC=9A=94=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 55a433b8..6206b90b 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -16,7 +16,6 @@ operation::post-controller-test/find-post[snippets='http-request,curl-request,pa operation::post-controller-test/find-vote-status[snippets='http-request,curl-request,request-headers,path-parameters,http-response,response-fields'] -[[게시글-목록-조회]] [[내가-작성한-게시글-조회]] === `GET` 내가 작성한 게시글 조회 From 3427dff36efcf21f24affed0307080b1edd06aad Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 15:13:53 +0900 Subject: [PATCH 158/258] =?UTF-8?q?refactor:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=ED=98=84=ED=99=A9=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20id=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/post/application/PostService.java | 2 +- .../post/presentation/dto/PostImageVoteStatusResponse.java | 1 + .../com/swyp8team2/post/application/PostServiceTest.java | 2 ++ .../com/swyp8team2/post/presentation/PostControllerTest.java | 5 +++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 1eb11b6f..ae02e2e9 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -120,7 +120,7 @@ public List findPostStatus(Long postId) { return post.getImages().stream() .map(image -> { String ratio = ratioCalculator.calculate(image.getVoteCount(), totalVoteCount); - return new PostImageVoteStatusResponse(image.getName(), image.getVoteCount(), ratio); + return new PostImageVoteStatusResponse(image.getId(), image.getName(), image.getVoteCount(), ratio); }).toList(); } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java index 652f0899..56c0f57f 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageVoteStatusResponse.java @@ -1,6 +1,7 @@ package com.swyp8team2.post.presentation.dto; public record PostImageVoteStatusResponse( + Long id, String imageName, int voteCount, String voteRatio diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 6921d19a..b86dd717 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -227,9 +227,11 @@ void findPostStatus() throws Exception { //then assertAll( () -> assertThat(response).hasSize(2), + () -> assertThat(response.get(0).id()).isEqualTo(post.getImages().get(0).getId()), () -> assertThat(response.get(0).imageName()).isEqualTo(post.getImages().get(0).getName()), () -> assertThat(response.get(0).voteCount()).isEqualTo(1), () -> assertThat(response.get(0).voteRatio()).isEqualTo("100.0"), + () -> assertThat(response.get(1).id()).isEqualTo(post.getImages().get(1).getId()), () -> assertThat(response.get(1).imageName()).isEqualTo(post.getImages().get(1).getName()), () -> assertThat(response.get(1).voteCount()).isEqualTo(0), () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 809dc400..11c0659c 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -125,8 +125,8 @@ void findPost() throws Exception { void findVoteStatus() throws Exception { //given var response = List.of( - new PostImageVoteStatusResponse("뽀또A", 2, "66.7"), - new PostImageVoteStatusResponse("뽀또B", 1, "33.3") + new PostImageVoteStatusResponse(1L, "뽀또A", 2, "66.7"), + new PostImageVoteStatusResponse(2L, "뽀또B", 1, "33.3") ); given(postService.findPostStatus(1L)) .willReturn(response); @@ -143,6 +143,7 @@ void findVoteStatus() throws Exception { ), responseFields( fieldWithPath("[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("이미지 Id"), fieldWithPath("[].imageName").type(JsonFieldType.STRING).description("사진 이름"), fieldWithPath("[].voteCount").type(JsonFieldType.NUMBER).description("투표 수"), fieldWithPath("[].voteRatio").type(JsonFieldType.STRING).description("투표 비율") From b588ad11516f5c525769a2d0a4583f1ba6458a35 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 15:39:15 +0900 Subject: [PATCH 159/258] =?UTF-8?q?fix:=20http=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=ED=91=9C=EC=8B=9C=20=EC=95=88=20=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/HttpLoggingFilter.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java index 8d235e3a..1794ab22 100644 --- a/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java +++ b/src/main/java/com/swyp8team2/common/presentation/HttpLoggingFilter.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.stream.Stream; @Slf4j @Component @@ -79,15 +80,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse private String getHeaders(HttpServletRequest request) { StringBuilder sb = new StringBuilder("{\n"); - List loggingHeaders = List.of( - HttpHeaders.USER_AGENT, - HttpHeaders.AUTHORIZATION, - HttpHeaders.COOKIE, - HttpHeaders.CONTENT_TYPE, - HttpHeaders.HOST, - HttpHeaders.REFERER, - HttpHeaders.ORIGIN - ); + List loggingHeaders = Stream.of( + HttpHeaders.USER_AGENT.toLowerCase(), + HttpHeaders.AUTHORIZATION.toLowerCase(), + HttpHeaders.COOKIE.toLowerCase(), + HttpHeaders.CONTENT_TYPE.toLowerCase(), + HttpHeaders.HOST.toLowerCase(), + HttpHeaders.REFERER.toLowerCase(), + HttpHeaders.ORIGIN.toLowerCase() + ).map(String::toLowerCase) + .toList(); Enumeration headerArray = request.getHeaderNames(); while (headerArray.hasMoreElements()) { String headerName = headerArray.nextElement(); From dd5d6b88cbde013997cfd33396d1d71eae10f17f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 15:59:30 +0900 Subject: [PATCH 160/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=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 --- .../exception/ApplicationControllerAdvice.java | 16 ++++++++++++++++ .../swyp8team2/vote/application/VoteService.java | 14 +++++++++++--- .../vote/presentation/VoteController.java | 6 ++---- .../vote/presentation/dto/VoteRequest.java | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 9abbe93f..2796149d 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -5,9 +5,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.HandlerMethodValidationException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.resource.NoResourceFoundException; import javax.naming.AuthenticationException; @@ -68,6 +70,20 @@ public ResponseEntity handle(AccessDeniedException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); } + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity handle(MissingRequestHeaderException e) { + log.debug("MissingRequestHeaderException {}", e.getMessage()); + return ResponseEntity.badRequest() + .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handle(MethodArgumentTypeMismatchException e) { + log.debug("MethodArgumentTypeMismatchException {}", e.getMessage()); + return ResponseEntity.badRequest() + .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); + } + @ExceptionHandler(Exception.class) public ResponseEntity handle(Exception e) { log.error("Exception", e); diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 1c7b2bf0..328d14c9 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -7,6 +7,7 @@ import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,12 +27,12 @@ public Long vote(Long userId, Long postId, Long imageId) { .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); voteRepository.findByUserSeqAndPostId(user.getSeq(), postId) .ifPresent(vote -> deleteExistingVote(postId, vote)); - Vote vote = createVote(postId, imageId, user); + Vote vote = createVote(postId, imageId, user.getSeq()); return vote.getId(); } - private Vote createVote(Long postId, Long imageId, User user) { - Vote vote = voteRepository.save(Vote.of(postId, imageId, user.getSeq())); + private Vote createVote(Long postId, Long imageId, String userSeq) { + Vote vote = voteRepository.save(Vote.of(postId, imageId, userSeq)); postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) .vote(imageId); @@ -44,4 +45,11 @@ private void deleteExistingVote(Long postId, Vote vote) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) .cancelVote(vote.getPostImageId()); } + + public Long guestVote(String guestId, Long postId, Long imageId) { + voteRepository.findByUserSeqAndPostId(guestId, postId) + .ifPresent(vote -> deleteExistingVote(postId, vote)); + Vote vote = createVote(postId, imageId, guestId); + return vote.getId(); + } } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index df3e6711..25025afd 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -4,14 +4,11 @@ import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.vote.application.VoteService; import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; -import com.swyp8team2.vote.presentation.dto.GuestVoteRequest; import com.swyp8team2.vote.presentation.dto.VoteRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -33,7 +30,7 @@ public ResponseEntity vote( @Valid @RequestBody VoteRequest request, @AuthenticationPrincipal UserInfo userInfo ) { - voteService.vote(userInfo.userId(), postId, request.voteId()); + voteService.vote(userInfo.userId(), postId, request.imageId()); return ResponseEntity.ok().build(); } @@ -43,6 +40,7 @@ public ResponseEntity guestVote( @RequestHeader(CustomHeader.GUEST_ID) String guestId, @Valid @RequestBody VoteRequest request ) { + voteService.guestVote(guestId, postId, request.imageId()); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java index 202c8157..9c7b2adb 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/VoteRequest.java @@ -4,6 +4,6 @@ public record VoteRequest( @NotNull - Long voteId + Long imageId ) { } From 913ab132fcf61975c69e76715759a3958d6e6c95 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 15:59:48 +0900 Subject: [PATCH 161/258] =?UTF-8?q?fix:=20=ED=88=AC=ED=91=9C=20=EB=B9=84?= =?UTF-8?q?=EC=9C=A8=20=EA=B3=84=EC=82=B0=20=EC=9D=B8=EC=9E=90=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/post/application/PostService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 1eb11b6f..32d0b391 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -119,7 +119,7 @@ public List findPostStatus(Long postId) { int totalVoteCount = getTotalVoteCount(post.getImages()); return post.getImages().stream() .map(image -> { - String ratio = ratioCalculator.calculate(image.getVoteCount(), totalVoteCount); + String ratio = ratioCalculator.calculate(totalVoteCount, image.getVoteCount()); return new PostImageVoteStatusResponse(image.getName(), image.getVoteCount(), ratio); }).toList(); } From 3ab22b246ebb18fd07f84493d23d8b3fd4592e4d Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 16:03:42 +0900 Subject: [PATCH 162/258] =?UTF-8?q?refactor:=20voteId=20->=20imageId=20?= =?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 --- .../comment/presentation/dto/CommentResponse.java | 2 +- .../comment/presentation/CommentControllerTest.java | 4 ++-- .../post/application/RatioCalculatorTest.java | 2 +- .../vote/presentation/VoteControllerTest.java | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index 702158f2..30af16ab 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -11,7 +11,7 @@ public record CommentResponse( Long commentId, String content, AuthorDto author, - Long voteId, + Long imageId, LocalDateTime createdAt ) implements CursorDto { diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index 3c55561e..46dfbf8b 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -119,10 +119,10 @@ void findComments() throws Exception { fieldWithPath("data[].author.profileUrl") .type(JsonFieldType.STRING) .description("작성자 프로필 이미지 url"), - fieldWithPath("data[].voteId") + fieldWithPath("data[].imageId") .type(JsonFieldType.NUMBER) .optional() - .description("작성자 투표 Id (투표 없을 시 null)"), + .description("작성자가 투표한 이미지 Id (투표 없을 시 null)"), fieldWithPath("data[].createdAt") .type(JsonFieldType.STRING) .description("댓글 작성일") diff --git a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java index c9262704..27e95e3d 100644 --- a/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java +++ b/src/test/java/com/swyp8team2/post/application/RatioCalculatorTest.java @@ -17,7 +17,7 @@ void setUp() { } @ParameterizedTest(name = "{index}: totalVoteCount={0}, voteCount={1} => result={2}") - @CsvSource({"3, 2, 66.7", "3, 1, 33.3", "4, 2, 50.0", "4, 3, 75.0", "0, 0, 0.0", "1, 0, 0.0", "1, 1, 100.0"}) + @CsvSource({"3, 2, 66.7", "3, 1, 33.3", "4, 2, 50.0", "4, 3, 75.0", "0, 0, 0.0", "1, 0, 0.0", "1, 1, 100.0", "10, 7, 70.0", "10, 3, 30.0"}) @DisplayName("비율 계산") void calculate(int totalVoteCount, int voteCount, String result) throws Exception { //given diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index e0e8263d..77907f64 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -44,7 +44,7 @@ void vote() throws Exception { parameterWithName("postId").description("게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) .description("투표 후보 Id") ) @@ -70,7 +70,7 @@ void guestVote() throws Exception { parameterWithName("postId").description("게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) .description("투표 후보 Id") ) @@ -96,9 +96,9 @@ void changeVote() throws Exception { parameterWithName("postId").description("변경할 게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) - .description("변경할 투표 후보 Id") + .description("변경할 투표 이미지 Id") ) )); } @@ -122,9 +122,9 @@ void guestChangeVote() throws Exception { parameterWithName("postId").description("변경활 게시글 Id") ), requestFields( - fieldWithPath("voteId") + fieldWithPath("imageId") .type(JsonFieldType.NUMBER) - .description("변경할 투표 후보 Id") + .description("변경할 투표 이미지 Id") ) )); } From 4cac8aae9a32ef02131178591d34db8e123a9a11 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 16:03:53 +0900 Subject: [PATCH 163/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dto/ChangeVoteRequest.java | 2 +- .../vote/application/VoteServiceTest.java | 50 +++++++++++++++++++ .../vote/presentation/VoteControllerTest.java | 7 +++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java index 816e9a7d..f9c102f8 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java +++ b/src/main/java/com/swyp8team2/vote/presentation/dto/ChangeVoteRequest.java @@ -4,6 +4,6 @@ public record ChangeVoteRequest( @NotNull - Long voteId + Long imageId ) { } diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index e17a96db..c12afc49 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -83,4 +83,54 @@ void vote_change() { () -> assertThat(findPost.getImages().get(1).getVoteCount()).isEqualTo(1) ); } + + @Test + @DisplayName("게스트 투표하기") + void guestVote() { + // given + String guestId = "guestId"; + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + // when + Long voteId = voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId()); + + // then + Vote vote = voteRepository.findById(voteId).get(); + Post findPost = postRepository.findById(post.getId()).get(); + assertAll( + () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) + ); + } + + @Test + @DisplayName("게스트 투표하기 - 다른 이미지로 투표 변경한 경우") + void guestVote_change() { + // given + String guestId = "guestId"; + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId()); + + // when + Long voteId = voteService.guestVote(guestId, post.getId(), post.getImages().get(1).getId()); + + // then + Vote vote = voteRepository.findById(voteId).get(); + Post findPost = postRepository.findById(post.getId()).get(); + assertAll( + () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), + () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), + () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), + () -> assertThat(findPost.getImages().get(1).getVoteCount()).isEqualTo(1) + ); + } } diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index 77907f64..c28de442 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -14,6 +14,11 @@ import java.util.UUID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -49,6 +54,7 @@ void vote() throws Exception { .description("투표 후보 Id") ) )); + verify(voteService, times(1)).vote(any(), any(), any()); } @Test @@ -75,6 +81,7 @@ void guestVote() throws Exception { .description("투표 후보 Id") ) )); + verify(voteService, times(1)).guestVote(any(), any(), any()); } @Test From 7976205fd9219c82b817eef418dc7d06a6bd1002 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 16:11:32 +0900 Subject: [PATCH 164/258] =?UTF-8?q?docs:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EA=B5=AC=ED=98=84=20=ED=98=84=ED=99=A9=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/docs/asciidoc/votes.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/asciidoc/votes.adoc b/src/docs/asciidoc/votes.adoc index e89d4323..98b1cdc6 100644 --- a/src/docs/asciidoc/votes.adoc +++ b/src/docs/asciidoc/votes.adoc @@ -7,7 +7,7 @@ operation::vote-controller-test/vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] [[게스트-투표]] -=== `POST` 게스트 투표 (미구현) +=== `POST` 게스트 투표 operation::vote-controller-test/guest-vote[snippets='http-request,curl-request,request-headers,request-fields,http-response'] From 6111e65071e053c98eab37c79cf67668cb96a784 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 16:33:45 +0900 Subject: [PATCH 165/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=A7=88=EA=B0=90=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 --- .../swyp8team2/common/exception/ErrorCode.java | 2 ++ .../post/application/PostService.java | 17 +++++++++++++++++ .../java/com/swyp8team2/post/domain/Post.java | 14 ++++++++++++++ .../post/presentation/PostController.java | 15 +++++++++++++-- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 0cbbdf1a..3a1f52d5 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -18,6 +18,8 @@ public enum ErrorCode { POST_NOT_FOUND("존재하지 않는 게시글"), DESCRIPTION_LENGTH_EXCEEDED("게시글 설명 길이 초과"), INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"), + NOT_POST_AUTHOR("게시글 작성자가 아님"), + POST_ALREADY_CLOSED("이미 마감된 게시글"), //401 EXPIRED_TOKEN("토큰 만료"), diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 13c3264f..d8fdaf7b 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -131,4 +131,21 @@ private int getTotalVoteCount(List images) { } return totalVoteCount; } + + public void delete(Long userId, String postId) { + Post post = postRepository.findById(Long.valueOf(postId)) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + if (!post.getUserId().equals(userId)) { + throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); + } + post.validateOwner(userId); + postRepository.delete(post); + } + + @Transactional + public void close(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.close(userId); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index b036b0e8..5ef39a8f 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -94,4 +94,18 @@ public void cancelVote(Long imageId) { .orElseThrow(() -> new InternalServerException(ErrorCode.POST_IMAGE_NOT_FOUND)); image.decreaseVoteCount(); } + + public void close(Long userId) { + validateOwner(userId); + if (state == State.CLOSED) { + throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); + } + this.state = State.CLOSED; + } + + public void validateOwner(Long userId) { + if (!this.userId.equals(userId)) { + throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); + } + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 1d67011e..f76bc24b 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -16,6 +16,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -80,11 +81,21 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh )); } - @DeleteMapping("/{shareUrl}") + @PostMapping("/{postId}/close") + public ResponseEntity closePost( + @PathVariable("postId") Long postId, + @AuthenticationPrincipal UserInfo userInfo + ) { + postService.close(userInfo.userId(), postId); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{postId}") public ResponseEntity deletePost( - @PathVariable("shareUrl") String shareUrl, + @PathVariable("postId") String postId, @AuthenticationPrincipal UserInfo userInfo ) { + postService.delete(userInfo.userId(), postId); return ResponseEntity.ok().build(); } From 1e74a9be98b58411aae2467f12348ce5c21b6e8b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 16:33:58 +0900 Subject: [PATCH 166/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=A7=88=EA=B0=90=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostServiceTest.java | 60 +++++++++++++++++++ .../com/swyp8team2/post/domain/PostTest.java | 53 ++++++++++++++++ .../post/presentation/PostControllerTest.java | 21 +++++++ 3 files changed, 134 insertions(+) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index b86dd717..0399ab04 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -7,6 +7,7 @@ import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.State; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.PostImageRequestDto; @@ -237,4 +238,63 @@ void findPostStatus() throws Exception { () -> assertThat(response.get(1).voteRatio()).isEqualTo("0.0") ); } + + @Test + @DisplayName("투표 마감") + void close() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + //when + post.close(user.getId()); + + //then + postRepository.findById(post.getId()).get(); + assertThat(post.getState()).isEqualTo(State.CLOSED); + } + + @Test + @DisplayName("투표 마감 - 게시글 작성자가 아닐 경우") + void close_notPostAuthor() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + //when then + assertThatThrownBy(() -> post.close(2L)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.NOT_POST_AUTHOR.getMessage()); + } + + @Test + @DisplayName("투표 마감 - 이미 마감된 게시글인 경우") + void close_alreadyClosed() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + post.close(user.getId()); + + //when then + assertThatThrownBy(() -> post.close(user.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } + + @Test + @DisplayName("투표 마감 - 존재하지 않는 게시글일 경우") + void close_notFoundPost() throws Exception { + //given + + //when then + assertThatThrownBy(() -> postService.close(1L, 1L)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_NOT_FOUND.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index def54c57..ab59071e 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -75,4 +76,56 @@ void create_descriptionCountExceeded() throws Exception { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } + + @Test + @DisplayName("투표 마감") + void close() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", State.PROGRESS, postImages, "shareUrl"); + + //when + post.close(userId); + + //then + assertThat(post.getState()).isEqualTo(State.CLOSED); + } + + @Test + @DisplayName("투표 마감 - 이미 마감된 게시글인 경우") + void close_alreadyClosed() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", State.CLOSED, postImages, "shareUrl"); + + //when then + assertThatThrownBy(() -> post.close(userId)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } + + @Test + @DisplayName("투표 마감 - 게시글 작성자가 아닌 경우") + void close_notPostAuthor() throws Exception { + //given + long userId = 1L; + List postImages = List.of( + PostImage.create("뽀또A", 1L), + PostImage.create("뽀또B", 2L) + ); + Post post = new Post(null, userId, "description", State.PROGRESS, postImages, "shareUrl"); + + //when then + assertThatThrownBy(() -> post.close(2L)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.NOT_POST_AUTHOR.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 11c0659c..781364b8 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -23,6 +23,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -278,4 +280,23 @@ void findVotedPost() throws Exception { ) )); } + + @Test + @WithMockUserInfo + @DisplayName("게시글 마감") + void closePost() throws Exception { + //given + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.post("/posts/{postId}/close", 1) + .header(HttpHeaders.AUTHORIZATION, "Bearer token")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + pathParameters( + parameterWithName("postId").description("게시글 Id") + ) + )); + verify(postService, times(1)).close(any(), any()); + } } From 18a0dc63991b6acfcf39f55c5024df00b13197b4 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 16:35:14 +0900 Subject: [PATCH 167/258] =?UTF-8?q?docs:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EB=A7=88=EA=B0=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 6206b90b..82d9ea18 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -27,6 +27,13 @@ operation::post-controller-test/find-my-post[snippets='http-request,curl-request operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] +[[게시글-투표-마감]] +=== `POST` 게시글 투표 마감 + +operation::post-controller-test/close-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] + +[[게시글-수정]] + [[게시글-삭제]] === 게시글 삭제 (미구현) From 123b43666edc6a1f92edd1437fd1332d99728b11 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 17:21:49 +0900 Subject: [PATCH 168/258] =?UTF-8?q?fix:=20CRC32=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=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/com/swyp8team2/image/config/S3Config.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/swyp8team2/image/config/S3Config.java b/src/main/java/com/swyp8team2/image/config/S3Config.java index 4238db0c..3e65b030 100644 --- a/src/main/java/com/swyp8team2/image/config/S3Config.java +++ b/src/main/java/com/swyp8team2/image/config/S3Config.java @@ -5,6 +5,8 @@ import org.springframework.context.annotation.Configuration; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; +import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Configuration; @@ -33,6 +35,8 @@ public S3Client s3Client() { .serviceConfiguration(S3Configuration.builder() .pathStyleAccessEnabled(true) .build()) + .requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED) + .responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED) .build(); } } From 28988015538461bb86992e69ed7123f2145a491f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 17:44:46 +0900 Subject: [PATCH 169/258] =?UTF-8?q?fix:=20=ED=88=AC=ED=91=9C=20=EB=A7=88?= =?UTF-8?q?=EA=B0=90=EB=90=90=EC=9D=84=20=EB=95=8C=20=ED=88=AC=ED=91=9C?= =?UTF-8?q?=ED=95=A0=20=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=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 --- .../java/com/swyp8team2/post/domain/Post.java | 6 ++ .../vote/application/VoteService.java | 39 +++++++------ .../vote/application/VoteServiceTest.java | 57 +++++++++++++++++++ 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 5ef39a8f..96ad72dd 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -108,4 +108,10 @@ public void validateOwner(Long userId) { throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); } } + + public void validateProgress() { + if (!this.state.equals(State.PROGRESS)) { + throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); + } + } } diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 328d14c9..9860e8a7 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -2,6 +2,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -22,33 +23,35 @@ public class VoteService { private final PostRepository postRepository; @Transactional - public Long vote(Long userId, Long postId, Long imageId) { - User user = userRepository.findById(userId) + public Long vote(Long voterId, Long postId, Long imageId) { + User voter = userRepository.findById(voterId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - voteRepository.findByUserSeqAndPostId(user.getSeq(), postId) - .ifPresent(vote -> deleteExistingVote(postId, vote)); - Vote vote = createVote(postId, imageId, user.getSeq()); + deleteVoteIfExisting(postId, voter.getSeq()); + Vote vote = createVote(postId, imageId, voter.getSeq()); return vote.getId(); } - private Vote createVote(Long postId, Long imageId, String userSeq) { - Vote vote = voteRepository.save(Vote.of(postId, imageId, userSeq)); - postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) - .vote(imageId); - return vote; + private void deleteVoteIfExisting(Long postId, String userSeq) { + voteRepository.findByUserSeqAndPostId(userSeq, postId) + .ifPresent(vote -> { + voteRepository.delete(vote); + postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) + .cancelVote(vote.getPostImageId()); + }); } - private void deleteExistingVote(Long postId, Vote vote) { - voteRepository.delete(vote); - postRepository.findById(postId) - .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)) - .cancelVote(vote.getPostImageId()); + private Vote createVote(Long postId, Long imageId, String userSeq) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); + post.validateProgress(); + Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userSeq)); + post.vote(imageId); + return vote; } public Long guestVote(String guestId, Long postId, Long imageId) { - voteRepository.findByUserSeqAndPostId(guestId, postId) - .ifPresent(vote -> deleteExistingVote(postId, vote)); + deleteVoteIfExisting(postId, guestId); Vote vote = createVote(postId, imageId, guestId); return vote.getId(); } diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index c12afc49..04c545cd 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -1,9 +1,13 @@ package com.swyp8team2.vote.application; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.post.domain.State; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -13,10 +17,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + import static com.swyp8team2.support.fixture.FixtureGenerator.createImageFile; import static com.swyp8team2.support.fixture.FixtureGenerator.createPost; import static com.swyp8team2.support.fixture.FixtureGenerator.createUser; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; class VoteServiceTest extends IntegrationTest { @@ -84,6 +91,31 @@ void vote_change() { ); } + @Test + @DisplayName("투표하기 - 투표 마감된 경우") + void vote_alreadyClosed() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(new Post( + null, + user.getId(), + "description", + State.CLOSED, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl" + )); + + // when + assertThatThrownBy(() -> voteService.vote(user.getId(), post.getId(), post.getImages().get(0).getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } + @Test @DisplayName("게스트 투표하기") void guestVote() { @@ -133,4 +165,29 @@ void guestVote_change() { () -> assertThat(findPost.getImages().get(1).getVoteCount()).isEqualTo(1) ); } + + @Test + @DisplayName("게스트 투표하기 - 투표 마감된 경우") + void guestVote_alreadyClosed() { + // given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(new Post( + null, + user.getId(), + "description", + State.CLOSED, + List.of( + PostImage.create("뽀또A", imageFile1.getId()), + PostImage.create("뽀또B", imageFile2.getId()) + ), + "shareUrl" + )); + + // when + assertThatThrownBy(() -> voteService.guestVote("guestId", post.getId(), post.getImages().get(0).getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); + } } From 20a4fabeb502acb4352187a4fa53e9593ecc76d3 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 17:48:05 +0900 Subject: [PATCH 170/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=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/com/swyp8team2/post/application/PostService.java | 8 +++----- .../com/swyp8team2/post/presentation/PostController.java | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index d8fdaf7b..7eea9767 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -132,12 +132,10 @@ private int getTotalVoteCount(List images) { return totalVoteCount; } - public void delete(Long userId, String postId) { - Post post = postRepository.findById(Long.valueOf(postId)) + @Transactional + public void delete(Long userId, Long postId) { + Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); - if (!post.getUserId().equals(userId)) { - throw new BadRequestException(ErrorCode.NOT_POST_AUTHOR); - } post.validateOwner(userId); postRepository.delete(post); } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index f76bc24b..52ff749d 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -92,7 +92,7 @@ public ResponseEntity closePost( @DeleteMapping("/{postId}") public ResponseEntity deletePost( - @PathVariable("postId") String postId, + @PathVariable("postId") Long postId, @AuthenticationPrincipal UserInfo userInfo ) { postService.delete(userInfo.userId(), postId); From dd43ca84a3363a7367073cb362f2aab95aa677f5 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 17:48:38 +0900 Subject: [PATCH 171/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostServiceTest.java | 16 ++++++++++++++++ .../post/presentation/PostControllerTest.java | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 0399ab04..53b927be 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -297,4 +297,20 @@ void close_notFoundPost() throws Exception { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.POST_NOT_FOUND.getMessage()); } + + @Test + @DisplayName("게시글 삭제") + void delete() throws Exception { + //given + User user = userRepository.save(createUser(1)); + ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); + ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); + Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); + + //when + postService.delete(user.getId(), post.getId()); + + //then + assertThat(postRepository.findById(post.getId())).isEmpty(); + } } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 781364b8..2615f5d9 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -160,15 +160,16 @@ void deletePost() throws Exception { //given //when then - mockMvc.perform(RestDocumentationRequestBuilders.delete("/posts/{shareUrl}", "shareUrl") + mockMvc.perform(RestDocumentationRequestBuilders.delete("/posts/{postId}", 1) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(authorizationHeader()), pathParameters( - parameterWithName("shareUrl").description("게시글 공유 URL") + parameterWithName("postId").description("게시글 Id") ) )); + verify(postService, times(1)).delete(any(), any()); } @Test From 4361e970c29f34b4a2d707e47588346e7042add4 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 17:51:09 +0900 Subject: [PATCH 172/258] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=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 --- .../com/swyp8team2/user/application/UserService.java | 10 ++++++++++ .../swyp8team2/user/presentation/UserController.java | 5 ++++- .../user/presentation/dto/UserInfoResponse.java | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index 1d09f4bc..71e1c954 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -1,7 +1,10 @@ package com.swyp8team2.user.application; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.user.presentation.dto.UserInfoResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,6 +12,7 @@ import java.util.Optional; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class UserService { @@ -29,4 +33,10 @@ private String getNickname(String email) { return Optional.ofNullable(email) .orElseGet(() -> "user_" + System.currentTimeMillis()); } + + public UserInfoResponse findById(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); + return UserInfoResponse.of(user); + } } diff --git a/src/main/java/com/swyp8team2/user/presentation/UserController.java b/src/main/java/com/swyp8team2/user/presentation/UserController.java index d232886b..dc5ac165 100644 --- a/src/main/java/com/swyp8team2/user/presentation/UserController.java +++ b/src/main/java/com/swyp8team2/user/presentation/UserController.java @@ -1,5 +1,6 @@ package com.swyp8team2.user.presentation; +import com.swyp8team2.user.application.UserService; import com.swyp8team2.user.presentation.dto.UserInfoResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -13,8 +14,10 @@ @RequestMapping("/users") public class UserController { + private final UserService userService; + @GetMapping("/{userId}") public ResponseEntity findUserInfo(@PathVariable("userId") Long userId) { - return ResponseEntity.ok(new UserInfoResponse(1L, "nickname", "https://image.com/profile-image")); + return ResponseEntity.ok(userService.findById(userId)); } } diff --git a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java index 19c204fb..13bf6e26 100644 --- a/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java +++ b/src/main/java/com/swyp8team2/user/presentation/dto/UserInfoResponse.java @@ -1,8 +1,13 @@ package com.swyp8team2.user.presentation.dto; +import com.swyp8team2.user.domain.User; + public record UserInfoResponse( Long id, String nickname, String profileUrl ) { + public static UserInfoResponse of(User user) { + return new UserInfoResponse(user.getId(), user.getNickname(), user.getProfileUrl()); + } } From 350ecc383218c54a8ac9c966ee79decdcdc4de89 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 17:51:17 +0900 Subject: [PATCH 173/258] =?UTF-8?q?test:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/swyp8team2/support/WebUnitTest.java | 4 ++++ .../com/swyp8team2/user/presentation/UserControllerTest.java | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index fbb726db..ebd1b17c 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -6,6 +6,7 @@ import com.swyp8team2.comment.application.CommentService; import com.swyp8team2.image.application.ImageService; import com.swyp8team2.post.application.PostService; +import com.swyp8team2.user.application.UserService; import com.swyp8team2.vote.application.VoteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -40,4 +41,7 @@ public abstract class WebUnitTest { @MockitoBean protected CommentService commentService; + + @MockitoBean + protected UserService userService; } diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index 63a47bd9..f11a7990 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -7,6 +7,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.security.test.context.support.WithMockUser; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -24,6 +25,8 @@ class UserControllerTest extends RestDocsTest { void findUserInfo() throws Exception { //given UserInfoResponse response = new UserInfoResponse(1L, "nickname", "https://image.com/profile-image"); + given(userService.findById(1L)) + .willReturn(response); //when then mockMvc.perform(RestDocumentationRequestBuilders.get("/users/{userId}", "1")) From 96d2256ce7b86a79a10caa27d11bcf362f10e810 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Tue, 25 Feb 2025 17:53:52 +0900 Subject: [PATCH 174/258] =?UTF-8?q?docs:=20=EA=B5=AC=ED=98=84=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/comments.adoc | 4 ++-- src/docs/asciidoc/posts.adoc | 2 +- src/docs/asciidoc/users.adoc | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/docs/asciidoc/comments.adoc b/src/docs/asciidoc/comments.adoc index 2321a088..1491db53 100644 --- a/src/docs/asciidoc/comments.adoc +++ b/src/docs/asciidoc/comments.adoc @@ -2,12 +2,12 @@ == 댓글 API [[댓글-생성]] -=== 댓글 생성 +=== `POST` 댓글 생성 operation::comment-controller-test/create-comment[snippets='http-request,curl-request,path-parameters,request-headers,request-fields,http-response'] [[댓글-조회]] -=== 댓글 조회 +=== `GET` 댓글 조회 operation::comment-controller-test/find-comments[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 82d9ea18..fc4dea9f 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -35,6 +35,6 @@ operation::post-controller-test/close-post[snippets='http-request,curl-request,p [[게시글-수정]] [[게시글-삭제]] -=== 게시글 삭제 (미구현) +=== `DELETE` 게시글 삭제 operation::post-controller-test/delete-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] diff --git a/src/docs/asciidoc/users.adoc b/src/docs/asciidoc/users.adoc index a342582e..9faf6982 100644 --- a/src/docs/asciidoc/users.adoc +++ b/src/docs/asciidoc/users.adoc @@ -2,6 +2,6 @@ == 유저 API [[유저-정보-조회]] -=== 유저 정보 조회 (미구현) +=== `GET` 유저 정보 조회 operation::user-controller-test/find-user-info[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] \ No newline at end of file From ac2a98d588142f9ef2dab2377a86a373cffa98ad Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 19:48:55 +0900 Subject: [PATCH 175/258] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=20=EB=8B=89=EB=84=A4=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/domain/NicknameAdjective.java | 26 +++++++++++++++++++ .../domain/NicknameAdjectiveRepository.java | 12 +++++++++ .../user/application/UserServiceTest.java | 4 +++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/com/swyp8team2/user/domain/NicknameAdjective.java create mode 100644 src/main/java/com/swyp8team2/user/domain/NicknameAdjectiveRepository.java create mode 100644 src/test/java/com/swyp8team2/user/application/UserServiceTest.java diff --git a/src/main/java/com/swyp8team2/user/domain/NicknameAdjective.java b/src/main/java/com/swyp8team2/user/domain/NicknameAdjective.java new file mode 100644 index 00000000..3a535294 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/domain/NicknameAdjective.java @@ -0,0 +1,26 @@ +package com.swyp8team2.user.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.GenerationType; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "nickname_adjectives") +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class NicknameAdjective { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String adjective; + + public NicknameAdjective(String adjective) { + this.adjective = adjective; + } +} diff --git a/src/main/java/com/swyp8team2/user/domain/NicknameAdjectiveRepository.java b/src/main/java/com/swyp8team2/user/domain/NicknameAdjectiveRepository.java new file mode 100644 index 00000000..13ce4e4c --- /dev/null +++ b/src/main/java/com/swyp8team2/user/domain/NicknameAdjectiveRepository.java @@ -0,0 +1,12 @@ +package com.swyp8team2.user.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface NicknameAdjectiveRepository extends JpaRepository { + + Optional findNicknameAdjectiveById(Long id); +} diff --git a/src/test/java/com/swyp8team2/user/application/UserServiceTest.java b/src/test/java/com/swyp8team2/user/application/UserServiceTest.java new file mode 100644 index 00000000..38379708 --- /dev/null +++ b/src/test/java/com/swyp8team2/user/application/UserServiceTest.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class UserServiceTest { + +} \ No newline at end of file From c2e61c73d3063717a7f0a004c5c0ce680333e1a1 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 19:51:45 +0900 Subject: [PATCH 176/258] =?UTF-8?q?feat:=20User=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20validate=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/com/swyp8team2/user/domain/User.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 30d35a0b..d63e1bd3 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -30,8 +30,6 @@ public class User { private String seq; public User(Long id, String nickname, String profileUrl, String seq) { - validateNull(nickname, profileUrl); - validateEmptyString(nickname, profileUrl); this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; From a749fe014bbbfc605ac468e781e0de15971dcb64 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 19:52:29 +0900 Subject: [PATCH 177/258] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EB=9E=9C=EB=8D=A4=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/user/application/UserService.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index 71e1c954..64e5e1c1 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -2,6 +2,8 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.user.domain.NicknameAdjective; +import com.swyp8team2.user.domain.NicknameAdjectiveRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.user.presentation.dto.UserInfoResponse; @@ -17,6 +19,7 @@ public class UserService { private final UserRepository userRepository; + private final NicknameAdjectiveRepository nicknameAdjectiveRepository; @Transactional public Long createUser(String nickname, String profileImageUrl) { @@ -29,9 +32,14 @@ private String getProfileImage(String profileImageUrl) { .orElse("defailt_profile_image"); } - private String getNickname(String email) { - return Optional.ofNullable(email) - .orElseGet(() -> "user_" + System.currentTimeMillis()); + private String getNickname(String nickname) { + return Optional.ofNullable(nickname) + .orElseGet(() -> { + long randomIndex = (long)(Math.random() * 500); + Optional adjective = nicknameAdjectiveRepository.findNicknameAdjectiveById(randomIndex); + return adjective.map(NicknameAdjective::getAdjective) + .orElse("user_" + System.currentTimeMillis()); + }); } public UserInfoResponse findById(Long userId) { From 46bd966623023bbb4d4aac905455795a144287c7 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 19:53:03 +0900 Subject: [PATCH 178/258] =?UTF-8?q?feat:=20=EA=B0=9C=EB=B0=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/common/dev/DataInitializer.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 0e7dc485..7e64548f 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -8,10 +8,11 @@ import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; +import com.swyp8team2.user.domain.NicknameAdjective; +import com.swyp8team2.user.domain.NicknameAdjectiveRepository; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.application.VoteService; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -26,6 +27,7 @@ @RequiredArgsConstructor public class DataInitializer { + private final NicknameAdjectiveRepository nicknameAdjectiveRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final PostRepository postRepository; @@ -34,13 +36,14 @@ public class DataInitializer { @Transactional public void init() { + List adjectives = nicknameAdjectiveRepository.findAll(); User testUser = userRepository.save(User.create("nickname", "defailt_profile_image")); TokenPair tokenPair = jwtService.createToken(testUser.getId()); System.out.println("accessToken = " + tokenPair.accessToken()); System.out.println("refreshToken = " + tokenPair.refreshToken()); List users = new ArrayList<>(); for (int i = 0; i < 10; i++) { - User user = userRepository.save(User.create("nickname" + i, "defailt_profile_image")); + User user = userRepository.save(User.create(adjectives.get(i).getAdjective(), "defailt_profile_image")); users.add(user); for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); From d1bdce1c655eb1418611214362f46110e1338c43 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 19:53:27 +0900 Subject: [PATCH 179/258] =?UTF-8?q?test:=20=EB=9E=9C=EB=8D=A4=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/UserServiceTest.java | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/swyp8team2/user/application/UserServiceTest.java b/src/test/java/com/swyp8team2/user/application/UserServiceTest.java index 38379708..a7099cd7 100644 --- a/src/test/java/com/swyp8team2/user/application/UserServiceTest.java +++ b/src/test/java/com/swyp8team2/user/application/UserServiceTest.java @@ -1,4 +1,46 @@ -import static org.junit.jupiter.api.Assertions.*; -class UserServiceTest { - +package com.swyp8team2.user.application; + +import com.swyp8team2.support.IntegrationTest; +import com.swyp8team2.user.domain.NicknameAdjective; +import com.swyp8team2.user.domain.NicknameAdjectiveRepository; +import com.swyp8team2.user.domain.User; +import com.swyp8team2.user.domain.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class UserServiceTest extends IntegrationTest { + + @Autowired + UserRepository userRepository; + + @Autowired + NicknameAdjectiveRepository nicknameAdjectiveRepository; + + @Autowired + UserService userService; + + @Test + void createUser() { + // given + User user = User.create(null, "https://image.com/1"); + // 메인 코드에서는 2개만 수정 후 테스트 진행 + nicknameAdjectiveRepository.save(new NicknameAdjective("호기심 많은 뽀또")); + nicknameAdjectiveRepository.save(new NicknameAdjective("배려 깊은 뽀또")); + + // when + Long userId = userService.createUser(user.getNickname(), user.getProfileUrl()); + Optional returnUser = userRepository.findById(userId); + + // when then + assertAll( + () -> assertThat(returnUser.get().getNickname()).isNotNull(), + () -> assertThat(returnUser.get().getNickname()).contains("뽀또") + ); + + } } \ No newline at end of file From a944abe7a35ee58cf1c7a596dbc50b5ebc85a78d Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 20:01:01 +0900 Subject: [PATCH 180/258] =?UTF-8?q?test:=20=EB=A9=94=EC=9D=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EA=B0=9C=EC=88=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/user/application/UserServiceTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/swyp8team2/user/application/UserServiceTest.java b/src/test/java/com/swyp8team2/user/application/UserServiceTest.java index a7099cd7..ce483924 100644 --- a/src/test/java/com/swyp8team2/user/application/UserServiceTest.java +++ b/src/test/java/com/swyp8team2/user/application/UserServiceTest.java @@ -28,9 +28,11 @@ class UserServiceTest extends IntegrationTest { void createUser() { // given User user = User.create(null, "https://image.com/1"); - // 메인 코드에서는 2개만 수정 후 테스트 진행 - nicknameAdjectiveRepository.save(new NicknameAdjective("호기심 많은 뽀또")); - nicknameAdjectiveRepository.save(new NicknameAdjective("배려 깊은 뽀또")); + + for (int i = 0; i < 250; i++) { + nicknameAdjectiveRepository.save(new NicknameAdjective("호기심 많은 뽀또")); + nicknameAdjectiveRepository.save(new NicknameAdjective("배려 깊은 뽀또")); + } // when Long userId = userService.createUser(user.getNickname(), user.getProfileUrl()); From 8adfac2a0ceb36a119bb5d24482b47727217f21f Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 20:02:11 +0900 Subject: [PATCH 181/258] =?UTF-8?q?chore:=20=EA=B9=A8=EC=A7=84=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 620e2b2b..350cb299 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,4 +2,4 @@ ## 📢 그 외 -## 📌 관련 이슈 +## 📌 관련 이슈 From ffe920bb1d3d30520d46192789f5a9581d868902 Mon Sep 17 00:00:00 2001 From: Nakji Date: Tue, 25 Feb 2025 20:04:19 +0900 Subject: [PATCH 182/258] =?UTF-8?q?test:=20User=20=EA=B0=9D=EC=B2=B4=20val?= =?UTF-8?q?idate=20=EC=A0=9C=EA=B1=B0=EB=A1=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=A4=ED=82=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/user/domain/UserTest.java | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/test/java/com/swyp8team2/user/domain/UserTest.java b/src/test/java/com/swyp8team2/user/domain/UserTest.java index 1c0edca0..104c44f7 100644 --- a/src/test/java/com/swyp8team2/user/domain/UserTest.java +++ b/src/test/java/com/swyp8team2/user/domain/UserTest.java @@ -1,12 +1,9 @@ package com.swyp8team2.user.domain; -import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.InternalServerException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; class UserTest { @@ -22,26 +19,4 @@ void create() throws Exception { //then assertThat(user.getNickname()).isEqualTo(nickname); } - - @Test - @DisplayName("user Entity 생성 - 파라미터가 null인 경우") - void create_null() throws Exception { - //given - - //when then - assertThatThrownBy(() -> User.create(null, "email")) - .isInstanceOf(InternalServerException.class) - .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); - } - - @Test - @DisplayName("user Entity 생성 - nickname이 빈 문자인 경우") - void create_emptyString() throws Exception { - //given - - //when then - assertThatThrownBy(() -> User.create("", "email")) - .isInstanceOf(InternalServerException.class) - .hasMessage(ErrorCode.INVALID_INPUT_VALUE.getMessage()); - } } From 43e98b35b40d7dafac075e46c31b2d05d4b0cea3 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 26 Feb 2025 22:57:15 +0900 Subject: [PATCH 183/258] =?UTF-8?q?chore:=20hikari=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 5d637c79..82e9e214 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 5d637c791045a025ca7543f14a83e4e5aaf9fcdf +Subproject commit 82e9e21444681760f0f697dc71f3ed6d01186231 From 8d539dee3ba9b8efbcdc8430b44c25947140d069 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Wed, 26 Feb 2025 22:58:18 +0900 Subject: [PATCH 184/258] =?UTF-8?q?fix:=20ddl=20=EC=84=A4=EC=A0=95=20updat?= =?UTF-8?q?e=EB=A1=9C=20=EB=B0=94=EB=80=8C=EB=A9=B4=EC=84=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/common/dev/DataInitializer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 7e64548f..7b97475a 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -42,16 +42,16 @@ public void init() { System.out.println("accessToken = " + tokenPair.accessToken()); System.out.println("refreshToken = " + tokenPair.refreshToken()); List users = new ArrayList<>(); + List posts = new ArrayList<>(); for (int i = 0; i < 10; i++) { User user = userRepository.save(User.create(adjectives.get(i).getAdjective(), "defailt_profile_image")); users.add(user); for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl")); + posts.add(postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"))); } } - List posts = postRepository.findAll(); for (User user : users) { for (Post post : posts) { Random random = new Random(); From b383a99ce10af549e5bdc8148876dbe2bef2b186 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 27 Feb 2025 02:39:31 +0900 Subject: [PATCH 185/258] =?UTF-8?q?chore:=20dev=20hikari=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 82e9e214..0afb5ee1 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 82e9e21444681760f0f697dc71f3ed6d01186231 +Subproject commit 0afb5ee104ee232e74ad823f355b7a6cad80e26a From 80cbe52d7cd8df5fd7cf722524979e07329af61a Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 27 Feb 2025 03:02:13 +0900 Subject: [PATCH 186/258] =?UTF-8?q?fix:=20PostImageResponse=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20=EC=9D=B4=EB=AF=B8=EC=A7=80=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 --- .../java/com/swyp8team2/post/application/PostService.java | 1 + .../java/com/swyp8team2/post/presentation/PostController.java | 4 ++-- .../swyp8team2/post/presentation/dto/PostImageResponse.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 7eea9767..f39e5c1e 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -79,6 +79,7 @@ private PostImageResponse createVoteResponseDto(PostImage image, Long userId, Lo image.getId(), image.getName(), imageFile.getImageUrl(), + imageFile.getThumbnailUrl(), voted ); } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 52ff749d..ca181605 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -73,8 +73,8 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh ), "description", List.of( - new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/1", true), - new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/2", false) + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/1", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java index 28b3280c..0fd9881e 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostImageResponse.java @@ -4,6 +4,7 @@ public record PostImageResponse( Long id, String imageName, String imageUrl, + String thumbnailUrl, boolean voted ) { } From ca789923f746de2c04a94770c7a0a56601a30150 Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 27 Feb 2025 03:02:47 +0900 Subject: [PATCH 187/258] =?UTF-8?q?test:=20images[].thumbnailUrl=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 --- .../com/swyp8team2/post/presentation/PostControllerTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 2615f5d9..6af99fb9 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -86,8 +86,8 @@ void findPost() throws Exception { ), "description", List.of( - new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/1", true), - new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/2", false) + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) ), "https://photopic.site/shareurl", LocalDateTime.of(2025, 2, 13, 12, 0) @@ -114,6 +114,7 @@ void findPost() throws Exception { fieldWithPath("images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), + fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간") From 39c371120d7b86589cb4770d6aed4afd68d81eed Mon Sep 17 00:00:00 2001 From: Nakji Date: Thu, 27 Feb 2025 03:03:12 +0900 Subject: [PATCH 188/258] =?UTF-8?q?chore:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EB=94=94=ED=8F=B4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/common/dev/DataInitializer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 7b97475a..5b5cff33 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -37,14 +37,14 @@ public class DataInitializer { @Transactional public void init() { List adjectives = nicknameAdjectiveRepository.findAll(); - User testUser = userRepository.save(User.create("nickname", "defailt_profile_image")); + User testUser = userRepository.save(User.create("nickname", "https://t1.kakaocdn.net/account_images/default_profile.jpeg")); TokenPair tokenPair = jwtService.createToken(testUser.getId()); System.out.println("accessToken = " + tokenPair.accessToken()); System.out.println("refreshToken = " + tokenPair.refreshToken()); List users = new ArrayList<>(); List posts = new ArrayList<>(); for (int i = 0; i < 10; i++) { - User user = userRepository.save(User.create(adjectives.get(i).getAdjective(), "defailt_profile_image")); + User user = userRepository.save(User.create(adjectives.get(i).getAdjective(), "https://t1.kakaocdn.net/account_images/default_profile.jpeg")); users.add(user); for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); From 9c459883a172f89028d7509cf0f362590898451a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 15:51:15 +0900 Subject: [PATCH 189/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=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 --- .../auth/application/AuthService.java | 23 +++++++- .../auth/presentation/AuthController.java | 7 +++ .../presentation/GuestTokenInterceptor.java | 27 +++++++++ .../presentation/dto/GuestTokenResponse.java | 4 ++ .../crypto/application/CryptoService.java | 55 +++++++++++++++++++ .../user/application/UserService.java | 6 ++ .../java/com/swyp8team2/user/domain/Role.java | 5 ++ .../java/com/swyp8team2/user/domain/User.java | 17 +++++- 8 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/dto/GuestTokenResponse.java create mode 100644 src/main/java/com/swyp8team2/crypto/application/CryptoService.java create mode 100644 src/main/java/com/swyp8team2/user/domain/Role.java diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 9b3898f9..f04588d3 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,19 +7,35 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.security.NoSuchAlgorithmException; + @Service -@RequiredArgsConstructor public class AuthService { private final JwtService jwtService; private final OAuthService oAuthService; private final SocialAccountRepository socialAccountRepository; private final UserService userService; + private final CryptoService cryptoService; + + public AuthService( + JwtService jwtService, + OAuthService oAuthService, + SocialAccountRepository socialAccountRepository, + UserService userService + ) throws Exception { + this.jwtService = jwtService; + this.oAuthService = oAuthService; + this.socialAccountRepository = socialAccountRepository; + this.userService = userService; + this.cryptoService = new CryptoService(); + } @Transactional public TokenPair oauthSignIn(String code, String redirectUri) { @@ -39,4 +55,9 @@ private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { public TokenPair reissue(String refreshToken) { return jwtService.reissue(refreshToken); } + + public String guestLogin() { + Long guestId = userService.createGuest(); + return cryptoService.encrypt(String.valueOf(guestId)); + } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index e7e2e908..32d2c508 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -3,6 +3,7 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; @@ -53,4 +54,10 @@ public ResponseEntity reissue( response.addCookie(cookie); return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); } + + @PostMapping("/guest/login") + public ResponseEntity guestLogin() { + String guestToken = authService.guestLogin(); + return ResponseEntity.ok(new GuestTokenResponse(guestToken)); + } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java b/src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java new file mode 100644 index 00000000..6334f37e --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java @@ -0,0 +1,27 @@ +package com.swyp8team2.auth.presentation; + +import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.crypto.application.CryptoService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.Objects; + +@Component +@RequiredArgsConstructor +public class GuestTokenInterceptor implements HandlerInterceptor { + + private final CryptoService cryptoService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { +// String token = request.getHeader(CustomHeader.GUEST_ID); +// if (Objects.isNull(token)) { +// return true; +// } + return true; + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/GuestTokenResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/GuestTokenResponse.java new file mode 100644 index 00000000..ee76d2f6 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/GuestTokenResponse.java @@ -0,0 +1,4 @@ +package com.swyp8team2.auth.presentation.dto; + +public record GuestTokenResponse(String guestToken) { +} diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java new file mode 100644 index 00000000..25fddd7b --- /dev/null +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -0,0 +1,55 @@ +package com.swyp8team2.crypto.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.util.Base64; + +@Slf4j +@Service +public class CryptoService { + + private static final String ALGORITHM = "AES"; + private final SecretKey secretKey; + + public CryptoService() throws Exception { + KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); + keyGenerator.init(256); + this.secretKey = keyGenerator.generateKey(); + } + + public String encrypt(String data) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + byte[] encryptedBytes = cipher.doFinal(data.getBytes()); + return Base64.getEncoder().encodeToString(encryptedBytes); + } catch (Exception e) { + log.error("encrypt error {}", e.getMessage()); + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + + public String decrypt(String encryptedData) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); + return new String(decryptedBytes); + } catch (IllegalBlockSizeException | BadPaddingException e) { + log.debug("decrypt error {}", e.getMessage()); + throw new BadRequestException(ErrorCode.INVALID_TOKEN); + } catch (Exception e) { + log.error("decrypt error {}", e.getMessage()); + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index 64e5e1c1..b089de58 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -42,6 +42,12 @@ private String getNickname(String nickname) { }); } + @Transactional + public Long createGuest() { + User user = userRepository.save(User.createGuest()); + return user.getId(); + } + public UserInfoResponse findById(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); diff --git a/src/main/java/com/swyp8team2/user/domain/Role.java b/src/main/java/com/swyp8team2/user/domain/Role.java new file mode 100644 index 00000000..3792a4c5 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/domain/Role.java @@ -0,0 +1,5 @@ +package com.swyp8team2.user.domain; + +public enum Role { + GUEST, USER +} diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index d63e1bd3..a9767fbf 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -29,14 +29,27 @@ public class User { private String seq; - public User(Long id, String nickname, String profileUrl, String seq) { + public Role role; + + public User(Long id, String nickname, String profileUrl, String seq, Role role) { this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; this.seq = seq; + this.role = role; } public static User create(String nickname, String profileUrl) { - return new User(null, nickname, profileUrl, UUID.randomUUID().toString()); + return new User(null, nickname, profileUrl, UUID.randomUUID().toString(), Role.USER); + } + + public static User createGuest() { + return new User( + null, + "guest_" + System.currentTimeMillis(), + "https://image.photopic.site/images-dev/resized_202502240006030.png", + UUID.randomUUID().toString(), + Role.GUEST + ); } } From c554bad1beeed65b5a638fec0671061eaf0daec1 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 16:09:32 +0900 Subject: [PATCH 190/258] =?UTF-8?q?refactor:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9E=90=20=ED=88=AC=ED=91=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20id=20=ED=95=84=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/comment/presentation/dto/CommentResponse.java | 2 +- .../swyp8team2/comment/presentation/CommentControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index 30af16ab..ea3c6f67 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -11,7 +11,7 @@ public record CommentResponse( Long commentId, String content, AuthorDto author, - Long imageId, + Long voteImageId, LocalDateTime createdAt ) implements CursorDto { diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index 46dfbf8b..ab316900 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -119,7 +119,7 @@ void findComments() throws Exception { fieldWithPath("data[].author.profileUrl") .type(JsonFieldType.STRING) .description("작성자 프로필 이미지 url"), - fieldWithPath("data[].imageId") + fieldWithPath("data[].voteImageId") .type(JsonFieldType.NUMBER) .optional() .description("작성자가 투표한 이미지 Id (투표 없을 시 null)"), From abf1c3e6f010f31f835a6a822eaece2c46d90ca7 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 16:09:51 +0900 Subject: [PATCH 191/258] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=9E=90=20=EB=B3=B8=EC=9D=B8=20=EC=97=AC=EB=B6=80=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 --- .../java/com/swyp8team2/post/application/PostService.java | 3 ++- .../java/com/swyp8team2/post/presentation/PostController.java | 1 + .../com/swyp8team2/post/presentation/dto/PostResponse.java | 4 +++- .../com/swyp8team2/post/presentation/PostControllerTest.java | 4 +++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index f39e5c1e..b9279815 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -61,7 +61,8 @@ public PostResponse findById(Long userId, Long postId) { User author = userRepository.findById(post.getUserId()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); List votes = createPostImageResponse(userId, postId, post); - return PostResponse.of(post, author, votes); + boolean isAuthor = post.getUserId().equals(userId); + return PostResponse.of(post, author, votes, isAuthor); } private List createPostImageResponse(Long userId, Long postId, Post post) { diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index ca181605..ac5e33d9 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -77,6 +77,7 @@ public ResponseEntity findPost(@PathVariable("shareUrl") String sh new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/1", false) ), "https://photopic.site/shareurl", + true, LocalDateTime.of(2025, 2, 13, 12, 0) )); } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java index cb32afc3..6f082117 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java @@ -12,15 +12,17 @@ public record PostResponse( String description, List images, String shareUrl, + boolean isAuthor, LocalDateTime createdAt ) { - public static PostResponse of(Post post, User user, List images) { + public static PostResponse of(Post post, User user, List images, boolean isAuthor) { return new PostResponse( post.getId(), AuthorDto.of(user), post.getDescription(), images, post.getShareUrl(), + isAuthor, post.getCreatedAt() ); } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 6af99fb9..1ca9d1cc 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -90,6 +90,7 @@ void findPost() throws Exception { new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) ), "https://photopic.site/shareurl", + true, LocalDateTime.of(2025, 2, 13, 12, 0) ); given(postService.findById(any(), any())) @@ -117,7 +118,8 @@ void findPost() throws Exception { fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), - fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간") + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), + fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") ) )); } From 773b8bcae9d2ebe3aaeff84583c93ac008482362 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 17:14:34 +0900 Subject: [PATCH 192/258] =?UTF-8?q?fix:=20=EB=82=B4=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1/=ED=88=AC=ED=91=9C=ED=95=9C=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20uri=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/presentation/PostController.java | 28 ++----------------- .../post/presentation/PostControllerTest.java | 4 +-- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index ac5e33d9..8ed8b674 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -3,12 +3,10 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.application.PostService; -import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; -import com.swyp8team2.post.presentation.dto.PostImageResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; @@ -16,7 +14,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -24,7 +21,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -62,26 +58,6 @@ public ResponseEntity> findVoteStatus( return ResponseEntity.ok(postService.findPostStatus(postId)); } -// @GetMapping("/{shareUrl}") - public ResponseEntity findPost(@PathVariable("shareUrl") String shareUrl) { - return ResponseEntity.ok(new PostResponse( - 1L, - new AuthorDto( - 1L, - "author", - "https://image.photopic.site/profile-image" - ), - "description", - List.of( - new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), - new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/1", false) - ), - "https://photopic.site/shareurl", - true, - LocalDateTime.of(2025, 2, 13, 12, 0) - )); - } - @PostMapping("/{postId}/close") public ResponseEntity closePost( @PathVariable("postId") Long postId, @@ -100,7 +76,7 @@ public ResponseEntity deletePost( return ResponseEntity.ok().build(); } - @GetMapping("/me") + @GetMapping("/user") public ResponseEntity> findMyPosts( @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, @@ -109,7 +85,7 @@ public ResponseEntity> findMyPos return ResponseEntity.ok(postService.findMyPosts(userInfo.userId(), cursor, size)); } - @GetMapping("/voted") + @GetMapping("/user/voted") public ResponseEntity> findVotedPosts( @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 1ca9d1cc..6823b72a 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -196,7 +196,7 @@ void findMyPost() throws Exception { .willReturn(response); //when then - mockMvc.perform(get("/posts/me") + mockMvc.perform(get("/posts/user") .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) @@ -251,7 +251,7 @@ void findVotedPost() throws Exception { .willReturn(response); //when then - mockMvc.perform(get("/posts/voted") + mockMvc.perform(get("/posts/user/voted") .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) From d5f8c3dfeb76e160c999b64d9e82c609caa008af Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 17:15:00 +0900 Subject: [PATCH 193/258] =?UTF-8?q?fix:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=BB=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=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 --- src/main/java/com/swyp8team2/common/dev/DataInitializer.java | 3 ++- src/main/resources/application.yml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 5b5cff33..c490ac30 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -44,7 +44,8 @@ public void init() { List users = new ArrayList<>(); List posts = new ArrayList<>(); for (int i = 0; i < 10; i++) { - User user = userRepository.save(User.create(adjectives.get(i).getAdjective(), "https://t1.kakaocdn.net/account_images/default_profile.jpeg")); + String userName = adjectives.size() < 10 ? "user" + i : adjectives.get(i).getAdjective(); + User user = userRepository.save(User.create(userName, "https://t1.kakaocdn.net/account_images/default_profile.jpeg")); users.add(user); for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 916e6a83..d75d118c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,8 @@ server: port: 8080 +spring: + profiles: + active: local --- From ef1c256f73be3888c4481127d88ad27e954f715e Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 17:24:26 +0900 Subject: [PATCH 194/258] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/presentation/CreatePostResponse.java | 4 ++++ .../swyp8team2/post/presentation/PostController.java | 6 +++--- .../post/presentation/PostControllerTest.java | 12 +++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java diff --git a/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java new file mode 100644 index 00000000..44467835 --- /dev/null +++ b/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java @@ -0,0 +1,4 @@ +package com.swyp8team2.post.presentation; + +public record CreatePostResponse(Long postId) { +} diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 8ed8b674..80596126 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -32,12 +32,12 @@ public class PostController { private final PostService postService; @PostMapping("") - public ResponseEntity createPost( + public ResponseEntity createPost( @Valid @RequestBody CreatePostRequest request, @AuthenticationPrincipal UserInfo userInfo ) { - postService.create(userInfo.userId(), request); - return ResponseEntity.ok().build(); + + return ResponseEntity.ok(new CreatePostResponse(postService.create(userInfo.userId(), request))); } @GetMapping("/{postId}") diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 6823b72a..6041d4d4 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -48,6 +48,9 @@ void createPost() throws Exception { "제목", List.of(new PostImageRequestDto(1L), new PostImageRequestDto(2L)) ); + given(postService.create(any(), any())) + .willReturn(1L); + CreatePostResponse response = new CreatePostResponse(1L); //when then mockMvc.perform(post("/posts") @@ -55,6 +58,7 @@ void createPost() throws Exception { .content(objectMapper.writeValueAsString(request)) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( requestHeaders(authorizationHeader()), requestFields( @@ -69,7 +73,13 @@ void createPost() throws Exception { fieldWithPath("images[].imageFileId") .type(JsonFieldType.NUMBER) .description("투표 후보 이미지 ID") - ))); + ), + responseFields( + fieldWithPath("postId") + .type(JsonFieldType.NUMBER) + .description("게시글 Id") + ) + )); } @Test From c2d42e5521d17238cc6ea53d505fbc1a85701d50 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 21:52:33 +0900 Subject: [PATCH 195/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=A1=B0=ED=9A=8C=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 --- .../auth/application/AuthService.java | 14 +---- .../com/swyp8team2/auth/domain/UserInfo.java | 7 ++- .../auth/presentation/AuthController.java | 2 +- .../presentation/GuestTokenInterceptor.java | 27 -------- .../presentation/filter/GuestAuthFilter.java | 61 +++++++++++++++++++ .../presentation/filter/JwtAuthFilter.java | 3 +- .../common/config/SecurityConfig.java | 25 +++++++- .../common/exception/ErrorCode.java | 1 + 8 files changed, 93 insertions(+), 47 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java create mode 100644 src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index f04588d3..470816d7 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -16,6 +16,7 @@ import java.security.NoSuchAlgorithmException; @Service +@RequiredArgsConstructor public class AuthService { private final JwtService jwtService; @@ -24,19 +25,6 @@ public class AuthService { private final UserService userService; private final CryptoService cryptoService; - public AuthService( - JwtService jwtService, - OAuthService oAuthService, - SocialAccountRepository socialAccountRepository, - UserService userService - ) throws Exception { - this.jwtService = jwtService; - this.oAuthService = oAuthService; - this.socialAccountRepository = socialAccountRepository; - this.userService = userService; - this.cryptoService = new CryptoService(); - } - @Transactional public TokenPair oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); diff --git a/src/main/java/com/swyp8team2/auth/domain/UserInfo.java b/src/main/java/com/swyp8team2/auth/domain/UserInfo.java index bd836070..84ac2c45 100644 --- a/src/main/java/com/swyp8team2/auth/domain/UserInfo.java +++ b/src/main/java/com/swyp8team2/auth/domain/UserInfo.java @@ -1,14 +1,17 @@ package com.swyp8team2.auth.domain; +import com.swyp8team2.user.domain.Role; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Collections; +import java.util.List; import static com.swyp8team2.common.util.Validator.validateNull; -public record UserInfo(long userId) implements UserDetails { +public record UserInfo(long userId, Role role) implements UserDetails { public UserInfo { validateNull(userId); @@ -16,7 +19,7 @@ public record UserInfo(long userId) implements UserDetails { @Override public Collection getAuthorities() { - return Collections.emptyList(); + return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } @Override diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 32d2c508..b14bd9c2 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -55,7 +55,7 @@ public ResponseEntity reissue( return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); } - @PostMapping("/guest/login") + @PostMapping("/guest/token") public ResponseEntity guestLogin() { String guestToken = authService.guestLogin(); return ResponseEntity.ok(new GuestTokenResponse(guestToken)); diff --git a/src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java b/src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java deleted file mode 100644 index 6334f37e..00000000 --- a/src/main/java/com/swyp8team2/auth/presentation/GuestTokenInterceptor.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.swyp8team2.auth.presentation; - -import com.swyp8team2.common.presentation.CustomHeader; -import com.swyp8team2.crypto.application.CryptoService; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.util.Objects; - -@Component -@RequiredArgsConstructor -public class GuestTokenInterceptor implements HandlerInterceptor { - - private final CryptoService cryptoService; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { -// String token = request.getHeader(CustomHeader.GUEST_ID); -// if (Objects.isNull(token)) { -// return true; -// } - return true; - } -} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java new file mode 100644 index 00000000..4b678645 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -0,0 +1,61 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.common.exception.ApplicationException; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.user.domain.Role; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Objects; + +import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; + +@Slf4j +@RequiredArgsConstructor +public class GuestAuthFilter extends OncePerRequestFilter { + + private final CryptoService cryptoService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + AntPathMatcher matcher = new AntPathMatcher(); + if (!matcher.match("/posts/{postId}/votes/guest", request.getRequestURI())) { + return; + } + String token = request.getHeader(CustomHeader.GUEST_ID); + if (Objects.isNull(token)) { + throw new BadRequestException(ErrorCode.INVALID_GUEST_HEADER); + } + String guestId = cryptoService.decrypt(token); + Authentication authentication = getAuthentication(Long.parseLong(guestId)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (ApplicationException e) { + request.setAttribute(EXCEPTION_KEY, e); + } finally { + doFilter(request, response, filterChain); + } + } + + private Authentication getAuthentication(long userId) { + UserInfo userInfo = new UserInfo(userId, Role.GUEST); + return new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index d77b476f..b7f16f08 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -4,6 +4,7 @@ import com.swyp8team2.auth.application.jwt.JwtProvider; import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.exception.ApplicationException; +import com.swyp8team2.user.domain.Role; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -44,7 +45,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private Authentication getAuthentication(long userId) { - UserInfo userInfo = new UserInfo(userId); + UserInfo userInfo = new UserInfo(userId, Role.USER); return new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); } } diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 4e94ba55..e5de05a9 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -1,9 +1,12 @@ package com.swyp8team2.common.config; import com.swyp8team2.auth.application.jwt.JwtProvider; +import com.swyp8team2.auth.presentation.filter.GuestAuthFilter; import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; @@ -32,11 +35,14 @@ public class SecurityConfig { private final HandlerExceptionResolver handlerExceptionResolver; + private final CryptoService cryptoService; public SecurityConfig( - @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver, + CryptoService cryptoService ) { this.handlerExceptionResolver = handlerExceptionResolver; + this.cryptoService = cryptoService; } @Bean @@ -84,11 +90,17 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize .requestMatchers(getWhiteList(introspect)).permitAll() + .requestMatchers(getGuestTokenRequestList(introspect)) + .hasAnyRole(Role.USER.name(), Role.GUEST.name()) .anyRequest().authenticated()) .addFilterBefore( new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor()), UsernamePasswordAuthenticationFilter.class) + .addFilterAfter( + new GuestAuthFilter(cryptoService), + JwtAuthFilter.class + ) .exceptionHandling(exception -> exception.authenticationEntryPoint( new JwtAuthenticationEntryPoint(handlerExceptionResolver))); @@ -99,11 +111,18 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), - mvc.pattern("/guest"), + mvc.pattern("/auth/guest/token"), mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), - mvc.pattern("/posts/{postId}/votes/guest/**"), +// mvc.pattern("/posts/{postId}/votes/guest/**"), mvc.pattern("/auth/oauth2/**") }; } + + public static MvcRequestMatcher[] getGuestTokenRequestList(HandlerMappingIntrospector introspect) { + MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); + return new MvcRequestMatcher[]{ + mvc.pattern("/posts/{postId}/votes/guest"), + }; + } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 3a1f52d5..bbc5ea04 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -20,6 +20,7 @@ public enum ErrorCode { INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"), NOT_POST_AUTHOR("게시글 작성자가 아님"), POST_ALREADY_CLOSED("이미 마감된 게시글"), + INVALID_GUEST_HEADER("잘못된 게스트 토큰 헤더"), //401 EXPIRED_TOKEN("토큰 만료"), From e81d3a167b24f6cb6c617b6a43539a1fa16c6d1c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 21:52:50 +0900 Subject: [PATCH 196/258] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicationControllerAdvice.java | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 2796149d..3d2fc530 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingRequestHeaderException; @@ -33,16 +34,21 @@ public ResponseEntity handle(UnauthorizedException e) { .body(response); } - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handle(MethodArgumentNotValidException e) { - log.debug("MethodArgumentNotValidException {}", e.getMessage()); + @ExceptionHandler({ + MethodArgumentNotValidException.class, + HttpMessageNotReadableException.class, + MissingRequestHeaderException.class, + HandlerMethodValidationException.class + }) + public ResponseEntity invalidArgument(Exception e) { + log.debug("invalidArgument: {}", e.getMessage()); return ResponseEntity.badRequest() .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); } - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity handle(HttpRequestMethodNotSupportedException e) { - log.debug("HttpRequestMethodNotSupportedException {}", e.getMessage()); + @ExceptionHandler({HttpRequestMethodNotSupportedException.class, MethodArgumentTypeMismatchException.class}) + public ResponseEntity notFound(HttpRequestMethodNotSupportedException e) { + log.debug("notFound: {}", e.getMessage()); return ResponseEntity.notFound().build(); } @@ -52,11 +58,6 @@ public ResponseEntity handle(NoResourceFoundException e) { return ResponseEntity.notFound().build(); } - @ExceptionHandler(HandlerMethodValidationException.class) - public ResponseEntity handle(HandlerMethodValidationException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); - } @ExceptionHandler(AuthenticationException.class) public ResponseEntity handle(AuthenticationException e) { @@ -70,20 +71,6 @@ public ResponseEntity handle(AccessDeniedException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); } - @ExceptionHandler(MissingRequestHeaderException.class) - public ResponseEntity handle(MissingRequestHeaderException e) { - log.debug("MissingRequestHeaderException {}", e.getMessage()); - return ResponseEntity.badRequest() - .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handle(MethodArgumentTypeMismatchException e) { - log.debug("MethodArgumentTypeMismatchException {}", e.getMessage()); - return ResponseEntity.badRequest() - .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); - } - @ExceptionHandler(Exception.class) public ResponseEntity handle(Exception e) { log.error("Exception", e); From beead95917beb97337563adbf55d2d38f0aa1018 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:20:00 +0900 Subject: [PATCH 197/258] =?UTF-8?q?refactor:=20=ED=88=AC=ED=91=9C=20userSe?= =?UTF-8?q?q=20->=20userId=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 --- .../post/application/PostService.java | 4 ++-- .../vote/application/VoteService.java | 20 +++++++++---------- .../java/com/swyp8team2/vote/domain/Vote.java | 10 +++++----- .../vote/domain/VoteRepository.java | 4 ++-- .../vote/presentation/VoteController.java | 6 +++--- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index b9279815..5b643b77 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -88,7 +88,7 @@ private PostImageResponse createVoteResponseDto(PostImage image, Long userId, Lo private Boolean getVoted(PostImage image, Long userId, Long postId) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - return voteRepository.findByUserSeqAndPostId(user.getSeq(), postId) + return voteRepository.findByUserIdAndPostId(user.getId(), postId) .map(vote -> vote.getPostImageId().equals(image.getId())) .orElse(false); } @@ -108,7 +108,7 @@ private SimplePostResponse createSimplePostResponse(Post post) { public CursorBasePaginatedResponse findVotedPosts(Long userId, Long cursor, int size) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - List postIds = voteRepository.findByUserSeq(user.getSeq()) + List postIds = voteRepository.findByUserId(user.getId()) .map(Vote::getPostId) .toList(); Slice postSlice = postRepository.findByIdIn(postIds, cursor, PageRequest.ofSize(size)); diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 9860e8a7..e7f093d9 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -8,7 +8,6 @@ import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; -import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,13 +25,13 @@ public class VoteService { public Long vote(Long voterId, Long postId, Long imageId) { User voter = userRepository.findById(voterId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - deleteVoteIfExisting(postId, voter.getSeq()); - Vote vote = createVote(postId, imageId, voter.getSeq()); + deleteVoteIfExisting(postId, voter.getId()); + Vote vote = createVote(postId, imageId, voter.getId()); return vote.getId(); } - private void deleteVoteIfExisting(Long postId, String userSeq) { - voteRepository.findByUserSeqAndPostId(userSeq, postId) + private void deleteVoteIfExisting(Long postId, Long userId) { + voteRepository.findByUserIdAndPostId(userId, postId) .ifPresent(vote -> { voteRepository.delete(vote); postRepository.findById(postId) @@ -41,18 +40,19 @@ private void deleteVoteIfExisting(Long postId, String userSeq) { }); } - private Vote createVote(Long postId, Long imageId, String userSeq) { + private Vote createVote(Long postId, Long imageId, Long userId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.validateProgress(); - Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userSeq)); + Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userId)); post.vote(imageId); return vote; } - public Long guestVote(String guestId, Long postId, Long imageId) { - deleteVoteIfExisting(postId, guestId); - Vote vote = createVote(postId, imageId, guestId); + @Transactional + public Long guestVote(Long userId, Long postId, Long imageId) { + deleteVoteIfExisting(postId, userId); + Vote vote = createVote(postId, imageId, userId); return vote.getId(); } } diff --git a/src/main/java/com/swyp8team2/vote/domain/Vote.java b/src/main/java/com/swyp8team2/vote/domain/Vote.java index dde80117..91c483fe 100644 --- a/src/main/java/com/swyp8team2/vote/domain/Vote.java +++ b/src/main/java/com/swyp8team2/vote/domain/Vote.java @@ -23,16 +23,16 @@ public class Vote { private Long postImageId; - private String userSeq; + private Long userId; - public Vote(Long id, Long postId, Long postImageId, String userSeq) { + public Vote(Long id, Long postId, Long postImageId, Long userId) { this.id = id; this.postId = postId; this.postImageId = postImageId; - this.userSeq = userSeq; + this.userId = userId; } - public static Vote of(Long postId, Long postImageId, String userSeq) { - return new Vote(null, postId, postImageId, userSeq); + public static Vote of(Long postId, Long postImageId, Long userId) { + return new Vote(null, postId, postImageId, userId); } } diff --git a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java index 786e845f..05c2ccf5 100644 --- a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java +++ b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java @@ -8,7 +8,7 @@ @Repository public interface VoteRepository extends JpaRepository { - Optional findByUserSeqAndPostId(String userSeq, Long postId); + Optional findByUserIdAndPostId(Long userId, Long postId); - Slice findByUserSeq(String seq); + Slice findByUserId(Long userId); } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index 25025afd..51d0caea 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -37,10 +37,10 @@ public ResponseEntity vote( @PostMapping("/guest") public ResponseEntity guestVote( @PathVariable("postId") Long postId, - @RequestHeader(CustomHeader.GUEST_ID) String guestId, - @Valid @RequestBody VoteRequest request + @Valid @RequestBody VoteRequest request, + @AuthenticationPrincipal UserInfo userInfo ) { - voteService.guestVote(guestId, postId, request.imageId()); + voteService.guestVote(userInfo.userId(), postId, request.imageId()); return ResponseEntity.ok().build(); } From c09d4d6d33a6b8aa65b227af805f8c949c0058eb Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:20:16 +0900 Subject: [PATCH 198/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/AuthControllerTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index fc93bafb..676f8231 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; @@ -146,4 +147,23 @@ void reissue_refreshTokenMismatched() throws Exception { .andExpect(status().isBadRequest()) .andExpect(content().json(objectMapper.writeValueAsString(response))); } + + @Test + @DisplayName("게스트 토큰 발급") + void guestLogin() throws Exception { + //given + String guestToken = "guestToken"; + given(authService.guestLogin()) + .willReturn(guestToken); + + //when then + mockMvc.perform(post("/auth/guest/token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(new GuestTokenResponse(guestToken)))) + .andDo(restDocs.document( + responseFields( + fieldWithPath("guestToken").description("게스트 토큰") + ) + )); + } } From 3cebe5a5a561607c60ecde7a5117d1df1094130e Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:25:32 +0900 Subject: [PATCH 199/258] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=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 --- .../comment/application/CommentServiceTest.java | 5 +++-- .../comment/domain/CommentRepositoryTest.java | 2 +- .../post/application/PostServiceTest.java | 2 +- .../com/swyp8team2/support/WithMockUserInfo.java | 2 ++ .../support/config/TestSecurityConfig.java | 8 ++++++-- .../security/TestSecurityContextFactory.java | 3 ++- .../vote/application/VoteServiceTest.java | 15 ++++++++------- .../vote/presentation/VoteControllerTest.java | 9 +++++---- 8 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 29b7f6f0..09d5e374 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -8,6 +8,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import org.junit.jupiter.api.DisplayName; @@ -48,7 +49,7 @@ void createComment() { // given Long postId = 1L; CreateCommentRequest request = new CreateCommentRequest("테스트 댓글"); - UserInfo userInfo = new UserInfo(100L); + UserInfo userInfo = new UserInfo(100L, Role.USER); Comment comment = new Comment(postId, userInfo.userId(), request.content()); // when @@ -69,7 +70,7 @@ void findComments() { Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); - User user = new User(100L, "닉네임","http://example.com/profile.png", "seq"); + User user = new User(100L, "닉네임","http://example.com/profile.png", "seq", Role.USER); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index aaa6f9ca..d8a44e7d 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -35,6 +35,6 @@ void select_CommentUser() { Slice result2 = commentRepository.findByPostId(1L, 1L, PageRequest.of(0, 10)); // then2 - assertThat(result2.getContent()).hasSize(2); + assertThat(result2.getContent()).hasSize(0); } } \ No newline at end of file diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 53b927be..4f5118d9 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -196,7 +196,7 @@ void findVotedPosts() throws Exception { List posts = createPosts(user); for (int i = 0; i < 15; i++) { Post post = posts.get(i); - voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getSeq())); + voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getId())); } int size = 10; diff --git a/src/test/java/com/swyp8team2/support/WithMockUserInfo.java b/src/test/java/com/swyp8team2/support/WithMockUserInfo.java index bb769e56..5b42ff3e 100644 --- a/src/test/java/com/swyp8team2/support/WithMockUserInfo.java +++ b/src/test/java/com/swyp8team2/support/WithMockUserInfo.java @@ -1,6 +1,7 @@ package com.swyp8team2.support; import com.swyp8team2.support.security.TestSecurityContextFactory; +import com.swyp8team2.user.domain.Role; import org.springframework.security.test.context.support.WithSecurityContext; import java.lang.annotation.Retention; @@ -10,4 +11,5 @@ @WithSecurityContext(factory = TestSecurityContextFactory.class) public @interface WithMockUserInfo { long userId() default 1L; + Role role() default Role.USER; } diff --git a/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java index 163d7423..2fd6de4a 100644 --- a/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java +++ b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java @@ -1,5 +1,6 @@ package com.swyp8team2.support.config; +import com.swyp8team2.user.domain.Role; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -9,6 +10,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import static com.swyp8team2.common.config.SecurityConfig.getGuestTokenRequestList; import static com.swyp8team2.common.config.SecurityConfig.getWhiteList; @TestConfiguration @@ -32,8 +34,10 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize - .requestMatchers(getWhiteList(introspect)).permitAll() - .anyRequest().authenticated() +// .requestMatchers(getWhiteList(introspect)).permitAll() +// .requestMatchers(getGuestTokenRequestList(introspect)) +// .hasAnyRole(Role.USER.name(), Role.GUEST.name()) + .anyRequest().permitAll() ); return http.build(); } diff --git a/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java b/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java index 822963d0..e6a06d4a 100644 --- a/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java +++ b/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.support.WithMockUserInfo; +import com.swyp8team2.user.domain.Role; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; @@ -15,7 +16,7 @@ public class TestSecurityContextFactory implements WithSecurityContextFactory assertThat(vote.getUserSeq()).isEqualTo(user.getSeq()), + () -> assertThat(vote.getUserId()).isEqualTo(user.getId()), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) @@ -83,7 +83,7 @@ void vote_change() { Vote vote = voteRepository.findById(voteId).get(); Post findPost = postRepository.findById(post.getId()).get(); assertAll( - () -> assertThat(vote.getUserSeq()).isEqualTo(user.getSeq()), + () -> assertThat(vote.getUserId()).isEqualTo(user.getId()), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), @@ -120,8 +120,8 @@ void vote_alreadyClosed() { @DisplayName("게스트 투표하기") void guestVote() { // given - String guestId = "guestId"; User user = userRepository.save(createUser(1)); + Long guestId = user.getId() + 1L; ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); @@ -133,7 +133,7 @@ void guestVote() { Vote vote = voteRepository.findById(voteId).get(); Post findPost = postRepository.findById(post.getId()).get(); assertAll( - () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getUserId()).isEqualTo(guestId), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) @@ -144,8 +144,8 @@ void guestVote() { @DisplayName("게스트 투표하기 - 다른 이미지로 투표 변경한 경우") void guestVote_change() { // given - String guestId = "guestId"; User user = userRepository.save(createUser(1)); + Long guestId = user.getId() + 1L; ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); @@ -158,7 +158,7 @@ void guestVote_change() { Vote vote = voteRepository.findById(voteId).get(); Post findPost = postRepository.findById(post.getId()).get(); assertAll( - () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getUserId()).isEqualTo(guestId), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), @@ -171,6 +171,7 @@ void guestVote_change() { void guestVote_alreadyClosed() { // given User user = userRepository.save(createUser(1)); + Long guestId = user.getId() + 1L; ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(new Post( @@ -186,7 +187,7 @@ void guestVote_alreadyClosed() { )); // when - assertThatThrownBy(() -> voteService.guestVote("guestId", post.getId(), post.getImages().get(0).getId())) + assertThatThrownBy(() -> voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId())) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); } diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index c28de442..129d98d2 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; +import com.swyp8team2.user.domain.Role; import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; import com.swyp8team2.vote.presentation.dto.VoteRequest; import org.junit.jupiter.api.DisplayName; @@ -58,7 +59,7 @@ void vote() throws Exception { } @Test - @WithAnonymousUser + @WithMockUserInfo(role = Role.GUEST) @DisplayName("게스트 투표") void guestVote() throws Exception { //given @@ -68,7 +69,7 @@ void guestVote() throws Exception { mockMvc.perform(post("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) + .header(CustomHeader.GUEST_ID, "guestToken")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()), @@ -111,7 +112,7 @@ void changeVote() throws Exception { } @Test - @WithAnonymousUser + @WithMockUserInfo(role = Role.GUEST) @DisplayName("게스트 투표 변경") void guestChangeVote() throws Exception { //given @@ -121,7 +122,7 @@ void guestChangeVote() throws Exception { mockMvc.perform(patch("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) + .header(CustomHeader.GUEST_ID, "guestToken")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()), From 25bed71442c4d22aab8256bbf54c93e7d2e51072 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:25:51 +0900 Subject: [PATCH 200/258] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/comment/domain/CommentRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java index dc36a80b..54e0f7e6 100644 --- a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -14,7 +14,7 @@ public interface CommentRepository extends JpaRepository { SELECT c FROM Comment c WHERE c.postId = :postId - AND (:cursor is null or c.id > :cursor) + AND (:cursor is null or c.id < :cursor) ORDER BY c.createdAt DESC """) Slice findByPostId( From 8280d6f7fe17dc73d0db287d575253c7344533aa Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:26:06 +0900 Subject: [PATCH 201/258] =?UTF-8?q?fix:=20=EB=82=B4=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20api=20url=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/post/presentation/PostController.java | 2 +- .../com/swyp8team2/post/presentation/PostControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 80596126..3d4273a0 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -76,7 +76,7 @@ public ResponseEntity deletePost( return ResponseEntity.ok().build(); } - @GetMapping("/user") + @GetMapping("/user/me") public ResponseEntity> findMyPosts( @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 6041d4d4..0d3701f1 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -206,7 +206,7 @@ void findMyPost() throws Exception { .willReturn(response); //when then - mockMvc.perform(get("/posts/user") + mockMvc.perform(get("/posts/user/me") .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) From 1212f2a2026a495a61663971f6c0caadfb958756 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:34:38 +0900 Subject: [PATCH 202/258] =?UTF-8?q?feat:=20baseEntity=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/com/swyp8team2/auth/domain/SocialAccount.java | 3 ++- src/main/java/com/swyp8team2/user/domain/User.java | 5 ++++- src/main/java/com/swyp8team2/vote/domain/Vote.java | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java index ff12cb20..23f784e7 100644 --- a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java @@ -1,6 +1,7 @@ package com.swyp8team2.auth.domain; import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; +import com.swyp8team2.common.domain.BaseEntity; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -16,7 +17,7 @@ @Getter @Entity @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) -public class SocialAccount { +public class SocialAccount extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index a9767fbf..85ad7041 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -1,6 +1,8 @@ package com.swyp8team2.user.domain; +import com.swyp8team2.common.domain.BaseEntity; import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -17,7 +19,7 @@ @Entity @Table(name = "users") @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) -public class User { +public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -29,6 +31,7 @@ public class User { private String seq; + @Enumerated(jakarta.persistence.EnumType.STRING) public Role role; public User(Long id, String nickname, String profileUrl, String seq, Role role) { diff --git a/src/main/java/com/swyp8team2/vote/domain/Vote.java b/src/main/java/com/swyp8team2/vote/domain/Vote.java index 91c483fe..23bd38a9 100644 --- a/src/main/java/com/swyp8team2/vote/domain/Vote.java +++ b/src/main/java/com/swyp8team2/vote/domain/Vote.java @@ -1,5 +1,6 @@ package com.swyp8team2.vote.domain; +import com.swyp8team2.common.domain.BaseEntity; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -13,7 +14,7 @@ @Entity @Table(name = "user_votes") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Vote { +public class Vote extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From 19af103cb35944432fee3b0bf0fc0a2eca5aa49f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:34:48 +0900 Subject: [PATCH 203/258] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=8D=94?= =?UTF-8?q?=EB=AF=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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/com/swyp8team2/common/dev/DataInitializer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index c490ac30..9665d462 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -2,6 +2,8 @@ import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.comment.domain.Comment; +import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; @@ -33,6 +35,7 @@ public class DataInitializer { private final PostRepository postRepository; private final JwtService jwtService; private final VoteService voteService; + private final CommentRepository commentRepository; @Transactional public void init() { @@ -52,12 +55,14 @@ public void init() { ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); posts.add(postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"))); } + } for (User user : users) { for (Post post : posts) { Random random = new Random(); int num = random.nextInt(2); voteService.vote(user.getId(), post.getId(), post.getImages().get(num).getId()); + commentRepository.save(new Comment(post.getId(), user.getId(), "댓글 내용" + random.nextInt(100))); } } } From 80de11da132072dd1221e8e202c341426e4fbc48 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:41:47 +0900 Subject: [PATCH 204/258] =?UTF-8?q?test:=20=EC=95=94=ED=98=B8=ED=99=94=20?= =?UTF-8?q?=EB=B3=B5=ED=98=B8=ED=99=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crypto/application/CryptoServiceTest.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java diff --git a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java new file mode 100644 index 00000000..3142366c --- /dev/null +++ b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java @@ -0,0 +1,61 @@ +package com.swyp8team2.crypto.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class CryptoServiceTest { + + CryptoService cryptoService; + + @BeforeEach + void setUp() throws Exception { + cryptoService = new CryptoService(); + } + + @Test + @DisplayName("암호화 및 복호화") + void encryptAndDecrypt() { + // given + String plainText = "Hello, World!"; + + // when + String encryptedText = cryptoService.encrypt(plainText); + String decryptedText = cryptoService.decrypt(encryptedText); + + // then + assertThat(decryptedText).isEqualTo(plainText); + } + + @Test + @DisplayName("암호화 및 복호화 - 다른 키") + void encryptAndDecrypt_differentKey() throws Exception { + // given + String plainText = "Hello, World!"; + CryptoService differentCryptoService = new CryptoService(); + String encryptedText = differentCryptoService.encrypt(plainText); + + // when then + assertThatThrownBy(() -> cryptoService.decrypt(encryptedText)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + + @Test + @DisplayName("복호화 - 이상한 토큰") + void decrypt_invalidToken() { + // given + String invalid = "invalidToken"; + + // when then + assertThatThrownBy(() -> cryptoService.decrypt(invalid)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } +} From 10b94b80badbb5b8c1f1cfb104337557df18a58c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:55:23 +0900 Subject: [PATCH 205/258] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EA=B4=80=EB=A0=A8=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/swyp8team2/auth/application/AuthService.java | 6 +++--- .../com/swyp8team2/auth/presentation/AuthController.java | 4 ++-- .../swyp8team2/auth/presentation/AuthControllerTest.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 470816d7..bbce4488 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -13,8 +13,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.security.NoSuchAlgorithmException; - @Service @RequiredArgsConstructor public class AuthService { @@ -40,11 +38,13 @@ private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { return socialAccountRepository.save(SocialAccount.create(userId, oAuthUserInfo)); } + @Transactional public TokenPair reissue(String refreshToken) { return jwtService.reissue(refreshToken); } - public String guestLogin() { + @Transactional + public String createGuestToken() { Long guestId = userService.createGuest(); return cryptoService.encrypt(String.valueOf(guestId)); } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index b14bd9c2..2bbeaa44 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -56,8 +56,8 @@ public ResponseEntity reissue( } @PostMapping("/guest/token") - public ResponseEntity guestLogin() { - String guestToken = authService.guestLogin(); + public ResponseEntity guestToken() { + String guestToken = authService.createGuestToken(); return ResponseEntity.ok(new GuestTokenResponse(guestToken)); } } diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 676f8231..4fb50574 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -150,10 +150,10 @@ void reissue_refreshTokenMismatched() throws Exception { @Test @DisplayName("게스트 토큰 발급") - void guestLogin() throws Exception { + void guestToken() throws Exception { //given String guestToken = "guestToken"; - given(authService.guestLogin()) + given(authService.createGuestToken()) .willReturn(guestToken); //when then From 813ea232dfdb61805f53211864964150d6fc2722 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 22:55:37 +0900 Subject: [PATCH 206/258] =?UTF-8?q?docs:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20api=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/docs/asciidoc/auth.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 04449d60..683a2657 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -10,3 +10,8 @@ operation::auth-controller-test/kakao-o-auth-sign-in[snippets='http-request,curl === `POST` 토큰 재발급 operation::auth-controller-test/reissue[snippets='http-request,curl-request,request-cookies,http-response,response-cookies,response-fields'] + +[[게스트-토큰-발급]] +=== `POST` 게스트 토큰 발급 + +operation::auth-controller-test/guest-token[snippets='http-request,curl-request,http-response,response-fields'] From a94201aa1f401ca31c0ae28ef5e81dd9cc0042a0 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 23:03:44 +0900 Subject: [PATCH 207/258] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentService.java | 14 ++++++++------ .../comment/presentation/CommentController.java | 3 +-- .../presentation/dto/CommentResponse.java | 16 +++++++++------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index ad46dd31..49d72c30 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -32,14 +32,16 @@ public void createComment(Long postId, CreateCommentRequest request, UserInfo us commentRepository.save(comment); } - public CursorBasePaginatedResponse findComments(Long postId, Long cursor, int size) { - Slice commentSlice = commentRepository.findByPostId(postId, cursor, PageRequest.of(0, size)); - return CursorBasePaginatedResponse.of(commentSlice.map(this::createCommentResponse)); + public CursorBasePaginatedResponse findComments(Long userId, Long postId, Long cursor, int size) { + Slice commentSlice = commentRepository.findByPostId(postId, cursor, PageRequest.ofSize(size)); + return CursorBasePaginatedResponse.of( + commentSlice.map(comment -> createCommentResponse(comment, userId)) + ); } - private CommentResponse createCommentResponse(Comment comment) { - User user = userRepository.findById(comment.getUserNo()) + private CommentResponse createCommentResponse(Comment comment, Long userId) { + User author = userRepository.findById(comment.getUserNo()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - return CommentResponse.of(comment, user); + return CommentResponse.of(comment, author, author.getId().equals(userId)); } } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index 9f21cba5..69002fca 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -47,8 +47,7 @@ public ResponseEntity> selectCommen @RequestParam(value = "size", required = false, defaultValue = "10") @Min(1) int size, @AuthenticationPrincipal UserInfo userInfo ) { - CursorBasePaginatedResponse response = commentService.findComments(postId, cursor, size); - return ResponseEntity.ok(response); + return ResponseEntity.ok(commentService.findComments(userInfo.userId(), postId, cursor, size)); } @DeleteMapping("/{commentId}") diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index ea3c6f67..3820ec88 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -12,7 +12,8 @@ public record CommentResponse( String content, AuthorDto author, Long voteImageId, - LocalDateTime createdAt + LocalDateTime createdAt, + boolean isAuthor ) implements CursorDto { @Override @@ -21,12 +22,13 @@ public long getId() { return commentId; } - public static CommentResponse of(Comment comment, User user) { + public static CommentResponse of(Comment comment, User user, boolean isAuthor) { return new CommentResponse(comment.getId(), - comment.getContent(), - new AuthorDto(user.getId(), user.getNickname(), user.getProfileUrl()), - null, - comment.getCreatedAt() - ); + comment.getContent(), + new AuthorDto(user.getId(), user.getNickname(), user.getProfileUrl()), + null, + comment.getCreatedAt(), + isAuthor + ); } } From 955d4267b2b3a23370b60bc30f8e3aac7dd0de74 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 23:07:08 +0900 Subject: [PATCH 208/258] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentServiceTest.java | 4 ++-- .../comment/presentation/CommentControllerTest.java | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 09d5e374..deb6f272 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -78,7 +78,7 @@ void findComments() { given(userRepository.findById(100L)).willReturn(Optional.of(user)); // when - CursorBasePaginatedResponse response = commentService.findComments(postId, cursor, size); + CursorBasePaginatedResponse response = commentService.findComments(user.getId(), postId, cursor, size); // then assertThat(response.data()).hasSize(2); @@ -113,7 +113,7 @@ void findComments_userNotFound() { given(userRepository.findById(100L)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> commentService.findComments(postId, cursor, size)) + assertThatThrownBy(() -> commentService.findComments(1L, postId, cursor, size)) .isInstanceOf(BadRequestException.class) .hasMessage((ErrorCode.USER_NOT_FOUND.getMessage())); } diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index ab316900..e568814e 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -72,14 +72,15 @@ void findComments() throws Exception { "댓글 내용", new AuthorDto(100L, "닉네임", "http://example.com/profile.png"), null, - LocalDateTime.now() + LocalDateTime.now(), + false ); List commentList = Collections.singletonList(commentResponse); CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>(null, false, commentList); - when(commentService.findComments(eq(postId), eq(cursor), eq(size))).thenReturn(response); + when(commentService.findComments(null, eq(postId), eq(cursor), eq(size))).thenReturn(response); //when mockMvc.perform(get("/posts/{postId}/comments", "1")) @@ -125,11 +126,14 @@ void findComments() throws Exception { .description("작성자가 투표한 이미지 Id (투표 없을 시 null)"), fieldWithPath("data[].createdAt") .type(JsonFieldType.STRING) - .description("댓글 작성일") + .description("댓글 작성일"), + fieldWithPath("data[].isAuthor") + .type(JsonFieldType.BOOLEAN) + .description("작성자 여부") ) )); - verify(commentService, times(1)).findComments(eq(postId), eq(cursor), eq(size)); + verify(commentService, times(1)).findComments(null, eq(postId), eq(cursor), eq(size)); } @Test From 7f7e69490aba62aa9b43f6fb08caa58ced0d108c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 23:10:45 +0900 Subject: [PATCH 209/258] =?UTF-8?q?feat:=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=9E=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/common/dev/DataInitializer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 9665d462..644eff7a 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -39,6 +39,9 @@ public class DataInitializer { @Transactional public void init() { + if (userRepository.count() > 0) { + return; + } List adjectives = nicknameAdjectiveRepository.findAll(); User testUser = userRepository.save(User.create("nickname", "https://t1.kakaocdn.net/account_images/default_profile.jpeg")); TokenPair tokenPair = jwtService.createToken(testUser.getId()); From defd9d4f0b0cc2fd6976b75bc6150e5f59d9d6eb Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 23:44:34 +0900 Subject: [PATCH 210/258] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/comment/application/CommentService.java | 8 +++++++- .../comment/presentation/CommentController.java | 4 +++- .../comment/presentation/dto/CommentResponse.java | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/application/CommentService.java b/src/main/java/com/swyp8team2/comment/application/CommentService.java index 49d72c30..bfe61db3 100644 --- a/src/main/java/com/swyp8team2/comment/application/CommentService.java +++ b/src/main/java/com/swyp8team2/comment/application/CommentService.java @@ -10,6 +10,8 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.Vote; +import com.swyp8team2.vote.domain.VoteRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; @@ -25,6 +27,7 @@ public class CommentService { private final UserRepository userRepository; private final CommentRepository commentRepository; + private final VoteRepository voteRepository; @Transactional public void createComment(Long postId, CreateCommentRequest request, UserInfo userInfo) { @@ -42,6 +45,9 @@ public CursorBasePaginatedResponse findComments(Long userId, Lo private CommentResponse createCommentResponse(Comment comment, Long userId) { User author = userRepository.findById(comment.getUserNo()) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - return CommentResponse.of(comment, author, author.getId().equals(userId)); + Long voteImageId = voteRepository.findByUserIdAndPostId(userId, comment.getPostId()) + .map(Vote::getPostImageId) + .orElse(null); + return CommentResponse.of(comment, author, author.getId().equals(userId), voteImageId); } } diff --git a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java index 69002fca..b727fb2b 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/CommentController.java +++ b/src/main/java/com/swyp8team2/comment/presentation/CommentController.java @@ -22,6 +22,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @RestController @RequiredArgsConstructor @@ -47,7 +48,8 @@ public ResponseEntity> selectCommen @RequestParam(value = "size", required = false, defaultValue = "10") @Min(1) int size, @AuthenticationPrincipal UserInfo userInfo ) { - return ResponseEntity.ok(commentService.findComments(userInfo.userId(), postId, cursor, size)); + Long userId = Optional.ofNullable(userInfo).map(UserInfo::userId).orElse(null); + return ResponseEntity.ok(commentService.findComments(userId, postId, cursor, size)); } @DeleteMapping("/{commentId}") diff --git a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java index 3820ec88..89848202 100644 --- a/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java +++ b/src/main/java/com/swyp8team2/comment/presentation/dto/CommentResponse.java @@ -22,11 +22,11 @@ public long getId() { return commentId; } - public static CommentResponse of(Comment comment, User user, boolean isAuthor) { + public static CommentResponse of(Comment comment, User user, boolean isAuthor, Long voteImageId) { return new CommentResponse(comment.getId(), comment.getContent(), new AuthorDto(user.getId(), user.getNickname(), user.getProfileUrl()), - null, + voteImageId, comment.getCreatedAt(), isAuthor ); From 14846a4f0efa8834f01b1a7d2c071905014f4e33 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 23:44:49 +0900 Subject: [PATCH 211/258] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=97=AC=EB=B6=80=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swyp8team2/comment/application/CommentServiceTest.java | 5 +++++ .../comment/presentation/CommentControllerTest.java | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index deb6f272..bd5b9844 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -11,6 +11,7 @@ import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; +import com.swyp8team2.vote.domain.VoteRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -43,6 +44,9 @@ class CommentServiceTest { @InjectMocks private CommentService commentService; + @Mock + private VoteRepository voteRepository; + @Test @DisplayName("댓글 생성") void createComment() { @@ -74,6 +78,7 @@ void findComments() { // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); + given(voteRepository.findByUserIdAndPostId(eq(user.getId()), eq(postId))).willReturn(Optional.empty()); // 각 댓글마다 user_no=100L 이므로, findById(100L)만 호출됨 given(userRepository.findById(100L)).willReturn(Optional.of(user)); diff --git a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java index e568814e..da30223a 100644 --- a/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java +++ b/src/test/java/com/swyp8team2/comment/presentation/CommentControllerTest.java @@ -80,7 +80,7 @@ void findComments() throws Exception { CursorBasePaginatedResponse response = new CursorBasePaginatedResponse<>(null, false, commentList); - when(commentService.findComments(null, eq(postId), eq(cursor), eq(size))).thenReturn(response); + when(commentService.findComments(eq(null), eq(postId), eq(cursor), eq(size))).thenReturn(response); //when mockMvc.perform(get("/posts/{postId}/comments", "1")) @@ -133,7 +133,7 @@ void findComments() throws Exception { ) )); - verify(commentService, times(1)).findComments(null, eq(postId), eq(cursor), eq(size)); + verify(commentService, times(1)).findComments(eq(null), eq(postId), eq(cursor), eq(size)); } @Test From af7cbf2e2ebccc532d72467d2f5dd69851bcffec Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 23:45:10 +0900 Subject: [PATCH 212/258] =?UTF-8?q?feat:=20social=20account=20unique=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/com/swyp8team2/auth/domain/SocialAccount.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java index 23f784e7..3675e6c4 100644 --- a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; import com.swyp8team2.common.domain.BaseEntity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -25,6 +26,7 @@ public class SocialAccount extends BaseEntity { private Long userId; + @Column(nullable = false, unique = true) private String socialId; @Enumerated(EnumType.STRING) From d21b201eb5ac0219ca4f6d6ca7afd86f4dec912a Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Thu, 27 Feb 2025 23:47:17 +0900 Subject: [PATCH 213/258] =?UTF-8?q?refactor:=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=A4=ED=8C=A8=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/auth/application/oauth/OAuthService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java index 826e77bc..dd6516a4 100644 --- a/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/oauth/OAuthService.java @@ -3,6 +3,7 @@ import com.swyp8team2.auth.application.oauth.dto.KakaoAuthResponse; import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; import com.swyp8team2.common.config.KakaoOAuthConfig; +import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; import lombok.RequiredArgsConstructor; @@ -29,8 +30,8 @@ public OAuthUserInfo getUserInfo(String code, String redirectUri) { .fetchUserInfo(BEARER + kakaoAuthResponse.accessToken()) .toOAuthUserInfo(); } catch (Exception e) { - log.error("소셜 로그인 실패", e); - throw new InternalServerException(ErrorCode.SOCIAL_AUTHENTICATION_FAILED); + log.debug("소셜 로그인 실패 {}", e.getMessage()); + throw new BadRequestException(ErrorCode.SOCIAL_AUTHENTICATION_FAILED); } } From 1e5d40afb57bbca1734754cdb95c699812e78426 Mon Sep 17 00:00:00 2001 From: Nakji Date: Fri, 28 Feb 2025 00:23:10 +0900 Subject: [PATCH 214/258] =?UTF-8?q?chore:=20=EC=BB=A4=EB=84=A5=EC=85=98=20?= =?UTF-8?q?=EB=88=84=EC=88=98=20=EB=B0=A9=EC=A7=80=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=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 --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 0afb5ee1..79366b38 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 0afb5ee104ee232e74ad823f355b7a6cad80e26a +Subproject commit 79366b381deaf7c65a0e6b83fcc2db23d3aedd6a From 787b7b9fd9204b268eb8e45937c917dcd4abc303 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 00:37:47 +0900 Subject: [PATCH 215/258] =?UTF-8?q?chore:=20base64=20=EC=9A=A9=EB=8F=84=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=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 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 8005ef46..23e36c35 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { // gson implementation 'com.google.code.gson:gson:2.8.6' + // base64 + implementation 'commons-codec:commons-codec:1.15' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' From 24ff903d08134a6342cfbdfa450450e0336e05fd Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 00:39:02 +0900 Subject: [PATCH 216/258] =?UTF-8?q?refactor:=20url=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20base62=EB=A1=9C=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 --- .../crypto/application/CryptoService.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java index 25fddd7b..612755d1 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -4,17 +4,15 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import org.apache.commons.codec.binary.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; -import java.util.Base64; @Slf4j -@Service public class CryptoService { private static final String ALGORITHM = "AES"; @@ -31,7 +29,9 @@ public String encrypt(String data) { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedBytes = cipher.doFinal(data.getBytes()); - return Base64.getEncoder().encodeToString(encryptedBytes); + return Base64.encodeBase64URLSafeString(encryptedBytes) + .replace('+', 'A') + .replace('/', 'B'); } catch (Exception e) { log.error("encrypt error {}", e.getMessage()); throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); @@ -42,8 +42,12 @@ public String decrypt(String encryptedData) { try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); - byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); - return new String(decryptedBytes); + byte[] decoded = Base64.decodeBase64( + encryptedData + .replace('A', '+') + .replace('B', '/') + ); + return new String(cipher.doFinal(decoded)); } catch (IllegalBlockSizeException | BadPaddingException e) { log.debug("decrypt error {}", e.getMessage()); throw new BadRequestException(ErrorCode.INVALID_TOKEN); From c06a4c12e98b34b703ff33a4aecd4f73d132515b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 00:40:26 +0900 Subject: [PATCH 217/258] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9C=A0=20url=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 --- .../auth/application/AuthService.java | 15 ++++++++- .../presentation/filter/GuestAuthFilter.java | 6 +++- .../annotation/GuestTokenCryptoService.java | 16 ++++++++++ .../annotation/ShareUrlCryptoService.java | 16 ++++++++++ .../common/config/CryptoConfig.java | 23 ++++++++++++++ .../common/config/SecurityConfig.java | 9 +++--- .../common/dev/DataInitializer.java | 31 +++++++++++++++++-- .../common/exception/ErrorCode.java | 1 + .../post/application/PostService.java | 28 +++++++++++++++-- .../java/com/swyp8team2/post/domain/Post.java | 14 ++++++--- .../post/presentation/PostController.java | 12 +++++++ .../{ => dto}/CreatePostResponse.java | 2 +- .../user/application/UserService.java | 2 +- 13 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java create mode 100644 src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java create mode 100644 src/main/java/com/swyp8team2/common/config/CryptoConfig.java rename src/main/java/com/swyp8team2/post/presentation/{ => dto}/CreatePostResponse.java (52%) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index bbce4488..99a4e109 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,6 +7,7 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; import lombok.RequiredArgsConstructor; @@ -14,7 +15,6 @@ import org.springframework.transaction.annotation.Transactional; @Service -@RequiredArgsConstructor public class AuthService { private final JwtService jwtService; @@ -23,6 +23,19 @@ public class AuthService { private final UserService userService; private final CryptoService cryptoService; + public AuthService( + JwtService jwtService, + OAuthService oAuthService, + SocialAccountRepository socialAccountRepository, + UserService userService, + @GuestTokenCryptoService CryptoService cryptoService) { + this.jwtService = jwtService; + this.oAuthService = oAuthService; + this.socialAccountRepository = socialAccountRepository; + this.userService = userService; + this.cryptoService = cryptoService; + } + @Transactional public TokenPair oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java index 4b678645..91de4d43 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -1,6 +1,7 @@ package com.swyp8team2.auth.presentation.filter; import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.common.exception.ApplicationException; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; @@ -27,11 +28,14 @@ import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; @Slf4j -@RequiredArgsConstructor public class GuestAuthFilter extends OncePerRequestFilter { private final CryptoService cryptoService; + public GuestAuthFilter(@GuestTokenCryptoService CryptoService cryptoService) { + this.cryptoService = cryptoService; + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java new file mode 100644 index 00000000..90a6e2db --- /dev/null +++ b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java @@ -0,0 +1,16 @@ +package com.swyp8team2.common.annotation; + +import org.springframework.beans.factory.annotation.Qualifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Qualifier(GuestTokenCryptoService.QUALIFIER) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.METHOD}) +public @interface GuestTokenCryptoService { + + String QUALIFIER = "guestTokenCryptoService"; +} diff --git a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java new file mode 100644 index 00000000..2ea4c9f4 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java @@ -0,0 +1,16 @@ +package com.swyp8team2.common.annotation; + +import org.springframework.beans.factory.annotation.Qualifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Qualifier(ShareUrlCryptoService.QUALIFIER) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.METHOD}) +public @interface ShareUrlCryptoService { + + String QUALIFIER = "shareUrlCryptoService"; +} diff --git a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java new file mode 100644 index 00000000..0249cb1a --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java @@ -0,0 +1,23 @@ +package com.swyp8team2.common.config; + +import com.swyp8team2.common.annotation.GuestTokenCryptoService; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.common.annotation.ShareUrlCryptoService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CryptoConfig { + + @GuestTokenCryptoService + @Bean(name = GuestTokenCryptoService.QUALIFIER) + public CryptoService guestTokenCryptoService() throws Exception { + return new CryptoService(); + } + + @ShareUrlCryptoService + @Bean(name = ShareUrlCryptoService.QUALIFIER) + public CryptoService shareUrlCryptoService() throws Exception { + return new CryptoService(); + } +} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index e5de05a9..0994c3fe 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -5,6 +5,7 @@ import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; +import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; @@ -23,13 +24,10 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; -import java.util.List; - @Configuration @EnableWebSecurity public class SecurityConfig { @@ -39,7 +37,7 @@ public class SecurityConfig { public SecurityConfig( @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver, - CryptoService cryptoService + @GuestTokenCryptoService CryptoService cryptoService ) { this.handlerExceptionResolver = handlerExceptionResolver; this.cryptoService = cryptoService; @@ -112,7 +110,8 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), mvc.pattern("/auth/guest/token"), - mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), + mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), + mvc.pattern(HttpMethod.GET, "/posts/{postId}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), // mvc.pattern("/posts/{postId}/votes/guest/**"), mvc.pattern("/auth/oauth2/**") diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 644eff7a..301755c9 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -4,9 +4,12 @@ import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; +import com.swyp8team2.common.annotation.ShareUrlCryptoService; +import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; +import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; @@ -16,6 +19,7 @@ import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.application.VoteService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -26,17 +30,38 @@ @Profile({"dev", "local"}) @Component -@RequiredArgsConstructor public class DataInitializer { private final NicknameAdjectiveRepository nicknameAdjectiveRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final PostRepository postRepository; + private final CryptoService shaereUrlCryptoService; private final JwtService jwtService; private final VoteService voteService; private final CommentRepository commentRepository; + public DataInitializer( + NicknameAdjectiveRepository nicknameAdjectiveRepository, + UserRepository userRepository, + ImageFileRepository imageFileRepository, + PostRepository postRepository, + @ShareUrlCryptoService CryptoService shaereUrlCryptoService, + JwtService jwtService, + VoteService voteService, + CommentRepository commentRepository + ) { + this.nicknameAdjectiveRepository = nicknameAdjectiveRepository; + this.userRepository = userRepository; + this.imageFileRepository = imageFileRepository; + this.postRepository = postRepository; + this.shaereUrlCryptoService = shaereUrlCryptoService; + this.jwtService = jwtService; + this.voteService = voteService; + this.commentRepository = commentRepository; + } + + @Transactional public void init() { if (userRepository.count() > 0) { @@ -56,7 +81,9 @@ public void init() { for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - posts.add(postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"))); + Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())))); + post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); + posts.add(post); } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index bbc5ea04..e717db55 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -35,6 +35,7 @@ public enum ErrorCode { POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"), POST_IMAGE_NOT_FOUND("게시글 이미지 없음"), + SHARE_URL_ALREADY_EXISTS("공유 URL이 이미 존재"), //503 SERVICE_UNAVAILABLE("서비스 이용 불가"), diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 5b643b77..1e97d25d 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,9 +1,11 @@ package com.swyp8team2.post.application; +import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; +import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; @@ -29,7 +31,6 @@ @Service @Transactional(readOnly = true) -@RequiredArgsConstructor public class PostService { private final PostRepository postRepository; @@ -37,12 +38,30 @@ public class PostService { private final RatioCalculator ratioCalculator; private final ImageFileRepository imageFileRepository; private final VoteRepository voteRepository; + private final CryptoService shareUrlCryptoService; + + public PostService( + PostRepository postRepository, + UserRepository userRepository, + RatioCalculator ratioCalculator, + ImageFileRepository imageFileRepository, + VoteRepository voteRepository, + @ShareUrlCryptoService CryptoService shareUrlCryptoService + ) { + this.postRepository = postRepository; + this.userRepository = userRepository; + this.ratioCalculator = ratioCalculator; + this.imageFileRepository = imageFileRepository; + this.voteRepository = voteRepository; + this.shareUrlCryptoService = shareUrlCryptoService; + } @Transactional public Long create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); - Post post = Post.create(userId, request.description(), postImages, "TODO: location"); + Post post = Post.create(userId, request.description(), postImages); Post save = postRepository.save(post); + save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); return save.getId(); } @@ -148,4 +167,9 @@ public void close(Long userId, Long postId) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.close(userId); } + + public PostResponse findByShareUrl(Long userId, String shareUrl) { + String decrypt = shareUrlCryptoService.decrypt(shareUrl); + return findById(userId, Long.valueOf(decrypt)); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 96ad72dd..5d8d2bb2 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -20,8 +20,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; - -import static com.swyp8team2.common.util.Validator.*; +import java.util.Objects; @Getter @Entity @@ -69,8 +68,8 @@ private void validateDescription(String description) { } } - public static Post create(Long userId, String description, List images, String shareUrl) { - return new Post(null, userId, description, State.PROGRESS, images, shareUrl); + public static Post create(Long userId, String description, List images) { + return new Post(null, userId, description, State.PROGRESS, images, null); } public PostImage getBestPickedImage() { @@ -114,4 +113,11 @@ public void validateProgress() { throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); } } + + public void setShareUrl(String shareUrl) { + if (Objects.nonNull(this.shareUrl)) { + throw new InternalServerException(ErrorCode.SHARE_URL_ALREADY_EXISTS); + } + this.shareUrl = shareUrl; + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 3d4273a0..c07264d7 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -4,6 +4,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; @@ -51,6 +52,17 @@ public ResponseEntity findPost( return ResponseEntity.ok(postService.findById(userId, postId)); } + @GetMapping("/shareUrl/{shareUrl}") + public ResponseEntity findPostByShareUrl( + @PathVariable("shareUrl") String shareUrl, + @AuthenticationPrincipal UserInfo userInfo + ) { + Long userId = Optional.ofNullable(userInfo) + .map(UserInfo::userId) + .orElse(null); + return ResponseEntity.ok(postService.findByShareUrl(userId, shareUrl)); + } + @GetMapping("/{postId}/status") public ResponseEntity> findVoteStatus( @PathVariable("postId") Long postId diff --git a/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java similarity index 52% rename from src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java rename to src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java index 44467835..f6629c64 100644 --- a/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java @@ -1,4 +1,4 @@ -package com.swyp8team2.post.presentation; +package com.swyp8team2.post.presentation.dto; public record CreatePostResponse(Long postId) { } diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index b089de58..b97757fb 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -29,7 +29,7 @@ public Long createUser(String nickname, String profileImageUrl) { private String getProfileImage(String profileImageUrl) { return Optional.ofNullable(profileImageUrl) - .orElse("defailt_profile_image"); + .orElse("https://t1.kakaocdn.net/account_images/default_profile.jpeg"); } private String getNickname(String nickname) { From e6bea438697f37fb95ebe4669a9d92f71ecb8fd3 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 00:40:46 +0900 Subject: [PATCH 218/258] =?UTF-8?q?refactor:=20user=20seq=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/swyp8team2/user/domain/User.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 85ad7041..5686f490 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -29,21 +29,18 @@ public class User extends BaseEntity { private String profileUrl; - private String seq; - @Enumerated(jakarta.persistence.EnumType.STRING) public Role role; - public User(Long id, String nickname, String profileUrl, String seq, Role role) { + public User(Long id, String nickname, String profileUrl, Role role) { this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; - this.seq = seq; this.role = role; } public static User create(String nickname, String profileUrl) { - return new User(null, nickname, profileUrl, UUID.randomUUID().toString(), Role.USER); + return new User(null, nickname, profileUrl, Role.USER); } public static User createGuest() { @@ -51,7 +48,6 @@ public static User createGuest() { null, "guest_" + System.currentTimeMillis(), "https://image.photopic.site/images-dev/resized_202502240006030.png", - UUID.randomUUID().toString(), Role.GUEST ); } From 7125a8e89bc6291b490fce3469d6643826c3e1a5 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 00:41:02 +0900 Subject: [PATCH 219/258] =?UTF-8?q?test:=20=EA=B3=B5=EC=9C=A0=20url=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CommentServiceTest.java | 2 +- .../com/swyp8team2/post/domain/PostTest.java | 10 ++-- .../post/presentation/PostControllerTest.java | 53 +++++++++++++++++++ .../support/fixture/FixtureGenerator.java | 3 +- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index bd5b9844..65ca8fbe 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -74,7 +74,7 @@ void findComments() { Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); - User user = new User(100L, "닉네임","http://example.com/profile.png", "seq", Role.USER); + User user = new User(100L, "닉네임","http://example.com/profile.png", Role.USER); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index ab59071e..c0908cf2 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -2,8 +2,6 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.InternalServerException; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,17 +23,15 @@ void create() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - String shareUrl = "shareUrl"; //when - Post post = Post.create(userId, description, postImages, shareUrl); + Post post = Post.create(userId, description, postImages); //then List images = post.getImages(); assertAll( () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(post.getDescription()).isEqualTo(description), - () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), () -> assertThat(post.getState()).isEqualTo(State.PROGRESS), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), @@ -56,7 +52,7 @@ void create_invalidPostImageCount() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, "description", postImages, "shareUrl")) + assertThatThrownBy(() -> Post.create(1L, "description", postImages)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); } @@ -72,7 +68,7 @@ void create_descriptionCountExceeded() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, description, postImages, "shareUrl")) + assertThatThrownBy(() -> Post.create(1L, description, postImages)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 0d3701f1..9d70892f 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; @@ -134,6 +135,58 @@ void findPost() throws Exception { )); } + @Test + @WithAnonymousUser + @DisplayName("게시글 공유 url 상세 조회") + void findPost_shareUrl() throws Exception { + //given + PostResponse response = new PostResponse( + 1L, + new AuthorDto( + 1L, + "author", + "https://image.photopic.site/profile-image" + ), + "description", + List.of( + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) + ), + "https://photopic.site/shareurl", + true, + LocalDateTime.of(2025, 2, 13, 12, 0) + ); + given(postService.findByShareUrl(any(), any())) + .willReturn(response); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/shareUrl/{shareUrl}", "JNOfBVfcG2z89afSiRrOyQ")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + pathParameters( + parameterWithName("shareUrl").description("공유 url") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 Id"), + fieldWithPath("author").type(JsonFieldType.OBJECT).description("게시글 작성자 정보"), + fieldWithPath("author.id").type(JsonFieldType.NUMBER).description("게시글 작성자 유저 Id"), + fieldWithPath("author.nickname").type(JsonFieldType.STRING).description("게시글 작성자 닉네임"), + fieldWithPath("author.profileUrl").type(JsonFieldType.STRING).description("게시글 작성자 프로필 이미지"), + fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("images[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), + fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), + fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), + fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), + fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), + fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), + fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") + ) + )); + } + @Test @WithMockUserInfo @DisplayName("게시글 투표 상태 조회") diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index feafc88d..a11bf052 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -17,8 +17,7 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) - ), - "shareUrl" + key + ) ); } From 213fd7672cecf126d8a282dbca6a7d262bd00691 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 00:53:44 +0900 Subject: [PATCH 220/258] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EA=B3=B5=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/post/application/PostImageNameGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java index ab1edd99..b764e035 100644 --- a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java +++ b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java @@ -16,6 +16,6 @@ public String generate() { if (index >= alphabets.length) { throw new InternalServerException(ErrorCode.POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND); } - return "뽀또" + alphabets[index++]; + return "뽀또 " + alphabets[index++]; } } From 24965cb4f7d91e8f26ef51427082cdeacf40781b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 01:13:45 +0900 Subject: [PATCH 221/258] =?UTF-8?q?docs:=20=EA=B3=B5=EC=9C=A0=20url=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/docs/asciidoc/posts.adoc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index fc4dea9f..54301e7e 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -11,6 +11,13 @@ operation::post-controller-test/create-post[snippets='http-request,curl-request, operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] +[[개사굴-공유-url-조회]] +=== `GET` 게시글 공유 url 조회 + +operation::post-controller-test/find-post_share-url[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] + +[[게시글-목록-조회]] + [[사진-투표-현황-조회]] === `GET` 사진 투표 현황 조회 @@ -32,8 +39,6 @@ operation::post-controller-test/find-voted-post[snippets='http-request,curl-requ operation::post-controller-test/close-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] -[[게시글-수정]] - [[게시글-삭제]] === `DELETE` 게시글 삭제 From 28e12be9d40448d3e85de3e5f11ac72173e6999f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 01:16:50 +0900 Subject: [PATCH 222/258] =?UTF-8?q?test:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EA=B3=B5=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostImageNameGeneratorTest.java | 4 ++-- .../java/com/swyp8team2/post/application/PostServiceTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java index 626bb21f..8f763b94 100644 --- a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java @@ -26,7 +26,7 @@ void generate() throws Exception { String generate2 = postImageNameGenerator.generate(); //then - assertThat(generate1).isEqualTo("뽀또A"); - assertThat(generate2).isEqualTo("뽀또B"); + assertThat(generate1).isEqualTo("뽀또 A"); + assertThat(generate2).isEqualTo("뽀또 B"); } } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 4f5118d9..dde9b629 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -73,10 +73,10 @@ void create() throws Exception { () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), - () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), + () -> assertThat(images.get(0).getName()).isEqualTo("뽀또 A"), () -> assertThat(images.get(0).getVoteCount()).isEqualTo(0), () -> assertThat(images.get(1).getImageFileId()).isEqualTo(2L), - () -> assertThat(images.get(1).getName()).isEqualTo("뽀또B"), + () -> assertThat(images.get(1).getName()).isEqualTo("뽀또 B"), () -> assertThat(images.get(1).getVoteCount()).isEqualTo(0) ); } From e67939210cfb5e1f689f761ba7faefb0cd8fd10b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 01:30:22 +0900 Subject: [PATCH 223/258] Revert "Merge pull request #65 from SWYP-team-2th/feature/shareurl" This reverts commit c54304a5d310fdae9edd476277ba0fbd132c5144, reversing changes made to 0e08871b29de9a5069548022bf965f3e8214d957. --- build.gradle | 3 -- src/docs/asciidoc/posts.adoc | 9 +--- .../auth/application/AuthService.java | 15 +----- .../presentation/filter/GuestAuthFilter.java | 6 +-- .../annotation/GuestTokenCryptoService.java | 16 ------ .../annotation/ShareUrlCryptoService.java | 16 ------ .../common/config/CryptoConfig.java | 23 -------- .../common/config/SecurityConfig.java | 9 ++-- .../common/dev/DataInitializer.java | 31 +---------- .../common/exception/ErrorCode.java | 1 - .../crypto/application/CryptoService.java | 16 +++--- .../application/PostImageNameGenerator.java | 2 +- .../post/application/PostService.java | 28 +--------- .../java/com/swyp8team2/post/domain/Post.java | 14 ++--- .../{dto => }/CreatePostResponse.java | 2 +- .../post/presentation/PostController.java | 12 ----- .../user/application/UserService.java | 2 +- .../java/com/swyp8team2/user/domain/User.java | 8 ++- .../application/CommentServiceTest.java | 2 +- .../PostImageNameGeneratorTest.java | 4 +- .../post/application/PostServiceTest.java | 4 +- .../com/swyp8team2/post/domain/PostTest.java | 10 ++-- .../post/presentation/PostControllerTest.java | 53 ------------------- .../support/fixture/FixtureGenerator.java | 3 +- 24 files changed, 46 insertions(+), 243 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java delete mode 100644 src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java delete mode 100644 src/main/java/com/swyp8team2/common/config/CryptoConfig.java rename src/main/java/com/swyp8team2/post/presentation/{dto => }/CreatePostResponse.java (52%) diff --git a/build.gradle b/build.gradle index 23e36c35..8005ef46 100644 --- a/build.gradle +++ b/build.gradle @@ -45,9 +45,6 @@ dependencies { // gson implementation 'com.google.code.gson:gson:2.8.6' - // base64 - implementation 'commons-codec:commons-codec:1.15' - compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 54301e7e..fc4dea9f 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -11,13 +11,6 @@ operation::post-controller-test/create-post[snippets='http-request,curl-request, operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] -[[개사굴-공유-url-조회]] -=== `GET` 게시글 공유 url 조회 - -operation::post-controller-test/find-post_share-url[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] - -[[게시글-목록-조회]] - [[사진-투표-현황-조회]] === `GET` 사진 투표 현황 조회 @@ -39,6 +32,8 @@ operation::post-controller-test/find-voted-post[snippets='http-request,curl-requ operation::post-controller-test/close-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] +[[게시글-수정]] + [[게시글-삭제]] === `DELETE` 게시글 삭제 diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 99a4e109..bbce4488 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,7 +7,6 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; -import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; import lombok.RequiredArgsConstructor; @@ -15,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class AuthService { private final JwtService jwtService; @@ -23,19 +23,6 @@ public class AuthService { private final UserService userService; private final CryptoService cryptoService; - public AuthService( - JwtService jwtService, - OAuthService oAuthService, - SocialAccountRepository socialAccountRepository, - UserService userService, - @GuestTokenCryptoService CryptoService cryptoService) { - this.jwtService = jwtService; - this.oAuthService = oAuthService; - this.socialAccountRepository = socialAccountRepository; - this.userService = userService; - this.cryptoService = cryptoService; - } - @Transactional public TokenPair oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java index 91de4d43..4b678645 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -1,7 +1,6 @@ package com.swyp8team2.auth.presentation.filter; import com.swyp8team2.auth.domain.UserInfo; -import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.common.exception.ApplicationException; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; @@ -28,14 +27,11 @@ import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; @Slf4j +@RequiredArgsConstructor public class GuestAuthFilter extends OncePerRequestFilter { private final CryptoService cryptoService; - public GuestAuthFilter(@GuestTokenCryptoService CryptoService cryptoService) { - this.cryptoService = cryptoService; - } - @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java deleted file mode 100644 index 90a6e2db..00000000 --- a/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.swyp8team2.common.annotation; - -import org.springframework.beans.factory.annotation.Qualifier; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Qualifier(GuestTokenCryptoService.QUALIFIER) -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER, ElementType.METHOD}) -public @interface GuestTokenCryptoService { - - String QUALIFIER = "guestTokenCryptoService"; -} diff --git a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java deleted file mode 100644 index 2ea4c9f4..00000000 --- a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.swyp8team2.common.annotation; - -import org.springframework.beans.factory.annotation.Qualifier; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Qualifier(ShareUrlCryptoService.QUALIFIER) -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER, ElementType.METHOD}) -public @interface ShareUrlCryptoService { - - String QUALIFIER = "shareUrlCryptoService"; -} diff --git a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java deleted file mode 100644 index 0249cb1a..00000000 --- a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.swyp8team2.common.config; - -import com.swyp8team2.common.annotation.GuestTokenCryptoService; -import com.swyp8team2.crypto.application.CryptoService; -import com.swyp8team2.common.annotation.ShareUrlCryptoService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class CryptoConfig { - - @GuestTokenCryptoService - @Bean(name = GuestTokenCryptoService.QUALIFIER) - public CryptoService guestTokenCryptoService() throws Exception { - return new CryptoService(); - } - - @ShareUrlCryptoService - @Bean(name = ShareUrlCryptoService.QUALIFIER) - public CryptoService shareUrlCryptoService() throws Exception { - return new CryptoService(); - } -} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 0994c3fe..e5de05a9 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -5,7 +5,6 @@ import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; -import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; @@ -24,10 +23,13 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import java.util.List; + @Configuration @EnableWebSecurity public class SecurityConfig { @@ -37,7 +39,7 @@ public class SecurityConfig { public SecurityConfig( @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver, - @GuestTokenCryptoService CryptoService cryptoService + CryptoService cryptoService ) { this.handlerExceptionResolver = handlerExceptionResolver; this.cryptoService = cryptoService; @@ -110,8 +112,7 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), mvc.pattern("/auth/guest/token"), - mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), - mvc.pattern(HttpMethod.GET, "/posts/{postId}"), + mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), // mvc.pattern("/posts/{postId}/votes/guest/**"), mvc.pattern("/auth/oauth2/**") diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 301755c9..644eff7a 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -4,12 +4,9 @@ import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; -import com.swyp8team2.common.annotation.ShareUrlCryptoService; -import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; -import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; @@ -19,7 +16,6 @@ import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.application.VoteService; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -30,38 +26,17 @@ @Profile({"dev", "local"}) @Component +@RequiredArgsConstructor public class DataInitializer { private final NicknameAdjectiveRepository nicknameAdjectiveRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final PostRepository postRepository; - private final CryptoService shaereUrlCryptoService; private final JwtService jwtService; private final VoteService voteService; private final CommentRepository commentRepository; - public DataInitializer( - NicknameAdjectiveRepository nicknameAdjectiveRepository, - UserRepository userRepository, - ImageFileRepository imageFileRepository, - PostRepository postRepository, - @ShareUrlCryptoService CryptoService shaereUrlCryptoService, - JwtService jwtService, - VoteService voteService, - CommentRepository commentRepository - ) { - this.nicknameAdjectiveRepository = nicknameAdjectiveRepository; - this.userRepository = userRepository; - this.imageFileRepository = imageFileRepository; - this.postRepository = postRepository; - this.shaereUrlCryptoService = shaereUrlCryptoService; - this.jwtService = jwtService; - this.voteService = voteService; - this.commentRepository = commentRepository; - } - - @Transactional public void init() { if (userRepository.count() > 0) { @@ -81,9 +56,7 @@ public void init() { for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())))); - post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); - posts.add(post); + posts.add(postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"))); } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index e717db55..bbc5ea04 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -35,7 +35,6 @@ public enum ErrorCode { POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"), POST_IMAGE_NOT_FOUND("게시글 이미지 없음"), - SHARE_URL_ALREADY_EXISTS("공유 URL이 이미 존재"), //503 SERVICE_UNAVAILABLE("서비스 이용 불가"), diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java index 612755d1..25fddd7b 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -4,15 +4,17 @@ import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.codec.binary.Base64; +import org.springframework.stereotype.Service; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; +import java.util.Base64; @Slf4j +@Service public class CryptoService { private static final String ALGORITHM = "AES"; @@ -29,9 +31,7 @@ public String encrypt(String data) { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedBytes = cipher.doFinal(data.getBytes()); - return Base64.encodeBase64URLSafeString(encryptedBytes) - .replace('+', 'A') - .replace('/', 'B'); + return Base64.getEncoder().encodeToString(encryptedBytes); } catch (Exception e) { log.error("encrypt error {}", e.getMessage()); throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); @@ -42,12 +42,8 @@ public String decrypt(String encryptedData) { try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); - byte[] decoded = Base64.decodeBase64( - encryptedData - .replace('A', '+') - .replace('B', '/') - ); - return new String(cipher.doFinal(decoded)); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); + return new String(decryptedBytes); } catch (IllegalBlockSizeException | BadPaddingException e) { log.debug("decrypt error {}", e.getMessage()); throw new BadRequestException(ErrorCode.INVALID_TOKEN); diff --git a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java index b764e035..ab1edd99 100644 --- a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java +++ b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java @@ -16,6 +16,6 @@ public String generate() { if (index >= alphabets.length) { throw new InternalServerException(ErrorCode.POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND); } - return "뽀또 " + alphabets[index++]; + return "뽀또" + alphabets[index++]; } } diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 1e97d25d..5b643b77 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,11 +1,9 @@ package com.swyp8team2.post.application; -import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; -import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; @@ -31,6 +29,7 @@ @Service @Transactional(readOnly = true) +@RequiredArgsConstructor public class PostService { private final PostRepository postRepository; @@ -38,30 +37,12 @@ public class PostService { private final RatioCalculator ratioCalculator; private final ImageFileRepository imageFileRepository; private final VoteRepository voteRepository; - private final CryptoService shareUrlCryptoService; - - public PostService( - PostRepository postRepository, - UserRepository userRepository, - RatioCalculator ratioCalculator, - ImageFileRepository imageFileRepository, - VoteRepository voteRepository, - @ShareUrlCryptoService CryptoService shareUrlCryptoService - ) { - this.postRepository = postRepository; - this.userRepository = userRepository; - this.ratioCalculator = ratioCalculator; - this.imageFileRepository = imageFileRepository; - this.voteRepository = voteRepository; - this.shareUrlCryptoService = shareUrlCryptoService; - } @Transactional public Long create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); - Post post = Post.create(userId, request.description(), postImages); + Post post = Post.create(userId, request.description(), postImages, "TODO: location"); Post save = postRepository.save(post); - save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); return save.getId(); } @@ -167,9 +148,4 @@ public void close(Long userId, Long postId) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.close(userId); } - - public PostResponse findByShareUrl(Long userId, String shareUrl) { - String decrypt = shareUrlCryptoService.decrypt(shareUrl); - return findById(userId, Long.valueOf(decrypt)); - } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 5d8d2bb2..96ad72dd 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -20,7 +20,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Objects; + +import static com.swyp8team2.common.util.Validator.*; @Getter @Entity @@ -68,8 +69,8 @@ private void validateDescription(String description) { } } - public static Post create(Long userId, String description, List images) { - return new Post(null, userId, description, State.PROGRESS, images, null); + public static Post create(Long userId, String description, List images, String shareUrl) { + return new Post(null, userId, description, State.PROGRESS, images, shareUrl); } public PostImage getBestPickedImage() { @@ -113,11 +114,4 @@ public void validateProgress() { throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); } } - - public void setShareUrl(String shareUrl) { - if (Objects.nonNull(this.shareUrl)) { - throw new InternalServerException(ErrorCode.SHARE_URL_ALREADY_EXISTS); - } - this.shareUrl = shareUrl; - } } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java similarity index 52% rename from src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java rename to src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java index f6629c64..44467835 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java @@ -1,4 +1,4 @@ -package com.swyp8team2.post.presentation.dto; +package com.swyp8team2.post.presentation; public record CreatePostResponse(Long postId) { } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index c07264d7..3d4273a0 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -4,7 +4,6 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.presentation.dto.CreatePostRequest; -import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; @@ -52,17 +51,6 @@ public ResponseEntity findPost( return ResponseEntity.ok(postService.findById(userId, postId)); } - @GetMapping("/shareUrl/{shareUrl}") - public ResponseEntity findPostByShareUrl( - @PathVariable("shareUrl") String shareUrl, - @AuthenticationPrincipal UserInfo userInfo - ) { - Long userId = Optional.ofNullable(userInfo) - .map(UserInfo::userId) - .orElse(null); - return ResponseEntity.ok(postService.findByShareUrl(userId, shareUrl)); - } - @GetMapping("/{postId}/status") public ResponseEntity> findVoteStatus( @PathVariable("postId") Long postId diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index b97757fb..b089de58 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -29,7 +29,7 @@ public Long createUser(String nickname, String profileImageUrl) { private String getProfileImage(String profileImageUrl) { return Optional.ofNullable(profileImageUrl) - .orElse("https://t1.kakaocdn.net/account_images/default_profile.jpeg"); + .orElse("defailt_profile_image"); } private String getNickname(String nickname) { diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 5686f490..85ad7041 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -29,18 +29,21 @@ public class User extends BaseEntity { private String profileUrl; + private String seq; + @Enumerated(jakarta.persistence.EnumType.STRING) public Role role; - public User(Long id, String nickname, String profileUrl, Role role) { + public User(Long id, String nickname, String profileUrl, String seq, Role role) { this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; + this.seq = seq; this.role = role; } public static User create(String nickname, String profileUrl) { - return new User(null, nickname, profileUrl, Role.USER); + return new User(null, nickname, profileUrl, UUID.randomUUID().toString(), Role.USER); } public static User createGuest() { @@ -48,6 +51,7 @@ public static User createGuest() { null, "guest_" + System.currentTimeMillis(), "https://image.photopic.site/images-dev/resized_202502240006030.png", + UUID.randomUUID().toString(), Role.GUEST ); } diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 65ca8fbe..bd5b9844 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -74,7 +74,7 @@ void findComments() { Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); - User user = new User(100L, "닉네임","http://example.com/profile.png", Role.USER); + User user = new User(100L, "닉네임","http://example.com/profile.png", "seq", Role.USER); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); diff --git a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java index 8f763b94..626bb21f 100644 --- a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java @@ -26,7 +26,7 @@ void generate() throws Exception { String generate2 = postImageNameGenerator.generate(); //then - assertThat(generate1).isEqualTo("뽀또 A"); - assertThat(generate2).isEqualTo("뽀또 B"); + assertThat(generate1).isEqualTo("뽀또A"); + assertThat(generate2).isEqualTo("뽀또B"); } } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index dde9b629..4f5118d9 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -73,10 +73,10 @@ void create() throws Exception { () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), - () -> assertThat(images.get(0).getName()).isEqualTo("뽀또 A"), + () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), () -> assertThat(images.get(0).getVoteCount()).isEqualTo(0), () -> assertThat(images.get(1).getImageFileId()).isEqualTo(2L), - () -> assertThat(images.get(1).getName()).isEqualTo("뽀또 B"), + () -> assertThat(images.get(1).getName()).isEqualTo("뽀또B"), () -> assertThat(images.get(1).getVoteCount()).isEqualTo(0) ); } diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index c0908cf2..ab59071e 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -2,6 +2,8 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,15 +25,17 @@ void create() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); + String shareUrl = "shareUrl"; //when - Post post = Post.create(userId, description, postImages); + Post post = Post.create(userId, description, postImages, shareUrl); //then List images = post.getImages(); assertAll( () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(post.getDescription()).isEqualTo(description), + () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), () -> assertThat(post.getState()).isEqualTo(State.PROGRESS), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), @@ -52,7 +56,7 @@ void create_invalidPostImageCount() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, "description", postImages)) + assertThatThrownBy(() -> Post.create(1L, "description", postImages, "shareUrl")) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); } @@ -68,7 +72,7 @@ void create_descriptionCountExceeded() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, description, postImages)) + assertThatThrownBy(() -> Post.create(1L, description, postImages, "shareUrl")) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 9d70892f..0d3701f1 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -3,7 +3,6 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; -import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; @@ -135,58 +134,6 @@ void findPost() throws Exception { )); } - @Test - @WithAnonymousUser - @DisplayName("게시글 공유 url 상세 조회") - void findPost_shareUrl() throws Exception { - //given - PostResponse response = new PostResponse( - 1L, - new AuthorDto( - 1L, - "author", - "https://image.photopic.site/profile-image" - ), - "description", - List.of( - new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), - new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) - ), - "https://photopic.site/shareurl", - true, - LocalDateTime.of(2025, 2, 13, 12, 0) - ); - given(postService.findByShareUrl(any(), any())) - .willReturn(response); - - //when then - mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/shareUrl/{shareUrl}", "JNOfBVfcG2z89afSiRrOyQ")) - .andExpect(status().isOk()) - .andExpect(content().json(objectMapper.writeValueAsString(response))) - .andDo(restDocs.document( - pathParameters( - parameterWithName("shareUrl").description("공유 url") - ), - responseFields( - fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 Id"), - fieldWithPath("author").type(JsonFieldType.OBJECT).description("게시글 작성자 정보"), - fieldWithPath("author.id").type(JsonFieldType.NUMBER).description("게시글 작성자 유저 Id"), - fieldWithPath("author.nickname").type(JsonFieldType.STRING).description("게시글 작성자 닉네임"), - fieldWithPath("author.profileUrl").type(JsonFieldType.STRING).description("게시글 작성자 프로필 이미지"), - fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), - fieldWithPath("images[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), - fieldWithPath("images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), - fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), - fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), - fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), - fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), - fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), - fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), - fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") - ) - )); - } - @Test @WithMockUserInfo @DisplayName("게시글 투표 상태 조회") diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index a11bf052..feafc88d 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -17,7 +17,8 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) - ) + ), + "shareUrl" + key ); } From 288908e10ace3ec606b86c566b27bb9d82f40e5b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 11:24:40 +0900 Subject: [PATCH 224/258] =?UTF-8?q?fix:=20encrypt=20decrypt=20=EC=9E=98=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=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 --- .../common/config/CryptoConfig.java | 20 +++++++- .../swyp8team2/crypto/application/Base62.java | 34 ++++++++++++++ .../crypto/application/CryptoService.java | 47 +++++-------------- .../crypto/application/Base62Test.java | 4 ++ 4 files changed, 69 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/swyp8team2/crypto/application/Base62.java create mode 100644 src/test/java/com/swyp8team2/crypto/application/Base62Test.java diff --git a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java index 0249cb1a..7e722dfd 100644 --- a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java +++ b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java @@ -3,21 +3,37 @@ import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.common.annotation.ShareUrlCryptoService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor; @Configuration public class CryptoConfig { + private final String guestTokenSymmetricKey; + private final String shareUrlSymmetricKey; + private final String salt; + + public CryptoConfig( + @Value("${crypto.secret-key.guest-token}") String guestTokenSymmetricKey, + @Value("${crypto.secret-key.share-url}") String shareUrlSymmetricKey, + @Value("${crypto.salt}") String salt + ) { + this.guestTokenSymmetricKey = guestTokenSymmetricKey; + this.shareUrlSymmetricKey = shareUrlSymmetricKey; + this.salt = salt; + } + @GuestTokenCryptoService @Bean(name = GuestTokenCryptoService.QUALIFIER) public CryptoService guestTokenCryptoService() throws Exception { - return new CryptoService(); + return new CryptoService(new AesBytesEncryptor(guestTokenSymmetricKey, salt)); } @ShareUrlCryptoService @Bean(name = ShareUrlCryptoService.QUALIFIER) public CryptoService shareUrlCryptoService() throws Exception { - return new CryptoService(); + return new CryptoService(new AesBytesEncryptor(shareUrlSymmetricKey, salt)); } } diff --git a/src/main/java/com/swyp8team2/crypto/application/Base62.java b/src/main/java/com/swyp8team2/crypto/application/Base62.java new file mode 100644 index 00000000..e83ae4c0 --- /dev/null +++ b/src/main/java/com/swyp8team2/crypto/application/Base62.java @@ -0,0 +1,34 @@ +package com.swyp8team2.crypto.application; + +import java.math.BigInteger; + +public class Base62 { + + private static final String BASE62_ALPHABET = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final int BASE = 62; + + public static String encode(byte[] bytes) { + BigInteger value = new BigInteger(1, bytes); + StringBuilder encoded = new StringBuilder(); + + while (value.compareTo(BigInteger.ZERO) > 0) { + BigInteger[] divRem = value.divideAndRemainder(BigInteger.valueOf(BASE)); + value = divRem[0]; + encoded.insert(0, BASE62_ALPHABET.charAt(divRem[1].intValue())); + } + + return encoded.toString(); + } + + public static byte[] decode(String encoded) { + BigInteger value = BigInteger.ZERO; + + for (char c : encoded.toCharArray()) { + value = value.multiply(BigInteger.valueOf(BASE)) + .add(BigInteger.valueOf(BASE62_ALPHABET.indexOf(c))); + } + + return value.toByteArray(); + } +} diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java index 612755d1..c3ecd7e0 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -3,57 +3,36 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.codec.binary.Base64; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; @Slf4j +@RequiredArgsConstructor public class CryptoService { - private static final String ALGORITHM = "AES"; - private final SecretKey secretKey; - - public CryptoService() throws Exception { - KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); - keyGenerator.init(256); - this.secretKey = keyGenerator.generateKey(); - } + private final AesBytesEncryptor encryptor; public String encrypt(String data) { try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] encryptedBytes = cipher.doFinal(data.getBytes()); - return Base64.encodeBase64URLSafeString(encryptedBytes) - .replace('+', 'A') - .replace('/', 'B'); + byte[] encrypt = encryptor.encrypt(data.getBytes(StandardCharsets.UTF_8)); + return Base62.encode(encrypt); } catch (Exception e) { - log.error("encrypt error {}", e.getMessage()); - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + log.debug("encrypt error {}", e.getMessage()); + throw new BadRequestException(ErrorCode.INVALID_TOKEN); } } public String decrypt(String encryptedData) { try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, secretKey); - byte[] decoded = Base64.decodeBase64( - encryptedData - .replace('A', '+') - .replace('B', '/') - ); - return new String(cipher.doFinal(decoded)); - } catch (IllegalBlockSizeException | BadPaddingException e) { + byte[] decryptBytes = Base62.decode(encryptedData); + byte[] decrypt = encryptor.decrypt(decryptBytes); + return new String(decrypt, StandardCharsets.UTF_8); + } catch (Exception e) { log.debug("decrypt error {}", e.getMessage()); throw new BadRequestException(ErrorCode.INVALID_TOKEN); - } catch (Exception e) { - log.error("decrypt error {}", e.getMessage()); - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); } } } diff --git a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java new file mode 100644 index 00000000..f795b84a --- /dev/null +++ b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class Base62Test { + +} From 276ac424e492f93f004e24bc813cbbc23883c3b0 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 11:24:50 +0900 Subject: [PATCH 225/258] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=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 --- .../crypto/application/Base62Test.java | 45 ++++++++++++++++++- .../crypto/application/CryptoServiceTest.java | 5 ++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java index f795b84a..bd0c180e 100644 --- a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java +++ b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java @@ -1,4 +1,45 @@ -import static org.junit.jupiter.api.Assertions.*; +package com.swyp8team2.crypto.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.*; + class Base62Test { - + + @Test + @DisplayName("인코딩 디코딩") + void encodingAndDecoding() throws Exception { + //given + String plainText = "Hello, World!"; + byte[] bytes = plainText.getBytes(StandardCharsets.UTF_8); + + //when + String encode = Base62.encode(bytes); + byte[] decode = Base62.decode(encode); + + String decodeText = new String(decode, StandardCharsets.UTF_8); + + //then + assertThat(decodeText).isEqualTo(plainText); + } + + @Test + @DisplayName("인코딩 디코딩 - 다른 문자열") + void encodingAndDecoding_differentText() throws Exception { + //given + String plainText = "Hello, World!"; + byte[] bytes = plainText.getBytes(StandardCharsets.UTF_8); + + //when + String encode = Base62.encode(bytes); + byte[] decode = Base62.decode("different"); + + String decodeText = new String(decode, StandardCharsets.UTF_8); + + //then + assertThat(decodeText).isNotEqualTo(plainText); + } } diff --git a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java index 3142366c..801b4151 100644 --- a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java +++ b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -16,7 +17,7 @@ class CryptoServiceTest { @BeforeEach void setUp() throws Exception { - cryptoService = new CryptoService(); + cryptoService = new CryptoService(new AesBytesEncryptor("test", "123456")); } @Test @@ -38,7 +39,7 @@ void encryptAndDecrypt() { void encryptAndDecrypt_differentKey() throws Exception { // given String plainText = "Hello, World!"; - CryptoService differentCryptoService = new CryptoService(); + CryptoService differentCryptoService = new CryptoService(new AesBytesEncryptor("different", "234562")); String encryptedText = differentCryptoService.encrypt(plainText); // when then From b0bd09f0d853177e5936d4e8508aeeb8fd7fd180 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 11:24:59 +0900 Subject: [PATCH 226/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 0afb5ee1..72816997 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 0afb5ee104ee232e74ad823f355b7a6cad80e26a +Subproject commit 72816997e97767c17a566b55a569c6faad7c5f25 From 9afc51bf7ec081fa4c2efaba7ffa0e5ba82b408f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 11:24:40 +0900 Subject: [PATCH 227/258] =?UTF-8?q?fix:=20encrypt=20decrypt=20=EC=9E=98=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=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 # Conflicts: # src/main/java/com/swyp8team2/common/config/CryptoConfig.java # src/main/java/com/swyp8team2/crypto/application/CryptoService.java --- server-config | 2 +- .../common/config/CryptoConfig.java | 39 +++++++++++++++++ .../swyp8team2/crypto/application/Base62.java | 34 +++++++++++++++ .../crypto/application/CryptoService.java | 43 ++++++------------- .../crypto/application/Base62Test.java | 4 ++ 5 files changed, 91 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/swyp8team2/common/config/CryptoConfig.java create mode 100644 src/main/java/com/swyp8team2/crypto/application/Base62.java create mode 100644 src/test/java/com/swyp8team2/crypto/application/Base62Test.java diff --git a/server-config b/server-config index 79366b38..72816997 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 79366b381deaf7c65a0e6b83fcc2db23d3aedd6a +Subproject commit 72816997e97767c17a566b55a569c6faad7c5f25 diff --git a/src/main/java/com/swyp8team2/common/config/CryptoConfig.java b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java new file mode 100644 index 00000000..7e722dfd --- /dev/null +++ b/src/main/java/com/swyp8team2/common/config/CryptoConfig.java @@ -0,0 +1,39 @@ +package com.swyp8team2.common.config; + +import com.swyp8team2.common.annotation.GuestTokenCryptoService; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.common.annotation.ShareUrlCryptoService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor; + +@Configuration +public class CryptoConfig { + + private final String guestTokenSymmetricKey; + private final String shareUrlSymmetricKey; + private final String salt; + + public CryptoConfig( + @Value("${crypto.secret-key.guest-token}") String guestTokenSymmetricKey, + @Value("${crypto.secret-key.share-url}") String shareUrlSymmetricKey, + @Value("${crypto.salt}") String salt + ) { + this.guestTokenSymmetricKey = guestTokenSymmetricKey; + this.shareUrlSymmetricKey = shareUrlSymmetricKey; + this.salt = salt; + } + + @GuestTokenCryptoService + @Bean(name = GuestTokenCryptoService.QUALIFIER) + public CryptoService guestTokenCryptoService() throws Exception { + return new CryptoService(new AesBytesEncryptor(guestTokenSymmetricKey, salt)); + } + + @ShareUrlCryptoService + @Bean(name = ShareUrlCryptoService.QUALIFIER) + public CryptoService shareUrlCryptoService() throws Exception { + return new CryptoService(new AesBytesEncryptor(shareUrlSymmetricKey, salt)); + } +} diff --git a/src/main/java/com/swyp8team2/crypto/application/Base62.java b/src/main/java/com/swyp8team2/crypto/application/Base62.java new file mode 100644 index 00000000..e83ae4c0 --- /dev/null +++ b/src/main/java/com/swyp8team2/crypto/application/Base62.java @@ -0,0 +1,34 @@ +package com.swyp8team2.crypto.application; + +import java.math.BigInteger; + +public class Base62 { + + private static final String BASE62_ALPHABET = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final int BASE = 62; + + public static String encode(byte[] bytes) { + BigInteger value = new BigInteger(1, bytes); + StringBuilder encoded = new StringBuilder(); + + while (value.compareTo(BigInteger.ZERO) > 0) { + BigInteger[] divRem = value.divideAndRemainder(BigInteger.valueOf(BASE)); + value = divRem[0]; + encoded.insert(0, BASE62_ALPHABET.charAt(divRem[1].intValue())); + } + + return encoded.toString(); + } + + public static byte[] decode(String encoded) { + BigInteger value = BigInteger.ZERO; + + for (char c : encoded.toCharArray()) { + value = value.multiply(BigInteger.valueOf(BASE)) + .add(BigInteger.valueOf(BASE62_ALPHABET.indexOf(c))); + } + + return value.toByteArray(); + } +} diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java index 25fddd7b..c3ecd7e0 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -3,53 +3,36 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.util.Base64; +import java.nio.charset.StandardCharsets; @Slf4j -@Service +@RequiredArgsConstructor public class CryptoService { - private static final String ALGORITHM = "AES"; - private final SecretKey secretKey; - - public CryptoService() throws Exception { - KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); - keyGenerator.init(256); - this.secretKey = keyGenerator.generateKey(); - } + private final AesBytesEncryptor encryptor; public String encrypt(String data) { try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] encryptedBytes = cipher.doFinal(data.getBytes()); - return Base64.getEncoder().encodeToString(encryptedBytes); + byte[] encrypt = encryptor.encrypt(data.getBytes(StandardCharsets.UTF_8)); + return Base62.encode(encrypt); } catch (Exception e) { - log.error("encrypt error {}", e.getMessage()); - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + log.debug("encrypt error {}", e.getMessage()); + throw new BadRequestException(ErrorCode.INVALID_TOKEN); } } public String decrypt(String encryptedData) { try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, secretKey); - byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); - return new String(decryptedBytes); - } catch (IllegalBlockSizeException | BadPaddingException e) { + byte[] decryptBytes = Base62.decode(encryptedData); + byte[] decrypt = encryptor.decrypt(decryptBytes); + return new String(decrypt, StandardCharsets.UTF_8); + } catch (Exception e) { log.debug("decrypt error {}", e.getMessage()); throw new BadRequestException(ErrorCode.INVALID_TOKEN); - } catch (Exception e) { - log.error("decrypt error {}", e.getMessage()); - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); } } } diff --git a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java new file mode 100644 index 00000000..f795b84a --- /dev/null +++ b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java @@ -0,0 +1,4 @@ +import static org.junit.jupiter.api.Assertions.*; +class Base62Test { + +} From 0f78923affadcdd0239684972e35448812d47ae8 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 11:24:50 +0900 Subject: [PATCH 228/258] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=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 --- .../crypto/application/Base62Test.java | 45 ++++++++++++++++++- .../crypto/application/CryptoServiceTest.java | 5 ++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java index f795b84a..bd0c180e 100644 --- a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java +++ b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java @@ -1,4 +1,45 @@ -import static org.junit.jupiter.api.Assertions.*; +package com.swyp8team2.crypto.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.*; + class Base62Test { - + + @Test + @DisplayName("인코딩 디코딩") + void encodingAndDecoding() throws Exception { + //given + String plainText = "Hello, World!"; + byte[] bytes = plainText.getBytes(StandardCharsets.UTF_8); + + //when + String encode = Base62.encode(bytes); + byte[] decode = Base62.decode(encode); + + String decodeText = new String(decode, StandardCharsets.UTF_8); + + //then + assertThat(decodeText).isEqualTo(plainText); + } + + @Test + @DisplayName("인코딩 디코딩 - 다른 문자열") + void encodingAndDecoding_differentText() throws Exception { + //given + String plainText = "Hello, World!"; + byte[] bytes = plainText.getBytes(StandardCharsets.UTF_8); + + //when + String encode = Base62.encode(bytes); + byte[] decode = Base62.decode("different"); + + String decodeText = new String(decode, StandardCharsets.UTF_8); + + //then + assertThat(decodeText).isNotEqualTo(plainText); + } } diff --git a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java index 3142366c..801b4151 100644 --- a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java +++ b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.encrypt.AesBytesEncryptor; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -16,7 +17,7 @@ class CryptoServiceTest { @BeforeEach void setUp() throws Exception { - cryptoService = new CryptoService(); + cryptoService = new CryptoService(new AesBytesEncryptor("test", "123456")); } @Test @@ -38,7 +39,7 @@ void encryptAndDecrypt() { void encryptAndDecrypt_differentKey() throws Exception { // given String plainText = "Hello, World!"; - CryptoService differentCryptoService = new CryptoService(); + CryptoService differentCryptoService = new CryptoService(new AesBytesEncryptor("different", "234562")); String encryptedText = differentCryptoService.encrypt(plainText); // when then From 035c899e112850d46b424e87d4db6c5baa3cd142 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 11:32:49 +0900 Subject: [PATCH 229/258] Merge pull request #65 from SWYP-team-2th/feature/shareurl Feature/shareurl --- build.gradle | 3 ++ src/docs/asciidoc/posts.adoc | 9 +++- .../auth/application/AuthService.java | 15 +++++- .../presentation/filter/GuestAuthFilter.java | 6 ++- .../annotation/GuestTokenCryptoService.java | 16 ++++++ .../annotation/ShareUrlCryptoService.java | 16 ++++++ .../common/config/SecurityConfig.java | 9 ++-- .../common/dev/DataInitializer.java | 31 ++++++++++- .../common/exception/ErrorCode.java | 1 + .../application/PostImageNameGenerator.java | 2 +- .../post/application/PostService.java | 28 +++++++++- .../java/com/swyp8team2/post/domain/Post.java | 14 +++-- .../post/presentation/PostController.java | 12 +++++ .../{ => dto}/CreatePostResponse.java | 2 +- .../user/application/UserService.java | 2 +- .../java/com/swyp8team2/user/domain/User.java | 8 +-- .../application/CommentServiceTest.java | 2 +- .../PostImageNameGeneratorTest.java | 4 +- .../post/application/PostServiceTest.java | 4 +- .../com/swyp8team2/post/domain/PostTest.java | 10 ++-- .../post/presentation/PostControllerTest.java | 53 +++++++++++++++++++ .../support/fixture/FixtureGenerator.java | 3 +- 22 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java create mode 100644 src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java rename src/main/java/com/swyp8team2/post/presentation/{ => dto}/CreatePostResponse.java (52%) diff --git a/build.gradle b/build.gradle index 8005ef46..23e36c35 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { // gson implementation 'com.google.code.gson:gson:2.8.6' + // base64 + implementation 'commons-codec:commons-codec:1.15' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index fc4dea9f..54301e7e 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -11,6 +11,13 @@ operation::post-controller-test/create-post[snippets='http-request,curl-request, operation::post-controller-test/find-post[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] +[[개사굴-공유-url-조회]] +=== `GET` 게시글 공유 url 조회 + +operation::post-controller-test/find-post_share-url[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] + +[[게시글-목록-조회]] + [[사진-투표-현황-조회]] === `GET` 사진 투표 현황 조회 @@ -32,8 +39,6 @@ operation::post-controller-test/find-voted-post[snippets='http-request,curl-requ operation::post-controller-test/close-post[snippets='http-request,curl-request,path-parameters,request-headers,http-response'] -[[게시글-수정]] - [[게시글-삭제]] === `DELETE` 게시글 삭제 diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index bbce4488..99a4e109 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,6 +7,7 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; import lombok.RequiredArgsConstructor; @@ -14,7 +15,6 @@ import org.springframework.transaction.annotation.Transactional; @Service -@RequiredArgsConstructor public class AuthService { private final JwtService jwtService; @@ -23,6 +23,19 @@ public class AuthService { private final UserService userService; private final CryptoService cryptoService; + public AuthService( + JwtService jwtService, + OAuthService oAuthService, + SocialAccountRepository socialAccountRepository, + UserService userService, + @GuestTokenCryptoService CryptoService cryptoService) { + this.jwtService = jwtService; + this.oAuthService = oAuthService; + this.socialAccountRepository = socialAccountRepository; + this.userService = userService; + this.cryptoService = cryptoService; + } + @Transactional public TokenPair oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java index 4b678645..91de4d43 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -1,6 +1,7 @@ package com.swyp8team2.auth.presentation.filter; import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.common.exception.ApplicationException; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; @@ -27,11 +28,14 @@ import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; @Slf4j -@RequiredArgsConstructor public class GuestAuthFilter extends OncePerRequestFilter { private final CryptoService cryptoService; + public GuestAuthFilter(@GuestTokenCryptoService CryptoService cryptoService) { + this.cryptoService = cryptoService; + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java new file mode 100644 index 00000000..90a6e2db --- /dev/null +++ b/src/main/java/com/swyp8team2/common/annotation/GuestTokenCryptoService.java @@ -0,0 +1,16 @@ +package com.swyp8team2.common.annotation; + +import org.springframework.beans.factory.annotation.Qualifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Qualifier(GuestTokenCryptoService.QUALIFIER) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.METHOD}) +public @interface GuestTokenCryptoService { + + String QUALIFIER = "guestTokenCryptoService"; +} diff --git a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java new file mode 100644 index 00000000..2ea4c9f4 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java @@ -0,0 +1,16 @@ +package com.swyp8team2.common.annotation; + +import org.springframework.beans.factory.annotation.Qualifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Qualifier(ShareUrlCryptoService.QUALIFIER) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.METHOD}) +public @interface ShareUrlCryptoService { + + String QUALIFIER = "shareUrlCryptoService"; +} diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index e5de05a9..0994c3fe 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -5,6 +5,7 @@ import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; +import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; @@ -23,13 +24,10 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; -import java.util.List; - @Configuration @EnableWebSecurity public class SecurityConfig { @@ -39,7 +37,7 @@ public class SecurityConfig { public SecurityConfig( @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver, - CryptoService cryptoService + @GuestTokenCryptoService CryptoService cryptoService ) { this.handlerExceptionResolver = handlerExceptionResolver; this.cryptoService = cryptoService; @@ -112,7 +110,8 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), mvc.pattern("/auth/guest/token"), - mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), + mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), + mvc.pattern(HttpMethod.GET, "/posts/{postId}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), // mvc.pattern("/posts/{postId}/votes/guest/**"), mvc.pattern("/auth/oauth2/**") diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 644eff7a..301755c9 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -4,9 +4,12 @@ import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; +import com.swyp8team2.common.annotation.ShareUrlCryptoService; +import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; +import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; @@ -16,6 +19,7 @@ import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.application.VoteService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -26,17 +30,38 @@ @Profile({"dev", "local"}) @Component -@RequiredArgsConstructor public class DataInitializer { private final NicknameAdjectiveRepository nicknameAdjectiveRepository; private final UserRepository userRepository; private final ImageFileRepository imageFileRepository; private final PostRepository postRepository; + private final CryptoService shaereUrlCryptoService; private final JwtService jwtService; private final VoteService voteService; private final CommentRepository commentRepository; + public DataInitializer( + NicknameAdjectiveRepository nicknameAdjectiveRepository, + UserRepository userRepository, + ImageFileRepository imageFileRepository, + PostRepository postRepository, + @ShareUrlCryptoService CryptoService shaereUrlCryptoService, + JwtService jwtService, + VoteService voteService, + CommentRepository commentRepository + ) { + this.nicknameAdjectiveRepository = nicknameAdjectiveRepository; + this.userRepository = userRepository; + this.imageFileRepository = imageFileRepository; + this.postRepository = postRepository; + this.shaereUrlCryptoService = shaereUrlCryptoService; + this.jwtService = jwtService; + this.voteService = voteService; + this.commentRepository = commentRepository; + } + + @Transactional public void init() { if (userRepository.count() > 0) { @@ -56,7 +81,9 @@ public void init() { for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - posts.add(postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"))); + Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())))); + post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); + posts.add(post); } } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index bbc5ea04..e717db55 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -35,6 +35,7 @@ public enum ErrorCode { POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND("이미지 이름 생성기 인덱스 초과"), IMAGE_FILE_NOT_FOUND("존재하지 않는 이미지"), POST_IMAGE_NOT_FOUND("게시글 이미지 없음"), + SHARE_URL_ALREADY_EXISTS("공유 URL이 이미 존재"), //503 SERVICE_UNAVAILABLE("서비스 이용 불가"), diff --git a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java index ab1edd99..b764e035 100644 --- a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java +++ b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java @@ -16,6 +16,6 @@ public String generate() { if (index >= alphabets.length) { throw new InternalServerException(ErrorCode.POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND); } - return "뽀또" + alphabets[index++]; + return "뽀또 " + alphabets[index++]; } } diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 5b643b77..1e97d25d 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -1,9 +1,11 @@ package com.swyp8team2.post.application; +import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; +import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; @@ -29,7 +31,6 @@ @Service @Transactional(readOnly = true) -@RequiredArgsConstructor public class PostService { private final PostRepository postRepository; @@ -37,12 +38,30 @@ public class PostService { private final RatioCalculator ratioCalculator; private final ImageFileRepository imageFileRepository; private final VoteRepository voteRepository; + private final CryptoService shareUrlCryptoService; + + public PostService( + PostRepository postRepository, + UserRepository userRepository, + RatioCalculator ratioCalculator, + ImageFileRepository imageFileRepository, + VoteRepository voteRepository, + @ShareUrlCryptoService CryptoService shareUrlCryptoService + ) { + this.postRepository = postRepository; + this.userRepository = userRepository; + this.ratioCalculator = ratioCalculator; + this.imageFileRepository = imageFileRepository; + this.voteRepository = voteRepository; + this.shareUrlCryptoService = shareUrlCryptoService; + } @Transactional public Long create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); - Post post = Post.create(userId, request.description(), postImages, "TODO: location"); + Post post = Post.create(userId, request.description(), postImages); Post save = postRepository.save(post); + save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); return save.getId(); } @@ -148,4 +167,9 @@ public void close(Long userId, Long postId) { .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.close(userId); } + + public PostResponse findByShareUrl(Long userId, String shareUrl) { + String decrypt = shareUrlCryptoService.decrypt(shareUrl); + return findById(userId, Long.valueOf(decrypt)); + } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 96ad72dd..5d8d2bb2 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -20,8 +20,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; - -import static com.swyp8team2.common.util.Validator.*; +import java.util.Objects; @Getter @Entity @@ -69,8 +68,8 @@ private void validateDescription(String description) { } } - public static Post create(Long userId, String description, List images, String shareUrl) { - return new Post(null, userId, description, State.PROGRESS, images, shareUrl); + public static Post create(Long userId, String description, List images) { + return new Post(null, userId, description, State.PROGRESS, images, null); } public PostImage getBestPickedImage() { @@ -114,4 +113,11 @@ public void validateProgress() { throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); } } + + public void setShareUrl(String shareUrl) { + if (Objects.nonNull(this.shareUrl)) { + throw new InternalServerException(ErrorCode.SHARE_URL_ALREADY_EXISTS); + } + this.shareUrl = shareUrl; + } } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 3d4273a0..c07264d7 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -4,6 +4,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.application.PostService; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; @@ -51,6 +52,17 @@ public ResponseEntity findPost( return ResponseEntity.ok(postService.findById(userId, postId)); } + @GetMapping("/shareUrl/{shareUrl}") + public ResponseEntity findPostByShareUrl( + @PathVariable("shareUrl") String shareUrl, + @AuthenticationPrincipal UserInfo userInfo + ) { + Long userId = Optional.ofNullable(userInfo) + .map(UserInfo::userId) + .orElse(null); + return ResponseEntity.ok(postService.findByShareUrl(userId, shareUrl)); + } + @GetMapping("/{postId}/status") public ResponseEntity> findVoteStatus( @PathVariable("postId") Long postId diff --git a/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java similarity index 52% rename from src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java rename to src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java index 44467835..f6629c64 100644 --- a/src/main/java/com/swyp8team2/post/presentation/CreatePostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java @@ -1,4 +1,4 @@ -package com.swyp8team2.post.presentation; +package com.swyp8team2.post.presentation.dto; public record CreatePostResponse(Long postId) { } diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index b089de58..b97757fb 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -29,7 +29,7 @@ public Long createUser(String nickname, String profileImageUrl) { private String getProfileImage(String profileImageUrl) { return Optional.ofNullable(profileImageUrl) - .orElse("defailt_profile_image"); + .orElse("https://t1.kakaocdn.net/account_images/default_profile.jpeg"); } private String getNickname(String nickname) { diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index 85ad7041..5686f490 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -29,21 +29,18 @@ public class User extends BaseEntity { private String profileUrl; - private String seq; - @Enumerated(jakarta.persistence.EnumType.STRING) public Role role; - public User(Long id, String nickname, String profileUrl, String seq, Role role) { + public User(Long id, String nickname, String profileUrl, Role role) { this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; - this.seq = seq; this.role = role; } public static User create(String nickname, String profileUrl) { - return new User(null, nickname, profileUrl, UUID.randomUUID().toString(), Role.USER); + return new User(null, nickname, profileUrl, Role.USER); } public static User createGuest() { @@ -51,7 +48,6 @@ public static User createGuest() { null, "guest_" + System.currentTimeMillis(), "https://image.photopic.site/images-dev/resized_202502240006030.png", - UUID.randomUUID().toString(), Role.GUEST ); } diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index bd5b9844..65ca8fbe 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -74,7 +74,7 @@ void findComments() { Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); - User user = new User(100L, "닉네임","http://example.com/profile.png", "seq", Role.USER); + User user = new User(100L, "닉네임","http://example.com/profile.png", Role.USER); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); diff --git a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java index 626bb21f..8f763b94 100644 --- a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java @@ -26,7 +26,7 @@ void generate() throws Exception { String generate2 = postImageNameGenerator.generate(); //then - assertThat(generate1).isEqualTo("뽀또A"); - assertThat(generate2).isEqualTo("뽀또B"); + assertThat(generate1).isEqualTo("뽀또 A"); + assertThat(generate2).isEqualTo("뽀또 B"); } } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 4f5118d9..dde9b629 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -73,10 +73,10 @@ void create() throws Exception { () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), - () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), + () -> assertThat(images.get(0).getName()).isEqualTo("뽀또 A"), () -> assertThat(images.get(0).getVoteCount()).isEqualTo(0), () -> assertThat(images.get(1).getImageFileId()).isEqualTo(2L), - () -> assertThat(images.get(1).getName()).isEqualTo("뽀또B"), + () -> assertThat(images.get(1).getName()).isEqualTo("뽀또 B"), () -> assertThat(images.get(1).getVoteCount()).isEqualTo(0) ); } diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index ab59071e..c0908cf2 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -2,8 +2,6 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; -import com.swyp8team2.common.exception.InternalServerException; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,17 +23,15 @@ void create() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - String shareUrl = "shareUrl"; //when - Post post = Post.create(userId, description, postImages, shareUrl); + Post post = Post.create(userId, description, postImages); //then List images = post.getImages(); assertAll( () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(post.getDescription()).isEqualTo(description), - () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), () -> assertThat(post.getState()).isEqualTo(State.PROGRESS), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), @@ -56,7 +52,7 @@ void create_invalidPostImageCount() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, "description", postImages, "shareUrl")) + assertThatThrownBy(() -> Post.create(1L, "description", postImages)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_POST_IMAGE_COUNT.getMessage()); } @@ -72,7 +68,7 @@ void create_descriptionCountExceeded() throws Exception { ); //when then - assertThatThrownBy(() -> Post.create(1L, description, postImages, "shareUrl")) + assertThatThrownBy(() -> Post.create(1L, description, postImages)) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.DESCRIPTION_LENGTH_EXCEEDED.getMessage()); } diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 0d3701f1..9d70892f 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; @@ -134,6 +135,58 @@ void findPost() throws Exception { )); } + @Test + @WithAnonymousUser + @DisplayName("게시글 공유 url 상세 조회") + void findPost_shareUrl() throws Exception { + //given + PostResponse response = new PostResponse( + 1L, + new AuthorDto( + 1L, + "author", + "https://image.photopic.site/profile-image" + ), + "description", + List.of( + new PostImageResponse(1L, "뽀또A", "https://image.photopic.site/image/1", "https://image.photopic.site/image/resize/1", true), + new PostImageResponse(2L, "뽀또B", "https://image.photopic.site/image/2", "https://image.photopic.site/image/resize/2", false) + ), + "https://photopic.site/shareurl", + true, + LocalDateTime.of(2025, 2, 13, 12, 0) + ); + given(postService.findByShareUrl(any(), any())) + .willReturn(response); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/shareUrl/{shareUrl}", "JNOfBVfcG2z89afSiRrOyQ")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + pathParameters( + parameterWithName("shareUrl").description("공유 url") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 Id"), + fieldWithPath("author").type(JsonFieldType.OBJECT).description("게시글 작성자 정보"), + fieldWithPath("author.id").type(JsonFieldType.NUMBER).description("게시글 작성자 유저 Id"), + fieldWithPath("author.nickname").type(JsonFieldType.STRING).description("게시글 작성자 닉네임"), + fieldWithPath("author.profileUrl").type(JsonFieldType.STRING).description("게시글 작성자 프로필 이미지"), + fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("images[]").type(JsonFieldType.ARRAY).description("투표 선택지 목록"), + fieldWithPath("images[].id").type(JsonFieldType.NUMBER).description("투표 선택지 Id"), + fieldWithPath("images[].imageName").type(JsonFieldType.STRING).description("사진 이름"), + fieldWithPath("images[].imageUrl").type(JsonFieldType.STRING).description("사진 이미지"), + fieldWithPath("images[].thumbnailUrl").type(JsonFieldType.STRING).description("확대 사진 이미지"), + fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), + fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), + fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") + ) + )); + } + @Test @WithMockUserInfo @DisplayName("게시글 투표 상태 조회") diff --git a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java index feafc88d..a11bf052 100644 --- a/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java +++ b/src/test/java/com/swyp8team2/support/fixture/FixtureGenerator.java @@ -17,8 +17,7 @@ public static Post createPost(Long userId, ImageFile imageFile1, ImageFile image List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) - ), - "shareUrl" + key + ) ); } From db7e97ab12022a386d3dad911eea0a1b986fd207 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 11:43:24 +0900 Subject: [PATCH 230/258] =?UTF-8?q?fix:=20=EB=8D=94=EB=AF=B8=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=88=AC=ED=91=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=A6=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/com/swyp8team2/common/dev/DataInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 301755c9..1ad636bf 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -81,7 +81,7 @@ public void init() { for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())))); + Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또 A", imageFile1.getId()), PostImage.create("뽀또 B", imageFile2.getId())))); post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); posts.add(post); } From 81376a909cba6c5b7d6338c183fa9b1e1c724094 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 15:54:51 +0900 Subject: [PATCH 231/258] =?UTF-8?q?feat:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=97=90=EB=9F=AC=20=EC=95=8C=EB=A6=BC=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 --- .../common/config/HttpInterfaceConfig.java | 6 ++ .../ApplicationControllerAdvice.java | 13 +++- .../common/exception/DiscordClient.java | 27 +++++++ .../common/exception/DiscordMessage.java | 18 +++++ .../exception/DiscordMessageSender.java | 78 +++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/swyp8team2/common/exception/DiscordClient.java create mode 100644 src/main/java/com/swyp8team2/common/exception/DiscordMessage.java create mode 100644 src/main/java/com/swyp8team2/common/exception/DiscordMessageSender.java diff --git a/src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java b/src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java index b95414e9..6f385379 100644 --- a/src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java +++ b/src/main/java/com/swyp8team2/common/config/HttpInterfaceConfig.java @@ -1,6 +1,7 @@ package com.swyp8team2.common.config; import com.swyp8team2.auth.application.oauth.KakaoOAuthClient; +import com.swyp8team2.common.exception.DiscordClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; @@ -17,4 +18,9 @@ public KakaoOAuthClient kakaoAuthClient() { .builderFor(adapter).build(); return build.createClient(KakaoOAuthClient.class); } + + @Bean + public RestClient restClient() { + return RestClient.create(); + } } diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 3d2fc530..4dc5fd55 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -1,6 +1,8 @@ package com.swyp8team2.common.exception; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -9,17 +11,23 @@ import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.resource.NoResourceFoundException; import javax.naming.AuthenticationException; import java.nio.file.AccessDeniedException; +import java.util.Arrays; @Slf4j @RestControllerAdvice +@RequiredArgsConstructor public class ApplicationControllerAdvice { + private final DiscordMessageSender discordMessageSender; + private final Environment environment; + @ExceptionHandler(BadRequestException.class) public ResponseEntity handle(BadRequestException e) { ErrorResponse response = new ErrorResponse(e.getErrorCode()); @@ -72,8 +80,11 @@ public ResponseEntity handle(AccessDeniedException e) { } @ExceptionHandler(Exception.class) - public ResponseEntity handle(Exception e) { + public ResponseEntity handle(Exception e, WebRequest webRequest) { log.error("Exception", e); + if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) { + discordMessageSender.sendDiscordAlarm(e, webRequest); + } return ResponseEntity.internalServerError() .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR)); } diff --git a/src/main/java/com/swyp8team2/common/exception/DiscordClient.java b/src/main/java/com/swyp8team2/common/exception/DiscordClient.java new file mode 100644 index 00000000..53de6ae4 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/DiscordClient.java @@ -0,0 +1,27 @@ +package com.swyp8team2.common.exception; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component +public class DiscordClient { + + private final RestClient restClient; + private final String discordWebhookUrl; + + public DiscordClient( + RestClient restClient, + @Value("${discord.webhook.url}") String discordWebhookUrl) { + this.restClient = restClient; + this.discordWebhookUrl = discordWebhookUrl; + } + + public void sendAlarm(DiscordMessage request) { + restClient.post() + .uri(discordWebhookUrl) + .body(request) + .retrieve() + .toBodilessEntity(); + } +} diff --git a/src/main/java/com/swyp8team2/common/exception/DiscordMessage.java b/src/main/java/com/swyp8team2/common/exception/DiscordMessage.java new file mode 100644 index 00000000..9f966081 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/DiscordMessage.java @@ -0,0 +1,18 @@ +package com.swyp8team2.common.exception; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record DiscordMessage( + String content, + List embeds +) { + + @Builder + record Embed( + String title, + String description + ) { } +} diff --git a/src/main/java/com/swyp8team2/common/exception/DiscordMessageSender.java b/src/main/java/com/swyp8team2/common/exception/DiscordMessageSender.java new file mode 100644 index 00000000..122e0d53 --- /dev/null +++ b/src/main/java/com/swyp8team2/common/exception/DiscordMessageSender.java @@ -0,0 +1,78 @@ +package com.swyp8team2.common.exception; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.Clock; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class DiscordMessageSender { + + private final DiscordClient discordClient; + private final Clock clock; + private final Environment environment; + + public void sendDiscordAlarm(Exception e, WebRequest request) { + discordClient.sendAlarm(createMessage(e, request)); + } + + private DiscordMessage createMessage(Exception e, WebRequest request) { + List profiles = Arrays.asList(environment.getActiveProfiles()); + return DiscordMessage.builder() + .content("# 🚨 에러 발생") + .embeds( + List.of( + DiscordMessage.Embed.builder() + .title("ℹ️ 에러 정보") + .description( + """ + ### 📝 환경 정보 + %s + ### 🕖 발생 시간 + %s + ### 🔗 요청 URL + %s + ### 📄 Stack Trace + ``` + %s + ``` + """.formatted( + String.join("", profiles), + Instant.now(clock), + createRequestFullPath(request), + getStackTrace(e).substring(0, 1000) + )) + .build() + ) + ) + .build(); + } + + private String createRequestFullPath(WebRequest webRequest) { + HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); + String fullPath = request.getMethod() + " " + request.getRequestURL(); + + String queryString = request.getQueryString(); + if (queryString != null) { + fullPath += "?" + queryString; + } + + return fullPath; + } + + private String getStackTrace(Exception e) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } +} From 9e2bbe44442f38bed5629a16f71fe8e5635a606e Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 16:10:36 +0900 Subject: [PATCH 232/258] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80=20Id=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 --- .../swyp8team2/auth/application/AuthService.java | 6 +++--- .../auth/application/jwt/JwtService.java | 9 +++++---- .../auth/presentation/AuthController.java | 15 +++++++++------ .../auth/presentation/dto/AuthResponse.java | 4 ++++ .../auth/presentation/dto/TokenResponse.java | 7 ++++++- .../swyp8team2/common/dev/DataInitializer.java | 4 +++- .../auth/application/AuthServiceTest.java | 4 +++- .../auth/application/JwtServiceTest.java | 7 +++++-- .../auth/presentation/AuthControllerTest.java | 16 ++++++++++------ 9 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 99a4e109..d19a6203 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,10 +7,10 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.annotation.GuestTokenCryptoService; import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -37,7 +37,7 @@ public AuthService( } @Transactional - public TokenPair oauthSignIn(String code, String redirectUri) { + public TokenResponse oauthSignIn(String code, String redirectUri) { OAuthUserInfo oAuthUserInfo = oAuthService.getUserInfo(code, redirectUri); SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider( oAuthUserInfo.socialId(), @@ -52,7 +52,7 @@ private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { } @Transactional - public TokenPair reissue(String refreshToken) { + public TokenResponse reissue(String refreshToken) { return jwtService.reissue(refreshToken); } diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java index 389e0461..b7963ca8 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.domain.RefreshToken; import com.swyp8team2.auth.domain.RefreshTokenRepository; +import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import jakarta.transaction.Transactional; @@ -18,7 +19,7 @@ public class JwtService { private final RefreshTokenRepository refreshTokenRepository; @Transactional - public TokenPair createToken(long userId) { + public TokenResponse createToken(long userId) { TokenPair tokenPair = jwtProvider.createToken(new JwtClaim(userId)); RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId) .orElseGet(() -> new RefreshToken(userId, tokenPair.refreshToken())); @@ -27,11 +28,11 @@ public TokenPair createToken(long userId) { log.debug("createToken userId: {} accessToken: {} refreshToken: {}", userId, tokenPair.accessToken(), tokenPair.refreshToken()); - return tokenPair; + return new TokenResponse(tokenPair, userId); } @Transactional - public TokenPair reissue(String refreshToken) { + public TokenResponse reissue(String refreshToken) { JwtClaim claim = jwtProvider.parseToken(refreshToken); RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(claim.idAsLong()) .orElseThrow(() -> new BadRequestException(ErrorCode.REFRESH_TOKEN_NOT_FOUND)); @@ -41,6 +42,6 @@ public TokenPair reissue(String refreshToken) { log.debug("reissue userId: {} accessToken: {} refreshToken: {}", claim.id(), tokenPair.accessToken(), tokenPair.refreshToken()); - return tokenPair; + return new TokenResponse(tokenPair, claim.idAsLong()); } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 2bbeaa44..249f2a7d 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -6,6 +6,7 @@ import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; +import com.swyp8team2.auth.presentation.dto.AuthResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.presentation.CustomHeader; @@ -31,28 +32,30 @@ public class AuthController { private final AuthService authService; @PostMapping("/oauth2/code/kakao") - public ResponseEntity kakaoOAuthSignIn( + public ResponseEntity kakaoOAuthSignIn( @Valid @RequestBody OAuthSignInRequest request, HttpServletResponse response ) { - TokenPair tokenPair = authService.oauthSignIn(request.code(), request.redirectUri()); + TokenResponse tokenResponse = authService.oauthSignIn(request.code(), request.redirectUri()); + TokenPair tokenPair = tokenResponse.tokenPair(); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); response.addCookie(cookie); - return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); + return ResponseEntity.ok(new AuthResponse(tokenPair.accessToken(), tokenResponse.userId())); } @PostMapping("/reissue") - public ResponseEntity reissue( + public ResponseEntity reissue( @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, HttpServletResponse response ) { if (Objects.isNull(refreshToken)) { throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); } - TokenPair tokenPair = authService.reissue(refreshToken); + TokenResponse tokenResponse = authService.reissue(refreshToken); + TokenPair tokenPair = tokenResponse.tokenPair(); Cookie cookie = refreshTokenCookieGenerator.createCookie(tokenPair.refreshToken()); response.addCookie(cookie); - return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); + return ResponseEntity.ok(new AuthResponse(tokenPair.accessToken(), tokenResponse.userId())); } @PostMapping("/guest/token") diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java new file mode 100644 index 00000000..65d2c20a --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/AuthResponse.java @@ -0,0 +1,4 @@ +package com.swyp8team2.auth.presentation.dto; + +public record AuthResponse(String accessToken, Long userId) { +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java index bb0cf374..dad3501e 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/TokenResponse.java @@ -1,4 +1,9 @@ package com.swyp8team2.auth.presentation.dto; -public record TokenResponse(String accessToken) { +import com.swyp8team2.auth.application.jwt.TokenPair; + +public record TokenResponse( + TokenPair tokenPair, + Long userId +) { } diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index 1ad636bf..a99ff0f1 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.comment.domain.Comment; import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.common.annotation.ShareUrlCryptoService; @@ -69,7 +70,8 @@ public void init() { } List adjectives = nicknameAdjectiveRepository.findAll(); User testUser = userRepository.save(User.create("nickname", "https://t1.kakaocdn.net/account_images/default_profile.jpeg")); - TokenPair tokenPair = jwtService.createToken(testUser.getId()); + TokenResponse tokenResponse = jwtService.createToken(testUser.getId()); + TokenPair tokenPair = tokenResponse.tokenPair(); System.out.println("accessToken = " + tokenPair.accessToken()); System.out.println("refreshToken = " + tokenPair.refreshToken()); List users = new ArrayList<>(); diff --git a/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java b/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java index 6932bd27..f2e4959e 100644 --- a/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java +++ b/src/test/java/com/swyp8team2/auth/application/AuthServiceTest.java @@ -7,6 +7,7 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -52,9 +53,10 @@ void oAuthSignIn() throws Exception { .willReturn(expectedTokenPair); //when - TokenPair tokenPair = authService.oauthSignIn("code", "https://dev.photopic.site"); + TokenResponse tokenResponse = authService.oauthSignIn("code", "https://dev.photopic.site"); //then + TokenPair tokenPair = tokenResponse.tokenPair(); SocialAccount socialAccount = socialAccountRepository.findBySocialIdAndProvider(oAuthUserInfo.socialId(), Provider.KAKAO).get(); User user = userRepository.findById(socialAccount.getId()).get(); assertAll( diff --git a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java index a21e7164..bd08fc2f 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java @@ -6,6 +6,7 @@ import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.domain.RefreshToken; import com.swyp8team2.auth.domain.RefreshTokenRepository; +import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.support.IntegrationTest; @@ -39,9 +40,10 @@ void createToken() throws Exception { .willReturn(expectedTokenPair); //when - TokenPair tokenPair = jwtService.createToken(givenUserId); + TokenResponse tokenResponse = jwtService.createToken(givenUserId); //then + TokenPair tokenPair = tokenResponse.tokenPair(); RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(givenUserId).get(); assertThat(tokenPair).isEqualTo(expectedTokenPair); assertThat(findRefreshToken.getToken()).isEqualTo(expectedTokenPair.refreshToken()); @@ -62,9 +64,10 @@ void reissue() throws Exception { refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); //when - TokenPair tokenPair = jwtService.reissue(givenRefreshToken); + TokenResponse tokenResponse = jwtService.reissue(givenRefreshToken); //then + TokenPair tokenPair = tokenResponse.tokenPair(); RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(givenUserId).get(); assertThat(tokenPair).isEqualTo(expectedTokenPair); assertThat(findRefreshToken.getToken()).isEqualTo(newRefreshToken); diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 4fb50574..a9a9aa4f 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -4,6 +4,7 @@ import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; +import com.swyp8team2.auth.presentation.dto.AuthResponse; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; @@ -41,9 +42,9 @@ class AuthControllerTest extends RestDocsTest { void kakaoOAuthSignIn() throws Exception { //given TokenPair expectedTokenPair = new TokenPair("accessToken", "refreshToken"); - TokenResponse response = new TokenResponse(expectedTokenPair.accessToken()); + AuthResponse response = new AuthResponse(expectedTokenPair.accessToken(), 1L); given(authService.oauthSignIn(anyString(), anyString())) - .willReturn(expectedTokenPair); + .willReturn(new TokenResponse(expectedTokenPair, 1L)); OAuthSignInRequest request = new OAuthSignInRequest("code", "https://dev.photopic.site"); //when then @@ -64,7 +65,8 @@ void kakaoOAuthSignIn() throws Exception { fieldWithPath("redirectUri").description("카카오 인증 redirect uri") ), responseFields( - fieldWithPath("accessToken").description("액세스 토큰") + fieldWithPath("accessToken").description("액세스 토큰"), + fieldWithPath("userId").description("유저 Id") ), responseCookies( cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") @@ -78,9 +80,10 @@ void kakaoOAuthSignIn() throws Exception { void reissue() throws Exception { //given String newRefreshToken = "newRefreshToken"; + TokenPair tokenPair = new TokenPair("accessToken", newRefreshToken); given(authService.reissue(anyString())) - .willReturn(new TokenPair("accessToken", newRefreshToken)); - TokenResponse response = new TokenResponse("accessToken"); + .willReturn(new TokenResponse(tokenPair, 1L)); + AuthResponse response = new AuthResponse(tokenPair.accessToken(), 1L); //when then mockMvc.perform(post("/auth/reissue") @@ -101,7 +104,8 @@ void reissue() throws Exception { cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("새 리프레시 토큰") ), responseFields( - fieldWithPath("accessToken").description("새 액세스 토큰") + fieldWithPath("accessToken").description("새 액세스 토큰"), + fieldWithPath("userId").description("유저 Id") ) )); } From 48c0c8873d09ae02e7f4888c2d49cbc8f754ad40 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 16:13:34 +0900 Subject: [PATCH 233/258] =?UTF-8?q?feat:=20=EB=B3=B8=EC=9D=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/presentation/UserController.java | 9 ++++++ .../user/presentation/UserControllerTest.java | 29 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp8team2/user/presentation/UserController.java b/src/main/java/com/swyp8team2/user/presentation/UserController.java index dc5ac165..332679ed 100644 --- a/src/main/java/com/swyp8team2/user/presentation/UserController.java +++ b/src/main/java/com/swyp8team2/user/presentation/UserController.java @@ -1,9 +1,11 @@ package com.swyp8team2.user.presentation; +import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.user.application.UserService; import com.swyp8team2.user.presentation.dto.UserInfoResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -20,4 +22,11 @@ public class UserController { public ResponseEntity findUserInfo(@PathVariable("userId") Long userId) { return ResponseEntity.ok(userService.findById(userId)); } + + @GetMapping("/me") + public ResponseEntity findMyInfo( + @AuthenticationPrincipal UserInfo userInfo + ) { + return ResponseEntity.ok(userService.findById(userInfo.userId())); + } } diff --git a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java index f11a7990..8490a66a 100644 --- a/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java +++ b/src/test/java/com/swyp8team2/user/presentation/UserControllerTest.java @@ -1,13 +1,16 @@ package com.swyp8team2.user.presentation; import com.swyp8team2.support.RestDocsTest; +import com.swyp8team2.support.WithMockUserInfo; import com.swyp8team2.user.presentation.dto.UserInfoResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.security.test.context.support.WithMockUser; import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -20,7 +23,7 @@ class UserControllerTest extends RestDocsTest { @Test - @WithMockUser + @WithMockUserInfo @DisplayName("유저 정보 조회") void findUserInfo() throws Exception { //given @@ -43,4 +46,28 @@ void findUserInfo() throws Exception { ) )); } + + @Test + @WithMockUserInfo + @DisplayName("본인 정보 조회") + void findMe() throws Exception { + //given + UserInfoResponse response = new UserInfoResponse(1L, "nickname", "https://image.com/profile-image"); + given(userService.findById(1L)) + .willReturn(response); + + //when then + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/me") + .header(HttpHeaders.AUTHORIZATION, "Bearer access-token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(response))) + .andDo(restDocs.document( + requestHeaders(authorizationHeader()), + responseFields( + fieldWithPath("id").description("유저 아이디").type(NUMBER), + fieldWithPath("nickname").description("닉네임").type(STRING), + fieldWithPath("profileUrl").description("프로필 이미지 URL").type(STRING) + ) + )); + } } From 18e5e0eeaa174ef8cab44fd046534bac4b125ed4 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 16:17:21 +0900 Subject: [PATCH 234/258] =?UTF-8?q?docs:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/users.adoc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/docs/asciidoc/users.adoc b/src/docs/asciidoc/users.adoc index 9faf6982..ae247c22 100644 --- a/src/docs/asciidoc/users.adoc +++ b/src/docs/asciidoc/users.adoc @@ -4,4 +4,9 @@ [[유저-정보-조회]] === `GET` 유저 정보 조회 -operation::user-controller-test/find-user-info[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] \ No newline at end of file +operation::user-controller-test/find-user-info[snippets='http-request,curl-request,path-parameters,http-response,response-fields'] + +[[본인-정보-조회]] +=== `GET` 본인 정보 조회 + +operation::user-controller-test/find-me[snippets='http-request,curl-request,request-headers,http-response,response-fields'] From c4c049986d0cbfa1b5ec46f8e00b99dbca7ff760 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Fri, 28 Feb 2025 16:22:27 +0900 Subject: [PATCH 235/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 72816997..68ef107f 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 72816997e97767c17a566b55a569c6faad7c5f25 +Subproject commit 68ef107fc0155483439e02560e94159e61241b0b From d389c0ec4a59786f5594cc2ac53deea5cd019bd7 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sat, 1 Mar 2025 03:05:33 +0900 Subject: [PATCH 236/258] =?UTF-8?q?chore:=20base62=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../swyp8team2/crypto/application/Base62.java | 34 -------------- .../post/domain/{State.java => Status.java} | 0 .../crypto/application/Base62Test.java | 45 ------------------- 4 files changed, 1 insertion(+), 80 deletions(-) delete mode 100644 src/main/java/com/swyp8team2/crypto/application/Base62.java rename src/main/java/com/swyp8team2/post/domain/{State.java => Status.java} (100%) delete mode 100644 src/test/java/com/swyp8team2/crypto/application/Base62Test.java diff --git a/build.gradle b/build.gradle index 23e36c35..42219f6a 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.6' // base64 - implementation 'commons-codec:commons-codec:1.15' + implementation 'io.seruco.encoding:base62:0.1.3' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' diff --git a/src/main/java/com/swyp8team2/crypto/application/Base62.java b/src/main/java/com/swyp8team2/crypto/application/Base62.java deleted file mode 100644 index e83ae4c0..00000000 --- a/src/main/java/com/swyp8team2/crypto/application/Base62.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.swyp8team2.crypto.application; - -import java.math.BigInteger; - -public class Base62 { - - private static final String BASE62_ALPHABET = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - private static final int BASE = 62; - - public static String encode(byte[] bytes) { - BigInteger value = new BigInteger(1, bytes); - StringBuilder encoded = new StringBuilder(); - - while (value.compareTo(BigInteger.ZERO) > 0) { - BigInteger[] divRem = value.divideAndRemainder(BigInteger.valueOf(BASE)); - value = divRem[0]; - encoded.insert(0, BASE62_ALPHABET.charAt(divRem[1].intValue())); - } - - return encoded.toString(); - } - - public static byte[] decode(String encoded) { - BigInteger value = BigInteger.ZERO; - - for (char c : encoded.toCharArray()) { - value = value.multiply(BigInteger.valueOf(BASE)) - .add(BigInteger.valueOf(BASE62_ALPHABET.indexOf(c))); - } - - return value.toByteArray(); - } -} diff --git a/src/main/java/com/swyp8team2/post/domain/State.java b/src/main/java/com/swyp8team2/post/domain/Status.java similarity index 100% rename from src/main/java/com/swyp8team2/post/domain/State.java rename to src/main/java/com/swyp8team2/post/domain/Status.java diff --git a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java b/src/test/java/com/swyp8team2/crypto/application/Base62Test.java deleted file mode 100644 index bd0c180e..00000000 --- a/src/test/java/com/swyp8team2/crypto/application/Base62Test.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.swyp8team2.crypto.application; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; - -import static org.assertj.core.api.Assertions.*; - -class Base62Test { - - @Test - @DisplayName("인코딩 디코딩") - void encodingAndDecoding() throws Exception { - //given - String plainText = "Hello, World!"; - byte[] bytes = plainText.getBytes(StandardCharsets.UTF_8); - - //when - String encode = Base62.encode(bytes); - byte[] decode = Base62.decode(encode); - - String decodeText = new String(decode, StandardCharsets.UTF_8); - - //then - assertThat(decodeText).isEqualTo(plainText); - } - - @Test - @DisplayName("인코딩 디코딩 - 다른 문자열") - void encodingAndDecoding_differentText() throws Exception { - //given - String plainText = "Hello, World!"; - byte[] bytes = plainText.getBytes(StandardCharsets.UTF_8); - - //when - String encode = Base62.encode(bytes); - byte[] decode = Base62.decode("different"); - - String decodeText = new String(decode, StandardCharsets.UTF_8); - - //then - assertThat(decodeText).isNotEqualTo(plainText); - } -} From 984ae512da3b619bf802799ca8d2f9f9be13a7fc Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sat, 1 Mar 2025 03:05:42 +0900 Subject: [PATCH 237/258] =?UTF-8?q?chore:=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 68ef107f..764ff3f7 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 68ef107fc0155483439e02560e94159e61241b0b +Subproject commit 764ff3f77cbac588a188c20df89dcfe9f5010c26 From 7d6d27327aca0404fca7a179c323bfb04fa3da4c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sat, 1 Mar 2025 03:06:54 +0900 Subject: [PATCH 238/258] =?UTF-8?q?refactor:=20guest-id=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20guest-token=EC=9C=BC=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 --- .../swyp8team2/auth/presentation/filter/GuestAuthFilter.java | 5 +---- .../com/swyp8team2/common/presentation/CustomHeader.java | 2 +- .../com/swyp8team2/crypto/application/CryptoService.java | 5 +++-- .../com/swyp8team2/vote/presentation/VoteController.java | 2 +- src/test/java/com/swyp8team2/support/RestDocsTest.java | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java index 91de4d43..f1286b03 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -12,13 +12,10 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; @@ -44,7 +41,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (!matcher.match("/posts/{postId}/votes/guest", request.getRequestURI())) { return; } - String token = request.getHeader(CustomHeader.GUEST_ID); + String token = request.getHeader(CustomHeader.GUEST_TOKEN); if (Objects.isNull(token)) { throw new BadRequestException(ErrorCode.INVALID_GUEST_HEADER); } diff --git a/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java index 903322ea..147385f9 100644 --- a/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java +++ b/src/main/java/com/swyp8team2/common/presentation/CustomHeader.java @@ -2,7 +2,7 @@ public abstract class CustomHeader { - public static final String GUEST_ID = "Guest-Id"; + public static final String GUEST_TOKEN = "Guest-Token"; public static final String AUTHORIZATION_REFRESH = "Authorization-Refresh"; public static class CustomCookie{ diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java index c3ecd7e0..9803147d 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.InternalServerException; +import io.seruco.encoding.base62.Base62; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.encrypt.AesBytesEncryptor; @@ -18,7 +19,7 @@ public class CryptoService { public String encrypt(String data) { try { byte[] encrypt = encryptor.encrypt(data.getBytes(StandardCharsets.UTF_8)); - return Base62.encode(encrypt); + return new String(Base62.createInstance().encode(encrypt), StandardCharsets.UTF_8); } catch (Exception e) { log.debug("encrypt error {}", e.getMessage()); throw new BadRequestException(ErrorCode.INVALID_TOKEN); @@ -27,7 +28,7 @@ public String encrypt(String data) { public String decrypt(String encryptedData) { try { - byte[] decryptBytes = Base62.decode(encryptedData); + byte[] decryptBytes = Base62.createInstance().decode(encryptedData.getBytes(StandardCharsets.UTF_8)); byte[] decrypt = encryptor.decrypt(decryptBytes); return new String(decrypt, StandardCharsets.UTF_8); } catch (Exception e) { diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index 51d0caea..6f97d68f 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -56,7 +56,7 @@ public ResponseEntity changeVote( @PatchMapping("/guest") public ResponseEntity changeGuestVote( @PathVariable("postId") Long postId, - @RequestHeader(CustomHeader.GUEST_ID) String guestId, + @RequestHeader(CustomHeader.GUEST_TOKEN) String guestId, @Valid @RequestBody ChangeVoteRequest request ) { return ResponseEntity.ok().build(); diff --git a/src/test/java/com/swyp8team2/support/RestDocsTest.java b/src/test/java/com/swyp8team2/support/RestDocsTest.java index f8bd6e39..8e72a1e0 100644 --- a/src/test/java/com/swyp8team2/support/RestDocsTest.java +++ b/src/test/java/com/swyp8team2/support/RestDocsTest.java @@ -34,7 +34,7 @@ protected static HeaderDescriptor authorizationHeader() { } protected static HeaderDescriptor guestHeader() { - return headerWithName(CustomHeader.GUEST_ID).description("게스트 Id (UUID 형식)"); + return headerWithName(CustomHeader.GUEST_TOKEN).description("게스트 토큰"); } protected static ParameterDescriptor[] cursorQueryParams() { From 3977fa9146f2a1ef2618b985cb39f553b87cb5e7 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sat, 1 Mar 2025 03:09:27 +0900 Subject: [PATCH 239/258] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostService.java | 5 ++-- .../java/com/swyp8team2/post/domain/Post.java | 14 +++++------ .../com/swyp8team2/post/domain/Status.java | 2 +- .../post/presentation/PostController.java | 2 +- .../presentation/dto/CreatePostResponse.java | 2 +- .../post/presentation/dto/PostResponse.java | 3 +++ .../crypto/application/CryptoServiceTest.java | 5 ++-- .../post/application/PostServiceTest.java | 23 +++++++++++++++---- .../com/swyp8team2/post/domain/PostTest.java | 10 ++++---- .../post/presentation/PostControllerTest.java | 14 ++++++++--- .../com/swyp8team2/support/WebUnitTest.java | 4 ++++ .../vote/application/VoteServiceTest.java | 6 ++--- .../vote/presentation/VoteControllerTest.java | 9 ++------ 13 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 1e97d25d..72ff11fa 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -12,6 +12,7 @@ import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.PostImageVoteStatusResponse; import com.swyp8team2.post.presentation.dto.SimplePostResponse; @@ -57,12 +58,12 @@ public PostService( } @Transactional - public Long create(Long userId, CreatePostRequest request) { + public CreatePostResponse create(Long userId, CreatePostRequest request) { List postImages = createPostImages(request); Post post = Post.create(userId, request.description(), postImages); Post save = postRepository.save(post); save.setShareUrl(shareUrlCryptoService.encrypt(String.valueOf(save.getId()))); - return save.getId(); + return new CreatePostResponse(save.getId(), save.getShareUrl()); } private List createPostImages(CreatePostRequest request) { diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 5d8d2bb2..6105b2b9 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -37,20 +37,20 @@ public class Post extends BaseEntity { private Long userId; @Enumerated(EnumType.STRING) - private State state; + private Status status; @OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.ALL) private List images = new ArrayList<>(); private String shareUrl; - public Post(Long id, Long userId, String description, State state, List images, String shareUrl) { + public Post(Long id, Long userId, String description, Status status, List images, String shareUrl) { validateDescription(description); validatePostImages(images); this.id = id; this.description = description; this.userId = userId; - this.state = state; + this.status = status; this.images = images; images.forEach(image -> image.setPost(this)); this.shareUrl = shareUrl; @@ -69,7 +69,7 @@ private void validateDescription(String description) { } public static Post create(Long userId, String description, List images) { - return new Post(null, userId, description, State.PROGRESS, images, null); + return new Post(null, userId, description, Status.PROGRESS, images, null); } public PostImage getBestPickedImage() { @@ -96,10 +96,10 @@ public void cancelVote(Long imageId) { public void close(Long userId) { validateOwner(userId); - if (state == State.CLOSED) { + if (status == Status.CLOSED) { throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); } - this.state = State.CLOSED; + this.status = Status.CLOSED; } public void validateOwner(Long userId) { @@ -109,7 +109,7 @@ public void validateOwner(Long userId) { } public void validateProgress() { - if (!this.state.equals(State.PROGRESS)) { + if (!this.status.equals(Status.PROGRESS)) { throw new BadRequestException(ErrorCode.POST_ALREADY_CLOSED); } } diff --git a/src/main/java/com/swyp8team2/post/domain/Status.java b/src/main/java/com/swyp8team2/post/domain/Status.java index cfdafbdd..bda22f18 100644 --- a/src/main/java/com/swyp8team2/post/domain/Status.java +++ b/src/main/java/com/swyp8team2/post/domain/Status.java @@ -1,5 +1,5 @@ package com.swyp8team2.post.domain; -public enum State { +public enum Status { PROGRESS, CLOSED } diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index c07264d7..52c1cb81 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -38,7 +38,7 @@ public ResponseEntity createPost( @AuthenticationPrincipal UserInfo userInfo ) { - return ResponseEntity.ok(new CreatePostResponse(postService.create(userInfo.userId(), request))); + return ResponseEntity.ok(postService.create(userInfo.userId(), request)); } @GetMapping("/{postId}") diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java index f6629c64..172d42b3 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/CreatePostResponse.java @@ -1,4 +1,4 @@ package com.swyp8team2.post.presentation.dto; -public record CreatePostResponse(Long postId) { +public record CreatePostResponse(Long postId, String shareUrl) { } diff --git a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java index 6f082117..b41421eb 100644 --- a/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java +++ b/src/main/java/com/swyp8team2/post/presentation/dto/PostResponse.java @@ -1,6 +1,7 @@ package com.swyp8team2.post.presentation.dto; import com.swyp8team2.post.domain.Post; +import com.swyp8team2.post.domain.Status; import com.swyp8team2.user.domain.User; import java.time.LocalDateTime; @@ -13,6 +14,7 @@ public record PostResponse( List images, String shareUrl, boolean isAuthor, + Status status, LocalDateTime createdAt ) { public static PostResponse of(Post post, User user, List images, boolean isAuthor) { @@ -23,6 +25,7 @@ public static PostResponse of(Post post, User user, List imag images, post.getShareUrl(), isAuthor, + post.getStatus(), post.getCreatedAt() ); } diff --git a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java index 801b4151..90e26e0d 100644 --- a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java +++ b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java @@ -17,17 +17,18 @@ class CryptoServiceTest { @BeforeEach void setUp() throws Exception { - cryptoService = new CryptoService(new AesBytesEncryptor("test", "123456")); + cryptoService = new CryptoService(new AesBytesEncryptor("asdfd", "1541235432")); } @Test @DisplayName("암호화 및 복호화") void encryptAndDecrypt() { // given - String plainText = "Hello, World!"; + String plainText = "15411"; // when String encryptedText = cryptoService.encrypt(plainText); + System.out.println("encryptedText = " + encryptedText); String decryptedText = cryptoService.decrypt(encryptedText); // then diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index dde9b629..4d06c2f3 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -1,14 +1,17 @@ package com.swyp8team2.post.application; +import com.swyp8team2.common.annotation.ShareUrlCryptoService; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; -import com.swyp8team2.post.domain.State; +import com.swyp8team2.post.domain.Status; import com.swyp8team2.post.presentation.dto.CreatePostRequest; +import com.swyp8team2.post.presentation.dto.CreatePostResponse; import com.swyp8team2.post.presentation.dto.PostResponse; import com.swyp8team2.post.presentation.dto.PostImageRequestDto; import com.swyp8team2.post.presentation.dto.PostImageResponse; @@ -21,6 +24,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import java.util.ArrayList; import java.util.List; @@ -31,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; class PostServiceTest extends IntegrationTest { @@ -52,6 +59,10 @@ class PostServiceTest extends IntegrationTest { @Autowired VoteService voteService; + @MockitoBean + @ShareUrlCryptoService + CryptoService shareUrlCryptoService; + @Test @DisplayName("게시글 작성") void create() throws Exception { @@ -61,16 +72,20 @@ void create() throws Exception { new PostImageRequestDto(1L), new PostImageRequestDto(2L) )); + String shareUrl = "shareUrl"; + given(shareUrlCryptoService.encrypt(any())) + .willReturn(shareUrl); //when - Long postId = postService.create(userId, request); + CreatePostResponse response = postService.create(userId, request); //then - Post post = postRepository.findById(postId).get(); + Post post = postRepository.findById(response.postId()).get(); List images = post.getImages(); assertAll( () -> assertThat(post.getDescription()).isEqualTo("description"), () -> assertThat(post.getUserId()).isEqualTo(userId), + () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), () -> assertThat(images.get(0).getName()).isEqualTo("뽀또 A"), @@ -253,7 +268,7 @@ void close() throws Exception { //then postRepository.findById(post.getId()).get(); - assertThat(post.getState()).isEqualTo(State.CLOSED); + assertThat(post.getStatus()).isEqualTo(Status.CLOSED); } @Test diff --git a/src/test/java/com/swyp8team2/post/domain/PostTest.java b/src/test/java/com/swyp8team2/post/domain/PostTest.java index c0908cf2..a41a2609 100644 --- a/src/test/java/com/swyp8team2/post/domain/PostTest.java +++ b/src/test/java/com/swyp8team2/post/domain/PostTest.java @@ -32,7 +32,7 @@ void create() throws Exception { assertAll( () -> assertThat(post.getUserId()).isEqualTo(userId), () -> assertThat(post.getDescription()).isEqualTo(description), - () -> assertThat(post.getState()).isEqualTo(State.PROGRESS), + () -> assertThat(post.getStatus()).isEqualTo(Status.PROGRESS), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), @@ -82,13 +82,13 @@ void close() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", State.PROGRESS, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, postImages, "shareUrl"); //when post.close(userId); //then - assertThat(post.getState()).isEqualTo(State.CLOSED); + assertThat(post.getStatus()).isEqualTo(Status.CLOSED); } @Test @@ -100,7 +100,7 @@ void close_alreadyClosed() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", State.CLOSED, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.CLOSED, postImages, "shareUrl"); //when then assertThatThrownBy(() -> post.close(userId)) @@ -117,7 +117,7 @@ void close_notPostAuthor() throws Exception { PostImage.create("뽀또A", 1L), PostImage.create("뽀또B", 2L) ); - Post post = new Post(null, userId, "description", State.PROGRESS, postImages, "shareUrl"); + Post post = new Post(null, userId, "description", Status.PROGRESS, postImages, "shareUrl"); //when then assertThatThrownBy(() -> post.close(2L)) diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 9d70892f..aff536f4 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -1,6 +1,7 @@ package com.swyp8team2.post.presentation; import com.swyp8team2.common.dto.CursorBasePaginatedResponse; +import com.swyp8team2.post.domain.Status; import com.swyp8team2.post.presentation.dto.AuthorDto; import com.swyp8team2.post.presentation.dto.CreatePostRequest; import com.swyp8team2.post.presentation.dto.CreatePostResponse; @@ -49,9 +50,9 @@ void createPost() throws Exception { "제목", List.of(new PostImageRequestDto(1L), new PostImageRequestDto(2L)) ); + CreatePostResponse response = new CreatePostResponse(1L, "shareUrl"); given(postService.create(any(), any())) - .willReturn(1L); - CreatePostResponse response = new CreatePostResponse(1L); + .willReturn(response); //when then mockMvc.perform(post("/posts") @@ -78,7 +79,10 @@ void createPost() throws Exception { responseFields( fieldWithPath("postId") .type(JsonFieldType.NUMBER) - .description("게시글 Id") + .description("게시글 Id"), + fieldWithPath("shareUrl") + .type(JsonFieldType.STRING) + .description("게시글 공유 url") ) )); } @@ -102,6 +106,7 @@ void findPost() throws Exception { ), "https://photopic.site/shareurl", true, + Status.PROGRESS, LocalDateTime.of(2025, 2, 13, 12, 0) ); given(postService.findById(any(), any())) @@ -130,6 +135,7 @@ void findPost() throws Exception { fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), + fieldWithPath("status").type(JsonFieldType.STRING).description("게시글 마감 여부 (PROGRESS, CLOSED)"), fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") ) )); @@ -154,6 +160,7 @@ void findPost_shareUrl() throws Exception { ), "https://photopic.site/shareurl", true, + Status.PROGRESS, LocalDateTime.of(2025, 2, 13, 12, 0) ); given(postService.findByShareUrl(any(), any())) @@ -182,6 +189,7 @@ void findPost_shareUrl() throws Exception { fieldWithPath("images[].voted").type(JsonFieldType.BOOLEAN).description("투표 여부"), fieldWithPath("shareUrl").type(JsonFieldType.STRING).description("게시글 공유 URL"), fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 생성 시간"), + fieldWithPath("status").type(JsonFieldType.STRING).description("게시글 마감 여부 (PROGRESS, CLOSED)"), fieldWithPath("isAuthor").type(JsonFieldType.BOOLEAN).description("게시글 작성자 여부") ) )); diff --git a/src/test/java/com/swyp8team2/support/WebUnitTest.java b/src/test/java/com/swyp8team2/support/WebUnitTest.java index ebd1b17c..aa1f1395 100644 --- a/src/test/java/com/swyp8team2/support/WebUnitTest.java +++ b/src/test/java/com/swyp8team2/support/WebUnitTest.java @@ -4,6 +4,7 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.presentation.RefreshTokenCookieGenerator; import com.swyp8team2.comment.application.CommentService; +import com.swyp8team2.common.exception.DiscordMessageSender; import com.swyp8team2.image.application.ImageService; import com.swyp8team2.post.application.PostService; import com.swyp8team2.user.application.UserService; @@ -44,4 +45,7 @@ public abstract class WebUnitTest { @MockitoBean protected UserService userService; + + @MockitoBean + protected DiscordMessageSender discordMessageSender; } diff --git a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java index adf54852..fff0993c 100644 --- a/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java +++ b/src/test/java/com/swyp8team2/vote/application/VoteServiceTest.java @@ -7,7 +7,7 @@ import com.swyp8team2.post.domain.Post; import com.swyp8team2.post.domain.PostImage; import com.swyp8team2.post.domain.PostRepository; -import com.swyp8team2.post.domain.State; +import com.swyp8team2.post.domain.Status; import com.swyp8team2.support.IntegrationTest; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; @@ -102,7 +102,7 @@ void vote_alreadyClosed() { null, user.getId(), "description", - State.CLOSED, + Status.CLOSED, List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) @@ -178,7 +178,7 @@ void guestVote_alreadyClosed() { null, user.getId(), "description", - State.CLOSED, + Status.CLOSED, List.of( PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId()) diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index 129d98d2..c1c9d502 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -11,13 +11,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.security.test.context.support.WithAnonymousUser; - -import java.util.UUID; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; @@ -69,7 +64,7 @@ void guestVote() throws Exception { mockMvc.perform(post("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_ID, "guestToken")) + .header(CustomHeader.GUEST_TOKEN, "guestToken")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()), @@ -122,7 +117,7 @@ void guestChangeVote() throws Exception { mockMvc.perform(patch("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_ID, "guestToken")) + .header(CustomHeader.GUEST_TOKEN, "guestToken")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()), From 7910fdba3f1f26b8df19e6fc6323c1e17b16d1fa Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sat, 1 Mar 2025 03:09:50 +0900 Subject: [PATCH 240/258] =?UTF-8?q?fix:=20webp=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(=EB=A6=AC=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=A0=9C=EC=99=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ .../annotation/ShareUrlCryptoService.java | 2 +- .../swyp8team2/common/dev/DataInitConfig.java | 2 +- .../common/exception/ErrorCode.java | 1 + .../image/application/R2Storage.java | 22 +++++++++++++++++-- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 42219f6a..f036af26 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,8 @@ dependencies { // image implementation 'software.amazon.awssdk:s3:2.30.18' implementation 'org.imgscalr:imgscalr-lib:4.2' + implementation 'com.sksamuel.scrimage:scrimage-core:4.3.0' + implementation 'com.sksamuel.scrimage:scrimage-webp:4.3.0' // gson implementation 'com.google.code.gson:gson:2.8.6' diff --git a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java index 2ea4c9f4..82be1bc7 100644 --- a/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java +++ b/src/main/java/com/swyp8team2/common/annotation/ShareUrlCryptoService.java @@ -9,7 +9,7 @@ @Qualifier(ShareUrlCryptoService.QUALIFIER) @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) public @interface ShareUrlCryptoService { String QUALIFIER = "shareUrlCryptoService"; diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java b/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java index 561a4c99..59bd22d8 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitConfig.java @@ -12,7 +12,7 @@ public class DataInitConfig { private final DataInitializer dataInitializer; - @PostConstruct +// @PostConstruct public void init() { dataInitializer.init(); } diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index e717db55..6ee4f4b3 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -21,6 +21,7 @@ public enum ErrorCode { NOT_POST_AUTHOR("게시글 작성자가 아님"), POST_ALREADY_CLOSED("이미 마감된 게시글"), INVALID_GUEST_HEADER("잘못된 게스트 토큰 헤더"), + FILE_NAME_TOO_LONG("파일 이름이 너무 김"), //401 EXPIRED_TOKEN("토큰 만료"), diff --git a/src/main/java/com/swyp8team2/image/application/R2Storage.java b/src/main/java/com/swyp8team2/image/application/R2Storage.java index f735799e..8222f2b0 100644 --- a/src/main/java/com/swyp8team2/image/application/R2Storage.java +++ b/src/main/java/com/swyp8team2/image/application/R2Storage.java @@ -1,5 +1,7 @@ package com.swyp8team2.image.application; +import com.sksamuel.scrimage.ImmutableImage; +import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; import com.swyp8team2.common.exception.ServiceUnavailableException; import com.swyp8team2.common.util.DateTime; @@ -13,6 +15,7 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -22,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; @Component @Slf4j @@ -52,6 +56,9 @@ public List uploadImageFile(MultipartFile... files) { for (int i = 0; i < files.length; i++) { MultipartFile file = files[i]; String originFileName = file.getOriginalFilename(); + if (originFileName.length() > 100) { + throw new BadRequestException(ErrorCode.FILE_NAME_TOO_LONG); + } String realFileName = getRealFileName(originFileName, filePath, i); File tempFile = File.createTempFile("upload_", "_" + originFileName); file.transferTo(tempFile); @@ -81,7 +88,18 @@ public List uploadImageFile(MultipartFile... files) { private String resizeImage(File file, String realFileName, int targetHeight) { try { - BufferedImage srcImage = ImageIO.read(file); + String ext = Optional.of(realFileName) + .filter(name -> name.contains(".")) + .map(name -> name.substring(name.lastIndexOf('.') + 1)) + .orElseThrow(() -> new BadRequestException(ErrorCode.MISSING_FILE_EXTENSION)) + .toLowerCase(); + + BufferedImage srcImage; + if ("webp".equals(ext)) { + srcImage = ImmutableImage.loader().fromFile(file).awt(); + } else { + srcImage = ImageIO.read(file); + } BufferedImage resizedImage = highQualityResize(srcImage, targetHeight); int splitIndex = realFileName.lastIndexOf("/") + 1; @@ -120,7 +138,7 @@ private File s3PutObject(File file, String realFileName, String imageType) { .key(realFileName) .build(); - s3Client.putObject(objectRequest, RequestBody.fromFile(file)); + PutObjectResponse putObjectResponse = s3Client.putObject(objectRequest, RequestBody.fromFile(file)); return file; } From 0c2e4083ae854ad84a3f08680759792b0fb3c464 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 1 Mar 2025 22:42:24 +0900 Subject: [PATCH 241/258] =?UTF-8?q?chore:=20=EC=B5=9C=EB=8C=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20webp=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=EC=9E=90=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- src/main/resources/static/index.html | 52 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/static/index.html diff --git a/server-config b/server-config index 764ff3f7..20392229 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 764ff3f77cbac588a188c20df89dcfe9f5010c26 +Subproject commit 20392229dd465dc63d8dd8a77ad5bf45b3884dd5 diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 00000000..ab7a06bc --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,52 @@ + + + + + 멀티 파일 업로드 테스트 + + +

여러 파일 업로드 테스트

+ + + + + + + From bd899bac141b3dbe918a17d536a71521f3c5fcd1 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 1 Mar 2025 22:52:04 +0900 Subject: [PATCH 242/258] =?UTF-8?q?chore:=20=EC=B5=9C=EB=8C=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=AC=B8=EB=B2=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index 20392229..b6d9f9e1 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit 20392229dd465dc63d8dd8a77ad5bf45b3884dd5 +Subproject commit b6d9f9e189eaa4093df0f6089fc30d23fd5c81ac From cca98f1e4a56d8d1afcde3d6afa7d86a383ff691 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sat, 1 Mar 2025 23:17:26 +0900 Subject: [PATCH 243/258] =?UTF-8?q?test:=20=EC=B5=9C=EB=8C=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 752cb96cba945aafc9dc7895cdb46389ddf82896 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sun, 2 Mar 2025 00:01:44 +0900 Subject: [PATCH 244/258] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=88=9C=EC=84=9C=20=EC=98=A4=EB=A6=84=EC=B0=A8?= =?UTF-8?q?=EC=88=9C=EC=9C=BC=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/com/swyp8team2/comment/domain/CommentRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java index 54e0f7e6..0dfbfbb9 100644 --- a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -14,8 +14,8 @@ public interface CommentRepository extends JpaRepository { SELECT c FROM Comment c WHERE c.postId = :postId - AND (:cursor is null or c.id < :cursor) - ORDER BY c.createdAt DESC + AND (:cursor is null or c.id > :cursor) + ORDER BY c.createdAt ASC """) Slice findByPostId( @Param("postId") Long postId, From 7bf2d416709b39e8f342aa0f7f7a41cc4cfd1b01 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sun, 2 Mar 2025 00:06:40 +0900 Subject: [PATCH 245/258] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/comment/domain/CommentRepositoryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index d8a44e7d..aaa6f9ca 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -35,6 +35,6 @@ void select_CommentUser() { Slice result2 = commentRepository.findByPostId(1L, 1L, PageRequest.of(0, 10)); // then2 - assertThat(result2.getContent()).hasSize(0); + assertThat(result2.getContent()).hasSize(2); } } \ No newline at end of file From 8681257912b1c01456b2050b9ef2f7f57f34aa1f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 14:29:26 +0900 Subject: [PATCH 246/258] =?UTF-8?q?chore:=20prod=20cd=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-prod.yml | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/cd-prod.yml diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml new file mode 100644 index 00000000..c5c5e509 --- /dev/null +++ b/.github/workflows/cd-prod.yml @@ -0,0 +1,72 @@ +name: cd prod + +on: + pull_request: + branches: [ "main" ] + types: [closed] + +jobs: + deploy: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + token: ${{ secrets.SUBMODULE_TOKEN }} + submodules: true + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Cache Gradle + id: cache-gradle + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Create directory resources + run: mkdir -p ./src/test/resources + + - name: Copy application.yml + run: | + cp ./server-config/*.yml ./src/main/resources/ + cp ./server-config/application-test.yml ./src/test/resources/application.yml + + - name: Build with Gradle + run: ./gradlew bootJar + + - name: Copy jar file + run: mv ./build/libs/*SNAPSHOT.jar ./photopic-prod.jar + + - name: (SCP) transfer build file + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.AWS_EC2_URL_PROD }} + username: ${{ secrets.AWS_EC2_USER }} + key: ${{ secrets.AWS_EC2_KEY }} + source: photopic-prod.jar + target: /home/${{ secrets.AWS_EC2_USER }} + + - name: (SSH) connect EC2 + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.AWS_EC2_URL_PROD }} + username: ${{ secrets.AWS_EC2_USER }} + key: ${{ secrets.AWS_EC2_KEY }} + script_stop: true + script: | + sudo fuser -k -n tcp 8080 || true + nohup java -jar -Dspring.profiles.active=prod photopic-prod.jar > ./output.log 2>&1 & From b3aad534f87fe9e500aaef5fe7e4da404dca0309 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 14:29:56 +0900 Subject: [PATCH 247/258] =?UTF-8?q?chore:=20prod=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-config b/server-config index b6d9f9e1..e30e07df 160000 --- a/server-config +++ b/server-config @@ -1 +1 @@ -Subproject commit b6d9f9e189eaa4093df0f6089fc30d23fd5c81ac +Subproject commit e30e07dfdf425a36d06189d1699d5edfb887a383 From b9f5ca52d9b424a913be302fc02bece456022dba Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 14:47:29 +0900 Subject: [PATCH 248/258] =?UTF-8?q?chore:=20gradle=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ------ 1 file changed, 6 deletions(-) diff --git a/build.gradle b/build.gradle index 6f0878a8..f036af26 100644 --- a/build.gradle +++ b/build.gradle @@ -41,21 +41,15 @@ dependencies { // image implementation 'software.amazon.awssdk:s3:2.30.18' implementation 'org.imgscalr:imgscalr-lib:4.2' -<<<<<<< HEAD implementation 'com.sksamuel.scrimage:scrimage-core:4.3.0' implementation 'com.sksamuel.scrimage:scrimage-webp:4.3.0' -======= ->>>>>>> origin/main // gson implementation 'com.google.code.gson:gson:2.8.6' -<<<<<<< HEAD // base64 implementation 'io.seruco.encoding:base62:0.1.3' -======= ->>>>>>> origin/main compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' From 733796b3f547d32a02ebec3f46389adf998e6b87 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 15:33:59 +0900 Subject: [PATCH 249/258] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=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 --- .../swyp8team2/auth/application/AuthService.java | 5 +++++ .../auth/application/jwt/JwtService.java | 6 ++++++ .../auth/domain/RefreshTokenRepository.java | 2 ++ .../auth/presentation/AuthController.java | 13 +++++++++++++ .../RefreshTokenCookieGenerator.java | 16 ++++++++++++++++ 5 files changed, 42 insertions(+) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index d19a6203..17afa7d2 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -61,4 +61,9 @@ public String createGuestToken() { Long guestId = userService.createGuest(); return cryptoService.encrypt(String.valueOf(guestId)); } + + @Transactional + public void signOut(String refreshToken) { + jwtService.signOut(refreshToken); + } } diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java index b7963ca8..14a4e5df 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java @@ -44,4 +44,10 @@ public TokenResponse reissue(String refreshToken) { claim.id(), tokenPair.accessToken(), tokenPair.refreshToken()); return new TokenResponse(tokenPair, claim.idAsLong()); } + + @Transactional + public void signOut(String refreshToken) { + JwtClaim claim = jwtProvider.parseToken(refreshToken); + refreshTokenRepository.deleteByUserId(claim.idAsLong()); + } } diff --git a/src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java b/src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java index b406ecfa..625a1c74 100644 --- a/src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java +++ b/src/main/java/com/swyp8team2/auth/domain/RefreshTokenRepository.java @@ -6,4 +6,6 @@ public interface RefreshTokenRepository extends JpaRepository { Optional findByUserId(Long userId); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index 249f2a7d..bb0a0f70 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -63,4 +63,17 @@ public ResponseEntity guestToken() { String guestToken = authService.createGuestToken(); return ResponseEntity.ok(new GuestTokenResponse(guestToken)); } + + @PostMapping("/sign-out") + public ResponseEntity signOut( + @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, + HttpServletResponse response + ) { + if (Objects.isNull(refreshToken)) { + throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); + } + refreshTokenCookieGenerator.removeCookie(response); + authService.signOut(refreshToken); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java index 8e0c50f8..825df15d 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java +++ b/src/main/java/com/swyp8team2/auth/presentation/RefreshTokenCookieGenerator.java @@ -2,6 +2,7 @@ import com.swyp8team2.common.presentation.CustomHeader; import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -24,4 +25,19 @@ public Cookie createCookie(String refreshToken) { cookie.setMaxAge(60 * 60 * 24 * 14); return cookie; } + + public void removeCookie(HttpServletResponse response) { + Cookie cookie = new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, null); + cookie.setHttpOnly(true); + cookie.setSecure(true); + if ("local".equals(activeProfile)) { + cookie.setSecure(false); + } else { + cookie.setSecure(true); + cookie.setAttribute("SameSite", "None"); + } + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } } From 2dd28b7707c4987374dd33f94e4c974ad3c1e88b Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 15:34:11 +0900 Subject: [PATCH 250/258] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- .../auth/application/JwtServiceTest.java | 18 +++++++++++++ .../auth/presentation/AuthControllerTest.java | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java index bd08fc2f..c39f8035 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; class JwtServiceTest extends IntegrationTest { @@ -103,4 +104,21 @@ void reissue_refreshTokenMismatched() throws Exception { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.REFRESH_TOKEN_MISMATCHED.getMessage()); } + + @Test + @DisplayName("로그아웃하면 refresh token을 db에서 삭제해야 함") + void signOut() throws Exception { + //given + long givenUserId = 1L; + String givenRefreshToken = "refreshToken"; + given(jwtProvider.parseToken(eq(givenRefreshToken))) + .willReturn(new JwtClaim(givenUserId)); + refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); + + //when + jwtService.signOut(givenRefreshToken); + + //then + assertThat(refreshTokenRepository.findByUserId(givenUserId)).isEmpty(); + } } diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index a9a9aa4f..6b4c6a51 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -19,6 +19,8 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; @@ -170,4 +172,28 @@ void guestToken() throws Exception { ) )); } + + @Test + @DisplayName("로그아웃") + void signOut() throws Exception { + //given + + //when then + mockMvc.perform(post("/auth/sign-out") + .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken"))) + .andExpect(status().isOk()) + .andExpect(cookie().httpOnly(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().path(CustomHeader.CustomCookie.REFRESH_TOKEN, "/")) + .andExpect(cookie().secure(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) + .andExpect(cookie().attribute(CustomHeader.CustomCookie.REFRESH_TOKEN, "SameSite", "None")) + .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 0)) + .andDo(restDocs.document( + requestCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") + ), + responseCookies( + cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") + ) + )); + } } From 86c7eca0db8afea215e3d8dc3cdfeaa4f60b75d2 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 16:13:51 +0900 Subject: [PATCH 251/258] =?UTF-8?q?feat:=20access=20token=20=EC=9E=88?= =?UTF-8?q?=EC=96=B4=EC=95=BC=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/swyp8team2/auth/application/AuthService.java | 5 ++--- .../swyp8team2/auth/application/jwt/JwtService.java | 11 ++++++++--- .../swyp8team2/auth/presentation/AuthController.java | 7 +++++-- .../com/swyp8team2/common/config/SecurityConfig.java | 3 +-- .../auth/presentation/AuthControllerTest.java | 8 +++++++- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 17afa7d2..24b8c3fc 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -1,7 +1,6 @@ package com.swyp8team2.auth.application; import com.swyp8team2.auth.application.jwt.JwtService; -import com.swyp8team2.auth.application.jwt.TokenPair; import com.swyp8team2.auth.application.oauth.OAuthService; import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; import com.swyp8team2.auth.domain.Provider; @@ -63,7 +62,7 @@ public String createGuestToken() { } @Transactional - public void signOut(String refreshToken) { - jwtService.signOut(refreshToken); + public void signOut(Long userId, String refreshToken) { + jwtService.signOut(userId, refreshToken); } } diff --git a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java index 14a4e5df..75228fbd 100644 --- a/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java +++ b/src/main/java/com/swyp8team2/auth/application/jwt/JwtService.java @@ -46,8 +46,13 @@ public TokenResponse reissue(String refreshToken) { } @Transactional - public void signOut(String refreshToken) { - JwtClaim claim = jwtProvider.parseToken(refreshToken); - refreshTokenRepository.deleteByUserId(claim.idAsLong()); + public void signOut(Long userId, String refreshToken) { + RefreshToken token = refreshTokenRepository.findByUserId(userId) + .orElseThrow(() -> new BadRequestException(ErrorCode.REFRESH_TOKEN_NOT_FOUND)); + + if (!token.getToken().equals(refreshToken)) { + throw new BadRequestException(ErrorCode.REFRESH_TOKEN_MISMATCHED); + } + refreshTokenRepository.delete(token); } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index bb0a0f70..f0c6eeb4 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -3,6 +3,7 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; @@ -15,6 +16,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -67,13 +69,14 @@ public ResponseEntity guestToken() { @PostMapping("/sign-out") public ResponseEntity signOut( @CookieValue(name = CustomHeader.CustomCookie.REFRESH_TOKEN, required = false) String refreshToken, - HttpServletResponse response + HttpServletResponse response, + @AuthenticationPrincipal UserInfo userInfo ) { if (Objects.isNull(refreshToken)) { throw new BadRequestException(ErrorCode.INVALID_REFRESH_TOKEN_HEADER); } refreshTokenCookieGenerator.removeCookie(response); - authService.signOut(refreshToken); + authService.signOut(userInfo.userId(), refreshToken); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 0994c3fe..f283fa95 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -113,8 +113,7 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros mvc.pattern(HttpMethod.GET, "/posts/shareUrl/{shareUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), -// mvc.pattern("/posts/{postId}/votes/guest/**"), - mvc.pattern("/auth/oauth2/**") + mvc.pattern("/auth/oauth2/**"), }; } diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index 6b4c6a51..60390dfb 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -11,10 +11,12 @@ import com.swyp8team2.common.exception.ErrorResponse; import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.RestDocsTest; +import com.swyp8team2.support.WithMockUserInfo; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; @@ -26,6 +28,7 @@ import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; @@ -174,13 +177,15 @@ void guestToken() throws Exception { } @Test + @WithMockUserInfo @DisplayName("로그아웃") void signOut() throws Exception { //given //when then mockMvc.perform(post("/auth/sign-out") - .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken"))) + .cookie(new Cookie(CustomHeader.CustomCookie.REFRESH_TOKEN, "refreshToken")) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken")) .andExpect(status().isOk()) .andExpect(cookie().httpOnly(CustomHeader.CustomCookie.REFRESH_TOKEN, true)) .andExpect(cookie().path(CustomHeader.CustomCookie.REFRESH_TOKEN, "/")) @@ -188,6 +193,7 @@ void signOut() throws Exception { .andExpect(cookie().attribute(CustomHeader.CustomCookie.REFRESH_TOKEN, "SameSite", "None")) .andExpect(cookie().maxAge(CustomHeader.CustomCookie.REFRESH_TOKEN, 0)) .andDo(restDocs.document( + requestHeaders(authorizationHeader()), requestCookies( cookieWithName(CustomHeader.CustomCookie.REFRESH_TOKEN).description("리프레시 토큰") ), From 6a42c82a8d7eee41ac8aa3957b647a5bf256029c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 16:14:48 +0900 Subject: [PATCH 252/258] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=B9=88=20=EA=B0=92=20=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EC=98=AC=20=EB=95=8C=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=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 --- .../presentation/filter/GuestAuthFilter.java | 3 +++ .../crypto/application/CryptoService.java | 4 ++++ .../auth/application/JwtServiceTest.java | 18 +++++++++++++++--- .../crypto/application/CryptoServiceTest.java | 12 ++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java index f1286b03..a7f6a006 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -50,6 +50,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContextHolder.getContext().setAuthentication(authentication); } catch (ApplicationException e) { request.setAttribute(EXCEPTION_KEY, e); + } catch (Exception e) { + log.debug("GuestAuthFilter error", e); + request.setAttribute(EXCEPTION_KEY, new BadRequestException(ErrorCode.INVALID_TOKEN)); } finally { doFilter(request, response, filterChain); } diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java index 9803147d..4ff34182 100644 --- a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.encrypt.AesBytesEncryptor; +import org.springframework.util.StringUtils; import java.nio.charset.StandardCharsets; @@ -28,6 +29,9 @@ public String encrypt(String data) { public String decrypt(String encryptedData) { try { + if (!StringUtils.hasText(encryptedData)) { + throw new InternalServerException(ErrorCode.INVALID_TOKEN); + } byte[] decryptBytes = Base62.createInstance().decode(encryptedData.getBytes(StandardCharsets.UTF_8)); byte[] decrypt = encryptor.decrypt(decryptBytes); return new String(decrypt, StandardCharsets.UTF_8); diff --git a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java index c39f8035..40cefbdb 100644 --- a/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java +++ b/src/test/java/com/swyp8team2/auth/application/JwtServiceTest.java @@ -111,14 +111,26 @@ void signOut() throws Exception { //given long givenUserId = 1L; String givenRefreshToken = "refreshToken"; - given(jwtProvider.parseToken(eq(givenRefreshToken))) - .willReturn(new JwtClaim(givenUserId)); refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); //when - jwtService.signOut(givenRefreshToken); + jwtService.signOut(givenUserId, givenRefreshToken); //then assertThat(refreshTokenRepository.findByUserId(givenUserId)).isEmpty(); } + + @Test + @DisplayName("로그아웃 - 유저의 refresh token이 아닌 경우") + void signOut_invalidRefreshToken() throws Exception { + //given + long givenUserId = 1L; + String givenRefreshToken = "refreshToken"; + refreshTokenRepository.save(new RefreshToken(givenUserId, givenRefreshToken)); + + //when then + assertThatThrownBy(() -> jwtService.signOut(givenUserId, "differentToken")) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.REFRESH_TOKEN_MISMATCHED.getMessage()); + } } diff --git a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java index 90e26e0d..89b9fd1e 100644 --- a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java +++ b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java @@ -60,4 +60,16 @@ void decrypt_invalidToken() { .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } + + @Test + @DisplayName("복호화 - empty string") + void decrypt_emptyString() { + // given + String invalid = ""; + + // when then + assertThatThrownBy(() -> cryptoService.decrypt(invalid)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } } From 58bab13008382fae3b8caddde72e9ce5dbfbc90d Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 16:14:58 +0900 Subject: [PATCH 253/258] =?UTF-8?q?docs:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20docs=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 683a2657..bf0d05db 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -15,3 +15,8 @@ operation::auth-controller-test/reissue[snippets='http-request,curl-request,requ === `POST` 게스트 토큰 발급 operation::auth-controller-test/guest-token[snippets='http-request,curl-request,http-response,response-fields'] + +[[로그아웃]] +=== `POST` 로그아웃 + +operation::auth-controller-test/sign-out[snippets='http-request,curl-request,request-cookies,request-headers,http-response,response-cookies'] From 1a3e9074ddeb836960e0a940002749f0ce28b053 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 16:22:58 +0900 Subject: [PATCH 254/258] =?UTF-8?q?refactor:=20=EB=82=B4=EA=B0=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1/=ED=88=AC=ED=91=9C=ED=95=9C=20->=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EA=B0=80=20=EC=9E=91=EC=84=B1/=ED=88=AC=ED=91=9C?= =?UTF-8?q?=ED=95=9C=20api=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 --- .../swyp8team2/post/application/PostService.java | 3 +-- .../post/presentation/PostController.java | 16 ++++++++-------- .../post/application/PostServiceTest.java | 15 +++++++-------- .../post/presentation/PostControllerTest.java | 12 +++++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index 72ff11fa..f48b9c13 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -21,7 +21,6 @@ import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; -import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -113,7 +112,7 @@ private Boolean getVoted(PostImage image, Long userId, Long postId) { .orElse(false); } - public CursorBasePaginatedResponse findMyPosts(Long userId, Long cursor, int size) { + public CursorBasePaginatedResponse findUserPosts(Long userId, Long cursor, int size) { Slice postSlice = postRepository.findByUserId(userId, cursor, PageRequest.ofSize(size)); return CursorBasePaginatedResponse.of(postSlice.map(this::createSimplePostResponse) ); diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 52c1cb81..625897dd 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -88,21 +88,21 @@ public ResponseEntity deletePost( return ResponseEntity.ok().build(); } - @GetMapping("/user/me") + @GetMapping("/users/{userId}") public ResponseEntity> findMyPosts( + @PathVariable("userId") Long userId, @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, - @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, - @AuthenticationPrincipal UserInfo userInfo + @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size ) { - return ResponseEntity.ok(postService.findMyPosts(userInfo.userId(), cursor, size)); + return ResponseEntity.ok(postService.findUserPosts(userId, cursor, size)); } - @GetMapping("/user/voted") + @GetMapping("/users/{userId}/voted") public ResponseEntity> findVotedPosts( + @PathVariable("userId") Long userId, @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, - @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, - @AuthenticationPrincipal UserInfo userInfo + @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size ) { - return ResponseEntity.ok(postService.findVotedPosts(userInfo.userId(), cursor, size)); + return ResponseEntity.ok(postService.findVotedPosts(userId, cursor, size)); } } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 4d06c2f3..af97d0a1 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import java.util.ArrayList; import java.util.List; @@ -156,15 +155,15 @@ void findById() throws Exception { } @Test - @DisplayName("내가 작성한 게시글 조회 - 커서 null인 경우") - void findMyPosts() throws Exception { + @DisplayName("유저가 작성한 게시글 조회 - 커서 null인 경우") + void findUserPosts() throws Exception { //given User user = userRepository.save(createUser(1)); List posts = createPosts(user); int size = 10; //when - var response = postService.findMyPosts(user.getId(), null, size); + var response = postService.findUserPosts(user.getId(), null, size); //then assertAll( @@ -175,15 +174,15 @@ void findMyPosts() throws Exception { } @Test - @DisplayName("내가 작성한 게시글 조회 - 커서 있는 경우") - void findMyPosts2() throws Exception { + @DisplayName("유저가 작성한 게시글 조회 - 커서 있는 경우") + void findUserPosts2() throws Exception { //given User user = userRepository.save(createUser(1)); List posts = createPosts(user); int size = 10; //when - var response = postService.findMyPosts(user.getId(), posts.get(3).getId(), size); + var response = postService.findUserPosts(user.getId(), posts.get(3).getId(), size); //then assertAll( @@ -204,7 +203,7 @@ private List createPosts(User user) { } @Test - @DisplayName("내가 투표한 게시글 조회 - 커서 null인 경우") + @DisplayName("유저가 투표한 게시글 조회 - 커서 null인 경우") void findVotedPosts() throws Exception { //given User user = userRepository.save(createUser(1)); diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index aff536f4..befad202 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -248,7 +248,7 @@ void deletePost() throws Exception { @Test @WithMockUserInfo - @DisplayName("내가 작성한 게시글 조회") + @DisplayName("유저가 작성한 게시글 조회") void findMyPost() throws Exception { //given var response = new CursorBasePaginatedResponse<>( @@ -263,15 +263,16 @@ void findMyPost() throws Exception { ) ) ); - given(postService.findMyPosts(1L, null, 10)) + given(postService.findUserPosts(1L, null, 10)) .willReturn(response); //when then - mockMvc.perform(get("/posts/user/me") + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/users/{userId}", 1) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( + pathParameters(parameterWithName("userId").description("유저 Id")), requestHeaders(authorizationHeader()), queryParameters(cursorQueryParams()), responseFields( @@ -303,7 +304,7 @@ void findMyPost() throws Exception { @Test @WithMockUserInfo - @DisplayName("내가 참여한 게시글 조회") + @DisplayName("유저가 참여한 게시글 조회") void findVotedPost() throws Exception { //given var response = new CursorBasePaginatedResponse<>( @@ -322,11 +323,12 @@ void findVotedPost() throws Exception { .willReturn(response); //when then - mockMvc.perform(get("/posts/user/voted") + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/users/{userId}/voted", 1) .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) .andDo(restDocs.document( + pathParameters(parameterWithName("userId").description("유저 Id")), requestHeaders(authorizationHeader()), queryParameters(cursorQueryParams()), responseFields( From 8372a6f50b4f2d3121591e256eaa983ba670d0d2 Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 16:35:21 +0900 Subject: [PATCH 255/258] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=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/com/swyp8team2/common/dev/DataInitializer.java | 2 +- .../swyp8team2/post/application/PostImageNameGenerator.java | 2 +- src/main/java/com/swyp8team2/post/domain/Post.java | 2 +- .../post/application/PostImageNameGeneratorTest.java | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index a99ff0f1..2328c4ef 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -83,7 +83,7 @@ public void init() { for (int j = 0; j < 30; j += 2) { ImageFile imageFile1 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); - Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또 A", imageFile1.getId()), PostImage.create("뽀또 B", imageFile2.getId())))); + Post post = postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())))); post.setShareUrl(shaereUrlCryptoService.encrypt(String.valueOf(post.getId()))); posts.add(post); } diff --git a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java index b764e035..ab1edd99 100644 --- a/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java +++ b/src/main/java/com/swyp8team2/post/application/PostImageNameGenerator.java @@ -16,6 +16,6 @@ public String generate() { if (index >= alphabets.length) { throw new InternalServerException(ErrorCode.POST_IMAGE_NAME_GENERATOR_INDEX_OUT_OF_BOUND); } - return "뽀또 " + alphabets[index++]; + return "뽀또" + alphabets[index++]; } } diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 6105b2b9..6d6597e9 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -57,7 +57,7 @@ public Post(Long id, Long userId, String description, Status status, List images) { - if (images.size() < 2) { + if (images.size() < 2 && images.size() > 9) { throw new BadRequestException(ErrorCode.INVALID_POST_IMAGE_COUNT); } } diff --git a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java index 8f763b94..626bb21f 100644 --- a/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostImageNameGeneratorTest.java @@ -26,7 +26,7 @@ void generate() throws Exception { String generate2 = postImageNameGenerator.generate(); //then - assertThat(generate1).isEqualTo("뽀또 A"); - assertThat(generate2).isEqualTo("뽀또 B"); + assertThat(generate1).isEqualTo("뽀또A"); + assertThat(generate2).isEqualTo("뽀또B"); } } From 5a9569f843145413395c48674685456172ce994c Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 16:40:17 +0900 Subject: [PATCH 256/258] =?UTF-8?q?docs:=20api=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/posts.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/docs/asciidoc/posts.adoc b/src/docs/asciidoc/posts.adoc index 54301e7e..7087793d 100644 --- a/src/docs/asciidoc/posts.adoc +++ b/src/docs/asciidoc/posts.adoc @@ -24,13 +24,13 @@ operation::post-controller-test/find-post_share-url[snippets='http-request,curl- operation::post-controller-test/find-vote-status[snippets='http-request,curl-request,request-headers,path-parameters,http-response,response-fields'] -[[내가-작성한-게시글-조회]] -=== `GET` 내가 작성한 게시글 조회 +[[유저가-작성한-게시글-조회]] +=== `GET` 유저가 작성한 게시글 조회 operation::post-controller-test/find-my-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] -[[내가-참여한-게시글-조회]] -=== `GET` 내가 참여한 게시글 조회 +[[유저가-참여한-게시글-조회]] +=== `GET` 유저가 참여한 게시글 조회 operation::post-controller-test/find-voted-post[snippets='http-request,curl-request,query-parameters,request-headers,http-response,response-fields'] From 71df662f95495188d272a33f0f15191d3a724558 Mon Sep 17 00:00:00 2001 From: Nakji Date: Sun, 2 Mar 2025 16:40:38 +0900 Subject: [PATCH 257/258] =?UTF-8?q?chore:=20=ED=9E=99=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EB=A6=AC=20=ED=81=AC=EA=B8=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 29812158..e648bde4 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -69,4 +69,4 @@ jobs: script_stop: true script: | sudo fuser -k -n tcp 8080 || true - nohup java -jar -Dspring.profiles.active=dev photopic-dev.jar > ./output.log 2>&1 & + nohup java -Xms256m -Xmx742m -jar -Dspring.profiles.active=dev photopic-dev.jar > ./output.log 2>&1 & From 3e7b8298830e2515cf99a7a433ee47e70832d19f Mon Sep 17 00:00:00 2001 From: wlsh44 Date: Sun, 2 Mar 2025 16:42:32 +0900 Subject: [PATCH 258/258] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=BA=A0=EC=A7=80=EB=8A=94=20=EB=B6=80=EB=B6=84=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 --- src/main/java/com/swyp8team2/post/domain/Post.java | 2 +- .../java/com/swyp8team2/post/application/PostServiceTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/swyp8team2/post/domain/Post.java b/src/main/java/com/swyp8team2/post/domain/Post.java index 6d6597e9..79ff5700 100644 --- a/src/main/java/com/swyp8team2/post/domain/Post.java +++ b/src/main/java/com/swyp8team2/post/domain/Post.java @@ -57,7 +57,7 @@ public Post(Long id, Long userId, String description, Status status, List images) { - if (images.size() < 2 && images.size() > 9) { + if (images.size() < 2 || images.size() > 9) { throw new BadRequestException(ErrorCode.INVALID_POST_IMAGE_COUNT); } } diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index af97d0a1..b85b73ee 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -87,10 +87,10 @@ void create() throws Exception { () -> assertThat(post.getShareUrl()).isEqualTo(shareUrl), () -> assertThat(images).hasSize(2), () -> assertThat(images.get(0).getImageFileId()).isEqualTo(1L), - () -> assertThat(images.get(0).getName()).isEqualTo("뽀또 A"), + () -> assertThat(images.get(0).getName()).isEqualTo("뽀또A"), () -> assertThat(images.get(0).getVoteCount()).isEqualTo(0), () -> assertThat(images.get(1).getImageFileId()).isEqualTo(2L), - () -> assertThat(images.get(1).getName()).isEqualTo("뽀또 B"), + () -> assertThat(images.get(1).getName()).isEqualTo("뽀또B"), () -> assertThat(images.get(1).getVoteCount()).isEqualTo(0) ); }