Skip to content

Commit

Permalink
Merge pull request #25 from depromeet/feat/LS-19
Browse files Browse the repository at this point in the history
feat: 스페이스 단건조회 / LS-13 리뷰 사항 적용
  • Loading branch information
raymondanythings authored Jul 13, 2024
2 parents 80514fa + 16abe79 commit edd871a
Show file tree
Hide file tree
Showing 20 changed files with 446 additions and 72 deletions.
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ project(":layer-domain") {
// h2
runtimeOnly 'com.h2database:h2'


//QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public class AuthValueConfig {
public static final String KAKAO_URI = "https://kapi.kakao.com/v2/user/me";



@PostConstruct
protected void init() {
JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
Expand Down
7 changes: 4 additions & 3 deletions layer-api/src/main/java/org/layer/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ public class SwaggerConfig {
private static final String AUTH_TOKEN = "Authorization";

SecurityScheme apiAuth = new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.type(SecurityScheme.Type.HTTP)
.in(SecurityScheme.In.HEADER)
.scheme("Bearer")
.bearerFormat("JWT")
.name(AUTH_TOKEN);

SecurityRequirement addSecurityItem = new SecurityRequirement()
Expand Down Expand Up @@ -50,8 +52,7 @@ public OpenAPI openAPI() {
@Bean
public OperationCustomizer customizeOperation() {
return (operation, handlerMethod) -> {
HandlerMethod method = (HandlerMethod) handlerMethod;
method.getMethodParameters();
HandlerMethod method = handlerMethod;
method.getMethodParameters();
if (Arrays.stream(method.getMethodParameters()).anyMatch(param -> param.hasParameterAnnotation(MemberId.class))) {
operation.getParameters().removeIf(param -> "memberId".equals(param.getName()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String accessToken = getJwtFromRequest(request);

if(jwtValidator.isValidToken(accessToken)) {

if (isValidToken(accessToken)) {
Long memberId = jwtValidator.getMemberIdFromToken(accessToken);
List<String> role = jwtValidator.getRoleFromToken(accessToken);
setAuthenticationToContext(memberId, MemberRole.valueOf(role.get(0)));
Expand Down
41 changes: 35 additions & 6 deletions layer-api/src/main/java/org/layer/domain/space/api/SpaceApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import org.layer.domain.space.dto.SpaceResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "스페이스 API")
public interface SpaceApi {
Expand All @@ -31,22 +32,50 @@ public interface SpaceApi {
)
}
)
ResponseEntity<SpaceResponse.SpacePage> getMySpaceList(@MemberId Long memberId, @RequestParam @Validated SpaceRequest.GetSpaceRequest getSpaceRequest);
ResponseEntity<SpaceResponse.SpacePage> getMySpaceList(@MemberId Long memberId, @ModelAttribute @Validated SpaceRequest.GetSpaceRequest getSpaceRequest);

@Operation(summary = "스페이스 생성하기", method = "POST", description = """
@Operation(summary = "스페이스 생성하기", method = "PUT", description = """
스페이스를 생성합니다. <br />
생성 성공 시 저장된 스페이스 정보를 반환합니다.
생성 성공 시 아무것도 반환하지 않습니다.
""")
@ApiResponses({
@ApiResponse(responseCode = "201",
content = {
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Boolean.class)
schema = @Schema()
)
}
)
}
)
ResponseEntity<Boolean> createSpace(@MemberId Long memberId, @RequestBody @Validated SpaceRequest.CreateSpaceRequest createSpaceRequest);
void createSpace(@MemberId Long memberId, @RequestBody @Validated SpaceRequest.CreateSpaceRequest createSpaceRequest);

@Operation(summary = "스페이스 수정하기", method = "POST", description = """
스페이스를 수정합니다. <br />
생성 성공 시 아무것도 반환하지 않습니다.
""")
@ApiResponses({
@ApiResponse(responseCode = "202",
content = {
@Content(
mediaType = "application/json",
schema = @Schema()
)
}
)
}
)
void updateSpace(@MemberId Long memberId, @RequestBody @Validated SpaceRequest.UpdateSpaceRequest updateSpaceRequest);

@Operation(summary = "스페이스 단건 조회하기", method = "GET", description = """
스페이스 아이디를 통해 하나의 스페이스를 조회합니다.
내가 속하지 않은 공간만 조회할 수 있습니다.
""")
@ApiResponses({
@ApiResponse(responseCode = "200", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = SpaceResponse.SpaceWithUserCountInfo.class))
})
})
ResponseEntity<SpaceResponse.SpaceWithUserCountInfo> getSpaceById(@MemberId Long memberId, @PathVariable Long spaceId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import org.layer.domain.space.dto.SpaceRequest;
import org.layer.domain.space.dto.SpaceResponse;
import org.layer.domain.space.service.SpaceService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

Expand All @@ -23,17 +23,28 @@ public class SpaceController implements SpaceApi {

@Override
@GetMapping("/list")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<SpaceResponse.SpacePage> getMySpaceList(@MemberId Long memberId, SpaceRequest.GetSpaceRequest getSpaceRequest) {
public ResponseEntity<SpaceResponse.SpacePage> getMySpaceList(@MemberId Long memberId, @ModelAttribute @Validated SpaceRequest.GetSpaceRequest getSpaceRequest) {
var response = spaceService.getSpaceListFromMemberId(memberId, getSpaceRequest);
return ResponseEntity.ok(response);
}

@Override
@PutMapping("/")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<Boolean> createSpace(@MemberId Long memberId, @RequestBody @Validated SpaceRequest.CreateSpaceRequest createSpaceRequest) {
var response = spaceService.createSpace(memberId, createSpaceRequest);
return ResponseEntity.ok(response);
@PutMapping("")
public void createSpace(@MemberId Long memberId, @RequestBody @Validated SpaceRequest.CreateSpaceRequest createSpaceRequest) {
spaceService.createSpace(memberId, createSpaceRequest);
}

@Override
@PostMapping("")
@ResponseStatus(HttpStatus.ACCEPTED)
public void updateSpace(@MemberId Long memberId, @RequestBody @Validated SpaceRequest.UpdateSpaceRequest updateSpaceRequest) {
spaceService.updateSpace(memberId, updateSpaceRequest);
}

@GetMapping("/{spaceId}")
public ResponseEntity<SpaceResponse.SpaceWithUserCountInfo> getSpaceById(@MemberId Long memberId, @PathVariable Long spaceId) {
var foundSpace = spaceService.getSpaceById(memberId, spaceId);
return ResponseEntity.ok((foundSpace));
}
}

Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package org.layer.domain.space.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import org.layer.common.annotation.AtLeastNotNull;
import org.layer.domain.space.entity.Space;
import org.layer.domain.space.entity.SpaceCategory;
import org.layer.domain.space.entity.SpaceField;

import java.util.Optional;

@Schema
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SpaceRequest {

// @Builder
@Schema(description = "스페이스 생성하기")
public record CreateSpaceRequest(
@Schema(description = "프로젝트 유형 카테고리", example = "INDIVIDUAL")
Expand All @@ -37,6 +40,39 @@ public Space toEntity(Long memberId) {
}
}

@AtLeastNotNull(min = 2)
@Schema(description = "스페이스 수정하기")
public record UpdateSpaceRequest(

@Schema(description = "수정하고자 하는 스페이스 아이디")
@NotNull
Long id,

@Schema(description = "프로젝트 유형 카테고리", example = "INDIVIDUAL", nullable = true)
SpaceCategory category,
@Schema(description = "진행중인 프로젝트 유형", nullable = true)

SpaceField field,
@Schema(description = "이름", nullable = true)

String name,

@Schema(description = "공간 설명", nullable = true)
String introduction
) {

public Space toEntity(Long memberId) {
return Space.builder()
.id(id)
.category(category)
.field(field)
.name(name)
.introduction(introduction)
.leaderId(memberId)
.build();
}
}

@Schema(description = "내가 속한 스페이스 조회")
public record GetSpaceRequest(
@Schema(description = "커서 아이디")
Expand All @@ -45,10 +81,18 @@ public record GetSpaceRequest(
@Schema(description = "조회하고자 하는 스페이스 타입")
Optional<SpaceCategory> category,

@Schema(description = "페이지 사이즈")
@Schema(description = "페이지 사이즈", defaultValue = "1")
int pageSize

) {
public GetSpaceRequest {
if (pageSize <= 0) {
pageSize = 1;
}
if (cursorId == null) {
cursorId = 0L;
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.layer.domain.space.exception;

import lombok.RequiredArgsConstructor;
import org.layer.common.exception.ExceptionType;
import org.springframework.http.HttpStatus;

import static org.springframework.http.HttpStatus.BAD_REQUEST;

@RequiredArgsConstructor
public enum SpaceExceptionType implements ExceptionType {

/**
*
*/

SPACE_NOT_FOUND(BAD_REQUEST, "스페이스를 찾을 수 없어요.");

private final HttpStatus status;
private final String message;

@Override
public HttpStatus httpStatus() {
return status;
}

@Override
public String message() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,67 @@
package org.layer.domain.space.service;

import jakarta.transaction.Transactional;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.layer.common.dto.Meta;
import org.layer.common.exception.BaseCustomException;
import org.layer.domain.space.dto.SpaceRequest;
import org.layer.domain.space.dto.SpaceResponse;
import org.layer.domain.space.dto.SpaceWithMemberCount;
import org.layer.domain.space.entity.MemberSpaceRelation;
import org.layer.domain.space.entity.Space;
import org.layer.domain.space.repository.MemberSpaceRelationRepository;
import org.layer.domain.space.repository.SpaceRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.stream.Collectors;

import static org.layer.domain.space.exception.SpaceExceptionType.SPACE_NOT_FOUND;

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class SpaceService {
private final SpaceRepository spaceRepository;
private final MemberSpaceRelationRepository memberSpaceRelationRepository;

public SpaceResponse.SpacePage getSpaceListFromMemberId(Long memberId, SpaceRequest.GetSpaceRequest getSpaceRequest) {
PageRequest pageRequest = PageRequest.of(0, getSpaceRequest.pageSize());
Page<SpaceWithMemberCount> spacePages;
if (getSpaceRequest.category().isPresent()) {
spacePages = spaceRepository.findAllSpacesByMemberIdAndCategoryAndCursor(memberId, getSpaceRequest.cursorId(), getSpaceRequest.category().get(), pageRequest);
} else {
spacePages = spaceRepository.findAllSpacesByMemberIdAndCursor(memberId, getSpaceRequest.cursorId(), pageRequest);

var spacePages = spaceRepository.findAllSpacesByMemberIdAndCategoryAndCursor(memberId, getSpaceRequest.cursorId(), getSpaceRequest.category(), getSpaceRequest.pageSize());

boolean hasNextPage = spacePages.size() > getSpaceRequest.pageSize();
if (hasNextPage) {
spacePages.remove(spacePages.size() - 1);
}
Long newCursor = !hasNextPage ? null : spacePages.isEmpty() ? null : spacePages.get(spacePages.size() - 1).getId();


var spaceList = spacePages.stream().map(SpaceResponse.SpaceWithUserCountInfo::toResponse).collect(Collectors.toList());
boolean hasNextPage = spacePages.hasNext();
Long newCursor = hasNextPage ? spacePages.getContent().get(spacePages.getNumberOfElements() - 1).getId() : null;

var meta = Meta.builder().cursor(newCursor).hasNextPage(hasNextPage).build();
return SpaceResponse.SpacePage.toResponse(spaceList, meta);
}

@Transactional
public Boolean createSpace(Long memberId, SpaceRequest.CreateSpaceRequest createSpaceRequest) {
Space newSpace = createSpaceRequest.toEntity(memberId);
public void createSpace(Long memberId, SpaceRequest.CreateSpaceRequest mutateSpaceRequest) {
var newSpace = spaceRepository.save(mutateSpaceRequest.toEntity(memberId));
var memberSpaceRelation = MemberSpaceRelation.builder().memberId(memberId).spaceId(newSpace.getId()).build();
spaceRepository.save(newSpace);

memberSpaceRelationRepository.save(memberSpaceRelation);
return true;
}

@Transactional
public void updateSpace(Long memberId, SpaceRequest.UpdateSpaceRequest updateSpaceRequest) {
spaceRepository.findByIdAndJoinedMemberId(updateSpaceRequest.id(), memberId).orElseThrow(() -> new BaseCustomException(SPACE_NOT_FOUND));
spaceRepository.updateSpace(updateSpaceRequest.id(), updateSpaceRequest.category(), updateSpaceRequest.field(), updateSpaceRequest.name(), updateSpaceRequest.introduction());
}

public SpaceResponse.SpaceWithUserCountInfo getSpaceById(Long memberId, Long spaceId) {
var foundSpace = spaceRepository.findByIdAndJoinedMemberId(spaceId, memberId).orElseThrow(() -> new BaseCustomException(SPACE_NOT_FOUND));

return SpaceResponse.SpaceWithUserCountInfo.toResponse(foundSpace);
}


}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.layer.common.annotation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import org.layer.common.validator.AtLeastNotNullValidator;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AtLeastNotNullValidator.class)
public @interface AtLeastNotNull {
String message() default "{min} 개 이상의 값이 필요해요.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

int min() default 1;

}
Loading

0 comments on commit edd871a

Please sign in to comment.