From 819c291573db8c60c94256b1108dcbc8bedc85ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=EC=83=81=EC=9D=80?= Date: Tue, 9 Sep 2025 02:07:48 +0900 Subject: [PATCH 1/2] feat(article): implement patch usecases in application layer for title, path, and series article --- .../article/exception/DraftErrorCode.java | 6 ++- .../application/port/DraftCommandPort.java | 11 ++++- .../service/DraftCommandService.java | 46 +++++++++++++++++-- .../usecase/DraftPatchUseCase.java | 13 ++++++ .../rdb/persistence/DraftCommandAdapter.java | 2 +- 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java diff --git a/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java b/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java index 4ca85613..3655852c 100644 --- a/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java +++ b/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java @@ -7,6 +7,7 @@ import java.util.function.Supplier; public enum DraftErrorCode implements ErrorCode { + // user input DRAFT_BLOG_ID_REQUIRED("블로그 ID를 반드시 제공해야 합니다.", HttpStatus.BAD_REQUEST), DRAFT_TITLE_MIN_LENGTH("블로그 제목은 반드시 3글자 이상입니다.", HttpStatus.BAD_REQUEST), @@ -19,7 +20,10 @@ public enum DraftErrorCode implements ErrorCode { DRAFT_GONE("더 이상 존재하지 않는 게시물입니다.", HttpStatus.GONE), DRAFT_FORBIDDEN("권한이 없습니다.", HttpStatus.FORBIDDEN), DRAFT_ALREADY_EXIST("임시글이 이미 존재합니다.", HttpStatus.CONFLICT), - DEFAULT("임시글 조작 오류", HttpStatus.INTERNAL_SERVER_ERROR); + DEFAULT("임시글 조작 오류", HttpStatus.INTERNAL_SERVER_ERROR), + + // series status + SERIES_ARTICLE_NOT_FOUND("시리즈 아티클을 찾을 수 없습니다.", HttpStatus.NOT_FOUND); private final String message; private final HttpStatus httpStatus; diff --git a/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java b/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java index b208253e..7077c37b 100644 --- a/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java +++ b/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java @@ -2,6 +2,7 @@ import nettee.blolet.article.domain.Draft; import nettee.blolet.article.domain.DraftImage; +import nettee.blolet.article.domain.SeriesArticle; import nettee.blolet.article.domain.sub.DraftStatus; import nettee.blolet.article.readmodel.DraftReadModels.DraftDetail; @@ -9,7 +10,9 @@ public interface DraftCommandPort { - Optional findById(String id); + Optional findDraftById(String id); + + Optional findSeriesArticleById(String id); Draft save(Draft draft); @@ -18,4 +21,10 @@ public interface DraftCommandPort { void updateStatus(String id, DraftStatus draftStatus); DraftImage save(DraftImage draftImage); + + Draft updateTitle(String draftId, String title); + + Draft updatePath(String draftId, String path); + + SeriesArticle updateSeriesArticle(String draftId, String seriesId, String articleId); } diff --git a/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java b/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java index 3e5ba83f..6b1150a2 100644 --- a/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java +++ b/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java @@ -5,9 +5,11 @@ import nettee.blolet.article.application.usecase.DraftCreateUseCase; import nettee.blolet.article.application.usecase.DraftDeleteUseCase; import nettee.blolet.article.application.usecase.DraftImageCreateUseCase; +import nettee.blolet.article.application.usecase.DraftPatchUseCase; import nettee.blolet.article.application.usecase.DraftUpdateUseCase; import nettee.blolet.article.domain.Draft; import nettee.blolet.article.domain.DraftImage; +import nettee.blolet.article.domain.SeriesArticle; import nettee.blolet.article.domain.sub.DraftStatus; import nettee.blolet.blog.export.client.api.BlogClient; import nettee.upload.port.ImageStorage; @@ -16,10 +18,18 @@ import static nettee.blolet.article.exception.DraftErrorCode.DRAFT_FORBIDDEN; import static nettee.blolet.article.exception.DraftErrorCode.DRAFT_NOT_FOUND; +import static nettee.blolet.article.exception.DraftErrorCode.SERIES_ARTICLE_NOT_FOUND; @Service @RequiredArgsConstructor -public class DraftCommandService implements DraftCreateUseCase, DraftUpdateUseCase, DraftDeleteUseCase, DraftImageCreateUseCase { +public class DraftCommandService implements + DraftCreateUseCase, + DraftUpdateUseCase, + DraftDeleteUseCase, + DraftImageCreateUseCase, + DraftPatchUseCase +{ + private final DraftCommandPort draftCommandPort; private final BlogClient blogClient; private final ImageStorage imageStorage; @@ -38,7 +48,7 @@ public Draft updateDraft(String userId, Draft draft) { @Override public void deleteDraft(String userId, String draftId) { - var blogId = draftCommandPort.findById(draftId) + var blogId = draftCommandPort.findDraftById(draftId) .orElseThrow(DRAFT_NOT_FOUND::exception) .blogId(); validateOwnership(userId, blogId); @@ -48,7 +58,7 @@ public void deleteDraft(String userId, String draftId) { @Override public DraftImage createDraftImage(String userId, String draftId, MultipartFile file, String targetName) { - var blogId = draftCommandPort.findById(draftId) + var blogId = draftCommandPort.findDraftById(draftId) .orElseThrow(DRAFT_NOT_FOUND::exception) .blogId(); validateOwnership(userId, blogId); @@ -72,4 +82,34 @@ private void validateOwnership(String userId, String draftId) { throw DRAFT_FORBIDDEN.exception(); } } + + @Override + public Draft patchTitle(String userId, String draftId, String title) { + var draft = draftCommandPort.findDraftById(draftId) + .orElseThrow(DRAFT_NOT_FOUND::exception); + + validateOwnership(userId, draft.blogId()); + + return draftCommandPort.updateTitle(draft.id(), title); + } + + @Override + public Draft patchPath(String userId, String draftId, String path) { + var draft = draftCommandPort.findDraftById(draftId) + .orElseThrow(DRAFT_NOT_FOUND::exception); + + validateOwnership(userId, draft.blogId()); + + return draftCommandPort.updatePath(draft.id(), path); + } + + @Override + public SeriesArticle patchSeriesArticle(String userId, String draftId, String seriesId, String articleId) { + var seriesArticle = draftCommandPort.findSeriesArticleById(draftId) + .orElseThrow(SERIES_ARTICLE_NOT_FOUND::exception); + + validateOwnership(userId, draftId); + + return draftCommandPort.updateSeriesArticle(seriesArticle.getDraftId(), seriesId, articleId); + } } diff --git a/services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java b/services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java new file mode 100644 index 00000000..01cde93a --- /dev/null +++ b/services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java @@ -0,0 +1,13 @@ +package nettee.blolet.article.application.usecase; + +import nettee.blolet.article.domain.Draft; +import nettee.blolet.article.domain.SeriesArticle; + +public interface DraftPatchUseCase { + + Draft patchTitle(String userId, String draftId, String title); + + Draft patchPath(String userId, String draftId, String path); + + SeriesArticle patchSeriesArticle(String userId, String draftId, String seriesId, String articleId); +} diff --git a/services/article/driven/rdb/src/main/java/nettee/blolet/article/rdb/persistence/DraftCommandAdapter.java b/services/article/driven/rdb/src/main/java/nettee/blolet/article/rdb/persistence/DraftCommandAdapter.java index 66e5f859..0360f501 100644 --- a/services/article/driven/rdb/src/main/java/nettee/blolet/article/rdb/persistence/DraftCommandAdapter.java +++ b/services/article/driven/rdb/src/main/java/nettee/blolet/article/rdb/persistence/DraftCommandAdapter.java @@ -27,7 +27,7 @@ public class DraftCommandAdapter implements DraftCommandPort { private final DraftEntityMapper mapper; @Override - public Optional findById(String id) { + public Optional findDraftById(String id) { var draft = draftJpaRepository.findById(id) .orElseThrow(DRAFT_NOT_FOUND::exception); return mapper.toOptionalDraftDetail(draft); From 4726c29f8e9476a3d8ff857aa6b2ac049eb57316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=EC=83=81=EC=9D=80?= Date: Sun, 14 Sep 2025 23:16:33 +0900 Subject: [PATCH 2/2] feat(article): apply code review feedback --- .../article/exception/DraftErrorCode.java | 5 +- .../application/port/DraftCommandPort.java | 15 ++--- .../service/DraftCommandService.java | 65 +++++++++++++------ .../usecase/DraftPatchUseCase.java | 2 +- 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java b/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java index 3655852c..e4ba4428 100644 --- a/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java +++ b/services/article/api/exception/src/main/java/nettee/blolet/article/exception/DraftErrorCode.java @@ -21,9 +21,12 @@ public enum DraftErrorCode implements ErrorCode { DRAFT_FORBIDDEN("권한이 없습니다.", HttpStatus.FORBIDDEN), DRAFT_ALREADY_EXIST("임시글이 이미 존재합니다.", HttpStatus.CONFLICT), DEFAULT("임시글 조작 오류", HttpStatus.INTERNAL_SERVER_ERROR), + BLOG_MISMATCH("드래프트와 시리즈가 같은 블로그에 속하지 않습니다.", HttpStatus.BAD_REQUEST), // series status - SERIES_ARTICLE_NOT_FOUND("시리즈 아티클을 찾을 수 없습니다.", HttpStatus.NOT_FOUND); + SERIES_ARTICLE_NOT_FOUND("시리즈 아티클을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + SERIES_NOT_FOUND("시리즈를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + SERIES_ALREADY_REGISTERED("이미 등록된 시리즈 아티클입니다.", HttpStatus.CONFLICT); private final String message; private final HttpStatus httpStatus; diff --git a/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java b/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java index 7077c37b..be6608d4 100644 --- a/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java +++ b/services/article/application/src/main/java/nettee/blolet/article/application/port/DraftCommandPort.java @@ -5,26 +5,25 @@ import nettee.blolet.article.domain.SeriesArticle; import nettee.blolet.article.domain.sub.DraftStatus; import nettee.blolet.article.readmodel.DraftReadModels.DraftDetail; +import nettee.blolet.article.readmodel.SeriesQueryModels.SeriesDetail; import java.util.Optional; public interface DraftCommandPort { Optional findDraftById(String id); + Optional findSeriesById(String seriesId); - Optional findSeriesArticleById(String id); + boolean existsSeriesArticle(String draftId, String seriesId); Draft save(Draft draft); - - Draft update(Draft draft); - - void updateStatus(String id, DraftStatus draftStatus); - DraftImage save(DraftImage draftImage); + SeriesArticle createSeriesArticle(String draftId, String seriesId, String articleId); + Draft update(Draft draft); Draft updateTitle(String draftId, String title); - Draft updatePath(String draftId, String path); - SeriesArticle updateSeriesArticle(String draftId, String seriesId, String articleId); + void updateDraftSeriesInfo(String draftId, String seriesId); + void updateStatus(String id, DraftStatus draftStatus); } diff --git a/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java b/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java index 6b1150a2..466bf511 100644 --- a/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java +++ b/services/article/application/src/main/java/nettee/blolet/article/application/service/DraftCommandService.java @@ -16,19 +16,20 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import static nettee.blolet.article.exception.DraftErrorCode.SERIES_ALREADY_REGISTERED; +import static nettee.blolet.article.exception.DraftErrorCode.BLOG_MISMATCH; import static nettee.blolet.article.exception.DraftErrorCode.DRAFT_FORBIDDEN; import static nettee.blolet.article.exception.DraftErrorCode.DRAFT_NOT_FOUND; -import static nettee.blolet.article.exception.DraftErrorCode.SERIES_ARTICLE_NOT_FOUND; +import static nettee.blolet.article.exception.DraftErrorCode.SERIES_NOT_FOUND; @Service @RequiredArgsConstructor public class DraftCommandService implements - DraftCreateUseCase, - DraftUpdateUseCase, - DraftDeleteUseCase, - DraftImageCreateUseCase, - DraftPatchUseCase -{ + DraftCreateUseCase, + DraftUpdateUseCase, + DraftDeleteUseCase, + DraftImageCreateUseCase, + DraftPatchUseCase { private final DraftCommandPort draftCommandPort; private final BlogClient blogClient; @@ -74,15 +75,6 @@ public DraftImage createDraftImage(String userId, String draftId, MultipartFile return draftCommandPort.save(draftImage); } - private void validateOwnership(String userId, String draftId) { - var isOwner = blogClient.verifyOwnership(userId, draftId) - .isOwner(); - - if (!isOwner) { - throw DRAFT_FORBIDDEN.exception(); - } - } - @Override public Draft patchTitle(String userId, String draftId, String title) { var draft = draftCommandPort.findDraftById(draftId) @@ -104,12 +96,43 @@ public Draft patchPath(String userId, String draftId, String path) { } @Override - public SeriesArticle patchSeriesArticle(String userId, String draftId, String seriesId, String articleId) { - var seriesArticle = draftCommandPort.findSeriesArticleById(draftId) - .orElseThrow(SERIES_ARTICLE_NOT_FOUND::exception); + public SeriesArticle registerSeriesArticle(String userId, String draftId, String seriesId, String articleId) { + // 드래프트, 시리즈 조회 + var draft = draftCommandPort.findDraftById(draftId) + .orElseThrow(DRAFT_NOT_FOUND::exception); + + var series = draftCommandPort.findSeriesById(seriesId) + .orElseThrow(SERIES_NOT_FOUND::exception); + + // 권한 검증 + validateOwnership(userId, draft.blogId()); + validateOwnership(userId, series.blogId()); - validateOwnership(userId, draftId); + // 블로그 동일 여부 확인 + if (!draft.blogId().equals(series.blogId())) { + throw BLOG_MISMATCH.exception(); + } + + // 시리즈 존재 여부 확인 + if (draftCommandPort.existsSeriesArticle(draftId, seriesId)) { + throw SERIES_ALREADY_REGISTERED.exception(); + } + + // 시리즈 등록 + var seriesArticle = draftCommandPort.createSeriesArticle(draftId, seriesId, articleId); + + // 드래프트에 시리즈 정보 업데이트 + draftCommandPort.updateDraftSeriesInfo(draftId, seriesId); + + return seriesArticle; + } - return draftCommandPort.updateSeriesArticle(seriesArticle.getDraftId(), seriesId, articleId); + private void validateOwnership(String userId, String draftId) { + var isOwner = blogClient.verifyOwnership(userId, draftId) + .isOwner(); + + if (!isOwner) { + throw DRAFT_FORBIDDEN.exception(); + } } } diff --git a/services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java b/services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java index 01cde93a..c0ba6461 100644 --- a/services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java +++ b/services/article/application/src/main/java/nettee/blolet/article/application/usecase/DraftPatchUseCase.java @@ -9,5 +9,5 @@ public interface DraftPatchUseCase { Draft patchPath(String userId, String draftId, String path); - SeriesArticle patchSeriesArticle(String userId, String draftId, String seriesId, String articleId); + SeriesArticle registerSeriesArticle(String userId, String draftId, String seriesId, String articleId); }