diff --git a/.github/workflows/backend-dev.yml b/.github/workflows/backend-dev.yml index 9e56c7615..4494c494f 100644 --- a/.github/workflows/backend-dev.yml +++ b/.github/workflows/backend-dev.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: [ self-hosted, backend-dev ] defaults: run: diff --git a/.github/workflows/backend-pull-request.yml b/.github/workflows/backend-pull-request.yml index 797f88326..7e0c561f1 100644 --- a/.github/workflows/backend-pull-request.yml +++ b/.github/workflows/backend-pull-request.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: [ self-hosted, backend-dev ] defaults: run: diff --git a/server/src/main/java/server/haengdong/application/EventService.java b/server/src/main/java/server/haengdong/application/EventService.java index 9ad502606..52080ae8f 100644 --- a/server/src/main/java/server/haengdong/application/EventService.java +++ b/server/src/main/java/server/haengdong/application/EventService.java @@ -7,9 +7,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import server.haengdong.application.request.EventAppRequest; +import server.haengdong.application.request.MemberUpdateAppRequest; import server.haengdong.application.response.ActionAppResponse; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; +import server.haengdong.application.response.MembersAppResponse; import server.haengdong.domain.action.BillAction; import server.haengdong.domain.action.BillActionRepository; import server.haengdong.domain.action.MemberAction; @@ -88,4 +90,31 @@ private List getActionAppResponses( return actionAppResponses; } + + public MembersAppResponse findAllMembers(String token) { + Event event = eventRepository.findByToken(token) + .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT)); + + List memberNames = memberActionRepository.findAllUniqueMemberByEvent(event); + + return new MembersAppResponse(memberNames); + } + + @Transactional + public void updateMember(String token, String memberName, MemberUpdateAppRequest request) { + Event event = eventRepository.findByToken(token) + .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT)); + String updatedMemberName = request.name(); + validateMemberNameUnique(event, updatedMemberName); + + memberActionRepository.findAllByAction_EventAndMemberName(event, memberName) + .forEach(memberAction -> memberAction.updateMemberName(updatedMemberName)); + } + + private void validateMemberNameUnique(Event event, String updatedMemberName) { + boolean isMemberNameExist = memberActionRepository.existsByAction_EventAndMemberName(event, updatedMemberName); + if (isMemberNameExist) { + throw new HaengdongException(HaengdongErrorCode.DUPLICATED_MEMBER_NAME); + } + } } diff --git a/server/src/main/java/server/haengdong/application/request/MemberUpdateAppRequest.java b/server/src/main/java/server/haengdong/application/request/MemberUpdateAppRequest.java new file mode 100644 index 000000000..cbef9dda9 --- /dev/null +++ b/server/src/main/java/server/haengdong/application/request/MemberUpdateAppRequest.java @@ -0,0 +1,4 @@ +package server.haengdong.application.request; + +public record MemberUpdateAppRequest(String name) { +} diff --git a/server/src/main/java/server/haengdong/application/response/MembersAppResponse.java b/server/src/main/java/server/haengdong/application/response/MembersAppResponse.java new file mode 100644 index 000000000..be8378b37 --- /dev/null +++ b/server/src/main/java/server/haengdong/application/response/MembersAppResponse.java @@ -0,0 +1,8 @@ +package server.haengdong.application.response; + +import java.util.List; + +public record MembersAppResponse( + List memberNames +) { +} diff --git a/server/src/main/java/server/haengdong/domain/action/MemberAction.java b/server/src/main/java/server/haengdong/domain/action/MemberAction.java index 67387cbde..1e78820b2 100644 --- a/server/src/main/java/server/haengdong/domain/action/MemberAction.java +++ b/server/src/main/java/server/haengdong/domain/action/MemberAction.java @@ -39,8 +39,8 @@ public MemberAction(Action action, String memberName, MemberActionStatus status, this.memberGroupId = memberGroupId; } - public boolean isSameName(String name) { - return memberName.equals(name); + public void updateMemberName(String memberName) { + this.memberName = memberName; } public boolean isIn() { diff --git a/server/src/main/java/server/haengdong/domain/action/MemberActionRepository.java b/server/src/main/java/server/haengdong/domain/action/MemberActionRepository.java index ebc2ffc2b..8cf9e42cf 100644 --- a/server/src/main/java/server/haengdong/domain/action/MemberActionRepository.java +++ b/server/src/main/java/server/haengdong/domain/action/MemberActionRepository.java @@ -15,6 +15,13 @@ public interface MemberActionRepository extends JpaRepository findAllByEvent(@Param("event") Event event); + @Query(""" + select distinct m.memberName + from MemberAction m + where m.action.event = :event + """) + List findAllUniqueMemberByEvent(Event event); + @Modifying @Query(""" delete @@ -32,4 +39,8 @@ public interface MemberActionRepository extends JpaRepository= :sequence """) void deleteAllByMemberNameAndMinSequence(String memberName, Long sequence); + + List findAllByAction_EventAndMemberName(Event event, String memberName); + + boolean existsByAction_EventAndMemberName(Event event, String updatedMemberName); } diff --git a/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java b/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java index 80b2cb6ae..c9f461c5f 100644 --- a/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java +++ b/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java @@ -7,6 +7,8 @@ public enum HaengdongErrorCode { BAD_REQUEST("R_001", "잘못된 요청입니다."), NO_RESOURCE_REQUEST("R_002", "잘못된 엔드포인트입니다."), MESSAGE_NOT_READABLE("R_003", "읽을 수 없는 요청 형식입니다."), + INTERNAL_SERVER_ERROR("S_001", "서버 내부에서 에러가 발생했습니다."), + DUPLICATED_MEMBER_NAME("EV_001", "중복된 행사 참여 인원 이름이 존재합니다."), NOT_FOUND_EVENT("EV_400", "존재하지 않는 행사입니다."), DUPLICATED_MEMBER_ACTION("MA_001", "중복된 인원이 존재합니다."), INVALID_MEMBER_IN_ACTION("MA_002", "현재 참여하고 있는 인원이 존재합니다."), @@ -14,7 +16,6 @@ public enum HaengdongErrorCode { NOT_FOUND_MEMBER_ACTION("MA_400", "존재하지 않는 멤버 액션입니다."), NOT_FOUND_BILL_ACTION("BA_400", "존재하지 않는 지출 액션입니다."), NOT_FOUND_ACTION("AC_400", "존재하지 않는 액션입니다."), - INTERNAL_SERVER_ERROR("S_001", "서버 내부에서 에러가 발생했습니다."), ; private final String code; diff --git a/server/src/main/java/server/haengdong/presentation/EventController.java b/server/src/main/java/server/haengdong/presentation/EventController.java index 816e7ed26..b6be87911 100644 --- a/server/src/main/java/server/haengdong/presentation/EventController.java +++ b/server/src/main/java/server/haengdong/presentation/EventController.java @@ -6,12 +6,15 @@ 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import server.haengdong.application.EventService; import server.haengdong.presentation.request.EventSaveRequest; +import server.haengdong.presentation.request.MemberUpdateRequest; import server.haengdong.presentation.response.EventDetailResponse; import server.haengdong.presentation.response.EventResponse; +import server.haengdong.presentation.response.MembersResponse; import server.haengdong.presentation.response.StepsResponse; @RequiredArgsConstructor @@ -40,4 +43,22 @@ public ResponseEntity findActions(@PathVariable("eventId") String return ResponseEntity.ok(stepsResponse); } + + @GetMapping("/api/events/{eventId}/members") + public ResponseEntity findAllMembers(@PathVariable("eventId") String token) { + MembersResponse response = MembersResponse.of(eventService.findAllMembers(token)); + + return ResponseEntity.ok(response); + } + + @PutMapping("/api/events/{eventId}/members/{memberName}") + public ResponseEntity updateMember( + @PathVariable("eventId") String token, + @PathVariable("memberName") String memberName, + @Valid @RequestBody MemberUpdateRequest request + ) { + eventService.updateMember(token, memberName, request.toAppRequest()); + + return ResponseEntity.ok().build(); + } } diff --git a/server/src/main/java/server/haengdong/presentation/request/MemberUpdateRequest.java b/server/src/main/java/server/haengdong/presentation/request/MemberUpdateRequest.java new file mode 100644 index 000000000..71f0b4ec7 --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/request/MemberUpdateRequest.java @@ -0,0 +1,13 @@ +package server.haengdong.presentation.request; + +import jakarta.validation.constraints.NotBlank; +import server.haengdong.application.request.MemberUpdateAppRequest; + +public record MemberUpdateRequest( + @NotBlank String name +) { + + public MemberUpdateAppRequest toAppRequest() { + return new MemberUpdateAppRequest(name); + } +} diff --git a/server/src/main/java/server/haengdong/presentation/response/MembersResponse.java b/server/src/main/java/server/haengdong/presentation/response/MembersResponse.java new file mode 100644 index 000000000..0947d9e02 --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/response/MembersResponse.java @@ -0,0 +1,13 @@ +package server.haengdong.presentation.response; + +import java.util.List; +import server.haengdong.application.response.MembersAppResponse; + +public record MembersResponse( + List memberNames +) { + + public static MembersResponse of(MembersAppResponse response) { + return new MembersResponse(response.memberNames()); + } +} diff --git a/server/src/test/java/server/haengdong/application/EventServiceTest.java b/server/src/test/java/server/haengdong/application/EventServiceTest.java index 5a3e8bce3..91a260829 100644 --- a/server/src/test/java/server/haengdong/application/EventServiceTest.java +++ b/server/src/test/java/server/haengdong/application/EventServiceTest.java @@ -2,8 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.BDDMockito.given; +import static server.haengdong.domain.action.MemberActionStatus.IN; +import static server.haengdong.domain.action.MemberActionStatus.OUT; import java.util.List; import org.junit.jupiter.api.AfterEach; @@ -13,19 +16,21 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import server.haengdong.application.request.EventAppRequest; +import server.haengdong.application.request.MemberUpdateAppRequest; import server.haengdong.application.response.ActionAppResponse; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; +import server.haengdong.application.response.MembersAppResponse; import server.haengdong.domain.action.Action; import server.haengdong.domain.action.ActionRepository; import server.haengdong.domain.action.BillAction; import server.haengdong.domain.action.BillActionRepository; import server.haengdong.domain.action.MemberAction; import server.haengdong.domain.action.MemberActionRepository; -import server.haengdong.domain.action.MemberActionStatus; import server.haengdong.domain.event.Event; import server.haengdong.domain.event.EventRepository; import server.haengdong.domain.event.EventTokenProvider; +import server.haengdong.exception.HaengdongException; @SpringBootTest class EventServiceTest { @@ -84,9 +89,9 @@ void findEventTest() { void findActionsTest() { Event event = new Event("행동대장 회식", "웨디_토큰"); Action action = new Action(event, 1L); - MemberAction memberAction = new MemberAction(action, "토다리", MemberActionStatus.IN, 1L); + MemberAction memberAction = new MemberAction(action, "토다리", IN, 1L); Action action1 = new Action(event, 2L); - MemberAction memberAction1 = new MemberAction(action1, "쿠키", MemberActionStatus.IN, 1L); + MemberAction memberAction1 = new MemberAction(action1, "쿠키", IN, 1L); Action action2 = new Action(event, 3L); BillAction billAction = new BillAction(action2, "뽕나무쟁이족발", 30000L); eventRepository.save(event); @@ -107,4 +112,72 @@ void findActionsTest() { tuple(3L, "뽕나무쟁이족발", 30000L, 3L, "BILL") ); } + + @DisplayName("행사에 참여한 전체 인원을 중복 없이 조회한다.") + @Test + void findAllMembersTest() { + String token = "웨디_토큰"; + Event event = new Event("행동대장 회식", token); + Action action1 = new Action(event, 1L); + Action action2 = new Action(event, 2L); + Action action3 = new Action(event, 3L); + Action action4 = new Action(event, 4L); + BillAction billAction = new BillAction(action3, "뽕나무쟁이족발", 30000L); + MemberAction memberAction1 = new MemberAction(action1, "토다리", IN, 1L); + MemberAction memberAction2 = new MemberAction(action2, "쿠키", IN, 1L); + MemberAction memberAction3 = new MemberAction(action4, "쿠키", OUT, 1L); + eventRepository.save(event); + billActionRepository.save(billAction); + memberActionRepository.saveAll(List.of(memberAction1, memberAction2, memberAction3)); + + MembersAppResponse membersAppResponse = eventService.findAllMembers(token); + + assertThat(membersAppResponse.memberNames()).containsExactlyInAnyOrder("토다리", "쿠키"); + } + + @DisplayName("행사 참여 인원의 이름을 변경한다.") + @Test + void updateMember() { + String token = "행동대장 회식"; + Event event = new Event("행동대장 회식", token); + MemberAction memberAction1 = new MemberAction(new Action(event, 1L), "토다리", IN, 1L); + MemberAction memberAction2 = new MemberAction(new Action(event, 2L), "쿠키", IN, 1L); + MemberAction memberAction3 = new MemberAction(new Action(event, 3L), "웨디", IN, 2L); + MemberAction memberAction4 = new MemberAction(new Action(event, 4L), "쿠키", OUT, 3L); + MemberAction memberAction5 = new MemberAction(new Action(event, 5L), "쿠키", IN, 4L); + MemberAction memberAction6 = new MemberAction(new Action(event, 6L), "쿠키", OUT, 5L); + eventRepository.save(event); + memberActionRepository.saveAll(List.of( + memberAction1, memberAction2, memberAction3, memberAction4, memberAction5, memberAction6 + )); + + eventService.updateMember(token, "쿠키", new MemberUpdateAppRequest("쿡쿡")); + + List foundMemberActions = memberActionRepository.findAllByEvent(event); + assertThat(foundMemberActions) + .extracting(MemberAction::getId, MemberAction::getMemberName) + .contains( + tuple(memberAction1.getId(), "토다리"), + tuple(memberAction2.getId(), "쿡쿡"), + tuple(memberAction3.getId(), "웨디"), + tuple(memberAction4.getId(), "쿡쿡"), + tuple(memberAction5.getId(), "쿡쿡"), + tuple(memberAction6.getId(), "쿡쿡") + ); + } + + @DisplayName("참여 인원 이름을 이미 존재하는 행사 참여 인원과 동일한 이름으로 변경할 수 없다.") + @Test + void updateMember1() { + String token = "행동대장 회식"; + Event event = new Event("행동대장 회식", token); + MemberAction memberAction1 = new MemberAction(new Action(event, 1L), "토다리", IN, 1L); + MemberAction memberAction2 = new MemberAction(new Action(event, 2L), "쿠키", IN, 1L); + MemberAction memberAction3 = new MemberAction(new Action(event, 3L), "웨디", IN, 2L); + eventRepository.save(event); + memberActionRepository.saveAll(List.of(memberAction1, memberAction2, memberAction3)); + + assertThatThrownBy(() -> eventService.updateMember(token, "쿠키", new MemberUpdateAppRequest("토다리"))) + .isInstanceOf(HaengdongException.class); + } } diff --git a/server/src/test/java/server/haengdong/presentation/EventControllerTest.java b/server/src/test/java/server/haengdong/presentation/EventControllerTest.java index 0b8527952..347b734bb 100644 --- a/server/src/test/java/server/haengdong/presentation/EventControllerTest.java +++ b/server/src/test/java/server/haengdong/presentation/EventControllerTest.java @@ -1,14 +1,17 @@ package server.haengdong.presentation; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -20,7 +23,9 @@ import server.haengdong.application.request.EventAppRequest; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; +import server.haengdong.application.response.MembersAppResponse; import server.haengdong.presentation.request.EventSaveRequest; +import server.haengdong.presentation.request.MemberUpdateRequest; @WebMvcTest(EventController.class) class EventControllerTest { @@ -63,4 +68,32 @@ void findEventTest() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.eventName").value("행동대장 회식")); } + + @DisplayName("행사에 참여한 전체 인원을 중복 없이 조회한다.") + @Test + void findAllMembersTest() throws Exception { + MembersAppResponse memberAppResponse = new MembersAppResponse(List.of("토다리", "쿠키")); + given(eventService.findAllMembers(anyString())).willReturn(memberAppResponse); + + mockMvc.perform(get("/api/events/{eventId}/members", "TOKEN")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.memberNames").isArray()) + .andExpect(jsonPath("$.memberNames[0]").value("토다리")) + .andExpect(jsonPath("$.memberNames[1]").value("쿠키")); + } + + @DisplayName("행사 참여 인원의 이름을 수정한다.") + @Test + void updateMember() throws Exception { + String token = "TOKEN"; + MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest("변경된 이름"); + String requestBody = objectMapper.writeValueAsString(memberUpdateRequest); + + mockMvc.perform(put("/api/events/{eventId}/members/{memberName}", token, "변경 전 이름") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isOk()); + } }