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