-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT][BUG] 사용자 시민해결사 목록 조회 및 알람 전송 시간 영역 재설정 #360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package issueissyu.backend.domain.user.dto.res; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonFormat; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| public record UserMySolverItemResDTO( | ||
| Long pinId, | ||
| String pinTitle, | ||
| String pinDetailAddress, | ||
| String issuePinState, | ||
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSSSSS") LocalDateTime createdAt) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package issueissyu.backend.domain.user.dto.res; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record UserMySolversResDTO(List<UserMySolverItemResDTO> pins, UserMyPinPageInfoResDTO pageInfo) {} |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||||||||||||||||||||||
| package issueissyu.backend.domain.user.repository; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import issueissyu.backend.domain.issue.entity.ProblemSolver; | ||||||||||||||||||||||||||
| import java.time.LocalDateTime; | ||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||
| import org.springframework.data.jpa.repository.JpaRepository; | ||||||||||||||||||||||||||
| import org.springframework.data.jpa.repository.Query; | ||||||||||||||||||||||||||
| import org.springframework.data.repository.query.Param; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| public interface UserMySolverRepository extends JpaRepository<ProblemSolver, Long> { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @Query( | ||||||||||||||||||||||||||
| value = | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| SELECT p.pin_id AS pinId, | ||||||||||||||||||||||||||
| p.pin_title AS pinTitle, | ||||||||||||||||||||||||||
| pl.detail_address AS pinDetailAddress, | ||||||||||||||||||||||||||
| ip.issue_pin_state AS issuePinState, | ||||||||||||||||||||||||||
| ps.created_at AS createdAt, | ||||||||||||||||||||||||||
| ps.problem_solver_id AS problemSolverId | ||||||||||||||||||||||||||
| FROM problem_solver ps | ||||||||||||||||||||||||||
| JOIN issue_pin ip ON ip.issue_pin_id = ps.issue_pin_id | ||||||||||||||||||||||||||
| JOIN pin p ON p.pin_id = ip.pin_id | ||||||||||||||||||||||||||
| LEFT JOIN LATERAL ( | ||||||||||||||||||||||||||
| SELECT pl2.detail_address | ||||||||||||||||||||||||||
| FROM pin_location pl2 | ||||||||||||||||||||||||||
| WHERE pl2.pin_id = p.pin_id | ||||||||||||||||||||||||||
| ORDER BY pl2.pin_location_id ASC | ||||||||||||||||||||||||||
| LIMIT 1 | ||||||||||||||||||||||||||
| ) pl ON TRUE | ||||||||||||||||||||||||||
| WHERE ps.uid = :uid | ||||||||||||||||||||||||||
| AND ps.problem_solve_state = 'EN_ROUTE' | ||||||||||||||||||||||||||
| AND ( | ||||||||||||||||||||||||||
| NOT CAST(:applyCursor AS boolean) | ||||||||||||||||||||||||||
| OR ps.created_at < CAST(:cursorCreatedAt AS timestamp) | ||||||||||||||||||||||||||
| OR ( | ||||||||||||||||||||||||||
| ps.created_at = CAST(:cursorCreatedAt AS timestamp) | ||||||||||||||||||||||||||
| AND ps.problem_solver_id < :cursorProblemSolverId | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
Comment on lines
+33
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PostgreSQL은 다중 열 비교(Row-value comparison)를 지원합니다. 기존의 복잡한 OR 조건문 대신 행 생성자(Row Constructor) 비교 연산자 (ps.created_at, ps.problem_solver_id) < (CAST(:cursorCreatedAt AS timestamp), :cursorProblemSolverId)를 사용하면 쿼리가 훨씬 간결해지고 가독성이 향상되며, 다중 열 인덱스(Multi-column index)를 활용한 최적화에도 유리합니다.
Suggested change
|
||||||||||||||||||||||||||
| ORDER BY ps.created_at DESC, ps.problem_solver_id DESC | ||||||||||||||||||||||||||
| LIMIT :limit | ||||||||||||||||||||||||||
| """, | ||||||||||||||||||||||||||
| nativeQuery = true) | ||||||||||||||||||||||||||
| List<UserMySolverRow> findMySolvers( | ||||||||||||||||||||||||||
| @Param("uid") String uid, | ||||||||||||||||||||||||||
| @Param("applyCursor") boolean applyCursor, | ||||||||||||||||||||||||||
| @Param("cursorCreatedAt") LocalDateTime cursorCreatedAt, | ||||||||||||||||||||||||||
| @Param("cursorProblemSolverId") long cursorProblemSolverId, | ||||||||||||||||||||||||||
| @Param("limit") int limit); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package issueissyu.backend.domain.user.repository; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public interface UserMySolverRow { | ||
| Long getPinId(); | ||
|
|
||
| String getPinTitle(); | ||
|
|
||
| String getPinDetailAddress(); | ||
|
|
||
| String getIssuePinState(); | ||
|
|
||
| LocalDateTime getCreatedAt(); | ||
|
|
||
| Long getProblemSolverId(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package issueissyu.backend.domain.user.service.query; | ||
|
|
||
| import issueissyu.backend.domain.user.dto.res.UserMySolversResDTO; | ||
|
|
||
| public interface UserSolverQueryService { | ||
|
|
||
| UserMySolversResDTO getMySolvers(String uid, Integer size, String cursor); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||
| package issueissyu.backend.domain.user.service.query; | ||||||||||
|
|
||||||||||
| import issueissyu.backend.domain.user.dto.res.UserMyPinPageInfoResDTO; | ||||||||||
| import issueissyu.backend.domain.user.dto.res.UserMySolverItemResDTO; | ||||||||||
| import issueissyu.backend.domain.user.dto.res.UserMySolversResDTO; | ||||||||||
| import issueissyu.backend.domain.user.exception.UserException; | ||||||||||
| import issueissyu.backend.domain.user.exception.code.UserErrorCode; | ||||||||||
| import issueissyu.backend.domain.user.repository.UserMySolverRepository; | ||||||||||
| import issueissyu.backend.domain.user.repository.UserMySolverRow; | ||||||||||
| import issueissyu.backend.domain.user.repository.UserRepository; | ||||||||||
| import issueissyu.backend.domain.user.support.UserSolverCursorCodec; | ||||||||||
| import issueissyu.backend.global.api.code.GeneralErrorCode; | ||||||||||
| import issueissyu.backend.global.exception.GeneralException; | ||||||||||
| import java.time.LocalDateTime; | ||||||||||
| import java.util.List; | ||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||
| import org.springframework.stereotype.Service; | ||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||
| import org.springframework.util.StringUtils; | ||||||||||
|
|
||||||||||
| @Service | ||||||||||
| @RequiredArgsConstructor | ||||||||||
| @Transactional(readOnly = true) | ||||||||||
| public class UserSolverQueryServiceImpl implements UserSolverQueryService { | ||||||||||
|
|
||||||||||
| private static final int SIZE_DEFAULT = 10; | ||||||||||
| private static final int SIZE_MIN = 1; | ||||||||||
| private static final int SIZE_MAX = 100; | ||||||||||
|
|
||||||||||
| private static final LocalDateTime CURSOR_DUMMY_TIME = LocalDateTime.of(1970, 1, 1, 0, 0, 0); | ||||||||||
|
|
||||||||||
| private final UserRepository userRepository; | ||||||||||
| private final UserMySolverRepository userMySolverRepository; | ||||||||||
| private final UserSolverCursorCodec userSolverCursorCodec; | ||||||||||
|
|
||||||||||
| @Override | ||||||||||
| public UserMySolversResDTO getMySolvers(String uid, Integer size, String cursor) { | ||||||||||
| int pageSize = resolveSize(size); | ||||||||||
|
|
||||||||||
| if (!userRepository.existsById(uid)) { | ||||||||||
| throw GeneralException.of(GeneralErrorCode.USER_NOT_FOUND); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자의 존재 여부만 확인하는 로직에서 userRepository.findById(uid)를 사용하면 해당 User 엔티티 전체와 연관된 관계들을 영속성 컨텍스트에 로드하게 되므로 불필요한 메모리 및 쿼리 오버헤드가 발생할 수 있습니다.\n\n단순히 존재 여부만 검증할 때는 existsById(uid)를 사용하는 것이 성능상 훨씬 효율적입니다.
Suggested change
|
||||||||||
| boolean applyCursor = StringUtils.hasText(cursor); | ||||||||||
| LocalDateTime cursorCreatedAt = CURSOR_DUMMY_TIME; | ||||||||||
| long cursorProblemSolverId = 0L; | ||||||||||
| if (applyCursor) { | ||||||||||
| UserSolverCursorCodec.Decoded decoded = userSolverCursorCodec.decode(cursor.trim()); | ||||||||||
| cursorCreatedAt = decoded.createdAt(); | ||||||||||
| cursorProblemSolverId = decoded.problemSolverId(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| List<UserMySolverRow> rows = | ||||||||||
| userMySolverRepository.findMySolvers( | ||||||||||
| uid, applyCursor, cursorCreatedAt, cursorProblemSolverId, pageSize + 1); | ||||||||||
|
|
||||||||||
| boolean hasNext = rows.size() > pageSize; | ||||||||||
| List<UserMySolverRow> pageRows = hasNext ? rows.subList(0, pageSize) : rows; | ||||||||||
|
|
||||||||||
| List<UserMySolverItemResDTO> pins = | ||||||||||
| pageRows.stream() | ||||||||||
| .map( | ||||||||||
| r -> | ||||||||||
| new UserMySolverItemResDTO( | ||||||||||
| r.getPinId(), | ||||||||||
| r.getPinTitle(), | ||||||||||
| r.getPinDetailAddress(), | ||||||||||
| r.getIssuePinState(), | ||||||||||
| r.getCreatedAt())) | ||||||||||
| .toList(); | ||||||||||
|
|
||||||||||
| String nextCursor = null; | ||||||||||
| if (hasNext && !pageRows.isEmpty()) { | ||||||||||
| UserMySolverRow last = pageRows.get(pageRows.size() - 1); | ||||||||||
| nextCursor = userSolverCursorCodec.encode(last.getCreatedAt(), last.getProblemSolverId()); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return new UserMySolversResDTO(pins, new UserMyPinPageInfoResDTO(hasNext, nextCursor)); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private int resolveSize(Integer size) { | ||||||||||
| int s = size == null ? SIZE_DEFAULT : size; | ||||||||||
| if (s < SIZE_MIN || s > SIZE_MAX) { | ||||||||||
| throw UserException.of(UserErrorCode.USER_SOLVER_400_1); | ||||||||||
| } | ||||||||||
| return s; | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package issueissyu.backend.domain.user.support; | ||
|
|
||
| import issueissyu.backend.domain.user.exception.UserException; | ||
| import issueissyu.backend.domain.user.exception.code.UserErrorCode; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.time.format.DateTimeFormatterBuilder; | ||
| import java.time.temporal.ChronoField; | ||
| import java.util.Base64; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class UserSolverCursorCodec { | ||
|
|
||
| private static final DateTimeFormatter CURSOR_TIME = | ||
| new DateTimeFormatterBuilder() | ||
| .appendPattern("yyyy-MM-dd HH:mm:ss") | ||
| .appendFraction(ChronoField.NANO_OF_SECOND, 6, 9, true) | ||
| .toFormatter(); | ||
|
|
||
| public record Decoded(LocalDateTime createdAt, long problemSolverId) {} | ||
|
|
||
| public String encode(LocalDateTime createdAt, long problemSolverId) { | ||
| String raw = createdAt.format(CURSOR_TIME) + ":" + problemSolverId; | ||
| return Base64.getUrlEncoder() | ||
| .withoutPadding() | ||
| .encodeToString(raw.getBytes(StandardCharsets.UTF_8)); | ||
| } | ||
|
|
||
| public Decoded decode(String cursor) { | ||
| if (cursor == null || cursor.isBlank()) { | ||
| throw UserException.of(UserErrorCode.USER_SOLVER_400_2); | ||
| } | ||
| try { | ||
| String raw = new String(Base64.getUrlDecoder().decode(cursor.trim()), StandardCharsets.UTF_8); | ||
| int delim = raw.lastIndexOf(':'); | ||
| if (delim < 0 || delim == raw.length() - 1) { | ||
| throw UserException.of(UserErrorCode.USER_SOLVER_400_2); | ||
| } | ||
| String createdAtRaw = raw.substring(0, delim); | ||
| String problemSolverIdRaw = raw.substring(delim + 1); | ||
| long problemSolverId = Long.parseLong(problemSolverIdRaw); | ||
| if (problemSolverId <= 0) { | ||
| throw UserException.of(UserErrorCode.USER_SOLVER_400_2); | ||
| } | ||
| LocalDateTime createdAt = LocalDateTime.parse(createdAtRaw, CURSOR_TIME); | ||
| return new Decoded(createdAt, problemSolverId); | ||
| } catch (UserException e) { | ||
| throw e; | ||
| } catch (Exception e) { | ||
| throw UserException.of(UserErrorCode.USER_SOLVER_400_2); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.