diff --git a/build.gradle b/build.gradle index 254f1c58..64b5d751 100644 --- a/build.gradle +++ b/build.gradle @@ -40,9 +40,18 @@ dependencies { // Spring Actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' + // Spring Cache + implementation 'org.springframework.boot:spring-boot-starter-cache' + + // Spring Retry + implementation 'org.springframework.retry:spring-retry' + // H2 runtimeOnly 'com.h2database:h2' + // Caffeine + implementation 'com.github.ben-manes.caffeine:caffeine' + // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/src/main/java/darkoverload/itzip/ItzipApplication.java b/src/main/java/darkoverload/itzip/ItzipApplication.java index 072e8d3f..e65a0eda 100644 --- a/src/main/java/darkoverload/itzip/ItzipApplication.java +++ b/src/main/java/darkoverload/itzip/ItzipApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableScheduling; +@EnableRetry @EnableScheduling @EnableJpaAuditing @SpringBootApplication diff --git a/src/main/java/darkoverload/itzip/feature/jwt/infrastructure/CustomUserDetails.java b/src/main/java/darkoverload/itzip/feature/jwt/infrastructure/CustomUserDetails.java index b1dcc0fc..a4542d5f 100644 --- a/src/main/java/darkoverload/itzip/feature/jwt/infrastructure/CustomUserDetails.java +++ b/src/main/java/darkoverload/itzip/feature/jwt/infrastructure/CustomUserDetails.java @@ -42,6 +42,10 @@ public String getUsername() { return email; } + public String getUserNickname() { + return nickname; + } + @Override public boolean isAccountNonExpired() { return true; diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/BlogEventHandler.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/BlogEventHandler.java new file mode 100644 index 00000000..36253091 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/BlogEventHandler.java @@ -0,0 +1,31 @@ +package darkoverload.itzip.feature.techinfo.application.event.handler; + +import darkoverload.itzip.feature.techinfo.application.event.payload.UserCreatedEvent; +import darkoverload.itzip.feature.techinfo.application.service.command.BlogCommandService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * 회원 생성 이벤트를 처리하여 블로그 생성을 수행하는 이벤트 핸들러입니다. + * + *

+ * 트랜잭션이 커밋된 후에 실행되며, + * {@link UserCreatedEvent} 발생 시 {@link BlogCommandService#create(Long)} 를 호출합니다. + *

+ */ +@Component +public class BlogEventHandler { + + private final BlogCommandService blogCommandService; + + public BlogEventHandler(final BlogCommandService blogCommandService) { + this.blogCommandService = blogCommandService; + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleUserCreated(final UserCreatedEvent event) { + blogCommandService.create(event.userId()); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/CommentEventHandler.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/CommentEventHandler.java new file mode 100644 index 00000000..9e24cc7f --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/CommentEventHandler.java @@ -0,0 +1,26 @@ +package darkoverload.itzip.feature.techinfo.application.event.handler; + +import darkoverload.itzip.feature.techinfo.application.event.payload.ArticleHiddenEvent; +import darkoverload.itzip.feature.techinfo.application.service.command.CommentCommandService; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 아티클 삭제(숨김) 이벤트를 처리하여 해당 아티클의 댓글을 삭제(숨김) 수행하는 핸들러입니다. + */ +@Component +public class CommentEventHandler { + + private final CommentCommandService commandService; + + public CommentEventHandler(final CommentCommandService commandService) { + this.commandService = commandService; + } + + @EventListener + public void handleArticleHidden(final ArticleHiddenEvent event) { + final String articleIdHex = event.articleId().toHexString(); + commandService.deleteByArticleId(articleIdHex); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/LikeEventHandler.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/LikeEventHandler.java new file mode 100644 index 00000000..5353082b --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/LikeEventHandler.java @@ -0,0 +1,33 @@ +package darkoverload.itzip.feature.techinfo.application.event.handler; + +import darkoverload.itzip.feature.techinfo.application.event.payload.LikeCancelledEvent; +import darkoverload.itzip.feature.techinfo.application.event.payload.LikedEvent; +import darkoverload.itzip.feature.techinfo.application.service.cache.LikeCacheService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * 좋아요 및 취소 이벤트를 수행하는 핸들러입니다. + * 트랜잭션 커밋 이후에 실행됩니다. + */ +@Component +public class LikeEventHandler { + + private final LikeCacheService cacheService; + + public LikeEventHandler(final LikeCacheService likeCacheService) { + this.cacheService = likeCacheService; + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleArticleLikedEvent(final LikedEvent event) { + cacheService.merge(event.articleId()); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleArticleLikeCancelledEvent(final LikeCancelledEvent event) { + cacheService.subtract(event.articleId()); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/ViewEventHandler.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/ViewEventHandler.java new file mode 100644 index 00000000..2810d658 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/handler/ViewEventHandler.java @@ -0,0 +1,25 @@ +package darkoverload.itzip.feature.techinfo.application.event.handler; + +import darkoverload.itzip.feature.techinfo.application.event.payload.ViewedEvent; +import darkoverload.itzip.feature.techinfo.application.service.cache.ViewCacheService; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 조회 이벤트를 수행하는 핸들러입니다. + */ +@Component +public class ViewEventHandler { + + private final ViewCacheService cacheService; + + public ViewEventHandler(final ViewCacheService cacheService) { + this.cacheService = cacheService; + } + + @EventListener + public void handleArticleViewedEvent(final ViewedEvent event) { + cacheService.merge(event.articleId()); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/ArticleHiddenEvent.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/ArticleHiddenEvent.java new file mode 100644 index 00000000..d372aefe --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/ArticleHiddenEvent.java @@ -0,0 +1,11 @@ +package darkoverload.itzip.feature.techinfo.application.event.payload; + +import org.bson.types.ObjectId; + +/** + * 아티클 숨김 이벤트를 나타내는 레코드입니다. + * + *

아티클이 숨김 처리될 때 발생하며, 숨김 대상 아티클의 식별자를 포함합니다.

+ */ +public record ArticleHiddenEvent(ObjectId articleId) { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/LikeCancelledEvent.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/LikeCancelledEvent.java new file mode 100644 index 00000000..a485aa65 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/LikeCancelledEvent.java @@ -0,0 +1,9 @@ +package darkoverload.itzip.feature.techinfo.application.event.payload; + +/** + * 좋아요 취소 이벤트를 나타내는 레코드입니다. + * + *

아티클의 좋아요가 취소될 때 발생하며, 해당 아티클의 식별자를 포함합니다.

+ */ +public record LikeCancelledEvent(String articleId) { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/LikedEvent.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/LikedEvent.java new file mode 100644 index 00000000..ce280213 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/LikedEvent.java @@ -0,0 +1,9 @@ +package darkoverload.itzip.feature.techinfo.application.event.payload; + +/** + * 좋아요 이벤트를 나타내는 레코드입니다. + * + *

아티클에 좋아요가 발생했을 때 발생하며, 해당 게시글의 식별자를 포함합니다.

+ */ +public record LikedEvent(String articleId) { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/UserCreatedEvent.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/UserCreatedEvent.java new file mode 100644 index 00000000..d5c77a6f --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/UserCreatedEvent.java @@ -0,0 +1,9 @@ +package darkoverload.itzip.feature.techinfo.application.event.payload; + +/** + * 사용자 생성 이벤트를 나타내는 레코드입니다. + * + *

사용자가 생성되었을 때 발생하며, 생성된 사용자의 식별자를 포함합니다.

+ */ +public record UserCreatedEvent(Long userId) { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/ViewedEvent.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/ViewedEvent.java new file mode 100644 index 00000000..17292bf4 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/event/payload/ViewedEvent.java @@ -0,0 +1,11 @@ +package darkoverload.itzip.feature.techinfo.application.event.payload; + +import org.bson.types.ObjectId; + +/** + * 조회 이벤트를 나타내는 레코드입니다. + * + *

아티클이 조회될 때 발생하며, 조회된 게시글의 식별자를 포함합니다.

+ */ +public record ViewedEvent(ObjectId articleId) { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/PageableGenerator.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/PageableGenerator.java new file mode 100644 index 00000000..ee935a36 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/PageableGenerator.java @@ -0,0 +1,29 @@ +package darkoverload.itzip.feature.techinfo.application.generator; + +import darkoverload.itzip.feature.techinfo.application.type.SortType; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * 페이지와 정렬 옵션에 따른 Pageable 객체를 생성하는 유틸리티 클래스입니다. + */ +public class PageableGenerator { + + private static final String FIELD_VIEW_COUNT = "viewCount"; + private static final String FIELD_LIKE_COUNT = "likesCount"; + private static final String FIELD_CREATED_AT = "createdAt"; + + private PageableGenerator() { + } + + public static Pageable generate(final int page, final int size, final SortType type) { + return switch (type) { + case VIEW_COUNT-> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, FIELD_VIEW_COUNT)); + case LIKE_COUNT -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, FIELD_LIKE_COUNT)); + case OLDEST -> PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, FIELD_CREATED_AT)); + default -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, FIELD_CREATED_AT)); + }; + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/PagedModelGenerator.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/PagedModelGenerator.java new file mode 100644 index 00000000..c6bac12d --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/PagedModelGenerator.java @@ -0,0 +1,31 @@ +package darkoverload.itzip.feature.techinfo.application.generator; + +import org.springframework.data.domain.Page; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.PagedModel; +import org.springframework.hateoas.PagedModel.PageMetadata; + +/** + * Spring Data의 Page 객체를 HATEOAS의 PagedModel로 변환하는 유틸리티 클래스입니다. + */ +public class PagedModelGenerator { + + private PagedModelGenerator() { + } + + public static PagedModel> generate(final Page page) { + final PageMetadata metadata = new PageMetadata( + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages()); + + return PagedModel.of( + page.stream() + .map(EntityModel::of) + .toList(), + metadata + ); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/UpperCaseGenerator.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/UpperCaseGenerator.java new file mode 100644 index 00000000..ec06698c --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/generator/UpperCaseGenerator.java @@ -0,0 +1,20 @@ +package darkoverload.itzip.feature.techinfo.application.generator; + +import java.util.Objects; + +/** + * 문자열을 대문자로 변환하는 유틸리티 클래스입니다. + */ +public class UpperCaseGenerator { + + private UpperCaseGenerator() { + } + + public static String generate(final String value) { + if (Objects.isNull(value) || value.isBlank()) { + throw new IllegalArgumentException("NULL 혹은 공백인 값을 변환할 수 없습니다."); + } + return value.toUpperCase(); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/LikeCacheService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/LikeCacheService.java new file mode 100644 index 00000000..3762af7d --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/LikeCacheService.java @@ -0,0 +1,11 @@ +package darkoverload.itzip.feature.techinfo.application.service.cache; + +public interface LikeCacheService { + + void merge(String articleId); + + void subtract(String articleId); + + void flush(); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/ViewCacheService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/ViewCacheService.java new file mode 100644 index 00000000..1654fe97 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/ViewCacheService.java @@ -0,0 +1,11 @@ +package darkoverload.itzip.feature.techinfo.application.service.cache; + +import org.bson.types.ObjectId; + +public interface ViewCacheService { + + void merge(ObjectId articleId); + + void flush(); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/impl/LikeCacheServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/impl/LikeCacheServiceImpl.java new file mode 100644 index 00000000..c374979a --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/impl/LikeCacheServiceImpl.java @@ -0,0 +1,78 @@ +package darkoverload.itzip.feature.techinfo.application.service.cache.impl; + +import darkoverload.itzip.feature.techinfo.application.service.cache.LikeCacheService; +import darkoverload.itzip.feature.techinfo.application.service.command.ArticleCommandService; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.cache.LikeCacheRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * 좋아요 캐시 처리를 위한 서비스 구현체입니다. + * + *

+ * 좋아요 병합, 감소, 그리고 캐시 플러시 작업을 수행합니다. + * 재시도 로직과 비동기 처리(@Async)를 적용하여 일시적인 오류에 대비합니다. + *

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LikeCacheServiceImpl implements LikeCacheService { + + private static final String FLUSH_DELAY = "30000"; + + private final LikeCacheRepository cacheRepository; + + private final ArticleCommandService articleCommandService; + + @Async + @Retryable( + value = Exception.class, + maxAttempts = 3, + backoff = @Backoff(delay = 1000) + ) + @Override + public void merge(final String articleId) { + cacheRepository.merge(articleId, 1L); + } + + @Recover + public void recoverMerge(final Exception e, final String articleId) { + log.error("좋아요 병합 재시도 실패: {}", articleId); + } + + @Async + @Retryable( + value = Exception.class, + maxAttempts = 3, + backoff = @Backoff(delay = 1000) + ) + @Override + public void subtract(final String articleId) { + cacheRepository.merge(articleId, -1L); + } + + @Recover + public void recoverSubtract(final Exception e, final String articleId) { + log.error("좋아요 감소 재시도 실패: {}", articleId); + } + + @Scheduled(fixedDelayString = FLUSH_DELAY) + public void flush() { + final Map batch = cacheRepository.retrieveAll(); + if (batch.isEmpty()) { + return; + } + cacheRepository.clear(); + batch.forEach(articleCommandService::updateLikesCount); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/impl/ViewCacheServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/impl/ViewCacheServiceImpl.java new file mode 100644 index 00000000..777f1e54 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/cache/impl/ViewCacheServiceImpl.java @@ -0,0 +1,61 @@ +package darkoverload.itzip.feature.techinfo.application.service.cache.impl; + +import darkoverload.itzip.feature.techinfo.application.service.cache.ViewCacheService; +import darkoverload.itzip.feature.techinfo.application.service.command.ArticleCommandService; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.cache.ViewCacheRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * 조회수 캐시 처리를 위한 서비스 구현체입니다. + * + *

비동기, 재시도, 스케줄링을 활용하여 조회수 캐시 병합 및 플러시 작업을 수행합니다.

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ViewCacheServiceImpl implements ViewCacheService { + + private static final String FLUSH_DELAY = "30000"; + + private final ViewCacheRepository viewCacheRepository; + + private final ArticleCommandService articleCommandService; + + @Async + @Retryable( + value = Exception.class, + maxAttempts = 3, + backoff = @Backoff(delay = 1000) + ) + @Override + public void merge(final ObjectId articleId) { + viewCacheRepository.merge(articleId, 1L); + } + + @Recover + public void recoverMerge(final Exception e, final ObjectId articleId) { + log.error("뷰 캐시 병합 재시도 실패: {}", articleId); + } + + @Scheduled(fixedDelayString = FLUSH_DELAY) + @Override + public void flush() { + final Map batch = viewCacheRepository.retrieveAll(); + if (batch.isEmpty()) { + return; + } + viewCacheRepository.clear(); + batch.forEach(articleCommandService::updateViewCount); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/ArticleCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/ArticleCommandService.java new file mode 100644 index 00000000..28401079 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/ArticleCommandService.java @@ -0,0 +1,20 @@ +package darkoverload.itzip.feature.techinfo.application.service.command; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleRegistrationRequest; +import org.bson.types.ObjectId; + +public interface ArticleCommandService { + + String create(CustomUserDetails userDetails, ArticleRegistrationRequest request); + + void update(CustomUserDetails userDetails, ArticleEditRequest request); + + void delete(CustomUserDetails userDetails, String articleId); + + void updateViewCount(ObjectId id, long count); + + void updateLikesCount(String id, long count); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/BlogCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/BlogCommandService.java new file mode 100644 index 00000000..464af49f --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/BlogCommandService.java @@ -0,0 +1,12 @@ +package darkoverload.itzip.feature.techinfo.application.service.command; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.ui.payload.request.blog.BlogIntroEditRequest; + +public interface BlogCommandService { + + void create(Long userId); + + void updateIntro(CustomUserDetails userDetails, BlogIntroEditRequest request); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/CommentCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/CommentCommandService.java new file mode 100644 index 00000000..eb325cc8 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/CommentCommandService.java @@ -0,0 +1,17 @@ +package darkoverload.itzip.feature.techinfo.application.service.command; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentRegistrationRequest; + +public interface CommentCommandService { + + void create(CustomUserDetails userDetails, CommentRegistrationRequest request); + + void update(CustomUserDetails userDetails, CommentEditRequest request); + + void delete(CustomUserDetails userDetails, Long commentId); + + void deleteByArticleId(String articleId); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/LikeCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/LikeCommandService.java new file mode 100644 index 00000000..16f0bcb5 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/LikeCommandService.java @@ -0,0 +1,11 @@ +package darkoverload.itzip.feature.techinfo.application.service.command; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; + +public interface LikeCommandService { + + void create(CustomUserDetails userDetails, String articleId); + + void delete(CustomUserDetails userDetails, String articleId); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/ScrapCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/ScrapCommandService.java new file mode 100644 index 00000000..8a2cea23 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/ScrapCommandService.java @@ -0,0 +1,11 @@ +package darkoverload.itzip.feature.techinfo.application.service.command; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; + +public interface ScrapCommandService { + + void create(CustomUserDetails userDetails, String articleId); + + void delete(CustomUserDetails userDetails, String articleId); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ArticleCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ArticleCommandServiceImpl.java new file mode 100644 index 00000000..9ff3f70c --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ArticleCommandServiceImpl.java @@ -0,0 +1,87 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.event.payload.ArticleHiddenEvent; +import darkoverload.itzip.feature.techinfo.application.service.command.ArticleCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.BlogQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleRegistrationRequest; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class ArticleCommandServiceImpl implements ArticleCommandService { + + private final ArticleRepository articleRepository; + + private final BlogQueryService blogQueryService; + + private final ApplicationEventPublisher eventPublisher; + + @Override + public String create(final CustomUserDetails userDetails, final ArticleRegistrationRequest request) { + this.checkUserDetails(userDetails); + final Long blogId = blogQueryService.getBlogIdByUserNickname(userDetails.getUserNickname()); + final Article article = Article.create( + blogId, + request.type(), + request.title(), + request.content(), + request.thumbnailImageUri() + ); + articleRepository.save(article); + return article.getId().toHexString(); + } + + @Override + public void update(final CustomUserDetails userDetails, final ArticleEditRequest request) { + this.checkUserDetails(userDetails); + final Long blogId = blogQueryService.getBlogIdByUserNickname(userDetails.getUserNickname()); + final Article article = articleRepository.findByIdAndBlogId(new ObjectId(request.articleId()), blogId) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND)); + final Article updatedArticle = article.update( + request.type(), + request.title(), + request.content(), + request.thumbnailImageUri() + ); + articleRepository.save(updatedArticle); + } + + @Override + public void delete(final CustomUserDetails userDetails, final String articleId) { + this.checkUserDetails(userDetails); + final Long blogId = blogQueryService.getBlogIdByUserNickname(userDetails.getUserNickname()); + final Article article = articleRepository.findByIdAndBlogId(new ObjectId(articleId), blogId) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND)); + article.hide(); + articleRepository.save(article); + eventPublisher.publishEvent(new ArticleHiddenEvent(article.getId())); + } + + @Override + public void updateViewCount(final ObjectId id, final long count) { + articleRepository.updateViewCount(id, count); + } + + @Override + public void updateLikesCount(final String id, final long count) { + articleRepository.updateLikesCount(new ObjectId(id), count); + } + + private void checkUserDetails(final CustomUserDetails userDetails) { + if (Objects.isNull(userDetails)) { + throw new RestApiException(CommonExceptionCode.UNAUTHORIZED); + } + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/BlogCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/BlogCommandServiceImpl.java new file mode 100644 index 00000000..8ad2210e --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/BlogCommandServiceImpl.java @@ -0,0 +1,64 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.service.command.BlogCommandService; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import darkoverload.itzip.feature.techinfo.domain.repository.BlogRepository; +import darkoverload.itzip.feature.techinfo.ui.payload.request.blog.BlogIntroEditRequest; +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.feature.user.repository.UserRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BlogCommandServiceImpl implements BlogCommandService { + + private final BlogRepository blogRepository; + private final UserRepository userRepository; + + @Async + @Retryable( + value = RestApiException.class, + maxAttempts = 3, + backoff = @Backoff(delay = 1000, multiplier = 2) + ) + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public void create(final Long userId) { + final UserEntity user = userRepository.findById(userId) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER)); + final Blog blog = Blog.create(user); + blogRepository.save(blog); + log.debug("블로그 생성 완료: {}", blog.getId()); + } + + @Recover + public void recoverCreate(final RestApiException e, final Long userId) { + log.error("블로그 생성 재시도 실패: {}", userId); + } + + @Transactional + @Override + public void updateIntro(final CustomUserDetails userDetails, final BlogIntroEditRequest request) { + if (Objects.isNull(userDetails)) { + throw new RestApiException(CommonExceptionCode.UNAUTHORIZED); + } + final Blog blog = blogRepository.findBlogByUser_Nickname(userDetails.getUserNickname()) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.BLOG_NOT_FOUND)); + blog.updateIntro(request.intro()); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/CommentCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/CommentCommandServiceImpl.java new file mode 100644 index 00000000..42b794d2 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/CommentCommandServiceImpl.java @@ -0,0 +1,87 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.service.command.CommentCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.ArticleQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Comment; +import darkoverload.itzip.feature.techinfo.domain.repository.CommentRepository; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentRegistrationRequest; +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.feature.user.repository.UserRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class CommentCommandServiceImpl implements CommentCommandService { + + private final CommentRepository commentRepository; + private final UserRepository userRepository; + + private final ArticleQueryService articleQueryService; + + @Override + public void create(final CustomUserDetails userDetails, final CommentRegistrationRequest request) { + this.checkUserDetails(userDetails); + final UserEntity user = userRepository.findByNickname(userDetails.getUserNickname()) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER)); + if (!articleQueryService.existsById(request.articleId())) { + throw new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + final Comment comment = Comment.create(user, request.articleId(), request.content()); + commentRepository.save(comment); + } + + @Override + public void update(final CustomUserDetails userDetails, final CommentEditRequest request) { + this.checkUserDetails(userDetails); + final Comment comment = commentRepository.findByIdAndUser_Nickname(request.commentId(), userDetails.getUserNickname()) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.COMMENT_NOT_FOUND)); + comment.updateContent(request.content()); + } + + @Override + public void delete(final CustomUserDetails userDetails, final Long commentId) { + this.checkUserDetails(userDetails); + final Comment comment = commentRepository.findByIdAndUser_Nickname(commentId, userDetails.getUserNickname()) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.COMMENT_NOT_FOUND)); + comment.hide(); + } + + @Async + @Retryable( + value = DataAccessException.class, + maxAttempts = 3, + backoff = @Backoff(delay = 1000, multiplier = 2) + ) + @Override + public void deleteByArticleId(final String articleId) { + commentRepository.setDisplayedFalseByArticleId(articleId); + } + + @Recover + public void recoverDeleteByArticleId(final DataAccessException e, final String articleId) { + log.error("댓글 삭제 재시도 실패: {}", articleId); + } + + private void checkUserDetails(final CustomUserDetails userDetails) { + if (Objects.isNull(userDetails)) { + throw new RestApiException(CommonExceptionCode.UNAUTHORIZED); + } + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/LikeCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/LikeCommandServiceImpl.java new file mode 100644 index 00000000..eef161f6 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/LikeCommandServiceImpl.java @@ -0,0 +1,64 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.event.payload.LikeCancelledEvent; +import darkoverload.itzip.feature.techinfo.application.event.payload.LikedEvent; +import darkoverload.itzip.feature.techinfo.application.service.command.LikeCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.ArticleQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Like; +import darkoverload.itzip.feature.techinfo.domain.repository.LikeRepository; +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.feature.user.repository.UserRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@Service +@Transactional +@RequiredArgsConstructor +public class LikeCommandServiceImpl implements LikeCommandService { + + private final LikeRepository likeRepository; + private final UserRepository userRepository; + + private final ArticleQueryService articleQueryService; + + private final ApplicationEventPublisher eventPublisher; + + @Override + public void create(final CustomUserDetails userDetails, final String articleId) { + this.checkUserDetails(userDetails); + final UserEntity user = userRepository.findByNickname(userDetails.getNickname()) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER)); + if (!articleQueryService.existsById(articleId)) { + throw new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + final Like like = Like.create(user, articleId); + try { + likeRepository.save(like); + } catch (DataIntegrityViolationException e) { + throw new RestApiException(CommonExceptionCode.ALREADY_LIKED_ARTICLE); + } + eventPublisher.publishEvent(new LikedEvent(articleId)); + } + + @Override + public void delete(final CustomUserDetails userDetails, final String articleId) { + this.checkUserDetails(userDetails); + likeRepository.deleteByUser_NicknameAndArticleId(userDetails.getNickname(), articleId); + eventPublisher.publishEvent(new LikeCancelledEvent(articleId)); + } + + private void checkUserDetails(final CustomUserDetails userDetails) { + if (Objects.isNull(userDetails)) { + throw new RestApiException(CommonExceptionCode.UNAUTHORIZED); + } + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ScrapCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ScrapCommandServiceImpl.java new file mode 100644 index 00000000..7d80a8cd --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ScrapCommandServiceImpl.java @@ -0,0 +1,57 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.service.command.ScrapCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.ArticleQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Scrap; +import darkoverload.itzip.feature.techinfo.domain.repository.ScrapRepository; +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.feature.user.repository.UserRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@Service +@Transactional +@RequiredArgsConstructor +public class ScrapCommandServiceImpl implements ScrapCommandService { + + private final ScrapRepository scrapRepository; + private final UserRepository userRepository; + + private final ArticleQueryService articleQueryService; + + @Override + public void create(final CustomUserDetails userDetails, final String articleId) { + this.checkUserDetails(userDetails); + final UserEntity user = userRepository.findByNickname(userDetails.getUserNickname()) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER)); + if (!articleQueryService.existsById(articleId)) { + throw new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + final Scrap scrap = Scrap.create(user, articleId); + try { + scrapRepository.save(scrap); + } catch (DataIntegrityViolationException e) { + throw new RestApiException(CommonExceptionCode.ALREADY_SCRAP_ARTICLE); + } + } + + @Override + public void delete(final CustomUserDetails userDetails, final String articleId) { + this.checkUserDetails(userDetails); + scrapRepository.deleteByUser_NicknameAndArticleId(userDetails.getNickname(), articleId); + } + + private void checkUserDetails(final CustomUserDetails userDetails) { + if (Objects.isNull(userDetails)) { + throw new RestApiException(CommonExceptionCode.UNAUTHORIZED); + } + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/ArticleQueryService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/ArticleQueryService.java new file mode 100644 index 00000000..67140894 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/ArticleQueryService.java @@ -0,0 +1,25 @@ +package darkoverload.itzip.feature.techinfo.application.service.query; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.ui.payload.response.ArticleResponse; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl.YearlyArticleStatistics; +import org.springframework.data.domain.Page; + +import java.time.LocalDateTime; +import java.util.List; + +public interface ArticleQueryService { + + boolean existsById(String id); + + ArticleResponse getArticleById(CustomUserDetails userDetails, String id); + + Page getArticlesPreviewByType(String articleType, int page, int size, String sortType); + + Page getArticlesPreviewByAuthor(String nickname, int page, int size, String sortType); + + List getYearlyArticleStatisticsByBlogId(Long blogId); + + List getAdjacentArticles(Long blogId, String articleType, LocalDateTime createdAt); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/BlogQueryService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/BlogQueryService.java new file mode 100644 index 00000000..9a5b4a35 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/BlogQueryService.java @@ -0,0 +1,21 @@ +package darkoverload.itzip.feature.techinfo.application.service.query; + +import darkoverload.itzip.feature.techinfo.ui.payload.response.BlogResponse; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; + +import java.util.Map; +import java.util.Set; + +public interface BlogQueryService { + + BlogResponse getBlogResponseById(Long id); + + Blog getBlogById(Long id); + + BlogResponse getBlogResponseByUserNickname(String nickname); + + Long getBlogIdByUserNickname(String nickname); + + Map getBlogMapByIds(Set blogIds); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/CommentQueryService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/CommentQueryService.java new file mode 100644 index 00000000..d6e2cd80 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/CommentQueryService.java @@ -0,0 +1,10 @@ +package darkoverload.itzip.feature.techinfo.application.service.query; + +import darkoverload.itzip.feature.techinfo.ui.payload.response.CommentResponse; +import org.springframework.data.domain.Page; + +public interface CommentQueryService { + + Page getCommentsByArticleId(String articleId, int page, int size); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/LikeQueryService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/LikeQueryService.java new file mode 100644 index 00000000..1817f39e --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/LikeQueryService.java @@ -0,0 +1,7 @@ +package darkoverload.itzip.feature.techinfo.application.service.query; + +public interface LikeQueryService { + + boolean existsByUserNicknameAndArticleId(String nickname, String articleId); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/ScrapQueryService.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/ScrapQueryService.java new file mode 100644 index 00000000..5d4075c2 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/ScrapQueryService.java @@ -0,0 +1,7 @@ +package darkoverload.itzip.feature.techinfo.application.service.query; + +public interface ScrapQueryService { + + boolean existsByUserNicknameAndArticleId(String nickname, String articleId); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ArticleQueryServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ArticleQueryServiceImpl.java new file mode 100644 index 00000000..3815d3ff --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ArticleQueryServiceImpl.java @@ -0,0 +1,177 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.event.payload.ViewedEvent; +import darkoverload.itzip.feature.techinfo.application.generator.PageableGenerator; +import darkoverload.itzip.feature.techinfo.ui.payload.response.ArticleResponse; +import darkoverload.itzip.feature.techinfo.application.service.query.ArticleQueryService; +import darkoverload.itzip.feature.techinfo.application.service.query.BlogQueryService; +import darkoverload.itzip.feature.techinfo.application.service.query.LikeQueryService; +import darkoverload.itzip.feature.techinfo.application.service.query.ScrapQueryService; +import darkoverload.itzip.feature.techinfo.application.type.SortType; +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.entity.ArticleType; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import darkoverload.itzip.feature.techinfo.domain.projection.ArticlePreview; +import darkoverload.itzip.feature.techinfo.domain.projection.ArticleSummary; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl.YearlyArticleStatistics; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.NonNull; +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@CacheConfig( + cacheManager = "caffeineCacheManager", + cacheNames = "articlesPreview" +) +@Service +public class ArticleQueryServiceImpl implements ArticleQueryService { + + private final ArticleRepository articleRepository; + + private final BlogQueryService blogQueryService; + private final LikeQueryService likeQueryService; + private final ScrapQueryService scrapQueryService; + + private final Executor asyncExecutor; + private final ApplicationEventPublisher eventPublisher; + + public ArticleQueryServiceImpl( + final ArticleRepository articleRepository, + final BlogQueryService blogQueryService, + final LikeQueryService likeQueryService, + final ScrapQueryService scrapQueryService, + @Qualifier("asyncExecutor") final Executor asyncExecutor, + final ApplicationEventPublisher eventPublisher + ) { + this.articleRepository = articleRepository; + this.blogQueryService = blogQueryService; + this.likeQueryService = likeQueryService; + this.scrapQueryService = scrapQueryService; + this.asyncExecutor = asyncExecutor; + this.eventPublisher = eventPublisher; + } + + @Override + public boolean existsById(final String id) { + return articleRepository.existsById(new ObjectId(id)); + } + + @Override + public ArticleResponse getArticleById(final CustomUserDetails userDetails, final String id) { + final CompletableFuture isLikedFuture; + final CompletableFuture isScrappedFuture; + + if (Objects.nonNull(userDetails)) { + isLikedFuture = CompletableFuture.supplyAsync(() -> + likeQueryService.existsByUserNicknameAndArticleId(userDetails.getUserNickname(), id), + asyncExecutor + ); + isScrappedFuture = CompletableFuture.supplyAsync(() -> + scrapQueryService.existsByUserNicknameAndArticleId(userDetails.getUserNickname(), id), + asyncExecutor + ); + } else { + isLikedFuture = CompletableFuture.completedFuture(false); + isScrappedFuture = CompletableFuture.completedFuture(false); + } + + final Article article = articleRepository.findById(new ObjectId(id)) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND)); + final Blog blog = blogQueryService.getBlogById(article.getBlogId()); + + eventPublisher.publishEvent(new ViewedEvent(article.getId())); + + final boolean isLiked = isLikedFuture.join(); + final boolean isScrapped = isScrappedFuture.join(); + + return ArticleResponse.from(blog, article, isLiked, isScrapped); + } + + @Cacheable( + key = "#articleType + '_' + #page + '_' + #size + '_' + #sortType", + condition = "#page <= 5" + ) + @Override + public Page getArticlesPreviewByType(final String articleType, final int page, final int size, final String sortType) { + final Pageable pageable = PageableGenerator.generate(page, size, SortType.from(sortType)); + + final Page articles = (articleType == null || articleType.isBlank()) + ? articleRepository.findAllByDisplayedIsTrue(pageable) + : articleRepository.findAllByTypeAndDisplayedIsTrue(ArticleType.from(articleType), pageable); + + if (articles.isEmpty()) { + throw new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + final Set blogIds = articles.stream() + .map(ArticlePreview::getBlogId) + .collect(Collectors.toSet()); + final Map blogMap = blogQueryService.getBlogMapByIds(blogIds); + + return articles.map(article -> { + final Blog blog = blogMap.get(article.getBlogId()); + return ArticleResponse.previewFrom(blog, article); + }); + } + + @Override + public Page getArticlesPreviewByAuthor(final String nickname, final int page, final int size, final String sortType) { + final Long blogId = blogQueryService.getBlogIdByUserNickname(nickname); + final Pageable pageable = PageableGenerator.generate(page, size, SortType.from(sortType)); + final Page articles = articleRepository.findAllByBlogIdAndDisplayedIsTrue(blogId, pageable); + if (articles.isEmpty()) { + throw new RestApiException(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + return articles.map(ArticleResponse::previewFrom); + } + + @Override + public List getYearlyArticleStatisticsByBlogId(final Long blogId) { + return articleRepository.findArticleYearlyStatisticsByBlogId(blogId); + } + + @NonNull + @Override + public List getAdjacentArticles(final Long blogId, final String articleType, final LocalDateTime createdAt) { + final ArticleType type = ArticleType.from(articleType); + final Pageable limit = PageRequest.of(0, 2); + + final List nextArticleSummaries = articleRepository + .findNextArticlesByBlogIdAndDisplayedIsTrue(blogId, type, createdAt, limit); + + final List previousArticlesSummaries = articleRepository + .findPreviousArticlesByBlogIdAndDisplayedIsTrue(blogId, type, createdAt, limit); + + final List adjacentArticles = Stream.concat( + nextArticleSummaries + .stream() + .map(ArticleResponse::summaryFrom), + previousArticlesSummaries + .stream() + .map(ArticleResponse::summaryFrom) + ).toList(); + + return adjacentArticles; + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/BlogQueryServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/BlogQueryServiceImpl.java new file mode 100644 index 00000000..da5798f1 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/BlogQueryServiceImpl.java @@ -0,0 +1,65 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.ui.payload.response.BlogResponse; +import darkoverload.itzip.feature.techinfo.application.service.query.BlogQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import darkoverload.itzip.feature.techinfo.domain.repository.BlogRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +@Service +@Transactional(readOnly = true) +public class BlogQueryServiceImpl implements BlogQueryService { + + private final BlogRepository repository; + + public BlogQueryServiceImpl(final BlogRepository repository) { + this.repository = repository; + } + + @Override + public BlogResponse getBlogResponseById(final Long id) { + final Blog blog = repository.findById(id) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.BLOG_NOT_FOUND)); + return BlogResponse.from(blog); + } + + @Override + public BlogResponse getBlogResponseByUserNickname(final String nickname) { + final Blog blog = repository.findBlogByUser_Nickname(nickname) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.BLOG_NOT_FOUND)); + return BlogResponse.from(blog); + } + + @Override + public Blog getBlogById(final Long id) { + return repository.findById(id) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.BLOG_NOT_FOUND)); + } + + + @Override + public Long getBlogIdByUserNickname(final String nickname) { + log.info("블로그 닉네임 {}", nickname); + return repository.findBlogIdByUserNickname(nickname) + .orElseThrow(() -> new RestApiException(CommonExceptionCode.BLOG_NOT_FOUND)); + } + + @Override + public Map getBlogMapByIds(final Set blogIds) { + final List blogs = repository.findAllByIdIn(blogIds); + return blogs.stream() + .collect(Collectors.toMap(Blog::getId, Function.identity())); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/CommentQueryServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/CommentQueryServiceImpl.java new file mode 100644 index 00000000..71ec5209 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/CommentQueryServiceImpl.java @@ -0,0 +1,36 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.application.generator.PageableGenerator; +import darkoverload.itzip.feature.techinfo.ui.payload.response.CommentResponse; +import darkoverload.itzip.feature.techinfo.application.service.query.CommentQueryService; +import darkoverload.itzip.feature.techinfo.application.type.SortType; +import darkoverload.itzip.feature.techinfo.domain.entity.Comment; +import darkoverload.itzip.feature.techinfo.domain.repository.CommentRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class CommentQueryServiceImpl implements CommentQueryService { + + private final CommentRepository commentRepository; + + public CommentQueryServiceImpl(final CommentRepository commentRepository) { + this.commentRepository = commentRepository; + } + + @Override + public Page getCommentsByArticleId(final String articleId, final int page, final int size) { + final Pageable pageable = PageableGenerator.generate(page, size, SortType.NEWEST); + final Page comments = commentRepository.findAllByArticleId(articleId, pageable); + if (comments.isEmpty()) { + throw new RestApiException(CommonExceptionCode.COMMENT_NOT_FOUND); + } + return comments.map(CommentResponse::from); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/LikeQueryServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/LikeQueryServiceImpl.java new file mode 100644 index 00000000..cc8a858e --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/LikeQueryServiceImpl.java @@ -0,0 +1,23 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.application.service.query.LikeQueryService; +import darkoverload.itzip.feature.techinfo.domain.repository.LikeRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class LikeQueryServiceImpl implements LikeQueryService { + + private final LikeRepository likeRepository; + + public LikeQueryServiceImpl(final LikeRepository likeRepository) { + this.likeRepository = likeRepository; + } + + @Override + public boolean existsByUserNicknameAndArticleId(final String nickname, final String articleId) { + return likeRepository.existsByUser_NicknameAndArticleId(nickname, articleId); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ScrapQueryServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ScrapQueryServiceImpl.java new file mode 100644 index 00000000..f5a020e5 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ScrapQueryServiceImpl.java @@ -0,0 +1,23 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.application.service.query.ScrapQueryService; +import darkoverload.itzip.feature.techinfo.domain.repository.ScrapRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class ScrapQueryServiceImpl implements ScrapQueryService { + + private final ScrapRepository scrapRepository; + + public ScrapQueryServiceImpl(final ScrapRepository scrapRepository) { + this.scrapRepository = scrapRepository; + } + + @Override + public boolean existsByUserNicknameAndArticleId(final String nickname, final String articleId) { + return scrapRepository.existsByUser_NicknameAndArticleId(nickname, articleId); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/application/type/SortType.java b/src/main/java/darkoverload/itzip/feature/techinfo/application/type/SortType.java new file mode 100644 index 00000000..99aa7387 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/application/type/SortType.java @@ -0,0 +1,28 @@ +package darkoverload.itzip.feature.techinfo.application.type; + +import darkoverload.itzip.feature.techinfo.application.generator.UpperCaseGenerator; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; + +public enum SortType { + + NEWEST, + OLDEST, + VIEW_COUNT, + LIKE_COUNT; + + public static SortType from(String type) { + final String normalizedType = UpperCaseGenerator.generate(type); + validate(normalizedType); + return valueOf(normalizedType); + } + + public static void validate(String type) { + try { + valueOf(type); + } catch (IllegalArgumentException e) { + throw new RestApiException(CommonExceptionCode.SORT_TYPE_NOT_FOUND); + } + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogController.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogController.java deleted file mode 100644 index f93b9864..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogController.java +++ /dev/null @@ -1,57 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog; - -import darkoverload.itzip.feature.techinfo.controller.blog.response.BlogDetailsResponse; -import darkoverload.itzip.feature.techinfo.controller.blog.response.BlogResponse; -import darkoverload.itzip.feature.techinfo.service.blog.BlogReadService; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag( - name = "Tech Info Blog", - description = "기술 정보 블로그 기본 및 상세 정보 조회 기능을 제공하는 API" -) -@Validated -@RestController -@RequiredArgsConstructor -@RequestMapping("/tech-info/blog") -public class BlogController { - - private final BlogReadService blogReadService; - - @Operation( - summary = "블로그 기본 정보 조회", - description = "지정된 블로그 ID를 사용해 블로그 소개글과 소유자의 프로필 사진, 닉네임, 이메일을 반환합니다." - ) - @ExceptionCodeAnnotations({CommonExceptionCode.NOT_FOUND_BLOG}) - @GetMapping("{id}") - public BlogResponse getBlogBasicInfo( - @Parameter(description = "블로그 ID", example = "1") @PathVariable @NotNull Long id - ) { - return BlogResponse.from(blogReadService.getById(id)); - } - - @Operation( - summary = "블로그 상세 정보 조회", - description = "사용자 닉네임으로 블로그의 상세 정보(포스트 목록, 카테고리, 월별 포스트 통계)를 조회합니다." - ) - @GetMapping("/{nickname}/detail") - @ExceptionCodeAnnotations({CommonExceptionCode.NOT_FOUND_BLOG}) - public BlogDetailsResponse getBlogDetails( - @Parameter(description = "사용자 닉네임", example = "hyoseung") - @PathVariable @NotBlank String nickname - ) { - return BlogDetailsResponse.from(blogReadService.getBlogDetailByNickname(nickname)); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogEditController.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogEditController.java deleted file mode 100644 index cc69f553..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogEditController.java +++ /dev/null @@ -1,62 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.blog.request.BlogUpdateIntroRequest; -import darkoverload.itzip.feature.techinfo.service.blog.BlogCommandService; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -@Tag( - name = "Tech Info Blog", - description = "기술 정보 블로그 수정 및 삭제 기능을 제공하는 API" -) -@RestController -@RequiredArgsConstructor -@RequestMapping("/tech-info/blog") -public class BlogEditController { - - private final BlogCommandService blogCommandService; - - @Operation( - summary = "블로그 수정", - description = "회원의 블로그 소개글을 업데이트합니다." - ) - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_USER, - CommonExceptionCode.UPDATE_FAIL_BLOG - }) - @PatchMapping("/intro") - public String editBlogIntro( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody @Valid BlogUpdateIntroRequest request - ) { - blogCommandService.update(userDetails, request); - return "블로그 소개글이 성공적으로 수정되었습니다."; - } - - @Operation( - summary = "블로그 임시 삭제 (비공개 처리)", - description = "블로그를 비공개 상태로 설정합니다." - ) - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_BLOG, - CommonExceptionCode.UPDATE_FAIL_BLOG - }) - @PatchMapping("/{blogId}/status") - public String editBlogStatus( - @Parameter(description = "블로그 ID", example = "1") - @PathVariable @NotNull Long blogId - ) { - blogCommandService.updateStatus(blogId, false); - return "블로그가 성공적으로 비활성화되었습니다."; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogPostController.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogPostController.java deleted file mode 100644 index c30a8b34..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/BlogPostController.java +++ /dev/null @@ -1,68 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog; - -import darkoverload.itzip.feature.techinfo.controller.blog.response.BlogPostPreviewResponse; -import darkoverload.itzip.feature.techinfo.controller.blog.response.BlogRecentPostsResponse; -import darkoverload.itzip.feature.techinfo.service.blog.BlogReadService; -import darkoverload.itzip.feature.techinfo.service.post.PostReadService; -import darkoverload.itzip.feature.techinfo.type.SortType; -import darkoverload.itzip.feature.techinfo.util.PagedModelUtil; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.PagedModel; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; - -@Tag( - name = "Tech Info Blog", - description = "기술 정보 블로그 포스트 미리보기 및 조회 기능" -) -@RestController -@RequiredArgsConstructor -@RequestMapping("/tech-info/blog/posts") -public class BlogPostController { - - private final BlogReadService blogReadService; - private final PostReadService postReadService; - - @Operation( - summary = "블로그의 최근 인접 포스트 조회", - description = "주어진 블로그 ID와 특정 생성 날짜를 사용하여 해당 블로그의 인접한 포스트 목록을 조회한다." - ) - @ExceptionCodeAnnotations({CommonExceptionCode.NOT_FOUND_BLOG}) - @GetMapping("/recent") - public BlogRecentPostsResponse getBlogRecentPosts( - @Parameter(description = "블로그 ID", example = "1") @RequestParam(value = "blogId") @NotNull Long blogId, - @Parameter(description = "생성 날짜", example = "2024-09-16T03:18:13.734") @RequestParam("createDate") @NotNull LocalDateTime createDate - ) { - return BlogRecentPostsResponse.from(blogReadService.getBlogRecentPostsByIdAndCreateDate(blogId, createDate)); - } - - @Operation( - summary = "블로그 포스트 미리보기 조회", - description = "지정된 블로그 ID를 사용해 포스트의 미리보기 정보를 반환한다." - ) - @ExceptionCodeAnnotations({CommonExceptionCode.NOT_FOUND_BLOG}) - @GetMapping("/{nickname}/preview") - public PagedModel> getBlogPostPreviews( - @Parameter(description = "닉네임", example = "hyoseung") @PathVariable @NotBlank String nickname, - @RequestParam(name = "sortType", required = false, defaultValue = "NEWEST") SortType sortType, - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = "6") int size - ) { - Page blogPostPreviewResponsePage = postReadService.getPostsByNickname(nickname, page, - size, sortType) - .map(BlogPostPreviewResponse::from); - - return PagedModelUtil.create(blogPostPreviewResponsePage); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/request/BlogUpdateIntroRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/request/BlogUpdateIntroRequest.java deleted file mode 100644 index 2c6d901b..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/request/BlogUpdateIntroRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; - -@Schema( - description = "기술 정보 블로그 소개글 수정 요청" -) -public record BlogUpdateIntroRequest( - @Schema( - description = "수정할 블로그 소개글", - example = "최신 기술 트렌드와 실용적인 개발 팁을 공유하는 기술 블로그입니다." - ) - @NotNull - String intro -) { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogDetailsResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogDetailsResponse.java deleted file mode 100644 index 39519475..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogDetailsResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog.response; - -import darkoverload.itzip.feature.techinfo.domain.blog.BlogDetails; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; -import lombok.Builder; - -@Schema( - description = "기술 정보 블로그 상세 정보 응답" -) -@Builder -public record BlogDetailsResponse( - @Schema(description = "블로그 ID", example = "1") - Long blogId, - - @Schema( - description = "블로그 소유자 프로필 이미지 URL", - example = "https://dy1vg9emkijkn.cloudfront.net/profile/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg" - ) - String profileImageUrl, - - @Schema(description = "블로그 소유자 닉네임", example = "hyoseung") - String nickname, - - @Schema(description = "블로그 소유자 이메일 주소", example = "dev.hyoseung@gmail.com") - String email, - - @Schema(description = "블로그 소개글", example = "최신 기술 트렌드와 실용적인 개발 팁을 공유하는 기술 블로그입니다.") - String intro, - - @Schema(description = "연도별 포스트 통계") - List postCountByYear -) { - - public static BlogDetailsResponse from(BlogDetails blogDetails) { - return BlogDetailsResponse.builder() - .blogId(blogDetails.getBlogId()) - .profileImageUrl(blogDetails.getProfileImageUrl()) - .nickname(blogDetails.getNickname()) - .email(blogDetails.getEmail()) - .intro(blogDetails.getIntro()) - .postCountByYear(blogDetails.getYearlyPostCounts()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogPostPreviewResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogPostPreviewResponse.java deleted file mode 100644 index 6a7982fb..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogPostPreviewResponse.java +++ /dev/null @@ -1,57 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog.response; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - -@Schema( - description = "기술 정보 블로그 게시글 미리보기 응답" -) -@Builder -public record BlogPostPreviewResponse( - @Schema(description = "게시글 ID", example = "66e724e50000000000db4e53") - String postId, - - @Schema(description = "카테고리 ID", example = "66ce18d84cb7d0b29ce602f5") - String categoryId, - - @Schema(description = "제목", example = "밤하늘 아래, 감정의 여정") - String title, - - @Schema( - description = "내용 요약 (최대 300자)", - example = """ - 이 세 개의 이미지는 감정의 복잡한 여정을 시각적으로 표현하고 있다. - 첫 번째 장면에서는 소녀가 꿈을 향해 하늘로 비상하려 하지만, 현실의 무게에 의해 아래로 끌려 내려가는 모습을 담고 있다. - 이는 꿈과 야망을 추구하는 과정에서 마주하는 좌절과 도전을 상징한다. 두 번째 장면에서는 소녀가 밤하늘을 응시하며, 자신의 과거와 잊지 못한 꿈들을 회상하는 장면이 그려진다. - 창문 밖으로 보이는 거친 파도는 그녀의 내면에서 일어나는 감정의 소용돌이를 나타낸다. 마지막 장면은 비가 내리는 고요한 거리에서 두 사람이 나란히 걷는 모습을 통해, 서 - """ - ) - String content, - - @Schema(description = "좋아요 수", example = "0") - int likeCount, - - @Schema(description = "작성일", example = "2024-09-16T03:18:13.734") - String createDate, - - @Schema( - description = "썸네일 이미지 URL", - example = "https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg" - ) - String thumbnailImagePath -) { - - public static BlogPostPreviewResponse from(Post post) { - return BlogPostPreviewResponse.builder() - .postId(post.getId()) - .categoryId(post.getCategoryId()) - .title(post.getTitle()) - .content(post.getContent()) - .likeCount(post.getLikeCount()) - .createDate(post.getCreateDate().toString()) - .thumbnailImagePath(post.getThumbnailImagePath()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogRecentPostsResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogRecentPostsResponse.java deleted file mode 100644 index a704634c..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogRecentPostsResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog.response; - -import darkoverload.itzip.feature.techinfo.controller.post.response.PostResponse; -import darkoverload.itzip.feature.techinfo.domain.blog.BlogPostTimeline; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; -import lombok.Builder; - -@Schema( - description = "기술 정보 블로그의 최근 게시글 타임라인 응답" -) -@Builder -public record BlogRecentPostsResponse( - @Schema(description = "블로그 소유자 닉네임", example = "hyoseung") - String nickname, - - @Schema(description = "기준 포스트 주변의 최근 포스트 목록") - List posts -) { - - public static BlogRecentPostsResponse from(BlogPostTimeline blogPostTimeline) { - List postResponses = blogPostTimeline.getPosts().stream() - .map(PostResponse::from) - .toList(); - - return BlogRecentPostsResponse.builder() - .nickname(blogPostTimeline.getNickname()) - .posts(postResponses) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogResponse.java deleted file mode 100644 index 931aef4a..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/blog/response/BlogResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.blog.response; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - -@Schema( - description = "블로그 기본 정보 응답" -) -@Builder -public record BlogResponse( - @Schema( - description = "블로그 소유자 프로필 이미지 URL", - example = "https://dy1vg9emkijkn.cloudfront.net/profile/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg" - ) - String profileImagePath, - - @Schema(description = "블로그 소유자의 닉네임", example = "hyoseung") - String nickname, - - @Schema(description = "블로그 소유자 이메일", example = "dev.hyoseung@gmail.com") - String email, - - @Schema(description = "블로그 소개", example = "최신 기술 트렌드와 실용적인 개발 팁을 공유하는 기술 블로그입니다.") - String intro -) { - - public static BlogResponse from(Blog blog) { - return BlogResponse.builder() - .profileImagePath(blog.getUser().getImageUrl()) - .nickname(blog.getUser().getNickname()) - .email(blog.getUser().getEmail()) - .intro(blog.getIntro()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostCommentController.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostCommentController.java deleted file mode 100644 index 8f31e63d..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostCommentController.java +++ /dev/null @@ -1,105 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentCreateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentUpdateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.response.PostCommentResponse; -import darkoverload.itzip.feature.techinfo.service.comment.CommentCommandService; -import darkoverload.itzip.feature.techinfo.service.comment.CommentReadService; -import darkoverload.itzip.feature.techinfo.util.PagedModelUtil; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.PagedModel; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@Tag( - name = "Tech Info Post Comment", - description = "댓글 조회, 생성, 수정 및 삭제 기능을 제공하는 API" -) -@Validated -@RestController -@RequiredArgsConstructor -@RequestMapping("/tech-info/post") -public class PostCommentController { - - private final CommentReadService commentReadService; - private final CommentCommandService commentCommandService; - - @Operation( - summary = "게시글 댓글 목록 조회", - description = "특정 게시글에 대한 댓글 목록을 페이징 처리하여 반환합니다." - ) - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_USER, - CommonExceptionCode.NOT_FOUND_POST - }) - @GetMapping("/comments") - public PagedModel> getPostComments( - @Parameter(description = "게시글 ID", example = "66e724e50000000000db4e53") @RequestParam(name = "postId") @NotBlank String postId, - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = "10") int size - ) { - Page postCommentResponsePage = commentReadService.getCommentsByPostId(postId, page, size) - .map(PostCommentResponse::from); - - return PagedModelUtil.create(postCommentResponsePage); - } - - @Operation( - summary = "게시글에 댓글 작성", - description = "특정 게시글에 새 댓글을 작성합니다." - ) - @ExceptionCodeAnnotations({CommonExceptionCode.NOT_FOUND_USER}) - @PostMapping("/comment") - public String addComment( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody PostCommentCreateRequest request - ) { - commentCommandService.create(userDetails, request); - return "댓글이 성공적으로 작성되었습니다."; - } - - @Operation( - summary = "게시글에 댓글 수정", - description = "특정 댓글의 내용을 수정합니다." - ) - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_USER, - CommonExceptionCode.UPDATE_FAIL_COMMENT - }) - @PatchMapping("/comment") - public String editComment( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody PostCommentUpdateRequest request - ) { - commentCommandService.update(userDetails, request); - return "게시글이 성공적으로 수정되었습니다."; - } - - @Operation( - summary = "게시글에 댓글 임시 삭제 (비공개 처리)", - description = "게시글에 댓글을 비공개 상태로 설정합니다." - ) - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_USER, - CommonExceptionCode.UPDATE_FAIL_COMMENT - }) - @PatchMapping("/{commentId}/unpublish") - public String unpublishComment( - @AuthenticationPrincipal CustomUserDetails userDetails, - @Parameter(description = "댓글 ID", example = "66eaeacb48e1841cc9893a60") @PathVariable String commentId - ) { - commentCommandService.updateVisibility(userDetails, commentId, false); - return "댓글이 성공적으로 비공개 처리되었습니다."; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostController.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostController.java deleted file mode 100644 index 7e151557..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostController.java +++ /dev/null @@ -1,136 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCreateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostUpdateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.response.PostDetailsInfoResponse; -import darkoverload.itzip.feature.techinfo.controller.post.response.PostPreviewResponse; -import darkoverload.itzip.feature.techinfo.service.post.PostCommandService; -import darkoverload.itzip.feature.techinfo.service.post.PostReadService; -import darkoverload.itzip.feature.techinfo.type.SortType; -import darkoverload.itzip.feature.techinfo.util.PagedModelUtil; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.PagedModel; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@Tag( - name = "Tech Info Post", - description = "게시글 조회, 생성, 수정 및 삭제 기능을 제공하는 API" -) -@Validated -@RestController -@RequiredArgsConstructor -@RequestMapping("/tech-info") -public class PostController { - - private final PostReadService postReadService; - private final PostCommandService postCommandService; - - @Operation( - summary = "전체 기술 정보 게시글 수 조회", - description = "현재 시스템에 등록된 모든 기술 정보 포스트의 총 개수를 반환합니다." - ) - @GetMapping("/post/count") - public long getPostCount() { - return postReadService.getPostCount(); - } - - @Operation( - summary = "게시글 상세 조회", - description = "특정 게시글의 상세 정보를 조회합니다." - ) - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_USER, - CommonExceptionCode.NOT_FOUND_POST, - CommonExceptionCode.NOT_FOUND_BLOG - }) - @GetMapping("/post/{postId}") - public PostDetailsInfoResponse viewPostDetails( - @AuthenticationPrincipal CustomUserDetails userDetails, - @Parameter(description = "게시글 ID", example = "66e724e50000000000db4e53") - @PathVariable @NotBlank String postId - ) { - return PostDetailsInfoResponse.from(postReadService.getPostDetailsById(postId, userDetails)); - } - - @Operation( - summary = "게시글 미리보기 목록 조회", - description = "카테고리별 필터링 및 정렬된 게시글 미리보기 목록을 페이징하여 반환합니다." - ) - @GetMapping("/posts/preview") - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_POST_IN_CATEGORY, - CommonExceptionCode.NOT_FOUND_POST, - CommonExceptionCode.NOT_FOUND_BLOG - }) - public PagedModel> getPostPreviews( - @Parameter(description = "카테고리 ID (선택사항)", example = "66ce18d84cb7d0b29ce602f5") - @RequestParam(name = "categoryId", required = false) String categoryId, - @RequestParam(name = "sortType", required = false, defaultValue = "NEWEST") SortType sortType, - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = "12") int size - ) { - Page postPreviewResponses = postReadService.getAllOrPostsByCategoryId(categoryId, page, - size, sortType) - .map(PostPreviewResponse::from); - - return PagedModelUtil.create(postPreviewResponses); - } - - @Operation( - summary = "게시글 작성", - description = "새로운 기술 정보 게시글을 작성합니다." - ) - @ExceptionCodeAnnotations({ - CommonExceptionCode.NOT_FOUND_USER, - CommonExceptionCode.NOT_FOUND_BLOG - }) - @PostMapping("/post") - public String addPost( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody @Valid PostCreateRequest request - ) { - postCommandService.create(userDetails, request); - return "게시글이 성공적으로 작성되었습니다."; - } - - @Operation( - summary = "게시글 수정", - description = "주어진 요청을 기반으로 포스트를 수정합니다." - ) - @ExceptionCodeAnnotations({CommonExceptionCode.UPDATE_FAIL_POST}) - @PatchMapping("/post") - public String editPost(@RequestBody @Valid PostUpdateRequest request) { - postCommandService.update(request); - return "게시글이 성공적으로 수정되었습니다."; - } - - @Operation( - summary = "게시글 임시 삭제 (비공개 처리)", - description = "게시글을 비공개 상태로 설정합니다." - ) - @ExceptionCodeAnnotations({CommonExceptionCode.UPDATE_FAIL_POST}) - @PatchMapping("/post/{postId}/unpublish") - public String unpublishPost( - @Parameter( - description = "게시글 ID", - example = "66e724e50000000000db4e53" - ) - @PathVariable @NotBlank String postId - ) { - postCommandService.update(postId, false); - return "게시글이 성공적으로 비공개 처리되었습니다."; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostLikeController.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostLikeController.java deleted file mode 100644 index 04f9bb0f..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostLikeController.java +++ /dev/null @@ -1,45 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.service.like.LikeService; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag( - name = "Tech Info Post Like", - description = "기술 정보 게시글 좋아요 토글 기능을 제공하는 API" -) -@Validated -@RestController -@RequiredArgsConstructor -@RequestMapping("/tech-info/post") -public class PostLikeController { - - private final LikeService likeService; - - @Operation( - summary = "게시글 좋아요 토글", - description = "특정 기술 게시글에 대한 좋아요를 추가하거나 취소합니다." - ) - @ExceptionCodeAnnotations({CommonExceptionCode.NOT_FOUND_USER}) - @PostMapping("/{postId}/like") - public String togglePostLike( - @AuthenticationPrincipal CustomUserDetails userDetails, - @Parameter(description = "포스트 ID", example = "66e724e50000000000db4e53") - @PathVariable @NotBlank String postId - ) { - return likeService.toggleLike(userDetails, postId) ? "게시글에 좋아요를 추가했습니다." : "게시글의 좋아요를 취소했습니다."; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostScrapController.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostScrapController.java deleted file mode 100644 index 30f21992..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/PostScrapController.java +++ /dev/null @@ -1,46 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.service.scrap.ScrapService; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag( - name = "Tech Info Post Scrap", - description = "기술 정보 게시글 스크랩 토글 기능을 제공하는 API" -) -@Validated -@RestController -@RequiredArgsConstructor -@RequestMapping("/tech-info/post") -public class PostScrapController { - - private final ScrapService scrapService; - - @Operation( - summary = "게시글 스크랩 토글", - description = "특정 기술 정보 포스트에 대한 스크랩을 추가하거나 취소합니다." - ) - @ExceptionCodeAnnotations(CommonExceptionCode.NOT_FOUND_USER) - @PostMapping("/{postId}/scrap") - public String toggleScrapStatus( - @AuthenticationPrincipal CustomUserDetails userDetails, - @Parameter(description = "포스트 ID", example = "66e724e50000000000db4e53") - @PathVariable @NotBlank String postId - ) { - boolean isScrapped = scrapService.toggleScrap(userDetails, postId); - return isScrapped ? "게시글을 스크랩했습니다." : "게시글의 스크랩을 취소했습니다."; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCommentCreateRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCommentCreateRequest.java deleted file mode 100644 index cad8276f..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCommentCreateRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Builder; - -@Schema( - description = "기술 정보 게시글 댓글 작성 요청" -) -@Builder -public record PostCommentCreateRequest( - @Schema(description = "게시글 ID", example = "66e724e50000000000db4e53") - @NotBlank(message = "게시글 ID는 필수입니다.") - String postId, - - @Schema(description = "댓글 내용", example = "이 포스트 정말 유익하네요!") - @NotBlank(message = "댓글 내용은 필수입니다.") - String content -) { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCommentUpdateRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCommentUpdateRequest.java deleted file mode 100644 index 52c89e0f..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCommentUpdateRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema( - description = "기술 정보 게시글 댓글 수정 요청" -) -public record PostCommentUpdateRequest( - @Schema(description = "댓글 ID", example = "674843181801af00dd3cbfee", required = true) - @NotBlank(message = "댓글 ID는 필수입니다.") - String commentId, - - @Schema(description = "수정할 댓글 내용", example = "수정된 댓글 내용입니다.", required = true) - @NotBlank(message = "댓글 내용은 필수입니다.") - String content -) { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCreateRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCreateRequest.java deleted file mode 100644 index 797482da..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostCreateRequest.java +++ /dev/null @@ -1,47 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Builder; - -import java.util.List; - -@Schema( - description = "기술 정보 포스트 생성 요청" -) -@Builder -public record PostCreateRequest( - @Schema(description = "카테고리 ID", example = "66ce18d84cb7d0b29ce602f5") - @NotBlank(message = "카테고리 ID는 필수입니다.") - String categoryId, - - @Schema(description = "게시글 제목", example = "밤하늘 아래, 감정의 여정") - @NotBlank(message = "제목은 필수입니다.") - String title, - - @Schema( - description = "게시글 본문", - example = "세 개의 이미지는 감정의 여정을 표현한다. 첫 번째 장면에서는 소녀가 자유를 꿈꾸며 하늘로 날아오르지만 현실의 무게에 의해 추락하는 모습을 담고 있다. 이는 인간이 꿈을 꾸고 좌절을 마주하는 과정을 상징한다. 두 번째 장면에서는 소녀가 밤하늘을 바라보며 과거의 기억과 꿈을 되새긴다. 창문 너머로 보이는 파도는 그녀의 내면의 감정을 표현한다. 마지막 장면은 비 내리는 거리에서 두 사람이 함께 걸어가는 모습으로, 서로에게 의지하는 관계를 보여준다. 빗소리와 고요한 풍경은 그들의 감정을 더욱 부각시킨다. 이 이미지는 꿈, 좌절, 고독, 그리고 위안을 주제로 하여 인간이 감정을 마주하고 성장하는 과정을 담아냈다." - ) - @NotBlank(message = "본문은 필수입니다.") - String content, - - @Schema( - description = "썸네일 이미지 URL", - example = "https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg" - ) - @NotBlank(message = "썸네일 이미지 URL은 필수입니다.") - String thumbnailImagePath, - - @Schema( - description = "본문 이미지 URL 목록", - example = """ - [ - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - ] - """ - ) - List contentImagePaths -) { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostUpdateRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostUpdateRequest.java deleted file mode 100644 index cac4f2ff..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/request/PostUpdateRequest.java +++ /dev/null @@ -1,48 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import java.util.List; - -@Schema( - description = "기술 정보 게시글 수정 요청" -) -public record PostUpdateRequest( - @Schema(description = "게시글 ID", example = "66e724e50000000000db4e53") - @NotBlank(message = "포스트 ID는 필수입니다.") - String postId, - - @Schema(description = "", example = "66ce18d84cb7d0b29ce602f5") - @NotBlank(message = "카테고리 ID는 필수입니다.") - String categoryId, - - @Schema(description = "게시글 제목", example = "밤하늘 아래, 감정의 여정") - @NotBlank(message = "제목은 필수입니다.") - String title, - - @Schema( - description = "게시글 본문", - example = "이 세 개의 이미지는 감정의 복잡한 여정을 시각적으로 표현하고 있다. 첫 번째 장면에서는 소녀가 꿈을 향해 하늘로 비상하려 하지만, 현실의 무게에 의해 아래로 끌려 내려가는 모습을 담고 있다. 이는 꿈과 야망을 추구하는 과정에서 마주하는 좌절과 도전을 상징한다. 두 번째 장면에서는 소녀가 밤하늘을 응시하며, 자신의 과거와 잊지 못한 꿈들을 회상하는 장면이 그려진다. 창문 밖으로 보이는 거친 파도는 그녀의 내면에서 일어나는 감정의 소용돌이를 나타낸다. 마지막 장면은 비가 내리는 고요한 거리에서 두 사람이 나란히 걷는 모습을 통해, 서로에게 의지하며 고난을 함께 이겨내는 모습을 보여준다. 이 이미지들은 꿈, 좌절, 그리고 관계 속에서 위로를 찾는 여정을 이야기하며, 인간이 감정을 어떻게 극복하고 성장하는지에 대한 메시지를 전달한다." - ) - @NotBlank(message = "본문은 필수입니다.") - String content, - - @Schema( - description = "썸네일 이미지 URL", - example = "https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg" - ) - @NotBlank(message = "썸네일 이미지 URL은 필수입니다.") - String thumbnailImagePath, - - @Schema( - description = "본문 이미지 URL 목록", - example = """ - [ - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - ] - """ - ) - List contentImagePaths -) { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostCommentResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostCommentResponse.java deleted file mode 100644 index d0ec50fd..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostCommentResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.response; - -import darkoverload.itzip.feature.techinfo.domain.comment.CommentDetails; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - -@Schema( - description = "기술 정보 게시글 댓글 상세 응답" -) -@Builder -public record PostCommentResponse( - @Schema(description = "댓글 ID", example = "66eaeacb48e1841cc9893a60") - String commentId, - - @Schema(description = "작성자 프로필 이미지 URL", example = "https://dy1vg9emkijkn.cloudfront.net/profile/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg") - String profileImagePath, - - @Schema(description = "작성자 닉네임", example = "hyoseung") - String nickname, - - @Schema(description = "댓글 내용", example = "이 포스트 정말 유익하네요!") - String content, - - @Schema(description = "작성일", example = "2024-09-18T23:59:23.242") - String createDate -) { - - public static PostCommentResponse from(CommentDetails commentDetails) { - return PostCommentResponse.builder() - .commentId(commentDetails.getCommentId()) - .profileImagePath(commentDetails.getProfileImagePath()) - .nickname(commentDetails.getNickname()) - .content(commentDetails.getContent()) - .createDate(commentDetails.getCreateDate().toString()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostDetailsInfoResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostDetailsInfoResponse.java deleted file mode 100644 index fc93ac19..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostDetailsInfoResponse.java +++ /dev/null @@ -1,94 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.response; - -import darkoverload.itzip.feature.techinfo.domain.post.PostDetails; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - -import java.util.List; - -@Schema( - description = "기술 정보 게시글 상세 응답" -) -@Builder -public record PostDetailsInfoResponse( - @Schema(description = "작성자 프로필 이미지 URL", example = "https://dy1vg9emkijkn.cloudfront.net/profile/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg") - String profileImagePath, - - @Schema(description = "작성자 닉네임", example = "hyoseung") - String author, - - @Schema(description = "작성자 이메일", example = "dev.hyoseung@gmail.com") - String email, - - @Schema(description = "블로그 ID", example = "1") - Long blogId, - - @Schema(description = "게시글 ID", example = "66e9a2ed666b0728ada7edbf") - String postId, - - @Schema(description = "카테고리 ID", example = "66ce18d84cb7d0b29ce602f5") - String categoryId, - - @Schema(description = "작성일", example = "2024-09-16T03:18:13.734") - String createDate, - - @Schema(description = "제목", example = "밤하늘 아래, 감정의 여정") - String title, - - @Schema( - description = "본문", - example = "세 개의 이미지는 감정의 여정을 표현한다. 첫 번째 장면에서는 소녀가 자유를 꿈꾸며 하늘로 날아오르지만 현실의 무게에 의해 추락하는 모습을 담고 있다. 이는 인간이 꿈을 꾸고 좌절을 마주하는 과정을 상징한다. 두 번째 장면에서는 소녀가 밤하늘을 바라보며 과거의 기억과 꿈을 되새긴다. 창문 너머로 보이는 파도는 그녀의 내면의 감정을 표현한다. 마지막 장면은 비 내리는 거리에서 두 사람이 함께 걸어가는 모습으로, 서로에게 의지하는 관계를 보여준다. 빗소리와 고요한 풍경은 그들의 감정을 더욱 부각시킨다. 이 이미지는 꿈, 좌절, 고독, 그리고 위안을 주제로 하여 인간이 감정을 마주하고 성장하는 과정을 담아냈다." - ) - String content, - - @Schema(description = "좋아요 수", example = "0") - int likeCount, - - @Schema(description = "조회수", example = "4") - int viewCount, - - @Schema( - description = "썸네일 이미지 URL", - example = "https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg" - ) - String thumbnailImagePath, - - @Schema( - description = "본문 이미지 URL 목록", - example = """ - [ - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - ] - """ - ) - List contentImagePaths, - - @Schema(description = "좋아요 상태 여부", example = "false") - boolean isLiked, - - @Schema(description = "스크랩 상태 여부", example = "false") - boolean isScrapped -) { - - public static PostDetailsInfoResponse from(PostDetails postDetails) { - return PostDetailsInfoResponse.builder() - .profileImagePath(postDetails.getProfileImagePath()) - .author(postDetails.getAuthor()) - .email(postDetails.getEmail()) - .blogId(postDetails.getBlogId()) - .postId(postDetails.getPostId()) - .categoryId(postDetails.getCategoryId()) - .createDate(postDetails.getCreateDate().toString()) - .title(postDetails.getTitle()) - .content(postDetails.getContent()) - .likeCount(postDetails.getLikeCount()) - .viewCount(postDetails.getViewCount()) - .thumbnailImagePath(postDetails.getThumbnailImagePath()) - .contentImagePaths(postDetails.getContentImagePaths()) - .isLiked(postDetails.isLiked()) - .isScrapped(postDetails.isScrapped()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostPreviewResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostPreviewResponse.java deleted file mode 100644 index 22f0aaa1..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostPreviewResponse.java +++ /dev/null @@ -1,65 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.response; - -import darkoverload.itzip.feature.techinfo.domain.post.PostInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - -@Schema( - description = "기술 정보 포스트 미리보기 응답" -) -@Builder -public record PostPreviewResponse( - @Schema(description = "포스트 ID", example = "66e724e50000000000db4e53") - String postId, - - @Schema(description = "카테고리 ID", example = "66ce18d84cb7d0b29ce602f5") - String categoryId, - - @Schema(description = "제목", example = "밤하늘 아래, 감정의 여정") - String title, - - @Schema( - description = "내용 요약 (최대 300자)", - example = """ - 이 세 개의 이미지는 감정의 복잡한 여정을 시각적으로 표현하고 있다. - 첫 번째 장면에서는 소녀가 꿈을 향해 하늘로 비상하려 하지만, 현실의 무게에 의해 아래로 끌려 내려가는 모습을 담고 있다. - 이는 꿈과 야망을 추구하는 과정에서 마주하는 좌절과 도전을 상징한다. 두 번째 장면에서는 소녀가 밤하늘을 응시하며, 자신의 과거와 잊지 못한 꿈들을 회상하는 장면이 그려진다. - 창문 밖으로 보이는 거친 파도는 그녀의 내면에서 일어나는 감정의 소용돌이를 나타낸다. 마지막 장면은 비가 내리는 고요한 거리에서 두 사람이 나란히 걷는 모습을 통해, 서 - """ - ) - String content, - - @Schema( - description = "썸네일 이미지 URL", - example = "https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg" - ) - String thumbnailImagePath, - - @Schema(description = "좋아요 수", example = "0") - Integer likeCount, - - @Schema(description = "작성자 프로필 이미지 URL", example = "") - String profileImagePath, - - @Schema(description = "작성자 닉네임", example = "hyoseung") - String author, - - @Schema(description = "작성일", example = "2024-09-16T03:18:13.734") - String createDate -) { - - public static PostPreviewResponse from(PostInfo postInfo) { - return PostPreviewResponse.builder() - .postId(postInfo.getPostId()) - .categoryId(postInfo.getCategoryId()) - .title(postInfo.getTitle()) - .content(postInfo.getContent()) - .thumbnailImagePath(postInfo.getThumbnailImagePath()) - .likeCount(postInfo.getLikeCount()) - .profileImagePath(postInfo.getProfileImagePath()) - .author(postInfo.getAuthor()) - .createDate(postInfo.getCreateDate().toString()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostResponse.java deleted file mode 100644 index bdebf06f..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/controller/post/response/PostResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package darkoverload.itzip.feature.techinfo.controller.post.response; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - -@Schema( - description = "기술 정보 게시글 기본 응답" -) -@Builder -public record PostResponse( - @Schema(description = "포스트 ID", example = "66e9a2ed666b0728ada7edbf") - String postId, - - @Schema(description = "제목", example = "밤하늘 아래, 감정의 여정") - String title, - - @Schema(description = "작성일", example = "2024-09-18T00:40:29.282") - String createDate -) { - - public static PostResponse from(Post post) { - return PostResponse.builder() - .postId(post.getId()) - .title(post.getTitle()) - .createDate(post.getCreateDate().toString()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/Blog.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/Blog.java deleted file mode 100644 index 2df4c984..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/Blog.java +++ /dev/null @@ -1,41 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.blog; - -import darkoverload.itzip.feature.user.domain.User; -import lombok.Builder; -import lombok.Getter; - -/** - * 기술 정보 블로그를 나타내는 도메인 클래스. - * 블로그의 ID, 소유자, 소개글, 공개 여부 정보를 포함합니다. - */ -@Getter -public class Blog { - - private final Long id; - private final User user; - private final String intro; - private final boolean isPublic; - - @Builder - public Blog(Long id, User user, String intro, Boolean isPublic) { - this.id = id; - this.user = user; - this.intro = intro; - this.isPublic = isPublic; - } - - /** - * 주어진 사용자로 새 블로그를 생성합니다. - * 생성된 블로그는 기본적으로 공개 상태입니다. - * - * @param user 블로그 소유자 - * @return Blog - */ - public static Blog from(User user) { - return Blog.builder() - .user(user) - .isPublic(true) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogDetails.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogDetails.java deleted file mode 100644 index 4c09bbcd..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogDetails.java +++ /dev/null @@ -1,51 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.blog; - -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import lombok.Builder; -import lombok.Getter; -import java.util.List; - -/** - * 블로그의 상세 정보를 나타내는 도메인 클래스. - * 블로그 ID, 프로필 이미지 URL, 닉네임, 이메일, 소개글, 연도별 포스트 통계를 포함합니다. - */ -@Getter -public class BlogDetails { - - private final long blogId; - private final String profileImageUrl; - private final String nickname; - private final String email; - private final String intro; - private final List yearlyPostCounts; - - @Builder - public BlogDetails(long blogId, String profileImageUrl, String nickname, String email, String intro, - List yearlyPostCounts) { - this.blogId = blogId; - this.profileImageUrl = profileImageUrl; - this.nickname = nickname; - this.email = email; - this.intro = intro; - this.yearlyPostCounts = yearlyPostCounts; - } - - /** - * Blog 객체와 연도별 포스트 통계로부터 BlogDetails 생성합니다. - * - * @param blog 블로그 정보 - * @param yearlyPostCounts 연도별 포스트 통계 - * @return BlogDetails - */ - public static BlogDetails from(Blog blog, List yearlyPostCounts) { - return BlogDetails.builder() - .blogId(blog.getId()) - .profileImageUrl(blog.getUser().getImageUrl()) - .nickname(blog.getUser().getNickname()) - .email(blog.getUser().getEmail()) - .intro(blog.getIntro()) - .yearlyPostCounts(yearlyPostCounts) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogPostTimeline.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogPostTimeline.java deleted file mode 100644 index 47b70b39..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogPostTimeline.java +++ /dev/null @@ -1,38 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.blog; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import lombok.Builder; -import lombok.Getter; -import java.util.List; - -/** - * 블로그의 포스트 타임라인을 나타내는 도메인 클래스. - * 블로그 소유자의 닉네임과 포스트 목록을 포함합니다. - */ -@Getter -public class BlogPostTimeline { - - private final String nickname; - private final List posts; - - @Builder - public BlogPostTimeline(String nickname, List posts) { - this.nickname = nickname; - this.posts = posts; - } - - /** - * 주어진 닉네임과 포스트 목록으로 BlogPostTimeline 생성합니다. - * - * @param nickname 블로그 소유자의 닉네임 - * @param posts 포스트 목록 - * @return BlogPostTimeline - */ - public static BlogPostTimeline from(String nickname, List posts) { - return BlogPostTimeline.builder() - .nickname(nickname) - .posts(posts) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/comment/Comment.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/comment/Comment.java deleted file mode 100644 index 9db1b356..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/comment/Comment.java +++ /dev/null @@ -1,55 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.comment; - -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentCreateRequest; -import lombok.Builder; -import lombok.Getter; -import java.time.LocalDateTime; - -/** - * 기술 정보 게시글의 댓글을 나타내는 도메인 클래스. - * 댓글 ID, 게시글 ID, 작성자 ID, 내용, 공개 여부, 작성 일시를 포함합니다. - */ -@Getter -public class Comment { - - private final String id; - private final String postId; - private final Long userId; - private final String content; - private final Boolean isPublic; - private final LocalDateTime createDate; - - @Builder - public Comment( - String id, - String postId, - Long userId, - String content, - Boolean isPublic, - LocalDateTime createDate - ) { - this.id = id; - this.postId = postId; - this.userId = userId; - this.content = content; - this.isPublic = isPublic; - this.createDate = createDate; - } - - /** - * 댓글 생성 요청과 사용자 ID로 새 Comment 생성합니다. - * - * @param request 댓글 생성 요청 - * @param userId 댓글 작성자의 ID - * @return Comment - */ - public static Comment from(PostCommentCreateRequest request, Long userId) { - return Comment.builder() - .postId(request.postId()) - .userId(userId) - .content(request.content()) - .isPublic(true) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentDetails.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentDetails.java deleted file mode 100644 index 1914e605..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentDetails.java +++ /dev/null @@ -1,53 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.comment; - -import darkoverload.itzip.feature.user.domain.User; -import lombok.Builder; -import lombok.Getter; -import java.time.LocalDateTime; - -/** - * 댓글의 상세 정보를 나타내는 도메인 클래스. - * 댓글 ID, 작성자 프로필 이미지 URL, 닉네임, 내용, 작성 일시를 포함합니다. - */ -@Getter -public class CommentDetails { - - private final String commentId; - private final String profileImagePath; - private final String nickname; - private final String content; - private final LocalDateTime createDate; - - @Builder - public CommentDetails( - String commentId, - String profileImagePath, - String nickname, - String content, - LocalDateTime createDate - ) { - this.commentId = commentId; - this.profileImagePath = profileImagePath; - this.nickname = nickname; - this.content = content; - this.createDate = createDate; - } - - /** - * Comment 와 User 로부터 CommentDetails 생성합니다. - * - * @param comment 댓글 정보 - * @param user 댓글 작성자 정보 - * @return CommentDetails - */ - public static CommentDetails from(Comment comment, User user) { - return CommentDetails.builder() - .commentId(comment.getId()) - .profileImagePath(user.getImageUrl()) - .nickname(user.getNickname()) - .content(comment.getContent()) - .createDate(comment.getCreateDate()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Article.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Article.java new file mode 100644 index 00000000..0c89bb6c --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Article.java @@ -0,0 +1,103 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Getter +@EqualsAndHashCode +@Document(collection = "articles") +@CompoundIndex( + name = "idx_type_displayed_created_at", + def = "{'type': 1, 'is_displayed': 1, 'created_at': -1}" +) +public class Article { + + @Id + private ObjectId id; + + @Field("blog_id") + private Long blogId; + + @Field("type") + private ArticleType type; + + @Field("title") + private String title; + + @Field("content") + private String content; + + @Field("thumbnail_image_uri") + private String thumbnailImageUri; + + @Field("likes_count") + private Long likesCount; + + @Field("view_count") + private Long viewCount; + + @CreatedDate + @Field("created_at") + private LocalDateTime createdAt; + + @LastModifiedDate + @Field("updated_at") + private LocalDateTime updatedAt; + + @Field("is_displayed") + private Boolean displayed; + + protected Article() { + } + + public Article(final ObjectId id, final long blogId, final ArticleType type, final String title, final String content, final String thumbnailImageUri, final long likesCount, final long viewCount, final LocalDateTime createdAt, final LocalDateTime updatedAt, final boolean displayed) { + checkTitle(title); + this.id = id; + this.blogId = blogId; + this.type = type; + this.title = title; + this.content = content; + this.thumbnailImageUri = thumbnailImageUri; + this.likesCount = likesCount; + this.viewCount = viewCount; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.displayed = displayed; + } + + private void checkTitle(final String title) { + if (Objects.isNull(title) || title.isBlank()) { + throw new RestApiException(CommonExceptionCode.ARTICLE_TITLE_REQUIRED); + } + } + + public Article(final long blogId, final ArticleType type, final String title, final String content, final String thumbnailImageUri, boolean displayed) { + this(null, blogId, type, title, content, thumbnailImageUri, 0L, 0L, null, null, displayed); + } + + public static Article create(final long blogId, final String type, final String title, final String content, final String thumbnailImageUri) { + return new Article(blogId, ArticleType.from(type), title, content, thumbnailImageUri, true); + } + + public Article update(final String type, final String title, final String content, final String thumbnailImageUri) { + return new Article(this.id, this.blogId, ArticleType.from(type), title, content, thumbnailImageUri, this.likesCount, this.viewCount, this.createdAt, null, this.displayed); + } + + public Article hide() { + this.displayed = false; + return this; + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleType.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleType.java new file mode 100644 index 00000000..f3e58243 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleType.java @@ -0,0 +1,51 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.techinfo.application.generator.UpperCaseGenerator; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; + +/** + * 아티클 유형(enum)을 정의하는 클래스입니다. + * + *

아티클의 유형을 나타내며, 문자열 입력을 해당 열거형 값으로 변환할 수 있습니다.

+ */ +public enum ArticleType { + + SOFTWARE_DEVELOPMENT_PROGRAMMING_LANGUAGE, + SOFTWARE_DEVELOPMENT_WEB_DEVELOPMENT, + SOFTWARE_DEVELOPMENT_MOBILE_DEVELOPMENT, + SOFTWARE_DEVELOPMENT_GAME_DEVELOPMENT, + SYSTEM_INFRA_DEVOPS, + SYSTEM_INFRA_DATABASE, + SYSTEM_INFRA_CLOUD, + SYSTEM_INFRA_SECURITY_NETWORK, + TECH_AI, + TECH_DATA_SCIENCE, + TECH_BLOCKCHAIN, + TECH_VR_AR, + TECH_HARDWARE, + DESIGN_ART_UI_UX, + DESIGN_ART_GRAPHICS, + DESIGN_ART_MODELING_3D, + DESIGN_ART_SOUND, + BUSINESS_OFFICE, + BUSINESS_PLANNING_PM, + BUSINESS_AUTOMATION, + BUSINESS_MARKETING, + OTHER; + + public static ArticleType from(final String type) { + final String normalizedType = UpperCaseGenerator.generate(type); + validate(normalizedType); + return valueOf(normalizedType); + } + + public static void validate(final String type) { + try { + valueOf(type); + } catch (IllegalArgumentException e) { + throw new RestApiException(CommonExceptionCode.ARTICLE_TYPE_NOT_FOUND); + } + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Blog.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Blog.java new file mode 100644 index 00000000..546a1a97 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Blog.java @@ -0,0 +1,77 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Getter +@Entity +@EqualsAndHashCode +@Table(name = "blogs") +@EntityListeners(AuditingEntityListener.class) +public class Blog { + + private static final String DEFAULT_BLOG_INTRO = "당신만의 블로그 소개글을 작성해주세요."; + + @Id + private Long id; + + @MapsId + @OneToOne(optional = false) + @JoinColumn(name = "user_id", nullable = false, unique = true) + private UserEntity user; + + private String intro; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + protected Blog() { + } + + public Blog(final Long id, final UserEntity user, final String intro, final LocalDateTime createdAt, final LocalDateTime updatedAt) { + checkIntro(intro); + this.id = id; + this.user = user; + this.intro = intro; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + private void checkIntro(final String intro) { + if (Objects.isNull(intro) || intro.isBlank()) { + throw new RestApiException(CommonExceptionCode.BLOG_INTRO_REQUIRED); + } + } + + public Blog(final UserEntity user, final String intro) { + this(null, user, intro, null, null); + } + + public static Blog create(final UserEntity user) { + return new Blog(user, DEFAULT_BLOG_INTRO); + } + + public void updateIntro(final String intro) { + checkIntro(intro); + this.intro = intro; + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Comment.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Comment.java new file mode 100644 index 00000000..4059b42d --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Comment.java @@ -0,0 +1,87 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Getter +@Entity +@EqualsAndHashCode +@Table(name = "comments") +@EntityListeners(AuditingEntityListener.class) +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id", nullable = false) + private UserEntity user; + + @Column(name = "article_id", nullable = false) + private String articleId; + + private String content; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "is_displayed", nullable = false) + private Boolean displayed; + + protected Comment() { + } + + public Comment(final Long id, final UserEntity user, final String articleId, final String content, final LocalDateTime createdAt, final LocalDateTime updatedAt, final boolean displayed) { + checkContent(content); + this.id = id; + this.user = user; + this.articleId = articleId; + this.content = content; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.displayed = displayed; + } + + private void checkContent(final String content) { + if (Objects.isNull(content) || content.isBlank()) { + throw new RestApiException(CommonExceptionCode.COMMENT_CONTENT_REQUIRED); + } + } + + public Comment(final UserEntity user, final String articleId, final String content, final boolean displayed) { + this(null, user, articleId, content, null, null, displayed); + } + + public static Comment create(final UserEntity user, final String articleId, final String content) { + return new Comment(user, articleId, content, true); + } + + public void updateContent(final String content) { + checkContent(content); + this.content = content; + } + + public void hide() { + this.displayed = false; + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Like.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Like.java new file mode 100644 index 00000000..dbf22630 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Like.java @@ -0,0 +1,59 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Getter +@Entity +@Table( + name = "likes", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "article_id"}) + } +) +@EqualsAndHashCode +@EntityListeners(AuditingEntityListener.class) +public class Like { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id", nullable = false) + private UserEntity user; + + @Column(name = "article_id", nullable = false) + private String articleId; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + protected Like() { + } + + public Like(final Long id, final UserEntity user, final String articleId, final LocalDateTime createdAt) { + this.id = id; + this.user = user; + this.articleId = articleId; + this.createdAt = createdAt; + } + + public Like(final UserEntity user, final String articleId) { + this(null, user, articleId, null); + } + + public static Like create(final UserEntity user, final String articleId) { + return new Like(user, articleId); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Scrap.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Scrap.java new file mode 100644 index 00000000..2187c974 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/entity/Scrap.java @@ -0,0 +1,59 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Getter +@Entity +@Table( + name = "scraps", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "article_id"}) + } +) +@EqualsAndHashCode +@EntityListeners(AuditingEntityListener.class) +public class Scrap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id", nullable = false) + private UserEntity user; + + @Column(name = "article_id", nullable = false) + private String articleId; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + protected Scrap() { + } + + public Scrap(final Long id, final UserEntity user, final String articleId, final LocalDateTime createdAt) { + this.id = id; + this.user = user; + this.articleId = articleId; + this.createdAt = createdAt; + } + + public Scrap(final UserEntity user, final String articleId) { + this(null, user, articleId, null); + } + + public static Scrap create(final UserEntity user, final String articleId) { + return new Scrap(user, articleId); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/like/Like.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/like/Like.java deleted file mode 100644 index 6d597089..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/like/Like.java +++ /dev/null @@ -1,38 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.like; - -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import lombok.Builder; -import lombok.Getter; - -/** - * 기술 정보 게시글의 좋아요를 나타내는 도메인 클래스. - * 좋아요 ID, 포스트 ID, 사용자 ID를 포함합니다. - */ -@Getter -public class Like { - - private final String id; - private final String postId; - private final Long userId; - - @Builder - public Like(String id, String postId, Long userId) { - this.id = id; - this.postId = postId; - this.userId = userId; - } - - /** - * LikeStatus 로부터 Like 생성합니다. - * - * @param likeStatus 좋아요 상태 정보 - * @return Like - */ - public static Like from(LikeStatus likeStatus) { - return Like.builder() - .postId(likeStatus.getPostId()) - .userId(likeStatus.getUserId()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/Post.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/Post.java deleted file mode 100644 index ed51dc93..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/Post.java +++ /dev/null @@ -1,76 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.post; - -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCreateRequest; -import lombok.Builder; -import lombok.Getter; -import java.time.LocalDateTime; -import java.util.List; - -/** - * 기술 정보 포스트를 나타내는 도메인 클래스. - * 게시글 ID, 블로그 ID, 카테고리 ID, 제목, 내용, 조회수, 좋아요 수, 공개 여부, 생성일, 썸네일 이미지 URL 및 본문 이미지 URL 목록을 포함합니다. - */ -@Getter -public class Post { - - private final String id; - private final Long blogId; - private final String categoryId; - private final String title; - private final String content; - private final Integer viewCount; - private final Integer likeCount; - private final Boolean isPublic; - private final LocalDateTime createDate; - private final String thumbnailImagePath; - private final List contentImagePaths; - - @Builder - public Post( - String id, - Long blogId, - String categoryId, - String title, - String content, - Integer viewCount, - Integer likeCount, - Boolean isPublic, - LocalDateTime createDate, - String thumbnailImagePath, - List contentImagePaths - ) { - this.id = id; - this.blogId = blogId; - this.categoryId = categoryId; - this.title = title; - this.content = content; - this.viewCount = viewCount; - this.likeCount = likeCount; - this.isPublic = isPublic; - this.createDate = createDate; - this.thumbnailImagePath = thumbnailImagePath; - this.contentImagePaths = contentImagePaths; - } - - /** - * PostCreateRequest 와 블로그 ID로부터 Post 생성합니다. - * - * @param request 게시글 생성 요청 - * @param blogId 게시글잎속한 블로그 ID - * @return Post - */ - public static Post from(PostCreateRequest request, Long blogId) { - return Post.builder() - .blogId(blogId) - .categoryId(request.categoryId()) - .title(request.title()) - .content(request.content()) - .viewCount(0) - .likeCount(0) - .isPublic(true) - .thumbnailImagePath(request.thumbnailImagePath()) - .contentImagePaths(request.contentImagePaths()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/PostDetails.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/PostDetails.java deleted file mode 100644 index 5085ea66..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/PostDetails.java +++ /dev/null @@ -1,98 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.post; - -import darkoverload.itzip.feature.user.domain.User; -import lombok.Builder; -import lombok.Getter; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * 기술 정보 게시글의 상세 정보를 나타내는 도메인 클래스. - * 작성자 정보, 게시글 정보, 좋아요 및 스크랩 상태 등을 포함합니다. - */ -@Getter -public class PostDetails { - - private final String profileImagePath; - private final String author; - private final String email; - private final Long blogId; - private final String postId; - private final String categoryId; - private final LocalDateTime createDate; - private final String title; - private final String content; - private final int viewCount; - private final int likeCount; - private final String thumbnailImagePath; - private final List contentImagePaths; - private final boolean isLiked; - private final boolean isScrapped; - - @Builder - public PostDetails( - String profileImagePath, - String author, - String email, - Long blogId, - String postId, - String categoryId, - LocalDateTime createDate, - String title, - String content, - int likeCount, - int viewCount, - String thumbnailImagePath, - List contentImagePaths, - boolean isLiked, - boolean isScrapped - ) { - this.profileImagePath = profileImagePath; - this.author = author; - this.email = email; - this.blogId = blogId; - this.postId = postId; - this.categoryId = categoryId; - this.createDate = createDate; - this.title = title; - this.content = content; - this.likeCount = likeCount; - this.viewCount = viewCount; - this.thumbnailImagePath = thumbnailImagePath; - this.contentImagePaths = contentImagePaths; - this.isLiked = isLiked; - this.isScrapped = isScrapped; - - } - - /** - * Post, User 와 좋아요, 스크랩 상태로부터 PostDetails 생성합니다. - * - * @param post 게시글 정보 - * @param user 작성자 정보 - * @param liked 좋아요 상태 - * @param scrapped 스크랩 상태 - * @return PostDetails - */ - public static PostDetails from(Post post, User user, boolean liked, boolean scrapped) { - return PostDetails.builder() - .profileImagePath(user.getImageUrl()) - .author(user.getNickname()) - .email(user.getEmail()) - .blogId(post.getBlogId()) - .postId(post.getId()) - .categoryId(post.getCategoryId()) - .createDate(post.getCreateDate()) - .title(post.getTitle()) - .content(post.getContent()) - .viewCount(post.getViewCount()) - .likeCount(post.getLikeCount()) - .thumbnailImagePath(post.getThumbnailImagePath()) - .contentImagePaths(post.getContentImagePaths()) - .isLiked(liked) - .isScrapped(scrapped) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/PostInfo.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/PostInfo.java deleted file mode 100644 index 3d319ae6..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/post/PostInfo.java +++ /dev/null @@ -1,67 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.post; - -import darkoverload.itzip.feature.user.domain.User; -import lombok.Builder; -import lombok.Getter; - -import java.time.LocalDateTime; - -@Getter -public class PostInfo { - - private final String profileImagePath; - private final String author; - private final String postId; - private final String categoryId; - private final LocalDateTime createDate; - private final String title; - private final String content; - private final String thumbnailImagePath; - private final int likeCount; - - @Builder - public PostInfo ( - String profileImagePath, - String author, - String postId, - String categoryId, - LocalDateTime createDate, - String title, - String content, - String thumbnailImagePath, - int likeCount - ) { - this.profileImagePath = profileImagePath; - this.author = author; - this.postId = postId; - this.categoryId = categoryId; - this.createDate = createDate; - this.title = title; - this.content = content; - this.thumbnailImagePath = thumbnailImagePath; - this.likeCount = likeCount; - } - - /** - * Post 와 User 로부터 기본 PostInfo 생성합니다. - * 좋아요와 스크랩 상태는 포함되지 않습니다. - * - * @param post 게시글 정보 - * @param user 작성자 정보 - * @return PostInfo - */ - public static PostInfo from(Post post, User user) { - return PostInfo.builder() - .profileImagePath(user.getImageUrl()) - .author(user.getNickname()) - .postId(post.getId()) - .categoryId(post.getCategoryId()) - .createDate(post.getCreateDate()) - .title(post.getTitle()) - .content(post.getContent()) - .thumbnailImagePath(post.getThumbnailImagePath()) - .likeCount(post.getLikeCount()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/projection/ArticlePreview.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/projection/ArticlePreview.java new file mode 100644 index 00000000..c416845e --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/projection/ArticlePreview.java @@ -0,0 +1,31 @@ +package darkoverload.itzip.feature.techinfo.domain.projection; + +import darkoverload.itzip.feature.techinfo.domain.entity.ArticleType; +import org.bson.types.ObjectId; + +import java.time.LocalDateTime; + +/** + * 아티클이 미리보기 정보를 제공하는 프로젝션 인터페이스입니다. + * + *

이 인터페이스는 아티클의 상세보기 전 미리보기 용도로 필요한 속성들을 제공합니다.

+ */ +public interface ArticlePreview { + + ObjectId getId(); + + long getBlogId(); + + ArticleType getType(); + + String getTitle(); + + String getContent(); + + String getThumbnailImageUri(); + + long getLikesCount(); + + LocalDateTime getCreatedAt(); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/projection/ArticleSummary.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/projection/ArticleSummary.java new file mode 100644 index 00000000..4369e79e --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/projection/ArticleSummary.java @@ -0,0 +1,20 @@ +package darkoverload.itzip.feature.techinfo.domain.projection; + +import org.bson.types.ObjectId; + +import java.time.LocalDateTime; + +/** + * 아티클 요약 정보를 제공하는 프로젝션 인터페이스입니다. + * + *

이 인터페이스는 아티클 목록 등에서 간략한 정보만 필요한 경우 사용됩니다.

+ */ +public interface ArticleSummary { + + ObjectId getId(); + + String getTitle(); + + LocalDateTime getCreatedAt(); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/ArticleRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/ArticleRepository.java new file mode 100644 index 00000000..2943b99c --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/ArticleRepository.java @@ -0,0 +1,53 @@ +package darkoverload.itzip.feature.techinfo.domain.repository; + +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.projection.ArticlePreview; +import darkoverload.itzip.feature.techinfo.domain.entity.ArticleType; +import darkoverload.itzip.feature.techinfo.domain.projection.ArticleSummary; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl.YearlyArticleStatistics; +import org.bson.types.ObjectId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.Query; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface ArticleRepository { + + Article save(Article article); + + Optional
findById(ObjectId id); + + Optional
findByIdAndBlogId(ObjectId id, Long blogId); + + Page findAllByDisplayedIsTrue(Pageable pageable); + + Page findAllByBlogIdAndDisplayedIsTrue(Long blogId, Pageable pageable); + + Page findAllByTypeAndDisplayedIsTrue(ArticleType type, Pageable pageable); + + @Query( + value = "{ 'blog_id': ?0, 'type': ?1, 'displayed': true, 'created_at': { $lt: ?2 } }", + sort = "{ 'created_at': -1 }" + ) + List findPreviousArticlesByBlogIdAndDisplayedIsTrue(Long blogId, ArticleType type, LocalDateTime createdAt, Pageable pageable); + + @Query( + value = "{ 'blog_id': ?0, 'type': ?1, 'displayed': true, 'created_at': { $gt: ?2 } }", + sort = "{ 'created_at': 1 }" + ) + List findNextArticlesByBlogIdAndDisplayedIsTrue(Long blogId, ArticleType type, LocalDateTime createdAt, Pageable pageable); + + boolean existsById(ObjectId id); + + List findArticleYearlyStatisticsByBlogId(Long blogId); + + void updateViewCount(ObjectId id, long count); + + void updateLikesCount(ObjectId id, long count); + + void deleteById(ObjectId id); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/BlogRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/BlogRepository.java new file mode 100644 index 00000000..d559ac7f --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/BlogRepository.java @@ -0,0 +1,30 @@ +package darkoverload.itzip.feature.techinfo.domain.repository; + +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface BlogRepository { + + Blog save(Blog blog); + + Optional findById(Long id); + + Optional findByUserId(Long userId); + + Optional findBlogByUser_Nickname(String nickname); + + @Query("SELECT b.id FROM Blog b WHERE b.user.nickname = :nickname") + Optional findBlogIdByUserNickname(@Param("nickname") String nickname); + + List findAllByIdIn(Set ids); + + void deleteById(Long id); + + void deleteAll(); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/CommentRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/CommentRepository.java new file mode 100644 index 00000000..d4d579d5 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/CommentRepository.java @@ -0,0 +1,30 @@ +package darkoverload.itzip.feature.techinfo.domain.repository; + +import darkoverload.itzip.feature.techinfo.domain.entity.Comment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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 CommentRepository { + + Comment save(Comment comment); + + Optional findById(Long id); + + Optional findByUserId(Long userId); + + Optional findByIdAndUser_Nickname(Long id, String nickname); + + Page findAllByArticleId(String articleId, Pageable pageable); + + @Modifying + @Query("UPDATE Comment c SET c.displayed = false WHERE c.articleId = :articleId") + void setDisplayedFalseByArticleId(@Param("articleId") String articleId); + + void deleteById(Long id); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/LikeRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/LikeRepository.java new file mode 100644 index 00000000..61108ebe --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/LikeRepository.java @@ -0,0 +1,19 @@ +package darkoverload.itzip.feature.techinfo.domain.repository; + +import darkoverload.itzip.feature.techinfo.domain.entity.Like; + +import java.util.Optional; + +public interface LikeRepository { + + Like save(Like like); + + Optional findByUserId(Long userId); + + boolean existsByUser_NicknameAndArticleId(String nickname, String articleId); + + void deleteById(Long id); + + void deleteByUser_NicknameAndArticleId(String nickname, String articleId); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/ScrapRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/ScrapRepository.java new file mode 100644 index 00000000..f7fd7bf5 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/domain/repository/ScrapRepository.java @@ -0,0 +1,19 @@ +package darkoverload.itzip.feature.techinfo.domain.repository; + +import darkoverload.itzip.feature.techinfo.domain.entity.Scrap; + +import java.util.Optional; + +public interface ScrapRepository { + + Scrap save(Scrap like); + + Optional findByUserId(Long userId); + + boolean existsByUser_NicknameAndArticleId(String nickname, String articleId); + + void deleteById(Long id); + + void deleteByUser_NicknameAndArticleId(String nickname, String articleId); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/domain/scrap/Scrap.java b/src/main/java/darkoverload/itzip/feature/techinfo/domain/scrap/Scrap.java deleted file mode 100644 index d6d97215..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/domain/scrap/Scrap.java +++ /dev/null @@ -1,38 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.scrap; - -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; -import lombok.Builder; -import lombok.Getter; - -/** - * 기술 정보 게시글의 스크랩을 나타내는 도메인 클래스. - * 스크랩 ID, 게시글 ID, 사용자 ID 를 포함합니다. - */ -@Getter -public class Scrap { - - private final String id; - private final String postId; - private final Long userId; - - @Builder - public Scrap(String id, String postId, Long userId) { - this.id = id; - this.postId = postId; - this.userId = userId; - } - - /** - * ScrapStatus 로부터 Scrap 생성합니다. - * - * @param scrapStatus 스크랩 상태 정보 - * @return Scrap - */ - public static Scrap from(ScrapStatus scrapStatus) { - return Scrap.builder() - .postId(scrapStatus.getPostId()) - .userId(scrapStatus.getUserId()) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/dto/like/LikeStatus.java b/src/main/java/darkoverload/itzip/feature/techinfo/dto/like/LikeStatus.java deleted file mode 100644 index 238e8e6e..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/dto/like/LikeStatus.java +++ /dev/null @@ -1,40 +0,0 @@ -package darkoverload.itzip.feature.techinfo.dto.like; - -import lombok.Builder; -import lombok.Getter; - -/** - * 기술 정보 게시글의 좋아요 상태를 나타내는 DTO 클래스. - * 좋아요 ID, 게시글 ID, 사용자 ID, 좋아요 여부를 포함합니다. - */ -@Getter -public class LikeStatus { - - private final String postId; - private final Long userId; - private final Boolean isLiked; - - @Builder - public LikeStatus(String postId, Long userId, Boolean isLiked) { - this.postId = postId; - this.userId = userId; - this.isLiked = isLiked; - } - - /** - * 게시글 ID, 사용자 ID, 좋아요 여부로부터 LikeStatus 생성합니다. - * - * @param postId 게시글 ID - * @param userId 사용자 ID - * @param isLiked 좋아요 ID - * @return LikeStatus - */ - public static LikeStatus from(String postId, Long userId, Boolean isLiked) { - return LikeStatus.builder() - .postId(postId) - .userId(userId) - .isLiked(isLiked) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/MonthlyPostStats.java b/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/MonthlyPostStats.java deleted file mode 100644 index 392b6660..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/MonthlyPostStats.java +++ /dev/null @@ -1,28 +0,0 @@ -package darkoverload.itzip.feature.techinfo.dto.post; - -import lombok.Getter; - -import java.util.List; - -/** - * 월별 게시글 통계를 나타내는 DTO 클래스. - * 월과 해당 월의 주별 게시글 통계 목록을 포함합니다. - */ -@Getter -public class MonthlyPostStats { - - private final int month; - private final List weeks; - - /** - * MonthlyPostStats 생성합니다. - * - * @param month 월 (1-12) - * @param weeks 해당 월의 주별 게시글 통계 목록 - */ - public MonthlyPostStats(int month, List weeks) { - this.month = month; - this.weeks = weeks; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/WeeklyPostStats.java b/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/WeeklyPostStats.java deleted file mode 100644 index 35aadd70..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/WeeklyPostStats.java +++ /dev/null @@ -1,26 +0,0 @@ -package darkoverload.itzip.feature.techinfo.dto.post; - -import lombok.Getter; - -/** - * 주별 게시글 통계를 나타내는 DTO 클래스. - * 주차와 해당 주의 게시글 개수를 포함합니다. - */ -@Getter -public class WeeklyPostStats { - - private int week; - private int postCount; - - /** - * WeeklyPostStats 생성합니다. - * - * @param week 주차 (1-5) - * @param postCount 해당 주의 게시글 개수 - */ - public WeeklyPostStats(int week, int postCount) { - this.week = week; - this.postCount = postCount; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/YearlyPostStats.java b/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/YearlyPostStats.java deleted file mode 100644 index e85594a8..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/dto/post/YearlyPostStats.java +++ /dev/null @@ -1,28 +0,0 @@ -package darkoverload.itzip.feature.techinfo.dto.post; - -import lombok.Getter; - -import java.util.List; - -/** - * 연간 게시글 통계를 나타내는 DTO 클래스. - * 년도와 해당 년도의 월별 게시글 통계 목록을 포함합니다. - */ -@Getter -public class YearlyPostStats { - - private final int year; - private final List months; - - /** - * YearlyPostStats 생성합니다. - * - * @param year 년도 - * @param months 해당 년도의 월별 게시글 통계 목록 - */ - public YearlyPostStats(int year, List months) { - this.year = year; - this.months = months; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/dto/scrap/ScrapStatus.java b/src/main/java/darkoverload/itzip/feature/techinfo/dto/scrap/ScrapStatus.java deleted file mode 100644 index 393b4968..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/dto/scrap/ScrapStatus.java +++ /dev/null @@ -1,28 +0,0 @@ -package darkoverload.itzip.feature.techinfo.dto.scrap; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class ScrapStatus { - - private final String postId; - private final Long userId; - private final Boolean isScrapped; - - @Builder - public ScrapStatus(String postId, Long userId, Boolean isScrapped) { - this.postId = postId; - this.userId = userId; - this.isScrapped = isScrapped; - } - - public static ScrapStatus from(String postId, Long userId, Boolean isScrapped) { - return ScrapStatus.builder() - .postId(postId) - .userId(userId) - .isScrapped(isScrapped) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/ArticleCrudRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/ArticleCrudRepository.java new file mode 100644 index 00000000..db139b46 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/ArticleCrudRepository.java @@ -0,0 +1,12 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence; + +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.ArticleCustomRepository; +import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; +import org.bson.types.ObjectId; +import org.springframework.data.repository.CrudRepository; + +@ExcludeFromJpaRepositories +public interface ArticleCrudRepository extends CrudRepository, ArticleRepository, ArticleCustomRepository { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/BlogJpaRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/BlogJpaRepository.java new file mode 100644 index 00000000..6d3f5b8d --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/BlogJpaRepository.java @@ -0,0 +1,8 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence; + +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import darkoverload.itzip.feature.techinfo.domain.repository.BlogRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BlogJpaRepository extends JpaRepository, BlogRepository { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/CommentJpaRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/CommentJpaRepository.java new file mode 100644 index 00000000..f301de60 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/CommentJpaRepository.java @@ -0,0 +1,8 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence; + +import darkoverload.itzip.feature.techinfo.domain.entity.Comment; +import darkoverload.itzip.feature.techinfo.domain.repository.CommentRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentJpaRepository extends JpaRepository, CommentRepository { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/LikeJpaRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/LikeJpaRepository.java new file mode 100644 index 00000000..c446bca6 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/LikeJpaRepository.java @@ -0,0 +1,8 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence; + +import darkoverload.itzip.feature.techinfo.domain.repository.LikeRepository; +import darkoverload.itzip.feature.techinfo.domain.entity.Like; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeJpaRepository extends JpaRepository, LikeRepository { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/ScrapJpaRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/ScrapJpaRepository.java new file mode 100644 index 00000000..a3c7d3c1 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/ScrapJpaRepository.java @@ -0,0 +1,8 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence; + +import darkoverload.itzip.feature.techinfo.domain.entity.Scrap; +import darkoverload.itzip.feature.techinfo.domain.repository.ScrapRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ScrapJpaRepository extends JpaRepository, ScrapRepository { +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/LikeCacheRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/LikeCacheRepository.java new file mode 100644 index 00000000..84f2f429 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/LikeCacheRepository.java @@ -0,0 +1,13 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence.cache; + +import java.util.Map; + +public interface LikeCacheRepository { + + void merge(String articleId, long value); + + Map retrieveAll(); + + void clear(); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/LikeInMemoryRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/LikeInMemoryRepositoryImpl.java new file mode 100644 index 00000000..23e70dd6 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/LikeInMemoryRepositoryImpl.java @@ -0,0 +1,30 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence.cache; + +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Repository +public class LikeInMemoryRepositoryImpl implements LikeCacheRepository { + + private final ConcurrentMap caches = new ConcurrentHashMap<>(); + + @Override + public void merge(final String articleId, final long value) { + caches.merge(articleId, value, Long::sum); + } + + @Override + public Map retrieveAll() { + return new HashMap<>(caches); + } + + @Override + public void clear() { + caches.clear(); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/ViewCacheRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/ViewCacheRepository.java new file mode 100644 index 00000000..5c2cd8ae --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/ViewCacheRepository.java @@ -0,0 +1,15 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence.cache; + +import org.bson.types.ObjectId; + +import java.util.Map; + +public interface ViewCacheRepository { + + void merge(ObjectId articleId, long value); + + Map retrieveAll(); + + void clear(); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/ViewInMemoryRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/ViewInMemoryRepositoryImpl.java new file mode 100644 index 00000000..3e4dd7a2 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/cache/ViewInMemoryRepositoryImpl.java @@ -0,0 +1,31 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence.cache; + +import org.bson.types.ObjectId; +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Repository +public class ViewInMemoryRepositoryImpl implements ViewCacheRepository { + + private final ConcurrentMap caches = new ConcurrentHashMap<>(); + + @Override + public void merge(final ObjectId articleId, final long value) { + caches.merge(articleId, value, Long::sum); + } + + @Override + public Map retrieveAll() { + return new HashMap<>(caches); + } + + @Override + public void clear() { + caches.clear(); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/ArticleCustomRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/ArticleCustomRepository.java new file mode 100644 index 00000000..1b4bf0ea --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/ArticleCustomRepository.java @@ -0,0 +1,16 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom; + +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl.YearlyArticleStatistics; +import org.bson.types.ObjectId; + +import java.util.List; + +public interface ArticleCustomRepository { + + List findArticleYearlyStatisticsByBlogId(Long blogId); + + void updateViewCount(ObjectId id, long count); + + void updateLikesCount(ObjectId id, long count); + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/impl/ArticleCustomRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/impl/ArticleCustomRepositoryImpl.java new file mode 100644 index 00000000..51ef189a --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/impl/ArticleCustomRepositoryImpl.java @@ -0,0 +1,128 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl; + +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.ArticleCustomRepository; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.*; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +/** + * MongoDB를 이용하여 커스텀 쿼리 및 집계 기능을 제공합니다. + * + *

이 클래스는 주로 아티클의 통계 정보 집계와 조회/좋아요 수 업데이트를 위한 기능을 포함합니다.

+ */ +public class ArticleCustomRepositoryImpl implements ArticleCustomRepository { + + private static final String FIELD_ID = "_id"; + private static final String FIELD_LIKES_COUNT = "likes_count"; + private static final String FIELD_VIEW_COUNT = "view_count"; + private static final String FIELD_CREATED_AT = "created_at"; + private static final String ARTICLE_COLLECTION = "articles"; + + private final MongoTemplate template; + + public ArticleCustomRepositoryImpl(final MongoTemplate template) { + this.template = template; + } + + @Override + public List findArticleYearlyStatisticsByBlogId(Long blogId) { + final LocalDateTime oneYearAgo = LocalDateTime.now().minusYears(1); + + final MatchOperation matchStage = Aggregation.match( + Criteria.where("blog_id").is(blogId) + .and(FIELD_CREATED_AT).gte(oneYearAgo) + .and("is_displayed").is(true) + ); + + final ProjectionOperation projectionStage = Aggregation.project() + .andExpression("year(created_at)").as("year") + .andExpression("month(created_at)").as("month") + .andExpression("1 + floor((dayOfMonth(created_at) - 1) / 7)").as("week"); + + final GroupOperation groupStage = Aggregation.group("year", "month", "week") + .count().as("article_count"); + + final GroupOperation monthGroupStage = Aggregation.group("_id.year", "_id.month") + .push(new Document("week", "$_id.week").append("articleCount", "$article_count")) + .as("weeks"); + + final GroupOperation yearGroupStage = Aggregation.group("year") + .push(new Document("month", "$_id.month").append("weeks", "$weeks")) + .as("months"); + + final AggregationOperation sortWeeks = context -> new Document("$set", + new Document("months", + new Document("$map", + new Document("input", "$months") + .append("as", "monthDoc") + .append("in", new Document("$mergeObjects", Arrays.asList( + // 원본 monthDoc + "$$monthDoc", + // 새로운 weeks 필드(주차 내림차순) + new Document("weeks", + new Document("$sortArray", + new Document("input", "$$monthDoc.weeks") + .append("sortBy", new Document("week", -1)) + ) + ) + ))) + ) + ) + ); + + final AggregationOperation sortMonths = context -> new Document("$set", + new Document("months", + new Document("$sortArray", + new Document("input", "$months") + .append("sortBy", new Document("month", -1)) + ) + ) + ); + + final ProjectionOperation finalProjectionStage = Aggregation.project() + .and(FIELD_ID).as("year") + .and("months").as("months"); + + final SortOperation sortYear = Aggregation.sort(Sort.by(Sort.Direction.DESC, "year")); + + final Aggregation aggregation = Aggregation.newAggregation( + matchStage, + projectionStage, + groupStage, + monthGroupStage, + yearGroupStage, + sortWeeks, + sortMonths, + finalProjectionStage, + sortYear + ); + + final AggregationResults results = template.aggregate(aggregation, ARTICLE_COLLECTION, YearlyArticleStatistics.class); + return results.getMappedResults(); + } + + @Override + public void updateViewCount(ObjectId id, long count) { + final Query query = new Query(Criteria.where(FIELD_ID).is(id)); + final Update update = new Update().inc(FIELD_VIEW_COUNT, count); + template.updateFirst(query, update, Article.class); + } + + @Override + public void updateLikesCount(ObjectId id, long count) { + final Query query = new Query(Criteria.where(FIELD_ID).is(id)); + final Update update = new Update().inc(FIELD_LIKES_COUNT, count); + template.updateFirst(query, update, Article.class); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/impl/YearlyArticleStatistics.java b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/impl/YearlyArticleStatistics.java new file mode 100644 index 00000000..66c253e2 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/infrastructure/persistence/custom/impl/YearlyArticleStatistics.java @@ -0,0 +1,29 @@ +package darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "연도별 아티클 통계") +public record YearlyArticleStatistics( + @Schema(description = "연도", example = "2025") + int year, + @Schema(description = "해당 연도의 월별 통계", required = true) + List months +) { } + +@Schema(description = "월별 아티클 통계") +record MonthlyArticleStatistics( + @Schema(description = "월", example = "3") + int month, + @Schema(description = "해당 월의 주별 통계", required = true) + List weeks +) { } + +@Schema(description = "주별 아티클 통계") +record WeeklyArticleStatistics( + @Schema(description = "주", example = "2") + int week, + @Schema(description = "해당 주의 아티클 수", example = "5") + long articleCount +) { } diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/CommentDocument.java b/src/main/java/darkoverload/itzip/feature/techinfo/model/document/CommentDocument.java deleted file mode 100644 index 3f73e1eb..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/CommentDocument.java +++ /dev/null @@ -1,70 +0,0 @@ -package darkoverload.itzip.feature.techinfo.model.document; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.global.entity.MongoAuditingFields; -import lombok.*; -import org.bson.types.ObjectId; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.data.mongodb.core.mapping.Field; - -/** - * MongoDb에 저장되는 댓글 문서를 나타내는 클래스. - * MongoAuditingFields 를 상속받아 생성 및 수정 일자를 자동으로 관리합니다. - */ -@Getter -@Builder -@AllArgsConstructor -@Document(collection = "comments") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CommentDocument extends MongoAuditingFields { - - @Id - @Field("_id") - private ObjectId id; - - @Field(name = "post_id") - private ObjectId postId; - - @Field(name = "user_id") - private Long userId; - - @Field(name = "content") - private String content; - - @Field(name = "is_public") - private Boolean isPublic; - - /** - * Comment 로부터 CommentDocument 생성합니다. - * - * @param comment - * @return - */ - public static CommentDocument from(Comment comment) { - return CommentDocument.builder() - .id(comment.getId() != null ? new ObjectId(comment.getId()) : null) - .postId(new ObjectId(comment.getPostId())) - .userId(comment.getUserId()) - .content(comment.getContent()) - .isPublic(comment.getIsPublic()) - .build(); - } - - /** - * CommentDocument 를 Comment 로 변환합니다. - * - * @return Comment - */ - public Comment toModel() { - return Comment.builder() - .id(this.id.toHexString()) - .postId(this.postId != null ? this.postId.toHexString() : null) - .userId(this.userId) - .content(this.content) - .isPublic(this.isPublic != null ? this.isPublic : null) - .createDate(this.createDate) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/LikeDocument.java b/src/main/java/darkoverload/itzip/feature/techinfo/model/document/LikeDocument.java deleted file mode 100644 index 7621a4c3..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/LikeDocument.java +++ /dev/null @@ -1,58 +0,0 @@ -package darkoverload.itzip.feature.techinfo.model.document; - -import darkoverload.itzip.feature.techinfo.domain.like.Like; -import darkoverload.itzip.global.entity.MongoAuditingFields; -import lombok.*; -import org.bson.types.ObjectId; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.data.mongodb.core.mapping.Field; - -/** - * MongoDB에 저장되는 좋아요 문서를 나타내는 클래스. - * MongoAuditingFields 를 상속받아 생성 및 수정 일자를 자동으로 관리합니다. - */ -@Getter -@Builder -@AllArgsConstructor -@Document(collection = "likes") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class LikeDocument extends MongoAuditingFields { - - @Id - @Field("_id") - private ObjectId id; - - @Field("post_id") - private ObjectId postId; - - @Field("user_id") - private Long userId; - - /** - * Like 로부터 LikeDocument 생성합니다. - * - * @param like 변환할 Like - * @return LikeDocument - */ - public static LikeDocument from(Like like) { - return LikeDocument.builder() - .id(like.getId() != null ? new ObjectId(like.getId()) : null) - .postId(new ObjectId(like.getPostId())) - .userId(like.getUserId()) - .build(); - } - - /** - * LikeDocument 를 Like 변환합니다. - * - * @return Like - */ - public Like toModel() { - return Like.builder() - .postId(this.postId.toHexString()) - .userId(this.userId) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/PostDocument.java b/src/main/java/darkoverload/itzip/feature/techinfo/model/document/PostDocument.java deleted file mode 100644 index d562dcd8..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/PostDocument.java +++ /dev/null @@ -1,97 +0,0 @@ -package darkoverload.itzip.feature.techinfo.model.document; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.global.entity.MongoAuditingFields; -import lombok.*; -import org.bson.types.ObjectId; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.data.mongodb.core.mapping.Field; - -import java.util.List; - -/** - * MongoDB에 저장되는 게시글 문서를 나타내는 클래스. - * MongoAuditingFields 를 상속받아 생성 및 수정 일자를 자동으로 관리합니다. - */ -@Getter -@Builder -@AllArgsConstructor -@Document(collection = "posts") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PostDocument extends MongoAuditingFields { - - @Id - @Field("_id") - private ObjectId id; - - @Field(name = "blog_id") - private Long blogId; - - @Field(name = "category_id") - private ObjectId categoryId; - - @Field(name = "title") - private String title; - - @Field(name = "content") - private String content; - - @Field(name = "view_count") - private Integer viewCount; - - @Field(name = "like_count") - private Integer likeCount; - - @Field(name = "is_public") - private Boolean isPublic; - - @Field(name = "thumbnail_image_path") - private String thumbnailImagePath; - - @Field(name = "content_image_paths") - private List contentImagePaths; - - /** - * Post 로부터 PostDocument 를 생성합니다. - * - * @param post 변환할 Post - * @return PostDocument - */ - public static PostDocument from(Post post) { - return PostDocument.builder() - .id(post.getId() != null ? new ObjectId(post.getId()) : null) - .blogId(post.getBlogId()) - .categoryId(new ObjectId(post.getCategoryId())) - .title(post.getTitle()) - .content(post.getContent()) - .viewCount(post.getViewCount()) - .likeCount(post.getLikeCount()) - .isPublic(post.getIsPublic()) - .thumbnailImagePath(post.getThumbnailImagePath()) - .contentImagePaths(post.getContentImagePaths()) - .build(); - } - - /** - * PostDocument 를 Post 로 변환합니다. - * - * @return Post - */ - public Post toModel() { - return Post.builder() - .id(this.id.toHexString()) - .blogId(this.blogId) - .categoryId(this.categoryId != null ? this.categoryId.toHexString() : null) - .title(this.title) - .content(this.content) - .viewCount(this.viewCount) - .likeCount(this.likeCount) - .isPublic(this.isPublic) - .createDate(this.createDate) - .thumbnailImagePath(this.thumbnailImagePath) - .contentImagePaths(this.contentImagePaths) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/ScrapDocument.java b/src/main/java/darkoverload/itzip/feature/techinfo/model/document/ScrapDocument.java deleted file mode 100644 index b8012f1c..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/model/document/ScrapDocument.java +++ /dev/null @@ -1,58 +0,0 @@ -package darkoverload.itzip.feature.techinfo.model.document; - -import darkoverload.itzip.feature.techinfo.domain.scrap.Scrap; -import darkoverload.itzip.global.entity.MongoAuditingFields; -import lombok.*; -import org.bson.types.ObjectId; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.data.mongodb.core.mapping.Field; - -/** - * MongoDB에 저장되는 스크랩 문서를 나타내는 클래스. - * MongoAuditingFields 를 상속받아 생성 및 수정 일자를 자동으로 관리한다. - */ -@Getter -@Builder -@AllArgsConstructor -@Document(collection = "scraps") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ScrapDocument extends MongoAuditingFields { - - @Id - @Field("_id") - private ObjectId id; - - @Field("post_id") - private ObjectId postId; - - @Field("user_id") - private Long userId; - - /** - * Scrap 로부터 ScrapDocument 를 생성합니다. - * - * @param scrap 변환할 Scrap - * @return ScrapDocument - */ - public static ScrapDocument from(Scrap scrap) { - return ScrapDocument.builder() - .id(scrap.getId() != null ? new ObjectId(scrap.getId()) : null) - .postId(new ObjectId(scrap.getPostId())) - .userId(scrap.getUserId()) - .build(); - } - - /** - * ScrapDocument 를 Scrap 로 변환합니다. - * - * @return Scrap - */ - public Scrap toModel() { - return Scrap.builder() - .postId(this.postId.toHexString()) - .userId(this.userId) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/model/entity/BlogEntity.java b/src/main/java/darkoverload/itzip/feature/techinfo/model/entity/BlogEntity.java deleted file mode 100644 index 926e60ff..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/model/entity/BlogEntity.java +++ /dev/null @@ -1,63 +0,0 @@ -package darkoverload.itzip.feature.techinfo.model.entity; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.user.entity.UserEntity; -import darkoverload.itzip.global.entity.AuditingFields; -import jakarta.persistence.*; -import lombok.*; - -/** - * 블로그 정보를 나타내는 엔티티 클래스. - * AuditingFields 를 상속받아 생성 및 수정 일자를 자동으로 관리합니다. - */ -@Entity -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "blogs") -public class BlogEntity extends AuditingFields { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne - @JoinColumn(name = "user_id") - private UserEntity user; - - private String intro; - - @Column(name = "is_public", nullable = false) - private Boolean isPublic; - - /** - * Blog 로부터 BlogEntity 를 생성합니다. - * - * @param blog - * @return - */ - public static BlogEntity from(Blog blog) { - return BlogEntity.builder() - .id(blog.getId()) - .user(blog.getUser().convertToEntity()) - .intro(blog.getIntro()) - .isPublic(blog.isPublic()) - .build(); - } - - /** - * BlogEntity 를 Blog 로 변환합니다. - * - * @return Blog - */ - public Blog toModel() { - return Blog.builder() - .id(this.id) - .user(this.user.convertToDomain()) - .intro(this.intro) - .isPublic(this.isPublic) - .build(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogCommandRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogCommandRepositoryImpl.java deleted file mode 100644 index 11691926..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogCommandRepositoryImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.model.entity.BlogEntity; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogCommandRepository; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogReadRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import jakarta.persistence.EntityNotFoundException; -import org.springframework.stereotype.Repository; -import lombok.RequiredArgsConstructor; - -/** - * 블로그 명령(생성, 수정)을 처리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class BlogCommandRepositoryImpl implements BlogCommandRepository { - - private final JpaBlogCommandRepository repository; - private final BlogReadRepository readRepository; - - /** - * 새로운 블로그를 저장합니다. - * - * @param blog 저장할 블로그 - */ - @Override - public Blog save(Blog blog) { - return repository.save(BlogEntity.from(blog)).toModel(); - } - - /** - * 블로그 소개글을 업데이트합니다. - * - * @param userId 사용자 ID - * @param newIntro 새로운 소개글 - * @throws RestApiException 블로그 업데이트 실패 시 발생 - */ - @Override - public Blog update(Long userId, String newIntro) { - if (repository.update(userId, newIntro) < 0) { - throw new RestApiException(CommonExceptionCode.UPDATE_FAIL_BLOG); - } - return readRepository.getByUserId(userId); - } - - /** - * 블로그의 공개 상태를 업데이트합니다. - * - * @param blogId 블로그 ID - * @param status 새로운 공개 상태 - * @throws RestApiException 블로그 상태 업데이트 실패 시 발생 - */ - @Override - public Blog update(Long blogId, boolean status) { - try { - if (repository.update(blogId, status) < 0) { - throw new RestApiException(CommonExceptionCode.UPDATE_FAIL_BLOG); - } - return readRepository.getReferenceById(blogId); - } catch (EntityNotFoundException e) { - throw new RestApiException(CommonExceptionCode.NOT_FOUND_BLOG); - } - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogReadRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogReadRepositoryImpl.java deleted file mode 100644 index 67fa22e7..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogReadRepositoryImpl.java +++ /dev/null @@ -1,108 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.model.entity.BlogEntity; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogReadRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -/** - * 블로그 조회를 처리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class BlogReadRepositoryImpl implements BlogReadRepository { - - private final JpaBlogReadRepository repository; - - /** - * 블로그 ID로 블로그를 조회합니다. - * - * @param id 블로그 ID - * @return Optional - */ - @Override - public Optional findByBlogId(Long id) { - return repository.findByBlogId(id).map(BlogEntity::toModel); - } - - /** - * 사용자 ID로 블로그를 조회합니다. - * - * @param id 사용자 ID - * @return Optional - */ - @Override - public Optional findByUserId(Long id) { - return repository.findByUserId(id).map(BlogEntity::toModel); - } - - /** - * 사용자 닉네임으로 블로그를 조회합니다. - * - * @param nickname 사용자 닉네임 - * @return Optional - */ - @Override - public Optional findByNickname(String nickname) { - return repository.findByNickname(nickname).map(BlogEntity::toModel); - } - - /** - * 블로그 ID로 블로그를 조회하고, 없으면 예외를 발생시킵니다. - * - * @param id 블로그 ID - * @return Blog - * @throws RestApiException 블로그를 찾을 수 없을 때 발생 - */ - @Override - public Blog getById(Long id) { - return this.findByBlogId(id).orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_BLOG) - ); - } - - /** - * 사용자 ID로 블로그를 조회하고, 없으면 예외를 발생시킵니다. - * - * @param id 사용자 ID - * @return Blog - * @throws RestApiException 블로그를 찾을 수 없을 때 발생 - */ - @Override - public Blog getByUserId(Long id) { - return this.findByUserId(id).orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_BLOG) - ); - } - - /** - * 사용자 닉네임으로 블로그를 조회하고, 없으면 예외를 발생시킵니다. - * - * @param nickname 사용자 닉네임 - * @return Blog - * @throws RestApiException 블로그를 찾을 수 없을 때 발생 - */ - @Override - public Blog getByNickname(String nickname) { - return this.findByNickname(nickname).orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_BLOG) - ); - } - - /** - * 블로그 ID로 블로그의 프록시 객체를 조회합니다. - * - * @param id 블로그 ID - * @return Blog - */ - @Override - public Blog getReferenceById(Long id) { - return repository.getReferenceById(id).toModel(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/JpaBlogCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/JpaBlogCommandRepository.java deleted file mode 100644 index 8d697ea3..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/JpaBlogCommandRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog; - -import darkoverload.itzip.feature.techinfo.model.entity.BlogEntity; -import darkoverload.itzip.feature.techinfo.repository.blog.custom.CustomBlogCommandRepository; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface JpaBlogCommandRepository extends JpaRepository, CustomBlogCommandRepository { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/JpaBlogReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/JpaBlogReadRepository.java deleted file mode 100644 index 20da74a5..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/JpaBlogReadRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog; - -import darkoverload.itzip.feature.techinfo.model.entity.BlogEntity; -import darkoverload.itzip.feature.techinfo.repository.blog.custom.CustomBlogReadRepository; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface JpaBlogReadRepository extends JpaRepository, CustomBlogReadRepository { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogCommandRepository.java deleted file mode 100644 index e1fb0870..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogCommandRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog.custom; - -public interface CustomBlogCommandRepository { - - long update(Long userId, String newIntro); - - long update(Long blogId, boolean status); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogCommandRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogCommandRepositoryImpl.java deleted file mode 100644 index 1c7cc848..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogCommandRepositoryImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog.custom; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import darkoverload.itzip.feature.techinfo.model.entity.QBlogEntity; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -/** - * 블로그 정보 수정을 위한 커스텀 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class CustomBlogCommandRepositoryImpl implements CustomBlogCommandRepository { - - private final JPAQueryFactory queryFactory; - - private final QBlogEntity qBlog = QBlogEntity.blogEntity; - - /** - * 사용자의 블로그 소개글을 업데이트합니다. - * - * @param userId 사용자 ID - * @param newIntro 새로운 소개글 - * @return 업데이트된 블로그 수 - */ - @Override - public long update(Long userId, String newIntro) { - return queryFactory - .update(qBlog) - .set(qBlog.intro, newIntro) - .where( - qBlog.user.id.eq(userId) - .and(qBlog.isPublic.isTrue()) - ) - .execute(); - } - - /** - * 블로그의 공개 상태를 업데이트합니다. - * - * @param blogId 블로그 ID - * @param status 새로운 공개 상태 - * @return 업데이트된 블로그 수 - */ - @Override - public long update(Long blogId, boolean status) { - return queryFactory - .update(qBlog) - .set(qBlog.isPublic, status) - .where( - qBlog.id.eq(blogId) - .and(qBlog.isPublic.isTrue()) - ) - .execute(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogReadRepository.java deleted file mode 100644 index b8220111..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogReadRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog.custom; - -import darkoverload.itzip.feature.techinfo.model.entity.BlogEntity; -import java.util.Optional; - -public interface CustomBlogReadRepository { - - Optional findByBlogId(Long id); - - Optional findByUserId(Long id); - - Optional findByNickname(String nickname); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogReadRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogReadRepositoryImpl.java deleted file mode 100644 index 706ad243..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/blog/custom/CustomBlogReadRepositoryImpl.java +++ /dev/null @@ -1,79 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog.custom; - -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.jpa.impl.JPAQueryFactory; -import darkoverload.itzip.feature.techinfo.model.entity.BlogEntity; -import darkoverload.itzip.feature.techinfo.model.entity.QBlogEntity; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -/** - * 블로그 조회를 위한 커스텀 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class CustomBlogReadRepositoryImpl implements CustomBlogReadRepository { - - private final JPAQueryFactory queryFactory; - private final QBlogEntity qBlog = QBlogEntity.blogEntity; - - /** - * 블로그 ID로 공개된 블로그를 조회합니다. - * - * @param id 블로그 ID - * @return Optional - */ - @Override - public Optional findByBlogId(Long id) { - return findBlogByCondition( - qBlog.id.eq(id) - .and(qBlog.isPublic.isTrue()) - ); - } - - /** - * 사용자 ID로 공개된 블로그를 조회합니다. - * - * @param id 사용자 ID - * @return Optional - */ - @Override - public Optional findByUserId(Long id) { - return findBlogByCondition( - qBlog.user.id.eq(id) - .and(qBlog.isPublic.isTrue()) - ); - } - - /** - * 사용자 닉네임으로 공개된 블로그를 조회합니다. - * - * @param nickname 사용자 닉네임 - * @return Optional - */ - @Override - public Optional findByNickname(String nickname) { - return findBlogByCondition( - qBlog.user.nickname.eq(nickname) - .and(qBlog.isPublic.isTrue()) - ); - } - - /** - * 주어진 조건에 맞는 블로그를 조회합니다. - * - * @param condition 조회 조건 - * @return Optional - */ - private Optional findBlogByCondition(BooleanExpression condition) { - BlogEntity result = queryFactory - .selectFrom(qBlog) - .where(condition) - .fetchOne(); - - return Optional.ofNullable(result); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentCommandRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentCommandRepositoryImpl.java deleted file mode 100644 index a0c476db..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentCommandRepositoryImpl.java +++ /dev/null @@ -1,93 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import darkoverload.itzip.feature.techinfo.service.comment.port.CommentCommandRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * 댓글 명령(생성, 수정, 삭제)을 처리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class CommentCommandRepositoryImpl implements CommentCommandRepository { - - private final MongoCommentCommandRepository repository; - - /** - * 새로운 댓글을 저장합니다. - * - * @param comment 저장할 댓글 - */ - @Override - public Comment save(Comment comment) { - return repository.save(CommentDocument.from(comment)).toModel(); - } - - /** - * 여러 댓글을 한꺼번에 저장합니다. - * - * @param comments 저장할 댓글 리스트 - * @return 저장된 댓글 리스트 - */ - @Override - public List saveAll(List comments) { - return repository.saveAll( - comments.stream() - .map(CommentDocument::from) - .toList() - ).stream() - .map(CommentDocument::toModel) - .toList(); - } - - /** - * 댓글 내용을 업데이트합니다. - * - * @param commentId 업데이트할 댓글의 ID - * @param userId 댓글 작성자의 ID - * @param content 새로운 댓글 내용 - * @throws RestApiException 댓글 업데이트 실패 시 발생 - */ - @Override - public Comment update(ObjectId commentId, Long userId, String content) { - return repository.update(commentId, userId, content) - .map(CommentDocument::toModel) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.UPDATE_FAIL_COMMENT) - ); - } - - /** - * 댓글의 공개 상태를 업데이트합니다. - * - * @param commentId 업데이트할 댓글의 ID - * @param userId 댓글 작성자의 ID - * @param status 새로운 공개 상태 - * @throws RestApiException 댓글 상태 업데이트 실패 시 발생 - */ - @Override - public Comment update(ObjectId commentId, Long userId, boolean status) { - return repository.update(commentId, userId, status) - .map(CommentDocument::toModel) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.UPDATE_FAIL_COMMENT) - ); - } - - /** - * 모든 댓글을 삭제합니다. - * 주로 테스트 환경이나 데이터 초기화에 사용됩니다. - */ - @Override - public void deleteAll() { - repository.deleteAll(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentReadRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentReadRepositoryImpl.java deleted file mode 100644 index 644eda7b..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentReadRepositoryImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import darkoverload.itzip.feature.techinfo.service.comment.port.CommentReadRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; - -/** - * 댓글 조회를 처리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class CommentReadRepositoryImpl implements CommentReadRepository { - - private final MongoCommentReadRepository repository; - - /** - * 특정 포스트의 댓글을 페이징하여 조회합니다. - * - * @param id 포스트 ID - * @param pageable 페이징 정보 - * @return Page - */ - public Page findCommentsByPostId(Object id, Pageable pageable) { - return repository.findCommentsByPostId(id, pageable).map(CommentDocument::toModel); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/MongoCommentCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/MongoCommentCommandRepository.java deleted file mode 100644 index 73917787..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/MongoCommentCommandRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment; - -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import darkoverload.itzip.feature.techinfo.repository.comment.custom.CustomCommentCommandRepository; -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.repository.MongoRepository; - -@ExcludeFromJpaRepositories -public interface MongoCommentCommandRepository extends MongoRepository, - CustomCommentCommandRepository { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/MongoCommentReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/MongoCommentReadRepository.java deleted file mode 100644 index 08dbc041..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/MongoCommentReadRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment; - -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import darkoverload.itzip.feature.techinfo.repository.comment.custom.CustomCommentReadRepository; -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.repository.MongoRepository; - -@ExcludeFromJpaRepositories -public interface MongoCommentReadRepository extends MongoRepository, - CustomCommentReadRepository { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentCommandRepository.java deleted file mode 100644 index 8e19c523..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentCommandRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment.custom; - -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import org.bson.types.ObjectId; -import java.util.Optional; - -public interface CustomCommentCommandRepository { - - Optional update(ObjectId commentId, Long userId, String content); - - Optional update(ObjectId commentId, Long userId, boolean status); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentCommandRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentCommandRepositoryImpl.java deleted file mode 100644 index 45b0d081..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentCommandRepositoryImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment.custom; - -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import org.springframework.data.mongodb.core.FindAndModifyOptions; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; -import org.springframework.stereotype.Repository; -import org.bson.types.ObjectId; -import lombok.RequiredArgsConstructor; -import java.time.LocalDateTime; -import java.util.Optional; - -/** - * 댓글 명령(수정, 삭제)을 처리하는 커스텀 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class CustomCommentCommandRepositoryImpl implements CustomCommentCommandRepository { - - private final MongoTemplate mongoTemplate; - - /** - * 댓글 내용을 업데이트합니다. - * - * @param commentId 업데이트할 댓글의 ID - * @param userId 댓글 작성자의 ID - * @param content 새로운 댓글 내용 - * @return 업데이트된 댓글의 수 - */ - @Override - public Optional update(ObjectId commentId, Long userId, String content) { - Query query = new Query(Criteria.where("_id").is(commentId).and("user_id").is(userId)); - - Update update = new Update() - .set("content", content) - .set("modify_date", LocalDateTime.now()); - - FindAndModifyOptions options = FindAndModifyOptions.options() - .returnNew(true) - .upsert(false); - - return Optional.ofNullable(mongoTemplate.findAndModify(query, update, options, CommentDocument.class)); - } - - /** - * 댓글의 공개 상태를 업데이트합니다. - * - * @param commentId 업데이트할 댓글의 ID - * @param userId 댓글 작성자의 ID - * @param status 새로운 공개 상태 - * @return 업데이트된 댓글의 수 - */ - @Override - public Optional update(ObjectId commentId, Long userId, boolean status) { - Query query = new Query(Criteria.where("_id").is(commentId).and("user_id").is(userId)); - Update update = new Update().set("is_public", status); - - FindAndModifyOptions options = FindAndModifyOptions.options() - .returnNew(true) - .upsert(false); - - return Optional.ofNullable(mongoTemplate.findAndModify(query, update, options, CommentDocument.class)); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentReadRepository.java deleted file mode 100644 index d965d36b..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentReadRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment.custom; - -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface CustomCommentReadRepository { - - Page findCommentsByPostId(Object id, Pageable pageable); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentReadRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentReadRepositoryImpl.java deleted file mode 100644 index e430147d..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/comment/custom/CustomCommentReadRepositoryImpl.java +++ /dev/null @@ -1,49 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment.custom; - -import darkoverload.itzip.feature.techinfo.model.document.CommentDocument; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * 댓글 조회를 위한 커스텀 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class CustomCommentReadRepositoryImpl implements CustomCommentReadRepository { - - private final MongoTemplate mongoTemplate; - - /** - * 특정 포스트의 공개 댓글을 페이징하여 조회합니다. - * - * @param id 게시글 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findCommentsByPostId(Object id, Pageable pageable) { - Query query = new Query(Criteria - .where("post_id").is(id) - .and("is_public").is(true)) - .with(pageable); - - query.fields() - .exclude("post_id") - .exclude("is_public"); - - List comments = mongoTemplate.find(query, CommentDocument.class); - - long total = mongoTemplate.count(query, CommentDocument.class); - - return new PageImpl<>(comments, pageable, total); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/LikeCacheRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/LikeCacheRepositoryImpl.java deleted file mode 100644 index f7284fe7..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/LikeCacheRepositoryImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.like; - -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import darkoverload.itzip.feature.techinfo.repository.like.redis.RedisLikeRepository; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeCacheRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * Redis 를 사용하여 좋아요 상태를 캐싱하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class LikeCacheRepositoryImpl implements LikeCacheRepository { - - private final RedisLikeRepository repository; - - /** - * 사용자의 포스트 좋아요 상태를 캐시에 저장합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @param isLiked 좋아요 상태 - * @param ttl 캐시 유효 시간(초) - */ - @Override - public void save(Long userId, String postId, boolean isLiked, long ttl) { - repository.save(userId, postId, isLiked, ttl); - } - - /** - * 사용자의 특정 포스트에 대한 좋아요 상태를 캐시에서 조회합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 좋아요 상태 (Boolean), 캐시에 없으면 null - */ - @Override - public Boolean getLikeStatus(Long userId, String postId) { - return repository.getLikeStatus(userId, postId); - } - - /** - * 캐시에 저장된 모든 좋아요 상태를 조회합니다. - * - * @return List - */ - @Override - public List getAllLikeStatuses() { - return repository.getAllLikeStatuses(); - } - - /** - * 캐시에 저장된 모든 좋아요 데이터를 삭제합니다. - * 주로 테스트 환경이나 캐시 초기화 용도로 사용됩니다. - */ - @Override - public void deleteAll() { - repository.deleteAll(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/LikeRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/LikeRepositoryImpl.java deleted file mode 100644 index a4510ca9..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/LikeRepositoryImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.like; - -import darkoverload.itzip.feature.techinfo.domain.like.Like; -import darkoverload.itzip.feature.techinfo.model.document.LikeDocument; -import darkoverload.itzip.feature.techinfo.repository.like.mongo.MongoLikeRepository; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Repository; - -/** - * MongoDB를 사용하여 좋아요 정보를 관리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class LikeRepositoryImpl implements LikeRepository { - - private final MongoLikeRepository repository; - - /** - * 새로운 좋아요 정보를 저장합니다. - * - * @param like 좋아요 - */ - @Override - public Like save(Like like) { - return repository.save(LikeDocument.from(like)).toModel(); - } - - /** - * 특정 사용자가 특정 포스트에 좋아요를 눌렀는지 확인합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 좋아요가 존재하면 true, 그렇지 않으면 false - */ - @Override - public boolean existsByUserIdAndPostId(Long userId, ObjectId postId) { - return repository.existsByUserIdAndPostId(userId, postId); - } - - /** - * 특정 사용자의 특정 포스트에 대한 좋아요를 삭제합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @throws RestApiException 좋아요 삭제에 실패했을 때 발생 - */ - @Override - public void deleteByUserIdAndPostId(Long userId, ObjectId postId) { - if (repository.deleteByUserIdAndPostId(userId, postId) <= 0) { - throw new RestApiException(CommonExceptionCode.DELETE_FAIL_LIKE_IN_POST); - } - } - - /** - * 저장된 모든 좋아요 데이터를 삭제합니다. - * 주로 테스트 환경이나 데이터 초기화 시 사용됩니다. - */ - @Override - public void deleteAll() { - repository.deleteAll(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/mongo/MongoLikeRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/mongo/MongoLikeRepository.java deleted file mode 100644 index 7b195d78..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/mongo/MongoLikeRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.like.mongo; - -import darkoverload.itzip.feature.techinfo.model.document.LikeDocument; -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.repository.MongoRepository; - -@ExcludeFromJpaRepositories -public interface MongoLikeRepository extends MongoRepository { - - /** - * 특정 사용자가 특정 포스트에 좋아요를 눌렀는지 확인합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 좋아요가 존재하면 true, 그렇지 않으면 false - */ - boolean existsByUserIdAndPostId(Long userId, ObjectId postId); - - /** - * 특정 사용자의 특정 포스트에 대한 좋아요를 삭제합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 삭제된 좋아요의 수 - */ - long deleteByUserIdAndPostId(Long userId, ObjectId postId); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/redis/RedisLikeRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/redis/RedisLikeRepository.java deleted file mode 100644 index 5b25616c..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/redis/RedisLikeRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.like.redis; - -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import java.util.List; - -public interface RedisLikeRepository { - - void save(Long userId, String postId, boolean isLiked, long ttl); - - Boolean getLikeStatus(Long userId, String postId); - - List getAllLikeStatuses(); - - void deleteAll(); - -} \ No newline at end of file diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/redis/RedisLikeRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/redis/RedisLikeRepositoryImpl.java deleted file mode 100644 index 771767cb..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/like/redis/RedisLikeRepositoryImpl.java +++ /dev/null @@ -1,88 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.like.redis; - -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import darkoverload.itzip.feature.techinfo.util.RedisKeyUtil; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Redis 를 사용하여 좋아요 상태를 관리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class RedisLikeRepositoryImpl implements RedisLikeRepository { - - private final RedisTemplate redisTemplate; - - /** - * 사용자의 포스트 좋아요 상태를 Redis 에 저장합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @param isLiked 좋아요 상태 - * @param ttl 데이터 유효 시간(초) - */ - @Override - public void save(Long userId, String postId, boolean isLiked, long ttl) { - String redisKey = RedisKeyUtil.buildRedisKey(userId, postId, "like"); - redisTemplate.opsForValue().set(redisKey, String.valueOf(isLiked), ttl, TimeUnit.SECONDS); - } - - /** - * 사용자의 특정 포스트에 대한 좋아요 상태를 조회합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 좋아요 상태 (Boolean), 없으면 null - */ - @Override - public Boolean getLikeStatus(Long userId, String postId) { - String redisKey = RedisKeyUtil.buildRedisKey(userId, postId, "like"); - String isLiked = (String) redisTemplate.opsForValue().get(redisKey); - return isLiked != null ? Boolean.valueOf(isLiked) : null; - } - - /** - * Redis 에 저장된 모든 좋아요 상태를 조회합니다. - * - * @return List - */ - @Override - public List getAllLikeStatuses() { - List likeStatuses = new ArrayList<>(); - - Set keys = redisTemplate.keys("post:*:user:*:like"); - - for (String key : keys) { - String likeStatus = (String) redisTemplate.opsForValue().get(key); - - if (likeStatus != null) { - Boolean isLiked = Boolean.valueOf(likeStatus); - String[] parts = key.split(":"); - String postId = parts[1]; - Long userId = Long.valueOf(parts[3]); - - likeStatuses.add(LikeStatus.from(postId, userId, isLiked)); - } - } - - return likeStatuses; - } - - /** - * Redis에 저장된 모든 좋아요 데이터를 삭제합니다. - * 주로 테스트 환경이나 데이터 초기화에 사용됩니다. - */ - @Override - public void deleteAll() { - // 좋아요 관련 모든 키 삭제 - redisTemplate.delete(redisTemplate.keys("post:*:user:*:like")); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/MongoPostCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/MongoPostCommandRepository.java deleted file mode 100644 index 5feb4fec..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/MongoPostCommandRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post; - -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import darkoverload.itzip.feature.techinfo.repository.post.custom.CustomPostCommandRepository; -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.repository.MongoRepository; - -@ExcludeFromJpaRepositories -public interface MongoPostCommandRepository extends MongoRepository, - CustomPostCommandRepository { -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/MongoPostReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/MongoPostReadRepository.java deleted file mode 100644 index 044808ec..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/MongoPostReadRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post; - -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import darkoverload.itzip.feature.techinfo.repository.post.custom.CustomPostReadRepository; -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.repository.MongoRepository; - -@ExcludeFromJpaRepositories -public interface MongoPostReadRepository extends MongoRepository, CustomPostReadRepository { - - boolean existsByIdAndIsPublicTrue(ObjectId postId); - - long countByIsPublicTrue(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/PostCommandRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/PostCommandRepositoryImpl.java deleted file mode 100644 index 1be27593..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/PostCommandRepositoryImpl.java +++ /dev/null @@ -1,112 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import darkoverload.itzip.feature.techinfo.service.post.port.PostCommandRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Repository; -import lombok.RequiredArgsConstructor; -import java.util.List; - -/** - * MongoDB를 사용하여 포스트 명령(생성, 수정)을 처리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class PostCommandRepositoryImpl implements PostCommandRepository { - - private final MongoPostCommandRepository repository; - - /** - * 새로운 포스트를 저장합니다. - * - * @param post 저장할 Post - */ - @Override - public Post save(Post post) { - return repository.save(PostDocument.from(post)).toModel(); - } - - @Override - public List saveAll(List post) { - return repository.saveAll( - post.stream() - .map(PostDocument::from) - .toList() - ).stream() - .map(PostDocument::toModel) - .toList(); - } - - /** - * 포스트의 상세 정보를 업데이트합니다. - * - * @param postId 포스트 ID - * @param categoryId 카테고리 ID - * @param title 제목 - * @param content 내용 - * @param thumbnailImageUrl 썸네일 이미지 URL - * @param contentImageUrls 본문 이미지 URL 목록 - * @throws RestApiException 포스트 업데이트 실패 시 발생 - */ - @Override - public Post update( - ObjectId postId, - ObjectId categoryId, - String title, - String content, - String thumbnailImageUrl, - List contentImageUrls - ) { - return repository.update(postId, categoryId, title, content, thumbnailImageUrl, contentImageUrls) - .map(PostDocument::toModel) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.UPDATE_FAIL_POST) - ); - } - - /** - * 포스트의 공개 상태를 업데이트합니다. - * - * @param postId 포스트 ID - * @param status 새로운 공개 상태 - * @throws RestApiException 포스트 상태 업데이트 실패 시 발생 - */ - @Override - public Post update(ObjectId postId, boolean status) { - return repository.update(postId, status) - .map(PostDocument::toModel) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.UPDATE_FAIL_POST) - ); - } - - /** - * 포스트의 특정 필드 값을 업데이트합니다. - * - * @param postId 포스트 ID - * @param fieldName 업데이트할 필드 이름 - * @param value 새로운 값 - * @throws RestApiException 포스트 필드 업데이트 실패 시 발생 - */ - @Override - public Post updateFieldWithValue(ObjectId postId, String fieldName, int value) { - return repository.updateFieldWithValue(postId, fieldName, value) - .map(PostDocument::toModel) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.UPDATE_FAIL_POST) - ); - } - - /** - * 모든 포스트를 삭제합니다. - * 주로 테스트 환경이나 데이터 초기화 시 사용됩니다. - */ - @Override - public void deleteAll() { - repository.deleteAll(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/PostReadRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/PostReadRepositoryImpl.java deleted file mode 100644 index ba725d03..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/PostReadRepositoryImpl.java +++ /dev/null @@ -1,117 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import darkoverload.itzip.feature.techinfo.service.post.port.PostReadRepository; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -/** - * MongoDB를 사용하여 포스트 조회 작업을 수행하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class PostReadRepositoryImpl implements PostReadRepository { - - private final MongoPostReadRepository repository; - - /** - * ID로 포스트를 조회합니다. - * - * @param id 포스트 ID - * @return Optional - */ - @Override - public Optional findById(ObjectId id) { - return repository.findByPostId(id).map(PostDocument::toModel); - } - - /** - * 모든 공개 포스트를 페이징하여 조회합니다. - * - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findAll(Pageable pageable) { - return repository.findAll(pageable).map(PostDocument::toModel); - } - - /** - * 특정 블로그의 공개 포스트를 페이징하여 조회합니다. - * - * @param blogId 블로그 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findPostsByBlogId(Long blogId, Pageable pageable) { - return repository.findPostsByBlogId(blogId, pageable).map(PostDocument::toModel); - } - - /** - * 특정 카테고리의 공개 포스트를 페이징하여 조회합니다. - * - * @param categoryId 카테고리 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findPostsByCategoryId(ObjectId categoryId, Pageable pageable) { - return repository.findPostsByCategoryId(categoryId, pageable).map(PostDocument::toModel); - } - - /** - * 특정 날짜 범위의 포스트를 조회합니다. - * - * @param blogId 블로그 ID - * @param createDate 기준 날짜 - * @param limit 조회할 포스트 수 - * @return List - */ - @Override - public List findPostsByDateRange(Long blogId, LocalDateTime createDate, int limit) { - return repository.findPostsByDateRange(blogId, createDate, limit).stream().map(PostDocument::toModel).toList(); - } - - /** - * 특정 블로그의 연간 포스트 통계를 조회합니다. - * - * @param blogId 블로그 ID - * @return List - */ - @Override - public List findYearlyPostStatsByBlogId(Long blogId) { - return repository.findYearlyPostStatsByBlogId(blogId); - } - - /** - * 전체 공개 포스트 수를 조회합니다. - * - * @return 공개 포스트 수 - */ - @Override - public long getPostCount() { - return repository.countByIsPublicTrue(); - } - - /** - * 특정 ID의 공개 포스트가 존재하는지 확인합니다. - * - * @param postId 포스트 ID - * @return 존재 여부 - */ - @Override - public boolean existsById(ObjectId postId) { - return repository.existsByIdAndIsPublicTrue(postId); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostCommandRepository.java deleted file mode 100644 index c02fa51f..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostCommandRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post.custom; - -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import org.bson.types.ObjectId; -import java.util.Optional; -import java.util.List; - -public interface CustomPostCommandRepository { - - Optional update(ObjectId postId, ObjectId categoryId, String title, String content, String thumbnailImagePath, - List contentImagePaths); - - Optional update(ObjectId postId, boolean status); - - Optional updateFieldWithValue(ObjectId postId, String fieldName, int value); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostCommandRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostCommandRepositoryImpl.java deleted file mode 100644 index 37d095fd..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostCommandRepositoryImpl.java +++ /dev/null @@ -1,94 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post.custom; - -import com.mongodb.client.result.UpdateResult; -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.core.FindAndModifyOptions; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -/** - * MongoDB를 사용하여 포스트 정보를 수정하는 커스텀 레포지토리 구현 클래스. - */ -@RequiredArgsConstructor -public class CustomPostCommandRepositoryImpl implements CustomPostCommandRepository { - - private final MongoTemplate mongoTemplate; - - /** - * 포스트의 상세 정보를 업데이트합니다. - * - * @param postId 포스트 ID - * @param categoryId 카테고리 ID - * @param title 제목 - * @param content 내용 - * @param thumbnailImagePath 썸네일 이미지 URL - * @param contentImagePaths 본문 이미지 URL 목록 - * @return Optional - */ - @Override - public Optional update(ObjectId postId, ObjectId categoryId, String title, String content, - String thumbnailImagePath, List contentImagePaths) { - Query query = new Query(Criteria.where("_id").is(postId)); - - Update update = new Update() - .set("category_id", categoryId) - .set("title", title) - .set("content", content) - .set("thumbnail_image_path", thumbnailImagePath) - .set("content_image_paths", contentImagePaths) - .set("modify_date", LocalDateTime.now()); - - FindAndModifyOptions options = FindAndModifyOptions.options() - .returnNew(true) - .upsert(false); - - return Optional.ofNullable(mongoTemplate.findAndModify(query, update, options, PostDocument.class)); - } - - /** - * 포스트의 공개 상태를 업데이트합니다. - * - * @param postId 포스트 ID - * @param status 새로운 공개 상태 - * @return Optional - */ - @Override - public Optional update(ObjectId postId, boolean status) { - Query query = new Query(Criteria.where("_id").is(postId)); - Update update = new Update().set("is_public", status); - - FindAndModifyOptions options = FindAndModifyOptions.options() - .returnNew(true) - .upsert(false); - - return Optional.ofNullable(mongoTemplate.findAndModify(query, update, options, PostDocument.class)); - } - - /** - * 포스트의 특정 필드 값을 증가시킵니다. - * - * @param postId 포스트 ID - * @param fieldName 업데이트할 필드 이름 - * @param value 증가시킬 값 - * @return PostDocument - */ - @Override - public Optional updateFieldWithValue(ObjectId postId, String fieldName, int value) { - Query query = new Query(Criteria.where("_id").is(postId)); - Update update = new Update().inc(fieldName, value); - - FindAndModifyOptions options = FindAndModifyOptions.options() - .returnNew(true) - .upsert(false); - - return Optional.ofNullable(mongoTemplate.findAndModify(query, update, options, PostDocument.class)); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostReadRepository.java deleted file mode 100644 index 8b76f456..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostReadRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post.custom; - -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface CustomPostReadRepository { - - Optional findByPostId(ObjectId id); - - Page findAll(Pageable pageable); - - Page findPostsByBlogId(Long blogId, Pageable pageable); - - Page findPostsByCategoryId(ObjectId categoryId, Pageable pageable); - - List findPostsByDateRange(Long blogId, LocalDateTime createDate, int limit); - - List findYearlyPostStatsByBlogId(Long blogId); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostReadRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostReadRepositoryImpl.java deleted file mode 100644 index 2bbade6c..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/post/custom/CustomPostReadRepositoryImpl.java +++ /dev/null @@ -1,245 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post.custom; - -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.techinfo.model.document.PostDocument; -import lombok.RequiredArgsConstructor; -import org.bson.Document; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.aggregation.Aggregation; -import org.springframework.data.mongodb.core.aggregation.AggregationResults; -import org.springframework.data.mongodb.core.aggregation.ProjectionOperation; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.stereotype.Repository; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; - -/** - * MongoDB를 사용하여 포스트 조회 작업을 수행하는 커스텀 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class CustomPostReadRepositoryImpl implements CustomPostReadRepository { - - private static final List POST_FIELDS = List.of("post_id", "title", "create_date"); - private static final String COLLECTION_NAME = "posts"; - - private final MongoTemplate mongoTemplate; - - /** - * 포스트 ID로 공개된 포스트를 조회합니다. - * - * @param id 포스트 ID - * @return Optional - */ - @Override - public Optional findByPostId(ObjectId id) { - Criteria criteria = Criteria.where("_id").is(id).and("is_public").is(true); - return Optional.ofNullable( - mongoTemplate.findOne(Query.query(criteria), PostDocument.class) - ); - } - - /** - * 모든 공개 포스트를 페이징하여 조회합니다. - * - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findAll(Pageable pageable) { - return queryPostsWithCriteria( - Criteria.where("is_public").is(true), pageable, true - ); - } - - /** - * 특정 블로그의 공개 포스트를 페이징하여 조회합니다. - * - * @param blogId 블로그 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findPostsByBlogId(Long blogId, Pageable pageable) { - return queryPostsWithCriteria( - Criteria.where("blog_id").is(blogId).and("is_public").is(true), pageable, true - ); - } - - /** - * 특정 카테고리의 공개 포스트를 페이징하여 조회합니다. - * - * @param categoryId 카테고리 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findPostsByCategoryId(ObjectId categoryId, Pageable pageable) { - return queryPostsWithCriteria( - Criteria.where("category_id").is(categoryId).and("is_public").is(true), pageable, true - ); - } - - /** - * 주어진 조건에 맞는 포스트를 페이징하여 조회합니다. - * - * @param criteria 조회 조건 - * @param pageable 페이징 정보 - * @param includeBlogId 블로그 ID 포함 여부 - * @return Page - */ - private Page queryPostsWithCriteria(Criteria criteria, Pageable pageable, boolean includeBlogId) { - Aggregation aggregation = newAggregation( - match(criteria), - createProjectionWithBlogId(includeBlogId), - sort(pageable.getSort()), - skip(pageable.getOffset()), - limit(pageable.getPageSize()) - ); - - List posts = mongoTemplate.aggregate(aggregation, COLLECTION_NAME, PostDocument.class) - .getMappedResults(); - - long total = mongoTemplate.count(Query.query(criteria), PostDocument.class); - return new PageImpl<>(posts, pageable, total); - } - - /** - * 블로그 ID 포함 여부에 따른 프로젝션 연산을 생성합니다. - * - * @param includeBlogId 블로그 ID 포함 여부 - * @return ProjectionOperation - */ - private ProjectionOperation createProjectionWithBlogId(boolean includeBlogId) { - ProjectionOperation projectionOperation = project( - "_id", "category_id", "title", "like_count", "create_date", "thumbnail_image_path") - .and(aggregationOperationContext -> new Document("$substrCP", List.of("$content", 0, 300))) - .as("content"); - - if (includeBlogId) { - projectionOperation = projectionOperation.and("blog_id").as("blog_id"); - } - - return projectionOperation; - } - - /** - * 특정 날짜 범위의 포스트를 조회합니다. - * - * @param blogId 블로그 ID - * @param createDate 기준 날짜 - * @param limit 조회할 포스트 수 - * @return List - */ - @Override - public List findPostsByDateRange(Long blogId, LocalDateTime createDate, int limit) { - Criteria previousCriteria = buildDateRangeCriteria(blogId, createDate, true); - Criteria nextCriteria = buildDateRangeCriteria(blogId, createDate, false); - - List previousPosts = queryPostsWithField(previousCriteria, Sort.Direction.DESC, limit); - List nextPosts = queryPostsWithField(nextCriteria, Sort.Direction.ASC, limit); - - return mergePosts(previousPosts, nextPosts, 2); - } - - /** - * 날짜 범위 조회를 위한 Criteria 를 생성합니다. - * - * @param blogId 블로그 ID - * @param createDate 기준 날짜 - * @param isPrevious 이전 포스트 조회 여부 - * @return Criteria - */ - private Criteria buildDateRangeCriteria(Long blogId, LocalDateTime createDate, boolean isPrevious) { - Criteria criteria = Criteria.where("blog_id").is(blogId).and("is_public").is(true); - return isPrevious ? criteria.and("create_date").lt(createDate) : criteria.and("create_date").gt(createDate); - } - - /** - * 주어진 조건에 맞는 포스트를 필드를 지정하여 조회합니다. - * - * @param criteria 조회 조건 - * @param direction 정렬 방향 - * @param limit 조회할 포스트 수 - * @return List - */ - private List queryPostsWithField(Criteria criteria, Sort.Direction direction, int limit) { - Query query = new Query(criteria) - .with(Sort.by(direction, "create_date")) - .limit(limit); - - POST_FIELDS.forEach(query.fields()::include); - return mongoTemplate.find(query, PostDocument.class); - } - - /** - * 이전 포스트와 다음 포스트를 병합합니다. - * - * @param previousPosts 이전 포스트 목록 - * @param nextPosts 다음 포스트 목록 - * @param maxPostsToMerge 병합할 최대 포스트 수 - * @return List - */ - private List mergePosts(List previousPosts, List nextPosts, - int maxPostsToMerge) { - if (!previousPosts.isEmpty() && !nextPosts.isEmpty()) { - return Stream.concat( - nextPosts.stream().limit(maxPostsToMerge), - previousPosts.stream().limit(maxPostsToMerge) - ).toList(); - } - - if (!previousPosts.isEmpty()) { - return previousPosts; - } - - return nextPosts; - } - - /** - * 특정 블로그의 연간 포스트 통계를 조회합니다. - * - * @param blogId 블로그 ID - * @return List - */ - @Override - public List findYearlyPostStatsByBlogId(Long blogId) { - LocalDateTime oneYearAgo = LocalDateTime.now().minusYears(1); - - Aggregation aggregation = newAggregation( - match(new Criteria("create_date").gte(oneYearAgo) - .and("is_public").is(true) - .and("blog_id").is(blogId)), - project() - .andExpression("year(create_date)").as("year") - .andExpression("month(create_date)").as("month") - .andExpression("1 + floor((dayOfMonth(create_date) - 1) / 7)").as("week"), - group("year", "month", "week") - .count().as("postCount"), - group("year", "month") - .push(new Document("week", "$_id.week").append("postCount", "$postCount")).as("weeks"), - group("year") - .push(new Document("month", "$_id.month").append("weeks", "$weeks")).as("months"), - project() - .and("_id").as("year") - .and("months").as("months"), - sort(Sort.by(Sort.Direction.DESC, "year")) - ); - - AggregationResults results = mongoTemplate.aggregate(aggregation, "posts", - YearlyPostStats.class); - return results.getMappedResults(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapCacheRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapCacheRepositoryImpl.java deleted file mode 100644 index 57443105..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapCacheRepositoryImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.scrap; - -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; -import darkoverload.itzip.feature.techinfo.repository.scrap.redis.RedisScrapRepository; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapCacheRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * Redis 를 사용하여 스크랩 상태를 캐싱하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class ScrapCacheRepositoryImpl implements ScrapCacheRepository { - - private final RedisScrapRepository repository; - - /** - * 사용자의 포스트 스크랩 상태를 캐시에 저장합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @param isScraped 스크랩 상태 - * @param ttl 캐시 유효 시간(초) - */ - @Override - public void save(Long userId, String postId, boolean isScraped, long ttl) { - repository.save(userId, postId, isScraped, ttl); - } - - /** - * 사용자의 특정 포스트에 대한 스크랩 상태를 캐시에서 조회합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 스크랩 상태 (Boolean), 캐시에 없으면 null - */ - @Override - public Boolean getScrapStatus(Long userId, String postId) { - return repository.getScrapStatus(userId, postId); - } - - /** - * 캐시에 저장된 모든 스크랩 상태를 조회합니다. - * - * @return List - */ - @Override - public List getAllScrapStatuses() { - return repository.getAllScrapStatuses(); - } - - /** - * 캐시에 저장된 모든 스크랩 데이터를 삭제합니다. - * 주로 테스트 환경이나 데이터 초기화에 사용됩니다. - */ - @Override - public void deleteAll() { - repository.deleteAll(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapRepositoryImpl.java deleted file mode 100644 index 4f487da7..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapRepositoryImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.scrap; - -import darkoverload.itzip.feature.techinfo.domain.scrap.Scrap; -import darkoverload.itzip.feature.techinfo.model.document.ScrapDocument; -import darkoverload.itzip.feature.techinfo.repository.scrap.mongo.MongoScrapRepository; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.springframework.stereotype.Repository; -import org.bson.types.ObjectId; -import lombok.RequiredArgsConstructor; - -/** - * MongoDB를 사용하여 스크랩 정보를 관리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class ScrapRepositoryImpl implements ScrapRepository { - - private final MongoScrapRepository repository; - - /** - * 새로운 스크랩 정보를 저장합니다. - * - * @param scrap 저장할 Scrap - */ - @Override - public Scrap save(Scrap scrap) { - return repository.save(ScrapDocument.from(scrap)).toModel(); - } - - /** - * 특정 사용자가 특정 포스트를 스크랩했는지 확인합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 스크랩이 존재하면 true, 그렇지 않으면 false - */ - @Override - public boolean existsByUserIdAndPostId(Long userId, ObjectId postId) { - return repository.existsByUserIdAndPostId(userId, postId); - } - - /** - * 특정 사용자의 특정 포스트에 대한 스크랩을 삭제합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @throws RestApiException 스크랩 삭제에 실패했을 때 발생 - */ - @Override - public void deleteByUserIdAndPostId(Long userId, ObjectId postId) { - if (repository.deleteByUserIdAndPostId(userId, postId) <= 0) { - throw new RestApiException(CommonExceptionCode.DELETE_FAIL_SCRAP_IN_POST); - } - } - - /** - * 저장된 모든 스크랩 데이터를 삭제합니다. - * 주로 테스트 환경이나 데이터 초기화 시 사용됩니다. - */ - @Override - public void deleteAll() { - repository.deleteAll(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/mongo/MongoScrapRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/mongo/MongoScrapRepository.java deleted file mode 100644 index a1a216cd..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/mongo/MongoScrapRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.scrap.mongo; - -import darkoverload.itzip.feature.techinfo.model.document.ScrapDocument; -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.repository.MongoRepository; - -@ExcludeFromJpaRepositories -public interface MongoScrapRepository extends MongoRepository { - - /** - * 특정 사용자가 특정 포스트를 스크랩했는지 확인합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 스크랩이 존재하면 true, 그렇지 않으면 false - */ - boolean existsByUserIdAndPostId(Long userId, ObjectId postId); - - /** - * 특정 사용자의 특정 포스트에 대한 스크랩을 삭제합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 삭제된 스크랩의 수 - */ - long deleteByUserIdAndPostId(Long userId, ObjectId postId); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/redis/RedisScrapRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/redis/RedisScrapRepository.java deleted file mode 100644 index e72b159e..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/redis/RedisScrapRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.scrap.redis; - -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; - -import java.util.List; - -public interface RedisScrapRepository { - - void save(Long userId, String postId, boolean isScraped, long ttl); - - Boolean getScrapStatus(Long userId, String postId); - - List getAllScrapStatuses(); - - void deleteAll(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/redis/RedisScrapRepositoryImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/redis/RedisScrapRepositoryImpl.java deleted file mode 100644 index 7c99f5a8..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/repository/scrap/redis/RedisScrapRepositoryImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.scrap.redis; - -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; -import darkoverload.itzip.feature.techinfo.util.RedisKeyUtil; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Redis 를 사용하여 스크랩 상태를 관리하는 레포지토리 구현 클래스. - */ -@Repository -@RequiredArgsConstructor -public class RedisScrapRepositoryImpl implements RedisScrapRepository { - - private final RedisTemplate redisTemplate; - - /** - * 사용자의 포스트 스크랩 상태를 Redis 에 저장합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @param isScrapped 스크랩 상태 - * @param ttl 데이터 유효 시간(초) - */ - @Override - public void save(Long userId, String postId, boolean isScrapped, long ttl) { - String redisKey = RedisKeyUtil.buildRedisKey(userId, postId, "scrap"); - redisTemplate.opsForValue().set(redisKey, String.valueOf(isScrapped), ttl, TimeUnit.SECONDS); - } - - /** - * 사용자의 특정 포스트에 대한 스크랩 상태를 조회합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 스크랩 상태 (Boolean), 없으면 null - */ - @Override - public Boolean getScrapStatus(Long userId, String postId) { - String redisKey = RedisKeyUtil.buildRedisKey(userId, postId, "scrap"); - String isScrapped = (String) redisTemplate.opsForValue().get(redisKey); - return isScrapped != null ? Boolean.valueOf(isScrapped) : null; - } - - /** - * Redis에 저장된 모든 스크랩 상태를 조회합니다. - * - * @return List - */ - @Override - public List getAllScrapStatuses() { - List scrapStatuses = new ArrayList<>(); - Set keys = redisTemplate.keys("post:*:user:*:scrap"); - - for (String key : keys) { - String scrapStatus = (String) redisTemplate.opsForValue().get(key); - if (scrapStatus != null) { - Boolean isScrapped = Boolean.valueOf(scrapStatus); - String[] parts = key.split(":"); - String postId = parts[1]; - Long userId = Long.valueOf(parts[3]); - scrapStatuses.add(ScrapStatus.from(postId, userId, isScrapped)); - } - } - - return scrapStatuses; - } - - /** - * Redis에 저장된 모든 스크랩 데이터를 삭제합니다. - * 주로 테스트 환경이나 데이터 초기화에 사용됩니다. - */ - @Override - public void deleteAll() { - redisTemplate.delete(redisTemplate.keys("post:*:user:*:scrap")); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogCommandService.java deleted file mode 100644 index bcaa328c..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogCommandService.java +++ /dev/null @@ -1,15 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.blog; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.blog.request.BlogUpdateIntroRequest; -import darkoverload.itzip.feature.user.domain.User; - -public interface BlogCommandService { - - void create(User user); - - void update(CustomUserDetails userDetails, BlogUpdateIntroRequest request); - - void updateStatus(Long blogId, boolean status); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogCommandServiceImpl.java deleted file mode 100644 index 8a02c4bc..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogCommandServiceImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.blog; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.blog.request.BlogUpdateIntroRequest; -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogCommandRepository; -import darkoverload.itzip.feature.user.domain.User; -import darkoverload.itzip.feature.user.entity.UserEntity; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 블로그 명령(생성, 수정) 관련 서비스 구현 클래스. - */ -@Service -@Transactional -@RequiredArgsConstructor -public class BlogCommandServiceImpl implements BlogCommandService { - - private final BlogCommandRepository blogCommandRepository; - private final UserRepository userRepository; - - /** - * 새로운 블로그를 생성합니다. - * - * @param user 블로그를 생성할 사용자 - */ - @Override - public void create(User user) { - blogCommandRepository.save(Blog.from(user)); - } - - /** - * 블로그 소개글을 업데이트합니다. - * - * @param userDetails 인증된 사용자 정보 - * @param request 블로그 소개글 업데이트 요청 - * @throws RestApiException 사용자를 찾을 수 없을 때 발생 - */ - @Override - public void update(CustomUserDetails userDetails, BlogUpdateIntroRequest request) { - Long userId = userRepository.findByEmail(userDetails.getEmail()) - .map(UserEntity::convertToDomain) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER) - ) - .getId(); - - blogCommandRepository.update(userId, request.intro()); - } - - /** - * 블로그의 공개 상태를 업데이트합니다. - * - * @param blogId 블로그 ID - * @param status 새로운 공개 상태 - */ - @Override - public void updateStatus(Long blogId, boolean status) { - blogCommandRepository.update(blogId, status); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogReadService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogReadService.java deleted file mode 100644 index 9c8d39be..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogReadService.java +++ /dev/null @@ -1,24 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.blog; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.domain.blog.BlogDetails; -import darkoverload.itzip.feature.techinfo.domain.blog.BlogPostTimeline; - -import java.time.LocalDateTime; -import java.util.Optional; - -public interface BlogReadService { - - Optional findById(Long id); - - Optional findByUserId(Long id); - - Optional findByNickname(String nickname); - - Blog getById(Long id); - - BlogDetails getBlogDetailByNickname(String nickname); - - BlogPostTimeline getBlogRecentPostsByIdAndCreateDate(Long blogId, LocalDateTime createDate); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogReadServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogReadServiceImpl.java deleted file mode 100644 index 070ccdc7..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/BlogReadServiceImpl.java +++ /dev/null @@ -1,120 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.blog; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.domain.blog.BlogDetails; -import darkoverload.itzip.feature.techinfo.domain.blog.BlogPostTimeline; -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogReadRepository; -import darkoverload.itzip.feature.techinfo.service.post.PostReadService; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -/** - * 블로그 조회 관련 서비스 구현 클래스. - */ -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class BlogReadServiceImpl implements BlogReadService { - - private static final int LIMIT = 4; - - private final BlogReadRepository blogReadRepository; - private final PostReadService postReadService; - - /** - * 블로그 ID로 블로그를 조회합니다. - * - * @param id 블로그 ID - * @return Optional - */ - @Override - public Optional findById(Long id) { - return blogReadRepository.findByBlogId(id); - } - - /** - * 사용자 ID로 블로그를 조회합니다. - * - * @param id 사용자 ID - * @return Optional - */ - @Override - public Optional findByUserId(Long id) { - return blogReadRepository.findByUserId(id); - } - - /** - * 사용자 닉네임으로 블로그를 조회합니다. - * - * @param nickname 사용자 닉네임 - * @return Optional - */ - @Override - public Optional findByNickname(String nickname) { - return blogReadRepository.findByNickname(nickname); - } - - /** - * 블로그 ID로 블로그를 조회하고, 없으면 예외를 발생시킵니다. - * - * @param id 블로그 ID - * @return Blog - * @throws RestApiException 블로그를 찾을 수 없을 때 발생 - */ - @Override - public Blog getById(Long id) { - return this.findById(id).orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_BLOG) - ); - } - - /** - * 사용자 닉네임으로 블로그 상세 정보를 조회합니다. - * - * @param nickname 사용자 닉네임 - * @return 블로그 상세 정보 - * @throws RestApiException 블로그를 찾을 수 없을 때 발생 - */ - @Override - public BlogDetails getBlogDetailByNickname(String nickname) { - Blog blog = this.findByNickname(nickname).orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_BLOG) - ); - - List yearlyPostCounts = postReadService.getYearlyPostStatsByBlogId(blog.getId()); - - return BlogDetails.from(blog, yearlyPostCounts); - } - - /** - * 블로그 ID와 생성 날짜를 기준으로 최근 포스트 타임라인을 조회합니다. - * - * @param blogId 블로그 ID - * @param createDate 기준 날짜 - * @return 블로그 게시글 타임라인 - * @throws RestApiException 블로그를 찾을 수 없을 때 발생 - */ - @Override - public BlogPostTimeline getBlogRecentPostsByIdAndCreateDate(Long blogId, LocalDateTime createDate) { - String nickname = this.findById(blogId) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_BLOG) - ) - .getUser() - .getNickname(); - - List posts = postReadService.getPostsByDateRange(blogId, createDate, LIMIT); - - return BlogPostTimeline.from(nickname, posts); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/port/BlogCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/port/BlogCommandRepository.java deleted file mode 100644 index 51be7b57..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/port/BlogCommandRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.blog.port; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; - -public interface BlogCommandRepository { - - Blog save(Blog blog); - - Blog update(Long userId, String newIntro); - - Blog update(Long blogId, boolean status); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/port/BlogReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/port/BlogReadRepository.java deleted file mode 100644 index e5c63cda..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/blog/port/BlogReadRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.blog.port; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; - -import java.util.Optional; - -public interface BlogReadRepository { - - Optional findByBlogId(Long id); - - Optional findByUserId(Long id); - - Optional findByNickname(String nickname); - - Blog getById(Long id); - - Blog getByUserId(Long id); - - Blog getByNickname(String nickname); - - Blog getReferenceById(Long id); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentCommandService.java deleted file mode 100644 index 9029cc31..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentCommandService.java +++ /dev/null @@ -1,15 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.comment; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentCreateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentUpdateRequest; - -public interface CommentCommandService { - - void create(CustomUserDetails userDetails, PostCommentCreateRequest request); - - void update(CustomUserDetails userDetails, PostCommentUpdateRequest request); - - void updateVisibility(CustomUserDetails userDetails, String commentId, boolean status); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentCommandServiceImpl.java deleted file mode 100644 index 221122f7..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentCommandServiceImpl.java +++ /dev/null @@ -1,86 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.comment; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentCreateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentUpdateRequest; -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.feature.techinfo.service.comment.port.CommentCommandRepository; -import darkoverload.itzip.feature.techinfo.service.post.PostReadService; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Service; - -/** - * 댓글 명령(생성, 수정, 삭제) 관련 서비스 구현 클래스. - */ -@Service -@RequiredArgsConstructor -public class CommentCommandServiceImpl implements CommentCommandService { - - private final CommentCommandRepository commentCommandRepository; - private final UserRepository userRepository; - - private final PostReadService postReadService; - - /** - * 새로운 댓글을 생성합니다. - * - * @param userDetails 인증된 사용자 정보 - * @param request 댓글 생성 요청 - * @throws RestApiException 사용자나 포스트를 찾을 수 없을 때 발생 - */ - @Override - public void create(CustomUserDetails userDetails, PostCommentCreateRequest request) { - Long userId = getUserId(userDetails); - - if (!postReadService.existsById(new ObjectId(request.postId()))) { - throw new RestApiException(CommonExceptionCode.NOT_FOUND_POST); - } - - Comment comment = Comment.from(request, userId); - commentCommandRepository.save(comment); - } - - /** - * 기존 댓글을 수정합니다. - * - * @param userDetails 인증된 사용자 정보 - * @param request 댓글 수정 요청 - * @throws RestApiException 사용자를 찾을 수 없을 때 발생 - */ - @Override - public void update(CustomUserDetails userDetails, PostCommentUpdateRequest request) { - Long userId = getUserId(userDetails); - commentCommandRepository.update(new ObjectId(request.commentId()), userId, request.content()); - } - - /** - * 댓글의 공개 상태를 변경합니다. - * - * @param userDetails 인증된 사용자 정보 - * @param commentId 댓글 ID - * @param status 새로운 공개 상태 - * @throws RestApiException 사용자를 찾을 수 없을 때 발생 - */ - public void updateVisibility(CustomUserDetails userDetails, String commentId, boolean status) { - Long userId = getUserId(userDetails); - commentCommandRepository.update(new ObjectId(commentId), userId, status); - } - - /** - * 사용자 이메일로 사용자 ID를 조회합니다. - * - * @param userDetails 인증된 사용자 정보 - * @return 사용자 ID - * @throws RestApiException 사용자를 찾을 수 없을 때 발생 - */ - private Long getUserId(CustomUserDetails userDetails) { - return userRepository.findByEmail(userDetails.getEmail()) - .orElseThrow(() -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER)) - .getId(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentReadService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentReadService.java deleted file mode 100644 index 2a73fea1..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentReadService.java +++ /dev/null @@ -1,14 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.comment; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.feature.techinfo.domain.comment.CommentDetails; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface CommentReadService { - - Page findCommentsByPostId(String postId, Pageable pageable); - - Page getCommentsByPostId(String postId, int page, int size); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentReadServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentReadServiceImpl.java deleted file mode 100644 index 2b0cea5f..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/CommentReadServiceImpl.java +++ /dev/null @@ -1,71 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.comment; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.feature.techinfo.domain.comment.CommentDetails; -import darkoverload.itzip.feature.techinfo.service.comment.port.CommentReadRepository; -import darkoverload.itzip.feature.techinfo.type.SortType; -import darkoverload.itzip.feature.techinfo.util.SortUtil; -import darkoverload.itzip.feature.user.domain.User; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -/** - * 댓글 조회 관련 서비스 구현 클래스. - */ -@Service -@RequiredArgsConstructor -public class CommentReadServiceImpl implements CommentReadService { - - private final CommentReadRepository commentReadRepository; - private final UserRepository userRepository; - - /** - * 포스트 ID로 댓글을 조회합니다. - * - * @param postId 포스트 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findCommentsByPostId(String postId, Pageable pageable) { - return commentReadRepository.findCommentsByPostId(new ObjectId(postId), pageable); - } - - /** - * 포스트 ID로 댓글 상세 정보를 조회합니다. - * - * @param postId 포스트 ID - * @param page 페이지 번호 - * @param size 페이지 크기 - * @return Page - * @throws RestApiException 댓글이나 사용자를 찾을 수 없을 때 발생 - */ - @Override - public Page getCommentsByPostId(String postId, int page, int size) { - Pageable pageable = PageRequest.of(page, size, SortUtil.getType(SortType.NEWEST)); - - Page comments = findCommentsByPostId(postId, pageable); - - if (comments.isEmpty()) { - throw new RestApiException(CommonExceptionCode.NOT_FOUND_COMMENT_IN_POST); - } - - return comments.map(post -> { - User user = userRepository.findById(post.getUserId()) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER) - ) - .convertToDomain(); - - return CommentDetails.from(post, user); - }); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/port/CommentCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/port/CommentCommandRepository.java deleted file mode 100644 index af000c97..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/port/CommentCommandRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.comment.port; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import org.bson.types.ObjectId; - -import java.util.List; - -public interface CommentCommandRepository { - - Comment save(Comment comment); - - List saveAll(List comments); - - Comment update(ObjectId commentId, Long userId, String content); - - Comment update(ObjectId commentId, Long userId, boolean status); - - void deleteAll(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/port/CommentReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/port/CommentReadRepository.java deleted file mode 100644 index 1f930698..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/comment/port/CommentReadRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.comment.port; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface CommentReadRepository { - - Page findCommentsByPostId(Object id, Pageable pageable); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeService.java deleted file mode 100644 index ced802ca..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeService.java +++ /dev/null @@ -1,11 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.like; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; - -public interface LikeService { - - boolean toggleLike(CustomUserDetails userDetails, String postId); - - boolean isLiked(Long userId, String postId); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeServiceImpl.java deleted file mode 100644 index 56bfe89e..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeServiceImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.like; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeCacheRepository; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeRepository; -import darkoverload.itzip.feature.user.entity.UserEntity; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 좋아요 관련 서비스 구현 클래스. - */ -@Service -@RequiredArgsConstructor -public class LikeServiceImpl implements LikeService { - - private final LikeRepository likeRepository; - private final LikeCacheRepository likeCacheRepository; - private final UserRepository userRepository; - - /** - * 사용자의 좋아요 상태를 토글합니다. - * - * @param userDetails 인증된 사용자 정보 - * @param postId 포스트 ID - * @return 토글 후 좋아요 상태 - * @throws RestApiException 사용자를 찾을 수 없을 때 발생 - */ - @Override - @Transactional(readOnly = true) - public boolean toggleLike(CustomUserDetails userDetails, String postId) { - Long userId = userRepository.findByEmail(userDetails.getEmail()) - .map(UserEntity::convertToDomain) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER) - ) - .getId(); - - boolean isLiked = isLiked(userId, postId); - - likeCacheRepository.save(userId, postId, !isLiked, 90); - - return !isLiked; - } - - /** - * 사용자의 특정 포스트에 대한 좋아요 상태를 확인합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 좋아요 상태 - */ - @Override - public boolean isLiked(Long userId, String postId) { - Boolean isLiked = likeCacheRepository.getLikeStatus(userId, postId); - - if (isLiked == null) { - isLiked = likeRepository.existsByUserIdAndPostId(userId, new ObjectId(postId)); - likeCacheRepository.save(userId, postId, isLiked, 90); - } - - return isLiked; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeSyncService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeSyncService.java deleted file mode 100644 index b71c0fc9..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeSyncService.java +++ /dev/null @@ -1,7 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.like; - -public interface LikeSyncService { - - void persistLikesToMongo(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeSyncServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeSyncServiceImpl.java deleted file mode 100644 index 724ff3b5..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/LikeSyncServiceImpl.java +++ /dev/null @@ -1,53 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.like; - -import darkoverload.itzip.feature.techinfo.domain.like.Like; -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeCacheRepository; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeRepository; -import darkoverload.itzip.feature.techinfo.service.post.PostCommandService; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import java.util.List; - -/** - * 좋아요 동기화 서비스 구현 클래스. - * 캐시된 좋아요 정보를 MongoDB에 주기적으로 동기화합니다. - */ -@Service -@RequiredArgsConstructor -public class LikeSyncServiceImpl implements LikeSyncService { - - private final LikeRepository likeRepository; - private final LikeCacheRepository likeCacheRepository; - private final PostCommandService postCommandService; - - /** - * 캐시된 좋아요 정보를 MongoDB에 동기화합니다. - * 이 메소드는 설정된 스케줄에 따라 주기적으로 실행됩니다. - */ - @Override - @Scheduled(cron = "${TECHINFO_LIKE_SCHEDULER_CRON}") - public void persistLikesToMongo() { - List cachedLikes = likeCacheRepository.getAllLikeStatuses(); - - for (LikeStatus likeStatus : cachedLikes) { - String postId = likeStatus.getPostId(); - Long userId = likeStatus.getUserId(); - boolean isLiked = likeStatus.getIsLiked(); - boolean exists = likeRepository.existsByUserIdAndPostId(userId, new ObjectId(postId)); - - if (isLiked && !exists) { - Like like = Like.from(likeStatus); - likeRepository.save(like); - postCommandService.updateFieldWithValue(postId, "like_count", 1); - } else if (!isLiked && exists) { - likeRepository.deleteByUserIdAndPostId(userId, new ObjectId(postId)); - postCommandService.updateFieldWithValue(postId, "like_count", -1); - } - } - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/port/LikeCacheRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/like/port/LikeCacheRepository.java deleted file mode 100644 index 0f33d009..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/port/LikeCacheRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.like.port; - -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import java.util.List; - -public interface LikeCacheRepository { - - void save(Long userId, String postId, boolean isLiked, long ttl); - - Boolean getLikeStatus(Long userId, String postId); - - List getAllLikeStatuses(); - - void deleteAll(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/port/LikeRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/like/port/LikeRepository.java deleted file mode 100644 index de779802..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/like/port/LikeRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.like.port; - -import darkoverload.itzip.feature.techinfo.domain.like.Like; -import org.bson.types.ObjectId; - -public interface LikeRepository { - - Like save(Like like); - - boolean existsByUserIdAndPostId(Long userId, ObjectId postId); - - void deleteByUserIdAndPostId(Long userId, ObjectId postId); - - void deleteAll(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostCommandService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostCommandService.java deleted file mode 100644 index f71dd13a..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostCommandService.java +++ /dev/null @@ -1,17 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCreateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostUpdateRequest; - -public interface PostCommandService { - - void create(CustomUserDetails userDetails, PostCreateRequest request); - - void update(PostUpdateRequest request); - - void update(String postId, boolean status); - - void updateFieldWithValue(String postId, String fieldName, int value); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostCommandServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostCommandServiceImpl.java deleted file mode 100644 index 9cfc44f5..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostCommandServiceImpl.java +++ /dev/null @@ -1,92 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCreateRequest; -import darkoverload.itzip.feature.techinfo.controller.post.request.PostUpdateRequest; -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogReadRepository; -import darkoverload.itzip.feature.techinfo.service.post.port.PostCommandRepository; -import darkoverload.itzip.feature.user.entity.UserEntity; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 포스트 명령(생성, 수정) 관련 서비스 구현 클래스. - */ -@Service -@RequiredArgsConstructor -public class PostCommandServiceImpl implements PostCommandService { - - private final BlogReadRepository blogReadRepository; - private final PostCommandRepository postCommandRepository; - private final UserRepository userRepository; - - /** - * 새로운 포스트를 생성합니다. - * - * @param userDetails 인증된 사용자 정보 - * @param request 포스트 생성 요청 - * @throws RestApiException 사용자를 찾을 수 없을 때 발생 - */ - @Override - @Transactional(readOnly = true) - public void create(CustomUserDetails userDetails, PostCreateRequest request) { - Long userId = userRepository.findByEmail(userDetails.getEmail()) - .map(UserEntity::getId) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER) - ); - - Long blogId = blogReadRepository.getByUserId(userId) - .getId(); - - Post post = Post.from(request, blogId); - postCommandRepository.save(post); - } - - /** - * 기존 포스트를 수정합니다. - * - * @param request 포스트 수정 요청 - */ - @Override - public void update(PostUpdateRequest request) { - postCommandRepository.update( - new ObjectId(request.postId()), - new ObjectId(request.categoryId()), - request.title(), - request.content(), - request.thumbnailImagePath(), - request.contentImagePaths() - ); - } - - /** - * 포스트의 공개 상태를 변경합니다. - * - * @param postId 포스트 ID - * @param status 새로운 공개 상태 - */ - @Override - public void update(String postId, boolean status) { - postCommandRepository.update(new ObjectId(postId), status); - } - - /** - * 포스트의 특정 필드 값을 업데이트합니다. - * - * @param postId 포스트 ID - * @param fieldName 업데이트할 필드 이름 - * @param value 새로운 값 - */ - @Override - public void updateFieldWithValue(String postId, String fieldName, int value) { - postCommandRepository.updateFieldWithValue(new ObjectId(postId), fieldName, value); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostReadService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostReadService.java deleted file mode 100644 index 9f306011..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostReadService.java +++ /dev/null @@ -1,45 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.domain.post.PostDetails; -import darkoverload.itzip.feature.techinfo.domain.post.PostInfo; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.techinfo.type.SortType; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -public interface PostReadService { - - Optional findById(ObjectId id); - - Page findAll(Pageable pageable); - - Page findPostsByBlogId(Long blogId, Pageable pageable); - - Page findPostsByCategoryId(String categoryId, Pageable pageable); - - List findPostsByDateRange(Long blogId, LocalDateTime creteDate, int limit); - - List findYearlyPostStatsByBlogId(Long blogId); - - boolean existsById(ObjectId postId); - - PostDetails getPostDetailsById(String postId, CustomUserDetails userDetails); - - Page getPostsByNickname(String nickname, int page, int size, SortType sortType); - - Page getAllOrPostsByCategoryId(String categoryId, int page, int size, SortType sortType); - - List getPostsByDateRange(Long blogId, LocalDateTime creteDate, int limit); - - List getYearlyPostStatsByBlogId(Long blogId); - - long getPostCount(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostReadServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostReadServiceImpl.java deleted file mode 100644 index 8e4224a0..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/PostReadServiceImpl.java +++ /dev/null @@ -1,249 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.post; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.domain.post.PostDetails; -import darkoverload.itzip.feature.techinfo.domain.post.PostInfo; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogReadRepository; -import darkoverload.itzip.feature.techinfo.service.like.LikeService; -import darkoverload.itzip.feature.techinfo.service.post.port.PostReadRepository; -import darkoverload.itzip.feature.techinfo.service.scrap.ScrapService; -import darkoverload.itzip.feature.techinfo.type.SortType; -import darkoverload.itzip.feature.techinfo.util.SortUtil; -import darkoverload.itzip.feature.user.entity.UserEntity; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class PostReadServiceImpl implements PostReadService { - - private final UserRepository userRepository; - private final BlogReadRepository blogReadRepository; - private final PostReadRepository postReadRepository; - - private final PostCommandService postCommandService; - private final ScrapService scrapService; - private final LikeService likeService; - - /** - * ID로 포스트를 조회합니다. - * - * @param id 포스트 ID - * @return Optional - */ - @Override - public Optional findById(ObjectId id) { - return postReadRepository.findById(id); - } - - /** - * 모든 공개 포스트를 페이징하여 조회합니다. - * - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findAll(Pageable pageable) { - return postReadRepository.findAll(pageable); - } - - /** - * 특정 블로그의 공개 포스트를 페이징하여 조회합니다. - * - * @param blogId 블로그 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findPostsByBlogId(Long blogId, Pageable pageable) { - return postReadRepository.findPostsByBlogId(blogId, pageable); - } - - /** - * 특정 카테고리의 공개 포스트를 페이징하여 조회합니다. - * - * @param categoryId 카테고리 ID - * @param pageable 페이징 정보 - * @return Page - */ - @Override - public Page findPostsByCategoryId(String categoryId, Pageable pageable) { - return postReadRepository.findPostsByCategoryId(new ObjectId(categoryId), pageable); - } - - /** - * 특정 날짜 범위의 포스트를 조회합니다. - * - * @param blogId 블로그 ID - * @param creteDate 기준 날짜 - * @param limit 조회할 포스트 수 - * @return List - */ - @Override - public List findPostsByDateRange(Long blogId, LocalDateTime creteDate, int limit) { - return postReadRepository.findPostsByDateRange(blogId, creteDate, limit); - } - - /** - * 특정 블로그의 연간 포스트 통계를 조회합니다. - * - * @param blogId 블로그 ID - * @return List - */ - @Override - public List findYearlyPostStatsByBlogId(Long blogId) { - return postReadRepository.findYearlyPostStatsByBlogId(blogId); - } - - /** - * 특정 ID의 포스트가 존재하는지 확인합니다. - * - * @param postId 포스트 ID - * @return 존재 여부 - */ - @Override - public boolean existsById(ObjectId postId) { - return postReadRepository.existsById(postId); - } - - /** - * 포스트 상세 정보를 조회합니다. - * - * @param postId 포스트 ID - * @param userDetails 인증된 사용자 정보 (nullable) - * @return 포스트 상세 정보 - * @throws RestApiException 사용자 또는 포스트를 찾을 수 없을 때 발생 - */ - @Override - @Transactional(readOnly = true) - public PostDetails getPostDetailsById(String postId, CustomUserDetails userDetails) { - Long loggedInUserId = null; - boolean isLiked = false; - boolean isScraped = false; - - if (userDetails != null) { - loggedInUserId = userRepository.findByEmail(userDetails.getEmail()) - .map(UserEntity::getId) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER) - ); - - isLiked = likeService.isLiked(loggedInUserId, postId); - isScraped = scrapService.isScrapped(loggedInUserId, postId); - } - - Post post = this.findById(new ObjectId(postId)) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_POST) - ); - - postCommandService.updateFieldWithValue(post.getId(), "view_count", 1); - Blog blog = blogReadRepository.getById(post.getBlogId()); - - return PostDetails.from(post, blog.getUser(), isLiked, isScraped); - } - - /** - * 특정 사용자의 포스트를 페이징하여 조회합니다. - * - * @param nickname 사용자 닉네임 - * @param page 페이지 번호 - * @param size 페이지 크기 - * @param sortType 정렬 방식 - * @return Page - */ - @Override - @Transactional(readOnly = true) - public Page getPostsByNickname(String nickname, int page, int size, SortType sortType) { - Pageable pageable = PageRequest.of(page, size, SortUtil.getType(sortType)); - Long blogId = blogReadRepository.getByNickname(nickname).getId(); - return this.findPostsByBlogId(blogId, pageable); - } - - /** - * 모든 포스트 또는 특정 카테고리의 포스트를 페이징하여 조회합니다. - * - * @param categoryId 카테고리 ID (nullable) - * @param page 페이지 번호 - * @param size 페이지 크기 - * @param sortType 정렬 방식 - * @return Page - * @throws RestApiException 포스트를 찾을 수 없을 때 발생 - */ - @Override - @Transactional(readOnly = true) - public Page getAllOrPostsByCategoryId(String categoryId, int page, int size, SortType sortType) { - Pageable pageable = PageRequest.of(page, size, SortUtil.getType(sortType)); - - Page posts = (categoryId != null) ? this.findPostsByCategoryId(categoryId, pageable) : this.findAll(pageable); - - if (posts.isEmpty()) { - throw new RestApiException( - categoryId != null ? CommonExceptionCode.NOT_FOUND_POST_IN_CATEGORY : CommonExceptionCode.NOT_FOUND_POST - ); - } - - return posts.map(post -> { - Blog blog = blogReadRepository.getById(post.getBlogId()); - return PostInfo.from(post, blog.getUser()); - }); - } - - /** - * 특정 날짜 범위의 포스트를 조회합니다. - * - * @param blogId 블로그 ID - * @param creteDate 기준 날짜 - * @param limit 조회할 포스트 수 - * @return List - * @throws RestApiException 포스트를 찾을 수 없을 때 발생 - */ - @Override - public List getPostsByDateRange(Long blogId, LocalDateTime creteDate, int limit) { - List posts = this.findPostsByDateRange(blogId, creteDate, limit); - - if (posts.isEmpty()) { - throw new RestApiException(CommonExceptionCode.NOT_FOUND_POST_IN_BLOG); - } - - return posts; - } - - /** - * 특정 블로그의 연간 포스트 통계를 조회합니다. - * - * @param blogId 블로그 ID - * @return List - * @throws RestApiException 포스트를 찾을 수 없을 때 발생 - */ - @Override - public List getYearlyPostStatsByBlogId(Long blogId) { - return this.findYearlyPostStatsByBlogId(blogId); - } - - /** - * 전체 공개 포스트 수를 조회합니다. - * - * @return 공개 포스트 수 - */ - @Override - public long getPostCount() { - return postReadRepository.getPostCount(); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/port/PostCommandRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/post/port/PostCommandRepository.java deleted file mode 100644 index 693c3d99..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/port/PostCommandRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.post.port; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import org.bson.types.ObjectId; -import java.util.List; - -public interface PostCommandRepository { - - Post save(Post post); - - List saveAll(List posts); - - Post update(ObjectId postId, ObjectId categoryId, String title, String content, String thumbnailImagePath, List contentImagePaths); - - Post update(ObjectId postId, boolean status); - - Post updateFieldWithValue(ObjectId postId, String fieldName, int value); - - void deleteAll(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/port/PostReadRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/post/port/PostReadRepository.java deleted file mode 100644 index 554944c3..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/post/port/PostReadRepository.java +++ /dev/null @@ -1,31 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.post.port; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import org.bson.types.ObjectId; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -public interface PostReadRepository { - - Optional findById(ObjectId postId); - - Page findAll(Pageable pageable); - - Page findPostsByBlogId(Long blogId, Pageable pageable); - - Page findPostsByCategoryId(ObjectId categoryId, Pageable pageable); - - List findPostsByDateRange(Long blogId, LocalDateTime createDate, int limit); - - List findYearlyPostStatsByBlogId(Long blogId); - - long getPostCount(); - - boolean existsById(ObjectId postId); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapService.java deleted file mode 100644 index cc01e348..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapService.java +++ /dev/null @@ -1,11 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.scrap; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; - -public interface ScrapService { - - boolean toggleScrap(CustomUserDetails userDetails, String postId); - - boolean isScrapped(Long userId, String postId); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapServiceImpl.java deleted file mode 100644 index a2fba5a2..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapServiceImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.scrap; - -import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapCacheRepository; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapRepository; -import darkoverload.itzip.feature.user.entity.UserEntity; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * 스크랩 관련 서비스 구현 클래스. - */ -@Service -@RequiredArgsConstructor -public class ScrapServiceImpl implements ScrapService { - - private final ScrapRepository scrapRepository; - private final ScrapCacheRepository scrapCacheRepository; - private final UserRepository userRepository; - - /** - * 사용자의 스크랩 상태를 토글합니다. - * - * @param userDetails 인증된 사용자 정보 - * @param postId 포스트 ID - * @return 토글 후 스크랩 상태 - * @throws RestApiException 사용자를 찾을 수 없을 때 발생 - */ - @Override - @Transactional(readOnly = true) - public boolean toggleScrap(CustomUserDetails userDetails, String postId) { - Long userId = userRepository.findByEmail(userDetails.getEmail()) - .map(UserEntity::convertToDomain) - .orElseThrow( - () -> new RestApiException(CommonExceptionCode.NOT_FOUND_USER) - ) - .getId(); - - boolean isScrapped = isScrapped(userId, postId); - - scrapCacheRepository.save(userId, postId, !isScrapped, 90); - - return !isScrapped; - } - - /** - * 사용자의 특정 포스트에 대한 스크랩 상태를 확인합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @return 스크랩 상태 - */ - @Override - public boolean isScrapped(Long userId, String postId) { - Boolean isScrapped = scrapCacheRepository.getScrapStatus(userId, postId); - - if (isScrapped == null) { - isScrapped = scrapRepository.existsByUserIdAndPostId(userId, new ObjectId(postId)); - scrapCacheRepository.save(userId, postId, isScrapped, 90); - } - - return isScrapped; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapSyncService.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapSyncService.java deleted file mode 100644 index fb6e40b5..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapSyncService.java +++ /dev/null @@ -1,7 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.scrap; - -public interface ScrapSyncService { - - void persistScrapsToMongo(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapSyncServiceImpl.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapSyncServiceImpl.java deleted file mode 100644 index 366c6485..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/ScrapSyncServiceImpl.java +++ /dev/null @@ -1,49 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.scrap; - -import darkoverload.itzip.feature.techinfo.domain.scrap.Scrap; -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapCacheRepository; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapRepository; -import lombok.RequiredArgsConstructor; -import org.bson.types.ObjectId; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import java.util.List; - -/** - * 스크랩 동기화 서비스 구현 클래스. - * 캐시된 스크랩 정보를 MongoDB에 주기적으로 동기화합니다. - */ -@Service -@RequiredArgsConstructor -public class ScrapSyncServiceImpl implements ScrapSyncService { - - private final ScrapCacheRepository scrapCacheRepository; - private final ScrapRepository scrapRepository; - - /** - * 캐시된 스크랩 정보를 MongoDB에 동기화합니다. - * 이 메소드는 설정된 스케줄에 따라 주기적으로 실행됩니다. - */ - @Override - @Scheduled(cron = "${TECHINFO_SCRAP_SCHEDULER_CRON}") - public void persistScrapsToMongo() { - List cachedScraps = scrapCacheRepository.getAllScrapStatuses(); - - for (ScrapStatus scrapStatus : cachedScraps) { - ObjectId postId = new ObjectId(scrapStatus.getPostId()); - Long userId = scrapStatus.getUserId(); - boolean isScrapped = scrapStatus.getIsScrapped(); - boolean exists = scrapRepository.existsByUserIdAndPostId(userId, postId); - - if (isScrapped && !exists) { - Scrap scrap = Scrap.from(scrapStatus); - scrapRepository.save(scrap); - } else if (!isScrapped && exists) { - scrapRepository.deleteByUserIdAndPostId(userId, postId); - } - } - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/port/ScrapCacheRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/port/ScrapCacheRepository.java deleted file mode 100644 index 1d682748..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/port/ScrapCacheRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.scrap.port; - -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; - -import java.util.List; - -public interface ScrapCacheRepository { - - void save(Long userId, String postId, boolean isScraped, long ttl); - - Boolean getScrapStatus(Long userId, String postId); - - List getAllScrapStatuses(); - - void deleteAll(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/port/ScrapRepository.java b/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/port/ScrapRepository.java deleted file mode 100644 index 69ba7f29..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/service/scrap/port/ScrapRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package darkoverload.itzip.feature.techinfo.service.scrap.port; - -import darkoverload.itzip.feature.techinfo.domain.scrap.Scrap; -import org.bson.types.ObjectId; - -public interface ScrapRepository { - - Scrap save(Scrap scrap); - - boolean existsByUserIdAndPostId(Long userId, ObjectId postId); - - void deleteByUserIdAndPostId(Long userId, ObjectId postId); - - void deleteAll(); - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/type/SortType.java b/src/main/java/darkoverload/itzip/feature/techinfo/type/SortType.java deleted file mode 100644 index d8165e02..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/type/SortType.java +++ /dev/null @@ -1,33 +0,0 @@ -package darkoverload.itzip.feature.techinfo.type; - -/** - * 정렬 유형을 나타내는 열거형. - * 포스트나 기타 항목들을 정렬할 때 사용되는 기준을 정의합니다. - */ -public enum SortType { - - /** - * 최신순 정렬. - * 가장 최근에 생성된 항목부터 정렬합니다. - */ - NEWEST, - - /** - * 오래된순 정렬. - * 가장 오래전에 생성된 항목부터 정렬합니다. - */ - OLDEST, - - /** - * 조회수순 정렬. - * 조회수가 높은 항목부터 정렬합니다. - */ - VIEWCOUNT, - - /** - * 좋아요순 정렬. - * 좋아요 수가 많은 항목부터 정렬합니다. - */ - LIKECOUNT - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/ArticleController.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/ArticleController.java new file mode 100644 index 00000000..92c82a83 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/ArticleController.java @@ -0,0 +1,248 @@ +package darkoverload.itzip.feature.techinfo.ui.controller; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.generator.PagedModelGenerator; +import darkoverload.itzip.feature.techinfo.ui.payload.response.ArticleResponse; +import darkoverload.itzip.feature.techinfo.application.service.command.ArticleCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.ArticleQueryService; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl.YearlyArticleStatistics; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleRegistrationRequest; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.code.CommonResponseCode; +import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; +import darkoverload.itzip.global.config.swagger.ResponseCodeAnnotation; +import darkoverload.itzip.global.config.swagger.SwaggerRequestBody; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.time.LocalDateTime; +import java.util.List; + +@Tag( + name = "Tech Info Article", + description = "아티클 조회, 등록, 변경, 취소 기능을 제공하는 API" +) +@RestController +@RequiredArgsConstructor +@RequestMapping("/tech-info") +public class ArticleController { + + private final ArticleCommandService commandService; + private final ArticleQueryService queryService; + + @Operation( + summary = "아티클 상세 조회", + description = "특정 아티클의 상세 정보를 조회합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @ExceptionCodeAnnotations(CommonExceptionCode.ARTICLE_NOT_FOUND) + @GetMapping("/article/{id}") + public ResponseEntity getArticleById( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @Parameter( + description = "조회할 아티클의 고유 식별자", + required = true, + example = "67d2b940d88d2b9236a1faff" + ) + @NotBlank(message = "아티클 ID는 필수 입니다.") + @PathVariable("id") final String id + ) { + final ArticleResponse response = queryService.getArticleById(userDetails, id); + return ResponseEntity.ok(response); + } + + @Operation( + summary = "카테고리 타입별 아티클 미리보기 목록 조회", + description = "카테고리별 필터링 및 정렬된 아티클 미리보기 목록을 페이징하여 반환합니다." + ) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @ExceptionCodeAnnotations(CommonExceptionCode.ARTICLE_NOT_FOUND) + @GetMapping("/articles/preview") + public ResponseEntity>> getArticlePreviews( + @Parameter( + description = "필터링할 아티클 타입 (예: other, tech_ai 등)" + ) + @RequestParam(name = "article_type", required = false) final String articleType, + @RequestParam(defaultValue = "0") final int page, + @RequestParam(defaultValue = "12") final int size, + @Parameter( + description = "정렬 기준 (예: newest, oldest, view_count, like_count)", + example = "newest" + ) + @RequestParam(name = "sort_type", defaultValue = "newest") final String sortType + ) { + final Page responses = queryService.getArticlesPreviewByType(articleType, page, size, sortType); + final PagedModel> pagedModel = PagedModelGenerator.generate(responses); + return ResponseEntity.ok(pagedModel); + } + + @Operation( + summary = "사용자 닉네임별 아티클 미리보기 목록 조회", + description = "지정된 닉네임으로 정렬된 아티클 미리보기 목록을 페이징하여 반환합니다." + ) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @ExceptionCodeAnnotations(CommonExceptionCode.ARTICLE_NOT_FOUND) + @GetMapping("/author/{nickname}/articles/preview") + public ResponseEntity>> getArticlePreviewsByNickname( + @Parameter( + description = "조회할 사용자의 닉네임", + required = true, + example = "빛나는 471번째 곰" + ) + @PathVariable final String nickname, + @RequestParam(defaultValue = "0") final int page, + @RequestParam(defaultValue = "10") final int size, + @Parameter( + description = "정렬 기준 (예: newest, oldest, view_count, like_count)", + example = "newest" + ) + @RequestParam(name = "sort_type", defaultValue = "newest") final String sortType + ) { + final Page responses = queryService.getArticlesPreviewByAuthor(nickname, page, size, sortType); + final PagedModel> pagedModel = PagedModelGenerator.generate(responses); + return ResponseEntity.ok(pagedModel); + } + + @Operation( + summary = "블로그 년간 아티클 통계 조회", + description = "주어진 블로그 ID로 연도별 아티클 통계 정보를 반환합니다." + ) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @GetMapping("/articles/{blog_id}/stats") + public ResponseEntity> getYearlyArticleStatistics( + @Parameter( + description = "통계를 조회할 대상 블로그 ID", + required = true, + example = "75" + ) + @NotNull(message = "블로그 ID는 필수 입니다.") + @PathVariable("blog_id") final long blogId + ) { + final List articleStatistics = queryService.getYearlyArticleStatisticsByBlogId(blogId); + return ResponseEntity.ok(articleStatistics); + } + + @Operation( + summary = "인접한 아티클 조회", + description = "주어진 블로그 ID, 아티클 타입, 등록일 기준으로 이후, 이전 아티클 목록을 반환합니다." + ) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @GetMapping("/articles/adjacent") + public ResponseEntity> getAdjacentArticles( + @Parameter( + description = "인접한 아티클을 조회할 대상 블로그 ID", + required = true, + example = "75" + ) + @NotNull(message = "블로그 ID는 필수 입니다.") + @RequestParam final Long blogId, + @Parameter( + description = "조회할 아티클의 타입", + required = true, + example = "other" + ) + @NotBlank(message = "아티클 타입은 필수 입니다.") + @RequestParam final String articleType, + @Parameter( + description = "조회 기준이 되는 아티클의 등록일", + required = true, + example = "2025-03-04T00:00:00" + ) + @NotBlank(message = "등록일은 필수 입니다.") + @RequestParam final LocalDateTime createdAt + ) { + final List articleResponses = queryService.getAdjacentArticles(blogId, articleType, createdAt); + return ResponseEntity.ok(articleResponses); + } + + @Operation( + summary = "아티클 등록", + description = "새로운 아티클을 등록합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.BLOG_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @PostMapping("/article") + public ResponseEntity register( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @SwaggerRequestBody( + description = "등록할 아티클의 정보를 담은 요청 Payload", + content = @Content(schema = @Schema(implementation = ArticleRegistrationRequest.class)) + ) + @RequestBody final ArticleRegistrationRequest request + ) { + final String response = commandService.create(userDetails, request); + final URI location = URI.create(String.format("/tech-info/article/%s", response)); + return ResponseEntity.created(location).build(); + } + + @Operation( + summary = "아티클 변경", + description = "주어진 요청을 기반으로 아티클을 변경합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.BLOG_NOT_FOUND, + CommonExceptionCode.ARTICLE_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @PutMapping("/article") + public String modify( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @SwaggerRequestBody( + description = "변경할 아티클의 정보를 담은 요청 Payload", + content = @Content(schema = @Schema(implementation = ArticleEditRequest.class)) + ) + @RequestBody final ArticleEditRequest request + ) { + commandService.update(userDetails, request); + return "아티클이 성공적으로 변경되었습니다."; + } + + @Operation( + summary = "아티클 등록 취소", + description = "아티클을 비공개 상태로 전환합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.BLOG_NOT_FOUND, + CommonExceptionCode.ARTICLE_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @DeleteMapping("/article/{id}") + public String cancel( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @Parameter( + description = "취소할 대상 아티클 ID", + required = true, + example = "67d2b940d88d2b9236a1faff" + ) + @NotBlank(message = "아티클 ID는 필수 입니다.") + @PathVariable(name = "id") final String articleId + ) { + commandService.delete(userDetails, articleId); + return "아티클이 성공적으로 삭제되었습니다."; + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/BlogController.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/BlogController.java new file mode 100644 index 00000000..576407b9 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/BlogController.java @@ -0,0 +1,101 @@ +package darkoverload.itzip.feature.techinfo.ui.controller; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.ui.payload.response.BlogResponse; +import darkoverload.itzip.feature.techinfo.application.service.command.BlogCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.BlogQueryService; +import darkoverload.itzip.feature.techinfo.ui.payload.request.blog.BlogIntroEditRequest; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.code.CommonResponseCode; +import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; +import darkoverload.itzip.global.config.swagger.ResponseCodeAnnotation; +import darkoverload.itzip.global.config.swagger.SwaggerRequestBody; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag( + name = "Tech Info Blog", + description = "블로그 조회, 변경을 제공하는 API" +) +@RestController +@RequiredArgsConstructor +@RequestMapping("/tech-info/blog") +public class BlogController { + + private final BlogCommandService commandService; + private final BlogQueryService queryService; + + @Operation( + summary = "블로그 기본 정보 조회", + description = "지정된 블로그 ID로 블로그 소개글과 소유자의 이메일, 닉네임, 프로필 이미지를 반환합니다." + ) + @ExceptionCodeAnnotations(CommonExceptionCode.BLOG_NOT_FOUND) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @GetMapping("/id/{id}") + public ResponseEntity getBlogById( + @Parameter( + description = "조회할 블로그의 고유 식별자", + required = true, + example = "75" + ) + @NotBlank(message = "블로그 ID는 필수 입니다.") + @PathVariable final Long id + ) { + final BlogResponse response = queryService.getBlogResponseById(id); + return ResponseEntity.ok(response); + } + + @Operation( + summary = "블로그 기존 정보 조회", + description = "지정된 닉네임으로 블로그 소개글과 소유자의 이메일, 닉네임, 프로필 이미지를 반환합니다." + ) + @ExceptionCodeAnnotations(CommonExceptionCode.BLOG_NOT_FOUND) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @GetMapping("/nickname/{nickname}") + public ResponseEntity getBlogByNickname( + @Parameter( + description = "조회할 회원의 닉네임", + required = true, + example = "빛나는 471번째 곰" + ) + @NotBlank(message = "회원 닉네임은 필수 입니다.") + @PathVariable final String nickname + ) { + final BlogResponse response = queryService.getBlogResponseByUserNickname(nickname); + return ResponseEntity.ok(response); + } + + @Operation( + summary = "블로그 소개글 변경", + description = "회원의 블로그 소개글을 변경합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.BLOG_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @PatchMapping("/intro") + public ResponseEntity modify( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @SwaggerRequestBody( + description = "변경할 블로그 소개글을 포함한 요청 Payload", + content = @Content(schema = @Schema(implementation = BlogIntroEditRequest.class)) + ) + @RequestBody @Valid final BlogIntroEditRequest request + ) { + commandService.updateIntro(userDetails, request); + return ResponseEntity.ok("소개글이 성공적으로 변경되었습니다."); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/CommentController.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/CommentController.java new file mode 100644 index 00000000..7b6ba135 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/CommentController.java @@ -0,0 +1,140 @@ +package darkoverload.itzip.feature.techinfo.ui.controller; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.generator.PagedModelGenerator; +import darkoverload.itzip.feature.techinfo.ui.payload.response.CommentResponse; +import darkoverload.itzip.feature.techinfo.application.service.command.CommentCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.CommentQueryService; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentRegistrationRequest; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.code.CommonResponseCode; +import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; +import darkoverload.itzip.global.config.swagger.ResponseCodeAnnotation; +import darkoverload.itzip.global.config.swagger.SwaggerRequestBody; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag( + name = "Tech Info Comment", + description = "댓글 조회, 등록, 변경, 취소을 제공하는 API" +) +@RestController +@RequiredArgsConstructor +@RequestMapping("/tech-info") +public class CommentController { + + private final CommentCommandService commandService; + private final CommentQueryService queryService; + + @Operation( + summary = "아티클 댓글 목록 조회", + description = "지정된 아티클 ID에 해당하는 댓글 목록을 반환합니다." + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.COMMENT_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @GetMapping("/{article_id}/comments") + public ResponseEntity>> getCommentsByArticleId( + @Parameter( + description = "댓글을 조회할 아티클의 고유 식별자", + required = true, + example = "67d2b940d88d2b9236a1faff" + ) + @NotBlank(message = "아티클 ID는 필수 입니다.") + @PathVariable(name = "article_id") final String articleId, + @RequestParam(defaultValue = "0") final int page, + @RequestParam(defaultValue = "10") final int size + ) { + final Page response = queryService.getCommentsByArticleId(articleId, page, size); + final PagedModel> pagedModel = PagedModelGenerator.generate(response); + return ResponseEntity.ok(pagedModel); + } + + @Operation( + summary = "댓글 등록", + description = "특정 아티클에 새 댓글을 등록합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.NOT_FOUND_USER, + CommonExceptionCode.ARTICLE_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @PostMapping("/comment") + public ResponseEntity registerComment( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @SwaggerRequestBody( + description = "등록할 댓글 정보를 담은 요청 Payload", + content = @Content(schema = @Schema(implementation = CommentRegistrationRequest.class)) + ) + @RequestBody final CommentRegistrationRequest request + ) { + commandService.create(userDetails, request); + return ResponseEntity.ok("댓글이 성공적으로 등록되었습니다."); + } + + @Operation( + summary = "댓글 변경", + description = "댓글을 변경합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.ARTICLE_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @PatchMapping("/comment") + public ResponseEntity modify( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @SwaggerRequestBody( + description = "변경할 댓글 정보를 담은 요청 Payload", + content = @Content(schema = @Schema(implementation = CommentEditRequest.class)) + ) + @RequestBody final CommentEditRequest request + ) { + commandService.update(userDetails, request); + return ResponseEntity.ok("댓글이 성공적으로 변경되었습니다."); + } + + @Operation( + summary = "댓글 등록 취소", + description = "댓글을 비공개 상태로 전환합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.ARTICLE_NOT_FOUND + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @DeleteMapping("/comment/{id}") + public ResponseEntity cancel( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @Parameter( + description = "취소할 댓글의 고유 식별자", + required = true, + example = "1" + ) + @NotBlank(message = "댓글 ID는 필수 입니다.") + @PathVariable(name = "id") final Long commentId + ) { + commandService.delete(userDetails, commentId); + return ResponseEntity.ok("댓글이 성공적으로 삭제되었습니다."); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/LikeController.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/LikeController.java new file mode 100644 index 00000000..0471ea2e --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/LikeController.java @@ -0,0 +1,85 @@ +package darkoverload.itzip.feature.techinfo.ui.controller; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.service.command.LikeCommandService; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.code.CommonResponseCode; +import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; +import darkoverload.itzip.global.config.swagger.ResponseCodeAnnotation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotBlank; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag( + name = "Tech Info Like", + description = "좋아요 등록, 취소 기능을 제공하는 API" +) +@RestController +@RequestMapping("/tech-info") +public class LikeController { + + private final LikeCommandService commandService; + + public LikeController(final LikeCommandService commandService) { + this.commandService = commandService; + } + + @Operation( + summary = "좋아요 등록", + description = "로그인한 사용자가 특정 아티클에 좋아요를 등록합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.NOT_FOUND_USER, + CommonExceptionCode.ARTICLE_NOT_FOUND, + CommonExceptionCode.ALREADY_LIKED_ARTICLE + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @PostMapping("/like/{article_id}") + public ResponseEntity register( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @Parameter( + description = "좋아요를 등록할 아티클의 고유 식별자", + required = true, + example = "67d2b940d88d2b9236a1faff" + ) + @NotBlank(message = "아티클 ID는 필수 입니다.") + @PathVariable(name = "article_id") final String articleId + ) { + commandService.create(userDetails, articleId); + return ResponseEntity.ok("좋아요가 성공적으로 등록되었습니다."); + } + + @Operation( + summary = "좋아요 취소", + description = "로그인한 사용자가 특정 아티클에 대해 좋아요를 취소합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.NOT_FOUND_USER, + CommonExceptionCode.ARTICLE_NOT_FOUND, + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @DeleteMapping("/like/{article_id}") + public ResponseEntity cancel( + @Parameter(hidden = true) @AuthenticationPrincipal final CustomUserDetails userDetails, + @Parameter( + description = "취소할 좋아요가 등록된 아티클의 고유 식별자", + required = true, + example = "67d2b940d88d2b9236a1faff" + ) + @NotBlank(message = "아티클 ID는 필수 입니다.") + @PathVariable(name = "article_id") final String articleId + ) { + commandService.delete(userDetails, articleId); + return ResponseEntity.ok("좋아요가 성공적으로 취소되었습니다."); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/ScrapController.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/ScrapController.java new file mode 100644 index 00000000..ce95a8e5 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/controller/ScrapController.java @@ -0,0 +1,82 @@ +package darkoverload.itzip.feature.techinfo.ui.controller; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.service.command.ScrapCommandService; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.code.CommonResponseCode; +import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; +import darkoverload.itzip.global.config.swagger.ResponseCodeAnnotation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag( + name = "Tech Info Scrap", + description = "스크랩 등록, 취소 기능을 제공하는 API" +) +@RestController +@RequiredArgsConstructor +@RequestMapping("/tech-info") +public class ScrapController { + + private final ScrapCommandService commandService; + + @Operation( + summary = "스크랩 등록", + description = "로그인한 사용자가 특정 아티클에 대해 스크랩을 등록합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.NOT_FOUND_USER, + CommonExceptionCode.ARTICLE_NOT_FOUND, + CommonExceptionCode.ALREADY_SCRAP_ARTICLE + }) + @ResponseCodeAnnotation(CommonResponseCode.SUCCESS) + @PostMapping("/scrap/{article_id}") + public ResponseEntity register( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, + @Parameter( + description = "좋아요를 등록할 아티클의 고유 식별자", + required = true, + example = "67d2b940d88d2b9236a1faff" + ) + @NotBlank(message = "아티클 ID는 필수 입니다.") + @PathVariable(name = "article_id") final String articleId + ) { + commandService.create(userDetails, articleId); + return ResponseEntity.ok("스크랩이 성공적으로 등록되었습니다."); + } + + @Operation( + summary = "스크랩 취소", + description = "로그인한 사용자가 특정 아티클에 대해 스크랩을 취소합니다.", + security = @SecurityRequirement(name = "bearerAuth") + ) + @ExceptionCodeAnnotations({ + CommonExceptionCode.UNAUTHORIZED, + CommonExceptionCode.NOT_FOUND_USER, + CommonExceptionCode.ARTICLE_NOT_FOUND, + }) + @DeleteMapping("/scrap/{article_id}") + public ResponseEntity cancel( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, + @Parameter( + description = "취소할 좋아요가 등록된 아티클의 고유 식별자", + required = true, + example = "67d2b940d88d2b9236a1faff" + ) + @NotBlank(message = "아티클 ID는 필수 입니다.") + @PathVariable(name = "article_id") final String articleId + ) { + commandService.delete(userDetails, articleId); + return ResponseEntity.ok("스크랩이 성공적으로 취소되었습니다."); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/article/ArticleEditRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/article/ArticleEditRequest.java new file mode 100644 index 00000000..d5b381f2 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/article/ArticleEditRequest.java @@ -0,0 +1,43 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.request.article; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "아티클 변경 요청에 대한 Payload") +public record ArticleEditRequest( + @Schema( + description = "아티클 ID", + example = "66e724e50000000000db4e53" + ) + @NotBlank(message = "아티클 ID는 필수입니다.") + @JsonProperty("article_id") String articleId, + + @Schema( + description = "아티클 유형", + example = "other" + ) + @NotBlank(message = "아티클 유형은 필수입니다.") + String type, + + @Schema( + description = "아티클 제목", + example = "아티클 제목입니다." + ) + @NotBlank(message = "아티클 제목은 필수입니다.") + String title, + + @Schema( + description = "아티클 상세 내용", + example = "여기에 아티클의 상세 내용이 들어갑니다.", + required = false + ) + String content, + + @Schema( + description = "아티클 썸네일 이미지 URI", + example = "http://example.com/image.jpg" + ) + @NotBlank(message = "아티클 썸네일 이미지 URI 필수입니다.") + @JsonProperty("thumbnail_image_uri") String thumbnailImageUri +) { } diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/article/ArticleRegistrationRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/article/ArticleRegistrationRequest.java new file mode 100644 index 00000000..fada8cc4 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/article/ArticleRegistrationRequest.java @@ -0,0 +1,37 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.request.article; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "아티클 등록 요청에 대한 Payload") +public record ArticleRegistrationRequest( + @Schema( + description = "아티클 유형", + example = "other" + ) + @NotBlank(message = "아티클 유형은 필수입니다.") + String type, + + @Schema( + description = "아티클 제목", + example = "아티클 제목입니다." + ) + @NotBlank(message = "아티클 제목은 필수입니다.") + String title, + + @Schema( + description = "아티클 상세 내용", + example = "여기에 아티클의 상세 내용이 들어갑니다.", + required = false + ) + String content, + + @Schema( + description = "아티클 썸네일 이미지 URI", + example = "http://example.com/image.jpg" + ) + @NotBlank(message = "아티클 썸네일 이미지 URI 필수입니다.") + @JsonProperty("thumbnail_image_uri") + String thumbnailImageUri +) { } diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/blog/BlogIntroEditRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/blog/BlogIntroEditRequest.java new file mode 100644 index 00000000..77c105fe --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/blog/BlogIntroEditRequest.java @@ -0,0 +1,14 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.request.blog; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "블로그 소개글 변경 요청에 대한 Payload") +public record BlogIntroEditRequest( + @Schema( + description = "새로운 블로그 소개글", + example = "새로운 블로그 소개글입니다." + ) + @NotBlank(message = "블로그 소개글은 필수입니다.") + String intro +) { } diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/comment/CommentEditRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/comment/CommentEditRequest.java new file mode 100644 index 00000000..feda504a --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/comment/CommentEditRequest.java @@ -0,0 +1,24 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.request.comment; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "댓글 변경 요청에 대한 Payload") +public record CommentEditRequest( + @Schema( + description = "댓글 ID", + example = "999" + ) + @NotNull(message = "댓글 ID는 필수입니다.") + @JsonProperty("comment_id") + Long commentId, + + @Schema( + description = "댓글 내용", + example = "수정된 댓글 내용" + ) + @NotBlank(message = "댓글 내용은 필수입니다.") + String content +) { } diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/comment/CommentRegistrationRequest.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/comment/CommentRegistrationRequest.java new file mode 100644 index 00000000..c3f36706 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/request/comment/CommentRegistrationRequest.java @@ -0,0 +1,22 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.request.comment; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "댓글 등록 요청에 대한 Payload") +public record CommentRegistrationRequest( + @Schema( + description = "아티클 ID", + example = "66e724e50000000000db4e53" + ) + @NotBlank(message = "아티클 ID는 필수입니다.") + @JsonProperty("article_id") + String articleId, + @Schema( + description = "댓글 내용", + example = "댓글 내용입니다." + ) + @NotBlank(message = "댓글 내용은 필수입니다.") + String content +) { } diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/ArticleResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/ArticleResponse.java new file mode 100644 index 00000000..e1200fcf --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/ArticleResponse.java @@ -0,0 +1,113 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.projection.ArticlePreview; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import darkoverload.itzip.feature.techinfo.domain.projection.ArticleSummary; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema(description = "아티클 응답 정보") +@Builder +public record ArticleResponse( + @Schema(description = "작성자", example = "rowing0328") + String author, + + @Schema(description = "프로필 이미지 URI", example = "http://example.com/profile.jpg") + @JsonProperty("profile_image_uri") + String profileImageUri, + + @Schema(description = "아티클 ID", example = "60a7a28b9a06f913c1f1f7b9") + String articleId, + + @Schema(description = "블로그 ID", example = "12345") + long blogId, + + @Schema(description = "아티클 유형", example = "other") + String type, + + @Schema(description = "아티클 제목", example = "오늘의 뉴스") + String title, + + @Schema(description = "아티클 내용", example = "이것은 아티클 내용입니다.") + String content, + + @Schema(description = "썸네일 이미지 URI", example = "http://example.com/thumb.jpg") + @JsonProperty("thumbnail_image_uri") + String thumbnailImageUri, + + @Schema(description = "좋아요 수", example = "100") + @JsonProperty("likes_count") + Long likesCount, + + @Schema(description = "조회 수", example = "1000") + @JsonProperty("view_count") + Long viewCount, + + @Schema(description = "생성 시간", example = "2025-03-13T12:34:56") + @JsonProperty("created_at") + String createdAt, + + @Schema(description = "좋아요 여부", example = "true") + @JsonProperty("is_liked") + Boolean isLiked, + + @Schema(description = "스크랩 여부", example = "false") + @JsonProperty("is_scrapped") + Boolean isScrapped +) { + + public static ArticleResponse from(final Blog blog, final Article article, final boolean isLiked, final boolean isScrapped) { + return ArticleResponse.builder() + .author(blog.getUser().getNickname()) + .profileImageUri(blog.getUser().getImageUrl()) + .articleId(article.getId().toHexString()) + .blogId(article.getBlogId()) + .type(article.getType().name().toLowerCase()) + .title(article.getTitle()) + .content(article.getContent()) + .thumbnailImageUri(article.getThumbnailImageUri()) + .likesCount(article.getLikesCount()) + .viewCount(article.getViewCount()) + .createdAt(article.getCreatedAt().toString()) + .isLiked(isLiked) + .isScrapped(isScrapped) + .build(); + } + + public static ArticleResponse previewFrom(final Blog blog, final ArticlePreview article) { + return ArticleResponse.builder() + .author(blog.getUser().getNickname()) + .profileImageUri(blog.getUser().getImageUrl()) + .articleId(article.getId().toHexString()) + .type(article.getType().name().toLowerCase()) + .title(article.getTitle()) + .content(article.getContent().length() > 300 ? article.getContent().substring(0, 300) : article.getContent()) + .thumbnailImageUri(article.getThumbnailImageUri()) + .likesCount(article.getLikesCount()) + .createdAt(article.getCreatedAt().toString()) + .build(); + } + + public static ArticleResponse previewFrom(final ArticlePreview article) { + return ArticleResponse.builder() + .articleId(article.getId().toHexString()) + .type(article.getType().name().toLowerCase()) + .title(article.getTitle()) + .content(article.getContent().length() > 300 ? article.getContent().substring(0, 300) : article.getContent()) + .thumbnailImageUri(article.getThumbnailImageUri()) + .likesCount(article.getLikesCount()) + .createdAt(article.getCreatedAt().toString()) + .build(); + } + + public static ArticleResponse summaryFrom(final ArticleSummary summary) { + return ArticleResponse.builder() + .articleId(summary.getId().toHexString()) + .title(summary.getTitle()) + .createdAt(summary.getCreatedAt().toString()) + .build(); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/BlogResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/BlogResponse.java new file mode 100644 index 00000000..ce5e8fba --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/BlogResponse.java @@ -0,0 +1,38 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema(description = "블로그 응답 정보") +@Builder +public record BlogResponse( + @Schema(description = "블로그 ID", example = "1") + Long id, + + @Schema(description = "사용자 이메일", example = "rowing0328@example.com") + String email, + + @Schema(description = "사용자 닉네임", example = "rowing0328") + String nickname, + + @Schema(description = "프로필 이미지 URI", example = "http://example.com/profile.jpg") + @JsonProperty("profile_image_uri") + String profileImageUri, + + @Schema(description = "블로그 소개", example = "안녕하세요, 블로그 소개입니다.") + String intro +) { + + public static BlogResponse from(final Blog blog) { + return builder() + .id(blog.getId()) + .email(blog.getUser().getEmail()) + .nickname(blog.getUser().getNickname()) + .profileImageUri(blog.getUser().getImageUrl()) + .intro(blog.getIntro()) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/CommentResponse.java b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/CommentResponse.java new file mode 100644 index 00000000..83873fe3 --- /dev/null +++ b/src/main/java/darkoverload/itzip/feature/techinfo/ui/payload/response/CommentResponse.java @@ -0,0 +1,40 @@ +package darkoverload.itzip.feature.techinfo.ui.payload.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import darkoverload.itzip.feature.techinfo.domain.entity.Comment; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema(description = "댓글 응답 정보") +@Builder +public record CommentResponse( + @Schema(description = "프로필 이미지 URI", example = "http://example.com/profile.jpg") + @JsonProperty("profile_image_uri") + String profileImageUrI, + + @Schema(description = "사용자 닉네임", example = "JohnDoe") + String nickname, + + @Schema(description = "댓글 ID", example = "12345") + @JsonProperty("comment_id") + long commentId, + + @Schema(description = "댓글 내용", example = "이것은 댓글 내용입니다.") + String content, + + @Schema(description = "작성 시간", example = "2025-03-13T12:34:56") + @JsonProperty("create_at") + String createAt +) { + + public static CommentResponse from(final Comment comment) { + return CommentResponse.builder() + .profileImageUrI(comment.getUser().getImageUrl()) + .nickname(comment.getUser().getNickname()) + .commentId(comment.getId()) + .content(comment.getContent()) + .createAt(comment.getCreatedAt().toString()) + .build(); + } + +} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/util/PagedModelUtil.java b/src/main/java/darkoverload/itzip/feature/techinfo/util/PagedModelUtil.java deleted file mode 100644 index 87a45a12..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/util/PagedModelUtil.java +++ /dev/null @@ -1,41 +0,0 @@ -package darkoverload.itzip.feature.techinfo.util; - -import org.springframework.data.domain.Page; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.PagedModel; - -/** - * 페이징된 모델(PagedModel) 생성을 위한 유틸리티 클래스. - */ -public class PagedModelUtil { - - /** - * 유틸리티 클래스의 인스턴스화를 방지하기 위한 private 생성자. - */ - private PagedModelUtil() { - } - - /** - * Page 객체로부터 PagedModel 을 생성합니다. - * - * @param page 변환할 Page 객체 - * @param 페이지 내 요소의 타입 - * @return 생성된 PagedModel 객체 - */ - public static PagedModel> create(Page page) { - PagedModel.PageMetadata pageMetadata = new PagedModel.PageMetadata( - page.getSize(), - page.getNumber(), - page.getTotalElements(), - page.getTotalPages() - ); - - return PagedModel.of( - page.stream() - .map(EntityModel::of) - .toList(), - pageMetadata - ); - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/util/RedisKeyUtil.java b/src/main/java/darkoverload/itzip/feature/techinfo/util/RedisKeyUtil.java deleted file mode 100644 index e43ad3a7..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/util/RedisKeyUtil.java +++ /dev/null @@ -1,26 +0,0 @@ -package darkoverload.itzip.feature.techinfo.util; - -/** - * Redis 키 생성을 위한 유틸리티 클래스. - */ -public class RedisKeyUtil { - - /** - * 유틸리티 클래스의 인스턴스화를 방지하기 위한 private 생성자. - */ - private RedisKeyUtil() { - } - - /** - * 사용자 ID, 포스트 ID, 키 접미사를 조합하여 Redis 키를 생성합니다. - * - * @param userId 사용자 ID - * @param postId 포스트 ID - * @param keySuffix 키 접미사 (예: "like", "scrap" 등) - * @return 생성된 Redis 키 문자열 - */ - public static String buildRedisKey(Long userId, String postId, String keySuffix) { - return "post:" + postId + ":user:" + userId + ":" + keySuffix; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/techinfo/util/SortUtil.java b/src/main/java/darkoverload/itzip/feature/techinfo/util/SortUtil.java deleted file mode 100644 index f9aa23fa..00000000 --- a/src/main/java/darkoverload/itzip/feature/techinfo/util/SortUtil.java +++ /dev/null @@ -1,36 +0,0 @@ -package darkoverload.itzip.feature.techinfo.util; - -import darkoverload.itzip.feature.techinfo.type.SortType; -import org.springframework.data.domain.Sort; - -/** - * 정렬 관련 유틸리티 클래스. - */ -public class SortUtil { - - private static final String VIEW_COUNT_FIELD = "view_count"; - private static final String LIKE_COUNT_FIELD = "like_count"; - private static final String CREATE_DATE_FIELD = "create_date"; - - /** - * 유틸리티 클래스의 인스턴스화를 방지하기 위한 private 생성자. - */ - private SortUtil() { - } - - /** - * 주어진 SortType 에 따라 적절한 Sort 객체를 생성합니다. - * - * @param sortType 정렬 유형 - * @return 생성된 Sort 객체 - */ - public static Sort getType(SortType sortType) { - return switch (sortType) { - case VIEWCOUNT -> Sort.by(Sort.Direction.DESC, VIEW_COUNT_FIELD); - case LIKECOUNT -> Sort.by(Sort.Direction.DESC, LIKE_COUNT_FIELD); - case OLDEST -> Sort.by(Sort.Direction.ASC, CREATE_DATE_FIELD); - default -> Sort.by(Sort.Direction.DESC, CREATE_DATE_FIELD); - }; - } - -} diff --git a/src/main/java/darkoverload/itzip/feature/user/entity/UserEntity.java b/src/main/java/darkoverload/itzip/feature/user/entity/UserEntity.java index bc19d0b2..6972fb2f 100644 --- a/src/main/java/darkoverload/itzip/feature/user/entity/UserEntity.java +++ b/src/main/java/darkoverload/itzip/feature/user/entity/UserEntity.java @@ -5,6 +5,8 @@ import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; + @ToString @Entity @Table(name = "users") @@ -37,6 +39,16 @@ public class UserEntity extends AuditingFields { private String snsType; + public UserEntity(String email, String nickname, String password, String imageUrl, Authority authority, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.email = email; + this.nickname = nickname; + this.password = password; + this.imageUrl = imageUrl; + this.authority = authority; + this.createDate = createdAt; + this.modifyDate = updatedAt; + } + public User convertToDomain(){ return User.builder() .id(this.id) diff --git a/src/main/java/darkoverload/itzip/feature/user/service/OAuthServiceImpl.java b/src/main/java/darkoverload/itzip/feature/user/service/OAuthServiceImpl.java index ee2ce966..dbf9e33a 100644 --- a/src/main/java/darkoverload/itzip/feature/user/service/OAuthServiceImpl.java +++ b/src/main/java/darkoverload/itzip/feature/user/service/OAuthServiceImpl.java @@ -1,6 +1,6 @@ package darkoverload.itzip.feature.user.service; -import darkoverload.itzip.feature.techinfo.service.blog.BlogCommandService; +import darkoverload.itzip.feature.techinfo.application.event.payload.UserCreatedEvent; import darkoverload.itzip.feature.user.controller.request.GithubUserInfo; import darkoverload.itzip.feature.user.controller.request.GithubUserRequest; import darkoverload.itzip.feature.user.controller.request.GoogleUserInfo; @@ -10,6 +10,7 @@ import darkoverload.itzip.global.config.response.code.CommonExceptionCode; import darkoverload.itzip.global.config.response.exception.RestApiException; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -24,7 +25,7 @@ public class OAuthServiceImpl implements OAuthService { private final UserService userService; private final UserRepository userRepository; - private final BlogCommandService blogCommandService; + private final ApplicationEventPublisher eventPublisher; @Override public ResponseEntity google(GoogleUserRequest googleUserRequest) { @@ -130,9 +131,9 @@ private GithubUserInfo fetchGithubUserInfo(GithubUserRequest githubUserRequest) * @param user 회원가입할 유저 */ private ResponseEntity save(User user) { - user.setNickname(userService.getUniqueNickname()); // 닉네임 중복 방지 로직 + user.setNickname(userService.getUniqueNickname()); // 닉네임 중복 방지 로직 User savedUser = userRepository.save(user.convertToEntity()).convertToDomain(); - blogCommandService.create(savedUser); // 블로그 생성 로직 + eventPublisher.publishEvent(new UserCreatedEvent(savedUser.getId())); // 회원 생성 이벤트 발행 return ResponseEntity.ok("회원가입이 완료되었습니다."); } } diff --git a/src/main/java/darkoverload/itzip/feature/user/service/UserServiceImpl.java b/src/main/java/darkoverload/itzip/feature/user/service/UserServiceImpl.java index 38f861f4..b0ea842b 100644 --- a/src/main/java/darkoverload/itzip/feature/user/service/UserServiceImpl.java +++ b/src/main/java/darkoverload/itzip/feature/user/service/UserServiceImpl.java @@ -4,7 +4,7 @@ import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; import darkoverload.itzip.feature.jwt.service.TokenService; import darkoverload.itzip.feature.jwt.util.JwtTokenizer; -import darkoverload.itzip.feature.techinfo.service.blog.BlogCommandService; +import darkoverload.itzip.feature.techinfo.application.event.payload.UserCreatedEvent; import darkoverload.itzip.feature.user.controller.request.*; import darkoverload.itzip.feature.user.controller.response.UserInfoResponse; import darkoverload.itzip.feature.user.controller.response.UserLoginResponse; @@ -21,6 +21,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -40,7 +41,7 @@ public class UserServiceImpl implements UserService { private final VerificationService verificationService; private final EmailService emailService; private final TokenService tokenService; - private final BlogCommandService blogCommandService; + private final ApplicationEventPublisher eventPublisher; private final PasswordEncoder passwordEncoder; private final JwtTokenizer jwtTokenizer; @@ -190,7 +191,7 @@ public String save(UserJoinRequest userJoinRequest) { User savedUser = userRepository.save(user.convertToEntity()).convertToDomain(); - blogCommandService.create(savedUser); + eventPublisher.publishEvent(new UserCreatedEvent(savedUser.getId())); // 회원 생성 이벤트 발행 return "회원가입이 완료되었습니다."; } diff --git a/src/main/java/darkoverload/itzip/global/config/async/AsyncConfig.java b/src/main/java/darkoverload/itzip/global/config/async/AsyncConfig.java new file mode 100644 index 00000000..908c2851 --- /dev/null +++ b/src/main/java/darkoverload/itzip/global/config/async/AsyncConfig.java @@ -0,0 +1,56 @@ +package darkoverload.itzip.global.config.async; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.lang.reflect.Method; +import java.util.concurrent.Executor; + +@EnableAsync +@EnableRetry +@Configuration +public class AsyncConfig implements AsyncConfigurer { + + @Bean(name = "asyncExecutor") + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(20); + executor.setThreadGroup(new ThreadGroup("Async Thread Pool")); + executor.setThreadNamePrefix("Async-Thread-"); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new CustomAsyncExceptionHandler(); + } + +} + +@Slf4j +class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + final Throwable rootCause = ExceptionUtils.getRootCause(ex); + log.error( + "비동기 작업 실패: {}.{}() - {}", + method.getDeclaringClass().getSimpleName(), + method.getName(), + rootCause != null ? rootCause.getMessage() : ex.getMessage(), + ex + ); + } + +} diff --git a/src/main/java/darkoverload/itzip/global/config/cache/caffeine/CacheType.java b/src/main/java/darkoverload/itzip/global/config/cache/caffeine/CacheType.java new file mode 100644 index 00000000..9af67a25 --- /dev/null +++ b/src/main/java/darkoverload/itzip/global/config/cache/caffeine/CacheType.java @@ -0,0 +1,20 @@ +package darkoverload.itzip.global.config.cache.caffeine; + +import lombok.Getter; + +@Getter +public enum CacheType { + + ARTICLE_PREVIEW("articlesPreview", 1800, 2000); + + private final String name; + private final int expiredAfterWrite; + private final int maximumSize; + + CacheType(String name, int expiredAfterWrite, int maximumSize) { + this.name = name; + this.expiredAfterWrite = expiredAfterWrite; + this.maximumSize = maximumSize; + } + +} diff --git a/src/main/java/darkoverload/itzip/global/config/cache/caffeine/CaffeineCacheConfig.java b/src/main/java/darkoverload/itzip/global/config/cache/caffeine/CaffeineCacheConfig.java new file mode 100644 index 00000000..f5514b24 --- /dev/null +++ b/src/main/java/darkoverload/itzip/global/config/cache/caffeine/CaffeineCacheConfig.java @@ -0,0 +1,36 @@ +package darkoverload.itzip.global.config.cache.caffeine; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@EnableCaching +@Configuration +public class CaffeineCacheConfig { + + @Bean + public List caffeineCaches() { + return Arrays.stream(CacheType.values()) + .map(cache -> new CaffeineCache(cache.getName(), Caffeine.newBuilder().recordStats() + .expireAfterWrite(cache.getExpiredAfterWrite(), TimeUnit.SECONDS) + .maximumSize(cache.getMaximumSize()) + .build())) + .toList(); + } + + @Bean(name = "caffeineCacheManager") + public CacheManager caffeineCacheManager(final List caffeineCaches) { + final SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(caffeineCaches); + return cacheManager; + } + +} diff --git a/src/main/java/darkoverload/itzip/global/config/redis/RedisConfig.java b/src/main/java/darkoverload/itzip/global/config/cache/redis/RedisConfig.java similarity index 93% rename from src/main/java/darkoverload/itzip/global/config/redis/RedisConfig.java rename to src/main/java/darkoverload/itzip/global/config/cache/redis/RedisConfig.java index 0f88fb1d..205abcce 100644 --- a/src/main/java/darkoverload/itzip/global/config/redis/RedisConfig.java +++ b/src/main/java/darkoverload/itzip/global/config/cache/redis/RedisConfig.java @@ -1,9 +1,10 @@ -package darkoverload.itzip.global.config.redis; +package darkoverload.itzip.global.config.cache.redis; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -59,7 +60,8 @@ public RedisTemplate redisTemplate(){ } @Bean - public CacheManager cacheManager(RedisConnectionFactory factory) { + @Primary + public CacheManager redisCacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) diff --git a/src/main/java/darkoverload/itzip/global/config/response/code/CommonExceptionCode.java b/src/main/java/darkoverload/itzip/global/config/response/code/CommonExceptionCode.java index 629ece26..05c74d82 100644 --- a/src/main/java/darkoverload/itzip/global/config/response/code/CommonExceptionCode.java +++ b/src/main/java/darkoverload/itzip/global/config/response/code/CommonExceptionCode.java @@ -86,34 +86,38 @@ public enum CommonExceptionCode implements ResponseCode { GITHUB_LOGIN_USER(HttpStatus.BAD_REQUEST, "깃허브 로그인으로 회원가입한 계정은 비밀번호 재설정이 불가합니다."), /** - * TechInfo - Blog Error + * Tech Info - Common */ - // ID에 해당하는 블로그를 찾을 수 없는 경우 - NOT_FOUND_BLOG(HttpStatus.NOT_FOUND, "블로그를 찾을 수 없습니다."), - // 블로그 업데이트 작업이 실패한 경우 - UPDATE_FAIL_BLOG(HttpStatus.BAD_REQUEST, "블로그 업데이트 오류"), + SORT_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "정렬 타입을 찾을 수 없습니다."), /** - * TechInfo - Post Error + * Tech Info - Blog */ - // ID에 해당하는 게시글를 찾을 수 없는 경우 - NOT_FOUND_POST(HttpStatus.NOT_FOUND, "게시글를 찾을 수 없습니다."), - // 블로그에 해당하는 게시글를 찾을 수 없는 경우 - NOT_FOUND_POST_IN_BLOG(HttpStatus.NOT_FOUND, "해당 블로그에 대한 게시물을 찾을 수 없습니다."), - // 카테고리에 해당하는 게시글를 찾을 수 없는 경우 - NOT_FOUND_POST_IN_CATEGORY(HttpStatus.NOT_FOUND, "해당 카테고리에 대한 게시물을 찾을 수 없습니다."), - // 포스트 업데이트 작업이 실패한 경우 - UPDATE_FAIL_POST(HttpStatus.BAD_REQUEST, "게시글 업데이트 오류"), - // 게시글에 해당하는 좋아요 삭제 작업이 실패한 경우 - DELETE_FAIL_LIKE_IN_POST(HttpStatus.BAD_REQUEST, "해당 게시물에 대한 좋아요 삭제 오류"), - // 게시글에 해당하는 스크랩 삭제 작업이 실패한 경우 - DELETE_FAIL_SCRAP_IN_POST(HttpStatus.BAD_REQUEST, "해당 게시물에 대한 스크랩 삭제 오류"), + BLOG_NOT_FOUND(HttpStatus.NOT_FOUND, "블로그를 찾을 수 없습니다."), + BLOG_INTRO_REQUIRED(HttpStatus.BAD_REQUEST, "블로그 소개글은 반드시 입력되어야 합니다."), /** - * TechInfo - Post - Comment Error + * Tech Info - Article */ - NOT_FOUND_COMMENT_IN_POST(HttpStatus.NOT_FOUND, "해당 게시글에 대한 댓글을 찾을 수 없습니다."), - UPDATE_FAIL_COMMENT(HttpStatus.BAD_REQUEST, "댓글 업데이트 오류"), + ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "아티클을 찾을 수 없습니다."), + ARTICLE_TITLE_REQUIRED(HttpStatus.BAD_REQUEST, "아티클 제목은 반드시 입력되어야 합니다."), + ARTICLE_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "아티클 타입을 찾을 수 없습니다."), + + /** + * Tech Info - Comment + */ + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다."), + COMMENT_CONTENT_REQUIRED(HttpStatus.BAD_REQUEST, "댓글 본문은 반드시 입력되어야 합니다."), + + /** + * Tech Info - Like + */ + ALREADY_LIKED_ARTICLE(HttpStatus.CONFLICT, "이미 해당 아티클에 좋아요를 눌렀습니다."), + + /** + * Tech Info - Scrap + */ + ALREADY_SCRAP_ARTICLE(HttpStatus.CONFLICT, "이미 해당 아티클에 스크랩을 눌렀습니다."), /** * Quiz Error diff --git a/src/main/java/darkoverload/itzip/global/config/security/SecurityConfig.java b/src/main/java/darkoverload/itzip/global/config/security/SecurityConfig.java index 1588438d..69ae0fc9 100644 --- a/src/main/java/darkoverload/itzip/global/config/security/SecurityConfig.java +++ b/src/main/java/darkoverload/itzip/global/config/security/SecurityConfig.java @@ -49,19 +49,18 @@ public class SecurityConfig { "/cs-quizzes/**", "/cs-quiz/**", - //알고리즘 임시허용 - "/algorithm/**", - // 기술 정보 임시 허용 "/tech-info/**", // 학교 정보 검색 허용 "/schoolsearch", + // 이력서 전체 검색 허용 "/resume/search", + // 직업 정보 임시 허용 "/job-info", - "/job-info/scrap" + "/job-info/scrap", }; // 비로그인 유저 허용 페이지 diff --git a/src/main/java/darkoverload/itzip/mongo/sample/domain/SampleMongo.java b/src/main/java/darkoverload/itzip/mongo/sample/domain/SampleMongo.java deleted file mode 100644 index 261be101..00000000 --- a/src/main/java/darkoverload/itzip/mongo/sample/domain/SampleMongo.java +++ /dev/null @@ -1,29 +0,0 @@ -package darkoverload.itzip.mongo.sample.domain; - - -import jakarta.persistence.Id; -import lombok.Getter; -import lombok.Setter; -import org.springframework.data.mongodb.core.mapping.Document; - -@Getter -@Setter -@Document(collection = "sample_mongo") -public class SampleMongo { - @Id - private String id; - - private String name; - - public SampleMongo() { - } - - public SampleMongo(String id, String name) { - this.id = id; - this.name = name; - } - - public void changeName(String name) { - this.name=name; - } -} diff --git a/src/main/java/darkoverload/itzip/mongo/sample/repository/SampleMongoRepository.java b/src/main/java/darkoverload/itzip/mongo/sample/repository/SampleMongoRepository.java deleted file mode 100644 index 1d07a75c..00000000 --- a/src/main/java/darkoverload/itzip/mongo/sample/repository/SampleMongoRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package darkoverload.itzip.mongo.sample.repository; - -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import darkoverload.itzip.mongo.sample.domain.SampleMongo; -import org.springframework.data.mongodb.repository.MongoRepository; - -@ExcludeFromJpaRepositories -public interface SampleMongoRepository extends MongoRepository { -} diff --git a/src/main/java/darkoverload/itzip/postgresql/sample/HelloEntity.java b/src/main/java/darkoverload/itzip/postgresql/sample/HelloEntity.java deleted file mode 100644 index 08ff7ee5..00000000 --- a/src/main/java/darkoverload/itzip/postgresql/sample/HelloEntity.java +++ /dev/null @@ -1,15 +0,0 @@ -package darkoverload.itzip.postgresql.sample; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import lombok.Getter; -import lombok.Setter; - -@Entity -@Getter -@Setter -public class HelloEntity { - @Id @GeneratedValue - private Long id; -} diff --git a/src/main/java/darkoverload/itzip/sample/Repository/SampleRedisRepository.java b/src/main/java/darkoverload/itzip/sample/Repository/SampleRedisRepository.java deleted file mode 100644 index f32a643f..00000000 --- a/src/main/java/darkoverload/itzip/sample/Repository/SampleRedisRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package darkoverload.itzip.sample.Repository; - -import darkoverload.itzip.global.config.querydsl.ExcludeFromJpaRepositories; -import darkoverload.itzip.sample.domain.SampleRedis; -import org.springframework.data.repository.CrudRepository; - -@ExcludeFromJpaRepositories -public interface SampleRedisRepository extends CrudRepository { - -} diff --git a/src/main/java/darkoverload/itzip/sample/domain/SampleRedis.java b/src/main/java/darkoverload/itzip/sample/domain/SampleRedis.java deleted file mode 100644 index 9dbda454..00000000 --- a/src/main/java/darkoverload/itzip/sample/domain/SampleRedis.java +++ /dev/null @@ -1,23 +0,0 @@ -package darkoverload.itzip.sample.domain; - -import lombok.Getter; -import org.springframework.data.annotation.Id; -import org.springframework.data.redis.core.RedisHash; - -@Getter -@RedisHash("redisSample") -public class SampleRedis { - @Id - private String id; - - private String name; - - public SampleRedis(String id, String name) { - this.id = id; - this.name = name; - } - - public void changeName(String name) { - this.name = name; - } -} diff --git a/src/main/java/darkoverload/itzip/sample/swagger/SwaggerUIExampleController.java b/src/main/java/darkoverload/itzip/sample/swagger/SwaggerUIExampleController.java deleted file mode 100644 index 7e0f5a33..00000000 --- a/src/main/java/darkoverload/itzip/sample/swagger/SwaggerUIExampleController.java +++ /dev/null @@ -1,166 +0,0 @@ -package darkoverload.itzip.sample.swagger; - -import darkoverload.itzip.global.config.response.code.CommonExceptionCode; -import darkoverload.itzip.global.config.response.code.CommonResponseCode; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import darkoverload.itzip.global.config.swagger.ExceptionCodeAnnotations; -import darkoverload.itzip.global.config.swagger.ResponseCodeAnnotation; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -//스웨거 관련예시 코드 -//자세한 문서는 github-wiki를 참고해주세용 -@RequiredArgsConstructor -@RestController -@RequestMapping("/swagger") -public class SwaggerUIExampleController { - - //아래 4가지는 자동으로 200에러로 처리해준다. - @GetMapping("/") - public String index() { - return "Hello World"; - } - - @GetMapping("/response") - public ResponseEntity index3() { - return ResponseEntity.ok("회원가입이 완료되었습니다."); - } - - @GetMapping(value ="/user/{id}") - public User getUser(@PathVariable Long id) { - User user = new User(); - return user; - } - - @PostMapping(value ="/user2/{id}") - public User postUser2(@PathVariable Long id) { - User user = new User(); - return user; - } - - //맞는 방식 - /* - @ResponseCodeAnnotation(CommonResponseCode.CREATED) 이를 통해서 swagger에 응답 문서 추가 - @ExceptionCodeAnnotations({CommonExceptionCode.BAD_GATEWAY, CommonExceptionCode.BAD_REQUEST}) 이를 통해 2가지 예외 추가 - */ - @GetMapping("/user") - @ResponseCodeAnnotation(CommonResponseCode.CREATED) - @ExceptionCodeAnnotations({CommonExceptionCode.BAD_GATEWAY, CommonExceptionCode.BAD_REQUEST}) - public ResponseEntity getUser() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.CREATED); - } - - //맞는 방식 - /* - @ResponseCodeAnnotation(CommonResponseCode.CREATED) 이를 통해서 swagger에 응답 문서 추가 - @ExceptionCodeAnnotations({CommonExceptionCode.BAD_GATEWAY, CommonExceptionCode.BAD_REQUEST}) 이를 통해 2가지 예외 추가 - throw new RestApiException(CommonExceptionCode.BAD_REQUEST); 를 통해서 예외 처리 - */ - @GetMapping("/user2") - @ResponseCodeAnnotation(CommonResponseCode.CREATED) - @ExceptionCodeAnnotations({CommonExceptionCode.BAD_GATEWAY, CommonExceptionCode.BAD_REQUEST}) - public ResponseEntity getUser2() { - User user = new User(); - if (user == null) { - throw new RestApiException(CommonExceptionCode.BAD_REQUEST); - } - return new ResponseEntity<>(user, HttpStatus.OK); - } - - //맞는 방식 - //@ResponseCodeAnnotation(CommonResponseCode.CREATED) 를통해서 swagger에 표시를 했다. - @GetMapping("/user3") - @ResponseCodeAnnotation(CommonResponseCode.CREATED) - public ResponseEntity getUser3() { - User user = new User(); - return ResponseEntity.status(HttpStatus.CREATED).body(user); - } - - //맞는 방식 - //@ResponseCodeAnnotation(CommonResponseCode.ACCEPTED) 를통해서 swagger에 표시를 했다. - @GetMapping("/user4") - @ResponseCodeAnnotation(CommonResponseCode.ACCEPTED) - public ResponseEntity getUser4() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.ACCEPTED); - } - - /*스웨거 문서에 @ResponseCodeAnnotation을 통해서 아래 내용이 출력되나 실제 응답에는 아무것도 안뜸 - { - "status": "204 NO_CONTENT", - "msg": "요청이 성공적으로 처리되었으나 반환할 내용이 없습니다", - "data": "string", - "code": "NO_CONTENT" - } - */ - @GetMapping("/user5") - @ResponseCodeAnnotation(CommonResponseCode.NO_CONTENT) - public ResponseEntity getUser5() { - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } - - //이렇게 사용하면 안됨 - /*현제 300번대를 설정하지 않았기 때문에 응답 형식이 다음과 같이 날라감 - { - "id": 1, - "name": "John Doe", - "email": "john.doe@gmail.com" - } - */ - @GetMapping("/user6") - public ResponseEntity getUser6() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.FOUND); - } - - //이렇게 사용하면 안됨 에러는 RestApiException을 사용해서 return해야함 - //요청이 404에러로 날라가긴하지만 return형식이 맞지 않음 - @GetMapping("/user7") - public ResponseEntity getUser7() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.NOT_FOUND); - } - - //이렇게 사용하면 안됨 에러는 RestApiException을 사용해서 return해야함 - //요청이 402에러로 날라가긴하지만 return형식이 맞지 않음 - @GetMapping("/user8") - public ResponseEntity getUser8() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.BAD_REQUEST); - } - - //이렇게 사용하면 안됨 에러는 RestApiException을 사용해서 return해야함 - //요청이 403에러로 날라가긴하지만 return형식이 맞지 않음 - @GetMapping("/user9") - public ResponseEntity getUser9() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.FORBIDDEN); - } - - //이렇게 사용하면 안됨 에러는 RestApiException을 사용해서 return해야함 - //요청이 401에러로 날라가긴하지만 return형식이 맞지 않음 - @GetMapping("/user10") - public ResponseEntity getUser10() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.UNAUTHORIZED); - } - - //이렇게 사용하면 안됨 에러는 RestApiException을 사용해서 return해야함 - //요청이 500에러로 날라가긴하지만 return형식이 맞지 않음 - @GetMapping("/user11") - public ResponseEntity getUser11() { - User user = new User(); - return new ResponseEntity<>(user, HttpStatus.INTERNAL_SERVER_ERROR); - } - - @Getter - class User { - Integer id = 2759; - String name = "gnoDgnoD"; - String email = " Aleph@tistory.com"; - } -} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index d267cb62..eb6f20e8 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -22,6 +22,7 @@ spring: database: ${LOC_MONGO_DATABASE} authentication-database: ${LOC_MONGO_AUTH} uri: ${LOC_MONGO_URI} + auto-index-creation: true mail: host: smtp.gmail.com diff --git a/src/test/java/darkoverload/itzip/feature/job/repository/JobInfoJdbcRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/job/repository/JobInfoJdbcRepositoryTest.java index 672fbcde..09400ee3 100644 --- a/src/test/java/darkoverload/itzip/feature/job/repository/JobInfoJdbcRepositoryTest.java +++ b/src/test/java/darkoverload/itzip/feature/job/repository/JobInfoJdbcRepositoryTest.java @@ -1,6 +1,5 @@ package darkoverload.itzip.feature.job.repository; - import darkoverload.itzip.feature.job.domain.job.JobInfo; import darkoverload.itzip.feature.job.mock.JobInfoMockData; import darkoverload.itzip.feature.job.service.connect.port.JobInfoConnectRepository; @@ -30,12 +29,12 @@ public class JobInfoJdbcRepositoryTest { @Autowired private JobInfoJpaRepository jpaRepository; - @Test - void jdbc_리스트_배치_저장_테스트() { - List jobInfos = List.of(JobInfoMockData.jobInfoDataOne, JobInfoMockData.jobInfoDataSecond); - - assertThat(repository.saveAll(jobInfos)).isEqualTo(2); - } +// @Test +// void jdbc_리스트_배치_저장_테스트() { +// List jobInfos = List.of(JobInfoMockData.jobInfoDataOne, JobInfoMockData.jobInfoDataSecond); +// +// assertThat(repository.saveAll(jobInfos)).isEqualTo(2); +// } @Test void jdbc_리스트_배치_업데이트_테스트() { diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ArticleCommandServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ArticleCommandServiceImplTest.java new file mode 100644 index 00000000..1538c027 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ArticleCommandServiceImplTest.java @@ -0,0 +1,225 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.event.payload.ArticleHiddenEvent; +import darkoverload.itzip.feature.techinfo.application.service.command.ArticleCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.BlogQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.article.ArticleRegistrationRequest; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.CustomUserDetailsFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.bson.types.ObjectId; +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; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/default-insert-article-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/default-delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class ArticleCommandServiceImplTest { + + private final ArticleCommandService articleCommandService; + + private final ArticleRepository articleRepository; + + private final ApplicationEventPublisher eventPublisher; + + @Autowired + public ArticleCommandServiceImplTest( + final ArticleRepository articleRepository, + final BlogQueryService blogQueryService, + final ApplicationEventPublisher eventPublisher + ) { + this.articleRepository = articleRepository; + this.eventPublisher = spy(eventPublisher); + this.articleCommandService = new ArticleCommandServiceImpl(articleRepository, blogQueryService, this.eventPublisher); + } + + @BeforeEach + void setUp() { + articleRepository.save(ArticleFixture.getSavedArticle()); + } + + @Test + @DisplayName("아티클을 생성한다.") + void create() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.DEFAULT_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.DEFAULT_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final ArticleRegistrationRequest request = new ArticleRegistrationRequest( + ArticleFixture.DEFAULT_TYPE.name().toLowerCase(), + ArticleFixture.DEFAULT_TITLE, + ArticleFixture.DEFAULT_CONTENT, + ArticleFixture.DEFAULT_THUMBNAIL_URI + ); + + // When + final String response = articleCommandService.create(userDetails, request); + + // Then + final Article result = articleRepository.findById(new ObjectId(response)).get(); + assertAll( + () -> assertThat(result.getId().toHexString()).isEqualTo(response), + () -> assertThat(result.getType().name().toLowerCase()).isEqualTo(request.type()), + () -> assertThat(result.getTitle()).isEqualTo(request.title()), + () -> assertThat(result.getContent()).isEqualTo(request.content()), + () -> assertThat(result.getThumbnailImageUri()).isEqualTo(request.thumbnailImageUri()) + ); + + articleRepository.deleteById(result.getId()); + } + + @Test + @DisplayName("아티클 생성 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void createWithInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final ArticleRegistrationRequest request = new ArticleRegistrationRequest( + ArticleFixture.DEFAULT_TYPE.name().toLowerCase(), + ArticleFixture.DEFAULT_TITLE, + ArticleFixture.DEFAULT_CONTENT, + ArticleFixture.DEFAULT_THUMBNAIL_URI + ); + + // When & Then + assertThatThrownBy(() -> articleCommandService.create(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("아티클을 수정한다.") + void update() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final ArticleEditRequest request = new ArticleEditRequest( + ArticleFixture.DEFAULT_ID.toHexString(), + ArticleFixture.SECOND_TYPE.name().toLowerCase(), + ArticleFixture.DEFAULT_NEW_TITLE, + ArticleFixture.DEFAULT_NEW_CONTENT, + ArticleFixture.DEFAULT_NEW_THUMBNAIL_URI + ); + + // When + articleCommandService.update(userDetails, request); + + // Then + final Article result = articleRepository.findById(ArticleFixture.DEFAULT_ID).get(); + assertAll( + () -> assertThat(result.getId()).isEqualTo(ArticleFixture.DEFAULT_ID), + () -> assertThat(result.getType().name().toLowerCase()).isEqualTo(request.type()), + () -> assertThat(result.getTitle()).isEqualTo(request.title()), + () -> assertThat(result.getContent()).isEqualTo(request.content()), + () -> assertThat(result.getThumbnailImageUri()).isEqualTo(request.thumbnailImageUri()) + ); + + articleRepository.deleteById(result.getId()); + } + + @Test + @DisplayName("아티클 수정 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void updateWithInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final ArticleEditRequest request = new ArticleEditRequest( + ArticleFixture.DEFAULT_ID.toHexString(), + ArticleFixture.SECOND_TYPE.name().toLowerCase(), + ArticleFixture.DEFAULT_NEW_TITLE, + ArticleFixture.DEFAULT_NEW_CONTENT, + ArticleFixture.DEFAULT_NEW_THUMBNAIL_URI + ); + + // When & Then + assertThatThrownBy(() -> articleCommandService.update(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("존재하지 않는 아티클 ID 수정 시 예외가 발생한다.") + void updateWithNonExistentArticleId() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final ArticleEditRequest request = new ArticleEditRequest( + ArticleFixture.NON_EXISTENT_ID.toHexString(), + ArticleFixture.SECOND_TYPE.name().toLowerCase(), + ArticleFixture.DEFAULT_NEW_TITLE, + ArticleFixture.DEFAULT_NEW_CONTENT, + ArticleFixture.DEFAULT_NEW_THUMBNAIL_URI + ); + + // When & Then + assertThatThrownBy(() -> articleCommandService.update(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + @Test + @DisplayName("아티클을 삭제(숨김) 한다.") + void delete() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + + // When + articleCommandService.delete(userDetails, ArticleFixture.DEFAULT_ID.toHexString()); + + // Then + verify(eventPublisher, times(1)).publishEvent(any(ArticleHiddenEvent.class)); + + final Article result = articleRepository.findById(ArticleFixture.DEFAULT_ID).get(); + assertThat(result.getDisplayed()).isFalse(); + + articleRepository.deleteById(result.getId()); + } + + @Test + @DisplayName("아티클 삭제 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void deleteWithInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + + // When & Then + assertThatThrownBy(() -> articleCommandService.delete(userDetails, ArticleFixture.DEFAULT_ID.toHexString())) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("존재하지 않는 아티클 ID 수정 시 예외가 발생한다.") + void deleteWithNonExistentArticleId() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final String nonExistentArticleId = ArticleFixture.NON_EXISTENT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> articleCommandService.delete(userDetails, nonExistentArticleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/BlogCommandServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/BlogCommandServiceImplTest.java new file mode 100644 index 00000000..ea422062 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/BlogCommandServiceImplTest.java @@ -0,0 +1,140 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import darkoverload.itzip.feature.techinfo.domain.repository.BlogRepository; +import darkoverload.itzip.feature.techinfo.ui.payload.request.blog.BlogIntroEditRequest; +import darkoverload.itzip.feature.user.repository.UserRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.BlogFixture; +import darkoverload.itzip.global.fixture.CustomUserDetailsFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/custom/custom-insert-blog-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/custom/custom-delete-blog-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class BlogCommandServiceImplTest { + + @SpyBean + private BlogCommandServiceImpl blogCommandService; + + @SpyBean + private UserRepository userRepository; + + @Autowired + private BlogRepository blogRepository; + + @Test + @DisplayName("블로그를 생성한다.") + void create() { + // Given + final Long savedUserId = UserFixture.SECOND_ID; + + // When + blogCommandService.create(savedUserId); + + // Then + await().atMost(5, TimeUnit.SECONDS) + .until(() -> blogRepository.findByUserId(savedUserId).isPresent()); + + final Blog result = blogRepository.findByUserId(savedUserId).get(); + assertAll( + () -> assertThat(result.getId()).isEqualTo(BlogFixture.SECOND_ID), + () -> assertThat(result.getIntro()).isEqualTo(BlogFixture.DEFAULT_INTRO), + () -> assertThat(result.getUser().getId()).isEqualTo(UserFixture.SECOND_ID), + () -> assertThat(result.getUser().getEmail()).isEqualTo(UserFixture.SECOND_EMAIL), + () -> assertThat(result.getUser().getNickname()).isEqualTo(UserFixture.SECOND_NICKNAME) + ); + } + + @Test + @DisplayName("블로그 생성 시 사용자 정보가 존재하지 않은 경우 예외 발생 시 최대 3회 재시도 후 리커버리를 호출한다.") + void createWithNonExistentUserId() { + // Given + final long nonExistentId = UserFixture.NON_EXISTENT_ID; + + // When & Then + blogCommandService.create(nonExistentId); + + // Then + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + verify(userRepository, times(3)).findById(nonExistentId); + verify(blogCommandService, times(1)).recoverCreate(any(RestApiException.class),eq(nonExistentId)); + }); + } + + @Test + @DisplayName("블로그 소개글을 변경한다.") + void updateIntro() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final BlogIntroEditRequest request = new BlogIntroEditRequest(BlogFixture.DEFAULT_NEW_INTRO); + + // When + blogCommandService.updateIntro(userDetails, request); + + // Then + final Blog result = blogRepository.findById(BlogFixture.DEFAULT_ID).get(); + assertAll( + () -> assertThat(result.getId()).isEqualTo(BlogFixture.DEFAULT_ID), + () -> assertThat(result.getIntro()).isEqualTo(request.intro()), + () -> assertThat(result.getUser().getEmail()).isEqualTo(userDetails.getEmail()), + () -> assertThat(result.getUser().getPassword()).isEqualTo(userDetails.getPassword()), + () -> assertThat(result.getUser().getNickname()).isEqualTo(userDetails.getNickname()), + () -> assertThat(result.getUser().getAuthority()).isEqualTo(userDetails.getAuthorities().stream().findFirst().get()) + ); + } + + @Test + @DisplayName("블로그 소개글 변경 시 사용자 정보가 존재하지 않은 경우 예외가 발생한다.") + void updateIntroWithNonExistentUser() { + // Given + final CustomUserDetails userDetails = null; + final BlogIntroEditRequest request = new BlogIntroEditRequest(BlogFixture.DEFAULT_NEW_INTRO); + + // When & Then + assertThatThrownBy(() -> blogCommandService.updateIntro(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("블로그 소개글 변경 시 요청된 회원의 블로그 정보가 존재하지 않은 경우 예외가 발생한다.") + void updateIntroWithNonExistentBlog() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.NON_EXISTENT_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.NON_EXISTENT_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final BlogIntroEditRequest request = new BlogIntroEditRequest(BlogFixture.DEFAULT_NEW_INTRO); + + // When & Then + assertThatThrownBy(() -> blogCommandService.updateIntro(userDetails, request)) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.BLOG_NOT_FOUND); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/CommentCommandServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/CommentCommandServiceImplTest.java new file mode 100644 index 00000000..66edc779 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/CommentCommandServiceImplTest.java @@ -0,0 +1,227 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.service.command.CommentCommandService; +import darkoverload.itzip.feature.techinfo.domain.entity.Comment; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.domain.repository.CommentRepository; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentEditRequest; +import darkoverload.itzip.feature.techinfo.ui.payload.request.comment.CommentRegistrationRequest; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.CommentFixture; +import darkoverload.itzip.global.fixture.CustomUserDetailsFixture; +import darkoverload.itzip.global.fixture.UserFixture; +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; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/custom/custom-insert-comment-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/custom/custom-delete-comment-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class CommentCommandServiceImplTest { + + @Autowired + private CommentCommandService commentCommandService; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private ArticleRepository articleRepository; + + @BeforeEach + void setUp() { + articleRepository.save(ArticleFixture.getSavedArticle()); + } + + @AfterEach + void tearDown() { + articleRepository.deleteById(ArticleFixture.DEFAULT_ID); + } + + @Test + @DisplayName("댓글을 생성한다.") + void create() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.SECOND_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.SECOND_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final CommentRegistrationRequest request = new CommentRegistrationRequest(ArticleFixture.DEFAULT_ID.toHexString(), CommentFixture.DEFAULT_CONTENT); + + // When + commentCommandService.create(userDetails, request); + + // Then + final Comment result = commentRepository.findByUserId(UserFixture.SECOND_ID).get(); + assertAll( + () -> assertThat(result.getUser().getEmail()).isEqualTo(userDetails.getEmail()), + () -> assertThat(result.getUser().getNickname()).isEqualTo(userDetails.getNickname()), + () -> assertThat(result.getArticleId()).isEqualTo(request.articleId()), + () -> assertThat(result.getContent()).isEqualTo(request.content()), + () -> assertThat(result.getDisplayed()).isTrue() + ); + } + + @Test + @DisplayName("댓글 생성 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void createInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final CommentRegistrationRequest request = new CommentRegistrationRequest(ArticleFixture.DEFAULT_ID.toHexString(), CommentFixture.DEFAULT_CONTENT); + + // When & Then + assertThatThrownBy(() -> commentCommandService.create(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("댓글 생성 시 사용자가 존재하지 않은 경우 예외가 발생한다.") + void createNonExistentUser() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.NON_EXISTENT_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.NON_EXISTENT_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final CommentRegistrationRequest request = new CommentRegistrationRequest(ArticleFixture.DEFAULT_ID.toHexString(), CommentFixture.DEFAULT_CONTENT); + + // When & Then + assertThatThrownBy(() -> commentCommandService.create(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.NOT_FOUND_USER); + } + + @Test + @DisplayName("댓글 생성 시 아티클이 존재하지 않은 경우 예외가 발생한다.") + void createWithNonExistentArticleId() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final CommentRegistrationRequest request = new CommentRegistrationRequest(ArticleFixture.NON_EXISTENT_ID.toHexString(), CommentFixture.DEFAULT_CONTENT); + + // When & Then + assertThatThrownBy(() -> commentCommandService.create(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + @Test + @DisplayName("댓글을 변경한다.") + void update() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final CommentEditRequest request = new CommentEditRequest(CommentFixture.DEFAULT_ID, CommentFixture.DEFAULT_NEW_CONTENT); + + // When + commentCommandService.update(userDetails, request); + + // Then + final Comment result = commentRepository.findById(request.commentId()).get(); + assertThat(result.getContent()).isEqualTo(request.content()); + } + + @Test + @DisplayName("댓글 변경 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void updateInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final CommentEditRequest request = new CommentEditRequest(CommentFixture.DEFAULT_ID, CommentFixture.DEFAULT_NEW_CONTENT); + + // When & Then + assertThatThrownBy(() -> commentCommandService.update(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("댓글 변경 시 요청된 회원의 댓글 정보가 존재하지 않은 경우 예외가 발생한다.") + void updateWithNonExistentCommentId() { + // Given + final long nonExistentCommentId = CommentFixture.NON_EXISTENT_COMMENT_ID; + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final CommentEditRequest request = new CommentEditRequest(nonExistentCommentId, CommentFixture.DEFAULT_NEW_CONTENT); + + // When & Then + assertThatThrownBy(() -> commentCommandService.update(userDetails, request)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.COMMENT_NOT_FOUND); + } + + @Test + @DisplayName("댓글을 삭제(숨김) 한다.") + void delete() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final long commentId = CommentFixture.DEFAULT_ID; + + // When + commentCommandService.delete(userDetails, commentId); + + // Then + final Comment result = commentRepository.findById(commentId).get(); + assertThat(result.getDisplayed()).isFalse(); + } + + @Test + @DisplayName("댓글 삭제(숨김) 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void deleteInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final long commentId = CommentFixture.DEFAULT_ID; + + // When & Then + assertThatThrownBy(() -> commentCommandService.delete(userDetails, commentId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("댓글 삭제(숨김) 시 요청된 회원의 댓글 정보가 존재하지 않은 경우 예외가 발생한다.") + void deleteWithNonExistentCommentId() { + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final long nonExistentCommentId = CommentFixture.NON_EXISTENT_COMMENT_ID; + + // When & Then + assertThatThrownBy(() -> commentCommandService.delete(userDetails, nonExistentCommentId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.COMMENT_NOT_FOUND); + } + + @Test + @DisplayName("아티클 ID와 관련된 댓글을 모두 삭제(숨김) 처리한다.") + void deleteByArticleId() { + // Given + final String articleIdHex = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + commentCommandService.deleteByArticleId(articleIdHex); + + // Then + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + final Comment result = commentRepository.findById(CommentFixture.DEFAULT_ID).get(); + assertThat(result.getDisplayed()).isFalse(); + }); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/LikeCommandServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/LikeCommandServiceImplTest.java new file mode 100644 index 00000000..a051a257 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/LikeCommandServiceImplTest.java @@ -0,0 +1,185 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.event.payload.LikeCancelledEvent; +import darkoverload.itzip.feature.techinfo.application.event.payload.LikedEvent; +import darkoverload.itzip.feature.techinfo.application.service.command.LikeCommandService; +import darkoverload.itzip.feature.techinfo.application.service.query.ArticleQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Like; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.domain.repository.LikeRepository; +import darkoverload.itzip.feature.user.repository.UserRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.CustomUserDetailsFixture; +import darkoverload.itzip.global.fixture.UserFixture; +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; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/custom/custom-insert-like-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/custom/custom-delete-like-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class LikeCommandServiceImplTest { + + private final LikeCommandService likeCommandService; + + private final ArticleRepository articleRepository; + + private final LikeRepository likeRepository; + + private final ApplicationEventPublisher eventPublisher; + + @Autowired + public LikeCommandServiceImplTest( + final ArticleRepository articleRepository, + final LikeRepository likeRepository, + final UserRepository userRepository, + final ArticleQueryService articleQueryService, + final ApplicationEventPublisher applicationEventPublisher + ) { + this.articleRepository = articleRepository; + this.likeRepository = likeRepository; + this.eventPublisher = spy(applicationEventPublisher); + this.likeCommandService = new LikeCommandServiceImpl(likeRepository, userRepository, articleQueryService, this.eventPublisher); + } + + @BeforeEach + void setUp() { + articleRepository.save(ArticleFixture.getSavedArticle()); + } + + @AfterEach + void tearDown() { + articleRepository.deleteById(ArticleFixture.DEFAULT_ID); + } + + @Test + @DisplayName("좋아요를 생성한다.") + void create() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.SECOND_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.SECOND_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + likeCommandService.create(userDetails, articleId); + + // Then + verify(eventPublisher, times(1)).publishEvent(any(LikedEvent.class)); + + final Like result = likeRepository.findByUserId(UserFixture.SECOND_ID).get(); + assertAll( + () -> assertThat(result.getUser().getEmail()).isEqualTo(userDetails.getEmail()), + () -> assertThat(result.getUser().getPassword()).isEqualTo(userDetails.getPassword()), + () -> assertThat(result.getUser().getNickname()).isEqualTo(userDetails.getNickname()), + () -> assertThat(result.getUser().getAuthority()).isEqualTo(userDetails.getAuthorities().stream().findFirst().get()), + () -> assertThat(result.getArticleId()).isEqualTo(articleId) + ); + } + + @Test + @DisplayName("좋아요 생성 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void createWithInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> likeCommandService.create(userDetails, articleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("좋아요 생성 시 사용자가 존재하지 않은 경우 예외가 발생한다.") + void createWithInvalidUser() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.NON_EXISTENT_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.NON_EXISTENT_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> likeCommandService.create(userDetails, articleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.NOT_FOUND_USER); + } + + @Test + @DisplayName("좋아요 생성 시 아티클이 존재하지 않은 경우 예외가 발생한다.") + void createWithInvalidArticleId() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final String invalidArticleId = ArticleFixture.NON_EXISTENT_ID.toHexString(); + + // When + assertThatThrownBy(() -> likeCommandService.create(userDetails, invalidArticleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + @Test + @DisplayName("이미 좋아요가 생성된 아티클에 대해 좋아요 생성 요청 시 예외가 발생한다.") + void createWithAlreadyLikedArticle() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final String alreadyLikedArticleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + assertThatThrownBy(() -> likeCommandService.create(userDetails, alreadyLikedArticleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ALREADY_LIKED_ARTICLE); + } + + @Test + @DisplayName("좋아요를 삭제한다.") + @Transactional + void delete() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + likeCommandService.delete(userDetails, articleId); + + // Then + verify(eventPublisher, times(1)).publishEvent(any(LikeCancelledEvent.class)); + assertThat(likeRepository.existsByUser_NicknameAndArticleId(userDetails.getNickname(), articleId)).isFalse(); + } + + @Test + @DisplayName("좋아요 삭제 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void deleteWithInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> likeCommandService.delete(userDetails, articleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ScrapCommandServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ScrapCommandServiceImplTest.java new file mode 100644 index 00000000..e07f85fe --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/command/impl/ScrapCommandServiceImplTest.java @@ -0,0 +1,163 @@ +package darkoverload.itzip.feature.techinfo.application.service.command.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.service.command.ScrapCommandService; +import darkoverload.itzip.feature.techinfo.domain.entity.Scrap; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.domain.repository.ScrapRepository; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.CustomUserDetailsFixture; +import darkoverload.itzip.global.fixture.UserFixture; +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; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/custom/custom-insert-scrap-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/custom/custom-delete-scrap-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class ScrapCommandServiceImplTest { + + @Autowired + private ScrapCommandService scrapCommandService; + + @Autowired + private ArticleRepository articleRepository; + + @Autowired + private ScrapRepository scrapRepository; + + @BeforeEach + void setUp() { + articleRepository.save(ArticleFixture.getSavedArticle()); + } + + @AfterEach + void tearDown() { + articleRepository.deleteById(ArticleFixture.DEFAULT_ID); + } + + @Test + @DisplayName("스크랩을 생성한다.") + void create() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.SECOND_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.SECOND_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + scrapCommandService.create(userDetails, articleId); + + // Then + final Scrap result = scrapRepository.findByUserId(UserFixture.SECOND_ID).get(); + assertAll( + () -> assertThat(result.getUser().getEmail()).isEqualTo(userDetails.getEmail()), + () -> assertThat(result.getUser().getPassword()).isEqualTo(userDetails.getPassword()), + () -> assertThat(result.getUser().getNickname()).isEqualTo(userDetails.getNickname()), + () -> assertThat(result.getUser().getAuthority()).isEqualTo(userDetails.getAuthorities().stream().findFirst().get()), + () -> assertThat(result.getArticleId()).isEqualTo(articleId) + ); + } + + @Test + @DisplayName("스크랩 생성 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void createWithInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> scrapCommandService.create(userDetails, articleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + + @Test + @DisplayName("스크랩 생성 시 사용자가 존재하지 않은 경우 예외가 발생한다.") + void createWithInvalidUser() { + // Given + final CustomUserDetails userDetails = new CustomUserDetails(UserFixture.NON_EXISTENT_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.NON_EXISTENT_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> scrapCommandService.create(userDetails, articleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.NOT_FOUND_USER); + } + + @Test + @DisplayName("스크랩 생성 시 아티클이 존재하지 않은 경우 예외가 발생한다.") + void createWithInvalidArticleId() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final String invalidArticleId = ArticleFixture.NON_EXISTENT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> scrapCommandService.create(userDetails, invalidArticleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + @Test + @DisplayName("이미 스크랩이 생성된 아티클에 대해 스크랩 생성 요청 시 예외가 발생한다.") + void createWithAlreadyScrappedArticle() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final String alreadyScrappedArticleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> scrapCommandService.create(userDetails, alreadyScrappedArticleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ALREADY_SCRAP_ARTICLE); + } + + @Test + @DisplayName("스크랩을 삭제한다.") + @Transactional + void delete() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + scrapCommandService.delete(userDetails, articleId); + + // Then + assertThat(scrapRepository.existsByUser_NicknameAndArticleId(userDetails.getNickname(), articleId)).isFalse(); + } + + @Test + @DisplayName("스크랩 삭제 시 사용자 정보가 누락일 경우 예외가 발생한다.") + void deleteWithInvalidUserDetails() { + // Given + final CustomUserDetails userDetails = null; + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When & Then + assertThatThrownBy(() -> scrapCommandService.delete(userDetails, articleId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.UNAUTHORIZED); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ArticleQueryServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ArticleQueryServiceImplTest.java new file mode 100644 index 00000000..90fbc069 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ArticleQueryServiceImplTest.java @@ -0,0 +1,272 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; +import darkoverload.itzip.feature.techinfo.application.event.payload.ViewedEvent; +import darkoverload.itzip.feature.techinfo.ui.payload.response.ArticleResponse; +import darkoverload.itzip.feature.techinfo.application.service.query.ArticleQueryService; +import darkoverload.itzip.feature.techinfo.application.service.query.BlogQueryService; +import darkoverload.itzip.feature.techinfo.application.service.query.LikeQueryService; +import darkoverload.itzip.feature.techinfo.application.service.query.ScrapQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.repository.ArticleRepository; +import darkoverload.itzip.feature.techinfo.infrastructure.persistence.custom.impl.YearlyArticleStatistics; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.BlogFixture; +import darkoverload.itzip.global.fixture.CustomUserDetailsFixture; +import darkoverload.itzip.global.fixture.UserFixture; +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; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import java.util.List; +import java.util.concurrent.Executor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/default-insert-article-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/default-delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class ArticleQueryServiceImplTest { + + private final ArticleQueryService articleQueryService; + + private final ArticleRepository articleRepository; + + private final Executor asyncExecutor; + + private final ApplicationEventPublisher eventPublisher; + + @Autowired + public ArticleQueryServiceImplTest( + ArticleRepository articleRepository, + BlogQueryService blogQueryService, + LikeQueryService likeQueryService, + ScrapQueryService scrapQueryService, + @Qualifier("asyncExecutor") Executor asyncExecutor, + ApplicationEventPublisher eventPublisher + ) { + this.articleRepository = articleRepository; + this.asyncExecutor = spy(asyncExecutor); + this.eventPublisher = spy(eventPublisher); + articleQueryService = new ArticleQueryServiceImpl(articleRepository, blogQueryService, likeQueryService, scrapQueryService, this.asyncExecutor, this.eventPublisher); + } + + @BeforeEach + void setUp() { + final Article article = ArticleFixture.getSavedArticle(); + articleRepository.save(article); + } + + @AfterEach + void tearDown() { + articleRepository.deleteById(ArticleFixture.DEFAULT_ID); + } + + @Test + @DisplayName("사용자 정보가 없을 경우, 아티클 조회 시 이벤트 발행은 1회, 비동기 작업은 실행되지 않아야 한다.") + void getArticleByIdWithoutUserDetails() { + // Given + final CustomUserDetails userDetails = null; + + // When + final ArticleResponse result = articleQueryService.getArticleById(userDetails, ArticleFixture.DEFAULT_ID.toHexString()); + + // Then + assertAll( + () -> assertThat(result.author()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.profileImageUri()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.articleId()).isEqualTo(ArticleFixture.DEFAULT_ID.toHexString()), + () -> assertThat(result.blogId()).isEqualTo(BlogFixture.DEFAULT_ID), + () -> assertThat(result.type()).isEqualTo(ArticleFixture.DEFAULT_TYPE.name().toLowerCase()), + () -> assertThat(result.title()).isEqualTo(ArticleFixture.DEFAULT_TITLE), + () -> assertThat(result.content()).isEqualTo(ArticleFixture.DEFAULT_CONTENT), + () -> assertThat(result.thumbnailImageUri()).isEqualTo(ArticleFixture.DEFAULT_THUMBNAIL_URI), + () -> assertThat(result.likesCount()).isEqualTo(ArticleFixture.DEFAULT_LIKES_COUNT), + () -> assertThat(result.viewCount()).isEqualTo(ArticleFixture.DEFAULT_VIEW_COUNT), + () -> assertThat(result.createdAt()).isEqualTo(ArticleFixture.DEFAULT_DATE_TIME.toString()), + () -> assertThat(result.isLiked()).isFalse(), + () -> assertThat(result.isScrapped()).isFalse() + ); + + verify(eventPublisher, times(1)).publishEvent(any(ViewedEvent.class)); + verify(asyncExecutor, times(0)).execute(any(Runnable.class)); + } + + @Test + @DisplayName("사용자 정보가 제공된 경우, 아티클 조회 시 이벤트 발행은 1회, 비동기 작업은 최소 2회 실행되어야 한다.") + void getArticleByIdWithUserDetails() { + // Given + final CustomUserDetails userDetails = CustomUserDetailsFixture.getCustomUserDetails(); + + // When + final ArticleResponse result = articleQueryService.getArticleById(userDetails, ArticleFixture.DEFAULT_ID.toHexString()); + + // Then + assertAll( + () -> assertThat(result.author()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.profileImageUri()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.articleId()).isEqualTo(ArticleFixture.DEFAULT_ID.toHexString()), + () -> assertThat(result.blogId()).isEqualTo(BlogFixture.DEFAULT_ID), + () -> assertThat(result.type()).isEqualTo(ArticleFixture.DEFAULT_TYPE.name().toLowerCase()), + () -> assertThat(result.title()).isEqualTo(ArticleFixture.DEFAULT_TITLE), + () -> assertThat(result.content()).isEqualTo(ArticleFixture.DEFAULT_CONTENT), + () -> assertThat(result.thumbnailImageUri()).isEqualTo(ArticleFixture.DEFAULT_THUMBNAIL_URI), + () -> assertThat(result.likesCount()).isEqualTo(ArticleFixture.DEFAULT_LIKES_COUNT), + () -> assertThat(result.viewCount()).isEqualTo(ArticleFixture.DEFAULT_VIEW_COUNT), + () -> assertThat(result.createdAt()).isEqualTo(ArticleFixture.DEFAULT_DATE_TIME.toString()), + () -> assertThat(result.isLiked()).isTrue(), + () -> assertThat(result.isScrapped()).isTrue() + ); + + verify(eventPublisher, times(1)).publishEvent(any(ViewedEvent.class)); + verify(asyncExecutor, times(2)).execute(any(Runnable.class)); + } + + @Test + @DisplayName("존재하지 않는 아티클 ID 조회 시 예외를 발생한다.") + void getArticleByIdWithInvalidArticleId() { + // Given + final CustomUserDetails userDetails = null; + + // When & Then + assertThatThrownBy(() -> articleQueryService.getArticleById(userDetails, "000000000000000000000000")) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + @Test + @DisplayName("아티클 미리보기를 타입 필터가 누락인 상태로 조회한다") + void getArticlesPreviewByTypeWithInvalidType() { + // When + final Page results = articleQueryService.getArticlesPreviewByType(null, 0, 12, "newest"); + + // Then + assertThat(results.getTotalElements()).isEqualTo(1); + results.forEach(result -> + assertAll( + () -> assertThat(result.author()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.profileImageUri()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.articleId()).isEqualTo(ArticleFixture.DEFAULT_ID.toHexString()), + () -> assertThat(result.type()).isEqualTo(ArticleFixture.DEFAULT_TYPE.name().toLowerCase()), + () -> assertThat(result.title()).isEqualTo(ArticleFixture.DEFAULT_TITLE), + () -> assertThat(result.content()).hasSize(300), + () -> assertThat(result.thumbnailImageUri()).isEqualTo(ArticleFixture.DEFAULT_THUMBNAIL_URI), + () -> assertThat(result.likesCount()).isEqualTo(ArticleFixture.DEFAULT_LIKES_COUNT), + () -> assertThat(result.createdAt()).isEqualTo(ArticleFixture.DEFAULT_DATE_TIME.toString()) + ) + ); + } + + @Test + @DisplayName("아티클 미리보기를 특정 타입으로 조회한다.") + void getArticlesPreviewByTypeWithValidType() { + // When + final Page results = articleQueryService.getArticlesPreviewByType("other", 0, 12, "newest"); + + // Then + assertThat(results.getTotalElements()).isEqualTo(1); + results.forEach(result -> + assertAll( + () -> assertThat(result.author()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.profileImageUri()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.articleId()).isEqualTo(ArticleFixture.DEFAULT_ID.toHexString()), + () -> assertThat(result.type()).isEqualTo(ArticleFixture.DEFAULT_TYPE.name().toLowerCase()), + () -> assertThat(result.title()).isEqualTo(ArticleFixture.DEFAULT_TITLE), + () -> assertThat(result.content()).hasSize(300), + () -> assertThat(result.thumbnailImageUri()).isEqualTo(ArticleFixture.DEFAULT_THUMBNAIL_URI), + () -> assertThat(result.likesCount()).isEqualTo(ArticleFixture.DEFAULT_LIKES_COUNT), + () -> assertThat(result.createdAt()).isEqualTo(ArticleFixture.DEFAULT_DATE_TIME.toString()) + ) + ); + } + + @Test + @DisplayName("아티클 미리보기를 특정 타입으로 조회 시 존재하지 않는 경우 예외를 발생한다.") + void getArticlesPreviewByTypeWithNonExistentArticle() { + // When & Then + assertThatThrownBy(() -> articleQueryService.getArticlesPreviewByType("tech_ai", 0, 12, "newest")) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + @Test + @DisplayName("아티클 미리보기를 닉네임 기반으로 조회한다.") + void getArticlesPreviewByAuthor() { + // Given + final String nickname = UserFixture.DEFAULT_NICKNAME; + + // When + final Page results = articleQueryService.getArticlesPreviewByAuthor(nickname, 0, 12, "newest"); + + // Then + assertThat(results.getTotalElements()).isEqualTo(1); + results.forEach(result -> + assertAll( + () -> assertThat(result.articleId()).isEqualTo(ArticleFixture.DEFAULT_ID.toHexString()), + () -> assertThat(result.type()).isEqualTo(ArticleFixture.DEFAULT_TYPE.name().toLowerCase()), + () -> assertThat(result.title()).isEqualTo(ArticleFixture.DEFAULT_TITLE), + () -> assertThat(result.content()).hasSize(300), + () -> assertThat(result.thumbnailImageUri()).isEqualTo(ArticleFixture.DEFAULT_THUMBNAIL_URI), + () -> assertThat(result.likesCount()).isEqualTo(ArticleFixture.DEFAULT_LIKES_COUNT), + () -> assertThat(result.createdAt()).isEqualTo(ArticleFixture.DEFAULT_DATE_TIME.toString()) + ) + ); + } + + @Test + @DisplayName("아티클 미리보기를 닉네임 기반으로 조회 시 존재하지 않는 경우 예외를 발생한다.") + void getArticlesPreviewByAuthorWithNonExistentArticle() { + // Given + articleRepository.deleteById(ArticleFixture.DEFAULT_ID); + final String nickname = UserFixture.DEFAULT_NICKNAME; + + // When & Then + assertThatThrownBy(() -> articleQueryService.getArticlesPreviewByAuthor(nickname, 0, 12, "newest")) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.ARTICLE_NOT_FOUND); + } + + @Test + @DisplayName("블로그 ID 기반 연도별 아티클 통계를 조회한다.") + void getYearlyArticleStatisticsByBlogId() { + // Given + final long blogId = BlogFixture.DEFAULT_ID; + + // When + final List stats = articleQueryService.getYearlyArticleStatisticsByBlogId(blogId); + + // Then + assertThat(stats.size()).isEqualTo(1); + } + + @Test + @DisplayName("아티클 존재 여부를 조회한다.") + void existsById() { + // When + final boolean exists = articleQueryService.existsById(ArticleFixture.DEFAULT_ID.toHexString()); + + // Then + assertThat(exists).isTrue(); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/BlogQueryServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/BlogQueryServiceImplTest.java new file mode 100644 index 00000000..5f2373cd --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/BlogQueryServiceImplTest.java @@ -0,0 +1,126 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.ui.payload.response.BlogResponse; +import darkoverload.itzip.feature.techinfo.application.service.query.BlogQueryService; +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.BlogFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/default-insert-blog-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/default-delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class BlogQueryServiceImplTest { + + @Autowired + private BlogQueryService blogQueryService; + + @Test + @DisplayName("블로그 ID로 블로그 정보를 반환한다.") + void getBlogResponseById() { + // When + final BlogResponse result = blogQueryService.getBlogResponseById(BlogFixture.DEFAULT_ID); + + // Then + assertAll( + () -> assertThat(result.id()).isEqualTo(BlogFixture.DEFAULT_ID), + () -> assertThat(result.email()).isEqualTo(UserFixture.DEFAULT_EMAIL), + () -> assertThat(result.nickname()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.profileImageUri()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.intro()).isEqualTo(BlogFixture.DEFAULT_INTRO) + ); + } + + @Test + @DisplayName("닉네임으로 블로그 정보를 반환한다.") + void getBlogResponseByUserNickname() { + // When + final BlogResponse result = blogQueryService.getBlogResponseByUserNickname(UserFixture.DEFAULT_NICKNAME); + + // Then + assertAll( + () -> assertThat(result.id()).isEqualTo(BlogFixture.DEFAULT_ID), + () -> assertThat(result.email()).isEqualTo(UserFixture.DEFAULT_EMAIL), + () -> assertThat(result.nickname()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.profileImageUri()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.intro()).isEqualTo(BlogFixture.DEFAULT_INTRO) + ); + } + + @Test + @DisplayName("블로그 ID로 블로그를 조회한다.") + void getBlogById() { + // When + final Blog result = blogQueryService.getBlogById(BlogFixture.DEFAULT_ID); + + // Then + assertAll( + () -> assertThat(result.getId()).isEqualTo(BlogFixture.DEFAULT_ID), + () -> assertThat(result.getUser().getId()).isEqualTo(UserFixture.DEFAULT_ID), + () -> assertThat(result.getUser().getEmail()).isEqualTo(UserFixture.DEFAULT_EMAIL), + () -> assertThat(result.getUser().getNickname()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.getUser().getPassword()).isEqualTo(UserFixture.DEFAULT_PASSWORD), + () -> assertThat(result.getUser().getImageUrl()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.getUser().getAuthority()).isEqualTo(UserFixture.DEFAULT_AUTHORITY), + () -> assertThat(result.getUser().getSnsType()).isEqualTo(UserFixture.DEFAULT_SNS_TYPE), + () -> assertThat(result.getIntro()).isEqualTo(BlogFixture.DEFAULT_INTRO), + () -> assertThat(result.getCreatedAt()).isEqualTo(BlogFixture.DEFAULT_DATE_TIME), + () -> assertThat(result.getUpdatedAt()).isEqualTo(BlogFixture.DEFAULT_DATE_TIME) + ); + } + + @Test + @DisplayName("닉네임으로 블로그 ID를 조회한다.") + void getBlogIdByUserNickname() { + // When + final Long result = blogQueryService.getBlogIdByUserNickname(UserFixture.DEFAULT_NICKNAME); + + // Then + assertThat(result).isEqualTo(BlogFixture.DEFAULT_ID); + } + + @Test + @DisplayName("여러 블로그 ID로 블로그 맵을 조회한다.") + void getBlogMapByIds() { + // Given + final Set blogIds = Set.of(BlogFixture.DEFAULT_ID); + + // When + final Map blogMap = blogQueryService.getBlogMapByIds(blogIds); + + // Then + assertThat(blogMap).containsKey(BlogFixture.DEFAULT_ID); + } + + @Test + @DisplayName("존재하지 않는 블로그 ID 조회 시 예외가 발생한다.") + void getBlogResponseByIdWithNonExistentBlogId() { + // Given + final Long nonExistentBlogId = -1L; + + // When & Then + assertThatThrownBy(() -> blogQueryService.getBlogResponseById(nonExistentBlogId)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.BLOG_NOT_FOUND); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/CommentQueryServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/CommentQueryServiceImplTest.java new file mode 100644 index 00000000..4e5d1037 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/CommentQueryServiceImplTest.java @@ -0,0 +1,63 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.ui.payload.response.CommentResponse; +import darkoverload.itzip.feature.techinfo.application.service.query.CommentQueryService; +import darkoverload.itzip.global.config.response.code.CommonExceptionCode; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.CommentFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/default-insert-comment-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/default-delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class CommentQueryServiceImplTest { + + @Autowired + private CommentQueryService commentQueryService; + + @Test + @DisplayName("특정 아티클 ID에 대한 댓글 목록을 반환한다.") + void getCommentsByArticleId() { + // When + final Page results = commentQueryService.getCommentsByArticleId(ArticleFixture.DEFAULT_ID.toHexString(), 0, 12); + + // Then + assertThat(results.getTotalElements()).isEqualTo(1); + results.forEach(result -> + assertAll( + () -> assertThat(result.profileImageUrI()).isEqualTo(UserFixture.DEFAULT_PROFILE_IMAGE_URI), + () -> assertThat(result.nickname()).isEqualTo(UserFixture.DEFAULT_NICKNAME), + () -> assertThat(result.commentId()).isEqualTo(CommentFixture.DEFAULT_ID), + () -> assertThat(result.content()).isEqualTo(CommentFixture.DEFAULT_CONTENT), + () -> assertThat(result.createAt()).isEqualTo(CommentFixture.DEFAULT_DATE_TIME.toString()) + ) + ); + } + + @Test + @DisplayName("특정 아티클 ID에 댓글이 존재하지 않은 경우 예외가 발생한다.") + void getCommentsByArticleIdWithNonExistentComment() { + // When & Then + assertThatThrownBy(() -> commentQueryService.getCommentsByArticleId("", 0, 12)) + .isInstanceOf(RestApiException.class) + .extracting("exceptionCode") + .isEqualTo(CommonExceptionCode.COMMENT_NOT_FOUND); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/LikeQueryServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/LikeQueryServiceImplTest.java new file mode 100644 index 00000000..1df97807 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/LikeQueryServiceImplTest.java @@ -0,0 +1,52 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.application.service.query.LikeQueryService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import static org.assertj.core.api.Assertions.assertThat; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/default-insert-like-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/default-delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class LikeQueryServiceImplTest { + + @Autowired + private LikeQueryService likeQueryService; + + @ParameterizedTest(name = "회원 닉네임: {0}, 아티클 ID: {1}") + @CsvSource({ + "hyoseung, 67d2b940d88d2b9236a1fb0e" + }) + @DisplayName("좋아요가 존재하는 경우, true를 반환한다.") + void existsByUserNicknameAndArticleId(final String userNickname, final String articleId) { + // When + final boolean result = likeQueryService.existsByUserNicknameAndArticleId(userNickname, articleId); + + // Then + assertThat(result).isEqualTo(true); + } + + @ParameterizedTest(name = "회원 닉네임: {0}, 아티클 ID: {1}") + @CsvSource({ + "rowing, 67d2b940d88d2b9236a1fb0e" + }) + @DisplayName("좋아요가 존재하지 않는 경우, false를 반환한다.") + void existsByUserNicknameAndArticleIdWithNonExistentUserNickname(final String userNickname, final String articleId) { + // When + final boolean result = likeQueryService.existsByUserNicknameAndArticleId(userNickname, articleId); + + // Then + assertThat(result).isEqualTo(false); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ScrapQueryServiceImplTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ScrapQueryServiceImplTest.java new file mode 100644 index 00000000..42ba98e9 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/application/service/query/impl/ScrapQueryServiceImplTest.java @@ -0,0 +1,52 @@ +package darkoverload.itzip.feature.techinfo.application.service.query.impl; + +import darkoverload.itzip.feature.techinfo.application.service.query.ScrapQueryService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; + +import static org.assertj.core.api.Assertions.assertThat; + +@SqlGroup({ + @Sql(scripts = "/sql/techinfo/default-insert-scrap-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "/sql/techinfo/default-delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +@SpringBootTest +@ActiveProfiles("test") +class ScrapQueryServiceImplTest { + + @Autowired + private ScrapQueryService scrrapQueryService; + + @ParameterizedTest(name = "회원 닉네임: {0}, 아티클 ID: {1}") + @CsvSource({ + "hyoseung, 67d2b940d88d2b9236a1fb0e" + }) + @DisplayName("스크랩이 존재하는 경우, true를 반환한다.") + void existsByUserNicknameAndArticleId(final String userNickname, final String articleId) { + // When + final boolean result = scrrapQueryService.existsByUserNicknameAndArticleId(userNickname, articleId); + + // Then + assertThat(result).isEqualTo(true); + } + + @ParameterizedTest(name = "회원 닉네임: {0}, 아티클 ID: {1}") + @CsvSource({ + "rowing, 67d2b940d88d2b9236a1fb0e" + }) + @DisplayName("스크랩이 존재하지 않는 경우, false를 반환한다.") + void existsByUserNicknameAndArticleIdWithNonExistentUserNickname(final String userNickname, final String articleId) { + // When + final boolean result = scrrapQueryService.existsByUserNicknameAndArticleId(userNickname, articleId); + + // Then + assertThat(result).isEqualTo(false); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogDetailsTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogDetailsTest.java deleted file mode 100644 index bfc711d6..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogDetailsTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.blog; - -import static java.util.Collections.EMPTY_LIST; -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.dto.post.MonthlyPostStats; -import darkoverload.itzip.feature.techinfo.dto.post.WeeklyPostStats; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.user.domain.User; -import org.junit.jupiter.api.Test; -import java.util.List; - -class BlogDetailsTest { - - @Test - void 블로그와_통계정보로_상세정보를_생성한다() { - // given - User user = User.builder() - .email("test@test.com") - .nickname("test") - .imageUrl("https://image.url") - .build(); - - Blog blog = Blog.builder() - .id(1L) - .user(user) - .intro("This is a blog intro.") - .isPublic(true) - .build(); - - YearlyPostStats yearlyPostStats = new YearlyPostStats(2024, EMPTY_LIST); - - // when - BlogDetails result = BlogDetails.from(blog, List.of(yearlyPostStats)); - - // then - assertThat(result).isNotNull(); - assertThat(result.getBlogId()).isEqualTo(blog.getId()); - assertThat(result.getProfileImageUrl()).isEqualTo(blog.getUser().getImageUrl()); - assertThat(result.getNickname()).isEqualTo(blog.getUser().getNickname()); - assertThat(result.getEmail()).isEqualTo(blog.getUser().getEmail()); - assertThat(result.getIntro()).isEqualTo(blog.getIntro()); - assertThat(result.getYearlyPostCounts().size()).isEqualTo(1); - } - - @Test - void 연도별_게시글_통계가_생성된다() { - // given - WeeklyPostStats weeklyPostStats = new WeeklyPostStats(1, 1); - MonthlyPostStats monthlyPostStats = new MonthlyPostStats(1, List.of(weeklyPostStats)); - - // when - YearlyPostStats result = new YearlyPostStats(2024, List.of(monthlyPostStats)); - - // then - assertThat(result).isNotNull(); - assertThat(result.getYear()).isEqualTo(2024); - assertThat(result.getMonths().size()).isEqualTo(1); - assertThat(result.getMonths().getFirst().getMonth()).isEqualTo(1); - } - - @Test - void 월별_게시글_통계가_생성된다() { - // given - WeeklyPostStats weeklyPostStats = new WeeklyPostStats(1, 5); - - // when - MonthlyPostStats result = new MonthlyPostStats(2, List.of(weeklyPostStats)); - - // then - assertThat(result).isNotNull(); - assertThat(result.getMonth()).isEqualTo(2); - assertThat(result.getWeeks().size()).isEqualTo(1); - assertThat(result.getWeeks().getFirst().getWeek()).isEqualTo(1); - assertThat(result.getWeeks().getFirst().getPostCount()).isEqualTo(5); - } - - @Test - void 주차와_게시글_수량으로_주별_게시글_통계를_생성한다() { - // given & when - WeeklyPostStats result = new WeeklyPostStats(3, 10); - - // then - assertThat(result).isNotNull(); - assertThat(result.getWeek()).isEqualTo(3); - assertThat(result.getPostCount()).isEqualTo(10); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogPostTimelineTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogPostTimelineTest.java deleted file mode 100644 index 2b36ff56..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogPostTimelineTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.blog; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.mock.PostMockData; -import org.junit.jupiter.api.Test; - -import java.util.List; - -class BlogPostTimelineTest { - - @Test - void 닉네임과_게시글_목록으로_블로그_게시글_타임라인을_생성한다() { - // given - String nickname = "techblogger"; - Post post = PostMockData.postDataOne; - - // when - BlogPostTimeline result = BlogPostTimeline.from(nickname, List.of(post)); - - // then - assertThat(result).isNotNull(); - assertThat(result.getNickname()).isEqualTo(nickname); - assertThat(result.getPosts().size()).isEqualTo(1); - assertThat(result.getPosts().getFirst().getTitle()).isEqualTo(post.getTitle()); - assertThat(result.getPosts().getFirst().getContent()).isEqualTo(post.getContent()); - assertThat(result.getPosts().getFirst().getIsPublic()).isTrue(); - } - - @Test - void 빈_목록으로_타임라인_생성시_빈_목록_반환한다() { - // given - String nickname = "emptyblogger"; - - // when - BlogPostTimeline result = BlogPostTimeline.from(nickname, List.of()); - - // then - assertThat(result).isNotNull(); - assertThat(result.getNickname()).isEqualTo(nickname); - assertThat(result.getPosts()).isEmpty(); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogTest.java deleted file mode 100644 index dc52eaf4..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/blog/BlogTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.blog; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.user.domain.User; -import org.junit.jupiter.api.Test; - -public class BlogTest { - - @Test - void 블로그_생성_시_모든_필드가_올바르게_매핑된다() { - // given - Long blogId = 1L; - User user = User.builder() - .email("test@test.com") - .nickname("test") - .build(); - String intro = "This is a sample blog"; - boolean isPublic = true; - - // when - Blog result = Blog.builder() - .id(blogId) - .user(user) - .intro(intro) - .isPublic(isPublic) - .build(); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(blogId); - assertThat(result.getUser()).isEqualTo(user); - assertThat(result.getIntro()).isEqualTo(intro); - assertThat(result.isPublic()).isEqualTo(isPublic); - } - - @Test - void 사용자로부터_블로그_생성_시_기본적으로_공개상태이다() { - // given - User user = User.builder() - .email("test@test.com") - .nickname("test") - .build(); - - // when - Blog blog = Blog.from(user); - - // then - assertThat(blog).isNotNull(); - assertThat(blog.getUser()).isEqualTo(user); - assertThat(blog.isPublic()).isTrue(); - } - - @Test - void 블로그가_비공개로_설정되면_isPublic이_false를_반환한다() { - // given - User user = User.builder() - .email("test@test.com") - .nickname("test") - .build(); - - // when - Blog result = Blog.builder() - .id(2L) - .user(user) - .intro("This is a sample blog") - .isPublic(false) - .build(); - - // then - assertThat(result).isNotNull(); - assertThat(result.isPublic()).isFalse(); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentDetailsTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentDetailsTest.java deleted file mode 100644 index c51d3c51..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentDetailsTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.comment; - -import darkoverload.itzip.feature.user.domain.User; -import org.junit.jupiter.api.Test; - -import java.time.LocalDateTime; - -import static org.assertj.core.api.Assertions.assertThat; - -class CommentDetailsTest { - - @Test - void 댓글과_작성자_정보로_상세_정보를_생성한다() { - // given - Comment comment = Comment.builder() - .id("675979e6605cda1eaf5d4c19") - .postId("675979e6605cda1eaf5d4c17") - .userId(100L) - .content("This is a test comment.") - .isPublic(true) - .createDate(LocalDateTime.now()) - .build(); - - User user = User.builder() - .nickname("test_user") - .imageUrl("/images/profile.jpg") - .build(); - - // when - CommentDetails result = CommentDetails.from(comment, user); - - // then - assertThat(result).isNotNull(); - assertThat(result.getCommentId()).isEqualTo(comment.getId()); - assertThat(result.getProfileImagePath()).isEqualTo(user.getImageUrl()); - assertThat(result.getNickname()).isEqualTo(user.getNickname()); - assertThat(result.getContent()).isEqualTo(comment.getContent()); - assertThat(result.getCreateDate()).isEqualTo(comment.getCreateDate()); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentTest.java deleted file mode 100644 index 321d67a7..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/comment/CommentTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.comment; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCommentCreateRequest; -import org.junit.jupiter.api.Test; -import java.time.LocalDateTime; - -class CommentTest { - - @Test - void 댓글_생성_시_모든_필드가_올바르게_매핑된다() { - // given - String id = "675979e6605cda1eaf5d4c19"; - String postId = "675979e6605cda1eaf5d4c17"; - Long userId = 103L; - String content = "test"; - Boolean isPublic = true; - LocalDateTime createDate = LocalDateTime.now(); - - // when - Comment result = Comment.builder() - .id(id) - .postId(postId) - .userId(userId) - .content(content) - .isPublic(isPublic) - .createDate(createDate) - .build(); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(id); - assertThat(result.getPostId()).isEqualTo(postId); - assertThat(result.getUserId()).isEqualTo(userId); - assertThat(result.getContent()).isEqualTo(content); - assertThat(result.getIsPublic()).isEqualTo(isPublic); - assertThat(result.getCreateDate()).isEqualTo(createDate); - } - - @Test - void 댓글_작성_요청_시_새로운_댓글을_생성한다() { - // given - Long userId = 100L; - - PostCommentCreateRequest request = PostCommentCreateRequest.builder() - .postId("675979e6605cda1eaf5d4c17") - .content("This is a comment") - .build(); - - // when - Comment result = Comment.from(request, userId); - - // then - assertThat(result).isNotNull(); - assertThat(result.getUserId()).isEqualTo(userId); - assertThat(result.getPostId()).isEqualTo(request.postId()); - assertThat(result.getContent()).isEqualTo(request.content()); - } - -} \ No newline at end of file diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticlePreviewTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticlePreviewTest.java new file mode 100644 index 00000000..ae5574cf --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticlePreviewTest.java @@ -0,0 +1,99 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.techinfo.domain.projection.ArticlePreview; +import darkoverload.itzip.global.fixture.ArticleFixture; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ArticlePreviewTest { + + private class DummyArticlePreview implements ArticlePreview { + + private final ObjectId id; + private final long blogId; + private final ArticleType type; + private final String title; + private final String content; + private final String thumbnailImageUrl; + private final long likesCount; + private final LocalDateTime createdAt; + + public DummyArticlePreview(final Article article) { + this.id = article.getId(); + this.blogId = article.getBlogId(); + this.type = article.getType(); + this.title = article.getTitle(); + this.content = article.getContent(); + this.thumbnailImageUrl = article.getThumbnailImageUri(); + this.likesCount = article.getLikesCount(); + this.createdAt = article.getCreatedAt(); + } + + @Override + public ObjectId getId() { + return this.id; + } + + @Override + public long getBlogId() { + return this.blogId; + } + + @Override + public ArticleType getType() { + return this.type; + } + + @Override + public String getTitle() { + return this.title; + } + + @Override + public String getContent() { + return this.content; + } + + @Override + public String getThumbnailImageUri() { + return this.thumbnailImageUrl; + } + + @Override + public long getLikesCount() { + return this.likesCount; + } + + @Override + public LocalDateTime getCreatedAt() { + return this.createdAt; + } + + } + + @Test + public void ArticlePreviewGetters() { + // Given + final Article article = ArticleFixture.getNewArticle(); + + // When + ArticlePreview result = new DummyArticlePreview(article); + + // Then + assertAll( + () -> assertThat(result.getId()).isEqualTo(article.getId()), + () -> assertThat(result.getBlogId()).isEqualTo(article.getBlogId()), + () -> assertThat(result.getTitle()).isEqualTo(article.getTitle()), + () -> assertThat(result.getContent()).isEqualTo(article.getContent()), + () -> assertThat(result.getThumbnailImageUri()).isEqualTo(article.getThumbnailImageUri()), + () -> assertThat(result.getLikesCount()).isEqualTo(article.getLikesCount()), + () -> assertThat(result.getCreatedAt()).isEqualTo(article.getCreatedAt()) + ); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleTest.java new file mode 100644 index 00000000..33507fda --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleTest.java @@ -0,0 +1,144 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.BlogFixture; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ArticleTest { + + @Test + void constructor() { + // Given + final ObjectId id = ArticleFixture.DEFAULT_ID; + final long blogId = BlogFixture.DEFAULT_ID; + final ArticleType type = ArticleFixture.DEFAULT_TYPE; + final String title = ArticleFixture.DEFAULT_TITLE; + final String content = ArticleFixture.DEFAULT_CONTENT; + final String thumbnailImageUri = ArticleFixture.DEFAULT_THUMBNAIL_URI; + final long likesCount = ArticleFixture.DEFAULT_LIKES_COUNT; + final long viewCount = ArticleFixture.DEFAULT_VIEW_COUNT; + final LocalDateTime createdAt = ArticleFixture.DEFAULT_DATE_TIME; + final LocalDateTime updatedAt = ArticleFixture.DEFAULT_DATE_TIME; + final boolean displayed = ArticleFixture.DEFAULT_DISPLAYED; + + // When + final Article article = new Article(id, blogId, type, title, content, thumbnailImageUri, 0, 0, createdAt, updatedAt, displayed); + + // Then + assertAll( + () -> assertThat(article.getId()).isEqualTo(id), + () -> assertThat(article.getBlogId()).isEqualTo(blogId), + () -> assertThat(article.getType()).isEqualTo(type), + () -> assertThat(article.getTitle()).isEqualTo(title), + () -> assertThat(article.getContent()).isEqualTo(content), + () -> assertThat(article.getThumbnailImageUri()).isEqualTo(thumbnailImageUri), + () -> assertThat(article.getLikesCount()).isEqualTo(likesCount), + () -> assertThat(article.getViewCount()).isEqualTo(viewCount), + () -> assertThat(article.getCreatedAt()).isEqualTo(createdAt), + () -> assertThat(article.getDisplayed()).isEqualTo(displayed) + ); + } + + @Test + @DisplayName("아티클을 생성한다.") + void create() { + // Given + final Long blogId = BlogFixture.DEFAULT_ID; + final String type = ArticleFixture.DEFAULT_TYPE.toString(); + final String title = ArticleFixture.DEFAULT_TITLE; + final String content = ArticleFixture.DEFAULT_CONTENT; + final String thumbnailImageUri = ArticleFixture.DEFAULT_THUMBNAIL_URI; + + // When + final Article result = Article.create(blogId, type, title, content, thumbnailImageUri); + + // Then + assertAll( + () -> assertThat(result.getBlogId()).isEqualTo(blogId), + () -> assertThat(result.getType()).isEqualTo(ArticleType.from(type)), + () -> assertThat(result.getTitle()).isEqualTo(title), + () -> assertThat(result.getContent()).isEqualTo(content), + () -> assertThat(result.getThumbnailImageUri()).isEqualTo(thumbnailImageUri), + () -> assertThat(result.getDisplayed()).isEqualTo(ArticleFixture.DEFAULT_DISPLAYED) + ); + } + + @Test + @DisplayName("아티클 생성 시, 제목이 NULL 혹은 공백인 경우 예외를 발생한다.") + void createWithInvalidTitle() { + // Given + final Long blogId = BlogFixture.DEFAULT_ID; + final String type = ArticleFixture.DEFAULT_TYPE.toString(); + final String title = ""; + final String content = ArticleFixture.DEFAULT_CONTENT; + final String thumbnailImageUri = ArticleFixture.DEFAULT_THUMBNAIL_URI; + + // When & Then + assertThatThrownBy(() -> Article.create(blogId, type, title, content, thumbnailImageUri)) + .isInstanceOf(RestApiException.class) + .withFailMessage("아티클 제목은 반드시 입력되어야 합니다."); + } + + @Test + @DisplayName("아티클을 수정한다.") + void update() { + // Given + final Article article = ArticleFixture.getNewArticle(); + final String changedType = "software_development_programming_language"; + final String newTitle = "new title"; + final String newContent = "new content"; + final String newThumbnailImageUri = "new thumbnail image"; + + // When + final Article result = article.update(changedType, newTitle, newContent, newThumbnailImageUri); + + // Then + assertAll( + () -> assertThat(result.getId()).isEqualTo(article.getId()), + () -> assertThat(result.getBlogId()).isEqualTo(article.getBlogId()), + () -> assertThat(result.getType()).isEqualTo(ArticleType.SOFTWARE_DEVELOPMENT_PROGRAMMING_LANGUAGE), + () -> assertThat(result.getTitle()).isEqualTo(newTitle), + () -> assertThat(result.getContent()).isEqualTo(newContent), + () -> assertThat(result.getThumbnailImageUri()).isEqualTo(newThumbnailImageUri), + () -> assertThat(result.getDisplayed()).isEqualTo(article.getDisplayed()) + ); + } + + @Test + @DisplayName("아티클 수정 시, 제목이 NULL 혹은 공백인 경우 예외를 발생한다.") + void updateWithInvalidTitle() { + // Given + final Article article = ArticleFixture.getNewArticle(); + final String changedType = "software_development_programming_language"; + final String newTitle = ""; + final String newContent = "new content"; + final String newThumbnailImageUri = "new thumbnail image"; + + // When & Then + assertThatThrownBy(() -> article.update(changedType, newTitle, newContent, newThumbnailImageUri)) + .isInstanceOf(RestApiException.class) + .withFailMessage("아티클 제목은 반드시 입력되어야 합니다."); + } + + @Test + @DisplayName("아티클을 비공개한다.") + void hide() { + // Given + final Article article = ArticleFixture.getNewArticle(); + + // When + article.hide(); + + // Then + assertThat(article.getDisplayed()).isFalse(); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleTypeTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleTypeTest.java new file mode 100644 index 00000000..3dabb40e --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ArticleTypeTest.java @@ -0,0 +1,20 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.global.config.response.exception.RestApiException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ArticleTypeTest { + + @Test + @DisplayName("존재하지 않는 아티클 타입이 주어졌을 때 예외를 발생한다.") + void validate() { + // When & Then + assertThatThrownBy(() -> ArticleType.validate("00000000000000000000000")) + .isInstanceOf(RestApiException.class) + .withFailMessage("아티클 타입을 찾을 수 없습니다."); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/BlogTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/BlogTest.java new file mode 100644 index 00000000..631d88e4 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/BlogTest.java @@ -0,0 +1,77 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.BlogFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +class BlogTest { + + @Test + void constructor() { + // Given + final long id = BlogFixture.DEFAULT_ID; + final UserEntity user = UserFixture.getUser(); + final String intro = BlogFixture.DEFAULT_INTRO; + final LocalDateTime createdAt = BlogFixture.DEFAULT_DATE_TIME; + final LocalDateTime updatedAt = BlogFixture.DEFAULT_DATE_TIME; + + // When + final Blog blog = new Blog(id, user, intro, createdAt, updatedAt); + + // Then + assertAll( + () -> assertThat(blog.getId()).isEqualTo(id), + () -> assertThat(blog.getUser()).isEqualTo(user), + () -> assertThat(blog.getIntro()).isEqualTo(intro), + () -> assertThat(blog.getCreatedAt()).isEqualTo(createdAt), + () -> assertThat(blog.getUpdatedAt()).isEqualTo(updatedAt) + ); + } + + @Test + @DisplayName("블로그를 생성한다.") + void create() { + // Given & When + final Blog result = Blog.create(UserFixture.getUser()); + + // Then + assertAll( + () -> assertThat(result.getUser()).isEqualTo(UserFixture.getUser()), + () -> assertThat(result.getIntro()).isEqualTo(BlogFixture.DEFAULT_INTRO) + ); + } + + @Test + @DisplayName("블로그의 소개글을 수정한다.") + void update() { + // Given + final Blog blog = BlogFixture.getBlog(); + + // When + blog.updateIntro(BlogFixture.DEFAULT_NEW_INTRO); + + // Then + assertThat(blog.getIntro()).isEqualTo(BlogFixture.DEFAULT_NEW_INTRO); + } + + @Test + @DisplayName("블로그 소개글 수정 시, 소개글이 NULL 혹은 공백인 경우 예외를 발생한다.") + void updateWithInvalidIntro() { + // Given + final Blog blog = BlogFixture.getBlog(); + + // When & Then + assertThatThrownBy(() -> blog.updateIntro("")) + .isInstanceOf(RestApiException.class) + .withFailMessage("블로그 소개글은 반드시 입력되어야 합니다."); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/CommentTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/CommentTest.java new file mode 100644 index 00000000..d91c9846 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/CommentTest.java @@ -0,0 +1,115 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.global.config.response.exception.RestApiException; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.CommentFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +class CommentTest { + + @Test + void constructor() { + // Given + final long id = CommentFixture.DEFAULT_ID; + final UserEntity user = UserFixture.getUser(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + final String content = CommentFixture.DEFAULT_CONTENT; + final LocalDateTime createdAt = CommentFixture.DEFAULT_DATE_TIME; + final LocalDateTime updatedAt = CommentFixture.DEFAULT_DATE_TIME; + final boolean isDisplayed = CommentFixture.DEFAULT_DISPLAYED; + + // When + Comment comment = new Comment(id, user, articleId, content, createdAt, updatedAt, isDisplayed); + + // Then + assertAll( + () -> assertThat(comment.getId()).isEqualTo(id), + () -> assertThat(comment.getUser()).isEqualTo(user), + () -> assertThat(comment.getArticleId()).isEqualTo(articleId), + () -> assertThat(comment.getContent()).isEqualTo(content), + () -> assertThat(comment.getCreatedAt()).isEqualTo(createdAt), + () -> assertThat(comment.getUpdatedAt()).isEqualTo(updatedAt), + () -> assertThat(comment.getDisplayed()).isEqualTo(isDisplayed) + ); + } + + @Test + @DisplayName("댓글을 생성한다.") + void create() { + // Given + final UserEntity user = UserFixture.getUser(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + final String content = CommentFixture.DEFAULT_CONTENT; + + // When + final Comment result = Comment.create(user, articleId, content); + + // Then + assertAll( + () -> assertThat(result.getUser()).isEqualTo(user), + () -> assertThat(result.getArticleId()).isEqualTo(articleId), + () -> assertThat(result.getContent()).isEqualTo(content) + ); + } + + @Test + @DisplayName("댓글 생성 시, 본문이 NULL 혹은 공백인 경우 예외를 발생한다.") + void createWithInvalidContent() { + // Given + final UserEntity user = UserFixture.getUser(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + final String content = ""; + + // Then & When + assertThatThrownBy(() -> Comment.create(user, articleId, content)) + .isInstanceOf(RestApiException.class) + .withFailMessage("댓글 본문은 반드시 입력되어야 합니다."); + } + + @Test + @DisplayName("댓글을 수정한다.") + void update() { + // Given + final Comment comment = CommentFixture.getComment(); + + // When + comment.updateContent(CommentFixture.DEFAULT_NEW_CONTENT); + + // Then + assertThat(comment.getContent()).isEqualTo(CommentFixture.DEFAULT_NEW_CONTENT); + } + + @Test + @DisplayName("댓글 수정 시, 본문이 NULL 혹은 공백인 경우 예외를 발생한다.") + void updateWithInvalidContent() { + // Given + final Comment comment = CommentFixture.getComment(); + + // Then & When + assertThatThrownBy(() -> comment.updateContent("")) + .isInstanceOf(RestApiException.class) + .withFailMessage("댓글 본문은 반드시 입력되어야 합니다."); + } + + @Test + @DisplayName("댓글을 비공개 처리한다.") + void hide() { + // Given + final Comment comment = CommentFixture.getComment(); + + // When + comment.hide(); + + // Then + assertThat(comment.getDisplayed()).isFalse(); + } + +} \ No newline at end of file diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/LikeTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/LikeTest.java new file mode 100644 index 00000000..051ddc99 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/LikeTest.java @@ -0,0 +1,54 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.LikeFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class LikeTest { + + @Test + void constructor() { + // Given + final long id = LikeFixture.DEFAULT_ID; + final UserEntity user = UserFixture.getUser(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + final LocalDateTime createdAt = LikeFixture.DEFAULT_DATE_TIME; + + // When + final Like like = new Like(id, user, articleId, createdAt); + + // Then + assertAll( + () -> assertThat(like.getId()).isEqualTo(id), + () -> assertThat(like.getUser()).isEqualTo(user), + () -> assertThat(like.getArticleId()).isEqualTo(articleId), + () -> assertThat(like.getCreatedAt()).isEqualTo(createdAt) + ); + } + + @Test + @DisplayName("좋아요을 생성한다.") + void create() { + // Given + final UserEntity user = UserFixture.getUser(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + final Like result = Like.create(user, articleId); + + // Then + assertAll( + () -> assertThat(result.getUser()).isEqualTo(user), + () -> assertThat(result.getArticleId()).isEqualTo(articleId) + ); + } + +} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ScrapTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ScrapTest.java new file mode 100644 index 00000000..c5bd95d3 --- /dev/null +++ b/src/test/java/darkoverload/itzip/feature/techinfo/domain/entity/ScrapTest.java @@ -0,0 +1,54 @@ +package darkoverload.itzip.feature.techinfo.domain.entity; + +import darkoverload.itzip.feature.user.entity.UserEntity; +import darkoverload.itzip.global.fixture.ArticleFixture; +import darkoverload.itzip.global.fixture.LikeFixture; +import darkoverload.itzip.global.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ScrapTest { + + @Test + void constructor() { + // Given + final long id = LikeFixture.DEFAULT_ID; + final UserEntity user = UserFixture.getUser(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + final LocalDateTime createdAt = LikeFixture.DEFAULT_DATE_TIME; + + // When + final Scrap scrap = new Scrap(id, user, articleId, createdAt); + + // Then + assertAll( + () -> assertThat(scrap.getId()).isEqualTo(id), + () -> assertThat(scrap.getUser()).isEqualTo(user), + () -> assertThat(scrap.getArticleId()).isEqualTo(articleId), + () -> assertThat(scrap.getCreatedAt()).isEqualTo(createdAt) + ); + } + + @Test + @DisplayName("스크랩을 생성한다.") + void create() { + // Given + final UserEntity user = UserFixture.getUser(); + final String articleId = ArticleFixture.DEFAULT_ID.toHexString(); + + // When + final Scrap result = Scrap.create(user, articleId); + + // Then + assertAll( + () -> assertThat(result.getUser()).isEqualTo(user), + () -> assertThat(result.getArticleId()).isEqualTo(articleId) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/like/LikeTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/like/LikeTest.java deleted file mode 100644 index 95f43847..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/like/LikeTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.like; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import org.junit.jupiter.api.Test; - -class LikeTest { - - @Test - void 좋아요_생성_시_모든_필드가_올바르게_매핑된다() { - // given - String id = "675979e6605cda1eaf5d4c20"; - String postId = "675979e6605cda1eaf5d4c17"; - Long userId = 100L; - - // when - Like result = Like.builder() - .id(id) - .postId(postId) - .userId(userId) - .build(); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(id); - assertThat(result.getPostId()).isEqualTo(postId); - assertThat(result.getUserId()).isEqualTo(userId); - } - - @Test - void 좋아요_상태로부터_좋아요를_생성한다() { - // given - LikeStatus likeStatus = LikeStatus.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(100L) - .isLiked(true) - .build(); - - // when - Like result = Like.from(likeStatus); - - // then - assertThat(result).isNotNull(); - assertThat(result.getPostId()).isEqualTo(likeStatus.getPostId()); - assertThat(result.getUserId()).isEqualTo(likeStatus.getUserId()); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostDetailsTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostDetailsTest.java deleted file mode 100644 index d8c305e4..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostDetailsTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.post; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.mock.PostMockData; -import darkoverload.itzip.feature.user.domain.User; -import org.junit.jupiter.api.Test; - -class PostDetailsTest { - - @Test - void 게시글과_작성자_정보로_상세_정보가_생성된다() { - // given - Post post = PostMockData.postDataOne; - - User user = User.builder() - .email("test@test.com") - .nickname("test") - .build(); - - boolean liked = true; - boolean scrapped = false; - - // when - PostDetails result = PostDetails.from(post, user, liked, scrapped); - - // then - assertThat(result).isNotNull(); - assertThat(result.getProfileImagePath()).isEqualTo(user.getImageUrl()); - assertThat(result.getAuthor()).isEqualTo(user.getNickname()); - assertThat(result.getBlogId()).isEqualTo(post.getBlogId()); - assertThat(result.getPostId()).isEqualTo(post.getId()); - assertThat(result.getCategoryId()).isEqualTo(post.getCategoryId()); - assertThat(result.getCreateDate()).isEqualTo(post.getCreateDate()); - assertThat(result.getTitle()).isEqualTo(post.getTitle()); - assertThat(result.getContent()).isEqualTo(post.getContent()); - assertThat(result.getViewCount()).isEqualTo(post.getViewCount()); - assertThat(result.getLikeCount()).isEqualTo(post.getLikeCount()); - assertThat(result.getThumbnailImagePath()).isEqualTo(post.getThumbnailImagePath()); - assertThat(result.getContentImagePaths()).isEqualTo(post.getContentImagePaths()); - assertThat(result.isLiked()).isEqualTo(liked); - assertThat(result.isScrapped()).isEqualTo(scrapped); - } - -} \ No newline at end of file diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostInfoTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostInfoTest.java deleted file mode 100644 index 2ddb48db..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostInfoTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.post; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.mock.PostMockData; -import darkoverload.itzip.feature.user.domain.User; -import org.junit.jupiter.api.Test; - -class PostInfoTest { - - @Test - void 게시글과_작성자_정보로_기본_정보가_생성된다() { - // given - Post post = PostMockData.postDataOne; - - User user = User.builder() - .email("test@test.com") - .nickname("test") - .build(); - - // when - PostInfo result = PostInfo.from(post, user); - - // then - assertThat(result).isNotNull(); - assertThat(result.getProfileImagePath()).isEqualTo(user.getImageUrl()); - assertThat(result.getAuthor()).isEqualTo(user.getNickname()); - assertThat(result.getPostId()).isEqualTo(post.getId()); - assertThat(result.getCategoryId()).isEqualTo(post.getCategoryId()); - assertThat(result.getCreateDate()).isEqualTo(post.getCreateDate()); - assertThat(result.getTitle()).isEqualTo(post.getTitle()); - assertThat(result.getContent()).isEqualTo(post.getContent()); - assertThat(result.getLikeCount()).isEqualTo(post.getLikeCount()); - assertThat(result.getThumbnailImagePath()).isEqualTo(post.getThumbnailImagePath()); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostTest.java deleted file mode 100644 index 65d42edf..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/post/PostTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.post; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.controller.post.request.PostCreateRequest; -import org.junit.jupiter.api.Test; -import java.time.LocalDateTime; -import java.util.List; - -class PostTest { - - @Test - void 게시글_생성_시_모든_필드가_올바르게_매핑된다() { - // given - String id = "675979e6605cda1eaf5d4c17"; - Long blogId = 1L; - String categoryId = "66ce18d84cb7d0b29ce602f5"; - String title = "밤하늘 아래, 감정의 여정"; - String content = "세 개의 이미지는 감정의 여정을 표현한다."; - Integer viewCount = 0; - Integer likeCount = 0; - boolean isPublic = true; - LocalDateTime createDate = LocalDateTime.now(); - String thumbnailImagePath = "/images/thumbnail.jpg"; - List contentImagePaths = List.of( - "/images/content1.jpg", - "/images/content2.jpg" - ); - - // when - Post result = Post.builder() - .id(id) - .blogId(blogId) - .categoryId(categoryId) - .title(title) - .content(content) - .viewCount(viewCount) - .likeCount(likeCount) - .isPublic(isPublic) - .createDate(createDate) - .thumbnailImagePath(thumbnailImagePath) - .contentImagePaths(contentImagePaths) - .build(); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(id); - assertThat(result.getBlogId()).isEqualTo(blogId); - assertThat(result.getCategoryId()).isEqualTo(categoryId); - assertThat(result.getTitle()).isEqualTo(title); - assertThat(result.getContent()).isEqualTo(content); - assertThat(result.getViewCount()).isEqualTo(viewCount); - assertThat(result.getLikeCount()).isEqualTo(likeCount); - assertThat(result.getIsPublic()).isTrue(); - assertThat(result.getCreateDate()).isEqualTo(createDate); - assertThat(result.getThumbnailImagePath()).isEqualTo(thumbnailImagePath); - assertThat(result.getContentImagePaths()).isEqualTo(contentImagePaths); - } - - @Test - void 게시글_작성_요청으로_새로운_게시글을_생성한다() { - // given - Long blogId = 1L; - PostCreateRequest request = PostCreateRequest.builder() - .categoryId("66ce18d84cb7d0b29ce602f5") - .title("밤하늘 아래, 감정의 여정") - .content("세 개의 이미지는 감정의 여정을 표현한다.") - .thumbnailImagePath("/images/thumbnail.jpg") - .contentImagePaths(List.of( - "/images/content1.jpg", - "/images/content2.jpg" - )) - .build(); - - // when - Post result = Post.from(request, blogId); - - // then - assertThat(result).isNotNull(); - assertThat(result.getBlogId()).isEqualTo(blogId); - assertThat(result.getCategoryId()).isEqualTo(request.categoryId()); - assertThat(result.getTitle()).isEqualTo(request.title()); - assertThat(result.getContent()).isEqualTo(request.content()); - assertThat(result.getThumbnailImagePath()).isEqualTo(request.thumbnailImagePath()); - assertThat(result.getContentImagePaths()).isEqualTo(request.contentImagePaths()); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/domain/scrap/ScrapTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/domain/scrap/ScrapTest.java deleted file mode 100644 index 3a42b423..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/domain/scrap/ScrapTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package darkoverload.itzip.feature.techinfo.domain.scrap; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; -import org.junit.jupiter.api.Test; - -class ScrapTest { - - @Test - void 스크랩_생성_시_모든_필드가_올바르게_매핑된다() { - // given - String id = "675979e6605cda1eaf5d4c20"; - String postId = "675979e6605cda1eaf5d4c17"; - Long userId = 100L; - - // when - Scrap result = Scrap.builder() - .id(id) - .postId(postId) - .userId(userId) - .build(); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(id); - assertThat(result.getPostId()).isEqualTo(postId); - assertThat(result.getUserId()).isEqualTo(userId); - } - - @Test - void 스크랩_상태로부터_스크랩을_생성한다() { - // given - ScrapStatus scrapStatus = ScrapStatus.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(100L) - .isScrapped(true) - .build(); - - // when - Scrap result = Scrap.from(scrapStatus); - - // then - assertThat(result).isNotNull(); - assertThat(result.getPostId()).isEqualTo(scrapStatus.getPostId()); - assertThat(result.getUserId()).isEqualTo(scrapStatus.getUserId()); - } - -} \ No newline at end of file diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/mock/CommentMockData.java b/src/test/java/darkoverload/itzip/feature/techinfo/mock/CommentMockData.java deleted file mode 100644 index 6c19d7d7..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/mock/CommentMockData.java +++ /dev/null @@ -1,36 +0,0 @@ -package darkoverload.itzip.feature.techinfo.mock; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; - -public class CommentMockData { - - public static Comment commentDataOne = Comment.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(100L) - .content("test1") - .isPublic(true) - .build(); - - public static Comment commentDataSecond = Comment.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(101L) - .content("test2") - .isPublic(true) - .build(); - - public static Comment commentDataThree = Comment.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(102L) - .content("test3") - .isPublic(true) - .build(); - - public static Comment commentDataFour = Comment.builder() - .id("675979e6605cda1eaf5d4c19") - .postId("675979e6605cda1eaf5d4c17") - .userId(103L) - .content("test4") - .isPublic(true) - .build(); - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/mock/LikeMockData.java b/src/test/java/darkoverload/itzip/feature/techinfo/mock/LikeMockData.java deleted file mode 100644 index 4ff930c1..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/mock/LikeMockData.java +++ /dev/null @@ -1,22 +0,0 @@ -package darkoverload.itzip.feature.techinfo.mock; - -import darkoverload.itzip.feature.techinfo.domain.like.Like; - -public class LikeMockData { - - public static Like likeDataOne = Like.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(100L) - .build(); - - public static Like likeDataSecond = Like.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(101L) - .build(); - - public static Like likeDataThree = Like.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(102L) - .build(); - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/mock/PostMockData.java b/src/test/java/darkoverload/itzip/feature/techinfo/mock/PostMockData.java deleted file mode 100644 index 80a96168..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/mock/PostMockData.java +++ /dev/null @@ -1,84 +0,0 @@ -package darkoverload.itzip.feature.techinfo.mock; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import java.util.List; - -public class PostMockData { - - public static Post postDataOne = Post.builder() - .blogId(100L) - .categoryId("66ce18d84cb7d0b29ce602f5") - .title("밤하늘 아래, 감정의 여정") - .content("세 개의 이미지는 감정의 여정을 표현한다.") - .viewCount(0) - .likeCount(0) - .isPublic(true) - .thumbnailImagePath("https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg") - .contentImagePaths(List.of( - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - )) - .build(); - - public static Post postDataSecond = Post.builder() - .blogId(100L) - .categoryId("66ce18d84cb7d0b29ce602f5") - .title("밤하늘 아래, 감정의 여정") - .content("세 개의 이미지는 감정의 여정을 표현한다.") - .viewCount(0) - .likeCount(0) - .isPublic(true) - .thumbnailImagePath("https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg") - .contentImagePaths(List.of( - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - )) - .build(); - - public static Post postDataThree = Post.builder() - .blogId(100L) - .categoryId("66ce18d84cb7d0b29ce602f5") - .title("밤하늘 아래, 감정의 여정") - .content("세 개의 이미지는 감정의 여정을 표현한다.") - .viewCount(0) - .likeCount(0) - .isPublic(true) - .thumbnailImagePath("https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg") - .contentImagePaths(List.of( - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - )) - .build(); - - public static Post postDataFour = Post.builder() - .blogId(100L) - .categoryId("66ce18d84cb7d0b29ce602f5") - .title("밤하늘 아래, 감정의 여정") - .content("세 개의 이미지는 감정의 여정을 표현한다.") - .viewCount(0) - .likeCount(0) - .isPublic(true) - .thumbnailImagePath("https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg") - .contentImagePaths(List.of( - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - )) - .build(); - - public static Post postDataFive = Post.builder() - .id("675979e6605cda1eaf5d4c17") - .blogId(100L) - .categoryId("66ce18d84cb7d0b29ce602f5") - .title("밤하늘 아래, 감정의 여정") - .content("세 개의 이미지는 감정의 여정을 표현한다.") - .viewCount(0) - .likeCount(0) - .isPublic(true) - .thumbnailImagePath("https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg") - .contentImagePaths(List.of( - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - )) - .build(); - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/mock/ScrapMockData.java b/src/test/java/darkoverload/itzip/feature/techinfo/mock/ScrapMockData.java deleted file mode 100644 index ee2de1fd..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/mock/ScrapMockData.java +++ /dev/null @@ -1,22 +0,0 @@ -package darkoverload.itzip.feature.techinfo.mock; - -import darkoverload.itzip.feature.techinfo.domain.scrap.Scrap; - -public class ScrapMockData { - - public static Scrap scrapDataOne = Scrap.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(100L) - .build(); - - public static Scrap scrapDataSecond = Scrap.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(101L) - .build(); - - public static Scrap scrapDataThree = Scrap.builder() - .postId("675979e6605cda1eaf5d4c17") - .userId(102L) - .build(); - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogCommandRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogCommandRepositoryTest.java deleted file mode 100644 index 703d7561..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogCommandRepositoryTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogCommandRepository; -import darkoverload.itzip.feature.user.domain.User; -import darkoverload.itzip.feature.user.entity.Authority; -import darkoverload.itzip.feature.user.repository.UserRepository; -import darkoverload.itzip.global.config.querydsl.TestQueryDslConfig; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.SqlGroup; - -@SqlGroup({ - @Sql(value = "/sql/techinfo/blog-repository-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), - @Sql(value = "/sql/techinfo/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) -}) -@Import({ - BlogCommandRepositoryImpl.class, - BlogReadRepositoryImpl.class -}) -@DataJpaTest -@ActiveProfiles("test") -@ContextConfiguration(classes = {TestQueryDslConfig.class}) -@TestPropertySource(locations = "classpath:properties/test-env.properties") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -class BlogCommandRepositoryTest { - - @Autowired - private UserRepository userRepository; - - @Autowired - private BlogCommandRepository blogCommandRepository; - - @Test - void 새로운_블로그가_주어졌을_때_저장된다() { - // given - User owner = User.builder() - .email("test2@test.com") - .nickname("아름다운 136번째 돌고래") - .password("$2a$10$5RifHVaUMq.7IXyJK40kpuaWzfhRBsPgdq1CAhB6LGXdwbxep0.Ba") - .imageUrl("https://itzip.com") - .authority(Authority.USER) - .build(); - - Blog blog = Blog.builder() - .intro("새로운 블로그 소개글") - .isPublic(true) - .user(owner) - .build(); - - // when - userRepository.save(owner.convertToEntity()); - Blog savedBlog = blogCommandRepository.save(blog); - - // then - assertThat(savedBlog).isNotNull(); - assertThat(savedBlog.getIntro()).isEqualTo(blog.getIntro()); - } - - @Test - void 사용자_ID와_새로운_소개글이_주어졌을_때_소개글을_업데이트한다() { - // given - Long userId = 100L; - String newIntro = "업데이트된 블로그 소개글"; - - // when - Blog result = blogCommandRepository.update(userId, newIntro); - - // then - assertThat(result).isNotNull(); - assertThat(result.getIntro()).isEqualTo(newIntro); - } - - @Test - void 존재하지_않는_사용자_ID와_새로운_소개글이_주어졌을_때_예외가_발생한다() { - // given - Long userId = 101L; - String newIntro = "업데이트된 블로그 소개글"; - - // when & then - assertThatThrownBy(() -> blogCommandRepository.update(userId, newIntro)) - .isInstanceOf(RestApiException.class); - } - - @Test - void 블로그_ID와_상태가_주어졌을_때_공개_상태가_업데이트된다() { - // given - Long blogId = 100L; - boolean newStatus = false; - - // when - Blog result = blogCommandRepository.update(blogId, newStatus); - - // then - assertThat(result).isNotNull(); - assertThat(result.isPublic()).isFalse(); - } - - @Test - void 존재하지_않는_블로그_ID와_상태가_주어졌을_때_예외가_발생한다() { - // given - Long invalidBlogId = 999L; - boolean newStatus = true; - - // when & then - assertThatThrownBy(() -> blogCommandRepository.update(invalidBlogId, newStatus)) - .isInstanceOf(RestApiException.class); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogReadRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogReadRepositoryTest.java deleted file mode 100644 index ce7c305f..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/blog/BlogReadRepositoryTest.java +++ /dev/null @@ -1,163 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.blog; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -import darkoverload.itzip.feature.techinfo.domain.blog.Blog; -import darkoverload.itzip.feature.techinfo.service.blog.port.BlogReadRepository; -import darkoverload.itzip.global.config.querydsl.TestQueryDslConfig; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.SqlGroup; -import org.junit.jupiter.api.Test; -import java.util.Optional; - -@SqlGroup({ - @Sql(value = "/sql/techinfo/blog-repository-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), - @Sql(value = "/sql/techinfo/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) -}) -@Import( - BlogReadRepositoryImpl.class -) -@DataJpaTest -@ActiveProfiles("test") -@ContextConfiguration(classes = TestQueryDslConfig.class) -class BlogReadRepositoryTest { - - @Autowired - private BlogReadRepository blogReadRepository; - - @Test - void 블로그ID로_블로그를_조회하면_정상적으로_데이터를_반환한다() { - // given - // when - Optional result = blogReadRepository.findByBlogId(100L); - - // then - assertThat(result.isPresent()).isTrue(); - } - - @Test - void 존재하지_않는_블로그ID로_조회하면_빈데이터를_반환한다() { - // given - // when - Optional result = blogReadRepository.findByBlogId(101L); - - // then - assertThat(result.isEmpty()).isTrue(); - } - - @Test - void 사용자ID로_블로그를_조회하면_정상적으로_데이터를_반환한다() { - // given - // when - Optional result = blogReadRepository.findByUserId(100L); - - // then - assertThat(result.isPresent()).isTrue(); - } - - @Test - void 존재하지_않는_사용자ID로_조회하면_빈데이터를_반환한다() { - // given - // when - Optional result = blogReadRepository.findByUserId(101L); - - // then - assertThat(result.isEmpty()).isTrue(); - } - - @Test - void 닉네임으로_블로그를_조회하면_정상적으로_데이터를_반환한다() { - // given - // when - Optional result = blogReadRepository.findByNickname("아름다운 135번째 돌고래"); - - // then - assertThat(result.isPresent()).isTrue(); - } - - @Test - void 존재하지_않는_닉네임을_조회하면_빈데이터를_반환한다() { - // given - // when - Optional result = blogReadRepository.findByNickname("아름다운 136번째 돌고래"); - - // then - assertThat(result.isEmpty()).isTrue(); - } - - @Test - void 존재하는_블로그ID로_조회하면_블로그를_반환한다() { - // given - // when - Blog blog = blogReadRepository.getById(100L); - - // then - assertThat(blog).isNotNull(); - assertThat(blog.getId()).isEqualTo(100L); - } - - @Test - void 존재하지_않는_블로그ID로_조회하면_NOT_FOUND_BLOG_예외가_발생한다() { - // given - Long nonExistentId = 999999L; - - // when & then - assertThatThrownBy( - () -> blogReadRepository.getById(nonExistentId) - ).isInstanceOf(RestApiException.class); - } - - @Test - void 존재하는_사용자ID로_블로그를_조회하면_블로그를_반환한다() { - // given - // when - Blog blog = blogReadRepository.getById(100L); - - // then - assertThat(blog).isNotNull(); - assertThat(blog.getId()).isEqualTo(100L); - } - - @Test - void 존재하지_않는_사용자ID로_블로그를_조회하면_NOT_FOUND_BLOG_예외가_발생한다() { - // given - Long nonExistentId = 999999L; - - // when & then - assertThatThrownBy( - () -> blogReadRepository.getById(nonExistentId) - ).isInstanceOf(RestApiException.class); - } - - @Test - void 존재하는_닉네임으로_블로그를_조회하면_블로그를_반환한다() { - // given - String nickname = "아름다운 135번째 돌고래"; - - // when - Blog blog = blogReadRepository.getByNickname(nickname); - - // then - assertThat(blog).isNotNull(); - assertThat(blog.getUser().getNickname()).isEqualTo(nickname); - } - - @Test - void 존재하지_않는_닉네임으로_블로그를_조회하면_NOT_FOUND_BLOG_예외가_발생한다() { - // given - String nonExistentNickname = "존재하지 않는 닉네임"; - - // when & then - assertThatThrownBy( - () -> blogReadRepository.getByNickname(nonExistentNickname) - ).isInstanceOf(RestApiException.class); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentCommandRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentCommandRepositoryTest.java deleted file mode 100644 index 7c9767fb..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentCommandRepositoryTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.feature.techinfo.mock.CommentMockData; -import darkoverload.itzip.feature.techinfo.service.comment.port.CommentCommandRepository; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.bson.types.ObjectId; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import java.util.List; - -@SpringBootTest -@ActiveProfiles("test") -class CommentCommandRepositoryTest { - - @Autowired - private CommentCommandRepository commentCommandRepository; - - @BeforeAll - static void setUp(@Autowired CommentCommandRepository commentCommandRepository) { - commentCommandRepository.save(CommentMockData.commentDataFour); - } - - @AfterAll - static void tearDown(@Autowired CommentCommandRepository commentCommandRepository) { - commentCommandRepository.deleteAll(); - } - - @Test - void 새로운_게시글이_주어졌을_때_저장된다() { - // given - String content = "test1"; - - // when - Comment result = commentCommandRepository.save(CommentMockData.commentDataOne); - - // then - assertThat(result).isNotNull(); - assertThat(result.getContent()).isEqualTo(content); - } - - @Test - void 여러_댓글이_주어졌을_때_저장된다() { - // given - // when - List result = commentCommandRepository.saveAll( - List.of( - CommentMockData.commentDataSecond, - CommentMockData.commentDataThree - ) - ); - - // then - assertThat(result).isNotNull(); - assertThat(result).hasSize(2); - } - - @Test - void 댓글_ID와_사용자_ID_그리고_업데이트_데이터가_주어졌을_때_댓글이_업데이트된다() { - // given - ObjectId commentId = new ObjectId("675979e6605cda1eaf5d4c19"); - Long userId = 103L; - String newContent = "Updated comment content"; - - // when - Comment result = commentCommandRepository.update(commentId, userId, newContent); - - // then - assertThat(result).isNotNull(); - assertThat(result.getContent()).isEqualTo(newContent); - } - - @Test - void 존재하지_않는_댓글_업데이트시_예외가_발생한다() { - // given - ObjectId nonExistentCommentId = new ObjectId("000000000000000000000000"); - Long userId = 103L; - String newContent = "Updated comment content"; - - // when & then - assertThatThrownBy( - () -> commentCommandRepository.update(nonExistentCommentId, userId, newContent) - ).isInstanceOf(RestApiException.class); - } - - @Test - void 댓글_ID와_사용자_ID_그리고_공개_상태가_주어졌을_때_상태가_업데이트된다() { - // given - ObjectId commentId = new ObjectId("675979e6605cda1eaf5d4c19"); - Long userId = 103L; - boolean newStatus = false; - - // when - Comment result = commentCommandRepository.update(commentId, userId, newStatus); - - // then - assertThat(result).isNotNull(); - assertThat(result.getIsPublic()).isFalse(); - } - - @Test - void 존재하지_않는_댓글_공개_상태_업데이트시_예외가_발생한다() { - // given - ObjectId nonExistentCommentId = new ObjectId("000000000000000000000000"); - Long userId = 103L; - boolean newStatus = false; - - // when & then - assertThatThrownBy( - () -> commentCommandRepository.update(nonExistentCommentId, userId, newStatus) - ).isInstanceOf(RestApiException.class); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentReadRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentReadRepositoryTest.java deleted file mode 100644 index 4e17368b..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/comment/CommentReadRepositoryTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.comment; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.domain.comment.Comment; -import darkoverload.itzip.feature.techinfo.mock.CommentMockData; -import darkoverload.itzip.feature.techinfo.mock.PostMockData; -import darkoverload.itzip.feature.techinfo.service.comment.port.CommentCommandRepository; -import darkoverload.itzip.feature.techinfo.service.comment.port.CommentReadRepository; -import darkoverload.itzip.feature.techinfo.service.post.port.PostCommandRepository; -import darkoverload.itzip.feature.techinfo.type.SortType; -import darkoverload.itzip.feature.techinfo.util.SortUtil; -import org.bson.types.ObjectId; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.test.context.ActiveProfiles; - -import java.util.List; - -@SpringBootTest -@ActiveProfiles("test") -class CommentReadRepositoryTest { - - @Autowired - private CommentReadRepository commentReadRepository; - - @BeforeAll - static void setUp( - @Autowired PostCommandRepository postCommandRepository, - @Autowired CommentCommandRepository commentCommandRepository - ) { - postCommandRepository.save(PostMockData.postDataFive); - commentCommandRepository.saveAll( - List.of( - CommentMockData.commentDataOne, - CommentMockData.commentDataSecond, - CommentMockData.commentDataThree - ) - ); - } - - @Test - void 게시글_ID로_댓글을_페이징하여_조회한다() { - // given - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - int page = 0; - int size = 10; - Sort sort = SortUtil.getType(SortType.NEWEST); - Pageable pageable = PageRequest.of(page, size, sort); - - // when - Page result = commentReadRepository.findCommentsByPostId(postId, pageable); - - // then - assertThat(result).isNotNull(); - assertThat(result.getTotalElements()).isEqualTo(3); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/like/LikeCacheRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/like/LikeCacheRepositoryTest.java deleted file mode 100644 index e5f99a09..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/like/LikeCacheRepositoryTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.like; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.dto.like.LikeStatus; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeCacheRepository; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import java.util.List; - -@SpringBootTest -@ActiveProfiles("test") -public class LikeCacheRepositoryTest { - - @Autowired - private LikeCacheRepository likeCacheRepository; - - private final Long userId = 100L; - private final String postId = "675979e6605cda1eaf5d4c17"; - - @BeforeEach - void setUp() { - likeCacheRepository.save(userId, postId, true, 90); - } - - @AfterEach - void tearDown() { - likeCacheRepository.deleteAll(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_저장된다() { - // given - Long userId = 101L; - String postId = "675979e6605cda1eaf5d4c17"; - - // when - likeCacheRepository.save(userId, postId, true, 90); - - // then - Boolean scrapStatus = likeCacheRepository.getLikeStatus(userId, postId); - assertThat(scrapStatus).isNotNull(); - assertThat(scrapStatus).isTrue(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_좋아요_상태를_조회한다() { - // when - Boolean scrapStatus = likeCacheRepository.getLikeStatus(userId, postId); - - // then - assertThat(scrapStatus).isNotNull(); - assertThat(scrapStatus).isTrue(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_좋아요_상태가_없으면_null을_반환한다() { - // given - Long nonExistentUserId = 102L; - String nonExistentPostId = "675979e6605cda1eaf5d4c18"; - - // when - Boolean scrapStatus = likeCacheRepository.getLikeStatus(nonExistentUserId, nonExistentPostId); - - // then - assertThat(scrapStatus).isNull(); - } - - @Test - void Redis에_저장된_모든_좋아요_상태를_조회한다() { - // given - likeCacheRepository.save(103L, "675979e6605cda1eaf5d4c17", true, 90); - - // when - List scrapStatuses = likeCacheRepository.getAllLikeStatuses(); - - // then - assertThat(scrapStatuses).isNotNull(); - assertThat(scrapStatuses).hasSize(2); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/like/LikeRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/like/LikeRepositoryTest.java deleted file mode 100644 index 03e1129e..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/like/LikeRepositoryTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.like; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import darkoverload.itzip.feature.techinfo.domain.like.Like; -import darkoverload.itzip.feature.techinfo.mock.LikeMockData; -import darkoverload.itzip.feature.techinfo.service.like.port.LikeRepository; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.bson.types.ObjectId; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles("test") -class LikeRepositoryTest { - - @Autowired - private LikeRepository likeRepository; - - @AfterAll - static void tearDown(@Autowired LikeRepository likeRepository) { - likeRepository.deleteAll(); - } - - @Test - void 새로운_좋아요가_주어졌을_때_저장된다() { - // given - String postId = "675979e6605cda1eaf5d4c17"; - - // when - Like like = likeRepository.save(LikeMockData.likeDataOne); - - // then - assertThat(like).isNotNull(); - assertThat(like.getPostId()).isEqualTo(postId); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_좋아요가_존재하는지_확인한다() { - // given - Long userId = 101L; - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - likeRepository.save(LikeMockData.likeDataSecond); - - // when - boolean exists = likeRepository.existsByUserIdAndPostId(userId, postId); - - // then - assertThat(exists).isTrue(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_좋아요가_존재하지_않으면_false를_반환한다() { - // given - Long userId = 100L; - ObjectId nonExistentPostId = new ObjectId("675979e6605cda1eaf5d4c18"); - - // when - boolean exists = likeRepository.existsByUserIdAndPostId(userId, nonExistentPostId); - - // then - assertThat(exists).isFalse(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_좋아요를_삭제한다() { - // given - Long userId = 102L; - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - likeRepository.save(LikeMockData.likeDataThree); - - // when - likeRepository.deleteByUserIdAndPostId(userId, postId); - - // then - boolean exists = likeRepository.existsByUserIdAndPostId(userId, postId); - assertThat(exists).isFalse(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_좋아요가_존재하지_않는_경우_예외가_발생한다() { - // given - Long userId = 100L; - ObjectId nonExistentPostId = new ObjectId("675979e6605cda1eaf5d4c18"); - - // when & then - assertThatThrownBy( - () -> likeRepository.deleteByUserIdAndPostId(userId, nonExistentPostId) - ).isInstanceOf(RestApiException.class); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/post/PostCommandRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/post/PostCommandRepositoryTest.java deleted file mode 100644 index 4eef8e60..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/post/PostCommandRepositoryTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.mock.PostMockData; -import darkoverload.itzip.feature.techinfo.service.post.port.PostCommandRepository; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.bson.types.ObjectId; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.List; - -@SpringBootTest -@ActiveProfiles("test") -class PostCommandRepositoryTest { - - @Autowired - private PostCommandRepository postCommandRepository; - - @BeforeAll - static void setUp(@Autowired PostCommandRepository postCommandRepository) { - postCommandRepository.save(PostMockData.postDataFive); - } - - @AfterAll - static void tearDown(@Autowired PostCommandRepository postCommandRepository) { - postCommandRepository.deleteAll(); - } - - @Test - void 새로운_게시글이_주어졌을_때_저장된다() { - // given - long blogId = 100L; - - // when - Post result = postCommandRepository.save(PostMockData.postDataOne); - - // then - assertThat(result).isNotNull(); - assertThat(result.getBlogId()).isEqualTo(blogId); - } - - @Test - void 여러_게시글이_주어졌을_때_저장된다() { - // given - // when - List result = postCommandRepository.saveAll( - List.of( - PostMockData.postDataSecond, - PostMockData.postDataThree, - PostMockData.postDataFour - ) - ); - - // then - assertThat(result).isNotNull(); - assertThat(result).hasSize(3); - } - - @Test - void 게시글_ID와_업데이트_데이터가_주어졌을_때_게시글이_업데이트된다() { - // given - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - ObjectId categoryId = new ObjectId("66ce18d84cb7d0b29ce602f5"); - String title = "Updated Title"; - String content = "Updated Content"; - String thumbnailImageUrl = "https://dy1vg9emkijkn.cloudfront.net/techinfo/19cc111f-c8f4-4d64-bd7a-129415e3ffa2.jpg"; - List contentImageUrls = List.of( - "https://dy1vg9emkijkn.cloudfront.net/techinfo/7635bb80-416a-4042-a901-552df46351a8.png", - "https://dy1vg9emkijkn.cloudfront.net/techinfo/50d081ca-b2f5-4162-926f-0f061aec2554.png" - ); - - // when - Post result = postCommandRepository.update(postId, categoryId, title, content, thumbnailImageUrl, contentImageUrls); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(postId.toHexString()); - } - - @Test - void 존재하지_않는_게시글_상세_업데이트시_예외가_발생한다() { - // given - ObjectId nonExistentPostId = new ObjectId("000000000000000000000000"); - ObjectId categoryId = new ObjectId("66ce18d84cb7d0b29ce602f5"); - String title = "Non-existent Title"; - String content = "Non-existent Content"; - String thumbnailImageUrl = "https://example.com/thumbnail.jpg"; - List contentImageUrls = List.of("https://example.com/image1.jpg"); - - // when & then - assertThatThrownBy( - () -> postCommandRepository.update(nonExistentPostId, categoryId, title, content, thumbnailImageUrl, contentImageUrls) - ).isInstanceOf(RestApiException.class); - } - - @Test - void 게시글_ID와_공개_상태가_주어졌을_때_상태가_업데이트된다() { - // given - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - boolean status = false; - - // when - Post result = postCommandRepository.update(postId, status); - - assertThat(result).isNotNull(); - assertThat(result.getIsPublic()).isFalse(); - } - - @Test - void 존재하지_않는_게시글_공개_상태_업데이트시_예외가_발생한다() { - // given - ObjectId nonExistentPostId = new ObjectId("000000000000000000000001"); - boolean status = false; - - // when & then - assertThatThrownBy(() -> - postCommandRepository.update(nonExistentPostId, status) - ).isInstanceOf(RestApiException.class); - } - - @Test - void 게시글_ID와_특정_필드값이_주어졌을_때_필드가_업데이트된다() { - // given - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - String fieldName = "view_count"; - int value = 1; - - // when - Post result = postCommandRepository.updateFieldWithValue(postId, fieldName, value); - - // then - assertThat(result).isNotNull(); - assertThat(result.getViewCount()).isEqualTo(value); - } - - @Test - void 존재하지_않는_게시글_필드값_업데이트시_예외가_발생한다() { - // Given - ObjectId nonExistentPostId = new ObjectId("000000000000000000000002"); - String fieldName = "view_count"; - int value = 1; - - // When & Then - assertThatThrownBy(() -> - postCommandRepository.updateFieldWithValue(nonExistentPostId, fieldName, value) - ).isInstanceOf(RestApiException.class); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/post/PostReadRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/post/PostReadRepositoryTest.java deleted file mode 100644 index 67356e2b..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/post/PostReadRepositoryTest.java +++ /dev/null @@ -1,144 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.post; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.domain.post.Post; -import darkoverload.itzip.feature.techinfo.dto.post.YearlyPostStats; -import darkoverload.itzip.feature.techinfo.mock.PostMockData; -import darkoverload.itzip.feature.techinfo.service.post.port.PostCommandRepository; -import darkoverload.itzip.feature.techinfo.service.post.port.PostReadRepository; -import darkoverload.itzip.feature.techinfo.type.SortType; -import darkoverload.itzip.feature.techinfo.util.SortUtil; -import org.bson.types.ObjectId; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.test.context.ActiveProfiles; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -@SpringBootTest -@ActiveProfiles("test") -class PostReadRepositoryTest { - - @Autowired - private PostReadRepository postReadRepository; - - @BeforeAll - static void setUp( - @Autowired - PostCommandRepository postCommandRepository - ) { - postCommandRepository.saveAll( - List.of( - PostMockData.postDataOne, - PostMockData.postDataSecond, - PostMockData.postDataThree, - PostMockData.postDataFour, - PostMockData.postDataFive - ) - ); - } - - @AfterAll - static void tearDown(@Autowired PostCommandRepository postCommandRepository) { - postCommandRepository.deleteAll(); - } - - @Test - void ID로_게시글을_조회한다() { - // given - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - - // when - Optional result = postReadRepository.findById(postId); - - // then - assertThat(result).isPresent(); - assertThat(result.get().getId()).isEqualTo(PostMockData.postDataFive.getId()); - } - - @Test - void 모든_공개_게시글을_페이징하여_조회한다() { - // given - int page = 0; - int size = 12; - Sort sort = SortUtil.getType(SortType.NEWEST); - Pageable pageable = PageRequest.of(page, size, sort); - - // when - Page result = postReadRepository.findAll(pageable); - - // then - assertThat(result).isNotNull(); - assertThat(result.getTotalElements()).isEqualTo(5); - } - - @Test - void 블로그_ID로_게시글을_페이징하여_조회한다() { - // given - Long blogId = 100L; - int page = 0; - int size = 12; - Sort sort = SortUtil.getType(SortType.NEWEST); - Pageable pageable = PageRequest.of(page, size, sort); - - // when - Page result = postReadRepository.findPostsByBlogId(blogId, pageable); - - // then - assertThat(result).isNotNull(); - assertThat(result.getTotalElements()).isEqualTo(5); - } - - @Test - void 카테고리_ID로_게시글을_페이징하여_조회한다() { - // given - ObjectId categoryId = new ObjectId("66ce18d84cb7d0b29ce602f5"); - int page = 0; - int size = 12; - Sort sort = SortUtil.getType(SortType.NEWEST); - Pageable pageable = PageRequest.of(page, size, sort); - - // when - Page result = postReadRepository.findPostsByCategoryId(categoryId, pageable); - - // then - assertThat(result.getTotalElements()).isEqualTo(5); - } - - @Test - void 날짜_범위로_게시글을_조회한다() { - // given - Long blogId = 100L; - LocalDateTime date = LocalDateTime.now(); - int limit = 4; - - // when - List result = postReadRepository.findPostsByDateRange(blogId, date, limit); - - // then - assertThat(result).isNotNull(); - assertThat(result).hasSize(4); - } - - @Test - void 블로그_ID로_연간_게시글_통계를_조회한다() { - // given - Long blogId = 100L; - - // when - List result = postReadRepository.findYearlyPostStatsByBlogId(blogId); - - // then - assertThat(result).isNotNull(); - assertThat(result.getFirst().getYear()).isEqualTo(2025); - } - -} \ No newline at end of file diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapCacheRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapCacheRepositoryTest.java deleted file mode 100644 index 46e7e378..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapCacheRepositoryTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.scrap; - -import static org.assertj.core.api.Assertions.assertThat; - -import darkoverload.itzip.feature.techinfo.dto.scrap.ScrapStatus; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapCacheRepository; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import java.util.List; - -@SpringBootTest -@ActiveProfiles("test") -class ScrapCacheRepositoryTest { - - @Autowired - private ScrapCacheRepository scrapCacheRepository; - - private final Long userId = 100L; - private final String postId = "675979e6605cda1eaf5d4c17"; - - @BeforeEach - void setUp() { - scrapCacheRepository.save(userId, postId, true, 90); - } - - @AfterEach - void tearDown() { - scrapCacheRepository.deleteAll(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_저장된다() { - // given - Long userId = 101L; - String postId = "675979e6605cda1eaf5d4c17"; - - // when - scrapCacheRepository.save(userId, postId, true, 90); - - // then - Boolean scrapStatus = scrapCacheRepository.getScrapStatus(userId, postId); - assertThat(scrapStatus).isNotNull(); - assertThat(scrapStatus).isTrue(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_스크랩_상태를_조회한다() { - // when - Boolean scrapStatus = scrapCacheRepository.getScrapStatus(userId, postId); - - // then - assertThat(scrapStatus).isNotNull(); - assertThat(scrapStatus).isTrue(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_스크랩_상태가_없으면_null을_반환한다() { - // given - Long nonExistentUserId = 102L; - String nonExistentPostId = "675979e6605cda1eaf5d4c18"; - - // when - Boolean scrapStatus = scrapCacheRepository.getScrapStatus(nonExistentUserId, nonExistentPostId); - - // then - assertThat(scrapStatus).isNull(); - } - - @Test - void Redis에_저장된_모든_스크랩_상태를_조회한다() { - // given - scrapCacheRepository.save(103L, "675979e6605cda1eaf5d4c17", true, 90); - - // when - List scrapStatuses = scrapCacheRepository.getAllScrapStatuses(); - - // then - assertThat(scrapStatuses).isNotNull(); - assertThat(scrapStatuses).hasSize(2); - } - -} diff --git a/src/test/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapRepositoryTest.java b/src/test/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapRepositoryTest.java deleted file mode 100644 index 22c5dde2..00000000 --- a/src/test/java/darkoverload/itzip/feature/techinfo/repository/scrap/ScrapRepositoryTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package darkoverload.itzip.feature.techinfo.repository.scrap; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import darkoverload.itzip.feature.techinfo.domain.scrap.Scrap; -import darkoverload.itzip.feature.techinfo.mock.ScrapMockData; -import darkoverload.itzip.feature.techinfo.service.scrap.port.ScrapRepository; -import darkoverload.itzip.global.config.response.exception.RestApiException; -import org.bson.types.ObjectId; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles("test") -public class ScrapRepositoryTest { - - @Autowired - private ScrapRepository scrapRepository; - - @AfterAll - static void tearDown(@Autowired ScrapRepository scrapRepository) { - scrapRepository.deleteAll(); - } - - @Test - void 새로운_스크랩이_주어졌을_때_저장된다() { - // given - String postId = "675979e6605cda1eaf5d4c17"; - - // when - Scrap scrap = scrapRepository.save(ScrapMockData.scrapDataOne); - - // then - assertThat(scrap).isNotNull(); - assertThat(scrap.getPostId()).isEqualTo(postId); - } - - @Test - void 사용자_ID와_스크랩_ID가_주어졌을_때_스크랩이_존재하는지_확인한다() { - // given - Long userId = 101L; - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - scrapRepository.save(ScrapMockData.scrapDataSecond); - - // when - boolean exists = scrapRepository.existsByUserIdAndPostId(userId, postId); - - // then - assertThat(exists).isTrue(); - } - - @Test - void 사용자_ID와_스크랩_ID가_주어졌을_때_스크랩이_존재하지_않으면_false를_반환한다() { - // given - Long userId = 100L; - ObjectId nonExistentPostId = new ObjectId("675979e6605cda1eaf5d4c18"); - - // when - boolean exists = scrapRepository.existsByUserIdAndPostId(userId, nonExistentPostId); - - // then - assertThat(exists).isFalse(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_스크랩을_삭제한다() { - // given - Long userId = 102L; - ObjectId postId = new ObjectId("675979e6605cda1eaf5d4c17"); - scrapRepository.save(ScrapMockData.scrapDataThree); - - // when - scrapRepository.deleteByUserIdAndPostId(userId, postId); - - // then - boolean exists = scrapRepository.existsByUserIdAndPostId(userId, postId); - assertThat(exists).isFalse(); - } - - @Test - void 사용자_ID와_게시글_ID가_주어졌을_때_스크랩이_존재하지_않는_경우_예외가_발생한다() { - // given - Long userId = 100L; - ObjectId nonExistentPostId = new ObjectId("675979e6605cda1eaf5d4c18"); - - // when & then - assertThatThrownBy( - () -> scrapRepository.deleteByUserIdAndPostId(userId, nonExistentPostId) - ).isInstanceOf(RestApiException.class); - } - -} diff --git a/src/test/java/darkoverload/itzip/global/fixture/ArticleFixture.java b/src/test/java/darkoverload/itzip/global/fixture/ArticleFixture.java new file mode 100644 index 00000000..1f5c1a5c --- /dev/null +++ b/src/test/java/darkoverload/itzip/global/fixture/ArticleFixture.java @@ -0,0 +1,50 @@ +package darkoverload.itzip.global.fixture; + +import darkoverload.itzip.feature.techinfo.domain.entity.Article; +import darkoverload.itzip.feature.techinfo.domain.entity.ArticleType; +import org.bson.types.ObjectId; + +import java.time.LocalDateTime; + +/** + * 테스트용 Article 엔티티의 Fixture 데이터를 제공하는 클래스입니다. + * + *

+ * 이 클래스는 Article 관련 테스트에서 사용될 기본 값들을 상수로 정의하고, + * 새 Article 객체와 저장된 Article 객체를 생성하는 헬퍼 메서드를 제공합니다. + *

+ */ +public class ArticleFixture { + + private ArticleFixture() { + } + + public static final ObjectId DEFAULT_ID = new ObjectId("67d2b940d88d2b9236a1fb0e"); + public static final ObjectId NON_EXISTENT_ID = new ObjectId("66e724e50000000000db4e53"); + + public static final ArticleType DEFAULT_TYPE = ArticleType.OTHER; + public static final ArticleType SECOND_TYPE = ArticleType.TECH_AI; + + public static final String DEFAULT_TITLE = "title"; + public static final String DEFAULT_NEW_TITLE = "new title"; + + public static final String DEFAULT_CONTENT = "이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 최종완료"; + public static final String DEFAULT_NEW_CONTENT = "이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 이것은 테스트용 내용입니다. 최종완료"; + + public static final String DEFAULT_THUMBNAIL_URI = ""; + public static final String DEFAULT_NEW_THUMBNAIL_URI = ""; + + public static final long DEFAULT_LIKES_COUNT = 0; + public static final long DEFAULT_VIEW_COUNT = 0; + public static final LocalDateTime DEFAULT_DATE_TIME = LocalDateTime.of(2025, 3, 4, 0, 0, 0); + public static final boolean DEFAULT_DISPLAYED = true; + + public static Article getNewArticle() { + return new Article(BlogFixture.DEFAULT_ID, DEFAULT_TYPE, DEFAULT_TITLE, DEFAULT_CONTENT, DEFAULT_THUMBNAIL_URI, DEFAULT_DISPLAYED); + } + + public static Article getSavedArticle() { + return new Article(DEFAULT_ID, BlogFixture.DEFAULT_ID, DEFAULT_TYPE, DEFAULT_TITLE, DEFAULT_CONTENT, DEFAULT_THUMBNAIL_URI, DEFAULT_LIKES_COUNT, DEFAULT_VIEW_COUNT, DEFAULT_DATE_TIME, DEFAULT_DATE_TIME, DEFAULT_DISPLAYED); + } + +} diff --git a/src/test/java/darkoverload/itzip/global/fixture/BlogFixture.java b/src/test/java/darkoverload/itzip/global/fixture/BlogFixture.java new file mode 100644 index 00000000..4021149a --- /dev/null +++ b/src/test/java/darkoverload/itzip/global/fixture/BlogFixture.java @@ -0,0 +1,32 @@ +package darkoverload.itzip.global.fixture; + +import darkoverload.itzip.feature.techinfo.domain.entity.Blog; + +import java.time.LocalDateTime; + +/** + * 테스트용 Blog 엔티티의 Fixture 데이터를 제공하는 클래스입니다. + * + *

+ * 이 클래스는 Blog 관련 테스트에서 사용될 기본 값들을 상수로 정의하고, + * Blog 객체를 생성하는 헬퍼 메서드를 제공합니다. + *

+ */ +public class BlogFixture { + + private BlogFixture() { + } + + public static final long DEFAULT_ID = 999L; + public static final long SECOND_ID = 1000L; + + public static final String DEFAULT_INTRO = "당신만의 블로그 소개글을 작성해주세요."; + public static final String DEFAULT_NEW_INTRO = "새로운 블로그 소개글 입니다."; + + public static final LocalDateTime DEFAULT_DATE_TIME = LocalDateTime.of(2025, 3, 4, 0, 0, 0); + + public static Blog getBlog() { + return new Blog(UserFixture.getUser(), DEFAULT_INTRO); + } + +} diff --git a/src/test/java/darkoverload/itzip/global/fixture/CommentFixture.java b/src/test/java/darkoverload/itzip/global/fixture/CommentFixture.java new file mode 100644 index 00000000..3fdc0537 --- /dev/null +++ b/src/test/java/darkoverload/itzip/global/fixture/CommentFixture.java @@ -0,0 +1,31 @@ +package darkoverload.itzip.global.fixture; + +import darkoverload.itzip.feature.techinfo.domain.entity.Comment; + +import java.time.LocalDateTime; + +/** + * 테스트용 Comment 엔티티의 Fixture 데이터를 제공하는 클래스입니다. + * + *

+ * Comment 관련 테스트에서 사용될 기본 값들을 상수로 정의하고, + * Comment 객체를 생성하는 헬퍼 메서드를 제공합니다. + *

+ */ +public class CommentFixture { + + private CommentFixture() { + } + + public static final long DEFAULT_ID = 999; + public static final long NON_EXISTENT_COMMENT_ID = -1; + public static final String DEFAULT_CONTENT = "content"; + public static final String DEFAULT_NEW_CONTENT = "new content"; + public static final LocalDateTime DEFAULT_DATE_TIME = LocalDateTime.of(2025, 3, 4, 0, 0, 0); + public static final boolean DEFAULT_DISPLAYED = true; + + public static Comment getComment() { + return new Comment(UserFixture.getUser(), ArticleFixture.DEFAULT_ID.toHexString(), DEFAULT_CONTENT, DEFAULT_DISPLAYED); + } + +} diff --git a/src/test/java/darkoverload/itzip/global/fixture/CustomUserDetailsFixture.java b/src/test/java/darkoverload/itzip/global/fixture/CustomUserDetailsFixture.java new file mode 100644 index 00000000..75f59876 --- /dev/null +++ b/src/test/java/darkoverload/itzip/global/fixture/CustomUserDetailsFixture.java @@ -0,0 +1,23 @@ +package darkoverload.itzip.global.fixture; + +import darkoverload.itzip.feature.jwt.infrastructure.CustomUserDetails; + +import java.util.List; + +/** + * 테스트용 CustomUserDetails 객체를 제공하는 Fixture 클래스입니다. + * + *

+ * 사용자 인증 정보 관련 테스트에서 사용될 CustomUserDetails 객체를 생성하며 반환합니다. + *

+ */ +public class CustomUserDetailsFixture { + + private CustomUserDetailsFixture() { + } + + public static CustomUserDetails getCustomUserDetails() { + return new CustomUserDetails(UserFixture.DEFAULT_EMAIL, UserFixture.DEFAULT_PASSWORD, UserFixture.DEFAULT_NICKNAME, List.of(UserFixture.DEFAULT_AUTHORITY)); + } + +} diff --git a/src/test/java/darkoverload/itzip/global/fixture/LikeFixture.java b/src/test/java/darkoverload/itzip/global/fixture/LikeFixture.java new file mode 100644 index 00000000..6331a604 --- /dev/null +++ b/src/test/java/darkoverload/itzip/global/fixture/LikeFixture.java @@ -0,0 +1,27 @@ +package darkoverload.itzip.global.fixture; + +import darkoverload.itzip.feature.techinfo.domain.entity.Like; + +import java.time.LocalDateTime; + +/** + * 테스트용 Like 엔티티의 Fixture 데이터를 제공하는 유틸리티 클래스이다. + * + *

+ * Like 관련 테스트에서 사용될 기본 값들을 상수로 정의하고, + * Like 객체를 생성하는 헬퍼 메서드를 제공합니다. + *

+ */ +public class LikeFixture { + + private LikeFixture() { + } + + public static final long DEFAULT_ID = 999L; + public static final LocalDateTime DEFAULT_DATE_TIME = LocalDateTime.of(2025, 3, 4, 0, 0, 0); + + public static Like getLike() { + return new Like(UserFixture.getUser(), ArticleFixture.DEFAULT_ID.toHexString()); + } + +} diff --git a/src/test/java/darkoverload/itzip/global/fixture/ScrapFixture.java b/src/test/java/darkoverload/itzip/global/fixture/ScrapFixture.java new file mode 100644 index 00000000..2801f135 --- /dev/null +++ b/src/test/java/darkoverload/itzip/global/fixture/ScrapFixture.java @@ -0,0 +1,27 @@ +package darkoverload.itzip.global.fixture; + +import darkoverload.itzip.feature.techinfo.domain.entity.Scrap; + +import java.time.LocalDateTime; + +/** + * 테스트용 Scrap 엔티티의 Fixture 데이터를 제공하는 클래스입니다. + * + *

+ * Scrap 관련 테스트에 사용될 기본 값들을 상수로 정의하고, + * Scrap 객체를 생성하는 헬퍼 메서드를 제공합니다. + *

+ */ +public class ScrapFixture { + + private ScrapFixture() { + } + + public static final long DEFAULT_ID = 999L; + public static final LocalDateTime DEFAULT_DATE_TIME = LocalDateTime.of(2025, 3, 4, 0, 0, 0); + + public static Scrap getScrap() { + return new Scrap(UserFixture.getUser(), ArticleFixture.DEFAULT_ID.toHexString()); + } + +} diff --git a/src/test/java/darkoverload/itzip/global/fixture/UserFixture.java b/src/test/java/darkoverload/itzip/global/fixture/UserFixture.java new file mode 100644 index 00000000..590995c3 --- /dev/null +++ b/src/test/java/darkoverload/itzip/global/fixture/UserFixture.java @@ -0,0 +1,43 @@ +package darkoverload.itzip.global.fixture; + +import darkoverload.itzip.feature.user.entity.Authority; +import darkoverload.itzip.feature.user.entity.UserEntity; + +import java.time.LocalDateTime; + +/** + * 테스트용 UserEntity의 Fixture 데이터를 제공하는 클래스입니다. + * + *

+ * User 관련 테스트에서 사용될 기본 값들을 상수로 정의하고, + * UserEntity 객체를 생성하는 헬퍼 메서드를 제공합니다. + *

+ */ +public class UserFixture { + + private UserFixture() { + } + + public static final Long DEFAULT_ID = 999L; + public static final Long SECOND_ID = 1000L; + public static final Long NON_EXISTENT_ID = -1L; + + public static final String DEFAULT_EMAIL = "dev.hyoseung@gmail.com"; + public static final String SECOND_EMAIL = "20181189@vision.hoseo.edu"; + public static final String NON_EXISTENT_EMAIL = "hoohoot0225@gmail.com"; + + public static final String DEFAULT_NICKNAME = "hyoseung"; + public static final String SECOND_NICKNAME = "rowing"; + public static final String NON_EXISTENT_NICKNAME = "hoohoot0225"; + + public static final String DEFAULT_PASSWORD = "password"; + public static final String DEFAULT_PROFILE_IMAGE_URI = ""; + public static final Authority DEFAULT_AUTHORITY = Authority.USER; + public static final LocalDateTime DEFAULT_DATE_TIME = LocalDateTime.of(2025, 3, 4, 0, 0, 0); + public static final String DEFAULT_SNS_TYPE = ""; + + public static UserEntity getUser() { + return new UserEntity(DEFAULT_EMAIL, DEFAULT_NICKNAME, DEFAULT_PASSWORD, DEFAULT_PROFILE_IMAGE_URI, DEFAULT_AUTHORITY, DEFAULT_DATE_TIME, DEFAULT_DATE_TIME); + } + +} diff --git a/src/test/java/darkoverload/itzip/sample/SampleMongoTest.java b/src/test/java/darkoverload/itzip/sample/SampleMongoTest.java deleted file mode 100644 index 9ea87199..00000000 --- a/src/test/java/darkoverload/itzip/sample/SampleMongoTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package darkoverload.itzip.sample; - - -import darkoverload.itzip.mongo.sample.repository.SampleMongoRepository; -import darkoverload.itzip.mongo.sample.domain.SampleMongo; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import static org.assertj.core.api.Assertions.assertThat; - - -@SpringBootTest -@ActiveProfiles("test") -public class SampleMongoTest { - - private SampleMongo sampleMongo; - - @Autowired - private SampleMongoRepository repository; - - @BeforeEach - void setUp() { - sampleMongo = new SampleMongo("SAMPLE00001", "샘플 테스트"); - } - @AfterEach - void afterEach(){ - repository.deleteAll(); //모두 롤백 - } - - @Test - void mongo_save() { - repository.save(sampleMongo); - SampleMongo persistSample = repository.findById(sampleMongo.getId()).orElseThrow(RuntimeException::new); - - assertThat(persistSample.getId()).isEqualTo(sampleMongo.getId()); - assertThat(persistSample.getName()).isEqualTo(sampleMongo.getName()); - } - - @Test - void mongo_update() { - repository.save(sampleMongo); - SampleMongo persistSample = repository.findById(sampleMongo.getId()).orElseThrow(RuntimeException::new); - - persistSample.changeName("샘플 테스트 수정"); - repository.save(persistSample); - - SampleMongo findById = repository.findById(persistSample.getId()).orElseThrow(RuntimeException::new); - assertThat(findById.getName()).isEqualTo("샘플 테스트 수정"); - } - -} diff --git a/src/test/java/darkoverload/itzip/sample/SampleQueryDslTest.java b/src/test/java/darkoverload/itzip/sample/SampleQueryDslTest.java deleted file mode 100644 index 286bef58..00000000 --- a/src/test/java/darkoverload/itzip/sample/SampleQueryDslTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package darkoverload.itzip.sample; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import darkoverload.itzip.postgresql.sample.HelloEntity; -import darkoverload.itzip.postgresql.sample.QHelloEntity; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.*; - -@Transactional -@SpringBootTest -@ActiveProfiles("test") -public class SampleQueryDslTest { - - @Autowired - EntityManager em; - - @Test - void queryDslTest(){ - HelloEntity helloEntity = new HelloEntity(); - em.persist(helloEntity); - - JPAQueryFactory queryFactory = new JPAQueryFactory(em); - QHelloEntity qHello = QHelloEntity.helloEntity; //Querydsl Q타입 동작 확인 - - HelloEntity result = queryFactory - .selectFrom(qHello) - .fetchOne(); - - assertThat(result).isEqualTo(helloEntity); - assertThat(result.getId()).isEqualTo(helloEntity.getId()); - } - -} \ No newline at end of file diff --git a/src/test/java/darkoverload/itzip/sample/SampleRedisTest.java b/src/test/java/darkoverload/itzip/sample/SampleRedisTest.java deleted file mode 100644 index 947f968f..00000000 --- a/src/test/java/darkoverload/itzip/sample/SampleRedisTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package darkoverload.itzip.sample; - -import darkoverload.itzip.sample.Repository.SampleRedisRepository; -import darkoverload.itzip.sample.domain.SampleRedis; -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; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; - -@DisplayName("RedisSample CRUD Test") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("test") -public class SampleRedisTest { - - @Autowired - private SampleRedisRepository repository; - - private SampleRedis sampleRedis; - - @BeforeEach - void setUp() { - sampleRedis = new SampleRedis("SAMPLE00001", "샘플 테스트"); - } - - @AfterEach - void setDown() { - repository.deleteById(sampleRedis.getId()); - } - - @Test - @DisplayName("Redis에 데이터를 저장") - void redis_save(){ - //given - repository.save(sampleRedis); - - // when - SampleRedis persistSample = repository.findById(sampleRedis.getId()).orElseThrow(RuntimeException::new); - - // then - assertThat(persistSample.getId()).isEqualTo(sampleRedis.getId()); - assertThat(persistSample.getName()).isEqualTo(sampleRedis.getName()); - } - - @Test - @DisplayName("Redis 데이터 수정") - void redis_update(){ - //given - repository.save(sampleRedis); - SampleRedis persistSample = repository.findById(sampleRedis.getId()).orElseThrow(RuntimeException::new); - - //when - persistSample.changeName("샘플 테스트 수정"); - repository.save(persistSample); - - //then - assertThat(persistSample.getName()).isEqualTo("샘플 테스트 수정"); - } - - - @Test - @DisplayName("Redis 데이터 삭제") - void redis_delete(){ - //given - repository.save(sampleRedis); - - //when - repository.delete(sampleRedis); - Optional deletedSampleRedis = repository.findById(sampleRedis.getId()); - - // then - assertThat(deletedSampleRedis.isEmpty()).isTrue(); - } -} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index b25ef62c..dea849d0 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -39,6 +39,7 @@ spring: database: ${TEST_MONGO_DATABASE} authentication-database: ${TEST_MONGO_AUTH} uri: ${TEST_MONGO_URI} + auto-index-creation: true mail: host: smtp.gmail.com diff --git a/src/test/resources/sql/techinfo/blog-repository-test-data.sql b/src/test/resources/sql/techinfo/blog-repository-test-data.sql deleted file mode 100644 index 702e18fa..00000000 --- a/src/test/resources/sql/techinfo/blog-repository-test-data.sql +++ /dev/null @@ -1,7 +0,0 @@ - -insert into users(id, authority, email, nickname, password, create_date, modify_date) -values (100, 'USER', 'test@test.com', '아름다운 135번째 돌고래', '$2a$10$5RifHVaUMq.7IXyJK40kpuaWzfhRBsPgdq1CAhB6LGXdwbxep0.Ba', - '2024-07-22 10:13:19.129274', '2024-07-22 10:13:19.129274'); - -INSERT INTO blogs(id, user_id, intro, is_public, create_date, modify_date) -values (100, 100, '블로그 소개 예시입니다.', true, '2024-07-23 10:13:19.129274', '2024-07-23 10:13:19.129274'); diff --git a/src/test/resources/sql/techinfo/custom/custom-delete-blog-data.sql b/src/test/resources/sql/techinfo/custom/custom-delete-blog-data.sql new file mode 100644 index 00000000..6c1e49a8 --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-delete-blog-data.sql @@ -0,0 +1,4 @@ + +DELETE FROM blogs WHERE user_id IN (999, 1000); + +DELETE FROM users WHERE id IN (999, 1000); diff --git a/src/test/resources/sql/techinfo/custom/custom-delete-comment-data.sql b/src/test/resources/sql/techinfo/custom/custom-delete-comment-data.sql new file mode 100644 index 00000000..43a755b3 --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-delete-comment-data.sql @@ -0,0 +1,4 @@ + +DELETE FROM comments WHERE user_id IN (999, 1000); + +DELETE FROM users WHERE id IN (999, 1000); diff --git a/src/test/resources/sql/techinfo/custom/custom-delete-like-data.sql b/src/test/resources/sql/techinfo/custom/custom-delete-like-data.sql new file mode 100644 index 00000000..d613d4b1 --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-delete-like-data.sql @@ -0,0 +1,4 @@ + +DELETE FROM likes WHERE user_id IN (999, 1000); + +DELETE FROM users WHERE id IN (999, 1000); diff --git a/src/test/resources/sql/techinfo/custom/custom-delete-scrap-data.sql b/src/test/resources/sql/techinfo/custom/custom-delete-scrap-data.sql new file mode 100644 index 00000000..32c2df1f --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-delete-scrap-data.sql @@ -0,0 +1,4 @@ + +DELETE FROM scraps WHERE user_id IN (999, 1000); + +DELETE FROM users WHERE id IN (999, 1000); diff --git a/src/test/resources/sql/techinfo/custom/custom-insert-blog-data.sql b/src/test/resources/sql/techinfo/custom/custom-insert-blog-data.sql new file mode 100644 index 00000000..d3eaec20 --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-insert-blog-data.sql @@ -0,0 +1,9 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (1000, '20181189@vision.hoseo.edu', 'rowing', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO blogs(user_id, intro, created_at, updated_at) +VALUES (999, '당신만의 블로그 소개글을 작성해주세요.', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); diff --git a/src/test/resources/sql/techinfo/custom/custom-insert-comment-data.sql b/src/test/resources/sql/techinfo/custom/custom-insert-comment-data.sql new file mode 100644 index 00000000..6513d8a6 --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-insert-comment-data.sql @@ -0,0 +1,9 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (1000, '20181189@vision.hoseo.edu', 'rowing', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO comments(id, user_id, article_id, content, created_at, updated_at, is_displayed) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', 'content', '2025-03-04 00:00:00', '2025-03-04 00:00:00', TRUE); diff --git a/src/test/resources/sql/techinfo/custom/custom-insert-like-data.sql b/src/test/resources/sql/techinfo/custom/custom-insert-like-data.sql new file mode 100644 index 00000000..9f2d2c55 --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-insert-like-data.sql @@ -0,0 +1,9 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (1000, '20181189@vision.hoseo.edu', 'rowing', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO likes(id, user_id, article_id, created_at) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', '2025-03-04 00:00:00'); diff --git a/src/test/resources/sql/techinfo/custom/custom-insert-scrap-data.sql b/src/test/resources/sql/techinfo/custom/custom-insert-scrap-data.sql new file mode 100644 index 00000000..6f10f508 --- /dev/null +++ b/src/test/resources/sql/techinfo/custom/custom-insert-scrap-data.sql @@ -0,0 +1,9 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (1000, '20181189@vision.hoseo.edu', 'rowing', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO scraps(id, user_id, article_id, created_at) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', '2025-03-04 00:00:00'); diff --git a/src/test/resources/sql/techinfo/default-delete-all-data.sql b/src/test/resources/sql/techinfo/default-delete-all-data.sql new file mode 100644 index 00000000..f7bcc664 --- /dev/null +++ b/src/test/resources/sql/techinfo/default-delete-all-data.sql @@ -0,0 +1,10 @@ + +DELETE FROM scraps WHERE id = 999; + +DELETE FROM likes WHERE id = 999; + +DELETE FROM comments WHERE id = 999; + +DELETE FROM blogs WHERE user_id = 999; + +DELETE FROM users WHERE id = 999; diff --git a/src/test/resources/sql/techinfo/default-insert-article-data.sql b/src/test/resources/sql/techinfo/default-insert-article-data.sql new file mode 100644 index 00000000..441a0b3b --- /dev/null +++ b/src/test/resources/sql/techinfo/default-insert-article-data.sql @@ -0,0 +1,12 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO blogs(user_id, intro, created_at, updated_at) +VALUES (999, '당신만의 블로그 소개글을 작성해주세요.', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO scraps(id, user_id, article_id, created_at) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', '2025-03-04 00:00:00'); + +INSERT INTO likes(id, user_id, article_id, created_at) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', '2025-03-04 00:00:00'); diff --git a/src/test/resources/sql/techinfo/default-insert-blog-data.sql b/src/test/resources/sql/techinfo/default-insert-blog-data.sql new file mode 100644 index 00000000..e00a22f0 --- /dev/null +++ b/src/test/resources/sql/techinfo/default-insert-blog-data.sql @@ -0,0 +1,6 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO blogs(user_id, intro, created_at, updated_at) +VALUES (999, '당신만의 블로그 소개글을 작성해주세요.', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); diff --git a/src/test/resources/sql/techinfo/default-insert-comment-data.sql b/src/test/resources/sql/techinfo/default-insert-comment-data.sql new file mode 100644 index 00000000..f952bdd9 --- /dev/null +++ b/src/test/resources/sql/techinfo/default-insert-comment-data.sql @@ -0,0 +1,6 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO comments(id, user_id, article_id, content, created_at, updated_at, is_displayed) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', 'content', '2025-03-04 00:00:00', '2025-03-04 00:00:00', TRUE); diff --git a/src/test/resources/sql/techinfo/default-insert-like-data.sql b/src/test/resources/sql/techinfo/default-insert-like-data.sql new file mode 100644 index 00000000..debbff0b --- /dev/null +++ b/src/test/resources/sql/techinfo/default-insert-like-data.sql @@ -0,0 +1,6 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO likes(id, user_id, article_id, created_at) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', '2025-03-04 00:00:00'); diff --git a/src/test/resources/sql/techinfo/default-insert-scrap-data.sql b/src/test/resources/sql/techinfo/default-insert-scrap-data.sql new file mode 100644 index 00000000..6780c586 --- /dev/null +++ b/src/test/resources/sql/techinfo/default-insert-scrap-data.sql @@ -0,0 +1,6 @@ + +INSERT INTO users(id, email, nickname, password, image_url, authority, sns_type, create_date, modify_date) +VALUES (999, 'dev.hyoseung@gmail.com', 'hyoseung', 'password', '', 'USER', '', '2025-03-04 00:00:00', '2025-03-04 00:00:00'); + +INSERT INTO scraps(id, user_id, article_id, created_at) +VALUES (999, 999, '67d2b940d88d2b9236a1fb0e', '2025-03-04 00:00:00'); diff --git a/src/test/resources/sql/techinfo/delete-all-data.sql b/src/test/resources/sql/techinfo/delete-all-data.sql deleted file mode 100644 index d0037b1d..00000000 --- a/src/test/resources/sql/techinfo/delete-all-data.sql +++ /dev/null @@ -1,4 +0,0 @@ - -DELETE FROM blogs where user_id = 100; - -DELETE FROM users where id = 100;