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 extends GrantedAuthority> authorities;
private Map attributes;
private OidcIdToken idToken;
+ private final boolean onboardingCompleted;
public UserPrincipal(String slug, String email, Collection extends GrantedAuthority> 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 extends GrantedAuthority> authorities) {
+ public UserPrincipal(
+ String slug,
+ String email,
+ Collection extends GrantedAuthority> 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() {
+ }
}