diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 5dee87723..4223d083b 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -23,15 +23,17 @@ jobs: NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }} NAVER_CLIENT_SECRET: ${{ secrets.NAVER_CLIENT_SECRET }} NAVER_SCOPE: ${{ secrets.NAVER_SCOPE }} - NAVER_REDIRECT_URL: ${{secrets.NAVER_REDIRECT_URL}} + NAVER_REDIRECT_URL: ${{ secrets.NAVER_REDIRECT_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} - FRONT_URL: ${{secrets.FRONT_URL}} - BACK_URL: ${{secrets.BACK_URL}} - BUCKET_NAME: ${{secrets.BUCKET_NAME}} - BUCKET_REGION: ${{secrets.BUCKET_REGION}} - IMG_BASE_URL: ${{secrets.BASE_URL}} - S3_ACCESS_KEY: ${{secrets.S3_ACCESS_KEY}} - S3_SECRET_KEY: ${{secrets.S3_SECRET_KEY}} + FRONT_URL: ${{ secrets.FRONT_URL }} + BACK_URL: ${{ secrets.BACK_URL }} + BUCKET_NAME: ${{ secrets.BUCKET_NAME }} + BUCKET_REGION: ${{ secrets.BUCKET_REGION }} + IMG_BASE_URL: ${{ secrets.BASE_URL }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + DEFAULT_IMG_URL: ${{ secrets.DEFAULT_IMG_URL }} + APP_DEVELOP_MODE: ${{ secrets.APP_DEVELOP_MODE }} steps: - name: Github Repository 파일 불러오기 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 629520d89..13ad59f60 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -31,15 +31,17 @@ jobs: NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }} NAVER_CLIENT_SECRET: ${{ secrets.NAVER_CLIENT_SECRET }} NAVER_SCOPE: ${{ secrets.NAVER_SCOPE }} - NAVER_REDIRECT_URL: ${{secrets.NAVER_REDIRECT_URL}} + NAVER_REDIRECT_URL: ${{ secrets.NAVER_REDIRECT_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} - FRONT_URL: ${{secrets.FRONT_URL}} - BACK_URL: ${{secrets.BACK_URL}} - BUCKET_NAME: ${{secrets.BUCKET_NAME}} - BUCKET_REGION: ${{secrets.BUCKET_REGION}} - IMG_BASE_URL: ${{secrets.BASE_URL}} - S3_ACCESS_KEY: ${{secrets.S3_ACCESS_KEY}} - S3_SECRET_KEY: ${{secrets.S3_SECRET_KEY}} + FRONT_URL: ${{ secrets.FRONT_URL }} + BACK_URL: ${{ secrets.BACK_URL }} + BUCKET_NAME: ${{ secrets.BUCKET_NAME }} + BUCKET_REGION: ${{ secrets.BUCKET_REGION }} + IMG_BASE_URL: ${{ secrets.BASE_URL }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + DEFAULT_IMG_URL: ${{ secrets.DEFAULT_IMG_URL }} + APP_DEVELOP_MODE: ${{ secrets.APP_DEVELOP_MODE }} steps: diff --git a/src/main/java/com/somemore/auth/jwt/filter/JwtAuthFilter.java b/src/main/java/com/somemore/auth/jwt/filter/JwtAuthFilter.java index 08ced03a6..071cf7677 100644 --- a/src/main/java/com/somemore/auth/jwt/filter/JwtAuthFilter.java +++ b/src/main/java/com/somemore/auth/jwt/filter/JwtAuthFilter.java @@ -9,6 +9,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; @@ -17,9 +19,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; -import java.util.List; - @RequiredArgsConstructor @Slf4j @Component @@ -29,12 +28,13 @@ public class JwtAuthFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) { - return true; // 개발 중 모든 요청 허용 -// return httpServletRequest.getRequestURI().contains("token"); + String token = request.getHeader("Authorization"); + return token == null || token.isEmpty(); } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { EncodedToken accessToken = getAccessToken(request); jwtUseCase.processAccessToken(accessToken, response); @@ -47,15 +47,19 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse private EncodedToken getAccessToken(HttpServletRequest request) { String accessToken = request.getHeader("Authorization"); - if (accessToken == null || accessToken.isEmpty()) { + if (!accessToken.startsWith("Bearer ")) { throw new JwtException(JwtErrorType.MISSING_TOKEN); } + + accessToken = accessToken.substring(7); + return new EncodedToken(accessToken); } - private JwtAuthenticationToken createAuthenticationToken(Claims claims, EncodedToken accessToken) { + private JwtAuthenticationToken createAuthenticationToken(Claims claims, + EncodedToken accessToken) { String userId = claims.get("id", String.class); - UserRole role = claims.get("role", UserRole.class); + UserRole role = UserRole.valueOf(claims.get("role", String.class)); return new JwtAuthenticationToken( userId, diff --git a/src/main/java/com/somemore/auth/util/DevAccountSetUpConfig.java b/src/main/java/com/somemore/auth/util/DevAccountSetUpConfig.java new file mode 100644 index 000000000..42e98fb60 --- /dev/null +++ b/src/main/java/com/somemore/auth/util/DevAccountSetUpConfig.java @@ -0,0 +1,99 @@ +package com.somemore.auth.util; + +import static com.somemore.auth.oauth.OAuthProvider.NAVER; + +import com.somemore.auth.jwt.domain.EncodedToken; +import com.somemore.auth.jwt.domain.TokenType; +import com.somemore.auth.jwt.domain.UserRole; +import com.somemore.auth.jwt.generator.JwtGenerator; +import com.somemore.auth.jwt.refresh.domain.RefreshToken; +import com.somemore.auth.jwt.refresh.manager.RefreshTokenManager; +import com.somemore.center.domain.Center; +import com.somemore.center.repository.CenterJpaRepository; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.repository.VolunteerJpaRepository; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DevAccountSetUpConfig { + + private final VolunteerJpaRepository volunteerRepository; + private final CenterJpaRepository centerRepository; + private final JwtGenerator jwtGenerator; + private final RefreshTokenManager refreshTokenManager; + + private Volunteer volunteer; + private Center center; + + @Value("${app.develop.mode}") + private boolean developMode; + + @PostConstruct + public void generateAccessTokenForDev() { + if (!developMode) { + return; // 개발 모드에서만 실행 + } + + volunteer = Volunteer.createDefault(NAVER, "bongdari"); + center = Center.create( + "봉다리 자원봉사센터", + "02-1234-5678", + "", + "봉다리 기관 테스트 계정입니다.", + "https://somemore.bongdari.com", + "bongdari", + "1234" + ); + + volunteer = volunteerRepository.findByOauthId(volunteer.getOauthId()) + .orElseGet(() -> volunteerRepository.save(volunteer)); + + center = centerRepository.findByName(center.getName()) + .orElseGet(() -> centerRepository.save(center)); + + EncodedToken volunteerToken = saveRefreshTokenAndReturnAccessToken(volunteer.getId(), + UserRole.VOLUNTEER); + EncodedToken centerToken = saveRefreshTokenAndReturnAccessToken(center.getId(), + UserRole.CENTER); + + log.info("테스트용 봉사자 AccessToken: {}", volunteerToken.value()); + log.info("테스트용 기관 AccessToken: {}", centerToken.value()); + } + + @PreDestroy + public void cleanup() { + if (volunteer != null) { + refreshTokenManager.removeRefreshToken(volunteer.getId().toString()); + log.info("테스트용 AccessToken 제거, 봉사자 ID: {}", volunteer.getId()); + } + if (center != null) { + refreshTokenManager.removeRefreshToken(center.getId().toString()); + log.info("테스트용 AccessToken 제거, 기관 ID: {}", center.getId()); + } + } + + private EncodedToken saveRefreshTokenAndReturnAccessToken(UUID id, UserRole role) { + EncodedToken accessToken = generateToken(id, role, TokenType.ACCESS); + RefreshToken refreshToken = generateRefreshToken(id, role, accessToken); + refreshTokenManager.save(refreshToken); + return accessToken; + } + + private EncodedToken generateToken(UUID id, UserRole role, TokenType tokenType) { + return jwtGenerator.generateToken(id.toString(), role.name(), tokenType); + } + + private RefreshToken generateRefreshToken(UUID id, UserRole role, EncodedToken accessToken) { + return new RefreshToken(id.toString(), accessToken, + generateToken(id, role, TokenType.REFRESH)); + } +} + diff --git a/src/main/java/com/somemore/center/repository/CenterJpaRepository.java b/src/main/java/com/somemore/center/repository/CenterJpaRepository.java index fde29e33a..c68bb8441 100644 --- a/src/main/java/com/somemore/center/repository/CenterJpaRepository.java +++ b/src/main/java/com/somemore/center/repository/CenterJpaRepository.java @@ -9,4 +9,5 @@ public interface CenterJpaRepository extends JpaRepository { boolean existsById(UUID id); Optional
findCenterById(UUID id); + Optional
findByName(String name); } diff --git a/src/main/java/com/somemore/community/domain/CommunityComment.java b/src/main/java/com/somemore/community/domain/CommunityComment.java index 42acddc2d..36cb2c792 100644 --- a/src/main/java/com/somemore/community/domain/CommunityComment.java +++ b/src/main/java/com/somemore/community/domain/CommunityComment.java @@ -22,6 +22,9 @@ public class CommunityComment extends BaseEntity { @Column(name = "id", nullable = false) private Long id; + @Column(name = "community_board_id", nullable = false) + private Long communityBoardId; + @Column(name = "writer_id", nullable = false, length = 16) private UUID writerId; @@ -33,7 +36,8 @@ public class CommunityComment extends BaseEntity { private Long parentCommentId; @Builder - public CommunityComment(UUID writerId, String content, Long parentCommentId) { + public CommunityComment(Long communityBoardId, UUID writerId, String content, Long parentCommentId) { + this.communityBoardId = communityBoardId; this.writerId = writerId; this.content = content; this.parentCommentId = parentCommentId; diff --git a/src/main/java/com/somemore/community/dto/request/CommunityCommentCreateRequestDto.java b/src/main/java/com/somemore/community/dto/request/CommunityCommentCreateRequestDto.java index 4366779a0..2c6a5b104 100644 --- a/src/main/java/com/somemore/community/dto/request/CommunityCommentCreateRequestDto.java +++ b/src/main/java/com/somemore/community/dto/request/CommunityCommentCreateRequestDto.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import java.util.UUID; @@ -13,15 +14,19 @@ @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @Builder public record CommunityCommentCreateRequestDto( + @Schema(description = "커뮤니티 게시글 ID", example = "33") + @NotNull(message = "게시글 ID는 필수 값입니다.") + Long communityBoardId, @Schema(description = "커뮤니티 댓글 내용", example = "저도 함께 하고 싶습니다.") @NotBlank(message = "댓글 내용은 필수 값입니다.") String content, - @Schema(description = "부모 댓글의 ID", example = "1234", nullable = true) + @Schema(description = "부모 댓글 ID", example = "1234", nullable = true) @Nullable Long parentCommentId ) { public CommunityComment toEntity(UUID writerId) { return CommunityComment.builder() + .communityBoardId(communityBoardId) .writerId(writerId) .content(content) .parentCommentId(parentCommentId) diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardJpaRepository.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardJpaRepository.java index 79b03cd1c..00d96d29e 100644 --- a/src/main/java/com/somemore/community/repository/board/CommunityBoardJpaRepository.java +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardJpaRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface CommunityBoardJpaRepository extends JpaRepository { + boolean existsByIdAndDeletedFalse(Long id); } diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java index 3340735a5..44b34f517 100644 --- a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java @@ -12,5 +12,9 @@ public interface CommunityBoardRepository { Optional findById(Long id); List getCommunityBoards(); List findByWriterId(UUID writerId); + boolean existsById(Long id); + default boolean doesNotExistById(Long id) { + return !existsById(id); + } void deleteAllInBatch(); } diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java index ad1d79924..72bed7542 100644 --- a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java @@ -52,6 +52,11 @@ public List findByWriterId(UUID writerId) { .fetch(); } + @Override + public boolean existsById(Long id) { + return communityBoardJpaRepository.existsByIdAndDeletedFalse(id); + } + private JPAQuery getCommunityBoardsQuery() { QCommunityBoard communityBoard = QCommunityBoard.communityBoard; QVolunteer volunteer = QVolunteer.volunteer; diff --git a/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java b/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java index 07722e607..c5db8228b 100644 --- a/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java +++ b/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java @@ -8,5 +8,8 @@ public interface CommunityCommentRepository { CommunityComment save(CommunityComment communityComment); Optional findById(Long id); boolean existsById(Long id); + default boolean doesNotExistById(Long id) { + return !existsById(id); + } void deleteAllInBatch(); } diff --git a/src/main/java/com/somemore/community/service/comment/CreateCommunityCommentService.java b/src/main/java/com/somemore/community/service/comment/CreateCommunityCommentService.java index e6da18066..ba7a116db 100644 --- a/src/main/java/com/somemore/community/service/comment/CreateCommunityCommentService.java +++ b/src/main/java/com/somemore/community/service/comment/CreateCommunityCommentService.java @@ -2,6 +2,7 @@ import com.somemore.community.domain.CommunityComment; import com.somemore.community.dto.request.CommunityCommentCreateRequestDto; +import com.somemore.community.repository.board.CommunityBoardRepository; import com.somemore.community.repository.comment.CommunityCommentRepository; import com.somemore.community.usecase.comment.CreateCommunityCommentUseCase; import com.somemore.global.exception.BadRequestException; @@ -11,6 +12,7 @@ import java.util.UUID; +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_COMMUNITY_BOARD; import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_COMMUNITY_COMMENT; @RequiredArgsConstructor @@ -18,19 +20,30 @@ @Service public class CreateCommunityCommentService implements CreateCommunityCommentUseCase { + private final CommunityBoardRepository communityBoardRepository; private final CommunityCommentRepository communityCommentRepository; @Override public Long createCommunityComment(CommunityCommentCreateRequestDto requestDto, UUID writerId) { CommunityComment communityComment = requestDto.toEntity(writerId); - validateParentCommentExists(communityComment.getParentCommentId()); + validateCommunityBoardExists(communityComment.getCommunityBoardId()); + + if (requestDto.parentCommentId() != null) { + validateParentCommentExists(communityComment.getParentCommentId()); + } return communityCommentRepository.save(communityComment).getId(); } + private void validateCommunityBoardExists(Long communityBoardId) { + if (communityBoardRepository.doesNotExistById(communityBoardId)) { + throw new BadRequestException(NOT_EXISTS_COMMUNITY_BOARD.getMessage()); + } + } + private void validateParentCommentExists(Long parentCommentId) { - if (parentCommentId != null && !communityCommentRepository.existsById(parentCommentId)) { + if (communityCommentRepository.doesNotExistById(parentCommentId)) { throw new BadRequestException(NOT_EXISTS_COMMUNITY_COMMENT.getMessage()); } } diff --git a/src/main/java/com/somemore/domains/InterestCenter.java b/src/main/java/com/somemore/domains/InterestCenter.java deleted file mode 100644 index 4c35d5119..000000000 --- a/src/main/java/com/somemore/domains/InterestCenter.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.somemore.domains; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.util.UUID; - -@Getter -@Setter -@Entity -@Table(name = "interest_center") -public class InterestCenter { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - -} \ No newline at end of file diff --git a/src/main/java/com/somemore/global/common/response/ApiResponse.java b/src/main/java/com/somemore/global/common/response/ApiResponse.java index ebdd60c3a..6b00a6c07 100644 --- a/src/main/java/com/somemore/global/common/response/ApiResponse.java +++ b/src/main/java/com/somemore/global/common/response/ApiResponse.java @@ -15,14 +15,10 @@ public static ApiResponse ok(int status, T data, String message) { return new ApiResponse<>(status, message, data); } - public static ApiResponse ok(String message) { + public static ApiResponse ok(String message) { return new ApiResponse<>(200, message, ""); } - public static ApiResponse error(int code, String message) { - return new ApiResponse<>(code, message, ""); - } - public ApiResponse(int code, String message, T data) { this.code = code; this.message = message; diff --git a/src/main/java/com/somemore/global/configure/SwaggerConfig.java b/src/main/java/com/somemore/global/configure/SwaggerConfig.java new file mode 100644 index 000000000..0ea82150a --- /dev/null +++ b/src/main/java/com/somemore/global/configure/SwaggerConfig.java @@ -0,0 +1,36 @@ +package com.somemore.global.configure; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + SecurityScheme securityScheme = new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("JWT 토큰을 이용한 인증"); + + SecurityRequirement securityRequirement = new SecurityRequirement() + .addList("AccessToken"); + + return new OpenAPI() + .info(new Info() + .title("Somemore API") + .version("1.0") + .description("Somemore swagger-ui 화면입니다.") + ) + .components(new Components() + .addSecuritySchemes("AccessToken", securityScheme) + ) + .addSecurityItem(securityRequirement); + } +} diff --git a/src/main/java/com/somemore/global/exception/DuplicateException.java b/src/main/java/com/somemore/global/exception/DuplicateException.java new file mode 100644 index 000000000..0a752c189 --- /dev/null +++ b/src/main/java/com/somemore/global/exception/DuplicateException.java @@ -0,0 +1,8 @@ +package com.somemore.global.exception; + +public class DuplicateException extends RuntimeException{ + + public DuplicateException(final String message) { + super(message); + } +} diff --git a/src/main/java/com/somemore/global/exception/ExceptionMessage.java b/src/main/java/com/somemore/global/exception/ExceptionMessage.java index 43a1be574..1aa5d8fdd 100644 --- a/src/main/java/com/somemore/global/exception/ExceptionMessage.java +++ b/src/main/java/com/somemore/global/exception/ExceptionMessage.java @@ -21,6 +21,8 @@ public enum ExceptionMessage { FILE_SIZE_EXCEEDED("파일 크기가 허용된 한도를 초과했습니다."), EMPTY_FILE("파일이 존재하지 않습니다."), INSTANTIATION_NOT_ALLOWED("인스턴스화 할 수 없는 클래스 입니다."), + CANNOT_CANCEL_DELETED_INTEREST_CENTER("이미 삭제된 관심 기관은 취소할 수 없습니다."), + DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다.") ; private final String message; diff --git a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java index 3f6cc0736..48fb90dfc 100644 --- a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java @@ -1,17 +1,14 @@ package com.somemore.global.handler; - import com.somemore.global.exception.BadRequestException; +import com.somemore.global.exception.DuplicateException; import com.somemore.global.exception.ImageUploadException; -import org.springframework.data.crossstore.ChangeSetPersister; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @@ -39,4 +36,13 @@ ProblemDetail handleImageUploadException(final ImageUploadException e) { return problemDetail; } + @ExceptionHandler(DuplicateException.class) + ProblemDetail handleDuplicateException(final DuplicateException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); + problemDetail.setTitle("중복 예외"); + + return problemDetail; + } + } diff --git a/src/main/java/com/somemore/interestcenter/controller/InterestCenterCommandApiController.java b/src/main/java/com/somemore/interestcenter/controller/InterestCenterCommandApiController.java new file mode 100644 index 000000000..bcd3df349 --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/controller/InterestCenterCommandApiController.java @@ -0,0 +1,40 @@ +package com.somemore.interestcenter.controller; + +import com.somemore.global.common.response.ApiResponse; +import com.somemore.interestcenter.dto.request.RegisterInterestCenterRequestDto; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; +import com.somemore.interestcenter.usecase.CancelInterestCenterUseCase; +import com.somemore.interestcenter.usecase.RegisterInterestCenterUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@Tag(name = "Interest Center Command API", description = "관심 기관의 등록과 취소 API를 제공합니다") +public class InterestCenterCommandApiController { + + private final RegisterInterestCenterUseCase registerInterestCenterUseCase; + private final CancelInterestCenterUseCase cancelInterestCenterUseCase; + + @Operation(summary = "관심기관 등록 API") + @PostMapping("/api/interest-center") + public ApiResponse registerInterestCenter(@Valid @RequestBody RegisterInterestCenterRequestDto requestDto) { + + RegisterInterestCenterResponseDto responseDto = registerInterestCenterUseCase.registerInterestCenter(requestDto); + + return ApiResponse.ok(200, responseDto, "관심 기관 등록 성공"); + } + + @Operation(summary = "관심기관 취소 API") + @DeleteMapping("/api/interest-center/{interest-center-id}") + public ApiResponse deleteInterestCenter(@PathVariable("interest-center-id") Long interestCenterId) { + + cancelInterestCenterUseCase.cancelInterestCenter(interestCenterId); + + return ApiResponse.ok("관심 기관 취소 성공"); + } + +} diff --git a/src/main/java/com/somemore/interestcenter/domain/InterestCenter.java b/src/main/java/com/somemore/interestcenter/domain/InterestCenter.java new file mode 100644 index 000000000..df1c045c7 --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/domain/InterestCenter.java @@ -0,0 +1,39 @@ +package com.somemore.interestcenter.domain; + +import com.somemore.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.util.UUID; + +import static jakarta.persistence.GenerationType.IDENTITY; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "interest_center") +public class InterestCenter extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(name = "volunteer_id", nullable = false) + private UUID volunteerId; + + @Column(name = "center_id", nullable = false) + private UUID centerId; + + @Builder + private InterestCenter(UUID volunteerId, UUID centerId) { + this.volunteerId = volunteerId; + this.centerId = centerId; + } + + public static InterestCenter create(UUID volunteerId, UUID centerId) { + return InterestCenter.builder() + .volunteerId(volunteerId) + .centerId(centerId) + .build(); + } +} diff --git a/src/main/java/com/somemore/interestcenter/dto/request/RegisterInterestCenterRequestDto.java b/src/main/java/com/somemore/interestcenter/dto/request/RegisterInterestCenterRequestDto.java new file mode 100644 index 000000000..beb917c84 --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/dto/request/RegisterInterestCenterRequestDto.java @@ -0,0 +1,25 @@ +package com.somemore.interestcenter.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.interestcenter.domain.InterestCenter; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record RegisterInterestCenterRequestDto( + + @Schema(description = "봉사자 ID", example = "123e4567-e89b-12d3-a456-426614174000") + @NotNull(message = "봉사자 ID는 필수값입니다.") + UUID volunteerId, + + @Schema(description = "기관 ID", example = "123e4567-e89b-12d3-a456-426614174000") + @NotNull(message = "기관 ID는 필수값입니다.") + UUID centerId +) { + public InterestCenter toEntity(){ + return InterestCenter.create(volunteerId, centerId); + } +} diff --git a/src/main/java/com/somemore/interestcenter/dto/response/RegisterInterestCenterResponseDto.java b/src/main/java/com/somemore/interestcenter/dto/response/RegisterInterestCenterResponseDto.java new file mode 100644 index 000000000..4a20fa957 --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/dto/response/RegisterInterestCenterResponseDto.java @@ -0,0 +1,30 @@ +package com.somemore.interestcenter.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.interestcenter.domain.InterestCenter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.UUID; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Builder +public record RegisterInterestCenterResponseDto( + @Schema(description = "관심 ID", example = "1111") + Long id, + + @Schema(description = "봉사자 ID", example = "123e4567-e89b-12d3-a456-426614174000") + UUID volunteerId, + + @Schema(description = "센터 ID", example = "123e4567-e89b-12d3-a456-426614174000") + UUID centerId +) { + public static RegisterInterestCenterResponseDto from(InterestCenter interestCenter) { + return RegisterInterestCenterResponseDto.builder() + .id(interestCenter.getId()) + .volunteerId(interestCenter.getVolunteerId()) + .centerId(interestCenter.getCenterId()) + .build(); + } +} diff --git a/src/main/java/com/somemore/interestcenter/repository/InterestCenterJpaRepository.java b/src/main/java/com/somemore/interestcenter/repository/InterestCenterJpaRepository.java new file mode 100644 index 000000000..34b9a37e1 --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/repository/InterestCenterJpaRepository.java @@ -0,0 +1,7 @@ +package com.somemore.interestcenter.repository; + +import com.somemore.interestcenter.domain.InterestCenter; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterestCenterJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/somemore/interestcenter/repository/InterestCenterRepository.java b/src/main/java/com/somemore/interestcenter/repository/InterestCenterRepository.java new file mode 100644 index 000000000..14b07b2d2 --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/repository/InterestCenterRepository.java @@ -0,0 +1,14 @@ +package com.somemore.interestcenter.repository; + +import com.somemore.interestcenter.domain.InterestCenter; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; + +import java.util.Optional; +import java.util.UUID; + +public interface InterestCenterRepository { + InterestCenter save(InterestCenter interestCenter); + Optional findById(Long id); + Optional findInterestCenterResponseById(Long id); + boolean existsByVolunteerIdAndCenterId(UUID volunteerId, UUID centerId); +} diff --git a/src/main/java/com/somemore/interestcenter/repository/InterestCenterRepositoryImpl.java b/src/main/java/com/somemore/interestcenter/repository/InterestCenterRepositoryImpl.java new file mode 100644 index 000000000..5667967b8 --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/repository/InterestCenterRepositoryImpl.java @@ -0,0 +1,81 @@ +package com.somemore.interestcenter.repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.somemore.interestcenter.domain.InterestCenter; +import com.somemore.interestcenter.domain.QInterestCenter; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@RequiredArgsConstructor +@Repository +public class InterestCenterRepositoryImpl implements InterestCenterRepository { + + private final JPAQueryFactory queryFactory; + private final InterestCenterJpaRepository interestCenterJpaRepository; + + @Override + public InterestCenter save(InterestCenter interestCenter) { + return interestCenterJpaRepository.save(interestCenter); + } + + @Override + public Optional findById(Long id) { + QInterestCenter interestCenter = QInterestCenter.interestCenter; + + InterestCenter result = queryFactory + .selectFrom(interestCenter) + .where( + interestCenter.id.eq(id) + .and(interestCenter.deleted.eq(false)) + ) + .fetchOne(); + + return Optional.ofNullable(result); + } + + @Override + public Optional findInterestCenterResponseById(Long id) { + QInterestCenter interestCenter = QInterestCenter.interestCenter; + + RegisterInterestCenterResponseDto result = queryFactory + .select( + Projections.constructor( + RegisterInterestCenterResponseDto.class, + interestCenter.id, + interestCenter.volunteerId, + interestCenter.centerId + ) + ) + .from(interestCenter) + .where( + interestCenter.id.eq(id) + .and(interestCenter.deleted.eq(false)) + ) + .fetchOne(); + + return Optional.ofNullable(result); + } + + @Override + public boolean existsByVolunteerIdAndCenterId(UUID volunteerId, UUID centerId) { + QInterestCenter interestCenter = QInterestCenter.interestCenter; + + Integer result = queryFactory + .selectOne() + .from(interestCenter) + .where( + interestCenter.volunteerId.eq(volunteerId) + .and(interestCenter.centerId.eq(centerId)) + .and(interestCenter.deleted.eq(false)) + ) + .fetchFirst(); + + return result != null; + } + +} diff --git a/src/main/java/com/somemore/interestcenter/service/CancelInterestCenterService.java b/src/main/java/com/somemore/interestcenter/service/CancelInterestCenterService.java new file mode 100644 index 000000000..a3c72145c --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/service/CancelInterestCenterService.java @@ -0,0 +1,28 @@ +package com.somemore.interestcenter.service; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.interestcenter.domain.InterestCenter; +import com.somemore.interestcenter.repository.InterestCenterRepository; +import com.somemore.interestcenter.usecase.CancelInterestCenterUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.somemore.global.exception.ExceptionMessage.CANNOT_CANCEL_DELETED_INTEREST_CENTER; + +@RequiredArgsConstructor +@Service +public class CancelInterestCenterService implements CancelInterestCenterUseCase { + + private final InterestCenterRepository interestCenterRepository; + + @Override + public void cancelInterestCenter(Long interestCenterId) { + InterestCenter interestCenter = interestCenterRepository.findById(interestCenterId) + .orElseThrow(() -> new BadRequestException(CANNOT_CANCEL_DELETED_INTEREST_CENTER.getMessage())); + + interestCenter.markAsDeleted(); + + interestCenterRepository.save(interestCenter); + } + +} diff --git a/src/main/java/com/somemore/interestcenter/service/RegisterInterestCenterService.java b/src/main/java/com/somemore/interestcenter/service/RegisterInterestCenterService.java new file mode 100644 index 000000000..c9fb8ce3d --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/service/RegisterInterestCenterService.java @@ -0,0 +1,37 @@ +package com.somemore.interestcenter.service; + +import com.somemore.center.usecase.query.CenterQueryUseCase; +import com.somemore.global.exception.DuplicateException; +import com.somemore.interestcenter.domain.InterestCenter; +import com.somemore.interestcenter.dto.request.RegisterInterestCenterRequestDto; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; +import com.somemore.interestcenter.repository.InterestCenterRepository; +import com.somemore.interestcenter.usecase.RegisterInterestCenterUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.somemore.global.exception.ExceptionMessage.DUPLICATE_INTEREST_CENTER; + +@RequiredArgsConstructor +@Service +public class RegisterInterestCenterService implements RegisterInterestCenterUseCase { + + private final InterestCenterRepository repository; + private final CenterQueryUseCase centerQueryUseCase; + + @Override + public RegisterInterestCenterResponseDto registerInterestCenter(RegisterInterestCenterRequestDto requestDto) { + + centerQueryUseCase.validateCenterExists(requestDto.centerId()); + + boolean isDuplicate = repository.existsByVolunteerIdAndCenterId(requestDto.volunteerId(), requestDto.centerId()); + if(isDuplicate){ + throw new DuplicateException(DUPLICATE_INTEREST_CENTER.getMessage()); + } + + InterestCenter interestCenter = repository.save(requestDto.toEntity()); + + return RegisterInterestCenterResponseDto.from(interestCenter); + } + +} diff --git a/src/main/java/com/somemore/interestcenter/usecase/CancelInterestCenterUseCase.java b/src/main/java/com/somemore/interestcenter/usecase/CancelInterestCenterUseCase.java new file mode 100644 index 000000000..53515a1cd --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/usecase/CancelInterestCenterUseCase.java @@ -0,0 +1,5 @@ +package com.somemore.interestcenter.usecase; + +public interface CancelInterestCenterUseCase { + void cancelInterestCenter(Long interestCenterId); +} diff --git a/src/main/java/com/somemore/interestcenter/usecase/RegisterInterestCenterUseCase.java b/src/main/java/com/somemore/interestcenter/usecase/RegisterInterestCenterUseCase.java new file mode 100644 index 000000000..f6ce68e7a --- /dev/null +++ b/src/main/java/com/somemore/interestcenter/usecase/RegisterInterestCenterUseCase.java @@ -0,0 +1,8 @@ +package com.somemore.interestcenter.usecase; + +import com.somemore.interestcenter.dto.request.RegisterInterestCenterRequestDto; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; + +public interface RegisterInterestCenterUseCase { + RegisterInterestCenterResponseDto registerInterestCenter(RegisterInterestCenterRequestDto requestDto); +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6621c5d18..cdbad3892 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,8 @@ app: front-url: ${FRONT_URL} back-url: ${BACK_URL} + develop: + mode: ${APP_DEVELOP_MODE} # AWS S3 cloud: diff --git a/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java b/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java index 2f6fdbd52..acb29075f 100644 --- a/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java +++ b/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java @@ -158,4 +158,27 @@ void getCommunityBoardsByWriterId() { assertThat(communityBoards.getLast().writerNickname()).isEqualTo(volunteer.getNickname()); assertThat(communityBoards.getLast().communityBoard().getCreatedAt()).isEqualTo(communityBoard1.getCreatedAt()); } + + @DisplayName("게시글 id로 게시글이 존재하는지 확인할 수 있다.") + @Test + void existsById() { + + //given + UUID writerId = UUID.randomUUID(); + + CommunityBoard communityBoard = CommunityBoard.builder() + .title("테스트 커뮤니티 게시글 제목") + .content("테스트 커뮤니티 게시글 내용") + .imgUrl("http://community.example.com/123") + .writerId(writerId) + .build(); + + CommunityBoard savedComment = communityBoardRepository.save(communityBoard); + + //when + boolean isExist = communityBoardRepository.existsById(savedComment.getId()); + + //then + assertThat(isExist).isTrue(); + } } diff --git a/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java b/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java index e3ba059d5..8903033ce 100644 --- a/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java +++ b/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java @@ -1,8 +1,11 @@ package com.somemore.community.repository; import com.somemore.IntegrationTestSupport; +import com.somemore.community.domain.CommunityBoard; import com.somemore.community.domain.CommunityComment; +import com.somemore.community.repository.board.CommunityBoardRepository; import com.somemore.community.repository.comment.CommunityCommentRepository; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,25 +18,47 @@ @Transactional class CommunityCommentRepositoryTest extends IntegrationTestSupport { + @Autowired CommunityCommentRepository communityCommentRepository; + @Autowired + CommunityBoardRepository communityBoardRepository; + + private Long boardId; + private UUID writerId; + private CommunityComment savedComment; + + @BeforeEach + void setUp() { + CommunityBoard communityBoard = CommunityBoard.builder() + .title("테스트 커뮤니티 게시글 제목") + .content("테스트 커뮤니티 게시글 내용") + .imgUrl("http://community.example.com/123") + .writerId(UUID.randomUUID()) + .build(); - @DisplayName("커뮤니티 게시글에 댓글을 생성할 수 있다. (Repository)") - @Test - void createCommunityComment() { + communityBoardRepository.save(communityBoard); - //given - UUID writerId = UUID.randomUUID(); + boardId = communityBoard.getId(); + + writerId = UUID.randomUUID(); CommunityComment communityComment = CommunityComment.builder() + .communityBoardId(boardId) .writerId(writerId) .content("커뮤니티 댓글 테스트 내용") .parentCommentId(null) .build(); - //when - CommunityComment savedComment = communityCommentRepository.save(communityComment); + savedComment = communityCommentRepository.save(communityComment); + } + + @DisplayName("커뮤니티 게시글에 댓글을 생성할 수 있다. (Repository)") + @Test + void createCommunityComment() { + //given + //when //then assertThat(savedComment.getWriterId()).isEqualTo(writerId); assertThat(savedComment.getContent()).isEqualTo("커뮤니티 댓글 테스트 내용"); @@ -45,21 +70,20 @@ void createCommunityComment() { void createCommunityCommentReply() { //given - UUID writerId = UUID.randomUUID(); - - CommunityComment communityComment = CommunityComment.builder() + CommunityComment communityCommentReply = CommunityComment.builder() + .communityBoardId(boardId) .writerId(writerId) .content("커뮤니티 댓글 테스트 내용") .parentCommentId(1L) .build(); //when - CommunityComment savedComment = communityCommentRepository.save(communityComment); + CommunityComment savedCommentReply = communityCommentRepository.save(communityCommentReply); //then - assertThat(savedComment.getWriterId()).isEqualTo(writerId); - assertThat(savedComment.getContent()).isEqualTo("커뮤니티 댓글 테스트 내용"); - assertThat(savedComment.getParentCommentId()).isEqualTo(1L); + assertThat(savedCommentReply.getWriterId()).isEqualTo(writerId); + assertThat(savedCommentReply.getContent()).isEqualTo("커뮤니티 댓글 테스트 내용"); + assertThat(savedCommentReply.getParentCommentId()).isEqualTo(1L); } @DisplayName("댓글을 id로 조회할 수 있다. (Repository)") @@ -67,16 +91,6 @@ void createCommunityCommentReply() { void findCommunityCommentById() { //given - UUID writerId = UUID.randomUUID(); - - CommunityComment communityComment = CommunityComment.builder() - .writerId(writerId) - .content("커뮤니티 댓글 테스트 내용") - .parentCommentId(null) - .build(); - - CommunityComment savedComment = communityCommentRepository.save(communityComment); - //when Optional comment = communityCommentRepository.findById(savedComment.getId()); @@ -92,16 +106,6 @@ void findCommunityCommentById() { void existsById() { //given - UUID writerId = UUID.randomUUID(); - - CommunityComment communityComment = CommunityComment.builder() - .writerId(writerId) - .content("커뮤니티 댓글 테스트 내용") - .parentCommentId(null) - .build(); - - CommunityComment savedComment = communityCommentRepository.save(communityComment); - //when boolean isExist = communityCommentRepository.existsById(savedComment.getId()); diff --git a/src/test/java/com/somemore/community/service/comment/CreateCommunityCommentServiceTest.java b/src/test/java/com/somemore/community/service/comment/CreateCommunityCommentServiceTest.java index bc955770c..b65a2c2c5 100644 --- a/src/test/java/com/somemore/community/service/comment/CreateCommunityCommentServiceTest.java +++ b/src/test/java/com/somemore/community/service/comment/CreateCommunityCommentServiceTest.java @@ -1,13 +1,16 @@ package com.somemore.community.service.comment; import com.somemore.IntegrationTestSupport; +import com.somemore.community.domain.CommunityBoard; import com.somemore.community.domain.CommunityComment; +import com.somemore.community.dto.request.CommunityBoardCreateRequestDto; import com.somemore.community.dto.request.CommunityCommentCreateRequestDto; +import com.somemore.community.repository.board.CommunityBoardRepository; import com.somemore.community.repository.comment.CommunityCommentRepository; import com.somemore.global.exception.BadRequestException; -import com.somemore.global.exception.ExceptionMessage; import org.assertj.core.api.ThrowableAssert; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,6 +18,8 @@ import java.util.Optional; import java.util.UUID; +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_COMMUNITY_BOARD; +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_COMMUNITY_COMMENT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -24,6 +29,24 @@ class CreateCommunityCommentServiceTest extends IntegrationTestSupport { private CreateCommunityCommentService createCommunityCommentService; @Autowired private CommunityCommentRepository communityCommentRepository; + @Autowired + private CommunityBoardRepository communityBoardRepository; + + private UUID writerId; + private Long boardId; + + @BeforeEach + void setUp() { + CommunityBoardCreateRequestDto boardDto = CommunityBoardCreateRequestDto.builder() + .title("커뮤니티 테스트 제목") + .content("커뮤니티 테스트 내용") + .build(); + + writerId = UUID.randomUUID(); + + CommunityBoard communityBoard = communityBoardRepository.save(boardDto.toEntity(writerId, "https://test.image/123")); + boardId = communityBoard.getId(); + } @AfterEach void tearDown() { @@ -36,12 +59,11 @@ void createCommunityCommentWithDto() { //given CommunityCommentCreateRequestDto dto = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) .content("커뮤니티 댓글 테스트 내용") .parentCommentId(null) .build(); - UUID writerId = UUID.randomUUID(); - //when Long commentId = createCommunityCommentService.createCommunityComment(dto, writerId); @@ -61,14 +83,15 @@ void createCommunityCommentRelyWithDto() { //given CommunityCommentCreateRequestDto commentDto = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) .content("커뮤니티 댓글 테스트 내용") .parentCommentId(null) .build(); - UUID writerId = UUID.randomUUID(); Long commentId = createCommunityCommentService.createCommunityComment(commentDto, writerId); CommunityCommentCreateRequestDto replyDto = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) .content("커뮤니티 대댓글 테스트 내용") .parentCommentId(commentId) .build(); @@ -86,12 +109,13 @@ void createCommunityCommentRelyWithDto() { assertThat(communityCommentReply.get().getParentCommentId()).isEqualTo(commentId); } - @DisplayName("삭제된 댓글에 대댓글을 등록할 때 예외를 던진다.") + @DisplayName("삭제된 댓글이나 존재하지 않는 댓글에 대댓글을 등록할 때 예외를 던진다.") @Test void createCommunityCommentReplyWithDeletedParentId() { //given CommunityCommentCreateRequestDto replyDto = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) .content("커뮤니티 대댓글 테스트 내용") .parentCommentId(2L) .build(); @@ -102,6 +126,28 @@ void createCommunityCommentReplyWithDeletedParentId() { //then assertThatExceptionOfType(BadRequestException.class) .isThrownBy(callable) - .withMessage(ExceptionMessage.NOT_EXISTS_COMMUNITY_COMMENT.getMessage()); + .withMessage(NOT_EXISTS_COMMUNITY_COMMENT.getMessage()); + } + + @DisplayName("삭제된 게시글에 댓글을 등록할 때 예외를 던진다.") + @Test + void createCommunityCommentWithDeletedBoardId() { + + //given + CommunityCommentCreateRequestDto commentDto = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) + .content("커뮤니티 댓글 테스트 내용") + .parentCommentId(null) + .build(); + + communityBoardRepository.deleteAllInBatch(); + + //when + ThrowableAssert.ThrowingCallable callable = () -> createCommunityCommentService.createCommunityComment(commentDto, UUID.randomUUID()); + + //then + assertThatExceptionOfType(BadRequestException.class) + .isThrownBy(callable) + .withMessage(NOT_EXISTS_COMMUNITY_BOARD.getMessage()); } } diff --git a/src/test/java/com/somemore/community/service/comment/DeleteCommunityCommentServiceTest.java b/src/test/java/com/somemore/community/service/comment/DeleteCommunityCommentServiceTest.java index 0b5df5d2c..2e6db5020 100644 --- a/src/test/java/com/somemore/community/service/comment/DeleteCommunityCommentServiceTest.java +++ b/src/test/java/com/somemore/community/service/comment/DeleteCommunityCommentServiceTest.java @@ -1,11 +1,13 @@ package com.somemore.community.service.comment; import com.somemore.IntegrationTestSupport; +import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.domain.CommunityComment; +import com.somemore.community.dto.request.CommunityBoardCreateRequestDto; import com.somemore.community.dto.request.CommunityCommentCreateRequestDto; +import com.somemore.community.repository.board.CommunityBoardRepository; import com.somemore.community.repository.comment.CommunityCommentRepository; -import com.somemore.community.usecase.comment.CreateCommunityCommentUseCase; import com.somemore.global.exception.BadRequestException; -import com.somemore.global.exception.ExceptionMessage; import org.assertj.core.api.ThrowableAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -15,6 +17,8 @@ import java.util.UUID; +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_COMMUNITY_COMMENT; +import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_COMMUNITY_COMMENT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -23,24 +27,33 @@ class DeleteCommunityCommentServiceTest extends IntegrationTestSupport { @Autowired private DeleteCommunityCommentService deleteCommunityCommentService; @Autowired - private CreateCommunityCommentUseCase createCommunityCommentUseCase; - @Autowired private CommunityCommentRepository communityCommentRepository; + @Autowired + private CommunityBoardRepository communityBoardRepository; private UUID writerId; private Long commentId; @BeforeEach void setUp() { + CommunityBoardCreateRequestDto boardDto = CommunityBoardCreateRequestDto.builder() + .title("커뮤니티 테스트 제목") + .content("커뮤니티 테스트 내용") + .build(); + + writerId = UUID.randomUUID(); + + CommunityBoard communityBoard = communityBoardRepository.save(boardDto.toEntity(writerId, "https://test.image/123")); + CommunityCommentCreateRequestDto dto = CommunityCommentCreateRequestDto.builder() + .communityBoardId(communityBoard.getId()) .content("커뮤니티 댓글 테스트 내용") .parentCommentId(null) .build(); - writerId = UUID.randomUUID(); + CommunityComment communityComment = communityCommentRepository.save(dto.toEntity(writerId)); - commentId = createCommunityCommentUseCase.createCommunityComment(dto, writerId); - } + commentId = communityComment.getId();} @AfterEach void tearDown() { @@ -50,6 +63,7 @@ void tearDown() { @DisplayName("댓글 id로 댓글을 삭제한다.") @Test void deleteCommunityCommentWithId() { + //given //when deleteCommunityCommentService.deleteCommunityComment(writerId, commentId); @@ -61,6 +75,7 @@ void deleteCommunityCommentWithId() { @DisplayName("삭제된 댓글의 id로 댓글을 삭제할 때 예외를 던진다.") @Test void deleteCommunityCommentWithDeletedId() { + //given deleteCommunityCommentService.deleteCommunityComment(writerId, commentId); @@ -70,12 +85,13 @@ void deleteCommunityCommentWithDeletedId() { //then assertThatExceptionOfType(BadRequestException.class) .isThrownBy(callable) - .withMessage(ExceptionMessage.NOT_EXISTS_COMMUNITY_COMMENT.getMessage()); + .withMessage(NOT_EXISTS_COMMUNITY_COMMENT.getMessage()); } @DisplayName("작성자가 아닌 id로 댓글을 삭제하고자 할 때 예외를 던진다.") @Test void deleteCommunityCommentWithNotWriterId() { + //given //when ThrowableAssert.ThrowingCallable callable = () -> deleteCommunityCommentService.deleteCommunityComment(UUID.randomUUID(), commentId); @@ -83,6 +99,6 @@ void deleteCommunityCommentWithNotWriterId() { //then assertThatExceptionOfType(BadRequestException.class) .isThrownBy(callable) - .withMessage(ExceptionMessage.UNAUTHORIZED_COMMUNITY_COMMENT.getMessage()); + .withMessage(UNAUTHORIZED_COMMUNITY_COMMENT.getMessage()); } } diff --git a/src/test/java/com/somemore/interestcenter/controller/InterestCenterCommandApiControllerTest.java b/src/test/java/com/somemore/interestcenter/controller/InterestCenterCommandApiControllerTest.java new file mode 100644 index 000000000..a49476b02 --- /dev/null +++ b/src/test/java/com/somemore/interestcenter/controller/InterestCenterCommandApiControllerTest.java @@ -0,0 +1,68 @@ +package com.somemore.interestcenter.controller; + +import com.somemore.ControllerTestSupport; +import com.somemore.interestcenter.dto.request.RegisterInterestCenterRequestDto; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; +import com.somemore.interestcenter.usecase.CancelInterestCenterUseCase; +import com.somemore.interestcenter.usecase.RegisterInterestCenterUseCase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.UUID; + +import static org.mockito.BDDMockito.given; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +class InterestCenterCommandApiControllerTest extends ControllerTestSupport { + + @MockBean + private RegisterInterestCenterUseCase registerInterestCenterUseCase; + + @MockBean + private CancelInterestCenterUseCase cancelInterestCenterUseCase; + + private RegisterInterestCenterRequestDto requestDto; + + @BeforeEach + void setUp() { + requestDto = new RegisterInterestCenterRequestDto( + UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), + UUID.fromString("123e4567-e89b-12d3-a456-426614174000") + ); + } + + @Test + void registerInterestCenter_ShouldReturnSuccess() throws Exception { + // given + RegisterInterestCenterResponseDto responseDto = new RegisterInterestCenterResponseDto(1L, requestDto.volunteerId(), requestDto.centerId()); + given(registerInterestCenterUseCase.registerInterestCenter(any(RegisterInterestCenterRequestDto.class))) + .willReturn(responseDto); + + // when & then + mockMvc.perform(post("/api/interest-center") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("관심 기관 등록 성공")) + .andExpect(jsonPath("$.data.id").value(responseDto.id())) + .andExpect(jsonPath("$.data.volunteer_id").value(responseDto.volunteerId().toString())) + .andExpect(jsonPath("$.data.center_id").value(responseDto.centerId().toString())); + } + + @Test + void deleteInterestCenter_ShouldReturnSuccess() throws Exception { + // given + Long interestCenterId = 1L; + doNothing().when(cancelInterestCenterUseCase).cancelInterestCenter(interestCenterId); + + // when & then + mockMvc.perform(delete("/api/interest-center/{interest-center-id}", interestCenterId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("관심 기관 취소 성공")); + } +} diff --git a/src/test/java/com/somemore/interestcenter/service/CancelInterestCenterServiceTest.java b/src/test/java/com/somemore/interestcenter/service/CancelInterestCenterServiceTest.java new file mode 100644 index 000000000..e89946ffc --- /dev/null +++ b/src/test/java/com/somemore/interestcenter/service/CancelInterestCenterServiceTest.java @@ -0,0 +1,98 @@ +package com.somemore.interestcenter.service; + +import com.somemore.IntegrationTestSupport; +import com.somemore.center.domain.Center; +import com.somemore.center.repository.CenterRepository; +import com.somemore.global.exception.BadRequestException; +import com.somemore.interestcenter.domain.InterestCenter; +import com.somemore.interestcenter.dto.request.RegisterInterestCenterRequestDto; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; +import com.somemore.interestcenter.repository.InterestCenterRepository; +import com.somemore.interestcenter.usecase.CancelInterestCenterUseCase; +import com.somemore.interestcenter.usecase.RegisterInterestCenterUseCase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +import static com.somemore.global.exception.ExceptionMessage.CANNOT_CANCEL_DELETED_INTEREST_CENTER; +import static org.junit.jupiter.api.Assertions.*; + +@Transactional +class CancelInterestCenterServiceTest extends IntegrationTestSupport { + + @Autowired + private CancelInterestCenterUseCase cancelInterestCenterUseCase; + + @Autowired + private RegisterInterestCenterUseCase registerInterestCenterUseCase; + + @Autowired + private InterestCenterRepository interestCenterRepository; + + @Autowired + private CenterRepository centerRepository; + + @DisplayName("봉사자는 기관에 대한 관심 표시를 취소할 수 있다.") + @Test + void CancelInterestCenter() { + //given + Center center = createCenter(); + UUID volunteerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + UUID centerId = center.getId(); + RegisterInterestCenterRequestDto requestDto = new RegisterInterestCenterRequestDto(volunteerId, centerId); + + RegisterInterestCenterResponseDto responseDto = registerInterestCenterUseCase.registerInterestCenter(requestDto); + + InterestCenter savedInterestCenter = interestCenterRepository.findById(responseDto.id()) + .orElseThrow(() -> new IllegalStateException("등록된 관심 기관이 없습니다.")); + assertEquals(savedInterestCenter.getId(), responseDto.id()); + + //when + cancelInterestCenterUseCase.cancelInterestCenter(responseDto.id()); + + //then + Optional deletedInterestCenterOptional = interestCenterRepository.findById(responseDto.id()); + assertTrue(deletedInterestCenterOptional.isEmpty()); + + } + + @DisplayName("이미 삭제된 관심 기관을 다시 취소하려 하면 예외가 발생한다.") + @Test + void cancelInterestCenter_AlreadyDeleted_ShouldThrowException() { + //given + Center center = createCenter(); + UUID volunteerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + UUID centerId = center.getId(); + RegisterInterestCenterRequestDto requestDto = new RegisterInterestCenterRequestDto(volunteerId, centerId); + RegisterInterestCenterResponseDto responseDto = registerInterestCenterUseCase.registerInterestCenter(requestDto); + cancelInterestCenterUseCase.cancelInterestCenter(responseDto.id()); + + //when, then + long interestCenterId = responseDto.id(); + assertThrows(BadRequestException.class, + () -> cancelInterestCenterUseCase.cancelInterestCenter(interestCenterId), + CANNOT_CANCEL_DELETED_INTEREST_CENTER.getMessage() + ); + } + + private Center createCenter() { + Center center = Center.create( + "기본 기관 이름", + "010-1234-5678", + "http://example.com/image.jpg", + "기관 소개 내용", + "http://example.com", + "account123", + "password123" + ); + + centerRepository.save(center); + + return center; + } + +} diff --git a/src/test/java/com/somemore/interestcenter/service/RegisterInterestCenterServiceTest.java b/src/test/java/com/somemore/interestcenter/service/RegisterInterestCenterServiceTest.java new file mode 100644 index 000000000..a7e7bbeee --- /dev/null +++ b/src/test/java/com/somemore/interestcenter/service/RegisterInterestCenterServiceTest.java @@ -0,0 +1,109 @@ +package com.somemore.interestcenter.service; + +import com.somemore.IntegrationTestSupport; +import com.somemore.center.domain.Center; +import com.somemore.center.repository.CenterRepository; +import com.somemore.global.exception.BadRequestException; +import com.somemore.global.exception.DuplicateException; +import com.somemore.interestcenter.dto.request.RegisterInterestCenterRequestDto; +import com.somemore.interestcenter.dto.response.RegisterInterestCenterResponseDto; +import com.somemore.interestcenter.repository.InterestCenterRepository; +import com.somemore.interestcenter.usecase.RegisterInterestCenterUseCase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@Transactional +class RegisterInterestCenterServiceTest extends IntegrationTestSupport { + + @Autowired + private RegisterInterestCenterUseCase registerInterestCenter; + + @Autowired + private InterestCenterRepository interestCenterRepository; + + @Autowired + private CenterRepository centerRepository; + + @DisplayName("봉사자는 관심 기관을 등록할 수 있다.") + @Test + void RegisterInterestCenter() { + //given + Center center = createCenter(); + UUID volunteerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + UUID centerId = center.getId(); + RegisterInterestCenterRequestDto requestDto = new RegisterInterestCenterRequestDto(volunteerId, centerId); + + //when + RegisterInterestCenterResponseDto responseDto = registerInterestCenter.registerInterestCenter(requestDto); + + //then + Optional result = interestCenterRepository.findInterestCenterResponseById(responseDto.id()); + assertTrue(result.isPresent()); + assertEquals(responseDto.id(), result.get().id()); + assertEquals(volunteerId, result.get().volunteerId()); + assertEquals(centerId, result.get().centerId()); + } + + @DisplayName("이미 관심 표시한 기관에 관심 표시를 시도하면 예외를 던져준다.") + @Test + void registerInterestCenter_WithDuplicateCenterId_ShouldThrowException() { + // given + Center center = createCenter(); + UUID volunteerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + UUID centerId = center.getId(); + RegisterInterestCenterRequestDto requestDto = new RegisterInterestCenterRequestDto(volunteerId, centerId); + + registerInterestCenter.registerInterestCenter(requestDto); + + // when + DuplicateException exception = assertThrows( + DuplicateException.class, + () -> registerInterestCenter.registerInterestCenter(requestDto) + ); + + // then + assertEquals("이미 관심 표시한 기관입니다.", exception.getMessage()); + } + + + @DisplayName("존재하지 않는 기관 Id로 관심 기관 등록 시 예외가 발생한다.") + @Test + void registerInterestCenter_WithInvalidCenterId_ShouldThrowException() { + // given + UUID volunteerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + UUID invalidCenterId = UUID.fromString("123e4567-e89b-12d3-a456-426614174001"); + RegisterInterestCenterRequestDto requestDto = new RegisterInterestCenterRequestDto(volunteerId, invalidCenterId); + + // when + BadRequestException exception = assertThrows(BadRequestException.class, () -> { + registerInterestCenter.registerInterestCenter(requestDto); + }); + + //then + assertEquals("존재하지 않는 기관 입니다.", exception.getMessage()); + } + + private Center createCenter() { + Center center = Center.create( + "기본 기관 이름", + "010-1234-5678", + "http://example.com/image.jpg", + "기관 소개 내용", + "http://example.com", + "account123", + "password123" + ); + + centerRepository.save(center); + + return center; + } + +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 50d9922c6..d0d564d75 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,6 +1,8 @@ app: front-url: "http://localhost:3000" back-url: "http://localhost:8080" + develop: + mode: false spring: config: