Skip to content

Commit

Permalink
feat: 회원 탈퇴 구현 (#28)
Browse files Browse the repository at this point in the history
* feat: 회원 탈퇴 API 구현

* chore: 코드 수정

* test: 테스트코드 추가

* chore: swagger 주석 추가

* chore: 요청 필드명 변경

* chore: 주석 추가
  • Loading branch information
songyi00 authored Aug 3, 2024
1 parent 0332a09 commit 944c7f0
Show file tree
Hide file tree
Showing 16 changed files with 150 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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.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;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -15,6 +17,7 @@
public class MemberService {

private final MemberRepository memberRepository;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public void updateProfile(final UpdateProfileCommand request) {
Expand All @@ -35,4 +38,10 @@ private void validateNickname(final String nickname) {
throw new AlreadyExistsException(ErrorCode.ALREADY_EXIST_NICKNAME, nickname);
});
}

@Transactional
public void deleteMember(final Long memberId) {
eventPublisher.publishEvent(new DeleteMemberEvent(memberId));
memberRepository.getMember(memberId).delete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,29 @@
import com.nexters.goalpanzi.domain.mission.DayOfWeek;
import com.nexters.goalpanzi.domain.mission.Mission;
import com.nexters.goalpanzi.domain.mission.TimeOfDay;
import io.swagger.v3.oas.annotations.media.Schema;

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

public record MissionDetailResponse(
@Schema(description = "미션 ID", requiredMode = Schema.RequiredMode.REQUIRED)
Long missionId,
@Schema(description = "회원(방장) ID", requiredMode = Schema.RequiredMode.REQUIRED)
Long hostMemberId,
@Schema(description = "목표 행동", requiredMode = Schema.RequiredMode.REQUIRED)
String description,
@Schema(description = "미션 시작 날짜", requiredMode = Schema.RequiredMode.REQUIRED)
LocalDateTime missionStartDate,
@Schema(description = "미션 종료 날짜", requiredMode = Schema.RequiredMode.REQUIRED)
LocalDateTime missionEndDate,
@Schema(description = "인증 업로드 시간", requiredMode = Schema.RequiredMode.REQUIRED)
TimeOfDay timeOfDay,
@Schema(description = "경쟁 빈도", requiredMode = Schema.RequiredMode.REQUIRED)
List<DayOfWeek> missionDays,
@Schema(description = "보드칸 개수", requiredMode = Schema.RequiredMode.REQUIRED)
Integer boardCount,
@Schema(description = "초대 코드", requiredMode = Schema.RequiredMode.REQUIRED)
String invitationCode
) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.nexters.goalpanzi.application.mission.handler;

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

import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionVerificationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
public class MissionMemberEventHandler {

private final MissionMemberRepository missionMemberRepository;
private final MissionVerificationRepository missionVerificationRepository;

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
void handelDeleteMemberEvent(final DeleteMemberEvent event) {
missionMemberRepository.findAllByMemberId(event.memberId())
.forEach(BaseEntity::delete);
missionVerificationRepository.findByMemberId(event.memberId())
.forEach(BaseEntity::delete);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public class BaseEntity {

@Column(name = "deleted_at")
private LocalDateTime deletedAt;

public void delete() {
this.deletedAt = LocalDateTime.now();
}
}
11 changes: 10 additions & 1 deletion src/main/java/com/nexters/goalpanzi/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package com.nexters.goalpanzi.domain.member;

import com.nexters.goalpanzi.domain.common.BaseEntity;
import jakarta.persistence.*;
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.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;

@Entity
@SQLRestriction("deleted_at is NULL")
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;

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

@Entity
@SQLRestriction("deleted_at is NULL")
@Table(name = "mission")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@

import com.nexters.goalpanzi.domain.common.BaseEntity;
import com.nexters.goalpanzi.domain.member.Member;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;

@Entity
@SQLRestriction("deleted_at is NULL")
@Table(name = "mission_member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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;
Expand All @@ -28,4 +29,14 @@ public ResponseEntity<Void> updateProfile(

return ResponseEntity.ok().build();
}

@Override
@DeleteMapping
public ResponseEntity<Void> deleteMember(
@LoginMemberId final Long memberId
) {
memberService.deleteMember(memberId);

return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "회원")
public interface MemberControllerDocs {

@Operation(summary = "프로필 생성", description = "캐릭터, 닉네임을 설정합니다.")
@PatchMapping("/profile")
ResponseEntity<Void> updateProfile(
@Parameter(in = ParameterIn.HEADER, hidden = true) @LoginMemberId final Long userId,
@RequestBody @Valid final UpdateProfileRequest request
);

@Operation(summary = "회원 탈퇴")
ResponseEntity<Void> deleteMember(
@Parameter(in = ParameterIn.HEADER, hidden = true) @LoginMemberId final Long memberId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public ResponseEntity<Void> joinMission(
@LoginMemberId final Long memberId,
@RequestBody final JoinMissionRequest request
) {
missionMemberService.joinMission(memberId, request.missionCode());
missionMemberService.joinMission(memberId, request.invitationCode());

return ResponseEntity.ok().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,30 @@
import com.nexters.goalpanzi.domain.mission.DayOfWeek;
import com.nexters.goalpanzi.domain.mission.TimeOfDay;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

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

public record CreateMissionRequest(
@NotNull
@Schema(description = "목표 행동", requiredMode = Schema.RequiredMode.REQUIRED)
String description,

@NotNull
@Schema(description = "미션 시작 시간", requiredMode = Schema.RequiredMode.REQUIRED)
LocalDateTime missionStartDate,

@NotNull
@Schema(description = "미션 종료 시간", requiredMode = Schema.RequiredMode.REQUIRED)
LocalDateTime missionEndDate,

@NotNull
@Schema(description = "인증 업로드 시간", requiredMode = Schema.RequiredMode.REQUIRED)
TimeOfDay timeOfDay,

@NotEmpty
@Schema(description = "경쟁 빈도", requiredMode = Schema.RequiredMode.REQUIRED)
List<DayOfWeek> missionDays,

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.nexters.goalpanzi.presentation.mission.dto;

public record JoinMissionRequest(
String missionCode
String invitationCode
) {
}
1 change: 1 addition & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ spring:
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: validate
show-sql: true
data:
redis:
host: localhost
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.nexters.goalpanzi.acceptance;

import com.nexters.goalpanzi.application.auth.dto.request.GoogleLoginCommand;
import com.nexters.goalpanzi.application.auth.dto.response.LoginResponse;
import com.nexters.goalpanzi.application.mission.dto.response.MissionDetailResponse;
import com.nexters.goalpanzi.common.jwt.JwtProvider;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import static com.nexters.goalpanzi.acceptance.AcceptanceStep.*;
import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL;
import static com.nexters.goalpanzi.fixture.MemberFixture.ID_TOKEN;
import static com.nexters.goalpanzi.fixture.TokenFixture.BEARER;
import static org.assertj.core.api.Assertions.assertThat;

public class MemberAcceptanceTest extends AcceptanceTest {

@Autowired
private MemberRepository memberRepository;

@Test
void 회원이_탈퇴한다() {
LoginResponse login = 구글_로그인(new GoogleLoginCommand(ID_TOKEN, EMAIL)).as(LoginResponse.class);
MissionDetailResponse mission = 미션_생성(login.accessToken()).as(MissionDetailResponse.class);
미션_참여(mission.invitationCode(), login.accessToken());

RestAssured.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, BEARER + login.accessToken())
.when().delete("/api/member")
.then().log().all()
.statusCode(HttpStatus.NO_CONTENT.value());

assertThat(memberRepository.findAll()).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class MissionTest {
LocalDateTime.now().plusDays(7),
TimeOfDay.EVERYDAY,
List.of(DayOfWeek.FRIDAY),
BOARD_COUNT
BOARD_COUNT,
InvitationCode.generate()
);

assertAll(
Expand All @@ -42,7 +43,8 @@ class MissionTest {
LocalDateTime.now().plusDays(7),
TimeOfDay.EVERYDAY,
List.of(DayOfWeek.FRIDAY),
0
0,
InvitationCode.generate()
));
}

Expand All @@ -55,7 +57,8 @@ class MissionTest {
LocalDateTime.now().minusDays(7),
TimeOfDay.EVERYDAY,
List.of(DayOfWeek.FRIDAY),
BOARD_COUNT
BOARD_COUNT,
InvitationCode.generate()
));
}
}

0 comments on commit 944c7f0

Please sign in to comment.