diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 86bb562..6b77e59 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -64,9 +64,8 @@ reviews: - Indentation: use spaces (no tabs), tab width = 4 spaces; files must end with Unix LF newline. (Team adaptation) - Maximum line length: 120 characters. - Imports: single-class imports only; allow wildcard for static imports; group imports with blank lines between sections. - - Assignment operators (`=`, `+=`, `-=`, etc.): always one space before and after; on line breaks, place the operator at the start of the next line. + - Operators: always one space before and after; on line breaks, place operators at the start of the next line (commas stay at end of line, dots at start of new line). - Lambda expressions: omit parentheses for a single parameter; surround `->` with spaces (`param -> expression`); use braces and explicit `return` for multi-statement bodies; choose short, clear parameter names. - - In multi-line expressions, place operators and ternary separators at end-of-line. - Prefer Java 21 standard APIs over Guava. - Do not annotate immutable local variables with `final` unless required for an inner class. - Allow the `var` keyword when the value is a cast `null`. @@ -90,6 +89,47 @@ reviews: - Prioritize correctness of assertions, coverage of edge cases, and clear test naming/documentation. - Be lenient on style and minor optimizations in tests. + - path: "**/application/usecase/**/*.java" + instructions: | + - Use cases must be defined as interfaces with a single public method representing one business operation. + - Method names should clearly express the business intent (e.g., `create`, `delete`, `accept`, `send`). + - Avoid multiple unrelated operations in a single use case interface. + - Keep method signatures simple and focused; use Command or Query objects for complex parameters. + + - path: "**/application/command/**/*.java" + instructions: | + - Commands must be immutable records. + - All input validation must occur in the compact constructor. + - Normalize inputs (e.g., `strip()` for strings) in the compact constructor before validation. + - Provide sensible defaults for optional fields (e.g., `Set.of()` for empty collections). + - Throw `BusinessException` with appropriate `ErrorCode` for validation failures. + - Commands should represent write operations; avoid query-related fields unless necessary for the write operation. + + - path: "**/application/query/**/*.java" + instructions: | + - Queries must be immutable records. + - All input validation must occur in the compact constructor. + - Validate pagination parameters (e.g., `size > 0`, `size <= 100`, `lastId > 0`). + - Throw `BusinessException` with appropriate `ErrorCode` for validation failures. + - Queries should only contain parameters needed for read operations. + + - path: "**/application/result/**/*.java" + instructions: | + - Results must be immutable records. + - Use static factory methods named `from` to convert from domain or query results. + - Results should only expose data needed by the presentation layer; avoid leaking domain internals. + - Prefer composition over deeply nested structures. + + - path: "**/application/service/**/*Service.java" + instructions: | + - Services must be annotated with `@Service` and implement one or more use case interfaces. + - Follow the Read/Write separation pattern: use separate classes for read operations (e.g., `FriendReadService`) and write operations (e.g., `FriendWriteService`). + - Write operations must be annotated with `@Transactional`. + - Read operations should use `@Transactional(readOnly = true)` when appropriate. + - Services should orchestrate domain logic, not contain it; delegate business rules to domain entities. + - Throw `BusinessException` with appropriate `ErrorCode` for business rule violations. + - Avoid direct manipulation of entities; prefer calling domain methods. + abort_on_close: true disable_cache: false diff --git a/.rules/checkstyle-rules.xml b/.rules/checkstyle-rules.xml index d0f2302..78bc416 100755 --- a/.rules/checkstyle-rules.xml +++ b/.rules/checkstyle-rules.xml @@ -356,7 +356,13 @@ The following rules in the Naver coding convention cannot be checked by this con value="[space-after-comma-semicolon]: ''{0}'' is not followed by whitespace."/> - + + + + + + diff --git a/src/main/java/queuing/core/CoreApplication.java b/src/main/java/queuing/core/CoreApplication.java index acaa260..d64cd24 100755 --- a/src/main/java/queuing/core/CoreApplication.java +++ b/src/main/java/queuing/core/CoreApplication.java @@ -7,9 +7,7 @@ @SpringBootApplication @ConfigurationPropertiesScan public class CoreApplication { - - public static void main(String[] args) { - SpringApplication.run(CoreApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(CoreApplication.class, args); + } } diff --git a/src/main/java/queuing/core/friend/application/dto/GetFriendListCommand.java b/src/main/java/queuing/core/friend/application/dto/GetFriendListCommand.java deleted file mode 100644 index 1590af6..0000000 --- a/src/main/java/queuing/core/friend/application/dto/GetFriendListCommand.java +++ /dev/null @@ -1,7 +0,0 @@ -package queuing.core.friend.application.dto; - -public record GetFriendListCommand( - String userSlug, - Long lastId, - int size -) {} diff --git a/src/main/java/queuing/core/friend/application/query/GetListFriendQuery.java b/src/main/java/queuing/core/friend/application/query/GetListFriendQuery.java new file mode 100644 index 0000000..10a179b --- /dev/null +++ b/src/main/java/queuing/core/friend/application/query/GetListFriendQuery.java @@ -0,0 +1,28 @@ +package queuing.core.friend.application.query; + +import queuing.core.global.exception.BusinessException; +import queuing.core.global.exception.ErrorCode; + +public record GetListFriendQuery( + String userSlug, + Long lastId, + int size +) { + public GetListFriendQuery { + if (userSlug == null || userSlug.isBlank()) { + throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); + } + userSlug = userSlug.strip(); + + if (lastId != null && lastId <= 0) { + throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); + } + + if (size <= 0) { + throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); + } + if (size > 100) { + throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); + } + } +} diff --git a/src/main/java/queuing/core/friend/application/dto/FriendSummary.java b/src/main/java/queuing/core/friend/application/result/FriendSummary.java similarity index 88% rename from src/main/java/queuing/core/friend/application/dto/FriendSummary.java rename to src/main/java/queuing/core/friend/application/result/FriendSummary.java index 21f8827..93dbd74 100644 --- a/src/main/java/queuing/core/friend/application/dto/FriendSummary.java +++ b/src/main/java/queuing/core/friend/application/result/FriendSummary.java @@ -1,4 +1,4 @@ -package queuing.core.friend.application.dto; +package queuing.core.friend.application.result; import queuing.core.user.domain.entity.User; diff --git a/src/main/java/queuing/core/friend/application/service/FriendReadService.java b/src/main/java/queuing/core/friend/application/service/FriendReadService.java index 12531a7..d432eca 100644 --- a/src/main/java/queuing/core/friend/application/service/FriendReadService.java +++ b/src/main/java/queuing/core/friend/application/service/FriendReadService.java @@ -6,8 +6,9 @@ import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import queuing.core.friend.application.dto.FriendSummary; -import queuing.core.friend.application.dto.GetFriendListCommand; + +import queuing.core.friend.application.query.GetListFriendQuery; +import queuing.core.friend.application.result.FriendSummary; import queuing.core.friend.application.usecase.GetFriendListUseCase; import queuing.core.friend.domain.entity.Friend; import queuing.core.friend.domain.repository.FriendRepository; @@ -21,21 +22,24 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class FriendReadService implements GetFriendListUseCase { - - private final UserRepository userRepository; private final FriendRepository friendRepository; + private final UserRepository userRepository; @Override - public SliceResult getFriendList(GetFriendListCommand command) { - User user = userRepository.findBySlug(command.userSlug()) + public SliceResult getFriendList(GetListFriendQuery query) { + User user = userRepository.findBySlug(query.userSlug()) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - SliceResult friends = friendRepository.findFriendsByUserId(user.getId(), command.lastId(), command.size()); + SliceResult result = friendRepository.findFriendsByUserId( + user.getId(), + query.lastId(), + query.size() + ); - List summaries = friends.items().stream() + List summaries = result.items().stream() .map(friend -> FriendSummary.from(friend.getCounterpart(user))) .toList(); - return SliceResult.of(summaries, friends.hasNext()); + return SliceResult.of(summaries, result.hasNext()); } -} \ No newline at end of file +} diff --git a/src/main/java/queuing/core/friend/application/service/FriendWriteService.java b/src/main/java/queuing/core/friend/application/service/FriendWriteService.java index 282dff2..cd74de6 100644 --- a/src/main/java/queuing/core/friend/application/service/FriendWriteService.java +++ b/src/main/java/queuing/core/friend/application/service/FriendWriteService.java @@ -32,19 +32,11 @@ public void sendRequest(String requesterSlug, String targetSlug) { User target = userRepository.findBySlug(targetSlug) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - if (requester.equals(target)) { - throw new BusinessException(ErrorCode.FRIEND_INVALID_REQUEST); - } - if (friendRepository.findRelationship(requester, target).isPresent()) { throw new BusinessException(ErrorCode.FRIEND_ALREADY_EXISTS); } - Friend friend = Friend.builder() - .requester(requester) - .receiver(target) - .status(FriendStatus.PENDING) - .build(); + Friend friend = Friend.createRequest(requester, target); friendRepository.save(friend); } @@ -58,15 +50,7 @@ public void acceptRequest(String userSlug, Long requestId) { Friend friend = friendRepository.findById(requestId) .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); - if (!friend.getReceiver().equals(user)) { - throw new BusinessException(ErrorCode.FRIEND_INVALID_REQUEST); - } - - if (friend.getStatus() != FriendStatus.PENDING) { - throw new BusinessException(ErrorCode.FRIEND_INVALID_REQUEST); - } - - friend.updateStatus(FriendStatus.ACCEPTED); + friend.accept(user); } @Override diff --git a/src/main/java/queuing/core/friend/application/usecase/DeleteFriendUseCase.java b/src/main/java/queuing/core/friend/application/usecase/DeleteFriendUseCase.java index 697acff..157ed44 100644 --- a/src/main/java/queuing/core/friend/application/usecase/DeleteFriendUseCase.java +++ b/src/main/java/queuing/core/friend/application/usecase/DeleteFriendUseCase.java @@ -2,4 +2,4 @@ public interface DeleteFriendUseCase { void deleteFriend(String userSlug, String targetSlug); -} \ No newline at end of file +} diff --git a/src/main/java/queuing/core/friend/application/usecase/GetFriendListUseCase.java b/src/main/java/queuing/core/friend/application/usecase/GetFriendListUseCase.java index 18e38e7..b81d574 100644 --- a/src/main/java/queuing/core/friend/application/usecase/GetFriendListUseCase.java +++ b/src/main/java/queuing/core/friend/application/usecase/GetFriendListUseCase.java @@ -1,9 +1,9 @@ package queuing.core.friend.application.usecase; -import queuing.core.friend.application.dto.FriendSummary; -import queuing.core.friend.application.dto.GetFriendListCommand; +import queuing.core.friend.application.query.GetListFriendQuery; +import queuing.core.friend.application.result.FriendSummary; import queuing.core.global.dto.SliceResult; public interface GetFriendListUseCase { - SliceResult getFriendList(GetFriendListCommand command); -} \ No newline at end of file + SliceResult getFriendList(GetListFriendQuery query); +} diff --git a/src/main/java/queuing/core/friend/application/usecase/SendFriendRequestUseCase.java b/src/main/java/queuing/core/friend/application/usecase/SendFriendRequestUseCase.java index ecfc82b..6d39cf2 100644 --- a/src/main/java/queuing/core/friend/application/usecase/SendFriendRequestUseCase.java +++ b/src/main/java/queuing/core/friend/application/usecase/SendFriendRequestUseCase.java @@ -2,4 +2,4 @@ public interface SendFriendRequestUseCase { void sendRequest(String requesterSlug, String targetSlug); -} \ No newline at end of file +} diff --git a/src/main/java/queuing/core/friend/domain/entity/Friend.java b/src/main/java/queuing/core/friend/domain/entity/Friend.java index abef34d..20de95c 100644 --- a/src/main/java/queuing/core/friend/domain/entity/Friend.java +++ b/src/main/java/queuing/core/friend/domain/entity/Friend.java @@ -1,6 +1,7 @@ package queuing.core.friend.domain.entity; import java.time.Instant; +import java.util.Objects; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -24,6 +25,9 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + +import queuing.core.global.exception.BusinessException; +import queuing.core.global.exception.ErrorCode; import queuing.core.user.domain.entity.User; @Entity @@ -76,4 +80,44 @@ public User getCounterpart(User user) { } return requester; } + + public static Friend createRequest(User requester, User receiver) { + if (requester.equals(receiver)) { + throw new BusinessException(ErrorCode.FRIEND_INVALID_REQUEST); + } + return Friend.builder() + .requester(requester) + .receiver(receiver) + .status(FriendStatus.PENDING) + .build(); + } + + public void accept(User user) { + if (!this.receiver.equals(user)) { + throw new BusinessException(ErrorCode.FRIEND_INVALID_REQUEST); + } + + if (this.status != FriendStatus.PENDING) { + throw new BusinessException(ErrorCode.FRIEND_INVALID_REQUEST); + } + + this.status = FriendStatus.ACCEPTED; + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Friend friend)) { + return false; + } + + return this.getId() != null && Objects.equals(this.getId(), friend.getId()); + } + + @Override + public final int hashCode() { + return Objects.hash(this.getId()); + } } diff --git a/src/main/java/queuing/core/friend/domain/repository/FriendRepository.java b/src/main/java/queuing/core/friend/domain/repository/FriendRepository.java index f2aa73e..7c196b1 100644 --- a/src/main/java/queuing/core/friend/domain/repository/FriendRepository.java +++ b/src/main/java/queuing/core/friend/domain/repository/FriendRepository.java @@ -18,10 +18,9 @@ public interface FriendRepository extends JpaRepository, FriendRep Optional findByRequesterAndReceiver(User requester, User receiver); - // Find a relationship in either direction - @Query("SELECT f FROM Friend f WHERE (f.requester = :user1 AND f.receiver = :user2) OR (f.requester = :user2 AND f.receiver = :user1)") + @Query("SELECT f FROM Friend f WHERE (f.requester = :user1 AND f.receiver = :user2) " + + "OR (f.requester = :user2 AND f.receiver = :user1)") Optional findRelationship(@Param("user1") User user1, @Param("user2") User user2); - // Find pending requests received by user Page findByReceiverAndStatus(User receiver, FriendStatus status, Pageable pageable); } diff --git a/src/main/java/queuing/core/friend/infrastructure/querydsl/FriendRepositoryImpl.java b/src/main/java/queuing/core/friend/infrastructure/querydsl/FriendRepositoryImpl.java index c770aca..9af45aa 100644 --- a/src/main/java/queuing/core/friend/infrastructure/querydsl/FriendRepositoryImpl.java +++ b/src/main/java/queuing/core/friend/infrastructure/querydsl/FriendRepositoryImpl.java @@ -8,6 +8,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; + import queuing.core.friend.domain.entity.Friend; import queuing.core.friend.domain.entity.FriendStatus; import queuing.core.friend.domain.entity.QFriend; @@ -25,7 +26,7 @@ public SliceResult findFriendsByUserId(Long userId, Long lastId, int siz BooleanExpression isFriend = (friend.requester.id.eq(userId).or(friend.receiver.id.eq(userId))) .and(friend.status.eq(FriendStatus.ACCEPTED)); - + BooleanExpression lessThanId = (lastId != null) ? friend.id.lt(lastId) : null; List friends = query.selectFrom(friend) @@ -44,4 +45,4 @@ public SliceResult findFriendsByUserId(Long userId, Long lastId, int siz return SliceResult.of(friends, hasNext); } -} \ No newline at end of file +} diff --git a/src/main/java/queuing/core/friend/presentation/controller/FriendController.java b/src/main/java/queuing/core/friend/presentation/controller/FriendController.java index 9e1025a..63bfd2e 100644 --- a/src/main/java/queuing/core/friend/presentation/controller/FriendController.java +++ b/src/main/java/queuing/core/friend/presentation/controller/FriendController.java @@ -14,8 +14,8 @@ import lombok.RequiredArgsConstructor; -import queuing.core.friend.application.dto.FriendSummary; -import queuing.core.friend.application.dto.GetFriendListCommand; +import queuing.core.friend.application.query.GetListFriendQuery; +import queuing.core.friend.application.result.FriendSummary; import queuing.core.friend.application.usecase.DeleteFriendUseCase; import queuing.core.friend.application.usecase.GetFriendListUseCase; import queuing.core.friend.presentation.response.FriendResponse; @@ -50,7 +50,7 @@ public ResponseEntity> getFriends( @RequestParam(defaultValue = "20") int size ) { SliceResult result = getFriendListUseCase.getFriendList( - new GetFriendListCommand(principal.getUsername(), lastId, size) + new GetListFriendQuery(principal.getUsername(), lastId, size) ); List items = result.items().stream() diff --git a/src/main/java/queuing/core/friend/presentation/request/SendFriendRequest.java b/src/main/java/queuing/core/friend/presentation/request/SendFriendRequest.java index d883243..b0ef363 100644 --- a/src/main/java/queuing/core/friend/presentation/request/SendFriendRequest.java +++ b/src/main/java/queuing/core/friend/presentation/request/SendFriendRequest.java @@ -5,4 +5,5 @@ public record SendFriendRequest( @NotBlank(message = "친구 추가할 대상을 입력해주세요.") String targetSlug -) {} +) { +} diff --git a/src/main/java/queuing/core/friend/presentation/response/FriendResponse.java b/src/main/java/queuing/core/friend/presentation/response/FriendResponse.java index 57493c0..4c42c71 100644 --- a/src/main/java/queuing/core/friend/presentation/response/FriendResponse.java +++ b/src/main/java/queuing/core/friend/presentation/response/FriendResponse.java @@ -1,6 +1,6 @@ package queuing.core.friend.presentation.response; -import queuing.core.friend.application.dto.FriendSummary; +import queuing.core.friend.application.result.FriendSummary; public record FriendResponse( Long id, diff --git a/src/main/java/queuing/core/global/dto/PageCondition.java b/src/main/java/queuing/core/global/dto/PageCondition.java new file mode 100644 index 0000000..95f3aad --- /dev/null +++ b/src/main/java/queuing/core/global/dto/PageCondition.java @@ -0,0 +1,55 @@ +package queuing.core.global.dto; + +/** + * 페이징 조회를 위한 요청 조건을 담습니다. + * + * @param page 현재 페이지 번호 (1부터 시작) + * @param size 한 페이지당 보여줄 아이템 개수 + * @param sort 정렬 조건 + */ +public record PageCondition( + int page, + int size, + String sort +) { + public PageCondition { + if (page < 1) { + throw new IllegalArgumentException("Page number must be 1 or greater."); + } + if (size < 1) { + throw new IllegalArgumentException("Page size must be 1 or greater."); + } + } + + /** + * 정렬 조건 없는 {@code PageCondition} 객체를 생성합니다. + * + * @param page 현재 페이지 번호 (1부터 시작) + * @param size 한 페이지당 보여줄 아이템 개수 + * @return PageCondition 객체 + */ + public static PageCondition of(int page, int size) { + return new PageCondition(page, size, null); + } + + /** + * 정렬 조건이 있는 {@code PageCondition} 객체를 생성합니다. + * + * @param page 현재 페이지 번호 (1부터 시작) + * @param size 한 페이지당 보여줄 아이템 개수 + * @param sort 정렬 조건 + * @return PageCondition 객체 + */ + public static PageCondition of(int page, int size, String sort) { + return new PageCondition(page, size, sort); + } + + /** + * 데이터베이스 조회 시작 지점을 반환합니다. + * + * @return offset + */ + public long offset() { + return (long) (page - 1) * size; + } +} diff --git a/src/main/java/queuing/core/global/dto/PageData.java b/src/main/java/queuing/core/global/dto/PageData.java deleted file mode 100644 index 0ab4675..0000000 --- a/src/main/java/queuing/core/global/dto/PageData.java +++ /dev/null @@ -1,18 +0,0 @@ -package queuing.core.global.dto; - -public record PageData( - int page, - int size, - String sort) { - public static PageData of(int page, int size) { - return new PageData(page, size, null); - } - - public static PageData of(int page, int size, String sort) { - return new PageData(page, size, sort); - } - - public long offset() { - return (long)page * size; - } -} diff --git a/src/main/java/queuing/core/global/dto/PageResult.java b/src/main/java/queuing/core/global/dto/PageResult.java index bc20e9a..c1ba0b6 100644 --- a/src/main/java/queuing/core/global/dto/PageResult.java +++ b/src/main/java/queuing/core/global/dto/PageResult.java @@ -2,12 +2,89 @@ import java.util.List; +/** + * 페이징 처리된 응답 데이터를 담습니다. + * 표현, 도메인, 영속성 계층 모두 사용 가능합니다. + * + * @param 페이지에 담긴 데이터의 타입 + * @param items 현재 페이지의 데이터 목록 + * @param page 현재 페이지 번호 (1부터 시작) + * @param size 페이지당 요청한 아이템 개수 + * @param totalElements 전체 아이템 개수 + * @param totalPages 전체 페이지 수 + * @param prevPage 이전 페이지 번호 (이전 페이지가 없으면 1) + * @param nextPage 다음 페이지 번호 (다음 페이지가 없으면 마지막 페이지 혹은 1) + * @param hasPrev 이전 페이지 존재 여부 + * @param hasNext 다음 페이지 존재 여부 + * @param startPage 현재 페이지 블록의 시작 번호 (예: 1, 11, 21...) + * @param endPage 현재 페이지 블록의 끝 번호 (예: 10, 20, 30...) + */ public record PageResult( - List content, - int pageNumber, - int pageSize, - long totalElements, - int totalPages, - boolean isLast, - boolean hasNext) { -} \ No newline at end of file + List items, + int page, + int size, + long totalElements, + int totalPages, + int prevPage, + int nextPage, + boolean hasPrev, + boolean hasNext, + int startPage, + int endPage +) { + public PageResult { + if (page < 1) { + throw new IllegalArgumentException("Page number must be 1 or greater."); + } + if (size < 1) { + throw new IllegalArgumentException("Page size must be 1 or greater."); + } + + items = (items == null) ? List.of() : List.copyOf(items); + } + + /** + * {@code PageResult} 객체를 생성합니다. + * + * @param items 현재 페이지의 데이터 목록 + * @param page 현재 페이지 번호 (1부터 시작) + * @param size 페이지당 요청한 아이템 개수 + * @param paginationSize 페이지 내비게이션 바에 표시할 페이지 번호 개수 (예: 10개씩 노출) + * @param totalElements 전체 아이템 개수 + * @return PageResult 객체 + */ + public static PageResult of(List items, int page, int size, int paginationSize, long totalElements) { + // 페이지당 요청 아이템 개수, 내비게이션 바 페이지 개수, 전체 아이템 개수 확인 + int fixedSize = Math.max(1, size); + int fixedPaginationSize = Math.max(1, paginationSize); + long fixedTotalElements = Math.max(0, totalElements); + + // 페이지 기본 정보 계산 + int totalPages = (fixedTotalElements == 0) ? 0 : (int) ((fixedTotalElements - 1) / fixedSize + 1); + int fixedPage = (totalPages == 0) ? 1 : Math.min(Math.max(1, page), totalPages); + + // 내비게이션 바 상태 확인 + boolean hasPrev = fixedPage > 1; + boolean hasNext = fixedPage < totalPages; + int prevPage = hasPrev ? fixedPage - 1 : 1; + int nextPage = hasNext ? fixedPage + 1 : (totalPages == 0 ? 1 : totalPages); + + // 내비게이션 바 범위 계산 + int startPage = ((fixedPage - 1) / fixedPaginationSize) * fixedPaginationSize + 1; + int endPage = (totalPages == 0) ? 1 : Math.min(startPage + fixedPaginationSize - 1, totalPages); + + return new PageResult<>( + items, + fixedPage, + fixedSize, + fixedTotalElements, + totalPages, + prevPage, + nextPage, + hasPrev, + hasNext, + startPage, + endPage + ); + } +} diff --git a/src/main/java/queuing/core/global/dto/SliceResult.java b/src/main/java/queuing/core/global/dto/SliceResult.java index f826e9a..0258872 100644 --- a/src/main/java/queuing/core/global/dto/SliceResult.java +++ b/src/main/java/queuing/core/global/dto/SliceResult.java @@ -2,10 +2,29 @@ import java.util.List; +/** + * 무한 스크롤을 위한 응답 데이터를 담습니다. + * 표현, 도메인, 영속성 계층 모두 사용 가능합니다. + * + * @param 데이터 타입 + * @param items 현재 슬라이스의 데이터 목록 + * @param hasNext 다음 데이터 존재 여부 + */ public record SliceResult( List items, boolean hasNext ) { + public SliceResult { + items = (items == null) ? List.of() : List.copyOf(items); + } + + /** + * {@code SliceResult} 객체를 생성합니다. + * + * @param items 현재 페이지의 데이터 목록 + * @param hasNext 다음 페이지 존재 여부 + * @return SliceResult 객체 + */ public static SliceResult of(List items, boolean hasNext) { return new SliceResult<>(items, hasNext); } diff --git a/src/main/java/queuing/core/global/exception/ErrorCode.java b/src/main/java/queuing/core/global/exception/ErrorCode.java index f554737..bae3ea6 100755 --- a/src/main/java/queuing/core/global/exception/ErrorCode.java +++ b/src/main/java/queuing/core/global/exception/ErrorCode.java @@ -13,10 +13,9 @@ public enum ErrorCode { // User USER_NOT_FOUND(NOT_FOUND, "user.not-found", "사용자 정보가 존재하지 않아요."), - USER_INVALID_INPUT(BAD_REQUEST, "user.invalid-input", "아이디 또는 비밀번호가 일치하지 않아요."), USER_NOT_AUTHENTICATED(UNAUTHORIZED, "user.not-authenticated", "인증된 사용자만 요청할 수 있어요."), USER_INSUFFICIENT_SCOPE(FORBIDDEN, "user.insufficient-scope", "특정 권한을 가진 사용자만 요청할 수 있어요."), - USER_ONBOARDING_REQUIRED(FORBIDDEN, "user.nickname-required", "닉네임 설정이 필요합니다."), + USER_ONBOARDING_REQUIRED(FORBIDDEN, "user.onboarding-required", "초기 프로필 필수 정보를 입력하지 않았어요."), USER_NICKNAME_DUPLICATED(CONFLICT, "user.nickname-duplicated", "이미 사용 중인 닉네임이에요."), USER_NICKNAME_FORBIDDEN(BAD_REQUEST, "user.nickname-forbidden", "사용할 수 없는 닉네임이에요."), @@ -24,6 +23,9 @@ public enum ErrorCode { FRIEND_ALREADY_EXISTS(CONFLICT, "friend.already-exists", "이미 친구 관계거나 요청 중이에요."), FRIEND_REQUEST_NOT_FOUND(NOT_FOUND, "friend.request-not-found", "친구 요청을 찾을 수 없어요."), FRIEND_INVALID_REQUEST(BAD_REQUEST, "friend.invalid-request", "허용되지 않은 친구 요청이에요."), + + // Room + ROOM_NOT_FOUND(NOT_FOUND, "room.not-found", "방 정보가 존재하지 않아요.") ; private final HttpStatus status; diff --git a/src/main/java/queuing/core/global/exception/ExceptionAdvice.java b/src/main/java/queuing/core/global/exception/ExceptionAdvice.java index f3a9c57..9e38d04 100755 --- a/src/main/java/queuing/core/global/exception/ExceptionAdvice.java +++ b/src/main/java/queuing/core/global/exception/ExceptionAdvice.java @@ -52,7 +52,9 @@ protected ResponseEntity> handleNoResourceFoundException(NoRe } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - protected ResponseEntity> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) { + protected ResponseEntity> handleHttpRequestMethodNotSupportedException( + HttpRequestMethodNotSupportedException ex + ) { ErrorCode errorCode = ErrorCode.COMMON_METHOD_NOT_ALLOWED; return ResponseEntity @@ -78,7 +80,9 @@ protected ResponseEntity> handleAuthorizationDeniedException( } @ExceptionHandler(MethodArgumentNotValidException.class) - protected ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + protected ResponseEntity> handleMethodArgumentNotValidException( + MethodArgumentNotValidException ex + ) { ErrorCode errorCode = ErrorCode.COMMON_INVALID_INPUT; return ResponseEntity @@ -92,7 +96,9 @@ protected ResponseEntity> handleMethodArgumentNotValidExcepti } @ExceptionHandler(MissingServletRequestParameterException.class) - protected ResponseEntity> handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) { + protected ResponseEntity> handleMissingServletRequestParameterException( + MissingServletRequestParameterException ex + ) { ErrorCode errorCode = ErrorCode.COMMON_INVALID_INPUT; return ResponseEntity @@ -103,8 +109,11 @@ protected ResponseEntity> handleMissingServletRequestParamete errorCode.getMessage() )); } + @ExceptionHandler(HandlerMethodValidationException.class) - protected ResponseEntity> handleHandlerMethodValidationException(HandlerMethodValidationException ex) { + protected ResponseEntity> handleHandlerMethodValidationException( + HandlerMethodValidationException ex + ) { ErrorCode errorCode = ErrorCode.COMMON_INVALID_INPUT; return ResponseEntity @@ -115,5 +124,4 @@ protected ResponseEntity> handleHandlerMethodValidationExcept errorCode.getMessage() )); } - } diff --git a/src/main/java/queuing/core/global/response/FailedResponseBody.java b/src/main/java/queuing/core/global/response/FailedResponseBody.java index da0820c..4e98583 100644 --- a/src/main/java/queuing/core/global/response/FailedResponseBody.java +++ b/src/main/java/queuing/core/global/response/FailedResponseBody.java @@ -10,7 +10,7 @@ public final class FailedResponseBody implements ResponseBody { private final ErrorContent error; - public FailedResponseBody(){ + public FailedResponseBody() { this(HttpStatus.INTERNAL_SERVER_ERROR.value(), null, null); } @@ -32,7 +32,8 @@ public record ErrorContent( String message, @JsonInclude(JsonInclude.Include.NON_EMPTY) List fieldErrors - ) {} + ) { + } public record FieldError( String field, diff --git a/src/main/java/queuing/core/global/security/Constants.java b/src/main/java/queuing/core/global/security/Constants.java index 49af92b..fc937ea 100644 --- a/src/main/java/queuing/core/global/security/Constants.java +++ b/src/main/java/queuing/core/global/security/Constants.java @@ -1,10 +1,12 @@ package queuing.core.global.security; public final class Constants { - private Constants() {} + private Constants() { + } public static final class Paths { - private Paths() {} + private Paths() { + } public static final String AUTH_BASE = "/api/auth"; public static final String AUTH_ALL = AUTH_BASE + "/**"; @@ -30,7 +32,8 @@ private Paths() {} } public static final class Cookies { - private Cookies() {} + private Cookies() { + } public static final String PARAM_CONTINUE = "continue"; public static final String REDIRECT_URL = "queuing.login.redirectUrl"; @@ -38,7 +41,8 @@ private Cookies() {} } public static final class Frontend { - private Frontend() {} + private Frontend() { + } public static final String LOGIN_ERROR_PATH = "/login?error"; } diff --git a/src/main/java/queuing/core/global/security/adapter/SpringSecurityAuthenticationAdapter.java b/src/main/java/queuing/core/global/security/adapter/SpringSecurityAuthenticationAdapter.java new file mode 100644 index 0000000..d039549 --- /dev/null +++ b/src/main/java/queuing/core/global/security/adapter/SpringSecurityAuthenticationAdapter.java @@ -0,0 +1,31 @@ +package queuing.core.global.security.adapter; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Component; + +import queuing.core.global.security.authorization.UserPrincipal; +import queuing.core.user.application.auth.AuthenticationOperations; + +@Component +public class SpringSecurityAuthenticationAdapter implements AuthenticationOperations { + + @Override + public void updateOnboardingStatus(boolean completed) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth instanceof OAuth2AuthenticationToken oauth2Token + && oauth2Token.getPrincipal() instanceof UserPrincipal principal) { + UserPrincipal newPrincipal = principal.updateOnboardingStatus(completed); + + Authentication newAuth = new OAuth2AuthenticationToken( + newPrincipal, + newPrincipal.getAuthorities(), + oauth2Token.getAuthorizedClientRegistrationId() + ); + + SecurityContextHolder.getContext().setAuthentication(newAuth); + } + } +} diff --git a/src/main/java/queuing/core/global/security/authorization/OnboardingRequiredAuthorizationManager.java b/src/main/java/queuing/core/global/security/authorization/OnboardingRequiredAuthorizationManager.java index ee67844..c952070 100644 --- a/src/main/java/queuing/core/global/security/authorization/OnboardingRequiredAuthorizationManager.java +++ b/src/main/java/queuing/core/global/security/authorization/OnboardingRequiredAuthorizationManager.java @@ -17,7 +17,7 @@ import queuing.core.global.exception.BusinessException; import queuing.core.global.security.exception.InvalidPrincipalException; import queuing.core.global.security.exception.UserOnboardingRequiredException; -import queuing.core.user.application.command.CheckOnboardingCompletedQuery; +import queuing.core.user.application.query.CheckOnboardingCompletedQuery; import queuing.core.user.application.usecase.SignUpUseCase; @Component diff --git a/src/main/java/queuing/core/global/security/authorization/UserPrincipal.java b/src/main/java/queuing/core/global/security/authorization/UserPrincipal.java index 91ccaea..eb25bd8 100644 --- a/src/main/java/queuing/core/global/security/authorization/UserPrincipal.java +++ b/src/main/java/queuing/core/global/security/authorization/UserPrincipal.java @@ -23,20 +23,28 @@ public class UserPrincipal implements OAuth2User, OidcUser, UserDetails { private Collection authorities; private Map attributes; private OidcIdToken idToken; + private final boolean onboardingCompleted; public UserPrincipal(String slug, String email, Collection authorities, - Map attributes, OidcIdToken idToken) { + Map attributes, OidcIdToken idToken, boolean onboardingCompleted) { this.slug = slug; this.email = email; this.authorities = authorities; this.attributes = attributes; this.idToken = idToken; + this.onboardingCompleted = onboardingCompleted; } - public UserPrincipal(String slug, String email, Collection authorities) { + public UserPrincipal( + String slug, + String email, + Collection authorities, + boolean onboardingCompleted + ) { this.slug = slug; this.email = email; this.authorities = authorities; + this.onboardingCompleted = onboardingCompleted; } public static UserPrincipal create(User user, OidcUser oidcUser) { @@ -45,7 +53,8 @@ public static UserPrincipal create(User user, OidcUser oidcUser) { user.getEmail(), List.of(new SimpleGrantedAuthority(ROLE_PREFIX + user.getRole().name())), oidcUser.getAttributes(), - oidcUser.getIdToken() + oidcUser.getIdToken(), + user.isOnboardingCompleted() ); } @@ -53,10 +62,22 @@ public static UserPrincipal create(User user) { return new UserPrincipal( user.getSlug(), user.getEmail(), - List.of(new SimpleGrantedAuthority(ROLE_PREFIX + user.getRole().name())) + List.of(new SimpleGrantedAuthority(ROLE_PREFIX + user.getRole().name())), + user.isOnboardingCompleted() ); } + public boolean isOnboardingCompleted() { + return this.onboardingCompleted; + } + + public UserPrincipal updateOnboardingStatus(boolean status) { + if (this.idToken != null) { + return new UserPrincipal(slug, email, authorities, attributes, idToken, status); + } + return new UserPrincipal(slug, email, authorities, status); + } + @Override public String getEmail() { return this.email; diff --git a/src/main/java/queuing/core/global/security/oidc/RedirectUrlOidcAuthorizationRequestRepository.java b/src/main/java/queuing/core/global/security/oidc/RedirectUrlOidcAuthorizationRequestRepository.java index 55bb754..2c39bd9 100644 --- a/src/main/java/queuing/core/global/security/oidc/RedirectUrlOidcAuthorizationRequestRepository.java +++ b/src/main/java/queuing/core/global/security/oidc/RedirectUrlOidcAuthorizationRequestRepository.java @@ -12,7 +12,8 @@ import queuing.core.global.security.Constants; -public class RedirectUrlOidcAuthorizationRequestRepository implements AuthorizationRequestRepository { +public class RedirectUrlOidcAuthorizationRequestRepository + implements AuthorizationRequestRepository { private final AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); diff --git a/src/main/java/queuing/core/global/security/properties/CorsProperties.java b/src/main/java/queuing/core/global/security/properties/CorsProperties.java index 5d9bfea..0c3d79f 100644 --- a/src/main/java/queuing/core/global/security/properties/CorsProperties.java +++ b/src/main/java/queuing/core/global/security/properties/CorsProperties.java @@ -5,7 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "core.cors") -public record CorsProperties ( +public record CorsProperties( List allowedOrigins, List allowedMethods, List allowedHeaders, diff --git a/src/main/java/queuing/core/global/utils/RedirectUrlUtils.java b/src/main/java/queuing/core/global/utils/RedirectUrlUtils.java index 2cd85b0..251b098 100644 --- a/src/main/java/queuing/core/global/utils/RedirectUrlUtils.java +++ b/src/main/java/queuing/core/global/utils/RedirectUrlUtils.java @@ -7,7 +7,8 @@ import java.util.Set; public final class RedirectUrlUtils { - private RedirectUrlUtils() {} + private RedirectUrlUtils() { + } private static final String SCHEME_HTTP = "http"; private static final String SCHEME_HTTPS = "https"; @@ -55,15 +56,15 @@ public static URI extractOrigin(URI uri) { return URI.create(origin.toOriginStringWithoutDefaultPort()); } - private static String requireNonBlank(String s, String message) { - if (s == null || s.isBlank()) { + private static String requireNonBlank(String str, String message) { + if (str == null || str.isBlank()) { throw new IllegalArgumentException(message); } - return s; + return str; } - private static void rejectCrlf(String s) { - if (s.indexOf('\r') >= 0 || s.indexOf('\n') >= 0) { + private static void rejectCrlf(String str) { + if (str.indexOf('\r') >= 0 || str.indexOf('\n') >= 0) { throw new IllegalArgumentException("잘못된 입력 값이에요."); } } @@ -138,8 +139,8 @@ private static boolean isDefaultPort(String scheme, int port) { || (SCHEME_HTTP.equals(scheme) && port == DEFAULT_HTTP_PORT); } - private static String normalizeLower(String s) { - return (s == null) ? "" : s.toLowerCase(Locale.ROOT); + private static String normalizeLower(String str) { + return (str == null) ? "" : str.toLowerCase(Locale.ROOT); } } } diff --git a/src/main/java/queuing/core/global/utils/SlugUtils.java b/src/main/java/queuing/core/global/utils/SlugUtils.java index d756b03..aef69c5 100644 --- a/src/main/java/queuing/core/global/utils/SlugUtils.java +++ b/src/main/java/queuing/core/global/utils/SlugUtils.java @@ -3,7 +3,8 @@ import java.security.SecureRandom; public final class SlugUtils { - private SlugUtils() {} + private SlugUtils() { + } private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; private static final SecureRandom RANDOM = new SecureRandom(); diff --git a/src/main/java/queuing/core/room/application/dto/CreateRoomCommand.java b/src/main/java/queuing/core/room/application/command/CreateRoomCommand.java similarity index 75% rename from src/main/java/queuing/core/room/application/dto/CreateRoomCommand.java rename to src/main/java/queuing/core/room/application/command/CreateRoomCommand.java index 66e9e36..24b8687 100644 --- a/src/main/java/queuing/core/room/application/dto/CreateRoomCommand.java +++ b/src/main/java/queuing/core/room/application/command/CreateRoomCommand.java @@ -1,4 +1,4 @@ -package queuing.core.room.application.dto; +package queuing.core.room.application.command; import java.util.Set; @@ -12,9 +12,9 @@ public record CreateRoomCommand( Set slugs ) { public CreateRoomCommand { - title = (title != null) ? title.trim() : null; - password = (password != null) ? password.trim() : null; - userSlug = (userSlug != null) ? userSlug.trim() : null; + title = (title != null) ? title.strip() : null; + password = (password != null) ? password.strip() : null; + userSlug = (userSlug != null) ? userSlug.strip() : null; slugs = (slugs != null) ? slugs : Set.of(); diff --git a/src/main/java/queuing/core/room/application/command/DeleteRoomCommand.java b/src/main/java/queuing/core/room/application/command/DeleteRoomCommand.java new file mode 100644 index 0000000..8f2955b --- /dev/null +++ b/src/main/java/queuing/core/room/application/command/DeleteRoomCommand.java @@ -0,0 +1,20 @@ +package queuing.core.room.application.command; + +import queuing.core.global.exception.BusinessException; +import queuing.core.global.exception.ErrorCode; + +public record DeleteRoomCommand( + String userSlug, + String roomSlug +) { + public DeleteRoomCommand { + if (userSlug == null || userSlug.isBlank()) { + throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); + } + userSlug = userSlug.strip(); + if (roomSlug == null || userSlug.isBlank()) { + throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); + } + roomSlug = roomSlug.strip(); + } +} diff --git a/src/main/java/queuing/core/room/application/dto/MusicTagDto.java b/src/main/java/queuing/core/room/application/dto/MusicTagDto.java deleted file mode 100644 index 39aaf18..0000000 --- a/src/main/java/queuing/core/room/application/dto/MusicTagDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package queuing.core.room.application.dto; - -import queuing.core.room.domain.entity.MusicTag; -import queuing.core.room.domain.query.MusicTagQueryResult; - -public record MusicTagDto( - String slug, - String name -) { - public static MusicTagDto from(MusicTag entity) { - return new MusicTagDto(entity.getSlug(), entity.getName()); - } - - public static MusicTagDto from(MusicTagQueryResult queryResult) { - return new MusicTagDto(queryResult.slug(), queryResult.name()); - } -} diff --git a/src/main/java/queuing/core/room/application/dto/GetListRoomCommand.java b/src/main/java/queuing/core/room/application/query/GetListRoomQuery.java similarity index 64% rename from src/main/java/queuing/core/room/application/dto/GetListRoomCommand.java rename to src/main/java/queuing/core/room/application/query/GetListRoomQuery.java index e1d4951..af1b580 100644 --- a/src/main/java/queuing/core/room/application/dto/GetListRoomCommand.java +++ b/src/main/java/queuing/core/room/application/query/GetListRoomQuery.java @@ -1,13 +1,13 @@ -package queuing.core.room.application.dto; +package queuing.core.room.application.query; import queuing.core.global.exception.BusinessException; import queuing.core.global.exception.ErrorCode; -public record GetListRoomCommand( +public record GetListRoomQuery( Long lastId, int size ) { - public GetListRoomCommand { + public GetListRoomQuery { if (lastId != null && lastId <= 0) { throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); } @@ -15,5 +15,8 @@ public record GetListRoomCommand( if (size <= 0) { throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); } + if (size > 100) { + throw new BusinessException(ErrorCode.COMMON_INVALID_INPUT); + } } } diff --git a/src/main/java/queuing/core/room/application/result/MusicTagResult.java b/src/main/java/queuing/core/room/application/result/MusicTagResult.java new file mode 100644 index 0000000..043e21b --- /dev/null +++ b/src/main/java/queuing/core/room/application/result/MusicTagResult.java @@ -0,0 +1,17 @@ +package queuing.core.room.application.result; + +import queuing.core.room.domain.entity.MusicTag; +import queuing.core.room.domain.query.MusicTagQueryResult; + +public record MusicTagResult( + String slug, + String name +) { + public static MusicTagResult from(MusicTag entity) { + return new MusicTagResult(entity.getSlug(), entity.getName()); + } + + public static MusicTagResult from(MusicTagQueryResult queryResult) { + return new MusicTagResult(queryResult.slug(), queryResult.name()); + } +} diff --git a/src/main/java/queuing/core/room/application/dto/RoomSummary.java b/src/main/java/queuing/core/room/application/result/RoomSummary.java similarity index 83% rename from src/main/java/queuing/core/room/application/dto/RoomSummary.java rename to src/main/java/queuing/core/room/application/result/RoomSummary.java index 613eb64..4c26b7a 100644 --- a/src/main/java/queuing/core/room/application/dto/RoomSummary.java +++ b/src/main/java/queuing/core/room/application/result/RoomSummary.java @@ -1,4 +1,4 @@ -package queuing.core.room.application.dto; +package queuing.core.room.application.result; import java.time.Instant; import java.util.List; @@ -11,7 +11,7 @@ public record RoomSummary( String title, boolean isPrivate, Instant createdAt, - List tags + List tags ) { public static RoomSummary from(RoomQueryResult queryResult) { return new RoomSummary( @@ -21,7 +21,7 @@ public static RoomSummary from(RoomQueryResult queryResult) { queryResult.isPrivate(), queryResult.createdAt(), queryResult.tags().stream() - .map(MusicTagDto::from) + .map(MusicTagResult::from) .toList() ); } diff --git a/src/main/java/queuing/core/room/application/service/MusicTagReadService.java b/src/main/java/queuing/core/room/application/service/MusicTagReadService.java index dcd4ae2..13400eb 100755 --- a/src/main/java/queuing/core/room/application/service/MusicTagReadService.java +++ b/src/main/java/queuing/core/room/application/service/MusicTagReadService.java @@ -7,23 +7,23 @@ import lombok.RequiredArgsConstructor; -import queuing.core.room.application.dto.MusicTagDto; +import queuing.core.room.application.result.MusicTagResult; import queuing.core.room.application.usecase.GetListMusicTagUseCase; import queuing.core.room.domain.entity.MusicTag; import queuing.core.room.domain.repository.MusicTagRepository; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class MusicTagReadService implements GetListMusicTagUseCase { private final MusicTagRepository musicTagRepository; @Override - @Transactional(readOnly = true) - public List getList() { + public List getList() { List tags = musicTagRepository.findAllByOrderBySlugAsc(); return tags.stream() - .map(MusicTagDto::from) + .map(MusicTagResult::from) .toList(); } } diff --git a/src/main/java/queuing/core/room/application/service/RoomReadService.java b/src/main/java/queuing/core/room/application/service/RoomReadService.java index 22eb4d9..d566bfc 100755 --- a/src/main/java/queuing/core/room/application/service/RoomReadService.java +++ b/src/main/java/queuing/core/room/application/service/RoomReadService.java @@ -7,23 +7,23 @@ import lombok.RequiredArgsConstructor; -import queuing.core.room.application.dto.GetListRoomCommand; -import queuing.core.room.application.dto.RoomSummary; -import queuing.core.room.application.usecase.GetListRoomUseCase; import queuing.core.global.dto.SliceResult; +import queuing.core.room.application.query.GetListRoomQuery; +import queuing.core.room.application.result.RoomSummary; +import queuing.core.room.application.usecase.GetListRoomUseCase; import queuing.core.room.domain.query.RoomQueryResult; import queuing.core.room.domain.repository.RoomRepositoryCustom; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class RoomReadService implements GetListRoomUseCase { private final RoomRepositoryCustom roomRepository; @Override - @Transactional(readOnly = true) - public SliceResult getList(GetListRoomCommand cmd) { - SliceResult result = roomRepository.findAllWithTags(cmd.lastId(), cmd.size()); - + public SliceResult getList(GetListRoomQuery query) { + SliceResult result = roomRepository.findAllWithTags(query.lastId(), query.size()); + List summaries = result.items().stream() .map(RoomSummary::from) .toList(); diff --git a/src/main/java/queuing/core/room/application/service/RoomWriteService.java b/src/main/java/queuing/core/room/application/service/RoomWriteService.java index aa5b637..e18ba6a 100755 --- a/src/main/java/queuing/core/room/application/service/RoomWriteService.java +++ b/src/main/java/queuing/core/room/application/service/RoomWriteService.java @@ -12,8 +12,10 @@ import queuing.core.global.exception.BusinessException; import queuing.core.global.exception.ErrorCode; import queuing.core.global.utils.SlugUtils; -import queuing.core.room.application.dto.CreateRoomCommand; +import queuing.core.room.application.command.CreateRoomCommand; +import queuing.core.room.application.command.DeleteRoomCommand; import queuing.core.room.application.usecase.CreateRoomUseCase; +import queuing.core.room.application.usecase.DeleteRoomUseCase; import queuing.core.room.domain.entity.MusicTag; import queuing.core.room.domain.entity.Room; import queuing.core.room.domain.repository.MusicTagRepository; @@ -23,7 +25,7 @@ @Service @RequiredArgsConstructor -public class RoomWriteService implements CreateRoomUseCase { +public class RoomWriteService implements CreateRoomUseCase, DeleteRoomUseCase { private final RoomRepository roomRepository; private final MusicTagRepository musicTagRepository; private final UserRepository userRepository; @@ -32,20 +34,20 @@ public class RoomWriteService implements CreateRoomUseCase { @Override @Transactional - public String create(CreateRoomCommand cmd) { + public String create(CreateRoomCommand command) { // 사용자 조회 - User owner = userRepository.findBySlug(cmd.userSlug()) + User owner = userRepository.findBySlug(command.userSlug()) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); // 태그 조회 - Set tags = new HashSet<>(musicTagRepository.findAllBySlugIn(cmd.slugs())); + Set tags = new HashSet<>(musicTagRepository.findAllBySlugIn(command.slugs())); // 방 생성 String roomSlug = SlugUtils.generateSlug(); - String encodedPassword = encodePasswordIfPresent(cmd.password()); + String encodedPassword = encodePasswordIfPresent(command.password()); Room room = Room.builder() .slug(roomSlug) - .title(cmd.title()) + .title(command.title()) .password(encodedPassword) .owner(owner) .build(); @@ -62,4 +64,20 @@ private String encodePasswordIfPresent(String password) { } return null; } + + @Override + @Transactional + public void delete(DeleteRoomCommand command) { + User user = userRepository.findBySlug(command.userSlug()) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + Room room = roomRepository.findBySlug(command.roomSlug()) + .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND)); + + if (!room.getOwner().equals(user)) { + throw new BusinessException(ErrorCode.USER_INSUFFICIENT_SCOPE); + } + + roomRepository.delete(room); + } } diff --git a/src/main/java/queuing/core/room/application/usecase/CreateRoomUseCase.java b/src/main/java/queuing/core/room/application/usecase/CreateRoomUseCase.java index 32e50cc..814585d 100755 --- a/src/main/java/queuing/core/room/application/usecase/CreateRoomUseCase.java +++ b/src/main/java/queuing/core/room/application/usecase/CreateRoomUseCase.java @@ -1,6 +1,6 @@ package queuing.core.room.application.usecase; -import queuing.core.room.application.dto.CreateRoomCommand; +import queuing.core.room.application.command.CreateRoomCommand; public interface CreateRoomUseCase { String create(CreateRoomCommand cmd); diff --git a/src/main/java/queuing/core/room/application/usecase/DeleteRoomUseCase.java b/src/main/java/queuing/core/room/application/usecase/DeleteRoomUseCase.java new file mode 100644 index 0000000..9d0bccf --- /dev/null +++ b/src/main/java/queuing/core/room/application/usecase/DeleteRoomUseCase.java @@ -0,0 +1,7 @@ +package queuing.core.room.application.usecase; + +import queuing.core.room.application.command.DeleteRoomCommand; + +public interface DeleteRoomUseCase { + void delete(DeleteRoomCommand command); +} diff --git a/src/main/java/queuing/core/room/application/usecase/GetListMusicTagUseCase.java b/src/main/java/queuing/core/room/application/usecase/GetListMusicTagUseCase.java index 4a1c391..1c47281 100755 --- a/src/main/java/queuing/core/room/application/usecase/GetListMusicTagUseCase.java +++ b/src/main/java/queuing/core/room/application/usecase/GetListMusicTagUseCase.java @@ -2,8 +2,8 @@ import java.util.List; -import queuing.core.room.application.dto.MusicTagDto; +import queuing.core.room.application.result.MusicTagResult; public interface GetListMusicTagUseCase { - List getList(); + List getList(); } diff --git a/src/main/java/queuing/core/room/application/usecase/GetListRoomUseCase.java b/src/main/java/queuing/core/room/application/usecase/GetListRoomUseCase.java index a20a8c3..46bd149 100755 --- a/src/main/java/queuing/core/room/application/usecase/GetListRoomUseCase.java +++ b/src/main/java/queuing/core/room/application/usecase/GetListRoomUseCase.java @@ -1,9 +1,9 @@ package queuing.core.room.application.usecase; -import queuing.core.room.application.dto.GetListRoomCommand; -import queuing.core.room.application.dto.RoomSummary; import queuing.core.global.dto.SliceResult; +import queuing.core.room.application.query.GetListRoomQuery; +import queuing.core.room.application.result.RoomSummary; public interface GetListRoomUseCase { - SliceResult getList(GetListRoomCommand cmd); + SliceResult getList(GetListRoomQuery query); } diff --git a/src/main/java/queuing/core/room/domain/entity/MusicTag.java b/src/main/java/queuing/core/room/domain/entity/MusicTag.java index 4462872..d73cbcc 100755 --- a/src/main/java/queuing/core/room/domain/entity/MusicTag.java +++ b/src/main/java/queuing/core/room/domain/entity/MusicTag.java @@ -1,5 +1,7 @@ package queuing.core.room.domain.entity; +import java.util.Objects; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -28,4 +30,21 @@ public class MusicTag { @Column(name = "name", nullable = false, length = 255) private String name; + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof MusicTag musicTag)) { + return false; + } + + return this.getId() != null && Objects.equals(this.getId(), musicTag.getId()); + } + + @Override + public final int hashCode() { + return Objects.hash(this.getId()); + } } diff --git a/src/main/java/queuing/core/room/domain/entity/Room.java b/src/main/java/queuing/core/room/domain/entity/Room.java index d94cd7f..3e75c9f 100755 --- a/src/main/java/queuing/core/room/domain/entity/Room.java +++ b/src/main/java/queuing/core/room/domain/entity/Room.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import jakarta.persistence.CascadeType; @@ -83,4 +84,21 @@ public void addTag(MusicTag musicTag) { RoomMusicTag link = new RoomMusicTag(this, musicTag); this.roomMusicTags.add(link); } + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Room room)) { + return false; + } + + return this.getId() != null && Objects.equals(this.getId(), room.getId()); + } + + @Override + public final int hashCode() { + return Objects.hash(this.getId()); + } } diff --git a/src/main/java/queuing/core/room/domain/entity/RoomMusicTag.java b/src/main/java/queuing/core/room/domain/entity/RoomMusicTag.java index 4c2d582..feaf6fc 100755 --- a/src/main/java/queuing/core/room/domain/entity/RoomMusicTag.java +++ b/src/main/java/queuing/core/room/domain/entity/RoomMusicTag.java @@ -1,5 +1,7 @@ package queuing.core.room.domain.entity; +import java.util.Objects; + import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -11,12 +13,14 @@ import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name = "rooms_music_tags", indexes = @Index(name = "idx_rooms_music_tags_on_tag_id_and_room_id", columnList = "tag_id, room_id") ) +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class RoomMusicTag { @EmbeddedId @@ -37,4 +41,21 @@ public RoomMusicTag(Room room, MusicTag tag) { this.tag = tag; this.id = new RoomTagId(); } + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RoomMusicTag roomMusicTag)) { + return false; + } + + return this.getId() != null && Objects.equals(this.getId(), roomMusicTag.getId()); + } + + @Override + public final int hashCode() { + return Objects.hash(this.getId()); + } } diff --git a/src/main/java/queuing/core/room/domain/repository/RoomRepository.java b/src/main/java/queuing/core/room/domain/repository/RoomRepository.java index ae9eeac..c60143c 100755 --- a/src/main/java/queuing/core/room/domain/repository/RoomRepository.java +++ b/src/main/java/queuing/core/room/domain/repository/RoomRepository.java @@ -1,8 +1,11 @@ package queuing.core.room.domain.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import queuing.core.room.domain.entity.Room; public interface RoomRepository extends JpaRepository { + Optional findBySlug(String slug); } diff --git a/src/main/java/queuing/core/room/infrastructure/querydsl/RoomRepositoryImpl.java b/src/main/java/queuing/core/room/infrastructure/querydsl/RoomRepositoryImpl.java index f88283e..9e19391 100755 --- a/src/main/java/queuing/core/room/infrastructure/querydsl/RoomRepositoryImpl.java +++ b/src/main/java/queuing/core/room/infrastructure/querydsl/RoomRepositoryImpl.java @@ -12,12 +12,12 @@ import lombok.RequiredArgsConstructor; +import queuing.core.global.dto.SliceResult; import queuing.core.room.domain.entity.QMusicTag; import queuing.core.room.domain.entity.QRoom; import queuing.core.room.domain.entity.QRoomMusicTag; import queuing.core.room.domain.entity.Room; import queuing.core.room.domain.query.MusicTagQueryResult; -import queuing.core.global.dto.SliceResult; import queuing.core.room.domain.query.RoomQueryResult; import queuing.core.room.domain.repository.RoomRepositoryCustom; @@ -81,4 +81,4 @@ public SliceResult findAllWithTags(Long lastId, int size) { return SliceResult.of(items, hasNext); } -} \ No newline at end of file +} diff --git a/src/main/java/queuing/core/room/presentation/controller/MusicTagController.java b/src/main/java/queuing/core/room/presentation/controller/MusicTagController.java index 5c32976..2ce08aa 100755 --- a/src/main/java/queuing/core/room/presentation/controller/MusicTagController.java +++ b/src/main/java/queuing/core/room/presentation/controller/MusicTagController.java @@ -26,7 +26,7 @@ public ResponseEntity> getList() { .map(MusicTagResponse::from) .toList(); - ListMusicTagResponse content = new ListMusicTagResponse(items); + ListMusicTagResponse content = ListMusicTagResponse.of(items); return ResponseEntity.ok().body(ResponseBody.success(content)); } diff --git a/src/main/java/queuing/core/room/presentation/controller/RoomController.java b/src/main/java/queuing/core/room/presentation/controller/RoomController.java index b9b6dd9..938c9db 100755 --- a/src/main/java/queuing/core/room/presentation/controller/RoomController.java +++ b/src/main/java/queuing/core/room/presentation/controller/RoomController.java @@ -8,7 +8,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -20,14 +22,16 @@ import queuing.core.global.dto.SliceResult; import queuing.core.global.response.ResponseBody; import queuing.core.global.security.authorization.UserPrincipal; -import queuing.core.room.application.dto.CreateRoomCommand; -import queuing.core.room.application.dto.GetListRoomCommand; -import queuing.core.room.application.dto.RoomSummary; +import queuing.core.room.application.command.CreateRoomCommand; +import queuing.core.room.application.command.DeleteRoomCommand; +import queuing.core.room.application.query.GetListRoomQuery; +import queuing.core.room.application.result.RoomSummary; import queuing.core.room.application.usecase.CreateRoomUseCase; +import queuing.core.room.application.usecase.DeleteRoomUseCase; import queuing.core.room.application.usecase.GetListRoomUseCase; import queuing.core.room.presentation.request.CreateRoomRequest; +import queuing.core.room.presentation.response.CreateRoomResponse; import queuing.core.room.presentation.response.ListRoomResponse; -import queuing.core.room.presentation.response.MusicTagResponse; import queuing.core.room.presentation.response.RoomSummaryResponse; @RestController @@ -36,10 +40,11 @@ public class RoomController { private final CreateRoomUseCase createRoomUseCase; private final GetListRoomUseCase getListRoomUseCase; + private final DeleteRoomUseCase deleteRoomUseCase; @PreAuthorize("isAuthenticated()") @PostMapping - public ResponseEntity> create( + public ResponseEntity> create( @AuthenticationPrincipal UserPrincipal principal, @RequestBody @Valid CreateRoomRequest request ) { @@ -51,8 +56,9 @@ public ResponseEntity> create( request.tags() ) ); + return ResponseEntity.created(URI.create(slug)) - .body(ResponseBody.success(true)); + .body(ResponseBody.success(CreateRoomResponse.from(slug))); } @GetMapping @@ -60,22 +66,30 @@ public ResponseEntity> getList( @RequestParam(required = false) Long lastId, @RequestParam(defaultValue = "30") int size ) { - SliceResult result = getListRoomUseCase.getList(new GetListRoomCommand(lastId, size)); + SliceResult result = getListRoomUseCase.getList(new GetListRoomQuery(lastId, size)); List items = result.items().stream() - .map(room -> new RoomSummaryResponse( - room.id(), - room.slug(), - room.title(), - room.isPrivate(), - room.createdAt(), - room.tags().stream() - .map(MusicTagResponse::from) - .toList() - )) + .map(RoomSummaryResponse::from) .toList(); return ResponseEntity.ok() - .body(ResponseBody.success(new ListRoomResponse(items, result.hasNext()))); + .body(ResponseBody.success(ListRoomResponse.of(items, result.hasNext()))); + } + + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{slug}") + public ResponseEntity> delete( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable String slug + ) { + deleteRoomUseCase.delete( + new DeleteRoomCommand( + principal.getUsername(), + slug + ) + ); + + return ResponseEntity.ok() + .body(ResponseBody.success(true)); } } diff --git a/src/main/java/queuing/core/room/presentation/response/CreateRoomResponse.java b/src/main/java/queuing/core/room/presentation/response/CreateRoomResponse.java new file mode 100644 index 0000000..4553a6d --- /dev/null +++ b/src/main/java/queuing/core/room/presentation/response/CreateRoomResponse.java @@ -0,0 +1,9 @@ +package queuing.core.room.presentation.response; + +public record CreateRoomResponse( + String slug +) { + public static CreateRoomResponse from(String slug) { + return new CreateRoomResponse(slug); + } +} diff --git a/src/main/java/queuing/core/room/presentation/response/ListMusicTagResponse.java b/src/main/java/queuing/core/room/presentation/response/ListMusicTagResponse.java index 2f01bbc..2823c75 100755 --- a/src/main/java/queuing/core/room/presentation/response/ListMusicTagResponse.java +++ b/src/main/java/queuing/core/room/presentation/response/ListMusicTagResponse.java @@ -5,4 +5,7 @@ public record ListMusicTagResponse( List tags ) { + public static ListMusicTagResponse of(List tags) { + return new ListMusicTagResponse(tags); + } } diff --git a/src/main/java/queuing/core/room/presentation/response/ListRoomResponse.java b/src/main/java/queuing/core/room/presentation/response/ListRoomResponse.java index 5266ffd..1d7ab75 100755 --- a/src/main/java/queuing/core/room/presentation/response/ListRoomResponse.java +++ b/src/main/java/queuing/core/room/presentation/response/ListRoomResponse.java @@ -6,4 +6,7 @@ public record ListRoomResponse( List rooms, boolean hasNext ) { + public static ListRoomResponse of(List rooms, boolean hasNext) { + return new ListRoomResponse(rooms, hasNext); + } } diff --git a/src/main/java/queuing/core/room/presentation/response/MusicTagResponse.java b/src/main/java/queuing/core/room/presentation/response/MusicTagResponse.java index 146dff0..a4444d2 100755 --- a/src/main/java/queuing/core/room/presentation/response/MusicTagResponse.java +++ b/src/main/java/queuing/core/room/presentation/response/MusicTagResponse.java @@ -1,12 +1,12 @@ package queuing.core.room.presentation.response; -import queuing.core.room.application.dto.MusicTagDto; +import queuing.core.room.application.result.MusicTagResult; public record MusicTagResponse( String slug, String name ) { - public static MusicTagResponse from(MusicTagDto dto) { + public static MusicTagResponse from(MusicTagResult dto) { return new MusicTagResponse(dto.slug(), dto.name()); } } diff --git a/src/main/java/queuing/core/room/presentation/response/RoomSummaryResponse.java b/src/main/java/queuing/core/room/presentation/response/RoomSummaryResponse.java index 198a177..5e74a26 100755 --- a/src/main/java/queuing/core/room/presentation/response/RoomSummaryResponse.java +++ b/src/main/java/queuing/core/room/presentation/response/RoomSummaryResponse.java @@ -3,6 +3,8 @@ import java.time.Instant; import java.util.List; +import queuing.core.room.application.result.RoomSummary; + public record RoomSummaryResponse( long id, String slug, @@ -11,4 +13,16 @@ public record RoomSummaryResponse( Instant createdAt, List tags ) { + public static RoomSummaryResponse from(RoomSummary summary) { + return new RoomSummaryResponse( + summary.id(), + summary.slug(), + summary.title(), + summary.isPrivate(), + summary.createdAt(), + summary.tags().stream() + .map(MusicTagResponse::from) + .toList() + ); + } } diff --git a/src/main/java/queuing/core/user/application/auth/AuthenticationOperations.java b/src/main/java/queuing/core/user/application/auth/AuthenticationOperations.java new file mode 100644 index 0000000..3790128 --- /dev/null +++ b/src/main/java/queuing/core/user/application/auth/AuthenticationOperations.java @@ -0,0 +1,5 @@ +package queuing.core.user.application.auth; + +public interface AuthenticationOperations { + void updateOnboardingStatus(boolean completed); +} diff --git a/src/main/java/queuing/core/user/application/command/UpdateNicknameCommand.java b/src/main/java/queuing/core/user/application/command/UpdateUserProfileCommand.java similarity index 70% rename from src/main/java/queuing/core/user/application/command/UpdateNicknameCommand.java rename to src/main/java/queuing/core/user/application/command/UpdateUserProfileCommand.java index 4907e17..8cb7e5b 100644 --- a/src/main/java/queuing/core/user/application/command/UpdateNicknameCommand.java +++ b/src/main/java/queuing/core/user/application/command/UpdateUserProfileCommand.java @@ -1,6 +1,6 @@ package queuing.core.user.application.command; -public record UpdateNicknameCommand( +public record UpdateUserProfileCommand( String userSlug, String nickname ) { diff --git a/src/main/java/queuing/core/user/application/command/CheckOnboardingCompletedQuery.java b/src/main/java/queuing/core/user/application/query/CheckOnboardingCompletedQuery.java similarity index 55% rename from src/main/java/queuing/core/user/application/command/CheckOnboardingCompletedQuery.java rename to src/main/java/queuing/core/user/application/query/CheckOnboardingCompletedQuery.java index 282d7ff..b82670c 100644 --- a/src/main/java/queuing/core/user/application/command/CheckOnboardingCompletedQuery.java +++ b/src/main/java/queuing/core/user/application/query/CheckOnboardingCompletedQuery.java @@ -1,5 +1,6 @@ -package queuing.core.user.application.command; +package queuing.core.user.application.query; public record CheckOnboardingCompletedQuery( String userSlug -) {} +) { +} diff --git a/src/main/java/queuing/core/user/application/dto/UserProfileDto.java b/src/main/java/queuing/core/user/application/result/UserProfileResult.java similarity index 57% rename from src/main/java/queuing/core/user/application/dto/UserProfileDto.java rename to src/main/java/queuing/core/user/application/result/UserProfileResult.java index 761c292..0d6b5b4 100644 --- a/src/main/java/queuing/core/user/application/dto/UserProfileDto.java +++ b/src/main/java/queuing/core/user/application/result/UserProfileResult.java @@ -1,14 +1,14 @@ -package queuing.core.user.application.dto; +package queuing.core.user.application.result; import queuing.core.user.domain.entity.User; -public record UserProfileDto( +public record UserProfileResult( String nickname, String slug, String profileImageUrl ) { - public static UserProfileDto from(User user) { - return new UserProfileDto( + public static UserProfileResult from(User user) { + return new UserProfileResult( user.getNickname(), user.getSlug(), user.getProfileImageUrl() diff --git a/src/main/java/queuing/core/user/application/service/AuthenticationService.java b/src/main/java/queuing/core/user/application/service/AuthenticationService.java index 30122eb..4efc4f5 100755 --- a/src/main/java/queuing/core/user/application/service/AuthenticationService.java +++ b/src/main/java/queuing/core/user/application/service/AuthenticationService.java @@ -10,8 +10,8 @@ import queuing.core.global.exception.BusinessException; import queuing.core.global.exception.ErrorCode; -import queuing.core.user.application.command.CheckOnboardingCompletedQuery; import queuing.core.user.application.command.SignUpCommand; +import queuing.core.user.application.query.CheckOnboardingCompletedQuery; import queuing.core.user.application.usecase.SignUpUseCase; import queuing.core.user.domain.entity.Role; import queuing.core.user.domain.entity.User; diff --git a/src/main/java/queuing/core/user/application/service/UserReadService.java b/src/main/java/queuing/core/user/application/service/UserReadService.java index 4280c7a..d7a8bab 100644 --- a/src/main/java/queuing/core/user/application/service/UserReadService.java +++ b/src/main/java/queuing/core/user/application/service/UserReadService.java @@ -7,7 +7,7 @@ import queuing.core.global.exception.BusinessException; import queuing.core.global.exception.ErrorCode; -import queuing.core.user.application.dto.UserProfileDto; +import queuing.core.user.application.result.UserProfileResult; import queuing.core.user.application.usecase.GetUserProfileUseCase; import queuing.core.user.domain.repository.UserRepository; @@ -18,14 +18,14 @@ public class UserReadService implements GetUserProfileUseCase { private final UserRepository userRepository; @Override - public UserProfileDto getUserProfile(String slug) { - return userRepository.findBySlug(slug) - .map(UserProfileDto::from) + public UserProfileResult getUserProfile(String userSlug) { + return userRepository.findBySlug(userSlug) + .map(UserProfileResult::from) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); } @Override - public boolean isNickname(String nickname) { + public boolean isNicknameAvailable(String nickname) { return !userRepository.existsByNickname(nickname); } } diff --git a/src/main/java/queuing/core/user/application/service/UserWriteService.java b/src/main/java/queuing/core/user/application/service/UserWriteService.java index aa7224f..84fe906 100644 --- a/src/main/java/queuing/core/user/application/service/UserWriteService.java +++ b/src/main/java/queuing/core/user/application/service/UserWriteService.java @@ -1,5 +1,6 @@ package queuing.core.user.application.service; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -7,42 +8,52 @@ import queuing.core.global.exception.BusinessException; import queuing.core.global.exception.ErrorCode; -import queuing.core.user.application.command.UpdateNicknameCommand; +import queuing.core.user.application.auth.AuthenticationOperations; +import queuing.core.user.application.command.UpdateUserProfileCommand; import queuing.core.user.application.usecase.UpdateUserProfileUseCase; import queuing.core.user.domain.entity.User; import queuing.core.user.domain.repository.UserRepository; @Service @RequiredArgsConstructor -@Transactional public class UserWriteService implements UpdateUserProfileUseCase { private final UserRepository userRepository; + private final AuthenticationOperations authenticationOperations; @Override - public void updateUserProfile(UpdateNicknameCommand cmd) { - User user = userRepository.findBySlug(cmd.userSlug()) + @Transactional + public void updateUserProfile(UpdateUserProfileCommand command) { + User user = userRepository.findBySlug(command.userSlug()) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - if (!cmd.nickname().equals(user.getNickname()) && userRepository.existsByNickname(cmd.nickname())) { - throw new BusinessException(ErrorCode.USER_NICKNAME_DUPLICATED); - } + validateNickname(user, command.nickname()); - user.updateNickname(cmd.nickname()); + user.changeProfile(command.nickname(), null); } @Override - public void completeOnboarding(UpdateNicknameCommand cmd) { - User user = userRepository.findBySlug(cmd.userSlug()) + @Transactional + @CacheEvict(cacheNames = "profileCompleted", key = "#command.userSlug()") + public void completeOnboarding(UpdateUserProfileCommand command) { + User user = userRepository.findBySlug(command.userSlug()) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); if (user.isOnboardingCompleted()) { throw new BusinessException(ErrorCode.USER_INSUFFICIENT_SCOPE); } - if (!cmd.nickname().equals(user.getNickname()) && userRepository.existsByNickname(cmd.nickname())) { + validateNickname(user, command.nickname()); + + user.changeProfile(command.nickname(), null); + userRepository.saveAndFlush(user); + + authenticationOperations.updateOnboardingStatus(true); + } + + private void validateNickname(User user, String newNickname) { + if (newNickname != null && !newNickname.equals(user.getNickname()) + && userRepository.existsByNickname(newNickname)) { throw new BusinessException(ErrorCode.USER_NICKNAME_DUPLICATED); } - - user.updateNickname(cmd.nickname()); } } diff --git a/src/main/java/queuing/core/user/application/usecase/GetUserProfileUseCase.java b/src/main/java/queuing/core/user/application/usecase/GetUserProfileUseCase.java index 1dfb1f4..f79724d 100644 --- a/src/main/java/queuing/core/user/application/usecase/GetUserProfileUseCase.java +++ b/src/main/java/queuing/core/user/application/usecase/GetUserProfileUseCase.java @@ -1,9 +1,9 @@ package queuing.core.user.application.usecase; -import queuing.core.user.application.dto.UserProfileDto; +import queuing.core.user.application.result.UserProfileResult; public interface GetUserProfileUseCase { - UserProfileDto getUserProfile(String slug); + UserProfileResult getUserProfile(String userSlug); - boolean isNickname(String nickname); + boolean isNicknameAvailable(String nickname); } diff --git a/src/main/java/queuing/core/user/application/usecase/SignUpUseCase.java b/src/main/java/queuing/core/user/application/usecase/SignUpUseCase.java index 6a93c84..f430aad 100644 --- a/src/main/java/queuing/core/user/application/usecase/SignUpUseCase.java +++ b/src/main/java/queuing/core/user/application/usecase/SignUpUseCase.java @@ -1,11 +1,11 @@ package queuing.core.user.application.usecase; -import queuing.core.user.application.command.CheckOnboardingCompletedQuery; import queuing.core.user.application.command.SignUpCommand; +import queuing.core.user.application.query.CheckOnboardingCompletedQuery; import queuing.core.user.domain.entity.User; public interface SignUpUseCase { - User signUp(SignUpCommand cmd); + User signUp(SignUpCommand command); boolean isOnboardingCompleted(CheckOnboardingCompletedQuery query); } diff --git a/src/main/java/queuing/core/user/application/usecase/UpdateUserProfileUseCase.java b/src/main/java/queuing/core/user/application/usecase/UpdateUserProfileUseCase.java index 85ef2e0..dbdaaaf 100644 --- a/src/main/java/queuing/core/user/application/usecase/UpdateUserProfileUseCase.java +++ b/src/main/java/queuing/core/user/application/usecase/UpdateUserProfileUseCase.java @@ -1,9 +1,9 @@ package queuing.core.user.application.usecase; -import queuing.core.user.application.command.UpdateNicknameCommand; +import queuing.core.user.application.command.UpdateUserProfileCommand; public interface UpdateUserProfileUseCase { - void completeOnboarding(UpdateNicknameCommand cmd); + void completeOnboarding(UpdateUserProfileCommand cmd); - void updateUserProfile(UpdateNicknameCommand cmd); + void updateUserProfile(UpdateUserProfileCommand cmd); } diff --git a/src/main/java/queuing/core/user/domain/entity/User.java b/src/main/java/queuing/core/user/domain/entity/User.java index d6998c2..1f90991 100755 --- a/src/main/java/queuing/core/user/domain/entity/User.java +++ b/src/main/java/queuing/core/user/domain/entity/User.java @@ -1,6 +1,7 @@ package queuing.core.user.domain.entity; import java.time.Instant; +import java.util.Objects; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -85,7 +86,29 @@ public boolean isOnboardingCompleted() { return isCompleted; } - public void updateNickname(String nickname) { - this.nickname = nickname; + public void changeProfile(String nickname, String profileImageUrl) { + if (nickname != null && !nickname.isBlank()) { + this.nickname = nickname; + } + if (profileImageUrl != null && !profileImageUrl.isBlank()) { + this.profileImageUrl = profileImageUrl; + } + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof User user)) { + return false; + } + + return this.getId() != null && Objects.equals(this.getId(), user.getId()); + } + + @Override + public final int hashCode() { + return Objects.hash(this.getId()); } } diff --git a/src/main/java/queuing/core/user/presentation/controller/UserProfileController.java b/src/main/java/queuing/core/user/presentation/controller/UserProfileController.java index d6ec360..9e4d78b 100644 --- a/src/main/java/queuing/core/user/presentation/controller/UserProfileController.java +++ b/src/main/java/queuing/core/user/presentation/controller/UserProfileController.java @@ -19,8 +19,8 @@ import queuing.core.global.response.ResponseBody; import queuing.core.global.security.authorization.UserPrincipal; -import queuing.core.user.application.command.UpdateNicknameCommand; -import queuing.core.user.application.dto.UserProfileDto; +import queuing.core.user.application.command.UpdateUserProfileCommand; +import queuing.core.user.application.result.UserProfileResult; import queuing.core.user.application.usecase.GetUserProfileUseCase; import queuing.core.user.application.usecase.UpdateUserProfileUseCase; import queuing.core.user.presentation.request.UpdateUserProfileRequest; @@ -38,16 +38,16 @@ public class UserProfileController { public ResponseEntity> getMyProfile( @AuthenticationPrincipal UserPrincipal principal ) { - UserProfileDto userProfile = getUserProfileUseCase.getUserProfile(principal.getUsername()); + UserProfileResult userProfile = getUserProfileUseCase.getUserProfile(principal.getUsername()); return ResponseEntity.ok(ResponseBody.success(UserProfileResponse.from(userProfile))); } - @GetMapping("/{slug}") + @GetMapping("/{userSlug}") @PreAuthorize("isAuthenticated()") public ResponseEntity> getUserProfile( - @PathVariable String slug + @PathVariable String userSlug ) { - UserProfileDto userProfile = getUserProfileUseCase.getUserProfile(slug); + UserProfileResult userProfile = getUserProfileUseCase.getUserProfile(userSlug); return ResponseEntity.ok(ResponseBody.success(UserProfileResponse.from(userProfile))); } @@ -58,7 +58,7 @@ public ResponseEntity> checkNickname( @Size(min = 2, max = 20, message = "닉네임은 2 ~ 20자 사이여야 해요.") @Valid @RequestParam String nickname ) { - return ResponseEntity.ok(ResponseBody.success(getUserProfileUseCase.isNickname(nickname))); + return ResponseEntity.ok(ResponseBody.success(getUserProfileUseCase.isNicknameAvailable(nickname))); } @PatchMapping("/me/onboarding") @@ -67,7 +67,9 @@ public ResponseEntity> completeOnboarding( @AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UpdateUserProfileRequest request ) { - updateUserProfileUseCase.completeOnboarding(new UpdateNicknameCommand(principal.getUsername(), request.nickname())); + updateUserProfileUseCase.completeOnboarding( + new UpdateUserProfileCommand(principal.getUsername(), request.nickname()) + ); return ResponseEntity.ok(ResponseBody.success(true)); } @@ -77,7 +79,10 @@ public ResponseEntity> updateProfile( @AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UpdateUserProfileRequest request ) { - updateUserProfileUseCase.updateUserProfile(new UpdateNicknameCommand(principal.getUsername(), request.nickname())); + updateUserProfileUseCase.updateUserProfile( + new UpdateUserProfileCommand(principal.getUsername(), request.nickname()) + ); return ResponseEntity.ok(ResponseBody.success(true)); } } + diff --git a/src/main/java/queuing/core/user/presentation/response/UserProfileResponse.java b/src/main/java/queuing/core/user/presentation/response/UserProfileResponse.java index 0e90202..159bb3e 100644 --- a/src/main/java/queuing/core/user/presentation/response/UserProfileResponse.java +++ b/src/main/java/queuing/core/user/presentation/response/UserProfileResponse.java @@ -1,22 +1,13 @@ package queuing.core.user.presentation.response; -import queuing.core.user.application.dto.UserProfileDto; -import queuing.core.user.domain.entity.User; +import queuing.core.user.application.result.UserProfileResult; public record UserProfileResponse( String nickname, String slug, String profileImageUrl ) { - public static UserProfileResponse from(User user) { - return new UserProfileResponse( - user.getNickname(), - user.getSlug(), - user.getProfileImageUrl() - ); - } - - public static UserProfileResponse from(UserProfileDto dto) { + public static UserProfileResponse from(UserProfileResult dto) { return new UserProfileResponse( dto.nickname(), dto.slug(), diff --git a/src/test/java/queuing/core/CoreApplicationTests.java b/src/test/java/queuing/core/CoreApplicationTests.java index 542a290..65dc1db 100644 --- a/src/test/java/queuing/core/CoreApplicationTests.java +++ b/src/test/java/queuing/core/CoreApplicationTests.java @@ -7,9 +7,7 @@ @SpringBootTest @ActiveProfiles("test") class CoreApplicationTests { - - @Test - void contextLoads() { - } - + @Test + void contextLoads() { + } }