Skip to content

Commit

Permalink
feat: 미션 삭제 API 구현 (#31)
Browse files Browse the repository at this point in the history
* refactor: auth 필터 로직 리팩터링

* chore: 패키지 이름 변경

* feat: 애플 로그인 토큰 3분 내 검증으로 변경

* feat: 에러 코드 추가

* feat: 트랜잭션 추가

* feat: 미션 삭제 API 구현

* chore: 컴파일 에러 수정

* test: 테스트 코드 추가

* chore: 패키지 위치 이동

* refactor: 서비스 의존 단방향으로 수정

* chore: 불필요한 코드 제거

* feat: 이벤트 처리 로직 고도화

* test: 테스트 코드 추가

* chore: 코드 보완

* fix: 충돌 해결
  • Loading branch information
songyi00 authored Aug 5, 2024
1 parent 92aaf4f commit 4af2366
Show file tree
Hide file tree
Showing 49 changed files with 344 additions and 136 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ FROM eclipse-temurin:21

ARG JAR_FILE=build/libs/goalpanzi-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} goalpanzi-0.0.1-SNAPSHOT.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","-Duser.timezone=KST","/goalpanzi-0.0.1-SNAPSHOT.jar"]
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","-Duser.timezone=Asia/Seoul","/goalpanzi-0.0.1-SNAPSHOT.jar"]
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse;
import com.nexters.goalpanzi.application.auth.dto.response.TokenResponse;
import com.nexters.goalpanzi.application.auth.google.GoogleIdentityToken;
import com.nexters.goalpanzi.common.jwt.Jwt;
import com.nexters.goalpanzi.common.jwt.JwtProvider;
import com.nexters.goalpanzi.common.auth.jwt.Jwt;
import com.nexters.goalpanzi.common.auth.jwt.JwtProvider;
import com.nexters.goalpanzi.domain.auth.repository.RefreshTokenRepository;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.SocialType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.nexters.goalpanzi.application.auth.apple;

import com.nexters.goalpanzi.common.util.Nonce;
import com.nexters.goalpanzi.common.auth.Nonce;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.nexters.goalpanzi.application.member;

import com.nexters.goalpanzi.application.member.dto.request.UpdateProfileCommand;
import com.nexters.goalpanzi.application.mission.handler.DeleteMemberEvent;
import com.nexters.goalpanzi.application.member.event.DeleteMemberEvent;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import com.nexters.goalpanzi.exception.AlreadyExistsException;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
Expand All @@ -22,16 +21,11 @@ public class MemberService {
@Transactional
public void updateProfile(final UpdateProfileCommand request) {
validateNickname(request.nickname());
Member member = getMember(request.memberId());
Member member = memberRepository.getMember(request.memberId());

member.updateProfile(request.nickname(), request.characterType());
}

private Member getMember(final Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER, memberId));
}

