Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
148 commits
Select commit Hold shift + click to select a range
5dbbde6
Fix: 내 체험단 상태 API 원복 및 상태 보조 라벨 DTO 추가
o-ddong Aug 26, 2025
d846668
Feat: 클라이언트 모듈화를 위해 상태 보조 라벨 DTO 추가
o-ddong Aug 26, 2025
d182ba0
Merge pull request #159 from o-ddong/refactor/campaign-my-status
o-ddong Aug 26, 2025
8588192
refactor: OAuth2AuthenticationProcessingException 생성자 파라미터명을 errorMes…
GoToBILL Aug 26, 2025
4e75b81
feat: OAuth2 에러 처리를 ErrorMessage enum으로 통일하고 provider conflict를 CONFL…
GoToBILL Aug 26, 2025
e68917e
refactor: CampaignStatusRepository에서 사용하지 않는 활동 알림 관련 쿼리를 제거했습니다
GoToBILL Aug 26, 2025
9e442c2
refactor: 이미 가입한 사용자인 경우 401 -> 409 에러
GoToBILL Aug 26, 2025
a35b2dc
refactor: 사용하지 않는 메서드 삭제
GoToBILL Aug 26, 2025
9dfeb48
refactor: 유효하지 않은 이메일의 경우 401 에러로 변경
GoToBILL Aug 26, 2025
c705dd1
Merge pull request #160 from GoToBILL/fix
GoToBILL Aug 26, 2025
ac1830f
refactor: 홈 화면 배너 DTO 개선
o-ddong Aug 27, 2025
ae955e5
refactor: 북마크 DTO 내 체험단 상태와 동일하게 전체 수정
o-ddong Aug 27, 2025
ea63df0
Merge pull request #161 from o-ddong/refactor/campaign-dto-update
o-ddong Aug 27, 2025
5f4dfeb
feat: 캠페인 상태 배치 업데이트/삭제 기능을 구현했습니다
GoToBILL Aug 27, 2025
6ca9797
refactor: OAuth2 제공자 충돌 에러 메시지를 동적으로 개선했습니다
GoToBILL Aug 27, 2025
07ce1fc
refactor: 실제 이메일까지 포함
GoToBILL Aug 27, 2025
299df7e
Merge pull request #162 from GoToBILL/fix
GoToBILL Aug 27, 2025
9b3ee00
refactor: 찜 DTO 기준으로 내 체험단 상태 DTO도 동일하게 개선
o-ddong Aug 28, 2025
3c7a6c1
Feat: 내 체험단 상태 중복 방지를 위해 unique 추가
o-ddong Aug 28, 2025
9c49adb
Merge pull request #163 from o-ddong/refactor/campaign-dto-update
o-ddong Aug 28, 2025
d096d94
refactor: 기존의 result null 반환에서 {} 반환으로 변경
GoToBILL Aug 30, 2025
9afc2ee
Merge branch 'develop' of https://github.com/prography/10th-5Team-BE …
GoToBILL Aug 30, 2025
2d004f9
Merge pull request #164 from GoToBILL/fix
GoToBILL Aug 30, 2025
d87a8a9
Merge branch 'develop' of https://github.com/prography/10th-5Team-BE …
GoToBILL Aug 30, 2025
2d1277c
refactor: n+1 문제 삭제 and 북마크 다 건 삭제로 수정
GoToBILL Aug 30, 2025
85fa224
refactor: campaignStatus isActive null 처리
GoToBILL Aug 30, 2025
cdab655
Merge pull request #165 from GoToBILL/fix
GoToBILL Aug 30, 2025
af80fdb
refactor: 엔드포인트 수정
GoToBILL Aug 30, 2025
a506355
Merge pull request #166 from GoToBILL/fix
GoToBILL Aug 30, 2025
26581b8
refactor: 내 체험단 상태 patch, delete DTO Validation 추가
o-ddong Aug 30, 2025
a0f3a0c
refactor: 북마크 취소 API 배열로 받도록 수정 및 DTO Validation 추가
o-ddong Aug 30, 2025
5909dc8
refactor: 내 체험단 팝업 조회 API 파라미터 기반 데이터 호출 로직으로 변경
o-ddong Aug 30, 2025
e72708b
Merge pull request #167 from o-ddong/refactor/campaign-service
o-ddong Aug 30, 2025
0447f0f
refactor: 쿼리 최적화
GoToBILL Sep 2, 2025
a290f9a
Merge pull request #168 from GoToBILL/fix
GoToBILL Sep 2, 2025
56cd0e8
feat: OAuth 통합 컨트롤러 및 Facade 패턴을 구현했습니다
GoToBILL Sep 6, 2025
529f44e
feat: OAuth Strategy 패턴 및 도메인 모델을 추가했습니다
GoToBILL Sep 6, 2025
cab6640
refactor: 기존 OAuth 컨트롤러들을 @Deprecated로 표시했습니다
GoToBILL Sep 6, 2025
f06a55e
feat: OAuth 도메인 서비스와 로그인 기록 서비스를 추가했습니다
GoToBILL Sep 6, 2025
7804921
test: RefreshTokenServiceTest를 서비스 로직에 맞게 수정했습니다
GoToBILL Sep 6, 2025
a53d725
refactor: RefreshToken 관련 서비스와 리포지토리를 개선했습니다
GoToBILL Sep 6, 2025
9d061aa
refactor: oauth/model에서 oauth/domain으로 패키지를 이동했습니다
GoToBILL Sep 6, 2025
ddbbfad
refactor: User 엔티티와 리포지토리의 AuthProvider import를 수정했습니다
GoToBILL Sep 6, 2025
7dd9feb
test: RefreshTokenServiceIntegrationTest의 import를 수정했습니다
GoToBILL Sep 6, 2025
46d08c8
Merge pull request #169 from GoToBILL/fix
GoToBILL Sep 6, 2025
b125394
feat: User 엔티티에 30일 이내 계정 복구 기능을 추가했습니다
GoToBILL Sep 6, 2025
f5b2678
refactor: OAuth 서비스를 DDD 패턴으로 리팩토링했습니다
GoToBILL Sep 6, 2025
ad4282b
remove: 사용하지 않는 OAuth 컨트롤러와 서비스를 삭제했습니다
GoToBILL Sep 6, 2025
346f4b8
config: OAuth 웹 로그인 설정을 제거했습니다
GoToBILL Sep 6, 2025
d279b17
refactor: yml dev -> local
GoToBILL Sep 6, 2025
cf32c1b
Merge pull request #170 from GoToBILL/fix
GoToBILL Sep 6, 2025
d00ec7b
Feat: 캠페인 관련 API Swagger nullable 추가
o-ddong Sep 9, 2025
f426d5c
Chore: Swagger UI 기본 펼침 설정 변경
o-ddong Sep 9, 2025
c0be6cf
Merge pull request #171 from o-ddong/chore/swagger-nullable
o-ddong Sep 9, 2025
d36e6e8
Remove: 북마크 관련 불필요 파일 제거
o-ddong Sep 9, 2025
168d4c9
refactor: 북마크 노출 문구 변경
o-ddong Sep 9, 2025
bb990e3
Feat: Swagger 누락된 import 추가
o-ddong Sep 9, 2025
0a01c04
Merge pull request #173 from o-ddong/chore/swagger-nullable
o-ddong Sep 9, 2025
16ab012
Merge pull request #172 from o-ddong/refactor/campaign-service
o-ddong Sep 9, 2025
9af6475
feat: ActivityAlert 도메인 모델과 AlertType enum을 추가했습니다
GoToBILL Sep 14, 2025
96c74fb
feat: Iterator 패턴 기반 알림 생성 전략을 구현했습니다
GoToBILL Sep 14, 2025
e0e8a1f
feat: 메모리 효율적인 배치 처리를 위한 Iterator 유틸리티를 추가했습니다
GoToBILL Sep 14, 2025
745ced2
refactor: ActivityAlertService를 Iterator 패턴으로 리팩토링했습니다
GoToBILL Sep 14, 2025
187ea5d
feat: ActivityAlertRepository에 벌크 저장 메서드를 추가했습니다
GoToBILL Sep 14, 2025
7a4ff75
feat: 알림 허용 사용자만 조회하는 Repository 메서드를 추가했습니다
GoToBILL Sep 14, 2025
f89e365
config: 비동기 처리를 위한 ThreadPoolTaskExecutor 설정을 추가했습니다
GoToBILL Sep 14, 2025
5e96f77
test: ActivityAlertService 통합 테스트를 추가했습니다
GoToBILL Sep 14, 2025
bbdb02b
test: 메모리 효율성 검증을 위한 테스트를 추가했습니다
GoToBILL Sep 14, 2025
6203ce6
refactor: ActivityController 코드 정리 및 개선했습니다
GoToBILL Sep 14, 2025
da2d7f5
refactor: 키워드 알림 관련 서비스 로직을 개선했습니다
GoToBILL Sep 14, 2025
36c94ee
refactor: User 관련 DTO 클래스들을 정리했습니다
GoToBILL Sep 14, 2025
24940fe
refactor: FCMTokenUpdateRequest DTO를 개선했습니다
GoToBILL Sep 14, 2025
6caa864
refactor: AppleOAuthStrategy 로직을 개선했습니다
GoToBILL Sep 14, 2025
981e97b
refactor: CampaignServiceImpl 비즈니스 로직을 개선했습니다
GoToBILL Sep 14, 2025
de1a1ac
chore: build.gradle 의존성을 정리했습니다
GoToBILL Sep 14, 2025
f7abd47
test: CustomOAuth2UserServiceTest를 업데이트했습니다
GoToBILL Sep 14, 2025
98116fe
refactor: 기존의 비동기 로직을 배치 처리를 통하여 OOM 위험을 감소시켰습니다.
GoToBILL Sep 25, 2025
82c86c2
refactor: sl4f 추가
GoToBILL Sep 25, 2025
b5d0227
Merge pull request #174 from GoToBILL/fix
o-ddong Sep 27, 2025
4cb0f79
docs: ActivityAlertResponseDTO에 Swagger nullable과 requiredMode를 명시했습니다
GoToBILL Oct 7, 2025
a007007
docs: Campaign Response DTO에 Swagger nullable과 requiredMode를 명시하고 버전 …
GoToBILL Oct 7, 2025
b826cf1
docs: Campaign Request DTO에 Swagger nullable과 requiredMode를 명시했습니다
GoToBILL Oct 7, 2025
95acaa3
docs: User DTO에 Swagger nullable과 requiredMode를 명시했습니다
GoToBILL Oct 7, 2025
9e39315
refactor: CampaignStatusController에서 사용하지 않는 import를 제거했습니다
GoToBILL Oct 7, 2025
c9a16d8
Merge pull request #175 from GoToBILL/docs/swagger-nullable-specifica…
GoToBILL Oct 7, 2025
cac6226
feat: ActivityAlert와 KeywordCampaignAlert Repository에 미읽은 알림 개수 조회 쿼리…
GoToBILL Oct 7, 2025
65b582d
feat: 미읽은 알림 개수 조회 API를 구현했습니다
GoToBILL Oct 7, 2025
c676a3a
refactor: Controller의 반환 타입을 ResponseEntity로 통일했습니다
GoToBILL Oct 7, 2025
a068abf
refactor: 알림 개수 타입을 Integer에서 Long으로 변경하여 불필요한 형변환을 제거했습니다
GoToBILL Oct 7, 2025
982fa6a
refactor: 알림 삭제를 Hard Delete에서 Soft Delete로 변경했습니다
GoToBILL Oct 7, 2025
112cdf0
test: AlertService 통합 테스트를 추가했습니다
GoToBILL Oct 7, 2025
9db0c91
feat: alertController add
GoToBILL Oct 7, 2025
68ef5da
Merge pull request #176 from GoToBILL/feature/alert
GoToBILL Oct 13, 2025
2c3b524
fix: 유효하지 않은 토큰을 헤더에 넣을시에 permitALl() 엔드포인트임에도 불구하고 토큰 에러나는 것을 수정했습니다.
GoToBILL Oct 14, 2025
21df7ba
refactor: 토큰 에러 메시지를 상수로 뺐습니다.
GoToBILL Oct 14, 2025
12ca992
Merge pull request #177 from GoToBILL/feature/alert
GoToBILL Oct 14, 2025
cc40a1e
feat: SNS OAuth에 JWT 기반 state 및 딥링크 리다이렉트를 추가했습니다
GoToBILL Oct 19, 2025
b8c304d
refactor: SnsConnection에서 불필요한 토큰 저장을 제거했습니다
GoToBILL Oct 19, 2025
4c553c9
refactor: IllegalArgumentException을 커스텀 예외로 변경했습니다
GoToBILL Oct 19, 2025
7c0b978
feat: 1년 경과 탈퇴 유저의 관련 데이터 삭제 기능을 추가했습니다
GoToBILL Oct 19, 2025
5a49f7d
feat: 슬로우 쿼리 파악을 위한 ServiceMetricsAspect를 추가했습니다
GoToBILL Oct 19, 2025
2c83d56
test: SnsOAuthService 및 UserDataCleanupService에 대한 통합 테스트를 추가했습니다
GoToBILL Oct 19, 2025
2ee3ee7
chore: SNS 설정 및 기타 도메인 수정사항을 반영했습니다
GoToBILL Oct 19, 2025
878e4ee
chore: static 테스트 파일을 gitignore에 추가했습니다
GoToBILL Oct 19, 2025
e94b404
refactor: JWT secret key 생성 로직을 JwtSecretKeyProvider 유틸로 통합했습니다
GoToBILL Oct 28, 2025
fe4f020
fix: 사용자 복구 기간 관련 주석 및 코드를 1년 기준으로 수정했습니다
GoToBILL Oct 28, 2025
dafab46
feat: 사용자 탈퇴 시 FCM 토큰 소프트 삭제 기능을 추가했습니다
GoToBILL Oct 28, 2025
091eeb2
perf: 알림 대상자 조회 쿼리를 DISTINCT에서 EXISTS로 최적화했습니다
GoToBILL Oct 28, 2025
f09514b
refactor: 삭제 기간이 1년이 초과된 유저의 삭제 스케쥴링 작업시 n명의 유저가 있을 때 특정 유저의 삭제 작업 실패…
GoToBILL Oct 29, 2025
b405e58
refactor: 유저 삭제 로직 정합성 보장
GoToBILL Oct 29, 2025
7566d56
Merge pull request #178 from GoToBILL/feature/alert
GoToBILL Oct 29, 2025
0d409a5
refactor: nullable 여부 로직 추가
GoToBILL Oct 29, 2025
410f620
Merge pull request #181 from GoToBILL/feature/alert
GoToBILL Oct 29, 2025
e2e5413
refactor: 기존의 키워드 쿼리를 최적화하였습니다.
GoToBILL Nov 14, 2025
b13fa77
Merge branch 'develop' of https://github.com/prography/10th-5Team-BE …
GoToBILL Nov 14, 2025
0b07d0a
fix: 내 체험단 상태(case)별 기능 분리 적용
o-ddong Nov 16, 2025
3005d4e
fix: 팝업 API 관심공고 노출 기준으로 변경
o-ddong Nov 16, 2025
7e9c7d9
fix: 관심 공고 API 통합 및 마감공고 한달 제한 기준 추가
o-ddong Nov 16, 2025
b0b0272
Merge pull request #183 from o-ddong/develop
o-ddong Nov 16, 2025
ad3b907
refactor: countByKeywordSimpleLike 쿼리를 수정했습니다.
GoToBILL Nov 24, 2025
38b08de
Merge pull request #182 from GoToBILL/feature/alert
GoToBILL Nov 24, 2025
c94f06d
fix: jpql 문법 오류 수정
GoToBILL Nov 24, 2025
19b24af
Merge pull request #184 from GoToBILL/feature/enhancement
GoToBILL Nov 24, 2025
ee3143e
refactor: 널 체크 추가
GoToBILL Nov 30, 2025
af1ba5f
refactor: setActive를 도메인 로직으로 변경하였습니다.
GoToBILL Nov 30, 2025
0e665b9
refactor: setActive와 setUdate를 도메인 로직으로 변경하였습니다.
GoToBILL Nov 30, 2025
241c7c5
refactor: if else 로직을 개선했습니다.
GoToBILL Nov 30, 2025
d386f02
refactor: 조회시 date가 오늘일 경우 새벽에 생성되는 daily 테이블을 조회하고 그 외에는 like 연산을 통해…
GoToBILL Nov 30, 2025
55b7866
refactor: if else 로직 개선
GoToBILL Nov 30, 2025
f218e32
refactor: 로직 개선
GoToBILL Nov 30, 2025
5fbeeb5
refactor: 스케쥴링 시간 변경
GoToBILL Nov 30, 2025
2ba3854
refactor: sns 커스텀 에러 추가
GoToBILL Nov 30, 2025
9854c60
refactor: 커스텀 에러 던지도록 수정
GoToBILL Nov 30, 2025
3098e5b
refactor: 푸시 메세지 상수화
GoToBILL Nov 30, 2025
c3bd8d3
feat: 전날 생성된 켐페인을 새로운 테이블 생성하는 jdbc 스케쥴러 생성
GoToBILL Nov 30, 2025
b10049e
feat: 캠페인 스케쥴러 테스트 추가
GoToBILL Nov 30, 2025
475fa55
refactor: 쿼리 최적화
GoToBILL Nov 30, 2025
5a0a8fe
refactor: 로직 개선
GoToBILL Nov 30, 2025
0221d0d
Merge branch 'develop' of https://github.com/prography/10th-5Team-BE …
GoToBILL Nov 30, 2025
0611225
feat: 하이브리드 로직 테스트 추가
GoToBILL Nov 30, 2025
629782b
Merge pull request #185 from GoToBILL/feature/enhancement
o-ddong Dec 2, 2025
2690d75
refactor: 기존의 NotificationRequest 만드는 로직을 VO 중심으로 리팩토링 했습니다.
GoToBILL Dec 16, 2025
023ee6b
refactor: illegalArgumentException 던지는 방식을 스프링의 Assert를 이용하게 수정
GoToBILL Dec 16, 2025
72c775b
fix: 리프레쉬 토큰 정합성 문제 해결
GoToBILL Dec 18, 2025
9f6ce82
refactor: 사용하지 않는 메서드 제거
GoToBILL Dec 18, 2025
34554b3
Merge pull request #186 from GoToBILL/feature/enhancement
GoToBILL Dec 18, 2025
f847b34
fix: enum error fix & pageable 컨밴션 유지
GoToBILL Dec 23, 2025
8f58435
Merge pull request #187 from GoToBILL/feature/enhancement
GoToBILL Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ src/main/resources/.env
### Firebase ###
firebase-service-account.json
**/firebase-service-account*.json

