diff --git a/.github/workflows/cicd-workflow.yml b/.github/workflows/cicd-workflow.yml index 6d95b47..24899f9 100644 --- a/.github/workflows/cicd-workflow.yml +++ b/.github/workflows/cicd-workflow.yml @@ -6,9 +6,29 @@ on: pull_request: branches: [ "develop" ] - jobs: - deploy: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Grant gradlew permission + run: chmod +x gradlew + + - name: Run format check & tests + run: ./gradlew clean check + + cd: + needs: ci + if: github.event_name == 'push' runs-on: ubuntu-latest steps: @@ -25,7 +45,7 @@ jobs: run: chmod +x gradlew - name: Build JAR - run: ./gradlew clean build -x test + run: ./gradlew clean bootJar # buildx 세팅 (amd64) - name: Set up Docker Buildx diff --git a/build.gradle b/build.gradle index 6ba2ce2..031666b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '4.0.1' id 'io.spring.dependency-management' version '1.1.7' + id 'com.diffplug.spotless' version '8.2.0' } group = 'com.payper' @@ -50,8 +51,32 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'com.h2database:h2' +} + +spotless { + java { + target("**/*.java") + palantirJavaFormat().formatJavadoc(true) // 포맷 스타일 + removeUnusedImports() // 불필요한 import 제거 + trimTrailingWhitespace() // 명시: 불필요한 공백 제거 + endWithNewline() // 명시: 파일 끝에 새로운 줄 추가 + } } tasks.named('test') { useJUnitPlatform() } + +tasks.register('updateGitHooks', Copy) { + from("${rootDir}/scripts/pre-push") + into("${rootDir}/.git/hooks") + + doLast { + file("${rootDir}/.git/hooks/pre-push").setExecutable(true) + } +} + +tasks.named('check') { + dependsOn("spotlessCheck") +} \ No newline at end of file diff --git a/scripts/pre-push b/scripts/pre-push new file mode 100644 index 0000000..3cf0af7 --- /dev/null +++ b/scripts/pre-push @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +PROJECT_ROOT=$(git rev-parse --show-toplevel) + +echo "Running spotlessCheck before push..." +cd $PROJECT_ROOT && ./gradlew spotlessCheck + +if [ $? -ne 0 ]; then + echo "Spotless check failed. Please run ./gradlew spotlessApply" + exit 1 +fi + +echo "Spotless check passed." \ No newline at end of file diff --git a/src/main/java/com/payper/server/PayperServerApplication.java b/src/main/java/com/payper/server/PayperServerApplication.java index b03b3fd..71710ce 100644 --- a/src/main/java/com/payper/server/PayperServerApplication.java +++ b/src/main/java/com/payper/server/PayperServerApplication.java @@ -8,8 +8,7 @@ @SpringBootApplication public class PayperServerApplication { - public static void main(String[] args) { - SpringApplication.run(PayperServerApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(PayperServerApplication.class, args); + } } diff --git a/src/main/java/com/payper/server/auth/AuthApi.java b/src/main/java/com/payper/server/auth/AuthApi.java index f0ed93f..17d8f23 100644 --- a/src/main/java/com/payper/server/auth/AuthApi.java +++ b/src/main/java/com/payper/server/auth/AuthApi.java @@ -13,21 +13,25 @@ @Tag(name = "인증", description = "로그인, 토큰 재발급, 로그아웃 API") public interface AuthApi { - @Operation(summary = "로그인", description = "OAuth 토큰으로 로그인 (미가입 시 자동 회원가입). Access Token은 응답 바디, Refresh Token은 HttpOnly 쿠키로 발급됩니다.", security = {}) - ResponseEntity> enroll( - LoginRequest loginRequest, - HttpServletResponse response - ); + @Operation( + summary = "로그인", + description = "OAuth 토큰으로 로그인 (미가입 시 자동 회원가입). Access Token은 응답 바디, Refresh Token은 HttpOnly 쿠키로 발급됩니다.", + security = {}) + ResponseEntity> enroll(LoginRequest loginRequest, HttpServletResponse response); - @Operation(summary = "토큰 재발급", description = "Refresh Token(쿠키)으로 새로운 Access Token을 발급합니다.", security = {}) + @Operation( + summary = "토큰 재발급", + description = "Refresh Token(쿠키)으로 새로운 Access Token을 발급합니다.", + security = {}) ResponseEntity> reissue( @Parameter(description = "Refresh Token (HttpOnly 쿠키로 자동 전송)") String refreshToken, - HttpServletResponse response - ); + HttpServletResponse response); - @Operation(summary = "로그아웃", description = "Refresh Token을 무효화하고 쿠키를 삭제합니다.", security = {}) + @Operation( + summary = "로그아웃", + description = "Refresh Token을 무효화하고 쿠키를 삭제합니다.", + security = {}) ResponseEntity> logout( @Parameter(description = "Refresh Token (HttpOnly 쿠키로 자동 전송)") String refreshToken, - HttpServletResponse response - ); + HttpServletResponse response); } diff --git a/src/main/java/com/payper/server/auth/AuthController.java b/src/main/java/com/payper/server/auth/AuthController.java index 1b11c51..e3c27c4 100644 --- a/src/main/java/com/payper/server/auth/AuthController.java +++ b/src/main/java/com/payper/server/auth/AuthController.java @@ -18,18 +18,13 @@ public class AuthController implements AuthApi { private final AuthService authService; - @PostMapping("/login") //로그인 시도 -> 필요하면 가입 -> 로그인 + @PostMapping("/login") // 로그인 시도 -> 필요하면 가입 -> 로그인 public ResponseEntity> enroll( - @RequestBody LoginRequest loginRequest, - HttpServletResponse response - ) { - //OAuth 리소스 서버와 통신 문제 생기면 예외 - OAuthUserInfo oauthUserInfo = authService.findOAuthUserInfo( - loginRequest.getOauthToken(), - AuthType.KAKAO - ); - - //유저가 inactive(밴, 정지) 되어있으면 예외 + @RequestBody LoginRequest loginRequest, HttpServletResponse response) { + // OAuth 리소스 서버와 통신 문제 생기면 예외 + OAuthUserInfo oauthUserInfo = authService.findOAuthUserInfo(loginRequest.getOauthToken(), AuthType.KAKAO); + + // 유저가 inactive(밴, 정지) 되어있으면 예외 User user = authService.findOrEnrollOAuthUser(oauthUserInfo); String accessToken = authService.enrollNewAuthTokens(user, response); @@ -37,24 +32,17 @@ public ResponseEntity> enroll( return ResponseEntity.ok(ApiResponse.ok(new LoginSuccessResponse(accessToken))); } - @PostMapping("/reissue") public ResponseEntity> reissue( - @CookieValue(required = false) String refreshToken, - HttpServletResponse response - ) { + @CookieValue(required = false) String refreshToken, HttpServletResponse response) { String accessToken = authService.reissueAccessToken(refreshToken, response); - return ResponseEntity.ok( - ApiResponse.ok(new ReissueSuccessResponse(accessToken)) - ); + return ResponseEntity.ok(ApiResponse.ok(new ReissueSuccessResponse(accessToken))); } @PostMapping("/logout") public ResponseEntity> logout( - @CookieValue(required = false) String refreshToken, - HttpServletResponse response - ) { + @CookieValue(required = false) String refreshToken, HttpServletResponse response) { authService.clearRefreshTokenAndEntity(refreshToken, response); return ResponseEntity.ok(ApiResponse.ok("logout success")); diff --git a/src/main/java/com/payper/server/auth/AuthService.java b/src/main/java/com/payper/server/auth/AuthService.java index 268c644..08bfe38 100644 --- a/src/main/java/com/payper/server/auth/AuthService.java +++ b/src/main/java/com/payper/server/auth/AuthService.java @@ -14,15 +14,14 @@ import com.payper.server.user.entity.User; import com.payper.server.user.entity.UserRole; import jakarta.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import java.util.Date; -import java.util.Optional; - @Service @Transactional @RequiredArgsConstructor @@ -36,33 +35,33 @@ public class AuthService { private final JwtParseUtil jwtParseUtil; private User findOrEnrollUser(OAuthUserInfo oauthUserInfo, UserRole userRole) { - //먼저 검증 + // 먼저 검증 Optional user = userService.getActiveOAuthUser(oauthUserInfo); - user.ifPresent( - u->{ - log.info("가입 유저 확인 - userId: {}, userName: {}, userRole: {}", u.getId(),u.getName(),u.getUserRole().name()); - } - ); - - return user.orElseGet( - () -> { - User savedUser = userService.save( - User.create( - AuthType.KAKAO, - oauthUserInfo.getName(), - oauthUserInfo.getOauthId(), - userRole, // 매개변수 사용 - true - ) - ); - - log.info("유저 가입 & 저장 - userId: {}, userName: {}, userRole: {}", - savedUser.getId(),savedUser.getName(),savedUser.getUserRole().name()); - - return savedUser; - } - ); + user.ifPresent(u -> { + log.info( + "가입 유저 확인 - userId: {}, userName: {}, userRole: {}", + u.getId(), + u.getName(), + u.getUserRole().name()); + }); + + return user.orElseGet(() -> { + User savedUser = userService.save(User.create( + AuthType.KAKAO, + oauthUserInfo.getName(), + oauthUserInfo.getOauthId(), + userRole, // 매개변수 사용 + true)); + + log.info( + "유저 가입 & 저장 - userId: {}, userName: {}, userRole: {}", + savedUser.getId(), + savedUser.getName(), + savedUser.getUserRole().name()); + + return savedUser; + }); } public User findOrEnrollOAuthUser(OAuthUserInfo oauthUserInfo) { @@ -86,7 +85,7 @@ public String enrollNewAuthTokens(User user, HttpServletResponse response) { upsertRefreshTokenAndEntity(user.getUserIdentifier(), response, issuedAt); String accessToken = upsertAccessToken(user.getUserIdentifier(), issuedAt); - log.info("업서트 토큰 - userId: {}, issuedAt: {}",user.getId(),issuedAt); + log.info("업서트 토큰 - userId: {}, issuedAt: {}", user.getId(), issuedAt); return accessToken; } @@ -113,30 +112,25 @@ public String reissueAccessToken(String refreshToken, HttpServletResponse respon } userIdentifier = jwtParseUtil.getUserIdentifier(refreshToken); } catch (AuthException e) { - throw - switch (e.getErrorCode()) { - case JWT_ERROR -> new ApiException(ErrorCode.JWT_REISSUE_ERROR); - case JWT_EXPIRED -> new ApiException(ErrorCode.JWT_REISSUE_EXPIRED); - default -> new ApiException(ErrorCode.REISSUE_ERROR); - }; + throw switch (e.getErrorCode()) { + case JWT_ERROR -> new ApiException(ErrorCode.JWT_REISSUE_ERROR); + case JWT_EXPIRED -> new ApiException(ErrorCode.JWT_REISSUE_EXPIRED); + default -> new ApiException(ErrorCode.REISSUE_ERROR); + }; } // 2) DB에 없으면 리플레이 공격 의심 -> 해당 유저 토큰 전부 폐기 Optional refreshTokenEntity = jwtRefreshTokenUtil.getRefreshTokenEntity(refreshToken); - refreshTokenEntity.ifPresentOrElse( - (r) -> { - }, - () -> { - jwtRefreshTokenUtil.deleteAllRefreshTokenEntity(userIdentifier); - throw new ApiException(ErrorCode.JWT_REISSUE_OLD); - } - ); + refreshTokenEntity.ifPresentOrElse((r) -> {}, () -> { + jwtRefreshTokenUtil.deleteAllRefreshTokenEntity(userIdentifier); + throw new ApiException(ErrorCode.JWT_REISSUE_OLD); + }); Date reissuedAt = jwtParseUtil.getIssuedAt(refreshToken); upsertRefreshTokenAndEntity(userIdentifier, response, reissuedAt); String accessToken = upsertAccessToken(userIdentifier, new Date()); - log.info("토큰 재발급 완료 reissuedAt: {}",reissuedAt); + log.info("토큰 재발급 완료 reissuedAt: {}", reissuedAt); return accessToken; } @@ -151,9 +145,7 @@ private void upsertRefreshTokenAndEntity(String userIdentifier, HttpServletRespo RefreshTokenEntity refreshTokenEntity = jwtRefreshTokenUtil.generateRefreshTokenEntity(userIdentifier, refreshToken); - - if(response!=null) - jwtRefreshTokenUtil.generateCookieRefreshToken(refreshToken, response); + if (response != null) jwtRefreshTokenUtil.generateCookieRefreshToken(refreshToken, response); jwtRefreshTokenUtil.upsertRefreshTokenEntity(refreshTokenEntity); } @@ -166,10 +158,7 @@ public void clearRefreshTokenAndEntity(String refreshToken, HttpServletResponse } Optional refreshTokenEntity = jwtRefreshTokenUtil.getRefreshTokenEntity(refreshToken); - refreshTokenEntity.ifPresent( - r -> - jwtRefreshTokenUtil.deleteAllRefreshTokenEntity(r.getUserIdentifier()) - ); + refreshTokenEntity.ifPresent(r -> jwtRefreshTokenUtil.deleteAllRefreshTokenEntity(r.getUserIdentifier())); log.info("리프레시 토큰 만료 완료"); } diff --git a/src/main/java/com/payper/server/auth/dto/request/LoginRequest.java b/src/main/java/com/payper/server/auth/dto/request/LoginRequest.java index 128a12f..2395782 100644 --- a/src/main/java/com/payper/server/auth/dto/request/LoginRequest.java +++ b/src/main/java/com/payper/server/auth/dto/request/LoginRequest.java @@ -1,6 +1,5 @@ package com.payper.server.auth.dto.request; - import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Getter; @@ -11,6 +10,6 @@ @Schema(description = "로그인 요청") public class LoginRequest { @Schema(description = "OAuth Provider가 제공한 access token", example = "kakao_oauth_token_example") - @NotBlank(message="OAuth Provider가 제공한 oauth resource access token이 필요합니다.") + @NotBlank(message = "OAuth Provider가 제공한 oauth resource access token이 필요합니다.") private String oauthToken; } diff --git a/src/main/java/com/payper/server/auth/jwt/RefreshTokenRepository.java b/src/main/java/com/payper/server/auth/jwt/RefreshTokenRepository.java index 7a9042a..19116bb 100644 --- a/src/main/java/com/payper/server/auth/jwt/RefreshTokenRepository.java +++ b/src/main/java/com/payper/server/auth/jwt/RefreshTokenRepository.java @@ -1,13 +1,12 @@ package com.payper.server.auth.jwt; import com.payper.server.auth.jwt.entity.RefreshTokenEntity; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.Optional; - public interface RefreshTokenRepository extends JpaRepository { @Modifying(clearAutomatically = true, flushAutomatically = true) diff --git a/src/main/java/com/payper/server/auth/jwt/entity/JwtType.java b/src/main/java/com/payper/server/auth/jwt/entity/JwtType.java index f2db02c..1944579 100644 --- a/src/main/java/com/payper/server/auth/jwt/entity/JwtType.java +++ b/src/main/java/com/payper/server/auth/jwt/entity/JwtType.java @@ -1,5 +1,6 @@ package com.payper.server.auth.jwt.entity; public enum JwtType { - ACCESS,REFRESH + ACCESS, + REFRESH } diff --git a/src/main/java/com/payper/server/auth/jwt/entity/RefreshTokenEntity.java b/src/main/java/com/payper/server/auth/jwt/entity/RefreshTokenEntity.java index 1e0f878..347f3f7 100644 --- a/src/main/java/com/payper/server/auth/jwt/entity/RefreshTokenEntity.java +++ b/src/main/java/com/payper/server/auth/jwt/entity/RefreshTokenEntity.java @@ -14,16 +14,13 @@ public class RefreshTokenEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false,unique = true,updatable = false) + @Column(nullable = false, unique = true, updatable = false) private String userIdentifier; - @Column(nullable = false,unique = true, updatable = false) + @Column(nullable = false, unique = true, updatable = false) private String hashedRefreshToken; - public static RefreshTokenEntity create( - String userIdentifier, - String hashedRefreshToken - ){ + public static RefreshTokenEntity create(String userIdentifier, String hashedRefreshToken) { return RefreshTokenEntity.builder() .userIdentifier(userIdentifier) .hashedRefreshToken(hashedRefreshToken) diff --git a/src/main/java/com/payper/server/auth/jwt/util/JwtParseUtil.java b/src/main/java/com/payper/server/auth/jwt/util/JwtParseUtil.java index b097f36..5972e00 100644 --- a/src/main/java/com/payper/server/auth/jwt/util/JwtParseUtil.java +++ b/src/main/java/com/payper/server/auth/jwt/util/JwtParseUtil.java @@ -7,15 +7,14 @@ import io.jsonwebtoken.security.SignatureException; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.util.Date; - @Component @RequiredArgsConstructor public class JwtParseUtil { @@ -25,10 +24,8 @@ public class JwtParseUtil { @PostConstruct protected void init() { key = new SecretKeySpec( - jwtProperties.getSecretKey() - .getBytes(StandardCharsets.UTF_8), - Jwts.SIG.HS512.key().build().getAlgorithm() - ); + jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS512.key().build().getAlgorithm()); } public String extractJwtTokenFromRequest(HttpServletRequest request) { @@ -37,8 +34,7 @@ public String extractJwtTokenFromRequest(HttpServletRequest request) { if (StringUtils.hasText(headerValue) && headerValue.startsWith("Bearer ")) { String token = headerValue.substring(7); - if (token.isEmpty()) - return null; + if (token.isEmpty()) return null; return token; } @@ -47,18 +43,15 @@ public String extractJwtTokenFromRequest(HttpServletRequest request) { } public String getUserIdentifier(String jwtToken) { - return getClaimsFromJwtToken(jwtToken) - .getSubject(); + return getClaimsFromJwtToken(jwtToken).getSubject(); } public Date getIssuedAt(String jwtToken) { - return getClaimsFromJwtToken(jwtToken) - .getIssuedAt(); + return getClaimsFromJwtToken(jwtToken).getIssuedAt(); } public Date getExpiresAt(String jwtToken) { - return getClaimsFromJwtToken(jwtToken) - .getExpiration(); + return getClaimsFromJwtToken(jwtToken).getExpiration(); } public JwtType getJwtType(String jwtToken) { @@ -71,14 +64,14 @@ private Claims getClaimsFromJwtToken(String jwtToken) { private Jws getJws(String jwtToken) { try { - return Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(jwtToken); + return Jwts.parser().verifyWith(key).build().parseSignedClaims(jwtToken); } catch (ExpiredJwtException e) { throw new AuthException(ErrorCode.JWT_EXPIRED); - } catch (UnsupportedJwtException | ClaimJwtException | SignatureException | - MalformedJwtException | IllegalArgumentException e) { + } catch (UnsupportedJwtException + | ClaimJwtException + | SignatureException + | MalformedJwtException + | IllegalArgumentException e) { throw new AuthException(ErrorCode.JWT_ERROR); } } diff --git a/src/main/java/com/payper/server/auth/jwt/util/JwtProperties.java b/src/main/java/com/payper/server/auth/jwt/util/JwtProperties.java index 68e88ba..762fe15 100644 --- a/src/main/java/com/payper/server/auth/jwt/util/JwtProperties.java +++ b/src/main/java/com/payper/server/auth/jwt/util/JwtProperties.java @@ -1,12 +1,11 @@ package com.payper.server.auth.jwt.util; +import java.time.Duration; import lombok.AccessLevel; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.time.Duration; - @Getter @Component public class JwtProperties { @@ -31,4 +30,4 @@ public long getAccessTokenTime() { public long getRefreshTokenTime() { return refreshTokenExpiration.toMillis(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/auth/jwt/util/JwtRefreshTokenUtil.java b/src/main/java/com/payper/server/auth/jwt/util/JwtRefreshTokenUtil.java index 3f4952d..47295bc 100644 --- a/src/main/java/com/payper/server/auth/jwt/util/JwtRefreshTokenUtil.java +++ b/src/main/java/com/payper/server/auth/jwt/util/JwtRefreshTokenUtil.java @@ -6,19 +6,18 @@ import jakarta.annotation.PostConstruct; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Base64; import java.util.Date; import java.util.Optional; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; @Component @RequiredArgsConstructor @@ -32,20 +31,13 @@ public class JwtRefreshTokenUtil { @PostConstruct protected void init() { refreshSecretKey = new SecretKeySpec( - jwtProperties.getRefreshTokenSecretKey() - .getBytes(StandardCharsets.UTF_8), - Jwts.SIG.HS512.key().build().getAlgorithm() - ); + jwtProperties.getRefreshTokenSecretKey().getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS512.key().build().getAlgorithm()); } - public RefreshTokenEntity generateRefreshTokenEntity( - String userIdentifier, String refreshToken - ) { + public RefreshTokenEntity generateRefreshTokenEntity(String userIdentifier, String refreshToken) { - return RefreshTokenEntity.create( - userIdentifier, - hashRefreshToken(refreshToken) - ); + return RefreshTokenEntity.create(userIdentifier, hashRefreshToken(refreshToken)); } private String hashRefreshToken(String refreshToken) { @@ -70,11 +62,9 @@ public int deleteAllRefreshTokenEntity(String userIdentifier) { } public Optional getRefreshTokenEntity(String refreshToken) { - return refreshTokenRepository - .findByHashedRefreshToken(hashRefreshToken(refreshToken)); + return refreshTokenRepository.findByHashedRefreshToken(hashRefreshToken(refreshToken)); } - public void generateCookieRefreshToken(String refreshToken, HttpServletResponse response) { Cookie cookie = new Cookie("refreshToken", refreshToken); cookie.setPath("/"); diff --git a/src/main/java/com/payper/server/auth/jwt/util/JwtTokenUtil.java b/src/main/java/com/payper/server/auth/jwt/util/JwtTokenUtil.java index c703977..6ca2742 100644 --- a/src/main/java/com/payper/server/auth/jwt/util/JwtTokenUtil.java +++ b/src/main/java/com/payper/server/auth/jwt/util/JwtTokenUtil.java @@ -3,14 +3,13 @@ import com.payper.server.auth.jwt.entity.JwtType; import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.UUID; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor @@ -22,18 +21,15 @@ public class JwtTokenUtil { @PostConstruct protected void init() { key = new SecretKeySpec( - jwtProperties.getSecretKey() - .getBytes(StandardCharsets.UTF_8), - Jwts.SIG.HS512.key().build().getAlgorithm() - ); + jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS512.key().build().getAlgorithm()); } public String generateJwtToken(JwtType jwtType, Date now, String userIdentifier) { - Date expDate = new Date( - now.getTime() + - (jwtType == JwtType.REFRESH ? - jwtProperties.getRefreshTokenTime() : jwtProperties.getAccessTokenTime()) - ); + Date expDate = new Date(now.getTime() + + (jwtType == JwtType.REFRESH + ? jwtProperties.getRefreshTokenTime() + : jwtProperties.getAccessTokenTime())); return Jwts.builder() .header() @@ -46,5 +42,4 @@ public String generateJwtToken(JwtType jwtType, Date now, String userIdentifier) .id(UUID.randomUUID().toString()) .compact(); } - } diff --git a/src/main/java/com/payper/server/auth/util/AuthDummyInit.java b/src/main/java/com/payper/server/auth/util/AuthDummyInit.java index faf625e..a7eac85 100644 --- a/src/main/java/com/payper/server/auth/util/AuthDummyInit.java +++ b/src/main/java/com/payper/server/auth/util/AuthDummyInit.java @@ -5,18 +5,12 @@ import com.payper.server.user.entity.User; import jakarta.servlet.*; import jakarta.servlet.http.*; +import java.util.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.Principal; -import java.util.*; @Component @RequiredArgsConstructor @@ -28,26 +22,10 @@ public class AuthDummyInit implements ApplicationRunner { public void run(ApplicationArguments args) throws Exception { log.info("Auth 더미 데이터 초기화 중"); - OAuthUserInfo dummyOAuth1=new OAuthUserInfo( - "최미나수", - "9999", - AuthType.KAKAO - ); - OAuthUserInfo dummyOAuth2=new OAuthUserInfo( - "김고은", - "8888", - AuthType.KAKAO - ); - OAuthUserInfo dummyOAuth3=new OAuthUserInfo( - "김민지", - "7777", - AuthType.KAKAO - ); - OAuthUserInfo adminDummyOAuth1=new OAuthUserInfo( - "관리자", - "2222", - AuthType.KAKAO - ); + OAuthUserInfo dummyOAuth1 = new OAuthUserInfo("최미나수", "9999", AuthType.KAKAO); + OAuthUserInfo dummyOAuth2 = new OAuthUserInfo("김고은", "8888", AuthType.KAKAO); + OAuthUserInfo dummyOAuth3 = new OAuthUserInfo("김민지", "7777", AuthType.KAKAO); + OAuthUserInfo adminDummyOAuth1 = new OAuthUserInfo("관리자", "2222", AuthType.KAKAO); User dummyUser1 = authService.findOrEnrollOAuthUser(dummyOAuth1); User dummyUser2 = authService.findOrEnrollOAuthUser(dummyOAuth2); @@ -59,11 +37,9 @@ public void run(ApplicationArguments args) throws Exception { String accessToken3 = authService.enrollNewAuthTokens(dummyUser3, null); String accessToken4 = authService.enrollNewAuthTokens(dummyUser4, null); - log.info("du1: name = {}, at = {}",dummyUser1.getName(),accessToken1); - log.info("du2: name = {}, at = {}",dummyUser2.getName(),accessToken2); - log.info("du3: name = {}, at = {}",dummyUser3.getName(),accessToken3); - log.info("admin du1: name = {}, at = {}",dummyUser4.getName(),accessToken4); + log.info("du1: name = {}, at = {}", dummyUser1.getName(), accessToken1); + log.info("du2: name = {}, at = {}", dummyUser2.getName(), accessToken2); + log.info("du3: name = {}, at = {}", dummyUser3.getName(), accessToken3); + log.info("admin du1: name = {}, at = {}", dummyUser4.getName(), accessToken4); } - - } diff --git a/src/main/java/com/payper/server/auth/util/KakaoOAuthUtilImpl.java b/src/main/java/com/payper/server/auth/util/KakaoOAuthUtilImpl.java index af72550..f1a4cf8 100644 --- a/src/main/java/com/payper/server/auth/util/KakaoOAuthUtilImpl.java +++ b/src/main/java/com/payper/server/auth/util/KakaoOAuthUtilImpl.java @@ -23,7 +23,7 @@ public class KakaoOAuthUtilImpl implements OAuthUtil { @PostConstruct protected void init() { - kakaoRestClient=RestClient.builder() + kakaoRestClient = RestClient.builder() .baseUrl("https://kapi.kakao.com") .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) .build(); @@ -31,29 +31,24 @@ protected void init() { @Override public OAuthUserInfo getUserInfoFromOAuthToken(String oAuthToken) { - String body = kakaoRestClient.get() + String body = kakaoRestClient + .get() .uri(uriBuilder -> uriBuilder .path("/v2/user/me") .queryParam("property_keys", PROPERTY_KEYS) .build()) .headers(headers -> headers.setBearerAuth(oAuthToken)) .retrieve() - .onStatus( - HttpStatusCode::isError, - (request, response) -> - { - throw new ApiException(ErrorCode.OAUTH_RESOURCE_ERROR); - } - ) + .onStatus(HttpStatusCode::isError, (request, response) -> { + throw new ApiException(ErrorCode.OAUTH_RESOURCE_ERROR); + }) .body(String.class); try { JsonNode json = objectMapper.readTree(body); - String name = json.path("kakao_account") - .path("profile") - .path("nickname") - .asString(); + String name = + json.path("kakao_account").path("profile").path("nickname").asString(); String kakaoId = json.path("id").asString(); diff --git a/src/main/java/com/payper/server/comment/controller/CommentApi.java b/src/main/java/com/payper/server/comment/controller/CommentApi.java index 8369083..ed503d9 100644 --- a/src/main/java/com/payper/server/comment/controller/CommentApi.java +++ b/src/main/java/com/payper/server/comment/controller/CommentApi.java @@ -18,28 +18,26 @@ public interface CommentApi { ResponseEntity> updateComment( CustomUserDetails user, @Parameter(description = "댓글 ID", example = "1") Long commentId, - CommentRequest.UpdateComment request - ); + CommentRequest.UpdateComment request); @Operation(summary = "댓글 삭제", description = "작성자만 삭제 가능. 자식 댓글은 삭제되지 않습니다.") @SecurityRequirement(name = "bearerAuth") ResponseEntity> deleteComment( - CustomUserDetails user, - @Parameter(description = "댓글 ID", example = "1") Long commentId - ); + CustomUserDetails user, @Parameter(description = "댓글 ID", example = "1") Long commentId); @Operation(summary = "내가 쓴 댓글 조회", description = "커서 기반 페이지네이션. 최신순 정렬. 삭제된 댓글은 제외됩니다.") @SecurityRequirement(name = "bearerAuth") ResponseEntity> getMyComments( CustomUserDetails user, @Parameter(description = "마지막 조회 댓글 ID (첫 요청 시 생략)") Long cursorId, - @Parameter(description = "조회 개수", example = "20") int size - ); + @Parameter(description = "조회 개수", example = "20") int size); - @Operation(summary = "대댓글 조회", description = "부모 댓글의 대댓글을 커서 기반으로 조회합니다. 삭제된 댓글은 제외됩니다.", security = {}) + @Operation( + summary = "대댓글 조회", + description = "부모 댓글의 대댓글을 커서 기반으로 조회합니다. 삭제된 댓글은 제외됩니다.", + security = {}) ResponseEntity> getReplies( @Parameter(description = "부모 댓글 ID", example = "1") Long parentId, @Parameter(description = "마지막 조회 댓글 ID (첫 요청 시 생략)") Long cursorId, - @Parameter(description = "조회 개수", example = "20") int size - ); + @Parameter(description = "조회 개수", example = "20") int size); } diff --git a/src/main/java/com/payper/server/comment/controller/CommentController.java b/src/main/java/com/payper/server/comment/controller/CommentController.java index 421e969..cb9d1b1 100644 --- a/src/main/java/com/payper/server/comment/controller/CommentController.java +++ b/src/main/java/com/payper/server/comment/controller/CommentController.java @@ -17,61 +17,41 @@ public class CommentController implements CommentApi { private final CommentService commentService; - /** - * 댓글 수정 - * 작성자만 수정 가능 - */ + /** 댓글 수정 */ @PutMapping("/{commentId}") public ResponseEntity> updateComment( @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long commentId, - @RequestBody @Valid CommentRequest.UpdateComment request - ) { + @RequestBody @Valid CommentRequest.UpdateComment request) { commentService.updateComment(user.getId(), commentId, request); return ResponseEntity.ok(ApiResponse.ok()); } - /** - * 댓글 삭제 - * 작성자만 삭제 가능 - * - * 자식 댓글은 삭제하지 않음 - */ + /** 댓글 삭제 */ @DeleteMapping("/{commentId}") public ResponseEntity> deleteComment( - @AuthenticationPrincipal CustomUserDetails user, - @PathVariable Long commentId) { + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long commentId) { commentService.deleteComment(user.getId(), commentId); return ResponseEntity.ok(ApiResponse.ok()); } - /** - * 내가 쓴 댓글 조회 - * - * 무한 스크롤 방식 - * - * 정렬: 최신 순 - */ + /** 내가 쓴 댓글 조회 */ @GetMapping("/me") public ResponseEntity> getMyComments( @AuthenticationPrincipal CustomUserDetails user, @RequestParam(required = false) Long cursorId, - @RequestParam(defaultValue = "20") int size - ) { + @RequestParam(defaultValue = "20") int size) { CommentResponse.MyCommentList response = commentService.getMyComments(user.getId(), cursorId, size); return ResponseEntity.ok(ApiResponse.ok(response)); } - /** - * 자식 댓글 조회 - */ + /** 자식 댓글 조회 */ @GetMapping("/{parentId}/replies") public ResponseEntity> getReplies( @PathVariable Long parentId, @RequestParam(required = false) Long cursorId, - @RequestParam(defaultValue = "20") int size - ) { + @RequestParam(defaultValue = "20") int size) { CommentResponse.CommentList response = commentService.getReplies(parentId, cursorId, size); return ResponseEntity.ok(ApiResponse.ok(response)); } diff --git a/src/main/java/com/payper/server/comment/dto/CommentRequest.java b/src/main/java/com/payper/server/comment/dto/CommentRequest.java index 426adf4..826f35c 100644 --- a/src/main/java/com/payper/server/comment/dto/CommentRequest.java +++ b/src/main/java/com/payper/server/comment/dto/CommentRequest.java @@ -7,9 +7,7 @@ public class CommentRequest { - /** - * 댓글 작성 DTO - */ + /** 댓글 작성 DTO */ @Schema(description = "댓글 작성 요청") public record CreateComment( @Schema(description = "댓글 내용", example = "좋은 글이네요!") @@ -17,19 +15,14 @@ public record CreateComment( @Size(max = 21800, message = "댓글은 21,800자 이하여야 합니다.") String content, - @Schema(description = "부모 댓글 ID (대댓글 작성 시)", example = "1", nullable = true) - @Nullable - Long parentCommentId - ) {} + @Schema(description = "부모 댓글 ID (대댓글 작성 시)", example = "1", nullable = true) @Nullable + Long parentCommentId) {} - /** - * 댓글 수정 DTO - */ + /** 댓글 수정 DTO */ @Schema(description = "댓글 수정 요청") public record UpdateComment( @Schema(description = "수정할 댓글 내용", example = "수정된 댓글입니다") @NotBlank(message = "댓글을 적어주세요.") @Size(max = 21800, message = "댓글은 21,800자 이하여야 합니다.") - String content - ) {} + String content) {} } diff --git a/src/main/java/com/payper/server/comment/dto/CommentResponse.java b/src/main/java/com/payper/server/comment/dto/CommentResponse.java index 1f1d4b3..d8029cf 100644 --- a/src/main/java/com/payper/server/comment/dto/CommentResponse.java +++ b/src/main/java/com/payper/server/comment/dto/CommentResponse.java @@ -2,112 +2,88 @@ import com.payper.server.comment.entity.Comment; import io.swagger.v3.oas.annotations.media.Schema; - import java.time.LocalDateTime; import java.util.List; public class CommentResponse { - /** - * Post에 달린 Comment 리스트 - */ + /** Post에 달린 Comment 리스트 */ @Schema(description = "댓글 목록 (커서 기반 페이지네이션)") public record CommentList( - @Schema(description = "댓글 목록") - List comments, + @Schema(description = "댓글 목록") List comments, + @Schema(description = "다음 페이지 커서 (다음 페이지가 없으면 null)", example = "25") Long nextCursor, + @Schema(description = "다음 페이지 존재 여부", example = "true") - boolean hasNext - ) { + boolean hasNext) { public static CommentList from(List comments, Long nextCursor, boolean hasNext) { return new CommentList( - comments.stream() - .map(CommentResponse.CommentItem::from) - .toList(), - nextCursor, - hasNext - ); + comments.stream().map(CommentResponse.CommentItem::from).toList(), nextCursor, hasNext); } } - /** - * Comment Item - */ + /** Comment Item */ @Schema(description = "댓글 항목") public record CommentItem( - @Schema(description = "댓글 ID", example = "1") - Long id, - @Schema(description = "작성자 이름", example = "홍길동") - String userName, + @Schema(description = "댓글 ID", example = "1") Long id, + @Schema(description = "작성자 이름", example = "홍길동") String userName, + @Schema(description = "부모 댓글 ID (최상위 댓글이면 null)", example = "1") Long parentCommentId, + @Schema(description = "댓글 내용 (삭제 시 '[삭제된 댓글입니다]')", example = "좋은 글이네요!") String content, - @Schema(description = "작성일시") - LocalDateTime createdAt, - @Schema(description = "수정일시") - LocalDateTime updatedAt - ) { + + @Schema(description = "작성일시") LocalDateTime createdAt, + @Schema(description = "수정일시") LocalDateTime updatedAt) { public static CommentItem from(Comment comment) { return new CommentItem( comment.getId(), comment.getUser().getName(), - comment.getParentComment() != null ? comment.getParentComment().getId() : null, + comment.getParentComment() != null + ? comment.getParentComment().getId() + : null, comment.isDeleted() ? "[삭제된 댓글입니다]" : comment.getContent(), comment.getCreatedAt(), - comment.getUpdatedAt() - ); + comment.getUpdatedAt()); } } - /** - * 내가 작성한 Comment 리스트 - */ + /** 내가 작성한 Comment 리스트 */ @Schema(description = "내가 작성한 댓글 목록 (커서 기반 페이지네이션)") public record MyCommentList( - @Schema(description = "댓글 목록") - List comments, + @Schema(description = "댓글 목록") List comments, + @Schema(description = "다음 페이지 커서 (다음 페이지가 없으면 null)", example = "25") Long nextCursor, + @Schema(description = "다음 페이지 존재 여부", example = "true") - boolean hasNext - ) { + boolean hasNext) { public static MyCommentList from(List comments, Long nextCursor, boolean hasNext) { return new MyCommentList( - comments.stream() - .map(CommentResponse.MyCommentItem::from) - .toList(), - nextCursor, - hasNext - ); + comments.stream().map(CommentResponse.MyCommentItem::from).toList(), nextCursor, hasNext); } } - /** - * 내가 작성한 Comment Item - */ + /** 내가 작성한 Comment Item */ @Schema(description = "내가 작성한 댓글 항목") public record MyCommentItem( - @Schema(description = "댓글 ID", example = "1") - Long id, - @Schema(description = "게시글 ID", example = "10") - Long postId, + @Schema(description = "댓글 ID", example = "1") Long id, + @Schema(description = "게시글 ID", example = "10") Long postId, + @Schema(description = "댓글 내용", example = "좋은 글이네요!") String content, - @Schema(description = "작성일시") - LocalDateTime createdAt, - @Schema(description = "수정일시") - LocalDateTime updatedAt - ) { + + @Schema(description = "작성일시") LocalDateTime createdAt, + @Schema(description = "수정일시") LocalDateTime updatedAt) { public static MyCommentItem from(Comment comment) { return new MyCommentItem( comment.getId(), comment.getPost().getId(), comment.getContent(), comment.getCreatedAt(), - comment.getUpdatedAt() - ); + comment.getUpdatedAt()); } } } diff --git a/src/main/java/com/payper/server/comment/entity/Comment.java b/src/main/java/com/payper/server/comment/entity/Comment.java index c1b6913..7161f88 100644 --- a/src/main/java/com/payper/server/comment/entity/Comment.java +++ b/src/main/java/com/payper/server/comment/entity/Comment.java @@ -4,9 +4,8 @@ import com.payper.server.post.entity.Post; import com.payper.server.user.entity.User; import jakarta.persistence.*; -import lombok.*; - import java.time.LocalDateTime; +import lombok.*; @Entity @Getter @@ -20,55 +19,38 @@ public class Comment extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 게시글 - */ + /** 게시글 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", nullable = false) private Post post; - /** - * 댓글을 단 유저 - */ + /** 댓글을 단 유저 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; - /** - * 부모 댓글 - */ + /** 부모 댓글 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_comment_id") private Comment parentComment; - /** - * 댓글 내용 (64KB, 대략 21,800자) - */ + /** 댓글 내용 (64KB, 대략 21,800자) */ @Column(columnDefinition = "TEXT", nullable = false) private String content; - /** - * 좋아요 수 TODO: CommentLike 테이블 고려 - */ + /** 좋아요 수 TODO: CommentLike 테이블 고려 */ @Column(name = "like_count", nullable = false) private long likeCount; - /** - * 삭제 여부 - */ + /** 삭제 여부 */ @Column(name = "is_deleted", nullable = false) private boolean isDeleted; - /** - * 삭제 시간 - */ + /** 삭제 시간 */ @Column(name = "deleted_at") private LocalDateTime deletedAt; - /** - * 댓글 삭제 - * soft delete - */ + /** 댓글 삭제 - soft delete */ public void delete() { if (this.isDeleted) { // 멱등성 고려 return; @@ -79,9 +61,7 @@ public void delete() { this.post.decreaseCommentCount(); } - /** - * 댓글 생성 - */ + /** 댓글 생성 */ public static Comment create(Post post, User user, Comment parentComment, String content) { return Comment.builder() .post(post) @@ -93,17 +73,13 @@ public static Comment create(Post post, User user, Comment parentComment, String .build(); } - /** - * 댓글 수정 - */ + /** 댓글 수정 */ public void update(String content) { this.content = content; } - /** - * 댓글 작성자인지 판단 - */ + /** 댓글 작성자인지 판단 */ public boolean isAuthor(Long userId) { return this.user.getId().equals(userId); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/comment/entity/CommentLike.java b/src/main/java/com/payper/server/comment/entity/CommentLike.java index 2d99ebd..d7474fc 100644 --- a/src/main/java/com/payper/server/comment/entity/CommentLike.java +++ b/src/main/java/com/payper/server/comment/entity/CommentLike.java @@ -11,28 +11,23 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table( name = "comment_like", - uniqueConstraints = @UniqueConstraint( - name = "uk_comment_like_comment_user", - columnNames = {"comment_id", "user_id"} - ) -) + uniqueConstraints = + @UniqueConstraint( + name = "uk_comment_like_comment_user", + columnNames = {"comment_id", "user_id"})) public class CommentLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 댓글 - */ + /** 댓글 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "comment_id", nullable = false) private Comment comment; - /** - * 유저 - */ + /** 유저 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/comment/repository/CommentRepository.java b/src/main/java/com/payper/server/comment/repository/CommentRepository.java index 5b41cc6..5f3b261 100644 --- a/src/main/java/com/payper/server/comment/repository/CommentRepository.java +++ b/src/main/java/com/payper/server/comment/repository/CommentRepository.java @@ -1,6 +1,8 @@ package com.payper.server.comment.repository; import com.payper.server.comment.entity.Comment; +import java.time.LocalDateTime; +import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,9 +11,6 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.time.LocalDateTime; -import java.util.Optional; - @Repository public interface CommentRepository extends JpaRepository { @@ -57,8 +56,7 @@ Slice findParentNext( @Param("postId") Long postId, @Param("cursorId") Long cursorId, @Param("createdAt") LocalDateTime createdAt, - Pageable pageable - ); + Pageable pageable); @Query(""" select c from Comment c @@ -84,8 +82,7 @@ Slice findReplyNext( @Param("parentId") Long parentId, @Param("cursorId") Long cursorId, @Param("createdAt") LocalDateTime createdAt, - Pageable pageable - ); + Pageable pageable); @Query(""" select c from Comment c @@ -109,8 +106,7 @@ Slice findNextMyCommentPage( @Param("userId") Long userId, @Param("cursorId") Long cursorId, @Param("createdAt") LocalDateTime createdAt, - Pageable pageable - ); + Pageable pageable); @Modifying @Query(""" diff --git a/src/main/java/com/payper/server/comment/service/CommentService.java b/src/main/java/com/payper/server/comment/service/CommentService.java index 736c090..cb3713a 100644 --- a/src/main/java/com/payper/server/comment/service/CommentService.java +++ b/src/main/java/com/payper/server/comment/service/CommentService.java @@ -1,5 +1,7 @@ package com.payper.server.comment.service; +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; + import com.payper.server.comment.dto.CommentRequest; import com.payper.server.comment.dto.CommentResponse; import com.payper.server.comment.entity.Comment; @@ -10,6 +12,7 @@ import com.payper.server.post.repository.PostRepository; import com.payper.server.user.entity.User; import com.payper.server.user.repository.UserRepository; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; @@ -18,10 +21,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; - -import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; - @Slf4j @Service @RequiredArgsConstructor @@ -31,27 +30,22 @@ public class CommentService { private final PostRepository postRepository; private final CommentRepository commentRepository; - /** - * 댓글 작성 - */ + /** 댓글 작성 */ @Transactional public Long createComment(Long userId, Long postId, CommentRequest.CreateComment request) { // 사용자 조회 - User user = userRepository.findById(userId) - .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User user = userRepository.findById(userId).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); // 게시글 조회 - Post post = postRepository.findById(postId) - .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); + Post post = postRepository.findById(postId).orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); // 삭제 또는 비활성화된 post에는 댓글 작성 불가 if (!post.isCommentable()) { throw new ApiException(ErrorCode.POST_NOT_COMMENTABLE); } - Comment parentComment = request.parentCommentId() != null - ? getValidatedParentComment(request.parentCommentId(), postId) - : null; + Comment parentComment = + request.parentCommentId() != null ? getValidatedParentComment(request.parentCommentId(), postId) : null; // 댓글 생성 Comment comment = Comment.create(post, user, parentComment, request.content()); @@ -65,7 +59,8 @@ public Long createComment(Long userId, Long postId, CommentRequest.CreateComment // 요청 body로 받은 parent comment id 검증 private Comment getValidatedParentComment(Long parentCommentId, Long postId) { - Comment parentComment = commentRepository.findById(parentCommentId) + Comment parentComment = commentRepository + .findById(parentCommentId) .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); if (!parentComment.getPost().getId().equals(postId)) { @@ -75,17 +70,16 @@ private Comment getValidatedParentComment(Long parentCommentId, Long postId) { return parentComment; } - /** - * 댓글 수정 - */ + /** 댓글 수정 */ @Transactional public void updateComment(Long userId, Long commentId, CommentRequest.UpdateComment request) { // 댓글 조회 - Comment comment = commentRepository.findByIdAndIsDeletedFalse(commentId) + Comment comment = commentRepository + .findByIdAndIsDeletedFalse(commentId) .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); // 댓글 수정 권한 조회 - if(!comment.isAuthor(userId)) { + if (!comment.isAuthor(userId)) { throw new ApiException(ErrorCode.NOT_COMMENT_AUTHOR); } @@ -95,26 +89,22 @@ public void updateComment(Long userId, Long commentId, CommentRequest.UpdateComm log.info("댓글 수정 완료 - commentId: {}", comment.getId()); } - /** - * 댓글 삭제 - */ + /** 댓글 삭제 */ @Transactional public void deleteComment(Long userId, Long commentId) { // 댓글 조회 - Comment comment = commentRepository.findById(commentId) - .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + Comment comment = + commentRepository.findById(commentId).orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); // 댓글 삭제 권한 조회 - if(!comment.isAuthor(userId)) { + if (!comment.isAuthor(userId)) { throw new ApiException(ErrorCode.NOT_COMMENT_AUTHOR); } comment.delete(); } - /** - * 내가 작성한 댓글 조회 - */ + /** 내가 작성한 댓글 조회 */ @Transactional(readOnly = true) public CommentResponse.MyCommentList getMyComments(Long userId, Long cursorId, int size) { Pageable pageable = PageRequest.of(0, size); @@ -124,24 +114,25 @@ public CommentResponse.MyCommentList getMyComments(Long userId, Long cursorId, i if (cursorId == null) { // 첫 요청 comments = commentRepository.findFirstMyCommentPage(userId, pageable); } else { // 첫 요청이 아닌 경우 - Comment lastComment = commentRepository.findById(cursorId) + Comment lastComment = commentRepository + .findById(cursorId) .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); comments = commentRepository.findNextMyCommentPage(userId, cursorId, lastComment.getCreatedAt(), pageable); } - Long nextCursor = comments.hasNext() ? comments.getContent().get(comments.getContent().size()-1).getId() : null; + Long nextCursor = comments.hasNext() + ? comments.getContent().get(comments.getContent().size() - 1).getId() + : null; return CommentResponse.MyCommentList.from(comments.getContent(), nextCursor, comments.hasNext()); } - /** - * 게시글 댓글 조회 - */ + /** 게시글 댓글 조회 */ @Transactional(readOnly = true) public CommentResponse.CommentList getPostComments(Long postId, Long cursorId, int size) { // 게시글 존재 및 삭제 여부 확인 - if(!postRepository.existsByIdAndIsDeletedFalse(postId)) { + if (!postRepository.existsByIdAndIsDeletedFalse(postId)) { throw new ApiException(ErrorCode.POST_NOT_FOUND); } @@ -152,24 +143,25 @@ public CommentResponse.CommentList getPostComments(Long postId, Long cursorId, i if (cursorId == null) { comments = commentRepository.findParent(postId, pageable); } else { - Comment lastComment = commentRepository.findById(cursorId) + Comment lastComment = commentRepository + .findById(cursorId) .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); comments = commentRepository.findParentNext(postId, cursorId, lastComment.getCreatedAt(), pageable); } - Long nextCursor = comments.hasNext() ? comments.getContent().get(comments.getContent().size()-1).getId() : null; + Long nextCursor = comments.hasNext() + ? comments.getContent().get(comments.getContent().size() - 1).getId() + : null; return CommentResponse.CommentList.from(comments.getContent(), nextCursor, comments.hasNext()); } - /** - * 자식 댓글 조회 - */ + /** 자식 댓글 조회 */ @Transactional(readOnly = true) public CommentResponse.CommentList getReplies(Long parentId, Long cursorId, int size) { // 부모 댓글 존재 여부 확인 - if(!commentRepository.existsById(parentId)) { + if (!commentRepository.existsById(parentId)) { throw new ApiException(ErrorCode.COMMENT_NOT_FOUND); } @@ -180,19 +172,20 @@ public CommentResponse.CommentList getReplies(Long parentId, Long cursorId, int if (cursorId == null) { comments = commentRepository.findReply(parentId, pageable); } else { - Comment lastComment = commentRepository.findById(cursorId) + Comment lastComment = commentRepository + .findById(cursorId) .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); comments = commentRepository.findReplyNext(parentId, cursorId, lastComment.getCreatedAt(), pageable); } - Long nextCursor = comments.hasNext() ? comments.getContent().get(comments.getContent().size()-1).getId() : null; + Long nextCursor = comments.hasNext() + ? comments.getContent().get(comments.getContent().size() - 1).getId() + : null; return CommentResponse.CommentList.from(comments.getContent(), nextCursor, comments.hasNext()); } - /** - * 게시글 삭제 시 댓글 삭제(soft delete) - */ + /** 게시글 삭제 시 댓글 삭제(soft delete) */ @Transactional(propagation = REQUIRES_NEW) public void softDeleteByPostId(Long postId) { long deletedCount = commentRepository.softDeleteByPostId(postId, LocalDateTime.now()); diff --git a/src/main/java/com/payper/server/domain/test/TestAuthController.java b/src/main/java/com/payper/server/domain/test/TestAuthController.java index 347dded..e77c7ee 100644 --- a/src/main/java/com/payper/server/domain/test/TestAuthController.java +++ b/src/main/java/com/payper/server/domain/test/TestAuthController.java @@ -1,12 +1,11 @@ package com.payper.server.domain.test; +import java.security.Principal; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RestController("/auth-test") public class TestAuthController { @GetMapping("/") @@ -15,10 +14,7 @@ public String index() { } @GetMapping("/me") - public String getMe( - Principal principal, - @AuthenticationPrincipal UserDetails userDetails - ) { + public String getMe(Principal principal, @AuthenticationPrincipal UserDetails userDetails) { System.out.println("principal.getName() = " + principal.getName()); System.out.println("userDetails.username() = " + userDetails.getUsername()); System.out.println("userDetails.password() = " + userDetails.getPassword()); diff --git a/src/main/java/com/payper/server/domain/test/TestController.java b/src/main/java/com/payper/server/domain/test/TestController.java index 7d0d3d3..a2c02b0 100644 --- a/src/main/java/com/payper/server/domain/test/TestController.java +++ b/src/main/java/com/payper/server/domain/test/TestController.java @@ -4,14 +4,10 @@ import com.payper.server.global.response.ApiResponse; import com.payper.server.global.response.ErrorCode; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RestController @RequestMapping("/test") public class TestController { @@ -34,6 +30,4 @@ public ResponseEntity> testFailure2() { public ResponseEntity> testFailure3() throws Exception { throw new Exception(); } - - } diff --git a/src/main/java/com/payper/server/favorite/entity/Favorite.java b/src/main/java/com/payper/server/favorite/entity/Favorite.java index 9e98134..d928bff 100644 --- a/src/main/java/com/payper/server/favorite/entity/Favorite.java +++ b/src/main/java/com/payper/server/favorite/entity/Favorite.java @@ -10,28 +10,23 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table( name = "favorite", - uniqueConstraints = @UniqueConstraint( - name = "uk_favorite_user_merchant", - columnNames = {"user_id", "merchant_id"} - ) -) + uniqueConstraints = + @UniqueConstraint( + name = "uk_favorite_user_merchant", + columnNames = {"user_id", "merchant_id"})) public class Favorite { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 유저 - */ + /** 유저 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; - /** - * 대상 가맹점 - */ + /** 대상 가맹점 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "merchant_id", nullable = false) private Merchant merchant; -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/global/config/SwaggerConfig.java b/src/main/java/com/payper/server/global/config/SwaggerConfig.java index f63cf15..4f1a6c9 100644 --- a/src/main/java/com/payper/server/global/config/SwaggerConfig.java +++ b/src/main/java/com/payper/server/global/config/SwaggerConfig.java @@ -16,13 +16,11 @@ public OpenAPI openAPI() { String securitySchemeName = "bearerAuth"; return new OpenAPI() - .info(new Info() - .title("Payper API") - .version("v1") - .description("Payper 서버 API 문서")) + .info(new Info().title("Payper API").version("v1").description("Payper 서버 API 문서")) .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) .components(new Components() - .addSecuritySchemes(securitySchemeName, + .addSecuritySchemes( + securitySchemeName, new SecurityScheme() .name(securitySchemeName) .type(SecurityScheme.Type.HTTP) diff --git a/src/main/java/com/payper/server/global/entity/BaseTimeEntity.java b/src/main/java/com/payper/server/global/entity/BaseTimeEntity.java index b9cb5d1..e7824aa 100644 --- a/src/main/java/com/payper/server/global/entity/BaseTimeEntity.java +++ b/src/main/java/com/payper/server/global/entity/BaseTimeEntity.java @@ -3,13 +3,12 @@ import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.LocalDateTime; - @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @@ -22,4 +21,4 @@ public abstract class BaseTimeEntity { @LastModifiedDate @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/global/exception/GlobalExceptionHandler.java b/src/main/java/com/payper/server/global/exception/GlobalExceptionHandler.java index b877dd3..6f23273 100644 --- a/src/main/java/com/payper/server/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/payper/server/global/exception/GlobalExceptionHandler.java @@ -4,14 +4,13 @@ import com.payper.server.global.response.ApiResponse; import com.payper.server.global.response.ErrorCode; import com.payper.server.global.response.FieldErrorDto; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.util.List; - @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -19,7 +18,11 @@ public class GlobalExceptionHandler { // 비즈니스 예외 @ExceptionHandler(ApiException.class) public ResponseEntity> handleApiException(ApiException e) { - log.warn("[API_EXCEPTION] code={}, message={}", e.getErrorCode().getCode(), e.getErrorCode().getMessage(), e); + log.warn( + "[API_EXCEPTION] code={}, message={}", + e.getErrorCode().getCode(), + e.getErrorCode().getMessage(), + e); return buildErrorResponse(e.getErrorCode()); } @@ -32,23 +35,26 @@ public ResponseEntity> handleIllegalArgumentException(IllegalA // @Valid 전용 @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + public ResponseEntity>> handleMethodArgumentNotValidException( + MethodArgumentNotValidException e) { List fieldErrors = e.getBindingResult().getFieldErrors().stream() - .map(fieldError -> new FieldErrorDto - (fieldError.getField(), fieldError.getDefaultMessage())).toList(); + .map(fieldError -> new FieldErrorDto(fieldError.getField(), fieldError.getDefaultMessage())) + .toList(); log.warn("[VALIDATION_FAILED] errors={}", fieldErrors); ErrorCode errorCode = ErrorCode.BAD_REQUEST; - return ResponseEntity - .status(errorCode.getStatus()) - .body(ApiResponse.fail(errorCode, fieldErrors)); + return ResponseEntity.status(errorCode.getStatus()).body(ApiResponse.fail(errorCode, fieldErrors)); } // 시큐리티 필터 밖에서 발생한 Auth 예외 @ExceptionHandler(AuthException.class) public ResponseEntity> handleAuthException(AuthException e) { - log.warn("[AUTH_EXCEPTION] code={}, message={}", e.getErrorCode().getCode(), e.getErrorCode().getMessage(), e); + log.warn( + "[AUTH_EXCEPTION] code={}, message={}", + e.getErrorCode().getCode(), + e.getErrorCode().getMessage(), + e); return buildErrorResponse(e.getErrorCode()); } @@ -63,8 +69,6 @@ public ResponseEntity> handleException(Exception e) { // 공통 응답 생성 헬퍼 // ====================== private ResponseEntity> buildErrorResponse(ErrorCode errorCode) { - return ResponseEntity - .status(errorCode.getStatus()) - .body(ApiResponse.fail(errorCode)); + return ResponseEntity.status(errorCode.getStatus()).body(ApiResponse.fail(errorCode)); } } diff --git a/src/main/java/com/payper/server/global/response/ApiResponse.java b/src/main/java/com/payper/server/global/response/ApiResponse.java index e4a3dd7..0679cc0 100644 --- a/src/main/java/com/payper/server/global/response/ApiResponse.java +++ b/src/main/java/com/payper/server/global/response/ApiResponse.java @@ -12,14 +12,15 @@ public class ApiResponse { @Schema(description = "HTTP 상태 코드", example = "200") private final Integer status; + @Schema(description = "응답 데이터") private final T data; + @Schema(description = "에러 정보 (성공 시 null)") private final ExceptionDto error; public static ApiResponse ok() { - return ApiResponse - .builder() + return ApiResponse.builder() .status(HttpStatus.OK.value()) .data(null) .error(null) @@ -27,8 +28,7 @@ public static ApiResponse ok() { } public static ApiResponse ok(@Nullable T data) { - return ApiResponse - .builder() + return ApiResponse.builder() .status(HttpStatus.OK.value()) .data(data) .error(null) @@ -36,8 +36,7 @@ public static ApiResponse ok(@Nullable T data) { } public static ApiResponse created(@Nullable T data) { - return ApiResponse - .builder() + return ApiResponse.builder() .status(HttpStatus.CREATED.value()) .data(data) .error(null) @@ -45,8 +44,7 @@ public static ApiResponse created(@Nullable T data) { } public static ApiResponse fail(ErrorCode errorCode) { - return ApiResponse - .builder() + return ApiResponse.builder() .status(errorCode.getStatus().value()) .data(null) .error(ExceptionDto.of(errorCode)) @@ -54,8 +52,7 @@ public static ApiResponse fail(ErrorCode errorCode) { } public static ApiResponse fail(ErrorCode errorCode, T data) { - return ApiResponse - .builder() + return ApiResponse.builder() .status(errorCode.getStatus().value()) .data(data) .error(ExceptionDto.of(errorCode)) diff --git a/src/main/java/com/payper/server/global/response/ErrorCode.java b/src/main/java/com/payper/server/global/response/ErrorCode.java index 1d32df0..4a4b0ea 100644 --- a/src/main/java/com/payper/server/global/response/ErrorCode.java +++ b/src/main/java/com/payper/server/global/response/ErrorCode.java @@ -7,14 +7,13 @@ @Getter @AllArgsConstructor public enum ErrorCode { - //GENERAL + // GENERAL BAD_REQUEST("GEN-001", HttpStatus.BAD_REQUEST, "Bad Request"), NOT_FOUND("GEN-003", HttpStatus.NOT_FOUND, "Not Found"), CONFLICT("GEN-004", HttpStatus.CONFLICT, "Conflict"), INTERNAL_SERVER_ERROR("GEN-005", HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error"), - // AUTHENTICATION - JWT JWT_ERROR("JWT_001", HttpStatus.UNAUTHORIZED, "토큰 만료 외 예외"), JWT_EXPIRED("JWT_002", HttpStatus.UNAUTHORIZED, "토큰 만료"), @@ -23,7 +22,7 @@ public enum ErrorCode { JWT_REISSUE_OLD("JWT_005", HttpStatus.INTERNAL_SERVER_ERROR, "만료 리프레시 토큰 사용"), REISSUE_ERROR("JWT_006", HttpStatus.INTERNAL_SERVER_ERROR, "토큰 리이슈 중 예외"), - //AUTHENTICATION - GENERAL + // AUTHENTICATION - GENERAL UNAUTHENTICATED("SEC-001", HttpStatus.UNAUTHORIZED, "인증 필요"), UNAUTHORIZED("SEC-002", HttpStatus.FORBIDDEN, "접근권한 없음"), USER_DUPLICATE("SEC-003", HttpStatus.INTERNAL_SERVER_ERROR, "이미 가입 유저 존재"), @@ -32,7 +31,6 @@ public enum ErrorCode { OAUTH_RESOURCE_ERROR("OAUTH-001", HttpStatus.SERVICE_UNAVAILABLE, "OAuth 리소스 서버와 통신 중 예외"), - // USER USER_NOT_FOUND("USER-001", HttpStatus.NOT_FOUND, "User Not Found"), NOT_AN_ADMIN("USER-002", HttpStatus.FORBIDDEN, "Not An Admin"), @@ -56,8 +54,8 @@ public enum ErrorCode { CATEGORY_ALREADY_EXISTS("CATEGORY-002", HttpStatus.CONFLICT, "Category Already Exists"), CATEGORY_DEPTH_EXCEEDED("CATEGORY-003", HttpStatus.BAD_REQUEST, "Category Depth Exceeded"), CATEGORY_CANNOT_BE_SELF_PARENT("CATEGORY-004", HttpStatus.BAD_REQUEST, "Category Cannot Be Self Parent"), - PARENT_CATEGORY_CANNOT_HAVE_PARENT("CATEGORY-005", HttpStatus.BAD_REQUEST, "Parent Category Cannot Be Changed To A Child"), - + PARENT_CATEGORY_CANNOT_HAVE_PARENT( + "CATEGORY-005", HttpStatus.BAD_REQUEST, "Parent Category Cannot Be Changed To A Child"), ; private final String code; diff --git a/src/main/java/com/payper/server/global/response/ExceptionDto.java b/src/main/java/com/payper/server/global/response/ExceptionDto.java index fa4be1c..67a90ce 100644 --- a/src/main/java/com/payper/server/global/response/ExceptionDto.java +++ b/src/main/java/com/payper/server/global/response/ExceptionDto.java @@ -8,6 +8,7 @@ public class ExceptionDto { @Schema(description = "에러 코드", example = "NOT_FOUND") private final String code; + @Schema(description = "에러 메시지", example = "해당 리소스를 찾을 수 없습니다.") private final String message; diff --git a/src/main/java/com/payper/server/global/response/FieldErrorDto.java b/src/main/java/com/payper/server/global/response/FieldErrorDto.java index 0322508..384beba 100644 --- a/src/main/java/com/payper/server/global/response/FieldErrorDto.java +++ b/src/main/java/com/payper/server/global/response/FieldErrorDto.java @@ -10,6 +10,7 @@ public class FieldErrorDto { @Schema(description = "에러 발생 필드명", example = "title") private final String field; + @Schema(description = "에러 메시지", example = "제목을 적어주세요.") private final String message; } diff --git a/src/main/java/com/payper/server/merchant/controller/CategoryApi.java b/src/main/java/com/payper/server/merchant/controller/CategoryApi.java index fd2d90c..a3bc310 100644 --- a/src/main/java/com/payper/server/merchant/controller/CategoryApi.java +++ b/src/main/java/com/payper/server/merchant/controller/CategoryApi.java @@ -8,28 +8,26 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; - import java.util.List; +import org.springframework.http.ResponseEntity; @Tag(name = "카테고리", description = "카테고리 등록/수정/조회 API") public interface CategoryApi { @Operation(summary = "카테고리 등록", description = "관리자만 등록 가능. depth는 최대 2.") @SecurityRequirement(name = "bearerAuth") - ResponseEntity> registerCategory( - @RequestBody CategoryRequest.RegisterCategory request - ); + ResponseEntity> registerCategory(@RequestBody CategoryRequest.RegisterCategory request); - @Operation(summary = "카테고리 수정", description = "관리자만 수정 가능. 부모 카테고리는 이름만, 자식 카테고리는 이름과 부모 변경 가능.") + @Operation(summary = "카테고리 수정", description = "관리자만 수정 가능. 부모 카테고리는 이름만, 자식 카테고리는 이름과 부모 변경 가능. depth는 수정할 수 없음") @SecurityRequirement(name = "bearerAuth") ResponseEntity> updateCategory( @Parameter(description = "카테고리 ID", required = true, example = "1") Long categoryId, - @RequestBody CategoryRequest.UpdateCategory request - ); + @RequestBody CategoryRequest.UpdateCategory request); - @Operation(summary = "카테고리 조회", description = "부모 카테고리 ID로 필터링 가능. 파라미터 미지정 시 부모 카테고리만 조회. 카테고리명 오름차순 정렬.", security = {}) + @Operation( + summary = "카테고리 조회", + description = "부모 카테고리 ID로 필터링 가능. 파라미터 미지정 시 부모 카테고리만 조회. 카테고리명 오름차순 정렬.", + security = {}) ResponseEntity>> getCategories( - @Parameter(description = "부모 카테고리 ID 필터", required = false) Long parentCategoryId - ); -} \ No newline at end of file + @Parameter(description = "부모 카테고리 ID 필터", required = false) Long parentCategoryId); +} diff --git a/src/main/java/com/payper/server/merchant/controller/CategoryController.java b/src/main/java/com/payper/server/merchant/controller/CategoryController.java index 8f6c22f..a0d062e 100644 --- a/src/main/java/com/payper/server/merchant/controller/CategoryController.java +++ b/src/main/java/com/payper/server/merchant/controller/CategoryController.java @@ -5,65 +5,42 @@ import com.payper.server.merchant.dto.CategoryResponse; import com.payper.server.merchant.service.CategoryService; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequestMapping("/api/v1/categories") @RequiredArgsConstructor public class CategoryController implements CategoryApi { private final CategoryService categoryService; - /** - * 카테고리 등록 - * 관리자만 등록 가능 - * depth는 최대 2 - */ + /** 카테고리 등록 */ @PreAuthorize("hasRole('ADMIN')") @PostMapping() public ResponseEntity> registerCategory( - @RequestBody @Valid CategoryRequest.RegisterCategory request - ) { + @RequestBody @Valid CategoryRequest.RegisterCategory request) { Long categoryId = categoryService.registerCategory(request); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(categoryId)); } - /** - * 카테고리 수정 (depth는 수정할 수 없음) - * 관리자만 수정 가능 - * 부모 카테고리 -> 이름만 변경 가능 - * 자식 카테고리 -> 이름, 부모 변경 가능 - */ + /** 카테고리 수정 */ @PreAuthorize("hasRole('ADMIN')") @PutMapping("/{categoryId}") public ResponseEntity> updateCategory( - @PathVariable Long categoryId, - @RequestBody @Valid CategoryRequest.UpdateCategory request - ) { + @PathVariable Long categoryId, @RequestBody @Valid CategoryRequest.UpdateCategory request) { categoryService.updateCategory(categoryId, request); return ResponseEntity.ok(ApiResponse.ok()); } - /** - * 카테고리 조회 - * - * 필터링 조건 - * 부모 카테고리 - * 파라미터 안 넣으면 부모 카테고리만 보임 - * - * 정렬 조건 - * 카테고리명, 오름차순 - */ + /** 카테고리 조회 */ @GetMapping() public ResponseEntity>> getCategories( - @RequestParam(required = false) Long parentCategoryId - ) { + @RequestParam(required = false) Long parentCategoryId) { List response = categoryService.getCategories(parentCategoryId); return ResponseEntity.ok(ApiResponse.ok(response)); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/merchant/controller/MerchantApi.java b/src/main/java/com/payper/server/merchant/controller/MerchantApi.java index 6b8e80b..0294124 100644 --- a/src/main/java/com/payper/server/merchant/controller/MerchantApi.java +++ b/src/main/java/com/payper/server/merchant/controller/MerchantApi.java @@ -10,36 +10,33 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; - import java.util.List; +import org.springframework.http.ResponseEntity; @Tag(name = "가맹점", description = "가맹점 등록/수정/조회 및 게시글 작성 API") public interface MerchantApi { @Operation(summary = "가맹점 등록", description = "관리자만 등록 가능. 카테고리를 선택하여 가맹점을 등록합니다.") @SecurityRequirement(name = "bearerAuth") - ResponseEntity> registerMerchant( - @RequestBody MerchantRequest.RegisterMerchant request - ); + ResponseEntity> registerMerchant(@RequestBody MerchantRequest.RegisterMerchant request); @Operation(summary = "가맹점 수정", description = "관리자만 수정 가능") @SecurityRequirement(name = "bearerAuth") ResponseEntity> updateMerchant( @Parameter(description = "가맹점 ID", required = true, example = "1") Long merchantId, - @RequestBody MerchantRequest.UpdateMerchant request - ); + @RequestBody MerchantRequest.UpdateMerchant request); - @Operation(summary = "가맹점 조회", description = "카테고리 ID로 필터링 가능. 가맹점명 오름차순 정렬.", security = {}) + @Operation( + summary = "가맹점 조회", + description = "카테고리 ID로 필터링 가능. 가맹점명 오름차순 정렬.", + security = {}) ResponseEntity>> getMerchants( - @Parameter(description = "카테고리 ID 필터", required = false) Long categoryId - ); + @Parameter(description = "카테고리 ID 필터", required = false) Long categoryId); @Operation(summary = "게시글 작성", description = "가맹점에 대한 게시글을 작성합니다.") @SecurityRequirement(name = "bearerAuth") ResponseEntity> createPost( CustomUserDetails user, @Parameter(description = "가맹점 ID", example = "1", required = true) Long merchantId, - @RequestBody PostRequest.CreatePost request - ); + @RequestBody PostRequest.CreatePost request); } diff --git a/src/main/java/com/payper/server/merchant/controller/MerchantController.java b/src/main/java/com/payper/server/merchant/controller/MerchantController.java index f1bb6f0..3cd317e 100644 --- a/src/main/java/com/payper/server/merchant/controller/MerchantController.java +++ b/src/main/java/com/payper/server/merchant/controller/MerchantController.java @@ -8,6 +8,7 @@ import com.payper.server.post.service.PostService; import com.payper.server.security.CustomUserDetails; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,8 +16,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequestMapping("/api/v1/merchants") @RequiredArgsConstructor @@ -24,67 +23,39 @@ public class MerchantController implements MerchantApi { private final MerchantService merchantService; private final PostService postService; - /** - * 가맹점 등록 - * - * 관리자만 등록 가능 - * 카테고리 리스트에서 카테고리를 선택해서 해당 카테고리의 id를 넘겨 받음 - */ + /** 가맹점 등록 */ @PreAuthorize("hasRole('ADMIN')") @PostMapping() public ResponseEntity> registerMerchant( - @RequestBody @Valid MerchantRequest.RegisterMerchant request - ) { + @RequestBody @Valid MerchantRequest.RegisterMerchant request) { Long merchantId = merchantService.registerMerchant(request); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(merchantId)); } - /** - * 가맹점 수정 - * 관리자만 수정 가능 - */ + /** 가맹점 수정 */ @PreAuthorize("hasRole('ADMIN')") @PutMapping("/{merchantId}") public ResponseEntity> updateMerchant( - @PathVariable Long merchantId, - @RequestBody @Valid MerchantRequest.UpdateMerchant request - ) { + @PathVariable Long merchantId, @RequestBody @Valid MerchantRequest.UpdateMerchant request) { merchantService.updateMerchant(merchantId, request); return ResponseEntity.ok(ApiResponse.ok()); } - /** - * 가맹점 조회 - * - * 필터링 조건 - * 카테고리 - * - * 정렬 조건 - * 가맹점명, 오름차순 - */ + /** 가맹점 조회 */ @GetMapping() public ResponseEntity>> getMerchants( - @RequestParam(required = false) Long categoryId - ) { + @RequestParam(required = false) Long categoryId) { List response = merchantService.getMerchants(categoryId); return ResponseEntity.ok(ApiResponse.ok(response)); } - /** - * 게시글 작성 - * 가맹점에 대해 글을 작성함 - * 가맹점 리스트에서 가맹점을 선택해서 해당 가맹점의 id를 넘겨 받음 - * TODO 가맹점이 없을 때는 어떻게 해야할까? - * - * 가입된 사용자만 글을 작성할 수 있음 - */ + /** 게시글 작성 TODO 가맹점이 없을 때는 어떻게 해야할까? */ @PostMapping("/{merchantId}/posts") public ResponseEntity> createPost( @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long merchantId, - @RequestBody @Valid PostRequest.CreatePost request - ) { + @RequestBody @Valid PostRequest.CreatePost request) { Long postId = postService.createPost(user.getId(), merchantId, request); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(postId)); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/merchant/dto/CategoryRequest.java b/src/main/java/com/payper/server/merchant/dto/CategoryRequest.java index 12197a1..865bce6 100644 --- a/src/main/java/com/payper/server/merchant/dto/CategoryRequest.java +++ b/src/main/java/com/payper/server/merchant/dto/CategoryRequest.java @@ -6,31 +6,21 @@ public class CategoryRequest { - /** - * 카테고리 등록 DTO - */ + /** 카테고리 등록 DTO */ @Schema(description = "카테고리 등록 요청") public record RegisterCategory( - @Schema(description = "카테고리명", example = "카페") - @NotBlank(message = "카테고리명을 적어주세요. 예) 카페") + @Schema(description = "카테고리명", example = "카페") @NotBlank(message = "카테고리명을 적어주세요. 예) 카페") String name, - @Schema(description = "부모 카테고리 ID (하위 카테고리 등록 시)", example = "1", nullable = true) - @Nullable - Long parentCategoryId - ) {} + @Schema(description = "부모 카테고리 ID (하위 카테고리 등록 시)", example = "1", nullable = true) @Nullable + Long parentCategoryId) {} - /** - * 카테고리 수정 DTO - */ + /** 카테고리 수정 DTO */ @Schema(description = "카테고리 수정 요청") public record UpdateCategory( - @Schema(description = "카테고리명", example = "카페") - @NotBlank(message = "카테고리명을 적어주세요. 예) 카페") + @Schema(description = "카테고리명", example = "카페") @NotBlank(message = "카테고리명을 적어주세요. 예) 카페") String name, - @Schema(description = "부모 카테고리 ID (하위 카테고리의 부모 변경 시)", example = "1", nullable = true) - @Nullable - Long parentCategoryId - ) {} -} \ No newline at end of file + @Schema(description = "부모 카테고리 ID (하위 카테고리의 부모 변경 시)", example = "1", nullable = true) @Nullable + Long parentCategoryId) {} +} diff --git a/src/main/java/com/payper/server/merchant/dto/CategoryResponse.java b/src/main/java/com/payper/server/merchant/dto/CategoryResponse.java index 8b76364..2993506 100644 --- a/src/main/java/com/payper/server/merchant/dto/CategoryResponse.java +++ b/src/main/java/com/payper/server/merchant/dto/CategoryResponse.java @@ -4,20 +4,15 @@ public class CategoryResponse { - /** - * Category Item - */ - public record CategoryItem( - Long id, - String name, - Long parentCategoryId - ) { + /** Category Item */ + public record CategoryItem(Long id, String name, Long parentCategoryId) { public static CategoryResponse.CategoryItem from(Category category) { return new CategoryResponse.CategoryItem( category.getId(), category.getName(), - category.getParentCategory() != null ? category.getParentCategory().getId() : null - ); + category.getParentCategory() != null + ? category.getParentCategory().getId() + : null); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/merchant/dto/MerchantRequest.java b/src/main/java/com/payper/server/merchant/dto/MerchantRequest.java index 9c3fe26..778ec5e 100644 --- a/src/main/java/com/payper/server/merchant/dto/MerchantRequest.java +++ b/src/main/java/com/payper/server/merchant/dto/MerchantRequest.java @@ -7,35 +7,24 @@ public class MerchantRequest { - /** - * 가맹점 등록 DTO - */ + /** 가맹점 등록 DTO */ @Schema(description = "가맹점 등록 요청") public record RegisterMerchant( - @Schema(description = "가맹점명", example = "스타벅스") - @NotBlank(message = "가맹점 명을 적어주세요. 예) 스타벅스") + @Schema(description = "가맹점명", example = "스타벅스") @NotBlank(message = "가맹점 명을 적어주세요. 예) 스타벅스") String name, - @Schema(description = "카테고리 ID", example = "1") - @NotNull(message = "카테고리 ID는 필수입니다.") + @Schema(description = "카테고리 ID", example = "1") @NotNull(message = "카테고리 ID는 필수입니다.") Long categoryId, - @Schema(description = "가맹점 이미지 URL", example = "https://example.com/image.png", nullable = true) - @Nullable - String imageUrl - ) {} + @Schema(description = "가맹점 이미지 URL", example = "https://example.com/image.png", nullable = true) @Nullable + String imageUrl) {} - /** - * 가맹점 수정 DTO - */ + /** 가맹점 수정 DTO */ @Schema(description = "가맹점 수정 요청") public record UpdateMerchant( - @Schema(description = "가맹점명", example = "스타벅스") - @NotBlank(message = "가맹점 명을 적어주세요. 예) 스타벅스") + @Schema(description = "가맹점명", example = "스타벅스") @NotBlank(message = "가맹점 명을 적어주세요. 예) 스타벅스") String name, - @Schema(description = "가맹점 이미지 URL", example = "https://example.com/image.png", nullable = true) - @Nullable - String imageUrl - ) {} -} \ No newline at end of file + @Schema(description = "가맹점 이미지 URL", example = "https://example.com/image.png", nullable = true) @Nullable + String imageUrl) {} +} diff --git a/src/main/java/com/payper/server/merchant/dto/MerchantResponse.java b/src/main/java/com/payper/server/merchant/dto/MerchantResponse.java index 4a29e25..6c03706 100644 --- a/src/main/java/com/payper/server/merchant/dto/MerchantResponse.java +++ b/src/main/java/com/payper/server/merchant/dto/MerchantResponse.java @@ -4,24 +4,15 @@ public class MerchantResponse { - /** - * Merchant Item - */ - public record MerchantItem( - Long id, - String name, - String imageUrl, - Long categoryId, - String categoryName - ) { + /** Merchant Item */ + public record MerchantItem(Long id, String name, String imageUrl, Long categoryId, String categoryName) { public static MerchantItem from(Merchant merchant) { return new MerchantItem( merchant.getId(), merchant.getName(), merchant.getImageUrl(), merchant.getCategory().getId(), - merchant.getCategory().getName() - ); + merchant.getCategory().getName()); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/merchant/entity/Category.java b/src/main/java/com/payper/server/merchant/entity/Category.java index bb822c7..3e1c407 100644 --- a/src/main/java/com/payper/server/merchant/entity/Category.java +++ b/src/main/java/com/payper/server/merchant/entity/Category.java @@ -14,55 +14,38 @@ public class Category { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 카테고리명 - */ + /** 카테고리명 */ @Column(nullable = false, unique = true) private String name; - /** - * 부모 카테고리 - */ + /** 부모 카테고리 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_category_id") private Category parentCategory; - /** - * 부모 카테고리인가? - */ + /** 부모 카테고리인가? */ public boolean isRoot() { return parentCategory == null; } - /** - * 자식 카테고리인가? - */ + /** 자식 카테고리인가? */ public boolean isDepth2() { return parentCategory != null; } - /** - * 카테고리 등록 - */ + /** 카테고리 등록 */ public static Category register(String name, Category parentCategory) { - return Category.builder() - .name(name) - .parentCategory(parentCategory) - .build(); + return Category.builder().name(name).parentCategory(parentCategory).build(); } - /** - * 이름 AND 부모 변경 - */ + /** 이름 AND 부모 변경 */ public void updateNameAndParentCategory(String name, Category parentCategory) { this.name = name; this.parentCategory = parentCategory; } - /** - * 이름 변경 - */ + /** 이름 변경 */ public void updateName(String name) { this.name = name; } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/merchant/entity/Merchant.java b/src/main/java/com/payper/server/merchant/entity/Merchant.java index a9e1742..450ae89 100644 --- a/src/main/java/com/payper/server/merchant/entity/Merchant.java +++ b/src/main/java/com/payper/server/merchant/entity/Merchant.java @@ -15,29 +15,20 @@ public class Merchant { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 가맹점명 - */ + /** 가맹점명 */ @Column(nullable = false, unique = true) private String name; - /** - * 카테고리 - */ + /** 카테고리 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id", nullable = false) private Category category; - /** - * 가맹점 이미지 URL - * TODO: 가맹점 등록할 때 무조건 이미지 url 넣도록 하고 만약 넣지 않는다면 default 이미지를 보여주도록 함 - */ + /** 가맹점 이미지 URL TODO: 가맹점 등록할 때 무조건 이미지 url 넣도록 하고 만약 넣지 않는다면 default 이미지를 보여주도록 함 */ @Column(name = "image_url") private String imageUrl; - /** - * 가맹점 등록 - */ + /** 가맹점 등록 */ public static Merchant register(String name, Category category, String imageUrl) { return Merchant.builder() .name(name) @@ -46,13 +37,11 @@ public static Merchant register(String name, Category category, String imageUrl) .build(); } - /** - * 가맹점 수정 - */ + /** 가맹점 수정 */ public void update(String name, @Nullable String imageUrl) { this.name = name; if (imageUrl != null) { this.imageUrl = imageUrl; } } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/merchant/repository/CategoryRepository.java b/src/main/java/com/payper/server/merchant/repository/CategoryRepository.java index 43b7b05..6658ac6 100644 --- a/src/main/java/com/payper/server/merchant/repository/CategoryRepository.java +++ b/src/main/java/com/payper/server/merchant/repository/CategoryRepository.java @@ -1,11 +1,10 @@ package com.payper.server.merchant.repository; import com.payper.server.merchant.entity.Category; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface CategoryRepository extends JpaRepository { boolean existsByName(String name); @@ -17,5 +16,4 @@ public interface CategoryRepository extends JpaRepository { List findByParentCategoryIsNullOrderByNameAsc(); List findByParentCategoryIdOrderByNameAsc(Long parentCategoryId); - } diff --git a/src/main/java/com/payper/server/merchant/repository/MerchantRepository.java b/src/main/java/com/payper/server/merchant/repository/MerchantRepository.java index 63e7e8c..2684ddb 100644 --- a/src/main/java/com/payper/server/merchant/repository/MerchantRepository.java +++ b/src/main/java/com/payper/server/merchant/repository/MerchantRepository.java @@ -1,13 +1,12 @@ package com.payper.server.merchant.repository; import com.payper.server.merchant.entity.Merchant; +import java.util.List; 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.List; - @Repository public interface MerchantRepository extends JpaRepository { boolean existsByName(String name); diff --git a/src/main/java/com/payper/server/merchant/service/CategoryService.java b/src/main/java/com/payper/server/merchant/service/CategoryService.java index 70db1d4..f96e460 100644 --- a/src/main/java/com/payper/server/merchant/service/CategoryService.java +++ b/src/main/java/com/payper/server/merchant/service/CategoryService.java @@ -6,14 +6,13 @@ import com.payper.server.merchant.dto.CategoryResponse; import com.payper.server.merchant.entity.Category; import com.payper.server.merchant.repository.CategoryRepository; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor @@ -21,9 +20,7 @@ public class CategoryService { private final CategoryRepository categoryRepository; - /** - * 카테고리 등록 - */ + /** 카테고리 등록 */ @Transactional public Long registerCategory(CategoryRequest.RegisterCategory request) { // 존재하는 카테고리명인지 체크 @@ -55,19 +52,19 @@ public Long registerCategory(CategoryRequest.RegisterCategory request) { // 요청 body로 받은 parent category id 검증 private Category getValidatedParentCategory(Long parentCategoryId) { // 부모 카테고리가 존재하는 카테고리인지 확인 - Category parentCategory = categoryRepository.findById(parentCategoryId) + Category parentCategory = categoryRepository + .findById(parentCategoryId) .orElseThrow(() -> new ApiException(ErrorCode.CATEGORY_NOT_FOUND)); return parentCategory; } - /** - * 카테고리 수정 - */ + /** 카테고리 수정 */ @Transactional public void updateCategory(Long categoryId, CategoryRequest.UpdateCategory request) { // 카테고리 조회 - Category category = categoryRepository.findById(categoryId) + Category category = categoryRepository + .findById(categoryId) .orElseThrow(() -> new ApiException(ErrorCode.CATEGORY_NOT_FOUND)); // 존재하는 카테고리명인지 체크 (나 제외) @@ -96,7 +93,8 @@ public void updateCategory(Long categoryId, CategoryRequest.UpdateCategory reque // 자식 카테고리인 경우 2. 이름, 부모 모두 바꾸는 경우 // 변경하고 싶은 부모 카테고리가 존재하는 카테고리인지 확인 - Category newParentCategory = categoryRepository.findById(request.parentCategoryId()) + Category newParentCategory = categoryRepository + .findById(request.parentCategoryId()) .orElseThrow(() -> new ApiException(ErrorCode.CATEGORY_NOT_FOUND)); // 자기 자신 불가 @@ -112,9 +110,7 @@ public void updateCategory(Long categoryId, CategoryRequest.UpdateCategory reque category.updateNameAndParentCategory(request.name(), newParentCategory); } - /** - * 카테고리 리스트 조회 - */ + /** 카테고리 리스트 조회 */ @Transactional(readOnly = true) public List getCategories(Long parentCategoryId) { List categories; @@ -127,8 +123,6 @@ public List getCategories(Long parentCategoryId) categories = categoryRepository.findByParentCategoryIdOrderByNameAsc(parentCategoryId); } - return categories.stream() - .map(CategoryResponse.CategoryItem::from) - .toList(); + return categories.stream().map(CategoryResponse.CategoryItem::from).toList(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/merchant/service/MerchantService.java b/src/main/java/com/payper/server/merchant/service/MerchantService.java index 70ab81a..4cdf155 100644 --- a/src/main/java/com/payper/server/merchant/service/MerchantService.java +++ b/src/main/java/com/payper/server/merchant/service/MerchantService.java @@ -8,14 +8,13 @@ import com.payper.server.merchant.entity.Merchant; import com.payper.server.merchant.repository.CategoryRepository; import com.payper.server.merchant.repository.MerchantRepository; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor @@ -24,13 +23,12 @@ public class MerchantService { private final MerchantRepository merchantRepository; private final CategoryRepository categoryRepository; - /** - * 가맹점 등록 - */ + /** 가맹점 등록 */ @Transactional public Long registerMerchant(MerchantRequest.RegisterMerchant request) { // 카테고리 조회 - Category category = categoryRepository.findById(request.categoryId()) + Category category = categoryRepository + .findById(request.categoryId()) .orElseThrow(() -> new ApiException(ErrorCode.CATEGORY_NOT_FOUND)); // 존재하는 가맹점명인지 체크 @@ -50,13 +48,12 @@ public Long registerMerchant(MerchantRequest.RegisterMerchant request) { return merchant.getId(); } - /** - * 가맹점 수정 - */ + /** 가맹점 수정 */ @Transactional public void updateMerchant(Long merchantId, MerchantRequest.UpdateMerchant request) { // 가맹점 조회 - Merchant merchant = merchantRepository.findById(merchantId) + Merchant merchant = merchantRepository + .findById(merchantId) .orElseThrow(() -> new ApiException(ErrorCode.MERCHANT_NOT_FOUND)); // 존재하는 가맹점명인지 체크 (나 제외) @@ -70,9 +67,7 @@ public void updateMerchant(Long merchantId, MerchantRequest.UpdateMerchant reque log.info("가맹점 수정 완료 - merchantId: {}", merchant.getId()); } - /** - * 가맹점 리스트 조회 - */ + /** 가맹점 리스트 조회 */ @Transactional(readOnly = true) public List getMerchants(Long categoryId) { if (categoryId != null && !categoryRepository.existsById(categoryId)) { @@ -81,8 +76,6 @@ public List getMerchants(Long categoryId) { List merchants = merchantRepository.findMerchants(categoryId); - return merchants.stream() - .map(MerchantResponse.MerchantItem::from) - .toList(); + return merchants.stream().map(MerchantResponse.MerchantItem::from).toList(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/post/controller/PostApi.java b/src/main/java/com/payper/server/post/controller/PostApi.java index fda53b7..04e9b2b 100644 --- a/src/main/java/com/payper/server/post/controller/PostApi.java +++ b/src/main/java/com/payper/server/post/controller/PostApi.java @@ -24,43 +24,48 @@ public interface PostApi { ResponseEntity> updatePost( CustomUserDetails user, @Parameter(description = "게시글 ID", example = "1") Long postId, - PostRequest.UpdatePost request - ); + PostRequest.UpdatePost request); @Operation(summary = "게시글 삭제", description = "작성자만 삭제 가능 (소프트 삭제)") @SecurityRequirement(name = "bearerAuth") ResponseEntity> deletePost( - CustomUserDetails user, - @Parameter(description = "게시글 ID", example = "1") Long postId - ); + CustomUserDetails user, @Parameter(description = "게시글 ID", example = "1") Long postId); - @Operation(summary = "게시글 상세 조회", description = "삭제되지 않은 게시글만 조회 가능", security = {}) + @Operation( + summary = "게시글 상세 조회", + description = "삭제되지 않은 게시글만 조회 가능", + security = {}) ResponseEntity> getPostDetail( - @Parameter(description = "게시글 ID", example = "1") Long postId - ); + @Parameter(description = "게시글 ID", example = "1") Long postId); - @Operation(summary = "게시글 목록 조회", description = "가맹점/타입 필터링, 정렬, 오프셋 기반 페이지네이션을 지원합니다.", security = {}) + @Operation( + summary = "게시글 목록 조회", + description = "가맹점/타입 필터링, 정렬, 오프셋 기반 페이지네이션을 지원합니다.", + security = {}) ResponseEntity>> getPosts( @Parameter(description = "가맹점 ID 필터") Long merchantId, @Parameter(description = "게시글 타입 필터 (BENEFIT, QUESTION, ETC)") PostType type, @Parameter(description = "정렬 기준 (POSTING_DATE, COMMENT_COUNT, LIKE_COUNT, VIEW_COUNT)") PostSortType sort, @Parameter(description = "정렬 방향") Sort.Direction direction, @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") int page, - @Parameter(description = "페이지 크기", example = "10") int size - ); + @Parameter(description = "페이지 크기", example = "10") int size); - @Operation(summary = "댓글 작성", description = "게시글에 댓글을 작성합니다. 대댓글은 parentCommentId를 지정합니다.") + @Operation( + summary = "댓글 작성", + description = + "게시글에 댓글을 작성합니다. 대댓글은 parentCommentId를 지정합니다. 부모 댓글이 삭제되어도 대댓글 작성 허용 삭제 또는 비활성화된 post에는 댓글 작성 불가") @SecurityRequirement(name = "bearerAuth") ResponseEntity> createComment( CustomUserDetails user, @Parameter(description = "게시글 ID", example = "1") Long postId, - CommentRequest.CreateComment request - ); + CommentRequest.CreateComment request); - @Operation(summary = "게시글 댓글 조회", description = "부모 댓글 기준 커서 페이지네이션. 삭제된 부모 댓글도 자식이 있으면 '[삭제된 댓글입니다]'로 표시됩니다.", security = {}) + @Operation( + summary = "게시글 댓글 조회", + description = "부모 댓글 기준 커서 페이지네이션. 삭제된 부모 댓글도 자식이 있으면 '[삭제된 댓글입니다]'로 표시됩니다.", + security = {}) ResponseEntity> getPostComments( @Parameter(description = "게시글 ID", example = "1") Long postId, @Parameter(description = "마지막 조회 댓글 ID (첫 요청 시 생략)") Long cursorId, - @Parameter(description = "조회 개수", example = "20") int size - ); + @Parameter(description = "조회 개수", example = "20") int size); } diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index 287ebc7..e5780f8 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -28,57 +28,32 @@ public class PostController implements PostApi { private final PostService postService; private final CommentService commentService; - /** - * 게시글 수정 - * 작성자만 수정 가능 - */ + /** 게시글 수정 */ @PutMapping("/{postId}") public ResponseEntity> updatePost( @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId, - @RequestBody @Valid PostRequest.UpdatePost request - ) { + @RequestBody @Valid PostRequest.UpdatePost request) { postService.updatePost(user.getId(), postId, request); return ResponseEntity.ok(ApiResponse.ok()); } - /** - * 게시글 삭제 - * 작성자만 삭제 가능 - */ + /** 게시글 삭제 */ @DeleteMapping("/{postId}") public ResponseEntity> deletePost( - @AuthenticationPrincipal CustomUserDetails user, - @PathVariable Long postId - ) { + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId) { postService.deletePost(user.getId(), postId); return ResponseEntity.ok(ApiResponse.ok()); } - /** - * 단일 게시글 조회 - * - * 삭제되지 않은 글만 조회함 - */ + /** 단일 게시글 조회 */ @GetMapping("/{postId}") - public ResponseEntity> getPostDetail( - @PathVariable Long postId - ) { + public ResponseEntity> getPostDetail(@PathVariable Long postId) { PostResponse.PostDetail response = postService.getPostDetail(postId); return ResponseEntity.ok(ApiResponse.ok(response)); } - /** - * 게시글 리스트 조회 - * - * 필터링 조건 - * 가맹점, postType - * - * 정렬 조건 - * 생성 순, 댓글 수, 좋아요 수, 조회 수 - * - * 페이지네이션 - */ + /** 게시글 리스트 조회 */ @GetMapping() public ResponseEntity>> getPosts( @RequestParam(required = false) Long merchantId, @@ -86,41 +61,28 @@ public ResponseEntity>> getPosts( @RequestParam(defaultValue = "POSTING_DATE") PostSortType sort, @RequestParam(defaultValue = "DESC") Sort.Direction direction, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size - ) { + @RequestParam(defaultValue = "10") int size) { Pageable pageable = PageRequest.of(page, size, sort.toSort(direction)); Page response = postService.getPosts(merchantId, type, pageable); return ResponseEntity.ok(ApiResponse.ok(response)); } - /** - * 댓글 작성 - * is inactive = false, is deleted = false 상태의 post에만 댓글을 작성할 수 있음 - * - * 부모 댓글이 삭제되어도 대댓글 작성 허용 - */ + /** 댓글 작성 */ @PostMapping("/{postId}/comments") public ResponseEntity> createComment( @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId, - @RequestBody @Valid CommentRequest.CreateComment request - ) { + @RequestBody @Valid CommentRequest.CreateComment request) { Long commentId = commentService.createComment(user.getId(), postId, request); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(commentId)); } - /** - * 게시글 댓글 조회 - * - * 부모 댓글로 페이지네이션 - * 주의) 부모 댓글이 삭제되어도 자식 댓글이 남아있으면 [삭제된 댓글입니다]로 제공 - */ + /** 게시글 댓글 조회 */ @GetMapping("/{postId}/comments") public ResponseEntity> getPostComments( @PathVariable Long postId, @RequestParam(required = false) Long cursorId, - @RequestParam(defaultValue = "20") int size - ) { + @RequestParam(defaultValue = "20") int size) { CommentResponse.CommentList response = commentService.getPostComments(postId, cursorId, size); return ResponseEntity.ok(ApiResponse.ok(response)); } diff --git a/src/main/java/com/payper/server/post/dto/PostRequest.java b/src/main/java/com/payper/server/post/dto/PostRequest.java index e5134d8..3a87a17 100644 --- a/src/main/java/com/payper/server/post/dto/PostRequest.java +++ b/src/main/java/com/payper/server/post/dto/PostRequest.java @@ -8,37 +8,29 @@ public class PostRequest { - /** - * 게시글 작성 DTO - */ + /** 게시글 작성 DTO */ @Schema(description = "게시글 작성 요청") public record CreatePost( @Schema(description = "게시글 타입 : BENEFIT|QUESTION|ETC ", example = "BENEFIT") @NotNull(message = "게시글의 타입을 선택해주세요.") PostType type, - @Schema(description = "게시글 제목", example = "맛있는 맛집 추천합니다") - @NotBlank(message = "제목을 적어주세요.") + @Schema(description = "게시글 제목", example = "맛있는 맛집 추천합니다") @NotBlank(message = "제목을 적어주세요.") String title, @Schema(description = "게시글 내용", example = "여기 정말 맛있어요!") @NotBlank(message = "내용을 적어주세요.") @Size(max = 5500000, message = "내용은 500만자 이내로 적어주세요.") - String content - ) {} + String content) {} - /** - * 게시글 수정 DTO - */ + /** 게시글 수정 DTO */ @Schema(description = "게시글 수정 요청") public record UpdatePost( - @Schema(description = "수정할 제목", example = "수정된 제목입니다") - @NotBlank(message = "제목을 적어주세요.") + @Schema(description = "수정할 제목", example = "수정된 제목입니다") @NotBlank(message = "제목을 적어주세요.") String title, @Schema(description = "수정할 내용", example = "수정된 내용입니다") @NotBlank(message = "내용을 적어주세요.") @Size(max = 5500000, message = "내용은 500만자 이내로 적어주세요.") - String content - ) {} + String content) {} } diff --git a/src/main/java/com/payper/server/post/dto/PostResponse.java b/src/main/java/com/payper/server/post/dto/PostResponse.java index 4f47bc6..2d82562 100644 --- a/src/main/java/com/payper/server/post/dto/PostResponse.java +++ b/src/main/java/com/payper/server/post/dto/PostResponse.java @@ -3,39 +3,31 @@ import com.payper.server.post.entity.Post; import com.payper.server.post.entity.PostType; import io.swagger.v3.oas.annotations.media.Schema; - import java.time.LocalDateTime; public class PostResponse { - /** - * 게시글 단일 조회 DTO - */ + /** 게시글 단일 조회 DTO */ @Schema(description = "게시글 상세 응답") public record PostDetail( - @Schema(description = "게시글 ID", example = "1") - Long id, - @Schema(description = "작성자 이름", example = "홍길동") - String authorName, - @Schema(description = "가맹점명", example = "스타벅스") - String merchantName, + @Schema(description = "게시글 ID", example = "1") Long id, + @Schema(description = "작성자 이름", example = "홍길동") String authorName, + @Schema(description = "가맹점명", example = "스타벅스") String merchantName, + @Schema(description = "게시글 타입", example = "BENEFIT") PostType type, + @Schema(description = "제목", example = "맛있는 맛집 추천합니다") String title, + @Schema(description = "내용", example = "여기 정말 맛있어요!") String content, - @Schema(description = "댓글 수", example = "5") - long commentCount, - @Schema(description = "조회 수", example = "100") - long viewCount, - @Schema(description = "좋아요 수", example = "10") - long likeCount, - @Schema(description = "작성일시") - LocalDateTime createdAt, - @Schema(description = "수정일시") - LocalDateTime updatedAt - ) { + + @Schema(description = "댓글 수", example = "5") long commentCount, + @Schema(description = "조회 수", example = "100") long viewCount, + @Schema(description = "좋아요 수", example = "10") long likeCount, + @Schema(description = "작성일시") LocalDateTime createdAt, + @Schema(description = "수정일시") LocalDateTime updatedAt) { public static PostDetail from(Post post) { return new PostDetail( post.getId(), @@ -48,36 +40,28 @@ public static PostDetail from(Post post) { post.getViewCount(), post.getLikeCount(), post.getCreatedAt(), - post.getUpdatedAt() - ); + post.getUpdatedAt()); } } - /** - * 게시글 리스트 조회 DTO - */ + /** 게시글 리스트 조회 DTO */ @Schema(description = "게시글 목록 항목") public record PostList( - @Schema(description = "게시글 ID", example = "1") - Long id, - @Schema(description = "작성자 이름", example = "홍길동") - String authorName, - @Schema(description = "가맹점명", example = "스타벅스") - String merchantName, + @Schema(description = "게시글 ID", example = "1") Long id, + @Schema(description = "작성자 이름", example = "홍길동") String authorName, + @Schema(description = "가맹점명", example = "스타벅스") String merchantName, + @Schema(description = "게시글 타입", example = "BENEFIT") PostType type, + @Schema(description = "제목", example = "맛있는 맛집 추천합니다") String title, - @Schema(description = "댓글 수", example = "5") - long commentCount, - @Schema(description = "조회 수", example = "100") - long viewCount, - @Schema(description = "좋아요 수", example = "10") - long likeCount, - @Schema(description = "작성일시") - LocalDateTime createdAt - ) { + @Schema(description = "댓글 수", example = "5") long commentCount, + @Schema(description = "조회 수", example = "100") long viewCount, + @Schema(description = "좋아요 수", example = "10") long likeCount, + @Schema(description = "작성일시") LocalDateTime createdAt) { + public static PostList from(Post post) { return new PostList( post.getId(), @@ -88,8 +72,7 @@ public static PostList from(Post post) { post.getCommentCount(), post.getViewCount(), post.getLikeCount(), - post.getCreatedAt() - ); + post.getCreatedAt()); } } } diff --git a/src/main/java/com/payper/server/post/dto/PostSortType.java b/src/main/java/com/payper/server/post/dto/PostSortType.java index cd58692..0c1c726 100644 --- a/src/main/java/com/payper/server/post/dto/PostSortType.java +++ b/src/main/java/com/payper/server/post/dto/PostSortType.java @@ -3,7 +3,6 @@ import org.springframework.data.domain.Sort; public enum PostSortType { - POSTING_DATE("createdAt"), COMMENT_COUNT("commentCount"), LIKE_COUNT("likeCount"), @@ -18,4 +17,4 @@ public enum PostSortType { public Sort toSort(Sort.Direction direction) { return Sort.by(direction, property); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/post/entity/Post.java b/src/main/java/com/payper/server/post/entity/Post.java index 2b79adb..e6898a0 100644 --- a/src/main/java/com/payper/server/post/entity/Post.java +++ b/src/main/java/com/payper/server/post/entity/Post.java @@ -1,12 +1,11 @@ package com.payper.server.post.entity; import com.payper.server.global.entity.BaseTimeEntity; -import com.payper.server.user.entity.User; import com.payper.server.merchant.entity.Merchant; +import com.payper.server.user.entity.User; import jakarta.persistence.*; -import lombok.*; - import java.time.LocalDateTime; +import lombok.*; @Entity @Getter @@ -20,105 +19,72 @@ public class Post extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 작성자 TODO: 탈퇴한 유저라면 게시글을 보여줄 때 탈퇴한 유저라고 표시해야 함 - */ + /** 작성자 TODO: 탈퇴한 유저라면 게시글을 보여줄 때 탈퇴한 유저라고 표시해야 함 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "author_id", nullable = false) private User author; - /** - * 대상 가맹점 - */ + /** 대상 가맹점 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "merchant_id", nullable = false) private Merchant merchant; - /** - * 게시글 타입 (BENEFIT, QUESTION, ETC) - */ + /** 게시글 타입 (BENEFIT, QUESTION, ETC) */ @Enumerated(EnumType.STRING) @Column(nullable = false) private PostType type; - /** - * 제목 - */ + /** 제목 */ @Column(nullable = false) private String title; - /** - * 내용 (16MB, 대략 5,592,400자) - */ + /** 내용 (16MB, 대략 5,592,400자) */ @Column(columnDefinition = "MEDIUMTEXT", nullable = false) private String content; - /** - * 댓글 수 - */ + /** 댓글 수 */ @Column(name = "comment_count", nullable = false) private long commentCount; - /** - * 조회 수 - */ + /** 조회 수 */ @Column(name = "view_count", nullable = false) private long viewCount; - /** - * 좋아요 수 TODO: PostLike 테이블 고려 - */ + /** 좋아요 수 TODO: PostLike 테이블 고려 */ @Column(name = "like_count", nullable = false) private long likeCount; - /** - * 신고 수 TODO: PostReport 테이블 고려 - */ + /** 신고 수 TODO: PostReport 테이블 고려 */ @Column(name = "report_count", nullable = false) private long reportCount; - /** - * 비활성 여부 - */ + /** 비활성 여부 */ @Column(name = "is_inactive", nullable = false) private boolean isInactive; - /** - * 비활성 시간 - */ + /** 비활성 시간 */ @Column(name = "inactive_at") private LocalDateTime inactiveAt; - /** - * 삭제 여부 - */ + /** 삭제 여부 */ @Column(name = "is_deleted", nullable = false) private boolean isDeleted; - /** - * 삭제 시간 - */ + /** 삭제 시간 */ @Column(name = "deleted_at") private LocalDateTime deletedAt; - /** - * 댓글 증가 - */ + /** 댓글 증가 */ public void increaseCommentCount() { this.commentCount++; } - /** - * 댓글 감소 - */ + /** 댓글 감소 */ public void decreaseCommentCount() { this.commentCount = Math.max(0, this.commentCount - 1); } - /** - * 게시글 삭제 - * soft delete - */ + /** 게시글 삭제 - soft delete */ public void softDelete() { if (this.isDeleted) { // 멱등성 고려 처리 return; @@ -128,18 +94,14 @@ public void softDelete() { this.deletedAt = LocalDateTime.now(); } - /** - * 게시글 비활성 - */ + /** 게시글 비활성 */ public void inactivate() { this.isInactive = true; -// this.inactiveAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + // this.inactiveAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); this.inactiveAt = LocalDateTime.now(); } - /** - * 게시글 생성 - */ + /** 게시글 생성 */ public static Post create(User author, Merchant merchant, PostType type, String title, String content) { return Post.builder() .author(author) @@ -156,25 +118,19 @@ public static Post create(User author, Merchant merchant, PostType type, String .build(); } - /** - * 게시글 수정 - */ + /** 게시글 수정 */ public void update(String title, String content) { this.title = title; this.content = content; } - /** - * 게시글 작성자인지 판단 - */ + /** 게시글 작성자인지 판단 */ public boolean isAuthor(Long authorId) { return this.author.getId().equals(authorId); } - /** - * 댓글을 달 수 있는 게시글인지 체크 - */ + /** 댓글을 달 수 있는 게시글인지 체크 */ public boolean isCommentable() { return !this.isDeleted && !this.isInactive; } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/post/entity/PostBookmark.java b/src/main/java/com/payper/server/post/entity/PostBookmark.java index 4237e05..8b57468 100644 --- a/src/main/java/com/payper/server/post/entity/PostBookmark.java +++ b/src/main/java/com/payper/server/post/entity/PostBookmark.java @@ -11,28 +11,23 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table( name = "post_bookmark", - uniqueConstraints = @UniqueConstraint( - name = "uk_post_bookmark_post_user", - columnNames = {"post_id", "user_id"} - ) -) + uniqueConstraints = + @UniqueConstraint( + name = "uk_post_bookmark_post_user", + columnNames = {"post_id", "user_id"})) public class PostBookmark { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 게시글 - */ + /** 게시글 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", nullable = false) private Post post; - /** - * 유저 - */ + /** 유저 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/post/entity/PostLike.java b/src/main/java/com/payper/server/post/entity/PostLike.java index a4ac72d..43bf9fc 100644 --- a/src/main/java/com/payper/server/post/entity/PostLike.java +++ b/src/main/java/com/payper/server/post/entity/PostLike.java @@ -11,28 +11,23 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table( name = "post_like", - uniqueConstraints = @UniqueConstraint( - name = "uk_post_like_post_user", - columnNames = {"post_id", "user_id"} - ) -) + uniqueConstraints = + @UniqueConstraint( + name = "uk_post_like_post_user", + columnNames = {"post_id", "user_id"})) public class PostLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 게시글 - */ + /** 게시글 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", nullable = false) private Post post; - /** - * 유저 - */ + /** 유저 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/post/entity/PostReport.java b/src/main/java/com/payper/server/post/entity/PostReport.java index 01d037b..2fd2824 100644 --- a/src/main/java/com/payper/server/post/entity/PostReport.java +++ b/src/main/java/com/payper/server/post/entity/PostReport.java @@ -12,40 +12,31 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table( name = "post_report", - uniqueConstraints = @UniqueConstraint( - name = "uk_post_report_post_reporter", - columnNames = {"post_id", "reporter_id"} - ) -) + uniqueConstraints = + @UniqueConstraint( + name = "uk_post_report_post_reporter", + columnNames = {"post_id", "reporter_id"})) public class PostReport extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 신고 대상 게시글 - */ + /** 신고 대상 게시글 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", nullable = false) private Post post; - /** - * 신고자 - */ + /** 신고자 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "reporter_id", nullable = false) private User reporter; - /** - * 신고 사유 - */ + /** 신고 사유 */ @Enumerated(EnumType.STRING) @Column(nullable = false) private ReportReason reason; - /** - * 상세 사유 - */ + /** 상세 사유 */ private String description; -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/post/entity/PostType.java b/src/main/java/com/payper/server/post/entity/PostType.java index a37e0b7..bb9087a 100644 --- a/src/main/java/com/payper/server/post/entity/PostType.java +++ b/src/main/java/com/payper/server/post/entity/PostType.java @@ -5,18 +5,12 @@ @Getter public enum PostType { - /** - * 혜택 - */ + /** 혜택 */ BENEFIT, - /** - * 질문 - */ + /** 질문 */ QUESTION, - /** - * 기타 - */ + /** 기타 */ ETC -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/post/repository/PostRepository.java b/src/main/java/com/payper/server/post/repository/PostRepository.java index 9e438f0..ec8c615 100644 --- a/src/main/java/com/payper/server/post/repository/PostRepository.java +++ b/src/main/java/com/payper/server/post/repository/PostRepository.java @@ -2,6 +2,7 @@ import com.payper.server.post.entity.Post; import com.payper.server.post.entity.PostType; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,8 +11,6 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.Optional; - @Repository public interface PostRepository extends JpaRepository { @@ -30,25 +29,19 @@ public interface PostRepository extends JpaRepository { """) void decreaseCommentCount(@Param("postId") Long postId, @Param("count") long count); - @Query( - value = """ + @Query(value = """ select p from Post p join fetch p.author join fetch p.merchant where p.isDeleted = false and (:merchantId IS NULL or p.merchant.id = :merchantId) and (:type IS NULL or p.type = :type) - """, - countQuery = """ + """, countQuery = """ select count(p) from Post p where p.isDeleted = false and (:merchantId IS NULL or p.merchant.id = :merchantId) and (:type IS NULL or p.type = :type) - """ - ) + """) Page findActivePostsByCondition( - @Param("merchantId") Long merchantId, - @Param("type") PostType type, - Pageable pageable - ); -} \ No newline at end of file + @Param("merchantId") Long merchantId, @Param("type") PostType type, Pageable pageable); +} diff --git a/src/main/java/com/payper/server/post/service/PostService.java b/src/main/java/com/payper/server/post/service/PostService.java index 8e5e57b..f8a1267 100644 --- a/src/main/java/com/payper/server/post/service/PostService.java +++ b/src/main/java/com/payper/server/post/service/PostService.java @@ -29,17 +29,15 @@ public class PostService { private final MerchantRepository merchantRepository; private final CommentService commentService; - /** - * 게시글 작성 - */ + /** 게시글 작성 */ @Transactional public Long createPost(Long userId, Long merchantId, PostRequest.CreatePost request) { // 사용자 조회 - User user = userRepository.findById(userId) - .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + User user = userRepository.findById(userId).orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); // 가맹점 조회 - Merchant merchant = merchantRepository.findById(merchantId) + Merchant merchant = merchantRepository + .findById(merchantId) .orElseThrow(() -> new ApiException(ErrorCode.MERCHANT_NOT_FOUND)); // 게시글 생성 @@ -50,17 +48,16 @@ public Long createPost(Long userId, Long merchantId, PostRequest.CreatePost requ return post.getId(); } - /** - * 게시글 수정 - */ + /** 게시글 수정 */ @Transactional public void updatePost(Long userId, Long postId, PostRequest.UpdatePost request) { // 게시글 조회 및 삭제 여부 체크 - Post post = postRepository.findByIdAndIsDeletedFalse(postId) + Post post = postRepository + .findByIdAndIsDeletedFalse(postId) .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); // 게시글 수정 권한 조회 - if(!post.isAuthor(userId)) { + if (!post.isAuthor(userId)) { throw new ApiException(ErrorCode.NOT_POST_AUTHOR); } @@ -70,17 +67,16 @@ public void updatePost(Long userId, Long postId, PostRequest.UpdatePost request) log.info("게시글 수정 완료 - postId: {}", post.getId()); } - /** - * 게시글 삭제 - */ + /** 게시글 삭제 */ @Transactional public void deletePost(Long userId, Long postId) { // 게시글 조회 - Post post = postRepository.findByIdAndIsDeletedFalse(postId) + Post post = postRepository + .findByIdAndIsDeletedFalse(postId) .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); // 게시글 삭제 권한 조회 - if(!post.isAuthor(userId)) { + if (!post.isAuthor(userId)) { throw new ApiException(ErrorCode.NOT_POST_AUTHOR); } @@ -96,21 +92,18 @@ public void deletePost(Long userId, Long postId) { } } - /** - * 게시글 단일 조회 - */ + /** 게시글 단일 조회 */ @Transactional(readOnly = true) public PostResponse.PostDetail getPostDetail(Long postId) { // 게시글 조회 - Post post = postRepository.findByIdAndIsDeletedFalse(postId) + Post post = postRepository + .findByIdAndIsDeletedFalse(postId) .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); return PostResponse.PostDetail.from(post); } - /** - * 게시글 리스트 조회 - */ + /** 게시글 리스트 조회 */ @Transactional(readOnly = true) public Page getPosts(Long merchantId, PostType type, Pageable pageable) { Page posts = postRepository.findActivePostsByCondition(merchantId, type, pageable); @@ -118,4 +111,4 @@ public Page getPosts(Long merchantId, PostType type, Page log.info("post 조회 완료 {}", posts); return posts.map(PostResponse.PostList::from); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/security/CustomAccessDeniedHandler.java b/src/main/java/com/payper/server/security/CustomAccessDeniedHandler.java index 3942ebb..3fc5fd6 100644 --- a/src/main/java/com/payper/server/security/CustomAccessDeniedHandler.java +++ b/src/main/java/com/payper/server/security/CustomAccessDeniedHandler.java @@ -5,6 +5,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.AccessDeniedException; @@ -14,9 +15,6 @@ import org.springframework.stereotype.Component; import tools.jackson.databind.ObjectMapper; - -import java.io.IOException; - @RequiredArgsConstructor @Component @Slf4j @@ -25,22 +23,21 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle( - HttpServletRequest request, - HttpServletResponse response, - AccessDeniedException accessDeniedException - ) throws IOException, ServletException { + HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) + throws IOException, ServletException { ApiResponse failResponseDto; Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !(authentication.isAuthenticated())) { - failResponseDto = - ApiResponse.fail(ErrorCode.UNAUTHENTICATED, accessDeniedException.getMessage()); + failResponseDto = ApiResponse.fail(ErrorCode.UNAUTHENTICATED, accessDeniedException.getMessage()); } else { - failResponseDto = - ApiResponse.fail(ErrorCode.UNAUTHORIZED, accessDeniedException.getMessage()); + failResponseDto = ApiResponse.fail(ErrorCode.UNAUTHORIZED, accessDeniedException.getMessage()); } - log.warn("[AUTH_EXCEPTION IN FILTER] code={}, message={}",failResponseDto.getError().getCode(),failResponseDto.getError().getMessage()); + log.warn( + "[AUTH_EXCEPTION IN FILTER] code={}, message={}", + failResponseDto.getError().getCode(), + failResponseDto.getError().getMessage()); response.setStatus(failResponseDto.getStatus()); response.setContentType("application/json"); diff --git a/src/main/java/com/payper/server/security/CustomAuthenticationEntryPoint.java b/src/main/java/com/payper/server/security/CustomAuthenticationEntryPoint.java index 5ab7ec5..7788b15 100644 --- a/src/main/java/com/payper/server/security/CustomAuthenticationEntryPoint.java +++ b/src/main/java/com/payper/server/security/CustomAuthenticationEntryPoint.java @@ -6,6 +6,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -15,9 +17,6 @@ import org.springframework.stereotype.Component; import tools.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - @RequiredArgsConstructor @Component @Slf4j @@ -27,10 +26,8 @@ public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint @Override public void commence( - HttpServletRequest request, - HttpServletResponse response, - AuthenticationException authException - ) throws IOException, ServletException { + HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException, ServletException { if (response.isCommitted()) { return; @@ -39,7 +36,10 @@ public void commence( ErrorCode errorCode = resolveErrorCode(authException); ApiResponse body = ApiResponse.fail(errorCode); - log.warn("[AUTH_EXCEPTION IN FILTER] code={}, message={}",body.getError().getCode(),body.getError().getMessage()); + log.warn( + "[AUTH_EXCEPTION IN FILTER] code={}, message={}", + body.getError().getCode(), + body.getError().getMessage()); response.setStatus(errorCode.getStatus().value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); @@ -50,7 +50,7 @@ public void commence( private ErrorCode resolveErrorCode(AuthenticationException ex) { // 1) security filter 체인 속 auth 예외 발생 케이스 - if(ex instanceof AuthException authException) { + if (ex instanceof AuthException authException) { return authException.getErrorCode(); } @@ -62,5 +62,4 @@ private ErrorCode resolveErrorCode(AuthenticationException ex) { // 3) 최후의 기본값 return ErrorCode.UNAUTHENTICATED; } - -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/security/CustomUserDetails.java b/src/main/java/com/payper/server/security/CustomUserDetails.java index 0137f18..6d043d3 100644 --- a/src/main/java/com/payper/server/security/CustomUserDetails.java +++ b/src/main/java/com/payper/server/security/CustomUserDetails.java @@ -2,25 +2,20 @@ import com.payper.server.user.entity.User; import jakarta.annotation.Nullable; +import java.util.Collection; +import java.util.List; import lombok.RequiredArgsConstructor; 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.List; - @RequiredArgsConstructor public class CustomUserDetails implements UserDetails { private final User user; @Override public Collection getAuthorities() { - return List.of( - new SimpleGrantedAuthority( - "ROLE_" + user.getUserRole().name() - ) - ); + return List.of(new SimpleGrantedAuthority("ROLE_" + user.getUserRole().name())); } @Override diff --git a/src/main/java/com/payper/server/security/CustomUserDetailsService.java b/src/main/java/com/payper/server/security/CustomUserDetailsService.java index 4c03b12..9595a0f 100644 --- a/src/main/java/com/payper/server/security/CustomUserDetailsService.java +++ b/src/main/java/com/payper/server/security/CustomUserDetailsService.java @@ -2,8 +2,8 @@ import com.payper.server.auth.AuthException; import com.payper.server.global.response.ErrorCode; -import com.payper.server.user.repository.UserRepository; import com.payper.server.user.entity.User; +import com.payper.server.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -24,7 +24,8 @@ public UserDetails loadUserByUsername(String userIdentifier) throws UsernameNotF } public User getActiveUserByUserIdentifier(String userIdentifier) { - User user = userRepository.findByUserIdentifier(userIdentifier) + User user = userRepository + .findByUserIdentifier(userIdentifier) .orElseThrow(() -> new AuthException(ErrorCode.USER_NOT_FOUND_AUTH)); if (!user.isActive()) { diff --git a/src/main/java/com/payper/server/security/JwtAuthenticationFilter.java b/src/main/java/com/payper/server/security/JwtAuthenticationFilter.java index dfa6b77..298c613 100644 --- a/src/main/java/com/payper/server/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/payper/server/security/JwtAuthenticationFilter.java @@ -5,6 +5,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -14,8 +15,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; - @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final RequestMatcher skipRequestMatcher; @@ -29,12 +28,9 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce } @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain - ) throws ServletException, IOException { - //System.out.println("JwtAuthenticationFilter.doFilterInternal"); + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // System.out.println("JwtAuthenticationFilter.doFilterInternal"); String accessToken = jwtParseUtil.extractJwtTokenFromRequest(request); @@ -43,16 +39,13 @@ protected void doFilterInternal( return; } - try{ - Authentication requestAuth = - UsernamePasswordAuthenticationToken.unauthenticated(null, accessToken); + try { + Authentication requestAuth = UsernamePasswordAuthenticationToken.unauthenticated(null, accessToken); - Authentication authenticated = - authenticationManager.authenticate(requestAuth); + Authentication authenticated = authenticationManager.authenticate(requestAuth); SecurityContextHolder.getContext().setAuthentication(authenticated); filterChain.doFilter(request, response); - } - catch(AuthenticationException e){ + } catch (AuthenticationException e) { SecurityContextHolder.clearContext(); customAuthenticationEntryPoint.commence(request, response, e); } diff --git a/src/main/java/com/payper/server/security/JwtAuthenticationProvider.java b/src/main/java/com/payper/server/security/JwtAuthenticationProvider.java index 06ad2f4..493e2c7 100644 --- a/src/main/java/com/payper/server/security/JwtAuthenticationProvider.java +++ b/src/main/java/com/payper/server/security/JwtAuthenticationProvider.java @@ -39,4 +39,3 @@ public boolean supports(Class authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } } - diff --git a/src/main/java/com/payper/server/security/SecurityConfig.java b/src/main/java/com/payper/server/security/SecurityConfig.java index 0e44954..8b971cc 100644 --- a/src/main/java/com/payper/server/security/SecurityConfig.java +++ b/src/main/java/com/payper/server/security/SecurityConfig.java @@ -41,11 +41,10 @@ public class SecurityConfig { @PostConstruct void init() { - var requestMatcher = - PathPatternRequestMatcher.withDefaults().basePath("/"); + var requestMatcher = PathPatternRequestMatcher.withDefaults().basePath("/"); permitAllRequestMatcher = new OrRequestMatcher( - //requestMatcher.matcher("/**"), + // requestMatcher.matcher("/**"), requestMatcher.matcher(HttpMethod.GET, "/swagger-ui/**"), requestMatcher.matcher(HttpMethod.GET, "/v3/api-docs/**"), requestMatcher.matcher(HttpMethod.GET, "/favicon.ico"), @@ -53,11 +52,10 @@ void init() { requestMatcher.matcher(HttpMethod.GET, "/api/v1/posts/**"), requestMatcher.matcher(HttpMethod.GET, "/api/v1/comments/*/replies"), requestMatcher.matcher(HttpMethod.GET, "/api/v1/merchants/**"), - requestMatcher.matcher(HttpMethod.GET, "/api/v1/categories/**") - ); + requestMatcher.matcher(HttpMethod.GET, "/api/v1/categories/**")); // 인증이 필요한 요청 authenticatedRequestMatcher = new OrRequestMatcher( - //requestMatcher.matcher("/**"), + // requestMatcher.matcher("/**"), requestMatcher.matcher(HttpMethod.GET, "/me"), // 댓글 관련 @@ -71,15 +69,13 @@ void init() { requestMatcher.matcher(HttpMethod.DELETE, "/api/v1/posts/**"), // 가맹점 관련 - requestMatcher.matcher(HttpMethod.POST, "/api/v1/merchants/*/posts") - ); + requestMatcher.matcher(HttpMethod.POST, "/api/v1/merchants/*/posts")); adminRequestMatcher = new OrRequestMatcher( requestMatcher.matcher(HttpMethod.GET, "/admin/**"), requestMatcher.matcher(HttpMethod.POST, "/api/v1/merchants"), requestMatcher.matcher(HttpMethod.PUT, "/api/v1/merchants/**"), requestMatcher.matcher(HttpMethod.POST, "/api/v1/categories"), - requestMatcher.matcher(HttpMethod.PUT, "/api/v1/categories/**") - ); + requestMatcher.matcher(HttpMethod.PUT, "/api/v1/categories/**")); } @Bean @@ -89,60 +85,39 @@ public PasswordEncoder passwordEncoder() { @Bean public AuthenticationManager authenticationManager() { - return new ProviderManager( - jwtAuthenticationProvider - ); + return new ProviderManager(jwtAuthenticationProvider); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { RequestMatcher skipEndPoints = permitAllRequestMatcher; return new JwtAuthenticationFilter( - skipEndPoints, - jwtParseUtil, - authenticationManager(), - customAuthenticationEntryPoint - ); + skipEndPoints, jwtParseUtil, authenticationManager(), customAuthenticationEntryPoint); } - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - return http - .csrf(AbstractHttpConfigurer::disable) + return http.csrf(AbstractHttpConfigurer::disable) .cors((registry) -> registry.configurationSource(corsConfigurationSource())) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .rememberMe(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) - .sessionManagement( - a -> a.sessionCreationPolicy( - SessionCreationPolicy.STATELESS - ) - ) - - .authorizeHttpRequests( - configurer -> configurer - .requestMatchers(permitAllRequestMatcher) - .permitAll() - .requestMatchers(authenticatedRequestMatcher) - .authenticated() - .requestMatchers(adminRequestMatcher) - .hasRole("ADMIN") - ) - + .sessionManagement(a -> a.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(configurer -> configurer + .requestMatchers(permitAllRequestMatcher) + .permitAll() + .requestMatchers(authenticatedRequestMatcher) + .authenticated() + .requestMatchers(adminRequestMatcher) + .hasRole("ADMIN")) .addFilterAfter(jwtAuthenticationFilter(), LogoutFilter.class) - - .exceptionHandling( - configurer -> configurer - .authenticationEntryPoint(customAuthenticationEntryPoint) - .accessDeniedHandler(customAccessDeniedHandler) - ) - + .exceptionHandling(configurer -> configurer + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler)) .build(); } - @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); @@ -159,4 +134,4 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/user/UserService.java b/src/main/java/com/payper/server/user/UserService.java index 3034c53..251dc7a 100644 --- a/src/main/java/com/payper/server/user/UserService.java +++ b/src/main/java/com/payper/server/user/UserService.java @@ -5,12 +5,11 @@ import com.payper.server.global.response.ErrorCode; import com.payper.server.user.entity.User; import com.payper.server.user.repository.UserRepository; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @RequiredArgsConstructor @Transactional @@ -22,11 +21,13 @@ public User save(final User user) { } private void validateDuplicate(final User user) { - Optional findUser = switch (user.getAuthType()) { - case KAKAO -> userRepository.findByOauthIdAndActive(user.getOauthId(), true); - default -> - throw new IllegalArgumentException("Unsupported AuthType for duplicate validation: " + user.getAuthType()); - }; + Optional findUser = + switch (user.getAuthType()) { + case KAKAO -> userRepository.findByOauthIdAndActive(user.getOauthId(), true); + default -> + throw new IllegalArgumentException( + "Unsupported AuthType for duplicate validation: " + user.getAuthType()); + }; if (findUser.isPresent()) { throw new AuthException(ErrorCode.USER_DUPLICATE); @@ -35,18 +36,13 @@ private void validateDuplicate(final User user) { public Optional getActiveOAuthUser(OAuthUserInfo oAuthUserInfo) { Optional user = - userRepository.findByOauthIdAndAuthType( - oAuthUserInfo.getOauthId(), - oAuthUserInfo.getAuthType() - ); - - user.ifPresent( - u -> { - if (!u.isActive()) { - throw new AuthException(ErrorCode.USER_INACTIVE); - } - } - ); + userRepository.findByOauthIdAndAuthType(oAuthUserInfo.getOauthId(), oAuthUserInfo.getAuthType()); + + user.ifPresent(u -> { + if (!u.isActive()) { + throw new AuthException(ErrorCode.USER_INACTIVE); + } + }); return user; } diff --git a/src/main/java/com/payper/server/user/entity/User.java b/src/main/java/com/payper/server/user/entity/User.java index c753caa..f6d06c4 100644 --- a/src/main/java/com/payper/server/user/entity/User.java +++ b/src/main/java/com/payper/server/user/entity/User.java @@ -1,9 +1,8 @@ package com.payper.server.user.entity; import jakarta.persistence.*; -import lombok.*; - import java.util.UUID; +import lombok.*; @Entity @Getter @@ -17,7 +16,7 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false,unique = true,updatable = false) + @Column(nullable = false, unique = true, updatable = false) private String userIdentifier; @Enumerated(EnumType.STRING) @@ -27,7 +26,7 @@ public class User { @Column(nullable = false) private String name; - @Column(nullable = false,updatable = false) + @Column(nullable = false, updatable = false) private String oauthId; @Enumerated(EnumType.STRING) @@ -37,13 +36,7 @@ public class User { @Column(nullable = false) private boolean active; - public static User create( - AuthType authType, - String name, - String oauthId, - UserRole userRole, - boolean active - ){ + public static User create(AuthType authType, String name, String oauthId, UserRole userRole, boolean active) { return User.builder() .userIdentifier(UUID.randomUUID().toString()) .authType(authType) @@ -53,4 +46,4 @@ public static User create( .active(active) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/payper/server/user/entity/UserRole.java b/src/main/java/com/payper/server/user/entity/UserRole.java index 147d260..3792796 100644 --- a/src/main/java/com/payper/server/user/entity/UserRole.java +++ b/src/main/java/com/payper/server/user/entity/UserRole.java @@ -1,5 +1,6 @@ package com.payper.server.user.entity; public enum UserRole { - USER, ADMIN + USER, + ADMIN } diff --git a/src/main/java/com/payper/server/user/repository/UserRepository.java b/src/main/java/com/payper/server/user/repository/UserRepository.java index 39ef0cc..e22e52c 100644 --- a/src/main/java/com/payper/server/user/repository/UserRepository.java +++ b/src/main/java/com/payper/server/user/repository/UserRepository.java @@ -2,12 +2,11 @@ import com.payper.server.user.entity.AuthType; import com.payper.server.user.entity.User; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import java.util.Optional; - public interface UserRepository extends JpaRepository { Optional findByUserIdentifier(String userIdentifier); diff --git a/src/test/java/com/payper/server/JwtModulesSpringBootIntegrationTest.java b/src/test/java/com/payper/server/JwtModulesSpringBootIntegrationTest.java index 134780c..04f79d9 100644 --- a/src/test/java/com/payper/server/JwtModulesSpringBootIntegrationTest.java +++ b/src/test/java/com/payper/server/JwtModulesSpringBootIntegrationTest.java @@ -1,13 +1,17 @@ package com.payper.server; +import static org.assertj.core.api.Assertions.*; + import com.payper.server.auth.AuthException; import com.payper.server.auth.jwt.RefreshTokenRepository; -import com.payper.server.auth.jwt.entity.RefreshTokenEntity; import com.payper.server.auth.jwt.entity.JwtType; +import com.payper.server.auth.jwt.entity.RefreshTokenEntity; import com.payper.server.auth.jwt.util.JwtParseUtil; import com.payper.server.auth.jwt.util.JwtProperties; import com.payper.server.auth.jwt.util.JwtRefreshTokenUtil; import com.payper.server.auth.jwt.util.JwtTokenUtil; +import java.util.Date; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,11 +21,6 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; -import java.util.Date; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; - @SpringBootTest @ActiveProfiles("test") class JwtModulesSpringBootIntegrationTest { @@ -31,20 +30,26 @@ class JwtModulesSpringBootIntegrationTest { @Autowired JwtTokenUtil jwtTokenUtil; + @Autowired JwtParseUtil jwtParseUtil; @Autowired JwtRefreshTokenUtil jwtRefreshTokenUtil; + @Autowired RefreshTokenRepository refreshTokenRepository; /** * ✅ 이 테스트는 "실제 MySQL 연동"이므로 - * - 각 테스트가 서로 간섭하지 않게 매번 DB를 비우고 - * - @Transactional이 적용된 테스트라면(기본 롤백)에도, - * 내부 util이 REQUIRES_NEW로 flush/commit을 때리며 남길 수 있어 - * BeforeEach에서 강제로 정리하는 방식이 안전함. + * + *

