Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6d354cd
feature: 회원 전체 조회 로직 구현
hyeunS-P Jul 24, 2025
3cf9d68
refactor: 불필요한 메서드 삭제 및 swagger 작성
hyeunS-P Jul 24, 2025
aeacf61
fix: 회원 프로필 수정 시 닉네임을 필수로 입력하지 않아도 되게 수정
hyeunS-P Jul 24, 2025
aeec95f
feature:알람을 위한 메시지 생성 로직과 알람 생성, 전체 읽음 로직 추가
hyeunS-P Jul 24, 2025
78507a2
refactor:알람 swagger 작성
hyeunS-P Jul 24, 2025
43c79a0
[EA3-167] feature: 종료되지않은 스터디 생성 10개 제한 기능
pia01190 Jul 24, 2025
443605a
[EA3-167] refactor: sql문 수정
pia01190 Jul 24, 2025
1a2d0a0
fix: 알람 entity의 read가 예약어라서 발생한 컴파일 에러 해결
hyeunS-P Jul 24, 2025
ca11eb0
[EA3-167] refactor: StudyService 코드 추상화
pia01190 Jul 24, 2025
5147c70
[EA3-167] feature: 종료되지않은 스터디 10개 초과 예외 테스트 작성
pia01190 Jul 24, 2025
73abe3c
[EA3-167] refactor: 게시글 조회 생성일 LocalDateTime 변경
pia01190 Jul 24, 2025
3d434fc
[EA3-156] feature: 리뷰 가능한 스터디 조회 수정
Tokwasp Jul 24, 2025
69422dd
Merge pull request #211 from prgrms-web-devcourse-final-project/featu…
endorsement0912 Jul 24, 2025
e9c43ba
[EA3-156] feature: 리뷰 저장시 버디에너지 service 순환 참조 문제 해결
Tokwasp Jul 24, 2025
91b9311
[EA3-156] feature: 리뷰 대상 스터디 조회시 컨트롤러 파라미터 수정
Tokwasp Jul 24, 2025
6d992f5
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
Tokwasp Jul 24, 2025
945be6e
[EA3-134] fix : TimeVoteStat 저장 시 version null 오류 방지를 위한 flush 및 기본값 설정
endorsement0912 Jul 24, 2025
ba953c4
refactor: 도메인 타입을 STUDY로 변경
hyeunS-P Jul 24, 2025
21974e5
Merge pull request #210 from prgrms-web-devcourse-final-project/featu…
hyeunS-P Jul 24, 2025
fc0f733
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
endorsement0912 Jul 24, 2025
61c6251
[EA3-111] refactor : 그룹 채팅 로직 studyId 기반으로 전면 리팩토링
dbrkdgus00 Jul 24, 2025
bc37aee
feature:스터디 초대 및 수락/거절 로직 추가
hyeunS-P Jul 24, 2025
026d7c9
feature:스터디 초대 및 수락/거절 로직 추가
hyeunS-P Jul 24, 2025
220924c
Merge remote-tracking branch 'origin/feature/EA3-178-study-invite' in…
hyeunS-P Jul 24, 2025
4a97fa0
feature:알람 전체 읽기에서 스터디 초대는 제외하도록 수정
hyeunS-P Jul 24, 2025
79e9852
feature:알람 메시지 동적으로 구성하도록 변경
hyeunS-P Jul 25, 2025
11d93f5
fix: 회원 프로필 수정 안되던 문제 수정
hyeunS-P Jul 25, 2025
b853185
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
hyeunS-P Jul 25, 2025
b17df69
[EA3-168] feature: 스터디 신청 및 승인 시 참여 10개 제한 기능
pia01190 Jul 25, 2025
e787cde
[EA3-168] refactor: 에러코드 수정
pia01190 Jul 25, 2025
d08e1a0
[EA3-168] refactor: 에러코드 위치 변경
pia01190 Jul 25, 2025
2828deb
[EA3-168] feature: 신청 및 신청 승인 예외 테스트 작성
pia01190 Jul 25, 2025
1a2d65b
[EA3-134] refactor : 투표기간 생성시 반환 타입을 엔티티에서 레스폰스로 변경
endorsement0912 Jul 25, 2025
8caa643
[EA3-134] refactor : 투표기간 생성시 투표 마지막날 자동으로 23:59:59으로 되게 보정
endorsement0912 Jul 25, 2025
e6ae9ac
[EA3-134] Chore : data.sql 수정
endorsement0912 Jul 25, 2025
55d97ee
Merge pull request #212 from prgrms-web-devcourse-final-project/featu…
dbrkdgus00 Jul 25, 2025
1588bd7
Merge pull request #213 from prgrms-web-devcourse-final-project/featu…
dbrkdgus00 Jul 25, 2025
6b37630
Merge pull request #215 from prgrms-web-devcourse-final-project/featu…
dbrkdgus00 Jul 25, 2025
6cef8c0
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
hyeunS-P Jul 25, 2025
25f59af
Merge pull request #216 from prgrms-web-devcourse-final-project/featu…
dbrkdgus00 Jul 25, 2025
13e721b
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
hyeunS-P Jul 25, 2025
9f47294
feature: 스터디 초대 수락 시 스터디 참여 제한에 걸리면 예외 발생하도록 기능 구현
hyeunS-P Jul 25, 2025
09961aa
Merge pull request #214 from prgrms-web-devcourse-final-project/featu…
endorsement0912 Jul 25, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package grep.neogulcoder.domain.alram.controller;