### Static Test Files ###
src/main/resources/static/
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// JOL (Java Object Layout) - 메모리 크기 측정용
testImplementation 'org.openjdk.jol:jol-core:0.17'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

Expand All @@ -36,50 +37,62 @@ public class ActivityController {
description = """
사용자의 북마크 기반 활동 알림 목록을 조회합니다.
북마크한 캠페인의 신청 마감이 3일 남았을 때 생성되는 알림들입니다.

**쿼리 파라미터 예시:**
- ?page=0&size=20&sort=alertDate,desc
- ?page=1&size=10&sort=alertDate,asc

**정렬 가능한 필드:**
- alertDate: 알림 생성 날짜 (기본값, DESC)

**주의:** 이는 Request Body가 아닌 **Query Parameter**입니다.
"""
)
@GetMapping("/bookmark-alerts")
public ApiResponse<PageListResponseDTO<ActivityAlertResponseDTO>> getBookmarkActivityAlerts(
public ResponseEntity<ApiResponse<PageListResponseDTO<ActivityAlertResponseDTO>>> getBookmarkActivityAlerts(
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl currentUser,
@PageableDefault(size = 20, sort = "alertDate", direction = Sort.Direction.DESC) Pageable pageable
) {
Page<ActivityAlertResponseDTO> alerts = activityAlertService.getUserActivityAlerts(currentUser.getId(), pageable);
PageListResponseDTO<ActivityAlertResponseDTO> response = PageListResponseDTO.from(alerts);
return ApiResponse.success("북마크 활동 알림 목록 조회 성공", response);
return ResponseEntity.ok(ApiResponse.success("북마크 활동 알림 목록 조회 성공", response));
}

@Operation(
summary = "북마크 기반 활동 알림 개수 조회",
description = "사용자의 북마크 기반 활동 알림 개수를 조회합니다."
)
@GetMapping("/bookmark-alerts/count")
public ResponseEntity<ApiResponse<Long>> getBookmarkActivityAlertsCount(
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl currentUser
) {
Long alertsCount = activityAlertService.getUserActivityAlertsCount(currentUser.getId());
return ResponseEntity.ok(ApiResponse.success("북마크 활동 알림 개수 조회 성공", alertsCount));
}

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

@Operation(
summary = "북마크 기반 활동 알림 읽음 처리",
description = "선택한 북마크 기반 활동 알림들을 읽음 상태로 변경합니다. 본인의 알림이 아닌 경우 403 에러를 반환합니다."
)
@PatchMapping("/bookmark-alerts/read")
public ApiResponse<Void> markBookmarkActivityAlertsAsRead(
public ResponseEntity<ApiResponse<Void>> markBookmarkActivityAlertsAsRead(
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl currentUser,
@RequestBody AlertIdsRequestDTO request
) {
activityAlertService.markActivityAlertsAsRead(currentUser.getId(), request.getAlertIds());
return ApiResponse.success("북마크 활동 알림 읽음 처리 성공", null);
return ResponseEntity.ok(ApiResponse.success("북마크 활동 알림 읽음 처리 성공", null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.cherrydan.activity.controller;

import com.example.cherrydan.activity.dto.UnreadAlertCountResponseDTO;
import com.example.cherrydan.activity.service.AlertService;
import com.example.cherrydan.common.response.ApiResponse;
import com.example.cherrydan.oauth.security.jwt.UserDetailsImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Alert", description = "알림 관련 API")
@RestController
@RequestMapping("/api/alerts")
@RequiredArgsConstructor
public class AlertController {

private final AlertService alertService;

@Operation(
summary = "미읽은 알림 개수 조회",
description = "사용자의 미읽은 알림 개수를 조회합니다. 활동 알림과 키워드 알림의 개수를 각각 제공하며, 전체 개수도 함께 반환합니다."
)
@GetMapping("/unread-count")
public ResponseEntity<ApiResponse<UnreadAlertCountResponseDTO>> getUnreadAlertCount(
@Parameter(hidden = true) @AuthenticationPrincipal UserDetailsImpl user
) {
UnreadAlertCountResponseDTO result = alertService.getUnreadAlertCount(user.getId());
return ResponseEntity.ok(ApiResponse.success(result));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
indexes = {
@Index(name = "idx_user_visible_alert_date", columnList = "user_id, is_visible_to_user, alert_date"),
@Index(name = "idx_alert_stage_visible", columnList = "alert_stage, is_visible_to_user")
},
uniqueConstraints = {
@UniqueConstraint(name = "uk_activity_alert", columnNames = {"user_id", "campaign_id", "alert_type", "alert_date"})
})
@Getter
@NoArgsConstructor
Expand All @@ -34,6 +37,10 @@ public class ActivityAlert extends BaseTimeEntity {

@Column(name = "alert_date", nullable = false)
private LocalDate alertDate;

@Enumerated(EnumType.STRING)
@Column(name = "alert_type", nullable = false, length = 50)
private ActivityAlertType alertType;

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

public boolean isNotified() {
return alertStage > 0;
}

public void markAsRead() {
this.isRead = true;
}

public void hide() {
this.isVisibleToUser = false;
}

public String getNotificationTitle() {
return alertType.getTitle();
}

public String getNotificationBody() {
return alertType.getBodyTemplate(campaign.getTitle());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.cherrydan.activity.domain;

import lombok.Getter;

@Getter
public enum ActivityAlertType {
// 북마크 기반 알림
BOOKMARK_DEADLINE_D1("마감알림", "D-1", "모집이 내일 완료됩니다. 얼른 신청해보세요."),
BOOKMARK_DEADLINE_DDAY("마감알림", "D-Day", "모집이 오늘 종료됩니다."),

// CampaignStatus 기반 알림
APPLY_RESULT_DDAY("공고 결과 알림", "D-Day", "선정 결과를 확인해보세요!"),

// 선정자 방문 알림 (REGION 타입만)
SELECTED_VISIT_D3("방문알림", "D-3", "방문 마감일이 3일 남았습니다."),
SELECTED_VISIT_DDAY("방문알림", "D-Day", "오늘이 마지막 방문 기회예요!"),

// 리뷰 작성 알림
REVIEWING_DEADLINE_D3("리뷰 작성 알림", "D-3", "리뷰 작성이 3일 남았습니다."),
REVIEWING_DEADLINE_DDAY("리뷰 작성 알림", "D-Day", "리뷰 작성이 오늘 마감됩니다. 놓치지 말고 작성해주세요.");

private final String category;
private final String dDayLabel;
private final String messageTemplate;

ActivityAlertType(String category, String dDayLabel, String messageTemplate) {
this.category = category;
this.dDayLabel = dDayLabel;
this.messageTemplate = messageTemplate;
}

public String getTitle() {
return category;
}

public String getBodyTemplate(String campaignTitle) {
return String.format("%s %s %s", dDayLabel, campaignTitle, messageTemplate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.cherrydan.activity.domain.vo;

import com.example.cherrydan.activity.domain.ActivityAlert;
import com.example.cherrydan.notification.domain.AlertMessage;

import java.util.Map;

public record ActivityAlertMessage(String title,
String body,
String imageUrl,
Map<String, String> data)
implements AlertMessage {
private static final String TYPE = "activity_alert";
private static final String ACTION = "open_activity_page";


public static ActivityAlertMessage create(ActivityAlert activityAlert){
Map<String, String> data = Map.of(
"type", TYPE,
"alert_type", activityAlert.getAlertType().name(),
"campaign_id", String.valueOf(activityAlert.getCampaign().getId()),
"campaign_title", activityAlert.getCampaign().getTitle(),
"action", ACTION
);

return new ActivityAlertMessage(
activityAlert.getNotificationTitle(),
activityAlert.getNotificationBody(),
activityAlert.getCampaign().getImageUrl(),
data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,41 @@
@Builder
@Schema(description = "활동 알림 응답 DTO")
public class ActivityAlertResponseDTO {
@Schema(description = "알림 ID", example = "1")

@Schema(description = "알림 ID", example = "1", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private Long id;
@Schema(description = "캠페인 ID", example = "123")

@Schema(description = "캠페인 ID", example = "123", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private Long campaignId;

@Schema(description = "캠페인 제목", example = "[양주] 리치마트 양주점_피드&릴스")

@Schema(description = "알림 타이틀", example = "방문 알림", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private String alertTitle;

@Schema(description = "dDay 알림 타이틀", example = "D-3", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private String dayTitle;

@Schema(description = "캠페인 타이틀", example = "[양주] 리치마트 양주점_피드&릴스", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private String campaignTitle;
@Schema(description = "신청 마감일", example = "2024-07-24")
private LocalDate applyEndDate;
@Schema(description = "알림 날짜", example = "2024-07-21")

@Schema(description = "며칠 남았는 지", example = "피드&릴스 방문일이 3일 남았습니다.", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private String alertBody;

@Schema(description = "알림 날짜", example = "2024-07-21", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate alertDate;
@Schema(description = "읽음 여부", example = "false")

@Schema(description = "읽음 여부", example = "false", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean isRead;

@Schema(description = "D-day (마감까지 남은 일수)", example = "3")
private Integer dDay;

public static ActivityAlertResponseDTO fromEntity(ActivityAlert activityAlert) {
final int FIXED_D_DAY = 3;
LocalDate applyEndDate = activityAlert.getCampaign().getApplyEnd();

return ActivityAlertResponseDTO.builder()
.id(activityAlert.getId())
.campaignId(activityAlert.getCampaign().getId())
.alertTitle(activityAlert.getAlertType().getTitle())
.dayTitle(activityAlert.getAlertType().getDDayLabel())
.campaignTitle(activityAlert.getCampaign().getTitle())
.applyEndDate(applyEndDate)
.alertBody(activityAlert.getAlertType().getMessageTemplate())
.alertDate(activityAlert.getAlertDate())
.isRead(activityAlert.getIsRead())
.dDay(FIXED_D_DAY)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.cherrydan.activity.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@AllArgsConstructor
public class UnreadAlertCountResponseDTO {

@Schema(description = "총 미읽은 알림 개수", example = "15", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private Long totalCount;

@Schema(description = "활동 알림 미읽은 개수", example = "8", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private Long activityAlertCount;

@Schema(description = "키워드 알림 미읽은 개수", example = "7", nullable = false, requiredMode = Schema.RequiredMode.REQUIRED)
private Long keywordAlertCount;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.example.cherrydan.activity.repository;

import com.example.cherrydan.activity.domain.ActivityAlert;
import com.example.cherrydan.activity.domain.ActivityAlertType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;
import java.util.Set;

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

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

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

/**
* 사용자와 캠페인으로 알림 존재 여부 확인
*/
@Query("SELECT COUNT(aa) > 0 FROM ActivityAlert aa WHERE aa.user.id = :userId AND aa.campaign.id = :campaignId AND aa.isVisibleToUser = true")
boolean existsByUserIdAndCampaignId(@Param("userId") Long userId, @Param("campaignId") Long campaignId);

/**
* 사용자의 미읽은 활동 알림 개수 조회
*/
@Query("SELECT COUNT(aa) FROM ActivityAlert aa WHERE aa.user.id = :userId AND aa.isRead = false AND aa.isVisibleToUser = true")
Long countUnreadByUserId(@Param("userId") Long userId);

@Modifying
@Query("DELETE FROM ActivityAlert aa WHERE aa.user.id = :userId")
void deleteByUserId(@Param("userId") Long userId);
}
Loading