- 각 테스트가 서로 간섭하지 않게 매번 DB를 비우고 + * + *

- @Transactional이 적용된 테스트라면(기본 롤백)에도, + * + *

내부 util이 REQUIRES_NEW로 flush/commit을 때리며 남길 수 있어 + * + *

BeforeEach에서 강제로 정리하는 방식이 안전함. */ @BeforeEach void cleanDb() { @@ -114,8 +119,7 @@ void expiredToken_throws() { String expired = jwtTokenUtil.generateJwtToken(JwtType.ACCESS, oldNow, userIdentifier); // when & then - assertThatThrownBy(() -> jwtParseUtil.getUserIdentifier(expired)) - .isInstanceOf(AuthException.class); + assertThatThrownBy(() -> jwtParseUtil.getUserIdentifier(expired)).isInstanceOf(AuthException.class); } @Test @@ -124,16 +128,19 @@ void extractFromRequest_bearer() { MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", "Bearer abc.def.ghi"); - assertThat(jwtParseUtil.extractJwtTokenFromRequest(request)) - .isEqualTo("abc.def.ghi"); + assertThat(jwtParseUtil.extractJwtTokenFromRequest(request)).isEqualTo("abc.def.ghi"); } /** * ✅ DB 통합 플로우 테스트들은 '테스트 메서드 단위 트랜잭션' 안에서 실행되도록 @Transactional 부여 - * - 이 테스트 클래스 전체에 @Transactional을 걸지 않는 이유: - * util 내부에 REQUIRES_NEW가 섞여 있으면 테스트 트랜잭션 롤백으로도 데이터가 남을 수 있어서 - * 오히려 오해를 만들기 쉬움. - * - 대신: 각 테스트 시작 전 cleanDb()로 완전 격리 + * + *

- 이 테스트 클래스 전체에 @Transactional을 걸지 않는 이유: + * + *

util 내부에 REQUIRES_NEW가 섞여 있으면 테스트 트랜잭션 롤백으로도 데이터가 남을 수 있어서 + * + *

오히려 오해를 만들기 쉬움. + * + *

- 대신: 각 테스트 시작 전 cleanDb()로 완전 격리 */ @Test @Transactional @@ -172,13 +179,12 @@ void refresh_upsert_replacesExistingTokenForUser() { Optional found2 = jwtRefreshTokenUtil.getRefreshTokenEntity(raw2); assertThat(found2).isNotNull(); - found2.ifPresent( - refreshTokenEntity -> assertThat(refreshTokenEntity.getUserIdentifier() - ).isEqualTo(userIdentifier)); + found2.ifPresent(refreshTokenEntity -> + assertThat(refreshTokenEntity.getUserIdentifier()).isEqualTo(userIdentifier)); } @Test - //@Transactional + // @Transactional @DisplayName("deleteAllRefreshTokenEntity는 사용자 토큰을 삭제하고, 이후 조회가 null이 된다") void refresh_deleteAll_deletesAndThenLookupNull() { String userIdentifier = "user-del"; diff --git a/src/test/java/com/payper/server/PayperServerApplicationTests.java b/src/test/java/com/payper/server/PayperServerApplicationTests.java index 148bcd5..80e6749 100644 --- a/src/test/java/com/payper/server/PayperServerApplicationTests.java +++ b/src/test/java/com/payper/server/PayperServerApplicationTests.java @@ -2,12 +2,12 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class PayperServerApplicationTests { - @Test - void contextLoads() { - } - + @Test + void contextLoads() {} } diff --git a/src/test/java/com/payper/server/UserAndRefreshTokenJpaTest.java b/src/test/java/com/payper/server/UserAndRefreshTokenJpaTest.java index 35be51e..90ee844 100644 --- a/src/test/java/com/payper/server/UserAndRefreshTokenJpaTest.java +++ b/src/test/java/com/payper/server/UserAndRefreshTokenJpaTest.java @@ -1,32 +1,35 @@ package com.payper.server; -import com.payper.server.auth.jwt.entity.RefreshTokenEntity; +import static org.assertj.core.api.Assertions.assertThat; + import com.payper.server.auth.jwt.RefreshTokenRepository; +import com.payper.server.auth.jwt.entity.RefreshTokenEntity; import com.payper.server.security.CustomUserDetails; -import com.payper.server.user.repository.UserRepository; import com.payper.server.user.entity.AuthType; import com.payper.server.user.entity.User; import com.payper.server.user.entity.UserRole; +import com.payper.server.user.repository.UserRepository; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; -import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; -import static org.assertj.core.api.Assertions.assertThat; - @DataJpaTest @Transactional @ActiveProfiles("test") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // <-- MySQL 그대로 사용 class UserAndRefreshTokenJpaTest { - @Autowired UserRepository userRepository; - @Autowired RefreshTokenRepository refreshTokenRepository; - @Autowired EntityManager em; + @Autowired + UserRepository userRepository; + + @Autowired + RefreshTokenRepository refreshTokenRepository; + + @Autowired + EntityManager em; @Test @DisplayName("User 저장 후 userIdentifier로 조회가 된다") @@ -42,8 +45,8 @@ void saveUser_and_findByUserIdentifier() { // then assertThat(saved.getId()).isNotNull(); - User found = userRepository.findByUserIdentifier(saved.getUserIdentifier()) - .orElseThrow(); + User found = + userRepository.findByUserIdentifier(saved.getUserIdentifier()).orElseThrow(); assertThat(found.getId()).isEqualTo(saved.getId()); assertThat(found.getOauthId()).isEqualTo("kakao-123"); @@ -61,8 +64,7 @@ void findByOauthIdAndActive() { em.clear(); // when - User found = userRepository.findByOauthIdAndActive("kakao-999", true) - .orElseThrow(); + User found = userRepository.findByOauthIdAndActive("kakao-999", true).orElseThrow(); // then assertThat(found.getOauthId()).isEqualTo("kakao-999"); @@ -104,7 +106,8 @@ void saveRefreshToken_and_findByHashedRefreshToken() { em.clear(); // then (repo 메서드가 Optional이 아니라 null 가능) - RefreshTokenEntity found = refreshTokenRepository.findByHashedRefreshToken("hashed-rt-123").get(); + RefreshTokenEntity found = + refreshTokenRepository.findByHashedRefreshToken("hashed-rt-123").get(); assertThat(found).isNotNull(); assertThat(found.getUserIdentifier()).isEqualTo(userIdentifier); assertThat(found.getHashedRefreshToken()).isEqualTo("hashed-rt-123"); @@ -128,16 +131,15 @@ void deleteRefreshToken_byUserIdentifier() { // then assertThat(deleted).isEqualTo(1); - assertThat(refreshTokenRepository.findByHashedRefreshToken("hashed-del-1")).isNull(); + assertThat(refreshTokenRepository.findByHashedRefreshToken("hashed-del-1")) + .isEmpty(); } @Test @DisplayName("CustomUserDetails로 User -> Principal 변환이 가능하다") void customUserDetails_canConvertUserToPrincipal() { // given (id 생성 필요) - User saved = userRepository.save( - User.create(AuthType.KAKAO, "경현", "kakao-principal", UserRole.ADMIN, true) - ); + User saved = userRepository.save(User.create(AuthType.KAKAO, "경현", "kakao-principal", UserRole.ADMIN, true)); em.flush(); em.clear(); diff --git a/src/main/resources/application-test.yml b/src/test/resources/application-test.yml similarity index 71% rename from src/main/resources/application-test.yml rename to src/test/resources/application-test.yml index cda853d..55878b5 100644 --- a/src/main/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,9 +1,7 @@ spring: datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/payper_v2_test?useSSL=false&serverTimezone=Asia/Seoul - username: root - password: 1234 + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver jpa: hibernate: