Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public ApiResponse<LikeAlarmSendResDTO> sendLikeAlarm(
description =
"""
동네 인증 지역의 축제(FESTIVAL) 핀에 대한 푸시 알람을 요청자에게 전송합니다.
event_start_time 이 현재 시각 ±12시간 이내인 핀만 대상이며, 제목·본문은 서버에서 고정값으로 생성합니다.
event_start_time 이 현재 시각 24시간 이내인 핀만 대상이며, 제목·본문은 서버에서 고정값으로 생성합니다.
user.event_alarm_active 가 false 이면 EVENT_ALARM_403 을 반환합니다.
EVENT_ALARM_404_1(동네 인증 없음), EVENT_ALARM_404_2(대상 축제 없음), EVENT_ALARM_404_3(커뮤니티 없음).
알람 클릭 시 GET /api/communities/{communityId} 로 이동합니다.
Expand All @@ -101,7 +101,7 @@ public ApiResponse<EventAlarmSendResDTO> sendEventAlarm(@AuthenticationPrincipal
description =
"""
동네 인증 지역의 가게(STORE) 핀에 대한 푸시 알람을 요청자에게 전송합니다.
event_start_time 이 현재 시각 ±12시간 이내인 핀만 대상이며, 제목·본문은 서버에서 고정값으로 생성합니다.
event_start_time 이 현재 시각 24시간 이내인 핀만 대상이며, 제목·본문은 서버에서 고정값으로 생성합니다.
user.store_alarm_active 가 false 이면 STORE_ALARM_403 을 반환합니다.
알람 클릭 시 GET /api/communities/{communityId} 로 이동합니다.
""")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,9 @@ private void dispatchFcmBatch(List<FcmNotificationPayload> payloads, String alar
fcmService.sendNotificationsBatchAsync(payloads);
log.info("[RegionalAlarm] {} FCM batch dispatched count={}", alarmType, payloads.size());
}

private static LocalDateTime[] alarmTimeWindow() {
LocalDateTime now = LocalDateTime.now();
return new LocalDateTime[] {now.minusHours(12), now.plusHours(12)};
LocalDateTime now = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"));
return new LocalDateTime[] {now.minusHours(24), now};
}

private User findUser(String uid) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ public interface CommunityRepository extends JpaRepository<Community, Long> {
join fetch c.pin p
join fetch p.user
where c.communityType = :type
and (
:type not in (
issueissyu.backend.domain.community.enums.CommunityType.STORE,
issueissyu.backend.domain.community.enums.CommunityType.FESTIVAL
)
or exists (
select 1
from EventPin ep
where ep.pin = p
and :now between ep.eventStartTime and ep.eventEndTime
)
)
and exists (
select 1
from PinLocation pl
Expand All @@ -62,6 +74,7 @@ and exists (
""")
List<Community> findFeedByTypeAndRegion(
@Param("type") CommunityType type,
@Param("now") LocalDateTime now,
@Param("locationId") Long locationId,
@Param("cursorCreatedAt") LocalDateTime cursorCreatedAt,
@Param("cursorId") Long cursorId,
Expand All @@ -80,6 +93,18 @@ List<Community> findFeedByTypeAndRegion(
join fetch c.pin p
join fetch p.user
where c.communityType in :types
and (
c.communityType not in (
issueissyu.backend.domain.community.enums.CommunityType.STORE,
issueissyu.backend.domain.community.enums.CommunityType.FESTIVAL
)
or exists (
select 1
from EventPin ep
where ep.pin = p
and :now between ep.eventStartTime and ep.eventEndTime
)
)
and exists (
select 1
from PinLocation pl
Expand All @@ -95,6 +120,7 @@ and exists (
""")
List<Community> findFeedByTypesAndRegion(
@Param("types") Collection<CommunityType> types,
@Param("now") LocalDateTime now,
@Param("locationId") Long locationId,
@Param("cursorCreatedAt") LocalDateTime cursorCreatedAt,
@Param("cursorId") Long cursorId,
Expand Down Expand Up @@ -175,6 +201,18 @@ List<Community> findCardnewsFeedByTypesWithImages(
where (
(
c.communityType in :regionTypes
and (
c.communityType not in (
issueissyu.backend.domain.community.enums.CommunityType.STORE,
issueissyu.backend.domain.community.enums.CommunityType.FESTIVAL
)
or exists (
select 1
from EventPin ep
where ep.pin = p
and :now between ep.eventStartTime and ep.eventEndTime
)
)
and exists (
select 1
from PinLocation pl
Expand All @@ -194,6 +232,7 @@ and exists (
List<Community> findFeedByRegionOrGlobalTypes(
@Param("regionTypes") Collection<CommunityType> regionTypes,
@Param("globalTypes") Collection<CommunityType> globalTypes,
@Param("now") LocalDateTime now,
@Param("locationId") Long locationId,
@Param("cursorCreatedAt") LocalDateTime cursorCreatedAt,
@Param("cursorId") Long cursorId,
Expand Down Expand Up @@ -221,6 +260,18 @@ List<Community> findFeedByRegionOrGlobalTypes(
where (
(
c.communityType in :regionTypes
and (
c.communityType not in (
issueissyu.backend.domain.community.enums.CommunityType.STORE,
issueissyu.backend.domain.community.enums.CommunityType.FESTIVAL
)
or exists (
select 1
from EventPin ep
where ep.pin = p
and :now between ep.eventStartTime and ep.eventEndTime
)
)
and exists (
select 1
from PinLocation pl
Expand All @@ -241,6 +292,7 @@ and exists (
List<Community> findHotFeedByRegionOrGlobalTypes(
@Param("regionTypes") Collection<CommunityType> regionTypes,
@Param("globalTypes") Collection<CommunityType> globalTypes,
@Param("now") LocalDateTime now,
@Param("locationId") Long locationId,
@Param("since") LocalDateTime since,
@Param("cursorPopularity") Double cursorPopularity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ public class CommunityHotQueryServiceImpl implements CommunityHotQueryService {

@Override
public Optional<HotCommunityTarget> findTopHotInRegion(Long locationId) {
LocalDateTime now = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"));
LocalDateTime since = LocalDateTime.now().minusDays(HOT_DAYS);
Pageable limit = PageRequest.of(0, 1);

List<Community> communities = communityRepository.findHotFeedByRegionOrGlobalTypes(
REGION_BASED_FEED_TYPES,
GLOBAL_FEED_TYPES,
now,
locationId,
since,
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,12 @@ public CommunityQueryService.CommunityDetailResult getCommunityDetail(

private List<Community> fetchCommunities(CommunityTab tab, Long locationId, CursorKey cursorKey) {
Pageable limit = PageRequest.of(0, sizeWithLookahead(cursorKey.requestSize()));
LocalDateTime now = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"));

return switch (tab) {
case ISSUE -> communityRepository.findFeedByTypeAndRegion(
CommunityType.ISSUE,
now,
locationId,
cursorKey.createdAt(),
cursorKey.communityId(),
Expand All @@ -261,6 +263,7 @@ private List<Community> fetchCommunities(CommunityTab tab, Long locationId, Curs

case STORE -> communityRepository.findFeedByTypeAndRegion(
CommunityType.STORE,
now,
locationId,
cursorKey.createdAt(),
cursorKey.communityId(),
Expand All @@ -269,6 +272,7 @@ private List<Community> fetchCommunities(CommunityTab tab, Long locationId, Curs

case COMMUNICATION -> communityRepository.findFeedByTypeAndRegion(
CommunityType.COMMUNICATION,
now,
locationId,
cursorKey.createdAt(),
cursorKey.communityId(),
Expand All @@ -277,6 +281,7 @@ private List<Community> fetchCommunities(CommunityTab tab, Long locationId, Curs

case FESTIVAL -> communityRepository.findFeedByTypeAndRegion(
CommunityType.FESTIVAL,
now,
locationId,
cursorKey.createdAt(),
cursorKey.communityId(),
Expand Down Expand Up @@ -307,6 +312,7 @@ private List<Community> fetchCommunities(CommunityTab tab, Long locationId, Curs
case ALL -> communityRepository.findFeedByRegionOrGlobalTypes(
REGION_BASED_FEED_TYPES,
GLOBAL_FEED_TYPES,
now,
locationId,
cursorKey.createdAt(),
cursorKey.communityId(),
Expand Down Expand Up @@ -344,9 +350,11 @@ private boolean usesRegion(CommunityTab tab) {
private List<CommunityFeedItemResDTO> fetchStorePromotions(Long locationId, int storeSize) {
int resolvedSize = Math.min(Math.max(1, storeSize), MAX_STORE_SIZE);
Pageable limit = PageRequest.of(0, resolvedSize);
LocalDateTime now = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"));

List<Community> communities = communityRepository.findFeedByTypeAndRegion(
CommunityType.STORE,
now,
locationId,
null,
null,
Expand All @@ -357,12 +365,14 @@ private List<CommunityFeedItemResDTO> fetchStorePromotions(Long locationId, int
}

private List<CommunityFeedItemResDTO> fetchHotPreviews(Long locationId) {
LocalDateTime now = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"));
LocalDateTime since = LocalDateTime.now().minusDays(HOT_DAYS);
Pageable limit = PageRequest.of(0, HOT_PREVIEW_SIZE);

return toFeedItems(communityRepository.findHotFeedByRegionOrGlobalTypes(
REGION_BASED_FEED_TYPES,
GLOBAL_FEED_TYPES,
now,
locationId,
since,
null,
Expand All @@ -374,9 +384,11 @@ private List<CommunityFeedItemResDTO> fetchHotPreviews(Long locationId) {
private CommunityCursorPageResDTO fetchRecentNews(Long locationId, String recentCursor, int recentSize) {
CursorKey cursorKey = CursorKey.parse(recentCursor, recentSize);
Pageable limit = PageRequest.of(0, sizeWithLookahead(cursorKey.requestSize()));
LocalDateTime now = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"));

List<Community> communities = communityRepository.findFeedByTypesAndRegion(
REGION_BASED_FEED_TYPES,
now,
locationId,
cursorKey.createdAt(),
cursorKey.communityId(),
Expand Down Expand Up @@ -715,12 +727,14 @@ private String resolveWriterProfileUrl(CommunityType type, Pin pin) {

private CommunityCursorPageResDTO getHotFeed(Long locationId, String cursor, int size) {
HotCursorKey cursorKey = HotCursorKey.parse(cursor, size);
LocalDateTime now = LocalDateTime.now(java.time.ZoneId.of("Asia/Seoul"));
LocalDateTime since = LocalDateTime.now().minusDays(HOT_DAYS);
Pageable limit = PageRequest.of(0, sizeWithLookahead(size));

List<Community> communities = communityRepository.findHotFeedByRegionOrGlobalTypes(
REGION_BASED_FEED_TYPES,
GLOBAL_FEED_TYPES,
now,
locationId,
since,
cursorKey.popularity(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public interface PinLocationRepository extends JpaRepository<PinLocation, Long>
INNER JOIN location l ON pl.location_id = l.location_id
LEFT JOIN communication_pin cp ON cp.pin_id = p.pin_id
LEFT JOIN event_pin ep ON ep.pin_id = p.pin_id
WHERE p.created_at >= NOW() - INTERVAL '1 year'
WHERE p.created_at >= (NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '1 year'
AND ST_Within(
pl.pin_point,
ST_MakeEnvelope(:swLng, :swLat, :neLng, :neLat, 4326)
Expand All @@ -55,10 +55,10 @@ AND ST_Within(
p.pin_type = 'ISSUE'
OR (p.pin_type = 'COMMUNICATION'
AND cp.communication_pin_id IS NOT NULL
AND COALESCE(cp.updated_at, cp.created_at) >= NOW() - INTERVAL '1 month')
AND COALESCE(cp.updated_at, cp.created_at) >= (NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '1 month')
OR (p.pin_type IN ('STORE', 'FESTIVAL')
AND ep.event_pin_id IS NOT NULL
AND NOW() BETWEEN ep.event_start_time AND ep.event_end_time)
AND (NOW() AT TIME ZONE 'Asia/Seoul') BETWEEN ep.event_start_time AND ep.event_end_time)
)
AND (:pinTypeFilter IS NULL OR p.pin_type = :pinTypeFilter)
""", nativeQuery = true)
Expand All @@ -85,7 +85,7 @@ List<MapPinView> findPinsInBoundingBox(
INNER JOIN location l ON pl.location_id = l.location_id
LEFT JOIN communication_pin cp ON cp.pin_id = p.pin_id
LEFT JOIN event_pin ep ON ep.pin_id = p.pin_id
WHERE p.created_at >= NOW() - INTERVAL '1 year'
WHERE p.created_at >= (NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '1 year'
AND ST_Within(
pl.pin_point,
ST_MakeEnvelope(:swLng, :swLat, :neLng, :neLat, 4326)
Expand All @@ -94,10 +94,10 @@ AND ST_Within(
p.pin_type = 'ISSUE'
OR (p.pin_type = 'COMMUNICATION'
AND cp.communication_pin_id IS NOT NULL
AND COALESCE(cp.updated_at, cp.created_at) >= NOW() - INTERVAL '1 month')
AND COALESCE(cp.updated_at, cp.created_at) >= (NOW() AT TIME ZONE 'Asia/Seoul') - INTERVAL '1 month')
OR (p.pin_type IN ('STORE', 'FESTIVAL')
AND ep.event_pin_id IS NOT NULL
AND NOW() BETWEEN ep.event_start_time AND ep.event_end_time)
AND (NOW() AT TIME ZONE 'Asia/Seoul') BETWEEN ep.event_start_time AND ep.event_end_time)
)
AND (:pinTypeFilter IS NULL OR p.pin_type = :pinTypeFilter)
ORDER BY clusterLat, clusterLng, p.pin_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import issueissyu.backend.domain.user.dto.res.UserAlarmStateResDTO;
import issueissyu.backend.domain.user.dto.res.UserAlarmToggleResDTO;
import issueissyu.backend.domain.user.dto.res.UserMyPinsResDTO;
import issueissyu.backend.domain.user.dto.res.UserMySolversResDTO;
import issueissyu.backend.domain.user.enums.UserAlarmType;
import issueissyu.backend.domain.alarm.exception.code.AlarmSuccessCode;
import issueissyu.backend.domain.user.exception.code.UserSuccessCode;
import issueissyu.backend.domain.user.service.command.UserCommandService;
import issueissyu.backend.domain.user.service.query.UserAlarmStateQueryService;
import issueissyu.backend.domain.user.service.query.UserPinQueryService;
import issueissyu.backend.domain.user.service.query.UserSolverQueryService;
import issueissyu.backend.global.api.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -33,6 +35,7 @@
public class UserController {

private final UserPinQueryService userPinQueryService;
private final UserSolverQueryService userSolverQueryService;
private final UserAlarmStateQueryService userAlarmStateQueryService;
private final UserCommandService userCommandService;

Expand Down Expand Up @@ -87,4 +90,17 @@ public ApiResponse<UserMyPinsResDTO> getMyPins(
@RequestParam(required = false) String cursor) {
return ApiResponse.onSuccess(UserSuccessCode.USER_PIN_200, userPinQueryService.getMyPins(uid, size, cursor));
}

@Operation(
summary = "내 시민해결사 참여 핀 조회",
description =
"로그인한 사용자가 시민해결사(EN_ROUTE)로 참여 중인 핀을 problem_solver.created_at 내림차순으로 커서 페이징 조회합니다.")
@GetMapping("/me/solvers")
public ApiResponse<UserMySolversResDTO> getMySolvers(
@AuthenticationPrincipal String uid,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) String cursor) {
return ApiResponse.onSuccess(
UserSuccessCode.USER_SOLVER_200, userSolverQueryService.getMySolvers(uid, size, cursor));
}
}
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
Expand Up @@ -11,6 +11,8 @@ public enum UserErrorCode implements BaseErrorCode {

USER_PIN_400_1(HttpStatus.BAD_REQUEST, "USER_PIN_400_1", "조회 불가능한 사이즈 입니다."),
USER_PIN_400_2(HttpStatus.BAD_REQUEST, "USER_PIN_400_2", "조회 불가능한 cursor 입니다."),
USER_SOLVER_400_1(HttpStatus.BAD_REQUEST, "USER_SOLVER_400_1", "조회 불가능한 사이즈 입니다."),
USER_SOLVER_400_2(HttpStatus.BAD_REQUEST, "USER_SOLVER_400_2", "조회 불가능한 cursor 입니다."),
USER_NICKNAME_400(HttpStatus.BAD_REQUEST, "USER_NICKNAME_400", "닉네임은 15자 이내의 영문, 숫자, 한글만 사용 가능합니다."),
USER_NICKNAME_409(HttpStatus.CONFLICT, "USER_NICKNAME_409", "이미 사용 중인 닉네임입니다."),
USER_ALARM_400(HttpStatus.BAD_REQUEST, "USER_ALARM_400", "존재하지 않는 알람 설정입니다.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
public enum UserSuccessCode implements BaseSuccessCode {

USER_PIN_200(HttpStatus.OK, "USER_PIN_200", "내 핀 조회에 성공했습니다."),
USER_SOLVER_200(HttpStatus.OK, "USER_SOLVER_200", "내 시민해결사 조회에 성공했습니다."),
USER_NICKNAME_200(HttpStatus.OK, "USER_NICKNAME_200", "닉네임 변경에 성공했습니다."),
USER_ALARM_200_1(HttpStatus.OK, "USER_ALARM_200_1", "핀 좋아요 알람 비/활성화에 성공했습니다."),
USER_ALARM_200_2(HttpStatus.OK, "USER_ALARM_200_2", "이벤트 알람 비/활성화에 성공했습니다."),
Expand Down
Loading
Loading