diff --git a/src/main/java/com/team/buddyya/certification/service/PhoneAuthenticationService.java b/src/main/java/com/team/buddyya/certification/service/PhoneAuthenticationService.java index 55b0c6a4..e024c470 100644 --- a/src/main/java/com/team/buddyya/certification/service/PhoneAuthenticationService.java +++ b/src/main/java/com/team/buddyya/certification/service/PhoneAuthenticationService.java @@ -9,19 +9,26 @@ import com.team.buddyya.certification.dto.response.TestAccountResponse; import com.team.buddyya.certification.exception.PhoneAuthenticationException; import com.team.buddyya.certification.exception.PhoneAuthenticationExceptionType; -import com.team.buddyya.certification.repository.*; +import com.team.buddyya.certification.repository.AdminAccountRepository; +import com.team.buddyya.certification.repository.PhoneInfoRepository; +import com.team.buddyya.certification.repository.RegisteredPhoneRepository; +import com.team.buddyya.certification.repository.StudentIdCardRepository; +import com.team.buddyya.certification.repository.TestAccountRepository; import com.team.buddyya.point.domain.Point; import com.team.buddyya.point.service.FindPointService; +import com.team.buddyya.student.domain.MatchingProfile; import com.team.buddyya.student.domain.Student; import com.team.buddyya.student.dto.response.UserResponse; +import com.team.buddyya.student.exception.StudentException; +import com.team.buddyya.student.exception.StudentExceptionType; +import com.team.buddyya.student.repository.MatchingProfileRepository; import com.team.buddyya.student.repository.StudentRepository; import com.team.buddyya.student.service.InvitationService; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @RequiredArgsConstructor @Transactional @@ -37,6 +44,7 @@ public class PhoneAuthenticationService { private final TestAccountRepository testAccountRepository; private final FindPointService findPointService; private final AdminAccountRepository adminAccountRepository; + private final MatchingProfileRepository matchingProfileRepository; private final InvitationService invitationService; private final JwtUtils jwtUtils; @@ -68,7 +76,9 @@ public UserResponse checkMembership(String phoneNumber) { student.getAvatar().setLoggedOut(false); boolean isStudentIdCardRequested = studentIdCardRepository.findByStudent(student).isPresent(); Point point = findPointService.findByStudent(student); - return UserResponse.fromCheckMembership(student, isStudentIdCardRequested, EXISTING_MEMBER, accessToken, refreshToken, point); + MatchingProfile matchingProfile = getMatchingProfile(student); + return UserResponse.fromCheckMembership(student, isStudentIdCardRequested, EXISTING_MEMBER, accessToken, + refreshToken, point, matchingProfile); } return UserResponse.fromCheckMembership(NEW_MEMBER); } @@ -106,7 +116,13 @@ private void resetMessageCountIfExists(String udId) { return; } PhoneInfo phoneInfo = phoneInfoRepository.findPhoneInfoByUdId(udId) - .orElseThrow(() -> new PhoneAuthenticationException(PhoneAuthenticationExceptionType.PHONE_INFO_NOT_FOUND)); + .orElseThrow( + () -> new PhoneAuthenticationException(PhoneAuthenticationExceptionType.PHONE_INFO_NOT_FOUND)); phoneInfo.resetMessageSendCount(); } + + private MatchingProfile getMatchingProfile(Student student) { + return matchingProfileRepository.findByStudent(student) + .orElseThrow(() -> new StudentException(StudentExceptionType.MATCHING_PROFILE_NOT_FOUND)); + } } diff --git a/src/main/java/com/team/buddyya/event/controller/EventController.java b/src/main/java/com/team/buddyya/event/controller/EventController.java new file mode 100644 index 00000000..33877461 --- /dev/null +++ b/src/main/java/com/team/buddyya/event/controller/EventController.java @@ -0,0 +1,28 @@ +package com.team.buddyya.event.controller; + +import com.team.buddyya.auth.domain.CustomUserDetails; +import com.team.buddyya.event.dto.CouponRequest; +import com.team.buddyya.event.dto.CouponResponse; +import com.team.buddyya.event.service.EventService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/events") +@RequiredArgsConstructor +public class EventController { + + private final EventService eventService; + + @PostMapping("/coupon") + public ResponseEntity registerCoupon(@AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody CouponRequest request) { + CouponResponse response = eventService.useCoupon(userDetails.getStudentInfo(), request); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/team/buddyya/event/domain/Coupon.java b/src/main/java/com/team/buddyya/event/domain/Coupon.java new file mode 100644 index 00000000..4ede1448 --- /dev/null +++ b/src/main/java/com/team/buddyya/event/domain/Coupon.java @@ -0,0 +1,36 @@ +package com.team.buddyya.event.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.GenerationType.IDENTITY; + +@Entity +@Table(name = "coupon") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Coupon { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(name = "code", length = 10, nullable = false, unique = true) + private String code; + + @Column(name = "used", nullable = false) + private Boolean isUsed; + + @Builder + public Coupon(String code) { + this.code = code; + this.isUsed = false; + } + + public void markAsUsed() { + this.isUsed = true; + } +} diff --git a/src/main/java/com/team/buddyya/event/dto/CouponRequest.java b/src/main/java/com/team/buddyya/event/dto/CouponRequest.java new file mode 100644 index 00000000..d4ddd4cb --- /dev/null +++ b/src/main/java/com/team/buddyya/event/dto/CouponRequest.java @@ -0,0 +1,4 @@ +package com.team.buddyya.event.dto; + +public record CouponRequest(String code) { +} diff --git a/src/main/java/com/team/buddyya/event/dto/CouponResponse.java b/src/main/java/com/team/buddyya/event/dto/CouponResponse.java new file mode 100644 index 00000000..3b11fe16 --- /dev/null +++ b/src/main/java/com/team/buddyya/event/dto/CouponResponse.java @@ -0,0 +1,12 @@ +package com.team.buddyya.event.dto; + +import com.team.buddyya.point.domain.Point; +import com.team.buddyya.point.domain.PointType; + +public record CouponResponse(Integer point, + int pointChange) { + + public static CouponResponse from(Point point, PointType pointType) { + return new CouponResponse(point.getCurrentPoint(), pointType.getPointChange()); + } +} diff --git a/src/main/java/com/team/buddyya/event/exception/EventException.java b/src/main/java/com/team/buddyya/event/exception/EventException.java new file mode 100644 index 00000000..eeb6c816 --- /dev/null +++ b/src/main/java/com/team/buddyya/event/exception/EventException.java @@ -0,0 +1,18 @@ +package com.team.buddyya.event.exception; + +import com.team.buddyya.common.exception.BaseException; +import com.team.buddyya.common.exception.BaseExceptionType; + +public class EventException extends BaseException { + + private final EventExceptionType exceptionType; + + public EventException(EventExceptionType exceptionType) { + this.exceptionType = exceptionType; + } + + @Override + public BaseExceptionType exceptionType() { + return exceptionType; + } +} diff --git a/src/main/java/com/team/buddyya/event/exception/EventExceptionType.java b/src/main/java/com/team/buddyya/event/exception/EventExceptionType.java new file mode 100644 index 00000000..12587cc2 --- /dev/null +++ b/src/main/java/com/team/buddyya/event/exception/EventExceptionType.java @@ -0,0 +1,35 @@ +package com.team.buddyya.event.exception; + +import com.team.buddyya.common.exception.BaseExceptionType; +import org.springframework.http.HttpStatus; + +public enum EventExceptionType implements BaseExceptionType { + + COUPON_NOT_FOUND(12000, HttpStatus.NOT_FOUND, "The coupon does not exist."), + COUPON_ALREADY_USED(12001, HttpStatus.BAD_REQUEST, "The coupon has already been used."); + + private final int errorCode; + private final HttpStatus httpStatus; + private final String errorMessage; + + EventExceptionType(int errorCode, HttpStatus httpStatus, String errorMessage) { + this.errorCode = errorCode; + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } + + @Override + public int errorCode() { + return errorCode; + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String errorMessage() { + return errorMessage; + } +} diff --git a/src/main/java/com/team/buddyya/event/repository/CouponRepository.java b/src/main/java/com/team/buddyya/event/repository/CouponRepository.java new file mode 100644 index 00000000..9ea28e44 --- /dev/null +++ b/src/main/java/com/team/buddyya/event/repository/CouponRepository.java @@ -0,0 +1,11 @@ +package com.team.buddyya.event.repository; + +import com.team.buddyya.event.domain.Coupon; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CouponRepository extends JpaRepository { + + Optional findByCode(String code); +} diff --git a/src/main/java/com/team/buddyya/event/service/EventService.java b/src/main/java/com/team/buddyya/event/service/EventService.java new file mode 100644 index 00000000..a08fd6d2 --- /dev/null +++ b/src/main/java/com/team/buddyya/event/service/EventService.java @@ -0,0 +1,39 @@ +package com.team.buddyya.event.service; + +import com.team.buddyya.auth.domain.StudentInfo; +import com.team.buddyya.event.domain.Coupon; +import com.team.buddyya.event.dto.CouponRequest; +import com.team.buddyya.event.dto.CouponResponse; +import com.team.buddyya.event.exception.EventException; +import com.team.buddyya.event.exception.EventExceptionType; +import com.team.buddyya.event.repository.CouponRepository; +import com.team.buddyya.point.domain.Point; +import com.team.buddyya.point.domain.PointType; +import com.team.buddyya.point.service.UpdatePointService; +import com.team.buddyya.student.domain.Student; +import com.team.buddyya.student.service.FindStudentService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class EventService { + + private final FindStudentService findStudentService; + private final UpdatePointService updatePointService; + private final CouponRepository couponRepository; + + public CouponResponse useCoupon(StudentInfo studentInfo, CouponRequest request) { + Coupon coupon = couponRepository.findByCode(request.code()) + .orElseThrow(() -> new EventException(EventExceptionType.COUPON_NOT_FOUND)); + if (coupon.getIsUsed()) { + throw new EventException(EventExceptionType.COUPON_ALREADY_USED); + } + Student student = findStudentService.findByStudentId(studentInfo.id()); + Point updatedPoint = updatePointService.updatePoint(student, PointType.COUPON_EVENT_REWARD); + coupon.markAsUsed(); + return CouponResponse.from(updatedPoint, PointType.COUPON_EVENT_REWARD); + } +} diff --git a/src/main/java/com/team/buddyya/notification/controller/NotificationController.java b/src/main/java/com/team/buddyya/notification/controller/NotificationController.java index 65306cb7..2632be52 100644 --- a/src/main/java/com/team/buddyya/notification/controller/NotificationController.java +++ b/src/main/java/com/team/buddyya/notification/controller/NotificationController.java @@ -1,6 +1,7 @@ package com.team.buddyya.notification.controller; import com.team.buddyya.auth.domain.CustomUserDetails; +import com.team.buddyya.notification.dto.request.PushToAllUsersRequest; import com.team.buddyya.notification.dto.request.SaveTokenRequest; import com.team.buddyya.notification.dto.response.SaveTokenResponse; import com.team.buddyya.notification.service.NotificationService; @@ -21,4 +22,10 @@ public ResponseEntity registerPushToken(@AuthenticationPrinci @RequestBody SaveTokenRequest request) { return ResponseEntity.ok(notificationService.savePushToken(userDetails.getStudentInfo().id(), request.token())); } + + @PostMapping("/send-to-all") + public ResponseEntity sendNotificationToAllUser(@RequestBody PushToAllUsersRequest request) { + notificationService.sendNotificationToAllUser(request); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/team/buddyya/notification/dto/request/PushToAllUsersRequest.java b/src/main/java/com/team/buddyya/notification/dto/request/PushToAllUsersRequest.java new file mode 100644 index 00000000..3a1c84d6 --- /dev/null +++ b/src/main/java/com/team/buddyya/notification/dto/request/PushToAllUsersRequest.java @@ -0,0 +1,4 @@ +package com.team.buddyya.notification.dto.request; + +public record PushToAllUsersRequest(long feedId, String title, String body) { +} diff --git a/src/main/java/com/team/buddyya/notification/service/NotificationService.java b/src/main/java/com/team/buddyya/notification/service/NotificationService.java index 0c2b127d..ec0cfdb6 100644 --- a/src/main/java/com/team/buddyya/notification/service/NotificationService.java +++ b/src/main/java/com/team/buddyya/notification/service/NotificationService.java @@ -4,9 +4,13 @@ import com.team.buddyya.chatting.domain.Chatroom; import com.team.buddyya.feed.domain.Comment; import com.team.buddyya.feed.domain.Feed; +import com.team.buddyya.feed.exception.FeedException; +import com.team.buddyya.feed.exception.FeedExceptionType; +import com.team.buddyya.feed.repository.FeedRepository; import com.team.buddyya.match.domain.MatchRequest; import com.team.buddyya.notification.domain.ExpoToken; import com.team.buddyya.notification.domain.RequestNotification; +import com.team.buddyya.notification.dto.request.PushToAllUsersRequest; import com.team.buddyya.notification.dto.response.SaveTokenResponse; import com.team.buddyya.notification.exception.NotificationException; import com.team.buddyya.notification.exception.NotificationExceptionType; @@ -42,6 +46,7 @@ public class NotificationService { private final ExpoTokenRepository expoTokenRepository; private final FindStudentService findStudentService; + private final FeedRepository feedRepository; private final RestTemplate restTemplate; private final ObjectMapper objectMapper; @@ -217,6 +222,35 @@ private String getCommentReplyNotificationTitle(boolean isKorean) { return isKorean ? FEED_REPLY_TITLE_KR : FEED_REPLY_TITLE_EN; } + public void sendNotificationToAllUser(PushToAllUsersRequest request) { + Feed feed = feedRepository.findById(request.feedId()) + .orElseThrow(() -> new FeedException(FeedExceptionType.FEED_NOT_FOUND)); + String title = request.title(); + String body = request.body(); + Map data = Map.of( + "feedId", feed.getId(), + "type", "FEED" + ); + List tokens = expoTokenRepository.findAll(); + for (ExpoToken expoToken : tokens) { + try { + String token = expoToken.getToken(); + if (token == null || token.isBlank()) continue; + RequestNotification notification = RequestNotification.builder() + .to(token) + .title(title) + .body(body) + .data(data) + .build(); + sendToExpo(notification); + } catch (NotificationException e) { + log.warn("전체 유저 알림 전송 실패 - studentId: {}, error: {}", + expoToken.getStudent().getId(), + e.exceptionType().errorMessage()); + } + } + } + public void sendCommentNotification(Long writerId, Feed feed, String commentContent) { boolean isFeedOwner = feed.isFeedOwner(writerId); if (!isFeedOwner) { diff --git a/src/main/java/com/team/buddyya/point/domain/PointType.java b/src/main/java/com/team/buddyya/point/domain/PointType.java index dd0daf39..d52aa47e 100644 --- a/src/main/java/com/team/buddyya/point/domain/PointType.java +++ b/src/main/java/com/team/buddyya/point/domain/PointType.java @@ -22,6 +22,7 @@ public enum PointType { MISSION_CERTIFICATION_REWARD("mission_certification_reward", 100, PointChangeType.MISSION), MISSION_VISIT_REWARD("mission_visit_reward", 10, PointChangeType.MISSION), FESTIVAL_REWARD("festival_reward", 50, PointChangeType.EARN), + COUPON_EVENT_REWARD("coupon_event_reward", 100, PointChangeType.EARN), EVENT_REWARD("event_reward", 10, PointChangeType.EARN); private final String displayName; diff --git a/src/main/java/com/team/buddyya/student/dto/response/UserResponse.java b/src/main/java/com/team/buddyya/student/dto/response/UserResponse.java index bbd34046..b09e1851 100644 --- a/src/main/java/com/team/buddyya/student/dto/response/UserResponse.java +++ b/src/main/java/com/team/buddyya/student/dto/response/UserResponse.java @@ -139,7 +139,8 @@ public static UserResponse fromOnboard(Student student, Boolean isStudentIdCardR } public static UserResponse fromCheckMembership(Student student, Boolean isStudentIdCardRequested, String status, - String accessToken, String refreshToken, Point point) { + String accessToken, String refreshToken, Point point, + MatchingProfile matchingProfile) { return new UserResponse( student.getId(), student.getRole().name(), @@ -163,9 +164,9 @@ public static UserResponse fromCheckMembership(Student student, Boolean isStuden null, accessToken, refreshToken, - null, - null, - null, + matchingProfile.getIntroduction(), + matchingProfile.getBuddyActivity(), + matchingProfile.isCompleted(), student.getUniversity().getIsMatchingActive(), student.getUniversity().getIsFeedActive() ); diff --git a/src/main/resources/db/migration/V31__Create_coupon_table.sql b/src/main/resources/db/migration/V31__Create_coupon_table.sql new file mode 100644 index 00000000..524ccf3d --- /dev/null +++ b/src/main/resources/db/migration/V31__Create_coupon_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE coupon +( + id BIGINT AUTO_INCREMENT NOT NULL, + code VARCHAR(10) NOT NULL, + used BIT(1) NOT NULL, + CONSTRAINT pk_coupon PRIMARY KEY (id) +); + +ALTER TABLE coupon + ADD CONSTRAINT uc_coupon_code UNIQUE (code); \ No newline at end of file