Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : 같이하기(party) 관련 API 구현 #51

Merged
merged 4 commits into from
Sep 22, 2023
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
11 changes: 10 additions & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,14 @@ operation::shutdown[snippets='http-request,http-response']
=== waiting event 취소
operation::cancel-waiting-event[snippets='http-request,http-response']


== Party

=== party 생성
operation::create-party[snippets='http-request,http-response']
=== party 참여
operation::join-party[snippets='http-request,http-response']
=== party running 시작
operation::start-party[snippets='http-request,http-response']
=== party 나가기
operation::quit-party[snippets='http-request,http-response']

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package online.partyrun.partyrunmatchingservice.domain.party.controller;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyEvent;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyIdResponse;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyRequest;
import online.partyrun.partyrunmatchingservice.domain.party.service.PartyService;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.security.Principal;

@RestController
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@RequestMapping("parties")
public class PartyController {
PartyService partyService;

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<PartyIdResponse> postParties(Mono<Authentication> auth, @RequestBody PartyRequest request) {
return partyService.create(auth.map(Principal::getName), request);
}

@GetMapping(path = "{entryCode}/join", produces = "text/event-stream")
public Flux<PartyEvent> getPartyEventStream(Mono<Authentication> auth, @PathVariable String entryCode) {
return partyService.joinAndConnectSink(auth.map(Principal::getName), entryCode);
}

@PostMapping("{entryCode}/start")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> postPartyStart(Mono<Authentication> auth, @PathVariable String entryCode) {
return partyService.start(auth.map(Principal::getName), entryCode);
}

@PostMapping("{entryCode}/quit")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> postPartyQuit(Mono<Authentication> auth, @PathVariable String entryCode) {
return partyService.quit(auth.map(Principal::getName), entryCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package online.partyrun.partyrunmatchingservice.domain.party.dto;

import online.partyrun.partyrunmatchingservice.domain.party.entity.Party;
import online.partyrun.partyrunmatchingservice.domain.party.entity.PartyStatus;

import java.util.List;

public record PartyEvent(String entryCode, String leaderId, PartyStatus status, List<String> participants, String battleId) {
public PartyEvent(Party party) {
this(party.getEntryCode().getCode(), party.getLeaderId(), party.getStatus(), party.getParticipants(), party.getBattleId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package online.partyrun.partyrunmatchingservice.domain.party.dto;

import online.partyrun.partyrunmatchingservice.domain.party.entity.Party;

public record PartyIdResponse(String code) {
public PartyIdResponse(Party party) {
this(party.getEntryCode().getCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package online.partyrun.partyrunmatchingservice.domain.party.dto;

public record PartyRequest(int distance) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package online.partyrun.partyrunmatchingservice.domain.party.entity;


import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;

import java.security.SecureRandom;

@Getter
@AllArgsConstructor
@EqualsAndHashCode
public class EntryCode {
private static int MIN_RANDOM_NUMBER = 100_000;
private static int MAX_RANDOM_NUMBER = 999_999;

private String code;

public EntryCode() {
this.code = generateRandomRoomId();
}

private String generateRandomRoomId() {
return String.valueOf(new SecureRandom().nextInt(MIN_RANDOM_NUMBER, MAX_RANDOM_NUMBER + 1));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package online.partyrun.partyrunmatchingservice.domain.party.entity;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import online.partyrun.partyrunmatchingservice.domain.waiting.root.RunningDistance;
import org.springframework.data.annotation.Id;

import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Party {
@Id
String id;
EntryCode entryCode = new EntryCode();
String leaderId;
RunningDistance distance;

PartyStatus status = PartyStatus.WAITING;
List<String> participants = new ArrayList<>();
String battleId;

public Party(String leaderId, RunningDistance distance) {
this.leaderId = leaderId;
this.distance = distance;
}

public void join(String memberId) {
participants.add(memberId);
}


public void start(String battleId) {
this.battleId = battleId;
status = PartyStatus.COMPLETED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package online.partyrun.partyrunmatchingservice.domain.party.entity;

public enum PartyStatus {
WAITING, COMPLETED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package online.partyrun.partyrunmatchingservice.domain.party.repository;

import online.partyrun.partyrunmatchingservice.domain.party.entity.EntryCode;
import online.partyrun.partyrunmatchingservice.domain.party.entity.Party;
import online.partyrun.partyrunmatchingservice.domain.party.entity.PartyStatus;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import reactor.core.publisher.Mono;

public interface PartyRepository extends ReactiveMongoRepository<Party, String> {
Mono<Party> findByEntryCodeAndStatus(EntryCode code, PartyStatus status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package online.partyrun.partyrunmatchingservice.domain.party.service;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import online.partyrun.partyrunmatchingservice.domain.battle.service.BattleService;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyEvent;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyIdResponse;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyRequest;
import online.partyrun.partyrunmatchingservice.domain.party.entity.EntryCode;
import online.partyrun.partyrunmatchingservice.domain.party.entity.Party;
import online.partyrun.partyrunmatchingservice.domain.party.entity.PartyStatus;
import online.partyrun.partyrunmatchingservice.domain.party.repository.PartyRepository;
import online.partyrun.partyrunmatchingservice.domain.waiting.root.RunningDistance;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@RequiredArgsConstructor
public class PartyService {
PartyRepository partyRepository;
PartySinkHandler partySinkHandler;
BattleService battleService;

public Mono<PartyIdResponse> create(Mono<String> member, PartyRequest request) {
return member.map(memberId ->
new Party(memberId, RunningDistance.getBy(request.distance())))
.flatMap(partyRepository::save)
.map(PartyIdResponse::new);
}

public Flux<PartyEvent> joinAndConnectSink(Mono<String> member, String entryCode) {
return member
.doOnNext(partySinkHandler::create)
.flatMap(memberId -> joinParty(memberId, entryCode))
.then(member)
.flatMapMany(partySinkHandler::connect);
}

private Mono<Void> joinParty(String memberId, String entryCode) {
return getWaitingParty(entryCode)
.flatMap(party -> {
party.join(memberId);
return partyRepository.save(party);
}
)
.then(multicast(entryCode));
}

private Mono<Void> multicast(String code) {
return getWaitingParty(code)
.doOnNext(party -> party.getParticipants().forEach(
member -> partySinkHandler.sendEvent(member, new PartyEvent(party))
)).then();
}

private Mono<Party> getWaitingParty(String code) {
return partyRepository.findByEntryCodeAndStatus(new EntryCode(code), PartyStatus.WAITING);
}

public Mono<Void> start(Mono<String> member, String code) {
// TODO 방장 여부 확인
return getWaitingParty(code).flatMap(party ->
battleService.create(party.getParticipants(), party.getDistance().getMeter())
).flatMap(battleId ->
getWaitingParty(code).doOnNext(party -> party.start(battleId))
.flatMap(partyRepository::save))
.doOnNext(party -> party.getParticipants().forEach(
partyMember -> {
partySinkHandler.sendEvent(partyMember, new PartyEvent(party));
partySinkHandler.complete(partyMember);
}
)).then();
}
public Mono<Void> quit(Mono<String> member, String code) {
// TODO
throw new UnsupportedOperationException("Not supported yet");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package online.partyrun.partyrunmatchingservice.domain.party.service;

import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyEvent;
import online.partyrun.partyrunmatchingservice.global.sse.SinkHandlerTemplate;
import org.springframework.stereotype.Component;

@Component
public class PartySinkHandler extends SinkHandlerTemplate<String, PartyEvent> {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package online.partyrun.partyrunmatchingservice.domain.matching.service;

import lombok.SneakyThrows;
import online.partyrun.partyrunmatchingservice.config.redis.RedisTestConfig;
import online.partyrun.partyrunmatchingservice.domain.battle.service.BattleService;
import online.partyrun.partyrunmatchingservice.domain.matching.controller.MatchingRequest;
Expand Down Expand Up @@ -179,7 +178,6 @@ class 스케줄러가_동작할_때 {

@Test
@DisplayName("TIMEOUT된 Match를 삭제한다")
@SneakyThrows
void runDeleteIfTimeOver() {
matchingService.create(members, 1000).block();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package online.partyrun.partyrunmatchingservice.domain.party.controller;

import online.partyrun.partyrunmatchingservice.config.docs.WebfluxDocsTest;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyEvent;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyIdResponse;
import online.partyrun.partyrunmatchingservice.domain.party.dto.PartyRequest;
import online.partyrun.partyrunmatchingservice.domain.party.entity.PartyStatus;
import online.partyrun.partyrunmatchingservice.domain.party.service.PartyService;
import online.partyrun.partyrunmatchingservice.global.controller.HttpControllerAdvice;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;

@ContextConfiguration(classes = {PartyController.class, HttpControllerAdvice.class})
@DisplayName("PartyController")
@WithMockUser
class PartyControllerTest extends WebfluxDocsTest {
@MockBean
PartyService partyService;

private static final String ENTRY_CODE = "123456";

@Test
@DisplayName("post : party 생성")
void postParties() {
PartyRequest request = new PartyRequest(1000);
given(partyService.create(any(Mono.class), any(PartyRequest.class)))
.willReturn(Mono.just(new PartyIdResponse("123456")));

client.post()
.uri("/parties")
.bodyValue(request)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isCreated()
.expectBody()
.consumeWith(document("create-party"));
}

@Test
@DisplayName("get : join party")
void getPartyEventStream() {
final Flux<PartyEvent> eventResult = Flux.just(
new PartyEvent(ENTRY_CODE, "member1", PartyStatus.WAITING, List.of("member1"), null),
new PartyEvent(ENTRY_CODE, "member1", PartyStatus.WAITING, List.of("member1", "member2"), null),
new PartyEvent(ENTRY_CODE, "member1", PartyStatus.COMPLETED, List.of("member1", "member2"), "battle-id")
);
given(partyService.joinAndConnectSink(any(Mono.class), any(String.class)))
.willReturn(eventResult);

client.get()
.uri("/parties/{entryCode}/join", ENTRY_CODE)
.accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.expectStatus()
.isOk()
.expectBody()
.consumeWith(document("join-party"));
}

@Test
@DisplayName("post : start party")
void partyStart() {
given(partyService.start(any(Mono.class), any(String.class)))
.willReturn(Mono.empty());

client.post()
.uri("/parties/{entryCode}/start",ENTRY_CODE)
.exchange()
.expectStatus()
.isNoContent()
.expectBody()
.consumeWith(document("start-party"));
}

@Test
@DisplayName("post : quit party")
void quitParty() {
given(partyService.start(any(Mono.class), any(String.class)))
.willReturn(Mono.empty());

client.post()
.uri("/parties/{entryCode}/quit",ENTRY_CODE)
.exchange()
.expectStatus()
.isNoContent()
.expectBody()
.consumeWith(document("quit-party"));
}

}
Loading
Loading