import grep.neogulcoder.domain.alram.controller.dto.response.AlarmResponse;
import grep.neogulcoder.domain.alram.service.AlarmService;
import grep.neogulcoder.global.auth.Principal;
import grep.neogulcoder.global.response.ApiResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/alarm")
public class AlarmController implements AlarmSpecification {

private final AlarmService alarmService;

@GetMapping("/my")
public ApiResponse<List<AlarmResponse>> getAllAlarm(
@AuthenticationPrincipal Principal userDetails) {
return ApiResponse.success(alarmService.getAllAlarms(userDetails.getUserId()));
}

@PostMapping("/my/check/all")
public ApiResponse<Void> checkAlarm(@AuthenticationPrincipal Principal userDetails) {
alarmService.checkAllAlarm(userDetails.getUserId());
return ApiResponse.noContent();
}

@PostMapping("/choose/{alarmId}/response")
public ApiResponse<Void> respondToInvite(@AuthenticationPrincipal Principal principal,
@PathVariable Long alarmId,
boolean accepted) {
if (accepted) {
alarmService.acceptInvite(principal.getUserId(), alarmId);
} else {
alarmService.rejectInvite(principal.getUserId());
}

return ApiResponse.success(accepted ? "스터디 초대를 수락했습니다." : "스터디 초대를 거절했습니다.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package grep.neogulcoder.domain.alram.controller;

import grep.neogulcoder.domain.alram.controller.dto.response.AlarmResponse;
import grep.neogulcoder.global.auth.Principal;
import grep.neogulcoder.global.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import org.springframework.security.core.annotation.AuthenticationPrincipal;

@Tag(name = "Alarm", description = "알림 관련 API 명세")
public interface AlarmSpecification {

@Operation(summary = "내 알림 목록 조회", description = "로그인한 사용자의 알림 목록을 조회합니다.")
ApiResponse<List<AlarmResponse>> getAllAlarm(@AuthenticationPrincipal Principal userDetails);

@Operation(summary = "내 알림 전체 읽음 처리", description = "로그인한 사용자의 모든 알림을 읽음 처리합니다.")
ApiResponse<Void> checkAlarm(@AuthenticationPrincipal Principal userDetails);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package grep.neogulcoder.domain.alram.controller.dto.response;

import grep.neogulcoder.domain.alram.entity.Alarm;
import grep.neogulcoder.domain.alram.type.AlarmType;
import grep.neogulcoder.domain.alram.type.DomainType;
import lombok.Builder;
import lombok.Data;

@Data
public class AlarmResponse {

private Long id;

private Long receiverUserId;

private AlarmType alarmType;

private DomainType domainType;

private Long domainId;

private String message;

public static AlarmResponse toResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType,
Long domainId, String message) {
return AlarmResponse.builder()
.id(id)
.receiverUserId(receiverUserId)
.alarmType(alarmType)
.domainType(domainType)
.domainId(domainId)
.message(message)
.build();
}

@Builder
private AlarmResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType,
Long domainId, String message) {
this.id = id;
this.receiverUserId = receiverUserId;
this.alarmType = alarmType;
this.domainType = domainType;
this.domainId = domainId;
this.message = message;
}
}
67 changes: 67 additions & 0 deletions src/main/java/grep/neogulcoder/domain/alram/entity/Alarm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package grep.neogulcoder.domain.alram.entity;

import grep.neogulcoder.domain.alram.type.AlarmType;
import grep.neogulcoder.domain.alram.type.DomainType;
import grep.neogulcoder.global.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;

@Entity
@Getter
@Schema(description = "알림 정보")
public class Alarm extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long receiverUserId;

@Enumerated(EnumType.STRING)
private AlarmType alarmType;

@Enumerated(EnumType.STRING)
private DomainType domainType;

private Long domainId;

private String message;

private boolean checked = false;

public void checkAlarm() {
this.checked = true;
}

public static Alarm init(AlarmType alarmType, Long receiverUserId , DomainType domainType, Long domainId, String message) {
return Alarm.builder()
.alarmType(alarmType)
.receiverUserId(receiverUserId)
.domainType(domainType)
.domainId(domainId)
.message(message)
.build();
}

@Builder
private Alarm(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType,
Long domainId, String message) {
this.id = id;
this.receiverUserId = receiverUserId;
this.alarmType = alarmType;
this.domainType = domainType;
this.domainId = domainId;
this.message = message;
}

protected Alarm() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package grep.neogulcoder.domain.alram.exception.code;

import grep.neogulcoder.global.response.code.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum AlarmErrorCode implements ErrorCode {

ALARM_NOT_FOUND("A001",HttpStatus.NOT_FOUND,"알람을 찾을 수 없습니다.");


private final String code;
private final HttpStatus status;
private final String message;

AlarmErrorCode(String code, HttpStatus status, String message) {
this.code = code;
this.status = status;
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package grep.neogulcoder.domain.alram.repository;

import grep.neogulcoder.domain.alram.entity.Alarm;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AlarmRepository extends JpaRepository<Alarm, Long> {
List<Alarm> findAllByReceiverUserIdAndCheckedFalse(Long receiverUserId);

}
105 changes: 105 additions & 0 deletions src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package grep.neogulcoder.domain.alram.service;

import grep.neogulcoder.domain.alram.controller.dto.response.AlarmResponse;
import grep.neogulcoder.domain.alram.entity.Alarm;
import grep.neogulcoder.domain.alram.exception.code.AlarmErrorCode;
import grep.neogulcoder.domain.alram.repository.AlarmRepository;
import grep.neogulcoder.domain.alram.type.AlarmType;
import grep.neogulcoder.domain.alram.type.DomainType;
import grep.neogulcoder.domain.study.Study;
import grep.neogulcoder.domain.study.StudyMember;
import grep.neogulcoder.domain.study.event.StudyInviteEvent;
import grep.neogulcoder.domain.study.repository.StudyMemberQueryRepository;
import grep.neogulcoder.domain.study.repository.StudyRepository;
import grep.neogulcoder.global.exception.business.BusinessException;
import grep.neogulcoder.global.exception.business.NotFoundException;
import grep.neogulcoder.global.provider.finder.MessageFinder;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND;
import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.APPLICATION_PARTICIPANT_LIMIT_EXCEEDED;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AlarmService {

private final AlarmRepository alarmRepository;
private final MessageFinder messageFinder;
private final StudyRepository studyRepository;
private final StudyMemberQueryRepository studyMemberQueryRepository;

@Transactional
public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainType, Long domainId) {
String message = messageFinder.findMessage(alarmType, domainType, domainId);
alarmRepository.save(Alarm.init(alarmType, receiverId, domainType, domainId, message));
}

public List<AlarmResponse> getAllAlarms(Long receiverUserId) {
return alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId).stream()
.map(alarm -> AlarmResponse.toResponse(
alarm.getId(),
alarm.getReceiverUserId(),
alarm.getAlarmType(),
alarm.getDomainType(),
alarm.getDomainId(),
alarm.getMessage()))
.toList();
}

@Transactional
public void checkAllAlarm(Long receiverUserId) {
List<Alarm> alarms = alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId);
alarms.stream()
.filter(alarm -> alarm.getAlarmType() != AlarmType.INVITE)
.forEach(Alarm::checkAlarm);
}

@EventListener
public void handleStudyInviteEvent(StudyInviteEvent event) {
saveAlarm(
event.targetUserId(),
AlarmType.INVITE,
DomainType.STUDY,
event.studyId()
);
}

@Transactional
public void acceptInvite(Long targetUserId, Long alarmId) {

validateParticipantStudyLimit(targetUserId);

Alarm alarm = findValidAlarm(alarmId);
Long studyId = alarm.getDomainId();
Study study = findValidStudy(studyId);
StudyMember.createMember(study,targetUserId);
alarm.checkAlarm();
}

@Transactional
public void rejectInvite(Long alarmId) {
Alarm alarm = findValidAlarm(alarmId);
alarm.checkAlarm();
}

private Alarm findValidAlarm(Long alarmId) {
return alarmRepository.findById(alarmId).orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND));
}

