diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java index 2d692d3..6e72b44 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java @@ -24,7 +24,7 @@ public class DateCourseLogQueryController { private final DateCourseLogQueryService dateCourseLogQueryService; - @Operation(summary = "최근 1개월 WithTime 사용자의 데이트 평균 횟수와 나의 데이트 횟수 조회 API by 피우", description = "메인 페이지의 데이트 나침반에 해당하는 API입니다. 최근 1개월 WithTime 사용자의 데이트 평균 횟수와 나의 데이트 횟수 조회하는 API입니다.") + @Operation(summary = "최근 1개월 WithTime 사용자의 데이트 평균 횟수와 나의 데이트 횟수 조회 API by 피우", description = "메인 페이지의 데이트 나침반에 해당하는 API입니다. 최근 1개월 WithTime 사용자의 데이트 평균 횟수와 나의 데이트 횟수 조회하는 API입니다. 비회원은 나의 데이트 횟수가 0회로 표시됩니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다.") }) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/annotation/GetPoint.java b/src/main/java/org/withtime/be/withtimebe/domain/member/annotation/GetPoint.java new file mode 100644 index 0000000..0429dff --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/annotation/GetPoint.java @@ -0,0 +1,16 @@ +package org.withtime.be.withtimebe.domain.member.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction; + +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface GetPoint { + PointAction action(); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/annotation/enums/PointAction.java b/src/main/java/org/withtime/be/withtimebe/domain/member/annotation/enums/PointAction.java new file mode 100644 index 0000000..0f51320 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/annotation/enums/PointAction.java @@ -0,0 +1,22 @@ +package org.withtime.be.withtimebe.domain.member.annotation.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PointAction { + + KEYWORD_SEARCH("키워드 검색", 3), + VIEW_DATE_COURSE("데이트 코스 조회", 5), + SAVE_DATE_COURSE("데이트 코스 저장", 10), + CREATE_DATE_COURSE("데이트 코스 생성", 30), + WRITE_REVIEW("리뷰 작성", 20), + COMPLETE_TEST("취향 테스트 완료", 20), + INPUT_PROFILE("프로필 입력", 10), + // COMPLETE_MISSION("미션 완료", 0), + ; + + private final String label; + private final Integer point; +} \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/aop/GetPointAspect.java b/src/main/java/org/withtime/be/withtimebe/domain/member/aop/GetPointAspect.java new file mode 100644 index 0000000..99071d9 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/aop/GetPointAspect.java @@ -0,0 +1,44 @@ +package org.withtime.be.withtimebe.domain.member.aop; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.withtime.be.withtimebe.domain.member.annotation.GetPoint; +import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction; +import org.withtime.be.withtimebe.domain.member.service.command.MemberCommandService; +import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; + +import lombok.RequiredArgsConstructor; + +@Aspect +@Component +@RequiredArgsConstructor +public class GetPointAspect { + + private final MemberCommandService memberCommandService; + + @AfterReturning("@annotation(getPoint)") + public void addPoint(GetPoint getPoint) { + + // 행동 및 포인트 추출 + PointAction action = getPoint.action(); + Integer point = action.getPoint(); + + // 인증 객체 추출 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(authentication == null || !authentication.isAuthenticated()) { + return; + } + + // 포인트 적립 + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + if(userDetails instanceof CustomUserDetails customUserDetails) { + Long memberId = customUserDetails.getMember().getId(); + memberCommandService.addPoint(memberId, point); + } + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/GradeController.java b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/GradeController.java new file mode 100644 index 0000000..0e328ac --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/GradeController.java @@ -0,0 +1,44 @@ +package org.withtime.be.withtimebe.domain.member.controller; + +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.member.converter.MemberConverter; +import org.withtime.be.withtimebe.domain.member.dto.GradeResponseDTO; +import org.withtime.be.withtimebe.domain.member.dto.MemberResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.service.query.GradeQueryService; +import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members/grade") +@Tag(name = "사용자 등급 관련 API") +public class GradeController { + + private final GradeQueryService gradeQueryService; + + @Operation(summary = "나의 등급 조회 API by 피우", description = "나의 등급을 조회하는 API 입니다. 로그인한 사용자만 조회 가능합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "등급 반환 성공"), + @ApiResponse( + responseCode = "404", + description = """ + 다음과 같은 이유로 실패할 수 있습니다: + - GRADE404_1: 등급을 찾지 못했습니다. + """ + ) + }) + @GetMapping + public DefaultResponse findMyGrade(@AuthenticatedMember Member member) { + GradeResponseDTO.FindMyGrade response = gradeQueryService.findMyGrade(member); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/MemberController.java b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/MemberController.java index 96feeec..f357664 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/MemberController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/MemberController.java @@ -6,12 +6,18 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.namul.api.payload.response.DefaultResponse; -import org.springframework.web.bind.annotation.*; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.withtime.be.withtimebe.domain.member.converter.MemberConverter; import org.withtime.be.withtimebe.domain.member.dto.MemberRequestDTO; import org.withtime.be.withtimebe.domain.member.dto.MemberResponseDTO; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.member.service.command.MemberCommandService; +import org.withtime.be.withtimebe.domain.member.service.query.MemberQueryService; import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember; @RestController @@ -21,6 +27,7 @@ public class MemberController { private final MemberCommandService memberCommandService; + private final MemberQueryService memberQueryService; @Operation(summary = "비밀번호 변경 API", description = "현재 비밀번호가 맞으면 새로운 비밀번호로 변경") @ApiResponses({ diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/GradeConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/GradeConverter.java new file mode 100644 index 0000000..a0d9703 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/GradeConverter.java @@ -0,0 +1,21 @@ +package org.withtime.be.withtimebe.domain.member.converter; + +import org.withtime.be.withtimebe.domain.member.dto.GradeResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Grade; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +public class GradeConverter { + + public static GradeResponseDTO.FindMyGrade toFindMyGrade(Member member, Grade current, Grade next) { + + int nextRequiredPoint = (next == null) ? 0 : next.getRequiredPoint() - member.getPoint(); + + return GradeResponseDTO.FindMyGrade.builder() + .username(member.getUsername()) + .grade(current.getGradeType().name()) + .level(current.getLevel()) + .description(current.getDescription()) + .nextRequiredPoint(nextRequiredPoint) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/GradeResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/GradeResponseDTO.java new file mode 100644 index 0000000..fcc72a7 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/GradeResponseDTO.java @@ -0,0 +1,15 @@ +package org.withtime.be.withtimebe.domain.member.dto; + +import lombok.Builder; + +public record GradeResponseDTO() { + + @Builder + public record FindMyGrade( + String username, + String grade, + String level, + String description, + Integer nextRequiredPoint + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberResponseDTO.java index c3225ea..0fa1321 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/MemberResponseDTO.java @@ -6,7 +6,5 @@ public record MemberResponseDTO() { @Builder public record ChangeInfo( String username - ) { - - } + ) {} } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Grade.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Grade.java new file mode 100644 index 0000000..353afa9 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Grade.java @@ -0,0 +1,45 @@ +package org.withtime.be.withtimebe.domain.member.entity; + +import org.withtime.be.withtimebe.domain.member.entity.enums.GradeType; +import org.withtime.be.withtimebe.global.common.BaseEntity; + +import jakarta.persistence.Column; +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 jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "grade") +public class Grade extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "grade_id") + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, unique = true) + private GradeType gradeType; + + @Column(nullable = false) + private String level; + + @Column(nullable = false) + private String description; + + @Column(nullable = false) + private Integer requiredPoint; +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java index a39b21d..747d264 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java @@ -76,6 +76,10 @@ public class Member extends BaseEntity { @Column(name = "role", nullable = false) private Role role; + @Column(name = "point", nullable = false) + @Builder.Default + private Integer point = 0; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "region_id") private Region region; @@ -98,6 +102,10 @@ public void updateAlarmSetting(Boolean pushAlarm, Boolean emailAlarm, Boolean sm this.smsAlarm = smsAlarm; } + public void addPoint(Integer point) { + this.point += point; + } + public void updateRegion(Region region) { this.region = region; } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/enums/GradeType.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/enums/GradeType.java new file mode 100644 index 0000000..05b64e0 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/enums/GradeType.java @@ -0,0 +1,14 @@ +package org.withtime.be.withtimebe.domain.member.entity.enums; + +public enum GradeType { + FLIRT, + EXPLORER, + SEEKER, + NAVIGATOR, + JOURNEYMAN, + TRAILBLAZER, + TIMEKEEPER, + ROMANTIC_NOMAD, + MASTER_OF_MOMENTS, + WITHTIME +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/repository/GradeRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/member/repository/GradeRepository.java new file mode 100644 index 0000000..c7cefa5 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/repository/GradeRepository.java @@ -0,0 +1,21 @@ +package org.withtime.be.withtimebe.domain.member.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.withtime.be.withtimebe.domain.member.entity.Grade; + +public interface GradeRepository extends JpaRepository { + + @Query("SELECT g FROM Grade g " + + "WHERE g.requiredPoint <= :point " + + "ORDER BY g.requiredPoint DESC") + Optional findCurrentGrade(@Param("point") int point); + + @Query("SELECT g FROM Grade g " + + "WHERE g.requiredPoint > :point " + + "ORDER BY g.requiredPoint ASC") + Optional findNextGrade(@Param("point") int point); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java index a721b3c..560484c 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java @@ -7,5 +7,6 @@ public interface MemberCommandService { void changePassword(Member member, MemberRequestDTO.ChangePassword request); void changePassword(String email, String password); Member changeInfo(Long memberId, MemberRequestDTO.ChangeInfo request); + void addPoint(Long memberId, Integer point); void deleteMember(Long memberId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java index 3f3a08f..c58ca70 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java @@ -47,6 +47,12 @@ public Member changeInfo(Long memberId, MemberRequestDTO.ChangeInfo request) { } @Override + public void addPoint(Long memberId, Integer point) { + Member member = memberRepository.findById(memberId).orElseThrow(() -> + new MemberException(MemberErrorCode.NOT_FOUND)); + member.addPoint(point); + } + public void deleteMember(Long memberId) { memberRepository.deleteById(memberId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/GradeQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/GradeQueryService.java new file mode 100644 index 0000000..c1a6b39 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/GradeQueryService.java @@ -0,0 +1,8 @@ +package org.withtime.be.withtimebe.domain.member.service.query; + +import org.withtime.be.withtimebe.domain.member.dto.GradeResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +public interface GradeQueryService { + GradeResponseDTO.FindMyGrade findMyGrade(Member member); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/GradeQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/GradeQueryServiceImpl.java new file mode 100644 index 0000000..a0e611f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/GradeQueryServiceImpl.java @@ -0,0 +1,34 @@ +package org.withtime.be.withtimebe.domain.member.service.query; + +import org.springframework.stereotype.Service; +import org.springframework.web.ErrorResponseException; +import org.withtime.be.withtimebe.domain.member.converter.GradeConverter; +import org.withtime.be.withtimebe.domain.member.dto.GradeResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Grade; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.repository.GradeRepository; +import org.withtime.be.withtimebe.global.error.code.GradeErrorCode; +import org.withtime.be.withtimebe.global.error.exception.GradeException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class GradeQueryServiceImpl implements GradeQueryService { + + private final GradeRepository gradeRepository; + + @Override + public GradeResponseDTO.FindMyGrade findMyGrade(Member member) { + + int currentPoint = member.getPoint(); + + Grade currentGrade = gradeRepository.findCurrentGrade(currentPoint) + .orElseThrow(() -> new GradeException(GradeErrorCode.GRADE_NOT_FOUND)); + + Grade nextGrade = gradeRepository.findNextGrade(currentPoint) + .orElse(null); + + return GradeConverter.toFindMyGrade(member, currentGrade, nextGrade); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/GradeErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/GradeErrorCode.java new file mode 100644 index 0000000..a0900c3 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/GradeErrorCode.java @@ -0,0 +1,27 @@ +package org.withtime.be.withtimebe.global.error.code; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.code.dto.supports.DefaultResponseErrorReasonDTO; +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum GradeErrorCode implements BaseErrorCode { + + GRADE_NOT_FOUND(HttpStatus.NOT_FOUND, "GRADE404_1", "등급을 찾지 못했습니다."), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public DefaultResponseErrorReasonDTO getReason() { + return DefaultResponseErrorReasonDTO.builder() + .httpStatus(this.httpStatus) + .code(this.code) + .message(this.message) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/exception/GradeException.java b/src/main/java/org/withtime/be/withtimebe/global/error/exception/GradeException.java new file mode 100644 index 0000000..a5a8aeb --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/exception/GradeException.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.global.error.exception; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.error.exception.ServerApplicationException; + +public class GradeException extends ServerApplicationException { + public GradeException(BaseErrorCode code) { + super(code); + } +}