private void validateNickname(final String nickname) {
memberRepository.findByNickname(nickname)
.ifPresent(member -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.nexters.goalpanzi.application.mission.handler;
package com.nexters.goalpanzi.application.member.event;

public record DeleteMemberEvent(
Long memberId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nexters.goalpanzi.application.mission;

import com.nexters.goalpanzi.application.mission.dto.response.MissionsResponse;
import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import com.nexters.goalpanzi.domain.mission.InvitationCode;
Expand All @@ -27,7 +28,7 @@ public class MissionMemberService {
private final MemberRepository memberRepository;

@Transactional
public void joinMission(final Long memberId, final String invitationCode) {
public void joinMission(final Long memberId, final InvitationCode invitationCode) {
Member member = memberRepository.getMember(memberId);
Mission mission = getMission(invitationCode);
validateAlreadyJoin(member, mission);
Expand All @@ -47,8 +48,20 @@ public MissionsResponse findAllByMemberId(final Long memberId) {
return MissionsResponse.of(member, missionMembers);
}

private Mission getMission(final String invitationCode) {
return missionRepository.findByInvitationCode(new InvitationCode(invitationCode))
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MISSION, invitationCode));
@Transactional
public void deleteAllByMemberId(final Long memberId) {
missionMemberRepository.findAllByMemberId(memberId)
.forEach(BaseEntity::delete);
}

@Transactional
public void deleteAllByMissionId(final Long missionId) {
missionMemberRepository.findAllByMissionId(missionId)
.forEach(BaseEntity::delete);
}

private Mission getMission(final InvitationCode invitationCode) {
return missionRepository.findByInvitationCode(invitationCode)
.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MISSION, invitationCode.getCode()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,52 @@

import com.nexters.goalpanzi.application.mission.dto.request.CreateMissionCommand;
import com.nexters.goalpanzi.application.mission.dto.response.MissionDetailResponse;
import com.nexters.goalpanzi.application.mission.event.DeleteMissionEvent;
import com.nexters.goalpanzi.application.mission.event.JoinMissionEvent;
import com.nexters.goalpanzi.domain.mission.InvitationCode;
import com.nexters.goalpanzi.domain.mission.Mission;
import com.nexters.goalpanzi.domain.mission.repository.MissionRepository;
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.ForbiddenException;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

private final MissionRepository missionRepository;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public MissionDetailResponse createMission(final CreateMissionCommand command) {
Mission mission = Mission.create(
command.hostMemberId(),
command.description(),
command.missionStartDate(),
command.missionEndDate(),
command.timeOfDay(),
command.missionDays(),
command.boardCount(),
generateInvitationCode()
Mission mission = missionRepository.save(
Mission.create(
command.hostMemberId(),
command.description(),
command.missionStartDate(),
command.missionEndDate(),
command.timeOfDay(),
command.missionDays(),
command.boardCount(),
generateInvitationCode()
)
);
eventPublisher.publishEvent(
new JoinMissionEvent(mission.getHostMemberId(), mission.getInvitationCode().getCode()));

return MissionDetailResponse.from(missionRepository.save(mission));
return MissionDetailResponse.from(mission);
}

private InvitationCode generateInvitationCode() {
InvitationCode invitationCode;
do {
invitationCode = InvitationCode.generate();
} while (alreadyExistInvitationCode(invitationCode));

return invitationCode;
}

Expand All @@ -46,4 +60,18 @@ public MissionDetailResponse getMission(final Long missionId) {

return MissionDetailResponse.from(mission);
}

@Transactional
public void deleteMission(final Long memberId, final Long missionId) {
Mission mission = missionRepository.getMission(missionId);
validateAuthority(memberId, mission);
mission.delete();
eventPublisher.publishEvent(new DeleteMissionEvent(mission.getId()));
}

private void validateAuthority(final Long memberId, final Mission mission) {
if (!mission.getHostMemberId().equals(memberId)) {
throw new ForbiddenException(ErrorCode.CANNOT_DELETE_MISSION);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.nexters.goalpanzi.application.mission.dto.request.MyMissionVerificationQuery;
import com.nexters.goalpanzi.application.mission.dto.response.MissionVerificationResponse;
import com.nexters.goalpanzi.application.ncp.ObjectStorageClient;
import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import com.nexters.goalpanzi.domain.mission.Mission;
Expand Down Expand Up @@ -95,4 +96,16 @@ private boolean isCompletedMission(final Mission mission, final MissionMember mi
private boolean isDuplicatedVerification(final Long memberId, final Long missionId, final LocalDate today) {
return missionVerificationRepository.findByMemberIdAndMissionIdAndDate(memberId, missionId, today).isPresent();
}

@Transactional
public void deleteAllByMemberId(final Long memberId) {
missionVerificationRepository.findAllByMemberId(memberId)
.forEach(BaseEntity::delete);
}

@Transactional
public void deleteAllByMissionId(final Long missionId) {
missionVerificationRepository.findAllByMissionId(missionId)
.forEach(BaseEntity::delete);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.nexters.goalpanzi.application.mission.event;

public record DeleteMissionEvent(
Long missionId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.nexters.goalpanzi.application.mission.event;

public record JoinMissionEvent(
Long memberId,
String invitationCode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.nexters.goalpanzi.application.mission.event.handler;

import com.nexters.goalpanzi.application.member.event.DeleteMemberEvent;
import com.nexters.goalpanzi.application.mission.MissionMemberService;
import com.nexters.goalpanzi.application.mission.MissionVerificationService;
import com.nexters.goalpanzi.application.mission.event.DeleteMissionEvent;
import com.nexters.goalpanzi.application.mission.event.JoinMissionEvent;
import com.nexters.goalpanzi.domain.mission.InvitationCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@Component
@RequiredArgsConstructor
public class MissionMemberEventHandler {
private final MissionMemberService missionMemberService;
private final MissionVerificationService missionVerificationService;

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void handleJoinMissionEvent(final JoinMissionEvent event) {
missionMemberService.joinMission(event.memberId(), new InvitationCode(event.invitationCode()));
log.info("Handled JoinMissionEvent for memberId: {}", event.memberId());
}

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void handleDeleteMemberEvent(final DeleteMemberEvent event) {
missionMemberService.deleteAllByMemberId(event.memberId());
missionVerificationService.deleteAllByMemberId(event.memberId());
log.info("Handled DeleteMemberEvent for memberId: {}", event.memberId());
}

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void handleDeleteMissionEvent(final DeleteMissionEvent event) {
missionMemberService.deleteAllByMissionId(event.missionId());
missionVerificationService.deleteAllByMissionId(event.missionId());
log.info("Handled DeleteMissionEvent for missionId: {}", event.missionId());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.nexters.goalpanzi.common.argumentresolver;

import com.nexters.goalpanzi.common.jwt.JwtParser;
import com.nexters.goalpanzi.common.jwt.JwtProvider;
import com.nexters.goalpanzi.common.auth.jwt.JwtParser;
import com.nexters.goalpanzi.common.auth.jwt.JwtProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.nexters.goalpanzi.common.util;
package com.nexters.goalpanzi.common.auth;

import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.UnauthorizedException;
Expand Down Expand Up @@ -36,7 +36,7 @@ public static boolean isValid(String target) {

private static boolean isIssuedInThreeMinutes(long nonceTime) {
var currentTime = new Date().getTime();
var threeMinutesInMillis = 30 * 60 * 1000; // 30분 = 30 * 60 * 1000 밀리초
var threeMinutesInMillis = 3 * 60 * 1000; // 3분 = 5 * 60 * 1000 밀리초
return currentTime - nonceTime <= threeMinutesInMillis;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.nexters.goalpanzi.common.jwt;
package com.nexters.goalpanzi.common.auth.jwt;

import com.sun.istack.NotNull;
import jakarta.validation.constraints.NotEmpty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.nexters.goalpanzi.common.jwt;
package com.nexters.goalpanzi.common.auth.jwt;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.nexters.goalpanzi.common.jwt;
package com.nexters.goalpanzi.common.auth.jwt;

import com.nexters.goalpanzi.exception.BaseException;
import com.nexters.goalpanzi.exception.ErrorCode;
Expand Down Expand Up @@ -31,7 +31,7 @@ public JwtProvider(
this.refreshExpiresIn = refreshExpiresIn;
}

public Jwt generateTokens(String subject) {
public com.nexters.goalpanzi.common.auth.jwt.Jwt generateTokens(String subject) {
return Jwt.builder()
.accessToken(createToken(subject, TokenType.ACCESS))
.refreshToken(createToken(subject, TokenType.REFRESH))
Expand Down
Loading

0 comments on commit 4af2366

Please sign in to comment.