Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1e9df07
:sparkles: feat: Add StoreDetail Api
sengjun0624 May 29, 2025
b43c30b
:sparkles: feat: Add StoreDetail Voucher Api
sengjun0624 May 29, 2025
a86c540
:sparkles: feat: Set TimeZone KR On Boot
sengjun0624 May 29, 2025
3382886
:sparkles: feat: 카카오맵 조회 API 최적화
sengjun0624 May 29, 2025
5b7a308
fix: HikariCP Pool size error
May 29, 2025
85471e1
Merge pull request #114 from Team-Tokkit/refactor/#100-notification
YoungjaeRo May 29, 2025
9a5ab94
Update src/main/java/com/example/Tokkit_server/store/service/query/St…
sengjun0624 May 30, 2025
037c37e
Update src/main/java/com/example/Tokkit_server/store/controller/Store…
sengjun0624 May 30, 2025
63fc10e
Merge pull request #113 from Team-Tokkit/feat/#112-kakao-map
jeongmin07262 May 30, 2025
68801c4
feat : 자동 전환 요청 DTO 구현
YoungjaeRo May 30, 2025
2e9b748
feat : 자동 전환 응답 DTO 구현
YoungjaeRo May 30, 2025
cb6d5c3
refactor : 거래 타입 (자동 전환) 추가
YoungjaeRo May 30, 2025
4996100
refactor : 거래타입 문자열 길이 확장
YoungjaeRo May 30, 2025
8143125
refactor : 자동 전환에 필요한 필드 추가 (전환신청 여부, 일, 시, 분)
YoungjaeRo May 30, 2025
dc97991
feat : 자동전환 API 구현
YoungjaeRo May 30, 2025
eceb4ca
feat : 전환 예약 등록 (업데이트) 서비스 로직 구현
YoungjaeRo May 30, 2025
300db89
feat : 자동 전환 예약한 wallet 테이블 조회 로직 구현
YoungjaeRo May 30, 2025
9db2e0f
feat : 자동 전환 스케줄러 서비스로직 구현
YoungjaeRo May 30, 2025
383b699
feat: 가맹점주 알림 조회 기능 구현
May 31, 2025
8182f25
feat: 가맹점주 알림 설정 기능 구현
May 31, 2025
8e22658
feat: 가맹점주 알림 생성 기능 구현
May 31, 2025
f3ae566
feat: 가맹점주 회원가입 시 알림 설정 기능 구현
May 31, 2025
22c0399
feat: 가맹점주 알림 기능 구현
noeyoes May 31, 2025
8e606b6
Merge branch 'develop' of https://github.com/Team-Tokkit/Tokkit-Serve…
YoungjaeRo May 31, 2025
d767e8d
Merge pull request #118 from Team-Tokkit/feature#111-Wallet_AutoConvert
YoungjaeRo Jun 2, 2025
99e0511
refactor : 전환 예약 현황에 분단위 추가
YoungjaeRo Jun 2, 2025
c332e8e
refactor : 지갑 개설시, 자동전환 상태 false로 초기화
YoungjaeRo Jun 2, 2025
171298d
feat : 자동 전환 설정 조회 API 구현
YoungjaeRo Jun 2, 2025
c8215ab
feat : 자동 전환 설정 조회 서비스 로직 구현
YoungjaeRo Jun 2, 2025
ac33c3c
chore : 테스트 코드 의존성 추가
YoungjaeRo Jun 2, 2025
5db8773
Merge pull request #120 from Team-Tokkit/feature#119-Wallet_AutoConve…
YoungjaeRo Jun 2, 2025
ed73933
chore: git conflict
Jun 2, 2025
8d117d0
Merge branch 'develop' of https://github.com/Team-Tokkit/Tokkit-Serve…
Jun 2, 2025
e4ca38c
feat: 가맹점주 알림 생성 기능 구현
Jun 2, 2025
c0f85bf
feat: 가맹점주 알림 생성 기능 구현
YoungjaeRo Jun 2, 2025
452ec77
feat: transaction description formatter 생성
Jun 2, 2025
f0f5ea8
feat: 가맹점주 블록체인 상세보기 코드 추가 구현
Jun 2, 2025
58e3ae9
Merge pull request #123 from Team-Tokkit/refactor/#93-transaction
YoungjaeRo Jun 2, 2025
c0c79d0
feat: 자동 전환 알림 생성 로직 추가
Jun 3, 2025
2d067b8
refactor: 이메일 전송 알림 템플릿 수정
Jun 3, 2025
6b23e18
refactor: 예금토큰 자동 전환 수정
Jun 4, 2025
9b722af
feat: 자동 전환 알림 생성 로직 추가
noeyoes Jun 4, 2025
2d6c8e3
feat: 알림 삭제 스케줄러 구현
Jun 4, 2025
a6dc62f
feat: 가맹점주 알림 삭제 스케줄러 구현
Jun 4, 2025
bdfc5b4
feat: 알림 스케줄러 적용
noeyoes Jun 4, 2025
854a0ed
Merge branch 'main' into develop
noeyoes Jun 4, 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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ dependencies {

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'


// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.Tokkit_server.merchant.auth;

import com.example.Tokkit_server.merchant.entity.Merchant;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
Expand Down Expand Up @@ -59,4 +60,14 @@ public String getUsername() {

@Override
public boolean isEnabled() { return true; }

public Merchant toMerchant() {
return Merchant.builder()
.id(this.id)
.businessNumber(this.businessNumber)
.email(this.email)
.password(this.password)
.roles(this.roles)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import com.example.Tokkit_server.merchant.entity.MerchantEmailValidation;
import com.example.Tokkit_server.merchant.repository.MerchantEmailValidationRepository;
import com.example.Tokkit_server.merchant.repository.MerchantRepository;
import com.example.Tokkit_server.notification.entity.MerchantNotificationCategorySetting;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import com.example.Tokkit_server.notification.repository.MerchantNotificationSettingRepository;
import com.example.Tokkit_server.ocr.service.KakaoAddressSearchService;
import com.example.Tokkit_server.ocr.utils.KakaoGeoResult;
import com.example.Tokkit_server.region.entity.Region;
Expand Down Expand Up @@ -42,6 +45,7 @@ public class MerchantService {
private final WalletCommandService walletCommandService;
private final PasswordEncoder passwordEncoder;
private final KakaoAddressSearchService kakaoAddressSearchService;
private final MerchantNotificationSettingRepository notificationSettingRepository;
private final GeometryFactory geometryFactory = new GeometryFactory();

// 회원가입
Expand Down Expand Up @@ -104,6 +108,16 @@ public MerchantRegisterResponseDto createMerchant(CreateMerchantRequestDto reque
Wallet wallet = walletCommandService.createInitialWalletForMerchant(merchant.getId());
merchant.setWallet(wallet);

// 9. 알림 설정
for (NotificationCategory category : NotificationCategory.values()) {
MerchantNotificationCategorySetting setting = MerchantNotificationCategorySetting.builder()
.merchant(merchant)
.category(category)
.enabled(true)
.build();
notificationSettingRepository.save(setting);
}

return MerchantRegisterResponseDto.from(merchant);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.example.Tokkit_server.notification.controller;

import com.example.Tokkit_server.global.apiPayload.ApiResponse;
import com.example.Tokkit_server.global.apiPayload.code.status.SuccessStatus;
import com.example.Tokkit_server.merchant.auth.CustomMerchantDetails;
import com.example.Tokkit_server.notification.dto.response.MerchantNotificationResponseDto;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import com.example.Tokkit_server.notification.service.MerchantNotificationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.List;

@RestController
@RequestMapping("/api/merchants/notifications")
@RequiredArgsConstructor
@Tag(name = "Merchant Notification", description = "가맹점주 알림 관련 API입니다.")
public class MerchantNotificationController {

private final MerchantNotificationService notificationService;

@GetMapping
@Operation(summary = "알림 전체 목록 조회", description = "가맹점주가 설정한 카테고리에 맞춰 전체 알림 목록을 조회합니다.")
public ApiResponse<List<MerchantNotificationResponseDto>> getAllNotifications(@AuthenticationPrincipal CustomMerchantDetails merchantDetails) {
return ApiResponse.onSuccess(notificationService.getAllNotifications(merchantDetails.toMerchant()));
}

@GetMapping("/category")
@Operation(summary = "알림 카테고리별 목록 조회", description = "가맹점주가 설정한 카테고리에 맞춰 카테고리별 알림 목록을 조회합니다.")
public ApiResponse<List<MerchantNotificationResponseDto>> getNotificationsByCategory(
@AuthenticationPrincipal CustomMerchantDetails merchantDetails,
@RequestParam NotificationCategory category) {
return ApiResponse.onSuccess(notificationService.getMerchantNotificationsByCategory(merchantDetails.toMerchant(), category));
}

@DeleteMapping("/{notificationId}")
@Operation(summary = "알림 삭제", description = "알림을 삭제합니다.")
public ApiResponse<?> deleteNotification(
@AuthenticationPrincipal CustomMerchantDetails merchantDetails,
@PathVariable Long notificationId) {
notificationService.deleteNotification(notificationId, merchantDetails.toMerchant());
return ApiResponse.onSuccess(SuccessStatus.NOTIFICATION_DELETE_OK);
}

@GetMapping(value = "/subscribe", produces = "text/event-stream;charset=UTF-8")
@Operation(summary = "알림 구독", description = "가맹점주가 설정한 카테고리에 맞게 SSE 알림을 구독 상태로 만들어줍니다.")
public SseEmitter subscribe(@AuthenticationPrincipal CustomMerchantDetails merchantDetails) {
return notificationService.subscribe(merchantDetails.getId());
}

@PostMapping("/send")
@Operation(summary = "모든 알림 전송", description = "가맹점주가 설정한 카테고리에 맞게 SSE 알림과 이메일 알림을 보냅니다.")
public ApiResponse<?> sendUnsentNotifications(@AuthenticationPrincipal CustomMerchantDetails merchantDetails) {
notificationService.sendUnsentMerchantNotifications(merchantDetails.toMerchant());
return ApiResponse.onSuccess(SuccessStatus.NOTIFICATION_SEND_OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.Tokkit_server.notification.controller;

import com.example.Tokkit_server.global.apiPayload.ApiResponse;
import com.example.Tokkit_server.global.apiPayload.code.status.SuccessStatus;
import com.example.Tokkit_server.merchant.auth.CustomMerchantDetails;
import com.example.Tokkit_server.notification.dto.request.MerchantNotificationCategoryUpdateRequestDto;import com.example.Tokkit_server.notification.dto.response.MerchantNotificationCategorySettingResponseDto;
import com.example.Tokkit_server.notification.service.MerchantNotificationSettingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/merchants/notifications/setting")
@RequiredArgsConstructor
@Tag(name = "Merchant Notification Setting", description = "가맹점주 알림 설정 관련 API입니다.")
public class MerchantNotificationSettingController {
private final MerchantNotificationSettingService notificationSettingService;

@GetMapping
@Operation(summary = "알림 설정 상태 조회", description = "유저의 알림 카테고리 설정 목록을 조회합니다.")
public ApiResponse<List<MerchantNotificationCategorySettingResponseDto>> getSettings(@AuthenticationPrincipal CustomMerchantDetails merchantDetails) {
return ApiResponse.onSuccess(notificationSettingService.getSettings(merchantDetails.getId()));
}

@PutMapping
@Operation(summary = "알림 설정 상태 수정", description = "유저의 알림 카테고리 설정을 수정합니다.")
public ApiResponse<?> updateSetting(
@AuthenticationPrincipal CustomMerchantDetails merchantDetails,
@RequestBody List<MerchantNotificationCategoryUpdateRequestDto> updateReqDtos
) {
notificationSettingService.updateSetting(merchantDetails.getId(), updateReqDtos);
return ApiResponse.onSuccess(SuccessStatus._OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.Tokkit_server.notification.dto.request;

import com.example.Tokkit_server.merchant.entity.Merchant;
import com.example.Tokkit_server.notification.entity.MerchantNotificationCategorySetting;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import lombok.Getter;

@Getter
public class MerchantNotificationCategoryUpdateRequestDto {
private NotificationCategory category;
private boolean enabled;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.Tokkit_server.notification.dto.response;

import com.example.Tokkit_server.notification.entity.MerchantNotificationCategorySetting;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class MerchantNotificationCategorySettingResponseDto {
private NotificationCategory category;
private boolean enabled;

public static MerchantNotificationCategorySettingResponseDto from(MerchantNotificationCategorySetting categorySetting) {
return MerchantNotificationCategorySettingResponseDto.builder()
.category(categorySetting.getCategory())
.enabled(categorySetting.isEnabled())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.Tokkit_server.notification.dto.response;

import com.example.Tokkit_server.notification.entity.MerchantNotification;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class MerchantNotificationResponseDto {
private Long id;
private String title;
private String content;
private NotificationCategory category;
private String deleted;
private LocalDateTime createdAt;

public static MerchantNotificationResponseDto from(MerchantNotification notification) {
return MerchantNotificationResponseDto.builder()
.id(notification.getId())
.title(notification.getTitle())
.content(notification.getContent())
.category(notification.getCategory())
.deleted(notification.isDeleted() ? "deleted" : "active")
.createdAt(notification.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.example.Tokkit_server.notification.entity;

import com.example.Tokkit_server.global.entity.BaseTimeEntity;
import com.example.Tokkit_server.merchant.entity.Merchant;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MerchantNotification extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "merchant_id", nullable = false)
private Merchant merchant;

@Enumerated(EnumType.STRING)
private NotificationCategory category;

private String title;

private String content;

private boolean deleted;

@Column(nullable = false)
private boolean sentSse;

@Column(nullable = false)
private boolean sentMail;

// soft delete 용 메서드
public void softDelete() {
this.deleted = true;
}

// SSE 알림 발송 여부 확인용 메서드
public void markAsSentSse() {
this.sentSse = true;
}

// Mail 알림 발송 여부 확인용 메서드
public void markAsSentMail() { this.sentMail = true; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.Tokkit_server.notification.entity;

import com.example.Tokkit_server.global.entity.BaseTimeEntity;
import com.example.Tokkit_server.merchant.entity.Merchant;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MerchantNotificationCategorySetting extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "merchant_id", nullable = false)
private Merchant merchant;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private NotificationCategory category;

@Column(nullable = false)
private boolean enabled;

public void update(boolean enabled) {
this.enabled = enabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum NotificationTemplate {

// TOKEN 알림
TOKEN_CONVERTED(NotificationCategory.TOKEN, "토큰 전환 완료", "예금 %d원이 토큰으로 전환되었습니다.", false, true),
TOKEN_AUTO_CONVERTED(NotificationCategory.TOKEN, "토큰 자동 전환 완료", "예금 %d원이 토큰으로 전환되었습니다.", true, true),
DEPOSIT_CONVERTED(NotificationCategory.TOKEN, "예금 전환 완료", "토큰 %d TKT가 예금으로 전환되었습니다.", false, true),
/**
* USER 알림
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.Tokkit_server.notification.repository;

import com.example.Tokkit_server.merchant.entity.Merchant;
import com.example.Tokkit_server.notification.entity.MerchantNotification;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

public interface MerchantNotificationRepository extends JpaRepository<MerchantNotification, Long> {
@Query("SELECT n FROM MerchantNotification n WHERE n.merchant = :merchant AND n.category IN :categories AND n.deleted = false")
List<MerchantNotification> findByMerchantAndCategoriesAndDeletedFalse(@Param("merchant") Merchant merchant, @Param("categories")List<NotificationCategory> categories);

@Query("SELECT n FROM MerchantNotification n WHERE n.merchant = :merchant AND n.category = :category AND n.deleted = false")
List<MerchantNotification> findByMerchantAndCategoryAndDeletedFalse(@Param("merchant") Merchant merchant, @Param("category") NotificationCategory category);

Optional<MerchantNotification> findByIdAndMerchant(Long id, Merchant merchant);

List<MerchantNotification> findByMerchantAndDeletedFalse(Merchant merchant);

@Modifying
@Query("UPDATE MerchantNotification n SET n.deleted = true WHERE n.deleted = false AND n.createdAt < :cutoff")
int softDeleteOldNotifications(@org.springframework.data.repository.query.Param("cutoff") LocalDateTime cutoff);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.Tokkit_server.notification.repository;

import com.example.Tokkit_server.merchant.entity.Merchant;
import com.example.Tokkit_server.notification.entity.MerchantNotificationCategorySetting;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MerchantNotificationSettingRepository extends JpaRepository<MerchantNotificationCategorySetting, Long> {
List<MerchantNotificationCategorySetting> findByMerchant(Merchant merchant);
MerchantNotificationCategorySetting findByMerchantAndCategory(Merchant merchant, NotificationCategory category);

List<MerchantNotificationCategorySetting> findByMerchantAndEnabledTrue(Merchant merchant);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.example.Tokkit_server.notification.repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import com.example.Tokkit_server.notification.entity.Notification;
import com.example.Tokkit_server.notification.enums.NotificationCategory;
import com.example.Tokkit_server.user.entity.User;
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;

Expand All @@ -23,4 +25,8 @@ public interface NotificationRepository extends JpaRepository<Notification, Long
Optional<Notification> findByIdAndUser(Long id, User user);

List<Notification> findByUserAndDeletedFalse(User user);

@Modifying
@Query("UPDATE Notification n SET n.deleted = true WHERE n.deleted = false AND n.createdAt < :cutoff")
int softDeleteOldNotifications(@Param("cutoff") LocalDateTime cutoff);
}
Loading
Loading