private Study findValidStudy(Long studyId) {
return studyRepository.findById(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
}

private void validateParticipantStudyLimit(Long userId) {
int count = studyMemberQueryRepository.countActiveUnfinishedStudies(userId);
if (count >= 10) {
throw new BusinessException(APPLICATION_PARTICIPANT_LIMIT_EXCEEDED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package grep.neogulcoder.domain.alram.type;

public enum AlarmType {

INVITE

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package grep.neogulcoder.domain.alram.type;

public enum DomainType {
STUDY
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,11 @@ public void updateEnergy(ReviewType reviewType) {
this.level -= 1;
}
}

public BuddyEnergyReason findReasonFrom(ReviewType reviewType) {
if(reviewType.isPositive()){
return BuddyEnergyReason.POSITIVE_REVIEW;
}
return BuddyEnergyReason.NEGATIVE_REVIEW;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public class GroupChatRestController implements GroupChatRestSpecification {

// 과거 채팅 메시지 페이징 조회 (무한 스크롤용)
@Override
@GetMapping("/room/{roomId}/messages")
@GetMapping("/study/{studyId}/messages")
public ApiResponse<PageResponse<GroupChatMessageResponseDto>> getMessages(
@PathVariable("roomId") Long roomId,
@PathVariable("studyId") Long studyId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
// 서비스에서 페이징된 메시지 조회
PageResponse<GroupChatMessageResponseDto> pageResponse =
groupChatService.getMessages(roomId, page, size);
groupChatService.getMessages(studyId, page, size);

return ApiResponse.success(pageResponse);
}
Expand Down
Loading