Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/main/java/com/meetkey/server/ServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableFeignClients
@EnableJpaAuditing
@EnableScheduling
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.meetkey.server.domain.auth.entity.RefreshToken;
import com.meetkey.server.domain.auth.repository.RefreshTokenRepository;
import com.meetkey.server.domain.badge.entity.Badge;
import com.meetkey.server.domain.badge.enums.BadgeLevel;
import com.meetkey.server.domain.badge.service.BadgeService;
import com.meetkey.server.domain.member.dto.MemberReqDTO;
import com.meetkey.server.domain.member.entity.Member;
import com.meetkey.server.domain.member.entity.SocialLogin;
Expand Down Expand Up @@ -41,6 +44,7 @@ public class AuthService {
private final OauthOidcHelper oAuthOIDCHelper;
private final JwtUtil jwtUtil;
private final MemberService memberService;
private final BadgeService badgeService;

private final SocialLoginRepository socialLoginRepository;
private final RefreshTokenRepository refreshTokenRepository;
Expand Down Expand Up @@ -74,6 +78,13 @@ else if (provider == Provider.APPLE) {
// Member 생성
Member member = memberService.signup(provider, providerId, memberReqDTO);

// 회원 가입시 자동적으로 뱃지 생성
Badge initalBadge = Badge.builder()
.member(member)
.total_score(0)
.level(BadgeLevel.NONE)
.build();

// 밋키 서비스 토큰 발급
return getJwtResponseDTO(member);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class Badge extends BaseEntity {
private Long id;

@JoinColumn(name = "member_id", nullable = false)
@OneToOne
@OneToOne(fetch = FetchType.LAZY)
private Member member;

@Builder.Default
Expand All @@ -28,4 +28,9 @@ public class Badge extends BaseEntity {
@Enumerated(EnumType.STRING)
private BadgeLevel level;

public void addScore(int score) {
this.total_score += score;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class PointHistory extends BaseEntity {
@Column(nullable = false)
private int changeAmount;

@Column(nullable = false)
@Column(nullable = false, length = 30)
@Enumerated(EnumType.STRING)
private ReasonType reasonType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ public enum ReasonType {
AUTH("본인 인증", 20),
PROFILE("상세 프로필 작성 완료", 15),
ACTIVE("활동적인 멤버", 25),
POSITIVE("긍정적인 평가", 15);
POSITIVE("긍정적인 평가", 15),
MISSION_SUCCESS("미션 완료", 3),
MISSION_FAILURE("미션 실패", -5);


private final String description;
private final int defaultScore;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.meetkey.server.domain.badge.respository;

import com.meetkey.server.domain.badge.entity.Badge;
import com.meetkey.server.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface BadgeRepository extends JpaRepository<Badge, Long> {

Optional<Badge> findByMember(Member member);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.meetkey.server.domain.badge.service;

import com.meetkey.server.domain.badge.converter.BadgeConverter;
import com.meetkey.server.domain.badge.entity.Badge;
import com.meetkey.server.domain.badge.entity.PointHistory;
import com.meetkey.server.domain.badge.enums.ReasonType;
import com.meetkey.server.domain.badge.respository.BadgeRepository;
import com.meetkey.server.domain.badge.respository.PointHistoryRepository;
import com.meetkey.server.domain.member.entity.Member;
import com.meetkey.server.domain.member.exception.MemberErrorStatus;
Expand All @@ -23,6 +25,7 @@ public class BadgeService {

private final MemberRepository memberRepository;
private final PointHistoryRepository pointHistoryRepository;
private final BadgeRepository badgeRepository;
private final BadgeConverter badgeConverter;

// 뱃지 정보 조회 (내역 포함)
Expand All @@ -48,45 +51,42 @@ public BadgeResponse getBadgeSummary(Long memberId) {
}

// 점수 부여
public void rewardPoints(Long memberId, ReasonType reasonType) {
Member member = getMember(memberId);
public void rewardPoints(Member member, ReasonType reasonType) {
int amount = reasonType.getDefaultScore();

if (pointHistoryRepository.existsByMemberAndReasonType(member, reasonType)) {
return;
}
Badge badge = badgeRepository.findByMember(member).orElseThrow(
() -> new MemberException(MemberErrorStatus.MEMBER_NOT_FOUND));

int currentTotalScore = pointHistoryRepository.calculateTotalScore(member);
int currentScore = badge.getTotal_score();

// 100점 이상이라면 지급 X
if (currentTotalScore >= 100) {
return;
}
int potetialScore = currentScore + amount;

int pointsToAdd = reasonType.getDefaultScore();
int potentialScore = currentTotalScore + pointsToAdd;
int potentialScore = currentScore + amount;

// 더해서 100점 초과라면 일부만 지급
if (potentialScore > 100) {
pointsToAdd = 100 - currentTotalScore;
amount = 100 - currentScore; // 100점까지만 채움
} else if (potentialScore < 0) {
amount = -currentScore;
}

if (pointsToAdd > 0) {
PointHistory pointHistory = PointHistory.builder()
.member(member)
.reasonType(reasonType)
.changeAmount(reasonType.getDefaultScore())
.build();
pointHistoryRepository.save(pointHistory);
}
if (amount == 0) return; // 변동 없으면 종료

badge.addScore(amount); // Badge 엔티티 업데이트

PointHistory pointHistory = PointHistory.builder()
.member(member)
.reasonType(reasonType)
.changeAmount(amount)
.build();
pointHistoryRepository.save(pointHistory);
}

// 본인 인증
public void checkAuthentication(Long memberId) {
Member member = getMember(memberId);

if (Boolean.TRUE.equals(member.isVerified())) {
rewardPoints(member.getId(), ReasonType.AUTH);
rewardPoints(member, ReasonType.AUTH);
}
}

Expand All @@ -101,7 +101,7 @@ public void checkProfileCompletion(Long memberId) {
member.getTargetLanguageLevel() != null;

if (isComplete) {
rewardPoints(member.getId(), ReasonType.PROFILE);
rewardPoints(member, ReasonType.PROFILE);
}
}

Expand All @@ -110,10 +110,15 @@ public void checkPositiveEvaluation(Long memberId) {
Member member = getMember(memberId);

if (member.getRecommendCount() >= 10) {
rewardPoints(member.getId(), ReasonType.POSITIVE);
rewardPoints(member, ReasonType.POSITIVE);
}
}

// 미션용 지급
public void rewardRepeatable(Long memberId, ReasonType reasonType) {
Member member = getMember(memberId);
rewardPoints(member, reasonType);
}

// 사용자 찾기 메소드
private Member getMember(Long memberId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import com.meetkey.server.domain.chat.entity.ChatMessage;
import com.meetkey.server.domain.chat.entity.ChatRoom;
import com.meetkey.server.domain.chat.entity.enums.MessageType;
import com.meetkey.server.domain.member.entity.Member;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;

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

Expand All @@ -24,4 +27,12 @@ Slice<ChatMessage> findByChatRoomAndIdLessThanOrderByIdAsc(
long countByChatRoom(ChatRoom chatRoom);

long countByChatRoomAndIdGreaterThan(ChatRoom chatRoom, Long messageId);

// 해당 채팅방에서 특정 시간 이후에 특정 타입의 메시지를 보낸적 있는지 판별
boolean existsByChatRoomAndMemberAndCreatedAtAfterAndMessageType(
ChatRoom chatRoom,
Member sender,
LocalDateTime timestamp,
MessageType messageType
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class Member extends BaseEntity {
@Column(nullable = false)
@Builder.Default
private Integer notRecommendCount = 0;

/*
* 관심사
*/
Expand Down Expand Up @@ -133,4 +134,5 @@ public void updateCertificated() {
this.isVerified = true;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.meetkey.server.domain.mission.controller;

import com.meetkey.server.domain.mission.service.MissionService;
import com.meetkey.server.global.apiPayload.response.BasicResponse;
import com.meetkey.server.global.apiPayload.status.CommonSuccessStatus;
import com.meetkey.server.global.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
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;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import static com.meetkey.server.domain.mission.dto.MissionResDTO.*;

@Tag(name = "Mission", description = "채팅방 미션 관련 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/chat-rooms/{chatRoomId}/missions")
public class MissionController {

private final MissionService missionService;

@Operation(summary = "오늘의 미션 조회", description = "해당 채팅방의 현재 진행 중인 미션을 조회합니다. (없으면 미션 자동 생성)")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = Info.class))),
@ApiResponse(responseCode = "400", description = "CHAT2041 : 존재하지 않는 채팅방입니다. , MEMBER4041 : 존재하지 않는 사용자입니다." +
"MISSION4041 : 존재하지 않는 미션입니다.")
})
@GetMapping("/today")
public BasicResponse<Info> getTodayMission(
@Parameter(description = "채팅방 ID", required = true)
@PathVariable Long chatRoomId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
Info response = missionService.getTodayMission(chatRoomId, userDetails.getMemberId());
return BasicResponse.success(CommonSuccessStatus._OK, response);

}

@Operation(summary = "미션 완료 인증", description = "사용자가 채팅을 보내면 호출 -> 내부 로직으로 미션 성공 여부를 판단" +
"미션 성공 로직 (내용은 신경쓰지 않음)" +
"1. 미션 타입이 PHOTO인 경우는 사진을 하나 이상 보내야한다. " +
"2. PHOTO가 아닌 다른 타입의 미션들은 텍스트 채팅을 한 번 이상 남겨야 한다." +
"3. 미션 받은 이후에 해당 내용을 수행해야함.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = Completion.class))),
@ApiResponse(responseCode = "400", description = "MEMBER4041 : 존재하지 않는 사용자입니다., MISSION4041 : 존재하지않는 미션입니다., MISSION4001 : 이미 완료된 미션입니다., " +
"MISSION4002 : 아직 미션을 수행하지 않았습니다., MISSION4042 : 참여정보를 찾을 수 없습니다.")
})
@PostMapping("/{missionId}/complete")
public BasicResponse<Completion> completeMission(
@Parameter(description = "채팅방 ID", required = true)
@PathVariable Long chatRoomId,

@Parameter(description = "오늘의 미션 조회 시 반환된 'missionId' 값 (ChatRoomMission ID). ※ 주의: 미션 내용 고유 번호(Mission ID)가 아닙니다.", required = true, example = "105")
@PathVariable Long missionId, // ChatRoomMission의 ID (Mission ID 아님 주의)

@AuthenticationPrincipal CustomUserDetails userDetails
) {
Completion response = missionService.completeMission(chatRoomId, missionId, userDetails.getMemberId());
return BasicResponse.success(CommonSuccessStatus._OK, response);
}




}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.meetkey.server.domain.mission.controller;

import com.meetkey.server.domain.mission.service.MissionService;
import com.meetkey.server.global.apiPayload.response.BasicResponse;
import com.meetkey.server.global.apiPayload.status.CommonSuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Test - Mission", description = "[테스트용] 미션 스케줄러 강제 실행 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/test/missions")
public class MissionTestController {

private final MissionService missionService;

@Operation(summary = "만료된 미션 강제 실패 처리",
description = "스케줄러를 기다리지 않고, 현재 시간 기준으로 만료된 미션을 즉시 실패 처리(-5점) 합니다.")
@PostMapping("/expire-now")
public BasicResponse<String> triggerExpire() {
int count = missionService.processExpiredMission();
return BasicResponse.success(CommonSuccessStatus._OK, "수동 실행 완료. 총 " + count + "건의 미션이 실패 처리되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.meetkey.server.domain.mission.converter;

import com.meetkey.server.domain.badge.entity.Badge;
import com.meetkey.server.domain.badge.enums.ReasonType;
import com.meetkey.server.domain.member.entity.Member;
import com.meetkey.server.domain.mission.entity.mapping.ChatRoomMission;
import com.meetkey.server.domain.mission.entity.mapping.MissionLog;
import org.springframework.stereotype.Component;

import static com.meetkey.server.domain.mission.dto.MissionResDTO.*;

@Component
public class MissionConverter {

// 오늘의 미션 조회 응답 변환
public Info toMissionInfo(ChatRoomMission chatRoomMission, MissionLog missionLog, long remainingSeconds) {
return Info.builder()
.missionId(chatRoomMission.getId())
.content(chatRoomMission.getMission().getContent())
.type(chatRoomMission.getMission().getMissionType())
.remainingSeconds(remainingSeconds)
.myStatus(missionLog.getMissionStatus())
.build();
}

// 미션 완료 응답 변환
public Completion toMissionCompletion(Badge badge, ReasonType reasonType) {
return Completion.builder()
.status("SUCCESS")
.gainedPoints(reasonType.getDefaultScore())
.currentScore(badge.getTotal_score())
.badgeName(badge.getLevel().name())
.build();
}


}
Loading