Skip to content

Commit 3d622af

Browse files
authored
Merge pull request #188 from prography/develop
iOS v1.1.2 버전 업데이트
2 parents 366e4f4 + 8f58435 commit 3d622af

File tree

150 files changed

+5319
-1929
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

150 files changed

+5319
-1929
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ src/main/resources/.env
4949
### Firebase ###
5050
firebase-service-account.json
5151
**/firebase-service-account*.json
52+
53+
### Static Test Files ###
54+
src/main/resources/static/

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ dependencies {
4747
testImplementation 'org.springframework.security:spring-security-test'
4848
testImplementation 'io.projectreactor:reactor-test'
4949
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
50+
51+
// JOL (Java Object Layout) - 메모리 크기 측정용
52+
testImplementation 'org.openjdk.jol:jol-core:0.17'
5053
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
5154
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
5255
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'

src/main/java/com/example/cherrydan/activity/controller/ActivityController.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.springframework.data.domain.Pageable;
1515
import org.springframework.data.domain.Sort;
1616
import org.springframework.data.web.PageableDefault;
17+
import org.springframework.http.ResponseEntity;
1718
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1819
import org.springframework.web.bind.annotation.*;
1920

@@ -36,50 +37,62 @@ public class ActivityController {
3637
description = """
3738
사용자의 북마크 기반 활동 알림 목록을 조회합니다.
3839
북마크한 캠페인의 신청 마감이 3일 남았을 때 생성되는 알림들입니다.
39-
40+
4041
**쿼리 파라미터 예시:**
4142
- ?page=0&size=20&sort=alertDate,desc
4243
- ?page=1&size=10&sort=alertDate,asc
43-
44+
4445
**정렬 가능한 필드:**
4546
- alertDate: 알림 생성 날짜 (기본값, DESC)
46-
47+
4748
**주의:** 이는 Request Body가 아닌 **Query Parameter**입니다.
4849
"""
4950
)
5051
@GetMapping("/bookmark-alerts")
51-
public ApiResponse<PageListResponseDTO<ActivityAlertResponseDTO>> getBookmarkActivityAlerts(
52+
public ResponseEntity<ApiResponse<PageListResponseDTO<ActivityAlertResponseDTO>>> getBookmarkActivityAlerts(
5253
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl currentUser,
5354
@PageableDefault(size = 20, sort = "alertDate", direction = Sort.Direction.DESC) Pageable pageable
5455
) {
5556
Page<ActivityAlertResponseDTO> alerts = activityAlertService.getUserActivityAlerts(currentUser.getId(), pageable);
5657
PageListResponseDTO<ActivityAlertResponseDTO> response = PageListResponseDTO.from(alerts);
57-
return ApiResponse.success("북마크 활동 알림 목록 조회 성공", response);
58+
return ResponseEntity.ok(ApiResponse.success("북마크 활동 알림 목록 조회 성공", response));
59+
}
60+
61+
@Operation(
62+
summary = "북마크 기반 활동 알림 개수 조회",
63+
description = "사용자의 북마크 기반 활동 알림 개수를 조회합니다."
64+
)
65+
@GetMapping("/bookmark-alerts/count")
66+
public ResponseEntity<ApiResponse<Long>> getBookmarkActivityAlertsCount(
67+
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl currentUser
68+
) {
69+
Long alertsCount = activityAlertService.getUserActivityAlertsCount(currentUser.getId());
70+
return ResponseEntity.ok(ApiResponse.success("북마크 활동 알림 개수 조회 성공", alertsCount));
5871
}
5972

6073
@Operation(
6174
summary = "북마크 기반 활동 알림 삭제",
6275
description = "선택한 북마크 기반 활동 알림들을 삭제합니다. 본인의 알림이 아닌 경우 403 에러를 반환합니다."
6376
)
6477
@DeleteMapping("/bookmark-alerts")
65-
public ApiResponse<Void> deleteBookmarkActivityAlerts(
78+
public ResponseEntity<ApiResponse<Void>> deleteBookmarkActivityAlerts(
6679
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl currentUser,
6780
@RequestBody AlertIdsRequestDTO request
6881
) {
6982
activityAlertService.deleteActivityAlert(currentUser.getId(), request.getAlertIds());
70-
return ApiResponse.success("북마크 활동 알림 삭제 성공", null);
83+
return ResponseEntity.ok(ApiResponse.success("북마크 활동 알림 삭제 성공", null));
7184
}
7285

7386
@Operation(
7487
summary = "북마크 기반 활동 알림 읽음 처리",
7588
description = "선택한 북마크 기반 활동 알림들을 읽음 상태로 변경합니다. 본인의 알림이 아닌 경우 403 에러를 반환합니다."
7689
)
7790
@PatchMapping("/bookmark-alerts/read")
78-
public ApiResponse<Void> markBookmarkActivityAlertsAsRead(
91+
public ResponseEntity<ApiResponse<Void>> markBookmarkActivityAlertsAsRead(
7992
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl currentUser,
8093
@RequestBody AlertIdsRequestDTO request
8194
) {
8295
activityAlertService.markActivityAlertsAsRead(currentUser.getId(), request.getAlertIds());
83-
return ApiResponse.success("북마크 활동 알림 읽음 처리 성공", null);
96+
return ResponseEntity.ok(ApiResponse.success("북마크 활동 알림 읽음 처리 성공", null));
8497
}
8598
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.example.cherrydan.activity.controller;
2+
3+
import com.example.cherrydan.activity.dto.UnreadAlertCountResponseDTO;
4+
import com.example.cherrydan.activity.service.AlertService;
5+
import com.example.cherrydan.common.response.ApiResponse;
6+
import com.example.cherrydan.oauth.security.jwt.UserDetailsImpl;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.Parameter;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
13+
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RestController;
16+
17+
@Tag(name = "Alert", description = "알림 관련 API")
18+
@RestController
19+
@RequestMapping("/api/alerts")
20+
@RequiredArgsConstructor
21+
public class AlertController {
22+
23+
private final AlertService alertService;
24+
25+
@Operation(
26+
summary = "미읽은 알림 개수 조회",
27+
description = "사용자의 미읽은 알림 개수를 조회합니다. 활동 알림과 키워드 알림의 개수를 각각 제공하며, 전체 개수도 함께 반환합니다."
28+
)
29+
@GetMapping("/unread-count")
30+
public ResponseEntity<ApiResponse<UnreadAlertCountResponseDTO>> getUnreadAlertCount(
31+
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl user
32+
) {
33+
UnreadAlertCountResponseDTO result = alertService.getUnreadAlertCount(user.getId());
34+
return ResponseEntity.ok(ApiResponse.success(result));
35+
}
36+
}

src/main/java/com/example/cherrydan/activity/domain/ActivityAlert.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
indexes = {
1414
@Index(name = "idx_user_visible_alert_date", columnList = "user_id, is_visible_to_user, alert_date"),
1515
@Index(name = "idx_alert_stage_visible", columnList = "alert_stage, is_visible_to_user")
16+
},
17+
uniqueConstraints = {
18+
@UniqueConstraint(name = "uk_activity_alert", columnNames = {"user_id", "campaign_id", "alert_type", "alert_date"})
1619
})
1720
@Getter
1821
@NoArgsConstructor
@@ -34,6 +37,10 @@ public class ActivityAlert extends BaseTimeEntity {
3437

3538
@Column(name = "alert_date", nullable = false)
3639
private LocalDate alertDate;
40+
41+
@Enumerated(EnumType.STRING)
42+
@Column(name = "alert_type", nullable = false, length = 50)
43+
private ActivityAlertType alertType;
3744

3845
@Column(name = "alert_stage", nullable = false)
3946
@Builder.Default
@@ -51,11 +58,19 @@ public void markAsNotified() {
5158
this.alertStage = 1;
5259
}
5360

54-
public boolean isNotified() {
55-
return alertStage > 0;
56-
}
57-
5861
public void markAsRead() {
5962
this.isRead = true;
6063
}
64+
65+
public void hide() {
66+
this.isVisibleToUser = false;
67+
}
68+
69+
public String getNotificationTitle() {
70+
return alertType.getTitle();
71+
}
72+
73+
public String getNotificationBody() {
74+
return alertType.getBodyTemplate(campaign.getTitle());
75+
}
6176
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.example.cherrydan.activity.domain;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum ActivityAlertType {
7+
// 북마크 기반 알림
8+
BOOKMARK_DEADLINE_D1("마감알림", "D-1", "모집이 내일 완료됩니다. 얼른 신청해보세요."),
9+
BOOKMARK_DEADLINE_DDAY("마감알림", "D-Day", "모집이 오늘 종료됩니다."),
10+
11+
// CampaignStatus 기반 알림
12+
APPLY_RESULT_DDAY("공고 결과 알림", "D-Day", "선정 결과를 확인해보세요!"),
13+
14+
// 선정자 방문 알림 (REGION 타입만)
15+
SELECTED_VISIT_D3("방문알림", "D-3", "방문 마감일이 3일 남았습니다."),
16+
SELECTED_VISIT_DDAY("방문알림", "D-Day", "오늘이 마지막 방문 기회예요!"),
17+
18+
// 리뷰 작성 알림
19+
REVIEWING_DEADLINE_D3("리뷰 작성 알림", "D-3", "리뷰 작성이 3일 남았습니다."),
20+
REVIEWING_DEADLINE_DDAY("리뷰 작성 알림", "D-Day", "리뷰 작성이 오늘 마감됩니다. 놓치지 말고 작성해주세요.");
21+
22+
private final String category;
23+
private final String dDayLabel;
24+
private final String messageTemplate;
25+
26+
ActivityAlertType(String category, String dDayLabel, String messageTemplate) {
27+
this.category = category;
28+
this.dDayLabel = dDayLabel;
29+
this.messageTemplate = messageTemplate;
30+
}
31+
32+
public String getTitle() {
33+
return category;
34+
}
35+
36+
public String getBodyTemplate(String campaignTitle) {
37+
return String.format("%s %s %s", dDayLabel, campaignTitle, messageTemplate);
38+
}
39+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.example.cherrydan.activity.domain.vo;
2+
3+
import com.example.cherrydan.activity.domain.ActivityAlert;
4+
import com.example.cherrydan.notification.domain.AlertMessage;
5+
6+
import java.util.Map;
7+
8+
public record ActivityAlertMessage(String title,
9+
String body,
10+
String imageUrl,
11+
Map<String, String> data)
12+
implements AlertMessage {
13+
private static final String TYPE = "activity_alert";
14+
private static final String ACTION = "open_activity_page";
15+
16+
17+
public static ActivityAlertMessage create(ActivityAlert activityAlert){
18+
Map<String, String> data = Map.of(
19+
"type", TYPE,
20+
"alert_type", activityAlert.getAlertType().name(),
21+
"campaign_id", String.valueOf(activityAlert.getCampaign().getId()),
22+
"campaign_title", activityAlert.getCampaign().getTitle(),
23+
"action", ACTION
24+
);
25+
26+
return new ActivityAlertMessage(
27+
activityAlert.getNotificationTitle(),
28+
activityAlert.getNotificationBody(),
29+
activityAlert.getCampaign().getImageUrl(),
30+
data);
31+
}
32+
}

src/main/java/com/example/cherrydan/activity/dto/ActivityAlertResponseDTO.java

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,41 @@
1515
@Builder
1616
@Schema(description = "활동 알림 응답 DTO")
1717
public class ActivityAlertResponseDTO {
18-
19-
@Schema(description = "알림 ID", example = "1")
18+
19+
@Schema(description = "알림 ID", example = "1", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
2020
private Long id;
21-
22-
@Schema(description = "캠페인 ID", example = "123")
21+
22+
@Schema(description = "캠페인 ID", example = "123", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
2323
private Long campaignId;
24-
25-
@Schema(description = "캠페인 제목", example = "[양주] 리치마트 양주점_피드&릴스")
24+
25+
@Schema(description = "알림 타이틀", example = "방문 알림", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
26+
private String alertTitle;
27+
28+
@Schema(description = "dDay 알림 타이틀", example = "D-3", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
29+
private String dayTitle;
30+
31+
@Schema(description = "캠페인 타이틀", example = "[양주] 리치마트 양주점_피드&릴스", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
2632
private String campaignTitle;
27-
28-
@Schema(description = "신청 마감일", example = "2024-07-24")
29-
private LocalDate applyEndDate;
30-
31-
@Schema(description = "알림 날짜", example = "2024-07-21")
33+
34+
@Schema(description = "며칠 남았는 지", example = "피드&릴스 방문일이 3일 남았습니다.", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
35+
private String alertBody;
36+
37+
@Schema(description = "알림 날짜", example = "2024-07-21", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
3238
private LocalDate alertDate;
33-
34-
@Schema(description = "읽음 여부", example = "false")
39+
40+
@Schema(description = "읽음 여부", example = "false", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
3541
private Boolean isRead;
36-
37-
@Schema(description = "D-day (마감까지 남은 일수)", example = "3")
38-
private Integer dDay;
3942

4043
public static ActivityAlertResponseDTO fromEntity(ActivityAlert activityAlert) {
41-
final int FIXED_D_DAY = 3;
42-
LocalDate applyEndDate = activityAlert.getCampaign().getApplyEnd();
43-
4444
return ActivityAlertResponseDTO.builder()
4545
.id(activityAlert.getId())
4646
.campaignId(activityAlert.getCampaign().getId())
47+
.alertTitle(activityAlert.getAlertType().getTitle())
48+
.dayTitle(activityAlert.getAlertType().getDDayLabel())
4749
.campaignTitle(activityAlert.getCampaign().getTitle())
48-
.applyEndDate(applyEndDate)
50+
.alertBody(activityAlert.getAlertType().getMessageTemplate())
4951
.alertDate(activityAlert.getAlertDate())
5052
.isRead(activityAlert.getIsRead())
51-
.dDay(FIXED_D_DAY)
5253
.build();
5354
}
5455
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.example.cherrydan.activity.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@Builder
10+
@AllArgsConstructor
11+
public class UnreadAlertCountResponseDTO {
12+
13+
@Schema(description = "총 미읽은 알림 개수", example = "15", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
14+
private Long totalCount;
15+
16+
@Schema(description = "활동 알림 미읽은 개수", example = "8", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
17+
private Long activityAlertCount;
18+
19+
@Schema(description = "키워드 알림 미읽은 개수", example = "7", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
20+
private Long keywordAlertCount;
21+
}

src/main/java/com/example/cherrydan/activity/repository/ActivityAlertRepository.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package com.example.cherrydan.activity.repository;
22

33
import com.example.cherrydan.activity.domain.ActivityAlert;
4+
import com.example.cherrydan.activity.domain.ActivityAlertType;
45
import org.springframework.data.domain.Page;
56
import org.springframework.data.domain.Pageable;
67
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
79
import org.springframework.data.jpa.repository.Query;
810
import org.springframework.data.repository.query.Param;
911
import org.springframework.stereotype.Repository;
1012

1113
import java.time.LocalDate;
1214
import java.util.List;
15+
import java.util.Set;
1316

1417
@Repository
1518
public interface ActivityAlertRepository extends JpaRepository<ActivityAlert, Long> {
@@ -20,19 +23,34 @@ public interface ActivityAlertRepository extends JpaRepository<ActivityAlert, Lo
2023
@Query("SELECT aa FROM ActivityAlert aa WHERE aa.user.id = :userId AND aa.isVisibleToUser = true ORDER BY aa.alertDate DESC")
2124
Page<ActivityAlert> findByUserIdAndIsVisibleToUserTrue(@Param("userId") Long userId, Pageable pageable);
2225

26+
/**
27+
* 사용자의 활동 알림 개수 조회
28+
*/
29+
@Query("SELECT COUNT(aa) FROM ActivityAlert aa WHERE aa.user.id = :userId AND aa.isVisibleToUser = true")
30+
Long countByUserIdAndIsVisibleToUserTrue(@Param("userId") Long userId);
2331

2432
/**
25-
* 당일 생성된 알림 미발송 활동 알림들 조회 (Campaign과 User를 Fetch Join으로 함께 조회)
33+
* 당일 생성된 알림 미발송 활동 알림들 페이징 조회 (Campaign과 User를 Fetch Join으로 함께 조회)
2634
*/
2735
@Query("SELECT aa FROM ActivityAlert aa " +
2836
"JOIN FETCH aa.campaign c " +
2937
"JOIN FETCH aa.user u " +
3038
"WHERE aa.alertStage = 0 AND aa.isVisibleToUser = true AND aa.alertDate = :alertDate")
31-
List<ActivityAlert> findTodayUnnotifiedAlerts(@Param("alertDate") LocalDate alertDate);
39+
Page<ActivityAlert> findTodayUnnotifiedAlertsWithPaging(@Param("alertDate") LocalDate alertDate, Pageable pageable);
3240

3341
/**
3442
* 사용자와 캠페인으로 알림 존재 여부 확인
3543
*/
3644
@Query("SELECT COUNT(aa) > 0 FROM ActivityAlert aa WHERE aa.user.id = :userId AND aa.campaign.id = :campaignId AND aa.isVisibleToUser = true")
3745
boolean existsByUserIdAndCampaignId(@Param("userId") Long userId, @Param("campaignId") Long campaignId);
46+
47+
/**
48+
* 사용자의 미읽은 활동 알림 개수 조회
49+
*/
50+
@Query("SELECT COUNT(aa) FROM ActivityAlert aa WHERE aa.user.id = :userId AND aa.isRead = false AND aa.isVisibleToUser = true")
51+
Long countUnreadByUserId(@Param("userId") Long userId);
52+
53+
@Modifying
54+
@Query("DELETE FROM ActivityAlert aa WHERE aa.user.id = :userId")
55+
void deleteByUserId(@Param("userId") Long userId);
3856
}

0 commit comments

Comments
 (0)