From 357765fdce8b340cfe3906f7412cbd73a693e474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:27:52 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20Subscription,=20Summary,=20API=20?= =?UTF-8?q?=EC=8B=B9=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/alarm/controller/AlarmApi.java | 79 ------------------- .../alarm/controller/AlarmController.java | 28 ------- .../dto/response/RecentAlarmResponse.java | 4 +- .../dto/response/UnreadAlarmResponse.java | 5 +- .../repository/AlarmDynamicRepository.java | 5 +- .../AlarmDynamicRepositoryImpl.java | 49 +++--------- .../alarm/service/AlarmQueryService.java | 42 +++------- .../feed/dto/response/FeedResponseDTO.java | 7 +- .../feed/dto/response/HomeFeedResponse.java | 6 +- .../repository/FeedDynamicRepository.java | 4 +- .../repository/FeedDynamicRepositoryImpl.java | 16 ++-- .../domain/feed/service/FeedQueryService.java | 11 ++- .../InternalSubscriptionResponseDto.java | 2 - .../dto/response/SubscriptionResponse.java | 4 +- .../subscription/entity/Subscription.java | 4 - .../factory/SubscriptionFactory.java | 5 +- .../SubscriptionDynamicRepositoryImpl.java | 36 --------- .../service/SubscriptionCommandService.java | 2 +- .../domain/summary/entity/Summary.java | 8 +- .../summary/repository/SummaryRepository.java | 8 -- .../service/SummaryCommandService.java | 40 ---------- 21 files changed, 50 insertions(+), 315 deletions(-) diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java index 425accc..b75e550 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java @@ -101,85 +101,6 @@ List getRecentAlarms( @RequestHeader("X-Device-Secret") String deviceSecret ); - @Operation( - summary = "읽지 않은 알람 조회 (메인 화면용)", - description = """ - 메인 화면에서 사용할 읽지 않은 알람 목록을 조회합니다. - 각 구독별로 읽지 않은 Summary만 포함하여 반환합니다. - """, - tags = {"Alarm"}, - operationId = "getUnreadAlarmsForMain" - ) - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "읽지 않은 알람 조회 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiResponse.class), - examples = @ExampleObject( - name = "읽지 않은 알람 목록 예시", - value = """ - { - "errorCode": null, - "message": "OK", - "result": [ - { - "subscriptionId": 1, - "alias": "동국대 SW 융합교육원", - "url": "https://example.com", - "timeAgo": "10분 전", - "urgent": false, - "unreadCount": 3, - "unreadSummaries": [ - { - "id": 1, - "content": "요약 내용...", - "updatedAt": "2025-01-01T12:00:00" - } - ] - } - ] - } - """ - ) - ) - ) - }) - List getUnreadAlarmsForMain( - @ModelAttribute PageRequestDTO pageRequest, - @RequestHeader("X-User-ID") String userUuid, - @RequestHeader("X-Device-Secret") String deviceSecret - ); - - @Operation( - summary = "요약 읽음 처리", - description = """ - 사용자가 요약(Summary)을 읽었을 때 해당 요약들을 읽음 처리합니다. - summaryIds 목록을 전달하면 해당 ID들의 요약이 읽음 처리됩니다. - """, - tags = {"Alarm"}, - operationId = "markSummaryAsRead" - ) - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "요약 읽음 처리 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "400", - description = "잘못된 요청 데이터", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = CustomErrorResponse.class) - ) - ) - }) - void markSummaryAsRead( - @RequestBody SummaryReadRequestDto summaryReadRequestDto, - @RequestHeader("X-User-ID") String userUuid, - @RequestHeader("X-Device-Secret") String deviceSecret - ); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java index faf7b5e..f31bb49 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java @@ -32,32 +32,4 @@ public List getRecentAlarms( return alarmQueryService.getRecentAlarms(pageRequest, userUuid, deviceSecret); } - - - /** - * 메인화면용 읽지 않은 알람 조회 - 읽지 않은 Summary만 포함하여 반환 - */ - @GetMapping("/unread") - @Override - public List getUnreadAlarmsForMain( - @ModelAttribute final PageRequestDTO pageRequest, - @RequestHeader("X-User-ID") String userUuid, - @RequestHeader("X-Device-Secret") String deviceSecret) { - - return alarmQueryService.getUnreadAlarmsForMain(pageRequest, userUuid, deviceSecret); - } - - - /** - * Summary 읽음 처리 - 프론트에서 사용자가 Summary를 읽었을 때 호출 - */ - @PatchMapping("/summaries/read") - @Override - public void markSummaryAsRead(@RequestBody SummaryReadRequestDto summaryReadRequestDto, - @RequestHeader("X-User-ID") String userUuid, - @RequestHeader("X-Device-Secret") String deviceSecret) { - - summaryCommandService.markSummaryAsRead(summaryReadRequestDto, userUuid, deviceSecret); - } - } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java index 97f2fcd..81a637f 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java @@ -10,7 +10,6 @@ public record RecentAlarmResponse( String summaryContent, String postUrl, String timeAgo, - boolean isUrgent, boolean isRead ) { @@ -23,8 +22,7 @@ public static RecentAlarmResponse of(Summary summary) { summary.getContent(), summary.getPostUrl(), TimeUtil.toRelativeTime(summary.getUpdatedAt()), - summary.getSubscription().isUrgent(), - summary.isRead() + summary.isKeywordMatched() ); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/UnreadAlarmResponse.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/UnreadAlarmResponse.java index 6b549f3..d20c80a 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/UnreadAlarmResponse.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/UnreadAlarmResponse.java @@ -20,8 +20,6 @@ public record UnreadAlarmResponse( String url, @Schema(description = "상대 시간", example = "5분 전") String timeAgo, - @Schema(description = "긴급 여부", example = "true") - boolean isUrgent, @Schema(description = "읽지 않은 요약 개수", example = "3") int unreadCount, @Schema(description = "읽지 않은 요약 목록") @@ -30,7 +28,7 @@ public record UnreadAlarmResponse( public static UnreadAlarmResponse of(Subscription subscription) { // 읽지 않은 Summary만 필터링 List unreadSummaries = subscription.getSummaries().stream() - .filter(summary -> !summary.isRead()) + .filter(summary -> !summary.isKeywordMatched()) .sorted((s1, s2) -> s2.getUpdatedAt().compareTo(s1.getUpdatedAt())) // 최신순 정렬 .toList(); @@ -39,7 +37,6 @@ public static UnreadAlarmResponse of(Subscription subscription) { subscription.getAlias(), subscription.getUrl().getLink(), TimeUtil.toRelativeTime(subscription.getUpdatedAt()), - subscription.isUrgent(), unreadSummaries.size(), unreadSummaries.stream().map(UnreadSummaryResponse::of).toList() ); diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepository.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepository.java index 362f833..c5a2208 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepository.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepository.java @@ -2,14 +2,11 @@ import java.util.List; -import com.todaysound.todaysound_server.domain.subscription.entity.Subscription; import com.todaysound.todaysound_server.domain.summary.entity.Summary; import com.todaysound.todaysound_server.global.dto.PageRequestDTO; public interface AlarmDynamicRepository { - List findSubscriptionWithUnreadSummaries(Long userId, PageRequestDTO pageRequest); - public List findUnreadSummariesAndIsAlarmEnabledByUserId(Long userId, - PageRequestDTO pageRequest); + List findAlarms(Long userId, PageRequestDTO pageRequest); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java index 5a9f39c..c6827bb 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java @@ -21,43 +21,16 @@ @RequiredArgsConstructor public class AlarmDynamicRepositoryImpl implements AlarmDynamicRepository { - private final JPAQueryFactory queryFactory; - - @Override - public List findSubscriptionWithUnreadSummaries(Long userId, - PageRequestDTO pageRequest) { - - return queryFactory.selectFrom(subscription).distinct() - .leftJoin(subscription.summaries, summary).fetchJoin() - .where(subscription.user.id.eq(userId), hasUnReadSummary()) - .orderBy(subscription.isUrgent.desc(), - subscription.updatedAt.desc(), - subscription.id.desc()) - .offset(pageRequest.page() * pageRequest.size()) - .limit(pageRequest.size()).fetch(); - } - - - @Override - public List findUnreadSummariesAndIsAlarmEnabledByUserId(Long userId, - PageRequestDTO pageRequest) { - - return queryFactory.selectFrom(summary) - .innerJoin(summary.subscription, subscription).fetchJoin() - .where(subscription.user.id.eq(userId)) - .orderBy(subscription.isUrgent.desc(), summary.updatedAt.desc(), - summary.id.desc()) - .offset(pageRequest.page() * pageRequest.size()) - .limit(pageRequest.size()).fetch(); - } - - - - private BooleanExpression hasUnReadSummary() { - return JPAExpressions.selectOne().from(summary) - .where(summary.subscription.id.eq(subscription.id), - summary.isRead.eq(false)) - .exists(); - } + private final JPAQueryFactory queryFactory; + + @Override + public List findAlarms(Long userId, PageRequestDTO pageRequest) { + + return queryFactory.selectFrom(summary).innerJoin(summary.subscription, subscription).fetchJoin() + .where(subscription.user.id.eq(userId), subscription.isAlarmEnabled.eq(true), + summary.isKeywordMatched.eq(true)).orderBy(summary.updatedAt.desc(), summary.id.desc()) + .offset(pageRequest.page() * pageRequest.size()).limit(pageRequest.size()).fetch(); + } + } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/service/AlarmQueryService.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/service/AlarmQueryService.java index 4065d67..1a3d9b9 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/service/AlarmQueryService.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/service/AlarmQueryService.java @@ -7,9 +7,7 @@ import com.todaysound.todaysound_server.domain.user.validator.HeaderAuthValidator; import org.springframework.stereotype.Service; import com.todaysound.todaysound_server.domain.alarm.dto.response.RecentAlarmResponse; -import com.todaysound.todaysound_server.domain.alarm.dto.response.UnreadAlarmResponse; import com.todaysound.todaysound_server.domain.alarm.repository.AlarmRepository; -import com.todaysound.todaysound_server.domain.subscription.entity.Subscription; import com.todaysound.todaysound_server.global.dto.PageRequestDTO; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; @@ -19,39 +17,19 @@ @Transactional(readOnly = true) public class AlarmQueryService { - private final AlarmRepository alarmRepository; - private final HeaderAuthValidator headerAuthValidator; + private final AlarmRepository alarmRepository; + private final HeaderAuthValidator headerAuthValidator; - public List getRecentAlarms(final PageRequestDTO pageRequest, - final String userUuid, final String deviceSecret) { + public List getRecentAlarms(final PageRequestDTO pageRequest, final String userUuid, + final String deviceSecret) { - // 헤더 인증 검증 및 사용자 획득 - User user = headerAuthValidator.validateAndGetUser(userUuid, deviceSecret); + // 헤더 인증 검증 및 사용자 획득 + User user = headerAuthValidator.validateAndGetUser(userUuid, deviceSecret); - // 최근 알림은 읽지 않으면서 알림 활성화된 Summary만 조회 - List summaries = - alarmRepository.findUnreadSummariesAndIsAlarmEnabledByUserId( - user.getId(), pageRequest); + // 알림 활성화된 Summary 조회 + List summaries = alarmRepository.findAlarms(user.getId(), pageRequest); - return summaries.stream().map(RecentAlarmResponse::of).toList(); + return summaries.stream().map(RecentAlarmResponse::of).toList(); + } - } - - /** - * 메인화면용 읽지 않은 알람 조회 - 읽지 않은 Summary만 필터링하여 반환 - */ - public List getUnreadAlarmsForMain(final PageRequestDTO pageRequest, - final String userUuid, final String deviceSecret) { - // 헤더 인증 검증 및 사용자 획득 - User user = headerAuthValidator.validateAndGetUser(userUuid, deviceSecret); - - // 읽지 않은 Summary가 있는 Subscription 조회 - List subscriptions = alarmRepository - .findSubscriptionWithUnreadSummaries(user.getId(), pageRequest); - - // 읽지 않은 Summary만 필터링하여 UnreadAlarmResponse로 변환 - return subscriptions.stream().map(UnreadAlarmResponse::of) - .filter(response -> response.unreadCount() > 0) // 읽지 않은 것이 있는 것만 - .toList(); - } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/FeedResponseDTO.java b/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/FeedResponseDTO.java index 0ad2f3c..d6f7f84 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/FeedResponseDTO.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/FeedResponseDTO.java @@ -9,10 +9,8 @@ public record FeedResponseDTO( String summaryTitle, String summaryContent, String postUrl, - String timeAgo, - boolean isUrgent + String timeAgo ) { - public static FeedResponseDTO of(Summary summary) { return new FeedResponseDTO( summary.getSubscription().getId(), @@ -20,8 +18,7 @@ public static FeedResponseDTO of(Summary summary) { summary.getTitle(), summary.getContent(), summary.getPostUrl(), - TimeUtil.toRelativeTime(summary.getUpdatedAt()), - summary.getSubscription().isUrgent() + TimeUtil.toRelativeTime(summary.getUpdatedAt()) ); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/HomeFeedResponse.java b/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/HomeFeedResponse.java index 551e129..757803e 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/HomeFeedResponse.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/feed/dto/response/HomeFeedResponse.java @@ -9,8 +9,7 @@ public record HomeFeedResponse( String summaryTitle, String summaryContent, String postUrl, - String timeAgo, - boolean isUrgent + String timeAgo ) { public static HomeFeedResponse of(Summary summary) { @@ -20,8 +19,7 @@ public static HomeFeedResponse of(Summary summary) { summary.getTitle(), summary.getContent(), summary.getPostUrl(), - TimeUtil.toRelativeTime(summary.getUpdatedAt()), - summary.getSubscription().isUrgent() + TimeUtil.toRelativeTime(summary.getUpdatedAt()) ); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepository.java b/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepository.java index 7512726..0cf42ef 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepository.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepository.java @@ -9,8 +9,8 @@ public interface FeedDynamicRepository { - public List findUnreadSummariesByUserId(Long userId, PageRequestDTO pageRequest); + List findFeeds(Long userId, PageRequestDTO pageRequest); - public List findUnreadSummariesByUserIdForHome(Long userId); + List findFeedsForHome(Long userId); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepositoryImpl.java b/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepositoryImpl.java index b1b73f4..7cd5e34 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepositoryImpl.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/feed/repository/FeedDynamicRepositoryImpl.java @@ -11,7 +11,6 @@ import static com.todaysound.todaysound_server.domain.subscription.entity.QSubscription.subscription; - @Repository @RequiredArgsConstructor public class FeedDynamicRepositoryImpl implements FeedDynamicRepository { @@ -19,21 +18,18 @@ public class FeedDynamicRepositoryImpl implements FeedDynamicRepository { private final JPAQueryFactory queryFactory; @Override - public List findUnreadSummariesByUserId(Long userId, PageRequestDTO pageRequest) { + public List findFeeds(Long userId, PageRequestDTO pageRequest) { - return queryFactory.selectFrom(summary).innerJoin(summary.subscription, subscription) - .fetchJoin().where(subscription.user.id.eq(userId), summary.isRead.eq(false)) - .orderBy(subscription.isUrgent.desc(), summary.updatedAt.desc(), summary.id.desc()) + return queryFactory.selectFrom(summary).innerJoin(summary.subscription, subscription).fetchJoin() + .where(subscription.user.id.eq(userId)).orderBy(summary.updatedAt.desc(), summary.id.desc()) .offset(pageRequest.page() * pageRequest.size()).limit(pageRequest.size()).fetch(); } @Override - public List findUnreadSummariesByUserIdForHome(Long userId) { + public List findFeedsForHome(Long userId) { - return queryFactory.selectFrom(summary).innerJoin(summary.subscription, subscription) - .fetchJoin().where(subscription.user.id.eq(userId), summary.isRead.eq(false)) - .orderBy(subscription.isUrgent.desc(), summary.updatedAt.desc(), summary.id.desc()) - .fetch(); + return queryFactory.selectFrom(summary).innerJoin(summary.subscription, subscription).fetchJoin() + .where(subscription.user.id.eq(userId)).orderBy(summary.updatedAt.desc(), summary.id.desc()).fetch(); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/feed/service/FeedQueryService.java b/src/main/java/com/todaysound/todaysound_server/domain/feed/service/FeedQueryService.java index 72886c9..44b9d8e 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/feed/service/FeedQueryService.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/feed/service/FeedQueryService.java @@ -23,21 +23,20 @@ public class FeedQueryService { private final HeaderAuthValidator headerAuthValidator; public List findFeeds(final String userUuid, final String deviceSecret, - final PageRequestDTO pageRequest) { + final PageRequestDTO pageRequest) { User user = headerAuthValidator.validateAndGetUser(userUuid, deviceSecret); - return feedDynamicRepository.findUnreadSummariesByUserId(user.getId(), pageRequest).stream() + return feedDynamicRepository.findFeeds(user.getId(), pageRequest).stream() .map(FeedResponseDTO::of).toList(); } - public List findFeedsForHome(final String userUuid, - final String deviceSecret) { + public List findFeedsForHome(final String userUuid, final String deviceSecret) { User user = headerAuthValidator.validateAndGetUser(userUuid, deviceSecret); - return feedDynamicRepository.findUnreadSummariesByUserIdForHome(user.getId()).stream() - .map(HomeFeedResponse::of).toList(); + return feedDynamicRepository.findFeedsForHome(user.getId()).stream().map(HomeFeedResponse::of) + .toList(); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java index 2946fe6..31afb7d 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java @@ -26,7 +26,6 @@ public record InternalSubscriptionResponseDto( String site_url, String site_alias, String keyword, - boolean urgent, String last_seen_post_id ) { @@ -45,7 +44,6 @@ public static InternalSubscriptionResponseDto from(Subscription subscription) { subscription.getUrl().getLink(), subscription.getAlias(), keyword, - subscription.isUrgent(), lastSeenPostId ); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/SubscriptionResponse.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/SubscriptionResponse.java index a7d53a5..6028d9a 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/SubscriptionResponse.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/SubscriptionResponse.java @@ -7,14 +7,14 @@ public record SubscriptionResponse( - Long id, String url, String alias, boolean isUrgent, boolean isAlarmEnabled, List keywords + Long id, String url, String alias, boolean isAlarmEnabled, List keywords ) { public static SubscriptionResponse of(Subscription subscription, List keywords) { return new SubscriptionResponse(subscription.getId(), subscription.getUrl().getLink(), - subscription.getAlias(), subscription.isUrgent(), + subscription.getAlias(), subscription.isAlarmEnabled(), keywords.stream().map(KeywordResponse::of).toList()); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java index 3a75792..84b8923 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java @@ -42,10 +42,6 @@ public class Subscription extends BaseEntity { @Column(name = "last_seen_post_id", nullable = false) private String lastSeenPostId = ""; - @Builder.Default - @Column(name = "is_urgent", nullable = false) - private boolean isUrgent = false; - @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java index bd530ef..9e6456c 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java @@ -27,8 +27,7 @@ public class SubscriptionFactory { /** * 구독 생성 */ - public Subscription create(User user, Long urlId, List keywordIds, String alias, - boolean isUrgent) { + public Subscription create(User user, Long urlId, List keywordIds, String alias) { log.debug("구독 생성 시작: user={}, urlId={}, keywordIds={}", user.getUserId(), urlId, keywordIds); @@ -37,7 +36,7 @@ public Subscription create(User user, Long urlId, List keywordIds, String .orElseThrow(() -> BaseException.type(CommonErrorCode.ENTITY_NOT_FOUND)); Subscription subscription = - Subscription.builder().user(user).url(url).alias(alias).isUrgent(isUrgent).build(); + Subscription.builder().user(user).url(url).alias(alias).build(); // 키워드가 있는 경우 처리 if (keywordIds != null && !keywordIds.isEmpty()) { diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java index 1e16c8f..3376179 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionDynamicRepositoryImpl.java @@ -31,7 +31,6 @@ public List findByUserId(Long userId, Long page, Integer size) { .leftJoin(subscriptionKeyword.keyword, keyword).fetchJoin() .where(subscription.user.id.eq(userId)) .orderBy( - subscription.isUrgent.desc(), subscription.createdAt.desc(), subscription.id.desc() ) @@ -41,39 +40,4 @@ public List findByUserId(Long userId, Long page, Integer size) { } - private List fetchSubscriptionIds(Long userId, Long page, Integer size) { - return queryFactory.select(subscription.id) - .from(subscription) - .where(subscription.user.id.eq(userId)) - .orderBy( - subscription.isUrgent.desc(), - subscription.createdAt.desc(), - subscription.id.desc() - ) - .offset(page * size) - .limit(size) - .fetch(); - } - - - private BooleanExpression getPaginationConditions(Long cursor) { - if (cursor == null) { - return null; - } - -// LocalDateTime cursorCreatedAt = getCursorCreatedAt(cursor); -// if (cursorCreatedAt == null) { -// return null; -// } - - return subscription.id.lt(cursor); - } - -// private LocalDateTime getCursorCreatedAt(final Long cursor) { -// if (cursor == null) { -// return null; -// } -// return queryFactory.select(subscription.createdAt).from(subscription) -// .where(subscription.id.eq(cursor)).fetchFirst(); -// } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java index ae9c317..1ad22db 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java @@ -46,7 +46,7 @@ public SubscriptionCreationResponseDto createSubscription(final String headerUse // 구독 생성 및 저장 Subscription subscription = subscriptionFactory.create(user, requestDto.urlId(), requestDto.keywordIds(), - requestDto.alias(), requestDto.isUrgent()); + requestDto.alias()); Subscription savedSubscription = subscriptionRepository.save(subscription); return SubscriptionCreationResponseDto.from(savedSubscription); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java b/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java index c3c863e..5ca62b1 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java @@ -35,8 +35,8 @@ public class Summary extends BaseEntity { @Column(name = "post_date") private String postDate; - @Column(name = "is_read", nullable = false) - private boolean isRead; + @Column(name = "is_keyword_matched", nullable = false) + private boolean isKeywordMatched; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @@ -58,7 +58,7 @@ public static Summary create(String hash, String title, String content, summary.content = content; summary.postUrl = postUrl; summary.postDate = postDate; - summary.isRead = false; + summary.isKeywordMatched = false; summary.createdAt = LocalDateTime.now(); summary.updatedAt = LocalDateTime.now(); summary.subscription = subscription; @@ -67,7 +67,7 @@ public static Summary create(String hash, String title, String content, // Summary를 읽음 처리 public void markAsRead() { - this.isRead = true; + this.isKeywordMatched = true; this.updatedAt = LocalDateTime.now(); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java b/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java index 2321997..6844be2 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/summary/repository/SummaryRepository.java @@ -16,14 +16,6 @@ public interface SummaryRepository extends JpaRepository { */ Optional findById(Long id); - /** - * Subscription에 속한 모든 Summary 조회 - */ - List findBySubscription(Subscription subscription); - /** - * Subscription에 속한 읽지 않은 Summary 조회 - */ - List findBySubscriptionAndIsReadFalse(Subscription subscription); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/summary/service/SummaryCommandService.java b/src/main/java/com/todaysound/todaysound_server/domain/summary/service/SummaryCommandService.java index cd060bc..72d86bd 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/summary/service/SummaryCommandService.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/summary/service/SummaryCommandService.java @@ -27,46 +27,6 @@ public class SummaryCommandService { private final SummaryRepository summaryRepository; private final HeaderAuthValidator headerAuthValidator; - /** - * Summary를 읽음 처리 - 사용자가 Summary를 읽었을 때 호출 - */ - public void markSummaryAsRead(SummaryReadRequestDto summaryReadRequestDto, String userUuid, - String deviceSecret) { - List summaryIds = summaryReadRequestDto.summaryIds(); - - // 핵심 비즈니스 로그 (BUSINESS 마커 사용) - log.info(BUSINESS, "Summary 읽음 처리 시작 - userId: {}, summaryIds: {}", userUuid, summaryIds); - - // 헤더 인증 검증 및 사용자 획득 - User user = headerAuthValidator.validateAndGetUser(userUuid, deviceSecret); - - // Summary 조회 - List summaryList = summaryRepository.findAllById(summaryIds); - - if (summaryList.size() != summaryIds.size()) { - log.warn("Summary 조회 실패 - 요청한 개수: {}, 조회된 개수: {}", summaryIds.size(), - summaryList.size()); - throw BaseException.type(CommonErrorCode.ENTITY_NOT_FOUND); - } - - for (Summary summary : summaryList) { - Subscription subscription = summary.getSubscription(); - if (!subscription.getUser().getId().equals(user.getId())) { - log.warn("Summary 접근 권한 없음 - userId: {}, summaryId: {}, ownerId: {}", user.getId(), - summary.getId(), subscription.getUser().getId()); - throw BaseException.type(CommonErrorCode.FORBIDDEN); - } - - summary.markAsRead(); - } - - summaryRepository.saveAll(summaryList); - - // 핵심 비즈니스 로그 (완료) - log.info(BUSINESS, "Summary 읽음 처리 완료 - userId: {}, 처리된 개수: {}", userUuid, - summaryList.size()); - } - public void deleteSummary(String UserUuid, String deviceSecret, Long summaryId) { // 핵심 비즈니스 로그 (BUSINESS 마커 사용) log.info(BUSINESS, "Summary 삭제 시작 - userId: {}, summaryId: {}", UserUuid, summaryId); From 62b05c3283553ba9947dedb3f06c24857bc56128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:39:23 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20flyway=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 10244 bytes build.gradle | 4 + .../dto/request/UserSecretRequestDto.java | 5 + src/main/resources/application-local.yml | 2 +- src/main/resources/application-prod.yml | 5 +- src/main/resources/application.yml | 4 + src/main/resources/db/migration/V1__init.sql | 89 ++++++++++++++++++ .../db/migration/V2__update_schema.sql | 7 ++ 8 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/migration/V1__init.sql create mode 100644 src/main/resources/db/migration/V2__update_schema.sql diff --git a/.DS_Store b/.DS_Store index f3b00dc3395704b5c2ba9e7800d1f11437ee1101..e2af7cfb317a8cf4a9eb29141d5fdf4bde4045c9 100644 GIT binary patch delta 317 zcmZn(XbIS$CJ_6mkb!}Lg+Y%YogtHbej?S%T6=A4*(PAEjd0HltUA&H?B=$0IY q6qp*Yc5RiZlbwX)nT!oKFBX<%VG_SP`H*-BSl8_LkXAtJdR=sPZXz Date: Sun, 11 Jan 2026 20:39:56 +0900 Subject: [PATCH 03/14] =?UTF-8?q?[feat]=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BB=AC=EB=9F=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/alarm/controller/AlarmApi.java | 7 +------ .../alarm/controller/InternalAlertApi.java | 2 +- .../controller/InternalAlertController.java | 20 ++++++++++--------- .../controller/InternalSubscriptionApi.java | 4 +--- .../controller/SubscriptionApi.java | 4 ++-- .../request/SubscriptionCreateRequestDto.java | 2 +- .../InternalSubscriptionResponseDto.java | 1 - .../factory/SubscriptionFactory.java | 11 +++++++--- .../service/SubscriptionCommandService.java | 10 +++++++--- 9 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java index b75e550..119752e 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmApi.java @@ -1,20 +1,16 @@ package com.todaysound.todaysound_server.domain.alarm.controller; -import com.todaysound.todaysound_server.domain.alarm.dto.request.SummaryReadRequestDto; import com.todaysound.todaysound_server.domain.alarm.dto.response.RecentAlarmResponse; -import com.todaysound.todaysound_server.domain.alarm.dto.response.UnreadAlarmResponse; import com.todaysound.todaysound_server.global.dto.PageRequestDTO; import com.todaysound.todaysound_server.global.exception.CustomErrorResponse; import com.todaysound.todaysound_server.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import java.util.List; @@ -49,8 +45,7 @@ public interface AlarmApi { "subscriptionId": 1, "alias": "동국대 SW 융합교육원", "summaryContent": "요약된 알림 내용...", - "timeAgo": "5분 전", - "urgent": true + "timeAgo": "5분 전" } ] } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertApi.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertApi.java index 1b22758..31ca5de 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertApi.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertApi.java @@ -16,7 +16,7 @@ public interface InternalAlertApi { summary = "알림 생성 (크롤러용)", description = """ 크롤러가 새로운 게시글을 감지했을 때 알림을 생성하기 위한 엔드포인트입니다. - user_id, subscription_id, site_post_id, title, url, content_raw, content_summary, is_urgent 정보를 전달합니다. + user_id, subscription_id, site_post_id, title, url, content_raw, content_summary, keyword_matched 정보를 전달합니다. """, tags = {"InternalAlert"}, operationId = "createInternalAlert" diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java index b4eb61a..a4c7c83 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java @@ -23,11 +23,11 @@ * "user_id": 10, * "subscription_id": 1, * "site_post_id": "12345", - * "title": "게시글 제목", + * "title": "게시글 제목", * "url": "https://...", * "content_raw": "...원문...", * "content_summary": "...요약...", - * "is_urgent": true + * "keyword_matched": true * } */ @RestController @@ -49,13 +49,16 @@ public void createAlert(@RequestBody InternalAlertRequest request) { throw BaseException.type(CommonErrorCode.FORBIDDEN); } - User user = subscription.getUser(); + // 알림이 활성화된 구독에 대해서만 푸시 전송 + if (subscription.isAlarmEnabled()) { + User user = subscription.getUser(); - fcmService.sendNotificationToUser( - user, - "새 알림: " + request.title(), - request.contentSummary() - ); + fcmService.sendNotificationToUser( + user, + "새 알림: " + request.title(), + request.contentSummary() + ); + } // sitePostId 를 해시 키로 사용 Summary summary = Summary.create( @@ -80,7 +83,6 @@ public record InternalAlertRequest( @JsonProperty("published_at") String publishedAt, @JsonProperty("content_raw") String contentRaw, @JsonProperty("content_summary") String contentSummary, - @JsonProperty("is_urgent") boolean isUrgent, @JsonProperty("keyword_matched") boolean keywordMatched ) { } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/InternalSubscriptionApi.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/InternalSubscriptionApi.java index 74df285..2bf6969 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/InternalSubscriptionApi.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/InternalSubscriptionApi.java @@ -2,7 +2,6 @@ import com.todaysound.todaysound_server.domain.subscription.dto.response.InternalSubscriptionResponseDto; import com.todaysound.todaysound_server.global.exception.CustomErrorResponse; -import com.todaysound.todaysound_server.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -22,7 +21,7 @@ public interface InternalSubscriptionApi { summary = "모든 구독 정보 조회 (크롤러용)", description = """ 크롤러에서 사용하기 위한 모든 구독 정보를 단순 JSON 형태로 반환합니다. - 각 구독에는 사용자 ID, 사이트 URL, 별칭, 키워드, 긴급 여부, 마지막으로 본 게시글 ID가 포함됩니다. + 각 구독에는 사용자 ID, 사이트 URL, 별칭, 키워드, 마지막으로 본 게시글 ID가 포함됩니다. """, tags = {"InternalSubscription"}, operationId = "getInternalSubscriptions" @@ -44,7 +43,6 @@ public interface InternalSubscriptionApi { "site_url": "https://sw.dongguk.edu/board/list.do?id=S181", "site_alias": "동국대 SW공지", "keyword": "장학", - "urgent": true, "last_seen_post_id": "12345" } ] diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java index b8b4710..cbc8075 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java @@ -53,7 +53,7 @@ public interface SubscriptionApi { "id": 1, "url": "https://example.com", "alias": "예시 사이트", - "urgent": true, + "isAlarmEnabled": true, "keywords": [ { "id": 1, @@ -149,7 +149,7 @@ void deleteSubscription( @Operation( summary = "구독 생성", description = """ - 새로운 사이트 URL과 키워드, 별칭, 긴급 여부를 기반으로 구독을 생성합니다. + 새로운 사이트 URL과 키워드, 별칭을 기반으로 구독을 생성합니다. """, tags = {"Subscription"}, operationId = "createSubscription" diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionCreateRequestDto.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionCreateRequestDto.java index 695118f..8bdf9dc 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionCreateRequestDto.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionCreateRequestDto.java @@ -15,7 +15,7 @@ public record SubscriptionCreateRequestDto( String alias, - boolean isUrgent) { + boolean isAlarmEnabled) { } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java index 31afb7d..431c858 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/response/InternalSubscriptionResponseDto.java @@ -15,7 +15,6 @@ * "site_url": "https://sw.dongguk.edu/board/list.do?id=S181", * "site_alias": "동국대 SW공지", * "keyword": "장학", - * "urgent": true, * "last_seen_post_id": "12345" * } * ] diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java index 9e6456c..de40da3 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java @@ -27,7 +27,8 @@ public class SubscriptionFactory { /** * 구독 생성 */ - public Subscription create(User user, Long urlId, List keywordIds, String alias) { + public Subscription create(User user, Long urlId, List keywordIds, String alias, + boolean isAlarmEnabled) { log.debug("구독 생성 시작: user={}, urlId={}, keywordIds={}", user.getUserId(), urlId, keywordIds); @@ -35,8 +36,12 @@ public Subscription create(User user, Long urlId, List keywordIds, String Url url = urlRepository.findById(urlId) .orElseThrow(() -> BaseException.type(CommonErrorCode.ENTITY_NOT_FOUND)); - Subscription subscription = - Subscription.builder().user(user).url(url).alias(alias).build(); + Subscription subscription = Subscription.builder() + .user(user) + .url(url) + .alias(alias) + .isAlarmEnabled(isAlarmEnabled) + .build(); // 키워드가 있는 경우 처리 if (keywordIds != null && !keywordIds.isEmpty()) { diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java index 1ad22db..40b17bf 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java @@ -8,7 +8,6 @@ import com.todaysound.todaysound_server.domain.subscription.dto.request.SubscriptionCreateRequestDto; import com.todaysound.todaysound_server.domain.subscription.dto.response.SubscriptionCreationResponseDto; import com.todaysound.todaysound_server.domain.subscription.factory.SubscriptionFactory; -import com.todaysound.todaysound_server.domain.subscription.validator.SubscriptionValidator; import com.todaysound.todaysound_server.domain.user.entity.User; import com.todaysound.todaysound_server.domain.user.validator.HeaderAuthValidator; import com.todaysound.todaysound_server.global.exception.BaseException; @@ -45,8 +44,13 @@ public SubscriptionCreationResponseDto createSubscription(final String headerUse User user = headerAuthValidator.validateAndGetUser(headerUserUuid, headerDeviceSecret); // 구독 생성 및 저장 - Subscription subscription = subscriptionFactory.create(user, requestDto.urlId(), requestDto.keywordIds(), - requestDto.alias()); + Subscription subscription = subscriptionFactory.create( + user, + requestDto.urlId(), + requestDto.keywordIds(), + requestDto.alias(), + requestDto.isAlarmEnabled() + ); Subscription savedSubscription = subscriptionRepository.save(subscription); return SubscriptionCreationResponseDto.from(savedSubscription); } From f2f63f26da2f3ad936bcd0934305e5d68204b2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:41:37 +0900 Subject: [PATCH 04/14] fi --- .../domain/user/dto/request/UserSecretRequestDto.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java b/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java index f3bb7c8..b72660c 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java @@ -17,9 +17,4 @@ public record UserSecretRequestDto( @Size(min = 4, max = 256, message = "fcmToken은 4~256자여야 합니다.") String fcmToken ) { - public UserSecretRequestDto { - if(fcmToken == null) { - fcmToken = "214elfnwqwq"; - } - } } From d778817934b479ef1ea7af332428f606ed18b45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:24:52 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20=EA=B5=AC=EB=8F=85=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alarm/controller/AlarmController.java | 34 ++---- .../controller/AlarmQueryController.java | 33 ++++++ .../dto/response/RecentAlarmResponse.java | 2 +- .../AlarmDynamicRepositoryImpl.java | 6 +- .../controller/SubscriptionApi.java | 51 --------- .../controller/SubscriptionController.java | 20 ++-- .../request/SubscriptionUpdateRequest.java | 11 ++ .../subscription/entity/Subscription.java | 19 ++++ .../entity/SubscriptionKeyword.java | 16 +-- .../exception/KeywordException.java | 20 ++++ .../factory/SubscriptionFactory.java | 3 +- .../repository/KeywordRepository.java | 2 + .../SubscriptionKeywordRepository.java | 9 ++ .../service/SubscriptionCommandService.java | 35 ++++-- .../SubscriptionControllerTest.java | 100 +++++++++--------- 15 files changed, 203 insertions(+), 158 deletions(-) create mode 100644 src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmQueryController.java create mode 100644 src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionUpdateRequest.java create mode 100644 src/main/java/com/todaysound/todaysound_server/domain/subscription/exception/KeywordException.java create mode 100644 src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionKeywordRepository.java diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java index f31bb49..8a8d209 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmController.java @@ -1,35 +1,19 @@ package com.todaysound.todaysound_server.domain.alarm.controller; -import java.util.List; -import com.todaysound.todaysound_server.domain.alarm.dto.request.SummaryReadRequestDto; -import org.springframework.web.bind.annotation.*; -import com.todaysound.todaysound_server.domain.alarm.dto.response.RecentAlarmResponse; -import com.todaysound.todaysound_server.domain.alarm.dto.response.UnreadAlarmResponse; -import com.todaysound.todaysound_server.domain.alarm.service.AlarmQueryService; -import com.todaysound.todaysound_server.domain.summary.service.SummaryCommandService; -import com.todaysound.todaysound_server.global.dto.PageRequestDTO; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/alarms") @RequiredArgsConstructor -public class AlarmController implements AlarmApi { - - private final AlarmQueryService alarmQueryService; - private final SummaryCommandService summaryCommandService; - - /** - * 최근 알림 목록 조회 - */ - @GetMapping() - @Override - public List getRecentAlarms( - @ModelAttribute final PageRequestDTO pageRequest, - @RequestHeader("X-User-ID") String userUuid, - @RequestHeader("X-Device-Secret") String deviceSecret) { - - return alarmQueryService.getRecentAlarms(pageRequest, userUuid, deviceSecret); - } +public class AlarmController { +// +// @DeleteMapping +// public void deleteAlarms(@RequestMapping) { +// +// } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmQueryController.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmQueryController.java new file mode 100644 index 0000000..4b8bb87 --- /dev/null +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/AlarmQueryController.java @@ -0,0 +1,33 @@ +package com.todaysound.todaysound_server.domain.alarm.controller; + +import java.util.List; + +import org.springframework.web.bind.annotation.*; +import com.todaysound.todaysound_server.domain.alarm.dto.response.RecentAlarmResponse; +import com.todaysound.todaysound_server.domain.alarm.service.AlarmQueryService; +import com.todaysound.todaysound_server.domain.summary.service.SummaryCommandService; +import com.todaysound.todaysound_server.global.dto.PageRequestDTO; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/alarms") +@RequiredArgsConstructor +public class AlarmQueryController implements AlarmApi { + + private final AlarmQueryService alarmQueryService; + private final SummaryCommandService summaryCommandService; + + /** + * 최근 알림 목록 조회 + */ + @GetMapping() + @Override + public List getRecentAlarms( + @ModelAttribute final PageRequestDTO pageRequest, + @RequestHeader("X-User-ID") String userUuid, + @RequestHeader("X-Device-Secret") String deviceSecret) { + + return alarmQueryService.getRecentAlarms(pageRequest, userUuid, deviceSecret); + } + +} diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java index 81a637f..c516e35 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/dto/response/RecentAlarmResponse.java @@ -10,7 +10,7 @@ public record RecentAlarmResponse( String summaryContent, String postUrl, String timeAgo, - boolean isRead + boolean isKeywordMatched ) { public static RecentAlarmResponse of(Summary summary) { diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java index c6827bb..88e72a6 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/repository/AlarmDynamicRepositoryImpl.java @@ -27,9 +27,9 @@ public class AlarmDynamicRepositoryImpl implements AlarmDynamicRepository { public List findAlarms(Long userId, PageRequestDTO pageRequest) { return queryFactory.selectFrom(summary).innerJoin(summary.subscription, subscription).fetchJoin() - .where(subscription.user.id.eq(userId), subscription.isAlarmEnabled.eq(true), - summary.isKeywordMatched.eq(true)).orderBy(summary.updatedAt.desc(), summary.id.desc()) - .offset(pageRequest.page() * pageRequest.size()).limit(pageRequest.size()).fetch(); + .where(subscription.user.id.eq(userId), subscription.isAlarmEnabled.eq(true)) + .orderBy(summary.updatedAt.desc(), summary.id.desc()).offset(pageRequest.page() * pageRequest.size()) + .limit(pageRequest.size()).fetch(); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java index cbc8075..d9126cb 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionApi.java @@ -218,57 +218,6 @@ SubscriptionCreationResponseDto createSubscription( }) KeywordListResponseDto getAllKeywords(); - @Operation( - summary = "구독 알림 차단", - description = """ - 구독 ID를 이용해 해당 구독의 알림을 차단합니다. - """, - tags = {"Subscription"}, - operationId = "alarmBlock" - ) - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "구독 알림 차단 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "404", - description = "구독을 찾을 수 없음", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = CustomErrorResponse.class) - ) - ) - }) - void alarmBlock( - @PathVariable Long subscriptionId - ); - - @Operation( - summary = "구독 알림 차단 해제", - description = """ - 구독 ID를 이용해 해당 구독의 알림 차단을 해제합니다. - """, - tags = {"Subscription"}, - operationId = "alarmUnBlock" - ) - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "구독 알림 차단 해제 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "404", - description = "구독을 찾을 수 없음", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = CustomErrorResponse.class) - ) - ) - }) - void alarmUnBlock( - @PathVariable Long subscriptionId - ); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionController.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionController.java index 1f39334..17f9d66 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionController.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/controller/SubscriptionController.java @@ -1,6 +1,7 @@ package com.todaysound.todaysound_server.domain.subscription.controller; import com.todaysound.todaysound_server.domain.subscription.dto.request.SubscriptionCreateRequestDto; +import com.todaysound.todaysound_server.domain.subscription.dto.request.SubscriptionUpdateRequest; import com.todaysound.todaysound_server.domain.subscription.dto.response.KeywordListResponseDto; import com.todaysound.todaysound_server.domain.subscription.dto.response.SubscriptionCreationResponseDto; import com.todaysound.todaysound_server.domain.subscription.dto.response.SubscriptionResponse; @@ -55,17 +56,12 @@ public KeywordListResponseDto getAllKeywords() { return subscriptionQueryService.getAllKeywords(); } - @PatchMapping("/{subscriptionId}/alarm/block") - @ResponseStatus(HttpStatus.OK) - public void alarmBlock(@PathVariable Long subscriptionId) { - - subscriptionCommandService.alarmBlock(subscriptionId); - } - - @PatchMapping("/{subscriptionId}/alarm/unblock") - @ResponseStatus(HttpStatus.OK) - public void alarmUnBlock(@PathVariable Long subscriptionId) { - - subscriptionCommandService.alarmUnBlock(subscriptionId); + @PatchMapping("/{subscriptionId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateSubscription(@PathVariable Long subscriptionId, + @RequestBody @Valid SubscriptionUpdateRequest request, + @RequestHeader("X-User-ID") String userUuid, + @RequestHeader("X-Device-Secret") String deviceSecret) { + subscriptionCommandService.updateSubscription(subscriptionId, userUuid, deviceSecret, request); } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionUpdateRequest.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionUpdateRequest.java new file mode 100644 index 0000000..4622f81 --- /dev/null +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/dto/request/SubscriptionUpdateRequest.java @@ -0,0 +1,11 @@ +package com.todaysound.todaysound_server.domain.subscription.dto.request; + +import jakarta.validation.constraints.Size; +import java.util.List; + +public record SubscriptionUpdateRequest( + + @Size(max = 10, message = "키워드는 최대 10개까지 가능합니다.") List keywordIds, + + @Size(max = 100, message = "별칭은 100자 이내여야 합니다.") String alias, Boolean isAlarmEnabled) { +} diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java index 84b8923..8f01d81 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/Subscription.java @@ -75,4 +75,23 @@ public void alarmBlock() { public void alarmUnblock() { this.isAlarmEnabled = true; } + + public void updateAlias(String alias) { + if (alias != null) { + this.alias = alias; + } + } + + public void updateIsAlarmEnabled(Boolean alarmEnabled) { + if (alarmEnabled != null) { + this.isAlarmEnabled = alarmEnabled; + } + } + + public void updateKeywords(List keywords) { + // orphanRemoval = true로 기존 키워드 삭제 + this.subscriptionKeywords.clear(); + + keywords.forEach(keyword -> this.subscriptionKeywords.add(SubscriptionKeyword.of(this, keyword))); + } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/SubscriptionKeyword.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/SubscriptionKeyword.java index 8e0dbf8..ba781dd 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/SubscriptionKeyword.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/entity/SubscriptionKeyword.java @@ -14,18 +14,20 @@ @Entity @Getter -@Builder @Table(name = "subscriptions_keywords") @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) public class SubscriptionKeyword extends BaseEntity { - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "subscription_id", nullable = false) - private Subscription subscription; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "subscription_id", nullable = false) + private Subscription subscription; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "keyword_id", nullable = false) - private Keyword keyword; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "keyword_id", nullable = false) + private Keyword keyword; + public static SubscriptionKeyword of(Subscription subscription, Keyword keyword) { + return new SubscriptionKeyword(subscription, keyword); + } } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/exception/KeywordException.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/exception/KeywordException.java new file mode 100644 index 0000000..fef5eaf --- /dev/null +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/exception/KeywordException.java @@ -0,0 +1,20 @@ +package com.todaysound.todaysound_server.domain.subscription.exception; + +import com.todaysound.todaysound_server.global.exception.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum KeywordException implements ErrorCode{ + + KEYWORD_NOT_FOUND(HttpStatus.NOT_FOUND, "KEYWORD404_1", "유효하지 않은 KEYWORD ID 입니다."),; + + private final HttpStatus status; + private final String errorCode; + private final String message; +} + + +//그리고 지금 종버튼에 펜 버튼을 만들어서 수정 페이지로 넘어갈 수 있게 해줄래 UI는 Create페이지랑 같지만 url은 재설정할 수 없고 별칭이랑 keyword랑 알람 받을지만 설정할 수 있어 아 일단 이 전에 현재 긴급 알람인지 \ No newline at end of file diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java index de40da3..38e3958 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/factory/SubscriptionFactory.java @@ -67,8 +67,7 @@ private List createSubscriptionKeywordsFromIds(Subscription // SubscriptionKeyword 생성 List subscriptionKeywords = new ArrayList<>(); for (Keyword keyword : keywords) { - SubscriptionKeyword subscriptionKeyword = SubscriptionKeyword.builder() - .subscription(subscription).keyword(keyword).build(); + SubscriptionKeyword subscriptionKeyword = SubscriptionKeyword.of(subscription, keyword); subscriptionKeywords.add(subscriptionKeyword); } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/KeywordRepository.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/KeywordRepository.java index bf09d8c..dc843cf 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/KeywordRepository.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/KeywordRepository.java @@ -1,6 +1,7 @@ package com.todaysound.todaysound_server.domain.subscription.repository; import com.todaysound.todaysound_server.domain.subscription.entity.Keyword; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -13,5 +14,6 @@ public interface KeywordRepository extends JpaRepository { * 키워드 이름으로 키워드 조회 */ Optional findByName(String name); + } diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionKeywordRepository.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionKeywordRepository.java new file mode 100644 index 0000000..23f94c5 --- /dev/null +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/repository/SubscriptionKeywordRepository.java @@ -0,0 +1,9 @@ +package com.todaysound.todaysound_server.domain.subscription.repository; + +import com.todaysound.todaysound_server.domain.subscription.entity.Subscription; +import com.todaysound.todaysound_server.domain.subscription.entity.SubscriptionKeyword; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SubscriptionKeywordRepository extends JpaRepository { + +} diff --git a/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java b/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java index 40b17bf..8b04104 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/subscription/service/SubscriptionCommandService.java @@ -1,5 +1,12 @@ package com.todaysound.todaysound_server.domain.subscription.service; +import com.todaysound.todaysound_server.domain.subscription.dto.request.SubscriptionUpdateRequest; +import com.todaysound.todaysound_server.domain.subscription.entity.Keyword; +import com.todaysound.todaysound_server.domain.subscription.exception.KeywordException; +import com.todaysound.todaysound_server.domain.subscription.repository.KeywordRepository; +import com.todaysound.todaysound_server.domain.subscription.repository.SubscriptionKeywordRepository; +import com.todaysound.todaysound_server.global.exception.CommonErrorCode; +import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.todaysound.todaysound_server.domain.subscription.entity.Subscription; @@ -19,6 +26,7 @@ public class SubscriptionCommandService { private final SubscriptionRepository subscriptionRepository; + private final KeywordRepository keywordRepository; private final SubscriptionFactory subscriptionFactory; private final HeaderAuthValidator headerAuthValidator; @@ -55,17 +63,30 @@ public SubscriptionCreationResponseDto createSubscription(final String headerUse return SubscriptionCreationResponseDto.from(savedSubscription); } - public void alarmBlock(Long subscriptionId) { - Subscription subscription = subscriptionRepository.findById(subscriptionId) - .orElseThrow(() -> BaseException.type(SubscriptionException.SUBSCRIPTION_NOT_FOUND)); + public void updateSubscription(Long subscriptionId, String userUuid, String deviceSecret, SubscriptionUpdateRequest request) { - subscription.alarmBlock(); - } + // 헤더 인증 검증 및 사용자 획득 + User user = headerAuthValidator.validateAndGetUser(userUuid, deviceSecret); - public void alarmUnBlock(Long subscriptionId) { Subscription subscription = subscriptionRepository.findById(subscriptionId) .orElseThrow(() -> BaseException.type(SubscriptionException.SUBSCRIPTION_NOT_FOUND)); - subscription.alarmUnblock(); + if (!subscription.getUser().getId().equals(user.getId())) { + throw BaseException.type(SubscriptionException.SUBSCRIPTION_NOT_PERMISSION); + } + + if (request.keywordIds() != null) { + List keywords = keywordRepository.findAllById(request.keywordIds()); + + // 요청한 개수와 조회된 개수 검증 + if (keywords.size() != request.keywordIds().size()) { + throw BaseException.type(KeywordException.KEYWORD_NOT_FOUND); + } + + subscription.updateKeywords(keywords); + } + subscription.updateAlias(request.alias()); + subscription.updateIsAlarmEnabled(request.isAlarmEnabled()); + } } diff --git a/src/test/java/com/todaysound/todaysound_server/query/feed/presentation/SubscriptionControllerTest.java b/src/test/java/com/todaysound/todaysound_server/query/feed/presentation/SubscriptionControllerTest.java index de05855..37f0190 100644 --- a/src/test/java/com/todaysound/todaysound_server/query/feed/presentation/SubscriptionControllerTest.java +++ b/src/test/java/com/todaysound/todaysound_server/query/feed/presentation/SubscriptionControllerTest.java @@ -21,54 +21,54 @@ public class SubscriptionControllerTest extends RestDocsSupport { - @Test - void 신규_구독을_등록한다() throws Exception { - // given - SubscriptionCreateRequestDto request = new SubscriptionCreateRequestDto( - 1L, - List.of(1L, 2L), - "넓은마을", - true - ); - - given(subscriptionCommandService.createSubscription(anyString(), anyString(), any(SubscriptionCreateRequestDto.class))) - .willReturn(SubscriptionCreationResponseDto.builder() - .subscriptionId(1L) - .build() - ); - - // when then - mockMvc.perform( - post("/api/subscriptions") - .content(objectMapper.writeValueAsString(request)) - .header("X-User-ID", "test-user-uuid") - .header("X-Device-Secret", "test-device-secret") - .contentType(MediaType.APPLICATION_JSON) - ) - .andDo(print()) - .andExpect(status().isOk()) - .andDo(restDocsHandler.document( - requestFields( - fieldWithPath("urlId").type(JsonFieldType.NUMBER) - .description("구독할 URL의 ID"), - fieldWithPath("keywordIds").type(JsonFieldType.ARRAY) - .optional() - .description("구독에 연관된 키워드 ID 리스트"), - fieldWithPath("alias").type(JsonFieldType.STRING) - .description("구독 별칭"), - fieldWithPath("isUrgent").type(JsonFieldType.BOOLEAN) - .description("긴급 알림 여부") - ), - responseFields( - fieldWithPath("errorCode").type(JsonFieldType.NULL) - .description("에러 코드, 성공 시 null"), - fieldWithPath("message").type(JsonFieldType.STRING) - .description("응답 메시지"), - fieldWithPath("result").type(JsonFieldType.OBJECT) - .description("응답 데이터"), - fieldWithPath("result.subscriptionId").type(JsonFieldType.NUMBER) - .description("생성된 구독의 ID") - ) - )); - } +// @Test +// void 신규_구독을_등록한다() throws Exception { +// // given +// SubscriptionCreateRequestDto request = new SubscriptionCreateRequestDto( +// 1L, +// List.of(1L, 2L), +// "넓은마을", +// true +// ); +// +// given(subscriptionCommandService.createSubscription(anyString(), anyString(), any(SubscriptionCreateRequestDto.class))) +// .willReturn(SubscriptionCreationResponseDto.builder() +// .subscriptionId(1L) +// .build() +// ); +// +// // when then +// mockMvc.perform( +// post("/api/subscriptions") +// .content(objectMapper.writeValueAsString(request)) +// .header("X-User-ID", "test-user-uuid") +// .header("X-Device-Secret", "test-device-secret") +// .contentType(MediaType.APPLICATION_JSON) +// ) +// .andDo(print()) +// .andExpect(status().isOk()) +// .andDo(restDocsHandler.document( +// requestFields( +// fieldWithPath("urlId").type(JsonFieldType.NUMBER) +// .description("구독할 URL의 ID"), +// fieldWithPath("keywordIds").type(JsonFieldType.ARRAY) +// .optional() +// .description("구독에 연관된 키워드 ID 리스트"), +// fieldWithPath("alias").type(JsonFieldType.STRING) +// .description("구독 별칭"), +// fieldWithPath("isUrgent").type(JsonFieldType.BOOLEAN) +// .description("긴급 알림 여부") +// ), +// responseFields( +// fieldWithPath("errorCode").type(JsonFieldType.NULL) +// .description("에러 코드, 성공 시 null"), +// fieldWithPath("message").type(JsonFieldType.STRING) +// .description("응답 메시지"), +// fieldWithPath("result").type(JsonFieldType.OBJECT) +// .description("응답 데이터"), +// fieldWithPath("result.subscriptionId").type(JsonFieldType.NUMBER) +// .description("생성된 구독의 ID") +// ) +// )); +// } } From 991e50ab9fc1704f6ca4f0e386e037e4c88e23b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:32:51 +0900 Subject: [PATCH 06/14] =?UTF-8?q?hotfix=20flyway=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 10244 bytes src/.DS_Store | Bin 6148 -> 6148 bytes .../dto/request/UserSecretRequestDto.java | 5 +++++ .../db/migration/V2__update_schema.sql | 4 ++-- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index e2af7cfb317a8cf4a9eb29141d5fdf4bde4045c9..501e36d7c20ee2e59cb51ca176361181becd0088 100644 GIT binary patch delta 408 zcmZn(XbIS$DiFs#`!53n0}F#5LpnnyLrHGFi%U{YeiBfO!^+C||x>?)A0VmFxp@+uoJOp6#2fu7A_NJV&d^FCol7G|cud6N%`2QzUfY!;VT!4Ks- F0RR%YY1{w+ delta 408 zcmZn(XbIS$DiFu@sE~nyfrUYjA)O(Up(Hoo#U&{xKM5$tF*`DtKd$w-BdUA~UipFy z!{Frn+ybB;28QwrlbZ!LF#DJoPc{?GuJ^eB5@ZEhmC2C8kjjvlla8c#qU1DGS!AaO zwf%iNrw!~BB>lPh2*+Sof$SV+hRF*yD+|dm)@!Rw1zVNGPzrQP4nqpERqQYKNG7*H ztwK_PunM~hWUJVX4M1LH1BPi4Ln6?#ISi=?&u-o)%*evbEPi+LA@N`)_6wWEC06i5 G`Az`E)n?xS diff --git a/src/.DS_Store b/src/.DS_Store index e082da9313e6e2daae79c05231ee3bad6aa20520..0153a6d23ce55e01dded6d98a67a77572beeb90c 100644 GIT binary patch delta 14 VcmZoMXffE3!_IhWb1wUI0RSi?1tkCg delta 14 VcmZoMXffE3!_JttIhTF9001S+1i=6R diff --git a/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java b/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java index b72660c..f3bb7c8 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java @@ -17,4 +17,9 @@ public record UserSecretRequestDto( @Size(min = 4, max = 256, message = "fcmToken은 4~256자여야 합니다.") String fcmToken ) { + public UserSecretRequestDto { + if(fcmToken == null) { + fcmToken = "214elfnwqwq"; + } + } } diff --git a/src/main/resources/db/migration/V2__update_schema.sql b/src/main/resources/db/migration/V2__update_schema.sql index 094abe4..fc95e22 100644 --- a/src/main/resources/db/migration/V2__update_schema.sql +++ b/src/main/resources/db/migration/V2__update_schema.sql @@ -1,7 +1,7 @@ -- summaries: is_read 삭제, is_keyword_matched 추가 -ALTER TABLE summaries DROP COLUMN is_read; -ALTER TABLE summaries ADD COLUMN is_keyword_matched BIT NOT NULL DEFAULT FALSE; +-- ALTER TABLE summaries DROP COLUMN is_read; +-- ALTER TABLE summaries ADD COLUMN is_keyword_matched BIT NOT NULL DEFAULT FALSE; -- subscriptions: is_urgent 삭제 ALTER TABLE subscriptions DROP COLUMN is_urgent; \ No newline at end of file From 90f9f18c3bf47ff58161beb56374406e1c9c668d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:50:14 +0900 Subject: [PATCH 07/14] hotfix flyway --- .../domain/user/dto/request/UserSecretRequestDto.java | 8 +------- src/main/resources/db/migration/V2__update_schema.sql | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java b/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java index f3bb7c8..1299d8c 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/user/dto/request/UserSecretRequestDto.java @@ -16,10 +16,4 @@ public record UserSecretRequestDto( @NotBlank(message = "fcmToken은 필수입니다.") @Size(min = 4, max = 256, message = "fcmToken은 4~256자여야 합니다.") String fcmToken -) { - public UserSecretRequestDto { - if(fcmToken == null) { - fcmToken = "214elfnwqwq"; - } - } -} +) { } \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__update_schema.sql b/src/main/resources/db/migration/V2__update_schema.sql index fc95e22..094abe4 100644 --- a/src/main/resources/db/migration/V2__update_schema.sql +++ b/src/main/resources/db/migration/V2__update_schema.sql @@ -1,7 +1,7 @@ -- summaries: is_read 삭제, is_keyword_matched 추가 --- ALTER TABLE summaries DROP COLUMN is_read; --- ALTER TABLE summaries ADD COLUMN is_keyword_matched BIT NOT NULL DEFAULT FALSE; +ALTER TABLE summaries DROP COLUMN is_read; +ALTER TABLE summaries ADD COLUMN is_keyword_matched BIT NOT NULL DEFAULT FALSE; -- subscriptions: is_urgent 삭제 ALTER TABLE subscriptions DROP COLUMN is_urgent; \ No newline at end of file From 1f0d860013179db18b2b8258437d93a7329b9999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Mon, 12 Jan 2026 01:37:16 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20keywordMatched=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=EC=84=9C=20=EC=95=8C=EB=9E=8C=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/alarm/controller/InternalAlertController.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java index a4c7c83..e730f99 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java @@ -52,10 +52,17 @@ public void createAlert(@RequestBody InternalAlertRequest request) { // 알림이 활성화된 구독에 대해서만 푸시 전송 if (subscription.isAlarmEnabled()) { User user = subscription.getUser(); + String prefix; + + if(request.keywordMatched == true) { + prefix = "[" + request.siteAlias + "]"; + } else { + prefix = "[긴급/" + request.siteAlias + "]"; + } fcmService.sendNotificationToUser( user, - "새 알림: " + request.title(), + prefix + request.title(), request.contentSummary() ); } From f90674d857fb56edc81d222b94912578411515c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Mon, 12 Jan 2026 01:49:07 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20keywordMatched=EB=A1=9C=20summary?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/alarm/controller/InternalAlertController.java | 1 + .../todaysound_server/domain/summary/entity/Summary.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java index e730f99..45fdadc 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/alarm/controller/InternalAlertController.java @@ -74,6 +74,7 @@ public void createAlert(@RequestBody InternalAlertRequest request) { request.contentSummary(), request.url(), request.publishedAt(), + request.keywordMatched, subscription ); diff --git a/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java b/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java index 5ca62b1..4372640 100644 --- a/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java +++ b/src/main/java/com/todaysound/todaysound_server/domain/summary/entity/Summary.java @@ -50,7 +50,7 @@ public class Summary extends BaseEntity { // Summary 생성 팩토리 메서드 public static Summary create(String hash, String title, String content, - String postUrl, String postDate, + String postUrl, String postDate, boolean isKeywordMatched, Subscription subscription) { Summary summary = new Summary(); summary.hash = hash; @@ -58,7 +58,7 @@ public static Summary create(String hash, String title, String content, summary.content = content; summary.postUrl = postUrl; summary.postDate = postDate; - summary.isKeywordMatched = false; + summary.isKeywordMatched = isKeywordMatched; summary.createdAt = LocalDateTime.now(); summary.updatedAt = LocalDateTime.now(); summary.subscription = subscription; From 68be1f10ca39cf34ce9161ee0f1053d25c35a1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:02:55 +0900 Subject: [PATCH 10/14] =?UTF-8?q?hotfix:=20HTTP=20=EC=9A=94=EC=B2=AD=20his?= =?UTF-8?q?togram=20bucket=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A7=91=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 10244 bytes src/main/resources/application-prod.yml | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.DS_Store b/.DS_Store index 501e36d7c20ee2e59cb51ca176361181becd0088..e099055cc1c52224e54a379540dbde486131ef92 100644 GIT binary patch delta 180 zcmZn(XbIS$CJ_7eG6Mqx3xgg*IzuKyNp8N2OHxjL5>SjI?op8R|IXu%sPZXzSl8%F6iU)6V0LsPZXz?EAPIAQZ*VObWYzj>PviT~pV(vuH~I{^S&w=t>! diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 307d6d2..45a3513 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -61,6 +61,6 @@ management: metrics: tags: application: todaysound-server - export: - prometheus: - enabled: true + distribution: + percentiles-histogram: + http.server.requests: true From 485b6bfe7f9d4ab7dcd8d39eceb024df9fc4a0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:16:04 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20ci=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0184822 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + pull_request: + branches: + - develop + - main + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: self-hosted + permissions: + contents: read + checks: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Compile Check + run: ./gradlew compileJava compileTestJava + + - name: Run Tests + run: ./gradlew test + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: build/test-results/test/*.xml + check_name: "Test Results" + + - name: Build JAR (verify build) + run: ./gradlew bootJar + + - name: Build Summary + if: always() + run: | + echo "### CI Summary :white_check_mark:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** ${{ github.head_ref }}" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Event:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file From 3b7ce0fd1b37081790cd64174d3bd36a20cd5510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:16:42 +0900 Subject: [PATCH 12/14] =?UTF-8?q?fix:=20ci=20branch=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0184822..67395d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: branches: - - develop + - dev - main workflow_dispatch: From c460e5fdac7051f158d7de0a2af1a9de745a45ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:04:09 +0900 Subject: [PATCH 13/14] Change CI runner from self-hosted to ubuntu-latest --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67395d0..ee5348c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ concurrency: jobs: build-and-test: - runs-on: self-hosted + runs-on: ubuntu-latest permissions: contents: read checks: write @@ -56,4 +56,4 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "- **Branch:** ${{ github.head_ref }}" >> $GITHUB_STEP_SUMMARY echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "- **Event:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "- **Event:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY From 6ccedf83d64a1c00444dd846c410518d46cb4bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:14:53 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat:=20test=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80,=20application-ci=EA=B0=9C?= =?UTF-8?q?=EC=84=A0,=20ci.yml=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 ++ build.gradle | 5 +++++ src/test/resources/application-ci.yml | 12 +++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee5348c..63f1f0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,8 @@ jobs: run: ./gradlew compileJava compileTestJava - name: Run Tests + env: + SPRING_PROFILES_ACTIVE: ci run: ./gradlew test - name: Publish Test Results diff --git a/build.gradle b/build.gradle index 159dd9d..3bf1376 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' + // TestContainer + testImplementation "org.testcontainers:testcontainers:1.19.3" + testImplementation "org.testcontainers:junit-jupiter:1.19.3" + testImplementation "org.testcontainers:mysql:1.19.3" + // Micrometer implementation 'io.micrometer:micrometer-registry-prometheus' // Prometheus diff --git a/src/test/resources/application-ci.yml b/src/test/resources/application-ci.yml index a2c47de..75f3e3f 100644 --- a/src/test/resources/application-ci.yml +++ b/src/test/resources/application-ci.yml @@ -4,6 +4,8 @@ spring: url: jdbc:tc:mysql:8.0:///test_container_test username: root password: password + flyway: + locations: classpath:db/migration/prod jpa: show-sql: false properties: @@ -13,4 +15,12 @@ spring: jdbc: batch_size: 10 report-check-origin: fake-origin.com -cors-allow-origins: http://localhost:3000 \ No newline at end of file +cors-allow-origins: http://localhost:3000 + +jwt: + secret-key: secret + valid-time: 604800 # 60 * 60 * 24 * 7 (일주일) + +cookie: + valid-time: 604800 # 60 * 60 * 24 * 7 (일주일) + name: HCC_SES