diff --git a/src/main/java/team/silvertown/masil/common/exception/GlobalExceptionHandler.java b/src/main/java/team/silvertown/masil/common/exception/GlobalExceptionHandler.java index ee910b84..48e7d232 100644 --- a/src/main/java/team/silvertown/masil/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/team/silvertown/masil/common/exception/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package team.silvertown.masil.common.exception; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanInstantiationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -91,4 +92,17 @@ public ResponseEntity handleHttpMessageNotReadableException( return handleUnknownException((Exception) rootCause); } + @ExceptionHandler(BeanInstantiationException.class) + public ResponseEntity handleBeanInstantiationException( + BeanInstantiationException e + ) { + Throwable rootCause = e.getRootCause(); + + if (rootCause instanceof BadRequestException) { + return handleBadRequestException((BadRequestException) rootCause); + } + + return handleUnknownException(e); + } + } diff --git a/src/main/java/team/silvertown/masil/common/scroll/OrderType.java b/src/main/java/team/silvertown/masil/common/scroll/OrderType.java new file mode 100644 index 00000000..231860ac --- /dev/null +++ b/src/main/java/team/silvertown/masil/common/scroll/OrderType.java @@ -0,0 +1,27 @@ +package team.silvertown.masil.common.scroll; + +import io.micrometer.common.util.StringUtils; +import java.util.Arrays; +import java.util.Objects; +import team.silvertown.masil.common.exception.BadRequestException; +import team.silvertown.masil.common.scroll.dto.ScrollErrorCode; + +public enum OrderType { + LATEST, + MOST_POPULAR; + + public static OrderType get(String order) { + if (StringUtils.isBlank(order)) { + return LATEST; + } + + return Arrays.stream(OrderType.values()) + .filter(orderType -> order.equals(orderType.name())) + .findFirst() + .orElseThrow(() -> new BadRequestException(ScrollErrorCode.INVALID_ORDER_TYPE)); + } + + public static boolean isMostPopular(OrderType orderType) { + return Objects.isNull(orderType) || orderType == MOST_POPULAR; + } +} diff --git a/src/main/java/team/silvertown/masil/common/scroll/dto/NormalListRequest.java b/src/main/java/team/silvertown/masil/common/scroll/dto/NormalListRequest.java new file mode 100644 index 00000000..ca5d3829 --- /dev/null +++ b/src/main/java/team/silvertown/masil/common/scroll/dto/NormalListRequest.java @@ -0,0 +1,38 @@ +package team.silvertown.masil.common.scroll.dto; + +import java.util.Objects; +import lombok.Builder; +import lombok.Getter; + +@Getter +public final class NormalListRequest { + + private final String depth1; + private final String depth2; + private final String depth3; + private final ScrollRequest scrollRequest; + + @Builder + private NormalListRequest( + String depth1, + String depth2, + String depth3, + String order, + String cursor, + int size + ) { + this.depth1 = depth1; + this.depth2 = depth2; + this.depth3 = depth3; + this.scrollRequest = new ScrollRequest(order, cursor, size); + } + + public int getSize() { + return this.scrollRequest.getSize(); + } + + public boolean isBasedOnAddress() { + return Objects.nonNull(depth1) && Objects.nonNull(depth2) && Objects.nonNull(depth3); + } + +} diff --git a/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollErrorCode.java b/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollErrorCode.java new file mode 100644 index 00000000..993e691c --- /dev/null +++ b/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollErrorCode.java @@ -0,0 +1,15 @@ +package team.silvertown.masil.common.scroll.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import team.silvertown.masil.common.exception.ErrorCode; + +@RequiredArgsConstructor +@Getter +public enum ScrollErrorCode implements ErrorCode { + INVALID_ORDER_TYPE(20311000, "올바르지 않은 정렬 기준입니다"), + INVALID_CURSOR_FORMAT(20311001, "정렬 기준에 맞지 않은 커서 형식입니다"); + + private final int code; + private final String message; +} diff --git a/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollRequest.java b/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollRequest.java new file mode 100644 index 00000000..e0b93f2f --- /dev/null +++ b/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollRequest.java @@ -0,0 +1,28 @@ +package team.silvertown.masil.common.scroll.dto; + +import lombok.Getter; +import team.silvertown.masil.common.scroll.OrderType; +import team.silvertown.masil.common.validator.ScrollValidator; + +@Getter +public final class ScrollRequest { + + private final OrderType order; + private final String cursor; + private final int size; + + public ScrollRequest( + String order, + String cursor, + int size + ) { + OrderType orderType = OrderType.get(order); + + ScrollValidator.validateCursorFormat(cursor, orderType); + + this.order = orderType; + this.cursor = cursor; + this.size = size; + } + +} diff --git a/src/main/java/team/silvertown/masil/common/response/ScrollResponse.java b/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollResponse.java similarity index 86% rename from src/main/java/team/silvertown/masil/common/response/ScrollResponse.java rename to src/main/java/team/silvertown/masil/common/scroll/dto/ScrollResponse.java index 4371e36a..c4af7740 100644 --- a/src/main/java/team/silvertown/masil/common/response/ScrollResponse.java +++ b/src/main/java/team/silvertown/masil/common/scroll/dto/ScrollResponse.java @@ -1,4 +1,4 @@ -package team.silvertown.masil.common.response; +package team.silvertown.masil.common.scroll.dto; import java.util.List; diff --git a/src/main/java/team/silvertown/masil/common/validator/ScrollValidator.java b/src/main/java/team/silvertown/masil/common/validator/ScrollValidator.java new file mode 100644 index 00000000..9d529114 --- /dev/null +++ b/src/main/java/team/silvertown/masil/common/validator/ScrollValidator.java @@ -0,0 +1,30 @@ +package team.silvertown.masil.common.validator; + +import io.micrometer.common.util.StringUtils; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import team.silvertown.masil.common.exception.BadRequestException; +import team.silvertown.masil.common.scroll.OrderType; +import team.silvertown.masil.common.scroll.dto.ScrollErrorCode; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ScrollValidator extends Validator { + + private static final int ID_CURSOR_LENGTH = 16; + private static final String INITIAL_CURSUR = "0"; + + public static void validateCursorFormat(String cursor, OrderType order) { + if (StringUtils.isBlank(cursor) || cursor.equals(INITIAL_CURSUR)) { + return; + } + + if (OrderType.isMostPopular(order)) { + throwIf(cursor.length() != ID_CURSOR_LENGTH, + () -> new BadRequestException(ScrollErrorCode.INVALID_CURSOR_FORMAT)); + return; + } + + notOver(cursor.length(), ID_CURSOR_LENGTH, ScrollErrorCode.INVALID_CURSOR_FORMAT); + } + +} diff --git a/src/main/java/team/silvertown/masil/config/WebMvcConfig.java b/src/main/java/team/silvertown/masil/config/WebMvcConfig.java index da79a9bd..42ed6f30 100644 --- a/src/main/java/team/silvertown/masil/config/WebMvcConfig.java +++ b/src/main/java/team/silvertown/masil/config/WebMvcConfig.java @@ -4,7 +4,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration diff --git a/src/main/java/team/silvertown/masil/config/security/HttpRequestsConfigurer.java b/src/main/java/team/silvertown/masil/config/security/HttpRequestsConfigurer.java index 0827aecb..6103072c 100644 --- a/src/main/java/team/silvertown/masil/config/security/HttpRequestsConfigurer.java +++ b/src/main/java/team/silvertown/masil/config/security/HttpRequestsConfigurer.java @@ -24,9 +24,18 @@ public class HttpRequestsConfigurer "/swagger-resources/**" }; private static final String AUTH_RESOURCE = "/api/v1/users/login"; - private static final String[] POST_GET_RESOURCES = { - "/api/v1/posts" + private static final String[] GET_PERMIT_ALL_RESOURCES = { + // users + "api/v1/users/**", + + // posts + "/api/v1/posts", + "/api/v1/posts/**", + + // mates + "/api/v1/mates/**" }; + private static final String USER_ME_RESOURCE = "/api/v1/users/me"; private static final String ADMIN_PANEL = "/{0}/**"; private final SnapAdminProperties snapAdminProperties; @@ -40,10 +49,12 @@ public void customize( .permitAll() .requestMatchers(AUTH_RESOURCE) .permitAll() - .requestMatchers(HttpMethod.GET, POST_GET_RESOURCES) + .requestMatchers(HttpMethod.GET, GET_PERMIT_ALL_RESOURCES) .permitAll() .requestMatchers(MessageFormat.format(ADMIN_PANEL, snapAdminProperties.getBaseUrl())) .permitAll() + .requestMatchers(HttpMethod.GET, USER_ME_RESOURCE) + .authenticated() .anyRequest() .authenticated(); } diff --git a/src/main/java/team/silvertown/masil/image/exception/ImageErrorCode.java b/src/main/java/team/silvertown/masil/image/exception/ImageErrorCode.java index d5f3fde2..e49b0f85 100644 --- a/src/main/java/team/silvertown/masil/image/exception/ImageErrorCode.java +++ b/src/main/java/team/silvertown/masil/image/exception/ImageErrorCode.java @@ -7,7 +7,8 @@ @Getter @AllArgsConstructor public enum ImageErrorCode implements ErrorCode { - NOT_SUPPORTED_CONTENT(500_16000, "지원하지 않는 파일 형식 입니다"); + NOT_SUPPORTED_CONTENT(500_16000, "지원하지 않는 파일 형식 입니다"), + FILE_IS_EMPTY(500_16001, "비어있는 파일입니다"); private final int code; private final String message; diff --git a/src/main/java/team/silvertown/masil/image/validator/ImageFileServiceValidator.java b/src/main/java/team/silvertown/masil/image/validator/ImageFileServiceValidator.java new file mode 100644 index 00000000..326666e6 --- /dev/null +++ b/src/main/java/team/silvertown/masil/image/validator/ImageFileServiceValidator.java @@ -0,0 +1,17 @@ +package team.silvertown.masil.image.validator; + +import org.springframework.web.multipart.MultipartFile; +import team.silvertown.masil.common.exception.BadRequestException; +import team.silvertown.masil.common.validator.Validator; +import team.silvertown.masil.image.exception.ImageErrorCode; + +public class ImageFileServiceValidator extends Validator { + + public static void validateImgFile(MultipartFile file) { + throwIf(file.isEmpty(), () -> new BadRequestException(ImageErrorCode.FILE_IS_EMPTY)); + String contentType = file.getContentType(); + throwIf(!ImageFileType.isImage(contentType), + () -> new BadRequestException(ImageErrorCode.NOT_SUPPORTED_CONTENT)); + } + +} diff --git a/src/main/java/team/silvertown/masil/masil/controller/MasilController.java b/src/main/java/team/silvertown/masil/masil/controller/MasilController.java index 063b0de9..d2728b5f 100644 --- a/src/main/java/team/silvertown/masil/masil/controller/MasilController.java +++ b/src/main/java/team/silvertown/masil/masil/controller/MasilController.java @@ -1,12 +1,16 @@ package team.silvertown.masil.masil.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.net.URI; +import java.time.LocalDate; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -14,6 +18,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import team.silvertown.masil.masil.dto.request.CreateMasilRequest; import team.silvertown.masil.masil.dto.request.PeriodRequest; @@ -30,7 +35,6 @@ public class MasilController { public final MasilService masilService; - @PostMapping("/api/v1/masils") @Operation(summary = "마실 생성") @ApiResponse( @@ -69,6 +73,7 @@ public ResponseEntity create( public ResponseEntity getRecent( @AuthenticationPrincipal Long userId, + @RequestParam(required = false, defaultValue = "10") Integer size ) { RecentMasilResponse response = masilService.getRecent(userId, size); @@ -85,9 +90,28 @@ public ResponseEntity getRecent( schema = @Schema(implementation = PeriodResponse.class) ) ) + @Parameters( + { + @Parameter( + name = "startDate", + in = ParameterIn.QUERY, + schema = @Schema(implementation = LocalDate.class), + example = "2024-03-12", + description = "없을 시 오늘이 포함된 월 초하루" + ), + @Parameter( + name = "endDate", + in = ParameterIn.QUERY, + schema = @Schema(implementation = LocalDate.class), + example = "2024-03-12", + description = "없을 시 시작 날짜가 포함된 월 말일" + ) + } + ) public ResponseEntity getInGivenPeriod( @AuthenticationPrincipal Long userId, + @Parameter(hidden = true) PeriodRequest request ) { PeriodResponse response = masilService.getInGivenPeriod(userId, request); diff --git a/src/main/java/team/silvertown/masil/masil/domain/Masil.java b/src/main/java/team/silvertown/masil/masil/domain/Masil.java index 5439b8ea..2242f48c 100644 --- a/src/main/java/team/silvertown/masil/masil/domain/Masil.java +++ b/src/main/java/team/silvertown/masil/masil/domain/Masil.java @@ -38,40 +38,29 @@ public class Masil extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", referencedColumnName = "id") private User user; - @Column(name = "post_id") private Long postId; - @Embedded @Getter(AccessLevel.NONE) private Address address; - @Column(name = "path", nullable = false) private LineString path; - @Column(name = "content", columnDefinition = "TEXT") private String content; - @Column(name = "thumbnail_url", length = 1024) private String thumbnailUrl; - @Column(name = "distance", nullable = false) private Integer distance; - @Column(name = "total_time", nullable = false) private Integer totalTime; - @Column(name = "calories", nullable = false) private Integer calories; - @Column(name = "started_at", nullable = false, columnDefinition = "TIMESTAMP(6)") @TimeZoneStorage(TimeZoneStorageType.NORMALIZE) private OffsetDateTime startedAt; - @OneToMany(mappedBy = "masil") private final List masilPins = new ArrayList<>(); diff --git a/src/main/java/team/silvertown/masil/masil/repository/MasilQueryRepositoryImpl.java b/src/main/java/team/silvertown/masil/masil/repository/MasilQueryRepositoryImpl.java index 1a3406bf..341ab696 100644 --- a/src/main/java/team/silvertown/masil/masil/repository/MasilQueryRepositoryImpl.java +++ b/src/main/java/team/silvertown/masil/masil/repository/MasilQueryRepositoryImpl.java @@ -26,9 +26,9 @@ public class MasilQueryRepositoryImpl implements MasilQueryRepository { private static final int DEFAULT_RECENT_SIZE = 10; private final JPAQueryFactory jpaQueryFactory; + private final QMasil masil = QMasil.masil; public List findRecent(User user, Integer size) { - QMasil masil = QMasil.masil; int limit = DEFAULT_RECENT_SIZE; if (Objects.nonNull(size) && size != 0) { @@ -49,7 +49,6 @@ public List findInGivenPeriod( OffsetDateTime startDateTime, OffsetDateTime endDateTime ) { - QMasil masil = QMasil.masil; BooleanBuilder condition = new BooleanBuilder(); StringTemplate startDate = convertToLocalDate(masil.startedAt); @@ -62,7 +61,7 @@ public List findInGivenPeriod( .orderBy(masil.startedAt.asc()) .transform( GroupBy.groupBy(startDate) - .as(projectDailyDetail(masil)) + .as(projectDailyDetail()) ) .entrySet() .stream() @@ -77,9 +76,7 @@ private StringTemplate convertToLocalDate(DateTimePath dateTime) ); } - private GroupExpression> projectDailyDetail( - QMasil masil - ) { + private GroupExpression> projectDailyDetail() { return GroupBy.list( Projections.constructor( MasilDailyDetailDto.class, diff --git a/src/main/java/team/silvertown/masil/mate/controller/MateController.java b/src/main/java/team/silvertown/masil/mate/controller/MateController.java new file mode 100644 index 00000000..cc81e846 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/controller/MateController.java @@ -0,0 +1,76 @@ +package team.silvertown.masil.mate.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import team.silvertown.masil.mate.dto.request.CreateMateRequest; +import team.silvertown.masil.mate.dto.response.CreateMateResponse; +import team.silvertown.masil.mate.dto.response.MateDetailResponse; +import team.silvertown.masil.mate.service.MateService; + +@RestController +@RequiredArgsConstructor +@Tag(name = "메이트 모집 관련 API") +public class MateController { + + private final MateService mateService; + + @PostMapping("/api/v1/mates") + @Operation(summary = "메이트 모집 생성") + @ApiResponse( + responseCode = "201", + headers = @Header( + name = "해당 메이트 모집 조회 API", + description = "/api/mates/{id}" + ), + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = CreateMateResponse.class) + ) + ) + public ResponseEntity create( + @AuthenticationPrincipal + Long userId, + @RequestBody + CreateMateRequest request + ) { + CreateMateResponse response = mateService.create(userId, request); + URI uri = URI.create("/api/v1/mates/" + response.id()); + + return ResponseEntity.created(uri) + .body(response); + } + + @GetMapping("/api/v1/mates/{id}") + @Operation(summary = "메이트 상세 조회") + @ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MateDetailResponse.class) + ) + ) + @SecurityRequirements + public ResponseEntity getDetailById( + @PathVariable + Long id + ) { + MateDetailResponse response = mateService.getDetailById(id); + + return ResponseEntity.ok(response); + } + +} diff --git a/src/main/java/team/silvertown/masil/mate/domain/MateParticipant.java b/src/main/java/team/silvertown/masil/mate/domain/MateParticipant.java index 9e64169f..647cb8ce 100644 --- a/src/main/java/team/silvertown/masil/mate/domain/MateParticipant.java +++ b/src/main/java/team/silvertown/masil/mate/domain/MateParticipant.java @@ -38,7 +38,7 @@ public class MateParticipant extends BaseEntity { @JoinColumn(name = "mate_id", referencedColumnName = "id") private Mate mate; - @Column(name = "message", length = 255) + @Column(name = "message") private String message; @Enumerated(EnumType.STRING) diff --git a/src/main/java/team/silvertown/masil/mate/dto/request/CreateMateRequest.java b/src/main/java/team/silvertown/masil/mate/dto/request/CreateMateRequest.java new file mode 100644 index 00000000..d5816304 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/dto/request/CreateMateRequest.java @@ -0,0 +1,38 @@ +package team.silvertown.masil.mate.dto.request; + +import java.time.OffsetDateTime; +import team.silvertown.masil.common.map.KakaoPoint; +import team.silvertown.masil.common.map.MapErrorCode; +import team.silvertown.masil.mate.exception.MateErrorCode; +import team.silvertown.masil.mate.validator.MateValidator; + +public record CreateMateRequest( + Long postId, + String depth1, + String depth2, + String depth3, + String depth4, + String title, + String content, + KakaoPoint gatheringPlacePoint, + String gatheringPlaceDetail, + // TODO: apply datetime parser + OffsetDateTime gatheringAt, + Integer capacity +) { + + public CreateMateRequest { + MateValidator.notNull(postId, MateErrorCode.NULL_POST); + MateValidator.notBlank(depth1, MapErrorCode.BLANK_DEPTH1); + MateValidator.notNull(depth2, MapErrorCode.NULL_DEPTH2); + MateValidator.notBlank(depth3, MapErrorCode.BLANK_DEPTH3); + MateValidator.notNull(depth4, MapErrorCode.NULL_DEPTH4); + MateValidator.validateTitle(title); + MateValidator.notBlank(content, MateErrorCode.BLANK_CONTENT); + MateValidator.notNull(gatheringPlacePoint, MapErrorCode.NULL_KAKAO_POINT); + MateValidator.notBlank(gatheringPlaceDetail, MateErrorCode.BLANK_DETAIL); + MateValidator.validateGatheringAt(gatheringAt); + MateValidator.validateCapacity(capacity); + } + +} diff --git a/src/main/java/team/silvertown/masil/mate/dto/response/CreateMateResponse.java b/src/main/java/team/silvertown/masil/mate/dto/response/CreateMateResponse.java new file mode 100644 index 00000000..d7be7aa4 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/dto/response/CreateMateResponse.java @@ -0,0 +1,5 @@ +package team.silvertown.masil.mate.dto.response; + +public record CreateMateResponse(Long id) { + +} diff --git a/src/main/java/team/silvertown/masil/mate/dto/response/GatheringResponse.java b/src/main/java/team/silvertown/masil/mate/dto/response/GatheringResponse.java new file mode 100644 index 00000000..6687a9ef --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/dto/response/GatheringResponse.java @@ -0,0 +1,23 @@ +package team.silvertown.masil.mate.dto.response; + +import java.time.OffsetDateTime; +import lombok.Builder; +import team.silvertown.masil.common.map.KakaoPoint; +import team.silvertown.masil.mate.domain.Gathering; + +@Builder +public record GatheringResponse( + KakaoPoint point, + String detail, + OffsetDateTime gatherAt +) { + + public static GatheringResponse from(Gathering gathering) { + return GatheringResponse.builder() + .point(gathering.getKakaoPoint()) + .detail(gathering.getDetail()) + .gatherAt(gathering.getGatheringAt()) + .build(); + } + +} diff --git a/src/main/java/team/silvertown/masil/mate/dto/response/MateDetailResponse.java b/src/main/java/team/silvertown/masil/mate/dto/response/MateDetailResponse.java new file mode 100644 index 00000000..71a52d4e --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/dto/response/MateDetailResponse.java @@ -0,0 +1,40 @@ +package team.silvertown.masil.mate.dto.response; + +import java.time.OffsetDateTime; +import java.util.List; +import lombok.Builder; +import team.silvertown.masil.common.map.KakaoPoint; +import team.silvertown.masil.mate.domain.Mate; + +@Builder +public record MateDetailResponse( + Long id, + String title, + String content, + KakaoPoint gatheringPlacePoint, + String gatheringPlaceDetail, + OffsetDateTime gatheringAt, + List participants, + Integer capacity, + Long authorId, + String authorNickname, + String authorProfileUrl +) { + + public static MateDetailResponse from(Mate mate, List participants) { + return MateDetailResponse.builder() + .id(mate.getId()) + .title(mate.getTitle()) + .content(mate.getContent()) + .gatheringPlacePoint(mate.getGatheringPlacePoint()) + .gatheringPlaceDetail(mate.getGatheringPlaceDetail()) + .gatheringAt(mate.getGatheringAt()) + .participants(participants) + .capacity(mate.getCapacity()) + .authorId(mate.getAuthor().getId()) + .authorNickname(mate.getAuthor().getNickname()) + .authorProfileUrl(mate.getAuthor().getProfileImg()) + .build(); + } + +} diff --git a/src/main/java/team/silvertown/masil/mate/dto/response/ParticipantResponse.java b/src/main/java/team/silvertown/masil/mate/dto/response/ParticipantResponse.java new file mode 100644 index 00000000..43c15ac0 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/dto/response/ParticipantResponse.java @@ -0,0 +1,24 @@ +package team.silvertown.masil.mate.dto.response; + +import lombok.Builder; +import team.silvertown.masil.mate.domain.MateParticipant; +import team.silvertown.masil.mate.domain.ParticipantStatus; + +@Builder +public record ParticipantResponse( + Long id, + String nickname, + String profileUrl, + ParticipantStatus status +) { + + public static ParticipantResponse from(MateParticipant participant) { + return ParticipantResponse.builder() + .id(participant.getId()) + .nickname(participant.getUser().getNickname()) + .profileUrl(participant.getUser().getProfileImg()) + .status(participant.getStatus()) + .build(); + } + +} diff --git a/src/main/java/team/silvertown/masil/mate/exception/MateErrorCode.java b/src/main/java/team/silvertown/masil/mate/exception/MateErrorCode.java index 3af5b7c4..08de2e77 100644 --- a/src/main/java/team/silvertown/masil/mate/exception/MateErrorCode.java +++ b/src/main/java/team/silvertown/masil/mate/exception/MateErrorCode.java @@ -19,12 +19,17 @@ public enum MateErrorCode implements ErrorCode { GATHER_AT_PAST(14000, "메이트 모집 시간은 현재 이후여야 합니다"), - NULL_MATE(20001, "해당 참여자의 메이트를 확인할 수 없습니다"), + NULL_MATE(20000, "해당 참여자의 메이트를 확인할 수 없습니다"), + MATE_NOT_FOUND(20400, "해당 아이디의 메이트가 존재하지 않습니다"), - NULL_POST(30000, "메이트 모집의 산책로 포스트를 확인할 수 없습니다"), + OVERGENERATION_IN_SIMILAR_TIME(30000, "비슷한 시간대에 참여하는 메이트가 있습니다"), + + NULL_POST(40000, "메이트 모집의 산책로 포스트를 확인할 수 없습니다"), + POST_NOT_FOUND(40400, "기존의 산책로 포스트를 찾을 수 없습니다"), NULL_AUTHOR(90000, "메이트 모집의 작성자를 확인할 수 없습니다"), - NULL_USER(90001, "해당 메이트 참여자를 확인할 수 없습니다"); + NULL_USER(90001, "해당 메이트 참여자를 확인할 수 없습니다"), + USER_NOT_FOUND(90400, "사용자가 존재하지 않습니다"); private static final int MATE_ERROR_CODE_PREFIX = 300_00000; diff --git a/src/main/java/team/silvertown/masil/mate/repository/mate/MateQueryRepository.java b/src/main/java/team/silvertown/masil/mate/repository/mate/MateQueryRepository.java new file mode 100644 index 00000000..160aebd6 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/repository/mate/MateQueryRepository.java @@ -0,0 +1,10 @@ +package team.silvertown.masil.mate.repository.mate; + +import java.util.Optional; +import team.silvertown.masil.mate.domain.Mate; + +public interface MateQueryRepository { + + Optional findDetailById(Long id); + +} diff --git a/src/main/java/team/silvertown/masil/mate/repository/mate/MateQueryRepositoryImpl.java b/src/main/java/team/silvertown/masil/mate/repository/mate/MateQueryRepositoryImpl.java new file mode 100644 index 00000000..da481505 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/repository/mate/MateQueryRepositoryImpl.java @@ -0,0 +1,30 @@ +package team.silvertown.masil.mate.repository.mate; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import team.silvertown.masil.mate.domain.Mate; +import team.silvertown.masil.mate.domain.QMate; +import team.silvertown.masil.user.domain.QUser; + +@Repository +@RequiredArgsConstructor +public class MateQueryRepositoryImpl implements MateQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + private final QMate mate = QMate.mate; + + @Override + public Optional findDetailById(Long id) { + QUser author = new QUser("author"); + + return Optional.ofNullable(jpaQueryFactory + .selectFrom(mate) + .join(mate.author, author) + .fetchJoin() + .where(mate.id.eq(id)) + .fetchFirst()); + } + +} diff --git a/src/main/java/team/silvertown/masil/mate/repository/mate/MateRepository.java b/src/main/java/team/silvertown/masil/mate/repository/mate/MateRepository.java new file mode 100644 index 00000000..04a046a0 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/repository/mate/MateRepository.java @@ -0,0 +1,8 @@ +package team.silvertown.masil.mate.repository.mate; + +import org.springframework.data.jpa.repository.JpaRepository; +import team.silvertown.masil.mate.domain.Mate; + +public interface MateRepository extends JpaRepository, MateQueryRepository { + +} diff --git a/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantQueryRepository.java b/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantQueryRepository.java new file mode 100644 index 00000000..9f6eeb7a --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantQueryRepository.java @@ -0,0 +1,15 @@ +package team.silvertown.masil.mate.repository.participant; + +import java.time.OffsetDateTime; +import java.util.List; +import team.silvertown.masil.mate.domain.Mate; +import team.silvertown.masil.mate.domain.MateParticipant; +import team.silvertown.masil.user.domain.User; + +public interface MateParticipantQueryRepository { + + boolean existsInSimilarTime(User user, OffsetDateTime gatheringAt); + + List findAllByMate(Mate mate); + +} diff --git a/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantQueryRepositoryImpl.java b/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantQueryRepositoryImpl.java new file mode 100644 index 00000000..f12a38e2 --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantQueryRepositoryImpl.java @@ -0,0 +1,56 @@ +package team.silvertown.masil.mate.repository.participant; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.OffsetDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import team.silvertown.masil.mate.domain.Mate; +import team.silvertown.masil.mate.domain.MateParticipant; +import team.silvertown.masil.mate.domain.ParticipantStatus; +import team.silvertown.masil.mate.domain.QMate; +import team.silvertown.masil.mate.domain.QMateParticipant; +import team.silvertown.masil.user.domain.QUser; +import team.silvertown.masil.user.domain.User; + +@Repository +@RequiredArgsConstructor +public class MateParticipantQueryRepositoryImpl implements MateParticipantQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + private final QMateParticipant mateParticipant = QMateParticipant.mateParticipant; + + @Override + public boolean existsInSimilarTime(User user, OffsetDateTime gatheringAt) { + QMate mate = QMate.mate; + OffsetDateTime beforeHour = gatheringAt.minusHours(1); + OffsetDateTime afterHour = gatheringAt.plusHours(1); + BooleanBuilder condition = new BooleanBuilder() + .and(mateParticipant.user.eq(user)) + .and(mateParticipant.status.eq(ParticipantStatus.ACCEPTED)) + .and(mate.gathering.gatheringAt.between(beforeHour, afterHour)); + + List participants = jpaQueryFactory + .selectFrom(mateParticipant) + .join(mateParticipant.mate, mate) + .where(condition) + .limit(1) + .fetch(); + + return !participants.isEmpty(); + } + + @Override + public List findAllByMate(Mate mate) { + QUser user = QUser.user; + + return jpaQueryFactory + .selectFrom(mateParticipant) + .join(mateParticipant.user, user) + .fetchJoin() + .where(mateParticipant.mate.eq(mate)) + .fetch(); + } + +} diff --git a/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantRepository.java b/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantRepository.java new file mode 100644 index 00000000..abdb859e --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/repository/participant/MateParticipantRepository.java @@ -0,0 +1,9 @@ +package team.silvertown.masil.mate.repository.participant; + +import org.springframework.data.jpa.repository.JpaRepository; +import team.silvertown.masil.mate.domain.MateParticipant; + +public interface MateParticipantRepository extends JpaRepository, + MateParticipantQueryRepository { + +} diff --git a/src/main/java/team/silvertown/masil/mate/service/MateService.java b/src/main/java/team/silvertown/masil/mate/service/MateService.java new file mode 100644 index 00000000..b6741acb --- /dev/null +++ b/src/main/java/team/silvertown/masil/mate/service/MateService.java @@ -0,0 +1,103 @@ +package team.silvertown.masil.mate.service; + +import java.util.List; +import java.util.function.Supplier; +import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import team.silvertown.masil.common.exception.BadRequestException; +import team.silvertown.masil.common.exception.DataNotFoundException; +import team.silvertown.masil.common.exception.ErrorCode; +import team.silvertown.masil.common.map.KakaoPointMapper; +import team.silvertown.masil.mate.domain.Mate; +import team.silvertown.masil.mate.domain.MateParticipant; +import team.silvertown.masil.mate.domain.ParticipantStatus; +import team.silvertown.masil.mate.dto.request.CreateMateRequest; +import team.silvertown.masil.mate.dto.response.CreateMateResponse; +import team.silvertown.masil.mate.dto.response.MateDetailResponse; +import team.silvertown.masil.mate.dto.response.ParticipantResponse; +import team.silvertown.masil.mate.exception.MateErrorCode; +import team.silvertown.masil.mate.repository.mate.MateRepository; +import team.silvertown.masil.mate.repository.participant.MateParticipantRepository; +import team.silvertown.masil.post.domain.Post; +import team.silvertown.masil.post.repository.PostRepository; +import team.silvertown.masil.user.domain.User; +import team.silvertown.masil.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class MateService { + + private final MateRepository mateRepository; + private final MateParticipantRepository mateParticipantRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + + @Transactional + public CreateMateResponse create(Long userId, CreateMateRequest request) { + User author = userRepository.findById(userId) + .orElseThrow(getNotFoundException(MateErrorCode.USER_NOT_FOUND)); + boolean isParticipating = mateParticipantRepository.existsInSimilarTime(author, + request.gatheringAt()); + + if (isParticipating) { + throw new BadRequestException(MateErrorCode.OVERGENERATION_IN_SIMILAR_TIME); + } + + Post post = postRepository.findById(request.postId()) + .orElseThrow(getNotFoundException(MateErrorCode.POST_NOT_FOUND)); + Mate mate = createMate(author, post, request); + + createMateParticipant(author, mate, ParticipantStatus.ACCEPTED.name()); + + return new CreateMateResponse(mate.getId()); + } + + @Transactional(readOnly = true) + public MateDetailResponse getDetailById(Long id) { + Mate mate = mateRepository.findDetailById(id) + .orElseThrow(getNotFoundException(MateErrorCode.MATE_NOT_FOUND)); + List participants = mateParticipantRepository.findAllByMate(mate) + .stream() + .map(ParticipantResponse::from) + .toList(); + + return MateDetailResponse.from(mate, participants); + } + + private Supplier getNotFoundException(ErrorCode errorCode) { + return () -> new DataNotFoundException(errorCode); + } + + private Mate createMate(User author, Post post, CreateMateRequest request) { + Point point = KakaoPointMapper.mapToPoint(request.gatheringPlacePoint()); + Mate mate = Mate.builder() + .author(author) + .post(post) + .depth1(request.depth1()) + .depth2(request.depth2()) + .depth3(request.depth3()) + .depth4(request.depth4()) + .title(request.title()) + .content(request.content()) + .gatheringPlacePoint(point) + .gatheringPlaceDetail(request.gatheringPlaceDetail()) + .gatheringAt(request.gatheringAt()) + .capacity(request.capacity()) + .build(); + + return mateRepository.save(mate); + } + + private void createMateParticipant(User user, Mate mate, String status) { + MateParticipant mateParticipant = MateParticipant.builder() + .user(user) + .mate(mate) + .status(status) + .build(); + + mateParticipantRepository.save(mateParticipant); + } + +} diff --git a/src/main/java/team/silvertown/masil/post/controller/PostController.java b/src/main/java/team/silvertown/masil/post/controller/PostController.java index f834086b..7558fbdc 100644 --- a/src/main/java/team/silvertown/masil/post/controller/PostController.java +++ b/src/main/java/team/silvertown/masil/post/controller/PostController.java @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; import java.net.URI; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -18,11 +19,12 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import team.silvertown.masil.common.response.ScrollResponse; +import team.silvertown.masil.common.scroll.OrderType; +import team.silvertown.masil.common.scroll.dto.NormalListRequest; +import team.silvertown.masil.common.scroll.dto.ScrollResponse; import team.silvertown.masil.post.dto.request.CreatePostRequest; -import team.silvertown.masil.post.dto.request.NormalListRequest; -import team.silvertown.masil.post.dto.request.PostOrderType; import team.silvertown.masil.post.dto.response.CreatePostResponse; import team.silvertown.masil.post.dto.response.PostDetailResponse; import team.silvertown.masil.post.dto.response.SimplePostResponse; @@ -102,7 +104,7 @@ public ResponseEntity getById( @Parameter( name = "order", in = ParameterIn.QUERY, - schema = @Schema(implementation = PostOrderType.class, defaultValue = "LATEST") + schema = @Schema(implementation = OrderType.class, defaultValue = "LATEST") ), @Parameter( name = "cursor", @@ -117,16 +119,29 @@ public ResponseEntity getById( } ) @SecurityRequirements() - public ResponseEntity> getSliceByAddress( + public ResponseEntity> getScrollBy( @AuthenticationPrincipal - Long userId, + Long loginId, + @RequestParam(required = false) + Long authorId, @Parameter(hidden = true) NormalListRequest request ) { - ScrollResponse response = postService.getSliceByAddress(userId, - request); + ScrollResponse response = getScrollResponse(loginId, authorId, request); return ResponseEntity.ok(response); } + private ScrollResponse getScrollResponse( + Long loginId, + Long authorId, + NormalListRequest request + ) { + if (Objects.isNull(authorId)) { + return postService.getScrollByAddress(loginId, request); + } + + return postService.getScrollByAuthor(loginId, authorId, request.getScrollRequest()); + } + } diff --git a/src/main/java/team/silvertown/masil/post/dto/request/NormalListRequest.java b/src/main/java/team/silvertown/masil/post/dto/request/NormalListRequest.java deleted file mode 100644 index 7b559f6b..00000000 --- a/src/main/java/team/silvertown/masil/post/dto/request/NormalListRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package team.silvertown.masil.post.dto.request; - -import lombok.Builder; -import team.silvertown.masil.common.map.MapErrorCode; -import team.silvertown.masil.post.validator.PostValidator; - -@Builder -public record NormalListRequest( - String depth1, - String depth2, - String depth3, - PostOrderType order, - String cursor, - int size -) { - - public NormalListRequest { - PostValidator.notBlank(depth1, MapErrorCode.BLANK_DEPTH1); - PostValidator.notNull(depth2, MapErrorCode.NULL_DEPTH2); - PostValidator.notBlank(depth3, MapErrorCode.BLANK_DEPTH3); - PostValidator.validateCursorFormat(cursor, order); - } - -} diff --git a/src/main/java/team/silvertown/masil/post/dto/request/PostOrderType.java b/src/main/java/team/silvertown/masil/post/dto/request/PostOrderType.java deleted file mode 100644 index 20c6b323..00000000 --- a/src/main/java/team/silvertown/masil/post/dto/request/PostOrderType.java +++ /dev/null @@ -1,27 +0,0 @@ -package team.silvertown.masil.post.dto.request; - -import io.micrometer.common.util.StringUtils; -import java.util.Arrays; -import java.util.Objects; -import team.silvertown.masil.common.exception.BadRequestException; -import team.silvertown.masil.post.exception.PostErrorCode; - -public enum PostOrderType { - LATEST, - MOST_POPULAR; - - public static PostOrderType get(String order) { - if (StringUtils.isBlank(order)) { - return LATEST; - } - - return Arrays.stream(PostOrderType.values()) - .filter(orderType -> order.equals(orderType.name())) - .findFirst() - .orElseThrow(() -> new BadRequestException(PostErrorCode.INVALID_ORDER_TYPE)); - } - - public static boolean isMostPopular(PostOrderType postOrderType) { - return Objects.isNull(postOrderType) || postOrderType == MOST_POPULAR; - } -} diff --git a/src/main/java/team/silvertown/masil/post/exception/PostErrorCode.java b/src/main/java/team/silvertown/masil/post/exception/PostErrorCode.java index e128565b..42da7ab7 100644 --- a/src/main/java/team/silvertown/masil/post/exception/PostErrorCode.java +++ b/src/main/java/team/silvertown/masil/post/exception/PostErrorCode.java @@ -1,8 +1,10 @@ package team.silvertown.masil.post.exception; import lombok.Getter; +import lombok.RequiredArgsConstructor; import team.silvertown.masil.common.exception.ErrorCode; +@RequiredArgsConstructor @Getter public enum PostErrorCode implements ErrorCode { NULL_IS_PUBLIC(20210000, "산책로 포스트의 공개 여부를 확인할 수 없습니다"), @@ -10,8 +12,6 @@ public enum PostErrorCode implements ErrorCode { THUMBNAIL_URL_TOO_LONG(20211000, "산책로 포스트 썸네일 URL 주소 길이가 제한을 초과했습니다"), TITLE_TOO_LONG(20211001, "산책로 포스트 제목 길이가 제한을 초과했습니다"), BLANK_TITLE(20211002, "산책로 포스트 제목이 입력되지 않았습니다"), - INVALID_ORDER_TYPE(20211003, "올바르지 않은 정렬 기준입니다"), - INVALID_CURSOR_FORMAT(20211004, "정렬 기준에 맞지 않은 커서 형식입니다"), NON_POSITIVE_DISTANCE(20212000, "산책로 포스트의 산책 거리는 양수여야 합니다"), NON_POSITIVE_TOTAL_TIME(20212001, "산책로 포스트의 산책 시간은 양수여야 합니다"), @@ -22,14 +22,9 @@ public enum PostErrorCode implements ErrorCode { PIN_OWNER_NOT_MATCHING(20230300, "산책로 포스트의 사용자와 핀의 사용자가 다릅니다"), NULL_USER(20290000, "산책로 포스트 사용자를 확인할 수 없습니다"), - USER_NOT_FOUND(20190400, "로그인한 사용자가 존재하지 않습니다"); + LOGIN_USER_NOT_FOUND(20290400, "로그인한 사용자가 존재하지 않습니다"), + AUTHOR_NOT_FOUND(20290401, "산책로 포스트의 작성자를 찾을 수 없습니다"); private final int code; private final String message; - - PostErrorCode(int code, String message) { - this.code = code; - this.message = message; - } - } diff --git a/src/main/java/team/silvertown/masil/post/repository/PostQueryRepository.java b/src/main/java/team/silvertown/masil/post/repository/PostQueryRepository.java index f08aa463..6f7afb67 100644 --- a/src/main/java/team/silvertown/masil/post/repository/PostQueryRepository.java +++ b/src/main/java/team/silvertown/masil/post/repository/PostQueryRepository.java @@ -1,12 +1,15 @@ package team.silvertown.masil.post.repository; import java.util.List; +import team.silvertown.masil.common.scroll.dto.NormalListRequest; +import team.silvertown.masil.common.scroll.dto.ScrollRequest; import team.silvertown.masil.post.dto.PostCursorDto; -import team.silvertown.masil.post.dto.request.NormalListRequest; import team.silvertown.masil.user.domain.User; public interface PostQueryRepository { - List findSliceBy(User user, NormalListRequest request); + List findScrollByAddress(User user, NormalListRequest request); + + List findScrollByUser(User loginUser, User author, ScrollRequest request); } diff --git a/src/main/java/team/silvertown/masil/post/repository/PostQueryRepositoryImpl.java b/src/main/java/team/silvertown/masil/post/repository/PostQueryRepositoryImpl.java index 9936c0a6..a0b35ec9 100644 --- a/src/main/java/team/silvertown/masil/post/repository/PostQueryRepositoryImpl.java +++ b/src/main/java/team/silvertown/masil/post/repository/PostQueryRepositoryImpl.java @@ -4,6 +4,7 @@ import com.querydsl.core.types.ConstructorExpression; import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; @@ -12,12 +13,14 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import io.micrometer.common.util.StringUtils; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import team.silvertown.masil.common.scroll.OrderType; +import team.silvertown.masil.common.scroll.dto.NormalListRequest; +import team.silvertown.masil.common.scroll.dto.ScrollRequest; import team.silvertown.masil.post.domain.QPost; import team.silvertown.masil.post.dto.PostCursorDto; -import team.silvertown.masil.post.dto.request.NormalListRequest; -import team.silvertown.masil.post.dto.request.PostOrderType; import team.silvertown.masil.post.dto.response.SimplePostResponse; import team.silvertown.masil.user.domain.User; @@ -33,43 +36,75 @@ public class PostQueryRepositoryImpl implements PostQueryRepository { private final QPost post = QPost.post; @Override - public List findSliceBy(User user, NormalListRequest request) { + public List findScrollByAddress(User user, NormalListRequest request) { // TODO: 좋아요 구현 후 로그인한 사용자 본인이 좋아요한 포스트인지 쿼리 추가 - BooleanExpression cursorCondition = getCursorFilter(request.order(), request.cursor()); - BooleanBuilder condition = new BooleanBuilder() + Predicate openness = getOpenness(user, null); + BooleanBuilder condition = getBasicCondition(request.getScrollRequest(), openness); + + if (request.isBasedOnAddress()) { + condition + .and(post.address.depth1.eq(request.getDepth1())) + .and(post.address.depth2.eq(request.getDepth2())) + .and(post.address.depth3.eq(request.getDepth3())); + } + + return queryPostsWith(user, condition, request.getScrollRequest()); + } + + @Override + public List findScrollByUser( + User loginUser, + User author, + ScrollRequest request + ) { + // TODO: 좋아요 구현 후 로그인한 사용자 본인이 좋아요한 포스트인지 쿼리 추가 + Predicate openness = getOpenness(loginUser, author); + BooleanBuilder condition = getBasicCondition(request, openness) + .and(post.user.eq(author)); + + return queryPostsWith(loginUser, condition, request); + } + + private Predicate getOpenness(User loginUser, User author) { + if (Objects.nonNull(loginUser) && Objects.equals(loginUser, author)) { + return null; + } + + return post.isPublic.isTrue(); + } + + private BooleanBuilder getBasicCondition(ScrollRequest request, Predicate openness) { + BooleanExpression cursorCondition = getCursorFilter(request.getOrder(), + request.getCursor()); + + return new BooleanBuilder() .and(cursorCondition) - .and(post.isPublic.isTrue()) - .and(post.address.depth1.eq(request.depth1())) - .and(post.address.depth2.eq(request.depth2())) - .and(post.address.depth3.eq(request.depth3())); - OrderSpecifier orderTarget = decideOrderTarget(request.order()); - StringExpression cursor = getCursor(request.order()); - - return queryPostsWith(user, cursor, condition, orderTarget, request.size()); + .and(openness); } private List queryPostsWith( User user, - StringExpression cursor, BooleanBuilder condition, - OrderSpecifier orderTarget, - int size + ScrollRequest request ) { + OrderSpecifier orderTarget = decideOrderTarget(request.getOrder()); + StringExpression cursor = getCursor(request.getOrder()); + return jpaQueryFactory .select(projectPostCursor(cursor)) .from(post) .where(condition) .orderBy(orderTarget, post.id.desc()) - .limit(size + 1) + .limit(request.getSize() + 1) .fetch(); } - private BooleanExpression getCursorFilter(PostOrderType order, String cursor) { + private BooleanExpression getCursorFilter(OrderType order, String cursor) { if (StringUtils.isBlank(cursor)) { return null; } - if (PostOrderType.isMostPopular(order)) { + if (OrderType.isMostPopular(order)) { StringExpression toScan = getCursor(order); return toScan.lt(cursor); @@ -84,8 +119,8 @@ private BooleanExpression getCursorFilter(PostOrderType order, String cursor) { return post.id.lt(idCursor); } - private OrderSpecifier decideOrderTarget(PostOrderType order) { - if (PostOrderType.isMostPopular(order)) { + private OrderSpecifier decideOrderTarget(OrderType order) { + if (OrderType.isMostPopular(order)) { return post.likeCount.desc(); } @@ -93,8 +128,8 @@ private OrderSpecifier decideOrderTarget(PostOrderType order) { } - private StringExpression getCursor(PostOrderType order) { - if (PostOrderType.isMostPopular(order)) { + private StringExpression getCursor(OrderType order) { + if (OrderType.isMostPopular(order)) { return StringExpressions.lpad(post.likeCount.stringValue(), LIKE_COUNT_CURSOR_LENGTH, PADDING) .concat(StringExpressions.lpad(post.id.stringValue(), ID_CURSOR_LENGTH, PADDING)); diff --git a/src/main/java/team/silvertown/masil/post/service/PostService.java b/src/main/java/team/silvertown/masil/post/service/PostService.java index 8002121c..45c31606 100644 --- a/src/main/java/team/silvertown/masil/post/service/PostService.java +++ b/src/main/java/team/silvertown/masil/post/service/PostService.java @@ -9,13 +9,14 @@ import team.silvertown.masil.common.exception.DataNotFoundException; import team.silvertown.masil.common.exception.ErrorCode; import team.silvertown.masil.common.map.KakaoPointMapper; -import team.silvertown.masil.common.response.ScrollResponse; +import team.silvertown.masil.common.scroll.dto.NormalListRequest; +import team.silvertown.masil.common.scroll.dto.ScrollRequest; +import team.silvertown.masil.common.scroll.dto.ScrollResponse; import team.silvertown.masil.post.domain.Post; import team.silvertown.masil.post.domain.PostPin; import team.silvertown.masil.post.dto.PostCursorDto; import team.silvertown.masil.post.dto.request.CreatePostPinRequest; import team.silvertown.masil.post.dto.request.CreatePostRequest; -import team.silvertown.masil.post.dto.request.NormalListRequest; import team.silvertown.masil.post.dto.response.CreatePostResponse; import team.silvertown.masil.post.dto.response.PostDetailResponse; import team.silvertown.masil.post.dto.response.PostPinDetailResponse; @@ -37,7 +38,7 @@ public class PostService { @Transactional public CreatePostResponse create(Long userId, CreatePostRequest request) { User user = userRepository.findById(userId) - .orElseThrow(throwNotFound(PostErrorCode.USER_NOT_FOUND)); + .orElseThrow(getNotFoundException(PostErrorCode.LOGIN_USER_NOT_FOUND)); Post post = createPost(request, user); savePins(request.pins(), post); @@ -48,24 +49,39 @@ public CreatePostResponse create(Long userId, CreatePostRequest request) { @Transactional(readOnly = true) public PostDetailResponse getById(Long id) { Post post = postRepository.findById(id) - .orElseThrow(throwNotFound(PostErrorCode.POST_NOT_FOUND)); + .orElseThrow(getNotFoundException(PostErrorCode.POST_NOT_FOUND)); List pins = PostPinDetailResponse.listFrom(post); return PostDetailResponse.from(post, pins); } @Transactional(readOnly = true) - public ScrollResponse getSliceByAddress( - Long userId, + public ScrollResponse getScrollByAddress( + Long loginId, NormalListRequest request ) { - User user = getUserIfLoggedIn(userId); - List postsWithCursor = postRepository.findSliceBy(user, request); + User user = getUserIfLoggedIn(loginId); + List postsWithCursor = postRepository.findScrollByAddress(user, request); + + return getScrollResponse(postsWithCursor, request.getSize()); + } + + @Transactional(readOnly = true) + public ScrollResponse getScrollByAuthor( + Long loginId, + Long userId, + ScrollRequest request + ) { + User author = userRepository.findById(userId) + .orElseThrow(getNotFoundException(PostErrorCode.AUTHOR_NOT_FOUND)); + User loginUser = getUserIfLoggedIn(loginId); + List postsWithCursor = postRepository.findScrollByUser(loginUser, author, + request); - return getScrollResponse(postsWithCursor, request.size()); + return getScrollResponse(postsWithCursor, request.getSize()); } - private Supplier throwNotFound(ErrorCode errorCode) { + private Supplier getNotFoundException(ErrorCode errorCode) { return () -> new DataNotFoundException(errorCode); } @@ -118,7 +134,7 @@ private User getUserIfLoggedIn(Long userId) { } return userRepository.findById(userId) - .orElseThrow(throwNotFound(PostErrorCode.USER_NOT_FOUND)); + .orElseThrow(getNotFoundException(PostErrorCode.LOGIN_USER_NOT_FOUND)); } private ScrollResponse getScrollResponse( diff --git a/src/main/java/team/silvertown/masil/post/validator/PostValidator.java b/src/main/java/team/silvertown/masil/post/validator/PostValidator.java index 2b2cc19b..c4fa60aa 100644 --- a/src/main/java/team/silvertown/masil/post/validator/PostValidator.java +++ b/src/main/java/team/silvertown/masil/post/validator/PostValidator.java @@ -7,7 +7,6 @@ import team.silvertown.masil.common.exception.ForbiddenException; import team.silvertown.masil.common.validator.Validator; import team.silvertown.masil.post.domain.Post; -import team.silvertown.masil.post.dto.request.PostOrderType; import team.silvertown.masil.post.exception.PostErrorCode; import team.silvertown.masil.user.domain.User; @@ -16,7 +15,6 @@ public final class PostValidator extends Validator { private static final int MAX_TITLE_LENGTH = 30; private static final int MAX_URL_LENGTH = 1024; - private static final int ID_CURSOR_LENGTH = 11; public static void validateTitle(String title) { notBlank(title, PostErrorCode.BLANK_TITLE); @@ -36,17 +34,4 @@ public static void validatePinOwner(Post post, Long userId) { () -> new ForbiddenException(PostErrorCode.PIN_OWNER_NOT_MATCHING)); } - public static void validateCursorFormat(String cursor, PostOrderType order) { - if (StringUtils.isBlank(cursor)) { - return; - } - - if (PostOrderType.isMostPopular(order)) { - notUnder(cursor.length(), ID_CURSOR_LENGTH, PostErrorCode.INVALID_CURSOR_FORMAT); - return; - } - - notOver(cursor.length(), ID_CURSOR_LENGTH, PostErrorCode.INVALID_CURSOR_FORMAT); - } - } diff --git a/src/main/java/team/silvertown/masil/user/controller/UserController.java b/src/main/java/team/silvertown/masil/user/controller/UserController.java index 31fe7743..10ca8aa6 100644 --- a/src/main/java/team/silvertown/masil/user/controller/UserController.java +++ b/src/main/java/team/silvertown/masil/user/controller/UserController.java @@ -9,21 +9,27 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +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.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import team.silvertown.masil.user.dto.LoginResponse; import team.silvertown.masil.user.dto.MeInfoResponse; +import team.silvertown.masil.user.dto.MyPageInfoResponse; import team.silvertown.masil.user.dto.NicknameCheckResponse; import team.silvertown.masil.user.dto.OnboardRequest; import team.silvertown.masil.user.dto.UpdateRequest; +import team.silvertown.masil.user.dto.UpdateResponse; import team.silvertown.masil.user.service.UserService; @RestController @@ -45,7 +51,7 @@ public ResponseEntity onboard( @AuthenticationPrincipal Long userId ) { - userService.onboard(userId, request); + userService.onboard(request, userId); return ResponseEntity.noContent() .build(); @@ -81,7 +87,6 @@ public ResponseEntity getMyInfo( } @PostMapping("/api/v1/users/login") - @SecurityRequirement(name = "토큰 받아오기") @Operation(summary = "카카오 토큰으로 로그인") @ApiResponse( responseCode = "200", @@ -98,6 +103,28 @@ public ResponseEntity login( return ResponseEntity.ok(userService.login(accessToken)); } + @PutMapping( + value = "api/v1/users/profiles", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Operation(summary = "유저 프로필 사진 업데이트") + @ApiResponse( + responseCode = "204", + description = "프로필 사진을 보내 유저 프로필 사진 변경" + ) + public ResponseEntity profileUpdate( + @RequestPart + MultipartFile profileImg, + @AuthenticationPrincipal + Long userId + ) { + userService.updateProfile(profileImg, userId); + + return ResponseEntity.noContent() + .build(); + } + @PatchMapping("/api/v1/users/is-public") @Operation(summary = "계정 공개여부 변경") @ApiResponse( @@ -115,16 +142,41 @@ public ResponseEntity changePublic( } @PutMapping("/api/v1/users") - public ResponseEntity updateInfo( + @Operation(summary = "유저 정보 업데이트 요청") + @ApiResponse( + responseCode = "200", + description = "유저 정보 업데이트 요청 성공 후 바뀐 콘텐츠를 확인한다", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UpdateResponse.class) + ) + ) + public ResponseEntity updateInfo( @RequestBody UpdateRequest updateRequest, @AuthenticationPrincipal Long memberId ) { - userService.updateInfo(memberId, updateRequest); + return ResponseEntity.ok(userService.updateInfo(memberId, updateRequest)); + } - return ResponseEntity.noContent() - .build(); + @GetMapping("api/v1/users/{userId}") + @Operation(summary = "유저 마이페이지 조회") + @ApiResponse( + responseCode = "200", + description = "유저의 마이페이지 정보 조회", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MyPageInfoResponse.class) + ) + ) + public ResponseEntity getMyPage( + @PathVariable + Long userId, + @AuthenticationPrincipal + Long loginId + ) { + return ResponseEntity.ok(userService.getMyPageInfo(userId, loginId)); } } diff --git a/src/main/java/team/silvertown/masil/user/domain/ExerciseIntensity.java b/src/main/java/team/silvertown/masil/user/domain/ExerciseIntensity.java index 656aebd4..088ba462 100644 --- a/src/main/java/team/silvertown/masil/user/domain/ExerciseIntensity.java +++ b/src/main/java/team/silvertown/masil/user/domain/ExerciseIntensity.java @@ -1,6 +1,7 @@ package team.silvertown.masil.user.domain; import java.util.Arrays; +import java.util.Objects; import team.silvertown.masil.common.exception.BadRequestException; import team.silvertown.masil.user.exception.UserErrorCode; @@ -12,11 +13,16 @@ public enum ExerciseIntensity { SUPER_HIGH; public static ExerciseIntensity get(String value) { + if (Objects.isNull(value)) { + return null; + } + return Arrays.stream(ExerciseIntensity.values()) .filter(exerciseIntensity -> exerciseIntensity.name() .equals(value)) .findFirst() - .orElseThrow(() -> new BadRequestException(UserErrorCode.INVALID_EXERCISE_INTENSITY)); + .orElseThrow( + () -> new BadRequestException(UserErrorCode.INVALID_EXERCISE_INTENSITY)); } } diff --git a/src/main/java/team/silvertown/masil/user/domain/Sex.java b/src/main/java/team/silvertown/masil/user/domain/Sex.java index 737ec28b..f0e8fe12 100644 --- a/src/main/java/team/silvertown/masil/user/domain/Sex.java +++ b/src/main/java/team/silvertown/masil/user/domain/Sex.java @@ -1,6 +1,7 @@ package team.silvertown.masil.user.domain; import java.util.Arrays; +import java.util.Objects; import lombok.Getter; import team.silvertown.masil.common.exception.BadRequestException; import team.silvertown.masil.user.exception.UserErrorCode; @@ -11,6 +12,10 @@ public enum Sex { FEMALE; public static Sex get(String value) { + if (Objects.isNull(value)) { + return null; + } + return Arrays.stream(Sex.values()) .filter(sex -> sex.name() .equals(value)) diff --git a/src/main/java/team/silvertown/masil/user/domain/User.java b/src/main/java/team/silvertown/masil/user/domain/User.java index 3249ef10..833e5cf5 100644 --- a/src/main/java/team/silvertown/masil/user/domain/User.java +++ b/src/main/java/team/silvertown/masil/user/domain/User.java @@ -83,14 +83,14 @@ public void updateNickname(String nickname) { } public void updateSex(String sex) { - UserValidator.validateSex(sex, UserErrorCode.INVALID_SEX); - this.sex = Sex.valueOf(sex); + Sex validatedSex = Sex.get(sex); + this.sex = validatedSex; } public void updateBirthDate(String birthDate) { UserValidator.validateBirthDate(birthDate, UserErrorCode.INVALID_BIRTH_DATE); - this.birthDate = DateValidator.parseDate(birthDate, - UserErrorCode.INVALID_BIRTH_DATE); + this.birthDate = DateValidator.parseDate(birthDate, UserErrorCode.INVALID_BIRTH_DATE); + } public void updateHeight(Integer height) { @@ -104,8 +104,12 @@ public void updateWeight(Integer weight) { } public void updateExerciseIntensity(String exerciseIntensity) { - UserValidator.validateExerciseIntensity(exerciseIntensity); - this.exerciseIntensity = ExerciseIntensity.valueOf(exerciseIntensity); + ExerciseIntensity validatedIntensity = ExerciseIntensity.get(exerciseIntensity); + this.exerciseIntensity = validatedIntensity; + } + + public void updateProfile(String profileImg) { + this.profileImg = profileImg; } public void toggleIsPublic() { diff --git a/src/main/java/team/silvertown/masil/user/dto/MyPageInfoResponse.java b/src/main/java/team/silvertown/masil/user/dto/MyPageInfoResponse.java new file mode 100644 index 00000000..c86e5925 --- /dev/null +++ b/src/main/java/team/silvertown/masil/user/dto/MyPageInfoResponse.java @@ -0,0 +1,32 @@ +package team.silvertown.masil.user.dto; + +import lombok.Builder; +import team.silvertown.masil.user.domain.User; + +@Builder +public record MyPageInfoResponse( + String nickname, + String profileImg, + Integer totalDistance, + Integer totalCount, + Integer totalCalories +) { + + public static MyPageInfoResponse from(User user) { + return MyPageInfoResponse.builder() + .nickname(user.getNickname()) + .profileImg(user.getProfileImg()) + .totalDistance(user.getTotalDistance()) + .totalCount(user.getTotalCount()) + .totalCalories(user.getTotalCalories()) + .build(); + } + + public static MyPageInfoResponse fromPrivateUser(User user) { + return MyPageInfoResponse.builder() + .nickname(user.getNickname()) + .profileImg(user.getProfileImg()) + .build(); + } + +} diff --git a/src/main/java/team/silvertown/masil/user/dto/NicknameCheckResponse.java b/src/main/java/team/silvertown/masil/user/dto/NicknameCheckResponse.java index b2728300..55894b1e 100644 --- a/src/main/java/team/silvertown/masil/user/dto/NicknameCheckResponse.java +++ b/src/main/java/team/silvertown/masil/user/dto/NicknameCheckResponse.java @@ -3,9 +3,9 @@ import lombok.Builder; @Builder -public record NicknameCheckResponse ( +public record NicknameCheckResponse( Boolean isDuplicated, String nickname -){ +) { } diff --git a/src/main/java/team/silvertown/masil/user/dto/UpdateResponse.java b/src/main/java/team/silvertown/masil/user/dto/UpdateResponse.java new file mode 100644 index 00000000..f18dea01 --- /dev/null +++ b/src/main/java/team/silvertown/masil/user/dto/UpdateResponse.java @@ -0,0 +1,27 @@ +package team.silvertown.masil.user.dto; + +import java.time.LocalDate; +import team.silvertown.masil.user.domain.User; + +public record UpdateResponse( + String nickname, + String sex, + LocalDate birthDate, + int height, + int weight, + String exerciseIntensity +) { + + public static UpdateResponse from(User user) { + return new UpdateResponse( + user.getNickname(), + user.getSex() + .name(), + user.getBirthDate(), + user.getHeight(), + user.getWeight(), + user.getExerciseIntensity() + .name()); + } + +} diff --git a/src/main/java/team/silvertown/masil/user/exception/UserErrorCode.java b/src/main/java/team/silvertown/masil/user/exception/UserErrorCode.java index 26919db7..0527399f 100644 --- a/src/main/java/team/silvertown/masil/user/exception/UserErrorCode.java +++ b/src/main/java/team/silvertown/masil/user/exception/UserErrorCode.java @@ -6,7 +6,7 @@ public enum UserErrorCode implements ErrorCode { INVALID_ALLOWING_MARKETING(10010000, "올바르지 않은 형식의 마케팅 동의입니다."), INVALID_PERSONAL_INFO_CONSENTED(10010001, "올바르지 않은 형식의 개인 정보 이용 동의입니다."), INVALID_LOCATION_INFO_CONSENTED(10010002, "올바르지 않은 형식의 위치 정보 이용 동의 동의입니다."), - INVALID_UNDER_AGE_CONSENTED(10010003, "올바르지 않은 형식의 이용 영령 동의입니다."), + INVALID_UNDER_AGE_CONSENTED(10010003, "올바르지 않은 형식의 이용 연령 동의입니다."), INVALID_NICKNAME(10011000, "올바르지 않은 형식의 닉네임 정보입니다."), INVALID_SEX(10011001, "올바르지 않은 형식의 성별 정보입니다."), INVALID_EXERCISE_INTENSITY(10011002, "올바르지 않은 형식의 운동강도 정보입니다."), diff --git a/src/main/java/team/silvertown/masil/user/repository/UserAgreementRepository.java b/src/main/java/team/silvertown/masil/user/repository/UserAgreementRepository.java index 1e1f69df..11af7e8f 100644 --- a/src/main/java/team/silvertown/masil/user/repository/UserAgreementRepository.java +++ b/src/main/java/team/silvertown/masil/user/repository/UserAgreementRepository.java @@ -1,5 +1,6 @@ package team.silvertown.masil.user.repository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import team.silvertown.masil.user.domain.User; import team.silvertown.masil.user.domain.UserAgreement; @@ -8,4 +9,6 @@ public interface UserAgreementRepository extends JpaRepository findByUser(User user); + } diff --git a/src/main/java/team/silvertown/masil/user/service/UserService.java b/src/main/java/team/silvertown/masil/user/service/UserService.java index 5036dcb6..e8b90540 100644 --- a/src/main/java/team/silvertown/masil/user/service/UserService.java +++ b/src/main/java/team/silvertown/masil/user/service/UserService.java @@ -1,16 +1,21 @@ package team.silvertown.masil.user.service; +import java.net.URI; import java.time.OffsetDateTime; import java.util.List; +import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import team.silvertown.masil.common.exception.DataNotFoundException; import team.silvertown.masil.common.exception.DuplicateResourceException; import team.silvertown.masil.common.validator.Validator; import team.silvertown.masil.config.jwt.JwtTokenProvider; +import team.silvertown.masil.image.service.ImageService; +import team.silvertown.masil.image.validator.ImageFileServiceValidator; import team.silvertown.masil.security.exception.InvalidAuthenticationException; import team.silvertown.masil.user.domain.Authority; import team.silvertown.masil.user.domain.Provider; @@ -19,10 +24,12 @@ import team.silvertown.masil.user.domain.UserAuthority; import team.silvertown.masil.user.dto.LoginResponse; import team.silvertown.masil.user.dto.MeInfoResponse; +import team.silvertown.masil.user.dto.MyPageInfoResponse; import team.silvertown.masil.user.dto.NicknameCheckResponse; import team.silvertown.masil.user.dto.OAuthResponse; import team.silvertown.masil.user.dto.OnboardRequest; import team.silvertown.masil.user.dto.UpdateRequest; +import team.silvertown.masil.user.dto.UpdateResponse; import team.silvertown.masil.user.exception.UserErrorCode; import team.silvertown.masil.user.repository.UserAgreementRepository; import team.silvertown.masil.user.repository.UserAuthorityRepository; @@ -38,6 +45,7 @@ public class UserService { private final UserAuthorityRepository userAuthorityRepository; private final KakaoOAuthService kakaoOAuthService; private final JwtTokenProvider tokenProvider; + private final ImageService imageService; @Transactional public LoginResponse login(String kakaoToken) { @@ -71,11 +79,11 @@ private LoginResponse joinedUserResponse(User joinedUser) { } @Transactional - public void onboard(long userId, OnboardRequest request) { + public void onboard(OnboardRequest request, Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new DataNotFoundException(UserErrorCode.USER_NOT_FOUND)); - checkNickname(request.nickname()); + checkNickname(request.nickname(), user); update(user, request); UserAgreement userAgreement = getUserAgreement(request, user); Validator.throwIf(agreementRepository.existsByUser(user), @@ -96,33 +104,61 @@ public void changePublic(Long memberId) { } @Transactional - public void updateInfo(Long memberId, UpdateRequest updateRequest) { + public UpdateResponse updateInfo(Long memberId, UpdateRequest updateRequest) { User user = userRepository.findById(memberId) .orElseThrow(() -> new DataNotFoundException(UserErrorCode.USER_NOT_FOUND)); - update(user, updateRequest); - user.toggleIsPublic(); + + checkNickname(updateRequest.nickname(), user); + return update(user, updateRequest); } public NicknameCheckResponse checkNickname(String nickname) { boolean isDuplicated = userRepository.existsByNickname(nickname); + return NicknameCheckResponse.builder() .nickname(nickname) .isDuplicated(isDuplicated) .build(); } - public MeInfoResponse getMe(Long memberId) { - User user = userRepository.findById(memberId) + public MyPageInfoResponse getMyPageInfo(Long userId, Long loginId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new DataNotFoundException(UserErrorCode.USER_NOT_FOUND)); + + if (!Objects.equals(loginId, userId) && !user.getIsPublic()) { + return MyPageInfoResponse.fromPrivateUser(user); + } + + return MyPageInfoResponse.from(user); + } + + public MeInfoResponse getMe(Long userId) { + User user = userRepository.findById(userId) .orElseThrow(() -> new DataNotFoundException(UserErrorCode.USER_NOT_FOUND)); return MeInfoResponse.from(user); } - private static UserAuthority generateUserAuthority(User user, Authority authority) { - return UserAuthority.builder() - .authority(authority) - .user(user) - .build(); + @Transactional + public void updateProfile(MultipartFile profileImg, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new DataNotFoundException(UserErrorCode.USER_NOT_FOUND)); + + String profileUrl = getProfileUrl(profileImg); + user.updateProfile(profileUrl); + } + + private String getProfileUrl(MultipartFile profileImg) { + if (Objects.nonNull(profileImg)) { + String profileUrl; + ImageFileServiceValidator.validateImgFile(profileImg); + URI uploadedUri = imageService.upload(profileImg); + profileUrl = uploadedUri.toString(); + + return profileUrl; + } + + return null; } private void updatingAuthority(List authorities, User user) { @@ -145,20 +181,32 @@ private void update(User user, OnboardRequest updateRequest) { user.updateExerciseIntensity(updateRequest.exerciseIntensity()); } - private void update(User user, UpdateRequest updateRequest) { + private UpdateResponse update(User user, UpdateRequest updateRequest) { + checkNickname(updateRequest.nickname(), user); + user.updateNickname(updateRequest.nickname()); user.updateSex(updateRequest.sex()); user.updateBirthDate(updateRequest.birthDate()); user.updateHeight(updateRequest.height()); user.updateWeight(updateRequest.weight()); user.updateExerciseIntensity(updateRequest.exerciseIntensity()); + + return UpdateResponse.from(user); + } + + private void checkNickname(String nickname, User user) { + boolean isDuplicated = userRepository.existsByNickname(nickname); + + if (isDuplicated && !Objects.equals(user.getNickname(), nickname)) { + throw new DuplicateResourceException(UserErrorCode.DUPLICATED_NICKNAME); + } } private UserAgreement getUserAgreement(OnboardRequest request, User user) { OffsetDateTime marketingConsentedAt = request.isAllowingMarketing() ? OffsetDateTime.now() : null; - UserAgreement userAgreement = UserAgreement.builder() + return UserAgreement.builder() .user(user) .isAllowingMarketing(request.isAllowingMarketing()) .isLocationInfoConsented(request.isLocationInfoConsented()) @@ -166,8 +214,6 @@ private UserAgreement getUserAgreement(OnboardRequest request, User user) { .isUnderAgeConsentConfirmed(request.isUnderAgeConsentConfirmed()) .marketingConsentedAt(marketingConsentedAt) .build(); - - return userAgreement; } private User createAndSave(Provider authenticatedProvider, String providerId) { @@ -199,4 +245,11 @@ private List getUserAuthorities(User user) { .toList(); } + private UserAuthority generateUserAuthority(User user, Authority authority) { + return UserAuthority.builder() + .authority(authority) + .user(user) + .build(); + } + } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f57037d6..2929266b 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -6,6 +6,7 @@ logging: orm: jpa: JpaTransactionManager: debug + SharedEntityManagerCreator: debug hibernate: event: internal: diff --git a/src/test/java/team/silvertown/masil/mate/service/MateServiceTest.java b/src/test/java/team/silvertown/masil/mate/service/MateServiceTest.java new file mode 100644 index 00000000..a15e1596 --- /dev/null +++ b/src/test/java/team/silvertown/masil/mate/service/MateServiceTest.java @@ -0,0 +1,199 @@ +package team.silvertown.masil.mate.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.time.OffsetDateTime; +import java.util.List; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import team.silvertown.masil.common.exception.BadRequestException; +import team.silvertown.masil.common.exception.DataNotFoundException; +import team.silvertown.masil.common.map.KakaoPoint; +import team.silvertown.masil.mate.domain.Mate; +import team.silvertown.masil.mate.domain.MateParticipant; +import team.silvertown.masil.mate.domain.ParticipantStatus; +import team.silvertown.masil.mate.dto.request.CreateMateRequest; +import team.silvertown.masil.mate.dto.response.CreateMateResponse; +import team.silvertown.masil.mate.dto.response.MateDetailResponse; +import team.silvertown.masil.mate.dto.response.ParticipantResponse; +import team.silvertown.masil.mate.exception.MateErrorCode; +import team.silvertown.masil.mate.repository.mate.MateRepository; +import team.silvertown.masil.mate.repository.participant.MateParticipantRepository; +import team.silvertown.masil.post.domain.Post; +import team.silvertown.masil.post.repository.PostRepository; +import team.silvertown.masil.texture.MapTexture; +import team.silvertown.masil.texture.MateTexture; +import team.silvertown.masil.texture.PostTexture; +import team.silvertown.masil.texture.UserTexture; +import team.silvertown.masil.user.domain.User; +import team.silvertown.masil.user.repository.UserRepository; + +@SpringBootTest +@Transactional +@DisplayNameGeneration(ReplaceUnderscores.class) +class MateServiceTest { + + @Autowired + MateService mateService; + + @Autowired + UserRepository userRepository; + + @Autowired + PostRepository postRepository; + + @Autowired + MateRepository mateRepository; + + @Autowired + MateParticipantRepository mateParticipantRepository; + + User author; + Post post; + String title; + String content; + KakaoPoint gatheringPlacePoint; + String gatheringPlaceDetail; + OffsetDateTime gatherAt; + Integer capacity; + + @BeforeEach + void setUp() { + author = userRepository.save(UserTexture.createValidUser()); + // TODO: apply post texture + post = postRepository.save(PostTexture.createDependentPost(author, 100)); + title = MateTexture.getRandomSentenceWithMax(30); + content = MateTexture.getRandomSentenceWithMax(1000); + gatheringPlacePoint = MapTexture.createKakaoPoint(); + gatheringPlaceDetail = MateTexture.getRandomSentenceWithMax(50); + gatherAt = MateTexture.getFutureDateTime(); + capacity = MateTexture.getRandomInt(1, 10); + } + + @Test + void 메이트_모집_생성을_성공한다() { + // given + CreateMateRequest request = new CreateMateRequest(post.getId(), post.getDepth1(), + post.getDepth2(), + post.getDepth3(), "", title, content, gatheringPlacePoint, gatheringPlaceDetail, + gatherAt, capacity); + + // when + CreateMateResponse expected = mateService.create(author.getId(), request); + + // then + Mate actual = mateRepository.findAll() + .stream() + .findFirst() + .orElseThrow(); + MateParticipant actualParticipant = mateParticipantRepository.findAll() + .stream() + .findFirst() + .orElseThrow(); + + assertThat(expected.id()).isEqualTo(actual.getId()); + assertThat(actualParticipant) + .hasFieldOrPropertyWithValue("user", author) + .hasFieldOrPropertyWithValue("status", ParticipantStatus.ACCEPTED); + } + + @Test + void 로그인한_사용자를_확인할_수_없으면_메이트_생성을_실패한다() { + // given + long invalidId = MateTexture.getRandomId(); + CreateMateRequest request = new CreateMateRequest(post.getId(), post.getDepth1(), + post.getDepth2(), + post.getDepth3(), "", title, content, gatheringPlacePoint, gatheringPlaceDetail, + gatherAt, capacity); + + // when + ThrowingCallable create = () -> mateService.create(invalidId, request); + + // then + assertThatExceptionOfType(DataNotFoundException.class).isThrownBy(create) + .withMessage(MateErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + void 생성하려는_메이트_모집_시간의_2시간_이내에_참여하는_메이트가_있으면_생성을_실패한다() { + // given + User anotherUser = userRepository.save(UserTexture.createValidUser()); + Post anotherPost = postRepository.save(PostTexture.createDependentPost(anotherUser, 100)); + Mate anotherMate = mateRepository.save( + MateTexture.createDependentMate(anotherUser, anotherPost, gatherAt.plusMinutes(30))); + MateParticipant mateParticipant = MateTexture.createMateParticipant(author, anotherMate, + ParticipantStatus.ACCEPTED.name()); + + mateParticipantRepository.save(mateParticipant); + + CreateMateRequest request = new CreateMateRequest(post.getId(), post.getDepth1(), + post.getDepth2(), + post.getDepth3(), "", title, content, gatheringPlacePoint, gatheringPlaceDetail, + gatherAt, capacity); + + // when + ThrowingCallable create = () -> mateService.create(author.getId(), request); + + // then + assertThatExceptionOfType(BadRequestException.class).isThrownBy(create) + .withMessage(MateErrorCode.OVERGENERATION_IN_SIMILAR_TIME.getMessage()); + } + + @Test + void 메이트의_산책로_포스트가_존재하지_않으면_메이트_생성을_실패한다() { + // given + long invalidId = MateTexture.getRandomId(); + CreateMateRequest request = new CreateMateRequest(invalidId, post.getDepth1(), + post.getDepth2(), + post.getDepth3(), "", title, content, gatheringPlacePoint, gatheringPlaceDetail, + gatherAt, capacity); + + // when + ThrowingCallable create = () -> mateService.create(author.getId(), request); + + // then + assertThatExceptionOfType(DataNotFoundException.class).isThrownBy(create) + .withMessage(MateErrorCode.POST_NOT_FOUND.getMessage()); + } + + @Test + void 메이트_모집_상세_조회를_성공한다() { + // given + Mate expected = mateRepository.save(MateTexture.createDependentMate(author, post)); + MateParticipant savedAuthor = mateParticipantRepository.save( + MateTexture.createMateParticipant(author, expected, ParticipantStatus.ACCEPTED.name())); + + // when + MateDetailResponse actual = mateService.getDetailById(expected.getId()); + + // then + ParticipantResponse expectedAuthor = ParticipantResponse.from(savedAuthor); + + assertThat(actual) + .hasFieldOrPropertyWithValue("id", expected.getId()) + .hasFieldOrPropertyWithValue("participants", List.of(expectedAuthor)) + .hasFieldOrPropertyWithValue("authorId", author.getId()) + .hasFieldOrPropertyWithValue("authorNickname", author.getNickname()); + } + + @Test + void 메이트가_존재하지_않으면_메이트_상세_조회를_실패한다() { + // given + long invalidId = MateTexture.getRandomId(); + + // when + ThrowingCallable getDetailById = () -> mateService.getDetailById(invalidId); + + // then + assertThatExceptionOfType(DataNotFoundException.class).isThrownBy(getDetailById) + .withMessage(MateErrorCode.MATE_NOT_FOUND.getMessage()); + } + +} diff --git a/src/test/java/team/silvertown/masil/post/service/PostServiceTest.java b/src/test/java/team/silvertown/masil/post/service/PostServiceTest.java index 0e98a168..69ea544b 100644 --- a/src/test/java/team/silvertown/masil/post/service/PostServiceTest.java +++ b/src/test/java/team/silvertown/masil/post/service/PostServiceTest.java @@ -21,13 +21,14 @@ import org.springframework.transaction.annotation.Transactional; import team.silvertown.masil.common.exception.DataNotFoundException; import team.silvertown.masil.common.map.KakaoPoint; -import team.silvertown.masil.common.response.ScrollResponse; +import team.silvertown.masil.common.scroll.OrderType; +import team.silvertown.masil.common.scroll.dto.NormalListRequest; +import team.silvertown.masil.common.scroll.dto.ScrollRequest; +import team.silvertown.masil.common.scroll.dto.ScrollResponse; import team.silvertown.masil.post.domain.Post; import team.silvertown.masil.post.domain.PostPin; import team.silvertown.masil.post.dto.request.CreatePostPinRequest; import team.silvertown.masil.post.dto.request.CreatePostRequest; -import team.silvertown.masil.post.dto.request.NormalListRequest; -import team.silvertown.masil.post.dto.request.PostOrderType; import team.silvertown.masil.post.dto.response.CreatePostResponse; import team.silvertown.masil.post.dto.response.PostDetailResponse; import team.silvertown.masil.post.dto.response.SimplePostResponse; @@ -118,7 +119,7 @@ void setUp() { // then assertThatExceptionOfType(DataNotFoundException.class).isThrownBy(create) - .withMessage(PostErrorCode.USER_NOT_FOUND.getMessage()); + .withMessage(PostErrorCode.LOGIN_USER_NOT_FOUND.getMessage()); } @Test @@ -179,12 +180,12 @@ void setUp() { .depth1(addressDepth1) .depth2(addressDepth2) .depth3(addressDepth3) - .order(PostOrderType.LATEST) + .order(OrderType.LATEST.name()) .size(expectedSize) .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -205,13 +206,13 @@ void setUp() { .depth1(addressDepth1) .depth2(addressDepth2) .depth3(addressDepth3) - .order(PostOrderType.LATEST) + .order(OrderType.LATEST.name()) .cursor(idCursor) .size(expectedSize) .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -233,12 +234,12 @@ void setUp() { .depth1(addressDepth1) .depth2(addressDepth2) .depth3(addressDepth3) - .order(PostOrderType.MOST_POPULAR) + .order(OrderType.MOST_POPULAR.name()) .size(expectedSize) .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -262,13 +263,13 @@ void setUp() { .depth1(addressDepth1) .depth2(addressDepth2) .depth3(addressDepth3) - .order(PostOrderType.MOST_POPULAR) + .order(OrderType.MOST_POPULAR.name()) .cursor(cursor) .size(expectedSize) .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -289,12 +290,12 @@ void setUp() { .depth1(addressDepth1) .depth2(addressDepth2) .depth3(addressDepth3) - .order(PostOrderType.LATEST) + .order(OrderType.LATEST.name()) .size(expectedSize) .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -306,7 +307,7 @@ void setUp() { @Test void 조회_대상_산책로_포스트_수가_조회_사이즈보다_작으면_다음_커서가_없다() { // given - int actualSize = 5; + int actualSize = PostTexture.getRandomInt(1, 9); createPostsAndGetLastId(actualSize); @@ -315,12 +316,12 @@ void setUp() { .depth1(addressDepth1) .depth2(addressDepth2) .depth3(addressDepth3) - .order(PostOrderType.LATEST) + .order(OrderType.LATEST.name()) .size(expectedSize) .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -337,20 +338,22 @@ void setUp() { @ValueSource(strings = " ") void 커서가_빈_값이면_처음부터_목록_조회한다(String cursor) { // given - createPostsAndGetLastId(30); + int totalSize = PostTexture.getRandomInt(21, 99); + + createPostsAndGetLastId(totalSize); int expectedSize = 10; NormalListRequest request = NormalListRequest.builder() .depth1(addressDepth1) .depth2(addressDepth2) .depth3(addressDepth3) - .order(PostOrderType.LATEST) + .order(OrderType.LATEST.name()) .cursor(cursor) .size(expectedSize) .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -363,7 +366,9 @@ void setUp() { @Test void 정렬_순서가_없으면_산책로_포스트를_최신순으로_조회한다() { // given - createPostsAndGetLastId(30); + int totalSize = PostTexture.getRandomInt(21, 99); + + createPostsAndGetLastId(totalSize); int expectedSize = 10; NormalListRequest request = NormalListRequest.builder() @@ -374,7 +379,7 @@ void setUp() { .build(); // when - ScrollResponse actual = postService.getSliceByAddress(null, + ScrollResponse actual = postService.getScrollByAddress(null, request); // then @@ -384,6 +389,138 @@ void setUp() { assertThat(actual.nextCursor()).contains(expectedLastCursor); } + @Test + void 특정_사용자의_산책로_포스트_최신순_조회를_성공한다() { + // given + int totalSize = PostTexture.getRandomInt(21, 99); + + createPostsAndGetLastId(totalSize); + + int expectedSize = 10; + ScrollRequest request = new ScrollRequest(OrderType.LATEST.name(), null, expectedSize); + + // when + ScrollResponse actual = postService.getScrollByAuthor(null, + user.getId(), request); + + // then + String expectedLastCursor = getLastLatestCursor(expectedSize - 1); + + assertThat(actual.contents()).hasSize(expectedSize); + assertThat(actual.nextCursor()).isEqualTo(expectedLastCursor); + } + + @Test + void 다음_커서_기반으로_특정_사용자의_산책로_포스트_최신순_조회를_성공한다() { + // given + int totalSize = PostTexture.getRandomInt(21, 99); + long lastId = createPostsAndGetLastId(totalSize); + int expectedSize = 10; + String idCursor = String.valueOf(lastId - expectedSize + 1); + ScrollRequest request = new ScrollRequest(OrderType.LATEST.name(), idCursor, + expectedSize); + + // when + ScrollResponse actual = postService.getScrollByAuthor(null, + user.getId(), request); + + // then + String expectedLastCursor = getLastLatestCursor( + (int) (lastId - (Integer.parseInt(idCursor) - expectedSize))); + System.out.println(expectedLastCursor); + + assertThat(actual.contents()).hasSize(expectedSize); + assertThat(actual.nextCursor()).contains(expectedLastCursor); + } + + @Test + void 특정_사용자의_산책로_포스트_인기순_조회를_성공한다() { + // given + int totalSize = PostTexture.getRandomInt(21, 99); + + createPostsAndGetLastId(totalSize); + + int expectedSize = 10; + ScrollRequest request = new ScrollRequest(OrderType.MOST_POPULAR.name(), null, + expectedSize); + + // when + ScrollResponse actual = postService.getScrollByAuthor(null, + user.getId(), request); + + // then + Post actualLast = getLastMostPopularPost(expectedSize - 1); + String likeCount = String.valueOf(actualLast.getLikeCount()); + String id = String.valueOf(actualLast.getId()); + + assertThat(actual.contents()).hasSize(expectedSize); + assertThat(actual.nextCursor()).contains(likeCount, id); + } + + @Test + void 다음_커서_기반으로_특정_사용자의_산책로_포스트_인기순_조회를_성공한다() { + // given + int totalSize = PostTexture.getRandomInt(21, 99); + long lastId = createPostsAndGetLastId(totalSize); + int expectedSize = 10; + String idCursor = String.valueOf(lastId - expectedSize + 1); + String cursor = "0000000000000000".substring(0, 16 - idCursor.length()) + idCursor; + ScrollRequest request = new ScrollRequest(OrderType.MOST_POPULAR.name(), cursor, + expectedSize); + + // when + ScrollResponse actual = postService.getScrollByAuthor(null, + user.getId(), request); + + // then + Post actualLast = getLastMostPopularPost( + (int) (lastId - (Integer.parseInt(idCursor) - expectedSize))); + String likeCount = String.valueOf(actualLast.getLikeCount()); + String id = String.valueOf(actualLast.getId()); + + assertThat(actual.contents()).hasSize(expectedSize); + assertThat(actual.nextCursor()).contains(likeCount, id); + } + + @Test + void 로그인_사용자와_조회_대상_사용자가_같으면_공개_여부에_관계없이_조회한다() { + // given + int totalPublicPostSize = PostTexture.getRandomInt(1, 7); + + createPostsAndGetLastId(totalPublicPostSize); + postRepository.save(PostTexture.createPrivatePost(user)); + + int expectedSize = 10; + ScrollRequest request = new ScrollRequest(OrderType.LATEST.name(), null, + expectedSize); + + // when + ScrollResponse actual = postService.getScrollByAuthor(user.getId(), + user.getId(), request); + + // then + assertThat(actual.contents()).hasSize(totalPublicPostSize + 1); + } + + @Test + void 사용자를_확인할_수_없으면_특정_사용자_포스트_목록_조회를_실패한다() { + // given + int totalSize = PostTexture.getRandomInt(21, 99); + + createPostsAndGetLastId(totalSize); + + int expectedSize = 10; + ScrollRequest request = new ScrollRequest(OrderType.LATEST.name(), null, expectedSize); + long invalidId = PostTexture.getRandomId(); + + // when + ThrowingCallable getScroll = () -> postService.getScrollByAuthor(null, invalidId, request); + + // then + assertThatExceptionOfType(DataNotFoundException.class).isThrownBy(getScroll) + .withMessage(PostErrorCode.AUTHOR_NOT_FOUND.getMessage()); + } + long createPostsAndGetLastId(int size) { List posts = new ArrayList<>(); diff --git a/src/test/java/team/silvertown/masil/texture/MapTexture.java b/src/test/java/team/silvertown/masil/texture/MapTexture.java index adba3800..c47e58b1 100644 --- a/src/test/java/team/silvertown/masil/texture/MapTexture.java +++ b/src/test/java/team/silvertown/masil/texture/MapTexture.java @@ -39,12 +39,16 @@ public static LineString createLineString(int size) { } public static Point createPoint() { + KakaoPoint point = createKakaoPoint(); + + return KakaoPointMapper.mapToPoint(point); + } + + public static KakaoPoint createKakaoPoint() { Random random = new Random(); double latitude = random.nextDouble(-90, 90); double longitude = random.nextDouble(-180, 180); - KakaoPoint point = new KakaoPoint(latitude, longitude); - - return KakaoPointMapper.mapToPoint(point); + return new KakaoPoint(latitude, longitude); } } diff --git a/src/test/java/team/silvertown/masil/texture/MateTexture.java b/src/test/java/team/silvertown/masil/texture/MateTexture.java index 898f778a..b2dfdec2 100644 --- a/src/test/java/team/silvertown/masil/texture/MateTexture.java +++ b/src/test/java/team/silvertown/masil/texture/MateTexture.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import org.locationtech.jts.geom.Point; import team.silvertown.masil.mate.domain.Mate; +import team.silvertown.masil.mate.domain.MateParticipant; import team.silvertown.masil.post.domain.Post; import team.silvertown.masil.user.domain.User; @@ -35,6 +36,10 @@ public static OffsetDateTime getFutureDateTime() { } public static Mate createDependentMate(User user, Post post) { + return createDependentMate(user, post, getFutureDateTime()); + } + + public static Mate createDependentMate(User user, Post post, OffsetDateTime gatherAt) { String addressDepth1 = post.getDepth1(); String addressDepth2 = post.getDepth2(); String addressDepth3 = post.getDepth3(); @@ -43,7 +48,6 @@ public static Mate createDependentMate(User user, Post post) { String content = getRandomSentenceWithMax(10000); Point point = MapTexture.createPoint(); String detail = getRandomSentenceWithMax(50); - OffsetDateTime gatherAt = getFutureDateTime(); int capacity = getRandomInt(1, 10); return createMate(user, post, addressDepth1, addressDepth2, addressDepth3, addressDepth4, @@ -80,4 +84,16 @@ public static Mate createMate( .build(); } + public static MateParticipant createMateParticipant(User user, Mate mate, String status) { + String message = faker.harryPotter() + .quote(); + + return MateParticipant.builder() + .user(user) + .mate(mate) + .status(status) + .message(message) + .build(); + } + } diff --git a/src/test/java/team/silvertown/masil/texture/PostTexture.java b/src/test/java/team/silvertown/masil/texture/PostTexture.java index 4a6a597c..f768e25a 100644 --- a/src/test/java/team/silvertown/masil/texture/PostTexture.java +++ b/src/test/java/team/silvertown/masil/texture/PostTexture.java @@ -32,7 +32,19 @@ public static Post createPostWithAddress( int totalTime = getRandomInt(600, 4200); return createPost(user, depth1, depth2, depth3, "", path, title, null, null, - (int) path.getLength(), totalTime); + (int) path.getLength(), totalTime, null); + } + + public static Post createPrivatePost(User user) { + String addressDepth1 = MasilTexture.createAddressDepth1(); + String addressDepth2 = MasilTexture.createAddressDepth2(); + String addressDepth3 = MasilTexture.createAddressDepth3(); + LineString path = MapTexture.createLineString(100); + String title = getRandomSentenceWithMax(29); + int totalTime = getRandomInt(600, 4200); + + return createPost(user, addressDepth1, addressDepth2, addressDepth3, "", path, title, null, + null, (int) path.getLength(), totalTime, false); } public static Post createDependentPost(User user, int pathSize) { @@ -44,7 +56,7 @@ public static Post createDependentPost(User user, int pathSize) { int totalTime = getRandomInt(600, 4200); return createPost(user, addressDepth1, addressDepth2, addressDepth3, "", path, title, null, - null, (int) path.getLength(), totalTime); + null, (int) path.getLength(), totalTime, null); } public static Post createPostWithOptional(String content, String thumbnailUrl) { @@ -57,7 +69,7 @@ public static Post createPostWithOptional(String content, String thumbnailUrl) { int totalTime = getRandomInt(600, 4200); return createPost(user, addressDepth1, addressDepth2, addressDepth3, "", path, title, - content, thumbnailUrl, (int) path.getLength(), totalTime); + content, thumbnailUrl, (int) path.getLength(), totalTime, null); } public static Post createPost( @@ -71,7 +83,8 @@ public static Post createPost( String content, String thumbnailUrl, Integer distance, - Integer totalTime + Integer totalTime, + Boolean isPublic ) { return Post.builder() .user(user) @@ -85,6 +98,7 @@ public static Post createPost( .thumbnailUrl(thumbnailUrl) .distance(distance) .totalTime(totalTime) + .isPublic(isPublic) .build(); } diff --git a/src/test/java/team/silvertown/masil/texture/UserAuthorityTexture.java b/src/test/java/team/silvertown/masil/texture/UserAuthorityTexture.java new file mode 100644 index 00000000..f8de4469 --- /dev/null +++ b/src/test/java/team/silvertown/masil/texture/UserAuthorityTexture.java @@ -0,0 +1,26 @@ +package team.silvertown.masil.texture; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import team.silvertown.masil.user.domain.Authority; +import team.silvertown.masil.user.domain.User; +import team.silvertown.masil.user.domain.UserAuthority; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserAuthorityTexture { + + public static UserAuthority generateRestrictAuthority(User user) { + return UserAuthority.builder() + .authority(Authority.RESTRICTED) + .user(user) + .build(); + } + + public static UserAuthority generateNormalAuthority(User user) { + return UserAuthority.builder() + .authority(Authority.NORMAL) + .user(user) + .build(); + } + +} diff --git a/src/test/java/team/silvertown/masil/texture/UserTexture.java b/src/test/java/team/silvertown/masil/texture/UserTexture.java index 52246138..9053af76 100644 --- a/src/test/java/team/silvertown/masil/texture/UserTexture.java +++ b/src/test/java/team/silvertown/masil/texture/UserTexture.java @@ -24,6 +24,32 @@ public static User createValidUser() { String.valueOf(getRandomId())); } + public static User createWalkedUser() { + String nickname = faker.funnyName() + .name(); + LocalDate birthDate = faker.date() + .birthdayLocalDate(20, 40); + int height = getRandomInt(170, 190); + int weight = getRandomInt(70, 90); + + return createUser(nickname, Sex.MALE, birthDate, height, weight, + ExerciseIntensity.MIDDLE, 5, 3, 200, true, true, Provider.KAKAO, + String.valueOf(getRandomId())); + } + + public static User createPrivateUser() { + String nickname = faker.funnyName() + .name(); + LocalDate birthDate = faker.date() + .birthdayLocalDate(20, 40); + int height = getRandomInt(170, 190); + int weight = getRandomInt(70, 90); + + return createUser(nickname, Sex.MALE, birthDate, height, weight, + ExerciseIntensity.MIDDLE, 5, 3, 200, false, true, Provider.KAKAO, + String.valueOf(getRandomId())); + } + public static User createUser( String nickname, Sex sex, diff --git a/src/test/java/team/silvertown/masil/user/service/UserServiceTest.java b/src/test/java/team/silvertown/masil/user/service/UserServiceTest.java index 98d64bc8..a166c04d 100644 --- a/src/test/java/team/silvertown/masil/user/service/UserServiceTest.java +++ b/src/test/java/team/silvertown/masil/user/service/UserServiceTest.java @@ -17,28 +17,39 @@ import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.transaction.annotation.Transactional; +import team.silvertown.masil.common.exception.BadRequestException; import team.silvertown.masil.common.exception.DataNotFoundException; import team.silvertown.masil.common.exception.DuplicateResourceException; import team.silvertown.masil.config.jwt.JwtTokenProvider; +import team.silvertown.masil.image.exception.ImageErrorCode; import team.silvertown.masil.security.exception.InvalidAuthenticationException; +import team.silvertown.masil.texture.UserAuthorityTexture; +import team.silvertown.masil.texture.UserTexture; +import team.silvertown.masil.test.LocalstackTest; import team.silvertown.masil.user.domain.Authority; import team.silvertown.masil.user.domain.ExerciseIntensity; import team.silvertown.masil.user.domain.Provider; import team.silvertown.masil.user.domain.Sex; import team.silvertown.masil.user.domain.User; +import team.silvertown.masil.user.domain.UserAgreement; import team.silvertown.masil.user.domain.UserAuthority; import team.silvertown.masil.user.dto.LoginResponse; import team.silvertown.masil.user.dto.MeInfoResponse; +import team.silvertown.masil.user.dto.MyPageInfoResponse; import team.silvertown.masil.user.dto.NicknameCheckResponse; import team.silvertown.masil.user.dto.OAuthResponse; import team.silvertown.masil.user.dto.OnboardRequest; import team.silvertown.masil.user.dto.UpdateRequest; import team.silvertown.masil.user.exception.UserErrorCode; +import team.silvertown.masil.user.repository.UserAgreementRepository; import team.silvertown.masil.user.repository.UserAuthorityRepository; import team.silvertown.masil.user.repository.UserRepository; @@ -46,7 +57,7 @@ @DisplayNameGeneration(ReplaceUnderscores.class) @SpringBootTest @Transactional -class UserServiceTest { +class UserServiceTest extends LocalstackTest { private static final Faker faker = new Faker(); private static final String VALID_PROVIDER = "kakao"; @@ -69,6 +80,9 @@ class UserServiceTest { @Autowired UserAuthorityRepository userAuthorityRepository; + @Autowired + UserAgreementRepository userAgreementRepository; + @MockBean KakaoOAuthService kakaoOAuthService; @@ -197,7 +211,7 @@ void setup() { .getAuthority()).isEqualTo(Authority.RESTRICTED); //when - userService.onboard(unTypedUser.getId(), request); + userService.onboard(request, unTypedUser.getId()); //then User updatedUser = userRepository.findById(unTypedUser.getId()) @@ -221,6 +235,59 @@ void setup() { .map(UserAuthority::getAuthority) .collect(Collectors.toList())) .contains(Authority.NORMAL); + + UserAgreement byUser = userAgreementRepository.findByUser(updatedUser) + .get(); + assertThat(byUser.getIsLocationInfoConsented()).isTrue(); + assertThat(byUser.getIsPersonalInfoConsented()).isTrue(); + assertThat(byUser.getIsUnderAgeConsentConfirmed()).isTrue(); + } + + @Test + public void 성별과_운동강도를_입력하지_않은경우에도_정상적으로_업데이트된다() throws Exception { + //given + OnboardRequest noSexAndExerciseIntensity = new OnboardRequest( + "nickname", + null, + format.format(faker.date() + .birthdayLocalDate(20, 40)), + getRandomInt(170, 190), + getRandomInt(70, 90), + null, + true, + true, + true, + true + ); + + //when + userService.onboard(noSexAndExerciseIntensity, unTypedUser.getId()); + User updatedUser = userRepository.findById(unTypedUser.getId()) + .get(); + + //then + assertThat(updatedUser.getSex()).isNull(); + assertThat(updatedUser.getExerciseIntensity()).isNull(); + } + + @Test + public void 이미_사용중인_닉네임이_라면_예외가_발생한다() throws Exception { + //given + User user = User.builder() + .nickname("nickname") + .build(); + userRepository.save(user); + OnboardRequest request = getNormalRequest(); + List beforeUpdatedAuthority = userAuthorityRepository.findByUser( + unTypedUser); + assertThat(beforeUpdatedAuthority).hasSize(1); + assertThat(beforeUpdatedAuthority.get(0) + .getAuthority()).isEqualTo(Authority.RESTRICTED); + + //when, then + assertThatThrownBy(() -> userService.onboard(request, unTypedUser.getId())) + .isInstanceOf(DuplicateResourceException.class) + .hasMessage(UserErrorCode.DUPLICATED_NICKNAME.getMessage()); } } @@ -274,7 +341,7 @@ void setup() { .getAuthority()).isEqualTo(Authority.RESTRICTED); //when - userService.onboard(unTypedUser.getId(), request); + userService.onboard(request, unTypedUser.getId()); //then User updatedUser = userRepository.findById(unTypedUser.getId()) @@ -423,4 +490,163 @@ void setup() { } + @Nested + class 유저의_my_page_정보를_정상적으로_가져온다 { + + private User user; + private User privateUser; + + @BeforeEach + public void setUp() { + user = UserTexture.createWalkedUser(); + userRepository.save(user); + UserAuthority userAuthority = UserAuthorityTexture.generateRestrictAuthority(user); + userAuthorityRepository.save(userAuthority); + privateUser = UserTexture.createPrivateUser(); + userRepository.save(privateUser); + UserAuthority priavteUserAuthority = UserAuthorityTexture.generateRestrictAuthority( + privateUser); + userAuthorityRepository.save(priavteUserAuthority); + } + + @Test + public void 해당_userId를_가진_user의_정보를_정확히_가져온다() throws Exception { + //given + User walkedUser = userRepository.findById(user.getId()) + .get(); + + //when + MyPageInfoResponse myPageInfo = userService.getMyPageInfo(walkedUser.getId(), null); + + //then + assertThat(myPageInfo) + .hasFieldOrPropertyWithValue("nickname", myPageInfo.nickname()) + .hasFieldOrPropertyWithValue("profileImg", myPageInfo.profileImg()) + .hasFieldOrPropertyWithValue("totalDistance", myPageInfo.totalDistance()) + .hasFieldOrPropertyWithValue("totalCount", myPageInfo.totalCount()) + .hasFieldOrPropertyWithValue("totalCalories", myPageInfo.totalCalories()); + } + + @Test + public void 존재하지_않는_유저의_마이페이지를_호출할_경우_예외가_발생한다() throws Exception { + //given, when, then + assertThatThrownBy(() -> userService.getMyPageInfo(user.getId() + 2, null)) + .isInstanceOf(DataNotFoundException.class) + .hasMessage(UserErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + public void 다른_사람이_계정_비공개를_한_경우_마실_기록은_볼_수_없다() throws Exception { + //given + User walkedUser = userRepository.findById(privateUser.getId()) + .get(); + + //when + MyPageInfoResponse myPageInfo = userService.getMyPageInfo(walkedUser.getId(), null); + + //then + assertThat(myPageInfo) + .hasFieldOrPropertyWithValue("nickname", myPageInfo.nickname()) + .hasFieldOrPropertyWithValue("profileImg", myPageInfo.profileImg()) + .hasFieldOrPropertyWithValue("totalDistance", null) + .hasFieldOrPropertyWithValue("totalCount", null) + .hasFieldOrPropertyWithValue("totalCalories", null); + } + + @Test + public void 내가_로그인_하고_내_정보를_받아보려고_할때는_계정이_비공계더라도_정보를_볼_수_있다() throws Exception { + //given, when + MyPageInfoResponse myPageInfo = userService.getMyPageInfo(user.getId(), user.getId()); + + //then + assertThat(myPageInfo) + .hasFieldOrPropertyWithValue("nickname", myPageInfo.nickname()) + .hasFieldOrPropertyWithValue("profileImg", myPageInfo.profileImg()) + .hasFieldOrPropertyWithValue("totalDistance", myPageInfo.totalDistance()) + .hasFieldOrPropertyWithValue("totalCount", myPageInfo.totalCount()) + .hasFieldOrPropertyWithValue("totalCalories", myPageInfo.totalCalories()); + } + + } + + @Nested + class 유저_프로필_업데이트_테스트 { + + private User user; + + @BeforeEach + public void setting() { + user = UserTexture.createValidUser(); + userRepository.save(user); + } + + @ParameterizedTest + @ValueSource( + strings = { + "image/apng", "image/avif", "image/gif", "image/jpeg", "image/png", "image/svg+xml", + "image/webp" + } + ) + public void 정상적으로_프로필을_업데이트한다(String validImageType) throws Exception { + //given + String filename = "valid file"; + String originalFilename = filename + ".jpeg"; + byte[] content = "content".getBytes(); + + MockMultipartFile file = new MockMultipartFile(filename, originalFilename, + validImageType, + content); + User savedUser = userRepository.findById(user.getId()) + .get(); + + //when + userService.updateProfile(file, savedUser.getId()); + + //then + String profileImg = user.getProfileImg(); + assertThat(profileImg).isNotBlank(); + } + + @Test + public void 프로필_사진을_보내지_않는_경우_null로_프로필을_업데이트한다() throws Exception { + //given + MockMultipartFile file = null; + User savedUser = userRepository.findById(user.getId()) + .get(); + + //when + userService.updateProfile(file, savedUser.getId()); + + //then + String profileImg = user.getProfileImg(); + assertThat(profileImg).isNull(); + } + + @ParameterizedTest + @ValueSource( + strings = { + "text/html; charset=utf-8", "application/javascript", "text/javascript", + "application/ecmascript", "text/ecmascript", " ", "" + } + ) + public void 비정상적인_확장자인_경우_예외가_발생한다(String invalidImageType) throws Exception { + //given + String filename = "valid file"; + String originalFilename = filename + ".jpeg"; + byte[] content = "content".getBytes(); + + MockMultipartFile file = new MockMultipartFile(filename, originalFilename, + invalidImageType, + content); + User savedUser = userRepository.findById(user.getId()) + .get(); + + //when, then + assertThatThrownBy(() -> userService.updateProfile(file, savedUser.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage(ImageErrorCode.NOT_SUPPORTED_CONTENT.getMessage()); + } + + } + }